@dotdrelle/wiki-manager 0.9.3 → 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 +21 -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 +1 -1
- package/src/commands/slash.js +6 -2
- 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 +158 -15
- package/src/core/agentEvents.test.js +54 -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/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 +322 -15
- package/src/runtime/server.test.js +339 -0
- package/src/runtime/store.js +59 -10
- package/src/runtime/store.test.js +47 -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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { assertContract, validateContract } from './schemas.js';
|
|
4
|
+
|
|
5
|
+
test('activity contract accepts the canonical v1 shape and extra fields', () => {
|
|
6
|
+
const activity = {
|
|
7
|
+
schemaVersion: '1',
|
|
8
|
+
id: 'job-1',
|
|
9
|
+
source: 'production',
|
|
10
|
+
kind: 'build',
|
|
11
|
+
label: 'Production build',
|
|
12
|
+
status: 'running',
|
|
13
|
+
progress: {
|
|
14
|
+
percent: 42,
|
|
15
|
+
stepId: 'draft',
|
|
16
|
+
parentActivityKey: 'production:parent',
|
|
17
|
+
detail: 'Drafting',
|
|
18
|
+
},
|
|
19
|
+
poll: null,
|
|
20
|
+
outputRefs: ['deliverables/report.md', { type: 'wiki_page', ref: 'Reports/Build' }],
|
|
21
|
+
agentSpecificField: true,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
assert.equal(validateContract('activity', activity).ok, true);
|
|
25
|
+
assert.equal(assertContract('activity', activity), activity);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('activity contract rejects missing canonical fields', () => {
|
|
29
|
+
const result = validateContract('activity', {
|
|
30
|
+
id: 'job-1',
|
|
31
|
+
source: 'production',
|
|
32
|
+
kind: 'build',
|
|
33
|
+
label: 'Production build',
|
|
34
|
+
status: 'running',
|
|
35
|
+
progress: {},
|
|
36
|
+
poll: null,
|
|
37
|
+
outputRefs: [],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert.equal(result.ok, false);
|
|
41
|
+
assert.ok(result.errors.some((error) => error.includes('schemaVersion')));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('agent event contract carries the unified audit identity fields', () => {
|
|
45
|
+
const event = {
|
|
46
|
+
id: 'event-1',
|
|
47
|
+
ts: new Date(0).toISOString(),
|
|
48
|
+
type: 'tool_call_result',
|
|
49
|
+
origin: 'runtime',
|
|
50
|
+
runId: 'run-1',
|
|
51
|
+
turnId: 'turn-1',
|
|
52
|
+
taskId: 'task-1',
|
|
53
|
+
workspace: 'juno',
|
|
54
|
+
payload: { toolCallId: 'call-1', ok: true },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
assert.equal(validateContract('agentRunEvent', event).ok, true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('run and control request contracts reject empty run input but accept explicit controls', () => {
|
|
61
|
+
assert.equal(validateContract('runRequest', { input: 'Build docs', workspace: 'juno' }).ok, true);
|
|
62
|
+
assert.equal(validateContract('runRequest', { input: '' }).ok, false);
|
|
63
|
+
assert.equal(validateContract('controlMessage', { action: 'message', input: 'Ou en est le build ?', intent: 'observe' }).ok, true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('plan and patch contracts cover structured dependencies and output refs', () => {
|
|
67
|
+
const plan = [{
|
|
68
|
+
step: 1,
|
|
69
|
+
id: 'task-a',
|
|
70
|
+
description: 'Export',
|
|
71
|
+
status: 'pending',
|
|
72
|
+
dependsOn: [],
|
|
73
|
+
executor: 'cme.cme_export_run',
|
|
74
|
+
executorQuery: null,
|
|
75
|
+
outputRefs: ['raw/export.json'],
|
|
76
|
+
}];
|
|
77
|
+
const patch = {
|
|
78
|
+
targetRunId: 'run-1',
|
|
79
|
+
basePlanRevision: 4,
|
|
80
|
+
operations: [{
|
|
81
|
+
op: 'add_task',
|
|
82
|
+
task: {
|
|
83
|
+
id: 'task-b',
|
|
84
|
+
description: 'Build',
|
|
85
|
+
dependsOn: ['task-a'],
|
|
86
|
+
executorQuery: { capability: 'production build' },
|
|
87
|
+
},
|
|
88
|
+
}],
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
assert.equal(validateContract('plan', plan).ok, true);
|
|
92
|
+
assert.equal(validateContract('planPatch', patch).ok, true);
|
|
93
|
+
});
|
package/src/core/activity.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { validateContractInDev } from '../contracts/schemas.js';
|
|
2
|
+
|
|
1
3
|
export function parseJsonText(text) {
|
|
2
4
|
try {
|
|
3
5
|
return JSON.parse(text);
|
|
@@ -14,7 +16,7 @@ function terminalStatus(status) {
|
|
|
14
16
|
return ['done', 'failed', 'cancelled', 'canceled', 'complete', 'completed', 'success', 'error'].includes(String(status ?? '').toLowerCase());
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
function activityKey(activity) {
|
|
19
|
+
export function activityKey(activity) {
|
|
18
20
|
const id = activity?.id ?? activity?.jobId ?? activity?.job_id;
|
|
19
21
|
const source = activity?.source ?? activity?.agent ?? activity?.poll?.server ?? 'mcp';
|
|
20
22
|
return `${source}:${id ?? activity?.kind ?? activity?.label ?? 'activity'}`;
|
|
@@ -25,6 +27,10 @@ function normalizePlanSteps(steps) {
|
|
|
25
27
|
return steps.map((s, i) => ({
|
|
26
28
|
id: s != null && s.id != null ? String(s.id) : String(i + 1),
|
|
27
29
|
label: s != null ? String(s.label ?? s.description ?? s.name ?? s.id ?? (i + 1)) : String(i + 1),
|
|
30
|
+
dependsOn: Array.isArray(s?.dependsOn) ? s.dependsOn.map(String) : [],
|
|
31
|
+
executor: s?.executor ?? null,
|
|
32
|
+
executorQuery: s?.executorQuery ?? null,
|
|
33
|
+
outputRefs: Array.isArray(s?.outputRefs) ? s.outputRefs.map(String) : [],
|
|
28
34
|
}));
|
|
29
35
|
}
|
|
30
36
|
|
|
@@ -62,9 +68,13 @@ export function normalizeActivity(activity, fallback = {}) {
|
|
|
62
68
|
kind,
|
|
63
69
|
step,
|
|
64
70
|
].filter(Boolean).join(' ');
|
|
71
|
+
const outputRefs = Array.isArray(activity.outputRefs)
|
|
72
|
+
? activity.outputRefs.map((ref) => (ref && typeof ref === 'object' ? { ...ref } : String(ref)))
|
|
73
|
+
: [];
|
|
65
74
|
const normalized = {
|
|
66
75
|
key: null,
|
|
67
|
-
|
|
76
|
+
schemaVersion: String(activity.schemaVersion ?? '1'),
|
|
77
|
+
id: String(id ?? kind ?? 'activity'),
|
|
68
78
|
source: String(source),
|
|
69
79
|
kind: String(kind),
|
|
70
80
|
label: String(label || `${source} ${kind}`),
|
|
@@ -79,12 +89,14 @@ export function normalizeActivity(activity, fallback = {}) {
|
|
|
79
89
|
},
|
|
80
90
|
plan: planSteps ? { steps: planSteps } : null,
|
|
81
91
|
poll: normalizePoll(activity.poll ?? fallback.poll),
|
|
92
|
+
outputRefs,
|
|
82
93
|
startedAt: activity.startedAt ?? fallback.startedAt ?? null,
|
|
83
94
|
updatedAt: activity.updatedAt ?? new Date().toISOString(),
|
|
84
95
|
error: activity.error ?? null,
|
|
85
96
|
terminal: Boolean(activity.terminal ?? terminalStatus(status)),
|
|
86
97
|
};
|
|
87
98
|
normalized.key = activityKey(normalized);
|
|
99
|
+
validateContractInDev('activity', normalized);
|
|
88
100
|
return normalized;
|
|
89
101
|
}
|
|
90
102
|
|
|
@@ -11,9 +11,11 @@ test('normalizeActivity: plan.steps preserved with id and label', () => {
|
|
|
11
11
|
plan: { steps: [{ id: 'extract', label: 'Extraction' }, { id: 'build', label: 'Build' }] },
|
|
12
12
|
progress: {},
|
|
13
13
|
});
|
|
14
|
+
assert.equal(a.schemaVersion, '1');
|
|
15
|
+
assert.deepEqual(a.outputRefs, []);
|
|
14
16
|
assert.deepEqual(a.plan.steps, [
|
|
15
|
-
{ id: 'extract', label: 'Extraction' },
|
|
16
|
-
{ id: 'build', label: 'Build' },
|
|
17
|
+
{ id: 'extract', label: 'Extraction', dependsOn: [], executor: null, executorQuery: null, outputRefs: [] },
|
|
18
|
+
{ id: 'build', label: 'Build', dependsOn: [], executor: null, executorQuery: null, outputRefs: [] },
|
|
17
19
|
]);
|
|
18
20
|
});
|
|
19
21
|
|
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',
|
|
@@ -26,6 +35,7 @@ const SESSION_PROJECTION_EVENTS = new Set([
|
|
|
26
35
|
const PLAN_MUTATING_EVENTS = new Set([
|
|
27
36
|
'run_started',
|
|
28
37
|
'plan_set',
|
|
38
|
+
'plan_patch_applied',
|
|
29
39
|
'plan_step_updated',
|
|
30
40
|
'activity_upserted',
|
|
31
41
|
'run_done',
|
|
@@ -36,18 +46,20 @@ export function createAgentEvent(type, {
|
|
|
36
46
|
payload = {},
|
|
37
47
|
runId = null,
|
|
38
48
|
turnId = null,
|
|
49
|
+
taskId = null,
|
|
39
50
|
workspace = null,
|
|
40
51
|
} = {}) {
|
|
41
|
-
return {
|
|
52
|
+
return validateContractInDev('agentRunEvent', {
|
|
42
53
|
id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
|
|
43
54
|
ts: new Date().toISOString(),
|
|
44
55
|
type,
|
|
45
56
|
origin,
|
|
46
57
|
runId,
|
|
47
58
|
turnId,
|
|
59
|
+
taskId,
|
|
48
60
|
workspace,
|
|
49
61
|
payload,
|
|
50
|
-
};
|
|
62
|
+
});
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
export function dispatchAgentEvent(session, event) {
|
|
@@ -55,6 +67,7 @@ export function dispatchAgentEvent(session, event) {
|
|
|
55
67
|
event.id && event.ts ? event : createAgentEvent(event.type, event),
|
|
56
68
|
session,
|
|
57
69
|
);
|
|
70
|
+
validateContractInDev('agentRunEvent', normalized);
|
|
58
71
|
const tracksPlan = PLAN_MUTATING_EVENTS.has(normalized.type);
|
|
59
72
|
const previousPlan = tracksPlan ? JSON.stringify(session.headlessPlan ?? null) : null;
|
|
60
73
|
session.agentEvents ??= [];
|
|
@@ -79,6 +92,7 @@ function withSessionRunIdentity(event, session) {
|
|
|
79
92
|
...event,
|
|
80
93
|
runId: event.runId ?? identity.runId ?? null,
|
|
81
94
|
turnId: event.turnId ?? identity.turnId ?? null,
|
|
95
|
+
taskId: event.taskId ?? identity.taskId ?? null,
|
|
82
96
|
workspace: event.workspace ?? identity.workspace ?? null,
|
|
83
97
|
};
|
|
84
98
|
}
|
|
@@ -103,6 +117,8 @@ function createProjectionState() {
|
|
|
103
117
|
evaluation: null,
|
|
104
118
|
replans: [],
|
|
105
119
|
approvals: [],
|
|
120
|
+
planRevision: 0,
|
|
121
|
+
planPatches: [],
|
|
106
122
|
controlQueue: [],
|
|
107
123
|
summary: null,
|
|
108
124
|
status: 'idle',
|
|
@@ -110,7 +126,7 @@ function createProjectionState() {
|
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
function publicProjection(state) {
|
|
113
|
-
|
|
129
|
+
const projection = {
|
|
114
130
|
conversation: state.conversation.map((message) => ({ ...message })),
|
|
115
131
|
chain: state.chain.map((step) => ({ ...step })),
|
|
116
132
|
plan: state.plan ? state.plan.map((step) => ({ ...step })) : null,
|
|
@@ -119,16 +135,29 @@ function publicProjection(state) {
|
|
|
119
135
|
evaluation: state.evaluation ? { ...state.evaluation } : null,
|
|
120
136
|
replans: state.replans.map((replan) => ({ ...replan, plan: [...(replan.plan ?? [])] })),
|
|
121
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
|
+
})),
|
|
122
144
|
controlQueue: state.controlQueue.map((item) => ({ ...item })),
|
|
123
145
|
summary: state.summary,
|
|
124
146
|
status: state.status,
|
|
125
147
|
};
|
|
148
|
+
return {
|
|
149
|
+
...projection,
|
|
150
|
+
workflow: projectWorkflow(projection),
|
|
151
|
+
};
|
|
126
152
|
}
|
|
127
153
|
|
|
128
154
|
export function applyAgentProjectionToSession(session, projection) {
|
|
129
155
|
session.headlessPlan = projection.plan ? projection.plan.map((step) => ({ ...step })) : null;
|
|
130
156
|
session.activities = Object.fromEntries((projection.activities ?? []).map((activity) => [activity.key, { ...activity }]));
|
|
131
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;
|
|
132
161
|
const production = (projection.activities ?? []).filter((activity) => activity.source === 'production').at(-1);
|
|
133
162
|
session.productionActivity = production ? {
|
|
134
163
|
jobId: production.id,
|
|
@@ -143,13 +172,15 @@ function applyEvent(state, event) {
|
|
|
143
172
|
switch (event.type) {
|
|
144
173
|
case 'run_started':
|
|
145
174
|
state.status = 'running';
|
|
146
|
-
state.plan =
|
|
175
|
+
state.plan = null;
|
|
147
176
|
state.chain = [];
|
|
148
177
|
state.activities = {};
|
|
149
178
|
state.logs = [];
|
|
150
179
|
state.evaluation = null;
|
|
151
180
|
state.replans = [];
|
|
152
181
|
state.approvals = [];
|
|
182
|
+
state.planRevision = 0;
|
|
183
|
+
state.planPatches = [];
|
|
153
184
|
state.summary = null;
|
|
154
185
|
return;
|
|
155
186
|
case 'user_message':
|
|
@@ -179,10 +210,97 @@ function applyEvent(state, event) {
|
|
|
179
210
|
return;
|
|
180
211
|
case 'plan_set':
|
|
181
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;
|
|
182
222
|
return;
|
|
183
223
|
case 'plan_step_updated':
|
|
184
224
|
updatePlanStep(state.plan, event.payload ?? {});
|
|
185
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;
|
|
186
304
|
case 'run_summary':
|
|
187
305
|
state.summary = String(event.payload?.content ?? '');
|
|
188
306
|
if (state.summary) state.conversation.push({ role: 'assistant', content: state.summary });
|
|
@@ -288,6 +406,15 @@ function upsertControlItem(queue, next) {
|
|
|
288
406
|
upsertById(queue, next);
|
|
289
407
|
}
|
|
290
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
|
+
|
|
291
418
|
function finishControlByRun(queue, runId, status, finishedAt) {
|
|
292
419
|
if (!runId) return;
|
|
293
420
|
const item = queue.find((entry) => entry.runId === runId && entry.status === 'running');
|
|
@@ -368,6 +495,10 @@ function ensurePlanFromActivityProjection(state, activity) {
|
|
|
368
495
|
id: step.id ?? null,
|
|
369
496
|
description: step.label,
|
|
370
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) : [],
|
|
371
502
|
owner: 'activity',
|
|
372
503
|
ownerActivityKey: activity.key,
|
|
373
504
|
_activityKey: activity.key,
|
|
@@ -379,6 +510,10 @@ function ensurePlanFromActivityProjection(state, activity) {
|
|
|
379
510
|
id: null,
|
|
380
511
|
description: activity.label,
|
|
381
512
|
status: 'pending',
|
|
513
|
+
dependsOn: [],
|
|
514
|
+
executor: null,
|
|
515
|
+
executorQuery: null,
|
|
516
|
+
outputRefs: [],
|
|
382
517
|
owner: 'activity',
|
|
383
518
|
ownerActivityKey: activity.key,
|
|
384
519
|
_activityKey: activity.key,
|
|
@@ -389,33 +524,41 @@ function normalizePlan(steps, payload = {}) {
|
|
|
389
524
|
if (!Array.isArray(steps)) return null;
|
|
390
525
|
const owner = payload.owner ?? 'orchestrator';
|
|
391
526
|
const ownerActivityKey = payload.ownerActivityKey ?? payload.activityKey ?? null;
|
|
392
|
-
|
|
527
|
+
const plan = steps.map((raw, i) => {
|
|
393
528
|
const item = typeof raw === 'string' ? { description: raw } : (raw ?? {});
|
|
394
529
|
return {
|
|
395
530
|
step: Number(item.step ?? i + 1),
|
|
396
531
|
id: item.id ?? null,
|
|
397
532
|
description: String(item.description ?? item.label ?? item.name ?? `Step ${i + 1}`),
|
|
398
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,
|
|
399
543
|
owner: item.owner ?? owner,
|
|
400
544
|
ownerActivityKey: item.ownerActivityKey ?? ownerActivityKey,
|
|
401
545
|
_activityKey: item._activityKey ?? payload.activityKey ?? null,
|
|
402
546
|
};
|
|
403
547
|
});
|
|
404
|
-
|
|
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
|
-
];
|
|
548
|
+
return validateContractInDev('plan', plan);
|
|
412
549
|
}
|
|
413
550
|
|
|
414
551
|
function updatePlanStep(plan, payload) {
|
|
415
552
|
if (!plan) return;
|
|
416
|
-
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));
|
|
417
557
|
if (!step) return;
|
|
418
|
-
|
|
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';
|
|
419
562
|
if (payload.activityKey) step.activityKey = payload.activityKey;
|
|
420
563
|
}
|
|
421
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
|
|
|
@@ -304,3 +308,41 @@ test('dispatchAgentEvent: assistant deltas do not rewrite session plan or activi
|
|
|
304
308
|
assert.equal(planUpdates, updatesAfterActivity);
|
|
305
309
|
assert.equal(session.agentProjection.conversation.at(-1).content, 'bonjour');
|
|
306
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
|
+
});
|