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