@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.
Files changed (85) 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 +101 -38
  11. package/src/agent/graph.test.js +75 -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 +147 -2
  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 +30 -3
  25. package/src/core/mcp.test.js +69 -1
  26. package/src/core/plan.js +46 -4
  27. package/src/core/plan.test.js +16 -1
  28. package/src/core/planPatch.js +64 -3
  29. package/src/core/planPatch.test.js +110 -1
  30. package/src/core/queueStore.test.js +21 -0
  31. package/src/core/runtimeLog.js +119 -0
  32. package/src/core/runtimeLog.test.js +84 -0
  33. package/src/core/wikirc.js +22 -0
  34. package/src/core/wikirc.test.js +49 -1
  35. package/src/core/workflow.js +14 -3
  36. package/src/graph/graphAggregator.js +5 -0
  37. package/src/graph/graphPatch.js +8 -0
  38. package/src/graph/graphSnapshot.js +14 -0
  39. package/src/graph/graphVisibilityPolicy.js +40 -0
  40. package/src/graph/runGraphProjector.js +87 -0
  41. package/src/graph/runGraphProjector.test.js +111 -0
  42. package/src/orchestrator/agentRegistry.js +172 -0
  43. package/src/orchestrator/agentRegistry.test.js +114 -0
  44. package/src/orchestrator/approvalPolicy.js +126 -0
  45. package/src/orchestrator/approvalPolicy.test.js +78 -0
  46. package/src/orchestrator/assignmentManager.js +80 -0
  47. package/src/orchestrator/attemptManager.js +168 -0
  48. package/src/orchestrator/attemptManager.test.js +160 -0
  49. package/src/orchestrator/budgetManager.js +127 -0
  50. package/src/orchestrator/capabilityRegistry.js +64 -0
  51. package/src/orchestrator/capabilityRegistry.test.js +59 -0
  52. package/src/orchestrator/capabilityResolver.js +151 -0
  53. package/src/orchestrator/capabilityResolver.test.js +127 -0
  54. package/src/orchestrator/dependencyResolver.js +112 -0
  55. package/src/orchestrator/dispatcher.js +302 -0
  56. package/src/orchestrator/lockManager.js +51 -0
  57. package/src/orchestrator/planIntegrator.js +268 -0
  58. package/src/orchestrator/planIntegrator.test.js +213 -0
  59. package/src/orchestrator/planValidator.js +534 -0
  60. package/src/orchestrator/planValidator.test.js +262 -0
  61. package/src/orchestrator/resultAggregator.js +248 -0
  62. package/src/orchestrator/resultAggregator.test.js +211 -0
  63. package/src/orchestrator/scheduler.js +103 -0
  64. package/src/orchestrator/scheduler.test.js +138 -0
  65. package/src/runtime/approvals.js +130 -1
  66. package/src/runtime/controlMessages.js +43 -0
  67. package/src/runtime/controlMessages.test.js +21 -0
  68. package/src/runtime/donna-contract.test.js +296 -33
  69. package/src/runtime/recoveryManager.js +176 -0
  70. package/src/runtime/recoveryManager.test.js +162 -0
  71. package/src/runtime/runner.e2e.test.js +98 -12
  72. package/src/runtime/runner.js +261 -227
  73. package/src/runtime/runner.test.js +249 -452
  74. package/src/runtime/server.js +142 -24
  75. package/src/runtime/server.test.js +192 -62
  76. package/src/runtime/store.js +837 -2
  77. package/src/runtime/store.test.js +269 -28
  78. package/src/runtime/supervisor.js +33 -1
  79. package/src/runtime/supervisor.test.js +47 -1
  80. package/src/shell/RightPane.tsx +8 -5
  81. package/src/shell/repl.js +7 -11
  82. package/src/shell/repl.test.js +26 -5
  83. package/src/shell/tui.tsx +1 -0
  84. package/src/shell/useAgent.ts +5 -1
  85. 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
- 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.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
+ }