117 lines
4.8 KiB
Rust
117 lines
4.8 KiB
Rust
//! `/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}"#
|
|
);
|
|
}
|
|
}
|