Files
HomeAgent/internal/agent/core/child/kind.go
JianFeeeee a02feaaff7 refactor(agent): 子 agent 类型抽象 + rt 输出异步 + 工具钩子统一
三件事,都是同一个根因的症状:子 agent 的类型逻辑复制成多份,
于是标准路径上的能力没跟着传过来。

## 1. 抽出 child 包(类型抽象 + 单一登记表)

新增 internal/agent/core/child/:
  child.go      Kind / Child(嵌入用)/ State / 残余与通道处置
  registry.go   单一 Registry + Entry 接口 + Cleanup 骨架
  kind.go       ChildKind —— 按两条主轴(上下文 / 工具与输出异步)
  kinds.go      四种子 agent 的清单与迁移状态

此前 Agent 上有两张互不知情的表(residents / rtChildren),各带一把锁。
实测新增一种子 agent 要改 18 个生产文件,并已因复制而漂移过一次
(StopRTChildren 漏注销入站登记,见 engineering-lessons.md §2.3)。

现在单数与复数停止**共用** Cleanup,那个形态在结构上不可能复现。

## 2. rt 的输出改为异步(用户的原始诉求)

rtEmit 曾直接 dev.Execute(...),插件侧延迟(TTS/网络编码/字幕渲染)
直接节流模型的 token 消费 —— 「不攒整段」的收益全被吃掉。

新增 outputpump.go(输出通道的异步壳):
  · Push 立即返回,交付由独立消费 goroutine 完成
  · 有界队列(64),满则**丢最旧**(符合 rt 的"最新者胜")
  · 丢弃与设备缺失都留痕,否则"用户听不到"没有任何线索
  · Close 排空后退出,不吞最后半句
  · 设备每片重新解析(插件崩溃会 Unregister,缓存指针会指向死设备)

## 3. 工具钩子统一(本轮最大发现)

实测:StageBeforeToolcall / StageAfterToolcall 全仓只在
task.go:1022/1079/1194,三处都在 runOneTool / stepToolAfter 里;
而 executeToolCallOutcome 内部**没有任何 runStage**。

⇒ rtchild 与工作式子两条路径**完全跳过插件钩子**:不能审计、
不能拒绝、不能改参、不能脱敏。README 承诺的 before_toolcall
在这两条路径上不成立。

新增 toolbatch.go(唯一批执行实现:批并发 + 双钩子 + 顺序落结果),
两种类型切过去。工作式子的"禁输出工具"与"保持串行"用 batchOpts 注入,
不各写一遍执行逻辑。

## 判据(均经变异验证)

  TestRTChild_OutputIsAsync              改回同步 ⇒ 红(实测确认)
  TestOutputPump_DropsOldestOnFull       改阻塞/无界 ⇒ 红
  TestOutputPump_CloseDrains             丢弃队列 ⇒ 红
  TestOutputPump_ResolvesDeviceEachTime  缓存指针 ⇒ 红
  TestRTChild_ToolsGoThroughPluginStages 绕开钩子 ⇒ 红
  TestRTChild_StageCanDenyTool           同上
  TestWorkerToolsGoThroughPluginStages   同上(变异实测确认)
  TestWorkerToolsStaySerial              改成并发 ⇒ 红
  TestNoChildTypeTablesInCore            源码扫描,禁类型专属表回潮
  TestRTChild_HasGraphMemory 等 5 条     rt 接入图记忆

## 顺带修复

  · rt 接入图记忆(参照驻留子的轻量内核形态):此前 graphMem() == nil,
    memory_recall / memory_commit 在 rt 里全部不可达,每通电话从零开始。
    默认只读(通话高频写入会灌满图谱噪声),4 个失败点合并成一个 rollback
    闭包(逐点挂必然漏一处 ⇒ 泄 SQLite 句柄)。
  · rt 的 PreferredProvider 走候选链(此前裸调 a.provider,优先源无效且零回退)。

## 保留的语义(逐项确认仍在)

  驻留子三层压缩:checkContextFull / handleChildContextFull / CompressResident
  三种通道处置:ChannelRelease / Suspend / Takeover
  残余 keep/drop + harvestResidentMemory
  child.Start() 的 7 个 goroutine

验证:build / vet / gofmt / go test -race 全绿;
且在干净检出(git worktree)上单独 build + test 通过。
2026-11-04 08:21:15 +08:00

