@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.
- package/README.md +68 -838
- package/bin/agent-relay-broker +0 -0
- package/dist/__tests__/contract-fixtures.test.d.ts +2 -0
- package/dist/__tests__/contract-fixtures.test.d.ts.map +1 -0
- package/dist/__tests__/contract-fixtures.test.js +85 -0
- package/dist/__tests__/contract-fixtures.test.js.map +1 -0
- package/dist/__tests__/facade.test.d.ts +2 -0
- package/dist/__tests__/facade.test.d.ts.map +1 -0
- package/dist/__tests__/facade.test.js +257 -0
- package/dist/__tests__/facade.test.js.map +1 -0
- package/dist/__tests__/integration.test.d.ts +2 -0
- package/dist/__tests__/integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration.test.js +164 -0
- package/dist/__tests__/integration.test.js.map +1 -0
- package/dist/__tests__/pty.test.d.ts +2 -0
- package/dist/__tests__/pty.test.d.ts.map +1 -0
- package/dist/__tests__/pty.test.js +20 -0
- package/dist/__tests__/pty.test.js.map +1 -0
- package/dist/__tests__/quickstart.test.d.ts +2 -0
- package/dist/__tests__/quickstart.test.d.ts.map +1 -0
- package/dist/__tests__/quickstart.test.js +176 -0
- package/dist/__tests__/quickstart.test.js.map +1 -0
- package/dist/__tests__/spawn-from-env.test.d.ts +2 -0
- package/dist/__tests__/spawn-from-env.test.d.ts.map +1 -0
- package/dist/__tests__/spawn-from-env.test.js +206 -0
- package/dist/__tests__/spawn-from-env.test.js.map +1 -0
- package/dist/__tests__/unit.test.d.ts +2 -0
- package/dist/__tests__/unit.test.d.ts.map +1 -0
- package/dist/__tests__/unit.test.js +311 -0
- package/dist/__tests__/unit.test.js.map +1 -0
- package/dist/browser.d.ts +16 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +19 -0
- package/dist/browser.js.map +1 -0
- package/dist/client.d.ts +138 -526
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +407 -1509
- package/dist/client.js.map +1 -1
- package/dist/consensus-helpers.d.ts +103 -0
- package/dist/consensus-helpers.d.ts.map +1 -0
- package/dist/consensus-helpers.js +147 -0
- package/dist/consensus-helpers.js.map +1 -0
- package/dist/consensus.d.ts +72 -0
- package/dist/consensus.d.ts.map +1 -0
- package/dist/consensus.js +378 -0
- package/dist/consensus.js.map +1 -0
- package/dist/examples/demo.d.ts +2 -0
- package/dist/examples/demo.d.ts.map +1 -0
- package/dist/examples/demo.js +63 -0
- package/dist/examples/demo.js.map +1 -0
- package/dist/examples/example.d.ts +2 -0
- package/dist/examples/example.d.ts.map +1 -0
- package/dist/examples/example.js +80 -0
- package/dist/examples/example.js.map +1 -0
- package/dist/examples/quickstart.d.ts +2 -0
- package/dist/examples/quickstart.d.ts.map +1 -0
- package/dist/examples/quickstart.js +56 -0
- package/dist/examples/quickstart.js.map +1 -0
- package/dist/examples/ralph-loop.d.ts +2 -0
- package/dist/examples/ralph-loop.d.ts.map +1 -0
- package/dist/examples/ralph-loop.js +281 -0
- package/dist/examples/ralph-loop.js.map +1 -0
- package/dist/examples/workflow-superiority.d.ts +32 -0
- package/dist/examples/workflow-superiority.d.ts.map +1 -0
- package/dist/examples/workflow-superiority.js +1421 -0
- package/dist/examples/workflow-superiority.js.map +1 -0
- package/dist/index.d.ts +13 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -26
- package/dist/index.js.map +1 -1
- package/dist/logs.d.ts +70 -25
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +238 -42
- package/dist/logs.js.map +1 -1
- package/dist/models.d.ts +9 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +17 -0
- package/dist/models.js.map +1 -0
- package/dist/protocol.d.ts +366 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +2 -0
- package/dist/protocol.js.map +1 -0
- package/dist/pty.d.ts +8 -0
- package/dist/pty.d.ts.map +1 -0
- package/dist/pty.js +26 -0
- package/dist/pty.js.map +1 -0
- package/dist/relay-adapter.d.ts +139 -0
- package/dist/relay-adapter.d.ts.map +1 -0
- package/dist/relay-adapter.js +210 -0
- package/dist/relay-adapter.js.map +1 -0
- package/dist/relay.d.ts +277 -0
- package/dist/relay.d.ts.map +1 -0
- package/dist/relay.js +853 -0
- package/dist/relay.js.map +1 -0
- package/dist/shadow.d.ts +101 -0
- package/dist/shadow.d.ts.map +1 -0
- package/dist/shadow.js +174 -0
- package/dist/shadow.js.map +1 -0
- package/dist/spawn-from-env.d.ts +77 -0
- package/dist/spawn-from-env.d.ts.map +1 -0
- package/dist/spawn-from-env.js +172 -0
- package/dist/spawn-from-env.js.map +1 -0
- package/dist/workflows/barrier.d.ts +72 -0
- package/dist/workflows/barrier.d.ts.map +1 -0
- package/dist/workflows/barrier.js +162 -0
- package/dist/workflows/barrier.js.map +1 -0
- package/dist/workflows/builder.d.ts +114 -0
- package/dist/workflows/builder.d.ts.map +1 -0
- package/dist/workflows/builder.js +201 -0
- package/dist/workflows/builder.js.map +1 -0
- package/dist/workflows/cli.d.ts +11 -0
- package/dist/workflows/cli.d.ts.map +1 -0
- package/dist/workflows/cli.js +144 -0
- package/dist/workflows/cli.js.map +1 -0
- package/dist/workflows/coordinator.d.ts +73 -0
- package/dist/workflows/coordinator.d.ts.map +1 -0
- package/dist/workflows/coordinator.js +647 -0
- package/dist/workflows/coordinator.js.map +1 -0
- package/dist/workflows/custom-steps.d.ts +73 -0
- package/dist/workflows/custom-steps.d.ts.map +1 -0
- package/dist/workflows/custom-steps.js +321 -0
- package/dist/workflows/custom-steps.js.map +1 -0
- package/dist/workflows/dry-run-format.d.ts +6 -0
- package/dist/workflows/dry-run-format.d.ts.map +1 -0
- package/dist/workflows/dry-run-format.js +68 -0
- package/dist/workflows/dry-run-format.js.map +1 -0
- package/dist/workflows/file-db.d.ts +33 -0
- package/dist/workflows/file-db.d.ts.map +1 -0
- package/dist/workflows/file-db.js +108 -0
- package/dist/workflows/file-db.js.map +1 -0
- package/dist/workflows/index.d.ts +15 -0
- package/dist/workflows/index.d.ts.map +1 -0
- package/dist/workflows/index.js +15 -0
- package/dist/workflows/index.js.map +1 -0
- package/dist/workflows/memory-db.d.ts +17 -0
- package/dist/workflows/memory-db.d.ts.map +1 -0
- package/dist/workflows/memory-db.js +33 -0
- package/dist/workflows/memory-db.js.map +1 -0
- package/dist/workflows/run.d.ts +38 -0
- package/dist/workflows/run.d.ts.map +1 -0
- package/dist/workflows/run.js +25 -0
- package/dist/workflows/run.js.map +1 -0
- package/dist/workflows/runner.d.ts +320 -0
- package/dist/workflows/runner.d.ts.map +1 -0
- package/dist/workflows/runner.js +2821 -0
- package/dist/workflows/runner.js.map +1 -0
- package/dist/workflows/state.d.ts +77 -0
- package/dist/workflows/state.d.ts.map +1 -0
- package/dist/workflows/state.js +140 -0
- package/dist/workflows/state.js.map +1 -0
- package/dist/workflows/templates.d.ts +47 -0
- package/dist/workflows/templates.d.ts.map +1 -0
- package/dist/workflows/templates.js +405 -0
- package/dist/workflows/templates.js.map +1 -0
- package/dist/workflows/trajectory.d.ts +87 -0
- package/dist/workflows/trajectory.d.ts.map +1 -0
- package/dist/workflows/trajectory.js +441 -0
- package/dist/workflows/trajectory.js.map +1 -0
- package/dist/workflows/types.d.ts +306 -0
- package/dist/workflows/types.d.ts.map +1 -0
- package/dist/workflows/types.js +23 -0
- package/dist/workflows/types.js.map +1 -0
- package/dist/workflows/validator.d.ts +11 -0
- package/dist/workflows/validator.d.ts.map +1 -0
- package/dist/workflows/validator.js +128 -0
- package/dist/workflows/validator.js.map +1 -0
- package/package.json +59 -53
- package/dist/discovery.d.ts +0 -10
- package/dist/discovery.d.ts.map +0 -1
- package/dist/discovery.js +0 -22
- package/dist/discovery.js.map +0 -1
- package/dist/errors.d.ts +0 -9
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -9
- package/dist/errors.js.map +0 -1
- package/dist/protocol/index.d.ts +0 -8
- package/dist/protocol/index.d.ts.map +0 -1
- package/dist/protocol/index.js +0 -8
- package/dist/protocol/index.js.map +0 -1
package/dist/client.js
CHANGED
|
@@ -1,1591 +1,489 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
127
|
-
|
|
55
|
+
static async start(options = {}) {
|
|
56
|
+
const client = new AgentRelayClient(options);
|
|
57
|
+
await client.start();
|
|
58
|
+
return client;
|
|
128
59
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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 (
|
|
196
|
-
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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
|
-
|
|
543
|
-
|
|
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
|
-
|
|
106
|
+
this.startingPromise = this.startInternal();
|
|
608
107
|
try {
|
|
609
|
-
|
|
610
|
-
return {
|
|
611
|
-
...spawnResult,
|
|
612
|
-
ready: true,
|
|
613
|
-
readyInfo,
|
|
614
|
-
};
|
|
108
|
+
await this.startingPromise;
|
|
615
109
|
}
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
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
|
|
647
|
-
if (
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
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
|
-
|
|
698
|
-
|
|
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
|
-
|
|
733
|
-
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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
|
-
|
|
1201
|
-
this.
|
|
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
|
-
|
|
1215
|
-
for (const frame of frames) {
|
|
1216
|
-
this.processFrame(frame);
|
|
1217
|
-
}
|
|
222
|
+
await this.requestOk('shutdown', {});
|
|
1218
223
|
}
|
|
1219
|
-
catch
|
|
1220
|
-
|
|
224
|
+
catch {
|
|
225
|
+
// Continue shutdown path if broker is already unhealthy.
|
|
1221
226
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
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
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
-
|
|
1289
|
-
|
|
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
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
-
|
|
1324
|
-
|
|
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
|
-
|
|
1329
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
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 (
|
|
1497
|
-
|
|
360
|
+
if (!envelope.request_id) {
|
|
361
|
+
return;
|
|
1498
362
|
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
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
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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 (
|
|
1511
|
-
|
|
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
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
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
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
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
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
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
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
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
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
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
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
-
|
|
1557
|
-
|
|
1558
|
-
clearTimeout(pending.timeoutHandle);
|
|
1559
|
-
pending.reject(error);
|
|
1560
|
-
this.pendingQueries.delete(id);
|
|
1561
|
-
}
|
|
436
|
+
if (hasModelArg(baseArgs)) {
|
|
437
|
+
return baseArgs;
|
|
1562
438
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
const
|
|
1582
|
-
|
|
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
|