SessionService, which keeps Rust aligned with the same session lifecycle semantics used by every other Meerkat surface while still avoiding subprocess or JSON-RPC overhead.
Method overview
| Area | Method / Type | Purpose |
|---|---|---|
| Setup | Config::load() | Load configuration from disk |
AgentFactory::new(store_root) | Create a factory for building agents | |
open_realm_persistence_in(...) | Open a realm-backed persistence bundle | |
build_persistent_service(factory, config, cap, persistence) | Build the runtime-backed persistent session substrate | |
build_ephemeral_service(factory, config, cap) | Build an in-memory Queue-only substrate | |
| Sessions | service.create_session(req) | Create a session and run the first turn |
service.start_turn(id, req) | Continue an existing session | |
service.read(id) | Read session state | |
service.list(query) | List active sessions | |
service.archive(id) | Remove a session | |
| Agent | agent.run(prompt) | Run agent with a prompt |
agent.run_with_events(prompt, tx) | Run with event streaming | |
agent.cancel() | Cancel the current run |
Installation
1
Add the dependency
[dependencies]
meerkat = "0.8.22"
tokio = { version = "1", features = ["full"] }
2
Choose feature flags
The default includes all three LLM providers and nothing else — add subsystems as needed:
[dependencies]
meerkat = "0.8.22"
[dependencies]
meerkat = { version = "0.8.22", features = ["sqlite-store", "session-store"] }
[dependencies]
meerkat = { version = "0.8.22", features = [
"sqlite-store", "session-store", "session-compaction",
"memory-store-session", "comms", "mcp", "skills", "live"
] }
[dependencies]
meerkat = { version = "0.8.22", default-features = false, features = ["anthropic"] }
Feature flag reference
Feature flag reference
| Feature | Description | Default |
|---|---|---|
anthropic | Anthropic Claude API client | Yes |
openai | OpenAI API client | Yes |
gemini | Google Gemini API client | Yes |
all-providers | Shorthand for all three providers | No |
sqlite-store | SQLite-backed persistent realms | No |
jsonl-store | File-based session persistence | No |
memory-store | In-memory session storage (testing) | No |
session-store | Persistent session lifecycle support | No |
session-compaction | Auto-compact long conversations | No |
memory-store-session | Semantic memory indexing | No |
comms | Ed25519 inter-agent messaging | No |
mcp | MCP protocol client and tool routing | No |
schedule | Durable scheduler surfaces | No |
workgraph | WorkGraph goal and attention surfaces | No |
live | Live-channel orchestration and realtime adapter helpers | No |
skills | Composable knowledge packs | No |
Quick start
Production surfaces (CLI, REST, RPC, MCP) use the runtime-backed path whereSessionService is substrate and MeerkatMachine owns keep-alive, Queue/Steer routing, and comms drain. See the JSON-RPC API or REST API for those entry points.
For the main production embedding path, use the runtime-backed persistent flow:
use meerkat::{
AgentFactory, Config, CreateSessionRequest, SessionService,
SystemPromptOverride, build_persistent_service, open_realm_persistence_in,
DeferredPromptPolicy,
};
use meerkat_core::service::InitialTurnPolicy;
use meerkat_store::RealmBackend;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = Config::load().await?;
let realms_root = std::env::current_dir()?.join(".rkat").join("realms");
let (_manifest, persistence) = open_realm_persistence_in(
&realms_root,
"team-alpha",
Some(RealmBackend::Sqlite),
None,
).await?;
let factory = AgentFactory::new(realms_root.clone()).runtime_root(realms_root);
let service = build_persistent_service(factory, config, 64, persistence);
let result = service.create_session(CreateSessionRequest {
model: "claude-sonnet-4-6".into(),
prompt: "What is the capital of France?".into(),
system_prompt: SystemPromptOverride::Set("You are a helpful assistant.".into()),
max_tokens: Some(1024),
event_tx: None,
initial_turn: InitialTurnPolicy::RunImmediately,
deferred_prompt_policy: DeferredPromptPolicy::Discard,
build: None,
labels: None,
}).await?;
println!("Response: {}", result.text);
println!("Session ID: {}", result.session_id);
Ok(())
}
build_ephemeral_service remains available as a direct Queue-only substrate.
Sessions
SessionService is the canonical lifecycle API. All surfaces (CLI, REST, MCP, RPC) route through it.
Multi-turn conversations
use meerkat::{
CreateSessionRequest, DeferredPromptPolicy, SessionService,
StartTurnRequest, StartTurnRuntimeSemantics, SystemPromptOverride,
};
use meerkat_core::service::InitialTurnPolicy;
// Turn 1: create session
let result = service.create_session(CreateSessionRequest {
model: "claude-sonnet-4-6".into(),
prompt: "My name is Alice.".into(),
system_prompt: SystemPromptOverride::Set("You are a helpful assistant with memory.".into()),
max_tokens: None,
event_tx: None,
initial_turn: InitialTurnPolicy::RunImmediately,
deferred_prompt_policy: DeferredPromptPolicy::Discard,
build: None,
labels: None,
}).await?;
let session_id = result.session_id;
// Turn 2: agent remembers "Alice"
let result = service.start_turn(&session_id, StartTurnRequest {
prompt: "What's my name?".into(),
system_prompt: None,
event_tx: None,
runtime: StartTurnRuntimeSemantics::default(),
}).await?;
// Read session state
let view = service.read(&session_id).await?;
println!("Messages: {}", view.state.message_count);
// Archive when done
service.archive(&session_id).await?;
Append a System message at a turn boundary
use meerkat::{
CreateSessionRequest, DeferredPromptPolicy, SessionService,
StartTurnRequest, StartTurnRuntimeSemantics, SystemPromptOverride,
};
use meerkat_core::service::InitialTurnPolicy;
let created = service.create_session(CreateSessionRequest {
model: "claude-sonnet-4-6".into(),
prompt: "Let's wait before we run.".into(),
system_prompt: SystemPromptOverride::Set("You are the original prompt.".into()),
max_tokens: None,
event_tx: None,
initial_turn: InitialTurnPolicy::Defer,
deferred_prompt_policy: DeferredPromptPolicy::Stage,
build: None,
labels: None,
}).await?;
let result = service.start_turn(&created.session_id, StartTurnRequest {
prompt: "Continue under the new instruction.".into(),
system_prompt: Some("For this point forward, answer concisely.".into()),
event_tx: None,
runtime: StartTurnRuntimeSemantics::default(),
}).await?;
system_prompt appends one ordinary ordered System message immediately
before that turn. It may be used on any turn; prior System messages and the
rest of the transcript remain unchanged.
Error handling
use meerkat::SessionError;
match service.start_turn(&id, req).await {
Ok(result) => println!("Response: {}", result.text),
Err(SessionError::NotFound { id }) => println!("Session {} not found", id),
Err(SessionError::Busy { id }) => println!("Session {} is busy, retry later", id),
Err(e) => println!("Error: {}", e),
}
Direct agent APIs
Agent::run(...) and AgentBuilder are expert-level escape hatches. Prefer SessionService for normal embedding so your Rust code follows the same runtime-backed session semantics as CLI, REST, RPC, MCP, Python, and TypeScript.Running agents directly
Basic run
let result = agent.run("What is 2 + 2?".into()).await?;
println!("Answer: {}", result.text);
Run with event streaming
use tokio::sync::mpsc;
use meerkat::AgentEvent;
let (tx, mut rx) = mpsc::channel::<AgentEvent>(100);
tokio::spawn(async move {
while let Some(event) = rx.recv().await {
match event {
AgentEvent::TextDelta { delta } => print!("{}", delta),
AgentEvent::ToolExecutionStarted { name, .. } => {
println!("[Calling {}...]", name);
}
AgentEvent::TurnCompleted { usage, .. } => {
println!("\n[Tokens: {}]", usage.total_tokens());
}
_ => {}
}
}
});
let result = agent.run_with_events("Tell me a story".into(), tx).await?;
Agent methods
| Method | Description |
|---|---|
run(prompt) | Run agent with a ContentInput prompt (text or multimodal) |
run_with_events(prompt, tx) | Run with event streaming; prompt is ContentInput |
session() | Get current session (read-only) |
budget() | Get current budget tracker |
state() | Get current loop state |
cancel() | Cancel the current run |
Error handling
use meerkat::AgentError;
match agent.run("prompt".into()).await {
Ok(result) => println!("Success: {}", result.text),
Err(AgentError::Llm { provider, message, .. }) => {
println!("LLM error ({}): {}", provider, message);
}
Err(AgentError::TokenBudgetExceeded { used, limit }) => {
println!("Token budget exceeded: {} / {}", used, limit);
}
Err(e) => println!("Other error: {}", e),
}
Events
All event types
All event types
use meerkat::AgentEvent;
match event {
// Session lifecycle
AgentEvent::RunStarted { session_id, input } => {}
AgentEvent::RunCompleted { session_id, result, structured_output, extraction_required, usage, .. } => {}
AgentEvent::RunFailed { session_id, error_report, .. } => {}
// Structured-output extraction (after a completed main run)
AgentEvent::ExtractionSucceeded { session_id, structured_output, schema_warnings } => {}
AgentEvent::ExtractionFailed { session_id, last_output, attempts, reason } => {}
// Hook lifecycle
AgentEvent::HookStarted { hook_id, point } => {}
AgentEvent::HookCompleted { hook_id, point, duration_ms } => {}
AgentEvent::HookFailed { hook_id, point, reason } => {}
AgentEvent::HookDenied { hook_id, point, reason_code, message, .. } => {}
// LLM interaction
AgentEvent::TurnStarted { turn_number } => {}
AgentEvent::ReasoningDelta { delta } => {}
AgentEvent::ReasoningComplete { content } => {}
AgentEvent::TextDelta { delta } => {}
AgentEvent::TextComplete { content } => {}
AgentEvent::ServerToolContent { id, kind, content } => {}
AgentEvent::AssistantImageAppended { image } => {}
AgentEvent::ToolCallRequested { id, name, args } => {}
AgentEvent::ToolResultReceived { id, name, content, is_error } => {}
AgentEvent::TurnCompleted { stop_reason, usage } => {}
// Tool execution
AgentEvent::ToolExecutionStarted { id, name } => {}
AgentEvent::ToolExecutionCompleted { id, name, content, is_error, duration_ms } => {}
AgentEvent::ToolExecutionTimedOut { id, name, timeout_ms } => {}
// Compaction
AgentEvent::CompactionStarted { input_tokens, estimated_history_tokens, message_count } => {}
AgentEvent::CompactionCompleted { summary_tokens, messages_before, messages_after } => {}
AgentEvent::CompactionFailed { reason } => {}
// Budget
AgentEvent::BudgetWarning { budget_type, used, limit, percent } => {}
// Retry (typed LlmRetrySchedule owns failure kind + attempt/delay plan)
AgentEvent::Retrying { retry } => {}
// Skills
AgentEvent::SkillsResolved { skills, injection_bytes } => {}
AgentEvent::SkillResolutionFailed { skill_key, reason } => {}
// Comms interaction lifecycle
AgentEvent::InteractionComplete { interaction_id, result, .. } => {}
AgentEvent::InteractionCallbackPending { interaction_id, tool_name, args } => {}
AgentEvent::InteractionFailed { interaction_id, reason } => {}
AgentEvent::StreamTruncated { reason } => {}
// Tool config changes
AgentEvent::ToolConfigChanged { payload } => {}
// Background jobs and transcript rewrites
AgentEvent::BackgroundJobCompleted { job_id, display_name, terminal_status, detail } => {}
AgentEvent::TranscriptRewriteCommitted { session_id, record } => {}
_ => {} // non_exhaustive: forward compatibility
}
Core types
Message and ContentBlock
use meerkat::{Message, UserMessage, BlockAssistantMessage, SystemMessage, ToolResult};
use meerkat_core::{ContentBlock, ImageData};
let system = Message::System(SystemMessage::new("You are helpful."));
// Text-only user message (convenience)
let user = Message::User(UserMessage::text("Hello!"));
// Multimodal user message with text and image
let user = Message::User(UserMessage::with_blocks(vec![
ContentBlock::Text { text: "What is in this image?".to_string() },
ContentBlock::Image {
media_type: "image/png".to_string(),
data: ImageData::Inline { data: base64_data },
},
]));
ContentInput
ContentInput is the prompt type accepted by CreateSessionRequest and StartTurnRequest. It supports both text-only and multimodal prompts:
use meerkat_core::ContentInput;
// Text-only (most common) — implements From<&str> and From<String>
let prompt: ContentInput = "What is Rust?".into();
// Multimodal — blocks with mixed content types
let prompt = ContentInput::Blocks(vec![
ContentBlock::Text { text: "Describe this image.".to_string() },
ContentBlock::Image {
media_type: "image/jpeg".to_string(),
data: ImageData::Inline { data: base64_data },
},
]);
ToolCall and ToolResult
use meerkat::{ToolCall, ToolResult};
let tool_call = ToolCall {
id: "tc_123".to_string(),
name: "get_weather".to_string(),
args: json!({"city": "Tokyo"}),
};
let result = ToolResult::new("tc_123".to_string(), "Sunny, 25C".to_string(), false);
let error = ToolResult::new("tc_123".to_string(), "City not found".to_string(), true);
RunResult
let result: RunResult = agent.run("Hello".into()).await?;
println!("Response: {}", result.text);
println!("Session: {}", result.session_id);
println!("Tokens: {}", result.usage.total_tokens());
println!("Turns: {}", result.turns);
println!("Tool calls: {}", result.tool_calls);
See also
- Tools and stores - tool system, session stores, MCP integration
- Advanced - expert-only direct agent construction, providers, budgets, and hooks
- API reference - quick-lookup type index
