ArtAdk

1.0.2

aiotrixdev/art-swift-adk

What's New

ART Swift ADK 1.0.2: File storage, notifications and conversation status

2026-09-24T12:44:22Z

This release adds file storage, file attachments for AI agents, live conversation status and a new, optional notifications library. It also makes message delivery more reliable and fixes several bugs.

Before you upgrade

  • If you switch over AgentEvent, handle the new .threadState case or add a default case.
  • push on targeted channels now waits for delivery confirmation. Don't block the UI while it waits, and handle ARTError.ackTimeout.

Added

  • File storage. Upload, list, fetch and delete files. A file can belong to your project, an agent, an orchestrator, a conversation thread or a channel. Uploads from disk are streamed and report their progress. If an upload fails, the error tells you which step failed and includes the HTTP status code.
  • Files for AI agents. Attach uploaded files to a message with thread.run(_:fileMeta:) so the agent can use them. Channel messages can carry files too, through PushConfig(fileMeta:).
  • Conversation status. Agent and orchestrator threads report their current status, such as queued, running, waiting for input, completed or failed. Follow it with listenState or read it with getState().
  • Complete event data. Agent events include every field the server sent, in AgentEventEnvelope.content.
  • Profiles. Update the signed-in user's profile with updateProfile(_:), and read or update the user's profile for a connector with connector(_:).
  • Notifications. The new, optional ArtAdkNotifications library receives notifications as they arrive, lists past notifications, marks them as read, sends notifications and registers devices for push notifications.
  • More ways to provide credentials. Pass credentials with setCredentials(_:), or load them automatically from adk-services.json with AdkConfig(autoLoadCredsFromJSON:).
  • REST call options. CallApiProps accepts a custom base URL and a timeout.
  • Removing a single listener. On regular channels, bind, listen, attachThreadListener and attachThreadBind return a token. Pass it to remove(event:id:) or detachThreadListener(_:_:id:) to remove that listener without affecting the others.

Changed

  • On targeted channels, push now waits until ART confirms delivery. If no confirmation arrives within 50 seconds, it throws ARTError.ackTimeout.
  • Messages on a channel are delivered to your listeners one at a time, in the order they arrive.
  • Adk.state reports .connected only after the server has accepted the connection, and .connecting while the ADK reconnects.
  • Access tokens are renewed 30 seconds before they expire, and requests made at the same time share a single renewal.
  • Agent threads include their thread ID in every message, and also receive replies addressed to that thread. New thread IDs are lowercase UUIDs.
  • Answers sent through the reply closure of a request for human input are encoded as JSON, so text answers arrive in quotes.
  • An interceptor that resolves with an array now sends the array unchanged.
  • adk-services.json is now loaded from the app bundle.
  • The art_notifications channel is now subscribed on the server. It's a broadcast channel, so notifications sent to subscribers now reach onNew.

Fixed

  • Agent responses were empty when the server sent their content as JSON text. The message, reference ID and question text are now read correctly.
  • pause() reconnected automatically after five seconds. The connection now stays paused until you call resume().
  • Subscription.thread() always failed on orchestrator-enabled channels.
  • Apps couldn't call the reply closure attached to a request for human input.
  • Presence updates on shared-object channels never reached fetchPresence.
  • Shared-object channels applied changes from messages other than updates.
  • Closing one orchestrator thread removed the trace listeners of other threads.
  • Data that can't be converted to JSON crashed the app. The ADK now throws ArtJSONError instead.

Deprecated

  • listenTrace on agent and orchestrator threads. Use listenState for status updates.

Installation

Swift Package Manager:

.package(url: "https://github.com/aiotrixdev/art-swift-adk.git", from: "1.0.2")

In Xcode, choose File → Add Package Dependencies…, paste https://github.com/aiotrixdev/art-swift-adk.git and select version 1.0.2. Add ArtAdkNotifications to your target only if you use notifications.

Full list of changes: CHANGELOG.md · Documentation: docs.arealtimetech.com

ART Swift ADK

Swift Platforms License

The ART Swift ADK connects iOS and macOS apps to ART (A Realtime Tech), a realtime communication platform for building intelligent applications with WebSocket-based messaging, AI Agents, AI Orchestrators, presence tracking, end-to-end encrypted channels, and CRDT-backed shared objects.

