Commit Graph

10 Commits

Author SHA1 Message Date
2ebbdadcd6 feat(resident): N3–N7 驻留式子 agent 全量落地(生命周期/双向投递/处理表/contextfull/e2e+压力)
设计:docs/zh/resident-subagent-design.md §6/§7/§8/§9/§10。

## N3 生命周期(resident.go)

- `SpawnResident`:主库**受限句柄** + 自己的 temp 实例(`LightMemory`)⇒ 子的轻量内核;
  划入 inputch(登记归属)、授权输出通道、注入任务提示词;为父登记 `child/<id>` 入站 inputch;
  建独立 `IOManager`(共享通道登记表);启动子。
- `DestroyResident`:停子内核、归还划入的 inputch(回到未分配)、丢弃 temp 目录、出登记表。
- `Stop()` → `StopResidents()`:**父退出必须销毁全部子、不留孤儿**(设计 §10 硬约束)。
- `Residents()` 登记表快照;`ResidentTable(id)` 父 pull 子的处理表(不打断)。

## N4 跨 agent 投递

- 子→父:`notify_parent` → 投进父的 `child/<id>` inputch,优先级 **L3**。
- 父→子:`SendToResident` → 投进子的 inputch,优先级 **L4**;
  `isKernelLevelSource` 泛化为"该 agent 的上级"(`AgentConfig.KernelSource`)⇒
  只有父能在子的阶梯上产生 L4(子内部一律 ≤L3)。
- 子的 contextfull → 父侧 `raiseKernelInterrupt`(内核级事件,带子标识,父侧 L4)。

## N5 inputch 处理表

- 子持有;`inputch_note` 主动写**优先**,轮末 `autoRecordInputch` 兜底 ⇒ 每轮必有记录。
- 压缩时清表(表记的是被压掉那段窗口的逐轮处理)。

## N6 contextfull(判据修正)

初版判据是"拼好的 `f.Msgs` 估算 > 90% 窗口",**结构上永不成立**:
`buildMessages` 拿到的 `budget.ContextTokens` 由 `targetUsage = 0.8 × 窗口` 推出,
时间线**在拼进消息之前就被预算裁过**,`f.Msgs` 封顶在 ~80% 窗口。
(初版测试用一个比系统提示词还小的窗口才勉强越线 —— 那等于什么都没测。)
现判据 = **未裁剪的积累上下文**(`a.context.Recent(0)`)超过窗口 90%:
它超过就说明下一轮必须丢事件,这正是"上下文满"。

三处置:`CompressResident`(保留语义:`TrimKeepRecent` 保留最近 N 条 + 清表)/
`ReclaimResident`(取消语义:`ExportTriples` 读 temp → 父选出要保留的 → `Commit` 进 main → 取消该子)/
`DestroyResident`(立刻销毁并移除)。

## 工具面

`resident_agents`(父,单工具多动作:list/create/send/inspect/compress/reclaim/destroy)、
`notify_parent` + `inputch_note`(子)。声明条件式:父才有前者,子才有后两者。

## 验收

`resident_test.go` 五项:生命周期与不留孤儿、双向投递(含"子的主动消息不得以 L4 出现")、
处理表(自动写 vs 主动写优先)、contextfull + 三处置、
**压力 8 子 × 12 轮(父→子 L4 与普通输入各半)+ 双向汇报 + 父退出清理**。
全仓 go test ./... 37 包 ok / 0 FAIL;`-race`(agent/memory/plugin)干净;
压力 `-race -count=3` 通过。
2026-09-13 10:20:06 +08:00
48cfa8fb6c feat(lightkernel): N2c —— 轻量内核 profile(窄接口 GraphMemory + nil 即禁用整理面)
按用户指出的关键点(a.memory 多数使用点属"主 agent 整理记忆"与"记忆整理流水线",
子不该有那些路径)实现,方案见设计 §16.0。

## 窄接口:只有 Recall + Commit

新增 `GraphMemory` 接口(memoryface.go)—— 按调用方实测分类后,真正"根与子都要"的只有这两个:
- `Recall`:上下文检索 / memory_recall
- `Commit`:自动写入路径的图部分 / memory_commit

新增 `Agent.graph`(共同面)与 `Agent.graphMem()` 访问器:
- `graph` 显式为 nil 时**回落**到 `memory` ⇒ 既有"只用 Agent 字面量设 memory"的测试无需改动
  (原本会出现"必须同时设两个字段"的脚坑,实测踩到后去掉了)
