@dotdrelle/wiki-manager 0.15.101 → 0.16.0
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/mcp.endpoints.example.json +1 -1
- package/package.json +2 -2
- package/src/agent/graph.js +63 -13
- package/src/agent/graph.test.js +125 -1
- package/src/agent/llm.js +13 -4
- package/src/agent/llm.test.js +59 -0
- package/src/cli/wiki-manager.js +43 -3
- package/src/commands/slash.js +2 -0
- package/src/core/agentEvents.js +12 -2
- package/src/core/buildInfo.json +2 -2
- package/src/core/env.js +11 -2
- package/src/core/env.test.js +22 -6
- package/src/core/llmCapabilities.js +31 -0
- package/src/core/llmCapabilities.test.js +27 -0
- package/src/core/logLabel.js +9 -0
- package/src/core/logLabel.test.js +12 -0
- package/src/core/mcp.js +2 -2
- package/src/core/toolLoop.js +222 -20
- package/src/core/toolLoop.test.js +324 -0
- package/src/core/wikiPresearch.js +58 -0
- package/src/core/wikirc.js +61 -0
- package/src/core/wikirc.test.js +40 -1
- package/src/core/workflow.js +4 -1
- package/src/orchestrator/attemptManager.js +21 -5
- package/src/orchestrator/attemptManager.test.js +19 -0
- package/src/orchestrator/dispatcher.js +49 -8
- package/src/orchestrator/dispatcher.test.js +33 -1
- package/src/orchestrator/lockManager.js +40 -5
- package/src/orchestrator/resultAggregator.js +12 -1
- package/src/orchestrator/resultAggregator.test.js +29 -0
- package/src/runtime/controlClassify.test.js +85 -1
- package/src/runtime/conversationCompact.js +39 -0
- package/src/runtime/conversationCompaction.test.js +72 -0
- package/src/runtime/runner.e2e.test.js +49 -0
- package/src/runtime/runner.js +65 -1
- package/src/runtime/server.js +83 -75
- package/src/runtime/server.test.js +121 -0
- package/src/runtime/store.js +17 -1
- package/src/runtime/store.test.js +22 -0
- package/src/runtime/workspaceIsolation.test.js +21 -12
- package/src/shell/repl.js +148 -27
- package/src/shell/repl.test.js +182 -1
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"chatAccess": {
|
|
30
30
|
"maxToolIterations": 8,
|
|
31
31
|
"servers": {
|
|
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", "wiki_outline", "template_read", "
|
|
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", "wiki_outline", "template_read", "wiki_read_deliverable", "wiki_graph_query", "wiki_graph_path"] },
|
|
33
33
|
"wiki-production": { "allow": ["production_job_status", "production_jobs_list"] },
|
|
34
34
|
"cme": { "allow": ["cme_status", "cme_sources_list", "cme_export_status", "cme_confluence_search", "cme_wiki_search"] }
|
|
35
35
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.00",
|
|
4
4
|
"description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"start": "bun ./bin/wiki-manager.js",
|
|
22
|
-
"test": "node --test src/core/skillInvocation.test.js src/core/skillCompiler.test.js src/runtime/skillRun.test.js src/runtime/controlDrain.test.js src/runtime/controlCancellation.test.js src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/agent/skillRecursion.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/profileServiceStatus.test.js src/core/workspaceProfile.test.js src/core/buildInfo.test.js src/core/testGate.test.js src/core/agentEvents.test.js src/core/skillChainView.test.js src/core/runtimeLog.test.js src/core/runtimeEventAdapter.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/mcpEndpoints.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/otherWorkspacesRunning.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikiWorkspaceStart.test.js src/core/wikirc.test.js src/core/workspaceInherit.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/currentArtifact.test.js src/core/googleGrants.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/taskStatuses.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/proactiveReviewScheduler.test.js src/orchestrator/knowledgeSignals.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/orchestrator/providers/fakeRuntimeProvider.test.js src/orchestrator/providers/runtimeProviders.test.js src/orchestrator/providers/dispatcherExternalRuntime.test.js src/orchestrator/providers/deepAgentsProvider.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardModality.test.js src/shell/setupWizardPlaceholders.test.js src/shell/setupWizardSuggestions.test.js src/shell/setupWizardDiscovery.test.js src/shell/wrapText.test.js src/runtime/lifecycle.test.js src/runtime/store.test.js src/runtime/workspaceIsolation.test.js src/runtime/controlMessages.test.js src/runtime/controlClassify.test.js src/runtime/deltaCoalescer.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/delegation.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/skillChain.e2e.test.js src/runtime/donna-contract.test.js src/runtime/approvals.test.js src/runtime/auth.test.js src/runtime/totp.test.js src/runtime/loginSession.test.js src/runtime/loginRoutes.test.js",
|
|
22
|
+
"test": "node --test src/core/skillInvocation.test.js src/core/skillCompiler.test.js src/runtime/skillRun.test.js src/runtime/controlDrain.test.js src/runtime/controlCancellation.test.js src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/agent/llm.test.js src/agent/skillRecursion.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/profileServiceStatus.test.js src/core/llmCapabilities.test.js src/core/workspaceProfile.test.js src/core/buildInfo.test.js src/core/testGate.test.js src/core/agentEvents.test.js src/core/skillChainView.test.js src/core/runtimeLog.test.js src/core/runtimeEventAdapter.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/mcpEndpoints.test.js src/core/toolLoop.test.js src/core/logLabel.test.js src/runtime/conversationCompaction.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/otherWorkspacesRunning.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikiWorkspaceStart.test.js src/core/wikirc.test.js src/core/workspaceInherit.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/currentArtifact.test.js src/core/googleGrants.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/taskStatuses.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/proactiveReviewScheduler.test.js src/orchestrator/knowledgeSignals.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/orchestrator/providers/fakeRuntimeProvider.test.js src/orchestrator/providers/runtimeProviders.test.js src/orchestrator/providers/dispatcherExternalRuntime.test.js src/orchestrator/providers/deepAgentsProvider.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardModality.test.js src/shell/setupWizardPlaceholders.test.js src/shell/setupWizardSuggestions.test.js src/shell/setupWizardDiscovery.test.js src/shell/wrapText.test.js src/runtime/lifecycle.test.js src/runtime/store.test.js src/runtime/workspaceIsolation.test.js src/runtime/controlMessages.test.js src/runtime/controlClassify.test.js src/runtime/deltaCoalescer.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/delegation.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/skillChain.e2e.test.js src/runtime/donna-contract.test.js src/runtime/approvals.test.js src/runtime/auth.test.js src/runtime/totp.test.js src/runtime/loginSession.test.js src/runtime/loginRoutes.test.js",
|
|
23
23
|
"check-versions": "node scripts/check-versions.js",
|
|
24
24
|
"prepack": "node scripts/check-versions.js",
|
|
25
25
|
"prepublishOnly": "node scripts/check-versions.js",
|
package/src/agent/graph.js
CHANGED
|
@@ -27,6 +27,8 @@ import { toolResultNote, toolStartNote } from '../core/progressNotes.js';
|
|
|
27
27
|
import { openWikiPagesPromptLine } from '../core/openWikiPages.js';
|
|
28
28
|
import { enqueueProductionJob, ensureJobQueue, formatQueue, productionLockBusy } from '../core/jobQueue.js';
|
|
29
29
|
import { loadWorkspaceProfile, updateWorkspaceProfilePreference } from '../core/profile.js';
|
|
30
|
+
import { formatLlmConfigFact } from '../core/wikirc.js';
|
|
31
|
+
import { wikiSearchContextMessages } from '../core/wikiPresearch.js';
|
|
30
32
|
import { artifactFromToolCall, currentArtifactFor, currentArtifactPromptLine, rememberArtifact } from '../core/currentArtifact.js';
|
|
31
33
|
import { capabilityRegistryForSession } from '../orchestrator/capabilityRegistry.js';
|
|
32
34
|
import { objectiveForResolution } from '../orchestrator/objectiveResolver.js';
|
|
@@ -445,7 +447,15 @@ export function bareToolCallJson(content, tools = []) {
|
|
|
445
447
|
* reached the user and nothing ran.
|
|
446
448
|
*/
|
|
447
449
|
export function narratedToolCallText(content) {
|
|
448
|
-
|
|
450
|
+
// Accept EITHER `name{...}` (bare JSON body) OR `name({...})` (function-call
|
|
451
|
+
// style). Some providers/gateways write the call with parentheses around the
|
|
452
|
+
// arguments object — `runtime__run_skill({...})` — and the old brace-only
|
|
453
|
+
// regex missed it, so the raw call surfaced to the user AND executed nothing.
|
|
454
|
+
// The parenthesis alone is NOT enough: `runtime__delegate (the delegation
|
|
455
|
+
// tool) hands…` is a legitimate answer about the tool, and treating it as a
|
|
456
|
+
// call discarded it and ended on "Action not executed". Require the `{` of an
|
|
457
|
+
// arguments object right after the parenthesis.
|
|
458
|
+
const match = String(content ?? '').trim().match(/^([a-z][a-z0-9_-]*__[a-z][a-z0-9_-]*)\s*(?:\{|\(\s*\{)/i);
|
|
449
459
|
return match ? match[1] : null;
|
|
450
460
|
}
|
|
451
461
|
|
|
@@ -1311,6 +1321,7 @@ export function buildAgentSystemPrompt(state) {
|
|
|
1311
1321
|
`Reply language: ${language}.`,
|
|
1312
1322
|
`Current workspace: ${workspace}.`,
|
|
1313
1323
|
`Current wikirc profile: ${wikirc}.`,
|
|
1324
|
+
formatLlmConfigFact(state.session.wikircConfig, state.session.wikirc),
|
|
1314
1325
|
`Available primitives: ${commandList(state.session)}.`,
|
|
1315
1326
|
'Only announce or call slash commands that appear exactly in Available primitives. Do not invent command names, subcommands, or arguments.',
|
|
1316
1327
|
'Connected MCP tools you may call directly (server__tool naming convention). Everything listed below is directly callable. When the requested action has no matching direct tool, call runtime__delegate with the original objective: the runtime resolves it against the discovered agent capability contracts, including executor-only single-task capabilities.',
|
|
@@ -1333,6 +1344,20 @@ export function buildAgentSystemPrompt(state) {
|
|
|
1333
1344
|
'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.',
|
|
1334
1345
|
'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.',
|
|
1335
1346
|
'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.',
|
|
1347
|
+
// Observed: « récupère le numéro de ticket depuis le wiki » delegated to
|
|
1348
|
+
// the external runtime's agent.answer, which burned 519k tokens and failed
|
|
1349
|
+
// on its budget, while one wiki search answers it in a second.
|
|
1350
|
+
'A question about the subject matter of this workspace (its projects, documents, tickets, people, decisions, figures, dates) is answered from the wiki: search it FIRST with the wiki search/read tools and answer from what they return. Never runtime__delegate a question those read tools can answer; delegation is for actions, or for an analysis the user explicitly asks an agent to perform.',
|
|
1351
|
+
// Observed (acpi): "cherche sur internet" was delegated to the external
|
|
1352
|
+
// runtime, which answered it could not search the internet — while a
|
|
1353
|
+
// connected web-search read tool was available and offered right here. The
|
|
1354
|
+
// wiki stays first for workspace facts; the web tools cover the rest and
|
|
1355
|
+
// are never denied.
|
|
1356
|
+
'A wiki-first rule covers workspace facts only. For an internet/web search the wiki cannot answer, use the connected web-search read tool directly when one is offered; otherwise delegate the objective to a research capability. Never answer that you cannot search the internet when a web-search tool is available, and never claim a search you did not run.',
|
|
1357
|
+
// Observed: « compare les options A et B » answered from the previous
|
|
1358
|
+
// answers alone — the history carries Donna's text, not the pages — and
|
|
1359
|
+
// option A was invented, the opposite of what the wiki says.
|
|
1360
|
+
'Your earlier answers in this conversation are not evidence: they keep your text, not the pages. For each new question, search the wiki again for every fact you have not quoted from a tool result in this very turn. Never fill a gap (an acronym expansion, a missing option, a figure) from general knowledge.',
|
|
1336
1361
|
'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.',
|
|
1337
1362
|
'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.',
|
|
1338
1363
|
'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.',
|
|
@@ -1382,7 +1407,7 @@ export function buildAgentSystemPrompt(state) {
|
|
|
1382
1407
|
: null,
|
|
1383
1408
|
openWikiPagesPromptLine(state.session.openWikiPages),
|
|
1384
1409
|
currentArtifactPromptLine(currentArtifactFor(state.session)),
|
|
1385
|
-
'Runtime control: you have runtime__status, runtime__cancel, runtime__kill 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. You have no approval tool: a pending approval is granted only by the user through the approval button or the /approve command. Never grant, claim or report an approval yourself; when the user asks to proceed with pending mutations, tell them to use those controls. A request to approve, validate, confirm or accept a pending run is a human control action: NEVER call runtime__delegate (or any capability) for it — answer with the control to use and nothing else. When the user asks for a NEW action while a run is active,
|
|
1410
|
+
'Runtime control: you have runtime__status, runtime__cancel, runtime__kill 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. You have no approval tool: a pending approval is granted only by the user through the approval button or the /approve command. Never grant, claim or report an approval yourself; when the user asks to proceed with pending mutations, tell them to use those controls. A request to approve, validate, confirm or accept a pending run is a human control action: NEVER call runtime__delegate (or any capability) for it — answer with the control to use and nothing else. When the user asks for a NEW action while a run is active: if one of your direct tools performs it (write or edit a template, a build-context file or a wiki page; a read or a check), do it now — a write may be refused because the running job is using that part of the workspace, and then say so plainly and offer runtime__enqueue. Heavier work (ingest, build, export, curate, rebuilding an index) is queued with runtime__enqueue (it runs after), or, if the user insists it replaces the current work, runtime__kill then the new action.',
|
|
1386
1411
|
'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.',
|
|
1387
1412
|
'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.',
|
|
1388
1413
|
'Durable profile updates are actions in this stabilized version: delegate them instead of writing directly.',
|
|
@@ -1433,7 +1458,14 @@ function toolsForClassification(classification, writeTools, session = null) {
|
|
|
1433
1458
|
// suite: she can answer, approve, enqueue for later, soft-cancel or
|
|
1434
1459
|
// kill — but she must not fire new MCP jobs alongside the run (that is
|
|
1435
1460
|
// what runtime__enqueue is for). No canned regex answers anywhere.
|
|
1436
|
-
|
|
1461
|
+
// The direct tools stay offered — reads, and unitary writes such as a
|
|
1462
|
+
// template or a page: without them a question sent as a run could only be
|
|
1463
|
+
// delegated (observed: a wiki question handed to the external runtime,
|
|
1464
|
+
// which spent 519k tokens on it), and a template asked for during an
|
|
1465
|
+
// ingest waited for the whole ingest. ordinaryDirectTools never carries a
|
|
1466
|
+
// job starter, and the engine refuses a write that would race with the
|
|
1467
|
+
// running job (plan-demandes-pendant-run.md, lot 3).
|
|
1468
|
+
return [SHELL_READ_COMMAND_TOOL, ...controlTools, ...capabilityRunTools, ...ordinaryDirectTools(writeTools)];
|
|
1437
1469
|
}
|
|
1438
1470
|
if (session?.runtime?.url) {
|
|
1439
1471
|
// Offer every connected tool directly EXCEPT orchestration-bypass tools
|
|
@@ -1476,7 +1508,7 @@ export function isDonnaReadTool(item) {
|
|
|
1476
1508
|
if (tool === 'wiki_workspace_status' || tool === 'agent_describe' || tool === 'agent_status') return true;
|
|
1477
1509
|
// Match a read verb anywhere in the underscore-tokenized name, not just as
|
|
1478
1510
|
// a trailing suffix — third-party MCPs don't all name tools verb-last
|
|
1479
|
-
// (e.g.
|
|
1511
|
+
// (e.g. a connector whose `web_search_x` puts the verb in the middle).
|
|
1480
1512
|
return tool.split('_').some((segment) => DONNA_READ_VERBS.has(segment));
|
|
1481
1513
|
}
|
|
1482
1514
|
|
|
@@ -1585,7 +1617,8 @@ export function createAgentGraph(options = {}) {
|
|
|
1585
1617
|
kind: 'agent_turn',
|
|
1586
1618
|
confidence: 1,
|
|
1587
1619
|
reason: 'agent_mode_llm_decision',
|
|
1588
|
-
activeRun: state.session?.
|
|
1620
|
+
activeRun: state.session?._runActive === true
|
|
1621
|
+
|| ['running', 'pending_approval'].includes(state.session?.agentProjection?.status)
|
|
1589
1622
|
|| sessionActivities(state.session).some((activity) => !activity.terminal),
|
|
1590
1623
|
})
|
|
1591
1624
|
: (state.inputClassification ?? { kind: 'modify_run', confidence: 1, reason: 'tool_iteration' });
|
|
@@ -1614,11 +1647,28 @@ export function createAgentGraph(options = {}) {
|
|
|
1614
1647
|
: toolsForClassification(classification, writeTools, state.session);
|
|
1615
1648
|
const system = buildAgentSystemPrompt(state);
|
|
1616
1649
|
|
|
1650
|
+
// The wiki is searched before the first model call of a turn, as in chat
|
|
1651
|
+
// mode (core/wikiPresearch.js): a question about the workspace was
|
|
1652
|
+
// otherwise answered from the previous answers, or delegated to an agent
|
|
1653
|
+
// that burned its whole token budget on it. Only on the user's own words —
|
|
1654
|
+
// never on a compiled skill objective (private material) nor on the
|
|
1655
|
+
// continuation prompts of a run's later turns. It lives for this turn
|
|
1656
|
+
// only: the next turn's history is rebuilt from the conversation.
|
|
1657
|
+
const runTurnId = state.session._currentRunIdentity?.turnId;
|
|
1658
|
+
const userWords = iterations === 0
|
|
1659
|
+
&& normalizedSkillStack(state.session).length === 0
|
|
1660
|
+
&& !state.session._responseSynthesisOnly
|
|
1661
|
+
&& (!state.session._currentRunIdentity || !runTurnId || String(runTurnId).endsWith(':turn-1'));
|
|
1662
|
+
const userTurn = [
|
|
1663
|
+
...(userWords ? await wikiSearchContextMessages(state.input, state.session, tools, state.session._onStep) : []),
|
|
1664
|
+
{ role: 'user', content: state.input },
|
|
1665
|
+
];
|
|
1666
|
+
|
|
1617
1667
|
// On iteration 0: prior history is in state.messages, user input must be appended.
|
|
1618
1668
|
// On subsequent iterations: user message was already stored in state.messages by the
|
|
1619
1669
|
// iteration-0 return below, so use state.messages as-is.
|
|
1620
1670
|
const conversationMessages = iterations === 0
|
|
1621
|
-
? [...(state.messages ?? []),
|
|
1671
|
+
? [...(state.messages ?? []), ...userTurn]
|
|
1622
1672
|
: (state.messages ?? []);
|
|
1623
1673
|
|
|
1624
1674
|
try {
|
|
@@ -1655,7 +1705,7 @@ export function createAgentGraph(options = {}) {
|
|
|
1655
1705
|
return {
|
|
1656
1706
|
pendingToolCalls: null,
|
|
1657
1707
|
messages: [
|
|
1658
|
-
...(iterations === 0 ?
|
|
1708
|
+
...(iterations === 0 ? userTurn : []),
|
|
1659
1709
|
{
|
|
1660
1710
|
role: 'user',
|
|
1661
1711
|
content: 'Your previous tool call was incomplete or contained invalid JSON arguments. Call the appropriate available tool again with one complete valid JSON object. Do not narrate or reproduce the broken call.',
|
|
@@ -1689,7 +1739,7 @@ export function createAgentGraph(options = {}) {
|
|
|
1689
1739
|
tool_calls: result.tool_calls,
|
|
1690
1740
|
};
|
|
1691
1741
|
const newMessages = iterations === 0
|
|
1692
|
-
? [
|
|
1742
|
+
? [...userTurn, assistantToolMessage]
|
|
1693
1743
|
: [assistantToolMessage];
|
|
1694
1744
|
return {
|
|
1695
1745
|
pendingToolCalls: result.tool_calls,
|
|
@@ -1722,7 +1772,7 @@ export function createAgentGraph(options = {}) {
|
|
|
1722
1772
|
return {
|
|
1723
1773
|
pendingToolCalls: null,
|
|
1724
1774
|
messages: [
|
|
1725
|
-
...(iterations === 0 ?
|
|
1775
|
+
...(iterations === 0 ? userTurn : []),
|
|
1726
1776
|
{
|
|
1727
1777
|
role: 'user',
|
|
1728
1778
|
content: `You described a call to ${bareCall} as JSON text instead of calling it. Issue a real tool call now, or answer in plain language. Never print the call as text.`,
|
|
@@ -1746,7 +1796,7 @@ export function createAgentGraph(options = {}) {
|
|
|
1746
1796
|
return {
|
|
1747
1797
|
pendingToolCalls: null,
|
|
1748
1798
|
messages: [
|
|
1749
|
-
|
|
1799
|
+
...userTurn,
|
|
1750
1800
|
result.message ?? { role: 'assistant', content: result.content ?? '' },
|
|
1751
1801
|
{
|
|
1752
1802
|
role: 'user',
|
|
@@ -1790,7 +1840,7 @@ export function createAgentGraph(options = {}) {
|
|
|
1790
1840
|
return {
|
|
1791
1841
|
pendingToolCalls: null,
|
|
1792
1842
|
messages: [
|
|
1793
|
-
|
|
1843
|
+
...userTurn,
|
|
1794
1844
|
result.message ?? { role: 'assistant', content: result.content ?? '' },
|
|
1795
1845
|
{ role: 'user', content: 'This is an action request. Call runtime__delegate now with the original objective only. Do not provide instructions or narration.' },
|
|
1796
1846
|
],
|
|
@@ -1834,7 +1884,7 @@ export function createAgentGraph(options = {}) {
|
|
|
1834
1884
|
return {
|
|
1835
1885
|
pendingToolCalls: null,
|
|
1836
1886
|
messages: [
|
|
1837
|
-
...(iterations === 0 ?
|
|
1887
|
+
...(iterations === 0 ? userTurn : []),
|
|
1838
1888
|
result.message ?? { role: 'assistant', content: result.content ?? '' },
|
|
1839
1889
|
{
|
|
1840
1890
|
role: 'user',
|
|
@@ -1863,7 +1913,7 @@ export function createAgentGraph(options = {}) {
|
|
|
1863
1913
|
emitAgentEvent(state.session, 'assistant_message', 'llm', { content: result.content ?? '' });
|
|
1864
1914
|
// Text was streamed inline via session._onStream — no second LLM call needed.
|
|
1865
1915
|
const newMessages = iterations === 0
|
|
1866
|
-
? [
|
|
1916
|
+
? [...userTurn, result.message]
|
|
1867
1917
|
: [result.message];
|
|
1868
1918
|
return {
|
|
1869
1919
|
response: null,
|
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 { bareToolCallJson, buildAgentSystemPrompt, connectorConfigurationTarget, createAgentGraph, invalidSuggestedSlashCommands, invalidUserFacingToolNames, isOrchestrationBypassTool, knownCapabilityIds, normalizeToolArgumentsFromSchema, nothingToDoForDonna } from './graph.js';
|
|
6
|
+
import { bareToolCallJson, buildAgentSystemPrompt, connectorConfigurationTarget, createAgentGraph, invalidSuggestedSlashCommands, invalidUserFacingToolNames, isOrchestrationBypassTool, knownCapabilityIds, narratedToolCallText, normalizeToolArgumentsFromSchema, nothingToDoForDonna } from './graph.js';
|
|
7
7
|
|
|
8
8
|
test('user-facing response guard hides MCP identifiers generically', () => {
|
|
9
9
|
const session = sessionBase();
|
|
@@ -1122,6 +1122,81 @@ test('buildAgentSystemPrompt includes .wiki/profile.md content so preferences ap
|
|
|
1122
1122
|
}
|
|
1123
1123
|
});
|
|
1124
1124
|
|
|
1125
|
+
test('buildAgentSystemPrompt answers workspace questions from the wiki instead of delegating them', () => {
|
|
1126
|
+
// Observed: a wiki question delegated to the external runtime's agent.answer
|
|
1127
|
+
// burned 519k tokens and failed on its budget.
|
|
1128
|
+
const prompt = buildAgentSystemPrompt({ session: sessionBase({}) });
|
|
1129
|
+
assert.match(prompt, /search it FIRST with the wiki search\/read tools/);
|
|
1130
|
+
assert.match(prompt, /Never runtime__delegate a question those read tools can answer/);
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
async function agentFirstCallMessages(sessionOverrides, input = 'compare les options A et B') {
|
|
1134
|
+
const searched = [];
|
|
1135
|
+
const originalFetch = globalThis.fetch;
|
|
1136
|
+
globalThis.fetch = async (url, init) => {
|
|
1137
|
+
const body = JSON.parse(init?.body ?? '{}');
|
|
1138
|
+
let result = {};
|
|
1139
|
+
if (body.method === 'tools/call') {
|
|
1140
|
+
searched.push(body.params.arguments.question);
|
|
1141
|
+
result = { content: [{ type: 'text', text: 'wiki/concepts/a.md: option A, sans tracé manuel' }] };
|
|
1142
|
+
}
|
|
1143
|
+
const text = JSON.stringify({ jsonrpc: '2.0', id: body.id ?? 1, result });
|
|
1144
|
+
return { ok: true, status: 200, headers: { get: () => null }, text: async () => text, json: async () => JSON.parse(text) };
|
|
1145
|
+
};
|
|
1146
|
+
const seen = [];
|
|
1147
|
+
const session = sessionBase({
|
|
1148
|
+
mcp: { wiki: { status: 'connected', url: `http://agent-presearch-${Math.random()}.test/mcp`, tools: [
|
|
1149
|
+
{ name: 'wiki_search_context', inputSchema: { type: 'object', properties: {} } },
|
|
1150
|
+
] } },
|
|
1151
|
+
llm: { async completeWithTools({ messages }) { seen.push(messages); return { tool_calls: [], content: 'ok' }; } },
|
|
1152
|
+
...sessionOverrides,
|
|
1153
|
+
});
|
|
1154
|
+
try {
|
|
1155
|
+
await createAgentGraph().invoke({ input, session });
|
|
1156
|
+
} finally {
|
|
1157
|
+
globalThis.fetch = originalFetch;
|
|
1158
|
+
}
|
|
1159
|
+
return { searched, first: seen[0] ?? [] };
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
test('an agent turn searches the wiki before its first model call', async () => {
|
|
1163
|
+
const { searched, first } = await agentFirstCallMessages({});
|
|
1164
|
+
assert.deepEqual(searched, ['compare les options A et B']);
|
|
1165
|
+
const index = first.findIndex((m) => /WIKI SEARCH RESULTS/.test(m.content));
|
|
1166
|
+
assert.ok(index >= 0);
|
|
1167
|
+
assert.equal(first[index + 1].content, 'compare les options A et B');
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
test('an agent turn never pre-searches a skill objective or a later run turn', async () => {
|
|
1171
|
+
const skill = await agentFirstCallMessages({ _skillStack: ['pipeline'] });
|
|
1172
|
+
assert.deepEqual(skill.searched, []);
|
|
1173
|
+
const laterTurn = await agentFirstCallMessages({ _currentRunIdentity: { runId: 'r1', turnId: 'r1:turn-2' } });
|
|
1174
|
+
assert.deepEqual(laterTurn.searched, []);
|
|
1175
|
+
const firstTurn = await agentFirstCallMessages({ _currentRunIdentity: { runId: 'r1', turnId: 'r1:turn-1' } });
|
|
1176
|
+
assert.deepEqual(firstTurn.searched, ['compare les options A et B']);
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
test('an agent turn during a pending run keeps its direct tools and cannot start a second run', async () => {
|
|
1180
|
+
// plan-demandes-pendant-run.md, lot 3. The runtime says a run is active
|
|
1181
|
+
// (_runActive): a projection in `pending_approval` used to read as idle.
|
|
1182
|
+
let offered = [];
|
|
1183
|
+
const session = sessionBase({
|
|
1184
|
+
runtime: { url: 'http://127.0.0.1:7788' },
|
|
1185
|
+
_runActive: true,
|
|
1186
|
+
agentProjection: { status: 'pending_approval' },
|
|
1187
|
+
mcp: { wiki: { status: 'connected', url: 'http://x.test/mcp', tools: [
|
|
1188
|
+
{ name: 'template_write', inputSchema: { type: 'object', properties: {} } },
|
|
1189
|
+
{ name: 'wiki_search_context', inputSchema: { type: 'object', properties: {} } },
|
|
1190
|
+
] }, production: sessionBase().mcp.production },
|
|
1191
|
+
llm: { async completeWithTools({ tools }) { offered = tools.map((item) => item.function.name); return { tool_calls: [], content: 'ok' }; } },
|
|
1192
|
+
});
|
|
1193
|
+
await createAgentGraph().invoke({ input: 'salut', session });
|
|
1194
|
+
assert.ok(offered.includes('wiki__template_write'), offered.join(','));
|
|
1195
|
+
assert.ok(offered.includes('runtime__enqueue'), offered.join(','));
|
|
1196
|
+
assert.ok(!offered.includes('runtime__delegate'), 'no second run beside the active one');
|
|
1197
|
+
assert.ok(!offered.includes('production__production_start_job'), 'never a job starter');
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1125
1200
|
test('buildAgentSystemPrompt omits the profile section when profile.md is missing or empty', () => {
|
|
1126
1201
|
const workspacePath = mkdtempSync(join(tmpdir(), 'donna-profile-empty-'));
|
|
1127
1202
|
try {
|
|
@@ -2612,3 +2687,52 @@ test('LOT F: the guard message stays English-only for every session language', a
|
|
|
2612
2687
|
assert.match(await answer(undefined), /repeatedly printed an internal tool request/);
|
|
2613
2688
|
assert.match(await answer('fr'), /repeatedly printed an internal tool request/);
|
|
2614
2689
|
});
|
|
2690
|
+
|
|
2691
|
+
test('narratedToolCallText recognises the brace and the function-call forms', () => {
|
|
2692
|
+
assert.equal(narratedToolCallText('runtime__delegate{"objective":"x"}'), 'runtime__delegate');
|
|
2693
|
+
// Some gateways write the call with parentheses around the arguments object;
|
|
2694
|
+
// the brace-only regex missed this and executed nothing.
|
|
2695
|
+
assert.equal(narratedToolCallText('runtime__run_skill({"skillName":"deliver"})'), 'runtime__run_skill');
|
|
2696
|
+
assert.equal(
|
|
2697
|
+
narratedToolCallText(' production__production_start_job({ "type": "build" }) '),
|
|
2698
|
+
'production__production_start_job',
|
|
2699
|
+
);
|
|
2700
|
+
assert.equal(narratedToolCallText('Bonjour, je peux vous aider.'), null);
|
|
2701
|
+
// Prose ABOUT a tool, opening on its name and a parenthesis, is an answer.
|
|
2702
|
+
assert.equal(
|
|
2703
|
+
narratedToolCallText('runtime__delegate (the delegation tool) hands an objective to the runtime.'),
|
|
2704
|
+
null,
|
|
2705
|
+
);
|
|
2706
|
+
assert.equal(narratedToolCallText('runtime__delegate() takes an objective.'), null);
|
|
2707
|
+
assert.equal(narratedToolCallText('{"name":"runtime__run_skill"}'), null);
|
|
2708
|
+
});
|
|
2709
|
+
|
|
2710
|
+
test('a skill call narrated with parentheses is neither executed nor shown', async () => {
|
|
2711
|
+
// Observed: `/deliver TechSections.export.md polish` answered with the raw
|
|
2712
|
+
// `runtime__run_skill({…})` text — nothing ran and the call leaked, because
|
|
2713
|
+
// the guard only matched `name{`.
|
|
2714
|
+
const raw =
|
|
2715
|
+
'runtime__run_skill({"skillName":"deliver","arguments":{"deliverable":"TechSections.export.md","polish":"true"},"selectionKind":"explicit_name"})';
|
|
2716
|
+
const ran = [];
|
|
2717
|
+
let calls = 0;
|
|
2718
|
+
const session = sessionBase({
|
|
2719
|
+
language: 'fr-FR',
|
|
2720
|
+
runtime: { url: 'http://runtime.test' },
|
|
2721
|
+
_runSkillWithinRun: async (name) => { ran.push(name); return { ok: true }; },
|
|
2722
|
+
_onStreamReset: () => {},
|
|
2723
|
+
llm: {
|
|
2724
|
+
async completeWithTools() {
|
|
2725
|
+
calls += 1;
|
|
2726
|
+
return { content: raw, message: { role: 'assistant', content: raw }, tool_calls: null };
|
|
2727
|
+
},
|
|
2728
|
+
},
|
|
2729
|
+
});
|
|
2730
|
+
|
|
2731
|
+
const result = await createAgentGraph().invoke({ input: 'Lance le polish.', session });
|
|
2732
|
+
|
|
2733
|
+
assert.deepEqual(ran, [], 'a call written as text must never reach the skill runner');
|
|
2734
|
+
assert.doesNotMatch(result.response, /runtime__run_skill/);
|
|
2735
|
+
assert.doesNotMatch(result.response, /[{}]/, 'no JSON fragment may survive');
|
|
2736
|
+
assert.match(result.response, /repeatedly printed an internal tool request/);
|
|
2737
|
+
assert.ok(calls >= 2, 'the first occurrence is retried, not surfaced');
|
|
2738
|
+
});
|
package/src/agent/llm.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { supportsTemperature } from '../core/llmCapabilities.js';
|
|
2
|
+
|
|
1
3
|
function trimTrailingSlash(value) {
|
|
2
4
|
return value.replace(/\/+$/, '');
|
|
3
5
|
}
|
|
@@ -25,6 +27,13 @@ export function createLlmClientFromWikiConfig(config) {
|
|
|
25
27
|
return null;
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
// A gpt-5-class model rejects `temperature` outright (HTTP 400, "Only the
|
|
31
|
+
// default (1) value is supported"), whether the profile sets one or not. When
|
|
32
|
+
// it is refused, the field is omitted whole rather than sent and rejected.
|
|
33
|
+
const temperatureBody = supportsTemperature(llmConfig)
|
|
34
|
+
? { temperature: typeof llmConfig.temperature === 'number' ? llmConfig.temperature : 0.2 }
|
|
35
|
+
: {};
|
|
36
|
+
|
|
28
37
|
return {
|
|
29
38
|
async complete({ system, input, signal }) {
|
|
30
39
|
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
@@ -40,7 +49,7 @@ export function createLlmClientFromWikiConfig(config) {
|
|
|
40
49
|
{ role: 'system', content: system },
|
|
41
50
|
{ role: 'user', content: input },
|
|
42
51
|
],
|
|
43
|
-
|
|
52
|
+
...temperatureBody,
|
|
44
53
|
}),
|
|
45
54
|
});
|
|
46
55
|
|
|
@@ -64,7 +73,7 @@ export function createLlmClientFromWikiConfig(config) {
|
|
|
64
73
|
const body = {
|
|
65
74
|
model,
|
|
66
75
|
messages: allMessages,
|
|
67
|
-
|
|
76
|
+
...temperatureBody,
|
|
68
77
|
};
|
|
69
78
|
if (tools.length > 0) {
|
|
70
79
|
body.tools = tools;
|
|
@@ -99,7 +108,7 @@ export function createLlmClientFromWikiConfig(config) {
|
|
|
99
108
|
const body = {
|
|
100
109
|
model,
|
|
101
110
|
messages: allMessages,
|
|
102
|
-
|
|
111
|
+
...temperatureBody,
|
|
103
112
|
stream: true,
|
|
104
113
|
};
|
|
105
114
|
if (tools.length > 0) {
|
|
@@ -195,7 +204,7 @@ export function createLlmClientFromWikiConfig(config) {
|
|
|
195
204
|
body: JSON.stringify({
|
|
196
205
|
model,
|
|
197
206
|
messages: allMessages,
|
|
198
|
-
|
|
207
|
+
...temperatureBody,
|
|
199
208
|
stream: true,
|
|
200
209
|
}),
|
|
201
210
|
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createLlmClientFromWikiConfig } from './llm.js';
|
|
4
|
+
|
|
5
|
+
function captureFetch(reply) {
|
|
6
|
+
const calls = [];
|
|
7
|
+
const original = globalThis.fetch;
|
|
8
|
+
globalThis.fetch = async (url, init) => {
|
|
9
|
+
calls.push({ url, body: JSON.parse(init.body) });
|
|
10
|
+
return { ok: true, json: async () => reply };
|
|
11
|
+
};
|
|
12
|
+
return { calls, restore: () => { globalThis.fetch = original; } };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const reply = { choices: [{ message: { content: 'ok' } }] };
|
|
16
|
+
const gateway = {
|
|
17
|
+
provider: 'ai-gateway',
|
|
18
|
+
baseUrl: 'https://gw.example.com/v1',
|
|
19
|
+
apiKey: 'k',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
test('omits temperature for a gpt-5-class model even when the profile sets one', async () => {
|
|
23
|
+
const { calls, restore } = captureFetch(reply);
|
|
24
|
+
try {
|
|
25
|
+
const client = createLlmClientFromWikiConfig({
|
|
26
|
+
llm: { ...gateway, model: 'openai/gpt-5-mini', temperature: 0.2 },
|
|
27
|
+
});
|
|
28
|
+
await client.complete({ system: 's', input: 'i' });
|
|
29
|
+
assert.equal('temperature' in calls[0].body, false);
|
|
30
|
+
} finally {
|
|
31
|
+
restore();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('keeps the configured temperature for a model that accepts it', async () => {
|
|
36
|
+
const { calls, restore } = captureFetch(reply);
|
|
37
|
+
try {
|
|
38
|
+
const client = createLlmClientFromWikiConfig({
|
|
39
|
+
llm: { ...gateway, model: 'openai/gpt-4.1', temperature: 0.3 },
|
|
40
|
+
});
|
|
41
|
+
await client.complete({ system: 's', input: 'i' });
|
|
42
|
+
assert.equal(calls[0].body.temperature, 0.3);
|
|
43
|
+
} finally {
|
|
44
|
+
restore();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('keeps the 0.2 default when the profile declares no temperature', async () => {
|
|
49
|
+
const { calls, restore } = captureFetch(reply);
|
|
50
|
+
try {
|
|
51
|
+
const client = createLlmClientFromWikiConfig({
|
|
52
|
+
llm: { ...gateway, model: 'openai/gpt-4.1' },
|
|
53
|
+
});
|
|
54
|
+
await client.complete({ system: 's', input: 'i' });
|
|
55
|
+
assert.equal(calls[0].body.temperature, 0.2);
|
|
56
|
+
} finally {
|
|
57
|
+
restore();
|
|
58
|
+
}
|
|
59
|
+
});
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -18,7 +18,7 @@ import { ensureManagerScaffold, loadManagerEnv } from '../core/env.js';
|
|
|
18
18
|
loadManagerEnv();
|
|
19
19
|
import { createAgentGraph } from '../agent/graph.js';
|
|
20
20
|
import { handleSlashCommand, printHelp, printVersion, refreshMcpRuntimeStatus } from '../commands/slash.js';
|
|
21
|
-
import { runShell, runHeadlessChatTurn, sanitizeOpenWikiPages } from '../shell/repl.js';
|
|
21
|
+
import { runShell, runHeadlessChatTurn, sanitizeOpenWikiPages, chatInputBudgetChars } from '../shell/repl.js';
|
|
22
22
|
import { runPreflightChecks, withRuntimePreflight } from '../core/startupCheck.js';
|
|
23
23
|
import { refreshRunningContainers } from '../core/wikiSetup.js';
|
|
24
24
|
import { applySessionWikircProfile } from '../core/sessionConfig.js';
|
|
@@ -32,6 +32,7 @@ import { runAgentTurn, runAgenticLoop } from '../core/agentLoop.js';
|
|
|
32
32
|
import { createDeltaCoalescer } from '../runtime/deltaCoalescer.js';
|
|
33
33
|
import { TERMINAL_STATUS_SET } from '../orchestrator/taskStatuses.js';
|
|
34
34
|
import { resolveCapabilityConcurrency } from '../orchestrator/scheduler.js';
|
|
35
|
+
import { workspaceLockRegistry } from '../orchestrator/lockManager.js';
|
|
35
36
|
import { capabilityRegistryForSession } from '../orchestrator/capabilityRegistry.js';
|
|
36
37
|
import { CapabilityUnavailableError, resolve as resolveCapability } from '../orchestrator/capabilityResolver.js';
|
|
37
38
|
import { listWorkspaces } from '../core/workspaces.js';
|
|
@@ -358,6 +359,9 @@ export function createInteractiveSession(context, { runtimeUrl, turnId, signal =
|
|
|
358
359
|
session.headlessPlan = null;
|
|
359
360
|
session.turnId = turnId ?? null;
|
|
360
361
|
session._abortSignal = signal;
|
|
362
|
+
// The SAME lock registry as the workspace's runs, never a copy: a direct
|
|
363
|
+
// write from this turn must see what a run holds (plan-demandes-pendant-run.md).
|
|
364
|
+
session._workspaceLocks = workspaceLockRegistry(source);
|
|
361
365
|
return session;
|
|
362
366
|
}
|
|
363
367
|
|
|
@@ -956,7 +960,8 @@ async function runRuntime(argv, agent) {
|
|
|
956
960
|
const { resolveRuntimeAuthToken } = await import('../runtime/auth.js');
|
|
957
961
|
const { createSqliteQueueStore } = await import('../runtime/queueStore.js');
|
|
958
962
|
const { createApprovalManager } = await import('../runtime/approvals.js');
|
|
959
|
-
const { conversationSeed, runRuntimeAgenticWorkflow } = await import('../runtime/runner.js');
|
|
963
|
+
const { conversationSeed, conversationCompactionPlan, compactionNoteForDonna, runRuntimeAgenticWorkflow } = await import('../runtime/runner.js');
|
|
964
|
+
const { summarizeCompactedConversation } = await import('../runtime/conversationCompact.js');
|
|
960
965
|
|
|
961
966
|
// Same default as wiki-workspace runtime up and ensureRuntime: a loopback
|
|
962
967
|
// bind is invisible to the serve container.
|
|
@@ -1741,13 +1746,17 @@ async function runRuntime(argv, agent) {
|
|
|
1741
1746
|
await refreshMcpRuntimeStatus(context.session);
|
|
1742
1747
|
}
|
|
1743
1748
|
const ephemeral = createInteractiveSession(context, { runtimeUrl: selfRuntimeUrl, turnId, signal });
|
|
1749
|
+
// Whether a run is active in this workspace, from the runtime itself: the
|
|
1750
|
+
// projection says `pending_approval` for an ingest waiting on its approval,
|
|
1751
|
+
// and reading only `running` made such a turn believe the workspace idle.
|
|
1752
|
+
ephemeral._runActive = Boolean(context.running);
|
|
1744
1753
|
// Seed from a freshly reduced COPY of persisted events. Interactive turn
|
|
1745
1754
|
// events deliberately do not mutate the canonical run projection, so the
|
|
1746
1755
|
// canonical session alone is not a reliable conversation-history source.
|
|
1747
1756
|
const persistedProjection = reduceAgentEvents(store.listEvents({
|
|
1748
1757
|
workspace: context.workspace ?? ephemeral.workspace ?? null,
|
|
1749
1758
|
}));
|
|
1750
|
-
|
|
1759
|
+
let messages = conversationSeed({ agentProjection: persistedProjection }, input);
|
|
1751
1760
|
// Streaming fragments are coalesced before they are persisted and pushed —
|
|
1752
1761
|
// one synchronous SQLite insert (plus one SSE write) per token stalled the
|
|
1753
1762
|
// event loop, freezing both chats (serve and ShellUI) while a long answer
|
|
@@ -1779,6 +1788,37 @@ async function runRuntime(argv, agent) {
|
|
|
1779
1788
|
workspace: context.workspace ?? null,
|
|
1780
1789
|
payload: { message },
|
|
1781
1790
|
}));
|
|
1791
|
+
// Automatic memory compaction, before this turn's own message enters the
|
|
1792
|
+
// conversation. Not during a run (the manual gauge refuses it too: the
|
|
1793
|
+
// boundary must not move under a run's own messages). A compaction whose
|
|
1794
|
+
// summary could not be produced moves nothing — dropping messages without
|
|
1795
|
+
// their summary is the silent loss this exists to end. Donna, not the
|
|
1796
|
+
// system, tells the user it happened.
|
|
1797
|
+
const compaction = context.running ? null : conversationCompactionPlan(persistedProjection, {
|
|
1798
|
+
budgetChars: chatInputBudgetChars(context.session?.wikircConfig),
|
|
1799
|
+
});
|
|
1800
|
+
if (compaction) {
|
|
1801
|
+
ephemeral._onStep?.('Condensing the earlier conversation…');
|
|
1802
|
+
const summary = await summarizeCompactedConversation(context.session, compaction);
|
|
1803
|
+
if (summary && summary !== compaction.previousSummary) {
|
|
1804
|
+
dispatchAgentEvent(ephemeral, createAgentEvent('conversation_reset', {
|
|
1805
|
+
origin: 'runtime_turn',
|
|
1806
|
+
turnId,
|
|
1807
|
+
workspace: context.workspace ?? null,
|
|
1808
|
+
payload: { summary, keepLast: compaction.keepLast, automatic: true, reason: compaction.reason },
|
|
1809
|
+
}));
|
|
1810
|
+
const compactedProjection = reduceAgentEvents(store.listEvents({
|
|
1811
|
+
workspace: context.workspace ?? ephemeral.workspace ?? null,
|
|
1812
|
+
}));
|
|
1813
|
+
const count = compaction.segment.filter((message) => ['user', 'assistant'].includes(message?.role)).length;
|
|
1814
|
+
// Right after the summary it refers to — never last: the chat branch
|
|
1815
|
+
// drops a trailing user message as the current input.
|
|
1816
|
+
const seed = conversationSeed({ agentProjection: compactedProjection }, input);
|
|
1817
|
+
messages = [seed[0], { role: 'user', content: compactionNoteForDonna(count) }, ...seed.slice(1)];
|
|
1818
|
+
} else {
|
|
1819
|
+
emitRuntimeLog(context.session, 'conversation-compact: automatic compaction skipped — no new summary; the conversation window is unchanged');
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1782
1822
|
dispatchAgentEvent(ephemeral, createAgentEvent('user_message', {
|
|
1783
1823
|
origin: 'runtime_turn',
|
|
1784
1824
|
turnId,
|
package/src/commands/slash.js
CHANGED
|
@@ -144,8 +144,10 @@ function wikircSummaryText(summary) {
|
|
|
144
144
|
`profile=${summary.profile}`,
|
|
145
145
|
`file=${summary.fileName}`,
|
|
146
146
|
`provider=${summary.provider ?? '-'}`,
|
|
147
|
+
`engine=${summary.engine ?? '-'}`,
|
|
147
148
|
`model=${summary.model ?? '-'}`,
|
|
148
149
|
`baseUrl=${summary.baseUrl ?? '-'}`,
|
|
150
|
+
`temperature=${summary.temperature ?? '-'}`,
|
|
149
151
|
`language=${summary.language ?? '-'}`,
|
|
150
152
|
`apiKey=${summary.hasApiKey ? 'configured' : 'missing'}`,
|
|
151
153
|
`vector=${summary.vectorEnabled ? 'enabled' : 'disabled'}`,
|
package/src/core/agentEvents.js
CHANGED
|
@@ -5,6 +5,7 @@ import { applyPlanPatch, normalizePlanPatch, normalizePlanRevision, rebasePlanPa
|
|
|
5
5
|
import { formatRuntimeLogPayload, isDispatchPlumbingLine, normalizeRuntimeLog, shortTaskLabel } from './runtimeLog.js';
|
|
6
6
|
import { projectSkillChains, TERMINAL as CONTROL_TERMINAL_STATUSES } from './skillChainView.js';
|
|
7
7
|
import { projectWorkflow } from './workflow.js';
|
|
8
|
+
import { compactLogLabel } from './logLabel.js';
|
|
8
9
|
import { validateContractInDev } from '../contracts/schemas.js';
|
|
9
10
|
import { isActive, isTerminal, isSuccessful, isUnknownStatus, normalizeTaskStatus } from '../orchestrator/taskStatuses.js';
|
|
10
11
|
|
|
@@ -362,7 +363,16 @@ function applyEvent(state, event) {
|
|
|
362
363
|
// (length at reset time) instead of clearing state.conversation is what
|
|
363
364
|
// keeps the two concerns apart: the gauge/seed read conversationSeedStart,
|
|
364
365
|
// the display reads conversation.
|
|
365
|
-
|
|
366
|
+
// `keepLast` (automatic compaction): the last exchanges stay verbatim in
|
|
367
|
+
// the seed — a follow-up such as « rajoute la liste » needs the previous
|
|
368
|
+
// answer word for word, not its summary. The manual gauge sends none.
|
|
369
|
+
{
|
|
370
|
+
const keepLast = Math.max(0, Math.floor(Number(event.payload?.keepLast) || 0));
|
|
371
|
+
state.conversationSeedStart = Math.max(
|
|
372
|
+
Number(state.conversationSeedStart) || 0,
|
|
373
|
+
state.conversation.length - keepLast,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
366
376
|
// The summary is best-effort (an LLM call the compact route makes before
|
|
367
377
|
// dispatching this event): when it succeeds it REPLACES the previous one
|
|
368
378
|
// — it is a rolling summary of "everything before this point", not an
|
|
@@ -1237,7 +1247,7 @@ function planTaskById(state, taskId) {
|
|
|
1237
1247
|
function taskLabelFor(state, taskId) {
|
|
1238
1248
|
const step = planTaskById(state, taskId);
|
|
1239
1249
|
const label = step?.label ?? step?.description ?? null;
|
|
1240
|
-
if (label && !/^Step \d+$/.test(label)) return label;
|
|
1250
|
+
if (label && !/^Step \d+$/.test(label)) return compactLogLabel(label);
|
|
1241
1251
|
return shortTaskLabel(taskId) || String(taskId ?? '') || 'task';
|
|
1242
1252
|
}
|
|
1243
1253
|
|