Skip to content

Engineering

How to Build an AI Agent in Rust: Tool Calling & Error Handling

Build a bounded Rust agent with a local Ollama model, validated tool arguments, checked arithmetic and explicit failure handling. Includes runnable source and tests.

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.toml

Replace 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

FailureExample behaviorProduction implication
Unknown tool or invalid argumentsReturn a structured tool errorPermit correction only within a step budget
Integer overflowReturn an explicit arithmetic errorNever silently wrap a business value
Transport or invalid JSONStop with a transport/protocol categoryRetry only with a defined time and attempt budget
Endless valid callsStop after the turn budgetBound total task work independently of per-call limits
Ambiguous external writeNot present in this read-only exampleReconcile 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.