- 轻量内核:`graph = *memory.LightMemory`,`memory = nil`

## nil 即禁用:整理面自动消失,不需要受限包装

子的 `a.memory == nil` ⇒ 既有的 22 处 `if a.memory != nil` 关卡自动禁掉全部整理面:
- 记忆整理流水线(distill.go 的 archive/review/merge 循环)
- 记忆块 + 媒体桥(graphmedia.go / medialoop.go)
- 记忆整理工具(memory_merge / memory_delete_entity / memory_block_merge /
  memory_purge / memory_edit / memory_introspect)—— 它们本就在 `if a.memory != nil` 块内

唯一拆开的一处是自动写入 `commitTriplesWithMedia`:
图部分走 `graphMem().Commit`(父落 main、子落 temp),块/媒体部分仍由 `a.memory != nil` 守卫。
`executeMemoryTool` 的读路径改走 `graphMem().Recall`;整理类 case 加 `requireFull()` 闸门,
被直调时明确报"本 agent 是轻量内核:记忆整理不可用",不静默降级。

## 验收(3 项新测试)

- `TestLightProfile_MemoryFaceWiring`:整理面为 nil、写入只落 temp(主库无子痕迹)、
  读是并集(主库实体 + temp 实体都看得到)
- `TestLightProfile_OrganizeToolsAbsentAndRefused`:整理类工具**不进工具表**;
  即便被直调也明确报"轻量内核不支持"
- `TestFullProfile_KeepsOrganizeFace`:对照,根 agent 仍保留整理面与整理工具

全仓 go test ./... 37 包 ok / 0 FAIL;-race(agent/memory)干净;gofmt 干净。
2026-09-13 10:02:02 +08:00
c5b1242980 docs(resident-subagent): 更正 N2c 方案 —— 窄接口(只剩 Recall/Commit)+ nil 即禁用
用户指出:a.memory 的多数使用点属于**主 agent 整理记忆**与**记忆整理流水线**,子根本不该有那些
代码路径。据此把上一版"约 20 方法的接口 + 受限包装"改成**实测分类 + 窄接口**。

按调用方实测分类(42 处):
- A 记忆整理流水线(distill.go 10 处:archive/review/merge 循环)→ root-only
- B 记忆块 + 媒体桥(graphmedia.go 18 + medialoop.go 4)→ root-only
- C 记忆整理工具(executeMemoryTool 8 处:merge/delete/block_merge/purge/edit/stats)→ root-only
- D 共同面:**只有 Recall + Commit**(自动写入路径 + memory_recall 工具)
- E 22 处 if a.memory != nil 既有关卡 + 状态/工具表判空

更正后的方案:
- `GraphMemory` 接口**只含 Recall + Commit**
- 根:a.graph = a.memory = 同一个 *GraphDB
- 子:a.graph = *LightMemory,**a.memory = nil** ⇒ 既有 22 处 nil 关卡自动禁掉全部 root-only 路径
  (executeMemoryTool 开头本来就是 `if a.memory == nil { return "图记忆系统不可用" }`)
- 唯一要拆的:自动写入 commitTriplesWithMedia(图部分走 a.graph.Commit;块/媒体部分用 a.memory != nil 守卫)
- 整理类工具**不进子的工具表**(而不是进去再报不可用)

⇒ 不必写"18 个方法都返回错误"的受限包装 —— 子压根没有那些路径。
2026-09-13 09:42:43 +08:00
818ce2698f feat(memory): N2a 第一块砖 —— 主图记忆的受限句柄(query_only),子是"读得到写不进"
按用户确认的形态:**独立存储实例**(不是给共享记忆层加 space 列)。

## 结构性保证

新增 `OpenGraphDBReadOnly(path)`:以**受限句柄**打开图库 ——
连接保持正常打开能力(可读、可恢复 WAL),但 `PRAGMA query_only=1` 让
任何 INSERT/UPDATE/DELETE 被 SQLite **直接拒绝**。

为什么不用 DSN 的 `mode=ro`:只读连接在 WAL 库上无法自行恢复 -wal,
而主库在父 agent 手里是持续写入的。query_only 只堵写、不堵读,语义正好。

⇒ "子改不了主记忆"是**结构性**的,不靠调用方自觉;也不建表、不迁移
(库由父建好,受限句柄不会凭空造出一个空主库)。

