设计:docs/zh/resident-subagent-design.md §4.5(里程碑 N1b)。 ## 输出通道授权集合(默认完整授权,父可收窄) `AgentConfig.AllowedOutputs`(nil/空 = 完整授权)。三处过滤点必须一致, 否则会出现「列表里看不到、按名字还能调」的裂缝: 1. **工具表**:不为未授权的通道生成 output_send__X(模型看不到就不会调) 2. **列表工具**:output_list_channels 只列授权的 3. **调用点**:凭名字直调未授权的输出门必须被拒(纵深防御) ## 输出通道 → 目标 agent 的 inputch 解析 `ChannelRegistry.BindOutputTarget / ResolveOutputTarget`: 把输出通道解析成「目标 agent + 目标 inputch」,这是"输出可寻址到具体 agent" (子→父、父→指定子)的**数据面**;真正的跨 agent 投递在里程碑 N4。 未登记的输出通道 ok=false —— 表示由传输层 device 自行处理(qq/webui 这类)。 已登记目标的输出通道,在 output_list_channels 里会标出「目标: <agent> / inputch <名字>」。 ## 验收 `internal/agent/core/output_grant_test.go`(3 项): - 默认完整授权:全部输出门生成 + 列表含全部 - 白名单收窄:三个过滤点同时生效(工具表 / 列表 / 直调被拒) - 目标解析:绑定/解析、未登记由传输层处理、列表标出目标、空名报错 全仓 go test ./... 37 包 ok / 0 FAIL。
28 KiB
驻留式子 Agent 设计(轻量内核 · 两级记忆 · 父子中断)
前置:本文建立在《输入调度器设计》(
docs/zh/input-scheduler-design.md)之上。 那里已经落地了:两类别输入(中断 / 排队)、四级中断优先级、可抢占、 现场保存/恢复、中断栈(LIFO,结构上界 4 帧)、任务级回执、panic→L4。 本文只描述驻留式子 agent 这一新能力,以及它对既有实现的改动。标记:[已定]= 用户明确拍板;[默认]= 本文给出的可逆默认取值,实现时在提交信息里标注。
1. 背景与目标
现状的子 agent 是工作式轻量子:spawn_child 起一个一次性 goroutine(临时 msgs),
跑完把结果经 selfInputCh 回投父,然后销毁。它没有自己的中断机制、没有通道分配、
没有可查询的状态面,父也无法在它运行途中干预它。
要让父 agent 能长期派驻一个下属去持续处理某类工作(一个 inputch 上的来源、一段长期目标), 就需要一种新的子 agent:驻留子。它必须满足:
- 有自己的轻量内核(自己的调度器、中断机制、上下文),所以父能"打断它"、"查它"、"回收它";
- 有自己的记忆空间(可写),但改不了父的主记忆 —— 记忆入库的决策权留在父手里;
- 与父之间有双向、可寻址的通道(子→父、父→指定子),且父对子的消息是最高级中断;
- 有一张可被父查询的状态面(inputch 处理表),使父不打断它也能知道它做到哪;
- 生命周期由父掌握:父可随时销毁它,父退出时必须销毁全部。
两类子 agent 并存[已定]:工作式轻量子原样保留(它是"内核自循环"的一部分, 不是对等 agent);驻留子是新增的第二类。
2. 术语
| 术语 | 含义 |
|---|---|
| 根 agent | 进程级主 agent,拥有完整内核(含记忆读写、全部通道) |
| 驻留子 | 父创建、长期驻留的子 agent,拥有轻量内核与临时记忆空间 |
| 工作式轻量子 | 现状 spawn_child 的一次性子任务(不是内核实例) |
| inputch | 最基本的输入路由单位:对"中断输入 / 排队输入"两者的高层抽象,是路由与分配的单位;由插件注册,一个插件可注册多个 |
| outputch | 输出通道;输出是 agent 的主动调用,并可寻址到具体 agent |
| main 空间 | 主记忆空间:父读写、所有子只读、全部子共享 |
| temp 空间 | 子临时记忆空间:该子读写、子之间互不可见 |
| 状态面 | 子对外(对父)可查询的状态:含 inputch 处理表与产出 |
| 处理表 | 按 inputch 记录每轮处理信息的状态表(子持有,父 pull) |
| 登记表 | 父持有的全部子 agent 名录(id / 状态 / 通道 / 状态面句柄) |
| contextfull | 子的上下文窗口满,产生 L4 中断通知父 |
| 压缩 / 回收 / 销毁 | 父对 contextfull 的三种处置(见 §9) |
3. 角色与内核形态
| 根 agent | 驻留子 | 工作式轻量子(保留不动) | |
|---|---|---|---|
| 内核 | 完整 | 轻量 | 无(不是内核实例) |
| 调度器 / 四级中断 / 中断栈 | ✅ | ✅(L4 只来自父) | ❌ |
| 上下文 | 完整(记忆介导) | 传统上下文(消息序列) | 临时 msgs |
| 记忆 | main 读写 | 读 temp ∪ main,写 temp | 无 |
| 通道 | 全部 | 父划入的输入通道 + 授权的输出通道 | 无(结果回投父) |
| 插件与工具 | 全部 | 父授权,默认完整授权[已定] | 现有剔除规则不变 |
| 状态面 / 处理表 | — | ✅(父 pull) | ❌ |
| 子→父 | — | 主动消息 = L3 中断 | injectSelfChannel → selfInputCh(排队) |
| 主→子 | — | L4 中断(取消当前状态 + 插入新消息) | 无 |
| 生命周期 | 进程级 | 父持登记表;父可随时销毁;父退出必须全部销毁 | 跑完即销毁 |
4. 通道模型
4.1 inputch 的定义 [已定]
inputch 不是通道名标签,也不是与 outputch 配对的东西。它是:
- 最基本的输入路由单位 —— 路由粒度到此为止:比"插件"细、比"通道名字符串"实;
- 由插件注册,且一个插件可注册多个 —— 同一个插件的多个 inputch 是彼此独立的
路由单位(可以绑给不同 agent、可以分别授权)。登记接口即现有的
RegisterInputChannel(name, def),调 N 次就是 N 个 inputch; - 对"中断输入"与"排队输入"两者的高层抽象 —— 两类输入都从 inputch 进出;
- 路由与分配的单位:
- 路由:一条输入投给哪个 inputch,就是"该由谁处理"的既定事实 ⇒ 路由发生在进内核之前;
- 分配:inputch 是可分配资源(父把输入通道划给子)。
「中断 vs 排队」是每条输入自己的类别(由注入 API 决定),级别(L1–L4)也是 每条输入的属性,都不是 inputch 的属性。
4.2 三个入场(沿用现有实现)
| 入口 | 承载 |
|---|---|
inputCh |
外部客户端 / 插件的排队与中断输入(已预寻址) |
interruptCh |
中断入站(外部 / 插件 / 内核自身) |
selfInputCh |
内核自循环:consolidation、工作式轻量子回投 |
4.3 输出是主动调用 [已定]
- 异步通道(qq / 微信 / 群聊…):必须显式调用
output_send__{通道名}才真正送达; - 同步通道(webui / cli / 终端):返回纯文本,内核把文本交给等待方 ——
走输入事件自带的
ResponseCh,这是事前定好的回程。
⇒ 内核不持有"当前通道"可变状态,提示词也不预设 outputch。
(要删 Agent.currentOutputChannel;要删 tooldefs.go 里"当前输入来源通道是 X,
对应输出门工具是 output_send__X"那两行。)
之前把这件事说成"内核路由"是错的:内核只负责投递到既定的回程与执行显式的输出调用。
4.4 通道分配:不对称 [已定]
- 不按对划分。父为子:划入若干 inputch(单位是 inputch,可以来自同一个插件的不同 inputch)
- 授权一组可用输出通道(授权集合,不是一对一)。
- 输出通道可寻址到具体 agent:
- 子 → 主:子直接打到主(经输出通道投进主的 inputch);
- 主 → 指定某个子:父经输出通道投进指定子的 inputch。
4.5 已经落地/待落地的两件事
已落地(N1a):inputch 登记表(归属插件 / 归属 agent / 容量 / 默认回程 / 策略)+ 共享登记表
- 单工具多视图总览(
input_channels,见 §4.6)。
已落地(N1b):
- 输出通道授权集合[已定:默认完整授权,父可收窄]:
AgentConfig.AllowedOutputs(nil/空 = 全部)。三处过滤点必须一致, 否则会出现"列表里看不到、按名字还能调"的裂缝:- 工具表:不为未授权的通道生成
output_send__X(模型看不到就不会调); - 列表工具:
output_list_channels只列授权的(已登记目标的会标出"目标: agent / inputch"); - 调用点:凭名字直调未授权的输出门必须被拒(纵深防御)。
- 工具表:不为未授权的通道生成
- 输出通道 → 目标 agent 的 inputch 解析:
ChannelRegistry.BindOutputTarget/ResolveOutputTarget(未登记的通道由传输层 device 自行处理,如 qq/webui)。 这是"输出可寻址到具体 agent"的数据面;真正的跨 agent 投递在 N4。
4.5.1 通道的一等化(后续要求)
现在 Source / OutputChannel 只是字符串标签,IOManager.inputCh 是一条全局 channel,
inputChannels 只是策略表(ChannelDef:NoMemory / Cleaner / ContextPolicy),
没有归属、没有绑定、没有容量。要实现 §4.4 需要新增:
- 通道注册层:
inputch成为一等对象。它已经是最基本的输入路由单位, 所以注册表以 inputch 为键(一个插件 → N 个 inputch),并给每个 inputch 带上: 归属/被划给的 agent · 容量 · 可接收类别 · 输出目标解析; ⇒ 同一个插件的两个 inputch 可以分别划给不同 agent、分别限额; - 输出通道 → 目标 agent 的 inputch 的解析;
- 授权过滤:
output_list_channels只列该 agent 被授权的通道。
4.6 inputch 总览:单工具多视图 [已定]
父 agent 必须能看清两件事:有哪些 inputch 已注册(谁注册的)、它们是怎么划分的。
按用户要求构筑为单工具多视图(一个工具 + 一个 view 参数),而不是一堆小工具 ——
视图切换比工具增殖更好用,也更省提示词预算。
工具:input_channels
| view | 内容 |
|---|---|
all(默认) |
全部已注册 inputch:名字 / 归属插件 / 归属 agent / 容量 / 记忆策略标记 |
mine |
划给本 agent 的 |
unassigned |
尚未划出的(可按需分配) |
by_agent |
划分情况总览:按归属 agent 分组列出各自拥有哪些 inputch |
detail(需 name) |
单个 inputch 的全字段(注册插件 / 归属 / 容量 / 默认回程 / 记忆策略) |
- 未知
view必须报错并列出可用值(拼错不得被静默当成默认视图)。 - 登记表是可共享对象(
*ChannelRegistry):根 agent 与它的驻留子共用同一份, 这样"划入/授权"才有意义(默认每个 agent 自带一份,向后兼容)。 - 插件重载不得抹掉划分:重复登记只更新「归属插件 + 策略」, 保留已有的 Owner / Capacity / Output。
5. 记忆模型:两级空间 [已定]
主记忆空间(main) ← 子【只读,可看到全部】;父【读写】;所有子共享
子临时记忆空间(temp) ← 该子【读写】;每个子独立、互不可见
子的记忆查询 = temp ∪ main 子的记忆写入 → 只落 temp
| 根 agent | 驻留子 | |
|---|---|---|
| main | 读写 | 只读(可见全部) |
| temp | —(它自己就是 main 的所有者) | 读写(自己的空间) |
| 查询范围 | main | temp ∪ main |
5.1 "轻量内核"的真正理由
不是砍功能,而是记忆层被作用域化:
- 写目标被限定到
temp(子自己的命名空间); - 读视图被扩成
temp ∪ main(两个空间的并集)。
完整内核的记忆层硬绑定在单一 main 空间上。要让每个 agent 都有自己的空间 + 并集读视图,记忆层就必须接受每个 agent 一份的作用域参数 —— 这就是轻量内核存在的理由。
5.2 推论:记忆写工具不禁用,而是重定向
子是能写的(写自己的 temp)。因此:
- 记忆写工具(图记忆写、文本记忆写、文档记忆写、向量索引写、媒体落盘) 不禁用,而是重定向到 temp 命名空间;
- 检索注入算"读",范围
temp ∪ main; - 子不能写 main ⇒ "哪些内容进入主记忆"的决策结构上只在父手里(§9.2)。
5.3 三种处置对记忆的作用
| 动作 | temp | main | 子 |
|---|---|---|---|
| 压缩 | 保留 | 不写 | 继续 |
| 回收 | 父读 → 选中的 promote 进 main → 丢弃 temp | 父写 | 取消 |
| 销毁 | 直接丢弃 | 不写 | 立刻移除 |
5.4 待钉的边界 [默认]
- temp 与 main 是同一套记忆子系统里的命名空间(同一批表/索引 + 一个 space 维度), 不是独立存储 ⇒ "并集读"就是一次查询里的两个 space 条件;
- 父可读子的 temp(要决定 promote 什么),属"查看状态面"的一部分;
- temp 与子同生共死;子之间 temp 互不可见。
6. 中断模型:两条独立阶梯 [已定]
在【父的】中断阶梯上:
子的主动消息 = L3 中断 (子主动汇报,带子标识)
子的 contextfull = L4 中断 (资源耗尽,需父立即决策,带子标识)
在【子的】中断阶梯上:
父的消息(发送消息) = L4 中断 ← 子的 L4 归父独占
6.1 L4 归属通则
某个 agent 的 L4 只属于它的"内核"。
- 根 agent 的内核 = 内核自身(panic / 内核事件 selfip)+ 内核级插件(WebUI 终止按钮);
- 驻留子的内核 = 父 agent。
⇒ 现有 isKernelLevelSource(只认编译期内置插件)泛化为"该 agent 的上级",不为子开特例。
6.1.1 父消息 = 子的 L4(落地机制,钉死)
父 → 子的"发送消息"是一条 L4 中断,它是子的阶梯上唯一的 L4 来源。具体落地:
父【发送消息】到指定子
└─ 经输出通道寻址到该子的某个 inputch
└─ 在该子的调度器里按 L4 登记(子的内核级来源 = 父)
└─ 子的 L4 > 子内部一切 ⇒ 立即打断子的当前任务
- 子内部任何来源都够不到 L4:子自己的输入注入、工具、定时器、插件…… 一律被夹到 ≤ L3(与根 agent 里"外部插件被夹到 L3"完全同一条规则, 只是"内核级来源"从"编译期内置插件"变成了"父 agent")。
- 后果一:子内部任何东西都压不过父(父的话是子的最高级输入)。
- 后果二:父的"取消当前状态 + 插入新消息"因此是确定能生效的 —— 不会因子内部正在跑什么而被挡住(除非子处于不可抢占临界区, 此时按调度器既有规则在安全点生效)。
子侧内核级事件如何上报(推论,待确认):子的 panic / contextfull 属于子侧的内核级事件, 但在子的阶梯上 L4 归父独占,所以它们不能作为"子自己的 L4";应当上报给父, 在**父的阶梯上以 L4(带子标识)**出现 —— 与 §9 的 contextfull 同一条通路。 (即:父侧 L4 = 子侧内核级事件的接收位;子侧 L4 = 父控制语的发射位。)
6.2 为什么 contextfull 是 L4 而不是 L3
它是必须由父立刻决策的场合(三个处置都与子的存续有关),且决策要读一整个状态面 ⇒ 只能"推信号 + 拉状态",不能把状态塞进中断消息里。
7. 父对子的控制面(6 个动作)[已定:原语在内核,决策在父的模型]
| 动作 | 语义 | 子是否继续存在 | 走哪条路 |
|---|---|---|---|
| 创建 | 划入输入通道 + 授权输出通道 + 授权插件/工具(默认完整)+ 在固定提示词之上注入任务提示词 | — | 内核原语 |
| 发送消息 | 经输出通道寻址到子的 inputch:取消当前状态 + 插入新消息 | 是 | 对子 = L4 中断 |
| 查看 | pull 子的状态面(处理表 + temp 产出),不打断子 | 是 | 纯查询,不走中断 |
| 压缩 | 保留语义:压上下文 + 清理处理表 | 是(同一驻留子) | 见 §9 |
| 回收 | 取消语义:父看状态面 → 选择哪些 promote 进 main → 取消该驻留子 | 否 | 见 §9 |
| 销毁 | 立刻销毁并从登记表移除(不收割) | 否 | 立即 |
- 创建/销毁/回收/查看/发送是父可调用的原语(工具);决策(压还是收、收哪些) 在父的模型手里 —— 内核不替父决定。
- 默认完整授权[已定]:子默认拿到全部插件与工具(含输出门); 父可在创建时收窄(收窄工具子集、收窄可用输出通道集合)。 ⚠️ 默认含输出门意味着子可以直接对用户通道发消息;若要默认收窄,改一处默认即可。
8. inputch 处理表
8.1 归属与方向
- 子是持有者;父主动查看(pull),不是推给父。
- 内容:按 inputch 记录每一轮子对该 inputch 的处理信息。
- 存在意义:长期驻留子的进度可见性 —— 父不必打断它就能知道它做到哪。
8.2 写入规则
- 子主动写入时,系统不自动写;
- 子未主动写入时,系统自动把该轮 inputch 对应的信息写进去;
- ⇒ 每一轮必有记录,父不会看到空洞。
[默认]"主动写入"的动作形态 = 子调用一个 inputch_note 类工具;
自动写入在轮次结束时由内核兜底。
8.3 生命周期 = 上下文窗口
处理表记的是"当前这段上下文窗口里每轮 inputch 做了什么"。 因此:
- 压缩必须清表(窗口被压成摘要后,逐轮记录被摘要取代;留着会让父看到与当前窗口 不对应的陈旧状态);
- 回收不必清表(表就是父刚读过的收割材料,子都没了,表自然作废);
- ⇒ 处理表天然有大小上界(窗口多大、表最长多长),不需要额外容量策略。
9. contextfull 的处置
子的上下文窗口满
└─ 产生 contextfull → **L4 中断**通知父(中断信息里标明是哪个子)—— 只推信号
└─ 父【查看】子的状态面(处理表 + temp 产出)
├─ 【压缩】压成摘要 → 清处理表 → 子续用 (保留语义)
├─ 【回收】选择 temp 中哪些 promote 进 main (取消语义)
│ → 丢弃 temp → 取消该驻留子
└─ 【销毁】立刻销毁并移除 (不收割)
| 动作 | 语义 | 子上下文/成果 | 子 agent | 处理表 |
|---|---|---|---|---|
| 压缩 | 保留 | 压成摘要 | 继续存在 | 必须清理 |
| 回收 | 取消 | 选中的 promote 进 main | 取消 | 不必清 |
| 销毁 | 立刻销毁并移除 | 不收割 | 立刻销毁 + 出登记表 | 无关 |
[默认]压缩由子的轻量内核自己执行(它拥有自己的上下文与 LLM)。
10. 登记表与生命周期硬约束
- 父持 agent 登记表,记录全部子:
id / 状态 / 划入的输入通道 / 授权的输出通道 / 授权的插件与工具 / 状态面句柄。 - 它是查看 · 发送 · 压缩 · 回收 · 销毁的寻址依据。
- 硬约束(必须写成测试):
- 父
Stop()⇒ 销毁全部子(取消运行中的任务、停轻量内核、释放其通道), 登记表清空、不留孤儿; - 子不得比父活得久(无孤儿 goroutine / 无悬空通道 / 无残留 temp)。
- 父
11. 并发与不变量
沿用输入调度器的并发模型,并按多 agent 扩展:
- 每个 agent 一个调度器 goroutine(根 agent 与每个驻留子各一个), 它独占自己的队列 / running / 中断栈 / 帧。
- 跨 agent 投递只经通道(值传递),不共享帧;父永远不能直接改子的帧。
- 子不得比父活得久(§10)。
- 父的"查看"是只读快照,不阻塞子、不参与子的调度决策。
- L4 独占:子的调度器只接受来自父的 L4(§6.1)。
12. 与现有实现的接合点(差距清单)
| 设计项 | 现状 | 要做 |
|---|---|---|
| 两类别 + 四级中断 + 抢占/挂起/恢复/中断栈 | ✅ 已落地(见 input-scheduler-design.md) | 复用 |
| inputch 一等化(归属/容量/授权) | ❌ inputCh 是全局单 channel;ChannelDef 只是策略表 |
新增通道注册与分配层 |
| 输出通道可寻址到 agent | ❌ 只有字符串标签;output_list_channels 列全部 |
通道解析表 + 授权过滤 |
| 子的 L4 = 父 | ⚠️ isKernelLevelSource 只认内置插件 |
泛化为"该 agent 的上级"(分层) |
| 轻量内核(记忆作用域化) | ❌ 记忆层绑定单一 main 空间 | 记忆子系统加 space 维度;AgentConfig 加作用域参数 |
| 记忆写路径重定向到 temp | ❌ 写路径无空间概念 | 所有写入口带 space;子的一切写 → temp |
| 驻留子生命周期 + 登记表 | ❌ 只有一次性 runChildTask |
驻留子 + 父的登记表 + 退出清理 |
| 跨 agent 投递(子→父 L3 / 父→子 L4) | ❌ 无 | 投递原语(复用注入层 + 通道寻址) |
| inputch 处理表 | ❌ 无 | 新数据结构 + 主动写入工具 + 自动写兜底 |
| contextfull 检测 | ❌ 完全没有 | 检测 + L4 通知(带子标识)+ 三处置 |
| 内核不持有"当前通道" | ❌ currentOutputChannel + 提示词预设 |
删字段、删预设 |
| 工作式轻量子 | ✅ | 不动 |
13. 非目标(本文明确不做)
- 跨进程 / 跨主机的驻留子(v1 只在同进程内)。
- 子的子(驻留子再创建驻留子)——先不做,保留扩展位。
- main 空间的多写者(父是唯一写者,不做并发合并)。
- temp 空间的持久化(跟子同生共死,不落盘)。
- 工作式轻量子的任何行为变更。
14. 测试点、方式与预期
| 编号 | 测试点 | 方式 | 预期 |
|---|---|---|---|
| S1 | 创建:划入输入通道 + 授权输出通道 | 创建子,向划入的 inputch 投输入 | 子处理它;未划入的 inputch 投不进(或报错) |
| S2 | 授权收窄 | 创建时只授权部分工具/插件 | 子的工具表恰为该子集;output_list_channels 只列授权的 |
| S3 | 默认完整授权 | 不传授权参数创建 | 子拿到全部插件/工具 |
| S4 | 子→父 L3 | 子在工作中主动发消息 | 父侧收到 L3 中断且带子标识;父可被打断(非临界区时) |
| S5 | contextfull → L4 | 灌满子的上下文 | 父侧收到 L4 中断、带子标识,且只推信号 |
| S6 | 父→子 L4 | 父"发送消息"到指定子 | 子在收到时被打断(L4 > 子内部一切),按 §15 的默认挂起/恢复 |
| S7 | 查看(pull 不打断) | 子在跑长任务时父"查看" | 返回状态面快照;子的 step/帧不变、未被抢占 |
| S8 | 处理表:自动写兜底 | 子一轮不主动写 | 该轮仍有记录(系统自动写) |
| S9 | 处理表:主动写优先 | 子主动写 inputch_note |
该轮只有主动写的内容,无自动写 |
| S10 | 压缩(保留语义) | 父选压缩 | 上下文变短;处理表被清空;子继续存在且能继续干活 |
| S11 | 回收(取消语义) | 父选回收并挑若干条 promote | 选中内容进 main、其余丢弃;temp 被丢弃;子被取消 |
| S12 | 销毁(立刻) | 父销毁(含子正在跑工具/LLM 时) | 子立刻消失、出登记表、其 temp 丢弃、通道释放 |
| S13 | 子不得写 main | 子调记忆写工具 | 落在 temp;main 无新增 |
| S14 | 子查询范围 = temp ∪ main | 子查只在 main 里的内容 / 只在 temp 里的内容 | 两者都能查到 |
| S15 | 子之间 temp 隔离 | 两个子各写 temp,互相查 | 查不到对方的 temp |
| S16 | 父退出清理 | 父 Stop()(多个子、有子在工作中) |
全部子被销毁;登记表空;无孤儿 goroutine / 无悬空通道 / 无残留 temp |
| S17 | L4 独占 | 子内部(子自己的输入/工具/定时器)试图产生 L4 | 被夹到 ≤L3;只有父的消息是 L4 |
| S21 | 一个插件多个 inputch 可分别路由 | 同一插件注册 2 个 inputch,分别划给父与子后各投一条输入 | 各自只到被划给的 agent,互不串台 |
| S22 | inputch 总览(单工具多视图) | 一个插件注册 2 个 inputch、另一插件 1 个;把其中若干划给本 agent | view=all 列出全部(带归属插件);mine/unassigned 各自正确;by_agent 给出划分总览;detail 给出单条全字段;未知 view 报错并列出可用值 |
| S23 | 插件重载不抹划分 | 先划分 inputch,再重复登记(模拟插件重载) | Owner/Capacity 保留,仅策略被更新 |
| S19 | 父消息必能打断子 | 子在长任务中(LLM 流式段)时父发送消息 | 子按 L4 被打断;若子在不可抢占临界区,则在安全点生效 |
| S20 | 子的内核级事件上报 | 子 panic / 子 contextfull | 在**父的阶梯上以 L4(带子标识)**出现;子侧不自己产生 L4 |
| S18 | 内核不持有"当前通道" | 抢占/中断后被打断任务恢复并发响应 | 提示词与事件标签都只来自输入事件(不再有被覆盖的字段) |
15. 待确认决策(含默认取值)
| 编号 | 问题 | 取值 |
|---|---|---|
| R1 | 驻留子的内核形态 | [已定]独立轻量内核(自己的调度器/中断栈/上下文/记忆作用域) |
| R2 | 插件与工具 | [已定]父授权,默认完整授权(可收窄) |
| R3 | 记忆模型 | [已定]两级空间:读 temp∪main,写 temp |
| R13 | inputch 总览的形态 | [已定]单工具多视图(input_channels + view) |
| R11 | 父消息的级别 | [已定]对子 = L4(子的阶梯上唯一 L4 来源;子内部一律 ≤L3) |
| R12 | 子的内核级事件(panic / contextfull)上报级别 | [默认/推论]在父的阶梯上以 L4(带子标识)上报 |
| R4 | 父→子消息落地 | [默认]挂起/恢复(现场不丢);一处开关可改"直接丢弃" |
| R5 | "主动写入处理表"的形态 | [默认]子调用 inputch_note 类工具;未调用则轮末自动写 |
| R6 | 输入通道"划入"的语义与容量 | [默认]读写授权(不转移所有权)+ 创建时给定容量;划入单位 = inputch(不是插件、不是通道组) |
| R7 | 压缩由谁执行 | [默认]子的轻量内核自己压(它有自己的上下文与 LLM),压缩后清表 |
| R8 | 压缩前父是否"查看后决定" | [默认]纯机械压缩(父只在选"压缩 vs 回收"时决策) |
| R9 | 回收时处理表 | [默认]不必清(子都没了);若日后要留作审计需单独策略 |
| R10 | 默认授权是否含输出门 | [默认]含("默认完整授权"的字面含义);若嫌宽,改默认即可 |
16. 实现里程碑(每步 = 一个可独立验收的提交)
| 里程碑 | 内容 | 验收 |
|---|---|---|
| N0 | 无状态化:删 Agent.currentOutputChannel、删提示词里的通道预设 |
S18;既有全部测试通过(这是纯收敛,不含新能力) |
| N1a | 通道登记层:inputch 一等化(归属插件 / 归属 agent / 容量 / 共享登记表)+ 单工具多视图总览 | S21–S23 |
| N1b | 输出通道授权过滤 + 目标解析(outputch → 目标 agent 的 inputch) | S1–S3 |
| N2 | 轻量内核 + 记忆作用域:记忆子系统加 space 维度;AgentConfig 加作用域参数(写 temp、读 temp∪main) |
S13–S15 |
| N3 | 驻留子生命周期:创建 / 销毁 / 登记表 / 父退出清理 | S12、S16 |
| N4 | 跨 agent 投递:子→父 L3、父→子 L4(+ isKernelLevelSource 分层) |
S4、S6、S17 |
| N5 | inputch 处理表:主动写入工具 + 自动写兜底 + 生命周期 | S8、S9 |
| N6 | contextfull:检测 + L4 通知(带子标识)+ 三处置(压缩/回收/销毁) | S5、S10、S11 |
| N7 | e2e + 压力:父子互中断、contextfull 处置矩阵、多子并发、父退出清理 | S1–S18 全绿;-race 干净 |
每步收尾命令:
export GOCACHE=/tmp/gocache GOPATH=/tmp/gopath TMPDIR=/var/tmp/gotmp
gofmt -l internal/agent internal/plugin internal/sdk # 本步新增文件必须为空
go build ./... && go vet ./...
go test -count=1 ./... && go test -race -count=1 ./internal/agent/... ./internal/plugin/...
17. 与发布纪律的关系
- 本设计在
feature/input-semantics之后的特性分支上开发,完成后合回main。 - 若需要动公开 SDK(例如新增
agent_*控制面原语、通道授权字段),按"只增不减、签名不改" 追加,并同步docs/zh/plugin-interface-matrix.md与 SDK 仓版本。