@forwardimpact/libeval 0.1.43 → 0.1.45
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 +212 -13
- package/bin/fit-benchmark.js +2 -2
- package/bin/fit-eval.js +101 -21
- package/bin/fit-trace.js +14 -0
- package/package.json +1 -1
- package/src/agent-runner.js +45 -181
- package/src/benchmark/runner.js +2 -2
- package/src/commands/benchmark-run.js +1 -1
- package/src/commands/by-discussion.js +84 -0
- package/src/commands/callback.js +104 -0
- package/src/commands/discuss.js +116 -0
- package/src/commands/facilitate.js +2 -2
- package/src/commands/supervise.js +6 -4
- package/src/discuss-tools.js +135 -0
- package/src/discusser.js +315 -0
- package/src/facilitator.js +46 -357
- package/src/index.js +12 -0
- package/src/judge.js +1 -1
- package/src/message-bus.js +27 -81
- package/src/orchestration-loop.js +316 -0
- package/src/orchestration-toolkit.js +272 -303
- package/src/orchestrator-helpers.js +9 -45
- package/src/redaction.js +12 -0
- package/src/render/orchestrator-filter.js +1 -8
- package/src/supervisor.js +79 -465
- package/src/trace-collector.js +4 -0
package/src/facilitator.js
CHANGED
|
@@ -1,379 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Facilitator —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Facilitator — facilitate-mode wrapper around `OrchestrationLoop`. The
|
|
3
|
+
* lead participant is named "facilitator" and ends the session via the
|
|
4
|
+
* `Conclude` tool. The within-run turn loop lives in
|
|
5
|
+
* `orchestration-loop.js`; this file owns only the facilitate-mode
|
|
6
|
+
* specifics (lead role name, system prompts, tool wiring, factory).
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { Writable } from "node:stream";
|
|
9
10
|
import { resolve } from "node:path";
|
|
10
11
|
import { createAgentRunner } from "./agent-runner.js";
|
|
11
12
|
import { composeProfilePrompt } from "./profile-prompt.js";
|
|
12
|
-
import { SequenceCounter } from "./sequence-counter.js";
|
|
13
13
|
import { createMessageBus } from "./message-bus.js";
|
|
14
14
|
import {
|
|
15
15
|
createOrchestrationContext,
|
|
16
16
|
createFacilitatorToolServer,
|
|
17
17
|
createFacilitatedAgentToolServer,
|
|
18
|
-
checkPendingAsk,
|
|
19
18
|
} from "./orchestration-toolkit.js";
|
|
20
|
-
import {
|
|
19
|
+
import { OrchestrationLoop } from "./orchestration-loop.js";
|
|
21
20
|
|
|
22
21
|
/** System prompt appended for the facilitator runner. */
|
|
23
22
|
export const FACILITATOR_SYSTEM_PROMPT =
|
|
24
|
-
"You coordinate multiple participants
|
|
25
|
-
"Ask sends a question
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"RollCall
|
|
29
|
-
"Conclude ends the session with a verdict ('success' or 'failure') and a summary
|
|
30
|
-
"
|
|
23
|
+
"You coordinate multiple participants via these tools: " +
|
|
24
|
+
"Ask sends a question and returns immediately with {askIds:[N,…]}. The reply arrives on a later turn as `[answer#N] <participant>: <text>` in your inbox — between turns you can plan, reflect, or send more Asks while participants work in parallel. End your turn with text after you've asked everything you intend to; the orchestrator wakes you again as soon as a reply (or any message) lands. " +
|
|
25
|
+
"Answer replies to an ask a participant addressed to you (you'll see it tagged `[ask#N] <participant>: …` in your inbox). Quote askId from the [ask#N] tag; omit it and the handler auto-picks the only pending ask or routes your message as an Announce. " +
|
|
26
|
+
"Announce delivers a message with no reply obligation. " +
|
|
27
|
+
"RollCall returns the participant roster. " +
|
|
28
|
+
"Conclude ends the session with a verdict ('success' or 'failure') and a summary. " +
|
|
29
|
+
"Multiple Ask / Announce calls in one assistant turn dispatch in parallel — issue them as parallel tool_use blocks rather than sending the same question both broadcast and individually. " +
|
|
30
|
+
"You MUST end every session with Conclude — never end a turn with only text *after* every Ask round has resolved. " +
|
|
31
|
+
"If you can answer the task yourself, still call Conclude with verdict='success' and the answer as the summary.";
|
|
31
32
|
|
|
32
33
|
/** System prompt appended for facilitated agent runners. */
|
|
33
34
|
export const FACILITATED_AGENT_SYSTEM_PROMPT =
|
|
34
35
|
"You participate in a coordinated session. " +
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
36
|
+
"Each question you receive carries an [ask#N] header — quote that N back as the askId field on Answer so the reply pairs with the right question. " +
|
|
37
|
+
"Answer replies to an ask addressed to you. askId is optional: omit it and the handler auto-picks if exactly one ask is owed to you, otherwise it routes your message as an Announce. " +
|
|
38
|
+
"Ask sends a question to another participant and returns immediately with {askIds:[N]}; the reply arrives on a later turn as `[answer#N] <participant>: <text>` in your inbox. " +
|
|
39
|
+
"Announce broadcasts a message to every other participant — use this for unsolicited remarks or to reply to an Announce. " +
|
|
38
40
|
"RollCall lists participants.";
|
|
39
41
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Facilitate-mode wrapper around `OrchestrationLoop`. The lead is named
|
|
44
|
+
* `"facilitator"`. `facilitatorRunner` getter is a readability shim for
|
|
45
|
+
* tests that read the runner directly.
|
|
46
|
+
*/
|
|
47
|
+
export class Facilitator extends OrchestrationLoop {
|
|
42
48
|
/**
|
|
43
49
|
* @param {object} deps
|
|
44
50
|
* @param {import("./agent-runner.js").AgentRunner} deps.facilitatorRunner
|
|
45
51
|
* @param {Array<{name: string, role: string, runner: import("./agent-runner.js").AgentRunner}>} deps.agents
|
|
46
52
|
* @param {import("./message-bus.js").MessageBus} deps.messageBus
|
|
47
53
|
* @param {import("stream").Writable} deps.output
|
|
48
|
-
* @param {
|
|
49
|
-
* @param {object}
|
|
50
|
-
* @param {
|
|
51
|
-
* @param {string} [deps.taskAmend] - Opaque addendum appended to the task before delivery.
|
|
54
|
+
* @param {object} deps.ctx
|
|
55
|
+
* @param {object} deps.redactor
|
|
56
|
+
* @param {string} [deps.taskAmend]
|
|
52
57
|
*/
|
|
53
|
-
constructor({
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
maxTurns,
|
|
59
|
-
ctx,
|
|
60
|
-
eventQueue,
|
|
61
|
-
taskAmend,
|
|
62
|
-
redactor,
|
|
63
|
-
}) {
|
|
64
|
-
if (!redactor) throw new Error("redactor is required");
|
|
65
|
-
this.redactor = redactor;
|
|
66
|
-
this.facilitatorRunner = facilitatorRunner;
|
|
67
|
-
this.agents = agents;
|
|
68
|
-
this.messageBus = messageBus;
|
|
69
|
-
this.output = output;
|
|
70
|
-
this.maxTurns = maxTurns ?? 20;
|
|
71
|
-
this.ctx = ctx ?? createOrchestrationContext();
|
|
72
|
-
this.counter = new SequenceCounter();
|
|
73
|
-
this.eventQueue = eventQueue ?? createAsyncQueue();
|
|
74
|
-
this.facilitatorTurns = 0;
|
|
75
|
-
this.taskAmend = taskAmend ?? null;
|
|
76
|
-
|
|
77
|
-
let resolve;
|
|
78
|
-
const promise = new Promise((r) => {
|
|
79
|
-
resolve = r;
|
|
80
|
-
});
|
|
81
|
-
this.concludePromise = promise;
|
|
82
|
-
this.concludeResolve = resolve;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Run the full facilitated session.
|
|
87
|
-
* @param {string} task
|
|
88
|
-
* @returns {Promise<{success: boolean, turns: number}>}
|
|
89
|
-
*/
|
|
90
|
-
async run(task) {
|
|
91
|
-
this.emitOrchestratorEvent({ type: "session_start" });
|
|
92
|
-
|
|
93
|
-
const initialTask = this.taskAmend ? `${task}\n\n${this.taskAmend}` : task;
|
|
94
|
-
|
|
95
|
-
// Launch agent loops first — they wait for messages via messageBus.
|
|
96
|
-
// This lets agents process Ask/Announce messages that arrive during
|
|
97
|
-
// the facilitator's initial run, rather than after it completes.
|
|
98
|
-
const agentPromises = this.agents.map((a) => this.#runAgent(a));
|
|
99
|
-
|
|
100
|
-
// Turn 0: facilitator receives the task
|
|
101
|
-
this.facilitatorTurns++;
|
|
102
|
-
await this.facilitatorRunner.run(initialTask);
|
|
103
|
-
|
|
104
|
-
// Handle redirect after turn 0
|
|
105
|
-
await this.#processRedirect();
|
|
106
|
-
|
|
107
|
-
if (this.ctx.concluded) {
|
|
108
|
-
// Facilitator concluded during its initial run. Let agents finish any
|
|
109
|
-
// in-progress work before returning — they may have received Ask/Answer
|
|
110
|
-
// messages and started processing concurrently.
|
|
111
|
-
this.concludeResolve();
|
|
112
|
-
await Promise.allSettled(agentPromises);
|
|
113
|
-
const success = this.ctx.verdict === "success";
|
|
114
|
-
this.emitSummary({
|
|
115
|
-
success,
|
|
116
|
-
verdict: this.ctx.verdict,
|
|
117
|
-
turns: this.facilitatorTurns,
|
|
118
|
-
summary: this.ctx.summary,
|
|
119
|
-
});
|
|
120
|
-
return { success, turns: this.facilitatorTurns };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Abort agents promptly when Conclude is called during the event loop
|
|
124
|
-
this.concludePromise.then(() => {
|
|
125
|
-
for (const agent of this.agents) {
|
|
126
|
-
agent.runner.currentAbortController?.abort();
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// Concurrent phase: facilitator event loop + already-running agent loops
|
|
131
|
-
const facilitatorPromise = this.#facilitatorLoop();
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
await Promise.all([...agentPromises, facilitatorPromise]);
|
|
135
|
-
} catch (err) {
|
|
136
|
-
for (const agent of this.agents) {
|
|
137
|
-
agent.runner.currentAbortController?.abort();
|
|
138
|
-
}
|
|
139
|
-
this.facilitatorRunner.currentAbortController?.abort();
|
|
140
|
-
throw err;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const success = this.ctx.concluded && this.ctx.verdict === "success";
|
|
144
|
-
const result = {
|
|
145
|
-
success,
|
|
146
|
-
turns: this.facilitatorTurns,
|
|
147
|
-
};
|
|
148
|
-
this.emitSummary({
|
|
149
|
-
success,
|
|
150
|
-
verdict: this.ctx.verdict,
|
|
151
|
-
turns: result.turns,
|
|
152
|
-
summary: this.ctx.summary,
|
|
153
|
-
});
|
|
154
|
-
return result;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
#checkAsk(name) {
|
|
158
|
-
return checkPendingAsk({
|
|
159
|
-
ctx: this.ctx,
|
|
160
|
-
messageBus: this.messageBus,
|
|
161
|
-
addresseeName: name,
|
|
58
|
+
constructor(deps) {
|
|
59
|
+
super({
|
|
60
|
+
...deps,
|
|
61
|
+
leadRunner: deps.facilitatorRunner,
|
|
62
|
+
leadName: "facilitator",
|
|
162
63
|
mode: "facilitated",
|
|
163
|
-
emitViolation: (e) => this.emitOrchestratorEvent(e),
|
|
164
64
|
});
|
|
165
65
|
}
|
|
166
66
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const reminders = this.messageBus.drain(agent.name);
|
|
171
|
-
if (reminders.length === 0) return;
|
|
172
|
-
await agent.runner.resume(formatMessages(reminders));
|
|
173
|
-
if (this.ctx.concluded) return;
|
|
174
|
-
this.#checkAsk(agent.name);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Agent outer loop — waits for messages, runs/resumes the agent.
|
|
179
|
-
* @param {{name: string, role: string, runner: import("./agent-runner.js").AgentRunner}} agent
|
|
180
|
-
*/
|
|
181
|
-
async #runAgent(agent) {
|
|
182
|
-
// Wait for first message (lazy start)
|
|
183
|
-
await Promise.race([
|
|
184
|
-
this.messageBus.waitForMessages(agent.name),
|
|
185
|
-
this.concludePromise,
|
|
186
|
-
]);
|
|
187
|
-
if (this.ctx.concluded) return;
|
|
188
|
-
|
|
189
|
-
let messages = this.messageBus.drain(agent.name);
|
|
190
|
-
if (messages.length === 0) return;
|
|
191
|
-
|
|
192
|
-
this.emitOrchestratorEvent({ type: "agent_start", agent: agent.name });
|
|
193
|
-
await agent.runner.run(formatMessages(messages));
|
|
194
|
-
if (await this.#settleAgentTurn(agent)) return;
|
|
195
|
-
|
|
196
|
-
// Loop: check for new messages, resume if any
|
|
197
|
-
while (!this.ctx.concluded) {
|
|
198
|
-
messages = await this.#awaitAgentMessages(agent.name);
|
|
199
|
-
if (messages.length === 0) break;
|
|
200
|
-
await agent.runner.resume(formatMessages(messages));
|
|
201
|
-
if (await this.#settleAgentTurn(agent)) break;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Enforce pending-ask and emit turn_complete. Returns true when the
|
|
207
|
-
* session has concluded and the caller should stop.
|
|
208
|
-
*/
|
|
209
|
-
async #settleAgentTurn(agent) {
|
|
210
|
-
if (this.ctx.concluded) return true;
|
|
211
|
-
await this.#enforcePendingAsk(agent);
|
|
212
|
-
if (this.ctx.concluded) return true;
|
|
213
|
-
this.eventQueue.enqueue({
|
|
214
|
-
type: "lifecycle",
|
|
215
|
-
agent: agent.name,
|
|
216
|
-
status: "turn_complete",
|
|
217
|
-
});
|
|
218
|
-
return false;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Wait for messages addressed to `name`, returning an empty array when
|
|
223
|
-
* the session concludes first.
|
|
224
|
-
*/
|
|
225
|
-
async #awaitAgentMessages(name) {
|
|
226
|
-
const messages = this.messageBus.drain(name);
|
|
227
|
-
if (messages.length > 0) return messages;
|
|
228
|
-
await Promise.race([
|
|
229
|
-
this.messageBus.waitForMessages(name),
|
|
230
|
-
this.concludePromise,
|
|
231
|
-
]);
|
|
232
|
-
if (this.ctx.concluded) return [];
|
|
233
|
-
return this.messageBus.drain(name);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Facilitator event loop — only runs when input arrives.
|
|
238
|
-
*/
|
|
239
|
-
async #facilitatorLoop() {
|
|
240
|
-
while (!this.ctx.concluded) {
|
|
241
|
-
const event = await this.eventQueue.dequeue();
|
|
242
|
-
if (this.ctx.concluded || event === null) break;
|
|
243
|
-
await this.#handleEvent(event);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async #handleEvent(event) {
|
|
248
|
-
switch (event.type) {
|
|
249
|
-
case "messages":
|
|
250
|
-
case "lifecycle": {
|
|
251
|
-
const msgs = this.messageBus.drain("facilitator");
|
|
252
|
-
if (msgs.length === 0) break;
|
|
253
|
-
this.facilitatorTurns++;
|
|
254
|
-
await this.facilitatorRunner.resume(formatMessages(msgs));
|
|
255
|
-
await this.#processRedirect();
|
|
256
|
-
if (!this.ctx.concluded) await this.#enforceFacilitatorPendingAsk();
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (this.ctx.concluded) {
|
|
262
|
-
this.concludeResolve();
|
|
263
|
-
this.eventQueue.close();
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async #enforceFacilitatorPendingAsk() {
|
|
268
|
-
if (this.#checkAsk("facilitator") !== "recheck") return;
|
|
269
|
-
if (this.ctx.concluded) return;
|
|
270
|
-
const reminders = this.messageBus.drain("facilitator");
|
|
271
|
-
if (reminders.length === 0) return;
|
|
272
|
-
this.facilitatorTurns++;
|
|
273
|
-
await this.facilitatorRunner.resume(formatMessages(reminders));
|
|
274
|
-
await this.#processRedirect();
|
|
275
|
-
if (this.ctx.concluded) return;
|
|
276
|
-
this.#checkAsk("facilitator");
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Process a pending redirect after a facilitator turn.
|
|
281
|
-
*/
|
|
282
|
-
async #processRedirect() {
|
|
283
|
-
if (!this.ctx.redirect) return;
|
|
284
|
-
const redirect = this.ctx.redirect;
|
|
285
|
-
this.ctx.redirect = null;
|
|
286
|
-
|
|
287
|
-
this.emitOrchestratorEvent({
|
|
288
|
-
type: "redirect",
|
|
289
|
-
to: redirect.to,
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
if (redirect.to === "all") {
|
|
293
|
-
// Abort all agents and deliver redirect via broadcast
|
|
294
|
-
for (const agent of this.agents) {
|
|
295
|
-
agent.runner.currentAbortController?.abort();
|
|
296
|
-
}
|
|
297
|
-
this.messageBus.announce("facilitator", redirect.message);
|
|
298
|
-
} else if (redirect.to) {
|
|
299
|
-
// Abort specific agent and deliver via direct message
|
|
300
|
-
const target = this.agents.find((a) => a.name === redirect.to);
|
|
301
|
-
if (target) {
|
|
302
|
-
target.runner.currentAbortController?.abort();
|
|
303
|
-
}
|
|
304
|
-
this.messageBus.direct("facilitator", redirect.to, redirect.message);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/** Return the last assistant text block from a runner's buffer, or the fallback if none exists. */
|
|
309
|
-
extractLastText(runner, fallback) {
|
|
310
|
-
const lines = runner.buffer;
|
|
311
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
312
|
-
const event = JSON.parse(lines[i]);
|
|
313
|
-
if (event.type !== "assistant") continue;
|
|
314
|
-
const content = event.message?.content ?? event.content;
|
|
315
|
-
if (!Array.isArray(content)) continue;
|
|
316
|
-
for (let j = content.length - 1; j >= 0; j--) {
|
|
317
|
-
if (content[j].type === "text" && content[j].text) {
|
|
318
|
-
return content[j].text;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
return fallback;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Emit a single NDJSON line tagged with source and seq.
|
|
327
|
-
* @param {string} source - Participant name
|
|
328
|
-
* @param {string} line - Raw NDJSON line
|
|
329
|
-
*/
|
|
330
|
-
emitLine(source, line) {
|
|
331
|
-
const event = JSON.parse(line);
|
|
332
|
-
this.output.write(
|
|
333
|
-
JSON.stringify(
|
|
334
|
-
this.redactor.redactValue({
|
|
335
|
-
source,
|
|
336
|
-
seq: this.counter.next(),
|
|
337
|
-
event,
|
|
338
|
-
}),
|
|
339
|
-
) + "\n",
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* @param {{type: string}} event
|
|
345
|
-
*/
|
|
346
|
-
emitOrchestratorEvent(event) {
|
|
347
|
-
this.output.write(
|
|
348
|
-
JSON.stringify(
|
|
349
|
-
this.redactor.redactValue({
|
|
350
|
-
source: "orchestrator",
|
|
351
|
-
seq: this.counter.next(),
|
|
352
|
-
event,
|
|
353
|
-
}),
|
|
354
|
-
) + "\n",
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* @param {{success: boolean, verdict?: string|null, turns: number, summary?: string}} result
|
|
360
|
-
*/
|
|
361
|
-
emitSummary(result) {
|
|
362
|
-
this.output.write(
|
|
363
|
-
JSON.stringify(
|
|
364
|
-
this.redactor.redactValue({
|
|
365
|
-
source: "orchestrator",
|
|
366
|
-
seq: this.counter.next(),
|
|
367
|
-
event: {
|
|
368
|
-
type: "summary",
|
|
369
|
-
success: result.success,
|
|
370
|
-
...(result.verdict && { verdict: result.verdict }),
|
|
371
|
-
turns: result.turns,
|
|
372
|
-
...(result.summary && { summary: result.summary }),
|
|
373
|
-
},
|
|
374
|
-
}),
|
|
375
|
-
) + "\n",
|
|
376
|
-
);
|
|
67
|
+
/** Readability shim — exposes the lead runner under its mode-specific name. */
|
|
68
|
+
get facilitatorRunner() {
|
|
69
|
+
return this.leadRunner;
|
|
377
70
|
}
|
|
378
71
|
}
|
|
379
72
|
|
|
@@ -390,15 +83,15 @@ const devNull = new Writable({
|
|
|
390
83
|
* @param {Array<{name: string, role: string, cwd?: string, maxTurns?: number, allowedTools?: string[], agentProfile?: string, systemPromptAmend?: string}>} deps.agentConfigs
|
|
391
84
|
* @param {function} deps.query
|
|
392
85
|
* @param {import("stream").Writable} deps.output
|
|
393
|
-
* @param {string} [deps.model]
|
|
394
|
-
* @param {string} [deps.agentModel]
|
|
395
|
-
* @param {string} [deps.facilitatorModel]
|
|
396
|
-
* @param {number} [deps.maxTurns] -
|
|
397
|
-
* @param {string[]} [deps.facilitatorAllowedTools]
|
|
398
|
-
* @param {string[]} [deps.facilitatorDisallowedTools]
|
|
399
|
-
* @param {string} [deps.facilitatorProfile]
|
|
400
|
-
* @param {string} [deps.profilesDir]
|
|
401
|
-
* @param {string} [deps.taskAmend]
|
|
86
|
+
* @param {string} [deps.model]
|
|
87
|
+
* @param {string} [deps.agentModel]
|
|
88
|
+
* @param {string} [deps.facilitatorModel]
|
|
89
|
+
* @param {number} [deps.maxTurns] - Per-SDK-call turn budget for the facilitator runner (default 80). Each agent's budget is taken from `config.maxTurns` (default 50). The lead is resumed once per inbox-drain round, so this caps the size of one such round, not the whole session — `OrchestrationLoop.maxLeadTurns` bounds session length.
|
|
90
|
+
* @param {string[]} [deps.facilitatorAllowedTools]
|
|
91
|
+
* @param {string[]} [deps.facilitatorDisallowedTools]
|
|
92
|
+
* @param {string} [deps.facilitatorProfile]
|
|
93
|
+
* @param {string} [deps.profilesDir]
|
|
94
|
+
* @param {string} [deps.taskAmend]
|
|
402
95
|
* @returns {Facilitator}
|
|
403
96
|
*/
|
|
404
97
|
export function createFacilitator({
|
|
@@ -441,8 +134,6 @@ export function createFacilitator({
|
|
|
441
134
|
|
|
442
135
|
let facilitator;
|
|
443
136
|
|
|
444
|
-
const eventQueue = createAsyncQueue();
|
|
445
|
-
|
|
446
137
|
const facilitatorServer = createFacilitatorToolServer(ctx);
|
|
447
138
|
|
|
448
139
|
const agents = agentConfigs.map((config) => {
|
|
@@ -484,7 +175,7 @@ export function createFacilitator({
|
|
|
484
175
|
query,
|
|
485
176
|
output: devNull,
|
|
486
177
|
model: facilitatorModel ?? model,
|
|
487
|
-
maxTurns: maxTurns ??
|
|
178
|
+
maxTurns: maxTurns ?? 80,
|
|
488
179
|
allowedTools: facilitatorAllowedTools ?? [
|
|
489
180
|
"Bash",
|
|
490
181
|
"Read",
|
|
@@ -509,9 +200,7 @@ export function createFacilitator({
|
|
|
509
200
|
agents,
|
|
510
201
|
messageBus,
|
|
511
202
|
output,
|
|
512
|
-
maxTurns,
|
|
513
203
|
ctx,
|
|
514
|
-
eventQueue,
|
|
515
204
|
taskAmend,
|
|
516
205
|
redactor,
|
|
517
206
|
});
|
package/src/index.js
CHANGED
|
@@ -26,12 +26,24 @@ export {
|
|
|
26
26
|
createJudgeToolServer,
|
|
27
27
|
} from "./orchestration-toolkit.js";
|
|
28
28
|
export { MessageBus, createMessageBus } from "./message-bus.js";
|
|
29
|
+
export { OrchestrationLoop } from "./orchestration-loop.js";
|
|
29
30
|
export {
|
|
30
31
|
Facilitator,
|
|
31
32
|
createFacilitator,
|
|
32
33
|
FACILITATOR_SYSTEM_PROMPT,
|
|
33
34
|
FACILITATED_AGENT_SYSTEM_PROMPT,
|
|
34
35
|
} from "./facilitator.js";
|
|
36
|
+
export {
|
|
37
|
+
Discusser,
|
|
38
|
+
createDiscusser,
|
|
39
|
+
DISCUSS_SYSTEM_PROMPT,
|
|
40
|
+
augmentContextForDiscuss,
|
|
41
|
+
} from "./discusser.js";
|
|
42
|
+
export {
|
|
43
|
+
createDiscussLeadToolServer,
|
|
44
|
+
createDiscussAgentToolServer,
|
|
45
|
+
DISCUSS_AGENT_SYSTEM_PROMPT,
|
|
46
|
+
} from "./discuss-tools.js";
|
|
35
47
|
export { Judge, createJudge, JUDGE_SYSTEM_PROMPT } from "./judge.js";
|
|
36
48
|
export {
|
|
37
49
|
Redactor,
|
package/src/judge.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Judge — one agent session that inspects a completed agent's work and emits
|
|
3
3
|
* a verdict via the orchestration `Conclude` tool. Parallel concept to
|
|
4
4
|
* `Supervisor` and `Facilitator`, but post-hoc and solo: no peer agents,
|
|
5
|
-
* no message bus, no
|
|
5
|
+
* no message bus, no orchestration loop. The judge reads the task, optionally
|
|
6
6
|
* inspects the working directory and trace via read-only tools, and calls
|
|
7
7
|
* Conclude exactly once.
|
|
8
8
|
*
|
package/src/message-bus.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MessageBus — in-memory per-participant message queues
|
|
3
|
-
* supervised modes. The message vocabulary mirrors the orchestration toolkit:
|
|
2
|
+
* MessageBus — in-memory per-participant message queues.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
4
|
+
* Four message kinds, each pushed onto the addressee's queue:
|
|
5
|
+
*
|
|
6
|
+
* - `ask(from, to, text, askId)` — direct question; the toolkit owns the
|
|
7
|
+
* pending-ask state separately. Fan-out (broadcast Ask) happens at the
|
|
8
|
+
* handler level by calling `ask()` once per addressee.
|
|
9
|
+
* - `answer(from, to, text, askId)` — direct reply to the original asker.
|
|
10
|
+
* The orchestrator may inject synthetic answers (`from === "@orchestrator"`)
|
|
11
|
+
* when an Ask times out.
|
|
12
|
+
* - `announce(from, text)` — broadcast, no reply expected; lands on every
|
|
13
|
+
* participant's queue except the sender's.
|
|
14
|
+
* - `synthetic(to, text)` — orchestrator-only reminder injection.
|
|
11
15
|
*
|
|
12
16
|
* Follows OO+DI: constructor injection, factory function, tests bypass factory.
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
|
-
/** In-memory per-participant message queues
|
|
19
|
+
/** In-memory per-participant message queues. */
|
|
16
20
|
export class MessageBus {
|
|
17
21
|
/**
|
|
18
22
|
* @param {object} deps
|
|
19
|
-
* @param {string[]} deps.participants -
|
|
23
|
+
* @param {string[]} deps.participants - Canonical participant names.
|
|
20
24
|
*/
|
|
21
25
|
constructor({ participants }) {
|
|
22
26
|
this.queues = new Map();
|
|
@@ -27,55 +31,30 @@ export class MessageBus {
|
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
/**
|
|
31
|
-
* Send a question to a participant (direct), or broadcast when
|
|
32
|
-
* `to === "@broadcast"`.
|
|
33
|
-
* @param {string} from
|
|
34
|
-
* @param {string} to - Recipient name or "@broadcast"
|
|
35
|
-
* @param {string} text
|
|
36
|
-
* @param {number} askId
|
|
37
|
-
*/
|
|
34
|
+
/** Send a question to one participant. */
|
|
38
35
|
ask(from, to, text, askId) {
|
|
39
36
|
this.#assertParticipant(from);
|
|
40
|
-
if (to === "@broadcast") {
|
|
41
|
-
for (const [name, queue] of this.queues) {
|
|
42
|
-
if (name === from) continue;
|
|
43
|
-
queue.push({ from, text, kind: "ask", askId, direct: false });
|
|
44
|
-
this.#resolveWaiter(name);
|
|
45
|
-
}
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
37
|
this.#assertParticipant(to);
|
|
49
|
-
this.queues.get(to).push({ from, text, kind: "ask", askId
|
|
38
|
+
this.queues.get(to).push({ from, text, kind: "ask", askId });
|
|
50
39
|
this.#resolveWaiter(to);
|
|
51
40
|
}
|
|
52
41
|
|
|
53
42
|
/**
|
|
54
|
-
* Reply to a pending ask.
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* @param {string} text
|
|
58
|
-
* @param {number} askId
|
|
43
|
+
* Reply to a pending ask. `from === "@orchestrator"` is allowed for
|
|
44
|
+
* synthetic null answers — the orchestrator is not a real participant
|
|
45
|
+
* but it routes through the bus.
|
|
59
46
|
*/
|
|
60
47
|
answer(from, to, text, askId) {
|
|
61
48
|
this.#assertParticipant(to);
|
|
62
|
-
// Synthetic answers from the orchestrator bypass the participant check
|
|
63
|
-
// on `from` — the orchestrator is not a message-bus participant.
|
|
64
49
|
if (from !== "@orchestrator") this.#assertParticipant(from);
|
|
65
|
-
this.queues
|
|
66
|
-
.get(to)
|
|
67
|
-
.push({ from, text, kind: "answer", askId, direct: true });
|
|
50
|
+
this.queues.get(to).push({ from, text, kind: "answer", askId });
|
|
68
51
|
this.#resolveWaiter(to);
|
|
69
52
|
}
|
|
70
53
|
|
|
71
|
-
/**
|
|
72
|
-
* Broadcast a message to every participant except the sender.
|
|
73
|
-
* @param {string} from
|
|
74
|
-
* @param {string} text
|
|
75
|
-
*/
|
|
54
|
+
/** Broadcast a message to every participant except the sender. */
|
|
76
55
|
announce(from, text) {
|
|
77
56
|
this.#assertParticipant(from);
|
|
78
|
-
const msg = { from, text, kind: "announce"
|
|
57
|
+
const msg = { from, text, kind: "announce" };
|
|
79
58
|
for (const [name, queue] of this.queues) {
|
|
80
59
|
if (name === from) continue;
|
|
81
60
|
queue.push(msg);
|
|
@@ -83,53 +62,24 @@ export class MessageBus {
|
|
|
83
62
|
}
|
|
84
63
|
}
|
|
85
64
|
|
|
86
|
-
/**
|
|
87
|
-
* Send a direct message with no reply expected. Used by the Redirect
|
|
88
|
-
* runtime plumbing (facilitator / supervisor) to deliver replacement
|
|
89
|
-
* instructions to a single participant without engaging the ask/answer
|
|
90
|
-
* contract.
|
|
91
|
-
* @param {string} from
|
|
92
|
-
* @param {string} to
|
|
93
|
-
* @param {string} text
|
|
94
|
-
*/
|
|
95
|
-
direct(from, to, text) {
|
|
96
|
-
this.#assertParticipant(from);
|
|
97
|
-
this.#assertParticipant(to);
|
|
98
|
-
this.queues.get(to).push({ from, text, kind: "direct", direct: true });
|
|
99
|
-
this.#resolveWaiter(to);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Inject an orchestrator-originated reminder onto a single participant's
|
|
104
|
-
* queue. Used by the turn-complete guard.
|
|
105
|
-
* @param {string} to
|
|
106
|
-
* @param {string} text
|
|
107
|
-
*/
|
|
65
|
+
/** Inject an orchestrator-originated reminder onto one participant's queue. */
|
|
108
66
|
synthetic(to, text) {
|
|
109
67
|
this.#assertParticipant(to);
|
|
110
68
|
this.queues
|
|
111
69
|
.get(to)
|
|
112
|
-
.push({ from: "@orchestrator", text, kind: "synthetic"
|
|
70
|
+
.push({ from: "@orchestrator", text, kind: "synthetic" });
|
|
113
71
|
this.#resolveWaiter(to);
|
|
114
72
|
}
|
|
115
73
|
|
|
116
|
-
/**
|
|
117
|
-
* Return and clear pending messages for a participant.
|
|
118
|
-
* @param {string} participant - Participant name
|
|
119
|
-
* @returns {{from: string, text: string, kind: string, direct: boolean, askId?: number}[]}
|
|
120
|
-
*/
|
|
74
|
+
/** Return and clear pending messages for a participant. */
|
|
121
75
|
drain(participant) {
|
|
122
76
|
this.#assertParticipant(participant);
|
|
123
|
-
|
|
124
|
-
const messages = queue.splice(0);
|
|
125
|
-
return messages;
|
|
77
|
+
return this.queues.get(participant).splice(0);
|
|
126
78
|
}
|
|
127
79
|
|
|
128
80
|
/**
|
|
129
81
|
* Return a Promise that resolves when at least one message is pending.
|
|
130
82
|
* Resolves immediately if messages are already queued.
|
|
131
|
-
* @param {string} participant - Participant name
|
|
132
|
-
* @returns {Promise<void>}
|
|
133
83
|
*/
|
|
134
84
|
waitForMessages(participant) {
|
|
135
85
|
this.#assertParticipant(participant);
|
|
@@ -156,11 +106,7 @@ export class MessageBus {
|
|
|
156
106
|
}
|
|
157
107
|
}
|
|
158
108
|
|
|
159
|
-
/**
|
|
160
|
-
* Factory function.
|
|
161
|
-
* @param {object} deps - Same as MessageBus constructor
|
|
162
|
-
* @returns {MessageBus}
|
|
163
|
-
*/
|
|
109
|
+
/** Factory function. */
|
|
164
110
|
export function createMessageBus(deps) {
|
|
165
111
|
return new MessageBus(deps);
|
|
166
112
|
}
|