agent-relay 3.2.14 → 3.2.15
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.
- package/dist/index.cjs +6 -1
- package/package.json +8 -8
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/dist/cli-registry.d.ts.map +1 -1
- package/packages/sdk/dist/cli-registry.js +6 -1
- package/packages/sdk/dist/cli-registry.js.map +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/workflow-runner.test.ts +2 -2
- package/packages/sdk/src/cli-registry.ts +6 -1
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/sdk-swift/Sources/AgentRelaySDK/RelayObserver.swift +323 -0
- package/packages/sdk-swift/Sources/AgentRelaySDK/RelayObserverTypes.swift +143 -0
- package/packages/sdk-swift/Tests/AgentRelaySDKTests/RelayObserverTests.swift +526 -0
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -54954,7 +54954,12 @@ var CLI_REGISTRY = {
|
|
|
54954
54954
|
},
|
|
54955
54955
|
codex: {
|
|
54956
54956
|
binaries: ["codex"],
|
|
54957
|
-
nonInteractiveArgs: (task, extra = []) => [
|
|
54957
|
+
nonInteractiveArgs: (task, extra = []) => [
|
|
54958
|
+
"exec",
|
|
54959
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
54960
|
+
task,
|
|
54961
|
+
...extra
|
|
54962
|
+
],
|
|
54958
54963
|
bypassFlag: "--dangerously-bypass-approvals-and-sandbox",
|
|
54959
54964
|
bypassAliases: ["--full-auto"],
|
|
54960
54965
|
searchPaths: ["~/.local/bin"]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.15",
|
|
4
4
|
"description": "Real-time agent-to-agent communication system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -176,13 +176,13 @@
|
|
|
176
176
|
},
|
|
177
177
|
"homepage": "https://github.com/AgentWorkforce/relay#readme",
|
|
178
178
|
"dependencies": {
|
|
179
|
-
"@agent-relay/config": "3.2.
|
|
180
|
-
"@agent-relay/hooks": "3.2.
|
|
181
|
-
"@agent-relay/sdk": "3.2.
|
|
182
|
-
"@agent-relay/telemetry": "3.2.
|
|
183
|
-
"@agent-relay/trajectory": "3.2.
|
|
184
|
-
"@agent-relay/user-directory": "3.2.
|
|
185
|
-
"@agent-relay/utils": "3.2.
|
|
179
|
+
"@agent-relay/config": "3.2.15",
|
|
180
|
+
"@agent-relay/hooks": "3.2.15",
|
|
181
|
+
"@agent-relay/sdk": "3.2.15",
|
|
182
|
+
"@agent-relay/telemetry": "3.2.15",
|
|
183
|
+
"@agent-relay/trajectory": "3.2.15",
|
|
184
|
+
"@agent-relay/user-directory": "3.2.15",
|
|
185
|
+
"@agent-relay/utils": "3.2.15",
|
|
186
186
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
187
187
|
"@relaycast/mcp": "1.0.0",
|
|
188
188
|
"@relaycast/sdk": "1.0.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/acp-bridge",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.15",
|
|
4
4
|
"description": "ACP (Agent Client Protocol) bridge for Agent Relay - expose relay agents to ACP-compatible editors like Zed",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@agent-relay/sdk": "3.2.
|
|
49
|
+
"@agent-relay/sdk": "3.2.15",
|
|
50
50
|
"@agentclientprotocol/sdk": "^0.12.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/hooks",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.15",
|
|
4
4
|
"description": "Hook emitter, registry, and trajectory hooks for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"test:watch": "vitest"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@agent-relay/config": "3.2.
|
|
41
|
-
"@agent-relay/trajectory": "3.2.
|
|
42
|
-
"@agent-relay/sdk": "3.2.
|
|
40
|
+
"@agent-relay/config": "3.2.15",
|
|
41
|
+
"@agent-relay/trajectory": "3.2.15",
|
|
42
|
+
"@agent-relay/sdk": "3.2.15"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^22.19.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/memory",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.15",
|
|
4
4
|
"description": "Semantic memory storage and retrieval system for agent-relay with multiple backend support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test:watch": "vitest"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agent-relay/hooks": "3.2.
|
|
25
|
+
"@agent-relay/hooks": "3.2.15"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.19.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/openclaw",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.15",
|
|
4
4
|
"description": "Relaycast bridge for OpenClaw — messaging, identity, runtime setup, and local spawning",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"postinstall": "node -e \"try{require('child_process').execSync('ldd --version 2>&1',{stdio:'pipe'})}catch{try{require('child_process').execSync('apk info gcompat 2>/dev/null',{stdio:'pipe'})}catch{console.warn('\\n\\u26a0\\ufe0f @agent-relay/openclaw: Alpine detected without gcompat. Spawning requires glibc.\\n Install with: apk add gcompat libstdc++\\n')}}\""
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@agent-relay/sdk": "3.2.
|
|
32
|
+
"@agent-relay/sdk": "3.2.15",
|
|
33
33
|
"@relaycast/sdk": "^1.0.0",
|
|
34
34
|
"ws": "^8.0.0"
|
|
35
35
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/policy",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.15",
|
|
4
4
|
"description": "Agent policy management with multi-level fallback (repo, local PRPM, cloud workspace)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test:watch": "vitest"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agent-relay/config": "3.2.
|
|
25
|
+
"@agent-relay/config": "3.2.15"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.19.3",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-registry.d.ts","sourceRoot":"","sources":["../src/cli-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAIrD,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,0DAA0D;IAC1D,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC;IACrE,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,sGAAsG;IACtG,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAID;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,UAQ/B,CAAC;
|
|
1
|
+
{"version":3,"file":"cli-registry.d.ts","sourceRoot":"","sources":["../src/cli-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAIrD,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,0DAA0D;IAC1D,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC;IACrE,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,sGAAsG;IACtG,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAID;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,UAQ/B,CAAC;AA6FF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAGvE;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC,CAE1E"}
|
|
@@ -41,7 +41,12 @@ const CLI_REGISTRY = {
|
|
|
41
41
|
},
|
|
42
42
|
codex: {
|
|
43
43
|
binaries: ['codex'],
|
|
44
|
-
nonInteractiveArgs: (task, extra = []) => [
|
|
44
|
+
nonInteractiveArgs: (task, extra = []) => [
|
|
45
|
+
'exec',
|
|
46
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
47
|
+
task,
|
|
48
|
+
...extra,
|
|
49
|
+
],
|
|
45
50
|
bypassFlag: '--dangerously-bypass-approvals-and-sandbox',
|
|
46
51
|
bypassAliases: ['--full-auto'],
|
|
47
52
|
searchPaths: ['~/.local/bin'],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-registry.js","sourceRoot":"","sources":["../src/cli-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAqBH,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,cAAc;IACd,iBAAiB;IACjB,iBAAiB;IACjB,gBAAgB;IAChB,UAAU;IACV,MAAM;IACN,mBAAmB;CACpB,CAAC;AAEF,8EAA8E;AAE9E,MAAM,YAAY,GAAoC;IACpD,MAAM,EAAE;QACN,QAAQ,EAAE,CAAC,QAAQ,CAAC;QACpB,kBAAkB,EAAE,CAAC,IAAI,EAAE,KAAK,GAAG,EAAE,EAAE,EAAE,CAAC;YACxC,IAAI;YACJ,gCAAgC;YAChC,IAAI;YACJ,GAAG,KAAK;SACT;QACD,UAAU,EAAE,gCAAgC;QAC5C,WAAW,EAAE,CAAC,iBAAiB,CAAC;KACjC;IACD,KAAK,EAAE;QACL,QAAQ,EAAE,CAAC,OAAO,CAAC;QACnB,kBAAkB,EAAE,CAAC,IAAI,EAAE,KAAK,GAAG,EAAE,EAAE,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"cli-registry.js","sourceRoot":"","sources":["../src/cli-registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/sdk",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.15",
|
|
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.
|
|
105
|
+
"@agent-relay/config": "3.2.15",
|
|
106
106
|
"@relaycast/sdk": "^1.0.0",
|
|
107
107
|
"@sinclair/typebox": "^0.34.48",
|
|
108
108
|
"chalk": "^4.1.2",
|
|
@@ -983,10 +983,10 @@ agents:
|
|
|
983
983
|
expect(args).toEqual(['-p', '--dangerously-skip-permissions', 'Do the thing']);
|
|
984
984
|
});
|
|
985
985
|
|
|
986
|
-
it('should build codex command with exec subcommand', () => {
|
|
986
|
+
it('should build codex command with exec subcommand and bypass flag', () => {
|
|
987
987
|
const { cmd, args } = WorkflowRunner.buildNonInteractiveCommand('codex', 'Build it');
|
|
988
988
|
expect(cmd).toBe('codex');
|
|
989
|
-
expect(args).toEqual(['exec', 'Build it']);
|
|
989
|
+
expect(args).toEqual(['exec', '--dangerously-bypass-approvals-and-sandbox', 'Build it']);
|
|
990
990
|
});
|
|
991
991
|
|
|
992
992
|
it('should build gemini command with -p flag', () => {
|
|
@@ -64,7 +64,12 @@ const CLI_REGISTRY: Record<AgentCli, CliDefinition> = {
|
|
|
64
64
|
},
|
|
65
65
|
codex: {
|
|
66
66
|
binaries: ['codex'],
|
|
67
|
-
nonInteractiveArgs: (task, extra = []) => [
|
|
67
|
+
nonInteractiveArgs: (task, extra = []) => [
|
|
68
|
+
'exec',
|
|
69
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
70
|
+
task,
|
|
71
|
+
...extra,
|
|
72
|
+
],
|
|
68
73
|
bypassFlag: '--dangerously-bypass-approvals-and-sandbox',
|
|
69
74
|
bypassAliases: ['--full-auto'],
|
|
70
75
|
searchPaths: ['~/.local/bin'],
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
// MARK: - Delegate
|
|
4
|
+
|
|
5
|
+
@MainActor
|
|
6
|
+
public protocol RelayObserverDelegate: AnyObject {
|
|
7
|
+
func relayObserver(_ observer: RelayObserver, didReceiveEvent event: RelayObserverEvent)
|
|
8
|
+
func relayObserverDidConnect(_ observer: RelayObserver)
|
|
9
|
+
func relayObserverDidDisconnect(_ observer: RelayObserver, error: Error?)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// MARK: - RelayObserver
|
|
13
|
+
|
|
14
|
+
public final class RelayObserver: NSObject, URLSessionWebSocketDelegate, @unchecked Sendable {
|
|
15
|
+
|
|
16
|
+
// MARK: - ConnectionState
|
|
17
|
+
|
|
18
|
+
public enum ConnectionState: Equatable, Sendable {
|
|
19
|
+
case disconnected
|
|
20
|
+
case connecting
|
|
21
|
+
case connected
|
|
22
|
+
case reconnecting(attempt: Int)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// MARK: - Public Properties (thread-safe via queue)
|
|
26
|
+
|
|
27
|
+
public var connectionState: ConnectionState {
|
|
28
|
+
queue.sync { _connectionState }
|
|
29
|
+
}
|
|
30
|
+
public var lastEvent: RelayObserverEvent? {
|
|
31
|
+
queue.sync { _lastEvent }
|
|
32
|
+
}
|
|
33
|
+
public var eventCounter: Int {
|
|
34
|
+
queue.sync { _eventCounter }
|
|
35
|
+
}
|
|
36
|
+
public weak var delegate: RelayObserverDelegate?
|
|
37
|
+
|
|
38
|
+
// MARK: - AsyncStream
|
|
39
|
+
|
|
40
|
+
public var events: AsyncStream<RelayObserverEvent> {
|
|
41
|
+
queue.sync {
|
|
42
|
+
_eventsContinuation?.finish()
|
|
43
|
+
return AsyncStream { continuation in
|
|
44
|
+
self._eventsContinuation = continuation
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// MARK: - Private Properties
|
|
50
|
+
|
|
51
|
+
private let queue = DispatchQueue(label: "com.agentrelay.observer", qos: .userInitiated)
|
|
52
|
+
private var _connectionState: ConnectionState = .disconnected
|
|
53
|
+
private var _lastEvent: RelayObserverEvent?
|
|
54
|
+
private var _eventCounter: Int = 0
|
|
55
|
+
private var webSocketTask: URLSessionWebSocketTask?
|
|
56
|
+
private var urlSession: URLSession?
|
|
57
|
+
private let maxReconnectAttempts: Int
|
|
58
|
+
private let baseReconnectDelay: TimeInterval
|
|
59
|
+
private var reconnectAttempts: Int = 0
|
|
60
|
+
private var reconnectTask: Task<Void, Never>?
|
|
61
|
+
private var subscribedChannel: String?
|
|
62
|
+
private var pendingOutbound: [String] = []
|
|
63
|
+
private var isConnectionReady: Bool = false
|
|
64
|
+
private var activeURL: URL?
|
|
65
|
+
private var _eventsContinuation: AsyncStream<RelayObserverEvent>.Continuation?
|
|
66
|
+
|
|
67
|
+
private let jsonEncoder: JSONEncoder = {
|
|
68
|
+
let encoder = JSONEncoder()
|
|
69
|
+
encoder.keyEncodingStrategy = .convertToSnakeCase
|
|
70
|
+
return encoder
|
|
71
|
+
}()
|
|
72
|
+
|
|
73
|
+
private let jsonDecoder: JSONDecoder = {
|
|
74
|
+
// NOT using convertFromSnakeCase — we use explicit CodingKeys
|
|
75
|
+
// because of dual-name fields (name/agent_name, step/step_name)
|
|
76
|
+
let decoder = JSONDecoder()
|
|
77
|
+
return decoder
|
|
78
|
+
}()
|
|
79
|
+
|
|
80
|
+
// MARK: - Init
|
|
81
|
+
|
|
82
|
+
public init(maxReconnectAttempts: Int = 8, baseReconnectDelay: TimeInterval = 1.0) {
|
|
83
|
+
self.maxReconnectAttempts = maxReconnectAttempts
|
|
84
|
+
self.baseReconnectDelay = baseReconnectDelay
|
|
85
|
+
super.init()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// MARK: - Public Methods
|
|
89
|
+
|
|
90
|
+
/// Connect to a WebSocket proxy URL and subscribe to a channel on open.
|
|
91
|
+
public func connect(url: URL, channel: String) {
|
|
92
|
+
queue.sync {
|
|
93
|
+
self.subscribedChannel = channel
|
|
94
|
+
self._openSocket(url: url)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Connect to a WebSocket proxy URL without channel subscription.
|
|
99
|
+
public func connect(url: URL) {
|
|
100
|
+
queue.sync {
|
|
101
|
+
self.subscribedChannel = nil
|
|
102
|
+
self._openSocket(url: url)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Disconnect — closes socket, cancels reconnect, clears state.
|
|
107
|
+
public func disconnect() {
|
|
108
|
+
queue.sync {
|
|
109
|
+
reconnectTask?.cancel()
|
|
110
|
+
reconnectTask = nil
|
|
111
|
+
reconnectAttempts = 0
|
|
112
|
+
isConnectionReady = false
|
|
113
|
+
subscribedChannel = nil
|
|
114
|
+
pendingOutbound.removeAll()
|
|
115
|
+
activeURL = nil
|
|
116
|
+
_closeSocket(code: .goingAway, reason: nil)
|
|
117
|
+
_connectionState = .disconnected
|
|
118
|
+
_eventsContinuation?.finish()
|
|
119
|
+
_eventsContinuation = nil
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Send a message to a channel through the proxy.
|
|
124
|
+
public func sendChannel(
|
|
125
|
+
channel: String,
|
|
126
|
+
text: String,
|
|
127
|
+
personas: [String]? = nil,
|
|
128
|
+
cliPreferences: [String: String]? = nil
|
|
129
|
+
) throws {
|
|
130
|
+
let msg = ObserverChannelSendMessage(
|
|
131
|
+
channel: channel,
|
|
132
|
+
text: text,
|
|
133
|
+
personas: personas,
|
|
134
|
+
cliPreferences: cliPreferences
|
|
135
|
+
)
|
|
136
|
+
try queue.sync {
|
|
137
|
+
try _sendEncodable(msg)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Send a direct message to a specific agent through the proxy.
|
|
142
|
+
public func sendDirect(to: String, text: String) throws {
|
|
143
|
+
let msg = ObserverDirectSendMessage(to: to, text: text)
|
|
144
|
+
try queue.sync {
|
|
145
|
+
try _sendEncodable(msg)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// MARK: - Private Methods (must be called on queue)
|
|
150
|
+
|
|
151
|
+
private func _openSocket(url: URL) {
|
|
152
|
+
_closeSocket(code: .goingAway, reason: nil)
|
|
153
|
+
activeURL = url
|
|
154
|
+
_connectionState = .connecting
|
|
155
|
+
isConnectionReady = false
|
|
156
|
+
|
|
157
|
+
let delegateQueue = OperationQueue()
|
|
158
|
+
delegateQueue.underlyingQueue = queue
|
|
159
|
+
delegateQueue.maxConcurrentOperationCount = 1
|
|
160
|
+
|
|
161
|
+
let session = URLSession(
|
|
162
|
+
configuration: .default,
|
|
163
|
+
delegate: self,
|
|
164
|
+
delegateQueue: delegateQueue
|
|
165
|
+
)
|
|
166
|
+
self.urlSession = session
|
|
167
|
+
let task = session.webSocketTask(with: url)
|
|
168
|
+
self.webSocketTask = task
|
|
169
|
+
task.resume()
|
|
170
|
+
_scheduleReceive()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private func _closeSocket(code: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
|
174
|
+
webSocketTask?.cancel(with: code, reason: reason)
|
|
175
|
+
webSocketTask = nil
|
|
176
|
+
urlSession?.invalidateAndCancel()
|
|
177
|
+
urlSession = nil
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private func _scheduleReceive() {
|
|
181
|
+
webSocketTask?.receive { [weak self] result in
|
|
182
|
+
guard let self else { return }
|
|
183
|
+
// Callback arrives on our queue via delegateQueue
|
|
184
|
+
switch result {
|
|
185
|
+
case .success(let message):
|
|
186
|
+
self._handleMessage(message)
|
|
187
|
+
self._scheduleReceive()
|
|
188
|
+
case .failure(let error):
|
|
189
|
+
self._handleSocketError(error)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private func _handleMessage(_ message: URLSessionWebSocketTask.Message) {
|
|
195
|
+
let data: Data
|
|
196
|
+
switch message {
|
|
197
|
+
case .string(let text):
|
|
198
|
+
guard let d = text.data(using: .utf8) else { return }
|
|
199
|
+
data = d
|
|
200
|
+
case .data(let d):
|
|
201
|
+
data = d
|
|
202
|
+
@unknown default:
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
guard let event = try? jsonDecoder.decode(RelayObserverEvent.self, from: data) else { return }
|
|
207
|
+
|
|
208
|
+
_lastEvent = event
|
|
209
|
+
_eventCounter += 1
|
|
210
|
+
|
|
211
|
+
if let delegate {
|
|
212
|
+
Task { @MainActor in
|
|
213
|
+
delegate.relayObserver(self, didReceiveEvent: event)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
_eventsContinuation?.yield(event)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private func _handleSocketError(_ error: Error) {
|
|
221
|
+
guard reconnectAttempts < maxReconnectAttempts else {
|
|
222
|
+
_connectionState = .disconnected
|
|
223
|
+
let delegate = self.delegate
|
|
224
|
+
Task { @MainActor in
|
|
225
|
+
delegate?.relayObserverDidDisconnect(self, error: error)
|
|
226
|
+
}
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
reconnectAttempts += 1
|
|
231
|
+
_connectionState = .reconnecting(attempt: reconnectAttempts)
|
|
232
|
+
|
|
233
|
+
let delay = baseReconnectDelay * pow(2.0, Double(reconnectAttempts - 1))
|
|
234
|
+
|
|
235
|
+
reconnectTask?.cancel()
|
|
236
|
+
reconnectTask = Task { [weak self] in
|
|
237
|
+
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
|
|
238
|
+
guard !Task.isCancelled, let self else { return }
|
|
239
|
+
self.queue.sync {
|
|
240
|
+
guard let url = self.activeURL else { return }
|
|
241
|
+
self._openSocket(url: url)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private func _sendSubscription() {
|
|
247
|
+
guard let channel = subscribedChannel else { return }
|
|
248
|
+
let msg = ObserverSubscribeMessage(channel: channel)
|
|
249
|
+
if let data = try? jsonEncoder.encode(msg),
|
|
250
|
+
let str = String(data: data, encoding: .utf8) {
|
|
251
|
+
webSocketTask?.send(.string(str)) { _ in }
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private func _sendEncodable<T: Encodable>(_ value: T) throws {
|
|
256
|
+
guard let task = webSocketTask else {
|
|
257
|
+
throw RelayObserverError.notConnected
|
|
258
|
+
}
|
|
259
|
+
guard let data = try? jsonEncoder.encode(value),
|
|
260
|
+
let str = String(data: data, encoding: .utf8) else {
|
|
261
|
+
throw RelayObserverError.encodingFailed
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if isConnectionReady {
|
|
265
|
+
task.send(.string(str)) { _ in }
|
|
266
|
+
} else {
|
|
267
|
+
pendingOutbound.append(str)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private func _flushPendingOutbound() {
|
|
272
|
+
guard let task = webSocketTask else { return }
|
|
273
|
+
for str in pendingOutbound {
|
|
274
|
+
task.send(.string(str)) { _ in }
|
|
275
|
+
}
|
|
276
|
+
pendingOutbound.removeAll()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// MARK: - URLSessionWebSocketDelegate (callbacks arrive on queue via delegateQueue)
|
|
280
|
+
|
|
281
|
+
public func urlSession(
|
|
282
|
+
_ session: URLSession,
|
|
283
|
+
webSocketTask: URLSessionWebSocketTask,
|
|
284
|
+
didOpenWithProtocol protocol: String?
|
|
285
|
+
) {
|
|
286
|
+
_connectionState = .connected
|
|
287
|
+
reconnectAttempts = 0
|
|
288
|
+
isConnectionReady = true
|
|
289
|
+
|
|
290
|
+
_flushPendingOutbound()
|
|
291
|
+
_sendSubscription()
|
|
292
|
+
|
|
293
|
+
let delegate = self.delegate
|
|
294
|
+
Task { @MainActor in
|
|
295
|
+
delegate?.relayObserverDidConnect(self)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
public func urlSession(
|
|
300
|
+
_ session: URLSession,
|
|
301
|
+
webSocketTask: URLSessionWebSocketTask,
|
|
302
|
+
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
|
|
303
|
+
reason: Data?
|
|
304
|
+
) {
|
|
305
|
+
isConnectionReady = false
|
|
306
|
+
|
|
307
|
+
if closeCode != .goingAway && closeCode != .normalClosure {
|
|
308
|
+
_handleSocketError(
|
|
309
|
+
NSError(
|
|
310
|
+
domain: "RelayObserver",
|
|
311
|
+
code: Int(closeCode.rawValue),
|
|
312
|
+
userInfo: [NSLocalizedDescriptionKey: "WebSocket closed with code \(closeCode.rawValue)"]
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
} else {
|
|
316
|
+
_connectionState = .disconnected
|
|
317
|
+
let delegate = self.delegate
|
|
318
|
+
Task { @MainActor in
|
|
319
|
+
delegate?.relayObserverDidDisconnect(self, error: nil)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
// MARK: - RelayObserverEventType
|
|
4
|
+
|
|
5
|
+
public enum RelayObserverEventType: String, Codable, Sendable {
|
|
6
|
+
case agentSpawned = "agent_spawned"
|
|
7
|
+
case agentReleased = "agent_released"
|
|
8
|
+
case agentIdle = "agent_idle"
|
|
9
|
+
case agentStatus = "agent_status"
|
|
10
|
+
case workerStream = "worker_stream"
|
|
11
|
+
case delivery = "delivery"
|
|
12
|
+
case channelMessage = "channel_message"
|
|
13
|
+
case stepStarted = "step_started"
|
|
14
|
+
case stepCompleted = "step_completed"
|
|
15
|
+
case runCompleted = "run_completed"
|
|
16
|
+
case relayConfig = "relay_config"
|
|
17
|
+
case relayWorkspace = "relay_workspace"
|
|
18
|
+
case commentPollTick = "comment_poll_tick"
|
|
19
|
+
case commentDetected = "comment_detected"
|
|
20
|
+
case error = "error"
|
|
21
|
+
case ack = "ack"
|
|
22
|
+
case connected = "connected"
|
|
23
|
+
case subscribed = "subscribed"
|
|
24
|
+
case pong = "pong"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// MARK: - RelayObserverEvent
|
|
28
|
+
|
|
29
|
+
public struct RelayObserverEvent: Decodable, Sendable {
|
|
30
|
+
public let type: RelayObserverEventType
|
|
31
|
+
|
|
32
|
+
// agent_spawned fields
|
|
33
|
+
public let name: String?
|
|
34
|
+
public let agentName: String?
|
|
35
|
+
public let cli: String?
|
|
36
|
+
public let channels: [String]?
|
|
37
|
+
|
|
38
|
+
// agent_released fields
|
|
39
|
+
public let reason: String?
|
|
40
|
+
|
|
41
|
+
// agent_idle fields
|
|
42
|
+
public let idleSecs: Int?
|
|
43
|
+
|
|
44
|
+
// agent_status fields
|
|
45
|
+
public let status: String?
|
|
46
|
+
|
|
47
|
+
// worker_stream fields
|
|
48
|
+
public let agent: String?
|
|
49
|
+
public let data: String?
|
|
50
|
+
public let stream: String?
|
|
51
|
+
|
|
52
|
+
// delivery fields
|
|
53
|
+
public let id: String?
|
|
54
|
+
public let from: String?
|
|
55
|
+
public let to: String?
|
|
56
|
+
public let text: String?
|
|
57
|
+
public let state: String?
|
|
58
|
+
|
|
59
|
+
// channel_message fields
|
|
60
|
+
public let channel: String?
|
|
61
|
+
public let timestamp: String?
|
|
62
|
+
|
|
63
|
+
// step_started / step_completed fields
|
|
64
|
+
public let step: String?
|
|
65
|
+
public let stepName: String?
|
|
66
|
+
public let output: String?
|
|
67
|
+
|
|
68
|
+
// run_completed fields
|
|
69
|
+
public let runId: String?
|
|
70
|
+
|
|
71
|
+
// relay_config fields
|
|
72
|
+
public let observerUrl: String?
|
|
73
|
+
|
|
74
|
+
// relay_workspace fields
|
|
75
|
+
public let workspaceId: String?
|
|
76
|
+
|
|
77
|
+
// comment_poll_tick fields
|
|
78
|
+
public let checkedAt: String?
|
|
79
|
+
public let intervalSeconds: Int?
|
|
80
|
+
|
|
81
|
+
// comment_detected / error fields
|
|
82
|
+
public let message: String?
|
|
83
|
+
|
|
84
|
+
enum CodingKeys: String, CodingKey {
|
|
85
|
+
case type
|
|
86
|
+
case name
|
|
87
|
+
case agentName = "agent_name"
|
|
88
|
+
case cli, channels, reason
|
|
89
|
+
case idleSecs = "idle_secs"
|
|
90
|
+
case status, agent, data, stream
|
|
91
|
+
case id, from, to, text, state
|
|
92
|
+
case channel, timestamp
|
|
93
|
+
case step
|
|
94
|
+
case stepName = "step_name"
|
|
95
|
+
case output
|
|
96
|
+
case runId = "run_id"
|
|
97
|
+
case observerUrl = "observer_url"
|
|
98
|
+
case workspaceId = "workspace_id"
|
|
99
|
+
case checkedAt = "checked_at"
|
|
100
|
+
case intervalSeconds = "interval_seconds"
|
|
101
|
+
case message
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// MARK: - RelayObserverError
|
|
106
|
+
|
|
107
|
+
public enum RelayObserverError: LocalizedError, Sendable {
|
|
108
|
+
case notConnected
|
|
109
|
+
case encodingFailed
|
|
110
|
+
|
|
111
|
+
public var errorDescription: String? {
|
|
112
|
+
switch self {
|
|
113
|
+
case .notConnected: return "Relay not connected"
|
|
114
|
+
case .encodingFailed: return "Message encoding failed"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// MARK: - Outbound Message Structs (internal)
|
|
120
|
+
|
|
121
|
+
struct ObserverSubscribeMessage: Encodable {
|
|
122
|
+
let type: String = "subscribe"
|
|
123
|
+
let channel: String
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
struct ObserverChannelSendMessage: Encodable {
|
|
127
|
+
let type: String = "channel_send"
|
|
128
|
+
let channel: String
|
|
129
|
+
let text: String
|
|
130
|
+
let personas: [String]?
|
|
131
|
+
let cliPreferences: [String: String]?
|
|
132
|
+
|
|
133
|
+
enum CodingKeys: String, CodingKey {
|
|
134
|
+
case type, channel, text, personas
|
|
135
|
+
case cliPreferences = "cli_preferences"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
struct ObserverDirectSendMessage: Encodable {
|
|
140
|
+
let type: String = "send"
|
|
141
|
+
let to: String
|
|
142
|
+
let text: String
|
|
143
|
+
}
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import AgentRelaySDK
|
|
3
|
+
|
|
4
|
+
// MARK: - Helpers
|
|
5
|
+
|
|
6
|
+
private func jsonData(_ jsonString: String) -> Data {
|
|
7
|
+
jsonString.data(using: .utf8)!
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
private func decodeEvent(_ json: String) throws -> RelayObserverEvent {
|
|
11
|
+
let decoder = JSONDecoder()
|
|
12
|
+
return try decoder.decode(RelayObserverEvent.self, from: jsonData(json))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private func encodeToDict<T: Encodable>(_ value: T) throws -> [String: Any] {
|
|
16
|
+
let encoder = JSONEncoder()
|
|
17
|
+
encoder.keyEncodingStrategy = .convertToSnakeCase
|
|
18
|
+
let data = try encoder.encode(value)
|
|
19
|
+
return try JSONSerialization.jsonObject(with: data) as! [String: Any]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// MARK: - Event Type Raw Values
|
|
23
|
+
|
|
24
|
+
final class RelayObserverEventTypeTests: XCTestCase {
|
|
25
|
+
|
|
26
|
+
func testAllRawValues() {
|
|
27
|
+
let expected: [(RelayObserverEventType, String)] = [
|
|
28
|
+
(.agentSpawned, "agent_spawned"),
|
|
29
|
+
(.agentReleased, "agent_released"),
|
|
30
|
+
(.agentIdle, "agent_idle"),
|
|
31
|
+
(.agentStatus, "agent_status"),
|
|
32
|
+
(.workerStream, "worker_stream"),
|
|
33
|
+
(.delivery, "delivery"),
|
|
34
|
+
(.channelMessage, "channel_message"),
|
|
35
|
+
(.stepStarted, "step_started"),
|
|
36
|
+
(.stepCompleted, "step_completed"),
|
|
37
|
+
(.runCompleted, "run_completed"),
|
|
38
|
+
(.relayConfig, "relay_config"),
|
|
39
|
+
(.relayWorkspace, "relay_workspace"),
|
|
40
|
+
(.commentPollTick, "comment_poll_tick"),
|
|
41
|
+
(.commentDetected, "comment_detected"),
|
|
42
|
+
(.error, "error"),
|
|
43
|
+
(.ack, "ack"),
|
|
44
|
+
(.connected, "connected"),
|
|
45
|
+
(.subscribed, "subscribed"),
|
|
46
|
+
(.pong, "pong"),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
XCTAssertEqual(expected.count, 19, "Should cover all 19 event types")
|
|
50
|
+
|
|
51
|
+
for (eventType, rawValue) in expected {
|
|
52
|
+
XCTAssertEqual(eventType.rawValue, rawValue, "Raw value mismatch for \(eventType)")
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func testRoundTripCodable() throws {
|
|
57
|
+
let encoder = JSONEncoder()
|
|
58
|
+
let decoder = JSONDecoder()
|
|
59
|
+
for eventType in [RelayObserverEventType.agentSpawned, .delivery, .pong, .error] {
|
|
60
|
+
let data = try encoder.encode(eventType)
|
|
61
|
+
let decoded = try decoder.decode(RelayObserverEventType.self, from: data)
|
|
62
|
+
XCTAssertEqual(decoded, eventType)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// MARK: - Event Decoding Tests
|
|
68
|
+
|
|
69
|
+
final class RelayObserverEventDecodingTests: XCTestCase {
|
|
70
|
+
|
|
71
|
+
// MARK: agent_spawned
|
|
72
|
+
|
|
73
|
+
func testAgentSpawnedWithName() throws {
|
|
74
|
+
let event = try decodeEvent("""
|
|
75
|
+
{
|
|
76
|
+
"type": "agent_spawned",
|
|
77
|
+
"name": "worker-1",
|
|
78
|
+
"cli": "claude",
|
|
79
|
+
"channels": ["ch-1", "ch-2"]
|
|
80
|
+
}
|
|
81
|
+
""")
|
|
82
|
+
XCTAssertEqual(event.type, .agentSpawned)
|
|
83
|
+
XCTAssertEqual(event.name, "worker-1")
|
|
84
|
+
XCTAssertNil(event.agentName)
|
|
85
|
+
XCTAssertEqual(event.cli, "claude")
|
|
86
|
+
XCTAssertEqual(event.channels, ["ch-1", "ch-2"])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func testAgentSpawnedWithAgentName() throws {
|
|
90
|
+
let event = try decodeEvent("""
|
|
91
|
+
{
|
|
92
|
+
"type": "agent_spawned",
|
|
93
|
+
"agent_name": "worker-2",
|
|
94
|
+
"cli": "codex",
|
|
95
|
+
"channels": ["ch-3"]
|
|
96
|
+
}
|
|
97
|
+
""")
|
|
98
|
+
XCTAssertEqual(event.type, .agentSpawned)
|
|
99
|
+
XCTAssertNil(event.name)
|
|
100
|
+
XCTAssertEqual(event.agentName, "worker-2")
|
|
101
|
+
XCTAssertEqual(event.cli, "codex")
|
|
102
|
+
XCTAssertEqual(event.channels, ["ch-3"])
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func testAgentSpawnedWithBothNames() throws {
|
|
106
|
+
let event = try decodeEvent("""
|
|
107
|
+
{
|
|
108
|
+
"type": "agent_spawned",
|
|
109
|
+
"name": "worker-a",
|
|
110
|
+
"agent_name": "worker-b",
|
|
111
|
+
"cli": "claude",
|
|
112
|
+
"channels": []
|
|
113
|
+
}
|
|
114
|
+
""")
|
|
115
|
+
XCTAssertEqual(event.name, "worker-a")
|
|
116
|
+
XCTAssertEqual(event.agentName, "worker-b")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// MARK: agent_released
|
|
120
|
+
|
|
121
|
+
func testAgentReleased() throws {
|
|
122
|
+
let event = try decodeEvent("""
|
|
123
|
+
{"type": "agent_released", "name": "worker-1", "reason": "task_complete"}
|
|
124
|
+
""")
|
|
125
|
+
XCTAssertEqual(event.type, .agentReleased)
|
|
126
|
+
XCTAssertEqual(event.name, "worker-1")
|
|
127
|
+
XCTAssertEqual(event.reason, "task_complete")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// MARK: agent_idle
|
|
131
|
+
|
|
132
|
+
func testAgentIdle() throws {
|
|
133
|
+
let event = try decodeEvent("""
|
|
134
|
+
{"type": "agent_idle", "name": "worker-1", "idle_secs": 120}
|
|
135
|
+
""")
|
|
136
|
+
XCTAssertEqual(event.type, .agentIdle)
|
|
137
|
+
XCTAssertEqual(event.name, "worker-1")
|
|
138
|
+
XCTAssertEqual(event.idleSecs, 120)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// MARK: agent_status
|
|
142
|
+
|
|
143
|
+
func testAgentStatus() throws {
|
|
144
|
+
let event = try decodeEvent("""
|
|
145
|
+
{"type": "agent_status", "name": "worker-1", "status": "busy"}
|
|
146
|
+
""")
|
|
147
|
+
XCTAssertEqual(event.type, .agentStatus)
|
|
148
|
+
XCTAssertEqual(event.name, "worker-1")
|
|
149
|
+
XCTAssertEqual(event.status, "busy")
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// MARK: worker_stream
|
|
153
|
+
|
|
154
|
+
func testWorkerStream() throws {
|
|
155
|
+
let event = try decodeEvent("""
|
|
156
|
+
{"type": "worker_stream", "agent": "worker-1", "data": "hello world", "stream": "stdout"}
|
|
157
|
+
""")
|
|
158
|
+
XCTAssertEqual(event.type, .workerStream)
|
|
159
|
+
XCTAssertEqual(event.agent, "worker-1")
|
|
160
|
+
XCTAssertEqual(event.data, "hello world")
|
|
161
|
+
XCTAssertEqual(event.stream, "stdout")
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// MARK: delivery
|
|
165
|
+
|
|
166
|
+
func testDeliveryCompleted() throws {
|
|
167
|
+
let event = try decodeEvent("""
|
|
168
|
+
{
|
|
169
|
+
"type": "delivery",
|
|
170
|
+
"id": "msg-1",
|
|
171
|
+
"from": "lead",
|
|
172
|
+
"to": "worker-1",
|
|
173
|
+
"text": "do this task",
|
|
174
|
+
"state": "completed"
|
|
175
|
+
}
|
|
176
|
+
""")
|
|
177
|
+
XCTAssertEqual(event.type, .delivery)
|
|
178
|
+
XCTAssertEqual(event.id, "msg-1")
|
|
179
|
+
XCTAssertEqual(event.from, "lead")
|
|
180
|
+
XCTAssertEqual(event.to, "worker-1")
|
|
181
|
+
XCTAssertEqual(event.text, "do this task")
|
|
182
|
+
XCTAssertEqual(event.state, "completed")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func testDeliveryFailed() throws {
|
|
186
|
+
let event = try decodeEvent("""
|
|
187
|
+
{
|
|
188
|
+
"type": "delivery",
|
|
189
|
+
"id": "msg-2",
|
|
190
|
+
"from": "lead",
|
|
191
|
+
"to": "worker-2",
|
|
192
|
+
"text": "another task",
|
|
193
|
+
"state": "failed"
|
|
194
|
+
}
|
|
195
|
+
""")
|
|
196
|
+
XCTAssertEqual(event.type, .delivery)
|
|
197
|
+
XCTAssertEqual(event.state, "failed")
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// MARK: channel_message
|
|
201
|
+
|
|
202
|
+
func testChannelMessage() throws {
|
|
203
|
+
let event = try decodeEvent("""
|
|
204
|
+
{
|
|
205
|
+
"type": "channel_message",
|
|
206
|
+
"channel": "general",
|
|
207
|
+
"from": "user-1",
|
|
208
|
+
"text": "hey team",
|
|
209
|
+
"timestamp": "2026-03-23T10:00:00Z"
|
|
210
|
+
}
|
|
211
|
+
""")
|
|
212
|
+
XCTAssertEqual(event.type, .channelMessage)
|
|
213
|
+
XCTAssertEqual(event.channel, "general")
|
|
214
|
+
XCTAssertEqual(event.from, "user-1")
|
|
215
|
+
XCTAssertEqual(event.text, "hey team")
|
|
216
|
+
XCTAssertEqual(event.timestamp, "2026-03-23T10:00:00Z")
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// MARK: step_started
|
|
220
|
+
|
|
221
|
+
func testStepStartedWithStep() throws {
|
|
222
|
+
let event = try decodeEvent("""
|
|
223
|
+
{"type": "step_started", "step": "build"}
|
|
224
|
+
""")
|
|
225
|
+
XCTAssertEqual(event.type, .stepStarted)
|
|
226
|
+
XCTAssertEqual(event.step, "build")
|
|
227
|
+
XCTAssertNil(event.stepName)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
func testStepStartedWithStepName() throws {
|
|
231
|
+
let event = try decodeEvent("""
|
|
232
|
+
{"type": "step_started", "step_name": "compile"}
|
|
233
|
+
""")
|
|
234
|
+
XCTAssertEqual(event.type, .stepStarted)
|
|
235
|
+
XCTAssertNil(event.step)
|
|
236
|
+
XCTAssertEqual(event.stepName, "compile")
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
func testStepStartedWithBothStepFields() throws {
|
|
240
|
+
let event = try decodeEvent("""
|
|
241
|
+
{"type": "step_started", "step": "step-1", "step_name": "Build App"}
|
|
242
|
+
""")
|
|
243
|
+
XCTAssertEqual(event.step, "step-1")
|
|
244
|
+
XCTAssertEqual(event.stepName, "Build App")
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// MARK: step_completed
|
|
248
|
+
|
|
249
|
+
func testStepCompleted() throws {
|
|
250
|
+
let event = try decodeEvent("""
|
|
251
|
+
{"type": "step_completed", "step": "build", "step_name": "Build App", "output": "success"}
|
|
252
|
+
""")
|
|
253
|
+
XCTAssertEqual(event.type, .stepCompleted)
|
|
254
|
+
XCTAssertEqual(event.step, "build")
|
|
255
|
+
XCTAssertEqual(event.stepName, "Build App")
|
|
256
|
+
XCTAssertEqual(event.output, "success")
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// MARK: run_completed
|
|
260
|
+
|
|
261
|
+
func testRunCompleted() throws {
|
|
262
|
+
let event = try decodeEvent("""
|
|
263
|
+
{"type": "run_completed", "run_id": "run-abc-123"}
|
|
264
|
+
""")
|
|
265
|
+
XCTAssertEqual(event.type, .runCompleted)
|
|
266
|
+
XCTAssertEqual(event.runId, "run-abc-123")
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// MARK: relay_config
|
|
270
|
+
|
|
271
|
+
func testRelayConfig() throws {
|
|
272
|
+
let event = try decodeEvent("""
|
|
273
|
+
{"type": "relay_config", "observer_url": "wss://relay.example.com/observe"}
|
|
274
|
+
""")
|
|
275
|
+
XCTAssertEqual(event.type, .relayConfig)
|
|
276
|
+
XCTAssertEqual(event.observerUrl, "wss://relay.example.com/observe")
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// MARK: relay_workspace
|
|
280
|
+
|
|
281
|
+
func testRelayWorkspace() throws {
|
|
282
|
+
let event = try decodeEvent("""
|
|
283
|
+
{"type": "relay_workspace", "workspace_id": "ws-42"}
|
|
284
|
+
""")
|
|
285
|
+
XCTAssertEqual(event.type, .relayWorkspace)
|
|
286
|
+
XCTAssertEqual(event.workspaceId, "ws-42")
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// MARK: comment_poll_tick
|
|
290
|
+
|
|
291
|
+
func testCommentPollTick() throws {
|
|
292
|
+
let event = try decodeEvent("""
|
|
293
|
+
{"type": "comment_poll_tick", "checked_at": "2026-03-23T12:00:00Z", "interval_seconds": 30}
|
|
294
|
+
""")
|
|
295
|
+
XCTAssertEqual(event.type, .commentPollTick)
|
|
296
|
+
XCTAssertEqual(event.checkedAt, "2026-03-23T12:00:00Z")
|
|
297
|
+
XCTAssertEqual(event.intervalSeconds, 30)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// MARK: comment_detected
|
|
301
|
+
|
|
302
|
+
func testCommentDetected() throws {
|
|
303
|
+
let event = try decodeEvent("""
|
|
304
|
+
{"type": "comment_detected", "message": "New comment on PR #42"}
|
|
305
|
+
""")
|
|
306
|
+
XCTAssertEqual(event.type, .commentDetected)
|
|
307
|
+
XCTAssertEqual(event.message, "New comment on PR #42")
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// MARK: error
|
|
311
|
+
|
|
312
|
+
func testError() throws {
|
|
313
|
+
let event = try decodeEvent("""
|
|
314
|
+
{"type": "error", "message": "something went wrong"}
|
|
315
|
+
""")
|
|
316
|
+
XCTAssertEqual(event.type, .error)
|
|
317
|
+
XCTAssertEqual(event.message, "something went wrong")
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// MARK: ack
|
|
321
|
+
|
|
322
|
+
func testAck() throws {
|
|
323
|
+
let event = try decodeEvent("""
|
|
324
|
+
{"type": "ack"}
|
|
325
|
+
""")
|
|
326
|
+
XCTAssertEqual(event.type, .ack)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// MARK: connected
|
|
330
|
+
|
|
331
|
+
func testConnected() throws {
|
|
332
|
+
let event = try decodeEvent("""
|
|
333
|
+
{"type": "connected"}
|
|
334
|
+
""")
|
|
335
|
+
XCTAssertEqual(event.type, .connected)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// MARK: subscribed
|
|
339
|
+
|
|
340
|
+
func testSubscribed() throws {
|
|
341
|
+
let event = try decodeEvent("""
|
|
342
|
+
{"type": "subscribed", "channel": "ops"}
|
|
343
|
+
""")
|
|
344
|
+
XCTAssertEqual(event.type, .subscribed)
|
|
345
|
+
XCTAssertEqual(event.channel, "ops")
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// MARK: pong
|
|
349
|
+
|
|
350
|
+
func testPong() throws {
|
|
351
|
+
let event = try decodeEvent("""
|
|
352
|
+
{"type": "pong"}
|
|
353
|
+
""")
|
|
354
|
+
XCTAssertEqual(event.type, .pong)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// MARK: - Optional Fields (minimal event)
|
|
359
|
+
|
|
360
|
+
final class RelayObserverEventOptionalFieldsTests: XCTestCase {
|
|
361
|
+
|
|
362
|
+
func testMinimalAckHasAllOptionalsNil() throws {
|
|
363
|
+
let event = try decodeEvent("""
|
|
364
|
+
{"type": "ack"}
|
|
365
|
+
""")
|
|
366
|
+
XCTAssertEqual(event.type, .ack)
|
|
367
|
+
XCTAssertNil(event.name)
|
|
368
|
+
XCTAssertNil(event.agentName)
|
|
369
|
+
XCTAssertNil(event.cli)
|
|
370
|
+
XCTAssertNil(event.channels)
|
|
371
|
+
XCTAssertNil(event.reason)
|
|
372
|
+
XCTAssertNil(event.idleSecs)
|
|
373
|
+
XCTAssertNil(event.status)
|
|
374
|
+
XCTAssertNil(event.agent)
|
|
375
|
+
XCTAssertNil(event.data)
|
|
376
|
+
XCTAssertNil(event.stream)
|
|
377
|
+
XCTAssertNil(event.id)
|
|
378
|
+
XCTAssertNil(event.from)
|
|
379
|
+
XCTAssertNil(event.to)
|
|
380
|
+
XCTAssertNil(event.text)
|
|
381
|
+
XCTAssertNil(event.state)
|
|
382
|
+
XCTAssertNil(event.channel)
|
|
383
|
+
XCTAssertNil(event.timestamp)
|
|
384
|
+
XCTAssertNil(event.step)
|
|
385
|
+
XCTAssertNil(event.stepName)
|
|
386
|
+
XCTAssertNil(event.output)
|
|
387
|
+
XCTAssertNil(event.runId)
|
|
388
|
+
XCTAssertNil(event.observerUrl)
|
|
389
|
+
XCTAssertNil(event.workspaceId)
|
|
390
|
+
XCTAssertNil(event.checkedAt)
|
|
391
|
+
XCTAssertNil(event.intervalSeconds)
|
|
392
|
+
XCTAssertNil(event.message)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// MARK: - Outbound Message Encoding
|
|
397
|
+
|
|
398
|
+
final class RelayObserverOutboundEncodingTests: XCTestCase {
|
|
399
|
+
|
|
400
|
+
func testSubscribeMessage() throws {
|
|
401
|
+
let msg = ObserverSubscribeMessage(channel: "ops")
|
|
402
|
+
let dict = try encodeToDict(msg)
|
|
403
|
+
XCTAssertEqual(dict["type"] as? String, "subscribe")
|
|
404
|
+
XCTAssertEqual(dict["channel"] as? String, "ops")
|
|
405
|
+
XCTAssertEqual(dict.count, 2)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
func testChannelSendMessageFull() throws {
|
|
409
|
+
let msg = ObserverChannelSendMessage(
|
|
410
|
+
channel: "general",
|
|
411
|
+
text: "hello",
|
|
412
|
+
personas: ["lead", "worker"],
|
|
413
|
+
cliPreferences: ["model": "opus"]
|
|
414
|
+
)
|
|
415
|
+
let dict = try encodeToDict(msg)
|
|
416
|
+
XCTAssertEqual(dict["type"] as? String, "channel_send")
|
|
417
|
+
XCTAssertEqual(dict["channel"] as? String, "general")
|
|
418
|
+
XCTAssertEqual(dict["text"] as? String, "hello")
|
|
419
|
+
XCTAssertEqual(dict["personas"] as? [String], ["lead", "worker"])
|
|
420
|
+
let prefs = dict["cli_preferences"] as? [String: String]
|
|
421
|
+
XCTAssertEqual(prefs?["model"], "opus")
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
func testChannelSendMessageMinimal() throws {
|
|
425
|
+
let msg = ObserverChannelSendMessage(
|
|
426
|
+
channel: "ops",
|
|
427
|
+
text: "ping",
|
|
428
|
+
personas: nil,
|
|
429
|
+
cliPreferences: nil
|
|
430
|
+
)
|
|
431
|
+
let dict = try encodeToDict(msg)
|
|
432
|
+
XCTAssertEqual(dict["type"] as? String, "channel_send")
|
|
433
|
+
XCTAssertEqual(dict["channel"] as? String, "ops")
|
|
434
|
+
XCTAssertEqual(dict["text"] as? String, "ping")
|
|
435
|
+
// nil optionals should not be present
|
|
436
|
+
XCTAssertNil(dict["personas"])
|
|
437
|
+
XCTAssertNil(dict["cli_preferences"])
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
func testDirectSendMessage() throws {
|
|
441
|
+
let msg = ObserverDirectSendMessage(to: "worker-1", text: "do it")
|
|
442
|
+
let dict = try encodeToDict(msg)
|
|
443
|
+
XCTAssertEqual(dict["type"] as? String, "send")
|
|
444
|
+
XCTAssertEqual(dict["to"] as? String, "worker-1")
|
|
445
|
+
XCTAssertEqual(dict["text"] as? String, "do it")
|
|
446
|
+
XCTAssertEqual(dict.count, 3)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// MARK: - RelayObserverError Tests
|
|
451
|
+
|
|
452
|
+
final class RelayObserverErrorTests: XCTestCase {
|
|
453
|
+
|
|
454
|
+
func testNotConnectedDescription() {
|
|
455
|
+
let error = RelayObserverError.notConnected
|
|
456
|
+
XCTAssertEqual(error.errorDescription, "Relay not connected")
|
|
457
|
+
XCTAssertEqual(error.localizedDescription, "Relay not connected")
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
func testEncodingFailedDescription() {
|
|
461
|
+
let error = RelayObserverError.encodingFailed
|
|
462
|
+
XCTAssertEqual(error.errorDescription, "Message encoding failed")
|
|
463
|
+
XCTAssertEqual(error.localizedDescription, "Message encoding failed")
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// MARK: - ConnectionState Tests
|
|
468
|
+
|
|
469
|
+
final class RelayObserverConnectionStateTests: XCTestCase {
|
|
470
|
+
|
|
471
|
+
func testEquatable() {
|
|
472
|
+
XCTAssertEqual(RelayObserver.ConnectionState.disconnected, .disconnected)
|
|
473
|
+
XCTAssertEqual(RelayObserver.ConnectionState.connecting, .connecting)
|
|
474
|
+
XCTAssertEqual(RelayObserver.ConnectionState.connected, .connected)
|
|
475
|
+
XCTAssertEqual(
|
|
476
|
+
RelayObserver.ConnectionState.reconnecting(attempt: 3),
|
|
477
|
+
.reconnecting(attempt: 3)
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
func testNotEqual() {
|
|
482
|
+
XCTAssertNotEqual(RelayObserver.ConnectionState.disconnected, .connecting)
|
|
483
|
+
XCTAssertNotEqual(RelayObserver.ConnectionState.connected, .disconnected)
|
|
484
|
+
XCTAssertNotEqual(
|
|
485
|
+
RelayObserver.ConnectionState.reconnecting(attempt: 1),
|
|
486
|
+
.reconnecting(attempt: 2)
|
|
487
|
+
)
|
|
488
|
+
XCTAssertNotEqual(
|
|
489
|
+
RelayObserver.ConnectionState.reconnecting(attempt: 1),
|
|
490
|
+
.connected
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// MARK: - RelayObserver Init Tests
|
|
496
|
+
|
|
497
|
+
final class RelayObserverInitTests: XCTestCase {
|
|
498
|
+
|
|
499
|
+
func testDefaultState() {
|
|
500
|
+
let observer = RelayObserver()
|
|
501
|
+
XCTAssertEqual(observer.connectionState, .disconnected)
|
|
502
|
+
XCTAssertEqual(observer.eventCounter, 0)
|
|
503
|
+
XCTAssertNil(observer.lastEvent)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
func testCustomInit() {
|
|
507
|
+
let observer = RelayObserver(maxReconnectAttempts: 3, baseReconnectDelay: 2.0)
|
|
508
|
+
XCTAssertEqual(observer.connectionState, .disconnected)
|
|
509
|
+
XCTAssertEqual(observer.eventCounter, 0)
|
|
510
|
+
XCTAssertNil(observer.lastEvent)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
func testSendChannelThrowsWhenDisconnected() {
|
|
514
|
+
let observer = RelayObserver()
|
|
515
|
+
XCTAssertThrowsError(try observer.sendChannel(channel: "ops", text: "hi")) { error in
|
|
516
|
+
XCTAssertEqual(error as? RelayObserverError, .notConnected)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
func testSendDirectThrowsWhenDisconnected() {
|
|
521
|
+
let observer = RelayObserver()
|
|
522
|
+
XCTAssertThrowsError(try observer.sendDirect(to: "agent", text: "hi")) { error in
|
|
523
|
+
XCTAssertEqual(error as? RelayObserverError, .notConnected)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/trajectory",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.15",
|
|
4
4
|
"description": "Trajectory integration utilities (trail/PDERO) for Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test:watch": "vitest"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agent-relay/config": "3.2.
|
|
25
|
+
"@agent-relay/config": "3.2.15"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.19.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/user-directory",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.15",
|
|
4
4
|
"description": "User directory service for agent-relay (per-user credential storage)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test:watch": "vitest"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agent-relay/utils": "3.2.
|
|
25
|
+
"@agent-relay/utils": "3.2.15"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.19.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/utils",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.15",
|
|
4
4
|
"description": "Shared utilities for agent-relay: logging, name generation, command resolution, update checking",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"vitest": "^3.2.4"
|
|
113
113
|
},
|
|
114
114
|
"dependencies": {
|
|
115
|
-
"@agent-relay/config": "3.2.
|
|
115
|
+
"@agent-relay/config": "3.2.15",
|
|
116
116
|
"compare-versions": "^6.1.1"
|
|
117
117
|
},
|
|
118
118
|
"publishConfig": {
|