Local AI Agents · Tool use and the agent loop · Lesson 1 of 4
Tool schemas and the call/result round-trip
A tool definition is a JSON Schema contract, not a description — and the call/result round trip that carries a tool request out and its answer back is wired differently on Ollama, OpenAI and Anthropic's APIs, extending the model underneath module's context map to the two pieces it set aside.
By the end, you can
- Write a tool definition as a JSON Schema contract with a name, description, typed parameters and a required list (LA-8).
- Compare how Ollama, OpenAI and Anthropic each represent a model's tool call request and a tool's result, using real, current API shapes (LA-8).
- Trace one tool call through a full request/response round trip on a chosen runtime's API (LA-8).
Before you start
This is Lesson 1 of 4 in Tool use and the agent loop, the module that follows Safe local setup and turns the course from "operate a model safely" into "give it capability." It assumes the model underneath module's vocabulary — specifically its System, user, tool and retrieved context lesson, which already showed that a tool's result shares the context window with everything else and arrives as one of four content categories on every runtime. This lesson does not re-teach that map. It looks at the two pieces that lesson deliberately set aside: how a tool is actually described to a model in the first place, and the exact wire shape of the request that carries a call out and the reply that carries a result back. No running model is required, though every JSON shape shown here is a real, current API payload, not a simplification.
A tool definition is a JSON Schema contract, not a description
When a builder "gives a model a tool," what actually happens is narrower and more mechanical than the phrase suggests: a JSON object is added to the request, and that object is itself a JSON Schema contract describing one callable function. JSON Schema's own reference is specific about what such a contract can require. Required fields are named explicitly — "the `required` keyword takes an array of zero or more strings" — and the fields themselves are declared under `properties`, "an object, where each key is the name of a property and each value is a schema used to validate that property," each with its own `type`. A contract can also close itself off from anything unnamed: setting `additionalProperties` to `false` "means no additional properties will be allowed" beyond the ones the schema lists.
This is not optional detail to skim past. A model never sees a tool's implementation, its docstring or its test suite — it sees exactly this JSON object, and it decides whether and how to call the tool almost entirely from the `name`, `description` and `parameters` fields inside it. A vague description or a loosely typed parameter is not a minor style choice; it is the entire interface the model has to reason from, and — as the next module will build on directly — the entire contract a dispatcher can check a call against before running anything.
Three vendors, one round trip, three different wire shapes
Every runtime that supports tool calling implements the same two-message idea — the model asks for a tool, your code runs it and answers back — but the exact field names differ enough to catch a builder moving between them.
Ollama's API reference documents the request field plainly: `tools`, "list of tools in JSON for the model to use if supported." A tool definition inside that list follows the same `{"type": "function", "function": {name, description, parameters}}` shape as the JSON Schema section above. When a supported model decides to call one, the reply's `message` object carries a `tool_calls` field — "a list of tools in JSON that the model wants to use" — where each entry's `function.arguments` is a JSON **object**, e.g. `{"city": "Tokyo"}`, not a string. To answer back, your code appends a new message with `"role": "tool"`, the result in `content`, and an optional `tool_name` field the docs describe as adding "the name of the tool that was executed to inform the model of the result."
OpenAI's function-calling guide uses the same `tools` array and the same nested `function` shape for a definition, with one addition: setting `strict` to `true`, the guide states, "will ensure function calls reliably adhere to the function schema, instead of being best effort" — a stronger schema-adherence assurance than Ollama's docs describe for the equivalent field. The reply arrives the same general way, in a `tool_calls` array, but each call's `function.arguments` is documented as "JSON-encoded `arguments`" — a **string** your code must parse, not an object handed to you directly. Answering back uses the same `role: "tool"` idea as Ollama, with one more required field: the result message carries a `tool_call_id` matching the specific call it answers, since one reply can request several tool calls in parallel and each result has to be paired back correctly.
Anthropic's Messages API renames two of these pieces outright rather than just reshaping them. A tool's parameter contract is not called `parameters` but `input_schema`; Anthropic's own tool-use walkthrough shows the identical JSON Schema shape underneath — `type: object`, `properties`, `required` — under that different key. When Claude decides to call a tool, there is no `tool_calls` field on the message at all: the model instead replies with `stop_reason: "tool_use"` and one or more `tool_use` content blocks, each carrying its own `id`, `name` and `input` (already a parsed object, matching Ollama's shape rather than OpenAI's string). Your code answers back not with a new `role: "tool"` message, but with a `tool_result` content block — matched to the request by `tool_use_id`, the same call-to-result pairing OpenAI's `tool_call_id` provides — nested inside an ordinary `role: "user"` message.
None of these three shapes is more "correct" than the others; each is a different serialization of the same request/dispatch/answer idea. What changes in practice is exactly where you look for the tool's name, whether its arguments arrive parsed or as a string you parse yourself, and what field name and message role carry a result back — three things worth checking against a runtime's own current docs the moment you port a working tool loop from one to another, rather than assuming the last runtime's shape.
A worked example: one tool, three payloads
A `check_order_status` tool takes one required argument, `order_id` (string), and returns a status string. Its JSON Schema parameter contract — `{"type": "object", "properties": {"order_id": {"type": "string", "description": "The order to check."}}, "required": ["order_id"]}` — is identical on all three runtimes; the differences are entirely in the surrounding wire shape.
On Ollama, the request carries `"tools": [{"type": "function", "function": {"name": "check_order_status", "description": "...", "parameters": {...}}}]`; a call comes back as `"tool_calls": [{"function": {"name": "check_order_status", "arguments": {"order_id": "A-1029"}}}]`; the answer goes back as `{"role": "tool", "content": "shipped", "tool_name": "check_order_status"}`.
On OpenAI, the request's `tools` entry adds `"strict": true` if you want the schema enforced rather than best-effort; a call comes back as `"tool_calls": [{"id": "call_abc123", "function": {"name": "check_order_status", "arguments": "{\"order_id\": \"A-1029\"}"}}]` — note the quoted, string-typed `arguments`; the answer goes back as `{"role": "tool", "tool_call_id": "call_abc123", "content": "shipped"}`.
On Anthropic, the request's `tools` entry uses `"input_schema"` instead of `"parameters"`; a call comes back as a content block `{"type": "tool_use", "id": "toolu_01A...", "name": "check_order_status", "input": {"order_id": "A-1029"}}`; the answer goes back as a `tool_result` content block, `{"type": "tool_result", "tool_use_id": "toolu_01A...", "content": "shipped"}`, inside a `role: "user"` message.
Same tool, same underlying idea, three genuinely different payloads to write and parse — exactly the reason a tool loop's dispatcher (the module's next lesson) is worth writing against the schema and the registry, not against one runtime's field names hard-coded throughout.
Accessibility notes
This lesson is text-first, with no images, audio, video or downloadable artifacts. All JSON and API examples appear as plain inline or block text, readable and copyable by assistive technology and keyboard-only users. The practice exercise's model answer sits behind a native disclosure control that is reachable and operable by keyboard and correctly announced by screen readers. The knowledge check uses native radio-button inputs with a visible question and options, and posts its result to a live status region so assistive technology announces the outcome without a page reload.
Practice
Translate a package-tracking tool across three runtimes
A `track_package` tool takes one required argument, `tracking_number` (string), and an optional `carrier` (string). It returns a short status string such as 'in transit' or 'delivered'. You need to wire this tool into an agent loop, but you don't yet know which of the three runtimes this lesson covered the project will end up using.
- Write the tool's JSON Schema parameter contract: the properties, their types, and the required list, using this lesson's `additionalProperties` note to decide whether to allow any other argument.
- Write what the request's tools entry looks like on Ollama and on OpenAI. Which field name differs between them for the parameter contract, and which stays the same?
- Write what the request's tools entry looks like on Anthropic. Name the one field that changes name here specifically.
- The model decides to call track_package with tracking_number '1Z999AA1' and no carrier. Write the tool_calls or tool_use shape each of the three runtimes would use to represent that call, being careful about whether arguments/input arrives as an object or a string.
- Write the result message or content block each runtime expects back, given the tool returns 'in transit'.
Compare with a bounded first version
Parameter contract: {type: object, properties: {tracking_number: {type: string}, carrier: {type: string}}, required: [tracking_number]}; additionalProperties: false is reasonable here since only these two named fields make sense for this tool. Ollama and OpenAI both nest this under tools: [{type: function, function: {name, description, parameters: (the contract above)}}] — the parameter contract's own field name, parameters, is identical on both; what differs between them is only how a later tool_calls response represents arguments (object on Ollama, JSON-encoded string on OpenAI), not the definition itself. Anthropic's tools entry is {name, description, input_schema: (the same contract)} — input_schema is the one renamed field; everything inside it is the identical JSON Schema shape. A call with no carrier supplied: Ollama's tool_calls entry is {function: {name: track_package, arguments: {tracking_number: '1Z999AA1'}}} (arguments is an object, and carrier is simply absent since it wasn't required); OpenAI's is {id: call_..., function: {name: track_package, arguments: a JSON-encoded string containing tracking_number set to '1Z999AA1'}} (arguments is a string, not an object); Anthropic's is a tool_use block {type: tool_use, id: toolu_..., name: track_package, input: {tracking_number: '1Z999AA1'}} (input is an object, like Ollama's). Results back: Ollama expects {role: tool, content: 'in transit', tool_name: track_package}; OpenAI expects {role: tool, tool_call_id: call_..., content: 'in transit'}; Anthropic expects a tool_result content block {type: tool_result, tool_use_id: toolu_..., content: 'in transit'} nested inside a role: user message.
Knowledge check
Try the idea
Low-stakes practice only. This does not score, block progress or create a learner record.Sources and limits
This lesson synthesises the sources below into a practical learning model. It is not a security standard, legal advice or a guarantee that any particular agent design is safe.
- Object — JSON Schema. Documents the required, properties and additionalProperties keywords used to write a tool's parameter contract.
- ollama/ollama docs/api.md — Ollama (GitHub). Documents the tools request field, the tool_calls response field with arguments as a JSON object, and the role: tool result message with an optional tool_name field.
- Function calling — OpenAI. Documents the tools array's function/parameters/strict shape, tool_calls with JSON-encoded string arguments, and submitting a result as a role: tool message with a matching tool_call_id.
- Tool use with Claude — Anthropic. Documents a tool's input_schema, the tool_use content block Claude returns to request a call, and the tool_result content block that returns a matching answer.