From a3dff3e556ff9b63f506647305b5b2877947d988 Mon Sep 17 00:00:00 2001 From: Anon2Tokyo Date: Wed, 5 Aug 2026 04:01:35 +0800 Subject: [PATCH 1/2] fix(agent): render tool parameters deterministically --- internal/agent/agent.go | 9 ++++++- internal/agent/agent_test.go | 49 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 00367acf..ae6baa49 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -1524,7 +1524,14 @@ func formatToolDefs(toolDefs []llm.ToolDef) string { } } } - for name, p := range params { + paramNames := make([]string, 0, len(params)) + for name := range params { + paramNames = append(paramNames, name) + } + sort.Strings(paramNames) + + for _, name := range paramNames { + p := params[name] suffix := "" if required[name] { suffix = " (required)" diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index d08ec928..9a68b3c3 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -295,6 +295,55 @@ func TestFormatToolDefs(t *testing.T) { } }) + t.Run("parameters are rendered in sorted order", func(t *testing.T) { + defs := []llm.ToolDef{ + { + Type: "function", + Function: llm.FunctionDef{ + Name: "code_search", + Description: "Search code", + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{ + "query": map[string]any{ + "description": "Query string", + }, + "case_sensitive": map[string]any{ + "description": "Match case", + }, + "path_glob": map[string]any{ + "description": "Path glob", + }, + "max_results": map[string]any{ + "description": "Maximum results", + }, + }, + "required": []any{"query"}, + }, + }, + }, + } + + got := formatToolDefs(defs) + wantLines := []string{ + " - case_sensitive: Match case", + " - max_results: Maximum results", + " - path_glob: Path glob", + " - query: Query string (required)", + } + last := -1 + for _, line := range wantLines { + idx := strings.Index(got, line) + if idx == -1 { + t.Fatalf("missing parameter line %q in:\n%s", line, got) + } + if idx <= last { + t.Fatalf("parameter line %q is out of order in:\n%s", line, got) + } + last = idx + } + }) + t.Run("tool without parameters", func(t *testing.T) { defs := []llm.ToolDef{ { From c1b123d59567ff55b3ebe8a8072717df2e0577e9 Mon Sep 17 00:00:00 2001 From: Anon2Tokyo Date: Wed, 5 Aug 2026 21:02:10 +0800 Subject: [PATCH 2/2] fix(agent): preserve tool parameter order --- internal/agent/agent.go | 90 +++++++++++++++++++++++++++++++++--- internal/agent/agent_test.go | 66 +++++++++++++++++++++++++- internal/llm/client.go | 7 +-- 3 files changed, 152 insertions(+), 11 deletions(-) diff --git a/internal/agent/agent.go b/internal/agent/agent.go index ae6baa49..1b29df7f 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -1,6 +1,7 @@ package agent import ( + "bytes" "context" "crypto/sha256" "encoding/binary" @@ -8,7 +9,9 @@ import ( "encoding/json" "errors" "fmt" + "maps" "runtime/debug" + "slices" "sort" "strconv" "strings" @@ -1514,6 +1517,17 @@ func formatToolDefs(toolDefs []llm.ToolDef) string { for _, td := range toolDefs { fn := &td.Function sb.WriteString(fmt.Sprintf("- **%s**: %s\n", fn.Name, fn.Description)) + if orderedParams, ok := orderedToolParameters(fn.RawDefinition); ok { + sb.WriteString(" Parameters:\n") + for _, p := range orderedParams { + suffix := "" + if p.Required { + suffix = " (required)" + } + sb.WriteString(fmt.Sprintf(" - %s: %s%s\n", p.Name, p.Description, suffix)) + } + continue + } if params, ok := fn.Parameters["properties"].(map[string]any); ok && len(params) > 0 { sb.WriteString(" Parameters:\n") required := make(map[string]bool) @@ -1524,13 +1538,7 @@ func formatToolDefs(toolDefs []llm.ToolDef) string { } } } - paramNames := make([]string, 0, len(params)) - for name := range params { - paramNames = append(paramNames, name) - } - sort.Strings(paramNames) - - for _, name := range paramNames { + for _, name := range slices.Sorted(maps.Keys(params)) { p := params[name] suffix := "" if required[name] { @@ -1548,6 +1556,73 @@ func formatToolDefs(toolDefs []llm.ToolDef) string { return sb.String() } +type orderedToolParameter struct { + Name string + Description string + Required bool +} + +func orderedToolParameters(raw json.RawMessage) ([]orderedToolParameter, bool) { + if len(raw) == 0 { + return nil, false + } + + var def struct { + Parameters struct { + Required []string `json:"required"` + Properties json.RawMessage `json:"properties"` + } `json:"parameters"` + } + if err := json.Unmarshal(raw, &def); err != nil || len(def.Parameters.Properties) == 0 { + return nil, false + } + + required := make(map[string]bool, len(def.Parameters.Required)) + for _, name := range def.Parameters.Required { + required[name] = true + } + + dec := json.NewDecoder(bytes.NewReader(def.Parameters.Properties)) + tok, err := dec.Token() + if err != nil { + return nil, false + } + if delim, ok := tok.(json.Delim); !ok || delim != '{' { + return nil, false + } + + var params []orderedToolParameter + for dec.More() { + keyTok, err := dec.Token() + if err != nil { + return nil, false + } + name, ok := keyTok.(string) + if !ok { + return nil, false + } + var meta struct { + Description string `json:"description"` + } + if err := dec.Decode(&meta); err != nil { + return nil, false + } + params = append(params, orderedToolParameter{ + Name: name, + Description: meta.Description, + Required: required[name], + }) + } + if _, err := dec.Token(); err != nil { + return nil, false + } + + if len(params) == 0 { + return nil, false + } + return params, true +} + // findDiff returns the Diff for the given file path, or nil if not found. func (a *Agent) findDiff(path string) *model.Diff { for i := range a.diffs { @@ -1572,6 +1647,7 @@ func BuildToolDefs(entries []toolsconfig.ToolConfigEntry, planOnly bool) []llm.T fmt.Fprintf(stdout.Writer(), "[ocr] WARNING: failed to parse tool definition %q: %v\n", e.Name, err) continue } + fn.RawDefinition = defRaw defs = append(defs, llm.ToolDef{ Type: "function", Function: fn, diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index 9a68b3c3..5a117880 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -295,7 +295,71 @@ func TestFormatToolDefs(t *testing.T) { } }) - t.Run("parameters are rendered in sorted order", func(t *testing.T) { + t.Run("parameters preserve raw JSON order", func(t *testing.T) { + raw := json.RawMessage(`{ + "name":"code_search", + "description":"Search code", + "parameters":{ + "type":"object", + "properties":{ + "query":{"description":"Query string"}, + "path_glob":{"description":"Path glob"}, + "case_sensitive":{"description":"Match case"}, + "max_results":{"description":"Maximum results"} + }, + "required":["query"] + } + }`) + defs := []llm.ToolDef{ + { + Type: "function", + Function: llm.FunctionDef{ + Name: "code_search", + Description: "Search code", + RawDefinition: raw, + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{ + "query": map[string]any{ + "description": "Query string", + }, + "path_glob": map[string]any{ + "description": "Path glob", + }, + "case_sensitive": map[string]any{ + "description": "Match case", + }, + "max_results": map[string]any{ + "description": "Maximum results", + }, + }, + "required": []any{"query"}, + }, + }, + }, + } + + got := formatToolDefs(defs) + wantLines := []string{ + " - query: Query string (required)", + " - path_glob: Path glob", + " - case_sensitive: Match case", + " - max_results: Maximum results", + } + last := -1 + for _, line := range wantLines { + idx := strings.Index(got, line) + if idx == -1 { + t.Fatalf("missing parameter line %q in:\n%s", line, got) + } + if idx <= last { + t.Fatalf("parameter line %q is out of order in:\n%s", line, got) + } + last = idx + } + }) + + t.Run("fallback parameters are sorted when raw order is unavailable", func(t *testing.T) { defs := []llm.ToolDef{ { Type: "function", diff --git a/internal/llm/client.go b/internal/llm/client.go index 7ffd4d3c..15062e7b 100644 --- a/internal/llm/client.go +++ b/internal/llm/client.go @@ -177,9 +177,10 @@ type ToolDef struct { // FunctionDef specifies the metadata for a tool definition. type FunctionDef struct { - Name string `json:"name"` - Description string `json:"description"` - Parameters map[string]any `json:"parameters"` + Name string `json:"name"` + Description string `json:"description"` + Parameters map[string]any `json:"parameters"` + RawDefinition json.RawMessage `json:"-"` } // ClientConfig holds configuration for connecting to an LLM service.