Skip to main content
Browse the documentation

SDKs

Streaming and pagination

Walk every page of a list with listAll, and follow a run's live events with a stream that reconnects on its own.

Pagination

Why a helper

List endpoints return one page at a time: an items array, has_more, and a cursor for the next page. The API applies the page limit before filtering, so a page can come back empty while more items wait behind it. Code that stops at the first empty page reports an empty list that is not empty. The SDK helpers get this right for you.

Every cursor-paginated operation has a second method, listAll (list_all in Python and Rust, List_All in Ada), that walks every page and yields items. It stops when the server says there are no more pages, when the cursor is null, or when a cursor repeats. An empty page does not stop it; three empty pages in a row do, so a server that never advances cannot loop forever.

Examples

for await (const agent of client.agents.listAll({ limit: 100 })) {
  console.log(agent.name);
}

// or collect with a cap
import { collect } from 'uarp-sdk';
const first500 = await collect(client.agents.listAll(), 500);
for agent in client.agents.list_all(limit=100):
    print(agent.name)

On AsyncClient it is an async iterator: async for agent in client.agents.list_all().

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);
}
client.agents.listAll(limit = 100).collect { agent ->
    println(agent.name)
}
for try await agent in client.agents.listAll(limit: 100) {
    print(agent.name)
}
Everything : constant UARP.Models.Agent_Vectors.Vector :=
  UARP.API.Agents.List_All (Client, Params, Max_Items => 500);

In Ada, List_All collects into a vector; Max_Items => 0 (the default) means no limit. limit is the page size, not a total: for GET /api/v1/agents it defaults to 20 and is clamped to 1–100.

Streaming

How event streams work

Some endpoints answer with server-sent events (SSE) instead of a single body. The one you will use most is GET /api/v1/runs/{runId}/events: the live event stream of a run, which closes when the run reaches a terminal status. Its last named event is done, whose data carries the run's final status and metrics; close on done, not on run.completed, which a finished run's stream may not contain. A literal data: [DONE] line follows, and the SDKs treat it as the end rather than as an event.

Every SDK returns these as a native stream: an AsyncIterable in TypeScript, an iterator (or async iterator) in Python, a futures::Stream in Rust, a cold Flow in Kotlin, an AsyncSequence in Swift, and a sink you derive from in Ada. When the connection ends, the SDK reopens it and sends the last event id it saw as Last-Event-ID, so you continue where you left off. A connection that delivered at least one event earns a fresh reconnect budget.

Reading a run's reply

The reply's text arrives in llm.chunk events, as payload.delta on chunks whose payload.chunk_type is content. Chunks of type thinking and tool_call carry the model's reasoning and tool calls, which are not the answer. Leaving the loop closes the request.

const stream = client.runs.streamRunEvents(runId);

for await (const event of stream) {
  if (event.event === 'llm.chunk') {
    const { payload } = event.json<{ payload: { chunk_type?: string; delta: string } }>();
    if ((payload.chunk_type ?? 'content') === 'content') process.stdout.write(payload.delta);
  }
  if (event.event === 'done') break;   // leaving the loop closes the request
}

// Or wait for one specific event:
await client.runs.streamRunEvents(runId).until((e) => e.event === 'done');
with client.runs.stream_run_events(run_id) as events:
    for event in events:
        if event.event == "llm.chunk":
            payload = event.json()["payload"]
            if payload.get("chunk_type", "content") == "content":
                print(payload.get("delta", ""), end="")
        if event.event == "done":
            break

In Rust, iterate client.runs().stream_run_events(&run_id, &params) with StreamExt::next; in Kotlin, collect client.runs.streamRunEvents(runId); in Swift, for try await event in client.runs.streamRunEvents(runId: id). Each package README has the full example.

Tuning reconnects

TypeScript and Python take stream options per call: whether to reconnect, how many attempts (five by default in TypeScript), which events end the stream, and an inactivity timeout.

from uarp_sdk import RequestOptions, StreamOptions

options = StreamOptions(
    terminal_events=frozenset({"done"}),
    inactivity_timeout=300.0,
)
for event in client.runs.stream_run_events(run_id, request_options=RequestOptions(stream=options)):
    print(event.event)

Browsers and proxies

A browser's EventSource cannot set an Authorization header. The API accepts a short-lived token as ?token= instead: mint one with POST /api/v1/auth/sse-tokens. Every SDK can put the key in the query for you, for proxies that strip the header: sseTokenInQuery: true in TypeScript and Swift, sse_token_in_query=True in Python, SSE_Token_In_Query => True in Ada.

Streaming an LLM completion

POST /api/v1/llm/chat/completions with "stream": true answers as SSE. Stream long completions: the platform cuts a silent, non-streamed request at 120 seconds. Use client.streamPost(path, body) (stream_post in Python and Rust). It makes exactly one attempt and never retries or reconnects, because replaying the POST would run, and bill, the model twice. The stream ends at data: [DONE], which is not delivered as an event.