@dotdrelle/wiki-manager 0.9.3 → 0.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -6
- package/agents.docker-compose.yml +5 -0
- package/package.json +5 -2
- package/src/agent/graph.js +68 -9
- package/src/agent/graph.test.js +64 -0
- package/src/cli/wiki-manager.js +1 -1
- package/src/commands/slash.js +6 -2
- package/src/contracts/README.md +16 -0
- package/src/contracts/schemas.js +302 -0
- package/src/contracts/schemas.test.js +93 -0
- package/src/core/activity.js +14 -2
- package/src/core/activity.test.js +4 -2
- package/src/core/agentEvents.js +158 -15
- package/src/core/agentEvents.test.js +54 -12
- package/src/core/agentLoop.js +32 -7
- package/src/core/mcp.js +3 -1
- package/src/core/plan.js +4 -0
- package/src/core/planPatch.js +224 -0
- package/src/core/planPatch.test.js +63 -0
- package/src/core/workflow.js +264 -0
- package/src/core/workflow.test.js +66 -0
- package/src/runtime/client.js +28 -1
- package/src/runtime/runner.js +432 -20
- package/src/runtime/runner.test.js +273 -1
- package/src/runtime/server.js +322 -15
- package/src/runtime/server.test.js +339 -0
- package/src/runtime/store.js +59 -10
- package/src/runtime/store.test.js +47 -1
- package/src/shell/RightPane.tsx +1 -7
- package/src/shell/StartupScreen.tsx +212 -0
- package/src/shell/repl.js +51 -7
- package/src/shell/repl.test.js +77 -1
- package/src/shell/textFit.ts +6 -0
- package/src/shell/tui.tsx +163 -0
- package/src/shell/useAgent.ts +17 -9
- package/src/shell/useSession.ts +34 -0
package/README.md
CHANGED
|
@@ -527,13 +527,17 @@ The shared `docker-compose.yml` starts one workspace stack:
|
|
|
527
527
|
|
|
528
528
|
| Service | Role | Port variable |
|
|
529
529
|
| --- | --- | --- |
|
|
530
|
-
| `serve` | Wiki web UI and browser chat | `WIKI_SERVE_PORT` |
|
|
531
|
-
| `mcp-http` | llm-wiki MCP endpoint | `WIKI_MCP_PORT` |
|
|
532
|
-
| `production-mcp` | Production job MCP endpoint | `PRODUCTION_MCP_PORT` |
|
|
530
|
+
| `serve` | Wiki web UI and browser chat, container port `3000` | `WIKI_SERVE_PORT` |
|
|
531
|
+
| `mcp-http` | llm-wiki MCP endpoint, container port `3333` | `WIKI_MCP_PORT` |
|
|
532
|
+
| `production-mcp` | Production job MCP endpoint, container port `8080` | `PRODUCTION_MCP_PORT` |
|
|
533
533
|
|
|
534
534
|
Use `wiki-workspace` whenever possible so Compose receives the right project
|
|
535
535
|
name, env file, ports, and volume mounts.
|
|
536
536
|
|
|
537
|
+
Runtime split: the host manager/runtime uses Node.js 22+ for `node:sqlite`; the
|
|
538
|
+
interactive OpenTUI shell uses Bun 1.2+; workspace Docker services run from the
|
|
539
|
+
published images and do not depend on host `node_modules`.
|
|
540
|
+
|
|
537
541
|
```bash
|
|
538
542
|
wiki-workspace list
|
|
539
543
|
wiki-workspace agents up
|
|
@@ -799,12 +803,23 @@ Files matching `docker-compose*.local.yml` are ignored by Git.
|
|
|
799
803
|
```bash
|
|
800
804
|
pnpm install
|
|
801
805
|
pnpm start
|
|
806
|
+
pnpm run check-versions
|
|
802
807
|
pnpm run check
|
|
803
808
|
```
|
|
804
809
|
|
|
805
|
-
When bumping
|
|
806
|
-
|
|
807
|
-
|
|
810
|
+
When bumping a coordinated release, keep `llm-wiki`, `llm-wiki-manager`, Python
|
|
811
|
+
agent `_AGENT_VERSION` values, MCP `clientInfo.version` / server versions, Git
|
|
812
|
+
tags, and Docker image tags aligned. Run:
|
|
813
|
+
|
|
814
|
+
```bash
|
|
815
|
+
pnpm run check-versions
|
|
816
|
+
CHECK_GIT_TAG=1 pnpm run check-versions # pre-release tag check
|
|
817
|
+
CHECK_DOCKER_IMAGES=1 pnpm run check-versions # after local image build
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
`build-and-push.sh` synchronizes the coordinated version, runs
|
|
821
|
+
`pnpm run check-versions`, builds images tagged with that version, and can push
|
|
822
|
+
the matching `latest` tags.
|
|
808
823
|
|
|
809
824
|
`pnpm run check` verifies the CLI version, help output, and limited `--once` mode.
|
|
810
825
|
For headless changes, also test a controlled error path, for example:
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
# MAILERSEND_API_KEY
|
|
22
22
|
# MAILERSEND_FROM_EMAIL
|
|
23
23
|
# MAILERSEND_FROM_NAME
|
|
24
|
+
# MAILERSEND_CA_CERT — optional CA bundle path inside the container
|
|
24
25
|
#
|
|
25
26
|
# Set these variables in a .env file at the directory where you run wiki-workspace,
|
|
26
27
|
# or export them in your shell before running wiki-workspace agents up.
|
|
@@ -90,7 +91,11 @@ services:
|
|
|
90
91
|
- MAILERSEND_FROM_NAME=${MAILERSEND_FROM_NAME:-Mailer Agent}
|
|
91
92
|
- MAILERSEND_USER_AGENT=${MAILERSEND_USER_AGENT:-curl/8.7.1}
|
|
92
93
|
- MAILERSEND_VERIFY_SSL=${MAILERSEND_VERIFY_SSL:-true}
|
|
94
|
+
- MAILERSEND_CA_CERT=${MAILERSEND_CA_CERT:-}
|
|
93
95
|
- MAILER_REQUIRE_CONFIRMATION=${MAILER_REQUIRE_CONFIRMATION:-true}
|
|
94
96
|
- MAILER_DRY_RUN=${MAILER_DRY_RUN:-false}
|
|
95
97
|
- MCP_AUTH_TOKEN=${MAILER_MCP_AUTH_TOKEN:-}
|
|
98
|
+
volumes:
|
|
99
|
+
# Optional: mount a CA bundle and set MAILERSEND_CA_CERT to its container path.
|
|
100
|
+
#- ${AGENTS_DATA_DIR:-./.agents-data}/certs:/certs:ro
|
|
96
101
|
restart: unless-stopped
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.4",
|
|
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,10 @@
|
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"start": "bun ./bin/wiki-manager.js",
|
|
14
|
-
"test": "node --test src/agent/graph.test.js 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/wikiWorkspace.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/runner.test.js src/runtime/auth.test.js",
|
|
14
|
+
"test": "node --test src/agent/graph.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/agentEvents.test.js src/core/workflow.test.js src/core/planPatch.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/wikiWorkspace.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/runner.test.js src/runtime/auth.test.js",
|
|
15
|
+
"check-versions": "node scripts/check-versions.js",
|
|
16
|
+
"prepack": "node scripts/check-versions.js",
|
|
17
|
+
"prepublishOnly": "node scripts/check-versions.js",
|
|
15
18
|
"check": "bun ./bin/wiki-manager.js --version && bun ./bin/wiki-manager.js --help && bun ./bin/wiki-manager.js --once \"verifie le mode agent\""
|
|
16
19
|
},
|
|
17
20
|
"engines": {
|
package/src/agent/graph.js
CHANGED
|
@@ -66,8 +66,26 @@ const WIKI_PLAN_SET_TOOL = {
|
|
|
66
66
|
properties: {
|
|
67
67
|
steps: {
|
|
68
68
|
type: 'array',
|
|
69
|
-
items: {
|
|
70
|
-
|
|
69
|
+
items: {
|
|
70
|
+
anyOf: [
|
|
71
|
+
{ type: 'string' },
|
|
72
|
+
{
|
|
73
|
+
type: 'object',
|
|
74
|
+
additionalProperties: false,
|
|
75
|
+
properties: {
|
|
76
|
+
id: { type: 'string' },
|
|
77
|
+
description: { type: 'string' },
|
|
78
|
+
status: { type: 'string', enum: ['pending', 'queued', 'running', 'waiting', 'pending_approval', 'done', 'failed', 'cancelled', 'stalled', 'added_during_run'] },
|
|
79
|
+
dependsOn: { type: 'array', items: { type: 'string' } },
|
|
80
|
+
executor: { type: ['string', 'null'] },
|
|
81
|
+
executorQuery: { type: ['object', 'null'], additionalProperties: true },
|
|
82
|
+
outputRefs: { type: 'array', items: { type: 'string' } },
|
|
83
|
+
},
|
|
84
|
+
required: ['description'],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
description: 'Ordered steps. Backward-compatible strings are accepted; structured steps may include id, dependsOn, executor, executorQuery, outputRefs.',
|
|
71
89
|
},
|
|
72
90
|
},
|
|
73
91
|
required: ['steps'],
|
|
@@ -344,11 +362,7 @@ function handleWikiTool(session, tool, args) {
|
|
|
344
362
|
if (tool === 'plan_set') {
|
|
345
363
|
const steps = Array.isArray(args.steps) ? args.steps : [];
|
|
346
364
|
emitAgentEvent(session, 'plan_set', 'tool', {
|
|
347
|
-
steps: steps.map((
|
|
348
|
-
step: i + 1,
|
|
349
|
-
description: String(description),
|
|
350
|
-
status: 'pending',
|
|
351
|
-
})),
|
|
365
|
+
steps: steps.map((raw, i) => normalizeDeclaredPlanStep(raw, i, session)),
|
|
352
366
|
});
|
|
353
367
|
return `Plan registered: ${steps.length} step${steps.length !== 1 ? 's' : ''}.`;
|
|
354
368
|
}
|
|
@@ -364,6 +378,51 @@ function handleWikiTool(session, tool, args) {
|
|
|
364
378
|
return `Unknown wiki tool: ${tool}`;
|
|
365
379
|
}
|
|
366
380
|
|
|
381
|
+
function normalizeDeclaredPlanStep(raw, index, session) {
|
|
382
|
+
const item = raw && typeof raw === 'object' && !Array.isArray(raw)
|
|
383
|
+
? raw
|
|
384
|
+
: { description: String(raw) };
|
|
385
|
+
const description = String(item.description ?? item.label ?? item.name ?? item.id ?? `Step ${index + 1}`);
|
|
386
|
+
return {
|
|
387
|
+
step: Number(item.step ?? index + 1),
|
|
388
|
+
id: item.id ? String(item.id) : slugStepId(description, index),
|
|
389
|
+
description,
|
|
390
|
+
status: item.status ?? 'pending',
|
|
391
|
+
dependsOn: Array.isArray(item.dependsOn) ? item.dependsOn.map(String) : [],
|
|
392
|
+
executor: item.executor ?? selectExecutorForStep(description, session),
|
|
393
|
+
executorQuery: item.executorQuery ?? null,
|
|
394
|
+
outputRefs: Array.isArray(item.outputRefs) ? item.outputRefs.map(String) : [],
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function slugStepId(description, index) {
|
|
399
|
+
const slug = String(description)
|
|
400
|
+
.toLowerCase()
|
|
401
|
+
.normalize('NFD')
|
|
402
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
403
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
404
|
+
.replace(/^-+|-+$/g, '')
|
|
405
|
+
.slice(0, 48);
|
|
406
|
+
return slug || `task-${index + 1}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function selectExecutorForStep(description, session) {
|
|
410
|
+
const text = String(description ?? '').toLowerCase();
|
|
411
|
+
let fallback = null;
|
|
412
|
+
for (const [serverName, value] of Object.entries(session.mcp ?? {})) {
|
|
413
|
+
if (value.status !== 'connected') continue;
|
|
414
|
+
for (const tool of value.tools ?? []) {
|
|
415
|
+
const executor = `${serverName}.${tool.name}`;
|
|
416
|
+
fallback ??= executor;
|
|
417
|
+
const haystack = `${serverName} ${tool.name} ${tool.description ?? ''}`.toLowerCase();
|
|
418
|
+
if (text.split(/[^a-z0-9]+/).filter((token) => token.length >= 4).some((token) => haystack.includes(token))) {
|
|
419
|
+
return executor;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return fallback;
|
|
424
|
+
}
|
|
425
|
+
|
|
367
426
|
export function buildAgentSystemPrompt(state) {
|
|
368
427
|
const workspace = state.session.workspace ?? 'no workspace selected';
|
|
369
428
|
const wikirc = state.session.wikirc?.profile ?? 'no profile loaded';
|
|
@@ -401,8 +460,8 @@ export function buildAgentSystemPrompt(state) {
|
|
|
401
460
|
'',
|
|
402
461
|
'Task startup:',
|
|
403
462
|
' 1. If the next MCP tool returns _activity.plan.steps, call that tool directly; the shell will create the visible plan from the returned activity.',
|
|
404
|
-
' 2. If the tool cannot declare its own plan, call wiki__plan_set
|
|
405
|
-
' Multi-tool example: wiki__plan_set(steps=["CME export",
|
|
463
|
+
' 2. If the tool cannot declare its own plan, call wiki__plan_set before executing the first step. Prefer structured steps: {id, description, dependsOn, executor, executorQuery, outputRefs}; a legacy list of strings is still accepted.',
|
|
464
|
+
' Multi-tool example: wiki__plan_set(steps=[{id:"cme-export",description:"CME export",dependsOn:[],executor:"cme.cme_export_run",outputRefs:["raw/untracked"]},{id:"production",description:"Production pipeline",dependsOn:["cme-export"],executor:"production.production_start_job",outputRefs:["deliverables"]}])',
|
|
406
465
|
' 3. Immediately execute the first step using the appropriate MCP tool. Do not start step 2 in the same turn unless one async pipeline tool owns and declares the whole sequence.',
|
|
407
466
|
' For synchronous steps (result is immediate, no _activity polling), call wiki__plan_done(step=1) after confirming success.',
|
|
408
467
|
' For async MCP jobs (returns _activity with poll), the orchestrator tracks completion automatically.',
|
package/src/agent/graph.test.js
CHANGED
|
@@ -143,3 +143,67 @@ test('agent graph waits for tool-level approval configured on endpoint', async (
|
|
|
143
143
|
}
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
test('agent graph accepts structured wiki plan steps and selects MCP executors', async () => {
|
|
147
|
+
let calls = 0;
|
|
148
|
+
const session = sessionBase({
|
|
149
|
+
mcp: {
|
|
150
|
+
cme: {
|
|
151
|
+
status: 'connected',
|
|
152
|
+
url: 'http://127.0.0.1:3001/mcp/',
|
|
153
|
+
tools: [{
|
|
154
|
+
name: 'cme_export_run',
|
|
155
|
+
description: 'Export CME pages',
|
|
156
|
+
inputSchema: { type: 'object', properties: {} },
|
|
157
|
+
}],
|
|
158
|
+
},
|
|
159
|
+
production: {
|
|
160
|
+
status: 'connected',
|
|
161
|
+
url: 'http://127.0.0.1:3000/mcp/',
|
|
162
|
+
tools: [{
|
|
163
|
+
name: 'production_start_job',
|
|
164
|
+
description: 'Start production job',
|
|
165
|
+
inputSchema: { type: 'object', properties: { type: { type: 'string' } } },
|
|
166
|
+
}],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
llm: {
|
|
170
|
+
async completeWithTools() {
|
|
171
|
+
calls += 1;
|
|
172
|
+
if (calls === 1) {
|
|
173
|
+
return {
|
|
174
|
+
content: null,
|
|
175
|
+
message: { role: 'assistant', content: null },
|
|
176
|
+
tool_calls: [{
|
|
177
|
+
id: 'plan-call',
|
|
178
|
+
type: 'function',
|
|
179
|
+
function: {
|
|
180
|
+
name: 'wiki__plan_set',
|
|
181
|
+
arguments: JSON.stringify({
|
|
182
|
+
steps: [
|
|
183
|
+
{ id: 'cme-export', description: 'Export CME pages', outputRefs: ['raw/untracked'] },
|
|
184
|
+
{ id: 'build', description: 'Run production build', dependsOn: ['cme-export'] },
|
|
185
|
+
],
|
|
186
|
+
}),
|
|
187
|
+
},
|
|
188
|
+
}],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
content: 'Plan ready.',
|
|
193
|
+
message: { role: 'assistant', content: 'Plan ready.' },
|
|
194
|
+
tool_calls: null,
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const agent = createAgentGraph();
|
|
201
|
+
const result = await agent.invoke({ input: 'Plan export then build', session });
|
|
202
|
+
|
|
203
|
+
assert.equal(result.response, 'Plan ready.');
|
|
204
|
+
assert.deepEqual(session.headlessPlan.map((step) => step.id), ['cme-export', 'build']);
|
|
205
|
+
assert.equal(session.headlessPlan[0].executor, 'cme.cme_export_run');
|
|
206
|
+
assert.equal(session.headlessPlan[1].executor, 'production.production_start_job');
|
|
207
|
+
assert.deepEqual(session.headlessPlan[1].dependsOn, ['cme-export']);
|
|
208
|
+
assert.deepEqual(session.headlessPlan[0].outputRefs, ['raw/untracked']);
|
|
209
|
+
});
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -163,7 +163,7 @@ async function runHeadlessAgenticLoop(agent, session, initialInput, log, { timeo
|
|
|
163
163
|
console.log(response);
|
|
164
164
|
},
|
|
165
165
|
onPlanExtracted: ({ steps }) => {
|
|
166
|
-
log.push(`agentic-loop: plan extracted from text (${steps.length} steps, fallback)`);
|
|
166
|
+
log.push(`agentic-loop: plan extracted from text (${steps.length} steps, deprecated fallback)`);
|
|
167
167
|
},
|
|
168
168
|
onPlanAlreadySet: ({ steps }) => {
|
|
169
169
|
log.push(`agentic-loop: plan set via tool (${steps.length} steps)`);
|
package/src/commands/slash.js
CHANGED
|
@@ -615,8 +615,8 @@ ${helpPair('/workspace list', 'Workspaces', '/new <n> [path]', 'New workspace')}
|
|
|
615
615
|
${helpPair('/use <workspace>', 'Use workspace', '/status', 'Session status')}
|
|
616
616
|
${helpPair('/config list', 'Config profiles', '/config use <n>', 'Use config')}
|
|
617
617
|
${helpPair('/config edit <n>', 'Edit config', '/workspace delete <n>', 'Delete workspace')}
|
|
618
|
-
${helpPair('/services', 'Services', '/start [service|agents]', 'Start service(s)')}
|
|
619
|
-
${helpPair('/stop [service|agents]', 'Stop service(s)', '/logs <service>', 'Service logs')}
|
|
618
|
+
${helpPair('/services', 'Services', '/start [all|service|agents]', 'Start service(s)')}
|
|
619
|
+
${helpPair('/stop [all|service|agents]', 'Stop service(s)', '/logs <service>', 'Service logs')}
|
|
620
620
|
${helpPair('/skills', 'List skills', '/skills show <n>', 'Show skill')}
|
|
621
621
|
${helpPair('/skills run <n>', 'Run skill guide', '/skills edit <n>', 'Edit skill')}
|
|
622
622
|
${helpPair('/mcp status', 'MCP status', '/mcp endpoints', 'MCP endpoints')}
|
|
@@ -810,6 +810,10 @@ export async function handleSlashCommand(line, context) {
|
|
|
810
810
|
}
|
|
811
811
|
}
|
|
812
812
|
case 'start': {
|
|
813
|
+
// 'all' already resolves correctly through serviceAliases() (DEFAULT_SERVICE_ALIASES.all
|
|
814
|
+
// = COMPOSE_SERVICES, overridable via docker-compose.yml's service-aliases.all.targets) —
|
|
815
|
+
// do not remap it to undefined, that bypasses any custom "all" target list and always
|
|
816
|
+
// falls back to the hardcoded COMPOSE_SERVICES constant instead.
|
|
813
817
|
const service = args[1];
|
|
814
818
|
if (service === 'agents') return runAgentCommand(startAgents, 'start');
|
|
815
819
|
try {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Runtime Contracts
|
|
2
|
+
|
|
3
|
+
Versioned JSON Schema-like contracts live in `schemas.js`.
|
|
4
|
+
|
|
5
|
+
Current schema version: `1`.
|
|
6
|
+
|
|
7
|
+
Validated boundaries:
|
|
8
|
+
|
|
9
|
+
- `_activity` after normalization in `core/activity.js`
|
|
10
|
+
- `AgentRunEvent` creation and dispatch in `core/agentEvents.js`
|
|
11
|
+
- structured plan and plan patch normalization
|
|
12
|
+
- runtime `/run` and `/control` request payloads
|
|
13
|
+
|
|
14
|
+
Validation is enabled when `WIKI_MANAGER_VALIDATE_CONTRACTS=1`, `CI=true`, or
|
|
15
|
+
`NODE_ENV` is set to a non-production value. Schemas tolerate additional fields
|
|
16
|
+
so agents can extend payloads without breaking older consumers.
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
const STATUS_VALUES = [
|
|
2
|
+
'pending',
|
|
3
|
+
'queued',
|
|
4
|
+
'running',
|
|
5
|
+
'waiting',
|
|
6
|
+
'pending_approval',
|
|
7
|
+
'done',
|
|
8
|
+
'failed',
|
|
9
|
+
'cancelled',
|
|
10
|
+
'canceled',
|
|
11
|
+
'stalled',
|
|
12
|
+
'added_during_run',
|
|
13
|
+
'error',
|
|
14
|
+
'complete',
|
|
15
|
+
'completed',
|
|
16
|
+
'success',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const nullableString = { type: ['string', 'null'] };
|
|
20
|
+
const nullableObject = { type: ['object', 'null'], additionalProperties: true };
|
|
21
|
+
const outputReferenceSchema = {
|
|
22
|
+
$id: 'https://dotdrelle.dev/wiki-manager/contracts/output-reference/v1',
|
|
23
|
+
title: 'OutputReference',
|
|
24
|
+
schemaVersion: '1',
|
|
25
|
+
oneOf: [
|
|
26
|
+
{ type: 'string' },
|
|
27
|
+
{
|
|
28
|
+
type: 'object',
|
|
29
|
+
required: ['type', 'ref'],
|
|
30
|
+
additionalProperties: true,
|
|
31
|
+
properties: {
|
|
32
|
+
type: { type: 'string' },
|
|
33
|
+
ref: { type: 'string' },
|
|
34
|
+
label: nullableString,
|
|
35
|
+
workspace: nullableString,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const planStepSchema = {
|
|
42
|
+
type: 'object',
|
|
43
|
+
required: ['description', 'status', 'dependsOn', 'outputRefs'],
|
|
44
|
+
additionalProperties: true,
|
|
45
|
+
properties: {
|
|
46
|
+
step: { type: 'number' },
|
|
47
|
+
id: nullableString,
|
|
48
|
+
description: { type: 'string' },
|
|
49
|
+
status: { type: 'string' },
|
|
50
|
+
dependsOn: { type: 'array', items: { type: 'string' } },
|
|
51
|
+
executor: nullableString,
|
|
52
|
+
executorQuery: nullableObject,
|
|
53
|
+
outputRefs: { type: 'array', items: outputReferenceSchema },
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const patchTaskSchema = {
|
|
58
|
+
type: 'object',
|
|
59
|
+
required: ['id', 'description'],
|
|
60
|
+
additionalProperties: true,
|
|
61
|
+
properties: {
|
|
62
|
+
id: { type: 'string' },
|
|
63
|
+
description: { type: 'string' },
|
|
64
|
+
status: { type: 'string' },
|
|
65
|
+
dependsOn: { type: 'array', items: { type: 'string' } },
|
|
66
|
+
executor: nullableString,
|
|
67
|
+
executorQuery: nullableObject,
|
|
68
|
+
outputRefs: { type: 'array', items: outputReferenceSchema },
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const contractSchemas = {
|
|
73
|
+
activity: {
|
|
74
|
+
$id: 'https://dotdrelle.dev/wiki-manager/contracts/activity/v1',
|
|
75
|
+
title: '_activity',
|
|
76
|
+
schemaVersion: '1',
|
|
77
|
+
type: 'object',
|
|
78
|
+
required: ['schemaVersion', 'id', 'source', 'kind', 'label', 'status', 'progress', 'poll', 'outputRefs'],
|
|
79
|
+
additionalProperties: true,
|
|
80
|
+
properties: {
|
|
81
|
+
schemaVersion: { const: '1' },
|
|
82
|
+
id: { type: 'string' },
|
|
83
|
+
source: { type: 'string' },
|
|
84
|
+
kind: { type: 'string' },
|
|
85
|
+
label: { type: 'string' },
|
|
86
|
+
status: { type: 'string' },
|
|
87
|
+
progress: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
additionalProperties: true,
|
|
90
|
+
properties: {
|
|
91
|
+
percent: { type: 'number', minimum: 0, maximum: 100 },
|
|
92
|
+
stepId: { type: 'string' },
|
|
93
|
+
parentActivityKey: { type: 'string' },
|
|
94
|
+
detail: { type: 'string' },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
poll: nullableObject,
|
|
98
|
+
outputRefs: { type: 'array', items: outputReferenceSchema },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
agentRunEvent: {
|
|
102
|
+
$id: 'https://dotdrelle.dev/wiki-manager/contracts/agent-run-event/v1',
|
|
103
|
+
title: 'AgentRunEvent',
|
|
104
|
+
schemaVersion: '1',
|
|
105
|
+
type: 'object',
|
|
106
|
+
required: ['id', 'ts', 'type', 'origin', 'payload'],
|
|
107
|
+
additionalProperties: true,
|
|
108
|
+
properties: {
|
|
109
|
+
id: { type: 'string' },
|
|
110
|
+
ts: { type: 'string' },
|
|
111
|
+
type: { type: 'string' },
|
|
112
|
+
origin: { type: 'string' },
|
|
113
|
+
runId: nullableString,
|
|
114
|
+
turnId: nullableString,
|
|
115
|
+
taskId: nullableString,
|
|
116
|
+
workspace: nullableString,
|
|
117
|
+
payload: { type: 'object', additionalProperties: true },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
runRequest: {
|
|
121
|
+
$id: 'https://dotdrelle.dev/wiki-manager/contracts/run-request/v1',
|
|
122
|
+
title: 'RuntimeRunRequest',
|
|
123
|
+
schemaVersion: '1',
|
|
124
|
+
type: 'object',
|
|
125
|
+
required: ['input'],
|
|
126
|
+
additionalProperties: true,
|
|
127
|
+
properties: {
|
|
128
|
+
input: { type: 'string', minLength: 1 },
|
|
129
|
+
prompt: { type: 'string' },
|
|
130
|
+
workspace: nullableString,
|
|
131
|
+
runId: nullableString,
|
|
132
|
+
turnId: nullableString,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
controlMessage: {
|
|
136
|
+
$id: 'https://dotdrelle.dev/wiki-manager/contracts/control-message/v1',
|
|
137
|
+
title: 'RuntimeControlMessage',
|
|
138
|
+
schemaVersion: '1',
|
|
139
|
+
type: 'object',
|
|
140
|
+
required: ['action'],
|
|
141
|
+
additionalProperties: true,
|
|
142
|
+
properties: {
|
|
143
|
+
action: { type: 'string', enum: ['status', 'explain', 'message', 'enqueue', 'approve_patch', 'reject_patch'] },
|
|
144
|
+
input: { type: 'string' },
|
|
145
|
+
message: { type: 'string' },
|
|
146
|
+
prompt: { type: 'string' },
|
|
147
|
+
request: { type: 'string' },
|
|
148
|
+
intent: { type: 'string', enum: ['observe', 'converse', 'mutate', 'enqueue'] },
|
|
149
|
+
workspace: nullableString,
|
|
150
|
+
patchId: { type: 'string' },
|
|
151
|
+
id: { type: 'string' },
|
|
152
|
+
reason: { type: 'string' },
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
plan: {
|
|
156
|
+
$id: 'https://dotdrelle.dev/wiki-manager/contracts/plan/v1',
|
|
157
|
+
title: 'StructuredPlan',
|
|
158
|
+
schemaVersion: '1',
|
|
159
|
+
type: 'array',
|
|
160
|
+
items: planStepSchema,
|
|
161
|
+
},
|
|
162
|
+
planPatch: {
|
|
163
|
+
$id: 'https://dotdrelle.dev/wiki-manager/contracts/plan-patch/v1',
|
|
164
|
+
title: 'PlanPatch',
|
|
165
|
+
schemaVersion: '1',
|
|
166
|
+
type: 'object',
|
|
167
|
+
required: ['basePlanRevision', 'operations'],
|
|
168
|
+
additionalProperties: true,
|
|
169
|
+
properties: {
|
|
170
|
+
id: nullableString,
|
|
171
|
+
targetRunId: nullableString,
|
|
172
|
+
basePlanRevision: { type: 'number', minimum: 0 },
|
|
173
|
+
reason: nullableString,
|
|
174
|
+
operations: {
|
|
175
|
+
type: 'array',
|
|
176
|
+
items: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
required: ['op'],
|
|
179
|
+
additionalProperties: true,
|
|
180
|
+
properties: {
|
|
181
|
+
op: { type: 'string', enum: ['add_task', 'add_dependency', 'remove_dependency', 'cancel_task', 'replace_executor', 'request_approval'] },
|
|
182
|
+
task: patchTaskSchema,
|
|
183
|
+
taskId: { type: 'string' },
|
|
184
|
+
targetTaskId: { type: 'string' },
|
|
185
|
+
dependencyId: { type: 'string' },
|
|
186
|
+
dependsOn: { type: 'string' },
|
|
187
|
+
executor: nullableString,
|
|
188
|
+
executorQuery: nullableObject,
|
|
189
|
+
reason: { type: 'string' },
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
outputReference: outputReferenceSchema,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export function validateContract(name, value) {
|
|
199
|
+
const schema = contractSchemas[name];
|
|
200
|
+
if (!schema) {
|
|
201
|
+
throw new Error(`Unknown contract schema: ${name}`);
|
|
202
|
+
}
|
|
203
|
+
const errors = [];
|
|
204
|
+
validateSchema(schema, value, name, errors);
|
|
205
|
+
return {
|
|
206
|
+
ok: errors.length === 0,
|
|
207
|
+
errors,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function assertContract(name, value) {
|
|
212
|
+
const result = validateContract(name, value);
|
|
213
|
+
if (!result.ok) {
|
|
214
|
+
throw new Error(`Contract ${name} invalid: ${result.errors.join('; ')}`);
|
|
215
|
+
}
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function validateContractInDev(name, value) {
|
|
220
|
+
if (!contractValidationEnabled()) return value;
|
|
221
|
+
return assertContract(name, value);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function contractValidationEnabled() {
|
|
225
|
+
return process.env.WIKI_MANAGER_VALIDATE_CONTRACTS === '1'
|
|
226
|
+
|| process.env.CI === 'true'
|
|
227
|
+
|| (process.env.NODE_ENV && process.env.NODE_ENV !== 'production');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function validateSchema(schema, value, path, errors) {
|
|
231
|
+
if (schema.oneOf) {
|
|
232
|
+
const matches = schema.oneOf.filter((candidate) => validateCandidate(candidate, value, path));
|
|
233
|
+
if (matches.length !== 1) errors.push(`${path} must match exactly one schema`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (schema.anyOf) {
|
|
237
|
+
const matches = schema.anyOf.filter((candidate) => validateCandidate(candidate, value, path));
|
|
238
|
+
if (matches.length < 1) errors.push(`${path} must match at least one schema`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (schema.const !== undefined && value !== schema.const) {
|
|
242
|
+
errors.push(`${path} must equal ${JSON.stringify(schema.const)}`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
246
|
+
errors.push(`${path} must be one of ${schema.enum.join(', ')}`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (schema.type && !typeMatches(schema.type, value)) {
|
|
250
|
+
errors.push(`${path} must be ${formatType(schema.type)}`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (typeof value === 'string' && schema.minLength != null && value.length < schema.minLength) {
|
|
254
|
+
errors.push(`${path} must have length >= ${schema.minLength}`);
|
|
255
|
+
}
|
|
256
|
+
if (typeof value === 'number') {
|
|
257
|
+
if (schema.minimum != null && value < schema.minimum) errors.push(`${path} must be >= ${schema.minimum}`);
|
|
258
|
+
if (schema.maximum != null && value > schema.maximum) errors.push(`${path} must be <= ${schema.maximum}`);
|
|
259
|
+
}
|
|
260
|
+
if (Array.isArray(value)) {
|
|
261
|
+
value.forEach((item, index) => validateSchema(schema.items ?? {}, item, `${path}[${index}]`, errors));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (value && typeof value === 'object') {
|
|
265
|
+
for (const key of schema.required ?? []) {
|
|
266
|
+
if (!Object.hasOwn(value, key)) errors.push(`${path}.${key} is required`);
|
|
267
|
+
}
|
|
268
|
+
for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
|
|
269
|
+
if (Object.hasOwn(value, key)) validateSchema(childSchema, value[key], `${path}.${key}`, errors);
|
|
270
|
+
}
|
|
271
|
+
if (schema.additionalProperties === false) {
|
|
272
|
+
const allowed = new Set(Object.keys(schema.properties ?? {}));
|
|
273
|
+
for (const key of Object.keys(value)) {
|
|
274
|
+
if (!allowed.has(key)) errors.push(`${path}.${key} is not allowed`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function validateCandidate(schema, value, path) {
|
|
281
|
+
const errors = [];
|
|
282
|
+
validateSchema(schema, value, path, errors);
|
|
283
|
+
return errors.length === 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function typeMatches(type, value) {
|
|
287
|
+
const types = Array.isArray(type) ? type : [type];
|
|
288
|
+
return types.some((candidate) => {
|
|
289
|
+
if (candidate === 'array') return Array.isArray(value);
|
|
290
|
+
if (candidate === 'null') return value === null;
|
|
291
|
+
if (candidate === 'integer') return Number.isInteger(value);
|
|
292
|
+
if (candidate === 'number') return typeof value === 'number' && Number.isFinite(value);
|
|
293
|
+
if (candidate === 'object') return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
294
|
+
return typeof value === candidate;
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function formatType(type) {
|
|
299
|
+
return Array.isArray(type) ? type.join('|') : type;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export { STATUS_VALUES };
|