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