Skip to main content
Advanced Rust SDK usage for the cases where the runtime-backed SessionService path is intentionally not enough. This page covers expert-level direct agent construction, provider configuration, budgets, hook helpers, and lower-level delivery internals.

Runtime-backed vs standalone builds

The Rust contract is explicit:
  • runtime-backed surfaces should call MeerkatMachine::prepare_bindings(session_id)
  • those bindings should be threaded through SessionBuildOptions.runtime_build_mode = RuntimeBuildMode::SessionOwned(bindings)
  • standalone/testing/embedded paths should use RuntimeBuildMode::StandaloneEphemeral
That split matters because runtime-backed bindings carry the session’s runtime epoch, canonical ops lifecycle registry, and shared cursor state used for recovery-safe background completion handling.

AgentBuilder vs AgentFactory

meerkat::AgentBuilder is the public facade builder. It accepts explicit client/tool/store overrides for embedded use, then still routes through AgentFactory::build_agent() so provider defaults, session metadata, hook wiring, and runtime build-mode defaults stay aligned with the factory path. AgentFactory (in the meerkat facade) is the opinionated composition layer. It knows about tool categories, runtime resources, comms, memory, mobs, and skills, and wires them into the dispatcher before passing them to AgentBuilder. All public surfaces go through the runtime-backed session path built on top of AgentFactory. The lower-level meerkat_core::AgentBuilder is not re-exported by the facade. Its standalone build path is reserved for core tests; public embedding code should use the facade builder or AgentFactory. Use AgentFactory via build_persistent_service() (main path), build_ephemeral_service() (substrate/testing path), or the public meerkat::AgentBuilder facade unless you explicitly need to bypass session lifecycle orchestration. Direct core builder usage means you own composition, persistence, event handling, and any drift from the standard runtime-backed path. If you are building a runtime-backed surface around SessionService, avoid the old hand-rolled register_session() + registry extraction pattern. Prefer the binding seam above so the runtime/session owner stays canonical.

AgentBuilder

For most use cases, prefer SessionService via the runtime-backed persistent path (see overview). The public AgentBuilder delegates to the factory pipeline. Use the core builder only inside core-owned tests that bypass the session service and facade composition entirely.
If you are embedding Meerkat into an application, start with SessionService. Use the facade builder or AgentFactory when you need direct Rust construction. Facade AgentBuilder rejects direct provider_tool_defaults, compactor, memory_store, and with_turn_state_handle injection so those standalone-only behaviors cannot be silently dropped.
use meerkat::AgentBuilder;

let mut agent = AgentBuilder::new()
    .model("claude-sonnet-4-6")
    .system_prompt("You are a helpful assistant.")
    .max_tokens_per_turn(4096)
    .temperature(0.7)
    .build(llm_client, tool_dispatcher, session_store)
    .await?;
MethodDescriptionDefault
model(name)Set the model identifierConfig default (config.agent.model); build(client, ...) falls back to client.model()
provider(provider)Set an explicit typed providerResolved from the model registry
system_prompt(prompt)Set an explicit per-request prompt (SystemPromptOverride::Set)Inherit (config/AGENTS.md/default chain)
disable_system_prompt()Suppress every prompt source (SystemPromptOverride::Disable)Inherit
max_tokens_per_turn(n)Max tokens per LLM callConfig max_tokens
temperature(t)Sampling temperature (0.0-1.0)None (model default)
budget(limits)Set resource limitsUnlimited
retry_policy(policy)Configure retry behavior3 retries with backoff
resume_session(session)Resume from existing sessionNew session
provider_params(params)Typed ProviderParamsOverride for this buildNone
output_schema(schema) / structured_output_retries(n)Structured-output extraction settingsNone / canonical default
with_hook_engine(engine)Attach a hook engineNone
with_hook_run_overrides(overrides)Run-scoped hook overridesEmpty

Providers

