@agent-relay/sdk 2.3.14 → 3.0.0

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.
Files changed (179) hide show
  1. package/README.md +68 -838
  2. package/bin/agent-relay-broker +0 -0
  3. package/dist/__tests__/contract-fixtures.test.d.ts +2 -0
  4. package/dist/__tests__/contract-fixtures.test.d.ts.map +1 -0
  5. package/dist/__tests__/contract-fixtures.test.js +85 -0
  6. package/dist/__tests__/contract-fixtures.test.js.map +1 -0
  7. package/dist/__tests__/facade.test.d.ts +2 -0
  8. package/dist/__tests__/facade.test.d.ts.map +1 -0
  9. package/dist/__tests__/facade.test.js +257 -0
  10. package/dist/__tests__/facade.test.js.map +1 -0
  11. package/dist/__tests__/integration.test.d.ts +2 -0
  12. package/dist/__tests__/integration.test.d.ts.map +1 -0
  13. package/dist/__tests__/integration.test.js +164 -0
  14. package/dist/__tests__/integration.test.js.map +1 -0
  15. package/dist/__tests__/pty.test.d.ts +2 -0
  16. package/dist/__tests__/pty.test.d.ts.map +1 -0
  17. package/dist/__tests__/pty.test.js +20 -0
  18. package/dist/__tests__/pty.test.js.map +1 -0
  19. package/dist/__tests__/quickstart.test.d.ts +2 -0
  20. package/dist/__tests__/quickstart.test.d.ts.map +1 -0
  21. package/dist/__tests__/quickstart.test.js +176 -0
  22. package/dist/__tests__/quickstart.test.js.map +1 -0
  23. package/dist/__tests__/spawn-from-env.test.d.ts +2 -0
  24. package/dist/__tests__/spawn-from-env.test.d.ts.map +1 -0
  25. package/dist/__tests__/spawn-from-env.test.js +206 -0
  26. package/dist/__tests__/spawn-from-env.test.js.map +1 -0
  27. package/dist/__tests__/unit.test.d.ts +2 -0
  28. package/dist/__tests__/unit.test.d.ts.map +1 -0
  29. package/dist/__tests__/unit.test.js +311 -0
  30. package/dist/__tests__/unit.test.js.map +1 -0
  31. package/dist/browser.d.ts +16 -0
  32. package/dist/browser.d.ts.map +1 -0
  33. package/dist/browser.js +19 -0
  34. package/dist/browser.js.map +1 -0
  35. package/dist/client.d.ts +138 -526
  36. package/dist/client.d.ts.map +1 -1
  37. package/dist/client.js +407 -1509
  38. package/dist/client.js.map +1 -1
  39. package/dist/consensus-helpers.d.ts +103 -0
  40. package/dist/consensus-helpers.d.ts.map +1 -0
  41. package/dist/consensus-helpers.js +147 -0
  42. package/dist/consensus-helpers.js.map +1 -0
  43. package/dist/consensus.d.ts +72 -0
  44. package/dist/consensus.d.ts.map +1 -0
  45. package/dist/consensus.js +378 -0
  46. package/dist/consensus.js.map +1 -0
  47. package/dist/examples/demo.d.ts +2 -0
  48. package/dist/examples/demo.d.ts.map +1 -0
  49. package/dist/examples/demo.js +63 -0
  50. package/dist/examples/demo.js.map +1 -0
  51. package/dist/examples/example.d.ts +2 -0
  52. package/dist/examples/example.d.ts.map +1 -0
  53. package/dist/examples/example.js +80 -0
  54. package/dist/examples/example.js.map +1 -0
  55. package/dist/examples/quickstart.d.ts +2 -0
  56. package/dist/examples/quickstart.d.ts.map +1 -0
  57. package/dist/examples/quickstart.js +56 -0
  58. package/dist/examples/quickstart.js.map +1 -0
  59. package/dist/examples/ralph-loop.d.ts +2 -0
  60. package/dist/examples/ralph-loop.d.ts.map +1 -0
  61. package/dist/examples/ralph-loop.js +281 -0
  62. package/dist/examples/ralph-loop.js.map +1 -0
  63. package/dist/examples/workflow-superiority.d.ts +32 -0
  64. package/dist/examples/workflow-superiority.d.ts.map +1 -0
  65. package/dist/examples/workflow-superiority.js +1421 -0
  66. package/dist/examples/workflow-superiority.js.map +1 -0
  67. package/dist/index.d.ts +13 -20
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +12 -26
  70. package/dist/index.js.map +1 -1
  71. package/dist/logs.d.ts +70 -25
  72. package/dist/logs.d.ts.map +1 -1
  73. package/dist/logs.js +238 -42
  74. package/dist/logs.js.map +1 -1
  75. package/dist/models.d.ts +9 -0
  76. package/dist/models.d.ts.map +1 -0
  77. package/dist/models.js +17 -0
  78. package/dist/models.js.map +1 -0
  79. package/dist/protocol.d.ts +366 -0
  80. package/dist/protocol.d.ts.map +1 -0
  81. package/dist/protocol.js +2 -0
  82. package/dist/protocol.js.map +1 -0
  83. package/dist/pty.d.ts +8 -0
  84. package/dist/pty.d.ts.map +1 -0
  85. package/dist/pty.js +26 -0
  86. package/dist/pty.js.map +1 -0
  87. package/dist/relay-adapter.d.ts +139 -0
  88. package/dist/relay-adapter.d.ts.map +1 -0
  89. package/dist/relay-adapter.js +210 -0
  90. package/dist/relay-adapter.js.map +1 -0
  91. package/dist/relay.d.ts +277 -0
  92. package/dist/relay.d.ts.map +1 -0
  93. package/dist/relay.js +853 -0
  94. package/dist/relay.js.map +1 -0
  95. package/dist/shadow.d.ts +101 -0
  96. package/dist/shadow.d.ts.map +1 -0
  97. package/dist/shadow.js +174 -0
  98. package/dist/shadow.js.map +1 -0
  99. package/dist/spawn-from-env.d.ts +77 -0
  100. package/dist/spawn-from-env.d.ts.map +1 -0
  101. package/dist/spawn-from-env.js +172 -0
  102. package/dist/spawn-from-env.js.map +1 -0
  103. package/dist/workflows/barrier.d.ts +72 -0
  104. package/dist/workflows/barrier.d.ts.map +1 -0
  105. package/dist/workflows/barrier.js +162 -0
  106. package/dist/workflows/barrier.js.map +1 -0
  107. package/dist/workflows/builder.d.ts +114 -0
  108. package/dist/workflows/builder.d.ts.map +1 -0
  109. package/dist/workflows/builder.js +201 -0
  110. package/dist/workflows/builder.js.map +1 -0
  111. package/dist/workflows/cli.d.ts +11 -0
  112. package/dist/workflows/cli.d.ts.map +1 -0
  113. package/dist/workflows/cli.js +144 -0
  114. package/dist/workflows/cli.js.map +1 -0
  115. package/dist/workflows/coordinator.d.ts +73 -0
  116. package/dist/workflows/coordinator.d.ts.map +1 -0
  117. package/dist/workflows/coordinator.js +647 -0
  118. package/dist/workflows/coordinator.js.map +1 -0
  119. package/dist/workflows/custom-steps.d.ts +73 -0
  120. package/dist/workflows/custom-steps.d.ts.map +1 -0
  121. package/dist/workflows/custom-steps.js +321 -0
  122. package/dist/workflows/custom-steps.js.map +1 -0
  123. package/dist/workflows/dry-run-format.d.ts +6 -0
  124. package/dist/workflows/dry-run-format.d.ts.map +1 -0
  125. package/dist/workflows/dry-run-format.js +68 -0
  126. package/dist/workflows/dry-run-format.js.map +1 -0
  127. package/dist/workflows/file-db.d.ts +33 -0
  128. package/dist/workflows/file-db.d.ts.map +1 -0
  129. package/dist/workflows/file-db.js +108 -0
  130. package/dist/workflows/file-db.js.map +1 -0
  131. package/dist/workflows/index.d.ts +15 -0
  132. package/dist/workflows/index.d.ts.map +1 -0
  133. package/dist/workflows/index.js +15 -0
  134. package/dist/workflows/index.js.map +1 -0
  135. package/dist/workflows/memory-db.d.ts +17 -0
  136. package/dist/workflows/memory-db.d.ts.map +1 -0
  137. package/dist/workflows/memory-db.js +33 -0
  138. package/dist/workflows/memory-db.js.map +1 -0
  139. package/dist/workflows/run.d.ts +38 -0
  140. package/dist/workflows/run.d.ts.map +1 -0
  141. package/dist/workflows/run.js +25 -0
  142. package/dist/workflows/run.js.map +1 -0
  143. package/dist/workflows/runner.d.ts +320 -0
  144. package/dist/workflows/runner.d.ts.map +1 -0
  145. package/dist/workflows/runner.js +2821 -0
  146. package/dist/workflows/runner.js.map +1 -0
  147. package/dist/workflows/state.d.ts +77 -0
  148. package/dist/workflows/state.d.ts.map +1 -0
  149. package/dist/workflows/state.js +140 -0
  150. package/dist/workflows/state.js.map +1 -0
  151. package/dist/workflows/templates.d.ts +47 -0
  152. package/dist/workflows/templates.d.ts.map +1 -0
  153. package/dist/workflows/templates.js +405 -0
  154. package/dist/workflows/templates.js.map +1 -0
  155. package/dist/workflows/trajectory.d.ts +87 -0
  156. package/dist/workflows/trajectory.d.ts.map +1 -0
  157. package/dist/workflows/trajectory.js +441 -0
  158. package/dist/workflows/trajectory.js.map +1 -0
  159. package/dist/workflows/types.d.ts +306 -0
  160. package/dist/workflows/types.d.ts.map +1 -0
  161. package/dist/workflows/types.js +23 -0
  162. package/dist/workflows/types.js.map +1 -0
  163. package/dist/workflows/validator.d.ts +11 -0
  164. package/dist/workflows/validator.d.ts.map +1 -0
  165. package/dist/workflows/validator.js +128 -0
  166. package/dist/workflows/validator.js.map +1 -0
  167. package/package.json +59 -53
  168. package/dist/discovery.d.ts +0 -10
  169. package/dist/discovery.d.ts.map +0 -1
  170. package/dist/discovery.js +0 -22
  171. package/dist/discovery.js.map +0 -1
  172. package/dist/errors.d.ts +0 -9
  173. package/dist/errors.d.ts.map +0 -1
  174. package/dist/errors.js +0 -9
  175. package/dist/errors.js.map +0 -1
  176. package/dist/protocol/index.d.ts +0 -8
  177. package/dist/protocol/index.d.ts.map +0 -1
  178. package/dist/protocol/index.js +0 -8
  179. package/dist/protocol/index.js.map +0 -1
