# 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`.
```toml
[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
```rust
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`:
```rust
use futures_util::StreamExt;
let runs = client.runs();
let params = Default::default();
let mut events = std::pin::pin!(runs.stream_run_events(&run_id, ¶ms));
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
```rust
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(¶ms));
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
```rust
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
```rust
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:
```rust
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(¶ms).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
```rust
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
```sh
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.