@dotdrelle/wiki-manager 0.11.10 → 0.12.0

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.
Files changed (81) hide show
  1. package/README.md +32 -7
  2. package/agents.docker-compose.yml +3 -0
  3. package/docker-compose.yml +1 -0
  4. package/package.json +3 -2
  5. package/src/activity/activityAggregator.js +109 -0
  6. package/src/activity/activityAggregator.test.js +60 -0
  7. package/src/activity/activityDeduplicator.js +50 -0
  8. package/src/activity/progressCalculator.js +61 -0
  9. package/src/activity/runSynthesis.js +15 -0
  10. package/src/agent/graph.js +65 -30
  11. package/src/agent/graph.test.js +28 -5
  12. package/src/cli/wiki-manager.js +22 -0
  13. package/src/contracts/schemas.js +249 -13
  14. package/src/contracts/schemas.test.js +145 -0
  15. package/src/core/activity.js +8 -5
  16. package/src/core/agentEvents.js +260 -23
  17. package/src/core/agentEvents.test.js +53 -0
  18. package/src/core/compose.js +6 -1
  19. package/src/core/dockerCompose.test.js +12 -0
  20. package/src/core/documentIntake.js +33 -38
  21. package/src/core/documentIntake.test.js +25 -0
  22. package/src/core/env.js +11 -1
  23. package/src/core/jobQueue.js +26 -2
  24. package/src/core/mcp.js +1 -1
  25. package/src/core/plan.js +46 -4
  26. package/src/core/plan.test.js +16 -1
  27. package/src/core/planPatch.js +64 -3
  28. package/src/core/planPatch.test.js +110 -1
  29. package/src/core/queueStore.test.js +21 -0
  30. package/src/core/runtimeLog.js +119 -0
  31. package/src/core/runtimeLog.test.js +84 -0
  32. package/src/core/wikirc.js +22 -0
  33. package/src/core/wikirc.test.js +49 -1
  34. package/src/core/workflow.js +14 -3
  35. package/src/graph/graphAggregator.js +5 -0
  36. package/src/graph/graphPatch.js +8 -0
  37. package/src/graph/graphSnapshot.js +14 -0
  38. package/src/graph/graphVisibilityPolicy.js +40 -0
  39. package/src/graph/runGraphProjector.js +87 -0
  40. package/src/graph/runGraphProjector.test.js +111 -0
  41. package/src/orchestrator/agentRegistry.js +172 -0
  42. package/src/orchestrator/agentRegistry.test.js +114 -0
  43. package/src/orchestrator/approvalPolicy.js +126 -0
  44. package/src/orchestrator/approvalPolicy.test.js +78 -0
  45. package/src/orchestrator/assignmentManager.js +80 -0
  46. package/src/orchestrator/attemptManager.js +168 -0
  47. package/src/orchestrator/attemptManager.test.js +160 -0
  48. package/src/orchestrator/budgetManager.js +127 -0
  49. package/src/orchestrator/capabilityRegistry.js +64 -0
  50. package/src/orchestrator/capabilityRegistry.test.js +59 -0
  51. package/src/orchestrator/capabilityResolver.js +151 -0
  52. package/src/orchestrator/capabilityResolver.test.js +127 -0
  53. package/src/orchestrator/dependencyResolver.js +112 -0
  54. package/src/orchestrator/dispatcher.js +302 -0
  55. package/src/orchestrator/lockManager.js +51 -0
  56. package/src/orchestrator/planIntegrator.js +268 -0
  57. package/src/orchestrator/planIntegrator.test.js +213 -0
  58. package/src/orchestrator/planValidator.js +534 -0
  59. package/src/orchestrator/planValidator.test.js +262 -0
  60. package/src/orchestrator/resultAggregator.js +248 -0
  61. package/src/orchestrator/resultAggregator.test.js +211 -0
  62. package/src/orchestrator/scheduler.js +103 -0
  63. package/src/orchestrator/scheduler.test.js +138 -0
  64. package/src/runtime/approvals.js +130 -1
  65. package/src/runtime/donna-contract.test.js +293 -30
  66. package/src/runtime/recoveryManager.js +176 -0
  67. package/src/runtime/recoveryManager.test.js +162 -0
  68. package/src/runtime/runner.e2e.test.js +98 -12
  69. package/src/runtime/runner.js +261 -227
  70. package/src/runtime/runner.test.js +249 -452
  71. package/src/runtime/server.js +137 -20
  72. package/src/runtime/server.test.js +137 -7
  73. package/src/runtime/store.js +837 -2
  74. package/src/runtime/store.test.js +245 -4
  75. package/src/runtime/supervisor.js +33 -1
  76. package/src/runtime/supervisor.test.js +47 -1
  77. package/src/shell/RightPane.tsx +8 -5
  78. package/src/shell/repl.js +4 -9
  79. package/src/shell/repl.test.js +25 -4
  80. package/src/shell/tui.tsx +1 -0
  81. package/src/shell/useSession.ts +38 -10
