mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
## 背景 仓库早已迁移到 GitHub(`github.com/JianFeeeee/HomeAgent`,CI/CD 都在那边跑), gitcode 保留为**国内只读镜像**(`ci.yml` 的 sync-gitcode-main 快进推送)。 但 go.mod 的 module 路径一直是 `gitcode.com/JianFeeeee/...` —— 既与权威源 不符,`meta.SourceURL`(AGPL §13 的源码提供入口)也指向镜像而非实际运行的代码。 ## 关键区分(这是本次改动安全的前提) 两个 module 是**不同**的,影响面差一个数量级: | module | 谁依赖 | 改它的后果 | |---|---|---| | `.../HomeAgent`(主仓) | **只有主仓自己** | 内部重构,零外部影响 | | `.../homeagent-sdk`(SDK 仓) | 主仓 49 文件 + 所有外部插件 | 需重编插件 | ★ 实测确认:**插件不 import 主仓 module**(`strings qq/plugin.bin | grep HomeAgent` = 0), 只依赖 SDK 仓。而插件与内核的边界是 **RPC 方法名字符串** (`plugin.init` / `tool.invoke`…,见 proc/protocol.go),**与 Go module 名无关**。 ⇒ module 改名**不改变任何 wire 协议**,现网已装的插件照常运行。 SDK 的 GitHub 仓名是 `homeagentsdk`(无连字符),module 路径必须与之精确匹配, 否则 `go get` 会因路径不符失败,故取 `github.com/JianFeeeee/homeagentsdk`。 ## 改动 - `go.mod` / `go.sum`:两个 module 路径 + require/replace - 277 个 Go 文件 + Makefile + 打包脚本的 import 与 ldflags 路径 - SDK 仓 `meta.CoreModule`、`tools/hmapdev` 的 module 声明与工程模板 - 对外文档/链接的仓库地址(README、PLUGIN_DEV、OVERVIEW 等) ## 明确**保留** gitcode 的地方(镜像机制,不能动) - `GITCODE_TOKEN`、`GC_URL="…@gitcode.com"`(CI 同步 job) - `gitcode.com/api/v5/...`(release 附件上传) - git remote `gitcode`(镜像拉取/推送) - `docs/zh/ci-cd-runbook.md`(整篇讲镜像运维) ## 验证 - `go build ./...` / `go vet ./...` 绿 - `go test ./... -count=1` → 48 包 ok / 0 FAIL - hmapdev 工具链编译通过(`third_party/homeagent-sdk/tools/hmapdev`) - 全仓残留检查:module 形态的 `gitcode.com/JianFeeeee` = 0 - 镜像机制抽验:GC_URL / @gitcode / api/v5 / remote 均完好 ## 顺带修正 `hmapdev/templates.go` 的注释此前写「gitcode 的模块不在 proxy.golang.org 上」—— module 已迁 GitHub,该论断不再成立。改为如实说明**为何仍默认 replace 到本机 SDK**: 离线可构建、避免版本漂移、内网可用。理由与托管地无关,故 replace 保留。
209 lines
7.6 KiB
Go
209 lines
7.6 KiB
Go
package core
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
|
||
agentAPI "github.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||
agentIO "github.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||
)
|
||
|
||
// 方案 B:工具结果**只统计不裁剪**。
|
||
//
|
||
// 现状核实:工具结果进 f.Msgs 时**没有任何长度上限**(task.go 直接
|
||
// `Content: result`),内核也**不预检**是否超长 —— 超限由上游 API 报错。
|
||
// 时间线那一侧有预算(ContextTokens = 0.8×窗口,进消息前就裁过),
|
||
// 但那只管 a.context 的历史事件,**不管单条工具结果**。
|
||
// ⇒ 一条巨大工具结果可能直接冲破预算,而内核**不会提前发现**。
|
||
//
|
||
// 本阶段的取舍:**不裁剪用户数据**(截断会让模型拿到残缺信息,且
|
||
// 截断位置由内核武断决定),改为**统计 + 告警**,把处置权交回给
|
||
// 调度器/上层。这与本仓「显式才是特权」的取向一致。
|
||
//
|
||
// 判据钉住四件事:会计数、会超阈值、默认**不**裁剪、报告可执行。
|
||
|
||
// bigToolDevice 返回一个指定大小的工具结果。
|
||
type bigToolDevice struct {
|
||
name string
|
||
toolName string
|
||
size int
|
||
}
|
||
|
||
func (d *bigToolDevice) Name() string { return d.name }
|
||
func (d *bigToolDevice) Type() agentIO.DeviceType { return agentIO.DeviceOutput }
|
||
func (d *bigToolDevice) Description() string { return "big result test device" }
|
||
func (d *bigToolDevice) Tools() []agentIO.ToolDef {
|
||
return []agentIO.ToolDef{{Name: d.toolName}}
|
||
}
|
||
func (d *bigToolDevice) Execute(string, map[string]interface{}) (interface{}, error) {
|
||
return strings.Repeat("x", d.size), nil
|
||
}
|
||
func (d *bigToolDevice) Start() error { return nil }
|
||
func (d *bigToolDevice) Stop() error { return nil }
|
||
func (d *bigToolDevice) OutputCapabilities() agentIO.OutputCapability { return agentIO.CapText }
|
||
func (d *bigToolDevice) ChannelDef() agentIO.ChannelDef { return agentIO.ChannelDef{} }
|
||
|
||
// ① 统计必须真的发生:巨大工具结果要触发一次「超预算」报告。
|
||
func TestHugeToolResultIsReported(t *testing.T) {
|
||
// 阈值刻意调小,让 200KB 的结果必然超限(不必真造 1M token)
|
||
a := newPreemptAgent(t, newPreemptProvider())
|
||
a.toolResultWarnTokens = 200 * 1024 // 200KB(EstimateTokens 为 rune×2)
|
||
|
||
rep := &countingReporter{}
|
||
a.toolResultReporter = rep
|
||
|
||
if err := a.io.RegisterDevice(&bigToolDevice{
|
||
name: "bigdev", toolName: "big_tool", size: 400 * 1024, // 400KB
|
||
}); err != nil {
|
||
t.Fatalf("注册设备失败: %v", err)
|
||
}
|
||
|
||
sp := &batchProvider{responses: []*agentAPI.CompletionResponse{
|
||
{ToolCalls: []agentAPI.ToolCall{{ID: "c1", Name: "big_tool", Arguments: map[string]interface{}{}}}},
|
||
{Content: "final"},
|
||
}}
|
||
a2, _ := newBatchAgent(t, sp)
|
||
a2.toolResultWarnTokens = a.toolResultWarnTokens
|
||
a2.toolResultReporter = rep
|
||
if err := a2.io.RegisterDevice(&bigToolDevice{
|
||
name: "bigdev", toolName: "big_tool", size: 400 * 1024,
|
||
}); err != nil {
|
||
t.Fatalf("注册设备失败: %v", err)
|
||
}
|
||
|
||
f := a2.newTaskFrame("go", a2.stageCtxFromInput("go", "", ""))
|
||
if out := a2.runTaskSteps(f); out != outcomeDone {
|
||
t.Fatalf("runTaskSteps=%v err=%v", out, f.Err)
|
||
}
|
||
if rep.n == 0 {
|
||
t.Error("400KB 的工具结果未触发任何超限报告 —— 统计没生效")
|
||
}
|
||
if rep.lastTool != "big_tool" {
|
||
t.Errorf("报告应指明是哪个工具,实际 %q", rep.lastTool)
|
||
}
|
||
if rep.lastTokens <= 0 {
|
||
t.Errorf("报告应带上估算 token 数,实际 %d", rep.lastTokens)
|
||
}
|
||
}
|
||
|
||
// ② ★ 方案 B 的核心:**默认不裁剪**。统计归统计,数据必须原样给模型。
|
||
func TestHugeToolResultIsNotTruncated(t *testing.T) {
|
||
a, _ := newBatchAgent(t, &batchProvider{responses: []*agentAPI.CompletionResponse{
|
||
{ToolCalls: []agentAPI.ToolCall{{ID: "c1", Name: "big_tool", Arguments: map[string]interface{}{}}}},
|
||
{Content: "final"},
|
||
}})
|
||
const size = 200 * 1024
|
||
a.toolResultWarnTokens = 10 // 阈值调到极小,必定触发
|
||
|
||
rep := &countingReporter{}
|
||
a.toolResultReporter = rep
|
||
if err := a.io.RegisterDevice(&bigToolDevice{
|
||
name: "bigdev", toolName: "big_tool", size: size,
|
||
}); err != nil {
|
||
t.Fatalf("注册设备失败: %v", err)
|
||
}
|
||
|
||
f := a.newTaskFrame("go", a.stageCtxFromInput("go", "", ""))
|
||
if out := a.runTaskSteps(f); out != outcomeDone {
|
||
t.Fatalf("runTaskSteps=%v err=%v", out, f.Err)
|
||
}
|
||
// 模型看到的必须**原样**
|
||
var got string
|
||
for _, m := range f.Msgs {
|
||
if m.Role == "tool" {
|
||
got = m.Content
|
||
}
|
||
}
|
||
if len(got) != size {
|
||
t.Errorf("工具结果被裁剪了:得到 %d 字节,期望 %d(方案 B 只统计不裁剪)",
|
||
len(got), size)
|
||
}
|
||
// 且确实报告过
|
||
if rep.n == 0 {
|
||
t.Error("未触发超限报告")
|
||
}
|
||
}
|
||
|
||
// ③ 小结果不该误报(避免噪音淹没有效信号)。
|
||
func TestNormalToolResultNotReported(t *testing.T) {
|
||
a, _ := newBatchAgent(t, &batchProvider{responses: []*agentAPI.CompletionResponse{
|
||
{ToolCalls: []agentAPI.ToolCall{{ID: "c1", Name: "small_tool", Arguments: map[string]interface{}{}}}},
|
||
{Content: "final"},
|
||
}})
|
||
a.toolResultWarnTokens = 100 * 1024 // 100KB
|
||
rep := &countingReporter{}
|
||
a.toolResultReporter = rep
|
||
if err := a.io.RegisterDevice(&smallToolDevice{}); err != nil {
|
||
t.Fatalf("注册失败: %v", err)
|
||
}
|
||
f := a.newTaskFrame("go", a.stageCtxFromInput("go", "", ""))
|
||
if out := a.runTaskSteps(f); out != outcomeDone {
|
||
t.Fatalf("runTaskSteps=%v err=%v", out, f.Err)
|
||
}
|
||
if rep.n != 0 {
|
||
t.Errorf("小结果被误报 %d 次 —— 噪音会淹没有效信号", rep.n)
|
||
}
|
||
}
|
||
|
||
// ④ 报告内容要可执行:说清是哪个工具、多大、占预算多少。
|
||
func TestReportIsActionable(t *testing.T) {
|
||
a, _ := newBatchAgent(t, &batchProvider{responses: []*agentAPI.CompletionResponse{
|
||
{ToolCalls: []agentAPI.ToolCall{{ID: "c1", Name: "big_tool", Arguments: map[string]interface{}{}}}},
|
||
{Content: "final"},
|
||
}})
|
||
a.toolResultWarnTokens = 1024
|
||
rep := &countingReporter{}
|
||
a.toolResultReporter = rep
|
||
if err := a.io.RegisterDevice(&bigToolDevice{
|
||
name: "bigdev", toolName: "big_tool", size: 50 * 1024,
|
||
}); err != nil {
|
||
t.Fatalf("注册失败: %v", err)
|
||
}
|
||
f := a.newTaskFrame("go", a.stageCtxFromInput("go", "", ""))
|
||
if out := a.runTaskSteps(f); out != outcomeDone {
|
||
t.Fatalf("runTaskSteps=%v err=%v", out, f.Err)
|
||
}
|
||
|
||
if rep.lastMsg == "" {
|
||
t.Fatal("报告文案为空")
|
||
}
|
||
for _, want := range []string{"big_tool", "token"} {
|
||
if !strings.Contains(rep.lastMsg, want) {
|
||
t.Errorf("报告缺少 %q:%s", want, rep.lastMsg)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- 测试替身 ----
|
||
|
||
type countingReporter struct {
|
||
n int
|
||
lastTool string
|
||
lastTokens int
|
||
lastMsg string
|
||
}
|
||
|
||
func (r *countingReporter) report(tool string, tokens, budget int, msg string) {
|
||
r.n++
|
||
r.lastTool = tool
|
||
r.lastTokens = tokens
|
||
r.lastMsg = msg
|
||
}
|
||
|
||
// smallToolDevice 返回一个很小的结果。
|
||
type smallToolDevice struct{}
|
||
|
||
func (d *smallToolDevice) Name() string { return "smalldev" }
|
||
func (d *smallToolDevice) Type() agentIO.DeviceType { return agentIO.DeviceOutput }
|
||
func (d *smallToolDevice) Description() string { return "small result test device" }
|
||
func (d *smallToolDevice) Tools() []agentIO.ToolDef {
|
||
return []agentIO.ToolDef{{Name: "small_tool"}}
|
||
}
|
||
func (d *smallToolDevice) Execute(string, map[string]interface{}) (interface{}, error) {
|
||
return "tiny", nil
|
||
}
|
||
func (d *smallToolDevice) Start() error { return nil }
|
||
func (d *smallToolDevice) Stop() error { return nil }
|
||
func (d *smallToolDevice) OutputCapabilities() agentIO.OutputCapability { return agentIO.CapText }
|
||
func (d *smallToolDevice) ChannelDef() agentIO.ChannelDef { return agentIO.ChannelDef{} }
|