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
117
swarm-controller-api/src/inference.rs
Normal file
117
swarm-controller-api/src/inference.rs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
//! `/api/agents/{name}/inference`, and the `inference` of `POST /api/agents`.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use swarm_wire::agent_inference::{Effort, Source};
|
||||
|
||||
/// The highest `max_subagents` a `PUT` may set. Far more concurrent claude
|
||||
/// sessions than one agent container has memory for, so a value above it is a
|
||||
/// typo rather than a cap.
|
||||
pub const MAX_SUBAGENTS: u32 = 32;
|
||||
|
||||
/// One agent's inference settings. An absent field is not set at swarm level,
|
||||
/// and the agent keeps its own value for it.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct InferenceSettings {
|
||||
/// The model the agent's turns run on, as claude's `--model` takes it.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
/// One of `low`, `medium`, `high`, `xhigh`, `max`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schema", schema(value_type = Option<String>))]
|
||||
pub effort: Option<Effort>,
|
||||
/// The models the agent may start its subagents on. At least one.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub subagent_models: Option<Vec<String>>,
|
||||
/// How many subagents the agent may have running at once, from 0 (no
|
||||
/// subagents) to [`MAX_SUBAGENTS`]. Absent is no cap.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_subagents: Option<u32>,
|
||||
}
|
||||
|
||||
/// What `GET` answers for one agent.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
|
||||
pub struct AgentInferenceView {
|
||||
/// The settings the swarm sets; `null` when it has none for the agent (no
|
||||
/// key, or a cleared one), `{}` for a stored setting with no fields.
|
||||
pub configured: Option<InferenceSettings>,
|
||||
/// What the agent reported running on in its latest turn-state header;
|
||||
/// `null` when this daemon has received none with these fields since it
|
||||
/// started.
|
||||
pub effective: Option<EffectiveInference>,
|
||||
}
|
||||
|
||||
/// The model and effort an agent runs on, and where each comes from.
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
|
||||
pub struct EffectiveInference {
|
||||
pub model: String,
|
||||
pub effort: String,
|
||||
/// `swarm`, `agent_nix`, `local_file` or `default`.
|
||||
#[cfg_attr(feature = "schema", schema(value_type = String))]
|
||||
pub model_source: Source,
|
||||
/// As `model_source`.
|
||||
#[cfg_attr(feature = "schema", schema(value_type = String))]
|
||||
pub effort_source: Source,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{AgentInferenceView, EffectiveInference, InferenceSettings};
|
||||
use swarm_wire::agent_inference::{Effort, Source};
|
||||
|
||||
#[test]
|
||||
fn settings_round_trip() {
|
||||
for (value, wire) in [
|
||||
(InferenceSettings::default(), "{}"),
|
||||
(
|
||||
InferenceSettings {
|
||||
model: Some("opus".to_owned()),
|
||||
effort: Some(Effort::Xhigh),
|
||||
subagent_models: Some(vec!["haiku".to_owned()]),
|
||||
max_subagents: Some(3),
|
||||
},
|
||||
r#"{"model":"opus","effort":"xhigh","subagent_models":["haiku"],"max_subagents":3}"#,
|
||||
),
|
||||
] {
|
||||
assert_eq!(serde_json::to_string(&value).unwrap(), wire);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<InferenceSettings>(wire).unwrap(),
|
||||
value
|
||||
);
|
||||
}
|
||||
assert!(serde_json::from_str::<InferenceSettings>(r#"{"effort":"extreme"}"#).is_err());
|
||||
assert!(serde_json::from_str::<InferenceSettings>(r#"{"modle":"x"}"#).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_view_round_trips() {
|
||||
let wire = r#"{"configured":{"model":"opus"},"effective":{"model":"opus","effort":"medium","model_source":"swarm","effort_source":"local_file"}}"#;
|
||||
let view = AgentInferenceView {
|
||||
configured: Some(InferenceSettings {
|
||||
model: Some("opus".to_owned()),
|
||||
..InferenceSettings::default()
|
||||
}),
|
||||
effective: Some(EffectiveInference {
|
||||
model: "opus".to_owned(),
|
||||
effort: "medium".to_owned(),
|
||||
model_source: Source::Swarm,
|
||||
effort_source: Source::LocalFile,
|
||||
}),
|
||||
};
|
||||
assert_eq!(serde_json::to_string(&view).unwrap(), wire);
|
||||
let back: AgentInferenceView = serde_json::from_str(wire).unwrap();
|
||||
assert_eq!(back.configured, view.configured);
|
||||
assert_eq!(back.effective, view.effective);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&AgentInferenceView {
|
||||
configured: None,
|
||||
effective: None
|
||||
})
|
||||
.unwrap(),
|
||||
r#"{"configured":null,"effective":null}"#
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue