diff --git a/src/pages/docs/ai-transport/messaging/accepting-user-input.mdx b/src/pages/docs/ai-transport/messaging/accepting-user-input.mdx
index 2a66b7f43d..eb9e5eb73e 100644
--- a/src/pages/docs/ai-transport/messaging/accepting-user-input.mdx
+++ b/src/pages/docs/ai-transport/messaging/accepting-user-input.mdx
@@ -43,6 +43,19 @@ const claims = {
'x-ably-clientId': 'user-123'
};
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example() {
+ // --- example code starts here ---
+*/}
+```swift
+let claims: [String: String] = [
+ "x-ably-clientId": "user-123"
+]
+```
+{/* --- end example code --- */}
The `clientId` is automatically attached to every message the user publishes, so agents can trust this identity.
@@ -58,6 +71,28 @@ await channel.subscribe('user-input', (message) => {
processAndRespond(channel, text, promptId, userId);
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example(channel: ARTRealtimeChannel) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+channel.subscribe("user-input") { message in
+ let userId = message.clientId
+ // promptId is a user-generated UUID for correlating responses
+ guard let data = message.data as? [String: Any],
+ let promptId = data["promptId"] as? String,
+ let text = data["text"] as? String else {
+ return
+ }
+
+ print("Received prompt from user \(userId ?? "")")
+ // processAndRespond(channel, text, promptId, userId)
+}
+```
+{/* --- end example code --- */}
### Verify by role
@@ -72,6 +107,19 @@ const claims = {
'ably.channel.*': 'user'
};
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example() {
+ // --- example code starts here ---
+*/}
+```swift
+let claims: [String: String] = [
+ "ably.channel.*": "user"
+]
+```
+{/* --- end example code --- */}
The user claim is automatically attached to every message the user publishes, so agents can trust this role information.
@@ -91,6 +139,32 @@ await channel.subscribe('user-input', (message) => {
processAndRespond(channel, text, promptId);
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example(channel: ARTRealtimeChannel) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+channel.subscribe("user-input") { message in
+ let role = message.extras?["userClaim"] as? String
+ // promptId is a user-generated UUID for correlating responses
+ guard let data = message.data as? [String: Any],
+ let promptId = data["promptId"] as? String,
+ let text = data["text"] as? String else {
+ return
+ }
+
+ if role != "user" {
+ print("Ignoring message from non-user")
+ return
+ }
+
+ // processAndRespond(channel, text, promptId)
+}
+```
+{/* --- end example code --- */}
## Publish user input
@@ -107,6 +181,26 @@ await channel.publish('user-input', {
text: 'What is the weather like today?'
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example(channel: ARTRealtimeChannel) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+let promptId = UUID().uuidString
+let message = ARTMessage(name: "user-input", data: [
+ "promptId": promptId,
+ "text": "What is the weather like today?"
+])
+channel.publish([message]) { error in
+ if let error {
+ print("Error publishing message: \(error)")
+ }
+}
+```
+{/* --- end example code --- */}
@@ -136,6 +230,29 @@ await channel.subscribe('user-input', (message) => {
processAndRespond(channel, text, promptId);
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example(channel: ARTRealtimeChannel) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+channel.subscribe("user-input") { message in
+ guard let data = message.data as? [String: Any],
+ let promptId = data["promptId"] as? String,
+ let text = data["text"] as? String else {
+ return
+ }
+ let userId = message.clientId
+
+ print("Received prompt from \(userId ?? ""): \(text)")
+
+ // Process the prompt and generate a response
+ // processAndRespond(channel, text, promptId)
+}
+```
+{/* --- end example code --- */}
@@ -166,6 +283,31 @@ async function processAndRespond(channel, prompt, promptId) {
});
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example(channel: ARTRealtimeChannel, prompt: String, promptId: String, generateAIResponse: @escaping (String) async -> String) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Generate the response (e.g., call your AI model)
+let response = await generateAIResponse(prompt)
+
+// Publish the response with the promptId for correlation
+let message = ARTMessage(name: "agent-response", data: response)
+message.extras = [
+ "headers": [
+ "promptId": promptId
+ ]
+]
+channel.publish([message]) { error in
+ if let error {
+ print("Error publishing response: \(error)")
+ }
+}
+```
+{/* --- end example code --- */}
The user's client can then match responses to their original prompts:
@@ -193,6 +335,47 @@ await channel.subscribe('agent-response', (message) => {
}
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example(channel: ARTRealtimeChannel) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+var pendingPrompts: [String: [String: String]] = [:]
+
+// Send a prompt and track it
+func sendPrompt(text: String) async throws -> String {
+ let promptId = UUID().uuidString
+ pendingPrompts[promptId] = ["text": text]
+ let message = ARTMessage(name: "user-input", data: [
+ "promptId": promptId,
+ "text": text
+ ])
+ channel.publish([message]) { error in
+ if let error {
+ print("Error publishing prompt: \(error)")
+ }
+ }
+ return promptId
+}
+
+// Handle responses
+channel.subscribe("agent-response") { message in
+ guard let extras = message.extras as? [String: Any],
+ let headers = extras["headers"] as? [String: Any],
+ let promptId = headers["promptId"] as? String,
+ pendingPrompts[promptId] != nil else {
+ return
+ }
+
+ let originalPrompt = pendingPrompts[promptId]!
+ print("Response for \"\(originalPrompt["text"] ?? "")\": \(message.data ?? "")")
+ pendingPrompts.removeValue(forKey: promptId)
+}
+```
+{/* --- end example code --- */}
@@ -233,6 +416,51 @@ async function streamResponse(channel, prompt, promptId) {
}
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example(channel: ARTRealtimeChannel, prompt: String, promptId: String, generateTokens: (String) -> any AsyncSequence) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Create initial message for message-per-response pattern
+let initialMessage = ARTMessage(name: "agent-response", data: "")
+initialMessage.extras = [
+ "headers": [
+ "promptId": promptId
+ ]
+]
+
+channel.publish([initialMessage]) { error in
+ if let error {
+ print("Error publishing initial message: \(error)")
+ return
+ }
+
+ Task {
+ // Stream tokens by appending to the message
+ for try await token in generateTokens(prompt) {
+ let appendMessage = ARTMessage()
+ appendMessage.data = token
+ appendMessage.extras = [
+ "headers": [
+ "promptId": promptId
+ ]
+ ]
+
+ // Note: ably-cocoa doesn't have appendMessage method in the same way as JS
+ // This is a conceptual translation showing the structure
+ channel.publish([appendMessage]) { error in
+ if let error {
+ print("Error appending token: \(error)")
+ }
+ }
+ }
+ }
+}
+```
+{/* --- end example code --- */}
@@ -265,4 +493,40 @@ await channel.subscribe('user-input', async (message) => {
}
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example(channel: ARTRealtimeChannel, streamResponse: @escaping (ARTRealtimeChannel, String, String) async throws -> Void) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Agent handling multiple concurrent prompts
+var activeRequests: [String: [String: String]] = [:]
+
+channel.subscribe("user-input") { message in
+ guard let data = message.data as? [String: Any],
+ let promptId = data["promptId"] as? String,
+ let text = data["text"] as? String else {
+ return
+ }
+ let userId = message.clientId
+
+ // Track active request
+ activeRequests[promptId] = [
+ "userId": userId ?? "",
+ "text": text
+ ]
+
+ Task {
+ do {
+ try await streamResponse(channel, text, promptId)
+ } catch {
+ print("Error streaming response: \(error)")
+ }
+ activeRequests.removeValue(forKey: promptId)
+ }
+}
+```
+{/* --- end example code --- */}
diff --git a/src/pages/docs/ai-transport/messaging/chain-of-thought.mdx b/src/pages/docs/ai-transport/messaging/chain-of-thought.mdx
index 15c40d0dc0..efe7db38a5 100644
--- a/src/pages/docs/ai-transport/messaging/chain-of-thought.mdx
+++ b/src/pages/docs/ai-transport/messaging/chain-of-thought.mdx
@@ -76,6 +76,52 @@ for await (const event of stream) {
}
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example(
+ realtime: ARTRealtime,
+ stream: any AsyncSequence<(type: String, text: String, responseId: String), Never>
+) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Example: stream returns events like:
+// { type: 'reasoning', text: "Let's break down the problem: 27 * 12", responseId: 'resp_abc123' }
+// { type: 'reasoning', text: 'First, I can split this into 27 * 10 + 27 * 2', responseId: 'resp_abc123' }
+// { type: 'reasoning', text: 'That gives us 270 + 54 = 324', responseId: 'resp_abc123' }
+// { type: 'message', text: '27 * 12 = 324', responseId: 'resp_abc123' }
+
+for await event in stream {
+ if event.type == "reasoning" {
+ // Publish reasoning messages
+ try await channel.publish(
+ name: "reasoning",
+ data: event.text,
+ extras: [
+ "headers": [
+ "responseId": event.responseId
+ ]
+ ]
+ )
+ } else if event.type == "message" {
+ // Publish model output messages
+ try await channel.publish(
+ name: "message",
+ data: event.text,
+ extras: [
+ "headers": [
+ "responseId": event.responseId
+ ]
+ ]
+ )
+ }
+}
+```
+{/* --- end example code --- */}
@@ -132,6 +178,46 @@ await channel.subscribe((message) => {
console.log(`Response ${responseId}:`, response);
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example(realtime: ARTRealtime) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Track responses by ID, each containing reasoning messages and final response
+var responses: [String: (reasoning: [String], message: String)] = [:]
+
+// Subscribe to all events on the channel
+try await channel.subscribe { message in
+ guard let responseId = message.extras?["headers"]?["responseId"] as? String else {
+ print("Message missing responseId")
+ return
+ }
+
+ // Initialize response object if needed
+ if responses[responseId] == nil {
+ responses[responseId] = (reasoning: [], message: "")
+ }
+
+ // Handle each message type
+ switch message.name {
+ case "message":
+ responses[responseId]?.message = message.data as? String ?? ""
+ case "reasoning":
+ responses[responseId]?.reasoning.append(message.data as? String ?? "")
+ default:
+ break
+ }
+
+ // Display the reasoning and response for this turn
+ print("Response \(responseId):", responses[responseId] ?? (reasoning: [], message: ""))
+}
+```
+{/* --- end example code --- */}
@@ -202,6 +288,60 @@ for await (const event of stream) {
}
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example(
+ realtime: ARTRealtime,
+ stream: any AsyncSequence<(type: String, text: String, responseId: String), Never>
+) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+let conversationChannel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Example: stream returns events like:
+// { type: 'start', responseId: 'resp_abc123' }
+// { type: 'reasoning', text: "Let's break down the problem: 27 * 12", responseId: 'resp_abc123' }
+// { type: 'reasoning', text: 'First, I can split this into 27 * 10 + 27 * 2', responseId: 'resp_abc123' }
+// { type: 'reasoning', text: 'That gives us 270 + 54 = 324', responseId: 'resp_abc123' }
+// { type: 'message', text: '27 * 12 = 324', responseId: 'resp_abc123' }
+
+for await event in stream {
+ if event.type == "start" {
+ // Publish response start control message
+ try await conversationChannel.publish(
+ name: "start",
+ extras: [
+ "headers": [
+ "responseId": event.responseId
+ ]
+ ]
+ )
+ } else if event.type == "reasoning" {
+ // Publish reasoning to separate reasoning channel
+ let reasoningChannel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}:\(event.responseId)")
+
+ try await reasoningChannel.publish(
+ name: "reasoning",
+ data: event.text
+ )
+ } else if event.type == "message" {
+ // Publish model output to main channel
+ try await conversationChannel.publish(
+ name: "message",
+ data: event.text,
+ extras: [
+ "headers": [
+ "responseId": event.responseId
+ ]
+ ]
+ )
+ }
+}
+```
+{/* --- end example code --- */}
@@ -259,6 +399,53 @@ async function onClickViewReasoning(responseId) {
});
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example(realtime: ARTRealtime) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+let conversationChannel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Track responses by ID
+var responses: [String: String] = [:]
+
+// Subscribe to all messages on the main channel
+try await conversationChannel.subscribe { message in
+ guard let responseId = message.extras?["headers"]?["responseId"] as? String else {
+ print("Message missing responseId")
+ return
+ }
+
+ // Handle response start control message
+ if message.name == "start" {
+ responses[responseId] = ""
+ }
+
+ // Handle model output message
+ if message.name == "message" {
+ responses[responseId] = message.data as? String ?? ""
+ }
+}
+
+// Subscribe to reasoning on demand (e.g., when user clicks to view reasoning)
+func onClickViewReasoning(responseId: String) async throws {
+ // Derive reasoning channel name from responseId and
+ // use rewind to retrieve historical reasoning
+ let reasoningChannel = realtime.channels.get(
+ "{{RANDOM_CHANNEL_NAME}}:\(responseId)",
+ options: ARTRealtimeChannelOptions(params: ["rewind": "2m"])
+ )
+
+ // Subscribe to reasoning messages
+ try await reasoningChannel.subscribe { message in
+ print("[Reasoning]: \(message.data as? String ?? "")")
+ }
+}
+```
+{/* --- end example code --- */}
diff --git a/src/pages/docs/ai-transport/messaging/citations.mdx b/src/pages/docs/ai-transport/messaging/citations.mdx
index 194e451385..3ea7ec850d 100644
--- a/src/pages/docs/ai-transport/messaging/citations.mdx
+++ b/src/pages/docs/ai-transport/messaging/citations.mdx
@@ -133,6 +133,64 @@ await channel.annotations.publish(msgSerial, {
}
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+import Ably
+
+func example_publishCitations(realtime: ARTRealtime, msgSerial: String) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("ai:{{RANDOM_CHANNEL_NAME}}")
+
+// Publish the AI response message
+let response = "The James Webb Space Telescope launched in December 2021 and its first images were released in July 2022."
+channel.publish("response", data: response) { error in
+ // Handle error if needed
+}
+
+// Add citations by annotating the response message
+let nasaCitation = ARTOutboundAnnotation(
+ id: nil,
+ type: "citations:multiple.v1",
+ clientId: nil,
+ name: "science.nasa.gov",
+ count: nil,
+ data: [
+ "url": "https://science.nasa.gov/mission/webb/",
+ "title": "James Webb Space Telescope - NASA Science",
+ "startOffset": 43,
+ "endOffset": 56,
+ "snippet": "Webb launched on Dec. 25th 2021"
+ ] as [String: Any],
+ extras: nil
+)
+channel.annotations.publish(forMessageSerial: msgSerial, annotation: nasaCitation) { error in
+ // Handle error if needed
+}
+
+let wikipediaCitation = ARTOutboundAnnotation(
+ id: nil,
+ type: "citations:multiple.v1",
+ clientId: nil,
+ name: "en.wikipedia.org",
+ count: nil,
+ data: [
+ "url": "https://en.wikipedia.org/wiki/James_Webb_Space_Telescope",
+ "title": "James Webb Space Telescope - Wikipedia",
+ "startOffset": 95,
+ "endOffset": 104,
+ "snippet": "The telescope's first image was released to the public on 11 July 2022."
+ ] as [String: Any],
+ extras: nil
+)
+channel.annotations.publish(forMessageSerial: msgSerial, annotation: wikipediaCitation) { error in
+ // Handle error if needed
+}
+```
+{/* --- end example code --- */}
@@ -171,6 +229,27 @@ await channel.subscribe((message) => {
}
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+import Ably
+
+func example_subscribeToSummaries(realtime: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("ai:{{RANDOM_CHANNEL_NAME}}")
+
+channel.subscribe { message in
+ if message.action == .messageSummary {
+ if let citations = message.annotations?.summary["citations:multiple.v1"] {
+ print("Citation summary:", citations)
+ }
+ }
+}
+```
+{/* --- end example code --- */}
The `multiple.v1` summary groups counts by the annotation `name`, with totals and per-client breakdowns for each group:
@@ -227,6 +306,33 @@ await channel.annotations.subscribe((annotation) => {
}
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+import Ably
+
+func example_subscribeToIndividualCitations(realtime: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let options = ARTRealtimeChannelOptions()
+options.modes = [.annotationSubscribe]
+let channel = realtime.channels.get("ai:{{RANDOM_CHANNEL_NAME}}", options: options)
+
+channel.annotations.subscribe { annotation in
+ if annotation.action == .create &&
+ annotation.type == "citations:multiple.v1" {
+ if let data = annotation.data as? [String: Any],
+ let url = data["url"] as? String,
+ let title = data["title"] as? String {
+ print("Citation: \(title) (\(url))")
+ // Output: Citation: James Webb Space Telescope - Wikipedia (https://en.wikipedia.org/wiki/James_Webb_Space_Telescope)
+ }
+ }
+}
+```
+{/* --- end example code --- */}
Each annotation event includes the `messageSerial` of the response message it is attached to, the `name` used for grouping in summaries, and the full citation `data` payload. This data can be used to render clickable source links or attach inline citation markers to specific portions of the response text:
diff --git a/src/pages/docs/ai-transport/messaging/human-in-the-loop.mdx b/src/pages/docs/ai-transport/messaging/human-in-the-loop.mdx
index 98207f1025..07af1b783f 100644
--- a/src/pages/docs/ai-transport/messaging/human-in-the-loop.mdx
+++ b/src/pages/docs/ai-transport/messaging/human-in-the-loop.mdx
@@ -58,6 +58,51 @@ async function requestHumanApproval(toolCall) {
});
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+// Package.swift dependencies: .package(url: "https://github.com/ably/ably-cocoa", from: "1.2.0")
+import Ably
+
+func exampleContext_requestHumanApproval() {
+ struct ToolCall {
+ let id: String
+ let name: String
+ let arguments: [String: Any]
+ }
+
+ func example(realtime: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+var pendingApprovals: [String: [String: Any]] = [:]
+
+func requestHumanApproval(toolCall: ToolCall) {
+ pendingApprovals[toolCall.id] = ["toolCall": toolCall]
+
+ let message = ARTMessage(
+ name: "approval-request",
+ data: [
+ "name": toolCall.name,
+ "arguments": toolCall.arguments
+ ]
+ )
+ message.extras = [
+ "headers": [
+ "toolCallId": toolCall.id
+ ]
+ ]
+
+ channel.publish([message]) { error in
+ if let error {
+ print("Error publishing approval request: \(error)")
+ }
+ }
+}
+```
+{/* --- end example code --- */}
@@ -77,6 +122,20 @@ const claims = {
'ably.channel.*': 'user'
};
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example() {
+ // --- example code starts here ---
+*/}
+```swift
+let claims: [String: String] = [
+ "x-ably-clientId": "user-123",
+ "ably.channel.*": "user"
+]
+```
+{/* --- end example code --- */}
The `clientId` and user claims are automatically attached to every message the user publishes and cannot be forged, so agents can trust this identity and role information.
@@ -124,6 +183,70 @@ async function reject(toolCallId) {
});
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+// Package.swift dependencies: .package(url: "https://github.com/ably/ably-cocoa", from: "1.2.0")
+import Ably
+
+func example(realtime: ARTRealtime, displayApprovalUI: ([String: Any], String) -> Void) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+channel.subscribe("approval-request") { message in
+ guard let request = message.data as? [String: Any],
+ let headers = message.extras?["headers"] as? [String: Any],
+ let toolCallId = headers["toolCallId"] as? String else {
+ return
+ }
+ // Display request for human review
+ displayApprovalUI(request, toolCallId)
+}
+
+func approve(toolCallId: String) {
+ let message = ARTMessage(
+ name: "approval-response",
+ data: [
+ "decision": "approved"
+ ]
+ )
+ message.extras = [
+ "headers": [
+ "toolCallId": toolCallId
+ ]
+ ]
+
+ channel.publish([message]) { error in
+ if let error {
+ print("Error publishing approval: \(error)")
+ }
+ }
+}
+
+func reject(toolCallId: String) {
+ let message = ARTMessage(
+ name: "approval-response",
+ data: [
+ "decision": "rejected"
+ ]
+ )
+ message.extras = [
+ "headers": [
+ "toolCallId": toolCallId
+ ]
+ ]
+
+ channel.publish([message]) { error in
+ if let error {
+ print("Error publishing rejection: \(error)")
+ }
+ }
+}
+```
+{/* --- end example code --- */}
@@ -178,6 +301,80 @@ await channel.subscribe('approval-response', async (message) => {
pendingApprovals.delete(toolCallId);
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+// Package.swift dependencies: .package(url: "https://github.com/ably/ably-cocoa", from: "1.2.0")
+import Ably
+
+func exampleContext_processApprovalByIdentity() {
+ struct ToolCall {
+ let id: String
+ let name: String
+ let arguments: [String: Any]
+ }
+
+ struct UserPermissions {
+ func canApprove(_ toolName: String) -> Bool {
+ return true
+ }
+ }
+
+ func example(
+ channel: ARTRealtimeChannel,
+ getUserPermissions: @escaping (String) async -> UserPermissions,
+ executeToolCall: @escaping (ToolCall) async -> Any
+ ) {
+ // --- example code starts here ---
+*/}
+```swift
+var pendingApprovals: [String: [String: Any]] = [:]
+
+channel.subscribe("approval-response") { message in
+ guard let response = message.data as? [String: Any],
+ let headers = message.extras?["headers"] as? [String: Any],
+ let toolCallId = headers["toolCallId"] as? String,
+ let pending = pendingApprovals[toolCallId],
+ let toolCallDict = pending["toolCall"] as? [String: Any] else {
+ return
+ }
+
+ // The clientId is the trusted approver identity
+ guard let approverId = message.clientId else {
+ return
+ }
+
+ Task {
+ // Look up user-specific permissions from your database
+ let userPermissions = await getUserPermissions(approverId)
+
+ guard let toolCallName = toolCallDict["name"] as? String else {
+ return
+ }
+
+ if !userPermissions.canApprove(toolCallName) {
+ print("User \(approverId) not authorized to approve \(toolCallName)")
+ return
+ }
+
+ if let decision = response["decision"] as? String, decision == "approved" {
+ let toolCall = ToolCall(
+ id: toolCallId,
+ name: toolCallName,
+ arguments: toolCallDict["arguments"] as? [String: Any] ?? [:]
+ )
+ _ = await executeToolCall(toolCall)
+ print("Action approved by \(approverId)")
+ } else {
+ print("Action rejected by \(approverId)")
+ }
+
+ pendingApprovals.removeValue(forKey: toolCallId)
+ }
+}
+```
+{/* --- end example code --- */}
### Verify by role
@@ -234,4 +431,85 @@ await channel.subscribe('approval-response', async (message) => {
pendingApprovals.delete(toolCallId);
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+// Package.swift dependencies: .package(url: "https://github.com/ably/ably-cocoa", from: "1.2.0")
+import Ably
+
+func exampleContext_processApprovalByRole() {
+ struct ToolCall {
+ let id: String
+ let name: String
+ let arguments: [String: Any]
+ }
+
+ func example(
+ channel: ARTRealtimeChannel,
+ pendingApprovals: inout [String: [String: Any]],
+ executeToolCall: @escaping (ToolCall) async -> Any
+ ) {
+ // --- example code starts here ---
+*/}
+```swift
+let roleHierarchy = ["editor", "publisher", "admin"]
+
+let approvalPolicies: [String: String] = [
+ "publish_blog_post": "publisher"
+]
+
+func canApprove(approverRole: String, requiredRole: String) -> Bool {
+ guard let approverLevel = roleHierarchy.firstIndex(of: approverRole),
+ let requiredLevel = roleHierarchy.firstIndex(of: requiredRole) else {
+ return false
+ }
+
+ return approverLevel >= requiredLevel
+}
+
+// When processing approval response
+channel.subscribe("approval-response") { message in
+ guard let response = message.data as? [String: Any],
+ let headers = message.extras?["headers"] as? [String: Any],
+ let toolCallId = headers["toolCallId"] as? String,
+ let pending = pendingApprovals[toolCallId],
+ let toolCallDict = pending["toolCall"] as? [String: Any],
+ let toolCallName = toolCallDict["name"] as? String else {
+ return
+ }
+
+ guard let policy = approvalPolicies[toolCallName] else {
+ return
+ }
+
+ // Get the trusted role from the JWT claim
+ guard let approverRole = message.extras?["userClaim"] as? String else {
+ return
+ }
+
+ // Verify the approver's role meets the minimum required role for this action
+ if !canApprove(approverRole: approverRole, requiredRole: policy) {
+ print("Approver role '\(approverRole)' insufficient: minimum required role is '\(policy)'")
+ return
+ }
+
+ Task {
+ if let decision = response["decision"] as? String, decision == "approved" {
+ let toolCall = ToolCall(
+ id: toolCallId,
+ name: toolCallName,
+ arguments: toolCallDict["arguments"] as? [String: Any] ?? [:]
+ )
+ _ = await executeToolCall(toolCall)
+ print("Action approved by role \(approverRole)")
+ } else {
+ print("Action rejected by role \(approverRole)")
+ }
+
+ pendingApprovals.removeValue(forKey: toolCallId)
+ }
+}
+```
+{/* --- end example code --- */}
diff --git a/src/pages/docs/ai-transport/messaging/tool-calls.mdx b/src/pages/docs/ai-transport/messaging/tool-calls.mdx
index 15de573ff2..5b509df4b0 100644
--- a/src/pages/docs/ai-transport/messaging/tool-calls.mdx
+++ b/src/pages/docs/ai-transport/messaging/tool-calls.mdx
@@ -84,6 +84,95 @@ for await (const event of stream) {
}
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+struct StreamEvent {
+ let type: String
+ let name: String?
+ let args: String?
+ let result: String?
+ let text: String?
+ let toolCallId: String?
+ let responseId: String
+}
+
+func example_publish_tool_calls(realtime: ARTRealtime, stream: AsyncStream) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Example: stream returns events like:
+// { type: 'tool_call', name: 'get_weather', args: '{"location":"San Francisco"}', toolCallId: 'tool_123', responseId: 'resp_abc123' }
+// { type: 'tool_result', name: 'get_weather', result: '{"temp":72,"conditions":"sunny"}', toolCallId: 'tool_123', responseId: 'resp_abc123' }
+// { type: 'message', text: 'The weather in San Francisco is 72°F and sunny.', responseId: 'resp_abc123' }
+
+for await event in stream {
+ if event.type == "tool_call" {
+ // Publish tool call arguments
+ let message = ARTMessage(name: "tool_call", data: [
+ "name": event.name ?? "",
+ "args": event.args ?? ""
+ ])
+ message.extras = [
+ "headers": [
+ "responseId": event.responseId,
+ "toolCallId": event.toolCallId ?? ""
+ ]
+ ] as ARTJsonCompatible
+ try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in
+ channel.publish([message]) { error in
+ if let error = error {
+ continuation.resume(throwing: error)
+ } else {
+ continuation.resume()
+ }
+ }
+ }
+ } else if event.type == "tool_result" {
+ // Publish tool call results
+ let message = ARTMessage(name: "tool_result", data: [
+ "name": event.name ?? "",
+ "result": event.result ?? ""
+ ])
+ message.extras = [
+ "headers": [
+ "responseId": event.responseId,
+ "toolCallId": event.toolCallId ?? ""
+ ]
+ ] as ARTJsonCompatible
+ try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in
+ channel.publish([message]) { error in
+ if let error = error {
+ continuation.resume(throwing: error)
+ } else {
+ continuation.resume()
+ }
+ }
+ }
+ } else if event.type == "message" {
+ // Publish model output messages
+ let message = ARTMessage(name: "message", data: event.text ?? "")
+ message.extras = [
+ "headers": [
+ "responseId": event.responseId
+ ]
+ ] as ARTJsonCompatible
+ try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in
+ channel.publish([message]) { error in
+ if let error = error {
+ continuation.resume(throwing: error)
+ } else {
+ continuation.resume()
+ }
+ }
+ }
+ }
+}
+```
+{/* --- end example code --- */}
@@ -155,6 +244,69 @@ await channel.subscribe((message) => {
console.log(`Response ${responseId}:`, response);
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_subscribe_tool_calls(realtime: ARTRealtime) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Track responses by ID, each containing tool calls and final response
+var responses: [String: (toolCalls: [String: [String: Any]], message: String)] = [:]
+
+// Subscribe to all events on the channel
+channel.subscribe { message in
+ guard let extras = message.extras as? [String: Any],
+ let headers = extras["headers"] as? [String: Any],
+ let responseId = headers["responseId"] as? String else {
+ print("Message missing responseId")
+ return
+ }
+
+ // Initialize response object if needed
+ if responses[responseId] == nil {
+ responses[responseId] = (toolCalls: [:], message: "")
+ }
+
+ var response = responses[responseId]!
+
+ // Handle each message type
+ switch message.name {
+ case "message":
+ response.message = message.data as? String ?? ""
+ responses[responseId] = response
+ case "tool_call":
+ if let toolCallId = headers["toolCallId"] as? String,
+ let data = message.data as? [String: Any],
+ let name = data["name"] as? String,
+ let args = data["args"] {
+ response.toolCalls[toolCallId] = [
+ "name": name,
+ "args": args
+ ]
+ responses[responseId] = response
+ }
+ case "tool_result":
+ if let resultToolCallId = headers["toolCallId"] as? String,
+ var toolCall = response.toolCalls[resultToolCallId],
+ let data = message.data as? [String: Any],
+ let result = data["result"] {
+ toolCall["result"] = result
+ response.toolCalls[resultToolCallId] = toolCall
+ responses[responseId] = response
+ }
+ default:
+ break
+ }
+
+ // Display the tool calls and response for this turn
+ print("Response \(responseId):", response)
+}
+```
+{/* --- end example code --- */}
@@ -185,6 +337,42 @@ await channel.subscribe((message) => {
}
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_generative_ui(realtime: ARTRealtime, renderWeatherCard: ([String: Any]) -> Void) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+channel.subscribe { message in
+ // Render component when tool is invoked
+ if message.name == "tool_call",
+ let data = message.data as? [String: Any],
+ let name = data["name"] as? String,
+ name == "get_weather",
+ let argsString = data["args"] as? String,
+ let argsData = argsString.data(using: .utf8),
+ let args = try? JSONSerialization.jsonObject(with: argsData) as? [String: Any],
+ let location = args["location"] as? String {
+ renderWeatherCard(["location": location, "loading": true])
+ }
+
+ // Update component with results
+ if message.name == "tool_result",
+ let data = message.data as? [String: Any],
+ let name = data["name"] as? String,
+ name == "get_weather",
+ let resultString = data["result"] as? String,
+ let resultData = resultString.data(using: .utf8),
+ let result = try? JSONSerialization.jsonObject(with: resultData) as? [String: Any] {
+ renderWeatherCard(result)
+ }
+}
+```
+{/* --- end example code --- */}
@@ -237,6 +425,52 @@ await channel.subscribe('tool_call', async (message) => {
}
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_client_side_tools_execution(realtime: ARTRealtime, getGeolocationPosition: () async -> (coords: (latitude: Double, longitude: Double), Void)) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+channel.subscribe("tool_call") { message in
+ guard let data = message.data as? [String: Any],
+ let name = data["name"] as? String,
+ let extras = message.extras as? [String: Any],
+ let headers = extras["headers"] as? [String: Any],
+ let responseId = headers["responseId"] as? String,
+ let toolCallId = headers["toolCallId"] as? String else {
+ return
+ }
+
+ if name == "get_location" {
+ Task {
+ let result = await getGeolocationPosition()
+ let resultMessage = ARTMessage(name: "tool_result", data: [
+ "name": name,
+ "result": [
+ "lat": result.coords.latitude,
+ "lng": result.coords.longitude
+ ]
+ ])
+ resultMessage.extras = [
+ "headers": [
+ "responseId": responseId,
+ "toolCallId": toolCallId
+ ]
+ ] as ARTJsonCompatible
+ channel.publish([resultMessage]) { error in
+ if let error = error {
+ print("Error publishing result: \(error)")
+ }
+ }
+ }
+ }
+}
+```
+{/* --- end example code --- */}
@@ -265,6 +499,32 @@ await channel.subscribe('tool_result', (message) => {
pendingToolCalls.delete(toolCallId);
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_client_side_tools_agent(realtime: ARTRealtime) async throws {
+ var pendingToolCalls: [String: [String: Any]] = [:]
+ let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+ // --- example code starts here ---
+*/}
+```swift
+channel.subscribe("tool_result") { message in
+ guard let data = message.data as? [String: Any],
+ let toolCallId = data["toolCallId"] as? String,
+ let result = data["result"],
+ let pending = pendingToolCalls[toolCallId],
+ let responseId = pending["responseId"] as? String else {
+ return
+ }
+
+ // Pass result back to the AI model to continue the conversation
+ processResult(responseId, toolCallId, result)
+
+ pendingToolCalls.removeValue(forKey: toolCallId)
+}
+```
+{/* --- end example code --- */}
## Human-in-the-loop workflows
diff --git a/src/pages/docs/ai-transport/sessions-identity/identifying-users-and-agents.mdx b/src/pages/docs/ai-transport/sessions-identity/identifying-users-and-agents.mdx
index f762d4ae87..575ce2f149 100644
--- a/src/pages/docs/ai-transport/sessions-identity/identifying-users-and-agents.mdx
+++ b/src/pages/docs/ai-transport/sessions-identity/identifying-users-and-agents.mdx
@@ -98,6 +98,45 @@ ably.connection.on("connected", () => {
console.log("Connected to Ably");
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_authCallback(authServerURL: URL) throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Client code
+import Ably
+
+let clientOptions = ARTClientOptions()
+clientOptions.authCallback = { tokenParams, completion in
+ let task = URLSession.shared.dataTask(with: authServerURL) { data, response, error in
+ if let error = error {
+ completion(nil, error as NSError)
+ return
+ }
+
+ guard let data = data, let token = String(data: data, encoding: .utf8) else {
+ let error = NSError(domain: "AuthError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid token response"])
+ completion(nil, error)
+ return
+ }
+
+ completion(token as ARTTokenDetailsCompatible, nil)
+ }
+ task.resume()
+}
+
+let realtime = ARTRealtime(options: clientOptions)
+
+realtime.connection.on { stateChange in
+ if stateChange.current == .connected {
+ print("Connected to Ably")
+ }
+}
+```
+{/* --- end example code --- */}
## Authenticating agents
@@ -117,6 +156,26 @@ ably.connection.on("connected", () => {
console.log("Connected to Ably");
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_agentAuth() throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Agent code
+import Ably
+
+let realtime = ARTRealtime(key: "{{API_KEY}}")
+
+realtime.connection.on { stateChange in
+ if stateChange.current == .connected {
+ print("Connected to Ably")
+ }
+}
+```
+{/* --- end example code --- */}
@@ -175,6 +234,39 @@ const announcementsChannel = ably.channels.get("announcements");
await announcementsChannel.publish("prompt", "What is the weather like today?"); // fails
await announcementsChannel.subscribe((msg) => console.log(msg)); // succeeds
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_testCapabilities(realtime: ARTRealtime) throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Client code
+let acmeChannel = realtime.channels.get("org:acme:{{RANDOM_CHANNEL_NAME}}")
+acmeChannel.publish("prompt", data: "What is the weather like today?") { error in
+ // succeeds
+ guard error == nil else {
+ print("Error publishing to acme channel: \(error!.message)")
+ return
+ }
+}
+
+let foobarChannel = realtime.channels.get("org:foobar:{{RANDOM_CHANNEL_NAME}}")
+foobarChannel.publish("prompt", data: "What is the weather like today?") { error in
+ // fails
+}
+
+let announcementsChannel = realtime.channels.get("announcements")
+announcementsChannel.publish("prompt", data: "What is the weather like today?") { error in
+ // fails
+}
+announcementsChannel.subscribe { message in
+ // succeeds
+ print(message.data ?? "")
+}
+```
+{/* --- end example code --- */}
@@ -218,6 +310,34 @@ const otherChannel = ably.channels.get("org:acme:other:{{RANDOM_CHANNEL_NAME}}")
await otherChannel.subscribe((msg) => console.log(msg)); // fails
await otherChannel.publish("update", "It's raining in London"); // fails
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_agentCapabilities(realtime: ARTRealtime) throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Agent code
+let weatherChannel = realtime.channels.get("org:acme:weather:{{RANDOM_CHANNEL_NAME}}")
+weatherChannel.subscribe { message in
+ // succeeds
+ print(message.data ?? "")
+}
+weatherChannel.publish("update", data: "It's raining in London") { error in
+ // succeeds
+}
+
+let otherChannel = realtime.channels.get("org:acme:other:{{RANDOM_CHANNEL_NAME}}")
+otherChannel.subscribe { message in
+ // fails
+ print(message.data ?? "")
+}
+otherChannel.publish("update", data: "It's raining in London") { error in
+ // fails
+}
+```
+{/* --- end example code --- */}
@@ -261,6 +381,26 @@ const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
// Publish a message - the clientId is automatically attached
await channel.publish("prompt", "What is the weather like today?");
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_publishWithClientId(realtime: ARTRealtime) throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Client code
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Publish a message - the clientId is automatically attached
+channel.publish("prompt", data: "What is the weather like today?") { error in
+ guard error == nil else {
+ print("Error publishing: \(error!.message)")
+ return
+ }
+}
+```
+{/* --- end example code --- */}
Agents can then access this verified identity to identify the sender:
@@ -280,6 +420,28 @@ await channel.subscribe("prompt", (message) => {
console.log(`Prompt:`, prompt);
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_agentSubscribeClientId(realtime: ARTRealtime) throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Agent code
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Subscribe to messages from clients
+channel.subscribe("prompt") { message in
+ // Access the verified clientId from the message
+ guard let userId = message.clientId else { return }
+ let prompt = message.data
+
+ print("Received message from user: \(userId)")
+ print("Prompt: \(prompt ?? "")")
+}
+```
+{/* --- end example code --- */}
The `clientId` in the message can be trusted, so agents can use this identity to make decisions about what actions the user can take. For example, agents can check user permissions before executing tool calls, route messages to appropriate AI models based on subscription tiers, or maintain per-user conversation history and context.
@@ -299,6 +461,24 @@ const ably = new Ably.Realtime({
clientId: "weather-agent"
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_agentIdentity() throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Agent code
+import Ably
+
+let clientOptions = ARTClientOptions(key: "{{API_KEY}}")
+// Specify an identity for this agent
+clientOptions.clientId = "weather-agent"
+
+let realtime = ARTRealtime(options: clientOptions)
+```
+{/* --- end example code --- */}
When subscribers receive messages, they can use the `clientId` to determine which agent published the message:
@@ -314,6 +494,24 @@ await channel.subscribe((message) => {
}
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_clientCheckClientId(realtime: ARTRealtime) throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Client code
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+channel.subscribe { message in
+ if message.clientId == "weather-agent" {
+ print("Weather agent response: \(message.data ?? "")")
+ }
+}
+```
+{/* --- end example code --- */}
@@ -364,6 +562,27 @@ await channel.subscribe("prompt", async (message) => {
console.log(`Message from user with role: ${role}`);
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_agentReadUserClaims(realtime: ARTRealtime) throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Agent code
+let channel = realtime.channels.get("org:acme:{{RANDOM_CHANNEL_NAME}}")
+
+// Subscribe to user prompts
+channel.subscribe("prompt") { message in
+ // Access the user's role from the user claim in message extras
+ if let extras = message.extras as? [String: Any],
+ let userClaim = extras["userClaim"] as? String {
+ print("Message from user with role: \(userClaim)")
+ }
+}
+```
+{/* --- end example code --- */}
The `message.extras.userClaim` in the message can be trusted, so agents can rely on this information to make decisions about what actions the user can take. For example, an agent could allow users with an "editor" role to execute tool calls that modify documents, while restricting users with a "guest" role to read-only operations.
@@ -400,6 +619,38 @@ await channel.publish({
}
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_agentPublishWithMetadata() throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Agent code
+import Ably
+
+let clientOptions = ARTClientOptions(key: "{{API_KEY}}")
+clientOptions.clientId = "weather-agent"
+let realtime = ARTRealtime(options: clientOptions)
+
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+let message = ARTMessage(name: "update", data: "It's raining in London")
+message.extras = [
+ "headers": [
+ "model": "gpt-4"
+ ]
+]
+
+channel.publish([message]) { error in
+ guard error == nil else {
+ print("Error publishing: \(error!.message)")
+ return
+ }
+}
+```
+{/* --- end example code --- */}
Clients and other agents can access this metadata when messages are received:
@@ -416,4 +667,26 @@ await channel.subscribe((message) => {
}
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_clientReadMetadata(realtime: ARTRealtime) throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Client code
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+channel.subscribe { message in
+ if message.clientId == "weather-agent" {
+ if let extras = message.extras as? [String: Any],
+ let headers = extras["headers"] as? [String: Any],
+ let model = headers["model"] as? String {
+ print("Response from weather agent using \(model): \(message.data ?? "")")
+ }
+ }
+}
+```
+{/* --- end example code --- */}
diff --git a/src/pages/docs/ai-transport/sessions-identity/online-status.mdx b/src/pages/docs/ai-transport/sessions-identity/online-status.mdx
index 3e9722d647..4ae241569c 100644
--- a/src/pages/docs/ai-transport/sessions-identity/online-status.mdx
+++ b/src/pages/docs/ai-transport/sessions-identity/online-status.mdx
@@ -42,6 +42,23 @@ await channel.presence.enter({
platform: "ios"
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_userEnter(ably: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Enter presence with metadata about the user's device
+channel.presence.enter([
+ "device": "mobile",
+ "platform": "ios"
+])
+```
+{/* --- end example code --- */}
Similarly, an agent can enter presence to signal that it's online:
@@ -56,6 +73,22 @@ await channel.presence.enter({
model: "gpt-4"
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_agentEnter(ably: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Enter presence with metadata about the agent
+channel.presence.enter([
+ "model": "gpt-4"
+])
+```
+{/* --- end example code --- */}
### Going online from multiple devices
@@ -72,6 +105,18 @@ For example, when the user connects from their desktop browser:
const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
await channel.presence.enter({ device: "desktop" });
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_desktopDevice(ably: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}")
+channel.presence.enter(["device": "desktop"])
+```
+{/* --- end example code --- */}
And then connects from their mobile app while still connected on desktop:
@@ -82,6 +127,18 @@ And then connects from their mobile app while still connected on desktop:
const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
await channel.presence.enter({ device: "mobile" });
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_mobileDevice(ably: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}")
+channel.presence.enter(["device": "mobile"])
+```
+{/* --- end example code --- */}
Both devices are now members of the presence set with the same `clientId` but different `connectionId` values. When you query the presence set, you'll see two separate entries:
@@ -97,6 +154,27 @@ for (const { clientId, connectionId, data } of members) {
// user-123 hd67s4!abcdef-0 { device: "desktop" }
// user-123 hd67s4!ghijkl-1 { device: "mobile" }
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_queryPresence(ably: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Query presence to see both devices
+channel.presence.get(nil) { members, error in
+ for member in members ?? [] {
+ print(member.clientId ?? "", member.connectionId ?? "", member.data ?? "")
+ }
+ // Example output:
+ // user-123 hd67s4!abcdef-0 { device: "desktop" }
+ // user-123 hd67s4!ghijkl-1 { device: "mobile" }
+}
+```
+{/* --- end example code --- */}
When either device leaves or disconnects, the other device remains in the presence set.
@@ -123,6 +201,20 @@ const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
// Leave presence when the user marks themselves offline
await channel.presence.leave();
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_userLeave(ably: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Leave presence when the user marks themselves offline
+channel.presence.leave(nil)
+```
+{/* --- end example code --- */}
Similarly, an agent can leave presence when it completes its work or shuts down:
@@ -135,6 +227,20 @@ const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}");
// Leave presence when the agent shuts down
await channel.presence.leave();
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_agentLeave(ably: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Leave presence when the agent shuts down
+channel.presence.leave(nil)
+```
+{/* --- end example code --- */}
Optionally include data when leaving presence to communicate the reason for going offline. This data is delivered to presence subscribers listening to `leave` events and is also available in [presence history](/docs/presence-occupancy/presence#history):
@@ -147,6 +253,23 @@ await channel.presence.leave({
timestamp: Date.now()
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_leaveWithReason(ably: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Leave with a reason
+channel.presence.leave([
+ "reason": "session-completed",
+ "timestamp": Date().timeIntervalSince1970 * 1000
+])
+```
+{/* --- end example code --- */}
Subscribers receive the `leave` data in the presence message:
@@ -161,6 +284,25 @@ await channel.presence.subscribe("leave", (presenceMessage) => {
}
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_subscribeLeave(ably: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Subscribe to leave events to see why members left
+channel.presence.subscribe(.leave) { presenceMessage in
+ print("\(presenceMessage.clientId ?? "") left")
+ if let data = presenceMessage.data {
+ print("Reason: \(data)")
+ }
+}
+```
+{/* --- end example code --- */}
### Going offline after disconnection
@@ -189,6 +331,25 @@ const ably = new Ably.Realtime({
}
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_transportParams() {
+ // --- example code starts here ---
+*/}
+```swift
+let options = ARTClientOptions(key: "{{API_KEY}}")
+options.clientId = "weather-agent"
+
+// Allow 30 seconds for agent resume and reconnection
+options.transportParams = [
+ "remainPresentFor": "30000"
+]
+
+let ably = ARTRealtime(options: options)
+```
+{/* --- end example code --- */}
## Viewing who is online
@@ -213,6 +374,25 @@ members.forEach((member) => {
console.log(`${member.clientId} (connection: ${member.connectionId})`);
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_getAllMembers(ably: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Get all currently present members
+channel.presence.get(nil) { members, error in
+ // Display each member - the same user will appear once per distinct connection
+ for member in members ?? [] {
+ print("\(member.clientId ?? "") (connection: \(member.connectionId ?? ""))")
+ }
+}
+```
+{/* --- end example code --- */}
### Subscribing to presence changes
@@ -235,6 +415,28 @@ await channel.presence.subscribe(async (presenceMessage) => {
});
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_subscribeChanges(ably: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Subscribe to changes to the presence set
+channel.presence.subscribe { presenceMessage in
+ // Get the current synced presence set after any change
+ channel.presence.get(nil) { members, error in
+ // Display each member - the same user will appear once per distinct connection
+ for member in members ?? [] {
+ print("\(member.clientId ?? "") (connection: \(member.connectionId ?? ""))")
+ }
+ }
+}
+```
+{/* --- end example code --- */}
You can also subscribe to specific presence events:
@@ -251,6 +453,27 @@ await channel.presence.subscribe("leave", (presenceMessage) => {
console.log(`${presenceMessage.clientId} left on connection ${presenceMessage.connectionId}`);
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_subscribeSpecific(ably: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Subscribe only to enter events
+channel.presence.subscribe(.enter) { presenceMessage in
+ print("\(presenceMessage.clientId ?? "") joined on connection \(presenceMessage.connectionId ?? "")")
+}
+
+// Subscribe only to leave events
+channel.presence.subscribe(.leave) { presenceMessage in
+ print("\(presenceMessage.clientId ?? "") left on connection \(presenceMessage.connectionId ?? "")")
+}
+```
+{/* --- end example code --- */}
### Detecting when a user is offline on all devices
@@ -277,4 +500,30 @@ await channel.presence.subscribe(async (presenceMessage) => {
}
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_detectOffline(ably: ARTRealtime, targetUserId: String) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+channel.presence.subscribe { presenceMessage in
+ // Get the current synced presence set
+ channel.presence.get(nil) { members, error in
+ // Check if all clients are offline
+ if members?.isEmpty ?? true {
+ print("All clients are offline")
+ }
+
+ // Check if a specific client is offline
+ if !(members?.contains(where: { $0.clientId == targetUserId }) ?? false) {
+ print("\(targetUserId) is now offline on all devices")
+ }
+ }
+}
+```
+{/* --- end example code --- */}
diff --git a/src/pages/docs/ai-transport/sessions-identity/resuming-sessions.mdx b/src/pages/docs/ai-transport/sessions-identity/resuming-sessions.mdx
index 56c99c93d4..3a0369a377 100644
--- a/src/pages/docs/ai-transport/sessions-identity/resuming-sessions.mdx
+++ b/src/pages/docs/ai-transport/sessions-identity/resuming-sessions.mdx
@@ -127,6 +127,83 @@ while (page) {
page = page.hasNext() ? await page.next() : null;
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+import Ably
+import Foundation
+
+func example_resuming_session_C4F8E2A1(
+ ably: ARTRealtime,
+ channelName: String,
+ lastProcessedTimestamp: Int64,
+ processMessage: @escaping (ARTMessage) -> Void,
+ saveLastProcessedTimestamp: @escaping (Int64) -> Void
+) {
+ // --- example code starts here ---
+*/}
+```swift
+// Use a channel in a namespace with persistence enabled
+// to access more than 2 minutes of message history
+let channel = ably.channels.get(channelName)
+
+// Subscribe to live messages (implicitly attaches the channel)
+channel.subscribe("prompt") { message in
+ // Process the live message
+ processMessage(message)
+
+ // Persist the timestamp after successful processing
+ saveLastProcessedTimestamp(message.timestamp.int64Value)
+}
+
+// Fetch history up until the point of attachment, starting from last checkpoint
+let query = ARTRealtimeHistoryQuery()
+query.untilAttach = true
+query.start = NSDate(timeIntervalSince1970: TimeInterval(lastProcessedTimestamp) / 1000.0)
+query.direction = .forwards
+
+do {
+ try channel.history(query) { result, error in
+ if let error = error {
+ print("Error fetching history: \(error.message)")
+ return
+ }
+
+ // Paginate through all missed messages
+ var page = result
+ func processPage(_ currentPage: ARTPaginatedResult?) {
+ guard let currentPage = currentPage else { return }
+
+ for message in currentPage.items {
+ // Process the historical message
+ processMessage(message)
+
+ // Persist the timestamp after successful processing
+ saveLastProcessedTimestamp(message.timestamp.int64Value)
+ }
+
+ // Move to next page if available
+ if currentPage.hasNext {
+ currentPage.next { nextPage, error in
+ if let error = error {
+ print("Error fetching next page: \(error.message)")
+ return
+ }
+ processPage(nextPage)
+ }
+ } else {
+ // All historical messages processed
+ }
+ }
+
+ processPage(page)
+ }
+} catch {
+ print("Error calling history: \(error)")
+}
+```
+{/* --- end example code --- */}
diff --git a/src/pages/docs/ai-transport/token-streaming/message-per-response.mdx b/src/pages/docs/ai-transport/token-streaming/message-per-response.mdx
index fb4fdd4cf5..84c539a72a 100644
--- a/src/pages/docs/ai-transport/token-streaming/message-per-response.mdx
+++ b/src/pages/docs/ai-transport/token-streaming/message-per-response.mdx
@@ -53,6 +53,17 @@ Use the [`get()`](/docs/api/realtime-sdk/channels#get) method to create or retri
```javascript
const channel = realtime.channels.get('ai:{{RANDOM_CHANNEL_NAME}}');
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_getChannel(realtime: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("ai:{{RANDOM_CHANNEL_NAME}}")
+```
+{/* --- end example code --- */}
To start streaming an AI response, publish the initial message. The message is identified by a server-assigned identifier called a [`serial`](/docs/messages#properties). Use the `serial` to append each subsequent token to the message as it arrives from the AI model:
@@ -70,6 +81,44 @@ for await (const event of stream) {
}
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+struct StreamEvent {
+ let type: String
+ let text: String
+}
+
+func example_publishAndAppend(channel: ARTRealtimeChannel, stream: AsyncStream) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Publish initial message and capture the serial for appending tokens
+let msgSerial = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in
+ channel.publish([ARTMessage(name: "response", data: "")]) { result, error in
+ if let error = error {
+ continuation.resume(throwing: error)
+ return
+ }
+ continuation.resume(returning: result.serials.first!)
+ }
+}
+
+// Example: stream returns events like { type: 'token', text: 'Hello' }
+for await event in stream {
+ // Append each token as it arrives
+ if event.type == "token" {
+ let messageAppend = ARTMessage(name: nil, data: event.text)
+ messageAppend.serial = msgSerial
+
+ channel.append(messageAppend, operation: nil, params: nil) { result, error in
+ // Error handling if needed
+ }
+ }
+}
+```
+{/* --- end example code --- */}
When publishing tokens, don't await the `channel.appendMessage()` call. Ably rolls up acknowledgments and debounces them for efficiency, which means awaiting each append would unnecessarily slow down your token stream. Messages are still published in the order that `appendMessage()` is called, so delivery order is not affected.
@@ -90,6 +139,50 @@ for await (const event of stream) {
}
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+struct StreamEvent {
+ let type: String
+ let text: String
+}
+
+func example_appendThroughput(channel: ARTRealtimeChannel, stream: AsyncStream, msgSerial: String) async {
+ // --- example code starts here ---
+*/}
+```swift
+// ✅ Do this - append without await for maximum throughput
+for await event in stream {
+ if event.type == "token" {
+ let messageAppend = ARTMessage(name: nil, data: event.text)
+ messageAppend.serial = msgSerial
+
+ channel.append(messageAppend, operation: nil, params: nil) { result, error in
+ // Error handling if needed
+ }
+ }
+}
+
+// ❌ Don't do this - awaiting each append reduces throughput
+for await event in stream {
+ if event.type == "token" {
+ let messageAppend = ARTMessage(name: nil, data: event.text)
+ messageAppend.serial = msgSerial
+
+ try? await withCheckedThrowingContinuation { continuation in
+ channel.append(messageAppend, operation: nil, params: nil) { result, error in
+ if let error = error {
+ continuation.resume(throwing: error)
+ } else {
+ continuation.resume(returning: ())
+ }
+ }
+ }
+ }
+}
+```
+{/* --- end example code --- */}
@@ -113,6 +206,19 @@ const realtime = new Ably.Realtime({
transportParams: { appendRollupWindow: 100 } // 10 messages/s
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_rollupConfig() {
+ // --- example code starts here ---
+*/}
+```swift
+let options = ARTClientOptions(key: "your-api-key")
+options.transportParams = ["appendRollupWindow": "100"] // 10 messages/s
+let realtime = ARTRealtime(options: options)
+```
+{/* --- end example code --- */}
The `appendRollupWindow` parameter controls how many tokens are combined into each published message for a given model output rate. This creates a trade-off between delivery smoothness and the number of concurrent model responses you can stream on a single connection:
@@ -160,6 +266,42 @@ await channel.subscribe((message) => {
}
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_subscribe(realtime: ARTRealtime) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("ai:{{RANDOM_CHANNEL_NAME}}")
+
+// Track responses by message serial
+var responses: [String: String] = [:]
+
+// Subscribe to live messages (implicitly attaches the channel)
+try await withCheckedThrowingContinuation { continuation in
+ channel.subscribe { message in
+ switch message.action {
+ case .messageCreate:
+ // New response started
+ responses[message.serial] = message.data as? String ?? ""
+ case .messageAppend:
+ // Append token to existing response
+ let current = responses[message.serial] ?? ""
+ let appendData = message.data as? String ?? ""
+ responses[message.serial] = current + appendData
+ case .messageUpdate:
+ // Replace entire response content
+ responses[message.serial] = message.data as? String ?? ""
+ default:
+ break
+ }
+ }
+ continuation.resume(returning: ())
+}
+```
+{/* --- end example code --- */}
## Client hydration
@@ -206,6 +348,46 @@ await channel.subscribe((message) => {
}
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_rewind(realtime: ARTRealtime) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Use rewind to receive recent historical messages
+let channelOptions = ARTRealtimeChannelOptions()
+channelOptions.params = ["rewind": "2m"] // or rewind: "10" for message count
+let channel = realtime.channels.get("ai:{{RANDOM_CHANNEL_NAME}}", options: channelOptions)
+
+// Track responses by message serial
+var responses: [String: String] = [:]
+
+// Subscribe to receive both recent historical and live messages,
+// which are delivered in order to the subscription
+try await withCheckedThrowingContinuation { continuation in
+ channel.subscribe { message in
+ switch message.action {
+ case .messageCreate:
+ // New response started
+ responses[message.serial] = message.data as? String ?? ""
+ case .messageAppend:
+ // Append token to existing response
+ let current = responses[message.serial] ?? ""
+ let appendData = message.data as? String ?? ""
+ responses[message.serial] = current + appendData
+ case .messageUpdate:
+ // Replace entire response content
+ responses[message.serial] = message.data as? String ?? ""
+ default:
+ break
+ }
+ }
+ continuation.resume(returning: ())
+}
+```
+{/* --- end example code --- */}
Rewind supports two formats:
@@ -262,6 +444,77 @@ while (page) {
page = page.hasNext() ? await page.next() : null;
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_history(realtime: ARTRealtime) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("ai:{{RANDOM_CHANNEL_NAME}}")
+
+// Track responses by message serial
+var responses: [String: String] = [:]
+
+// Subscribe to live messages (implicitly attaches the channel)
+channel.subscribe { message in
+ switch message.action {
+ case .messageCreate:
+ // New response started
+ responses[message.serial] = message.data as? String ?? ""
+ case .messageAppend:
+ // Append token to existing response
+ let current = responses[message.serial] ?? ""
+ let appendData = message.data as? String ?? ""
+ responses[message.serial] = current + appendData
+ case .messageUpdate:
+ // Replace entire response content
+ responses[message.serial] = message.data as? String ?? ""
+ default:
+ break
+ }
+}
+
+// Fetch history up until the point of attachment
+let query = ARTRealtimeHistoryQuery()
+query.untilAttach = true
+
+var page = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation?, Error>) in
+ channel.history(query) { result, error in
+ if let error = error {
+ continuation.resume(throwing: error)
+ } else {
+ continuation.resume(returning: result)
+ }
+ }
+}
+
+// Paginate backwards through history
+while let currentPage = page {
+ // Messages are newest-first
+ for message in currentPage.items {
+ // message.data contains the full concatenated text
+ responses[message.serial] = message.data as? String ?? ""
+ }
+
+ // Move to next page if available
+ if currentPage.hasNext {
+ page = try await withCheckedThrowingContinuation { continuation in
+ currentPage.next { result, error in
+ if let error = error {
+ continuation.resume(throwing: error)
+ } else {
+ continuation.resume(returning: result)
+ }
+ }
+ }
+ } else {
+ page = nil
+ }
+}
+```
+{/* --- end example code --- */}
### Hydrating an in-progress response
@@ -300,6 +553,55 @@ for await (const event of stream) {
}
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+struct StreamEvent {
+ let type: String
+ let text: String
+}
+
+func example_metadata(channel: ARTRealtimeChannel, stream: AsyncStream) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Publish initial message with responseId in extras
+let message = ARTMessage(name: "response", data: "")
+message.extras = [
+ "headers": [
+ "responseId": "resp_abc123" // Your database response ID
+ ]
+]
+
+let msgSerial = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in
+ channel.publish([message]) { result, error in
+ if let error = error {
+ continuation.resume(throwing: error)
+ return
+ }
+ continuation.resume(returning: result.serials.first!)
+ }
+}
+
+// Append tokens, including extras to preserve headers
+for await event in stream {
+ if event.type == "token" {
+ let messageAppend = ARTMessage(name: nil, data: event.text)
+ messageAppend.serial = msgSerial
+ messageAppend.extras = [
+ "headers": [
+ "responseId": "resp_abc123"
+ ]
+ ]
+
+ channel.append(messageAppend, operation: nil, params: nil) { result, error in
+ // Error handling if needed
+ }
+ }
+}
+```
+{/* --- end example code --- */}
@@ -354,6 +656,64 @@ await channel.subscribe((message) => {
}
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func loadResponsesFromDatabase() async -> Set {
+ return Set()
+}
+
+func example_hydrateRewind(realtime: ARTRealtime) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Load completed responses from your database
+// completedResponses is a Set of responseIds
+let completedResponses = await loadResponsesFromDatabase()
+
+// Use rewind to receive recent historical messages
+let channelOptions = ARTRealtimeChannelOptions()
+channelOptions.params = ["rewind": "2m"]
+let channel = realtime.channels.get("ai:responses", options: channelOptions)
+
+// Track in-progress responses by responseId
+var inProgressResponses: [String: String] = [:]
+
+try await withCheckedThrowingContinuation { continuation in
+ channel.subscribe { message in
+ guard let extras = message.extras as? [String: Any],
+ let headers = extras["headers"] as? [String: Any],
+ let responseId = headers["responseId"] as? String else {
+ print("Message missing responseId")
+ return
+ }
+
+ // Skip messages for responses already loaded from database
+ if completedResponses.contains(responseId) {
+ return
+ }
+
+ switch message.action {
+ case .messageCreate:
+ // New response started
+ inProgressResponses[responseId] = message.data as? String ?? ""
+ case .messageAppend:
+ // Append token to existing response
+ let current = inProgressResponses[responseId] ?? ""
+ let appendData = message.data as? String ?? ""
+ inProgressResponses[responseId] = current + appendData
+ case .messageUpdate:
+ // Replace entire response content
+ inProgressResponses[responseId] = message.data as? String ?? ""
+ default:
+ break
+ }
+ }
+ continuation.resume(returning: ())
+}
+```
+{/* --- end example code --- */}
@@ -433,6 +793,124 @@ while (page) {
page = page.hasNext() ? await page.next() : null;
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+struct CompletedResponse {
+ let id: String
+ let timestamp: Date
+}
+
+struct CompletedResponses {
+ let responses: [CompletedResponse]
+
+ func latest() -> CompletedResponse {
+ return responses.last!
+ }
+
+ func has(_ id: String) -> Bool {
+ return responses.contains { $0.id == id }
+ }
+}
+
+func loadResponsesFromDatabase() async -> CompletedResponses {
+ return CompletedResponses(responses: [])
+}
+
+func example_hydrateHistory(realtime: ARTRealtime) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Load completed responses from database (sorted by timestamp, oldest first)
+let completedResponses = await loadResponsesFromDatabase()
+
+// Get the timestamp of the latest completed response
+let latestTimestamp = completedResponses.latest().timestamp
+
+let channel = realtime.channels.get("ai:{{RANDOM_CHANNEL_NAME}}")
+
+// Track in progress responses by ID
+var inProgressResponses: [String: String] = [:]
+
+// Subscribe to live messages (implicitly attaches)
+channel.subscribe { message in
+ guard let extras = message.extras as? [String: Any],
+ let headers = extras["headers"] as? [String: Any],
+ let responseId = headers["responseId"] as? String else {
+ print("Message missing responseId")
+ return
+ }
+
+ // Skip messages for responses already loaded from database
+ if completedResponses.has(responseId) {
+ return
+ }
+
+ switch message.action {
+ case .messageCreate:
+ // New response started
+ inProgressResponses[responseId] = message.data as? String ?? ""
+ case .messageAppend:
+ // Append token to existing response
+ let current = inProgressResponses[responseId] ?? ""
+ let appendData = message.data as? String ?? ""
+ inProgressResponses[responseId] = current + appendData
+ case .messageUpdate:
+ // Replace entire response content
+ inProgressResponses[responseId] = message.data as? String ?? ""
+ default:
+ break
+ }
+}
+
+// Fetch history from the last completed response until attachment
+let query = ARTRealtimeHistoryQuery()
+query.untilAttach = true
+query.start = latestTimestamp
+query.direction = .forwards
+
+var page = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation?, Error>) in
+ channel.history(query) { result, error in
+ if let error = error {
+ continuation.resume(throwing: error)
+ } else {
+ continuation.resume(returning: result)
+ }
+ }
+}
+
+// Paginate through all missed messages
+while let currentPage = page {
+ for message in currentPage.items {
+ guard let extras = message.extras as? [String: Any],
+ let headers = extras["headers"] as? [String: Any],
+ let responseId = headers["responseId"] as? String else {
+ print("Message missing responseId")
+ continue
+ }
+
+ // message.data contains the full concatenated text so far
+ inProgressResponses[responseId] = message.data as? String ?? ""
+ }
+
+ // Move to next page if available
+ if currentPage.hasNext {
+ page = try await withCheckedThrowingContinuation { continuation in
+ currentPage.next { result, error in
+ if let error = error {
+ continuation.resume(throwing: error)
+ } else {
+ continuation.resume(returning: result)
+ }
+ }
+ }
+ } else {
+ page = nil
+ }
+}
+```
+{/* --- end example code --- */}
diff --git a/src/pages/docs/ai-transport/token-streaming/message-per-token.mdx b/src/pages/docs/ai-transport/token-streaming/message-per-token.mdx
index 8ea595cfc6..3a932b1a56 100644
--- a/src/pages/docs/ai-transport/token-streaming/message-per-token.mdx
+++ b/src/pages/docs/ai-transport/token-streaming/message-per-token.mdx
@@ -24,6 +24,17 @@ Use the [`get()`](/docs/api/realtime-sdk/channels#get) method to create or retri
```javascript
const channel = realtime.channels.get('{{RANDOM_CHANNEL_NAME}}');
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_channelGet(realtime: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+```
+{/* --- end example code --- */}
When publishing tokens, don't await the `channel.publish()` call. Ably rolls up acknowledgments and debounces them for efficiency, which means awaiting each publish would unnecessarily slow down your token stream. Messages are still published in the order that `publish()` is called, so delivery order is not affected.
@@ -44,6 +55,34 @@ for await (const event of stream) {
}
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+struct StreamEvent {
+ let type: String
+ let text: String
+}
+
+func example_publishWithoutAwait(channel: ARTRealtimeChannel, stream: AsyncStream) async {
+ // --- example code starts here ---
+*/}
+```swift
+// ✅ Do this - publish without await for maximum throughput
+for await event in stream {
+ if event.type == "token" {
+ channel.publish("token", data: event.text)
+ }
+}
+
+// ❌ Don't do this - awaiting each publish reduces throughput
+for await event in stream {
+ if event.type == "token" {
+ try? await channel.publish("token", data: event.text)
+ }
+}
+```
+{/* --- end example code --- */}
This approach maximizes throughput while maintaining ordering guarantees, allowing you to stream tokens as fast as your AI model generates them.
@@ -77,6 +116,29 @@ for await (const event of stream) {
}
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+struct StreamEvent {
+ let type: String
+ let text: String
+}
+
+func example_continuousPublish(realtime: ARTRealtime, stream: AsyncStream) async {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Example: stream returns events like { type: 'token', text: 'Hello' }
+for await event in stream {
+ if event.type == "token" {
+ channel.publish("token", data: event.text)
+ }
+}
+```
+{/* --- end example code --- */}
#### Subscribe to tokens
@@ -91,6 +153,23 @@ await channel.subscribe('token', (message) => {
console.log(token); // log each token as it arrives
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_continuousSubscribe(realtime: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Subscribe to token messages
+channel.subscribe("token") { message in
+ let token = message.data
+ print(token) // log each token as it arrives
+}
+```
+{/* --- end example code --- */}
This pattern is simple and works well when you're displaying a single, continuous stream of tokens.
@@ -120,6 +199,39 @@ for await (const event of stream) {
}
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+struct StreamEvent {
+ let type: String
+ let text: String
+ let responseId: String
+}
+
+func example_multipleResponsesPublish(realtime: ARTRealtime, stream: AsyncStream) async {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Example: stream returns events like { type: 'token', text: 'Hello', responseId: 'resp_abc123' }
+for await event in stream {
+ if event.type == "token" {
+ let message = ARTMessage(
+ name: "token",
+ data: event.text
+ )
+ message.extras = [
+ "headers": [
+ "responseId": event.responseId
+ ]
+ ]
+ channel.publish([message])
+ }
+}
+```
+{/* --- end example code --- */}
#### Subscribe to tokens
@@ -151,6 +263,38 @@ await channel.subscribe('token', (message) => {
responses.set(responseId, responses.get(responseId) + token);
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_multipleResponsesSubscribe(realtime: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Track responses by ID
+var responses: [String: String] = [:]
+
+channel.subscribe("token") { message in
+ guard let token = message.data as? String else { return }
+ guard let extras = message.extras as? [String: Any],
+ let headers = extras["headers"] as? [String: Any],
+ let responseId = headers["responseId"] as? String else {
+ print("Token missing responseId")
+ return
+ }
+
+ // Create an empty response
+ if responses[responseId] == nil {
+ responses[responseId] = ""
+ }
+
+ // Append token to response
+ responses[responseId] = (responses[responseId] ?? "") + token
+}
+```
+{/* --- end example code --- */}
### Token stream with explicit start/stop events
@@ -203,6 +347,59 @@ for await (const event of stream) {
}
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+struct StreamEvent {
+ let type: String
+ let responseId: String
+ let text: String?
+}
+
+func example_explicitEventsPublish(realtime: ARTRealtime, stream: AsyncStream) async {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+// Example: stream returns events like:
+// { type: 'message_start', responseId: 'resp_abc123' }
+// { type: 'message_delta', responseId: 'resp_abc123', text: 'Hello' }
+// { type: 'message_stop', responseId: 'resp_abc123' }
+
+for await event in stream {
+ if event.type == "message_start" {
+ // Publish response start
+ let message = ARTMessage(name: "start", data: nil)
+ message.extras = [
+ "headers": [
+ "responseId": event.responseId
+ ]
+ ]
+ channel.publish([message])
+ } else if event.type == "message_delta" {
+ // Publish tokens
+ let message = ARTMessage(name: "token", data: event.text)
+ message.extras = [
+ "headers": [
+ "responseId": event.responseId
+ ]
+ ]
+ channel.publish([message])
+ } else if event.type == "message_stop" {
+ // Publish response stop
+ let message = ARTMessage(name: "stop", data: nil)
+ message.extras = [
+ "headers": [
+ "responseId": event.responseId
+ ]
+ ]
+ channel.publish([message])
+ }
+}
+```
+{/* --- end example code --- */}
#### Subscribe to tokens
@@ -237,6 +434,47 @@ await channel.subscribe('stop', (message) => {
console.log('Response complete:', finalText);
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_explicitEventsSubscribe(realtime: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}")
+
+var responses: [String: String] = [:]
+
+// Handle response start
+channel.subscribe("start") { message in
+ guard let extras = message.extras as? [String: Any],
+ let headers = extras["headers"] as? [String: Any],
+ let responseId = headers["responseId"] as? String else { return }
+ responses[responseId] = ""
+}
+
+// Handle tokens
+channel.subscribe("token") { message in
+ guard let token = message.data as? String,
+ let extras = message.extras as? [String: Any],
+ let headers = extras["headers"] as? [String: Any],
+ let responseId = headers["responseId"] as? String else { return }
+
+ let currentText = responses[responseId] ?? ""
+ responses[responseId] = currentText + token
+}
+
+// Handle response stop
+channel.subscribe("stop") { message in
+ guard let extras = message.extras as? [String: Any],
+ let headers = extras["headers"] as? [String: Any],
+ let responseId = headers["responseId"] as? String else { return }
+ let finalText = responses[responseId]
+ print("Response complete:", finalText ?? "")
+}
+```
+{/* --- end example code --- */}
## Client hydration
@@ -271,6 +509,29 @@ await channel.subscribe('token', (message) => {
console.log('Token received:', token);
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_rewind(realtime: ARTRealtime) {
+ // --- example code starts here ---
+*/}
+```swift
+// Use rewind to receive recent historical messages
+let channelOptions = ARTRealtimeChannelOptions()
+channelOptions.params = ["rewind": "2m"] // or rewind: 100 for message count
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}", options: channelOptions)
+
+// Subscribe to receive both recent historical and live messages,
+// which are delivered in order to the subscription
+channel.subscribe("token") { message in
+ guard let token = message.data as? String else { return }
+
+ // Process tokens from both recent history and live stream
+ print("Token received:", token)
+}
+```
+{/* --- end example code --- */}
Rewind supports two formats:
@@ -315,6 +576,45 @@ while (page) {
page = page.hasNext() ? await page.next() : null;
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_history(realtime: ARTRealtime) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Use a channel in a namespace called 'persisted', which has persistence enabled
+let channel = realtime.channels.get("persisted:{{RANDOM_CHANNEL_NAME}}")
+
+var response = ""
+
+// Subscribe to live messages (implicitly attaches the channel)
+channel.subscribe("token") { message in
+ guard let token = message.data as? String else { return }
+ // Append the token to the end of the response
+ response += token
+}
+
+// Fetch history up until the point of attachment
+let query = ARTRealtimeHistoryQuery()
+query.untilAttach = true
+var page = try await channel.history(query)
+
+// Paginate backwards through history
+while page != nil {
+ // Messages are newest-first, so prepend them to response
+ for message in page!.items {
+ if let token = message.data as? String {
+ response = token + response
+ }
+ }
+
+ // Move to next page if available
+ page = page!.hasNext ? try await page!.next() : nil
+}
+```
+{/* --- end example code --- */}
### Hydrating an in-progress response
@@ -367,6 +667,55 @@ await channel.subscribe('token', (message) => {
inProgressResponses.set(responseId, inProgressResponses.get(responseId) + token);
});
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func loadResponsesFromDatabase() async -> Set {
+ return []
+}
+
+func example_hydrateRewind(realtime: ARTRealtime) async {
+ // --- example code starts here ---
+*/}
+```swift
+// Load completed responses from database
+let completedResponses = await loadResponsesFromDatabase()
+
+// Use rewind to receive recent historical messages
+let channelOptions = ARTRealtimeChannelOptions()
+channelOptions.params = ["rewind": "2m"]
+let channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}", options: channelOptions)
+
+// Track in progress responses by ID
+var inProgressResponses: [String: String] = [:]
+
+// Subscribe to receive both recent historical and live messages,
+// which are delivered in order to the subscription
+channel.subscribe("token") { message in
+ guard let token = message.data as? String,
+ let extras = message.extras as? [String: Any],
+ let headers = extras["headers"] as? [String: Any],
+ let responseId = headers["responseId"] as? String else {
+ print("Token missing responseId")
+ return
+ }
+
+ // Skip tokens for responses already hydrated from database
+ if completedResponses.contains(responseId) {
+ return
+ }
+
+ // Create an empty in-progress response
+ if inProgressResponses[responseId] == nil {
+ inProgressResponses[responseId] = ""
+ }
+
+ // Append tokens for new responses
+ inProgressResponses[responseId] = (inProgressResponses[responseId] ?? "") + token
+}
+```
+{/* --- end example code --- */}
#### Hydrate using history
@@ -438,4 +787,84 @@ while (page && !done) {
page = page.hasNext() ? await page.next() : null;
}
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func loadResponsesFromDatabase() async -> Set {
+ return []
+}
+
+func example_hydrateHistory(realtime: ARTRealtime) async throws {
+ // --- example code starts here ---
+*/}
+```swift
+// Load completed responses from database
+let completedResponses = await loadResponsesFromDatabase()
+
+// Use a channel in a namespace called 'persisted', which has persistence enabled
+let channel = realtime.channels.get("persisted:{{RANDOM_CHANNEL_NAME}}")
+
+// Track in progress responses by ID
+var inProgressResponses: [String: String] = [:]
+
+// Subscribe to live tokens (implicitly attaches)
+channel.subscribe("token") { message in
+ guard let token = message.data as? String,
+ let extras = message.extras as? [String: Any],
+ let headers = extras["headers"] as? [String: Any],
+ let responseId = headers["responseId"] as? String else {
+ print("Token missing responseId")
+ return
+ }
+
+ // Skip tokens for responses already hydrated from database
+ if completedResponses.contains(responseId) {
+ return
+ }
+
+ // Create an empty in-progress response
+ if inProgressResponses[responseId] == nil {
+ inProgressResponses[responseId] = ""
+ }
+
+ // Append live tokens for in-progress responses
+ inProgressResponses[responseId] = (inProgressResponses[responseId] ?? "") + token
+}
+
+// Paginate backwards through history until we encounter a hydrated response
+let query = ARTRealtimeHistoryQuery()
+query.untilAttach = true
+var page = try await channel.history(query)
+
+// Paginate backwards through history
+var done = false
+while let currentPage = page, !done {
+ // Messages are newest-first, so prepend them to response
+ for message in currentPage.items {
+ guard let token = message.data as? String,
+ let extras = message.extras as? [String: Any],
+ let headers = extras["headers"] as? [String: Any],
+ let responseId = headers["responseId"] as? String else { continue }
+
+ // Stop when we reach a response already loaded from database
+ if completedResponses.contains(responseId) {
+ done = true
+ break
+ }
+
+ // Create an empty in-progress response
+ if inProgressResponses[responseId] == nil {
+ inProgressResponses[responseId] = ""
+ }
+
+ // Prepend historical tokens for in-progress responses
+ inProgressResponses[responseId] = token + (inProgressResponses[responseId] ?? "")
+ }
+
+ // Move to next page if available
+ page = currentPage.hasNext ? try await currentPage.next() : nil
+}
+```
+{/* --- end example code --- */}
diff --git a/src/pages/docs/ai-transport/token-streaming/token-rate-limits.mdx b/src/pages/docs/ai-transport/token-streaming/token-rate-limits.mdx
index 8ea7a13087..d54667797b 100644
--- a/src/pages/docs/ai-transport/token-streaming/token-rate-limits.mdx
+++ b/src/pages/docs/ai-transport/token-streaming/token-rate-limits.mdx
@@ -48,6 +48,19 @@ const ably = new Ably.Realtime(
}
);
```
+
+{/* Swift example test harness: to modify and check it compiles, copy this comment into a
+temporary Swift file, paste the example code into the function body, and run `swift build`
+
+func example_appendRollupWindow() {
+ // --- example code starts here ---
+*/}
+```swift
+let options = ARTClientOptions(key: "your-api-key")
+options.transportParams = ["appendRollupWindow": "100"]
+let ably = ARTRealtime(options: options)
+```
+{/* --- end example code --- */}