Built-in clients for major LLM providers:
use meerkat::AnthropicClient;

let client = AnthropicClient::new("sk-ant-...".to_string())?;
let client = AnthropicClient::new("key".to_string())?
    .with_base_url("https://my-proxy.example.com".to_string());

Provider parameters

Pass provider parameter overrides via AgentBuildConfig. The JSON bag is retired: provider_params is the typed ProviderParamsOverride (temperature, top_p, max_output_tokens, reasoning mode, thinking_budget_tokens, and an optional provider-specific provider_tag), parsed fail-closed at each surface ingress. Provider tags also carry opt-in provider cache hints: OpenAI can override Responses store (default remains false) and set prompt_cache_key / prompt_cache_retention; Anthropic can mark the stable system prefix with cache_control; Gemini can pass an explicit cached_content_name. When OpenAI store is explicitly enabled, stored response IDs may be reused as previous_response_id hints; Meerkat still keeps the local transcript as the canonical replay source and falls back to full replay if the provider rejects the hint.
use meerkat_core::lifecycle::run_primitive::ProviderParamsOverride;

let mut build_config = AgentBuildConfig::new("claude-sonnet-4-6".into());
build_config.provider_params = Some(ProviderParamsOverride {
    thinking_budget_tokens: Some(10_000),
    ..Default::default()
});
use async_trait::async_trait;
use meerkat::{AgentLlmClient, AgentError, LlmStreamResult, Message, ToolDef, StopReason, Usage};
use meerkat::AssistantBlock;
use meerkat_core::Provider;
use meerkat_core::lifecycle::run_primitive::ProviderParamsOverride;
use std::sync::Arc;

struct MyCustomClient { api_key: String }

#[async_trait]
impl AgentLlmClient for MyCustomClient {
    async fn stream_response(
        &self,
        messages: &[Message],
        tools: &[Arc<ToolDef>],
        max_tokens: u32,
        temperature: Option<f32>,
        provider_params: Option<&ProviderParamsOverride>,
    ) -> Result<LlmStreamResult, AgentError> {
        // Call your LLM API here
        Ok(LlmStreamResult::new(
            vec![AssistantBlock::Text { text: "Response text".to_string(), meta: None }],
            StopReason::EndTurn,
            Usage { input_tokens: 10, output_tokens: 20, ..Default::default() },
        ))
    }

    fn provider(&self) -> Provider { Provider::Other }
    fn model(&self) -> &str { "my-model" }
}

Budget configuration

use meerkat::BudgetLimits;
use std::time::Duration;

let budget = BudgetLimits::default()
    .with_max_tokens(100_000)
    .with_max_duration(Duration::from_secs(300))
    .with_max_tool_calls(50);

let mut build_config = AgentBuildConfig::new("claude-sonnet-4-6".into());
build_config.budget_limits = Some(budget);

Retry configuration

use meerkat::RetryPolicy;
use std::time::Duration;

let retry = RetryPolicy {
    max_retries: 5,
    initial_delay: Duration::from_millis(500),
    max_delay: Duration::from_secs(30),
    multiplier: 2.0,
    // Hard per-call LLM timeout; `None` inherits the model-profile default.
    call_timeout: None,
};

When to bypass SessionService

Direct agent construction still makes sense when you need one of these expert-only scenarios:
  • You are embedding a single transient agent loop with fully custom persistence and tool composition.
  • You are testing a lower-level trait contract in isolation from session orchestration.
  • You are implementing infrastructure that itself sits underneath a SessionService.
For ordinary product code, prefer SessionService so runtime admission, history, interruption, and external-event handling stay canonical.

Hook helpers

use meerkat::{create_default_hook_engine, resolve_layered_hooks_config};

let config = meerkat::Config::load().await?;
let cwd = std::env::current_dir()?;

