mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
设计: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/...` 干净
105 lines
4.3 KiB
Go
105 lines
4.3 KiB
Go
package core
|
||
|
||
// inputch 总览工具(单工具多视图)的验收。
|
||
//
|
||
// 需求:父 agent 能看到**所有已注册的 inputch**以及**它们的划分情况**。
|
||
// 构筑方式:单工具(input_channels)+ 多视图(view=all|mine|unassigned|by_agent|detail)。
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
|
||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||
)
|
||
|
||
func inputChToolCall(args map[string]interface{}) agentAPI.ToolCall {
|
||
return agentAPI.ToolCall{ID: "ic1", Name: "input_channels", Arguments: args}
|
||
}
|
||
|
||
func TestInputChannelsTool_SingleToolMultipleViews(t *testing.T) {
|
||
a := newPreemptAgent(t, newPreemptProvider())
|
||
me := string(a.id)
|
||
|
||
// 同一个插件注册多个 inputch(最基本的输入路由单位);另一个插件再注册一个。
|
||
if err := a.io.RegisterInputChannelFrom("qq", "qq", agentIO.ChannelDef{NoMemory: true}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := a.io.RegisterInputChannelFrom("qq", "qq/device-2", agentIO.ChannelDef{}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := a.io.RegisterInputChannelFrom("sub", "sub/in", agentIO.ChannelDef{}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// 划分:qq 与 sub/in 归本 agent,qq/device-2 留未分配。
|
||
if err := a.io.AssignInputChannel("qq", me, 64); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := a.io.AssignInputChannel("sub/in", me, 0); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// view=all(默认):全部已注册,且带归属插件。
|
||
all := a.executeInputChannels(inputChToolCall(nil))
|
||
for _, want := range []string{"qq", "qq/device-2", "sub/in", "插件 qq", "插件 sub", "无记忆"} {
|
||
if !strings.Contains(all, want) {
|
||
t.Fatalf("view=all 缺少 %q:\n%s", want, all)
|
||
}
|
||
}
|
||
|
||
// view=mine:只有划给本 agent 的。
|
||
mine := a.executeInputChannels(inputChToolCall(map[string]interface{}{"view": "mine"}))
|
||
if !strings.Contains(mine, "qq") || !strings.Contains(mine, "sub/in") {
|
||
t.Fatalf("view=mine 应含 qq 与 sub/in:\n%s", mine)
|
||
}
|
||
if strings.Contains(mine, "qq/device-2") {
|
||
t.Fatalf("view=mine 不应含未分配的 qq/device-2:\n%s", mine)
|
||
}
|
||
|
||
// view=unassigned:只有尚未划出的。
|
||
un := a.executeInputChannels(inputChToolCall(map[string]interface{}{"view": "unassigned"}))
|
||
if !strings.Contains(un, "qq/device-2") {
|
||
t.Fatalf("view=unassigned 应含 qq/device-2:\n%s", un)
|
||
}
|
||
if strings.Contains(un, "sub/in") {
|
||
t.Fatalf("view=unassigned 不应含已划分的 sub/in:\n%s", un)
|
||
}
|
||
|
||
// view=by_agent:划分情况总览(按归属分组)。
|
||
byAgent := a.executeInputChannels(inputChToolCall(map[string]interface{}{"view": "by_agent"}))
|
||
if !strings.Contains(byAgent, me+":") {
|
||
t.Fatalf("view=by_agent 应列出归属 %q:\n%s", me, byAgent)
|
||
}
|
||
if !strings.Contains(byAgent, "未分配") {
|
||
t.Fatalf("view=by_agent 应列出未分配一组:\n%s", byAgent)
|
||
}
|
||
|
||
// view=detail:单个 inputch 的全字段(容量 / 策略 / 回程 / 归属)。
|
||
detail := a.executeInputChannels(inputChToolCall(map[string]interface{}{"view": "detail", "name": "qq"}))
|
||
for _, want := range []string{"inputch: qq", "注册插件: qq", "容量: 64", "无记忆", me} {
|
||
if !strings.Contains(detail, want) {
|
||
t.Fatalf("view=detail 缺少 %q:\n%s", want, detail)
|
||
}
|
||
}
|
||
if miss := a.executeInputChannels(inputChToolCall(map[string]interface{}{"view": "detail", "name": "nope"})); !strings.Contains(miss, "未注册") {
|
||
t.Fatalf("detail 查未注册的 inputch 应明确报错:%s", miss)
|
||
}
|
||
if noName := a.executeInputChannels(inputChToolCall(map[string]interface{}{"view": "detail"})); !strings.Contains(noName, "需要 name") {
|
||
t.Fatalf("detail 缺 name 应提示:%s", noName)
|
||
}
|
||
|
||
// 未知视图必须报错并列出可用值(不要把拼错静默当成默认视图)。
|
||
bad := a.executeInputChannels(inputChToolCall(map[string]interface{}{"view": "whatever"}))
|
||
if !strings.Contains(bad, "未知 view") || !strings.Contains(bad, "by_agent") {
|
||
t.Fatalf("未知 view 应报错并列出可用值:%s", bad)
|
||
}
|
||
}
|
||
|
||
// 一个 inputch 都没有时应给出明确说明,而不是空串。
|
||
func TestInputChannelsTool_NoChannels(t *testing.T) {
|
||
a := newPreemptAgent(t, newPreemptProvider())
|
||
if out := a.executeInputChannels(inputChToolCall(nil)); !strings.Contains(out, "没有任何已注册") {
|
||
t.Fatalf("空登记表应明确说明:%q", out)
|
||
}
|
||
}
|