Skip to main content

j_agent/tools/
sub_agent.rs

1use crate::agent::thread_identity::{
2    clear_thread_cwd, current_agent_name, set_current_agent_name, set_current_agent_type,
3    set_thread_cwd, thread_cwd,
4};
5use crate::llm::ToolDefinition;
6use crate::permission::JcliConfig;
7use crate::permission::queue::AgentType;
8use crate::storage::{
9    ChatMessage, DisplayHint, MessageRole, ModelProvider, SessionEvent, SessionPaths, ToolCallItem,
10    append_event_to_path,
11};
12use crate::tools::derived_shared::{
13    DerivedAgentShared, LlmNonStreamRequest, SubAgentHandle, SubAgentStatus, ToolExecContext,
14    call_llm_non_stream, create_runtime_and_client, execute_tool_with_permission,
15    extract_tool_items,
16};
17use crate::tools::worktree::{create_agent_worktree, remove_agent_worktree};
18use crate::tools::{
19    PlanDecision, Tool, ToolRegistry, ToolResult, parse_tool_args, schema_to_tool_params,
20};
21use crate::util::log::write_info_log;
22use crate::util::safe_lock;
23use schemars::JsonSchema;
24use serde::Deserialize;
25use serde_json::{Value, json};
26use std::borrow::Cow;
27use std::sync::{
28    Arc, Mutex,
29    atomic::{AtomicBool, AtomicUsize, Ordering},
30};
31
32/// 子 Agent 运行时状态引用集合,供 loop 同步状态/系统提示/消息列表。
33struct SubAgentLoopStateRefs {
34    system_prompt: Arc<Mutex<String>>,
35    messages: Arc<Mutex<Vec<ChatMessage>>>,
36    status: Arc<Mutex<SubAgentStatus>>,
37    current_tool: Arc<Mutex<Option<String>>>,
38    tool_calls_count: Arc<AtomicUsize>,
39    current_round: Arc<AtomicUsize>,
40}
41
42impl SubAgentLoopStateRefs {
43    fn from_handle(handle: &SubAgentHandle) -> Self {
44        Self {
45            system_prompt: Arc::clone(&handle.system_prompt),
46            messages: Arc::clone(&handle.messages),
47            status: Arc::clone(&handle.status),
48            current_tool: Arc::clone(&handle.current_tool),
49            tool_calls_count: Arc::clone(&handle.tool_calls_count),
50            current_round: Arc::clone(&handle.current_round),
51        }
52    }
53
54    fn set_status(&self, status: SubAgentStatus) {
55        if let Ok(mut s) = self.status.lock() {
56            *s = status;
57        }
58    }
59
60    fn set_current_tool(&self, name: Option<String>) {
61        if let Ok(mut t) = self.current_tool.lock() {
62            *t = name;
63        }
64    }
65}
66
67/// 无 UI 子代理循环的参数集合
68struct SubAgentLoopParams {
69    provider: ModelProvider,
70    system_prompt: Option<String>,
71    prompt: String,
72    tools: Vec<ToolDefinition>,
73    registry: Arc<ToolRegistry>,
74    jcli_config: Arc<JcliConfig>,
75    snapshot: Option<SubAgentLoopStateRefs>,
76    description: String,
77    /// 独立 transcript JSONL 路径:每轮消息 append 到此(崩溃安全)。
78    transcript_path: Option<std::path::PathBuf>,
79    /// 父 agent 的上下文配置快照(供 select_messages + micro_compact 复用)
80    context_config: crate::tools::derived_shared::AgentContextConfig,
81    /// 子 Agent metrics 累加器(与 DerivedAgentShared.sub_agent_metrics 共享)
82    sub_agent_metrics: Arc<Mutex<crate::tools::derived_shared::SubAgentMetrics>>,
83}
84
85/// 将任意描述转为适合作为 <前缀> 显示的名字(去空白,限长度)
86fn sanitize_agent_name(description: &str) -> String {
87    let cleaned: String = description
88        .chars()
89        .map(|c| if c.is_whitespace() { '_' } else { c })
90        .collect();
91    // 控制显示长度,避免前缀挤占正文
92    if cleaned.chars().count() <= 24 {
93        cleaned
94    } else {
95        let truncated: String = cleaned.chars().take(24).collect();
96        format!("{}…", truncated)
97    }
98}
99
100/// 构建 SubAgent 专用的 system prompt
101///
102/// 从嵌入模板加载,将 `{{.base_prompt}}` 替换为父 agent 的 system prompt,
103/// 使 SubAgent 继承基础能力同时拥有独立的身份和限制说明。
104fn build_sub_agent_system_prompt(base_prompt: Option<&str>) -> String {
105    let template = crate::template::sub_agent_system_prompt_template();
106    let base = base_prompt.unwrap_or("You are a helpful assistant.");
107    template.replace("{{.base_prompt}}", base)
108}
109
110/// SubAgentTool 参数
111#[derive(Deserialize, JsonSchema)]
112struct AgentParams {
113    /// The task for the sub-agent to perform
114    prompt: String,
115    /// A short (3-5 word) description of the task
116    #[serde(default)]
117    description: Option<String>,
118    /// Set to true to run in background. Returns task_id immediately.
119    #[serde(default)]
120    run_in_background: bool,
121    /// If true, create an isolated git worktree for this sub-agent.
122    /// Recommended when running multiple parallel agents that may edit overlapping files.
123    /// The worktree is automatically cleaned up when the agent finishes.
124    #[serde(default)]
125    worktree: bool,
126    /// If true, the sub-agent inherits all tool permissions (allow_all=true).
127    /// Use this when you trust the agent to run tools without confirmation prompts.
128    #[serde(default)]
129    inherit_permissions: bool,
130}
131
132// ========== SubAgentTool ==========
133
134/// SubAgent 工具:启动子代理执行复杂多步任务
135#[allow(dead_code)]
136pub struct SubAgentTool {
137    pub shared: DerivedAgentShared,
138}
139
140impl SubAgentTool {
141    pub const NAME: &'static str = "Agent";
142}
143
144impl Tool for SubAgentTool {
145    fn name(&self) -> &str {
146        Self::NAME
147    }
148
149    fn description(&self) -> Cow<'_, str> {
150        r#"
151        Launch a sub-agent to handle complex, multi-step tasks autonomously.
152        The sub-agent runs with a fresh context (system prompt + your prompt as user message).
153        It can use all tools except Agent (to prevent recursion).
154
155        When NOT to use the Agent tool:
156        - If you want to read a specific file path, use Read or Glob instead
157        - If you are searching for a specific class/function definition, use Grep or Glob instead
158        - If you are searching code within a specific file or 2-3 files, use Read instead
159
160        Usage notes:
161        - Always include a short description (3-5 words) summarizing what the agent will do
162        - The result returned by the agent is not visible to the user. To show the user the result, send a text message with a concise summary
163        - Use foreground (default) when you need the agent's results before proceeding
164        - Use background when you have genuinely independent work to do in parallel
165        - Clearly tell the agent whether you expect it to write code or just do research (search, file reads, web fetches, etc.)
166        - Provide clear, detailed prompts so the agent can work autonomously — explain what you're trying to accomplish, what you've already learned, and give enough context for the agent to make judgment calls
167        "#.into()
168    }
169
170    fn parameters_schema(&self) -> Value {
171        schema_to_tool_params::<AgentParams>()
172    }
173
174    fn execute(&self, arguments: &str, cancelled: &Arc<AtomicBool>) -> ToolResult {
175        let params: AgentParams = match parse_tool_args(arguments) {
176            Ok(p) => p,
177            Err(e) => return e,
178        };
179
180        let prompt = params.prompt;
181        let description = params
182            .description
183            .unwrap_or_else(|| "sub-agent task".to_string());
184        let run_in_background = params.run_in_background;
185        let use_worktree = params.worktree;
186
187        // 获取 provider 和构建 SubAgent 独立 system prompt
188        let provider = safe_lock(&self.shared.provider, "SubAgentTool::provider").clone();
189        let base_prompt =
190            safe_lock(&self.shared.system_prompt, "SubAgentTool::system_prompt").clone();
191        let system_prompt = build_sub_agent_system_prompt(base_prompt.as_deref());
192
193        // worktree 隔离:提前创建(在调用线程中;失败则提前退出,避免浪费 sub_id)
194        let worktree_info: Option<(std::path::PathBuf, String)> = if use_worktree {
195            match create_agent_worktree(&description) {
196                Ok(info) => Some(info),
197                Err(e) => {
198                    return ToolResult {
199                        output: format!("创建 worktree 失败: {}", e),
200                        is_error: true,
201                        images: vec![],
202                        plan_decision: PlanDecision::None,
203                    };
204                }
205            }
206        } else {
207            None
208        };
209
210        // 提前分配 sub_id,用于构造独立 todos/transcript 路径
211        let sub_id = self.shared.sub_agent_tracker.allocate_id();
212        let session_id_snapshot =
213            safe_lock(&self.shared.session_id, "SubAgentTool::session_id").clone();
214        let session_paths = SessionPaths::new(&session_id_snapshot);
215        let subagent_todos_path = session_paths.subagent_todos_file(&sub_id);
216        let subagent_transcript_path = session_paths.subagent_transcript(&sub_id);
217
218        // 构建子 registry(排除 "Agent" 工具防递归,独立 todos 文件)
219        let (child_registry, _) = self.shared.build_child_registry(subagent_todos_path);
220        let child_registry = Arc::new(child_registry);
221
222        let mut disabled = self.shared.disabled_tools.as_ref().clone();
223        disabled.push(Self::NAME.to_string());
224        // 子 agent 继承父 agent 的 deferred 快照作为初始工具过滤,
225        // 但不支持动态 LoadTool(子 agent 是一次性执行,每轮不重建工具列表)
226        let deferred = match self.shared.deferred_tools.lock() {
227            Ok(guard) => guard,
228            Err(e) => e.into_inner(),
229        }
230        .clone();
231        let tools = child_registry.to_llm_tools_non_deferred(&disabled, &deferred);
232
233        // inherit_permissions:复制 JcliConfig 并启用 allow_all
234        let jcli_config = if params.inherit_permissions {
235            let mut cfg = self.shared.jcli_config.as_ref().clone();
236            cfg.permissions.allow_all = true;
237            Arc::new(cfg)
238        } else {
239            Arc::clone(&self.shared.jcli_config)
240        };
241
242        // 复用父 agent 的上下文配置快照
243        let context_config = safe_lock(
244            &self.shared.agent_context_config,
245            "SubAgentTool::context_config",
246        )
247        .clone();
248
249        if run_in_background {
250            // 后台模式:先注册到 tracker 获得 handle,再 spawn_command
251            self.shared.sub_agent_tracker.gc_finished();
252            let handle = self.shared.sub_agent_tracker.register_with_id(
253                sub_id.clone(),
254                &description,
255                "background",
256            );
257
258            // 注册到 background_manager,传入线程存活标记
259            let (task_id, output_buffer) = self.shared.background_manager.spawn_command(
260                &format!("Agent: {}", description),
261                None,
262                0,
263                Some(Arc::clone(&handle.is_running)), // 线程存活标记
264            );
265
266            let snap_running = Arc::clone(&handle.is_running);
267            let snapshot_refs = SubAgentLoopStateRefs::from_handle(&handle);
268
269            let bg_manager = Arc::clone(&self.shared.background_manager);
270            let task_id_clone = task_id.clone();
271            let cancelled_clone = Arc::clone(cancelled);
272
273            let description_clone = description.clone();
274            let display_clone = Arc::clone(&self.shared.display_messages);
275            let context_clone = Arc::clone(&self.shared.context_messages);
276            let transcript_path = subagent_transcript_path.clone();
277            let _sub_id_for_thread = sub_id.clone();
278            let context_config_clone = context_config.clone();
279            let agent_identity = format!("SubAgent@{}", sanitize_agent_name(&description));
280            let sub_agent_metrics_clone = Arc::clone(&self.shared.sub_agent_metrics);
281            std::thread::spawn(move || {
282                // 设置线程的 agent 身份(含类型前缀,与广播 <SubAgent@Name> 格式一致)
283                set_current_agent_name(&agent_identity);
284                set_current_agent_type(AgentType::SubAgent);
285
286                // 设置 worktree CWD
287                if let Some((ref wt_path, _)) = worktree_info {
288                    set_thread_cwd(wt_path);
289                }
290
291                let result = run_sub_agent_loop(
292                    SubAgentLoopParams {
293                        provider,
294                        system_prompt: Some(system_prompt),
295                        prompt,
296                        tools,
297                        registry: child_registry,
298                        jcli_config,
299                        snapshot: Some(snapshot_refs),
300                        description: description_clone.clone(),
301                        transcript_path: Some(transcript_path),
302                        context_config: context_config_clone,
303                        sub_agent_metrics: sub_agent_metrics_clone,
304                    },
305                    &cancelled_clone,
306                    &display_clone,
307                    &context_clone,
308                );
309
310                snap_running.store(false, Ordering::Relaxed);
311
312                // 清理 worktree
313                if let Some((ref wt_path, ref branch)) = worktree_info {
314                    remove_agent_worktree(wt_path, branch);
315                }
316
317                // 写入输出缓冲区
318                {
319                    let mut buf = safe_lock(&output_buffer, "SubAgentTool::bg_output");
320                    buf.push_str(&result);
321                }
322
323                bg_manager.complete_task(&task_id_clone, "completed", result);
324            });
325
326            ToolResult {
327                output: json!({
328                    "task_id": task_id,
329                    "sub_id": sub_id,
330                    "description": description,
331                    "status": "running in background"
332                })
333                .to_string(),
334                is_error: false,
335                images: vec![],
336                plan_decision: PlanDecision::None,
337            }
338        } else {
339            // 前台模式:阻塞执行
340            // 保存旧身份 + CWD,执行完后恢复(前台 agent 在调用线程中运行)
341            let old_agent_name = current_agent_name();
342            let old_cwd = thread_cwd();
343            let agent_identity = format!("SubAgent@{}", sanitize_agent_name(&description));
344            set_current_agent_name(&agent_identity);
345            set_current_agent_type(AgentType::SubAgent);
346            if let Some((ref wt_path, _)) = worktree_info {
347                set_thread_cwd(wt_path);
348            }
349
350            // 注册到子 Agent tracker 供 /dump + UI dashboard 读取
351            self.shared.sub_agent_tracker.gc_finished();
352            let handle = self.shared.sub_agent_tracker.register_with_id(
353                sub_id.clone(),
354                &description,
355                "foreground",
356            );
357            let snap_running = Arc::clone(&handle.is_running);
358            let snapshot_refs = SubAgentLoopStateRefs::from_handle(&handle);
359
360            let cancelled_clone = Arc::clone(cancelled);
361            let result = run_sub_agent_loop(
362                SubAgentLoopParams {
363                    provider,
364                    system_prompt: Some(system_prompt),
365                    prompt,
366                    tools,
367                    registry: child_registry,
368                    jcli_config,
369                    snapshot: Some(snapshot_refs),
370                    description,
371                    transcript_path: Some(subagent_transcript_path),
372                    context_config,
373                    sub_agent_metrics: Arc::clone(&self.shared.sub_agent_metrics),
374                },
375                &cancelled_clone,
376                &self.shared.display_messages,
377                &self.shared.context_messages,
378            );
379
380            snap_running.store(false, Ordering::Relaxed);
381
382            // 清理 worktree 并恢复身份 + CWD
383            if let Some((ref wt_path, ref branch)) = worktree_info {
384                remove_agent_worktree(wt_path, branch);
385            }
386            set_current_agent_name(&old_agent_name);
387            match old_cwd {
388                Some(p) => set_thread_cwd(&p),
389                None => clear_thread_cwd(),
390            }
391
392            ToolResult {
393                output: result,
394                is_error: false,
395                images: vec![],
396                plan_decision: PlanDecision::None,
397            }
398        }
399    }
400
401    fn requires_confirmation(&self) -> bool {
402        false
403    }
404}
405
406// ========== SubAgent Loop ==========
407
408/// 无 UI 的子代理循环:执行工具调用直到完成或达到限制
409///
410/// - 不发送 StreamMsg(无 UI 交互)
411/// - 需要确认的工具通过 permission 检查:允许则执行,否则返回 "Tool denied"
412/// - 返回最终的 assistant 文本
413fn run_sub_agent_loop(
414    params: SubAgentLoopParams,
415    cancelled: &Arc<AtomicBool>,
416    display_messages: &Arc<Mutex<Vec<ChatMessage>>>,
417    context_messages: &Arc<Mutex<Vec<ChatMessage>>>,
418) -> String {
419    let agent_name = sanitize_agent_name(&params.description);
420    let sender_label = format!("SubAgent@{}", agent_name);
421    // SubAgent 的中间消息通过双通道推送:
422    // - display_messages:UI 显示(TUI 渲染)— 纯文本/结构体 + sender_name 字段
423    // - context_messages:显式注入 Main Agent LLM context — XML 包裹
424    //
425    // Main Agent 能看到子代理的中间文本回复和工具调用名,以便感知工作进度。
426    // 文本消息双通道分异(display 干净文本,context XML 包裹)
427    let push_display_and_context =
428        |display_content: String, context_content: String, sender: &str| {
429            if let Ok(mut display) = display_messages.lock() {
430                display.push(
431                    ChatMessage::text(MessageRole::Assistant, &display_content).with_sender(sender),
432                );
433            }
434            if let Ok(mut context) = context_messages.lock() {
435                context.push(
436                    ChatMessage::text(MessageRole::Assistant, &context_content).with_sender(sender),
437                );
438            }
439        };
440    // 工具调用推入 display only(结构体格式,渲染为工具卡片)
441    let push_tool_call_to_display = |item: &ToolCallItem, sender: &str| {
442        if let Ok(mut display) = display_messages.lock() {
443            display.push(ChatMessage {
444                role: MessageRole::Assistant,
445                content: String::new(),
446                tool_calls: Some(vec![item.clone()]),
447                tool_call_id: None,
448                images: None,
449                reasoning_content: None,
450                sender_name: Some(sender.to_string()),
451                recipient_name: None,
452                display_hint: DisplayHint::Normal,
453            });
454        }
455    };
456    // 工具结果推入 display only
457    let push_tool_result_to_display =
458        |result_content: String, tool_call_id: String, sender: &str| {
459            if let Ok(mut display) = display_messages.lock() {
460                display.push(ChatMessage {
461                    role: MessageRole::Tool,
462                    content: result_content,
463                    tool_calls: None,
464                    tool_call_id: Some(tool_call_id),
465                    images: None,
466                    reasoning_content: None,
467                    sender_name: Some(sender.to_string()),
468                    recipient_name: None,
469                    display_hint: DisplayHint::Normal,
470                });
471            }
472        };
473    let max_rounds = 30; // 子代理最大轮数
474
475    // 进入 Thinking 状态(即将调用 LLM)
476    if let Some(ref refs) = params.snapshot {
477        refs.set_status(SubAgentStatus::Thinking);
478    }
479
480    let (rt, client) = match create_runtime_and_client(&params.provider) {
481        Ok(pair) => pair,
482        Err(e) => {
483            if let Some(ref refs) = params.snapshot {
484                refs.set_status(SubAgentStatus::Error(e.clone()));
485            }
486            return e;
487        }
488    };
489
490    // 写入 system prompt 快照(供 /dump 读取)
491    if let Some(ref refs) = params.snapshot
492        && let Ok(mut sp) = refs.system_prompt.lock()
493    {
494        *sp = params.system_prompt.clone().unwrap_or_default();
495    }
496
497    let mut messages: Vec<ChatMessage> = vec![ChatMessage {
498        role: MessageRole::User,
499        content: params.prompt,
500        tool_calls: None,
501        tool_call_id: None,
502        images: None,
503        reasoning_content: None,
504        sender_name: None,
505        recipient_name: None,
506        display_hint: DisplayHint::Normal,
507    }];
508
509    let sync_messages = |msgs: &Vec<ChatMessage>| {
510        if let Some(ref refs) = params.snapshot
511            && let Ok(mut snap) = refs.messages.lock()
512        {
513            *snap = msgs.clone();
514        }
515    };
516
517    // 独立 transcript append:每条新消息 append 一行 SessionEvent::Msg 到 jsonl 文件
518    let transcript_path = params.transcript_path.clone();
519    let append_to_transcript = |msgs: &[ChatMessage]| {
520        if let Some(ref path) = transcript_path {
521            for m in msgs {
522                let _ = append_event_to_path(path, &SessionEvent::msg(m.clone()));
523            }
524        }
525    };
526
527    sync_messages(&messages);
528    append_to_transcript(&messages);
529
530    let mut final_text = String::new();
531
532    for round in 0..max_rounds {
533        if cancelled.load(Ordering::Relaxed) {
534            if let Some(ref refs) = params.snapshot {
535                refs.set_status(SubAgentStatus::Cancelled);
536                refs.set_current_tool(None);
537            }
538            return format!("{}\n[Sub-agent cancelled]", final_text);
539        }
540
541        if let Some(ref refs) = params.snapshot {
542            refs.current_round.store(round + 1, Ordering::Relaxed);
543        }
544
545        write_info_log("SubAgent", &format!("Round {}/{}", round + 1, max_rounds));
546
547        // 进入 Thinking 状态(即将调用 LLM,等待模型回复)
548        if let Some(ref refs) = params.snapshot {
549            refs.set_status(SubAgentStatus::Thinking);
550        }
551
552        // 构建重试回调:更新 SubAgent 状态为 Retrying
553        let status_for_retry = params.snapshot.as_ref().map(|r| Arc::clone(&r.status));
554        let retry_callback = move |attempt: u32, max_attempts: u32, delay_ms: u64, error: &str| {
555            if let Some(ref status_arc) = status_for_retry
556                && let Ok(mut s) = status_arc.lock()
557            {
558                *s = SubAgentStatus::Retrying {
559                    attempt,
560                    max_attempts,
561                    delay_ms,
562                    error: error.to_string(),
563                };
564            }
565        };
566
567        // 上下文裁剪:复用父 agent 的 select_messages + micro_compact
568        // SubAgent 是隔离 loop,messages 里没有其他 agent 的广播,无需 compress_other_agent_toolcalls。
569        let mut api_messages = crate::context::window::select_messages(
570            &messages,
571            params.context_config.max_history_messages,
572            params.context_config.max_context_tokens,
573            params.context_config.compact.keep_recent,
574            &params.context_config.compact.micro_compact_exempt_tools,
575        );
576        if params.context_config.compact.enabled {
577            crate::context::compact::micro_compact(
578                &mut api_messages,
579                params.context_config.compact.keep_recent,
580                &params.context_config.compact.micro_compact_exempt_tools,
581            );
582        }
583
584        let response = match call_llm_non_stream(&LlmNonStreamRequest {
585            rt: &rt,
586            client: &client,
587            provider: &params.provider,
588            messages: &api_messages,
589            tools: &params.tools,
590            system_prompt: params.system_prompt.as_deref(),
591            on_retry: Some(&retry_callback),
592        }) {
593            Ok(r) => {
594                // LLM 调用成功,保持 Thinking(等待工具执行时切换为 Working)
595                r
596            }
597            Err(e) => {
598                if let Some(ref refs) = params.snapshot {
599                    refs.set_status(SubAgentStatus::Error(e.clone()));
600                    refs.set_current_tool(None);
601                }
602                return format!("{}\n{}", final_text, e);
603            }
604        };
605
606        // 提取第一个 choice(非流式调用保证有且仅有一个)
607        let choice = response
608            .choices
609            .into_iter()
610            .next()
611            .expect("call_llm_non_stream validates non-empty choices");
612
613        // 累加 LLM metrics(usage 可能为空,某些 provider 不返回)
614        if let Some(usage) = response.usage
615            && let Ok(mut m) = params.sub_agent_metrics.lock()
616        {
617            m.total_llm_calls += 1;
618            m.total_input_tokens += usage.prompt_tokens;
619            m.total_output_tokens += usage.completion_tokens;
620        }
621
622        let assistant_text = choice.message.content.clone().unwrap_or_default();
623        let reasoning_content = choice.message.reasoning_content.clone();
624        if !assistant_text.is_empty() {
625            write_info_log("SubAgent", &format!("Reply: {}", &assistant_text));
626            // UI 状态行:显示 sub-agent 的文字回复
627            // ★ 此消息通过双通道推送(display + context),会同步到 Main Agent 的 LLM 上下文(有意为之的设计)。
628            // display: 纯文本 + sender_name | context: XML 包裹
629            push_display_and_context(
630                assistant_text.clone(),
631                format!("<{}>{}</{}>", sender_label, &assistant_text, sender_label),
632                &sender_label,
633            );
634        }
635
636        // 检查是否有工具调用
637        let is_tool_calls = choice.finish_reason.as_deref() == Some("tool_calls");
638
639        if !is_tool_calls || choice.message.tool_calls.is_none() {
640            // 纯文本回复结束:用当前轮次的文本作为最终返回(而非之前的中间文本)
641            final_text = assistant_text.clone();
642            if !assistant_text.is_empty() {
643                let final_msg = ChatMessage::text(MessageRole::Assistant, assistant_text.clone());
644                messages.push(final_msg);
645                if let Some(last) = messages.last() {
646                    append_to_transcript(std::slice::from_ref(last));
647                }
648                sync_messages(&messages);
649            }
650            break;
651        }
652
653        // 上面已检查 tool_calls.is_none() 会 break,此处用 let else 确保安全
654        let Some(tool_calls) = choice.message.tool_calls.as_ref() else {
655            break;
656        };
657        let tool_items = extract_tool_items(tool_calls);
658        if tool_items.is_empty() {
659            break;
660        }
661
662        // UI 状态行:显示 sub-agent 的工具调用名(不含参数/结果)
663        // ★ context 通道:XML 包裹文本(Main Agent LLM context)
664        // ★ display 通道:tool_calls 结构体(渲染为工具卡片)
665        for item in &tool_items {
666            // context:文本格式(XML 包裹)
667            if let Ok(mut context) = context_messages.lock() {
668                context.push(
669                    ChatMessage::text(
670                        MessageRole::Assistant,
671                        format!(
672                            "<{}>[调用工具 {}]</{}>",
673                            sender_label, item.name, sender_label
674                        ),
675                    )
676                    .with_sender(&sender_label),
677                );
678            }
679            // display:结构体格式
680            push_tool_call_to_display(item, &sender_label);
681        }
682
683        // 将 assistant 消息(含 tool_calls)加入历史
684        let assistant_msg = ChatMessage {
685            role: MessageRole::Assistant,
686            content: assistant_text,
687            tool_calls: Some(tool_items.clone()),
688            tool_call_id: None,
689            images: None,
690            reasoning_content,
691            sender_name: None,
692            recipient_name: None,
693            display_hint: DisplayHint::Normal,
694        };
695        messages.push(assistant_msg);
696        if let Some(last) = messages.last() {
697            append_to_transcript(std::slice::from_ref(last));
698        }
699
700        // 逐个执行工具
701        for item in &tool_items {
702            if let Some(ref refs) = params.snapshot {
703                refs.set_current_tool(Some(item.name.clone()));
704                refs.set_status(SubAgentStatus::Working);
705                refs.tool_calls_count.fetch_add(1, Ordering::Relaxed);
706            }
707            let result_msg = execute_tool_with_permission(
708                item,
709                &ToolExecContext {
710                    registry: &params.registry,
711                    jcli_config: &params.jcli_config,
712                    cancelled,
713                    log_tag: "SubAgent",
714                    verbose: true,
715                },
716            );
717
718            // 累加工具调用 metrics
719            if let Ok(mut m) = params.sub_agent_metrics.lock() {
720                m.total_tool_calls += 1;
721            }
722
723            // 工具结果推入 display only(完整内容,渲染为工具结果卡片)
724            push_tool_result_to_display(
725                result_msg.content.clone(),
726                result_msg.tool_call_id.clone().unwrap_or_default(),
727                &sender_label,
728            );
729            messages.push(result_msg);
730            if let Some(last) = messages.last() {
731                append_to_transcript(std::slice::from_ref(last));
732            }
733        }
734        if let Some(ref refs) = params.snapshot {
735            refs.set_current_tool(None);
736            // 工具全部执行完毕,切回 Thinking(下一轮 LLM 调用前)
737            refs.set_status(SubAgentStatus::Thinking);
738        }
739
740        // 本轮工具结果写入后同步快照
741        sync_messages(&messages);
742    }
743
744    // UI 状态行:sub-agent 结束
745    // ★ 此消息通过双通道推送(display + context),会同步到 Main Agent 的 LLM 上下文。
746    push_display_and_context(
747        "[已完成]".to_string(),
748        format!("<{}>[已完成]</{}>", sender_label, sender_label),
749        &sender_label,
750    );
751
752    if let Some(ref refs) = params.snapshot {
753        refs.set_status(SubAgentStatus::Completed);
754        refs.set_current_tool(None);
755    }
756
757    if final_text.is_empty() {
758        "[Sub-agent completed with no text output]".to_string()
759    } else {
760        final_text
761    }
762}