@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.
- package/README.md +32 -7
- package/agents.docker-compose.yml +3 -0
- package/docker-compose.yml +1 -0
- package/package.json +3 -2
- package/src/activity/activityAggregator.js +109 -0
- package/src/activity/activityAggregator.test.js +60 -0
- package/src/activity/activityDeduplicator.js +50 -0
- package/src/activity/progressCalculator.js +61 -0
- package/src/activity/runSynthesis.js +15 -0
- package/src/agent/graph.js +65 -30
- package/src/agent/graph.test.js +28 -5
- package/src/cli/wiki-manager.js +22 -0
- package/src/contracts/schemas.js +249 -13
- package/src/contracts/schemas.test.js +145 -0
- package/src/core/activity.js +8 -5
- package/src/core/agentEvents.js +260 -23
- package/src/core/agentEvents.test.js +53 -0
- package/src/core/compose.js +6 -1
- package/src/core/dockerCompose.test.js +12 -0
- package/src/core/documentIntake.js +33 -38
- package/src/core/documentIntake.test.js +25 -0
- package/src/core/env.js +11 -1
- package/src/core/jobQueue.js +26 -2
- package/src/core/mcp.js +1 -1
- package/src/core/plan.js +46 -4
- package/src/core/plan.test.js +16 -1
- package/src/core/planPatch.js +64 -3
- package/src/core/planPatch.test.js +110 -1
- package/src/core/queueStore.test.js +21 -0
- package/src/core/runtimeLog.js +119 -0
- package/src/core/runtimeLog.test.js +84 -0
- package/src/core/wikirc.js +22 -0
- package/src/core/wikirc.test.js +49 -1
- package/src/core/workflow.js +14 -3
- package/src/graph/graphAggregator.js +5 -0
- package/src/graph/graphPatch.js +8 -0
- package/src/graph/graphSnapshot.js +14 -0
- package/src/graph/graphVisibilityPolicy.js +40 -0
- package/src/graph/runGraphProjector.js +87 -0
- package/src/graph/runGraphProjector.test.js +111 -0
- package/src/orchestrator/agentRegistry.js +172 -0
- package/src/orchestrator/agentRegistry.test.js +114 -0
- package/src/orchestrator/approvalPolicy.js +126 -0
- package/src/orchestrator/approvalPolicy.test.js +78 -0
- package/src/orchestrator/assignmentManager.js +80 -0
- package/src/orchestrator/attemptManager.js +168 -0
- package/src/orchestrator/attemptManager.test.js +160 -0
- package/src/orchestrator/budgetManager.js +127 -0
- package/src/orchestrator/capabilityRegistry.js +64 -0
- package/src/orchestrator/capabilityRegistry.test.js +59 -0
- package/src/orchestrator/capabilityResolver.js +151 -0
- package/src/orchestrator/capabilityResolver.test.js +127 -0
- package/src/orchestrator/dependencyResolver.js +112 -0
- package/src/orchestrator/dispatcher.js +302 -0
- package/src/orchestrator/lockManager.js +51 -0
- package/src/orchestrator/planIntegrator.js +268 -0
- package/src/orchestrator/planIntegrator.test.js +213 -0
- package/src/orchestrator/planValidator.js +534 -0
- package/src/orchestrator/planValidator.test.js +262 -0
- package/src/orchestrator/resultAggregator.js +248 -0
- package/src/orchestrator/resultAggregator.test.js +211 -0
- package/src/orchestrator/scheduler.js +103 -0
- package/src/orchestrator/scheduler.test.js +138 -0
- package/src/runtime/approvals.js +130 -1
- package/src/runtime/donna-contract.test.js +293 -30
- package/src/runtime/recoveryManager.js +176 -0
- package/src/runtime/recoveryManager.test.js +162 -0
- package/src/runtime/runner.e2e.test.js +98 -12
- package/src/runtime/runner.js +261 -227
- package/src/runtime/runner.test.js +249 -452
- package/src/runtime/server.js +137 -20
- package/src/runtime/server.test.js +137 -7
- package/src/runtime/store.js +837 -2
- package/src/runtime/store.test.js +245 -4
- package/src/runtime/supervisor.js +33 -1
- package/src/runtime/supervisor.test.js +47 -1
- package/src/shell/RightPane.tsx +8 -5
- package/src/shell/repl.js +4 -9
- package/src/shell/repl.test.js +25 -4
- package/src/shell/tui.tsx +1 -0
- 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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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:
|
package/docker-compose.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.
|
|
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
|
+
}
|
package/src/agent/graph.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
475
|
-
|
|
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,
|
|
560
|
-
' Multi-tool example: wiki__plan_set(steps=[{id:"cme-export",description:"CME export",dependsOn:[],
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|