Skip to content

Conversations & History

An agent needs the earlier turns of a conversation to answer the next one. In Kova, that history is yours to own. The agent is stateless: it reads no store and writes no store. You pass the history in, you get everything the turn produced back, and you decide where it lives.

Agent::run(&[ConversationMessage]) is the one and only entry point. You pass the full history in; you get back everything the turn produced in new_messages. Nothing is read from or written to any store.

let mut history = vec![user_message("What's the capital of France?")];
let response = agent.run(&history).await?;
history.extend(response.new_messages); // you decide where this goes
// later, continue the conversation:
history.push(user_message("And its population?"));
let response = agent.run(&history).await?;

That’s the whole model. history is a plain Vec<ConversationMessage>. To continue a conversation you keep the vec around (in memory, a database row, a Redis key) and feed it back on the next run.

Persistence, sessions, and compaction are host concerns, and hosts already have opinions (and infrastructure) for them. By keeping them out of the SDK:

  • Scaling is free. A stateless agent is cheap to Arc-share across tasks and safe to run behind a load balancer — each request loads history from your store, calls run, and saves new_messages back. No sticky sessions.
  • You use your own database. No adapter trait to implement, no second source of truth. The messages are ordinary serializable structs (serde) — store them as JSON, rows, or documents, however you already store data.
  • You control the context window. Truncation, summarization, and retrieval are policies you tune per application, not behavior baked into a store.

The pattern for any request-scoped app (a web handler, a job worker):

// 1. Load this conversation's history from your store.
let mut history: Vec<ConversationMessage> = db.load_history(conversation_id).await?;
// 2. Append the new user message and run one turn.
history.push(user_message(&user_input));
let response = agent.run(&history).await?;
// 3. Persist everything the turn produced. Do this only after run() succeeds,
// so a failed turn never leaves a dangling user message or an orphaned
// tool-use block in your store.
db.append_messages(conversation_id, &response.new_messages).await?;
// 4. Reply.
Ok(response.text)

new_messages holds everything the turn generated — the assistant’s tool-use requests, the tool results, and the final answer — in order. Appending it verbatim keeps your stored history valid for the next turn.

History is a Vec<ConversationMessage>:

pub struct ConversationMessage {
pub role: Role, // User | Assistant | System | Tool
pub content: Vec<ContentBlock>, // Text | ToolUse | ToolResult | Thinking
}

A single assistant turn can contain multiple content blocks — e.g. some text plus two ToolUse requests. This is why run returns new_messages as a Vec: one turn can legitimately produce several messages.

You generally don’t build assistant/tool messages by hand — they arrive in new_messages. You only construct Role::User messages (the helper above), and occasionally a Role::System one, though the system prompt is better set on the builder (below).

Because you own the history, trimming it is up to you. Two common strategies:

  • Window it. Keep the most recent N turns, but never split a tool pair — a Role::Assistant message with a ToolUse block must keep its following Role::Tool result, or the provider rejects the request. Cut at a user-message boundary.
  • Summarize it. Periodically replace old turns with a short summary message (run a cheap model to produce it). This keeps unbounded conversations within a token budget without losing the gist.

To fail fast instead of silently truncating, set AgentBuilder::context_budget — the agent checks the assembled prompt against a token budget before each provider call and returns KovaError::ContextBudgetExceeded rather than sending a doomed request.

A system prompt sets the agent’s persona and rules. Set it once on the builder; Kova prepends it to every turn’s messages automatically:

let agent = AgentBuilder::new()
.provider(provider)
.system_prompt("You are a terse assistant. Answer in one sentence.")
.build()?;

You don’t put the system prompt in your history — the agent adds it for you on each provider call, so it also survives any windowing you do to the history.

  • The agent is stateless: run takes history in, hands new_messages back, and touches no store.
  • Persist new_messages after run succeeds — atomically with the user message — so a failed turn never corrupts your history.
  • Messages are plain serde structs; store them in whatever database you already use.
  • Window or summarize history yourself to bound context; context_budget makes over-long prompts fail fast.
  • Set the system prompt on the builder; Kova prepends it every turn.