ennodia 0.1.0-rc.0
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/CONTRIBUTING.md +21 -0
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/bench/README.md +32 -0
- package/bench/bug-recall/run.ts +331 -0
- package/bench/bug-recall/scorer.ts +424 -0
- package/bench/fixtures/bug-recall/001-missing-await/input.ts +33 -0
- package/bench/fixtures/bug-recall/001-missing-await/oracle.json +28 -0
- package/bench/fixtures/bug-recall/001-missing-await/responses/claude-code-solo.md +1 -0
- package/bench/fixtures/bug-recall/001-missing-await/responses/codex-solo.md +1 -0
- package/bench/fixtures/bug-recall/001-missing-await/responses/ennodia-parallel-compare.md +1 -0
- package/bench/fixtures/bug-recall/002-timeout-drain/input.ts +40 -0
- package/bench/fixtures/bug-recall/002-timeout-drain/oracle.json +28 -0
- package/bench/fixtures/bug-recall/002-timeout-drain/responses/claude-code-solo.md +1 -0
- package/bench/fixtures/bug-recall/002-timeout-drain/responses/codex-solo.md +1 -0
- package/bench/fixtures/bug-recall/002-timeout-drain/responses/ennodia-parallel-compare.md +1 -0
- package/bench/fixtures/bug-recall/003-schema-contract/input.ts +22 -0
- package/bench/fixtures/bug-recall/003-schema-contract/oracle.json +28 -0
- package/bench/fixtures/bug-recall/003-schema-contract/responses/claude-code-solo.md +1 -0
- package/bench/fixtures/bug-recall/003-schema-contract/responses/codex-solo.md +1 -0
- package/bench/fixtures/bug-recall/003-schema-contract/responses/ennodia-parallel-compare.md +1 -0
- package/bench/fixtures/bug-recall/004-cancel-propagation/input.ts +28 -0
- package/bench/fixtures/bug-recall/004-cancel-propagation/oracle.json +28 -0
- package/bench/fixtures/bug-recall/004-cancel-propagation/responses/claude-code-solo.md +1 -0
- package/bench/fixtures/bug-recall/004-cancel-propagation/responses/codex-solo.md +1 -0
- package/bench/fixtures/bug-recall/004-cancel-propagation/responses/ennodia-parallel-compare.md +1 -0
- package/bench/results/.gitkeep +1 -0
- package/docs/assets/agents/antigravity-color.svg +1 -0
- package/docs/assets/agents/claudecode-color.svg +1 -0
- package/docs/assets/agents/codex-color.svg +1 -0
- package/docs/assets/agents/opencode-dark.svg +1 -0
- package/docs/assets/agents/opencode-light.svg +1 -0
- package/docs/assets/icons/arrow.svg +1 -0
- package/docs/assets/icons/github.svg +1 -0
- package/docs/assets/icons/link.svg +1 -0
- package/docs/assets/logo-dark.svg +1 -0
- package/docs/assets/logo.svg +1 -0
- package/docs/assets/orchestration-pipeline.svg +74 -0
- package/docs/getting-started.md +107 -0
- package/docs/in-depth/architecture.md +53 -0
- package/docs/in-depth/releasing.md +120 -0
- package/docs/index.md +16 -0
- package/docs/reference/mcp-tools.md +104 -0
- package/package.json +87 -0
- package/src/compare.ts +777 -0
- package/src/diagnosis.ts +161 -0
- package/src/harnesses.ts +302 -0
- package/src/index.ts +52 -0
- package/src/planner.ts +106 -0
- package/src/runs.ts +777 -0
- package/src/server.ts +747 -0
- package/src/tasks.ts +602 -0
- package/src/version.ts +1 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
CompareManager,
|
|
5
|
+
type CompareCandidateInput,
|
|
6
|
+
} from "./compare";
|
|
7
|
+
import {
|
|
8
|
+
discoverHarnesses,
|
|
9
|
+
findHarnessAdapter,
|
|
10
|
+
} from "./harnesses";
|
|
11
|
+
import { planRoute } from "./planner";
|
|
12
|
+
import { RunManager } from "./runs";
|
|
13
|
+
import { TaskManager } from "./tasks";
|
|
14
|
+
import { ENNODIA_VERSION } from "./version";
|
|
15
|
+
|
|
16
|
+
const taskManager = new TaskManager();
|
|
17
|
+
const compareManager = new CompareManager(taskManager, resolveRunnableHarness);
|
|
18
|
+
const runManager = new RunManager({
|
|
19
|
+
taskManager,
|
|
20
|
+
compareManager,
|
|
21
|
+
discoverHarnesses,
|
|
22
|
+
findHarnessAdapter,
|
|
23
|
+
planRoute,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export type EnnodiaShutdownOptions = {
|
|
27
|
+
deadlineMs?: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function createEnnodiaServer(): McpServer {
|
|
31
|
+
const server = new McpServer({
|
|
32
|
+
name: "ennodia",
|
|
33
|
+
version: ENNODIA_VERSION,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
server.registerTool(
|
|
37
|
+
"ennodia_list_harnesses",
|
|
38
|
+
{
|
|
39
|
+
title: "List Ennodia harnesses",
|
|
40
|
+
description:
|
|
41
|
+
"Discover supported local AI harnesses and report availability, runnable state, command path, version, capabilities, and adapter notes. Use first when setup or routing fails.",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
refresh: z
|
|
44
|
+
.boolean()
|
|
45
|
+
.default(false)
|
|
46
|
+
.describe(
|
|
47
|
+
"Re-scan available harnesses instead of returning the short-lived discovery cache.",
|
|
48
|
+
),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
async ({ refresh }) => jsonResult(await discoverHarnesses({ refresh })),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
server.registerTool(
|
|
55
|
+
"ennodia_plan",
|
|
56
|
+
{
|
|
57
|
+
title: "Plan an Ennodia route",
|
|
58
|
+
description:
|
|
59
|
+
"Classify a prompt and preview the route Ennodia would take without starting any child process. Use before ennodia_run when you want to inspect routing.",
|
|
60
|
+
inputSchema: {
|
|
61
|
+
prompt: z
|
|
62
|
+
.string()
|
|
63
|
+
.min(1)
|
|
64
|
+
.describe("The task to classify and route."),
|
|
65
|
+
refresh: z
|
|
66
|
+
.boolean()
|
|
67
|
+
.default(false)
|
|
68
|
+
.describe("Re-scan harnesses before planning."),
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
async ({ prompt, refresh }) => {
|
|
72
|
+
const harnesses = await discoverHarnesses({ refresh });
|
|
73
|
+
return jsonResult(planRoute(prompt, harnesses));
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
server.registerTool(
|
|
78
|
+
"ennodia_start",
|
|
79
|
+
{
|
|
80
|
+
title: "Start an Ennodia task",
|
|
81
|
+
description:
|
|
82
|
+
"Start raw child harness tasks without run-level Compare or final synthesis. Use for debugging, direct harness calls, or manual Compare workflows; prefer ennodia_run for normal end-to-end use.",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
prompt: z
|
|
85
|
+
.string()
|
|
86
|
+
.min(1)
|
|
87
|
+
.describe("The task to send to the selected local harness or harnesses."),
|
|
88
|
+
harnessId: z
|
|
89
|
+
.string()
|
|
90
|
+
.optional()
|
|
91
|
+
.describe(
|
|
92
|
+
"Force one harness by ID, such as codex or claude-code. Omit to let Ennodia pick from the plan.",
|
|
93
|
+
),
|
|
94
|
+
mode: z
|
|
95
|
+
.enum(["single", "parallel"])
|
|
96
|
+
.default("single")
|
|
97
|
+
.describe(
|
|
98
|
+
"single starts the selected or top-ranked harness; parallel starts every planned candidate.",
|
|
99
|
+
),
|
|
100
|
+
cwd: z
|
|
101
|
+
.string()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe("Working directory for child harness commands."),
|
|
104
|
+
model: z
|
|
105
|
+
.string()
|
|
106
|
+
.optional()
|
|
107
|
+
.describe("Optional model override passed through to harnesses that support it."),
|
|
108
|
+
timeoutMs: z
|
|
109
|
+
.number()
|
|
110
|
+
.int()
|
|
111
|
+
.positive()
|
|
112
|
+
.max(60 * 60 * 1000)
|
|
113
|
+
.optional()
|
|
114
|
+
.describe(
|
|
115
|
+
"Per-task timeout in milliseconds. Defaults to the task manager timeout and is capped at one hour.",
|
|
116
|
+
),
|
|
117
|
+
refresh: z
|
|
118
|
+
.boolean()
|
|
119
|
+
.default(false)
|
|
120
|
+
.describe("Re-scan harnesses before planning and starting tasks."),
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
async ({ prompt, harnessId, mode, cwd, model, timeoutMs, refresh }) => {
|
|
124
|
+
const harnesses = await discoverHarnesses({ refresh });
|
|
125
|
+
const plan = planRoute(prompt, harnesses);
|
|
126
|
+
const selectedIds = harnessId
|
|
127
|
+
? [harnessId]
|
|
128
|
+
: mode === "parallel"
|
|
129
|
+
? plan.candidates
|
|
130
|
+
: plan.selected
|
|
131
|
+
? [plan.selected]
|
|
132
|
+
: [];
|
|
133
|
+
|
|
134
|
+
if (selectedIds.length === 0) {
|
|
135
|
+
throw new Error("No runnable harnesses were found.");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const tasks = selectedIds.map((id) => {
|
|
139
|
+
const adapter = findHarnessAdapter(id);
|
|
140
|
+
const discovery = harnesses.find((harness) => harness.id === id);
|
|
141
|
+
|
|
142
|
+
if (!adapter || !discovery) {
|
|
143
|
+
throw new Error(`Unknown harness: ${id}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return taskManager.start(adapter, discovery, {
|
|
147
|
+
prompt,
|
|
148
|
+
cwd,
|
|
149
|
+
model,
|
|
150
|
+
timeoutMs,
|
|
151
|
+
}).task;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return jsonResult({ plan, tasks });
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
server.registerTool(
|
|
159
|
+
"ennodia_run",
|
|
160
|
+
{
|
|
161
|
+
title: "Run Ennodia",
|
|
162
|
+
description:
|
|
163
|
+
"Start a high-level Ennodia orchestration. Use this as the default entrypoint: it plans routing, starts one or more local harness tasks, optionally compares successful outputs, and returns a run ID to inspect with ennodia_get_run.",
|
|
164
|
+
inputSchema: {
|
|
165
|
+
prompt: z
|
|
166
|
+
.string()
|
|
167
|
+
.min(1)
|
|
168
|
+
.describe("The user task to route to local AI harnesses."),
|
|
169
|
+
harnessId: z
|
|
170
|
+
.string()
|
|
171
|
+
.optional()
|
|
172
|
+
.describe(
|
|
173
|
+
"Force one harness by ID, such as codex or claude-code. Omit to let Ennodia plan.",
|
|
174
|
+
),
|
|
175
|
+
mode: z
|
|
176
|
+
.enum(["auto", "single", "parallel"])
|
|
177
|
+
.default("auto")
|
|
178
|
+
.describe(
|
|
179
|
+
"auto follows the planner; single runs one selected harness; parallel runs all candidate harnesses.",
|
|
180
|
+
),
|
|
181
|
+
compare: z
|
|
182
|
+
.union([z.literal("auto"), z.boolean()])
|
|
183
|
+
.default("auto")
|
|
184
|
+
.describe(
|
|
185
|
+
"Whether to run Compare after tasks finish. auto compares when the planner sees value and at least two tasks produce output.",
|
|
186
|
+
),
|
|
187
|
+
cwd: z
|
|
188
|
+
.string()
|
|
189
|
+
.optional()
|
|
190
|
+
.describe("Working directory for child harness commands."),
|
|
191
|
+
model: z
|
|
192
|
+
.string()
|
|
193
|
+
.optional()
|
|
194
|
+
.describe("Optional model override passed to the selected task harnesses."),
|
|
195
|
+
timeoutMs: z
|
|
196
|
+
.number()
|
|
197
|
+
.int()
|
|
198
|
+
.positive()
|
|
199
|
+
.max(60 * 60 * 1000)
|
|
200
|
+
.optional()
|
|
201
|
+
.describe(
|
|
202
|
+
"Per-task timeout in milliseconds. Defaults to the task manager timeout and is capped at one hour.",
|
|
203
|
+
),
|
|
204
|
+
refresh: z
|
|
205
|
+
.boolean()
|
|
206
|
+
.default(false)
|
|
207
|
+
.describe("Re-scan harnesses before planning the run."),
|
|
208
|
+
judgeHarnessId: z
|
|
209
|
+
.string()
|
|
210
|
+
.optional()
|
|
211
|
+
.describe(
|
|
212
|
+
"Harness to use as the Compare judge. Omit to use the default priority: claude-code, codex, antigravity, then opencode.",
|
|
213
|
+
),
|
|
214
|
+
judgeModel: z
|
|
215
|
+
.string()
|
|
216
|
+
.optional()
|
|
217
|
+
.describe("Optional model override for the Compare judge."),
|
|
218
|
+
synthesizerHarnessId: z
|
|
219
|
+
.string()
|
|
220
|
+
.optional()
|
|
221
|
+
.describe(
|
|
222
|
+
"Harness that writes the final synthesized answer. Defaults to the judge harness selection.",
|
|
223
|
+
),
|
|
224
|
+
synthesizerModel: z
|
|
225
|
+
.string()
|
|
226
|
+
.optional()
|
|
227
|
+
.describe("Optional model override for the Compare synthesizer."),
|
|
228
|
+
maxOutputChars: z
|
|
229
|
+
.number()
|
|
230
|
+
.int()
|
|
231
|
+
.nonnegative()
|
|
232
|
+
.max(200_000)
|
|
233
|
+
.optional()
|
|
234
|
+
.describe(
|
|
235
|
+
"Maximum characters from each successful task output to pass into Compare. Use 0 to suppress candidate text.",
|
|
236
|
+
),
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
async ({
|
|
240
|
+
prompt,
|
|
241
|
+
harnessId,
|
|
242
|
+
mode,
|
|
243
|
+
compare,
|
|
244
|
+
cwd,
|
|
245
|
+
model,
|
|
246
|
+
timeoutMs,
|
|
247
|
+
refresh,
|
|
248
|
+
judgeHarnessId,
|
|
249
|
+
judgeModel,
|
|
250
|
+
synthesizerHarnessId,
|
|
251
|
+
synthesizerModel,
|
|
252
|
+
maxOutputChars,
|
|
253
|
+
}) =>
|
|
254
|
+
jsonResult(
|
|
255
|
+
await runManager.start({
|
|
256
|
+
prompt,
|
|
257
|
+
harnessId,
|
|
258
|
+
mode,
|
|
259
|
+
compare,
|
|
260
|
+
cwd,
|
|
261
|
+
model,
|
|
262
|
+
timeoutMs,
|
|
263
|
+
refresh,
|
|
264
|
+
judgeHarnessId,
|
|
265
|
+
judgeModel,
|
|
266
|
+
synthesizerHarnessId,
|
|
267
|
+
synthesizerModel,
|
|
268
|
+
maxOutputChars,
|
|
269
|
+
}),
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
server.registerTool(
|
|
274
|
+
"ennodia_list_runs",
|
|
275
|
+
{
|
|
276
|
+
title: "List Ennodia runs",
|
|
277
|
+
description: "List high-level Ennodia runs started by this MCP server process.",
|
|
278
|
+
inputSchema: {
|
|
279
|
+
includeEvents: z
|
|
280
|
+
.boolean()
|
|
281
|
+
.default(false)
|
|
282
|
+
.describe("Include bounded run event history for each listed run."),
|
|
283
|
+
maxEvents: z
|
|
284
|
+
.number()
|
|
285
|
+
.int()
|
|
286
|
+
.nonnegative()
|
|
287
|
+
.max(300)
|
|
288
|
+
.default(25)
|
|
289
|
+
.describe("Maximum run events to include per run when includeEvents is true."),
|
|
290
|
+
maxAnswerChars: z
|
|
291
|
+
.number()
|
|
292
|
+
.int()
|
|
293
|
+
.nonnegative()
|
|
294
|
+
.max(200_000)
|
|
295
|
+
.default(2_000)
|
|
296
|
+
.describe(
|
|
297
|
+
"Maximum final-answer characters to include per run. Use 0 to omit answer text.",
|
|
298
|
+
),
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
async ({ includeEvents, maxEvents, maxAnswerChars }) =>
|
|
302
|
+
jsonResult(
|
|
303
|
+
runManager.listViews({
|
|
304
|
+
includeEvents,
|
|
305
|
+
maxEvents,
|
|
306
|
+
maxAnswerChars,
|
|
307
|
+
}),
|
|
308
|
+
),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
server.registerTool(
|
|
312
|
+
"ennodia_get_run",
|
|
313
|
+
{
|
|
314
|
+
title: "Get Ennodia run",
|
|
315
|
+
description:
|
|
316
|
+
"Inspect run status, selected harnesses, child task IDs, Compare ID, final answer, events, and ETA.",
|
|
317
|
+
inputSchema: {
|
|
318
|
+
runId: z.string().min(1).describe("Run ID returned by ennodia_run."),
|
|
319
|
+
includeEvents: z
|
|
320
|
+
.boolean()
|
|
321
|
+
.default(true)
|
|
322
|
+
.describe("Include bounded run event history."),
|
|
323
|
+
maxEvents: z
|
|
324
|
+
.number()
|
|
325
|
+
.int()
|
|
326
|
+
.nonnegative()
|
|
327
|
+
.max(300)
|
|
328
|
+
.default(100)
|
|
329
|
+
.describe("Maximum run events to include. Use 0 to omit events."),
|
|
330
|
+
maxAnswerChars: z
|
|
331
|
+
.number()
|
|
332
|
+
.int()
|
|
333
|
+
.nonnegative()
|
|
334
|
+
.max(200_000)
|
|
335
|
+
.default(80_000)
|
|
336
|
+
.describe(
|
|
337
|
+
"Maximum final-answer characters to include. Use 0 to omit answer text.",
|
|
338
|
+
),
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
async ({ runId, includeEvents, maxEvents, maxAnswerChars }) => {
|
|
342
|
+
const run = runManager.get(runId, {
|
|
343
|
+
includeEvents,
|
|
344
|
+
maxEvents,
|
|
345
|
+
maxAnswerChars,
|
|
346
|
+
});
|
|
347
|
+
if (!run) {
|
|
348
|
+
throw new Error(`Unknown run: ${runId}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return jsonResult(run);
|
|
352
|
+
},
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
server.registerTool(
|
|
356
|
+
"ennodia_cancel_run",
|
|
357
|
+
{
|
|
358
|
+
title: "Cancel Ennodia run",
|
|
359
|
+
description: "Cancel a high-level run and any active child task or Compare.",
|
|
360
|
+
inputSchema: {
|
|
361
|
+
runId: z.string().min(1).describe("Run ID returned by ennodia_run."),
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
async ({ runId }) => jsonResult(runManager.cancel(runId)),
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
server.registerTool(
|
|
368
|
+
"ennodia_list_tasks",
|
|
369
|
+
{
|
|
370
|
+
title: "List Ennodia tasks",
|
|
371
|
+
description: "List tasks started by this Ennodia MCP server process.",
|
|
372
|
+
inputSchema: {
|
|
373
|
+
includeOutput: z
|
|
374
|
+
.boolean()
|
|
375
|
+
.default(false)
|
|
376
|
+
.describe("Include bounded stdout and stderr previews for each task."),
|
|
377
|
+
includeEvents: z
|
|
378
|
+
.boolean()
|
|
379
|
+
.default(false)
|
|
380
|
+
.describe("Include bounded task event history for each task."),
|
|
381
|
+
maxOutputChars: z
|
|
382
|
+
.number()
|
|
383
|
+
.int()
|
|
384
|
+
.nonnegative()
|
|
385
|
+
.max(200_000)
|
|
386
|
+
.default(4_000)
|
|
387
|
+
.describe(
|
|
388
|
+
"Maximum stdout and stderr characters to include per task. Use 0 to omit output text.",
|
|
389
|
+
),
|
|
390
|
+
maxEvents: z
|
|
391
|
+
.number()
|
|
392
|
+
.int()
|
|
393
|
+
.nonnegative()
|
|
394
|
+
.max(300)
|
|
395
|
+
.default(25)
|
|
396
|
+
.describe("Maximum task events to include per task when includeEvents is true."),
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
async ({ includeOutput, includeEvents, maxOutputChars, maxEvents }) =>
|
|
400
|
+
jsonResult(
|
|
401
|
+
taskManager.listViews({
|
|
402
|
+
includeOutput,
|
|
403
|
+
includeEvents,
|
|
404
|
+
maxOutputChars,
|
|
405
|
+
maxEvents,
|
|
406
|
+
}),
|
|
407
|
+
),
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
server.registerTool(
|
|
411
|
+
"ennodia_start_compare",
|
|
412
|
+
{
|
|
413
|
+
title: "Start Ennodia Compare",
|
|
414
|
+
description:
|
|
415
|
+
"Compare completed Ennodia task outputs or caller-supplied responses. Runs a judge pass, then a synthesizer pass, and returns a Compare ID to inspect with ennodia_get_compare.",
|
|
416
|
+
inputSchema: {
|
|
417
|
+
prompt: z
|
|
418
|
+
.string()
|
|
419
|
+
.min(1)
|
|
420
|
+
.describe("Original user task or question the candidate responses answer."),
|
|
421
|
+
taskIds: z
|
|
422
|
+
.array(z.string().min(1).describe("Completed Ennodia task ID."))
|
|
423
|
+
.default([])
|
|
424
|
+
.describe(
|
|
425
|
+
"IDs of completed tasks from this server session to include as candidates.",
|
|
426
|
+
),
|
|
427
|
+
responses: z
|
|
428
|
+
.array(
|
|
429
|
+
z.object({
|
|
430
|
+
id: z
|
|
431
|
+
.string()
|
|
432
|
+
.min(1)
|
|
433
|
+
.describe("Stable candidate ID used in judge analysis."),
|
|
434
|
+
label: z
|
|
435
|
+
.string()
|
|
436
|
+
.optional()
|
|
437
|
+
.describe("Human-readable candidate label."),
|
|
438
|
+
text: z.string().describe("Candidate response text to compare."),
|
|
439
|
+
metadata: z
|
|
440
|
+
.record(z.string(), z.unknown())
|
|
441
|
+
.optional()
|
|
442
|
+
.describe("Optional structured metadata to retain with the candidate."),
|
|
443
|
+
}),
|
|
444
|
+
)
|
|
445
|
+
.default([])
|
|
446
|
+
.describe(
|
|
447
|
+
"Freeform text responses to compare directly when you do not have task IDs.",
|
|
448
|
+
),
|
|
449
|
+
judgeHarnessId: z
|
|
450
|
+
.string()
|
|
451
|
+
.optional()
|
|
452
|
+
.describe(
|
|
453
|
+
"Harness to use as the Compare judge. Omit to use the default priority: claude-code, codex, antigravity, then opencode.",
|
|
454
|
+
),
|
|
455
|
+
judgeModel: z
|
|
456
|
+
.string()
|
|
457
|
+
.optional()
|
|
458
|
+
.describe("Optional model override for the Compare judge."),
|
|
459
|
+
synthesizerHarnessId: z
|
|
460
|
+
.string()
|
|
461
|
+
.optional()
|
|
462
|
+
.describe(
|
|
463
|
+
"Harness that writes the final synthesized answer. Defaults to the judge harness selection.",
|
|
464
|
+
),
|
|
465
|
+
synthesizerModel: z
|
|
466
|
+
.string()
|
|
467
|
+
.optional()
|
|
468
|
+
.describe("Optional model override for the Compare synthesizer."),
|
|
469
|
+
cwd: z
|
|
470
|
+
.string()
|
|
471
|
+
.optional()
|
|
472
|
+
.describe("Working directory for judge and synthesizer child tasks."),
|
|
473
|
+
timeoutMs: z
|
|
474
|
+
.number()
|
|
475
|
+
.int()
|
|
476
|
+
.positive()
|
|
477
|
+
.max(60 * 60 * 1000)
|
|
478
|
+
.optional()
|
|
479
|
+
.describe(
|
|
480
|
+
"Per-child timeout for judge and synthesizer tasks in milliseconds, capped at one hour.",
|
|
481
|
+
),
|
|
482
|
+
maxOutputChars: z
|
|
483
|
+
.number()
|
|
484
|
+
.int()
|
|
485
|
+
.nonnegative()
|
|
486
|
+
.max(200_000)
|
|
487
|
+
.optional()
|
|
488
|
+
.describe(
|
|
489
|
+
"Maximum characters from each task output to include as a Compare candidate. Use 0 to suppress task output text.",
|
|
490
|
+
),
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
async ({
|
|
494
|
+
prompt,
|
|
495
|
+
taskIds,
|
|
496
|
+
responses,
|
|
497
|
+
judgeHarnessId,
|
|
498
|
+
judgeModel,
|
|
499
|
+
synthesizerHarnessId,
|
|
500
|
+
synthesizerModel,
|
|
501
|
+
cwd,
|
|
502
|
+
timeoutMs,
|
|
503
|
+
maxOutputChars,
|
|
504
|
+
}) =>
|
|
505
|
+
jsonResult(
|
|
506
|
+
await compareManager.start({
|
|
507
|
+
prompt,
|
|
508
|
+
taskIds,
|
|
509
|
+
responses: responses as CompareCandidateInput[],
|
|
510
|
+
judgeHarnessId,
|
|
511
|
+
judgeModel,
|
|
512
|
+
synthesizerHarnessId,
|
|
513
|
+
synthesizerModel,
|
|
514
|
+
cwd,
|
|
515
|
+
timeoutMs,
|
|
516
|
+
maxOutputChars,
|
|
517
|
+
}),
|
|
518
|
+
),
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
server.registerTool(
|
|
522
|
+
"ennodia_list_compares",
|
|
523
|
+
{
|
|
524
|
+
title: "List Ennodia compares",
|
|
525
|
+
description: "List Compare runs started by this Ennodia MCP server process.",
|
|
526
|
+
inputSchema: {
|
|
527
|
+
includeCandidates: z
|
|
528
|
+
.boolean()
|
|
529
|
+
.default(false)
|
|
530
|
+
.describe("Include bounded candidate response previews for each Compare."),
|
|
531
|
+
includeEvents: z
|
|
532
|
+
.boolean()
|
|
533
|
+
.default(false)
|
|
534
|
+
.describe("Include bounded Compare event history."),
|
|
535
|
+
maxCandidateChars: z
|
|
536
|
+
.number()
|
|
537
|
+
.int()
|
|
538
|
+
.nonnegative()
|
|
539
|
+
.max(200_000)
|
|
540
|
+
.default(2_000)
|
|
541
|
+
.describe(
|
|
542
|
+
"Maximum characters to include per candidate. Use 0 to omit candidate text.",
|
|
543
|
+
),
|
|
544
|
+
maxEvents: z
|
|
545
|
+
.number()
|
|
546
|
+
.int()
|
|
547
|
+
.nonnegative()
|
|
548
|
+
.max(300)
|
|
549
|
+
.default(25)
|
|
550
|
+
.describe(
|
|
551
|
+
"Maximum Compare events to include when includeEvents is true.",
|
|
552
|
+
),
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
async ({ includeCandidates, includeEvents, maxCandidateChars, maxEvents }) =>
|
|
556
|
+
jsonResult(
|
|
557
|
+
compareManager.listViews({
|
|
558
|
+
includeCandidates,
|
|
559
|
+
includeEvents,
|
|
560
|
+
maxCandidateChars,
|
|
561
|
+
maxEvents,
|
|
562
|
+
}),
|
|
563
|
+
),
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
server.registerTool(
|
|
567
|
+
"ennodia_get_compare",
|
|
568
|
+
{
|
|
569
|
+
title: "Get Ennodia Compare",
|
|
570
|
+
description:
|
|
571
|
+
"Inspect Compare status, candidate inputs, judge analysis, final synthesis, child task IDs, and ETA.",
|
|
572
|
+
inputSchema: {
|
|
573
|
+
compareId: z
|
|
574
|
+
.string()
|
|
575
|
+
.min(1)
|
|
576
|
+
.describe("Compare ID returned by ennodia_start_compare or ennodia_run."),
|
|
577
|
+
includeCandidates: z
|
|
578
|
+
.boolean()
|
|
579
|
+
.default(true)
|
|
580
|
+
.describe("Include bounded candidate response previews."),
|
|
581
|
+
includeEvents: z
|
|
582
|
+
.boolean()
|
|
583
|
+
.default(true)
|
|
584
|
+
.describe("Include bounded Compare event history."),
|
|
585
|
+
maxCandidateChars: z
|
|
586
|
+
.number()
|
|
587
|
+
.int()
|
|
588
|
+
.nonnegative()
|
|
589
|
+
.max(200_000)
|
|
590
|
+
.default(8_000)
|
|
591
|
+
.describe(
|
|
592
|
+
"Maximum characters to include per candidate. Use 0 to omit candidate text.",
|
|
593
|
+
),
|
|
594
|
+
maxEvents: z
|
|
595
|
+
.number()
|
|
596
|
+
.int()
|
|
597
|
+
.nonnegative()
|
|
598
|
+
.max(300)
|
|
599
|
+
.default(100)
|
|
600
|
+
.describe("Maximum Compare events to include. Use 0 to omit events."),
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
async ({
|
|
604
|
+
compareId,
|
|
605
|
+
includeCandidates,
|
|
606
|
+
includeEvents,
|
|
607
|
+
maxCandidateChars,
|
|
608
|
+
maxEvents,
|
|
609
|
+
}) => {
|
|
610
|
+
const compare = compareManager.get(compareId, {
|
|
611
|
+
includeCandidates,
|
|
612
|
+
includeEvents,
|
|
613
|
+
maxCandidateChars,
|
|
614
|
+
maxEvents,
|
|
615
|
+
});
|
|
616
|
+
if (!compare) {
|
|
617
|
+
throw new Error(`Unknown compare: ${compareId}`);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return jsonResult(compare);
|
|
621
|
+
},
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
server.registerTool(
|
|
625
|
+
"ennodia_cancel_compare",
|
|
626
|
+
{
|
|
627
|
+
title: "Cancel Ennodia Compare",
|
|
628
|
+
description: "Cancel a running Compare and its active child task.",
|
|
629
|
+
inputSchema: {
|
|
630
|
+
compareId: z
|
|
631
|
+
.string()
|
|
632
|
+
.min(1)
|
|
633
|
+
.describe("Compare ID returned by ennodia_start_compare or ennodia_run."),
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
async ({ compareId }) => jsonResult(compareManager.cancel(compareId)),
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
server.registerTool(
|
|
640
|
+
"ennodia_get_task",
|
|
641
|
+
{
|
|
642
|
+
title: "Get Ennodia task",
|
|
643
|
+
description: "Inspect task status, captured output, events, and ETA.",
|
|
644
|
+
inputSchema: {
|
|
645
|
+
taskId: z
|
|
646
|
+
.string()
|
|
647
|
+
.min(1)
|
|
648
|
+
.describe("Task ID returned by ennodia_start, ennodia_run, or Compare."),
|
|
649
|
+
includeOutput: z
|
|
650
|
+
.boolean()
|
|
651
|
+
.default(true)
|
|
652
|
+
.describe("Include bounded stdout and stderr previews."),
|
|
653
|
+
includeEvents: z
|
|
654
|
+
.boolean()
|
|
655
|
+
.default(true)
|
|
656
|
+
.describe("Include bounded task event history."),
|
|
657
|
+
maxOutputChars: z
|
|
658
|
+
.number()
|
|
659
|
+
.int()
|
|
660
|
+
.nonnegative()
|
|
661
|
+
.max(200_000)
|
|
662
|
+
.default(20_000)
|
|
663
|
+
.describe(
|
|
664
|
+
"Maximum stdout and stderr characters to include. Use 0 to omit output text.",
|
|
665
|
+
),
|
|
666
|
+
maxEvents: z
|
|
667
|
+
.number()
|
|
668
|
+
.int()
|
|
669
|
+
.nonnegative()
|
|
670
|
+
.max(300)
|
|
671
|
+
.default(100)
|
|
672
|
+
.describe("Maximum task events to include. Use 0 to omit events."),
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
async ({ taskId, includeOutput, includeEvents, maxOutputChars, maxEvents }) => {
|
|
676
|
+
const task = taskManager.get(taskId, {
|
|
677
|
+
includeOutput,
|
|
678
|
+
includeEvents,
|
|
679
|
+
maxOutputChars,
|
|
680
|
+
maxEvents,
|
|
681
|
+
});
|
|
682
|
+
if (!task) {
|
|
683
|
+
throw new Error(`Unknown task: ${taskId}`);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return jsonResult(task);
|
|
687
|
+
},
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
server.registerTool(
|
|
691
|
+
"ennodia_cancel_task",
|
|
692
|
+
{
|
|
693
|
+
title: "Cancel Ennodia task",
|
|
694
|
+
description: "Cancel a running task by task ID.",
|
|
695
|
+
inputSchema: {
|
|
696
|
+
taskId: z
|
|
697
|
+
.string()
|
|
698
|
+
.min(1)
|
|
699
|
+
.describe("Task ID returned by ennodia_start, ennodia_run, or Compare."),
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
async ({ taskId }) => jsonResult(taskManager.cancel(taskId)),
|
|
703
|
+
);
|
|
704
|
+
|
|
705
|
+
return server;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
export async function shutdownEnnodia(
|
|
709
|
+
options: EnnodiaShutdownOptions = {},
|
|
710
|
+
): Promise<void> {
|
|
711
|
+
await runManager.shutdown(options);
|
|
712
|
+
await compareManager.shutdown(options);
|
|
713
|
+
await taskManager.shutdown(options);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function resolveRunnableHarness(harnessId?: string) {
|
|
717
|
+
const harnesses = await discoverHarnesses();
|
|
718
|
+
const preferredIds = harnessId
|
|
719
|
+
? [harnessId]
|
|
720
|
+
: ["claude-code", "codex", "antigravity", "opencode"];
|
|
721
|
+
|
|
722
|
+
for (const id of preferredIds) {
|
|
723
|
+
const adapter = findHarnessAdapter(id);
|
|
724
|
+
const discovery = harnesses.find((harness) => harness.id === id);
|
|
725
|
+
|
|
726
|
+
if (adapter?.buildCommand && discovery?.runnable) {
|
|
727
|
+
return { adapter, discovery };
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
throw new Error(
|
|
732
|
+
harnessId
|
|
733
|
+
? `Harness is not runnable: ${harnessId}`
|
|
734
|
+
: "No runnable harness was found for Compare.",
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function jsonResult(value: unknown) {
|
|
739
|
+
return {
|
|
740
|
+
content: [
|
|
741
|
+
{
|
|
742
|
+
type: "text" as const,
|
|
743
|
+
text: JSON.stringify(value, null, 2),
|
|
744
|
+
},
|
|
745
|
+
],
|
|
746
|
+
};
|
|
747
|
+
}
|