@dotdrelle/wiki-manager 0.15.93 → 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/docker-compose.yml +1 -1
- package/package.json +2 -2
- package/src/agent/graph.js +78 -26
- package/src/agent/graph.test.js +28 -2
- package/src/cli/wiki-manager.js +138 -18
- package/src/commands/slash.js +38 -4
- package/src/commands/slash.test.js +11 -1
- package/src/core/agentEvents.js +24 -3
- package/src/core/agentEvents.test.js +37 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/dockerCompose.test.js +14 -0
- 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/skillCompiler.test.js +21 -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/objectiveResolver.test.js +27 -0
- 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 +129 -0
- package/src/runtime/loginRoutes.test.js +131 -0
- package/src/runtime/loginSession.js +223 -0
- package/src/runtime/loginSession.test.js +143 -0
- package/src/runtime/qrCode.js +15 -0
- package/src/runtime/runner.js +29 -3
- package/src/runtime/runner.test.js +90 -1
- package/src/runtime/server.js +240 -1
- 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/runtime/totp.js +87 -0
- package/src/runtime/totp.test.js +80 -0
- package/src/runtime/totpLogin.js +123 -0
- package/src/runtime/vendor/qrcode.cjs +2297 -0
- package/src/shell/repl.js +7 -3
- package/src/orchestrator/.fuse_hidden0000001c00000001 +0 -316
package/src/shell/repl.js
CHANGED
|
@@ -1583,13 +1583,17 @@ async function runChatToolLoop({ input, session, history, donnaMessage, onUpdate
|
|
|
1583
1583
|
executeCall,
|
|
1584
1584
|
maxIterations: Math.min(8, Number(session?.chatAccess?.maxToolIterations) || 4),
|
|
1585
1585
|
signal: session._abortSignal,
|
|
1586
|
-
onStep: (
|
|
1586
|
+
onStep: () => onStep?.('Chat: consulting…'),
|
|
1587
1587
|
onTextDelta,
|
|
1588
1588
|
onTextReset,
|
|
1589
1589
|
});
|
|
1590
|
-
|
|
1590
|
+
// A capped turn now asks the model for a final answer without tools, so an
|
|
1591
|
+
// answer may exist even when the loop hit its limit: show it. Only fall back
|
|
1592
|
+
// to the honest limit notice when there is genuinely nothing to show.
|
|
1593
|
+
const answer = stripDsmlArtifacts(content).trim();
|
|
1594
|
+
donnaMessage.content = answer || (capped
|
|
1591
1595
|
? 'Could not finish within the chat mode iteration limit. Switch to /agent if needed.'
|
|
1592
|
-
:
|
|
1596
|
+
: formatLlmUnavailableMessage('empty response'));
|
|
1593
1597
|
onUpdate?.();
|
|
1594
1598
|
}
|
|
1595
1599
|
|
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
2
|
-
import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
|
|
3
|
-
import { normalizeRuntimeLog } from '../core/runtimeLog.js';
|
|
4
|
-
import { assertContract } from '../contracts/schemas.js';
|
|
5
|
-
|
|
6
|
-
const AVAILABLE = 'available';
|
|
7
|
-
const UNAVAILABLE = 'unavailable';
|
|
8
|
-
/**
|
|
9
|
-
* Santé d'un agent restauré depuis le journal, tant qu'aucun `agent_describe`
|
|
10
|
-
* n'a réussi dans le processus courant.
|
|
11
|
-
*
|
|
12
|
-
* Ni `available` ni `unavailable` : on ne SAIT pas. Le distinguer de
|
|
13
|
-
* `unavailable` a une conséquence pratique — un agent inconnu redevient
|
|
14
|
-
* disponible en silence dès le premier scan réussi, là où un agent déclaré
|
|
15
|
-
* indisponible mériterait d'être signalé comme tel à l'utilisateur.
|
|
16
|
-
*/
|
|
17
|
-
const UNKNOWN = 'unknown';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Un agent persisté n'est pas un agent joignable.
|
|
21
|
-
*
|
|
22
|
-
* Au redémarrage, `hydrateSession` rejoue le journal d'événements et
|
|
23
|
-
* reconstruit les agents avec la santé qu'ils avaient AU MOMENT où l'événement
|
|
24
|
-
* a été écrit. `cme-main` réapparaissait donc `available` alors que son
|
|
25
|
-
* endpoint n'existe plus, était retenu comme fournisseur, et la tâche partait
|
|
26
|
-
* vers un agent absent — la panne observée le 2026-08-04.
|
|
27
|
-
*
|
|
28
|
-
* La persistance dit ce qui a existé, pas ce qui répond maintenant. Seul un
|
|
29
|
-
* `agent_describe` réussi dans ce processus autorise à parler de disponibilité.
|
|
30
|
-
*/
|
|
31
|
-
export function markPersistedAgentsStale(session) {
|
|
32
|
-
if (!session || typeof session !== 'object') return [];
|
|
33
|
-
const stale = (agent) => ({
|
|
34
|
-
...agent,
|
|
35
|
-
health: UNKNOWN,
|
|
36
|
-
stale: true,
|
|
37
|
-
// La santé d'origine est conservée : elle raconte ce qu'on savait avant
|
|
38
|
-
// l'arrêt, ce qui aide à lire un journal, sans jamais servir au routage.
|
|
39
|
-
healthBeforeRestart: agent?.health ?? null,
|
|
40
|
-
});
|
|
41
|
-
/*
|
|
42
|
-
TOUTES les représentations restaurées, `agentRegistrySnapshot` compris.
|
|
43
|
-
|
|
44
|
-
J'avais d'abord épargné le snapshot, au motif qu'il pouvait porter un scan
|
|
45
|
-
vivant. C'était une inversion : à l'hydratation, aucun scan n'a encore eu
|
|
46
|
-
lieu — l'ordre est hydrate → invalidate → discover, et rien ne s'exécute
|
|
47
|
-
entre les deux premiers. Le snapshot vient donc de la même projection
|
|
48
|
-
persistée que `session.agents`. L'épargner laissait `cme-main` routable avec
|
|
49
|
-
son endpoint éteint, ce que la validation à chaud a montré.
|
|
50
|
-
|
|
51
|
-
Le seul scan qui compte est celui qui suivra : `discover()` réécrit le
|
|
52
|
-
snapshot en entier à partir des `agent_describe` réussis.
|
|
53
|
-
*/
|
|
54
|
-
session.agents = (session.agents ?? []).map(stale);
|
|
55
|
-
session.agentRegistrySnapshot = (session.agentRegistrySnapshot ?? []).map(stale);
|
|
56
|
-
return session.agentRegistrySnapshot;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function createAgentRegistry({
|
|
60
|
-
callTool = callMcpTool,
|
|
61
|
-
now = () => new Date(),
|
|
62
|
-
} = {}) {
|
|
63
|
-
const agentsByInstance = new Map();
|
|
64
|
-
const instanceByServer = new Map();
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
async discover(session, { signal = null } = {}) {
|
|
68
|
-
const discovered = [];
|
|
69
|
-
const endpoints = Object.entries(session?.mcp ?? {});
|
|
70
|
-
const activeServers = new Set(endpoints.map(([serverName]) => serverName));
|
|
71
|
-
for (const [serverName, endpoint] of endpoints) {
|
|
72
|
-
const agent = await discoverServerAgent(session, serverName, endpoint, { callTool, signal, now });
|
|
73
|
-
discovered.push(registerAgent(session, agent, { agentsByInstance, instanceByServer }));
|
|
74
|
-
}
|
|
75
|
-
for (const [serverName, instanceId] of instanceByServer) {
|
|
76
|
-
if (activeServers.has(serverName)) continue;
|
|
77
|
-
const previous = agentsByInstance.get(instanceId);
|
|
78
|
-
instanceByServer.delete(serverName);
|
|
79
|
-
agentsByInstance.delete(instanceId);
|
|
80
|
-
if (previous) dispatchRegistryEvent(session, 'agent.unregistered', {
|
|
81
|
-
agentInstanceId: instanceId,
|
|
82
|
-
serverName,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
session.agentRegistry = this;
|
|
86
|
-
session.agentRegistrySnapshot = this.snapshot();
|
|
87
|
-
return discovered;
|
|
88
|
-
},
|
|
89
|
-
snapshot() {
|
|
90
|
-
return [...agentsByInstance.values()]
|
|
91
|
-
.map((agent) => cloneAgent(agent))
|
|
92
|
-
.sort((a, b) => a.agentInstanceId.localeCompare(b.agentInstanceId));
|
|
93
|
-
},
|
|
94
|
-
get(agentInstanceId) {
|
|
95
|
-
const agent = agentsByInstance.get(String(agentInstanceId));
|
|
96
|
-
return agent ? cloneAgent(agent) : null;
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function discoverServerAgent(session, serverName, endpoint = {}, { callTool, signal, now }) {
|
|
102
|
-
const lastSeenAt = now().toISOString();
|
|
103
|
-
if (endpoint.status !== 'connected') {
|
|
104
|
-
return legacyAgent(serverName, endpoint, { health: UNAVAILABLE, lastSeenAt });
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const tool = findAgentDescribeTool(serverName, endpoint.tools ?? []);
|
|
108
|
-
if (!tool) {
|
|
109
|
-
return legacyAgent(serverName, endpoint, { health: AVAILABLE, lastSeenAt });
|
|
110
|
-
}
|
|
111
|
-
const toolName = tool.name;
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
const result = await callTool(
|
|
115
|
-
session.mcp,
|
|
116
|
-
serverName,
|
|
117
|
-
toolName,
|
|
118
|
-
describeArguments(tool, session?.workspace),
|
|
119
|
-
signal,
|
|
120
|
-
);
|
|
121
|
-
const description = assertContract('agentDescription', parseToolJsonResult(result));
|
|
122
|
-
return {
|
|
123
|
-
serverName,
|
|
124
|
-
toolName,
|
|
125
|
-
agentInstanceId: description.agentInstanceId,
|
|
126
|
-
description,
|
|
127
|
-
health: description.health?.status ?? AVAILABLE,
|
|
128
|
-
firstSeenAt: lastSeenAt,
|
|
129
|
-
lastSeenAt,
|
|
130
|
-
legacy: false,
|
|
131
|
-
orchestrable: true,
|
|
132
|
-
};
|
|
133
|
-
} catch (error) {
|
|
134
|
-
return legacyAgent(serverName, endpoint, {
|
|
135
|
-
health: UNAVAILABLE,
|
|
136
|
-
lastSeenAt,
|
|
137
|
-
toolName,
|
|
138
|
-
error: error instanceof Error ? error.message : String(error),
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function registerAgent(session, agent, { agentsByInstance, instanceByServer }) {
|
|
144
|
-
const previousInstanceId = instanceByServer.get(agent.serverName);
|
|
145
|
-
const previous = previousInstanceId ? agentsByInstance.get(previousInstanceId) : null;
|
|
146
|
-
|
|
147
|
-
/*
|
|
148
|
-
A failed discovery must not erase a known orchestrator agent.
|
|
149
|
-
|
|
150
|
-
When the endpoint is transiently unreachable — the runtime boots before its
|
|
151
|
-
containers (the normal boot order), or a single probe times out —
|
|
152
|
-
`discoverServerAgent` falls back to a legacy agent with no capabilities. The
|
|
153
|
-
old code replaced the orchestrator agent with that fallback, so every
|
|
154
|
-
capability silently vanished from the registry and did not come back until a
|
|
155
|
-
LATER successful discovery. Keep the orchestrator agent and only refresh its
|
|
156
|
-
probe timestamp: its capabilities are still real, only the endpoint is down.
|
|
157
|
-
*/
|
|
158
|
-
if (agent.legacy && previous && !previous.legacy) {
|
|
159
|
-
agentsByInstance.set(previous.agentInstanceId, { ...previous, lastSeenAt: agent.lastSeenAt });
|
|
160
|
-
/*
|
|
161
|
-
Preserving is right; preserving in silence is what caused the hunt.
|
|
162
|
-
|
|
163
|
-
Every defect this registry produced was invisible: capabilities vanished
|
|
164
|
-
without an event, and the resolver could only report the consequence ("no
|
|
165
|
-
agent provides X") long afterwards. Keeping the agent fixes the loss, not
|
|
166
|
-
the blindness — a probe that failed is a fact worth stating, once, where
|
|
167
|
-
the panels and the shell already read.
|
|
168
|
-
|
|
169
|
-
Deliberately NOT a health change: the endpoint is down but the agent stays
|
|
170
|
-
usable by design here, and moving `health` would make `capabilityResolver`
|
|
171
|
-
refuse it — trading a silent loss for a silent refusal.
|
|
172
|
-
*/
|
|
173
|
-
dispatchRuntimeLog(session, `agent-registry: ${agent.serverName} did not answer agent_describe`
|
|
174
|
-
+ `${agent.error ? ` (${agent.error})` : ''}; keeping its known capabilities`
|
|
175
|
-
+ ` (${(previous.description?.capabilities ?? []).map((capability) => capability.id).join(', ') || 'none'}).`);
|
|
176
|
-
return cloneAgent(previous);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const firstSeenAt = previous?.firstSeenAt ?? agent.firstSeenAt ?? agent.lastSeenAt;
|
|
180
|
-
const next = {
|
|
181
|
-
...agent,
|
|
182
|
-
firstSeenAt,
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
if (previous && previous.agentInstanceId !== next.agentInstanceId) {
|
|
186
|
-
agentsByInstance.delete(previous.agentInstanceId);
|
|
187
|
-
}
|
|
188
|
-
agentsByInstance.set(next.agentInstanceId, next);
|
|
189
|
-
instanceByServer.set(next.serverName, next.agentInstanceId);
|
|
190
|
-
|
|
191
|
-
if (!previous || previous.agentInstanceId !== next.agentInstanceId) {
|
|
192
|
-
dispatchRegistryEvent(session, 'agent.registered', { agent: next });
|
|
193
|
-
} else if (previous.health !== next.health) {
|
|
194
|
-
dispatchRegistryEvent(session, 'agent.health_changed', {
|
|
195
|
-
agent: next,
|
|
196
|
-
agentInstanceId: next.agentInstanceId,
|
|
197
|
-
previousHealth: previous.health,
|
|
198
|
-
health: next.health,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
return cloneAgent(next);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Runtime log line, emitted without importing the supervisor.
|
|
206
|
-
*
|
|
207
|
-
* `emitRuntimeLog` lives in `runtime/supervisor.js`, which already imports THIS
|
|
208
|
-
* module: importing it back would close a cycle for one log line. The event
|
|
209
|
-
* shape is the contract, not the helper, so we build it from the same
|
|
210
|
-
* normalizer the supervisor uses.
|
|
211
|
-
*/
|
|
212
|
-
function dispatchRuntimeLog(session, message) {
|
|
213
|
-
if (!session) return;
|
|
214
|
-
const payload = normalizeRuntimeLog(message, { session });
|
|
215
|
-
dispatchAgentEvent(session, createAgentEvent('runtime_log', {
|
|
216
|
-
origin: 'runtime',
|
|
217
|
-
runId: payload.runId ?? null,
|
|
218
|
-
taskId: payload.taskId ?? null,
|
|
219
|
-
workspace: payload.workspaceId ?? null,
|
|
220
|
-
payload,
|
|
221
|
-
}));
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function dispatchRegistryEvent(session, type, payload) {
|
|
225
|
-
if (!session) return;
|
|
226
|
-
dispatchAgentEvent(session, createAgentEvent(type, {
|
|
227
|
-
origin: 'agent_registry',
|
|
228
|
-
workspace: session.workspace ?? null,
|
|
229
|
-
payload,
|
|
230
|
-
}));
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function findAgentDescribeTool(serverName, tools) {
|
|
234
|
-
const named = tools.filter((tool) => String(tool?.name ?? ''));
|
|
235
|
-
const byName = (predicate) => named.find((tool) => predicate(String(tool.name)));
|
|
236
|
-
return byName((name) => name === 'agent_describe')
|
|
237
|
-
?? byName((name) => name === `${serverName}__agent_describe`)
|
|
238
|
-
?? byName((name) => name.endsWith('__agent_describe'))
|
|
239
|
-
?? null;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Parts of a contract are workspace-scoped — typically the closed vocabulary of
|
|
243
|
-
// an argument (the sources declared in THIS workspace). Published as a bare
|
|
244
|
-
// string, such a field is unverifiable and a planner fills it with any noun
|
|
245
|
-
// from the objective, so it is worth telling the agent which workspace we are
|
|
246
|
-
// asking about.
|
|
247
|
-
//
|
|
248
|
-
// But the orchestrator must not assume an agent accepts an argument it never
|
|
249
|
-
// declared: an agent whose agent_describe schema is `additionalProperties:
|
|
250
|
-
// false` REJECTS the call, drops out of the registry, and its capabilities
|
|
251
|
-
// silently vanish — the objective then resolves to whatever agent is left.
|
|
252
|
-
// Send the workspace only to agents whose own schema says they can take it.
|
|
253
|
-
function describeArguments(tool, workspace) {
|
|
254
|
-
if (!workspace) return {};
|
|
255
|
-
const schema = tool?.inputSchema;
|
|
256
|
-
if (!schema || typeof schema !== 'object') return {};
|
|
257
|
-
const declaresWorkspace = Object.hasOwn(schema.properties ?? {}, 'workspace');
|
|
258
|
-
const acceptsExtra = schema.additionalProperties !== false;
|
|
259
|
-
return declaresWorkspace || acceptsExtra ? { workspace: String(workspace) } : {};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function parseToolJsonResult(result) {
|
|
263
|
-
if (result && typeof result === 'object' && !Array.isArray(result) && !Array.isArray(result.content)) {
|
|
264
|
-
return result;
|
|
265
|
-
}
|
|
266
|
-
const text = formatMcpToolResult(result);
|
|
267
|
-
return JSON.parse(text);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function legacyAgent(serverName, endpoint = {}, { health, lastSeenAt, toolName = null, error = null }) {
|
|
271
|
-
const displayName = endpoint.displayName ?? serverName;
|
|
272
|
-
const description = {
|
|
273
|
-
contractVersion: 'legacy',
|
|
274
|
-
agentType: serverName,
|
|
275
|
-
agentInstanceId: `${serverName}-legacy`,
|
|
276
|
-
displayName,
|
|
277
|
-
capabilities: [],
|
|
278
|
-
orchestration: {
|
|
279
|
-
canPlan: false,
|
|
280
|
-
canExpandPlan: false,
|
|
281
|
-
canExecute: false,
|
|
282
|
-
canCancel: false,
|
|
283
|
-
canResume: false,
|
|
284
|
-
supportsIdempotency: false,
|
|
285
|
-
supportsParallelWorkers: false,
|
|
286
|
-
},
|
|
287
|
-
limits: {
|
|
288
|
-
recommendedConcurrency: 0,
|
|
289
|
-
maxConcurrency: 0,
|
|
290
|
-
},
|
|
291
|
-
health: { status: health },
|
|
292
|
-
};
|
|
293
|
-
return {
|
|
294
|
-
serverName,
|
|
295
|
-
toolName,
|
|
296
|
-
agentInstanceId: description.agentInstanceId,
|
|
297
|
-
description,
|
|
298
|
-
health,
|
|
299
|
-
firstSeenAt: lastSeenAt,
|
|
300
|
-
lastSeenAt,
|
|
301
|
-
legacy: true,
|
|
302
|
-
orchestrable: false,
|
|
303
|
-
error,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function cloneAgent(agent) {
|
|
308
|
-
return {
|
|
309
|
-
...agent,
|
|
310
|
-
description: cloneJson(agent.description),
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function cloneJson(value) {
|
|
315
|
-
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
316
|
-
}
|