fix(v2): coerce string-encoded numbers/bools in tool arguments
CI / Root Module (push) Failing after 30s
CI / Lint (push) Failing after 3s
CI / V2 Module (push) Successful in 1m54s

LLMs occasionally return numeric or boolean tool-call fields as JSON
strings (e.g. "3" instead of 3, "true" instead of true), which Go's
strict json.Unmarshal rejects. The strict unmarshal stays as the happy
path; on failure we retry with a coercion pass that walks the target
struct (recursing into nested structs, slices, maps, and pointer fields)
and converts strings to the appropriate kind. Returns the original error
if coercion can't recover.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 22:12:56 +00:00
parent cbaf41f50c
commit 5c5d861915
3 changed files with 274 additions and 2 deletions
+10 -2
View File
@@ -104,8 +104,16 @@ func (t Tool) Execute(ctx context.Context, argsJSON string) (string, error) {
// Typed tool: unmarshal JSON into the struct, call the function
p := reflect.New(t.pTyp)
if argsJSON != "" && argsJSON != "{}" {
if err := json.Unmarshal([]byte(argsJSON), p.Interface()); err != nil {
return "", fmt.Errorf("invalid tool arguments: %w", err)
err := json.Unmarshal([]byte(argsJSON), p.Interface())
if err != nil {
// LLMs sometimes return numeric/boolean fields as JSON strings
// (e.g. "3" instead of 3). Retry with type coercion.
if coerced, cerr := coerceArgsToType([]byte(argsJSON), t.pTyp); cerr == nil {
err = json.Unmarshal(coerced, p.Interface())
}
if err != nil {
return "", fmt.Errorf("invalid tool arguments: %w", err)
}
}
}
out := t.fn.Call([]reflect.Value{reflect.ValueOf(ctx), p.Elem()})