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}