@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
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 ?? {};
|
|
@@ -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 {
|
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,30 @@ 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
|
+
.join(', ');
|
|
679
|
+
lines.push(`${runtimeId}: ${health}`);
|
|
680
|
+
lines.push(`capabilities: ${capabilities}`);
|
|
681
|
+
}
|
|
682
|
+
return sectionBlock('Agentic runtime', lines);
|
|
683
|
+
}
|
|
684
|
+
|
|
663
685
|
async function statusText(session) {
|
|
664
686
|
const states = await refreshMcpRuntimeStatus(session);
|
|
687
|
+
await discoverRuntimeProvidersOnce(session);
|
|
665
688
|
const workspaceStats = collectWorkspaceStats(session);
|
|
666
689
|
const workspaceColumn = sectionBlock(`Workspace · ${session.workspace ?? '-'}`, [
|
|
667
690
|
`path: ${compactPath(session.workspacePath ?? '-')}`,
|
|
@@ -678,11 +701,20 @@ async function statusText(session) {
|
|
|
678
701
|
`baseUrl: ${compactBaseUrl(session.wikircConfig?.llm?.baseUrl)}`,
|
|
679
702
|
...(skillDiagnosticCount ? [`skill diagnostics: ${skillDiagnostics.rejected.length} rejected, ${skillDiagnostics.warnings.length} warning(s) (/skills)`] : []),
|
|
680
703
|
]);
|
|
681
|
-
|
|
704
|
+
// The workspace stack (serve / mcp-http / production-mcp) only means
|
|
705
|
+
// something once a workspace is loaded AND services are actually declared;
|
|
706
|
+
// an empty list is noise either way. Named "Services" (not "Runtime") to
|
|
707
|
+
// avoid colliding with the "Agentic runtime" section that lists the
|
|
708
|
+
// external engines.
|
|
709
|
+
const hasServices = Boolean(states && Object.keys(states).length > 0);
|
|
710
|
+
const runtimeColumn = hasServices
|
|
711
|
+
? sectionBlock('Services', serviceStatesText(states).split('\n'))
|
|
712
|
+
: null;
|
|
682
713
|
const mcpColumn = sectionBlock('MCP', compactMcpStatus(session.mcp).split('\n'));
|
|
714
|
+
const runtimesColumn = runtimeProvidersSection(session);
|
|
683
715
|
const stats = workspaceStatsColumns(workspaceStats, session);
|
|
684
716
|
|
|
685
|
-
const leftColumn = [workspaceColumn, stats.left, runtimeColumn, mcpColumn].filter(Boolean).join('\n\n');
|
|
717
|
+
const leftColumn = [workspaceColumn, stats.left, runtimeColumn, mcpColumn, runtimesColumn].filter(Boolean).join('\n\n');
|
|
686
718
|
const rightColumn = [configColumn, stats.right].filter(Boolean).join('\n\n');
|
|
687
719
|
|
|
688
720
|
// Leading/trailing blank row so the boxed pair doesn't butt directly against
|
|
@@ -1396,7 +1428,10 @@ export async function handleSlashCommand(line, context) {
|
|
|
1396
1428
|
},
|
|
1397
1429
|
});
|
|
1398
1430
|
if (result?.runId) {
|
|
1399
|
-
|
|
1431
|
+
// Honest and generic: the runtime decides, per task, whether a
|
|
1432
|
+
// mutation needs approval. Claiming "awaiting approval" here would
|
|
1433
|
+
// be false for read-only capabilities like agent.review.
|
|
1434
|
+
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
1435
|
}
|
|
1401
1436
|
return { output: `Run not started: ${result?.explanation ?? result?.error ?? JSON.stringify(result)}` };
|
|
1402
1437
|
}
|
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,
|
package/src/core/agentEvents.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { normalizeActivity } from './activity.js';
|
|
2
2
|
import { attachActivityToExistingPlan, syncActivitiesToPlan } from './plan.js';
|
|
3
3
|
import { applyPlanPatch, normalizePlanPatch, normalizePlanRevision, rebasePlanPatch } from './planPatch.js';
|
|
4
|
-
import { formatRuntimeLogPayload } from './runtimeLog.js';
|
|
4
|
+
import { formatRuntimeLogPayload, normalizeRuntimeLog } from './runtimeLog.js';
|
|
5
5
|
import { projectSkillChains, TERMINAL as CONTROL_TERMINAL_STATUSES } from './skillChainView.js';
|
|
6
6
|
import { projectWorkflow } from './workflow.js';
|
|
7
7
|
import { validateContractInDev } from '../contracts/schemas.js';
|
|
@@ -108,6 +108,23 @@ export function dispatchAgentEvent(session, event) {
|
|
|
108
108
|
return normalized;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// Shared `runtime_log` shaping — was independently reimplemented byte-for-byte
|
|
112
|
+
// in runtime/supervisor.js, orchestrator/agentRegistry.js and
|
|
113
|
+
// orchestrator/providers/runtimeProviders.js (each citing the same "avoid a
|
|
114
|
+
// cycle with supervisor.js" reason, even where no such cycle existed). This
|
|
115
|
+
// module already sits below all three, so it is the one safe common home.
|
|
116
|
+
export function dispatchRuntimeLog(session, message) {
|
|
117
|
+
if (!session) return;
|
|
118
|
+
const payload = normalizeRuntimeLog(message, { session });
|
|
119
|
+
return dispatchAgentEvent(session, createAgentEvent('runtime_log', {
|
|
120
|
+
origin: 'runtime',
|
|
121
|
+
runId: payload.runId ?? null,
|
|
122
|
+
taskId: payload.taskId ?? null,
|
|
123
|
+
workspace: payload.workspaceId ?? null,
|
|
124
|
+
payload,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
|
|
111
128
|
// Full in-memory projection reset for a session. The runtime keeps the live
|
|
112
129
|
// projection in memory (session.agentProjection) and serves it from /state, so
|
|
113
130
|
// interrupting runs is not enough to clear the PLAN/ACTIVITY/LOGS panels — the
|
package/src/core/buildInfo.json
CHANGED
|
@@ -27,52 +27,20 @@ test('workspace production agent enables restore by default', async () => {
|
|
|
27
27
|
assert.match(String(allowed), /(?:^|,)restore(?:,|})/);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
test('
|
|
30
|
+
test('the shipped default carries no step the engine retired in 0.15.66', async () => {
|
|
31
31
|
/*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
avait corrigé côté moteur. Le défaut de Python porte `taxonomy` ; un compose
|
|
39
|
-
POSITIONNE toujours la variable, donc ce défaut ne s'applique jamais là.
|
|
32
|
+
0.15.66 a retiré du moteur llm-wiki les commandes `wiki concepts`,
|
|
33
|
+
`reclassify-concepts` et `taxonomy` (simplification : le concept EST le
|
|
34
|
+
dossier). Un défaut livré qui les autoriserait encore ferait échouer le
|
|
35
|
+
pipeline à la première de ces étapes — `unknown command 'concepts'` — sans
|
|
36
|
+
jamais les exécuter. Ces trois étapes ne doivent donc figurer dans AUCUN
|
|
37
|
+
défaut livré, ni ici ni dans agent-production.
|
|
40
38
|
*/
|
|
41
39
|
const raw = await readFile(new URL('../../docker-compose.yml', import.meta.url), 'utf8');
|
|
42
40
|
const compose = YAML.parse(raw);
|
|
43
41
|
const allowed = compose.services['production-mcp'].environment
|
|
44
42
|
.find((entry) => String(entry).startsWith('PRODUCTION_ALLOWED_STEPS='));
|
|
45
|
-
assert.
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('every shipped default allows the concepts step', async () => {
|
|
49
|
-
/*
|
|
50
|
-
Same silent-omission risk as `taxonomy` above, one lot earlier: without
|
|
51
|
-
`concepts`, `wiki concepts --apply` (which writes wiki/concepts-grid.md) is
|
|
52
|
-
never reachable through /wiki-sync, /pipeline, or any orchestrated flow.
|
|
53
|
-
Every ingest then files every concept page under the reserved
|
|
54
|
-
`unclassified` class forever, with nothing surfacing why.
|
|
55
|
-
*/
|
|
56
|
-
const raw = await readFile(new URL('../../docker-compose.yml', import.meta.url), 'utf8');
|
|
57
|
-
const compose = YAML.parse(raw);
|
|
58
|
-
const allowed = compose.services['production-mcp'].environment
|
|
59
|
-
.find((entry) => String(entry).startsWith('PRODUCTION_ALLOWED_STEPS='));
|
|
60
|
-
assert.match(String(allowed), /(?:^|,)concepts(?:,|})/);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('every shipped default allows the reclassify-concepts step', async () => {
|
|
64
|
-
/*
|
|
65
|
-
One step further than `concepts`: without `reclassify-concepts`, a page
|
|
66
|
-
already stuck under wiki/concepts/unclassified stays there even after a
|
|
67
|
-
grid exists — re-ingesting its source is not a reliable fix, since the
|
|
68
|
-
ingest prompt updates an existing leaf at its existing path instead of
|
|
69
|
-
moving it.
|
|
70
|
-
*/
|
|
71
|
-
const raw = await readFile(new URL('../../docker-compose.yml', import.meta.url), 'utf8');
|
|
72
|
-
const compose = YAML.parse(raw);
|
|
73
|
-
const allowed = compose.services['production-mcp'].environment
|
|
74
|
-
.find((entry) => String(entry).startsWith('PRODUCTION_ALLOWED_STEPS='));
|
|
75
|
-
assert.match(String(allowed), /(?:^|,)reclassify-concepts(?:,|})/);
|
|
43
|
+
assert.doesNotMatch(String(allowed), /(?:^|,)(?:concepts|reclassify-concepts|taxonomy)(?:,|})/);
|
|
76
44
|
});
|
|
77
45
|
|
|
78
46
|
test('shipped compose files never carry a build context', async () => {
|
package/src/core/env.js
CHANGED
|
@@ -49,6 +49,10 @@ export function managerMcpEndpointsFile() {
|
|
|
49
49
|
return join(managerStateDir(), 'mcp.endpoints.json');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export function managerAgentRuntimesFile() {
|
|
53
|
+
return join(managerStateDir(), 'agent-runtimes.json');
|
|
54
|
+
}
|
|
55
|
+
|
|
52
56
|
// User-owned compose overrides, one per stack. `.wiki/compose` is persistent
|
|
53
57
|
// operator configuration; `.wiki/runtime` is generated state rewritten by
|
|
54
58
|
// compose commands. Keeping those directories separate makes the lifecycle
|
|
@@ -164,6 +168,16 @@ export function ensureManagerScaffold({ log = () => {} } = {}) {
|
|
|
164
168
|
}
|
|
165
169
|
}
|
|
166
170
|
}
|
|
171
|
+
// External agent runtimes (RFC § 37) are optional and DISABLED in the
|
|
172
|
+
// packaged example: seeding a live endpoint would make every fresh install
|
|
173
|
+
// probe a runtime that does not ship. Copy once, never rewrite — an existing
|
|
174
|
+
// file is the operator's.
|
|
175
|
+
const runtimesFile = managerAgentRuntimesFile();
|
|
176
|
+
const runtimesExample = join(packageRoot, 'agent-runtimes.example.json');
|
|
177
|
+
if (existsSync(runtimesExample) && !existsSync(runtimesFile)) {
|
|
178
|
+
copyFileSync(runtimesExample, runtimesFile);
|
|
179
|
+
created.push('agent-runtimes.json');
|
|
180
|
+
}
|
|
167
181
|
const envFile = managerEnvFile();
|
|
168
182
|
const envExample = join(packageRoot, '.env.example');
|
|
169
183
|
if (!existsSync(envFile) && existsSync(envExample)) {
|
package/src/core/env.test.js
CHANGED
|
@@ -37,6 +37,25 @@ test('scaffold copies the packaged examples into a fresh directory', () => {
|
|
|
37
37
|
});
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
test('scaffold seeds an enabled agent-runtimes example and never rewrites an existing one', () => {
|
|
41
|
+
withTempManagerDir((dir) => {
|
|
42
|
+
const created = ensureManagerScaffold();
|
|
43
|
+
assert.ok(created.includes('agent-runtimes.json'));
|
|
44
|
+
const runtimes = JSON.parse(readFileSync(join(dir, 'agent-runtimes.json'), 'utf8'));
|
|
45
|
+
assert.ok(Array.isArray(runtimes.runtimes));
|
|
46
|
+
assert.equal(runtimes.runtimes[0].enabled, true, 'the scaffold ships the gateway enabled (GATEWAY_ENABLED=true)');
|
|
47
|
+
|
|
48
|
+
// Operator-owned: once present, the file is never touched again.
|
|
49
|
+
writeFileSync(join(dir, 'agent-runtimes.json'), JSON.stringify({ runtimes: [] }));
|
|
50
|
+
const second = ensureManagerScaffold();
|
|
51
|
+
assert.ok(!second.includes('agent-runtimes.json'));
|
|
52
|
+
assert.deepEqual(
|
|
53
|
+
JSON.parse(readFileSync(join(dir, 'agent-runtimes.json'), 'utf8')),
|
|
54
|
+
{ runtimes: [] },
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
40
59
|
test('an install predating the required runtime host receives it on its placeholder', () => {
|
|
41
60
|
withTempManagerDir((dir) => {
|
|
42
61
|
const envFile = join(dir, '.env');
|
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { GOOGLE_GRANTS, GOOGLE_GRANT_LABELS, defaultGoogleGrants } from './googleGrants.js';
|
|
6
6
|
|
|
7
7
|
// `agent-connectors` n'est pas cloné par le CI du manager, qui ne tire que
|
|
8
|
-
// llm-wiki, agent-
|
|
8
|
+
// llm-wiki, agent-production, agent-cme et agent-documents (voir
|
|
9
9
|
// check-versions.js). Les contrôles de cohérence croisée ci-dessous lisent la
|
|
10
10
|
// source de l'agent connectors : sans elle, on les saute plutôt que d'échouer
|
|
11
11
|
// sur un ENOENT. Le flux de release complet (build-and-push.sh) la fournit,
|
package/src/core/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { managerEnvFile, managerMcpEndpointsFile, readEnvFile } from './env.js';
|
|
3
3
|
|
|
4
|
-
const WIKI_MANAGER_VERSION = '0.15.
|
|
4
|
+
const WIKI_MANAGER_VERSION = '0.15.70';
|
|
5
5
|
|
|
6
6
|
function envValue(key) {
|
|
7
7
|
const filePath = managerEnvFile();
|