@dotdrelle/wiki-manager 0.11.10 → 0.12.1

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 (85) 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 +101 -38
  11. package/src/agent/graph.test.js +75 -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 +147 -2
  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 +30 -3
  25. package/src/core/mcp.test.js +69 -1
  26. package/src/core/plan.js +46 -4
  27. package/src/core/plan.test.js +16 -1
  28. package/src/core/planPatch.js +64 -3
  29. package/src/core/planPatch.test.js +110 -1
  30. package/src/core/queueStore.test.js +21 -0
  31. package/src/core/runtimeLog.js +119 -0
  32. package/src/core/runtimeLog.test.js +84 -0
  33. package/src/core/wikirc.js +22 -0
  34. package/src/core/wikirc.test.js +49 -1
  35. package/src/core/workflow.js +14 -3
  36. package/src/graph/graphAggregator.js +5 -0
  37. package/src/graph/graphPatch.js +8 -0
  38. package/src/graph/graphSnapshot.js +14 -0
  39. package/src/graph/graphVisibilityPolicy.js +40 -0
  40. package/src/graph/runGraphProjector.js +87 -0
  41. package/src/graph/runGraphProjector.test.js +111 -0
  42. package/src/orchestrator/agentRegistry.js +172 -0
  43. package/src/orchestrator/agentRegistry.test.js +114 -0
  44. package/src/orchestrator/approvalPolicy.js +126 -0
  45. package/src/orchestrator/approvalPolicy.test.js +78 -0
  46. package/src/orchestrator/assignmentManager.js +80 -0
  47. package/src/orchestrator/attemptManager.js +168 -0
  48. package/src/orchestrator/attemptManager.test.js +160 -0
  49. package/src/orchestrator/budgetManager.js +127 -0
  50. package/src/orchestrator/capabilityRegistry.js +64 -0
  51. package/src/orchestrator/capabilityRegistry.test.js +59 -0
  52. package/src/orchestrator/capabilityResolver.js +151 -0
  53. package/src/orchestrator/capabilityResolver.test.js +127 -0
  54. package/src/orchestrator/dependencyResolver.js +112 -0
  55. package/src/orchestrator/dispatcher.js +302 -0
  56. package/src/orchestrator/lockManager.js +51 -0
  57. package/src/orchestrator/planIntegrator.js +268 -0
  58. package/src/orchestrator/planIntegrator.test.js +213 -0
  59. package/src/orchestrator/planValidator.js +534 -0
  60. package/src/orchestrator/planValidator.test.js +262 -0
  61. package/src/orchestrator/resultAggregator.js +248 -0
  62. package/src/orchestrator/resultAggregator.test.js +211 -0
  63. package/src/orchestrator/scheduler.js +103 -0
  64. package/src/orchestrator/scheduler.test.js +138 -0
  65. package/src/runtime/approvals.js +130 -1
  66. package/src/runtime/controlMessages.js +43 -0
  67. package/src/runtime/controlMessages.test.js +21 -0
  68. package/src/runtime/donna-contract.test.js +296 -33
  69. package/src/runtime/recoveryManager.js +176 -0
  70. package/src/runtime/recoveryManager.test.js +162 -0
  71. package/src/runtime/runner.e2e.test.js +98 -12
  72. package/src/runtime/runner.js +261 -227
  73. package/src/runtime/runner.test.js +249 -452
  74. package/src/runtime/server.js +142 -24
  75. package/src/runtime/server.test.js +192 -62
  76. package/src/runtime/store.js +837 -2
  77. package/src/runtime/store.test.js +269 -28
  78. package/src/runtime/supervisor.js +33 -1
  79. package/src/runtime/supervisor.test.js +47 -1
  80. package/src/shell/RightPane.tsx +8 -5
  81. package/src/shell/repl.js +7 -11
  82. package/src/shell/repl.test.js +26 -5
  83. package/src/shell/tui.tsx +1 -0
  84. package/src/shell/useAgent.ts +5 -1
  85. package/src/shell/useSession.ts +75 -19
@@ -1,12 +1,18 @@
1
1
  import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
