rs_agent/agent/
mod.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use crate::agent::utcp::{ensure_agent_cli_transport, InProcessTool};
5use anyhow::anyhow;
6use chrono::Utc;
7use futures::FutureExt;
8use rs_utcp::plugins::codemode::{CodeModeUtcp, CodemodeOrchestrator};
9use rs_utcp::providers::base::Provider as UtcpProvider;
10use rs_utcp::providers::cli::CliProvider;
11use rs_utcp::tools::Tool as UtcpTool;
12use rs_utcp::tools::ToolInputOutputSchema;
13use rs_utcp::UtcpClientInterface;
14use serde_json::{json, Value};
15use toon_format::encode_default;
16use uuid::Uuid;
17
18use crate::agent::codemode::{build_orchestrator, format_codemode_value, CodeModeTool};
19use crate::error::{AgentError, Result};
20use crate::memory::{MemoryRecord, SessionMemory};
21use crate::models::LLM;
22use crate::tools::ToolCatalog;
23use crate::types::{AgentOptions, File, GenerationResponse, Message, Role, ToolRequest};
24
25mod codemode;
26mod utcp;
27
28/// Main Agent orchestrator
29pub struct Agent {
30    model: Arc<dyn LLM>,
31    memory: Arc<SessionMemory>,
32    system_prompt: String,
33    context_limit: usize,
34    tool_catalog: Arc<ToolCatalog>,
35    codemode: Option<Arc<CodeModeUtcp>>,
36    codemode_orchestrator: Option<Arc<CodemodeOrchestrator>>,
37}
38
39impl Agent {
40    /// Creates a new Agent with the given configuration
41    pub fn new(model: Arc<dyn LLM>, memory: Arc<SessionMemory>, options: AgentOptions) -> Self {
42        Self {
43            model,
44            memory,
45            system_prompt: options.system_prompt.unwrap_or_default(),
46            context_limit: options.context_limit.unwrap_or(8192),
47            tool_catalog: Arc::new(ToolCatalog::new()),
48            codemode: None,
49            codemode_orchestrator: None,
50        }
51    }
52
53    /// Sets the system prompt
54    pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
55        self.system_prompt = prompt.into();
56        self
57    }
58
59    /// Sets the tool catalog
60    pub fn with_tools(mut self, catalog: Arc<ToolCatalog>) -> Self {
61        self.tool_catalog = catalog;
62        self
63    }
64
65    /// Enables CodeMode execution as a first-class tool (`codemode.run_code`).
66    pub fn with_codemode(mut self, engine: Arc<CodeModeUtcp>) -> Self {
67        self.set_codemode(engine);
68        self
69    }
70
71    /// Enables CodeMode plus the Codemode orchestrator for automatic tool routing.
72    /// If `orchestrator_model` is None, the primary agent model is reused.
73    pub fn with_codemode_orchestrator(
74        mut self,
75        engine: Arc<CodeModeUtcp>,
76        orchestrator_model: Option<Arc<dyn LLM>>,
77    ) -> Self {
78        self.set_codemode(engine.clone());
79
80        let llm = orchestrator_model.unwrap_or_else(|| Arc::clone(&self.model));
81        let orchestrator = build_orchestrator(engine, llm);
82        self.codemode_orchestrator = Some(Arc::new(orchestrator));
83        self
84    }
85
86    /// Registers a UTCP provider and loads its tools into the agent's catalog.
87    pub async fn register_utcp_provider(
88        &self,
89        client: Arc<dyn UtcpClientInterface>,
90        provider: Arc<dyn UtcpProvider>,
91    ) -> Result<Vec<UtcpTool>> {
92        let tools = client
93            .register_tool_provider(provider)
94            .await
95            .map_err(|e| AgentError::UtcpError(e.to_string()))?;
96
97        crate::utcp::register_utcp_tools(self.tool_catalog.as_ref(), client, tools.clone())?;
98        Ok(tools)
99    }
100
101    /// Registers a UTCP provider using a predefined set of tools and adds them to the catalog.
102    pub async fn register_utcp_provider_with_tools(
103        &self,
104        client: Arc<dyn UtcpClientInterface>,
105        provider: Arc<dyn UtcpProvider>,
106        tools: Vec<UtcpTool>,
107    ) -> Result<Vec<UtcpTool>> {
108        let registered_tools = client
109            .register_tool_provider_with_tools(provider, tools)
110            .await
111            .map_err(|e| AgentError::UtcpError(e.to_string()))?;
112
113        crate::utcp::register_utcp_tools(
114            self.tool_catalog.as_ref(),
115            client,
116            registered_tools.clone(),
117        )?;
118
119        Ok(registered_tools)
120    }
121
122    /// Registers UTCP tools into the agent's catalog without re-registering the provider.
123    pub fn register_utcp_tools(
124        &self,
125        client: Arc<dyn UtcpClientInterface>,
126        tools: Vec<UtcpTool>,
127    ) -> Result<()> {
128        crate::utcp::register_utcp_tools(self.tool_catalog.as_ref(), client, tools)
129    }
130
131    /// Returns a UTCP tool specification representing this agent as an in-process tool.
132    pub fn as_utcp_tool(
133        &self,
134        name: impl Into<String>,
135        description: impl Into<String>,
136    ) -> UtcpTool {
137        let name = name.into();
138        let description = description.into();
139        let provider_name = name
140            .split('.')
141            .next()
142            .map(str::trim)
143            .filter(|s| !s.is_empty())
144            .unwrap_or("agent")
145            .to_string();
146
147        let inputs = ToolInputOutputSchema {
148            type_: "object".to_string(),
149            properties: Some(HashMap::from([
150                (
151                    "instruction".to_string(),
152                    json!({
153                        "type": "string",
154                        "description": "The instruction or query for the agent."
155                    }),
156                ),
157                (
158                    "session_id".to_string(),
159                    json!({
160                        "type": "string",
161                        "description": "Optional session id; defaults to the provider-derived session."
162                    }),
163                ),
164            ])),
165            required: Some(vec!["instruction".to_string()]),
166            description: Some("Call the agent with an instruction".to_string()),
167            title: Some("AgentInvocation".to_string()),
168            items: None,
169            enum_: None,
170            minimum: None,
171            maximum: None,
172            format: None,
173        };
174
175        let outputs = ToolInputOutputSchema {
176            type_: "object".to_string(),
177            properties: Some(HashMap::from([
178                ("response".to_string(), json!({ "type": "string" })),
179                ("session_id".to_string(), json!({ "type": "string" })),
180            ])),
181            required: None,
182            description: Some("Agent response payload".to_string()),
183            title: Some("AgentResponse".to_string()),
184            items: None,
185            enum_: None,
186            minimum: None,
187            maximum: None,
188            format: None,
189        };
190
191        UtcpTool {
192            name,
193            description,
194            inputs,
195            outputs,
196            tags: vec![
197                "agent".to_string(),
198                "rs-agent".to_string(),
199                "inproc".to_string(),
200            ],
201            average_response_size: None,
202            provider: Some(json!({
203                "name": provider_name,
204                "provider_type": "cli",
205            })),
206        }
207    }
208
209    /// Registers this agent as a UTCP provider using an in-process CLI shim.
210    pub async fn register_as_utcp_provider(
211        self: Arc<Self>,
212        utcp_client: &dyn UtcpClientInterface,
213        name: impl Into<String>,
214        description: impl Into<String>,
215    ) -> Result<()> {
216        let name = name.into();
217        let description = description.into();
218
219        let provider_name = name
220            .split('.')
221            .next()
222            .map(str::trim)
223            .filter(|s| !s.is_empty())
224            .unwrap_or("agent")
225            .to_string();
226
227        let tool_spec = self.as_utcp_tool(&name, &description);
228        let default_session = format!("{}.session", provider_name);
229        let agent = Arc::clone(&self);
230        let handler = Arc::new(move |args: HashMap<String, Value>| {
231            let agent = Arc::clone(&agent);
232            let default_session = default_session.clone();
233            async move {
234                let instruction = args
235                    .get("instruction")
236                    .and_then(|v| v.as_str())
237                    .map(str::to_string)
238                    .filter(|s| !s.trim().is_empty())
239                    .ok_or_else(|| anyhow!("missing or invalid 'instruction'"))?;
240
241                let session_id = args
242                    .get("session_id")
243                    .and_then(|v| v.as_str())
244                    .map(str::to_string)
245                    .filter(|s| !s.trim().is_empty())
246                    .unwrap_or_else(|| default_session.clone());
247
248                let content = agent
249                    .generate(session_id, instruction)
250                    .await
251                    .map_err(|e| anyhow!(e.to_string()))?;
252
253                Ok(Value::String(content))
254            }
255            .boxed()
256        });
257
258        let inproc_tool = InProcessTool {
259            spec: tool_spec.clone(),
260            handler,
261        };
262
263        let transport = ensure_agent_cli_transport();
264        transport.register(&provider_name, inproc_tool);
265
266        let provider = CliProvider::new(
267            provider_name.clone(),
268            format!("rs-agent-{}", provider_name),
269            None,
270        );
271
272        utcp_client
273            .register_tool_provider_with_tools(Arc::new(provider), vec![tool_spec])
274            .await
275            .map_err(|e| AgentError::UtcpError(e.to_string()))?;
276
277        Ok(())
278    }
279
280    /// Generates a response for the given user input
281    pub async fn generate(
282        &self,
283        session_id: impl Into<String>,
284        user_input: impl Into<String>,
285    ) -> Result<String> {
286        let response = self
287            .generate_internal(session_id.into(), user_input.into(), None)
288            .await?;
289
290        Ok(response.content)
291    }
292
293    /// Generates a response encoded as TOON for token-efficient downstream parsing
294    pub async fn generate_toon(
295        &self,
296        session_id: impl Into<String>,
297        user_input: impl Into<String>,
298    ) -> Result<String> {
299        let response = self
300            .generate_internal(session_id.into(), user_input.into(), None)
301            .await?;
302
303        encode_default(&response).map_err(|e| AgentError::ToonFormatError(e.to_string()))
304    }
305
306    /// Generates a response with file attachments
307    pub async fn generate_with_files(
308        &self,
309        session_id: impl Into<String>,
310        user_input: impl Into<String>,
311        files: Vec<File>,
312    ) -> Result<String> {
313        let response = self
314            .generate_internal(session_id.into(), user_input.into(), Some(files))
315            .await?;
316
317        Ok(response.content)
318    }
319
320    /// Invokes a tool by name
321    pub async fn invoke_tool(
322        &self,
323        session_id: impl Into<String>,
324        tool_name: &str,
325        arguments: HashMap<String, serde_json::Value>,
326    ) -> Result<String> {
327        let session_id = session_id.into();
328
329        let request = ToolRequest {
330            session_id: session_id.clone(),
331            arguments,
332        };
333
334        let response = self.tool_catalog.invoke(tool_name, request).await?;
335
336        // Store tool invocation in memory
337        self.store_memory(
338            &session_id,
339            "tool",
340            &format!("Called {}: {}", tool_name, response.content),
341            response.metadata,
342        )
343        .await?;
344
345        Ok(response.content)
346    }
347
348    /// Builds the prompt with system message and context
349    async fn build_prompt(&self, session_id: &str, user_input: &str) -> Result<Vec<Message>> {
350        let mut messages = Vec::new();
351
352        // Add system prompt if set
353        if !self.system_prompt.is_empty() {
354            messages.push(Message {
355                role: Role::System,
356                content: self.system_prompt.clone(),
357                metadata: None,
358            });
359        }
360
361        // Retrieve recent conversation history
362        let recent_memories = self.memory.retrieve_recent(session_id).await?;
363
364        // Add context from memory (limited by context_limit)
365        let mut token_count = 0;
366        for record in recent_memories.iter().rev() {
367            // Simple token estimation (4 chars ≈ 1 token)
368            let estimated_tokens = record.content.len() / 4;
369            if token_count + estimated_tokens > self.context_limit {
370                break;
371            }
372
373            messages.push(Message {
374                role: match record.role.as_str() {
375                    "user" => Role::User,
376                    "assistant" => Role::Assistant,
377                    "tool" => Role::Tool,
378                    _ => Role::User,
379                },
380                content: record.content.clone(),
381                metadata: record.metadata.clone(),
382            });
383
384            token_count += estimated_tokens;
385        }
386
387        // Add current user input
388        messages.push(Message {
389            role: Role::User,
390            content: user_input.to_string(),
391            metadata: None,
392        });
393
394        Ok(messages)
395    }
396
397    async fn generate_internal(
398        &self,
399        session_id: String,
400        user_input: String,
401        files: Option<Vec<File>>,
402    ) -> Result<GenerationResponse> {
403        // Store user message in memory
404        self.store_memory(&session_id, "user", &user_input, None)
405            .await?;
406
407        // Try CodeMode orchestration before invoking the primary model
408        let has_files = files.as_ref().map(|f| !f.is_empty()).unwrap_or(false);
409        if !has_files {
410            if let Some((content, metadata)) = self
411                .try_codemode_orchestration(&session_id, &user_input)
412                .await?
413            {
414                self.store_memory(&session_id, "assistant", &content, metadata.clone())
415                    .await?;
416
417                return Ok(GenerationResponse { content, metadata });
418            }
419        }
420
421        // Build prompt with context
422        let messages = self.build_prompt(&session_id, &user_input).await?;
423
424        // Generate response
425        let response = self.model.generate(messages, files).await?;
426
427        // Store assistant response in memory
428        self.store_memory(&session_id, "assistant", &response.content, None)
429            .await?;
430
431        Ok(response)
432    }
433
434    fn set_codemode(&mut self, engine: Arc<CodeModeUtcp>) {
435        self.codemode = Some(engine.clone());
436        // Expose codemode.run_code as a tool; ignore duplicate registrations
437        let _ = self
438            .tool_catalog
439            .register(Box::new(CodeModeTool::new(engine)));
440    }
441
442    async fn try_codemode_orchestration(
443        &self,
444        _session_id: &str,
445        user_input: &str,
446    ) -> Result<Option<(String, Option<HashMap<String, String>>)>> {
447        let orchestrator = match self.codemode_orchestrator.as_ref() {
448            Some(o) => o,
449            None => return Ok(None),
450        };
451
452        let value = orchestrator
453            .call_prompt(user_input)
454            .await
455            .map_err(|e| AgentError::Other(e.to_string()))?;
456
457        if let Some(v) = value {
458            let content = format_codemode_value(&v);
459            let metadata = Some(HashMap::from([(
460                "source".to_string(),
461                "codemode_orchestrator".to_string(),
462            )]));
463            return Ok(Some((content, metadata)));
464        }
465
466        Ok(None)
467    }
468
469    /// Stores a memory record
470    async fn store_memory(
471        &self,
472        session_id: &str,
473        role: &str,
474        content: &str,
475        metadata: Option<HashMap<String, String>>,
476    ) -> Result<()> {
477        let record = MemoryRecord {
478            id: Uuid::new_v4(),
479            session_id: session_id.to_string(),
480            role: role.to_string(),
481            content: content.to_string(),
482            importance: 0.5, // Default importance
483            timestamp: Utc::now(),
484            metadata,
485            embedding: None,
486        };
487
488        self.memory.store(record).await
489    }
490
491    /// Flushes memory to persistent store
492    pub async fn flush(&self, _session_id: &str) -> Result<()> {
493        self.memory.flush().await
494    }
495
496    /// Returns the tool catalog
497    pub fn tools(&self) -> Arc<ToolCatalog> {
498        Arc::clone(&self.tool_catalog)
499    }
500
501    /// Checkpoints the agent state for persistence
502    pub async fn checkpoint(&self, session_id: &str) -> Result<Vec<u8>> {
503        let recent = self.memory.retrieve_recent(session_id).await?;
504
505        let state = AgentState {
506            system_prompt: self.system_prompt.clone(),
507            short_term: recent,
508            timestamp: Utc::now(),
509        };
510
511        serde_json::to_vec(&state).map_err(|e| AgentError::SerializationError(e))
512    }
513
514    /// Restores agent state from checkpoint
515    pub async fn restore(&self, _session_id: &str, data: &[u8]) -> Result<()> {
516        let state: AgentState =
517            serde_json::from_slice(data).map_err(|e| AgentError::SerializationError(e))?;
518
519        // Restore memories
520        for record in state.short_term {
521            self.memory.store(record).await?;
522        }
523
524        Ok(())
525    }
526}
527
528/// Serializable agent state for checkpointing
529#[derive(serde::Serialize, serde::Deserialize)]
530struct AgentState {
531    system_prompt: String,
532    short_term: Vec<MemoryRecord>,
533    timestamp: chrono::DateTime<Utc>,
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539    use crate::memory::InMemoryStore;
540    use crate::types::GenerationResponse;
541    use anyhow::anyhow;
542    use async_trait::async_trait;
543    use rs_utcp::config::UtcpClientConfig;
544    use rs_utcp::plugins::codemode::CodeModeUtcp;
545    use rs_utcp::providers::base::Provider;
546    use rs_utcp::repository::in_memory::InMemoryToolRepository;
547    use rs_utcp::tag::tag_search::TagSearchStrategy;
548    use rs_utcp::tools::{Tool, ToolInputOutputSchema};
549    use rs_utcp::transports::stream::StreamResult;
550    use rs_utcp::transports::CommunicationProtocol;
551    use rs_utcp::UtcpClient;
552    use serde_json::json;
553    use toon_format::decode_default;
554
555    struct MockLLM;
556
557    #[async_trait]
558    impl LLM for MockLLM {
559        async fn generate(
560            &self,
561            _messages: Vec<Message>,
562            _files: Option<Vec<File>>,
563        ) -> Result<GenerationResponse> {
564            Ok(GenerationResponse {
565                content: "Mock response".to_string(),
566                metadata: None,
567            })
568        }
569
570        fn model_name(&self) -> &str {
571            "mock"
572        }
573    }
574
575    #[tokio::test]
576    async fn test_agent_generate() {
577        let model = Arc::new(MockLLM);
578        let store = Box::new(InMemoryStore::new());
579        let memory = Arc::new(SessionMemory::new(store, 10));
580
581        let agent = Agent::new(model, memory, AgentOptions::default())
582            .with_system_prompt("You are a helpful assistant");
583
584        let response = agent.generate("test_session", "Hello").await.unwrap();
585
586        assert_eq!(response, "Mock response");
587    }
588
589    #[tokio::test]
590    async fn test_agent_generate_toon() {
591        let model = Arc::new(MockLLM);
592        let store = Box::new(InMemoryStore::new());
593        let memory = Arc::new(SessionMemory::new(store, 10));
594
595        let agent = Agent::new(model, memory, AgentOptions::default())
596            .with_system_prompt("You are a helpful assistant");
597
598        let response = agent.generate_toon("test_session", "Hello").await.unwrap();
599
600        let decoded: GenerationResponse = decode_default(&response).unwrap();
601        assert_eq!(decoded.content, "Mock response");
602    }
603
604    struct EchoLLM;
605
606    #[async_trait]
607    impl LLM for EchoLLM {
608        async fn generate(
609            &self,
610            messages: Vec<Message>,
611            _files: Option<Vec<File>>,
612        ) -> Result<GenerationResponse> {
613            let last = messages
614                .last()
615                .map(|m| m.content.clone())
616                .unwrap_or_default();
617            Ok(GenerationResponse {
618                content: format!("Echo: {}", last),
619                metadata: None,
620            })
621        }
622
623        fn model_name(&self) -> &str {
624            "echo-llm"
625        }
626    }
627
628    #[tokio::test]
629    async fn agent_as_utcp_tool_registers_and_handles_calls() {
630        let model = Arc::new(EchoLLM);
631        let store = Box::new(InMemoryStore::new());
632        let memory = Arc::new(SessionMemory::new(store, 4));
633        let agent = Arc::new(Agent::new(model, memory, AgentOptions::default()));
634
635        let repo = Arc::new(InMemoryToolRepository::new());
636        let search = Arc::new(TagSearchStrategy::new(repo.clone(), 1.0));
637        let utcp = UtcpClient::create(UtcpClientConfig::new(), repo, search)
638            .await
639            .unwrap();
640
641        agent
642            .clone()
643            .register_as_utcp_provider(&utcp, "local.agent", "Test agent")
644            .await
645            .unwrap();
646
647        let mut args = HashMap::new();
648        args.insert("instruction".to_string(), json!("ping"));
649
650        let result = utcp.call_tool("local.agent", args).await.unwrap();
651        assert_eq!(result.as_str(), Some("Echo: ping"));
652    }
653
654    struct NoopUtcpClient;
655
656    #[async_trait]
657    impl UtcpClientInterface for NoopUtcpClient {
658        async fn register_tool_provider(
659            &self,
660            _prov: Arc<dyn Provider>,
661        ) -> anyhow::Result<Vec<Tool>> {
662            Ok(vec![])
663        }
664
665        async fn register_tool_provider_with_tools(
666            &self,
667            _prov: Arc<dyn Provider>,
668            tools: Vec<Tool>,
669        ) -> anyhow::Result<Vec<Tool>> {
670            Ok(tools)
671        }
672
673        async fn deregister_tool_provider(&self, _provider_name: &str) -> anyhow::Result<()> {
674            Ok(())
675        }
676
677        async fn call_tool(
678            &self,
679            _tool_name: &str,
680            _args: HashMap<String, serde_json::Value>,
681        ) -> anyhow::Result<serde_json::Value> {
682            Ok(json!({"ok": true}))
683        }
684
685        async fn search_tools(&self, _query: &str, _limit: usize) -> anyhow::Result<Vec<Tool>> {
686            Ok(vec![])
687        }
688
689        fn get_transports(&self) -> HashMap<String, Arc<dyn CommunicationProtocol>> {
690            HashMap::new()
691        }
692
693        async fn call_tool_stream(
694            &self,
695            _tool_name: &str,
696            _args: HashMap<String, serde_json::Value>,
697        ) -> anyhow::Result<Box<dyn StreamResult>> {
698            Err(anyhow!("streaming not supported"))
699        }
700    }
701
702    #[tokio::test(flavor = "multi_thread")]
703    async fn codemode_tool_can_execute_snippet() {
704        let model = Arc::new(MockLLM);
705        let store = Box::new(InMemoryStore::new());
706        let memory = Arc::new(SessionMemory::new(store, 4));
707
708        let codemode = Arc::new(CodeModeUtcp::new(Arc::new(NoopUtcpClient)));
709        let agent = Agent::new(model, memory, AgentOptions::default()).with_codemode(codemode);
710
711        let mut args = HashMap::new();
712        args.insert("code".to_string(), json!(r#"{"result": "ok"}"#));
713
714        let output = agent
715            .invoke_tool("session", "codemode.run_code", args)
716            .await
717            .unwrap();
718
719        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
720        assert_eq!(parsed["value"], json!({"result": "ok"}));
721        assert_eq!(parsed["stdout"], json!(""));
722    }
723
724    struct OrchestratorLLM;
725
726    #[async_trait]
727    impl LLM for OrchestratorLLM {
728        async fn generate(
729            &self,
730            messages: Vec<Message>,
731            _files: Option<Vec<File>>,
732        ) -> Result<GenerationResponse> {
733            let prompt = messages
734                .last()
735                .map(|m| m.content.as_str())
736                .unwrap_or_default();
737
738            let content = if prompt.contains("Respond with only 'yes' or 'no'") {
739                "yes"
740            } else if prompt.contains("comma-separated list of names only") {
741                "demo.echo"
742            } else if prompt.contains("Generate a Rhai snippet") {
743                "let res = call_tool(\"demo.echo\", #{\"message\": \"hi\"}); res"
744            } else {
745                "fallback"
746            };
747
748            Ok(GenerationResponse {
749                content: content.to_string(),
750                metadata: None,
751            })
752        }
753
754        fn model_name(&self) -> &str {
755            "orchestrator-llm"
756        }
757    }
758
759    struct EchoUtcpClient;
760
761    #[async_trait]
762    impl UtcpClientInterface for EchoUtcpClient {
763        async fn register_tool_provider(
764            &self,
765            _prov: Arc<dyn Provider>,
766        ) -> anyhow::Result<Vec<Tool>> {
767            Ok(vec![])
768        }
769
770        async fn register_tool_provider_with_tools(
771            &self,
772            _prov: Arc<dyn Provider>,
773            tools: Vec<Tool>,
774        ) -> anyhow::Result<Vec<Tool>> {
775            Ok(tools)
776        }
777
778        async fn deregister_tool_provider(&self, _provider_name: &str) -> anyhow::Result<()> {
779            Ok(())
780        }
781
782        async fn call_tool(
783            &self,
784            tool_name: &str,
785            args: HashMap<String, serde_json::Value>,
786        ) -> anyhow::Result<serde_json::Value> {
787            if tool_name != "demo.echo" {
788                return Err(anyhow!("unknown tool"));
789            }
790            let message = args
791                .get("message")
792                .and_then(|v| v.as_str())
793                .unwrap_or("missing");
794            Ok(json!({ "message": message }))
795        }
796
797        async fn search_tools(&self, _query: &str, _limit: usize) -> anyhow::Result<Vec<Tool>> {
798            Ok(vec![Tool {
799                name: "demo.echo".to_string(),
800                description: "Echo a message".to_string(),
801                inputs: ToolInputOutputSchema {
802                    type_: "object".to_string(),
803                    properties: Some(HashMap::from([(
804                        "message".to_string(),
805                        json!({"type": "string"}),
806                    )])),
807                    required: Some(vec!["message".to_string()]),
808                    description: None,
809                    title: None,
810                    items: None,
811                    enum_: None,
812                    minimum: None,
813                    maximum: None,
814                    format: None,
815                },
816                outputs: ToolInputOutputSchema {
817                    type_: "object".to_string(),
818                    properties: None,
819                    required: None,
820                    description: None,
821                    title: None,
822                    items: None,
823                    enum_: None,
824                    minimum: None,
825                    maximum: None,
826                    format: None,
827                },
828                tags: vec![],
829                average_response_size: None,
830                provider: None,
831            }])
832        }
833
834        fn get_transports(&self) -> HashMap<String, Arc<dyn CommunicationProtocol>> {
835            HashMap::new()
836        }
837
838        async fn call_tool_stream(
839            &self,
840            _tool_name: &str,
841            _args: HashMap<String, serde_json::Value>,
842        ) -> anyhow::Result<Box<dyn StreamResult>> {
843            Err(anyhow!("streaming not supported"))
844        }
845    }
846
847    #[tokio::test(flavor = "multi_thread")]
848    async fn codemode_orchestrator_handles_prompt() {
849        let model = Arc::new(OrchestratorLLM);
850        let store = Box::new(InMemoryStore::new());
851        let memory = Arc::new(SessionMemory::new(store, 8));
852
853        let codemode = Arc::new(CodeModeUtcp::new(Arc::new(EchoUtcpClient)));
854        let agent = Agent::new(model, memory, AgentOptions::default())
855            .with_codemode_orchestrator(codemode, None);
856
857        let response = agent
858            .generate("session", "please call the echo tool")
859            .await
860            .unwrap();
861
862        let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
863        assert_eq!(parsed["message"], json!("hi"));
864    }
865}