With the ADK you can:

  • Send and receive messages in real time
  • See who is online in a channel
  • Encrypt conversations end-to-end
  • Keep shared data in sync across users and devices
  • Check or block messages before they are delivered
  • Chat with AI agents and run multi-agent workflows
  • Upload files and share them with AI agents
  • Receive in-app notifications and register devices for push notifications

Contents

Requirements

  • iOS 15 or later, or macOS 13 or later
  • Xcode 15 or later (Swift 5.9)
  • An ART project with its environment, project key, organisation and client ID

Installation

Swift Package Manager

Add the package to your Package.swift:

dependencies: [
    .package(url: "https://github.com/aiotrixdev/art-swift-adk.git", from: "1.0.2")
]

Then add the required libraries to your target:

.target(
    name: "MyApp",
    dependencies: [
        .product(name: "ArtAdk", package: "art-swift-adk"),
        .product(name: "ArtAdkNotifications", package: "art-swift-adk") // optional
    ]
)

Xcode

  1. Choose File › Add Package Dependencies…
  2. Enter https://github.com/aiotrixdev/art-swift-adk.git.
  3. Add ArtAdk to your app target. Add ArtAdkNotifications only if your app uses notifications.

Getting started

1. Get a user passcode

ART signs users in with a short-lived passcode. Your backend requests the passcode from ART using your client secret, and returns only the passcode to the app:

func fetchPasscode(for username: String) async throws -> String {
    var request = URLRequest(url: URL(string: "PASSCODE_URL")!)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONSerialization.data(withJSONObject: ["username": username])

    let (data, _) = try await URLSession.shared.data(for: request)
    let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
    return json?["passcode"] as? String ?? ""
}

Important: Never put your client secret in the app. Anything shipped inside an app can be extracted. Whether users can sign in using only a passcode depends on your ART project settings, so confirm this with the ART team before release.

2. Create credentials and connect

import ArtAdk

let passcode = try await fetchPasscode(for: "john_doe")

let adk = Adk(config: AdkConfig(uri: "YOUR_WEBSOCKET_URI", authToken: passcode))

adk.setCredentials(CredentialStore(
    environment: "YOUR_ENV",
    projectKey:  "YOUR_PROJECT_KEY",
    orgTitle:    "YOUR_ORG",
    clientID:    "YOUR_CLIENT_ID",
    accessToken: passcode
))

await adk.connect()

3. Send and receive a message

let channel = try await adk.subscribe(channel: "CHANNEL_NAME")

channel.emitter.on("message") { data in
    print("Received:", data)
}

try await channel.push(event: "message", data: ["text": "Hello ART!"])

Other ways to provide credentials

setCredentials(_:) is the simplest option. You can also use one of these:

Option When to use it
AdkConfig(getCredentials: { … }) Your credentials change over time, for example when the passcode is refreshed. The ADK calls the closure each time it signs in.
AdkConfig(autoLoadCredsFromJSON: true) You keep credentials in an adk-services.json file, either in the app bundle or in the folder set with AdkConfig(root:). The file uses the keys Client-ID, Environment, Org-Title and ProjectKey.

If you use more than one option, getCredentials takes priority, then setCredentials(_:), then the JSON file.

Connection

connect() opens the connection and signs the user in. If the connection drops, the ADK reconnects automatically.

adk.on("connection") { data in
    if let connection = data as? ConnectionDetail {
        print("Connected:", connection.connectionId)
    }
}

adk.on("close") { reason in
    print("Connection closed:", reason)
}

await adk.connect()

You can pause the connection and resume it later. A paused connection stays closed until you call resume().

adk.pause()
await adk.resume()

await adk.disconnect()   // close the connection

adk.state tells you where the connection stands:

State Meaning
.connecting Opening the connection, or reconnecting after it dropped
.connected Connected and signed in
.paused Paused with pause()
.stopped Not connected

If your project reaches its billing or concurrency limit, the ADK emits a limitExceeded event and stops reconnecting.

Channels and messages

A channel is a named stream of messages. Subscribe to a channel to send and receive messages on it. subscribe returns a Subscription for regular channels and a LiveObjSubscription for shared-object channels.

let channel = try await adk.subscribe(channel: "room-42")

// When you no longer need the channel:
await channel.unsubscribe()

Sending messages

try await channel.push(event: "message", data: ["text": "Hello"])

To send a message to specific users, list their usernames in the to option:

try await channel.push(
    event: "message",
    data: ["text": "Hi Bob"],
    options: PushConfig(to: ["bob"])
)

