@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
@@ -0,0 +1,162 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { mkdtempSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import { createAgentEvent } from '../core/agentEvents.js';
8
+ import { openRuntimeStore } from './store.js';
9
+ import { recoverActiveRuns } from './recoveryManager.js';
10
+
11
+ test('recoveryManager attaches a terminal active job through agent_status', async () => {
12
+ const { store, root, runId, taskId } = storeWithActiveTask();
13
+ const session = recoverySession();
14
+ store.hydrateSession(session, { workspace: 'docs' });
15
+ const statusCalls = [];
16
+
17
+ try {
18
+ const result = await recoverActiveRuns({
19
+ store,
20
+ session,
21
+ workspace: 'docs',
22
+ callTool: async (_mcp, serverName, toolName, args) => {
23
+ statusCalls.push({ serverName, toolName, args });
24
+ return {
25
+ content: [{ type: 'text', text: JSON.stringify({
26
+ jobId: args.jobId,
27
+ taskId,
28
+ status: 'done',
29
+ result: {
30
+ status: 'succeeded',
31
+ outputRefs: [{ type: 'file', ref: 'deliverables/a.md' }],
32
+ metrics: { durationMs: 12 },
33
+ },
34
+ }) }],
35
+ };
36
+ },
37
+ });
38
+
39
+ assert.equal(result.ok, true);
40
+ assert.deepEqual(statusCalls, [{ serverName: 'production', toolName: 'agent_status', args: { jobId: 'job-1' } }]);
41
+ assert.equal(result.recovered[0].taskId, taskId);
42
+ assert.equal(store.listTasks({ runId })[0].status, 'done');
43
+ assert.equal(store.getTaskResult({ taskId }).status, 'succeeded');
44
+ assert.deepEqual(store.getTaskResult({ taskId }).outputRefs, [{ type: 'file', ref: 'deliverables/a.md' }]);
45
+ } finally {
46
+ store.close();
47
+ rmSync(root, { recursive: true, force: true });
48
+ }
49
+ });
50
+
51
+ test('recoveryManager requeues an active job by idempotencyKey when status is still running', async () => {
52
+ const { store, root, runId, taskId } = storeWithActiveTask();
53
+ const session = recoverySession();
54
+ store.hydrateSession(session, { workspace: 'docs' });
55
+
56
+ try {
57
+ const result = await recoverActiveRuns({
58
+ store,
59
+ session,
60
+ workspace: 'docs',
61
+ callTool: async (_mcp, _serverName, _toolName, args) => ({
62
+ content: [{ type: 'text', text: JSON.stringify({
63
+ jobId: args.jobId,
64
+ taskId,
65
+ status: 'running',
66
+ progress: { percent: 50 },
67
+ }) }],
68
+ }),
69
+ });
70
+
71
+ assert.equal(result.ok, true);
72
+ assert.equal(result.rescheduled[0].idempotencyKey, 'idem-a');
73
+ assert.equal(store.listTasks({ runId })[0].status, 'pending');
74
+ assert.ok(session.agentEvents.some((event) => event.type === 'plan_step_updated'
75
+ && event.payload?.recovery?.idempotencyKey === 'idem-a'));
76
+ } finally {
77
+ store.close();
78
+ rmSync(root, { recursive: true, force: true });
79
+ }
80
+ });
81
+
82
+ function storeWithActiveTask() {
83
+ const root = mkdtempSync(join(tmpdir(), 'wiki-manager-recovery-'));
84
+ const stateDir = join(root, '.wiki-manager');
85
+ const store = openRuntimeStore({ stateDir });
86
+ const runId = 'run-recovery';
87
+ const taskId = 'run-recovery:build-a';
88
+ const attemptId = `${taskId}:attempt-1`;
89
+ for (const event of [
90
+ createAgentEvent('run_started', {
91
+ origin: 'runtime',
92
+ runId,
93
+ workspace: 'docs',
94
+ payload: { input: 'build docs', workspace: 'docs' },
95
+ }),
96
+ createAgentEvent('task.created', {
97
+ origin: 'plan_integrator',
98
+ runId,
99
+ taskId,
100
+ workspace: 'docs',
101
+ payload: { runId, task: plannedTask(taskId) },
102
+ }),
103
+ createAgentEvent('task.assigned', {
104
+ origin: 'assignment_manager',
105
+ runId,
106
+ taskId,
107
+ workspace: 'docs',
108
+ payload: {
109
+ runId,
110
+ taskId,
111
+ attemptId,
112
+ assignment: { attemptId, agentInstanceId: 'production-main', agentId: 'production' },
113
+ },
114
+ }),
115
+ createAgentEvent('task.started', {
116
+ origin: 'dispatcher',
117
+ runId,
118
+ taskId,
119
+ workspace: 'docs',
120
+ payload: { runId, taskId, attemptId, jobId: 'job-1', startedAt: '2026-07-07T10:00:00.000Z' },
121
+ }),
122
+ ]) {
123
+ store.persistEvent(event);
124
+ }
125
+ return { store, root, runId, taskId };
126
+ }
127
+
128
+ function plannedTask(id) {
129
+ return {
130
+ id,
131
+ localId: 'build-a',
132
+ label: 'Build A',
133
+ description: 'Build A',
134
+ requiredCapability: 'document.build',
135
+ operation: 'build',
136
+ arguments: { templates: ['a.md'] },
137
+ groupId: null,
138
+ dependsOn: [],
139
+ inputRefs: [],
140
+ expectedOutputRefs: [{ type: 'file', ref: 'deliverables/a.md' }],
141
+ locks: ['workspace-write'],
142
+ requiresApproval: false,
143
+ idempotencyKey: 'idem-a',
144
+ progressWeight: 1,
145
+ status: 'running',
146
+ };
147
+ }
148
+
149
+ function recoverySession() {
150
+ return {
151
+ workspace: 'docs',
152
+ activities: {},
153
+ mcp: {
154
+ production: { tools: [{ name: 'agent_status' }] },
155
+ },
156
+ agentRegistrySnapshot: [{
157
+ agentInstanceId: 'production-main',
158
+ serverName: 'production',
159
+ description: { agentType: 'production', contractVersion: '1' },
160
+ }],
161
+ };
162
+ }
@@ -12,32 +12,118 @@ const MAX_PARALLEL_TO_SEQUENTIAL_RATIO = 0.65;
12
12
 
13
13
  function buildTwoTemplatePlan() {
14
14
  return [
15
- { step: 1, id: 'template-a', description: 'Build template A', status: 'pending', dependsOn: [] },
16
- { step: 2, id: 'template-b', description: 'Build template B', status: 'pending', dependsOn: [] },
15
+ plannedBuildTask(1, 'template-a'),
16
+ plannedBuildTask(2, 'template-b'),
17
17
  ];
18
18
  }
19
19
 
20
- function latencyAgent() {
21
- return {
22
- async invoke() {
23
- await new Promise((resolve) => setTimeout(resolve, SIMULATED_BUILD_LATENCY_MS));
24
- return { response: 'done' };
20
+ async function timedRun(concurrency, runId) {
21
+ const jobs = new Map();
22
+ const session = {
23
+ workspace: 'demo-workspace',
24
+ activities: {},
25
+ headlessPlan: buildTwoTemplatePlan(),
26
+ mcp: {
27
+ production: {
28
+ status: 'connected',
29
+ tools: [{ name: 'agent_execute' }, { name: 'agent_status' }, { name: 'agent_cancel' }],
30
+ },
25
31
  },
32
+ agentRegistrySnapshot: [productionAgent()],
33
+ wikircConfig: { capabilityRouting: {} },
34
+ };
35
+ const callTool = async (_mcp, _serverName, toolName, args) => {
36
+ if (toolName === 'agent_execute') {
37
+ const jobId = `job-${args.taskId}`;
38
+ jobs.set(jobId, { jobId, taskId: args.taskId, startedAt: Date.now() });
39
+ return toolResult({ accepted: true, jobId, status: 'queued' });
40
+ }
41
+ if (toolName === 'agent_status') {
42
+ const job = jobs.get(args.jobId);
43
+ const done = Date.now() - job.startedAt >= SIMULATED_BUILD_LATENCY_MS;
44
+ return toolResult({
45
+ jobId: job.jobId,
46
+ taskId: job.taskId,
47
+ operation: 'build',
48
+ status: done ? 'done' : 'running',
49
+ progress: { percent: done ? 100 : 50 },
50
+ ...(done ? { result: { status: 'succeeded', outputRefs: [], metrics: { durationMs: SIMULATED_BUILD_LATENCY_MS } } } : {}),
51
+ });
52
+ }
53
+ if (toolName === 'agent_cancel') return toolResult({ ok: true });
54
+ throw new Error(`unexpected tool: ${toolName}`);
26
55
  };
27
- }
28
-
29
- async function timedRun(concurrency, runId) {
30
- const session = { activities: {}, headlessPlan: buildTwoTemplatePlan() };
31
56
  const startedAt = Date.now();
32
- const result = await runRuntimeParallelPlan(latencyAgent(), session, 'Build 2 templates', {
57
+ const result = await runRuntimeParallelPlan({ invoke: async () => assert.fail('child Donna loop must not run') }, session, 'Build 2 templates', {
33
58
  runId,
34
59
  timeoutMs: 10_000,
35
60
  maxTurns: 1,
36
61
  concurrency,
62
+ callTool,
63
+ dispatcherPollIntervalMs: 5,
37
64
  });
38
65
  return { result, durationMs: Date.now() - startedAt };
39
66
  }
40
67
 
68
+ function plannedBuildTask(step, id) {
69
+ return {
70
+ step,
71
+ id,
72
+ label: `Build ${id}`,
73
+ description: `Build ${id}`,
74
+ status: 'pending',
75
+ dependsOn: [],
76
+ requiredCapability: 'document.build',
77
+ operation: 'build',
78
+ arguments: { templates: [`${id}.md`] },
79
+ parallelizable: true,
80
+ inputRefs: [],
81
+ expectedOutputRefs: [],
82
+ locks: [],
83
+ requiresApproval: false,
84
+ idempotencyKey: 'test-idempotency-key',
85
+ progressWeight: 1,
86
+ outputRefs: [],
87
+ };
88
+ }
89
+
90
+ function productionAgent() {
91
+ return {
92
+ serverName: 'production',
93
+ agentInstanceId: 'production-main',
94
+ health: 'available',
95
+ description: {
96
+ contractVersion: '1',
97
+ agentType: 'production',
98
+ agentInstanceId: 'production-main',
99
+ displayName: 'Production',
100
+ capabilities: [{
101
+ id: 'document.build',
102
+ version: '1',
103
+ description: 'Build documents',
104
+ inputSchema: { type: 'object', additionalProperties: true },
105
+ outputSchema: { type: 'object', additionalProperties: true },
106
+ supportedOperations: ['build'],
107
+ }],
108
+ orchestration: {
109
+ canPlan: true,
110
+ canExpandPlan: false,
111
+ canExecute: true,
112
+ canCancel: true,
113
+ canResume: false,
114
+ supportsIdempotency: false,
115
+ supportsParallelWorkers: true,
116
+ },
117
+ limits: { recommendedConcurrency: 2, maxConcurrency: 2, maxTaskDurationMs: 10_000 },
118
+ health: { status: 'available' },
119
+ },
120
+ };
121
+ }
122
+
123
+ function toolResult(payload) {
124
+ return { content: [{ type: 'text', text: JSON.stringify(payload) }] };
125
+ }
126
+
41
127
  test('multi-agent scheduler beats sequential on 2 independent build tasks by the required margin (plan 0.11.4 §3 guard)', async () => {
42
128
  const { result: sequentialResult, durationMs: sequentialDurationMs } = await timedRun(1, 'e2e-accel-sequential');
43
129
  const { result: parallelResult, durationMs: parallelDurationMs } = await timedRun(2, 'e2e-accel-parallel');