@dotdrelle/wiki-manager 0.15.27 → 0.15.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/agents.docker-compose.override.example.yml +1 -1
- package/docker-compose.override.example.yml +2 -2
- package/mcp.endpoints.example.json +1 -1
- package/package.json +2 -2
- package/src/agent/graph.js +77 -13
- package/src/agent/graph.test.js +148 -4
- package/src/cli/wiki-manager.js +48 -8
- package/src/cli/wiki-manager.test.js +35 -0
- package/src/commands/slash.js +159 -39
- package/src/commands/slash.test.js +100 -1
- package/src/core/agentsCompose.js +79 -0
- package/src/core/agentsCompose.test.js +99 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/composeOverrides.test.js +17 -3
- package/src/core/env.js +27 -10
- package/src/core/mcp.js +1 -1
- package/src/core/startupCheck.js +64 -27
- package/src/core/startupCheck.test.js +49 -1
- package/src/core/wikiSetup.js +57 -22
- package/src/core/wikiSetup.test.js +87 -0
- package/src/orchestrator/objectiveResolver.js +24 -0
- package/src/orchestrator/objectiveResolver.test.js +27 -0
- package/src/shell/LeftPane.tsx +49 -24
- package/src/shell/repl.js +114 -21
- package/src/shell/repl.test.js +102 -15
- package/src/shell/tui.tsx +0 -1
- package/src/shell/useSession.ts +21 -4
- package/wiki-workspace +29 -12
package/README.md
CHANGED
|
@@ -731,16 +731,17 @@ capabilityRouting:
|
|
|
731
731
|
|
|
732
732
|
#### Compose overrides — optional agents, proxies, local fixes
|
|
733
733
|
|
|
734
|
-
Two override files sit
|
|
734
|
+
Two override files sit under **`.wiki/compose/`**, one per stack:
|
|
735
735
|
|
|
736
736
|
| File | Applies to |
|
|
737
737
|
| --- | --- |
|
|
738
|
-
|
|
|
739
|
-
|
|
|
738
|
+
| `.wiki/compose/docker-compose.override.yml` | workspace stack (`serve`, `mcp-http`, `production-mcp`, `wiki`) |
|
|
739
|
+
| `.wiki/compose/agents.docker-compose.override.yml` | agents stack (`cme`, `documents`, `connectors`) |
|
|
740
740
|
|
|
741
741
|
Both are created for you on first use, from packaged templates full of
|
|
742
742
|
ready-to-uncomment examples, and are **never rewritten afterwards** — your edits
|
|
743
|
-
survive package updates.
|
|
743
|
+
survive package updates. Existing root-level files are migrated automatically.
|
|
744
|
+
Do not confuse them with `.wiki/runtime/*.compose.yml`,
|
|
744
745
|
which the manager regenerates on every Compose command; editing those is always
|
|
745
746
|
lost.
|
|
746
747
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# agents.docker-compose.override.yml — user-owned overrides
|
|
1
|
+
# .wiki/compose/agents.docker-compose.override.yml — user-owned agent overrides
|
|
2
2
|
#
|
|
3
3
|
# Copied here once by wiki-manager when absent, then NEVER touched again:
|
|
4
4
|
# your edits survive every package update. Do not confuse it with
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# docker-compose.override.yml — user-owned
|
|
1
|
+
# .wiki/compose/docker-compose.override.yml — user-owned workspace overrides
|
|
2
2
|
#
|
|
3
3
|
# Copied here once by wiki-manager when absent, then NEVER touched again:
|
|
4
4
|
# your edits survive every package update. Do not confuse it with
|
|
5
5
|
# .wiki/runtime/cacert.compose.yml, which is generated state and is rewritten
|
|
6
6
|
# on every compose command — editing that one is always lost.
|
|
7
7
|
#
|
|
8
|
-
# This file is GLOBAL: it lives
|
|
8
|
+
# This file is GLOBAL: it lives under .wiki/compose and applies to every
|
|
9
9
|
# workspace stack (`wiki-workspace up <workspace>`, `/start` in the shell). A
|
|
10
10
|
# proxy or a private registry is a property of the machine, not of a workspace,
|
|
11
11
|
# so there is deliberately no per-workspace variant. For values that really do
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"chatAccess": {
|
|
30
30
|
"maxToolIterations": 8,
|
|
31
31
|
"servers": {
|
|
32
|
-
"llm-wiki": { "allow": ["help_list", "help_read", "wiki_workspace_status", "wiki_list_pages", "wiki_read_page", "wiki_read_pages", "wiki_search_context", "wiki_collect_context", "wiki_read_ingested_source"] },
|
|
32
|
+
"llm-wiki": { "allow": ["help_list", "help_read", "help_search", "wiki_workspace_status", "wiki_list_pages", "wiki_read_page", "wiki_read_pages", "wiki_search_context", "wiki_collect_context", "wiki_read_ingested_source"] },
|
|
33
33
|
"wiki-production": { "allow": ["production_job_status", "production_jobs_list"] },
|
|
34
34
|
"cme": { "allow": ["cme_status", "cme_sources_list", "cme_export_status"] }
|
|
35
35
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.29",
|
|
4
4
|
"description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
|
|
5
5
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
6
6
|
"author": "dotrelle",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"start": "bun ./bin/wiki-manager.js",
|
|
14
|
-
"test": "node --test src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/runtimeLog.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/wikiWorkspace.test.js src/core/wikirc.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardPlaceholders.test.js src/runtime/store.test.js src/runtime/controlMessages.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/donna-contract.test.js src/runtime/auth.test.js",
|
|
14
|
+
"test": "node --test src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/runtimeLog.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikirc.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardPlaceholders.test.js src/runtime/store.test.js src/runtime/controlMessages.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/donna-contract.test.js src/runtime/auth.test.js",
|
|
15
15
|
"check-versions": "node scripts/check-versions.js",
|
|
16
16
|
"prepack": "node scripts/check-versions.js",
|
|
17
17
|
"prepublishOnly": "node scripts/check-versions.js",
|
package/src/agent/graph.js
CHANGED
|
@@ -376,7 +376,10 @@ async function classifyRequestedAction(llm, input, signal) {
|
|
|
376
376
|
const system = [
|
|
377
377
|
'Classify whether the user explicitly requests a real state-changing action now.',
|
|
378
378
|
'Actions include starting, stopping, importing, ingesting, building, exporting, configuring, writing, deleting, or sending.',
|
|
379
|
-
'Questions, explanations, status questions, greetings,
|
|
379
|
+
'Questions, explanations, status questions, greetings, hypothetical discussions, and bare capability questions are not actions.',
|
|
380
|
+
'Requests to refresh, show, or update the displayed plan/status are status reads, not state-changing actions.',
|
|
381
|
+
'A bare capability question such as "can you send an email?" or "peux-tu envoyer un mail ?" asks what the assistant can do; it does not request execution.',
|
|
382
|
+
'A concrete imperative or polite request such as "send this email to Alice" or "peux-tu envoyer ce message à Alice ?" is an action.',
|
|
380
383
|
'Return JSON only: {"action":true} or {"action":false}.',
|
|
381
384
|
].join('\n');
|
|
382
385
|
const messages = [{ role: 'user', content: String(input ?? '') }];
|
|
@@ -427,6 +430,28 @@ async function classifyRequestedAction(llm, input, signal) {
|
|
|
427
430
|
}
|
|
428
431
|
}
|
|
429
432
|
|
|
433
|
+
function looksLikeCapabilityQuestion(input) {
|
|
434
|
+
return /^(?:can|could|would)\s+you\b|^are\s+you\s+able\b|^do\s+you\s+(?:know\s+how|support)\b|^tu\s+peux\b|^vous\s+pouvez\b|^peux[\s-]*tu\b|^pouvez[\s-]*vous\b|^est[\s-]*ce\s+que\s+tu\s+peux\b/i
|
|
435
|
+
.test(String(input ?? '').trim());
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function delegationBlockerForDonna(rawFailure) {
|
|
439
|
+
const cleaned = String(rawFailure ?? '')
|
|
440
|
+
.replace(/^[A-Za-z][A-Za-z0-9_]*Error\s*:?\s*/i, '')
|
|
441
|
+
.replace(/\s*Available capabilities:\s*[\s\S]*$/i, '')
|
|
442
|
+
.trim();
|
|
443
|
+
const reason = /No connected agent can do that|No orchestrable capability/i.test(cleaned)
|
|
444
|
+
? 'No connected agent currently supports the requested action.'
|
|
445
|
+
: 'The requested action could not be assigned to a connected agent.';
|
|
446
|
+
return JSON.stringify({
|
|
447
|
+
delegated: false,
|
|
448
|
+
blocker: 'unsupported_action',
|
|
449
|
+
reason,
|
|
450
|
+
instruction:
|
|
451
|
+
'Answer the user naturally in their language. Explain the concrete limitation briefly. Do not expose exception names, capability identifiers, tool names, UUIDs, or internal routing details. Do not retry or claim that an action started.',
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
430
455
|
function summarizeToolArguments(rawArguments) {
|
|
431
456
|
if (!rawArguments || rawArguments === '{}') return '';
|
|
432
457
|
try {
|
|
@@ -816,9 +841,14 @@ async function handleRuntimeControlTool(session, tool, args = {}) {
|
|
|
816
841
|
}
|
|
817
842
|
}
|
|
818
843
|
|
|
819
|
-
function connectorConfigurationTarget(session, objective) {
|
|
820
|
-
const
|
|
821
|
-
|
|
844
|
+
export function connectorConfigurationTarget(session, objective) {
|
|
845
|
+
const recentContext = (session?.agentProjection?.conversation ?? [])
|
|
846
|
+
.slice(-6)
|
|
847
|
+
.filter((message) => message?.role === 'user')
|
|
848
|
+
.map((message) => String(message?.content ?? ''))
|
|
849
|
+
.join(' ');
|
|
850
|
+
const text = `${recentContext} ${String(objective ?? '')}`.trim().toLowerCase();
|
|
851
|
+
if (!/(?:configur|connect|authent|oauth|setup|sign[ -]?in|\bpat\b|api[ _-]?token|credential|identifiant|mot de passe|password)/i.test(text)) return null;
|
|
822
852
|
for (const [serverName, server] of Object.entries(session?.mcp ?? {})) {
|
|
823
853
|
if (server?.status !== 'connected' || !Array.isArray(server.tools) || server.tools.length === 0) continue;
|
|
824
854
|
const genericAliasParts = new Set([
|
|
@@ -977,6 +1007,7 @@ export function buildAgentSystemPrompt(state) {
|
|
|
977
1007
|
'When calling a tool, emit no preliminary narration. Call it directly; the PLAN and Activity panels show progress. After completion, keep the final response concise and proportional to the result.',
|
|
978
1008
|
'Write the way a thoughtful colleague speaks: warm, plain, and to the point. For a simple factual question, 1 to 3 sentences is the sweet spot. Stay synthetic and information-dense — use only the lines needed, and never exceed roughly 15 to 20 short lines even for a detailed answer. Never expose internal reasoning, repeated checks, tool-selection commentary, or a chronological diary. Prioritize the result, essential facts, concrete errors, and actual outputs — but say them in human language, not as a field dump.',
|
|
979
1009
|
'Call a matching direct tool when one is offered. Otherwise, for an action backed by a discovered agent capability, call runtime__delegate with the original objective. This applies to both planner agents and executor-only single-task agents. Never call an agent orchestration-contract or plan tool directly.',
|
|
1010
|
+
'Configuration is not a business run. When a connected server offers a setup or configuration tool, use it directly; never delegate configuration to an export, collect, send, build, or ingest capability. Read that server status first when existing non-secret values are needed, then ask only for required values that are still missing.',
|
|
980
1011
|
'For any question about the current workspace inventory or what is waiting there, call wiki__wiki_workspace_status first and answer only from its result. This is the canonical read-only workspace state; do not reconstruct it from upload, connector, or production tools.',
|
|
981
1012
|
'Tool identifiers are private implementation details. Never print MCP tool names such as server__tool in a user-facing answer. Describe the human result instead.',
|
|
982
1013
|
'Internal data shapes are private too. Never quote raw JSON field names (e.g. pendingSources.files), internal directory paths (e.g. raw/untracked/), or config keys in a user-facing answer — translate them into plain language. Say "36 pages sources sont en attente d\'ingestion", not the field or path they came from.',
|
|
@@ -998,6 +1029,7 @@ export function buildAgentSystemPrompt(state) {
|
|
|
998
1029
|
? `Workspace profile (.wiki/profile.md) — durable user preferences, apply these to every reply (tone, tutoiement/vouvoiement, formatting, etc.):\n${workspaceProfile}`
|
|
999
1030
|
: null,
|
|
1000
1031
|
'Runtime control: you have runtime__status, runtime__cancel, runtime__kill, runtime__approve and runtime__enqueue. When the user asks to stop, remove, clean or kill the current run, its jobs or the queue ("supprime le job et la queue", "arr\u00eate tout"), call runtime__kill (or runtime__cancel for a soft stop of just the run) and confirm what was stopped. When the user explicitly asks to delete, reset, abandon or replace the current plan, call runtime__kill with purge=true; never set purge=true for a simple stop. For questions about what is running or queued, call runtime__status and answer from its data. When the user consents to a pending approval in any phrasing ("vas-y", "ok pour l\'export"), call runtime__approve. When the user asks for a NEW action while a run is active, do not execute it: propose runtime__enqueue (run it after) or, if they insist it replaces the current work, runtime__kill then the new action.',
|
|
1032
|
+
'When the user asks to refresh, show, or update the displayed plan or status, call runtime__status. This is a state refresh request, not a new business capability, and must never be delegated.',
|
|
1001
1033
|
'Report every runtime control outcome exactly as the tool returned it \u2014 never embellish. If runtime__kill reports 0 run(s)/0 task(s)/0 purged, say there was nothing active to stop or purge; do NOT claim a run, plan, pending approval or queue item was removed. If runtime__status returns an error or could not be read, say the runtime state could not be retrieved and do not describe a state you never obtained. Never assert that something was cleaned, cancelled, approved or purged unless that specific tool result confirms it.',
|
|
1002
1034
|
'Durable profile updates are actions in this stabilized version: delegate them instead of writing directly.',
|
|
1003
1035
|
].filter(Boolean).join('\n');
|
|
@@ -1089,7 +1121,10 @@ export function isOrchestrationBypassTool(name) {
|
|
|
1089
1121
|
if (full === 'wiki__plan_set' || full === 'wiki__plan_done') return true;
|
|
1090
1122
|
const sep = full.indexOf('__');
|
|
1091
1123
|
const tool = sep === -1 ? full : full.slice(sep + 2);
|
|
1092
|
-
return tool === 'agent_plan'
|
|
1124
|
+
return tool === 'agent_plan'
|
|
1125
|
+
|| tool === 'agent_execute'
|
|
1126
|
+
|| tool === 'production_start_job'
|
|
1127
|
+
|| tool === 'cme_export_run';
|
|
1093
1128
|
}
|
|
1094
1129
|
|
|
1095
1130
|
function isReadOnlyMcpCall(session, server, tool) {
|
|
@@ -1195,7 +1230,9 @@ export function createAgentGraph(options = {}) {
|
|
|
1195
1230
|
WIKI_PLAN_DONE_TOOL,
|
|
1196
1231
|
...buildLlmTools(state.session.mcp),
|
|
1197
1232
|
];
|
|
1198
|
-
const tools =
|
|
1233
|
+
const tools = state.terminalToolFailure || state.session._responseSynthesisOnly
|
|
1234
|
+
? []
|
|
1235
|
+
: toolsForClassification(classification, writeTools, state.session);
|
|
1199
1236
|
const system = buildAgentSystemPrompt(state);
|
|
1200
1237
|
|
|
1201
1238
|
// On iteration 0: prior history is in state.messages, user input must be appended.
|
|
@@ -1445,6 +1482,7 @@ export function createAgentGraph(options = {}) {
|
|
|
1445
1482
|
}
|
|
1446
1483
|
|
|
1447
1484
|
async function toolExecutorNode(state) {
|
|
1485
|
+
const llm = state.session.llm ?? options.llm ?? null;
|
|
1448
1486
|
const toolCalls = state.pendingToolCalls ?? [];
|
|
1449
1487
|
const toolResultMessages = [];
|
|
1450
1488
|
let terminalFailure = null;
|
|
@@ -1535,12 +1573,41 @@ export function createAgentGraph(options = {}) {
|
|
|
1535
1573
|
const result = await updateWorkspaceProfilePreference(state.session, args.preference);
|
|
1536
1574
|
resultText = JSON.stringify(result, null, 2);
|
|
1537
1575
|
} else if (server === 'runtime') {
|
|
1538
|
-
|
|
1576
|
+
const isCapabilityQuestion = tool === 'delegate'
|
|
1577
|
+
&& !state.session._currentRunIdentity
|
|
1578
|
+
&& looksLikeCapabilityQuestion(String(args.objective ?? state.input ?? ''))
|
|
1579
|
+
&& !await classifyRequestedAction(
|
|
1580
|
+
llm,
|
|
1581
|
+
String(args.objective ?? state.input ?? ''),
|
|
1582
|
+
state.session._abortSignal,
|
|
1583
|
+
);
|
|
1584
|
+
resultText = isCapabilityQuestion
|
|
1585
|
+
? JSON.stringify({
|
|
1586
|
+
delegated: false,
|
|
1587
|
+
capabilityQuestion: true,
|
|
1588
|
+
instruction: 'Answer the user conversationally about whether this action is supported. Do not create a plan or claim that execution started.',
|
|
1589
|
+
})
|
|
1590
|
+
: await handleRuntimeControlTool(state.session, tool, args);
|
|
1539
1591
|
if (tool === 'delegate' && /^Runtime control error \(delegate\):/i.test(resultText)) {
|
|
1540
|
-
|
|
1592
|
+
const delegationFailure = resultText
|
|
1541
1593
|
.replace(/^Runtime control error \(delegate\):\s*/i, '')
|
|
1542
1594
|
.replace(/^Delegation failed during objective_resolution:\s*/i, '');
|
|
1543
|
-
|
|
1595
|
+
const needsInput = delegationFailure.match(/^Delegation requires input:\s*(.+)$/i);
|
|
1596
|
+
if (needsInput) {
|
|
1597
|
+
// Missing provider-required fields are a conversational blocker,
|
|
1598
|
+
// not an execution failure. Feed the generic field list back to
|
|
1599
|
+
// Donna so she can ask naturally in the workspace language.
|
|
1600
|
+
resultText = JSON.stringify({
|
|
1601
|
+
delegated: false,
|
|
1602
|
+
needsInput: true,
|
|
1603
|
+
missingRequiredFields: needsInput[1].split(',').map((item) => item.trim()).filter(Boolean),
|
|
1604
|
+
instruction: 'Ask the user for the missing required information. Do not expose internal validation details.',
|
|
1605
|
+
});
|
|
1606
|
+
} else {
|
|
1607
|
+
terminalFailure = delegationFailure;
|
|
1608
|
+
resultText = delegationBlockerForDonna(delegationFailure);
|
|
1609
|
+
ok = false;
|
|
1610
|
+
}
|
|
1544
1611
|
}
|
|
1545
1612
|
} else if (server !== 'shell') {
|
|
1546
1613
|
await awaitRunApproval(state.session, { runId, tool: toolName });
|
|
@@ -1641,10 +1708,7 @@ export function createAgentGraph(options = {}) {
|
|
|
1641
1708
|
}
|
|
1642
1709
|
|
|
1643
1710
|
if (terminalFailure) {
|
|
1644
|
-
const response = `Action non lancée : ${terminalFailure}`;
|
|
1645
|
-
emitAgentEvent(state.session, 'assistant_message', 'agent_guard', { content: response });
|
|
1646
1711
|
return {
|
|
1647
|
-
response,
|
|
1648
1712
|
messages: toolResultMessages,
|
|
1649
1713
|
pendingToolCalls: null,
|
|
1650
1714
|
forceDelegation: false,
|
|
@@ -1666,7 +1730,7 @@ export function createAgentGraph(options = {}) {
|
|
|
1666
1730
|
}
|
|
1667
1731
|
|
|
1668
1732
|
function routeToolExecutor(state) {
|
|
1669
|
-
return
|
|
1733
|
+
return 'orchestrator';
|
|
1670
1734
|
}
|
|
1671
1735
|
|
|
1672
1736
|
function routeOrchestrator(state) {
|
package/src/agent/graph.test.js
CHANGED
|
@@ -3,7 +3,7 @@ import test from 'node:test';
|
|
|
3
3
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
import { buildAgentSystemPrompt, createAgentGraph, invalidSuggestedSlashCommands, invalidUserFacingToolNames, knownCapabilityIds, normalizeToolArgumentsFromSchema } from './graph.js';
|
|
6
|
+
import { buildAgentSystemPrompt, connectorConfigurationTarget, createAgentGraph, invalidSuggestedSlashCommands, invalidUserFacingToolNames, isOrchestrationBypassTool, knownCapabilityIds, normalizeToolArgumentsFromSchema } from './graph.js';
|
|
7
7
|
|
|
8
8
|
test('user-facing response guard hides MCP identifiers generically', () => {
|
|
9
9
|
const session = sessionBase();
|
|
@@ -13,6 +13,29 @@ test('user-facing response guard hides MCP identifiers generically', () => {
|
|
|
13
13
|
);
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
+
test('CME setup stays direct while CME export execution stays orchestrated', () => {
|
|
17
|
+
assert.equal(isOrchestrationBypassTool('cme__cme_export_run'), true);
|
|
18
|
+
assert.equal(isOrchestrationBypassTool('cme__cme_setup'), false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('configuration routing retains the recent CME conversation context', () => {
|
|
22
|
+
const target = connectorConfigurationTarget({
|
|
23
|
+
agentProjection: {
|
|
24
|
+
conversation: [{ role: 'user', content: 'je veux configurer le CME' }],
|
|
25
|
+
},
|
|
26
|
+
mcp: {
|
|
27
|
+
cme: {
|
|
28
|
+
status: 'connected',
|
|
29
|
+
tools: [
|
|
30
|
+
{ name: 'cme_setup', description: 'Configure Confluence credentials.' },
|
|
31
|
+
{ name: 'cme_export_run', description: 'Run export.' },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}, 'configurer l’agent wiki');
|
|
36
|
+
assert.deepEqual(target, { serverName: 'cme', setupTool: 'cme_setup' });
|
|
37
|
+
});
|
|
38
|
+
|
|
16
39
|
test('Donna cannot answer an explicit action with manual instructions instead of delegating', async () => {
|
|
17
40
|
const originalFetch = globalThis.fetch;
|
|
18
41
|
let delegated = false;
|
|
@@ -57,6 +80,68 @@ test('Donna cannot answer an explicit action with manual instructions instead of
|
|
|
57
80
|
}
|
|
58
81
|
});
|
|
59
82
|
|
|
83
|
+
test('a bare capability question returns to Donna without runtime delegation', async () => {
|
|
84
|
+
const originalFetch = globalThis.fetch;
|
|
85
|
+
let delegated = false;
|
|
86
|
+
globalThis.fetch = async (url) => {
|
|
87
|
+
delegated ||= String(url).includes('/delegate');
|
|
88
|
+
if (String(url).includes('/delegate')) throw new Error(`Unexpected runtime request: ${url}`);
|
|
89
|
+
return { ok: true, status: 200, json: async () => ({ status: 'idle', running: false }) };
|
|
90
|
+
};
|
|
91
|
+
let mainCalls = 0;
|
|
92
|
+
const session = sessionBase({
|
|
93
|
+
runtime: { url: 'http://runtime.test' },
|
|
94
|
+
llm: {
|
|
95
|
+
async completeWithTools({ tools, messages }) {
|
|
96
|
+
if (tools.some((tool) => tool.function?.name === 'classify_action_request')) {
|
|
97
|
+
return {
|
|
98
|
+
content: null,
|
|
99
|
+
message: { role: 'assistant', content: null },
|
|
100
|
+
tool_calls: [{
|
|
101
|
+
id: 'classify-capability-question',
|
|
102
|
+
type: 'function',
|
|
103
|
+
function: { name: 'classify_action_request', arguments: '{"action":false}' },
|
|
104
|
+
}],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
mainCalls += 1;
|
|
108
|
+
if (mainCalls === 1) {
|
|
109
|
+
return {
|
|
110
|
+
content: null,
|
|
111
|
+
message: { role: 'assistant', content: null },
|
|
112
|
+
tool_calls: [{
|
|
113
|
+
id: 'wrong-delegate',
|
|
114
|
+
type: 'function',
|
|
115
|
+
function: { name: 'runtime__delegate', arguments: '{"objective":"tu peux envoyer un mail ?"}' },
|
|
116
|
+
}],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const result = JSON.parse(
|
|
120
|
+
String((messages ?? []).filter((message) => message.role === 'tool').at(-1)?.content ?? '{}'),
|
|
121
|
+
);
|
|
122
|
+
assert.equal(result.capabilityQuestion, true);
|
|
123
|
+
return {
|
|
124
|
+
content: 'Oui, je peux envoyer un mail si tu me donnes le destinataire, le sujet et le contenu.',
|
|
125
|
+
message: {
|
|
126
|
+
role: 'assistant',
|
|
127
|
+
content: 'Oui, je peux envoyer un mail si tu me donnes le destinataire, le sujet et le contenu.',
|
|
128
|
+
},
|
|
129
|
+
tool_calls: null,
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const result = await createAgentGraph().invoke({ input: 'tu peux envoyer un mail ?', session });
|
|
137
|
+
assert.equal(delegated, false);
|
|
138
|
+
assert.equal(mainCalls, 2);
|
|
139
|
+
assert.match(result.response, /Oui, je peux envoyer un mail/);
|
|
140
|
+
} finally {
|
|
141
|
+
globalThis.fetch = originalFetch;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
60
145
|
function sessionBase(overrides = {}) {
|
|
61
146
|
return {
|
|
62
147
|
commands: ['status'],
|
|
@@ -949,7 +1034,7 @@ test('forced delegation is cleared after one valid tool call and does not loop',
|
|
|
949
1034
|
}
|
|
950
1035
|
});
|
|
951
1036
|
|
|
952
|
-
test('a rejected runtime delegation
|
|
1037
|
+
test('a rejected runtime delegation returns to Donna once without leaking technical details', async () => {
|
|
953
1038
|
const originalFetch = globalThis.fetch;
|
|
954
1039
|
globalThis.fetch = async () => ({
|
|
955
1040
|
ok: false,
|
|
@@ -964,6 +1049,16 @@ test('a rejected runtime delegation is terminal and never loops', async () => {
|
|
|
964
1049
|
llm: {
|
|
965
1050
|
async completeWithTools() {
|
|
966
1051
|
calls += 1;
|
|
1052
|
+
if (calls === 2) {
|
|
1053
|
+
return {
|
|
1054
|
+
content: 'Je ne peux pas lancer cette action : aucun agent connecté ne la prend actuellement en charge.',
|
|
1055
|
+
message: {
|
|
1056
|
+
role: 'assistant',
|
|
1057
|
+
content: 'Je ne peux pas lancer cette action : aucun agent connecté ne la prend actuellement en charge.',
|
|
1058
|
+
},
|
|
1059
|
+
tool_calls: [],
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
967
1062
|
return {
|
|
968
1063
|
content: null,
|
|
969
1064
|
message: { role: 'assistant', content: null },
|
|
@@ -979,14 +1074,63 @@ test('a rejected runtime delegation is terminal and never loops', async () => {
|
|
|
979
1074
|
|
|
980
1075
|
try {
|
|
981
1076
|
const result = await createAgentGraph().invoke({ input: 'lance ingestion', session });
|
|
982
|
-
assert.equal(calls,
|
|
983
|
-
assert.equal(
|
|
1077
|
+
assert.equal(calls, 2);
|
|
1078
|
+
assert.equal(
|
|
1079
|
+
result.response,
|
|
1080
|
+
'Je ne peux pas lancer cette action : aucun agent connecté ne la prend actuellement en charge.',
|
|
1081
|
+
);
|
|
1082
|
+
assert.doesNotMatch(result.response, /ObjectiveNotOrchestrableError|capabilit|runtime__|[0-9a-f]{8}-/i);
|
|
984
1083
|
assert.equal(result.terminalToolFailure, true);
|
|
985
1084
|
} finally {
|
|
986
1085
|
globalThis.fetch = originalFetch;
|
|
987
1086
|
}
|
|
988
1087
|
});
|
|
989
1088
|
|
|
1089
|
+
test('a delegation missing required provider inputs returns to Donna for clarification', async () => {
|
|
1090
|
+
const originalFetch = globalThis.fetch;
|
|
1091
|
+
globalThis.fetch = async () => ({
|
|
1092
|
+
ok: false,
|
|
1093
|
+
status: 422,
|
|
1094
|
+
json: async () => ({
|
|
1095
|
+
error: 'Delegation requires input: to, subject, body',
|
|
1096
|
+
}),
|
|
1097
|
+
});
|
|
1098
|
+
let calls = 0;
|
|
1099
|
+
const session = sessionBase({
|
|
1100
|
+
runtime: { url: 'http://runtime.test' },
|
|
1101
|
+
llm: {
|
|
1102
|
+
async completeWithTools() {
|
|
1103
|
+
calls += 1;
|
|
1104
|
+
if (calls === 1) {
|
|
1105
|
+
return {
|
|
1106
|
+
content: null,
|
|
1107
|
+
message: { role: 'assistant', content: null },
|
|
1108
|
+
tool_calls: [{
|
|
1109
|
+
id: 'delegate-needs-input',
|
|
1110
|
+
type: 'function',
|
|
1111
|
+
function: { name: 'runtime__delegate', arguments: '{"objective":"envoie un mail"}' },
|
|
1112
|
+
}],
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
return {
|
|
1116
|
+
content: 'Oui. À qui dois-je écrire, avec quel objet et quel message ?',
|
|
1117
|
+
message: { role: 'assistant', content: 'Oui. À qui dois-je écrire, avec quel objet et quel message ?' },
|
|
1118
|
+
tool_calls: [],
|
|
1119
|
+
};
|
|
1120
|
+
},
|
|
1121
|
+
},
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
try {
|
|
1125
|
+
const result = await createAgentGraph().invoke({ input: 'envoie un mail', session });
|
|
1126
|
+
assert.equal(calls, 2);
|
|
1127
|
+
assert.equal(result.response, 'Oui. À qui dois-je écrire, avec quel objet et quel message ?');
|
|
1128
|
+
assert.equal(result.terminalToolFailure, false);
|
|
1129
|
+
} finally {
|
|
1130
|
+
globalThis.fetch = originalFetch;
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
|
|
990
1134
|
// Guard: the system prompt must never show a connected tool's bare name
|
|
991
1135
|
// outside its qualified server__tool form. Bare mentions are what teach the
|
|
992
1136
|
// model to emit unqualified tool calls (the cme_status incident). The bare
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -171,6 +171,18 @@ export async function resolveExecutorArguments({ llm, objective, capability, sig
|
|
|
171
171
|
return {};
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
export function missingRequiredArguments(schema, args) {
|
|
175
|
+
const required = Array.isArray(schema?.required) ? schema.required.map(String) : [];
|
|
176
|
+
const values = args && typeof args === 'object' && !Array.isArray(args) ? args : {};
|
|
177
|
+
return required.filter((key) => {
|
|
178
|
+
const value = values[key];
|
|
179
|
+
if (value === undefined || value === null) return true;
|
|
180
|
+
if (typeof value === 'string') return value.trim() === '';
|
|
181
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
182
|
+
return false;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
174
186
|
function safeParseArgumentObject(text) {
|
|
175
187
|
const cleaned = String(text ?? '').trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
|
|
176
188
|
if (!cleaned) return null;
|
|
@@ -1013,6 +1025,13 @@ async function runRuntime(argv, agent) {
|
|
|
1013
1025
|
capability: provider.capability,
|
|
1014
1026
|
signal: session._abortSignal,
|
|
1015
1027
|
});
|
|
1028
|
+
const missingArguments = missingRequiredArguments(
|
|
1029
|
+
provider.capability?.inputSchema,
|
|
1030
|
+
extractedArguments,
|
|
1031
|
+
);
|
|
1032
|
+
if (missingArguments.length > 0) {
|
|
1033
|
+
throw new Error(`Delegation requires input: ${missingArguments.join(', ')}`);
|
|
1034
|
+
}
|
|
1016
1035
|
fragment = buildExecutorOnlyFragment({
|
|
1017
1036
|
objective,
|
|
1018
1037
|
workspace: session.workspace ?? context.workspace ?? 'workspace',
|
|
@@ -1434,21 +1453,35 @@ export async function runCli(argv) {
|
|
|
1434
1453
|
// in a random cwd must not litter files.
|
|
1435
1454
|
const scaffolded = ensureManagerScaffold({ log: (message) => console.log(`[wiki-manager] ${message}`) });
|
|
1436
1455
|
if (scaffolded.length > 0) loadManagerEnv();
|
|
1437
|
-
const reportCheck = ({ kind, ok, detail, context, skipped, pending }) => {
|
|
1456
|
+
const reportCheck = ({ kind, ok, detail, context, skipped, pending, requested }) => {
|
|
1457
|
+
// "waiting" checks (containers not up yet, MCP not connected yet) are the
|
|
1458
|
+
// normal state at launch — the shell starts them and reports the real
|
|
1459
|
+
// status afterwards, so echoing them here is pure noise. Only what is
|
|
1460
|
+
// ready or actually broken is printed.
|
|
1461
|
+
//
|
|
1462
|
+
// `pending` alone cannot express this: a service the operator explicitly
|
|
1463
|
+
// asked for (`requested`) is still "pending" right after /start, and that
|
|
1464
|
+
// one MUST be shown — silence there is how a failed start reads as a
|
|
1465
|
+
// success. Boot silence applies to unrequested checks only.
|
|
1466
|
+
if (!ok && (pending || skipped) && !requested) return;
|
|
1438
1467
|
const labels = { docker: 'Docker', internet: 'Internet', agents: 'Agent containers', workspace: 'Workspaces', containers: 'Workspace containers', mcp: 'MCP' };
|
|
1439
1468
|
const label = labels[kind] ?? kind;
|
|
1440
1469
|
const instruction = !ok && context?.command ? ` — command: ${context.command}` : '';
|
|
1441
|
-
|
|
1470
|
+
// On success the detail is diagnostic only (the probe URL for Internet);
|
|
1471
|
+
// the label already says what passed.
|
|
1472
|
+
const suffix = ok ? '' : ` — ${detail || context?.error || context?.dockerError || 'waiting'}`;
|
|
1442
1473
|
const color = ok ? '\x1b[32m' : '\x1b[33m';
|
|
1443
|
-
const state = ok ? 'ready' :
|
|
1444
|
-
const icon = ok ? '✓' :
|
|
1474
|
+
const state = ok ? 'ready' : 'needs attention';
|
|
1475
|
+
const icon = ok ? '✓' : '✗';
|
|
1445
1476
|
console.log(`${color}${icon} configuration: ${label} ${state}${suffix}${instruction}\x1b[0m`);
|
|
1446
1477
|
};
|
|
1447
1478
|
let preflight = await runPreflightChecks({ onCheck: reportCheck });
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
//
|
|
1479
|
+
const wizardGaps = startupWizardGaps(preflight.gaps);
|
|
1480
|
+
if (wizardGaps.length > 0) {
|
|
1481
|
+
await runStartupWizard(wizardGaps);
|
|
1482
|
+
// The wizard may have created a workspace or repaired configuration.
|
|
1483
|
+
// Agents are deliberately excluded: starting them is an explicit console
|
|
1484
|
+
// action through /start agents or /start all.
|
|
1452
1485
|
preflight = await runPreflightChecks();
|
|
1453
1486
|
}
|
|
1454
1487
|
const dockerReady = preflight.checks.some((check) => check.kind === 'docker' && check.ok);
|
|
@@ -1503,3 +1536,10 @@ export async function runCli(argv) {
|
|
|
1503
1536
|
await shutdownOwnedRuntime(runtime, { log: (message) => console.log(`[wiki-manager] ${message}`) });
|
|
1504
1537
|
}
|
|
1505
1538
|
}
|
|
1539
|
+
|
|
1540
|
+
// Missing/stopped agents are status information, never a reason to interrupt
|
|
1541
|
+
// startup with a confirmation screen. The operator owns their lifecycle from
|
|
1542
|
+
// the console (`/start agents`, `/start all`, `/stop agents`).
|
|
1543
|
+
export function startupWizardGaps(gaps = []) {
|
|
1544
|
+
return gaps.filter((gap) => gap?.kind !== 'agents');
|
|
1545
|
+
}
|
|
@@ -3,8 +3,10 @@ import test from 'node:test';
|
|
|
3
3
|
import {
|
|
4
4
|
buildExecutorOnlyFragment,
|
|
5
5
|
forwardRuntimeApproval,
|
|
6
|
+
missingRequiredArguments,
|
|
6
7
|
resolveExecutorArguments,
|
|
7
8
|
resolvePreparedDelegationApproval,
|
|
9
|
+
startupWizardGaps,
|
|
8
10
|
} from './wiki-manager.js';
|
|
9
11
|
|
|
10
12
|
const COLLECT_CAPABILITY = {
|
|
@@ -19,6 +21,18 @@ const COLLECT_CAPABILITY = {
|
|
|
19
21
|
},
|
|
20
22
|
};
|
|
21
23
|
|
|
24
|
+
test('startup never opens the setup wizard just because agents are stopped', () => {
|
|
25
|
+
const workspace = { kind: 'workspace', context: {} };
|
|
26
|
+
assert.deepEqual(
|
|
27
|
+
startupWizardGaps([
|
|
28
|
+
{ kind: 'agents', context: { downServices: ['cme', 'documents'] } },
|
|
29
|
+
workspace,
|
|
30
|
+
]),
|
|
31
|
+
[workspace],
|
|
32
|
+
);
|
|
33
|
+
assert.deepEqual(startupWizardGaps([{ kind: 'agents' }]), []);
|
|
34
|
+
});
|
|
35
|
+
|
|
22
36
|
test('executor-only capabilities receive one manager-authored executable task', () => {
|
|
23
37
|
const fragment = buildExecutorOnlyFragment({
|
|
24
38
|
objective: 'donne-moi mes derniers mails',
|
|
@@ -96,6 +110,27 @@ test('argument extraction stays agnostic and safe when it cannot extract', async
|
|
|
96
110
|
);
|
|
97
111
|
});
|
|
98
112
|
|
|
113
|
+
test('required executor arguments become a conversational blocker before plan validation', () => {
|
|
114
|
+
const schema = {
|
|
115
|
+
type: 'object',
|
|
116
|
+
required: ['to', 'subject', 'body'],
|
|
117
|
+
properties: {
|
|
118
|
+
to: { type: 'string' },
|
|
119
|
+
subject: { type: 'string' },
|
|
120
|
+
body: { type: 'string' },
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
assert.deepEqual(missingRequiredArguments(schema, {}), ['to', 'subject', 'body']);
|
|
124
|
+
assert.deepEqual(
|
|
125
|
+
missingRequiredArguments(schema, { to: 'a@example.test', subject: 'Hello', body: 'Message' }),
|
|
126
|
+
[],
|
|
127
|
+
);
|
|
128
|
+
assert.deepEqual(
|
|
129
|
+
missingRequiredArguments(schema, { to: [], subject: ' ', body: 'Message' }),
|
|
130
|
+
['to', 'subject'],
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
99
134
|
test('runtime approval bridge preserves the complete run-scoped grant', async () => {
|
|
100
135
|
let forwarded = null;
|
|
101
136
|
const request = {
|