@dotdrelle/wiki-manager 0.15.40 → 0.15.42
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/package.json +2 -2
- package/src/activity/activityAggregator.js +3 -3
- package/src/activity/progressCalculator.js +3 -4
- package/src/agent/graph.js +40 -1
- package/src/cli/wiki-manager.js +64 -38
- package/src/commands/slash.js +47 -9
- package/src/core/activity.js +3 -2
- package/src/core/agentEvents.js +68 -11
- package/src/core/agentEvents.test.js +76 -0
- package/src/core/agentLoop.js +32 -6
- package/src/core/buildInfo.json +2 -2
- package/src/core/compose.js +32 -0
- package/src/core/dockerCompose.test.js +32 -0
- package/src/core/jobQueue.js +9 -0
- package/src/core/mcp.js +1 -1
- package/src/core/otherWorkspacesRunning.test.js +51 -0
- package/src/core/plan.js +3 -2
- package/src/core/planPatch.js +2 -1
- package/src/core/profileServiceStatus.test.js +26 -2
- package/src/core/toolLoop.js +32 -7
- package/src/core/toolLoop.test.js +95 -0
- package/src/core/wikiSetup.js +50 -7
- package/src/core/wikiWorkspace.test.js +30 -0
- package/src/core/wikirc.test.js +121 -2
- package/src/core/workflow.js +11 -2
- package/src/core/workspaceInherit.js +149 -0
- package/src/core/workspaceInherit.test.js +181 -0
- package/src/graph/graphVisibilityPolicy.js +2 -2
- package/src/orchestrator/agentRegistry.js +50 -0
- package/src/orchestrator/agentRegistry.test.js +76 -1
- package/src/orchestrator/approvalPolicy.js +2 -2
- package/src/orchestrator/dependencyResolver.js +52 -12
- package/src/orchestrator/dispatcher.js +2 -5
- package/src/orchestrator/planIntegrator.js +5 -5
- package/src/orchestrator/resultAggregator.js +2 -1
- package/src/orchestrator/scheduler.test.js +62 -1
- package/src/orchestrator/taskStatuses.js +99 -0
- package/src/orchestrator/taskStatuses.test.js +112 -0
- package/src/runtime/delegation.js +158 -0
- package/src/runtime/delegation.test.js +281 -0
- package/src/runtime/recoveryManager.js +2 -5
- package/src/runtime/recoveryManager.test.js +5 -1
- package/src/runtime/runner.js +129 -15
- package/src/runtime/runner.test.js +153 -6
- package/src/runtime/server.test.js +28 -0
- package/src/runtime/store.js +16 -1
- package/src/runtime/store.test.js +50 -0
- package/src/shell/repl.js +21 -11
- package/src/shell/setupWizardModality.test.js +55 -0
- package/src/shell/tui.tsx +29 -9
- package/wiki-workspace +10 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.42",
|
|
4
4
|
"description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
|
|
5
5
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
6
6
|
"author": "dotrelle",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"start": "bun ./bin/wiki-manager.js",
|
|
14
|
-
"test": "node --test src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/profileServiceStatus.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/runtimeLog.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikirc.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/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/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardPlaceholders.test.js src/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/controlMessages.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/donna-contract.test.js src/runtime/auth.test.js",
|
|
14
|
+
"test": "node --test src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/profileServiceStatus.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/runtimeLog.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/otherWorkspacesRunning.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.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/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/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/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/donna-contract.test.js src/runtime/auth.test.js",
|
|
15
15
|
"check-versions": "node scripts/check-versions.js",
|
|
16
16
|
"prepack": "node scripts/check-versions.js",
|
|
17
17
|
"prepublishOnly": "node scripts/check-versions.js",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { calculateWeightedProgress } from './progressCalculator.js';
|
|
2
2
|
import { deduplicateActivities } from './activityDeduplicator.js';
|
|
3
3
|
import { initialSynthesisFromState } from './runSynthesis.js';
|
|
4
|
+
import { isSuccessful } from '../orchestrator/taskStatuses.js';
|
|
4
5
|
|
|
5
|
-
const DONE = new Set(['done', 'complete', 'completed', 'success', 'succeeded']);
|
|
6
6
|
const ACTIVE = new Set(['running', 'starting', 'queued']);
|
|
7
7
|
|
|
8
8
|
export function aggregateActivity(state = {}, events = []) {
|
|
@@ -67,7 +67,7 @@ function groupLabel(task) {
|
|
|
67
67
|
|
|
68
68
|
function groupLine(group, activities) {
|
|
69
69
|
const total = group.tasks.length;
|
|
70
|
-
const done = group.tasks.filter((task) =>
|
|
70
|
+
const done = group.tasks.filter((task) => isSuccessful(statusOf(task))).length;
|
|
71
71
|
const running = group.tasks.filter((task) => ACTIVE.has(statusOf(task)));
|
|
72
72
|
const failed = group.tasks.find((task) => statusOf(task) === 'failed');
|
|
73
73
|
const waitingApproval = group.tasks.some((task) => ['pending_approval', 'waiting_approval'].includes(statusOf(task)));
|
|
@@ -76,7 +76,7 @@ function groupLine(group, activities) {
|
|
|
76
76
|
// the whole group as "validation 0%" while a worker visibly runs at 35%.
|
|
77
77
|
const activePair = group.tasks
|
|
78
78
|
.map((task) => ({ task, activity: activityForTask(task, activities) }))
|
|
79
|
-
.find(({ activity }) => activity && !activity.terminal && !
|
|
79
|
+
.find(({ activity }) => activity && !activity.terminal && !isSuccessful(statusOf(activity)));
|
|
80
80
|
const activeTask = activePair?.task ?? running[0] ?? null;
|
|
81
81
|
const activeActivity = activePair?.activity
|
|
82
82
|
?? running.map((task) => activityForTask(task, activities)).find(Boolean);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
const TERMINAL_ANY = new Set([...TERMINAL_DONE, 'failed', 'cancelled', 'canceled', 'error']);
|
|
1
|
+
import { isSuccessful, isTerminal } from '../orchestrator/taskStatuses.js';
|
|
3
2
|
|
|
4
3
|
export function calculateWeightedProgress(tasks = [], activities = []) {
|
|
5
4
|
const items = Array.isArray(tasks) ? tasks : [];
|
|
@@ -10,10 +9,10 @@ export function calculateWeightedProgress(tasks = [], activities = []) {
|
|
|
10
9
|
for (const task of items) {
|
|
11
10
|
const weight = taskWeight(task);
|
|
12
11
|
const status = normalizeStatus(task.status);
|
|
13
|
-
if (
|
|
12
|
+
if (isSuccessful(status)) {
|
|
14
13
|
completedWeight += weight;
|
|
15
14
|
done += 1;
|
|
16
|
-
} else if (!
|
|
15
|
+
} else if (!isTerminal(status)) {
|
|
17
16
|
completedWeight += weight * taskProgressRatio(task, activities);
|
|
18
17
|
}
|
|
19
18
|
}
|
package/src/agent/graph.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @statuses-vocabulary
|
|
3
|
+
*
|
|
4
|
+
* JSON schema enumeration exposed to models. A schema declares what it
|
|
5
|
+
* accepts, including values the orchestrator no longer produces.
|
|
6
|
+
*
|
|
7
|
+
* Declared here rather than in a central exception list so the waiver
|
|
8
|
+
* travels with the code it excuses (see orchestrator/taskStatuses.test.js).
|
|
9
|
+
*/
|
|
10
|
+
import { isTerminal } from '../orchestrator/taskStatuses.js';
|
|
1
11
|
import { join } from 'node:path';
|
|
2
12
|
import { Annotation, END, START, StateGraph } from '@langchain/langgraph';
|
|
3
13
|
import {
|
|
@@ -640,7 +650,7 @@ function rememberProductionProgress(session, payload, label) {
|
|
|
640
650
|
jobId: jobId ?? session.productionActivity?.jobId ?? null,
|
|
641
651
|
status,
|
|
642
652
|
label: label ?? `Production: ${status}`,
|
|
643
|
-
terminal:
|
|
653
|
+
terminal: isTerminal(status),
|
|
644
654
|
updatedAt: new Date().toISOString(),
|
|
645
655
|
};
|
|
646
656
|
}
|
|
@@ -804,6 +814,35 @@ async function handleRuntimeControlTool(session, tool, args = {}) {
|
|
|
804
814
|
if (connectorConfig) {
|
|
805
815
|
return `Delegation rejected: ${connectorConfig.serverName} advertises no setup or authentication tool. Do not call an unrelated data tool and do not delegate to export. Explain conversationally that authentication must be completed outside MCP, using only configuration instructions already available in the current context.`;
|
|
806
816
|
}
|
|
817
|
+
/*
|
|
818
|
+
Un run ne passe pas par le réseau pour se déléguer à lui-même.
|
|
819
|
+
|
|
820
|
+
Donna délègue DEPUIS l'intérieur du run conversationnel qui vient d'être
|
|
821
|
+
marqué actif. Passer par POST /delegate revenait à demander au runtime
|
|
822
|
+
l'autorisation de démarrer un run alors qu'un run tourne déjà — le sien —
|
|
823
|
+
et l'endpoint répondait 409, à juste titre de son point de vue. Le run
|
|
824
|
+
refusait sa propre délégation.
|
|
825
|
+
|
|
826
|
+
Déléguer n'est pas démarrer un second run : c'est faire passer celui-ci
|
|
827
|
+
de la décision à l'exécution. Quand ce chemin interne existe (agent
|
|
828
|
+
exécuté dans le processus du runtime), on l'emprunte : même `runId`,
|
|
829
|
+
aucun run concurrent, aucun 409. Le chemin HTTP reste pour les appelants
|
|
830
|
+
réellement extérieurs — le Shell, un client tiers —, et c'est là que le
|
|
831
|
+
409 garde tout son sens.
|
|
832
|
+
*/
|
|
833
|
+
if (typeof session?._delegateWithinRun === 'function') {
|
|
834
|
+
try {
|
|
835
|
+
const inRun = await session._delegateWithinRun(objective);
|
|
836
|
+
return JSON.stringify({
|
|
837
|
+
delegated: true,
|
|
838
|
+
runId: inRun.runId,
|
|
839
|
+
summary: inRun.summary ?? null,
|
|
840
|
+
message: `Action lancée (${String(inRun.runId).slice(0, 8)}) après validation du plan réel : ${inRun.summary?.tasks ?? 0} tâche(s), ${inRun.summary?.agent ?? 'agent résolu'}. Exécution en cours.`,
|
|
841
|
+
});
|
|
842
|
+
} catch (err) {
|
|
843
|
+
return `Délégation refusée : ${err instanceof Error ? err.message : String(err)}`;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
807
846
|
const result = await postRuntimeDelegate(objective, { url, workspace });
|
|
808
847
|
return result?.runId
|
|
809
848
|
? JSON.stringify({
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @statuses-vocabulary
|
|
3
|
+
*
|
|
4
|
+
* JOB statuses as reported by headless polling, mapped to a process exit
|
|
5
|
+
* code — a different contract from the orchestrator's task vocabulary.
|
|
6
|
+
*
|
|
7
|
+
* Declared here rather than in a central exception list so the waiver
|
|
8
|
+
* travels with the code it excuses (see orchestrator/taskStatuses.test.js).
|
|
9
|
+
*/
|
|
1
10
|
import { randomUUID } from 'node:crypto';
|
|
2
11
|
import { spawnSync } from 'node:child_process';
|
|
3
12
|
import { readFileSync } from 'node:fs';
|
|
@@ -297,11 +306,28 @@ export function createInteractiveSession(context, { runtimeUrl, turnId, signal =
|
|
|
297
306
|
return session;
|
|
298
307
|
}
|
|
299
308
|
|
|
309
|
+
/*
|
|
310
|
+
Un tour interactif se termine TOUJOURS par un assistant_message.
|
|
311
|
+
|
|
312
|
+
C'est la condition de fin que les deux interfaces attendent : côté `serve`, la
|
|
313
|
+
bulle « Request received · Donna is preparing… » n'est retirée que lorsqu'un
|
|
314
|
+
message assistant non vide arrive. La garde `!content` renvoyait donc `false`
|
|
315
|
+
en silence quand le tour ne produisait rien — modèle qui répond vide, boucle
|
|
316
|
+
d'outils qui s'arrête sans conclure — et le point d'attente tournait
|
|
317
|
+
indéfiniment, sans erreur nulle part.
|
|
318
|
+
|
|
319
|
+
Une réponse vide est un résultat, pas une raison de ne rien dire.
|
|
320
|
+
*/
|
|
300
321
|
export function ensureInteractiveAssistantMessage(session, response, { turnId, workspace } = {}) {
|
|
322
|
+
if (session.agentEvents.some((event) => event.type === 'assistant_message')) return false;
|
|
301
323
|
const content = String(response ?? '').trim();
|
|
302
|
-
if (!content || session.agentEvents.some((event) => event.type === 'assistant_message')) return false;
|
|
303
324
|
dispatchAgentEvent(session, createAgentEvent('assistant_message', {
|
|
304
|
-
origin: 'runtime_turn',
|
|
325
|
+
origin: 'runtime_turn',
|
|
326
|
+
turnId,
|
|
327
|
+
workspace,
|
|
328
|
+
payload: {
|
|
329
|
+
content: content || 'No answer was produced for this turn. The model returned nothing — try rephrasing, or switch to /agent if the request needs an action.',
|
|
330
|
+
},
|
|
305
331
|
}));
|
|
306
332
|
return true;
|
|
307
333
|
}
|
|
@@ -317,17 +343,7 @@ export async function forwardRuntimeApproval(getWorkspaceContext, request = {})
|
|
|
317
343
|
return context.approvalManager?.approve(request) ?? { approved: false };
|
|
318
344
|
}
|
|
319
345
|
|
|
320
|
-
export
|
|
321
|
-
autoApprove = false,
|
|
322
|
-
approvalManager = null,
|
|
323
|
-
runId,
|
|
324
|
-
} = {}) {
|
|
325
|
-
if (autoApprove !== true || typeof approvalManager?.approve !== 'function') {
|
|
326
|
-
return { approved: false, awaitingApproval: true };
|
|
327
|
-
}
|
|
328
|
-
const result = approvalManager.approve({ scope: 'run', runId });
|
|
329
|
-
return { approved: true, awaitingApproval: false, result };
|
|
330
|
-
}
|
|
346
|
+
export { resolvePreparedDelegationApproval } from '../runtime/delegation.js';
|
|
331
347
|
|
|
332
348
|
function timestampForFile() {
|
|
333
349
|
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
@@ -1168,39 +1184,33 @@ async function runRuntime(argv, agent) {
|
|
|
1168
1184
|
: undefined;
|
|
1169
1185
|
supervisor?.setRunSignal(signal);
|
|
1170
1186
|
session._onStep = (message) => emitRuntimeLog(session, message);
|
|
1171
|
-
|
|
1172
|
-
const {
|
|
1173
|
-
|
|
1174
|
-
|
|
1187
|
+
session._delegateWithinRun = async (objective) => {
|
|
1188
|
+
const { delegateWithinRun } = await import('../runtime/delegation.js');
|
|
1189
|
+
return delegateWithinRun(session, objective, {
|
|
1190
|
+
prepare: ({ objective: goal }) => prepareDelegation(context, { objective: goal }),
|
|
1175
1191
|
registry: capabilityRegistryForSession(session),
|
|
1176
|
-
session,
|
|
1177
1192
|
store,
|
|
1178
|
-
|
|
1179
|
-
|
|
1193
|
+
approvalManager: context.approvalManager,
|
|
1194
|
+
autoApprove: body.autoApprove === true,
|
|
1180
1195
|
});
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1196
|
+
};
|
|
1197
|
+
if (body.preparedDelegation?.fragment) {
|
|
1198
|
+
const { integratePreparedDelegation } = await import('../runtime/delegation.js');
|
|
1199
|
+
const prepared = body.preparedDelegation;
|
|
1185
1200
|
// Real approval gate (opt-out): a directly-delegated run only skips the
|
|
1186
1201
|
// human approval step when the caller explicitly opts in via
|
|
1187
1202
|
// `autoApprove` (e.g. headless/CI, or a future "trust this run" toggle).
|
|
1188
|
-
// By default the run WAITS
|
|
1189
|
-
//
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
// could ever render.
|
|
1194
|
-
const approval = resolvePreparedDelegationApproval({
|
|
1195
|
-
autoApprove: body.autoApprove,
|
|
1196
|
-
approvalManager: context.approvalManager,
|
|
1203
|
+
// By default the run WAITS, so the pending_approval window stays
|
|
1204
|
+
// visible instead of being resolved ~30ms after launch.
|
|
1205
|
+
integratePreparedDelegation({
|
|
1206
|
+
session,
|
|
1207
|
+
store,
|
|
1197
1208
|
runId,
|
|
1209
|
+
prepared,
|
|
1210
|
+
registry: capabilityRegistryForSession(session),
|
|
1211
|
+
approvalManager: context.approvalManager,
|
|
1212
|
+
autoApprove: body.autoApprove === true,
|
|
1198
1213
|
});
|
|
1199
|
-
if (approval.approved) {
|
|
1200
|
-
emitRuntimeLog(session, `approval: run ${runId} auto-approved (autoApprove opt-in)`);
|
|
1201
|
-
} else {
|
|
1202
|
-
emitRuntimeLog(session, `approval: run ${runId} awaiting explicit approval before mutations (/approve or « valide tout »)`);
|
|
1203
|
-
}
|
|
1204
1214
|
body._planReady?.resolve?.({ runId, planRevision: session.agentProjection?.planRevision ?? 0 });
|
|
1205
1215
|
}
|
|
1206
1216
|
// Deterministic capability run (/ingest): ask the capable agent for its
|
|
@@ -1372,6 +1382,22 @@ async function runRuntime(argv, agent) {
|
|
|
1372
1382
|
response = await runHeadlessChatTurn(ephemeral, input, {
|
|
1373
1383
|
history,
|
|
1374
1384
|
onStep: ephemeral._onStep,
|
|
1385
|
+
// Fragments de réponse publiés au fil de l'eau. Le réducteur les
|
|
1386
|
+
// agrège dans la dernière entrée de conversation (`assistant_delta`),
|
|
1387
|
+
// que `assistant_message` vient ensuite figer : les deux interfaces
|
|
1388
|
+
// voient la réponse s'écrire, au lieu d'attendre le tour complet.
|
|
1389
|
+
onTextDelta: (delta) => dispatchAgentEvent(ephemeral, createAgentEvent('assistant_delta', {
|
|
1390
|
+
origin: 'runtime_turn',
|
|
1391
|
+
turnId,
|
|
1392
|
+
workspace: context.workspace ?? null,
|
|
1393
|
+
payload: { delta },
|
|
1394
|
+
})),
|
|
1395
|
+
onTextReset: () => dispatchAgentEvent(ephemeral, createAgentEvent('assistant_delta_reset', {
|
|
1396
|
+
origin: 'runtime_turn',
|
|
1397
|
+
turnId,
|
|
1398
|
+
workspace: context.workspace ?? null,
|
|
1399
|
+
payload: {},
|
|
1400
|
+
})),
|
|
1375
1401
|
// UI context from `wiki serve`: up to five selected wiki or raw
|
|
1376
1402
|
// documents. Only paths are prompted; Donna reads through tools.
|
|
1377
1403
|
openWikiPages: body.context?.openWikiPages ?? body.context?.openWikiPage,
|
package/src/commands/slash.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @statuses-vocabulary
|
|
3
|
+
*
|
|
4
|
+
* CONTROL QUEUE item statuses, which include `expired`. A control request
|
|
5
|
+
* is not a task and does not share its lifecycle.
|
|
6
|
+
*
|
|
7
|
+
* Declared here rather than in a central exception list so the waiver
|
|
8
|
+
* travels with the code it excuses (see orchestrator/taskStatuses.test.js).
|
|
9
|
+
*/
|
|
10
|
+
import { isTerminal } from '../orchestrator/taskStatuses.js';
|
|
1
11
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
12
|
import { openExternalUrl } from '../shell/openExternal.js';
|
|
3
13
|
import { classifyCommandFailure, failureHint, rawFailureText } from '../core/commandFailure.js';
|
|
4
14
|
import { join, relative } from 'node:path';
|
|
5
|
-
import { composeServices, listServices, runWikiCli, serviceLogs, serviceNames, serviceStates, startService, stopService } from '../core/compose.js';
|
|
15
|
+
import { composeServices, listServices, otherWorkspacesRunning, runWikiCli, serviceLogs, serviceNames, serviceStates, startService, stopService } from '../core/compose.js';
|
|
6
16
|
import { agentServiceNames, profileServiceStatus } from '../core/agentsCompose.js';
|
|
7
17
|
import { GOOGLE_GRANTS, GOOGLE_GRANT_LABELS, defaultGoogleGrants } from '../core/googleGrants.js';
|
|
8
18
|
import {
|
|
@@ -558,12 +568,23 @@ async function createWorkspaceCommand(context, workspaceName, targetPath) {
|
|
|
558
568
|
try {
|
|
559
569
|
context.onStep?.(`Workspace: creating ${workspaceName}…`);
|
|
560
570
|
const output = await createWorkspace(workspaceName, targetPath, { timeout: 600_000 });
|
|
561
|
-
|
|
571
|
+
// Seed the new workspace from the one in use: the LLM endpoint, key and
|
|
572
|
+
// model are almost always the same, and re-entering them by hand was the
|
|
573
|
+
// first thing to do after every /new.
|
|
574
|
+
const { inherited } = await finalizeCreatedWorkspace(workspaceName, {
|
|
575
|
+
inheritFrom: context.session?.workspace ?? null,
|
|
576
|
+
});
|
|
562
577
|
return {
|
|
563
578
|
output: [
|
|
564
579
|
output,
|
|
565
580
|
'',
|
|
566
581
|
`Workspace created: ${workspaceName}`,
|
|
582
|
+
// State what was carried over. Inheriting silently would make a wrong
|
|
583
|
+
// endpoint look like a scaffold default and send the operator hunting
|
|
584
|
+
// in the wrong file.
|
|
585
|
+
inherited.length > 0
|
|
586
|
+
? `Inherited from ${context.session.workspace}: ${inherited.join(', ')}`
|
|
587
|
+
: null,
|
|
567
588
|
`Use /use ${workspaceName} to load it.`,
|
|
568
589
|
].filter(Boolean).join('\n'),
|
|
569
590
|
};
|
|
@@ -715,7 +736,7 @@ ${helpPair('/use <workspace>', 'Use workspace', '/status', 'Session status')}
|
|
|
715
736
|
${helpPair('/config list', 'Config profiles', '/config use <n>', 'Use config')}
|
|
716
737
|
${helpPair('/config edit <n>', 'Edit config', '/workspace delete <n>', 'Delete workspace')}
|
|
717
738
|
${helpPair('/services', 'Services', '/start [all|agents|services]', 'all = services + agents')}
|
|
718
|
-
${helpPair('/stop [all|service|agents]', 'Stop service(s)', '/logs <service>', 'Service logs')}
|
|
739
|
+
${helpPair('/stop [all|everything|service|agents]', 'Stop service(s)', '/logs <service>', 'Service logs')}
|
|
719
740
|
${helpPair('/skills', 'List skills', '/skills show <n>', 'Show skill')}
|
|
720
741
|
${helpPair('/skills run <n>', 'Run skill guide', '/skills edit <n>', 'Edit skill')}
|
|
721
742
|
${helpPair('/mcp status', 'MCP status', '/mcp endpoints', 'MCP endpoints')}
|
|
@@ -850,7 +871,7 @@ function formatRuntimeRunStatus(state) {
|
|
|
850
871
|
? state.controlQueue.filter((item) => item.status === 'queued').length
|
|
851
872
|
: 0;
|
|
852
873
|
const tasks = Array.isArray(state?.workflow?.nodes)
|
|
853
|
-
? state.workflow.nodes.filter((node) => node.type === 'task' && !
|
|
874
|
+
? state.workflow.nodes.filter((node) => node.type === 'task' && !isTerminal(node.status)).length
|
|
854
875
|
: 0;
|
|
855
876
|
return `runtime: ${status}${runId} · queued=${queued} · activeTasks=${tasks}`;
|
|
856
877
|
}
|
|
@@ -1118,14 +1139,31 @@ export async function handleSlashCommand(line, context) {
|
|
|
1118
1139
|
// compris. Il ne stoppait que les services du workspace et laissait les
|
|
1119
1140
|
// agents debout — donc `/start all` puis `/stop all` ne revenait pas à
|
|
1120
1141
|
// l'état de départ.
|
|
1121
|
-
|
|
1122
|
-
|
|
1142
|
+
//
|
|
1143
|
+
// Cette symétrie ne tient que tant qu'un seul workspace tourne. Les
|
|
1144
|
+
// agents externes sont UNE pile partagée : les arrêter depuis un
|
|
1145
|
+
// workspace coupait les autres, qui n'avaient rien demandé et ne
|
|
1146
|
+
// voyaient qu'une panne. « all » reste donc « toute ma pile », et les
|
|
1147
|
+
// agents ne tombent que s'ils ne servent plus personne. `/stop
|
|
1148
|
+
// everything` garde la coupure franche, explicitement demandée.
|
|
1149
|
+
const stopsEverything = service === 'everything';
|
|
1150
|
+
const stopsAgents = service === 'all' || stopsEverything;
|
|
1151
|
+
const stopTarget = service === 'services' || stopsEverything ? undefined : service;
|
|
1123
1152
|
try {
|
|
1124
|
-
step(`Services: stopping ${service ?? 'workspace services'}…`);
|
|
1153
|
+
step(`Services: stopping ${stopsEverything ? 'all workspaces and agents' : (service ?? 'workspace services')}…`);
|
|
1125
1154
|
await stopService(context.session, stopTarget);
|
|
1126
1155
|
if (stopsAgents) {
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1156
|
+
const busy = stopsEverything
|
|
1157
|
+
? []
|
|
1158
|
+
: await otherWorkspacesRunning(context.session, listWorkspaces());
|
|
1159
|
+
if (busy.length > 0) {
|
|
1160
|
+
// Say who is holding them, and how to override. A silent skip
|
|
1161
|
+
// would look exactly like the bug we just fixed.
|
|
1162
|
+
step(`Services: agents left running for ${busy.join(', ')} — use /stop everything to stop them anyway.`);
|
|
1163
|
+
} else {
|
|
1164
|
+
const agentsResult = await runAgentCommand(stopAgents, 'stop');
|
|
1165
|
+
if (agentsResult?.failed) return agentsResult;
|
|
1166
|
+
}
|
|
1129
1167
|
}
|
|
1130
1168
|
step('Services: refreshing MCP runtime…');
|
|
1131
1169
|
await refreshMcpRuntimeStatus(context.session);
|
package/src/core/activity.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validateContractInDev } from '../contracts/schemas.js';
|
|
2
|
+
import { isTerminal, isUnsuccessfulTerminal } from '../orchestrator/taskStatuses.js';
|
|
2
3
|
|
|
3
4
|
export function parseJsonText(text) {
|
|
4
5
|
try {
|
|
@@ -13,7 +14,7 @@ function basename(value) {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
function terminalStatus(status) {
|
|
16
|
-
return
|
|
17
|
+
return isTerminal(status);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function activityKey(activity) {
|
|
@@ -251,7 +252,7 @@ export function newNonTerminalActivities(snapshotBefore, session) {
|
|
|
251
252
|
|
|
252
253
|
export function terminalFailures(activities) {
|
|
253
254
|
return activities.filter(
|
|
254
|
-
(a) => a.terminal &&
|
|
255
|
+
(a) => a.terminal && isUnsuccessfulTerminal(a.status),
|
|
255
256
|
);
|
|
256
257
|
}
|
|
257
258
|
|
package/src/core/agentEvents.js
CHANGED
|
@@ -4,6 +4,7 @@ import { applyPlanPatch, normalizePlanPatch, normalizePlanRevision, rebasePlanPa
|
|
|
4
4
|
import { formatRuntimeLogPayload } from './runtimeLog.js';
|
|
5
5
|
import { projectWorkflow } from './workflow.js';
|
|
6
6
|
import { validateContractInDev } from '../contracts/schemas.js';
|
|
7
|
+
import { isTerminal, isSuccessful, isUnknownStatus, normalizeTaskStatus } from '../orchestrator/taskStatuses.js';
|
|
7
8
|
|
|
8
9
|
const SESSION_PROJECTION_EVENTS = new Set([
|
|
9
10
|
'run_started',
|
|
@@ -267,6 +268,9 @@ function applyEvent(state, event) {
|
|
|
267
268
|
case 'assistant_delta':
|
|
268
269
|
appendAssistantDelta(state, String(event.payload?.delta ?? ''));
|
|
269
270
|
return;
|
|
271
|
+
case 'assistant_delta_reset':
|
|
272
|
+
discardStreamingAssistantMessage(state);
|
|
273
|
+
return;
|
|
270
274
|
case 'tool_call_started':
|
|
271
275
|
state.chain.push({
|
|
272
276
|
type: 'tool',
|
|
@@ -343,7 +347,13 @@ function applyEvent(state, event) {
|
|
|
343
347
|
state.planRevision = normalizePlanRevision(event.payload?.planRevision ?? state.planRevision + 1);
|
|
344
348
|
return;
|
|
345
349
|
case 'plan_step_updated':
|
|
346
|
-
|
|
350
|
+
{
|
|
351
|
+
// L'anomalie remonte par la valeur de retour plutôt que par une
|
|
352
|
+
// référence au state : `updatePlanStep` reste une fonction sur un
|
|
353
|
+
// plan, et le journal reste la responsabilité de l'appelant.
|
|
354
|
+
const anomaly = updatePlanStep(state.plan, event.payload ?? {});
|
|
355
|
+
if (anomaly) state.logs.push(anomaly);
|
|
356
|
+
}
|
|
347
357
|
return;
|
|
348
358
|
case 'control_message_received':
|
|
349
359
|
state.logs.push(`Control message: ${String(event.payload?.input ?? '')}`);
|
|
@@ -659,6 +669,26 @@ function appendAssistantDelta(state, delta) {
|
|
|
659
669
|
}
|
|
660
670
|
}
|
|
661
671
|
|
|
672
|
+
/*
|
|
673
|
+
Jeter une réponse en cours d'écriture, sans retirer son entrée.
|
|
674
|
+
|
|
675
|
+
Une itération de la boucle d'outils peut produire du texte puis décider
|
|
676
|
+
d'appeler un outil : ce texte est un raisonnement intermédiaire que le tour
|
|
677
|
+
suivant remplace, il ne doit pas rester à l'écran.
|
|
678
|
+
|
|
679
|
+
L'entrée est vidée, jamais dépilée. La réconciliation de `serve`
|
|
680
|
+
(`chatHtml.ts`) suppose une conversation en ajout seul — « le serveur ne mute
|
|
681
|
+
que la dernière entrée, tout ce qui précède est acquis » — et n'indexe la
|
|
682
|
+
boucle que sur la longueur croissante. Un `pop` la ferait passer sous le
|
|
683
|
+
nombre de références déjà rendues : l'élément DOM en trop resterait affiché
|
|
684
|
+
avec le texte qu'on voulait justement effacer, et tous les messages suivants
|
|
685
|
+
se décaleraient d'un cran.
|
|
686
|
+
*/
|
|
687
|
+
function discardStreamingAssistantMessage(state) {
|
|
688
|
+
const last = state.conversation.at(-1);
|
|
689
|
+
if (last?.role === 'assistant' && last.streaming) last.content = '';
|
|
690
|
+
}
|
|
691
|
+
|
|
662
692
|
function finalizeAssistantMessage(state, content) {
|
|
663
693
|
const last = state.conversation.at(-1);
|
|
664
694
|
if (last?.role === 'assistant' && last.streaming) {
|
|
@@ -725,7 +755,7 @@ function markCoveredApprovalsApproved(approvals, grant, ts) {
|
|
|
725
755
|
|
|
726
756
|
function cancelPendingPlanSteps(plan) {
|
|
727
757
|
for (const step of plan ?? []) {
|
|
728
|
-
if (!
|
|
758
|
+
if (!isTerminal(step.status)) step.status = 'cancelled';
|
|
729
759
|
}
|
|
730
760
|
}
|
|
731
761
|
|
|
@@ -852,24 +882,51 @@ function normalizePlanTask(raw, index, { owner = 'orchestrator', ownerActivityKe
|
|
|
852
882
|
}
|
|
853
883
|
|
|
854
884
|
function updatePlanStep(plan, payload) {
|
|
855
|
-
|
|
885
|
+
let anomaly = null;
|
|
886
|
+
if (!plan) return anomaly;
|
|
856
887
|
const requestedTaskId = payload.taskId ?? payload.id ?? payload.targetTaskId;
|
|
857
888
|
const step = requestedTaskId != null
|
|
858
889
|
? plan.find((item) => String(item.id ?? item.step) === String(requestedTaskId))
|
|
859
890
|
: plan.find((item) => item.step === Number(payload.step));
|
|
860
|
-
if (!step) return;
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
891
|
+
if (!step) return anomaly;
|
|
892
|
+
/*
|
|
893
|
+
Un statut inconnu ne vaut pas « réussi ».
|
|
894
|
+
|
|
895
|
+
La cascade se terminait par `else step.status = 'done'` : tout statut non
|
|
896
|
+
énuméré — `skipped`, par exemple — était projeté en succès. Le runner
|
|
897
|
+
marquait bien une tâche ignorée, la projection la déclarait faite, et le
|
|
898
|
+
résumé de run comptait une réussite qui n'a jamais eu lieu. Le défaut le
|
|
899
|
+
plus dangereux est celui qui transforme une inconnue en bonne nouvelle.
|
|
900
|
+
|
|
901
|
+
Trois cas, et un seul mène à `done` :
|
|
902
|
+
|
|
903
|
+
- un statut reconnu par le vocabulaire commun est repris tel quel, alias
|
|
904
|
+
compris (`succeeded` → `done`, `error` → `failed`) ;
|
|
905
|
+
- l'ABSENCE de statut garde le contrat historique — un événement de fin
|
|
906
|
+
sans précision signifie « terminé » ;
|
|
907
|
+
- un statut présent mais incompréhensible laisse l'étape dans l'état où
|
|
908
|
+
elle était, et signale une anomalie de projection. On ne sait pas ce qui
|
|
909
|
+
s'est passé : le dire est plus utile que d'inventer une réponse.
|
|
910
|
+
*/
|
|
911
|
+
const canonical = normalizeTaskStatus(payload.status);
|
|
912
|
+
if (canonical) {
|
|
913
|
+
step.status = canonical;
|
|
914
|
+
} else if (isUnknownStatus(payload.status)) {
|
|
915
|
+
anomaly = `projection: unknown status "${String(payload.status)}" for plan step ${String(requestedTaskId ?? payload.step ?? '?')} — kept "${String(step.status ?? 'pending')}"`;
|
|
916
|
+
} else {
|
|
917
|
+
step.status = 'done';
|
|
918
|
+
}
|
|
919
|
+
// Le motif d'un abandon est la seule chose qui le rende actionnable :
|
|
920
|
+
// « ignorée » sans « parce que » n'apprend rien à qui relance.
|
|
921
|
+
if (step.status === 'skipped' && payload.reason && !step.error) {
|
|
922
|
+
step.error = { code: 'dependency_failed', message: String(payload.reason) };
|
|
923
|
+
}
|
|
868
924
|
if (payload.activityKey) step.activityKey = payload.activityKey;
|
|
869
925
|
if (Array.isArray(payload.outputRefs)) step.outputRefs = payload.outputRefs.map(cloneRef);
|
|
870
926
|
if (payload.result) step.result = cloneJson(payload.result);
|
|
871
927
|
if (payload.retryState) step.retryState = cloneJson(payload.retryState);
|
|
872
928
|
if (payload.retryAssignment) step.retryAssignment = cloneJson(payload.retryAssignment);
|
|
929
|
+
return anomaly;
|
|
873
930
|
}
|
|
874
931
|
|
|
875
932
|
function formatPlanErrors(errors) {
|
|
@@ -547,3 +547,79 @@ test('run_error cancels pending plan steps and active activities (no ghosts at r
|
|
|
547
547
|
assert.equal(activity.status, 'cancelled');
|
|
548
548
|
assert.equal(activity.terminal, true);
|
|
549
549
|
});
|
|
550
|
+
|
|
551
|
+
/*
|
|
552
|
+
La cascade de statuts se terminait par `else step.status = 'done'` : un statut
|
|
553
|
+
non énuméré était projeté en succès. Le runner marquait une tâche `skipped`
|
|
554
|
+
faute de dépendance, la projection la déclarait faite, et le résumé comptait
|
|
555
|
+
une réussite qui n'avait jamais eu lieu — le pire des défauts, celui qui
|
|
556
|
+
transforme une inconnue en bonne nouvelle.
|
|
557
|
+
*/
|
|
558
|
+
test('reduceAgentEvents: un statut de plan inconnu ne devient pas un succès', () => {
|
|
559
|
+
const projection = reduceAgentEvents([
|
|
560
|
+
createAgentEvent('plan_set', { origin: 'tool', payload: { steps: ['Ingest a', 'Ingest b'] } }),
|
|
561
|
+
createAgentEvent('plan_step_updated', {
|
|
562
|
+
origin: 'runtime',
|
|
563
|
+
payload: { step: 1, status: 'skipped', reason: 'dependency_failed:convert' },
|
|
564
|
+
}),
|
|
565
|
+
createAgentEvent('plan_step_updated', {
|
|
566
|
+
origin: 'runtime',
|
|
567
|
+
payload: { step: 2, status: 'brouette' },
|
|
568
|
+
}),
|
|
569
|
+
]);
|
|
570
|
+
|
|
571
|
+
assert.equal(projection.plan[0].status, 'skipped');
|
|
572
|
+
assert.equal(projection.plan[0].error?.code, 'dependency_failed');
|
|
573
|
+
assert.match(projection.plan[0].error.message, /convert/);
|
|
574
|
+
// Un statut incompréhensible laisse l'étape où elle était et se signale :
|
|
575
|
+
// on ne sait pas ce qui s'est passé, le dire vaut mieux que d'inventer.
|
|
576
|
+
assert.equal(projection.plan[1].status, 'pending');
|
|
577
|
+
assert.equal(projection.logs.some((line) => /unknown status "brouette"/.test(line)), true);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test('reduceAgentEvents: les alias de statut tombent sur le canonique', () => {
|
|
581
|
+
const projection = reduceAgentEvents([
|
|
582
|
+
createAgentEvent('plan_set', { origin: 'tool', payload: { steps: ['A', 'B', 'C'] } }),
|
|
583
|
+
createAgentEvent('plan_step_updated', { origin: 'runtime', payload: { step: 1, status: 'succeeded' } }),
|
|
584
|
+
createAgentEvent('plan_step_updated', { origin: 'runtime', payload: { step: 2, status: 'error' } }),
|
|
585
|
+
// Contrat historique : un événement de fin sans statut vaut « terminé ».
|
|
586
|
+
createAgentEvent('plan_step_updated', { origin: 'runtime', payload: { step: 3 } }),
|
|
587
|
+
]);
|
|
588
|
+
|
|
589
|
+
assert.equal(projection.plan[0].status, 'done');
|
|
590
|
+
assert.equal(projection.plan[1].status, 'failed');
|
|
591
|
+
assert.equal(projection.plan[2].status, 'done');
|
|
592
|
+
assert.equal(projection.logs.some((line) => /unknown status/.test(line)), false);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// La bulle « Request received · Donna is preparing… » n'est retirée côté serve
|
|
596
|
+
// que lorsqu'un message assistant NON VIDE arrive. Un tour sans réponse ne
|
|
597
|
+
// publiait rien : le point d'attente tournait jusqu'au rechargement de la page.
|
|
598
|
+
test('a discarded stream is emptied, never popped', () => {
|
|
599
|
+
const projection = reduceAgentEvents([
|
|
600
|
+
createAgentEvent('user_message', { origin: 'user', payload: { content: 'question' } }),
|
|
601
|
+
createAgentEvent('assistant_delta', { origin: 'runtime', payload: { delta: 'Je vais regarder…' } }),
|
|
602
|
+
createAgentEvent('assistant_delta_reset', { origin: 'runtime', payload: {} }),
|
|
603
|
+
]);
|
|
604
|
+
|
|
605
|
+
// La réconciliation de serve suppose une conversation en ajout seul : dépiler
|
|
606
|
+
// la ferait passer sous le nombre d'éléments déjà rendus, laissant à l'écran
|
|
607
|
+
// le texte qu'on voulait effacer et décalant tous les messages suivants.
|
|
608
|
+
assert.equal(projection.conversation.length, 2);
|
|
609
|
+
assert.deepEqual(projection.conversation[1], { role: 'assistant', content: '', streaming: true });
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test('a stream resumed after a discard carries only the final text', () => {
|
|
613
|
+
const projection = reduceAgentEvents([
|
|
614
|
+
createAgentEvent('user_message', { origin: 'user', payload: { content: 'question' } }),
|
|
615
|
+
createAgentEvent('assistant_delta', { origin: 'runtime', payload: { delta: 'Je vais regarder…' } }),
|
|
616
|
+
createAgentEvent('assistant_delta_reset', { origin: 'runtime', payload: {} }),
|
|
617
|
+
createAgentEvent('assistant_delta', { origin: 'runtime', payload: { delta: '12 ' } }),
|
|
618
|
+
createAgentEvent('assistant_delta', { origin: 'runtime', payload: { delta: 'pages.' } }),
|
|
619
|
+
createAgentEvent('assistant_message', { origin: 'runtime', payload: { content: '12 pages.' } }),
|
|
620
|
+
]);
|
|
621
|
+
|
|
622
|
+
assert.equal(projection.conversation.length, 2);
|
|
623
|
+
assert.equal(projection.conversation[1].content, '12 pages.');
|
|
624
|
+
assert.equal(projection.conversation[1].streaming, undefined, 'le message doit être figé');
|
|
625
|
+
});
|