package tool import ( "encoding/json" "reflect" "strconv" "strings" ) // unmarshalArgsLenient decodes the raw JSON arguments the model supplied // into the tool's typed Args struct, tolerating the classic LLM tool-call // bug of emitting numbers and booleans as strings ("3" where the schema // said integer, "true" where it said boolean). // // Why mort-side (vs relying on the library): legacy gollm's Define performed // this coercion internally (tool_coerce.go), and several years of tool // traffic depend on the tolerance. majordomo's DefineTool decodes // strictly by design, so the gated wrappers re-create the leniency here // — the strict path is tried first and coercion only runs on failure, // which makes the happy path zero-cost. func unmarshalArgsLenient(raw json.RawMessage, target any) error { if len(raw) == 0 { return nil } strictErr := json.Unmarshal(raw, target) if strictErr == nil { return nil } coerced, err := coerceArgsToType(raw, reflect.TypeOf(target).Elem()) if err != nil { // Malformed JSON: surface the original strict error, which // names the real problem. return strictErr } if err := json.Unmarshal(coerced, target); err != nil { return strictErr } return nil } // coerceArgsToType reparses argsJSON with leniency: where the target // struct expects a numeric or boolean field but the JSON value is a // string, it converts the string to the target kind. Recurses into // nested structs, slices, maps, and pointer fields. Returns a freshly // marshaled JSON byte slice that unmarshals strictly into the target. func coerceArgsToType(argsJSON []byte, target reflect.Type) ([]byte, error) { var raw any if err := json.Unmarshal(argsJSON, &raw); err != nil { return nil, err } raw = coerceValue(raw, target) return json.Marshal(raw) } func coerceValue(v any, t reflect.Type) any { if t == nil { return v } for t.Kind() == reflect.Pointer { t = t.Elem() } switch t.Kind() { case reflect.Struct: m, ok := v.(map[string]any) if !ok { return v } for i := 0; i < t.NumField(); i++ { f := t.Field(i) if !f.IsExported() { continue } name := jsonFieldName(f) if name == "-" { continue } if val, present := m[name]; present { m[name] = coerceValue(val, f.Type) } } return m case reflect.Slice, reflect.Array: arr, ok := v.([]any) if !ok { return v } elemType := t.Elem() for i := range arr { arr[i] = coerceValue(arr[i], elemType) } return arr case reflect.Map: m, ok := v.(map[string]any) if !ok { return v } valType := t.Elem() for k := range m { m[k] = coerceValue(m[k], valType) } return m case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if s, ok := v.(string); ok { s = strings.TrimSpace(s) s = strings.TrimPrefix(s, "+") if n, err := strconv.ParseInt(s, 10, 64); err == nil { return n } if f, err := strconv.ParseFloat(s, 64); err == nil { return int64(f) } } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if s, ok := v.(string); ok { s = strings.TrimSpace(s) s = strings.TrimPrefix(s, "+") if n, err := strconv.ParseUint(s, 10, 64); err == nil { return n } if f, err := strconv.ParseFloat(s, 64); err == nil && f >= 0 { return uint64(f) } } case reflect.Float32, reflect.Float64: if s, ok := v.(string); ok { s = strings.TrimSpace(s) if f, err := strconv.ParseFloat(s, 64); err == nil { return f } } case reflect.Bool: if s, ok := v.(string); ok { if b, err := strconv.ParseBool(strings.TrimSpace(s)); err == nil { return b } } } return v } // jsonFieldName returns the effective JSON key for a struct field: // the json tag's name part when present, the Go field name otherwise, // or "-" when the field is excluded. func jsonFieldName(f reflect.StructField) string { tag, ok := f.Tag.Lookup("json") if !ok { return f.Name } name, _, _ := strings.Cut(tag, ",") if name == "" { return f.Name } return name }