On targeted channels, push waits until ART confirms delivery. If no confirmation arrives within 50 seconds, it throws ARTError.ackTimeout.

Receiving messages

channel.emitter.on("message") { data in
    print("Received:", data)
}

On regular channels, bind(event:) also delivers messages that arrived before you started listening. It returns a token that you can use to remove that listener later:

if let subscription = channel as? Subscription {
    let token = subscription.bind(event: "message") { content in
        print(content)
    }

    // Later:
    subscription.remove(event: "message", id: token)
}

Presence

Find out who is online in the channel. The callback runs again whenever someone joins or leaves.

let stopPresence = try await channel.fetchPresence { users in
    print("Online:", users)
}

// Later, to stop receiving updates:
try await stopPresence()

Encrypted channels

Messages on encrypted channels are encrypted on the sender's device, and only the recipients can read them. Before using an encrypted channel, create a key pair for the current user. generateKeyPair() creates the keys and registers the public key with ART.

_ = try await adk.generateKeyPair()

let secure = try await adk.subscribe(channel: "SECURE_CHANNEL")

secure.emitter.on("message") { data in
    print("Decrypted:", data)
}

try await secure.push(
    event: "message",
    data: ["text": "Private"],
    options: PushConfig(to: ["bob"])
)

Shared objects

A shared-object channel holds a document that every subscriber can read and edit. Changes are merged automatically using CRDTs (conflict-free replicated data types), so everyone ends up with the same data.

let channel = try await adk.subscribe(channel: "CRDT_CHANNEL")

if let live = channel as? LiveObjSubscription {
    // Edit the document, then send your changes.
    live.state()["document"]["title"].set("My Doc")
    await live.flush()
    // Read a value.
    if let document = await live.query(path: "document").execute() {
        print(document)
    }

    // Watch for changes. The callback also receives the current value.
    let stopWatching = await live.query(path: "document").listen { value in
        print("Updated:", value)
    }

    // Later:
    stopWatching()
}

Lists support the usual operations:

let items = live.state()["items"]

items.push("alpha")                                     // add to the end
items.unshift("zero")                                   // add to the start
items.pop()                                             // remove the last item
items.removeAt(2)                                       // remove the item at an index
items.splice(start: 1, deleteCount: 1, insert: ["x"])   // replace a range

await live.flush()

Interceptors

An interceptor sees messages before they are delivered and decides what happens to each one. Call resolve to deliver the message, with or without changes, or reject to block it.

_ = try await adk.intercept(interceptor: "profanity-filter") { payload, resolve, reject in
    if let text = payload["text"] as? String, text.contains("badword") {
        reject("Message blocked")
        return
    }
    resolve(payload)
}

The name must match an interceptor set up in your ART project.

AI agents

Chat with an agent built on ART. Each conversation takes place in a thread.

Start a conversation

let agent = adk.agent("YOUR_AGENT_ID")
let thread = agent.thread()   // or agent.thread("THREAD_ID") to continue a conversation

await thread.listen { envelope in
    print("Event:", envelope.event)
}

let run = try await thread.run("Plan a 3-day trip to Dubai")

do {
    let output = try await run.done()
    print(output.message)
} catch let error as AgentError {
    print("The agent couldn't finish:", error.message)
}

run.done() waits for the agent's final answer. listen receives every event in the thread as it happens, such as progress updates and questions from the agent. envelope.content holds all the fields the server sent with the event.

Display progress

Use the thread's state to drive a status indicator, such as "Queued" or "Waiting for your input":

let stopUpdates = await thread.listenState { state in
    print(state.phase, state.message)
}

print(thread.getState().phase)   // the latest state
stopUpdates()                    // stop receiving updates

The phase is one of idle, submitted, queued, running, waitingForAgent, waitingForApproval, waitingForWorkspace, completed, failed or cancelled.

Answer questions from the agent

An agent can pause and ask the user for more information. This is often called human-in-the-loop. Register a handler before you start the run, and send the answer with sendFeedback:

thread.feedbackRequest { request, run in
    print("The agent asks:", request.prompt)

    Task {
        do {
            try await run.sendFeedback("Budget is 50,000, travelling in December")
        } catch {
            print("Couldn't send the answer:", error)
        }
    }
}

Share files with an agent

Upload the file first, then attach it to your message:

let file = try await agent.upload(fileURL: documentURL, options: UploadOptions(
    progress: { fraction in print("Uploaded \(Int(fraction * 100))%") }
))