package/README.md CHANGED
@@ -9,13 +9,19 @@ inspect workspaces, run safe manager commands, call MCP tools, guide production
9
9
  jobs, and run one-shot headless tasks.
10
10
 
11
11
  The manager does not implement the wiki engine or the external agents. It
12
- **orchestrates** them.
13
-
14
- Scope note: 0.11.0 is an industrialized single-user deployment baseline. The
15
- multi-user model is specified in `llm-wiki/docs/industrialisation.md` and
16
- planned for 0.12.0. Until then, do not expose the runtime as a shared write
17
- surface; it binds to `127.0.0.1` by default, and `--host 0.0.0.0` must be an
18
- explicit deployment choice with bearer-token and network protection.
12
+ **orchestrates** them — generically. Since 0.12.0 the Donna core is
13
+ business-agnostic: agents declare their capabilities through a standard
14
+ contract (`agent_describe` / `agent_plan` / `agent_execute` / `agent_status` /
15
+ `agent_cancel`), plans target *capabilities* rather than agent names, and a
16
+ deterministic dispatcher executes bounded tasks with idempotency, bounded
17
+ approvals, per-run budgets and automatic recovery after restart. The chat
18
+ stays available while runs execute; additional requests are queued.
19
+
20
+ Scope note: this is a single-user deployment baseline. The multi-user model is
21
+ specified in `llm-wiki/docs/industrialisation.md` and planned next. Until
22
+ then, do not expose the runtime as a shared write surface; it binds to
23
+ `127.0.0.1` by default, and `--host 0.0.0.0` must be an explicit deployment
24
+ choice with bearer-token and network protection.
19
25
 
20
26
  ---
21
27
 
@@ -780,6 +786,25 @@ The existing native payload should stay intact. `_activity` is additive metadata
780
786
  for the manager. When `poll` is present, the shell/TUI and headless loop call the
781
787
  declared MCP tool until the activity becomes terminal.
782
788
 
789
+ ## Orchestration Contract
790
+
791
+ Beyond `_activity`, an MCP server can become a fully **orchestrable agent** by
792
+ exposing five tools: `agent_describe` (capabilities, limits, health),
793
+ `agent_plan` (returns a task-graph fragment for an objective — planner agents
794
+ only), `agent_execute` (starts one bounded, idempotent task), `agent_status`
795
+ and `agent_cancel`. The manager discovers these at startup and on a periodic
796
+ re-scan, and routes tasks by capability: workspace config can pin
797
+ `preferredAgents` / `allowedAgents` / `fallbackAgents` per capability under
798
+ `capabilityRouting`. Executor-only agents (like `agent-cme`) declare
799
+ `canPlan: false` and receive single tasks planned elsewhere. Mutating
800
+ operations must carry an `idempotencyKey` — the agent persists key→job
801
+ mappings so a retry never duplicates work. Capabilities that mutate external
802
+ systems should declare `defaultRequiresApproval: true`; the manager then
803
+ requires a bounded approval (scoped to run, plan revision and approval class)
804
+ before dispatch. Contracts and schemas live in
805
+ `plan-directeur-orchestration.md` at the wikiLLM workspace root and in
806
+ `src/contracts/schemas.js`.
807
+
783
808
  ## Local Compose Overrides
784
809
 
785
810
  Do not put machine-specific settings in the shared `docker-compose.yml`.
@@ -43,6 +43,7 @@ services:
43
43
 
44
44
  cme:
45
45
  image: dotdrelle/agent-cme:latest
46
+ user: "${UID:-1000}:${GID:-1000}"
46
47
  ports:
47
48
  - "${CME_MCP_PORT:-3336}:8080"
48
49
  environment:
@@ -59,6 +60,7 @@ services:
59
60
 
60
61
  documents:
61
62
  image: dotdrelle/agent-wiki-documents:latest
63
+ user: "${UID:-1000}:${GID:-1000}"
62
64
  ports:
63
65
  - "${DOCUMENTS_MCP_PORT:-3337}:8080"
64
66
  environment:
@@ -83,6 +85,7 @@ services:
83
85
 
84
86
  mailer:
85
87
  image: dotdrelle/agent-mailer-api:latest
88
+ user: "${UID:-1000}:${GID:-1000}"
86
89
  ports:
87
90
  - "${MAILER_MCP_PORT:-3335}:8080"
88
91
  environment:
@@ -101,6 +101,7 @@ services:
101
101
 
102
102
  production-mcp:
103
103
  image: dotdrelle/agent-wiki-production:latest
