@dotdrelle/wiki-manager 0.15.64 → 0.15.70
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 +10 -3
- package/README.md +54 -0
- package/agent-runtimes.example.json +68 -0
- package/agents.docker-compose.yml +35 -1
- package/docker-compose.yml +3 -3
- package/package.json +3 -2
- package/src/agent/graph.js +15 -14
- package/src/agent/graph.test.js +1 -1
- package/src/agent/skillRecursion.test.js +13 -12
- package/src/cli/wiki-manager.js +124 -36
- package/src/commands/slash.js +48 -13
- package/src/contracts/schemas.js +67 -0
- package/src/core/activity.js +5 -0
- package/src/core/agentEvents.js +18 -1
- package/src/core/agentLoop.js +3 -3
- package/src/core/agentLoop.test.js +1 -1
- package/src/core/buildInfo.json +2 -2
- package/src/core/dockerCompose.test.js +8 -40
- package/src/core/env.js +14 -0
- package/src/core/env.test.js +19 -0
- package/src/core/googleGrants.test.js +1 -1
- package/src/core/mcp.js +1 -1
- package/src/core/runtimeEventAdapter.js +81 -0
- package/src/core/runtimeEventAdapter.test.js +61 -0
- package/src/core/skillChainView.test.js +2 -2
- package/src/core/skillCompiler.test.js +1 -1
- package/src/core/skillInvocation.js +13 -8
- package/src/core/startupCheck.js +58 -0
- package/src/core/startupCheck.test.js +29 -1
- package/src/orchestrator/agentRegistry.js +1 -22
- package/src/orchestrator/assignmentManager.js +16 -4
- package/src/orchestrator/capabilityRegistry.js +8 -1
- package/src/orchestrator/dispatcher.js +361 -2
- package/src/orchestrator/dispatcher.test.js +112 -1
- package/src/orchestrator/objectiveResolver.js +10 -6
- package/src/orchestrator/objectiveResolver.test.js +26 -27
- package/src/orchestrator/providers/deepAgentsProvider.js +168 -0
- package/src/orchestrator/providers/deepAgentsProvider.test.js +178 -0
- package/src/orchestrator/providers/dispatcherExternalRuntime.test.js +409 -0
- package/src/orchestrator/providers/fakeRuntimeProvider.js +164 -0
- package/src/orchestrator/providers/fakeRuntimeProvider.test.js +201 -0
- package/src/orchestrator/providers/runtimeProvider.js +101 -0
- package/src/orchestrator/providers/runtimeProviders.js +325 -0
- package/src/orchestrator/providers/runtimeProviders.test.js +361 -0
- package/src/orchestrator/resultAggregator.js +35 -2
- package/src/orchestrator/resultAggregator.test.js +62 -0
- package/src/runtime/recoveryManager.js +70 -5
- package/src/runtime/runner.js +6 -6
- package/src/runtime/runner.test.js +1 -1
- package/src/runtime/skillChain.e2e.test.js +2 -2
- package/src/runtime/supervisor.js +5 -10
- package/src/shell/RightPane.tsx +25 -9
- package/src/shell/StartupScreen.tsx +44 -7
- package/src/shell/repl.js +12 -12
- package/src/shell/repl.test.js +18 -5
- package/src/shell/tui.tsx +6 -6
- package/src/shell/useAgent.ts +1 -1
- package/src/shell/useSession.ts +1 -1
- package/wiki-workspace +19 -3
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RUNTIME_PROTOCOL_VERSION,
|
|
3
|
+
RuntimeProviderUnavailableError,
|
|
4
|
+
normalizeRuntimeEvent,
|
|
5
|
+
} from './runtimeProvider.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* DeepAgentsProvider — client HTTP vers un runtime Deep Agents externe
|
|
9
|
+
* (RFC § 11, option A). Implémente le contrat RuntimeProvider :
|
|
10
|
+
*
|
|
11
|
+
* GET {endpoint}/health -> { ok, version? } (describe)
|
|
12
|
+
* GET {endpoint}/capabilities -> [{ name, operations }] (discover)
|
|
13
|
+
* POST {endpoint}/runs -> { runId, status } (execute)
|
|
14
|
+
* GET {endpoint}/runs/:id -> { runId, status, result? } (status)
|
|
15
|
+
* POST {endpoint}/runs/:id/cancel -> { ok } (cancel)
|
|
16
|
+
* GET {endpoint}/runs/:id/events -> SSE `data: {json}` (subscribe)
|
|
17
|
+
*
|
|
18
|
+
* `fetchImpl` est injectable pour les tests ; par défaut `globalThis.fetch`
|
|
19
|
+
* (Node 22 / Bun). Un runtime injoignable se manifeste par une `describe()`
|
|
20
|
+
* qui retourne `health: 'unavailable'` (jamais une exception) : l'isolation de
|
|
21
|
+
* panne du discovery s'appuie dessus.
|
|
22
|
+
*/
|
|
23
|
+
export function createDeepAgentsProvider({
|
|
24
|
+
id = 'deepagents',
|
|
25
|
+
endpoint = 'http://agent-runtime:7789',
|
|
26
|
+
capabilities = null,
|
|
27
|
+
fetchImpl = globalThis.fetch,
|
|
28
|
+
headers = {},
|
|
29
|
+
version = null,
|
|
30
|
+
timeoutMs = 10_000,
|
|
31
|
+
} = {}) {
|
|
32
|
+
const base = String(endpoint).replace(/\/+$/, '');
|
|
33
|
+
|
|
34
|
+
async function httpJson(path, { method = 'GET', body = null, signal = null } = {}) {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const timer = setTimeout(() => controller.abort(new Error(`timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
37
|
+
const onAbort = () => controller.abort();
|
|
38
|
+
signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetchImpl(`${base}${path}`, {
|
|
41
|
+
method,
|
|
42
|
+
headers: { 'content-type': 'application/json', accept: 'application/json', ...headers },
|
|
43
|
+
...(body !== null ? { body: JSON.stringify(body) } : {}),
|
|
44
|
+
signal: controller.signal,
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new RuntimeProviderUnavailableError(id, `HTTP ${response.status} on ${method} ${path}`);
|
|
48
|
+
}
|
|
49
|
+
return await response.json();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error instanceof RuntimeProviderUnavailableError) throw error;
|
|
52
|
+
throw new RuntimeProviderUnavailableError(id, error instanceof Error ? error.message : String(error));
|
|
53
|
+
} finally {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
signal?.removeEventListener?.('abort', onAbort);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
async describe() {
|
|
61
|
+
let health = 'available';
|
|
62
|
+
let runtimeVersion = version ?? null;
|
|
63
|
+
let lastError = null;
|
|
64
|
+
try {
|
|
65
|
+
const info = await httpJson('/health');
|
|
66
|
+
runtimeVersion = info?.version ?? runtimeVersion;
|
|
67
|
+
health = info?.ok === false ? 'unavailable' : 'available';
|
|
68
|
+
} catch (error) {
|
|
69
|
+
health = 'unavailable';
|
|
70
|
+
lastError = error?.reason ?? (error instanceof Error ? error.message : String(error));
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
runtime: id,
|
|
74
|
+
version: runtimeVersion ?? 'unknown',
|
|
75
|
+
protocolVersion: RUNTIME_PROTOCOL_VERSION,
|
|
76
|
+
health,
|
|
77
|
+
...(lastError ? { error: lastError } : {}),
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
async discoverCapabilities() {
|
|
81
|
+
if (Array.isArray(capabilities)) return capabilities;
|
|
82
|
+
const list = await httpJson('/capabilities');
|
|
83
|
+
return Array.isArray(list) ? list : [];
|
|
84
|
+
},
|
|
85
|
+
async execute(request = {}) {
|
|
86
|
+
const accepted = await httpJson('/runs', {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
body: {
|
|
89
|
+
objective: request.objective ?? request.input ?? null,
|
|
90
|
+
operation: request.operation ?? null,
|
|
91
|
+
capability: request.capability ?? null,
|
|
92
|
+
arguments: request.arguments ?? {},
|
|
93
|
+
workspace: request.workspace ?? null,
|
|
94
|
+
model: request.model ?? null,
|
|
95
|
+
language: request.language ?? null,
|
|
96
|
+
mcp: Array.isArray(request.mcp) ? request.mcp : [],
|
|
97
|
+
systemPrompt: request.systemPrompt ?? null,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
const runId = String(accepted?.runId ?? '');
|
|
101
|
+
if (!runId) throw new RuntimeProviderUnavailableError(id, 'execute did not return runId');
|
|
102
|
+
return { runId, status: String(accepted?.status ?? 'running') };
|
|
103
|
+
},
|
|
104
|
+
async status(runId) {
|
|
105
|
+
const state = await httpJson(`/runs/${encodeURIComponent(String(runId))}`);
|
|
106
|
+
return {
|
|
107
|
+
runId: String(state?.runId ?? runId),
|
|
108
|
+
status: String(state?.status ?? 'running'),
|
|
109
|
+
...(state?.result ? { result: state.result } : {}),
|
|
110
|
+
// The gateway reports its failure at the TOP level; dropping it here
|
|
111
|
+
// swallowed the only actionable sentence ("Unable to infer model
|
|
112
|
+
// provider…") and left the manager to invent a cause.
|
|
113
|
+
...(state?.error ? { error: state.error } : {}),
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
async cancel(runId) {
|
|
117
|
+
await httpJson(`/runs/${encodeURIComponent(String(runId))}/cancel`, { method: 'POST' });
|
|
118
|
+
},
|
|
119
|
+
async approve(runId, { approved = true, reason = null, scope = null } = {}) {
|
|
120
|
+
await httpJson(`/runs/${encodeURIComponent(String(runId))}/approve`, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
body: {
|
|
123
|
+
approved: approved === true,
|
|
124
|
+
...(reason ? { reason } : {}),
|
|
125
|
+
...(scope ? { scope } : {}),
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
subscribe(runId, listener) {
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
const url = `${base}/runs/${encodeURIComponent(String(runId))}/events`;
|
|
132
|
+
void (async () => {
|
|
133
|
+
try {
|
|
134
|
+
const response = await fetchImpl(url, {
|
|
135
|
+
headers: { accept: 'text/event-stream', ...headers },
|
|
136
|
+
signal: controller.signal,
|
|
137
|
+
});
|
|
138
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
139
|
+
const reader = response.body.getReader();
|
|
140
|
+
const decoder = new TextDecoder();
|
|
141
|
+
let buffer = '';
|
|
142
|
+
while (true) {
|
|
143
|
+
const { done, value } = await reader.read();
|
|
144
|
+
if (done) break;
|
|
145
|
+
buffer += decoder.decode(value, { stream: true });
|
|
146
|
+
const blocks = buffer.split('\n\n');
|
|
147
|
+
buffer = blocks.pop() ?? '';
|
|
148
|
+
for (const block of blocks) {
|
|
149
|
+
let data = '';
|
|
150
|
+
for (const line of block.split('\n')) {
|
|
151
|
+
if (line.startsWith('data: ')) data += line.slice(6);
|
|
152
|
+
}
|
|
153
|
+
if (!data) continue;
|
|
154
|
+
try {
|
|
155
|
+
listener(normalizeRuntimeEvent(JSON.parse(data)));
|
|
156
|
+
} catch {
|
|
157
|
+
// malformed or out-of-contract frame — skip
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// stream ended or aborted — the unsubscribe path is a no-op
|
|
163
|
+
}
|
|
164
|
+
})();
|
|
165
|
+
return () => controller.abort();
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createDeepAgentsProvider } from './deepAgentsProvider.js';
|
|
4
|
+
import { RUNTIME_PROTOCOL_VERSION, assertRuntimeProvider } from './runtimeProvider.js';
|
|
5
|
+
import { resolveRuntimeProviders } from './runtimeProviders.js';
|
|
6
|
+
|
|
7
|
+
function jsonResponse(status, data, ok = status < 400) {
|
|
8
|
+
return { ok, status, json: async () => data };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function mockFetch(routes) {
|
|
12
|
+
const calls = [];
|
|
13
|
+
const fetchImpl = async (url, options = {}) => {
|
|
14
|
+
const parsed = new URL(url);
|
|
15
|
+
const key = `${options.method ?? 'GET'} ${parsed.pathname}`;
|
|
16
|
+
calls.push({ method: options.method ?? 'GET', path: parsed.pathname, body: options.body, url });
|
|
17
|
+
const handler = routes[key];
|
|
18
|
+
if (!handler) return { ok: false, status: 404, json: async () => ({}) };
|
|
19
|
+
return handler(parsed, options);
|
|
20
|
+
};
|
|
21
|
+
fetchImpl.calls = calls;
|
|
22
|
+
return fetchImpl;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sseBody(events) {
|
|
26
|
+
const encoder = new TextEncoder();
|
|
27
|
+
const chunks = events.map((event) => encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
|
|
28
|
+
return new ReadableStream({
|
|
29
|
+
start(controller) {
|
|
30
|
+
for (const chunk of chunks) controller.enqueue(chunk);
|
|
31
|
+
controller.close();
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function waitFor(predicate, { timeoutMs = 1000 } = {}) {
|
|
37
|
+
const deadline = Date.now() + timeoutMs;
|
|
38
|
+
while (Date.now() < deadline) {
|
|
39
|
+
if (predicate()) return;
|
|
40
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 2));
|
|
41
|
+
}
|
|
42
|
+
throw new Error('condition not met in time');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test('describe reports available and reads the version from /health', async () => {
|
|
46
|
+
const fetchImpl = mockFetch({
|
|
47
|
+
'GET /health': () => jsonResponse(200, { ok: true, version: '0.6.10' }),
|
|
48
|
+
});
|
|
49
|
+
const provider = createDeepAgentsProvider({ id: 'deepagents', endpoint: 'http://agent-runtime:8080', fetchImpl });
|
|
50
|
+
assertRuntimeProvider(provider);
|
|
51
|
+
|
|
52
|
+
const description = await provider.describe();
|
|
53
|
+
assert.equal(description.runtime, 'deepagents');
|
|
54
|
+
assert.equal(description.version, '0.6.10');
|
|
55
|
+
assert.equal(description.protocolVersion, RUNTIME_PROTOCOL_VERSION);
|
|
56
|
+
assert.equal(description.health, 'available');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('describe reports unavailable when /health fails, without throwing', async () => {
|
|
60
|
+
const fetchImpl = mockFetch({}); // every route 404
|
|
61
|
+
const provider = createDeepAgentsProvider({ id: 'deepagents', endpoint: 'http://agent-runtime:8080', fetchImpl });
|
|
62
|
+
|
|
63
|
+
const description = await provider.describe();
|
|
64
|
+
assert.equal(description.health, 'unavailable');
|
|
65
|
+
assert.ok(description.error, 'the reason is carried');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('discoverCapabilities fetches /capabilities', async () => {
|
|
69
|
+
const fetchImpl = mockFetch({
|
|
70
|
+
'GET /capabilities': () => jsonResponse(200, [{ name: 'agent.review', operations: ['run'] }]),
|
|
71
|
+
});
|
|
72
|
+
const provider = createDeepAgentsProvider({ endpoint: 'http://agent-runtime:8080', fetchImpl });
|
|
73
|
+
|
|
74
|
+
assert.deepEqual(await provider.discoverCapabilities(), [{ name: 'agent.review', operations: ['run'] }]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('discoverCapabilities uses the static config when provided', async () => {
|
|
78
|
+
const fetchImpl = mockFetch({});
|
|
79
|
+
const provider = createDeepAgentsProvider({
|
|
80
|
+
endpoint: 'http://agent-runtime:8080',
|
|
81
|
+
capabilities: [{ name: 'agent.review', operations: ['run'] }],
|
|
82
|
+
fetchImpl,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
assert.deepEqual(await provider.discoverCapabilities(), [{ name: 'agent.review', operations: ['run'] }]);
|
|
86
|
+
assert.equal(fetchImpl.calls.length, 0, 'no HTTP call when capabilities are static');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('execute POSTs /runs and returns the runId', async () => {
|
|
90
|
+
const fetchImpl = mockFetch({
|
|
91
|
+
'POST /runs': () => jsonResponse(200, { runId: 'run-1', status: 'running' }),
|
|
92
|
+
});
|
|
93
|
+
const provider = createDeepAgentsProvider({ endpoint: 'http://agent-runtime:8080', fetchImpl });
|
|
94
|
+
|
|
95
|
+
const run = await provider.execute({
|
|
96
|
+
objective: 'analyse JUNO',
|
|
97
|
+
operation: 'run',
|
|
98
|
+
arguments: {},
|
|
99
|
+
model: { baseUrl: 'http://llm:11434/v1', model: 'qwen3:14b', apiKey: 'secret' },
|
|
100
|
+
});
|
|
101
|
+
assert.deepEqual(run, { runId: 'run-1', status: 'running' });
|
|
102
|
+
assert.equal(fetchImpl.calls[0].path, '/runs');
|
|
103
|
+
const sent = JSON.parse(fetchImpl.calls[0].body);
|
|
104
|
+
assert.equal(sent.objective, 'analyse JUNO');
|
|
105
|
+
assert.deepEqual(sent.model, { baseUrl: 'http://llm:11434/v1', model: 'qwen3:14b', apiKey: 'secret' });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('status and cancel hit the run-scoped routes', async () => {
|
|
109
|
+
const fetchImpl = mockFetch({
|
|
110
|
+
'GET /runs/run-1': () => jsonResponse(200, { runId: 'run-1', status: 'completed', result: { status: 'completed' } }),
|
|
111
|
+
'POST /runs/run-1/cancel': () => jsonResponse(200, { ok: true }),
|
|
112
|
+
});
|
|
113
|
+
const provider = createDeepAgentsProvider({ endpoint: 'http://agent-runtime:8080', fetchImpl });
|
|
114
|
+
|
|
115
|
+
const status = await provider.status('run-1');
|
|
116
|
+
assert.equal(status.status, 'completed');
|
|
117
|
+
assert.equal(status.result.status, 'completed');
|
|
118
|
+
|
|
119
|
+
await provider.cancel('run-1');
|
|
120
|
+
assert.equal(fetchImpl.calls.at(-1).path, '/runs/run-1/cancel');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('status forwards the top-level error reported by the gateway', async () => {
|
|
124
|
+
const fetchImpl = mockFetch({
|
|
125
|
+
'GET /runs/run-2': () => jsonResponse(200, { runId: 'run-2', status: 'failed', error: 'Unable to infer model provider' }),
|
|
126
|
+
});
|
|
127
|
+
const provider = createDeepAgentsProvider({ endpoint: 'http://agent-runtime:8080', fetchImpl });
|
|
128
|
+
|
|
129
|
+
const status = await provider.status('run-2');
|
|
130
|
+
assert.equal(status.status, 'failed');
|
|
131
|
+
assert.equal(status.error, 'Unable to infer model provider');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('subscribe parses the SSE stream and forwards normalized events', async () => {
|
|
135
|
+
const fetchImpl = mockFetch({
|
|
136
|
+
'GET /runs/run-1/events': () => ({
|
|
137
|
+
ok: true,
|
|
138
|
+
status: 200,
|
|
139
|
+
body: sseBody([
|
|
140
|
+
{ type: 'tool_started', tool: 'wiki_search' },
|
|
141
|
+
{ type: 'tool_finished', tool: 'wiki_search', resultSummary: '17 found' },
|
|
142
|
+
]),
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
const provider = createDeepAgentsProvider({ endpoint: 'http://agent-runtime:8080', fetchImpl });
|
|
146
|
+
|
|
147
|
+
const events = [];
|
|
148
|
+
provider.subscribe('run-1', (event) => events.push(event));
|
|
149
|
+
|
|
150
|
+
await waitFor(() => events.length === 2);
|
|
151
|
+
assert.deepEqual(events.map((event) => event.type), ['tool_started', 'tool_finished']);
|
|
152
|
+
assert.equal(events[0].tool, 'wiki_search');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('approve posts the decision to the run approval route', async () => {
|
|
156
|
+
const fetchImpl = mockFetch({
|
|
157
|
+
'POST /runs/run-1/approve': () => jsonResponse(200, { ok: true }),
|
|
158
|
+
});
|
|
159
|
+
const provider = createDeepAgentsProvider({ endpoint: 'http://agent-runtime:8080', fetchImpl });
|
|
160
|
+
|
|
161
|
+
await provider.approve('run-1', { approved: true, scope: ['email'] });
|
|
162
|
+
|
|
163
|
+
assert.equal(fetchImpl.calls.at(-1).path, '/runs/run-1/approve');
|
|
164
|
+
const sent = JSON.parse(fetchImpl.calls.at(-1).body);
|
|
165
|
+
assert.equal(sent.approved, true);
|
|
166
|
+
assert.deepEqual(sent.scope, ['email']);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('the deepagents factory is reachable from the agentRuntimes config', () => {
|
|
170
|
+
const { providers, skipped } = resolveRuntimeProviders([
|
|
171
|
+
{ id: 'deepagents', type: 'deepagents', endpoint: 'http://agent-runtime:8080', capabilities: [{ name: 'agent.review', operations: ['run'] }] },
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
assert.equal(skipped.length, 0);
|
|
175
|
+
assert.equal(providers.length, 1);
|
|
176
|
+
assert.equal(providers[0].type, 'deepagents');
|
|
177
|
+
assertRuntimeProvider(providers[0].provider);
|
|
178
|
+
});
|