Conversation — multi-turn memory
Rust · crate toolnexus · SPEC §8
impl Client { pub fn conversation(&self, tk: &Toolkit) -> Conversation }pub struct Conversation { pub messages: Vec<Value>, .. }impl Conversation { pub async fn send(&mut self, prompt: impl Into<Prompt>) -> Result<RunResult, ClientError> }Keep a transcript across turns so the model remembers what it already did.
When to use it
Section titled “When to use it”For a chat inside one process: each send carries the transcript so far, so the model remembers what it did.
Why this and not the alternative
Section titled “Why this and not the alternative”Use Conversation when the history lives with your object; use ask(prompt, tk, id) with a store when it must survive restarts or be shared by id.
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 — two turns, transcript grows
Section titled “1. The smallest useful call — two turns, transcript grows”use std::sync::Arc;
use toolnexus::client::{GenerateFn, InProcessOptions, InProcessResponse, create_in_process_client};use toolnexus::{BuiltinsConfig, ToolkitOptions, create_toolkit};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let model: GenerateFn = Arc::new(|_req| Box::pin(async { Ok(InProcessResponse { content: "ack".into(), ..Default::default() }) })); let client = create_in_process_client(InProcessOptions { model: "m".into(), generate: Some(model), ..Default::default() }); let tk = create_toolkit(ToolkitOptions { builtins: Some(BuiltinsConfig::off()), ..Default::default() }).await?;
let mut conv = client.conversation(&tk); conv.send("one").await?; let after_first = conv.messages.len(); conv.send("two").await?; assert!(conv.messages.len() > after_first, "transcript did not grow"); println!("ok: transcript grew from {} to {}", after_first, conv.messages.len()); Ok(())}2. The realistic case — the system prompt is sent once, not per turn
Section titled “2. The realistic case — the system prompt is sent once, not per turn”use std::sync::{Arc, Mutex};
use toolnexus::client::{GenerateFn, InProcessOptions, InProcessResponse, create_in_process_client};use toolnexus::{BuiltinsConfig, ToolkitOptions, create_toolkit};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { // Record the roles of every message the model is sent. let last_roles: Arc<Mutex<Vec<String>>> = Arc::default(); let seen = last_roles.clone(); let model: GenerateFn = Arc::new(move |req| { *seen.lock().unwrap() = req.messages.iter().map(|m| m["role"].as_str().unwrap_or("").to_string()).collect(); Box::pin(async { Ok(InProcessResponse { content: "ok".into(), ..Default::default() }) }) }); let client = create_in_process_client(InProcessOptions { model: "m".into(), generate: Some(model), system_prompt: "You are terse.".into(), ..Default::default() }); let tk = create_toolkit(ToolkitOptions { builtins: Some(BuiltinsConfig::off()), ..Default::default() }).await?; let mut conv = client.conversation(&tk);
conv.send("first").await?; assert_eq!(*last_roles.lock().unwrap(), ["system", "user"]);
conv.send("second").await?; // Turn 2: [system, user, assistant, user]; the system message is NOT repeated. let roles = last_roles.lock().unwrap().clone(); assert_eq!(roles, ["system", "user", "assistant", "user"]); println!("ok: system sent once, transcript on turn 2 = {roles:?}"); Ok(())}3. The full surface — reset clears memory, then continues fresh
Section titled “3. The full surface — reset clears memory, then continues fresh”use std::sync::Arc;
use toolnexus::client::{GenerateFn, InProcessOptions, InProcessResponse, create_in_process_client};use toolnexus::{BuiltinsConfig, ToolkitOptions, create_toolkit};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let model: GenerateFn = Arc::new(|_req| Box::pin(async { Ok(InProcessResponse { content: "ack".into(), ..Default::default() }) })); let client = create_in_process_client(InProcessOptions { model: "m".into(), generate: Some(model), ..Default::default() }); let tk = create_toolkit(ToolkitOptions { builtins: Some(BuiltinsConfig::off()), ..Default::default() }).await?; let mut conv = client.conversation(&tk);
conv.send("hello").await?; assert!(!conv.messages.is_empty());
conv.reset(); assert!(conv.messages.is_empty());
// A fresh send after reset starts a new transcript from scratch. let res = conv.send("hello again").await?; assert_eq!((res.turns, conv.messages.len()), (1, 2)); // [user, assistant] println!("ok: reset then {} messages after one fresh turn", conv.messages.len()); Ok(())}Conversation surface
Section titled “Conversation surface”| Member | What it does |
|---|---|
Client::conversation(&self, tk: &Toolkit) -> Conversation |
Builds a Conversation bound to this client and toolkit. |
messages: Vec<Value> |
The full running transcript — a public field; read it to inspect or persist it yourself. |
send(&mut self, prompt) -> Result<RunResult, ClientError> |
Sends the next user turn with prior history retained; updates messages to the returned RunResult.messages. |
reset(&mut self) |
Clears messages — the next send starts a brand-new transcript. |
See also
Section titled “See also”create_client— The unified client: system prompt, skills injection, parallel and chained tool calls, retries, memory.Client.run— Send a prompt, let the loop call tools until the model stops, get a RunResult.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.