## 设计文档

新增 §5.6「实现形态:独立存储实例(不做 space 列)」,写明轻量内核的记忆装配:

    子的轻量内核
    ├─ temp 图记忆实例(独立存储,读写)  ← 与子同生共死
    └─ 主图记忆的受限句柄(只读)
    子的查询 = 两个实例各查一次 + 应用层合并(并集)
    回收时由父读 temp、选记录、写进主图记忆

并记录用户给的备选简化:`AllowTempGraphWrite` 开关(默认开);设为 false 时
子对图记忆完全只读,没有 temp 实例、没有合入。

## 验收

`internal/memory/graph_readonly_test.go`(2 项):
- 受限句柄读得到、写被拒(Commit/Purge 双双报错),且**主库不留痕迹**
- 库不存在时受限句柄的查询报错,而不是凭空建表后返回空结果

全仓 go test ./... 37 包 ok / 0 FAIL。
2026-09-13 09:32:29 +08:00
36e7556a02 docs(resident-subagent): 子的记忆面收窄为「传统上下文 + 图记忆」
用户澄清:**doc 记忆**与 **context 动态上下文**是内核独立设计的记忆能力(父专属),
子 agent 的记忆面只有 **图记忆**。据此更正设计:

- §5 开头加适用范围界定:两级空间(读 temp∪main / 写 temp)**只针对图记忆**
- 新增 §5.5「子的记忆面」对照表:
    | 能力 | 根 | 驻留子 |
    | 图记忆(含向量检索) |  main 读写 |  作用域化 |
    | doc 记忆 / context 动态上下文 / 蒸馏 / 归档 / consolidation / 文本 / 知识库 / 媒体 / 社交 |  |  父专属 |
- 修正「轻量内核」的表述:不是"记忆变轻",而是**记忆面裁到只剩图记忆 + 图记忆被作用域化**
- §16.1 记忆面清单加「子可用?」列;**v1 只给图记忆(含其向量检索)加 space 维度**
  —— 其余面子根本够不到,加 space 是白工
- 里程碑:N2a = 图记忆 space 维度;N2b = 图记忆的向量检索接入同一过滤
- 决策表补 R3b

纯文档更正;全仓 go test ./... 37 包 ok。
2026-09-13 09:24:09 +08:00
069552e921 docs(resident-subagent): 把 N2 拆成 N2a–N2d(记忆作用域逐面铺开)+ 记忆面清单
N2(轻量内核 + 记忆作用域)是目前最大的一块:记忆子系统有 7 个面
(图记忆/向量索引/文档/知识库/文本/媒体/社交),每个面都要加 space 维度。

拆成可独立验收的四步:
- N2a 作用域对象 + **图记忆** space 维度(先做这一条纵切)
- N2b 其余记忆面加 space(逐面验收)
- N2c Agent 级作用域接线(根 = {main,[main]};驻留子 = {sub/<id>,[sub/<id>,main]})
- N2d 晋升与丢弃(回收时父把选中的 temp promote 进 main;销毁/回收丢弃 temp)

文档新增 §16.1 记忆面清单(面 → 载体 → 表),说明为何先做图记忆这条纵切。
2026-09-13 09:15:58 +08:00
f7c3a4e81d feat(channel): N1b —— 输出通道授权集合(三处过滤一致)+ 输出通道→目标 agent 的 inputch 解析
设计: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。
2026-09-13 09:13:19 +08:00
3c961b7bd7 feat(channel): N1a —— inputch 一等化(归属插件/归属 agent/容量/共享登记表)+ 单工具多视图总览
设计:docs/zh/resident-subagent-design.md §4.6(里程碑 N1a)。

## 用户要求

「父 agent 可以看到所有已注册的 inputch 以及 inputch 的划分情况,用**单工具多视图**方式构筑」

## 登记层(internal/agent/io/inputch.go)

inputch 是**最基本的输入路由单位**(由插件注册,一个插件可注册多个),
所以登记表以 inputch 为键,每条记录:

    名字 / 归属插件 / 归属 agent(被划给谁) / 容量 / 默认回程 outputch / 记忆策略(ChannelDef)

- 新增 `ChannelRegistry`,设计成**可共享对象**(`*ChannelRegistry`):
  根 agent 与驻留子共用同一份,"划入/授权"才有意义;`SetChannelRegistry` 注入。