package/dist/client.js CHANGED
@@ -1,1591 +1,489 @@
1
- /**
2
- * RelayClient - Agent Relay SDK Client
3
- * @agent-relay/sdk
4
- *
5
- * Lightweight client for agent-to-agent communication via Agent Relay daemon.
6
- */
7
- import net from 'node:net';
8
- import { randomUUID } from 'node:crypto';
9
- import { discoverSocket } from '@agent-relay/utils/discovery';
10
- import { DaemonNotRunningError, ConnectionError } from '@agent-relay/utils/errors';
11
- // Import shared protocol types and framing utilities from @agent-relay/protocol
12
- import { PROTOCOL_VERSION, encodeFrameLegacy, FrameParser, } from '@agent-relay/protocol';
13
- const DEFAULT_SOCKET_PATH = '/tmp/agent-relay.sock';
14
- /**
15
- * Resolve the socket path using discovery if not explicitly provided.
16
- * Falls back to /tmp/agent-relay.sock if discovery fails.
17
- */
18
- function resolveSocketPath(configPath) {
19
- if (configPath)
20
- return configPath;
21
- const discovery = discoverSocket();
22
- return discovery?.socketPath || DEFAULT_SOCKET_PATH;
23
- }
24
- const DEFAULT_CLIENT_CONFIG = {
25
- socketPath: DEFAULT_SOCKET_PATH,
26
- agentName: 'agent',
27
- cli: undefined,
28
- quiet: false,
29
- reconnect: true,
30
- maxReconnectAttempts: 10,
31
- reconnectDelayMs: 1000, // Increased from 100ms to prevent reconnect storms
32
- reconnectMaxDelayMs: 30000,
33
- };
34
- // Simple ID generator
35
- let idCounter = 0;
36
- function generateId() {
37
- return `${Date.now().toString(36)}-${(++idCounter).toString(36)}`;
38
- }
39
- /**
40
- * Circular buffer for O(1) deduplication with bounded memory.
41
- */
42
- class CircularDedupeCache {
43
- ids = new Set();
44
- ring;
45
- head = 0;
46
- capacity;
47
- constructor(capacity = 2000) {
48
- this.capacity = capacity;
49
- this.ring = new Array(capacity);
50
- }
51
- check(id) {
52
- if (this.ids.has(id))
53
- return true;
54
- if (this.ids.size >= this.capacity) {
55
- const oldest = this.ring[this.head];
56
- if (oldest)
57
- this.ids.delete(oldest);
58
- }
59
- this.ring[this.head] = id;
60
- this.ids.add(id);
61
- this.head = (this.head + 1) % this.capacity;
62
- return false;
63
- }
64
- clear() {
65
- this.ids.clear();
66
- this.ring = new Array(this.capacity);
67
- this.head = 0;
1
+ import { once } from 'node:events';
2
+ import { spawn } from 'node:child_process';
3
+ import { createInterface } from 'node:readline';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { PROTOCOL_VERSION, } from './protocol.js';
9
+ export class AgentRelayProtocolError extends Error {
10
+ code;
11
+ retryable;
12
+ data;
13
+ constructor(payload) {
14
+ super(payload.message);
15
+ this.name = 'AgentRelayProtocolError';
16
+ this.code = payload.code;
17
+ this.retryable = payload.retryable;
18
+ this.data = payload.data;
68
19
  }
69
20
  }
70
- /**
71
- * RelayClient for agent-to-agent communication.
72
- */
73
- export class RelayClient {
74
- config;
75
- socket;
76
- parser;
77
- _state = 'DISCONNECTED';
78
- sessionId;
79
- resumeToken;
80
- reconnectAttempts = 0;
81
- reconnectDelay;
82
- reconnectTimer;
83
- _destroyed = false;
84
- dedupeCache = new CircularDedupeCache(2000);
85
- writeQueue = [];
86
- writeScheduled = false;
87
- pendingSyncAcks = new Map();
88
- pendingSpawns = new Map();
89
- pendingReleases = new Map();
90
- pendingSendInputs = new Map();
91
- pendingSetModels = new Map();
92
- pendingListWorkers = new Map();
93
- pendingQueries = new Map();
94
- pendingRequests = new Map();
95
- pendingAgentReady = new Map();
96
- // Event handlers
97
- onMessage;
98
- /**
99
- * Callback for channel messages.
100
- */
101
- onChannelMessage;
102
- onStateChange;
103
- onError;
104
- /**
105
- * Callback when an agent becomes ready (completes HELLO/WELCOME handshake).
106
- * This is broadcast by the daemon when any agent connects.
107
- * Useful for knowing when a spawned agent is ready to receive messages.
108
- */
109
- onAgentReady;
110
- constructor(config = {}) {
111
- this.config = { ...DEFAULT_CLIENT_CONFIG, ...config };
112
- // Use socket discovery if no explicit socketPath was provided
113
- if (!config.socketPath) {
114
- this.config.socketPath = resolveSocketPath();
115
- }
116
- this.parser = new FrameParser();
117
- this.parser.setLegacyMode(true);
118
- this.reconnectDelay = this.config.reconnectDelayMs;
21
+ export class AgentRelayProcessError extends Error {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = 'AgentRelayProcessError';
119
25
  }
120
- get state() {
121
- return this._state;
122
- }
123
- get agentName() {
124
- return this.config.agentName;
26
+ }
27
+ export class AgentRelayClient {
28
+ options;
29
+ child;
30
+ stdoutRl;
31
+ stderrRl;
32
+ lastStderrLine;
33
+ requestSeq = 0;
34
+ pending = new Map();
35
+ startingPromise;
36
+ eventListeners = new Set();
37
+ stderrListeners = new Set();
38
+ eventBuffer = [];
39
+ maxBufferSize = 1000;
40
+ exitPromise;
41
+ constructor(options = {}) {
42
+ this.options = {
43
+ binaryPath: options.binaryPath ?? resolveDefaultBinaryPath(),
44
+ binaryArgs: options.binaryArgs ?? [],
45
+ brokerName: options.brokerName ?? (path.basename(options.cwd ?? process.cwd()) || 'project'),
46
+ channels: options.channels ?? ['general'],
47
+ cwd: options.cwd ?? process.cwd(),
48
+ env: options.env ?? process.env,
49
+ requestTimeoutMs: options.requestTimeoutMs ?? 10_000,
50
+ shutdownTimeoutMs: options.shutdownTimeoutMs ?? 3_000,
51
+ clientName: options.clientName ?? '@agent-relay/sdk',
52
+ clientVersion: options.clientVersion ?? '0.1.0',
53
+ };
125
54
  }
126
- get currentSessionId() {
127
- return this.sessionId;
55
+ static async start(options = {}) {
56
+ const client = new AgentRelayClient(options);
57
+ await client.start();
58
+ return client;
128
59
  }
129
- /**
130
- * Connect to the relay daemon.
131
- */
132
- connect() {
133
- if (this._state !== 'DISCONNECTED' && this._state !== 'BACKOFF') {
134
- return Promise.resolve();
135
- }
136
- return new Promise((resolve, reject) => {
137
- let settled = false;
138
- const settleResolve = () => {
139
- if (settled)
140
- return;
141
- settled = true;
142
- resolve();
143
- };
144
- const settleReject = (err) => {
145
- if (settled)
146
- return;
147
- settled = true;
148
- reject(err);
149
- };
150
- this.setState('CONNECTING');
151
- this.socket = net.createConnection(this.config.socketPath, () => {
152
- this.setState('HANDSHAKING');
153
- this.sendHello();
154
- });
155
- this.socket.on('data', (data) => this.handleData(data));
156
- this.socket.on('close', () => {
157
- this.handleDisconnect();
158
- });
159
- this.socket.on('error', (err) => {
160
- if (this._state === 'CONNECTING') {
161
- const errno = err.code;
162
- if (errno === 'ECONNREFUSED' || errno === 'ENOENT') {
163
- settleReject(new DaemonNotRunningError(`Cannot connect to daemon at ${this.config.socketPath}`));
164
- }
165
- else {
166
- settleReject(new ConnectionError(err.message));
167
- }
168
- }
169
- this.handleError(err);
170
- });
171
- const checkReady = setInterval(() => {
172
- if (this._state === 'READY') {
173
- clearInterval(checkReady);
174
- clearTimeout(timeout);
175
- settleResolve();
176
- }
177
- }, 10);
178
- const timeout = setTimeout(() => {
179
- if (this._state !== 'READY') {
180
- clearInterval(checkReady);
181
- this.socket?.destroy();
182
- settleReject(new Error('Connection timeout'));
183
- }
184
- }, 5000);
185
- });
60
+ onEvent(listener) {
61
+ this.eventListeners.add(listener);
62
+ return () => {
63
+ this.eventListeners.delete(listener);
64
+ };
186
65
  }
187
- /**
188
- * Disconnect from the relay daemon.
189
- */
190
- disconnect() {
191
- if (this.reconnectTimer) {
192
- clearTimeout(this.reconnectTimer);
193
- this.reconnectTimer = undefined;
66
+ queryEvents(filter) {
67
+ let events = [...this.eventBuffer];
68
+ if (filter?.kind) {
69
+ events = events.filter((event) => event.kind === filter.kind);
194
70
  }
195
- if (this.socket) {
196
- this.send({
197
- v: PROTOCOL_VERSION,
198
- type: 'BYE',
199
- id: generateId(),
200
- ts: Date.now(),
201
- payload: {},
202
- });
203
- this.socket.end();
204
- this.socket = undefined;
205
- }
206
- this.setState('DISCONNECTED');
207
- }
208
- /**
209
- * Permanently destroy the client.
210
- */
211
- destroy() {
212
- this._destroyed = true;
213
- this.disconnect();
214
- }
215
- /**
216
- * Send a message to another agent.
217
- */
218
- sendMessage(to, body, kind = 'message', data, thread, meta) {
219
- if (this._state !== 'READY') {
220
- return false;
71
+ if (filter?.name) {
72
+ events = events.filter((event) => 'name' in event && event.name === filter.name);
221
73
  }
222
- const envelope = {
223
- v: PROTOCOL_VERSION,
224
- type: 'SEND',
225
- id: generateId(),
226
- ts: Date.now(),
227
- to,
228
- payload: {
229
- kind,
230
- body,
231
- data,
232
- thread,
233
- },
234
- payload_meta: meta,
235
- };
236
- return this.send(envelope);
237
- }
238
- /**
239
- * Send an ACK for a delivered message.
240
- */
241
- sendAck(payload) {
242
- if (this._state !== 'READY') {
243
- return false;
74
+ const since = filter?.since;
75
+ if (since !== undefined) {
76
+ events = events.filter((event) => 'timestamp' in event && typeof event.timestamp === 'number' && event.timestamp >= since);
244
77
  }
245
- const envelope = {
246
- v: PROTOCOL_VERSION,
247
- type: 'ACK',
248
- id: generateId(),
249
- ts: Date.now(),
250
- payload,
251
- };
252
- return this.send(envelope);
253
- }
254
- /**
255
- * Send a message and wait for ACK response.
256
- */
257
- async sendAndWait(to, body, options = {}) {
258
- if (this._state !== 'READY') {
259
- throw new Error('Client not ready');
78
+ const limit = filter?.limit;
79
+ if (limit !== undefined) {
80
+ events = events.slice(-limit);
260
81
  }
261
- const correlationId = randomUUID();
262
- const timeoutMs = options.timeoutMs ?? 30000;
263
- const kind = options.kind ?? 'message';
264
- return new Promise((resolve, reject) => {
265
- const timeoutHandle = setTimeout(() => {
266
- this.pendingSyncAcks.delete(correlationId);
267
- reject(new Error(`ACK timeout after ${timeoutMs}ms`));
268
- }, timeoutMs);
269
- this.pendingSyncAcks.set(correlationId, { resolve, reject, timeoutHandle });
270
- const envelope = {
271
- v: PROTOCOL_VERSION,
272
- type: 'SEND',
273
- id: generateId(),
274
- ts: Date.now(),
275
- to,
276
- payload: {
277
- kind,
278
- body,
279
- data: options.data,
280
- thread: options.thread,
281
- },
282
- payload_meta: {
283
- sync: {
284
- correlationId,
285
- timeoutMs,
286
- blocking: true,
287
- },
288
- },
289
- };
290
- const sent = this.send(envelope);
291
- if (!sent) {
292
- clearTimeout(timeoutHandle);
293
- this.pendingSyncAcks.delete(correlationId);
294
- reject(new Error('Failed to send message'));
295
- }
296
- });
82
+ return events;
297
83
  }
298
- /**
299
- * Send a request to another agent and wait for their response.
300
- *
301
- * This implements a request/response pattern where the message is sent with
302
- * a correlation ID, and the method waits for the target agent to respond
303
- * with a message containing that correlation ID.
304
- *
305
- * @example
306
- * ```typescript
307
- * // Simple request
308
- * const response = await client.request('Worker', 'Process this task');
309
- * console.log(response.body); // Worker's response
310
- *
311
- * // With options
312
- * const response = await client.request('Worker', 'Process task', {
313
- * timeout: 60000,
314
- * data: { taskId: '123', priority: 'high' },
315
- * thread: 'task-thread-1',
316
- * });
317
- * ```
318
- *
319
- * @param to - Target agent name
320
- * @param body - Request message body
321
- * @param options - Request options (timeout, data, thread, kind)
322
- * @returns Promise that resolves with the response from the target agent
323
- * @throws Error if client is not ready, send fails, timeout occurs, or agent disconnects
324
- */
325
- async request(to, body, options = {}) {
326
- if (this._state !== 'READY') {
327
- throw new Error('Client not ready');
328
- }
329
- const correlationId = randomUUID();
330
- const timeoutMs = options.timeout ?? 30000;
331
- const kind = options.kind ?? 'message';
332
- return new Promise((resolve, reject) => {
333
- const timeoutHandle = setTimeout(() => {
334
- this.pendingRequests.delete(correlationId);
335
- reject(new Error(`Request timeout after ${timeoutMs}ms waiting for response from ${to}`));
336
- }, timeoutMs);
337
- this.pendingRequests.set(correlationId, {
338
- resolve,
339
- reject,
340
- timeoutHandle,
341
- targetAgent: to,
342
- });
343
- const envelope = {
344
- v: PROTOCOL_VERSION,
345
- type: 'SEND',
346
- id: generateId(),
347
- ts: Date.now(),
348
- to,
349
- payload: {
350
- kind,
351
- body,
352
- data: {
353
- ...options.data,
354
- _correlationId: correlationId,
355
- },
356
- thread: options.thread,
357
- },
358
- payload_meta: {
359
- replyTo: correlationId,
360
- },
361
- };
362
- const sent = this.send(envelope);
363
- if (!sent) {
364
- clearTimeout(timeoutHandle);
365
- this.pendingRequests.delete(correlationId);
366
- reject(new Error('Failed to send request'));
84
+ getLastEvent(kind, name) {
85
+ for (let i = this.eventBuffer.length - 1; i >= 0; i -= 1) {
86
+ const event = this.eventBuffer[i];
87
+ if (event.kind === kind && (!name || ('name' in event && event.name === name))) {
88
+ return event;
367
89
  }
368
- });
369
- }
370
- /**
371
- * Respond to a request from another agent.
372
- *
373
- * This is a convenience method for responding to messages that have a
374
- * correlation ID. The response will be routed back to the requesting agent.
375
- *
376
- * @param correlationId - The correlation ID from the original request (from data._correlationId or meta.replyTo)
377
- * @param to - Target agent (the one who sent the original request)
378
- * @param body - Response body
379
- * @param data - Optional structured data to include in the response
380
- * @returns true if the message was sent
381
- */
382
- respond(correlationId, to, body, data) {
383
- if (this._state !== 'READY') {
384
- return false;
385
90
  }
386
- const envelope = {
387
- v: PROTOCOL_VERSION,
388
- type: 'SEND',
389
- id: generateId(),
390
- ts: Date.now(),
391
- to,
392
- payload: {
393
- kind: 'message',
394
- body,
395
- data: {
396
- ...data,
397
- _correlationId: correlationId,
398
- _isResponse: true,
399
- },
400
- },
401
- payload_meta: {
402
- replyTo: correlationId,
403
- },
404
- };
405
- return this.send(envelope);
406
- }
407
- /**
408
- * Broadcast a message to all agents.
409
- */
410
- broadcast(body, kind = 'message', data) {
411
- return this.sendMessage('*', body, kind, data);
412
- }
413
- /**
414
- * Subscribe to a topic.
415
- */
416
- subscribe(topic) {
417
- if (this._state !== 'READY')
418
- return false;
419
- return this.send({
420
- v: PROTOCOL_VERSION,
421
- type: 'SUBSCRIBE',
422
- id: generateId(),
423
- ts: Date.now(),
424
- topic,
425
- payload: {},
426
- });
427
- }
428
- /**
429
- * Unsubscribe from a topic.
430
- */
431
- unsubscribe(topic) {
432
- if (this._state !== 'READY')
433
- return false;
434
- return this.send({
435
- v: PROTOCOL_VERSION,
436
- type: 'UNSUBSCRIBE',
437
- id: generateId(),
438
- ts: Date.now(),
439
- topic,
440
- payload: {},
441
- });
442
- }
443
- /**
444
- * Bind as a shadow to a primary agent.
445
- */
446
- bindAsShadow(primaryAgent, options = {}) {
447
- if (this._state !== 'READY')
448
- return false;
449
- return this.send({
450
- v: PROTOCOL_VERSION,
451
- type: 'SHADOW_BIND',
452
- id: generateId(),
453
- ts: Date.now(),
454
- payload: {
455
- primaryAgent,
456
- speakOn: options.speakOn,
457
- receiveIncoming: options.receiveIncoming,
458
- receiveOutgoing: options.receiveOutgoing,
459
- },
460
- });
461
- }
462
- /**
463
- * Unbind from a primary agent.
464
- */
465
- unbindAsShadow(primaryAgent) {
466
- if (this._state !== 'READY')
467
- return false;
468
- return this.send({
469
- v: PROTOCOL_VERSION,
470
- type: 'SHADOW_UNBIND',
471
- id: generateId(),
472
- ts: Date.now(),
473
- payload: {
474
- primaryAgent,
475
- },
476
- });
91
+ return undefined;
477
92
  }
478
- /**
479
- * Send log output to the daemon for dashboard streaming.
480
- */
481
- sendLog(data) {
482
- if (this._state !== 'READY') {
483
- return false;
484
- }
485
- const envelope = {
486
- v: PROTOCOL_VERSION,
487
- type: 'LOG',
488
- id: generateId(),
489
- ts: Date.now(),
490
- payload: {
491
- data,
492
- timestamp: Date.now(),
493
- },
93
+ onBrokerStderr(listener) {
94
+ this.stderrListeners.add(listener);
95
+ return () => {
96
+ this.stderrListeners.delete(listener);
494
97
  };
495
- return this.send(envelope);
496
98
  }
497
- // =============================================================================
498
- // Spawn/Release Operations
499
- // =============================================================================
500
- /**
501
- * Spawn a new agent via the relay daemon.
502
- * @param options - Spawn options
503
- * @param options.name - Name for the new agent
504
- * @param options.cli - CLI to use (claude, codex, gemini, etc.)
505
- * @param options.task - Task description
506
- * @param options.cwd - Working directory
507
- * @param options.team - Team name
508
- * @param options.interactive - Interactive mode
509
- * @param options.shadowMode - Shadow execution mode ('subagent' or 'process')
510
- * @param options.shadowOf - Spawn as shadow of this agent
511
- * @param options.shadowAgent - Shadow agent profile to use (for subagent mode)
512
- * @param options.shadowTriggers - When to trigger the shadow (for subagent mode)
513
- * @param options.shadowSpeakOn - Shadow speak-on triggers
514
- * @param options.waitForReady - Wait for the agent to be ready before resolving (default: false)
515
- * @param options.readyTimeoutMs - Timeout for agent to become ready (default: 60000ms)
516
- * @param timeoutMs - Timeout for spawn operation (default: 60000ms)
517
- * @returns Spawn result. When waitForReady is true, includes `ready` and `readyInfo` fields.
518
- */
519
- async spawn(options, timeoutMs = 60000) {
520
- if (this._state !== 'READY') {
521
- throw new Error('Client not ready');
522
- }
523
- const envelopeId = generateId();
524
- const waitForReady = options.waitForReady ?? false;
525
- const readyTimeoutMs = options.readyTimeoutMs ?? 60000;
526
- // If waitForReady, set up the agent ready listener BEFORE spawning
527
- // This ensures we don't miss the AGENT_READY event if it arrives quickly
528
- let readyPromise;
529
- if (waitForReady) {
530
- // Check if we're already waiting for this agent (prevents overwriting existing waiter)
531
- if (this.pendingAgentReady.has(options.name)) {
532
- throw new Error(`Already waiting for agent ${options.name} to be ready`);
533
- }
534
- readyPromise = new Promise((resolve, reject) => {
535
- const timeoutHandle = setTimeout(() => {
536
- this.pendingAgentReady.delete(options.name);
537
- reject(new Error(`Agent ${options.name} did not become ready within ${readyTimeoutMs}ms`));
538
- }, readyTimeoutMs);
539
- this.pendingAgentReady.set(options.name, { resolve, reject, timeoutHandle });
540
- });
99
+ async start() {
100
+ if (this.child) {
101
+ return;
541
102
  }
542
- // Send the spawn request
543
- const spawnResult = await new Promise((resolve, reject) => {
544
- const timeoutHandle = setTimeout(() => {
545
- this.pendingSpawns.delete(envelopeId);
546
- // Also clean up pending agent ready if spawn times out
547
- if (waitForReady) {
548
- const pending = this.pendingAgentReady.get(options.name);
549
- if (pending) {
550
- clearTimeout(pending.timeoutHandle);
551
- this.pendingAgentReady.delete(options.name);
552
- }
553
- }
554
- reject(new Error(`Spawn timeout after ${timeoutMs}ms`));
555
- }, timeoutMs);
556
- this.pendingSpawns.set(envelopeId, { resolve, reject, timeoutHandle });
557
- const envelope = {
558
- v: PROTOCOL_VERSION,
559
- type: 'SPAWN',
560
- id: envelopeId,
561
- ts: Date.now(),
562
- payload: {
563
- name: options.name,
564
- cli: options.cli,
565
- task: options.task || '',
566
- cwd: options.cwd,
567
- team: options.team,
568
- model: options.model,
569
- interactive: options.interactive,
570
- shadowMode: options.shadowMode,
571
- shadowOf: options.shadowOf,
572
- shadowAgent: options.shadowAgent,
573
- shadowTriggers: options.shadowTriggers,
574
- shadowSpeakOn: options.shadowSpeakOn,
575
- userId: options.userId,
576
- includeWorkflowConventions: options.includeWorkflowConventions,
577
- spawnerName: options.spawnerName || this.config.agentName,
578
- },
579
- };
580
- const sent = this.send(envelope);
581
- if (!sent) {
582
- clearTimeout(timeoutHandle);
583
- this.pendingSpawns.delete(envelopeId);
584
- // Also clean up pending agent ready if send fails
585
- if (waitForReady) {
586
- const pending = this.pendingAgentReady.get(options.name);
587
- if (pending) {
588
- clearTimeout(pending.timeoutHandle);
589
- this.pendingAgentReady.delete(options.name);
590
- }
591
- }
592
- reject(new Error('Failed to send spawn message'));
593
- }
594
- });
595
- // If spawn failed or we don't need to wait for ready, return immediately
596
- if (!spawnResult.success || !waitForReady || !readyPromise) {
597
- // Clean up pending agent ready if spawn failed
598
- if (!spawnResult.success && waitForReady) {
599
- const pending = this.pendingAgentReady.get(options.name);
600
- if (pending) {
601
- clearTimeout(pending.timeoutHandle);
602
- this.pendingAgentReady.delete(options.name);
603
- }
604
- }
605
- return spawnResult;
103
+ if (this.startingPromise) {
104
+ return this.startingPromise;
606
105
  }
607
- // Wait for the agent to become ready
106
+ this.startingPromise = this.startInternal();
608
107
  try {
609
- const readyInfo = await readyPromise;
610
- return {
611
- ...spawnResult,
612
- ready: true,
613
- readyInfo,
614
- };
108
+ await this.startingPromise;
615
109
  }
616
- catch (err) {
617
- // Agent spawned but didn't become ready in time
618
- // Return the spawn result with ready: false
619
- return {
620
- ...spawnResult,
621
- ready: false,
622
- };
110
+ finally {
111
+ this.startingPromise = undefined;
623
112
  }
624
113
  }
625
114
  /**
626
- * Wait for an agent to become ready (complete HELLO/WELCOME handshake).
627
- * This is useful when you want to wait for an agent that was spawned through
628
- * another mechanism, or to verify an agent is connected before sending messages.
629
- *
630
- * @example
631
- * ```typescript
632
- * // Wait for an agent that might be spawning
633
- * try {
634
- * const readyInfo = await client.waitForAgentReady('Worker', 30000);
635
- * console.log(`Worker is ready: ${readyInfo.cli}`);
636
- * } catch (err) {
637
- * console.error('Worker did not become ready in time');
638
- * }
639
- * ```
640
- *
641
- * @param name - Agent name to wait for
642
- * @param timeoutMs - Timeout in milliseconds (default: 60000ms)
643
- * @returns Promise that resolves with AgentReadyPayload when the agent connects
644
- * @throws Error if the agent doesn't become ready within the timeout
115
+ * Pre-register a batch of agents with Relaycast before their steps execute.
116
+ * The broker warms its token cache in parallel; subsequent spawn_agent calls
117
+ * hit the cache rather than waiting on individual HTTP registrations.
118
+ * Fire-and-forget from the caller's perspective — broker responds immediately
119
+ * and registers in the background.
645
120
  */
646
- async waitForAgentReady(name, timeoutMs = 60000) {
647
- if (this._state !== 'READY') {
648
- throw new Error('Client not ready');
649
- }
650
- // Check if we're already waiting for this agent
651
- if (this.pendingAgentReady.has(name)) {
652
- throw new Error(`Already waiting for agent ${name} to be ready`);
653
- }
654
- return new Promise((resolve, reject) => {
655
- const timeoutHandle = setTimeout(() => {
656
- this.pendingAgentReady.delete(name);
657
- reject(new Error(`Agent ${name} did not become ready within ${timeoutMs}ms`));
658
- }, timeoutMs);
659
- this.pendingAgentReady.set(name, { resolve, reject, timeoutHandle });
121
+ async preflightAgents(agents) {
122
+ if (agents.length === 0)
123
+ return;
124
+ await this.start();
125
+ await this.requestOk('preflight_agents', { agents });
126
+ }
127
+ async spawnPty(input) {
128
+ await this.start();
129
+ const args = buildPtyArgsWithModel(input.cli, input.args ?? [], input.model);
130
+ const agent = {
131
+ name: input.name,
132
+ runtime: 'pty',
133
+ cli: input.cli,
134
+ args,
135
+ channels: input.channels ?? [],
136
+ model: input.model,
137
+ cwd: input.cwd ?? this.options.cwd,
138
+ team: input.team,
139
+ shadow_of: input.shadowOf,
140
+ shadow_mode: input.shadowMode,
141
+ restart_policy: input.restartPolicy,
142
+ };
143
+ const result = await this.requestOk('spawn_agent', {
144
+ agent,
145
+ ...(input.task != null ? { initial_task: input.task } : {}),
146
+ ...(input.idleThresholdSecs != null ? { idle_threshold_secs: input.idleThresholdSecs } : {}),
147
+ ...(input.continueFrom != null ? { continue_from: input.continueFrom } : {}),
660
148
  });
661
- }
662
- /**
663
- * Release (terminate) an agent via the relay daemon.
664
- * @param name - Agent name to release
665
- * @param timeoutMs - Timeout for release operation (default: 10000ms)
666
- */
667
- async release(name, reason, timeoutMs = 10000) {
668
- if (this._state !== 'READY') {
669
- throw new Error('Client not ready');
670
- }
671
- const envelopeId = generateId();
672
- return new Promise((resolve, reject) => {
673
- const timeoutHandle = setTimeout(() => {
674
- this.pendingReleases.delete(envelopeId);
675
- reject(new Error(`Release timeout after ${timeoutMs}ms`));
676
- }, timeoutMs);
677
- this.pendingReleases.set(envelopeId, { resolve, reject, timeoutHandle });
678
- const envelope = {
679
- v: PROTOCOL_VERSION,
680
- type: 'RELEASE',
681
- id: envelopeId,
682
- ts: Date.now(),
683
- payload: {
684
- name,
685
- reason,
686
- },
687
- };
688
- const sent = this.send(envelope);
689
- if (!sent) {
690
- clearTimeout(timeoutHandle);
691
- this.pendingReleases.delete(envelopeId);
692
- reject(new Error('Failed to send release message'));
693
- }
149
+ return result;
150
+ }
151
+ async spawnHeadlessClaude(input) {
152
+ await this.start();
153
+ const agent = {
154
+ name: input.name,
155
+ runtime: 'headless_claude',
156
+ args: input.args ?? [],
157
+ channels: input.channels ?? [],
158
+ };
159
+ const result = await this.requestOk('spawn_agent', {
160
+ agent,
161
+ ...(input.task != null ? { initial_task: input.task } : {}),
694
162
  });
163
+ return result;
695
164
  }
696
- /**
697
- * Send input data to a spawned agent's PTY.
698
- * @param name - Agent name to send input to
699
- * @param data - Input data to send
700
- * @param timeoutMs - Timeout for the operation (default: 10000ms)
701
- */
702
- async sendWorkerInput(name, data, timeoutMs = 10000) {
703
- if (this._state !== 'READY') {
704
- throw new Error('Client not ready');
705
- }
706
- const envelopeId = generateId();
707
- return new Promise((resolve, reject) => {
708
- const timeoutHandle = setTimeout(() => {
709
- this.pendingSendInputs.delete(envelopeId);
710
- reject(new Error(`Send input timeout after ${timeoutMs}ms`));
711
- }, timeoutMs);
712
- this.pendingSendInputs.set(envelopeId, { resolve, reject, timeoutHandle });
713
- const envelope = {
714
- v: PROTOCOL_VERSION,
715
- type: 'SEND_INPUT',
716
- id: envelopeId,
717
- ts: Date.now(),
718
- payload: {
719
- name,
720
- data,
721
- },
722
- };
723
- const sent = this.send(envelope);
724
- if (!sent) {
725
- clearTimeout(timeoutHandle);
726
- this.pendingSendInputs.delete(envelopeId);
727
- reject(new Error('Failed to send input message'));
728
- }
729
- });
165
+ async release(name, reason) {
166
+ await this.start();
167
+ return this.requestOk('release_agent', { name, reason });
730
168
  }
731
- /**
732
- * Change the model of a running spawned agent.
733
- * The command waits for the agent to be idle before sending the model switch command.
734
- *
735
- * @param name - Agent name to switch model for
736
- * @param model - Target model (e.g., 'opus', 'sonnet', 'haiku')
737
- * @param options - Options including idle wait timeout
738
- * @param operationTimeoutMs - Timeout for the overall protocol operation (default: 45000ms)
739
- */
740
- async setWorkerModel(name, model, options, operationTimeoutMs = 45000) {
741
- if (this._state !== 'READY') {
742
- throw new Error('Client not ready');
743
- }
744
- const envelopeId = generateId();
745
- return new Promise((resolve, reject) => {
746
- const timeoutHandle = setTimeout(() => {
747
- this.pendingSetModels.delete(envelopeId);
748
- reject(new Error(`Set model timeout after ${operationTimeoutMs}ms`));
749
- }, operationTimeoutMs);
750
- this.pendingSetModels.set(envelopeId, { resolve, reject, timeoutHandle });
751
- const envelope = {
752
- v: PROTOCOL_VERSION,
753
- type: 'SET_MODEL',
754
- id: envelopeId,
755
- ts: Date.now(),
756
- payload: {
757
- name,
758
- model,
759
- timeoutMs: options?.timeoutMs,
760
- },
761
- };
762
- const sent = this.send(envelope);
763
- if (!sent) {
764
- clearTimeout(timeoutHandle);
765
- this.pendingSetModels.delete(envelopeId);
766
- reject(new Error('Failed to send set model message'));
767
- }
768
- });
169
+ async sendInput(name, data) {
170
+ await this.start();
171
+ return this.requestOk('send_input', { name, data });
769
172
  }
770
- /**
771
- * List active spawned workers.
772
- * @param timeoutMs - Timeout for the operation (default: 10000ms)
773
- */
774
- async listWorkers(timeoutMs = 10000) {
775
- if (this._state !== 'READY') {
776
- throw new Error('Client not ready');
777
- }
778
- const envelopeId = generateId();
779
- return new Promise((resolve, reject) => {
780
- const timeoutHandle = setTimeout(() => {
781
- this.pendingListWorkers.delete(envelopeId);
782
- reject(new Error(`List workers timeout after ${timeoutMs}ms`));
783
- }, timeoutMs);
784
- this.pendingListWorkers.set(envelopeId, { resolve, reject, timeoutHandle });
785
- const envelope = {
786
- v: PROTOCOL_VERSION,
787
- type: 'LIST_WORKERS',
788
- id: envelopeId,
789
- ts: Date.now(),
790
- payload: {},
791
- };
792
- const sent = this.send(envelope);
793
- if (!sent) {
794
- clearTimeout(timeoutHandle);
795
- this.pendingListWorkers.delete(envelopeId);
796
- reject(new Error('Failed to send list workers message'));
797
- }
173
+ async setModel(name, model, opts) {
174
+ await this.start();
175
+ return this.requestOk('set_model', {
176
+ name,
177
+ model,
178
+ timeout_ms: opts?.timeoutMs,
798
179
  });
799
180
  }
800
- // =============================================================================
801
- // Channel Operations
802
- // =============================================================================
803
- /**
804
- * Join a channel.
805
- * @param channel - Channel name (e.g., '#general', 'dm:alice:bob')
806
- * @param displayName - Optional display name for this member
807
- */
808
- joinChannel(channel, displayName) {
809
- if (this._state !== 'READY') {
810
- return false;
811
- }
812
- const envelope = {
813
- v: PROTOCOL_VERSION,
814
- type: 'CHANNEL_JOIN',
815
- id: generateId(),
816
- ts: Date.now(),
817
- payload: {
818
- channel,
819
- displayName,
820
- },
821
- };
822
- return this.send(envelope);
823
- }
824
- /**
825
- * Admin join: Add any member to a channel (does not require member to be connected).
826
- * @param channel - Channel name
827
- * @param member - Name of the member to add
828
- */
829
- adminJoinChannel(channel, member) {
830
- if (this._state !== 'READY') {
831
- return false;
832
- }
833
- const envelope = {
834
- v: PROTOCOL_VERSION,
835
- type: 'CHANNEL_JOIN',
836
- id: generateId(),
837
- ts: Date.now(),
838
- payload: {
839
- channel,
840
- member,
841
- },
842
- };
843
- return this.send(envelope);
844
- }
845
- /**
846
- * Leave a channel.
847
- * @param channel - Channel name to leave
848
- * @param reason - Optional reason for leaving
849
- */
850
- leaveChannel(channel, reason) {
851
- if (this._state !== 'READY')
852
- return false;
853
- const envelope = {
854
- v: PROTOCOL_VERSION,
855
- type: 'CHANNEL_LEAVE',
856
- id: generateId(),
857
- ts: Date.now(),
858
- payload: {
859
- channel,
860
- reason,
861
- },
862
- };
863
- return this.send(envelope);
864
- }
865
- /**
866
- * Admin remove: Remove any member from a channel.
867
- * @param channel - Channel name
868
- * @param member - Name of the member to remove
869
- */
870
- adminRemoveMember(channel, member) {
871
- if (this._state !== 'READY') {
872
- return false;
873
- }
874
- const envelope = {
875
- v: PROTOCOL_VERSION,
876
- type: 'CHANNEL_LEAVE',
877
- id: generateId(),
878
- ts: Date.now(),
879
- payload: {
880
- channel,
881
- member,
882
- },
883
- };
884
- return this.send(envelope);
885
- }
886
- /**
887
- * Send a message to a channel.
888
- * @param channel - Channel name
889
- * @param body - Message content
890
- * @param options - Optional thread, mentions, attachments
891
- */
892
- sendChannelMessage(channel, body, options) {
893
- if (this._state !== 'READY') {
894
- return false;
895
- }
896
- const envelope = {
897
- v: PROTOCOL_VERSION,
898
- type: 'CHANNEL_MESSAGE',
899
- id: generateId(),
900
- ts: Date.now(),
901
- payload: {
902
- channel,
903
- body,
904
- thread: options?.thread,
905
- mentions: options?.mentions,
906
- attachments: options?.attachments,
907
- data: options?.data,
908
- },
909
- };
910
- return this.send(envelope);
911
- }
912
- // =============================================================================
913
- // Consensus Operations
914
- // =============================================================================
915
- /**
916
- * Create a consensus proposal.
917
- *
918
- * The proposal will be broadcast to all participants. They can vote using
919
- * the `vote()` method. Results are delivered via `onMessage` callback.
920
- *
921
- * @example
922
- * ```typescript
923
- * client.createProposal({
924
- * title: 'Approve API design',
925
- * description: 'Should we proceed with the REST API design?',
926
- * participants: ['Developer', 'Reviewer', 'Lead'],
927
- * consensusType: 'majority',
928
- * });
929
- * ```
930
- *
931
- * @param options - Proposal options
932
- * @returns true if the message was sent
933
- */
934
- createProposal(options) {
935
- if (this._state !== 'READY') {
936
- return false;
937
- }
938
- // Build the PROPOSE command message
939
- const lines = [
940
- `PROPOSE: ${options.title}`,
941
- `TYPE: ${options.consensusType ?? 'majority'}`,
942
- `PARTICIPANTS: ${options.participants.join(', ')}`,
943
- `DESCRIPTION: ${options.description}`,
944
- ];
945
- if (options.timeoutMs !== undefined) {
946
- lines.push(`TIMEOUT: ${options.timeoutMs}`);
947
- }
948
- if (options.quorum !== undefined) {
949
- lines.push(`QUORUM: ${options.quorum}`);
950
- }
951
- if (options.threshold !== undefined) {
952
- lines.push(`THRESHOLD: ${options.threshold}`);
953
- }
954
- const body = lines.join('\n');
955
- // Send to the special _consensus recipient
956
- return this.sendMessage('_consensus', body, 'action');
181
+ async getMetrics(agent) {
182
+ await this.start();
183
+ return this.requestOk('get_metrics', { agent });
957
184
  }
958
- /**
959
- * Vote on a consensus proposal.
960
- *
961
- * @example
962
- * ```typescript
963
- * // Approve with a reason
964
- * client.vote({
965
- * proposalId: 'prop_123',
966
- * value: 'approve',
967
- * reason: 'Looks good to me',
968
- * });
969
- *
970
- * // Reject without reason
971
- * client.vote({ proposalId: 'prop_123', value: 'reject' });
972
- * ```
973
- *
974
- * @param options - Vote options
975
- * @returns true if the message was sent
976
- */
977
- vote(options) {
978
- if (this._state !== 'READY') {
979
- return false;
980
- }
981
- // Build the VOTE command
982
- let body = `VOTE ${options.proposalId} ${options.value}`;
983
- if (options.reason) {
984
- body += ` ${options.reason}`;
985
- }
986
- // Send to the special _consensus recipient
987
- return this.sendMessage('_consensus', body, 'action');
185
+ async getCrashInsights() {
186
+ await this.start();
187
+ return this.requestOk('get_crash_insights', {});
988
188
  }
989
- // =============================================================================
990
- // Query Operations
991
- // =============================================================================
992
- /**
993
- * Send a query to the daemon and wait for a response.
994
- * @internal
995
- */
996
- async query(type, payload, timeoutMs = 5000) {
997
- if (this._state !== 'READY') {
998
- throw new Error('Client not ready');
999
- }
1000
- const envelopeId = generateId();
1001
- return new Promise((resolve, reject) => {
1002
- const timeoutHandle = setTimeout(() => {
1003
- this.pendingQueries.delete(envelopeId);
1004
- reject(new Error(`Query timeout after ${timeoutMs}ms`));
1005
- }, timeoutMs);
1006
- this.pendingQueries.set(envelopeId, {
1007
- resolve: resolve,
1008
- reject,
1009
- timeoutHandle,
189
+ async sendMessage(input) {
190
+ await this.start();
191
+ try {
192
+ return await this.requestOk('send_message', {
193
+ to: input.to,
194
+ text: input.text,
195
+ from: input.from,
196
+ thread_id: input.threadId,
197
+ priority: input.priority,
198
+ data: input.data,
1010
199
  });
1011
- const envelope = {
1012
- v: PROTOCOL_VERSION,
1013
- type: type,
1014
- id: envelopeId,
1015
- ts: Date.now(),
1016
- payload,
1017
- };
1018
- const sent = this.send(envelope);
1019
- if (!sent) {
1020
- clearTimeout(timeoutHandle);
1021
- this.pendingQueries.delete(envelopeId);
1022
- reject(new Error(`Failed to send ${type} query`));
200
+ }
201
+ catch (error) {
202
+ if (error instanceof AgentRelayProtocolError && error.code === 'unsupported_operation') {
203
+ return { event_id: 'unsupported_operation', targets: [] };
1023
204
  }
1024
- });
1025
- }
1026
- /**
1027
- * Get daemon status information.
1028
- * @returns Daemon status including version, uptime, and counts
1029
- */
1030
- async getStatus() {
1031
- return this.query('STATUS', {});
1032
- }
1033
- /**
1034
- * Get messages from the inbox.
1035
- * @param options - Filter options
1036
- * @param options.limit - Maximum number of messages to return
1037
- * @param options.unreadOnly - Only return unread messages
1038
- * @param options.from - Filter by sender
1039
- * @param options.channel - Filter by channel
1040
- * @returns Array of inbox messages
1041
- */
1042
- async getInbox(options = {}) {
1043
- const payload = {
1044
- agent: this.config.agentName,
1045
- limit: options.limit,
1046
- unreadOnly: options.unreadOnly,
1047
- from: options.from,
1048
- channel: options.channel,
1049
- };
1050
- const response = await this.query('INBOX', payload);
1051
- return response.messages || [];
1052
- }
1053
- /**
1054
- * Query all messages (not filtered by recipient).
1055
- * Used by dashboard to get message history.
1056
- * @param options - Query options
1057
- * @param options.limit - Maximum number of messages to return (default: 100)
1058
- * @param options.sinceTs - Only return messages after this timestamp
1059
- * @param options.from - Filter by sender
1060
- * @param options.to - Filter by recipient
1061
- * @param options.thread - Filter by thread ID
1062
- * @param options.order - Sort order ('asc' or 'desc', default: 'desc')
1063
- * @returns Array of messages
1064
- */
1065
- async queryMessages(options = {}) {
1066
- const payload = {
1067
- limit: options.limit,
1068
- sinceTs: options.sinceTs,
1069
- from: options.from,
1070
- to: options.to,
1071
- thread: options.thread,
1072
- order: options.order,
1073
- };
1074
- const response = await this.query('MESSAGES_QUERY', payload);
1075
- return response.messages || [];
1076
- }
1077
- /**
1078
- * List online agents.
1079
- * @param options - Filter options
1080
- * @param options.includeIdle - Include idle agents (default: true)
1081
- * @param options.project - Filter by project
1082
- * @returns Array of agent info
1083
- */
1084
- async listAgents(options = {}) {
1085
- const payload = {
1086
- includeIdle: options.includeIdle ?? true,
1087
- project: options.project,
1088
- };
1089
- const response = await this.query('LIST_AGENTS', payload);
1090
- return response.agents || [];
1091
- }
1092
- /**
1093
- * Get system health information.
1094
- * @param options - Include options
1095
- * @param options.includeCrashes - Include crash history (default: true)
1096
- * @param options.includeAlerts - Include alerts (default: true)
1097
- * @returns Health information including score, issues, and recommendations
1098
- */
1099
- async getHealth(options = {}) {
1100
- const payload = {
1101
- includeCrashes: options.includeCrashes ?? true,
1102
- includeAlerts: options.includeAlerts ?? true,
1103
- };
1104
- return this.query('HEALTH', payload);
1105
- }
1106
- /**
1107
- * Get resource metrics for agents.
1108
- * @param options - Filter options
1109
- * @param options.agent - Filter to a specific agent
1110
- * @returns Metrics including memory, CPU, and system info
1111
- */
1112
- async getMetrics(options = {}) {
1113
- const payload = {
1114
- agent: options.agent,
1115
- };
1116
- return this.query('METRICS', payload);
1117
- }
1118
- /**
1119
- * List only currently connected agents (not historical/registered agents).
1120
- * Use this instead of listAgents() when you need accurate liveness information.
1121
- * @param options - Filter options
1122
- * @param options.project - Filter by project
1123
- * @returns Array of currently connected agent info
1124
- */
1125
- async listConnectedAgents(options = {}) {
1126
- const payload = {
1127
- project: options.project,
1128
- };
1129
- const response = await this.query('LIST_CONNECTED_AGENTS', payload);
1130
- return response.agents || [];
1131
- }
1132
- /**
1133
- * Remove an agent from the registry (sessions, agents.json).
1134
- * Use this to clean up stale agents that are no longer needed.
1135
- * @param name - Agent name to remove
1136
- * @param options - Removal options
1137
- * @param options.removeMessages - Also remove all messages from/to this agent (default: false)
1138
- * @returns Result indicating if the agent was removed
1139
- */
1140
- async removeAgent(name, options = {}) {
1141
- const payload = {
1142
- name,
1143
- removeMessages: options.removeMessages,
1144
- };
1145
- return this.query('REMOVE_AGENT', payload);
1146
- }
1147
- // Private methods
1148
- setState(state) {
1149
- this._state = state;
1150
- if (this.onStateChange) {
1151
- this.onStateChange(state);
205
+ throw error;
1152
206
  }
1153
207
  }
1154
- sendHello() {
1155
- const hello = {
1156
- v: PROTOCOL_VERSION,
1157
- type: 'HELLO',
1158
- id: generateId(),
1159
- ts: Date.now(),
1160
- payload: {
1161
- agent: this.config.agentName,
1162
- entityType: this.config.entityType,
1163
- cli: this.config.cli,
1164
- program: this.config.program,
1165
- model: this.config.model,
1166
- task: this.config.task,
1167
- workingDirectory: this.config.workingDirectory,
1168
- team: this.config.team,
1169
- displayName: this.config.displayName,
1170
- avatarUrl: this.config.avatarUrl,
1171
- capabilities: {
1172
- ack: true,
1173
- resume: true,
1174
- max_inflight: 256,
1175
- supports_topics: true,
1176
- },
1177
- session: this.resumeToken ? { resume_token: this.resumeToken } : undefined,
1178
- _isSystemComponent: this.config._isSystemComponent,
1179
- },
1180
- };
1181
- this.send(hello);
208
+ async listAgents() {
209
+ await this.start();
210
+ const result = await this.requestOk('list_agents', {});
211
+ return result.agents;
1182
212
  }
1183
- send(envelope) {
1184
- if (!this.socket)
1185
- return false;
1186
- try {
1187
- const frame = encodeFrameLegacy(envelope);
1188
- this.writeQueue.push(frame);
1189
- if (!this.writeScheduled) {
1190
- this.writeScheduled = true;
1191
- setImmediate(() => this.flushWrites());
1192
- }
1193
- return true;
1194
- }
1195
- catch (err) {
1196
- this.handleError(err);
1197
- return false;
1198
- }
213
+ async getStatus() {
214
+ await this.start();
215
+ return this.requestOk('get_status', {});
1199
216
  }
1200
- flushWrites() {
1201
- this.writeScheduled = false;
1202
- if (this.writeQueue.length === 0 || !this.socket)
217
+ async shutdown() {
218
+ if (!this.child) {
1203
219
  return;
1204
- if (this.writeQueue.length === 1) {
1205
- this.socket.write(this.writeQueue[0]);
1206
- }
1207
- else {
1208
- this.socket.write(Buffer.concat(this.writeQueue));
1209
220
  }
1210
- this.writeQueue = [];
1211
- }
1212
- handleData(data) {
1213
221
  try {
1214
- const frames = this.parser.push(data);
1215
- for (const frame of frames) {
1216
- this.processFrame(frame);
1217
- }
222
+ await this.requestOk('shutdown', {});
1218
223
  }
1219
- catch (err) {
1220
- this.handleError(err);
224
+ catch {
225
+ // Continue shutdown path if broker is already unhealthy.
1221
226
  }
1222
- }
1223
- processFrame(envelope) {
1224
- switch (envelope.type) {
1225
- case 'WELCOME':
1226
- this.handleWelcome(envelope);
1227
- break;
1228
- case 'DELIVER':
1229
- this.handleDeliver(envelope);
1230
- break;
1231
- case 'CHANNEL_MESSAGE':
1232
- this.handleChannelMessage(envelope);
1233
- break;
1234
- case 'PING':
1235
- this.handlePing(envelope);
1236
- break;
1237
- case 'ACK':
1238
- this.handleAck(envelope);
1239
- break;
1240
- case 'SPAWN_RESULT':
1241
- this.handleSpawnResult(envelope);
1242
- break;
1243
- case 'RELEASE_RESULT':
1244
- this.handleReleaseResult(envelope);
1245
- break;
1246
- case 'SEND_INPUT_RESULT':
1247
- this.handleSendInputResult(envelope);
1248
- break;
1249
- case 'SET_MODEL_RESULT':
1250
- this.handleSetModelResult(envelope);
1251
- break;
1252
- case 'LIST_WORKERS_RESULT':
1253
- this.handleListWorkersResult(envelope);
1254
- break;
1255
- case 'AGENT_READY':
1256
- this.handleAgentReady(envelope);
1257
- break;
1258
- case 'ERROR':
1259
- this.handleErrorFrame(envelope);
1260
- break;
1261
- case 'BUSY':
1262
- if (!this.config.quiet) {
1263
- console.warn('[sdk] Server busy, backing off');
1264
- }
1265
- break;
1266
- case 'STATUS_RESPONSE':
1267
- case 'INBOX_RESPONSE':
1268
- case 'MESSAGES_RESPONSE':
1269
- case 'LIST_AGENTS_RESPONSE':
1270
- case 'LIST_CONNECTED_AGENTS_RESPONSE':
1271
- case 'REMOVE_AGENT_RESPONSE':
1272
- case 'HEALTH_RESPONSE':
1273
- case 'METRICS_RESPONSE':
1274
- this.handleQueryResponse(envelope);
1275
- break;
227
+ const child = this.child;
228
+ const wait = this.exitPromise ?? Promise.resolve();
229
+ const timeout = setTimeout(() => {
230
+ if (!child.killed) {
231
+ child.kill('SIGTERM');
232
+ }
233
+ }, this.options.shutdownTimeoutMs);
234
+ try {
235
+ await wait;
1276
236
  }
1277
- }
1278
- handleWelcome(envelope) {
1279
- this.sessionId = envelope.payload.session_id;
1280
- this.resumeToken = envelope.payload.resume_token;
1281
- this.reconnectAttempts = 0;
1282
- this.reconnectDelay = this.config.reconnectDelayMs;
1283
- this.setState('READY');
1284
- if (!this.config.quiet) {
1285
- console.log(`[sdk] Connected as ${this.config.agentName} (session: ${this.sessionId})`);
237
+ finally {
238
+ clearTimeout(timeout);
239
+ if (this.child) {
240
+ this.child.kill('SIGKILL');
241
+ }
1286
242
  }
1287
243
  }
1288
- handleDeliver(envelope) {
1289
- // Send ACK
1290
- this.send({
1291
- v: PROTOCOL_VERSION,
1292
- type: 'ACK',
1293
- id: generateId(),
1294
- ts: Date.now(),
1295
- payload: {
1296
- ack_id: envelope.id,
1297
- seq: envelope.delivery.seq,
1298
- },
1299
- });
1300
- const duplicate = this.dedupeCache.check(envelope.id);
1301
- if (duplicate) {
244
+ async waitForExit() {
245
+ if (!this.child) {
1302
246
  return;
1303
247
  }
1304
- // Check if this is a response to a pending request
1305
- const correlationId = this.extractCorrelationId(envelope);
1306
- if (correlationId && envelope.from) {
1307
- const pending = this.pendingRequests.get(correlationId);
1308
- if (pending) {
1309
- // This is a response to our request
1310
- clearTimeout(pending.timeoutHandle);
1311
- this.pendingRequests.delete(correlationId);
1312
- pending.resolve({
1313
- from: envelope.from,
1314
- body: envelope.payload.body,
1315
- data: envelope.payload.data,
1316
- correlationId,
1317
- thread: envelope.payload.thread,
1318
- payload: envelope.payload,
1319
- });
1320
- // Still call onMessage so the app is aware of the response if needed
248
+ await this.exitPromise;
249
+ }
250
+ async startInternal() {
251
+ const resolvedBinary = expandTilde(this.options.binaryPath);
252
+ if (isExplicitPath(this.options.binaryPath) && !fs.existsSync(resolvedBinary)) {
253
+ throw new AgentRelayProcessError(`broker binary not found: ${this.options.binaryPath}`);
254
+ }
255
+ this.lastStderrLine = undefined;
256
+ const args = [
257
+ 'init',
258
+ '--name',
259
+ this.options.brokerName,
260
+ '--channels',
261
+ this.options.channels.join(','),
262
+ ...this.options.binaryArgs,
263
+ ];
264
+ // Ensure the SDK bin directory (containing agent-relay-broker + relay_send) is on
265
+ // PATH so spawned workers can find relay_send without any user setup.
266
+ const env = { ...this.options.env };
267
+ if (isExplicitPath(this.options.binaryPath)) {
268
+ const binDir = path.dirname(path.resolve(resolvedBinary));
269
+ const currentPath = env.PATH ?? env.Path ?? '';
270
+ if (!currentPath.split(path.delimiter).includes(binDir)) {
271
+ env.PATH = `${binDir}${path.delimiter}${currentPath}`;
1321
272
  }
1322
273
  }
1323
- if (this.onMessage && envelope.from) {
1324
- this.onMessage(envelope.from, envelope.payload, envelope.id, envelope.payload_meta, envelope.delivery.originalTo);
274
+ console.log(`[broker] Starting: ${resolvedBinary} ${args.join(' ')}`);
275
+ const child = spawn(resolvedBinary, args, {
276
+ cwd: this.options.cwd,
277
+ env,
278
+ stdio: 'pipe',
279
+ });
280
+ this.child = child;
281
+ this.stdoutRl = createInterface({ input: child.stdout, crlfDelay: Infinity });
282
+ this.stderrRl = createInterface({ input: child.stderr, crlfDelay: Infinity });
283
+ this.stdoutRl.on('line', (line) => {
284
+ this.handleStdoutLine(line);
285
+ });
286
+ this.stderrRl.on('line', (line) => {
287
+ const trimmed = line.trim();
288
+ if (trimmed) {
289
+ this.lastStderrLine = trimmed;
290
+ }
291
+ for (const listener of this.stderrListeners) {
292
+ listener(line);
293
+ }
294
+ });
295
+ this.exitPromise = new Promise((resolve) => {
296
+ child.once('exit', (code, signal) => {
297
+ const detail = this.lastStderrLine ? `: ${this.lastStderrLine}` : '';
298
+ const error = new AgentRelayProcessError(`broker exited (code=${code ?? 'null'}, signal=${signal ?? 'null'})${detail}`);
299
+ this.failAllPending(error);
300
+ this.disposeProcessHandles();
301
+ resolve();
302
+ });
303
+ child.once('error', (error) => {
304
+ this.failAllPending(error);
305
+ this.disposeProcessHandles();
306
+ resolve();
307
+ });
308
+ });
309
+ await this.requestHello();
310
+ console.log('[broker] Broker ready (hello handshake complete)');
311
+ }
312
+ disposeProcessHandles() {
313
+ this.stdoutRl?.close();
314
+ this.stderrRl?.close();
315
+ this.stdoutRl = undefined;
316
+ this.stderrRl = undefined;
317
+ this.lastStderrLine = undefined;
318
+ this.child = undefined;
319
+ this.exitPromise = undefined;
320
+ }
321
+ failAllPending(error) {
322
+ for (const pending of this.pending.values()) {
323
+ clearTimeout(pending.timeout);
324
+ pending.reject(error);
1325
325
  }
326
+ this.pending.clear();
1326
327
  }
1327
- /**
1328
- * Extract correlation ID from a delivered message.
1329
- * Checks both payload_meta.replyTo and payload.data._correlationId
1330
- */
1331
- extractCorrelationId(envelope) {
1332
- // Check payload_meta.replyTo first (the preferred location)
1333
- if (envelope.payload_meta?.replyTo) {
1334
- return envelope.payload_meta.replyTo;
1335
- }
1336
- // Fall back to checking data._correlationId
1337
- if (envelope.payload.data && typeof envelope.payload.data._correlationId === 'string') {
1338
- return envelope.payload.data._correlationId;
328
+ handleStdoutLine(line) {
329
+ let parsed;
330
+ try {
331
+ parsed = JSON.parse(line);
1339
332
  }
1340
- return undefined;
1341
- }
1342
- handleChannelMessage(envelope) {
1343
- const duplicate = this.dedupeCache.check(envelope.id);
1344
- if (duplicate) {
333
+ catch {
334
+ // Non-protocol output should not crash the SDK.
1345
335
  return;
1346
336
  }
1347
- // Notify channel message handler
1348
- if (this.onChannelMessage && envelope.from) {
1349
- this.onChannelMessage(envelope.from, envelope.payload.channel, envelope.payload.body, envelope);
1350
- }
1351
- // Also call onMessage for backwards compatibility
1352
- if (this.onMessage && envelope.from) {
1353
- const sendPayload = {
1354
- kind: 'message',
1355
- body: envelope.payload.body,
1356
- data: {
1357
- _isChannelMessage: true,
1358
- _channel: envelope.payload.channel,
1359
- _mentions: envelope.payload.mentions,
1360
- },
1361
- thread: envelope.payload.thread,
1362
- };
1363
- this.onMessage(envelope.from, sendPayload, envelope.id, undefined, envelope.payload.channel);
1364
- }
1365
- }
1366
- handleAck(envelope) {
1367
- const correlationId = envelope.payload.correlationId;
1368
- if (!correlationId)
1369
- return;
1370
- const pending = this.pendingSyncAcks.get(correlationId);
1371
- if (!pending)
1372
- return;
1373
- clearTimeout(pending.timeoutHandle);
1374
- this.pendingSyncAcks.delete(correlationId);
1375
- pending.resolve(envelope.payload);
1376
- }
1377
- handleSpawnResult(envelope) {
1378
- const replyTo = envelope.payload.replyTo;
1379
- if (!replyTo)
1380
- return;
1381
- const pending = this.pendingSpawns.get(replyTo);
1382
- if (!pending)
1383
- return;
1384
- clearTimeout(pending.timeoutHandle);
1385
- this.pendingSpawns.delete(replyTo);
1386
- pending.resolve(envelope.payload);
1387
- }
1388
- handleReleaseResult(envelope) {
1389
- const replyTo = envelope.payload.replyTo;
1390
- if (!replyTo)
1391
- return;
1392
- const pending = this.pendingReleases.get(replyTo);
1393
- if (!pending)
1394
- return;
1395
- clearTimeout(pending.timeoutHandle);
1396
- this.pendingReleases.delete(replyTo);
1397
- pending.resolve(envelope.payload);
1398
- }
1399
- handleSendInputResult(envelope) {
1400
- const replyTo = envelope.payload.replyTo;
1401
- if (!replyTo)
337
+ if (!parsed || typeof parsed !== 'object') {
1402
338
  return;
1403
- const pending = this.pendingSendInputs.get(replyTo);
1404
- if (!pending)
1405
- return;
1406
- clearTimeout(pending.timeoutHandle);
1407
- this.pendingSendInputs.delete(replyTo);
1408
- pending.resolve(envelope.payload);
1409
- }
1410
- handleSetModelResult(envelope) {
1411
- const replyTo = envelope.payload.replyTo;
1412
- if (!replyTo)
1413
- return;
1414
- const pending = this.pendingSetModels.get(replyTo);
1415
- if (!pending)
1416
- return;
1417
- clearTimeout(pending.timeoutHandle);
1418
- this.pendingSetModels.delete(replyTo);
1419
- pending.resolve(envelope.payload);
1420
- }
1421
- handleListWorkersResult(envelope) {
1422
- const replyTo = envelope.payload.replyTo;
1423
- if (!replyTo)
1424
- return;
1425
- const pending = this.pendingListWorkers.get(replyTo);
1426
- if (!pending)
1427
- return;
1428
- clearTimeout(pending.timeoutHandle);
1429
- this.pendingListWorkers.delete(replyTo);
1430
- pending.resolve(envelope.payload);
1431
- }
1432
- handleAgentReady(envelope) {
1433
- const agentName = envelope.payload.name;
1434
- // Resolve any pending waitForReady promises for this agent
1435
- const pending = this.pendingAgentReady.get(agentName);
1436
- if (pending) {
1437
- clearTimeout(pending.timeoutHandle);
1438
- this.pendingAgentReady.delete(agentName);
1439
- pending.resolve(envelope.payload);
1440
339
  }
1441
- // Call the onAgentReady callback if registered
1442
- if (this.onAgentReady) {
1443
- this.onAgentReady(envelope.payload);
1444
- }
1445
- }
1446
- handleQueryResponse(envelope) {
1447
- // Query responses use the envelope id to match requests
1448
- const pending = this.pendingQueries.get(envelope.id);
1449
- if (!pending)
340
+ if (parsed.v !== PROTOCOL_VERSION || typeof parsed.type !== 'string') {
1450
341
  return;
1451
- clearTimeout(pending.timeoutHandle);
1452
- this.pendingQueries.delete(envelope.id);
1453
- pending.resolve(envelope.payload);
1454
- }
1455
- handlePing(envelope) {
1456
- this.send({
1457
- v: PROTOCOL_VERSION,
1458
- type: 'PONG',
1459
- id: generateId(),
1460
- ts: Date.now(),
1461
- payload: envelope.payload ?? {},
1462
- });
1463
- }
1464
- handleErrorFrame(envelope) {
1465
- if (!this.config.quiet) {
1466
- console.error('[sdk] Server error:', envelope.payload);
1467
- }
1468
- if (envelope.payload.code === 'RESUME_TOO_OLD') {
1469
- this.resumeToken = undefined;
1470
- this.sessionId = undefined;
1471
342
  }
1472
- // Fatal errors (like DUPLICATE_CONNECTION) should prevent reconnection
1473
- if (envelope.payload.fatal) {
1474
- if (!this.config.quiet) {
1475
- console.error('[sdk] Fatal error received, will not reconnect:', envelope.payload.message);
343
+ const envelope = {
344
+ v: parsed.v,
345
+ type: parsed.type,
346
+ request_id: parsed.request_id,
347
+ payload: parsed.payload,
348
+ };
349
+ if (envelope.type === 'event') {
350
+ const payload = envelope.payload;
351
+ this.eventBuffer.push(payload);
352
+ if (this.eventBuffer.length > this.maxBufferSize) {
353
+ this.eventBuffer.shift();
354
+ }
355
+ for (const listener of this.eventListeners) {
356
+ listener(payload);
1476
357
  }
1477
- this._destroyed = true;
1478
- }
1479
- }
1480
- handleDisconnect() {
1481
- this.parser.reset();
1482
- this.socket = undefined;
1483
- this.rejectPendingSyncAcks(new Error('Disconnected while awaiting ACK'));
1484
- this.rejectPendingSpawns(new Error('Disconnected while awaiting spawn result'));
1485
- this.rejectPendingReleases(new Error('Disconnected while awaiting release result'));
1486
- this.rejectPendingSendInputs(new Error('Disconnected while awaiting send input result'));
1487
- this.rejectPendingSetModels(new Error('Disconnected while awaiting set model result'));
1488
- this.rejectPendingListWorkers(new Error('Disconnected while awaiting list workers result'));
1489
- this.rejectPendingQueries(new Error('Disconnected while awaiting query response'));
1490
- this.rejectPendingRequests(new Error('Disconnected while awaiting request response'));
1491
- this.rejectPendingAgentReady(new Error('Disconnected while awaiting agent ready'));
1492
- if (this._destroyed) {
1493
- this.setState('DISCONNECTED');
1494
358
  return;
1495
359
  }
1496
- if (this.config.reconnect && this.reconnectAttempts < this.config.maxReconnectAttempts) {
1497
- this.scheduleReconnect();
360
+ if (!envelope.request_id) {
361
+ return;
1498
362
  }
1499
- else {
1500
- this.setState('DISCONNECTED');
1501
- if (this.reconnectAttempts >= this.config.maxReconnectAttempts && !this.config.quiet) {
1502
- console.error(`[sdk] Max reconnect attempts reached (${this.config.maxReconnectAttempts}), giving up`);
1503
- }
363
+ const pending = this.pending.get(envelope.request_id);
364
+ if (!pending) {
365
+ return;
1504
366
  }
1505
- }
1506
- handleError(error) {
1507
- if (!this.config.quiet) {
1508
- console.error('[sdk] Error:', error.message);
367
+ if (envelope.type === 'error') {
368
+ clearTimeout(pending.timeout);
369
+ this.pending.delete(envelope.request_id);
370
+ pending.reject(new AgentRelayProtocolError(envelope.payload));
371
+ return;
1509
372
  }
1510
- if (this.onError) {
1511
- this.onError(error);
373
+ if (envelope.type !== pending.expectedType) {
374
+ clearTimeout(pending.timeout);
375
+ this.pending.delete(envelope.request_id);
376
+ pending.reject(new AgentRelayProcessError(`unexpected response type '${envelope.type}' for request '${envelope.request_id}' (expected '${pending.expectedType}')`));
377
+ return;
1512
378
  }
379
+ clearTimeout(pending.timeout);
380
+ this.pending.delete(envelope.request_id);
381
+ pending.resolve(envelope);
1513
382
  }
1514
- rejectPendingSyncAcks(error) {
1515
- for (const [correlationId, pending] of this.pendingSyncAcks.entries()) {
1516
- clearTimeout(pending.timeoutHandle);
1517
- pending.reject(error);
1518
- this.pendingSyncAcks.delete(correlationId);
1519
- }
383
+ async requestHello() {
384
+ const payload = {
385
+ client_name: this.options.clientName,
386
+ client_version: this.options.clientVersion,
387
+ };
388
+ const frame = await this.sendRequest('hello', payload, 'hello_ack');
389
+ return frame.payload;
1520
390
  }
1521
- rejectPendingSpawns(error) {
1522
- for (const [id, pending] of this.pendingSpawns.entries()) {
1523
- clearTimeout(pending.timeoutHandle);
1524
- pending.reject(error);
1525
- this.pendingSpawns.delete(id);
1526
- }
391
+ async requestOk(type, payload) {
392
+ const frame = await this.sendRequest(type, payload, 'ok');
393
+ const result = frame.payload;
394
+ return result.result;
1527
395
  }
1528
- rejectPendingReleases(error) {
1529
- for (const [id, pending] of this.pendingReleases.entries()) {
1530
- clearTimeout(pending.timeoutHandle);
1531
- pending.reject(error);
1532
- this.pendingReleases.delete(id);
396
+ async sendRequest(type, payload, expectedType) {
397
+ if (!this.child) {
398
+ throw new AgentRelayProcessError('broker is not running');
1533
399
  }
1534
- }
1535
- rejectPendingSendInputs(error) {
1536
- for (const [id, pending] of this.pendingSendInputs.entries()) {
1537
- clearTimeout(pending.timeoutHandle);
1538
- pending.reject(error);
1539
- this.pendingSendInputs.delete(id);
400
+ const requestId = `req_${++this.requestSeq}`;
401
+ const message = {
402
+ v: PROTOCOL_VERSION,
403
+ type,
404
+ request_id: requestId,
405
+ payload,
406
+ };
407
+ const responsePromise = new Promise((resolve, reject) => {
408
+ const timeout = setTimeout(() => {
409
+ this.pending.delete(requestId);
410
+ reject(new AgentRelayProcessError(`request timed out after ${this.options.requestTimeoutMs}ms (type='${type}', request_id='${requestId}')`));
411
+ }, this.options.requestTimeoutMs);
412
+ this.pending.set(requestId, {
413
+ expectedType,
414
+ resolve,
415
+ reject,
416
+ timeout,
417
+ });
418
+ });
419
+ const line = `${JSON.stringify(message)}\n`;
420
+ if (!this.child.stdin.write(line)) {
421
+ await once(this.child.stdin, 'drain');
1540
422
  }
423
+ return responsePromise;
1541
424
  }
1542
- rejectPendingSetModels(error) {
1543
- for (const [id, pending] of this.pendingSetModels.entries()) {
1544
- clearTimeout(pending.timeoutHandle);
1545
- pending.reject(error);
1546
- this.pendingSetModels.delete(id);
1547
- }
425
+ }
426
+ const CLI_MODEL_FLAG_CLIS = new Set(['claude', 'codex', 'gemini', 'goose', 'aider']);
427
+ function buildPtyArgsWithModel(cli, args, model) {
428
+ const baseArgs = [...args];
429
+ if (!model) {
430
+ return baseArgs;
1548
431
  }
1549
- rejectPendingListWorkers(error) {
1550
- for (const [id, pending] of this.pendingListWorkers.entries()) {
1551
- clearTimeout(pending.timeoutHandle);
1552
- pending.reject(error);
1553
- this.pendingListWorkers.delete(id);
1554
- }
432
+ const cliName = cli.split(':')[0].trim().toLowerCase();
433
+ if (!CLI_MODEL_FLAG_CLIS.has(cliName)) {
434
+ return baseArgs;
1555
435
  }
1556
- rejectPendingQueries(error) {
1557
- for (const [id, pending] of this.pendingQueries.entries()) {
1558
- clearTimeout(pending.timeoutHandle);
1559
- pending.reject(error);
1560
- this.pendingQueries.delete(id);
1561
- }
436
+ if (hasModelArg(baseArgs)) {
437
+ return baseArgs;
1562
438
  }
1563
- rejectPendingRequests(error) {
1564
- for (const [correlationId, pending] of this.pendingRequests.entries()) {
1565
- clearTimeout(pending.timeoutHandle);
1566
- pending.reject(error);
1567
- this.pendingRequests.delete(correlationId);
439
+ return ['--model', model, ...baseArgs];
440
+ }
441
+ function hasModelArg(args) {
442
+ for (let i = 0; i < args.length; i += 1) {
443
+ const arg = args[i];
444
+ if (arg === '--model') {
445
+ return true;
1568
446
  }
1569
- }
1570
- rejectPendingAgentReady(error) {
1571
- for (const [agentName, pending] of this.pendingAgentReady.entries()) {
1572
- clearTimeout(pending.timeoutHandle);
1573
- pending.reject(error);
1574
- this.pendingAgentReady.delete(agentName);
447
+ if (arg.startsWith('--model=')) {
448
+ return true;
1575
449
  }
1576
450
  }
1577
- scheduleReconnect() {
1578
- this.setState('BACKOFF');
1579
- this.reconnectAttempts++;
1580
- const jitter = Math.random() * 0.3 + 0.85;
1581
- const delay = Math.min(this.reconnectDelay * jitter, this.config.reconnectMaxDelayMs);
1582
- this.reconnectDelay *= 2;
1583
- if (!this.config.quiet) {
1584
- console.log(`[sdk] Reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts})`);
1585
- }
1586
- this.reconnectTimer = setTimeout(() => {
1587
- this.connect().catch(() => { });
1588
- }, delay);
451
+ return false;
452
+ }
453
+ function expandTilde(p) {
454
+ if (p === '~' || p.startsWith('~/') || p.startsWith('~\\')) {
455
+ const home = os.homedir();
456
+ return path.join(home, p.slice(2));
1589
457
  }
458
+ return p;
459
+ }
460
+ function isExplicitPath(binaryPath) {
461
+ return (binaryPath.includes('/') ||
462
+ binaryPath.includes('\\') ||
463
+ binaryPath.startsWith('.') ||
464
+ binaryPath.startsWith('~'));
465
+ }
466
+ function resolveDefaultBinaryPath() {
467
+ const brokerExe = process.platform === 'win32' ? 'agent-relay-broker.exe' : 'agent-relay-broker';
468
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
469
+ // 1. In a source checkout, prefer Cargo's release binary to avoid stale bundled
470
+ // copies when local dev rebuilds happen while broker processes are running.
471
+ const workspaceRelease = path.resolve(moduleDir, '..', '..', '..', 'target', 'release', brokerExe);
472
+ if (fs.existsSync(workspaceRelease)) {
473
+ return workspaceRelease;
474
+ }
475
+ // 2. Check for bundled broker binary in SDK package (npm install)
476
+ const bundled = path.resolve(moduleDir, '..', 'bin', brokerExe);
477
+ if (fs.existsSync(bundled)) {
478
+ return bundled;
479
+ }
480
+ // 3. Check for standalone broker binary in ~/.agent-relay/bin/ (install.sh)
481
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
482
+ const standaloneBroker = path.join(homeDir, '.agent-relay', 'bin', brokerExe);
483
+ if (fs.existsSync(standaloneBroker)) {
484
+ return standaloneBroker;
485
+ }
486
+ // 4. Fall back to agent-relay on PATH (may be Node CLI — will fail for broker ops)
487
+ return 'agent-relay';
1590
488
  }
1591
489
  //# sourceMappingURL=client.js.map