96 lines
4.0 KiB
Go
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.

package child
// kind.go 定义 ChildKind —— 一种子 agent 类型的**全部差异点**。
//
// # 设计依据(2026-11-04 与用户对齐)
//
// 用户裁定的差异只有两条:
//
// ① 上下文管理策略
// ② 工具异步调用(含输出通道异步调用)
//
// 实测这条判断是准确的。三种类型的**上下文管理**确实完全不同:
//
// 类型 上下文载体 裁剪方式 溢出处理
// 主 agent RelevanceContext 相关性 topK + 冷上下文下沉 enforceContextBudget
// 驻留子 RelevanceContext(轻量) 同 + 三层压缩协议 enforceContextBudget
// + contextfull 上报
// + 父驱动压缩
// rtchild 跨轮 history 切片 按窗口丢最旧组(只裁视图) 无(视图裁完继续)
// 工作式子 局部 msgs 无(跑完即弃) maxTurns 硬上限
//
// 而"输出通道异步调用"也一样:rt 逐 chunk、其余整段。
//
// ⇒ 所以接口的主轴就是这两条,其余(登记/停止/撤销策略/注销入站)全在骨架里。
import (
agentAPI "github.com/JianFeeeee/HologramAgent/internal/agent/api"
agentIO "github.com/JianFeeeee/HologramAgent/internal/agent/io"
)
// ChildKind 是一种子 agent 类型的完整差异实现。
//
// 实现者放在 core 包里(`*_child.go`),**不放本包**:
// 实现需要载体 Agent 的全部能力(provider / 工具执行 / 记忆 / 安全),
// 把它们抽象成接口会造出一个比 Agent 还大的假接口 —— 那是把复杂度
// 换个地方放,不是消除。本包只提供接口、骨架与登记表。
type ChildKind interface {
// Kind 返回类型标签(与 kinds.go 的常量一致)。
Kind() Kind
// ── ① 输入策略 ──
// Deliver 投递一条输入。
//
// 队列型(驻留子)返回 false 表示"交回默认路径"(DeliverRouted 进调度器)。
// 单槽型(rt)自行覆盖并返回 true。
Deliver(e Entry, evt *agentIO.InputEvent, isInterrupt bool) bool
// ── ② 上下文管理策略(本接口的主轴之一)──
// BuildContext 装配本轮送入模型的上下文视图。
//
// 必须遵守"视图与存储分离":返回的切片可以是裁剪后的视图,
// 但**不得**修改本类型的长期存储(rt 的 history 永不裁剪)。
BuildContext(e Entry, input string) []agentAPI.Message
// AppendHistory 把一轮产出写回长期存储。
// 队列型(驻留子)用 RelevanceContext 承载,不需要它 ⇒ no-op。
AppendHistory(e Entry, msgs ...agentAPI.Message)
// Compact 按本类型的上下文策略压缩。返回被处理的事件数。
//
// 只有驻留子实现了真正的压缩(摘要替换 + 失败回退截断)。
// 不支持的类型返回 (0, ErrCompactUnsupported)。
Compact(e Entry, keepRecent int) (int, error)
// ── ③ 工具与输出异步调用(主轴之二)──
// Emit 把一段增量文本送到输出通道。
//
// 异步批大小由实现决定:rt 逐 chunk、其余整段。**实现必须异步**,
// 插件延迟不得节流调用方的生成循环。
Emit(e Entry, text string)
// RunTools 执行一批工具,返回与入参**同序**的结果。
//
// 必须让整批经过 before_toolcall / after_toolcall 插件钩子。
RunTools(e Entry, calls []agentAPI.ToolCall) []ToolResult
// ── ④ 销毁语义 ──
// Dispose 按本类型语义收尾。policy 只对支持残余处置的类型有意义。
Dispose(e Entry, policy ResidualPolicy) string
// ── ⑤ 工具面 ──
// ParentTools 声明该类型暴露给**父模型**的控制工具。
//
// 定义与派发都从这一份派生 —— 这是消灭 toolcall.go 与 toolroute
// 两份工具表不同步的落点。
ParentTools() []ToolSpec
}
// ErrCompactUnsupported 表示该类型不支持压缩。
var ErrCompactUnsupported = "该类型不支持上下文压缩"