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