Skip to main content

aether_core/core/
agent_deps.rs

1use crate::core::AgentRegistry;
2use crate::events::{AgentObserver, DynObserverFactory, TraceContext};
3use aether_auth::OAuthCredentialStorage;
4use rmcp::model::ClientCapabilities;
5use std::sync::Arc;
6
7/// Cross-cutting dependencies threaded to every agent a run spawns — the root
8/// agent and any sub-agents created by in-memory MCP servers. Bundling them
9/// keeps the plumbing through builders and servers a single value.
10#[derive(Clone, Default)]
11pub struct AgentDeps {
12    pub oauth_credential_store: Option<Arc<dyn OAuthCredentialStorage>>,
13    pub observer_factory: Option<DynObserverFactory>,
14    /// Remote trace these agents continue, set by whoever handled the request
15    /// that spawned them.
16    pub parent_trace_context: Option<TraceContext>,
17    pub agent_registry: AgentRegistry,
18    pub mcp_client_capabilities: Option<ClientCapabilities>,
19    pub session_affinity_key: Option<String>,
20}
21
22impl AgentDeps {
23    pub fn new(
24        oauth_credential_store: Arc<dyn OAuthCredentialStorage>,
25        observer_factory: Option<DynObserverFactory>,
26    ) -> Self {
27        Self { oauth_credential_store: Some(oauth_credential_store), observer_factory, ..Self::default() }
28    }
29
30    /// Continue `parent`'s trace in every agent built from these deps.
31    pub fn with_parent_trace_context(mut self, parent: Option<TraceContext>) -> Self {
32        self.parent_trace_context = parent;
33        self
34    }
35
36    pub fn with_agent_registry(mut self, registry: AgentRegistry) -> Self {
37        self.agent_registry = registry;
38        self
39    }
40
41    pub fn with_mcp_client_capabilities(mut self, capabilities: ClientCapabilities) -> Self {
42        self.mcp_client_capabilities = Some(capabilities);
43        self
44    }
45
46    pub fn with_session_affinity_key(mut self, key: impl Into<String>) -> Self {
47        self.session_affinity_key = Some(key.into());
48        self
49    }
50
51    pub fn supports_mcp_url_elicitation(&self) -> bool {
52        self.mcp_client_capabilities
53            .as_ref()
54            .and_then(|capabilities| capabilities.elicitation.as_ref())
55            .is_some_and(|elicitation| elicitation.url.is_some())
56    }
57
58    /// A fresh observer isolated to one agent, if a factory is configured.
59    pub fn observer(&self, agent_name: &str) -> Option<Box<dyn AgentObserver>> {
60        self.observer_factory
61            .as_ref()
62            .map(|factory| factory.agent(Some(agent_name), self.parent_trace_context.as_ref()))
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use rmcp::model::{ElicitationCapability, FormElicitationCapability, UrlElicitationCapability};
70
71    #[test]
72    fn mcp_url_elicitation_support_requires_advertised_url_capability() {
73        assert!(!AgentDeps::default().supports_mcp_url_elicitation());
74
75        let mut form_only = ClientCapabilities::default();
76        form_only.elicitation = Some(ElicitationCapability::new().with_form(FormElicitationCapability::new()));
77        assert!(!AgentDeps::default().with_mcp_client_capabilities(form_only).supports_mcp_url_elicitation());
78
79        let mut url = ClientCapabilities::default();
80        url.elicitation = Some(ElicitationCapability::new().with_url(UrlElicitationCapability::new()));
81        assert!(AgentDeps::default().with_mcp_client_capabilities(url).supports_mcp_url_elicitation());
82    }
83}