@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/runtime/server.js
CHANGED
|
@@ -85,6 +85,35 @@ export function startRuntimeServer({
|
|
|
85
85
|
});
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
88
|
+
if (request.method === 'GET') {
|
|
89
|
+
const taskRead = readTaskEndpoint(url);
|
|
90
|
+
if (taskRead) {
|
|
91
|
+
if (taskRead.kind === 'run_tasks') {
|
|
92
|
+
sendJson(response, 200, {
|
|
93
|
+
ok: true,
|
|
94
|
+
runId: taskRead.runId,
|
|
95
|
+
tasks: typeof store.listTasks === 'function' ? store.listTasks({ runId: taskRead.runId }) : [],
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (taskRead.kind === 'task_attempts') {
|
|
100
|
+
sendJson(response, 200, {
|
|
101
|
+
ok: true,
|
|
102
|
+
taskId: taskRead.taskId,
|
|
103
|
+
attempts: typeof store.listTaskAttempts === 'function' ? store.listTaskAttempts({ taskId: taskRead.taskId }) : [],
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (taskRead.kind === 'task_result') {
|
|
108
|
+
sendJson(response, 200, {
|
|
109
|
+
ok: true,
|
|
110
|
+
taskId: taskRead.taskId,
|
|
111
|
+
result: typeof store.getTaskResult === 'function' ? store.getTaskResult({ taskId: taskRead.taskId }) : null,
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
88
117
|
if (request.method === 'GET' && url.pathname === '/control') {
|
|
89
118
|
const workspace = workspaceFromUrl(url);
|
|
90
119
|
const context = await resolveContext({ workspace });
|
|
@@ -112,9 +141,11 @@ export function startRuntimeServer({
|
|
|
112
141
|
return;
|
|
113
142
|
}
|
|
114
143
|
validateContractInDev('controlMessage', { ...body, action, input });
|
|
115
|
-
const result = handleControlMessage(context, store, input, {
|
|
144
|
+
const result = await handleControlMessage(context, store, input, {
|
|
116
145
|
intent: body.intent,
|
|
117
146
|
startNextControlRequest,
|
|
147
|
+
cancel,
|
|
148
|
+
approve,
|
|
118
149
|
});
|
|
119
150
|
sendJson(response, result.statusCode, result.body);
|
|
120
151
|
return;
|
|
@@ -202,10 +233,6 @@ export function startRuntimeServer({
|
|
|
202
233
|
}
|
|
203
234
|
if (request.method === 'POST' && url.pathname === '/run') {
|
|
204
235
|
const { body, context } = await resolveBodyContext(request, url);
|
|
205
|
-
if (context.running) {
|
|
206
|
-
sendJson(response, 409, { error: 'A runtime run is already active.' });
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
236
|
try {
|
|
210
237
|
const input = String(body.input ?? body.prompt ?? '').trim();
|
|
211
238
|
if (!input) {
|
|
@@ -213,6 +240,18 @@ export function startRuntimeServer({
|
|
|
213
240
|
return;
|
|
214
241
|
}
|
|
215
242
|
validateContractInDev('runRequest', { ...body, input });
|
|
243
|
+
if (context.running) {
|
|
244
|
+
const item = enqueueControlRequest(context, input);
|
|
245
|
+
sendJson(response, 202, {
|
|
246
|
+
accepted: true,
|
|
247
|
+
queued: true,
|
|
248
|
+
kind: 'enqueue_run',
|
|
249
|
+
item,
|
|
250
|
+
...controlStatus(context, store),
|
|
251
|
+
explanation: 'Request queued for a future run.',
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
216
255
|
const accepted = startRuntimeRun(context, body);
|
|
217
256
|
sendJson(response, 202, accepted);
|
|
218
257
|
} catch (err) {
|
|
@@ -262,9 +301,15 @@ export function startRuntimeServer({
|
|
|
262
301
|
const workspace = workspaceFromBody(body) ?? workspaceFromUrl(url);
|
|
263
302
|
const result = await approve?.({
|
|
264
303
|
workspace,
|
|
304
|
+
workspaceId: workspace,
|
|
265
305
|
runId: url.searchParams.get('runId') ?? body.runId ?? null,
|
|
266
306
|
itemId: url.searchParams.get('itemId') ?? body.itemId ?? null,
|
|
267
307
|
approvalId: url.searchParams.get('approvalId') ?? body.approvalId ?? null,
|
|
308
|
+
scope: url.searchParams.get('scope') ?? body.scope ?? null,
|
|
309
|
+
taskId: url.searchParams.get('taskId') ?? body.taskId ?? null,
|
|
310
|
+
groupId: url.searchParams.get('groupId') ?? body.groupId ?? null,
|
|
311
|
+
planRevision: readOptionalNumber(url.searchParams.get('planRevision') ?? body.planRevision),
|
|
312
|
+
approvalClasses: readOptionalList(body.approvalClasses ?? body.approvalClass ?? url.searchParams.get('approvalClass')),
|
|
268
313
|
});
|
|
269
314
|
sendJson(response, result?.approved ? 202 : 404, result ?? { approved: false });
|
|
270
315
|
return;
|
|
@@ -363,6 +408,20 @@ function workspaceFromBody(body) {
|
|
|
363
408
|
return workspace == null ? null : String(workspace).trim() || null;
|
|
364
409
|
}
|
|
365
410
|
|
|
411
|
+
function readTaskEndpoint(url) {
|
|
412
|
+
const parts = url.pathname.split('/').filter(Boolean).map((part) => decodeURIComponent(part));
|
|
413
|
+
if (parts.length === 3 && parts[0] === 'runs' && parts[2] === 'tasks') {
|
|
414
|
+
return { kind: 'run_tasks', runId: parts[1] };
|
|
415
|
+
}
|
|
416
|
+
if (parts.length === 3 && parts[0] === 'tasks' && parts[2] === 'attempts') {
|
|
417
|
+
return { kind: 'task_attempts', taskId: parts[1] };
|
|
418
|
+
}
|
|
419
|
+
if (parts.length === 3 && parts[0] === 'tasks' && parts[2] === 'result') {
|
|
420
|
+
return { kind: 'task_result', taskId: parts[1] };
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
|
|
366
425
|
function controlStatus(context, store) {
|
|
367
426
|
const workspace = context?.workspace ?? context?.session?.workspace ?? null;
|
|
368
427
|
const state = runtimeState(context, store, { workspace });
|
|
@@ -419,19 +478,47 @@ function readOnlyControlResponse(kind, classification, status, explanation, { ac
|
|
|
419
478
|
};
|
|
420
479
|
}
|
|
421
480
|
|
|
422
|
-
function
|
|
481
|
+
function approvalRequestFromStatus(status) {
|
|
482
|
+
const runId = status.runId ?? status.runs?.find((run) => run.status === 'running' || run.status === 'pending_approval')?.id ?? null;
|
|
483
|
+
const pending = (status.approvals ?? []).filter((approval) => approval.status === 'pending_approval');
|
|
484
|
+
const classes = [...new Set(pending.flatMap((approval) => readOptionalList(approval.approvalClasses ?? approval.approvalClass)))];
|
|
485
|
+
return {
|
|
486
|
+
workspace: status.workspace ?? null,
|
|
487
|
+
workspaceId: status.workspace ?? null,
|
|
488
|
+
runId,
|
|
489
|
+
scope: 'run',
|
|
490
|
+
planRevision: status.planRevision ?? null,
|
|
491
|
+
approvalClasses: classes.length > 0 ? classes : ['default'],
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function handleControlMessage(context, store, input, { intent = null, startNextControlRequest = () => false, cancel = null, approve = null } = {}) {
|
|
423
496
|
const status = controlStatus(context, store);
|
|
424
497
|
const classification = classifyControlMessage(input, status, intent);
|
|
425
498
|
if (classification.kind === 'observe') {
|
|
426
499
|
return readOnlyControlResponse('observe', classification, status, explainControlState(status));
|
|
427
500
|
}
|
|
428
|
-
if (classification.kind === '
|
|
501
|
+
if (classification.kind === 'cancel') {
|
|
502
|
+
if (context.running && context.currentAbortController) {
|
|
503
|
+
context.currentAbortController.abort();
|
|
504
|
+
await cancel?.(context);
|
|
505
|
+
return readOnlyControlResponse('cancel', classification, controlStatus(context, store), 'Runtime cancellation requested.', { accepted: true });
|
|
506
|
+
}
|
|
507
|
+
return readOnlyControlResponse('cancel', classification, status, 'No active run to cancel.', { accepted: false });
|
|
508
|
+
}
|
|
509
|
+
if (classification.kind === 'approve') {
|
|
510
|
+
const result = await approve?.(approvalRequestFromStatus(status));
|
|
511
|
+
return readOnlyControlResponse('approve', classification, controlStatus(context, store), result?.approved
|
|
512
|
+
? 'Approval grant recorded for the current run revision.'
|
|
513
|
+
: 'Approval intent recorded; no pending approval was available to grant.', { accepted: result?.approved === true, extra: { approval: result ?? null } });
|
|
514
|
+
}
|
|
515
|
+
if (classification.kind === 'modify_run') {
|
|
429
516
|
const proposal = storeControlProposal(context, input, classification, status);
|
|
430
517
|
return {
|
|
431
518
|
statusCode: 202,
|
|
432
519
|
body: {
|
|
433
520
|
accepted: true,
|
|
434
|
-
kind: '
|
|
521
|
+
kind: 'modify_run',
|
|
435
522
|
classification,
|
|
436
523
|
proposal,
|
|
437
524
|
...controlStatus(context, store),
|
|
@@ -439,9 +526,9 @@ function handleControlMessage(context, store, input, { intent = null, startNextC
|
|
|
439
526
|
},
|
|
440
527
|
};
|
|
441
528
|
}
|
|
442
|
-
if (classification.kind === '
|
|
529
|
+
if (classification.kind === 'enqueue_run') {
|
|
443
530
|
const item = enqueueControlRequest(context, input);
|
|
444
|
-
// Unlike `
|
|
531
|
+
// Unlike `modify_run`, this may synchronously start a queued run (see
|
|
445
532
|
// startNextControlRequest), which can change running/plan/status — a full
|
|
446
533
|
// controlStatus() recompute is required here, not just controlQueue.
|
|
447
534
|
void startNextControlRequest(context);
|
|
@@ -449,10 +536,11 @@ function handleControlMessage(context, store, input, { intent = null, startNextC
|
|
|
449
536
|
statusCode: 202,
|
|
450
537
|
body: {
|
|
451
538
|
accepted: true,
|
|
452
|
-
kind: '
|
|
539
|
+
kind: 'enqueue_run',
|
|
453
540
|
classification,
|
|
454
541
|
item,
|
|
455
542
|
...controlStatus(context, store),
|
|
543
|
+
explanation: 'Request queued for a future run.',
|
|
456
544
|
},
|
|
457
545
|
};
|
|
458
546
|
}
|
|
@@ -462,8 +550,8 @@ function handleControlMessage(context, store, input, { intent = null, startNextC
|
|
|
462
550
|
extra: {
|
|
463
551
|
choices: [
|
|
464
552
|
{ action: 'message', intent: 'observe', label: 'Ask about this run' },
|
|
465
|
-
{ action: 'message', intent: '
|
|
466
|
-
{ action: 'enqueue', intent: '
|
|
553
|
+
{ action: 'message', intent: 'modify_run', label: 'Propose a change to this run' },
|
|
554
|
+
{ action: 'enqueue', intent: 'enqueue_run', label: 'Queue as a future run' },
|
|
467
555
|
],
|
|
468
556
|
},
|
|
469
557
|
});
|
|
@@ -510,7 +598,7 @@ function storeControlProposal(context, input, classification, status) {
|
|
|
510
598
|
origin: 'runtime',
|
|
511
599
|
runId: context.currentRunId ?? status.runId ?? null,
|
|
512
600
|
workspace: proposal.workspace,
|
|
513
|
-
payload: { input, intent: '
|
|
601
|
+
payload: { input, intent: 'modify_run', classification },
|
|
514
602
|
}));
|
|
515
603
|
dispatchAgentEvent(context.session, createAgentEvent('plan_patch_proposed', {
|
|
516
604
|
origin: 'runtime',
|
|
@@ -636,17 +724,34 @@ function classifyControlMessage(input, status, forcedIntent = null) {
|
|
|
636
724
|
// Caller (the /control message route) already trims and rejects empty input.
|
|
637
725
|
const lower = String(input ?? '').toLowerCase();
|
|
638
726
|
const intent = forcedIntent ? String(forcedIntent).toLowerCase() : null;
|
|
639
|
-
|
|
640
|
-
|
|
727
|
+
const explicit = {
|
|
728
|
+
observe: 'observe',
|
|
729
|
+
converse: 'converse',
|
|
730
|
+
approve: 'approve',
|
|
731
|
+
modify_run: 'modify_run',
|
|
732
|
+
mutate: 'modify_run',
|
|
733
|
+
enqueue_run: 'enqueue_run',
|
|
734
|
+
enqueue: 'enqueue_run',
|
|
735
|
+
cancel: 'cancel',
|
|
736
|
+
ambiguous: 'ambiguous',
|
|
737
|
+
}[intent];
|
|
738
|
+
if (explicit) {
|
|
739
|
+
return { kind: explicit, confidence: 1, reason: 'explicit_intent' };
|
|
740
|
+
}
|
|
741
|
+
if (/\b(valide tout|approve all|approve|approuve|valid[eé]|ok pour tout|go pour tout)\b/i.test(lower)) {
|
|
742
|
+
return { kind: 'approve', confidence: 0.86, reason: 'approval_request' };
|
|
743
|
+
}
|
|
744
|
+
if (/\b(cancel|annule|stop|arr[eê]te|interromps|abort)\b/i.test(lower)) {
|
|
745
|
+
return { kind: 'cancel', confidence: 0.86, reason: 'cancel_request' };
|
|
746
|
+
}
|
|
747
|
+
if (/\b(plus tard|later|ensuite|apr[eè]s ce run|enqueue|mets en file|met en file|futur|next run|future run)\b/i.test(lower)) {
|
|
748
|
+
return { kind: 'enqueue_run', confidence: 0.8, reason: 'future_run_request' };
|
|
641
749
|
}
|
|
642
750
|
if (/\b(o[uù] en es[t-]|status|statut|progress|progression|build|run|job|queue|file|logs?|explique|explain|inspect|show|montre|quoi de neuf)\b/i.test(lower)) {
|
|
643
751
|
return { kind: 'observe', confidence: 0.86, reason: 'status_or_explanation_request' };
|
|
644
752
|
}
|
|
645
753
|
if (status.running && /\b(ajoute|add|change|modifie|modify|remplace|replace|retire|remove|skip|ignore|apr[eè]s|before|after|chaque|each|plan|step|t[aâ]che)\b/i.test(lower)) {
|
|
646
|
-
return { kind: '
|
|
647
|
-
}
|
|
648
|
-
if (/\b(plus tard|later|ensuite|apr[eè]s ce run|apr[eè]s|enqueue|queue|mets en file|met en file|futur|next run|future run)\b/i.test(lower)) {
|
|
649
|
-
return { kind: 'enqueue', confidence: 0.8, reason: 'future_run_request' };
|
|
754
|
+
return { kind: 'modify_run', confidence: 0.78, reason: 'active_run_change_request' };
|
|
650
755
|
}
|
|
651
756
|
if (status.running && /\b(lance|run|g[eé]n[eè]re|build|export|cr[eé]e|create|send|envoie|ingest|convert|importe|import)\b/i.test(lower)) {
|
|
652
757
|
return { kind: 'ambiguous', confidence: 0.45, reason: 'active_run_action_is_ambiguous' };
|
|
@@ -688,6 +793,18 @@ function readRequiredPatchId(body, response) {
|
|
|
688
793
|
return patchId;
|
|
689
794
|
}
|
|
690
795
|
|
|
796
|
+
function readOptionalNumber(value) {
|
|
797
|
+
if (value == null || value === '') return null;
|
|
798
|
+
const number = Number(value);
|
|
799
|
+
return Number.isFinite(number) ? number : null;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function readOptionalList(value) {
|
|
803
|
+
if (value == null || value === '') return [];
|
|
804
|
+
if (Array.isArray(value)) return value.map(String).map((item) => item.trim()).filter(Boolean);
|
|
805
|
+
return String(value).split(',').map((item) => item.trim()).filter(Boolean);
|
|
806
|
+
}
|
|
807
|
+
|
|
691
808
|
function readJson(request) {
|
|
692
809
|
return new Promise((resolve, reject) => {
|
|
693
810
|
let raw = '';
|
|
@@ -128,6 +128,64 @@ test('runtime server shutdown endpoint closes the listener', async (t) => {
|
|
|
128
128
|
}));
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
+
test('runtime server exposes task assignment attempt and result read endpoints', async (t) => {
|
|
132
|
+
const taskId = 'run-1:task-a';
|
|
133
|
+
let handle;
|
|
134
|
+
try {
|
|
135
|
+
handle = await startRuntimeServer({
|
|
136
|
+
host: '127.0.0.1',
|
|
137
|
+
port: 0,
|
|
138
|
+
token: 'runtime-secret',
|
|
139
|
+
store: {
|
|
140
|
+
dbPath: ':memory:',
|
|
141
|
+
getState: () => ({ status: 'idle' }),
|
|
142
|
+
listEvents: () => [],
|
|
143
|
+
listTasks: ({ runId }) => runId === 'run-1' ? [{ id: taskId, runId, status: 'done' }] : [],
|
|
144
|
+
listTaskAttempts: ({ taskId: requested }) => requested === taskId ? [{
|
|
145
|
+
attemptId: 'attempt-1',
|
|
146
|
+
taskId,
|
|
147
|
+
runId: 'run-1',
|
|
148
|
+
status: 'done',
|
|
149
|
+
jobId: 'job-1',
|
|
150
|
+
}] : [],
|
|
151
|
+
getTaskResult: ({ taskId: requested }) => requested === taskId ? {
|
|
152
|
+
attemptId: 'attempt-1',
|
|
153
|
+
taskId,
|
|
154
|
+
status: 'succeeded',
|
|
155
|
+
outputRefs: [{ type: 'file', ref: 'deliverables/a.md' }],
|
|
156
|
+
metrics: { durationMs: 7 },
|
|
157
|
+
} : null,
|
|
158
|
+
},
|
|
159
|
+
session: {},
|
|
160
|
+
run: async () => {},
|
|
161
|
+
});
|
|
162
|
+
} catch (err) {
|
|
163
|
+
if (err?.code === 'EPERM') {
|
|
164
|
+
t.skip('network listen is not permitted in this sandbox');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const headers = { authorization: 'Bearer runtime-secret' };
|
|
172
|
+
const tasksResponse = await fetch(`http://127.0.0.1:${handle.port}/runs/run-1/tasks`, { headers });
|
|
173
|
+
assert.equal(tasksResponse.status, 200);
|
|
174
|
+
assert.deepEqual((await tasksResponse.json()).tasks, [{ id: taskId, runId: 'run-1', status: 'done' }]);
|
|
175
|
+
|
|
176
|
+
const encodedTaskId = encodeURIComponent(taskId);
|
|
177
|
+
const attemptsResponse = await fetch(`http://127.0.0.1:${handle.port}/tasks/${encodedTaskId}/attempts`, { headers });
|
|
178
|
+
assert.equal(attemptsResponse.status, 200);
|
|
179
|
+
assert.equal((await attemptsResponse.json()).attempts[0].attemptId, 'attempt-1');
|
|
180
|
+
|
|
181
|
+
const resultResponse = await fetch(`http://127.0.0.1:${handle.port}/tasks/${encodedTaskId}/result`, { headers });
|
|
182
|
+
assert.equal(resultResponse.status, 200);
|
|
183
|
+
assert.equal((await resultResponse.json()).result.metrics.durationMs, 7);
|
|
184
|
+
} finally {
|
|
185
|
+
await handle.close();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
131
189
|
test('runtime server accepts only one active run', async (t) => {
|
|
132
190
|
let releaseRun;
|
|
133
191
|
let runCount = 0;
|
|
@@ -157,7 +215,6 @@ test('runtime server accepts only one active run', async (t) => {
|
|
|
157
215
|
|
|
158
216
|
try {
|
|
159
217
|
const url = `http://127.0.0.1:${handle.port}/run`;
|
|
160
|
-
let acceptedRun = null;
|
|
161
218
|
const [first, second] = await Promise.all([
|
|
162
219
|
fetch(url, {
|
|
163
220
|
method: 'POST',
|
|
@@ -171,9 +228,11 @@ test('runtime server accepts only one active run', async (t) => {
|
|
|
171
228
|
}),
|
|
172
229
|
]);
|
|
173
230
|
|
|
174
|
-
assert.deepEqual([first.status, second.status]
|
|
175
|
-
const
|
|
176
|
-
acceptedRun =
|
|
231
|
+
assert.deepEqual([first.status, second.status], [202, 202]);
|
|
232
|
+
const bodies = [await first.json(), await second.json()];
|
|
233
|
+
const acceptedRun = bodies.find((body) => body.accepted && !body.queued);
|
|
234
|
+
const queuedRun = bodies.find((body) => body.queued);
|
|
235
|
+
assert.equal(queuedRun.kind, 'enqueue_run');
|
|
177
236
|
assert.equal(acceptedRun.accepted, true);
|
|
178
237
|
assert.match(acceptedRun.runId, /^[0-9a-f-]{36}$/);
|
|
179
238
|
assert.equal(runCount, 1);
|
|
@@ -336,12 +395,13 @@ test('runtime server isolates active runs by workspace', async (t) => {
|
|
|
336
395
|
]);
|
|
337
396
|
assert.deepEqual([juno.status, docs.status], [202, 202]);
|
|
338
397
|
|
|
339
|
-
const
|
|
398
|
+
const queued = await fetch(`${url}?workspace=juno`, {
|
|
340
399
|
method: 'POST',
|
|
341
400
|
headers: { 'Content-Type': 'application/json' },
|
|
342
401
|
body: JSON.stringify({ input: 'third' }),
|
|
343
402
|
});
|
|
344
|
-
assert.equal(
|
|
403
|
+
assert.equal(queued.status, 202);
|
|
404
|
+
assert.equal((await queued.json()).queued, true);
|
|
345
405
|
assert.deepEqual(runWorkspaces.sort(), ['docs', 'juno']);
|
|
346
406
|
} finally {
|
|
347
407
|
releases.get('juno')?.();
|
|
@@ -521,9 +581,15 @@ test('runtime server exposes approval endpoint', async (t) => {
|
|
|
521
581
|
assert.equal(response.status, 202);
|
|
522
582
|
assert.deepEqual(approved, {
|
|
523
583
|
workspace: 'juno',
|
|
584
|
+
workspaceId: 'juno',
|
|
524
585
|
runId: 'run-1',
|
|
525
586
|
itemId: 'item-1',
|
|
526
587
|
approvalId: null,
|
|
588
|
+
scope: null,
|
|
589
|
+
taskId: null,
|
|
590
|
+
groupId: null,
|
|
591
|
+
planRevision: null,
|
|
592
|
+
approvalClasses: [],
|
|
527
593
|
});
|
|
528
594
|
assert.deepEqual(await response.json(), { approved: true, runId: 'run-1', itemId: 'item-1' });
|
|
529
595
|
} finally {
|
|
@@ -705,6 +771,70 @@ test('runtime server control message observes an active run without enqueueing',
|
|
|
705
771
|
}
|
|
706
772
|
});
|
|
707
773
|
|
|
774
|
+
test('runtime server control message handles approve and cancel intents during an active run', async (t) => {
|
|
775
|
+
const session = {
|
|
776
|
+
workspace: 'juno',
|
|
777
|
+
controlQueue: [],
|
|
778
|
+
};
|
|
779
|
+
const abortController = new AbortController();
|
|
780
|
+
let cancelled = false;
|
|
781
|
+
let handle;
|
|
782
|
+
try {
|
|
783
|
+
handle = await startRuntimeServer({
|
|
784
|
+
host: '127.0.0.1',
|
|
785
|
+
port: 0,
|
|
786
|
+
store: {
|
|
787
|
+
dbPath: ':memory:',
|
|
788
|
+
getState: () => ({
|
|
789
|
+
status: 'running',
|
|
790
|
+
plan: [{ step: 1, description: 'Build documents', status: 'running' }],
|
|
791
|
+
queue: [],
|
|
792
|
+
approvals: [],
|
|
793
|
+
summary: null,
|
|
794
|
+
}),
|
|
795
|
+
listEvents: () => [],
|
|
796
|
+
},
|
|
797
|
+
getContext: async () => ({
|
|
798
|
+
workspace: 'juno',
|
|
799
|
+
session,
|
|
800
|
+
running: true,
|
|
801
|
+
currentAbortController: abortController,
|
|
802
|
+
}),
|
|
803
|
+
cancel: async () => { cancelled = true; },
|
|
804
|
+
run: async () => {},
|
|
805
|
+
});
|
|
806
|
+
} catch (err) {
|
|
807
|
+
if (err?.code === 'EPERM') {
|
|
808
|
+
t.skip('network listen is not permitted in this sandbox');
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
throw err;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
try {
|
|
815
|
+
const approveResponse = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=juno`, {
|
|
816
|
+
method: 'POST',
|
|
817
|
+
headers: { 'Content-Type': 'application/json' },
|
|
818
|
+
body: JSON.stringify({ action: 'message', input: 'valide tout' }),
|
|
819
|
+
});
|
|
820
|
+
assert.equal(approveResponse.status, 200);
|
|
821
|
+
assert.equal((await approveResponse.json()).kind, 'approve');
|
|
822
|
+
|
|
823
|
+
const cancelResponse = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=juno`, {
|
|
824
|
+
method: 'POST',
|
|
825
|
+
headers: { 'Content-Type': 'application/json' },
|
|
826
|
+
body: JSON.stringify({ action: 'message', input: 'annule le run' }),
|
|
827
|
+
});
|
|
828
|
+
assert.equal(cancelResponse.status, 200);
|
|
829
|
+
assert.equal((await cancelResponse.json()).kind, 'cancel');
|
|
830
|
+
assert.equal(cancelled, true);
|
|
831
|
+
assert.equal(abortController.signal.aborted, true);
|
|
832
|
+
assert.equal(session.controlQueue.length, 0);
|
|
833
|
+
} finally {
|
|
834
|
+
await handle.close();
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
708
838
|
test('runtime server control message records active plan mutation as a proposal', async (t) => {
|
|
709
839
|
const session = {
|
|
710
840
|
workspace: 'juno',
|
|
@@ -765,7 +895,7 @@ test('runtime server control message records active plan mutation as a proposal'
|
|
|
765
895
|
});
|
|
766
896
|
assert.equal(response.status, 202);
|
|
767
897
|
const body = await response.json();
|
|
768
|
-
assert.equal(body.kind, '
|
|
898
|
+
assert.equal(body.kind, 'modify_run');
|
|
769
899
|
assert.equal(body.proposal.status, 'proposed');
|
|
770
900
|
assert.equal(body.proposal.input, 'Ajoute un envoi après chaque génération');
|
|
771
901
|
assert.equal(session.agentProjection.planPatches[0].status, 'proposed');
|