@dotdrelle/wiki-manager 0.6.47 → 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/bin/wiki-manager.js +2 -2
- package/docker-compose.yml +2 -0
- 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 +459 -131
- package/src/commands/slash.js +1 -1
- package/src/core/activity.js +14 -0
- package/src/core/agentEvents.js +148 -6
- package/src/core/agentEvents.test.js +145 -4
- package/src/core/agentLoop.js +167 -0
- package/src/core/agentLoop.test.js +85 -0
- package/src/core/cacert.js +4 -3
- package/src/core/dockerCompose.test.js +14 -0
- package/src/core/documentIntake.test.js +7 -7
- package/src/core/env.js +6 -0
- package/src/core/jobQueue.js +47 -16
- 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.js +27 -0
- package/src/core/queueStore.test.js +32 -0
- package/src/core/wikiWorkspace.test.js +24 -0
- package/src/core/workspaces.js +8 -1
- package/src/runtime/approvals.js +113 -0
- package/src/runtime/auth.js +35 -0
- package/src/runtime/auth.test.js +29 -0
- package/src/runtime/client.js +154 -0
- package/src/runtime/lifecycle.js +84 -0
- package/src/runtime/queueStore.js +6 -0
- package/src/runtime/runner.js +413 -0
- package/src/runtime/runner.test.js +270 -0
- package/src/runtime/server.js +216 -0
- package/src/runtime/server.test.js +308 -0
- package/src/runtime/store.js +431 -0
- package/src/runtime/store.test.js +437 -0
- package/src/runtime/supervisor.js +104 -0
- package/src/runtime/supervisor.test.js +240 -0
- package/src/shell/RightPane.tsx +1 -1
- package/src/shell/repl.js +148 -18
- package/src/shell/repl.test.js +38 -0
- package/src/shell/tui.tsx +4 -1
- package/src/shell/useAgent.ts +20 -2
- package/src/shell/useSession.ts +153 -13
- package/wiki-workspace +221 -22
package/src/cli/wiki-manager.js
CHANGED
|
@@ -4,19 +4,22 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { loadManagerEnv } from '../core/env.js';
|
|
6
6
|
loadManagerEnv();
|
|
7
|
-
import { createAgentGraph
|
|
8
|
-
import { handleSlashCommand, printHelp, printVersion } from '../commands/slash.js';
|
|
7
|
+
import { createAgentGraph } from '../agent/graph.js';
|
|
8
|
+
import { handleSlashCommand, printHelp, printVersion, refreshMcpRuntimeStatus } from '../commands/slash.js';
|
|
9
9
|
import { runShell } from '../shell/repl.js';
|
|
10
10
|
import { runChecks } from '../core/startupCheck.js';
|
|
11
11
|
import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
|
|
12
|
-
import { extractActivity, parseJsonText, sessionActivities } from '../core/activity.js';
|
|
13
|
-
import {
|
|
12
|
+
import { extractActivity, parseJsonText, sessionActivities, terminalFailures } from '../core/activity.js';
|
|
13
|
+
import { syncActivitiesToPlan, formatPlanStatus } from '../core/plan.js';
|
|
14
14
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
15
|
+
import { runAgentTurn, runAgenticLoop } from '../core/agentLoop.js';
|
|
16
|
+
// Runtime modules use node:sqlite (Node.js built-in unavailable in Bun).
|
|
17
|
+
// They are imported dynamically so the shell / TUI path never loads them.
|
|
15
18
|
|
|
16
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
20
|
const packageJsonPath = resolve(__dirname, '../../package.json');
|
|
18
21
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
19
|
-
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'];
|
|
20
23
|
|
|
21
24
|
function valueAfter(argv, flag) {
|
|
22
25
|
const index = argv.indexOf(flag);
|
|
@@ -39,6 +42,7 @@ function createSession() {
|
|
|
39
42
|
packageJson,
|
|
40
43
|
conversations: { __global__: [] },
|
|
41
44
|
activities: {},
|
|
45
|
+
jobQueue: [],
|
|
42
46
|
productionActivity: null,
|
|
43
47
|
headlessPlan: null,
|
|
44
48
|
};
|
|
@@ -57,20 +61,6 @@ async function writeHeadlessLog(session, lines, explicitPath) {
|
|
|
57
61
|
return logPath;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
|
-
function activitySnapshot(session) {
|
|
61
|
-
return new Set(sessionActivities(session).map((a) => a.key));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function newNonTerminalActivities(snapshotBefore, session) {
|
|
65
|
-
return sessionActivities(session).filter((a) => !snapshotBefore.has(a.key) && !a.terminal);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function terminalFailures(activities) {
|
|
69
|
-
return activities.filter(
|
|
70
|
-
(a) => a.terminal && ['failed', 'error', 'cancelled', 'canceled'].includes(String(a.status).toLowerCase()),
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
64
|
|
|
75
65
|
async function runHeadlessActivityLoop(session, log, { wait, timeoutMs }) {
|
|
76
66
|
if (!wait) return { exitCode: 0, completed: [], timedOut: false };
|
|
@@ -150,118 +140,54 @@ async function runHeadlessActivityLoop(session, log, { wait, timeoutMs }) {
|
|
|
150
140
|
return { exitCode: 1, completed, timedOut: true };
|
|
151
141
|
}
|
|
152
142
|
|
|
153
|
-
async function runHeadlessAgentTurn(agent, session, input, log, messages = []) {
|
|
154
|
-
session.packageJson = packageJson;
|
|
155
|
-
let streamedContent = '';
|
|
156
|
-
session._onStream = (delta) => { streamedContent += delta; };
|
|
157
|
-
session._onStreamReset = () => { streamedContent = ''; };
|
|
158
|
-
let result;
|
|
159
|
-
try {
|
|
160
|
-
result = await agent.invoke({ input, session, messages });
|
|
161
|
-
} finally {
|
|
162
|
-
delete session._onStream;
|
|
163
|
-
delete session._onStreamReset;
|
|
164
|
-
}
|
|
165
|
-
if (result.streamedInline) {
|
|
166
|
-
return streamedContent.trim() || buildLimitedAgentResponse({ input, session }, 'LLM stream ended without content');
|
|
167
|
-
}
|
|
168
|
-
if (result.response != null) return result.response;
|
|
169
|
-
if (result.readyToStream && session.llm?.stream) {
|
|
170
|
-
const { system, messages: streamMessages = [] } = result.streamContext ?? {};
|
|
171
|
-
let content = '';
|
|
172
|
-
for await (const delta of session.llm.stream({
|
|
173
|
-
system: system ?? buildAgentSystemPrompt({ input, session }),
|
|
174
|
-
messages: streamMessages,
|
|
175
|
-
})) {
|
|
176
|
-
content += delta;
|
|
177
|
-
}
|
|
178
|
-
return content.trim() || buildLimitedAgentResponse({ input, session }, 'LLM stream ended without content');
|
|
179
|
-
}
|
|
180
|
-
return buildLimitedAgentResponse({ input, session });
|
|
181
|
-
}
|
|
182
|
-
|
|
183
143
|
async function runHeadlessAgenticLoop(agent, session, initialInput, log, { timeoutMs, maxTurns }) {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
} else if (turn === 1 && session.headlessPlan) {
|
|
215
|
-
log.push(`agentic-loop: plan set via tool (${session.headlessPlan.length} steps)`);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const newPending = newNonTerminalActivities(snapshot, session);
|
|
219
|
-
if (newPending.length === 0) {
|
|
220
|
-
const pendingSteps = (session.headlessPlan ?? []).filter((s) => s.status === 'pending');
|
|
221
|
-
if (pendingSteps.length === 0) {
|
|
222
|
-
log.push('agentic-loop: no new non-terminal activities — plan complete');
|
|
223
|
-
console.log('[headless] Plan complete.');
|
|
224
|
-
return { exitCode: 0 };
|
|
225
|
-
}
|
|
144
|
+
const result = await runAgenticLoop(agent, session, initialInput, {
|
|
145
|
+
timeoutMs,
|
|
146
|
+
maxTurns,
|
|
147
|
+
waitForActivities: async (turnSession, _startedActivities, waitOptions) => {
|
|
148
|
+
const waitResult = await runHeadlessActivityLoop(turnSession, log, { wait: true, timeoutMs: waitOptions.timeoutMs });
|
|
149
|
+
return {
|
|
150
|
+
ok: waitResult.exitCode === 0 && !waitResult.timedOut,
|
|
151
|
+
...waitResult,
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
onTurnStart: ({ turn, maxTurns: totalTurns }) => {
|
|
155
|
+
log.push(`agentic-loop: turn ${turn}/${totalTurns}`);
|
|
156
|
+
console.log(`[headless] Agent turn ${turn}/${totalTurns}…`);
|
|
157
|
+
},
|
|
158
|
+
onTurnResponse: ({ turn, response }) => {
|
|
159
|
+
log.push(`agentic-loop: turn ${turn} response:`);
|
|
160
|
+
log.push(response);
|
|
161
|
+
console.log(response);
|
|
162
|
+
},
|
|
163
|
+
onPlanExtracted: ({ steps }) => {
|
|
164
|
+
log.push(`agentic-loop: plan extracted from text (${steps.length} steps, fallback)`);
|
|
165
|
+
},
|
|
166
|
+
onPlanAlreadySet: ({ steps }) => {
|
|
167
|
+
log.push(`agentic-loop: plan set via tool (${steps.length} steps)`);
|
|
168
|
+
},
|
|
169
|
+
onComplete: () => {
|
|
170
|
+
log.push('agentic-loop: no new non-terminal activities — plan complete');
|
|
171
|
+
console.log('[headless] Plan complete.');
|
|
172
|
+
},
|
|
173
|
+
onPendingSteps: ({ pendingSteps }) => {
|
|
226
174
|
log.push(`agentic-loop: no new async activity, ${pendingSteps.length} pending step(s) remain`);
|
|
227
175
|
if (session.headlessPlan) log.push(`agentic-loop: plan status:\n${formatPlanStatus(session.headlessPlan)}`);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (timedOut || exitCode !== 0) return { exitCode };
|
|
245
|
-
|
|
246
|
-
const summary = formatCompletedActivities(completed);
|
|
247
|
-
log.push(`agentic-loop: completed activities:\n${summary}`);
|
|
248
|
-
if (session.headlessPlan) log.push(`agentic-loop: plan status:\n${formatPlanStatus(session.headlessPlan)}`);
|
|
249
|
-
currentInput = [
|
|
250
|
-
'Original task:',
|
|
251
|
-
initialInput,
|
|
252
|
-
'',
|
|
253
|
-
session.headlessPlan ? `Plan status:\n${formatPlanStatus(session.headlessPlan)}\n` : null,
|
|
254
|
-
'Completed activities:',
|
|
255
|
-
summary,
|
|
256
|
-
'',
|
|
257
|
-
'Continue the original plan. Start the next required step only.',
|
|
258
|
-
'If all steps are complete, provide the final summary.',
|
|
259
|
-
].filter(Boolean).join('\n');
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
log.push(`agentic-loop: max turns (${maxTurns}) reached without completing`);
|
|
263
|
-
console.error(`[headless] Max agent turns (${maxTurns}) reached.`);
|
|
264
|
-
return { exitCode: 1 };
|
|
176
|
+
},
|
|
177
|
+
onActivitiesStarted: ({ activities }) => {
|
|
178
|
+
log.push(`agentic-loop: ${activities.length} new job(s) started, waiting…`);
|
|
179
|
+
},
|
|
180
|
+
onActivitiesCompleted: ({ summary }) => {
|
|
181
|
+
log.push(`agentic-loop: completed activities:\n${summary}`);
|
|
182
|
+
if (session.headlessPlan) log.push(`agentic-loop: plan status:\n${formatPlanStatus(session.headlessPlan)}`);
|
|
183
|
+
},
|
|
184
|
+
onMaxTurns: ({ maxTurns: totalTurns }) => {
|
|
185
|
+
log.push(`agentic-loop: max turns (${totalTurns}) reached without completing`);
|
|
186
|
+
console.error(`[headless] Max agent turns (${totalTurns}) reached.`);
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return { exitCode: result.ok ? 0 : (result.waitResult?.exitCode ?? 1) };
|
|
265
191
|
}
|
|
266
192
|
|
|
267
193
|
async function runHeadless(argv, agent) {
|
|
@@ -326,7 +252,7 @@ async function runHeadless(argv, agent) {
|
|
|
326
252
|
if (useAgenticLoop) {
|
|
327
253
|
({ exitCode } = await runHeadlessAgenticLoop(agent, session, input, log, { timeoutMs, maxTurns }));
|
|
328
254
|
} else {
|
|
329
|
-
const response = await
|
|
255
|
+
const response = await runAgentTurn(agent, session, input);
|
|
330
256
|
log.push('response:');
|
|
331
257
|
log.push(response);
|
|
332
258
|
console.log(response);
|
|
@@ -345,7 +271,393 @@ async function runHeadless(argv, agent) {
|
|
|
345
271
|
}
|
|
346
272
|
}
|
|
347
273
|
|
|
274
|
+
async function runRuntime(argv, agent) {
|
|
275
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
276
|
+
console.log([
|
|
277
|
+
'Usage: wiki-manager runtime [--host <host>] [--port <port>] [--state-dir <dir>]',
|
|
278
|
+
'',
|
|
279
|
+
'Runs the local agentic runtime used by wiki-manager Shell and llm-wiki serve.',
|
|
280
|
+
'',
|
|
281
|
+
'Defaults:',
|
|
282
|
+
' --host 0.0.0.0',
|
|
283
|
+
' --port 7788',
|
|
284
|
+
' --state-dir .wiki-manager',
|
|
285
|
+
].join('\n'));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const { defaultRuntimeStateDir, openRuntimeStore, RECOVERABLE_QUEUE_STATUSES } = await import('../runtime/store.js');
|
|
289
|
+
const { startRuntimeServer } = await import('../runtime/server.js');
|
|
290
|
+
const { emitRuntimeLog, startActivitySupervisor } = await import('../runtime/supervisor.js');
|
|
291
|
+
const { resolveRuntimeAuthToken } = await import('../runtime/auth.js');
|
|
292
|
+
const { createSqliteQueueStore } = await import('../runtime/queueStore.js');
|
|
293
|
+
const { createApprovalManager } = await import('../runtime/approvals.js');
|
|
294
|
+
const { runRuntimeAgenticWorkflow } = await import('../runtime/runner.js');
|
|
295
|
+
|
|
296
|
+
const host = valueAfter(argv, '--host') ?? process.env.WIKI_MANAGER_RUNTIME_HOST ?? '0.0.0.0';
|
|
297
|
+
const port = Number(valueAfter(argv, '--port') ?? process.env.WIKI_MANAGER_RUNTIME_PORT ?? 7788);
|
|
298
|
+
const stateDir = valueAfter(argv, '--state-dir') ?? defaultRuntimeStateDir();
|
|
299
|
+
const auth = resolveRuntimeAuthToken({ host, stateDir });
|
|
300
|
+
if (auth.token) process.env.WIKI_MANAGER_RUNTIME_TOKEN = auth.token;
|
|
301
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
302
|
+
throw new Error(`Invalid runtime port: ${port}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const store = openRuntimeStore({ stateDir });
|
|
306
|
+
let serverHandle = null;
|
|
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
|
+
}
|
|
531
|
+
|
|
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
|
+
}
|
|
541
|
+
|
|
542
|
+
async function executeRun(context, body, { signal } = {}) {
|
|
543
|
+
const session = context.session;
|
|
544
|
+
const supervisor = context.supervisor;
|
|
545
|
+
const input = String(body.input ?? body.prompt ?? '').trim();
|
|
546
|
+
const workspace = body.workspace ? String(body.workspace).trim() : null;
|
|
547
|
+
const timeoutMs = (Number.isFinite(Number(body.timeout)) ? Math.max(1, Number(body.timeout)) : 3600) * 1000;
|
|
548
|
+
const maxTurns = Number.isFinite(Number(body.maxTurns)) ? Math.max(1, Number(body.maxTurns)) : 20;
|
|
549
|
+
const maxReplans = Number.isFinite(Number(body.replans)) ? Math.max(0, Math.floor(Number(body.replans))) : undefined;
|
|
550
|
+
const runId = String(body.runId);
|
|
551
|
+
try {
|
|
552
|
+
if (workspace && session.workspace !== workspace) {
|
|
553
|
+
const result = await handleSlashCommand(`/use ${workspace}`, { packageJson, session });
|
|
554
|
+
if (!session.workspacePath) throw new Error(result.output || `Workspace not loaded: ${workspace}`);
|
|
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
|
+
};
|
|
562
|
+
dispatchAgentEvent(session, createAgentEvent('run_started', {
|
|
563
|
+
origin: 'runtime',
|
|
564
|
+
runId,
|
|
565
|
+
payload: { input, workspace: session._currentRunIdentity.workspace },
|
|
566
|
+
}));
|
|
567
|
+
dispatchAgentEvent(session, createAgentEvent('user_message', {
|
|
568
|
+
origin: 'user',
|
|
569
|
+
runId,
|
|
570
|
+
payload: { content: input },
|
|
571
|
+
}));
|
|
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;
|
|
578
|
+
supervisor?.setRunSignal(signal);
|
|
579
|
+
session._onStep = (message) => emitRuntimeLog(session, message);
|
|
580
|
+
await runRuntimeAgenticWorkflow(agent, session, input, {
|
|
581
|
+
signal,
|
|
582
|
+
timeoutMs,
|
|
583
|
+
maxTurns,
|
|
584
|
+
runId,
|
|
585
|
+
pollBusy: supervisor?.pollBusy,
|
|
586
|
+
evaluate: body.evaluate !== false,
|
|
587
|
+
...(maxReplans === undefined ? {} : { maxReplans }),
|
|
588
|
+
});
|
|
589
|
+
} catch (err) {
|
|
590
|
+
if (err?.name === 'AbortError') {
|
|
591
|
+
dispatchAgentEvent(session, createAgentEvent('run_cancelled', {
|
|
592
|
+
origin: 'runtime',
|
|
593
|
+
runId,
|
|
594
|
+
payload: {
|
|
595
|
+
runId,
|
|
596
|
+
message: 'Agent run cancelled.',
|
|
597
|
+
},
|
|
598
|
+
}));
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
dispatchAgentEvent(session, createAgentEvent('run_error', {
|
|
602
|
+
origin: 'runtime',
|
|
603
|
+
runId,
|
|
604
|
+
payload: {
|
|
605
|
+
runId,
|
|
606
|
+
message: err instanceof Error ? err.message : String(err),
|
|
607
|
+
},
|
|
608
|
+
}));
|
|
609
|
+
} finally {
|
|
610
|
+
supervisor?.setRunSignal(null);
|
|
611
|
+
delete session._abortSignal;
|
|
612
|
+
delete session._onStep;
|
|
613
|
+
delete session._currentRunIdentity;
|
|
614
|
+
delete session._runApprovalRequired;
|
|
615
|
+
delete session._runApprovalResolved;
|
|
616
|
+
delete session._approvalTimeoutMs;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
serverHandle = await startRuntimeServer({
|
|
621
|
+
host,
|
|
622
|
+
port,
|
|
623
|
+
store,
|
|
624
|
+
getContext: getWorkspaceContext,
|
|
625
|
+
run: executeRun,
|
|
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
|
+
},
|
|
632
|
+
token: auth.token,
|
|
633
|
+
});
|
|
634
|
+
const recovery = await recoverRuntime();
|
|
635
|
+
|
|
636
|
+
console.log(`wiki-manager runtime listening on http://${host}:${port}`);
|
|
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
|
+
}
|
|
641
|
+
if (auth.tokenPath) console.log(`runtime token: ${auth.tokenPath}`);
|
|
642
|
+
|
|
643
|
+
const shutdown = async () => {
|
|
644
|
+
await Promise.all([...new Set(contexts.values())].map(async (v) => { (await v).supervisor?.stop(); }));
|
|
645
|
+
await serverHandle.close();
|
|
646
|
+
store.close();
|
|
647
|
+
process.exit(0);
|
|
648
|
+
};
|
|
649
|
+
process.once('SIGINT', shutdown);
|
|
650
|
+
process.once('SIGTERM', shutdown);
|
|
651
|
+
await new Promise(() => {});
|
|
652
|
+
}
|
|
653
|
+
|
|
348
654
|
export async function runCli(argv) {
|
|
655
|
+
if (argv[0] === 'runtime') {
|
|
656
|
+
const agent = createAgentGraph();
|
|
657
|
+
await runRuntime(argv.slice(1), agent);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
349
661
|
if (argv.includes('--setup-wizard')) {
|
|
350
662
|
if (!process.versions.bun) {
|
|
351
663
|
throw new Error('Setup wizard requires Bun. Run: bun ./bin/wiki-manager.js --setup-wizard');
|
|
@@ -396,9 +708,25 @@ export async function runCli(argv) {
|
|
|
396
708
|
const { runOpenTuiShell, runStartupWizard } = await import('../shell/tui.tsx');
|
|
397
709
|
const gaps = await runChecks();
|
|
398
710
|
if (gaps.length > 0) await runStartupWizard(gaps);
|
|
399
|
-
|
|
711
|
+
let runtime = null;
|
|
712
|
+
try {
|
|
713
|
+
const { ensureRuntime } = await import('../runtime/lifecycle.js');
|
|
714
|
+
runtime = await ensureRuntime();
|
|
715
|
+
} catch (err) {
|
|
716
|
+
console.error(`Runtime unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
717
|
+
}
|
|
718
|
+
await runOpenTuiShell({ agent, packageJson, runtime });
|
|
400
719
|
return;
|
|
401
720
|
}
|
|
402
721
|
|
|
403
|
-
|
|
722
|
+
let runtime = null;
|
|
723
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
724
|
+
try {
|
|
725
|
+
const { ensureRuntime } = await import('../runtime/lifecycle.js');
|
|
726
|
+
runtime = await ensureRuntime();
|
|
727
|
+
} catch (err) {
|
|
728
|
+
console.error(`Runtime unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
await runShell({ agent, packageJson, runtime });
|
|
404
732
|
}
|
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/activity.js
CHANGED
|
@@ -222,6 +222,20 @@ export function formatActivitySummary(source, action, resultText) {
|
|
|
222
222
|
return usefulLine ? `${source}.${action}: ${usefulLine.slice(0, 120)}` : null;
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
export function activitySnapshot(session) {
|
|
226
|
+
return new Set(sessionActivities(session).map((a) => a.key));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function newNonTerminalActivities(snapshotBefore, session) {
|
|
230
|
+
return sessionActivities(session).filter((a) => !snapshotBefore.has(a.key) && !a.terminal);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function terminalFailures(activities) {
|
|
234
|
+
return activities.filter(
|
|
235
|
+
(a) => a.terminal && ['failed', 'error', 'cancelled', 'canceled'].includes(String(a.status).toLowerCase()),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
225
239
|
export function formatActivityError(source, action, err) {
|
|
226
240
|
const message = err instanceof Error ? err.message : String(err);
|
|
227
241
|
const lines = message
|