Swift client for the UARP — Universal Agent Runtime Platform API. Every
operation the API describes, async/await throughout, no dependencies
beyond Foundation.
// Package.swift
dependencies: [
.package(url: "https://github.com/Snaga-AI/uarp-swift", from: "0.7.0"),
],
targets: [
.target(name: "App", dependencies: [.product(name: "UARPSDK", package: "uarp-swift")]),
]Built for Apple platforms: macOS 12+, iOS 15+, tvOS 15+, watchOS 8+, visionOS 1+, with Swift 5.9+.
The package also builds on Linux and every request/response operation works
there. The event-stream endpoints do not:
URLSession.bytes(for:) is missing from swift-corelibs-foundation, and every
other way of reading a response on that platform buffers it to completion, which
never happens on a stream that stays open. Calling one on Linux throws
UARPError.stream with that explanation rather than failing to compile.
import UARPSDK
let client = try UARPClient.fromEnvironment() // UARP_API_KEY (or SNAGA_API_KEY), UARP_BASE_URL
// or: UARPClient(apiKey: "uarp_...")
// The platform selects the model itself, so a create is just a name.
let agent = try await client.agents.create(body: CreateAgentRequest(name: "demo"))
let page = try await client.agents.list(limit: 20)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.
Resource groups are computed properties on the client: client.agents,
client.runs, client.sessions, … one for each tag in the
API description. Parameters are flattened into
labelled arguments with nil defaults, so only what you set is sent.
SSE endpoints return an EventStream, an AsyncSequence that reconnects with
Last-Event-ID:
// The reply's text is `payload.delta` on chunks whose `payload.chunk_type` is
// "content"; "thinking" and "tool_call" chunks carry the model's reasoning
// and tool calls, which are not the answer. The rest of the envelope is
// platform bookkeeping.
struct Chunk: Decodable {
struct Payload: Decodable { let chunk_type: String?; let delta: String }
let payload: Payload
}
for try await event in client.runs.streamRunEvents(runId: id) {
if event.event == "llm.chunk" {
let payload = try event.json(as: Chunk.self).payload
if (payload.chunk_type ?? "content") == "content" { print(payload.delta, terminator: "") }
}
if event.event == "run.completed" { break } // leaving the loop cancels the request
}
// Or wait for one event:
let done = try await client.runs.streamRunEvents(runId: id).until { $0.event == "run.completed" }client.streamPost(path:body:) sends a JSON body and reads the answer as an
event stream: use it for an LLM completion with "stream": true. The platform
cuts a silent non-streamed request at 120 s, while a streamed one stays open for
as long as the model writes.
let body: JSONObject = [
"model": "…",
"stream": true,
"messages": .array([.object(["role": "user", "content": "hi"])]),
]
for try await event in client.streamPost(path: "/api/v1/llm/chat/completions", body: body) {
let chunk = try event.json(as: JSONValue.self)
print(chunk["choices"]?.arrayValue?.first?["delta"]?["content"]?.stringValue ?? "", terminator: "")
}It makes exactly one attempt. It never reconnects and never retries, not even
on a 429, because replaying the POST would run and bill the model twice. The
stream ends at data: [DONE] (not delivered as an event), at the end of the
body, or when you leave the loop. A connection dropped mid-answer throws
instead of ending quietly. A refusal throws UARPError.api with the status and
problem document. So does a 2xx that is not text/event-stream, such as plain
JSON from a body without "stream": true. It carries the status as received,
so an answer with no events never reads as a finished stream.
for try await agent in client.agents.listAll(limit: 100) {
print(agent.name)
}
let firstPage = try await client.agents.listAll().collect(limit: 50)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, as does a repeated cursor.
do {
_ = try await client.agents.get(agentId: id)
} catch let UARPError.api(error) {
switch error.kind {
case .notFound: print("no such agent")
case .unprocessableEntity: print(error.validationErrors)
case .rateLimit: print(error.retryAfterSeconds ?? 0)
default: print(error.status, error.correlationId ?? "")
}
} catch UARPError.timeout {
print("timed out")
}var configuration = Configuration(apiKey: key)
configuration.baseURL = URL(string: "http://localhost:8080")!
configuration.timeout = 30
configuration.maxRetries = 3
configuration.defaultHeaders = ["X-Tenant": "acme"]
configuration.userAgentSuffix = "my-app/1.2.3"
let client = UARPClient(configuration: configuration, session: .shared)Per-call overrides use the trailing options: argument:
try await client.agents.create(
body: request,
options: RequestOptions(timeout: 5, maxRetries: 0, idempotencyKey: "order-4711")
)Retries. 408, 409, 429, 500, 502, 503 and 504, plus connection errors, retry with
full-jitter backoff (0.5 s → 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.
- Enums are
RawRepresentablestructs with static constants rather than Swiftenums, so a value the API adds later decodes instead of throwing. Compare with==, list the known ones with.knownValues. - Models that declare
additionalPropertieskeep unmodelled keys inadditionalProperties: [String: JSONValue]. - A model named
Errorin the spec is emitted asErrorModelso it does not shadowSwift.Error; the same rule applies to any other reserved name. - Timestamps are ISO-8601
Strings, notDates. - Pass
sseTokenInQuery: truewhen a proxy strips theAuthorizationheader from event-stream requests. sendRaw(_ spec:)returns the raw(Data, HTTPURLResponse)without throwing on a non-2xx, retrying transient failures assenddoes. Use it when you decode responses through your own decoder and need an error body thatsend/sendDatadiscard — e.g. an endpoint that refuses with a bare{"error":"…"}. An emptyapiKeysends noAuthorizationheader, so a guest client carries no credentials.
swift build
swift test
swift run uarp-exampleFiles under Sources/UARP/Generated/ come from generator/ in Snaga-AI/uarp-sdks;
edit the emitter, not the output.
Snaga-AI/uarp-swift is a mirror of packages/swift in that repository,
rewritten on every release. Issues and pull requests belong in
Snaga-AI/uarp-sdks; a change made in the mirror is overwritten by the next one.