swarmctl shares the create-agent, inference and permissions shapes with the controller
This commit is contained in:
parent
249aab6642
commit
0831b85b73
1 changed files with 675 additions and 434 deletions
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue