# ADR-0010: Tools and structured output — one canonical shape, native mappings **Status:** Accepted — 2026-06-10 ## Context Tool calling and schema-constrained output exist on every target but with different wire shapes (verified against current docs, June 2026; shapes recorded in each provider's package doc). The canonical API must hide all of it. ## Decision Canonical: `Tool{Name, Description, Parameters (JSON Schema), Handler}`; `Response.ToolCalls[]{ID, Name, Arguments json.RawMessage}`; results return as `ToolResultsMessage(ToolResult{ID, Name, Content, IsError})`. Structured output via `WithSchema(schema, name)`. Per-provider mapping: | Concern | OpenAI(+compat) | Anthropic(+compat) | Ollama/foreman | Google (Phase 4) | |---|---|---|---|---| | Tool def | `tools[].function{name,description,parameters}` | `tools[]{name,description,input_schema}` | `tools[].function` | `FunctionDeclaration.ParametersJsonSchema` | | Call args | JSON **string** → RawMessage | `tool_use.input` object | `arguments` **object** | `FunctionCall.Args` map | | Results | one `role:tool` msg per result (`tool_call_id`) | one **user** msg, `tool_result` blocks (`is_error` native) | `role:tool` + `tool_name` | `FunctionResponse` parts | | IsError | `"ERROR: "` content prefix | `is_error: true` | `"ERROR: "` prefix | response payload field | | Forced choice | `tool_choice` string / named object | `{"type":"any"/"tool"/"none"}` | none → drop tools; others best-effort ignored | `FunctionCallingConfig` modes | | Structured | `response_format json_schema` (no strict flag) | `output_config.format json_schema` (GA mechanism) | `format: ` | `ResponseJsonSchema` + JSON MIME | Cross-cutting decisions: - **Missing call ids are synthesized** (`call_`) — Ollama and some compat servers omit them; the agent loop needs ids to match results. - **Streaming buffers tool-call arguments to completion** (ADR-0002): OpenAI fragments accumulate by index, Anthropic `input_json_delta` fragments accumulate per block; consumers only ever see parseable calls. - **No strict-mode flag is sent** to OpenAI: strict mode imposes schema constraints (every property required, additionalProperties:false) that caller-supplied schemas may not satisfy. The `Generate[T]` reflector (Phase 5) emits strict-compatible schemas anyway. - `SchemaName` feeds providers that need a name (OpenAI; default "response"); others ignore it. - Tool handlers never panic the loop: `Toolbox.Execute`/`ExecuteTool` recover panics and JSON-encode results (ADR to agent loop, Phase 5). ## Consequences - One test matrix per provider asserts the exact wire JSON both directions; drift is caught by httptest fixtures, not in production. - Ollama's missing tool_choice means "required" cannot be enforced there — documented in the README matrix rather than emulated.