TECH / Field note
StreamMA 实现记录
6 月 18 到 19 日把 StreamMA 接进 Go agent 的记录:从第一版 runtime、真实 subagent、drain loop、cache 命中率、ctx_a 复用,到最后的 subagent 降载和多行输入优化。

Filed on 2026.06.22 · Mono Weekend
这篇只是一次工程日常记录。6 月 18 到 19 日,我把 StreamMA 从一个论文里的通信想法,接到了当前 Go agent 的 subagent 体系里。它不是官方实现搬运。官方仓库当时只有 README 和图片,真正的 runtime、命令入口、事件流和 UI 反馈都是在本项目里自己做出来的。
我最开始想验证的点很简单:上游 agent 不必等完整回答结束,可以按 END_STEP 把 reasoning step 逐段发给下游。听起来像开个 stream=true 就行,真写起来不是。模型输出、DAG 调度、subagent session、上下文预算、cache 统计、TUI 输入框,最后全被牵进来了。
先让 subagent 的结果能回到父上下文
第一步不是 StreamMA runtime,而是 3203da5:把后台 subagent 的结果注入父上下文。
这个改动看起来不大,但它解决的是一个基础问题:subagent 跑完以后,父会话必须知道它做了什么。否则后面即使能起多个 worker,也只是旁路执行,主流程拿不到可以继续推理的材料。
这一步改了 internal/subagent/manager.go,也补了测试。后面 /streamma 能接真实 subagent,而不是只在一个 model client 里扮演多个角色,靠的就是这条路先通了。
第一版 runtime 很快写出来,但还不够真实
ae033a8 是第一版 StreamMA runtime。这个提交一次性加了 internal/streamma 下面的大部分骨架:parser、broker、context、event log、replay、runtime、types,还有 /streamma 命令和 Bubble Tea 命令注册。
那时的图还很小,是 A 到 B 到 D。/streamma 会把任务交给 runtime,runtime 解析模型流里的 END_STEP,把 step fanout 给下游,再从最后的 step 里拿答案。
这一版主要解决了几个底层问题。
END_STEP必须独立成行,代码块里的同名文本不能被误判。- broker 要能广播原始问题,也要能把上游 step 发给后继节点。
- runtime 需要事件日志,否则后面根本看不清 step 到底什么时候提交。
- 多前驱 DAG 不能等所有前驱收齐才跑。收到一个前驱 step,就应该能触发一次下游调用。
但这一版有个明显问题:它还是直接用同一个 model path 去模拟多个 agent。测试能过,论文机制也能演示,可它不像真实协作。多个逻辑 agent 共用一个上下文,本质上还是一条模型调用链。
改成真实 subagent 后,问题才开始变具体
d49f807 把 /streamma 接到了真实 subagent worker。这里开始才算进入当前实现的形态。
这次我把默认图改成更适合实现类任务的几个角色:planner、scout、builder、verifier、finalizer。/streamma 不再直接走主模型,而是要求必须有 subagent backend。没有 backend 就直接报 streamma requires subagent backend。
这个改动解决了“假多 agent”的问题,但也暴露了新的问题。
第一,最终回答里可能漏出 END_STEP。这在纯 runtime 里不一定明显,接到真实 worker 后就很刺眼。于是这里清理了 final answer 的边界文本,并补了测试。
第二,subagent prompt 必须带严格输出契约。每个 worker 都要知道 END_STEP 是协议边界,不是随手写的分隔符。
第三,工具不能随便进 StreamMA runtime。这个阶段的选择比较保守:runtime 传播公共 reasoning step,工具动作仍留在 subagent turn 内。早期 step 没验证就直接触发工具,风险太大。
drain loop 的 bug 很小,但很典型
接着是 9c6c34e。这个提交名里写得很直白:清理死代码、消除重复、修复排水循环。
里面最值得记的是 drain loop 的 bug。runtime cancel 后要排空 signal channel,本来代码里用了 break,但它写在 select 里面。Go 里这种 break 只会跳出 select,不会跳出外层 drain loop。
这种 bug 很烦。代码读起来像是“该跳出去了”,实际没有。修复方式也朴素:换成 labeled break,让它明确跳出 drain。
同一个提交还顺手把几个热路径和小浪费修了。
allCompleted和activeInvocationCount从每次 O(N) 扫描改成activeCount、completedCount字段。- broker 的
Dequeue从 copy-shift 改成重切片,并把头部置空,方便 GC 回收。 PropagateEOF里重复算的edgeKey改成局部变量。event_log.Append里重复 clone 的 event 去掉。- parser 里两段几乎一样的 size check 合成一个函数。
- 没被 runtime 读取的
sinks、topologyTag和旧 helper 都删掉。
这一轮不是为了加功能,而是把刚写出来的 runtime 磨到不那么松。
UI 先显示出来,再慢慢显示对
2b3fb2d 和 7c9f305 主要是 UI 和统计。
StreamMA 一跑起来,侧边栏和 context meter 的问题就很明显。只看父会话 token 不够,因为真正消耗 token 的还有子 agent。于是 WorkerResult、TaskSnapshot、Manager、Runner 一路加了 subagent token 汇总,ContextStats() 开始把父会话和子 agent 的 token 合起来算。
通知也调了一下。之前系统消息标题都叫 streamma 或 subagent,看起来很糊。后来改成显示具体 agent,比如 planner、builder,body 里再写 started、finished、depth。侧边栏 running 状态也从一个重复的图标改成运行时长,再后来加了 spinner。
还有一个很实际的小 bug:补全候选加载完成后,高度变了,但布局没重新算,所以输入框会错位。修复就是在 fileCompletionLoadedMsg 后补 relayout()。不高级,但这种就是 TUI 里最常见的坑。
cache 命中率也先算错过。开始用的是 labelCache / limit,这更像“cache 占上下文窗口比例”,不是命中率。后来改成 labelCache / labelUsed,测试里的期望也从 5% 改成 20%。
cache 一直是 0,根因不在面板
41b97c7 是一个很干净的修复:Anthropic prompt caching 没真正启用。
现象是 cache 面板一直显示 0。前面 UI 算法修过以后,值还是不对。最后查到根因在请求构造:buildAnthropicMessagesRequest 把 system prompt 当纯字符串发出去,没有带 cache_control。Anthropic API 不会为这种 system prompt 建缓存,所以 cache_read_input_tokens 永远是 0。
修复方式是把 System 从 string 改成 json.RawMessage,然后把 system prompt 编成这种结构:
[
{
"type": "text",
"text": "...",
"cache_control": {
"type": "ephemeral"
}
}
]这样第一次请求会记到 cache_creation_input_tokens,后面相同 system prompt 才会进 cache_read_input_tokens。这个问题对 StreamMA 很关键,因为论文里成本优势很依赖 prefix cache。cache 没开,StreamMA 很容易只剩下更多调用次数。
ctx_a 复用是后来才真正对齐的
0f8d2e3 是这组提交里最关键的一次返工:对齐 ctx_a 会话复用。
前面的真实 subagent 版本已经比伪多 agent 好很多,但还有一个问题:同一个逻辑 agent 被多次触发时,如果每次都像新任务一样塞完整 problem 和完整 prompt,它就不是论文里的 ctx_a。它会重复吃上下文,也会破坏 cache。
所以这里做了几件事。
第一,每个逻辑 agent 维护自己的 session key。第一次 builder 被调用时还没有 session id,后面再被 step 触发,就复用同一个 builder session。
第二,prompt 分成 bootstrap 和 incremental。第一次写入 Original problem,后续只写 New inbound step from ...。测试里专门检查了后续 builder prompt 不再重复 base problem。
第三,subagent path 改成 streaming。测试 TestRunTurnStreamMAFansOutSubagentStepBeforeDone 卡住 planner 的 Done,让 planner 先吐一个 step,再确认 builder 已经启动。这证明 fanout 发生在上游完成之前,不是等整个 subagent 结束后批处理。
第四,严格边界开始变硬。测试里让 planner 和 scout 故意漏掉 END_STEP,runtime 会报 stream ended before exact boundary,builder 不会从 recovered step 启动。
这一步之后,我才觉得这个实现接近 StreamMA 本来的样子。
最后发现 subagent 负载也要管
d6321ba 是第二天的收口提交,标题是“实现 StreamMA 子代理降载与输入框多行优化”。
这里的“降载”不是把功能砍掉,而是控制每次 subagent 调用吃进去的东西。
第一,StreamMA problem、conversation context、inbound step 都加了字节级上限。当前任务保留头部,历史上下文保留尾部,并写入明确的 truncated marker。这样可以避免一段很长的历史或上游 step 把请求撑爆。
第二,StreamMA subagent 的 system prompt 不再塞完整工具 schema。SetCompactToolPrompt(true) 后,system prompt 只保留工具名和简短描述。完整 schema 仍然通过 native tools 传给模型。这样 subagent 还能用工具,但不会把大段 schema 复制进 system prompt。
这个坑很实际。agent 越多,重复系统提示和工具 schema 的成本就越显眼。StreamMA 本来就会增加调用次数,如果每次调用还带一大坨工具 schema,账单和上下文都会炸。
输入框也被 StreamMA 拖出来修了
同一个收口提交还修了多行输入。
StreamMA 任务经常不是一句话,用户可能直接粘一大段需求。原来的输入框在多行、长行和大段粘贴时,容易把底部布局顶得很难看,cursor anchor 也会偏。
后来做了几个处理。
- 输入框最少显示固定行数,最多显示 10 行。
- 逻辑内容可以超过 10 行,不因为可视高度限制丢内容。
- 大段粘贴后,输入框只展示前几行和后几行,中间用
... 3 lines folded ...折起来。 - 回车提交时仍然提交完整文本,不提交折叠后的投影文本。
- 光标移到折叠区域时,临时展开中间内容,避免用户编辑时看不见自己在哪。
这些和论文没关系,但和真正使用 /streamma 很有关系。一个多 agent 命令如果连输入大段需求都不舒服,后面的 runtime 再漂亮也没什么用。
这两天真正解决的问题
回头看提交历史,问题大概分成几类。
一开始的问题是“不真实”。ae033a8 能跑,但还是单个 model path 模拟多个 agent。d49f807 把它接到真实 subagent,后面又通过 0f8d2e3 做 session 复用,才把这个问题收住。
第二类问题是“不稳”。END_STEP 会泄漏,缺边界会被 recovered step 带着继续传播,drain loop cancel 后可能不按预期退出。这些都靠测试逼出来,然后一点点收紧。
第三类问题是“不省”。cache 统计先是公式不对,后来发现 API 请求根本没启用 Anthropic prompt caching。再往后,subagent prompt 又需要降载,不能每次把历史和工具 schema 都塞进去。
第四类问题是“不好看,也不好用”。侧边栏不知道哪个 agent 在跑,token 不含子 agent,候选框加载后输入框错位,大段粘贴会撑坏输入区域。这些不是核心算法,但会直接影响我愿不愿意继续用它。
现在这版到什么程度
现在这版能做到几件事。
/streamma必须走真实 subagent backend。- step parser 能按严格
END_STEP解析增量输出。 - runtime 能在上游 Done 前 fanout 已提交 step。
- DAG 支持 chain、tree、graph,且多前驱节点是到达即触发。
- 同一个逻辑 agent 会复用自己的 session。
/streamma-trace能看到 committed step、producer、target、usage、cache hit 和 cache miss。- prompt 和 inbound step 有字节上限,StreamMA subagent 使用 compact tool prompt。
- TUI 对多行输入和大段粘贴更稳。
没做的也很明确。现在还没有 Single、Serial、Stream 的自动协议选择,没有 step verifier,没有扰动实验,也没有完整 benchmark。也就是说,它已经是能用来跑工程任务的实现雏形,但还不能说复现了论文里的收益。
这次最有用的经验其实很普通:先把东西接进真实 runtime,再让测试和实际 UI 把问题暴露出来。论文给的是方向,提交历史里留下的才是实现时真正会遇到的坑。
提交线索
3203da5:把后台 subagent 结果注入父上下文。ae033a8:加 StreamMA runtime、parser、broker、event log、replay 和/streamma命令。d49f807:接入真实 subagent worker,改成 planner、scout、builder、verifier、finalizer。9c6c34e:修 drain loop,清死代码,优化 broker 和 runtime 热路径。2b3fb2d:补 subagent token 统计,调整 context meter 和侧边栏显示。7c9f305:修 cache 命中率公式、补全框 relayout、侧边栏颜色和动画。41b97c7:给 Anthropic system prompt 加cache_control,让 prompt caching 真正生效。0f8d2e3:对齐ctx_a会话复用,改成 incremental prompt 和 streaming subagent。d6321ba:给 StreamMA subagent 做上下文降载,优化多行输入和大段粘贴。