Skip to content

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.

  1. Create a project and add the crate.

    Terminal window
    cargo new my-agent && cd my-agent
    cargo add kova-sdk
    cargo add tokio --features macros,rt-multi-thread
  2. 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(())
    }
  3. Run it.

    Terminal window
    cargo run

That’s a complete agent. run took your history, sent it to the model, and returned an AgentResponseresponse.text is the reply, and response.new_messages holds everything the turn produced, ready for you to keep.

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:

src/main.rs
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:

  1. sends your question and the tool’s JSON-schema definition to the model,
  2. the model responds “call get_weather with { "city": "Tokyo" }”,
  3. Kova executes GetWeather::execute,
  4. 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.

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.

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.