@dotdrelle/wiki-manager 0.8.2 → 0.10.4
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 +36 -6
- package/agents.docker-compose.yml +5 -0
- package/package.json +5 -2
- package/src/agent/graph.js +68 -9
- package/src/agent/graph.test.js +64 -0
- package/src/cli/wiki-manager.js +34 -2
- package/src/commands/slash.js +9 -25
- package/src/contracts/README.md +16 -0
- package/src/contracts/schemas.js +302 -0
- package/src/contracts/schemas.test.js +93 -0
- package/src/core/activity.js +14 -2
- package/src/core/activity.test.js +4 -2
- package/src/core/agentEvents.js +212 -24
- package/src/core/agentEvents.test.js +85 -12
- package/src/core/agentLoop.js +32 -7
- package/src/core/mcp.js +3 -1
- package/src/core/plan.js +4 -0
- package/src/core/planPatch.js +224 -0
- package/src/core/planPatch.test.js +63 -0
- package/src/core/sessionConfig.js +24 -0
- package/src/core/workflow.js +264 -0
- package/src/core/workflow.test.js +66 -0
- package/src/runtime/client.js +28 -1
- package/src/runtime/runner.js +432 -20
- package/src/runtime/runner.test.js +273 -1
- package/src/runtime/server.js +494 -24
- package/src/runtime/server.test.js +661 -0
- package/src/runtime/store.js +59 -7
- package/src/runtime/store.test.js +72 -1
- package/src/shell/RightPane.tsx +1 -7
- package/src/shell/StartupScreen.tsx +212 -0
- package/src/shell/repl.js +51 -7
- package/src/shell/repl.test.js +77 -1
- package/src/shell/textFit.ts +6 -0
- package/src/shell/tui.tsx +163 -0
- package/src/shell/useAgent.ts +17 -9
- package/src/shell/useSession.ts +34 -0
package/src/core/agentEvents.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { normalizeActivity } from './activity.js';
|
|
2
2
|
import { attachActivityToExistingPlan, syncActivitiesToPlan } from './plan.js';
|
|
3
|
+
import { applyPlanPatch, normalizePlanPatch, normalizePlanRevision, rebasePlanPatch } from './planPatch.js';
|
|
4
|
+
import { projectWorkflow } from './workflow.js';
|
|
5
|
+
import { validateContractInDev } from '../contracts/schemas.js';
|
|
3
6
|
|
|
4
7
|
const SESSION_PROJECTION_EVENTS = new Set([
|
|
5
8
|
'run_started',
|
|
6
9
|
'plan_set',
|
|
7
10
|
'plan_step_updated',
|
|
11
|
+
'control_message_received',
|
|
12
|
+
'plan_patch_proposed',
|
|
13
|
+
'plan_patch_approved',
|
|
14
|
+
'plan_patch_applied',
|
|
15
|
+
'plan_patch_rebased',
|
|
16
|
+
'plan_patch_rejected',
|
|
8
17
|
'activity_upserted',
|
|
9
18
|
'run_evaluated',
|
|
10
19
|
'run_replanned',
|
|
@@ -12,6 +21,8 @@ const SESSION_PROJECTION_EVENTS = new Set([
|
|
|
12
21
|
'run_approved',
|
|
13
22
|
'tool_pending_approval',
|
|
14
23
|
'tool_approved',
|
|
24
|
+
'control_enqueued',
|
|
25
|
+
'control_started',
|
|
15
26
|
'run_done',
|
|
16
27
|
'run_error',
|
|
17
28
|
'run_cancelled',
|
|
@@ -24,6 +35,7 @@ const SESSION_PROJECTION_EVENTS = new Set([
|
|
|
24
35
|
const PLAN_MUTATING_EVENTS = new Set([
|
|
25
36
|
'run_started',
|
|
26
37
|
'plan_set',
|
|
38
|
+
'plan_patch_applied',
|
|
27
39
|
'plan_step_updated',
|
|
28
40
|
'activity_upserted',
|
|
29
41
|
'run_done',
|
|
@@ -34,18 +46,20 @@ export function createAgentEvent(type, {
|
|
|
34
46
|
payload = {},
|
|
35
47
|
runId = null,
|
|
36
48
|
turnId = null,
|
|
49
|
+
taskId = null,
|
|
37
50
|
workspace = null,
|
|
38
51
|
} = {}) {
|
|
39
|
-
return {
|
|
52
|
+
return validateContractInDev('agentRunEvent', {
|
|
40
53
|
id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
|
|
41
54
|
ts: new Date().toISOString(),
|
|
42
55
|
type,
|
|
43
56
|
origin,
|
|
44
57
|
runId,
|
|
45
58
|
turnId,
|
|
59
|
+
taskId,
|
|
46
60
|
workspace,
|
|
47
61
|
payload,
|
|
48
|
-
};
|
|
62
|
+
});
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
export function dispatchAgentEvent(session, event) {
|
|
@@ -53,6 +67,7 @@ export function dispatchAgentEvent(session, event) {
|
|
|
53
67
|
event.id && event.ts ? event : createAgentEvent(event.type, event),
|
|
54
68
|
session,
|
|
55
69
|
);
|
|
70
|
+
validateContractInDev('agentRunEvent', normalized);
|
|
56
71
|
const tracksPlan = PLAN_MUTATING_EVENTS.has(normalized.type);
|
|
57
72
|
const previousPlan = tracksPlan ? JSON.stringify(session.headlessPlan ?? null) : null;
|
|
58
73
|
session.agentEvents ??= [];
|
|
@@ -77,6 +92,7 @@ function withSessionRunIdentity(event, session) {
|
|
|
77
92
|
...event,
|
|
78
93
|
runId: event.runId ?? identity.runId ?? null,
|
|
79
94
|
turnId: event.turnId ?? identity.turnId ?? null,
|
|
95
|
+
taskId: event.taskId ?? identity.taskId ?? null,
|
|
80
96
|
workspace: event.workspace ?? identity.workspace ?? null,
|
|
81
97
|
};
|
|
82
98
|
}
|
|
@@ -101,13 +117,16 @@ function createProjectionState() {
|
|
|
101
117
|
evaluation: null,
|
|
102
118
|
replans: [],
|
|
103
119
|
approvals: [],
|
|
120
|
+
planRevision: 0,
|
|
121
|
+
planPatches: [],
|
|
122
|
+
controlQueue: [],
|
|
104
123
|
summary: null,
|
|
105
124
|
status: 'idle',
|
|
106
125
|
};
|
|
107
126
|
}
|
|
108
127
|
|
|
109
128
|
function publicProjection(state) {
|
|
110
|
-
|
|
129
|
+
const projection = {
|
|
111
130
|
conversation: state.conversation.map((message) => ({ ...message })),
|
|
112
131
|
chain: state.chain.map((step) => ({ ...step })),
|
|
113
132
|
plan: state.plan ? state.plan.map((step) => ({ ...step })) : null,
|
|
@@ -116,14 +135,29 @@ function publicProjection(state) {
|
|
|
116
135
|
evaluation: state.evaluation ? { ...state.evaluation } : null,
|
|
117
136
|
replans: state.replans.map((replan) => ({ ...replan, plan: [...(replan.plan ?? [])] })),
|
|
118
137
|
approvals: state.approvals.map((approval) => ({ ...approval })),
|
|
138
|
+
planRevision: state.planRevision,
|
|
139
|
+
planPatches: state.planPatches.map((patch) => ({
|
|
140
|
+
...patch,
|
|
141
|
+
operations: (patch.operations ?? []).map((operation) => ({ ...operation })),
|
|
142
|
+
patch: patch.patch ? { ...patch.patch, operations: (patch.patch.operations ?? []).map((operation) => ({ ...operation })) } : null,
|
|
143
|
+
})),
|
|
144
|
+
controlQueue: state.controlQueue.map((item) => ({ ...item })),
|
|
119
145
|
summary: state.summary,
|
|
120
146
|
status: state.status,
|
|
121
147
|
};
|
|
148
|
+
return {
|
|
149
|
+
...projection,
|
|
150
|
+
workflow: projectWorkflow(projection),
|
|
151
|
+
};
|
|
122
152
|
}
|
|
123
153
|
|
|
124
154
|
export function applyAgentProjectionToSession(session, projection) {
|
|
125
155
|
session.headlessPlan = projection.plan ? projection.plan.map((step) => ({ ...step })) : null;
|
|
126
156
|
session.activities = Object.fromEntries((projection.activities ?? []).map((activity) => [activity.key, { ...activity }]));
|
|
157
|
+
session.controlQueue = (projection.controlQueue ?? []).map((item) => ({ ...item }));
|
|
158
|
+
session.planRevision = projection.planRevision ?? 0;
|
|
159
|
+
session.planPatches = (projection.planPatches ?? []).map((patch) => ({ ...patch }));
|
|
160
|
+
session.workflow = projection.workflow ? { ...projection.workflow } : null;
|
|
127
161
|
const production = (projection.activities ?? []).filter((activity) => activity.source === 'production').at(-1);
|
|
128
162
|
session.productionActivity = production ? {
|
|
129
163
|
jobId: production.id,
|
|
@@ -138,13 +172,15 @@ function applyEvent(state, event) {
|
|
|
138
172
|
switch (event.type) {
|
|
139
173
|
case 'run_started':
|
|
140
174
|
state.status = 'running';
|
|
141
|
-
state.plan =
|
|
175
|
+
state.plan = null;
|
|
142
176
|
state.chain = [];
|
|
143
177
|
state.activities = {};
|
|
144
178
|
state.logs = [];
|
|
145
179
|
state.evaluation = null;
|
|
146
180
|
state.replans = [];
|
|
147
181
|
state.approvals = [];
|
|
182
|
+
state.planRevision = 0;
|
|
183
|
+
state.planPatches = [];
|
|
148
184
|
state.summary = null;
|
|
149
185
|
return;
|
|
150
186
|
case 'user_message':
|
|
@@ -174,10 +210,97 @@ function applyEvent(state, event) {
|
|
|
174
210
|
return;
|
|
175
211
|
case 'plan_set':
|
|
176
212
|
state.plan = normalizePlan(event.payload?.steps, event.payload ?? {});
|
|
213
|
+
// A plan_set always replaces the plan wholesale (initial declaration,
|
|
214
|
+
// fallback extraction, or a full replan) — bump the revision so any
|
|
215
|
+
// patch proposed against the prior plan is detected as stale and
|
|
216
|
+
// rebased instead of silently applying against a structure that no
|
|
217
|
+
// longer matches what it was built for. An explicit payload.planRevision
|
|
218
|
+
// still wins, for callers that manage revisions themselves.
|
|
219
|
+
state.planRevision = event.payload?.planRevision != null
|
|
220
|
+
? normalizePlanRevision(event.payload.planRevision)
|
|
221
|
+
: state.planRevision + 1;
|
|
177
222
|
return;
|
|
178
223
|
case 'plan_step_updated':
|
|
179
224
|
updatePlanStep(state.plan, event.payload ?? {});
|
|
180
225
|
return;
|
|
226
|
+
case 'control_message_received':
|
|
227
|
+
state.logs.push(`Control message: ${String(event.payload?.input ?? '')}`);
|
|
228
|
+
state.logs = state.logs.slice(-200);
|
|
229
|
+
return;
|
|
230
|
+
case 'plan_patch_proposed':
|
|
231
|
+
upsertPlanPatch(state, {
|
|
232
|
+
id: patchIdFromEvent(event),
|
|
233
|
+
status: 'proposed',
|
|
234
|
+
runId: event.runId ?? event.payload?.targetRunId ?? null,
|
|
235
|
+
workspace: event.workspace ?? null,
|
|
236
|
+
input: event.payload?.input ?? null,
|
|
237
|
+
patch: normalizePlanPatch(event.payload?.patch ?? {}, {
|
|
238
|
+
targetRunId: event.runId ?? event.payload?.targetRunId ?? null,
|
|
239
|
+
basePlanRevision: state.planRevision,
|
|
240
|
+
}),
|
|
241
|
+
createdAt: event.ts,
|
|
242
|
+
updatedAt: event.ts,
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
case 'plan_patch_approved':
|
|
246
|
+
upsertPlanPatch(state, {
|
|
247
|
+
id: patchIdFromEvent(event),
|
|
248
|
+
status: 'approved',
|
|
249
|
+
approvedAt: event.ts,
|
|
250
|
+
updatedAt: event.ts,
|
|
251
|
+
});
|
|
252
|
+
return;
|
|
253
|
+
case 'plan_patch_rebased': {
|
|
254
|
+
const existing = state.planPatches.find((patch) => patch.id === patchIdFromEvent(event));
|
|
255
|
+
const rebased = normalizePlanPatch(event.payload?.patch ?? rebasePlanPatch(existing?.patch ?? {}, { currentRevision: state.planRevision }), {
|
|
256
|
+
targetRunId: event.runId ?? null,
|
|
257
|
+
basePlanRevision: state.planRevision,
|
|
258
|
+
});
|
|
259
|
+
upsertPlanPatch(state, {
|
|
260
|
+
id: patchIdFromEvent(event),
|
|
261
|
+
status: 'rebased',
|
|
262
|
+
patch: rebased,
|
|
263
|
+
updatedAt: event.ts,
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
case 'plan_patch_applied': {
|
|
268
|
+
const patchId = patchIdFromEvent(event);
|
|
269
|
+
const patch = normalizePlanPatch(event.payload?.patch ?? {}, {
|
|
270
|
+
targetRunId: event.runId ?? event.payload?.targetRunId ?? null,
|
|
271
|
+
basePlanRevision: state.planRevision,
|
|
272
|
+
});
|
|
273
|
+
const applied = applyPlanPatch(state.plan, patch, { currentRevision: state.planRevision });
|
|
274
|
+
if (!applied.ok) {
|
|
275
|
+
upsertPlanPatch(state, {
|
|
276
|
+
id: patchId,
|
|
277
|
+
status: 'rejected',
|
|
278
|
+
rejectionReason: applied.reason,
|
|
279
|
+
currentRevision: applied.currentRevision,
|
|
280
|
+
updatedAt: event.ts,
|
|
281
|
+
});
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
state.plan = applied.plan;
|
|
285
|
+
state.planRevision = applied.planRevision;
|
|
286
|
+
upsertPlanPatch(state, {
|
|
287
|
+
id: patchId,
|
|
288
|
+
status: 'applied',
|
|
289
|
+
patch,
|
|
290
|
+
appliedAt: event.ts,
|
|
291
|
+
planRevision: state.planRevision,
|
|
292
|
+
updatedAt: event.ts,
|
|
293
|
+
});
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
case 'plan_patch_rejected':
|
|
297
|
+
upsertPlanPatch(state, {
|
|
298
|
+
id: patchIdFromEvent(event),
|
|
299
|
+
status: 'rejected',
|
|
300
|
+
rejectionReason: event.payload?.reason ?? null,
|
|
301
|
+
updatedAt: event.ts,
|
|
302
|
+
});
|
|
303
|
+
return;
|
|
181
304
|
case 'run_summary':
|
|
182
305
|
state.summary = String(event.payload?.content ?? '');
|
|
183
306
|
if (state.summary) state.conversation.push({ role: 'assistant', content: state.summary });
|
|
@@ -226,14 +349,36 @@ function applyEvent(state, event) {
|
|
|
226
349
|
case 'run_done':
|
|
227
350
|
state.status = 'done';
|
|
228
351
|
finishPendingPlanSteps(state.plan);
|
|
352
|
+
finishControlByRun(state.controlQueue, event.runId ?? event.payload?.runId ?? null, 'done', event.ts);
|
|
229
353
|
return;
|
|
230
354
|
case 'run_cancelled':
|
|
231
355
|
state.status = 'cancelled';
|
|
232
356
|
state.logs.push(String(event.payload?.message ?? 'Agent run cancelled.'));
|
|
357
|
+
finishControlByRun(state.controlQueue, event.runId ?? event.payload?.runId ?? null, 'cancelled', event.ts);
|
|
233
358
|
return;
|
|
234
359
|
case 'run_error':
|
|
235
360
|
state.status = 'error';
|
|
236
361
|
state.logs.push(String(event.payload?.message ?? 'Agent run failed.'));
|
|
362
|
+
finishControlByRun(state.controlQueue, event.runId ?? event.payload?.runId ?? null, 'failed', event.ts);
|
|
363
|
+
return;
|
|
364
|
+
case 'control_enqueued':
|
|
365
|
+
upsertControlItem(state.controlQueue, {
|
|
366
|
+
id: event.payload?.id ?? event.id,
|
|
367
|
+
workspace: event.workspace ?? event.payload?.workspace ?? null,
|
|
368
|
+
input: String(event.payload?.input ?? ''),
|
|
369
|
+
status: 'queued',
|
|
370
|
+
createdAt: event.payload?.createdAt ?? event.ts,
|
|
371
|
+
updatedAt: event.ts,
|
|
372
|
+
});
|
|
373
|
+
return;
|
|
374
|
+
case 'control_started':
|
|
375
|
+
upsertControlItem(state.controlQueue, {
|
|
376
|
+
id: event.payload?.id ?? null,
|
|
377
|
+
runId: event.runId ?? event.payload?.runId ?? null,
|
|
378
|
+
status: 'running',
|
|
379
|
+
startedAt: event.payload?.startedAt ?? event.ts,
|
|
380
|
+
updatedAt: event.ts,
|
|
381
|
+
});
|
|
237
382
|
return;
|
|
238
383
|
case 'runtime_log':
|
|
239
384
|
state.logs.push(String(event.payload?.message ?? ''));
|
|
@@ -244,6 +389,41 @@ function applyEvent(state, event) {
|
|
|
244
389
|
}
|
|
245
390
|
}
|
|
246
391
|
|
|
392
|
+
function upsertById(list, next) {
|
|
393
|
+
const index = list.findIndex((item) => item.id === next.id);
|
|
394
|
+
if (index === -1) {
|
|
395
|
+
list.push(next);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
list[index] = {
|
|
399
|
+
...list[index],
|
|
400
|
+
...next,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function upsertControlItem(queue, next) {
|
|
405
|
+
if (!next.id) return;
|
|
406
|
+
upsertById(queue, next);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function upsertPlanPatch(state, next) {
|
|
410
|
+
if (!next.id) return;
|
|
411
|
+
upsertById(state.planPatches, next);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function patchIdFromEvent(event) {
|
|
415
|
+
return event.payload?.id ?? event.payload?.patchId ?? event.id;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function finishControlByRun(queue, runId, status, finishedAt) {
|
|
419
|
+
if (!runId) return;
|
|
420
|
+
const item = queue.find((entry) => entry.runId === runId && entry.status === 'running');
|
|
421
|
+
if (!item) return;
|
|
422
|
+
item.status = status;
|
|
423
|
+
item.finishedAt = finishedAt;
|
|
424
|
+
item.updatedAt = finishedAt;
|
|
425
|
+
}
|
|
426
|
+
|
|
247
427
|
function appendAssistantDelta(state, delta) {
|
|
248
428
|
if (!delta) return;
|
|
249
429
|
const last = state.conversation.at(-1);
|
|
@@ -279,15 +459,7 @@ function finishToolCall(state, payload = {}) {
|
|
|
279
459
|
}
|
|
280
460
|
|
|
281
461
|
function upsertApproval(state, next) {
|
|
282
|
-
|
|
283
|
-
if (index === -1) {
|
|
284
|
-
state.approvals.push(next);
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
state.approvals[index] = {
|
|
288
|
-
...state.approvals[index],
|
|
289
|
-
...next,
|
|
290
|
-
};
|
|
462
|
+
upsertById(state.approvals, next);
|
|
291
463
|
}
|
|
292
464
|
|
|
293
465
|
function finishPendingPlanSteps(plan) {
|
|
@@ -323,6 +495,10 @@ function ensurePlanFromActivityProjection(state, activity) {
|
|
|
323
495
|
id: step.id ?? null,
|
|
324
496
|
description: step.label,
|
|
325
497
|
status: 'pending',
|
|
498
|
+
dependsOn: Array.isArray(step.dependsOn) ? step.dependsOn.map(String) : [],
|
|
499
|
+
executor: step.executor ?? null,
|
|
500
|
+
executorQuery: step.executorQuery ?? null,
|
|
501
|
+
outputRefs: Array.isArray(step.outputRefs) ? step.outputRefs.map(String) : [],
|
|
326
502
|
owner: 'activity',
|
|
327
503
|
ownerActivityKey: activity.key,
|
|
328
504
|
_activityKey: activity.key,
|
|
@@ -334,6 +510,10 @@ function ensurePlanFromActivityProjection(state, activity) {
|
|
|
334
510
|
id: null,
|
|
335
511
|
description: activity.label,
|
|
336
512
|
status: 'pending',
|
|
513
|
+
dependsOn: [],
|
|
514
|
+
executor: null,
|
|
515
|
+
executorQuery: null,
|
|
516
|
+
outputRefs: [],
|
|
337
517
|
owner: 'activity',
|
|
338
518
|
ownerActivityKey: activity.key,
|
|
339
519
|
_activityKey: activity.key,
|
|
@@ -344,33 +524,41 @@ function normalizePlan(steps, payload = {}) {
|
|
|
344
524
|
if (!Array.isArray(steps)) return null;
|
|
345
525
|
const owner = payload.owner ?? 'orchestrator';
|
|
346
526
|
const ownerActivityKey = payload.ownerActivityKey ?? payload.activityKey ?? null;
|
|
347
|
-
|
|
527
|
+
const plan = steps.map((raw, i) => {
|
|
348
528
|
const item = typeof raw === 'string' ? { description: raw } : (raw ?? {});
|
|
349
529
|
return {
|
|
350
530
|
step: Number(item.step ?? i + 1),
|
|
351
531
|
id: item.id ?? null,
|
|
352
532
|
description: String(item.description ?? item.label ?? item.name ?? `Step ${i + 1}`),
|
|
353
533
|
status: item.status ?? 'pending',
|
|
534
|
+
dependsOn: Array.isArray(item.dependsOn) ? item.dependsOn.map(String) : [],
|
|
535
|
+
executor: item.executor ?? null,
|
|
536
|
+
executorQuery: item.executorQuery ?? null,
|
|
537
|
+
outputRefs: Array.isArray(item.outputRefs) ? item.outputRefs.map(String) : [],
|
|
538
|
+
locks: Array.isArray(item.locks) ? item.locks.map(String) : item.locks ?? null,
|
|
539
|
+
writeLocks: Array.isArray(item.writeLocks) ? item.writeLocks.map(String) : item.writeLocks ?? null,
|
|
540
|
+
deliverableWrites: Array.isArray(item.deliverableWrites) ? item.deliverableWrites.map(String) : [],
|
|
541
|
+
wikiPageWrites: Array.isArray(item.wikiPageWrites) ? item.wikiPageWrites.map(String) : [],
|
|
542
|
+
workspaceWrite: item.workspaceWrite === true,
|
|
354
543
|
owner: item.owner ?? owner,
|
|
355
544
|
ownerActivityKey: item.ownerActivityKey ?? ownerActivityKey,
|
|
356
545
|
_activityKey: item._activityKey ?? payload.activityKey ?? null,
|
|
357
546
|
};
|
|
358
547
|
});
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
function defaultRunPlan() {
|
|
362
|
-
return [
|
|
363
|
-
{ step: 1, id: 'analyze', description: 'Analyze the request', status: 'running', owner: 'orchestrator', ownerActivityKey: null, _activityKey: null },
|
|
364
|
-
{ step: 2, id: 'execute', description: 'Execute the required actions', status: 'pending', owner: 'orchestrator', ownerActivityKey: null, _activityKey: null },
|
|
365
|
-
{ step: 3, id: 'verify', description: 'Verify the result', status: 'pending', owner: 'orchestrator', ownerActivityKey: null, _activityKey: null },
|
|
366
|
-
];
|
|
548
|
+
return validateContractInDev('plan', plan);
|
|
367
549
|
}
|
|
368
550
|
|
|
369
551
|
function updatePlanStep(plan, payload) {
|
|
370
552
|
if (!plan) return;
|
|
371
|
-
const
|
|
553
|
+
const requestedTaskId = payload.taskId ?? payload.id ?? payload.targetTaskId;
|
|
554
|
+
const step = requestedTaskId != null
|
|
555
|
+
? plan.find((item) => String(item.id ?? item.step) === String(requestedTaskId))
|
|
556
|
+
: plan.find((item) => item.step === Number(payload.step));
|
|
372
557
|
if (!step) return;
|
|
373
|
-
|
|
558
|
+
if (payload.status === 'failed') step.status = 'failed';
|
|
559
|
+
else if (payload.status === 'running') step.status = 'running';
|
|
560
|
+
else if (payload.status === 'cancelled') step.status = 'cancelled';
|
|
561
|
+
else step.status = 'done';
|
|
374
562
|
if (payload.activityKey) step.activityKey = payload.activityKey;
|
|
375
563
|
}
|
|
376
564
|
|
|
@@ -22,9 +22,7 @@ test('reduceAgentEvents: run_started clears stale plan', () => {
|
|
|
22
22
|
}),
|
|
23
23
|
createAgentEvent('run_started', { origin: 'user' }),
|
|
24
24
|
]);
|
|
25
|
-
assert.equal(projection.plan
|
|
26
|
-
assert.equal(projection.plan[0].description, 'Analyze the request');
|
|
27
|
-
assert.equal(projection.plan[0].owner, 'orchestrator');
|
|
25
|
+
assert.equal(projection.plan, null);
|
|
28
26
|
assert.equal(projection.activities.length, 0);
|
|
29
27
|
assert.equal(projection.status, 'running');
|
|
30
28
|
});
|
|
@@ -95,7 +93,7 @@ test('reduceAgentEvents: activity attaches to orchestrator plan without replacin
|
|
|
95
93
|
assert.equal(projection.plan[0].ownerActivityKey, 'cme:export-1');
|
|
96
94
|
});
|
|
97
95
|
|
|
98
|
-
test('reduceAgentEvents: run activity
|
|
96
|
+
test('reduceAgentEvents: run activity creates activity-owned plan when no explicit plan exists', () => {
|
|
99
97
|
const projection = reduceAgentEvents([
|
|
100
98
|
createAgentEvent('run_started', { origin: 'runtime' }),
|
|
101
99
|
createAgentEvent('activity_upserted', {
|
|
@@ -112,28 +110,34 @@ test('reduceAgentEvents: run activity attaches to execution step of default plan
|
|
|
112
110
|
}),
|
|
113
111
|
]);
|
|
114
112
|
|
|
115
|
-
assert.equal(projection.plan
|
|
116
|
-
assert.equal(projection.plan[
|
|
117
|
-
assert.equal(projection.plan[
|
|
118
|
-
assert.equal(projection.plan[
|
|
113
|
+
assert.equal(projection.plan.length, 1);
|
|
114
|
+
assert.equal(projection.plan[0].description, 'Production build');
|
|
115
|
+
assert.equal(projection.plan[0].status, 'running');
|
|
116
|
+
assert.equal(projection.plan[0].owner, 'activity');
|
|
119
117
|
});
|
|
120
118
|
|
|
121
119
|
test('reduceAgentEvents: run_done finalizes running and pending plan steps', () => {
|
|
122
120
|
const projection = reduceAgentEvents([
|
|
123
|
-
createAgentEvent('
|
|
121
|
+
createAgentEvent('plan_set', {
|
|
122
|
+
origin: 'tool',
|
|
123
|
+
payload: { steps: ['Analyze', 'Execute'] },
|
|
124
|
+
}),
|
|
124
125
|
createAgentEvent('run_done', { origin: 'runtime' }),
|
|
125
126
|
]);
|
|
126
127
|
|
|
127
|
-
assert.deepEqual(projection.plan.map((step) => step.status), ['done', 'done'
|
|
128
|
+
assert.deepEqual(projection.plan.map((step) => step.status), ['done', 'done']);
|
|
128
129
|
assert.equal(projection.status, 'done');
|
|
129
130
|
});
|
|
130
131
|
|
|
131
132
|
test('dispatchAgentEvent: run_done finalizes session plan', () => {
|
|
132
133
|
const session = {};
|
|
133
|
-
dispatchAgentEvent(session, createAgentEvent('
|
|
134
|
+
dispatchAgentEvent(session, createAgentEvent('plan_set', {
|
|
135
|
+
origin: 'tool',
|
|
136
|
+
payload: { steps: ['Analyze', 'Execute'] },
|
|
137
|
+
}));
|
|
134
138
|
dispatchAgentEvent(session, createAgentEvent('run_done', { origin: 'runtime' }));
|
|
135
139
|
|
|
136
|
-
assert.deepEqual(session.headlessPlan.map((step) => step.status), ['done', 'done'
|
|
140
|
+
assert.deepEqual(session.headlessPlan.map((step) => step.status), ['done', 'done']);
|
|
137
141
|
assert.equal(session.agentProjection.status, 'done');
|
|
138
142
|
});
|
|
139
143
|
|
|
@@ -210,6 +214,37 @@ test('reduceAgentEvents: approvals move from pending to approved', () => {
|
|
|
210
214
|
assert.deepEqual(projection.approvals[0].plan, ['Build']);
|
|
211
215
|
});
|
|
212
216
|
|
|
217
|
+
test('reduceAgentEvents: control queue is event sourced and follows run status', () => {
|
|
218
|
+
const projection = reduceAgentEvents([
|
|
219
|
+
createAgentEvent('control_enqueued', {
|
|
220
|
+
origin: 'runtime',
|
|
221
|
+
workspace: 'docs',
|
|
222
|
+
payload: {
|
|
223
|
+
id: 'control-1',
|
|
224
|
+
workspace: 'docs',
|
|
225
|
+
input: 'Run after current task',
|
|
226
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
227
|
+
},
|
|
228
|
+
}),
|
|
229
|
+
createAgentEvent('control_started', {
|
|
230
|
+
origin: 'runtime',
|
|
231
|
+
runId: 'run-control-1',
|
|
232
|
+
workspace: 'docs',
|
|
233
|
+
payload: { id: 'control-1', runId: 'run-control-1' },
|
|
234
|
+
}),
|
|
235
|
+
createAgentEvent('run_done', {
|
|
236
|
+
origin: 'runtime',
|
|
237
|
+
runId: 'run-control-1',
|
|
238
|
+
workspace: 'docs',
|
|
239
|
+
}),
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
assert.equal(projection.controlQueue.length, 1);
|
|
243
|
+
assert.equal(projection.controlQueue[0].id, 'control-1');
|
|
244
|
+
assert.equal(projection.controlQueue[0].status, 'done');
|
|
245
|
+
assert.equal(projection.controlQueue[0].runId, 'run-control-1');
|
|
246
|
+
});
|
|
247
|
+
|
|
213
248
|
test('reduceAgentEvents: activity-owned plan is used when no orchestrator plan exists', () => {
|
|
214
249
|
const projection = reduceAgentEvents([
|
|
215
250
|
createAgentEvent('activity_upserted', {
|
|
@@ -273,3 +308,41 @@ test('dispatchAgentEvent: assistant deltas do not rewrite session plan or activi
|
|
|
273
308
|
assert.equal(planUpdates, updatesAfterActivity);
|
|
274
309
|
assert.equal(session.agentProjection.conversation.at(-1).content, 'bonjour');
|
|
275
310
|
});
|
|
311
|
+
|
|
312
|
+
test('reduceAgentEvents: plan patches are proposed, approved and applied with revisions', () => {
|
|
313
|
+
// plan_set bumps planRevision (it wholesale-replaces the plan), so a patch
|
|
314
|
+
// proposed after it must target that new revision (1), not 0.
|
|
315
|
+
const patch = {
|
|
316
|
+
targetRunId: 'run-patch',
|
|
317
|
+
basePlanRevision: 1,
|
|
318
|
+
operations: [{ op: 'add_task', task: { id: 'task-b', description: 'B', dependsOn: ['task-a'] } }],
|
|
319
|
+
};
|
|
320
|
+
const projection = reduceAgentEvents([
|
|
321
|
+
createAgentEvent('run_started', { origin: 'runtime', runId: 'run-patch' }),
|
|
322
|
+
createAgentEvent('plan_set', {
|
|
323
|
+
origin: 'tool',
|
|
324
|
+
runId: 'run-patch',
|
|
325
|
+
payload: { steps: [{ id: 'task-a', description: 'A', status: 'done' }] },
|
|
326
|
+
}),
|
|
327
|
+
createAgentEvent('plan_patch_proposed', {
|
|
328
|
+
origin: 'runtime',
|
|
329
|
+
runId: 'run-patch',
|
|
330
|
+
payload: { id: 'patch-1', patch },
|
|
331
|
+
}),
|
|
332
|
+
createAgentEvent('plan_patch_approved', {
|
|
333
|
+
origin: 'runtime',
|
|
334
|
+
runId: 'run-patch',
|
|
335
|
+
payload: { patchId: 'patch-1' },
|
|
336
|
+
}),
|
|
337
|
+
createAgentEvent('plan_patch_applied', {
|
|
338
|
+
origin: 'runtime',
|
|
339
|
+
runId: 'run-patch',
|
|
340
|
+
payload: { patchId: 'patch-1', patch },
|
|
341
|
+
}),
|
|
342
|
+
]);
|
|
343
|
+
|
|
344
|
+
assert.equal(projection.planRevision, 2);
|
|
345
|
+
assert.deepEqual(projection.plan.map((step) => step.id), ['task-a', 'task-b']);
|
|
346
|
+
assert.equal(projection.plan[1].status, 'pending');
|
|
347
|
+
assert.equal(projection.planPatches[0].status, 'applied');
|
|
348
|
+
});
|
package/src/core/agentLoop.js
CHANGED
|
@@ -2,6 +2,7 @@ import { buildAgentSystemPrompt, buildLimitedAgentResponse } from '../agent/grap
|
|
|
2
2
|
import { createAgentEvent, dispatchAgentEvent } from './agentEvents.js';
|
|
3
3
|
import { activitySnapshot, newNonTerminalActivities } from './activity.js';
|
|
4
4
|
import { extractHeadlessPlan, formatCompletedActivities, formatPlanStatus } from './plan.js';
|
|
5
|
+
import { formatReadyTaskPrompt, nextReadyPlanTask, readyPlanTasks } from './planPatch.js';
|
|
5
6
|
|
|
6
7
|
export function abortError(message = 'Agent run cancelled.') {
|
|
7
8
|
const err = new Error(message);
|
|
@@ -20,12 +21,14 @@ export async function runAgentTurn(agent, session, input, {
|
|
|
20
21
|
let streamedContent = '';
|
|
21
22
|
session._onStream = (delta) => { streamedContent += delta; };
|
|
22
23
|
session._onStreamReset = () => { streamedContent = ''; };
|
|
24
|
+
if (signal) session._abortSignal = signal;
|
|
23
25
|
let result;
|
|
24
26
|
try {
|
|
25
|
-
result = await agent.invoke({ input, session, messages });
|
|
27
|
+
result = await agent.invoke({ input, session, messages, signal });
|
|
26
28
|
} finally {
|
|
27
29
|
delete session._onStream;
|
|
28
30
|
delete session._onStreamReset;
|
|
31
|
+
if (session._abortSignal === signal) delete session._abortSignal;
|
|
29
32
|
}
|
|
30
33
|
if (result.streamedInline) {
|
|
31
34
|
return streamedContent.trim() || buildLimitedAgentResponse({ input, session }, 'LLM stream ended without content');
|
|
@@ -65,6 +68,7 @@ export async function runAgenticLoop(agent, session, initialInput, {
|
|
|
65
68
|
onActivitiesCompleted = null,
|
|
66
69
|
onMaxTurns = null,
|
|
67
70
|
abortMessage = 'Agent run cancelled.',
|
|
71
|
+
parallelHandoff = false,
|
|
68
72
|
} = {}) {
|
|
69
73
|
if (!waitForActivities) throw new Error('runAgenticLoop requires waitForActivities.');
|
|
70
74
|
const conversationHistory = [];
|
|
@@ -108,13 +112,25 @@ export async function runAgenticLoop(agent, session, initialInput, {
|
|
|
108
112
|
|
|
109
113
|
const newPending = newNonTerminalActivities(snapshot, session);
|
|
110
114
|
if (newPending.length === 0) {
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
// pending_approval is unfinished work too (a request_approval patch op
|
|
116
|
+
// sets it) — it must not be treated as "nothing left to do" just
|
|
117
|
+
// because no step is literally 'pending' anymore.
|
|
118
|
+
const pending = (session.headlessPlan ?? []).filter((step) => step.status === 'pending' || step.status === 'pending_approval');
|
|
119
|
+
const pendingSteps = readyPlanTasks(session.headlessPlan);
|
|
120
|
+
if (pending.length === 0) {
|
|
113
121
|
onComplete?.();
|
|
114
122
|
return { ok: true };
|
|
115
123
|
}
|
|
124
|
+
if (pendingSteps.length === 0) {
|
|
125
|
+
const reason = pending.every((step) => step.status === 'pending_approval') ? 'awaiting_approval' : 'no_ready_plan_task';
|
|
126
|
+
onPendingSteps?.({ pendingSteps: pending, blocked: true });
|
|
127
|
+
return { ok: false, stalled: true, reason };
|
|
128
|
+
}
|
|
129
|
+
if (parallelHandoff && pendingSteps.length > 1) {
|
|
130
|
+
return { ok: true, handoff: true };
|
|
131
|
+
}
|
|
116
132
|
onPendingSteps?.({ pendingSteps });
|
|
117
|
-
currentInput = pendingStepsPrompt(initialInput, session.headlessPlan);
|
|
133
|
+
currentInput = pendingStepsPrompt(initialInput, session.headlessPlan, pendingSteps[0]);
|
|
118
134
|
continue;
|
|
119
135
|
}
|
|
120
136
|
|
|
@@ -132,6 +148,9 @@ export async function runAgenticLoop(agent, session, initialInput, {
|
|
|
132
148
|
const completed = waitResult.completed ?? [];
|
|
133
149
|
const summary = formatCompletedActivities(completed);
|
|
134
150
|
onActivitiesCompleted?.({ completed, summary });
|
|
151
|
+
if (parallelHandoff && readyPlanTasks(session.headlessPlan).length > 1) {
|
|
152
|
+
return { ok: true, handoff: true };
|
|
153
|
+
}
|
|
135
154
|
currentInput = completedActivitiesPrompt(initialInput, session.headlessPlan, summary);
|
|
136
155
|
}
|
|
137
156
|
|
|
@@ -139,15 +158,18 @@ export async function runAgenticLoop(agent, session, initialInput, {
|
|
|
139
158
|
return { ok: false, maxTurns: true };
|
|
140
159
|
}
|
|
141
160
|
|
|
142
|
-
function pendingStepsPrompt(initialInput, plan) {
|
|
161
|
+
function pendingStepsPrompt(initialInput, plan, readyTask) {
|
|
143
162
|
return [
|
|
144
163
|
'Original task:',
|
|
145
164
|
initialInput,
|
|
146
165
|
'',
|
|
147
166
|
`Plan status:\n${formatPlanStatus(plan)}`,
|
|
148
167
|
'',
|
|
168
|
+
formatReadyTaskPrompt(readyTask),
|
|
169
|
+
'',
|
|
149
170
|
'No new background activity was started in the previous turn.',
|
|
150
|
-
'Continue the original plan. Start
|
|
171
|
+
'Continue the original plan. Start exactly this next ready task only.',
|
|
172
|
+
'Do not start tasks whose dependencies are not done.',
|
|
151
173
|
'If required information is missing and cannot be inferred, stop with a clear blocker.',
|
|
152
174
|
].join('\n');
|
|
153
175
|
}
|
|
@@ -161,7 +183,10 @@ function completedActivitiesPrompt(initialInput, plan, summary) {
|
|
|
161
183
|
'Completed activities:',
|
|
162
184
|
summary,
|
|
163
185
|
'',
|
|
164
|
-
|
|
186
|
+
formatReadyTaskPrompt(nextReadyPlanTask(plan)),
|
|
187
|
+
'',
|
|
188
|
+
'Continue the original plan. Start exactly this next ready task only.',
|
|
189
|
+
'Do not start tasks whose dependencies are not done.',
|
|
165
190
|
'If all steps are complete, provide the final summary.',
|
|
166
191
|
].filter(Boolean).join('\n');
|
|
167
192
|
}
|
package/src/core/mcp.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { managerEnvFile, managerMcpEndpointsFile, readEnvFile } from './env.js';
|
|
3
3
|
|
|
4
|
+
const WIKI_MANAGER_VERSION = '0.10.4';
|
|
5
|
+
|
|
4
6
|
function envValue(key) {
|
|
5
7
|
const filePath = managerEnvFile();
|
|
6
8
|
if (existsSync(filePath)) {
|
|
@@ -230,7 +232,7 @@ async function mcpRequest(endpoint, method, params, signal, options = {}) {
|
|
|
230
232
|
params: {
|
|
231
233
|
protocolVersion: '2025-06-18',
|
|
232
234
|
capabilities: {},
|
|
233
|
-
clientInfo: { name: 'wiki-manager', version:
|
|
235
|
+
clientInfo: { name: 'wiki-manager', version: WIKI_MANAGER_VERSION },
|
|
234
236
|
},
|
|
235
237
|
}),
|
|
236
238
|
});
|
package/src/core/plan.js
CHANGED
|
@@ -15,6 +15,10 @@ export function ensurePlanFromActivity(session, activity) {
|
|
|
15
15
|
id: s.id ?? null,
|
|
16
16
|
description: s.label,
|
|
17
17
|
status: 'pending',
|
|
18
|
+
dependsOn: Array.isArray(s.dependsOn) ? s.dependsOn.map(String) : [],
|
|
19
|
+
executor: s.executor ?? null,
|
|
20
|
+
executorQuery: s.executorQuery ?? null,
|
|
21
|
+
outputRefs: Array.isArray(s.outputRefs) ? s.outputRefs.map(String) : [],
|
|
18
22
|
owner: 'activity',
|
|
19
23
|
ownerActivityKey: activity.key,
|
|
20
24
|
_activityKey: activity.key,
|