- **插件重载不得抹掉划分**:重复登记只更新「归属插件 + 策略」,
  保留已有 Owner/Capacity/Output(否则一次 reload 就把父做的划分清空)。
- `Assign` 语义按设计 R6 默认:**读写授权,不转移所有权**(Plugin 与 Owner 分别记录)。
- 原 `inputChannels map[string]ChannelDef` 被登记表取代;`GetInputChannelDef` 保持兼容。
- `plugin.Registry` 注册时带上**归属插件名**(此前完全无归属信息)。

## 总览工具(internal/agent/core/inputch.go)

单工具 **`input_channels`** + `view` 参数(不是一堆小工具):

    all(默认)= 全部已注册(带归属插件)
    mine       = 划给本 agent 的
    unassigned = 尚未划出的
    by_agent   = 划分情况总览(按归属分组)
    detail     = 单个 inputch 全字段(需 name)

未知 view **报错并列出可用值**(拼错不得被静默当成默认视图);登记表为空时明确说明。

## 验收

- `internal/agent/io/inputch_test.go`:归属记录、重载保划分、Assign/视图数据面、
  **跨 manager 共享登记表**、策略查询向后兼容 —— 5 项
- `internal/agent/core/inputch_test.go`:单工具多视图逐视图断言(含未知 view 与空表)—— 2 项
- 全仓 `go test ./...` 37 包 ok / 0 FAIL;`-race ./internal/agent/... ./internal/plugin/...` 干净
2026-09-13 09:06:50 +08:00
3956610134 docs(resident-subagent): inputch 是最基本的输入路由单位(插件可注册多个)
按用户补充收窄定义:
- inputch 是**最基本的输入路由单位** —— 路由粒度到此为止(比「插件」细、比「通道名字符串」实)
- **由插件注册,且一个插件可注册多个**(登记接口即现有 RegisterInputChannel,
  调 N 次就是 N 个 inputch;同一插件的多个 inputch 彼此独立,可绑给不同 agent、可分别限额)
- 「划入输入通道」的单位 = **inputch**(不是插件、不是通道组)
- 通道注册层以 inputch 为键(一个插件 → N 个 inputch),每个 inputch 带
  归属/被划给的 agent · 容量 · 可接收类别 · 输出目标解析

新增测试点 S21(同一插件两个 inputch 分别划给父与子,互不串台)。
2026-09-13 08:40:04 +08:00
fc41e160db docs(resident-subagent): 驻留式子 agent 设计稿(轻量内核 · 两级记忆 · 父子中断)
把用户口述的设计固化成文档,与已落地的 input-scheduler-design.md 配套。

定下来的核心:
- inputch = 对「中断输入/排队输入」两者的高层抽象,是**路由与分配**单位
  (路由发生在进内核之前;类别与级别是每条输入的属性,不是 inputch 的属性)
- 输出是 agent 的**主动调用**(output_send__{通道});内核不持「当前通道」可变状态、
  提示词不预设 outputch
- 通道不配对:父**划入若干输入通道** + 授权**一组可用输出通道**;输出可寻址到具体 agent
- 驻留子 = **独立轻量内核**:读 temp∪main、写 temp(记忆层按 agent 作用域化 ——
  这才是「轻量」的真正理由,不是砍功能)
- 两条独立中断阶梯:父侧 子的主动消息=L3 / contextfull=L4;子侧 **父的消息=L4**
  (L4 通则:某 agent 的 L4 只属于它的内核 ⇒ isKernelLevelSource 泛化为「该 agent 的上级」;
  子内部一切来源被夹到 ≤L3,所以父的消息确定能打断子)
- 父对子 6 个动作:创建/发送消息/查看/压缩/回收/销毁(原语在内核、决策在父的模型);
  插件与工具由父授权,**默认完整授权**
- contextfull 三处置:压缩=保留(清处理表)/ 回收=取消(promote 选中的 temp 进 main)/
  销毁=立刻销毁并移除
- inputch 处理表:子持有、父 pull;主动写入优先,否则系统自动写;生命周期 = 当前上下文窗口
- 登记表 + 硬约束:父可随时销毁,父退出必须销毁全部,子不得比父活得久
- 工作式轻量子 agent **原样保留**,两类并存

含 20 个测试点(S1–S20)与 8 个里程碑(N0–N7,每步独立可验收)、
12 条待确认决策(全部带可逆默认值)。
2026-09-13 08:39:40 +08:00