2
- import { activityKey, sessionActivities, terminalFailures } from '../core/activity.js';
2
+ import { sessionActivities, terminalFailures } from '../core/activity.js';
3
3
  import { runAgenticLoop, throwIfAborted } from '../core/agentLoop.js';
4
4
  import { formatPlanStatus } from '../core/plan.js';
5
- import { formatReadyTaskPrompt, readyPlanTasks, sanitizePlanForExecution } from '../core/planPatch.js';
5
+ import { readyPlanTasks, sanitizePlanForExecution } from '../core/planPatch.js';
6
+ import { createAssignmentManager } from '../orchestrator/assignmentManager.js';
7
+ import { createAttemptManager } from '../orchestrator/attemptManager.js';
8
+ import { createBudgetManager, BudgetExceededError } from '../orchestrator/budgetManager.js';
9
+ import { createDispatcher } from '../orchestrator/dispatcher.js';
10
+ import { assertValidatedFragment } from '../orchestrator/planValidator.js';
11
+ import { createResultAggregator } from '../orchestrator/resultAggregator.js';
12
+ import { drainActive, resolveSchedulerConcurrency, startReadyTasks } from '../orchestrator/scheduler.js';
6
13
  import { emitRuntimeLog, pollActivitiesOnce } from './supervisor.js';
7
14
 
8
15
  const DEFAULT_MAX_REPLANS = 2;
9
- const DEFAULT_SCHEDULER_CONCURRENCY = 3;
10
16
 
