Skip to main content

matrixcode_core/agent/
run.rs

1//! Agent run loop and public methods.
2
3use anyhow::Result;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicU8, Ordering};
6use tokio::sync::mpsc;
7
8use crate::approval::ApproveMode;
9use crate::cancel::CancellationToken;
10use crate::compress::{CompressionConfig, CompressionStrategy, compress_messages, estimate_total_tokens, should_compress};
11use crate::event::{AgentEvent, EventData, EventType};
12use crate::prompt::{PromptProfile, preprocess::{preprocess_with_skills, ProcessResult}};
13use crate::providers::{ChatRequest, Message, MessageContent, Role};
14use crate::skills::Skill;
15use crate::tools::Tool;
16use crate::tools::ToolDefinition;
17use crate::tools::toolproxy::{ProxyToolDef, ProxyToolExecutor};
18
19use super::core::{AgentConfig, AgentState};
20use super::context::AgentContext;
21use super::session::SessionManager;
22use super::types::{Agent, AgentBuilder, MAX_ITERATIONS};
23
24#[allow(dead_code)]
25impl Agent {
26    pub(crate) fn new(builder: AgentBuilder) -> Self {
27        // Create event channel if not provided
28        let event_tx = builder.event_tx.unwrap_or_else(|| {
29            let (tx, _) = mpsc::channel(100);
30            tx
31        });
32
33        // Create modular components from builder
34        let config = AgentConfig {
35            max_tokens: builder.max_tokens,
36            context_size_override: builder.context_size_override,
37            think: builder.think,
38            compression: builder.compression_config,
39            verify_strategy: builder.verify_strategy,
40            project_path: builder.project_path.clone(),
41            ..AgentConfig::default()
42        };
43
44        let state = AgentState::new();
45
46        // AgentContext builds system_prompt internally from profile, skills, overview, memory
47        let context = AgentContext::with_context(
48            builder.profile,
49            builder.skills,
50            builder.project_overview,
51            builder.memory_summary,
52            builder.project_path,
53        );
54
55        // SessionManager handles pending_input_rx and ask_rx
56        let session = SessionManager::with_all_channels(
57            event_tx.clone(),
58            None, // ask_rx will be set later if needed
59            builder.pending_input_rx,
60        );
61
62        Self {
63            // Core components
64            config,
65            state,
66            context,
67            session,
68
69            // Provider & Tools
70            provider: builder.provider,
71            model_name: builder.model_name,
72            tools: builder.tools,
73
74            // Event channel
75            event_tx,
76
77            // Approval
78            approve_mode: Arc::new(AtomicU8::new(builder.approve_mode.to_u8())),
79
80            // Proxy tools
81            proxy_tool_defs: builder.proxy_tool_defs,
82            proxy_executor: builder.proxy_executor,
83
84            // External registries
85            mcp_registry: builder.mcp_registry,
86            lsp_registry: builder.lsp_registry,
87        }
88    }
89
90    // === Field Accessors (delegating to components) ===
91    // Some methods may be unused now but kept for future extensibility.
92
93    /// Get messages (from state)
94    pub(crate) fn messages(&self) -> &Vec<Message> {
95        self.state.messages()
96    }
97
98    /// Get mutable messages (from state)
99    pub(crate) fn messages_mut(&mut self) -> &mut Vec<Message> {
100        self.state.messages_mut()
101    }
102
103    /// Get system prompt (from context)
104    pub(crate) fn system_prompt(&self) -> &str {
105        self.context.system_prompt()
106    }
107
108    /// Get max tokens (from config)
109    pub(crate) fn max_tokens(&self) -> u32 {
110        self.config.max_tokens()
111    }
112
113    /// Get context size override (from config)
114    pub(crate) fn context_size_override(&self) -> Option<u32> {
115        self.config.context_size_override()
116    }
117
118    /// Get think flag (from config)
119    pub(crate) fn think(&self) -> bool {
120        self.config.think()
121    }
122
123    /// Get verify strategy (from config)
124    pub(crate) fn verify_strategy(&self) -> crate::tools::code_quality_hook::VerificationStrategy {
125        self.config.verify_strategy()
126    }
127
128    /// Get project path for verification (from config)
129    pub(crate) fn verify_project_path(&self) -> Option<&std::path::Path> {
130        self.config.project_path()
131    }
132
133    /// Get compression config (from config)
134    pub(crate) fn compression_config(&self) -> &CompressionConfig {
135        self.config.compression_config()
136    }
137
138    /// Get mutable compression config (from config)
139    pub(crate) fn compression_config_mut(&mut self) -> &mut CompressionConfig {
140        self.config.compression_config_mut()
141    }
142
143    /// Get cancellation token (from session)
144    pub(crate) fn cancel_token(&self) -> Option<&CancellationToken> {
145        self.session.cancel_token()
146    }
147
148    /// Get event sender (direct field access)
149    pub(crate) fn event_tx(&self) -> &mpsc::Sender<AgentEvent> {
150        &self.event_tx
151    }
152
153    /// Get skills (from context)
154    pub(crate) fn skills(&self) -> &[Skill] {
155        self.context.skills()
156    }
157
158    /// Get profile (from context)
159    pub(crate) fn profile(&self) -> &PromptProfile {
160        self.context.profile()
161    }
162
163    /// Get project overview (from context)
164    pub(crate) fn project_overview(&self) -> Option<&str> {
165        self.context.project_overview()
166    }
167
168    /// Get memory summary (from context)
169    pub(crate) fn memory_summary(&self) -> Option<&str> {
170        self.context.memory_summary()
171    }
172
173    /// Get project path (from context)
174    pub(crate) fn project_path(&self) -> Option<&std::path::PathBuf> {
175        self.context.project_path()
176    }
177
178    /// Check if cancelled (from session)
179    pub(crate) fn is_cancelled(&self) -> bool {
180        self.session.is_cancelled()
181    }
182
183    /// Get total input tokens (from state)
184    pub(crate) fn total_input_tokens(&self) -> u64 {
185        self.state.total_input_tokens()
186    }
187
188    /// Get total output tokens (from state)
189    pub(crate) fn total_output_tokens(&self) -> u64 {
190        self.state.total_output_tokens()
191    }
192
193    /// Get last input tokens (from state)
194    pub(crate) fn last_input_tokens(&self) -> u64 {
195        self.state.last_input_tokens()
196    }
197
198    /// Get todo reminder count (from state)
199    pub(crate) fn todo_reminder_count(&self) -> &std::collections::HashMap<String, usize> {
200        self.state.todo_reminder_count_map()
201    }
202
203    /// Get mutable todo reminder count (from state)
204    pub(crate) fn todo_reminder_count_mut(&mut self) -> &mut std::collections::HashMap<String, usize> {
205        self.state.todo_reminder_count_map_mut()
206    }
207
208    /// Get pending inputs (from state)
209    pub(crate) fn pending_inputs(&self) -> &Vec<String> {
210        self.state.pending_inputs_vec()
211    }
212
213    /// Get mutable pending inputs (from state)
214    pub(crate) fn pending_inputs_mut(&mut self) -> &mut Vec<String> {
215        self.state.pending_inputs_vec_mut()
216    }
217
218    /// Get ask rx (from session)
219    pub(crate) fn ask_rx(&mut self) -> Option<&mut mpsc::Receiver<String>> {
220        self.session.ask_rx()
221    }
222
223    /// Effective context window size, preferring explicit configuration over model inference.
224    pub(crate) fn effective_context_size(&self) -> Option<u32> {
225        self.config.context_size_override()
226            .or_else(|| self.provider.context_size())
227    }
228
229    /// Get event sender for streaming
230    pub fn event_sender(&self) -> mpsc::Sender<AgentEvent> {
231        self.event_tx.clone()
232    }
233
234    /// Set ask response channel (for TUI mode) - delegates to SessionManager
235    pub fn set_ask_channel(&mut self, rx: mpsc::Receiver<String>) {
236        self.session.set_ask_channel(rx);
237    }
238
239    /// Check if ask channel is available
240    pub(crate) fn has_ask_channel(&self) -> bool {
241        self.session.has_ask_channel()
242    }
243
244    /// Get ask channel receiver (for approval/ask tool)
245    pub(crate) fn ask_channel(&mut self) -> Option<&mut mpsc::Receiver<String>> {
246        self.session.ask_rx()
247    }
248
249    /// 设置代理工具执行器
250    pub fn set_proxy_executor(
251        &mut self,
252        executor: Arc<dyn ProxyToolExecutor>,
253        tool_defs: Vec<ProxyToolDef>,
254    ) {
255        self.proxy_executor = Some(executor);
256        self.proxy_tool_defs = tool_defs;
257    }
258
259    /// Set cancellation token - delegates to SessionManager
260    pub fn set_cancel_token(&mut self, token: CancellationToken) {
261        self.session.set_cancel_token(token);
262    }
263
264    /// Get cancellation token reference
265    pub(crate) fn get_cancel_token(&self) -> Option<&CancellationToken> {
266        self.session.cancel_token()
267    }
268
269    /// Set approve mode at runtime
270    pub fn set_approve_mode(&mut self, mode: ApproveMode) {
271        let old = ApproveMode::from_u8(self.approve_mode.load(Ordering::Relaxed));
272        log::info!("Agent approve mode changed: {} -> {}", old, mode);
273        self.approve_mode.store(mode.to_u8(), Ordering::Relaxed);
274    }
275
276    /// Get a shared reference to the approve mode atomic.
277    pub fn approve_mode_shared(&self) -> Arc<AtomicU8> {
278        self.approve_mode.clone()
279    }
280
281    /// Replace the internal approve mode with an externally-created shared atomic.
282    pub fn set_approve_mode_shared(&mut self, shared: Arc<AtomicU8>) {
283        self.approve_mode = shared;
284    }
285
286    /// Update memory summary and rebuild system prompt.
287    /// Note: Uses build_system_prompt (without project_path) to preserve cache.
288    pub fn update_memory_summary(&mut self, summary: Option<String>) {
289        self.context.update_memory(summary);
290        // Context is now the source of truth for system_prompt
291    }
292
293    /// Refresh CodeGraph tools after /init or codegraph init.
294    /// This rebuilds both tools and system prompt with project_path.
295    /// Call this only when CodeGraph state changes (not every request) to preserve cache.
296    pub fn refresh_codegraph_tools(&mut self) {
297        if let Some(path) = self.context.project_path() {
298            // Check if CodeGraph should be injected now
299            let should_have_codegraph =
300                crate::tools::codegraph::should_inject_codegraph_tools(path);
301
302            // Check if we currently have CodeGraph tools
303            let has_codegraph = self.tools.iter().any(|t| {
304                let name = t.definition().name;
305                name.starts_with("code_") && name != "code_review"
306            });
307
308            // Only update if state changed
309            if should_have_codegraph != has_codegraph {
310                // Add or remove CodeGraph tools
311                if should_have_codegraph {
312                    let codegraph_tools = crate::tools::codegraph::codegraph_tools(path);
313                    for tool in codegraph_tools {
314                        self.tools.push(Arc::from(tool));
315                    }
316                } else {
317                    // Remove CodeGraph tools
318                    self.tools.retain(|t| {
319                        let name = t.definition().name;
320                        !name.starts_with("code_") || name == "code_review"
321                    });
322                }
323                // Update system prompt via context (includes/excludes CodeGraph rules)
324                self.context.rebuild_system_prompt_with_workflows(Some(path.clone()));
325            }
326        }
327    }
328
329    /// Run chat loop with tool execution (streaming version).
330    pub async fn run(&mut self, user_input: String) -> Result<Vec<AgentEvent>> {
331        self.emit(AgentEvent::session_started())?;
332
333        // Step 1: 预处理 - 检测技能/工作流触发
334        let preprocess_result = self.preprocess_input(&user_input);
335
336        // Step 2: 如果有阻塞触发的技能,先注入技能内容
337        let processed_input = match preprocess_result {
338            ProcessResult::SkillTriggered {
339                skill_id,
340                confidence,
341                skill_body,
342            } => {
343                log::info!(
344                    "Skill triggered: {} (confidence: {:.2})",
345                    skill_id,
346                    confidence
347                );
348                self.emit(AgentEvent::progress(
349                    format!("🎯 触发技能: {}", skill_id),
350                    None,
351                ))?;
352
353                // 注入技能内容作为系统提示上下文
354                if let Some(body) = skill_body {
355                    // 技能内容已自动加载,直接注入到用户输入前
356                    let enhanced_input = format!(
357                        "<command-name>{}</command-name>\n\n{}\n\n---\n\nUser request: {}",
358                        skill_id,
359                        body,
360                        user_input
361                    );
362                    enhanced_input
363                } else {
364                    // 技能未自动加载,添加提示让模型调用 skill 工具
365                    let enhanced_input = format!(
366                        "User invoked skill '{}'. Use the `skill` tool with name '{}' to load its instructions before proceeding.\n\nUser request: {}",
367                        skill_id,
368                        skill_id,
369                        user_input
370                    );
371                    enhanced_input
372                }
373            }
374            ProcessResult::WorkflowTriggered {
375                workflow_id,
376                inputs,
377            } => {
378                log::info!("Workflow triggered: {} with inputs: {:?}", workflow_id, inputs);
379                self.emit(AgentEvent::progress(
380                    format!("🔄 触发工作流: {}", workflow_id),
381                    None,
382                ))?;
383                // 工作流触发:注入提示让模型知道应该执行工作流
384                let inputs_json = serde_json::to_string_pretty(&inputs).unwrap_or_default();
385                let enhanced_input = format!(
386                    "Workflow '{}' triggered with extracted inputs:\n{}\n\nUser request: {}",
387                    workflow_id,
388                    inputs_json,
389                    user_input
390                );
391                enhanced_input
392            }
393            ProcessResult::Continue => {
394                // 无触发,正常处理
395                user_input
396            }
397        };
398
399        // Step 3: 添加处理后的用户消息
400        self.state.add_message(Message {
401            role: Role::User,
402            content: MessageContent::Text(processed_input),
403        });
404
405        let mut iterations = 0;
406        let mut should_continue = true;
407        const ITERATION_WARNING_THRESHOLD: usize = MAX_ITERATIONS - 10;
408
409        while should_continue && iterations < MAX_ITERATIONS {
410            iterations += 1;
411
412            // Check for pending inputs BEFORE building request
413            // This ensures appended messages are sent in this iteration's API call
414            self.drain_pending_inputs();
415            if self.has_pending_inputs() {
416                let pending = self.take_pending_inputs();
417                let count = pending.len();
418                let merged = pending.join("\n\n---\n\n");
419                log::info!("Adding {} pending input messages to request", count);
420
421                // Send queue processed event to TUI with messages content
422                self.emit(AgentEvent::queue_processed(count, pending.clone()))?;
423
424                self.state.add_message(Message {
425                    role: Role::User,
426                    content: MessageContent::Text(merged),
427                });
428            }
429
430            if self.session.is_cancelled() {
431                self.emit(AgentEvent::error(
432                    crate::prompt::MSG_OPERATION_CANCELLED.to_string(),
433                    None,
434                    None,
435                ))?;
436                break;
437            }
438
439            // Warn when approaching iteration limit (UI only, not in messages history)
440            if iterations == ITERATION_WARNING_THRESHOLD {
441                self.emit(AgentEvent::progress(
442                    crate::prompt::MSG_ITERATION_WARNING_UI
443                        .replace("{iterations}", &iterations.to_string())
444                        .replace("{max_iterations}", &MAX_ITERATIONS.to_string()),
445                    None,
446                ))?;
447            }
448
449            // Proactive compression: check context size BEFORE API call
450            // For long conversations, compress early to avoid timeout issues
451            let context_size = self.effective_context_size();
452            let estimated_tokens = estimate_total_tokens(self.state.messages());
453
454            if should_compress(estimated_tokens, context_size, self.config.compression_config()) {
455                self.emit(AgentEvent::progress("⚠️ 上下文过大,正在预压缩...", None))?;
456
457                match compress_messages(
458                    self.state.messages(),
459                    CompressionStrategy::SlidingWindow,
460                    self.config.compression_config(),
461                ) {
462                    Ok(compressed) => {
463                        let compressed_tokens = estimate_total_tokens(&compressed);
464                        self.state.set_messages(compressed);
465                        crate::debug::debug_log().compression(
466                            estimated_tokens,
467                            compressed_tokens,
468                            compressed_tokens as f32 / estimated_tokens as f32,
469                        );
470                    }
471                    Err(e) => {
472                        self.emit(AgentEvent::progress(format!("预压缩失败: {}", e), None))?;
473                    }
474                }
475            }
476
477            // Build request with current messages (including any pending inputs)
478            let tool_defs: Vec<ToolDefinition> = {
479                let mut defs: Vec<ToolDefinition> = self
480                    .tools
481                    .iter()
482                    .map(|t| {
483                        let def = t.definition();
484                        let description = def.description_for_llm();
485                        ToolDefinition {
486                            name: def.name,
487                            description,
488                            parameters: def.parameters,
489                            is_priority: def.is_priority,
490                        }
491                    })
492                    .collect();
493                // 添加代理工具定义
494                defs.extend(self.proxy_tool_defs.iter().map(|t| {
495                    let def = &t.definition;
496                    let description = def.description_for_llm();
497                    ToolDefinition {
498                        name: def.name.clone(),
499                        description,
500                        parameters: def.parameters.clone(),
501                        is_priority: def.is_priority,
502                    }
503                }));
504                defs
505            };
506            let request = ChatRequest {
507                system: Some(self.system_prompt().to_string()),
508                messages: self.state.messages().clone(),
509                max_tokens: self.max_tokens(),
510                tools: tool_defs,
511                think: self.think(),
512                enable_caching: true,
513                server_tools: Vec::new(),
514            };
515
516            let response = self.call_streaming(&request).await?;
517
518            self.track_usage(&response.usage);
519
520            crate::debug::debug_log().api_call(
521                &self.model_name,
522                response.usage.input_tokens,
523                response.usage.cache_read_input_tokens > 0,
524            );
525
526            should_continue = self.process_response(&response).await?;
527
528            // If model wants to stop, check for pending inputs first (higher priority than todos)
529            // This ensures appended messages are processed before session ends
530            if !should_continue && iterations < MAX_ITERATIONS - 1 {
531                // Final drain of pending inputs before checking todos
532                self.drain_pending_inputs();
533
534                if self.has_pending_inputs() {
535                    log::info!("Agent: found pending inputs at session end, continuing loop");
536                    should_continue = true;
537                    continue; // Will be processed at start of next iteration
538                }
539
540                // Then check for pending todos
541                // First check if we just sent a reminder (prevent immediate duplicate)
542                if self.last_message_was_todo_reminder() {
543                    log::info!("Skipping todo check: reminder already sent in recent messages");
544                } else {
545                    const MAX_TODO_REMINDERS: usize = 2;
546
547                    // Clone todo_reminder_count to avoid borrow conflict
548                    let reminder_count_clone = self.state.todo_reminder_count_map().clone();
549                    let (pending, all_at_limit) = self.get_pending_todos_with_limit(
550                        &reminder_count_clone,
551                        MAX_TODO_REMINDERS
552                    );
553
554                    if !pending.is_empty() {
555                        // Update reminder counts for todos we're about to remind about
556                        for (_, content) in &pending {
557                            self.state.increment_todo_reminder(content.clone());
558                        }
559
560                        let pending_list = pending
561                            .iter()
562                            .map(|(status, content)| {
563                                let marker = match status.as_str() {
564                                    "in_progress" => "[~]",
565                                    "pending" => "[ ]",
566                                    _ => "[?]",
567                                };
568                                format!("  {} {}", marker, content)
569                            })
570                            .collect::<Vec<_>>()
571                            .join("\n");
572
573                        let reminder = format!(
574                            "📋 任务尚未完成。以下待办项需要处理:\n{}\n\n请继续执行,或在 todo_write 中标记为 completed。如遇阻塞请说明原因。",
575                            pending_list
576                        );
577
578                        self.state.add_message(Message {
579                            role: Role::User,
580                            content: MessageContent::Text(reminder),
581                        });
582                        should_continue = true;
583                    } else if all_at_limit && !self.state.todo_reminder_count_map().is_empty() {
584                        // All todos have reached reminder limit, allow session to end
585                        // but inform user that todos remain incomplete
586                        let remaining_count = self.state.todo_reminder_count_map().len();
587                        self.emit(AgentEvent::progress(
588                            format!(
589                                "⚠️ 会话结束:{} 个待办项未完成(已提醒 {} 次,达到上限)",
590                                remaining_count, MAX_TODO_REMINDERS
591                            ),
592                            None,
593                        ))?;
594                        log::warn!(
595                            "Session ending with {} incomplete todos (reminder limit reached)",
596                            remaining_count
597                        );
598                    }
599                }
600            }
601
602            let context_size = self.effective_context_size();
603            let api_tokens = self.state.last_input_tokens() as u32;
604            let estimated_tokens = estimate_total_tokens(self.state.messages());
605
606            // 优先使用 API 返回的真实 token 数量,因为它更准确(包含缓存信息)
607            // 只有当 API 没有返回有效值时才使用估算值
608            // 注意:之前的条件 `api_tokens >= estimated_tokens / 2` 是不合理的,
609            // 因为当有缓存时,api_tokens 可能远小于 estimated_tokens,但这正是真实情况
610            let current_tokens = if api_tokens > 0 {
611                api_tokens
612            } else {
613                estimated_tokens
614            };
615
616            // Only log compression check when context is getting full (> 30%)
617            // This avoids cluttering debug panel with meaningless checks
618            if let Some(ctx_size) = context_size {
619                // Send context size to TUI for accurate display
620                self.emit(AgentEvent::with_data(
621                    EventType::ContextSize,
622                    EventData::ContextSize {
623                        context_size: ctx_size as u64,
624                    },
625                ))?;
626
627                let usage_ratio = current_tokens as f64 / ctx_size as f64;
628                if usage_ratio >= 0.3 {
629                    crate::debug::debug_log().log(
630                        "checkcompress",
631                        &format!(
632                            "usage={:.1}%, tokens={}, context={}, threshold={}%",
633                            usage_ratio * 100.0,
634                            current_tokens,
635                            ctx_size,
636                            self.config.compression_config().threshold * 100.0
637                        ),
638                    );
639                }
640            }
641
642            if should_compress(current_tokens, context_size, self.config.compression_config()) {
643                self.emit(AgentEvent::progress(crate::prompt::MSG_COMPRESSING_CONTEXT, None))?;
644
645                let original_tokens = current_tokens;
646
647                match compress_messages(
648                    self.state.messages(),
649                    CompressionStrategy::SlidingWindow,
650                    self.config.compression_config(),
651                ) {
652                    Ok(compressed) => {
653                        let compressed_tokens = estimate_total_tokens(&compressed);
654                        self.state.set_messages(compressed);
655                        self.state.set_total_input_tokens(compressed_tokens as u64);
656                        self.state.set_last_input_tokens(compressed_tokens as u64);
657
658                        let ratio = compressed_tokens as f32 / original_tokens as f32;
659                        crate::debug::debug_log().compression(
660                            original_tokens,
661                            compressed_tokens,
662                            ratio,
663                        );
664
665                        self.emit(AgentEvent::with_data(
666                            EventType::CompressionCompleted,
667                            EventData::Compression {
668                                original_tokens: original_tokens as u64,
669                                compressed_tokens: compressed_tokens as u64,
670                                ratio: compressed_tokens as f32 / original_tokens as f32,
671                            },
672                        ))?;
673                    }
674                    Err(e) => {
675                        self.emit(AgentEvent::progress(
676                            format!("{}{}", crate::prompt::MSG_COMPRESSION_FAILED, e),
677                            None,
678                        ))?;
679                    }
680                }
681            }
682        }
683
684        // Check if we stopped due to reaching MAX_ITERATIONS
685        if iterations >= MAX_ITERATIONS && should_continue {
686            self.emit(AgentEvent::error(
687                crate::prompt::MSG_MAX_ITERATIONS_REACHED
688                    .replace("{max_iterations}", &MAX_ITERATIONS.to_string())
689                    .replace("{iterations}", &iterations.to_string()),
690                Some("MAX_ITERATIONS_REACHED".to_string()),
691                Some("agent/run.rs".to_string()),
692            ))?;
693        }
694
695        self.emit(AgentEvent::usage_with_cache(
696            self.state.total_input_tokens(),
697            self.state.total_output_tokens(),
698            0,
699            0,
700        ))?;
701
702        self.emit(AgentEvent::session_ended())?;
703
704        Ok(Vec::new())
705    }
706
707    /// Restore message history (for session continue/resume)
708    pub fn set_messages(&mut self, messages: Vec<Message>) {
709        self.state.set_messages(messages);
710    }
711
712    /// Get current messages (for session saving)
713    pub fn get_messages(&self) -> &[Message] {
714        self.messages()
715    }
716
717    /// Get available tools
718    pub fn get_tools(&self) -> &[Arc<dyn Tool>] {
719        &self.tools
720    }
721
722    /// Get system prompt
723    pub fn get_system_prompt(&self) -> &str {
724        self.system_prompt()
725    }
726
727    /// Get current token counts
728    pub fn get_token_counts(&self) -> (u64, u64) {
729        (
730            self.state.total_input_tokens(),
731            self.state.total_output_tokens(),
732        )
733    }
734
735    /// Clear message history
736    pub fn clear_history(&mut self) {
737        self.messages_mut().clear();
738        self.state.set_total_input_tokens(0);
739        self.state.set_total_output_tokens(0);
740        self.state.set_last_input_tokens(0);
741    }
742
743    /// Get message count
744    pub fn message_count(&self) -> usize {
745        self.messages().len()
746    }
747
748    // ========================================================================
749    // Skill/Workflow Trigger Detection
750    // ========================================================================
751
752    /// 预处理用户输入,检测技能/工作流触发
753    ///
754    /// # 触发类型处理
755    /// - **slash_command** (/review, /debug): 阻塞调用,自动注入技能内容
756    /// - **keyword** ("审查代码", "调试问题"): 阻塞调用,自动注入技能内容
757    /// - **workflow**: 注入工作流上下文,让模型执行工作流
758    ///
759    /// # Returns
760    /// - `SkillTriggered`: 技能被触发,包含技能ID、置信度和可选的技能内容
761    /// - `WorkflowTriggered`: 工作流被触发,包含工作流ID和提取的输入
762    /// - `Continue`: 无触发,继续正常处理
763    pub fn preprocess_input(&self, user_input: &str) -> ProcessResult {
764        // 使用动态触发加载:从已加载的技能中提取触发模式
765        preprocess_with_skills(user_input, self.skills())
766    }
767
768    /// 强制执行触发的技能(注入技能内容到消息历史)
769    ///
770    /// 当技能触发时,此方法将技能内容作为系统上下文注入,
771    /// 确保模型在处理用户请求之前已经加载了技能指令。
772    ///
773    /// # Arguments
774    /// * `skill_id` - 技能标识符
775    /// * `skill_body` - 技能内容(如果已自动加载)
776    ///
777    /// # Returns
778    /// 注入后的增强消息内容
779    pub fn inject_skill_context(&self, skill_id: &str, skill_body: Option<&str>) -> String {
780        if let Some(body) = skill_body {
781            format!(
782                "<command-name>{}</command-name>\n\n{}\n\n**Important**: Follow the skill instructions above before responding to the user request below.",
783                skill_id,
784                body.trim_end()
785            )
786        } else {
787            format!(
788                "Skill '{}' was triggered but not auto-loaded. The model should call the `skill` tool with name '{}' to load its instructions.",
789                skill_id,
790                skill_id
791            )
792        }
793    }
794
795    // ========================================================================
796    // MCP Runtime Management
797    // ========================================================================
798
799    /// 动态添加 MCP 服务器
800    ///
801    /// # Example
802    /// ```ignore
803    /// use matrixcode_core::mcp::McpServerConfig;
804    ///
805    /// let config = McpServerConfig::stdio("npx", vec!["-y", "@playwright/mcp@latest"]);
806    /// agent.add_mcp_server("playwright", config).await?;
807    /// ```
808    pub async fn add_mcp_server(
809        &mut self,
810        name: &str,
811        config: crate::mcp::McpServerConfig,
812    ) -> Result<()> {
813        if let Some(registry) = &self.mcp_registry {
814            let mut reg = registry.write().await;
815            reg.add_server(name.to_string(), config);
816            log::info!("MCP server '{}' added to registry", name);
817        } else {
818            log::warn!("MCP registry not initialized, cannot add server '{}'", name);
819        }
820        Ok(())
821    }
822
823    /// 移除 MCP 服务器
824    pub async fn remove_mcp_server(&mut self, name: &str) -> Result<()> {
825        if let Some(registry) = &self.mcp_registry {
826            let mut reg = registry.write().await;
827            reg.remove_server(name).await?;
828            log::info!("MCP server '{}' removed from registry", name);
829        }
830        Ok(())
831    }
832
833    /// 获取 MCP 服务器状态列表
834    pub async fn mcp_server_status(&self) -> Vec<crate::mcp::ServerStatus> {
835        if let Some(registry) = &self.mcp_registry {
836            let reg = registry.read().await;
837            reg.server_status().await.values().cloned().collect()
838        } else {
839            Vec::new()
840        }
841    }
842
843    /// 启动指定的 MCP 服务器
844    pub async fn start_mcp_server(
845        &self,
846        name: &str,
847    ) -> Result<Vec<Arc<crate::mcp::McpToolWrapper>>> {
848        if let Some(registry) = &self.mcp_registry {
849            let reg = registry.read().await;
850            if let Some(placeholder) = reg.get_server(name) {
851                let tools = placeholder.start().await?;
852                log::info!("MCP server '{}' started with {} tools", name, tools.len());
853                Ok(tools)
854            } else {
855                Err(anyhow::anyhow!(
856                    "MCP server '{}' not found in registry",
857                    name
858                ))
859            }
860        } else {
861            Err(anyhow::anyhow!("MCP registry not initialized"))
862        }
863    }
864}