@dotdrelle/wiki-manager 0.11.9 → 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
package/src/core/agentEvents.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { normalizeActivity } from './activity.js';
|
|
2
2
|
import { attachActivityToExistingPlan, syncActivitiesToPlan } from './plan.js';
|
|
3
3
|
import { applyPlanPatch, normalizePlanPatch, normalizePlanRevision, rebasePlanPatch } from './planPatch.js';
|
|
4
|
+
import { formatRuntimeLogPayload } from './runtimeLog.js';
|
|
4
5
|
import { projectWorkflow } from './workflow.js';
|
|
5
6
|
import { validateContractInDev } from '../contracts/schemas.js';
|
|
6
7
|
|
|
@@ -14,6 +15,18 @@ const SESSION_PROJECTION_EVENTS = new Set([
|
|
|
14
15
|
'plan_patch_applied',
|
|
15
16
|
'plan_patch_rebased',
|
|
16
17
|
'plan_patch_rejected',
|
|
18
|
+
'plan.received',
|
|
19
|
+
'plan.validated',
|
|
20
|
+
'plan.rejected',
|
|
21
|
+
'task_group.created',
|
|
22
|
+
'task.created',
|
|
23
|
+
'task.assigned',
|
|
24
|
+
'task.started',
|
|
25
|
+
'task.retry_scheduled',
|
|
26
|
+
'task.result_returned',
|
|
27
|
+
'task.completed',
|
|
28
|
+
'task.failed',
|
|
29
|
+
'plan.revision_changed',
|
|
17
30
|
'activity_upserted',
|
|
18
31
|
'run_evaluated',
|
|
19
32
|
'run_replanned',
|
|
@@ -21,8 +34,13 @@ const SESSION_PROJECTION_EVENTS = new Set([
|
|
|
21
34
|
'run_approved',
|
|
22
35
|
'tool_pending_approval',
|
|
23
36
|
'tool_approved',
|
|
37
|
+
'approval.requested',
|
|
38
|
+
'approval.granted',
|
|
39
|
+
'approval.rejected',
|
|
24
40
|
'control_enqueued',
|
|
25
41
|
'control_started',
|
|
42
|
+
'agent.registered',
|
|
43
|
+
'agent.health_changed',
|
|
26
44
|
'run_done',
|
|
27
45
|
'run_error',
|
|
28
46
|
'run_cancelled',
|
|
@@ -36,6 +54,7 @@ const PLAN_MUTATING_EVENTS = new Set([
|
|
|
36
54
|
'run_started',
|
|
37
55
|
'plan_set',
|
|
38
56
|
'plan_patch_applied',
|
|
57
|
+
'task.created',
|
|
39
58
|
'plan_step_updated',
|
|
40
59
|
'activity_upserted',
|
|
41
60
|
'run_done',
|
|
@@ -120,6 +139,7 @@ function createProjectionState() {
|
|
|
120
139
|
planRevision: 0,
|
|
121
140
|
planPatches: [],
|
|
122
141
|
controlQueue: [],
|
|
142
|
+
agents: {},
|
|
123
143
|
summary: null,
|
|
124
144
|
status: 'idle',
|
|
125
145
|
};
|
|
@@ -142,6 +162,9 @@ function publicProjection(state) {
|
|
|
142
162
|
patch: patch.patch ? { ...patch.patch, operations: (patch.patch.operations ?? []).map((operation) => ({ ...operation })) } : null,
|
|
143
163
|
})),
|
|
144
164
|
controlQueue: state.controlQueue.map((item) => ({ ...item })),
|
|
165
|
+
agents: Object.values(state.agents)
|
|
166
|
+
.map((agent) => ({ ...agent, description: cloneJson(agent.description) }))
|
|
167
|
+
.sort((a, b) => a.agentInstanceId.localeCompare(b.agentInstanceId)),
|
|
145
168
|
summary: state.summary,
|
|
146
169
|
status: state.status,
|
|
147
170
|
};
|
|
@@ -155,6 +178,7 @@ export function applyAgentProjectionToSession(session, projection) {
|
|
|
155
178
|
session.headlessPlan = projection.plan ? projection.plan.map((step) => ({ ...step })) : null;
|
|
156
179
|
session.activities = Object.fromEntries((projection.activities ?? []).map((activity) => [activity.key, { ...activity }]));
|
|
157
180
|
session.controlQueue = (projection.controlQueue ?? []).map((item) => ({ ...item }));
|
|
181
|
+
session.agents = (projection.agents ?? []).map((agent) => ({ ...agent, description: cloneJson(agent.description) }));
|
|
158
182
|
session.planRevision = projection.planRevision ?? 0;
|
|
159
183
|
session.planPatches = (projection.planPatches ?? []).map((patch) => ({ ...patch }));
|
|
160
184
|
session.workflow = projection.workflow ? { ...projection.workflow } : null;
|
|
@@ -220,6 +244,53 @@ function applyEvent(state, event) {
|
|
|
220
244
|
? normalizePlanRevision(event.payload.planRevision)
|
|
221
245
|
: state.planRevision + 1;
|
|
222
246
|
return;
|
|
247
|
+
case 'plan.received':
|
|
248
|
+
state.logs.push(`Plan received for run ${String(event.runId ?? event.payload?.runId ?? '')}`.trim());
|
|
249
|
+
state.logs = state.logs.slice(-200);
|
|
250
|
+
return;
|
|
251
|
+
case 'plan.validated':
|
|
252
|
+
state.logs.push(`Plan validated for run ${String(event.runId ?? event.payload?.runId ?? '')}`.trim());
|
|
253
|
+
state.logs = state.logs.slice(-200);
|
|
254
|
+
return;
|
|
255
|
+
case 'plan.rejected':
|
|
256
|
+
state.logs.push(`Plan rejected: ${formatPlanErrors(event.payload?.errors)}`);
|
|
257
|
+
state.logs = state.logs.slice(-200);
|
|
258
|
+
return;
|
|
259
|
+
case 'task_group.created':
|
|
260
|
+
return;
|
|
261
|
+
case 'task.created':
|
|
262
|
+
appendCreatedTask(state, event.payload?.task);
|
|
263
|
+
return;
|
|
264
|
+
case 'task.assigned':
|
|
265
|
+
state.logs.push(`Task assigned: ${String(event.taskId ?? event.payload?.taskId ?? '')}`.trim());
|
|
266
|
+
state.logs = state.logs.slice(-200);
|
|
267
|
+
return;
|
|
268
|
+
case 'task.started':
|
|
269
|
+
state.logs.push(`Task started: ${String(event.taskId ?? event.payload?.taskId ?? '')}`.trim());
|
|
270
|
+
state.logs = state.logs.slice(-200);
|
|
271
|
+
return;
|
|
272
|
+
case 'task.retry_scheduled':
|
|
273
|
+
state.logs.push(`Task retry scheduled: ${String(event.taskId ?? event.payload?.taskId ?? '')}`.trim());
|
|
274
|
+
state.logs = state.logs.slice(-200);
|
|
275
|
+
return;
|
|
276
|
+
case 'task.result_returned':
|
|
277
|
+
state.logs.push(`Task result returned: ${String(event.taskId ?? event.payload?.taskId ?? '')}`.trim());
|
|
278
|
+
state.logs = state.logs.slice(-200);
|
|
279
|
+
return;
|
|
280
|
+
case 'task.completed':
|
|
281
|
+
state.logs.push(`Task completed: ${String(event.taskId ?? event.payload?.taskId ?? '')}`.trim());
|
|
282
|
+
state.logs = state.logs.slice(-200);
|
|
283
|
+
return;
|
|
284
|
+
case 'task.failed':
|
|
285
|
+
state.logs.push(`Task failed: ${String(event.taskId ?? event.payload?.taskId ?? '')}`.trim());
|
|
286
|
+
state.logs = state.logs.slice(-200);
|
|
287
|
+
return;
|
|
288
|
+
case 'plan.revision_changed':
|
|
289
|
+
if (Array.isArray(event.payload?.tasks)) {
|
|
290
|
+
state.plan = normalizePlan(event.payload.tasks, { owner: 'orchestrator', planRevision: state.planRevision });
|
|
291
|
+
}
|
|
292
|
+
state.planRevision = normalizePlanRevision(event.payload?.planRevision ?? state.planRevision + 1);
|
|
293
|
+
return;
|
|
223
294
|
case 'plan_step_updated':
|
|
224
295
|
updatePlanStep(state.plan, event.payload ?? {});
|
|
225
296
|
return;
|
|
@@ -346,6 +417,60 @@ function applyEvent(state, event) {
|
|
|
346
417
|
approvedAt: event.ts,
|
|
347
418
|
});
|
|
348
419
|
return;
|
|
420
|
+
case 'approval.requested':
|
|
421
|
+
upsertApproval(state, {
|
|
422
|
+
id: approvalProjectionId(event),
|
|
423
|
+
approvalId: event.payload?.approvalId ?? event.payload?.id ?? null,
|
|
424
|
+
scope: event.payload?.scope ?? 'task',
|
|
425
|
+
status: 'pending_approval',
|
|
426
|
+
runId: event.runId ?? event.payload?.runId ?? null,
|
|
427
|
+
workspaceId: event.workspace ?? event.payload?.workspaceId ?? event.payload?.workspace ?? null,
|
|
428
|
+
planRevision: event.payload?.planRevision ?? null,
|
|
429
|
+
taskId: event.taskId ?? event.payload?.taskId ?? null,
|
|
430
|
+
itemId: event.payload?.itemId ?? event.payload?.taskId ?? null,
|
|
431
|
+
groupId: event.payload?.groupId ?? null,
|
|
432
|
+
approvalClasses: normalizeApprovalClasses(event.payload?.approvalClasses ?? event.payload?.approvalClass),
|
|
433
|
+
reason: event.payload?.reason ?? null,
|
|
434
|
+
createdAt: event.ts,
|
|
435
|
+
});
|
|
436
|
+
return;
|
|
437
|
+
case 'approval.granted': {
|
|
438
|
+
const grant = {
|
|
439
|
+
id: approvalProjectionId(event),
|
|
440
|
+
approvalId: event.payload?.approvalId ?? event.payload?.id ?? null,
|
|
441
|
+
scope: event.payload?.scope ?? 'run',
|
|
442
|
+
status: 'approved',
|
|
443
|
+
runId: event.runId ?? event.payload?.runId ?? null,
|
|
444
|
+
workspaceId: event.workspace ?? event.payload?.workspaceId ?? event.payload?.workspace ?? null,
|
|
445
|
+
planRevision: event.payload?.planRevision ?? null,
|
|
446
|
+
taskId: event.taskId ?? event.payload?.taskId ?? null,
|
|
447
|
+
itemId: event.payload?.itemId ?? event.payload?.taskId ?? null,
|
|
448
|
+
groupId: event.payload?.groupId ?? null,
|
|
449
|
+
approvalClasses: normalizeApprovalClasses(event.payload?.approvalClasses ?? event.payload?.approvalClass),
|
|
450
|
+
reason: event.payload?.reason ?? null,
|
|
451
|
+
approvedAt: event.ts,
|
|
452
|
+
};
|
|
453
|
+
upsertApproval(state, grant);
|
|
454
|
+
markCoveredApprovalsApproved(state.approvals, grant, event.ts);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
case 'approval.rejected':
|
|
458
|
+
upsertApproval(state, {
|
|
459
|
+
id: approvalProjectionId(event),
|
|
460
|
+
approvalId: event.payload?.approvalId ?? event.payload?.id ?? null,
|
|
461
|
+
scope: event.payload?.scope ?? 'run',
|
|
462
|
+
status: 'rejected',
|
|
463
|
+
runId: event.runId ?? event.payload?.runId ?? null,
|
|
464
|
+
workspaceId: event.workspace ?? event.payload?.workspaceId ?? event.payload?.workspace ?? null,
|
|
465
|
+
planRevision: event.payload?.planRevision ?? null,
|
|
466
|
+
taskId: event.taskId ?? event.payload?.taskId ?? null,
|
|
467
|
+
itemId: event.payload?.itemId ?? event.payload?.taskId ?? null,
|
|
468
|
+
groupId: event.payload?.groupId ?? null,
|
|
469
|
+
approvalClasses: normalizeApprovalClasses(event.payload?.approvalClasses ?? event.payload?.approvalClass),
|
|
470
|
+
reason: event.payload?.reason ?? null,
|
|
471
|
+
rejectedAt: event.ts,
|
|
472
|
+
});
|
|
473
|
+
return;
|
|
349
474
|
case 'run_done':
|
|
350
475
|
state.status = 'done';
|
|
351
476
|
finishPendingPlanSteps(state.plan);
|
|
@@ -380,15 +505,36 @@ function applyEvent(state, event) {
|
|
|
380
505
|
updatedAt: event.ts,
|
|
381
506
|
});
|
|
382
507
|
return;
|
|
508
|
+
case 'agent.registered':
|
|
509
|
+
upsertAgent(state, event.payload?.agent, event.ts);
|
|
510
|
+
return;
|
|
511
|
+
case 'agent.health_changed':
|
|
512
|
+
upsertAgent(state, {
|
|
513
|
+
...(event.payload?.agent ?? {}),
|
|
514
|
+
agentInstanceId: event.payload?.agentInstanceId ?? event.payload?.agent?.agentInstanceId,
|
|
515
|
+
health: event.payload?.health ?? event.payload?.agent?.health,
|
|
516
|
+
}, event.ts);
|
|
517
|
+
return;
|
|
383
518
|
case 'runtime_log':
|
|
384
|
-
state.logs.push(
|
|
385
|
-
state.logs = state.logs.slice(-200);
|
|
519
|
+
state.logs.push(formatRuntimeLogPayload(event.payload ?? {}, event.ts));
|
|
386
520
|
return;
|
|
387
521
|
default:
|
|
388
522
|
return;
|
|
389
523
|
}
|
|
390
524
|
}
|
|
391
525
|
|
|
526
|
+
function upsertAgent(state, rawAgent, ts) {
|
|
527
|
+
if (!rawAgent?.agentInstanceId) return;
|
|
528
|
+
const existing = state.agents[rawAgent.agentInstanceId] ?? {};
|
|
529
|
+
state.agents[rawAgent.agentInstanceId] = {
|
|
530
|
+
...existing,
|
|
531
|
+
...rawAgent,
|
|
532
|
+
description: cloneJson(rawAgent.description ?? existing.description ?? null),
|
|
533
|
+
health: rawAgent.health ?? rawAgent.description?.health?.status ?? existing.health ?? 'unavailable',
|
|
534
|
+
lastSeenAt: rawAgent.lastSeenAt ?? ts,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
392
538
|
function upsertById(list, next) {
|
|
393
539
|
const index = list.findIndex((item) => item.id === next.id);
|
|
394
540
|
if (index === -1) {
|
|
@@ -411,6 +557,14 @@ function upsertPlanPatch(state, next) {
|
|
|
411
557
|
upsertById(state.planPatches, next);
|
|
412
558
|
}
|
|
413
559
|
|
|
560
|
+
function appendCreatedTask(state, rawTask) {
|
|
561
|
+
if (!rawTask?.id) return;
|
|
562
|
+
state.plan ??= [];
|
|
563
|
+
const existing = state.plan.find((task) => String(task.id ?? task.step) === String(rawTask.id));
|
|
564
|
+
if (existing) return;
|
|
565
|
+
state.plan.push(normalizePlanTask(rawTask, state.plan.length, { owner: 'orchestrator' }));
|
|
566
|
+
}
|
|
567
|
+
|
|
414
568
|
function patchIdFromEvent(event) {
|
|
415
569
|
return event.payload?.id ?? event.payload?.patchId ?? event.id;
|
|
416
570
|
}
|
|
@@ -462,6 +616,42 @@ function upsertApproval(state, next) {
|
|
|
462
616
|
upsertById(state.approvals, next);
|
|
463
617
|
}
|
|
464
618
|
|
|
619
|
+
function approvalProjectionId(event) {
|
|
620
|
+
return event.payload?.id
|
|
621
|
+
?? event.payload?.approvalId
|
|
622
|
+
?? event.payload?.taskId
|
|
623
|
+
?? event.payload?.itemId
|
|
624
|
+
?? event.payload?.groupId
|
|
625
|
+
?? event.payload?.runId
|
|
626
|
+
?? event.id;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function normalizeApprovalClasses(value) {
|
|
630
|
+
if (value == null) return [];
|
|
631
|
+
const values = Array.isArray(value) ? value : [value];
|
|
632
|
+
return [...new Set(values.map((item) => String(item).trim()).filter(Boolean))];
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function markCoveredApprovalsApproved(approvals, grant, ts) {
|
|
636
|
+
const classes = normalizeApprovalClasses(grant.approvalClasses);
|
|
637
|
+
for (const approval of approvals) {
|
|
638
|
+
if (approval.status !== 'pending_approval') continue;
|
|
639
|
+
if (grant.runId != null && approval.runId != null && String(grant.runId) !== String(approval.runId)) continue;
|
|
640
|
+
if (grant.workspaceId != null && approval.workspaceId != null && String(grant.workspaceId) !== String(approval.workspaceId)) continue;
|
|
641
|
+
if (grant.planRevision != null && approval.planRevision != null && Number(grant.planRevision) !== Number(approval.planRevision)) continue;
|
|
642
|
+
const approvalClasses = normalizeApprovalClasses(approval.approvalClasses);
|
|
643
|
+
if (classes.length > 0 && approvalClasses.length > 0 && !approvalClasses.some((item) => classes.includes(item))) continue;
|
|
644
|
+
if (grant.scope === 'group' && String(grant.groupId ?? '') !== String(approval.groupId ?? '')) continue;
|
|
645
|
+
if (grant.scope === 'task' || grant.scope === 'tool') {
|
|
646
|
+
const grantIds = new Set([grant.taskId, grant.itemId, grant.approvalId, grant.id].filter(Boolean).map(String));
|
|
647
|
+
const approvalIds = [approval.taskId, approval.itemId, approval.approvalId, approval.id].filter(Boolean).map(String);
|
|
648
|
+
if (!approvalIds.some((id) => grantIds.has(id))) continue;
|
|
649
|
+
}
|
|
650
|
+
approval.status = 'approved';
|
|
651
|
+
approval.approvedAt = ts;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
465
655
|
function finishPendingPlanSteps(plan) {
|
|
466
656
|
for (const step of plan ?? []) {
|
|
467
657
|
if (step.status === 'running' || step.status === 'pending') {
|
|
@@ -524,30 +714,56 @@ function normalizePlan(steps, payload = {}) {
|
|
|
524
714
|
if (!Array.isArray(steps)) return null;
|
|
525
715
|
const owner = payload.owner ?? 'orchestrator';
|
|
526
716
|
const ownerActivityKey = payload.ownerActivityKey ?? payload.activityKey ?? null;
|
|
527
|
-
const plan = steps.map((raw, i) => {
|
|
528
|
-
const item = typeof raw === 'string' ? { description: raw } : (raw ?? {});
|
|
529
|
-
return {
|
|
530
|
-
step: Number(item.step ?? i + 1),
|
|
531
|
-
id: item.id ?? null,
|
|
532
|
-
description: String(item.description ?? item.label ?? item.name ?? `Step ${i + 1}`),
|
|
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,
|
|
543
|
-
owner: item.owner ?? owner,
|
|
544
|
-
ownerActivityKey: item.ownerActivityKey ?? ownerActivityKey,
|
|
545
|
-
_activityKey: item._activityKey ?? payload.activityKey ?? null,
|
|
546
|
-
};
|
|
547
|
-
});
|
|
717
|
+
const plan = steps.map((raw, i) => normalizePlanTask(raw, i, { owner, ownerActivityKey, activityKey: payload.activityKey ?? null }));
|
|
548
718
|
return validateContractInDev('plan', plan);
|
|
549
719
|
}
|
|
550
720
|
|
|
721
|
+
function normalizePlanTask(raw, index, { owner = 'orchestrator', ownerActivityKey = null, activityKey = null } = {}) {
|
|
722
|
+
const item = typeof raw === 'string' ? { description: raw } : (raw ?? {});
|
|
723
|
+
const task = {
|
|
724
|
+
step: Number(item.step ?? index + 1),
|
|
725
|
+
id: item.id ?? null,
|
|
726
|
+
label: item.label ?? item.description ?? item.name ?? `Step ${index + 1}`,
|
|
727
|
+
description: String(item.description ?? item.label ?? item.name ?? `Step ${index + 1}`),
|
|
728
|
+
status: item.status ?? 'pending',
|
|
729
|
+
dependsOn: Array.isArray(item.dependsOn) ? item.dependsOn.map(String) : [],
|
|
730
|
+
requiredCapability: item.requiredCapability ?? null,
|
|
731
|
+
operation: item.operation ?? null,
|
|
732
|
+
arguments: item.arguments && typeof item.arguments === 'object' && !Array.isArray(item.arguments) ? { ...item.arguments } : {},
|
|
733
|
+
groupId: item.groupId ?? null,
|
|
734
|
+
dependsOnGroup: item.dependsOnGroup ?? null,
|
|
735
|
+
barrier: item.barrier === true,
|
|
736
|
+
parallelizable: item.parallelizable === true,
|
|
737
|
+
recommendedConcurrency: item.recommendedConcurrency,
|
|
738
|
+
inputRefs: Array.isArray(item.inputRefs) ? item.inputRefs.map(cloneRef) : [],
|
|
739
|
+
expectedOutputRefs: Array.isArray(item.expectedOutputRefs) ? item.expectedOutputRefs.map(cloneRef) : [],
|
|
740
|
+
executor: item.executor ?? null,
|
|
741
|
+
executorQuery: item.executorQuery ?? null,
|
|
742
|
+
outputRefs: Array.isArray(item.outputRefs) ? item.outputRefs.map(String) : [],
|
|
743
|
+
locks: Array.isArray(item.locks) ? item.locks.map(String) : [],
|
|
744
|
+
requiresApproval: item.requiresApproval === true,
|
|
745
|
+
approvalClass: item.approvalClass ?? null,
|
|
746
|
+
approvalSummary: item.approvalSummary ?? null,
|
|
747
|
+
idempotencyKey: item.idempotencyKey ?? null,
|
|
748
|
+
progressWeight: Number.isFinite(Number(item.progressWeight)) ? Number(item.progressWeight) : 1,
|
|
749
|
+
priority: item.priority,
|
|
750
|
+
retryPolicy: item.retryPolicy ? cloneJson(item.retryPolicy) : undefined,
|
|
751
|
+
retryState: item.retryState ? cloneJson(item.retryState) : undefined,
|
|
752
|
+
retryAssignment: item.retryAssignment ? cloneJson(item.retryAssignment) : undefined,
|
|
753
|
+
writeLocks: Array.isArray(item.writeLocks) ? item.writeLocks.map(String) : item.writeLocks ?? null,
|
|
754
|
+
deliverableWrites: Array.isArray(item.deliverableWrites) ? item.deliverableWrites.map(String) : [],
|
|
755
|
+
wikiPageWrites: Array.isArray(item.wikiPageWrites) ? item.wikiPageWrites.map(String) : [],
|
|
756
|
+
workspaceWrite: item.workspaceWrite === true,
|
|
757
|
+
owner: item.owner ?? owner,
|
|
758
|
+
ownerActivityKey: item.ownerActivityKey ?? ownerActivityKey,
|
|
759
|
+
_activityKey: item._activityKey ?? activityKey,
|
|
760
|
+
};
|
|
761
|
+
for (const key of ['recommendedConcurrency', 'priority', 'retryPolicy']) {
|
|
762
|
+
if (task[key] === undefined) delete task[key];
|
|
763
|
+
}
|
|
764
|
+
return task;
|
|
765
|
+
}
|
|
766
|
+
|
|
551
767
|
function updatePlanStep(plan, payload) {
|
|
552
768
|
if (!plan) return;
|
|
553
769
|
const requestedTaskId = payload.taskId ?? payload.id ?? payload.targetTaskId;
|
|
@@ -557,12 +773,33 @@ function updatePlanStep(plan, payload) {
|
|
|
557
773
|
if (!step) return;
|
|
558
774
|
if (payload.status === 'failed') step.status = 'failed';
|
|
559
775
|
else if (payload.status === 'running') step.status = 'running';
|
|
776
|
+
else if (payload.status === 'pending') step.status = 'pending';
|
|
777
|
+
else if (payload.status === 'pending_approval') step.status = 'pending_approval';
|
|
778
|
+
else if (payload.status === 'waiting_approval') step.status = 'waiting_approval';
|
|
560
779
|
else if (payload.status === 'cancelled') step.status = 'cancelled';
|
|
561
780
|
else step.status = 'done';
|
|
562
781
|
if (payload.activityKey) step.activityKey = payload.activityKey;
|
|
782
|
+
if (Array.isArray(payload.outputRefs)) step.outputRefs = payload.outputRefs.map(cloneRef);
|
|
783
|
+
if (payload.result) step.result = cloneJson(payload.result);
|
|
784
|
+
if (payload.retryState) step.retryState = cloneJson(payload.retryState);
|
|
785
|
+
if (payload.retryAssignment) step.retryAssignment = cloneJson(payload.retryAssignment);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function formatPlanErrors(errors) {
|
|
789
|
+
return Array.isArray(errors) && errors.length > 0
|
|
790
|
+
? errors.map((error) => error.code ?? error.message ?? String(error)).join(', ')
|
|
791
|
+
: 'unknown';
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function cloneRef(value) {
|
|
795
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : String(value);
|
|
563
796
|
}
|
|
564
797
|
|
|
565
798
|
function sortedActivities(activities) {
|
|
566
799
|
return Object.values(activities ?? {})
|
|
567
800
|
.sort((a, b) => String(a.updatedAt ?? '').localeCompare(String(b.updatedAt ?? '')));
|
|
568
801
|
}
|
|
802
|
+
|
|
803
|
+
function cloneJson(value) {
|
|
804
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
805
|
+
}
|
|
@@ -214,6 +214,59 @@ test('reduceAgentEvents: approvals move from pending to approved', () => {
|
|
|
214
214
|
assert.deepEqual(projection.approvals[0].plan, ['Build']);
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
+
test('reduceAgentEvents: bounded approval grant covers matching pending requests only', () => {
|
|
218
|
+
const projection = reduceAgentEvents([
|
|
219
|
+
createAgentEvent('approval.requested', {
|
|
220
|
+
origin: 'orchestrator',
|
|
221
|
+
runId: 'run-1',
|
|
222
|
+
workspace: 'docs',
|
|
223
|
+
taskId: 'run-1:a',
|
|
224
|
+
payload: {
|
|
225
|
+
id: 'approval-a',
|
|
226
|
+
scope: 'task',
|
|
227
|
+
runId: 'run-1',
|
|
228
|
+
workspaceId: 'docs',
|
|
229
|
+
planRevision: 2,
|
|
230
|
+
taskId: 'run-1:a',
|
|
231
|
+
approvalClasses: ['workspace-write'],
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
createAgentEvent('approval.requested', {
|
|
235
|
+
origin: 'orchestrator',
|
|
236
|
+
runId: 'run-1',
|
|
237
|
+
workspace: 'docs',
|
|
238
|
+
taskId: 'run-1:b',
|
|
239
|
+
payload: {
|
|
240
|
+
id: 'approval-b',
|
|
241
|
+
scope: 'task',
|
|
242
|
+
runId: 'run-1',
|
|
243
|
+
workspaceId: 'docs',
|
|
244
|
+
planRevision: 2,
|
|
245
|
+
taskId: 'run-1:b',
|
|
246
|
+
approvalClasses: ['publish'],
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
249
|
+
createAgentEvent('approval.granted', {
|
|
250
|
+
origin: 'runtime',
|
|
251
|
+
runId: 'run-1',
|
|
252
|
+
workspace: 'docs',
|
|
253
|
+
payload: {
|
|
254
|
+
id: 'grant-1',
|
|
255
|
+
scope: 'run',
|
|
256
|
+
runId: 'run-1',
|
|
257
|
+
workspaceId: 'docs',
|
|
258
|
+
planRevision: 2,
|
|
259
|
+
approvalClasses: ['workspace-write'],
|
|
260
|
+
},
|
|
261
|
+
}),
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
const byId = Object.fromEntries(projection.approvals.map((approval) => [approval.id, approval]));
|
|
265
|
+
assert.equal(byId['approval-a'].status, 'approved');
|
|
266
|
+
assert.equal(byId['approval-b'].status, 'pending_approval');
|
|
267
|
+
assert.equal(byId['grant-1'].status, 'approved');
|
|
268
|
+
});
|
|
269
|
+
|
|
217
270
|
test('reduceAgentEvents: control queue is event sourced and follows run status', () => {
|
|
218
271
|
const projection = reduceAgentEvents([
|
|
219
272
|
createAgentEvent('control_enqueued', {
|
package/src/core/compose.js
CHANGED
|
@@ -4,7 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import YAML from 'yaml';
|
|
6
6
|
import { cacertEnv, ensureCacertComposeOverride } from './cacert.js';
|
|
7
|
-
import { managerEnvFile, readEnvFile } from './env.js';
|
|
7
|
+
import { managerEnvFile, readEnvFile, resolveAgentsDataDir } from './env.js';
|
|
8
8
|
import { managerRoot } from './workspaces.js';
|
|
9
9
|
|
|
10
10
|
const execFileAsync = promisify(execFile);
|
|
@@ -106,11 +106,16 @@ function composeBaseArgs(session) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
function composeEnv(session) {
|
|
109
|
+
const uid = typeof process.getuid === 'function' ? String(process.getuid()) : (process.env.UID ?? '1000');
|
|
110
|
+
const gid = typeof process.getgid === 'function' ? String(process.getgid()) : (process.env.GID ?? '1000');
|
|
109
111
|
return {
|
|
110
112
|
...process.env,
|
|
111
113
|
...readManagerEnv(),
|
|
112
114
|
...(session.workspaceEnv ?? {}),
|
|
113
115
|
...cacertEnv(),
|
|
116
|
+
UID: uid,
|
|
117
|
+
GID: gid,
|
|
118
|
+
AGENTS_DATA_DIR: resolveAgentsDataDir(session),
|
|
114
119
|
WORKSPACE_NAME: session.workspace,
|
|
115
120
|
WIKI_WORKSPACE_PATH: session.workspacePath,
|
|
116
121
|
...(session.wikirc?.fileName && { WIKI_CONFIG_PATH: session.wikirc.fileName }),
|
|
@@ -12,3 +12,15 @@ test('workspace compose does not start a per-workspace agent runtime', async ()
|
|
|
12
12
|
assert.deepEqual(aliases.all.targets, ['serve', 'mcp-http', 'production-mcp']);
|
|
13
13
|
assert.equal(aliases.runtime, undefined);
|
|
14
14
|
});
|
|
15
|
+
|
|
16
|
+
test('agent compose services run as the host uid and gid', async () => {
|
|
17
|
+
const workspaceRaw = await readFile(new URL('../../docker-compose.yml', import.meta.url), 'utf8');
|
|
18
|
+
const workspaceCompose = YAML.parse(workspaceRaw);
|
|
19
|
+
assert.equal(workspaceCompose.services['production-mcp'].user, '${UID:-1000}:${GID:-1000}');
|
|
20
|
+
|
|
21
|
+
const agentsRaw = await readFile(new URL('../../agents.docker-compose.yml', import.meta.url), 'utf8');
|
|
22
|
+
const agentsCompose = YAML.parse(agentsRaw);
|
|
23
|
+
assert.equal(agentsCompose.services.cme.user, '${UID:-1000}:${GID:-1000}');
|
|
24
|
+
assert.equal(agentsCompose.services.documents.user, '${UID:-1000}:${GID:-1000}');
|
|
25
|
+
assert.equal(agentsCompose.services.mailer.user, '${UID:-1000}:${GID:-1000}');
|
|
26
|
+
});
|