11
17
  async function waitForRuntimeActivities(session, startedActivities, { timeoutMs, signal, pollBusy }) {
12
18
  const deadline = Date.now() + timeoutMs;
@@ -87,6 +93,8 @@ export async function runRuntimeAgenticWorkflow(agent, session, input, {
87
93
  pollBusy,
88
94
  evaluate = true,
89
95
  maxReplans = resolveMaxReplans(),
96
+ callTool = null,
97
+ dispatcherPollIntervalMs = 250,
90
98
  } = {}) {
91
99
  let currentInput = initialInput ?? input;
92
100
  let replansLeft = Math.max(0, Math.floor(Number(maxReplans) || 0));
@@ -100,6 +108,8 @@ export async function runRuntimeAgenticWorkflow(agent, session, input, {
100
108
  maxTurns,
101
109
  runId,
102
110
  pollBusy,
111
+ callTool,
112
+ dispatcherPollIntervalMs,
103
113
  })
104
114
  : await runRuntimeAgenticLoop(agent, session, currentInput, {
105
115
  signal,
@@ -209,10 +219,28 @@ export async function runRuntimeParallelPlan(agent, session, input, {
209
219
  runId = null,
210
220
  pollBusy,
211
221
  concurrency = resolveSchedulerConcurrency(),
222
+ fragment = null,
223
+ assignmentManager = null,
224
+ attemptManager = null,
225
+ dispatcher = null,
226
+ resultAggregator = null,
227
+ budgetManager = null,
228
+ budgets = {},
229
+ callTool = null,
230
+ dispatcherPollIntervalMs = 250,
212
231
  } = {}) {
232
+ if (fragment != null) assertValidatedFragment(fragment);
213
233
  const limit = Math.max(1, Math.floor(Number(concurrency) || 1));
214
234
  const active = new Map();
215
- const locks = new Set();
235
+ const attempts = attemptManager ?? createAttemptManager();
236
+ const assigner = assignmentManager ?? createAssignmentManager({ session });
237
+ const executor = dispatcher ?? createDispatcher({
238
+ session,
239
+ ...(callTool ? { callTool } : {}),
240
+ pollIntervalMs: dispatcherPollIntervalMs,
241
+ });
242
+ const aggregator = resultAggregator ?? createResultAggregator({ session, runId });
243
+ const budget = budgetManager ?? createBudgetManager({ budgets, runId });
216
244
  const failures = [];
217
245
  const previousIdentity = session._currentRunIdentity;
218
246
  const previousPlanUpdate = session._onPlanUpdate;
@@ -232,10 +260,10 @@ export async function runRuntimeParallelPlan(agent, session, input, {
232
260
  try {
233
261
  while (true) {
234
262
  if (signal?.aborted) {
235
- await drainActive(active, locks);
263
+ await drainActive(active, attempts);
236
264
  throwIfAborted(signal, 'Runtime run cancelled.');
237
265
  }
238
- const pending = (session.headlessPlan ?? []).filter((step) => step.status === 'pending' || step.status === 'pending_approval');
266
+ const pending = (session.headlessPlan ?? []).filter((step) => pendingSchedulerStatus(step.status));
239
267
  if (pending.length === 0 && active.size === 0) {
240
268
  emitRuntimeLog(session, 'scheduler: all plan tasks terminal');
241
269
  return failures.length > 0
@@ -243,18 +271,85 @@ export async function runRuntimeParallelPlan(agent, session, input, {
243
271
  : { ok: true };
244
272
  }
245
273
 
246
- const started = startReadyTasks(agent, session, input, {
274
+ const started = startReadyTasks({
275
+ plan: session.headlessPlan,
247
276
  active,
248
- locks,
249
- signal,
250
- timeoutMs,
251
- maxTurns,
252
- runId,
253
- pollBusy,
277
+ attemptManager: attempts,
278
+ lockManager: attempts,
279
+ budgetManager: budget,
280
+ registry: session.capabilityRegistry ?? null,
281
+ approvals: session.agentProjection?.approvals ?? session.approvals ?? [],
254
282
  limit,
283
+ onDuplicateTask: (taskId) => {
284
+ // Two distinct ready tasks resolved to the same id/step — starting
285
+ // both would let plan_step_updated resolve to whichever one the
286
+ // reducer finds first, silently marking the wrong task done. Skip the
287
+ // duplicate rather than risk misattribution; it stays pending and
288
+ // will surface as a stalled plan instead of corrupting sibling state.
289
+ emitRuntimeLog(session, `scheduler: skipping task with duplicate id/step "${taskId}"`);
290
+ },
291
+ onTaskReady: (task) => {
292
+ emitRuntimeLog(session, taskLogPayload('task.ready', task, { runId, detail: 'ready for assignment' }));
293
+ },
294
+ onAttemptCreated: (task, attempt) => {
295
+ emitRuntimeLog(session, taskLogPayload('attempt.created', task, {
296
+ runId,
297
+ attempt,
298
+ detail: 'attempt created',
299
+ }));
300
+ emitRuntimeLog(session, taskLogPayload('lock.acquired', task, {
301
+ runId,
302
+ attempt,
303
+ detail: attempt?.locks?.join(',') || 'no locks',
304
+ }));
305
+ },
306
+ onTaskStarting: (task) => {
307
+ const taskId = planTaskId(task);
308
+ dispatchAgentEvent(session, createAgentEvent('plan_step_updated', {
309
+ origin: 'runtime',
310
+ runId,
311
+ taskId,
312
+ payload: { taskId, status: 'running' },
313
+ }));
314
+ emitRuntimeLog(session, taskLogPayload('task.starting', task, { runId, detail: `starting task ${taskId}` }));
315
+ },
316
+ startTask: (task, attempt) => {
317
+ const taskAbort = createTaskAbortSignal(signal);
318
+ const promise = runDispatchedTask(task, {
319
+ session,
320
+ assignmentManager: assigner,
321
+ dispatcher: executor,
322
+ resultAggregator: aggregator,
323
+ signal: taskAbort.signal,
324
+ timeoutMs,
325
+ runId,
326
+ pollBusy,
327
+ attempt,
328
+ });
329
+ return {
330
+ promise,
331
+ controller: taskAbort.controller,
332
+ signal: taskAbort.signal,
333
+ cleanup: taskAbort.cleanup,
334
+ };
335
+ },
255
336
  });
256
337
  if (started === 0 && active.size === 0) {
257
- const reason = pending.every((step) => step.status === 'pending_approval') ? 'awaiting_approval' : 'no_ready_plan_task';
338
+ const exceeded = budget.exceeded?.();
339
+ if (exceeded) {
340
+ dispatchAgentEvent(session, createAgentEvent('run_error', {
341
+ origin: 'runtime',
342
+ runId,
343
+ payload: {
344
+ runId,
345
+ message: `Run budget exceeded: ${exceeded.reason}`,
346
+ budget: exceeded,
347
+ },
348
+ }));
349
+ emitRuntimeLog(session, `scheduler: budget exceeded (${exceeded.reason})`);
350
+ return { ok: false, budgetExceeded: true, reason: exceeded.reason, budget: exceeded, completed: sessionActivities(session), failures };
351
+ }
352
+ const reason = pending.every((step) => approvalWaitingStatus(step.status)) ? 'awaiting_approval' : 'no_ready_plan_task';
258
353
  emitRuntimeLog(session, `scheduler: stalled (${reason})`);
259
354
  return { ok: false, stalled: true, reason, completed: sessionActivities(session), failures };
260
355
  }
@@ -263,15 +358,45 @@ export async function runRuntimeParallelPlan(agent, session, input, {
263
358
  const activeEntry = active.get(settled.taskId);
264
359
  active.delete(settled.taskId);
265
360
  activeEntry?.cleanup?.();
266
- for (const lock of settled.locks) locks.delete(lock);
267
361
  if (settled.cancelled) {
268
362
  if (signal?.aborted) {
269
- await drainActive(active, locks);
363
+ await drainActive(active, attempts);
270
364
  throwIfAborted(signal, 'Runtime run cancelled.');
271
365
  }
272
366
  continue;
273
367
  }
274
- if (!settled.ok) failures.push(settled);
368
+ try {
369
+ budget.recordTaskResult?.(settled.result ?? {});
370
+ } catch (err) {
371
+ if (err instanceof BudgetExceededError) {
372
+ dispatchAgentEvent(session, createAgentEvent('run_error', {
373
+ origin: 'runtime',
374
+ runId,
375
+ payload: {
376
+ runId,
377
+ message: err.message,
378
+ budget: { reason: err.reason, details: err.details },
379
+ },
380
+ }));
381
+ emitRuntimeLog(session, `scheduler: budget exceeded (${err.reason})`);
382
+ return { ok: false, budgetExceeded: true, reason: err.reason, budget: err.details, completed: sessionActivities(session), failures };
383
+ }
384
+ throw err;
385
+ }
386
+ if (!settled.ok) {
387
+ const task = (session.headlessPlan ?? []).find((item) => planTaskId(item) === settled.taskId);
388
+ const retry = attempts.scheduleRetry?.(task, settled, {
389
+ assignment: settled.assignment,
390
+ registry: session.capabilityRegistry ?? null,
391
+ session,
392
+ runId,
393
+ });
394
+ if (retry?.scheduled) {
395
+ emitRuntimeLog(session, `scheduler: retry scheduled for task ${settled.taskId}${retry.newAgentInstanceId ? ` on ${retry.newAgentInstanceId}` : ''}`);
396
+ continue;
397
+ }
398
+ failures.push(settled);
399
+ }
275
400
  }
276
401
  } finally {
277
402
  if (previousIdentity) session._currentRunIdentity = previousIdentity;
@@ -295,15 +420,6 @@ function sanitizeSessionPlanForExecution(session, runId = null) {
295
420
  }));
296
421
  }
297
422
 
298
- async function drainActive(active, locks) {
299
- if (active.size === 0) return;
300
- const entries = [...active.values()];
301
- await Promise.all(entries.map((entry) => entry.promise));
302
- for (const entry of entries) entry.cleanup?.();
303
- active.clear();
304
- locks.clear();
305
- }
306
-
307
423
  function abortCancelledActiveTasks(session, active) {
308
424
  for (const [taskId, entry] of active.entries()) {
309
425
  const current = (session.headlessPlan ?? []).find((step) => planTaskId(step) === taskId);
@@ -313,6 +429,14 @@ function abortCancelledActiveTasks(session, active) {
313
429
  }
314
430
  }
315
431
 
432
+ function pendingSchedulerStatus(status) {
433
+ return ['pending', 'pending_approval', 'waiting_approval'].includes(String(status ?? ''));
434
+ }
435
+
436
+ function approvalWaitingStatus(status) {
437
+ return ['pending_approval', 'waiting_approval'].includes(String(status ?? ''));
438
+ }
439
+
316
440
  // Scope the evaluator/replanner's view of "completed" activities to the
317
441
  // tasks that actually failed, instead of every activity the whole run has
318
442
  // ever recorded — a long-running sibling's stale (or unrelated, already
@@ -340,64 +464,6 @@ function ensurePlanProjection(session, runId) {
340
464
  }));
341
465
  }
342
466
 
343
- function startReadyTasks(agent, session, input, {
344
- active,
345
- locks,
346
- signal,
347
- timeoutMs,
348
- maxTurns,
349
- runId,
350
- pollBusy,
351
- limit,
352
- }) {
353
- let started = 0;
354
- const seenTaskIds = new Set(active.keys());
355
- for (const task of readyPlanTasks(session.headlessPlan)) {
356
- if (active.size >= limit) break;
357
- const taskId = planTaskId(task);
358
- if (active.has(taskId)) continue;
359
- if (seenTaskIds.has(taskId)) {
360
- // Two distinct ready tasks resolved to the same id/step — starting
361
- // both would let plan_step_updated resolve to whichever one the
362
- // reducer finds first, silently marking the wrong task done. Skip the
363
- // duplicate rather than risk misattribution; it stays pending and
364
- // will surface as a stalled plan instead of corrupting sibling state.
365
- emitRuntimeLog(session, `scheduler: skipping task with duplicate id/step "${taskId}"`);
366
- continue;
367
- }
368
- seenTaskIds.add(taskId);
369
- const taskLocks = locksForTask(task);
370
- if (taskLocks.some((lock) => locks.has(lock))) continue;
371
- for (const lock of taskLocks) locks.add(lock);
372
- dispatchAgentEvent(session, createAgentEvent('plan_step_updated', {
373
- origin: 'runtime',
374
- runId,
375
- taskId,
376
- payload: { taskId, status: 'running' },
377
- }));
378
- emitRuntimeLog(session, `scheduler: starting task ${taskId}`);
379
- const taskAbort = createTaskAbortSignal(signal);
380
- const promise = runParallelTask(agent, session, input, task, {
381
- signal: taskAbort.signal,
382
- timeoutMs,
383
- maxTurns,
384
- runId,
385
- pollBusy,
386
- locks: taskLocks,
387
- });
388
- active.set(taskId, {
389
- taskId,
390
- locks: taskLocks,
391
- promise,
392
- controller: taskAbort.controller,
393
- signal: taskAbort.signal,
394
- cleanup: taskAbort.cleanup,
395
- });
396
- started += 1;
397
- }
398
- return started;
399
- }
400
-
401
467
  function createTaskAbortSignal(parentSignal) {
402
468
  const controller = new AbortController();
403
469
  if (!parentSignal) return { controller, signal: controller.signal, cleanup: null };
@@ -414,55 +480,112 @@ function createTaskAbortSignal(parentSignal) {
414
480
  };
415
481
  }
416
482
 
417
- async function runParallelTask(agent, parentSession, input, task, {
483
+ async function runDispatchedTask(task, {
484
+ session,
485
+ assignmentManager,
486
+ dispatcher,
487
+ resultAggregator,
418
488
  signal,
419
489
  timeoutMs,
420
- maxTurns,
421
490
  runId,
422
491
  pollBusy,
423
- locks,
492
+ attempt,
424
493
  }) {
425
494
  const taskId = planTaskId(task);
426
- const childSession = createTaskSession(parentSession, task, { runId, taskId });
495
+ let assignment = null;
427
496
  try {
428
- const result = await runRuntimeAgenticLoop(agent, childSession, buildTaskPrompt(input, parentSession.headlessPlan, task), {
497
+ emitRuntimeLog(session, taskLogPayload('capability.resolving', task, {
498
+ runId,
499
+ attempt,
500
+ detail: `resolving ${task.requiredCapability}`,
501
+ }));
502
+ assignment = await assignmentManager.assign(task, {
503
+ session,
504
+ signal,
505
+ });
506
+ emitRuntimeLog(session, taskLogPayload('agent.selected', task, {
507
+ runId,
508
+ attempt,
509
+ assignment,
510
+ detail: assignment.agentInstanceId,
511
+ }));
512
+ dispatchAgentEvent(session, createAgentEvent('task.assigned', {
513
+ origin: 'assignment_manager',
514
+ runId,
515
+ taskId,
516
+ payload: {
517
+ runId,
518
+ taskId,
519
+ attemptId: attempt?.attemptId ?? null,
520
+ assignment: {
521
+ ...assignment,
522
+ attemptId: attempt?.attemptId ?? null,
523
+ agent: undefined,
524
+ },
525
+ },
526
+ }));
527
+ emitRuntimeLog(session, taskLogPayload('task.assigned', task, {
528
+ runId,
529
+ attempt,
530
+ assignment,
531
+ detail: 'assignment created',
532
+ }));
533
+ const result = await dispatcher.execute(task, assignment, {
534
+ session,
429
535
  signal,
430
536
  timeoutMs,
431
- maxTurns,
432
537
  runId,
433
538
  pollBusy,
539
+ attempt,
434
540
  });
435
- if (!result.ok) {
436
- dispatchAgentEvent(parentSession, createAgentEvent('plan_step_updated', {
437
- origin: 'runtime',
541
+ const aggregate = await resultAggregator.accept(result, { task, assignment });
542
+ if (!aggregate.ok) {
543
+ emitRuntimeLog(session, taskLogPayload('task.failed', task, {
438
544
  runId,
439
- taskId,
440
- payload: { taskId, status: 'failed' },
545
+ attempt,
546
+ assignment,
547
+ jobId: result?.jobId,
548
+ error: result?.error?.code ?? result?.error?.message ?? result?.status ?? 'failed',
549
+ detail: 'task terminal',
441
550
  }));
442
- return { ok: false, taskId, locks, result };
443
- }
444
- if ((parentSession.headlessPlan ?? []).find((step) => planTaskId(step) === taskId)?.status === 'cancelled') {
445
- return { ok: false, taskId, locks, cancelled: true };
551
+ return { ok: false, taskId, result, assignment };
446
552
  }
447
- dispatchAgentEvent(parentSession, createAgentEvent('plan_step_updated', {
448
- origin: 'runtime',
553
+ emitRuntimeLog(session, taskLogPayload('task.completed', task, {
449
554
  runId,
450
- taskId,
451
- payload: { taskId, status: 'done' },
555
+ attempt,
556
+ assignment,
557
+ jobId: result?.jobId,
558
+ outputs: result?.outputRefs ?? [],
559
+ detail: 'returned to donna',
452
560
  }));
453
- emitRuntimeLog(parentSession, `scheduler: task ${taskId} done`);
454
- return { ok: true, taskId, locks, result };
561
+ return { ok: true, taskId, result, assignment };
455
562
  } catch (err) {
456
563
  if (err?.name === 'AbortError') {
457
- dispatchAgentEvent(parentSession, createAgentEvent('plan_step_updated', {
564
+ attempt?.release?.();
565
+ dispatchAgentEvent(session, createAgentEvent('plan_step_updated', {
458
566
  origin: 'runtime',
459
567
  runId,
460
568
  taskId,
461
569
  payload: { taskId, status: 'cancelled' },
462
570
  }));
463
- return { ok: false, taskId, locks, cancelled: true };
571
+ return { ok: false, taskId, cancelled: true, assignment };
464
572
  }
465
- dispatchAgentEvent(parentSession, createAgentEvent('plan_step_updated', {
573
+ attempt?.release?.();
574
+ const result = {
575
+ ok: false,
576
+ taskId,
577
+ attemptId: attempt?.attemptId ?? null,
578
+ status: 'failed',
579
+ outputRefs: [],
580
+ metrics: {},
581
+ error: {
582
+ code: 'dispatcher_error',
583
+ message: err instanceof Error ? err.message : String(err),
584
+ retryable: false,
585
+ },
586
+ };
587
+ await resultAggregator.accept(result, { task, assignment });
588
+ dispatchAgentEvent(session, createAgentEvent('plan_step_updated', {
466
589
  origin: 'runtime',
467
590
  runId,
468
591
  taskId,
@@ -471,131 +594,49 @@ async function runParallelTask(agent, parentSession, input, task, {
471
594
  return {
472
595
  ok: false,
473
596
  taskId,
474
- locks,
597
+ result,
598
+ assignment,
475
599
  error: err instanceof Error ? err.message : String(err),
476
600
  };
477
601
  }
478
602
  }
479
603
 
480
- function createTaskSession(parent, task, { runId, taskId }) {
481
- const child = {
482
- ...parent,
483
- // Copy (not share) the parent's activities: a child's own diffing needs
484
- // isolation from mutation, but local guards like productionLockBusy must
485
- // still see activities already running when this task started.
486
- activities: { ...parent.activities },
487
- agentEvents: [],
488
- agentProjection: null,
489
- _agentProjectionState: null,
490
- headlessPlan: null,
491
- _currentRunIdentity: {
492
- ...(parent._currentRunIdentity ?? {}),
493
- runId,
494
- taskId,
495
- workspace: parent.workspace ?? parent._currentRunIdentity?.workspace ?? null,
496
- },
497
- _onAgentEvent: (event) => {
498
- if (event.type === 'plan_set') return;
499
- if (event.type === 'plan_step_updated') {
500
- // The child always targets its own single task, regardless of what
501
- // step/id the child's LLM passed to wiki__plan_done — a child has no
502
- // visibility into the parent's real multi-task numbering.
503
- dispatchAgentEvent(parent, {
504
- ...event,
505
- taskId: taskId,
506
- runId: event.runId ?? runId,
507
- payload: { ...event.payload, taskId },
508
- });
509
- return;
510
- }
511
- if (event.type === 'activity_upserted') {
512
- stampOwnerActivityKey(parent, taskId, event.payload?.activity);
513
- }
514
- dispatchAgentEvent(parent, {
515
- ...event,
516
- taskId: event.taskId ?? taskId,
517
- runId: event.runId ?? runId,
518
- });
519
- },
520
- };
521
- Object.defineProperty(child, '_runApprovalResolved', {
522
- get: () => parent._runApprovalResolved,
523
- set: (value) => { parent._runApprovalResolved = value; },
524
- enumerable: true,
525
- configurable: true,
526
- });
527
- if (parent._requestApproval) {
528
- child._requestApproval = (request) => {
529
- if (request?.scope === 'tool') return parent._requestApproval(request);
530
- // Run-level approval is one decision for the whole run: concurrent
531
- // children must join the same in-flight request instead of each
532
- // minting their own (only one of which /approve could ever resolve).
533
- parent._pendingRunApproval ??= parent._requestApproval(request)
534
- .finally(() => { delete parent._pendingRunApproval; });
535
- return parent._pendingRunApproval;
536
- };
537
- }
538
- // Seed the plan through the real reducer (not a raw property write): the
539
- // child's own _agentProjectionState knows nothing about this task
540
- // otherwise, and the very next session-projection event (even a plain
541
- // runtime_log from onTurnStart) would overwrite headlessPlan back to null
542
- // via applyAgentProjectionToSession. The _onAgentEvent filter above already
543
- // keeps this plan_set from being forwarded to the parent.
544
- dispatchAgentEvent(child, createAgentEvent('plan_set', {
545
- origin: 'runtime',
546
- runId,
547
- taskId,
548
- payload: { steps: [{ ...task, status: 'running' }] },
549
- }));
550
- return child;
551
- }
552
-
553
- function stampOwnerActivityKey(parent, taskId, rawActivity) {
554
- if (!rawActivity) return;
555
- const entry = (parent.headlessPlan ?? []).find((item) => String(item.id ?? item.step) === taskId);
556
- if (!entry || entry.ownerActivityKey) return;
557
- entry.ownerActivityKey = activityKey(rawActivity);
558
- }
559
-
560
- function buildTaskPrompt(input, plan, task) {
561
- return [
562
- 'Original task:',
563
- input || '(unknown)',
564
- '',
565
- `Plan status:\n${formatPlanStatus(plan)}`,
566
- '',
567
- formatReadyTaskPrompt(task),
568
- '',
569
- 'Execute exactly this task as an autonomous child task.',
570
- 'Do not start sibling tasks. Do not modify unrelated plan steps.',
571
- 'If this task cannot complete, report the blocker clearly.',
572
- ].join('\n');
604
+ function shouldUseParallelScheduler(plan) {
605
+ return readyPlanTasks(plan).some((task) => task.requiredCapability && task.operation);
573
606
  }
574
607
 
575
- function shouldUseParallelScheduler(plan) {
576
- return readyPlanTasks(plan).length > 0;
608
+ function taskLogPayload(event, task, {
609
+ runId = null,
610
+ attempt = null,
611
+ assignment = null,
612
+ jobId = null,
613
+ outputs = null,
614
+ error = null,
615
+ detail = null,
616
+ } = {}) {
617
+ return {
618
+ event,
619
+ runId,
620
+ planRevision: task?.planRevision ?? null,
621
+ groupId: task?.groupId ?? null,
622
+ taskId: planTaskId(task),
623
+ attemptId: attempt?.attemptId ?? null,
624
+ agentType: assignment?.agent?.description?.agentType ?? assignment?.description?.agentType ?? null,
625
+ agentInstanceId: assignment?.agentInstanceId ?? null,
626
+ agentId: assignment?.agentId ?? null,
627
+ jobId,
628
+ capability: task?.requiredCapability ?? assignment?.capability ?? null,
629
+ operation: task?.operation ?? assignment?.operation ?? null,
630
+ outputs,
631
+ error,
632
+ detail,
633
+ };
577
634
  }
578
635
 
579
636
  function planTaskId(task) {
580
637
  return String(task.id ?? task.step);
581
638
  }
582
639
 
583
- function locksForTask(task) {
584
- const locks = new Set();
585
- const explicit = task.locks ?? task.writeLocks ?? null;
586
- if (Array.isArray(explicit)) {
587
- for (const lock of explicit) if (lock) locks.add(String(lock));
588
- } else if (explicit && typeof explicit === 'object') {
589
- if (explicit.workspaceWrite || explicit.workspace) locks.add('workspace:write');
590
- for (const value of explicit.deliverableWrites ?? explicit.deliverables ?? []) locks.add(`deliverable:${value}`);
591
- for (const value of explicit.wikiPageWrites ?? explicit.wikiPages ?? []) locks.add(`wiki-page:${value}`);
592
- }
593
- for (const value of task.deliverableWrites ?? []) locks.add(`deliverable:${value}`);
594
- for (const value of task.wikiPageWrites ?? []) locks.add(`wiki-page:${value}`);
595
- if (task.workspaceWrite) locks.add('workspace:write');
596
- return [...locks].sort();
597
- }
598
-
599
640
  export async function finishRuntimeRun(session, input, {
600
641
  runId,
601
642
  signal = null,
@@ -934,10 +975,3 @@ function resolveMaxReplans(value = process.env.WIKI_MANAGER_REPLANNER_MAX_REPLAN
934
975
  const parsed = Number(value);
935
976
  return Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed)) : DEFAULT_MAX_REPLANS;
936
977
  }
937
-
938
- function resolveSchedulerConcurrency(value = process.env.WIKI_MANAGER_SCHEDULER_CONCURRENCY) {
939
- const parsed = Number(value);
940
- return Number.isFinite(parsed) && parsed > 0
941
- ? Math.max(1, Math.floor(parsed))
942
- : DEFAULT_SCHEDULER_CONCURRENCY;
943
- }