@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.
- package/README.md +186 -21
- package/bin/fit-selfedit.js +162 -0
- package/package.json +7 -3
- package/src/agent-runner.js +45 -181
- package/src/benchmark/runner.js +2 -2
- package/src/commands/supervise.js +3 -1
- package/src/discuss-tools.js +72 -140
- package/src/discusser.js +18 -35
- package/src/facilitator.js +26 -43
- package/src/index.js +0 -2
- package/src/judge.js +1 -1
- package/src/message-bus.js +27 -81
- package/src/orchestration-loop.js +176 -229
- package/src/orchestration-toolkit.js +272 -303
- package/src/orchestrator-helpers.js +9 -45
- package/src/redaction.js +2 -0
- package/src/render/orchestrator-filter.js +1 -9
- package/src/supervisor.js +79 -465
|
@@ -1,36 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OrchestrationToolkit — tool schemas, per-role tool sets, and handler
|
|
3
|
-
* factories for orchestration between
|
|
3
|
+
* factories for orchestration between leads (facilitator, supervisor,
|
|
4
|
+
* discuss-lead) and their participating agents.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
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<
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
/**
|
|
71
|
+
/** Return the list of participants and their roles. */
|
|
60
72
|
export function createRollCallHandler(ctx) {
|
|
61
|
-
return async () => {
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
70
|
-
*
|
|
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
|
|
75
|
-
* @param {string|undefined} opts.defaultTo -
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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.
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
177
|
+
return textResult("Announcement delivered.");
|
|
146
178
|
};
|
|
147
179
|
}
|
|
148
180
|
|
|
149
181
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
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}
|
|
158
|
-
* @param {
|
|
159
|
-
* @param {
|
|
160
|
-
*
|
|
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
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
*
|
|
204
|
-
*
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* @param {
|
|
291
|
-
*
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
*
|
|
353
|
-
*
|
|
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
|
|
359
|
-
return
|
|
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 };
|