//! `/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, /// One of `low`, `medium`, `high`, `xhigh`, `max`. #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "schema", schema(value_type = Option))] pub effort: Option, /// The models the agent may start its subagents on. At least one. #[serde(default, skip_serializing_if = "Option::is_none")] pub subagent_models: Option>, /// 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, } /// 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, /// 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, } /// 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::(wire).unwrap(), value ); } assert!(serde_json::from_str::(r#"{"effort":"extreme"}"#).is_err()); assert!(serde_json::from_str::(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}"# ); } }