@dotdrelle/wiki-manager 0.11.10 → 0.12.1
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 +101 -38
- package/src/agent/graph.test.js +75 -5
- package/src/cli/wiki-manager.js +22 -0
- package/src/contracts/schemas.js +249 -13
- package/src/contracts/schemas.test.js +147 -2
- 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 +30 -3
- package/src/core/mcp.test.js +69 -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/controlMessages.js +43 -0
- package/src/runtime/controlMessages.test.js +21 -0
- package/src/runtime/donna-contract.test.js +296 -33
- 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 +142 -24
- package/src/runtime/server.test.js +192 -62
- package/src/runtime/store.js +837 -2
- package/src/runtime/store.test.js +269 -28
- 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 +7 -11
- package/src/shell/repl.test.js +26 -5
- package/src/shell/tui.tsx +1 -0
- package/src/shell/useAgent.ts +5 -1
- package/src/shell/useSession.ts +75 -19
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.1",
|
|
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/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",
|
|
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
|
+
}
|