1use std::sync::Arc;
12
13use async_trait::async_trait;
14use atomr_agents_core::{AgentId, CallCtx, Result};
15use atomr_agents_instruction::InstructionStrategy;
16use atomr_agents_observability::EventBus;
17use atomr_agents_strategy::{MemoryStrategy, SkillStrategy, ToolStrategy};
18
19use crate::inference::{InferenceClient, TurnResult};
20use crate::pipeline::{run_turn_impl, AgentBudgets};
21use crate::r#trait::AgentDispatch;
22
23pub struct BoxedAgent {
28 pub id: AgentId,
29 pub model: String,
30 pub instructions: Box<dyn InstructionStrategy>,
31 pub tools: Box<dyn ToolStrategy>,
32 pub memory: Box<dyn MemoryStrategy>,
33 pub skills: Box<dyn SkillStrategy>,
34 pub inference: Arc<dyn InferenceClient>,
35 pub bus: EventBus,
36 pub max_tool_iterations: u32,
37}
38
39impl BoxedAgent {
40 pub async fn run_turn(&self, user: String, budgets: AgentBudgets) -> Result<TurnResult> {
42 run_turn_impl(
43 &self.id,
44 &self.model,
45 &*self.instructions,
46 &*self.tools,
47 &*self.memory,
48 &*self.skills,
49 &self.inference,
50 &self.bus,
51 self.max_tool_iterations,
52 user,
53 budgets,
54 )
55 .await
56 }
57}
58
59#[async_trait]
60impl AgentDispatch for BoxedAgent {
61 async fn dispatch(&self, user: String, ctx: CallCtx) -> Result<TurnResult> {
62 self.run_turn(
63 user,
64 AgentBudgets {
65 tokens: ctx.tokens,
66 time: ctx.time,
67 money: ctx.money,
68 iterations: ctx.iterations,
69 },
70 )
71 .await
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 use async_trait::async_trait;
80 use atomr_agents_core::{
81 InvokeCtx, IterationBudget, MoneyBudget, TimeBudget, TokenBudget, ToolId, Value,
82 };
83 use atomr_agents_instruction::{ComposedInstructionStrategy, StaticBehaviorStrategy, StaticTaskStrategy};
84 use atomr_agents_memory::{InMemoryStore, RecencyMemoryStrategy};
85 use atomr_agents_persona::StaticPersonaStrategy;
86 use atomr_agents_skill::StaticSkillStrategy;
87 use atomr_agents_tool::{DynTool, Provider, StaticToolStrategy, Tool, ToolDescriptor, ToolSchema};
88
89 use crate::inference::LocalRunnerClient;
90 use atomr_infer_testkit::{MockRunner, MockScript};
91
92 struct CalculatorTool {
95 d: ToolDescriptor,
96 }
97 impl CalculatorTool {
98 fn new() -> Self {
99 Self {
100 d: ToolDescriptor {
101 id: ToolId::from("calculator"),
102 name: "calculator".into(),
103 description: "evaluate simple arithmetic".into(),
104 schema: ToolSchema::empty_object(),
105 },
106 }
107 }
108 }
109 #[async_trait]
110 impl Tool for CalculatorTool {
111 fn descriptor(&self) -> &ToolDescriptor {
112 &self.d
113 }
114 async fn invoke(&self, args: Value, _ctx: &InvokeCtx) -> Result<Value> {
115 let a = args.get("a").and_then(|v| v.as_i64()).unwrap_or(0);
116 let b = args.get("b").and_then(|v| v.as_i64()).unwrap_or(0);
117 Ok(serde_json::json!({"sum": a + b}))
118 }
119 }
120
121 fn build_boxed_agent(runner: MockRunner) -> BoxedAgent {
122 let store = Arc::new(InMemoryStore::new());
123 let mem = RecencyMemoryStrategy::new(store, 5, 30);
124 let tools: Vec<DynTool> = vec![Arc::new(CalculatorTool::new())];
125 let tool_strat = StaticToolStrategy::new(tools);
126 let instr = ComposedInstructionStrategy::new(
127 StaticPersonaStrategy::new("You are a calculator assistant."),
128 StaticTaskStrategy("Use tools to answer arithmetic questions.".into()),
129 StaticBehaviorStrategy("Reply tersely.".into()),
130 );
131 let skill_strat = StaticSkillStrategy::new(vec![]);
132 let inference: Arc<dyn InferenceClient> = Arc::new(LocalRunnerClient::new(runner, Provider::OpenAi));
133 BoxedAgent {
134 id: AgentId::from("boxed-1"),
135 model: "mock".into(),
136 instructions: Box::new(instr),
137 tools: Box::new(tool_strat),
138 memory: Box::new(mem),
139 skills: Box::new(skill_strat),
140 inference,
141 bus: EventBus::new(),
142 max_tool_iterations: 3,
143 }
144 }
145
146 #[tokio::test]
147 async fn boxed_agent_runs_simple_text_turn() {
148 let runner = MockRunner::new(MockScript::from_text(["the answer is ", "42"]));
149 let agent = build_boxed_agent(runner);
150 let r = agent
151 .run_turn(
152 "what's 1+2".into(),
153 AgentBudgets {
154 tokens: TokenBudget::new(10_000),
155 time: TimeBudget::new(std::time::Duration::from_secs(30)),
156 money: MoneyBudget::from_usd(1.0),
157 iterations: IterationBudget::new(5),
158 },
159 )
160 .await
161 .unwrap();
162 assert!(r.text.contains("42"));
163 assert_eq!(r.usage.output_tokens, 2);
164 }
165
166 #[tokio::test]
167 async fn boxed_agent_dispatches_through_trait() {
168 let runner = MockRunner::new(MockScript::from_text(["pong"]));
169 let agent = build_boxed_agent(runner);
170 let ctx = CallCtx {
171 agent_id: Some(agent.id.clone()),
172 tokens: TokenBudget::new(10_000),
173 time: TimeBudget::new(std::time::Duration::from_secs(30)),
174 money: MoneyBudget::from_usd(1.0),
175 iterations: IterationBudget::new(5),
176 trace: vec![],
177 };
178 let r = AgentDispatch::dispatch(&agent, "ping".into(), ctx).await.unwrap();
179 assert_eq!(r.text, "pong");
180 }
181}