@agent-relay/sdk 2.3.13 → 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/relay.js
ADDED
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level facade for the Agent Relay SDK.
|
|
3
|
+
*
|
|
4
|
+
* Provides a clean, property-based API on top of the lower-level
|
|
5
|
+
* {@link AgentRelayClient} protocol client.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { AgentRelay } from "@agent-relay/sdk";
|
|
10
|
+
*
|
|
11
|
+
* const relay = new AgentRelay();
|
|
12
|
+
*
|
|
13
|
+
* relay.onMessageReceived = (message) => console.log(message);
|
|
14
|
+
* relay.onAgentSpawned = (agent) => console.log("spawned", agent.name);
|
|
15
|
+
*
|
|
16
|
+
* const codex = await relay.codex.spawn();
|
|
17
|
+
* const human = relay.human({ name: "System" });
|
|
18
|
+
* await human.sendMessage({ to: codex.name, text: "Hello!" });
|
|
19
|
+
*
|
|
20
|
+
* const agents = await relay.listAgents();
|
|
21
|
+
* for (const a of agents) await a.release();
|
|
22
|
+
* await relay.shutdown();
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import { randomBytes } from 'node:crypto';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { AgentRelayClient, AgentRelayProtocolError, } from './client.js';
|
|
28
|
+
import { followLogs as followLogsFromFile, getLogs as getLogsFromFile, listLoggedAgents as listLoggedAgentsFromFile, } from './logs.js';
|
|
29
|
+
function isUnsupportedOperation(error) {
|
|
30
|
+
return error instanceof AgentRelayProtocolError && error.code === 'unsupported_operation';
|
|
31
|
+
}
|
|
32
|
+
function buildUnsupportedOperationMessage(from, input) {
|
|
33
|
+
return {
|
|
34
|
+
eventId: 'unsupported_operation',
|
|
35
|
+
from,
|
|
36
|
+
to: input.to,
|
|
37
|
+
text: input.text,
|
|
38
|
+
threadId: input.threadId,
|
|
39
|
+
data: input.data,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// ── AgentRelay facade ───────────────────────────────────────────────────────
|
|
43
|
+
export class AgentRelay {
|
|
44
|
+
// Event hooks — assign a callback or null to clear.
|
|
45
|
+
onMessageReceived = null;
|
|
46
|
+
onMessageSent = null;
|
|
47
|
+
onAgentSpawned = null;
|
|
48
|
+
onAgentReleased = null;
|
|
49
|
+
onAgentExited = null;
|
|
50
|
+
onAgentReady = null;
|
|
51
|
+
onWorkerOutput = null;
|
|
52
|
+
onDeliveryUpdate = null;
|
|
53
|
+
onAgentExitRequested = null;
|
|
54
|
+
onAgentIdle = null;
|
|
55
|
+
// Shorthand spawners
|
|
56
|
+
codex;
|
|
57
|
+
claude;
|
|
58
|
+
gemini;
|
|
59
|
+
clientOptions;
|
|
60
|
+
defaultChannels;
|
|
61
|
+
client;
|
|
62
|
+
startPromise;
|
|
63
|
+
unsubEvent;
|
|
64
|
+
knownAgents = new Map();
|
|
65
|
+
readyAgents = new Set();
|
|
66
|
+
messageReadyAgents = new Set();
|
|
67
|
+
exitedAgents = new Set();
|
|
68
|
+
idleAgents = new Set();
|
|
69
|
+
deliveryStates = new Map();
|
|
70
|
+
outputListeners = new Map();
|
|
71
|
+
exitResolvers = new Map();
|
|
72
|
+
exitResolverSeq = 0;
|
|
73
|
+
idleResolvers = new Map();
|
|
74
|
+
idleResolverSeq = 0;
|
|
75
|
+
constructor(options = {}) {
|
|
76
|
+
this.defaultChannels = options.channels ?? ['general'];
|
|
77
|
+
this.clientOptions = {
|
|
78
|
+
binaryPath: options.binaryPath,
|
|
79
|
+
binaryArgs: options.binaryArgs,
|
|
80
|
+
brokerName: options.brokerName,
|
|
81
|
+
channels: this.defaultChannels,
|
|
82
|
+
cwd: options.cwd,
|
|
83
|
+
env: options.env,
|
|
84
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
85
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs,
|
|
86
|
+
};
|
|
87
|
+
this.codex = this.createSpawner('codex', 'Codex', 'pty');
|
|
88
|
+
this.claude = this.createSpawner('claude', 'Claude', 'pty');
|
|
89
|
+
this.gemini = this.createSpawner('gemini', 'Gemini', 'pty');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Subscribe to broker stderr output. Listener is wired immediately if the
|
|
93
|
+
* client is already started, otherwise it is attached when the client starts.
|
|
94
|
+
* Returns an unsubscribe function.
|
|
95
|
+
*/
|
|
96
|
+
onBrokerStderr(listener) {
|
|
97
|
+
if (this.client) {
|
|
98
|
+
return this.client.onBrokerStderr(listener);
|
|
99
|
+
}
|
|
100
|
+
// Queue it: once ensureStarted completes, wire it up
|
|
101
|
+
let unsub;
|
|
102
|
+
const queuedUnsub = () => {
|
|
103
|
+
unsub?.();
|
|
104
|
+
};
|
|
105
|
+
// Use the start promise if one is pending
|
|
106
|
+
const promise = this.startPromise ?? this.ensureStarted();
|
|
107
|
+
promise
|
|
108
|
+
.then((c) => {
|
|
109
|
+
unsub = c.onBrokerStderr(listener);
|
|
110
|
+
})
|
|
111
|
+
.catch(() => { });
|
|
112
|
+
return queuedUnsub;
|
|
113
|
+
}
|
|
114
|
+
// ── Spawning ────────────────────────────────────────────────────────────
|
|
115
|
+
async spawnPty(input) {
|
|
116
|
+
const client = await this.ensureStarted();
|
|
117
|
+
if (!input.channels || input.channels.length === 0) {
|
|
118
|
+
console.warn(`[AgentRelay] spawnPty("${input.name}"): no channels specified, defaulting to "general". ` +
|
|
119
|
+
'Set explicit channels for workflow isolation.');
|
|
120
|
+
}
|
|
121
|
+
const channels = input.channels ?? ['general'];
|
|
122
|
+
const result = await client.spawnPty({
|
|
123
|
+
name: input.name,
|
|
124
|
+
cli: input.cli,
|
|
125
|
+
args: input.args,
|
|
126
|
+
channels,
|
|
127
|
+
task: input.task,
|
|
128
|
+
model: input.model,
|
|
129
|
+
cwd: input.cwd,
|
|
130
|
+
team: input.team,
|
|
131
|
+
shadowOf: input.shadowOf,
|
|
132
|
+
shadowMode: input.shadowMode,
|
|
133
|
+
idleThresholdSecs: input.idleThresholdSecs,
|
|
134
|
+
restartPolicy: input.restartPolicy,
|
|
135
|
+
});
|
|
136
|
+
this.readyAgents.delete(result.name);
|
|
137
|
+
this.messageReadyAgents.delete(result.name);
|
|
138
|
+
this.exitedAgents.delete(result.name);
|
|
139
|
+
this.idleAgents.delete(result.name);
|
|
140
|
+
const agent = this.makeAgent(result.name, result.runtime, channels);
|
|
141
|
+
this.knownAgents.set(agent.name, agent);
|
|
142
|
+
return agent;
|
|
143
|
+
}
|
|
144
|
+
async spawn(name, cli, task, options) {
|
|
145
|
+
return this.spawnPty({
|
|
146
|
+
name,
|
|
147
|
+
cli,
|
|
148
|
+
task,
|
|
149
|
+
args: options?.args,
|
|
150
|
+
channels: options?.channels,
|
|
151
|
+
model: options?.model,
|
|
152
|
+
cwd: options?.cwd,
|
|
153
|
+
team: options?.team,
|
|
154
|
+
shadowOf: options?.shadowOf,
|
|
155
|
+
shadowMode: options?.shadowMode,
|
|
156
|
+
idleThresholdSecs: options?.idleThresholdSecs,
|
|
157
|
+
restartPolicy: options?.restartPolicy,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async spawnAndWait(name, cli, task, options) {
|
|
161
|
+
const { timeoutMs, waitForMessage, ...spawnOptions } = options ?? {};
|
|
162
|
+
await this.spawn(name, cli, task, spawnOptions);
|
|
163
|
+
if (waitForMessage) {
|
|
164
|
+
return this.waitForAgentMessage(name, timeoutMs ?? 60_000);
|
|
165
|
+
}
|
|
166
|
+
return this.waitForAgentReady(name, timeoutMs ?? 30_000);
|
|
167
|
+
}
|
|
168
|
+
// ── Human source ────────────────────────────────────────────────────────
|
|
169
|
+
human(opts) {
|
|
170
|
+
return {
|
|
171
|
+
name: opts.name,
|
|
172
|
+
sendMessage: async (input) => {
|
|
173
|
+
const client = await this.ensureStarted();
|
|
174
|
+
let result;
|
|
175
|
+
try {
|
|
176
|
+
result = await client.sendMessage({
|
|
177
|
+
to: input.to,
|
|
178
|
+
text: input.text,
|
|
179
|
+
from: opts.name,
|
|
180
|
+
threadId: input.threadId,
|
|
181
|
+
priority: input.priority,
|
|
182
|
+
data: input.data,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
if (isUnsupportedOperation(error)) {
|
|
187
|
+
return buildUnsupportedOperationMessage(opts.name, input);
|
|
188
|
+
}
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
if (result?.event_id === 'unsupported_operation') {
|
|
192
|
+
return buildUnsupportedOperationMessage(opts.name, input);
|
|
193
|
+
}
|
|
194
|
+
const eventId = result?.event_id ?? randomBytes(8).toString('hex');
|
|
195
|
+
const msg = {
|
|
196
|
+
eventId,
|
|
197
|
+
from: opts.name,
|
|
198
|
+
to: input.to,
|
|
199
|
+
text: input.text,
|
|
200
|
+
threadId: input.threadId,
|
|
201
|
+
data: input.data,
|
|
202
|
+
};
|
|
203
|
+
this.onMessageSent?.(msg);
|
|
204
|
+
return msg;
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
system() {
|
|
209
|
+
return this.human({ name: 'system' });
|
|
210
|
+
}
|
|
211
|
+
// ── Messaging ─────────────────────────────────────────────────────────
|
|
212
|
+
/**
|
|
213
|
+
* Broadcast a message to all connected agents.
|
|
214
|
+
* @param text — the message body
|
|
215
|
+
* @param options — optional sender name (defaults to "human:orchestrator")
|
|
216
|
+
*/
|
|
217
|
+
async broadcast(text, options) {
|
|
218
|
+
const from = options?.from ?? 'human:orchestrator';
|
|
219
|
+
return this.human({ name: from }).sendMessage({ to: '*', text });
|
|
220
|
+
}
|
|
221
|
+
async sendAndWaitForDelivery(input, timeoutMs = 30_000) {
|
|
222
|
+
const client = await this.ensureStarted();
|
|
223
|
+
const result = await client.sendMessage(input);
|
|
224
|
+
if (!result.targets.length) {
|
|
225
|
+
return { eventId: result.event_id, status: 'failed', targets: [] };
|
|
226
|
+
}
|
|
227
|
+
return new Promise((resolve) => {
|
|
228
|
+
let resolved = false;
|
|
229
|
+
const ackedTargets = new Set();
|
|
230
|
+
// eslint-disable-next-line prefer-const
|
|
231
|
+
let unsubscribe;
|
|
232
|
+
const timer = setTimeout(() => {
|
|
233
|
+
if (!resolved) {
|
|
234
|
+
resolved = true;
|
|
235
|
+
unsubscribe?.();
|
|
236
|
+
resolve({ eventId: result.event_id, status: 'timeout', targets: result.targets });
|
|
237
|
+
}
|
|
238
|
+
}, timeoutMs);
|
|
239
|
+
unsubscribe = client.onEvent((event) => {
|
|
240
|
+
if (resolved)
|
|
241
|
+
return;
|
|
242
|
+
if (event.kind === 'delivery_ack' &&
|
|
243
|
+
event.event_id === result.event_id &&
|
|
244
|
+
result.targets.includes(event.name)) {
|
|
245
|
+
ackedTargets.add(event.name);
|
|
246
|
+
if (ackedTargets.size >= result.targets.length) {
|
|
247
|
+
resolved = true;
|
|
248
|
+
clearTimeout(timer);
|
|
249
|
+
unsubscribe?.();
|
|
250
|
+
resolve({ eventId: result.event_id, status: 'ack', targets: result.targets });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (event.kind === 'delivery_failed' &&
|
|
254
|
+
event.event_id === result.event_id &&
|
|
255
|
+
result.targets.includes(event.name)) {
|
|
256
|
+
resolved = true;
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
unsubscribe?.();
|
|
259
|
+
resolve({ eventId: result.event_id, status: 'failed', targets: result.targets });
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
// ── Listing ─────────────────────────────────────────────────────────────
|
|
265
|
+
async listAgents() {
|
|
266
|
+
const client = await this.ensureStarted();
|
|
267
|
+
const list = await client.listAgents();
|
|
268
|
+
return list.map((entry) => {
|
|
269
|
+
const existing = this.knownAgents.get(entry.name);
|
|
270
|
+
if (existing)
|
|
271
|
+
return existing;
|
|
272
|
+
const agent = this.makeAgent(entry.name, entry.runtime, entry.channels);
|
|
273
|
+
this.knownAgents.set(agent.name, agent);
|
|
274
|
+
return agent;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
/** Pre-register a batch of agents with Relaycast before steps execute. */
|
|
278
|
+
async preflightAgents(agents) {
|
|
279
|
+
const client = await this.ensureStarted();
|
|
280
|
+
await client.preflightAgents(agents);
|
|
281
|
+
}
|
|
282
|
+
/** List agents with PIDs from the broker (for worker registration). */
|
|
283
|
+
async listAgentsRaw() {
|
|
284
|
+
const client = await this.ensureStarted();
|
|
285
|
+
return client.listAgents();
|
|
286
|
+
}
|
|
287
|
+
// ── Status ────────────────────────────────────────────────────────────
|
|
288
|
+
async getStatus() {
|
|
289
|
+
const client = await this.ensureStarted();
|
|
290
|
+
return client.getStatus();
|
|
291
|
+
}
|
|
292
|
+
getDeliveryState(eventId) {
|
|
293
|
+
return this.deliveryStates.get(eventId);
|
|
294
|
+
}
|
|
295
|
+
// ── Logs ──────────────────────────────────────────────────────────────
|
|
296
|
+
/**
|
|
297
|
+
* Read the last N lines of an agent's log file.
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```ts
|
|
301
|
+
* const logs = await relay.getLogs("Worker1", { lines: 100 });
|
|
302
|
+
* if (logs.found) console.log(logs.content);
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
async getLogs(agentName, options) {
|
|
306
|
+
const cwd = this.clientOptions.cwd ?? process.cwd();
|
|
307
|
+
const logsDir = path.join(cwd, '.agent-relay', 'team', 'worker-logs');
|
|
308
|
+
return getLogsFromFile(agentName, { logsDir, lines: options?.lines });
|
|
309
|
+
}
|
|
310
|
+
/** List all agents that have log files. */
|
|
311
|
+
async listLoggedAgents() {
|
|
312
|
+
const cwd = this.clientOptions.cwd ?? process.cwd();
|
|
313
|
+
const logsDir = path.join(cwd, '.agent-relay', 'team', 'worker-logs');
|
|
314
|
+
return listLoggedAgentsFromFile(logsDir);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Follow an agent's local log file with history bootstrap + incremental updates.
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* ```ts
|
|
321
|
+
* const handle = relay.followLogs("Worker1", {
|
|
322
|
+
* historyLines: 100,
|
|
323
|
+
* onEvent(event) {
|
|
324
|
+
* if (event.type === "log") console.log(event.content);
|
|
325
|
+
* },
|
|
326
|
+
* });
|
|
327
|
+
*
|
|
328
|
+
* // Later:
|
|
329
|
+
* handle.unsubscribe();
|
|
330
|
+
* ```
|
|
331
|
+
*/
|
|
332
|
+
followLogs(agentName, options) {
|
|
333
|
+
const cwd = this.clientOptions.cwd ?? process.cwd();
|
|
334
|
+
const logsDir = path.join(cwd, '.agent-relay', 'team', 'worker-logs');
|
|
335
|
+
return followLogsFromFile(agentName, { ...options, logsDir });
|
|
336
|
+
}
|
|
337
|
+
// ── Wait helpers ──────────────────────────────────────────────────────
|
|
338
|
+
/**
|
|
339
|
+
* Wait for any one of the given agents to exit. Returns the first agent
|
|
340
|
+
* that exits along with its exit reason.
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* ```ts
|
|
344
|
+
* const { agent, result } = await AgentRelay.waitForAny([worker1, worker2], 60_000);
|
|
345
|
+
* console.log(`${agent.name} finished: ${result}`);
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
348
|
+
static async waitForAny(agents, timeoutMs) {
|
|
349
|
+
if (agents.length === 0) {
|
|
350
|
+
throw new Error('waitForAny requires at least one agent');
|
|
351
|
+
}
|
|
352
|
+
return Promise.race(agents.map(async (agent) => {
|
|
353
|
+
const result = await agent.waitForExit(timeoutMs);
|
|
354
|
+
return { agent, result };
|
|
355
|
+
}));
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Resolves when the agent process has started and connected to the broker.
|
|
359
|
+
* The agent's CLI may not yet be ready to receive messages.
|
|
360
|
+
* Use `waitForAgentMessage()` for full readiness.
|
|
361
|
+
*/
|
|
362
|
+
async waitForAgentReady(name, timeoutMs = 30_000) {
|
|
363
|
+
const client = await this.ensureStarted();
|
|
364
|
+
const existing = this.knownAgents.get(name);
|
|
365
|
+
if (existing && this.readyAgents.has(name)) {
|
|
366
|
+
return existing;
|
|
367
|
+
}
|
|
368
|
+
return new Promise((resolve, reject) => {
|
|
369
|
+
let settled = false;
|
|
370
|
+
let timeout;
|
|
371
|
+
const cleanup = () => {
|
|
372
|
+
unsubscribe();
|
|
373
|
+
if (timeout) {
|
|
374
|
+
clearTimeout(timeout);
|
|
375
|
+
timeout = undefined;
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const resolveWith = (agent) => {
|
|
379
|
+
if (settled)
|
|
380
|
+
return;
|
|
381
|
+
settled = true;
|
|
382
|
+
cleanup();
|
|
383
|
+
resolve(agent);
|
|
384
|
+
};
|
|
385
|
+
const rejectWith = (error) => {
|
|
386
|
+
if (settled)
|
|
387
|
+
return;
|
|
388
|
+
settled = true;
|
|
389
|
+
cleanup();
|
|
390
|
+
reject(error);
|
|
391
|
+
};
|
|
392
|
+
const unsubscribe = client.onEvent((event) => {
|
|
393
|
+
if (event.kind !== 'worker_ready' || event.name !== name) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const agent = this.ensureAgentHandle(event.name, event.runtime);
|
|
397
|
+
this.readyAgents.add(event.name);
|
|
398
|
+
this.exitedAgents.delete(event.name);
|
|
399
|
+
resolveWith(agent);
|
|
400
|
+
});
|
|
401
|
+
timeout = setTimeout(() => {
|
|
402
|
+
rejectWith(new Error(`Timed out waiting for worker_ready for '${name}' after ${timeoutMs}ms`));
|
|
403
|
+
}, timeoutMs);
|
|
404
|
+
const known = this.knownAgents.get(name);
|
|
405
|
+
if (known && this.readyAgents.has(name)) {
|
|
406
|
+
resolveWith(known);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
async waitForAgentMessage(name, timeoutMs = 60_000) {
|
|
411
|
+
const client = await this.ensureStarted();
|
|
412
|
+
const existing = this.knownAgents.get(name);
|
|
413
|
+
if (existing && this.messageReadyAgents.has(name)) {
|
|
414
|
+
return existing;
|
|
415
|
+
}
|
|
416
|
+
return new Promise((resolve, reject) => {
|
|
417
|
+
let settled = false;
|
|
418
|
+
let timeout;
|
|
419
|
+
const cleanup = () => {
|
|
420
|
+
unsubscribe();
|
|
421
|
+
if (timeout) {
|
|
422
|
+
clearTimeout(timeout);
|
|
423
|
+
timeout = undefined;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
const resolveWith = (agent) => {
|
|
427
|
+
if (settled)
|
|
428
|
+
return;
|
|
429
|
+
settled = true;
|
|
430
|
+
cleanup();
|
|
431
|
+
resolve(agent);
|
|
432
|
+
};
|
|
433
|
+
const rejectWith = (error) => {
|
|
434
|
+
if (settled)
|
|
435
|
+
return;
|
|
436
|
+
settled = true;
|
|
437
|
+
cleanup();
|
|
438
|
+
reject(error);
|
|
439
|
+
};
|
|
440
|
+
const unsubscribe = client.onEvent((event) => {
|
|
441
|
+
if (event.kind === 'relay_inbound' && event.from === name) {
|
|
442
|
+
this.messageReadyAgents.add(name);
|
|
443
|
+
this.exitedAgents.delete(name);
|
|
444
|
+
resolveWith(this.ensureAgentHandle(name));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (event.kind === 'agent_exited' && event.name === name) {
|
|
448
|
+
rejectWith(new Error(`Agent '${name}' exited before sending its first relay message`));
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (event.kind === 'agent_released' && event.name === name) {
|
|
452
|
+
rejectWith(new Error(`Agent '${name}' was released before sending its first relay message`));
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
timeout = setTimeout(() => {
|
|
456
|
+
rejectWith(new Error(`Timed out waiting for first relay message from '${name}' after ${timeoutMs}ms`));
|
|
457
|
+
}, timeoutMs);
|
|
458
|
+
const known = this.knownAgents.get(name);
|
|
459
|
+
if (known && this.messageReadyAgents.has(name)) {
|
|
460
|
+
resolveWith(known);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
// ── Lifecycle ───────────────────────────────────────────────────────────
|
|
465
|
+
async shutdown() {
|
|
466
|
+
if (this.unsubEvent) {
|
|
467
|
+
this.unsubEvent();
|
|
468
|
+
this.unsubEvent = undefined;
|
|
469
|
+
}
|
|
470
|
+
if (this.client) {
|
|
471
|
+
await this.client.shutdown();
|
|
472
|
+
this.client = undefined;
|
|
473
|
+
}
|
|
474
|
+
this.knownAgents.clear();
|
|
475
|
+
this.readyAgents.clear();
|
|
476
|
+
this.messageReadyAgents.clear();
|
|
477
|
+
this.exitedAgents.clear();
|
|
478
|
+
this.idleAgents.clear();
|
|
479
|
+
this.deliveryStates.clear();
|
|
480
|
+
this.outputListeners.clear();
|
|
481
|
+
for (const entry of this.exitResolvers.values()) {
|
|
482
|
+
entry.resolve('released');
|
|
483
|
+
}
|
|
484
|
+
this.exitResolvers.clear();
|
|
485
|
+
for (const entry of this.idleResolvers.values()) {
|
|
486
|
+
entry.resolve('exited');
|
|
487
|
+
}
|
|
488
|
+
this.idleResolvers.clear();
|
|
489
|
+
}
|
|
490
|
+
// ── Private helpers ─────────────────────────────────────────────────────
|
|
491
|
+
ensureAgentHandle(name, runtime = 'pty', channels = []) {
|
|
492
|
+
const existing = this.knownAgents.get(name);
|
|
493
|
+
if (existing) {
|
|
494
|
+
return existing;
|
|
495
|
+
}
|
|
496
|
+
const agent = this.makeAgent(name, runtime, channels);
|
|
497
|
+
this.knownAgents.set(name, agent);
|
|
498
|
+
return agent;
|
|
499
|
+
}
|
|
500
|
+
updateDeliveryState(eventId, to, status, updatedAt) {
|
|
501
|
+
this.deliveryStates.set(eventId, { eventId, to, status, updatedAt });
|
|
502
|
+
}
|
|
503
|
+
resolveEventTimestamp(candidate) {
|
|
504
|
+
return typeof candidate === 'number' ? candidate : Date.now();
|
|
505
|
+
}
|
|
506
|
+
/** Resolve a target to a channel name. If `to` is `#channel`, use that
|
|
507
|
+
* channel. If it's a known agent name, use the agent's first channel.
|
|
508
|
+
* Otherwise fall back to the relay's default channel. */
|
|
509
|
+
resolveChannel(to) {
|
|
510
|
+
if (to.startsWith('#'))
|
|
511
|
+
return to.slice(1);
|
|
512
|
+
const agent = this.knownAgents.get(to);
|
|
513
|
+
if (agent && agent.channels.length > 0)
|
|
514
|
+
return agent.channels[0];
|
|
515
|
+
return this.defaultChannels[0];
|
|
516
|
+
}
|
|
517
|
+
inferOutputMode(callback) {
|
|
518
|
+
const source = callback.toString().trim().replace(/\s+/g, ' ');
|
|
519
|
+
if (source.startsWith('({') || source.startsWith('async ({') || source.startsWith('function ({')) {
|
|
520
|
+
return 'structured';
|
|
521
|
+
}
|
|
522
|
+
return 'chunk';
|
|
523
|
+
}
|
|
524
|
+
dispatchOutput(name, stream, chunk) {
|
|
525
|
+
const listeners = this.outputListeners.get(name);
|
|
526
|
+
if (!listeners)
|
|
527
|
+
return;
|
|
528
|
+
for (const listener of listeners) {
|
|
529
|
+
if (listener.mode === 'structured') {
|
|
530
|
+
listener.callback({ stream, chunk });
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
listener.callback(chunk);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async ensureStarted() {
|
|
538
|
+
if (this.client)
|
|
539
|
+
return this.client;
|
|
540
|
+
if (this.startPromise)
|
|
541
|
+
return this.startPromise;
|
|
542
|
+
this.startPromise = AgentRelayClient.start(this.clientOptions)
|
|
543
|
+
.then((c) => {
|
|
544
|
+
this.client = c;
|
|
545
|
+
this.startPromise = undefined;
|
|
546
|
+
this.wireEvents(c);
|
|
547
|
+
return c;
|
|
548
|
+
})
|
|
549
|
+
.catch((err) => {
|
|
550
|
+
this.startPromise = undefined;
|
|
551
|
+
throw err;
|
|
552
|
+
});
|
|
553
|
+
return this.startPromise;
|
|
554
|
+
}
|
|
555
|
+
wireEvents(client) {
|
|
556
|
+
// eslint-disable-next-line complexity
|
|
557
|
+
this.unsubEvent = client.onEvent((event) => {
|
|
558
|
+
switch (event.kind) {
|
|
559
|
+
case 'relay_inbound': {
|
|
560
|
+
if (this.knownAgents.has(event.from)) {
|
|
561
|
+
this.messageReadyAgents.add(event.from);
|
|
562
|
+
this.exitedAgents.delete(event.from);
|
|
563
|
+
}
|
|
564
|
+
const msg = {
|
|
565
|
+
eventId: event.event_id,
|
|
566
|
+
from: event.from,
|
|
567
|
+
to: event.target,
|
|
568
|
+
text: event.body,
|
|
569
|
+
threadId: event.thread_id,
|
|
570
|
+
};
|
|
571
|
+
this.onMessageReceived?.(msg);
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
case 'agent_spawned': {
|
|
575
|
+
const agent = this.ensureAgentHandle(event.name, event.runtime);
|
|
576
|
+
this.readyAgents.delete(event.name);
|
|
577
|
+
this.messageReadyAgents.delete(event.name);
|
|
578
|
+
this.exitedAgents.delete(event.name);
|
|
579
|
+
this.idleAgents.delete(event.name);
|
|
580
|
+
this.onAgentSpawned?.(agent);
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
case 'agent_released': {
|
|
584
|
+
const agent = this.knownAgents.get(event.name) ?? this.ensureAgentHandle(event.name, 'pty', []);
|
|
585
|
+
this.exitedAgents.add(event.name);
|
|
586
|
+
this.readyAgents.delete(event.name);
|
|
587
|
+
this.messageReadyAgents.delete(event.name);
|
|
588
|
+
this.idleAgents.delete(event.name);
|
|
589
|
+
this.onAgentReleased?.(agent);
|
|
590
|
+
this.knownAgents.delete(event.name);
|
|
591
|
+
this.outputListeners.delete(event.name);
|
|
592
|
+
this.exitResolvers.get(event.name)?.resolve('released');
|
|
593
|
+
this.exitResolvers.delete(event.name);
|
|
594
|
+
this.idleResolvers.get(event.name)?.resolve('exited');
|
|
595
|
+
this.idleResolvers.delete(event.name);
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
case 'agent_exited': {
|
|
599
|
+
const agent = this.knownAgents.get(event.name) ?? this.ensureAgentHandle(event.name, 'pty', []);
|
|
600
|
+
this.exitedAgents.add(event.name);
|
|
601
|
+
this.readyAgents.delete(event.name);
|
|
602
|
+
this.messageReadyAgents.delete(event.name);
|
|
603
|
+
this.idleAgents.delete(event.name);
|
|
604
|
+
// Populate exit info before firing the hook
|
|
605
|
+
agent.exitCode = event.code;
|
|
606
|
+
agent.exitSignal = event.signal;
|
|
607
|
+
this.onAgentExited?.(agent);
|
|
608
|
+
this.knownAgents.delete(event.name);
|
|
609
|
+
this.outputListeners.delete(event.name);
|
|
610
|
+
this.exitResolvers.get(event.name)?.resolve('exited');
|
|
611
|
+
this.exitResolvers.delete(event.name);
|
|
612
|
+
this.idleResolvers.get(event.name)?.resolve('exited');
|
|
613
|
+
this.idleResolvers.delete(event.name);
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
case 'agent_exit': {
|
|
617
|
+
const agent = this.knownAgents.get(event.name) ?? this.ensureAgentHandle(event.name, 'pty', []);
|
|
618
|
+
agent.exitReason = event.reason;
|
|
619
|
+
this.onAgentExitRequested?.({ name: event.name, reason: event.reason });
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
case 'worker_ready': {
|
|
623
|
+
const agent = this.ensureAgentHandle(event.name, event.runtime);
|
|
624
|
+
this.readyAgents.add(event.name);
|
|
625
|
+
this.exitedAgents.delete(event.name);
|
|
626
|
+
this.idleAgents.delete(event.name);
|
|
627
|
+
this.onAgentReady?.(agent);
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
case 'delivery_queued': {
|
|
631
|
+
this.updateDeliveryState(event.event_id, event.name, 'queued', this.resolveEventTimestamp(event.timestamp));
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
case 'delivery_injected': {
|
|
635
|
+
this.updateDeliveryState(event.event_id, event.name, 'injected', this.resolveEventTimestamp(event.timestamp));
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
case 'delivery_active': {
|
|
639
|
+
this.updateDeliveryState(event.event_id, event.name, 'active', this.resolveEventTimestamp());
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
case 'delivery_verified': {
|
|
643
|
+
this.updateDeliveryState(event.event_id, event.name, 'verified', this.resolveEventTimestamp());
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
case 'delivery_failed': {
|
|
647
|
+
this.updateDeliveryState(event.event_id, event.name, 'failed', this.resolveEventTimestamp());
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
case 'worker_stream': {
|
|
651
|
+
// Agent producing output is no longer idle
|
|
652
|
+
this.idleAgents.delete(event.name);
|
|
653
|
+
this.onWorkerOutput?.({
|
|
654
|
+
name: event.name,
|
|
655
|
+
stream: event.stream,
|
|
656
|
+
chunk: event.chunk,
|
|
657
|
+
});
|
|
658
|
+
// Dispatch to per-agent output listeners
|
|
659
|
+
this.dispatchOutput(event.name, event.stream, event.chunk);
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
case 'agent_idle': {
|
|
663
|
+
this.idleAgents.add(event.name);
|
|
664
|
+
this.onAgentIdle?.({
|
|
665
|
+
name: event.name,
|
|
666
|
+
idleSecs: event.idle_secs,
|
|
667
|
+
});
|
|
668
|
+
// Resolve idle waiters
|
|
669
|
+
this.idleResolvers.get(event.name)?.resolve('idle');
|
|
670
|
+
this.idleResolvers.delete(event.name);
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (event.kind.startsWith('delivery_')) {
|
|
675
|
+
this.onDeliveryUpdate?.(event);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
makeAgent(name, runtime, channels) {
|
|
680
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
681
|
+
const relay = this;
|
|
682
|
+
return {
|
|
683
|
+
name,
|
|
684
|
+
runtime,
|
|
685
|
+
channels,
|
|
686
|
+
get status() {
|
|
687
|
+
if (relay.exitedAgents.has(name))
|
|
688
|
+
return 'exited';
|
|
689
|
+
if (relay.idleAgents.has(name))
|
|
690
|
+
return 'idle';
|
|
691
|
+
if (relay.readyAgents.has(name))
|
|
692
|
+
return 'ready';
|
|
693
|
+
return 'spawning';
|
|
694
|
+
},
|
|
695
|
+
exitCode: undefined,
|
|
696
|
+
exitSignal: undefined,
|
|
697
|
+
async release(reason) {
|
|
698
|
+
const client = await relay.ensureStarted();
|
|
699
|
+
await client.release(name, reason);
|
|
700
|
+
},
|
|
701
|
+
async waitForReady(timeoutMs = 30_000) {
|
|
702
|
+
await relay.waitForAgentReady(name, timeoutMs);
|
|
703
|
+
},
|
|
704
|
+
waitForExit(timeoutMs) {
|
|
705
|
+
return new Promise((resolve) => {
|
|
706
|
+
// If already gone, resolve immediately
|
|
707
|
+
if (!relay.knownAgents.has(name)) {
|
|
708
|
+
resolve('exited');
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
// Non-blocking poll: timeoutMs === 0 means "check now, return immediately"
|
|
712
|
+
if (timeoutMs === 0) {
|
|
713
|
+
resolve('timeout');
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
let timer;
|
|
717
|
+
const token = ++relay.exitResolverSeq;
|
|
718
|
+
relay.exitResolvers.set(name, {
|
|
719
|
+
resolve(reason) {
|
|
720
|
+
if (timer)
|
|
721
|
+
clearTimeout(timer);
|
|
722
|
+
resolve(reason);
|
|
723
|
+
},
|
|
724
|
+
token,
|
|
725
|
+
});
|
|
726
|
+
if (timeoutMs !== undefined) {
|
|
727
|
+
timer = setTimeout(() => {
|
|
728
|
+
// Only delete if this is still our resolver (not one from a later call)
|
|
729
|
+
const current = relay.exitResolvers.get(name);
|
|
730
|
+
if (current?.token === token) {
|
|
731
|
+
relay.exitResolvers.delete(name);
|
|
732
|
+
}
|
|
733
|
+
resolve('timeout');
|
|
734
|
+
}, timeoutMs);
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
},
|
|
738
|
+
waitForIdle(timeoutMs) {
|
|
739
|
+
return new Promise((resolve) => {
|
|
740
|
+
if (!relay.knownAgents.has(name)) {
|
|
741
|
+
resolve('exited');
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (timeoutMs === 0) {
|
|
745
|
+
resolve('timeout');
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
let timer;
|
|
749
|
+
const token = ++relay.idleResolverSeq;
|
|
750
|
+
relay.idleResolvers.set(name, {
|
|
751
|
+
resolve(reason) {
|
|
752
|
+
if (timer)
|
|
753
|
+
clearTimeout(timer);
|
|
754
|
+
resolve(reason);
|
|
755
|
+
},
|
|
756
|
+
token,
|
|
757
|
+
});
|
|
758
|
+
if (timeoutMs !== undefined) {
|
|
759
|
+
timer = setTimeout(() => {
|
|
760
|
+
const current = relay.idleResolvers.get(name);
|
|
761
|
+
if (current?.token === token) {
|
|
762
|
+
relay.idleResolvers.delete(name);
|
|
763
|
+
}
|
|
764
|
+
resolve('timeout');
|
|
765
|
+
}, timeoutMs);
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
},
|
|
769
|
+
async sendMessage(input) {
|
|
770
|
+
const client = await relay.ensureStarted();
|
|
771
|
+
let result;
|
|
772
|
+
try {
|
|
773
|
+
result = await client.sendMessage({
|
|
774
|
+
to: input.to,
|
|
775
|
+
text: input.text,
|
|
776
|
+
from: name,
|
|
777
|
+
threadId: input.threadId,
|
|
778
|
+
priority: input.priority,
|
|
779
|
+
data: input.data,
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
if (isUnsupportedOperation(error)) {
|
|
784
|
+
return buildUnsupportedOperationMessage(name, input);
|
|
785
|
+
}
|
|
786
|
+
throw error;
|
|
787
|
+
}
|
|
788
|
+
if (result?.event_id === 'unsupported_operation') {
|
|
789
|
+
return buildUnsupportedOperationMessage(name, input);
|
|
790
|
+
}
|
|
791
|
+
const eventId = result?.event_id ?? randomBytes(8).toString('hex');
|
|
792
|
+
const msg = {
|
|
793
|
+
eventId,
|
|
794
|
+
from: name,
|
|
795
|
+
to: input.to,
|
|
796
|
+
text: input.text,
|
|
797
|
+
threadId: input.threadId,
|
|
798
|
+
data: input.data,
|
|
799
|
+
};
|
|
800
|
+
relay.onMessageSent?.(msg);
|
|
801
|
+
return msg;
|
|
802
|
+
},
|
|
803
|
+
onOutput(callback) {
|
|
804
|
+
let listeners = relay.outputListeners.get(name);
|
|
805
|
+
if (!listeners) {
|
|
806
|
+
listeners = new Set();
|
|
807
|
+
relay.outputListeners.set(name, listeners);
|
|
808
|
+
}
|
|
809
|
+
const listener = {
|
|
810
|
+
callback,
|
|
811
|
+
mode: relay.inferOutputMode(callback),
|
|
812
|
+
};
|
|
813
|
+
listeners.add(listener);
|
|
814
|
+
return () => {
|
|
815
|
+
listeners.delete(listener);
|
|
816
|
+
if (listeners.size === 0) {
|
|
817
|
+
relay.outputListeners.delete(name);
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
},
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
createSpawner(cli, defaultName, runtime) {
|
|
824
|
+
return {
|
|
825
|
+
spawn: async (options) => {
|
|
826
|
+
const client = await this.ensureStarted();
|
|
827
|
+
const name = options?.name ?? defaultName;
|
|
828
|
+
const channels = options?.channels ?? ['general'];
|
|
829
|
+
const args = options?.args ?? [];
|
|
830
|
+
const task = options?.task;
|
|
831
|
+
let result;
|
|
832
|
+
if (runtime === 'headless_claude') {
|
|
833
|
+
result = await client.spawnHeadlessClaude({ name, args, channels, task });
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
result = await client.spawnPty({
|
|
837
|
+
name,
|
|
838
|
+
cli,
|
|
839
|
+
args,
|
|
840
|
+
channels,
|
|
841
|
+
task,
|
|
842
|
+
model: options?.model,
|
|
843
|
+
cwd: options?.cwd,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
const agent = this.makeAgent(result.name, result.runtime, channels);
|
|
847
|
+
this.knownAgents.set(agent.name, agent);
|
|
848
|
+
return agent;
|
|
849
|
+
},
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
//# sourceMappingURL=relay.js.map
|