104
+ user: "${UID:-1000}:${GID:-1000}"
104
105
  labels:
105
106
  wiki-manager.description: "Production MCP server for ingest/build/export jobs."
106
107
  volumes:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdrelle/wiki-manager",
3
- "version": "0.11.10",
3
+ "version": "0.12.0",
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/agent/graph.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/agentEvents.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/documentIntake.test.js src/core/dockerCompose.test.js src/core/wikiWorkspace.test.js src/core/wikirc.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/commands/slash.test.js src/shell/repl.test.js src/runtime/store.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/agent/graph.test.js src/contracts/schemas.test.js src/core/activity.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/documentIntake.test.js src/core/dockerCompose.test.js src/core/wikiWorkspace.test.js src/core/wikirc.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/commands/slash.test.js src/shell/repl.test.js src/runtime/store.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",
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",
@@ -32,6 +32,7 @@
32
32
  "tsconfig.json",
33
33
  "bunfig.toml",
34
34
  "workspaces/.env.example",
35
+ "!**/.gitkeep",
35
36
  "README.md",
36
37
  "LICENSE"
37
38
  ],
@@ -0,0 +1,109 @@
1
+ import { calculateWeightedProgress } from './progressCalculator.js';
2
+ import { deduplicateActivities } from './activityDeduplicator.js';
3
+ import { initialSynthesisFromState } from './runSynthesis.js';
4
+
5
+ const DONE = new Set(['done', 'complete', 'completed', 'success', 'succeeded']);
6
+ const ACTIVE = new Set(['running', 'starting', 'queued']);
7
+
8
+ export function aggregateActivity(state = {}, events = []) {
9
+ const tasks = Array.isArray(state.plan) ? state.plan : [];
10
+ const activities = Array.isArray(state.activities) ? state.activities : [];
11
+ const progress = calculateWeightedProgress(tasks, activities);
12
+ const groups = groupTasks(tasks);
13
+ const lines = groups.length > 0
14
+ ? groups.map((group) => groupLine(group, activities))
15
+ : deduplicateActivities(activities).map(activityLine);
16
+ return {
17
+ initialSynthesis: initialSynthesisFromState(state, events),
18
+ progress,
19
+ lines,
20
+ };
21
+ }
22
+
23
+ function groupTasks(tasks) {
24
+ const groups = new Map();
25
+ for (const task of tasks) {
26
+ const id = task.groupId ?? task.dependsOnGroup ?? task.requiredCapability ?? task.id ?? task.step;
27
+ if (!groups.has(id)) groups.set(id, { id, label: groupLabel(task), tasks: [] });
28
+ groups.get(id).tasks.push(task);
29
+ }
30
+ return [...groups.values()];
31
+ }
32
+
33
+ function groupLabel(task) {
34
+ return task.groupLabel
35
+ ?? task.group?.label
36
+ ?? task.requiredCapability
37
+ ?? task.groupId
38
+ ?? task.dependsOnGroup
39
+ ?? task.label
40
+ ?? task.description
41
+ ?? `Task ${task.step ?? ''}`.trim();
42
+ }
43
+
44
+ function groupLine(group, activities) {
45
+ const total = group.tasks.length;
46
+ const done = group.tasks.filter((task) => DONE.has(statusOf(task))).length;
47
+ const running = group.tasks.filter((task) => ACTIVE.has(statusOf(task)));
48
+ const failed = group.tasks.find((task) => statusOf(task) === 'failed');
49
+ const waitingApproval = group.tasks.some((task) => ['pending_approval', 'waiting_approval'].includes(statusOf(task)));
50
+ const activeAgents = new Set(running.map((task) => task.agentInstanceId).filter(Boolean)).size;
51
+ const activeProgress = running
52
+ .map((task) => progressForTask(task, activities))
53
+ .find((value) => value != null);
54
+ let icon = '[ ]';
55
+ let status = 'en attente';
56
+ if (failed) {
57
+ icon = '[!]';
58
+ status = 'error';
59
+ } else if (done === total) {
60
+ icon = '[x]';
61
+ status = 'done';
62
+ } else if (running.length > 0) {
63
+ icon = '[...]';
64
+ status = activeProgress != null ? `${Math.round(activeProgress)} %` : `${done}/${total}`;
65
+ } else if (waitingApproval) {
66
+ icon = '[!]';
67
+ status = 'validation';
68
+ } else if (done > 0) {
69
+ icon = '[...]';
70
+ status = `${done}/${total}`;
71
+ }
72
+ const agents = activeAgents > 0 ? ` - ${activeAgents} agent${activeAgents > 1 ? 's' : ''}` : '';
73
+ return {
74
+ id: `group:${group.id}`,
75
+ label: `${icon} ${group.label} - ${status}${agents}`,
76
+ status,
77
+ progress: { done, total, percent: total > 0 ? Math.round((done / total) * 100) : null },
78
+ activeAgents,
79
+ };
80
+ }
81
+
82
+ function activityLine(activity) {
83
+ const percent = Number(activity?.progress?.percent);
84
+ const progress = Number.isFinite(percent) ? ` - ${Math.round(percent)} %` : '';
85
+ return {
86
+ id: activity.key ?? activity.id ?? activity.label,
87
+ label: `... ${activity.label ?? activity.source ?? 'Activity'}${progress}`,
88
+ status: activity.status ?? 'running',
89
+ progress: activity.progress ?? null,
90
+ };
91
+ }
92
+
93
+ function progressForTask(task, activities) {
94
+ const taskId = String(task.id ?? task.step ?? '');
95
+ const activityKey = task.activityKey ?? task.ownerActivityKey ?? null;
96
+ const match = activities.find((activity) =>
97
+ (activityKey && (activity.key === activityKey || activity.id === activityKey))
98
+ || String(activity?.progress?.stepId ?? '') === taskId,
99
+ );
100
+ const value = Number(match?.progress?.percent ?? task.progress?.percent);
101
+ return Number.isFinite(value) ? value : null;
102
+ }
103
+
104
+ function statusOf(task) {
105
+ const value = String(task?.status ?? '').toLowerCase();
106
+ if (['complete', 'completed', 'success', 'succeeded'].includes(value)) return 'done';
107
+ if (value === 'error') return 'failed';
108
+ return value || 'pending';
109
+ }
@@ -0,0 +1,60 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { aggregateActivity } from './activityAggregator.js';
5
+ import { visibleActivityEvents } from './activityDeduplicator.js';
6
+ import { calculateWeightedProgress } from './progressCalculator.js';
7
+
8
+ test('activityDeduplicator keeps one visible entry for repeated 2 percent polls', () => {
9
+ const events = Array.from({ length: 50 }, () => ({
10
+ type: 'activity_upserted',
11
+ payload: {
12
+ activity: {
13
+ id: 'ingest',
14
+ label: 'ingest',
15
+ status: 'running',
16
+ progress: { percent: 2, phase: 'ingest' },
17
+ },
18
+ },
19
+ }));
20
+
21
+ assert.equal(visibleActivityEvents(events).length, 1);
22
+ });
23
+
24
+ test('calculateWeightedProgress includes active task partial progress', () => {
25
+ const progress = calculateWeightedProgress([
26
+ { id: 'collect', status: 'done', progressWeight: 2 },
27
+ { id: 'build', status: 'running', progressWeight: 3, activityKey: 'activity-build' },
28
+ ], [
29
+ { key: 'activity-build', progress: { percent: 50 } },
30
+ ]);
31
+
32
+ assert.equal(progress.mode, 'weighted_tasks');
33
+ assert.equal(progress.percent, 70);
34
+ });
35
+
36
+ test('aggregateActivity exposes initial synthesis and grouped display lines', () => {
37
+ const activity = aggregateActivity({
38
+ plan: [
39
+ { id: 'collect', label: 'Collecte externe', groupId: 'collect', status: 'done', progressWeight: 1 },
40
+ { id: 'enrich', label: 'Enrichissement commercial', groupId: 'enrich', requiredCapability: 'customer-data.enrich', status: 'running', progressWeight: 1, activityKey: 'activity-enrich' },
41
+ { id: 'publish', label: 'Publication', groupId: 'publish', status: 'pending', progressWeight: 1 },
42
+ ],
43
+ activities: [{ key: 'activity-enrich', label: 'Enrichissement commercial', status: 'running', progress: { percent: 63, stepId: 'enrich' } }],
44
+ }, [{
45
+ type: 'plan.received',
46
+ payload: {
47
+ fragment: {
48
+ summary: {
49
+ initialSynthesis: ['120 sources detectees', '6 traitements simultanes recommandes'],
50
+ },
51
+ },
52
+ },
53
+ }]);
54
+
55
+ assert.deepEqual(activity.initialSynthesis, ['120 sources detectees', '6 traitements simultanes recommandes']);
56
+ assert.equal(activity.progress.percent, 54);
57
+ assert.ok(activity.lines.some((line) => /\[x\] collect - done/.test(line.label)));
58
+ assert.ok(activity.lines.some((line) => /\[\.\.\.\] customer-data\.enrich - 63 %/.test(line.label)));
59
+ assert.ok(activity.lines.some((line) => /\[ \] publish - en attente/.test(line.label)));
60
+ });
@@ -0,0 +1,50 @@
1
+ export function activitySignature(entry = {}) {
2
+ const progress = entry.progress ?? {};
3
+ return JSON.stringify({
4
+ id: entry.id ?? entry.key ?? entry.label ?? null,
5
+ status: normalize(entry.status),
6
+ phase: progress.phase ?? progress.step ?? progress.stepId ?? null,
7
+ progressBucket: progressBucket(progress.percent),
8
+ done: progress.done ?? progress.completed ?? progress.sourceDoneCount ?? null,
9
+ total: progress.total ?? progress.sourceCount ?? null,
10
+ activeAgents: entry.activeAgents ?? progress.activeAgents ?? null,
11
+ error: entry.error ?? null,
12
+ retries: entry.retries ?? progress.retries ?? null,
13
+ approval: entry.approval ?? entry.approvalStatus ?? null,
14
+ blocking: entry.blocking ?? entry.blocked ?? null,
15
+ });
16
+ }
17
+
18
+ export function deduplicateActivities(entries = []) {
19
+ const seen = new Set();
20
+ const result = [];
21
+ for (const entry of entries) {
22
+ const signature = activitySignature(entry);
23
+ if (seen.has(signature)) continue;
24
+ seen.add(signature);
25
+ result.push(entry);
26
+ }
27
+ return result;
28
+ }
29
+
30
+ export function visibleActivityEvents(events = []) {
31
+ const visible = [];
32
+ let previous = null;
33
+ for (const event of events) {
34
+ const activity = event?.payload?.activity ?? event?.activity ?? event;
35
+ const signature = activitySignature(activity);
36
+ if (signature !== previous) visible.push(event);
37
+ previous = signature;
38
+ }
39
+ return visible;
40
+ }
41
+
42
+ function progressBucket(value) {
43
+ const number = Number(value);
44
+ if (!Number.isFinite(number)) return null;
45
+ return Math.floor(Math.max(0, Math.min(100, number)) / 5) * 5;
46
+ }
47
+
48
+ function normalize(value) {
49
+ return String(value ?? '').toLowerCase();
50
+ }
@@ -0,0 +1,61 @@
1
+ const TERMINAL_DONE = new Set(['done', 'complete', 'completed', 'success', 'succeeded']);
2
+ const TERMINAL_ANY = new Set([...TERMINAL_DONE, 'failed', 'cancelled', 'canceled', 'error']);
3
+
4
+ export function calculateWeightedProgress(tasks = [], activities = []) {
5
+ const items = Array.isArray(tasks) ? tasks : [];
6
+ if (items.length === 0) return { mode: 'indeterminate', percent: null, done: 0, total: 0 };
7
+ const totalWeight = items.reduce((sum, task) => sum + taskWeight(task), 0) || items.length;
8
+ let completedWeight = 0;
9
+ let done = 0;
10
+ for (const task of items) {
11
+ const weight = taskWeight(task);
12
+ const status = normalizeStatus(task.status);
13
+ if (TERMINAL_DONE.has(status)) {
14
+ completedWeight += weight;
15
+ done += 1;
16
+ } else if (!TERMINAL_ANY.has(status)) {
17
+ completedWeight += weight * taskProgressRatio(task, activities);
18
+ }
19
+ }
20
+ return {
21
+ mode: 'weighted_tasks',
22
+ percent: Math.round((completedWeight / totalWeight) * 100),
23
+ done,
24
+ total: items.length,
25
+ completedWeight,
26
+ totalWeight,
27
+ };
28
+ }
29
+
30
+ export function taskProgressRatio(task, activities = []) {
31
+ const direct = progressPercent(task?.progress);
32
+ if (direct != null) return direct / 100;
33
+ const taskId = String(task?.id ?? task?.step ?? task?.stepId ?? '');
34
+ const activityKey = task?.activityKey ?? task?.ownerActivityKey ?? null;
35
+ const match = (activities ?? []).find((activity) =>
36
+ (activityKey && (activity.key === activityKey || activity.id === activityKey))
37
+ || String(activity?.progress?.stepId ?? '') === taskId
38
+ || String(activity?.raw?.progress?.stepId ?? '') === taskId,
39
+ );
40
+ const activityPercent = progressPercent(match?.progress ?? match?.raw?.progress);
41
+ return activityPercent == null ? 0 : activityPercent / 100;
42
+ }
43
+
44
+ export function progressPercent(progress) {
45
+ const value = Number(progress?.percent);
46
+ if (!Number.isFinite(value)) return null;
47
+ return Math.max(0, Math.min(100, value));
48
+ }
49
+
50
+ function taskWeight(task) {
51
+ const value = Number(task?.progressWeight);
52
+ return Number.isFinite(value) && value > 0 ? value : 1;
53
+ }
54
+
55
+ function normalizeStatus(status) {
56
+ const value = String(status ?? '').toLowerCase();
57
+ if (value === 'error') return 'failed';
58
+ if (value === 'canceled') return 'cancelled';
59
+ if (value === 'complete' || value === 'completed' || value === 'success') return 'done';
60
+ return value || 'pending';
61
+ }
@@ -0,0 +1,15 @@
1
+ export function initialSynthesisFromEvents(events = []) {
2
+ for (const event of [...(events ?? [])].reverse()) {
3
+ const synthesis = event.payload?.fragment?.summary?.initialSynthesis
4
+ ?? event.payload?.normalizedFragment?.summary?.initialSynthesis
5
+ ?? event.payload?.summary?.initialSynthesis;
6
+ if (Array.isArray(synthesis)) return synthesis.map(String);
7
+ }
8
+ return [];
9
+ }
10
+
11
+ export function initialSynthesisFromState(state = {}, events = []) {
12
+ const direct = state.summary?.initialSynthesis ?? state.activity?.initialSynthesis;
13
+ if (Array.isArray(direct)) return direct.map(String);
14
+ return initialSynthesisFromEvents(events);
15
+ }
@@ -9,7 +9,7 @@ import {
9
9
  } from '../core/mcp.js';
