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