Skip to main content

j_agent/teammate/
teammate_loop.rs

1use crate::infra::hook::{HookContext, HookEvent, HookManager};
2use crate::permission::JcliConfig;
3use crate::storage::{
4    ChatMessage, DisplayHint, MessageRole, ModelProvider, SessionEvent, SessionPaths,
5    append_event_to_path, sanitize_filename,
6};
7use crate::teammate::{TeammateManager, TeammateStatus};
8use crate::tools::ToolRegistry;
9use crate::tools::derived_shared::{
10    AgentContextConfig, LlmNonStreamRequest, SubAgentMetrics, ToolExecContext, call_llm_non_stream,
11    create_runtime_and_client, execute_tool_with_permission, extract_tool_items,
12};
13use crate::util::log::write_info_log;
14use std::path::PathBuf;
15use std::sync::{
16    Arc, Mutex,
17    atomic::{AtomicBool, AtomicUsize, Ordering},
18};
19use tokio_util::sync::CancellationToken;
20
21/// Teammate loop 最大轮数(足够大,实际由 cancel_token 控制生命周期)
22const MAX_TEAMMATE_ROUNDS: u32 = 200;
23/// 连续空闲轮询上限(约 2 分钟后退出)
24const MAX_CONSECUTIVE_IDLE_POLLS: u32 = 120;
25/// 轮询等待期间的内层循环次数(每次休眠 POLL_SLEEP_MILLIS)
26const POLL_CHECK_INTERVAL: u32 = 10;
27/// 轮询等待期间每次休眠的毫秒数
28const POLL_SLEEP_MILLIS: u64 = 100;
29/// SendMessage gate 最大重试次数(超过后强制执行,防止无限循环)
30const MAX_SEND_GATE_RETRIES: u32 = 2;
31
32/// Teammate agent loop 的配置
33pub struct TeammateLoopConfig {
34    pub name: String,
35    pub role: String,
36    pub initial_prompt: String,
37    pub provider: ModelProvider,
38    pub base_system_prompt: Option<String>,
39    /// 共享的当前 session id 槽(session 切换时会被主线程更新)
40    pub session_id: Arc<Mutex<String>>,
41    /// 禁用工具名列表
42    pub disabled_tools: Vec<String>,
43    /// 延迟加载的工具列表(与主 agent 共享同一 Arc)
44    pub deferred_tools: Arc<Mutex<Vec<String>>>,
45    /// 本会话 LoadTool 已加载的 deferred 工具(与主 agent 共享同一 Arc)
46    #[allow(dead_code)]
47    pub session_loaded_deferred: Arc<Mutex<Vec<String>>>,
48    pub registry: Arc<ToolRegistry>,
49    pub jcli_config: Arc<JcliConfig>,
50    pub teammate_manager: Arc<Mutex<TeammateManager>>,
51    /// 广播收件箱(来自其他 agent 的广播消息)
52    pub broadcast_inbox: Arc<Mutex<Vec<ChatMessage>>>,
53    pub cancel_token: CancellationToken,
54    /// 供 /dump 读取的 system prompt 快照
55    pub system_prompt_snapshot: Arc<Mutex<String>>,
56    /// 供 /dump 读取的 messages 快照
57    pub messages_snapshot: Arc<Mutex<Vec<ChatMessage>>>,
58    /// 细粒度运行状态(与 TeammateHandle 共享)
59    pub status: Arc<Mutex<TeammateStatus>>,
60    /// 累计工具调用次数(与 TeammateHandle 共享)
61    pub tool_calls_count: Arc<AtomicUsize>,
62    /// 当前正在执行的工具名(与 TeammateHandle 共享)
63    pub current_tool: Arc<Mutex<Option<String>>>,
64    /// 唤醒标志(与 TeammateHandle 共享):@self 或 from==Main 的广播会 set 它
65    /// WorkDone 后仅此标志能触发重新激活(清除 work_done),未 WorkDone 时任何消息都唤醒
66    pub wake_flag: Arc<AtomicBool>,
67    /// WorkDone 终态标志(与 TeammateHandle 共享):WorkDone 工具调用后 set,loop 看到后退出
68    pub work_done: Arc<AtomicBool>,
69    /// 父 agent 共享的 HookManager(Teammate 调 LLM 前走 PreLlmRequest hook 链)
70    pub hook_manager: Arc<Mutex<HookManager>>,
71    /// 父 agent 的 disabled_hooks 快照(Teammate 走 hook 链时用)
72    pub disabled_hooks: Arc<Mutex<Vec<String>>>,
73    /// 父 agent 的上下文配置快照(供 select_messages + micro_compact 复用)
74    pub context_config: Arc<Mutex<AgentContextConfig>>,
75    /// 子 Agent metrics 累加器(与 DerivedAgentShared.sub_agent_metrics 共享)
76    pub sub_agent_metrics: Arc<Mutex<SubAgentMetrics>>,
77}
78
79/// Teammate 专用的 agent loop
80///
81/// 与 sub_agent_loop 的关键区别:
82/// 1. 无 TUI 交互式确认(通过 permission 规则自动决定)
83/// 2. 每轮开始检查 broadcast_inbox(来自广播)
84/// 3. 使用 SendMessage 工具与其他 agent 通信
85/// 4. idle polling — 无工具调用时不立即退出,而是轮询等待新消息
86/// 5. loop 结束后通知团队
87pub fn run_teammate_loop(config: TeammateLoopConfig) -> String {
88    let TeammateLoopConfig {
89        name,
90        role,
91        initial_prompt,
92        provider,
93        base_system_prompt,
94        session_id,
95        disabled_tools,
96        deferred_tools,
97        session_loaded_deferred: _,
98        registry,
99        jcli_config,
100        teammate_manager,
101        broadcast_inbox,
102        cancel_token,
103        system_prompt_snapshot,
104        messages_snapshot,
105        status,
106        tool_calls_count,
107        current_tool,
108        wake_flag,
109        work_done,
110        hook_manager,
111        disabled_hooks,
112        context_config,
113        sub_agent_metrics,
114    } = config;
115
116    // 定位当前 teammate 的 transcript JSONL 路径(按 session_id 实时解析,切换 session 也能落到正确位置)
117    let transcript_path = |name: &str| -> PathBuf {
118        let sid = session_id
119            .lock()
120            .map(|s| s.clone())
121            .unwrap_or_else(|_| "unknown".to_string());
122        SessionPaths::new(&sid).teammate_transcript(&sanitize_filename(name))
123    };
124
125    let append_messages = |msgs: &[ChatMessage]| {
126        let path = transcript_path(&name);
127        for m in msgs {
128            let _ = append_event_to_path(&path, &SessionEvent::msg(m.clone()));
129        }
130    };
131
132    // 辅助闭包:更新状态
133    let set_status = |new_status: TeammateStatus| {
134        if let Ok(mut s) = status.lock() {
135            *s = new_status;
136        }
137    };
138
139    set_status(TeammateStatus::Initializing);
140
141    let (rt, client) = match create_runtime_and_client(&provider) {
142        Ok(pair) => pair,
143        Err(e) => return e,
144    };
145
146    // 构建 teammate 专用 system prompt
147    let system_prompt = build_teammate_system_prompt(
148        &name,
149        &role,
150        base_system_prompt.as_deref(),
151        &teammate_manager,
152    );
153
154    // 写入 system prompt 快照(供 /dump 读取)
155    if let Ok(mut sp) = system_prompt_snapshot.lock() {
156        *sp = system_prompt.clone();
157    }
158
159    let mut messages: Vec<ChatMessage> = Vec::with_capacity(1 + initial_prompt.len());
160    messages.push(ChatMessage {
161        role: MessageRole::User,
162        content: initial_prompt,
163        tool_calls: None,
164        tool_call_id: None,
165        images: None,
166        reasoning_content: None,
167        sender_name: None,
168        recipient_name: None,
169        display_hint: DisplayHint::Normal,
170    });
171    // 初始 prompt 也要写入 transcript,便于恢复时重现对话
172    append_messages(&messages);
173
174    // 辅助闭包:将当前 messages clone 到共享快照
175    let sync_messages = |msgs: &Vec<ChatMessage>| {
176        if let Ok(mut snap) = messages_snapshot.lock() {
177            *snap = msgs.clone();
178        }
179    };
180    sync_messages(&messages);
181
182    let mut last_assistant_text = String::new();
183    let mut consecutive_idle_polls = 0;
184    let mut send_gate_retries: u32 = 0;
185
186    // 创建 AtomicBool 作为取消信号(与 CancellationToken 桥接)
187    let cancel_flag = Arc::new(AtomicBool::new(false));
188
189    for round in 0..MAX_TEAMMATE_ROUNDS {
190        // 每轮开始时动态获取可用工具(与 Main Agent 对齐),
191        // 确保 SendMessage 等依赖 is_available() 的工具在 teammate 注册后立即可见。
192        let deferred_snapshot = match deferred_tools.lock() {
193            Ok(guard) => guard.clone(),
194            Err(e) => e.into_inner().clone(),
195        };
196        let tools = registry.to_llm_tools_non_deferred(&disabled_tools, &deferred_snapshot);
197
198        // 检查取消
199        if cancel_token.is_cancelled() || cancel_flag.load(Ordering::Relaxed) {
200            set_status(TeammateStatus::Cancelled);
201            return format!("{}\n[Teammate '{}' cancelled]", last_assistant_text, name);
202        }
203
204        // WorkDone 终态检查:teammate 明确声明完成工作后立即退出
205        if work_done.load(Ordering::Relaxed) {
206            write_info_log(
207                "TeammateLoop",
208                &format!("{}: WorkDone flag set, exiting", name),
209            );
210            break;
211        }
212
213        // Drain 来自广播的消息(包括旁听消息,保留上下文)
214        // 注意:consecutive_idle_polls 的管理下放到 WaitingForMessage 分支,
215        // 本处不再根据 had_new_messages 重置,避免"任何消息都触发 LLM"。
216        let len_before_drain = messages.len();
217        let _ = drain_broadcast_messages(&mut messages, &broadcast_inbox);
218        if messages.len() > len_before_drain {
219            append_messages(&messages[len_before_drain..]);
220        }
221
222        // 同步 messages 快照(供 /dump 读取)
223        sync_messages(&messages);
224
225        write_info_log(
226            "TeammateLoop",
227            &format!(
228                "{}: Round {}/{}, messages={}",
229                name,
230                round + 1,
231                MAX_TEAMMATE_ROUNDS,
232                messages.len(),
233            ),
234        );
235
236        // 更新状态为 Thinking(即将调用 LLM,等待模型回复)
237        set_status(TeammateStatus::Thinking);
238
239        // 复用父 agent 的 context 配置,对齐 Main 管线:
240        //   select_messages → micro_compact → PreLlmRequest hook 链 (含 broadcast_compress)
241        let ctx_cfg = match context_config.lock() {
242            Ok(g) => g.clone(),
243            Err(e) => {
244                set_status(TeammateStatus::Error(format!("context_config lock: {}", e)));
245                return format!("{}\ncontext_config lock poisoned", last_assistant_text);
246            }
247        };
248        let mut api_messages = crate::context::window::select_messages(
249            &messages,
250            ctx_cfg.max_history_messages,
251            ctx_cfg.max_context_tokens,
252            ctx_cfg.compact.keep_recent,
253            &ctx_cfg.compact.micro_compact_exempt_tools,
254        );
255        if ctx_cfg.compact.enabled {
256            crate::context::compact::micro_compact(
257                &mut api_messages,
258                ctx_cfg.compact.keep_recent,
259                &ctx_cfg.compact.micro_compact_exempt_tools,
260            );
261        }
262
263        // PreLlmRequest hook 链(内置 broadcast_compress 会按线程本地身份折叠其他 agent 广播)
264        let mut effective_system_prompt = system_prompt.clone();
265        {
266            let hook_mgr = match hook_manager.lock() {
267                Ok(g) => g,
268                Err(e) => {
269                    set_status(TeammateStatus::Error(format!("hook_manager lock: {}", e)));
270                    return format!("{}\nhook_manager lock poisoned", last_assistant_text);
271                }
272            };
273            if hook_mgr.has_hooks_for(HookEvent::PreLlmRequest) {
274                let disabled_snapshot: Vec<String> =
275                    disabled_hooks.lock().map(|g| g.clone()).unwrap_or_default();
276                let ctx = HookContext {
277                    event: HookEvent::PreLlmRequest,
278                    messages: Some(api_messages.clone()),
279                    system_prompt: Some(effective_system_prompt.clone()),
280                    model: Some(provider.model.clone()),
281                    session_id: session_id.lock().ok().map(|g| g.clone()),
282                    cwd: std::env::current_dir()
283                        .map(|p| p.display().to_string())
284                        .unwrap_or_else(|_| ".".to_string()),
285                    ..Default::default()
286                };
287                if let Some(result) =
288                    hook_mgr.execute(HookEvent::PreLlmRequest, ctx, &disabled_snapshot)
289                {
290                    if result.is_stop() {
291                        set_status(TeammateStatus::Error("hook requested stop".to_string()));
292                        return format!(
293                            "{}\n[Teammate halted by PreLlmRequest hook]",
294                            last_assistant_text
295                        );
296                    }
297                    if let Some(new_msgs) = result.messages {
298                        api_messages = new_msgs;
299                    }
300                    if let Some(new_prompt) = result.system_prompt {
301                        effective_system_prompt = new_prompt;
302                    }
303                    if let Some(inject) = result.inject_messages {
304                        api_messages.extend(inject);
305                    }
306                }
307            }
308        }
309
310        let status_for_retry = Arc::clone(&status);
311        let retry_callback = move |attempt: u32, max_attempts: u32, delay_ms: u64, error: &str| {
312            if let Ok(mut s) = status_for_retry.lock() {
313                *s = TeammateStatus::Retrying {
314                    attempt,
315                    max_attempts,
316                    delay_ms,
317                    error: error.to_string(),
318                };
319            }
320        };
321
322        let response = match call_llm_non_stream(&LlmNonStreamRequest {
323            rt: &rt,
324            client: &client,
325            provider: &provider,
326            messages: &api_messages,
327            tools: &tools,
328            system_prompt: Some(&effective_system_prompt),
329            on_retry: Some(&retry_callback),
330        }) {
331            Ok(r) => r,
332            Err(e) => {
333                set_status(TeammateStatus::Error(e.clone()));
334                return format!("{}\n{}", last_assistant_text, e);
335            }
336        };
337
338        // 提取第一个 choice(非流式调用保证有且仅有一个)
339        let response_choice = response
340            .choices
341            .into_iter()
342            .next()
343            .expect("call_llm_non_stream validates non-empty choices");
344
345        // 累加 LLM metrics(usage 可能为空,某些 provider 不返回)
346        if let Some(usage) = response.usage
347            && let Ok(mut m) = sub_agent_metrics.lock()
348        {
349            m.total_llm_calls += 1;
350            m.total_input_tokens += usage.prompt_tokens;
351            m.total_output_tokens += usage.completion_tokens;
352        }
353
354        let assistant_text = response_choice.message.content.clone().unwrap_or_default();
355        let reasoning_content = response_choice.message.reasoning_content.clone();
356
357        // 本轮是否仅调 IgnoreMessage:若是,跳过 assistant_text 的聊天室广播,
358        // 避免「我选择不响应」这段 prose 反过来唤醒别的 agent,制造连锁。
359        let is_ignore_only = response_choice
360            .message
361            .tool_calls
362            .as_ref()
363            .map(|calls| {
364                !calls.is_empty() && calls.iter().all(|c| c.function.name == "IgnoreMessage")
365            })
366            .unwrap_or(false);
367
368        if !assistant_text.is_empty() && !is_ignore_only {
369            last_assistant_text = assistant_text.clone();
370            // Draft: 纯文本视为内部思考,仅推送到 display(用户 TUI 可见),
371            // 不进 context_messages(Main Agent LLM 不可见),不广播到其他 teammate。
372            // 只有 SendMessage 工具调用才会真正广播。
373            if let Ok(manager) = teammate_manager.lock() {
374                let sender_label = format!("Teammate@{}", name);
375                let display_msg = ChatMessage::text(MessageRole::Assistant, &assistant_text)
376                    .with_sender(&sender_label)
377                    .with_display_hint(DisplayHint::Draft);
378                if let Ok(mut display) = manager.display_messages.lock() {
379                    display.push(display_msg);
380                }
381            }
382        }
383
384        // 检查是否有工具调用
385        let has_tool_calls = response_choice.finish_reason.as_deref() == Some("tool_calls");
386
387        if !has_tool_calls || response_choice.message.tool_calls.is_none() {
388            // 无工具调用 — 进入轮询等待模式
389            set_status(TeammateStatus::WaitingForMessage);
390
391            // 文字回复也写入 messages + jsonl
392            // 否则独立 jsonl 缺少这部分,resume 时 existing_count > synthesized.len() 导致 delta 补齐失效
393            if !assistant_text.is_empty() {
394                messages.push(ChatMessage::text(
395                    MessageRole::Assistant,
396                    assistant_text.clone(),
397                ));
398                if let Some(last) = messages.last() {
399                    append_messages(std::slice::from_ref(last));
400                }
401            }
402
403            // 先把已到达的旁听消息 drain 到 messages(保留上下文,但不自动唤醒)
404            let len_before_drain = messages.len();
405            let _ = drain_broadcast_messages(&mut messages, &broadcast_inbox);
406            if messages.len() > len_before_drain {
407                append_messages(&messages[len_before_drain..]);
408            }
409
410            // 唤醒判断:仅当被 @self 或 from=Main 时(即 wake_flag set)才唤醒。
411            // 其他广播消息已 drain 进 transcript(下次唤醒时 LLM 自然看到),
412            // 但本身不打扰当前 idle 状态,避免 thrash 与互相凑话。
413            // WorkDone 后被 @self 同样会清除 work_done 重新激活。
414            if wake_flag.swap(false, Ordering::Relaxed) {
415                if work_done.load(Ordering::Relaxed) {
416                    work_done.store(false, Ordering::Relaxed);
417                    write_info_log(
418                        "TeammateLoop",
419                        &format!("{}: re-activated after WorkDone by @mention", name),
420                    );
421                }
422                consecutive_idle_polls = 0;
423                continue;
424            }
425
426            consecutive_idle_polls += 1;
427            if consecutive_idle_polls >= MAX_CONSECUTIVE_IDLE_POLLS {
428                write_info_log(
429                    "TeammateLoop",
430                    &format!(
431                        "{}: idle for {} rounds (~2min), exiting",
432                        name, consecutive_idle_polls
433                    ),
434                );
435                break;
436            }
437
438            // 等待 1 秒后再检查(可被 cancel_token 中断)
439            for _ in 0..POLL_CHECK_INTERVAL {
440                if cancel_token.is_cancelled() {
441                    set_status(TeammateStatus::Cancelled);
442                    return format!("{}\n[Teammate '{}' cancelled]", last_assistant_text, name);
443                }
444                if work_done.load(Ordering::Relaxed) {
445                    break;
446                }
447                std::thread::sleep(std::time::Duration::from_millis(POLL_SLEEP_MILLIS));
448
449                // 在轮询期间也 drain 消息到 messages(保持上下文可见性,
450                // 但不据此唤醒 — 仅 wake_flag set 时才打断 idle)
451                let len_before_drain = messages.len();
452                let _ = drain_broadcast_messages(&mut messages, &broadcast_inbox);
453                if messages.len() > len_before_drain {
454                    append_messages(&messages[len_before_drain..]);
455                }
456
457                if wake_flag.swap(false, Ordering::Relaxed) {
458                    if work_done.load(Ordering::Relaxed) {
459                        work_done.store(false, Ordering::Relaxed);
460                        write_info_log(
461                            "TeammateLoop",
462                            &format!("{}: re-activated after WorkDone by @mention", name),
463                        );
464                    }
465                    consecutive_idle_polls = 0;
466                    break;
467                }
468            }
469            continue;
470        }
471
472        // 上面已检查 tool_calls.is_none() 会 continue,此处用 let else 确保安全
473        let Some(tool_calls) = response_choice.message.tool_calls.as_ref() else {
474            continue;
475        };
476        let tool_items = extract_tool_items(tool_calls);
477        if tool_items.is_empty() {
478            break;
479        }
480
481        // 重置空闲计数(有工具调用说明正在工作)
482        consecutive_idle_polls = 0;
483
484        messages.push(ChatMessage {
485            role: MessageRole::Assistant,
486            content: assistant_text,
487            tool_calls: Some(tool_items.clone()),
488            tool_call_id: None,
489            images: None,
490            reasoning_content,
491            sender_name: None,
492            recipient_name: None,
493            display_hint: DisplayHint::Normal,
494        });
495        if let Some(last) = messages.last() {
496            append_messages(std::slice::from_ref(last));
497        }
498
499        // 在 TUI 中显示 teammate 的工具调用
500        // - SendMessage:display 通道显示工具卡片,context 通道跳过(broadcast 已单独推入消息内容)
501        // - IgnoreMessage:display 通道显示工具卡片,context 通道跳过(语义是"静默")
502        // ★ context 通道:XML 包裹文本(Main Agent LLM context)
503        // ★ display 通道:tool_calls 结构体(渲染为工具卡片)
504        if let Ok(manager) = teammate_manager.lock() {
505            let sender_label = format!("Teammate@{}", name);
506            for item in &tool_items {
507                // context:文本格式(XML 包裹)— SendMessage/IgnoreMessage 跳过
508                if !matches!(item.name.as_str(), "SendMessage" | "IgnoreMessage") {
509                    let context_msg = ChatMessage::text(
510                        MessageRole::Assistant,
511                        format!(
512                            "<{}>[called tool {}]</{}>",
513                            sender_label, item.name, sender_label
514                        ),
515                    )
516                    .with_sender(&sender_label);
517                    if let Ok(mut context) = manager.context_messages.lock() {
518                        context.push(context_msg);
519                    }
520                }
521                // display:结构体格式 — 所有工具都显示(包括 SendMessage/IgnoreMessage)
522                if let Ok(mut display) = manager.display_messages.lock() {
523                    display.push(ChatMessage {
524                        role: MessageRole::Assistant,
525                        content: String::new(),
526                        tool_calls: Some(vec![item.clone()]),
527                        tool_call_id: None,
528                        images: None,
529                        reasoning_content: None,
530                        sender_name: Some(sender_label.clone()),
531                        recipient_name: None,
532                        display_hint: DisplayHint::Normal,
533                    });
534                }
535            }
536        }
537
538        // 执行工具(SendMessage 有 gate 机制:inbox 有新消息时拦截,让 agent 重新决策)
539        let mut gate_triggered = false;
540        let mut gated_send_content: Option<String> = None;
541
542        for item in &tool_items {
543            if cancel_token.is_cancelled() {
544                messages.push(ChatMessage {
545                    role: MessageRole::Tool,
546                    content: "[Cancelled]".to_string(),
547                    tool_calls: None,
548                    tool_call_id: Some(item.id.clone()),
549                    images: None,
550                    reasoning_content: None,
551                    sender_name: None,
552                    recipient_name: None,
553                    display_hint: DisplayHint::Normal,
554                });
555                if let Some(last) = messages.last() {
556                    append_messages(std::slice::from_ref(last));
557                }
558                continue;
559            }
560
561            // SendMessage gate: 检查 inbox 是否有新消息到达
562            if item.name == "SendMessage" {
563                let inbox_has_new = broadcast_inbox
564                    .lock()
565                    .map(|inbox| !inbox.is_empty())
566                    .unwrap_or(false);
567
568                if inbox_has_new && send_gate_retries < MAX_SEND_GATE_RETRIES {
569                    // Gate 触发:跳过 SendMessage,记录被拦截内容
570                    gate_triggered = true;
571                    gated_send_content = Some(item.arguments.clone());
572                    send_gate_retries += 1;
573
574                    write_info_log(
575                        "TeammateLoop",
576                        &format!(
577                            "{}: SendMessage gated (attempt {}/{}), new messages in inbox",
578                            name, send_gate_retries, MAX_SEND_GATE_RETRIES
579                        ),
580                    );
581
582                    // 返回 tool result 占位(保持 LLM 对话结构完整)
583                    messages.push(ChatMessage {
584                        role: MessageRole::Tool,
585                        content: "[SendMessage held: new messages arrived during your thinking, please re-evaluate]".to_string(),
586                        tool_calls: None,
587                        tool_call_id: Some(item.id.clone()),
588                        images: None,
589                        reasoning_content: None,
590                        sender_name: None,
591                        recipient_name: None,
592                        display_hint: DisplayHint::Normal,
593                    });
594                    if let Some(last) = messages.last() {
595                        append_messages(std::slice::from_ref(last));
596                    }
597                    continue; // 跳过执行,但继续处理其他非 SendMessage 工具
598                } else if !inbox_has_new {
599                    // 无新消息,正常执行并重置计数
600                    send_gate_retries = 0;
601                }
602                // inbox_has_new && retries >= MAX: 强制执行(不 reset,下次仍 force)
603            }
604
605            // 更新当前工具名 + 切换为 Working(正在执行工具)
606            if let Ok(mut ct) = current_tool.lock() {
607                *ct = Some(item.name.clone());
608            }
609            set_status(TeammateStatus::Working);
610            tool_calls_count.fetch_add(1, Ordering::Relaxed);
611
612            let result_msg = execute_tool_with_permission(
613                item,
614                &ToolExecContext {
615                    registry: &registry,
616                    jcli_config: &jcli_config,
617                    cancelled: &cancel_flag,
618                    log_tag: "TeammateLoop",
619                    verbose: false,
620                },
621            );
622
623            // 累加工具调用 metrics
624            if let Ok(mut m) = sub_agent_metrics.lock() {
625                m.total_tool_calls += 1;
626            }
627
628            // 工具结果推入 display only(完整内容,渲染为工具结果卡片)
629            if let Ok(manager) = teammate_manager.lock()
630                && let Ok(mut display) = manager.display_messages.lock()
631            {
632                display.push(ChatMessage {
633                    role: MessageRole::Tool,
634                    content: result_msg.content.clone(),
635                    tool_calls: None,
636                    tool_call_id: result_msg.tool_call_id.clone(),
637                    images: None,
638                    reasoning_content: None,
639                    sender_name: Some(format!("Teammate@{}", name)),
640                    recipient_name: None,
641                    display_hint: DisplayHint::Normal,
642                });
643            }
644            messages.push(result_msg);
645            if let Some(last) = messages.last() {
646                append_messages(std::slice::from_ref(last));
647            }
648
649            // 清除当前工具名 + 切回 Thinking(工具执行完毕,下一轮 LLM 调用前)
650            if let Ok(mut ct) = current_tool.lock() {
651                *ct = None;
652            }
653            set_status(TeammateStatus::Thinking);
654        }
655
656        // Gate 触发后:drain inbox + 注入 system_reminder 让 agent 重新决策
657        if gate_triggered {
658            let len_before = messages.len();
659            let _ = drain_broadcast_messages(&mut messages, &broadcast_inbox);
660            if messages.len() > len_before {
661                append_messages(&messages[len_before..]);
662            }
663
664            if let Some(ref content) = gated_send_content {
665                let reminder = format!(
666                    "<system_reminder>\
667                    New messages arrived while you were thinking. Your pending SendMessage was held (attempt {}/{}).\n\
668                    Held content: {}\n\n\
669                    The new messages have been injected above. Please review and decide:\n\
670                    - Call SendMessage again (possibly revised) to send\n\
671                    - Or don't call SendMessage to discard the held message\
672                    </system_reminder>",
673                    send_gate_retries, MAX_SEND_GATE_RETRIES, content
674                );
675                messages.push(ChatMessage::text(MessageRole::User, &reminder));
676                if let Some(last) = messages.last() {
677                    append_messages(std::slice::from_ref(last));
678                }
679            }
680        }
681
682        // 本轮工具结果写入后同步快照
683        sync_messages(&messages);
684    }
685
686    // 通知团队:teammate 已完成
687    set_status(TeammateStatus::Completed);
688    // WorkDone 工具自己已广播过 [work completed],避免重复;其他路径(idle 超时等)补一次
689    // ★ 此消息通过双通道推送(display + context),会同步到 Main Agent 的 LLM 上下文(有意为之的设计)。
690    if !work_done.load(Ordering::Relaxed)
691        && let Ok(manager) = teammate_manager.lock()
692    {
693        let sender_label = format!("Teammate@{}", name);
694        let display_msg = ChatMessage::text(MessageRole::Assistant, "[work completed]")
695            .with_sender(&sender_label);
696        let context_msg = ChatMessage::text(
697            MessageRole::Assistant,
698            format!("<{}>[work completed]</{}>", sender_label, sender_label),
699        )
700        .with_sender(&sender_label);
701        if let Ok(mut display) = manager.display_messages.lock() {
702            display.push(display_msg);
703        }
704        if let Ok(mut context) = manager.context_messages.lock() {
705            context.push(context_msg);
706        }
707        // 同步写入独立 jsonl(不带 <Name> 前缀,合成时会加前缀)
708        let done_msg = ChatMessage::text(MessageRole::Assistant, "[work completed]".to_string());
709        append_messages(std::slice::from_ref(&done_msg));
710    }
711
712    if last_assistant_text.is_empty() {
713        format!("[Teammate '{}' completed with no output]", name)
714    } else {
715        last_assistant_text
716    }
717}
718
719/// 构建 teammate 专用的 system prompt
720///
721/// 从嵌入的模板文件加载并替换占位符:
722/// - `{{.base_prompt}}` — 主 agent 的 base system prompt
723/// - `{{.name}}` — teammate 名字
724/// - `{{.role}}` — teammate 角色
725/// - `{{.team_summary}}` — 团队成员列表摘要
726fn build_teammate_system_prompt(
727    name: &str,
728    role: &str,
729    base_prompt: Option<&str>,
730    teammate_manager: &Arc<Mutex<TeammateManager>>,
731) -> String {
732    let template = crate::template::teammate_system_prompt_template();
733    let base = base_prompt.unwrap_or("You are a helpful assistant.");
734    let team_summary = teammate_manager
735        .lock()
736        .map(|m| m.team_summary())
737        .unwrap_or_default();
738
739    template
740        .replace("{{.base_prompt}}", base)
741        .replace("{{.name}}", name)
742        .replace("{{.role}}", role)
743        .replace("{{.team_summary}}", &team_summary)
744}
745
746/// 从 broadcast_inbox 中 drain 广播消息到 messages
747/// 返回 true 表示有新消息
748fn drain_broadcast_messages(
749    messages: &mut Vec<ChatMessage>,
750    pending: &Arc<Mutex<Vec<ChatMessage>>>,
751) -> bool {
752    if let Ok(mut pending_msgs) = pending.lock() {
753        if pending_msgs.is_empty() {
754            return false;
755        }
756        messages.append(&mut *pending_msgs);
757        true
758    } else {
759        false
760    }
761}