Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion internal/agent/agent.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package agent

import (
"bytes"
"context"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"maps"
"runtime/debug"
"slices"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -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)
Expand All @@ -1524,7 +1538,8 @@ func formatToolDefs(toolDefs []llm.ToolDef) string {
}
}
}
for name, p := range params {
for _, name := range slices.Sorted(maps.Keys(params)) {
p := params[name]
suffix := ""
if required[name] {
suffix = " (required)"
Expand All @@ -1541,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 {
Expand All @@ -1565,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,
Expand Down
113 changes: 113 additions & 0 deletions internal/agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,119 @@ func TestFormatToolDefs(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",
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{
{
Expand Down
7 changes: 4 additions & 3 deletions internal/llm/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down