@dotdrelle/wiki-manager 0.15.40 → 0.15.41
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 +29 -36
- package/src/commands/slash.js +11 -1
- package/src/core/activity.js +3 -2
- package/src/core/agentEvents.js +45 -11
- package/src/core/agentEvents.test.js +44 -0
- package/src/core/agentLoop.js +32 -6
- package/src/core/buildInfo.json +2 -2
- package/src/core/jobQueue.js +9 -0
- package/src/core/mcp.js +1 -1
- package/src/core/plan.js +3 -2
- package/src/core/planPatch.js +2 -1
- package/src/core/workflow.js +11 -2
- 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/store.js +16 -1
- package/src/runtime/store.test.js +50 -0
- package/src/shell/repl.js +4 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.41",
|
|
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/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/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/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';
|
|
@@ -317,17 +326,7 @@ export async function forwardRuntimeApproval(getWorkspaceContext, request = {})
|
|
|
317
326
|
return context.approvalManager?.approve(request) ?? { approved: false };
|
|
318
327
|
}
|
|
319
328
|
|
|
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
|
-
}
|
|
329
|
+
export { resolvePreparedDelegationApproval } from '../runtime/delegation.js';
|
|
331
330
|
|
|
332
331
|
function timestampForFile() {
|
|
333
332
|
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
@@ -1168,39 +1167,33 @@ async function runRuntime(argv, agent) {
|
|
|
1168
1167
|
: undefined;
|
|
1169
1168
|
supervisor?.setRunSignal(signal);
|
|
1170
1169
|
session._onStep = (message) => emitRuntimeLog(session, message);
|
|
1171
|
-
|
|
1172
|
-
const {
|
|
1173
|
-
|
|
1174
|
-
|
|
1170
|
+
session._delegateWithinRun = async (objective) => {
|
|
1171
|
+
const { delegateWithinRun } = await import('../runtime/delegation.js');
|
|
1172
|
+
return delegateWithinRun(session, objective, {
|
|
1173
|
+
prepare: ({ objective: goal }) => prepareDelegation(context, { objective: goal }),
|
|
1175
1174
|
registry: capabilityRegistryForSession(session),
|
|
1176
|
-
session,
|
|
1177
1175
|
store,
|
|
1178
|
-
|
|
1179
|
-
|
|
1176
|
+
approvalManager: context.approvalManager,
|
|
1177
|
+
autoApprove: body.autoApprove === true,
|
|
1180
1178
|
});
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1179
|
+
};
|
|
1180
|
+
if (body.preparedDelegation?.fragment) {
|
|
1181
|
+
const { integratePreparedDelegation } = await import('../runtime/delegation.js');
|
|
1182
|
+
const prepared = body.preparedDelegation;
|
|
1185
1183
|
// Real approval gate (opt-out): a directly-delegated run only skips the
|
|
1186
1184
|
// human approval step when the caller explicitly opts in via
|
|
1187
1185
|
// `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,
|
|
1186
|
+
// By default the run WAITS, so the pending_approval window stays
|
|
1187
|
+
// visible instead of being resolved ~30ms after launch.
|
|
1188
|
+
integratePreparedDelegation({
|
|
1189
|
+
session,
|
|
1190
|
+
store,
|
|
1197
1191
|
runId,
|
|
1192
|
+
prepared,
|
|
1193
|
+
registry: capabilityRegistryForSession(session),
|
|
1194
|
+
approvalManager: context.approvalManager,
|
|
1195
|
+
autoApprove: body.autoApprove === true,
|
|
1198
1196
|
});
|
|
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
1197
|
body._planReady?.resolve?.({ runId, planRevision: session.agentProjection?.planRevision ?? 0 });
|
|
1205
1198
|
}
|
|
1206
1199
|
// Deterministic capability run (/ingest): ask the capable agent for its
|
package/src/commands/slash.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
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';
|
|
@@ -850,7 +860,7 @@ function formatRuntimeRunStatus(state) {
|
|
|
850
860
|
? state.controlQueue.filter((item) => item.status === 'queued').length
|
|
851
861
|
: 0;
|
|
852
862
|
const tasks = Array.isArray(state?.workflow?.nodes)
|
|
853
|
-
? state.workflow.nodes.filter((node) => node.type === 'task' && !
|
|
863
|
+
? state.workflow.nodes.filter((node) => node.type === 'task' && !isTerminal(node.status)).length
|
|
854
864
|
: 0;
|
|
855
865
|
return `runtime: ${status}${runId} · queued=${queued} · activeTasks=${tasks}`;
|
|
856
866
|
}
|
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',
|
|
@@ -343,7 +344,13 @@ function applyEvent(state, event) {
|
|
|
343
344
|
state.planRevision = normalizePlanRevision(event.payload?.planRevision ?? state.planRevision + 1);
|
|
344
345
|
return;
|
|
345
346
|
case 'plan_step_updated':
|
|
346
|
-
|
|
347
|
+
{
|
|
348
|
+
// L'anomalie remonte par la valeur de retour plutôt que par une
|
|
349
|
+
// référence au state : `updatePlanStep` reste une fonction sur un
|
|
350
|
+
// plan, et le journal reste la responsabilité de l'appelant.
|
|
351
|
+
const anomaly = updatePlanStep(state.plan, event.payload ?? {});
|
|
352
|
+
if (anomaly) state.logs.push(anomaly);
|
|
353
|
+
}
|
|
347
354
|
return;
|
|
348
355
|
case 'control_message_received':
|
|
349
356
|
state.logs.push(`Control message: ${String(event.payload?.input ?? '')}`);
|
|
@@ -725,7 +732,7 @@ function markCoveredApprovalsApproved(approvals, grant, ts) {
|
|
|
725
732
|
|
|
726
733
|
function cancelPendingPlanSteps(plan) {
|
|
727
734
|
for (const step of plan ?? []) {
|
|
728
|
-
if (!
|
|
735
|
+
if (!isTerminal(step.status)) step.status = 'cancelled';
|
|
729
736
|
}
|
|
730
737
|
}
|
|
731
738
|
|
|
@@ -852,24 +859,51 @@ function normalizePlanTask(raw, index, { owner = 'orchestrator', ownerActivityKe
|
|
|
852
859
|
}
|
|
853
860
|
|
|
854
861
|
function updatePlanStep(plan, payload) {
|
|
855
|
-
|
|
862
|
+
let anomaly = null;
|
|
863
|
+
if (!plan) return anomaly;
|
|
856
864
|
const requestedTaskId = payload.taskId ?? payload.id ?? payload.targetTaskId;
|
|
857
865
|
const step = requestedTaskId != null
|
|
858
866
|
? plan.find((item) => String(item.id ?? item.step) === String(requestedTaskId))
|
|
859
867
|
: plan.find((item) => item.step === Number(payload.step));
|
|
860
|
-
if (!step) return;
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
+
if (!step) return anomaly;
|
|
869
|
+
/*
|
|
870
|
+
Un statut inconnu ne vaut pas « réussi ».
|
|
871
|
+
|
|
872
|
+
La cascade se terminait par `else step.status = 'done'` : tout statut non
|
|
873
|
+
énuméré — `skipped`, par exemple — était projeté en succès. Le runner
|
|
874
|
+
marquait bien une tâche ignorée, la projection la déclarait faite, et le
|
|
875
|
+
résumé de run comptait une réussite qui n'a jamais eu lieu. Le défaut le
|
|
876
|
+
plus dangereux est celui qui transforme une inconnue en bonne nouvelle.
|
|
877
|
+
|
|
878
|
+
Trois cas, et un seul mène à `done` :
|
|
879
|
+
|
|
880
|
+
- un statut reconnu par le vocabulaire commun est repris tel quel, alias
|
|
881
|
+
compris (`succeeded` → `done`, `error` → `failed`) ;
|
|
882
|
+
- l'ABSENCE de statut garde le contrat historique — un événement de fin
|
|
883
|
+
sans précision signifie « terminé » ;
|
|
884
|
+
- un statut présent mais incompréhensible laisse l'étape dans l'état où
|
|
885
|
+
elle était, et signale une anomalie de projection. On ne sait pas ce qui
|
|
886
|
+
s'est passé : le dire est plus utile que d'inventer une réponse.
|
|
887
|
+
*/
|
|
888
|
+
const canonical = normalizeTaskStatus(payload.status);
|
|
889
|
+
if (canonical) {
|
|
890
|
+
step.status = canonical;
|
|
891
|
+
} else if (isUnknownStatus(payload.status)) {
|
|
892
|
+
anomaly = `projection: unknown status "${String(payload.status)}" for plan step ${String(requestedTaskId ?? payload.step ?? '?')} — kept "${String(step.status ?? 'pending')}"`;
|
|
893
|
+
} else {
|
|
894
|
+
step.status = 'done';
|
|
895
|
+
}
|
|
896
|
+
// Le motif d'un abandon est la seule chose qui le rende actionnable :
|
|
897
|
+
// « ignorée » sans « parce que » n'apprend rien à qui relance.
|
|
898
|
+
if (step.status === 'skipped' && payload.reason && !step.error) {
|
|
899
|
+
step.error = { code: 'dependency_failed', message: String(payload.reason) };
|
|
900
|
+
}
|
|
868
901
|
if (payload.activityKey) step.activityKey = payload.activityKey;
|
|
869
902
|
if (Array.isArray(payload.outputRefs)) step.outputRefs = payload.outputRefs.map(cloneRef);
|
|
870
903
|
if (payload.result) step.result = cloneJson(payload.result);
|
|
871
904
|
if (payload.retryState) step.retryState = cloneJson(payload.retryState);
|
|
872
905
|
if (payload.retryAssignment) step.retryAssignment = cloneJson(payload.retryAssignment);
|
|
906
|
+
return anomaly;
|
|
873
907
|
}
|
|
874
908
|
|
|
875
909
|
function formatPlanErrors(errors) {
|
|
@@ -547,3 +547,47 @@ 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
|
+
});
|
package/src/core/agentLoop.js
CHANGED
|
@@ -3,6 +3,11 @@ import { createAgentEvent, dispatchAgentEvent } from './agentEvents.js';
|
|
|
3
3
|
import { activitySnapshot, newNonTerminalActivities } from './activity.js';
|
|
4
4
|
import { formatCompletedActivities, formatPlanStatus } from './plan.js';
|
|
5
5
|
import { formatReadyTaskPrompt, nextReadyPlanTask, readyPlanTasks, sanitizePlanForExecution } from './planPatch.js';
|
|
6
|
+
import { isActive, isPending, normalizeTaskStatus } from '../orchestrator/taskStatuses.js';
|
|
7
|
+
|
|
8
|
+
// Les deux formes d'attente d'un accord humain. Distinguées de `pending` :
|
|
9
|
+
// approuver ne les rend pas prêtes de la même manière.
|
|
10
|
+
const isPendingApproval = (status) => ['pending_approval', 'waiting_approval'].includes(normalizeTaskStatus(status));
|
|
6
11
|
|
|
7
12
|
export function abortError(message = 'Agent run cancelled.') {
|
|
8
13
|
const err = new Error(message);
|
|
@@ -111,19 +116,40 @@ export async function runAgenticLoop(agent, session, initialInput, {
|
|
|
111
116
|
// integrated agent_plan fragment. Prose stays prose.
|
|
112
117
|
sanitizeSessionPlan(session, { runId });
|
|
113
118
|
|
|
119
|
+
/*
|
|
120
|
+
Une décision prise arrête la conversation.
|
|
121
|
+
|
|
122
|
+
Dès qu'un fragment structuré est intégré pendant ce tour — Donna vient de
|
|
123
|
+
déléguer —, la boucle conversationnelle n'a plus rien à décider : le plan
|
|
124
|
+
existe, validé par l'agent qui l'exécutera. Elle rendait pourtant la main
|
|
125
|
+
au modèle, parce que le seul critère de bascule était « au moins deux
|
|
126
|
+
tâches PRÊTES » et qu'un plan intégralement en attente d'approbation n'en
|
|
127
|
+
compte aucune. Le modèle repartait donc, l'évaluateur jugeait le plan
|
|
128
|
+
incomplet, le replanificateur relançait une délégation : cinq tâches,
|
|
129
|
+
puis dix, puis quinze, à l'identique.
|
|
130
|
+
|
|
131
|
+
L'attente d'approbation et l'exécution appartiennent au planificateur
|
|
132
|
+
parallèle, qui sait faire les deux. On lui rend la main tout de suite.
|
|
133
|
+
*/
|
|
134
|
+
if (parallelHandoff && session._structuredPlanIntegrated) {
|
|
135
|
+
session._structuredPlanIntegrated = false;
|
|
136
|
+
return { ok: true, handoff: true };
|
|
137
|
+
}
|
|
138
|
+
|
|
114
139
|
const newPending = newNonTerminalActivities(snapshot, session);
|
|
115
140
|
if (newPending.length === 0) {
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
141
|
+
// Toute attente est du travail inachevé — approbation comprise. La liste
|
|
142
|
+
// énumérait `pending` et `pending_approval` mais oubliait
|
|
143
|
+
// `waiting_approval`, que produit justement l'intégration d'un fragment
|
|
144
|
+
// délégué : un plan entier en attente se lisait « plus rien à faire ».
|
|
145
|
+
const pending = (session.headlessPlan ?? []).filter((step) => isPending(step.status));
|
|
120
146
|
const pendingSteps = readyPlanTasks(session.headlessPlan);
|
|
121
147
|
if (pending.length === 0) {
|
|
122
148
|
onComplete?.();
|
|
123
149
|
return { ok: true };
|
|
124
150
|
}
|
|
125
151
|
if (pendingSteps.length === 0) {
|
|
126
|
-
const reason = pending.every((step) => step.status
|
|
152
|
+
const reason = pending.every((step) => isPendingApproval(step.status)) ? 'awaiting_approval' : 'no_ready_plan_task';
|
|
127
153
|
onPendingSteps?.({ pendingSteps: pending, blocked: true });
|
|
128
154
|
return { ok: false, stalled: true, reason };
|
|
129
155
|
}
|
|
@@ -150,7 +176,7 @@ export async function runAgenticLoop(agent, session, initialInput, {
|
|
|
150
176
|
const summary = formatCompletedActivities(completed);
|
|
151
177
|
onActivitiesCompleted?.({ completed, summary });
|
|
152
178
|
const unfinished = (session.headlessPlan ?? []).some((step) =>
|
|
153
|
-
|
|
179
|
+
isPending(step.status) || isActive(step.status));
|
|
154
180
|
if (deterministicTerminalSummary && !unfinished) {
|
|
155
181
|
return { ok: true, completed, summary, deterministicSummary: true };
|
|
156
182
|
}
|
package/src/core/buildInfo.json
CHANGED
package/src/core/jobQueue.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @statuses-vocabulary
|
|
3
|
+
*
|
|
4
|
+
* JOB queue statuses. A queued job has its own lifecycle, independent of
|
|
5
|
+
* the task that may have enqueued it.
|
|
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 { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
12
|
import { extractActivity, parseJsonText, sessionActivities } from './activity.js';
|
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.41';
|
|
5
5
|
|
|
6
6
|
function envValue(key) {
|
|
7
7
|
const filePath = managerEnvFile();
|
package/src/core/plan.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isUnsuccessfulTerminal } from '../orchestrator/taskStatuses.js';
|
|
1
2
|
export function ensurePlanFromActivity(session, activity) {
|
|
2
3
|
if (!activity) return;
|
|
3
4
|
const actKey = activity.key ?? null;
|
|
@@ -57,7 +58,7 @@ export function syncActivitiesToPlan(plan, activities) {
|
|
|
57
58
|
const contractTaskPlan = isContractTaskPlan(plan);
|
|
58
59
|
for (const activity of activities ?? []) {
|
|
59
60
|
const terminal = Boolean(activity.terminal);
|
|
60
|
-
const failed =
|
|
61
|
+
const failed = isUnsuccessfulTerminal(activity.status);
|
|
61
62
|
const actKey = activity.key ?? activity.id ?? activity.jobId ?? null;
|
|
62
63
|
const structuredMatch = findMatchingPlanStepByStructure(plan, activity);
|
|
63
64
|
const matched = structuredMatch ?? findMatchingPlanStep(plan, activity);
|
|
@@ -244,7 +245,7 @@ export function attachActivityToExistingPlan(plan, activity) {
|
|
|
244
245
|
if (!matched) return;
|
|
245
246
|
matched.activityKey = actKey;
|
|
246
247
|
if (!matched.ownerActivityKey) matched.ownerActivityKey = actKey;
|
|
247
|
-
const failed =
|
|
248
|
+
const failed = isUnsuccessfulTerminal(activity.status);
|
|
248
249
|
if (activity.terminal) {
|
|
249
250
|
matched.status = failed ? 'failed' : 'done';
|
|
250
251
|
return;
|
package/src/core/planPatch.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validateContractInDev } from '../contracts/schemas.js';
|
|
2
|
+
import { isUnsuccessfulTerminal } from '../orchestrator/taskStatuses.js';
|
|
2
3
|
|
|
3
4
|
const PATCH_OPS = new Set([
|
|
4
5
|
'add_task',
|
|
@@ -164,7 +165,7 @@ export function sanitizePlanForExecution(plan) {
|
|
|
164
165
|
const done = new Set(tasks.filter((task) => task.status === 'done').map(taskId));
|
|
165
166
|
const terminalBlocked = new Set(
|
|
166
167
|
tasks
|
|
167
|
-
.filter((task) =>
|
|
168
|
+
.filter((task) => isUnsuccessfulTerminal(task.status))
|
|
168
169
|
.map(taskId),
|
|
169
170
|
);
|
|
170
171
|
const hasReady = pending.some((task) => task.dependsOn.every((dep) => done.has(String(dep))));
|
package/src/core/workflow.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @statuses-vocabulary
|
|
3
|
+
*
|
|
4
|
+
* DISPLAY normalization for the execution graph, with `added_during_run`,
|
|
5
|
+
* which exists only as a rendering hint.
|
|
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 { aggregateActivity } from '../activity/activityAggregator.js';
|
|
2
11
|
import { calculateWeightedProgress } from '../activity/progressCalculator.js';
|
|
3
12
|
import { aggregateGraph } from '../graph/graphAggregator.js';
|
|
13
|
+
import { isTerminal } from '../orchestrator/taskStatuses.js';
|
|
4
14
|
|
|
5
|
-
const TERMINAL_STATUSES = new Set(['done', 'failed', 'cancelled', 'canceled', 'error', 'complete', 'completed', 'success']);
|
|
6
15
|
const RUNNING_STATUSES = new Set(['running', 'starting', 'queued', 'waiting', 'pending_approval']);
|
|
7
16
|
|
|
8
17
|
// Canonical workflow projection for 0.9.6.
|
|
@@ -278,7 +287,7 @@ function isActiveStatus(status) {
|
|
|
278
287
|
}
|
|
279
288
|
|
|
280
289
|
function isTerminalStatus(status) {
|
|
281
|
-
return
|
|
290
|
+
return isTerminal(status);
|
|
282
291
|
}
|
|
283
292
|
|
|
284
293
|
function findCurrentNode(nodes) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { isFailed, isCancelled } from '../orchestrator/taskStatuses.js';
|
|
1
2
|
const ALWAYS_VISIBLE = new Set(['run', 'task_group', 'barrier', 'approval']);
|
|
2
3
|
const ACTIVE = new Set(['running', 'queued', 'pending_approval', 'waiting_approval']);
|
|
3
|
-
const ERROR = new Set(['failed', 'error', 'cancelled']);
|
|
4
4
|
|
|
5
5
|
export function applyGraphVisibility(graph, { maxNodes = 18 } = {}) {
|
|
6
6
|
const nodes = graph.nodes ?? [];
|
|
@@ -35,6 +35,6 @@ function mustShow(node) {
|
|
|
35
35
|
const status = String(node.status ?? '').toLowerCase();
|
|
36
36
|
return ALWAYS_VISIBLE.has(node.type)
|
|
37
37
|
|| ACTIVE.has(status)
|
|
38
|
-
||
|
|
38
|
+
|| isFailed(status) || isCancelled(status)
|
|
39
39
|
|| node.type === 'plan_expansion';
|
|
40
40
|
}
|