From 1b14128e212180a7079a4d6c383c737bc9a5eef8 Mon Sep 17 00:00:00 2001 From: sumleo Date: Wed, 17 Jun 2026 08:32:50 +0800 Subject: [PATCH] fix: sort tools deterministically to preserve prompt-cache prefix --- tools/registry.go | 13 ++++++- tools/registry_test.go | 83 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 tools/registry_test.go diff --git a/tools/registry.go b/tools/registry.go index 0e4691a..3009fe0 100644 --- a/tools/registry.go +++ b/tools/registry.go @@ -1,6 +1,8 @@ package tools import ( + "sort" + "github.com/codeany-ai/open-agent-sdk-go/types" ) @@ -26,21 +28,28 @@ func (r *Registry) Get(name string) types.Tool { return r.tools[name] } -// All returns all registered tools. +// All returns all registered tools, sorted by name. The order is stable +// across calls so callers (such as the agent request builder) produce a +// deterministic tools list, which keeps the cacheable request prefix from +// shifting between requests. func (r *Registry) All() []types.Tool { result := make([]types.Tool, 0, len(r.tools)) for _, t := range r.tools { result = append(result, t) } + sort.Slice(result, func(i, j int) bool { + return result[i].Name() < result[j].Name() + }) return result } -// Names returns all registered tool names. +// Names returns all registered tool names, sorted for a stable order. func (r *Registry) Names() []string { names := make([]string, 0, len(r.tools)) for name := range r.tools { names = append(names, name) } + sort.Strings(names) return names } diff --git a/tools/registry_test.go b/tools/registry_test.go new file mode 100644 index 0000000..245bc72 --- /dev/null +++ b/tools/registry_test.go @@ -0,0 +1,83 @@ +package tools + +import ( + "sort" + "testing" +) + +// registerOutOfOrder builds a registry by inserting tools in an order that +// does not match their sorted name order, so a stable result from All/Names +// cannot be an accident of insertion order. +func registerOutOfOrder() *Registry { + r := NewRegistry() + r.Register(NewWebSearchTool()) + r.Register(NewBashTool()) + r.Register(NewGlobTool()) + r.Register(NewFileReadTool()) + r.Register(NewGrepTool()) + return r +} + +func TestRegistryAllSorted(t *testing.T) { + r := registerOutOfOrder() + + got := r.All() + names := make([]string, len(got)) + for i, tool := range got { + names[i] = tool.Name() + } + + want := append([]string(nil), names...) + sort.Strings(want) + + for i := range want { + if names[i] != want[i] { + t.Fatalf("All() not sorted by name: got %v, want %v", names, want) + } + } +} + +func TestRegistryNamesSorted(t *testing.T) { + r := registerOutOfOrder() + + got := r.Names() + want := append([]string(nil), got...) + sort.Strings(want) + + for i := range want { + if got[i] != want[i] { + t.Fatalf("Names() not sorted: got %v, want %v", got, want) + } + } +} + +func TestRegistryOrderStableAcrossCalls(t *testing.T) { + r := registerOutOfOrder() + + firstAll := r.All() + firstNames := r.Names() + + for i := 0; i < 20; i++ { + all := r.All() + if len(all) != len(firstAll) { + t.Fatalf("All() length changed: got %d, want %d", len(all), len(firstAll)) + } + for j := range all { + if all[j].Name() != firstAll[j].Name() { + t.Fatalf("All() order changed on call %d at index %d: got %q, want %q", + i, j, all[j].Name(), firstAll[j].Name()) + } + } + + names := r.Names() + if len(names) != len(firstNames) { + t.Fatalf("Names() length changed: got %d, want %d", len(names), len(firstNames)) + } + for j := range names { + if names[j] != firstNames[j] { + t.Fatalf("Names() order changed on call %d at index %d: got %q, want %q", + i, j, names[j], firstNames[j]) + } + } + } +}