Skip to main content

skilllite_agent/agent_loop/
mod.rs

1//! Core agent loop: LLM ↔ tool execution cycle.
2//!
3//! Phase 1: simple loop (no task planning).
4//! Phase 2: task-planning-aware loop + run_command + LLM summarization.
5//!
6//! Ported from Python `AgenticLoop._run_openai`. Single implementation
7//! that works for both CLI and RPC via the `EventSink` trait.
8//!
9//! Sub-modules:
10//!   - `planning`   — pre-loop setup, LLM task-list generation, checkpoint saving
11//!   - `execution`  — tool-call batch processing, progressive disclosure, depth limits
12//!   - `reflection` — no-tool response handling, hallucination guard, auto-nudge
13//!   - `helpers`    — shared low-level utilities (tool execution, result processing, …)
14
15mod execution;
16mod helpers;
17mod planning;
18mod reflection;
19
20use anyhow::Result;
21use std::collections::HashSet;
22use std::path::Path;
23
24use super::extensions::{self, MemoryVectorContext};
25use super::llm::{self, LlmClient};
26use super::prompt;
27use super::skills::LoadedSkill;
28use super::soul::Soul;
29use super::types::*;
30use skilllite_core::config::EmbeddingConfig;
31
32use execution::{
33    execute_tool_batch_planning, execute_tool_batch_simple,
34    should_suppress_planning_assistant_text, ExecutionState,
35};
36use helpers::build_agent_result;
37use planning::{
38    build_task_focus_message, maybe_save_checkpoint, run_planning_phase, PlanningResult,
39};
40use reflection::{reflect_planning, reflect_simple, ReflectionOutcome};
41
42/// Maximum number of clarification round-trips before the agent stops unconditionally.
43const MAX_CLARIFICATIONS: usize = 3;
44
45/// Maximum number of context overflow recovery retries before giving up.
46const MAX_CONTEXT_OVERFLOW_RETRIES: usize = 3;
47
48/// Run the agent loop.
49///
50/// Dispatches to either the simple loop (Phase 1) or the task-planning loop
51/// (Phase 2) based on `config.enable_task_planning`.
52pub async fn run_agent_loop(
53    config: &AgentConfig,
54    initial_messages: Vec<ChatMessage>,
55    user_message: &str,
56    skills: &[LoadedSkill],
57    event_sink: &mut dyn EventSink,
58    session_key: Option<&str>,
59) -> Result<AgentResult> {
60    if config.enable_task_planning {
61        run_with_task_planning(
62            config,
63            initial_messages,
64            user_message,
65            skills,
66            event_sink,
67            session_key,
68        )
69        .await
70    } else {
71        run_simple_loop(
72            config,
73            initial_messages,
74            user_message,
75            skills,
76            event_sink,
77            session_key,
78        )
79        .await
80    }
81}
82
83// ═══════════════════════════════════════════════════════════════════════════════
84// Simple loop (Phase 1)
85// ═══════════════════════════════════════════════════════════════════════════════
86
87async fn run_simple_loop(
88    config: &AgentConfig,
89    initial_messages: Vec<ChatMessage>,
90    user_message: &str,
91    skills: &[LoadedSkill],
92    event_sink: &mut dyn EventSink,
93    session_key: Option<&str>,
94) -> Result<AgentResult> {
95    let start_time = std::time::Instant::now();
96    let client = LlmClient::new(&config.api_base, &config.api_key)?;
97    let workspace = Path::new(&config.workspace);
98    let embed_config = EmbeddingConfig::from_env();
99    let embed_ctx = (config.enable_memory_vector && !config.api_key.is_empty()).then_some(
100        MemoryVectorContext {
101            client: &client,
102            embed_config: &embed_config,
103        },
104    );
105
106    let registry = if config.read_only_tools {
107        extensions::ExtensionRegistry::read_only_with_task_planning(
108            config.enable_memory,
109            config.enable_memory_vector,
110            config.enable_task_planning,
111            skills,
112        )
113    } else {
114        extensions::ExtensionRegistry::with_task_planning(
115            config.enable_memory,
116            config.enable_memory_vector,
117            config.enable_task_planning,
118            skills,
119        )
120    };
121    let all_tools = registry.all_tool_definitions();
122
123    // Build system prompt and initial message list
124    let chat_root = skilllite_executor::chat_root();
125    let soul = Soul::auto_load(config.soul_path.as_deref(), &config.workspace);
126    let system_prompt = prompt::build_system_prompt(
127        config.system_prompt.as_deref(),
128        skills,
129        &config.workspace,
130        session_key,
131        config.enable_memory,
132        Some(registry.availability()),
133        Some(&chat_root),
134        soul.as_ref(),
135        config.context_append.as_deref(),
136    );
137    let mut messages = Vec::new();
138    messages.push(ChatMessage::system(&system_prompt));
139    messages.extend(initial_messages);
140    messages.push(ChatMessage::user(user_message));
141
142    let mut documented_skills: HashSet<String> = HashSet::new();
143    let mut state = ExecutionState::new();
144    let mut no_tool_retries = 0usize;
145    let max_no_tool_retries = 3;
146    let mut task_completed = true;
147    let mut clarification_count = 0usize;
148
149    let tools_ref = if all_tools.is_empty() {
150        None
151    } else {
152        Some(all_tools.as_slice())
153    };
154
155    loop {
156        if state.iterations >= config.max_iterations {
157            tracing::warn!(
158                "Agent loop reached max iterations ({})",
159                config.max_iterations
160            );
161            if clarification_count < MAX_CLARIFICATIONS {
162                let req = ClarificationRequest {
163                    reason: "max_iterations".into(),
164                    message: format!(
165                        "已达到最大执行轮次 ({}),任务可能尚未完成。",
166                        config.max_iterations
167                    ),
168                    suggestions: vec!["继续执行更多轮次".into(), "请指定接下来要做什么".into()],
169                };
170                match event_sink.on_clarification_request(&req) {
171                    ClarificationResponse::Continue(hint) => {
172                        clarification_count += 1;
173                        state.iterations = 0;
174                        if let Some(h) = hint {
175                            messages.push(ChatMessage::user(&h));
176                        }
177                        continue;
178                    }
179                    ClarificationResponse::Stop => {}
180                }
181            }
182            task_completed = false;
183            break;
184        }
185        state.iterations += 1;
186
187        // ── LLM call (with context-overflow recovery) ─────────────────────
188        let response = match client
189            .chat_completion_stream(
190                &config.model,
191                &messages,
192                tools_ref,
193                config.temperature,
194                event_sink,
195            )
196            .await
197        {
198            Ok(resp) => {
199                state.context_overflow_retries = 0;
200                resp
201            }
202            Err(e) => {
203                if llm::is_context_overflow_error(&e.to_string()) {
204                    state.context_overflow_retries += 1;
205                    if state.context_overflow_retries >= MAX_CONTEXT_OVERFLOW_RETRIES {
206                        tracing::error!(
207                            "Context overflow persists after {} retries, giving up",
208                            MAX_CONTEXT_OVERFLOW_RETRIES
209                        );
210                        return Err(e);
211                    }
212                    let rc = get_tool_result_recovery_max_chars();
213                    tracing::warn!(
214                        "Context overflow (attempt {}/{}), truncating to {} chars",
215                        state.context_overflow_retries,
216                        MAX_CONTEXT_OVERFLOW_RETRIES,
217                        rc
218                    );
219                    llm::truncate_tool_messages(&mut messages, rc);
220                    continue;
221                }
222                return Err(e);
223            }
224        };
225
226        let choice = response
227            .choices
228            .into_iter()
229            .next()
230            .ok_or_else(|| anyhow::anyhow!("No choices in LLM response"))?;
231        let assistant_content = choice.message.content;
232        let tool_calls = choice.message.tool_calls;
233        let has_tool_calls = tool_calls.as_ref().is_some_and(|tc| !tc.is_empty());
234
235        // Add assistant message to history (move tool_calls into message, avoid clone)
236        if let Some(tcs) = tool_calls {
237            messages.push(ChatMessage::assistant_with_tool_calls(
238                assistant_content.as_deref(),
239                tcs,
240            ));
241        } else if let Some(ref content) = assistant_content {
242            messages.push(ChatMessage::assistant(content));
243        }
244
245        // ── Reflection phase (no tool calls) ─────────────────────────────
246        if !has_tool_calls {
247            match reflect_simple(
248                &assistant_content,
249                all_tools.len(),
250                state.iterations,
251                &mut no_tool_retries,
252                max_no_tool_retries,
253                event_sink,
254                &mut messages,
255            ) {
256                ReflectionOutcome::Nudge(msg) => {
257                    messages.push(ChatMessage::user(&msg));
258                    continue;
259                }
260                ReflectionOutcome::Break => {
261                    if clarification_count < MAX_CLARIFICATIONS {
262                        let req = ClarificationRequest {
263                            reason: "no_progress".into(),
264                            message: "Agent 多次尝试后无法继续执行任务,可能需要更多信息。".into(),
265                            suggestions: vec![
266                                "请补充更多细节或换一种方式描述需求".into(),
267                                "继续尝试,不做更改".into(),
268                            ],
269                        };
270                        match event_sink.on_clarification_request(&req) {
271                            ClarificationResponse::Continue(hint) => {
272                                clarification_count += 1;
273                                no_tool_retries = 0;
274                                if let Some(h) = hint {
275                                    messages.push(ChatMessage::user(&h));
276                                }
277                                continue;
278                            }
279                            ClarificationResponse::Stop => {}
280                        }
281                    }
282                    break;
283                }
284                ReflectionOutcome::Continue => continue,
285            }
286        }
287
288        // ── Execution phase (tool calls present) ──────────────────────────
289        no_tool_retries = 0;
290        let tool_calls = match messages.last().and_then(|m| m.tool_calls.clone()) {
291            Some(tc) if !tc.is_empty() => tc,
292            _ => continue,
293        };
294
295        let outcome = execute_tool_batch_simple(
296            &tool_calls,
297            &registry,
298            workspace,
299            event_sink,
300            embed_ctx.as_ref(),
301            &client,
302            &config.model,
303            skills,
304            &mut messages,
305            &mut documented_skills,
306            &mut state,
307            config.max_consecutive_failures,
308            session_key,
309        )
310        .await;
311
312        if outcome.disclosure_injected {
313            continue;
314        }
315        if outcome.failure_limit_reached {
316            tracing::warn!(
317                "Stopping: {} consecutive tool failures",
318                state.consecutive_failures
319            );
320            if clarification_count < MAX_CLARIFICATIONS {
321                let req = ClarificationRequest {
322                    reason: "too_many_failures".into(),
323                    message: format!(
324                        "工具执行连续失败 {} 次,可能遇到了环境或权限问题。",
325                        state.consecutive_failures
326                    ),
327                    suggestions: vec![
328                        "跳过失败的步骤,继续后续任务".into(),
329                        "请补充信息帮助 Agent 解决问题".into(),
330                    ],
331                };
332                match event_sink.on_clarification_request(&req) {
333                    ClarificationResponse::Continue(hint) => {
334                        clarification_count += 1;
335                        state.consecutive_failures = 0;
336                        if let Some(h) = hint {
337                            messages.push(ChatMessage::user(&h));
338                        }
339                        continue;
340                    }
341                    ClarificationResponse::Stop => {}
342                }
343            }
344            task_completed = false;
345            break;
346        }
347
348        // Global tool call depth limit
349        if state.total_tool_calls >= config.max_iterations * config.max_tool_calls_per_task {
350            tracing::warn!("Agent loop reached total tool call limit");
351            if clarification_count < MAX_CLARIFICATIONS {
352                let req = ClarificationRequest {
353                    reason: "tool_call_limit".into(),
354                    message: "已达到工具调用次数上限,任务可能尚未完成。".into(),
355                    suggestions: vec!["继续执行".into(), "请指定接下来要做什么".into()],
356                };
357                match event_sink.on_clarification_request(&req) {
358                    ClarificationResponse::Continue(hint) => {
359                        clarification_count += 1;
360                        if let Some(h) = hint {
361                            messages.push(ChatMessage::user(&h));
362                        }
363                        continue;
364                    }
365                    ClarificationResponse::Stop => {}
366                }
367            }
368            task_completed = false;
369            break;
370        }
371    }
372
373    let feedback = ExecutionFeedback {
374        total_tools: state.total_tool_calls,
375        failed_tools: state.failed_tool_calls,
376        replans: 0,
377        iterations: state.iterations,
378        elapsed_ms: start_time.elapsed().as_millis() as u64,
379        context_overflow_retries: state.context_overflow_retries,
380        task_completed,
381        task_description: Some(user_message.to_string()),
382        rules_used: state.rules_used,
383        tools_detail: state.tools_detail,
384    };
385    Ok(build_agent_result(
386        messages,
387        state.total_tool_calls,
388        state.iterations,
389        Vec::new(),
390        feedback,
391    ))
392}
393
394// ═══════════════════════════════════════════════════════════════════════════════
395// Task-planning loop (Phase 2)
396// ═══════════════════════════════════════════════════════════════════════════════
397
398/// Agent loop with task planning: TaskPlanner + Auto-Nudge + per-task depth.
399/// Uses planning / execution / reflection sub-modules as building blocks.
400async fn run_with_task_planning(
401    config: &AgentConfig,
402    initial_messages: Vec<ChatMessage>,
403    user_message: &str,
404    skills: &[LoadedSkill],
405    event_sink: &mut dyn EventSink,
406    session_key: Option<&str>,
407) -> Result<AgentResult> {
408    let start_time = std::time::Instant::now();
409    let client = LlmClient::new(&config.api_base, &config.api_key)?;
410    let workspace = Path::new(&config.workspace);
411    let embed_config = EmbeddingConfig::from_env();
412    let embed_ctx = (config.enable_memory_vector && !config.api_key.is_empty()).then_some(
413        MemoryVectorContext {
414            client: &client,
415            embed_config: &embed_config,
416        },
417    );
418
419    let registry = if config.read_only_tools {
420        extensions::ExtensionRegistry::read_only_with_task_planning(
421            config.enable_memory,
422            config.enable_memory_vector,
423            config.enable_task_planning,
424            skills,
425        )
426    } else {
427        extensions::ExtensionRegistry::with_task_planning(
428            config.enable_memory,
429            config.enable_memory_vector,
430            config.enable_task_planning,
431            skills,
432        )
433    };
434    let all_tools = registry.all_tool_definitions();
435
436    // ── Planning phase ─────────────────────────────────────────────────────
437    let PlanningResult {
438        mut planner,
439        mut messages,
440        chat_root,
441        ..
442    } = run_planning_phase(
443        config,
444        initial_messages,
445        user_message,
446        skills,
447        registry.availability(),
448        event_sink,
449        session_key,
450        &client,
451        workspace,
452    )
453    .await?;
454
455    let mut state = ExecutionState::new();
456    let mut documented_skills: HashSet<String> = HashSet::new();
457    let mut consecutive_no_tool = 0usize;
458    let max_no_tool_retries = 3;
459    let mut clarification_count = 0usize;
460
461    // Plan-based budget: min(global, num_tasks × per_task).
462    // Empty plan uses global max — the clarification mechanism handles runaway loops.
463    // Non-empty plan uses per-task budget with a floor of max_tool_calls_per_task
464    // to ensure at least one full task's worth of budget.
465    let num_tasks = planner.task_list.len();
466    let effective_max = if num_tasks == 0 {
467        config.max_iterations
468    } else {
469        config
470            .max_iterations
471            .min((num_tasks * config.max_tool_calls_per_task).max(config.max_tool_calls_per_task))
472    };
473
474    let tools_ref = if all_tools.is_empty() {
475        None
476    } else {
477        Some(all_tools.as_slice())
478    };
479
480    loop {
481        if state.iterations >= effective_max {
482            tracing::warn!(
483                "Agent loop reached effective max iterations ({})",
484                effective_max
485            );
486            if clarification_count < MAX_CLARIFICATIONS {
487                let req = ClarificationRequest {
488                    reason: "max_iterations".into(),
489                    message: format!("已达到最大执行轮次 ({}),任务可能尚未完成。", effective_max),
490                    suggestions: vec!["继续执行更多轮次".into(), "请指定接下来要做什么".into()],
491                };
492                match event_sink.on_clarification_request(&req) {
493                    ClarificationResponse::Continue(hint) => {
494                        clarification_count += 1;
495                        state.iterations = 0;
496                        if let Some(h) = hint {
497                            messages.push(ChatMessage::user(&h));
498                        }
499                        continue;
500                    }
501                    ClarificationResponse::Stop => {}
502                }
503            }
504            break;
505        }
506        state.iterations += 1;
507
508        // ── Suppress streaming while tasks are pending ──────────────────────────
509        // Prevents premature summary text from leaking to the user via streaming
510        // before we can inspect and filter it. Tool results and the final summary
511        // (after all_completed) still reach the user through dedicated event_sink calls.
512        let suppress_stream = !planner.all_completed() && planner.current_task().is_some();
513
514        // ── LLM call (with context-overflow recovery) ─────────────────────────
515        let llm_result = if suppress_stream {
516            client
517                .chat_completion(&config.model, &messages, tools_ref, config.temperature)
518                .await
519        } else {
520            client
521                .chat_completion_stream(
522                    &config.model,
523                    &messages,
524                    tools_ref,
525                    config.temperature,
526                    event_sink,
527                )
528                .await
529        };
530
531        let response = match llm_result {
532            Ok(resp) => {
533                state.context_overflow_retries = 0;
534                resp
535            }
536            Err(e) => {
537                if llm::is_context_overflow_error(&e.to_string()) {
538                    state.context_overflow_retries += 1;
539                    if state.context_overflow_retries >= MAX_CONTEXT_OVERFLOW_RETRIES {
540                        tracing::error!(
541                            "Context overflow persists after {} retries, giving up",
542                            MAX_CONTEXT_OVERFLOW_RETRIES
543                        );
544                        return Err(e);
545                    }
546                    let rc = get_tool_result_recovery_max_chars();
547                    tracing::warn!(
548                        "Context overflow (attempt {}/{}), truncating to {} chars",
549                        state.context_overflow_retries,
550                        MAX_CONTEXT_OVERFLOW_RETRIES,
551                        rc
552                    );
553                    llm::truncate_tool_messages(&mut messages, rc);
554                    continue;
555                }
556                return Err(e);
557            }
558        };
559
560        let choice = response
561            .choices
562            .into_iter()
563            .next()
564            .ok_or_else(|| anyhow::anyhow!("No choices in LLM response"))?;
565        let mut assistant_content = choice.message.content;
566        let tool_calls = choice.message.tool_calls;
567        let has_tool_calls = tool_calls.as_ref().is_some_and(|tc| !tc.is_empty());
568        let suppressed_planning_text =
569            should_suppress_planning_assistant_text(&planner, has_tool_calls)
570                && assistant_content
571                    .as_ref()
572                    .is_some_and(|content| !content.trim().is_empty());
573        if suppressed_planning_text {
574            tracing::info!("Suppressed free-form assistant text during pending task execution");
575            assistant_content = None;
576        }
577
578        if let Some(tcs) = tool_calls {
579            messages.push(ChatMessage::assistant_with_tool_calls(
580                assistant_content.as_deref(),
581                tcs,
582            ));
583        } else if let Some(ref content) = assistant_content {
584            messages.push(ChatMessage::assistant(content));
585        }
586
587        // Emit suppressed text when LLM did return real tool calls (not a hallucination)
588        if suppress_stream && has_tool_calls {
589            if let Some(ref content) = assistant_content {
590                event_sink.on_text(content);
591            }
592        }
593
594        // ── Reflection phase (no tool calls) ──────────────────────────────────
595        if !has_tool_calls {
596            match reflect_planning(
597                &assistant_content,
598                suppress_stream,
599                &mut planner,
600                &mut consecutive_no_tool,
601                max_no_tool_retries,
602                event_sink,
603                &mut messages,
604            ) {
605                ReflectionOutcome::Nudge(msg) => {
606                    messages.push(ChatMessage::user(&msg));
607                    continue;
608                }
609                ReflectionOutcome::Break => {
610                    if !planner.all_completed() && clarification_count < MAX_CLARIFICATIONS {
611                        let req = ClarificationRequest {
612                            reason: "no_progress".into(),
613                            message: "Agent 多次尝试后无法继续执行任务,可能需要更多信息。".into(),
614                            suggestions: vec![
615                                "请补充更多细节或换一种方式描述需求".into(),
616                                "继续尝试,不做更改".into(),
617                            ],
618                        };
619                        match event_sink.on_clarification_request(&req) {
620                            ClarificationResponse::Continue(hint) => {
621                                clarification_count += 1;
622                                consecutive_no_tool = 0;
623                                if let Some(h) = hint {
624                                    messages.push(ChatMessage::user(&h));
625                                }
626                                continue;
627                            }
628                            ClarificationResponse::Stop => {}
629                        }
630                    }
631                    break;
632                }
633                _ => continue,
634            }
635        }
636
637        // ── Execution phase (tool calls present) ──────────────────────────────
638        consecutive_no_tool = 0;
639        let tool_calls = match messages.last().and_then(|m| m.tool_calls.clone()) {
640            Some(tc) if !tc.is_empty() => tc,
641            _ => continue,
642        };
643
644        let outcome = execute_tool_batch_planning(
645            &tool_calls,
646            &registry,
647            workspace,
648            event_sink,
649            embed_ctx.as_ref(),
650            &client,
651            &config.model,
652            &mut planner,
653            skills,
654            &mut messages,
655            &mut documented_skills,
656            &mut state,
657            config.max_tool_calls_per_task,
658            config.max_consecutive_failures,
659            session_key,
660        )
661        .await;
662
663        if outcome.disclosure_injected {
664            continue;
665        }
666        if outcome.failure_limit_reached {
667            tracing::warn!(
668                "Stopping: {} consecutive tool failures",
669                state.consecutive_failures
670            );
671            if clarification_count < MAX_CLARIFICATIONS {
672                let req = ClarificationRequest {
673                    reason: "too_many_failures".into(),
674                    message: format!(
675                        "工具执行连续失败 {} 次,可能遇到了环境或权限问题。",
676                        state.consecutive_failures
677                    ),
678                    suggestions: vec![
679                        "跳过失败的步骤,继续后续任务".into(),
680                        "请补充信息帮助 Agent 解决问题".into(),
681                    ],
682                };
683                match event_sink.on_clarification_request(&req) {
684                    ClarificationResponse::Continue(hint) => {
685                        clarification_count += 1;
686                        state.consecutive_failures = 0;
687                        if let Some(h) = hint {
688                            messages.push(ChatMessage::user(&h));
689                        }
690                        continue;
691                    }
692                    ClarificationResponse::Stop => {}
693                }
694            }
695            break;
696        }
697        if suppressed_planning_text && !planner.all_completed() {
698            if let Some(nudge) = planner.build_nudge_message() {
699                messages.push(ChatMessage::user(&format!(
700                    "Pending tasks still exist. During execution, do not use free-form completion or wrap-up text. \
701                     Complete the current task structurally with `complete_task`, then continue.\n\n{}",
702                    nudge
703                )));
704            }
705        }
706        if outcome.depth_limit_reached {
707            let depth_msg = planner.build_depth_limit_message(config.max_tool_calls_per_task);
708            messages.push(ChatMessage::user(&depth_msg));
709            state.tool_calls_current_task = 0; // reset so next task gets its full quota
710        }
711
712        // ── Post-tool completion check ─────────────────────────────────────────
713        // Task completion is now handled structurally: either via complete_task tool call
714        // (intercepted in execute_tool_batch_planning) or try_auto_mark_task_on_success.
715        // No text-based detection needed here.
716        if planner.all_completed() {
717            tracing::info!("All tasks completed, ending iteration");
718            let has_substantial = assistant_content
719                .as_ref()
720                .is_some_and(|c| c.trim().len() > 50);
721            if !has_substantial {
722                if let Ok(resp) = client
723                    .chat_completion_stream(
724                        &config.model,
725                        &messages,
726                        None,
727                        config.temperature,
728                        event_sink,
729                    )
730                    .await
731                {
732                    if let Some(ch) = resp.choices.into_iter().next() {
733                        if let Some(ref content) = ch.message.content {
734                            event_sink.on_text(content);
735                            messages.push(ChatMessage::assistant(content));
736                        }
737                    }
738                }
739            }
740            break;
741        }
742
743        // A13: Per-iteration checkpoint (run mode) for --resume
744        maybe_save_checkpoint(
745            session_key,
746            user_message,
747            config,
748            &planner,
749            &messages,
750            &chat_root,
751        );
752
753        // Task focus: inject progress update with already-called tools
754        let tools_called: Vec<String> = {
755            let mut seen = HashSet::new();
756            let mut result = Vec::new();
757            for d in state.tools_detail.iter().filter(|d| d.success) {
758                if seen.insert(d.tool.as_str()) {
759                    result.push(d.tool.clone());
760                }
761            }
762            result
763        };
764        if let Some(focus_msg) = build_task_focus_message(&planner, &tools_called) {
765            messages.push(ChatMessage::system(&focus_msg));
766        }
767
768        // Global tool call depth limit
769        if state.total_tool_calls >= effective_max * config.max_tool_calls_per_task {
770            tracing::warn!("Agent loop reached total tool call limit");
771            if clarification_count < MAX_CLARIFICATIONS {
772                let req = ClarificationRequest {
773                    reason: "tool_call_limit".into(),
774                    message: "已达到工具调用次数上限,任务可能尚未完成。".into(),
775                    suggestions: vec!["继续执行".into(), "请指定接下来要做什么".into()],
776                };
777                match event_sink.on_clarification_request(&req) {
778                    ClarificationResponse::Continue(hint) => {
779                        clarification_count += 1;
780                        if let Some(h) = hint {
781                            messages.push(ChatMessage::user(&h));
782                        }
783                        continue;
784                    }
785                    ClarificationResponse::Stop => {}
786                }
787            }
788            break;
789        }
790    }
791
792    let feedback = ExecutionFeedback {
793        total_tools: state.total_tool_calls,
794        failed_tools: state.failed_tool_calls,
795        replans: state.replan_count,
796        iterations: state.iterations,
797        elapsed_ms: start_time.elapsed().as_millis() as u64,
798        context_overflow_retries: state.context_overflow_retries,
799        task_completed: planner.all_completed(),
800        task_description: Some(user_message.to_string()),
801        rules_used: planner.matched_rule_ids().to_vec(),
802        tools_detail: state.tools_detail,
803    };
804
805    Ok(build_agent_result(
806        messages,
807        state.total_tool_calls,
808        state.iterations,
809        planner.task_list,
810        feedback,
811    ))
812}