@dotdrelle/wiki-manager 0.6.47 → 0.7.3
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/bin/wiki-manager.js +2 -2
- package/docker-compose.yml +25 -1
- package/package.json +2 -2
- package/src/cli/wiki-manager.js +236 -129
- package/src/core/activity.js +14 -0
- package/src/core/agentEvents.js +24 -2
- package/src/core/agentLoop.js +164 -0
- package/src/core/agentLoop.test.js +85 -0
- package/src/core/cacert.js +4 -3
- package/src/core/compose.js +2 -1
- 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 +18 -4
- package/src/core/mcp.js +1 -1
- package/src/core/queueStore.js +27 -0
- package/src/core/queueStore.test.js +31 -0
- package/src/core/workspaces.js +8 -1
- package/src/runtime/auth.js +35 -0
- package/src/runtime/auth.test.js +21 -0
- package/src/runtime/client.js +108 -0
- package/src/runtime/lifecycle.js +60 -0
- package/src/runtime/queueStore.js +6 -0
- package/src/runtime/runner.js +73 -0
- package/src/runtime/server.js +160 -0
- package/src/runtime/server.test.js +53 -0
- package/src/runtime/store.js +292 -0
- package/src/runtime/store.test.js +103 -0
- package/src/runtime/supervisor.js +98 -0
- package/src/runtime/supervisor.test.js +99 -0
- package/src/shell/repl.js +131 -17
- 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 +146 -11
- package/wiki-workspace +23 -18
package/bin/wiki-manager.js
CHANGED
|
@@ -80,8 +80,8 @@ async function main() {
|
|
|
80
80
|
// Fallback for already-bootstrapped direct invocations; the shell wrapper
|
|
81
81
|
// exports these before Bun starts.
|
|
82
82
|
if (parsed.cacert) Object.assign(process.env, cacertEnvVars(parsed.cacert));
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
const interactive = process.stdout.isTTY && process.stdin.isTTY && argv[0] !== 'runtime' && !argv.includes('--setup-wizard') && !argv.includes('--headless') && !argv.includes('--once') && !argv.includes('--version') && !argv.includes('-v') && !argv.includes('--help') && !argv.includes('-h');
|
|
84
|
+
if (interactive || argv.includes('--setup-wizard')) await import('@opentui/solid/preload');
|
|
85
85
|
if (interactive) process.stdout.write('Starting wiki-manager…\r');
|
|
86
86
|
const { runCli } = await import('../src/cli/wiki-manager.js');
|
|
87
87
|
await runCli(argv);
|
package/docker-compose.yml
CHANGED
|
@@ -62,6 +62,8 @@ services:
|
|
|
62
62
|
- DOCUMENT_MAX_UPLOAD_BYTES=${DOCUMENT_MAX_UPLOAD_BYTES:-52428800}
|
|
63
63
|
- WIKI_MCP_PROXY_URL=http://host.docker.internal:${WIKI_MCP_PORT:-3101}/mcp
|
|
64
64
|
- PRODUCTION_MCP_PROXY_URL=http://host.docker.internal:${PRODUCTION_MCP_PORT:-3102}/mcp/
|
|
65
|
+
- WIKI_MANAGER_RUNTIME_URL=http://host.docker.internal:7788
|
|
66
|
+
- WIKI_MANAGER_RUNTIME_TOKEN=${WIKI_MANAGER_RUNTIME_TOKEN:-}
|
|
65
67
|
# HTTPS — set paths inside the container (e.g. /certs/server.crt) and uncomment the volume above
|
|
66
68
|
#- WIKI_SERVE_TLS_CERT_PATH=/certs/server.crt
|
|
67
69
|
#- WIKI_SERVE_TLS_KEY_PATH=/certs/server.key
|
|
@@ -95,6 +97,25 @@ services:
|
|
|
95
97
|
- host.docker.internal:host-gateway
|
|
96
98
|
restart: unless-stopped
|
|
97
99
|
|
|
100
|
+
# ── wiki-manager runtime ─────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
agent-runtime:
|
|
103
|
+
image: dotdrelle/llm-wiki-manager:latest
|
|
104
|
+
labels:
|
|
105
|
+
wiki-manager.description: "Agentic runtime — runs, plan, activities, queue."
|
|
106
|
+
command: runtime --host 0.0.0.0 --port 7788 --state-dir /state
|
|
107
|
+
volumes:
|
|
108
|
+
- ./.wiki-manager:/state
|
|
109
|
+
- ${WIKI_WORKSPACES_DIR:-./workspaces}:/workspaces
|
|
110
|
+
environment:
|
|
111
|
+
- WIKI_WORKSPACES_DIR=/workspaces
|
|
112
|
+
- WIKI_MANAGER_RUNTIME_TOKEN=${WIKI_MANAGER_RUNTIME_TOKEN:-}
|
|
113
|
+
ports:
|
|
114
|
+
- '127.0.0.1:7788:7788'
|
|
115
|
+
extra_hosts:
|
|
116
|
+
- host.docker.internal:host-gateway
|
|
117
|
+
restart: unless-stopped
|
|
118
|
+
|
|
98
119
|
# ── agent-wiki-production ─────────────────────────────────────────────────
|
|
99
120
|
|
|
100
121
|
production-mcp:
|
|
@@ -119,7 +140,7 @@ services:
|
|
|
119
140
|
x-wiki-manager:
|
|
120
141
|
service-aliases:
|
|
121
142
|
all:
|
|
122
|
-
targets: [serve, mcp-http, production-mcp]
|
|
143
|
+
targets: [serve, mcp-http, agent-runtime, production-mcp]
|
|
123
144
|
description: "Full workspace service set."
|
|
124
145
|
ui:
|
|
125
146
|
targets: [serve]
|
|
@@ -130,6 +151,9 @@ x-wiki-manager:
|
|
|
130
151
|
mcp:
|
|
131
152
|
targets: [mcp-http]
|
|
132
153
|
description: "Alias for mcp-http: wiki MCP server."
|
|
154
|
+
runtime:
|
|
155
|
+
targets: [agent-runtime]
|
|
156
|
+
description: "Alias for agent-runtime: agentic runtime."
|
|
133
157
|
production:
|
|
134
158
|
targets: [production-mcp]
|
|
135
159
|
description: "Alias for production-mcp: production jobs."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
|
|
5
5
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
6
6
|
"author": "dotrelle",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"start": "bun ./bin/wiki-manager.js",
|
|
14
|
-
"test": "node --test src/core/activity.test.js src/core/agentEvents.test.js src/core/plan.test.js src/core/mcp.test.js src/core/documentIntake.test.js src/core/wikirc.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/commands/slash.test.js",
|
|
14
|
+
"test": "node --test src/core/activity.test.js src/core/agentEvents.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/wikirc.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/commands/slash.test.js src/shell/repl.test.js src/runtime/store.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/auth.test.js",
|
|
15
15
|
"check": "bun ./bin/wiki-manager.js --version && bun ./bin/wiki-manager.js --help && bun ./bin/wiki-manager.js --once \"verifie le mode agent\""
|
|
16
16
|
},
|
|
17
17
|
"engines": {
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { dirname, join, resolve } from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { loadManagerEnv } from '../core/env.js';
|
|
6
7
|
loadManagerEnv();
|
|
7
|
-
import { createAgentGraph
|
|
8
|
+
import { createAgentGraph } from '../agent/graph.js';
|
|
8
9
|
import { handleSlashCommand, printHelp, printVersion } from '../commands/slash.js';
|
|
9
10
|
import { runShell } from '../shell/repl.js';
|
|
10
11
|
import { runChecks } from '../core/startupCheck.js';
|
|
11
12
|
import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
|
|
12
|
-
import { extractActivity, parseJsonText, sessionActivities } from '../core/activity.js';
|
|
13
|
-
import {
|
|
13
|
+
import { extractActivity, parseJsonText, sessionActivities, terminalFailures } from '../core/activity.js';
|
|
14
|
+
import { syncActivitiesToPlan, formatPlanStatus } from '../core/plan.js';
|
|
14
15
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
16
|
+
import { runAgentTurn, runAgenticLoop } from '../core/agentLoop.js';
|
|
17
|
+
// Runtime modules use node:sqlite (Node.js built-in unavailable in Bun).
|
|
18
|
+
// They are imported dynamically so the shell / TUI path never loads them.
|
|
15
19
|
|
|
16
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
21
|
const packageJsonPath = resolve(__dirname, '../../package.json');
|
|
@@ -39,6 +43,7 @@ function createSession() {
|
|
|
39
43
|
packageJson,
|
|
40
44
|
conversations: { __global__: [] },
|
|
41
45
|
activities: {},
|
|
46
|
+
jobQueue: [],
|
|
42
47
|
productionActivity: null,
|
|
43
48
|
headlessPlan: null,
|
|
44
49
|
};
|
|
@@ -57,20 +62,6 @@ async function writeHeadlessLog(session, lines, explicitPath) {
|
|
|
57
62
|
return logPath;
|
|
58
63
|
}
|
|
59
64
|
|
|
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
65
|
|
|
75
66
|
async function runHeadlessActivityLoop(session, log, { wait, timeoutMs }) {
|
|
76
67
|
if (!wait) return { exitCode: 0, completed: [], timedOut: false };
|
|
@@ -150,118 +141,54 @@ async function runHeadlessActivityLoop(session, log, { wait, timeoutMs }) {
|
|
|
150
141
|
return { exitCode: 1, completed, timedOut: true };
|
|
151
142
|
}
|
|
152
143
|
|
|
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
144
|
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
|
-
}
|
|
145
|
+
const result = await runAgenticLoop(agent, session, initialInput, {
|
|
146
|
+
timeoutMs,
|
|
147
|
+
maxTurns,
|
|
148
|
+
waitForActivities: async (turnSession, _startedActivities, waitOptions) => {
|
|
149
|
+
const waitResult = await runHeadlessActivityLoop(turnSession, log, { wait: true, timeoutMs: waitOptions.timeoutMs });
|
|
150
|
+
return {
|
|
151
|
+
ok: waitResult.exitCode === 0 && !waitResult.timedOut,
|
|
152
|
+
...waitResult,
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
onTurnStart: ({ turn, maxTurns: totalTurns }) => {
|
|
156
|
+
log.push(`agentic-loop: turn ${turn}/${totalTurns}`);
|
|
157
|
+
console.log(`[headless] Agent turn ${turn}/${totalTurns}…`);
|
|
158
|
+
},
|
|
159
|
+
onTurnResponse: ({ turn, response }) => {
|
|
160
|
+
log.push(`agentic-loop: turn ${turn} response:`);
|
|
161
|
+
log.push(response);
|
|
162
|
+
console.log(response);
|
|
163
|
+
},
|
|
164
|
+
onPlanExtracted: ({ steps }) => {
|
|
165
|
+
log.push(`agentic-loop: plan extracted from text (${steps.length} steps, fallback)`);
|
|
166
|
+
},
|
|
167
|
+
onPlanAlreadySet: ({ steps }) => {
|
|
168
|
+
log.push(`agentic-loop: plan set via tool (${steps.length} steps)`);
|
|
169
|
+
},
|
|
170
|
+
onComplete: () => {
|
|
171
|
+
log.push('agentic-loop: no new non-terminal activities — plan complete');
|
|
172
|
+
console.log('[headless] Plan complete.');
|
|
173
|
+
},
|
|
174
|
+
onPendingSteps: ({ pendingSteps }) => {
|
|
226
175
|
log.push(`agentic-loop: no new async activity, ${pendingSteps.length} pending step(s) remain`);
|
|
227
176
|
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 };
|
|
177
|
+
},
|
|
178
|
+
onActivitiesStarted: ({ activities }) => {
|
|
179
|
+
log.push(`agentic-loop: ${activities.length} new job(s) started, waiting…`);
|
|
180
|
+
},
|
|
181
|
+
onActivitiesCompleted: ({ summary }) => {
|
|
182
|
+
log.push(`agentic-loop: completed activities:\n${summary}`);
|
|
183
|
+
if (session.headlessPlan) log.push(`agentic-loop: plan status:\n${formatPlanStatus(session.headlessPlan)}`);
|
|
184
|
+
},
|
|
185
|
+
onMaxTurns: ({ maxTurns: totalTurns }) => {
|
|
186
|
+
log.push(`agentic-loop: max turns (${totalTurns}) reached without completing`);
|
|
187
|
+
console.error(`[headless] Max agent turns (${totalTurns}) reached.`);
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return { exitCode: result.ok ? 0 : (result.waitResult?.exitCode ?? 1) };
|
|
265
192
|
}
|
|
266
193
|
|
|
267
194
|
async function runHeadless(argv, agent) {
|
|
@@ -326,7 +253,7 @@ async function runHeadless(argv, agent) {
|
|
|
326
253
|
if (useAgenticLoop) {
|
|
327
254
|
({ exitCode } = await runHeadlessAgenticLoop(agent, session, input, log, { timeoutMs, maxTurns }));
|
|
328
255
|
} else {
|
|
329
|
-
const response = await
|
|
256
|
+
const response = await runAgentTurn(agent, session, input);
|
|
330
257
|
log.push('response:');
|
|
331
258
|
log.push(response);
|
|
332
259
|
console.log(response);
|
|
@@ -345,7 +272,171 @@ async function runHeadless(argv, agent) {
|
|
|
345
272
|
}
|
|
346
273
|
}
|
|
347
274
|
|
|
275
|
+
async function runRuntime(argv, agent) {
|
|
276
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
277
|
+
console.log([
|
|
278
|
+
'Usage: wiki-manager runtime [--host <host>] [--port <port>] [--state-dir <dir>]',
|
|
279
|
+
'',
|
|
280
|
+
'Runs the local agentic runtime used by wiki-manager Shell and llm-wiki serve.',
|
|
281
|
+
'',
|
|
282
|
+
'Defaults:',
|
|
283
|
+
' --host 0.0.0.0',
|
|
284
|
+
' --port 7788',
|
|
285
|
+
' --state-dir .wiki-manager',
|
|
286
|
+
].join('\n'));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const { defaultRuntimeStateDir, openRuntimeStore } = await import('../runtime/store.js');
|
|
290
|
+
const { startRuntimeServer } = await import('../runtime/server.js');
|
|
291
|
+
const { emitRuntimeLog, startActivitySupervisor } = await import('../runtime/supervisor.js');
|
|
292
|
+
const { resolveRuntimeAuthToken } = await import('../runtime/auth.js');
|
|
293
|
+
const { createSqliteQueueStore } = await import('../runtime/queueStore.js');
|
|
294
|
+
const { runRuntimeAgenticLoop } = 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 session = createSession();
|
|
306
|
+
session.headless = true;
|
|
307
|
+
session.chatMode = false;
|
|
308
|
+
session.packageJson = packageJson;
|
|
309
|
+
|
|
310
|
+
const store = openRuntimeStore({ stateDir });
|
|
311
|
+
store.hydrateSession(session);
|
|
312
|
+
session.queueStore = createSqliteQueueStore(store, session);
|
|
313
|
+
|
|
314
|
+
let serverHandle = null;
|
|
315
|
+
session._onAgentEvent = (event) => {
|
|
316
|
+
store.persistEvent(event);
|
|
317
|
+
serverHandle?.publish(event);
|
|
318
|
+
};
|
|
319
|
+
session._onRuntimeError = (err) => {
|
|
320
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
321
|
+
dispatchAgentEvent(session, createAgentEvent('run_error', {
|
|
322
|
+
origin: 'runtime',
|
|
323
|
+
payload: { message },
|
|
324
|
+
}));
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
let supervisor = null;
|
|
328
|
+
|
|
329
|
+
async function executeRun(body, { signal } = {}) {
|
|
330
|
+
const input = String(body.input ?? body.prompt ?? '').trim();
|
|
331
|
+
const workspace = body.workspace ? String(body.workspace).trim() : null;
|
|
332
|
+
const timeoutMs = (Number.isFinite(Number(body.timeout)) ? Math.max(1, Number(body.timeout)) : 3600) * 1000;
|
|
333
|
+
const maxTurns = Number.isFinite(Number(body.maxTurns)) ? Math.max(1, Number(body.maxTurns)) : 20;
|
|
334
|
+
const runId = randomUUID();
|
|
335
|
+
try {
|
|
336
|
+
if (workspace && session.workspace !== workspace) {
|
|
337
|
+
const result = await handleSlashCommand(`/use ${workspace}`, { packageJson, session });
|
|
338
|
+
if (!session.workspacePath) throw new Error(result.output || `Workspace not loaded: ${workspace}`);
|
|
339
|
+
}
|
|
340
|
+
dispatchAgentEvent(session, createAgentEvent('run_started', {
|
|
341
|
+
origin: 'runtime',
|
|
342
|
+
runId,
|
|
343
|
+
payload: { input, workspace },
|
|
344
|
+
}));
|
|
345
|
+
dispatchAgentEvent(session, createAgentEvent('user_message', {
|
|
346
|
+
origin: 'user',
|
|
347
|
+
runId,
|
|
348
|
+
payload: { content: input },
|
|
349
|
+
}));
|
|
350
|
+
session._abortSignal = signal ?? null;
|
|
351
|
+
supervisor?.setRunSignal(signal);
|
|
352
|
+
session._onStep = (message) => emitRuntimeLog(session, message);
|
|
353
|
+
const result = await runRuntimeAgenticLoop(agent, session, input, {
|
|
354
|
+
signal,
|
|
355
|
+
timeoutMs,
|
|
356
|
+
maxTurns,
|
|
357
|
+
runId,
|
|
358
|
+
pollBusy: supervisor?.pollBusy,
|
|
359
|
+
});
|
|
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
|
+
} catch (err) {
|
|
381
|
+
if (err?.name === 'AbortError') {
|
|
382
|
+
dispatchAgentEvent(session, createAgentEvent('run_cancelled', {
|
|
383
|
+
origin: 'runtime',
|
|
384
|
+
runId,
|
|
385
|
+
payload: {
|
|
386
|
+
runId,
|
|
387
|
+
message: 'Agent run cancelled.',
|
|
388
|
+
},
|
|
389
|
+
}));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
dispatchAgentEvent(session, createAgentEvent('run_error', {
|
|
393
|
+
origin: 'runtime',
|
|
394
|
+
runId,
|
|
395
|
+
payload: {
|
|
396
|
+
runId,
|
|
397
|
+
message: err instanceof Error ? err.message : String(err),
|
|
398
|
+
},
|
|
399
|
+
}));
|
|
400
|
+
} finally {
|
|
401
|
+
supervisor?.setRunSignal(null);
|
|
402
|
+
delete session._abortSignal;
|
|
403
|
+
delete session._onStep;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
serverHandle = await startRuntimeServer({
|
|
408
|
+
host,
|
|
409
|
+
port,
|
|
410
|
+
store,
|
|
411
|
+
session,
|
|
412
|
+
run: executeRun,
|
|
413
|
+
cancel: () => emitRuntimeLog(session, 'runtime: cancel requested'),
|
|
414
|
+
token: auth.token,
|
|
415
|
+
});
|
|
416
|
+
supervisor = startActivitySupervisor(session);
|
|
417
|
+
|
|
418
|
+
console.log(`wiki-manager runtime listening on http://${host}:${port}`);
|
|
419
|
+
console.log(`runtime state: ${store.dbPath}`);
|
|
420
|
+
if (auth.tokenPath) console.log(`runtime token: ${auth.tokenPath}`);
|
|
421
|
+
|
|
422
|
+
const shutdown = async () => {
|
|
423
|
+
supervisor?.stop();
|
|
424
|
+
await serverHandle.close();
|
|
425
|
+
store.close();
|
|
426
|
+
process.exit(0);
|
|
427
|
+
};
|
|
428
|
+
process.once('SIGINT', shutdown);
|
|
429
|
+
process.once('SIGTERM', shutdown);
|
|
430
|
+
await new Promise(() => {});
|
|
431
|
+
}
|
|
432
|
+
|
|
348
433
|
export async function runCli(argv) {
|
|
434
|
+
if (argv[0] === 'runtime') {
|
|
435
|
+
const agent = createAgentGraph();
|
|
436
|
+
await runRuntime(argv.slice(1), agent);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
349
440
|
if (argv.includes('--setup-wizard')) {
|
|
350
441
|
if (!process.versions.bun) {
|
|
351
442
|
throw new Error('Setup wizard requires Bun. Run: bun ./bin/wiki-manager.js --setup-wizard');
|
|
@@ -396,9 +487,25 @@ export async function runCli(argv) {
|
|
|
396
487
|
const { runOpenTuiShell, runStartupWizard } = await import('../shell/tui.tsx');
|
|
397
488
|
const gaps = await runChecks();
|
|
398
489
|
if (gaps.length > 0) await runStartupWizard(gaps);
|
|
399
|
-
|
|
490
|
+
let runtime = null;
|
|
491
|
+
try {
|
|
492
|
+
const { ensureRuntime } = await import('../runtime/lifecycle.js');
|
|
493
|
+
runtime = await ensureRuntime();
|
|
494
|
+
} catch (err) {
|
|
495
|
+
console.error(`Runtime unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
496
|
+
}
|
|
497
|
+
await runOpenTuiShell({ agent, packageJson, runtime });
|
|
400
498
|
return;
|
|
401
499
|
}
|
|
402
500
|
|
|
403
|
-
|
|
501
|
+
let runtime = null;
|
|
502
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
503
|
+
try {
|
|
504
|
+
const { ensureRuntime } = await import('../runtime/lifecycle.js');
|
|
505
|
+
runtime = await ensureRuntime();
|
|
506
|
+
} catch (err) {
|
|
507
|
+
console.error(`Runtime unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
await runShell({ agent, packageJson, runtime });
|
|
404
511
|
}
|
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
|
package/src/core/agentEvents.js
CHANGED
|
@@ -7,6 +7,18 @@ const SESSION_PROJECTION_EVENTS = new Set([
|
|
|
7
7
|
'plan_step_updated',
|
|
8
8
|
'activity_upserted',
|
|
9
9
|
'run_error',
|
|
10
|
+
'run_cancelled',
|
|
11
|
+
'runtime_log',
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
// Events that can mutate state.plan in applyEvent() — only these warrant the
|
|
15
|
+
// before/after plan comparison below (runtime_log fires far more often and
|
|
16
|
+
// never touches the plan).
|
|
17
|
+
const PLAN_MUTATING_EVENTS = new Set([
|
|
18
|
+
'run_started',
|
|
19
|
+
'plan_set',
|
|
20
|
+
'plan_step_updated',
|
|
21
|
+
'activity_upserted',
|
|
10
22
|
]);
|
|
11
23
|
|
|
12
24
|
export function createAgentEvent(type, { origin = 'system', payload = {}, runId = null, turnId = null } = {}) {
|
|
@@ -23,7 +35,8 @@ export function createAgentEvent(type, { origin = 'system', payload = {}, runId
|
|
|
23
35
|
|
|
24
36
|
export function dispatchAgentEvent(session, event) {
|
|
25
37
|
const normalized = event.id && event.ts ? event : createAgentEvent(event.type, event);
|
|
26
|
-
const
|
|
38
|
+
const tracksPlan = PLAN_MUTATING_EVENTS.has(normalized.type);
|
|
39
|
+
const previousPlan = tracksPlan ? JSON.stringify(session.headlessPlan ?? null) : null;
|
|
27
40
|
session.agentEvents ??= [];
|
|
28
41
|
session.agentEvents.push(normalized);
|
|
29
42
|
session._agentProjectionState ??= createProjectionState();
|
|
@@ -31,10 +44,11 @@ export function dispatchAgentEvent(session, event) {
|
|
|
31
44
|
session.agentProjection = publicProjection(session._agentProjectionState);
|
|
32
45
|
if (SESSION_PROJECTION_EVENTS.has(normalized.type)) {
|
|
33
46
|
applyAgentProjectionToSession(session, session.agentProjection);
|
|
34
|
-
if (JSON.stringify(session.headlessPlan ?? null) !== previousPlan) {
|
|
47
|
+
if (tracksPlan && JSON.stringify(session.headlessPlan ?? null) !== previousPlan) {
|
|
35
48
|
session._onPlanUpdate?.();
|
|
36
49
|
}
|
|
37
50
|
}
|
|
51
|
+
session._onAgentEvent?.(normalized, session.agentProjection);
|
|
38
52
|
return normalized;
|
|
39
53
|
}
|
|
40
54
|
|
|
@@ -133,10 +147,18 @@ function applyEvent(state, event) {
|
|
|
133
147
|
case 'run_done':
|
|
134
148
|
state.status = 'done';
|
|
135
149
|
return;
|
|
150
|
+
case 'run_cancelled':
|
|
151
|
+
state.status = 'cancelled';
|
|
152
|
+
state.logs.push(String(event.payload?.message ?? 'Agent run cancelled.'));
|
|
153
|
+
return;
|
|
136
154
|
case 'run_error':
|
|
137
155
|
state.status = 'error';
|
|
138
156
|
state.logs.push(String(event.payload?.message ?? 'Agent run failed.'));
|
|
139
157
|
return;
|
|
158
|
+
case 'runtime_log':
|
|
159
|
+
state.logs.push(String(event.payload?.message ?? ''));
|
|
160
|
+
state.logs = state.logs.slice(-200);
|
|
161
|
+
return;
|
|
140
162
|
default:
|
|
141
163
|
return;
|
|
142
164
|
}
|