Skip to content
Get Started

AgentRunner

AgentRunner is the driver behind Rig’s high-level agent prompt APIs. Most applications can use agent.prompt("...").await?, but a runner gives you an explicit value for one run: one prompt, one set of per-run options, and one call to run() or stream().

Think of the layers like this:

  • Agent stores reusable configuration: model, preamble, tools, RAG context, memory, and default hooks.
  • AgentRunner drives one prompt through the agent loop. It owns per-run options such as turn limits, memory behavior, tool concurrency, tool extensions, and hooks.
  • AgentRun is the lower-level, sans-IO state machine. Use it only when you need to persist and resume the loop yourself, for example durable out-of-process approvals.

agent.runner(prompt) builds a runner seeded from the agent’s configuration. run() drives the loop to completion and returns a PromptResponse, including the final output, aggregate usage, per-call usage, and message history.

use rig::client::{CompletionClient, ProviderClient};
use rig::providers::openai;
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let agent = openai::Client::from_env()?
.agent(openai::GPT_4O)
.preamble("You are a helpful assistant.")
.build();
let response = agent
.runner("Find the answer and show your work.")
.max_turns(5)
.run()
.await?;
println!("answer: {}", response.output);
println!("model calls: {}", response.completion_calls.len());
println!("tokens: {}", response.usage.total_tokens);
Ok(())
}

This is equivalent in spirit to:

let response = agent
.prompt("Find the answer and show your work.")
.max_turns(5)
.extended_details()
.await?;

Use whichever shape reads better in your code. AgentRunner is especially useful when you build a run in stages or share configuration between the blocking and streaming surfaces.

MethodWhat it changes
max_turns(n)Maximum multi-turn depth before PromptError::MaxTurnsError.
max_invalid_tool_call_retries(n)Retry budget for invalid tool-call recovery. Retries also consume normal turns.
history(messages)Explicit chat history. This bypasses conversation memory for the run.
conversation(id)Conversation id used to load/save configured memory.
without_memory()Disable memory load and save for this run.
tool_concurrency(n)Execute up to n tool calls from one model turn at once; 0 is clamped to 1.
tool_extensions(ext)Pass runtime-only values to tools via Tool::call_with_extensions, without exposing them to the model.
add_hook(hook)Append a hook after any default hooks on the agent. See Hooks.

max_turns is the agent loop’s safety bound: it limits how many follow-up model calls Rig may make after tool results. If the model keeps calling tools past the limit, Rig returns PromptError::MaxTurnsError with the history accumulated so far.

max_invalid_tool_call_retries is separate. It only applies when a hook recovers from an invalid tool call with Flow::retry(...); each retry asks the model to try the turn again and also consumes normal turn budget. For the hook side of that recovery, see Hooks.

If the agent has a memory backend and a conversation id, the runner loads history before the first model call and saves the completed turn after the run finishes:

let response = agent
.runner("What did I ask earlier?")
.conversation("user-42")
.run()
.await?;

Passing explicit history(...) bypasses memory completely for that run: Rig neither loads from nor saves to the memory backend. Use without_memory() when you want the same bypass without providing manual history.

When a model emits multiple tool calls in a single turn, the runner can execute them concurrently:

let response = agent
.runner("Check inventory, shipping, and pricing.")
.max_turns(3)
.tool_concurrency(3)
.run()
.await?;

Final message history is still persisted in tool-call order. With tool_concurrency > 1, per-tool side effects such as logs, spans, and hook callbacks may interleave in completion order, so make those side effects concurrency-safe.

tool_extensions lets application code pass runtime-only values to tools: auth tokens, tenant IDs, request metadata, session state, or other values the model should not see. Tools read these values from Tool::call_with_extensions.

let response = agent
.runner("Update my account preferences.")
.tool_extensions(extensions)
.run()
.await?;

Use extensions for trusted application context. Do not put secrets in the prompt just so a tool can read them.

run() drives the blocking path and returns one PromptResponse. stream() drives the same runner through the streaming path, yielding assistant deltas, tool activity, and a final response.

Both surfaces share the same run construction, tool execution, memory behavior, tracing span shape, and hook handling. The streaming path adds streaming-specific items and hook events, such as text deltas and tool-call argument deltas.

let stream = agent
.runner("Explain the result as you work.")
.max_turns(3)
.stream()
.await;

See Streaming for consuming streaming items and Hooks for streaming hook events.

Use AgentRunner when you still want Rig to perform IO: send model requests, execute tools, apply memory, emit tracing spans, and run hooks. Use AgentRun when you want a serializable state machine and will provide the IO yourself.

AgentRun is the right tool for durable human-in-the-loop systems: serialize the run while tools are pending, wait for an approval in another service, then deserialize and feed tool results back. It does not contain hooks because hooks are async, side-effecting driver code; hooks live at the AgentRunner layer.

  • Hooks — observe and steer runner events.
  • Agents — the high-level agent abstraction.
  • Tools — define tools that the runner executes.
  • Memory — conversation memory used by conversation(...).
  • Streaming — stream responses from the runner.
  • AgentRunner source on GitHub — exact main-branch type and builder methods.