1use std::time::Instant;
8
9use async_trait::async_trait;
10
11use crate::agent::{AgentOutput, RunUsage};
12use crate::traits::execution_strategy::PendingToolCall;
13use crate::traits::hook::HookAction;
14use crate::traits::strategy::{AgentRuntime, AgentStrategy};
15use crate::types::agent_state::AgentState;
16use crate::types::completion::{CompletionRequest, ResponseContent};
17use crate::types::message::Message;
18use crate::types::tool_call::ToolCall;
19use crate::Result;
20
21pub struct DefaultStrategy;
28
29#[async_trait]
30impl AgentStrategy for DefaultStrategy {
31 #[tracing::instrument(skip_all, fields(session_id = session_id, model = %runtime.provider.model_info().name))]
32 async fn execute(
33 &self,
34 runtime: &AgentRuntime,
35 input: &str,
36 session_id: &str,
37 ) -> Result<AgentOutput> {
38 let start = Instant::now();
39 let model_info = runtime.provider.model_info();
40
41 for hook in &runtime.hooks {
43 hook.on_agent_start(input).await;
44 }
45
46 let mut state = AgentState::new(model_info.tier, model_info.context_window);
47 if let Some(budget) = runtime.config.token_budget {
48 state.token_budget = budget;
49 }
50
51 let mut messages = match load_context(runtime, session_id, input).await {
52 Ok(msgs) => msgs,
53 Err(e) => {
54 for hook in &runtime.hooks {
55 hook.on_error(&e).await;
56 }
57 return Err(e);
58 }
59 };
60 let tool_schemas = runtime.tools.iter().map(|t| t.schema()).collect::<Vec<_>>();
61
62 for _iteration in 0..runtime.config.max_iterations {
64 state.iteration_count += 1;
65 runtime.tracker.on_iteration(&mut state);
66
67 inject_hints(runtime, &state, &mut messages);
68
69 runtime
70 .context_manager
71 .prepare(&mut messages, model_info.context_window, &mut state)
72 .await;
73
74 let request = CompletionRequest {
75 model: model_info.name.clone(),
76 messages: messages.clone(),
77 tools: tool_schemas.clone(),
78 max_tokens: runtime.config.max_tokens,
79 temperature: runtime.config.temperature,
80 response_format: None,
81 stream: false,
82 };
83
84 for hook in &runtime.hooks {
86 hook.on_provider_start(&request).await;
87 }
88
89 let provider_start = Instant::now();
90 let llm_span = tracing::info_span!(
91 target: "traitclaw::llm",
92 "gen_ai.chat",
93 gen_ai.system = "traitclaw",
94 gen_ai.request.model = %model_info.name,
95 gen_ai.usage.input_tokens = tracing::field::Empty,
96 gen_ai.usage.output_tokens = tracing::field::Empty,
97 );
98 let _llm_guard = llm_span.enter();
99 let provider_result = runtime.provider.complete(request).await;
100 drop(_llm_guard);
101
102 let response = match provider_result {
103 Ok(res) => {
104 llm_span.record("gen_ai.usage.input_tokens", res.usage.prompt_tokens);
105 llm_span.record("gen_ai.usage.output_tokens", res.usage.completion_tokens);
106 res
107 }
108 Err(e) => {
109 for hook in &runtime.hooks {
110 hook.on_error(&e).await;
111 }
112 return Err(e);
113 }
114 };
115 let provider_duration = provider_start.elapsed();
116
117 for hook in &runtime.hooks {
119 hook.on_provider_end(&response, provider_duration).await;
120 }
121
122 state.token_usage += response.usage.total_tokens;
123 state.total_context_tokens = response.usage.prompt_tokens;
124 runtime.tracker.on_llm_response(&response, &mut state);
125
126 match response.content {
127 ResponseContent::Text(text) => {
128 let assistant_msg = Message::assistant(&text);
129 if let Err(e) = runtime.memory.append(session_id, assistant_msg).await {
130 tracing::warn!("Failed to save assistant response to memory: {e}");
131 }
132
133 let usage = RunUsage {
134 tokens: state.token_usage,
135 iterations: state.iteration_count,
136 duration: start.elapsed(),
137 };
138
139 #[allow(clippy::cast_possible_truncation)]
140 let duration_ms = usage.duration.as_millis() as u64;
141
142 tracing::info!(
143 iterations = usage.iterations,
144 tokens = usage.tokens,
145 duration_ms,
146 "Agent completed"
147 );
148
149 let output = AgentOutput::text_with_usage(text, usage);
150
151 for hook in &runtime.hooks {
153 hook.on_agent_end(&output, start.elapsed()).await;
154 }
155
156 return Ok(output);
157 }
158 ResponseContent::ToolCalls(tool_calls) => {
159 process_tool_calls(runtime, &tool_calls, &state, &mut messages).await;
160 }
161 }
162 }
163
164 let err = crate::Error::Runtime(format!(
165 "Agent reached maximum iterations ({})",
166 runtime.config.max_iterations
167 ));
168
169 for hook in &runtime.hooks {
171 hook.on_error(&err).await;
172 }
173
174 Err(err)
175 }
176
177 fn stream(
178 &self,
179 runtime: &AgentRuntime,
180 input: &str,
181 session_id: &str,
182 ) -> std::pin::Pin<
183 Box<dyn tokio_stream::Stream<Item = Result<crate::types::stream::StreamEvent>> + Send>,
184 > {
185 crate::streaming::stream_runtime(runtime.clone(), input.to_string(), session_id.to_string())
187 }
188}
189
190async fn load_context(
192 runtime: &AgentRuntime,
193 session_id: &str,
194 input: &str,
195) -> Result<Vec<Message>> {
196 let mut messages = runtime
197 .memory
198 .messages(session_id)
199 .await
200 .unwrap_or_else(|e| {
201 tracing::warn!("Failed to load memory (continuing fresh): {e}");
202 Vec::new()
203 });
204
205 if let Some(ref system_prompt) = runtime.config.system_prompt {
206 if messages.is_empty() || messages[0].role != crate::types::message::MessageRole::System {
207 messages.insert(0, Message::system(system_prompt));
208 }
209 }
210
211 let user_msg = Message::user(input);
212 messages.push(user_msg.clone());
213
214 if let Err(e) = runtime.memory.append(session_id, user_msg).await {
215 tracing::warn!("Failed to save user message to memory: {e}");
216 }
217
218 Ok(messages)
219}
220
221fn inject_hints(runtime: &AgentRuntime, state: &AgentState, messages: &mut Vec<Message>) {
223 for hint in &runtime.hints {
224 if hint.should_trigger(state) {
225 let hint_msg = hint.generate(state);
226 messages.push(Message {
227 role: hint_msg.role,
228 content: hint_msg.content,
229 tool_call_id: None,
230 });
231 tracing::debug!(
232 target: "traitclaw::hint",
233 hint_name = hint.name(),
234 "Hint injected"
235 );
236 }
237 }
238}
239
240async fn process_tool_calls(
242 runtime: &AgentRuntime,
243 tool_calls: &[ToolCall],
244 state: &AgentState,
245 messages: &mut Vec<Message>,
246) {
247 if tool_calls.is_empty() {
248 tracing::debug!("process_tool_calls: empty tool-call slice, skipping");
249 return;
250 }
251
252 let summary: Vec<String> = tool_calls
253 .iter()
254 .map(|tc| format!("{}({})", tc.name, tc.arguments))
255 .collect();
256 messages.push(Message::assistant(format!(
257 "[Tool calls: {}]",
258 summary.join(", ")
259 )));
260
261 for tc in tool_calls {
263 let mut blocked = false;
264
265 for hook in &runtime.hooks {
266 if let HookAction::Block(reason) =
267 hook.before_tool_execute(&tc.name, &tc.arguments).await
268 {
269 messages.push(Message::tool_result(&tc.id, &reason));
270 tracing::debug!(
271 tool = tc.name.as_str(),
272 reason = reason.as_str(),
273 "Tool blocked by hook"
274 );
275 blocked = true;
276 break;
277 }
278 }
279
280 if blocked {
281 continue;
282 }
283
284 let tool_start = Instant::now();
285
286 let pending = vec![PendingToolCall::from(tc)];
288 let results = runtime
289 .execution_strategy
290 .execute_batch(pending, &runtime.tools, &runtime.guards, state)
291 .await;
292
293 for result in results {
294 let processed = runtime
295 .output_transformer
296 .transform(result.output, &tc.name, state)
297 .await;
298
299 for hook in &runtime.hooks {
301 hook.after_tool_execute(&tc.name, &processed, tool_start.elapsed())
302 .await;
303 }
304
305 messages.push(Message::tool_result(&result.id, &processed));
306 tracing::debug!(tool_call_id = result.id.as_str(), "Tool call processed");
307 }
308 }
309}