@@ -1149,12 +1149,13 @@ func validateToolArgs(tool Tool, call ToolCall) error {
11491149 if expectedType == "" {
11501150 continue
11511151 }
1152- if received , mismatch := typeMismatch (val , expectedType ); mismatch {
1152+ if received , hint , mismatch := typeMismatch (val , expectedType ); mismatch {
11531153 issues = append (issues , ValidationIssue {
11541154 Kind : IssueType ,
11551155 Path : key ,
11561156 Expected : expectedType ,
11571157 Received : received ,
1158+ Hint : hint ,
11581159 })
11591160 }
11601161 }
@@ -1168,37 +1169,62 @@ func validateToolArgs(tool Tool, call ToolCall) error {
11681169
11691170// typeMismatch reports whether val conflicts with the JSON Schema type. When it
11701171// does, the returned name is the user-facing JSON type name of the actual value
1171- // ("string" / "integer" / "number" / "boolean" / "array" / "object").
1172- func typeMismatch (val any , expected string ) (string , bool ) {
1172+ // ("string" / "integer" / "number" / "boolean" / "array" / "object"). The hint
1173+ // is non-empty only for known LLM mistake patterns — currently the
1174+ // "JSON-encoded literal passed as a string" mistake that weaker models repeat
1175+ // across turns without nudging.
1176+ func typeMismatch (val any , expected string ) (received string , hint string , mismatch bool ) {
11731177 switch expected {
11741178 case "string" :
11751179 if _ , ok := val .(string ); ok {
1176- return "" , false
1180+ return "" , "" , false
11771181 }
11781182 case "integer" :
11791183 if f , ok := val .(float64 ); ok && f == float64 (int64 (f )) {
1180- return "" , false
1184+ return "" , "" , false
11811185 }
11821186 case "number" :
11831187 if _ , ok := val .(float64 ); ok {
1184- return "" , false
1188+ return "" , "" , false
11851189 }
11861190 case "boolean" :
11871191 if _ , ok := val .(bool ); ok {
1188- return "" , false
1192+ return "" , "" , false
11891193 }
11901194 case "array" :
11911195 if _ , ok := val .([]any ); ok {
1192- return "" , false
1196+ return "" , "" , false
11931197 }
11941198 case "object" :
11951199 if _ , ok := val .(map [string ]any ); ok {
1196- return "" , false
1200+ return "" , "" , false
11971201 }
11981202 default :
1199- return "" , false
1203+ return "" , "" , false
12001204 }
1201- return jsonTypeName (val ), true
1205+ return jsonTypeName (val ), mismatchHint (val , expected ), true
1206+ }
1207+
1208+ // mismatchHint catches the single most common LLM mistake: serializing an
1209+ // array/object to a JSON string and passing the string. The hint nudges the
1210+ // model to drop the surrounding quotes on the next turn. Other mismatches get
1211+ // no hint to keep the error message terse.
1212+ func mismatchHint (val any , expected string ) string {
1213+ if expected != "array" && expected != "object" {
1214+ return ""
1215+ }
1216+ s , ok := val .(string )
1217+ if ! ok {
1218+ return ""
1219+ }
1220+ trimmed := strings .TrimSpace (s )
1221+ if expected == "array" && strings .HasPrefix (trimmed , "[" ) {
1222+ return `Looks like a JSON-encoded array — pass the value directly (e.g. ["a","b"]), not wrapped in quotes.`
1223+ }
1224+ if expected == "object" && strings .HasPrefix (trimmed , "{" ) {
1225+ return `Looks like a JSON-encoded object — pass the value directly (e.g. {"k":"v"}), not wrapped in quotes.`
1226+ }
1227+ return ""
12021228}
12031229
12041230// jsonTypeName returns the JSON Schema type name for val.
0 commit comments