@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.
@@ -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
- if (options?.abortSignal?.aborted) {
1453
- return this.buildTeamRunResult(agentResults, goal, taskRecords);
1454
- }
1455
- if (maxTokenBudget !== undefined
1456
- && cumulativeUsage.input_tokens + cumulativeUsage.output_tokens > maxTokenBudget) {
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
- const synthesisPrompt = await this.buildSynthesisPrompt(goal, queue.list(), team);
1460
- const synthesisAgent = buildAgent(withModelRoute(coordinatorBaseConfig, routeMatches(options?.modelRouting, { phase: 'synthesis', agent: 'coordinator' })));
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 and it does not implement durable
1526
- * checkpoints.
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
- return [
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
- '## Dependency Guidance',
1746
- 'Prefer the minimum set of upstream tasks each assignee needs. When deciding dependsOn for agent X:',
1747
- ' 1. Use X\'s system prompt as the primary signal for what inputs it consumes.',
1748
- ' 2. Lean toward including a task as a dependency only when X\'s system prompt names or describes needing that kind of input.',
1749
- ' 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.',
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) {