// Called internally by AgentFactory::build_agent().
// Layers user-root hooks under project (context-root) hooks.
let layered_hooks = resolve_layered_hooks_config(Some(&cwd), None, &config).await;
// Returns None when no hooks are configured.
let hook_engine = create_default_hook_engine(layered_hooks);

Comms delivery and observation

Public comms injection is a delivery API. event_injector() gives you an Arc<dyn EventInjector>, and inject() queues a runtime-backed external event for later admission.
use meerkat_core::{EventInjector, HandlingMode, PlainEventSource};

let injector = service.event_injector(&session_id).await
    .expect("comms must be enabled");

injector.inject(
    "review PR #42".into(),
    PlainEventSource::Rpc,
    HandlingMode::Queue,
    None, // render metadata
)?;
  1. inject() queues a plain event into the session inbox.
  2. The runtime-backed surface admits that queued event as future turn work.
  3. Observation is selected independently:
    • use the session’s primary event_tx for full session activity,
    • use mob member subscriptions for agent-scoped activity,
    • use mob event subscriptions for attributed mob-wide activity.
  4. Public callers no longer receive an interaction-scoped stream from injection.
This distinction matters: queueing an event and consuming it are no longer treated as the same seam. The keep-alive/drain lifecycle owns consumption truth.Requirements:
  • Agent must be in keep-alive mode if you want the keep-alive loop to drain inbox events.
  • Comms must be enabled.
  • If you need streaming output, configure an observation surface separately.
async fn handle_chat(
    session_id: SessionId,
    user_msg: String,
    session_events: tokio::sync::broadcast::Sender<SessionEvent>,
) -> impl IntoResponse {
    let injector = service.event_injector(&session_id).await.unwrap();
    let mut events = session_events.subscribe();
    injector.inject(user_msg.into(), PlainEventSource::Rpc, HandlingMode::Queue, None)?;

    Sse::new(async_stream::stream! {
        while let Ok(session_event) = events.recv().await {
            match session_event.event.payload {
                AgentEvent::TextDelta { delta } => yield sse_event("delta", &delta),
                AgentEvent::RunCompleted { ref result, .. } => {
                    yield sse_event("done", result);
                    break;
                }
                AgentEvent::RunFailed { ref error, .. } => {
                    yield sse_event("error", error);
                    break;
                }
                _ => {}
            }
        }
    })
}

Complete example

use meerkat::{AgentFactory, Config, build_ephemeral_service};
use meerkat::{
    CreateSessionRequest, DeferredPromptPolicy, SessionService,
    StartTurnRequest, StartTurnRuntimeSemantics, SystemPromptOverride,
};
use meerkat_core::service::InitialTurnPolicy;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = Config::load().await?;
    let factory = AgentFactory::new(std::env::current_dir()?);
    let service = build_ephemeral_service(factory, config, 64); // embedded/testing substrate only

    let result = service.create_session(CreateSessionRequest {
        model: "claude-sonnet-4-6".into(),
        prompt: "What is 25 * 17?".into(),
        system_prompt: SystemPromptOverride::Set("You are a helpful math assistant.".into()),
        max_tokens: Some(2048),
        event_tx: None,
        initial_turn: InitialTurnPolicy::RunImmediately,
        deferred_prompt_policy: DeferredPromptPolicy::Discard,
        build: None,
        labels: None,
    }).await?;

    println!("Response: {}", result.text);

    let result = service.start_turn(&result.session_id, StartTurnRequest {
        prompt: "Now divide that result by 5.".into(),
        system_prompt: None,
        event_tx: None,
        runtime: StartTurnRuntimeSemantics::default(),
    }).await?;

    println!("Follow-up: {}", result.text);

    let view = service.read(&result.session_id).await?;
    println!("Total tokens: {}", view.billing.total_tokens);

    Ok(())
}

Python and TypeScript SDKs

Both communicate with a local rkat-rpc subprocess over JSON-RPC 2.0 (with optional explicit realm/instance scoping) — no native bindings required.

See also