Watch
0
0
Fork
You've already forked hyperhive
0

swarmctl shares the create-agent, inference and permissions shapes with the controller

This commit is contained in:
atlas 2026-10-19 17:57:21 +02:00
commit 0831b85b73

View file

@ -1,6 +1,11 @@
//! `POST /api/agents` and `POST /api/agents/{name}/forge-token`.
use hive_types::Permission;
use serde::{Deserialize, Serialize};
use swarm_wire::agent_archetype::ArchetypeRef;
use crate::inference::InferenceSettings;
use crate::limits::Caps;
/// Where the queued job landed — a caller polls `/api/jobq/graph` (or
/// `?states=`) with this id to watch it settle, same as every other job
@ -33,9 +38,119 @@ pub struct MintAgentForgeTokenResponse {
pub node_id: u64,
}
/// Body of `POST /api/agents` — the agent name to create, and the hive the
/// creation is aimed at. The repo name inside `forge::CONFIG_ORG` is the
/// same string as `name`: one repo per agent, named after it, same
/// convention `hive-c0re::forge` already uses for its own single-hive
/// `CreateRepo` path.
///
/// `hive` is an **address**, not an attribute of the agent: it is where a
/// deploy message goes over the queue. Nothing here writes it into the
/// agent's config — a config naming its own hive would be a second
/// statement of where the agent lives, free to drift from the queue that
/// actually delivers to it.
///
/// It is required rather than optional because it is only knowable here,
/// at creation, from the operator making the choice. An optional field
/// would have been the friendlier migration and would have left every
/// creation in the state this endpoint exists to avoid — one nothing can
/// address.
///
/// Validated here and carried no further: none of the nodes this endpoint
/// queues talks to a hive, so none of them needs the address. It reaches
/// its consumer when the node that *sends* a deploy message exists, and
/// that node takes it from this field. Settling the request shape now is
/// the point — it is the breaking half, and doing it once is cheaper for
/// every caller than doing it again later.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct CreateAgentRequest {
pub name: String,
pub hive: String,
/// CPU and memory caps to set for the agent, as `PUT
/// /api/agents/{name}/limits` takes them. Absent leaves any caps already
/// stored for the name alone.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limits: Option<Caps>,
/// Inference settings to set for the agent, as `PUT
/// /api/agents/{name}/inference` takes them. Absent as `limits`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inference: Option<InferenceSettings>,
/// The agent's permissions, stored before the job is queued. Left out, the
/// agent holds the default set.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schema", schema(value_type = Option<Vec<String>>))]
pub permissions: Option<Vec<Permission>>,
/// The archetype to build the agent from, one `GET /api/archetypes`
/// lists. The agent then gets no `agent-configs/<name>` repo: its hive
/// builds it from the archetype's module. Absent is a custom agent.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(
feature = "schema",
schema(value_type = Option<Object>, example = json!({"source": "hyperhive", "name": "reviewer"}))
)]
pub archetype: Option<ArchetypeRef>,
/// The text the agent's `AGENTS.md` starts with, at most
/// `swarm_queue_client::agents_md_initial::MAX_BYTES` bytes. The agent's
/// harness writes it on its first start, and only when the agent has no
/// `AGENTS.md` yet: an existing file is never replaced. Absent leaves the
/// file to the harness.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agents_md: Option<String>,
}
#[cfg(test)]
mod tests {
use super::{CreateAgentResponse, MintAgentForgeTokenResponse};
use super::{CreateAgentRequest, CreateAgentResponse, MintAgentForgeTokenResponse};
use crate::{inference::InferenceSettings, limits::Caps};
use hive_types::Permission;
use swarm_wire::{agent_archetype::ArchetypeRef, agent_inference::Effort};
/// `swarmctl` sends only the keys it was given; the controller reads every
/// field.
#[test]
fn a_create_request_round_trips() {
let minimal = CreateAgentRequest {
name: "iris".to_owned(),
hive: "alpha".to_owned(),
limits: None,
inference: None,
permissions: None,
archetype: None,
agents_md: None,
};
let full = CreateAgentRequest {
limits: Some(Caps {
cpu_quota: Some("200%".to_owned()),
memory_max: None,
}),
inference: Some(InferenceSettings {
effort: Some(Effort::High),
..InferenceSettings::default()
}),
permissions: Some(vec![Permission::Messaging]),
archetype: Some(ArchetypeRef {
source: "hyperhive".to_owned(),
name: "reviewer".to_owned(),
}),
agents_md: Some("# iris".to_owned()),
..minimal.clone()
};
for (req, wire) in [
(minimal, r#"{"name":"iris","hive":"alpha"}"#),
(
full,
r##"{"name":"iris","hive":"alpha","limits":{"cpu_quota":"200%"},"inference":{"effort":"high"},"permissions":["messaging"],"archetype":{"source":"hyperhive","name":"reviewer"},"agents_md":"# iris"}"##,
),
] {
assert_eq!(serde_json::to_string(&req).unwrap(), wire);
assert_eq!(
serde_json::from_str::<CreateAgentRequest>(wire).unwrap(),
req
);
}
assert!(serde_json::from_str::<CreateAgentRequest>(r#"{"name":"iris"}"#).is_err());
}
/// No warnings means no `warnings` key, and a body without one still
/// decodes: that is the normal response.