Quick Start
import { Steps } from ‘@astrojs/starlight/components’;
This page gets you from an empty project to a running agent — first a plain one-shot reply, then one that can call a tool, then a multi-turn conversation you own.
Your first agent
Section titled “Your first agent”-
Create a project and add the crate.
Terminal window cargo new my-agent && cd my-agentcargo add kova-sdkcargo add tokio --features macros,rt-multi-thread -
Write
src/main.rs.src/main.rs use std::sync::Arc;use kova_sdk::prelude::*;use kova_sdk::provider::openai::{OpenAiCompatibleProvider, OpenAiProviderConfig};#[tokio::main]async fn main() -> Result<(), KovaError> {// Point at any OpenAI-compatible endpoint. A local server (Ollama,// LM Studio, vLLM) needs no API key; for hosted OpenAI, add .with_api_key(...).let config = OpenAiProviderConfig::new("http://127.0.0.1:1234", "my-model");let provider = Arc::new(OpenAiCompatibleProvider::new(config)?);// Build an agent. The provider is the only required piece.let agent = AgentBuilder::new().provider(provider).build()?;// Kova is stateless: you pass the conversation history in, the agent runs// one turn, and hands back everything it produced. A message is a role +// a list of content blocks.let history = vec![ConversationMessage {role: Role::User,content: vec![ContentBlock::Text { text: "Hello! Say hi back in one sentence.".into() }],}];let response = agent.run(&history).await?;println!("{}", response.text);Ok(())} -
Run it.
Terminal window cargo run
That’s a complete agent. run took your history, sent it to the model, and
returned an AgentResponse —
response.text is the reply, and response.new_messages holds everything the
turn produced, ready for you to keep.
Add a tool
Section titled “Add a tool”Agents get useful when the model can do things. A tool is any type that
implements the Tool trait. Here’s one that reports the weather:
use std::sync::Arc;use async_trait::async_trait;use kova_sdk::prelude::*;use kova_sdk::provider::openai::{OpenAiCompatibleProvider, OpenAiProviderConfig};use serde_json::{json, Value};
struct GetWeather;
#[async_trait]impl Tool for GetWeather { fn name(&self) -> &str { "get_weather" }
fn description(&self) -> &str { "Get the current weather for a city" }
fn parameters_schema(&self) -> Value { json!({ "type": "object", "properties": { "city": { "type": "string", "description": "City name" } }, "required": ["city"] }) }
async fn execute(&self, args: Value) -> Result<ToolResult, KovaError> { let city = args["city"].as_str().unwrap_or("unknown"); // A real tool would call a weather API here. Ok(ToolResult { content: format!("72°F and sunny in {city}"), is_error: false }) }}
fn user_message(text: &str) -> ConversationMessage { ConversationMessage { role: Role::User, content: vec![ContentBlock::Text { text: text.into() }] }}
#[tokio::main]async fn main() -> Result<(), KovaError> { let config = OpenAiProviderConfig::new("http://127.0.0.1:1234", "my-model"); let provider = Arc::new(OpenAiCompatibleProvider::new(config)?);
let agent = AgentBuilder::new() .provider(provider) .tool(Arc::new(GetWeather)) // register the tool .build()?;
let history = vec![user_message("What's the weather in Tokyo?")]; let response = agent.run(&history).await?; println!("{}", response.text); Ok(())}When you run this, the agent:
- sends your question and the tool’s JSON-schema definition to the model,
- the model responds “call
get_weatherwith{ "city": "Tokyo" }”, - Kova executes
GetWeather::execute, - feeds the result back to the model, which writes the final sentence.
You wrote the tool and one .tool(...) line — Kova ran the whole loop. That
loop is the heart of the SDK; read The Agentic Loop
to understand exactly what happened.
Continue the conversation
Section titled “Continue the conversation”Kova holds no conversation state — you do. To ask a follow-up, keep the
new_messages from each turn and feed them back on the next call:
let mut history = vec![user_message("What's the weather in Tokyo?")];
let response = agent.run(&history).await?;println!("{} ({} tokens)", response.text, response.usage.total_tokens);history.extend(response.new_messages); // remember the turn
// follow-up — the model sees the earlier exchange because it's in `history`history.push(user_message("How about Paris?"));let response = agent.run(&history).await?;println!("{}", response.text);history is a plain Vec<ConversationMessage>. Persist it wherever you like —
a database row, a Redis key, a request/response cycle in a web server. This is
what makes Kova agents cheap to scale horizontally: nothing lives in the agent.
See Conversations & History for the patterns.
Stream the output
Section titled “Stream the output”To render tokens as they arrive instead of waiting for the whole reply, use the pull-based event stream:
use futures::StreamExt;
let stream = agent.run_stream(&history);futures::pin_mut!(stream);while let Some(event) = stream.next().await { match event? { AgentEvent::TextDelta { text } => print!("{text}"), AgentEvent::ToolCallStarted { name, .. } => eprintln!("\n⚙ calling {name}…"), AgentEvent::TurnCompleted { response } => { history.extend(response.new_messages); } _ => {} }}Full details in Streaming.
Where to next
Section titled “Where to next”- The Agentic Loop — what actually
happens inside
run. - Configuring a Provider — use Anthropic, OpenAI, Bedrock, Gemini, or Ollama.
- Writing a Custom Tool — go deeper on
the
Tooltrait. - Structured Output — get typed JSON back instead of free text.
- A CLI Chatbot — a complete, copy-pasteable program.