> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rkat.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Rust SDK advanced

> Expert-level direct agent construction, provider configuration, budgets, hooks, and runtime delivery internals.

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 theme={null}
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 theme={null}
    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 theme={null}
    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 theme={null}
    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 theme={null}
    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 theme={null}
    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 theme={null}
    let mut build_config = AgentBuildConfig::new("gemini-3-flash-preview".into());
    build_config.provider_params = Some(json!({"thinking_budget": 8000, "top_k": 40}));
    ```
  </Tab>
</Tabs>

<Accordion title="Implementing a custom LLM client">
  ```rust theme={null}
  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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
  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 theme={null}
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
