uarp-sdk 0.5.12

Async Rust client for the UARP (Snaga) Universal Agent Runtime Platform API
Documentation

uarp-sdk

Async Rust client for the UARP — Universal Agent Runtime Platform API. Full coverage of all 557 endpoints, built on reqwest, serde and tokio.

[dependencies]
uarp-sdk = "0.3"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
futures-util = "0.3"   # only if you use streams

Rust 1.88+. TLS is rustls by default; features = ["native-tls"] switches.

Quick start

use uarp_sdk::models::CreateAgentRequest;

#[tokio::main]
async fn main() -> Result<(), uarp_sdk::Error> {
    let client = uarp_sdk::Client::from_env()?;   // UARP_API_KEY, UARP_BASE_URL

    let agent = client
        .agents()
        // The platform selects the model itself, so a create is just a name.
        .create(&CreateAgentRequest { name: "demo".into(), ..Default::default() })
        .await?;

    println!("{} {}", agent.agent_id, agent.name);
    Ok(())
}

Getting a key. Sign in at https://snaga.ai and create one in your tenant's settings. A key looks like uarp_<prefix>_<secret>; the secret half is shown once and never again. With a key that carries tenants:write you can mint more through POST /api/v1/tenants/me/keys. Give each one the narrowest set of scopes that does its job.

Client is cheap to clone — every clone shares one connection pool. Resource groups are accessor methods: client.agents(), client.runs(), client.sessions(), … 43 in all, each in uarp_sdk::api.

Streaming

SSE endpoints return an EventStream, a futures::Stream that reconnects with Last-Event-ID:

use futures_util::StreamExt;

let runs = client.runs();
let params = Default::default();
let mut events = std::pin::pin!(runs.stream_run_events(&run_id, &params));

while let Some(event) = events.next().await {
    let event = event?;
    // The text arrives as `payload.delta`; the rest of the envelope is
    // platform bookkeeping.
    if event.event == "llm.chunk" {
        let chunk = event.json::<serde_json::Value>()?;
        if let Some(delta) = chunk["payload"]["delta"].as_str() {
            print!("{delta}");
        }
    }
    if event.event == "run.completed" { break; }   // dropping the stream closes it
}

Pagination

use futures_util::StreamExt;

let agents = client.agents();
let params = uarp_sdk::api::ListAgentsParams { limit: Some(100), ..Default::default() };
let mut all = std::pin::pin!(agents.list_all(&params));

while let Some(agent) = all.next().await {
    println!("{}", agent?.name);
}

list_all follows the cursor until the server reports no further pages, and stops if a server ever repeats a cursor.

An empty page does not end the walk. This API applies the page limit before filtering, so a page can come back with no items and more behind it — reading one as the end of the collection is what made 0.2.0 report empty lists. Three empty pages in a row do stop it.

Errors

use uarp_sdk::{ApiErrorKind, Error};

match client.agents().get("missing").await {
    Ok(agent) => println!("{}", agent.name),
    Err(Error::Api(api)) => match api.kind() {
        ApiErrorKind::NotFound => println!("no such agent"),
        ApiErrorKind::UnprocessableEntity => println!("{:?}", api.problem.errors),
        ApiErrorKind::RateLimit => println!("retry after {:?}", api.retry_after_seconds()),
        _ => println!("{api}"),
    },
    Err(Error::Timeout) => println!("timed out"),
    Err(other) => println!("{other}"),
}

ApiError carries the status, the parsed problem document, the response headers, and correlation_id() for support tickets.

Configuration

let client = uarp_sdk::Client::builder()
    .api_key(key)
    .base_url("http://localhost:8080")
    .timeout(std::time::Duration::from_secs(30))
    .max_retries(3)
    .user_agent("my-app/1.2.3")
    .default_header("X-Tenant", "acme")?
    .http_client(my_reqwest_client)   // proxies, tracing, custom TLS
    .build()?;

Per-call overrides

Rust has no default arguments, so rather than an options parameter on all 557 methods the overrides ride on a cheap clone of the client — the connection pool is shared:

client.with_idempotency_key("order-4711").agents().create(&body).await?;
client.with_timeout(Duration::from_secs(5)).agents().get(id).await?;
client.with_max_retries(0).agents().get(id).await?;
client.with_header("X-Trace", trace_id).agents().list(&params).await?;

// Reconnection behaviour for an event stream:
let quiet = client.with_stream_options(StreamOptions { reconnect: false, ..Default::default() });
let mut events = std::pin::pin!(quiet.runs().stream_run_events(&run_id, &Default::default()));

with_options(RequestOptions { .. }) sets several at once.

Retries. 408, 409, 429, 500, 502, 503 and 504, plus connection errors, retry with full-jitter backoff (500 ms → 8 s) and honour Retry-After. Reads always retry; writes only when they carry an idempotency key, which every mutating /api/v1/* call sends automatically.

Escape hatch

let value: serde_json::Value = client
    .raw(reqwest::Method::POST, "/api/v1/experimental/thing", Some(&payload))
    .await?;

Notes

  • Fields the spec marks required are plain values; everything else is Option<T>. Unknown response fields are ignored, except on models that declare additionalProperties, which keep them in extra.
  • Enums are real enums with an Other(String) catch-all, so a value the server adds later round-trips unchanged. as_str(), Display and From<&str> are provided.
  • The three oneOf bodies in the spec are exposed as serde_json::Value.
  • Timestamps are String (ISO-8601); the crate does not pull in a date library.

Development

cargo test --all-features
cargo clippy --all-targets
cargo run --example quickstart

Files under src/generated/ come from generator/ in the repository root; edit the emitter, not the output.