@dotdrelle/wiki-manager 0.15.28 → 0.15.32
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 +5 -2
- package/README.md +5 -4
- package/agents.docker-compose.override.example.yml +1 -1
- package/agents.docker-compose.yml +10 -0
- package/bin/wiki-manager.js +1 -1
- package/docker-compose.override.example.yml +3 -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 +165 -8
- package/src/cli/runtimeStartup.test.js +14 -0
- package/src/cli/wiki-manager.js +118 -19
- package/src/cli/wiki-manager.test.js +192 -0
- package/src/commands/slash.js +168 -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 +113 -38
- package/src/core/mcp.test.js +181 -20
- 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/core/wikiWorkspace.test.js +16 -0
- package/src/orchestrator/agentRegistry.js +35 -7
- package/src/orchestrator/agentRegistry.test.js +73 -0
- package/src/orchestrator/dispatcher.js +6 -1
- package/src/orchestrator/dispatcher.test.js +24 -1
- package/src/orchestrator/objectiveResolver.js +24 -0
- package/src/orchestrator/objectiveResolver.test.js +27 -0
- package/src/runtime/auth.test.js +1 -65
- package/src/runtime/donna-contract.test.js +2 -1
- package/src/runtime/lifecycle.js +0 -33
- package/src/runtime/runner.test.js +10 -1
- package/src/runtime/supervisor.test.js +11 -1
- package/src/shell/LeftPane.tsx +49 -24
- package/src/shell/repl.js +114 -21
- package/src/shell/repl.test.js +88 -17
- package/src/shell/tui.tsx +10 -19
- package/wiki-workspace +98 -12
package/.env.example
CHANGED
|
@@ -56,8 +56,11 @@ CONNECTORS_MCP_AUTH_TOKEN=
|
|
|
56
56
|
# Enable the opt-in agent-connectors service:
|
|
57
57
|
CONNECTORS_ENABLED=false
|
|
58
58
|
#
|
|
59
|
-
# The
|
|
60
|
-
#
|
|
59
|
+
# The wikiLLM Google OAuth application is baked into the agent-connectors image
|
|
60
|
+
# at build time, from agent-external/agent-connectors/.env.build.local. When you
|
|
61
|
+
# build the image locally, that file must exist — otherwise the container starts
|
|
62
|
+
# with empty credentials.
|
|
63
|
+
# Optional runtime override (no rebuild needed):
|
|
61
64
|
# GOOGLE_OAUTH_CLIENT_ID=
|
|
62
65
|
# Optional confidential-client compatibility override:
|
|
63
66
|
# GOOGLE_OAUTH_CLIENT_SECRET=
|
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
|
|
@@ -100,6 +100,16 @@ services:
|
|
|
100
100
|
build:
|
|
101
101
|
context: ../agent-external/agent-connectors
|
|
102
102
|
dockerfile: Dockerfile
|
|
103
|
+
args:
|
|
104
|
+
# Without these the ARGs stay unset and the image ships empty
|
|
105
|
+
# WIKILLM_GOOGLE_OAUTH_* values, so the container ends up with no Google
|
|
106
|
+
# client at all. `wiki-workspace agents up` exports them from the
|
|
107
|
+
# connectors repo's .env.build.local before invoking Compose.
|
|
108
|
+
# Deliberately value-less: Compose then forwards each host variable only
|
|
109
|
+
# when it is defined. Never write `${VAR:-}` here — an empty
|
|
110
|
+
# --build-arg pins the ARG to the empty string.
|
|
111
|
+
WIKILLM_GOOGLE_OAUTH_CLIENT_ID:
|
|
112
|
+
WIKILLM_GOOGLE_OAUTH_CLIENT_SECRET:
|
|
103
113
|
image: dotdrelle/agent-connectors:latest
|
|
104
114
|
user: "${UID:-1000}:${GID:-1000}"
|
|
105
115
|
ports:
|
package/bin/wiki-manager.js
CHANGED
|
@@ -80,7 +80,7 @@ async function main() {
|
|
|
80
80
|
// Fallback for already-bootstrapped direct invocations; the shell wrapper
|
|
81
81
|
// exports these before Bun starts.
|
|
82
82
|
if (parsed.cacert) Object.assign(process.env, cacertEnvVars(parsed.cacert));
|
|
83
|
-
const interactive = process.stdout.isTTY && process.stdin.isTTY && argv[0] !== 'runtime' && !argv.includes('--setup-wizard') && !argv.includes('--headless') && !argv.includes('--once') && !argv.includes('--version') && !argv.includes('-v') && !argv.includes('--help') && !argv.includes('-h');
|
|
83
|
+
const interactive = process.stdout.isTTY && process.stdin.isTTY && argv[0] !== 'runtime' && !argv.includes('--refresh') && !argv.includes('--setup-wizard') && !argv.includes('--headless') && !argv.includes('--once') && !argv.includes('--version') && !argv.includes('-v') && !argv.includes('--help') && !argv.includes('-h');
|
|
84
84
|
if (interactive || argv.includes('--setup-wizard')) await import('@opentui/solid/preload');
|
|
85
85
|
if (interactive) process.stdout.write('Starting wiki-manager…\r');
|
|
86
86
|
const { runCli } = await import('../src/cli/wiki-manager.js');
|
|
@@ -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
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
# services:
|
|
41
41
|
# serve:
|
|
42
42
|
# environment:
|
|
43
|
+
# - NODE_USE_ENV_PROXY=${NODE_USE_ENV_PROXY:-1}
|
|
43
44
|
# - HTTP_PROXY=${HTTP_PROXY:-}
|
|
44
45
|
# - HTTPS_PROXY=${HTTPS_PROXY:-}
|
|
45
46
|
# - http_proxy=${HTTP_PROXY:-}
|
|
@@ -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.32",
|
|
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'],
|
|
@@ -118,8 +203,10 @@ function toolCallingLlm() {
|
|
|
118
203
|
test('agent graph waits for run-level approval before first MCP action', async () => {
|
|
119
204
|
const originalFetch = globalThis.fetch;
|
|
120
205
|
let fetchCalls = 0;
|
|
121
|
-
globalThis.fetch = async () => {
|
|
122
|
-
|
|
206
|
+
globalThis.fetch = async (_url, init) => {
|
|
207
|
+
// Count tool traffic only: the MCP session handshake is transport
|
|
208
|
+
// plumbing, not an action the user needs to approve or observe.
|
|
209
|
+
if (JSON.parse(init.body).method === 'tools/call') fetchCalls += 1;
|
|
123
210
|
return {
|
|
124
211
|
ok: true,
|
|
125
212
|
status: 200,
|
|
@@ -403,6 +490,15 @@ test('Donna refuses to delegate connector authentication to an export capability
|
|
|
403
490
|
globalThis.fetch = async (url, options = {}) => {
|
|
404
491
|
fetchedUrls.push(String(url));
|
|
405
492
|
const body = JSON.parse(String(options.body ?? '{}'));
|
|
493
|
+
// MCP session handshake: answer it, then assert on the real tool call.
|
|
494
|
+
if (body.method === 'initialize') {
|
|
495
|
+
return {
|
|
496
|
+
ok: true,
|
|
497
|
+
status: 200,
|
|
498
|
+
headers: { get: () => null },
|
|
499
|
+
text: async () => '{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-06-18"}}',
|
|
500
|
+
};
|
|
501
|
+
}
|
|
406
502
|
assert.equal(body.params?.name, 'connectors_google_oauth_start');
|
|
407
503
|
assert.deepEqual(body.params?.arguments, { workspace: 'docs' });
|
|
408
504
|
return {
|
|
@@ -949,7 +1045,7 @@ test('forced delegation is cleared after one valid tool call and does not loop',
|
|
|
949
1045
|
}
|
|
950
1046
|
});
|
|
951
1047
|
|
|
952
|
-
test('a rejected runtime delegation
|
|
1048
|
+
test('a rejected runtime delegation returns to Donna once without leaking technical details', async () => {
|
|
953
1049
|
const originalFetch = globalThis.fetch;
|
|
954
1050
|
globalThis.fetch = async () => ({
|
|
955
1051
|
ok: false,
|
|
@@ -964,6 +1060,16 @@ test('a rejected runtime delegation is terminal and never loops', async () => {
|
|
|
964
1060
|
llm: {
|
|
965
1061
|
async completeWithTools() {
|
|
966
1062
|
calls += 1;
|
|
1063
|
+
if (calls === 2) {
|
|
1064
|
+
return {
|
|
1065
|
+
content: 'Je ne peux pas lancer cette action : aucun agent connecté ne la prend actuellement en charge.',
|
|
1066
|
+
message: {
|
|
1067
|
+
role: 'assistant',
|
|
1068
|
+
content: 'Je ne peux pas lancer cette action : aucun agent connecté ne la prend actuellement en charge.',
|
|
1069
|
+
},
|
|
1070
|
+
tool_calls: [],
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
967
1073
|
return {
|
|
968
1074
|
content: null,
|
|
969
1075
|
message: { role: 'assistant', content: null },
|
|
@@ -979,14 +1085,63 @@ test('a rejected runtime delegation is terminal and never loops', async () => {
|
|
|
979
1085
|
|
|
980
1086
|
try {
|
|
981
1087
|
const result = await createAgentGraph().invoke({ input: 'lance ingestion', session });
|
|
982
|
-
assert.equal(calls,
|
|
983
|
-
assert.equal(
|
|
1088
|
+
assert.equal(calls, 2);
|
|
1089
|
+
assert.equal(
|
|
1090
|
+
result.response,
|
|
1091
|
+
'Je ne peux pas lancer cette action : aucun agent connecté ne la prend actuellement en charge.',
|
|
1092
|
+
);
|
|
1093
|
+
assert.doesNotMatch(result.response, /ObjectiveNotOrchestrableError|capabilit|runtime__|[0-9a-f]{8}-/i);
|
|
984
1094
|
assert.equal(result.terminalToolFailure, true);
|
|
985
1095
|
} finally {
|
|
986
1096
|
globalThis.fetch = originalFetch;
|
|
987
1097
|
}
|
|
988
1098
|
});
|
|
989
1099
|
|
|
1100
|
+
test('a delegation missing required provider inputs returns to Donna for clarification', async () => {
|
|
1101
|
+
const originalFetch = globalThis.fetch;
|
|
1102
|
+
globalThis.fetch = async () => ({
|
|
1103
|
+
ok: false,
|
|
1104
|
+
status: 422,
|
|
1105
|
+
json: async () => ({
|
|
1106
|
+
error: 'Delegation requires input: to, subject, body',
|
|
1107
|
+
}),
|
|
1108
|
+
});
|
|
1109
|
+
let calls = 0;
|
|
1110
|
+
const session = sessionBase({
|
|
1111
|
+
runtime: { url: 'http://runtime.test' },
|
|
1112
|
+
llm: {
|
|
1113
|
+
async completeWithTools() {
|
|
1114
|
+
calls += 1;
|
|
1115
|
+
if (calls === 1) {
|
|
1116
|
+
return {
|
|
1117
|
+
content: null,
|
|
1118
|
+
message: { role: 'assistant', content: null },
|
|
1119
|
+
tool_calls: [{
|
|
1120
|
+
id: 'delegate-needs-input',
|
|
1121
|
+
type: 'function',
|
|
1122
|
+
function: { name: 'runtime__delegate', arguments: '{"objective":"envoie un mail"}' },
|
|
1123
|
+
}],
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
content: 'Oui. À qui dois-je écrire, avec quel objet et quel message ?',
|
|
1128
|
+
message: { role: 'assistant', content: 'Oui. À qui dois-je écrire, avec quel objet et quel message ?' },
|
|
1129
|
+
tool_calls: [],
|
|
1130
|
+
};
|
|
1131
|
+
},
|
|
1132
|
+
},
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
try {
|
|
1136
|
+
const result = await createAgentGraph().invoke({ input: 'envoie un mail', session });
|
|
1137
|
+
assert.equal(calls, 2);
|
|
1138
|
+
assert.equal(result.response, 'Oui. À qui dois-je écrire, avec quel objet et quel message ?');
|
|
1139
|
+
assert.equal(result.terminalToolFailure, false);
|
|
1140
|
+
} finally {
|
|
1141
|
+
globalThis.fetch = originalFetch;
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
|
|
990
1145
|
// Guard: the system prompt must never show a connected tool's bare name
|
|
991
1146
|
// outside its qualified server__tool form. Bare mentions are what teach the
|
|
992
1147
|
// model to emit unqualified tool calls (the cme_status incident). The bare
|
|
@@ -1158,8 +1313,10 @@ test('agent graph executes action inputs inside a runtime run instead of asking
|
|
|
1158
1313
|
// returned a canned clarification — "lance l'ingestion" did nothing.
|
|
1159
1314
|
const originalFetch = globalThis.fetch;
|
|
1160
1315
|
let fetchCalls = 0;
|
|
1161
|
-
globalThis.fetch = async () => {
|
|
1162
|
-
|
|
1316
|
+
globalThis.fetch = async (_url, init) => {
|
|
1317
|
+
// Count tool traffic only: the MCP session handshake is transport
|
|
1318
|
+
// plumbing, not an action the user needs to approve or observe.
|
|
1319
|
+
if (JSON.parse(init.body).method === 'tools/call') fetchCalls += 1;
|
|
1163
1320
|
return {
|
|
1164
1321
|
ok: true,
|
|
1165
1322
|
status: 200,
|
|
@@ -12,3 +12,17 @@ test('runtime startup is not blocked by optional Docker image maintenance', asyn
|
|
|
12
12
|
assert.match(runtimeBranch, /await runRuntime\(argv\.slice\(1\), agent\)/);
|
|
13
13
|
assert.doesNotMatch(runtimeBranch, /refreshRunningContainers/);
|
|
14
14
|
});
|
|
15
|
+
|
|
16
|
+
test('shell exit leaves the shared runtime alive and refresh is explicit', async () => {
|
|
17
|
+
const cli = await readFile(new URL('./wiki-manager.js', import.meta.url), 'utf8');
|
|
18
|
+
const tui = await readFile(new URL('../shell/tui.tsx', import.meta.url), 'utf8');
|
|
19
|
+
const bin = await readFile(new URL('../../bin/wiki-manager.js', import.meta.url), 'utf8');
|
|
20
|
+
|
|
21
|
+
assert.doesNotMatch(cli, /await shutdownOwnedRuntime\(runtime/);
|
|
22
|
+
assert.doesNotMatch(tui, /shutdownOwnedRuntime/);
|
|
23
|
+
assert.match(tui, /shell closed; shared runtime left running/);
|
|
24
|
+
assert.match(tui, /process\.exit\(0\)/);
|
|
25
|
+
assert.match(cli, /argv\.includes\('--refresh'\)/);
|
|
26
|
+
assert.match(cli, /spawnSync\(workspaceCliPath, \['refresh'\]/);
|
|
27
|
+
assert.match(bin, /!argv\.includes\('--refresh'\)/);
|
|
28
|
+
});
|