@dotdrelle/wiki-manager 0.7.3 → 0.8.2
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/.env.example +20 -0
- package/README.md +35 -1
- package/docker-compose.yml +1 -23
- package/mcp.endpoints.example.json +13 -0
- package/package.json +2 -2
- package/src/agent/graph.js +101 -15
- package/src/agent/graph.test.js +145 -0
- package/src/cli/wiki-manager.js +274 -53
- package/src/commands/slash.js +1 -1
- package/src/core/agentEvents.js +124 -4
- package/src/core/agentEvents.test.js +145 -4
- package/src/core/agentLoop.js +3 -0
- package/src/core/compose.js +1 -2
- package/src/core/dockerCompose.test.js +5 -5
- package/src/core/jobQueue.js +29 -12
- package/src/core/mcp.js +120 -10
- package/src/core/mcp.test.js +121 -1
- package/src/core/plan.js +33 -0
- package/src/core/queueStore.test.js +1 -0
- package/src/core/wikiWorkspace.test.js +24 -0
- package/src/runtime/approvals.js +113 -0
- package/src/runtime/auth.test.js +8 -0
- package/src/runtime/client.js +52 -6
- package/src/runtime/lifecycle.js +27 -3
- package/src/runtime/queueStore.js +3 -3
- package/src/runtime/runner.js +340 -0
- package/src/runtime/runner.test.js +270 -0
- package/src/runtime/server.js +85 -29
- package/src/runtime/server.test.js +255 -0
- package/src/runtime/store.js +178 -39
- package/src/runtime/store.test.js +338 -4
- package/src/runtime/supervisor.js +6 -0
- package/src/runtime/supervisor.test.js +141 -0
- package/src/shell/RightPane.tsx +1 -1
- package/src/shell/repl.js +22 -6
- package/src/shell/useAgent.ts +1 -1
- package/src/shell/useSession.ts +10 -5
- package/wiki-workspace +198 -4
package/src/cli/wiki-manager.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
1
|
import { readFileSync } from 'node:fs';
|
|
3
2
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
3
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -6,7 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
5
|
import { loadManagerEnv } from '../core/env.js';
|
|
7
6
|
loadManagerEnv();
|
|
8
7
|
import { createAgentGraph } from '../agent/graph.js';
|
|
9
|
-
import { handleSlashCommand, printHelp, printVersion } from '../commands/slash.js';
|
|
8
|
+
import { handleSlashCommand, printHelp, printVersion, refreshMcpRuntimeStatus } from '../commands/slash.js';
|
|
10
9
|
import { runShell } from '../shell/repl.js';
|
|
11
10
|
import { runChecks } from '../core/startupCheck.js';
|
|
12
11
|
import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
|
|
@@ -20,7 +19,7 @@ import { runAgentTurn, runAgenticLoop } from '../core/agentLoop.js';
|
|
|
20
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
20
|
const packageJsonPath = resolve(__dirname, '../../package.json');
|
|
22
21
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
23
|
-
const SHELL_COMMANDS = ['help', 'version', 'exit', 'workspace', 'new', 'use', 'config', 'status', 'services', 'start', 'stop', 'logs', 'mcp', 'wiki', 'skills', 'clear', 'chat', 'agent'];
|
|
22
|
+
const SHELL_COMMANDS = ['help', 'version', 'exit', 'workspace', 'new', 'use', 'config', 'status', 'services', 'start', 'stop', 'logs', 'mcp', 'wiki', 'skills', 'clear', 'chat', 'agent', 'approve'];
|
|
24
23
|
|
|
25
24
|
function valueAfter(argv, flag) {
|
|
26
25
|
const index = argv.indexOf(flag);
|
|
@@ -286,12 +285,13 @@ async function runRuntime(argv, agent) {
|
|
|
286
285
|
].join('\n'));
|
|
287
286
|
return;
|
|
288
287
|
}
|
|
289
|
-
const { defaultRuntimeStateDir, openRuntimeStore } = await import('../runtime/store.js');
|
|
288
|
+
const { defaultRuntimeStateDir, openRuntimeStore, RECOVERABLE_QUEUE_STATUSES } = await import('../runtime/store.js');
|
|
290
289
|
const { startRuntimeServer } = await import('../runtime/server.js');
|
|
291
290
|
const { emitRuntimeLog, startActivitySupervisor } = await import('../runtime/supervisor.js');
|
|
292
291
|
const { resolveRuntimeAuthToken } = await import('../runtime/auth.js');
|
|
293
292
|
const { createSqliteQueueStore } = await import('../runtime/queueStore.js');
|
|
294
|
-
const {
|
|
293
|
+
const { createApprovalManager } = await import('../runtime/approvals.js');
|
|
294
|
+
const { runRuntimeAgenticWorkflow } = await import('../runtime/runner.js');
|
|
295
295
|
|
|
296
296
|
const host = valueAfter(argv, '--host') ?? process.env.WIKI_MANAGER_RUNTIME_HOST ?? '0.0.0.0';
|
|
297
297
|
const port = Number(valueAfter(argv, '--port') ?? process.env.WIKI_MANAGER_RUNTIME_PORT ?? 7788);
|
|
@@ -302,45 +302,267 @@ async function runRuntime(argv, agent) {
|
|
|
302
302
|
throw new Error(`Invalid runtime port: ${port}`);
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
const session = createSession();
|
|
306
|
-
session.headless = true;
|
|
307
|
-
session.chatMode = false;
|
|
308
|
-
session.packageJson = packageJson;
|
|
309
|
-
|
|
310
305
|
const store = openRuntimeStore({ stateDir });
|
|
311
|
-
store.hydrateSession(session);
|
|
312
|
-
session.queueStore = createSqliteQueueStore(store, session);
|
|
313
|
-
|
|
314
306
|
let serverHandle = null;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
307
|
+
const contexts = new Map();
|
|
308
|
+
|
|
309
|
+
async function getWorkspaceContext(workspaceName = null) {
|
|
310
|
+
const requestedWorkspace = workspaceName ? String(workspaceName).trim() : null;
|
|
311
|
+
const key = requestedWorkspace ?? '__default__';
|
|
312
|
+
if (contexts.has(key)) return contexts.get(key);
|
|
313
|
+
|
|
314
|
+
const pending = (async () => {
|
|
315
|
+
const session = createSession();
|
|
316
|
+
session.headless = true;
|
|
317
|
+
session.chatMode = false;
|
|
318
|
+
session.packageJson = packageJson;
|
|
319
|
+
|
|
320
|
+
if (requestedWorkspace) {
|
|
321
|
+
const result = await handleSlashCommand(`/use ${requestedWorkspace}`, { packageJson, session });
|
|
322
|
+
if (!session.workspacePath) throw new Error(result.output || `Workspace not loaded: ${requestedWorkspace}`);
|
|
323
|
+
}
|
|
324
|
+
const workspace = session.workspace ?? requestedWorkspace ?? null;
|
|
325
|
+
store.hydrateSession(session, { workspace });
|
|
326
|
+
session.queueStore = createSqliteQueueStore(store, session, { workspace });
|
|
327
|
+
|
|
328
|
+
const context = {
|
|
329
|
+
workspace,
|
|
330
|
+
session,
|
|
331
|
+
supervisor: null,
|
|
332
|
+
running: false,
|
|
333
|
+
currentAbortController: null,
|
|
334
|
+
approvalManager: null,
|
|
335
|
+
};
|
|
336
|
+
session._onAgentEvent = (event) => {
|
|
337
|
+
store.persistEvent(event);
|
|
338
|
+
serverHandle?.publish(event);
|
|
339
|
+
};
|
|
340
|
+
session._onRuntimeError = (err) => {
|
|
341
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
342
|
+
dispatchAgentEvent(session, createAgentEvent('run_error', {
|
|
343
|
+
origin: 'runtime',
|
|
344
|
+
payload: { message, workspace },
|
|
345
|
+
}));
|
|
346
|
+
};
|
|
347
|
+
context.approvalManager = createApprovalManager(session, {
|
|
348
|
+
defaultTimeoutMs: Number.isFinite(Number(process.env.WIKI_MANAGER_APPROVAL_TIMEOUT_MS))
|
|
349
|
+
? Math.max(1, Number(process.env.WIKI_MANAGER_APPROVAL_TIMEOUT_MS))
|
|
350
|
+
: undefined,
|
|
351
|
+
});
|
|
352
|
+
session._requestApproval = (request) => context.approvalManager.requestApproval(request);
|
|
353
|
+
context.supervisor = startActivitySupervisor(session);
|
|
354
|
+
contexts.set(key, context);
|
|
355
|
+
if (workspace && workspace !== key) contexts.set(workspace, context);
|
|
356
|
+
return context;
|
|
357
|
+
})();
|
|
358
|
+
contexts.set(key, pending);
|
|
359
|
+
pending.catch(() => {
|
|
360
|
+
if (contexts.get(key) === pending) contexts.delete(key);
|
|
361
|
+
});
|
|
362
|
+
return pending;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function recoveryMcpGaps(context) {
|
|
366
|
+
const gaps = [];
|
|
367
|
+
const session = context.session;
|
|
368
|
+
for (const activity of sessionActivities(session)) {
|
|
369
|
+
if (activity.terminal || !activity.poll?.server) continue;
|
|
370
|
+
const endpoint = session.mcp?.[activity.poll.server];
|
|
371
|
+
if (endpoint?.status !== 'connected') gaps.push(activity.poll.server);
|
|
372
|
+
}
|
|
373
|
+
for (const item of session.queueStore?.list?.() ?? []) {
|
|
374
|
+
const status = String(item.status ?? '').toLowerCase();
|
|
375
|
+
if (!RECOVERABLE_QUEUE_STATUSES.includes(status)) continue;
|
|
376
|
+
const endpoint = session.mcp?.[item.server ?? 'production'];
|
|
377
|
+
if (endpoint?.status !== 'connected') gaps.push(item.server ?? 'production');
|
|
378
|
+
}
|
|
379
|
+
return [...new Set(gaps)];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function activeNonTerminalActivities(session) {
|
|
383
|
+
return sessionActivities(session).filter((activity) => !activity.terminal);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function activePollingActivities(session) {
|
|
387
|
+
return activeNonTerminalActivities(session).filter((activity) => activity.poll);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function buildRecoveryPrompt(run, session) {
|
|
391
|
+
const conversation = session.agentProjection?.conversation ?? [];
|
|
392
|
+
const recentConversation = conversation
|
|
393
|
+
.slice(-8)
|
|
394
|
+
.map((message) => `${message.role}: ${message.content}`)
|
|
395
|
+
.join('\n');
|
|
396
|
+
return [
|
|
397
|
+
'Resume an interrupted runtime run.',
|
|
398
|
+
'',
|
|
399
|
+
'Original task:',
|
|
400
|
+
run.input ?? '(unknown)',
|
|
401
|
+
'',
|
|
402
|
+
session.headlessPlan ? `Current plan:\n${formatPlanStatus(session.headlessPlan)}` : null,
|
|
403
|
+
recentConversation ? `Recent conversation:\n${recentConversation}` : null,
|
|
404
|
+
'',
|
|
405
|
+
'Continue from the current plan state. Start only the next pending step.',
|
|
406
|
+
'If the work is already complete, provide a concise final summary.',
|
|
407
|
+
].filter(Boolean).join('\n');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function startRecoveredAgenticRun(context, run) {
|
|
411
|
+
if (context.running) return false;
|
|
412
|
+
const session = context.session;
|
|
413
|
+
const supervisor = context.supervisor;
|
|
414
|
+
const runId = run.id;
|
|
415
|
+
const input = buildRecoveryPrompt(run, session);
|
|
416
|
+
context.running = true;
|
|
417
|
+
context.currentAbortController = new AbortController();
|
|
418
|
+
session._currentRunIdentity = {
|
|
419
|
+
runId,
|
|
420
|
+
turnId: `${runId}:resume-0`,
|
|
421
|
+
workspace: context.workspace,
|
|
422
|
+
};
|
|
423
|
+
session._abortSignal = context.currentAbortController.signal;
|
|
424
|
+
supervisor?.setRunSignal(context.currentAbortController.signal);
|
|
425
|
+
session._onStep = (message) => emitRuntimeLog(session, message);
|
|
426
|
+
emitRuntimeLog(session, `runtime: resuming interrupted run ${runId}`);
|
|
427
|
+
|
|
428
|
+
runRuntimeAgenticWorkflow(agent, session, run.input ?? input, {
|
|
429
|
+
initialInput: input,
|
|
430
|
+
signal: context.currentAbortController.signal,
|
|
431
|
+
timeoutMs: 3600 * 1000,
|
|
432
|
+
maxTurns: 20,
|
|
433
|
+
runId,
|
|
434
|
+
pollBusy: supervisor?.pollBusy,
|
|
435
|
+
})
|
|
436
|
+
.catch((err) => {
|
|
437
|
+
if (err?.name === 'AbortError') {
|
|
438
|
+
dispatchAgentEvent(session, createAgentEvent('run_cancelled', {
|
|
439
|
+
origin: 'runtime',
|
|
440
|
+
runId,
|
|
441
|
+
payload: { runId, message: 'Recovered runtime run cancelled.' },
|
|
442
|
+
}));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
dispatchAgentEvent(session, createAgentEvent('run_error', {
|
|
446
|
+
origin: 'runtime',
|
|
447
|
+
runId,
|
|
448
|
+
payload: {
|
|
449
|
+
runId,
|
|
450
|
+
message: err instanceof Error ? err.message : String(err),
|
|
451
|
+
},
|
|
452
|
+
}));
|
|
453
|
+
})
|
|
454
|
+
.finally(() => {
|
|
455
|
+
context.running = false;
|
|
456
|
+
context.currentAbortController = null;
|
|
457
|
+
supervisor?.setRunSignal(null);
|
|
458
|
+
delete session._abortSignal;
|
|
459
|
+
delete session._onStep;
|
|
460
|
+
delete session._currentRunIdentity;
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function recoverWorkspace(workspace, { manual = false } = {}) {
|
|
467
|
+
try {
|
|
468
|
+
const context = await getWorkspaceContext(workspace);
|
|
469
|
+
await refreshMcpRuntimeStatus(context.session);
|
|
470
|
+
const gaps = recoveryMcpGaps(context);
|
|
471
|
+
if (gaps.length > 0) {
|
|
472
|
+
const interrupted = store.interruptRuns({ workspace: context.workspace });
|
|
473
|
+
return {
|
|
474
|
+
workspace: context.workspace ?? workspace ?? null,
|
|
475
|
+
resumed: false,
|
|
476
|
+
interrupted,
|
|
477
|
+
reason: `MCP unavailable: ${gaps.join(', ')}`,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const recoverableRuns = store.listRecoverableRuns({ workspace: context.workspace });
|
|
481
|
+
const runningRun = recoverableRuns.find((run) => run.status === 'running');
|
|
482
|
+
const activeActivities = activeNonTerminalActivities(context.session);
|
|
483
|
+
const pollingActivities = activePollingActivities(context.session);
|
|
484
|
+
if (runningRun && activeActivities.length === 0) {
|
|
485
|
+
if (!runningRun.input) {
|
|
486
|
+
const interrupted = store.interruptRuns({ workspace: context.workspace });
|
|
487
|
+
return {
|
|
488
|
+
workspace: context.workspace ?? workspace ?? null,
|
|
489
|
+
resumed: false,
|
|
490
|
+
interrupted,
|
|
491
|
+
reason: 'Missing original run input.',
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
const started = startRecoveredAgenticRun(context, runningRun);
|
|
495
|
+
return {
|
|
496
|
+
workspace: context.workspace ?? workspace ?? null,
|
|
497
|
+
resumed: started,
|
|
498
|
+
interrupted: 0,
|
|
499
|
+
mode: 'agentic_loop',
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
if (runningRun && runningRun.input && pollingActivities.length > 0) {
|
|
503
|
+
context.session._onActivitiesTerminal = () => {
|
|
504
|
+
startRecoveredAgenticRun(context, runningRun);
|
|
505
|
+
};
|
|
506
|
+
emitRuntimeLog(context.session, `runtime: recovery watching ${pollingActivities.length} active activity(s)`);
|
|
507
|
+
return {
|
|
508
|
+
workspace: context.workspace ?? workspace ?? null,
|
|
509
|
+
resumed: true,
|
|
510
|
+
interrupted: 0,
|
|
511
|
+
mode: 'activity_poll_then_resume',
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
emitRuntimeLog(context.session, manual ? 'runtime: manual resume completed' : 'runtime: recovery completed');
|
|
515
|
+
return {
|
|
516
|
+
workspace: context.workspace ?? workspace ?? null,
|
|
517
|
+
resumed: true,
|
|
518
|
+
interrupted: 0,
|
|
519
|
+
mode: pollingActivities.length > 0 ? 'activity_poll' : 'context',
|
|
520
|
+
};
|
|
521
|
+
} catch (err) {
|
|
522
|
+
const interrupted = store.interruptRuns({ workspace });
|
|
523
|
+
return {
|
|
524
|
+
workspace: workspace ?? null,
|
|
525
|
+
resumed: false,
|
|
526
|
+
interrupted,
|
|
527
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
}
|
|
326
531
|
|
|
327
|
-
|
|
532
|
+
async function recoverRuntime({ workspace = null, manual = false } = {}) {
|
|
533
|
+
const workspaces = workspace ? [workspace] : store.listRecoverableWorkspaces();
|
|
534
|
+
const results = await Promise.all(workspaces.map((item) => recoverWorkspace(item, { manual })));
|
|
535
|
+
return {
|
|
536
|
+
resumed: results.filter((result) => result.resumed).length,
|
|
537
|
+
interrupted: results.reduce((sum, result) => sum + Number(result.interrupted ?? 0), 0),
|
|
538
|
+
workspaces: results,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
328
541
|
|
|
329
|
-
async function executeRun(body, { signal } = {}) {
|
|
542
|
+
async function executeRun(context, body, { signal } = {}) {
|
|
543
|
+
const session = context.session;
|
|
544
|
+
const supervisor = context.supervisor;
|
|
330
545
|
const input = String(body.input ?? body.prompt ?? '').trim();
|
|
331
546
|
const workspace = body.workspace ? String(body.workspace).trim() : null;
|
|
332
547
|
const timeoutMs = (Number.isFinite(Number(body.timeout)) ? Math.max(1, Number(body.timeout)) : 3600) * 1000;
|
|
333
548
|
const maxTurns = Number.isFinite(Number(body.maxTurns)) ? Math.max(1, Number(body.maxTurns)) : 20;
|
|
334
|
-
const
|
|
549
|
+
const maxReplans = Number.isFinite(Number(body.replans)) ? Math.max(0, Math.floor(Number(body.replans))) : undefined;
|
|
550
|
+
const runId = String(body.runId);
|
|
335
551
|
try {
|
|
336
552
|
if (workspace && session.workspace !== workspace) {
|
|
337
553
|
const result = await handleSlashCommand(`/use ${workspace}`, { packageJson, session });
|
|
338
554
|
if (!session.workspacePath) throw new Error(result.output || `Workspace not loaded: ${workspace}`);
|
|
339
555
|
}
|
|
556
|
+
context.workspace = session.workspace ?? workspace ?? context.workspace ?? null;
|
|
557
|
+
session._currentRunIdentity = {
|
|
558
|
+
runId,
|
|
559
|
+
turnId: `${runId}:turn-0`,
|
|
560
|
+
workspace: context.workspace,
|
|
561
|
+
};
|
|
340
562
|
dispatchAgentEvent(session, createAgentEvent('run_started', {
|
|
341
563
|
origin: 'runtime',
|
|
342
564
|
runId,
|
|
343
|
-
payload: { input, workspace },
|
|
565
|
+
payload: { input, workspace: session._currentRunIdentity.workspace },
|
|
344
566
|
}));
|
|
345
567
|
dispatchAgentEvent(session, createAgentEvent('user_message', {
|
|
346
568
|
origin: 'user',
|
|
@@ -348,35 +570,22 @@ async function runRuntime(argv, agent) {
|
|
|
348
570
|
payload: { content: input },
|
|
349
571
|
}));
|
|
350
572
|
session._abortSignal = signal ?? null;
|
|
573
|
+
session._runApprovalRequired = body.requireApproval === true;
|
|
574
|
+
session._runApprovalResolved = false;
|
|
575
|
+
session._approvalTimeoutMs = Number.isFinite(Number(body.approvalTimeoutMs))
|
|
576
|
+
? Math.max(1, Number(body.approvalTimeoutMs))
|
|
577
|
+
: undefined;
|
|
351
578
|
supervisor?.setRunSignal(signal);
|
|
352
579
|
session._onStep = (message) => emitRuntimeLog(session, message);
|
|
353
|
-
|
|
580
|
+
await runRuntimeAgenticWorkflow(agent, session, input, {
|
|
354
581
|
signal,
|
|
355
582
|
timeoutMs,
|
|
356
583
|
maxTurns,
|
|
357
584
|
runId,
|
|
358
585
|
pollBusy: supervisor?.pollBusy,
|
|
586
|
+
evaluate: body.evaluate !== false,
|
|
587
|
+
...(maxReplans === undefined ? {} : { maxReplans }),
|
|
359
588
|
});
|
|
360
|
-
if (!result.ok) {
|
|
361
|
-
dispatchAgentEvent(session, createAgentEvent('run_error', {
|
|
362
|
-
origin: 'runtime',
|
|
363
|
-
runId,
|
|
364
|
-
payload: {
|
|
365
|
-
runId,
|
|
366
|
-
message: result.timedOut
|
|
367
|
-
? 'Runtime agentic loop timed out.'
|
|
368
|
-
: result.maxTurns
|
|
369
|
-
? `Runtime agentic loop reached max turns (${maxTurns}).`
|
|
370
|
-
: 'Runtime agentic loop failed.',
|
|
371
|
-
},
|
|
372
|
-
}));
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
dispatchAgentEvent(session, createAgentEvent('run_done', {
|
|
376
|
-
origin: 'runtime',
|
|
377
|
-
runId,
|
|
378
|
-
payload: { runId },
|
|
379
|
-
}));
|
|
380
589
|
} catch (err) {
|
|
381
590
|
if (err?.name === 'AbortError') {
|
|
382
591
|
dispatchAgentEvent(session, createAgentEvent('run_cancelled', {
|
|
@@ -401,6 +610,10 @@ async function runRuntime(argv, agent) {
|
|
|
401
610
|
supervisor?.setRunSignal(null);
|
|
402
611
|
delete session._abortSignal;
|
|
403
612
|
delete session._onStep;
|
|
613
|
+
delete session._currentRunIdentity;
|
|
614
|
+
delete session._runApprovalRequired;
|
|
615
|
+
delete session._runApprovalResolved;
|
|
616
|
+
delete session._approvalTimeoutMs;
|
|
404
617
|
}
|
|
405
618
|
}
|
|
406
619
|
|
|
@@ -408,19 +621,27 @@ async function runRuntime(argv, agent) {
|
|
|
408
621
|
host,
|
|
409
622
|
port,
|
|
410
623
|
store,
|
|
411
|
-
|
|
624
|
+
getContext: getWorkspaceContext,
|
|
412
625
|
run: executeRun,
|
|
413
|
-
cancel: () => emitRuntimeLog(session, 'runtime: cancel requested'),
|
|
626
|
+
cancel: (context) => emitRuntimeLog(context.session, 'runtime: cancel requested'),
|
|
627
|
+
resume: ({ workspace }) => recoverRuntime({ workspace, manual: true }),
|
|
628
|
+
approve: async ({ workspace, runId, itemId, approvalId }) => {
|
|
629
|
+
const context = await getWorkspaceContext(workspace);
|
|
630
|
+
return context.approvalManager?.approve({ runId, itemId, approvalId }) ?? { approved: false };
|
|
631
|
+
},
|
|
414
632
|
token: auth.token,
|
|
415
633
|
});
|
|
416
|
-
|
|
634
|
+
const recovery = await recoverRuntime();
|
|
417
635
|
|
|
418
636
|
console.log(`wiki-manager runtime listening on http://${host}:${port}`);
|
|
419
637
|
console.log(`runtime state: ${store.dbPath}`);
|
|
638
|
+
if (recovery.resumed > 0 || recovery.interrupted > 0) {
|
|
639
|
+
console.log(`runtime recovery: resumed=${recovery.resumed} interrupted=${recovery.interrupted}`);
|
|
640
|
+
}
|
|
420
641
|
if (auth.tokenPath) console.log(`runtime token: ${auth.tokenPath}`);
|
|
421
642
|
|
|
422
643
|
const shutdown = async () => {
|
|
423
|
-
supervisor?.stop();
|
|
644
|
+
await Promise.all([...new Set(contexts.values())].map(async (v) => { (await v).supervisor?.stop(); }));
|
|
424
645
|
await serverHandle.close();
|
|
425
646
|
store.close();
|
|
426
647
|
process.exit(0);
|
package/src/commands/slash.js
CHANGED
|
@@ -490,7 +490,7 @@ function publishDocumentActivity(session, activity) {
|
|
|
490
490
|
return publishPayloadActivity(session, { _activity: activity }, { server: 'documents', tool: 'documents_convert_to_markdown' });
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
-
async function refreshMcpRuntimeStatus(session) {
|
|
493
|
+
export async function refreshMcpRuntimeStatus(session) {
|
|
494
494
|
session.mcp = buildMcpStatus(session);
|
|
495
495
|
if (!session.workspacePath) return null;
|
|
496
496
|
try {
|
package/src/core/agentEvents.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { normalizeActivity } from './activity.js';
|
|
2
|
-
import { syncActivitiesToPlan } from './plan.js';
|
|
2
|
+
import { attachActivityToExistingPlan, syncActivitiesToPlan } from './plan.js';
|
|
3
3
|
|
|
4
4
|
const SESSION_PROJECTION_EVENTS = new Set([
|
|
5
5
|
'run_started',
|
|
6
6
|
'plan_set',
|
|
7
7
|
'plan_step_updated',
|
|
8
8
|
'activity_upserted',
|
|
9
|
+
'run_evaluated',
|
|
10
|
+
'run_replanned',
|
|
11
|
+
'run_pending_approval',
|
|
12
|
+
'run_approved',
|
|
13
|
+
'tool_pending_approval',
|
|
14
|
+
'tool_approved',
|
|
15
|
+
'run_done',
|
|
9
16
|
'run_error',
|
|
10
17
|
'run_cancelled',
|
|
11
18
|
'runtime_log',
|
|
@@ -19,9 +26,16 @@ const PLAN_MUTATING_EVENTS = new Set([
|
|
|
19
26
|
'plan_set',
|
|
20
27
|
'plan_step_updated',
|
|
21
28
|
'activity_upserted',
|
|
29
|
+
'run_done',
|
|
22
30
|
]);
|
|
23
31
|
|
|
24
|
-
export function createAgentEvent(type, {
|
|
32
|
+
export function createAgentEvent(type, {
|
|
33
|
+
origin = 'system',
|
|
34
|
+
payload = {},
|
|
35
|
+
runId = null,
|
|
36
|
+
turnId = null,
|
|
37
|
+
workspace = null,
|
|
38
|
+
} = {}) {
|
|
25
39
|
return {
|
|
26
40
|
id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
|
|
27
41
|
ts: new Date().toISOString(),
|
|
@@ -29,12 +43,16 @@ export function createAgentEvent(type, { origin = 'system', payload = {}, runId
|
|
|
29
43
|
origin,
|
|
30
44
|
runId,
|
|
31
45
|
turnId,
|
|
46
|
+
workspace,
|
|
32
47
|
payload,
|
|
33
48
|
};
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
export function dispatchAgentEvent(session, event) {
|
|
37
|
-
const normalized =
|
|
52
|
+
const normalized = withSessionRunIdentity(
|
|
53
|
+
event.id && event.ts ? event : createAgentEvent(event.type, event),
|
|
54
|
+
session,
|
|
55
|
+
);
|
|
38
56
|
const tracksPlan = PLAN_MUTATING_EVENTS.has(normalized.type);
|
|
39
57
|
const previousPlan = tracksPlan ? JSON.stringify(session.headlessPlan ?? null) : null;
|
|
40
58
|
session.agentEvents ??= [];
|
|
@@ -52,6 +70,17 @@ export function dispatchAgentEvent(session, event) {
|
|
|
52
70
|
return normalized;
|
|
53
71
|
}
|
|
54
72
|
|
|
73
|
+
function withSessionRunIdentity(event, session) {
|
|
74
|
+
const identity = session?._currentRunIdentity;
|
|
75
|
+
if (!identity) return event;
|
|
76
|
+
return {
|
|
77
|
+
...event,
|
|
78
|
+
runId: event.runId ?? identity.runId ?? null,
|
|
79
|
+
turnId: event.turnId ?? identity.turnId ?? null,
|
|
80
|
+
workspace: event.workspace ?? identity.workspace ?? null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
export function reduceAgentEvents(events = []) {
|
|
56
85
|
const state = createProjectionState();
|
|
57
86
|
|
|
@@ -69,6 +98,9 @@ function createProjectionState() {
|
|
|
69
98
|
plan: null,
|
|
70
99
|
activities: {},
|
|
71
100
|
logs: [],
|
|
101
|
+
evaluation: null,
|
|
102
|
+
replans: [],
|
|
103
|
+
approvals: [],
|
|
72
104
|
summary: null,
|
|
73
105
|
status: 'idle',
|
|
74
106
|
};
|
|
@@ -81,6 +113,9 @@ function publicProjection(state) {
|
|
|
81
113
|
plan: state.plan ? state.plan.map((step) => ({ ...step })) : null,
|
|
82
114
|
activities: sortedActivities(state.activities).map((activity) => ({ ...activity })),
|
|
83
115
|
logs: [...state.logs],
|
|
116
|
+
evaluation: state.evaluation ? { ...state.evaluation } : null,
|
|
117
|
+
replans: state.replans.map((replan) => ({ ...replan, plan: [...(replan.plan ?? [])] })),
|
|
118
|
+
approvals: state.approvals.map((approval) => ({ ...approval })),
|
|
84
119
|
summary: state.summary,
|
|
85
120
|
status: state.status,
|
|
86
121
|
};
|
|
@@ -103,10 +138,13 @@ function applyEvent(state, event) {
|
|
|
103
138
|
switch (event.type) {
|
|
104
139
|
case 'run_started':
|
|
105
140
|
state.status = 'running';
|
|
106
|
-
state.plan =
|
|
141
|
+
state.plan = defaultRunPlan();
|
|
107
142
|
state.chain = [];
|
|
108
143
|
state.activities = {};
|
|
109
144
|
state.logs = [];
|
|
145
|
+
state.evaluation = null;
|
|
146
|
+
state.replans = [];
|
|
147
|
+
state.approvals = [];
|
|
110
148
|
state.summary = null;
|
|
111
149
|
return;
|
|
112
150
|
case 'user_message':
|
|
@@ -144,8 +182,50 @@ function applyEvent(state, event) {
|
|
|
144
182
|
state.summary = String(event.payload?.content ?? '');
|
|
145
183
|
if (state.summary) state.conversation.push({ role: 'assistant', content: state.summary });
|
|
146
184
|
return;
|
|
185
|
+
case 'run_evaluated':
|
|
186
|
+
state.evaluation = {
|
|
187
|
+
ok: event.payload?.ok === true,
|
|
188
|
+
reason: String(event.payload?.reason ?? ''),
|
|
189
|
+
suggestedAction: event.payload?.suggestedAction ?? null,
|
|
190
|
+
runId: event.runId ?? event.payload?.runId ?? null,
|
|
191
|
+
};
|
|
192
|
+
return;
|
|
193
|
+
case 'run_replanned':
|
|
194
|
+
state.replans.push({
|
|
195
|
+
reason: String(event.payload?.reason ?? ''),
|
|
196
|
+
plan: Array.isArray(event.payload?.plan) ? event.payload.plan.map(String) : [],
|
|
197
|
+
replansLeft: Number(event.payload?.replansLeft ?? 0),
|
|
198
|
+
runId: event.runId ?? event.payload?.runId ?? null,
|
|
199
|
+
});
|
|
200
|
+
return;
|
|
201
|
+
case 'run_pending_approval':
|
|
202
|
+
case 'tool_pending_approval':
|
|
203
|
+
upsertApproval(state, {
|
|
204
|
+
id: event.payload?.approvalId ?? event.payload?.runId ?? event.payload?.itemId ?? event.id,
|
|
205
|
+
scope: event.type === 'run_pending_approval' ? 'run' : 'tool',
|
|
206
|
+
status: 'pending_approval',
|
|
207
|
+
runId: event.runId ?? event.payload?.runId ?? null,
|
|
208
|
+
itemId: event.payload?.itemId ?? null,
|
|
209
|
+
reason: event.payload?.reason ?? null,
|
|
210
|
+
tool: event.payload?.tool ?? null,
|
|
211
|
+
plan: event.payload?.plan ?? null,
|
|
212
|
+
createdAt: event.ts,
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
case 'run_approved':
|
|
216
|
+
case 'tool_approved':
|
|
217
|
+
upsertApproval(state, {
|
|
218
|
+
id: event.payload?.approvalId ?? event.payload?.runId ?? event.payload?.itemId ?? event.id,
|
|
219
|
+
scope: event.type === 'run_approved' ? 'run' : 'tool',
|
|
220
|
+
status: 'approved',
|
|
221
|
+
runId: event.runId ?? event.payload?.runId ?? null,
|
|
222
|
+
itemId: event.payload?.itemId ?? null,
|
|
223
|
+
approvedAt: event.ts,
|
|
224
|
+
});
|
|
225
|
+
return;
|
|
147
226
|
case 'run_done':
|
|
148
227
|
state.status = 'done';
|
|
228
|
+
finishPendingPlanSteps(state.plan);
|
|
149
229
|
return;
|
|
150
230
|
case 'run_cancelled':
|
|
151
231
|
state.status = 'cancelled';
|
|
@@ -198,6 +278,26 @@ function finishToolCall(state, payload = {}) {
|
|
|
198
278
|
if (!existing) state.chain.push(step);
|
|
199
279
|
}
|
|
200
280
|
|
|
281
|
+
function upsertApproval(state, next) {
|
|
282
|
+
const index = state.approvals.findIndex((approval) => approval.id === next.id);
|
|
283
|
+
if (index === -1) {
|
|
284
|
+
state.approvals.push(next);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
state.approvals[index] = {
|
|
288
|
+
...state.approvals[index],
|
|
289
|
+
...next,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function finishPendingPlanSteps(plan) {
|
|
294
|
+
for (const step of plan ?? []) {
|
|
295
|
+
if (step.status === 'running' || step.status === 'pending') {
|
|
296
|
+
step.status = 'done';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
201
301
|
function upsertActivity(state, rawActivity) {
|
|
202
302
|
const activity = normalizeActivity(rawActivity);
|
|
203
303
|
if (!activity) return;
|
|
@@ -211,6 +311,10 @@ function upsertActivity(state, rawActivity) {
|
|
|
211
311
|
|
|
212
312
|
function ensurePlanFromActivityProjection(state, activity) {
|
|
213
313
|
const actKey = activity.key ?? null;
|
|
314
|
+
if (state.plan?.some((step) => step.owner === 'orchestrator')) {
|
|
315
|
+
attachActivityToExistingPlan(state.plan, activity);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
214
318
|
if (state.plan && actKey !== null && state.plan[0]?._activityKey === actKey) return;
|
|
215
319
|
const steps = activity.plan?.steps;
|
|
216
320
|
if (Array.isArray(steps) && steps.length > 0) {
|
|
@@ -219,6 +323,8 @@ function ensurePlanFromActivityProjection(state, activity) {
|
|
|
219
323
|
id: step.id ?? null,
|
|
220
324
|
description: step.label,
|
|
221
325
|
status: 'pending',
|
|
326
|
+
owner: 'activity',
|
|
327
|
+
ownerActivityKey: activity.key,
|
|
222
328
|
_activityKey: activity.key,
|
|
223
329
|
}));
|
|
224
330
|
return;
|
|
@@ -228,12 +334,16 @@ function ensurePlanFromActivityProjection(state, activity) {
|
|
|
228
334
|
id: null,
|
|
229
335
|
description: activity.label,
|
|
230
336
|
status: 'pending',
|
|
337
|
+
owner: 'activity',
|
|
338
|
+
ownerActivityKey: activity.key,
|
|
231
339
|
_activityKey: activity.key,
|
|
232
340
|
}];
|
|
233
341
|
}
|
|
234
342
|
|
|
235
343
|
function normalizePlan(steps, payload = {}) {
|
|
236
344
|
if (!Array.isArray(steps)) return null;
|
|
345
|
+
const owner = payload.owner ?? 'orchestrator';
|
|
346
|
+
const ownerActivityKey = payload.ownerActivityKey ?? payload.activityKey ?? null;
|
|
237
347
|
return steps.map((raw, i) => {
|
|
238
348
|
const item = typeof raw === 'string' ? { description: raw } : (raw ?? {});
|
|
239
349
|
return {
|
|
@@ -241,11 +351,21 @@ function normalizePlan(steps, payload = {}) {
|
|
|
241
351
|
id: item.id ?? null,
|
|
242
352
|
description: String(item.description ?? item.label ?? item.name ?? `Step ${i + 1}`),
|
|
243
353
|
status: item.status ?? 'pending',
|
|
354
|
+
owner: item.owner ?? owner,
|
|
355
|
+
ownerActivityKey: item.ownerActivityKey ?? ownerActivityKey,
|
|
244
356
|
_activityKey: item._activityKey ?? payload.activityKey ?? null,
|
|
245
357
|
};
|
|
246
358
|
});
|
|
247
359
|
}
|
|
248
360
|
|
|
361
|
+
function defaultRunPlan() {
|
|
362
|
+
return [
|
|
363
|
+
{ step: 1, id: 'analyze', description: 'Analyze the request', status: 'running', owner: 'orchestrator', ownerActivityKey: null, _activityKey: null },
|
|
364
|
+
{ step: 2, id: 'execute', description: 'Execute the required actions', status: 'pending', owner: 'orchestrator', ownerActivityKey: null, _activityKey: null },
|
|
365
|
+
{ step: 3, id: 'verify', description: 'Verify the result', status: 'pending', owner: 'orchestrator', ownerActivityKey: null, _activityKey: null },
|
|
366
|
+
];
|
|
367
|
+
}
|
|
368
|
+
|
|
249
369
|
function updatePlanStep(plan, payload) {
|
|
250
370
|
if (!plan) return;
|
|
251
371
|
const step = plan.find((item) => item.step === Number(payload.step));
|