mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 17:27:31 +00:00
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
This commit is contained in:
@ -1,44 +1,34 @@
|
||||
// Package child 是**子 agent 的类型抽象层**。
|
||||
package child
|
||||
|
||||
// child.go 是子 agent 的**公共部分**与类型标签。
|
||||
//
|
||||
// # 为什么要有这个包(用户裁定 2026-11-04)
|
||||
// # 这个包只做一件事
|
||||
//
|
||||
// 此前每新增一种子 agent,都要往 `internal/agent/core` 的同一个文件里追加代码:
|
||||
// 把所有类型的子 agent 共有的、**最容易"改一处漏一处"**的生命周期收成一处:
|
||||
//
|
||||
// agent.go 加一组类型专属字段(锁 + 表 + 专属状态),约 20 行/类型
|
||||
// inputroute.go 加一段"查我这张表 → 分流"的分支
|
||||
// toolcall.go 加若干 `case tc.Name == "<该类型的控制工具>":`
|
||||
// 以及独立的 <type>.go 放整份生命周期实现
|
||||
// 单一登记表(Registry) ← 替换掉 residents / rtChildren 两张表
|
||||
// 单一清理路径(Cleanup) ← 单数与复数共用,防"复数漏一步"
|
||||
// 公共字段(Child:id/ctx/IO/入站通道)
|
||||
//
|
||||
// 实测:新增一种子 agent 需要改 **18 个生产文件**。已经因此漂移过一次
|
||||
// (`StopRTChildren` 漏注销入站登记,见 docs/zh/engineering-lessons.md §2.3)。
|
||||
// # 为什么这里没有 ChildKind 接口
|
||||
//
|
||||
// # 抽象的形状
|
||||
// 曾设计过 `ChildKind`(Deliver / BuildContext / Emit / RunTools / Dispose /
|
||||
// ParentTools),但在实现过程中发现它是**假抽象**:
|
||||
//
|
||||
// 分成两半:
|
||||
// · 各类型要用载体 Agent 的大半能力(provider / 工具执行 / 记忆 / 安全)
|
||||
// · 要满足它就得以 `Entry` 为参数、在实现里做类型断言回 `*Agent`
|
||||
// · 接口比被抽象的东西还大 ⇒ 只是把复杂度换个地方放
|
||||
//
|
||||
// **骨架(本包提供,只写一遍)**
|
||||
// spawnChild 校验 → 去重 → 独立 ctx → 独立 IO(共享通道登记表 + 父视图回退)
|
||||
// → 构造载体 → 绑定 → 声明子策略 → 登记 → 启动
|
||||
// stopChild 撤销策略 → 注销入站登记 → 取消 ctx → kind.Dispose
|
||||
// stopAll 对全部子执行 stopChild
|
||||
//
|
||||
// **差异(由 ChildKind 实现,每种类型一份)**
|
||||
// Deliver 输入策略:队列(不丢活)还是单槽(最新者胜)
|
||||
// BuildContext 上下文策略:相关性裁剪 还是 跨轮 history + 窗口
|
||||
// Emit 输出策略(含异步批大小)
|
||||
// RunTools 工具策略(并发批 还是 串行)
|
||||
// Dispose 销毁语义(残余 keep/drop;通道释放还是悬空)
|
||||
// ParentTools 暴露给父的控制工具
|
||||
// 实测结果是它零实现、零调用(26 个导出符号里 20 个无人使用)。
|
||||
// 按"不留技术债"的要求,接口与其配套的 Host / ToolSpec / ToolResult 已删除,
|
||||
// 类型差异直接写在各自的文件里(rtchild.go / resident.go / spawn.go)。
|
||||
//
|
||||
// # 已确认的边界(不要为了"更统一"而抹掉)
|
||||
//
|
||||
// - 驻留子的 `DestroyResident` 有三种 ChannelDisposition(释放/保留/归还)
|
||||
// - rtchild 的 rt 通道**销毁后悬空**,不归还(用户裁定,见 rt 的 Dispose 实现)
|
||||
// - 驻留子 `child.Start()` 起 7 个 goroutine;rtchild **只起 1 个** rtLoop
|
||||
// (rt 明确不要 schedulerLoop / interceptLoop / 记忆整理循环)
|
||||
//
|
||||
// 这三条是**语义差异**,属于 ChildKind 的职责,不属于骨架。
|
||||
package child
|
||||
// - 驻留子的 `DestroyResident` 有三种 ChannelDisposition(释放/挂起/接回)
|
||||
// - rtchild 的 rt 通道**销毁后悬空**,不归还
|
||||
// - 驻留子 `Start()` 起 7 个 goroutine;rtchild **只起 1 个** rtLoop
|
||||
// - 工作式子**不进**登记表(无 io/通道,结果缓存与存活登记语义不同)
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -50,32 +40,30 @@ import (
|
||||
|
||||
// Kind 是子 agent 的类型标签。
|
||||
//
|
||||
// 为什么用字符串而不是 iota 枚举:它要出现在状态面、日志、以及
|
||||
// ChannelRegistry 的归属判定里;字符串在日志里直接可读,
|
||||
// 且新增类型不需要改一个中心 switch(这与包的目的正好一致)。
|
||||
// 用字符串而不是 iota:它要出现在状态面与日志里,字符串直接可读,
|
||||
// 且新增类型不需要改一个中心 switch。
|
||||
type Kind string
|
||||
|
||||
const (
|
||||
// KindResident 是长期派驻的子 agent(有任务队列、有残余可回灌、不丢活)。
|
||||
// KindResident 是长期派驻的子 agent(任务队列、残余可回灌、不丢活)。
|
||||
KindResident Kind = "resident"
|
||||
// KindRT 是实时通话子 agent(单槽覆盖、最新者胜、可饿死、丢现场)。
|
||||
KindRT Kind = "rt"
|
||||
)
|
||||
|
||||
// Child 是一个**已建立**的子 agent 实例的**公共部分**。
|
||||
// Child 是一个**已建立**的子 agent 实例的公共部分。
|
||||
//
|
||||
// ★ 用法是**嵌入**:类型专属状态放在各实现自己的结构体里,
|
||||
// 公共部分靠嵌入提升字段:
|
||||
//
|
||||
// type rtChild struct {
|
||||
// *child.Child // ID / Kind / Ctx / IO / InboundCh / 状态 / 轮次
|
||||
// *child.Child // ID / Kind / Ctx / IO / InboundCh / stopped
|
||||
// agent *Agent // 载体(类型专属,不进公共结构)
|
||||
// slot *rtSlot
|
||||
// }
|
||||
//
|
||||
// 为什么不把 agent *Agent 放进本结构:本包不能 import core(否则成环),
|
||||
// 而 core 可以 import 本包。嵌入让公共部分与载体指针各归各位,
|
||||
// 同时避免为此造一个“只有 ID 的假 Agent” —— 那种结构既不能用又会误导后来者。
|
||||
// 而 core 可以 import 本包。嵌入让公共部分与载体指针各归各位。
|
||||
type Child struct {
|
||||
// ID 是登记表的键,也是通道 Owner 记的值。
|
||||
//
|
||||
@ -84,7 +72,7 @@ type Child struct {
|
||||
// 就会永远查不到(曾因写成 "rt/"+id 而让插件输入全打到父身上)。
|
||||
ID string
|
||||
|
||||
// Kind 是本实例的类型标签。
|
||||
// Kind 是本实例的类型标签(Add 时由 Entry.Kind() 写入)。
|
||||
Kind Kind
|
||||
|
||||
// IO 是本子自己的 IOManager(独立输入入口 + 共享通道登记表 + 父视图回退)。
|
||||
@ -98,20 +86,16 @@ type Child struct {
|
||||
Cancel context.CancelFunc
|
||||
|
||||
// InboundCh 是本子在父侧注册的入站通道名("child/<id>"),
|
||||
// stopChild 负责注销它。空 = 本类型不需要入站通道。
|
||||
// Cleanup 负责注销它。空 = 本类型不需要入站通道。
|
||||
InboundCh string
|
||||
|
||||
// ParentID 是父的 agent id(策略撤销、日志用)。
|
||||
// ParentID 是父的 agent id(日志用)。
|
||||
ParentID string
|
||||
|
||||
// CreatedAt 是 Unix 纳秒,供状态面排序。
|
||||
CreatedAt int64
|
||||
|
||||
mu sync.Mutex
|
||||
state State
|
||||
rounds uint64
|
||||
// stopped 标记已 Stop。rtLoop 取到输入后先查它,防止
|
||||
// “Wake 信号先于 cancel 到达 select”时白跑一轮(D8)。
|
||||
mu sync.Mutex
|
||||
stopped bool
|
||||
}
|
||||
|
||||
@ -122,7 +106,6 @@ func (c *Child) Stop() {
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.stopped = true
|
||||
c.state = StateStopped
|
||||
c.mu.Unlock()
|
||||
if c.Cancel != nil {
|
||||
c.Cancel()
|
||||
@ -139,61 +122,6 @@ func (c *Child) Stopped() bool {
|
||||
return c.stopped
|
||||
}
|
||||
|
||||
// State 是子 agent 的运行态。
|
||||
type State string
|
||||
|
||||
const (
|
||||
StateRunning State = "running"
|
||||
StateContextFull State = "contextfull"
|
||||
StateStopped State = "stopped"
|
||||
)
|
||||
|
||||
// String 让状态能直接进日志与状态面。
|
||||
func (s State) String() string { return string(s) }
|
||||
|
||||
// SetState 设置运行态(并发安全)。
|
||||
func (c *Child) SetState(s State) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.state = s
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// State 返回当前运行态。
|
||||
func (c *Child) State() State {
|
||||
if c == nil {
|
||||
return StateStopped
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.state == "" {
|
||||
return StateRunning
|
||||
}
|
||||
return c.state
|
||||
}
|
||||
|
||||
// BumpRound 累加轮次(可观测性用)。
|
||||
func (c *Child) BumpRound() {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.rounds++
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// Rounds 返回已跑轮次。
|
||||
func (c *Child) Rounds() uint64 {
|
||||
if c == nil {
|
||||
return 0
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.rounds
|
||||
}
|
||||
|
||||
// ── 错误 ──
|
||||
|
||||
var (
|
||||
@ -201,57 +129,7 @@ var (
|
||||
//
|
||||
// 为什么不允许"同名覆盖":登记表与通道 Owner 都以 id 为键,
|
||||
// 覆盖会让上一代的残留(通道归属、策略、留档)被当成这一代的。
|
||||
// 使用者必须先显式 stop。
|
||||
ErrChildExists = errors.New("同名子 agent 已存在")
|
||||
// ErrChildNotFound 表示 id 未登记。
|
||||
ErrChildNotFound = errors.New("子 agent 不存在")
|
||||
// ErrUnknownKind 表示 Kind 没有注册实现。
|
||||
ErrUnknownKind = errors.New("子 agent 类型未注册")
|
||||
)
|
||||
|
||||
// 差异点接口见 kind.go。
|
||||
|
||||
// ToolResult 是一条工具执行结果。
|
||||
type ToolResult struct {
|
||||
CallID string
|
||||
Name string
|
||||
Text string
|
||||
Raw interface{}
|
||||
Denied bool
|
||||
}
|
||||
|
||||
// ToolSpec 是父控制工具的定义与派发。
|
||||
type ToolSpec struct {
|
||||
Name string
|
||||
Description string
|
||||
Parameters map[string]interface{}
|
||||
Required []string
|
||||
// Handler 接收父发起的调用。c 是**被操作的子**(可能为 nil,如 list 动作)。
|
||||
Handler func(c *Child, args map[string]interface{}) string
|
||||
// Batchable 声明该工具在父侧是否可并发执行。
|
||||
Batchable bool
|
||||
// NeedsChild 为 true 时骨架会先按 args["id"] 查出子再传 Handler。
|
||||
NeedsChild bool
|
||||
}
|
||||
|
||||
// ResidualPolicy 是销毁时对未处理输入的处置(驻留子用)。
|
||||
type ResidualPolicy string
|
||||
|
||||
const (
|
||||
// ResidualKeep 把残余回灌给父(不丢活)。
|
||||
ResidualKeep ResidualPolicy = "keep"
|
||||
// ResidualDrop 丢弃残余。
|
||||
ResidualDrop ResidualPolicy = "drop"
|
||||
)
|
||||
|
||||
// ChannelDisposition 是销毁时对通道归属的处置(驻留子用)。
|
||||
type ChannelDisposition string
|
||||
|
||||
const (
|
||||
// ChannelRelease 注销通道登记。
|
||||
ChannelRelease ChannelDisposition = "release"
|
||||
// ChannelKeep 保留登记,Owner 仍指向已销毁的 id(悬空)。
|
||||
ChannelKeep ChannelDisposition = "keep"
|
||||
// ChannelHandBack 归还父(Owner 清空或改指父)。
|
||||
ChannelHandBack ChannelDisposition = "handback"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user