Skip to main content

matrixcode_core/
agent.rs

1//! Agent Core - Full Event-driven Implementation
2//!
3//! Complete agent with streaming, tool execution loop, and event output.
4
5use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
6use std::sync::Arc;
7use anyhow::Result;
8use tokio::sync::mpsc;
9
10use crate::event::{AgentEvent, EventType, EventData};
11use crate::providers::{ChatRequest, ChatResponse, ContentBlock, Message, MessageContent, Provider, Role, StopReason, Usage};
12use crate::tools::{Tool, ToolDefinition};
13use crate::approval::{ApproveMode, needs_approval};
14use crate::compress::{CompressionConfig, should_compress};
15use crate::cancel::CancellationToken;
16
17const MAX_ITERATIONS: usize = 50;
18
19/// Full Agent with event output
20#[allow(dead_code)]  // Some fields are for future features
21pub struct Agent {
22    provider: Box<dyn Provider>,
23    model_name: String,  // For debug logging
24    tools: Vec<Arc<dyn Tool>>,
25    messages: Vec<Message>,
26    system_prompt: String,
27    max_tokens: u32,
28    think: bool,
29    approve_mode: Arc<AtomicU8>,
30    event_tx: mpsc::Sender<AgentEvent>,
31    
32    // New fields
33    skills: Vec<crate::skills::Skill>,
34    profile: crate::prompt::PromptProfile,
35    project_overview: Option<String>,
36    memory_summary: Option<String>,
37    
38    // State tracking
39    total_input_tokens: AtomicU64,
40    total_output_tokens: AtomicU64,
41    /// The most recent API call's input_tokens — represents actual context window usage.
42    last_input_tokens: AtomicU64,
43    cancel_token: Option<CancellationToken>,
44    compression_config: CompressionConfig,
45    
46    // Ask tool channel: receives user answers from TUI
47    ask_rx: Option<mpsc::Receiver<String>>,
48}
49
50/// Agent builder
51pub struct AgentBuilder {
52    provider: Box<dyn Provider>,
53    model_name: String,
54    tools: Vec<Arc<dyn Tool>>,
55    system_prompt: String,
56    max_tokens: u32,
57    think: bool,
58    approve_mode: ApproveMode,
59    event_tx: Option<mpsc::Sender<AgentEvent>>,
60    // New fields
61    skills: Vec<crate::skills::Skill>,
62    profile: crate::prompt::PromptProfile,
63    project_overview: Option<String>,
64    memory_summary: Option<String>,
65}
66
67impl AgentBuilder {
68    pub fn new(provider: Box<dyn Provider>) -> Self {
69        Self {
70            provider,
71            model_name: "unknown".to_string(),
72            tools: Vec::new(),
73            system_prompt: "You are a helpful AI coding assistant.".to_string(),
74            max_tokens: 4096,
75            think: false,
76            approve_mode: ApproveMode::Ask,
77            event_tx: None,
78            skills: Vec::new(),
79            profile: crate::prompt::PromptProfile::Default,
80            project_overview: None,
81            memory_summary: None,
82        }
83    }
84
85    pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
86        self.system_prompt = prompt.into();
87        self
88    }
89
90    pub fn model_name(mut self, name: impl Into<String>) -> Self {
91        self.model_name = name.into();
92        self
93    }
94
95    pub fn max_tokens(mut self, tokens: u32) -> Self {
96        self.max_tokens = tokens;
97        self
98    }
99
100    pub fn think(mut self, enabled: bool) -> Self {
101        self.think = enabled;
102        self
103    }
104
105    pub fn approve_mode(mut self, mode: ApproveMode) -> Self {
106        self.approve_mode = mode;
107        self
108    }
109
110    pub fn tool(mut self, tool: Arc<dyn Tool>) -> Self {
111        self.tools.push(tool);
112        self
113    }
114
115    /// Add multiple tools
116    pub fn tools(mut self, tools: Vec<Box<dyn Tool>>) -> Self {
117        self.tools.extend(tools.into_iter().map(Arc::from));
118        self
119    }
120
121    /// Set external event sender for streaming events
122    pub fn event_tx(mut self, tx: mpsc::Sender<AgentEvent>) -> Self {
123        self.event_tx = Some(tx);
124        self
125    }
126
127    /// Add skills
128    pub fn skills(mut self, skills: Vec<crate::skills::Skill>) -> Self {
129        self.skills = skills;
130        self
131    }
132
133    /// Set prompt profile
134    pub fn profile(mut self, profile: crate::prompt::PromptProfile) -> Self {
135        self.profile = profile;
136        self
137    }
138
139    /// Set project overview
140    pub fn overview(mut self, overview: impl Into<String>) -> Self {
141        self.project_overview = Some(overview.into());
142        self
143    }
144
145    /// Set memory summary
146    pub fn memory(mut self, summary: impl Into<String>) -> Self {
147        self.memory_summary = Some(summary.into());
148        self
149    }
150
151    pub fn build(self) -> Agent {
152        Agent::new(self)
153    }
154}
155
156impl Agent {
157    fn new(builder: AgentBuilder) -> Self {
158        // Use external event_tx if provided, otherwise create internal one
159        let event_tx = builder.event_tx.unwrap_or_else(|| {
160            let (tx, _) = mpsc::channel(100);
161            tx
162        });
163
164        Self {
165            provider: builder.provider,
166            model_name: builder.model_name,
167            tools: builder.tools,
168            messages: Vec::new(),
169            system_prompt: builder.system_prompt,
170            max_tokens: builder.max_tokens,
171            think: builder.think,
172            approve_mode: Arc::new(AtomicU8::new(builder.approve_mode.to_u8())),
173            event_tx,
174            skills: builder.skills,
175            profile: builder.profile,
176            project_overview: builder.project_overview,
177            memory_summary: builder.memory_summary,
178            total_input_tokens: AtomicU64::new(0),
179            total_output_tokens: AtomicU64::new(0),
180            last_input_tokens: AtomicU64::new(0),
181            cancel_token: None,
182            compression_config: CompressionConfig::default(),
183            ask_rx: None,
184        }
185    }
186
187    /// Get event sender for streaming
188    pub fn event_sender(&self) -> mpsc::Sender<AgentEvent> {
189        self.event_tx.clone()
190    }
191
192    /// Set ask response channel (for TUI mode)
193    pub fn set_ask_channel(&mut self, rx: mpsc::Receiver<String>) {
194        self.ask_rx = Some(rx);
195    }
196
197    /// Set cancellation token
198    pub fn set_cancel_token(&mut self, token: CancellationToken) {
199        self.cancel_token = Some(token);
200    }
201
202    /// Set approve mode at runtime
203    pub fn set_approve_mode(&mut self, mode: ApproveMode) {
204        let old = ApproveMode::from_u8(self.approve_mode.load(Ordering::Relaxed));
205        log::info!("Agent approve mode changed: {} -> {}", old, mode);
206        self.approve_mode.store(mode.to_u8(), Ordering::Relaxed);
207    }
208
209    /// Get a shared reference to the approve mode atomic.
210    /// TUI can hold this and update it directly, even while agent is running.
211    pub fn approve_mode_shared(&self) -> Arc<AtomicU8> {
212        self.approve_mode.clone()
213    }
214
215    /// Replace the internal approve mode with an externally-created shared atomic.
216    /// Replace the internal approve mode with an externally-created shared atomic.
217    /// This allows the TUI to update the mode while the agent is running.
218    pub fn set_approve_mode_shared(&mut self, shared: Arc<AtomicU8>) {
219        self.approve_mode = shared;
220    }
221
222    /// Update memory summary and rebuild system prompt.
223    /// This is called before each turn to use context-aware memory retrieval.
224    pub fn update_memory_summary(&mut self, summary: Option<String>) {
225        self.memory_summary = summary;
226        // Rebuild system prompt with new memory summary
227        self.system_prompt = crate::prompt::build_system_prompt(
228            &self.profile,
229            &self.skills,
230            self.project_overview.as_deref(),
231            self.memory_summary.as_deref(),
232        );
233    }
234
235    /// Run chat loop with tool execution (streaming version).
236    /// Events are emitted via the `event_tx` channel in real-time.
237    /// The returned Vec is intentionally empty — callers should consume
238    /// events from the channel instead.
239    pub async fn run(&mut self, user_input: String) -> Result<Vec<AgentEvent>> {
240        // Send session started
241        self.emit(AgentEvent::session_started())?;
242
243        // Add user message
244        self.messages.push(Message {
245            role: Role::User,
246            content: MessageContent::Text(user_input.clone()),
247        });
248
249        // Run agent loop (handle tool_use iterations)
250        let mut iterations = 0;
251        let mut should_continue = true;
252
253        while should_continue && iterations < MAX_ITERATIONS {
254            iterations += 1;
255            
256            // Check cancellation
257            if let Some(token) = &self.cancel_token
258                && token.is_cancelled()
259            {
260                self.emit(AgentEvent::error("Operation cancelled".to_string(), None, None))?;
261                break;
262            }
263
264            // Build request
265            let tool_defs: Vec<ToolDefinition> = self.tools.iter().map(|t| t.definition()).collect();
266            let request = ChatRequest {
267                system: Some(self.system_prompt.clone()),
268                messages: self.messages.clone(),
269                max_tokens: self.max_tokens,
270                tools: tool_defs,
271                think: self.think,
272                enable_caching: true,
273                server_tools: Vec::new(),
274            };
275
276            // Call provider with streaming
277
278            // Use streaming API for real-time output
279            let response = self.call_streaming(&request).await?;
280
281            // Track usage
282            self.track_usage(&response.usage);
283
284            // Debug log: API call
285            crate::debug::debug_log().api_call(
286                &self.model_name,
287                response.usage.input_tokens,
288                response.usage.cache_read_input_tokens > 0
289            );
290
291            // Process response
292            should_continue = self.process_response(&response).await?;
293
294            // Check compression (use last_input_tokens = actual context window usage)
295            let context_size = self.provider.context_size();
296
297            // Use API-reported tokens as primary source (should be accurate)
298            // Only estimate if API returns 0 or unreasonable value
299            let api_tokens = self.last_input_tokens.load(Ordering::Relaxed) as u32;
300            let estimated_tokens = crate::compress::estimate_total_tokens(&self.messages);
301
302            // Trust API tokens unless it's clearly wrong (0 or way less than estimate)
303            let current_tokens = if api_tokens > 0 && api_tokens >= estimated_tokens / 2 {
304                api_tokens  // API reported reasonable value, use it
305            } else {
306                estimated_tokens  // API returned 0 or suspiciously low, use estimate
307            };
308
309            // Debug: log compression check
310            crate::debug::debug_log().log(
311                "compression",
312                &format!("check: api={}, estimated={}, using={}, context={}, threshold={}",
313                    api_tokens, estimated_tokens, current_tokens, context_size.unwrap_or(0), self.compression_config.threshold)
314            );
315            
316            if should_compress(current_tokens, context_size, &self.compression_config) {
317                self.emit(AgentEvent::progress("Compressing context...", None))?;
318                
319                let _original_count = self.messages.len();
320                let original_tokens = current_tokens;
321                
322                // Use sliding window compression (no AI needed)
323                match crate::compress::compress_messages(
324                    &self.messages,
325                    crate::compress::CompressionStrategy::SlidingWindow,
326                    &self.compression_config,
327                ) {
328                    Ok(compressed) => {
329                        let compressed_tokens = crate::compress::estimate_total_tokens(&compressed);
330                        self.messages = compressed;
331                        self.total_input_tokens.store(compressed_tokens as u64, Ordering::Relaxed);
332                        self.last_input_tokens.store(compressed_tokens as u64, Ordering::Relaxed);
333                        
334                        // Debug log: compression
335                        let ratio = compressed_tokens as f32 / original_tokens as f32;
336                        crate::debug::debug_log().compression(original_tokens, compressed_tokens, ratio);
337                        
338                        self.emit(AgentEvent::with_data(
339                            crate::event::EventType::CompressionCompleted,
340                            crate::event::EventData::Compression {
341                                original_tokens: original_tokens as u64,
342                                compressed_tokens: compressed_tokens as u64,
343                                ratio: compressed_tokens as f32 / original_tokens as f32,
344                            },
345                        ))?;
346                    }
347                    Err(e) => {
348                        self.emit(AgentEvent::progress(
349                            format!("Compression failed: {}", e),
350                            None,
351                        ))?;
352                    }
353                }
354            }
355        }
356
357        // Send final usage stats (use total_input_tokens for cumulative context display)
358        self.emit(AgentEvent::usage_with_cache(
359            self.total_input_tokens.load(Ordering::Relaxed),
360            self.total_output_tokens.load(Ordering::Relaxed),
361            0, 0,  // Cache info already sent per-request
362        ))?;
363
364        // Send session ended
365        self.emit(AgentEvent::session_ended())?;
366
367        Ok(Vec::new())
368    }
369
370    /// Call provider with streaming and emit events in real-time
371    async fn call_streaming(&mut self, request: &ChatRequest) -> Result<ChatResponse> {
372        use crate::providers::StreamEvent;
373        
374        const MAX_RETRIES: u32 = 5;
375        const RETRY_DELAY_MS: u64 = 1000;  // 1 second base delay
376        
377        let mut attempt = 0;
378        
379        loop {
380            attempt += 1;
381            
382            // Check cancellation before starting stream
383            if let Some(token) = &self.cancel_token && token.is_cancelled() {
384                return Err(anyhow::anyhow!("Operation cancelled"));
385            }
386            
387            // Try to start streaming
388            let rx_result = self.provider.chat_stream(request.clone()).await;
389            
390            match rx_result {
391                Ok(mut rx) => {
392                    // Successfully started streaming
393                    let mut response_content: Vec<ContentBlock> = Vec::new();
394                    let mut current_text = String::new();
395                    let mut current_thinking = String::new();
396                    let mut usage = Usage {
397                        input_tokens: 0,
398                        output_tokens: 0,
399                        cache_creation_input_tokens: 0,
400                        cache_read_input_tokens: 0,
401                    };
402                    let mut should_retry = false;
403
404                    // Use select! to check cancellation while streaming
405                    loop {
406                        // Check cancellation periodically
407                        if let Some(token) = &self.cancel_token && token.is_cancelled() {
408                            return Err(anyhow::anyhow!("Operation cancelled"));
409                        }
410                        
411                        // Try to receive with a small timeout to allow cancellation check
412                        let event = tokio::select! {
413                            event = rx.recv() => event,
414                            _ = tokio::time::sleep(tokio::time::Duration::from_millis(100)) => {
415                                // Timeout - continue loop to check cancellation
416                                continue;
417                            }
418                        };
419                        
420                        match event {
421                            None => {
422                                // Stream ended
423                                break;
424                            }
425                            Some(StreamEvent::FirstByte) => {
426                                // First byte received, streaming starts
427                            }
428                            Some(StreamEvent::ThinkingDelta(delta)) => {
429                                if current_thinking.is_empty() {
430                                    self.emit(AgentEvent::thinking_start())?;
431                                }
432                                current_thinking.push_str(&delta);
433                                self.emit(AgentEvent::thinking_delta(delta, None))?;
434                            }
435                            Some(StreamEvent::TextDelta(delta)) => {
436                                if current_text.is_empty() {
437                                    self.emit(AgentEvent::text_start())?;
438                                }
439                                current_text.push_str(&delta);
440                                self.emit(AgentEvent::text_delta(delta))?;
441                            }
442                            Some(StreamEvent::ToolUseStart { id, name }) => {
443                                // Finish any pending thinking first
444                                if !current_thinking.is_empty() {
445                                    self.emit(AgentEvent::thinking_end())?;
446                                    response_content.push(ContentBlock::Thinking {
447                                        thinking: current_thinking.clone(),
448                                        signature: None,
449                                    });
450                                    current_thinking.clear();
451                                }
452                                // Then finish any pending text
453                                if !current_text.is_empty() {
454                                    self.emit(AgentEvent::text_end())?;
455                                    response_content.push(ContentBlock::Text { text: current_text.clone() });
456                                    current_text.clear();
457                                }
458                                self.emit(AgentEvent::tool_use_start(&id, &name, None))?;
459                            }
460                            Some(StreamEvent::ToolInputDelta { bytes_so_far: _ }) => {
461                                // Tool input progress - could emit progress event
462                            }
463                            Some(StreamEvent::Usage { output_tokens }) => {
464                                // Real-time usage update - emit to TUI and update internal usage
465                                self.emit(AgentEvent::usage_with_cache(
466                                    0,  // input_tokens not available in stream
467                                    output_tokens as u64,
468                                    0, 0  // cache info not available in stream
469                                ))?;
470                                usage.output_tokens = output_tokens;
471                            }
472                            Some(StreamEvent::Done(resp)) => {
473                                // Finish any pending thinking first
474                                if !current_thinking.is_empty() {
475                                    self.emit(AgentEvent::thinking_end())?;
476                                    response_content.push(ContentBlock::Thinking {
477                                        thinking: current_thinking.clone(),
478                                        signature: None,
479                                    });
480                                }
481                                // Then finish any pending text
482                                if !current_text.is_empty() {
483                                    self.emit(AgentEvent::text_end())?;
484                                    response_content.push(ContentBlock::Text { text: current_text.clone() });
485                                }
486                                // Add any remaining blocks from response
487                                for block in &resp.content {
488                                    if !response_content.iter().any(|b| b == block) {
489                                        response_content.push(block.clone());
490                                    }
491                                }
492                                usage = resp.usage;
493                            }
494                            Some(StreamEvent::Error(msg)) => {
495                                // Stream error - might be retryable
496                                if attempt < MAX_RETRIES {
497                                    self.emit(AgentEvent::progress(
498                                        format!("⚠️ Stream error, retrying ({}/{}): {}", attempt, MAX_RETRIES, &msg),
499                                        None,
500                                    ))?;
501                                    // Exponential backoff
502                                    let delay = RETRY_DELAY_MS * (1 << (attempt - 1));
503                                    tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
504                                    should_retry = true;
505                                    break;  // Break inner loop to retry in outer loop
506                                } else {
507                                    self.emit(AgentEvent::error(msg.clone(), None, None))?;
508                                    return Err(anyhow::anyhow!("Stream error after {} retries: {}", MAX_RETRIES, msg));
509                                }
510                            }
511                        }
512                    }
513
514                    if should_retry {
515                        continue;  // Retry the outer loop
516                    }
517
518                    return Ok(ChatResponse {
519                        content: response_content,
520                        stop_reason: StopReason::EndTurn,
521                        usage,
522                    });
523                }
524                Err(e) => {
525                    // Failed to start streaming
526                    if attempt < MAX_RETRIES {
527                        let error_msg = e.to_string();
528                        self.emit(AgentEvent::progress(
529                            format!("⚠️ API error, retrying ({}/{}): {}", attempt, MAX_RETRIES, &error_msg),
530                            None,
531                        ))?;
532                        // Exponential backoff: 1s, 2s, 4s, 8s, 16s
533                        let delay = RETRY_DELAY_MS * (1 << (attempt - 1));
534                        tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
535                    } else {
536                        return Err(anyhow::anyhow!("API error after {} retries: {}", MAX_RETRIES, e));
537                    }
538                }
539            }
540        }
541    }
542
543    /// Process response and handle tool_use (Text/Thinking events already sent via streaming)
544    async fn process_response(&mut self, response: &ChatResponse) -> Result<bool> {
545        let mut has_tool_use = false;
546        let mut assistant_content: Vec<ContentBlock> = Vec::new();
547        let mut tool_results: Vec<Message> = Vec::new();
548
549        for block in &response.content {
550            match block {
551                // Text and Thinking events already sent via streaming, just add to history
552                ContentBlock::Text { text } => {
553                    assistant_content.push(ContentBlock::Text { text: text.clone() });
554                }
555
556                ContentBlock::Thinking { thinking, signature } => {
557                    assistant_content.push(ContentBlock::Thinking {
558                        thinking: thinking.clone(),
559                        signature: signature.clone(),
560                    });
561                }
562
563                ContentBlock::ToolUse { id, name, input } => {
564                    // Check cancellation before executing tool
565                    if let Some(token) = &self.cancel_token && token.is_cancelled() {
566                        return Err(anyhow::anyhow!("Operation cancelled"));
567                    }
568                    
569                    has_tool_use = true;
570                    
571                    // Note: ToolUseStart event was already emitted during streaming.
572                    // Here we only emit the input details for tools that need it.
573                    
574                    // Execute tool
575                    let result = self.execute_tool(name, input.clone()).await;
576                    
577                    let (content, is_error) = match result {
578                        Ok(output) => (output, false),
579                        Err(e) => (e.to_string(), true),
580                    };
581
582                    self.emit(AgentEvent::tool_result(id.clone(), name.clone(), extract_tool_detail(name, input), content.clone(), is_error))?;
583
584                    // Add tool_use to assistant content
585                    assistant_content.push(ContentBlock::ToolUse {
586                        id: id.clone(),
587                        name: name.clone(),
588                        input: input.clone(),
589                    });
590
591                    // Collect tool results (will be added after assistant message)
592                    tool_results.push(Message {
593                        role: Role::User,
594                        content: MessageContent::Blocks(vec![ContentBlock::ToolResult {
595                            tool_use_id: id.clone(),
596                            content: format!("{}: {}", if is_error { "Error" } else { "Result" }, content),
597                        }]),
598                    });
599                }
600
601                _ => {}
602            }
603        }
604
605        // Add assistant message to history FIRST
606        if !assistant_content.is_empty() {
607            self.messages.push(Message {
608                role: Role::Assistant,
609                content: MessageContent::Blocks(assistant_content),
610            });
611        }
612
613        // Then add tool results (User messages)
614        for msg in tool_results {
615            self.messages.push(msg);
616        }
617
618        // Continue if there were tool calls
619        Ok(has_tool_use)
620    }
621
622    /// Execute a tool
623    async fn execute_tool(&mut self, name: &str, input: serde_json::Value) -> Result<String> {
624        let tool = self.tools.iter().find(|t| t.definition().name == name);
625
626        if let Some(tool) = tool {
627            // Load current approve mode from shared atomic
628            let current_mode = ApproveMode::from_u8(self.approve_mode.load(Ordering::Relaxed));
629            
630            // Debug: log approval check
631            log::debug!(
632                "Tool '{}' approval check: mode={}, risk={}, needs_approval={}",
633                name, current_mode, tool.risk_level(),
634                needs_approval(current_mode, tool.risk_level())
635            );
636            
637            // Check approval
638            if needs_approval(current_mode, tool.risk_level()) {
639                // Ask user for approval via TUI
640                if self.ask_rx.is_some() {
641                    // Build approval question with tool details
642                    let detail = match name {
643                        "bash" => format!("Command: {}", input["command"].as_str().unwrap_or("?")),
644                        "write" => format!("File: {}", input["path"].as_str().unwrap_or("?")),
645                        "edit" | "multi_edit" => format!("File: {}", input["path"].as_str().unwrap_or("?")),
646                        _ => format!("Tool: {}", name),
647                    };
648                    
649                    let question = format!(
650                        "⚠️ Tool '{}' requires approval (risk: {})\n{}\n\nAllow? (y/n)",
651                        name, tool.risk_level(), detail
652                    );
653                    
654                    // Send approval request to TUI
655                    self.emit(AgentEvent::with_data(
656                        EventType::AskQuestion,
657                        EventData::AskQuestion { question, options: None },
658                    ))?;
659                    
660                    // Wait for user response
661                    if let Some(rx) = &mut self.ask_rx {
662                        match rx.recv().await {
663                            Some(answer) => {
664                                let answer_lower = answer.trim().to_lowercase();
665                                // Check for abort
666                                if matches!(answer_lower.as_str(), "a" | "abort" | "q" | "quit" | "stop") {
667                                    self.emit(AgentEvent::with_data(
668                                        EventType::Error,
669                                        EventData::Error { message: "Aborted by user".into(), code: None, source: None },
670                                    ))?;
671                                    return Err(anyhow::anyhow!("Session aborted by user"));
672                                }
673                                // Check for approval
674                                let approved = matches!(
675                                    answer_lower.as_str(),
676                                    "y" | "yes" | "ok" | "approve" | ""
677                                );
678                                if !approved {
679                                    // Rejected - return error to AI so it can try alternative approach
680                                    return Err(anyhow::anyhow!(
681                                        "Tool '{}' rejected by user (answer: '{}')", name, answer_lower
682                                    ));
683                                }
684                            }
685                            None => {
686                                return Err(anyhow::anyhow!("Approval channel closed"));
687                            }
688                        }
689                    }
690                } else {
691                    // No ask channel - reject dangerous/mutating tools
692                    return Err(anyhow::anyhow!(
693                        "Tool '{}' requires manual approval (risk: {}). Use --approve-mode auto to auto-approve.",
694                        name, tool.risk_level()
695                    ));
696                }
697            }
698
699            // Special handling for "ask" tool in TUI mode
700            if name == "ask" && self.ask_rx.is_some() {
701                let question = input["question"].as_str().unwrap_or("").to_string();
702                let options = input.get("options").cloned();
703                
704                // Send AskQuestion event to TUI
705                self.emit(AgentEvent::with_data(
706                    EventType::AskQuestion,
707                    EventData::AskQuestion { question, options },
708                ))?;
709                
710                // Wait for user answer from TUI
711                if let Some(rx) = &mut self.ask_rx {
712                    match rx.recv().await {
713                        Some(answer) => return Ok(answer),
714                        None => return Err(anyhow::anyhow!("Ask channel closed")),
715                    }
716                }
717            }
718
719            // Execute tool normally
720            self.emit(AgentEvent::progress(format!("Executing: {}", name), None))?;
721            tool.execute(input).await
722        } else {
723            Err(anyhow::anyhow!("Tool '{}' not found", name))
724        }
725    }
726
727    /// Track token usage
728    fn track_usage(&self, usage: &Usage) {
729        self.total_input_tokens.fetch_add(usage.input_tokens as u64, Ordering::Relaxed);
730        self.total_output_tokens.fetch_add(usage.output_tokens as u64, Ordering::Relaxed);
731        // Store the latest request's input tokens — this is the actual context window usage.
732        self.last_input_tokens.store(usage.input_tokens as u64, Ordering::Relaxed);
733
734        // Debug: log usage tracking
735        crate::debug::debug_log().log(
736            "usage",
737            &format!("tracked: input_tokens={}, output_tokens={}, cache_read={}, cache_created={}",
738                usage.input_tokens, usage.output_tokens, usage.cache_read_input_tokens, usage.cache_creation_input_tokens)
739        );
740
741        // Emit usage event with cache info (use total_input_tokens for cumulative context display)
742        let _ = self.event_tx.try_send(AgentEvent::usage_with_cache(
743            self.total_input_tokens.load(Ordering::Relaxed),
744            usage.output_tokens as u64,
745            usage.cache_read_input_tokens as u64,
746            usage.cache_creation_input_tokens as u64,
747        ));
748    }
749
750    /// Estimate context size
751    #[allow(dead_code)]
752    fn estimate_context_size(&self) -> u32 {
753        // Rough estimate: each message ~100 tokens average
754        (self.messages.len() as u32) * 100 + self.total_input_tokens.load(Ordering::Relaxed) as u32
755    }
756
757    /// Emit event (non-blocking)
758    fn emit(&self, event: AgentEvent) -> Result<()> {
759        // Use try_send to avoid blocking in async context
760        match self.event_tx.try_send(event) {
761            Ok(_) => Ok(()),
762            Err(mpsc::error::TrySendError::Full(_)) => {
763                // Channel full, drop event - not critical
764                Ok(())
765            }
766            Err(mpsc::error::TrySendError::Closed(_)) => {
767                // Channel closed, receiver dropped
768                Err(anyhow::anyhow!("Event channel closed"))
769            }
770        }
771    }
772
773    /// Restore message history (for session continue/resume)
774    pub fn set_messages(&mut self, messages: Vec<Message>) {
775        self.messages = messages;
776    }
777
778    /// Get current messages (for session saving)
779    pub fn get_messages(&self) -> &[Message] {
780        &self.messages
781    }
782
783    /// Get current token counts
784    pub fn get_token_counts(&self) -> (u64, u64) {
785        (
786            self.total_input_tokens.load(Ordering::Relaxed),
787            self.total_output_tokens.load(Ordering::Relaxed),
788        )
789    }
790
791    /// Clear message history
792    pub fn clear_history(&mut self) {
793        self.messages.clear();
794        self.total_input_tokens.store(0, Ordering::Relaxed);
795        self.total_output_tokens.store(0, Ordering::Relaxed);
796        self.last_input_tokens.store(0, Ordering::Relaxed);
797    }
798
799    /// Get message count
800    pub fn message_count(&self) -> usize {
801        self.messages.len()
802    }
803}
804
805/// Extract tool detail for display (what the tool is doing)
806fn extract_tool_detail(tool_name: &str, input: &serde_json::Value) -> Option<String> {
807    match tool_name.to_lowercase().as_str() {
808        "read" => input.get("path").and_then(|v| v.as_str())
809            .map(|s| truncate_str(s, 50)),
810        "write" => input.get("path").and_then(|v| v.as_str())
811            .map(|s| truncate_str(s, 50)),
812        "edit" | "multi_edit" => {
813            let path = input.get("path").and_then(|v| v.as_str());
814            let old = input.get("old_string").and_then(|v| v.as_str());
815            match (path, old) {
816                (Some(p), Some(o)) => Some(format!("{}: \"{}\"", truncate_str(p, 30), truncate_str(o, 20))),
817                (Some(p), None) => Some(truncate_str(p, 50)),
818                _ => None,
819            }
820        }
821        "bash" => input.get("command").and_then(|v| v.as_str())
822            .map(|s| truncate_str(s, 60)),
823        "search" | "grep" => input.get("pattern").and_then(|v| v.as_str())
824            .map(|s| format!("\"{}\"", truncate_str(s, 30))),
825        "glob" => input.get("pattern").and_then(|v| v.as_str())
826            .map(|s| truncate_str(s, 40)),
827        "ls" => input.get("path").and_then(|v| v.as_str())
828            .map(|s| truncate_str(s, 50)),
829        "websearch" => input.get("query").and_then(|v| v.as_str())
830            .map(|s| truncate_str(s, 40)),
831        "webfetch" => input.get("url").and_then(|v| v.as_str())
832            .map(|s| truncate_str(s, 50)),
833        "task" => input.get("description").and_then(|v| v.as_str())
834            .map(|s| truncate_str(s, 40)),
835        "task_create" => input.get("description").and_then(|v| v.as_str())
836            .map(|s| truncate_str(s, 40)),
837        "task_get" | "task_stop" => input.get("task_id").and_then(|v| v.as_str())
838            .map(|s| s.to_string()),
839        _ => None,
840    }
841}
842
843/// Truncate string at char boundary
844fn truncate_str(s: &str, max: usize) -> String {
845    if s.chars().count() <= max { s.to_string() }
846    else { s.chars().take(max.saturating_sub(3)).collect::<String>() + "..." }
847}
848