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

101 lines
4.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: kernel-rtchild
description: Realtime child for live calls; inputs interrupt, output is text only.
version: 1.0.0
---
# 实时子 agent(rtchild)
rtchild 是**第三种**子 agent,只为一件事存在:**通话**。
它不是"更快的驻留子",也不是"带语音的子任务"——如果场景不是
"用户正在说话、你必须立刻接",就不要用它。
## 1. 三个子 agent 的分界
| 类型 | 工具 | 生命周期 | 适用 |
| --- | --- | --- | --- |
| 一次性子任务 | `spawn_child` | 一轮内跑完就没 | 单一明确的小活 |
| 驻留子 | `resident_agents` | 长期存在,有自己的调度器/记忆 | 长期派驻、持续盯一条线 |
| **rtchild** | `rt_children` | 一次通话 | **实时对话**,输入随时打断 |
选错的代价不是"慢一点":把通话场景做成驻留子,用户每句话都要排队等前一轮
跑完,主观感受是"这个助手反应特别慢";把长期任务做成 rtchild,它会被
每一句新输入反复打断,永远做不完。
## 2. 通道必须成对,且两侧都要声明 rt
```text
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` 分段取,不要期待整段自动进上下文 |