swarmctl shares the create-agent, inference and permissions shapes with the controller
This commit is contained in:
parent
249aab6642
commit
0831b85b73
26 changed files with 675 additions and 434 deletions
|
|
@ -9,7 +9,7 @@ use axum::{
|
|||
extract::{Path, State},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use swarm_controller_api::agents::CreateAgentResponse;
|
||||
use swarm_controller_api::agents::{CreateAgentRequest, CreateAgentResponse};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::jobq::{SwarmNodeKind, SwarmResourceKind, declare_agent_job};
|
||||
|
|
@ -467,63 +467,6 @@ pub(crate) async fn get_agents_status(
|
|||
}
|
||||
}
|
||||
|
||||
/// 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, Deserialize, ToSchema)]
|
||||
pub(crate) struct CreateAgentRequest {
|
||||
pub(crate) name: String,
|
||||
pub(crate) 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)]
|
||||
pub(crate) limits: Option<agent_limits::Caps>,
|
||||
/// Inference settings to set for the agent, as `PUT
|
||||
/// /api/agents/{name}/inference` takes them. Absent as `limits`.
|
||||
#[serde(default)]
|
||||
pub(crate) inference: Option<inference::InferenceSettings>,
|
||||
/// The agent's permissions, stored before the job is queued. Left out, the
|
||||
/// agent holds the default set.
|
||||
#[serde(default)]
|
||||
#[schema(value_type = Option<Vec<String>>)]
|
||||
pub(crate) permissions: Option<Vec<hive_types::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)]
|
||||
#[schema(value_type = Option<Object>, example = json!({"source": "hyperhive", "name": "reviewer"}))]
|
||||
pub(crate) archetype: Option<swarm_queue_client::agent_archetype::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)]
|
||||
pub(crate) agents_md: Option<String>,
|
||||
}
|
||||
|
||||
/// Queue the whole agent-creation job graph for `name` — two independent
|
||||
/// roots, `CreateIdentity` and `CreateForgeUser`, since an authelia
|
||||
/// subject and a forge account need nothing from each other. `CreateRepo`
|
||||
|
|
@ -739,10 +682,8 @@ impl CreateSettings {
|
|||
}
|
||||
}
|
||||
Ok(Self {
|
||||
limits: limits.map(agent_limits::Caps::into_stored).transpose()?,
|
||||
inference: inference
|
||||
.map(inference::InferenceSettings::into_stored)
|
||||
.transpose()?,
|
||||
limits: limits.as_ref().map(agent_limits::into_stored).transpose()?,
|
||||
inference: inference.map(inference::into_stored).transpose()?,
|
||||
permissions: permissions.map(agent_permissions::at_create).transpose()?,
|
||||
archetype,
|
||||
agents_md,
|
||||
|
|
@ -1256,7 +1197,7 @@ mod tests {
|
|||
|
||||
let err = super::create_agent(
|
||||
axum::extract::State(state),
|
||||
axum::Json(super::CreateAgentRequest {
|
||||
axum::Json(CreateAgentRequest {
|
||||
name: "atlas".to_owned(),
|
||||
hive: "pr1maa".to_owned(),
|
||||
limits: None,
|
||||
|
|
@ -1299,7 +1240,7 @@ mod tests {
|
|||
// accept arm is actually for is the queue count below.
|
||||
let _queued = super::create_agent(
|
||||
axum::extract::State(state),
|
||||
axum::Json(super::CreateAgentRequest {
|
||||
axum::Json(CreateAgentRequest {
|
||||
name: "atlas".to_owned(),
|
||||
hive: "pr1ma".to_owned(),
|
||||
limits: None,
|
||||
|
|
@ -1325,18 +1266,18 @@ mod tests {
|
|||
/// existed, and each field is checked by the same rules as its `PUT`.
|
||||
#[test]
|
||||
fn a_creation_request_takes_optional_settings() {
|
||||
let plain: super::CreateAgentRequest =
|
||||
let plain: CreateAgentRequest =
|
||||
serde_json::from_str(r#"{"name":"atlas","hive":"pr1ma"}"#).expect("plain body");
|
||||
assert!(plain.limits.is_none() && plain.inference.is_none());
|
||||
|
||||
let full: super::CreateAgentRequest = serde_json::from_str(
|
||||
let full: CreateAgentRequest = serde_json::from_str(
|
||||
r#"{"name":"atlas","hive":"pr1ma","limits":{"cpu_quota":"400%"},
|
||||
"inference":{"model":"opus","effort":"high"}}"#,
|
||||
)
|
||||
.expect("body with settings");
|
||||
assert!(full.limits.is_some() && full.inference.is_some());
|
||||
|
||||
let typo = serde_json::from_str::<super::CreateAgentRequest>(
|
||||
let typo = serde_json::from_str::<CreateAgentRequest>(
|
||||
r#"{"name":"atlas","hive":"pr1ma","limits":{"cpu":"400%"}}"#,
|
||||
);
|
||||
assert!(typo.is_err(), "an unknown cap field must be refused");
|
||||
|
|
@ -1355,7 +1296,7 @@ mod tests {
|
|||
for (value, field) in bad {
|
||||
let (state, sched) = state_with_roster();
|
||||
let body = format!(r#"{{"name":"atlas","hive":"pr1ma","{field}":{value}}}"#);
|
||||
let req: super::CreateAgentRequest = serde_json::from_str(&body).expect("body parses");
|
||||
let req: CreateAgentRequest = serde_json::from_str(&body).expect("body parses");
|
||||
let err = super::create_agent(axum::extract::State(state), axum::Json(req))
|
||||
.await
|
||||
.expect_err("a bad setting must be refused");
|
||||
|
|
@ -1373,7 +1314,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn settings_that_cannot_be_stored_refuse_the_creation() {
|
||||
let (state, sched) = state_with_roster();
|
||||
let req: super::CreateAgentRequest = serde_json::from_str(
|
||||
let req: CreateAgentRequest = serde_json::from_str(
|
||||
r#"{"name":"atlas","hive":"pr1ma","limits":{"cpu_quota":"400%"}}"#,
|
||||
)
|
||||
.expect("body parses");
|
||||
|
|
@ -1394,7 +1335,7 @@ mod tests {
|
|||
let nats = crate::test_nats::TestNats::start();
|
||||
let client = nats.client().await;
|
||||
let state = crate::agent_limits::tests::state(&client);
|
||||
let req: super::CreateAgentRequest = serde_json::from_str(
|
||||
let req: CreateAgentRequest = serde_json::from_str(
|
||||
r#"{"name":"atlas","hive":"pr1ma","limits":{"cpu_quota":"400%"},
|
||||
"inference":{"model":"opus","max_subagents":3}}"#,
|
||||
)
|
||||
|
|
@ -1449,16 +1390,18 @@ mod tests {
|
|||
.await
|
||||
.expect("a bucket that refuses any value");
|
||||
let caps = |json: &str| {
|
||||
serde_json::from_str::<crate::agent_limits::Caps>(json)
|
||||
.expect("caps")
|
||||
.into_stored()
|
||||
.expect("valid")
|
||||
crate::agent_limits::into_stored(
|
||||
&serde_json::from_str::<swarm_controller_api::limits::Caps>(json).expect("caps"),
|
||||
)
|
||||
.expect("valid")
|
||||
};
|
||||
let inference =
|
||||
serde_json::from_str::<crate::inference::InferenceSettings>(r#"{"model":"opus"}"#)
|
||||
.expect("settings")
|
||||
.into_stored()
|
||||
.expect("valid");
|
||||
let inference = crate::inference::into_stored(
|
||||
serde_json::from_str::<swarm_controller_api::inference::InferenceSettings>(
|
||||
r#"{"model":"opus"}"#,
|
||||
)
|
||||
.expect("settings"),
|
||||
)
|
||||
.expect("valid");
|
||||
crate::agent_limits::store(&client, "atlas", &caps(r#"{"cpu_quota":"100%"}"#))
|
||||
.await
|
||||
.expect("an earlier cap");
|
||||
|
|
@ -1515,7 +1458,7 @@ mod tests {
|
|||
.expect("an earlier grant");
|
||||
|
||||
for agent in ["atlas", "iris"] {
|
||||
let req: super::CreateAgentRequest = serde_json::from_str(&format!(
|
||||
let req: CreateAgentRequest = serde_json::from_str(&format!(
|
||||
r#"{{"name":"{agent}","hive":"pr1ma","permissions":["web_tools"],
|
||||
"inference":{{"model":"opus"}}}}"#
|
||||
))
|
||||
|
|
@ -1560,7 +1503,7 @@ mod tests {
|
|||
let nats = crate::test_nats::TestNats::start();
|
||||
let client = nats.client().await;
|
||||
let state = crate::agent_limits::tests::state(&client);
|
||||
let req: super::CreateAgentRequest = serde_json::from_str(
|
||||
let req: CreateAgentRequest = serde_json::from_str(
|
||||
r#"{"name":"atlas","hive":"pr1ma","limits":{"cpu_quota":"400%"},
|
||||
"inference":{"model":"-opus"}}"#,
|
||||
)
|
||||
|
|
@ -1602,7 +1545,7 @@ mod tests {
|
|||
) -> Result<(), problem_details::ProblemDetails> {
|
||||
super::create_agent(
|
||||
axum::extract::State(state.clone()),
|
||||
axum::Json(super::CreateAgentRequest {
|
||||
axum::Json(CreateAgentRequest {
|
||||
name: name.to_owned(),
|
||||
hive: hive.to_owned(),
|
||||
limits: None,
|
||||
|
|
@ -2243,7 +2186,7 @@ mod tests {
|
|||
let client = nats.client().await;
|
||||
seed_archetypes(&client).await;
|
||||
let state = crate::agent_limits::tests::state(&client);
|
||||
let req: super::CreateAgentRequest = serde_json::from_str(
|
||||
let req: CreateAgentRequest = serde_json::from_str(
|
||||
r#"{"name":"atlas","hive":"pr1ma",
|
||||
"archetype":{"source":"hyperhive","name":"reviewer"}}"#,
|
||||
)
|
||||
|
|
@ -2285,7 +2228,7 @@ mod tests {
|
|||
r#"{"source":"elsewhere","name":"reviewer"}"#,
|
||||
r#"{"source":"hyperhive","name":"a.b"}"#,
|
||||
] {
|
||||
let req: super::CreateAgentRequest = serde_json::from_str(&format!(
|
||||
let req: CreateAgentRequest = serde_json::from_str(&format!(
|
||||
r#"{{"name":"atlas","hive":"pr1ma","permissions":["messaging"],
|
||||
"archetype":{archetype}}}"#
|
||||
))
|
||||
|
|
|
|||
Loading…
Reference in a new issue