10
10
  import { formatSkillsForAgent, readOptionalText } from '../core/skills.js';
11
11
  import { handleSlashCommand } from '../commands/slash.js';
12
- import { extractActivity, formatActivitySummary, parseJsonText } from '../core/activity.js';
12
+ import { extractActivity, formatActivitySummary, parseJsonText, sessionActivities } from '../core/activity.js';
13
13
  import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
14
14
  import { enqueueProductionJob, ensureJobQueue, formatQueue, productionLockBusy } from '../core/jobQueue.js';
15
15
  import { updateWorkspaceProfilePreference } from '../core/profile.js';
@@ -124,17 +124,16 @@ const WIKI_PLAN_SET_TOOL = {
124
124
  properties: {
125
125
  id: { type: 'string' },
126
126
  description: { type: 'string' },
127
+ requiredCapability: { type: ['string', 'null'] },
127
128
  status: { type: 'string', enum: ['pending', 'queued', 'running', 'waiting', 'pending_approval', 'done', 'failed', 'cancelled', 'stalled', 'added_during_run'] },
128
129
  dependsOn: { type: 'array', items: { type: 'string' } },
129
- executor: { type: ['string', 'null'] },
130
- executorQuery: { type: ['object', 'null'], additionalProperties: true },
131
130
  outputRefs: { type: 'array', items: { type: 'string' } },
132
131
  },
133
132
  required: ['description'],
134
133
  },
