@forwardimpact/libeval 0.1.20 → 0.1.22
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-trace.js +49 -0
- package/package.json +6 -3
- package/src/agent-runner.js +5 -1
- package/src/commands/facilitate.js +3 -2
- package/src/commands/run.js +4 -2
- package/src/commands/supervise.js +3 -2
- package/src/commands/trace.js +46 -14
- package/src/facilitator.js +78 -135
- package/src/index.js +1 -0
- package/src/message-bus.js +78 -13
- package/src/orchestration-toolkit.js +211 -63
- package/src/orchestrator-helpers.js +58 -0
- package/src/render/tool-hints.js +3 -3
- package/src/signature-filter.js +27 -0
- package/src/supervisor.js +110 -38
- package/src/tee-writer.js +21 -0
- package/src/trace-collector.js +52 -3
- package/src/trace-query.js +141 -28
package/src/facilitator.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Facilitator —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Follows OO+DI: constructor injection, factory function, tests bypass factory.
|
|
2
|
+
* Facilitator — N agent sessions + one facilitator LLM session. The Ask/Answer
|
|
3
|
+
* contract is enforced at turn boundaries via checkPendingAsk: one synthetic
|
|
4
|
+
* reminder, then a `protocol_violation` event plus a null-answer injection so
|
|
5
|
+
* the session advances instead of deadlocking.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { Writable } from "node:stream";
|
|
@@ -16,62 +15,26 @@ import {
|
|
|
16
15
|
createOrchestrationContext,
|
|
17
16
|
createFacilitatorToolServer,
|
|
18
17
|
createFacilitatedAgentToolServer,
|
|
18
|
+
checkPendingAsk,
|
|
19
19
|
} from "./orchestration-toolkit.js";
|
|
20
|
+
import { createAsyncQueue, formatMessages } from "./orchestrator-helpers.js";
|
|
20
21
|
|
|
21
22
|
/** System prompt appended for the facilitator runner. */
|
|
22
23
|
export const FACILITATOR_SYSTEM_PROMPT =
|
|
23
|
-
"You coordinate multiple
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"Redirect interrupts a participant
|
|
27
|
-
"RollCall lists
|
|
28
|
-
"Conclude ends the session with a summary.
|
|
29
|
-
"Participants communicate with you via Share and may Ask you questions. " +
|
|
30
|
-
"IMPORTANT: After sending messages via Tell or Share, stop making tool " +
|
|
31
|
-
"calls and produce a text response. The system will resume you with " +
|
|
32
|
-
"participant responses. Do not proceed to the next question or call " +
|
|
33
|
-
"Conclude until you have received responses from participants.";
|
|
24
|
+
"You coordinate multiple participants. " +
|
|
25
|
+
"Ask sends a question to a participant; omit the addressee to broadcast. " +
|
|
26
|
+
"Announce sends a message with no reply obligation. " +
|
|
27
|
+
"Redirect interrupts a participant with replacement instructions. " +
|
|
28
|
+
"RollCall lists participants. " +
|
|
29
|
+
"Conclude ends the session with a summary.";
|
|
34
30
|
|
|
35
31
|
/** System prompt appended for facilitated agent runners. */
|
|
36
32
|
export const FACILITATED_AGENT_SYSTEM_PROMPT =
|
|
37
|
-
"You
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"RollCall lists available participants and their roles. " +
|
|
43
|
-
"The facilitator may Redirect you with new instructions " +
|
|
44
|
-
"— treat redirections as authoritative.";
|
|
45
|
-
|
|
46
|
-
function createAsyncQueue() {
|
|
47
|
-
const items = [];
|
|
48
|
-
let waiter = null;
|
|
49
|
-
let closed = false;
|
|
50
|
-
return {
|
|
51
|
-
enqueue(item) {
|
|
52
|
-
items.push(item);
|
|
53
|
-
if (waiter) {
|
|
54
|
-
waiter();
|
|
55
|
-
waiter = null;
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
async dequeue() {
|
|
59
|
-
if (items.length > 0) return items.shift();
|
|
60
|
-
if (closed) return null;
|
|
61
|
-
await new Promise((resolve) => {
|
|
62
|
-
waiter = resolve;
|
|
63
|
-
});
|
|
64
|
-
return items.length > 0 ? items.shift() : null;
|
|
65
|
-
},
|
|
66
|
-
close() {
|
|
67
|
-
closed = true;
|
|
68
|
-
if (waiter) {
|
|
69
|
-
waiter();
|
|
70
|
-
waiter = null;
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
}
|
|
33
|
+
"You participate in a coordinated session. " +
|
|
34
|
+
"Answer replies to an ask addressed to you. " +
|
|
35
|
+
"Ask sends a question to another participant. " +
|
|
36
|
+
"Announce broadcasts a message. " +
|
|
37
|
+
"RollCall lists participants.";
|
|
75
38
|
|
|
76
39
|
export class Facilitator {
|
|
77
40
|
/**
|
|
@@ -83,6 +46,7 @@ export class Facilitator {
|
|
|
83
46
|
* @param {number} [deps.maxTurns]
|
|
84
47
|
* @param {object} [deps.ctx]
|
|
85
48
|
* @param {object} [deps.eventQueue]
|
|
49
|
+
* @param {string} [deps.taskAmend] - Opaque addendum appended to the task before delivery.
|
|
86
50
|
*/
|
|
87
51
|
constructor({
|
|
88
52
|
facilitatorRunner,
|
|
@@ -92,6 +56,7 @@ export class Facilitator {
|
|
|
92
56
|
maxTurns,
|
|
93
57
|
ctx,
|
|
94
58
|
eventQueue,
|
|
59
|
+
taskAmend,
|
|
95
60
|
}) {
|
|
96
61
|
this.facilitatorRunner = facilitatorRunner;
|
|
97
62
|
this.agents = agents;
|
|
@@ -102,6 +67,7 @@ export class Facilitator {
|
|
|
102
67
|
this.counter = new SequenceCounter();
|
|
103
68
|
this.eventQueue = eventQueue ?? createAsyncQueue();
|
|
104
69
|
this.facilitatorTurns = 0;
|
|
70
|
+
this.taskAmend = taskAmend ?? null;
|
|
105
71
|
|
|
106
72
|
let resolve;
|
|
107
73
|
const promise = new Promise((r) => {
|
|
@@ -119,21 +85,23 @@ export class Facilitator {
|
|
|
119
85
|
async run(task) {
|
|
120
86
|
this.emitOrchestratorEvent({ type: "session_start" });
|
|
121
87
|
|
|
88
|
+
const initialTask = this.taskAmend ? `${task}\n\n${this.taskAmend}` : task;
|
|
89
|
+
|
|
122
90
|
// Launch agent loops first — they wait for messages via messageBus.
|
|
123
|
-
// This lets agents process
|
|
124
|
-
// facilitator's initial run, rather than after it completes.
|
|
91
|
+
// This lets agents process Ask/Announce messages that arrive during
|
|
92
|
+
// the facilitator's initial run, rather than after it completes.
|
|
125
93
|
const agentPromises = this.agents.map((a) => this.#runAgent(a));
|
|
126
94
|
|
|
127
95
|
// Turn 0: facilitator receives the task
|
|
128
96
|
this.facilitatorTurns++;
|
|
129
|
-
await this.facilitatorRunner.run(
|
|
97
|
+
await this.facilitatorRunner.run(initialTask);
|
|
130
98
|
|
|
131
99
|
// Handle redirect after turn 0
|
|
132
100
|
await this.#processRedirect();
|
|
133
101
|
|
|
134
102
|
if (this.ctx.concluded) {
|
|
135
103
|
// Facilitator concluded during its initial run. Let agents finish any
|
|
136
|
-
// in-progress work before returning — they may have received
|
|
104
|
+
// in-progress work before returning — they may have received Ask/Answer
|
|
137
105
|
// messages and started processing concurrently.
|
|
138
106
|
this.concludeResolve();
|
|
139
107
|
await Promise.allSettled(agentPromises);
|
|
@@ -177,6 +145,26 @@ export class Facilitator {
|
|
|
177
145
|
return result;
|
|
178
146
|
}
|
|
179
147
|
|
|
148
|
+
#checkAsk(name) {
|
|
149
|
+
return checkPendingAsk({
|
|
150
|
+
ctx: this.ctx,
|
|
151
|
+
messageBus: this.messageBus,
|
|
152
|
+
addresseeName: name,
|
|
153
|
+
mode: "facilitated",
|
|
154
|
+
emitViolation: (e) => this.emitOrchestratorEvent(e),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async #enforcePendingAsk(agent) {
|
|
159
|
+
if (this.#checkAsk(agent.name) !== "recheck") return;
|
|
160
|
+
if (this.ctx.concluded) return;
|
|
161
|
+
const reminders = this.messageBus.drain(agent.name);
|
|
162
|
+
if (reminders.length === 0) return;
|
|
163
|
+
await agent.runner.resume(formatMessages(reminders));
|
|
164
|
+
if (this.ctx.concluded) return;
|
|
165
|
+
this.#checkAsk(agent.name);
|
|
166
|
+
}
|
|
167
|
+
|
|
180
168
|
/**
|
|
181
169
|
* Agent outer loop — waits for messages, runs/resumes the agent.
|
|
182
170
|
* @param {{name: string, role: string, runner: import("./agent-runner.js").AgentRunner}} agent
|
|
@@ -196,7 +184,9 @@ export class Facilitator {
|
|
|
196
184
|
type: "agent_start",
|
|
197
185
|
agent: agent.name,
|
|
198
186
|
});
|
|
199
|
-
await agent.runner.run(
|
|
187
|
+
await agent.runner.run(formatMessages(messages));
|
|
188
|
+
if (this.ctx.concluded) return;
|
|
189
|
+
await this.#enforcePendingAsk(agent);
|
|
200
190
|
if (this.ctx.concluded) return;
|
|
201
191
|
this.eventQueue.enqueue({
|
|
202
192
|
type: "lifecycle",
|
|
@@ -216,7 +206,9 @@ export class Facilitator {
|
|
|
216
206
|
messages = this.messageBus.drain(agent.name);
|
|
217
207
|
if (messages.length === 0) break;
|
|
218
208
|
}
|
|
219
|
-
await agent.runner.resume(
|
|
209
|
+
await agent.runner.resume(formatMessages(messages));
|
|
210
|
+
if (this.ctx.concluded) break;
|
|
211
|
+
await this.#enforcePendingAsk(agent);
|
|
220
212
|
if (this.ctx.concluded) break;
|
|
221
213
|
this.eventQueue.enqueue({
|
|
222
214
|
type: "lifecycle",
|
|
@@ -239,46 +231,14 @@ export class Facilitator {
|
|
|
239
231
|
|
|
240
232
|
async #handleEvent(event) {
|
|
241
233
|
switch (event.type) {
|
|
242
|
-
case "
|
|
243
|
-
if (this.ctx.concluded) {
|
|
244
|
-
event.resolve("Session has concluded.");
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
this.facilitatorTurns++;
|
|
248
|
-
this.emitOrchestratorEvent({
|
|
249
|
-
type: "ask_received",
|
|
250
|
-
from: event.from,
|
|
251
|
-
});
|
|
252
|
-
await this.facilitatorRunner.resume(
|
|
253
|
-
`Agent "${event.from}" asks: "${event.question}"\nAnswer the question.`,
|
|
254
|
-
);
|
|
255
|
-
const answer = this.extractLastText(
|
|
256
|
-
this.facilitatorRunner,
|
|
257
|
-
"No answer.",
|
|
258
|
-
);
|
|
259
|
-
this.emitOrchestratorEvent({
|
|
260
|
-
type: "ask_answered",
|
|
261
|
-
from: event.from,
|
|
262
|
-
});
|
|
263
|
-
event.resolve(answer);
|
|
264
|
-
await this.#processRedirect();
|
|
265
|
-
break;
|
|
266
|
-
}
|
|
267
|
-
case "messages": {
|
|
268
|
-
const msgs = this.messageBus.drain("facilitator");
|
|
269
|
-
if (msgs.length === 0) break;
|
|
270
|
-
this.facilitatorTurns++;
|
|
271
|
-
await this.facilitatorRunner.resume(this.#formatMessages(msgs));
|
|
272
|
-
await this.#processRedirect();
|
|
273
|
-
break;
|
|
274
|
-
}
|
|
234
|
+
case "messages":
|
|
275
235
|
case "lifecycle": {
|
|
276
|
-
// Check for pending shared messages for the facilitator
|
|
277
236
|
const msgs = this.messageBus.drain("facilitator");
|
|
278
237
|
if (msgs.length === 0) break;
|
|
279
238
|
this.facilitatorTurns++;
|
|
280
|
-
await this.facilitatorRunner.resume(
|
|
239
|
+
await this.facilitatorRunner.resume(formatMessages(msgs));
|
|
281
240
|
await this.#processRedirect();
|
|
241
|
+
if (!this.ctx.concluded) await this.#enforceFacilitatorPendingAsk();
|
|
282
242
|
break;
|
|
283
243
|
}
|
|
284
244
|
}
|
|
@@ -289,6 +249,18 @@ export class Facilitator {
|
|
|
289
249
|
}
|
|
290
250
|
}
|
|
291
251
|
|
|
252
|
+
async #enforceFacilitatorPendingAsk() {
|
|
253
|
+
if (this.#checkAsk("facilitator") !== "recheck") return;
|
|
254
|
+
if (this.ctx.concluded) return;
|
|
255
|
+
const reminders = this.messageBus.drain("facilitator");
|
|
256
|
+
if (reminders.length === 0) return;
|
|
257
|
+
this.facilitatorTurns++;
|
|
258
|
+
await this.facilitatorRunner.resume(formatMessages(reminders));
|
|
259
|
+
await this.#processRedirect();
|
|
260
|
+
if (this.ctx.concluded) return;
|
|
261
|
+
this.#checkAsk("facilitator");
|
|
262
|
+
}
|
|
263
|
+
|
|
292
264
|
/**
|
|
293
265
|
* Process a pending redirect after a facilitator turn.
|
|
294
266
|
*/
|
|
@@ -307,37 +279,17 @@ export class Facilitator {
|
|
|
307
279
|
for (const agent of this.agents) {
|
|
308
280
|
agent.runner.currentAbortController?.abort();
|
|
309
281
|
}
|
|
310
|
-
this.messageBus.
|
|
282
|
+
this.messageBus.announce("facilitator", redirect.message);
|
|
311
283
|
} else if (redirect.to) {
|
|
312
284
|
// Abort specific agent and deliver via direct message
|
|
313
285
|
const target = this.agents.find((a) => a.name === redirect.to);
|
|
314
286
|
if (target) {
|
|
315
287
|
target.runner.currentAbortController?.abort();
|
|
316
288
|
}
|
|
317
|
-
this.messageBus.
|
|
289
|
+
this.messageBus.direct("facilitator", redirect.to, redirect.message);
|
|
318
290
|
}
|
|
319
291
|
}
|
|
320
292
|
|
|
321
|
-
/**
|
|
322
|
-
* Format messages for an agent prompt.
|
|
323
|
-
* @param {Array<{from: string, text: string, direct: boolean}>} messages
|
|
324
|
-
* @returns {string}
|
|
325
|
-
*/
|
|
326
|
-
#formatMessages(messages) {
|
|
327
|
-
return messages
|
|
328
|
-
.map((m) => {
|
|
329
|
-
const tag = m.direct ? "[direct]" : "[shared]";
|
|
330
|
-
return `${tag} ${m.from}: ${m.text}`;
|
|
331
|
-
})
|
|
332
|
-
.join("\n");
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Extract the last assistant text block from a runner's buffer.
|
|
337
|
-
* @param {import("./agent-runner.js").AgentRunner} runner
|
|
338
|
-
* @param {string} fallback
|
|
339
|
-
* @returns {string}
|
|
340
|
-
*/
|
|
341
293
|
extractLastText(runner, fallback) {
|
|
342
294
|
const lines = runner.buffer;
|
|
343
295
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -412,13 +364,14 @@ const devNull = new Writable({
|
|
|
412
364
|
* Factory function — wires all participants with MCP servers.
|
|
413
365
|
* @param {object} deps
|
|
414
366
|
* @param {string} deps.facilitatorCwd
|
|
415
|
-
* @param {Array<{name: string, role: string, cwd?: string, maxTurns?: number, allowedTools?: string[], agentProfile?: string}>} deps.agentConfigs
|
|
367
|
+
* @param {Array<{name: string, role: string, cwd?: string, maxTurns?: number, allowedTools?: string[], agentProfile?: string, systemPromptAmend?: string}>} deps.agentConfigs
|
|
416
368
|
* @param {function} deps.query
|
|
417
369
|
* @param {import("stream").Writable} deps.output
|
|
418
370
|
* @param {string} [deps.model]
|
|
419
371
|
* @param {number} [deps.maxTurns]
|
|
420
372
|
* @param {string} [deps.facilitatorProfile] - Facilitator profile name; resolved into the main-thread system prompt via `composeProfilePrompt`.
|
|
421
373
|
* @param {string} [deps.profilesDir] - Directory containing `<name>.md` profile files. Defaults to `<facilitatorCwd>/.claude/agents`. Resolved once from the facilitator's cwd so profiles travel with the project, not with per-agent sandboxes.
|
|
374
|
+
* @param {string} [deps.taskAmend] - Opaque addendum appended to the task before delivery.
|
|
422
375
|
* @returns {Facilitator}
|
|
423
376
|
*/
|
|
424
377
|
export function createFacilitator({
|
|
@@ -430,6 +383,7 @@ export function createFacilitator({
|
|
|
430
383
|
maxTurns,
|
|
431
384
|
facilitatorProfile,
|
|
432
385
|
profilesDir,
|
|
386
|
+
taskAmend,
|
|
433
387
|
}) {
|
|
434
388
|
const resolvedProfilesDir =
|
|
435
389
|
profilesDir ?? resolve(facilitatorCwd, ".claude/agents");
|
|
@@ -461,21 +415,12 @@ export function createFacilitator({
|
|
|
461
415
|
const agents = agentConfigs.map((config) => {
|
|
462
416
|
const agentServer = createFacilitatedAgentToolServer(ctx, {
|
|
463
417
|
from: config.name,
|
|
464
|
-
onAsk: async (question) => {
|
|
465
|
-
let resolve;
|
|
466
|
-
const promise = new Promise((r) => {
|
|
467
|
-
resolve = r;
|
|
468
|
-
});
|
|
469
|
-
eventQueue.enqueue({
|
|
470
|
-
type: "ask",
|
|
471
|
-
from: config.name,
|
|
472
|
-
question,
|
|
473
|
-
resolve,
|
|
474
|
-
});
|
|
475
|
-
return promise;
|
|
476
|
-
},
|
|
477
418
|
});
|
|
478
419
|
|
|
420
|
+
const agentTrailer = config.systemPromptAmend
|
|
421
|
+
? `${FACILITATED_AGENT_SYSTEM_PROMPT}\n\n${config.systemPromptAmend}`
|
|
422
|
+
: FACILITATED_AGENT_SYSTEM_PROMPT;
|
|
423
|
+
|
|
479
424
|
const runner = createAgentRunner({
|
|
480
425
|
cwd: config.cwd ?? facilitatorCwd,
|
|
481
426
|
query,
|
|
@@ -486,10 +431,7 @@ export function createFacilitator({
|
|
|
486
431
|
onLine: (line) => facilitator.emitLine(config.name, line),
|
|
487
432
|
mcpServers: { orchestration: agentServer },
|
|
488
433
|
settingSources: ["project"],
|
|
489
|
-
systemPrompt: systemPromptFor(
|
|
490
|
-
config.agentProfile,
|
|
491
|
-
FACILITATED_AGENT_SYSTEM_PROMPT,
|
|
492
|
-
),
|
|
434
|
+
systemPrompt: systemPromptFor(config.agentProfile, agentTrailer),
|
|
493
435
|
});
|
|
494
436
|
|
|
495
437
|
return { name: config.name, role: config.role, runner };
|
|
@@ -518,6 +460,7 @@ export function createFacilitator({
|
|
|
518
460
|
maxTurns,
|
|
519
461
|
ctx,
|
|
520
462
|
eventQueue,
|
|
463
|
+
taskAmend,
|
|
521
464
|
});
|
|
522
465
|
return facilitator;
|
|
523
466
|
}
|
package/src/index.js
CHANGED
package/src/message-bus.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MessageBus — in-memory per-participant message queues for
|
|
2
|
+
* MessageBus — in-memory per-participant message queues for facilitated and
|
|
3
|
+
* supervised modes. The message vocabulary mirrors the orchestration toolkit:
|
|
4
|
+
*
|
|
5
|
+
* - ask(from, to, text, askId) — direct question; registers nothing
|
|
6
|
+
* itself (the handler's caller owns pending-ask state). `to === "@broadcast"`
|
|
7
|
+
* sends an identical entry to every participant except the sender.
|
|
8
|
+
* - answer(from, to, text, askId) — direct reply to the asker.
|
|
9
|
+
* - announce(from, text) — broadcast, no reply expected.
|
|
10
|
+
* - synthetic(to, text) — orchestrator-only reminder injection.
|
|
3
11
|
*
|
|
4
12
|
* Follows OO+DI: constructor injection, factory function, tests bypass factory.
|
|
5
13
|
*/
|
|
@@ -18,14 +26,55 @@ export class MessageBus {
|
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Send a question to a participant (direct), or broadcast when
|
|
31
|
+
* `to === "@broadcast"`.
|
|
32
|
+
* @param {string} from
|
|
33
|
+
* @param {string} to - Recipient name or "@broadcast"
|
|
34
|
+
* @param {string} text
|
|
35
|
+
* @param {number} askId
|
|
36
|
+
*/
|
|
37
|
+
ask(from, to, text, askId) {
|
|
38
|
+
this.#assertParticipant(from);
|
|
39
|
+
if (to === "@broadcast") {
|
|
40
|
+
for (const [name, queue] of this.queues) {
|
|
41
|
+
if (name === from) continue;
|
|
42
|
+
queue.push({ from, text, kind: "ask", askId, direct: false });
|
|
43
|
+
this.#resolveWaiter(name);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.#assertParticipant(to);
|
|
48
|
+
this.queues.get(to).push({ from, text, kind: "ask", askId, direct: true });
|
|
49
|
+
this.#resolveWaiter(to);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Reply to a pending ask.
|
|
54
|
+
* @param {string} from - Answerer (or "@orchestrator" for a synthetic answer)
|
|
55
|
+
* @param {string} to - Original asker
|
|
56
|
+
* @param {string} text
|
|
57
|
+
* @param {number} askId
|
|
58
|
+
*/
|
|
59
|
+
answer(from, to, text, askId) {
|
|
60
|
+
this.#assertParticipant(to);
|
|
61
|
+
// Synthetic answers from the orchestrator bypass the participant check
|
|
62
|
+
// on `from` — the orchestrator is not a message-bus participant.
|
|
63
|
+
if (from !== "@orchestrator") this.#assertParticipant(from);
|
|
64
|
+
this.queues
|
|
65
|
+
.get(to)
|
|
66
|
+
.push({ from, text, kind: "answer", askId, direct: true });
|
|
67
|
+
this.#resolveWaiter(to);
|
|
68
|
+
}
|
|
69
|
+
|
|
21
70
|
/**
|
|
22
71
|
* Broadcast a message to every participant except the sender.
|
|
23
|
-
* @param {string} from
|
|
24
|
-
* @param {string}
|
|
72
|
+
* @param {string} from
|
|
73
|
+
* @param {string} text
|
|
25
74
|
*/
|
|
26
|
-
|
|
75
|
+
announce(from, text) {
|
|
27
76
|
this.#assertParticipant(from);
|
|
28
|
-
const msg = { from, text:
|
|
77
|
+
const msg = { from, text, kind: "announce", direct: false };
|
|
29
78
|
for (const [name, queue] of this.queues) {
|
|
30
79
|
if (name === from) continue;
|
|
31
80
|
queue.push(msg);
|
|
@@ -34,23 +83,39 @@ export class MessageBus {
|
|
|
34
83
|
}
|
|
35
84
|
|
|
36
85
|
/**
|
|
37
|
-
* Send a direct message
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
86
|
+
* Send a direct message with no reply expected. Used by the Redirect
|
|
87
|
+
* runtime plumbing (facilitator / supervisor) to deliver replacement
|
|
88
|
+
* instructions to a single participant without engaging the ask/answer
|
|
89
|
+
* contract.
|
|
90
|
+
* @param {string} from
|
|
91
|
+
* @param {string} to
|
|
92
|
+
* @param {string} text
|
|
41
93
|
*/
|
|
42
|
-
|
|
94
|
+
direct(from, to, text) {
|
|
43
95
|
this.#assertParticipant(from);
|
|
44
96
|
this.#assertParticipant(to);
|
|
45
|
-
|
|
46
|
-
this
|
|
97
|
+
this.queues.get(to).push({ from, text, kind: "direct", direct: true });
|
|
98
|
+
this.#resolveWaiter(to);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Inject an orchestrator-originated reminder onto a single participant's
|
|
103
|
+
* queue. Used by the turn-complete guard.
|
|
104
|
+
* @param {string} to
|
|
105
|
+
* @param {string} text
|
|
106
|
+
*/
|
|
107
|
+
synthetic(to, text) {
|
|
108
|
+
this.#assertParticipant(to);
|
|
109
|
+
this.queues
|
|
110
|
+
.get(to)
|
|
111
|
+
.push({ from: "@orchestrator", text, kind: "synthetic", direct: true });
|
|
47
112
|
this.#resolveWaiter(to);
|
|
48
113
|
}
|
|
49
114
|
|
|
50
115
|
/**
|
|
51
116
|
* Return and clear pending messages for a participant.
|
|
52
117
|
* @param {string} participant - Participant name
|
|
53
|
-
* @returns {{
|
|
118
|
+
* @returns {{from: string, text: string, kind: string, direct: boolean, askId?: number}[]}
|
|
54
119
|
*/
|
|
55
120
|
drain(participant) {
|
|
56
121
|
this.#assertParticipant(participant);
|