---
title: "Rust SDK advanced"
description: "Expert-level direct agent construction, provider configuration, budgets, hooks, and runtime delivery internals."
icon: "gears"
---

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

<Note>
For most use cases, prefer `SessionService` via the runtime-backed persistent path (see
[overview](/rust/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.
</Note>

<Warning>
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.
</Warning>

```rust
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?;
```

<Accordion title="All builder methods">
| Method | Description | Default |
|--------|-------------|---------|
| `model(name)` | Set the model identifier | `"claude-opus-4-6"` |
| `system_prompt(prompt)` | Set the system prompt | None |
| `max_tokens_per_turn(n)` | Max tokens per LLM call | 8192 |
| `temperature(t)` | Sampling temperature (0.0-1.0) | None (model default) |
| `budget(limits)` | Set resource limits | Unlimited |
| `retry_policy(policy)` | Configure retry behavior | 3 retries with backoff |
| `resume_session(session)` | Resume from existing session | New session |
| `provider_params(json)` | Provider-specific parameters | None |
| `with_hook_engine(engine)` | Attach a hook engine | None |
| `with_hook_run_overrides(overrides)` | Run-scoped hook overrides | Empty |
</Accordion>

---

## Providers

Built-in clients for major LLM providers:

<Tabs>
  <Tab title="Anthropic">
    ```rust
    use meerkat::AnthropicClient;

    let client = AnthropicClient::new("sk-ant-...".to_string())?;
    let client = AnthropicClient::from_env()?;
    let client = AnthropicClient::new("key".to_string())?
        .with_base_url("https://my-proxy.example.com".to_string());
    ```
  </Tab>
  <Tab title="OpenAI">
    ```rust
    use meerkat::OpenAiClient;

    let client = OpenAiClient::new("sk-...".to_string());
    let client = OpenAiClient::from_env()?;
    let client = OpenAiClient::new("key".to_string())
        .with_base_url("https://my-deployment.openai.azure.com".to_string());
    ```
  </Tab>
  <Tab title="Gemini">
    ```rust
    use meerkat::GeminiClient;

    let client = GeminiClient::new("...".to_string());
    let client = GeminiClient::from_env()?;
    ```
  </Tab>
</Tabs>

### Provider parameters

Pass provider-specific options via `AgentBuildConfig`:

<Tabs>
  <Tab title="Anthropic">
    ```rust
    let mut build_config = AgentBuildConfig::new("claude-sonnet-4-6".into());
    build_config.provider_params = Some(json!({"thinking_budget": 10000}));
    ```
  </Tab>
  <Tab title="OpenAI">
    ```rust
    let mut build_config = AgentBuildConfig::new("gpt-5.5".into());
    build_config.provider_params = Some(json!({"reasoning_effort": "high", "seed": 42}));
    ```
  </Tab>
  <Tab title="Gemini">
    ```rust
    let mut build_config = AgentBuildConfig::new("gemini-3.5-flash".into());
    build_config.provider_params = Some(json!({"thinking_budget": 8000, "top_k": 40}));
    ```
  </Tab>
</Tabs>

<Accordion title="Implementing a custom LLM client">

```rust
use async_trait::async_trait;
use meerkat::{AgentLlmClient, AgentError, LlmStreamResult, Message, ToolDef, StopReason, Usage};
use meerkat::AssistantBlock;
use serde_json::Value;
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<&Value>,
    ) -> 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) -> &'static str { "my-provider" }
    fn model(&self) -> &str { "my-model" }
}
```
</Accordion>

---

## Budget configuration

```rust
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 = Some(budget);
```

## Retry configuration

```rust
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,
};
```

---

## 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

```rust
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()
let layered_hooks = resolve_layered_hooks_config(&cwd, &config).await;
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 `EventInjector`, and `inject()` queues a runtime-backed external event for later admission.

```rust
use meerkat::{EventInjector, PlainEventSource};

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

injector.inject(
    "review PR #42".into(),
    PlainEventSource::Rpc,
)?;
```

<Accordion title="How it works">

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.
</Accordion>

<Accordion title="SSE streaming example">

```rust
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, PlainEventSource::Rpc)?;

    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;
                }
                _ => {}
            }
        }
    })
}
```
</Accordion>

---

## Complete example

```rust
use meerkat::{AgentFactory, Config, build_ephemeral_service};
use meerkat::{
    CreateSessionRequest, DeferredPromptPolicy, SessionService,
    StartTurnRequest, StartTurnRuntimeSemantics,
};
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(),
        render_metadata: None,
        system_prompt: Some("You are a helpful math assistant.".into()),
        max_tokens: Some(2048),
        event_tx: None,
        skill_references: 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.

- **Python**: [Python SDK overview](/sdks/python/overview)
- **TypeScript**: [TypeScript SDK overview](/sdks/typescript/overview)

---

## See also

- [Rust SDK overview](/rust/overview) - getting started, sessions, events
- [Tools and stores](/rust/tools-and-stores) - tool system, stores, MCP
- [API reference](/reference/api-reference) - type index
- [Architecture](/reference/architecture) - system design and internals
