wavetermdev/waveterm · error

tool input is not an object

Error message

tool input is not an object

What it means

The Anthropic tool_use contract requires tool input to be a JSON object. FinalObject parses the accumulated stream successfully but the top-level value is not an object (e.g. an array, string, or number), so it refuses to return it as tool input. This is a defensive check against provider/tool-schema mismatches.

Source

Thrown at pkg/aiusechat/anthropic/anthropic-backend.go:342

func (p *partialJSON) Bytes() []byte { return p.buf.Bytes() }

func (p *partialJSON) FinalObject() (json.RawMessage, error) {
	raw := p.buf.Bytes()
	// If empty, treat as "{}"
	if len(bytes.TrimSpace(raw)) == 0 {
		return json.RawMessage(`{}`), nil
	}
	// The accumulated content should be a valid JSON object string; parse it.
	var v interface{}
	if err := json.Unmarshal(raw, &v); err != nil {
		return nil, fmt.Errorf("invalid accumulated tool input JSON: %w", err)
	}
	// Ensure it's an object per Anthropic contract
	switch v.(type) {
	case map[string]interface{}:
		return json.RawMessage(raw), nil
	default:
		return nil, fmt.Errorf("tool input is not an object")
	}
}

// sanitizeHostnameInError removes the Wave cloud hostname from error messages
func sanitizeHostnameInError(err error) error {
	if err == nil {
		return nil
	}
	errStr := err.Error()
	parsedURL, parseErr := url.Parse(uctypes.DefaultAIEndpoint)
	if parseErr == nil && parsedURL.Host != "" && strings.Contains(errStr, parsedURL.Host) {
		errStr = strings.ReplaceAll(errStr, uctypes.DefaultAIEndpoint, "AI service")
		errStr = strings.ReplaceAll(errStr, parsedURL.Host, "host")
	}
	return fmt.Errorf("%s", errStr)
}

// makeThinkingOpts creates thinking options based on level and max tokens

View on GitHub (pinned to a4447c1563)

Solutions

  1. Fix the tool's input_schema so it declares type: object with defined properties.
  2. Wrap the call and return a clear tool_result error back to the model so it can self-correct on the next turn.
  3. Check Anthropic API/model version changes affecting tool input emission.
  4. Log the raw accumulated JSON (json.RawMessage) to see what the model actually produced.

Example fix

// before
schema := map[string]any{"type": "array", "items": ...} // non-object input
// after
schema := map[string]any{"type": "object", "properties": map[string]any{"items": map[string]any{"type": "array"}} // object root
Defensive patterns

Strategy: type-guard

Validate before calling

func isJSONObject(raw json.RawMessage) bool {
    var m map[string]interface{}
    return json.Unmarshal(raw, &m) == nil
}

Type guard

func asObject(raw json.RawMessage) (map[string]interface{}, bool) {
    var m map[string]interface{}
    if err := json.Unmarshal(raw, &m); err != nil { return nil, false }
    return m, true
}

Try / catch

input, err := acc.FinalObject()
if err != nil {
    if strings.Contains(err.Error(), "not an object") {
        return emitToolResultError("tool returned non-object input") // let model retry
    }
    return err
}

Prevention

When it happens

Trigger: A tool_use content block whose accumulated input_json_delta stream parses to valid JSON but with a non-object top-level type — e.g. the model emitted "[1,2]" or "null" as tool arguments.

Common situations: Tool schemas declared without a proper object input_schema (parameters as arrays or free types); a model misbehaving on a poorly-described tool; version changes in the Anthropic API emitting alternative structures.

Related errors


AI-assisted analysis of wavetermdev/waveterm@a4447c1563 (2026-09-01). Data as JSON: /api/errors/d069aab98f748da4. Report an issue: GitHub.