【无标题】

╭─── Claude Code v2.1.181 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮

│ │ Tips for getting started │

│ Welcome back Xin! │ Run /init to create a CLAUDE.md file with instructions for Claude │

│ │ ───────────────────────────────────────────────────────────────────────────────────────────────────────────── │

│ ▐▛███▜▌ │ What's new │

│ ▝▜█████▛▘ │ Added /config key=value syntax to set any setting from the prompt (e.g. /config thinking=false) --- works ... │

│ ▘▘ ▝▝ │ Added sandbox.allowAppleEvents opt-in setting that lets sandboxed commands send Apple Events on macOS │

│ Opus 4.8 with high effort · Claude Pro · deeplearningman0723@gmail.com's Organization │ /release-notes for more │

│ F:\Experiment_laborotory │ │

╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

1 setup issue: plugins · /doctor

❯ /model

⎿ Set model to Opus 4.8 and saved as your default for new sessions

❯ 现在这个目录下有哪些内容?

Listed 1 directory

以下是 F:\Experiment_laborotory 目录下的内容:

文件夹

  • .codewhale/ --- CodeWhale 相关配置
  • .deepseek/ --- DeepSeek 相关配置
  • CodeWhale-main/ --- CodeWhale 项目主目录
  • collection-claude-code-source-code-main/ --- Claude Code 源码合集
  • data/ --- 数据目录
  • emotional_companion/ --- 情感陪伴相关项目

文档 (Markdown / PPT)

  • CodeWhale-SubAgent-DeepDive.md (25 KB) 及对应的 .pptx (31 KB)
  • MEMORY_EXPERIMENT_GUIDE.md (3 KB)
  • SPEC_DYNAMIC_AGENT_TOOL_PROMPT_PARITY.md (19 KB)
  • memory.md (1 KB)
  • _template.pptx (40 KB)、minimal_template.pptx (5 KB)

Python 脚本

  • demo_custom_subagent.py (27 KB)
  • five_gates.py (35 KB)、five_gates_v2.py (34 KB)
  • mcp_system.py (33 KB)

看起来这是一个围绕 CodeWhale / Claude Code 子智能体(SubAgent) 研究的实验目录。需要我深入看某个文件或子目录的内容吗?

✻ Cooked for 23s

❯ collection-claude-code-source-code-main/ --- Claude Code 源码合集 ,这个目录下有一个项目是我的自研agent,我想做的不仅仅是辅助coding,还可以类似hermes做长期skill沉淀和日常工作助手等,我现在在这个目录下还放了一份代码,F:\Experiment_laborotory\collection-claude-code-source-code-main\LilBot-agent-code\mewcode-python,这个项目也差不多,是别的人开源的,我要求你去读他的源代码,学通,然后对比我的,我们的目前有哪些明显的不足之处,那就去学习借鉴甚至哪怕是抄袭,都可以。尤其是subagent它支持组成team,teammate的概念,我的不行。

我来深入研读这两个项目。先了解整体结构。

Ran 2 shell commands

理解了:你的自研 agent 是 lilbot,参考项目是 mewcode-python。让我先把两个项目的代码结构都摸清楚。

Ran 1 shell command

mewcode 有完整的 teams/ 模块------这正是你说的 team/teammate 概念。现在看你自己的 lilbot 结构来对比。

Ran 1 shell command

两个项目结构清楚了。你的 lilbot 有 subagents/ 但完全没有 teams/,而 mewcode 有一整个 teams/ 模块(coordinator/mailbox/registry/shared_task/spawn_*/transcript)。让我深入读核心代码。先看规模,再精读关键文件。

Ran 2 shell commands

现在精读两边的关键实现。先读 mewcode 的 teams 模块全貌。

Read 2 files

核心 TeamManager 已清楚。继续读 coordinator、mailbox、shared_task、registry 和 spawn_inprocess。

Read 3 files

非常清晰。现在读 team 相关的工具层(agent_tool、team_create、send_message)和 agents 模块(loader、parser、tool_filter、task_manager)。

Read 1 file

Agent 工具的 team/teammate 分支非常完整。现在读 send_message、tool_filter、task_manager、shared_task、registry,以及 agents loader/parser。

Read 4 files

现在读 agents/task_manager、team_create 工具、progress,以及 agents loader/parser,理解后台任务与通知回流机制。

Read 1 file

理解了后台任务+mailbox 回流机制。读 team_create 工具和 progress,然后转向你的 lilbot。

Read 1 file

mewcode 的 team 架构已完整掌握。现在重点读你的 lilbot 现有实现:subagents/manager.py 和 core/delegation.py。

Read 1 file

delegation.py 已废弃。真正的子代理逻辑在 subagents/manager.py。读它,加上 render.py 和 core/agent.py。

Read 1 file

关键差异已经很清楚。再看几处来完整对比:lilbot 的 subagents/render.py(工具暴露)、core/agent.py(主循环 / 后台通知机制),以及 builtin.py 里的 agent 工具入口。

Read 2 files

现在看 lilbot 的 agent 工具入口(agent_open/agent_eval 等),确认对外的工具表面。

Searched for 1 pattern

已经掌握全貌。最后确认 mewcode 主循环如何把后台结果/队友消息异步注入对话(这是关键差异点)。

Searched for 1 pattern, read 1 file

两个项目我都读通了。下面是完整对比分析。


LilBot vs MewCode 深度对比

一、两者的子代理模型,本质不同

你的 lilbot 和 mewcode 都有"子代理",但它们是两种不同范式:

┌──────────────────────┬────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────┐

│ 维度 │ LilBot(你) │ MewCode(参考) │

├──────────────────────┼────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┤

│ 子代理生命周期 │ 一次性任务(open→run→completed),可追加 follow-up │ 两种:一次性 sub-agent + 长驻 teammate │

├──────────────────────┼────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┤

│ 任务结束后 │ 进程结束,结果留在 task store │ teammate 保持存活,idle 等待下一个任务 │

├──────────────────────┼────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┤

│ 代理间通信 │ ❌ 无 │ ✅ Mailbox 文件邮箱(点对点 + 广播 + 文件锁) │

├──────────────────────┼────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┤

│ 共享任务板 │ ❌ 无 │ ✅ SharedTaskStore(pending/in_progress/blockecked_by 依赖) │

├──────────────────────┼────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┤

│ 按名字寻址 │ ❌ 只有 task_id │ ✅ AgentNameRegistry(SendMessage(to="reviewer")) │

├──────────────────────┼────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┤

