@open-multi-agent/core 1.7.0 → 1.8.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/README.md +49 -21
- package/dist/dashboard/render-team-run-dashboard.d.ts.map +1 -1
- package/dist/dashboard/render-team-run-dashboard.js +5 -1
- package/dist/dashboard/render-team-run-dashboard.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/bedrock.d.ts +1 -1
- package/dist/llm/bedrock.d.ts.map +1 -1
- package/dist/llm/bedrock.js +58 -25
- package/dist/llm/bedrock.js.map +1 -1
- package/dist/memory/checkpoint.d.ts +28 -0
- package/dist/memory/checkpoint.d.ts.map +1 -0
- package/dist/memory/checkpoint.js +95 -0
- package/dist/memory/checkpoint.js.map +1 -0
- package/dist/memory/shared.d.ts +25 -1
- package/dist/memory/shared.d.ts.map +1 -1
- package/dist/memory/shared.js +74 -1
- package/dist/memory/shared.js.map +1 -1
- package/dist/orchestrator/orchestrator.d.ts +44 -19
- package/dist/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/orchestrator.js +357 -78
- package/dist/orchestrator/orchestrator.js.map +1 -1
- package/dist/task/queue.d.ts +18 -1
- package/dist/task/queue.d.ts.map +1 -1
- package/dist/task/queue.js +100 -0
- package/dist/task/queue.js.map +1 -1
- package/dist/types.d.ts +140 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -3
|
@@ -49,6 +49,8 @@ import { registerBuiltInTools } from '../tool/built-in/index.js';
|
|
|
49
49
|
import { defaultWorkspaceDir } from '../tool/built-in/path-safety.js';
|
|
50
50
|
import { Team } from '../team/team.js';
|
|
51
51
|
import { TaskQueue } from '../task/queue.js';
|
|
52
|
+
import { Checkpoint } from '../memory/checkpoint.js';
|
|
53
|
+
import { InMemoryStore } from '../memory/store.js';
|
|
52
54
|
import { createTask, validateTaskDependencies } from '../task/task.js';
|
|
53
55
|
import { extractJSON, validateOutput } from '../agent/structured-output.js';
|
|
54
56
|
import { Scheduler } from './scheduler.js';
|
|
@@ -292,6 +294,53 @@ export async function executeWithRetry(run, task, onRetry, delayFn = sleep) {
|
|
|
292
294
|
toolCalls: [],
|
|
293
295
|
};
|
|
294
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Resolve a parsed task spec's `verify` field into a full
|
|
299
|
+
* {@link ConsensusVerifyOptions} (or `undefined` when no verify should run).
|
|
300
|
+
*
|
|
301
|
+
* - Full `ConsensusVerifyOptions` (already has `judges`): used as-is.
|
|
302
|
+
* - `true` or `CoordinatorVerifySpec` (no `judges`): merged with
|
|
303
|
+
* `verifyJudges` when provided; ignored when `verifyJudges` is absent.
|
|
304
|
+
* - `undefined`: no verify.
|
|
305
|
+
*/
|
|
306
|
+
function resolveVerify(spec, verifyJudges) {
|
|
307
|
+
if (spec === undefined)
|
|
308
|
+
return undefined;
|
|
309
|
+
if (spec !== true && 'judges' in spec)
|
|
310
|
+
return spec;
|
|
311
|
+
if (!verifyJudges || verifyJudges.length === 0)
|
|
312
|
+
return undefined;
|
|
313
|
+
const partial = spec === true ? {} : spec;
|
|
314
|
+
return {
|
|
315
|
+
judges: verifyJudges,
|
|
316
|
+
...(partial.mode !== undefined ? { mode: partial.mode } : {}),
|
|
317
|
+
...(partial.quorum !== undefined ? { quorum: partial.quorum } : {}),
|
|
318
|
+
...(partial.maxRounds !== undefined ? { maxRounds: partial.maxRounds } : {}),
|
|
319
|
+
...(partial.onDissent !== undefined ? { onDissent: partial.onDissent } : {}),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Parse the coordinator-emitted `verify` field on a task JSON object.
|
|
324
|
+
* Accepts `true` (use all defaults) or a partial object with `mode`,
|
|
325
|
+
* `quorum`, `maxRounds`, and/or `onDissent`. Returns `undefined` for any
|
|
326
|
+
* other value so missing / null / unrecognised values are ignored safely.
|
|
327
|
+
*/
|
|
328
|
+
function parseCoordinatorVerify(raw) {
|
|
329
|
+
if (raw === true)
|
|
330
|
+
return true;
|
|
331
|
+
if (typeof raw !== 'object' || raw === null)
|
|
332
|
+
return undefined;
|
|
333
|
+
const obj = raw;
|
|
334
|
+
const mode = obj['mode'] === 'refute' || obj['mode'] === 'lens' ? obj['mode'] : undefined;
|
|
335
|
+
const quorum = typeof obj['quorum'] === 'number' && obj['quorum'] >= 1 ? Math.floor(obj['quorum']) : undefined;
|
|
336
|
+
const maxRounds = typeof obj['maxRounds'] === 'number' && obj['maxRounds'] >= 1 ? Math.floor(obj['maxRounds']) : undefined;
|
|
337
|
+
const onDissent = obj['onDissent'] === 'revise' || obj['onDissent'] === 'reject' || obj['onDissent'] === 'keep'
|
|
338
|
+
? obj['onDissent']
|
|
339
|
+
: undefined;
|
|
340
|
+
if (mode === undefined && quorum === undefined && maxRounds === undefined && onDissent === undefined)
|
|
341
|
+
return true;
|
|
342
|
+
return { mode, quorum, maxRounds, onDissent };
|
|
343
|
+
}
|
|
295
344
|
/**
|
|
296
345
|
* Attempt to extract a JSON array of task specs from the coordinator's raw
|
|
297
346
|
* output. The coordinator is prompted to emit JSON inside a ```json … ``` fence
|
|
@@ -336,6 +385,7 @@ function parseTaskSpecs(raw) {
|
|
|
336
385
|
priority: obj['priority'] === 'low' || obj['priority'] === 'normal' || obj['priority'] === 'high' || obj['priority'] === 'critical'
|
|
337
386
|
? obj['priority']
|
|
338
387
|
: undefined,
|
|
388
|
+
verify: parseCoordinatorVerify(obj['verify']),
|
|
339
389
|
});
|
|
340
390
|
}
|
|
341
391
|
return specs.length > 0 ? specs : null;
|
|
@@ -473,6 +523,54 @@ function buildTaskAgentTeamInfo(ctx, taskId, traceBase, delegationDepth, delegat
|
|
|
473
523
|
runDelegatedAgent,
|
|
474
524
|
};
|
|
475
525
|
}
|
|
526
|
+
async function saveRunCheckpoint(queue, ctx) {
|
|
527
|
+
const active = ctx.checkpoint;
|
|
528
|
+
if (!active)
|
|
529
|
+
return;
|
|
530
|
+
// Best-effort: a checkpoint write must never take down the run it protects.
|
|
531
|
+
// Both snapshot construction and the store write are guarded, so a failing
|
|
532
|
+
// store (e.g. a transient Redis/SQLite error) is surfaced via `onProgress`
|
|
533
|
+
// and the run continues — the next completed task retries the write.
|
|
534
|
+
const save = async () => {
|
|
535
|
+
const sharedMem = ctx.team.getSharedMemoryInstance();
|
|
536
|
+
const completedTaskResults = queue.getByStatus('completed').map((task) => ({
|
|
537
|
+
taskId: task.id,
|
|
538
|
+
...(task.assignee !== undefined ? { assignee: task.assignee } : {}),
|
|
539
|
+
...(task.result !== undefined ? { result: task.result } : {}),
|
|
540
|
+
}));
|
|
541
|
+
const snapshot = {
|
|
542
|
+
version: 1,
|
|
543
|
+
mode: active.mode,
|
|
544
|
+
createdAt: new Date().toISOString(),
|
|
545
|
+
...(active.runId !== undefined ? { runId: active.runId } : {}),
|
|
546
|
+
...(active.goal !== undefined ? { goal: active.goal } : {}),
|
|
547
|
+
queue: queue.snapshot(),
|
|
548
|
+
// When the checkpoint store IS the shared-memory store, the entries are
|
|
549
|
+
// already durable there — embedding a full snapshot on every task would
|
|
550
|
+
// be ~O(N^2) write volume. Persist only the turn counter (cheap) so TTL
|
|
551
|
+
// expiry stays correct; restore reads the entries straight from the store.
|
|
552
|
+
...(sharedMem && !active.reusesSharedMemoryStore
|
|
553
|
+
? { sharedMemory: await sharedMem.snapshot() }
|
|
554
|
+
: {}),
|
|
555
|
+
...(sharedMem ? { turnCount: sharedMem.getTurnCount() } : {}),
|
|
556
|
+
completedTaskResults,
|
|
557
|
+
};
|
|
558
|
+
await active.manager.save(snapshot);
|
|
559
|
+
};
|
|
560
|
+
const nextSave = active.saveChain.catch(() => undefined).then(save);
|
|
561
|
+
// Keep the stored chain non-rejecting so a failed save never leaves an
|
|
562
|
+
// unhandled rejection or blocks the next checkpoint in the chain.
|
|
563
|
+
active.saveChain = nextSave.catch(() => undefined);
|
|
564
|
+
try {
|
|
565
|
+
await nextSave;
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
ctx.config.onProgress?.({
|
|
569
|
+
type: 'error',
|
|
570
|
+
data: { kind: 'checkpoint_save_failed', error },
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
476
574
|
/**
|
|
477
575
|
* Execute all tasks in `queue` using agents in `pool`, respecting dependencies
|
|
478
576
|
* and running independent tasks in parallel.
|
|
@@ -690,6 +788,7 @@ async function executeQueue(queue, ctx) {
|
|
|
690
788
|
}
|
|
691
789
|
const completedTask = queue.complete(task.id, effective.output);
|
|
692
790
|
completedThisRound.push(completedTask);
|
|
791
|
+
await saveRunCheckpoint(queue, ctx);
|
|
693
792
|
config.onProgress?.({
|
|
694
793
|
type: 'task_complete',
|
|
695
794
|
task: task.id,
|
|
@@ -1057,6 +1156,7 @@ async function runTaskVerify(task, assignee, result, sharedMem, ctx) {
|
|
|
1057
1156
|
export class OpenMultiAgent {
|
|
1058
1157
|
config;
|
|
1059
1158
|
teams = new Map();
|
|
1159
|
+
fallbackCheckpointStore = new InMemoryStore();
|
|
1060
1160
|
completedTaskCount = 0;
|
|
1061
1161
|
/**
|
|
1062
1162
|
* @param config - Optional top-level configuration.
|
|
@@ -1081,6 +1181,7 @@ export class OpenMultiAgent {
|
|
|
1081
1181
|
defaultCwd: config.defaultCwd === undefined ? defaultWorkspaceDir() : config.defaultCwd,
|
|
1082
1182
|
maxTokenBudget: config.maxTokenBudget,
|
|
1083
1183
|
defaultToolPreset: config.defaultToolPreset,
|
|
1184
|
+
checkpoint: config.checkpoint,
|
|
1084
1185
|
onApproval: config.onApproval,
|
|
1085
1186
|
onPlanReady: config.onPlanReady,
|
|
1086
1187
|
onAgentStream: config.onAgentStream,
|
|
@@ -1270,33 +1371,7 @@ export class OpenMultiAgent {
|
|
|
1270
1371
|
// ------------------------------------------------------------------
|
|
1271
1372
|
// Step 1: Coordinator decomposes goal into tasks
|
|
1272
1373
|
// ------------------------------------------------------------------
|
|
1273
|
-
const coordinatorBaseConfig =
|
|
1274
|
-
name: 'coordinator',
|
|
1275
|
-
model: coordinatorOverrides?.model ?? this.config.defaultModel,
|
|
1276
|
-
...(coordinatorOverrides?.adapter !== undefined ? { adapter: coordinatorOverrides.adapter } : {}),
|
|
1277
|
-
provider: coordinatorOverrides?.provider ?? this.config.defaultProvider,
|
|
1278
|
-
baseURL: coordinatorOverrides?.baseURL ?? this.config.defaultBaseURL,
|
|
1279
|
-
apiKey: coordinatorOverrides?.apiKey ?? this.config.defaultApiKey,
|
|
1280
|
-
systemPrompt: this.buildCoordinatorPrompt(agentConfigs, coordinatorOverrides),
|
|
1281
|
-
maxTurns: coordinatorOverrides?.maxTurns ?? 3,
|
|
1282
|
-
maxTokens: coordinatorOverrides?.maxTokens,
|
|
1283
|
-
temperature: coordinatorOverrides?.temperature,
|
|
1284
|
-
topP: coordinatorOverrides?.topP,
|
|
1285
|
-
topK: coordinatorOverrides?.topK,
|
|
1286
|
-
minP: coordinatorOverrides?.minP,
|
|
1287
|
-
parallelToolCalls: coordinatorOverrides?.parallelToolCalls,
|
|
1288
|
-
frequencyPenalty: coordinatorOverrides?.frequencyPenalty,
|
|
1289
|
-
presencePenalty: coordinatorOverrides?.presencePenalty,
|
|
1290
|
-
extraBody: coordinatorOverrides?.extraBody,
|
|
1291
|
-
toolPreset: coordinatorOverrides?.toolPreset,
|
|
1292
|
-
tools: coordinatorOverrides?.tools,
|
|
1293
|
-
disallowedTools: coordinatorOverrides?.disallowedTools,
|
|
1294
|
-
cwd: coordinatorOverrides?.cwd === undefined
|
|
1295
|
-
? this.config.defaultCwd
|
|
1296
|
-
: coordinatorOverrides.cwd,
|
|
1297
|
-
loopDetection: coordinatorOverrides?.loopDetection,
|
|
1298
|
-
timeoutMs: coordinatorOverrides?.timeoutMs,
|
|
1299
|
-
};
|
|
1374
|
+
const coordinatorBaseConfig = this.buildCoordinatorBaseConfig(coordinatorOverrides, agentConfigs, (options?.verifyJudges?.length ?? 0) > 0);
|
|
1300
1375
|
const coordinatorConfig = withModelRoute(coordinatorBaseConfig, routeMatches(options?.modelRouting, { phase: 'coordinator', agent: 'coordinator' }));
|
|
1301
1376
|
const decompositionPrompt = this.buildDecompositionPrompt(goal, agentConfigs);
|
|
1302
1377
|
const coordinatorAgent = buildAgent(coordinatorConfig);
|
|
@@ -1333,7 +1408,7 @@ export class OpenMultiAgent {
|
|
|
1333
1408
|
if (taskSpecs && taskSpecs.length > 0) {
|
|
1334
1409
|
// Map title-based dependsOn references to real task IDs so we can
|
|
1335
1410
|
// build the dependency graph before adding tasks to the queue.
|
|
1336
|
-
this.loadSpecsIntoQueue(taskSpecs, agentConfigs, queue);
|
|
1411
|
+
this.loadSpecsIntoQueue(taskSpecs, agentConfigs, queue, options?.verifyJudges);
|
|
1337
1412
|
}
|
|
1338
1413
|
else {
|
|
1339
1414
|
// Coordinator failed to produce structured output — fall back to
|
|
@@ -1355,12 +1430,14 @@ export class OpenMultiAgent {
|
|
|
1355
1430
|
// Step 4: Build pool and execute
|
|
1356
1431
|
// ------------------------------------------------------------------
|
|
1357
1432
|
const pool = this.buildPool(agentConfigs);
|
|
1433
|
+
const activeCheckpoint = this.createActiveCheckpoint(team, options?.checkpoint ?? this.config.checkpoint, 'runTeam', goal);
|
|
1358
1434
|
const ctx = {
|
|
1359
1435
|
team,
|
|
1360
1436
|
pool,
|
|
1361
1437
|
scheduler,
|
|
1362
1438
|
agentResults,
|
|
1363
1439
|
config: this.config,
|
|
1440
|
+
...(activeCheckpoint ? { checkpoint: activeCheckpoint } : {}),
|
|
1364
1441
|
runId,
|
|
1365
1442
|
abortSignal: options?.abortSignal,
|
|
1366
1443
|
cumulativeUsage,
|
|
@@ -1419,6 +1496,7 @@ export class OpenMultiAgent {
|
|
|
1419
1496
|
maxRetries: task.maxRetries,
|
|
1420
1497
|
retryDelayMs: task.retryDelayMs,
|
|
1421
1498
|
retryBackoff: task.retryBackoff,
|
|
1499
|
+
verify: task.verify,
|
|
1422
1500
|
metrics: undefined,
|
|
1423
1501
|
}));
|
|
1424
1502
|
this.config.onProgress?.({
|
|
@@ -1444,39 +1522,25 @@ export class OpenMultiAgent {
|
|
|
1444
1522
|
maxRetries: task.maxRetries,
|
|
1445
1523
|
retryDelayMs: task.retryDelayMs,
|
|
1446
1524
|
retryBackoff: task.retryBackoff,
|
|
1525
|
+
verify: task.verify,
|
|
1447
1526
|
metrics: taskMetrics.get(task.id),
|
|
1448
1527
|
}));
|
|
1449
1528
|
// ------------------------------------------------------------------
|
|
1450
1529
|
// Step 5: Coordinator synthesises final result
|
|
1451
1530
|
// ------------------------------------------------------------------
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1531
|
+
const synthesis = await this.runCoordinatorSynthesis(team, queue, goal, coordinatorBaseConfig, {
|
|
1532
|
+
modelRouting: options?.modelRouting,
|
|
1533
|
+
runId,
|
|
1534
|
+
abortSignal: options?.abortSignal,
|
|
1535
|
+
cumulativeUsage,
|
|
1536
|
+
maxTokenBudget,
|
|
1537
|
+
});
|
|
1538
|
+
if (synthesis === null) {
|
|
1539
|
+
// Aborted or already over budget — return raw task outputs, no synthesis.
|
|
1457
1540
|
return this.buildTeamRunResult(agentResults, goal, taskRecords);
|
|
1458
1541
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
const synthTraceOptions = this.config.onTrace
|
|
1462
|
-
? { onTrace: this.config.onTrace, runId: runId ?? '', traceAgent: 'coordinator' }
|
|
1463
|
-
: undefined;
|
|
1464
|
-
const synthesisResult = await synthesisAgent.run(synthesisPrompt, synthTraceOptions);
|
|
1465
|
-
agentResults.set('coordinator', synthesisResult);
|
|
1466
|
-
cumulativeUsage = addUsage(cumulativeUsage, synthesisResult.tokenUsage);
|
|
1467
|
-
if (maxTokenBudget !== undefined
|
|
1468
|
-
&& cumulativeUsage.input_tokens + cumulativeUsage.output_tokens > maxTokenBudget) {
|
|
1469
|
-
this.config.onProgress?.({
|
|
1470
|
-
type: 'budget_exceeded',
|
|
1471
|
-
agent: 'coordinator',
|
|
1472
|
-
data: new TokenBudgetExceededError('coordinator', cumulativeUsage.input_tokens + cumulativeUsage.output_tokens, maxTokenBudget),
|
|
1473
|
-
});
|
|
1474
|
-
}
|
|
1475
|
-
this.config.onProgress?.({
|
|
1476
|
-
type: 'agent_complete',
|
|
1477
|
-
agent: 'coordinator',
|
|
1478
|
-
data: synthesisResult,
|
|
1479
|
-
});
|
|
1542
|
+
agentResults.set('coordinator', synthesis.result);
|
|
1543
|
+
cumulativeUsage = synthesis.cumulativeUsage;
|
|
1480
1544
|
// Note: coordinator decompose and synthesis are internal meta-steps.
|
|
1481
1545
|
// Only actual user tasks (non-coordinator keys) are counted in
|
|
1482
1546
|
// buildTeamRunResult, so we do not increment completedTaskCount here.
|
|
@@ -1522,8 +1586,8 @@ export class OpenMultiAgent {
|
|
|
1522
1586
|
*
|
|
1523
1587
|
* Task IDs, dependencies, assignees, titles, and descriptions are used exactly
|
|
1524
1588
|
* as stored in the artifact. This is intentionally execution-only; it does not
|
|
1525
|
-
* synthesize a coordinator final answer
|
|
1526
|
-
*
|
|
1589
|
+
* synthesize a coordinator final answer. Durable checkpoints are available
|
|
1590
|
+
* through the same opt-in `checkpoint` option used by `runTasks`.
|
|
1527
1591
|
*/
|
|
1528
1592
|
async runFromPlan(team, plan, options) {
|
|
1529
1593
|
if (plan.version !== 1) {
|
|
@@ -1538,6 +1602,63 @@ export class OpenMultiAgent {
|
|
|
1538
1602
|
queue.addBatch(tasks);
|
|
1539
1603
|
return this.executeExplicitTaskQueue(team, queue, options, plan.goal);
|
|
1540
1604
|
}
|
|
1605
|
+
async restore(team, tasksOrOptions, maybeOptions) {
|
|
1606
|
+
const hasTaskSource = Array.isArray(tasksOrOptions) || this.isPlanArtifact(tasksOrOptions);
|
|
1607
|
+
const options = hasTaskSource ? maybeOptions : tasksOrOptions;
|
|
1608
|
+
const activeCheckpoint = this.createActiveCheckpoint(team, options?.checkpoint ?? this.config.checkpoint ?? true, 'runTasks', options?.goal);
|
|
1609
|
+
const snapshot = activeCheckpoint ? await activeCheckpoint.manager.loadLatest() : null;
|
|
1610
|
+
if (!snapshot) {
|
|
1611
|
+
if (Array.isArray(tasksOrOptions)) {
|
|
1612
|
+
const queue = new TaskQueue();
|
|
1613
|
+
this.loadSpecsIntoQueue(tasksOrOptions.map((t) => ({
|
|
1614
|
+
title: t.title,
|
|
1615
|
+
description: t.description,
|
|
1616
|
+
assignee: t.assignee,
|
|
1617
|
+
dependsOn: t.dependsOn,
|
|
1618
|
+
memoryScope: t.memoryScope,
|
|
1619
|
+
maxRetries: t.maxRetries,
|
|
1620
|
+
retryDelayMs: t.retryDelayMs,
|
|
1621
|
+
retryBackoff: t.retryBackoff,
|
|
1622
|
+
role: t.role,
|
|
1623
|
+
priority: t.priority,
|
|
1624
|
+
verify: t.verify,
|
|
1625
|
+
})), team.getAgents(), queue);
|
|
1626
|
+
return this.executeExplicitTaskQueue(team, queue, options, options?.goal, undefined, activeCheckpoint);
|
|
1627
|
+
}
|
|
1628
|
+
if (this.isPlanArtifact(tasksOrOptions)) {
|
|
1629
|
+
const queue = new TaskQueue();
|
|
1630
|
+
const tasks = this.tasksFromPlan(tasksOrOptions);
|
|
1631
|
+
const validation = validateTaskDependencies(tasks);
|
|
1632
|
+
if (!validation.valid) {
|
|
1633
|
+
throw new Error(`Invalid plan artifact: ${validation.errors.join(' ')}`);
|
|
1634
|
+
}
|
|
1635
|
+
queue.addBatch(tasks);
|
|
1636
|
+
return this.executeExplicitTaskQueue(team, queue, options, tasksOrOptions.goal ?? options?.goal, undefined, activeCheckpoint);
|
|
1637
|
+
}
|
|
1638
|
+
const queue = new TaskQueue();
|
|
1639
|
+
return this.executeExplicitTaskQueue(team, queue, options, options?.goal, undefined, activeCheckpoint);
|
|
1640
|
+
}
|
|
1641
|
+
const sharedMem = team.getSharedMemoryInstance();
|
|
1642
|
+
if (sharedMem && snapshot.sharedMemory) {
|
|
1643
|
+
await sharedMem.restore(snapshot.sharedMemory);
|
|
1644
|
+
}
|
|
1645
|
+
else if (sharedMem && snapshot.turnCount !== undefined) {
|
|
1646
|
+
// Reused-store checkpoint: entries are already in the store; only the
|
|
1647
|
+
// turn counter needs restoring so TTL expiry resumes correctly.
|
|
1648
|
+
sharedMem.setTurnCount(snapshot.turnCount);
|
|
1649
|
+
}
|
|
1650
|
+
const queue = TaskQueue.fromSnapshot(snapshot.queue, { resetInProgress: true });
|
|
1651
|
+
const agentResults = this.agentResultsFromCheckpoint(snapshot, queue);
|
|
1652
|
+
const checkpointForResume = activeCheckpoint
|
|
1653
|
+
? {
|
|
1654
|
+
...activeCheckpoint,
|
|
1655
|
+
mode: snapshot.mode,
|
|
1656
|
+
...(snapshot.goal !== undefined ? { goal: snapshot.goal } : {}),
|
|
1657
|
+
...(snapshot.runId !== undefined ? { runId: snapshot.runId } : {}),
|
|
1658
|
+
}
|
|
1659
|
+
: undefined;
|
|
1660
|
+
return this.executeExplicitTaskQueue(team, queue, options, snapshot.goal ?? options?.goal, agentResults, checkpointForResume, options?.coordinator);
|
|
1661
|
+
}
|
|
1541
1662
|
/**
|
|
1542
1663
|
* Run a team with an explicitly provided task list.
|
|
1543
1664
|
*
|
|
@@ -1685,32 +1806,32 @@ export class OpenMultiAgent {
|
|
|
1685
1806
|
// Private helpers
|
|
1686
1807
|
// -------------------------------------------------------------------------
|
|
1687
1808
|
/** Build the system prompt given to the coordinator agent. */
|
|
1688
|
-
buildCoordinatorSystemPrompt(agents) {
|
|
1809
|
+
buildCoordinatorSystemPrompt(agents, hasVerifyJudges) {
|
|
1689
1810
|
return [
|
|
1690
1811
|
'You are a task coordinator responsible for decomposing high-level goals',
|
|
1691
1812
|
'into concrete, actionable tasks and assigning them to the right team members.',
|
|
1692
1813
|
'',
|
|
1693
1814
|
this.buildCoordinatorRosterSection(agents),
|
|
1694
1815
|
'',
|
|
1695
|
-
this.buildCoordinatorOutputFormatSection(),
|
|
1816
|
+
this.buildCoordinatorOutputFormatSection(hasVerifyJudges),
|
|
1696
1817
|
'',
|
|
1697
1818
|
this.buildCoordinatorSynthesisSection(),
|
|
1698
1819
|
].join('\n');
|
|
1699
1820
|
}
|
|
1700
1821
|
/** Build coordinator system prompt with optional caller overrides. */
|
|
1701
|
-
buildCoordinatorPrompt(agents, config) {
|
|
1822
|
+
buildCoordinatorPrompt(agents, config, hasVerifyJudges) {
|
|
1702
1823
|
if (config?.systemPrompt) {
|
|
1703
1824
|
return [
|
|
1704
1825
|
config.systemPrompt,
|
|
1705
1826
|
'',
|
|
1706
1827
|
this.buildCoordinatorRosterSection(agents),
|
|
1707
1828
|
'',
|
|
1708
|
-
this.buildCoordinatorOutputFormatSection(),
|
|
1829
|
+
this.buildCoordinatorOutputFormatSection(hasVerifyJudges),
|
|
1709
1830
|
'',
|
|
1710
1831
|
this.buildCoordinatorSynthesisSection(),
|
|
1711
1832
|
].join('\n');
|
|
1712
1833
|
}
|
|
1713
|
-
const base = this.buildCoordinatorSystemPrompt(agents);
|
|
1834
|
+
const base = this.buildCoordinatorSystemPrompt(agents, hasVerifyJudges);
|
|
1714
1835
|
if (!config?.instructions) {
|
|
1715
1836
|
return base;
|
|
1716
1837
|
}
|
|
@@ -1732,8 +1853,8 @@ export class OpenMultiAgent {
|
|
|
1732
1853
|
].join('\n');
|
|
1733
1854
|
}
|
|
1734
1855
|
/** Build the coordinator JSON output-format section. */
|
|
1735
|
-
buildCoordinatorOutputFormatSection() {
|
|
1736
|
-
|
|
1856
|
+
buildCoordinatorOutputFormatSection(hasVerifyJudges) {
|
|
1857
|
+
const lines = [
|
|
1737
1858
|
'## Output Format',
|
|
1738
1859
|
'When asked to decompose a goal, respond ONLY with a JSON array of task objects.',
|
|
1739
1860
|
'Each task must have:',
|
|
@@ -1741,17 +1862,12 @@ export class OpenMultiAgent {
|
|
|
1741
1862
|
' - "description": Full task description with context and expected output (string)',
|
|
1742
1863
|
' - "assignee": One of the agent names listed in the roster (string)',
|
|
1743
1864
|
' - "dependsOn": Array of titles of tasks this task depends on (string[], may be empty).',
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
'
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
' 4. When uncertain, prefer fewer dependencies over more — extra parents cost parallelism and tokens.',
|
|
1751
|
-
'',
|
|
1752
|
-
'Wrap the JSON in a ```json code fence.',
|
|
1753
|
-
'Do not include any text outside the code fence.',
|
|
1754
|
-
].join('\n');
|
|
1865
|
+
];
|
|
1866
|
+
if (hasVerifyJudges) {
|
|
1867
|
+
lines.push(' - "verify": (optional) Set to true to apply consensus judge verification on this task\'s result.', ' Or set to an object with any of: "mode" ("refute"|"lens"), "quorum" (number),', ' "maxRounds" (number), "onDissent" ("revise"|"reject"|"keep").', ' Omit for tasks where a single agent\'s answer is sufficient.');
|
|
1868
|
+
}
|
|
1869
|
+
lines.push('', '## Dependency Guidance', 'Prefer the minimum set of upstream tasks each assignee needs. When deciding dependsOn for agent X:', ' 1. Use X\'s system prompt as the primary signal for what inputs it consumes.', ' 2. Lean toward including a task as a dependency only when X\'s system prompt names or describes needing that kind of input.', ' 3. Avoid adding a dependency just because the information "would be useful" or matches general best practice; if X\'s system prompt gives no indication it consumes that input, prefer to leave it out.', ' 4. When uncertain, prefer fewer dependencies over more — extra parents cost parallelism and tokens.', '', 'Wrap the JSON in a ```json code fence.', 'Do not include any text outside the code fence.');
|
|
1870
|
+
return lines.join('\n');
|
|
1755
1871
|
}
|
|
1756
1872
|
/** Build the coordinator synthesis guidance section. */
|
|
1757
1873
|
buildCoordinatorSynthesisSection() {
|
|
@@ -1773,6 +1889,77 @@ export class OpenMultiAgent {
|
|
|
1773
1889
|
'Return ONLY the JSON task array in a ```json code fence.',
|
|
1774
1890
|
].join('\n');
|
|
1775
1891
|
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Build the base coordinator {@link AgentConfig} shared by the decomposition
|
|
1894
|
+
* and synthesis passes. Falls back to orchestrator defaults for any field the
|
|
1895
|
+
* caller's {@link CoordinatorConfig} leaves unset.
|
|
1896
|
+
*/
|
|
1897
|
+
buildCoordinatorBaseConfig(coordinatorOverrides, agentConfigs, hasVerifyJudges) {
|
|
1898
|
+
return {
|
|
1899
|
+
name: 'coordinator',
|
|
1900
|
+
model: coordinatorOverrides?.model ?? this.config.defaultModel,
|
|
1901
|
+
...(coordinatorOverrides?.adapter !== undefined ? { adapter: coordinatorOverrides.adapter } : {}),
|
|
1902
|
+
provider: coordinatorOverrides?.provider ?? this.config.defaultProvider,
|
|
1903
|
+
baseURL: coordinatorOverrides?.baseURL ?? this.config.defaultBaseURL,
|
|
1904
|
+
apiKey: coordinatorOverrides?.apiKey ?? this.config.defaultApiKey,
|
|
1905
|
+
systemPrompt: this.buildCoordinatorPrompt(agentConfigs, coordinatorOverrides, hasVerifyJudges),
|
|
1906
|
+
maxTurns: coordinatorOverrides?.maxTurns ?? 3,
|
|
1907
|
+
maxTokens: coordinatorOverrides?.maxTokens,
|
|
1908
|
+
temperature: coordinatorOverrides?.temperature,
|
|
1909
|
+
topP: coordinatorOverrides?.topP,
|
|
1910
|
+
topK: coordinatorOverrides?.topK,
|
|
1911
|
+
minP: coordinatorOverrides?.minP,
|
|
1912
|
+
parallelToolCalls: coordinatorOverrides?.parallelToolCalls,
|
|
1913
|
+
frequencyPenalty: coordinatorOverrides?.frequencyPenalty,
|
|
1914
|
+
presencePenalty: coordinatorOverrides?.presencePenalty,
|
|
1915
|
+
extraBody: coordinatorOverrides?.extraBody,
|
|
1916
|
+
toolPreset: coordinatorOverrides?.toolPreset,
|
|
1917
|
+
tools: coordinatorOverrides?.tools,
|
|
1918
|
+
disallowedTools: coordinatorOverrides?.disallowedTools,
|
|
1919
|
+
cwd: coordinatorOverrides?.cwd === undefined
|
|
1920
|
+
? this.config.defaultCwd
|
|
1921
|
+
: coordinatorOverrides.cwd,
|
|
1922
|
+
loopDetection: coordinatorOverrides?.loopDetection,
|
|
1923
|
+
timeoutMs: coordinatorOverrides?.timeoutMs,
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Run the coordinator synthesis pass over completed task results. Returns the
|
|
1928
|
+
* synthesis result plus updated cumulative usage, or `null` when synthesis is
|
|
1929
|
+
* skipped (run aborted, or the token budget was already exhausted before the
|
|
1930
|
+
* pass). Emits `budget_exceeded` (when synthesis tips over budget) and
|
|
1931
|
+
* `agent_complete`, mirroring the inline `runTeam` path. Does not mutate
|
|
1932
|
+
* `agentResults` — the caller records the `'coordinator'` entry.
|
|
1933
|
+
*/
|
|
1934
|
+
async runCoordinatorSynthesis(team, queue, goal, coordinatorBaseConfig, opts) {
|
|
1935
|
+
if (opts.abortSignal?.aborted)
|
|
1936
|
+
return null;
|
|
1937
|
+
if (opts.maxTokenBudget !== undefined
|
|
1938
|
+
&& opts.cumulativeUsage.input_tokens + opts.cumulativeUsage.output_tokens > opts.maxTokenBudget) {
|
|
1939
|
+
return null;
|
|
1940
|
+
}
|
|
1941
|
+
const synthesisPrompt = await this.buildSynthesisPrompt(goal, queue.list(), team);
|
|
1942
|
+
const synthesisAgent = buildAgent(withModelRoute(coordinatorBaseConfig, routeMatches(opts.modelRouting, { phase: 'synthesis', agent: 'coordinator' })));
|
|
1943
|
+
const synthTraceOptions = this.config.onTrace
|
|
1944
|
+
? { onTrace: this.config.onTrace, runId: opts.runId ?? '', traceAgent: 'coordinator' }
|
|
1945
|
+
: undefined;
|
|
1946
|
+
const result = await synthesisAgent.run(synthesisPrompt, synthTraceOptions);
|
|
1947
|
+
const cumulativeUsage = addUsage(opts.cumulativeUsage, result.tokenUsage);
|
|
1948
|
+
if (opts.maxTokenBudget !== undefined
|
|
1949
|
+
&& cumulativeUsage.input_tokens + cumulativeUsage.output_tokens > opts.maxTokenBudget) {
|
|
1950
|
+
this.config.onProgress?.({
|
|
1951
|
+
type: 'budget_exceeded',
|
|
1952
|
+
agent: 'coordinator',
|
|
1953
|
+
data: new TokenBudgetExceededError('coordinator', cumulativeUsage.input_tokens + cumulativeUsage.output_tokens, opts.maxTokenBudget),
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
this.config.onProgress?.({
|
|
1957
|
+
type: 'agent_complete',
|
|
1958
|
+
agent: 'coordinator',
|
|
1959
|
+
data: result,
|
|
1960
|
+
});
|
|
1961
|
+
return { result, cumulativeUsage };
|
|
1962
|
+
}
|
|
1776
1963
|
/** Build the synthesis prompt shown to the coordinator after all tasks complete. */
|
|
1777
1964
|
async buildSynthesisPrompt(goal, tasks, team) {
|
|
1778
1965
|
const completedTasks = tasks.filter((t) => t.status === 'completed');
|
|
@@ -1823,18 +2010,20 @@ export class OpenMultiAgent {
|
|
|
1823
2010
|
...(task.retryBackoff !== undefined ? { retryBackoff: task.retryBackoff } : {}),
|
|
1824
2011
|
}));
|
|
1825
2012
|
}
|
|
1826
|
-
async executeExplicitTaskQueue(team, queue, options, goal) {
|
|
2013
|
+
async executeExplicitTaskQueue(team, queue, options, goal, initialAgentResults, activeCheckpoint, coordinatorForSynthesis) {
|
|
1827
2014
|
const agentConfigs = team.getAgents();
|
|
1828
2015
|
const scheduler = new Scheduler('dependency-first');
|
|
1829
2016
|
scheduler.autoAssign(queue, agentConfigs);
|
|
1830
2017
|
const pool = this.buildPool(agentConfigs);
|
|
1831
|
-
const agentResults = new Map();
|
|
2018
|
+
const agentResults = initialAgentResults ?? new Map();
|
|
2019
|
+
const checkpoint = activeCheckpoint ?? this.createActiveCheckpoint(team, options?.checkpoint ?? this.config.checkpoint, 'runTasks', goal);
|
|
1832
2020
|
const ctx = {
|
|
1833
2021
|
team,
|
|
1834
2022
|
pool,
|
|
1835
2023
|
scheduler,
|
|
1836
2024
|
agentResults,
|
|
1837
2025
|
config: this.config,
|
|
2026
|
+
...(checkpoint ? { checkpoint } : {}),
|
|
1838
2027
|
runId: this.config.onTrace ? generateRunId() : undefined,
|
|
1839
2028
|
abortSignal: options?.abortSignal,
|
|
1840
2029
|
cumulativeUsage: ZERO_USAGE,
|
|
@@ -1847,6 +2036,45 @@ export class OpenMultiAgent {
|
|
|
1847
2036
|
taskLeafById: new Map(queue.list().map((task) => [task.id, isLeafTask(task, queue.list())])),
|
|
1848
2037
|
};
|
|
1849
2038
|
await executeQueue(queue, ctx);
|
|
2039
|
+
// A resumed `runTeam` re-runs the coordinator synthesis so the restored
|
|
2040
|
+
// result matches a fresh `runTeam` (a synthesized final answer, not raw
|
|
2041
|
+
// per-task outputs). Best-effort: a missing/unusable coordinator config or
|
|
2042
|
+
// a failing synthesis call must not discard the recovered work — on failure
|
|
2043
|
+
// we surface `synthesis_failed` and fall back to raw outputs.
|
|
2044
|
+
if (checkpoint?.mode === 'runTeam' && goal !== undefined) {
|
|
2045
|
+
try {
|
|
2046
|
+
const coordinatorBaseConfig = this.buildCoordinatorBaseConfig(coordinatorForSynthesis, agentConfigs, false);
|
|
2047
|
+
const synthesis = await this.runCoordinatorSynthesis(team, queue, goal, coordinatorBaseConfig, {
|
|
2048
|
+
modelRouting: options?.modelRouting,
|
|
2049
|
+
runId: ctx.runId,
|
|
2050
|
+
abortSignal: options?.abortSignal,
|
|
2051
|
+
cumulativeUsage: ctx.cumulativeUsage,
|
|
2052
|
+
maxTokenBudget: ctx.maxTokenBudget,
|
|
2053
|
+
});
|
|
2054
|
+
if (synthesis !== null && synthesis.result.success) {
|
|
2055
|
+
agentResults.set('coordinator', synthesis.result);
|
|
2056
|
+
ctx.cumulativeUsage = synthesis.cumulativeUsage;
|
|
2057
|
+
}
|
|
2058
|
+
else if (synthesis !== null) {
|
|
2059
|
+
// Synthesis ran but the coordinator agent failed (e.g. the LLM call
|
|
2060
|
+
// errored). Keep the recovered task outputs and surface the failure
|
|
2061
|
+
// rather than attaching a failed answer under `'coordinator'`.
|
|
2062
|
+
this.config.onProgress?.({
|
|
2063
|
+
type: 'error',
|
|
2064
|
+
data: {
|
|
2065
|
+
kind: 'synthesis_failed',
|
|
2066
|
+
error: new Error(synthesis.result.output || 'coordinator synthesis failed'),
|
|
2067
|
+
},
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
catch (error) {
|
|
2072
|
+
this.config.onProgress?.({
|
|
2073
|
+
type: 'error',
|
|
2074
|
+
data: { kind: 'synthesis_failed', error },
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
1850
2078
|
const taskRecords = queue.list().map((task) => ({
|
|
1851
2079
|
id: task.id,
|
|
1852
2080
|
title: task.title,
|
|
@@ -1858,17 +2086,68 @@ export class OpenMultiAgent {
|
|
|
1858
2086
|
maxRetries: task.maxRetries,
|
|
1859
2087
|
retryDelayMs: task.retryDelayMs,
|
|
1860
2088
|
retryBackoff: task.retryBackoff,
|
|
2089
|
+
verify: task.verify,
|
|
1861
2090
|
metrics: ctx.taskMetrics.get(task.id),
|
|
1862
2091
|
}));
|
|
1863
2092
|
return this.buildTeamRunResult(agentResults, goal, taskRecords);
|
|
1864
2093
|
}
|
|
2094
|
+
createActiveCheckpoint(team, config, mode, goal) {
|
|
2095
|
+
if (config === undefined || config === false)
|
|
2096
|
+
return undefined;
|
|
2097
|
+
const options = config === true ? {} : config;
|
|
2098
|
+
if (options.enabled === false)
|
|
2099
|
+
return undefined;
|
|
2100
|
+
// The instance-level fallback store is shared across every run on this
|
|
2101
|
+
// orchestrator, so concurrent runs would overwrite each other at the
|
|
2102
|
+
// default checkpoint key. Require a `runId` (or an explicit `key`/`store`)
|
|
2103
|
+
// before falling back, so each run resolves to a distinct, resumable key.
|
|
2104
|
+
const sharedStore = team.getSharedMemory();
|
|
2105
|
+
const explicitStore = options.store ?? sharedStore;
|
|
2106
|
+
if (!explicitStore && options.runId === undefined && options.key === undefined) {
|
|
2107
|
+
throw new Error('Checkpoint requires a `runId` (or an explicit `store`/`key`) when the team has no ' +
|
|
2108
|
+
'shared-memory store. Without one, concurrent runs would share the fallback store and ' +
|
|
2109
|
+
"overwrite each other's checkpoint at the default key.");
|
|
2110
|
+
}
|
|
2111
|
+
const store = explicitStore ?? this.fallbackCheckpointStore;
|
|
2112
|
+
return {
|
|
2113
|
+
manager: new Checkpoint(store, options),
|
|
2114
|
+
mode,
|
|
2115
|
+
...(goal !== undefined ? { goal } : {}),
|
|
2116
|
+
...(options.runId !== undefined ? { runId: options.runId } : {}),
|
|
2117
|
+
reusesSharedMemoryStore: sharedStore !== undefined && store === sharedStore,
|
|
2118
|
+
saveChain: Promise.resolve(),
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
agentResultsFromCheckpoint(snapshot, queue) {
|
|
2122
|
+
const taskById = new Map(queue.list().map((task) => [task.id, task]));
|
|
2123
|
+
const agentResults = new Map();
|
|
2124
|
+
for (const completed of snapshot.completedTaskResults) {
|
|
2125
|
+
const task = taskById.get(completed.taskId);
|
|
2126
|
+
const assignee = completed.assignee ?? task?.assignee ?? 'unknown';
|
|
2127
|
+
const output = completed.result ?? task?.result ?? '';
|
|
2128
|
+
agentResults.set(`${assignee}:${completed.taskId}`, {
|
|
2129
|
+
success: true,
|
|
2130
|
+
output,
|
|
2131
|
+
messages: [],
|
|
2132
|
+
tokenUsage: ZERO_USAGE,
|
|
2133
|
+
toolCalls: [],
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
return agentResults;
|
|
2137
|
+
}
|
|
2138
|
+
isPlanArtifact(value) {
|
|
2139
|
+
if (value === null || typeof value !== 'object')
|
|
2140
|
+
return false;
|
|
2141
|
+
const artifact = value;
|
|
2142
|
+
return artifact['version'] === 1 && Array.isArray(artifact['tasks']);
|
|
2143
|
+
}
|
|
1865
2144
|
/**
|
|
1866
2145
|
* Load a list of task specs into a queue.
|
|
1867
2146
|
*
|
|
1868
2147
|
* Handles title-based `dependsOn` references by building a title→id map first,
|
|
1869
2148
|
* then resolving them to real IDs before adding tasks to the queue.
|
|
1870
2149
|
*/
|
|
1871
|
-
loadSpecsIntoQueue(specs, agentConfigs, queue) {
|
|
2150
|
+
loadSpecsIntoQueue(specs, agentConfigs, queue, verifyJudges) {
|
|
1872
2151
|
const agentNames = new Set(agentConfigs.map((a) => a.name));
|
|
1873
2152
|
const normalizeTitle = (title) => title.toLowerCase().trim();
|
|
1874
2153
|
const titleCounts = new Map();
|
|
@@ -1892,7 +2171,7 @@ export class OpenMultiAgent {
|
|
|
1892
2171
|
retryBackoff: spec.retryBackoff,
|
|
1893
2172
|
role: spec.role,
|
|
1894
2173
|
priority: spec.priority,
|
|
1895
|
-
verify: spec.verify,
|
|
2174
|
+
verify: resolveVerify(spec.verify, verifyJudges),
|
|
1896
2175
|
});
|
|
1897
2176
|
const titleKey = normalizeTitle(spec.title);
|
|
1898
2177
|
if ((titleCounts.get(titleKey) ?? 0) === 1) {
|