1use crate::context::{CompactionConfig, CompactionError, CompactionResult, Compactor, TokenTracker};
2use crate::core::PromptCache;
3use crate::core::prompt_cache_key::derive_prompt_cache_key;
4pub use crate::core::retry_config::RetryConfig;
5use crate::events::{
6 AgentCommand, AgentEvent, AgentObserver, Command, CompactionOutcome, ContextEvent, ContextUsage, LlmCallOutcome,
7 LlmCallPurpose, ModelEvent, ToolEvent, TurnEvent, TurnOutcome, UserCommand,
8};
9use crate::mcp::run_mcp_task::{McpCommand, ToolExecutionEvent};
10use futures::Stream;
11use llm::types::IsoString;
12use llm::{
13 AssistantReasoning, ChatMessage, Context, EncryptedReasoningContent, LlmError, LlmResponse, StopReason,
14 StreamingModelProvider, TokenUsage, ToolCallError, ToolCallRequest, ToolCallResult,
15};
16use std::collections::{HashMap, HashSet, VecDeque};
17use std::pin::Pin;
18use std::sync::Arc;
19use std::time::Duration;
20use tokio::sync::mpsc;
21use tokio::time::sleep;
22use tokio_stream::StreamExt;
23use tokio_stream::StreamMap;
24use tokio_stream::wrappers::ReceiverStream;
25
26#[derive(Debug)]
28enum StreamEvent {
29 LlmRequestStarted { attempt: u32 },
30 Llm(Result<LlmResponse, LlmError>),
31 ToolExecution(ToolExecutionEvent),
32 Command(Command),
33 Compaction(Result<CompactionResult, CompactionError>),
34}
35
36type EventStream = Pin<Box<dyn Stream<Item = StreamEvent> + Send>>;
37
38#[derive(Debug, Clone, PartialEq, Eq, Hash)]
41enum StreamKey {
42 User,
43 Llm,
44 Compaction,
45 Tool(String),
46}
47
48pub(crate) struct AgentConfig {
49 pub llm: Arc<dyn StreamingModelProvider>,
50 pub context: Context,
51 pub mcp_command_tx: Option<mpsc::Sender<McpCommand>>,
52 pub tool_timeout: Duration,
53 pub compaction_config: Option<CompactionConfig>,
54 pub auto_continue: AutoContinue,
55 pub retry_config: RetryConfig,
56 pub context_window: Option<u32>,
57 pub prompt_cache: PromptCache,
58 pub observers: Vec<Box<dyn AgentObserver>>,
59}
60
61pub struct Agent {
62 llm: Arc<dyn StreamingModelProvider>,
63 context: Context,
64 mcp_command_tx: Option<mpsc::Sender<McpCommand>>,
65 message_tx: mpsc::Sender<AgentEvent>,
66 observers: Vec<Box<dyn AgentObserver>>,
67 streams: StreamMap<StreamKey, EventStream>,
68 tool_timeout: Duration,
69 token_tracker: TokenTracker,
70 compaction_config: Option<CompactionConfig>,
71 auto_continue: AutoContinue,
72 retry_config: RetryConfig,
73 active_requests: HashMap<String, ToolCallRequest>,
74 queued_user_messages: VecDeque<Vec<llm::ContentBlock>>,
75 pending_turn_messages: Vec<Vec<llm::ContentBlock>>,
76 context_window: Option<u32>,
77 prompt_cache: PromptCache,
78 turn_active: bool,
79 llm_call_active: bool,
80}
81
82impl Agent {
83 pub(crate) fn new(
84 config: AgentConfig,
85 command_rx: mpsc::Receiver<Command>,
86 message_tx: mpsc::Sender<AgentEvent>,
87 ) -> Self {
88 let mut streams: StreamMap<StreamKey, EventStream> = StreamMap::new();
89 streams.insert(StreamKey::User, Box::pin(ReceiverStream::new(command_rx).map(StreamEvent::Command)));
90
91 let context_limit = config.context_window.or_else(|| config.llm.context_window());
92
93 Self {
94 llm: config.llm,
95 context: config.context,
96 mcp_command_tx: config.mcp_command_tx,
97 message_tx,
98 observers: config.observers,
99 streams,
100 tool_timeout: config.tool_timeout,
101 token_tracker: TokenTracker::new(context_limit),
102 compaction_config: config.compaction_config,
103 auto_continue: config.auto_continue,
104 retry_config: config.retry_config,
105 active_requests: HashMap::new(),
106 queued_user_messages: VecDeque::new(),
107 pending_turn_messages: Vec::new(),
108 context_window: config.context_window,
109 prompt_cache: config.prompt_cache,
110 turn_active: false,
111 llm_call_active: false,
112 }
113 }
114
115 pub fn current_model_display_name(&self) -> String {
116 self.llm.display_name()
117 }
118
119 pub fn token_tracker(&self) -> &TokenTracker {
121 &self.token_tracker
122 }
123
124 pub async fn run(mut self) {
125 let mut state = IterationState::new();
126 self.emit_tool_definitions().await;
127
128 while let Some((_, event)) = self.streams.next().await {
129 match event {
130 StreamEvent::Command(Command::UserCommand(UserCommand::Cancel)) => {
131 self.on_user_cancel(&mut state).await;
132 }
133
134 StreamEvent::Command(Command::UserCommand(UserCommand::ClearContext)) => {
135 self.on_user_clear_context(&mut state).await;
136 }
137
138 StreamEvent::Command(Command::UserCommand(UserCommand::Text { content })) => {
139 if self.is_busy() {
140 self.queued_user_messages.push_back(content);
141 } else {
142 state = IterationState::new();
143 self.on_user_text(content).await;
144 }
145 }
146
147 StreamEvent::Command(Command::AgentCommand(AgentCommand::SwitchModel(new_provider))) => {
148 self.on_switch_model(new_provider).await;
149 }
150
151 StreamEvent::Command(Command::AgentCommand(AgentCommand::UpdateTools(tools))) => {
152 self.context.set_tools(tools);
153 self.emit_tool_definitions().await;
154 }
155
156 StreamEvent::Command(Command::AgentCommand(AgentCommand::UpdateMcpInstructions { server, body })) => {
157 self.on_update_instruction(server, body).await;
158 }
159
160 StreamEvent::Command(Command::AgentCommand(AgentCommand::SetReasoningEffort(effort))) => {
161 self.context.set_reasoning_effort(effort);
162 }
163
164 StreamEvent::Command(Command::AgentCommand(AgentCommand::ReplaceConversation(messages))) => {
165 self.on_replace_conversation(messages, &mut state).await;
166 }
167
168 StreamEvent::LlmRequestStarted { attempt } => {
169 self.begin_chat_call(attempt).await;
170 }
171
172 StreamEvent::Llm(llm_event) => {
173 self.on_llm_event(llm_event, &mut state).await;
174 }
175
176 StreamEvent::ToolExecution(tool_event) => {
177 self.on_tool_execution_event(tool_event, &mut state).await;
178 }
179
180 StreamEvent::Compaction(result) => {
181 self.on_compaction_complete(result).await;
182 }
183 }
184
185 if state.is_complete() {
186 let Some(id) = state.current_message_id.take() else {
187 continue;
188 };
189 let iteration = std::mem::replace(&mut state, IterationState::new());
190 self.on_iteration_complete(id, iteration).await;
191 }
192 }
193
194 tracing::debug!("Agent task shutting down - input channel closed");
195 }
196
197 async fn on_iteration_complete(&mut self, id: String, iteration: IterationState) {
198 let IterationState {
199 message_content,
200 reasoning_summary_text,
201 encrypted_reasoning,
202 completed_tool_calls,
203 stop_reason,
204 ..
205 } = iteration;
206 let has_tool_calls = !completed_tool_calls.is_empty();
207 let has_content = !message_content.is_empty() || has_tool_calls;
208 let should_auto_continue = self.auto_continue.should_continue(stop_reason.as_ref());
209
210 if has_content {
211 let reasoning = AssistantReasoning::from_parts(reasoning_summary_text.clone(), encrypted_reasoning);
212 self.update_context(&message_content, reasoning, completed_tool_calls);
213
214 self.emit(AgentEvent::text(&id, &message_content, true)).await;
215
216 if !reasoning_summary_text.is_empty() {
217 self.emit(AgentEvent::thought(&id, &reasoning_summary_text, true)).await;
218 }
219 }
220
221 let has_queued_text = !self.queued_user_messages.is_empty();
222 if has_queued_text || has_tool_calls {
223 self.auto_continue.reset();
224 self.start_next_turn().await;
225 } else if should_auto_continue {
226 self.auto_continue.advance();
227 tracing::info!(
228 "LLM stopped with {:?}, auto-continuing (attempt {}/{})",
229 stop_reason,
230 self.auto_continue.count(),
231 self.auto_continue.max()
232 );
233
234 self.emit(AgentEvent::Turn(TurnEvent::AutoContinue {
235 attempt: self.auto_continue.count(),
236 max_attempts: self.auto_continue.max(),
237 }))
238 .await;
239
240 self.inject_continuation_prompt(&message_content, stop_reason.as_ref());
241 self.start_next_turn().await;
242 } else {
243 tracing::debug!("LLM completed turn with stop reason: {:?}", stop_reason);
244 self.auto_continue.reset();
245 self.finish_turn(TurnOutcome::Completed).await;
246 }
247 }
248
249 async fn start_next_turn(&mut self) {
250 self.pending_turn_messages.extend(self.queued_user_messages.drain(..));
251 if self.compaction_needed() {
252 self.begin_compaction().await;
253 } else {
254 self.start_chat_turn().await;
255 }
256 }
257
258 async fn start_chat_turn(&mut self) {
259 self.commit_pending_turn_messages();
260 self.start_llm_stream(None, 0).await;
261 }
262
263 async fn on_user_cancel(&mut self, state: &mut IterationState) {
264 self.abort_in_flight_work().await;
265 self.commit_pending_turn_messages();
266 self.queued_user_messages.clear();
267 *state = IterationState::new();
268 self.finish_turn(TurnOutcome::Cancelled).await;
269 }
270
271 async fn on_user_clear_context(&mut self, state: &mut IterationState) {
272 self.abort_in_flight_work().await;
273 self.queued_user_messages.clear();
274 self.pending_turn_messages.clear();
275 self.context.clear_conversation();
276 self.token_tracker.reset_current_usage();
277 self.auto_continue.reset();
278 *state = IterationState::new();
279
280 self.emit(AgentEvent::Context(ContextEvent::Cleared)).await;
281 self.finish_turn(TurnOutcome::Cancelled).await;
282 }
283
284 async fn on_replace_conversation(&mut self, messages: Vec<ChatMessage>, state: &mut IterationState) {
285 self.abort_in_flight_work().await;
286 self.queued_user_messages.clear();
287 self.pending_turn_messages.clear();
288 self.context.replace_conversation(messages);
289 self.auto_continue.reset();
290 *state = IterationState::new();
291 self.emit(self.context_usage_message()).await;
292 self.finish_turn(TurnOutcome::Cancelled).await;
293 }
294
295 async fn on_user_text(&mut self, content: Vec<llm::ContentBlock>) {
296 self.auto_continue.reset();
297 self.turn_active = true;
298 self.emit(AgentEvent::Turn(TurnEvent::Started { content: content.clone() })).await;
299 self.queued_user_messages.push_back(content);
300 self.start_next_turn().await;
301 }
302
303 async fn on_update_instruction(&mut self, server: String, body: Option<String>) {
304 self.prompt_cache.update_mcp_instruction(server, body);
305 match self.prompt_cache.render().await {
306 Ok(content) => self.context.set_system_content(content),
307 Err(e) => tracing::warn!("Failed to rebuild system prompt after instructions update: {e}"),
308 }
309 }
310
311 async fn on_switch_model(&mut self, new_provider: Box<dyn StreamingModelProvider>) {
312 let previous = self.llm.display_name();
313 let new_context_limit = self.context_window.or_else(|| new_provider.context_window());
314 self.llm = Arc::from(new_provider);
315 self.token_tracker.reset_current_usage();
316 self.token_tracker.set_context_limit(new_context_limit);
317 let new = self.llm.display_name();
318 self.emit(AgentEvent::Model(ModelEvent::Switched { previous, new })).await;
319
320 self.emit(self.context_usage_message()).await;
321 }
322
323 async fn start_llm_stream(&mut self, delay: Option<Duration>, attempt: u32) {
324 self.refresh_prompt_cache_key();
325 self.streams.remove(&StreamKey::Llm);
326 let stream: EventStream = match delay {
327 None => {
328 self.begin_chat_call(attempt).await;
329 Box::pin(self.llm.stream_response(&self.context).map(StreamEvent::Llm))
330 }
331 Some(delay) => {
332 self.emit(AgentEvent::Turn(TurnEvent::RetryScheduled {
333 purpose: LlmCallPurpose::Chat,
334 attempt,
335 max_attempts: self.retry_config.max_attempts,
336 delay_ms: u64::try_from(delay.as_millis()).unwrap_or(u64::MAX),
337 }))
338 .await;
339 let llm = Arc::clone(&self.llm);
340 let context = self.context.clone();
341 Box::pin(async_stream::stream! {
342 sleep(delay).await;
343 yield StreamEvent::LlmRequestStarted { attempt };
344 let mut inner = llm.stream_response(&context);
345 while let Some(item) = inner.next().await {
346 yield StreamEvent::Llm(item);
347 }
348 })
349 }
350 };
351 self.streams.insert(StreamKey::Llm, stream);
352 }
353
354 async fn on_llm_error(&mut self, error: LlmError, state: &mut IterationState) {
355 let will_retry = error.is_retryable() && state.retry_attempt < self.retry_config.max_attempts;
356 let error_message = error.to_string();
357 self.finish_chat_call(LlmCallOutcome::Failed { error: error_message.clone(), will_retry }).await;
358
359 if !will_retry {
360 self.finish_turn(TurnOutcome::Failed { error: error_message }).await;
361 return;
362 }
363
364 state.retry_attempt += 1;
365 let delay = self.retry_config.compute_delay(state.retry_attempt);
366
367 tracing::warn!(
368 attempt = state.retry_attempt,
369 max_attempts = self.retry_config.max_attempts,
370 delay_ms = u64::try_from(delay.as_millis()).unwrap_or(u64::MAX),
371 error = %error,
372 "Retrying LLM request after transient failure"
373 );
374
375 self.active_requests.clear();
378 self.start_llm_stream(Some(delay), state.retry_attempt).await;
379 }
380
381 fn is_busy(&self) -> bool {
382 self.streams.contains_key(&StreamKey::Llm)
383 || self.streams.contains_key(&StreamKey::Compaction)
384 || !self.active_requests.is_empty()
385 }
386
387 async fn abort_in_flight_work(&mut self) {
388 if self.llm_call_active {
389 self.finish_chat_call(LlmCallOutcome::Cancelled).await;
390 }
391 if self.streams.remove(&StreamKey::Compaction).is_some() {
392 self.emit(AgentEvent::Turn(TurnEvent::LlmCallEnded {
393 purpose: LlmCallPurpose::Compaction,
394 outcome: LlmCallOutcome::Cancelled,
395 }))
396 .await;
397 self.emit(AgentEvent::Context(ContextEvent::CompactionEnded { outcome: CompactionOutcome::Cancelled }))
398 .await;
399 }
400 self.streams.remove(&StreamKey::Llm);
401 for tool_id in self.active_requests.keys().cloned().collect::<Vec<_>>() {
402 self.streams.remove(&StreamKey::Tool(tool_id));
403 }
404 self.active_requests.clear();
405 }
406
407 fn inject_continuation_prompt(&mut self, previous_response: &str, stop_reason: Option<&StopReason>) {
409 if !previous_response.is_empty() {
410 self.context.add_message(ChatMessage::Assistant {
411 content: previous_response.to_string(),
412 reasoning: AssistantReasoning::default(),
413 timestamp: IsoString::now(),
414 tool_calls: Vec::new(),
415 });
416 }
417
418 let reason = stop_reason.map_or_else(|| "Unknown".to_string(), |reason| format!("{reason:?}"));
419
420 self.context.add_message(ChatMessage::User {
421 content: vec![llm::ContentBlock::text(format!(
422 "<system-notification>The LLM API stopped with reason '{reason}'. Continue from where you left off and finish your task.</system-notification>"
423 ))],
424 timestamp: IsoString::now(),
425 });
426 }
427
428 async fn on_llm_event(&mut self, result: Result<LlmResponse, LlmError>, state: &mut IterationState) {
429 use LlmResponse::{
430 Done, EncryptedReasoning, Error, Reasoning, Start, Text, ToolRequestArg, ToolRequestComplete,
431 ToolRequestStart, Usage,
432 };
433
434 let response = match result {
435 Ok(response) => response,
436 Err(e) => {
437 self.on_llm_error(e, state).await;
438 return;
439 }
440 };
441
442 match response {
443 Start { message_id } => {
444 state.on_llm_start(message_id);
445 }
446
447 Text { chunk } => {
448 self.handle_llm_text(chunk, state).await;
449 }
450
451 Reasoning { chunk } => {
452 state.reasoning_summary_text.push_str(&chunk);
453 if let Some(id) = state.current_message_id.clone() {
454 self.emit(AgentEvent::thought(&id, &chunk, false)).await;
455 }
456 }
457
458 EncryptedReasoning { id, content } => {
459 if let Some(model) = self.llm.model() {
460 state.encrypted_reasoning = Some(EncryptedReasoningContent { id, model, content });
461 }
462 }
463
464 ToolRequestStart { id, name } => {
465 self.handle_tool_request_start(id, name).await;
466 }
467
468 ToolRequestArg { id, chunk } => {
469 self.handle_tool_request_arg(id, chunk).await;
470 }
471
472 ToolRequestComplete { tool_call } => {
473 self.handle_tool_completion(tool_call, state).await;
474 }
475
476 Done { stop_reason } => {
477 state.llm_done = true;
478 state.stop_reason = stop_reason;
479 self.finish_chat_call(LlmCallOutcome::Completed {
480 stop_reason: state.stop_reason.clone(),
481 usage: state.call_usage.take(),
482 })
483 .await;
484 }
485
486 Error { message } => {
487 self.finish_chat_call(LlmCallOutcome::Failed { error: message.clone(), will_retry: false }).await;
488 self.finish_turn(TurnOutcome::Failed { error: message }).await;
489 }
490
491 Usage { tokens: sample } => {
492 self.handle_llm_usage(sample, state).await;
493 }
494 }
495 }
496
497 async fn handle_llm_text(&mut self, chunk: String, state: &mut IterationState) {
498 state.message_content.push_str(&chunk);
499
500 if let Some(id) = state.current_message_id.clone() {
501 self.emit(AgentEvent::text(&id, &chunk, false)).await;
502 }
503 }
504
505 async fn handle_tool_request_start(&mut self, id: String, name: String) {
506 let request = ToolCallRequest { id: id.clone(), name, arguments: String::new() };
507 self.active_requests.insert(id, request.clone());
508
509 self.emit(AgentEvent::Tool(ToolEvent::Call { request })).await;
510 }
511
512 async fn handle_tool_request_arg(&mut self, id: String, chunk: String) {
513 let Some(request) = self.active_requests.get_mut(&id) else {
514 return;
515 };
516 request.arguments.push_str(&chunk);
517
518 self.emit(AgentEvent::Tool(ToolEvent::CallUpdate { tool_call_id: id, chunk })).await;
519 }
520
521 async fn handle_tool_completion(&mut self, tool_call: ToolCallRequest, state: &mut IterationState) {
522 state.pending_tool_ids.insert(tool_call.id.clone());
523 debug_assert!(
524 self.active_requests.contains_key(&tool_call.id),
525 "tool call {} should already be in active_requests from handle_tool_request_start",
526 tool_call.id
527 );
528
529 let (tx, rx) = mpsc::channel(100);
530 let stream = ReceiverStream::new(rx).map(StreamEvent::ToolExecution);
531 self.streams.insert(StreamKey::Tool(tool_call.id.clone()), Box::pin(stream));
532
533 if let Some(ref mcp_command_tx) = self.mcp_command_tx {
534 let mcp_future =
535 mcp_command_tx.send(McpCommand::ExecuteTool { request: tool_call, timeout: self.tool_timeout, tx });
536 if let Err(e) = mcp_future.await {
537 tracing::warn!("Failed to send tool request to MCP task: {:?}", e);
538 }
539 }
540 }
541
542 async fn handle_llm_usage(&mut self, sample: TokenUsage, state: &mut IterationState) {
543 state.call_usage = Some(sample);
544 self.token_tracker.record_usage(sample);
545 let ratio_pct = self.token_tracker.usage_ratio().map(|r| r * 100.0);
546 let remaining = self.token_tracker.tokens_remaining();
547 tracing::debug!(?sample, ?ratio_pct, ?remaining, "Token usage");
548
549 self.emit(self.context_usage_message()).await;
550 }
551
552 fn context_usage_message(&self) -> AgentEvent {
553 let last = self.token_tracker.last_usage();
554 AgentEvent::Context(ContextEvent::UsageUpdated {
555 usage: ContextUsage {
556 usage_ratio: self.token_tracker.usage_ratio(),
557 context_limit: self.token_tracker.context_limit(),
558 input_tokens: last.input_tokens,
559 output_tokens: last.output_tokens,
560 cache_read_tokens: last.cache_read_tokens,
561 cache_creation_tokens: last.cache_creation_tokens,
562 reasoning_tokens: last.reasoning_tokens,
563 total_input_tokens: self.token_tracker.total_input_tokens(),
564 total_output_tokens: self.token_tracker.total_output_tokens(),
565 total_cache_read_tokens: self.token_tracker.total_cache_read_tokens(),
566 total_cache_creation_tokens: self.token_tracker.total_cache_creation_tokens(),
567 total_reasoning_tokens: self.token_tracker.total_reasoning_tokens(),
568 },
569 })
570 }
571
572 fn compaction_needed(&self) -> bool {
573 self.compaction_config.as_ref().is_some_and(|config| {
574 self.token_tracker.needs_compaction(self.context.estimated_token_count(), config.threshold)
575 })
576 }
577
578 async fn begin_compaction(&mut self) {
579 tracing::info!("Starting context compaction - {} messages", self.context.message_count());
580 self.emit(AgentEvent::Context(ContextEvent::CompactionStarted { message_count: self.context.message_count() }))
581 .await;
582 self.emit(self.llm_call_started(LlmCallPurpose::Compaction, 0)).await;
583
584 let compactor = Compactor::new(self.llm.clone());
585 let context = self.context.clone();
586 let stream: EventStream =
587 Box::pin(futures::stream::once(async move { StreamEvent::Compaction(compactor.compact(context).await) }));
588 self.streams.insert(StreamKey::Compaction, stream);
589 }
590
591 async fn on_compaction_complete(&mut self, result: Result<CompactionResult, CompactionError>) {
592 let outcome = match &result {
593 Ok(result) => LlmCallOutcome::Completed { stop_reason: None, usage: result.usage },
594 Err(e) => LlmCallOutcome::Failed { error: e.to_string(), will_retry: false },
595 };
596 self.emit(AgentEvent::Turn(TurnEvent::LlmCallEnded { purpose: LlmCallPurpose::Compaction, outcome })).await;
597
598 match result {
599 Ok(result) => {
600 tracing::info!("Context compacted: {} messages removed", result.messages_removed);
601 self.context = self.context.with_compacted_summary(&result.summary);
602 self.token_tracker.reset_current_usage();
603 self.emit(AgentEvent::Context(ContextEvent::CompactionResult {
604 summary: result.summary,
605 messages_removed: result.messages_removed,
606 }))
607 .await;
608 self.emit(AgentEvent::Context(ContextEvent::CompactionEnded { outcome: CompactionOutcome::Completed }))
609 .await;
610 }
611 Err(e) => {
612 tracing::warn!("Context compaction failed: {e}");
613 self.emit(AgentEvent::Context(ContextEvent::CompactionEnded {
614 outcome: CompactionOutcome::Failed { error: e.to_string() },
615 }))
616 .await;
617 }
618 }
619
620 self.start_chat_turn().await;
621 }
622
623 async fn on_tool_execution_event(&mut self, event: ToolExecutionEvent, state: &mut IterationState) {
624 match event {
625 ToolExecutionEvent::Started { tool_id, tool_name } => {
626 tracing::debug!("Tool execution started: {} ({})", tool_name, tool_id);
627 self.emit(AgentEvent::Tool(ToolEvent::ExecutionStarted { tool_id, tool_name })).await;
628 }
629
630 ToolExecutionEvent::Progress { tool_id, progress } => {
631 tracing::debug!(
632 "Tool progress for {}: {}/{}",
633 tool_id,
634 progress.progress,
635 progress.total.unwrap_or(0.0)
636 );
637
638 if let Some(request) = self.active_requests.get(&tool_id).cloned() {
639 self.emit(AgentEvent::Tool(ToolEvent::Progress {
640 request,
641 progress: progress.progress,
642 total: progress.total,
643 message: progress.message.clone(),
644 }))
645 .await;
646 }
647 }
648
649 ToolExecutionEvent::Complete { tool_id: _, result, result_meta } => match result {
650 Ok(tool_result) => {
651 tracing::debug!("Tool result received: {} -> {}", tool_result.name, tool_result.result.len());
652
653 if state.pending_tool_ids.remove(&tool_result.id) {
654 self.active_requests.remove(&tool_result.id);
655 state.completed_tool_calls.push(Ok(tool_result.clone()));
656
657 self.emit(AgentEvent::Tool(ToolEvent::Result { result: tool_result, result_meta })).await;
658 } else {
659 tracing::debug!("Ignoring stale tool result for id: {}", tool_result.id);
660 }
661 }
662
663 Err(tool_error) => {
664 if state.pending_tool_ids.remove(&tool_error.id) {
665 self.active_requests.remove(&tool_error.id);
666 state.completed_tool_calls.push(Err(tool_error.clone()));
667
668 self.emit(AgentEvent::Tool(ToolEvent::Error { error: tool_error })).await;
669 }
670 }
671 },
672 }
673 }
674
675 fn refresh_prompt_cache_key(&mut self) {
676 let key = derive_prompt_cache_key(self.llm.as_ref(), &self.context);
677 self.context.set_prompt_cache_key(Some(key));
678 }
679
680 fn update_context(
681 &mut self,
682 message_content: &str,
683 reasoning: AssistantReasoning,
684 completed_tools: Vec<Result<ToolCallResult, ToolCallError>>,
685 ) {
686 self.context.push_assistant_turn(message_content, reasoning, completed_tools);
687 }
688
689 fn commit_pending_turn_messages(&mut self) {
690 let content: Vec<_> = self.pending_turn_messages.drain(..).flatten().collect();
691 if !content.is_empty() {
692 self.context.add_message(ChatMessage::User { content, timestamp: IsoString::now() });
693 }
694 }
695
696 async fn emit_tool_definitions(&mut self) {
697 let tools = self.context.tools().clone();
698 if !tools.is_empty() {
699 self.emit(AgentEvent::Tool(ToolEvent::DefinitionsUpdated { tools })).await;
700 }
701 }
702
703 async fn emit(&mut self, message: AgentEvent) {
704 for observer in &mut self.observers {
705 observer.on_event(&message);
706 }
707
708 if let Err(e) = self.message_tx.send(message).await {
709 tracing::warn!("Failed to send agent message: {e:?}");
710 }
711 }
712
713 async fn finish_turn(&mut self, outcome: TurnOutcome) {
714 if std::mem::take(&mut self.turn_active) {
715 self.emit(AgentEvent::turn_ended(outcome)).await;
716 }
717 }
718
719 async fn begin_chat_call(&mut self, attempt: u32) {
720 self.llm_call_active = true;
721 self.emit(self.llm_call_started(LlmCallPurpose::Chat, attempt)).await;
722 }
723
724 async fn finish_chat_call(&mut self, outcome: LlmCallOutcome) {
725 if std::mem::take(&mut self.llm_call_active) {
726 self.emit(AgentEvent::Turn(TurnEvent::LlmCallEnded { purpose: LlmCallPurpose::Chat, outcome })).await;
727 }
728 }
729
730 fn llm_call_started(&self, purpose: LlmCallPurpose, attempt: u32) -> AgentEvent {
731 let model = self.llm.model();
732 AgentEvent::Turn(TurnEvent::LlmCallStarted {
733 purpose,
734 provider: model.as_ref().map(|m| m.provider().to_string()),
735 model: model.map(|m| m.model_id().into_owned()),
736 display_name: self.llm.display_name(),
737 attempt,
738 max_attempts: self.retry_config.max_attempts,
739 })
740 }
741}
742pub(crate) struct AutoContinue {
743 max: u32,
744 count: u32,
745}
746
747impl AutoContinue {
748 pub(crate) fn new(max: u32) -> Self {
749 Self { max, count: 0 }
750 }
751
752 fn reset(&mut self) {
753 self.count = 0;
754 }
755
756 fn should_continue(&self, stop_reason: Option<&StopReason>) -> bool {
757 matches!(stop_reason, Some(StopReason::Length)) && self.count < self.max
758 }
759
760 fn advance(&mut self) {
761 self.count += 1;
762 }
763
764 fn count(&self) -> u32 {
765 self.count
766 }
767
768 fn max(&self) -> u32 {
769 self.max
770 }
771}
772
773#[derive(Debug)]
774struct IterationState {
775 current_message_id: Option<String>,
776 message_content: String,
777 reasoning_summary_text: String,
778 encrypted_reasoning: Option<EncryptedReasoningContent>,
779 pending_tool_ids: HashSet<String>,
780 completed_tool_calls: Vec<Result<ToolCallResult, ToolCallError>>,
781 llm_done: bool,
782 stop_reason: Option<StopReason>,
783 retry_attempt: u32,
784 call_usage: Option<TokenUsage>,
785}
786
787impl IterationState {
788 fn new() -> Self {
789 Self {
790 current_message_id: None,
791 message_content: String::new(),
792 reasoning_summary_text: String::new(),
793 encrypted_reasoning: None,
794 pending_tool_ids: HashSet::new(),
795 completed_tool_calls: Vec::new(),
796 llm_done: false,
797 stop_reason: None,
798 retry_attempt: 0,
799 call_usage: None,
800 }
801 }
802
803 fn on_llm_start(&mut self, message_id: String) {
804 self.current_message_id = Some(message_id);
805 self.message_content.clear();
806 self.reasoning_summary_text.clear();
807 self.encrypted_reasoning = None;
808 self.stop_reason = None;
809 self.call_usage = None;
810 }
811
812 fn is_complete(&self) -> bool {
813 self.llm_done && self.pending_tool_ids.is_empty()
814 }
815}