Skip to main content

aether_core/
agent_spec.rs

1//! Agent specification types for authored agent definitions.
2//!
3//! `AgentSpec` is the canonical abstraction for authored agent definitions across the stack.
4//! It represents a resolved runtime type, not a raw settings DTO.
5
6use crate::core::Prompt;
7use crate::mcp::{McpConfig, ToolFilter};
8use llm::{LlmModel, ModelSettings, ProviderConnectionOverrides, ReasoningEffort};
9
10/// A resolved agent specification ready for runtime use.
11///
12/// This type is produced by validating and resolving authored agent configuration.
13/// All validation happens before constructing these runtime types.
14#[derive(Debug, Clone, Default)]
15pub struct AgentSpec {
16    /// The canonical lookup key for this agent.
17    pub name: String,
18    /// Human-readable description of this agent's purpose.
19    pub description: String,
20    /// The validated model spec to use for this agent.
21    ///
22    /// This is stored as a canonical string so authored settings can represent
23    /// both single models (`provider:model`) and alloy specs
24    /// (`provider1:model1,provider2:model2`).
25    pub model: String,
26    /// Optional reasoning effort level for models that support it.
27    pub reasoning_effort: Option<ReasoningEffort>,
28    /// Sampling controls applied to this agent's model calls.
29    pub model_settings: ModelSettings,
30    /// Effective context window in tokens for this agent.
31    pub context_window: Option<u32>,
32    /// The prompt stack for this agent.
33    pub prompts: Vec<Prompt>,
34    /// Provider connection overrides keyed by model provider name.
35    pub provider_connections: ProviderConnectionOverrides,
36    /// MCP servers this agent connects to
37    pub mcp: McpConfig,
38    /// How this agent can be invoked.
39    pub exposure: AgentSpecExposure,
40    /// Tool filter for restricting which MCP tools this agent can use.
41    pub tools: ToolFilter,
42}
43
44impl AgentSpec {
45    /// Create a bare no-mode spec without catalog defaults or runtime policy.
46    /// Production callers should prefer their catalog's `default_spec` API.
47    pub fn bare(model: &LlmModel, reasoning_effort: Option<ReasoningEffort>, prompts: Vec<Prompt>) -> Self {
48        Self {
49            name: "__default__".to_string(),
50            description: "Default agent".to_string(),
51            model: model.to_string(),
52            reasoning_effort,
53            prompts,
54            ..Self::default()
55        }
56    }
57}
58
59/// Defines how an agent can be invoked.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
61pub struct AgentSpecExposure {
62    /// Whether this agent can be invoked by users (e.g., as an ACP mode).
63    pub user_invocable: bool,
64    /// Whether this agent can be invoked by other agents (e.g., as a sub-agent).
65    pub agent_invocable: bool,
66}
67
68impl AgentSpecExposure {
69    /// Create an exposure that is only user invocable.
70    pub fn user_only() -> Self {
71        Self { user_invocable: true, agent_invocable: false }
72    }
73
74    /// Create an exposure that is only agent invocable.
75    pub fn agent_only() -> Self {
76        Self { user_invocable: false, agent_invocable: true }
77    }
78
79    /// Create an exposure that is both user and agent invocable.
80    pub fn both() -> Self {
81        Self { user_invocable: true, agent_invocable: true }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use std::path::PathBuf;
89
90    #[test]
91    fn default_spec_has_expected_fields() {
92        let model: LlmModel = "anthropic:claude-sonnet-4-5".parse().unwrap();
93        let prompts = vec![Prompt::file(PathBuf::from("/tmp/BASE.md"), PathBuf::from("/tmp"))];
94        let spec = AgentSpec::bare(&model, None, prompts.clone());
95
96        assert_eq!(spec.name, "__default__");
97        assert_eq!(spec.description, "Default agent");
98        assert_eq!(spec.model, model.to_string());
99        assert!(spec.reasoning_effort.is_none());
100        assert_eq!(spec.prompts.len(), 1);
101        assert!(spec.mcp.is_empty());
102        assert_eq!(spec.exposure, AgentSpecExposure::default());
103    }
104}