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