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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/sdk",
3
- "version": "3.2.13",
3
+ "version": "3.2.14",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -102,7 +102,7 @@
102
102
  "typescript": "^5.7.3"
103
103
  },
104
104
  "dependencies": {
105
- "@agent-relay/config": "3.2.13",
105
+ "@agent-relay/config": "3.2.14",
106
106
  "@relaycast/sdk": "^1.0.0",
107
107
  "@sinclair/typebox": "^0.34.48",
108
108
  "chalk": "^4.1.2",
@@ -5089,7 +5089,7 @@ export class WorkflowRunner {
5089
5089
  channels: agentChannels,
5090
5090
  task: taskWithExit,
5091
5091
  idleThresholdSecs: agentDef.constraints?.idleThresholdSecs,
5092
- cwd: agentCwd !== this.cwd ? agentCwd : undefined,
5092
+ cwd: agentCwd,
5093
5093
  });
5094
5094
 
5095
5095
  // Re-key PTY maps if broker assigned a different name than requested
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agent-relay-sdk"
7
- version = "3.2.13"
7
+ version = "3.2.14"
8
8
  description = "Python SDK for Agent Relay workflows"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -0,0 +1,26 @@
1
+ // swift-tools-version: 5.9
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "AgentRelaySDK",
6
+ platforms: [
7
+ .macOS(.v13),
8
+ .iOS(.v16),
9
+ .watchOS(.v9),
10
+ .tvOS(.v16)
11
+ ],
12
+ products: [
13
+ .library(name: "AgentRelaySDK", targets: ["AgentRelaySDK"])
14
+ ],
15
+ targets: [
16
+ .target(
17
+ name: "AgentRelaySDK",
18
+ path: "Sources/AgentRelaySDK"
19
+ ),
20
+ .testTarget(
21
+ name: "AgentRelaySDKTests",
22
+ dependencies: ["AgentRelaySDK"],
23
+ path: "Tests/AgentRelaySDKTests"
24
+ )
25
+ ]
26
+ )
@@ -0,0 +1,39 @@
1
+ # AgentRelaySDK
2
+
3
+ Native Swift SDK for the Agent Relay broker.
4
+
5
+ ## Installation
6
+
7
+ Add the package in Swift Package Manager:
8
+
9
+ ```swift
10
+ .package(url: "https://github.com/AgentWorkforce/relay.git", revision: "0a2c878748dc34af8b617c8da5ce70af447dfa37")
11
+ ```
12
+
13
+ > Temporary until the SDK is released under a stable tag.
14
+
15
+ Then depend on `AgentRelaySDK`.
16
+
17
+ ## Quick start
18
+
19
+ ```swift
20
+ import AgentRelaySDK
21
+
22
+ let relay = RelayCast(apiKey: "rk_live_...")
23
+ let channel = relay.channel("wf-my-workflow")
24
+ try await channel.subscribe()
25
+ try await channel.post("Hello from Swift")
26
+
27
+ for await event in channel.events {
28
+ print("\(event.from): \(event.body)")
29
+ }
30
+ ```
31
+
32
+ ## API
33
+
34
+ - `RelayCast(apiKey:baseURL:)`
35
+ - `channel(_:) -> Channel`
36
+ - `registerOrRotate(name:)`
37
+ - `AgentRegistration.asClient()`
38
+ - `AgentClient.post(to:message:)`
39
+ - `AgentClient.dm(to:message:)`
@@ -0,0 +1,405 @@
1
+ import Foundation
2
+
3
+ public enum RelayError: Error, Sendable {
4
+ case invalidBaseURL(String)
5
+ case connectionFailed(String)
6
+ case handshakeFailed(String)
7
+ case protocolError(code: String, message: String, retryable: Bool)
8
+ case encodingFailed(String)
9
+ case decodingFailed(String)
10
+ case notConnected
11
+ case unsupported(String)
12
+ case timeout(String)
13
+ }
14
+
15
+ /// Connection state changes emitted by the SDK.
16
+ public enum ConnectionStateChange: Sendable {
17
+ case connected
18
+ case disconnected
19
+ case reconnecting(attempt: Int)
20
+ }
21
+
22
+ public struct RelayChannelEvent: Sendable {
23
+ public let from: String
24
+ public let body: String
25
+ public let threadId: String?
26
+ public let timestamp: Date
27
+
28
+ public init(from: String, body: String, threadId: String?, timestamp: Date = Date()) {
29
+ self.from = from
30
+ self.body = body
31
+ self.threadId = threadId
32
+ self.timestamp = timestamp
33
+ }
34
+ }
35
+
36
+ public struct AgentRegistration: Sendable {
37
+ public let agentName: String
38
+ public let token: String
39
+ private let factory: @Sendable (String, String) -> AgentClient
40
+
41
+ public init(agentName: String, token: String, factory: @escaping @Sendable (String, String) -> AgentClient) {
42
+ self.agentName = agentName
43
+ self.token = token
44
+ self.factory = factory
45
+ }
46
+
47
+ public func asClient() -> AgentClient {
48
+ factory(agentName, token)
49
+ }
50
+ }
51
+
52
+ actor RelayCore {
53
+ let apiKey: String
54
+ let transport: RelayTransport
55
+ let encoder = JSONEncoder()
56
+ let decoder = JSONDecoder()
57
+
58
+ private var handshakeInFlight = false
59
+ private var handshakeGeneration = 0
60
+ private var handshakeContinuations: [CheckedContinuation<Void, Error>] = []
61
+ private var routerTask: Task<Void, Never>?
62
+ private var channelContinuations: [String: [AsyncStream<RelayChannelEvent>.Continuation]] = [:]
63
+ private var brokerEventContinuations: [AsyncStream<BrokerEvent>.Continuation] = []
64
+ private var inboundMessageContinuations: [AsyncStream<InboundMessage>.Continuation] = []
65
+ private var connectionStateContinuations: [AsyncStream<ConnectionStateChange>.Continuation] = []
66
+
67
+ init(apiKey: String, transport: RelayTransport) {
68
+ self.apiKey = apiKey
69
+ self.transport = transport
70
+ }
71
+
72
+ func configureTransportCallbacks() async {
73
+ await transport.setOnConnect { [weak self] in
74
+ await self?.transportDidConnect()
75
+ }
76
+ }
77
+
78
+ func ensureConnected() async throws {
79
+ if routerTask == nil || routerTask?.isCancelled == true {
80
+ routerTask = Task { [weak self] in await self?.routeFrames() }
81
+ }
82
+ if handshakeInFlight {
83
+ return try await waitForHandshake()
84
+ }
85
+ handshakeInFlight = true
86
+ handshakeGeneration &+= 1
87
+ try await transport.connect()
88
+ try await send(.hello(HelloPayload(clientName: "AgentRelaySDK.Swift", clientVersion: "0.1.0", apiKey: apiKey)))
89
+ try await waitForHandshake()
90
+ }
91
+
92
+ func transportDidConnect() async {
93
+ if handshakeInFlight {
94
+ finishHandshake(with: RelayError.connectionFailed("Transport reconnected before previous handshake completed"))
95
+ }
96
+ handshakeInFlight = true
97
+ handshakeGeneration &+= 1
98
+ notifyConnectionState(.connected)
99
+ do {
100
+ try await send(.hello(HelloPayload(clientName: "AgentRelaySDK.Swift", clientVersion: "0.1.0", apiKey: apiKey)))
101
+ } catch {
102
+ finishHandshake(with: error)
103
+ }
104
+ }
105
+
106
+ func registerChannelContinuation(_ continuation: AsyncStream<RelayChannelEvent>.Continuation, for channel: String) {
107
+ channelContinuations[channel, default: []].append(continuation)
108
+ }
109
+
110
+ func registerBrokerEventContinuation(_ continuation: AsyncStream<BrokerEvent>.Continuation) {
111
+ brokerEventContinuations.append(continuation)
112
+ }
113
+
114
+ func registerInboundMessageContinuation(_ continuation: AsyncStream<InboundMessage>.Continuation) {
115
+ inboundMessageContinuations.append(continuation)
116
+ }
117
+
118
+ func registerConnectionStateContinuation(_ continuation: AsyncStream<ConnectionStateChange>.Continuation) {
119
+ connectionStateContinuations.append(continuation)
120
+ }
121
+
122
+ func sendChannelPost(channel: String, text: String) async throws {
123
+ try await ensureConnected()
124
+ try await send(.sendMessage(SendMessagePayload(to: channel, text: text, from: nil, threadId: nil, workspaceId: nil, workspaceAlias: nil, priority: nil, data: nil)))
125
+ }
126
+
127
+ func sendAgentMessage(from agentName: String, to target: String, text: String) async throws {
128
+ try await ensureConnected()
129
+ try await send(.sendMessage(SendMessagePayload(to: target, text: text, from: agentName, threadId: nil, workspaceId: nil, workspaceAlias: nil, priority: nil, data: nil)))
130
+ }
131
+
132
+ func spawnAgent(_ spec: AgentSpec, initialTask: String? = nil, skipRelayPrompt: Bool? = nil) async throws {
133
+ try await ensureConnected()
134
+ try await send(.spawnAgent(SpawnAgentPayload(agent: spec, initialTask: initialTask, skipRelayPrompt: skipRelayPrompt)))
135
+ }
136
+
137
+ func releaseAgent(name: String, reason: String? = nil) async throws {
138
+ try await ensureConnected()
139
+ try await send(.releaseAgent(ReleaseAgentPayload(name: name, reason: reason)))
140
+ }
141
+
142
+ func registerOrRotate(name: String) async throws -> AgentRegistration {
143
+ try await ensureConnected()
144
+ return AgentRegistration(agentName: name, token: name) { agentName, token in
145
+ AgentClient(core: self, agentName: agentName, token: token)
146
+ }
147
+ }
148
+
149
+ func disconnect() async {
150
+ routerTask?.cancel()
151
+ routerTask = nil
152
+ handshakeInFlight = false
153
+ let pendingHandshakes = handshakeContinuations
154
+ handshakeContinuations.removeAll()
155
+ for continuation in pendingHandshakes {
156
+ continuation.resume(throwing: RelayError.notConnected)
157
+ }
158
+ await transport.disconnect()
159
+ notifyConnectionState(.disconnected)
160
+ // Finish all event stream continuations
161
+ for continuation in brokerEventContinuations { continuation.finish() }
162
+ brokerEventContinuations.removeAll()
163
+ for continuation in inboundMessageContinuations { continuation.finish() }
164
+ inboundMessageContinuations.removeAll()
165
+ for continuations in channelContinuations.values {
166
+ for continuation in continuations { continuation.finish() }
167
+ }
168
+ channelContinuations.removeAll()
169
+ for continuation in connectionStateContinuations { continuation.finish() }
170
+ connectionStateContinuations.removeAll()
171
+ }
172
+
173
+ private func send(_ message: OutboundMessage) async throws {
174
+ do {
175
+ let data = try encoder.encode(message)
176
+ try await transport.send(data)
177
+ } catch let error as RelayTransport.TransportError {
178
+ switch error {
179
+ case .notConnected: throw RelayError.notConnected
180
+ case .connectionFailed(let message), .sendFailed(let message): throw RelayError.connectionFailed(message)
181
+ case .invalidResponse: throw RelayError.connectionFailed("Invalid response")
182
+ }
183
+ } catch {
184
+ throw RelayError.encodingFailed(String(describing: error))
185
+ }
186
+ }
187
+
188
+ private func waitForHandshake() async throws {
189
+ let generation = handshakeGeneration
190
+ try await withCheckedThrowingContinuation { continuation in
191
+ handshakeContinuations.append(continuation)
192
+ Task { [weak self] in
193
+ try? await Task.sleep(for: .seconds(10))
194
+ await self?.failHandshakeIfPending(generation: generation, with: RelayError.timeout("Timed out waiting for hello_ack"))
195
+ }
196
+ }
197
+ }
198
+
199
+ private func finishHandshake() {
200
+ handshakeInFlight = false
201
+ let continuations = handshakeContinuations
202
+ handshakeContinuations.removeAll()
203
+ for continuation in continuations {
204
+ continuation.resume(returning: ())
205
+ }
206
+ }
207
+
208
+ private func finishHandshake(with error: Error) {
209
+ handshakeInFlight = false
210
+ let continuations = handshakeContinuations
211
+ handshakeContinuations.removeAll()
212
+ for continuation in continuations {
213
+ continuation.resume(throwing: error)
214
+ }
215
+ }
216
+
217
+ private func failHandshakeIfPending(generation: Int, with error: Error) {
218
+ guard handshakeInFlight, handshakeGeneration == generation else { return }
219
+ finishHandshake(with: error)
220
+ }
221
+
222
+ private func notifyConnectionState(_ state: ConnectionStateChange) {
223
+ for continuation in connectionStateContinuations {
224
+ continuation.yield(state)
225
+ }
226
+ }
227
+
228
+ private func routeFrames() async {
229
+ for await data in transport.inbound {
230
+ do {
231
+ let inbound = try decoder.decode(InboundMessage.self, from: data)
232
+
233
+ // Notify all raw inbound message subscribers
234
+ for continuation in inboundMessageContinuations {
235
+ continuation.yield(inbound)
236
+ }
237
+
238
+ switch inbound {
239
+ case .helloAck:
240
+ finishHandshake()
241
+ case .event(let event):
242
+ // Notify all broker event subscribers
243
+ for continuation in brokerEventContinuations {
244
+ continuation.yield(event)
245
+ }
246
+
247
+ // Route relay_inbound events to channel subscribers
248
+ if case .relayInbound(let relayEvent) = event {
249
+ let message = RelayChannelEvent(from: relayEvent.from, body: relayEvent.body, threadId: relayEvent.threadId)
250
+ for continuation in channelContinuations[relayEvent.target] ?? [] {
251
+ continuation.yield(message)
252
+ }
253
+ }
254
+ case .deliverRelay(let delivery):
255
+ // Route relay deliveries to channel subscribers as RelayChannelEvents
256
+ let message = RelayChannelEvent(from: delivery.from, body: delivery.body, threadId: delivery.threadId)
257
+ for continuation in channelContinuations[delivery.target] ?? [] {
258
+ continuation.yield(message)
259
+ }
260
+ case .error(let error):
261
+ finishHandshake(with: RelayError.protocolError(code: error.code, message: error.message, retryable: error.retryable))
262
+ default:
263
+ break
264
+ }
265
+ } catch {
266
+ continue
267
+ }
268
+ }
269
+ // Transport stream ended (disconnection)
270
+ notifyConnectionState(.disconnected)
271
+ }
272
+ }
273
+
274
+ public final class RelayCast: @unchecked Sendable {
275
+ private let core: RelayCore
276
+ public let apiKey: String
277
+ public let baseURL: URL
278
+
279
+ public init(apiKey: String, baseURL: URL? = nil) {
280
+ self.apiKey = apiKey
281
+ let resolved = Self.resolveBaseURL(from: baseURL)
282
+ self.baseURL = resolved
283
+ let transport = RelayTransport(baseURL: resolved, authToken: apiKey)
284
+ self.core = RelayCore(apiKey: apiKey, transport: transport)
285
+ Task {
286
+ await self.core.configureTransportCallbacks()
287
+ }
288
+ }
289
+
290
+ /// Create a channel handle for subscribing and posting.
291
+ public func channel(_ name: String) -> Channel {
292
+ Channel(name: name, core: core)
293
+ }
294
+
295
+ /// Register (or re-register) an agent identity with the broker.
296
+ public func registerOrRotate(name: String) async throws -> AgentRegistration {
297
+ try await core.registerOrRotate(name: name)
298
+ }
299
+
300
+ /// Create an agent client from an existing agent name and token.
301
+ public func `as`(agentName: String, token: String) -> AgentClient {
302
+ AgentClient(core: core, agentName: agentName, token: token)
303
+ }
304
+
305
+ /// Spawn a new agent process on the broker.
306
+ public func spawnAgent(_ spec: AgentSpec, initialTask: String? = nil, skipRelayPrompt: Bool? = nil) async throws {
307
+ try await core.spawnAgent(spec, initialTask: initialTask, skipRelayPrompt: skipRelayPrompt)
308
+ }
309
+
310
+ /// Release (stop) a named agent on the broker.
311
+ public func releaseAgent(name: String, reason: String? = nil) async throws {
312
+ try await core.releaseAgent(name: name, reason: reason)
313
+ }
314
+
315
+ /// Disconnect from the broker and cancel all event streams.
316
+ public func disconnect() async {
317
+ await core.disconnect()
318
+ }
319
+
320
+ /// Stream of all broker events (agent_spawned, worker_stream, delivery_*, etc.).
321
+ ///
322
+ /// This provides full visibility into broker activity, suitable for building
323
+ /// agent dashboards, monitoring tools, or custom event routing.
324
+ ///
325
+ /// Call `ensureConnected()` on a channel or register an agent first to start
326
+ /// receiving events.
327
+ public var brokerEvents: AsyncStream<BrokerEvent> {
328
+ AsyncStream<BrokerEvent> { continuation in
329
+ Task { await core.registerBrokerEventContinuation(continuation) }
330
+ }
331
+ }
332
+
333
+ /// Stream of all raw inbound protocol messages.
334
+ ///
335
+ /// This is the lowest-level event stream, including hello_ack, ok, error,
336
+ /// event, deliver_relay, worker_stream, worker_exited, and pong frames.
337
+ /// Use this when you need full protocol visibility.
338
+ public var inboundMessages: AsyncStream<InboundMessage> {
339
+ AsyncStream<InboundMessage> { continuation in
340
+ Task { await core.registerInboundMessageContinuation(continuation) }
341
+ }
342
+ }
343
+
344
+ /// Stream of connection state changes (connected, disconnected, reconnecting).
345
+ public var connectionState: AsyncStream<ConnectionStateChange> {
346
+ AsyncStream<ConnectionStateChange> { continuation in
347
+ Task { await core.registerConnectionStateContinuation(continuation) }
348
+ }
349
+ }
350
+
351
+ private static func resolveBaseURL(from baseURL: URL?) -> URL {
352
+ if let baseURL {
353
+ return baseURL
354
+ }
355
+ return URL(string: "http://localhost:3889")!
356
+ }
357
+ }
358
+
359
+ public final class Channel: @unchecked Sendable {
360
+ public let name: String
361
+ private let core: RelayCore
362
+ private let continuationRef: AsyncStream<RelayChannelEvent>.Continuation?
363
+ public let events: AsyncStream<RelayChannelEvent>
364
+
365
+ init(name: String, core: RelayCore) {
366
+ self.name = name
367
+ self.core = core
368
+ var continuation: AsyncStream<RelayChannelEvent>.Continuation?
369
+ self.events = AsyncStream<RelayChannelEvent> { incoming in
370
+ continuation = incoming
371
+ }
372
+ self.continuationRef = continuation
373
+ }
374
+
375
+ public func subscribe() async throws {
376
+ if let continuationRef {
377
+ await core.registerChannelContinuation(continuationRef, for: name)
378
+ }
379
+ try await core.ensureConnected()
380
+ }
381
+
382
+ public func post(_ text: String) async throws {
383
+ try await core.sendChannelPost(channel: name, text: text)
384
+ }
385
+ }
386
+
387
+ public final class AgentClient: @unchecked Sendable {
388
+ private let core: RelayCore
389
+ public let agentName: String
390
+ public let token: String
391
+
392
+ init(core: RelayCore, agentName: String, token: String) {
393
+ self.core = core
394
+ self.agentName = agentName
395
+ self.token = token
396
+ }
397
+
398
+ public func post(to channel: String, message: String) async throws {
399
+ try await core.sendAgentMessage(from: agentName, to: channel, text: message)
400
+ }
401
+
402
+ public func dm(to agentName: String, message: String) async throws {
403
+ try await core.sendAgentMessage(from: self.agentName, to: agentName, text: message)
404
+ }
405
+ }