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.
The stateless model
Section titled “The stateless model”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.
Why stateless?
Section titled “Why stateless?”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, callsrun, and savesnew_messagesback. 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.
Persisting a turn
Section titled “Persisting a turn”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.
The shape of a message
Section titled “The shape of a message”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).
Managing context length
Section titled “Managing context length”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::Assistantmessage with aToolUseblock must keep its followingRole::Toolresult, 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.
System prompts
Section titled “System prompts”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.
Key takeaways
Section titled “Key takeaways”- The agent is stateless:
runtakes history in, handsnew_messagesback, and touches no store. - Persist
new_messagesafterrunsucceeds — atomically with the user message — so a failed turn never corrupts your history. - Messages are plain
serdestructs; store them in whatever database you already use. - Window or summarize history yourself to bound context;
context_budgetmakes over-long prompts fail fast. - Set the system prompt on the builder; Kova prepends it every turn.