On this page
This Rust AI agent tutorial builds a small, bounded tool-calling loop. A local Ollama model may request one read-only tool, add; Rust validates the request, executes checked integer addition and sends the result back to the model. Four local tests exercise dispatch, conversation state, stopping and malformed responses.
The example uses Rust, serde_json and the curl executable to keep the HTTP adapter inspectable. It is a teaching application, not a production service or a performance benchmark. Official Ollama chat and tool-calling documentation were reviewed on 12 November 2026. Live model behavior was not measured.
Prerequisites and complete source
Use a Rust toolchain, Cargo, curl and a running local Ollama installation. Select an installed local model whose documentation supports tool calling. Do not assume every model exposes the same behavior.
The complete downloadable Cargo project accompanies this article in examples/blog/rust-agent. From the repository root:
cargo test --locked --manifest-path examples/blog/rust-agent/Cargo.toml
OLLAMA_MODEL=your-installed-local-tool-model cargo run --locked --manifest-path examples/blog/rust-agent/Cargo.tomlReplace the model value with an actual installed model name. The second command needs the local service and model; it was not executed as part of the fixture tests. Dependency downloads require network access on a clean machine. The download contains the manifest, lockfile and complete source.
Define one capability with a narrow schema
The request sends a function named add, with required integer fields a and b and no additional properties. The host still validates every call. A schema guides model output but is not an authorization or runtime safety guarantee.
The core dispatch logic uses checked addition:
fn add(a: i64, b: i64) -> Result<i64, &'static str> {
a.checked_add(b).ok_or("integer_overflow")
}The complete dispatcher rejects unknown tool names, non-object arguments, extra fields and values that cannot be represented as signed 64-bit integers. It returns structured errors instead of panicking. There is no shell tool, arbitrary URL fetch or model-selected filesystem access.
This deliberate scope makes correctness independently checkable. A customer-record tool would also need trusted identity and resource authorization. Do not carry the arithmetic example's permissive caller assumptions into a business API.
Keep the assistant message and tool result in order
Each model turn receives the conversation history and tool schema. If the assistant requests tools, the program preserves that assistant message before appending tool results. The next model turn can then interpret the returned values in context.
For the example, the model receives “19 plus 23.” A valid add call returns the JSON value {"sum":42}. The model may turn that into prose, but the host cannot assume the final prose is faithful merely because the tool result is correct. Validate important structured outcomes at the application boundary.
The loop accepts at most four model turns and four tool calls per turn. Empty final answers, malformed call arrays and invalid roles fail explicitly. A model that keeps asking for tools eventually returns step_budget_exhausted rather than running indefinitely.
Understand the transport boundary
The adapter posts JSON to the fixed local address http://127.0.0.1:11434/api/chat with streaming disabled. It sends arguments directly to a curl child process without invoking a shell. Model content therefore does not become shell command text.
A 20-second request timeout and a response-size limit constrain ordinary network behavior. The request also sets an output-token limit. These controls do not amount to a production-wide deadline or process supervisor; an operating-system or child-process problem needs stronger lifecycle management in a real service.
Transport failure terminates the example without retrying. The code suppresses curl stderr and returns bounded error categories. A production implementation should use an appropriate HTTP client, cancellation and structured observability while keeping secrets and private input out of logs.
Classify errors before deciding to retry
| Failure | Example behavior | Production implication |
|---|---|---|
| Unknown tool or invalid arguments | Return a structured tool error | Permit correction only within a step budget |
| Integer overflow | Return an explicit arithmetic error | Never silently wrap a business value |
| Transport or invalid JSON | Stop with a transport/protocol category | Retry only with a defined time and attempt budget |
| Endless valid calls | Stop after the turn budget | Bound total task work independently of per-call limits |
| Ambiguous external write | Not present in this read-only example | Reconcile or use idempotency before repeating |
Do not treat every error as temporary. Repeating denied access or malformed arguments is usually wasteful. A timed-out write is especially different from a failed read: it may already have completed remotely.
What the tests actually prove
The dispatch test checks normal addition, unknown tools, invalid argument types, extra arguments and overflow. The two-turn fixture verifies that a known tool result reaches the next model call. Another test exercises the step budget, and the failure test checks transport errors, malformed calls and empty final output.
These are deterministic tests with scripted model responses. They establish local control-flow behavior, not a model's ability to choose the correct tool, inference speed or production reliability. Run a separate live evaluation with the selected model before extending the application.
Next implementation steps
Add a trusted identity boundary before tools touch private resources. Persist task and action identifiers before introducing side effects. Bound concurrent tasks and total elapsed work, and require review for consequential actions.
The Rust backend guide covers concurrency and cancellation. The agent-versus-MCP comparison explains how an MCP tool interface could replace the direct dispatcher. Publishing the resulting product is a separate task covered by the agent release guide.
Frequently asked questions
Do I need an agent framework?
No. A small explicit loop can teach the boundary clearly. A framework becomes useful when its abstractions solve requirements you actually have.
Can this run offline?
After dependencies and a suitable local model are installed, the adapter targets a local service. Verify the selected model and surrounding workflow do not use cloud features.