@nanobpm/bojtos-kit 0.5.0 → 0.6.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 CHANGED
@@ -27,6 +27,27 @@ Every command returns the post-run `Snapshot`: `activeElementIds` /
27
27
  For React, use [`@nanobpm/bojtos-react`](../bojtos-react), which owns the session
28
28
  lifecycle and reactive state on top of this kit.
29
29
 
30
+ ## Trace model
31
+
32
+ The kit also holds the framework-agnostic **trace model** the shared
33
+ `<TraceTimeline>` (in `@nanobpm/bojtos-react`) renders — one normalized
34
+ row/turn-group model plus the two adapters that map a source into it, so the two
35
+ formerly forked timelines share one fold instead of drifting apart:
36
+
37
+ - **`foldEngineEvents(events)`** — the engine-event fold: a `WasmEvent[]` (from
38
+ `session.events()` / `useBojtos().events`) → normalized `TraceRow[]`, keeping the
39
+ run's milestones and dropping low-signal lifecycle noise. The non-agentic /
40
+ test-view case.
41
+ - **`traceEntriesToRows(entries)`** — the handler-emitted adapter: agent/tool/turn
42
+ `TraceEntry` lines (with the additive `turn` grouping field) → `TraceRow[]`. The
43
+ agentic web-demo case.
44
+ - **`buildTraceItems(rows)`** — folds consecutive same-`turn` rows into
45
+ `TraceTurnGroup`s; rows with no `turn` stay flat. `isTraceTurnGroup` narrows an
46
+ item. This is the grouping the view consumes.
47
+
48
+ It is pure and React-free (no React import in the kit), keeping the presentational
49
+ layer thin.
50
+
30
51
  ## Build
31
52
 
32
53
  `dist/` (the tsc-emitted JS + `.d.ts`) is what ships, built by `prepack` on
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export { ensureWasm, createBojtosSession, type BojtosSession, type WasmSource, } from "./session.js";
2
2
  export { dispatchWorkers, dispatchRound, settleReason, unhandledJobTypes, JobFailure, type JobHandler, type JobResult, type AgentHandler, type DispatchOptions, type DispatchResult, type RoundResult, type SettleReason, } from "./worker.js";
3
+ export { buildTraceItems, isTraceTurnGroup, foldEngineEvents, traceEntriesToRows, } from "./trace.js";
4
+ export type { TraceRowKind, TraceEntry, TraceRow, TraceTurnGroup, TraceItem, TraceAdapter, } from "./trace.js";
3
5
  export type { Snapshot, InstanceDto, JobDto, ActivatedJob, IncidentDto, TimerDto, UserTaskDto, MessageSubscriptionDto, SignalSubscriptionDto, ElementStatDto, SequenceFlowDto, DecisionInstanceDto, ActiveEl, ActivateInstruction, AgentActivation, AgentResult, WasmEvent, } from "./types.js";
package/dist/index.js CHANGED
@@ -3,3 +3,7 @@
3
3
  // runner and re-exports the engine's snapshot/event contract types.
4
4
  export { ensureWasm, createBojtosSession, } from "./session.js";
5
5
  export { dispatchWorkers, dispatchRound, settleReason, unhandledJobTypes, JobFailure, } from "./worker.js";
