@dotdrelle/wiki-manager 0.15.94 → 0.15.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -28
- package/package.json +2 -2
- package/src/agent/graph.js +36 -18
- package/src/agent/graph.test.js +4 -1
- package/src/cli/wiki-manager.js +35 -17
- package/src/core/agentEvents.js +1 -3
- package/src/core/buildInfo.json +2 -2
- package/src/core/googleGrants.js +0 -3
- package/src/core/json.js +9 -0
- package/src/core/mcp.js +1 -1
- package/src/core/plan.js +0 -4
- package/src/core/progressNotes.js +0 -4
- package/src/core/skillChainView.js +3 -1
- package/src/core/toolLoop.js +56 -2
- package/src/core/toolLoop.test.js +35 -4
- package/src/orchestrator/agentRegistry.js +1 -3
- package/src/orchestrator/dependencyResolver.js +0 -3
- package/src/orchestrator/planValidator.js +1 -3
- package/src/orchestrator/providers/runtimeProvider.js +0 -14
- package/src/orchestrator/taskStatuses.js +8 -0
- package/src/runtime/client.js +0 -16
- package/src/runtime/controlDrain.js +6 -3
- package/src/runtime/deltaCoalescer.js +53 -0
- package/src/runtime/deltaCoalescer.test.js +56 -0
- package/src/runtime/loginPage.js +0 -3
- package/src/runtime/loginSession.js +1 -4
- package/src/runtime/runner.js +17 -3
- package/src/runtime/runner.test.js +32 -1
- package/src/runtime/server.js +34 -0
- package/src/runtime/server.test.js +87 -0
- package/src/runtime/skillRun.js +1 -1
- package/src/runtime/skillRun.test.js +3 -0
- package/src/shell/repl.js +7 -3
- package/src/orchestrator/.fuse_hidden0000001c00000001 +0 -316
package/README.md
CHANGED
|
@@ -121,47 +121,60 @@ in isolated workspaces.
|
|
|
121
121
|
|
|
122
122
|
## How wikiLLM compares
|
|
123
123
|
|
|
124
|
-
Several open projects now build
|
|
124
|
+
Several open projects now build an LLM-maintained knowledge layer over your
|
|
125
|
+
sources — some as a wiki, some as a retrieval and answer platform. They target
|
|
125
126
|
**different problems** — the useful questions are *what goes in, what comes out,
|
|
126
|
-
and who operates it*. Snapshot as of 2026
|
|
127
|
+
and who operates it*. Snapshot as of **September 2026**; all of these move quickly.
|
|
127
128
|
|
|
128
129
|
✅ first-class · 🟡 partial or indirect · ❌ not a goal
|
|
129
130
|
|
|
130
|
-
Projects compared: [OpenWiki](https://github.com/langchain-ai/openwiki)
|
|
131
|
-
[DeepWiki-Open](https://github.com/asyncfuncai/deepwiki-open)
|
|
131
|
+
Projects compared: [OpenWiki](https://github.com/langchain-ai/openwiki) and
|
|
132
|
+
[DeepWiki-Open](https://github.com/asyncfuncai/deepwiki-open) (code / knowledge
|
|
133
|
+
wikis), [RAGFlow](https://github.com/infiniflow/ragflow) and
|
|
134
|
+
[Onyx](https://github.com/onyx-dot-app/onyx) (RAG platforms), and
|
|
132
135
|
[GraphRAG](https://github.com/microsoft/graphrag).
|
|
133
136
|
|
|
134
137
|
<table>
|
|
135
138
|
<thead>
|
|
136
|
-
<tr
|
|
139
|
+
<tr>
|
|
140
|
+
<th><small>Need</small></th>
|
|
141
|
+
<th><small><strong>wikiLLM</strong></small></th>
|
|
142
|
+
<th><small><strong>Code / knowledge wikis</strong><br>OpenWiki · DeepWiki-Open</small></th>
|
|
143
|
+
<th><small><strong>RAG platforms</strong><br>RAGFlow · Onyx</small></th>
|
|
144
|
+
<th><small><strong>Graph pipeline</strong><br>GraphRAG</small></th>
|
|
145
|
+
</tr>
|
|
137
146
|
</thead>
|
|
138
147
|
<tbody>
|
|
139
|
-
<tr><td><small>Input</small></td><td><small>✅ Business docs — Confluence, PDF, Office, SaaS</small></td><td><small>✅
|
|
140
|
-
<tr><td><small>Output</small></td><td><small>✅ Maintained wiki <strong>+ deliverables</strong
|
|
141
|
-
<tr><td><small>
|
|
142
|
-
<tr><td><small>
|
|
143
|
-
<tr><td><small>
|
|
144
|
-
<tr><td><small>
|
|
145
|
-
<tr><td><small>
|
|
146
|
-
<tr><td><small>
|
|
147
|
-
<tr><td><small>Connectors as services</small></td><td><small>✅ Independent MCP agents (Confluence, docs, e-mail…)</small></td><td><small>🟡 Built-in connector set</small></td><td><small>❌</small></td><td><small>❌</small></td></tr>
|
|
148
|
-
<tr><td><small>Offline / local models</small></td><td><small>✅ Per-workspace OpenAI-compatible or gateway (Ollama, vLLM, MLX…)</small></td><td><small>✅ 13+ providers</small></td><td><small>✅ Ollama</small></td><td><small>✅ Any OpenAI-compatible</small></td></tr>
|
|
149
|
-
<tr><td><small>License</small></td><td><small>❌ PolyForm <strong>Noncommercial</strong></small></td><td><small>✅ MIT</small></td><td><small>✅ MIT</small></td><td><small>✅ MIT</small></td></tr>
|
|
148
|
+
<tr><td><small>Input</small></td><td><small>✅ Business docs — Confluence, PDF, Office, SaaS</small></td><td><small>✅ Code repo; OpenWiki also personal sources (Notion, Gmail, Slack…)</small></td><td><small>✅ PDF/Office/images, Confluence, S3, 50+ connectors</small></td><td><small>🟡 Plain text</small></td></tr>
|
|
149
|
+
<tr><td><small>Output</small></td><td><small>✅ Maintained wiki <strong>+ deliverables</strong></small></td><td><small>✅ Linked Markdown wiki (+ graph / diagrams)</small></td><td><small>🟡 Grounded RAG answers, not a wiki</small></td><td><small>❌ Entity graph + summaries</small></td></tr>
|
|
150
|
+
<tr><td><small>Agents & actions</small></td><td><small>✅ Capability dispatcher, external agentic runtime, MCP tools</small></td><td><small>✅ Doc-generation agents, MCP connectors</small></td><td><small>✅ Agentic workflow, MCP actions, deep research</small></td><td><small>❌ Indexing pipeline</small></td></tr>
|
|
151
|
+
<tr><td><small>Keep current</small></td><td><small>✅ Re-ingest / scheduled</small></td><td><small>✅ <code>--update</code> / regenerate, CI</small></td><td><small>✅ Scheduled connector sync</small></td><td><small>🟡 Maintenance mode</small></td></tr>
|
|
152
|
+
<tr><td><small>Team UI</small></td><td><small>✅ Web console — wiki, graph, chat, runs</small></td><td><small>🟡 CLI + visualizer; DeepWiki-Open web app</small></td><td><small>✅ Self-hosted web app</small></td><td><small>❌ Library / CLI</small></td></tr>
|
|
153
|
+
<tr><td><small>Multi-project isolation</small></td><td><small>✅ Workspaces, own services, ports, secrets</small></td><td><small>❌ One wiki per repo / directory</small></td><td><small>🟡 Datasets / teams, not isolated services</small></td><td><small>❌ One index per corpus</small></td></tr>
|
|
154
|
+
<tr><td><small>Human approval & governance</small></td><td><small>✅ Approval-gated dispatcher, budgets, crash recovery</small></td><td><small>❌ PR review only</small></td><td><small>🟡 RBAC / SSO (Onyx enterprise)</small></td><td><small>❌</small></td></tr>
|
|
155
|
+
<tr><td><small>License</small></td><td><small>❌ PolyForm <strong>Noncommercial</strong></small></td><td><small>✅ MIT</small></td><td><small>✅ Apache-2.0 / MIT CE</small></td><td><small>✅ MIT</small></td></tr>
|
|
150
156
|
</tbody>
|
|
151
157
|
</table>
|
|
152
158
|
|
|
159
|
+
<small>Per-project details: OpenWiki emits OKF v0.2 and maintains a versioned
|
|
160
|
+
evidence graph of “grounded claims”; GraphRAG is a research project now in
|
|
161
|
+
maintenance mode; Onyx’s SSO/RBAC live in its enterprise edition.</small>
|
|
162
|
+
|
|
153
163
|
**The short version:**
|
|
154
164
|
|
|
155
|
-
- **OpenWiki** and **DeepWiki-Open**
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
- **OpenWiki** and **DeepWiki-Open** build wikis from a **code repository**
|
|
166
|
+
(OpenWiki also ingests personal sources — Notion, Gmail, Slack…). Their input
|
|
167
|
+
is code or personal feeds, not a stack of business documents feeding your
|
|
168
|
+
deliverable templates.
|
|
169
|
+
- **RAGFlow** and **Onyx** are RAG platforms: they index sources and answer
|
|
170
|
+
questions from them, with agents and connectors, but the output is an answer,
|
|
171
|
+
not a wiki you browse or a document you ship.
|
|
172
|
+
- **GraphRAG** builds retrieval structure and is now in maintenance mode — a
|
|
173
|
+
complementary back end, not a wiki.
|
|
174
|
+
- **wikiLLM** is the only one whose output is *both* a browsable wiki *and*
|
|
175
|
+
regenerated business documents, and the only one with the operational layer —
|
|
176
|
+
isolated projects, bounded approvals, automatic recovery, a web console —
|
|
177
|
+
that a shared internal tool needs.
|
|
165
178
|
|
|
166
179
|
**What wikiLLM does *not* try to do (today):**
|
|
167
180
|
|
|
@@ -170,8 +183,6 @@ Projects compared: [OpenWiki](https://github.com/langchain-ai/openwiki),
|
|
|
170
183
|
- Serve a true multi-user instance with per-user identity and an attributed
|
|
171
184
|
audit trail. This is a single-user deployment baseline (see the scope note
|
|
172
185
|
above).
|
|
173
|
-
- Expose a graph-query API over the corpus the way GraphRAG does; retrieval is
|
|
174
|
-
BM25 plus a vector index feeding generation.
|
|
175
186
|
- Ship or host the multi-provider AI gateway — routing to several providers is
|
|
176
187
|
supported, the gateway itself is infrastructure you bring.
|
|
177
188
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.96",
|
|
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/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/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/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/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/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/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/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/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/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
|
@@ -31,6 +31,7 @@ import { artifactFromToolCall, currentArtifactFor, currentArtifactPromptLine, re
|
|
|
31
31
|
import { capabilityRegistryForSession } from '../orchestrator/capabilityRegistry.js';
|
|
32
32
|
import { objectiveForResolution } from '../orchestrator/objectiveResolver.js';
|
|
33
33
|
import { fetchRuntimeState, postRuntimeCancel, postRuntimeControl, postRuntimeDelegate, postRuntimeKill, postRuntimeSkill } from '../runtime/client.js';
|
|
34
|
+
import { formatPublicSkillInvocation, generateSkillAcknowledgment } from '../runtime/skillRun.js';
|
|
34
35
|
|
|
35
36
|
const MAX_TOOL_ITERATIONS = 80;
|
|
36
37
|
/**
|
|
@@ -1377,24 +1378,6 @@ export function buildAgentSystemPrompt(state) {
|
|
|
1377
1378
|
return customPrompt ? `${customPrompt}\n\n${agentContext}` : agentContext;
|
|
1378
1379
|
}
|
|
1379
1380
|
|
|
1380
|
-
export function buildLimitedAgentResponse(state, reason = 'no workspace loaded with .wikirc.yaml') {
|
|
1381
|
-
const workspace = state.session.workspace ?? 'no workspace selected';
|
|
1382
|
-
const wikirc = state.session.wikirc?.profile ?? 'no profile loaded';
|
|
1383
|
-
return [
|
|
1384
|
-
`Donna is active. Current workspace: ${workspace}.`,
|
|
1385
|
-
`Current wikirc profile: ${wikirc}.`,
|
|
1386
|
-
'',
|
|
1387
|
-
'I am the shell agent mode: use `/agent` to route free text through this LangGraph graph, or `/chat` for direct chat.',
|
|
1388
|
-
`LLM connection: limited mode (${reason}).`,
|
|
1389
|
-
`Available primitives: ${commandList(state.session)}.`,
|
|
1390
|
-
'',
|
|
1391
|
-
'Connected MCP tools:',
|
|
1392
|
-
formatMcpToolsForAgent(state.session.mcp),
|
|
1393
|
-
'',
|
|
1394
|
-
'Limited mode: workspace, Docker Compose tools, MCP calls, /wiki fallback, skill discovery, and headless mode are wired.',
|
|
1395
|
-
'Use `/help` to see deterministic shell commands.',
|
|
1396
|
-
].join('\n');
|
|
1397
|
-
}
|
|
1398
1381
|
|
|
1399
1382
|
export function formatLlmUnavailableMessage(reason) {
|
|
1400
1383
|
const clean = String(reason ?? 'unknown reason').replace(/\s+/g, ' ').trim();
|
|
@@ -1903,6 +1886,7 @@ export function createAgentGraph(options = {}) {
|
|
|
1903
1886
|
const toolCalls = state.pendingToolCalls ?? [];
|
|
1904
1887
|
const toolResultMessages = [];
|
|
1905
1888
|
let terminalFailure = null;
|
|
1889
|
+
let skillLaunch = null;
|
|
1906
1890
|
|
|
1907
1891
|
for (const call of toolCalls) {
|
|
1908
1892
|
const resolved = resolveToolCallName(state.session.mcp, call.function.name, INTERNAL_TOOL_SERVERS);
|
|
@@ -2017,6 +2001,19 @@ export function createAgentGraph(options = {}) {
|
|
|
2017
2001
|
if (skillResult?.terminal === true) {
|
|
2018
2002
|
terminalFailure = skillResult.code ?? 'skill_failed';
|
|
2019
2003
|
ok = false;
|
|
2004
|
+
} else if (skillResult?.accepted === true) {
|
|
2005
|
+
// The skill owns execution from here. Without this the turn kept
|
|
2006
|
+
// going and the model could re-delegate or refuse the very
|
|
2007
|
+
// objective it had just launched — the "launched, then no agent
|
|
2008
|
+
// capable" contradiction. End the turn with the launch
|
|
2009
|
+
// acknowledgement instead of another tool round.
|
|
2010
|
+
skillLaunch = {
|
|
2011
|
+
publicInput: formatPublicSkillInvocation(
|
|
2012
|
+
skillResult.skill ?? args.skillName ?? '',
|
|
2013
|
+
args.arguments && typeof args.arguments === 'object' ? args.arguments : {},
|
|
2014
|
+
),
|
|
2015
|
+
objectives: Number(skillResult.objectiveCount ?? skillResult.objectives ?? 1) || 1,
|
|
2016
|
+
};
|
|
2020
2017
|
}
|
|
2021
2018
|
}
|
|
2022
2019
|
if (tool === 'delegate' && /^Runtime control error \(delegate\):/i.test(resultText)) {
|
|
@@ -2181,6 +2178,24 @@ export function createAgentGraph(options = {}) {
|
|
|
2181
2178
|
invalidResponseRetries: 0,
|
|
2182
2179
|
};
|
|
2183
2180
|
}
|
|
2181
|
+
if (skillLaunch) {
|
|
2182
|
+
// Deterministic, localized acknowledgement, generated in the session
|
|
2183
|
+
// language like the `/turn` skill path. No further model turn: the skill
|
|
2184
|
+
// is launched and owns execution, so nothing can contradict it.
|
|
2185
|
+
const response = await generateSkillAcknowledgment(state.session, skillLaunch).catch(
|
|
2186
|
+
() => `Started ${skillLaunch.publicInput} — ${skillLaunch.objectives} step(s) in progress.`,
|
|
2187
|
+
);
|
|
2188
|
+
return {
|
|
2189
|
+
messages: toolResultMessages,
|
|
2190
|
+
pendingToolCalls: null,
|
|
2191
|
+
response,
|
|
2192
|
+
forceDelegation: false,
|
|
2193
|
+
retryWithoutTool: false,
|
|
2194
|
+
terminalToolFailure: false,
|
|
2195
|
+
invalidToolCallRetries: 0,
|
|
2196
|
+
invalidResponseRetries: 0,
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2184
2199
|
return {
|
|
2185
2200
|
messages: toolResultMessages,
|
|
2186
2201
|
pendingToolCalls: null,
|
|
@@ -2193,6 +2208,9 @@ export function createAgentGraph(options = {}) {
|
|
|
2193
2208
|
}
|
|
2194
2209
|
|
|
2195
2210
|
function routeToolExecutor(state) {
|
|
2211
|
+
// A skill launch already produced its final acknowledgement: end the turn
|
|
2212
|
+
// instead of routing back for another model call that could contradict it.
|
|
2213
|
+
if (state.response != null) return END;
|
|
2196
2214
|
return 'orchestrator';
|
|
2197
2215
|
}
|
|
2198
2216
|
|
package/src/agent/graph.test.js
CHANGED
|
@@ -624,7 +624,10 @@ test('an explicitly selected skill runs through the intra-runtime path with name
|
|
|
624
624
|
},
|
|
625
625
|
});
|
|
626
626
|
const result = await createAgentGraph().invoke({ input: 'lance le skill deliver avec le template Quarterly report', session });
|
|
627
|
-
|
|
627
|
+
// Launching a skill ends the turn: the acknowledgement is generated once and
|
|
628
|
+
// the model is not given a second chance to re-delegate or contradict it.
|
|
629
|
+
assert.equal(result.response, 'Started /deliver deliverable="Quarterly report" — 1 step(s) in progress.');
|
|
630
|
+
assert.equal(mainCalls, 1);
|
|
628
631
|
// `skillStack` accompagne désormais la demande : le run imbriqué démarre après
|
|
629
632
|
// le nettoyage de celui-ci, et c'est le seul canal par lequel il peut savoir
|
|
630
633
|
// quelles compétences sont déjà ouvertes au-dessus de lui.
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -29,6 +29,8 @@ import { extractActivity, mergePolledActivity, parseJsonText, sessionActivities,
|
|
|
29
29
|
import { syncActivitiesToPlan, formatPlanStatus } from '../core/plan.js';
|
|
30
30
|
import { createAgentEvent, dispatchAgentEvent, reduceAgentEvents } from '../core/agentEvents.js';
|
|
31
31
|
import { runAgentTurn, runAgenticLoop } from '../core/agentLoop.js';
|
|
32
|
+
import { createDeltaCoalescer } from '../runtime/deltaCoalescer.js';
|
|
33
|
+
import { TERMINAL_STATUS_SET } from '../orchestrator/taskStatuses.js';
|
|
32
34
|
import { resolveCapabilityConcurrency } from '../orchestrator/scheduler.js';
|
|
33
35
|
import { capabilityRegistryForSession } from '../orchestrator/capabilityRegistry.js';
|
|
34
36
|
import { CapabilityUnavailableError, resolve as resolveCapability } from '../orchestrator/capabilityResolver.js';
|
|
@@ -827,7 +829,7 @@ async function runHeadless(argv, agent) {
|
|
|
827
829
|
// export finished, before the ingest had even started. The control queue is the
|
|
828
830
|
// only place where the whole chain is observable, so the wait is scoped to
|
|
829
831
|
// chainId and ends when every item of that chain is terminal.
|
|
830
|
-
const CHAIN_TERMINAL_STATUSES =
|
|
832
|
+
const CHAIN_TERMINAL_STATUSES = TERMINAL_STATUS_SET;
|
|
831
833
|
|
|
832
834
|
export async function waitForRuntimeChain(session, log, {
|
|
833
835
|
chainId,
|
|
@@ -1739,7 +1741,21 @@ async function runRuntime(argv, agent) {
|
|
|
1739
1741
|
workspace: context.workspace ?? ephemeral.workspace ?? null,
|
|
1740
1742
|
}));
|
|
1741
1743
|
const messages = conversationSeed({ agentProjection: persistedProjection }, input);
|
|
1744
|
+
// Streaming fragments are coalesced before they are persisted and pushed —
|
|
1745
|
+
// one synchronous SQLite insert (plus one SSE write) per token stalled the
|
|
1746
|
+
// event loop, freezing both chats (serve and ShellUI) while a long answer
|
|
1747
|
+
// was still being produced. Flushed before any non-delta event so ordering
|
|
1748
|
+
// and the final tail are preserved.
|
|
1749
|
+
const deltaCoalescer = createDeltaCoalescer((delta) => dispatchAgentEvent(ephemeral, createAgentEvent('assistant_delta', {
|
|
1750
|
+
origin: 'runtime_turn',
|
|
1751
|
+
turnId,
|
|
1752
|
+
workspace: context.workspace ?? null,
|
|
1753
|
+
payload: { delta },
|
|
1754
|
+
})), { intervalMs: 80 });
|
|
1742
1755
|
ephemeral._onAgentEvent = (event) => {
|
|
1756
|
+
// Never let a final message (or any other event) overtake the buffered
|
|
1757
|
+
// fragments that precede it: flush them first, in order.
|
|
1758
|
+
if (event.type !== 'assistant_delta' && event.type !== 'assistant_delta_reset') deltaCoalescer.flush();
|
|
1743
1759
|
const interactiveEvent = {
|
|
1744
1760
|
...event,
|
|
1745
1761
|
origin: 'runtime_turn',
|
|
@@ -1785,28 +1801,30 @@ async function runRuntime(argv, agent) {
|
|
|
1785
1801
|
response = await runHeadlessChatTurn(ephemeral, input, {
|
|
1786
1802
|
history,
|
|
1787
1803
|
onStep: ephemeral._onStep,
|
|
1788
|
-
// Fragments de réponse publiés au fil de l'eau
|
|
1789
|
-
// agrège dans la dernière
|
|
1790
|
-
// que `assistant_message`
|
|
1791
|
-
// voient la réponse s'écrire
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
})),
|
|
1804
|
+
// Fragments de réponse publiés au fil de l'eau, coalescés (voir
|
|
1805
|
+
// deltaCoalescer ci-dessus). Le réducteur les agrège dans la dernière
|
|
1806
|
+
// entrée de conversation (`assistant_delta`), que `assistant_message`
|
|
1807
|
+
// vient ensuite figer : les deux interfaces voient la réponse s'écrire
|
|
1808
|
+
// sans qu'un insert SQLite par token ne bloque le flux.
|
|
1809
|
+
onTextDelta: (delta) => deltaCoalescer.push(delta),
|
|
1810
|
+
onTextReset: () => {
|
|
1811
|
+
deltaCoalescer.reset();
|
|
1812
|
+
dispatchAgentEvent(ephemeral, createAgentEvent('assistant_delta_reset', {
|
|
1813
|
+
origin: 'runtime_turn',
|
|
1814
|
+
turnId,
|
|
1815
|
+
workspace: context.workspace ?? null,
|
|
1816
|
+
payload: {},
|
|
1817
|
+
}));
|
|
1818
|
+
},
|
|
1804
1819
|
openWikiPages,
|
|
1805
1820
|
});
|
|
1806
1821
|
} else {
|
|
1807
1822
|
ephemeral.openWikiPages = openWikiPages;
|
|
1808
1823
|
response = await runAgentTurn(agent, ephemeral, input, { messages, signal });
|
|
1809
1824
|
}
|
|
1825
|
+
// Flush the tail before the turn is finalized, then stop the timer.
|
|
1826
|
+
deltaCoalescer.flush();
|
|
1827
|
+
deltaCoalescer.dispose();
|
|
1810
1828
|
// Persist the artifact the turn may have opened/edited (template_write,
|
|
1811
1829
|
// template_read, …) back onto the long-lived session, so the next /turn —
|
|
1812
1830
|
// chat or agent — sees it. The ephemeral session is otherwise discarded.
|
package/src/core/agentEvents.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeActivity } from './activity.js';
|
|
2
|
+
import { cloneJson } from './json.js';
|
|
2
3
|
import { attachActivityToExistingPlan, syncActivitiesToPlan } from './plan.js';
|
|
3
4
|
import { applyPlanPatch, normalizePlanPatch, normalizePlanRevision, rebasePlanPatch } from './planPatch.js';
|
|
4
5
|
import { formatRuntimeLogPayload, isDispatchPlumbingLine, normalizeRuntimeLog, shortTaskLabel } from './runtimeLog.js';
|
|
@@ -1240,6 +1241,3 @@ function sortedActivities(activities) {
|
|
|
1240
1241
|
.sort((a, b) => String(a.updatedAt ?? '').localeCompare(String(b.updatedAt ?? '')));
|
|
1241
1242
|
}
|
|
1242
1243
|
|
|
1243
|
-
function cloneJson(value) {
|
|
1244
|
-
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
1245
|
-
}
|
package/src/core/buildInfo.json
CHANGED
package/src/core/googleGrants.js
CHANGED
package/src/core/json.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Deep clone via a JSON round-trip; `null`/`undefined` pass through.
|
|
3
|
+
*
|
|
4
|
+
* Three modules had grown their own byte-identical copy (agentEvents,
|
|
5
|
+
* agentRegistry, planValidator). One implementation, imported.
|
|
6
|
+
*/
|
|
7
|
+
export function cloneJson(value) {
|
|
8
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
9
|
+
}
|
package/src/core/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { managerEnvFile, managerMcpEndpointsFile, readEnvFile } from './env.js';
|
|
3
3
|
|
|
4
|
-
const WIKI_MANAGER_VERSION = '0.15.
|
|
4
|
+
const WIKI_MANAGER_VERSION = '0.15.96';
|
|
5
5
|
|
|
6
6
|
function envValue(key) {
|
|
7
7
|
const filePath = managerEnvFile();
|
package/src/core/plan.js
CHANGED
|
@@ -48,10 +48,6 @@ export function extractHeadlessPlan(text) {
|
|
|
48
48
|
return steps;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
export function matchCompletedToPlan(plan, completed) {
|
|
52
|
-
if (!plan) return;
|
|
53
|
-
syncActivitiesToPlan(plan, completed.filter((activity) => activity.terminal));
|
|
54
|
-
}
|
|
55
51
|
|
|
56
52
|
export function syncActivitiesToPlan(plan, activities) {
|
|
57
53
|
if (!plan) return;
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* which is why the shaping lives here and not in either renderer.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { TERMINAL_STATUS_SET } from '../orchestrator/taskStatuses.js';
|
|
11
|
+
|
|
10
12
|
const SYMBOLS = {
|
|
11
13
|
done: '✓',
|
|
12
14
|
running: '●',
|
|
@@ -29,7 +31,7 @@ export function selectionKindLabel(selectionKind) {
|
|
|
29
31
|
return SELECTION_KIND_LABELS[selectionKind] ?? selectionKind ?? null;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
export const TERMINAL =
|
|
34
|
+
export const TERMINAL = TERMINAL_STATUS_SET;
|
|
33
35
|
|
|
34
36
|
// Objectives are whole paragraphs; a chain view needs a line. Keep the first
|
|
35
37
|
// sentence, drop the parameter block the compiler appends, and never cut a word
|
package/src/core/toolLoop.js
CHANGED
|
@@ -38,7 +38,15 @@ export async function runBoundedToolLoop({
|
|
|
38
38
|
// livre le texte au fil de l'eau. Sans lui, la réponse finale n'apparaissait
|
|
39
39
|
// qu'une fois complète — le tour paraissait figé pendant toute sa durée.
|
|
40
40
|
const canStream = typeof onTextDelta === 'function' && typeof llm?.streamWithTools === 'function';
|
|
41
|
+
// The exact same tool + arguments called again is a loop, not progress: a
|
|
42
|
+
// model that keeps re-issuing `search("x")` will never finish, and burning
|
|
43
|
+
// the whole iteration cap on it only produced "could not finish". Track the
|
|
44
|
+
// signatures and stop as soon as a turn repeats one already executed.
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const signature = (call) => `${call?.function?.name ?? ''}\u0000${String(call?.function?.arguments ?? '')}`;
|
|
47
|
+
let iterations = 0;
|
|
41
48
|
for (let i = 0; i < cap; i += 1) {
|
|
49
|
+
iterations = i + 1;
|
|
42
50
|
onStep?.(i + 1, cap);
|
|
43
51
|
let streamedText = false;
|
|
44
52
|
const result = canStream
|
|
@@ -62,10 +70,12 @@ export async function runBoundedToolLoop({
|
|
|
62
70
|
if (calls.length === 0) {
|
|
63
71
|
return {
|
|
64
72
|
content: result?.content ?? result?.message?.content ?? '',
|
|
65
|
-
iterations
|
|
73
|
+
iterations,
|
|
66
74
|
capped: false,
|
|
67
75
|
};
|
|
68
76
|
}
|
|
77
|
+
if (calls.every((call) => seen.has(signature(call)))) break;
|
|
78
|
+
for (const call of calls) seen.add(signature(call));
|
|
69
79
|
convo.push(result.message ?? { role: 'assistant', content: result.content ?? '', tool_calls: calls });
|
|
70
80
|
// Tool calls within one turn are independent: dispatch concurrently, then
|
|
71
81
|
// replay results in the model's call order so the transcript stays stable.
|
|
@@ -77,5 +87,49 @@ export async function runBoundedToolLoop({
|
|
|
77
87
|
convo.push({ role: 'tool', tool_call_id: outcome.tool_call_id, content: outcome.content });
|
|
78
88
|
}
|
|
79
89
|
}
|
|
80
|
-
|
|
90
|
+
// Cap reached or a loop detected: ask once more WITHOUT tools for the best
|
|
91
|
+
// answer the results gathered so far support. Returning '' here is what made
|
|
92
|
+
// a long search end in a dead-end instead of the partial answer it had
|
|
93
|
+
// already collected.
|
|
94
|
+
const content = await finalAnswerWithoutTools({ llm, system, convo, canStream, onTextDelta, onTextReset, signal });
|
|
95
|
+
return { content, iterations, capped: true };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function finalAnswerWithoutTools({
|
|
99
|
+
llm,
|
|
100
|
+
system,
|
|
101
|
+
convo,
|
|
102
|
+
canStream,
|
|
103
|
+
onTextDelta,
|
|
104
|
+
onTextReset,
|
|
105
|
+
signal,
|
|
106
|
+
}) {
|
|
107
|
+
try {
|
|
108
|
+
if (canStream) {
|
|
109
|
+
let text = '';
|
|
110
|
+
const result = await llm.streamWithTools({
|
|
111
|
+
system,
|
|
112
|
+
tools: [],
|
|
113
|
+
messages: convo,
|
|
114
|
+
toolChoice: 'auto',
|
|
115
|
+
onTextDelta: (delta) => { text += delta; onTextDelta(delta); },
|
|
116
|
+
signal,
|
|
117
|
+
});
|
|
118
|
+
// A tool call despite the empty toolset is not an answer: drop whatever
|
|
119
|
+
// it streamed and let the caller fall back to its own message.
|
|
120
|
+
if (result?.tool_calls?.length) { onTextReset?.(); return ''; }
|
|
121
|
+
return String(result?.content ?? text ?? '').trim();
|
|
122
|
+
}
|
|
123
|
+
const result = await llm.completeWithTools({
|
|
124
|
+
system,
|
|
125
|
+
tools: [],
|
|
126
|
+
messages: convo,
|
|
127
|
+
toolChoice: 'auto',
|
|
128
|
+
signal,
|
|
129
|
+
});
|
|
130
|
+
if (result?.tool_calls?.length) return '';
|
|
131
|
+
return String(result?.content ?? result?.message?.content ?? '').trim();
|
|
132
|
+
} catch {
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
81
135
|
}
|
|
@@ -61,16 +61,47 @@ test('runs concurrent tool calls and replays results in call order', async () =>
|
|
|
61
61
|
assert.deepEqual(order, ['a', 'b']); // preserved model call order
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
test('
|
|
64
|
+
test('stops on a repeated identical tool call instead of burning the cap', async () => {
|
|
65
65
|
const llm = {
|
|
66
|
-
async completeWithTools() {
|
|
66
|
+
async completeWithTools({ tools }) {
|
|
67
|
+
if (tools.length === 0) return { content: 'Synthèse des résultats.', tool_calls: [] };
|
|
67
68
|
const calls = [toolCall('x', 's__status')];
|
|
68
69
|
return { message: { role: 'assistant', content: '', tool_calls: calls }, tool_calls: calls };
|
|
69
70
|
},
|
|
70
71
|
};
|
|
71
|
-
const out = await runBoundedToolLoop({
|
|
72
|
+
const out = await runBoundedToolLoop({
|
|
73
|
+
llm,
|
|
74
|
+
tools: [{ function: { name: 's__status' } }],
|
|
75
|
+
executeCall: async () => 'r',
|
|
76
|
+
maxIterations: 8,
|
|
77
|
+
});
|
|
78
|
+
assert.equal(out.capped, true);
|
|
79
|
+
// The same call twice is a loop: it stopped well before the cap.
|
|
80
|
+
assert.ok(out.iterations < 8, `expected an early stop, got ${out.iterations}`);
|
|
81
|
+
// And the turn still answers from what it gathered instead of a dead-end.
|
|
82
|
+
assert.equal(out.content, 'Synthèse des résultats.');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('answers from the gathered results when the cap is reached', async () => {
|
|
86
|
+
let round = 0;
|
|
87
|
+
const llm = {
|
|
88
|
+
async completeWithTools({ tools }) {
|
|
89
|
+
round += 1;
|
|
90
|
+
if (round <= 2 && tools.length > 0) {
|
|
91
|
+
const calls = [toolCall('x', 's__search', `{"q":"${round}"}`)];
|
|
92
|
+
return { message: { role: 'assistant', content: '', tool_calls: calls }, tool_calls: calls };
|
|
93
|
+
}
|
|
94
|
+
return { content: "Voici ce que j'ai trouvé.", tool_calls: [] };
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
const out = await runBoundedToolLoop({
|
|
98
|
+
llm,
|
|
99
|
+
tools: [{ function: { name: 's__search' } }],
|
|
100
|
+
executeCall: async () => 'r',
|
|
101
|
+
maxIterations: 2,
|
|
102
|
+
});
|
|
72
103
|
assert.equal(out.capped, true);
|
|
73
|
-
assert.equal(out.
|
|
104
|
+
assert.equal(out.content, "Voici ce que j'ai trouvé.");
|
|
74
105
|
});
|
|
75
106
|
|
|
76
107
|
test('propagates an abort thrown by executeCall', async () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createAgentEvent, dispatchAgentEvent, dispatchRuntimeLog } from '../core/agentEvents.js';
|
|
2
2
|
import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
|
|
3
|
+
import { cloneJson } from '../core/json.js';
|
|
3
4
|
import { assertContract } from '../contracts/schemas.js';
|
|
4
5
|
|
|
5
6
|
const AVAILABLE = 'available';
|
|
@@ -320,6 +321,3 @@ function cloneAgent(agent) {
|
|
|
320
321
|
};
|
|
321
322
|
}
|
|
322
323
|
|
|
323
|
-
function cloneJson(value) {
|
|
324
|
-
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
325
|
-
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validateContract } from '../contracts/schemas.js';
|
|
2
|
+
import { cloneJson } from '../core/json.js';
|
|
2
3
|
|
|
3
4
|
const SUPPORTED_CONTRACT_VERSIONS = new Set(['1']);
|
|
4
5
|
const MUTATING_OPERATIONS = new Set([
|
|
@@ -529,6 +530,3 @@ function issue(code, message, details = {}) {
|
|
|
529
530
|
return { code, message, details };
|
|
530
531
|
}
|
|
531
532
|
|
|
532
|
-
function cloneJson(value) {
|
|
533
|
-
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
534
|
-
}
|
|
@@ -43,20 +43,6 @@ import { assertContract } from '../../contracts/schemas.js';
|
|
|
43
43
|
|
|
44
44
|
export const RUNTIME_PROTOCOL_VERSION = '1';
|
|
45
45
|
|
|
46
|
-
export const RUNTIME_EVENT_TYPES = [
|
|
47
|
-
'run_created',
|
|
48
|
-
'run_started',
|
|
49
|
-
'agent_thinking',
|
|
50
|
-
'tool_started',
|
|
51
|
-
'tool_finished',
|
|
52
|
-
'subagent_started',
|
|
53
|
-
'subagent_finished',
|
|
54
|
-
'message',
|
|
55
|
-
'approval_required',
|
|
56
|
-
'run_completed',
|
|
57
|
-
'run_failed',
|
|
58
|
-
'run_cancelled',
|
|
59
|
-
];
|
|
60
46
|
|
|
61
47
|
export class RuntimeProviderUnavailableError extends Error {
|
|
62
48
|
constructor(runtime, reason) {
|