@dotdrelle/wiki-manager 0.11.9 → 0.12.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.
Files changed (81) hide show
  1. package/README.md +32 -7
  2. package/agents.docker-compose.yml +3 -0
  3. package/docker-compose.yml +1 -0
  4. package/package.json +3 -2
  5. package/src/activity/activityAggregator.js +109 -0
  6. package/src/activity/activityAggregator.test.js +60 -0
  7. package/src/activity/activityDeduplicator.js +50 -0
  8. package/src/activity/progressCalculator.js +61 -0
  9. package/src/activity/runSynthesis.js +15 -0
  10. package/src/agent/graph.js +65 -30
  11. package/src/agent/graph.test.js +28 -5
  12. package/src/cli/wiki-manager.js +22 -0
  13. package/src/contracts/schemas.js +249 -13
  14. package/src/contracts/schemas.test.js +145 -0
  15. package/src/core/activity.js +8 -5
  16. package/src/core/agentEvents.js +260 -23
  17. package/src/core/agentEvents.test.js +53 -0
  18. package/src/core/compose.js +6 -1
  19. package/src/core/dockerCompose.test.js +12 -0
  20. package/src/core/documentIntake.js +33 -38
  21. package/src/core/documentIntake.test.js +25 -0
  22. package/src/core/env.js +11 -1
  23. package/src/core/jobQueue.js +26 -2
  24. package/src/core/mcp.js +1 -1
  25. package/src/core/plan.js +46 -4
  26. package/src/core/plan.test.js +16 -1
  27. package/src/core/planPatch.js +64 -3
  28. package/src/core/planPatch.test.js +110 -1
  29. package/src/core/queueStore.test.js +21 -0
  30. package/src/core/runtimeLog.js +119 -0
  31. package/src/core/runtimeLog.test.js +84 -0
  32. package/src/core/wikirc.js +22 -0
  33. package/src/core/wikirc.test.js +49 -1
  34. package/src/core/workflow.js +14 -3
  35. package/src/graph/graphAggregator.js +5 -0
  36. package/src/graph/graphPatch.js +8 -0
  37. package/src/graph/graphSnapshot.js +14 -0
  38. package/src/graph/graphVisibilityPolicy.js +40 -0
  39. package/src/graph/runGraphProjector.js +87 -0
  40. package/src/graph/runGraphProjector.test.js +111 -0
  41. package/src/orchestrator/agentRegistry.js +172 -0
  42. package/src/orchestrator/agentRegistry.test.js +114 -0
  43. package/src/orchestrator/approvalPolicy.js +126 -0
  44. package/src/orchestrator/approvalPolicy.test.js +78 -0
  45. package/src/orchestrator/assignmentManager.js +80 -0
  46. package/src/orchestrator/attemptManager.js +168 -0
  47. package/src/orchestrator/attemptManager.test.js +160 -0
  48. package/src/orchestrator/budgetManager.js +127 -0
  49. package/src/orchestrator/capabilityRegistry.js +64 -0
  50. package/src/orchestrator/capabilityRegistry.test.js +59 -0
  51. package/src/orchestrator/capabilityResolver.js +151 -0
  52. package/src/orchestrator/capabilityResolver.test.js +127 -0
  53. package/src/orchestrator/dependencyResolver.js +112 -0
  54. package/src/orchestrator/dispatcher.js +302 -0
  55. package/src/orchestrator/lockManager.js +51 -0
  56. package/src/orchestrator/planIntegrator.js +268 -0
  57. package/src/orchestrator/planIntegrator.test.js +213 -0
  58. package/src/orchestrator/planValidator.js +534 -0
  59. package/src/orchestrator/planValidator.test.js +262 -0
  60. package/src/orchestrator/resultAggregator.js +248 -0
  61. package/src/orchestrator/resultAggregator.test.js +211 -0
  62. package/src/orchestrator/scheduler.js +103 -0
  63. package/src/orchestrator/scheduler.test.js +138 -0
  64. package/src/runtime/approvals.js +130 -1
  65. package/src/runtime/donna-contract.test.js +293 -30
  66. package/src/runtime/recoveryManager.js +176 -0
  67. package/src/runtime/recoveryManager.test.js +162 -0
  68. package/src/runtime/runner.e2e.test.js +98 -12
  69. package/src/runtime/runner.js +261 -227
  70. package/src/runtime/runner.test.js +249 -452
  71. package/src/runtime/server.js +137 -20
  72. package/src/runtime/server.test.js +137 -7
  73. package/src/runtime/store.js +837 -2
  74. package/src/runtime/store.test.js +245 -4
  75. package/src/runtime/supervisor.js +33 -1
  76. package/src/runtime/supervisor.test.js +47 -1
  77. package/src/shell/RightPane.tsx +8 -5
  78. package/src/shell/repl.js +4 -9
  79. package/src/shell/repl.test.js +25 -4
  80. package/src/shell/tui.tsx +1 -0
  81. package/src/shell/useSession.ts +38 -10
