@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.
- package/README.md +32 -7
- package/agents.docker-compose.yml +3 -0
- package/docker-compose.yml +1 -0
- package/package.json +3 -2
- package/src/activity/activityAggregator.js +109 -0
- package/src/activity/activityAggregator.test.js +60 -0
- package/src/activity/activityDeduplicator.js +50 -0
- package/src/activity/progressCalculator.js +61 -0
- package/src/activity/runSynthesis.js +15 -0
- package/src/agent/graph.js +65 -30
- package/src/agent/graph.test.js +28 -5
- package/src/cli/wiki-manager.js +22 -0
- package/src/contracts/schemas.js +249 -13
- package/src/contracts/schemas.test.js +145 -0
- package/src/core/activity.js +8 -5
- package/src/core/agentEvents.js +260 -23
- package/src/core/agentEvents.test.js +53 -0
- package/src/core/compose.js +6 -1
- package/src/core/dockerCompose.test.js +12 -0
- package/src/core/documentIntake.js +33 -38
- package/src/core/documentIntake.test.js +25 -0
- package/src/core/env.js +11 -1
- package/src/core/jobQueue.js +26 -2
- package/src/core/mcp.js +1 -1
- package/src/core/plan.js +46 -4
- package/src/core/plan.test.js +16 -1
- package/src/core/planPatch.js +64 -3
- package/src/core/planPatch.test.js +110 -1
- package/src/core/queueStore.test.js +21 -0
- package/src/core/runtimeLog.js +119 -0
- package/src/core/runtimeLog.test.js +84 -0
- package/src/core/wikirc.js +22 -0
- package/src/core/wikirc.test.js +49 -1
- package/src/core/workflow.js +14 -3
- package/src/graph/graphAggregator.js +5 -0
- package/src/graph/graphPatch.js +8 -0
- package/src/graph/graphSnapshot.js +14 -0
- package/src/graph/graphVisibilityPolicy.js +40 -0
- package/src/graph/runGraphProjector.js +87 -0
- package/src/graph/runGraphProjector.test.js +111 -0
- package/src/orchestrator/agentRegistry.js +172 -0
- package/src/orchestrator/agentRegistry.test.js +114 -0
- package/src/orchestrator/approvalPolicy.js +126 -0
- package/src/orchestrator/approvalPolicy.test.js +78 -0
- package/src/orchestrator/assignmentManager.js +80 -0
- package/src/orchestrator/attemptManager.js +168 -0
- package/src/orchestrator/attemptManager.test.js +160 -0
- package/src/orchestrator/budgetManager.js +127 -0
- package/src/orchestrator/capabilityRegistry.js +64 -0
- package/src/orchestrator/capabilityRegistry.test.js +59 -0
- package/src/orchestrator/capabilityResolver.js +151 -0
- package/src/orchestrator/capabilityResolver.test.js +127 -0
- package/src/orchestrator/dependencyResolver.js +112 -0
- package/src/orchestrator/dispatcher.js +302 -0
- package/src/orchestrator/lockManager.js +51 -0
- package/src/orchestrator/planIntegrator.js +268 -0
- package/src/orchestrator/planIntegrator.test.js +213 -0
- package/src/orchestrator/planValidator.js +534 -0
- package/src/orchestrator/planValidator.test.js +262 -0
- package/src/orchestrator/resultAggregator.js +248 -0
- package/src/orchestrator/resultAggregator.test.js +211 -0
- package/src/orchestrator/scheduler.js +103 -0
- package/src/orchestrator/scheduler.test.js +138 -0
- package/src/runtime/approvals.js +130 -1
- package/src/runtime/donna-contract.test.js +293 -30
- package/src/runtime/recoveryManager.js +176 -0
- package/src/runtime/recoveryManager.test.js +162 -0
- package/src/runtime/runner.e2e.test.js +98 -12
- package/src/runtime/runner.js +261 -227
- package/src/runtime/runner.test.js +249 -452
- package/src/runtime/server.js +137 -20
- package/src/runtime/server.test.js +137 -7
- package/src/runtime/store.js +837 -2
- package/src/runtime/store.test.js +245 -4
- package/src/runtime/supervisor.js +33 -1
- package/src/runtime/supervisor.test.js +47 -1
- package/src/shell/RightPane.tsx +8 -5
- package/src/shell/repl.js +4 -9
- package/src/shell/repl.test.js +25 -4
- package/src/shell/tui.tsx +1 -0
- 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
|
-
|
|
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: {
|
|
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
|
|
408
|
-
|
|
409
|
-
const
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
435
|
+
plannedDoctorTask('a', []),
|
|
436
|
+
plannedDoctorTask('b', []),
|
|
437
|
+
plannedDoctorTask('join', ['a', 'b']),
|
|
416
438
|
],
|
|
417
439
|
};
|
|
418
440
|
const agent = {
|
|
419
|
-
async invoke(
|
|
420
|
-
|
|
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, '
|
|
430
|
-
runId: 'run-
|
|
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(() =>
|
|
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
|
-
|
|
438
|
-
|
|
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.
|
|
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.
|
|
446
|
-
assert.
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
|
525
|
-
const release = {};
|
|
521
|
+
test('runRuntimeParallelPlan fails cleanly when scheduler budget is exceeded', async () => {
|
|
526
522
|
const session = {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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(
|
|
535
|
-
|
|
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
|
|
542
|
-
runId: 'run-
|
|
533
|
+
const result = await runRuntimeParallelPlan(agent, session, 'Run one task', {
|
|
534
|
+
runId: 'run-budget',
|
|
543
535
|
timeoutMs: 1000,
|
|
544
536
|
maxTurns: 1,
|
|
545
|
-
|
|
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,
|
|
575
|
-
assert.
|
|
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
|
|
579
|
-
const
|
|
580
|
-
let bAborted = false;
|
|
547
|
+
test('runRuntimeParallelPlan retries a retryable task on a fallback agent', async () => {
|
|
548
|
+
const executeServers = [];
|
|
581
549
|
const session = {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
{
|
|
585
|
-
|
|
586
|
-
],
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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 {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
|
659
|
-
runId: 'run-
|
|
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:
|
|
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.
|
|
673
|
-
assert.
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
-
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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;
|