@dotdrelle/wiki-manager 0.11.10 → 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
|
@@ -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 });
|
|
@@ -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 });
|
|
@@ -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) {
|
package/src/shell/repl.test.js
CHANGED
|
@@ -59,6 +59,10 @@ test('applyRuntimeStateToShellSession projects runtime state into shell session'
|
|
|
59
59
|
|
|
60
60
|
assert.equal(applied, true);
|
|
61
61
|
assert.equal(session.agentProjection.status, 'running');
|
|
62
|
+
assert.deepEqual(session.agentProjection.conversation, [
|
|
63
|
+
{ role: 'user', content: 'Build docs' },
|
|
64
|
+
{ role: 'assistant', content: 'Working.' },
|
|
65
|
+
]);
|
|
62
66
|
assert.equal(session.headlessPlan[0].description, 'Build');
|
|
63
67
|
assert.equal(session.activities['production:job-1'].status, 'running');
|
|
64
68
|
assert.equal(session.productionActivity.jobId, 'job-1');
|
|
@@ -66,10 +70,7 @@ test('applyRuntimeStateToShellSession projects runtime state into shell session'
|
|
|
66
70
|
assert.equal(session.workflow.nodes[0].id, 'task:build');
|
|
67
71
|
assert.equal(session.workflow.relations[0].type, 'contains');
|
|
68
72
|
assert.deepEqual(session.workflow.waitingReasons, ['queue:q-1']);
|
|
69
|
-
assert.deepEqual(conversationMessages(session), [
|
|
70
|
-
{ role: 'user', content: 'Build docs' },
|
|
71
|
-
{ role: 'donna', content: 'Working.' },
|
|
72
|
-
]);
|
|
73
|
+
assert.deepEqual(conversationMessages(session), []);
|
|
73
74
|
});
|
|
74
75
|
|
|
75
76
|
test('submitRuntimeRun reports acceptance without throwing', async () => {
|
|
@@ -108,6 +109,26 @@ test('submitRuntimeRun routes busy runtime input through the control lane', asyn
|
|
|
108
109
|
}
|
|
109
110
|
});
|
|
110
111
|
|
|
112
|
+
test('submitRuntimeRun reports queued runs from non-blocking runtime', async () => {
|
|
113
|
+
const restore = stubFetch(async (url) => {
|
|
114
|
+
assert.equal(pathOf(url), '/run');
|
|
115
|
+
return jsonResponse(202, {
|
|
116
|
+
accepted: true,
|
|
117
|
+
queued: true,
|
|
118
|
+
kind: 'enqueue_run',
|
|
119
|
+
item: { id: 'control-1', status: 'queued' },
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
const session = createSession();
|
|
124
|
+
const outcome = await submitRuntimeRun('build the doc', { runtime: { url: 'http://runtime.test' }, session });
|
|
125
|
+
assert.equal(outcome.kind, 'queued');
|
|
126
|
+
assert.equal(outcome.result.item.id, 'control-1');
|
|
127
|
+
} finally {
|
|
128
|
+
restore();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
111
132
|
test('submitRuntimeRun reports a non-409 error without throwing or calling /control', async () => {
|
|
112
133
|
let controlCalled = false;
|
|
113
134
|
const restore = stubFetch(async (url) => {
|
package/src/shell/tui.tsx
CHANGED
|
@@ -336,6 +336,7 @@ function App(props: {
|
|
|
336
336
|
queueItems={state.queueItems()}
|
|
337
337
|
queueInfo={state.queueInfo()}
|
|
338
338
|
activeTab={state.rightTab()}
|
|
339
|
+
logFilter={state.runtimeLogFilter()}
|
|
339
340
|
onTabClick={state.selectRightTab}
|
|
340
341
|
/>
|
|
341
342
|
<SlashDialog context={state.activeEditor() ? null : state.slash()} />
|