@@ -209,10 +209,11 @@ test('runRuntimeAgenticWorkflow replans after negative evaluation', async () =>
209
209
  }));
210
210
  return { response: 'Initial done.' };
211
211
  }
212
- if (turnSession.headlessPlan?.[0]?.status === 'pending') {
212
+ const pending = turnSession.headlessPlan?.find((step) => step.status === 'pending');
213
+ if (pending) {
213
214
  dispatchAgentEvent(turnSession, createAgentEvent('plan_step_updated', {
214
215
  origin: 'tool',
215
- payload: { step: 1, status: 'done' },
216
+ payload: { taskId: pending.id, status: 'done' },
216
217
  }));
217
218
  }
218
219
  return { response: 'Export done.' };
@@ -404,515 +405,311 @@ test('runRuntimeAgenticWorkflow stops after replan budget is exhausted', async (
404
405
  assert.equal(session.agentProjection.replans.length, 0);
405
406
  });
406
407
 
407
- test('runRuntimeParallelPlan executes ready tasks concurrently in one parent run', async () => {
408
- const started = [];
409
- const release = {};
408
+ test('runRuntimeParallelPlan dispatches orchestrated tasks without child LLM loops', async () => {
409
+ let llmCalls = 0;
410
+ const executeOrder = [];
411
+ const completed = new Set(['join']);
412
+ const jobs = new Map();
410
413
  const session = {
414
+ workspace: 'demo-workspace',
411
415
  activities: {},
416
+ mcp: {
417
+ production: {
418
+ status: 'connected',
419
+ tools: [
420
+ { name: 'agent_execute' },
421
+ { name: 'agent_status' },
422
+ { name: 'agent_cancel' },
423
+ ],
424
+ },
425
+ },
426
+ agentRegistrySnapshot: [productionAgent()],
427
+ wikircConfig: { capabilityRouting: {} },
428
+ llm: {
429
+ async completeWithTools() {
430
+ llmCalls += 1;
431
+ return { content: '{"ok":true,"reason":"unused"}' };
432
+ },
433
+ },
412
434
  headlessPlan: [
413
- { step: 1, id: 'a', description: 'Task A', status: 'pending', dependsOn: [] },
414
- { step: 2, id: 'b', description: 'Task B', status: 'pending', dependsOn: [] },
415
- { step: 3, id: 'join', description: 'Join', status: 'pending', dependsOn: ['a', 'b'] },
435
+ plannedDoctorTask('a', []),
436
+ plannedDoctorTask('b', []),
437
+ plannedDoctorTask('join', ['a', 'b']),
416
438
  ],
417
439
  };
418
440
  const agent = {
419
- async invoke({ input }) {
420
- const id = input.match(/Task id: ([^\n]+)/)?.[1];
421
- started.push(id);
422
- if (id !== 'join') {
423
- await new Promise((resolve) => { release[id] = resolve; });
424
- }
425
- return { response: `done ${id}` };
441
+ async invoke() {
442
+ assert.fail('orchestrated task execution must not invoke a child Donna loop');
426
443
  },
427
444
  };
445
+ const callTool = async (_mcp, _serverName, toolName, args) => {
446
+ if (toolName === 'agent_execute') {
447
+ executeOrder.push(args.taskId);
448
+ const jobId = `job-${args.taskId}`;
449
+ jobs.set(jobId, { jobId, taskId: args.taskId });
450
+ return toolResult({ accepted: true, jobId, status: 'queued' });
451
+ }
452
+ if (toolName === 'agent_status') {
453
+ const job = jobs.get(args.jobId);
454
+ const done = completed.has(job.taskId);
455
+ return toolResult({
456
+ jobId: job.jobId,
457
+ taskId: job.taskId,
458
+ operation: 'doctor',
459
+ status: done ? 'done' : 'running',
460
+ progress: { percent: done ? 100 : 50 },
461
+ ...(done ? {
462
+ result: {
463
+ status: 'succeeded',
464
+ outputRefs: [{ type: 'file', ref: `out/${job.taskId}.json` }],
465
+ metrics: { durationMs: 1 },
466
+ },
467
+ } : {}),
468
+ });
469
+ }
470
+ if (toolName === 'agent_cancel') return toolResult({ ok: true });
471
+ throw new Error(`unexpected tool: ${toolName}`);
472
+ };
428
473
 
429
- const running = runRuntimeParallelPlan(agent, session, 'Do work', {
430
- runId: 'run-parallel',
474
+ const running = runRuntimeParallelPlan(agent, session, 'Run three tasks', {
475
+ runId: 'run-dispatch',
431
476
  timeoutMs: 1000,
432
477
  maxTurns: 1,
433
478
  concurrency: 2,
479
+ callTool,
480
+ dispatcherPollIntervalMs: 1,
434
481
  });
435
- await waitFor(() => started.includes('a') && started.includes('b'));
482
+ await waitFor(() => executeOrder.includes('a') && executeOrder.includes('b'));
436
483
  assert.deepEqual(session.headlessPlan.slice(0, 2).map((step) => step.status), ['running', 'running']);
437
- release.a();
438
- release.b();
484
+ completed.add('a');
485
+ completed.add('b');
439
486
 
440
487
  const result = await running;
441
488
 
442
489
  assert.equal(result.ok, true);
443
- assert.deepEqual(started, ['a', 'b', 'join']);
490
+ assert.equal(llmCalls, 0);
491
+ assert.deepEqual(executeOrder, ['a', 'b', 'join']);
444
492
  assert.deepEqual(session.headlessPlan.map((step) => step.status), ['done', 'done', 'done']);
445
- assert.ok(session.agentEvents.every((event) => event.runId === 'run-parallel'));
446
- assert.ok(session.agentEvents.some((event) => event.taskId === 'a'));
447
- assert.ok(session.agentEvents.some((event) => event.taskId === 'b'));
448
- });
449
-
450
- test('runRuntimeParallelPlan schedules a plan patch applied during an active parallel run', async () => {
451
- const started = [];
452
- const release = {};
453
- const session = {
454
- activities: {},
455
- headlessPlan: [
456
- { step: 1, id: 'a', description: 'Task A', status: 'pending', dependsOn: [] },
457
- { step: 2, id: 'b', description: 'Task B', status: 'pending', dependsOn: [] },
493
+ assert.deepEqual(session.headlessPlan.map((step) => step.outputRefs?.[0]?.ref), ['out/a.json', 'out/b.json', 'out/join.json']);
494
+ assert.deepEqual(
495
+ session.agentEvents
496
+ .filter((event) => ['task.result_returned', 'task.completed'].includes(event.type))
497
+ .map((event) => `${event.type}:${event.taskId}`),
498
+ [
499
+ 'task.result_returned:a',
500
+ 'task.completed:a',
501
+ 'task.result_returned:b',
502
+ 'task.completed:b',
503
+ 'task.result_returned:join',
504
+ 'task.completed:join',
458
505
  ],
459
- };
460
- const agent = {
461
- async invoke({ input }) {
462
- const id = input.match(/Task id: ([^\n]+)/)?.[1];
463
- started.push(id);
464
- if (id === 'a' || id === 'b') {
465
- await new Promise((resolve) => { release[id] = resolve; });
466
- }
467
- return { response: `done ${id}` };
468
- },
469
- };
470
-
471
- const running = runRuntimeParallelPlan(agent, session, 'Do patched work', {
472
- runId: 'run-patch-active',
473
- timeoutMs: 1000,
474
- maxTurns: 1,
475
- concurrency: 2,
476
- });
477
- await waitFor(() => started.includes('a') && started.includes('b'));
478
- const patch = {
479
- targetRunId: 'run-patch-active',
480
- basePlanRevision: session.agentProjection.planRevision,
481
- operations: [
482
- {
483
- op: 'add_task',
484
- task: {
485
- id: 'c',
486
- description: 'Task C added during run',
487
- status: 'pending',
488
- dependsOn: ['a'],
489
- executor: null,
490
- outputRefs: [],
491
- },
492
- },
493
- ],
494
- };
495
- dispatchAgentEvent(session, createAgentEvent('plan_patch_proposed', {
496
- origin: 'runtime',
497
- runId: 'run-patch-active',
498
- payload: { id: 'patch-active', patch },
499
- }));
500
- dispatchAgentEvent(session, createAgentEvent('plan_patch_approved', {
501
- origin: 'runtime',
502
- runId: 'run-patch-active',
503
- payload: { patchId: 'patch-active' },
504
- }));
505
- dispatchAgentEvent(session, createAgentEvent('plan_patch_applied', {
506
- origin: 'runtime',
507
- runId: 'run-patch-active',
508
- payload: { patchId: 'patch-active', patch },
509
- }));
510
-
511
- assert.deepEqual(session.headlessPlan.map((step) => step.id), ['a', 'b', 'c']);
512
- release.a();
513
- await waitFor(() => started.includes('c'));
514
- release.b();
515
-
516
- const result = await running;
517
-
518
- assert.equal(result.ok, true);
519
- assert.deepEqual(started, ['a', 'b', 'c']);
520
- assert.deepEqual(session.headlessPlan.map((step) => step.status), ['done', 'done', 'done']);
521
- assert.equal(session.headlessPlan.find((step) => step.id === 'c').dependsOn[0], 'a');
506
+ );
507
+ const logs = session.agentProjection.logs.join('\n');
508
+ for (const label of ['READY', 'RESOLVING', 'SELECTED', 'ASSIGNED', 'CREATED', 'ACQUIRED', 'AGENT_EXECUTE', 'ACCEPTED', 'AGENT_STATUS', 'RESULT_RETURNED', 'RELEASED', 'COMPLETED']) {
509
+ assert.match(logs, new RegExp(`\\b${label}\\b`));
510
+ }
511
+ assert.match(logs, /run=run-dispatch/);
512
+ assert.match(logs, /task=a/);
513
+ assert.match(logs, /attempt=a:attempt-1/);
514
+ assert.match(logs, /agentInstance=production-main/);
515
+ assert.match(logs, /job=job-a/);
516
+ assert.match(logs, /workspace=demo-workspace/);
517
+ assert.match(logs, /capability=workspace\.diagnose/);
518
+ assert.match(logs, /operation=doctor/);
522
519
  });
523
520
 
524
- test('runRuntimeParallelPlan rejects a stale patch during an active parallel run without silently applying it', async () => {
525
- const release = {};
521
+ test('runRuntimeParallelPlan fails cleanly when scheduler budget is exceeded', async () => {
526
522
  const session = {
527
- activities: {},
528
- headlessPlan: [
529
- { step: 1, id: 'a', description: 'Task A', status: 'pending', dependsOn: [] },
530
- { step: 2, id: 'b', description: 'Task B', status: 'pending', dependsOn: [] },
531
- ],
523
+ mcp: { tools: {} },
524
+ agentEvents: [],
525
+ headlessPlan: [plannedDoctorTask('a', [])],
532
526
  };
533
527
  const agent = {
534
- async invoke({ input }) {
535
- const id = input.match(/Task id: ([^\n]+)/)?.[1];
536
- await new Promise((resolve) => { release[id] = resolve; });
537
- return { response: `done ${id}` };
528
+ async invoke() {
529
+ assert.fail('budget rejection must happen before child execution');
538
530
  },
539
531
  };
540
532
 
541
- const running = runRuntimeParallelPlan(agent, session, 'Do stale patch work', {
542
- runId: 'run-stale-patch',
533
+ const result = await runRuntimeParallelPlan(agent, session, 'Run one task', {
534
+ runId: 'run-budget',
543
535
  timeoutMs: 1000,
544
536
  maxTurns: 1,
545
- concurrency: 2,
537
+ budgets: { maxTasks: 0 },
546
538
  });
547
- await waitFor(() => release.a && release.b);
548
- const patch = {
549
- targetRunId: 'run-stale-patch',
550
- basePlanRevision: session.agentProjection.planRevision + 99,
551
- operations: [
552
- { op: 'add_task', task: { id: 'c', description: 'Should not apply', dependsOn: ['a'] } },
553
- ],
554
- };
555
- dispatchAgentEvent(session, createAgentEvent('plan_patch_proposed', {
556
- origin: 'runtime',
557
- runId: 'run-stale-patch',
558
- payload: { id: 'patch-stale', patch },
559
- }));
560
- dispatchAgentEvent(session, createAgentEvent('plan_patch_applied', {
561
- origin: 'runtime',
562
- runId: 'run-stale-patch',
563
- payload: { patchId: 'patch-stale', patch },
564
- }));
565
-
566
- assert.deepEqual(session.headlessPlan.map((step) => step.id), ['a', 'b']);
567
- assert.equal(session.agentProjection.planPatches.find((item) => item.id === 'patch-stale').status, 'rejected');
568
- assert.equal(session.agentProjection.planPatches.find((item) => item.id === 'patch-stale').rejectionReason, 'revision_mismatch');
569
- release.a();
570
- release.b();
571
-
572
- const result = await running;
573
539
 
574
- assert.equal(result.ok, true);
575
- assert.deepEqual(session.headlessPlan.map((step) => step.status), ['done', 'done']);
540
+ assert.equal(result.ok, false);
541
+ assert.equal(result.budgetExceeded, true);
542
+ assert.equal(result.reason, 'max_tasks_exceeded');
543
+ assert.ok(session.agentEvents.some((event) => event.type === 'run_error' && event.payload?.budget?.reason === 'max_tasks_exceeded'));
544
+ assert.equal(session.headlessPlan[0].status, 'pending');
576
545
  });
577
546
 
578
- test('runRuntimeParallelPlan aborts a running child task cancelled by plan patch without stopping siblings', async () => {
579
- const started = [];
580
- let bAborted = false;
547
+ test('runRuntimeParallelPlan retries a retryable task on a fallback agent', async () => {
548
+ const executeServers = [];
581
549
  const session = {
582
- activities: {},
583
- headlessPlan: [
584
- { step: 1, id: 'a', description: 'Task A', status: 'pending', dependsOn: [] },
585
- { step: 2, id: 'b', description: 'Task B', status: 'pending', dependsOn: [] },
586
- ],
587
- };
588
- const agent = {
589
- async invoke({ input, signal }) {
590
- const id = input.match(/Task id: ([^\n]+)/)?.[1];
591
- started.push(id);
592
- if (id === 'b') {
593
- await new Promise((resolve, reject) => {
594
- signal.addEventListener('abort', () => {
595
- bAborted = true;
596
- const err = new Error('cancelled');
597
- err.name = 'AbortError';
598
- reject(err);
599
- }, { once: true });
550
+ mcp: {
551
+ cme: { tools: [{ name: 'agent_execute' }, { name: 'agent_status' }, { name: 'agent_cancel' }] },
552
+ commercial: { tools: [{ name: 'agent_execute' }, { name: 'agent_status' }, { name: 'agent_cancel' }] },
553
+ },
554
+ agentEvents: [],
555
+ headlessPlan: [{
556
+ ...plannedDoctorTask('a', []),
557
+ retryPolicy: {
558
+ maxAttempts: 2,
559
+ retryableErrors: ['temporarily_unavailable'],
560
+ allowAgentFallback: true,
561
+ },
562
+ }],
563
+ capabilityRegistry: fallbackRegistry(),
564
+ };
565
+ const jobs = new Map();
566
+ const callTool = async (_mcp, serverName, toolName, args) => {
567
+ if (toolName === 'agent_execute') {
568
+ executeServers.push(serverName);
569
+ const jobId = `${serverName}-job`;
570
+ jobs.set(jobId, { serverName, taskId: args.taskId });
571
+ return toolResult({ accepted: true, jobId, status: 'queued' });
572
+ }
573
+ if (toolName === 'agent_status') {
574
+ const job = jobs.get(args.jobId);
575
+ if (job.serverName === 'cme') {
576
+ return toolResult({
577
+ jobId: args.jobId,
578
+ taskId: job.taskId,
579
+ status: 'failed',
580
+ result: {
581
+ status: 'failed',
582
+ error: { code: 'temporarily_unavailable', message: 'CME unavailable', retryable: true },
583
+ },
600
584
  });
601
585
  }
602
- return { response: `done ${id}` };
603
- },
604
- };
605
-
606
- const running = runRuntimeParallelPlan(agent, session, 'Cancel one child', {
607
- runId: 'run-cancel-task',
608
- timeoutMs: 1000,
609
- maxTurns: 1,
610
- concurrency: 2,
611
- });
612
- await waitFor(() => started.includes('a') && started.includes('b'));
613
- await waitFor(() => session.headlessPlan.find((step) => step.id === 'a')?.status === 'done');
614
- const patch = {
615
- targetRunId: 'run-cancel-task',
616
- basePlanRevision: session.agentProjection.planRevision,
617
- operations: [{ op: 'cancel_task', taskId: 'b' }],
618
- };
619
- dispatchAgentEvent(session, createAgentEvent('plan_patch_applied', {
620
- origin: 'runtime',
621
- runId: 'run-cancel-task',
622
- payload: { patchId: 'patch-cancel-b', patch },
623
- }));
624
-
625
- const result = await running;
626
-
627
- assert.equal(result.ok, true);
628
- assert.equal(bAborted, true);
629
- assert.deepEqual(session.headlessPlan.map((step) => step.status), ['done', 'cancelled']);
630
- assert.equal(session.agentProjection.planPatches.find((item) => item.id === 'patch-cancel-b').status, 'applied');
631
- });
632
-
633
- test('runRuntimeParallelPlan never exceeds the configured concurrency with twice the limit ready', async () => {
634
- let active = 0;
635
- let maxActive = 0;
636
- const release = {};
637
- const session = {
638
- activities: {},
639
- headlessPlan: Array.from({ length: 6 }, (_, index) => ({
640
- step: index + 1,
641
- id: `task-${index + 1}`,
642
- description: `Task ${index + 1}`,
643
- status: 'pending',
644
- dependsOn: [],
645
- })),
646
- };
647
- const agent = {
648
- async invoke({ input }) {
649
- const id = input.match(/Task id: ([^\n]+)/)?.[1];
650
- active += 1;
651
- maxActive = Math.max(maxActive, active);
652
- await new Promise((resolve) => { release[id] = resolve; });
653
- active -= 1;
654
- return { response: `done ${id}` };
655
- },
586
+ return toolResult({
587
+ jobId: args.jobId,
588
+ taskId: job.taskId,
589
+ status: 'done',
590
+ result: {
591
+ status: 'succeeded',
592
+ outputRefs: [{ type: 'file', ref: 'out/a.json' }],
593
+ metrics: { durationMs: 1 },
594
+ },
595
+ });
596
+ }
597
+ return toolResult({ ok: true });
656
598
  };
657
599
 
658
- const running = runRuntimeParallelPlan(agent, session, 'Respect limit', {
659
- runId: 'run-limit',
600
+ const result = await runRuntimeParallelPlan({ invoke: async () => assert.fail('no child loop') }, session, 'Retry task', {
601
+ runId: 'run-retry-fallback',
660
602
  timeoutMs: 1000,
661
603
  maxTurns: 1,
662
- concurrency: 3,
604
+ concurrency: 1,
605
+ callTool,
606
+ dispatcherPollIntervalMs: 1,
663
607
  });
664
- for (let batch = 0; batch < 2; batch += 1) {
665
- await waitFor(() => Object.keys(release).length >= (batch + 1) * 3);
666
- for (const id of Object.keys(release).slice(batch * 3, (batch + 1) * 3)) release[id]();
667
- }
668
-
669
- const result = await running;
670
608
 
671
609
  assert.equal(result.ok, true);
672
- assert.equal(maxActive, 3);
673
- assert.deepEqual(session.headlessPlan.map((step) => step.status), ['done', 'done', 'done', 'done', 'done', 'done']);
610
+ assert.deepEqual(executeServers, ['cme', 'commercial']);
611
+ assert.equal(session.headlessPlan[0].status, 'done');
612
+ assert.ok(session.agentEvents.some((event) => event.type === 'task.retry_scheduled'
613
+ && event.payload.previousAgentInstanceId === 'cme-main'
614
+ && event.payload.newAgentInstanceId === 'commercial-main'));
674
615
  });
675
616
 
676
- test('runRuntimeParallelPlan serializes conflicting write locks', async () => {
677
- let active = 0;
678
- let maxActive = 0;
679
- const session = {
680
- activities: {},
681
- headlessPlan: [
682
- { step: 1, id: 'a', description: 'Write A', status: 'pending', dependsOn: [], locks: ['workspace:write'] },
683
- { step: 2, id: 'b', description: 'Write B', status: 'pending', dependsOn: [], locks: ['workspace:write'] },
684
- ],
685
- };
686
- const agent = {
687
- async invoke() {
688
- active += 1;
689
- maxActive = Math.max(maxActive, active);
690
- await new Promise((resolve) => setTimeout(resolve, 10));
691
- active -= 1;
692
- return { response: 'done' };
693
- },
617
+ function plannedDoctorTask(id, dependsOn) {
618
+ return {
619
+ step: id === 'a' ? 1 : id === 'b' ? 2 : 3,
620
+ id,
621
+ label: `Task ${id}`,
622
+ description: `Task ${id}`,
623
+ status: 'pending',
624
+ dependsOn,
625
+ requiredCapability: 'workspace.diagnose',
626
+ operation: 'doctor',
627
+ arguments: {},
628
+ parallelizable: true,
629
+ inputRefs: [],
630
+ expectedOutputRefs: [],
631
+ locks: [],
632
+ requiresApproval: false,
633
+ idempotencyKey: null,
634
+ progressWeight: 1,
635
+ outputRefs: [],
694
636
  };
637
+ }
695
638
 
696
- const result = await runRuntimeParallelPlan(agent, session, 'Write safely', {
697
- runId: 'run-locks',
698
- timeoutMs: 1000,
699
- maxTurns: 1,
700
- concurrency: 2,
701
- });
702
-
703
- assert.equal(result.ok, true);
704
- assert.equal(maxActive, 1);
705
- assert.deepEqual(session.headlessPlan.map((step) => step.status), ['done', 'done']);
706
- });
707
-
708
- test('runRuntimeParallelPlan keeps independent child running after sibling failure', async () => {
709
- const releaseB = {};
710
- const session = {
711
- activities: {},
712
- headlessPlan: [
713
- { step: 1, id: 'a', description: 'Failing branch', status: 'pending', dependsOn: [] },
714
- { step: 2, id: 'b', description: 'Independent branch', status: 'pending', dependsOn: [] },
715
- { step: 3, id: 'join', description: 'Converge', status: 'pending', dependsOn: ['a', 'b'] },
716
- ],
717
- };
718
- const agent = {
719
- async invoke({ input }) {
720
- const id = input.match(/Task id: ([^\n]+)/)?.[1];
721
- if (id === 'a') throw new Error('branch failed');
722
- if (id === 'b') await new Promise((resolve) => { releaseB.resolve = resolve; });
723
- return { response: `done ${id}` };
639
+ function fallbackRegistry() {
640
+ const providers = [
641
+ fallbackProvider('cme-main', 'cme'),
642
+ fallbackProvider('commercial-main', 'commercial'),
643
+ ];
644
+ return {
645
+ providersFor(capability) {
646
+ return capability === 'workspace.diagnose' ? providers : [];
724
647
  },
725
- };
726
-
727
- const running = runRuntimeParallelPlan(agent, session, 'Parallel failure', {
728
- runId: 'run-failure',
729
- timeoutMs: 1000,
730
- maxTurns: 1,
731
- concurrency: 2,
732
- });
733
- await waitFor(() => session.headlessPlan.find((step) => step.id === 'a')?.status === 'failed');
734
- assert.equal(session.headlessPlan.find((step) => step.id === 'b')?.status, 'running');
735
- releaseB.resolve();
736
-
737
- const result = await running;
738
-
739
- assert.equal(result.ok, false);
740
- assert.equal(result.reason, 'no_ready_plan_task');
741
- assert.deepEqual(session.headlessPlan.map((step) => step.status), ['failed', 'done', 'pending']);
742
- });
743
-
744
- test('runRuntimeParallelPlan propagates parent cancellation to child tasks', async () => {
745
- const controller = new AbortController();
746
- const session = {
747
- activities: {},
748
- headlessPlan: [
749
- { step: 1, id: 'a', description: 'Long task', status: 'pending', dependsOn: [] },
750
- ],
751
- };
752
- const agent = {
753
- async invoke({ session: childSession, signal }) {
754
- assert.equal(childSession._abortSignal, signal);
755
- await new Promise((resolve, reject) => {
756
- signal.addEventListener('abort', () => {
757
- const err = new Error('cancelled');
758
- err.name = 'AbortError';
759
- reject(err);
760
- }, { once: true });
761
- });
762
- return { response: 'unreachable' };
648
+ isCompatible(contractVersion) {
649
+ return String(contractVersion) === '1';
763
650
  },
764
651
  };
652
+ }
765
653
 
766
- const running = runRuntimeParallelPlan(agent, session, 'Cancel work', {
767
- runId: 'run-cancel',
768
- signal: controller.signal,
769
- timeoutMs: 1000,
770
- maxTurns: 1,
771
- concurrency: 1,
772
- });
773
- await waitFor(() => session.headlessPlan[0]?.status === 'running');
774
- controller.abort();
775
-
776
- await assert.rejects(running, { name: 'AbortError' });
777
- assert.equal(session.headlessPlan[0].status, 'cancelled');
778
- });
779
-
780
- test('runRuntimeParallelPlan aborts every active child on parent cancellation and records terminal updates after running updates', async () => {
781
- const controller = new AbortController();
782
- const aborted = [];
783
- const session = {
784
- activities: {},
785
- headlessPlan: [
786
- { step: 1, id: 'a', description: 'Task A', status: 'pending', dependsOn: [] },
787
- { step: 2, id: 'b', description: 'Task B', status: 'pending', dependsOn: [] },
788
- { step: 3, id: 'c', description: 'Task C', status: 'pending', dependsOn: [] },
789
- ],
790
- };
791
- const agent = {
792
- async invoke({ input, signal }) {
793
- const id = input.match(/Task id: ([^\n]+)/)?.[1];
794
- await new Promise((resolve, reject) => {
795
- signal.addEventListener('abort', () => {
796
- aborted.push(id);
797
- const err = new Error('cancelled');
798
- err.name = 'AbortError';
799
- reject(err);
800
- }, { once: true });
801
- });
802
- return { response: `done ${id}` };
654
+ function fallbackProvider(agentInstanceId, serverName) {
655
+ return {
656
+ agentInstanceId,
657
+ serverName,
658
+ health: 'available',
659
+ capability: {
660
+ id: 'workspace.diagnose',
661
+ version: '1',
662
+ description: 'Diagnose',
663
+ inputSchema: { type: 'object', additionalProperties: true },
664
+ outputSchema: {},
665
+ supportedOperations: ['doctor'],
803
666
  },
667
+ description: { contractVersion: '1', limits: { maxTaskDurationMs: 1000 } },
804
668
  };
669
+ }
805
670
 
806
- const running = runRuntimeParallelPlan(agent, session, 'Cancel all children', {
807
- runId: 'run-cancel-all',
808
- signal: controller.signal,
809
- timeoutMs: 1000,
810
- maxTurns: 1,
811
- concurrency: 3,
812
- });
813
- await waitFor(() => session.headlessPlan.every((step) => step.status === 'running'));
814
- controller.abort();
815
-
816
- await assert.rejects(running, { name: 'AbortError' });
817
- assert.deepEqual(aborted.sort(), ['a', 'b', 'c']);
818
- assert.deepEqual(session.headlessPlan.map((step) => step.status), ['cancelled', 'cancelled', 'cancelled']);
819
- const updates = session.agentEvents
820
- .filter((event) => event.type === 'plan_step_updated')
821
- .map((event) => `${event.taskId}:${event.payload.status}`);
822
- assert.deepEqual(updates.slice(0, 3).sort(), ['a:running', 'b:running', 'c:running']);
823
- assert.deepEqual(updates.slice(3).sort(), ['a:cancelled', 'b:cancelled', 'c:cancelled']);
824
- });
825
-
826
- test('runRuntimeAgenticWorkflow hands off to the parallel scheduler when turn 1 sets a multi-task ready plan', async () => {
827
- const session = {
828
- activities: {},
829
- headlessPlan: null,
830
- llm: {
831
- async completeWithTools() {
832
- return { content: '{"ok":true,"reason":"Done.","suggestedAction":null}' };
671
+ function productionAgent() {
672
+ return {
673
+ serverName: 'production',
674
+ agentInstanceId: 'production-main',
675
+ health: 'available',
676
+ description: {
677
+ contractVersion: '1',
678
+ agentType: 'production',
679
+ agentInstanceId: 'production-main',
680
+ displayName: 'Production',
681
+ capabilities: [
682
+ {
683
+ id: 'workspace.diagnose',
684
+ version: '1',
685
+ description: 'Diagnose workspace',
686
+ inputSchema: { type: 'object', additionalProperties: true },
687
+ outputSchema: { type: 'object', additionalProperties: true },
688
+ supportedOperations: ['doctor'],
689
+ },
690
+ ],
691
+ orchestration: {
692
+ canPlan: true,
693
+ canExpandPlan: false,
694
+ canExecute: true,
695
+ canCancel: true,
696
+ canResume: false,
697
+ supportsIdempotency: false,
698
+ supportsParallelWorkers: true,
833
699
  },
700
+ limits: {
701
+ recommendedConcurrency: 2,
702
+ maxConcurrency: 2,
703
+ maxTaskDurationMs: 1000,
704
+ },
705
+ health: { status: 'available' },
834
706
  },
835
707
  };
836
- const started = [];
837
- const agent = {
838
- async invoke({ session: turnSession, input }) {
839
- if (turnSession.headlessPlan === null) {
840
- dispatchAgentEvent(turnSession, createAgentEvent('plan_set', {
841
- origin: 'tool',
842
- payload: {
843
- steps: [
844
- { id: 'a', description: 'Task A', dependsOn: [] },
845
- { id: 'b', description: 'Task B', dependsOn: [] },
846
- ],
847
- },
848
- }));
849
- return { response: 'Plan set.' };
850
- }
851
- const id = input.match(/Task id: ([^\n]+)/)?.[1];
852
- started.push(id);
853
- return { response: `done ${id}` };
854
- },
855
- };
856
-
857
- const result = await runRuntimeAgenticWorkflow(agent, session, 'Do two independent things', {
858
- runId: 'run-handoff',
859
- timeoutMs: 1000,
860
- maxTurns: 3,
861
- maxReplans: 0,
862
- });
863
-
864
- assert.equal(result.ok, true);
865
- assert.deepEqual(started.sort(), ['a', 'b']);
866
- assert.deepEqual(session.headlessPlan.map((step) => step.status), ['done', 'done']);
867
- });
868
-
869
- test('runRuntimeParallelPlan drains other active tasks before rejecting on cancellation', async () => {
870
- const controller = new AbortController();
871
- let bSettled = false;
872
- const session = {
873
- activities: {},
874
- headlessPlan: [
875
- { step: 1, id: 'a', description: 'Task A', status: 'pending', dependsOn: [] },
876
- { step: 2, id: 'b', description: 'Task B', status: 'pending', dependsOn: [] },
877
- ],
878
- };
879
- const agent = {
880
- async invoke({ input, signal }) {
881
- const id = input.match(/Task id: ([^\n]+)/)?.[1];
882
- if (id === 'a') {
883
- await new Promise((resolve, reject) => {
884
- signal.addEventListener('abort', () => {
885
- const err = new Error('cancelled');
886
- err.name = 'AbortError';
887
- reject(err);
888
- }, { once: true });
889
- });
890
- }
891
- if (id === 'b') {
892
- await new Promise((resolve) => {
893
- signal.addEventListener('abort', () => {
894
- setTimeout(() => { bSettled = true; resolve(); }, 20);
895
- }, { once: true });
896
- });
897
- }
898
- return { response: `done ${id}` };
899
- },
900
- };
901
-
902
- const running = runRuntimeParallelPlan(agent, session, 'Cancel work', {
903
- runId: 'run-cancel-drain',
904
- signal: controller.signal,
905
- timeoutMs: 1000,
906
- maxTurns: 1,
907
- concurrency: 2,
908
- });
909
- await waitFor(() => session.headlessPlan.every((step) => step.status === 'running'));
910
- controller.abort();
708
+ }
911
709
 
912
- await assert.rejects(running, { name: 'AbortError' });
913
- assert.equal(bSettled, true);
914
- assert.equal(session.headlessPlan.find((step) => step.id === 'b').status, 'done');
915
- });
710
+ function toolResult(payload) {
711
+ return { content: [{ type: 'text', text: JSON.stringify(payload) }] };
712
+ }
916
713
 
917
714
  async function waitFor(predicate, timeoutMs = 500) {
918
715
  const deadline = Date.now() + timeoutMs;