Skip to main content

lc_a2a/
agent_adapter.rs

1//! P1-8: adapt a stateful [`AgentExecutor`] to the stateless [`BaseChain`] facade.
2//!
3//! A2A models "one task = one conversation", which needs multi-turn state. A
4//! `BaseChain` is stateless: every `invoke` is a fresh shot. An
5//! [`AgentExecutor`] — particularly one built with `.with_memory(...)` so that
6//! conversation history accumulates across turns — is the stateful counterpart.
7//!
8//! [`AgentExecutorChain`] bridges the two so an `A2AServer` can be backed
9//! directly by an agent via [`A2AServer::from_agent`](crate::A2AServer::from_agent), giving each A2A task
10//! genuine conversational continuity instead of a series of independent chain
11//! invocations.
12
13use std::collections::HashMap;
14use std::sync::Arc;
15
16use lc_agents::AgentExecutor;
17use lc_chains::base::{BaseChain, ChainError, ChainResult};
18use serde_json::Value;
19
20/// Wraps an [`AgentExecutor`] behind the [`BaseChain`] trait.
21///
22/// Inputs follow the agent convention: a single `input` string (or any string
23/// key the agent's planner reads). Output is produced under `output`.
24pub struct AgentExecutorChain {
25    executor: Arc<AgentExecutor>,
26}
27
28impl AgentExecutorChain {
29    /// Create an adapter around a ready-built executor.
30    ///
31    /// Attach memory *before* wrapping (e.g. `.with_memory(...)`) if multi-turn
32    /// state across A2A tasks is desired.
33    pub fn new(executor: Arc<AgentExecutor>) -> Self {
34        Self { executor }
35    }
36
37    /// The inner executor, for inspection or configuration.
38    pub fn inner(&self) -> &AgentExecutor {
39        &self.executor
40    }
41}
42
43#[async_trait::async_trait]
44impl BaseChain for AgentExecutorChain {
45    fn input_keys(&self) -> Vec<&str> {
46        vec!["input"]
47    }
48
49    fn output_keys(&self) -> Vec<&str> {
50        vec!["output"]
51    }
52
53    async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
54        let raw = inputs
55            .get("input")
56            .ok_or_else(|| ChainError::MissingInput("input".to_string()))?;
57        let input = raw
58            .as_str()
59            .ok_or_else(|| ChainError::InputError("input must be a string".to_string()))?
60            .to_string();
61
62        let output =
63            self.executor.invoke(input).await.map_err(|e| {
64                ChainError::ExecutionError(format!("Agent execution failed: {}", e))
65            })?;
66
67        let mut result = HashMap::new();
68        result.insert("output".to_string(), Value::String(output));
69        Ok(result)
70    }
71
72    fn name(&self) -> &str {
73        "agent-executor"
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use lc_agents::{AgentError, AgentFinish, AgentOutput, AgentStep, BaseAgent};
81    use serde_json::json;
82
83    /// A planner that echoes its `input` back verbatim.
84    struct EchoAgent;
85
86    #[async_trait::async_trait]
87    impl BaseAgent for EchoAgent {
88        async fn plan(
89            &self,
90            _intermediate_steps: &[AgentStep],
91            inputs: &HashMap<String, String>,
92        ) -> Result<AgentOutput, AgentError> {
93            let input = inputs.get("input").cloned().unwrap_or_default();
94            Ok(AgentOutput::Finish(AgentFinish::new(
95                format!("echo: {}", input),
96                String::new(),
97            )))
98        }
99    }
100
101    /// A planner that always fails, so agent errors can be observed.
102    struct FailAgent;
103
104    #[async_trait::async_trait]
105    impl BaseAgent for FailAgent {
106        async fn plan(
107            &self,
108            _intermediate_steps: &[AgentStep],
109            _inputs: &HashMap<String, String>,
110        ) -> Result<AgentOutput, AgentError> {
111            Err(AgentError::Other("boom".to_string()))
112        }
113    }
114
115    fn echo_chain() -> AgentExecutorChain {
116        let executor = AgentExecutor::new(Arc::new(EchoAgent), Vec::new());
117        AgentExecutorChain::new(Arc::new(executor))
118    }
119
120    #[tokio::test]
121    async fn invokes_agent_and_returns_output() {
122        let chain = echo_chain();
123        let mut inputs = HashMap::new();
124        inputs.insert("input".to_string(), json!("hello"));
125        let result = chain.invoke(inputs).await.unwrap();
126        assert_eq!(result.get("output"), Some(&json!("echo: hello")));
127    }
128
129    #[tokio::test]
130    async fn missing_input_returns_missing_input_error() {
131        let chain = echo_chain();
132        let result = chain.invoke(HashMap::new()).await;
133        assert!(matches!(result, Err(ChainError::MissingInput(_))));
134    }
135
136    #[tokio::test]
137    async fn non_string_input_returns_input_error() {
138        let chain = echo_chain();
139        let mut inputs = HashMap::new();
140        inputs.insert("input".to_string(), json!(42));
141        let result = chain.invoke(inputs).await;
142        assert!(matches!(result, Err(ChainError::InputError(_))));
143    }
144
145    #[tokio::test]
146    async fn agent_error_maps_to_execution_error() {
147        let executor = AgentExecutor::new(Arc::new(FailAgent), Vec::new());
148        let chain = AgentExecutorChain::new(Arc::new(executor));
149        let mut inputs = HashMap::new();
150        inputs.insert("input".to_string(), json!("hi"));
151        let result = chain.invoke(inputs).await;
152        assert!(matches!(result, Err(ChainError::ExecutionError(_))));
153    }
154
155    #[test]
156    fn exposes_expected_keys_and_name() {
157        let chain = echo_chain();
158        assert_eq!(chain.input_keys(), vec!["input"]);
159        assert_eq!(chain.output_keys(), vec!["output"]);
160        assert_eq!(chain.name(), "agent-executor");
161    }
162}