6
+ // The shared trace model + both adapters (engine-event fold and handler-emitted
7
+ // `TraceEntry`) that retired the two forked `TraceTimeline` copies (#9). Pure and
8
+ // React-free — the presentational component lives in @nanobpm/bojtos-react.
9
+ export { buildTraceItems, isTraceTurnGroup, foldEngineEvents, traceEntriesToRows, } from "./trace.js";
@@ -0,0 +1,97 @@
1
+ import type { WasmEvent } from "./types.js";
2
+ /**
3
+ * Semantic classification of a trace row, driving how the view styles it (the
4
+ * `log-<kind>` class the two forked timelines already keyed off) and which
5
+ * affordance it carries. Framework-agnostic — an engine-event fold and an
6
+ * agentic handler stream both land in this shared vocabulary:
7
+ *
8
+ * - `start` — the run/instance began.
9
+ * - `agent` — an agent decision, or a tool the agent activated this turn.
10
+ * - `llm` — a raw model reply.
11
+ * - `tool` — a tool/handler log line (a job running, a timer, a message).
12
+ * - `human` — a user task awaiting or completed by a person.
13
+ * - `done` — the final outcome (the instance completed/terminated).
14
+ * - `error` — a failure, incident, or thrown error.
15
+ * - `vars` — a variables/result update (e.g. what a tool returned).
16
+ */
17
+ export type TraceRowKind = "start" | "agent" | "llm" | "tool" | "human" | "done" | "error" | "vars";
18
+ /**
19
+ * A source line before it is placed in the normalized model. This is the shape a
20
+ * handler emits (the web-demo framework's `TraceEntry`): `kind`/`text` are all a
21
+ * plain consumer needs; every other field is additive and safe to ignore.
22
+ */
23
+ export interface TraceEntry {
24
+ kind: TraceRowKind;
25
+ /** The human-readable line. */
26
+ text: string;
27
+ /**
28
+ * Stable id for an entry that updates in place — a streaming completion grows
29
+ * one line rather than spamming forty.
30
+ */
31
+ key?: string;
32
+ /** True while the entry is still being produced (renders a spinner). */
33
+ pending?: boolean;
34
+ /**
35
+ * Groups every entry produced by one agent turn together (the streamed LLM
36
+ * reply, each tool it activated, and that tool's result). Consecutive entries
37
+ * sharing a `turn` fold into one turn card; entries with no `turn` render as
38
+ * plain rows in their original order.
39
+ */
40
+ turn?: number;
41
+ /** The BPMN element (tool or task) this entry concerns. */
42
+ elementId?: string;
43
+ /** Arguments supplied when activating a tool — the coerced values, not the raw reply. */
44
+ args?: Record<string, unknown>;
45
+ /** What a tool/handler returned, paired with its activation by `elementId`. */
46
+ result?: unknown;
47
+ }
48
+ /**
49
+ * A normalized row: a {@link TraceEntry} stamped with a stable, monotonic `id`.
50
+ * The `id` is what the view keys off and what pairs a tool's result with its
51
+ * activation and orders loose lines within a turn.
52
+ */
53
+ export interface TraceRow extends TraceEntry {
54
+ id: number;
55
+ }
56
+ /** Consecutive same-turn rows, folded into one group by {@link buildTraceItems}. */
57
+ export interface TraceTurnGroup {
58
+ turn: number;
59
+ rows: TraceRow[];
60
+ }
61
+ /** A top-level item the view renders: either a plain row or a turn group. */
62
+ export type TraceItem = TraceRow | TraceTurnGroup;
63
+ /** Narrow a {@link TraceItem} to a {@link TraceTurnGroup}. */
64
+ export declare function isTraceTurnGroup(item: TraceItem): item is TraceTurnGroup;
65
+ /**
66
+ * An adapter maps a source (`WasmEvent[]`, a `TraceEntry[]`, …) into the shared
67
+ * normalized row model. Both built-in adapters — {@link foldEngineEvents} and
68
+ * {@link traceEntriesToRows} — satisfy this; a consumer can supply its own for a
69
+ * bespoke source.
70
+ */
71
+ export type TraceAdapter<TSource> = (source: TSource) => TraceRow[];
72
+ /**
73
+ * Fold a flat list of normalized rows into the view model: consecutive rows
74
+ * sharing a `turn` become one {@link TraceTurnGroup}; everything else stays a
75
+ * plain row in its original order. A row with no `turn` breaks the current group,
76
+ * so an interleaved non-turn line never gets swallowed into a card. This is the
77
+ * grouping both forked timelines did by hand, lifted into the shared kit.
78
+ */
79
+ export declare function buildTraceItems(rows: TraceRow[]): TraceItem[];
80
+ /**
81
+ * The handler-emitted `TraceEntry` adapter: stamp each entry with a stable `id`
82
+ * (its index) to lift it into a {@link TraceRow}. The entries already carry the
83
+ * additive `turn`/`elementId`/`args`/`result` fields, so {@link buildTraceItems}
84
+ * over the result reproduces the agentic turn-grouped card view without any
85
+ * re-forked grouping logic. The input is never mutated.
86
+ */
87
+ export declare function traceEntriesToRows(entries: readonly TraceEntry[]): TraceRow[];
88
+ /**
89
+ * The engine-event fold adapter: map a `WasmEvent[]` (the flattened
90
+ * `{ seq, now, type, …snake_case }` stream from `useBojtos().events`) into
91
+ * normalized rows, keeping only the meaningful milestones (see
92
+ * {@link ENGINE_EVENT_RULES}). Each row's `id` is the event's `seq`, so ids stay
93
+ * stable and monotonic across re-reads of a growing log. Engine events carry no
94
+ * turn, so {@link buildTraceItems} over the result is a flat list of rows — the
95
+ * non-agentic test-view shape.
96
+ */
97
+ export declare function foldEngineEvents(events: readonly WasmEvent[]): TraceRow[];
package/dist/trace.js ADDED
@@ -0,0 +1,185 @@
1
+ // The framework-agnostic trace model shared by the Bojtos demo framework and the
2
+ // console test-view — the single source that retired the two drifted, forked
3
+ // `TraceTimeline` copies (nanobpm/bojtos#9). It defines one normalized row/turn
4
+ // model plus the two adapters that map a source into it:
5
+ //
6
+ // 1. the **engine-event fold** — `WasmEvent[]` (from `useBojtos().events`) →
7
+ // rows, covering the non-agentic / test-view case; and
8
+ // 2. the **handler-emitted `TraceEntry`** adapter — the agent/tool/turn entries
9
+ // (with the additive `turn` grouping field) → rows, covering the agentic
10
+ // web-demo case.
11
+ //
12
+ // It is deliberately React-free: the presentational component (`TraceTimeline` in
13
+ // `@nanobpm/bojtos-react`) renders this model, keeping the view layer thin.
14
+ /** Narrow a {@link TraceItem} to a {@link TraceTurnGroup}. */
15
+ export function isTraceTurnGroup(item) {
16
+ return item.rows !== undefined;
17
+ }
18
+ /**
19
+ * Fold a flat list of normalized rows into the view model: consecutive rows
20
+ * sharing a `turn` become one {@link TraceTurnGroup}; everything else stays a
21
+ * plain row in its original order. A row with no `turn` breaks the current group,
22
+ * so an interleaved non-turn line never gets swallowed into a card. This is the
23
+ * grouping both forked timelines did by hand, lifted into the shared kit.
24
+ */
25
+ export function buildTraceItems(rows) {
26
+ const items = [];
27
+ let current = null;
28
+ for (const row of rows) {
29
+ if (row.turn !== undefined) {
30
+ if (current && current.turn === row.turn) {
31
+ current.rows.push(row);
32
+ }
33
+ else {
34
+ current = { turn: row.turn, rows: [row] };
35
+ items.push(current);
36
+ }
37
+ }
38
+ else {
39
+ current = null;
40
+ items.push(row);
41
+ }
42
+ }
43
+ return items;
44
+ }
45
+ /**
46
+ * The handler-emitted `TraceEntry` adapter: stamp each entry with a stable `id`
47
+ * (its index) to lift it into a {@link TraceRow}. The entries already carry the
48
+ * additive `turn`/`elementId`/`args`/`result` fields, so {@link buildTraceItems}
49
+ * over the result reproduces the agentic turn-grouped card view without any
50
+ * re-forked grouping logic. The input is never mutated.
51
+ */
52
+ export function traceEntriesToRows(entries) {
53
+ return entries.map((entry, id) => ({ ...entry, id }));
54
+ }
55
+ /** Read a string field off a `WasmEvent`'s open payload, or `undefined`. */
56
+ function str(ev, key) {
57
+ const v = ev[key];
58
+ return typeof v === "string" ? v : undefined;
59
+ }
60
+ /** The element this event concerns, if it names one — for pairing/labelling. */
61
+ function elementOf(ev) {
62
+ return str(ev, "element_id");
63
+ }
64
+ const ENGINE_EVENT_RULES = {
65
+ ProcessInstanceCreated: {
66
+ kind: "start",
67
+ text: (ev) => `Process ${str(ev, "process_id") ?? "instance"} started`,
68
+ },
69
+ JobCreated: {
70
+ kind: "tool",
71
+ text: (ev) => `Job ${str(ev, "job_type") ?? ""} created`.trim() +
72
+ (elementOf(ev) ? ` on ${elementOf(ev)}` : ""),
73
+ },
74
+ JobCompleted: {
75
+ kind: "vars",
76
+ text: (ev) => `Job ${str(ev, "job_type") ?? ""} completed`.trim(),
77
+ },
78
+ JobFailed: {
79
+ kind: "error",
80
+ text: (ev) => `Job ${str(ev, "job_type") ?? ""} failed`.trim(),
81
+ },
82
+ JobErrorThrown: {
83
+ kind: "error",
84
+ text: (ev) => `Job threw error ${str(ev, "error_code") ?? ""}`.trim() +
85
+ (elementOf(ev) ? ` on ${elementOf(ev)}` : ""),
86
+ },
87
+ IncidentRaised: {
88
+ kind: "error",
89
+ text: (ev) => `Incident on ${elementOf(ev) ?? "instance"}` +
90
+ (str(ev, "reason") ? `: ${str(ev, "reason")}` : ""),
91
+ },
92
+ IncidentResolved: {
93
+ kind: "tool",
94
+ text: (ev) => `Incident resolved on ${elementOf(ev) ?? "instance"}`,
95
+ },
96
+ UserTaskCreated: {
97
+ kind: "human",
98
+ text: (ev) => `User task ${elementOf(ev) ?? ""} awaiting a human`.trim(),
99
+ },
100
+ UserTaskAssigned: {
101
+ kind: "human",
102
+ text: (ev) => `User task ${elementOf(ev) ?? ""} assigned`.trim() +
103
+ (str(ev, "assignee") ? ` to ${str(ev, "assignee")}` : ""),
104
+ },
105
+ UserTaskCompleted: {
106
+ kind: "human",
107
+ text: (ev) => `User task ${elementOf(ev) ?? ""} completed`.trim(),
108
+ },
109
+ UserTaskCanceled: {
110
+ kind: "human",
111
+ text: (ev) => `User task ${elementOf(ev) ?? ""} canceled`.trim(),
112
+ },
113
+ TimerCreated: {
114
+ kind: "tool",
115
+ text: (ev) => `Timer set on ${elementOf(ev) ?? "instance"}`,
116
+ },
117
+ TimerTriggered: {
118
+ kind: "tool",
119
+ text: (ev) => `Timer fired on ${elementOf(ev) ?? "instance"}`,
120
+ },
121
+ MessagePublished: {
122
+ kind: "tool",
123
+ text: (ev) => `Message ${str(ev, "message_name") ?? ""} published`.trim(),
124
+ },
125
+ MessageCorrelated: {
126
+ kind: "tool",
127
+ text: (ev) => `Message ${str(ev, "message_name") ?? ""} correlated`.trim(),
128
+ },
129
+ SignalBroadcast: {
130
+ kind: "tool",
131
+ text: (ev) => `Signal ${str(ev, "signal_name") ?? ""} broadcast`.trim(),
132
+ },
133
+ SignalCorrelated: {
134
+ kind: "tool",
135
+ text: (ev) => `Signal ${str(ev, "signal_name") ?? ""} correlated`.trim(),
136
+ },
137
+ AdHocActivated: {
138
+ kind: "agent",
139
+ text: (ev) => `Ad-hoc sub-process ${elementOf(ev) ?? ""} activated`.trim(),
140
+ },
141
+ AdHocToolActivated: {
142
+ kind: "agent",
143
+ text: (ev) => `Tool ${elementOf(ev) ?? ""} activated`.trim(),
144
+ },
145
+ AdHocToolCompleted: {
146
+ kind: "vars",
147
+ text: (ev) => `Tool ${elementOf(ev) ?? ""} returned`.trim(),
148
+ },
149
+ AdHocCompleted: {
150
+ kind: "agent",
151
+ text: (ev) => `Ad-hoc sub-process ${elementOf(ev) ?? ""} completed`.trim(),
152
+ },
153
+ ProcessInstanceCompleted: {
154
+ kind: "done",
155
+ text: () => "Process completed",
156
+ },
157
+ ProcessInstanceTerminated: {
158
+ kind: "error",
159
+ text: () => "Process terminated",
160
+ },
161
+ };
162
+ /**
163
+ * The engine-event fold adapter: map a `WasmEvent[]` (the flattened
164
+ * `{ seq, now, type, …snake_case }` stream from `useBojtos().events`) into
165
+ * normalized rows, keeping only the meaningful milestones (see
166
+ * {@link ENGINE_EVENT_RULES}). Each row's `id` is the event's `seq`, so ids stay
167
+ * stable and monotonic across re-reads of a growing log. Engine events carry no
168
+ * turn, so {@link buildTraceItems} over the result is a flat list of rows — the
169
+ * non-agentic test-view shape.
170
+ */
171
+ export function foldEngineEvents(events) {
172
+ const rows = [];
173
+ for (const ev of events) {
174
+ const rule = ENGINE_EVENT_RULES[ev.type];
175
+ if (!rule)
176
+ continue;
177
+ rows.push({
178
+ id: ev.seq,
179
+ kind: rule.kind,
180
+ text: rule.text(ev),
181
+ elementId: elementOf(ev),
182
+ });
183
+ }
184
+ return rows;
185
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/bojtos-kit",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Framework-agnostic core of the Bojtos in-browser BPMN demo framework (ADR 0043): a single scenario runner over the @nanobpm/engine-wasm engine (deploy, start instances, complete/fail jobs, advance the clock, read snapshots and the event log), plus the engine's snapshot/event contract types. Consumed by @nanobpm/bojtos-react and the console test-run panel.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/src/index.ts CHANGED
@@ -22,6 +22,23 @@ export {
22
22
  type RoundResult,
23
23
  type SettleReason,
24
24
  } from "./worker.js";
25
+ // The shared trace model + both adapters (engine-event fold and handler-emitted
26
+ // `TraceEntry`) that retired the two forked `TraceTimeline` copies (#9). Pure and
27
+ // React-free — the presentational component lives in @nanobpm/bojtos-react.
28
+ export {
29
+ buildTraceItems,
30
+ isTraceTurnGroup,
31
+ foldEngineEvents,
32
+ traceEntriesToRows,
33
+ } from "./trace.js";
34
+ export type {
35
+ TraceRowKind,
36
+ TraceEntry,
37
+ TraceRow,
38
+ TraceTurnGroup,
39
+ TraceItem,
40
+ TraceAdapter,
41
+ } from "./trace.js";
25
42
  // Every type reachable from `Snapshot` is exported: a consumer that can read
26
43
  // `snapshot.userTasks` must also be able to name `UserTaskDto` to write a
27
44
  // helper for it. Keep this list exhaustive when adding to `types.ts`.
package/src/trace.ts ADDED
@@ -0,0 +1,291 @@
1
+ // The framework-agnostic trace model shared by the Bojtos demo framework and the
2
+ // console test-view — the single source that retired the two drifted, forked
3
+ // `TraceTimeline` copies (nanobpm/bojtos#9). It defines one normalized row/turn
4
+ // model plus the two adapters that map a source into it:
5
+ //
6
+ // 1. the **engine-event fold** — `WasmEvent[]` (from `useBojtos().events`) →
7
+ // rows, covering the non-agentic / test-view case; and
8
+ // 2. the **handler-emitted `TraceEntry`** adapter — the agent/tool/turn entries
9
+ // (with the additive `turn` grouping field) → rows, covering the agentic
10
+ // web-demo case.
11
+ //
12
+ // It is deliberately React-free: the presentational component (`TraceTimeline` in
13
+ // `@nanobpm/bojtos-react`) renders this model, keeping the view layer thin.
14
+
15
+ import type { WasmEvent } from "./types.js";
16
+
17
+ /**
18
+ * Semantic classification of a trace row, driving how the view styles it (the
19
+ * `log-<kind>` class the two forked timelines already keyed off) and which
20
+ * affordance it carries. Framework-agnostic — an engine-event fold and an
21
+ * agentic handler stream both land in this shared vocabulary:
22
+ *
23
+ * - `start` — the run/instance began.
24
+ * - `agent` — an agent decision, or a tool the agent activated this turn.
25
+ * - `llm` — a raw model reply.
26
+ * - `tool` — a tool/handler log line (a job running, a timer, a message).
27
+ * - `human` — a user task awaiting or completed by a person.
28
+ * - `done` — the final outcome (the instance completed/terminated).
29
+ * - `error` — a failure, incident, or thrown error.
30
+ * - `vars` — a variables/result update (e.g. what a tool returned).
31
+ */
32
+ export type TraceRowKind =
33
+ | "start"
34
+ | "agent"
35
+ | "llm"
36
+ | "tool"
37
+ | "human"
38
+ | "done"
39
+ | "error"
40
+ | "vars";
41
+
42
+ /**
43
+ * A source line before it is placed in the normalized model. This is the shape a
44
+ * handler emits (the web-demo framework's `TraceEntry`): `kind`/`text` are all a
45
+ * plain consumer needs; every other field is additive and safe to ignore.
46
+ */
47
+ export interface TraceEntry {
48
+ kind: TraceRowKind;
49
+ /** The human-readable line. */
50
+ text: string;
51
+ /**
52
+ * Stable id for an entry that updates in place — a streaming completion grows
53
+ * one line rather than spamming forty.
54
+ */
55
+ key?: string;
56
+ /** True while the entry is still being produced (renders a spinner). */
57
+ pending?: boolean;
58
+ /**
59
+ * Groups every entry produced by one agent turn together (the streamed LLM
60
+ * reply, each tool it activated, and that tool's result). Consecutive entries
61
+ * sharing a `turn` fold into one turn card; entries with no `turn` render as
62
+ * plain rows in their original order.
63
+ */
64
+ turn?: number;
65
+ /** The BPMN element (tool or task) this entry concerns. */
66
+ elementId?: string;
67
+ /** Arguments supplied when activating a tool — the coerced values, not the raw reply. */
68
+ args?: Record<string, unknown>;
69
+ /** What a tool/handler returned, paired with its activation by `elementId`. */
70
+ result?: unknown;
71
+ }
72
+
73
+ /**
74
+ * A normalized row: a {@link TraceEntry} stamped with a stable, monotonic `id`.
75
+ * The `id` is what the view keys off and what pairs a tool's result with its
76
+ * activation and orders loose lines within a turn.
77
+ */
78
+ export interface TraceRow extends TraceEntry {
79
+ id: number;
80
+ }
81
+
82
+ /** Consecutive same-turn rows, folded into one group by {@link buildTraceItems}. */
83
+ export interface TraceTurnGroup {
84
+ turn: number;
85
+ rows: TraceRow[];
86
+ }
87
+
88
+ /** A top-level item the view renders: either a plain row or a turn group. */
89
+ export type TraceItem = TraceRow | TraceTurnGroup;
90
+
91
+ /** Narrow a {@link TraceItem} to a {@link TraceTurnGroup}. */
92
+ export function isTraceTurnGroup(item: TraceItem): item is TraceTurnGroup {
93
+ return (item as TraceTurnGroup).rows !== undefined;
94
+ }
95
+
96
+ /**
97
+ * An adapter maps a source (`WasmEvent[]`, a `TraceEntry[]`, …) into the shared
98
+ * normalized row model. Both built-in adapters — {@link foldEngineEvents} and
99
+ * {@link traceEntriesToRows} — satisfy this; a consumer can supply its own for a
100
+ * bespoke source.
101
+ */
102
+ export type TraceAdapter<TSource> = (source: TSource) => TraceRow[];
103
+
104
+ /**
105
+ * Fold a flat list of normalized rows into the view model: consecutive rows
106
+ * sharing a `turn` become one {@link TraceTurnGroup}; everything else stays a
107
+ * plain row in its original order. A row with no `turn` breaks the current group,
108
+ * so an interleaved non-turn line never gets swallowed into a card. This is the
109
+ * grouping both forked timelines did by hand, lifted into the shared kit.
110
+ */
111
+ export function buildTraceItems(rows: TraceRow[]): TraceItem[] {
112
+ const items: TraceItem[] = [];
113
+ let current: TraceTurnGroup | null = null;
114
+ for (const row of rows) {
115
+ if (row.turn !== undefined) {
116
+ if (current && current.turn === row.turn) {
117
+ current.rows.push(row);
118
+ } else {
119
+ current = { turn: row.turn, rows: [row] };
120
+ items.push(current);
121
+ }
122
+ } else {
123
+ current = null;
124
+ items.push(row);
125
+ }
126
+ }
127
+ return items;
128
+ }
129
+
130
+ /**
131
+ * The handler-emitted `TraceEntry` adapter: stamp each entry with a stable `id`
132
+ * (its index) to lift it into a {@link TraceRow}. The entries already carry the
133
+ * additive `turn`/`elementId`/`args`/`result` fields, so {@link buildTraceItems}
134
+ * over the result reproduces the agentic turn-grouped card view without any
135
+ * re-forked grouping logic. The input is never mutated.
136
+ */
137
+ export function traceEntriesToRows(entries: readonly TraceEntry[]): TraceRow[] {
138
+ return entries.map((entry, id) => ({ ...entry, id }));
139
+ }
140
+
141
+ /**
142
+ * How one engine event type folds into the trace: its row {@link TraceRowKind}
143
+ * and a function turning the event's snake_case payload into a line. Returning
144
+ * a mapping opts the event into the story; every type absent from the table
145
+ * below is deliberately dropped as low-signal lifecycle noise (`ElementActivating`
146
+ * / `ElementCompleting`, `JobActivated`, `SequenceFlowTaken`, the scoped-variable
147
+ * and parallel-join bookkeeping), so the fold reads as a run's milestones rather
148
+ * than a raw trace.
149
+ */
150
+ interface EngineEventRule {
151
+ kind: TraceRowKind;
152
+ text: (ev: WasmEvent) => string;
153
+ }
154
+
155
+ /** Read a string field off a `WasmEvent`'s open payload, or `undefined`. */
156
+ function str(ev: WasmEvent, key: string): string | undefined {
157
+ const v = ev[key];
158
+ return typeof v === "string" ? v : undefined;
159
+ }
160
+
161
+ /** The element this event concerns, if it names one — for pairing/labelling. */
162
+ function elementOf(ev: WasmEvent): string | undefined {
163
+ return str(ev, "element_id");
164
+ }
165
+
166
+ const ENGINE_EVENT_RULES: Record<string, EngineEventRule> = {
167
+ ProcessInstanceCreated: {
168
+ kind: "start",
169
+ text: (ev) => `Process ${str(ev, "process_id") ?? "instance"} started`,
170
+ },
171
+ JobCreated: {
172
+ kind: "tool",
173
+ text: (ev) =>
174
+ `Job ${str(ev, "job_type") ?? ""} created`.trim() +
175
+ (elementOf(ev) ? ` on ${elementOf(ev)}` : ""),
176
+ },
177
+ JobCompleted: {
178
+ kind: "vars",
179
+ text: (ev) => `Job ${str(ev, "job_type") ?? ""} completed`.trim(),
180
+ },
181
+ JobFailed: {
182
+ kind: "error",
183
+ text: (ev) => `Job ${str(ev, "job_type") ?? ""} failed`.trim(),
184
+ },
185
+ JobErrorThrown: {
186
+ kind: "error",
187
+ text: (ev) =>
188
+ `Job threw error ${str(ev, "error_code") ?? ""}`.trim() +
189
+ (elementOf(ev) ? ` on ${elementOf(ev)}` : ""),
190
+ },
191
+ IncidentRaised: {
192
+ kind: "error",
193
+ text: (ev) =>
194
+ `Incident on ${elementOf(ev) ?? "instance"}` +
195
+ (str(ev, "reason") ? `: ${str(ev, "reason")}` : ""),
196
+ },
197
+ IncidentResolved: {
198
+ kind: "tool",
199
+ text: (ev) => `Incident resolved on ${elementOf(ev) ?? "instance"}`,
200
+ },
201
+ UserTaskCreated: {
202
+ kind: "human",
203
+ text: (ev) => `User task ${elementOf(ev) ?? ""} awaiting a human`.trim(),
204
+ },
205
+ UserTaskAssigned: {
206
+ kind: "human",
207
+ text: (ev) =>
208
+ `User task ${elementOf(ev) ?? ""} assigned`.trim() +
209
+ (str(ev, "assignee") ? ` to ${str(ev, "assignee")}` : ""),
210
+ },
211
+ UserTaskCompleted: {
212
+ kind: "human",
213
+ text: (ev) => `User task ${elementOf(ev) ?? ""} completed`.trim(),
214
+ },
215
+ UserTaskCanceled: {
216
+ kind: "human",
217
+ text: (ev) => `User task ${elementOf(ev) ?? ""} canceled`.trim(),
218
+ },
219
+ TimerCreated: {
220
+ kind: "tool",
221
+ text: (ev) => `Timer set on ${elementOf(ev) ?? "instance"}`,
222
+ },
223
+ TimerTriggered: {
224
+ kind: "tool",
225
+ text: (ev) => `Timer fired on ${elementOf(ev) ?? "instance"}`,
226
+ },
227
+ MessagePublished: {
228
+ kind: "tool",
229
+ text: (ev) => `Message ${str(ev, "message_name") ?? ""} published`.trim(),
230
+ },
231
+ MessageCorrelated: {
232
+ kind: "tool",
233
+ text: (ev) => `Message ${str(ev, "message_name") ?? ""} correlated`.trim(),
234
+ },
235
+ SignalBroadcast: {
236
+ kind: "tool",
237
+ text: (ev) => `Signal ${str(ev, "signal_name") ?? ""} broadcast`.trim(),
238
+ },
239
+ SignalCorrelated: {
240
+ kind: "tool",
241
+ text: (ev) => `Signal ${str(ev, "signal_name") ?? ""} correlated`.trim(),
242
+ },
243
+ AdHocActivated: {
244
+ kind: "agent",
245
+ text: (ev) => `Ad-hoc sub-process ${elementOf(ev) ?? ""} activated`.trim(),
246
+ },
247
+ AdHocToolActivated: {
248
+ kind: "agent",
249
+ text: (ev) => `Tool ${elementOf(ev) ?? ""} activated`.trim(),
250
+ },
251
+ AdHocToolCompleted: {
252
+ kind: "vars",
253
+ text: (ev) => `Tool ${elementOf(ev) ?? ""} returned`.trim(),
254
+ },
255
+ AdHocCompleted: {
256
+ kind: "agent",
257
+ text: (ev) => `Ad-hoc sub-process ${elementOf(ev) ?? ""} completed`.trim(),
258
+ },
259
+ ProcessInstanceCompleted: {
260
+ kind: "done",
261
+ text: () => "Process completed",
262
+ },
263
+ ProcessInstanceTerminated: {
264
+ kind: "error",
265
+ text: () => "Process terminated",
266
+ },
267
+ };
268
+
269
+ /**
270
+ * The engine-event fold adapter: map a `WasmEvent[]` (the flattened
271
+ * `{ seq, now, type, …snake_case }` stream from `useBojtos().events`) into
272
+ * normalized rows, keeping only the meaningful milestones (see
273
+ * {@link ENGINE_EVENT_RULES}). Each row's `id` is the event's `seq`, so ids stay
274
+ * stable and monotonic across re-reads of a growing log. Engine events carry no
275
+ * turn, so {@link buildTraceItems} over the result is a flat list of rows — the
276
+ * non-agentic test-view shape.
277
+ */
278
+ export function foldEngineEvents(events: readonly WasmEvent[]): TraceRow[] {
279
+ const rows: TraceRow[] = [];
280
+ for (const ev of events) {
281
+ const rule = ENGINE_EVENT_RULES[ev.type];
282
+ if (!rule) continue;
283
+ rows.push({
284
+ id: ev.seq,
285
+ kind: rule.kind,
286
+ text: rule.text(ev),
287
+ elementId: elementOf(ev),
288
+ });
289
+ }
290
+ return rows;
291
+ }