let run = try await thread.run("Summarize this document", fileMeta: [FileMeta(file)])

By default, only the file's owner can use it. To allow other agents to use it as well, list their IDs in scope, for example FileMeta(file, scope: ["OTHER_AGENT_ID"]).

AI orchestrators

An orchestrator coordinates several agents to complete a larger task. Start a thread, listen for its events, and send the user's input:

let orchestrator = adk.orchestrator("YOUR_ORCHESTRATOR_ID")
let thread = try await orchestrator.thread()

thread.listen { event in
    print(event)
}

thread.listenState { state in
    print(state.phase)
}

try await thread.push(event: "user_input", data: ["message": "Plan a 3-day trip to Goa"])

File storage

Upload files to ART storage, then list, fetch or delete them. Each upload returns a FileRef with the file's ID, name, size, content type and a URL for reading it.

let ref = try await adk.upload(data: pngData, filename: "chart.png", contentType: "image/png")

let page = try await adk.listFiles(options: ListOptions(configType: .media, page: 1, limit: 20))
let file = try await adk.getFile(fileId: ref.fileId)   // includes a signed URL for reading the file
try await adk.deleteFile(fileId: ref.fileId)           // pass hard: true to delete it permanently

To upload a file from disk, use upload(fileURL:). The file is streamed rather than loaded into memory, and you can follow its progress with UploadOptions(progress:). To cancel an upload, cancel the task that runs it.

Every file has an owner, which depends on where you upload it from:

Uploaded with Owner
adk.upload Your project, or the ID you pass in UploadOptions(configId:)
agent.upload The agent
upload on an agent thread The thread
orchestrator.upload The orchestrator
upload on an orchestrator thread The thread
upload on a channel The channel (orchestrator-enabled channels only)

When an upload fails, the ADK throws an UploadError. Its step tells you which stage failed, and status holds the HTTP status code. A 403 at the .signedURL step means the user's role doesn't have storage permission.

do {
    _ = try await adk.upload(data: pngData)
} catch let error as UploadError where error.step == .signedURL && error.status == 403 {
    print("This user's role doesn't allow uploads")
}

Profiles and connectors

Update the signed-in user's profile. Only the fields you set are changed.

try await adk.updateProfile(UpdateProfileData(firstName: "Ada", email: "ada@example.com"))

Each connector keeps its own profile for the user. Read it, or update the fields that the connector allows:

let crm = try adk.connector("YOUR_CONNECTOR_ID")

let profile = try await crm.profile()          // the allowed fields and their current values
try await crm.updateProfile(["region": "eu"])  // only allowed fields can be changed

Notifications

Notifications are in the optional ArtAdkNotifications library. Add it to your Adk instance once and keep the object it returns:

import ArtAdkNotifications

let inbox = adk.use(notifications())

// Receive new notifications as they arrive.
let stopListening = try await inbox.onNew { notification in
    print(notification.title)
}

let unread = try await inbox.list(NotificationListParams(status: .unread))
try await inbox.markRead()   // marks every unread notification as read

// Register the device for push notifications.
try await inbox.registerDevice(RegisterDeviceInput(token: pushToken, platform: .ios))

You can also send notifications, count unread notifications and manage registered devices. To get the same object elsewhere in your app, call adk.plugin("notifications", as: NotificationsApi.self).

Calling ART APIs

call sends an authenticated request to an ART REST endpoint and returns the decoded JSON response:

let result: [String: Any] = try await adk.call(
    endpoint: "/v1/some-endpoint",
    options: CallApiProps(method: "POST", payload: ["key": "value"])
)

Logging

The ADK doesn't print anything by default. To see its warnings and errors, set a log handler:

ArtLog.handler = { level, message in
    print("[ART][\(level)] \(message)")
}

Documentation

For full guides, see the ART ADK documentation. For what's new in each release and how to upgrade, see the changelog.

Topic Link
Overview ADK overview
Installation Swift installation
Publish and subscribe Pub/sub
Connection management Connections
User presence Presence
Encrypted channels Encryption
Shared object channels Shared objects
Interceptors Interceptors
Agents Agents
Orchestrators Orchestrators

License

The ART Swift ADK is released under the MIT License.

Description

  • Swift Tools 5.9.0
View More Packages from this Author

Dependencies

Last updated: Sun Oct 25 2026 13:56:53 GMT-0900 (Hawaii-Aleutian Daylight Time)