@dotdrelle/wiki-manager 0.7.3 → 0.9.3
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/.env.example +20 -0
- package/README.md +50 -1
- package/docker-compose.yml +1 -23
- package/mcp.endpoints.example.json +13 -0
- package/package.json +2 -2
- package/src/agent/graph.js +101 -15
- package/src/agent/graph.test.js +145 -0
- package/src/cli/wiki-manager.js +306 -53
- package/src/commands/slash.js +4 -24
- package/src/core/agentEvents.js +169 -4
- package/src/core/agentEvents.test.js +176 -4
- package/src/core/agentLoop.js +3 -0
- package/src/core/compose.js +1 -2
- package/src/core/dockerCompose.test.js +5 -5
- package/src/core/jobQueue.js +29 -12
- package/src/core/mcp.js +120 -10
- package/src/core/mcp.test.js +121 -1
- package/src/core/plan.js +33 -0
- package/src/core/queueStore.test.js +1 -0
- package/src/core/sessionConfig.js +24 -0
- package/src/core/wikiWorkspace.test.js +24 -0
- package/src/runtime/approvals.js +113 -0
- package/src/runtime/auth.test.js +8 -0
- package/src/runtime/client.js +52 -6
- package/src/runtime/lifecycle.js +27 -3
- package/src/runtime/queueStore.js +3 -3
- package/src/runtime/runner.js +340 -0
- package/src/runtime/runner.test.js +270 -0
- package/src/runtime/server.js +252 -33
- package/src/runtime/server.test.js +577 -0
- package/src/runtime/store.js +181 -39
- package/src/runtime/store.test.js +363 -4
- package/src/runtime/supervisor.js +6 -0
- package/src/runtime/supervisor.test.js +141 -0
- package/src/shell/RightPane.tsx +1 -1
- package/src/shell/repl.js +22 -6
- package/src/shell/useAgent.ts +1 -1
- package/src/shell/useSession.ts +10 -5
- package/wiki-workspace +198 -4
package/src/core/agentEvents.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { normalizeActivity } from './activity.js';
|
|
2
|
-
import { syncActivitiesToPlan } from './plan.js';
|
|
2
|
+
import { attachActivityToExistingPlan, syncActivitiesToPlan } from './plan.js';
|
|
3
3
|
|
|
4
4
|
const SESSION_PROJECTION_EVENTS = new Set([
|
|
5
5
|
'run_started',
|
|
6
6
|
'plan_set',
|
|
7
7
|
'plan_step_updated',
|
|
8
8
|
'activity_upserted',
|
|
9
|
+
'run_evaluated',
|
|
10
|
+
'run_replanned',
|
|
11
|
+
'run_pending_approval',
|
|
12
|
+
'run_approved',
|
|
13
|
+
'tool_pending_approval',
|
|
14
|
+
'tool_approved',
|
|
15
|
+
'control_enqueued',
|
|
16
|
+
'control_started',
|
|
17
|
+
'run_done',
|
|
9
18
|
'run_error',
|
|
10
19
|
'run_cancelled',
|
|
11
20
|
'runtime_log',
|
|
@@ -19,9 +28,16 @@ const PLAN_MUTATING_EVENTS = new Set([
|
|
|
19
28
|
'plan_set',
|
|
20
29
|
'plan_step_updated',
|
|
21
30
|
'activity_upserted',
|
|
31
|
+
'run_done',
|
|
22
32
|
]);
|
|
23
33
|
|
|
24
|
-
export function createAgentEvent(type, {
|
|
34
|
+
export function createAgentEvent(type, {
|
|
35
|
+
origin = 'system',
|
|
36
|
+
payload = {},
|
|
37
|
+
runId = null,
|
|
38
|
+
turnId = null,
|
|
39
|
+
workspace = null,
|
|
40
|
+
} = {}) {
|
|
25
41
|
return {
|
|
26
42
|
id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
|
|
27
43
|
ts: new Date().toISOString(),
|
|
@@ -29,12 +45,16 @@ export function createAgentEvent(type, { origin = 'system', payload = {}, runId
|
|
|
29
45
|
origin,
|
|
30
46
|
runId,
|
|
31
47
|
turnId,
|
|
48
|
+
workspace,
|
|
32
49
|
payload,
|
|
33
50
|
};
|
|
34
51
|
}
|
|
35
52
|
|
|
36
53
|
export function dispatchAgentEvent(session, event) {
|
|
37
|
-
const normalized =
|
|
54
|
+
const normalized = withSessionRunIdentity(
|
|
55
|
+
event.id && event.ts ? event : createAgentEvent(event.type, event),
|
|
56
|
+
session,
|
|
57
|
+
);
|
|
38
58
|
const tracksPlan = PLAN_MUTATING_EVENTS.has(normalized.type);
|
|
39
59
|
const previousPlan = tracksPlan ? JSON.stringify(session.headlessPlan ?? null) : null;
|
|
40
60
|
session.agentEvents ??= [];
|
|
@@ -52,6 +72,17 @@ export function dispatchAgentEvent(session, event) {
|
|
|
52
72
|
return normalized;
|
|
53
73
|
}
|
|
54
74
|
|
|
75
|
+
function withSessionRunIdentity(event, session) {
|
|
76
|
+
const identity = session?._currentRunIdentity;
|
|
77
|
+
if (!identity) return event;
|
|
78
|
+
return {
|
|
79
|
+
...event,
|
|
80
|
+
runId: event.runId ?? identity.runId ?? null,
|
|
81
|
+
turnId: event.turnId ?? identity.turnId ?? null,
|
|
82
|
+
workspace: event.workspace ?? identity.workspace ?? null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
55
86
|
export function reduceAgentEvents(events = []) {
|
|
56
87
|
const state = createProjectionState();
|
|
57
88
|
|
|
@@ -69,6 +100,10 @@ function createProjectionState() {
|
|
|
69
100
|
plan: null,
|
|
70
101
|
activities: {},
|
|
71
102
|
logs: [],
|
|
103
|
+
evaluation: null,
|
|
104
|
+
replans: [],
|
|
105
|
+
approvals: [],
|
|
106
|
+
controlQueue: [],
|
|
72
107
|
summary: null,
|
|
73
108
|
status: 'idle',
|
|
74
109
|
};
|
|
@@ -81,6 +116,10 @@ function publicProjection(state) {
|
|
|
81
116
|
plan: state.plan ? state.plan.map((step) => ({ ...step })) : null,
|
|
82
117
|
activities: sortedActivities(state.activities).map((activity) => ({ ...activity })),
|
|
83
118
|
logs: [...state.logs],
|
|
119
|
+
evaluation: state.evaluation ? { ...state.evaluation } : null,
|
|
120
|
+
replans: state.replans.map((replan) => ({ ...replan, plan: [...(replan.plan ?? [])] })),
|
|
121
|
+
approvals: state.approvals.map((approval) => ({ ...approval })),
|
|
122
|
+
controlQueue: state.controlQueue.map((item) => ({ ...item })),
|
|
84
123
|
summary: state.summary,
|
|
85
124
|
status: state.status,
|
|
86
125
|
};
|
|
@@ -89,6 +128,7 @@ function publicProjection(state) {
|
|
|
89
128
|
export function applyAgentProjectionToSession(session, projection) {
|
|
90
129
|
session.headlessPlan = projection.plan ? projection.plan.map((step) => ({ ...step })) : null;
|
|
91
130
|
session.activities = Object.fromEntries((projection.activities ?? []).map((activity) => [activity.key, { ...activity }]));
|
|
131
|
+
session.controlQueue = (projection.controlQueue ?? []).map((item) => ({ ...item }));
|
|
92
132
|
const production = (projection.activities ?? []).filter((activity) => activity.source === 'production').at(-1);
|
|
93
133
|
session.productionActivity = production ? {
|
|
94
134
|
jobId: production.id,
|
|
@@ -103,10 +143,13 @@ function applyEvent(state, event) {
|
|
|
103
143
|
switch (event.type) {
|
|
104
144
|
case 'run_started':
|
|
105
145
|
state.status = 'running';
|
|
106
|
-
state.plan =
|
|
146
|
+
state.plan = defaultRunPlan();
|
|
107
147
|
state.chain = [];
|
|
108
148
|
state.activities = {};
|
|
109
149
|
state.logs = [];
|
|
150
|
+
state.evaluation = null;
|
|
151
|
+
state.replans = [];
|
|
152
|
+
state.approvals = [];
|
|
110
153
|
state.summary = null;
|
|
111
154
|
return;
|
|
112
155
|
case 'user_message':
|
|
@@ -144,16 +187,80 @@ function applyEvent(state, event) {
|
|
|
144
187
|
state.summary = String(event.payload?.content ?? '');
|
|
145
188
|
if (state.summary) state.conversation.push({ role: 'assistant', content: state.summary });
|
|
146
189
|
return;
|
|
190
|
+
case 'run_evaluated':
|
|
191
|
+
state.evaluation = {
|
|
192
|
+
ok: event.payload?.ok === true,
|
|
193
|
+
reason: String(event.payload?.reason ?? ''),
|
|
194
|
+
suggestedAction: event.payload?.suggestedAction ?? null,
|
|
195
|
+
runId: event.runId ?? event.payload?.runId ?? null,
|
|
196
|
+
};
|
|
197
|
+
return;
|
|
198
|
+
case 'run_replanned':
|
|
199
|
+
state.replans.push({
|
|
200
|
+
reason: String(event.payload?.reason ?? ''),
|
|
201
|
+
plan: Array.isArray(event.payload?.plan) ? event.payload.plan.map(String) : [],
|
|
202
|
+
replansLeft: Number(event.payload?.replansLeft ?? 0),
|
|
203
|
+
runId: event.runId ?? event.payload?.runId ?? null,
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
case 'run_pending_approval':
|
|
207
|
+
case 'tool_pending_approval':
|
|
208
|
+
upsertApproval(state, {
|
|
209
|
+
id: event.payload?.approvalId ?? event.payload?.runId ?? event.payload?.itemId ?? event.id,
|
|
210
|
+
scope: event.type === 'run_pending_approval' ? 'run' : 'tool',
|
|
211
|
+
status: 'pending_approval',
|
|
212
|
+
runId: event.runId ?? event.payload?.runId ?? null,
|
|
213
|
+
itemId: event.payload?.itemId ?? null,
|
|
214
|
+
reason: event.payload?.reason ?? null,
|
|
215
|
+
tool: event.payload?.tool ?? null,
|
|
216
|
+
plan: event.payload?.plan ?? null,
|
|
217
|
+
createdAt: event.ts,
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
case 'run_approved':
|
|
221
|
+
case 'tool_approved':
|
|
222
|
+
upsertApproval(state, {
|
|
223
|
+
id: event.payload?.approvalId ?? event.payload?.runId ?? event.payload?.itemId ?? event.id,
|
|
224
|
+
scope: event.type === 'run_approved' ? 'run' : 'tool',
|
|
225
|
+
status: 'approved',
|
|
226
|
+
runId: event.runId ?? event.payload?.runId ?? null,
|
|
227
|
+
itemId: event.payload?.itemId ?? null,
|
|
228
|
+
approvedAt: event.ts,
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
147
231
|
case 'run_done':
|
|
148
232
|
state.status = 'done';
|
|
233
|
+
finishPendingPlanSteps(state.plan);
|
|
234
|
+
finishControlByRun(state.controlQueue, event.runId ?? event.payload?.runId ?? null, 'done', event.ts);
|
|
149
235
|
return;
|
|
150
236
|
case 'run_cancelled':
|
|
151
237
|
state.status = 'cancelled';
|
|
152
238
|
state.logs.push(String(event.payload?.message ?? 'Agent run cancelled.'));
|
|
239
|
+
finishControlByRun(state.controlQueue, event.runId ?? event.payload?.runId ?? null, 'cancelled', event.ts);
|
|
153
240
|
return;
|
|
154
241
|
case 'run_error':
|
|
155
242
|
state.status = 'error';
|
|
156
243
|
state.logs.push(String(event.payload?.message ?? 'Agent run failed.'));
|
|
244
|
+
finishControlByRun(state.controlQueue, event.runId ?? event.payload?.runId ?? null, 'failed', event.ts);
|
|
245
|
+
return;
|
|
246
|
+
case 'control_enqueued':
|
|
247
|
+
upsertControlItem(state.controlQueue, {
|
|
248
|
+
id: event.payload?.id ?? event.id,
|
|
249
|
+
workspace: event.workspace ?? event.payload?.workspace ?? null,
|
|
250
|
+
input: String(event.payload?.input ?? ''),
|
|
251
|
+
status: 'queued',
|
|
252
|
+
createdAt: event.payload?.createdAt ?? event.ts,
|
|
253
|
+
updatedAt: event.ts,
|
|
254
|
+
});
|
|
255
|
+
return;
|
|
256
|
+
case 'control_started':
|
|
257
|
+
upsertControlItem(state.controlQueue, {
|
|
258
|
+
id: event.payload?.id ?? null,
|
|
259
|
+
runId: event.runId ?? event.payload?.runId ?? null,
|
|
260
|
+
status: 'running',
|
|
261
|
+
startedAt: event.payload?.startedAt ?? event.ts,
|
|
262
|
+
updatedAt: event.ts,
|
|
263
|
+
});
|
|
157
264
|
return;
|
|
158
265
|
case 'runtime_log':
|
|
159
266
|
state.logs.push(String(event.payload?.message ?? ''));
|
|
@@ -164,6 +271,32 @@ function applyEvent(state, event) {
|
|
|
164
271
|
}
|
|
165
272
|
}
|
|
166
273
|
|
|
274
|
+
function upsertById(list, next) {
|
|
275
|
+
const index = list.findIndex((item) => item.id === next.id);
|
|
276
|
+
if (index === -1) {
|
|
277
|
+
list.push(next);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
list[index] = {
|
|
281
|
+
...list[index],
|
|
282
|
+
...next,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function upsertControlItem(queue, next) {
|
|
287
|
+
if (!next.id) return;
|
|
288
|
+
upsertById(queue, next);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function finishControlByRun(queue, runId, status, finishedAt) {
|
|
292
|
+
if (!runId) return;
|
|
293
|
+
const item = queue.find((entry) => entry.runId === runId && entry.status === 'running');
|
|
294
|
+
if (!item) return;
|
|
295
|
+
item.status = status;
|
|
296
|
+
item.finishedAt = finishedAt;
|
|
297
|
+
item.updatedAt = finishedAt;
|
|
298
|
+
}
|
|
299
|
+
|
|
167
300
|
function appendAssistantDelta(state, delta) {
|
|
168
301
|
if (!delta) return;
|
|
169
302
|
const last = state.conversation.at(-1);
|
|
@@ -198,6 +331,18 @@ function finishToolCall(state, payload = {}) {
|
|
|
198
331
|
if (!existing) state.chain.push(step);
|
|
199
332
|
}
|
|
200
333
|
|
|
334
|
+
function upsertApproval(state, next) {
|
|
335
|
+
upsertById(state.approvals, next);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function finishPendingPlanSteps(plan) {
|
|
339
|
+
for (const step of plan ?? []) {
|
|
340
|
+
if (step.status === 'running' || step.status === 'pending') {
|
|
341
|
+
step.status = 'done';
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
201
346
|
function upsertActivity(state, rawActivity) {
|
|
202
347
|
const activity = normalizeActivity(rawActivity);
|
|
203
348
|
if (!activity) return;
|
|
@@ -211,6 +356,10 @@ function upsertActivity(state, rawActivity) {
|
|
|
211
356
|
|
|
212
357
|
function ensurePlanFromActivityProjection(state, activity) {
|
|
213
358
|
const actKey = activity.key ?? null;
|
|
359
|
+
if (state.plan?.some((step) => step.owner === 'orchestrator')) {
|
|
360
|
+
attachActivityToExistingPlan(state.plan, activity);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
214
363
|
if (state.plan && actKey !== null && state.plan[0]?._activityKey === actKey) return;
|
|
215
364
|
const steps = activity.plan?.steps;
|
|
216
365
|
if (Array.isArray(steps) && steps.length > 0) {
|
|
@@ -219,6 +368,8 @@ function ensurePlanFromActivityProjection(state, activity) {
|
|
|
219
368
|
id: step.id ?? null,
|
|
220
369
|
description: step.label,
|
|
221
370
|
status: 'pending',
|
|
371
|
+
owner: 'activity',
|
|
372
|
+
ownerActivityKey: activity.key,
|
|
222
373
|
_activityKey: activity.key,
|
|
223
374
|
}));
|
|
224
375
|
return;
|
|
@@ -228,12 +379,16 @@ function ensurePlanFromActivityProjection(state, activity) {
|
|
|
228
379
|
id: null,
|
|
229
380
|
description: activity.label,
|
|
230
381
|
status: 'pending',
|
|
382
|
+
owner: 'activity',
|
|
383
|
+
ownerActivityKey: activity.key,
|
|
231
384
|
_activityKey: activity.key,
|
|
232
385
|
}];
|
|
233
386
|
}
|
|
234
387
|
|
|
235
388
|
function normalizePlan(steps, payload = {}) {
|
|
236
389
|
if (!Array.isArray(steps)) return null;
|
|
390
|
+
const owner = payload.owner ?? 'orchestrator';
|
|
391
|
+
const ownerActivityKey = payload.ownerActivityKey ?? payload.activityKey ?? null;
|
|
237
392
|
return steps.map((raw, i) => {
|
|
238
393
|
const item = typeof raw === 'string' ? { description: raw } : (raw ?? {});
|
|
239
394
|
return {
|
|
@@ -241,11 +396,21 @@ function normalizePlan(steps, payload = {}) {
|
|
|
241
396
|
id: item.id ?? null,
|
|
242
397
|
description: String(item.description ?? item.label ?? item.name ?? `Step ${i + 1}`),
|
|
243
398
|
status: item.status ?? 'pending',
|
|
399
|
+
owner: item.owner ?? owner,
|
|
400
|
+
ownerActivityKey: item.ownerActivityKey ?? ownerActivityKey,
|
|
244
401
|
_activityKey: item._activityKey ?? payload.activityKey ?? null,
|
|
245
402
|
};
|
|
246
403
|
});
|
|
247
404
|
}
|
|
248
405
|
|
|
406
|
+
function defaultRunPlan() {
|
|
407
|
+
return [
|
|
408
|
+
{ step: 1, id: 'analyze', description: 'Analyze the request', status: 'running', owner: 'orchestrator', ownerActivityKey: null, _activityKey: null },
|
|
409
|
+
{ step: 2, id: 'execute', description: 'Execute the required actions', status: 'pending', owner: 'orchestrator', ownerActivityKey: null, _activityKey: null },
|
|
410
|
+
{ step: 3, id: 'verify', description: 'Verify the result', status: 'pending', owner: 'orchestrator', ownerActivityKey: null, _activityKey: null },
|
|
411
|
+
];
|
|
412
|
+
}
|
|
413
|
+
|
|
249
414
|
function updatePlanStep(plan, payload) {
|
|
250
415
|
if (!plan) return;
|
|
251
416
|
const step = plan.find((item) => item.step === Number(payload.step));
|
|
@@ -22,7 +22,9 @@ test('reduceAgentEvents: run_started clears stale plan', () => {
|
|
|
22
22
|
}),
|
|
23
23
|
createAgentEvent('run_started', { origin: 'user' }),
|
|
24
24
|
]);
|
|
25
|
-
assert.equal(projection.plan,
|
|
25
|
+
assert.equal(projection.plan.length, 3);
|
|
26
|
+
assert.equal(projection.plan[0].description, 'Analyze the request');
|
|
27
|
+
assert.equal(projection.plan[0].owner, 'orchestrator');
|
|
26
28
|
assert.equal(projection.activities.length, 0);
|
|
27
29
|
assert.equal(projection.status, 'running');
|
|
28
30
|
});
|
|
@@ -67,7 +69,7 @@ test('reduceAgentEvents: activity with plan creates visible plan and progress',
|
|
|
67
69
|
assert.equal(projection.plan[1].status, 'pending');
|
|
68
70
|
});
|
|
69
71
|
|
|
70
|
-
test('reduceAgentEvents:
|
|
72
|
+
test('reduceAgentEvents: activity attaches to orchestrator plan without replacing it', () => {
|
|
71
73
|
const projection = reduceAgentEvents([
|
|
72
74
|
createAgentEvent('plan_set', {
|
|
73
75
|
origin: 'tool',
|
|
@@ -87,8 +89,178 @@ test('reduceAgentEvents: real activity replaces minimal MCP plan', () => {
|
|
|
87
89
|
}),
|
|
88
90
|
]);
|
|
89
91
|
assert.equal(projection.plan.length, 1);
|
|
90
|
-
assert.equal(projection.plan[0].description, '
|
|
91
|
-
assert.equal(projection.plan[0].
|
|
92
|
+
assert.equal(projection.plan[0].description, 'cme.cme_export_run');
|
|
93
|
+
assert.equal(projection.plan[0].owner, 'orchestrator');
|
|
94
|
+
assert.equal(projection.plan[0].activityKey, 'cme:export-1');
|
|
95
|
+
assert.equal(projection.plan[0].ownerActivityKey, 'cme:export-1');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('reduceAgentEvents: run activity attaches to execution step of default plan', () => {
|
|
99
|
+
const projection = reduceAgentEvents([
|
|
100
|
+
createAgentEvent('run_started', { origin: 'runtime' }),
|
|
101
|
+
createAgentEvent('activity_upserted', {
|
|
102
|
+
origin: 'tool',
|
|
103
|
+
payload: {
|
|
104
|
+
activity: {
|
|
105
|
+
key: 'production:build-1',
|
|
106
|
+
id: 'build-1',
|
|
107
|
+
source: 'production',
|
|
108
|
+
label: 'Production build',
|
|
109
|
+
status: 'running',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
assert.equal(projection.plan[0].status, 'done');
|
|
116
|
+
assert.equal(projection.plan[1].status, 'running');
|
|
117
|
+
assert.equal(projection.plan[1].activityKey, 'production:build-1');
|
|
118
|
+
assert.equal(projection.plan[2].status, 'pending');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('reduceAgentEvents: run_done finalizes running and pending plan steps', () => {
|
|
122
|
+
const projection = reduceAgentEvents([
|
|
123
|
+
createAgentEvent('run_started', { origin: 'runtime' }),
|
|
124
|
+
createAgentEvent('run_done', { origin: 'runtime' }),
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
assert.deepEqual(projection.plan.map((step) => step.status), ['done', 'done', 'done']);
|
|
128
|
+
assert.equal(projection.status, 'done');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('dispatchAgentEvent: run_done finalizes session plan', () => {
|
|
132
|
+
const session = {};
|
|
133
|
+
dispatchAgentEvent(session, createAgentEvent('run_started', { origin: 'runtime' }));
|
|
134
|
+
dispatchAgentEvent(session, createAgentEvent('run_done', { origin: 'runtime' }));
|
|
135
|
+
|
|
136
|
+
assert.deepEqual(session.headlessPlan.map((step) => step.status), ['done', 'done', 'done']);
|
|
137
|
+
assert.equal(session.agentProjection.status, 'done');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('reduceAgentEvents: run_evaluated exposes evaluator verdict', () => {
|
|
141
|
+
const projection = reduceAgentEvents([
|
|
142
|
+
createAgentEvent('run_started', { origin: 'runtime', runId: 'run-1' }),
|
|
143
|
+
createAgentEvent('run_evaluated', {
|
|
144
|
+
origin: 'runtime',
|
|
145
|
+
runId: 'run-1',
|
|
146
|
+
payload: {
|
|
147
|
+
ok: false,
|
|
148
|
+
reason: 'Missing export.',
|
|
149
|
+
suggestedAction: 'Run export step.',
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
assert.deepEqual(projection.evaluation, {
|
|
155
|
+
ok: false,
|
|
156
|
+
reason: 'Missing export.',
|
|
157
|
+
suggestedAction: 'Run export step.',
|
|
158
|
+
runId: 'run-1',
|
|
159
|
+
});
|
|
160
|
+
assert.equal(projection.status, 'running');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('reduceAgentEvents: run_replanned records replan trace', () => {
|
|
164
|
+
const projection = reduceAgentEvents([
|
|
165
|
+
createAgentEvent('run_started', { origin: 'runtime', runId: 'run-1' }),
|
|
166
|
+
createAgentEvent('run_replanned', {
|
|
167
|
+
origin: 'runtime',
|
|
168
|
+
runId: 'run-1',
|
|
169
|
+
payload: {
|
|
170
|
+
reason: 'Export file missing.',
|
|
171
|
+
plan: ['Run export again'],
|
|
172
|
+
replansLeft: 1,
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
assert.deepEqual(projection.replans, [{
|
|
178
|
+
reason: 'Export file missing.',
|
|
179
|
+
plan: ['Run export again'],
|
|
180
|
+
replansLeft: 1,
|
|
181
|
+
runId: 'run-1',
|
|
182
|
+
}]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('reduceAgentEvents: approvals move from pending to approved', () => {
|
|
186
|
+
const projection = reduceAgentEvents([
|
|
187
|
+
createAgentEvent('run_pending_approval', {
|
|
188
|
+
origin: 'runtime',
|
|
189
|
+
runId: 'run-1',
|
|
190
|
+
payload: {
|
|
191
|
+
approvalId: 'approval-1',
|
|
192
|
+
runId: 'run-1',
|
|
193
|
+
reason: 'Approve plan.',
|
|
194
|
+
plan: ['Build'],
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
197
|
+
createAgentEvent('run_approved', {
|
|
198
|
+
origin: 'runtime',
|
|
199
|
+
runId: 'run-1',
|
|
200
|
+
payload: {
|
|
201
|
+
approvalId: 'approval-1',
|
|
202
|
+
runId: 'run-1',
|
|
203
|
+
},
|
|
204
|
+
}),
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
assert.equal(projection.approvals.length, 1);
|
|
208
|
+
assert.equal(projection.approvals[0].status, 'approved');
|
|
209
|
+
assert.equal(projection.approvals[0].scope, 'run');
|
|
210
|
+
assert.deepEqual(projection.approvals[0].plan, ['Build']);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('reduceAgentEvents: control queue is event sourced and follows run status', () => {
|
|
214
|
+
const projection = reduceAgentEvents([
|
|
215
|
+
createAgentEvent('control_enqueued', {
|
|
216
|
+
origin: 'runtime',
|
|
217
|
+
workspace: 'docs',
|
|
218
|
+
payload: {
|
|
219
|
+
id: 'control-1',
|
|
220
|
+
workspace: 'docs',
|
|
221
|
+
input: 'Run after current task',
|
|
222
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
createAgentEvent('control_started', {
|
|
226
|
+
origin: 'runtime',
|
|
227
|
+
runId: 'run-control-1',
|
|
228
|
+
workspace: 'docs',
|
|
229
|
+
payload: { id: 'control-1', runId: 'run-control-1' },
|
|
230
|
+
}),
|
|
231
|
+
createAgentEvent('run_done', {
|
|
232
|
+
origin: 'runtime',
|
|
233
|
+
runId: 'run-control-1',
|
|
234
|
+
workspace: 'docs',
|
|
235
|
+
}),
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
assert.equal(projection.controlQueue.length, 1);
|
|
239
|
+
assert.equal(projection.controlQueue[0].id, 'control-1');
|
|
240
|
+
assert.equal(projection.controlQueue[0].status, 'done');
|
|
241
|
+
assert.equal(projection.controlQueue[0].runId, 'run-control-1');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('reduceAgentEvents: activity-owned plan is used when no orchestrator plan exists', () => {
|
|
245
|
+
const projection = reduceAgentEvents([
|
|
246
|
+
createAgentEvent('activity_upserted', {
|
|
247
|
+
origin: 'tool',
|
|
248
|
+
payload: {
|
|
249
|
+
activity: {
|
|
250
|
+
key: 'production:job-1',
|
|
251
|
+
id: 'job-1',
|
|
252
|
+
source: 'production',
|
|
253
|
+
label: 'Production build',
|
|
254
|
+
status: 'running',
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
}),
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
assert.equal(projection.plan.length, 1);
|
|
261
|
+
assert.equal(projection.plan[0].description, 'Production build');
|
|
262
|
+
assert.equal(projection.plan[0].owner, 'activity');
|
|
263
|
+
assert.equal(projection.plan[0].ownerActivityKey, 'production:job-1');
|
|
92
264
|
});
|
|
93
265
|
|
|
94
266
|
test('dispatchAgentEvent: writes compatibility projections to session', () => {
|
package/src/core/agentLoop.js
CHANGED
|
@@ -72,6 +72,9 @@ export async function runAgenticLoop(agent, session, initialInput, {
|
|
|
72
72
|
|
|
73
73
|
for (let turn = 1; turn <= maxTurns; turn += 1) {
|
|
74
74
|
throwIfAborted(signal, abortMessage);
|
|
75
|
+
if (session._currentRunIdentity && runId) {
|
|
76
|
+
session._currentRunIdentity.turnId = `${runId}:turn-${turn}`;
|
|
77
|
+
}
|
|
75
78
|
onTurnStart?.({ turn, maxTurns });
|
|
76
79
|
|
|
77
80
|
const snapshot = activitySnapshot(session);
|
package/src/core/compose.js
CHANGED
|
@@ -9,7 +9,7 @@ import { managerRoot } from './workspaces.js';
|
|
|
9
9
|
|
|
10
10
|
const execFileAsync = promisify(execFile);
|
|
11
11
|
|
|
12
|
-
export const COMPOSE_SERVICES = ['serve', 'mcp-http', '
|
|
12
|
+
export const COMPOSE_SERVICES = ['serve', 'mcp-http', 'production-mcp'];
|
|
13
13
|
const SERVICE_DESCRIPTION_LABEL = 'wiki-manager.description';
|
|
14
14
|
|
|
15
15
|
const DEFAULT_SERVICE_ALIASES = {
|
|
@@ -17,7 +17,6 @@ const DEFAULT_SERVICE_ALIASES = {
|
|
|
17
17
|
ui: ['serve'],
|
|
18
18
|
wiki: ['mcp-http'],
|
|
19
19
|
mcp: ['mcp-http'],
|
|
20
|
-
runtime: ['agent-runtime'],
|
|
21
20
|
production: ['production-mcp'],
|
|
22
21
|
};
|
|
23
22
|
|
|
@@ -3,12 +3,12 @@ import { test } from 'node:test';
|
|
|
3
3
|
import assert from 'node:assert/strict';
|
|
4
4
|
import YAML from 'yaml';
|
|
5
5
|
|
|
6
|
-
test('
|
|
6
|
+
test('workspace compose does not start a per-workspace agent runtime', async () => {
|
|
7
7
|
const raw = await readFile(new URL('../../docker-compose.yml', import.meta.url), 'utf8');
|
|
8
8
|
const compose = YAML.parse(raw);
|
|
9
|
-
const
|
|
9
|
+
const aliases = compose['x-wiki-manager']['service-aliases'];
|
|
10
10
|
|
|
11
|
-
assert.equal(
|
|
12
|
-
assert.
|
|
13
|
-
assert.
|
|
11
|
+
assert.equal(compose.services['agent-runtime'], undefined);
|
|
12
|
+
assert.deepEqual(aliases.all.targets, ['serve', 'mcp-http', 'production-mcp']);
|
|
13
|
+
assert.equal(aliases.runtime, undefined);
|
|
14
14
|
});
|
package/src/core/jobQueue.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { extractActivity, parseJsonText, sessionActivities } from './activity.js';
|
|
2
3
|
import { createAgentEvent, dispatchAgentEvent } from './agentEvents.js';
|
|
3
4
|
import { callMcpTool, formatMcpToolResult } from './mcp.js';
|
|
@@ -14,7 +15,7 @@ function notifyQueueUpdate(session) {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
function shortId() {
|
|
17
|
-
return `q-${
|
|
18
|
+
return `q-${randomUUID()}`;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
function terminalStatus(status) {
|
|
@@ -61,17 +62,33 @@ export function queueSummary(args = {}) {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
export function queueCounts(session) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
65
|
+
const queue = projectQueue(session.headlessPlan, ensureJobQueue(session), { workspace: session.workspace ?? null });
|
|
66
|
+
return {
|
|
67
|
+
active: queue.length,
|
|
68
|
+
current: queue.filter((item) => item.workspace === session.workspace || !item.workspace).length,
|
|
69
|
+
frozen: queue.filter((item) => item.workspace && item.workspace !== session.workspace).length,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function projectQueue(plan, queue, { workspace = null } = {}) {
|
|
74
|
+
const blockedJobs = (queue ?? [])
|
|
75
|
+
.filter((item) => ['waiting', 'blocked', 'pending_approval'].includes(String(item.status ?? '').toLowerCase()))
|
|
76
|
+
.map((item) => ({
|
|
77
|
+
...item,
|
|
78
|
+
queueType: 'blocked_job',
|
|
79
|
+
}));
|
|
80
|
+
const pendingSteps = (plan ?? [])
|
|
81
|
+
.filter((step) => step.status === 'pending')
|
|
82
|
+
.map((step) => ({
|
|
83
|
+
id: `plan-${step.step}`,
|
|
84
|
+
queueType: 'pending_step',
|
|
85
|
+
status: 'pending',
|
|
86
|
+
workspace,
|
|
87
|
+
step: step.step,
|
|
88
|
+
activityKey: step.activityKey ?? step.ownerActivityKey ?? undefined,
|
|
89
|
+
args: { type: step.description },
|
|
90
|
+
}));
|
|
91
|
+
return [...blockedJobs, ...pendingSteps];
|
|
75
92
|
}
|
|
76
93
|
|
|
77
94
|
export function formatQueue(session) {
|