Skip to content

About

Swift client for the UARP (Snaga) Universal Agent Runtime Platform API. Mirror of packages/swift from Snaga-AI/uarp-sdks — open issues and pull requests there.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

20 Commits

Folders and files

Repository files navigation

UARPSDK (Swift)

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")]),
]

Platforms

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.

Quick start

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.

Streaming

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" }

Streaming a POST

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.

Pagination

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.

Errors

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")
}

Configuration

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.

Notes

  • Enums are RawRepresentable structs with static constants rather than Swift enums, so a value the API adds later decodes instead of throwing. Compare with ==, list the known ones with .knownValues.
  • Models that declare additionalProperties keep unmodelled keys in additionalProperties: [String: JSONValue].
  • A model named Error in the spec is emitted as ErrorModel so it does not shadow Swift.Error; the same rule applies to any other reserved name.
  • Timestamps are ISO-8601 Strings, not Dates.
  • Pass sseTokenInQuery: true when a proxy strips the Authorization header from event-stream requests.
  • sendRaw(_ spec:) returns the raw (Data, HTTPURLResponse) without throwing on a non-2xx, retrying transient failures as send does. Use it when you decode responses through your own decoder and need an error body that send/sendData discard — e.g. an endpoint that refuses with a bare {"error":"…"}. An empty apiKey sends no Authorization header, so a guest client carries no credentials.

Development

swift build
swift test
swift run uarp-example

Files 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.

About

Swift client for the UARP (Snaga) Universal Agent Runtime Platform API. Mirror of packages/swift from Snaga-AI/uarp-sdks — open issues and pull requests there.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages