@forwardimpact/libeval 0.1.44 → 0.1.46

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.
@@ -1,36 +1,44 @@
1
1
  /**
2
2
  * OrchestrationToolkit — tool schemas, per-role tool sets, and handler
3
- * factories for orchestration between supervisors, facilitators, and agents.
3
+ * factories for orchestration between leads (facilitator, supervisor,
4
+ * discuss-lead) and their participating agents.
4
5
  *
5
- * The tool surface is Ask / Answer / Announce + Redirect / Conclude / RollCall,
6
- * shared across facilitation and supervision. Ask registers a pending-ask in
7
- * the context; Answer clears it and routes the reply. The orchestrator's
8
- * turn-complete guard (see checkPendingAsk) holds the request-response
9
- * contract at the runtime instead of the prompt layer.
6
+ * **Tool surface, by role:**
10
7
  *
11
- * Handlers communicate via a shared context object. The orchestrator reads
12
- * context at natural checkpoints (after resume(), after onBatch).
8
+ * | | Ask | Answer | Announce | RollCall | Conclude | …extras |
9
+ * |-------------|-----|--------|----------|----------|----------|---------|
10
+ * | Facilitator | ✓ | ✓ | ✓ | ✓ | ✓ | |
11
+ * | Fac. agent | ✓ | ✓ | ✓ | ✓ | | |
12
+ * | Supervisor | ✓ | ✓ | ✓ | ✓ | ✓ | |
13
+ * | Sup. agent | ✓ | ✓ | ✓ | ✓ | | |
14
+ * | Discuss lead| ✓ | ✓ | ✓ | ✓ | | RFC / Recess / Adjourn |
15
+ * | Discuss agt | ✓ | ✓ | ✓ | ✓ | | |
16
+ * | Judge | | | | | ✓ | |
17
+ *
18
+ * **Ask is async.** Ask returns `{askIds:[…]}` immediately and posts the
19
+ * question to the addressee's bus queue. The reply arrives on the asker's
20
+ * next turn as `[answer#N] <participant>: <text>`. Pending state keys by
21
+ * `askId` (visible in `[ask#N]` tags), so duplicate Asks to the same
22
+ * addressee coexist without overwriting.
23
+ *
24
+ * **Answer's `askId` is optional.** With a matching askId, the reply
25
+ * routes to that specific asker. Without, the handler auto-picks if
26
+ * exactly one ask is owed to the caller, otherwise routes the message
27
+ * as an Announce so it still reaches everyone.
13
28
  */
14
29
 
15
30
  import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
16
31
  import { z } from "zod";
17
32
 
18
- /**
19
- * Create a fresh orchestration context object.
20
- * @returns {object}
21
- */
33
+ /** Create a fresh orchestration context object. */
22
34
  export function createOrchestrationContext() {
23
35
  return {
24
36
  concluded: false,
25
37
  verdict: null,
26
38
  summary: null,
27
- redirect: null,
28
39
  participants: [],
29
40
  messageBus: null,
30
- // Map<addresseeName, {askId, askerName, question, reminded}>
31
- // Always keyed by an addressee name. Broadcast asks write one entry
32
- // per named participant, so every pending entry has a concrete
33
- // addressee and the match rule is uniform.
41
+ // Map<askId, {askId, askerName, addresseeName, reminded}>.
34
42
  pendingAsks: new Map(),
35
43
  askIdCounter: 0,
36
44
  };
@@ -38,351 +46,312 @@ export function createOrchestrationContext() {
38
46
 
39
47
  // --- Handler factories ---
40
48
 
41
- /** Create a handler that marks the session as concluded and records the verdict and summary. */
49
+ /** Mark the session as concluded; cancel any open Asks so askers see the synthetic null on their next turn. */
42
50
  export function createConcludeHandler(ctx) {
43
51
  return async ({ verdict, summary }) => {
44
- ctx.concluded = true;
45
- ctx.verdict = verdict;
46
- ctx.summary = summary;
52
+ concludeSession(ctx, { verdict, summary, reason: "session concluded" });
47
53
  return { content: [{ type: "text", text: "Session concluded." }] };
48
54
  };
49
55
  }
50
56
 
51
- /** Create a handler that queues a redirect to interrupt a participant with replacement instructions. */
52
- export function createRedirectHandler(ctx) {
53
- return async ({ message, to }) => {
54
- ctx.redirect = { message, to: to ?? null };
55
- return { content: [{ type: "text", text: "Redirect queued." }] };
56
- };
57
+ /**
58
+ * Shared terminal-tool helper. Conclude / Adjourn / Recess all set the
59
+ * same three context fields (`concluded`, `verdict`, `summary`) and
60
+ * cancel any in-flight Asks for the same reason: nobody will ever
61
+ * answer them now. Mode-specific handlers (Adjourn, Recess) layer
62
+ * extra state on top before calling this.
63
+ */
64
+ export function concludeSession(ctx, { verdict, summary, reason }) {
65
+ ctx.concluded = true;
66
+ ctx.verdict = verdict;
67
+ ctx.summary = summary;
68
+ cancelPendingAsks(ctx, reason);
57
69
  }
58
70
 
59
- /** Create a handler that returns the list of all session participants and their roles. */
71
+ /** Return the list of participants and their roles. */
60
72
  export function createRollCallHandler(ctx) {
61
- return async () => {
62
- return {
63
- content: [{ type: "text", text: JSON.stringify(ctx.participants) }],
64
- };
65
- };
73
+ return async () => ({
74
+ content: [{ type: "text", text: JSON.stringify(ctx.participants) }],
75
+ });
76
+ }
77
+
78
+ function resolveAddressees(ctx, { from, to, defaultTo }) {
79
+ const explicitTo = typeof to === "string" && to.length > 0 ? to : null;
80
+ const effectiveTo = explicitTo ?? defaultTo ?? null;
81
+ if (effectiveTo) return [effectiveTo];
82
+ return ctx.participants.map((p) => p.name).filter((n) => n !== from);
83
+ }
84
+
85
+ function registerPendingAsk(ctx, { from, addressee, question }) {
86
+ const askId = ++ctx.askIdCounter;
87
+ ctx.pendingAsks.set(askId, {
88
+ askId,
89
+ askerName: from,
90
+ addresseeName: addressee,
91
+ reminded: false,
92
+ });
93
+ ctx.messageBus.ask(from, addressee, question, askId);
94
+ return askId;
66
95
  }
67
96
 
68
97
  /**
69
- * Create an Ask handler for a given caller. Ask registers a pending-ask
70
- * in ctx and routes the question to the addressee via the message bus.
98
+ * Create an Ask handler. Registers a pending entry per addressee, posts
99
+ * the ask on the bus, returns `{askIds:[…]}` immediately. The LLM uses
100
+ * those ids to match the `[answer#N]` it sees on a later turn.
71
101
  *
72
102
  * @param {object} ctx
73
103
  * @param {object} opts
74
- * @param {string} opts.from - Canonical name of the asker.
75
- * @param {string|undefined} opts.defaultTo - Default addressee when the
76
- * caller omits `to`. Use `undefined` to signal "broadcast across all
77
- * non-asker participants" (facilitator-only).
104
+ * @param {string} opts.from
105
+ * @param {string|undefined} opts.defaultTo - `undefined` means "broadcast
106
+ * to everyone else"; a participant name means "target that one when
107
+ * `to` is omitted."
78
108
  */
79
109
  export function createAskHandler(ctx, { from, defaultTo }) {
80
110
  return async ({ question, to }) => {
81
- const explicitTo = typeof to === "string" && to.length > 0 ? to : null;
82
- const effectiveTo = explicitTo ?? defaultTo ?? null;
83
-
84
- const addressees = effectiveTo
85
- ? [effectiveTo]
86
- : ctx.participants.map((p) => p.name).filter((name) => name !== from);
87
-
88
- if (addressees.length === 0) {
89
- return {
90
- content: [{ type: "text", text: "No addressee for Ask." }],
91
- isError: true,
92
- };
111
+ if (ctx.concluded) {
112
+ return errorResult("Session is concluded; Ask was not delivered.");
93
113
  }
94
-
95
- for (const addressee of addressees) {
96
- const askId = ++ctx.askIdCounter;
97
- ctx.pendingAsks.set(addressee, {
98
- askId,
99
- askerName: from,
100
- question,
101
- reminded: false,
102
- });
103
- ctx.messageBus.ask(from, addressee, question, askId);
114
+ const addressees = resolveAddressees(ctx, { from, to, defaultTo });
115
+ if (addressees.length === 0) {
116
+ return errorResult("No addressee for Ask.");
104
117
  }
105
-
106
- return { content: [{ type: "text", text: "Ask delivered." }] };
118
+ const askIds = addressees.map((addressee) =>
119
+ registerPendingAsk(ctx, { from, addressee, question }),
120
+ );
121
+ return jsonResult({ askIds });
107
122
  };
108
123
  }
109
124
 
110
125
  /**
111
- * Create an Answer handler for a given caller. Answer clears the caller's
112
- * pending-ask entry (keyed by the caller's canonical name) and routes the
113
- * reply to the original asker via the message bus.
126
+ * Create an Answer handler with optional askId.
114
127
  *
115
- * @param {object} ctx
116
- * @param {object} opts
117
- * @param {string} opts.from - Canonical name of the answerer.
128
+ * - askId provided + matches a pending entry whose addressee is the caller →
129
+ * route the reply to the asker's queue and clear the pending entry.
130
+ * - askId provided but unknown or wrong addressee → `isError`. The caller
131
+ * tried to specify; we tell them why it didn't match.
132
+ * - askId omitted + exactly one ask owed by the caller → auto-pick it.
133
+ * - askId omitted + 0 or many pending → broadcast as Announce so the
134
+ * message still reaches every other participant.
118
135
  */
119
136
  export function createAnswerHandler(ctx, { from }) {
120
- return async ({ message }) => {
121
- const entry = ctx.pendingAsks.get(from);
122
- if (!entry) {
123
- return {
124
- content: [{ type: "text", text: "No pending ask to answer." }],
125
- isError: true,
126
- };
137
+ return async ({ askId, message }) => {
138
+ if (typeof askId === "number") {
139
+ return routeAnswerByAskId(ctx, { from, askId, message });
140
+ }
141
+ const owed = [...ctx.pendingAsks.values()].filter(
142
+ (e) => e.addresseeName === from,
143
+ );
144
+ if (owed.length === 1) {
145
+ return routeAnswerByAskId(ctx, {
146
+ from,
147
+ askId: owed[0].askId,
148
+ message,
149
+ });
127
150
  }
128
- ctx.pendingAsks.delete(from);
129
- ctx.messageBus.answer(from, entry.askerName, message, entry.askId);
130
- return { content: [{ type: "text", text: "Answer delivered." }] };
151
+ ctx.messageBus.announce(from, message);
152
+ const reason =
153
+ owed.length === 0
154
+ ? "no pending ask for you"
155
+ : `${owed.length} pending asks (askId omitted is ambiguous)`;
156
+ return textResult(`Answer routed as Announce — ${reason}.`);
131
157
  };
132
158
  }
133
159
 
134
- /**
135
- * Create an Announce handler. Announce broadcasts a message to every
136
- * participant except the sender; it never touches pendingAsks.
137
- *
138
- * @param {object} ctx
139
- * @param {object} opts
140
- * @param {string} opts.from
141
- */
160
+ function routeAnswerByAskId(ctx, { from, askId, message }) {
161
+ const entry = ctx.pendingAsks.get(askId);
162
+ if (!entry) return errorResult(`No pending ask with askId=${askId}.`);
163
+ if (entry.addresseeName !== from) {
164
+ return errorResult(
165
+ `Ask #${askId} is addressed to ${entry.addresseeName}, not ${from}.`,
166
+ );
167
+ }
168
+ ctx.pendingAsks.delete(askId);
169
+ ctx.messageBus.answer(from, entry.askerName, message, askId);
170
+ return textResult("Answer delivered.");
171
+ }
172
+
173
+ /** Broadcast a message to every participant except the sender. */
142
174
  export function createAnnounceHandler(ctx, { from }) {
143
175
  return async ({ message }) => {
144
176
  ctx.messageBus.announce(from, message);
145
- return { content: [{ type: "text", text: "Announcement delivered." }] };
177
+ return textResult("Announcement delivered.");
146
178
  };
147
179
  }
148
180
 
149
181
  /**
150
- * Shared turn-complete guard. Consulted by Facilitator#runAgent and
151
- * Supervisor#runAgentTurn / #endOfTurnReview before finalising an agent's
152
- * turn. Returns "advance" when no pending-ask is owed by `addresseeName`;
153
- * "recheck" after queueing a single synthetic reminder; "advance" after
154
- * emitting a protocol_violation event and injecting a synthetic null
155
- * answer so the original asker unblocks.
182
+ * Cancel pending Asks and route a synthetic `[no answer: <reason>]` to
183
+ * each asker's queue so callers never deadlock on a participant ignoring
184
+ * its inbox.
156
185
  *
157
- * @param {object} args
158
- * @param {object} args.ctx
159
- * @param {object} args.messageBus
160
- * @param {string} args.addresseeName
161
- * @param {"facilitated"|"supervised"} args.mode
162
- * @param {(event: object) => void} args.emitViolation
163
- * @returns {"advance"|"recheck"}
186
+ * @param {object} ctx
187
+ * @param {string} reason - Surfaced inside `[no answer: <reason>]`.
188
+ * @param {string} [addressee] - When set, only cancel asks owed by this
189
+ * addressee. Omit to cancel every pending ask.
164
190
  */
165
- export function checkPendingAsk({
166
- ctx,
167
- messageBus,
168
- addresseeName,
169
- mode,
170
- emitViolation,
171
- }) {
172
- const entry = ctx.pendingAsks.get(addresseeName);
173
- if (!entry) return "advance";
174
-
175
- if (!entry.reminded) {
176
- entry.reminded = true;
177
- messageBus.synthetic(
178
- addresseeName,
179
- `You have an unanswered ask from ${entry.askerName}. Reply via Answer.`,
180
- );
181
- return "recheck";
191
+ export function cancelPendingAsks(ctx, reason, addressee) {
192
+ const text = `[no answer: ${reason}]`;
193
+ for (const [askId, entry] of [...ctx.pendingAsks]) {
194
+ if (addressee && entry.addresseeName !== addressee) continue;
195
+ ctx.pendingAsks.delete(askId);
196
+ ctx.messageBus.answer("@orchestrator", entry.askerName, text, askId);
182
197
  }
198
+ }
183
199
 
184
- emitViolation({
185
- type: "protocol_violation",
186
- agent: addresseeName,
187
- askId: entry.askId,
188
- mode,
189
- });
190
- messageBus.answer(
191
- "@orchestrator",
192
- entry.askerName,
193
- `[no answer: ${addresseeName} did not reply to ask ${entry.askId}]`,
194
- entry.askId,
200
+ /** Return the list of pending Asks the named participant owes an Answer to. */
201
+ export function pendingAsksOwedBy(ctx, addressee) {
202
+ return [...ctx.pendingAsks.values()].filter(
203
+ (e) => e.addresseeName === addressee,
195
204
  );
196
- ctx.pendingAsks.delete(addresseeName);
197
- return "advance";
198
205
  }
199
206
 
200
- // --- Per-role MCP server factories ---
201
-
202
207
  /**
203
- * Supervisor tools: Ask + Announce + Conclude + Redirect + RollCall.
204
- * @param {object} ctx - Orchestration context
205
- * @returns {object} MCP server config (type: "sdk")
208
+ * Inject a synthetic reminder onto the addressee's bus queue and mark
209
+ * each owed ask as reminded. Returns true when a reminder fired.
206
210
  */
207
- export function createSupervisorToolServer(ctx) {
208
- return createSdkMcpServer({
209
- name: "orchestration",
210
- tools: [
211
- tool(
212
- "Ask",
213
- "Send a question to the agent. The reply arrives via Answer.",
214
- { question: z.string() },
215
- createAskHandler(ctx, { from: "supervisor", defaultTo: "agent" }),
216
- ),
217
- tool(
218
- "Announce",
219
- "Broadcast a message with no reply expected.",
220
- { message: z.string() },
221
- createAnnounceHandler(ctx, { from: "supervisor" }),
222
- ),
223
- tool(
224
- "Conclude",
225
- "End the session with a verdict and a summary. verdict='success' if the agent's work meets the criteria stated in the task; 'failure' otherwise.",
226
- { verdict: z.enum(["success", "failure"]), summary: z.string() },
227
- createConcludeHandler(ctx),
228
- ),
229
- tool(
230
- "Redirect",
231
- "Interrupt the agent with replacement instructions.",
232
- { message: z.string(), to: z.string().optional() },
233
- createRedirectHandler(ctx),
234
- ),
235
- tool(
236
- "RollCall",
237
- "List all participants in the session.",
238
- {},
239
- createRollCallHandler(ctx),
240
- ),
241
- ],
242
- });
211
+ export function remindOwedAsks(ctx, addressee) {
212
+ const owed = pendingAsksOwedBy(ctx, addressee).filter((e) => !e.reminded);
213
+ if (owed.length === 0) return false;
214
+ for (const entry of owed) entry.reminded = true;
215
+ const lines = owed.map(
216
+ (e) =>
217
+ `You have an unanswered ask from ${e.askerName} (askId=${e.askId}). Reply with Answer(message=…, askId=${e.askId}).`,
218
+ );
219
+ ctx.messageBus.synthetic(addressee, lines.join("\n"));
220
+ return true;
243
221
  }
244
222
 
245
- /**
246
- * Supervised agent tools: Ask + Answer + Announce + RollCall.
247
- * @param {object} ctx - Orchestration context
248
- * @returns {object} MCP server config (type: "sdk")
249
- */
250
- export function createSupervisedAgentToolServer(ctx) {
251
- return createSdkMcpServer({
252
- name: "orchestration",
253
- tools: [
254
- tool(
255
- "Ask",
256
- "Send a question to the supervisor. The reply arrives via Answer.",
257
- { question: z.string() },
258
- createAskHandler(ctx, { from: "agent", defaultTo: "supervisor" }),
259
- ),
260
- tool(
261
- "Answer",
262
- "Reply to an ask addressed to you.",
263
- { message: z.string() },
264
- createAnswerHandler(ctx, { from: "agent" }),
265
- ),
266
- tool(
267
- "Announce",
268
- "Broadcast a message with no reply expected.",
269
- { message: z.string() },
270
- createAnnounceHandler(ctx, { from: "agent" }),
271
- ),
272
- tool(
273
- "RollCall",
274
- "List all participants in the session.",
275
- {},
276
- createRollCallHandler(ctx),
277
- ),
278
- ],
279
- });
223
+ // --- Tool descriptions (shared across roles) ---
224
+
225
+ const ASK_DESC_BROADCAST =
226
+ "Send a question to one named participant, or omit 'to' to broadcast to every other participant. Returns {askIds:[…]} immediately; the reply arrives on a later turn as `[answer#N] <from>: <text>` in your inbox.";
227
+
228
+ const ASK_DESC_TARGETED = (target) =>
229
+ `Send a question to ${target}. Returns {askIds:[N]} immediately; the reply arrives on a later turn as \`[answer#N] ${target}: <text>\` in your inbox.`;
230
+
231
+ const ANSWER_DESC =
232
+ "Reply to an ask addressed to you. Quote askId from the [ask#N] tag on the question; omit it and the handler auto-picks the only pending ask, or routes your message as an Announce when 0 or many are pending.";
233
+
234
+ const ANNOUNCE_DESC = "Broadcast a message with no reply expected.";
235
+
236
+ const ROLLCALL_DESC = "List all participants in the session.";
237
+
238
+ const CONCLUDE_DESC =
239
+ "End the session with a verdict ('success' or 'failure') and a summary.";
240
+
241
+ // --- Tool builders ---
242
+
243
+ /** Helper utilities for handler return values. */
244
+ function textResult(text) {
245
+ return { content: [{ type: "text", text }] };
246
+ }
247
+ function errorResult(text) {
248
+ return { content: [{ type: "text", text }], isError: true };
249
+ }
250
+ function jsonResult(obj) {
251
+ return { content: [{ type: "text", text: JSON.stringify(obj) }] };
280
252
  }
281
253
 
282
254
  /**
283
- * Judge tools: Conclude only.
255
+ * Build the four-tool base for any role (lead or participant). Differences
256
+ * across roles live in `from` / `defaultTo` / whether broadcast is allowed.
284
257
  *
285
- * The judge runs a single post-hoc session with no peer participants —
286
- * Ask/Answer/Announce/Redirect/RollCall are all moot. The judge inspects
287
- * the agent's working directory and trace via the host's read-only tools
288
- * and emits its verdict via Conclude.
289
- *
290
- * @param {object} ctx - Orchestration context
291
- * @returns {object} MCP server config (type: "sdk")
258
+ * @param {object} ctx
259
+ * @param {object} opts
260
+ * @param {string} opts.from - Caller's canonical name.
261
+ * @param {string|undefined} opts.defaultTo - Default Ask target; `undefined`
262
+ * means "broadcast across everyone else when `to` is omitted."
263
+ * @param {boolean} opts.broadcast - Whether Ask accepts a `to` field at all.
264
+ * Leads with multiple participants set this true; supervise's
265
+ * single-participant roles set it false.
292
266
  */
293
- export function createJudgeToolServer(ctx) {
294
- return createSdkMcpServer({
295
- name: "orchestration",
296
- tools: [
297
- tool(
298
- "Conclude",
299
- "End the session with a verdict and a summary. verdict='success' if the agent's work meets the criteria stated in the task; 'failure' otherwise.",
300
- { verdict: z.enum(["success", "failure"]), summary: z.string() },
301
- createConcludeHandler(ctx),
302
- ),
303
- ],
304
- });
267
+ function baseTools(ctx, { from, defaultTo, broadcast }) {
268
+ const askSchema = broadcast
269
+ ? { question: z.string(), to: z.string().optional() }
270
+ : { question: z.string() };
271
+ const askDesc = broadcast ? ASK_DESC_BROADCAST : ASK_DESC_TARGETED(defaultTo);
272
+ return [
273
+ tool("Ask", askDesc, askSchema, createAskHandler(ctx, { from, defaultTo })),
274
+ tool(
275
+ "Answer",
276
+ ANSWER_DESC,
277
+ { message: z.string(), askId: z.number().optional() },
278
+ createAnswerHandler(ctx, { from }),
279
+ ),
280
+ tool(
281
+ "Announce",
282
+ ANNOUNCE_DESC,
283
+ { message: z.string() },
284
+ createAnnounceHandler(ctx, { from }),
285
+ ),
286
+ tool("RollCall", ROLLCALL_DESC, {}, createRollCallHandler(ctx)),
287
+ ];
305
288
  }
306
289
 
307
- /**
308
- * Facilitator tools: Ask + Announce + Conclude + RollCall.
309
- *
310
- * Redirect is intentionally omitted. In facilitated mode the facilitator
311
- * can re-Ask a participant to course-correct — Ask overwrites the pending
312
- * slot, giving the agent a proper round-trip path. Redirect (abort +
313
- * direct message) belongs in supervised mode where a single agent is
314
- * steered by a supervisor.
315
- *
316
- * @param {object} ctx - Orchestration context
317
- * @returns {object} MCP server config (type: "sdk")
318
- */
290
+ /** Conclude tool — shared by facilitator + supervisor. */
291
+ function concludeTool(ctx) {
292
+ return tool(
293
+ "Conclude",
294
+ CONCLUDE_DESC,
295
+ { verdict: z.enum(["success", "failure"]), summary: z.string() },
296
+ createConcludeHandler(ctx),
297
+ );
298
+ }
299
+
300
+ const orchestrationServer = (tools) =>
301
+ createSdkMcpServer({ name: "orchestration", tools });
302
+
303
+ // --- Per-role MCP server factories ---
304
+
305
+ /** Supervisor tools: Ask + Answer + Announce + RollCall + Conclude. */
306
+ export function createSupervisorToolServer(ctx) {
307
+ return orchestrationServer([
308
+ ...baseTools(ctx, {
309
+ from: "supervisor",
310
+ defaultTo: "agent",
311
+ broadcast: false,
312
+ }),
313
+ concludeTool(ctx),
314
+ ]);
315
+ }
316
+
317
+ /** Supervised agent tools: Ask + Answer + Announce + RollCall. */
318
+ export function createSupervisedAgentToolServer(ctx) {
319
+ return orchestrationServer(
320
+ baseTools(ctx, {
321
+ from: "agent",
322
+ defaultTo: "supervisor",
323
+ broadcast: false,
324
+ }),
325
+ );
326
+ }
327
+
328
+ /** Facilitator tools: Ask + Answer + Announce + RollCall + Conclude. */
319
329
  export function createFacilitatorToolServer(ctx) {
320
- return createSdkMcpServer({
321
- name: "orchestration",
322
- tools: [
323
- tool(
324
- "Ask",
325
- "Send a question to a participant. Omit 'to' to broadcast. The reply arrives via Answer.",
326
- { question: z.string(), to: z.string().optional() },
327
- createAskHandler(ctx, { from: "facilitator", defaultTo: undefined }),
328
- ),
329
- tool(
330
- "Announce",
331
- "Broadcast a message with no reply expected.",
332
- { message: z.string() },
333
- createAnnounceHandler(ctx, { from: "facilitator" }),
334
- ),
335
- tool(
336
- "Conclude",
337
- "End the session with a verdict and a summary. verdict='success' if the agent's work meets the criteria stated in the task; 'failure' otherwise.",
338
- { verdict: z.enum(["success", "failure"]), summary: z.string() },
339
- createConcludeHandler(ctx),
340
- ),
341
- tool(
342
- "RollCall",
343
- "List all participants in the session.",
344
- {},
345
- createRollCallHandler(ctx),
346
- ),
347
- ],
348
- });
330
+ return orchestrationServer([
331
+ ...baseTools(ctx, {
332
+ from: "facilitator",
333
+ defaultTo: undefined,
334
+ broadcast: true,
335
+ }),
336
+ concludeTool(ctx),
337
+ ]);
338
+ }
339
+
340
+ /** Facilitated agent tools: Ask + Answer + Announce + RollCall. */
341
+ export function createFacilitatedAgentToolServer(ctx, { from }) {
342
+ return orchestrationServer(
343
+ baseTools(ctx, { from, defaultTo: "facilitator", broadcast: true }),
344
+ );
349
345
  }
350
346
 
351
347
  /**
352
- * Facilitated agent tools: Ask + Answer + Announce + RollCall.
353
- * @param {object} ctx - Orchestration context
354
- * @param {object} opts
355
- * @param {string} opts.from - Agent name (canonical, used for handler wiring)
356
- * @returns {object} MCP server config (type: "sdk")
348
+ * Judge tools: Conclude only. The judge runs a single post-hoc session
349
+ * with no peer participants.
357
350
  */
358
- export function createFacilitatedAgentToolServer(ctx, { from }) {
359
- return createSdkMcpServer({
360
- name: "orchestration",
361
- tools: [
362
- tool(
363
- "Ask",
364
- "Send a question to another participant. Omit 'to' to ask the facilitator.",
365
- { question: z.string(), to: z.string().optional() },
366
- createAskHandler(ctx, { from, defaultTo: "facilitator" }),
367
- ),
368
- tool(
369
- "Answer",
370
- "Reply to an ask addressed to you.",
371
- { message: z.string() },
372
- createAnswerHandler(ctx, { from }),
373
- ),
374
- tool(
375
- "Announce",
376
- "Broadcast a message with no reply expected.",
377
- { message: z.string() },
378
- createAnnounceHandler(ctx, { from }),
379
- ),
380
- tool(
381
- "RollCall",
382
- "List all participants in the session.",
383
- {},
384
- createRollCallHandler(ctx),
385
- ),
386
- ],
387
- });
351
+ export function createJudgeToolServer(ctx) {
352
+ return orchestrationServer([concludeTool(ctx)]);
388
353
  }
354
+
355
+ // Re-export the building blocks discuss-tools.js needs to assemble its
356
+ // own lead tool surface (it has three extra terminal tools).
357
+ export { baseTools, orchestrationServer };