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
28pub 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 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 pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
55 self.system_prompt = prompt.into();
56 self
57 }
58
59 pub fn with_tools(mut self, catalog: Arc<ToolCatalog>) -> Self {
61 self.tool_catalog = catalog;
62 self
63 }
64
65 pub fn with_codemode(mut self, engine: Arc<CodeModeUtcp>) -> Self {
67 self.set_codemode(engine);
68 self
69 }
70
71 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 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 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 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 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 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 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 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 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 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 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 async fn build_prompt(&self, session_id: &str, user_input: &str) -> Result<Vec<Message>> {
350 let mut messages = Vec::new();
351
352 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 let recent_memories = self.memory.retrieve_recent(session_id).await?;
363
364 let mut token_count = 0;
366 for record in recent_memories.iter().rev() {
367 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 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 self.store_memory(&session_id, "user", &user_input, None)
405 .await?;
406
407 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 let messages = self.build_prompt(&session_id, &user_input).await?;
423
424 let response = self.model.generate(messages, files).await?;
426
427 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 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 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, timestamp: Utc::now(),
484 metadata,
485 embedding: None,
486 };
487
488 self.memory.store(record).await
489 }
490
491 pub async fn flush(&self, _session_id: &str) -> Result<()> {
493 self.memory.flush().await
494 }
495
496 pub fn tools(&self) -> Arc<ToolCatalog> {
498 Arc::clone(&self.tool_catalog)
499 }
500
501 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 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 for record in state.short_term {
521 self.memory.store(record).await?;
522 }
523
524 Ok(())
525 }
526}
527
528#[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}