终端 AI Agent 之间如何可靠通信:原生 PTY 与 API 封装对比
Claude Code、Codex 和 Pi 是为坐在键盘前的人设计的,而不是为了接收其他 Agent 的消息。本文讲清楚 Agent 互相通信时会出什么问题、三种解决思路,以及如何保证每条消息既不丢失、也不会被投递两次。
- 交互式 Agent CLI 没有接收外部消息的接口。想把消息送进去,要么用 API 封装把它替换掉,要么往它的终端里输入。
- API 封装会丢掉官方 CLI 的工具、配置和订阅;直接用
tmux send-keys虽然保住了 CLI,却会丢消息、输错窗口、冲掉你写到一半的草稿。 - 可靠投递需要六项保证:先落盘、认准目标、同一时间只有一个写入者、绝不覆盖人的输入、遵守每个 CLI 自己的提交规则、如实回执。
- Team Agent 用追加式收件箱文件加原生 PTY 输入实现了这六项保证,消息以普通提示词的形式到达 Agent。
01CLI Agent 之间通信为什么这么难
Web 服务有 API,交互式编程 Agent 有的是终端用户界面:一个输入框、一块不断重绘的屏幕,外加一堆快捷键。这对你来说很顺手,对另一个程序来说却很别扭。凡是尝试过让两个 Agent 协作的人,都会遇到这些问题:
- 没有入站通道。官方并没有提供从外部向运行中的 Claude Code 或 Codex 会话推送消息的方式。
- 每个 CLI 的提交方式都不一样。有的会特殊处理粘贴文本,有的需要在回车前稍等片刻,多行输入在每个 CLI 里的表现都不同,而且这些规则还会随版本变化。
- Agent 经常在忙。任务进行中收到的消息必须等待,但不能丢。
- 人和 Agent 共用一个窗口。如果你正写到一半,另一个 Agent 的消息落进了同一个输入框,两边的内容都会被毁掉。
- 终端地址会被复用。tmux 重启后,像
%3这样的窗格编号可能已经属于一个完全不同的程序。 - 没有确认回执。按下回车之后,CLI 不会告诉另一个程序“已收到”。你只知道自己输入了什么。
02Agent 间通信的三种思路
1. API 封装
绕过 CLI,直接调用模型 API,自己写一套 Agent 循环。你得到了干净的编程接口,却也丢掉了 CLI 最有价值的几乎所有东西:内置工具、项目配置、权限模型、斜杠命令、会话历史,以及你已经付费的订阅登录。从此按 token 计费,而且这个 Agent 的表现也和你每天在用的那个不一样了。
2. 直接往终端里敲
把每个 CLI 放进 tmux,用 tmux send-keys 往里输入。真正的 CLI 保住了,演示时一行命令也确实能跑通:
tmux send-keys -t %3 "请审查最新的一次提交" Enter
可一到日常使用就漏洞百出:多行文本只提交了一半;粘贴还没处理完,回车就先到了;Agent 正忙,文字就堆在输入框里;你正在打字,草稿就成了一团乱码;%3 被复用了,消息就发给了错误的程序。而一旦出错,没有任何地方记得这条消息存在过。
3. 带保证的原生 PTY 投递
保留真正的 CLI 和它的终端,但在“打字”外面包一层投递协议,补上存储、身份校验和安全规则。PTY(伪终端)是每个交互式程序读取按键的虚拟终端设备,向它写入内容就完全等同于打字。Team Agent 采用的就是这种方式。
| API 封装 | 直接 send-keys | 原生 PTY + 收件箱 | |
|---|---|---|---|
| 使用官方 CLI 及其工具 | 否 | 是 | 是 |
| 沿用你的订阅登录 | 否,按 API 计费 | 是 | 是 |
| Agent 在忙或不在时消息不丢 | 取决于你的代码 | 否 | 是,先落盘 |
| 保护你写到一半的草稿 | 不适用 | 否 | 是 |
| 输入前校验目标 | 不适用 | 否 | 是 |
| 避免重复投递 | 视情况而定 | 否 | 是,结果不明时绝不重复输入 |
| 可读的消息历史 | 视情况而定 | 否 | 普通的追加式文件 |
03Team Agent 如何投递一条消息
无论是 Agent 调用 send_message 工具,还是你执行 team-agent send backend "…",投递都按固定顺序进行:
- 先落盘。在输入任何内容之前,消息会作为一条完整记录追加到收件人的收件箱文件中。从这一刻起,无论后面发生什么,消息都不会丢。记录只会追加,从不修改。
- 认准目标终端。Team Agent 会确认目标窗格仍然属于这个 Agent,而不只是“存在一个同号的窗格”。被复用的地址会被拒绝,而不是硬往里输入。
- 同一时间只有一个写入者。对同一个目标的输入是串行的,两个发送方的按键永远不会交错成一段乱码。
- 绝不覆盖人的输入。如果输入框里已经有内容,比如你正在打字,Team Agent 会在有限时间内(大约一分钟)等它清空。等不到就不输入,消息安然留在收件箱里。
- 遵守每个 CLI 的“方言”。先粘贴文本,再按该 CLI 需要的规则提交。例如 Claude Code 和 Cursor 在回车前停顿 100 毫秒,Grok 停顿 150 毫秒,Pi 和 Codex 不需要停顿。只发送一次回车,不做盲目重试。
- 如实回执。发送方会得知实际发生了什么:已提交、已延后、目标不存在,或者结果未知。只要结果不明,消息就绝不会被输入第二次,因为重复的指令比迟到的指令更糟糕。
输入并按下回车,只能证明消息到达了 Agent 的输入框,并不能证明 Agent 已经做完了工作。所以“回复”是单独的、明确的一步。
04回复是怎么回来的
每名队员都会获得三个小工具,通过 MCP(Claude Code、Codex 和 Pi 都支持的模型上下文协议)提供:
send_message:直接给其他队员或组长发消息。report_result:提交已完成的任务,附带状态、测试和风险说明。get_team_status:查看团队成员及各自的状态。
结果会先写入组长的收件箱,再按同样的安全投递步骤显示到组长的终端里。如果组长正在忙,结果就在收件箱里等着,不会凭空消失。除此之外,Agent 不会被添加任何东西,它们自己的工具和配置保持原样。
05在命令行中使用
# 给队员派任务(先落盘,再输入到它的终端)
team-agent send backend "给 GET /v1/invoices 加上分页,并补上测试"
# 只存入收件箱,不输入到对话中
team-agent send backend "有空时看一下这个" --mailbox
# 查看组长收到的最新回复
team-agent inbox leader -n 3
# 查看每名队员的供应商、活动状态和健康状况
team-agent status
收件箱就是普通的追加式文件,消息历史很容易审计和备份。不需要打开、加锁或迁移任何数据库。
06Agent 间消息系统的设计清单
想自己实现?下面这些特性最重要,按“缺了它最常引发真实事故”的顺序排列:
- 先持久化,再投递。在产生任何副作用之前,先把消息存下来。
- 屏幕上最多出现一次。无法确认结果的尝试,绝不重新输入。
- 认身份,不认位置。校验终端地址背后的进程,而不只是地址本身。
- 按目标加锁。对每个终端的输入串行化;绝不为了给一个 Agent 发消息而锁住所有 Agent。
- 保护草稿。确认输入框为空;人的草稿永远优先。
- 每个 CLI 一套提交规则。把粘贴和回车的行为作为每个 CLI 的配置数据,而不是靠猜。
- 如实回执。区分“已提交”“已延后”“已不在”和“未知”,不要一律归为“已投递”。
- 可读的存储。使用开发者能用普通工具直接查看的格式,出问题时一眼就能看清。
07常见问题
多个 Claude Code Agent 之间怎样互相通信?
Claude Code 这类交互式 CLI 本身没有接收其他 Agent 消息的机制。Team Agent 这样的协调工具会给每个 Agent 提供发消息和汇报结果的小工具,先把每条消息存进收件人的收件箱,再把它输入到收件人的终端里,让消息以普通提示词的形式到达。
怎样从另一个终端给正在运行的 Claude Code 或 Codex 会话发消息?
如果会话运行在 tmux 中,可以用 tmux send-keys 向它输入,但你得自己处理多行文本、回车时机、目标窗格的身份,以及输入框里已有的内容。Team Agent 把这些检查封装成了一条命令:team-agent send <agent> "消息"。
什么是 PTY?
PTY(伪终端)是交互式程序读取键盘输入、绘制屏幕所用的虚拟终端设备。终端模拟器和 tmux 都会创建 PTY。向 Agent 的 PTY 写入内容,就等同于在它的窗口里打字。
接收方 Agent 正在忙,消息会丢吗?
采用“先落盘再投递”的设计就不会。Team Agent 会先把每条消息写入收件人的追加式收件箱,再尝试输入,所以即使 Agent 正在忙、终端已经不在或者投递需要等待,消息都还在。
为什么不直接调用模型 API,而要驱动 CLI?
API 封装相当于重写一遍 Agent 循环:你会失去官方 CLI 的工具、配置、权限模型和订阅登录,还要按 token 付费。通过终端驱动真正的 CLI,一切都保持你配置好的样子。