Files
HomeAgent/internal/plugins/skillmgr/builtin/kernel-rtchild/SKILL.md
JianFeeeee bcf45498a0 feat(skill): 补 kernel-rtchild + 两条判据锁住 rt_children 静默失败
## kernel-rtchild skill

`resident_agents` 有 `resident-subagent-hygiene` skill,而
`rt_children`/`rt_transcript` 这一对整个**没有 skill** —— 模型只能靠
工具描述猜语义。

最容易踩的坑:**工具执行期不可打断**。模型一旦进入工具执行期就听不见
新输入,所以调工具前必须先用 outputch 说一句"稍等",否则用户会觉得
"它没在听我说话",而实际上它只是卡在一个 3 秒的命令里。

skill 覆盖:三种子 agent 的分界、通道必须成对且两侧声明 rt、打断语义、
只出文本(TTS 是插件的活)、窗口裁剪但原文保留、回收后用 rt_transcript
分段取、闲置零 token 但必须 stop、四条回收判据、五种常见错误对照表。

## 两条判据(都来自一次真实事故)

生产 e2e 时我用 `input="..."`(工具声明的是 `inputch`)创建 rtchild:
`strArg` 只查精确键名 ⇒ 读到空串 ⇒ **本应报"必须成对给出",但它成功了**,
还把 output 通道划给了新子。之后灌语音,该子轮次永远是 0。

症状是"通道注册了、插件活着、推送 ok、但子没反应",
与真因(参数名拼错)隔了三层。

- `WrongParamNameIsRejectedNotSilent`:写错必须失败、必须指名正确参数名、
  必须无副作用(不建子、不动通道归属)
- `PairAssignmentIsAtomic`:成对分配第二个失败时第一个必须回滚
  (原来那句 `rollback()` 没有判据覆盖,是死代码)

变异验证:让代码对 `input`/`output` 静默兜底 ⇒ 第一条变红
(`参数名写错却创建成功了:[typo-child] —— 这会得到一个收不到输入的哑子`)。

go build/vet/gofmt 干净;go test ./... -count=1 -race = 62 ok / 0 FAIL / 0 RACE
2026-10-30 18:09:50 +08:00

4.6 KiB
Raw Blame History

name, description, version
name description version
kernel-rtchild Realtime child for live calls; inputs interrupt, output is text only. 1.0.0

实时子 agent(rtchild)

rtchild 是第三种子 agent,只为一件事存在:通话。 它不是"更快的驻留子",也不是"带语音的子任务"——如果场景不是 "用户正在说话、你必须立刻接",就不要用它。

1. 三个子 agent 的分界

类型 工具 生命周期 适用
一次性子任务 spawn_child 一轮内跑完就没 单一明确的小活
驻留子 resident_agents 长期存在,有自己的调度器/记忆 长期派驻、持续盯一条线
rtchild rt_children 一次通话 实时对话,输入随时打断

选错的代价不是"慢一点":把通话场景做成驻留子,用户每句话都要排队等前一轮 跑完,主观感受是"这个助手反应特别慢";把长期任务做成 rtchild,它会被 每一句新输入反复打断,永远做不完。

2. 通道必须成对,且两侧都要声明 rt

inputch(rt)   ← 采集层喂进来的话
outputch(rt)  → 你说的文本(内核只出文本,TTS 由插件做)
  • 必须成对:只给一侧会被拒(两侧都会校验 Realtime)。
  • 两侧都要 Realtime: true:普通通道也能划给 rtchild,但承载不了 rt 流。 这条以前被跨进程白名单丢过——插件声明了、内核收到 false、报错却指向 "创建 rtchild 失败",与真因隔两层。
  • 划进去必须验一次:投一条输入后看 list 里轮次是否 +1。没动就是没划对。

3. 打断是默认语义,不是例外

往 rt 输入通道投递就是打断:当前轮的 LLM 流被取消,新输入立刻开新一轮。 不排队、不重排、不重放残留。

唯一不被打断的窗口是工具执行期。 工具跑到一半被取消会留下半完成的 副作用(文件写一半、命令跑到一半),所以那里挂起取消、等它跑完。

⇒ 由此得出一条必须遵守的规矩:

调工具之前,先用 outputch 说一句"稍等"。

模型一旦进入工具执行期就听不见用户的新输入了。不说这句,用户会觉得 "它没在听我说话"——而实际上它只是卡在一个 3 秒的命令里。

4. 输出只有文本

outputch(rt) 收到的永远是文本。内核不做音频合成:

  • TTS / 字幕 / 播放 → 插件自己的事
  • 不要指望内核返回音频,也不要为了"发语音"去挂 audio 能力位

5. 上下文:窗口裁剪,但原文不丢

rtchild 用的是普通上下文 + 窗口(没有 rt 专用的分页/锚点那一套):

  • 旧内容不再进模型(否则窗口撑爆,且早年的寒暄会污染当前对话)
  • 旧内容原文仍在(不删,只是不进视图)
  • 裁剪从不切开 tool call 与它的结果(切开会留下无主的 tool result)

6. 回收:原文交给你,取舍由你定

rt_children action="stop" 会把这场通话作为一条 L4 中断交给你, 并告诉你共多少条消息。

★ 不要直接把整段塞进上下文——那正是窗口爆掉的原因。 用 rt_transcript(id, offset, limit) 分段取,只留你真正需要的。

如果 stop 回的是"本次通话没有上下文需要交接",那是真的没有 (该子还没产生任何消息),此时不该再调 rt_transcript。

7. 闲置零 token

没有输入时不跑任何 LLM 调用。所以通话结束必须 stop—— 挂着的 rtchild 虽然不烧 token,但它占着 rt 通道、占着登记表, 而且你迟早会忘了它。

8. 回收判据(出现任一条,当轮就 stop)

  1. 通话结束:用户挂了/说了再见/话题收口 → stop,并按第 6 条取原文。
  2. 通道没了:插件卸载、对应通道注销 → stop,否则是个收不到活的僵尸。
  3. 建错了(输入通道填成输出通道名等)→ 立刻 stop 重建, 别留着调试——它会把本该给别人的输入吃掉。

9. 常见错误

现象 真因
create 报"通道不能承载 rt 流" 该通道没声明 Realtime,或用了只注册了一侧的通道
create 成功但 list 里轮次永远 0 inputch 名填错了(常见:把 outputch 名当 inputch 用)
用户说"它不听我说话" 工具执行期不可打断,且模型没先说"稍等"
期望收到音频却只有文本 内核只出文本,TTS 是插件的事
stop 后拿不到原文 顺着提示用 rt_transcript 分段取,不要期待整段自动进上下文