@dotdrelle/wiki-manager 0.15.38 → 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/README.md +5 -0
- package/docker-compose.yml +1 -1
- 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 +34 -39
- package/src/commands/slash.js +11 -1
- package/src/core/activity.js +3 -2
- package/src/core/agentEvents.js +46 -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/dockerCompose.test.js +8 -0
- 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 +6 -7
- package/src/orchestrator/dispatcher.test.js +33 -0
- 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.js +24 -1
- package/src/runtime/server.test.js +113 -0
- package/src/runtime/store.js +16 -1
- package/src/runtime/store.test.js +50 -0
- package/src/shell/repl.js +4 -5
package/README.md
CHANGED
|
@@ -771,6 +771,11 @@ capabilityRouting:
|
|
|
771
771
|
allowedAgents: [connectors]
|
|
772
772
|
```
|
|
773
773
|
|
|
774
|
+
The production agent also advertises `workspace.restore` for Git-backed
|
|
775
|
+
rollback. It is workspace-scoped and remains subject to the normal runtime
|
|
776
|
+
approval and lock checks; it can be pinned in the same way when several agents
|
|
777
|
+
provide that capability.
|
|
778
|
+
|
|
774
779
|
#### Compose overrides — optional agents, proxies, local fixes
|
|
775
780
|
|
|
776
781
|
Two override files sit under **`.wiki/compose/`**, one per stack:
|
package/docker-compose.yml
CHANGED
|
@@ -124,7 +124,7 @@ services:
|
|
|
124
124
|
- WORKSPACE_NAME=${WORKSPACE_NAME:-workspace}
|
|
125
125
|
- WIKI_WORKSPACE_PATH=/workspace
|
|
126
126
|
- WIKI_CONFIG_PATH=${WIKI_CONFIG_PATH:-}
|
|
127
|
-
- PRODUCTION_ALLOWED_STEPS=${PRODUCTION_ALLOWED_STEPS:-doctor,ingest,ingest_plan,ingest_apply,build,export,polish,pipeline}
|
|
127
|
+
- PRODUCTION_ALLOWED_STEPS=${PRODUCTION_ALLOWED_STEPS:-doctor,ingest,ingest_plan,ingest_apply,build,export,polish,restore,pipeline}
|
|
128
128
|
- PRODUCTION_REQUIRE_CONFIRMATION=${PRODUCTION_REQUIRE_CONFIRMATION:-false}
|
|
129
129
|
# Parallelism levers — effective concurrency ≈ recommendedConcurrency.
|
|
130
130
|
# Intermediate defaults (4/8). Low profile 2/4, high profile 8/16.
|
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
|
|
@@ -1231,9 +1224,11 @@ async function runRuntime(argv, agent) {
|
|
|
1231
1224
|
),
|
|
1232
1225
|
requireApprovalForMutations: body.capabilityPlan.requireApproval !== false,
|
|
1233
1226
|
},
|
|
1234
|
-
...(
|
|
1235
|
-
? { arguments:
|
|
1236
|
-
:
|
|
1227
|
+
...(body.capabilityPlan.arguments && typeof body.capabilityPlan.arguments === 'object'
|
|
1228
|
+
? { arguments: body.capabilityPlan.arguments }
|
|
1229
|
+
: Array.isArray(body.capabilityPlan.inputs) && body.capabilityPlan.inputs.length > 0
|
|
1230
|
+
? { arguments: { inputs: body.capabilityPlan.inputs } }
|
|
1231
|
+
: {}),
|
|
1237
1232
|
})));
|
|
1238
1233
|
if (!Array.isArray(fragment?.tasks) || fragment.tasks.length === 0) {
|
|
1239
1234
|
dispatchAgentEvent(session, createAgentEvent('assistant_message', {
|
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 ?? '')}`);
|
|
@@ -556,6 +563,7 @@ function applyEvent(state, event) {
|
|
|
556
563
|
status: 'queued',
|
|
557
564
|
createdAt: event.payload?.createdAt ?? event.ts,
|
|
558
565
|
updatedAt: event.ts,
|
|
566
|
+
...(event.payload?.capabilityPlan !== undefined ? { capabilityPlan: event.payload.capabilityPlan } : {}),
|
|
559
567
|
});
|
|
560
568
|
return;
|
|
561
569
|
case 'control_started':
|
|
@@ -724,7 +732,7 @@ function markCoveredApprovalsApproved(approvals, grant, ts) {
|
|
|
724
732
|
|
|
725
733
|
function cancelPendingPlanSteps(plan) {
|
|
726
734
|
for (const step of plan ?? []) {
|
|
727
|
-
if (!
|
|
735
|
+
if (!isTerminal(step.status)) step.status = 'cancelled';
|
|
728
736
|
}
|
|
729
737
|
}
|
|
730
738
|
|
|
@@ -851,24 +859,51 @@ function normalizePlanTask(raw, index, { owner = 'orchestrator', ownerActivityKe
|
|
|
851
859
|
}
|
|
852
860
|
|
|
853
861
|
function updatePlanStep(plan, payload) {
|
|
854
|
-
|
|
862
|
+
let anomaly = null;
|
|
863
|
+
if (!plan) return anomaly;
|
|
855
864
|
const requestedTaskId = payload.taskId ?? payload.id ?? payload.targetTaskId;
|
|
856
865
|
const step = requestedTaskId != null
|
|
857
866
|
? plan.find((item) => String(item.id ?? item.step) === String(requestedTaskId))
|
|
858
867
|
: plan.find((item) => item.step === Number(payload.step));
|
|
859
|
-
if (!step) return;
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|
+
}
|
|
867
901
|
if (payload.activityKey) step.activityKey = payload.activityKey;
|
|
868
902
|
if (Array.isArray(payload.outputRefs)) step.outputRefs = payload.outputRefs.map(cloneRef);
|
|
869
903
|
if (payload.result) step.result = cloneJson(payload.result);
|
|
870
904
|
if (payload.retryState) step.retryState = cloneJson(payload.retryState);
|
|
871
905
|
if (payload.retryAssignment) step.retryAssignment = cloneJson(payload.retryAssignment);
|
|
906
|
+
return anomaly;
|
|
872
907
|
}
|
|
873
908
|
|
|
874
909
|
function formatPlanErrors(errors) {
|
|
@@ -364,6 +364,38 @@ test('reduceAgentEvents: control queue is event sourced and follows run status',
|
|
|
364
364
|
assert.equal(projection.controlQueue[1].status, 'cancelled');
|
|
365
365
|
});
|
|
366
366
|
|
|
367
|
+
test('reduceAgentEvents: control_enqueued preserves a structured capabilityPlan across replay', () => {
|
|
368
|
+
const capabilityPlan = {
|
|
369
|
+
capability: 'workspace.restore',
|
|
370
|
+
operation: 'restore',
|
|
371
|
+
arguments: { run: 'abc123' },
|
|
372
|
+
requireApproval: true,
|
|
373
|
+
};
|
|
374
|
+
const projection = reduceAgentEvents([
|
|
375
|
+
createAgentEvent('control_enqueued', {
|
|
376
|
+
origin: 'runtime',
|
|
377
|
+
workspace: 'docs',
|
|
378
|
+
payload: {
|
|
379
|
+
id: 'control-restore',
|
|
380
|
+
workspace: 'docs',
|
|
381
|
+
input: 'restore',
|
|
382
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
383
|
+
capabilityPlan,
|
|
384
|
+
},
|
|
385
|
+
}),
|
|
386
|
+
createAgentEvent('control_started', {
|
|
387
|
+
origin: 'runtime',
|
|
388
|
+
runId: 'run-control-restore',
|
|
389
|
+
workspace: 'docs',
|
|
390
|
+
payload: { id: 'control-restore', runId: 'run-control-restore' },
|
|
391
|
+
}),
|
|
392
|
+
]);
|
|
393
|
+
|
|
394
|
+
const item = projection.controlQueue.find((entry) => entry.id === 'control-restore');
|
|
395
|
+
assert.deepEqual(item.capabilityPlan, capabilityPlan);
|
|
396
|
+
assert.equal(item.status, 'running');
|
|
397
|
+
});
|
|
398
|
+
|
|
367
399
|
test('reduceAgentEvents: activity-owned plan is used when no orchestrator plan exists', () => {
|
|
368
400
|
const projection = reduceAgentEvents([
|
|
369
401
|
createAgentEvent('activity_upserted', {
|
|
@@ -515,3 +547,47 @@ test('run_error cancels pending plan steps and active activities (no ghosts at r
|
|
|
515
547
|
assert.equal(activity.status, 'cancelled');
|
|
516
548
|
assert.equal(activity.terminal, true);
|
|
517
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
|
@@ -19,6 +19,14 @@ test('workspace compose does not start a per-workspace agent runtime', async ()
|
|
|
19
19
|
);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
test('workspace production agent enables restore by default', async () => {
|
|
23
|
+
const raw = await readFile(new URL('../../docker-compose.yml', import.meta.url), 'utf8');
|
|
24
|
+
const compose = YAML.parse(raw);
|
|
25
|
+
const allowed = compose.services['production-mcp'].environment
|
|
26
|
+
.find((entry) => String(entry).startsWith('PRODUCTION_ALLOWED_STEPS='));
|
|
27
|
+
assert.match(String(allowed), /(?:^|,)restore(?:,|})/);
|
|
28
|
+
});
|
|
29
|
+
|
|
22
30
|
test('shipped compose files never carry a build context', async () => {
|
|
23
31
|
// Ces deux fichiers partent dans le paquet npm, où les dépôts frères
|
|
24
32
|
// (`../agent-external/…`) n'existent pas : un `build:` y rend toute commande
|
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;
|