Files
HomeAgent/internal/agent/core/child/child.go
JianFeeeee 637a3a3d3f refactor(agent): R4 工作式子语义落地 + 清除 R1–R3 遗留的死代码
## 1. 工作式子的工具禁令收成单一事实来源

禁令此前写了两遍:定义侧(把工具从 childTools 滤掉)与执行侧
(模型硬调时拒绝)。两处名单必须逐字一致,任何一边落后就是真实缺口:

  定义侧漏滤 ⇒ 模型看得见但不该能用(白耗 token,还会反复试)
  执行侧漏拒 ⇒ 模型绕过定义直接调用**就能生效**

现在只有 workerForbidden 一张表,每条禁令把"匹配方式 + 类别 + 解释"
绑在一个结构体里;定义侧 workerToolAllowed、执行侧 workerToolReject
都从它派生。

上一版我用 reason 字符串 + 下标猜分类 + hint 查表做了三处间接,
属于新造的技术债,已重写为单表直查。

判据(均经变异验证):
  TestWorkerToolDenyListIsSingleSource  长出第二份名单 ⇒ 红(实测确认)
  TestWorkerToolAllowedMatchesReject    两侧判据漂移 ⇒ 红
  TestWorkerToolRejectExplainsWhy       拒绝文案不可执行 ⇒ 红

## 2. 删除零使用的抽象(不留技术债)

R1–R3 引入了 ChildKind / Host / Deliverer / DeviceResolver / ToolSpec /
ToolResult / DeclaredKinds / KindWorker / State / SetState / BumpRound /
ChannelDisposition,实现过程中发现它们是假抽象:

  · 各类型要用载体 Agent 的大半能力,要满足接口就得在实现里类型断言回 *Agent
  · 接口比被抽象的东西还大 ⇒ 只是把复杂度换个地方放
  · 实测:26 个导出符号里 20 个零使用、零实现

已删除 host.go 与 kind.go,并把"四种类型清单"和"转投驻留子为何不是类型"
这两个有价值的结论移进 kinds.go 的文档注释(知识保留,死代码删除)。

child 包现在只剩实际被用的 9 个符号:Kind / Child / Entry / Registry /
StopDeps / ErrChildExists / ErrChildNotFound(+ 2 个 Kind 常量)。

## 3. 修掉误导性签名

Entry.Dispose(policy ResidualPolicy) 里 policy **两个实现都不读** ——
残余处置实际走 ApplyResidual / FinishResidentWith,在 Dispose 之前就
已经完成。看签名会以为这里处理残余,是误导。已去掉该参数,
并删掉 child 包里重复定义的 ResidualPolicy(core 包已有一份,且多一个 stash)。

## 验证

  go build ./...                                  PASS
  go vet ./...                                    PASS
  gofmt -l                                        干净
  go test ./internal/agent/core/ -count=1         ok 15.300s
  go test ./internal/agent/core/ -count=1 -race   ok 18.040s
  干净检出(git worktree)单独 build + vet + test  PASS
2026-11-04 08:35:23 +08:00