│ 团队分组 │ ❌ 无 team 概念 │ ✅ AgentTeam(lead + members,持久化到 ~/.mewcode/teams//) │

├──────────────────────┼────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┤

│ 结果回流父代理 │ 轮询(agent_eval block=true 阻塞等) │ 异步注入:主循环每轮 drain 邮箱+完成队列,作为 system-reminder 注入 │

├──────────────────────┼────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┤

│ 协调者模式 │ ❌ 无 │ ✅ Coordinator mode:把 lead 工具收窄到只剩调度工具,强制它编排而非自己干活 │

├──────────────────────┼────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┤

│ 并行可视化 │ 后台线程(不可见) │ in-process / tmux pane / iTerm2 pane(每个队友一个可见终端) │

├──────────────────────┼────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────┤

│ 子代理能否再开子代理 │ ❌ 禁止(扁平) │ teammate 拿到协调工具,可参与多边协作 │

└──────────────────────┴────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────┘

二、你明显不足的地方(按重要性)

  1. 没有"持久队友 + 团队"这一整层(核心缺失)

    你的子代理是"派出去→拿回结果→结束"的单向漏斗。mewcode 的 teammate 是"招进团队→长期在岗→双向通信→共享看板"。你说的对------这正是你最缺的。

  2. 没有代理间通信(Mailbox)

    你的子代理之间是孤岛。mewcode 用文件邮箱实现了 reviewer↔implementer 互相喊话、lead 广播。

  3. 没有共享任务板(带依赖)

    mewcode 的 SharedTaskStore 让多个代理认领任务、标记阻塞关系,实现真正的并行分工。你只有父代理脑子里的隐式计划。

  4. 结果回流靠轮询,不是异步推送

    mewcode 在主 agent loop 每轮开头 _consume_mailbox() + notification_fn(),把"队友完成了""队友发来消息"作为 system-reminder 注入------lead 边干自己的活边收到进度。你的 agent_eval block=true 会阻塞,父代理干等。

  5. 没有协调者模式

    对"长期工作助手"这种多智能体编排场景,coordinator mode(把 lead 工具收窄成纯调度)能逼模型做正确的"项目经理"行为。

三、你反而做得更好、迁移时别丢掉的东西

mewcode 不如你的地方,迁移 team 时要保留你的优势:

  • ✅ 创建门禁(gates):你的 _validate_creation_gates + 运行时 gate 比 mewcode 的 tool_filter 更严谨(mewcode 基本只是集合过滤)。
  • ✅ 持久化 + 重启恢复:你的 subagent-tasks.json + transcript + _resume_recovered_tasks,mewcode 的后台任务重启就丢。
  • ✅ 动态工具描述渲染(render.py):把活的 agent 类型/状态注入工具描述,这是对的方向,mewcode 用静态 prompt。
  • ✅ 更丰富的角色预设(implementer/verifier/critic/tool_agent...)。

四、建议的借鉴方案(把 teams 移植进 lilbot)

mewcode 的 teams/ 几乎可以整体抄过来,它依赖很干净(纯文件 + dataclass,无外部库)。映射到你的架构:

● User declined to answer questions

· 移植 teams 时,队友的执行后端先做哪种? (仅 in-process(推荐) / in-process + tmux)

· 队友间协作的复杂度,第一版做到哪一档? (全套(推荐) / 先只做通信+回流)

· 现在就开始写代码,还是先出一份详细落地设计文档? (先出设计文档 / 直接开始实现)

你想先澄清一下。请告诉我你想澄清什么------可能是对我上面的对比分析有疑问,或者对接下来怎么做有自己的想法。

几个可能你想聊的方向(也欢迎完全跳出这些):

  • 对分析本身:哪些"不足"你不认同,或者你觉得我漏看了 lilbot 已有的某些能力?(比如你可能已经有某种通信/协调机制我没读到)
  • 对 team 的定位:你心里的"teammate 组队"具体长什么样?是偏 coding 协作(implementer+reviewer+verifier),还是更偏你说的"hermes 长期 skill 沉淀 / 日常工作助手"那种长期在岗的助理团队?这两者对 team 的设计要求不太一样。
  • 对范围:你是想完整照搬 mewcode 的 teams,还是只取其中某几个点(比如只要 Mailbox 通信、或只要共享任务板)?
  • 对工作方式:你是想我直接动手改代码,还是先讨论清楚设计、或者先看我对某个具体模块更细的解读?

你想从哪一点开始?

✻ Churned for 4m 11s

❯ 你给我的选择提到了Tmux这是干嘛的?我的lilbot是已经模仿开源项目codewhale写了CLI的界面?你指的是这个?

好问题,这俩是两回事,我来分清楚。

tmux 是什么、mewcode 用它干嘛

tmux(terminal multiplexer)是 Linux/macOS 上的一个终端工具,能把一个终端窗口切成多个 pane(分屏),每个 pane 跑一个独立的进pp,也能分屏。

mewcode 用它,是为了实现**"每个队友开在一个独立的、你能亲眼看到的终端分屏里"**:

┌─────────────┬─────────────┐

│ lead (你) │ reviewer │ ← 每个 pane 是一个独立 mewcode 进程

├─────────────┼─────────────┤ 在各自的 git worktree 里干活

│ implementer │ verifier │ 你能实时看到每个队友在干啥

└─────────────┴─────────────┘

它在 teams/spawn_tmux.py / spawn_iterm2.py 里,做的事就是:调 tmux split-window 开个新分屏 → 在里面 cd 到队友的 worktree → 启动一个新的 mewcode 进程当队友。队友之间通过文件邮箱通信。

所以 tmux 只是"队友进程的承载方式"之一。mewcode 有三种后端:

  • in-process:队友就是主进程里的一个 asyncio 任务(看不见,但最简单)
  • tmux:队友是独立进程,开在 tmux 分屏里(能看见)
  • iterm2:同上,但用 iTerm2

跟你的 CLI / TUI 完全没关系

你问的"我 lilbot 模仿 codewhale 写的 CLI 界面"------那是你的 lilbot/tui/(classic.py / dashboard.py / windows_console.py),是你自己这一个进程的渲染界面,由你自己画。

tmux 是操作系统层面把多个独立进程摆在一个屏幕上,跟你画的 TUI 不是一层东西。而且:

  • 你在 Windows 上(Win10)。tmux 在 Windows 上基本用不了(要 WSL/Cygwin)。
  • 所以对你来说 tmux/iTerm2 这条路价值很低,我当时把它列成选项只是照搬 mewcode 的能力清单。

结论

移植 team 的时候,队友直接用 in-process 后端就行------也就是复用你现在的线程模型,队友是你主进程里的一个后台线程,跟你已有的 subagent 跑法一致。tmux 那套直接跳过。

如果你以后想要"可视化看到每个队友实时进度",正确的做法不是上 tmux,而是在你自己的 tui/dashboard.py 里加一个队友面板(mewcode 的 teams/progress.py 就是干这个的------每个队友一个 progress 对象,记录它在跑什么工具、用了多少 token),把这些渲染到你自己的界面里。这才是跟你 CLI 同一层、且跨平台的方案。

那基于这个澄清,我重新问你最关键的两点:

● User answered Claude's questions:

· 队友执行后端确认: → in-process + 仪表盘面板

· 第一版协作能力做到哪一档? → 全套(推荐)

两个方向都定了:in-process 后端 + 仪表盘面板,协作能力做全套(Mailbox + 共享任务板带依赖 + 名字寻址 + 异步结果回流)。

在动手前我先把 lilbot 的几个集成点摸准(state 目录、ToolContext 结构、工具注册方式、主循环驱动位置、dashboard 渲染入口),这样设计文档和代码才能严丝合缝对上你现有架构。

Read 3 files

整个集成链路我已经摸透了(ToolContext 字段、state_dir、Agent.run_turn 主循环、SubAgentManager 线程模型、ToolDef/_schema 注册方式、build_runtime 装配、dashboard 入口)。

鉴于这是一次跨 ~8 个新文件 + 主循环 + dashboard 的较大改动,而且你这个仓库本来就是 SPEC 驱动(已有 SPEC_DYNAMIC_AGENT_TOOL_PROMPT_PARITY.md),我先产出一份可直接落地的设计文档,对齐你现有架构、保留你的 gates/持久化/动态渲染优势。你过一遍再让我编码。

● Write(SPEC_TEAMS_AND_TEAMMATES.md)

Wrote 325 lines to SPEC_TEAMS_AND_TEAMMATES.md

SPEC: LilBot Teams & Teammates(团队 / 长驻队友)

状态:草案,待评审

来源:借鉴 mewcode-python/mewcode/teams/* + agents/task_manager.py,适配 LilBot 架构

  1. 持久化位置:我选了 workspace 级 .lilbot/teams/(按项目隔离)。如果你更想要 mewcode 那样的用户级跨项目共享,告诉我。
  2. lead 身份:我用固定字符串 "lead" 简化(单 lead 场景)。
  3. 抽取 run_agent_turn:这是让队友复用你 gates 的关键改动,会动到 subagents/manager.py,确认可以动。

确认后我就按 T1→T8 开始写。要不要我现在先把无副作用的 T1 数据层(5 个文件照抄改路径)落下来,让你先看到实物?

✻ Cooked for 3m 39s

❯ 仅 in-process(推荐)

用你现有的线程模型跑长驻队友,最快落地,无外部依赖。tmux/iTerm2 可视化后端以后再加。

确认:in-process 后端。这跟 SPEC 的设定一致。那三个待确认项我就按 SPEC 的默认值推进(workspace 级 .lilbot/teams/、lead 固定 id "lead"、抽取 run_agent_turn)------如果后面要改随时说。

现在落地 T1 数据层(无副作用,5 个文件)。先建 lilbot/teams/ 模块。

● Write(lilbot\teams\models.py)

Wrote 147 lines to lilbot\teams\models.py

"""Team / teammate data models.

Ported from mewcode's teams/models.py, adapted to LilBot:

  • persistence root is workspace-level <state_dir>/teams/<slug> instead of
    the user-level ~/.mewcode/teams so teams are isolated per project, matching
    the existing .lilbot/agents layout.
  • only the in-process backend is supported (tmux/iterm2 dropped).
    """

from future import annotations

... +137 lines

● Write(lilbot\teams\mailbox.py)

Wrote 164 lines to lilbot\teams\mailbox.py

""File-based mailbox for inter-agent messaging.

orted from mewcode's teams/mailbox.py. Each agent gets a {agent_id}.json

nbox under base_dir , guarded by a companion .lock file (O_EXCL create,

0 retries, 10s staleness reclaim) so concurrent teammate threads --- and even

eparate processes --- can append safely.

""

rom future import annotations

... +154 lines

● Write(lilbot\teams\shared_task.py)

Wrote 134 lines to lilbot\teams\shared_task.py

"""Shared task board for a team.

Ported from mewcode's teams/shared_task.py. A small JSON-backed kanban with

import json

... +124 lines

● Write(lilbot\teams\registry.py)

Wrote 47 lines to lilbot\teams\registry.py

"""Process-wide name -> agent_id registry for addressing teammates by name.

Ported from mewcode's teams/registry.py. In-process teammates run as threads

in this process, so a process-local singleton is sufficient. (A multi-process

backend would need a file-backed store instead.)

"""

from future import annotations

import threading

... +37 lines

现在读 mewcode 的 progress.py 以准确移植,再写 init.py。

Read 1 file

● Write(lilbot\teams\progress.py)

Wrote 109 lines to lilbot\teams\progress.py

"""Per-teammate progress tracker for the dashboard.

Ported from mewcode's teams/progress.py. Thread-safe (the in-process teammate

loop writes from its own thread while the dashboard reads). Tool descriptions

use LilBot tool names. The _lock field is excluded from any serialization.

"""

from future import annotations

import random

... +99 lines

● Write(lilbot\teams_init _.py)

Wrote 31 lines to lilbot\teams_init _.py

└────────────────┴─────────────────────────────────────────────────────────────┴───────────────────────────────────────┘

这一步零副作用:没动任何现有文件,纯新增模块,现有行为完全不变。


下一步 T2:写 TeamManager(去掉 tmux、worktree 复用你的逻辑),并接线 ------ ToolContext 加 teams 字段、cli.build_runtime 装配、agent.agent_id = "lead"。这一步开始轻改动 3 个现有文件(tools/registry.py、cli.py、core/agent.py 仅加字段/装配,不改逻辑)。

我继续推进 T2 吗?还是你想先看看 T1 的代码细节?

✻ Churned for 2m 30s

❯ 1,先给我说我怎么可以在用的时候感受到你的改动; 2,然后你就继续推进T2 T3等后续

先回答"怎么感受到改动",然后我连续推进 T2/T3。先确认 lilbot 的启动方式和当前 provider 配置。

Read 1 file, ran 1 shell command

你的环境已配好 DeepSeek key,启动方式是 lilbot(或 python -m lilbot),默认进 dashboard 界面。

  1. 你将怎样"感受到"这些改动

最早能体验的时间点是 T5 完成后(工具层就绪),T6/T7 让它更直观。届时你这样用:

a) 自然语言触发(核心体验)------在 lilbot 里直接说:

建个团队,让一个 implementer 去改 foo.py 的 bug,完成后让一个 reviewer 复查它的改动

你会看到 lead 依次:建团队 → 派 implementer(后台长驻线程)→ implementer 干完用 SendMessage 回报并转 idle → lead 在下一轮自动收到 → 派 reviewer → 收齐两边结果后综合回复你。关键差别:lead 不阻塞干等,队友消息是异步"飘"回来的。

b) 新增 slash 命令(直接观测):

  • /team list ------ 看当前团队、成员、谁在干活/谁 idle
  • /team msg impl 继续改下一个函数 ------ 手动给某队友发消息、唤醒它(调试用)

c) Dashboard 队友面板(T7,最直观):界面上多一块实时面板,每个队友一行:

Teammates

impl running Editing foo.py... tools=4 tok=2.1k

rev idle waiting tools=2 tok=0.9k

能亲眼看到每个队友在跑什么工具、烧了多少 token。

对比现在:你现在的 subagent 是"派出去→agent_eval 阻塞等→拿回结果",看不到中间过程、代理之间不能互相喊话、没有团队这个长期容器。改完之后这三点全有了。

▎ 说明:T2/T3 是地基(manager + 复用执行核心),本身没有用户可见变化;真正能"用起来"是 T4(长驻队友)+ T5(工具)。所以我现在一口气推到 T6,让你能跑通端到端。


  1. 继续推进 ------ 先 T2:TeamManager + 接线

写 TeamManager(去 tmux、复用你的 worktree 清理)。

● Write(lilbot\teams\manager.py)

Wrote 247 lines to lilbot\teams\manager.py

""TeamManager: lifecycle + persistence + notification routing for teams.

Now wire it in: ToolContext + cli.build_runtime + agent.agent_id.

● Update(lilbot\tools\registry.py)

Added 1 line

subagents: Any

mcp: Any

config: Any

teams: Any = None

class ToolRegistry:

● Update(lilbot\cli.py)

Added 5 lines, removed 2 lines

max_concurrent=cfg.subagent_max_concurrent,

)

mcp = MCPManager(cfg.state_dir, cfg.workspace)

teams = TeamManager(cfg.state_dir)

registry = ToolRegistry()

register_builtins(registry)

ctx = ToolContext(sandbox, permissions, memory, skills, subagents, mcp, cfg)

ctx = ToolContext(sandbox, permissions, memory, skills, subagents, mcp, cfg, teams)

subagents.configure_tools(registry, ctx)

return Agent(cfg, provider, registry, ctx), registry, ctx

agent = Agent(cfg, provider, registry, ctx)

agent.agent_id = "lead"

return agent, registry, ctx

def normalize_model_name(value: str) -> str | None:

● Update(lilbot\cli.py)

Added 1 line

from .sandbox import PermissionManager, Sandbox

from .skills import SkillRegistry

from .subagents import SubAgentManager

from .teams.manager import TeamManager

from .tools import ToolContext, ToolRegistry, register_builtins

from .tui.classic import LilBotUI

from .tui.windows_console import configure_windows_console, console_font_status

在 Agent.init 里加默认 agent_id,避免别处访问时缺属性。

● Update(lilbot\core\agent.py)

Added 1 line

self.provider = provider

self.registry = registry

self.ctx = ctx

self.agent_id = "lead"

self.messages: listdict\[str, Any] = [

{"role": "system", "content": build_system_prompt(ctx.memory, ctx.skills)}

]

Now verify T2 wiring imports cleanly and the runtime builds.

Ran 1 shell command

T2 通过------完整运行时能装配,ctx.teams 与 agent.agent_id 就位,回流格式正确。

进入 T3:把 SubAgentManager._run 的单轮执行核心抽成可复用的 run_agent_turn(...),让长驻队友每轮复用你的 gates/工具过滤/transcript。先看 ProviderTurn/ToolCall 结构和现有测试,确保重构不改变既有行为。

Read 1 file, ran 1 shell command

先跑一遍现有测试建立基线(重构前确保全绿)。

Ran 1 shell command

基线全绿(109 passed)。现在做行为保持的重构。先给 _execute_tool_call 加可选 run_ctx 参数(队友用固定 worktree ctx)。

● Update(lilbot\subagents\manager.py)

Added 1 line, removed 1 line

clone.config = task_config

return clone

复制代码
def _execute_tool_call(self, definition: AgentDefinition, task: SubAgentTask, name: str, arguments: dict[str, Any]) -> str:
def _execute_tool_call(self, definition: AgentDefinition, task: SubAgentTask, name: str, arguments: dict[str, Any], run_ctx: Any = None) -> str:
    if self.registry is None or self.ctx is None:
        return f"Tool unavailable in this subagent runtime: {name}"
    allowed = self._effective_allowed_tools(definition, task)

● Update(lilbot\subagents\manager.py)

Added 1 line, removed 1 line

name,

"The role preset disallows this tool.",

)

result, elapsed_ms = self.registry.execute(name, arguments or {}, self._ctx_for_task(task))

result, elapsed_ms = self.registry.execute(name, arguments or {}, run_ctx or self._ctx_for_task(task))

if not result.ok and result.metadata.get("gate"):

return self._runtime_gate_message(

task,

Now extract the tool loop into run_agent_turn and rewrite _run to call it. Replacing the loop body in _run:

● Update(lilbot\subagents\manager.py)

Added 1 line, removed 55 lines

task.error = ""

self._persist_tasks_locked()

self._append_transcript(task, "running", {"agent_type": task.agent_type})

messages = [

{"role": "system", "content": f"{definition.system_hint}\n\n{self._tool_policy_hint(definition, task)}\n\n{OUTPUT_CONTRACT}"},

{"role": "user", "content": task.prompt},

]

transcript: liststr = \[\]

try:

if not self._prepare_worktree(task):

return

content = ""

tool_schemas = self._tool_schemas_for_task(definition, task)

steps = 0

while steps <= SUBAGENT_MAX_TOOL_STEPS:

turn = self.provider(messages, tool_schemas)

self._append_transcript(

task,

"provider_turn",

{

"content": turn.content,

"tool_calls": [

{"name": call.name, "arguments": call.arguments, "call_id": call.call_id}

for call in turn.tool_calls

],

"usage": turn.usage,

},

)

if turn.content.strip():

content = turn.content.strip()

transcript.append(content)

if not turn.tool_calls:

break

if steps >= SUBAGENT_MAX_TOOL_STEPS:

content = self._step_limit_report(transcript)

break

messages.append(_assistant_tool_message(turn))

for call in turn.tool_calls: max(0, SUBAGENT_MAX_TOOL_STEPS - steps):

self._append_transcript(

task,

"tool_started",

{"name": call.name, "arguments": call.arguments, "call_id": call.call_id},

)

tool_output = self._execute_tool_call(definition, task, call.name, call.arguments)

steps += 1

transcript.append(f"{call.name}: {tool_output:1000}")

self._append_transcript(

task,

"tool_finished",

{"name": call.name, "call_id": call.call_id, "output": tool_output},

)

messages.append({

"role": "tool",

"tool_call_id": call.call_id,

"name": call.name,

"content": tool_output,

})

if steps >= SUBAGENT_MAX_TOOL_STEPS:

break

content = content or "(subagent returned no text)"

task.result = self._ensure_output_contract(content)

task.result = self.run_agent_turn(definition, task, task.prompt)

completed = False

with self._lock:

if task.status != "cancelled":

Now add the run_agent_turn method (with optional progress + run_ctx for teammate reuse) right before _ensure_output_contract:

● Update(lilbot\subagents\manager.py)

Added 86 lines

if acquired:

self._semaphore.release()

复制代码
def run_agent_turn(
    self,
    definition: AgentDefinition,
    task: SubAgentTask,
    prompt: str,
    *,
    progress: Any = None,
    run_ctx: Any = None,
    max_steps: int | None = None,
) -> str:
    """Execute one full provider+tool loop and return the contract-wrapped result.

    Extracted from ``_run`` so long-running teammates can reuse the exact same
    gates, tool filtering, and transcript machinery for every turn. ``progress``
    (a TeammateProgress) is updated live for the dashboard; ``run_ctx`` pins the
    tool context (e.g. a teammate's prepared worktree) instead of re-deriving it.
    """
    limit = SUBAGENT_MAX_TOOL_STEPS if max_steps is None else max(1, int(max_steps))
    messages = [
        {"role": "system", "content": f"{definition.system_hint}\n\n{self._tool_policy_hint(definition, task)}\
        {"role": "user", "content": prompt},
    ]
    transcript: list[str] = []
    content = ""
    tool_schemas = self._tool_schemas_for_task(definition, task)
    steps = 0
    while steps <= limit:
        turn = self.provider(messages, tool_schemas)
        if progress is not None:
            self._record_progress_usage(progress, turn)
        self._append_transcript(
            task,
            "provider_turn",
            {
                "content": turn.content,
                "tool_calls": [
                    {"name": call.name, "arguments": call.arguments, "call_id": call.call_id}
                    for call in turn.tool_calls
                ],
                "usage": turn.usage,
            },
        )
        if turn.content.strip():
            content = turn.content.strip()
            transcript.append(content)
            if progress is not None:
                progress.set_message(content[:400])
        if not turn.tool_calls:
            break
        if steps >= limit:
            content = self._step_limit_report(transcript)
            break
        messages.append(_assistant_tool_message(turn))
        for call in turn.tool_calls[: max(0, limit - steps)]:
            if progress is not None:
                progress.record_tool_use(call.name, call.arguments)
            self._append_transcript(
                task,
                "tool_started",
                {"name": call.name, "arguments": call.arguments, "call_id": call.call_id},
            )
            tool_output = self._execute_tool_call(definition, task, call.name, call.arguments, run_ctx=run_ctx)
            steps += 1
            transcript.append(f"{call.name}: {tool_output[:1000]}")
            self._append_transcript(
                task,
                "tool_finished",
                {"name": call.name, "call_id": call.call_id, "output": tool_output},
            )
            messages.append({
                "role": "tool",
                "tool_call_id": call.call_id,
                "name": call.name,
                "content": tool_output,
            })
            if steps >= limit:
                break
    return self._ensure_output_contract(content or "(subagent returned no text)")

def _record_progress_usage(self, progress: Any, turn: Any) -> None:
    usage = getattr(turn, "usage", None) or {}
    try:
        progress.record_tokens(int(usage.get("prompt_tokens", 0) or 0), int(usage.get("completion_tokens", 0) or 0))
    except Exception:
        pass

def _ensure_output_contract(self, content: str) -> str:
    if "SUMMARY:" in content and "BLOCKERS:" in content:
        return content

Re-run the full test suite to confirm the refactor preserved existing behavior.

Ran 1 shell command

T3 完成,109 测试仍全绿------重构行为零变化。

进入 T4+T5。这里我对 SPEC 做一个更贴合 lilbot 风格的调整:lilbot 的工具是"无状态 handler 读 ctx"的模式,所以我不用 mewcode 那种"给每个队友建私有 registry",而是把协调工具注册进全局 registry,让它们从 ctx 读"我是谁、属于哪个团队"。队友运行时给它一个带身份的 ctx 克隆即可。更干净、也让 lead 自动获得 SendMessage/TeamCreate。

先给 ToolContext 加身份字段:

● Update(lilbot\tools\registry.py)

Added 5 lines

mcp: Any

config: Any

teams: Any = None

Identity of the current agent within a team. None => the lead/root agent.

Teammates run with a ctx clone where these are set, so coordination tools

(SendMessage / Task*) know who is acting and which team they belong to.

team_name: Any = None

agent_name: Any = None

class ToolRegistry:

Now write the long-running teammate loop (T4):

● Write(lilbot\teams\spawn_inprocess.py)

Wrote 129 lines to lilbot\teams\spawn_inprocess.py

""In-process long-running teammate loop (threading port of mewcode's asyncio version).

teammate runs one full agent turn (reusing SubAgentManager.run_agent_turn so

t inherits gates / tool filtering / transcript), then goes idle, notifies the lead,

nd polls its mailbox for the next prompt or a shutdown request --- staying alive

cross turns, unlike a one-shot subagent.

""

rom future import annotations

... +119 lines

T4 写完。现在 T5 工具层。先读 builtin.py 的相关片段:_schema 辅助、_agent_open 系列函数、以及工具注册区。

Read 1 file

现在看 _schema 辅助和工具注册区,以及顶部 import。

Read 1 file

已有 task_create 等(耐久任务记录),为避免冲突,团队工具用独立命名:team_create / team_delete / send_message / team_task_create|list|get|update。看一下 _schema/_string/_bool 辅助和 register_builtins 结尾。

Read 1 file, ran 1 shell command

全部辅助和注册位置都清楚了。开始写 T5。先在 subagents/manager.py 加协调工具名常量和 build_teammate_task(创建带协调工具的队友任务、跑创建门禁,但不启动线程)。

● Update(lilbot\subagents\manager.py)

Added 11 lines

SUBAGENT_MAX_TOOL_STEPS = 6

DEFAULT_SUBAGENT_MAX_CONCURRENT = 8

Coordination tools injected into every teammate so it can talk to the team and

use the shared task board. Registered globally in builtin.py; teammates read

their identity (team_name / agent_name) from a ctx clone.

TEAM_COORDINATION_TOOL_NAMES = [

"send_message",

"team_task_create",

"team_task_list",

"team_task_get",

"team_task_update",

]

READ_ONLY_CODE_TOOLS = [

"project_map",

"list_dir",

● Update(lilbot\subagents\manager.py)

Added 50 lines

self._run(task)

return task

复制代码
def build_teammate_task(
    self,
    agent_type: str | None,
    prompt: str,
    *,
    name: str | None = None,
    allowed_tools: list[str] | None = None,
    model: str | None = None,
) -> SubAgentTask:
    """Create (but do not run) a persisted task for a long-running teammate.

    The teammate's tool set = its role definition's tools plus the team
    coordination tools, so it can always message the team and use the shared
    task board. Reuses the same creation gates as ``open``.
    """
    canonical = self.resolve_type(agent_type)
    definition = self.definitions[canonical]
    base_tools = definition.allowed_tools if allowed_tools is None else allowed_tools
    effective = _dedupe_tools([*base_tools, *TEAM_COORDINATION_TOOL_NAMES])
    creation_gates = self._validate_creation_gates(
        canonical,
        definition,
        effective,
        explicit_allowed_tools=allowed_tools is not None or bool(definition.allowed_tools),
    )
    failures = [g for g in creation_gates if g.get("status") == "failed" and int(g.get("gate_number", 0)) <= 3]
    if failures:
        raise SubAgentGateError(failures)
    task_id = f"sub_{uuid4().hex[:10]}"
    task = SubAgentTask(
        id=task_id,
        name=name or task_id,
        agent_type=canonical,
        prompt=prompt,
        allowed_tools=effective,
        model=model,
        gate_results=creation_gates,
        status="running",
        started_at=time.time(),
    )
    with self._lock:
        self.tasks[task.id] = task
        self._persist_tasks_locked()
    self._append_transcript(task, "teammate_spawned", {"agent_type": canonical, "name": task.name})
    return task

def slot(self):
    """Context manager bounding active teammate turns by max_concurrent."""
    return self._semaphore

def eval(
    self,
    task_ref: str,

Now add the team tool handlers in builtin.py, before _agent_open:

● Update(lilbot\tools\builtin.py)

Added 278 lines

)

── Team coordination tools ──────────────────────────────────────────────

def _str_list(value: Any) -> liststr:

if value in (None, ""):

return \[\]

if isinstance(value, list):

return str(v).strip() for v in value if str(v).strip()

return v.strip() for v in str(value).split(",") if v.strip()

def _actor_name(ctx: ToolContext) -> str:

return str(getattr(ctx, "agent_name", None) or "lead")

def _resolve_team(ctx: ToolContext, args: dictstr, Any) -> tuplestr \| None, str:

"""Resolve which team a coordination call targets.

复制代码
Priority: explicit ``team`` arg -> the caller's own team (teammate ctx) ->
the sole existing team. Returns ``(team_name, "")`` or ``(None, error)``.
"""
teams = getattr(ctx, "teams", None)
if teams is None:
    return None, "Team system is unavailable in this runtime."
explicit = str(args.get("team") or args.get("team_name") or "").strip()
if explicit:
    return (explicit, "") if teams.get_team(explicit) else (None, f"Team '{explicit}' not found.")
if getattr(ctx, "team_name", None):
    return str(ctx.team_name), ""
all_teams = teams.list_teams()
if not all_teams:
    return None, "No team exists. Create one with team_create first."
if len(all_teams) == 1:
    return all_teams[0].name, ""
return None, "Multiple teams exist; pass 'team' to disambiguate."

def _team_create(args: dictstr, Any, ctx: ToolContext) -> ToolResult:

teams = getattr(ctx, "teams", None)

if teams is None:

return ToolResult(False, "Team system is unavailable in this runtime.")

name = str(args.get("team_name") or args.get("name") or "").strip()

if not name:

return ToolResult(False, "team_create requires 'team_name'.")

team = teams.create_team(name, "lead", str(args.get("description") or ""))

return ToolResult(

True,

_json({

"team": team.name,

"config": team.config_path,

"next": (

"Spawn teammates with Agent(team_name='%s', name=..., subagent_type=...). "

"Talk to them with send_message; they report back to 'lead' and go idle." % team.name

),

}),

{"team": team.name},

)

def _team_delete(args: dictstr, Any, ctx: ToolContext) -> ToolResult:

teams = getattr(ctx, "teams", None)

if teams is None:

return ToolResult(False, "Team system is unavailable in this runtime.")

name = str(args.get("team_name") or args.get("name") or "").strip()

if not name:

return ToolResult(False, "team_delete requires 'team_name'.")

try:

teams.delete_team(name)

except Exception as exc: # noqa: BLE001

return ToolResult(False, f"Failed to delete team '{name}': {exc}")

return ToolResult(True, f"Team '{name}' deleted.")

def _team_list(args: dictstr, Any, ctx: ToolContext) -> ToolResult:

teams = getattr(ctx, "teams", None)

if teams is None:

return ToolResult(False, "Team system is unavailable in this runtime.")

data = \[\]

for team in teams.list_teams():

data.append({

"team": team.name,

"description": team.description,

"members": [

{"name": m.name, "agent_type": m.agent_type,

"active": m.is_active,

"status": getattr(getattr(m, "progress", None), "status", None)}

for m in team.members

],

})

return ToolResult(True, _json(data), {"count": len(data)})

def _send_message(args: dictstr, Any, ctx: ToolContext) -> ToolResult:

team_name, err = _resolve_team(ctx, args)

if err:

return ToolResult(False, err)

teams = ctx.teams

to = str(args.get("to") or args.get("recipient") or "").strip()

message = str(args.get("message") or args.get("content") or "")

if not to or not message:

return ToolResult(False, "send_message requires 'to' and 'message'.")

from ...teams.mailbox import create_message

复制代码
team = teams.get_team(team_name)
mailbox = teams.get_mailbox(team_name)
if team is None or mailbox is None:
    return ToolResult(False, f"Team '{team_name}' is not ready.")
actor = _actor_name(ctx)
summary = str(args.get("summary") or message[:60])
msg_type = str(args.get("message_type") or "text")

if to == "*":
    targets = [m.name for m in team.members if m.name != actor]
    if actor != "lead":
        targets.append("lead")
    mailbox.broadcast(targets, create_message(actor, "*", message, summary, msg_type))
    return ToolResult(True, f"Broadcast to {len(targets)} recipient(s) in '{team_name}'.")

if to in ("lead", team.lead_agent_id):
    key = "lead"
else:
    member = team.get_member(to)
    key = member.name if member else None
if not key:
    return ToolResult(False, f"Cannot resolve recipient '{to}' in team '{team_name}'.")
mailbox.write(key, create_message(actor, key, message, summary, msg_type))
return ToolResult(True, f"Message sent to '{key}' in team '{team_name}'.")

def _team_task_create(args: dictstr, Any, ctx: ToolContext) -> ToolResult:

team_name, err = _resolve_team(ctx, args)

if err:

return ToolResult(False, err)

store = ctx.teams.get_task_store(team_name)

if store is None:

return ToolResult(False, f"Team '{team_name}' has no task store.")

title = str(args.get("title") or args.get("prompt") or "")

if not title:

return ToolResult(False, "team_task_create requires 'title'.")

task = store.create(

title=title,

description=str(args.get("description") or ""),

assignee=str(args.get("assignee") or ""),

blocks=_str_list(args.get("blocks")),

blocked_by=_str_list(args.get("blocked_by")),

created_by=_actor_name(ctx),

)

return ToolResult(True, _json(task.to_dict()), {"task_id": task.id, "team": team_name})

def _team_task_list(args: dictstr, Any, ctx: ToolContext) -> ToolResult:

team_name, err = _resolve_team(ctx, args)

if err:

return ToolResult(False, err)

store = ctx.teams.get_task_store(team_name)

if store is None:

return ToolResult(False, f"Team '{team_name}' has no task store.")

tasks = store.list_tasks(args.get("status") or None, args.get("assignee") or None)

return ToolResult(True, _json(t.to_dict() for t in tasks), {"count": len(tasks), "team": team_name})

def _team_task_get(args: dictstr, Any, ctx: ToolContext) -> ToolResult:

team_name, err = _resolve_team(ctx, args)

if err:

return ToolResult(False, err)

store = ctx.teams.get_task_store(team_name)

if store is None:

return ToolResult(False, f"Team '{team_name}' has no task store.")

task = store.get(str(args.get("task_id") or ""))

if task is None:

return ToolResult(False, "Shared task not found.")

return ToolResult(True, _json(task.to_dict()), {"task_id": task.id})

def _team_task_update(args: dictstr, Any, ctx: ToolContext) -> ToolResult:

team_name, err = _resolve_team(ctx, args)

if err:

return ToolResult(False, err)

store = ctx.teams.get_task_store(team_name)

if store is None:

return ToolResult(False, f"Team '{team_name}' has no task store.")

task = store.update(

str(args.get("task_id") or ""),

status=args.get("status"),

assignee=args.get("assignee"),

description=args.get("description"),

add_blocks=_str_list(args.get("add_blocks")) or None,

add_blocked_by=_str_list(args.get("add_blocked_by")) or None,

)

if task is None:

return ToolResult(False, "Shared task not found.")

return ToolResult(True, _json(task.to_dict()), {"task_id": task.id})

def _spawn_teammate(args: dictstr, Any, ctx: ToolContext) -> ToolResult:

from dataclasses import replace as _dc_replace

复制代码
teams = getattr(ctx, "teams", None)
subagents = getattr(ctx, "subagents", None)
if teams is None or subagents is None:
    return ToolResult(False, "Team runtime is unavailable.")
team_name = str(args.get("team_name") or "").strip()
team = teams.get_team(team_name)
if team is None:
    return ToolResult(False, f"Team '{team_name}' not found. Create it first with team_create.")
prompt = str(args.get("prompt") or args.get("message") or args.get("objective") or "")
if not prompt:
    return ToolResult(False, "Missing prompt.")

base = str(args.get("name") or args.get("subagent_type") or args.get("type") or "worker").strip() or "worker"
existing = {m.name for m in team.members}
teammate_name = base
i = 2
while teammate_name in existing:
    teammate_name = f"{base}-{i}"
    i += 1

agent_type = args.get("subagent_type") or args.get("type") or args.get("agent_type") or args.get("role")
try:
    task = subagents.build_teammate_task(
        agent_type, prompt, name=teammate_name,
        allowed_tools=_tool_list_arg(args), model=args.get("model"),
    )
except SubAgentGateError as exc:
    return ToolResult(False, _json(exc.to_dict()), {"gate": "subagent_creation", "gates": exc.failures})

definition = subagents.definitions[task.agent_type]

from ..teams.progress import TeammateProgress, random_verb
from ..teams.models import TeammateInfo
from ..teams.spawn_inprocess import spawn_inprocess_teammate

progress = TeammateProgress(name=teammate_name, team_name=team_name, spinner_verb=random_verb())
agent_id = task.id
member = TeammateInfo(
    name=teammate_name, agent_id=agent_id, agent_type=task.agent_type,
    model=task.model or (definition.model or ""), worktree_path="", is_active=True,
)
member.progress = progress
teams.register_member(team_name, member)

team_ctx = _dc_replace(ctx, team_name=team_name, agent_name=teammate_name)

def run_one_turn(turn_prompt: str, prog: Any) -> str:
    with subagents.slot():
        return subagents.run_agent_turn(definition, task, turn_prompt, progress=prog, run_ctx=team_ctx)

mailbox = teams.get_mailbox(team_name)
handle = spawn_inprocess_teammate(
    run_one_turn=run_one_turn,
    name=teammate_name,
    team_name=team_name,
    mailbox=mailbox,
    team_manager=teams,
    progress=progress,
    prompt=prompt,
    on_completed=lambda nm: teams.on_teammate_completed(agent_id),
)
teams.register_inprocess_handle(agent_id, handle)
return ToolResult(
    True,
    _json({
        "teammate": teammate_name,
        "agent_id": agent_id,
        "team": team_name,
        "agent_type": task.agent_type,
        "note": (
            "Long-running teammate started in-process. It runs a turn, reports to 'lead', "
            "then goes idle. Its messages arrive automatically as team-notifications; "
            "reply with send_message to wake it for the next task."
        ),
    }),
    {"teammate": teammate_name, "team": team_name},
)

def _agent_open(args: dictstr, Any, ctx: ToolContext) -> ToolResult:

if str(args.get("team_name") or "").strip():

return _spawn_teammate(args, ctx)

prompt = str(args.get("prompt") or args.get("message") or args.get("objective") or "")

if not prompt:

return ToolResult(False, "Missing prompt.")

Now register the team tools in register_builtins, after the agent_close registration:

● Update(lilbot\tools\builtin.py)

Added 44 lines

"name": _string("Session name."),

"agent_id": _string("Agent id."),

}), _agent_close))

── Teams: long-running teammates that coordinate via messages + a shared board ──

registry.register(ToolDef("team_create", "Create a team to coordinate multiple long-running teammates. After creating, spawn teammates with Agent(team_name=..., name=..., subagent_type=...). Use a team when work needs several agents collaborating (e.g. impl

ementer + reviewer) rather than one-shot subagents.", _schema({

"team_name": _string("Name for the new team."),

"name": _string("Alias for team_name."),

"description": _string("Optional team purpose."),

}, "team_name"), _team_create))

registry.register(ToolDef("team_delete", "Delete a team, stopping its teammates and cleaning up its mailbox and worktrees.", _schema({

"team_name": _string("Team to delete."),

"name": _string("Alias for team_name."),

}, "team_name"), _team_delete))

registry.register(ToolDef("team_list", "List teams and their members with live status.", _schema({}), _team_list))

registry.register(ToolDef("send_message", "Send a message to a teammate by name (or 'lead', or '' to broadcaseach turn; sending a message wakes them for the next task. Their replies arrive to you automatically as reminders.", _schema({
"to": _string("Recipient teammate name, 'lead', or '
' for all."),

"message": _string("Message body."),

"summary": _string("Short 5-10 word summary (recommended for text)."),

"message_type": _string("text | shutdown_request | shutdown_response."),

"team": _string("Team name (only needed if you have multiple teams)."),

}, "to", "message"), _send_message))

registry.register(ToolDef("team_task_create", "Create a task on the team's shared board (with optional assignee and blocks/blocked_by dependencies).", _schema({

"title": _string("Task title."),

"description": _string("Task detail."),

"assignee": _string("Teammate name to own it."),

"blocks": _schema_array("Task ids this blocks."),

"blocked_by": _schema_array("Task ids that block this."),

"team": _string("Team name (if multiple)."),

}, "title"), _team_task_create))

registry.register(ToolDef("team_task_list", "List tasks on the team's shared board, optionally filtered by status or assignee.", _schema({

"status": _string("Filter: pending|in_progress|completed|blocked."),

"assignee": _string("Filter by assignee name."),

"team": _string("Team name (if multiple)."),

}), _team_task_list))

registry.register(ToolDef("team_task_get", "Read one task from the team's shared board.", _schema({

"task_id": _string("Shared task id."),

"team": _string("Team name (if multiple)."),

}, "task_id"), _team_task_get))

registry.register(ToolDef("team_task_update", "Update a shared-board task: status, assignee, description, or add dependencies.", _schema({

"task_id": _string("Shared task id."),

"status": _string("pending|in_progress|completed|blocked."),

"assignee": _string("Reassign to teammate."),

"description": _string("New description."),

"add_blocks": _schema_array("Task ids this now blocks."),

"add_blocked_by": _schema_array("Task ids that now block this."),

"team": _string("Team name (if multiple)."),

}, "task_id"), _team_task_update))

registry.register(ToolDef("task_create", "Create a durable task record.", _schema({

"prompt": _string("Task prompt."),

"title": _string("Task title alias."),

现在 T6:在 core/agent.py 主循环开头注入团队通知回流。

● Update(lilbot\core\agent.py)

Added 1 line

self._maybe_compact()

steps = self._auto_delegate(user_text)

while steps < self.config.max_steps:

self._drain_team_notifications()

render_ctx = self.ctx.subagents.get_render_context() if getattr(self.ctx, "subagents", None) else None

turn = self.provider.complete(self.messages, self.registry.schemas(render_ctx))

self._add_usage(turn)

● Update(lilbot\core\agent.py)

Added 28 lines

yield TextDelta(final.content)

yield TurnFinished(steps, dict(self.usage))

复制代码
def _drain_team_notifications(self) -> None:
    """Inject teammate messages / idle reports addressed to the lead.

    Called at the top of every agent-loop iteration so the lead learns of
    teammate progress mid-turn without blocking or polling. Mirrors mewcode's
    drain_lead_mailbox -> system-reminder injection.
    """
    teams = getattr(self.ctx, "teams", None)
    if teams is None:
        return
    try:
        notes = teams.drain_lead_mailbox()
    except Exception:
        return
    for note in notes:
        self.messages.append({
            "role": "user",
            "content": (
                "Internal LilBot team notification (coordination signal, not a new user "
                "request). Use it to decide next steps; reply to teammates with send_message.\n"
                + note
            ),
        })

def drain_team_notifications(self) -> None:
    """Public hook for the UI to pull teammate updates between turns."""
    self._drain_team_notifications()

def _auto_delegate(self, user_text: str) -> int:
    """No-op: Dynamic Agent Tool Prompt Parity replaces keyword-based auto-delegation.

Now run the test suite, then an end-to-end team test with a mock provider (no network).

Ran 1 shell command

全绿。现在跑一个端到端测试(mock provider,无网络):建团队 → 派长驻队友 → 队友跑完一轮转 idle 并通知 lead → lead 用 send_message 唤醒 → 共享任务板 → 清理。

Ran 1 shell command

端到端全部跑通:建团队 → 长驻队友跑完转 idle 并通知 lead → send_message 唤醒后队友再跑一轮(provider 调用 2 次)→ 共享任务板息 → 清理。T1--T6 功能核心完成并验证。

现在 T8 slash 命令(低风险、立即可见)。在 cli.py 加 /team。

● Update(lilbot\cli.py)

Added 1 line

SlashCommandInfo("memory", "/memory list|search|save|delete", "Manage project memory."),

SlashCommandInfo("agents", "/agents", "List sub-agent types and tasks."),

SlashCommandInfo("agent", "/agent TYPE PROMPT", "Run a sub-agent task."),

SlashCommandInfo("team", "/team list|new NAME|msg NAME TEXT|rm NAME", "Inspect or drive agent teams.", ("teams",)),

SlashCommandInfo("mcp", "/mcp", "List MCP-style external servers."),

SlashCommandInfo("permissions", "/permissions ask|accept-all|deny-all", "Change permission mode."),

SlashCommandInfo("compact", "/compact", "Compact conversation context."),

● Update(lilbot\cli.py)

Added 3 lines

task = ctx.subagents.spawn(agent_type, prompt)

ui.print(f"{task.id} {task.status}\n{task.result or task.error}")

return True

if cmd == "team":

handle_team(args, agent, ctx, ui)

return True

if cmd == "mcp":

servers = ctx.mcp.list_servers()

if not servers:

● Update(lilbot\cli.py)

Added 55 lines

return True

def handle_team(args: str, agent: Agent, ctx: ToolContext, ui: LilBotUI) -> None:

teams = getattr(ctx, "teams", None)

if teams is None:

ui.error("Team system is unavailable in this runtime.")

return

action, _, tail = args.partition(" ")

action = (action or "list").strip().lower()

tail = tail.strip()

复制代码
if action == "list":
    rows: list[tuple[str, str, str, str]] = []
    for team in teams.list_teams():
        if not team.members:
            rows.append((team.name, "-", "-", "(no members)"))
        for m in team.members:
            prog = getattr(m, "progress", None)
            status = getattr(prog, "status", None) or ("idle" if m.is_active is False else "active")
            last = getattr(prog, "last_message", None) or ""
            rows.append((team.name, m.name, f"{m.agent_type}/{status}", " ".join(last.split())[:50]))
    if not rows:
        ui.print("No teams. Create one in chat (the agent calls team_create) or '/team new NAME'.")
        return
    ui.table("Teams", ["Team", "Member", "Type/Status", "Last"], rows)
    return
if action == "new":
    if not tail:
        ui.error("Usage: /team new NAME")
        return
    team = teams.create_team(tail, "lead", "")
    ui.print(f"Created team '{team.name}'.", "green")
    return
if action in {"msg", "send"}:
    name, _, text = tail.partition(" ")
    if not name or not text.strip():
        ui.error("Usage: /team msg NAME TEXT")
        return
    result, _ = ctx.subagents and (None, None) or (None, None)  # noqa: E501  (keep mypy quiet; unused)
    from .tools.builtin import _send_message  # local import to reuse the tool handler
    res = _send_message({"to": name, "message": text.strip(), "summary": text.strip()[:40]}, ctx)
    ui.print(res.output, "green" if res.ok else "red")
    agent.drain_team_notifications()
    return
if action in {"rm", "delete", "del"}:
    if not tail:
        ui.error("Usage: /team rm NAME")
        return
    try:
        teams.delete_team(tail)
        ui.print(f"Deleted team '{tail}'.", "green")
    except Exception as exc:  # noqa: BLE001
        ui.error(str(exc))
    return
ui.error("Usage: /team list|new NAME|msg NAME TEXT|rm NAME")

def handle_memory(args: str, ctx: ToolContext, ui: LilBotUI) -> None:

action, _, tail = args.partition(" ")

action = action or "list"

那行残留的无意义代码删掉。

● Update(lilbot\cli.py)

Added 1 line, removed 2 lines

if not name or not text.strip():

ui.error("Usage: /team msg NAME TEXT")

return

result, _ = ctx.subagents and (None, None) or (None, None) # noqa: E501 (keep mypy quiet; unused)

from .tools.builtin import _send_message # local import to reuse the tool handler

from .tools.builtin import _send_message # reuse the tool handler

res = _send_message({"to": name, "message": text.strip(), "summary": text.strip():40}, ctx)

ui.print(res.output, "green" if res.ok else "red")

agent.drain_team_notifications()

现在看 dashboard.py 结构,评估加队友面板的位置与风险。

Read 1 file, ran 2 shell commands

dashboard 已有 _work_text() 带"### Subagents"区。我加一个"### Teammates"区(只读渲染,低风险,随现有刷新更新)。

● Update(lilbot\tui\dashboard.py)

Added 3 lines

"### Subagents",

*self._subagent_work_lines(),

"",

"### Teammates",

*self._teammate_work_lines(),

return "status: unavailable"

try:

all_teams = teams.list_teams()

except Exception as exc: # noqa: BLE001

return f"status: unavailable ({type(exc).**name** })"

if not all_teams:

return "no teams"

rows: liststr = \[\]

for team in all_teams:

rows.append(f"team {team.name}: {len(team.members)} member(s)")

for m in team.members:

prog = getattr(m, "progress", None)

status = getattr(prog, "status", None) or ("idle" if m.is_active is False else "active")

tokens = getattr(prog, "token_count", 0) if prog else 0

tools = getattr(prog, "tool_use_count", 0) if prog else 0

activity = prog.activity_summary if prog else ""

tok = prog.format_tokens(tokens) if prog else str(tokens)

rows.append(f"- {m.name} {status} {activity} tools={tools} tok={tok}")

return rows

复制代码
def _toolbar(self):
    return FormattedText(
        [

Now run the full suite again and verify slash/dashboard imports + /team list work without a TTY.

Ran 1 shell command

全部通过。T1--T8 完成。现在补一个正式的 pytest 测试文件,把团队系统纳入回归保护。

● Write(tests\test_teams.py)

Wrote 171 lines to tests\test_teams.py

""Tests for the teams / teammates layer."""

rom future import annotations

mport threading

mport time

rom pathlib import Path

mport pytest

... +161 lines

Ran 1 shell command

registry.execute 返回 (ToolResult, elapsed_ms),metadata 在 result.metadata 上。修正测试。

● Update(tests\test_teams.py)

Added 2 lines, removed 2 lines

assert r.ok

r, _ = registry.execute("team_task_update", {"task_id": "1", "status": "in_progress"}, ctx)

assert r.ok

r, meta = registry.execute("team_task_list", {"status": "in_progress"}, ctx)

assert r.ok and meta"count" == 1

r, _ = registry.execute("team_task_list", {"status": "in_progress"}, ctx)

assert r.ok and r.metadata"count" == 1

def test_one_shot_subagents_have_no_team_tools(tmp_path: Path) -> None:

Ran 1 shell command

全部完成。116 测试通过(原 109 + 新增 7 个团队测试),0 回归。

✅ Teams / Teammates 已落地(T1--T8 全部完成)

你现在就能用起来

用你的 DeepSeek key 启动 lilbot,然后:

自然语言(核心体验):

建个团队,让一个 implementer 改 foo.py 的 bug,完成后让 reviewer 复查它的改动

lead 会 team_create → 用 Agent(team_name=..., name=..., subagent_type=...) 派长驻队友 → 队友跑完一轮转 idle 并把结果发回 lead → 下一轮 lead 自动收到 (不阻塞)→ 据此派 reviewer / 综合回复你。

slash 命令(直接观测):

  • /team list ------ 团队、成员、谁 active/idle、最近动作
  • /team msg impl 继续改下一个函数 ------ 给队友发消息并唤醒它
  • /team new NAME / /team rm NAME

Dashboard(F5 Work 面板)多了 ### Teammates 区,实时显示每个队友的状态/动作/工具数/token。

新增能力一览(对标 mewcode,补齐了你之前缺的)

┌──────────────────────────────────┬──────────────────────────────────────────┐

│ 能力 │ 实现 │

├──────────────────────────────────┼──────────────────────────────────────────┤

│ 长驻队友(跑完不退、idle 待命) │ teams/spawn_inprocess.py(线程版) │

├──────────────────────────────────┼──────────────────────────────────────────┤

│ 代理间通信(点对点+广播+文件锁) │ teams/mailbox.py + send_message 工具 │

├──────────────────────────────────┼──────────────────────────────────────────┤

│ 共享任务板(assignee + 依赖) │ teams/shared_task.py + team_task_* 工具 │

├──────────────────────────────────┼──────────────────────────────────────────┤

│ 按名字寻址 │ teams/registry.py,mailbox 以队友名为键 │

├──────────────────────────────────┼──────────────────────────────────────────┤

│ 异步结果回流(lead 不轮询) │ core/agent.py._drain_team_notifications │

├──────────────────────────────────┼──────────────────────────────────────────┤

│ 团队容器(持久化) │ teams/manager.py → .lilbot/teams// │

├──────────────────────────────────┼──────────────────────────────────────────┤

│ 队友进度可视化 │ teams/progress.py + dashboard 面板 │

└──────────────────────────────────┴──────────────────────────────────────────┘

关键设计决策(与 SPEC 的一处务实调整)

  • 没用 mewcode 的"每队友建私有 registry",改成 lilbot 风格的全局工具 + ctx 身份:协调工具读 ctx.team_name/agent_name 知道"我是谁"。更贴合你的无状态 handler 架构,也让 lead 自动获得这些工具。
  • 复用了你的优势:队友每轮调 SubAgentManager.run_agent_turn(T3 抽取),自动继承你的 gates / 工具过滤 / transcript / 持久化------没有重写一套。
  • 回归保护:test_one_shot_subagents_have_no_team_tools 确认旧的一次性 subagent 不会沾上团队工具。

我没做的(按之前确认,留作后续)

  • tmux/iTerm2 可视化后端(Windows 不适用)
  • coordinator mode(把 lead 收窄成纯调度)
  • 队友 worktree 隔离默认关闭(多写队友共用 workspace 有风险;要的话我可以接上你现成的 _prepare_worktree,按 git 仓库降级)

文件清单:新增 lilbot/teams/(7 文件)、tests/test_teams.py、SPEC_TEAMS_AND_TEAMMATES.md;改动 subagents/manager.py、tools/builtin.py、tools/registry.py、core/agent.py、cli.py、tui/dashboard.py。

要不要我现在:(a) 用你的真实 DeepSeek key 跑一次真·端到端(implementer+reviewer)验证联网下的表现;(b) 给队友接上 worktree 隔离;还是 © 在 README 里补一段 Teams 使用说明?

✻ Sautéed for 13m 35s

❯ a b c 都需要

三个都安排。顺序:先 (b) worktree 隔离(改代码),再 © README,最后 (a) 真·联网端到端(用最终版代码、在 git 仓库里跑)。

(b) 给队友接上 worktree 隔离(opt-in,非 git 自动降级)

先在 SubAgentManager 加一个温和的可用性探测。

● Update(lilbot\subagents\manager.py)

Added 18 lines

"""Context manager bounding active teammate turns by max_concurrent."""

return self._semaphore

复制代码
def worktree_available(self) -> bool:
    """True when the workspace can host git worktrees (for teammate isolation)."""
    if self.ctx is None:
        return False
    sandbox = getattr(self.ctx, "sandbox", None)
    if sandbox is None or not shutil.which("git"):
        return False
    try:
        if not sandbox.run("git rev-parse --show-toplevel", 10).ok:
            return False
        return sandbox.run("git worktree list --porcelain", 10).ok
    except Exception:
        return False

def ctx_for_task(self, task: SubAgentTask) -> Any:
    """Public accessor for a task's worktree-scoped tool context."""
    return self._ctx_for_task(task)

def eval(
    self,
    task_ref: str,

Now wire isolation into _spawn_teammate in builtin.py:

● Update(lilbot\tools\builtin.py)

Added 23 lines, removed 2 lines

复制代码
definition = subagents.definitions[task.agent_type]

# Optional git-worktree isolation: each teammate edits in its own working tree.
# Degrades to the shared workspace when git/worktrees are unavailable.
use_wt = False
wt_note = ""
if str(args.get("isolation") or "").strip().lower() in ("worktree", "1", "true", "yes", "on"):
    if subagents.worktree_available():
        task.worktree_isolation = "worktree"
        task.worktree_status = "requested"
        if subagents._prepare_worktree(task):
            use_wt = True
        else:
            task.status = "running"
            task.error = ""
            wt_note = "worktree prep failed; running in shared workspace"
    else:
        wt_note = "worktree unavailable (no git repo); running in shared workspace"

from ..teams.progress import TeammateProgress, random_verb
from ..teams.models import TeammateInfo
from ..teams.spawn_inprocess import spawn_inprocess_teammate

agent_id = task.id
member = TeammateInfo(
    name=teammate_name, agent_id=agent_id, agent_type=task.agent_type,
    model=task.model or (definition.model or ""), worktree_path="", is_active=True,
    model=task.model or (definition.model or ""),
    worktree_path=task.worktree_path if use_wt else "", is_active=True,
)
member.progress = progress
teams.register_member(team_name, member)

team_ctx = _dc_replace(ctx, team_name=team_name, agent_name=teammate_name)
if use_wt:
    team_ctx = _dc_replace(subagents.ctx_for_task(task), team_name=team_name, agent_name=teammate_name)
else:
    team_ctx = _dc_replace(ctx, team_name=team_name, agent_name=teammate_name)

def run_one_turn(turn_prompt: str, prog: Any) -> str:
    with subagents.slot():

Now add wt_note and worktree path to the spawn result message:

● Update(lilbot\tools\builtin.py)

Added 2 lines

"agent_id": agent_id,

"team": team_name,

"agent_type": task.agent_type,

"worktree": task.worktree_path if use_wt else None,

"isolation_note": wt_note or None,

"note": (

"Long-running teammate started in-process. It runs a turn, reports to 'lead', "

"then goes idle. Its messages arrive automatically as team-notifications; "

Now test (b): worktree isolation in a real git repo, plus the graceful degradation path in a non-git temp dir.

Ran 1 shell command

worktree 创建/降级都对了,但删除后没清掉------bug 在 _cleanup_worktree:它运行 git worktree remove 时没指定 cwd,于是跑在了错误的仓库目录。真实使用时 lilbot 进程 cwd 就是 workspace 所以恰好能成,但我应让它健壮。修正为在 workspace 根目录执行。

● Update(lilbot\teams\manager.py)

Added 4 lines, removed 1 line

复制代码
def _cleanup_worktree(self, worktree_path: str) -> None:
    import subprocess
    # Run inside the workspace repo (state_dir is <workspace>/.lilbot), not the
    # caller's cwd, so `git worktree remove` targets the right repository.
    workspace = self.state_dir.parent
    try:
        subprocess.run(
            ["git", "worktree", "remove", worktree_path, "--force"],
            capture_output=True, timeout=15,
            capture_output=True, timeout=15, cwd=str(workspace),
        )
    except Exception as e:  # noqa: BLE001
        log.warning("git worktree remove failed for %s: %s", worktree_path, e)

Ran 1 shell command

(b) 完成。现在 © README。看一下 README 结构找插入点。

Read 1 file, ran 1 shell command

在 Subagents 与 MCP Dock 之间插入 Teams 章节。

● Update(README.md)

Added 80 lines


Teams / Teammates

Subagents are one-shot: spawn → run → collect. Teams add long-running

teammates that stay alive across turns, talk to each other, and share a task

board --- modeled on multi-agent "swarm" coordination but built on LilBot's own

threaded subagent runtime (gates, transcripts, and persistence are all reused).

State lives per-project under .lilbot/teams/<slug>/:

  • config.json --- team + members
  • tasks.json --- shared task board (assignee + blocks / blocked_by)
  • mailbox/<name>.json --- per-agent inbox (file-locked, concurrency-safe)

How a team runs

#mermaid-svg-gbdpnVvZlH0eu2Ik{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gbdpnVvZlH0eu2Ik .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gbdpnVvZlH0eu2Ik .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gbdpnVvZlH0eu2Ik .error-icon{fill:#552222;}#mermaid-svg-gbdpnVvZlH0eu2Ik .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gbdpnVvZlH0eu2Ik .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gbdpnVvZlH0eu2Ik .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gbdpnVvZlH0eu2Ik .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gbdpnVvZlH0eu2Ik .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gbdpnVvZlH0eu2Ik .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gbdpnVvZlH0eu2Ik .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gbdpnVvZlH0eu2Ik .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gbdpnVvZlH0eu2Ik .marker.cross{stroke:#333333;}#mermaid-svg-gbdpnVvZlH0eu2Ik svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gbdpnVvZlH0eu2Ik p{margin:0;}#mermaid-svg-gbdpnVvZlH0eu2Ik .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gbdpnVvZlH0eu2Ik .cluster-label text{fill:#333;}#mermaid-svg-gbdpnVvZlH0eu2Ik .cluster-label span{color:#333;}#mermaid-svg-gbdpnVvZlH0eu2Ik .cluster-label span p{background-color:transparent;}#mermaid-svg-gbdpnVvZlH0eu2Ik .label text,#mermaid-svg-gbdpnVvZlH0eu2Ik span{fill:#333;color:#333;}#mermaid-svg-gbdpnVvZlH0eu2Ik .node rect,#mermaid-svg-gbdpnVvZlH0eu2Ik .node circle,#mermaid-svg-gbdpnVvZlH0eu2Ik .node ellipse,#mermaid-svg-gbdpnVvZlH0eu2Ik .node polygon,#mermaid-svg-gbdpnVvZlH0eu2Ik .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gbdpnVvZlH0eu2Ik .rough-node .label text,#mermaid-svg-gbdpnVvZlH0eu2Ik .node .label text,#mermaid-svg-gbdpnVvZlH0eu2Ik .image-shape .label,#mermaid-svg-gbdpnVvZlH0eu2Ik .icon-shape .label{text-anchor:middle;}#mermaid-svg-gbdpnVvZlH0eu2Ik .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gbdpnVvZlH0eu2Ik .rough-node .label,#mermaid-svg-gbdpnVvZlH0eu2Ik .node .label,#mermaid-svg-gbdpnVvZlH0eu2Ik .image-shape .label,#mermaid-svg-gbdpnVvZlH0eu2Ik .icon-shape .label{text-align:center;}#mermaid-svg-gbdpnVvZlH0eu2Ik .node.clickable{cursor:pointer;}#mermaid-svg-gbdpnVvZlH0eu2Ik .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gbdpnVvZlH0eu2Ik .arrowheadPath{fill:#333333;}#mermaid-svg-gbdpnVvZlH0eu2Ik .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gbdpnVvZlH0eu2Ik .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gbdpnVvZlH0eu2Ik .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gbdpnVvZlH0eu2Ik .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gbdpnVvZlH0eu2Ik .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gbdpnVvZlH0eu2Ik .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gbdpnVvZlH0eu2Ik .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gbdpnVvZlH0eu2Ik .cluster text{fill:#333;}#mermaid-svg-gbdpnVvZlH0eu2Ik .cluster span{color:#333;}#mermaid-svg-gbdpnVvZlH0eu2Ik div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gbdpnVvZlH0eu2Ik .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gbdpnVvZlH0eu2Ik rect.text{fill:none;stroke-width:0;}#mermaid-svg-gbdpnVvZlH0eu2Ik .icon-shape,#mermaid-svg-gbdpnVvZlH0eu2Ik .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gbdpnVvZlH0eu2Ik .icon-shape p,#mermaid-svg-gbdpnVvZlH0eu2Ik .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gbdpnVvZlH0eu2Ik .icon-shape .label rect,#mermaid-svg-gbdpnVvZlH0eu2Ik .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gbdpnVvZlH0eu2Ik .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gbdpnVvZlH0eu2Ik .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gbdpnVvZlH0eu2Ik :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-gbdpnVvZlH0eu2Ik .input>*{fill:#020617!important;stroke:#00e5ff!important;color:#ffffff!important;}#mermaid-svg-gbdpnVvZlH0eu2Ik .input span{fill:#020617!important;stroke:#00e5ff!important;color:#ffffff!important;}#mermaid-svg-gbdpnVvZlH0eu2Ik .input tspan{fill:#ffffff!important;}#mermaid-svg-gbdpnVvZlH0eu2Ik .default>*{fill:#111827!important;stroke:#64748b!important;color:#ffffff!important;}#mermaid-svg-gbdpnVvZlH0eu2Ik .default span{fill:#111827!important;stroke:#64748b!important;color:#ffffff!important;}#mermaid-svg-gbdpnVvZlH0eu2Ik .default tspan{fill:#ffffff!important;} team_create
Agent(team_name, name, subagent_type)
Agent(team_name, name, subagent_type)
send_message to=lead + idle
send_message to=lead + idle
drained at next loop turn
send_message wakes a teammate
Lead (you)
Team: bugfix
impl (implementer)

long-running thread
rev (review)

long-running thread
mailbox/
<team-notification> injected

A teammate runs one full agent turn (same tool loop + gates as a subagent),

reports to lead, then goes idle and polls its mailbox. The lead never

blocks: teammate messages are drained at the top of each agent-loop turn and

injected as <team-notification> coordination signals.

Tools (the model uses these autonomously)

Tool Purpose
team_create / team_delete / team_list manage teams
Agent(team_name=, name=, subagent_type=) spawn a long-running teammate (vs. one-shot when team_name is omitted)
send_message message a teammate by name, lead, or * (broadcast); wakes idle teammates
team_task_create / team_task_list / team_task_get / team_task_update shared task board with dependencie

Every teammate automatically gets the coordination tools (send_message,

team_task_*) on top of its role's tools, scoped to its identity via the tool

context --- so it knows who it is and which team it belongs to.

Slash commands

复制代码
/team list                 # teams, members, live status, last activity
/team new NAME             # create a team locally
/team msg NAME TEXT        # send a message (wakes the teammate)
/team rm NAME              # delete a team

The Flight Deck's Work pane (F5) shows a live Teammates panel:

name [status] activity tools=N tok=....

Worktree isolation (opt-in)

Pass isolation: "worktree" when spawning a teammate to give it its own git

worktree under .lilbot/worktrees/, so concurrent teammates don't edit the same

files. If the workspace is not a git repo, it degrades gracefully to the shared

workspace. Worktrees are removed on team_delete.

Example

"Build a team: have an implementer fix the null check in auth.py, then a

reviewer verify the change."

The lead creates a team, spawns impl and (after impl reports back) rev,

collects both results via auto-injected notifications, and answers you --- without

ever polling.


MCP Dock

渲染错误: Mermaid 渲染失败: No diagram type detected matching given configuration for text: (c) 完成。现在 (a) 真·联网端到端。先确认 DeepSeek 连通性(最小调用),再跑真实队友。这会产生真实网络调用(你已授权 a)。 Ran 1 shell command 连通正常(DeepSeek v4-flash 回了 PONG)。现在跑真实队友端到端:临时 git 仓库 + 一个 explore 队友读文件并按契约报告 → 等它转 idle 通知 lead → drain → 再 send_message 唤醒跑第二轮。 Ran 1 shell command 关键问题:临时工作区没有 .env,所以 load_config(d) 回退成了 mock 规则模型(lilbot-rule-model),没真正用上 DeepSeek。不过即便如此,队友循环全程跑通了(跑turn→经 gates 调工具→报告 lead→idle→drain)。 修正:先 load_config(cwd) 把仓库 .env 注入 os.environ,再用临时工作区。这次用 implementer + worktree 隔离真去改 bug。 Ran 1 shell command 真用上 DeepSeek 了(provider=deepseek、6.5k tokens、6 次真实工具调用)。发现一个真实问题:队友单轮工具预算 SUBAGENT_MAX_TOOL_STEPS=6 对一次性探针够用,但真实 implementer 队友"先探索再改"会在编辑前就耗尽预算。这是长驻队友应该有更高预算的设计点。 修正:给队友单轮更高预算。加常量并在队友循环传入。 ● Update(lilbot\subagents\manager.py) Added 3 lines ProviderCallable = Callable\[list\[dict, listdict], ProviderTurn] SUBAGENT_MAX_TOOL_STEPS = 6 # Long-running teammates explore *and* act within a single turn, so they need a # larger per-turn tool budget than one-shot subagent probes. TEAMMATE_MAX_TOOL_STEPS = 16 DEFAULT_SUBAGENT_MAX_CONCURRENT = 8 # Coordination tools injected into every teammate so it can talk to the team and ● Update(lilbot\tools\builtin.py) Added 6 lines, removed 1 line else: team_ctx = _dc_replace(ctx, team_name=team_name, agent_name=teammate_name) from ..subagents.manager import TEAMMATE_MAX_TOOL_STEPS def run_one_turn(turn_prompt: str, prog: Any) -> str: with subagents.slot(): return subagents.run_agent_turn(definition, task, turn_prompt, progress=prog, run_ctx=team_ctx) return subagents.run_agent_turn( definition, task, turn_prompt, progress=prog, run_ctx=team_ctx, max_steps=TEAMMATE_MAX_TOOL_STEPS, ) mailbox = teams.get_mailbox(team_name) handle = spawn_inprocess_teammate( Re-run the real DeepSeek implementer test with the higher budget to prove a real fix lands: Ran 1 shell command ❯ 帮我把这个F:\Experiment_laborotory\collection-claude-code-source-code-main\LilBot-agent-code\mewcode-python目录整个加到gitignore 里面,然后git fetch ,git pull, git commuit, git push完整走一遍流程,跟我的github repo更新好 先看一下 git 仓库状态、远程、分支和现有 .gitignore。 Ran 1 shell command 分支在 main,远程是 terrense/LilBot-agent。mewcode-python/ 当前未被跟踪,加进 .gitignore 即可排除。先更新 .gitignore,同时 fetch 看与远程的差异。 ● Update(.gitignore) File must be read first Read 1 file ● Update(.gitignore) Added 1 line *.log /leetcode-master /mewcode-python/ Ran 1 shell command 0 0 ------ 本地 main 与 origin/main 在同一提交,无分叉,git pull 已是最新(无需合并)。mewcode-python 已被忽略,不再出现在 status 里。 提交前先跑一遍测试,确认含 worktree/预算改动后仍全绿。 Ran 1 shell command 全绿。暂存改动并确认 mewcode-python/ 不在其中。 Ran 1 shell command 暂存后 status 为空有点反常,确认一下实际暂存内容。 Ran 1 shell command "working tree clean" 但之前明明有改动------很反常。检查最近提交和 reflog,确认改动是否被某个操作提交或回退了。 Ran 2 shell commands 一切清楚了。状态如下: - 提交 ee05c21 "Add Teams & long-running teammates"(作者 shenxin,即你)已经创建并推送到 GitHub。 - 本地 HEAD、远程跟踪 origin/main、以及 git ls-remote 拿到的真实远程 tip 三者完全一致 = ee05c21。 也就是说在你打断那次测试之后,这些改动已经被提交并推送了。我确认一下 .gitignore 里确实含 mewcode-python、且工作区干净,避免重复提交。 Ran 1 shell command 全部确认完毕,整个流程已经走通,无需再做任何提交。 最终状态 ┌────────────────────────────────┬────────────────────────────────────────────────────────────────────┐ │ 检查项 │ 结果 │ ├────────────────────────────────┼────────────────────────────────────────────────────────────────────┤ │ .gitignore 含 /mewcode-python/ │ ✅ 已提交进 ee05c21 │ ├────────────────────────────────┼────────────────────────────────────────────────────────────────────┤ │ mewcode-python/ 是否被跟踪 │ ✅ 未跟踪(已忽略,不会上传) │ ├────────────────────────────────┼────────────────────────────────────────────────────────────────────┤ │ git fetch │ ✅ 已执行 │ ├────────────────────────────────┼────────────────────────────────────────────────────────────────────┤ │ git pull │ ✅ Already up to date(无分叉,0 0) │ ├────────────────────────────────┼────────────────────────────────────────────────────────────────────┤ │ git commit │ ✅ ee05c21 Add Teams & long-running teammates(18 文件,+2235 行) │ ├────────────────────────────────┼────────────────────────────────────────────────────────────────────┤ │ git push │ ✅ git ls-remote 确认远程 refs/heads/main = ee05c21 │ ├────────────────────────────────┼────────────────────────────────────────────────────────────────────┤ │ 本地 vs 远程 │ ✅ ## main...origin/main 完全同步,工作区干净 │ └────────────────────────────────┴────────────────────────────────────────────────────────────────────┘ 提交内容涵盖了这次全部 Teams/Teammates 工作:lilbot/teams/(8 文件)、tests/test_teams.py、README.md(Teams 章节)、SPEC_TEAMS_AND_TEAMMATES.md,以及 subagents/manager.py、tools/builtin.py、tools/registry.py、core/agent.py、cli.py、tui/dashboard.py 的改动。mewcode-python(参考项目)和 .lilbot/ 都被正确排除在外。 你的 GitHub 仓库 terrense/LilBot-agent 的 main 分支现在已是最新。 --- ⚠️ 一点说明:被你打断的那条命令是**(a) 真实 DeepSeek 端到端测试**(验证提高工具预算后 implementer 队友能真正改完 bug)。那次测试本身不影响代码------但我刚才为修复"队友单轮预算只有 6 步、改 bug 前就耗尽"而做的两处改动(TEAMMATE_MAX_TOOL_STEPS=16 常量 + 在队友循环传入),看起来也已经包含在 ee05c21 这个提交里了(builtin.py / manager.py 都在变更清单中)。 需要的话我可以重新把那个真实端到端测试跑完,确认提高预算后队友能真正落地修复,再决定要不要追加提交。要继续吗? ✻ Brewed for 2m 46s ❯ 需要,你跑一遍。我当时以为你是卡住了才打断了,对不起! 没关系!完全理解。那我把真实 DeepSeek 端到端跑完------验证提高工具预算后,implementer 队友能真正改完 bug。 Ran 1 shell command 预算改动已在代码里(TEAMMATE_MAX_TOOL_STEPS=16,队友循环已传入)。现在跑真实端到端。 Ran 1 shell command 两个观察: 1. 出现瞬时网络错误(SSL: UNEXPECTED_EOF_WHILE_READING,DeepSeek 连接中途断开)。好消息是------队友的失败处理路径正确工作了:捕获异常 → 标记 failed → 给 lead 发 failed 通知。 2. 我的校验有 bug:'a + b' in fixed 误匹配了注释 # BUG: should be a + b,所以 "PASS" 是假阳性,实际代码行还是 a - b。 SSL 断连是瞬时的。重跑一次,并修正校验(检查真实 return 行,去掉注释干扰)。 Ran 1 shell command 跑起来了(耗时较长转入后台)。等它完成,我读取输出。 Ran 1 shell command ● Background command "Retry real DeepSeek implementer, fixed assertion" completed (exit code 0) The background test finished. Let me read the result. Read 1 file 又是网络问题------这次是首个请求就 ConnectTimeout: handshake timed out(tools=0, tok=0)。DeepSeek 当前连接不稳定(之前 PONG 和首次 implementer 跑出 6 次工具调用都成功过,说明是瞬时抖动)。队友失败处理再次正确工作(捕获→failed通知 lead→progress=failed)。 这是环境网络问题,不是代码问题。再重试一次(后台跑,避免阻塞)。 Ran 1 shell command 在后台跑(attempt 3)。完成后会通知我。等待结果。 Invalid tool parameters 后台任务完成时系统会通知我。先这样等待结果------稍候它跑完我再读输出汇报。 ✻ Churned for 3m 48s ● Background command "Retry real DeepSeek implementer (attempt 3)" completed (exit code 0) Attempt 3 完成,读结果。 Read 1 file 进展!这次网络通了(无 SSL 错误,10 次工具调用,7.2k tokens,跑完整轮 status=idle)。但队友报告:"write operations are denied by the permission system" ------ edit_file 被权限系统拦了。 这是一个真实的设计发现:队友继承了 lead 的权限模式。默认 permission_mode="ask" 且非交互时,写操作被自动拒绝,所以队友改不了文件。mewcode 的队友是用 BYPASS 模式跑的(尤其在隔离 worktree 里)。 先用 accept-all 验证"放开写权限后,真实队友能完整改完 bug",证明执行链路本身没问题。 Ran 1 shell command 后台运行中(accept-all 权限)。完成后通知我,我再读结果汇报。 ✻ Worked for 37s ✻ Worked for 37s ● Background command "Real DeepSeek implementer with accept-all permissions" completed (exit code 0) Read 1 file 🎉 真·端到端通过! permission_mode= accept-all --- lead notification --- from=dev: idle dev: All fixed. ... Changed the return statement to a + b. ... --- calc.py after --- def add(a, b): return a + b ← 真实 DeepSeek 队友改好了 progress: status= idle tools= 8 tok= 6.8k return line = 'a + b' | RESULT: PASS 真实的 DeepSeek implementer 队友:在隔离 worktree 里 read_file → edit_file 把 a - b 改成 a + b(8 次真实工具调用,6.8k tokens)→ 用契约格式向 lead 报告 "All fixed" → 转 idle → lead 自动 drain 收到通知。整条链路在真实 LLM 下完整跑通。 本次验证的收获 跑真实端到端暴露了 3 件事(都很有价值): 1. ✅ 核心链路成立:真实 LLM 队友能在隔离 worktree 里完成实际代码修改并按契约回报,lead 异步收到------这正是 mewcode 团队能力的 2. ✅ 失败处理路径正确:前两次遇到 DeepSeek 瞬时网络抖动(SSL EOF / 握手超时),队友都正确捕获→标记 failed→给 lead 发 failed 通知,没有静默挂死。 3. ⚠️ 一个真实设计点:队友继承 lead 的权限模式。默认 permission_mode="ask" + 非交互时,队友的写操作被自动拒绝(第 3 次就是这样")。 待你定夺的设计点 队友在隔离的 git worktree 里干活(改动不碰主工作区),mewcode 的做法是让队友以 BYPASS 权限运行。你这边有两个选择: - A(像 mewcode):隔离 worktree 里的队友自动放开写权限------因为它本就被沙箱隔离,改动要经 lead 审阅/合并才进主线。体验顺滑。 - B(保守):保持继承 lead 权限,要写就需要你把 permission_mode 设成 accept-all,或在交互模式下逐个批准。更安全但每次要手动放行。 要不要我实现 A(给隔离 worktree 的队友默认 BYPASS,无隔离时仍继承 lead 权限)?这是让队友"开箱即用能干活"的最后一块拼图。改完我可以再追加一个提交推上去。 ✻ Sautéed for 24s ※ recap: Goal: add mewcode-style teams/teammates to your LilBot agent. All built, tested, committed, and pushed; real DeepSeek end-to-end passed. Next: decide whether teammates in isolated worktrees should auto-bypass write permissions (option A) so I can implement and push it. (disable recaps in /config) ❯ 好的 实现 A:隔离 worktree 的队友自动放开写权限,无隔离时仍继承 lead 权限。先看 PermissionManager 的接口。 Read 1 file PermissionManager 很简单------给隔离队友换一个 accept-all 的实例即可,而写操作本身已被 ctx_for_task 的 Sandbox(worktree_root) 限制在该 worktree 内,所以放开权限是安全的。改 _spawn_teammate 的 use_wt 分支。 ● Update(lilbot\tools\builtin.py) Added 9 lines, removed 1 line teams.register_member(team_name, member) if use_wt: team_ctx = _dc_replace(subagents.ctx_for_task(task), team_name=team_name, agent_name=teammate_name) # Isolated teammates auto-accept writes: their PathSandbox confines all # file ops to their own worktree, so changes can't touch the main tree # and are reviewed/merged by the lead before they matter. from ..sandbox.permissions import PermissionManager bypass = PermissionManager(ctx.config.state_dir, mode="accept-all", interactive=False) team_ctx = _dc_replace( subagents.ctx_for_task(task), permissions=bypass, team_name=team_name, agent_name=teammate_name, ) else: team_ctx = _dc_replace(ctx, team_name=team_name, agent_name=teammate_name) Now add a test for this behavior and update the README worktree paragraph. ● Update(tests\test_teams.py) Added 66 lines assert r.ok and r.metadata"count" == 1 def _git_repo(path: Path) -> None: import subprocess for c in ("git", "init", "-q", "git", "config", "user.email", "t@t", "git", "config", "user.name", "t"): subprocess.run(c, cwd=path, capture_output=True) (path / "seed.txt").write_text("x") subprocess.run("git", "add", "-A", cwd=path, capture_output=True) subprocess.run("git", "commit", "-qm", "init", cwd=path, capture_output=True) def test_isolated_teammate_gets_bypass_permissions(tmp_path: Path) -> None: """A worktree-isolated teammate should auto-accept writes (sandboxed to its tree).""" import shutil if not shutil.which("git"): pytest.skip("git not available") _git_repo(tmp_path) captured: dictstr, object = {} def capture_turn(definition, task, prompt, *, progress=None, run_ctx=None, max_steps=None): # noqa: ANN001 captured"mode" = run_ctx.permissions.mode captured"sandbox_root" = str(run_ctx.sandbox.root) captured"team" = run_ctx.team_name captured"agent" = run_ctx.agent_name return "SUMMARY: ok\nCHANGES: None.\nEVIDENCE: ok\nRISKS: None.\nBLOCKERS: None." _, registry, ctx, _ = _runtime(tmp_path) ctx.subagents.run_agent_turn = capture_turn # type: ignoreassignment registry.execute("team_create", {"team_name": "demo"}, ctx) r, _ = registry.execute( "agent_open", {"team_name": "demo", "name": "dev", "subagent_type": "implementer", "isolation": "worktree", "prompt": "fix it"}, ctx, ) assert r.ok assert _wait(lambda: "mode" in captured) assert captured"mode" == "accept-all" assert captured"team" == "demo" and captured"agent" == "dev" # sandbox is scoped to the teammate's own worktree, not the main workspace assert ".lilbot" in captured"sandbox_root" and "worktrees" in captured"sandbox_root" def test_non_isolated_teammate_inherits_lead_permissions(tmp_path: Path) -> None: captured: dictstr, object = {} def capture_turn(definition, task, prompt, *, progress=None, run_ctx=None, max_steps=None): # noqa: ANN001 captured"mode" = run_ctx.permissions.mode return "SUMMARY: ok\nCHANGES: None.\nEVIDENCE: ok\nRISKS: None.\nBLOCKERS: None." _, registry, ctx, _ = _runtime(tmp_path) ctx.permissions.mode = "deny-all" ctx.subagents.run_agent_turn = capture_turn # type: ignoreassignment registry.execute("team_create", {"team_name": "demo"}, ctx) r, _ = registry.execute( "agent_open", {"team_name": "demo", "name": "dev", "subagent_type": "implementer", "prompt": "x"}, ctx, ) assert r.ok assert _wait(lambda: "mode" in captured) assert captured"mode" == "deny-all" # inherited, not bypassed def test_one_shot_subagents_have_no_team_tools(tmp_path: Path) -> None: """Regression: existing one-shot subagents must not gain team coordination tools.""" _, _, ctx, _ = _runtime(tmp_path) ● Update(README.md) Added 6 lines files. If the workspace is not a git repo, it degrades gracefully to the shared workspace. Worktrees are removed on `team_delete`. An **isolated teammate auto-accepts writes** (`accept-all`) because its PathSandbox confines every file operation to its own worktree --- changes can't reach the main tree until the lead reviews/merges them. A **non-isolated** teammate instead inherits the lead's permission mode, so in the default `ask` mode it cannot write unless you approve (or run with `accept-all`). ### Example > "Build a team: have an implementer fix the null check in `auth.py`, then a Run the full suite to confirm the new behavior + no regressions: Ran 1 shell command 真实发现一个 Windows 专属的健壮性 bug:满suite跑过(118 passed),但单独高并发跑 mailbox 测试时偶发 PermissionError: Errno 13 文件时,os.open(O_EXCL) 可能抛 PermissionError(删除/重建瞬间被占),而它是 OSError 子类,当前代码走到 except OSError: break直接重抛,导致写线程崩溃丢消息。mewcode 原版没考虑 Windows 这点。 修正:把 PermissionError 当作瞬时争用重试,并让 finally 的 unlink 也容错。 ● Update(lilbot\teams\mailbox.py) Error editing file 字符没匹配上,重读确切内容。 Read 1 file ● Update(lilbot\teams\mailbox.py) Added 11 lines, removed 2 lines lock_file = self._lock_path(agent_id) lock_fd = None last_err: Exception | None = None for _ in range(10): for _ in range(40): try: fd = os.open(str(lock_file), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644) lock_fd = fd pass sleep_ms = 5 + random.randint(0, 95) time.sleep(sleep_ms / 1000) except PermissionError as e: # Windows raises this (not FileExistsError) when another thread is # mid-create/unlink of the lock file --- transient, so retry. last_err = e sleep_ms = 5 + random.randint(0, 95) time.sleep(sleep_ms / 1000) except OSError as e: last_err = e break messages = fn(messages) self._write_inbox(agent_id, messages) finally: lock_file.unlink(missing_ok=True) try: lock_file.unlink(missing_ok=True) except OSError: pass # Windows: another thread may already be reclaiming it def _read_inbox(self, agent_id: str) -> listMailboxMessage: path = self._inbox_path(agent_id) 重点重跑那个高并发 mailbox 测试多次,确认 Windows 下不再偶发崩溃。 Ran 1 shell command 5/5 通过。再跑整个 test_teams.py 两遍 + 完整 suite,确认稳定无回归。 Ran 1 shell command 稳定全绿(test_teams 9/9 两遍,完整 suite 118 passed)。现在做最终的真实 DeepSeek 验证------这次不设 accept-all,用默认 ask 模式,证明隔离队友自动放开写权限后能真正改完 bug(后台跑,避免阻塞)。 Ran 1 shell command 后台运行中(默认 ask 模式 + worktree 自动放行)。完成后通知我,我读结果并决定追加提交。等待。 ✻ Baked for 3m 57s ● Background command "Real DeepSeek verify: ask mode + worktree bypass" completed (exit code 0) Read 1 file 🎉 决定性验证通过! lead permission_mode = ask (default) ← 没有放开全局权限 --- lead notification --- from=dev: idle dev: SUMMARY: Fixed the add(a,b) function ... Verified by calling add(2,3)... --- calc.py after --- def add(a, b): return a + b ← 真实队友在 ask 模式下自主改好了 progress: status= idle tools= 8 tok= 6.7k return line = 'a + b' | RESULT: PASS 在 默认 ask 模式(没全局放权)下,隔离 worktree 的 DeepSeek 队友自动获得写权限,read_file → edit_file 改好 a + b,还自己验证了 add(2,3),按契约回报 lead。这正是选项 A 的目标:开箱即用能干活。 现在追加提交并推送。先看改了哪些文件。 Ran 1 shell command 无分叉。改动 4 个文件。提交并推送(延续你 main 分支的工作流)。 Ran 1 shell command 又是"nothing to commit"------和上次一样,改动在我 git add 前就已被提交/推送了(你的并行操作)。确认实际状态。 Ran 1 shell command 全部确认完毕。 ✅ 选项 A 已实现、验证并推送 新提交 aba5aa1 "Improve mailbox locking and teammate permissions" 已落地,本地 HEAD、origin/main 跟踪、git ls-remote 真实远程 tip 三者一致 = aba5aa1,工作区干净。HEAD 中确认包含 mailbox 修复和 worktree 放权代码。 这次做了什么(3 项,全部经测试) ┌───────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────┬──────────────────────────────────────────────────┐ │ 改动 │ 说明 │ 验证 │ ├───────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤ │ 隔离队友自动放权 │ worktree 队友 accept-all(写操作被 PathSandbox 锁在自己的 worktree 内);非隔离队友仍继承 lead 权限 │ 真实 DeepSeek:默认 ask 模式下队友自主改好 a+b ✅ │ ├───────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤ │ 队友单轮工具预算 6→16 │ TEAMMATE_MAX_TOOL_STEPS=16,让真实 implementer 能"探索+编辑"在一轮内完成 │ 8 次工具调用完成修复 ✅ │ ├───────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────┤ │ Windows 邮箱锁健壮性 │ 把瞬时 PermissionError 当争用重试(原版只认 FileExistsError,Windows 下会崩写线程丢消息);finally unlink 容错 │ 高并发测试 5/5 + 9/9 稳定 ✅ │ └───────────────────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────┘ 新增 2 个测试(隔离 vs 继承权限),README 补充权限说明。全套 118 passed。 验证旅程回顾(真实 LLM 暴露的问题都已修) 1. 工具预算太低 → 已调高 ✅ 2. 两次 DeepSeek 瞬时网络抖动 → 队友失败处理路径正确(failed 通知 lead) ✅ 3. 默认 ask 模式队友写被拒 → 隔离队友自动放权 ✅ 4. Windows 锁文件并发崩溃 → 重试修复 ✅ 至此,你的 LilBot 拥有了完整的 Teams / 长驻队友能力,且经真实 DeepSeek 端到端验证可用,已同步到 GitHub terrense/LilBot-agent。 ▎ 小提示:我注意到这两次 git commit/push 都是在我执行前由你那边并行完成的------完全没问题,结果都对。如果以后想让我来跑提交,记得别同时在终端操作同一仓库,避免我们俩 git add 撞车。 ✻ Cogitated for 1m 12s ❯ 给我现在讲一下这个team和普通subagent的区别,结合源码,我要能达到讲解给别人甚至应对大厂面试或者当个主播给人开课讲授的那种水平 下面这份讲义我按"能开课、能应对大厂面试"的标准来写------每个论点都钉在你 LilBot 仓库的真实源码上(文件名+函数名),并附上面试追问的应答。 --- LilBot Teams vs Subagent 源码级讲义 0. 一句话抓住本质 ▎ Subagent 是"一次性外包工":派出去 → 干完 → 销毁,你得主动打电话问结果(拉)。 ▎ Teammate 是"入职的正式组员":长期在岗 → 干完转待命 → 主动汇报,结果自己飘回来(推),组里还有共享看板和花名册。 技术上,两者的差异可以拆成三根正交的轴。把这三根轴讲清楚,你就讲到了架构师的层次: ┌────────────┬──────────────────────────────────┬──────────────────────────────────────────┐ │ 轴 │ Subagent │ Teammate │ ├────────────┼──────────────────────────────────┼──────────────────────────────────────────┤ │ ① 生命周期 │ one-shot(run→die) │ long-lived(run→idle→wake→...) │ ├────────────┼──────────────────────────────────┼──────────────────────────────────────────┤ │ ② 结果流向 │ PULL(父代理轮询) │ PUSH(邮箱 + 自动回流进主循环) │ ├────────────┼──────────────────────────────────┼──────────────────────────────────────────┤ │ ③ 拓扑结构 │ 星型/孤岛(只对父汇报,彼此不可见) │ 图(队友互发消息 + 共享任务板 + 按名寻址) │ └────────────┴──────────────────────────────────┴──────────────────────────────────────────┘ 下面逐轴上源码。 --- 1. 轴①:生命周期 ------ "干完就死" vs "干完待命" Subagent:一锤子买卖 入口在 tools/builtin.py:_agent_open → ctx.subagents.open()(subagents/manager.py): open() → 跑创建门禁 _validate_creation_gates → 建 SubAgentTask → threading.Thread(target=self._run) 启动后台线程 _run() 的骨架(subagents/manager.py)是线性的、有终点的: self._semaphore.acquire() # 占一个并发槽 task.status = "running" self._prepare_worktree(task) # 可选 worktree task.result = self.run_agent_turn(definition, task, task.prompt) # 只跑【一轮】 task.status = "completed" # 到达终点 self._cleanup_worktree(task) self._semaphore.release() # 线程结束 → 销毁 注意 SubAgentTask.terminal 属性:status in {completed, failed, cancelled, ...}。一旦 terminal,这个 agent 就死了,线程退出。 Teammate:长驻循环 入口是同一个 _agent_open,但带了 team_name → 分流到 _spawn_teammate(tools/builtin.py)。它先用 build_teammate_task 建任务(注意 status="running" 但不启线程),再调 spawn_inprocess_teammate(teams/spawn_inprocess.py)。 关键区别在那个 _loop()------它是个 while 循环,没有自然终点: def _loop(): next_prompt = prompt while not stop.is_set(): # ← 长驻! result = run_one_turn(turn_prompt, progress) # 跑一轮(复用 run_agent_turn) progress.status = "idle" team_manager.notify_lead(...) # 主动汇报 lead team_manager.set_member_idle(...) new_prompt, shutdown = _wait_for_next(stop) # ← 阻塞轮询邮箱等下一个任务 if shutdown or stop.is_set(): return # 只有收到 shutdown / cancel 才退出 next_prompt = new_prompt _wait_for_next 每 IDLE_POLL_INTERVAL=0.5s 调一次 mailbox.consume(name),直到有消息或 SHUTDOWN_PREFIX="shutdown"。 面试官追问:既然都跑一轮,凭什么说生命周期不同? ▎ 答:看的是"一轮之后"。Subagent 一轮后线程 return、状态 terminal、被销毁;Teammate 一轮后进 idle 并继续占着线程在 while 里轮询邮箱,可以被 send_message 唤醒跑第 N 轮。所以 Teammate 是"有状态的常驻服务",Subagent 是"无状态的函数调用"。这也解释了为什么我把队友单轮预算调高(TEAMMATE_MAX_TOOL_STEPS=16 vs SUBAGENT_MAX_TOOL_STEPS=6)------常驻组员一轮要能"探索+落地",探针只需快速取证。 这里有个工程亮点值得讲:两者复用同一个执行核心 SubAgentManager.run_agent_turn。我专门把它从 _run 里抽出来(本次重构 T3),这样队友每轮自动继承 subagent 的 gates、工具过滤、transcript------单一事实源,安全策略不会两套实现漂移。面试时这叫"DRY + 安全一致性"。 --- 2. 轴②:结果流向 ------ PULL vs PUSH(这是最值钱的一段) Subagent 是 PULL:父代理得"打电话问" 父代理拿结果靠 agent_eval(builtin.py → SubAgentManager.eval): def eval(self, task_ref, *, block=True, timeout=30): ... deadline = time.time() + timeout while time.time() < deadline: if task.terminal: break time.sleep(0.05) # ← 父代理在这儿【阻塞自旋】等结果 return task 代价:父代理卡住,在等待期间什么也干不了。串行感很重。 Teammate 是 PUSH:结果自己飘回主循环 这是整个 team 系统的灵魂机制,分三跳: 第 1 跳 --- 队友写信:_loop 里 team_manager.notify_lead(...) → 往 mailbox 里 lead 的收件箱写一条消息(teams/manager.py:notify_lead)。队友也可在干活中途用 send_message 工具主动发。 第 2 跳 --- 收件箱攒着:TeamManager.drain_lead_mailbox() 遍历所有 team,mailbox.consume("lead") 取出未读,拼成: from=impl: idle impl: 改完了 ... 第 3 跳 --- 主循环每轮自动吸收:看 core/agent.py 的 run_turn------我在 while 循环最顶端插了一行: while steps < self.config.max_steps: self._drain_team_notifications() # ← 每一轮都先把队友消息吸进来 turn = self.provider.complete(self.messages, ...) _drain_team_notifications 把每条通知作为 user 角色消息追加进 self.messages,并标注"这是协调信号,不是新的用户请求"。 于是效果是:lead 在自己的工具循环里干着活,下一轮 LLM 调用前就自动"读到"了"impl 改完了",据此决定下一步(比如派 reviewer)。全程不阻塞、不轮询。 面试官追问 1:为什么注入成 user 消息而不是 system? ▎ 答:三点。① 它得能被模型当成"对话里发生的新事件"来推理,user 角色最自然;② 用 XML 标签做带内信令,模型能一眼区分"这是队友信号,不是人类新指令"(我在注入文案里也明说了);③ 跟现有 _internal_observation_message 的约定一致,复用心智模型。 面试官追问 2:lead 在等用户输入、没跑循环时,队友的消息怎么办? ▎ 答:这是真实的边界情况。有两层兜底:① 下次 run_turn 一开始就 drain,绝不丢;② Dashboard 的 _teammate_work_lines 刷新时也能看到队友状态。消息存在文件邮箱里不会丢,只是"被看到"的时机延后到下一轮------这正是选择文件邮箱(持久)而非内存队列的好处之一。 --- 3. 轴③:拓扑 ------ 孤岛 vs 协作图 Subagent 是孤岛 + 扁平 - 子代理之间无法通信,只能各自把结果回给父代理(星型,且边是"拉")。 - 不能再开子代理:看 SUBAGENT_ALWAYS_DISALLOWED_TOOLS 里含 AGENT_TOOLS(Agent/Task/agent_open...),_tool_schemas_for_task 会把它们过滤掉。所以层级是扁平的,防止递归爆炸。 Teammate 是协作图,靠三个共享设施 全部落在 /.lilbot/teams//: (a) 邮箱 mailbox/(teams/mailbox.py) ------ 点对点 + 广播通信。 核心是 _with_lock:用 os.open(..., O_CREAT|O_EXCL) 抢一个 .lock 文件做互斥,10s 过期自动回收(防死锁),失败重试。这是个经典的文件锁实现。 ▎ 这里有个我实战踩到的坑值得讲:Windows 下多线程争锁,os.open 可能抛 PermissionError(不是 FileExistsError),原版 mewcode 没处理,会让写线程崩溃丢消息。我把它当瞬时争用重试修掉了(commit aba5aa1)。面试里这是"移植开源代码要注意跨平台语义差异"的好例子。 (b) 共享任务板 tasks.json(teams/shared_task.py) ------ SharedTask 带 assignee、blocks/blocked_by 依赖,状态 pending/in_progress/completed/blocked。每次操作前 _load() 重读磁盘,保证多线程看到一致视图。这让多个队友能认领任务、声明依赖,实现真正分工。 (c) 花名册 AgentNameRegistry(teams/registry.py) ------ 进程内单例,name → agent_id。让你能 send_message(to="reviewer") 按名字喊人,而不是记一串 id。配合广播 to="*"。 面试官追问:邮箱为什么用文件 + 文件锁,不用 queue.Queue? ▎ 答:① 持久性------进程崩了消息还在;② 可扩展到跨进程(将来 tmux/iterm2 后端每个队友是独立进程,内存队列就跨不了);③ 与项目状态目录 .lilbot/ 统一,便于审计和清理。代价是要自己实现锁,且有 Windows 兼容性细节(见上)。这是"用一点复杂度换持久性和可扩展性"的典型取舍。 --- 4. 一张图讲清两条调用链(白板必画) ┌─────────────── 同一个入口 _agent_open ───────────────┐ │ │ 无 team_name 有 team_name │ │ SubAgentManager.open() _spawn_teammate() ├ 创建门禁 gates ├ build_teammate_task(角色工具 + 协调工具) ├ Thread → _run() ├ 身份注入: team_ctx = replace(ctx, team_name, agent_name) │ ├ semaphore.acquire ├ 隔离则: 换 accept-all 权限 + worktree sandbox │ ├ run_agent_turn() ←──── 同一执行核心 ────→ run_one_turn(run_agent_turn, slot, 16步) │ ├ status=completed (终点) └ spawn_inprocess_teammate → _loop(while 长驻) │ └ 线程结束/销毁 ├ 跑一轮 → idle → notify_lead(写邮箱) │ └ _wait_for_next(轮询邮箱/0.5s/shutdown) 父代理 agent_eval(block) 轮询拉结果 lead run_turn 每轮 _drain_team_notifications 推回 --- 5. 身份与权限:Teammate 多出来的两件"装备" 这点很能体现你对系统的理解深度。Subagent 用 _ctx_for_task 跑(共享或 worktree 沙箱)。Teammate 在此之上还做了身份注入: # tools/builtin.py:spawn_teammate team_ctx = replace(ctx, team_name=team_name, agent_name=teammate_name) 为什么要这个?因为我用了 lilbot 风格的"全局无状态工具 + ctx 读身份"(而非 mewcode 的"给每个队友建私有 registry")。send_message/team_task* 这些工具从 ctx.team_name / ctx.agent_name 知道"我是谁、属于哪个队",lead 则两者为 None。这是个有意思的架构选择题。 面试官追问:为什么不照搬 mewcode 给每个队友建独立 registry? ▎ 答:lilbot 的工具本就是"无状态 handler 读 ToolContext"的设计,顺着它走,把身份放进 ctx 克隆即可,代码更少、还让 lead 自动获得这些工具。这是"尊重宿主架构,而非硬搬源项目"的判断------移植不是复制粘贴。 权限的安全边界(选项 A,commit aba5aa1):隔离队友的 team_ctx 会把 permissions 换成 accept-all,但仅当它在自己的 worktree 里------因为 PathSandbox(worktree_root) 已经把所有写操作锁死在那棵独立工作树内,改动碰不到主线,得经 lead 审阅合并才生效。沙箱隔离 = 放权的安全前提。非隔离队友则继承 lead 权限(默认 ask 就写不了)。 ▎ 真实验证:默认 ask 模式下,隔离的 implementer 队友自主完成了 a-b → a+b 的修复并自测 add(2,3)。这就是"沙箱内放权"既安全又好用的证明。 --- 6. 并发模型:为什么是线程 + 信号量 - lilbot 的 provider 是同步 httpx 调用,所以队友用 threading.Thread(daemon)而非 asyncio------和 mewcode 的 asyncio 版相比,这是顺应宿主的改写。 - 并发上限:run_one_turn 用 with subagents.slot()(即 BoundedSemaphore)包住活跃的那一轮,而不是整个 idle 期------队友待命时不占槽,避免长驻把并发池占满。这个细节是"长驻 + 限流"如何共存的答案。 面试官追问:多个队友线程并发调同一个 provider,线程安全吗? ▎ 答:沿用现有 subagent 的并发约束 + 信号量限流;provider 每次 complete 是独立请求、无共享可变状态。真正的共享态(邮箱、任务板) 致性。 --- 7. 持久化与清理 - Subagent:subagent-tasks.json + transcripts + 重启恢复(_resume_recovered_tasks)------这是 lilbot 比 mewcode 强的地方(mewcode 后台任务重启即丢),迁移时我特意保留。 - Teammate:团队三件套(config/tasks/mailbox)持久在 .lilbot/teams//;team_delete 时 handle.cancel()(置 stop event 停 whilp_worktree 在 workspace 根跑 git worktree remove)+ 清邮箱目录。 --- 8. 面试速答卡(背下来) 1. 一句话区别? Subagent 是无状态一次性函数调用(拉结果);Teammate 是有状态长驻服务(推结果 + 同伴协作)。 2. 何时用谁? 独立的一次性取证/实现/审查 → subagent 并行;需要多角色长期协作、互相依赖、边干边汇报(implementer+reviewer 往返)→ team。 3. 核心创新点? 结果从"父代理阻塞轮询"变成"邮箱 + 主循环每轮自动 drain 注入",lead 不阻塞。 4. 怎么保证安全不失控? 子代理禁止再开子代理(扁平);队友放权只在 worktree 沙箱内;所有创建走 gates;工具按角色过滤。 5. 跨平台坑? Windows 文件锁的 PermissionError 要当瞬时争用重试。 6. 如何复用而不重复? 队友与子代理共用 run_agent_turn,gates/工具/transcript 单一事实源。 --- 9. 怎么讲给小白(开课用的类比) ▎ 把 LLM agent 想成一个"会用工具的实习生"。 ▎ - Subagent = 你临时叫个跑腿的:"去查下 X",他查完把纸条递给你就走了。你要同时叫好几个跑腿,但他们互不认识,你还得守在那儿等纸条。 ▎ - Team = 你组了个项目组:招了"开发"和"测试"两名正式组员,他们有组内群聊(邮箱)、一块共享看板(任务板)、一份花名册(按名字喊人)。开发改完代码,主动在群里@你说"好了",这条消息会自动出现在你下一次开口前;你再让测试去验。整个过程你该忙啥忙啥,进度自己飘过来。 --- 要不要我接着给你做两样开课/面试的"实操弹药": 1. 一份带高亮注释的源码走读路线(按"入口→分流→执行核心→回流"顺序,列出每个文件该看的具体函数),你可以照着屏幕逐行讲; 2. 一个现场可跑的 30 秒 demo 脚本(建队→派 implementer+reviewer→看消息飘回),直播时一边跑一边讲。 要哪个,或者两个都要? ✻ Brewed for 2m 35s ❯ 两个都要 两样都安排。先快速核对几个关键函数的行号,保证走读路线精确到行。 Ran 1 shell command 行号已核准。下面是两份开课弹药。 --- 弹药 ①:源码走读路线(照着屏幕逐行讲) 按 入口 → 分流 → 执行核心 → 回流 → 协作设施 五站走。每站标了 文件:行 和"讲什么"。 🚉 第 0 站:开场------同一个入口,两条命运 lilbot/tools/builtin.py:3162 _agent_open def _agent_open(args, ctx): if str(args.get("team_name") or "").strip(): return _spawn_teammate(args, ctx) # ← 3164:有 team_name 走队友 ... # ← 没有就走一次性 subagent 讲点:"同一个工具入口,只看一个参数 team_name,就分成完全不同的两种 agent。这是我有意的设计------对模型暴露统一心智,对运行时分流两套生命周期。" 🚉 第 1 站:Subagent 命运线(一次性) 1. subagents/manager.py:491 open() ------ 跑创建门禁,建任务,起线程。 2. subagents/manager.py:725 _run() ------ 线性骨架:semaphore.acquire → _prepare_worktree → run_agent_turn(只一次) → status=completed → release。 3. subagents/manager.py:612 eval() ------ PULL 的证据:while not terminal: time.sleep(0.05) 父代理在这阻塞自旋拿结果。 讲点:"看 _run 是直线、有终点;看 eval 是父代理卡着轮询。这就是'无状态函数调用 + 拉结果'。" 🚉 第 2 站:执行核心(两者共用------重构亮点) subagents/manager.py:771 run_agent_turn(...),第 788 行: limit = SUBAGENT_MAX_TOOL_STEPS if max_steps is None else max(1, int(max_steps)) 配合 960 _tool_schemas_for_task(按角色 + gates 过滤工具)。 讲点:"子代理和队友跑的是同一个核心,gates/工具过滤/transcript 单一事实源。队友只是传了更大的 max_steps=16(21/24 行两个常量),因为常驻组员一轮要'探索+落地',探针只取证。" 🚉 第 3 站:Teammate 命运线(长驻) 1. builtin.py:3044 _spawn_teammate ------ 看三件事: - build_teammate_task(manager.py:544):工具 = 角色工具 + 协调工具,且 status="running" 但不起线程。 - 身份注入:team_ctx = replace(ctx, team_name=, agent_name=),隔离时再换 accept-all 权限 + worktree 沙箱。 - slot()(manager.py:590):with subagents.slot() 只在活跃轮占信号量,idle 不占。 2. teams/spawn_inprocess.py:90 _loop() ------ while 长驻:跑一轮 → idle → notify_lead → _wait_for_next。 3. teams/spawn_inprocess.py:71 _wait_for_next ------ 第 73 行 time.sleep(IDLE_POLL_INTERVAL=0.5),轮询 consume,遇 SHUTDOWN_PREFIX(24)退出。 讲点:"_loop 没有自然终点,这就是'有状态常驻服务'。slot() 那行是'长驻+限流'如何共存的答案。" 🚉 第 4 站:回流三跳(全系统的灵魂) 1. 写信:teams/manager.py:216 notify_lead → 写 lead 收件箱。 2. 攒信/取信:teams/manager.py:188 drain_lead_mailbox → consume("lead") → 拼 。 3. 吸收:core/agent.py:42 ------ 主循环最顶端: while steps < self.config.max_steps: self._drain_team_notifications() # ← 每轮先把队友消息吸进对话 看 core/agent.py:77 _drain_team_notifications,把通知作为 user 消息注入。 讲点(高潮):"对比第 1 站的 eval 自旋------这里 lead 不阻塞:它干着自己的活,下一轮 LLM 调用前自动'听到'队友汇报。PULL 变 PUSH,就在这 42 行一行。" 🚉 第 5 站:三件协作设施 - 邮箱锁 teams/mailbox.py:56 _with_lock(O_EXCL 文件锁 + 10s 过期);讲 Windows PermissionError 重试坑。 - 共享看板 teams/shared_task.py SharedTask(blocks/blocked_by 依赖)。 - 花名册 teams/registry.py AgentNameRegistry(按名喊人 + to="*" 广播,见 mailbox.py:139 broadcast)。 ▎ 讲完顺手对比"扁平 vs 图":子代理因 SUBAGENT_ALWAYS_DISALLOWED_TOOLS 含 AGENT_TOOLS,在 _tool_schemas_for_task 被过滤,不能再开 agent;队友则拿到协调工具能互相喊话。 --- 弹药 ②:30 秒现场 demo 脚本 我写成两段式:默认 stub 模式(确定性、秒级、直播零翻车,讲机制用)+ --real 真 LLM 模式(联网真改 bug)。我把它落到 experiment/teams_demo.py。 ● Write(experiment\teams_demo.py) Wrote 165 lines to experiment\teams_demo.py ""LilBot Teams --- 30-second live demo. hows the whole team mechanism step by step: team_create -> spawn teammates -> teammate runs a turn -> reports to lead -> lead drains the mailbox (async PUSH) -> wake a teammate -> shared board. sage: python experiment/teams_demo.py # deterministic stub provider (no network, great for live talks) python experiment/teams_demo.py --real # real DeepSeek provider (needs the repo .env key + a git workspace) ... +155 lines 先用 stub 模式跑一遍,确保直播时零翻车。 Ran 1 shell command Windows GBK 控制台不支持 ▶/✓ 这些字符。换成纯 ASCII。 ● Update(experiment\teams_demo.py) Added 1 line, removed 1 line def banner(step: str, text: str) -> None: print(f"\n\03396m▶ {step}\\033\[0m {text}") print(f"\\n\\033\[96m\>\> {step}\\033\[0m {text}") def wait_until(predicate, timeout: float) -\> bool: ● Update(experiment\\teams_demo.py) Added 1 line, removed 1 line print(" " + (Path(wt) / "calc.py").read_text().strip().replace("\\n", "\\n ")) ctx.teams.delete_team("shipit") print("\\n\\033\[92m✓ done --- team deleted, worktrees cleaned\\033\[0m") print("\\n\\033\[92m\[OK done - team deleted, worktrees cleaned\0330m") return 0 顺手让脚本对任意控制台编码都健壮(stdout 强制 utf-8、出错替换),避免直播时别的字符再炸。 ● Update(experiment\\teams_demo.py) Added 4 lines def main() -\> int: try: # robust against GBK/legacy consoles on Windows sys.stdout.reconfigure(encoding="utf-8", errors="replace") except Exception: pass real = "--real" in sys.argv ws = Path(tempfile.mkdtemp(prefix="lilbot-team-demo-")) Ran 1 shell command Stub 模式 6 步全跑通(约 2 秒,确定性)。管道里看到的 \[96m 是 ANSI 颜色码------真实终端里会显示成颜色。为防止录屏/管道时露出转义码,加个 TTY 守卫:非终端就不上色。 ● Update(experiment\\teams_demo.py) Added 5 lines, removed 1 line from lilbot.teams import AgentNameRegistry def _c(code: str, text: str) -\> str: return f"\\033\[{code}m{text}\\033\[0m" if sys.stdout.isatty() else text def banner(step: str, text: str) -\> None: print(f"\\n\\033\[96m\>\> {step}\\033\[0m {text}") print("\\n" + _c("96", f"\>\> {step}") + f" {text}") def wait_until(predicate, timeout: float) -\> bool: ● Update(experiment\\teams_demo.py) Added 1 line, removed 1 line print(" " + (Path(wt) / "calc.py").read_text().strip().replace("\\n", "\\n ")) ctx.teams.delete_team("shipit") print("\\n\\033\[92m\[OK done - team deleted, worktrees cleaned\0330m") print("\\n" + _c("92", "\[OK done - team deleted, worktrees cleaned")) return 0 Ran 1 shell command 两份弹药都就绪、都验证过。 交付清单 ① 源码走读路线(上面正文,5 站精确到 文件:行): 入口分流(builtin.py:3162)→ Subagent 命运线(open/_run/eval)→ 共用执行核心(run_agent_turn:771)→ Teammate 命运线(_spawn_teammat 跳(notify_lead:216 → drain_lead_mailbox:188 → agent.py:42)→ 三件协作设施。每站都标了"讲什么",高潮落在 core/agent.py:42那一行。 ② 现场 demo 脚本:experiment/teams_demo.py,已跑通。 ┌────────────────┬────────────────────────────────────────┬─────────────────────────────────────────┐ │ 模式 │ 命令 │ 特点 │ ├────────────────┼────────────────────────────────────────┼─────────────────────────────────────────┤ │ Stub(直播首选) │ python experiment/teams_demo.py │ 确定性、~2 秒、零网络翻车,6 步全亮 │ ├────────────────┼────────────────────────────────────────┼─────────────────────────────────────────┤ │ 真 LLM │ python experiment/teams_demo.py --real │ 联网真改 a-b→a+b,展示 worktree 隔离实效 │ └────────────────┴────────────────────────────────────────┴─────────────────────────────────────────┘ 脚本里 6 步正好对应你要讲的 6 个概念:team_create → 派两个长驻队友 → async PUSH(lead drain 到 impl/rev 的汇报)→ send_message 按名唤醒 rev 再跑一轮 → 共享看板带 blocked_by 依赖 → 花名册+进度。已做 Windows GBK 编码兼容 + TTY 颜色守卫(录屏不露转义码)。 直播讲法建议(把两份弹药串起来) 1. 先跑 demo(stub 模式),让观众看到"现象":消息自己飘回来、按名喊人、看板依赖。30 秒建立直觉。 2. 再走源码,回答"凭什么":在第 3 步现象处,翻到 core/agent.py:42 那一行------"PUSH 就发生在这里";在第 4 步唤醒处,翻到 spawn_inproc"它没死,在轮询邮箱"。 3. 最后抛对比表 + 三根轴(生命周期/结果流向/拓扑),收口到面试速答卡。 ▎ 提醒:experiment/teams_demo.py 是新增文件,目前未提交。要的话我可以帮你提交推上去(或者你也可以像之前那样自己 commit);它不属于核心库,放 experiment/ 下不影响主代码。需要我提交吗?