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)]
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            model_settings: ModelSettings::default(),
54            context_window: None,
55            prompts,
56            provider_connections: ProviderConnectionOverrides::default(),
57            mcp: McpConfig::default(),
58            exposure: AgentSpecExposure::none(),
59            tools: ToolFilter::default(),
60        }
61    }
62}
63
64/// Defines how an agent can be invoked.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
66pub struct AgentSpecExposure {
67    /// Whether this agent can be invoked by users (e.g., as an ACP mode).
68    pub user_invocable: bool,
69    /// Whether this agent can be invoked by other agents (e.g., as a sub-agent).
70    pub agent_invocable: bool,
71}
72
73impl AgentSpecExposure {
74    /// Create an exposure that is neither user nor agent invocable.
75    ///
76    /// Used internally for synthesized default specs (e.g., no-mode sessions).
77    /// Not intended for authored agent definitions — all authored agents must
78    /// have at least one invocation surface.
79    pub fn none() -> Self {
80        Self { user_invocable: false, agent_invocable: false }
81    }
82
83    /// Create an exposure that is only user invocable.
84    pub fn user_only() -> Self {
85        Self { user_invocable: true, agent_invocable: false }
86    }
87
88    /// Create an exposure that is only agent invocable.
89    pub fn agent_only() -> Self {
90        Self { user_invocable: false, agent_invocable: true }
91    }
92
93    /// Create an exposure that is both user and agent invocable.
94    pub fn both() -> Self {
95        Self { user_invocable: true, agent_invocable: true }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use std::path::PathBuf;
103
104    #[test]
105    fn default_spec_has_expected_fields() {
106        let model: LlmModel = "anthropic:claude-sonnet-4-5".parse().unwrap();
107        let prompts = vec![Prompt::file(PathBuf::from("/tmp/BASE.md"), PathBuf::from("/tmp"))];
108        let spec = AgentSpec::bare(&model, None, prompts.clone());
109
110        assert_eq!(spec.name, "__default__");
111        assert_eq!(spec.description, "Default agent");
112        assert_eq!(spec.model, model.to_string());
113        assert!(spec.reasoning_effort.is_none());
114        assert_eq!(spec.prompts.len(), 1);
115        assert!(spec.mcp.is_empty());
116        assert_eq!(spec.exposure, AgentSpecExposure::none());
117    }
118}