135
134
  ],
136
135
  },
137
- description: 'Ordered steps. Backward-compatible strings are accepted; structured steps may include id, dependsOn, executor, executorQuery, outputRefs.',
136
+ description: 'Ordered steps. Backward-compatible strings are accepted; structured steps may include id, requiredCapability, dependsOn, outputRefs.',
138
137
  },
139
138
  },
140
139
  required: ['steps'],
@@ -180,6 +179,7 @@ const AgentState = Annotation.Root({
180
179
  }),
181
180
  toolIterations: Annotation({ default: () => 0 }),
182
181
  pendingToolCalls: Annotation(),
182
+ inputClassification: Annotation(),
183
183
  readyToStream: Annotation(),
184
184
  streamContext: Annotation(),
185
185
  streamedInline: Annotation(),
@@ -460,7 +460,7 @@ function handleWikiTool(session, tool, args) {
460
460
  return `Unknown wiki tool: ${tool}`;
461
461
  }
462
462
 
463
- function normalizeDeclaredPlanStep(raw, index, session) {
463
+ function normalizeDeclaredPlanStep(raw, index) {
464
464
  const item = raw && typeof raw === 'object' && !Array.isArray(raw)
465
465
  ? raw
466
466
  : { description: String(raw) };
@@ -471,8 +471,9 @@ function normalizeDeclaredPlanStep(raw, index, session) {
471
471
  description,
472
472
  status: item.status ?? 'pending',
473
473
  dependsOn: Array.isArray(item.dependsOn) ? item.dependsOn.map(String) : [],
474
- executor: item.executor ?? selectExecutorForStep(description, session),
475
- executorQuery: item.executorQuery ?? null,
474
+ requiredCapability: item.requiredCapability != null ? String(item.requiredCapability) : null,
475
+ executor: null,
476
+ executorQuery: null,
476
477
  outputRefs: Array.isArray(item.outputRefs) ? item.outputRefs.map(String) : [],
477
478
  };
478
479
  }
@@ -488,23 +489,6 @@ function slugStepId(description, index) {
488
489
  return slug || `task-${index + 1}`;
489
490
  }
490
491
 
491
- function selectExecutorForStep(description, session) {
492
- const text = String(description ?? '').toLowerCase();
493
- let fallback = null;
494
- for (const [serverName, value] of Object.entries(session.mcp ?? {})) {
495
- if (value.status !== 'connected') continue;
496
- for (const tool of value.tools ?? []) {
497
- const executor = `${serverName}.${tool.name}`;
498
- fallback ??= executor;
499
- const haystack = `${serverName} ${tool.name} ${tool.description ?? ''}`.toLowerCase();
500
- if (text.split(/[^a-z0-9]+/).filter((token) => token.length >= 4).some((token) => haystack.includes(token))) {
501
- return executor;
502
- }
503
- }
504
- }
505
- return fallback;
506
- }
507
-
508
492
  // The manager runs on the same host filesystem as the workspace directory
509
493
  // (this is the same local file wiki__profile_update writes to via its
510
494
  // volume-mounted container), so read it fresh on every turn instead of
@@ -535,6 +519,7 @@ export function buildAgentSystemPrompt(state) {
535
519
  `Current workspace: ${workspace}.`,
536
520
  `Current wikirc profile: ${wikirc}.`,
537
521
  `Available primitives: ${commandList(state.session)}.`,
522
+ 'Only announce or call slash commands that appear exactly in Available primitives. Do not invent command names, subcommands, or arguments.',
538
523
  'Connected MCP tools (use the server__tool naming convention for tool calls):',
539
524
  mcpTools,
540
525
  'Current local MCP job queue:',
@@ -556,8 +541,8 @@ export function buildAgentSystemPrompt(state) {
556
541
  '',
557
542
  'Task startup:',
558
543
  ' 1. If the next MCP tool returns _activity.plan.steps, call that tool directly; the shell will create the visible plan from the returned activity.',
559
- ' 2. If the tool cannot declare its own plan, call wiki__plan_set before executing the first step. Prefer structured steps: {id, description, dependsOn, executor, executorQuery, outputRefs}; a legacy list of strings is still accepted.',
560
- ' Multi-tool example: wiki__plan_set(steps=[{id:"cme-export",description:"CME export",dependsOn:[],executor:"cme.cme_export_run",outputRefs:["raw/untracked"]},{id:"production",description:"Production pipeline",dependsOn:["cme-export"],executor:"production.production_start_job",outputRefs:["deliverables"]}])',
544
+ ' 2. If the tool cannot declare its own plan, call wiki__plan_set before executing the first step. Prefer structured steps: {id, description, requiredCapability, dependsOn, outputRefs}; a legacy list of strings is still accepted.',
545
+ ' Multi-tool example: wiki__plan_set(steps=[{id:"cme-export",description:"CME export",requiredCapability:"external-source.export",dependsOn:[],outputRefs:["raw/untracked"]},{id:"production",description:"Production pipeline",requiredCapability:"knowledge.pipeline",dependsOn:["cme-export"],outputRefs:["deliverables"]}])',
561
546
  ' 3. Immediately execute the first step using the appropriate MCP tool. Do not start step 2 in the same turn unless one async pipeline tool owns and declares the whole sequence.',
562
547
  ' For synchronous steps (result is immediate, no _activity polling), call wiki__plan_done(step=1) after confirming success.',
563
548
  ' For async MCP jobs (returns _activity with poll), the orchestrator tracks completion automatically.',
@@ -575,7 +560,7 @@ export function buildAgentSystemPrompt(state) {
575
560
  '',
576
561
  'On failure: if a completed activity is failed/error/cancelled, call wiki__plan_done(step=N, status="failed") then stop with a clear error report.',
577
562
  ].filter(Boolean).join('\n'),
578
- 'For service actions, recommend /services, /start, /stop or /logs with the exact service name.',
563
+ 'For service actions, recommend only available service primitives from Available primitives, with the exact service name when the primitive supports one.',
579
564
  'Disambiguate export requests carefully.',
580
565
  'Confluence/CME/source export means exporting external Confluence sources into raw/untracked: use cme MCP tools (`cme_export_run`, then `cme_export_status`). Never use production `type=export` for Confluence source export.',
581
566
  'Wiki/deliverable/publication export means exporting generated deliverables from the wiki: use production MCP tools (`production_start_job` with `type:"export"` or pipeline steps). Require the deliverable path when exporting deliverables.',
@@ -617,6 +602,36 @@ export function formatLlmUnavailableMessage(reason) {
617
602
  return `⚠ LLM injoignable : ${clean || 'raison inconnue'}`;
618
603
  }
619
604
 
605
+ function classifyAgentInput(input, session) {
606
+ const lower = String(input ?? '').toLowerCase();
607
+ const hasActiveRun = session?.agentProjection?.status === 'running'
608
+ || sessionActivities(session).some((activity) => !activity.terminal);
609
+ if (/\b(valide tout|approve all|approve|approuve|valid[eé]|ok pour tout|go pour tout)\b/i.test(lower)) {
610
+ return { kind: 'approve', confidence: 0.86, reason: 'approval_request', activeRun: hasActiveRun };
611
+ }
612
+ if (/\b(cancel|annule|stop|arr[eê]te|interromps|abort)\b/i.test(lower)) {
613
+ return { kind: 'cancel', confidence: 0.86, reason: 'cancel_request', activeRun: hasActiveRun };
614
+ }
615
+ if (/\b(plus tard|later|ensuite|apr[eè]s ce run|enqueue|mets en file|met en file|futur|next run|future run)\b/i.test(lower)) {
616
+ return { kind: 'enqueue_run', confidence: 0.8, reason: 'future_run_request', activeRun: hasActiveRun };
617
+ }
618
+ if (/\b(o[uù] en es[t-]|status|statut|progress|progression|run|job|queue|logs?|explique|explain|inspect|show|montre|quoi de neuf)\b/i.test(lower)) {
619
+ return { kind: 'observe', confidence: 0.86, reason: 'status_or_explanation_request', activeRun: hasActiveRun };
620
+ }
621
+ if (hasActiveRun && /\b(ajoute|add|change|modifie|modify|remplace|replace|retire|remove|skip|ignore|plan|step|t[aâ]che)\b/i.test(lower)) {
622
+ return { kind: 'modify_run', confidence: 0.78, reason: 'active_run_change_request', activeRun: hasActiveRun };
623
+ }
624
+ if (hasActiveRun && /\b(lance|run|g[eé]n[eè]re|build|export|cr[eé]e|create|send|envoie|ingest|convert|importe|import)\b/i.test(lower)) {
625
+ return { kind: 'ambiguous', confidence: 0.45, reason: 'active_run_action_is_ambiguous', activeRun: hasActiveRun };
626
+ }
627
+ return { kind: 'converse', confidence: 0.62, reason: 'plain_conversation', activeRun: hasActiveRun };
628
+ }
629
+
630
+ function toolsForClassification(classification, writeTools) {
631
+ if (classification.activeRun && ['converse', 'observe'].includes(classification.kind)) return [SHELL_READ_COMMAND_TOOL];
632
+ return [SHELL_READ_COMMAND_TOOL, ...writeTools];
633
+ }
634
+
620
635
  export function createAgentGraph(options = {}) {
621
636
  async function orchestratorNode(state) {
622
637
  const llm = state.session.llm ?? options.llm ?? null;
@@ -640,15 +655,33 @@ export function createAgentGraph(options = {}) {
640
655
  state.session._onStep?.('Agent: planning next action…');
641
656
  }
642
657
 
643
- const allTools = [
658
+ const classification = iterations === 0
659
+ ? classifyAgentInput(state.input, state.session)
660
+ : (state.inputClassification ?? { kind: 'modify_run', confidence: 1, reason: 'tool_iteration' });
661
+ if (iterations === 0) {
662
+ state.session._onStep?.(`Agent: classified input as ${classification.kind}`);
663
+ emitAgentEvent(state.session, 'control_message_received', 'agent_classifier', {
664
+ input: state.input,
665
+ classification,
666
+ });
667
+ }
668
+ if (iterations === 0 && classification.kind === 'ambiguous') {
669
+ return {
670
+ response: 'Je peux répondre sur le run en cours, modifier ce run, mettre une nouvelle demande en file, approuver, ou annuler. Peux-tu préciser ce que tu veux faire ?',
671
+ pendingToolCalls: null,
672
+ readyToStream: false,
673
+ inputClassification: classification,
674
+ };
675
+ }
676
+
677
+ const writeTools = [
644
678
  SHELL_RUN_COMMAND_TOOL,
645
- SHELL_READ_COMMAND_TOOL,
646
679
  SHELL_PROFILE_UPDATE_TOOL,
647
680
  WIKI_PLAN_SET_TOOL,
648
681
  WIKI_PLAN_DONE_TOOL,
649
682
  ...buildLlmTools(state.session.mcp),
650
683
  ];
651
- const tools = allTools;
684
+ const tools = toolsForClassification(classification, writeTools);
652
685
  const system = buildAgentSystemPrompt(state);
653
686
 
654
687
  // On iteration 0: prior history is in state.messages, user input must be appended.
@@ -690,6 +723,7 @@ export function createAgentGraph(options = {}) {
690
723
  messages: newMessages,
691
724
  toolIterations: iterations + 1,
692
725
  readyToStream: false,
726
+ inputClassification: classification,
693
727
  };
694
728
  }
695
729
 
@@ -705,6 +739,7 @@ export function createAgentGraph(options = {}) {
705
739
  readyToStream: false,
706
740
  streamedInline: true,
707
741
  messages: newMessages,
742
+ inputClassification: classification,
708
743
  };
709
744
  }
710
745