@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.
- 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 +101 -38
- package/src/agent/graph.test.js +75 -5
- package/src/cli/wiki-manager.js +22 -0
- package/src/contracts/schemas.js +249 -13
- package/src/contracts/schemas.test.js +147 -2
- 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 +30 -3
- package/src/core/mcp.test.js +69 -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/controlMessages.js +43 -0
- package/src/runtime/controlMessages.test.js +21 -0
- package/src/runtime/donna-contract.test.js +296 -33
- 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 +142 -24
- package/src/runtime/server.test.js +192 -62
- package/src/runtime/store.js +837 -2
- package/src/runtime/store.test.js +269 -28
- 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 +7 -11
- package/src/shell/repl.test.js +26 -5
- package/src/shell/tui.tsx +1 -0
- package/src/shell/useAgent.ts +5 -1
- package/src/shell/useSession.ts +75 -19
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { DatabaseSync } from 'node:sqlite';
|
|
@@ -11,6 +11,26 @@ function runtimeStateDir() {
|
|
|
11
11
|
return join(mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-')), '.wiki-manager');
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function plannedTask(id) {
|
|
15
|
+
return {
|
|
16
|
+
id,
|
|
17
|
+
label: 'Build A',
|
|
18
|
+
description: 'Build A',
|
|
19
|
+
requiredCapability: 'document.build',
|
|
20
|
+
operation: 'build',
|
|
21
|
+
arguments: { templates: ['templates/a.md'] },
|
|
22
|
+
dependsOn: [],
|
|
23
|
+
parallelizable: true,
|
|
24
|
+
inputRefs: [],
|
|
25
|
+
expectedOutputRefs: [{ type: 'file', ref: 'deliverables/a.md' }],
|
|
26
|
+
locks: ['deliverable:deliverables/a.md'],
|
|
27
|
+
requiresApproval: false,
|
|
28
|
+
idempotencyKey: 'test-key',
|
|
29
|
+
progressWeight: 1,
|
|
30
|
+
status: 'pending',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
14
34
|
test('runtime store persists and replays agent events into a projection', () => {
|
|
15
35
|
const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
|
|
16
36
|
const store = openRuntimeStore({ stateDir });
|
|
@@ -33,15 +53,184 @@ test('runtime store persists and replays agent events into a projection', () =>
|
|
|
33
53
|
reopened.close();
|
|
34
54
|
});
|
|
35
55
|
|
|
56
|
+
test('runtime store persists agent registry tables and exposes registry in state', () => {
|
|
57
|
+
const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
|
|
58
|
+
const store = openRuntimeStore({ stateDir });
|
|
59
|
+
const agent = {
|
|
60
|
+
agentInstanceId: 'production-main',
|
|
61
|
+
health: 'available',
|
|
62
|
+
firstSeenAt: '2026-01-01T00:00:00.000Z',
|
|
63
|
+
lastSeenAt: '2026-01-01T00:00:00.000Z',
|
|
64
|
+
description: {
|
|
65
|
+
contractVersion: '1',
|
|
66
|
+
agentType: 'production',
|
|
67
|
+
agentInstanceId: 'production-main',
|
|
68
|
+
displayName: 'Production',
|
|
69
|
+
capabilities: [{
|
|
70
|
+
id: 'knowledge.update',
|
|
71
|
+
version: '1',
|
|
72
|
+
description: 'Update wiki knowledge',
|
|
73
|
+
inputSchema: {},
|
|
74
|
+
outputSchema: {},
|
|
75
|
+
supportedOperations: ['ingest'],
|
|
76
|
+
defaultRequiresApproval: true,
|
|
77
|
+
}],
|
|
78
|
+
orchestration: {
|
|
79
|
+
canPlan: true,
|
|
80
|
+
canExpandPlan: false,
|
|
81
|
+
canExecute: true,
|
|
82
|
+
canCancel: true,
|
|
83
|
+
canResume: false,
|
|
84
|
+
supportsIdempotency: false,
|
|
85
|
+
supportsParallelWorkers: true,
|
|
86
|
+
},
|
|
87
|
+
limits: {
|
|
88
|
+
recommendedConcurrency: 1,
|
|
89
|
+
maxConcurrency: 2,
|
|
90
|
+
},
|
|
91
|
+
health: { status: 'available' },
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
store.persistEvent(createAgentEvent('agent.registered', {
|
|
96
|
+
origin: 'agent_registry',
|
|
97
|
+
payload: { agent },
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
assert.equal(store.db.prepare('SELECT COUNT(*) AS n FROM agents').get().n, 1);
|
|
101
|
+
assert.equal(store.db.prepare('SELECT COUNT(*) AS n FROM agent_capabilities').get().n, 1);
|
|
102
|
+
assert.equal(store.listAgents()[0].agentInstanceId, 'production-main');
|
|
103
|
+
const state = store.getState();
|
|
104
|
+
assert.equal(state.agents[0].agentInstanceId, 'production-main');
|
|
105
|
+
assert.equal(state.capabilityRegistry['knowledge.update@1'][0].agentInstanceId, 'production-main');
|
|
106
|
+
store.close();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('runtime store persists task assignments attempts and results from events', () => {
|
|
110
|
+
const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
|
|
111
|
+
const store = openRuntimeStore({ stateDir });
|
|
112
|
+
const runId = 'run-task-persistence';
|
|
113
|
+
const taskId = 'run-task-persistence:build-a';
|
|
114
|
+
const attemptId = 'attempt-1';
|
|
115
|
+
|
|
116
|
+
for (const event of [
|
|
117
|
+
createAgentEvent('run_started', {
|
|
118
|
+
origin: 'runtime',
|
|
119
|
+
runId,
|
|
120
|
+
workspace: 'docs',
|
|
121
|
+
payload: { input: 'build docs', workspace: 'docs' },
|
|
122
|
+
}),
|
|
123
|
+
createAgentEvent('task.created', {
|
|
124
|
+
origin: 'plan_integrator',
|
|
125
|
+
runId,
|
|
126
|
+
taskId,
|
|
127
|
+
workspace: 'docs',
|
|
128
|
+
payload: { runId, task: plannedTask(taskId) },
|
|
129
|
+
}),
|
|
130
|
+
createAgentEvent('task.assigned', {
|
|
131
|
+
origin: 'assignment_manager',
|
|
132
|
+
runId,
|
|
133
|
+
taskId,
|
|
134
|
+
workspace: 'docs',
|
|
135
|
+
payload: {
|
|
136
|
+
runId,
|
|
137
|
+
taskId,
|
|
138
|
+
attemptId,
|
|
139
|
+
assignment: {
|
|
140
|
+
attemptId,
|
|
141
|
+
agentInstanceId: 'production-main',
|
|
142
|
+
agentId: 'production',
|
|
143
|
+
poolId: 'production-pool',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
createAgentEvent('task.started', {
|
|
148
|
+
origin: 'dispatcher',
|
|
149
|
+
runId,
|
|
150
|
+
taskId,
|
|
151
|
+
workspace: 'docs',
|
|
152
|
+
payload: { runId, taskId, attemptId, jobId: 'job-1', startedAt: '2026-07-07T10:00:00.000Z' },
|
|
153
|
+
}),
|
|
154
|
+
createAgentEvent('task.result_returned', {
|
|
155
|
+
origin: 'result_aggregator',
|
|
156
|
+
runId,
|
|
157
|
+
taskId,
|
|
158
|
+
workspace: 'docs',
|
|
159
|
+
payload: {
|
|
160
|
+
runId,
|
|
161
|
+
taskId,
|
|
162
|
+
attemptId,
|
|
163
|
+
result: {
|
|
164
|
+
attemptId,
|
|
165
|
+
taskId,
|
|
166
|
+
jobId: 'job-1',
|
|
167
|
+
status: 'succeeded',
|
|
168
|
+
summary: 'Built A',
|
|
169
|
+
outputRefs: [{ type: 'file', ref: 'deliverables/a.md' }],
|
|
170
|
+
metrics: { durationMs: 42 },
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
createAgentEvent('task.completed', {
|
|
175
|
+
origin: 'result_aggregator',
|
|
176
|
+
runId,
|
|
177
|
+
taskId,
|
|
178
|
+
workspace: 'docs',
|
|
179
|
+
payload: { runId, taskId, attemptId, result: { attemptId, taskId, status: 'succeeded' } },
|
|
180
|
+
}),
|
|
181
|
+
createAgentEvent('task.retry_scheduled', {
|
|
182
|
+
origin: 'attempt_manager',
|
|
183
|
+
runId,
|
|
184
|
+
taskId,
|
|
185
|
+
workspace: 'docs',
|
|
186
|
+
payload: {
|
|
187
|
+
runId,
|
|
188
|
+
taskId,
|
|
189
|
+
attemptId: 'attempt-2',
|
|
190
|
+
reason: 'retryable_error',
|
|
191
|
+
error: { code: 'rate_limit', retryable: true },
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
]) {
|
|
195
|
+
store.persistEvent(event);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
assert.equal(store.db.prepare('SELECT COUNT(*) AS n FROM task_assignments').get().n, 1);
|
|
199
|
+
assert.equal(store.db.prepare('SELECT COUNT(*) AS n FROM task_attempts').get().n, 2);
|
|
200
|
+
assert.equal(store.db.prepare('SELECT COUNT(*) AS n FROM task_results').get().n, 1);
|
|
201
|
+
assert.equal(store.listTasks({ runId })[0].id, taskId);
|
|
202
|
+
assert.deepEqual(store.listTaskAssignments({ taskId })[0], {
|
|
203
|
+
taskId,
|
|
204
|
+
attemptId,
|
|
205
|
+
agentInstanceId: 'production-main',
|
|
206
|
+
agentId: 'production',
|
|
207
|
+
poolId: 'production-pool',
|
|
208
|
+
assignedAt: store.listTaskAssignments({ taskId })[0].assignedAt,
|
|
209
|
+
});
|
|
210
|
+
assert.deepEqual(store.listTaskAttempts({ taskId }).map((attempt) => attempt.status), ['done', 'retry_scheduled']);
|
|
211
|
+
assert.deepEqual(store.getTaskResult({ taskId }).outputRefs, [{ type: 'file', ref: 'deliverables/a.md' }]);
|
|
212
|
+
assert.equal(store.getTaskResult({ taskId }).metrics.durationMs, 42);
|
|
213
|
+
store.close();
|
|
214
|
+
|
|
215
|
+
const reopened = openRuntimeStore({ stateDir });
|
|
216
|
+
const session = { activities: {}, headlessPlan: null };
|
|
217
|
+
reopened.hydrateSession(session, { workspace: 'docs' });
|
|
218
|
+
assert.ok(reopened.getState(session, { workspace: 'docs' }).logs.some((line) => line === `Task assigned: ${taskId}`));
|
|
219
|
+
assert.equal(reopened.listTaskAttempts({ taskId })[0].jobId, 'job-1');
|
|
220
|
+
assert.equal(reopened.getTaskResult({ taskId }).status, 'succeeded');
|
|
221
|
+
reopened.close();
|
|
222
|
+
});
|
|
223
|
+
|
|
36
224
|
test('runtime store writes schema versions to sqlite and meta file', () => {
|
|
37
225
|
const root = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
|
|
38
226
|
const stateDir = join(root, '.wiki-manager');
|
|
39
227
|
const store = openRuntimeStore({ stateDir });
|
|
40
228
|
const version = store.db.prepare('PRAGMA user_version').get().user_version;
|
|
41
|
-
const meta = JSON.parse(readFileSync(join(root, '.wiki', 'meta.json'), 'utf8'));
|
|
229
|
+
const meta = JSON.parse(readFileSync(join(root, '.wiki-manager', 'meta.json'), 'utf8'));
|
|
42
230
|
|
|
43
231
|
assert.equal(version, 1);
|
|
44
232
|
assert.equal(meta.schemaVersion, 1);
|
|
233
|
+
assert.equal(existsSync(join(root, '.wiki', 'meta.json')), false);
|
|
45
234
|
store.close();
|
|
46
235
|
});
|
|
47
236
|
|
|
@@ -61,8 +250,8 @@ test('runtime store refuses unknown sqlite schema versions', () => {
|
|
|
61
250
|
test('runtime store refuses unknown runtime metadata versions', () => {
|
|
62
251
|
const root = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
|
|
63
252
|
const stateDir = join(root, '.wiki-manager');
|
|
64
|
-
mkdirSync(
|
|
65
|
-
writeFileSync(join(
|
|
253
|
+
mkdirSync(stateDir, { recursive: true });
|
|
254
|
+
writeFileSync(join(stateDir, 'meta.json'), '{"schemaVersion":99}\n', 'utf8');
|
|
66
255
|
|
|
67
256
|
assert.throws(
|
|
68
257
|
() => openRuntimeStore({ stateDir }),
|
|
@@ -70,6 +259,21 @@ test('runtime store refuses unknown runtime metadata versions', () => {
|
|
|
70
259
|
);
|
|
71
260
|
});
|
|
72
261
|
|
|
262
|
+
test('runtime store migrates legacy .wiki metadata to .wiki-manager', () => {
|
|
263
|
+
const root = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
|
|
264
|
+
const stateDir = join(root, '.wiki-manager');
|
|
265
|
+
mkdirSync(join(root, '.wiki'), { recursive: true });
|
|
266
|
+
writeFileSync(join(root, '.wiki', 'meta.json'), '{"schemaVersion":1,"legacy":true}\n', 'utf8');
|
|
267
|
+
|
|
268
|
+
const store = openRuntimeStore({ stateDir });
|
|
269
|
+
const meta = JSON.parse(readFileSync(join(stateDir, 'meta.json'), 'utf8'));
|
|
270
|
+
|
|
271
|
+
assert.equal(meta.schemaVersion, 1);
|
|
272
|
+
assert.equal(meta.legacy, true);
|
|
273
|
+
assert.equal(existsSync(join(root, '.wiki', 'meta.json')), false);
|
|
274
|
+
store.close();
|
|
275
|
+
});
|
|
276
|
+
|
|
73
277
|
test('runtime store persists duplicate events idempotently', () => {
|
|
74
278
|
const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
|
|
75
279
|
const store = openRuntimeStore({ stateDir });
|
|
@@ -77,8 +281,8 @@ test('runtime store persists duplicate events idempotently', () => {
|
|
|
77
281
|
origin: 'test',
|
|
78
282
|
runId: 'run-1',
|
|
79
283
|
turnId: 'run-1:turn-0',
|
|
80
|
-
workspace: '
|
|
81
|
-
payload: { input: 'build wiki', workspace: '
|
|
284
|
+
workspace: 'acme',
|
|
285
|
+
payload: { input: 'build wiki', workspace: 'acme' },
|
|
82
286
|
});
|
|
83
287
|
|
|
84
288
|
store.persistEvent(event);
|
|
@@ -88,7 +292,7 @@ test('runtime store persists duplicate events idempotently', () => {
|
|
|
88
292
|
assert.equal(store.listEvents()[0].sequence, 1);
|
|
89
293
|
assert.equal(store.getState().eventsCursor, 1);
|
|
90
294
|
assert.equal(store.listRuns()[0].id, 'run-1');
|
|
91
|
-
assert.equal(store.listRuns()[0].workspace, '
|
|
295
|
+
assert.equal(store.listRuns()[0].workspace, 'acme');
|
|
92
296
|
assert.equal(store.listRuns()[0].status, 'running');
|
|
93
297
|
store.close();
|
|
94
298
|
});
|
|
@@ -299,6 +503,43 @@ test('runtime store tracks pending approval run status', () => {
|
|
|
299
503
|
store.close();
|
|
300
504
|
});
|
|
301
505
|
|
|
506
|
+
test('runtime store persists bounded approval grants', () => {
|
|
507
|
+
const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
|
|
508
|
+
const store = openRuntimeStore({ stateDir });
|
|
509
|
+
store.persistEvent(createAgentEvent('approval.granted', {
|
|
510
|
+
origin: 'runtime',
|
|
511
|
+
runId: 'run-approval-grant',
|
|
512
|
+
workspace: 'docs',
|
|
513
|
+
payload: {
|
|
514
|
+
id: 'grant-1',
|
|
515
|
+
scope: 'run',
|
|
516
|
+
runId: 'run-approval-grant',
|
|
517
|
+
workspaceId: 'docs',
|
|
518
|
+
planRevision: 3,
|
|
519
|
+
approvalClasses: ['workspace-write'],
|
|
520
|
+
reason: 'valide tout',
|
|
521
|
+
},
|
|
522
|
+
}));
|
|
523
|
+
|
|
524
|
+
const grants = store.listApprovalGrants({ runId: 'run-approval-grant' });
|
|
525
|
+
assert.deepEqual(grants, [{
|
|
526
|
+
id: 'grant-1',
|
|
527
|
+
runId: 'run-approval-grant',
|
|
528
|
+
workspaceId: 'docs',
|
|
529
|
+
planRevision: 3,
|
|
530
|
+
scope: 'run',
|
|
531
|
+
taskId: null,
|
|
532
|
+
groupId: null,
|
|
533
|
+
approvalClasses: ['workspace-write'],
|
|
534
|
+
status: 'approved',
|
|
535
|
+
reason: 'valide tout',
|
|
536
|
+
createdAt: grants[0].createdAt,
|
|
537
|
+
grantedAt: grants[0].grantedAt,
|
|
538
|
+
rejectedAt: null,
|
|
539
|
+
}]);
|
|
540
|
+
store.close();
|
|
541
|
+
});
|
|
542
|
+
|
|
302
543
|
test('runtime store orders events by durable sequence', () => {
|
|
303
544
|
const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
|
|
304
545
|
const store = openRuntimeStore({ stateDir });
|
|
@@ -349,19 +590,19 @@ test('runtime store persists cancelled run status', () => {
|
|
|
349
590
|
origin: 'test',
|
|
350
591
|
runId: 'run-1',
|
|
351
592
|
turnId: 'run-1:turn-0',
|
|
352
|
-
workspace: '
|
|
353
|
-
payload: { input: 'build wiki', workspace: '
|
|
593
|
+
workspace: 'acme',
|
|
594
|
+
payload: { input: 'build wiki', workspace: 'acme' },
|
|
354
595
|
}));
|
|
355
596
|
store.persistEvent(createAgentEvent('run_cancelled', {
|
|
356
597
|
origin: 'test',
|
|
357
598
|
runId: 'run-1',
|
|
358
599
|
turnId: 'run-1:turn-1',
|
|
359
|
-
workspace: '
|
|
360
|
-
payload: { runId: 'run-1', workspace: '
|
|
600
|
+
workspace: 'acme',
|
|
601
|
+
payload: { runId: 'run-1', workspace: 'acme' },
|
|
361
602
|
}));
|
|
362
603
|
|
|
363
604
|
assert.equal(store.listRuns()[0].status, 'cancelled');
|
|
364
|
-
assert.equal(store.listRuns()[0].workspace, '
|
|
605
|
+
assert.equal(store.listRuns()[0].workspace, 'acme');
|
|
365
606
|
store.close();
|
|
366
607
|
});
|
|
367
608
|
|
|
@@ -483,10 +724,10 @@ test('runtime store filters events runs and queue by workspace', () => {
|
|
|
483
724
|
const store = openRuntimeStore({ stateDir });
|
|
484
725
|
store.persistEvent(createAgentEvent('run_started', {
|
|
485
726
|
origin: 'test',
|
|
486
|
-
runId: 'run-
|
|
487
|
-
turnId: 'run-
|
|
488
|
-
workspace: '
|
|
489
|
-
payload: { input: '
|
|
727
|
+
runId: 'run-acme',
|
|
728
|
+
turnId: 'run-acme:turn-0',
|
|
729
|
+
workspace: 'acme',
|
|
730
|
+
payload: { input: 'acme', workspace: 'acme' },
|
|
490
731
|
}));
|
|
491
732
|
store.persistEvent(createAgentEvent('run_started', {
|
|
492
733
|
origin: 'test',
|
|
@@ -495,12 +736,12 @@ test('runtime store filters events runs and queue by workspace', () => {
|
|
|
495
736
|
workspace: 'docs',
|
|
496
737
|
payload: { input: 'docs', workspace: 'docs' },
|
|
497
738
|
}));
|
|
498
|
-
store.saveQueue([{ id: 'q-
|
|
739
|
+
store.saveQueue([{ id: 'q-acme', workspace: 'acme', status: 'waiting' }], { workspace: 'acme' });
|
|
499
740
|
store.saveQueue([{ id: 'q-docs', workspace: 'docs', status: 'waiting' }], { workspace: 'docs' });
|
|
500
741
|
|
|
501
|
-
assert.deepEqual(store.listEvents({ workspace: '
|
|
742
|
+
assert.deepEqual(store.listEvents({ workspace: 'acme' }).map((event) => event.runId), ['run-acme']);
|
|
502
743
|
assert.deepEqual(store.listRuns({ workspace: 'docs' }).map((run) => run.id), ['run-docs']);
|
|
503
|
-
assert.deepEqual(store.listQueue({ workspace: '
|
|
744
|
+
assert.deepEqual(store.listQueue({ workspace: 'acme' }).map((item) => item.id), ['q-acme']);
|
|
504
745
|
|
|
505
746
|
const session = { activities: {}, headlessPlan: null };
|
|
506
747
|
store.hydrateSession(session, { workspace: 'docs' });
|
|
@@ -553,8 +794,8 @@ test('runtime store persists and replays control queue events without mixing MCP
|
|
|
553
794
|
}));
|
|
554
795
|
store.persistEvent(createAgentEvent('control_enqueued', {
|
|
555
796
|
origin: 'runtime',
|
|
556
|
-
workspace: '
|
|
557
|
-
payload: { id: 'control-
|
|
797
|
+
workspace: 'acme',
|
|
798
|
+
payload: { id: 'control-acme', workspace: 'acme', input: 'other' },
|
|
558
799
|
}));
|
|
559
800
|
store.close();
|
|
560
801
|
|
|
@@ -684,10 +925,10 @@ test('runtime store identifies recoverable workspaces and interrupts stale runs'
|
|
|
684
925
|
const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
|
|
685
926
|
const store = openRuntimeStore({ stateDir });
|
|
686
927
|
store.persistRun({
|
|
687
|
-
id: 'run-
|
|
688
|
-
workspace: '
|
|
928
|
+
id: 'run-acme',
|
|
929
|
+
workspace: 'acme',
|
|
689
930
|
status: 'running',
|
|
690
|
-
input: 'build
|
|
931
|
+
input: 'build acme',
|
|
691
932
|
});
|
|
692
933
|
store.persistRun({
|
|
693
934
|
id: 'run-done',
|
|
@@ -697,9 +938,9 @@ test('runtime store identifies recoverable workspaces and interrupts stale runs'
|
|
|
697
938
|
});
|
|
698
939
|
store.saveQueue([{ id: 'q-docs', workspace: 'docs', status: 'waiting' }], { workspace: 'docs' });
|
|
699
940
|
|
|
700
|
-
assert.deepEqual(store.listRecoverableWorkspaces(), ['
|
|
701
|
-
assert.deepEqual(store.listRecoverableRuns({ workspace: '
|
|
702
|
-
assert.equal(store.interruptRuns({ workspace: '
|
|
703
|
-
assert.equal(store.listRuns({ workspace: '
|
|
941
|
+
assert.deepEqual(store.listRecoverableWorkspaces(), ['acme', 'docs']);
|
|
942
|
+
assert.deepEqual(store.listRecoverableRuns({ workspace: 'acme' }).map((run) => run.id), ['run-acme']);
|
|
943
|
+
assert.equal(store.interruptRuns({ workspace: 'acme' }), 1);
|
|
944
|
+
assert.equal(store.listRuns({ workspace: 'acme' })[0].status, 'interrupted');
|
|
704
945
|
store.close();
|
|
705
946
|
});
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
2
2
|
import { extractActivity, parseJsonText, sessionActivities } from '../core/activity.js';
|
|
3
3
|
import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
|
|
4
|
+
import { normalizeRuntimeLog } from '../core/runtimeLog.js';
|
|
4
5
|
import { startNextQueuedJob, syncQueueWithActivity } from '../core/jobQueue.js';
|
|
6
|
+
import { createAgentRegistry } from '../orchestrator/agentRegistry.js';
|
|
5
7
|
|
|
6
8
|
export function startActivitySupervisor(session, {
|
|
7
9
|
intervalMs = 1000,
|
|
8
10
|
queueIntervalMs = 10000,
|
|
11
|
+
agentRegistryIntervalMs = registryIntervalFromEnv(),
|
|
12
|
+
agentRegistry = null,
|
|
9
13
|
callTool = callMcpTool,
|
|
10
14
|
} = {}) {
|
|
11
15
|
const pollBusy = new Set();
|
|
12
16
|
let stopped = false;
|
|
13
17
|
let runSignal = null;
|
|
18
|
+
const registry = agentRegistry ?? createAgentRegistry({ callTool });
|
|
19
|
+
session.agentRegistry ??= registry;
|
|
14
20
|
|
|
15
21
|
const pollTimer = setInterval(() => {
|
|
16
22
|
void pollActivitiesOnce(session, { pollBusy, callTool, signal: runSignal });
|
|
@@ -24,7 +30,14 @@ export function startActivitySupervisor(session, {
|
|
|
24
30
|
}
|
|
25
31
|
}, queueIntervalMs);
|
|
26
32
|
|
|
33
|
+
const agentRegistryTimer = agentRegistryIntervalMs > 0
|
|
34
|
+
? setInterval(() => {
|
|
35
|
+
void discoverAgentsOnce(session, { registry, signal: runSignal });
|
|
36
|
+
}, agentRegistryIntervalMs)
|
|
37
|
+
: null;
|
|
38
|
+
|
|
27
39
|
void pollActivitiesOnce(session, { pollBusy, callTool, signal: runSignal });
|
|
40
|
+
void discoverAgentsOnce(session, { registry, signal: runSignal });
|
|
28
41
|
|
|
29
42
|
return {
|
|
30
43
|
pollBusy,
|
|
@@ -36,11 +49,21 @@ export function startActivitySupervisor(session, {
|
|
|
36
49
|
stopped = true;
|
|
37
50
|
clearInterval(pollTimer);
|
|
38
51
|
clearInterval(queueTimer);
|
|
52
|
+
if (agentRegistryTimer) clearInterval(agentRegistryTimer);
|
|
39
53
|
pollBusy.clear();
|
|
40
54
|
},
|
|
41
55
|
};
|
|
42
56
|
}
|
|
43
57
|
|
|
58
|
+
export async function discoverAgentsOnce(session, {
|
|
59
|
+
registry = session?.agentRegistry ?? createAgentRegistry(),
|
|
60
|
+
signal = null,
|
|
61
|
+
} = {}) {
|
|
62
|
+
if (!session) return [];
|
|
63
|
+
session.agentRegistry ??= registry;
|
|
64
|
+
return registry.discover(session, { signal });
|
|
65
|
+
}
|
|
66
|
+
|
|
44
67
|
export async function pollActivitiesOnce(session, {
|
|
45
68
|
pollBusy = new Set(),
|
|
46
69
|
callTool = callMcpTool,
|
|
@@ -97,8 +120,17 @@ export async function pollActivitiesOnce(session, {
|
|
|
97
120
|
}
|
|
98
121
|
|
|
99
122
|
export function emitRuntimeLog(session, message) {
|
|
123
|
+
const payload = normalizeRuntimeLog(message, { session });
|
|
100
124
|
dispatchAgentEvent(session, createAgentEvent('runtime_log', {
|
|
101
125
|
origin: 'runtime',
|
|
102
|
-
|
|
126
|
+
runId: payload.runId ?? null,
|
|
127
|
+
taskId: payload.taskId ?? null,
|
|
128
|
+
workspace: payload.workspaceId ?? null,
|
|
129
|
+
payload,
|
|
103
130
|
}));
|
|
104
131
|
}
|
|
132
|
+
|
|
133
|
+
function registryIntervalFromEnv() {
|
|
134
|
+
const value = Number(process.env.WIKI_MANAGER_AGENT_REGISTRY_INTERVAL_MS);
|
|
135
|
+
return Number.isFinite(value) && value >= 0 ? Math.floor(value) : 60000;
|
|
136
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
3
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
4
|
-
import { pollActivitiesOnce, startActivitySupervisor } from './supervisor.js';
|
|
4
|
+
import { discoverAgentsOnce, pollActivitiesOnce, startActivitySupervisor } from './supervisor.js';
|
|
5
5
|
|
|
6
6
|
test('pollActivitiesOnce updates activity through the event reducer', async () => {
|
|
7
7
|
const session = {
|
|
@@ -154,6 +154,52 @@ test('startActivitySupervisor passes the active run signal to background polls',
|
|
|
154
154
|
}
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
+
test('startActivitySupervisor periodically re-scans the agent registry', async () => {
|
|
158
|
+
const session = {
|
|
159
|
+
mcp: {},
|
|
160
|
+
activities: {},
|
|
161
|
+
headlessPlan: null,
|
|
162
|
+
jobQueue: [],
|
|
163
|
+
};
|
|
164
|
+
let discoveries = 0;
|
|
165
|
+
const registry = {
|
|
166
|
+
async discover() {
|
|
167
|
+
discoveries += 1;
|
|
168
|
+
return [];
|
|
169
|
+
},
|
|
170
|
+
snapshot() {
|
|
171
|
+
return [];
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const supervisor = startActivitySupervisor(session, {
|
|
176
|
+
intervalMs: 1000,
|
|
177
|
+
queueIntervalMs: 1000,
|
|
178
|
+
agentRegistryIntervalMs: 10,
|
|
179
|
+
agentRegistry: registry,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await waitFor(() => discoveries >= 2);
|
|
184
|
+
assert.equal(session.agentRegistry, registry);
|
|
185
|
+
} finally {
|
|
186
|
+
supervisor.stop();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('discoverAgentsOnce uses the session registry and returns discovered agents', async () => {
|
|
191
|
+
const expected = [{ agentInstanceId: 'a' }];
|
|
192
|
+
const registry = {
|
|
193
|
+
async discover(session) {
|
|
194
|
+
assert.equal(session.workspace, 'docs');
|
|
195
|
+
return expected;
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
const session = { workspace: 'docs', agentRegistry: registry };
|
|
199
|
+
|
|
200
|
+
assert.equal(await discoverAgentsOnce(session), expected);
|
|
201
|
+
});
|
|
202
|
+
|
|
157
203
|
test('pollActivitiesOnce fires _onActivitiesTerminal when last activity becomes terminal', async () => {
|
|
158
204
|
let fired = false;
|
|
159
205
|
const session = {
|
package/src/shell/RightPane.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import { createMemo, Index, Show } from 'solid-js';
|
|
3
|
+
import { filterRuntimeLogs } from '../core/runtimeLog.js';
|
|
3
4
|
import { fit } from './textFit';
|
|
4
5
|
|
|
5
6
|
type PlanStep = { step: number; description: string; status: string };
|
|
@@ -182,9 +183,10 @@ export function ActivityPanel(props: { activities: any[]; width: number }) {
|
|
|
182
183
|
);
|
|
183
184
|
}
|
|
184
185
|
|
|
185
|
-
export function LogPanel(props: { logs: string[]; width: number }) {
|
|
186
|
+
export function LogPanel(props: { logs: string[]; width: number; filter?: string }) {
|
|
186
187
|
const lineWidth = () => Math.max(8, props.width - 2);
|
|
187
|
-
const
|
|
188
|
+
const filteredLogs = () => filterRuntimeLogs(props.logs, props.filter ?? '');
|
|
189
|
+
const visibleLines = () => filteredLogs().flatMap((line) => wrapLine(line, lineWidth())).slice(-24);
|
|
188
190
|
const logLineAt = (index: number) => visibleLines()[index] ?? '';
|
|
189
191
|
return (
|
|
190
192
|
<box flexGrow={1} flexDirection="column" padding={1}>
|
|
@@ -202,8 +204,8 @@ export function LogPanel(props: { logs: string[]; width: number }) {
|
|
|
202
204
|
fallback={<text width={lineWidth()} fg="#AAB7C4" content={line()} />}
|
|
203
205
|
>
|
|
204
206
|
<box height={1} flexDirection="row" overflow="hidden">
|
|
205
|
-
<text width={timeWidth()} fg="#89B4FA" content={
|
|
206
|
-
<text width={messageWidth()} fg="#AAB7C4" content={
|
|
207
|
+
<text width={timeWidth()} fg="#89B4FA" content={`${parts().time} `} />
|
|
208
|
+
<text width={messageWidth()} fg="#AAB7C4" content={parts().message} />
|
|
207
209
|
</box>
|
|
208
210
|
</Show>
|
|
209
211
|
);
|
|
@@ -284,6 +286,7 @@ export function RightPane(props: {
|
|
|
284
286
|
queueItems: QueueItem[];
|
|
285
287
|
queueInfo: QueueInfo;
|
|
286
288
|
activeTab: 'plan' | 'queue';
|
|
289
|
+
logFilter?: string;
|
|
287
290
|
onTabClick: (tab: 'plan' | 'queue') => void;
|
|
288
291
|
}) {
|
|
289
292
|
const planJobName = () => activityJobName(
|
|
@@ -302,7 +305,7 @@ export function RightPane(props: {
|
|
|
302
305
|
)}>
|
|
303
306
|
<QueuePanel width={props.width} items={props.queueItems} info={props.queueInfo} />
|
|
304
307
|
</Show>
|
|
305
|
-
<LogPanel width={props.width} logs={props.logs} />
|
|
308
|
+
<LogPanel width={props.width} logs={props.logs} filter={props.logFilter} />
|
|
306
309
|
</box>
|
|
307
310
|
);
|
|
308
311
|
}
|
package/src/shell/repl.js
CHANGED
|
@@ -797,14 +797,6 @@ export function applyRuntimeStateToShellSession(session, state) {
|
|
|
797
797
|
updatedAt: production.updatedAt,
|
|
798
798
|
};
|
|
799
799
|
}
|
|
800
|
-
const runtimeConversation = session.agentProjection.conversation;
|
|
801
|
-
if (runtimeConversation.length > 0) {
|
|
802
|
-
session.conversations ??= { [GLOBAL_CONVERSATION_KEY]: [] };
|
|
803
|
-
session.conversations[conversationKey(session)] = runtimeConversation.map((message) => ({
|
|
804
|
-
role: message.role === 'assistant' ? 'donna' : message.role,
|
|
805
|
-
content: String(message.content ?? ''),
|
|
806
|
-
}));
|
|
807
|
-
}
|
|
808
800
|
return true;
|
|
809
801
|
}
|
|
810
802
|
|
|
@@ -814,7 +806,10 @@ export function applyRuntimeStateToShellSession(session, state) {
|
|
|
814
806
|
export async function submitRuntimeRun(line, { runtime, session }) {
|
|
815
807
|
const workspace = session.workspace ?? null;
|
|
816
808
|
try {
|
|
817
|
-
await postRuntimeRun(line, { url: runtime.url, workspace });
|
|
809
|
+
const result = await postRuntimeRun(line, { url: runtime.url, workspace });
|
|
810
|
+
if (result?.queued || result?.kind === 'enqueue_run' || result?.kind === 'enqueue') {
|
|
811
|
+
return { kind: 'queued', result };
|
|
812
|
+
}
|
|
818
813
|
return { kind: 'accepted' };
|
|
819
814
|
} catch (err) {
|
|
820
815
|
if (err?.status !== 409) {
|
|
@@ -1585,14 +1580,15 @@ async function runTuiShell({ agent, packageJson, session, runtime = null }) {
|
|
|
1585
1580
|
if (outcome.kind === 'accepted') {
|
|
1586
1581
|
activityLines = [...activityLines, 'runtime: run accepted'].slice(-LOWER_DETAIL_ROWS);
|
|
1587
1582
|
} else if (outcome.kind === 'queued') {
|
|
1588
|
-
|
|
1583
|
+
// Server-localized acknowledgement (src/runtime/controlMessages.js).
|
|
1584
|
+
conversationMessages(session).push({ role: 'command', content: String(outcome.result?.explanation ?? 'Request added to the queue.') });
|
|
1589
1585
|
activityLines = [...activityLines, 'runtime: control queued'].slice(-LOWER_DETAIL_ROWS);
|
|
1590
1586
|
} else if (outcome.kind === 'observe' || outcome.kind === 'converse' || outcome.kind === 'mutate') {
|
|
1591
1587
|
const explanation = outcome.result?.explanation ?? 'Runtime control message accepted.';
|
|
1592
1588
|
conversationMessages(session).push({ role: 'command', content: explanation });
|
|
1593
1589
|
activityLines = [...activityLines, `runtime: ${outcome.kind}`].slice(-LOWER_DETAIL_ROWS);
|
|
1594
1590
|
} else if (outcome.kind === 'ambiguous') {
|
|
1595
|
-
conversationMessages(session).push({ role: 'command', content: 'Runtime could not classify that message. Use /queue for a future run, or ask/status more explicitly.' });
|
|
1591
|
+
conversationMessages(session).push({ role: 'command', content: String(outcome.result?.explanation ?? 'Runtime could not classify that message. Use /queue for a future run, or ask/status more explicitly.') });
|
|
1596
1592
|
activityLines = [...activityLines, 'runtime: ambiguous control'].slice(-LOWER_DETAIL_ROWS);
|
|
1597
1593
|
} else {
|
|
1598
1594
|
conversationMessages(session).push({ role: 'command', content: `Runtime error: ${outcome.message}` });
|