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}