@dotdrelle/wiki-manager 0.15.66 → 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 +12 -11
- package/src/agent/skillRecursion.test.js +13 -12
- package/src/cli/wiki-manager.js +124 -36
- package/src/commands/slash.js +38 -3
- package/src/contracts/schemas.js +67 -0
- package/src/core/activity.js +5 -0
- package/src/core/agentEvents.js +18 -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/skillChain.e2e.test.js +2 -2
- package/src/runtime/supervisor.js +5 -10
- package/src/shell/RightPane.tsx +9 -1
- package/src/shell/StartupScreen.tsx +44 -7
- package/src/shell/repl.test.js +13 -0
- package/wiki-workspace +19 -3
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter RuntimeEvent -> événements natifs du manager (RFC § 16).
|
|
3
|
+
*
|
|
4
|
+
* Le flux d'événements d'un runtime externe est traduit ici dans le
|
|
5
|
+
* vocabulaire du reducer (`core/agentEvents.js`) SANS aucune refonte de l'UI :
|
|
6
|
+
*
|
|
7
|
+
* - `message` devient un `assistant_message` : c'est ce que Donna affiche ;
|
|
8
|
+
* - les événements d'action (`tool_*`, `subagent_*`, `approval_required`)
|
|
9
|
+
* deviennent des lignes de journal structurées (`runtime_log`) ;
|
|
10
|
+
* - le raisonnement privé (`agent_thinking`) n'est jamais ré-émis (RFC § 15) ;
|
|
11
|
+
* - les événements terminaux (`run_completed`/`run_failed`/`run_cancelled`)
|
|
12
|
+
* ne sont pas ré-émis : ils sont déjà portés par le poll `status()` du
|
|
13
|
+
* dispatcher, qui construit le résultat de tâche à partir de là.
|
|
14
|
+
*
|
|
15
|
+
* La fonction est pure et déterministe : un événement produit zéro ou
|
|
16
|
+
* plusieurs descripteurs `{ type, payload }`. Le dispatcher porte l'identité
|
|
17
|
+
* run/task au moment de la dépêche.
|
|
18
|
+
*/
|
|
19
|
+
export function mapRuntimeEvent(event) {
|
|
20
|
+
const type = String(event?.type ?? '');
|
|
21
|
+
switch (type) {
|
|
22
|
+
case 'message': {
|
|
23
|
+
const content = String(event?.content ?? event?.message ?? '').trim();
|
|
24
|
+
return content ? [{ type: 'assistant_message', payload: { content } }] : [];
|
|
25
|
+
}
|
|
26
|
+
case 'tool_started':
|
|
27
|
+
return log(`tool ${toolLabel(event)} started`);
|
|
28
|
+
case 'tool_finished': {
|
|
29
|
+
const duration = Number.isFinite(Number(event?.durationMs))
|
|
30
|
+
? ` (${Math.round(Number(event.durationMs))}ms)`
|
|
31
|
+
: '';
|
|
32
|
+
const summary = String(event?.resultSummary ?? '').trim();
|
|
33
|
+
const error = String(event?.error ?? '').trim();
|
|
34
|
+
if (error) return log(`tool ${toolLabel(event)} failed: ${error}${duration}`);
|
|
35
|
+
return log(`tool ${toolLabel(event)} done${duration}${summary ? ` — ${summary}` : ''}`);
|
|
36
|
+
}
|
|
37
|
+
case 'subagent_started':
|
|
38
|
+
return log(`subagent ${subagentLabel(event)} started`);
|
|
39
|
+
case 'subagent_finished':
|
|
40
|
+
return log(`subagent ${subagentLabel(event)} finished`);
|
|
41
|
+
case 'approval_required': {
|
|
42
|
+
// Human-in-the-loop du runtime (RFC § 14) : l'analyse pré-exécution
|
|
43
|
+
// devient une demande d'approbation native. Les mutations annoncées
|
|
44
|
+
// deviennent les classes d'approbation ; le dispatcher attend qu'un
|
|
45
|
+
// grant humain les couvre avant de débloquer le runtime.
|
|
46
|
+
const proposal = event?.proposal && typeof event.proposal === 'object' ? event.proposal : {};
|
|
47
|
+
const mutations = Array.isArray(proposal?.mutations) ? proposal.mutations : [];
|
|
48
|
+
const classes = [...new Set(mutations.map((mutation) => String(mutation?.kind ?? '').trim()).filter(Boolean))];
|
|
49
|
+
return [{
|
|
50
|
+
type: 'approval.requested',
|
|
51
|
+
payload: {
|
|
52
|
+
approvalId: String(event?.approvalId ?? 'runtime-approval'),
|
|
53
|
+
scope: 'run',
|
|
54
|
+
approvalClasses: classes,
|
|
55
|
+
reason: String(event?.reason ?? proposal?.summary ?? ''),
|
|
56
|
+
proposal,
|
|
57
|
+
},
|
|
58
|
+
}];
|
|
59
|
+
}
|
|
60
|
+
case 'run_started':
|
|
61
|
+
case 'run_created':
|
|
62
|
+
case 'agent_thinking':
|
|
63
|
+
case 'run_completed':
|
|
64
|
+
case 'run_failed':
|
|
65
|
+
case 'run_cancelled':
|
|
66
|
+
default:
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function log(message) {
|
|
72
|
+
return [{ type: 'runtime_log', payload: { message } }];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toolLabel(event) {
|
|
76
|
+
return String(event?.tool ?? event?.name ?? 'tool');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function subagentLabel(event) {
|
|
80
|
+
return String(event?.subagent ?? event?.tool ?? event?.name ?? 'subagent');
|
|
81
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { mapRuntimeEvent } from './runtimeEventAdapter.js';
|
|
4
|
+
|
|
5
|
+
test('message becomes an assistant_message', () => {
|
|
6
|
+
const mapped = mapRuntimeEvent({ type: 'message', content: 'analysis complete' });
|
|
7
|
+
assert.deepEqual(mapped, [{ type: 'assistant_message', payload: { content: 'analysis complete' } }]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('an empty message produces nothing', () => {
|
|
11
|
+
assert.deepEqual(mapRuntimeEvent({ type: 'message', content: ' ' }), []);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('tool_started and tool_finished become structured log lines', () => {
|
|
15
|
+
const started = mapRuntimeEvent({ type: 'tool_started', tool: 'wiki_search' });
|
|
16
|
+
assert.equal(started.length, 1);
|
|
17
|
+
assert.equal(started[0].type, 'runtime_log');
|
|
18
|
+
assert.match(started[0].payload.message, /wiki_search started/);
|
|
19
|
+
|
|
20
|
+
const finished = mapRuntimeEvent({ type: 'tool_finished', tool: 'wiki_search', durationMs: 842, resultSummary: '17 documents found' });
|
|
21
|
+
assert.match(finished[0].payload.message, /wiki_search done \(842ms\) — 17 documents found/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('a failed tool is reported as such, not as a success', () => {
|
|
25
|
+
const mapped = mapRuntimeEvent({ type: 'tool_finished', tool: 'wiki_read', error: 'permission denied' });
|
|
26
|
+
assert.match(mapped[0].payload.message, /wiki_read failed: permission denied/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('subagent events surface as logs', () => {
|
|
30
|
+
assert.match(mapRuntimeEvent({ type: 'subagent_started', subagent: 'reviewer' })[0].payload.message, /subagent reviewer started/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('approval_required becomes an approval.requested with the proposal classes', () => {
|
|
34
|
+
const mapped = mapRuntimeEvent({
|
|
35
|
+
type: 'approval_required',
|
|
36
|
+
approvalId: 'prop-1',
|
|
37
|
+
reason: 'analysis complete',
|
|
38
|
+
proposal: {
|
|
39
|
+
summary: 'analyse',
|
|
40
|
+
mutations: [{ kind: 'send_email' }, { kind: 'plan_expansion' }],
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
assert.equal(mapped.length, 1);
|
|
45
|
+
assert.equal(mapped[0].type, 'approval.requested');
|
|
46
|
+
assert.equal(mapped[0].payload.approvalId, 'prop-1');
|
|
47
|
+
assert.equal(mapped[0].payload.scope, 'run');
|
|
48
|
+
assert.deepEqual(mapped[0].payload.approvalClasses, ['send_email', 'plan_expansion']);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('private reasoning and terminal events are never re-emitted', () => {
|
|
52
|
+
assert.deepEqual(mapRuntimeEvent({ type: 'agent_thinking', content: 'secret chain of thought' }), []);
|
|
53
|
+
assert.deepEqual(mapRuntimeEvent({ type: 'run_started' }), []);
|
|
54
|
+
assert.deepEqual(mapRuntimeEvent({ type: 'run_completed' }), []);
|
|
55
|
+
assert.deepEqual(mapRuntimeEvent({ type: 'run_failed' }), []);
|
|
56
|
+
assert.deepEqual(mapRuntimeEvent({ type: 'run_cancelled' }), []);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('an unknown event type produces nothing', () => {
|
|
60
|
+
assert.deepEqual(mapRuntimeEvent({ type: 'made_up' }), []);
|
|
61
|
+
});
|
|
@@ -54,9 +54,9 @@ test('the selection reason is humanized, not leaked as an audit enum', () => {
|
|
|
54
54
|
assert.equal(selectionKindLabel('description_match'), 'description match');
|
|
55
55
|
assert.equal(selectionKindLabel(null), null);
|
|
56
56
|
const [chain] = projectSkillChains([
|
|
57
|
-
{ id: 'c0', chainId: 'k', chainSequence: 0, skillName: 'wiki-
|
|
57
|
+
{ id: 'c0', chainId: 'k', chainSequence: 0, skillName: 'wiki-build', selectionKind: 'explicit_name', status: 'running', input: '/wiki-build' },
|
|
58
58
|
]);
|
|
59
59
|
assert.equal(chain.selectionKind, 'explicit_name');
|
|
60
60
|
assert.equal(chain.selectionLabel, 'explicit name');
|
|
61
|
-
assert.equal(renderSkillChain(chain).split('\n')[0], 'wiki-
|
|
61
|
+
assert.equal(renderSkillChain(chain).split('\n')[0], 'wiki-build · explicit name');
|
|
62
62
|
});
|
|
@@ -31,7 +31,7 @@ test('validation rejects technical routing details', () => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
test('scaffold skills preserve existing capabilities and split only wiki-sync', async () => {
|
|
34
|
-
const expected = { pipeline: 1, 'wiki-ingest':
|
|
34
|
+
const expected = { pipeline: 1, 'wiki-ingest': 1, 'wiki-build': 1, deliver: 1, diagnose: 1, status: 1, 'new-template': 1, 'wiki-sync': 2 };
|
|
35
35
|
for (const [name, count] of Object.entries(expected)) {
|
|
36
36
|
const raw = readFileSync(resolve('../llm-wiki/scaffold/workspace/.wiki/skills', `${name}.md`), 'utf8');
|
|
37
37
|
const { meta, body } = parseFrontmatter(raw);
|
|
@@ -24,11 +24,12 @@ export function explicitSkillReference(input, skillName, language = null) {
|
|
|
24
24
|
elle ?
|
|
25
25
|
|
|
26
26
|
Le corps d'une compétence est compilé en intentions métier, et une intention
|
|
27
|
-
décrit forcément ce que fait une compétence voisine :
|
|
28
|
-
`/wiki-ingest`
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
décrit forcément ce que fait une compétence voisine : c'est ainsi qu'un seul
|
|
28
|
+
`/wiki-ingest` relançait autrefois la compétence sœur qui reconstruisait la
|
|
29
|
+
grille de concepts, qui relançait la suivante — concepts et taxonomie produits
|
|
30
|
+
plusieurs fois pour une seule demande. (Ces compétences sœurs ont disparu avec
|
|
31
|
+
la simplification 0.15.66 ; la garde contre la sélection par description
|
|
32
|
+
reste, c'est elle que ces règles verrouillent.)
|
|
32
33
|
|
|
33
34
|
La composition volontaire reste possible : un corps qui écrit `/deliver` ou
|
|
34
35
|
« the deliver skill » nomme sa cible, et se distingue ainsi d'une intention
|
|
@@ -44,8 +45,8 @@ export function objectiveNamesSkill(input, skillName) {
|
|
|
44
45
|
if (text.toLowerCase() === raw.toLowerCase()) return true;
|
|
45
46
|
/*
|
|
46
47
|
Le nom seul ne suffit pas : plusieurs compétences du scaffold portent un nom
|
|
47
|
-
qui est aussi un mot courant. « Run the production pipeline steps
|
|
48
|
-
|
|
48
|
+
qui est aussi un mot courant. « Run the production pipeline steps ingest,
|
|
49
|
+
build, export and polish » nomme ainsi la compétence `pipeline`, qui
|
|
49
50
|
relance ingest + build + export + polish — bien pire que la cascade qu'on
|
|
50
51
|
corrige. Le nom doit donc être cité EN TANT QUE compétence : forme slash, ou
|
|
51
52
|
tournure explicite. La borne droite est écrite à la main, `\b` ne bornant pas
|
|
@@ -61,7 +62,11 @@ export function objectiveNamesSkill(input, skillName) {
|
|
|
61
62
|
const keyword = '(?:skill|workflow|compétence)';
|
|
62
63
|
return [
|
|
63
64
|
new RegExp(`(?:^|[^A-Za-z0-9_-])/${name}${slashEnd}`, 'i'),
|
|
64
|
-
|
|
65
|
+
// Same path-vs-invocation distinction as the slash form above: unlike
|
|
66
|
+
// the third pattern below, nothing after this one forces a following
|
|
67
|
+
// whitespace, so a trailing `/` here would otherwise satisfy `end` and
|
|
68
|
+
// let "the skill /wiki-build/export-context" match `wiki-build`.
|
|
69
|
+
new RegExp(`\\b${keyword}\\s+/?${name}${slashEnd}`, 'i'),
|
|
65
70
|
new RegExp(`(?:^|[^A-Za-z0-9_-])/?${name}${end}\\s+${keyword}\\b`, 'i'),
|
|
66
71
|
new RegExp(`/skills\\s+run\\s+${name}${end}`, 'i'),
|
|
67
72
|
].some((pattern) => pattern.test(text));
|
package/src/core/startupCheck.js
CHANGED
|
@@ -8,6 +8,7 @@ import { resolveAgentsComposeContext } from './agentsCompose.js';
|
|
|
8
8
|
import { buildMcpStatus, discoverMcpTools } from './mcp.js';
|
|
9
9
|
import { listWikircProfiles, loadWikircProfile, summarizeWikircConfig } from './wikirc.js';
|
|
10
10
|
import { listWorkspaces, managerRoot, workspacesDir } from './workspaces.js';
|
|
11
|
+
import { loadAgentRuntimesConfig, resolveRuntimeProviders } from '../orchestrator/providers/runtimeProviders.js';
|
|
11
12
|
|
|
12
13
|
const execFileAsync = promisify(execFile);
|
|
13
14
|
const DEFAULT_CONNECTIVITY_URL = 'https://registry.npmjs.org/-/ping';
|
|
@@ -326,6 +327,58 @@ export async function checkMcpConnections(workspace, {
|
|
|
326
327
|
};
|
|
327
328
|
}
|
|
328
329
|
|
|
330
|
+
// Agentic runtime gateway (GATEWAY_ENABLED / agent-runtimes.json). HTTP
|
|
331
|
+
// probe, independent of Docker: the gateway may be a host-native service.
|
|
332
|
+
// Nothing declared or enabled = skipped, never degraded — an optional engine
|
|
333
|
+
// must not gate the startup of the manager it complements.
|
|
334
|
+
export async function checkAgenticRuntimes({
|
|
335
|
+
loadConfig = loadAgentRuntimesConfig,
|
|
336
|
+
resolve = resolveRuntimeProviders,
|
|
337
|
+
env = process.env,
|
|
338
|
+
} = {}) {
|
|
339
|
+
let config;
|
|
340
|
+
try {
|
|
341
|
+
config = loadConfig({ env });
|
|
342
|
+
} catch (err) {
|
|
343
|
+
return {
|
|
344
|
+
ok: false,
|
|
345
|
+
detail: 'Agentic runtime configuration invalid',
|
|
346
|
+
context: { error: commandError(err), runtimes: [] },
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const { providers, skipped } = resolve(config);
|
|
350
|
+
if (providers.length === 0 && skipped.length === 0) {
|
|
351
|
+
return { ok: true, skipped: true, detail: 'No agentic runtime enabled', context: { runtimes: [] } };
|
|
352
|
+
}
|
|
353
|
+
const runtimes = [];
|
|
354
|
+
for (const item of skipped) {
|
|
355
|
+
runtimes.push({ name: item.id || '(unnamed)', status: 'skipped', reason: item.reason });
|
|
356
|
+
}
|
|
357
|
+
for (const { id, provider } of providers) {
|
|
358
|
+
try {
|
|
359
|
+
const description = await provider.describe();
|
|
360
|
+
runtimes.push({
|
|
361
|
+
name: id,
|
|
362
|
+
status: description.health === 'unavailable' ? 'failed' : 'connected',
|
|
363
|
+
capabilities: (description.capabilities ?? [])
|
|
364
|
+
.map((capability) => capability?.name ?? capability?.id ?? '')
|
|
365
|
+
.filter(Boolean),
|
|
366
|
+
reason: description.health === 'unavailable' ? (description.error ?? 'unavailable') : null,
|
|
367
|
+
});
|
|
368
|
+
} catch (err) {
|
|
369
|
+
runtimes.push({ name: id, status: 'failed', reason: commandError(err) });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const connected = runtimes.filter((runtime) => runtime.status === 'connected');
|
|
373
|
+
const failed = runtimes.filter((runtime) => runtime.status === 'failed');
|
|
374
|
+
return {
|
|
375
|
+
ok: failed.length === 0,
|
|
376
|
+
pending: failed.length > 0,
|
|
377
|
+
detail: `${connected.length} connected${failed.length ? `, ${failed.length} pending` : ''}`,
|
|
378
|
+
context: { runtimes, command: '/status' },
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
329
382
|
function preflightStatus(gaps, checks) {
|
|
330
383
|
const setupRequired = gaps.some((gap) => gap.kind === 'workspace' || gap.kind === 'llm');
|
|
331
384
|
if (setupRequired) return 'setup_required';
|
|
@@ -360,6 +413,7 @@ export async function runChecks({
|
|
|
360
413
|
dockerCheck = checkDockerAvailability,
|
|
361
414
|
internetCheck = checkInternetConnectivity,
|
|
362
415
|
agentsCheck = checkAgents,
|
|
416
|
+
agenticRuntimesCheck = checkAgenticRuntimes,
|
|
363
417
|
workspaceContainersCheck = checkWorkspaceContainers,
|
|
364
418
|
mcpCheck = checkMcpConnections,
|
|
365
419
|
onCheck = () => {},
|
|
@@ -398,6 +452,10 @@ export async function runChecks({
|
|
|
398
452
|
: 'Running',
|
|
399
453
|
context: { ...(agents?.context ?? {}), command: 'wiki-workspace agents up' },
|
|
400
454
|
});
|
|
455
|
+
// The gateway is an HTTP probe, independent of Docker: it may be a
|
|
456
|
+
// host-native service. Nothing enabled = skipped, never degraded.
|
|
457
|
+
const agenticRuntimes = await agenticRuntimesCheck();
|
|
458
|
+
onCheck({ kind: 'agentic', ...agenticRuntimes });
|
|
401
459
|
const workspaceGap = checkWorkspace(workspaces);
|
|
402
460
|
if (workspaceGap) {
|
|
403
461
|
gaps.push(workspaceGap);
|
|
@@ -4,7 +4,7 @@ import { mkdtemp } from 'node:fs/promises';
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import test from 'node:test';
|
|
7
|
-
import { checkAgents, checkInternetConnectivity, checkMcpConnections, runChecks, runPreflightChecks, withRuntimePreflight } from './startupCheck.js';
|
|
7
|
+
import { checkAgents, checkAgenticRuntimes, checkInternetConnectivity, checkMcpConnections, runChecks, runPreflightChecks, withRuntimePreflight } from './startupCheck.js';
|
|
8
8
|
|
|
9
9
|
async function withWorkspace(wikircLines, fn) {
|
|
10
10
|
const root = await mkdtemp(join(tmpdir(), 'wiki-manager-startup-check-'));
|
|
@@ -299,3 +299,31 @@ test('checkInternetConnectivity uses a fresh Node process with proxy and CA envi
|
|
|
299
299
|
else process.env.NODE_USE_ENV_PROXY = previousProxyFlag;
|
|
300
300
|
}
|
|
301
301
|
});
|
|
302
|
+
|
|
303
|
+
test('checkAgenticRuntimes skips when nothing is enabled', async () => {
|
|
304
|
+
const result = await checkAgenticRuntimes({ loadConfig: () => [], env: {} });
|
|
305
|
+
assert.equal(result.ok, true);
|
|
306
|
+
assert.equal(result.skipped, true);
|
|
307
|
+
assert.equal(result.context.runtimes.length, 0);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('checkAgenticRuntimes reports a healthy runtime as connected', async () => {
|
|
311
|
+
const result = await checkAgenticRuntimes({
|
|
312
|
+
loadConfig: () => [{ id: 'fake', type: 'fake', enabled: true }],
|
|
313
|
+
env: {},
|
|
314
|
+
});
|
|
315
|
+
assert.equal(result.ok, true);
|
|
316
|
+
assert.equal(result.pending, false);
|
|
317
|
+
assert.equal(result.context.runtimes[0].status, 'connected');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('checkAgenticRuntimes reports a down runtime as failed', async () => {
|
|
321
|
+
const result = await checkAgenticRuntimes({
|
|
322
|
+
loadConfig: () => [{ id: 'down', type: 'fake', available: false, enabled: true }],
|
|
323
|
+
env: {},
|
|
324
|
+
});
|
|
325
|
+
assert.equal(result.ok, false);
|
|
326
|
+
assert.equal(result.pending, true);
|
|
327
|
+
assert.equal(result.context.runtimes[0].status, 'failed');
|
|
328
|
+
assert.match(result.detail, /pending/);
|
|
329
|
+
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
1
|
+
import { createAgentEvent, dispatchAgentEvent, dispatchRuntimeLog } from '../core/agentEvents.js';
|
|
2
2
|
import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
|
|
3
|
-
import { normalizeRuntimeLog } from '../core/runtimeLog.js';
|
|
4
3
|
import { assertContract } from '../contracts/schemas.js';
|
|
5
4
|
|
|
6
5
|
const AVAILABLE = 'available';
|
|
@@ -220,26 +219,6 @@ function registerAgent(session, agent, { agentsByInstance, instanceByServer, las
|
|
|
220
219
|
return cloneAgent(next);
|
|
221
220
|
}
|
|
222
221
|
|
|
223
|
-
/**
|
|
224
|
-
* Runtime log line, emitted without importing the supervisor.
|
|
225
|
-
*
|
|
226
|
-
* `emitRuntimeLog` lives in `runtime/supervisor.js`, which already imports THIS
|
|
227
|
-
* module: importing it back would close a cycle for one log line. The event
|
|
228
|
-
* shape is the contract, not the helper, so we build it from the same
|
|
229
|
-
* normalizer the supervisor uses.
|
|
230
|
-
*/
|
|
231
|
-
function dispatchRuntimeLog(session, message) {
|
|
232
|
-
if (!session) return;
|
|
233
|
-
const payload = normalizeRuntimeLog(message, { session });
|
|
234
|
-
dispatchAgentEvent(session, createAgentEvent('runtime_log', {
|
|
235
|
-
origin: 'runtime',
|
|
236
|
-
runId: payload.runId ?? null,
|
|
237
|
-
taskId: payload.taskId ?? null,
|
|
238
|
-
workspace: payload.workspaceId ?? null,
|
|
239
|
-
payload,
|
|
240
|
-
}));
|
|
241
|
-
}
|
|
242
|
-
|
|
243
222
|
function dispatchRegistryEvent(session, type, payload) {
|
|
244
223
|
if (!session) return;
|
|
245
224
|
dispatchAgentEvent(session, createAgentEvent(type, {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { capabilityRegistryForSession } from './capabilityRegistry.js';
|
|
2
2
|
import { CapabilityUnavailableError, resolve } from './capabilityResolver.js';
|
|
3
3
|
|
|
4
4
|
export function createAssignmentManager({
|
|
@@ -27,9 +27,7 @@ export async function assign(task, {
|
|
|
27
27
|
if (!capability) {
|
|
28
28
|
throw new CapabilityUnavailableError(capability, 'task_missing_required_capability', { taskId: task?.id ?? task?.step });
|
|
29
29
|
}
|
|
30
|
-
const effectiveRegistry = registry ?? session
|
|
31
|
-
agents: session?.agentRegistrySnapshot ?? session?.agents ?? [],
|
|
32
|
-
});
|
|
30
|
+
const effectiveRegistry = registry ?? capabilityRegistryForSession(session);
|
|
33
31
|
const effectiveWorkspaceConfig = workspaceConfig ?? session?.wikircConfig ?? session?.wikirc?.config ?? {};
|
|
34
32
|
const retryAssignment = task?.retryAssignment;
|
|
35
33
|
if (retryAssignment?.agentInstanceId) {
|
|
@@ -46,6 +44,14 @@ export async function assign(task, {
|
|
|
46
44
|
capability,
|
|
47
45
|
operation: task?.operation ?? null,
|
|
48
46
|
serverName: agent?.serverName ?? provider?.serverName ?? null,
|
|
47
|
+
// Mirrors the primary resolution branch below: without this, retrying a
|
|
48
|
+
// task assigned to an external-runtime provider drops the routing
|
|
49
|
+
// discriminator and the dispatcher misroutes it to the MCP path, which
|
|
50
|
+
// throws "No MCP server found" since external-runtime agents carry no
|
|
51
|
+
// serverName.
|
|
52
|
+
providerKind: provider?.providerKind ?? 'mcp-agent',
|
|
53
|
+
runtimeProvider: provider?.runtimeProvider ?? null,
|
|
54
|
+
runtimeId: provider?.runtimeId ?? null,
|
|
49
55
|
agent,
|
|
50
56
|
retry: true,
|
|
51
57
|
previousAgentInstanceId: retryAssignment.previousAgentInstanceId ?? null,
|
|
@@ -62,6 +68,12 @@ export async function assign(task, {
|
|
|
62
68
|
capability,
|
|
63
69
|
operation: task?.operation ?? null,
|
|
64
70
|
serverName: agent?.serverName ?? provider?.serverName ?? null,
|
|
71
|
+
// External runtime providers (RFC § 8) resolve through the same registry
|
|
72
|
+
// as MCP agents; the assignment carries the routing discriminator so the
|
|
73
|
+
// dispatcher can hand the task to the runtime instead of agent_execute.
|
|
74
|
+
providerKind: provider?.providerKind ?? 'mcp-agent',
|
|
75
|
+
runtimeProvider: provider?.runtimeProvider ?? null,
|
|
76
|
+
runtimeId: provider?.runtimeId ?? null,
|
|
65
77
|
agent,
|
|
66
78
|
};
|
|
67
79
|
}
|
|
@@ -19,6 +19,11 @@ export function createCapabilityRegistry({ agents = [], compatibleContractVersio
|
|
|
19
19
|
capability,
|
|
20
20
|
description: agent.description,
|
|
21
21
|
lastSeenAt: agent.lastSeenAt ?? null,
|
|
22
|
+
// External runtime providers (RFC § 8) ride the same registry as MCP
|
|
23
|
+
// agents. These fields are null for every ordinary MCP agent.
|
|
24
|
+
providerKind: agent.providerKind ?? null,
|
|
25
|
+
runtimeId: agent.runtimeId ?? null,
|
|
26
|
+
runtimeProvider: agent.runtimeProvider ?? null,
|
|
22
27
|
};
|
|
23
28
|
const list = providers.get(key) ?? [];
|
|
24
29
|
list.push(entry);
|
|
@@ -54,7 +59,9 @@ export function capabilityRegistryForSession(session) {
|
|
|
54
59
|
?? session?.agentRegistrySnapshot
|
|
55
60
|
?? session?.agents
|
|
56
61
|
?? [];
|
|
57
|
-
|
|
62
|
+
const runtimeAgents = session?.runtimeProviderAgents ?? [];
|
|
63
|
+
const merged = [...agents, ...runtimeAgents];
|
|
64
|
+
if (merged.length > 0) return createCapabilityRegistry({ agents: merged });
|
|
58
65
|
if (session?.capabilityRegistry?.providersFor) return session.capabilityRegistry;
|
|
59
66
|
return createCapabilityRegistry();
|
|
60
67
|
}
|