agent-relay 3.2.13 → 3.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,220 @@
1
+ import Foundation
2
+
3
+ public actor RelayTransport {
4
+ public enum ConnectionState: Sendable {
5
+ case disconnected
6
+ case connecting
7
+ case connected
8
+ case reconnecting
9
+ }
10
+
11
+ public enum TransportError: Error, Sendable {
12
+ case invalidResponse
13
+ case notConnected
14
+ case sendFailed(String)
15
+ case connectionFailed(String)
16
+ }
17
+
18
+ public nonisolated let inbound: AsyncStream<Data>
19
+
20
+ private let baseURL: URL
21
+ private let session: URLSession
22
+ private let authToken: String
23
+ private var webSocketTask: URLSessionWebSocketTask?
24
+ private var receiveTask: Task<Void, Never>?
25
+ private var pingTask: Task<Void, Never>?
26
+ private var reconnectTask: Task<Void, Never>?
27
+ private var inboundContinuation: AsyncStream<Data>.Continuation?
28
+ private var state: ConnectionState = .disconnected
29
+ private var manuallyDisconnected = false
30
+ private var reconnectAttempt = 0
31
+ private var lastPongAt = Date()
32
+ private var onConnect: (@Sendable () async -> Void)?
33
+
34
+ public init(baseURL: URL, authToken: String, session: URLSession = .shared) {
35
+ self.baseURL = baseURL
36
+ self.authToken = authToken
37
+ self.session = session
38
+ var continuationRef: AsyncStream<Data>.Continuation?
39
+ self.inbound = AsyncStream<Data> { continuation in
40
+ continuationRef = continuation
41
+ }
42
+ self.inboundContinuation = continuationRef
43
+ }
44
+
45
+ public func setOnConnect(_ handler: @escaping @Sendable () async -> Void) {
46
+ self.onConnect = handler
47
+ }
48
+
49
+ public func connect() async throws {
50
+ switch state {
51
+ case .connected, .connecting:
52
+ return
53
+ case .disconnected, .reconnecting:
54
+ break
55
+ }
56
+
57
+ manuallyDisconnected = false
58
+ state = reconnectAttempt == 0 ? .connecting : .reconnecting
59
+
60
+ let isReconnect = reconnectAttempt > 0
61
+ let request = websocketRequest()
62
+ let task = session.webSocketTask(with: request)
63
+ webSocketTask = task
64
+ task.resume()
65
+ state = .connected
66
+ reconnectAttempt = 0
67
+ lastPongAt = Date()
68
+
69
+ startReceiveLoop()
70
+ startPingLoop()
71
+ if isReconnect, let onConnect {
72
+ await onConnect()
73
+ }
74
+ }
75
+
76
+ public func disconnect() {
77
+ manuallyDisconnected = true
78
+ receiveTask?.cancel()
79
+ pingTask?.cancel()
80
+ reconnectTask?.cancel()
81
+ receiveTask = nil
82
+ pingTask = nil
83
+ reconnectTask = nil
84
+ webSocketTask?.cancel(with: .goingAway, reason: nil)
85
+ webSocketTask = nil
86
+ state = .disconnected
87
+ }
88
+
89
+ public func send(_ message: Data) async throws {
90
+ guard let task = webSocketTask, state == .connected else {
91
+ throw TransportError.notConnected
92
+ }
93
+
94
+ do {
95
+ try await task.send(.data(message))
96
+ } catch {
97
+ throw TransportError.sendFailed(String(describing: error))
98
+ }
99
+ }
100
+
101
+ private func websocketRequest() -> URLRequest {
102
+ var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)
103
+ if components?.scheme == "http" { components?.scheme = "ws" }
104
+ if components?.scheme == "https" { components?.scheme = "wss" }
105
+ if components?.path.isEmpty ?? true { components?.path = "/v1/ws" }
106
+ if !(components?.path.hasSuffix("/v1/ws") ?? false) && !(components?.path.hasSuffix("/ws") ?? false) {
107
+ components?.path = "/v1/ws"
108
+ }
109
+ let existingItems = components?.queryItems ?? []
110
+ components?.queryItems = existingItems + [URLQueryItem(name: "token", value: authToken)]
111
+
112
+ var request = URLRequest(url: components?.url ?? baseURL)
113
+ request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")
114
+ request.setValue(authToken, forHTTPHeaderField: "X-API-Key")
115
+ return request
116
+ }
117
+
118
+ private func startReceiveLoop() {
119
+ receiveTask?.cancel()
120
+ receiveTask = Task { [weak self] in
121
+ guard let self else { return }
122
+ while !Task.isCancelled {
123
+ do {
124
+ guard let task = await self.webSocketTask else { return }
125
+ let message = try await task.receive()
126
+ await self.handle(message)
127
+ } catch {
128
+ await self.handleDisconnect(error: error)
129
+ return
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ private func startPingLoop() {
136
+ pingTask?.cancel()
137
+ pingTask = Task { [weak self] in
138
+ guard let self else { return }
139
+ while !Task.isCancelled {
140
+ try? await Task.sleep(for: .seconds(20))
141
+ if Task.isCancelled { return }
142
+ do {
143
+ try await self.sendPing()
144
+ } catch {
145
+ await self.handleDisconnect(error: error)
146
+ return
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ private func sendPing() async throws {
153
+ guard let task = webSocketTask else { throw TransportError.notConnected }
154
+ let before = Date()
155
+ try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
156
+ task.sendPing { error in
157
+ if let error {
158
+ continuation.resume(throwing: error)
159
+ } else {
160
+ continuation.resume(returning: ())
161
+ }
162
+ }
163
+ }
164
+ lastPongAt = Date()
165
+ if lastPongAt.timeIntervalSince(before) > 10 {
166
+ throw TransportError.connectionFailed("Pong exceeded watchdog")
167
+ }
168
+ }
169
+
170
+ private func handle(_ message: URLSessionWebSocketTask.Message) {
171
+ switch message {
172
+ case .data(let data):
173
+ inboundContinuation?.yield(data)
174
+ case .string(let string):
175
+ if let data = string.data(using: .utf8) {
176
+ inboundContinuation?.yield(data)
177
+ }
178
+ @unknown default:
179
+ break
180
+ }
181
+ }
182
+
183
+ private func handleDisconnect(error: Error) async {
184
+ receiveTask?.cancel()
185
+ pingTask?.cancel()
186
+ webSocketTask?.cancel(with: .goingAway, reason: nil)
187
+ webSocketTask = nil
188
+
189
+ guard !manuallyDisconnected else {
190
+ state = .disconnected
191
+ return
192
+ }
193
+
194
+ state = .reconnecting
195
+ let delay = reconnectDelay(for: reconnectAttempt)
196
+ reconnectAttempt += 1
197
+ reconnectTask?.cancel()
198
+ reconnectTask = Task { [weak self] in
199
+ guard let self else { return }
200
+ try? await Task.sleep(for: .milliseconds(delay))
201
+ do {
202
+ try await self.connect()
203
+ } catch {
204
+ await self.handleDisconnect(error: error)
205
+ }
206
+ }
207
+ }
208
+
209
+ private func reconnectDelay(for attempt: Int) -> Int {
210
+ switch attempt {
211
+ case 0: return 500
212
+ case 1: return 1_000
213
+ case 2: return 2_000
214
+ case 3: return 4_000
215
+ case 4: return 8_000
216
+ case 5: return 16_000
217
+ default: return 30_000
218
+ }
219
+ }
220
+ }
@@ -0,0 +1,435 @@
1
+ import Foundation
2
+
3
+ public enum AgentRuntime: String, Codable, Sendable {
4
+ case pty
5
+ case headless
6
+ }
7
+
8
+ public enum HeadlessProvider: String, Codable, Sendable {
9
+ case claude
10
+ case opencode
11
+ }
12
+
13
+ public struct RestartPolicy: Codable, Sendable {
14
+ public var enabled: Bool?
15
+ public var maxRestarts: Int?
16
+ public var cooldownMs: Int?
17
+ public var maxConsecutiveFailures: Int?
18
+
19
+ enum CodingKeys: String, CodingKey {
20
+ case enabled
21
+ case maxRestarts = "max_restarts"
22
+ case cooldownMs = "cooldown_ms"
23
+ case maxConsecutiveFailures = "max_consecutive_failures"
24
+ }
25
+
26
+ public init(enabled: Bool? = nil, maxRestarts: Int? = nil, cooldownMs: Int? = nil, maxConsecutiveFailures: Int? = nil) {
27
+ self.enabled = enabled
28
+ self.maxRestarts = maxRestarts
29
+ self.cooldownMs = cooldownMs
30
+ self.maxConsecutiveFailures = maxConsecutiveFailures
31
+ }
32
+ }
33
+
34
+ public struct AgentSpec: Codable, Sendable {
35
+ public var name: String
36
+ public var runtime: AgentRuntime
37
+ public var provider: HeadlessProvider?
38
+ public var cli: String?
39
+ public var args: [String]?
40
+ public var channels: [String]?
41
+ public var model: String?
42
+ public var cwd: String?
43
+ public var team: String?
44
+ public var shadowOf: String?
45
+ public var shadowMode: String?
46
+ public var restartPolicy: RestartPolicy?
47
+
48
+ enum CodingKeys: String, CodingKey {
49
+ case name, runtime, provider, cli, args, channels, model, cwd, team
50
+ case shadowOf = "shadow_of"
51
+ case shadowMode = "shadow_mode"
52
+ case restartPolicy = "restart_policy"
53
+ }
54
+
55
+ public init(
56
+ name: String,
57
+ runtime: AgentRuntime,
58
+ provider: HeadlessProvider? = nil,
59
+ cli: String? = nil,
60
+ args: [String]? = nil,
61
+ channels: [String]? = nil,
62
+ model: String? = nil,
63
+ cwd: String? = nil,
64
+ team: String? = nil,
65
+ shadowOf: String? = nil,
66
+ shadowMode: String? = nil,
67
+ restartPolicy: RestartPolicy? = nil
68
+ ) {
69
+ self.name = name
70
+ self.runtime = runtime
71
+ self.provider = provider
72
+ self.cli = cli
73
+ self.args = args
74
+ self.channels = channels
75
+ self.model = model
76
+ self.cwd = cwd
77
+ self.team = team
78
+ self.shadowOf = shadowOf
79
+ self.shadowMode = shadowMode
80
+ self.restartPolicy = restartPolicy
81
+ }
82
+ }
83
+
84
+ public struct RelayDelivery: Codable, Sendable {
85
+ public var deliveryId: String
86
+ public var eventId: String
87
+ public var workspaceId: String?
88
+ public var workspaceAlias: String?
89
+ public var from: String
90
+ public var target: String
91
+ public var body: String
92
+ public var threadId: String?
93
+ public var priority: Int?
94
+
95
+ enum CodingKeys: String, CodingKey {
96
+ case deliveryId = "delivery_id"
97
+ case eventId = "event_id"
98
+ case workspaceId = "workspace_id"
99
+ case workspaceAlias = "workspace_alias"
100
+ case from, target, body
101
+ case threadId = "thread_id"
102
+ case priority
103
+ }
104
+ }
105
+
106
+ public struct ProtocolErrorPayload: Codable, Sendable, Error {
107
+ public var code: String
108
+ public var message: String
109
+ public var retryable: Bool
110
+ public var data: JSONValue?
111
+ }
112
+
113
+ public struct HelloAck: Codable, Sendable {
114
+ public var brokerVersion: String
115
+ public var protocolVersion: Int
116
+
117
+ enum CodingKeys: String, CodingKey {
118
+ case brokerVersion = "broker_version"
119
+ case protocolVersion = "protocol_version"
120
+ }
121
+ }
122
+
123
+ public struct OkResponse: Codable, Sendable {
124
+ public var result: JSONValue?
125
+ }
126
+
127
+ public struct PongPayload: Codable, Sendable {
128
+ public var tsMs: Int64
129
+
130
+ enum CodingKeys: String, CodingKey {
131
+ case tsMs = "ts_ms"
132
+ }
133
+ }
134
+
135
+ public struct WorkerStreamPayload: Codable, Sendable {
136
+ public var stream: String
137
+ public var chunk: String
138
+ }
139
+
140
+ public struct WorkerExitedPayload: Codable, Sendable {
141
+ public var code: Int?
142
+ public var signal: String?
143
+ }
144
+
145
+ public struct EmptyPayload: Codable, Sendable {
146
+ public init() {}
147
+ }
148
+
149
+ public struct HelloPayload: Codable, Sendable {
150
+ public var clientName: String
151
+ public var clientVersion: String
152
+ public var apiKey: String?
153
+
154
+ enum CodingKeys: String, CodingKey {
155
+ case clientName = "client_name"
156
+ case clientVersion = "client_version"
157
+ case apiKey = "api_key"
158
+ }
159
+
160
+ public init(clientName: String, clientVersion: String, apiKey: String? = nil) {
161
+ self.clientName = clientName
162
+ self.clientVersion = clientVersion
163
+ self.apiKey = apiKey
164
+ }
165
+ }
166
+
167
+ public struct SendMessagePayload: Codable, Sendable {
168
+ public var to: String
169
+ public var text: String
170
+ public var from: String?
171
+ public var threadId: String?
172
+ public var workspaceId: String?
173
+ public var workspaceAlias: String?
174
+ public var priority: Int?
175
+ public var data: [String: JSONValue]?
176
+
177
+ enum CodingKeys: String, CodingKey {
178
+ case to, text, from, priority, data
179
+ case threadId = "thread_id"
180
+ case workspaceId = "workspace_id"
181
+ case workspaceAlias = "workspace_alias"
182
+ }
183
+ }
184
+
185
+ public struct SpawnAgentPayload: Codable, Sendable {
186
+ public var agent: AgentSpec
187
+ public var initialTask: String?
188
+ public var skipRelayPrompt: Bool?
189
+
190
+ enum CodingKeys: String, CodingKey {
191
+ case agent
192
+ case initialTask = "initial_task"
193
+ case skipRelayPrompt = "skip_relay_prompt"
194
+ }
195
+ }
196
+
197
+ public struct ReleaseAgentPayload: Codable, Sendable {
198
+ public var name: String
199
+ public var reason: String?
200
+ }
201
+
202
+ public struct PingPayload: Codable, Sendable {
203
+ public var tsMs: Int64
204
+
205
+ enum CodingKeys: String, CodingKey {
206
+ case tsMs = "ts_ms"
207
+ }
208
+ }
209
+
210
+ public enum BrokerEvent: Sendable {
211
+ case agentSpawned(AgentSpawnedEvent)
212
+ case agentReleased(AgentReleasedEvent)
213
+ case agentExit(AgentExitRequestedEvent)
214
+ case agentExited(AgentExitedEvent)
215
+ case relayInbound(RelayInboundEvent)
216
+ case workerStream(WorkerStreamEvent)
217
+ case deliveryRetry(DeliveryRetryEvent)
218
+ case deliveryDropped(DeliveryDroppedEvent)
219
+ case deliveryQueued(DeliveryStateEvent)
220
+ case deliveryInjected(DeliveryStateEvent)
221
+ case deliveryVerified(DeliveryStateEvent)
222
+ case deliveryFailed(DeliveryFailedEvent)
223
+ case deliveryActive(DeliveryStateEvent)
224
+ case deliveryAck(DeliveryStateEvent)
225
+ case workerReady(WorkerReadyEvent)
226
+ case workerError(WorkerErrorEvent)
227
+ case relaycastPublished(RelaycastPublishedEvent)
228
+ case relaycastPublishFailed(RelaycastPublishFailedEvent)
229
+ case aclDenied(ACLDeniedEvent)
230
+ case agentIdle(AgentIdleEvent)
231
+ case agentRestarting(AgentRestartingEvent)
232
+ case agentRestarted(AgentRestartedEvent)
233
+ case agentPermanentlyDead(AgentPermanentlyDeadEvent)
234
+ /// Catch-all for unrecognized broker event kinds, preserving the raw kind string
235
+ /// and the full JSON payload for forward compatibility.
236
+ case unknown(kind: String, rawJSON: Data?)
237
+ }
238
+
239
+ extension BrokerEvent: Codable {
240
+ enum CodingKeys: String, CodingKey { case kind }
241
+
242
+ public init(from decoder: Decoder) throws {
243
+ let container = try decoder.container(keyedBy: CodingKeys.self)
244
+ switch try container.decode(String.self, forKey: .kind) {
245
+ case "agent_spawned": self = .agentSpawned(try AgentSpawnedEvent(from: decoder))
246
+ case "agent_released": self = .agentReleased(try AgentReleasedEvent(from: decoder))
247
+ case "agent_exit": self = .agentExit(try AgentExitRequestedEvent(from: decoder))
248
+ case "agent_exited": self = .agentExited(try AgentExitedEvent(from: decoder))
249
+ case "relay_inbound": self = .relayInbound(try RelayInboundEvent(from: decoder))
250
+ case "worker_stream": self = .workerStream(try WorkerStreamEvent(from: decoder))
251
+ case "delivery_retry": self = .deliveryRetry(try DeliveryRetryEvent(from: decoder))
252
+ case "delivery_dropped": self = .deliveryDropped(try DeliveryDroppedEvent(from: decoder))
253
+ case "delivery_queued": self = .deliveryQueued(try DeliveryStateEvent(from: decoder))
254
+ case "delivery_injected": self = .deliveryInjected(try DeliveryStateEvent(from: decoder))
255
+ case "delivery_verified": self = .deliveryVerified(try DeliveryStateEvent(from: decoder))
256
+ case "delivery_failed": self = .deliveryFailed(try DeliveryFailedEvent(from: decoder))
257
+ case "delivery_active": self = .deliveryActive(try DeliveryStateEvent(from: decoder))
258
+ case "delivery_ack": self = .deliveryAck(try DeliveryStateEvent(from: decoder))
259
+ case "worker_ready": self = .workerReady(try WorkerReadyEvent(from: decoder))
260
+ case "worker_error": self = .workerError(try WorkerErrorEvent(from: decoder))
261
+ case "relaycast_published": self = .relaycastPublished(try RelaycastPublishedEvent(from: decoder))
262
+ case "relaycast_publish_failed": self = .relaycastPublishFailed(try RelaycastPublishFailedEvent(from: decoder))
263
+ case "acl_denied": self = .aclDenied(try ACLDeniedEvent(from: decoder))
264
+ case "agent_idle": self = .agentIdle(try AgentIdleEvent(from: decoder))
265
+ case "agent_restarting": self = .agentRestarting(try AgentRestartingEvent(from: decoder))
266
+ case "agent_restarted": self = .agentRestarted(try AgentRestartedEvent(from: decoder))
267
+ case "agent_permanently_dead": self = .agentPermanentlyDead(try AgentPermanentlyDeadEvent(from: decoder))
268
+ default:
269
+ // Forward-compatible: preserve unknown event kinds with raw JSON data
270
+ // so consumers can handle new broker events without SDK updates.
271
+ let kind = try container.decode(String.self, forKey: .kind)
272
+ self = .unknown(kind: kind, rawJSON: nil)
273
+ }
274
+ }
275
+
276
+ public func encode(to encoder: Encoder) throws {
277
+ switch self {
278
+ case .agentSpawned(let value): try value.encode(to: encoder)
279
+ case .agentReleased(let value): try value.encode(to: encoder)
280
+ case .agentExit(let value): try value.encode(to: encoder)
281
+ case .agentExited(let value): try value.encode(to: encoder)
282
+ case .relayInbound(let value): try value.encode(to: encoder)
283
+ case .workerStream(let value): try value.encode(to: encoder)
284
+ case .deliveryRetry(let value): try value.encode(to: encoder)
285
+ case .deliveryDropped(let value): try value.encode(to: encoder)
286
+ case .deliveryQueued(let value): try value.encode(to: encoder)
287
+ case .deliveryInjected(let value): try value.encode(to: encoder)
288
+ case .deliveryVerified(let value): try value.encode(to: encoder)
289
+ case .deliveryFailed(let value): try value.encode(to: encoder)
290
+ case .deliveryActive(let value): try value.encode(to: encoder)
291
+ case .deliveryAck(let value): try value.encode(to: encoder)
292
+ case .workerReady(let value): try value.encode(to: encoder)
293
+ case .workerError(let value): try value.encode(to: encoder)
294
+ case .relaycastPublished(let value): try value.encode(to: encoder)
295
+ case .relaycastPublishFailed(let value): try value.encode(to: encoder)
296
+ case .aclDenied(let value): try value.encode(to: encoder)
297
+ case .agentIdle(let value): try value.encode(to: encoder)
298
+ case .agentRestarting(let value): try value.encode(to: encoder)
299
+ case .agentRestarted(let value): try value.encode(to: encoder)
300
+ case .agentPermanentlyDead(let value): try value.encode(to: encoder)
301
+ case .unknown(let kind, _):
302
+ var container = encoder.container(keyedBy: CodingKeys.self)
303
+ try container.encode(kind, forKey: .kind)
304
+ }
305
+ }
306
+ }
307
+
308
+ public enum InboundMessage: Sendable {
309
+ case helloAck(HelloAck)
310
+ case ok(OkResponse)
311
+ case error(ProtocolErrorPayload)
312
+ case event(BrokerEvent)
313
+ case deliverRelay(RelayDelivery)
314
+ case workerStream(WorkerStreamPayload)
315
+ case workerExited(WorkerExitedPayload)
316
+ case pong(PongPayload)
317
+ /// Catch-all for unrecognized inbound message types for forward compatibility.
318
+ case unknown(type: String, rawJSON: Data?)
319
+ }
320
+
321
+ extension InboundMessage: Codable {
322
+ enum CodingKeys: String, CodingKey { case type, payload }
323
+
324
+ public init(from decoder: Decoder) throws {
325
+ let container = try decoder.container(keyedBy: CodingKeys.self)
326
+ let type = try container.decode(String.self, forKey: .type)
327
+ switch type {
328
+ case "hello_ack": self = .helloAck(try container.decode(HelloAck.self, forKey: .payload))
329
+ case "ok": self = .ok(try container.decode(OkResponse.self, forKey: .payload))
330
+ case "error": self = .error(try container.decode(ProtocolErrorPayload.self, forKey: .payload))
331
+ case "event": self = .event(try container.decode(BrokerEvent.self, forKey: .payload))
332
+ case "deliver_relay": self = .deliverRelay(try container.decode(RelayDelivery.self, forKey: .payload))
333
+ case "worker_stream": self = .workerStream(try container.decode(WorkerStreamPayload.self, forKey: .payload))
334
+ case "worker_exited": self = .workerExited(try container.decode(WorkerExitedPayload.self, forKey: .payload))
335
+ case "pong", "ping": self = .pong(try container.decode(PongPayload.self, forKey: .payload))
336
+ default:
337
+ // Forward-compatible: preserve unknown message types so consumers
338
+ // can handle new protocol frames without SDK updates.
339
+ self = .unknown(type: type, rawJSON: nil)
340
+ }
341
+ }
342
+
343
+ public func encode(to encoder: Encoder) throws {
344
+ var container = encoder.container(keyedBy: CodingKeys.self)
345
+ switch self {
346
+ case .helloAck(let payload): try container.encode("hello_ack", forKey: .type); try container.encode(payload, forKey: .payload)
347
+ case .ok(let payload): try container.encode("ok", forKey: .type); try container.encode(payload, forKey: .payload)
348
+ case .error(let payload): try container.encode("error", forKey: .type); try container.encode(payload, forKey: .payload)
349
+ case .event(let payload): try container.encode("event", forKey: .type); try container.encode(payload, forKey: .payload)
350
+ case .deliverRelay(let payload): try container.encode("deliver_relay", forKey: .type); try container.encode(payload, forKey: .payload)
351
+ case .workerStream(let payload): try container.encode("worker_stream", forKey: .type); try container.encode(payload, forKey: .payload)
352
+ case .workerExited(let payload): try container.encode("worker_exited", forKey: .type); try container.encode(payload, forKey: .payload)
353
+ case .pong(let payload): try container.encode("pong", forKey: .type); try container.encode(payload, forKey: .payload)
354
+ case .unknown(let type, _): try container.encode(type, forKey: .type)
355
+ }
356
+ }
357
+ }
358
+
359
+ public enum OutboundMessage: Sendable {
360
+ case hello(HelloPayload)
361
+ case sendMessage(SendMessagePayload)
362
+ case spawnAgent(SpawnAgentPayload)
363
+ case releaseAgent(ReleaseAgentPayload)
364
+ case ping(PingPayload)
365
+ case listAgents(EmptyPayload)
366
+ }
367
+
368
+ extension OutboundMessage: Encodable {
369
+ enum CodingKeys: String, CodingKey { case v, type, payload }
370
+
371
+ public func encode(to encoder: Encoder) throws {
372
+ var container = encoder.container(keyedBy: CodingKeys.self)
373
+ try container.encode(1, forKey: .v)
374
+ switch self {
375
+ case .hello(let payload): try container.encode("hello", forKey: .type); try container.encode(payload, forKey: .payload)
376
+ case .sendMessage(let payload): try container.encode("send_message", forKey: .type); try container.encode(payload, forKey: .payload)
377
+ case .spawnAgent(let payload): try container.encode("spawn_agent", forKey: .type); try container.encode(payload, forKey: .payload)
378
+ case .releaseAgent(let payload): try container.encode("release_agent", forKey: .type); try container.encode(payload, forKey: .payload)
379
+ case .ping(let payload): try container.encode("ping", forKey: .type); try container.encode(payload, forKey: .payload)
380
+ case .listAgents(let payload): try container.encode("list_agents", forKey: .type); try container.encode(payload, forKey: .payload)
381
+ }
382
+ }
383
+ }
384
+
385
+ public struct AgentSpawnedEvent: Codable, Sendable { public var kind: String = "agent_spawned"; public var name: String; public var runtime: AgentRuntime; public var provider: HeadlessProvider?; public var cli: String?; public var model: String?; public var parent: String?; public var pid: Int?; public var source: String? }
386
+ public struct AgentReleasedEvent: Codable, Sendable { public var kind: String = "agent_released"; public var name: String }
387
+ public struct AgentExitRequestedEvent: Codable, Sendable { public var kind: String = "agent_exit"; public var name: String; public var reason: String }
388
+ public struct AgentExitedEvent: Codable, Sendable { public var kind: String = "agent_exited"; public var name: String; public var code: Int?; public var signal: String? }
389
+ public struct RelayInboundEvent: Codable, Sendable { public var kind: String = "relay_inbound"; public var eventId: String; public var from: String; public var target: String; public var body: String; public var threadId: String?; enum CodingKeys: String, CodingKey { case kind, from, target, body; case eventId = "event_id"; case threadId = "thread_id" } }
390
+ public struct WorkerStreamEvent: Codable, Sendable { public var kind: String = "worker_stream"; public var name: String; public var stream: String; public var chunk: String }
391
+ public struct DeliveryRetryEvent: Codable, Sendable { public var kind: String = "delivery_retry"; public var name: String; public var deliveryId: String; public var eventId: String; public var attempts: Int; enum CodingKeys: String, CodingKey { case kind, name, attempts; case deliveryId = "delivery_id"; case eventId = "event_id" } }
392
+ public struct DeliveryDroppedEvent: Codable, Sendable { public var kind: String = "delivery_dropped"; public var name: String; public var count: Int; public var reason: String }
393
+ public struct DeliveryStateEvent: Codable, Sendable { public var kind: String; public var name: String; public var deliveryId: String; public var eventId: String; enum CodingKeys: String, CodingKey { case kind, name; case deliveryId = "delivery_id"; case eventId = "event_id" } }
394
+ public struct DeliveryFailedEvent: Codable, Sendable { public var kind: String = "delivery_failed"; public var name: String; public var deliveryId: String; public var eventId: String; public var reason: String; enum CodingKeys: String, CodingKey { case kind, name, reason; case deliveryId = "delivery_id"; case eventId = "event_id" } }
395
+ public struct WorkerReadyEvent: Codable, Sendable { public var kind: String = "worker_ready"; public var name: String; public var runtime: AgentRuntime; public var provider: HeadlessProvider?; public var cli: String?; public var model: String? }
396
+ public struct WorkerErrorEvent: Codable, Sendable { public var kind: String = "worker_error"; public var name: String; public var code: String; public var message: String }
397
+ public struct RelaycastPublishedEvent: Codable, Sendable { public var kind: String = "relaycast_published"; public var eventId: String; public var to: String; public var targetType: String; enum CodingKeys: String, CodingKey { case kind, to; case eventId = "event_id"; case targetType = "target_type" } }
398
+ public struct RelaycastPublishFailedEvent: Codable, Sendable { public var kind: String = "relaycast_publish_failed"; public var eventId: String; public var to: String; public var reason: String; enum CodingKeys: String, CodingKey { case kind, to, reason; case eventId = "event_id" } }
399
+ public struct ACLDeniedEvent: Codable, Sendable { public var kind: String = "acl_denied"; public var name: String; public var sender: String; public var ownerChain: [String]; enum CodingKeys: String, CodingKey { case kind, name, sender; case ownerChain = "owner_chain" } }
400
+ public struct AgentIdleEvent: Codable, Sendable { public var kind: String = "agent_idle"; public var name: String; public var idleSecs: Int; enum CodingKeys: String, CodingKey { case kind, name; case idleSecs = "idle_secs" } }
401
+ public struct AgentRestartingEvent: Codable, Sendable { public var kind: String = "agent_restarting"; public var name: String; public var code: Int?; public var signal: String?; public var restartCount: Int; public var delayMs: Int; enum CodingKeys: String, CodingKey { case kind, name, code, signal; case restartCount = "restart_count"; case delayMs = "delay_ms" } }
402
+ public struct AgentRestartedEvent: Codable, Sendable { public var kind: String = "agent_restarted"; public var name: String; public var restartCount: Int; enum CodingKeys: String, CodingKey { case kind, name; case restartCount = "restart_count" } }
403
+ public struct AgentPermanentlyDeadEvent: Codable, Sendable { public var kind: String = "agent_permanently_dead"; public var name: String; public var reason: String }
404
+
405
+ public enum JSONValue: Codable, Sendable {
406
+ case string(String)
407
+ case number(Double)
408
+ case bool(Bool)
409
+ case object([String: JSONValue])
410
+ case array([JSONValue])
411
+ case null
412
+
413
+ public init(from decoder: Decoder) throws {
414
+ let container = try decoder.singleValueContainer()
415
+ if container.decodeNil() { self = .null }
416
+ else if let value = try? container.decode(Bool.self) { self = .bool(value) }
417
+ else if let value = try? container.decode(Double.self) { self = .number(value) }
418
+ else if let value = try? container.decode(String.self) { self = .string(value) }
419
+ else if let value = try? container.decode([String: JSONValue].self) { self = .object(value) }
420
+ else if let value = try? container.decode([JSONValue].self) { self = .array(value) }
421
+ else { throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON value") }
422
+ }
423
+
424
+ public func encode(to encoder: Encoder) throws {
425
+ var container = encoder.singleValueContainer()
426
+ switch self {
427
+ case .string(let value): try container.encode(value)
428
+ case .number(let value): try container.encode(value)
429
+ case .bool(let value): try container.encode(value)
430
+ case .object(let value): try container.encode(value)
431
+ case .array(let value): try container.encode(value)
432
+ case .null: try container.encodeNil()
433
+ }
434
+ }
435
+ }
@@ -0,0 +1,15 @@
1
+ import XCTest
2
+ @testable import AgentRelaySDK
3
+
4
+ final class AgentRelaySDKTests: XCTestCase {
5
+ func testRelayCastInit() {
6
+ let relay = RelayCast(apiKey: "rk_test_key")
7
+ XCTAssertEqual(relay.apiKey, "rk_test_key")
8
+ }
9
+
10
+ func testChannelCreation() {
11
+ let relay = RelayCast(apiKey: "rk_test_key")
12
+ let channel = relay.channel("test-channel")
13
+ XCTAssertEqual(channel.name, "test-channel")
14
+ }
15
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/telemetry",
3
- "version": "3.2.13",
3
+ "version": "3.2.14",
4
4
  "description": "Anonymous telemetry for Agent Relay usage analytics",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",