@dotdrelle/wiki-manager 0.15.66 → 0.15.71
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 +57 -0
- package/agent-runtimes.example.json +68 -0
- package/agents.docker-compose.yml +39 -1
- package/docker-compose.yml +3 -3
- package/package.json +3 -2
- package/src/activity/activityAggregator.test.js +2 -2
- package/src/agent/graph.js +13 -11
- package/src/agent/skillRecursion.test.js +13 -12
- package/src/cli/wiki-manager.js +125 -37
- package/src/cli/wiki-manager.test.js +16 -16
- package/src/commands/slash.js +59 -5
- package/src/contracts/schemas.js +67 -0
- package/src/core/activity.js +5 -0
- package/src/core/agentEvents.js +139 -25
- package/src/core/agentEvents.test.js +26 -1
- package/src/core/buildInfo.json +2 -2
- package/src/core/commandFailure.test.js +2 -2
- package/src/core/currentArtifact.test.js +5 -5
- 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/mcp.test.js +1 -1
- package/src/core/otherWorkspacesRunning.test.js +6 -6
- package/src/core/runtimeEventAdapter.js +81 -0
- package/src/core/runtimeEventAdapter.test.js +61 -0
- package/src/core/runtimeLog.js +35 -1
- package/src/core/runtimeLog.test.js +27 -2
- 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/skillInvocation.test.js +1 -1
- package/src/core/startupCheck.js +58 -0
- package/src/core/startupCheck.test.js +29 -1
- package/src/core/wikiSetup.js +25 -0
- package/src/core/wikiSetup.test.js +35 -0
- package/src/core/wikirc.test.js +6 -6
- package/src/core/workspaceInherit.test.js +14 -14
- package/src/orchestrator/agentRegistry.js +1 -22
- package/src/orchestrator/agentRegistry.test.js +6 -6
- package/src/orchestrator/assignmentManager.js +16 -4
- package/src/orchestrator/capabilityRegistry.js +8 -1
- package/src/orchestrator/dispatcher.js +405 -2
- package/src/orchestrator/dispatcher.test.js +158 -4
- 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 +378 -0
- package/src/orchestrator/providers/runtimeProviders.test.js +384 -0
- package/src/orchestrator/resultAggregator.js +35 -2
- package/src/orchestrator/resultAggregator.test.js +62 -0
- package/src/orchestrator/scheduler.test.js +4 -4
- package/src/runtime/delegation.test.js +11 -11
- package/src/runtime/recoveryManager.js +70 -5
- package/src/runtime/runner.test.js +1 -1
- package/src/runtime/server.test.js +2 -2
- package/src/runtime/skillChain.e2e.test.js +2 -2
- package/src/runtime/store.test.js +8 -5
- package/src/runtime/supervisor.js +5 -10
- package/src/runtime/workspaceIsolation.test.js +26 -26
- package/src/shell/RightPane.tsx +23 -3
- package/src/shell/StartupScreen.tsx +44 -7
- package/src/shell/repl.js +24 -2
- package/src/shell/repl.test.js +13 -0
- package/wiki-workspace +53 -3
package/src/cli/wiki-manager.js
CHANGED
|
@@ -30,6 +30,7 @@ import { createAgentEvent, dispatchAgentEvent, reduceAgentEvents } from '../core
|
|
|
30
30
|
import { runAgentTurn, runAgenticLoop } from '../core/agentLoop.js';
|
|
31
31
|
import { resolveCapabilityConcurrency } from '../orchestrator/scheduler.js';
|
|
32
32
|
import { capabilityRegistryForSession } from '../orchestrator/capabilityRegistry.js';
|
|
33
|
+
import { CapabilityUnavailableError, resolve as resolveCapability } from '../orchestrator/capabilityResolver.js';
|
|
33
34
|
import { listWorkspaces } from '../core/workspaces.js';
|
|
34
35
|
import { findSkill } from '../core/skills.js';
|
|
35
36
|
import { rememberArtifact } from '../core/currentArtifact.js';
|
|
@@ -69,6 +70,20 @@ function unavailableRuntime(err) {
|
|
|
69
70
|
return { url: null, error: reason };
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
// The user-facing reason a capability cannot start — one sentence, the likely
|
|
74
|
+
// cause, and the fix. The raw registry goes to the logs, never to the chat.
|
|
75
|
+
function capabilityUnavailableMessage(capabilityId, seen = []) {
|
|
76
|
+
const agentic = String(capabilityId).startsWith('agent.');
|
|
77
|
+
const servicesDown = seen.some((entry) => /\[none\]$/.test(entry));
|
|
78
|
+
if (agentic) {
|
|
79
|
+
return `No agent provides capability ${capabilityId} — the agentic runtime is not enabled or not reachable. Enable it (GATEWAY_ENABLED=true + wiki-workspace agents up) and check /status.`;
|
|
80
|
+
}
|
|
81
|
+
if (servicesDown) {
|
|
82
|
+
return `No agent provides capability ${capabilityId} — the workspace services are not running. Start them (wiki-workspace up <workspace>), then retry.`;
|
|
83
|
+
}
|
|
84
|
+
return `No agent provides capability ${capabilityId} — no connected agent declares it. Check /status.`;
|
|
85
|
+
}
|
|
86
|
+
|
|
72
87
|
export function buildExecutorOnlyFragment({ objective, workspace, selection }) {
|
|
73
88
|
const provider = selection.provider;
|
|
74
89
|
const capability = provider.capability ?? {};
|
|
@@ -155,7 +170,7 @@ export async function resolveExecutorArguments({
|
|
|
155
170
|
'Only fill a field when the objective explicitly states or clearly implies its value.',
|
|
156
171
|
'Omit every field that is not stated. Never invent identifiers, queries, filters or counts.',
|
|
157
172
|
// The objective almost always names the workspace ("export the pages of
|
|
158
|
-
// workspace
|
|
173
|
+
// workspace acme"), and the orchestrator already binds it out of band. With
|
|
159
174
|
// one free-text field in the schema and no field for the workspace, a model
|
|
160
175
|
// reliably misbinds the two — that is how a workspace name ended up as a
|
|
161
176
|
// source name and failed the task.
|
|
@@ -923,6 +938,7 @@ async function runRuntime(argv, agent) {
|
|
|
923
938
|
const { startRuntimeServer } = await import('../runtime/server.js');
|
|
924
939
|
const { recoverActiveRuns } = await import('../runtime/recoveryManager.js');
|
|
925
940
|
const { emitRuntimeLog, startActivitySupervisor, cancelActiveActivityJobs, discoverAgentsOnce } = await import('../runtime/supervisor.js');
|
|
941
|
+
const { discoverRuntimeProvidersOnce } = await import('../orchestrator/providers/runtimeProviders.js');
|
|
926
942
|
const { resolveRuntimeAuthToken } = await import('../runtime/auth.js');
|
|
927
943
|
const { createSqliteQueueStore } = await import('../runtime/queueStore.js');
|
|
928
944
|
const { createApprovalManager } = await import('../runtime/approvals.js');
|
|
@@ -950,6 +966,7 @@ async function runRuntime(argv, agent) {
|
|
|
950
966
|
await Promise.all(resolved.map(async (context) => {
|
|
951
967
|
await refreshMcpRuntimeStatus(context.session);
|
|
952
968
|
await discoverAgentsOnce(context.session);
|
|
969
|
+
await discoverRuntimeProvidersOnce(context.session);
|
|
953
970
|
}));
|
|
954
971
|
}
|
|
955
972
|
|
|
@@ -1137,6 +1154,14 @@ async function runRuntime(argv, agent) {
|
|
|
1137
1154
|
try {
|
|
1138
1155
|
const context = await getWorkspaceContext(workspace);
|
|
1139
1156
|
await refreshMcpRuntimeStatus(context.session);
|
|
1157
|
+
// The supervisor kicks off agent + runtime-provider discovery fire-and-
|
|
1158
|
+
// forget, so at boot `session.runtimeProviderAgents` is usually still
|
|
1159
|
+
// undefined here. recoverActiveRuns resolves an external-runtime task by
|
|
1160
|
+
// finding its synthetic agent in that list; without it the task falls to
|
|
1161
|
+
// the MCP path, calls a non-existent server named after the capability,
|
|
1162
|
+
// throws, and the run is interrupted instead of recovered. Await the same
|
|
1163
|
+
// discovery the /run path already awaits (wiki-manager.js's turn setup).
|
|
1164
|
+
await discoverRuntimeProvidersOnce(context.session);
|
|
1140
1165
|
const gaps = recoveryMcpGaps(context);
|
|
1141
1166
|
if (gaps.length > 0) {
|
|
1142
1167
|
const interrupted = store.interruptRuns({ workspace: context.workspace });
|
|
@@ -1243,6 +1268,7 @@ async function runRuntime(argv, agent) {
|
|
|
1243
1268
|
// live endpoints and await one discovery pass before resolving.
|
|
1244
1269
|
await refreshMcpRuntimeStatus(session);
|
|
1245
1270
|
await discoverAgentsOnce(session, { registry: session.agentRegistry });
|
|
1271
|
+
await discoverRuntimeProvidersOnce(session);
|
|
1246
1272
|
let selection;
|
|
1247
1273
|
try {
|
|
1248
1274
|
selection = await resolveObjective(objective, session);
|
|
@@ -1457,11 +1483,37 @@ async function runRuntime(argv, agent) {
|
|
|
1457
1483
|
if (body.capabilityPlan?.capability) {
|
|
1458
1484
|
const { validateFragment } = await import('../orchestrator/planValidator.js');
|
|
1459
1485
|
const { integrate } = await import('../orchestrator/planIntegrator.js');
|
|
1486
|
+
// Same boot-race guard as prepareDelegation below: discovery is
|
|
1487
|
+
// asynchronous at startup, so a structured capability run submitted
|
|
1488
|
+
// right after boot must not observe the transient empty registry and
|
|
1489
|
+
// fail while the provider is already healthy (first E2E run of the
|
|
1490
|
+
// day did exactly that: "No agent provides capability agent.review"
|
|
1491
|
+
// while the gateway answered /health).
|
|
1492
|
+
await refreshMcpRuntimeStatus(session);
|
|
1493
|
+
await discoverAgentsOnce(session, { registry: session.agentRegistry });
|
|
1494
|
+
await discoverRuntimeProvidersOnce(session);
|
|
1460
1495
|
const registry = capabilityRegistryForSession(session);
|
|
1461
|
-
const
|
|
1462
|
-
const
|
|
1463
|
-
|
|
1464
|
-
|
|
1496
|
+
const capabilityId = String(body.capabilityPlan.capability);
|
|
1497
|
+
const candidates = registry.providersFor(capabilityId) ?? [];
|
|
1498
|
+
// Route through the same resolver every other task assignment uses
|
|
1499
|
+
// (assignmentManager.js, resultAggregator.js), instead of a bespoke
|
|
1500
|
+
// "prefer external-runtime, else any with serverName" chain that
|
|
1501
|
+
// ignored the workspace's own capabilityRouting config (preferred/
|
|
1502
|
+
// allowed/fallback agents) and never checked health/availability at
|
|
1503
|
+
// all — this could hand a direct /run capability request to a
|
|
1504
|
+
// disallowed or unhealthy agent while every other entry point honors
|
|
1505
|
+
// the routing config.
|
|
1506
|
+
const workspaceConfig = session?.wikircConfig ?? session?.wikirc?.config ?? {};
|
|
1507
|
+
let provider = null;
|
|
1508
|
+
let resolutionReason = null;
|
|
1509
|
+
try {
|
|
1510
|
+
const resolved = resolveCapability(capabilityId, { workspaceConfig, registry });
|
|
1511
|
+
provider = candidates.find((item) => item.agentInstanceId === resolved.agentInstanceId) ?? null;
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
if (!(error instanceof CapabilityUnavailableError)) throw error;
|
|
1514
|
+
resolutionReason = error.reason;
|
|
1515
|
+
}
|
|
1516
|
+
if (!provider) {
|
|
1465
1517
|
/*
|
|
1466
1518
|
Say what WAS seen, not only what was missing.
|
|
1467
1519
|
|
|
@@ -1471,38 +1523,60 @@ async function runRuntime(argv, agent) {
|
|
|
1471
1523
|
PRODUCTION_ALLOWED_STEPS drops its whole capability from
|
|
1472
1524
|
agent_describe, silently), or a name mismatch. Listing the registry
|
|
1473
1525
|
turns the next occurrence into its own diagnosis instead of a guess.
|
|
1474
|
-
|
|
1475
|
-
const seen =
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
);
|
|
1526
|
+
*/
|
|
1527
|
+
const seen = [
|
|
1528
|
+
...(session.agentRegistry?.snapshot?.() ?? session.agentRegistrySnapshot ?? []),
|
|
1529
|
+
...(session.runtimeProviderAgents ?? []),
|
|
1530
|
+
].map((item) => {
|
|
1531
|
+
const ids = (item.description?.capabilities ?? []).map((capability) => capability.id);
|
|
1532
|
+
return `${item.serverName ?? item.runtimeId ?? '?'}[${ids.join(', ') || 'none'}]`;
|
|
1533
|
+
});
|
|
1534
|
+
// The raw registry stays for diagnosis, in the LOGS — never in the
|
|
1535
|
+
// user-facing message, which explains the likely cause instead.
|
|
1536
|
+
emitRuntimeLog(session, `capability-plan: ${capabilityId} unresolvable (${resolutionReason ?? 'no_candidate'}) — registry: ${seen.join('; ') || 'no agent answered agent_describe'}`);
|
|
1537
|
+
const error = new Error(capabilityUnavailableMessage(capabilityId, seen));
|
|
1538
|
+
error.code = 'capability_unavailable';
|
|
1539
|
+
throw error;
|
|
1485
1540
|
}
|
|
1486
|
-
const
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1541
|
+
const operation = body.capabilityPlan.operation
|
|
1542
|
+
?? provider.capability?.supportedOperations?.[0]
|
|
1543
|
+
?? 'run';
|
|
1544
|
+
const canPlan = provider.description?.orchestration?.canPlan !== false;
|
|
1545
|
+
const fragment = canPlan
|
|
1546
|
+
? parseJsonText(formatMcpToolResult(await callMcpTool(session.mcp, provider.serverName, 'agent_plan', {
|
|
1547
|
+
capability: capabilityId,
|
|
1548
|
+
operation,
|
|
1549
|
+
workspace: { revision: String(Date.now()) },
|
|
1550
|
+
constraints: {
|
|
1551
|
+
// The agent declares its capacity. Request/env values are only
|
|
1552
|
+
// constraints: they may lower that capacity, never raise it.
|
|
1553
|
+
maxConcurrency: resolveCapabilityConcurrency(
|
|
1554
|
+
provider,
|
|
1555
|
+
body.capabilityPlan.maxConcurrency,
|
|
1556
|
+
process.env.WIKI_MANAGER_CAPABILITY_CONCURRENCY,
|
|
1557
|
+
),
|
|
1558
|
+
requireApprovalForMutations: body.capabilityPlan.requireApproval !== false,
|
|
1559
|
+
},
|
|
1560
|
+
...(body.capabilityPlan.arguments && typeof body.capabilityPlan.arguments === 'object'
|
|
1561
|
+
? { arguments: body.capabilityPlan.arguments }
|
|
1562
|
+
: Array.isArray(body.capabilityPlan.inputs) && body.capabilityPlan.inputs.length > 0
|
|
1563
|
+
? { arguments: { inputs: body.capabilityPlan.inputs } }
|
|
1564
|
+
: {}),
|
|
1565
|
+
})))
|
|
1566
|
+
: buildExecutorOnlyFragment({
|
|
1567
|
+
objective: `Capability run ${capabilityId}`,
|
|
1568
|
+
workspace: session.workspace ?? 'workspace',
|
|
1569
|
+
selection: {
|
|
1570
|
+
capability: capabilityId,
|
|
1571
|
+
operation,
|
|
1572
|
+
provider,
|
|
1573
|
+
arguments: body.capabilityPlan.arguments && typeof body.capabilityPlan.arguments === 'object'
|
|
1574
|
+
? body.capabilityPlan.arguments
|
|
1575
|
+
: Array.isArray(body.capabilityPlan.inputs) && body.capabilityPlan.inputs.length > 0
|
|
1576
|
+
? { inputs: body.capabilityPlan.inputs }
|
|
1577
|
+
: {},
|
|
1578
|
+
},
|
|
1579
|
+
});
|
|
1506
1580
|
if (!Array.isArray(fragment?.tasks) || fragment.tasks.length === 0) {
|
|
1507
1581
|
dispatchAgentEvent(session, createAgentEvent('assistant_message', {
|
|
1508
1582
|
origin: 'runtime',
|
|
@@ -1534,7 +1608,18 @@ async function runRuntime(argv, agent) {
|
|
|
1534
1608
|
if (!integrated.ok) {
|
|
1535
1609
|
throw new Error(`Capability plan integration failed: ${(integrated.errors ?? []).map((error) => error.message ?? error.code ?? String(error)).join('; ')}`);
|
|
1536
1610
|
}
|
|
1537
|
-
|
|
1611
|
+
// The structured path integrates the plan directly, so it must honor
|
|
1612
|
+
// the same explicit opt-in as the delegation path (delegation.js
|
|
1613
|
+
// `resolvePreparedDelegationApproval`): autoApprove grants one
|
|
1614
|
+
// run-scoped approval right after integration. Without it, a
|
|
1615
|
+
// headless/CI caller with autoApprove:true deadlocks on the
|
|
1616
|
+
// scheduler's approval gate until the timeout — first E2E research
|
|
1617
|
+
// run did exactly that.
|
|
1618
|
+
if (body.autoApprove === true) {
|
|
1619
|
+
const { resolvePreparedDelegationApproval } = await import('../runtime/delegation.js');
|
|
1620
|
+
resolvePreparedDelegationApproval({ autoApprove: true, approvalManager: context.approvalManager, runId });
|
|
1621
|
+
}
|
|
1622
|
+
emitRuntimeLog(session, `capability-plan: ${fragment.tasks.length} task(s) integrated${provider.serverName ? ` from ${provider.serverName}.agent_plan` : ''} (${body.capabilityPlan.capability}); approvals: ${(session.agentProjection?.approvals ?? []).filter((approval) => approval.status === 'pending_approval').length} pending`);
|
|
1538
1623
|
}
|
|
1539
1624
|
await runRuntimeAgenticWorkflow(agent, session, input, {
|
|
1540
1625
|
signal,
|
|
@@ -1568,6 +1653,9 @@ async function runRuntime(argv, agent) {
|
|
|
1568
1653
|
payload: {
|
|
1569
1654
|
runId,
|
|
1570
1655
|
message: err instanceof Error ? err.message : String(err),
|
|
1656
|
+
// UIs map this to amber ("service not available") instead of red
|
|
1657
|
+
// ("the run failed"): the run could not START, it did not crash.
|
|
1658
|
+
...(err?.code === 'capability_unavailable' ? { kind: 'capability_unavailable' } : {}),
|
|
1571
1659
|
},
|
|
1572
1660
|
}));
|
|
1573
1661
|
} finally {
|
|
@@ -66,7 +66,7 @@ test('interactive runtime refreshes configured MCP endpoints that started late',
|
|
|
66
66
|
test('executor-only capabilities receive one manager-authored executable task', () => {
|
|
67
67
|
const fragment = buildExecutorOnlyFragment({
|
|
68
68
|
objective: 'donne-moi mes derniers mails',
|
|
69
|
-
workspace: '
|
|
69
|
+
workspace: 'demo',
|
|
70
70
|
selection: {
|
|
71
71
|
capability: 'external-source.collect',
|
|
72
72
|
operation: 'collect',
|
|
@@ -91,7 +91,7 @@ test('executor-only capabilities receive one manager-authored executable task',
|
|
|
91
91
|
assert.equal(fragment.tasks[0].requiredCapability, 'external-source.collect');
|
|
92
92
|
assert.equal(fragment.tasks[0].operation, 'collect');
|
|
93
93
|
assert.deepEqual(fragment.tasks[0].arguments, { maxMessages: 10 });
|
|
94
|
-
assert.deepEqual(fragment.tasks[0].locks, ['external-source.collect:
|
|
94
|
+
assert.deepEqual(fragment.tasks[0].locks, ['external-source.collect:demo']);
|
|
95
95
|
assert.equal(fragment.tasks[0].requiresApproval, true);
|
|
96
96
|
assert.equal(fragment.tasks[0].approvalClass, 'external-source');
|
|
97
97
|
assert.match(fragment.tasks[0].idempotencyKey, /^[0-9a-f-]{36}$/);
|
|
@@ -263,14 +263,14 @@ const EXPORT_CAPABILITY = {
|
|
|
263
263
|
};
|
|
264
264
|
|
|
265
265
|
test('argument extraction drops a value that only echoes the active workspace', async () => {
|
|
266
|
-
// Regression: "
|
|
266
|
+
// Regression: "export the Confluence pages of the acme workspace" against a
|
|
267
267
|
// schema whose single free-text field is source_name. The model binds the
|
|
268
|
-
// workspace name to it, and the executor fails with "source '
|
|
268
|
+
// workspace name to it, and the executor fails with "source 'acme' not
|
|
269
269
|
// found". The workspace is already bound out of band, so the echo is noise.
|
|
270
270
|
const llm = {
|
|
271
271
|
completeWithTools: async () => ({
|
|
272
272
|
tool_calls: [{
|
|
273
|
-
function: { name: 'set_task_arguments', arguments: JSON.stringify({ source_name: '
|
|
273
|
+
function: { name: 'set_task_arguments', arguments: JSON.stringify({ source_name: 'acme' }) },
|
|
274
274
|
}],
|
|
275
275
|
}),
|
|
276
276
|
};
|
|
@@ -278,9 +278,9 @@ test('argument extraction drops a value that only echoes the active workspace',
|
|
|
278
278
|
assert.deepEqual(
|
|
279
279
|
await resolveExecutorArguments({
|
|
280
280
|
llm,
|
|
281
|
-
objective: '
|
|
281
|
+
objective: 'export the Confluence pages of the acme workspace',
|
|
282
282
|
capability: EXPORT_CAPABILITY,
|
|
283
|
-
workspace: '
|
|
283
|
+
workspace: 'acme',
|
|
284
284
|
}),
|
|
285
285
|
{},
|
|
286
286
|
);
|
|
@@ -303,7 +303,7 @@ test('argument extraction keeps a real value that is not the workspace name', as
|
|
|
303
303
|
llm,
|
|
304
304
|
objective: 'exporter la source EAS_Avant_projet_ACPI',
|
|
305
305
|
capability: EXPORT_CAPABILITY,
|
|
306
|
-
workspace: '
|
|
306
|
+
workspace: 'acme',
|
|
307
307
|
}),
|
|
308
308
|
{ source_name: 'EAS_Avant_projet_ACPI' },
|
|
309
309
|
);
|
|
@@ -320,17 +320,17 @@ test('argument extraction tells the model the workspace is already bound', async
|
|
|
320
320
|
|
|
321
321
|
await resolveExecutorArguments({
|
|
322
322
|
llm,
|
|
323
|
-
objective: '
|
|
323
|
+
objective: 'export the pages of the acme workspace',
|
|
324
324
|
capability: EXPORT_CAPABILITY,
|
|
325
|
-
workspace: '
|
|
325
|
+
workspace: 'acme',
|
|
326
326
|
});
|
|
327
327
|
|
|
328
|
-
assert.match(seenSystem, /already runs against workspace "
|
|
328
|
+
assert.match(seenSystem, /already runs against workspace "acme"/);
|
|
329
329
|
});
|
|
330
330
|
|
|
331
331
|
test('argument extraction rejects a value outside the closed vocabulary', async () => {
|
|
332
|
-
// Regression: "
|
|
333
|
-
// workspace guard removes "
|
|
332
|
+
// Regression: "export the Confluence pages of the demo workspace" — the
|
|
333
|
+
// workspace guard removes "demo", so the model reaches for the next noun and
|
|
334
334
|
// emits "Confluence". Only the agent knows the valid names; once it publishes
|
|
335
335
|
// them as an enum, the orchestrator can check without guessing at meaning.
|
|
336
336
|
const capability = {
|
|
@@ -354,9 +354,9 @@ test('argument extraction rejects a value outside the closed vocabulary', async
|
|
|
354
354
|
assert.deepEqual(
|
|
355
355
|
await resolveExecutorArguments({
|
|
356
356
|
llm,
|
|
357
|
-
objective: '
|
|
357
|
+
objective: 'export the Confluence pages of the demo workspace',
|
|
358
358
|
capability,
|
|
359
|
-
workspace: '
|
|
359
|
+
workspace: 'demo',
|
|
360
360
|
}),
|
|
361
361
|
{},
|
|
362
362
|
);
|
|
@@ -389,7 +389,7 @@ test('argument extraction keeps a value the vocabulary allows', async () => {
|
|
|
389
389
|
llm,
|
|
390
390
|
objective: 'exporter la source EAS_Avant_projet_ACPI',
|
|
391
391
|
capability,
|
|
392
|
-
workspace: '
|
|
392
|
+
workspace: 'acme',
|
|
393
393
|
}),
|
|
394
394
|
{ source_name: 'EAS_Avant_projet_ACPI' },
|
|
395
395
|
);
|
package/src/commands/slash.js
CHANGED
|
@@ -30,6 +30,7 @@ import { findSkill, inspectSkills, listSkills } from '../core/skills.js';
|
|
|
30
30
|
import { extractActivity, formatActivityError, formatActivityLine, formatActivitySummary, parseJsonText } from '../core/activity.js';
|
|
31
31
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
32
32
|
import { emitRuntimeLog } from '../runtime/supervisor.js';
|
|
33
|
+
import { discoverRuntimeProvidersOnce } from '../orchestrator/providers/runtimeProviders.js';
|
|
33
34
|
import {
|
|
34
35
|
cancelQueueItem,
|
|
35
36
|
clearFinishedQueueItems,
|
|
@@ -660,8 +661,34 @@ function reportNewlyDegradedMcp(session, previousMcp) {
|
|
|
660
661
|
}
|
|
661
662
|
}
|
|
662
663
|
|
|
664
|
+
function runtimeProvidersSection(session) {
|
|
665
|
+
const agents = session.runtimeProviderAgents ?? [];
|
|
666
|
+
if (agents.length === 0) return null;
|
|
667
|
+
const byRuntime = new Map();
|
|
668
|
+
for (const agent of agents) {
|
|
669
|
+
const runtimeId = agent.runtimeId ?? 'external';
|
|
670
|
+
if (!byRuntime.has(runtimeId)) byRuntime.set(runtimeId, []);
|
|
671
|
+
byRuntime.get(runtimeId).push(agent);
|
|
672
|
+
}
|
|
673
|
+
const lines = [];
|
|
674
|
+
for (const [runtimeId, list] of byRuntime) {
|
|
675
|
+
const health = list[0]?.health ?? 'unknown';
|
|
676
|
+
const capabilities = list
|
|
677
|
+
.map((agent) => agent.description?.capabilities?.[0]?.id ?? agent.agentInstanceId);
|
|
678
|
+
lines.push(`${runtimeId}: ${health}`);
|
|
679
|
+
// One capability per line: the status pane is a fixed-width box that
|
|
680
|
+
// TRUNCATES long lines, and a joined list ended mid-word
|
|
681
|
+
// ("capabilities: agent.review, agent.research, agent"). A per-line list
|
|
682
|
+
// never truncates and stays scannable.
|
|
683
|
+
lines.push('capabilities:');
|
|
684
|
+
for (const capability of capabilities) lines.push(` ${capability}`);
|
|
685
|
+
}
|
|
686
|
+
return sectionBlock('Agentic runtime', lines);
|
|
687
|
+
}
|
|
688
|
+
|
|
663
689
|
async function statusText(session) {
|
|
664
690
|
const states = await refreshMcpRuntimeStatus(session);
|
|
691
|
+
await discoverRuntimeProvidersOnce(session);
|
|
665
692
|
const workspaceStats = collectWorkspaceStats(session);
|
|
666
693
|
const workspaceColumn = sectionBlock(`Workspace · ${session.workspace ?? '-'}`, [
|
|
667
694
|
`path: ${compactPath(session.workspacePath ?? '-')}`,
|
|
@@ -678,11 +705,20 @@ async function statusText(session) {
|
|
|
678
705
|
`baseUrl: ${compactBaseUrl(session.wikircConfig?.llm?.baseUrl)}`,
|
|
679
706
|
...(skillDiagnosticCount ? [`skill diagnostics: ${skillDiagnostics.rejected.length} rejected, ${skillDiagnostics.warnings.length} warning(s) (/skills)`] : []),
|
|
680
707
|
]);
|
|
681
|
-
|
|
708
|
+
// The workspace stack (serve / mcp-http / production-mcp) only means
|
|
709
|
+
// something once a workspace is loaded AND services are actually declared;
|
|
710
|
+
// an empty list is noise either way. Named "Services" (not "Runtime") to
|
|
711
|
+
// avoid colliding with the "Agentic runtime" section that lists the
|
|
712
|
+
// external engines.
|
|
713
|
+
const hasServices = Boolean(states && Object.keys(states).length > 0);
|
|
714
|
+
const runtimeColumn = hasServices
|
|
715
|
+
? sectionBlock('Services', serviceStatesText(states).split('\n'))
|
|
716
|
+
: null;
|
|
682
717
|
const mcpColumn = sectionBlock('MCP', compactMcpStatus(session.mcp).split('\n'));
|
|
718
|
+
const runtimesColumn = runtimeProvidersSection(session);
|
|
683
719
|
const stats = workspaceStatsColumns(workspaceStats, session);
|
|
684
720
|
|
|
685
|
-
const leftColumn = [workspaceColumn, stats.left, runtimeColumn, mcpColumn].filter(Boolean).join('\n\n');
|
|
721
|
+
const leftColumn = [workspaceColumn, stats.left, runtimeColumn, mcpColumn, runtimesColumn].filter(Boolean).join('\n\n');
|
|
686
722
|
const rightColumn = [configColumn, stats.right].filter(Boolean).join('\n\n');
|
|
687
723
|
|
|
688
724
|
// Leading/trailing blank row so the boxed pair doesn't butt directly against
|
|
@@ -850,12 +886,13 @@ function componentInstallAction(missingImages) {
|
|
|
850
886
|
return missingImages.length > 0 ? 'downloaded-and-installed-missing-components' : null;
|
|
851
887
|
}
|
|
852
888
|
|
|
853
|
-
export function localizedOperationResult({ operation, target, status = 'succeeded', componentAction = null, images = [] }) {
|
|
889
|
+
export function localizedOperationResult({ operation, target, status = 'succeeded', componentAction = null, images = [], detail = null }) {
|
|
854
890
|
const facts = JSON.stringify({
|
|
855
891
|
operation,
|
|
856
892
|
target,
|
|
857
893
|
status,
|
|
858
894
|
...(componentAction ? { componentAction, images } : {}),
|
|
895
|
+
...(detail ? { detail } : {}),
|
|
859
896
|
});
|
|
860
897
|
return {
|
|
861
898
|
output: facts,
|
|
@@ -937,7 +974,10 @@ export async function handleSlashCommand(line, context) {
|
|
|
937
974
|
const runAgentCommand = async (fn, verb) => {
|
|
938
975
|
try {
|
|
939
976
|
step(`Agents: ${verb}ing external agents…`);
|
|
940
|
-
|
|
977
|
+
let outcome = null;
|
|
978
|
+
const missingImages = await collectMissingImages(step, async (options) => {
|
|
979
|
+
outcome = await fn(options);
|
|
980
|
+
});
|
|
941
981
|
if (verb === 'start') {
|
|
942
982
|
// `wiki-workspace agents up` generates the agent tokens into the
|
|
943
983
|
// manager .env and adds the profiled entries to mcp.endpoints.json.
|
|
@@ -951,6 +991,17 @@ export async function handleSlashCommand(line, context) {
|
|
|
951
991
|
loadManagerEnv({ override: true });
|
|
952
992
|
await refreshMcpRuntimeStatus(context.session);
|
|
953
993
|
}
|
|
994
|
+
if (outcome?.degraded) {
|
|
995
|
+
step('Agents: stack started degraded — one optional agent failed; the base agents are up.');
|
|
996
|
+
return localizedOperationResult({
|
|
997
|
+
operation: verb,
|
|
998
|
+
target: 'agents',
|
|
999
|
+
status: 'degraded',
|
|
1000
|
+
componentAction: componentInstallAction(missingImages),
|
|
1001
|
+
images: missingImages,
|
|
1002
|
+
detail: rawFailureText(outcome.degradedError),
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
954
1005
|
return localizedOperationResult({
|
|
955
1006
|
operation: verb,
|
|
956
1007
|
target: 'agents',
|
|
@@ -1396,7 +1447,10 @@ export async function handleSlashCommand(line, context) {
|
|
|
1396
1447
|
},
|
|
1397
1448
|
});
|
|
1398
1449
|
if (result?.runId) {
|
|
1399
|
-
|
|
1450
|
+
// Honest and generic: the runtime decides, per task, whether a
|
|
1451
|
+
// mutation needs approval. Claiming "awaiting approval" here would
|
|
1452
|
+
// be false for read-only capabilities like agent.review.
|
|
1453
|
+
return { output: `▶ Capability run accepted (${String(result.runId).slice(0, 8)}) — the plan will be integrated and dispatched in parallel; any mutating step will wait for /approve.` };
|
|
1400
1454
|
}
|
|
1401
1455
|
return { output: `Run not started: ${result?.explanation ?? result?.error ?? JSON.stringify(result)}` };
|
|
1402
1456
|
}
|
package/src/contracts/schemas.js
CHANGED
|
@@ -330,6 +330,71 @@ const patchTaskSchema = {
|
|
|
330
330
|
},
|
|
331
331
|
};
|
|
332
332
|
|
|
333
|
+
const runtimeCapabilitySchema = {
|
|
334
|
+
type: 'object',
|
|
335
|
+
required: ['name'],
|
|
336
|
+
additionalProperties: true,
|
|
337
|
+
properties: {
|
|
338
|
+
name: { type: 'string', minLength: 1 },
|
|
339
|
+
description: { type: 'string' },
|
|
340
|
+
operations: stringArraySchema,
|
|
341
|
+
aliases: stringArraySchema,
|
|
342
|
+
aliasOperations: { type: 'object', additionalProperties: true },
|
|
343
|
+
mutationClass: { type: 'string' },
|
|
344
|
+
defaultRequiresApproval: { type: 'boolean' },
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const runtimeDescriptionSchema = {
|
|
349
|
+
$id: 'https://dotdrelle.dev/wiki-manager/contracts/runtime-description/v1',
|
|
350
|
+
title: 'RuntimeDescription',
|
|
351
|
+
schemaVersion: '1',
|
|
352
|
+
type: 'object',
|
|
353
|
+
required: ['runtime', 'version', 'protocolVersion'],
|
|
354
|
+
additionalProperties: true,
|
|
355
|
+
properties: {
|
|
356
|
+
runtime: { type: 'string', minLength: 1 },
|
|
357
|
+
version: { type: 'string', minLength: 1 },
|
|
358
|
+
protocolVersion: { type: 'string', minLength: 1 },
|
|
359
|
+
health: { type: 'string' },
|
|
360
|
+
capabilities: { type: 'array', items: runtimeCapabilitySchema },
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const runtimeEventSchema = {
|
|
365
|
+
$id: 'https://dotdrelle.dev/wiki-manager/contracts/runtime-event/v1',
|
|
366
|
+
title: 'RuntimeEvent',
|
|
367
|
+
schemaVersion: '1',
|
|
368
|
+
type: 'object',
|
|
369
|
+
required: ['type'],
|
|
370
|
+
additionalProperties: true,
|
|
371
|
+
properties: {
|
|
372
|
+
type: {
|
|
373
|
+
type: 'string',
|
|
374
|
+
enum: [
|
|
375
|
+
'run_created',
|
|
376
|
+
'run_started',
|
|
377
|
+
'agent_thinking',
|
|
378
|
+
'tool_started',
|
|
379
|
+
'tool_finished',
|
|
380
|
+
'subagent_started',
|
|
381
|
+
'subagent_finished',
|
|
382
|
+
'message',
|
|
383
|
+
'approval_required',
|
|
384
|
+
'run_completed',
|
|
385
|
+
'run_failed',
|
|
386
|
+
'run_cancelled',
|
|
387
|
+
],
|
|
388
|
+
},
|
|
389
|
+
runId: { type: 'string' },
|
|
390
|
+
tool: { type: 'string' },
|
|
391
|
+
durationMs: { type: 'number' },
|
|
392
|
+
resultSummary: { type: 'string' },
|
|
393
|
+
error: nullableString,
|
|
394
|
+
args: nullableObject,
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
|
|
333
398
|
export const contractSchemas = {
|
|
334
399
|
activity: {
|
|
335
400
|
$id: 'https://dotdrelle.dev/wiki-manager/contracts/activity/v1',
|
|
@@ -462,6 +527,8 @@ export const contractSchemas = {
|
|
|
462
527
|
plannedTask: plannedTaskSchema,
|
|
463
528
|
taskGraphFragment: taskGraphFragmentSchema,
|
|
464
529
|
planExpansionRequest: planExpansionRequestSchema,
|
|
530
|
+
runtimeDescription: runtimeDescriptionSchema,
|
|
531
|
+
runtimeEvent: runtimeEventSchema,
|
|
465
532
|
};
|
|
466
533
|
|
|
467
534
|
export function validateContract(name, value) {
|
package/src/core/activity.js
CHANGED
|
@@ -214,6 +214,11 @@ export function mergePolledActivity(tracked, polled) {
|
|
|
214
214
|
// Identity: keep the tracked source/id so activityKey() stays stable.
|
|
215
215
|
id: tracked.id ?? polled.id,
|
|
216
216
|
source: tracked.source ?? polled.source,
|
|
217
|
+
// A later poll often reports only {status, progress} with no phase/type,
|
|
218
|
+
// and activityFromStatusPayload then falls back to the generic 'job' —
|
|
219
|
+
// spreading that over the tracked value degraded a real kind (e.g.
|
|
220
|
+
// 'knowledge.update') to 'job' on the very next poll tick.
|
|
221
|
+
kind: tracked.kind ?? polled.kind,
|
|
217
222
|
// Keep polling with the descriptor that worked when the agent's answer
|
|
218
223
|
// does not carry one of its own.
|
|
219
224
|
poll: polled.poll ?? tracked.poll,
|