run — one turn of the loop
Rust · crate toolnexus · SPEC §8
pub async fn run(&self, prompt: impl Into<Prompt>, tk: &Toolkit) -> Result<RunResult, ClientError>Send a prompt, let the loop call tools until the model stops, get a RunResult.
When to use it
Section titled “When to use it”For one task: send the prompt, let the loop call tools until the model stops (or max_turns), and get RunResult \{ text, tool_calls, turns, usage, status, pending \}.
Why this and not the alternative
Section titled “Why this and not the alternative”Use run for a one-shot; use ask(prompt, tk, id) or conversation(tk) when the next prompt must remember this one.
Examples
Section titled “Examples”Each example is a complete program. The model is an in-process stand-in (create_in_process_client): the loop is exactly the one create_client runs, with a host function in place of the HTTP call, so no network and no API key are involved.
1. The smallest useful call — a plain text answer
Section titled “1. The smallest useful call — a plain text answer”No tools, one round trip: the model answers directly and the loop stops.
use std::sync::Arc;
use toolnexus::client::{GenerateFn, InProcessOptions, InProcessResponse, InProcessUsage, create_in_process_client};use toolnexus::{BuiltinsConfig, ToolkitOptions, create_toolkit};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { // An in-process stand-in for the model: it answers directly, no tool calls. let model: GenerateFn = Arc::new(|_req| Box::pin(async { Ok(InProcessResponse { content: "Paris".into(), usage: Some(InProcessUsage { prompt_tokens: 4, completion_tokens: 1, total_tokens: 5 }), ..Default::default() }) })); let client = create_in_process_client(InProcessOptions { model: "gpt-4o-mini".into(), generate: Some(model), ..Default::default() }); let tk = create_toolkit(ToolkitOptions { builtins: Some(BuiltinsConfig::off()), ..Default::default() }).await?;
let res = client.run("capital of France?", &tk).await?; assert_eq!((res.text.as_str(), res.status.as_str(), res.turns), ("Paris", "done", 1)); println!("ok: {}", res.text); Ok(())}2. The realistic case — a tool call in the loop
Section titled “2. The realistic case — a tool call in the loop”The model asks for add, the loop executes it, feeds the result back, and the model answers on the second turn.
This is the shape of nearly every real agent call.
use std::sync::Arc;
use serde_json::json;use toolnexus::client::{GenerateFn, InProcessOptions, InProcessResponse, InProcessToolCall, create_in_process_client};use toolnexus::{BuiltinsConfig, ToolkitOptions, create_toolkit, define_tool};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { // Turn 1: the model asks for add(2, 3). Turn 2: it has the tool result and answers. let model: GenerateFn = Arc::new(|req| Box::pin(async move { let tool = req.messages.iter().rev().find(|m| m["role"] == "tool").cloned(); Ok(match tool { None => InProcessResponse { tool_calls: vec![InProcessToolCall { id: "c1".into(), name: "add".into(), arguments: json!({"a": 2, "b": 3}) }], ..Default::default() }, Some(m) => InProcessResponse { content: m["content"].as_str().unwrap_or("").into(), ..Default::default() }, }) })); let add = define_tool("add", "add two numbers", Some(json!({"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}}})), |a, _| async move { Ok::<_, String>((a["a"].as_f64().unwrap_or(0.0) + a["b"].as_f64().unwrap_or(0.0)).to_string()) }); let tk = create_toolkit(ToolkitOptions { extra_tools: vec![add], builtins: Some(BuiltinsConfig::off()), ..Default::default() }).await?; let client = create_in_process_client(InProcessOptions { model: "gpt-4o-mini".into(), generate: Some(model), ..Default::default() });
let res = client.run("what is 2+3?", &tk).await?; assert_eq!((res.text.as_str(), res.turns, res.tool_call_count), ("5", 2, 1)); assert_eq!((res.tool_calls[0].name.as_str(), res.tool_calls[0].output.as_str()), ("add", "5")); println!("ok: {} in {} turns", res.text, res.turns); Ok(())}3. The full surface — options and the whole RunResult
Section titled “3. The full surface — options and the whole RunResult”system_prompt is prepended to the toolkit’s skills prompt; max_turns caps the loop and, when hit mid-tool-call
with no final answer, the run comes back loudly as "incomplete" rather than silently "done".
use std::sync::Arc;
use serde_json::json;use toolnexus::client::{GenerateFn, InProcessOptions, InProcessResponse, InProcessToolCall, InProcessUsage, create_in_process_client};use toolnexus::{BuiltinsConfig, ToolkitOptions, create_toolkit, define_tool};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { // The model NEVER stops calling the tool, to exercise the max_turns cap. let model: GenerateFn = Arc::new(|_req| Box::pin(async { Ok(InProcessResponse { tool_calls: vec![InProcessToolCall { id: "c1".into(), name: "ping".into(), arguments: json!({}) }], usage: Some(InProcessUsage { prompt_tokens: 2, completion_tokens: 2, total_tokens: 4 }), ..Default::default() }) })); let ping = define_tool("ping", "ping", None, |_, _| async { Ok::<_, String>("pong".into()) }); let tk = create_toolkit(ToolkitOptions { extra_tools: vec![ping], builtins: Some(BuiltinsConfig::off()), ..Default::default() }).await?; let client = create_in_process_client(InProcessOptions { model: "gpt-4o-mini".into(), generate: Some(model), system_prompt: "Be terse.".into(), max_turns: 2, // hit the cap while the model is still mid tool-call ..Default::default() });
let res = client.run("keep pinging", &tk).await?; assert_eq!((res.status.as_str(), res.limit.as_str()), ("incomplete", "maxTurns")); assert_eq!((res.turns, res.tool_call_count, res.model.as_str()), (2, 2, "gpt-4o-mini")); // Partial work is preserved even on a loud stop. assert!(!res.messages.is_empty() && res.usage.total_tokens > 0); println!("ok: stopped at {} after {} turns, {} tool calls", res.limit, res.turns, res.tool_call_count); Ok(())}RunResult fields
Section titled “RunResult fields”| Field | Type | What it holds |
|---|---|---|
text |
String |
The model’s final answer text ("" on an incomplete run with no text yet). |
messages |
Vec<Value> |
The full transcript — system/user/assistant/tool messages, provider-native shape. |
tool_calls |
Vec<ToolCall> |
One record per tool call: name, args, output, is_error, metadata. |
tool_call_count |
usize |
tool_calls.len(). |
turns |
usize |
Number of LLM round trips this run took. |
usage |
Usage |
prompt_tokens / completion_tokens / total_tokens (+ cache tokens), summed across every turn. |
model |
String |
The model used. |
status |
String |
"done" normally; "pending" iff a tool suspended with no wait_for configured (§10); "incomplete" iff a limit was hit. |
limit |
String |
Set ("maxTurns", "timeout") only when status == "incomplete". |
pending |
Option<Request> |
Set only when status == "pending". |
See also
Section titled “See also”create_client— The unified client: system prompt, skills injection, parallel and chained tool calls, retries, memory.Client.stream— The streaming loop: text deltas, tool-call events, and suspension events as they happen.Hooks— Intercept before/after model calls and tool calls: audit, redact, veto, or rewrite.Conversation— Keep a transcript across turns so the model remembers what it already did.