@dotdrelle/wiki-manager 0.11.4 → 0.11.7
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/agents.docker-compose.yml +3 -3
- package/package.json +2 -2
- package/src/agent/graph.js +118 -5
- package/src/agent/graph.test.js +158 -1
- package/src/cli/wiki-manager.js +9 -2
- package/src/commands/slash.js +36 -11
- package/src/core/agentLoop.js +20 -5
- package/src/core/agentLoop.test.js +19 -1
- package/src/core/mcp.js +1 -1
- package/src/core/planPatch.js +39 -1
- package/src/core/planPatch.test.js +23 -1
- package/src/core/profile.js +111 -0
- package/src/core/skills.js +1 -1
- package/src/runtime/client.js +12 -0
- package/src/runtime/donna-contract.test.js +359 -0
- package/src/runtime/runner.js +72 -3
- package/src/runtime/runner.test.js +113 -3
- package/src/runtime/server.js +27 -1
- package/src/runtime/server.test.js +38 -0
- package/src/shell/LeftPane.tsx +2 -1
- package/src/shell/StartupScreen.tsx +118 -23
- package/src/shell/repl.js +95 -42
- package/src/shell/repl.test.js +59 -1
- package/src/shell/tui.tsx +90 -75
- package/src/shell/useAgent.ts +14 -4
- package/src/shell/useSession.ts +92 -17
|
@@ -95,7 +95,7 @@ services:
|
|
|
95
95
|
- MAILER_REQUIRE_CONFIRMATION=${MAILER_REQUIRE_CONFIRMATION:-true}
|
|
96
96
|
- MAILER_DRY_RUN=${MAILER_DRY_RUN:-false}
|
|
97
97
|
- MCP_AUTH_TOKEN=${MAILER_MCP_AUTH_TOKEN:-}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
# Optional: mount a CA bundle and set MAILERSEND_CA_CERT to its container path.
|
|
99
|
+
# volumes:
|
|
100
|
+
# - ${AGENTS_DATA_DIR:-./.agents-data}/certs:/certs:ro
|
|
101
101
|
restart: unless-stopped
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.7",
|
|
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/agent/graph.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/agentEvents.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/documentIntake.test.js src/core/dockerCompose.test.js src/core/wikiWorkspace.test.js src/core/wikirc.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/commands/slash.test.js src/shell/repl.test.js src/runtime/store.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/auth.test.js",
|
|
14
|
+
"test": "node --test src/agent/graph.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/agentEvents.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/documentIntake.test.js src/core/dockerCompose.test.js src/core/wikiWorkspace.test.js src/core/wikirc.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/commands/slash.test.js src/shell/repl.test.js src/runtime/store.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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
1
2
|
import { Annotation, END, START, StateGraph } from '@langchain/langgraph';
|
|
2
3
|
import {
|
|
3
4
|
buildLlmTools,
|
|
@@ -6,14 +7,17 @@ import {
|
|
|
6
7
|
formatMcpToolsForAgent,
|
|
7
8
|
parseToolCallName,
|
|
8
9
|
} from '../core/mcp.js';
|
|
9
|
-
import { formatSkillsForAgent } from '../core/skills.js';
|
|
10
|
+
import { formatSkillsForAgent, readOptionalText } from '../core/skills.js';
|
|
10
11
|
import { handleSlashCommand } from '../commands/slash.js';
|
|
11
12
|
import { extractActivity, formatActivitySummary, parseJsonText } from '../core/activity.js';
|
|
12
13
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
13
14
|
import { enqueueProductionJob, ensureJobQueue, formatQueue, productionLockBusy } from '../core/jobQueue.js';
|
|
15
|
+
import { updateWorkspaceProfilePreference } from '../core/profile.js';
|
|
14
16
|
|
|
15
17
|
const MAX_TOOL_ITERATIONS = 80;
|
|
16
18
|
const MAX_SPINNER_ARG_LENGTH = 96;
|
|
19
|
+
const MAX_PROFILE_CHARS = 4000;
|
|
20
|
+
|
|
17
21
|
const AGENT_SLASH_COMMANDS = new Set([
|
|
18
22
|
'help',
|
|
19
23
|
'version',
|
|
@@ -51,6 +55,51 @@ const SHELL_RUN_COMMAND_TOOL = {
|
|
|
51
55
|
},
|
|
52
56
|
};
|
|
53
57
|
|
|
58
|
+
const SHELL_READ_COMMAND_TOOL = {
|
|
59
|
+
type: 'function',
|
|
60
|
+
function: {
|
|
61
|
+
name: 'shell__read_command',
|
|
62
|
+
description: [
|
|
63
|
+
'Run a read-only deterministic wiki-manager slash command inside the current shell session.',
|
|
64
|
+
'Allowed commands: /help, /version, /config, /config list, /config status, /status, /services, /skills, /skills list, /skills show <name>, /uploads, /uploads list, /queue.',
|
|
65
|
+
'Do not use for workspace creation/deletion, uploads conversion, service start/stop, MCP calls, wiki runs, or any mutation.',
|
|
66
|
+
].join(' '),
|
|
67
|
+
parameters: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
properties: {
|
|
71
|
+
command: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
description: 'Read-only slash command to run, for example "/status", "/config status", or "/services".',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
required: ['command'],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const SHELL_PROFILE_UPDATE_TOOL = {
|
|
82
|
+
type: 'function',
|
|
83
|
+
function: {
|
|
84
|
+
name: 'shell__profile_update',
|
|
85
|
+
description: [
|
|
86
|
+
'Append one explicit durable user preference to the current workspace .wiki/profile.md.',
|
|
87
|
+
'Use when the user explicitly asks to remember, persist, note, or update profile information and wiki__profile_update is not available.',
|
|
88
|
+
].join(' '),
|
|
89
|
+
parameters: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
additionalProperties: false,
|
|
92
|
+
properties: {
|
|
93
|
+
preference: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'The durable preference to append, without Markdown bullet syntax.',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
required: ['preference'],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
54
103
|
const WIKI_PLAN_SET_TOOL = {
|
|
55
104
|
type: 'function',
|
|
56
105
|
function: {
|
|
@@ -238,6 +287,24 @@ function assertAgentSlashCommandAllowed(commandLine) {
|
|
|
238
287
|
}
|
|
239
288
|
}
|
|
240
289
|
|
|
290
|
+
function assertAgentReadSlashCommandAllowed(commandLine) {
|
|
291
|
+
const parts = commandLine.slice(1).trim().split(/\s+/).filter(Boolean);
|
|
292
|
+
const command = parts[0] ?? '';
|
|
293
|
+
const subcommand = parts[1] ?? '';
|
|
294
|
+
const allowed =
|
|
295
|
+
command === 'help' ||
|
|
296
|
+
command === 'version' ||
|
|
297
|
+
command === 'status' ||
|
|
298
|
+
command === 'services' ||
|
|
299
|
+
command === 'queue' ||
|
|
300
|
+
(command === 'config' && ['', 'list', 'status'].includes(subcommand)) ||
|
|
301
|
+
(command === 'skills' && ['', 'list', 'show'].includes(subcommand)) ||
|
|
302
|
+
(command === 'uploads' && ['', 'list'].includes(subcommand));
|
|
303
|
+
if (!allowed) {
|
|
304
|
+
throw new Error(`Read-only command is not available to the agent: /${parts.join(' ')}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
241
308
|
function withActiveWorkspaceForExternalTool(session, server, tool, args) {
|
|
242
309
|
const needsWorkspace =
|
|
243
310
|
(server === 'documents' && tool.startsWith('documents_') && tool !== 'documents_status') ||
|
|
@@ -269,6 +336,21 @@ async function runShellCommandTool(session, commandLine) {
|
|
|
269
336
|
return result.output ?? 'Command completed.';
|
|
270
337
|
}
|
|
271
338
|
|
|
339
|
+
async function runShellReadCommandTool(session, commandLine) {
|
|
340
|
+
const command = normalizeShellCommand(commandLine);
|
|
341
|
+
assertAgentReadSlashCommandAllowed(command);
|
|
342
|
+
session._onStep?.(`Shell: ${command}`);
|
|
343
|
+
const result = await handleSlashCommand(command, {
|
|
344
|
+
packageJson: session.packageJson ?? { version: '0.0.0' },
|
|
345
|
+
session,
|
|
346
|
+
onStep: session._onStep,
|
|
347
|
+
});
|
|
348
|
+
if (result.exit) {
|
|
349
|
+
throw new Error('/exit is not available to the agent.');
|
|
350
|
+
}
|
|
351
|
+
return result.output ?? 'Command completed.';
|
|
352
|
+
}
|
|
353
|
+
|
|
272
354
|
function rememberProductionProgress(session, payload, label) {
|
|
273
355
|
const job = payload?.job;
|
|
274
356
|
const jobId = payload?.jobId ?? job?.jobId;
|
|
@@ -423,6 +505,18 @@ function selectExecutorForStep(description, session) {
|
|
|
423
505
|
return fallback;
|
|
424
506
|
}
|
|
425
507
|
|
|
508
|
+
// The manager runs on the same host filesystem as the workspace directory
|
|
509
|
+
// (this is the same local file wiki__profile_update writes to via its
|
|
510
|
+
// volume-mounted container), so read it fresh on every turn instead of
|
|
511
|
+
// relying on the model proactively calling wiki__profile_read — profile
|
|
512
|
+
// content (tutoiement, formatting preferences, etc.) is meant to shape every
|
|
513
|
+
// reply, not just ones where the model happens to think to check it.
|
|
514
|
+
function loadWorkspaceProfile(workspacePath) {
|
|
515
|
+
if (!workspacePath) return null;
|
|
516
|
+
const content = readOptionalText(join(workspacePath, '.wiki', 'profile.md'));
|
|
517
|
+
return content ? content.slice(0, MAX_PROFILE_CHARS) : null;
|
|
518
|
+
}
|
|
519
|
+
|
|
426
520
|
export function buildAgentSystemPrompt(state) {
|
|
427
521
|
const workspace = state.session.workspace ?? 'no workspace selected';
|
|
428
522
|
const wikirc = state.session.wikirc?.profile ?? 'no profile loaded';
|
|
@@ -430,10 +524,12 @@ export function buildAgentSystemPrompt(state) {
|
|
|
430
524
|
const mcpTools = formatMcpToolsForAgent(state.session.mcp);
|
|
431
525
|
const skills = formatSkillsForAgent(state.session);
|
|
432
526
|
const customPrompt = state.session.systemPrompt ?? null;
|
|
527
|
+
const workspaceProfile = loadWorkspaceProfile(state.session.workspacePath);
|
|
433
528
|
|
|
434
529
|
const agentContext = [
|
|
435
530
|
'You are Donna, the terminal orchestrator agent for llm-wiki-manager.',
|
|
436
531
|
'The shell is agent-first: every input without a leading slash is routed to you.',
|
|
532
|
+
'Default to a plain conversational reply with no tool call. Only call a tool, create a plan, or start a job when the user\'s message clearly requests an action (ingest, build, export, configure, run a skill, check a concrete status, etc.). Greetings, small talk, thanks, and general questions do not warrant starting a job or calling a tool — just answer in text.',
|
|
437
533
|
'Commands starting with / are deterministic primitives. You may run a safe subset through shell__run_command.',
|
|
438
534
|
`Reply language: ${language}.`,
|
|
439
535
|
`Current workspace: ${workspace}.`,
|
|
@@ -488,7 +584,11 @@ export function buildAgentSystemPrompt(state) {
|
|
|
488
584
|
'If production_start_job is returned as queued/waiting by the manager, report that it is waiting in the local queue and return control. Do not continue as if the production job has started.',
|
|
489
585
|
'For diagnostics, use /wiki run doctor when the user asks for doctor. Use /workspace init <name> [path] for low-level non-interactive workspace creation. In the interactive TUI, /new <name> opens the setup wizard. Use /wiki for index, or /wiki run index through the explicit backup hatch. Use /wiki run init only for explicit current-workspace llm-wiki init.',
|
|
490
586
|
'If an action requires tools or skills not available yet, explain the limitation and name the expected primitive.',
|
|
491
|
-
|
|
587
|
+
workspaceProfile
|
|
588
|
+
? `Workspace profile (.wiki/profile.md) — durable user preferences, apply these to every reply (tone, tutoiement/vouvoiement, formatting, etc.):\n${workspaceProfile}`
|
|
589
|
+
: null,
|
|
590
|
+
'When the user explicitly asks you to remember, persist, or update durable preference/profile information, call wiki__profile_update when it is available; otherwise call shell__profile_update. Do not just acknowledge in text without calling a profile update tool.',
|
|
591
|
+
].filter(Boolean).join('\n');
|
|
492
592
|
|
|
493
593
|
return customPrompt ? `${customPrompt}\n\n${agentContext}` : agentContext;
|
|
494
594
|
}
|
|
@@ -512,12 +612,17 @@ export function buildLimitedAgentResponse(state, reason = 'no workspace loaded w
|
|
|
512
612
|
].join('\n');
|
|
513
613
|
}
|
|
514
614
|
|
|
615
|
+
export function formatLlmUnavailableMessage(reason) {
|
|
616
|
+
const clean = String(reason ?? 'raison inconnue').replace(/\s+/g, ' ').trim();
|
|
617
|
+
return `⚠ LLM injoignable : ${clean || 'raison inconnue'}`;
|
|
618
|
+
}
|
|
619
|
+
|
|
515
620
|
export function createAgentGraph(options = {}) {
|
|
516
621
|
async function orchestratorNode(state) {
|
|
517
622
|
const llm = state.session.llm ?? options.llm ?? null;
|
|
518
623
|
|
|
519
624
|
if (!llm) {
|
|
520
|
-
return { response:
|
|
625
|
+
return { response: formatLlmUnavailableMessage('aucun client LLM configure'), pendingToolCalls: null, readyToStream: false };
|
|
521
626
|
}
|
|
522
627
|
|
|
523
628
|
const iterations = state.toolIterations ?? 0;
|
|
@@ -535,12 +640,15 @@ export function createAgentGraph(options = {}) {
|
|
|
535
640
|
state.session._onStep?.('Agent: planning next action…');
|
|
536
641
|
}
|
|
537
642
|
|
|
538
|
-
const
|
|
643
|
+
const allTools = [
|
|
539
644
|
SHELL_RUN_COMMAND_TOOL,
|
|
645
|
+
SHELL_READ_COMMAND_TOOL,
|
|
646
|
+
SHELL_PROFILE_UPDATE_TOOL,
|
|
540
647
|
WIKI_PLAN_SET_TOOL,
|
|
541
648
|
WIKI_PLAN_DONE_TOOL,
|
|
542
649
|
...buildLlmTools(state.session.mcp),
|
|
543
650
|
];
|
|
651
|
+
const tools = allTools;
|
|
544
652
|
const system = buildAgentSystemPrompt(state);
|
|
545
653
|
|
|
546
654
|
// On iteration 0: prior history is in state.messages, user input must be appended.
|
|
@@ -619,7 +727,7 @@ export function createAgentGraph(options = {}) {
|
|
|
619
727
|
} catch (err) {
|
|
620
728
|
if (err.name === 'AbortError') throw err;
|
|
621
729
|
const message = err instanceof Error ? err.message : String(err);
|
|
622
|
-
return { response:
|
|
730
|
+
return { response: formatLlmUnavailableMessage(message), pendingToolCalls: null, readyToStream: false };
|
|
623
731
|
}
|
|
624
732
|
}
|
|
625
733
|
|
|
@@ -663,6 +771,11 @@ export function createAgentGraph(options = {}) {
|
|
|
663
771
|
} else if (server === 'shell' && tool === 'run_command') {
|
|
664
772
|
await awaitRunApproval(state.session, { runId, tool: toolName });
|
|
665
773
|
resultText = await runShellCommandTool(state.session, args.command);
|
|
774
|
+
} else if (server === 'shell' && tool === 'read_command') {
|
|
775
|
+
resultText = await runShellReadCommandTool(state.session, args.command);
|
|
776
|
+
} else if (server === 'shell' && tool === 'profile_update') {
|
|
777
|
+
const result = await updateWorkspaceProfilePreference(state.session, args.preference);
|
|
778
|
+
resultText = JSON.stringify(result, null, 2);
|
|
666
779
|
} else if (server !== 'shell') {
|
|
667
780
|
await awaitRunApproval(state.session, { runId, tool: toolName });
|
|
668
781
|
await awaitToolApproval(state.session, {
|
package/src/agent/graph.test.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
|
-
import {
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { buildAgentSystemPrompt, createAgentGraph } from './graph.js';
|
|
4
7
|
|
|
5
8
|
function sessionBase(overrides = {}) {
|
|
6
9
|
return {
|
|
@@ -99,6 +102,160 @@ test('agent graph waits for run-level approval before first MCP action', async (
|
|
|
99
102
|
}
|
|
100
103
|
});
|
|
101
104
|
|
|
105
|
+
test('agent graph reports LLM unavailable without Donna active boilerplate', async () => {
|
|
106
|
+
const agent = createAgentGraph();
|
|
107
|
+
const result = await agent.invoke({ input: 'salut', session: sessionBase({ llm: null }) });
|
|
108
|
+
|
|
109
|
+
assert.equal(result.response, '⚠ LLM injoignable : aucun client LLM configure');
|
|
110
|
+
assert.doesNotMatch(result.response, /Donna is active/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('agent graph binds the full toolset and lets Donna decide whether to call tools', async () => {
|
|
114
|
+
const seenTools = [];
|
|
115
|
+
const session = sessionBase({
|
|
116
|
+
llm: {
|
|
117
|
+
async completeWithTools({ tools }) {
|
|
118
|
+
seenTools.push(...tools.map((tool) => tool.function.name));
|
|
119
|
+
return {
|
|
120
|
+
content: 'Salut, je suis là.',
|
|
121
|
+
message: { role: 'assistant', content: 'Salut, je suis là.' },
|
|
122
|
+
tool_calls: null,
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const agent = createAgentGraph();
|
|
129
|
+
const result = await agent.invoke({ input: 'salut', session });
|
|
130
|
+
|
|
131
|
+
assert.equal(result.response, 'Salut, je suis là.');
|
|
132
|
+
assert.ok(seenTools.length > 0);
|
|
133
|
+
assert.ok(seenTools.includes('shell__read_command'));
|
|
134
|
+
assert.ok(seenTools.includes('shell__run_command'));
|
|
135
|
+
assert.ok(seenTools.includes('shell__profile_update'));
|
|
136
|
+
assert.ok(seenTools.includes('wiki__plan_set'));
|
|
137
|
+
assert.ok(seenTools.includes('wiki__plan_done'));
|
|
138
|
+
assert.equal(session.headlessPlan ?? null, null);
|
|
139
|
+
assert.equal(Object.keys(session.activities ?? {}).length, 0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('agent graph does not pre-filter mutating MCP tools for config questions', async () => {
|
|
143
|
+
const seenTools = [];
|
|
144
|
+
const session = sessionBase({
|
|
145
|
+
mcp: {
|
|
146
|
+
production: {
|
|
147
|
+
status: 'connected',
|
|
148
|
+
url: 'http://127.0.0.1:3000/mcp/',
|
|
149
|
+
tools: [
|
|
150
|
+
{
|
|
151
|
+
name: 'production_start_job',
|
|
152
|
+
description: 'Start production job',
|
|
153
|
+
inputSchema: { type: 'object', properties: { type: { type: 'string' } } },
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'production_job_status',
|
|
157
|
+
description: 'Read production job status',
|
|
158
|
+
inputSchema: { type: 'object', properties: { jobId: { type: 'string' } } },
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
llm: {
|
|
164
|
+
async completeWithTools({ tools }) {
|
|
165
|
+
seenTools.push(...tools.map((tool) => tool.function.name));
|
|
166
|
+
return {
|
|
167
|
+
content: 'Le profil actif est docs.',
|
|
168
|
+
message: { role: 'assistant', content: 'Le profil actif est docs.' },
|
|
169
|
+
tool_calls: null,
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const agent = createAgentGraph();
|
|
176
|
+
const result = await agent.invoke({ input: 'quel est le profil actif ?', session });
|
|
177
|
+
|
|
178
|
+
assert.equal(result.response, 'Le profil actif est docs.');
|
|
179
|
+
assert.ok(seenTools.includes('shell__read_command'));
|
|
180
|
+
assert.ok(seenTools.includes('production__production_job_status'));
|
|
181
|
+
assert.ok(seenTools.includes('wiki__plan_set'));
|
|
182
|
+
assert.ok(seenTools.includes('production__production_start_job'));
|
|
183
|
+
assert.ok(seenTools.includes('shell__run_command'));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('agent graph binds the full toolset for a "remember my preference" request, not just read-only tools', async () => {
|
|
187
|
+
const seenTools = [];
|
|
188
|
+
const session = sessionBase({
|
|
189
|
+
mcp: {
|
|
190
|
+
wiki: {
|
|
191
|
+
status: 'connected',
|
|
192
|
+
url: 'http://127.0.0.1:3001/mcp/',
|
|
193
|
+
tools: [
|
|
194
|
+
{
|
|
195
|
+
name: 'profile_read',
|
|
196
|
+
description: 'Read the workspace profile from .wiki/profile.md.',
|
|
197
|
+
inputSchema: { type: 'object', properties: {} },
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'profile_update',
|
|
201
|
+
description: 'Write the workspace profile to .wiki/profile.md.',
|
|
202
|
+
inputSchema: { type: 'object', properties: { content: { type: 'string' } } },
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
llm: {
|
|
208
|
+
async completeWithTools({ tools }) {
|
|
209
|
+
seenTools.push(...tools.map((tool) => tool.function.name));
|
|
210
|
+
return {
|
|
211
|
+
content: 'Noté, je retiens cette préférence.',
|
|
212
|
+
message: { role: 'assistant', content: 'Noté, je retiens cette préférence.' },
|
|
213
|
+
tool_calls: null,
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const agent = createAgentGraph();
|
|
220
|
+
const result = await agent.invoke({ input: 'retiens que je préfère des réponses courtes', session });
|
|
221
|
+
|
|
222
|
+
assert.equal(result.response, 'Noté, je retiens cette préférence.');
|
|
223
|
+
// profile_update is a write tool (doesn't match the read-only name pattern)
|
|
224
|
+
// and "retiens" doesn't appear in the config/status read-only phrasing —
|
|
225
|
+
// without action-intent coverage for remember/save/update requests, this
|
|
226
|
+
// tool would silently never be offered to the LLM at all.
|
|
227
|
+
assert.ok(seenTools.includes('wiki__profile_update'));
|
|
228
|
+
assert.ok(seenTools.includes('wiki__profile_read'));
|
|
229
|
+
assert.ok(seenTools.includes('shell__profile_update'));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('buildAgentSystemPrompt includes .wiki/profile.md content so preferences apply without a tool call', () => {
|
|
233
|
+
const workspacePath = mkdtempSync(join(tmpdir(), 'donna-profile-'));
|
|
234
|
+
mkdirSync(join(workspacePath, '.wiki'), { recursive: true });
|
|
235
|
+
writeFileSync(
|
|
236
|
+
join(workspacePath, '.wiki', 'profile.md'),
|
|
237
|
+
'# Workspace Profile\n\n## User Preferences\n\n- Tutoiement : me tutoyer\n',
|
|
238
|
+
);
|
|
239
|
+
try {
|
|
240
|
+
const prompt = buildAgentSystemPrompt({
|
|
241
|
+
session: sessionBase({ workspacePath }),
|
|
242
|
+
});
|
|
243
|
+
assert.match(prompt, /Tutoiement : me tutoyer/);
|
|
244
|
+
} finally {
|
|
245
|
+
rmSync(workspacePath, { recursive: true, force: true });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('buildAgentSystemPrompt omits the profile section when profile.md is missing or empty', () => {
|
|
250
|
+
const workspacePath = mkdtempSync(join(tmpdir(), 'donna-profile-empty-'));
|
|
251
|
+
try {
|
|
252
|
+
const prompt = buildAgentSystemPrompt({ session: sessionBase({ workspacePath }) });
|
|
253
|
+
assert.doesNotMatch(prompt, /Workspace profile \(\.wiki\/profile\.md\)/);
|
|
254
|
+
} finally {
|
|
255
|
+
rmSync(workspacePath, { recursive: true, force: true });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
102
259
|
test('agent graph waits for tool-level approval configured on endpoint', async () => {
|
|
103
260
|
const originalFetch = globalThis.fetch;
|
|
104
261
|
globalThis.fetch = async () => ({
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -29,6 +29,11 @@ function valueAfter(argv, flag) {
|
|
|
29
29
|
return argv[index + 1];
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function unavailableRuntime(err) {
|
|
33
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
34
|
+
return { url: null, error: reason };
|
|
35
|
+
}
|
|
36
|
+
|
|
32
37
|
function createSession() {
|
|
33
38
|
return {
|
|
34
39
|
workspace: null,
|
|
@@ -745,7 +750,8 @@ export async function runCli(argv) {
|
|
|
745
750
|
const { ensureRuntime } = await import('../runtime/lifecycle.js');
|
|
746
751
|
runtime = await ensureRuntime();
|
|
747
752
|
} catch (err) {
|
|
748
|
-
|
|
753
|
+
runtime = unavailableRuntime(err);
|
|
754
|
+
console.error(`Runtime unavailable: ${runtime.error}`);
|
|
749
755
|
}
|
|
750
756
|
await runOpenTuiShell({ agent, packageJson, runtime });
|
|
751
757
|
return;
|
|
@@ -757,7 +763,8 @@ export async function runCli(argv) {
|
|
|
757
763
|
const { ensureRuntime } = await import('../runtime/lifecycle.js');
|
|
758
764
|
runtime = await ensureRuntime();
|
|
759
765
|
} catch (err) {
|
|
760
|
-
|
|
766
|
+
runtime = unavailableRuntime(err);
|
|
767
|
+
console.error(`Runtime unavailable: ${runtime.error}`);
|
|
761
768
|
}
|
|
762
769
|
}
|
|
763
770
|
await runShell({ agent, packageJson, runtime });
|
package/src/commands/slash.js
CHANGED
|
@@ -655,15 +655,38 @@ export function printHelp(packageJson) {
|
|
|
655
655
|
console.log(helpText(packageJson));
|
|
656
656
|
}
|
|
657
657
|
|
|
658
|
+
function rawCommandAgentPrompt(command, output) {
|
|
659
|
+
return [
|
|
660
|
+
`L'utilisateur a lancé la commande shell ${command}.`,
|
|
661
|
+
'Voici la sortie brute collectée par la commande déterministe. Ne relance pas la commande et ne modifie pas les données.',
|
|
662
|
+
'Réponds à l’utilisateur à partir de ces faits, en appliquant le profil workspace et les préférences de présentation déjà chargés dans ton prompt système.',
|
|
663
|
+
'',
|
|
664
|
+
'Sortie brute:',
|
|
665
|
+
'```text',
|
|
666
|
+
output || '(empty)',
|
|
667
|
+
'```',
|
|
668
|
+
].join('\n');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function rawCommandResult(command, output) {
|
|
672
|
+
return {
|
|
673
|
+
output,
|
|
674
|
+
rawOutput: true,
|
|
675
|
+
agentTrigger: rawCommandAgentPrompt(command, output),
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
658
679
|
export async function handleSlashCommand(line, context) {
|
|
659
680
|
const args = line.slice(1).trim().split(/\s+/).filter(Boolean);
|
|
660
681
|
const [command] = args;
|
|
661
682
|
const step = context.onStep ?? (() => {});
|
|
662
|
-
const runAgentCommand = async (fn, verb) => {
|
|
683
|
+
const runAgentCommand = async (fn, verb, commandLabel) => {
|
|
663
684
|
try {
|
|
664
685
|
step(`Agents: ${verb}ing external agents…`);
|
|
665
686
|
const output = await fn();
|
|
666
|
-
return
|
|
687
|
+
return output
|
|
688
|
+
? rawCommandResult(commandLabel, output)
|
|
689
|
+
: { output: `Agents ${verb}ed.` };
|
|
667
690
|
} catch (err) {
|
|
668
691
|
step(formatActivityError('agents', verb, err));
|
|
669
692
|
return { output: err instanceof Error ? err.message : String(err) };
|
|
@@ -808,7 +831,8 @@ export async function handleSlashCommand(line, context) {
|
|
|
808
831
|
try {
|
|
809
832
|
step('Services: reading compose state…');
|
|
810
833
|
await refreshMcpRuntimeStatus(context.session);
|
|
811
|
-
|
|
834
|
+
const output = await listServices(context.session);
|
|
835
|
+
return rawCommandResult('/services', output);
|
|
812
836
|
} catch (err) {
|
|
813
837
|
const message = err instanceof Error ? err.message : String(err);
|
|
814
838
|
step(formatActivityError('services', 'list', err));
|
|
@@ -821,13 +845,13 @@ export async function handleSlashCommand(line, context) {
|
|
|
821
845
|
// do not remap it to undefined, that bypasses any custom "all" target list and always
|
|
822
846
|
// falls back to the hardcoded COMPOSE_SERVICES constant instead.
|
|
823
847
|
const service = args[1];
|
|
824
|
-
if (service === 'agents') return runAgentCommand(startAgents, 'start');
|
|
848
|
+
if (service === 'agents') return runAgentCommand(startAgents, 'start', '/start agents');
|
|
825
849
|
try {
|
|
826
850
|
step(`Services: starting ${service ?? 'workspace services'}…`);
|
|
827
851
|
const output = await startService(context.session, service);
|
|
828
852
|
step('Services: refreshing MCP runtime…');
|
|
829
853
|
await refreshMcpRuntimeStatus(context.session);
|
|
830
|
-
return {
|
|
854
|
+
return rawCommandResult(`/start${service ? ` ${service}` : ''}`, output);
|
|
831
855
|
} catch (err) {
|
|
832
856
|
const message = err instanceof Error ? err.message : String(err);
|
|
833
857
|
step(formatActivityError('services', 'stop', err));
|
|
@@ -836,13 +860,13 @@ export async function handleSlashCommand(line, context) {
|
|
|
836
860
|
}
|
|
837
861
|
case 'stop': {
|
|
838
862
|
const service = args[1];
|
|
839
|
-
if (service === 'agents') return runAgentCommand(stopAgents, 'stop');
|
|
863
|
+
if (service === 'agents') return runAgentCommand(stopAgents, 'stop', '/stop agents');
|
|
840
864
|
try {
|
|
841
865
|
step(`Services: stopping ${service ?? 'workspace services'}…`);
|
|
842
866
|
const output = await stopService(context.session, service);
|
|
843
867
|
step('Services: refreshing MCP runtime…');
|
|
844
868
|
await refreshMcpRuntimeStatus(context.session);
|
|
845
|
-
return {
|
|
869
|
+
return rawCommandResult(`/stop${service ? ` ${service}` : ''}`, output);
|
|
846
870
|
} catch (err) {
|
|
847
871
|
const message = err instanceof Error ? err.message : String(err);
|
|
848
872
|
step(formatActivityError('services', 'logs', err));
|
|
@@ -854,7 +878,8 @@ export async function handleSlashCommand(line, context) {
|
|
|
854
878
|
const tail = args[2] ? Number(args[2]) : 120;
|
|
855
879
|
try {
|
|
856
880
|
step(`Services: reading logs for ${service ?? 'service'}…`);
|
|
857
|
-
|
|
881
|
+
const output = await serviceLogs(context.session, service, { tail });
|
|
882
|
+
return rawCommandResult(`/logs ${[service, args[2]].filter(Boolean).join(' ')}`.trim(), output);
|
|
858
883
|
} catch (err) {
|
|
859
884
|
const message = err instanceof Error ? err.message : String(err);
|
|
860
885
|
return { output: message };
|
|
@@ -903,7 +928,7 @@ export async function handleSlashCommand(line, context) {
|
|
|
903
928
|
}
|
|
904
929
|
const activity = formatMcpCallActivity(serverName, toolName, output);
|
|
905
930
|
if (activity) step(activity);
|
|
906
|
-
return { output
|
|
931
|
+
return rawCommandResult(`/mcp call ${serverName} ${toolName}`, output);
|
|
907
932
|
} catch (err) {
|
|
908
933
|
const message = err instanceof Error ? err.message : String(err);
|
|
909
934
|
step(formatActivityError(serverName, toolName, err));
|
|
@@ -1056,7 +1081,7 @@ export async function handleSlashCommand(line, context) {
|
|
|
1056
1081
|
});
|
|
1057
1082
|
const activity = formatActivitySummary('wiki', 'index', output);
|
|
1058
1083
|
if (activity) step(activity);
|
|
1059
|
-
return
|
|
1084
|
+
return rawCommandResult('/wiki', output);
|
|
1060
1085
|
} catch (err) {
|
|
1061
1086
|
const message = err instanceof Error ? err.message : String(err);
|
|
1062
1087
|
step(formatActivityError('wiki', 'index', err));
|
|
@@ -1073,7 +1098,7 @@ export async function handleSlashCommand(line, context) {
|
|
|
1073
1098
|
});
|
|
1074
1099
|
const activity = formatActivitySummary('wiki', wikiArgs[0] ?? 'run', output);
|
|
1075
1100
|
if (activity) step(activity);
|
|
1076
|
-
return { output
|
|
1101
|
+
return rawCommandResult(`/wiki run ${wikiArgs.join(' ')}`, output);
|
|
1077
1102
|
}
|
|
1078
1103
|
return {
|
|
1079
1104
|
output: [
|
package/src/core/agentLoop.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { buildAgentSystemPrompt,
|
|
1
|
+
import { buildAgentSystemPrompt, formatLlmUnavailableMessage } from '../agent/graph.js';
|
|
2
2
|
import { createAgentEvent, dispatchAgentEvent } from './agentEvents.js';
|
|
3
3
|
import { activitySnapshot, newNonTerminalActivities } from './activity.js';
|
|
4
4
|
import { extractHeadlessPlan, formatCompletedActivities, formatPlanStatus } from './plan.js';
|
|
5
|
-
import { formatReadyTaskPrompt, nextReadyPlanTask, readyPlanTasks } from './planPatch.js';
|
|
5
|
+
import { formatReadyTaskPrompt, nextReadyPlanTask, readyPlanTasks, sanitizePlanForExecution } from './planPatch.js';
|
|
6
6
|
|
|
7
7
|
export function abortError(message = 'Agent run cancelled.') {
|
|
8
8
|
const err = new Error(message);
|
|
@@ -31,7 +31,7 @@ export async function runAgentTurn(agent, session, input, {
|
|
|
31
31
|
if (session._abortSignal === signal) delete session._abortSignal;
|
|
32
32
|
}
|
|
33
33
|
if (result.streamedInline) {
|
|
34
|
-
return streamedContent.trim() ||
|
|
34
|
+
return streamedContent.trim() || formatLlmUnavailableMessage('flux vide');
|
|
35
35
|
}
|
|
36
36
|
if (result.response != null) return result.response;
|
|
37
37
|
if (result.readyToStream && session.llm?.stream) {
|
|
@@ -44,9 +44,9 @@ export async function runAgentTurn(agent, session, input, {
|
|
|
44
44
|
})) {
|
|
45
45
|
content += delta;
|
|
46
46
|
}
|
|
47
|
-
return content.trim() ||
|
|
47
|
+
return content.trim() || formatLlmUnavailableMessage('flux vide');
|
|
48
48
|
}
|
|
49
|
-
return
|
|
49
|
+
return formatLlmUnavailableMessage('reponse vide');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export async function runAgenticLoop(agent, session, initialInput, {
|
|
@@ -109,6 +109,7 @@ export async function runAgenticLoop(agent, session, initialInput, {
|
|
|
109
109
|
onPlanAlreadySet?.({ steps: session.headlessPlan });
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
+
sanitizeSessionPlan(session, { runId });
|
|
112
113
|
|
|
113
114
|
const newPending = newNonTerminalActivities(snapshot, session);
|
|
114
115
|
if (newPending.length === 0) {
|
|
@@ -158,6 +159,20 @@ export async function runAgenticLoop(agent, session, initialInput, {
|
|
|
158
159
|
return { ok: false, maxTurns: true };
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
function sanitizeSessionPlan(session, { runId = null } = {}) {
|
|
163
|
+
if (!session.headlessPlan) return;
|
|
164
|
+
const sanitized = sanitizePlanForExecution(session.headlessPlan);
|
|
165
|
+
if (sanitized.warnings.length === 0) return;
|
|
166
|
+
session.headlessPlan = sanitized.plan;
|
|
167
|
+
dispatchAgentEvent(session, createAgentEvent('runtime_log', {
|
|
168
|
+
origin: 'runtime',
|
|
169
|
+
runId,
|
|
170
|
+
payload: {
|
|
171
|
+
message: `plan warning: ${sanitized.warnings.join('; ')}`,
|
|
172
|
+
},
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
|
|
161
176
|
function pendingStepsPrompt(initialInput, plan, readyTask) {
|
|
162
177
|
return [
|
|
163
178
|
'Original task:',
|
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
3
|
import { createAgentEvent, dispatchAgentEvent } from './agentEvents.js';
|
|
4
|
-
import { runAgenticLoop } from './agentLoop.js';
|
|
4
|
+
import { runAgentTurn, runAgenticLoop } from './agentLoop.js';
|
|
5
|
+
|
|
6
|
+
test('runAgentTurn returns a one-line LLM error on empty stream', async () => {
|
|
7
|
+
const session = {
|
|
8
|
+
commands: [],
|
|
9
|
+
llm: {
|
|
10
|
+
async *stream() {},
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
const agent = {
|
|
14
|
+
async invoke() {
|
|
15
|
+
return { readyToStream: true, streamContext: { messages: [] } };
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const response = await runAgentTurn(agent, session, 'salut');
|
|
20
|
+
|
|
21
|
+
assert.equal(response, '⚠ LLM injoignable : flux vide');
|
|
22
|
+
});
|
|
5
23
|
|
|
6
24
|
test('runAgenticLoop waits for new activities and continues with a completion summary', async () => {
|
|
7
25
|
const session = {
|
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.11.
|
|
4
|
+
const WIKI_MANAGER_VERSION = '0.11.7';
|
|
5
5
|
|
|
6
6
|
function envValue(key) {
|
|
7
7
|
const filePath = managerEnvFile();
|