136 lines
4.7 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
// child.go 是子 agent 的**公共部分**与类型标签。
//
// # 这个包只做一件事
//
// 把所有类型的子 agent 共有的、**最容易"改一处漏一处"**的生命周期收成一处:
//
// 单一登记表(Registry) ← 替换掉 residents / rtChildren 两张表
// 单一清理路径(Cleanup) ← 单数与复数共用,防"复数漏一步"
// 公共字段(Child:id/ctx/IO/入站通道)
//
// # 为什么这里没有 ChildKind 接口
//
// 曾设计过 `ChildKind`(Deliver / BuildContext / Emit / RunTools / Dispose /
// ParentTools),但在实现过程中发现它是**假抽象**:
//
// · 各类型要用载体 Agent 的大半能力(provider / 工具执行 / 记忆 / 安全)
// · 要满足它就得以 `Entry` 为参数、在实现里做类型断言回 `*Agent`
// · 接口比被抽象的东西还大 ⇒ 只是把复杂度换个地方放
//
// 实测结果是它零实现、零调用(26 个导出符号里 20 个无人使用)。
// 按"不留技术债"的要求,接口与其配套的 Host / ToolSpec / ToolResult 已删除,
// 类型差异直接写在各自的文件里(rtchild.go / resident.go / spawn.go)。
//
// # 已确认的边界(不要为了"更统一"而抹掉)
//
// - 驻留子的 `DestroyResident` 有三种 ChannelDisposition(释放/挂起/接回)
// - rtchild 的 rt 通道**销毁后悬空**,不归还
// - 驻留子 `Start()` 起 7 个 goroutine;rtchild **只起 1 个** rtLoop
// - 工作式子**不进**登记表(无 io/通道,结果缓存与存活登记语义不同)
import (
"context"
"errors"
"sync"
agentIO "github.com/JianFeeeee/HologramAgent/internal/agent/io"
)
// Kind 是子 agent 的类型标签。
//
// 用字符串而不是 iota:它要出现在状态面与日志里,字符串直接可读,
// 且新增类型不需要改一个中心 switch。
type Kind string
const (
// KindResident 是长期派驻的子 agent(任务队列、残余可回灌、不丢活)。
KindResident Kind = "resident"
// KindRT 是实时通话子 agent(单槽覆盖、最新者胜、可饿死、丢现场)。
KindRT Kind = "rt"
)
// Child 是一个**已建立**的子 agent 实例的公共部分。
//
// ★ 用法是**嵌入**:类型专属状态放在各实现自己的结构体里,
// 公共部分靠嵌入提升字段:
//
// type rtChild struct {
// *child.Child // ID / Kind / Ctx / IO / InboundCh / stopped
// agent *Agent // 载体(类型专属,不进公共结构)
// slot *rtSlot
// }
//
// 为什么不把 agent *Agent 放进本结构:本包不能 import core(否则成环),
// 而 core 可以 import 本包。嵌入让公共部分与载体指针各归各位。
type Child struct {
// ID 是登记表的键,也是通道 Owner 记的值。
//
// ★ 必须与载体 Agent 的 ID **逐字相同**:AssignRealtime 会把通道 Owner
// 记成 carrier.id,而路由按 entry.Owner 在本表里查 —— 两处各拼一次名字
// 就会永远查不到(曾因写成 "rt/"+id 而让插件输入全打到父身上)。
ID string
// Kind 是本实例的类型标签(Add 时由 Entry.Kind() 写入)。
Kind Kind
// IO 是本子自己的 IOManager(独立输入入口 + 共享通道登记表 + 父视图回退)。
IO *agentIO.IOManager
// Ctx / Cancel 是本子**自己的**取消域。
//
// 必须是 WithCancel(parentCtx) 派生:父停则子停(不留孤儿),
// 但单独 cancel 只影响本子,不波及父与兄弟。
Ctx context.Context
Cancel context.CancelFunc
// InboundCh 是本子在父侧注册的入站通道名("child/<id>"),
// Cleanup 负责注销它。空 = 本类型不需要入站通道。
InboundCh string
// ParentID 是父的 agent id(日志用)。
ParentID string
// CreatedAt 是 Unix 纳秒,供状态面排序。
CreatedAt int64
mu sync.Mutex
stopped bool
}
// Stop 原子地标记停止并取消本子的 ctx。可重复调用(幂等)。
func (c *Child) Stop() {
if c == nil {
return
}
c.mu.Lock()
c.stopped = true
c.mu.Unlock()
if c.Cancel != nil {
c.Cancel()
}
}
// Stopped 报告是否已被 Stop。
func (c *Child) Stopped() bool {
if c == nil {
return true
}
c.mu.Lock()
defer c.mu.Unlock()
return c.stopped
}
// ── 错误 ──
var (
// ErrChildExists 表示同名子已存在。
//
// 为什么不允许"同名覆盖":登记表与通道 Owner 都以 id 为键,
// 覆盖会让上一代的残留(通道归属、策略、留档)被当成这一代的。
ErrChildExists = errors.New("同名子 agent 已存在")
// ErrChildNotFound 表示 id 未登记。
ErrChildNotFound = errors.New("子 agent 不存在")
)