Why: the completion doc's N2 requires old tool results to become file paths (ZCode compact pattern) and cache-friendly prefix rules; today the micro-compactor only left a transcript pointer and nothing pinned prefix stability. alignToolTail had no direct edge tests. Evidence: GOWORK=off go vet + go test -count=1 ./... green; new TestClearOldToolResultsSpillsToDisk / TestSpillResultSanitizesCallID / TestAlignToolTailEdges / TestSystemTextDeterministicAcrossIdenticalBuilds; legacy placeholder bytes unchanged when ResultsDir is empty (TestClearOldToolResultsEmptyDirKeepsLegacyPlaceholder). Impact: opt-in via Config.ResultsDir; the native adapter wires it to <workDir>/.agentx/results. No behavior change for other hosts. Refs: 2026-10-07-agent-stack-n2-plan WP1
71 lines
2.3 KiB
Go
71 lines
2.3 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"agent-native/runtime/llm"
|
|
"agent-native/runtime/tools"
|
|
)
|
|
|
|
// ---- N2 WP1:前缀稳定契约(缓存友好的机器钉住:同一注册序列 → 逐字节确定)----
|
|
|
|
type contractTool struct {
|
|
name, desc string
|
|
deferred bool
|
|
}
|
|
|
|
func (t contractTool) Name() string { return t.name }
|
|
func (t contractTool) Description() string { return t.desc }
|
|
func (t contractTool) Parameters() json.RawMessage { return json.RawMessage(`{"type":"object","properties":{}}`) }
|
|
func (t contractTool) Execute(_ context.Context, _ json.RawMessage) (tools.Result, error) {
|
|
return tools.Result{Content: "ok"}, nil
|
|
}
|
|
func (t contractTool) Exposure() string {
|
|
if t.deferred {
|
|
return tools.ExposureDeferred
|
|
}
|
|
return tools.ExposureDirect
|
|
}
|
|
|
|
func buildContractRegistry() *tools.Registry {
|
|
reg := tools.NewRegistry("")
|
|
reg.Register(contractTool{name: "read_file", desc: "读文件"})
|
|
reg.Register(contractTool{name: "write_file", desc: "写文件"})
|
|
reg.Register(contractTool{name: "grep", desc: "搜索", deferred: true})
|
|
return reg
|
|
}
|
|
|
|
func TestSystemTextDeterministicAcrossIdenticalBuilds(t *testing.T) {
|
|
a := systemText(Config{SystemPrompt: "sys", Tools: buildContractRegistry()})
|
|
b := systemText(Config{SystemPrompt: "sys", Tools: buildContractRegistry()})
|
|
if a != b {
|
|
t.Fatalf("同一注册序列的系统文本必须逐字节一致:\n%q\nvs\n%q", a, b)
|
|
}
|
|
// 契约内容:直接工具全在清单内且按注册序;deferred 工具不进入清单。
|
|
idx := strings.Index(a, "read_file")
|
|
if idx < 0 || strings.Index(a, "write_file") < idx {
|
|
t.Fatalf("直接工具须按注册序出现:%q", a)
|
|
}
|
|
if strings.Contains(a, "grep") {
|
|
t.Fatalf("deferred 工具不得出现在系统文本:%q", a)
|
|
}
|
|
// 元工具注入位置确定(tool_search/call_tool 存在且在 Decls 中)。
|
|
decls := buildContractRegistry().Decls()
|
|
names := []string{}
|
|
for _, d := range decls {
|
|
names = append(names, d.Name)
|
|
}
|
|
if len(names) != 4 || names[0] != "read_file" || names[1] != "write_file" {
|
|
t.Fatalf("Decls 顺序契约:%v", names)
|
|
}
|
|
if names[2] != "tool_search" && names[3] != "tool_search" {
|
|
if names[2] != "call_tool" && names[3] != "call_tool" {
|
|
t.Fatalf("元工具缺失:%v", names)
|
|
}
|
|
}
|
|
var _ = llm.ToolDecl{} // 保持导入面稳定
|
|
}
|