@dotdrelle/wiki-manager 0.9.3 → 0.11.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 +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
|
@@ -1,7 +1,51 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
|
+
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
3
4
|
import { startRuntimeServer } from './server.js';
|
|
4
5
|
|
|
6
|
+
test('runtime server checks bearer and x-runtime-token credentials', async (t) => {
|
|
7
|
+
let handle;
|
|
8
|
+
try {
|
|
9
|
+
handle = await startRuntimeServer({
|
|
10
|
+
host: '127.0.0.1',
|
|
11
|
+
port: 0,
|
|
12
|
+
token: 'runtime-secret',
|
|
13
|
+
store: {
|
|
14
|
+
dbPath: ':memory:',
|
|
15
|
+
getState: () => ({ status: 'idle' }),
|
|
16
|
+
listEvents: () => [],
|
|
17
|
+
},
|
|
18
|
+
session: {},
|
|
19
|
+
run: async () => {},
|
|
20
|
+
});
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err?.code === 'EPERM') {
|
|
23
|
+
t.skip('network listen is not permitted in this sandbox');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const url = `http://127.0.0.1:${handle.port}/health`;
|
|
31
|
+
|
|
32
|
+
const missing = await fetch(url);
|
|
33
|
+
assert.equal(missing.status, 401);
|
|
34
|
+
|
|
35
|
+
const bearer = await fetch(url, {
|
|
36
|
+
headers: { authorization: 'Bearer runtime-secret' },
|
|
37
|
+
});
|
|
38
|
+
assert.equal(bearer.status, 200);
|
|
39
|
+
|
|
40
|
+
const legacy = await fetch(url, {
|
|
41
|
+
headers: { 'x-runtime-token': 'runtime-secret' },
|
|
42
|
+
});
|
|
43
|
+
assert.equal(legacy.status, 200);
|
|
44
|
+
} finally {
|
|
45
|
+
await handle.close();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
5
49
|
test('runtime server accepts only one active run', async (t) => {
|
|
6
50
|
let releaseRun;
|
|
7
51
|
let runCount = 0;
|
|
@@ -102,6 +146,58 @@ test('runtime server returns the accepted run id and passes it to the runner', a
|
|
|
102
146
|
}
|
|
103
147
|
});
|
|
104
148
|
|
|
149
|
+
test('runtime server state exposes active run identity while running', async (t) => {
|
|
150
|
+
let releaseRun;
|
|
151
|
+
const context = {
|
|
152
|
+
workspace: 'juno',
|
|
153
|
+
session: { workspace: 'juno' },
|
|
154
|
+
running: false,
|
|
155
|
+
currentAbortController: null,
|
|
156
|
+
currentRunId: null,
|
|
157
|
+
};
|
|
158
|
+
let acceptedRun;
|
|
159
|
+
let handle;
|
|
160
|
+
try {
|
|
161
|
+
handle = await startRuntimeServer({
|
|
162
|
+
host: '127.0.0.1',
|
|
163
|
+
port: 0,
|
|
164
|
+
store: {
|
|
165
|
+
dbPath: ':memory:',
|
|
166
|
+
getState: () => ({ status: 'idle', plan: [] }),
|
|
167
|
+
listEvents: () => [],
|
|
168
|
+
},
|
|
169
|
+
getContext: async () => context,
|
|
170
|
+
run: async () => {
|
|
171
|
+
await new Promise((resolve) => { releaseRun = resolve; });
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (err?.code === 'EPERM') {
|
|
176
|
+
t.skip('network listen is not permitted in this sandbox');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const runResponse = await fetch(`http://127.0.0.1:${handle.port}/run?workspace=juno`, {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: { 'Content-Type': 'application/json' },
|
|
186
|
+
body: JSON.stringify({ input: 'build' }),
|
|
187
|
+
});
|
|
188
|
+
acceptedRun = await runResponse.json();
|
|
189
|
+
const stateResponse = await fetch(`http://127.0.0.1:${handle.port}/state?workspace=juno`);
|
|
190
|
+
const state = await stateResponse.json();
|
|
191
|
+
assert.equal(state.status, 'running');
|
|
192
|
+
assert.equal(state.running, true);
|
|
193
|
+
assert.equal(state.runId, acceptedRun.runId);
|
|
194
|
+
assert.equal(state.workspace, 'juno');
|
|
195
|
+
} finally {
|
|
196
|
+
releaseRun?.();
|
|
197
|
+
await handle.close();
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
105
201
|
test('runtime server isolates active runs by workspace', async (t) => {
|
|
106
202
|
const releases = new Map();
|
|
107
203
|
const runWorkspaces = [];
|
|
@@ -222,6 +318,52 @@ test('runtime server filters state and events by workspace', async (t) => {
|
|
|
222
318
|
}
|
|
223
319
|
});
|
|
224
320
|
|
|
321
|
+
test('runtime server exposes a correlated audit trail endpoint', async (t) => {
|
|
322
|
+
let auditArgs = null;
|
|
323
|
+
let handle;
|
|
324
|
+
try {
|
|
325
|
+
handle = await startRuntimeServer({
|
|
326
|
+
host: '127.0.0.1',
|
|
327
|
+
port: 0,
|
|
328
|
+
store: {
|
|
329
|
+
dbPath: ':memory:',
|
|
330
|
+
getState: () => ({ status: 'idle' }),
|
|
331
|
+
listEvents: () => [],
|
|
332
|
+
listAuditTrail: (options) => {
|
|
333
|
+
auditArgs = options;
|
|
334
|
+
return [{ sequence: 1, type: 'tool_call_started', runId: options.runId, taskId: 'task-a' }];
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
getContext: async (workspace) => ({
|
|
338
|
+
workspace,
|
|
339
|
+
session: { workspace },
|
|
340
|
+
running: false,
|
|
341
|
+
currentAbortController: null,
|
|
342
|
+
}),
|
|
343
|
+
run: async () => {},
|
|
344
|
+
});
|
|
345
|
+
} catch (err) {
|
|
346
|
+
if (err?.code === 'EPERM') {
|
|
347
|
+
t.skip('network listen is not permitted in this sandbox');
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
throw err;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/audit?workspace=docs&runId=run-1`);
|
|
355
|
+
assert.equal(response.status, 200);
|
|
356
|
+
const body = await response.json();
|
|
357
|
+
assert.equal(body.ok, true);
|
|
358
|
+
assert.equal(body.workspace, 'docs');
|
|
359
|
+
assert.equal(body.runId, 'run-1');
|
|
360
|
+
assert.deepEqual(body.audit, [{ sequence: 1, type: 'tool_call_started', runId: 'run-1', taskId: 'task-a' }]);
|
|
361
|
+
assert.deepEqual(auditArgs, { workspace: 'docs', runId: 'run-1' });
|
|
362
|
+
} finally {
|
|
363
|
+
await handle.close();
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
225
367
|
test('runtime server exposes manual resume endpoint', async (t) => {
|
|
226
368
|
let resumedWorkspace = null;
|
|
227
369
|
let handle;
|
|
@@ -426,6 +568,203 @@ test('runtime server control enqueue emits events but does not patch an active p
|
|
|
426
568
|
}
|
|
427
569
|
});
|
|
428
570
|
|
|
571
|
+
test('runtime server control message observes an active run without enqueueing', async (t) => {
|
|
572
|
+
const session = {
|
|
573
|
+
workspace: 'juno',
|
|
574
|
+
controlQueue: [],
|
|
575
|
+
};
|
|
576
|
+
let runCount = 0;
|
|
577
|
+
let handle;
|
|
578
|
+
try {
|
|
579
|
+
handle = await startRuntimeServer({
|
|
580
|
+
host: '127.0.0.1',
|
|
581
|
+
port: 0,
|
|
582
|
+
store: {
|
|
583
|
+
dbPath: ':memory:',
|
|
584
|
+
getState: () => ({
|
|
585
|
+
status: 'running',
|
|
586
|
+
plan: [{ step: 1, description: 'Build documents', status: 'running' }],
|
|
587
|
+
queue: [],
|
|
588
|
+
approvals: [],
|
|
589
|
+
summary: null,
|
|
590
|
+
}),
|
|
591
|
+
listEvents: () => [],
|
|
592
|
+
},
|
|
593
|
+
getContext: async () => ({
|
|
594
|
+
workspace: 'juno',
|
|
595
|
+
session,
|
|
596
|
+
running: true,
|
|
597
|
+
currentAbortController: new AbortController(),
|
|
598
|
+
}),
|
|
599
|
+
run: async () => { runCount += 1; },
|
|
600
|
+
});
|
|
601
|
+
} catch (err) {
|
|
602
|
+
if (err?.code === 'EPERM') {
|
|
603
|
+
t.skip('network listen is not permitted in this sandbox');
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
throw err;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=juno`, {
|
|
611
|
+
method: 'POST',
|
|
612
|
+
headers: { 'Content-Type': 'application/json' },
|
|
613
|
+
body: JSON.stringify({ action: 'message', input: 'Où en est le build ?' }),
|
|
614
|
+
});
|
|
615
|
+
assert.equal(response.status, 200);
|
|
616
|
+
const body = await response.json();
|
|
617
|
+
assert.equal(body.kind, 'observe');
|
|
618
|
+
assert.match(body.explanation, /Build documents/);
|
|
619
|
+
assert.equal(session.controlQueue.length, 0);
|
|
620
|
+
assert.equal(runCount, 0);
|
|
621
|
+
} finally {
|
|
622
|
+
await handle.close();
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test('runtime server control message records active plan mutation as a proposal', async (t) => {
|
|
627
|
+
const session = {
|
|
628
|
+
workspace: 'juno',
|
|
629
|
+
controlQueue: [],
|
|
630
|
+
};
|
|
631
|
+
dispatchAgentEvent(session, createAgentEvent('run_started', {
|
|
632
|
+
origin: 'runtime',
|
|
633
|
+
runId: 'run-mutate',
|
|
634
|
+
workspace: 'juno',
|
|
635
|
+
}));
|
|
636
|
+
dispatchAgentEvent(session, createAgentEvent('plan_set', {
|
|
637
|
+
origin: 'tool',
|
|
638
|
+
runId: 'run-mutate',
|
|
639
|
+
workspace: 'juno',
|
|
640
|
+
payload: {
|
|
641
|
+
steps: [{ step: 1, id: 'generate', description: 'Generate', status: 'running' }],
|
|
642
|
+
},
|
|
643
|
+
}));
|
|
644
|
+
let runCount = 0;
|
|
645
|
+
let handle;
|
|
646
|
+
try {
|
|
647
|
+
handle = await startRuntimeServer({
|
|
648
|
+
host: '127.0.0.1',
|
|
649
|
+
port: 0,
|
|
650
|
+
store: {
|
|
651
|
+
dbPath: ':memory:',
|
|
652
|
+
getState: () => ({
|
|
653
|
+
...session.agentProjection,
|
|
654
|
+
status: session.agentProjection?.status ?? 'running',
|
|
655
|
+
queue: [],
|
|
656
|
+
runs: [{ id: 'run-mutate', workspace: 'juno', status: 'running' }],
|
|
657
|
+
runId: 'run-mutate',
|
|
658
|
+
}),
|
|
659
|
+
listEvents: () => [],
|
|
660
|
+
},
|
|
661
|
+
getContext: async () => ({
|
|
662
|
+
workspace: 'juno',
|
|
663
|
+
session,
|
|
664
|
+
running: true,
|
|
665
|
+
currentAbortController: new AbortController(),
|
|
666
|
+
currentRunId: 'run-mutate',
|
|
667
|
+
}),
|
|
668
|
+
run: async () => { runCount += 1; },
|
|
669
|
+
});
|
|
670
|
+
} catch (err) {
|
|
671
|
+
if (err?.code === 'EPERM') {
|
|
672
|
+
t.skip('network listen is not permitted in this sandbox');
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
throw err;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
try {
|
|
679
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=juno`, {
|
|
680
|
+
method: 'POST',
|
|
681
|
+
headers: { 'Content-Type': 'application/json' },
|
|
682
|
+
body: JSON.stringify({ action: 'message', input: 'Ajoute un envoi après chaque génération' }),
|
|
683
|
+
});
|
|
684
|
+
assert.equal(response.status, 202);
|
|
685
|
+
const body = await response.json();
|
|
686
|
+
assert.equal(body.kind, 'mutate');
|
|
687
|
+
assert.equal(body.proposal.status, 'proposed');
|
|
688
|
+
assert.equal(body.proposal.input, 'Ajoute un envoi après chaque génération');
|
|
689
|
+
assert.equal(session.agentProjection.planPatches[0].status, 'proposed');
|
|
690
|
+
assert.ok(session.agentEvents.some((event) => event.type === 'control_message_received'));
|
|
691
|
+
assert.ok(session.agentEvents.some((event) => event.type === 'plan_patch_proposed'));
|
|
692
|
+
const approveResponse = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=juno`, {
|
|
693
|
+
method: 'POST',
|
|
694
|
+
headers: { 'Content-Type': 'application/json' },
|
|
695
|
+
body: JSON.stringify({ action: 'approve_patch', patchId: body.proposal.id }),
|
|
696
|
+
});
|
|
697
|
+
assert.equal(approveResponse.status, 202);
|
|
698
|
+
const approved = await approveResponse.json();
|
|
699
|
+
assert.equal(approved.kind, 'approve_patch');
|
|
700
|
+
// plan_set (revision 0->1) then the approved patch application (1->2).
|
|
701
|
+
assert.equal(session.agentProjection.planRevision, 2);
|
|
702
|
+
assert.deepEqual(session.agentProjection.plan.map((step) => step.id), ['generate', session.agentProjection.plan[1].id]);
|
|
703
|
+
assert.deepEqual(session.agentProjection.plan[1].dependsOn, ['generate']);
|
|
704
|
+
assert.ok(session.agentEvents.some((event) => event.type === 'plan_patch_approved'));
|
|
705
|
+
assert.ok(session.agentEvents.some((event) => event.type === 'plan_patch_applied'));
|
|
706
|
+
assert.equal(session.controlQueue.length, 0);
|
|
707
|
+
assert.equal(runCount, 0);
|
|
708
|
+
} finally {
|
|
709
|
+
await handle.close();
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
test('runtime server control message reports ambiguity without starting a run', async (t) => {
|
|
714
|
+
const session = {
|
|
715
|
+
workspace: 'juno',
|
|
716
|
+
controlQueue: [],
|
|
717
|
+
};
|
|
718
|
+
let runCount = 0;
|
|
719
|
+
let handle;
|
|
720
|
+
try {
|
|
721
|
+
handle = await startRuntimeServer({
|
|
722
|
+
host: '127.0.0.1',
|
|
723
|
+
port: 0,
|
|
724
|
+
store: {
|
|
725
|
+
dbPath: ':memory:',
|
|
726
|
+
getState: () => ({
|
|
727
|
+
status: 'running',
|
|
728
|
+
plan: [{ step: 1, description: 'Generate', status: 'running' }],
|
|
729
|
+
queue: [],
|
|
730
|
+
approvals: [],
|
|
731
|
+
summary: null,
|
|
732
|
+
}),
|
|
733
|
+
listEvents: () => [],
|
|
734
|
+
},
|
|
735
|
+
getContext: async () => ({
|
|
736
|
+
workspace: 'juno',
|
|
737
|
+
session,
|
|
738
|
+
running: true,
|
|
739
|
+
currentAbortController: new AbortController(),
|
|
740
|
+
}),
|
|
741
|
+
run: async () => { runCount += 1; },
|
|
742
|
+
});
|
|
743
|
+
} catch (err) {
|
|
744
|
+
if (err?.code === 'EPERM') {
|
|
745
|
+
t.skip('network listen is not permitted in this sandbox');
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
throw err;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
try {
|
|
752
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=juno`, {
|
|
753
|
+
method: 'POST',
|
|
754
|
+
headers: { 'Content-Type': 'application/json' },
|
|
755
|
+
body: JSON.stringify({ action: 'message', input: 'Lance aussi la publication' }),
|
|
756
|
+
});
|
|
757
|
+
assert.equal(response.status, 200);
|
|
758
|
+
const body = await response.json();
|
|
759
|
+
assert.equal(body.kind, 'ambiguous');
|
|
760
|
+
assert.equal(body.choices.length, 3);
|
|
761
|
+
assert.equal(session.controlQueue.length, 0);
|
|
762
|
+
assert.equal(runCount, 0);
|
|
763
|
+
} finally {
|
|
764
|
+
await handle.close();
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
|
|
429
768
|
test('runtime server drains queued control requests when idle', async (t) => {
|
|
430
769
|
const session = {
|
|
431
770
|
workspace: 'juno',
|
package/src/runtime/store.js
CHANGED
|
@@ -4,6 +4,7 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
4
4
|
import { applyAgentProjectionToSession, dispatchAgentEvent, reduceAgentEvents } from '../core/agentEvents.js';
|
|
5
5
|
import { defaultRuntimeStateDir } from '../core/env.js';
|
|
6
6
|
import { projectQueue } from '../core/jobQueue.js';
|
|
7
|
+
import { projectWorkflow } from '../core/workflow.js';
|
|
7
8
|
|
|
8
9
|
export { defaultRuntimeStateDir };
|
|
9
10
|
|
|
@@ -35,6 +36,7 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
|
|
|
35
36
|
type TEXT NOT NULL,
|
|
36
37
|
run_id TEXT,
|
|
37
38
|
turn_id TEXT,
|
|
39
|
+
task_id TEXT,
|
|
38
40
|
workspace TEXT,
|
|
39
41
|
origin TEXT,
|
|
40
42
|
payload TEXT NOT NULL
|
|
@@ -69,6 +71,7 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
|
|
|
69
71
|
`);
|
|
70
72
|
ensureColumn(db, 'events', 'sequence', 'INTEGER');
|
|
71
73
|
ensureColumn(db, 'events', 'workspace', 'TEXT');
|
|
74
|
+
ensureColumn(db, 'events', 'task_id', 'TEXT');
|
|
72
75
|
ensureColumn(db, 'runs', 'workspace', 'TEXT');
|
|
73
76
|
backfillEventSequence(db);
|
|
74
77
|
db.exec('CREATE INDEX IF NOT EXISTS idx_events_workspace ON events(workspace)');
|
|
@@ -78,17 +81,17 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
|
|
|
78
81
|
let lastEventSequence = null;
|
|
79
82
|
|
|
80
83
|
const insertEvent = db.prepare(`
|
|
81
|
-
INSERT OR IGNORE INTO events (sequence, id, ts, type, run_id, turn_id, workspace, origin, payload)
|
|
82
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
84
|
+
INSERT OR IGNORE INTO events (sequence, id, ts, type, run_id, turn_id, task_id, workspace, origin, payload)
|
|
85
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
83
86
|
`);
|
|
84
87
|
const nextEventSequenceStatement = db.prepare('SELECT COALESCE(MAX(sequence), 0) + 1 AS next_sequence FROM events');
|
|
85
88
|
const listEventsStatement = db.prepare(`
|
|
86
|
-
SELECT sequence, id, ts, type, run_id, turn_id, workspace, origin, payload
|
|
89
|
+
SELECT sequence, id, ts, type, run_id, turn_id, task_id, workspace, origin, payload
|
|
87
90
|
FROM events
|
|
88
91
|
ORDER BY sequence ASC
|
|
89
92
|
`);
|
|
90
93
|
const listEventsByWorkspaceStatement = db.prepare(`
|
|
91
|
-
SELECT sequence, id, ts, type, run_id, turn_id, workspace, origin, payload
|
|
94
|
+
SELECT sequence, id, ts, type, run_id, turn_id, task_id, workspace, origin, payload
|
|
92
95
|
FROM events
|
|
93
96
|
WHERE workspace = ?
|
|
94
97
|
ORDER BY sequence ASC
|
|
@@ -195,6 +198,7 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
|
|
|
195
198
|
event.type,
|
|
196
199
|
event.runId ?? null,
|
|
197
200
|
event.turnId ?? null,
|
|
201
|
+
event.taskId ?? event.payload?.taskId ?? null,
|
|
198
202
|
ws,
|
|
199
203
|
event.origin ?? null,
|
|
200
204
|
JSON.stringify(event.payload ?? {}),
|
|
@@ -240,6 +244,12 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
|
|
|
240
244
|
return rows.map(rowToEvent);
|
|
241
245
|
}
|
|
242
246
|
|
|
247
|
+
function listAuditTrail({ workspace = null, runId = null } = {}) {
|
|
248
|
+
return listEvents({ workspace })
|
|
249
|
+
.filter((event) => !runId || event.runId === runId || event.payload?.runId === runId)
|
|
250
|
+
.map(eventToAuditEntry);
|
|
251
|
+
}
|
|
252
|
+
|
|
243
253
|
function listRuns({ workspace = null } = {}) {
|
|
244
254
|
const rows = workspace
|
|
245
255
|
? listRunsByWorkspaceStatement.all(workspace)
|
|
@@ -362,15 +372,22 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
|
|
|
362
372
|
const events = session?.agentProjection ? null : listEvents({ workspace });
|
|
363
373
|
const projection = session?.agentProjection ?? reduceAgentEvents(events);
|
|
364
374
|
const rawQueue = session?.queueStore?.list() ?? listQueue({ workspace });
|
|
365
|
-
|
|
375
|
+
const queue = projectQueue(projection.plan, rawQueue, { workspace });
|
|
376
|
+
const controlQueue = Array.isArray(projection.controlQueue)
|
|
377
|
+
? projection.controlQueue.filter((item) => !workspace || item.workspace === workspace || !item.workspace).map((item) => ({ ...item }))
|
|
378
|
+
: [];
|
|
379
|
+
const baseState = {
|
|
366
380
|
...projection,
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
controlQueue: Array.isArray(projection.controlQueue)
|
|
370
|
-
? projection.controlQueue.filter((item) => !workspace || item.workspace === workspace || !item.workspace).map((item) => ({ ...item }))
|
|
371
|
-
: [],
|
|
381
|
+
queue,
|
|
382
|
+
controlQueue,
|
|
372
383
|
eventsCursor: session?.agentEvents?.at(-1)?.sequence ?? events?.at(-1)?.sequence ?? lastEventSequence,
|
|
373
384
|
};
|
|
385
|
+
const runs = listRuns({ workspace });
|
|
386
|
+
return {
|
|
387
|
+
...baseState,
|
|
388
|
+
runs,
|
|
389
|
+
workflow: projectWorkflow({ ...baseState, runs, workspace }, events ?? session?.agentEvents ?? []),
|
|
390
|
+
};
|
|
374
391
|
}
|
|
375
392
|
|
|
376
393
|
function hydrateSession(session, { workspace = null } = {}) {
|
|
@@ -391,6 +408,7 @@ export function openRuntimeStore({ stateDir = defaultRuntimeStateDir(), fileName
|
|
|
391
408
|
persistEvent,
|
|
392
409
|
persistRun,
|
|
393
410
|
listEvents,
|
|
411
|
+
listAuditTrail,
|
|
394
412
|
listRuns,
|
|
395
413
|
listRecoverableRuns,
|
|
396
414
|
listRecoverableWorkspaces,
|
|
@@ -414,11 +432,42 @@ function rowToEvent(row) {
|
|
|
414
432
|
origin: row.origin ?? 'system',
|
|
415
433
|
runId: row.run_id ?? null,
|
|
416
434
|
turnId: row.turn_id ?? null,
|
|
435
|
+
taskId: row.task_id ?? null,
|
|
417
436
|
workspace: row.workspace ?? null,
|
|
418
437
|
payload: row.payload ? JSON.parse(row.payload) : {},
|
|
419
438
|
};
|
|
420
439
|
}
|
|
421
440
|
|
|
441
|
+
function eventToAuditEntry(event) {
|
|
442
|
+
const payload = event.payload ?? {};
|
|
443
|
+
const activity = payload.activity ?? {};
|
|
444
|
+
return {
|
|
445
|
+
sequence: event.sequence ?? null,
|
|
446
|
+
ts: event.ts,
|
|
447
|
+
type: event.type,
|
|
448
|
+
runId: event.runId ?? payload.runId ?? null,
|
|
449
|
+
turnId: event.turnId ?? payload.turnId ?? null,
|
|
450
|
+
taskId: event.taskId ?? payload.taskId ?? activity.taskId ?? null,
|
|
451
|
+
activityId: payload.activityId ?? activity.id ?? activity.key ?? null,
|
|
452
|
+
toolCallId: payload.toolCallId ?? payload.callId ?? null,
|
|
453
|
+
workspace: event.workspace ?? payload.workspace ?? null,
|
|
454
|
+
caller: event.origin ?? payload.caller ?? 'system',
|
|
455
|
+
status: payload.status ?? activity.status ?? null,
|
|
456
|
+
tool: payload.tool ?? payload.name ?? activity.tool ?? null,
|
|
457
|
+
summary: auditSummary(event),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function auditSummary(event) {
|
|
462
|
+
const payload = event.payload ?? {};
|
|
463
|
+
if (typeof payload.message === 'string') return payload.message;
|
|
464
|
+
if (typeof payload.summary === 'string') return payload.summary;
|
|
465
|
+
if (typeof payload.input === 'string') return payload.input;
|
|
466
|
+
if (typeof payload.content === 'string') return payload.content.slice(0, 240);
|
|
467
|
+
if (payload.activity?.label) return payload.activity.label;
|
|
468
|
+
return event.type;
|
|
469
|
+
}
|
|
470
|
+
|
|
422
471
|
function ensureColumn(db, table, column, definition) {
|
|
423
472
|
const existing = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
424
473
|
if (existing.some((row) => row.name === column)) return;
|
|
@@ -59,6 +59,7 @@ test('runtime store persists run identity fields on events', () => {
|
|
|
59
59
|
origin: 'agent',
|
|
60
60
|
runId: 'run-2',
|
|
61
61
|
turnId: 'run-2:turn-1',
|
|
62
|
+
taskId: 'task-1',
|
|
62
63
|
workspace: 'docs',
|
|
63
64
|
payload: { content: 'done' },
|
|
64
65
|
}));
|
|
@@ -66,10 +67,51 @@ test('runtime store persists run identity fields on events', () => {
|
|
|
66
67
|
const [event] = store.listEvents();
|
|
67
68
|
assert.equal(event.runId, 'run-2');
|
|
68
69
|
assert.equal(event.turnId, 'run-2:turn-1');
|
|
70
|
+
assert.equal(event.taskId, 'task-1');
|
|
69
71
|
assert.equal(event.workspace, 'docs');
|
|
70
72
|
store.close();
|
|
71
73
|
});
|
|
72
74
|
|
|
75
|
+
test('runtime store exposes a correlated audit trail', () => {
|
|
76
|
+
const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
|
|
77
|
+
const store = openRuntimeStore({ stateDir });
|
|
78
|
+
store.persistEvent(createAgentEvent('tool_call_started', {
|
|
79
|
+
origin: 'agent',
|
|
80
|
+
runId: 'run-audit',
|
|
81
|
+
turnId: 'run-audit:turn-1',
|
|
82
|
+
taskId: 'task-a',
|
|
83
|
+
workspace: 'docs',
|
|
84
|
+
payload: { callId: 'call-1', name: 'production_start_job' },
|
|
85
|
+
}));
|
|
86
|
+
store.persistEvent(createAgentEvent('activity_upserted', {
|
|
87
|
+
origin: 'tool',
|
|
88
|
+
runId: 'run-audit',
|
|
89
|
+
turnId: 'run-audit:turn-1',
|
|
90
|
+
taskId: 'task-a',
|
|
91
|
+
workspace: 'docs',
|
|
92
|
+
payload: {
|
|
93
|
+
activity: {
|
|
94
|
+
id: 'job-1',
|
|
95
|
+
source: 'production',
|
|
96
|
+
kind: 'job',
|
|
97
|
+
label: 'Production job',
|
|
98
|
+
status: 'running',
|
|
99
|
+
progress: {},
|
|
100
|
+
poll: null,
|
|
101
|
+
outputRefs: [],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
const audit = store.listAuditTrail({ workspace: 'docs', runId: 'run-audit' });
|
|
107
|
+
assert.equal(audit.length, 2);
|
|
108
|
+
assert.equal(audit[0].toolCallId, 'call-1');
|
|
109
|
+
assert.equal(audit[0].taskId, 'task-a');
|
|
110
|
+
assert.equal(audit[1].activityId, 'job-1');
|
|
111
|
+
assert.equal(audit[1].workspace, 'docs');
|
|
112
|
+
store.close();
|
|
113
|
+
});
|
|
114
|
+
|
|
73
115
|
test('runtime store replays evaluator verdict into state', () => {
|
|
74
116
|
const stateDir = mkdtempSync(join(tmpdir(), 'wiki-manager-runtime-'));
|
|
75
117
|
const store = openRuntimeStore({ stateDir });
|
|
@@ -404,11 +446,15 @@ test('runtime state exposes queue as blocked jobs and pending plan steps', () =>
|
|
|
404
446
|
{ id: 'q-done', workspace: 'docs', status: 'done', args: { type: 'done' } },
|
|
405
447
|
], { workspace: 'docs' });
|
|
406
448
|
|
|
407
|
-
const
|
|
449
|
+
const state = store.getState(session, { workspace: 'docs' });
|
|
450
|
+
const queue = state.queue;
|
|
408
451
|
assert.deepEqual(queue.map((item) => item.id), ['q-wait', 'plan-2', 'plan-3']);
|
|
409
452
|
assert.equal(queue[0].queueType, 'blocked_job');
|
|
410
453
|
assert.equal(queue[1].queueType, 'pending_step');
|
|
411
454
|
assert.equal(queue[1].args.type, 'Build');
|
|
455
|
+
assert.ok(state.workflow.nodes.some((node) => node.id === 'task:2'));
|
|
456
|
+
assert.ok(state.workflow.nodes.some((node) => node.id === 'queue:q-wait'));
|
|
457
|
+
assert.ok(state.workflow.waitingReasons.includes('queue:q-wait'));
|
|
412
458
|
store.close();
|
|
413
459
|
});
|
|
414
460
|
|
package/src/shell/RightPane.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import { createMemo, Index, Show } from 'solid-js';
|
|
3
|
+
import { fit } from './textFit';
|
|
3
4
|
|
|
4
5
|
type PlanStep = { step: number; description: string; status: string };
|
|
5
6
|
type QueueItem = {
|
|
@@ -45,13 +46,6 @@ function wrapLine(value: string, width: number) {
|
|
|
45
46
|
return lines;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
function fit(value: string, width: number) {
|
|
49
|
-
const max = Math.max(1, width);
|
|
50
|
-
if (value.length <= max) return value;
|
|
51
|
-
if (max <= 1) return '…';
|
|
52
|
-
return value.slice(0, max - 1) + '…';
|
|
53
|
-
}
|
|
54
|
-
|
|
55
49
|
function logLineParts(line: string): LogLineParts {
|
|
56
50
|
const match = line.match(/^((?:\d{1,2}:\d{2}(?::\d{2})?(?:\s?[AP]M)?|\d{4}-\d{2}-\d{2}[T ][0-9:.]+Z?))\s+(.+)$/i);
|
|
57
51
|
if (!match) return { time: null, message: line };
|