会话之间互发消息
同一台机器上可以同时开着好几个 ZettCode 会话:一个在修解析器,一个在写文档,一个在审查分支。它们彼此看得见, 也能互相传话:你用 @ 点名的会话是一个引用,你的模型可以用工具把消息传给它。
它是怎么工作的
每个会话启动时都会向一个叫 broker 的后台小进程登记。第一个启动的会话负责把它拉起来;只要还有会话连着它就一直运行, 最后一个会话退出约三十秒后它自己退出。它只监听 127.0.0.1,并且只有能读取 ~/.zettcode/broker/ 里那个文件(里面是连接时必须出示的令牌)的进程才能和它说话。 连不上它的会话和以前完全一样:没有任何东西为它等待,也不会出错。
每个会话在列表里的名字就是你在 @ 后面输入的内容,按下面的顺序取第一个存在的:
- 会话的标题:
Fix the parser对应fix-the-parser; - 它所在的文件夹:还没有标题、工作目录是
~/work/zettdemos的会话叫zettdemos; - 标题和文件夹里都没有字母时,叫
session-加 id 的最后六个字符。
名字全部是小写 ASCII。两个会话会重名时(同一个目录里两个没标题的会话就是),后来的保留这个名字并接上 id 末六位(zettdemos-4a77c1), 既唯一又不会变。标题会在第一次回复之后才写入,名字也会随之变化, 所以请以菜单里显示的为准,不要沿用一分钟前看到的名字。
你自己这个会话的名字显示在顶栏里,在标题后面(◈ zettcode Fix the parser · @fix-the-parser · ~/work), 想让别人联系你时,告诉他输入什么就行。
给另一个会话写消息
输入 @ 和一个正在运行的会话的名字。输入时菜单会列出会话,每一项写明在做什么、是否在工作中、所在文件夹;你输入的内容会在名字、标题、文件夹和 id 开头里匹配,名字优先, 所以 @parser 能找到标题是 "Fix the parser" 的会话。
❯ @fix-the-parser 你看一下 tokenizer 现在还能处理 tab 吗?@ 后面的名字和文件、技能一样只是个引用,所以你的消息仍然发给本会话的模型。模型读到的是把 @ 换成 session "fix-the-parser" 之后的句子,并被告知这是哪个会话、怎么联系它,然后自己调用 send_message。 消息以名字开头时(如上面的例子),名字后面的内容就是模型要转达的话;名字出现在句子中间时,由它判断你的请求是不是给那个会话的。 无论哪种,shell 自己都不发送:你的消息在对话里仍然显示为你输入的样子,发送是模型的工具行,显示发给了谁、结果如何 (Delivered to …、queued,或者没送达的原因)。queued 表示对方正忙,等它当前请求结束后会开始处理。对方看到的发送者是本会话的 agent,因为发消息的是它。
工具只发文字,超过 32,000 个字符的消息会被拒绝。名字没有匹配、或者会话已经离开时,它只是普通文本:模型读到的就是原样。
收到消息
收到的消息按"你在那个会话里自己输入的"处理,只有两处看得出区别:
画法和你自己的消息一样——同样的
›、同样的文字列——只是背景换成略有不同的颜色(深色主题里是偏薰衣草的灰,浅色主题里是淡淡的鼠尾草绿),文字上方一行说明它来自哪个会话、是那个会话的 agent 还是它的使用者写的。这一行的颜色能区分这三种情况:对方 agent 是柔紫色,对方使用者是黄色,你自己是绿色。text← agent of session fix-the-parser › tokenizer 的改动已经合并。请对着 main 重新跑一遍解析器测试。模型会被告知它从哪来。来自另一个 agent 的消息是同事的请求,不是你的指令;模型会把它和你已经交代的事情对照着权衡, 不会仅凭这一条就做破坏性或不可逆的操作。
之后就是一次普通请求:等待行、回答、改动文件清单、标题、保存的会话。这条消息随会话一起保存,恢复会话时画法不变。
会话空闲时,消息立刻开始一次请求。这会在你没按回车的情况下消耗 token——这正是"来自另一个会话的消息"的用处;不想要的话可以关掉这个功能。
会话忙碌时,由消息自己说明想要哪一种处理:steer 消息(默认)直接交给正在运行的请求,让它现在就改变方向;follow-up 消息排队, 等当前请求结束后再开始,排在你已排队的追加消息之后。无法转向的消息——正在跑的是命令或旁问而不是对话请求、转向次数达到上限, 或者它来自一对一的聊天窗口(窗口由这条消息自己开始的那次请求来回答)——会像 follow-up 一样等待,发送方会被告知它已排队。 最多排 16 条,满了会告诉发送方。
daemon 从不转向——它一次只跑一个回合,回答
queued;网页网关的聊天回答delivered:它把消息收进正在显示的对话,而不是启动回合。
对一条消息的回答显示给你看。发送它的会话看不到,除非模型用工具回复。
模型能做什么
模型多了两个工具:
| 工具 | 作用 |
|---|---|
list_sessions | 列出其他正在运行的会话:该用的名字、id、各自在做什么、空闲还是工作中、所在目录。/btw 旁问可以用它。 |
send_message | 按 id、名字或标题给某个会话发消息,对方接收后即返回,不等回答:回复稍后会作为一条独立的消息到来。它可以带 mode:steer(默认)把消息交给对方正在运行的请求,followup 等那次请求结束后再说。旁问不能用它。 |
对话里,一次 send_message 调用是单独的一行,柔紫色,和别的会话的 agent 发来消息时标题的颜色相同。它显示发给谁、 说了什么的头几行,以及结果(Delivered to …,或没送达的原因)。点击这一行,可以看到完整的消息和完整的结果。
两个 agent 互相不停回复会永远停不下来,所以每条消息带着"已经经过几个会话"的计数,broker 会拦下第七跳。新消息从计数开头算起。
从 ZettCode 之外发消息
broker 是 127.0.0.1 上的一个小型 web 服务,所以本机上任何能读到令牌的东西都能用它:脚本、CI 钩子、别的工具。端口和令牌在 ~/.zettcode/broker/broker.json 里。
PORT=$(python -c "import json,os;print(json.load(open(os.path.expanduser('~/.zettcode/broker/broker.json')))['port'])")
TOKEN=$(python -c "import json,os;print(json.load(open(os.path.expanduser('~/.zettcode/broker/broker.json')))['token'])")
# 看看有哪些会话
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:$PORT/sessions
# 以 "ci" 的名义给某个会话发消息
curl -s -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"to": "fix-the-parser", "text": "构建红了", "from": {"name": "ci"}}' \
http://127.0.0.1:$PORT/messages会话收下消息(或说明为什么不收)之后才会返回结果。消息带 "mode": "followup" 时,忙碌的会话会等自己正在跑的请求结束再处理;不写这个字段就是默认的 steer,直接交给正在运行的请求,而空闲的会话两种情况下都会立刻开始。对方会把它显示成来自外部来源,并告诉自己的模型:发送者没有经过验证——模型会像对待任何外部请求那样权衡它,而不是当成用户在打字。不管消息自称什么,它都不能说自己来自某位用户或另一个智能体。
想出现在名单里、也想被回复的程序,要像会话一样注册:用同一个令牌打开 /inbox 的 WebSocket,发送 {"op": "session", "session_id": "...", "title": "...", "kind": "external"},之后大家就能给它写消息了。发给它的消息从这条连接上到达,它用拿到的 key 发消息。完整的交互写在 zettcode/src/zettcode/broker/protocol.py 开头。
管理 broker
平时不需要手动启动:第一个需要它的会话会把它拉起来,最后一个会话离开后约半分钟它自己退出。想自己启动、停止或重启 (升级之后,或者它卡住时),在 shell 里运行:
zettcode broker start # 没有 broker 时启动一个,并打印它的 pid 和端口
zettcode broker stop # 停掉这台机器上正在运行的 broker
zettcode broker restart # 先停,再启动一个新的每条命令都会直说做了什么:start 发现已经有 broker 时会说它正在运行(带 pid 和端口),而不是再启动一个; stop 发现没有在运行时也会说清楚,而不是报错。只有被认出确实是 ZettCode 的 broker 进程才会被结束(命令行必须是 python -m zettcode.broker),先请它自己停,超过时间还没释放锁才会强制结束。连在它上面的会话会发现这一点,并连到下一个 broker。
限制与注意
- 只限同一台机器、同一个用户的会话,不经过网络。
- 消息在送到终端之前会去掉转义序列和控制字符。
- 同一个会话每分钟最多发 20 条。
- 连不上 broker 或没有 broker 时,菜单里没有会话名字;
@name只是给模型的普通引用。 - 对方 agent 有它自己的权限,一条消息不会让它得到更多。
关闭
[sessions]
enabled = false关闭后会话不登记,@ 不再提供会话名字,模型也没有这两个工具。
broker 的文件
全部在 ~/.zettcode/broker/(或 $ZETTCODE_BROKER_DIR):broker.json 存端口、协议版本和令牌(只有你能读),broker.lock 保证只运行一个 broker, broker.log 记录启动和出现的问题。要停掉它,结束 broker.json 里 pid 对应的进程即可,下一个会话会重新拉起。 ZETTCODE_BROKER_IDLE 以秒为单位设置最后一个会话离开后它还保留多久。
升级 ZettCode 之后,旧版本留下的 broker 和新版本说不上话。新会话会发现这一点,结束那个 broker(只有确认它确实是 ZettCode 的 broker 才会结束) 并启动自己的,一两秒内完成;原来连着它的会话会自动重连到新的 broker。不需要等,也不需要你做什么。