@forwardimpact/libeval 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/fit-eval.js +92 -44
- package/package.json +3 -2
- package/src/agent-runner.js +29 -40
- package/src/commands/facilitate.js +109 -0
- package/src/commands/run.js +17 -1
- package/src/facilitator.js +492 -0
- package/src/index.js +15 -2
- package/src/message-bus.js +100 -0
- package/src/orchestration-toolkit.js +209 -0
- package/src/sequence-counter.js +17 -0
- package/src/supervisor.js +128 -210
- package/src/tee-writer.js +20 -26
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facilitator — orchestrates multi-agent concurrent sessions with tool-based
|
|
3
|
+
* communication. Manages N agent sessions and one facilitator LLM session,
|
|
4
|
+
* communicating through OrchestrationToolkit primitives and the MessageBus.
|
|
5
|
+
*
|
|
6
|
+
* Follows OO+DI: constructor injection, factory function, tests bypass factory.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Writable } from "node:stream";
|
|
10
|
+
import { createAgentRunner } from "./agent-runner.js";
|
|
11
|
+
import { SequenceCounter } from "./sequence-counter.js";
|
|
12
|
+
import { createMessageBus } from "./message-bus.js";
|
|
13
|
+
import {
|
|
14
|
+
createOrchestrationContext,
|
|
15
|
+
createFacilitatorToolServer,
|
|
16
|
+
createFacilitatedAgentToolServer,
|
|
17
|
+
} from "./orchestration-toolkit.js";
|
|
18
|
+
|
|
19
|
+
/** System prompt appended for the facilitator runner. */
|
|
20
|
+
export const FACILITATOR_SYSTEM_PROMPT =
|
|
21
|
+
"You coordinate multiple agents working on a shared task. Use Tell to " +
|
|
22
|
+
"assign work to individual agents. Use Share to broadcast to all. Use " +
|
|
23
|
+
"Redirect to interrupt and correct agents. Use RollCall to see who is " +
|
|
24
|
+
"available. Use Conclude with a summary when the task is done. Agents " +
|
|
25
|
+
"communicate with you via Share and may Ask you questions directly.";
|
|
26
|
+
|
|
27
|
+
/** System prompt appended for facilitated agent runners. */
|
|
28
|
+
export const FACILITATED_AGENT_SYSTEM_PROMPT =
|
|
29
|
+
"You are one of several agents working on a shared task under a " +
|
|
30
|
+
"facilitator's coordination. Use Share to broadcast findings. Use Tell " +
|
|
31
|
+
"to message a specific participant. Use Ask to ask the facilitator a " +
|
|
32
|
+
"question (you will block until answered). Use RollCall to see who " +
|
|
33
|
+
"else is working. The facilitator may Redirect you with new instructions " +
|
|
34
|
+
"— treat redirections as authoritative.";
|
|
35
|
+
|
|
36
|
+
function createAsyncQueue() {
|
|
37
|
+
const items = [];
|
|
38
|
+
let waiter = null;
|
|
39
|
+
let closed = false;
|
|
40
|
+
return {
|
|
41
|
+
enqueue(item) {
|
|
42
|
+
items.push(item);
|
|
43
|
+
if (waiter) {
|
|
44
|
+
waiter();
|
|
45
|
+
waiter = null;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
async dequeue() {
|
|
49
|
+
if (items.length > 0) return items.shift();
|
|
50
|
+
if (closed) return null;
|
|
51
|
+
await new Promise((resolve) => {
|
|
52
|
+
waiter = resolve;
|
|
53
|
+
});
|
|
54
|
+
return items.length > 0 ? items.shift() : null;
|
|
55
|
+
},
|
|
56
|
+
close() {
|
|
57
|
+
closed = true;
|
|
58
|
+
if (waiter) {
|
|
59
|
+
waiter();
|
|
60
|
+
waiter = null;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class Facilitator {
|
|
67
|
+
/**
|
|
68
|
+
* @param {object} deps
|
|
69
|
+
* @param {import("./agent-runner.js").AgentRunner} deps.facilitatorRunner
|
|
70
|
+
* @param {Array<{name: string, role: string, runner: import("./agent-runner.js").AgentRunner}>} deps.agents
|
|
71
|
+
* @param {import("./message-bus.js").MessageBus} deps.messageBus
|
|
72
|
+
* @param {import("stream").Writable} deps.output
|
|
73
|
+
* @param {number} [deps.maxTurns]
|
|
74
|
+
* @param {object} [deps.ctx]
|
|
75
|
+
* @param {object} [deps.eventQueue]
|
|
76
|
+
*/
|
|
77
|
+
constructor({
|
|
78
|
+
facilitatorRunner,
|
|
79
|
+
agents,
|
|
80
|
+
messageBus,
|
|
81
|
+
output,
|
|
82
|
+
maxTurns,
|
|
83
|
+
ctx,
|
|
84
|
+
eventQueue,
|
|
85
|
+
}) {
|
|
86
|
+
this.facilitatorRunner = facilitatorRunner;
|
|
87
|
+
this.agents = agents;
|
|
88
|
+
this.messageBus = messageBus;
|
|
89
|
+
this.output = output;
|
|
90
|
+
this.maxTurns = maxTurns ?? 20;
|
|
91
|
+
this.ctx = ctx ?? createOrchestrationContext();
|
|
92
|
+
this.counter = new SequenceCounter();
|
|
93
|
+
this.eventQueue = eventQueue ?? createAsyncQueue();
|
|
94
|
+
this.facilitatorTurns = 0;
|
|
95
|
+
|
|
96
|
+
let resolve;
|
|
97
|
+
const promise = new Promise((r) => {
|
|
98
|
+
resolve = r;
|
|
99
|
+
});
|
|
100
|
+
this.concludePromise = promise;
|
|
101
|
+
this.concludeResolve = resolve;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Run the full facilitated session.
|
|
106
|
+
* @param {string} task
|
|
107
|
+
* @returns {Promise<{success: boolean, turns: number}>}
|
|
108
|
+
*/
|
|
109
|
+
async run(task) {
|
|
110
|
+
this.emitOrchestratorEvent({ type: "session_start" });
|
|
111
|
+
|
|
112
|
+
// Turn 0: facilitator receives the task
|
|
113
|
+
this.facilitatorTurns++;
|
|
114
|
+
await this.facilitatorRunner.run(task);
|
|
115
|
+
|
|
116
|
+
if (this.ctx.concluded) {
|
|
117
|
+
this.concludeResolve();
|
|
118
|
+
this.emitSummary({ success: true, turns: 0, summary: this.ctx.summary });
|
|
119
|
+
return { success: true, turns: 0 };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Handle redirect after turn 0
|
|
123
|
+
await this.#processRedirect();
|
|
124
|
+
|
|
125
|
+
// Abort agents promptly when Conclude is called
|
|
126
|
+
this.concludePromise.then(() => {
|
|
127
|
+
for (const agent of this.agents) {
|
|
128
|
+
agent.runner.currentAbortController?.abort();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Launch all loops concurrently
|
|
133
|
+
const agentPromises = this.agents.map((a) => this.#runAgent(a));
|
|
134
|
+
const facilitatorPromise = this.#facilitatorLoop();
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
await Promise.all([...agentPromises, facilitatorPromise]);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
for (const agent of this.agents) {
|
|
140
|
+
agent.runner.currentAbortController?.abort();
|
|
141
|
+
}
|
|
142
|
+
this.facilitatorRunner.currentAbortController?.abort();
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const result = {
|
|
147
|
+
success: this.ctx.concluded,
|
|
148
|
+
turns: this.facilitatorTurns,
|
|
149
|
+
};
|
|
150
|
+
this.emitSummary({
|
|
151
|
+
success: result.success,
|
|
152
|
+
turns: result.turns,
|
|
153
|
+
summary: this.ctx.summary,
|
|
154
|
+
});
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Agent outer loop — waits for messages, runs/resumes the agent.
|
|
160
|
+
* @param {{name: string, role: string, runner: import("./agent-runner.js").AgentRunner}} agent
|
|
161
|
+
*/
|
|
162
|
+
async #runAgent(agent) {
|
|
163
|
+
// Wait for first message (lazy start)
|
|
164
|
+
await Promise.race([
|
|
165
|
+
this.messageBus.waitForMessages(agent.name),
|
|
166
|
+
this.concludePromise,
|
|
167
|
+
]);
|
|
168
|
+
if (this.ctx.concluded) return;
|
|
169
|
+
|
|
170
|
+
let messages = this.messageBus.drain(agent.name);
|
|
171
|
+
if (messages.length === 0) return;
|
|
172
|
+
|
|
173
|
+
this.emitOrchestratorEvent({
|
|
174
|
+
type: "agent_start",
|
|
175
|
+
agent: agent.name,
|
|
176
|
+
});
|
|
177
|
+
await agent.runner.run(this.#formatMessages(messages));
|
|
178
|
+
if (this.ctx.concluded) return;
|
|
179
|
+
this.eventQueue.enqueue({
|
|
180
|
+
type: "lifecycle",
|
|
181
|
+
agent: agent.name,
|
|
182
|
+
status: "turn_complete",
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Loop: check for new messages, resume if any
|
|
186
|
+
while (!this.ctx.concluded) {
|
|
187
|
+
messages = this.messageBus.drain(agent.name);
|
|
188
|
+
if (messages.length === 0) {
|
|
189
|
+
await Promise.race([
|
|
190
|
+
this.messageBus.waitForMessages(agent.name),
|
|
191
|
+
this.concludePromise,
|
|
192
|
+
]);
|
|
193
|
+
if (this.ctx.concluded) break;
|
|
194
|
+
messages = this.messageBus.drain(agent.name);
|
|
195
|
+
if (messages.length === 0) break;
|
|
196
|
+
}
|
|
197
|
+
await agent.runner.resume(this.#formatMessages(messages));
|
|
198
|
+
if (this.ctx.concluded) break;
|
|
199
|
+
this.eventQueue.enqueue({
|
|
200
|
+
type: "lifecycle",
|
|
201
|
+
agent: agent.name,
|
|
202
|
+
status: "turn_complete",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Facilitator event loop — only runs when input arrives.
|
|
209
|
+
*/
|
|
210
|
+
async #facilitatorLoop() {
|
|
211
|
+
while (!this.ctx.concluded) {
|
|
212
|
+
const event = await this.eventQueue.dequeue();
|
|
213
|
+
if (this.ctx.concluded || event === null) break;
|
|
214
|
+
await this.#handleEvent(event);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async #handleEvent(event) {
|
|
219
|
+
switch (event.type) {
|
|
220
|
+
case "ask": {
|
|
221
|
+
if (this.ctx.concluded) {
|
|
222
|
+
event.resolve("Session has concluded.");
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
this.facilitatorTurns++;
|
|
226
|
+
this.emitOrchestratorEvent({
|
|
227
|
+
type: "ask_received",
|
|
228
|
+
from: event.from,
|
|
229
|
+
});
|
|
230
|
+
await this.facilitatorRunner.resume(
|
|
231
|
+
`Agent "${event.from}" asks: "${event.question}"\nAnswer the question.`,
|
|
232
|
+
);
|
|
233
|
+
const answer = this.extractLastText(
|
|
234
|
+
this.facilitatorRunner,
|
|
235
|
+
"No answer.",
|
|
236
|
+
);
|
|
237
|
+
this.emitOrchestratorEvent({
|
|
238
|
+
type: "ask_answered",
|
|
239
|
+
from: event.from,
|
|
240
|
+
});
|
|
241
|
+
event.resolve(answer);
|
|
242
|
+
await this.#processRedirect();
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case "messages": {
|
|
246
|
+
const msgs = this.messageBus.drain("facilitator");
|
|
247
|
+
if (msgs.length === 0) break;
|
|
248
|
+
this.facilitatorTurns++;
|
|
249
|
+
await this.facilitatorRunner.resume(this.#formatMessages(msgs));
|
|
250
|
+
await this.#processRedirect();
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case "lifecycle": {
|
|
254
|
+
// Check for pending shared messages for the facilitator
|
|
255
|
+
const msgs = this.messageBus.drain("facilitator");
|
|
256
|
+
if (msgs.length === 0) break;
|
|
257
|
+
this.facilitatorTurns++;
|
|
258
|
+
await this.facilitatorRunner.resume(this.#formatMessages(msgs));
|
|
259
|
+
await this.#processRedirect();
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (this.ctx.concluded) {
|
|
265
|
+
this.concludeResolve();
|
|
266
|
+
this.eventQueue.close();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Process a pending redirect after a facilitator turn.
|
|
272
|
+
*/
|
|
273
|
+
async #processRedirect() {
|
|
274
|
+
if (!this.ctx.redirect) return;
|
|
275
|
+
const redirect = this.ctx.redirect;
|
|
276
|
+
this.ctx.redirect = null;
|
|
277
|
+
|
|
278
|
+
this.emitOrchestratorEvent({
|
|
279
|
+
type: "redirect",
|
|
280
|
+
to: redirect.to,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (redirect.to === "all") {
|
|
284
|
+
// Abort all agents and deliver redirect via broadcast
|
|
285
|
+
for (const agent of this.agents) {
|
|
286
|
+
agent.runner.currentAbortController?.abort();
|
|
287
|
+
}
|
|
288
|
+
this.messageBus.share("facilitator", redirect.message);
|
|
289
|
+
} else if (redirect.to) {
|
|
290
|
+
// Abort specific agent and deliver via direct message
|
|
291
|
+
const target = this.agents.find((a) => a.name === redirect.to);
|
|
292
|
+
if (target) {
|
|
293
|
+
target.runner.currentAbortController?.abort();
|
|
294
|
+
}
|
|
295
|
+
this.messageBus.tell("facilitator", redirect.to, redirect.message);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Format messages for an agent prompt.
|
|
301
|
+
* @param {Array<{from: string, text: string, direct: boolean}>} messages
|
|
302
|
+
* @returns {string}
|
|
303
|
+
*/
|
|
304
|
+
#formatMessages(messages) {
|
|
305
|
+
return messages
|
|
306
|
+
.map((m) => {
|
|
307
|
+
const tag = m.direct ? "[direct]" : "[shared]";
|
|
308
|
+
return `${tag} ${m.from}: ${m.text}`;
|
|
309
|
+
})
|
|
310
|
+
.join("\n");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Extract the last assistant text block from a runner's buffer.
|
|
315
|
+
* @param {import("./agent-runner.js").AgentRunner} runner
|
|
316
|
+
* @param {string} fallback
|
|
317
|
+
* @returns {string}
|
|
318
|
+
*/
|
|
319
|
+
extractLastText(runner, fallback) {
|
|
320
|
+
const lines = runner.buffer;
|
|
321
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
322
|
+
const event = JSON.parse(lines[i]);
|
|
323
|
+
if (event.type !== "assistant") continue;
|
|
324
|
+
const content = event.message?.content ?? event.content;
|
|
325
|
+
if (!Array.isArray(content)) continue;
|
|
326
|
+
for (let j = content.length - 1; j >= 0; j--) {
|
|
327
|
+
if (content[j].type === "text" && content[j].text) {
|
|
328
|
+
return content[j].text;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return fallback;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Emit a single NDJSON line tagged with source and seq.
|
|
337
|
+
* @param {string} source - Participant name
|
|
338
|
+
* @param {string} line - Raw NDJSON line
|
|
339
|
+
*/
|
|
340
|
+
emitLine(source, line) {
|
|
341
|
+
const event = JSON.parse(line);
|
|
342
|
+
this.output.write(
|
|
343
|
+
JSON.stringify({
|
|
344
|
+
source,
|
|
345
|
+
seq: this.counter.next(),
|
|
346
|
+
event,
|
|
347
|
+
}) + "\n",
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @param {{type: string}} event
|
|
353
|
+
*/
|
|
354
|
+
emitOrchestratorEvent(event) {
|
|
355
|
+
this.output.write(
|
|
356
|
+
JSON.stringify({
|
|
357
|
+
source: "orchestrator",
|
|
358
|
+
seq: this.counter.next(),
|
|
359
|
+
event,
|
|
360
|
+
}) + "\n",
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* @param {{success: boolean, turns: number, summary?: string}} result
|
|
366
|
+
*/
|
|
367
|
+
emitSummary(result) {
|
|
368
|
+
this.output.write(
|
|
369
|
+
JSON.stringify({
|
|
370
|
+
source: "orchestrator",
|
|
371
|
+
seq: this.counter.next(),
|
|
372
|
+
event: {
|
|
373
|
+
type: "summary",
|
|
374
|
+
success: result.success,
|
|
375
|
+
turns: result.turns,
|
|
376
|
+
...(result.summary && { summary: result.summary }),
|
|
377
|
+
},
|
|
378
|
+
}) + "\n",
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const devNull = new Writable({
|
|
384
|
+
write(_chunk, _enc, cb) {
|
|
385
|
+
cb();
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Factory function — wires all participants with MCP servers.
|
|
391
|
+
* @param {object} deps
|
|
392
|
+
* @param {string} deps.facilitatorCwd
|
|
393
|
+
* @param {Array<{name: string, role: string, cwd?: string, maxTurns?: number, allowedTools?: string[], agentProfile?: string}>} deps.agentConfigs
|
|
394
|
+
* @param {function} deps.query
|
|
395
|
+
* @param {import("stream").Writable} deps.output
|
|
396
|
+
* @param {string} [deps.model]
|
|
397
|
+
* @param {number} [deps.maxTurns]
|
|
398
|
+
* @param {string} [deps.facilitatorProfile]
|
|
399
|
+
* @returns {Facilitator}
|
|
400
|
+
*/
|
|
401
|
+
export function createFacilitator({
|
|
402
|
+
facilitatorCwd,
|
|
403
|
+
agentConfigs,
|
|
404
|
+
query,
|
|
405
|
+
output,
|
|
406
|
+
model,
|
|
407
|
+
maxTurns,
|
|
408
|
+
facilitatorProfile,
|
|
409
|
+
}) {
|
|
410
|
+
const ctx = createOrchestrationContext();
|
|
411
|
+
const messageBus = createMessageBus({
|
|
412
|
+
participants: ["facilitator", ...agentConfigs.map((a) => a.name)],
|
|
413
|
+
});
|
|
414
|
+
ctx.messageBus = messageBus;
|
|
415
|
+
ctx.participants = [
|
|
416
|
+
{ name: "facilitator", role: "facilitator" },
|
|
417
|
+
...agentConfigs.map((a) => ({ name: a.name, role: a.role })),
|
|
418
|
+
];
|
|
419
|
+
|
|
420
|
+
let facilitator;
|
|
421
|
+
|
|
422
|
+
const eventQueue = createAsyncQueue();
|
|
423
|
+
|
|
424
|
+
const facilitatorServer = createFacilitatorToolServer(ctx);
|
|
425
|
+
|
|
426
|
+
const agents = agentConfigs.map((config) => {
|
|
427
|
+
const agentServer = createFacilitatedAgentToolServer(ctx, {
|
|
428
|
+
from: config.name,
|
|
429
|
+
onAsk: async (question) => {
|
|
430
|
+
let resolve;
|
|
431
|
+
const promise = new Promise((r) => {
|
|
432
|
+
resolve = r;
|
|
433
|
+
});
|
|
434
|
+
eventQueue.enqueue({
|
|
435
|
+
type: "ask",
|
|
436
|
+
from: config.name,
|
|
437
|
+
question,
|
|
438
|
+
resolve,
|
|
439
|
+
});
|
|
440
|
+
return promise;
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const runner = createAgentRunner({
|
|
445
|
+
cwd: config.cwd ?? facilitatorCwd,
|
|
446
|
+
query,
|
|
447
|
+
output: devNull,
|
|
448
|
+
model,
|
|
449
|
+
maxTurns: config.maxTurns ?? 50,
|
|
450
|
+
allowedTools: config.allowedTools,
|
|
451
|
+
onLine: (line) => facilitator.emitLine(config.name, line),
|
|
452
|
+
mcpServers: { orchestration: agentServer },
|
|
453
|
+
settingSources: ["project"],
|
|
454
|
+
agentProfile: config.agentProfile,
|
|
455
|
+
systemPrompt: {
|
|
456
|
+
type: "preset",
|
|
457
|
+
preset: "claude_code",
|
|
458
|
+
append: FACILITATED_AGENT_SYSTEM_PROMPT,
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
return { name: config.name, role: config.role, runner };
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const facilitatorRunner = createAgentRunner({
|
|
466
|
+
cwd: facilitatorCwd,
|
|
467
|
+
query,
|
|
468
|
+
output: devNull,
|
|
469
|
+
model,
|
|
470
|
+
maxTurns: maxTurns ?? 20,
|
|
471
|
+
onLine: (line) => facilitator.emitLine("facilitator", line),
|
|
472
|
+
mcpServers: { orchestration: facilitatorServer },
|
|
473
|
+
settingSources: ["project"],
|
|
474
|
+
agentProfile: facilitatorProfile,
|
|
475
|
+
systemPrompt: {
|
|
476
|
+
type: "preset",
|
|
477
|
+
preset: "claude_code",
|
|
478
|
+
append: FACILITATOR_SYSTEM_PROMPT,
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
facilitator = new Facilitator({
|
|
483
|
+
facilitatorRunner,
|
|
484
|
+
agents,
|
|
485
|
+
messageBus,
|
|
486
|
+
output,
|
|
487
|
+
maxTurns,
|
|
488
|
+
ctx,
|
|
489
|
+
eventQueue,
|
|
490
|
+
});
|
|
491
|
+
return facilitator;
|
|
492
|
+
}
|
package/src/index.js
CHANGED
|
@@ -5,7 +5,20 @@ export {
|
|
|
5
5
|
createSupervisor,
|
|
6
6
|
SUPERVISOR_SYSTEM_PROMPT,
|
|
7
7
|
AGENT_SYSTEM_PROMPT,
|
|
8
|
-
isComplete,
|
|
9
|
-
isIntervention,
|
|
10
8
|
} from "./supervisor.js";
|
|
11
9
|
export { TeeWriter, createTeeWriter } from "./tee-writer.js";
|
|
10
|
+
export { SequenceCounter, createSequenceCounter } from "./sequence-counter.js";
|
|
11
|
+
export {
|
|
12
|
+
createOrchestrationContext,
|
|
13
|
+
createSupervisorToolServer,
|
|
14
|
+
createSupervisedAgentToolServer,
|
|
15
|
+
createFacilitatorToolServer,
|
|
16
|
+
createFacilitatedAgentToolServer,
|
|
17
|
+
} from "./orchestration-toolkit.js";
|
|
18
|
+
export { MessageBus, createMessageBus } from "./message-bus.js";
|
|
19
|
+
export {
|
|
20
|
+
Facilitator,
|
|
21
|
+
createFacilitator,
|
|
22
|
+
FACILITATOR_SYSTEM_PROMPT,
|
|
23
|
+
FACILITATED_AGENT_SYSTEM_PROMPT,
|
|
24
|
+
} from "./facilitator.js";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessageBus — in-memory per-participant message queues for facilitate mode.
|
|
3
|
+
*
|
|
4
|
+
* Follows OO+DI: constructor injection, factory function, tests bypass factory.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class MessageBus {
|
|
8
|
+
/**
|
|
9
|
+
* @param {object} deps
|
|
10
|
+
* @param {string[]} deps.participants - Participant names
|
|
11
|
+
*/
|
|
12
|
+
constructor({ participants }) {
|
|
13
|
+
this.queues = new Map();
|
|
14
|
+
this.waiters = new Map();
|
|
15
|
+
for (const name of participants) {
|
|
16
|
+
this.queues.set(name, []);
|
|
17
|
+
this.waiters.set(name, null);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Broadcast a message to every participant except the sender.
|
|
23
|
+
* @param {string} from - Sender name
|
|
24
|
+
* @param {string} message - Message text
|
|
25
|
+
*/
|
|
26
|
+
share(from, message) {
|
|
27
|
+
this.#assertParticipant(from);
|
|
28
|
+
const msg = { from, text: message, direct: false };
|
|
29
|
+
for (const [name, queue] of this.queues) {
|
|
30
|
+
if (name === from) continue;
|
|
31
|
+
queue.push(msg);
|
|
32
|
+
this.#resolveWaiter(name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Send a direct message to one participant.
|
|
38
|
+
* @param {string} from - Sender name
|
|
39
|
+
* @param {string} to - Recipient name
|
|
40
|
+
* @param {string} message - Message text
|
|
41
|
+
*/
|
|
42
|
+
tell(from, to, message) {
|
|
43
|
+
this.#assertParticipant(from);
|
|
44
|
+
this.#assertParticipant(to);
|
|
45
|
+
const msg = { from, text: message, direct: true };
|
|
46
|
+
this.queues.get(to).push(msg);
|
|
47
|
+
this.#resolveWaiter(to);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Return and clear pending messages for a participant.
|
|
52
|
+
* @param {string} participant - Participant name
|
|
53
|
+
* @returns {{ from: string, text: string, direct: boolean }[]}
|
|
54
|
+
*/
|
|
55
|
+
drain(participant) {
|
|
56
|
+
this.#assertParticipant(participant);
|
|
57
|
+
const queue = this.queues.get(participant);
|
|
58
|
+
const messages = queue.splice(0);
|
|
59
|
+
return messages;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Return a Promise that resolves when at least one message is pending.
|
|
64
|
+
* Resolves immediately if messages are already queued.
|
|
65
|
+
* @param {string} participant - Participant name
|
|
66
|
+
* @returns {Promise<void>}
|
|
67
|
+
*/
|
|
68
|
+
waitForMessages(participant) {
|
|
69
|
+
this.#assertParticipant(participant);
|
|
70
|
+
if (this.queues.get(participant).length > 0) {
|
|
71
|
+
return Promise.resolve();
|
|
72
|
+
}
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
this.waiters.set(participant, resolve);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#assertParticipant(name) {
|
|
79
|
+
if (!this.queues.has(name)) {
|
|
80
|
+
throw new Error(`Unknown participant: ${name}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#resolveWaiter(name) {
|
|
85
|
+
const waiter = this.waiters.get(name);
|
|
86
|
+
if (waiter) {
|
|
87
|
+
this.waiters.set(name, null);
|
|
88
|
+
waiter();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Factory function.
|
|
95
|
+
* @param {object} deps - Same as MessageBus constructor
|
|
96
|
+
* @returns {MessageBus}
|
|
97
|
+
*/
|
|
98
|
+
export function createMessageBus(deps) {
|
|
99
|
+
return new MessageBus(deps);
|
|
100
|
+
}
|