@nanobpm/bojtos-react 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 +29 -0
- package/dist/TraceTimeline.d.ts +50 -0
- package/dist/TraceTimeline.js +81 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -1
- package/package.json +2 -2
- package/src/TraceTimeline.tsx +303 -0
- package/src/index.ts +13 -0
package/README.md
CHANGED
|
@@ -10,6 +10,15 @@ React binding for the **Bojtos** in-browser BPMN demo framework
|
|
|
10
10
|
- **`<BpmnRuntimeView xml activeIds incidentIds />`** — the live diagram: it
|
|
11
11
|
imports the XML once and updates token (`nano-active`) / incident
|
|
12
12
|
(`nano-incident`) markers in place, so zoom/scroll survive stepping.
|
|
13
|
+
- **`<TraceTimeline rows />`** — the shared activity log: it renders the
|
|
14
|
+
framework-agnostic trace model from [`@nanobpm/bojtos-kit`](../bojtos-kit) as a
|
|
15
|
+
turn-by-turn story (consecutive same-`turn` rows fold into one card; rows with
|
|
16
|
+
no `turn` render as plain lines). Feed it a kit adapter —
|
|
17
|
+
`foldEngineEvents(run.events)` for a plain engine run, or
|
|
18
|
+
`traceEntriesToRows(entries)` for handler-emitted agent/tool/turn entries. It
|
|
19
|
+
imports **only** the kit and React, so a trace-only import tree-shakes `bpmn-js`
|
|
20
|
+
out (the package is `sideEffects: false`); a test walks the built module graph
|
|
21
|
+
to pin that.
|
|
13
22
|
|
|
14
23
|
## Install
|
|
15
24
|
|
|
@@ -42,6 +51,26 @@ function Demo({ bpmn }: { bpmn: string }) {
|
|
|
42
51
|
}
|
|
43
52
|
```
|
|
44
53
|
|
|
54
|
+
## Trace timeline
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { useBojtos, TraceTimeline, foldEngineEvents } from "@nanobpm/bojtos-react";
|
|
58
|
+
|
|
59
|
+
function RunLog({ bpmn }: { bpmn: string }) {
|
|
60
|
+
const run = useBojtos({ bpmn });
|
|
61
|
+
// Engine-event fold — the non-agentic / test-view case.
|
|
62
|
+
return <TraceTimeline rows={foldEngineEvents(run.events)} />;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For an agentic run, emit `TraceEntry` lines from your handlers (with the additive
|
|
67
|
+
`turn` / `elementId` / `args` / `result` fields) and pass
|
|
68
|
+
`traceEntriesToRows(entries)` instead — same component, turn-grouped card view.
|
|
69
|
+
`TraceTimeline` keeps the class names (`timeline`, `timeline-turn`,
|
|
70
|
+
`log-line log-<kind>`, …) the demo stylesheet already targets, so your CSS applies
|
|
71
|
+
unchanged. It never imports `bpmn-js`, so importing it alone won't pull the
|
|
72
|
+
diagram bundle in.
|
|
73
|
+
|
|
45
74
|
## Peer requirements
|
|
46
75
|
|
|
47
76
|
`react` and `bpmn-js` are peer dependencies (the consumer already has them). The
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type ElementStatDto, type IncidentDto, type TraceRow } from "@nanobpm/bojtos-kit";
|
|
2
|
+
/**
|
|
3
|
+
* The shared activity log — the run told as a story rather than a flat stack of
|
|
4
|
+
* lines. It is the single component that retired the two drifted, forked
|
|
5
|
+
* `TraceTimeline` copies (nanobpm/bojtos#9): the web-demo framework's agent/tool/
|
|
6
|
+
* turn view and the console test-view's engine-event fold.
|
|
7
|
+
*
|
|
8
|
+
* It renders the framework-agnostic {@link TraceRow} model from
|
|
9
|
+
* `@nanobpm/bojtos-kit` — feed it whichever adapter matches your source:
|
|
10
|
+
*
|
|
11
|
+
* - `foldEngineEvents(useBojtos().events)` for the non-agentic / test-view case, or
|
|
12
|
+
* - `traceEntriesToRows(entries)` for handler-emitted agent/tool/turn entries.
|
|
13
|
+
*
|
|
14
|
+
* Consecutive rows sharing a `turn` fold into one card (the model's raw LLM reply,
|
|
15
|
+
* each tool it activated with its arguments, and — once it lands — what that tool
|
|
16
|
+
* returned); rows with no `turn` render as plain lines in order, so a non-agentic
|
|
17
|
+
* run looks exactly like the flat log it replaces.
|
|
18
|
+
*
|
|
19
|
+
* This module deliberately imports **only** the kit and React — never
|
|
20
|
+
* `./BpmnRuntimeView` or `bpmn-js` — so a trace-only import tree-shakes the
|
|
21
|
+
* diagram renderer out (the package is `sideEffects: false`). See
|
|
22
|
+
* `test/trace.timeline.test.ts`, which walks the built module graph to pin that.
|
|
23
|
+
*
|
|
24
|
+
* The markup keeps the class names the forked copies' CSS already targets
|
|
25
|
+
* (`timeline`, `timeline-turn`, `timeline-tool`, `log-line log-<kind>`, …) so a
|
|
26
|
+
* consumer's existing stylesheet applies unchanged; no design-system dependency
|
|
27
|
+
* is pulled in.
|
|
28
|
+
*/
|
|
29
|
+
export interface TraceTimelineProps {
|
|
30
|
+
/**
|
|
31
|
+
* The normalized rows to render. Produce them with a kit adapter
|
|
32
|
+
* (`foldEngineEvents` / `traceEntriesToRows`) or your own {@link TraceRow[]}.
|
|
33
|
+
*/
|
|
34
|
+
rows: TraceRow[];
|
|
35
|
+
/** `snapshot.elementStats` — per-element completion/active counts, engine-side. */
|
|
36
|
+
elementStats?: ElementStatDto[];
|
|
37
|
+
/** Incidents on the current snapshot, with their reason. */
|
|
38
|
+
incidents?: IncidentDto[];
|
|
39
|
+
/** BPMN element id → human label, for both the timeline and the panels below. */
|
|
40
|
+
labelFor?: (elementId: string) => string;
|
|
41
|
+
/** Card heading. Defaults to "Activity". */
|
|
42
|
+
title?: string;
|
|
43
|
+
/** Sub-heading under the title. */
|
|
44
|
+
description?: string;
|
|
45
|
+
/** Shown when there are no rows yet. Defaults to "Press Run to start.". */
|
|
46
|
+
emptyText?: string;
|
|
47
|
+
/** Optional class for the outer container. */
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
export declare function TraceTimeline({ rows, elementStats, incidents, labelFor, title, description, emptyText, className, }: TraceTimelineProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { buildTraceItems, isTraceTurnGroup, } from "@nanobpm/bojtos-kit";
|
|
4
|
+
function safeStringify(value, space) {
|
|
5
|
+
// `JSON.stringify(undefined)` returns the JS value `undefined` (not a string),
|
|
6
|
+
// which would render as nothing. Emit the literal "undefined" so a handler/tool
|
|
7
|
+
// that actually returned `undefined` is shown explicitly rather than vanishing.
|
|
8
|
+
if (value === undefined)
|
|
9
|
+
return "undefined";
|
|
10
|
+
try {
|
|
11
|
+
// `JSON.stringify` throws on BigInt and circular structures; a replacer
|
|
12
|
+
// renders BigInt losslessly as its decimal string so trace payloads that
|
|
13
|
+
// carry engine-native BigInts don't crash serialization.
|
|
14
|
+
return JSON.stringify(value, (_key, val) => (typeof val === "bigint" ? val.toString() : val), space);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return "[unserializable value]";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function ToolStep({ activation, result, labelFor, }) {
|
|
21
|
+
const elementId = activation.elementId ?? "";
|
|
22
|
+
return (_jsxs("div", { className: "timeline-tool", children: [_jsxs("div", { className: "timeline-tool-head", children: [_jsx("span", { className: "timeline-badge timeline-badge-info", children: "tool" }), _jsx("strong", { children: labelFor(elementId) || elementId }), _jsx("code", { children: elementId })] }), activation.args !== undefined &&
|
|
23
|
+
Object.keys(activation.args).length > 0 && (_jsxs("div", { className: "timeline-kv", children: [_jsx("span", { className: "timeline-kv-label", children: "arguments" }), _jsx("code", { children: safeStringify(activation.args) })] })), _jsxs("div", { className: "timeline-kv", children: [_jsx("span", { className: "timeline-kv-label", children: "returned" }), _jsx("code", { children: result
|
|
24
|
+
? safeStringify(result.result)
|
|
25
|
+
: "— waiting for the job to complete —" })] })] }));
|
|
26
|
+
}
|
|
27
|
+
function TurnCard({ group, labelFor, }) {
|
|
28
|
+
const reply = group.rows.find((e) => e.kind === "llm");
|
|
29
|
+
const activations = group.rows.filter((e) => e.kind === "agent" && e.elementId);
|
|
30
|
+
const results = group.rows.filter((e) => e.kind === "vars" && e.elementId);
|
|
31
|
+
const decisions = group.rows.filter((e) => e.kind === "agent" && !e.elementId);
|
|
32
|
+
const errors = group.rows.filter((e) => e.kind === "error");
|
|
33
|
+
// Entries a handler's own trace call emits (kind "tool") and any "vars" result
|
|
34
|
+
// that never paired with an activation above would otherwise vanish once
|
|
35
|
+
// stamped with a turn — render them as plain lines within the card, in order.
|
|
36
|
+
const activatedElementIds = new Set(activations.map((a) => a.elementId));
|
|
37
|
+
const loose = group.rows
|
|
38
|
+
.filter((e) => e.kind === "tool" ||
|
|
39
|
+
(e.kind === "vars" &&
|
|
40
|
+
e.elementId &&
|
|
41
|
+
!activatedElementIds.has(e.elementId)))
|
|
42
|
+
.sort((a, b) => a.id - b.id);
|
|
43
|
+
return (_jsxs("div", { className: "timeline-turn", children: [_jsxs("div", { className: "timeline-turn-head", children: [_jsxs("span", { className: `timeline-badge ${reply?.pending ? "timeline-badge-warning" : "timeline-badge-neutral"}`, children: ["Turn ", group.turn] }), reply?.pending && _jsx("span", { className: "timeline-pending", children: "thinking\u2026" })] }), reply && _jsx("blockquote", { className: "timeline-reply", children: reply.text }), decisions.map((d) => (_jsx("div", { className: "timeline-note", children: d.text }, d.key ?? d.id))), activations.map((a) => (_jsx(ToolStep, { activation: a, result: results.find((r) => r.elementId === a.elementId), labelFor: labelFor }, a.key ?? a.id))), loose.map((e) => (_jsxs("div", { className: `log-line log-${e.kind}`, children: [e.pending ? "⏳ " : "", e.text] }, e.key ?? e.id))), errors.map((e) => (_jsxs("div", { className: "timeline-error", children: ["\u26A0 ", e.text] }, e.id)))] }));
|
|
44
|
+
}
|
|
45
|
+
export function TraceTimeline({ rows, elementStats = [], incidents = [], labelFor = (id) => id, title = "Activity", description = "Agent turns, model replies, and tool calls — read top to bottom as a story.", emptyText = "Press Run to start.", className, }) {
|
|
46
|
+
const items = useMemo(() => buildTraceItems(rows), [rows]);
|
|
47
|
+
const [copied, setCopied] = useState(false);
|
|
48
|
+
const scrollRef = useRef(null);
|
|
49
|
+
// Keep the newest step in view as the run grows, same as the flat logs this
|
|
50
|
+
// replaces.
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const el = scrollRef.current;
|
|
53
|
+
if (el)
|
|
54
|
+
el.scrollTop = el.scrollHeight;
|
|
55
|
+
}, [items]);
|
|
56
|
+
const copyJson = () => {
|
|
57
|
+
const payload = {
|
|
58
|
+
log: rows.map(({ id: _id, ...rest }) => rest),
|
|
59
|
+
elementStats,
|
|
60
|
+
incidents,
|
|
61
|
+
};
|
|
62
|
+
const text = safeStringify(payload, 2);
|
|
63
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
64
|
+
navigator.clipboard
|
|
65
|
+
.writeText(text)
|
|
66
|
+
.then(() => {
|
|
67
|
+
setCopied(true);
|
|
68
|
+
setTimeout(() => setCopied(false), 1500);
|
|
69
|
+
})
|
|
70
|
+
.catch(() => {
|
|
71
|
+
// Clipboard access can be denied (permissions policy, insecure
|
|
72
|
+
// context, an embed iframe without a clipboard-write allowance) —
|
|
73
|
+
// fail quietly rather than surfacing an error for a convenience
|
|
74
|
+
// action, not a run-blocking one.
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
return (_jsxs("div", { className: className ? `timeline-panel ${className}` : "timeline-panel", children: [_jsxs("div", { className: "timeline-header", children: [_jsx("div", { className: "timeline-title", children: title }), description && (_jsx("div", { className: "timeline-description", children: description }))] }), _jsx("div", { className: "timeline-toolbar", children: _jsx("button", { type: "button", onClick: copyJson, children: copied ? "Copied!" : "Copy run as JSON" }) }), _jsx("div", { className: "timeline", ref: scrollRef, children: items.length === 0 ? (_jsx("div", { className: "log-empty", children: emptyText })) : (items.map((item) => isTraceTurnGroup(item) ? (_jsx(TurnCard, { group: item, labelFor: labelFor }, `turn-${item.turn}-${item.rows[0].key ?? item.rows[0].id}`)) : (_jsxs("div", { className: `log-line log-${item.kind}`, children: [item.pending ? "⏳ " : "", item.text] }, item.key ?? item.id)))) }), (elementStats.length > 0 || incidents.length > 0) && (_jsxs("div", { className: "timeline-engine-view", children: [elementStats.length > 0 && (_jsxs("div", { className: "timeline-stats", children: [_jsx("span", { className: "timeline-kv-label", children: "Element completion" }), _jsx("ul", { children: elementStats
|
|
79
|
+
.filter((s) => s.completed > 0 || (s.active ?? 0) > 0)
|
|
80
|
+
.map((s) => (_jsxs("li", { children: [_jsx("code", { children: labelFor(s.elementId) || s.elementId }), " ", "completed ", s.completed, s.active ? `, ${s.active} active` : ""] }, s.elementId))) })] })), incidents.length > 0 && (_jsxs("div", { className: "timeline-incidents", children: [_jsx("span", { className: "timeline-kv-label", children: "Incidents" }), _jsx("ul", { children: incidents.map((inc, i) => (_jsxs("li", { children: [_jsx("code", { children: labelFor(inc.elementId) || inc.elementId }), " \u2014", " ", inc.reason] }, `${inc.elementId}-${i}`))) })] }))] }))] }));
|
|
81
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { useBojtos, type UseBojtosOptions, type BojtosControls, type BojtosPhase
|
|
|
2
2
|
export { Bojtos, type BojtosProps, type TraceEvent } from "./Bojtos.js";
|
|
3
3
|
export { OrderFulfillmentDemo, ORDER_FULFILLMENT_BPMN, orderFulfillmentWorkers, } from "./examples/orderFulfillment.js";
|
|
4
4
|
export { BpmnRuntimeView, type BpmnRuntimeViewProps, } from "./BpmnRuntimeView.js";
|
|
5
|
+
export { TraceTimeline, type TraceTimelineProps } from "./TraceTimeline.js";
|
|
5
6
|
export { describeRunState, markerKey, bpmnKey, resourceList, capEvents, } from "./runState.js";
|
|
6
|
-
export { JobFailure, settleReason, unhandledJobTypes, type JobHandler, type JobResult, type AgentHandler, type DispatchOptions, type DispatchResult, type RoundResult, type SettleReason, } from "@nanobpm/bojtos-kit";
|
|
7
|
-
export type { BojtosSession, Snapshot, InstanceDto, JobDto, ActivatedJob, IncidentDto, TimerDto, UserTaskDto, MessageSubscriptionDto, SignalSubscriptionDto, ElementStatDto, SequenceFlowDto, DecisionInstanceDto, ActiveEl, ActivateInstruction, AgentActivation, AgentResult, WasmEvent, } from "@nanobpm/bojtos-kit";
|
|
7
|
+
export { JobFailure, settleReason, unhandledJobTypes, buildTraceItems, isTraceTurnGroup, foldEngineEvents, traceEntriesToRows, type JobHandler, type JobResult, type AgentHandler, type DispatchOptions, type DispatchResult, type RoundResult, type SettleReason, } from "@nanobpm/bojtos-kit";
|
|
8
|
+
export type { BojtosSession, Snapshot, InstanceDto, JobDto, ActivatedJob, IncidentDto, TimerDto, UserTaskDto, MessageSubscriptionDto, SignalSubscriptionDto, ElementStatDto, SequenceFlowDto, DecisionInstanceDto, ActiveEl, ActivateInstruction, AgentActivation, AgentResult, WasmEvent, TraceRowKind, TraceEntry, TraceRow, TraceTurnGroup, TraceItem, TraceAdapter, } from "@nanobpm/bojtos-kit";
|
package/dist/index.js
CHANGED
|
@@ -7,5 +7,8 @@ export { useBojtos, } from "./useBojtos.js";
|
|
|
7
7
|
export { Bojtos } from "./Bojtos.js";
|
|
8
8
|
export { OrderFulfillmentDemo, ORDER_FULFILLMENT_BPMN, orderFulfillmentWorkers, } from "./examples/orderFulfillment.js";
|
|
9
9
|
export { BpmnRuntimeView, } from "./BpmnRuntimeView.js";
|
|
10
|
+
// The shared activity log (#9). Trace-only imports tree-shake bpmn-js out —
|
|
11
|
+
// TraceTimeline imports only the kit + React, never BpmnRuntimeView.
|
|
12
|
+
export { TraceTimeline } from "./TraceTimeline.js";
|
|
10
13
|
export { describeRunState, markerKey, bpmnKey, resourceList, capEvents, } from "./runState.js";
|
|
11
|
-
export { JobFailure, settleReason, unhandledJobTypes, } from "@nanobpm/bojtos-kit";
|
|
14
|
+
export { JobFailure, settleReason, unhandledJobTypes, buildTraceItems, isTraceTurnGroup, foldEngineEvents, traceEntriesToRows, } from "@nanobpm/bojtos-kit";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/bojtos-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "React binding for the Bojtos in-browser BPMN demo framework (ADR 0043): the useBojtos hook (owns the engine session + reactive snapshot/event state) and the <BpmnRuntimeView> live token/incident diagram. Built on @nanobpm/bojtos-kit; the console test-run panel is its first consumer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"test:ci": "node --experimental-strip-types --test test/*.test.ts"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@nanobpm/bojtos-kit": "^0.
|
|
35
|
+
"@nanobpm/bojtos-kit": "^0.6.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"bpmn-js": ">=17",
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
buildTraceItems,
|
|
4
|
+
isTraceTurnGroup,
|
|
5
|
+
type ElementStatDto,
|
|
6
|
+
type IncidentDto,
|
|
7
|
+
type TraceItem,
|
|
8
|
+
type TraceRow,
|
|
9
|
+
type TraceTurnGroup,
|
|
10
|
+
} from "@nanobpm/bojtos-kit";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The shared activity log — the run told as a story rather than a flat stack of
|
|
14
|
+
* lines. It is the single component that retired the two drifted, forked
|
|
15
|
+
* `TraceTimeline` copies (nanobpm/bojtos#9): the web-demo framework's agent/tool/
|
|
16
|
+
* turn view and the console test-view's engine-event fold.
|
|
17
|
+
*
|
|
18
|
+
* It renders the framework-agnostic {@link TraceRow} model from
|
|
19
|
+
* `@nanobpm/bojtos-kit` — feed it whichever adapter matches your source:
|
|
20
|
+
*
|
|
21
|
+
* - `foldEngineEvents(useBojtos().events)` for the non-agentic / test-view case, or
|
|
22
|
+
* - `traceEntriesToRows(entries)` for handler-emitted agent/tool/turn entries.
|
|
23
|
+
*
|
|
24
|
+
* Consecutive rows sharing a `turn` fold into one card (the model's raw LLM reply,
|
|
25
|
+
* each tool it activated with its arguments, and — once it lands — what that tool
|
|
26
|
+
* returned); rows with no `turn` render as plain lines in order, so a non-agentic
|
|
27
|
+
* run looks exactly like the flat log it replaces.
|
|
28
|
+
*
|
|
29
|
+
* This module deliberately imports **only** the kit and React — never
|
|
30
|
+
* `./BpmnRuntimeView` or `bpmn-js` — so a trace-only import tree-shakes the
|
|
31
|
+
* diagram renderer out (the package is `sideEffects: false`). See
|
|
32
|
+
* `test/trace.timeline.test.ts`, which walks the built module graph to pin that.
|
|
33
|
+
*
|
|
34
|
+
* The markup keeps the class names the forked copies' CSS already targets
|
|
35
|
+
* (`timeline`, `timeline-turn`, `timeline-tool`, `log-line log-<kind>`, …) so a
|
|
36
|
+
* consumer's existing stylesheet applies unchanged; no design-system dependency
|
|
37
|
+
* is pulled in.
|
|
38
|
+
*/
|
|
39
|
+
export interface TraceTimelineProps {
|
|
40
|
+
/**
|
|
41
|
+
* The normalized rows to render. Produce them with a kit adapter
|
|
42
|
+
* (`foldEngineEvents` / `traceEntriesToRows`) or your own {@link TraceRow[]}.
|
|
43
|
+
*/
|
|
44
|
+
rows: TraceRow[];
|
|
45
|
+
/** `snapshot.elementStats` — per-element completion/active counts, engine-side. */
|
|
46
|
+
elementStats?: ElementStatDto[];
|
|
47
|
+
/** Incidents on the current snapshot, with their reason. */
|
|
48
|
+
incidents?: IncidentDto[];
|
|
49
|
+
/** BPMN element id → human label, for both the timeline and the panels below. */
|
|
50
|
+
labelFor?: (elementId: string) => string;
|
|
51
|
+
/** Card heading. Defaults to "Activity". */
|
|
52
|
+
title?: string;
|
|
53
|
+
/** Sub-heading under the title. */
|
|
54
|
+
description?: string;
|
|
55
|
+
/** Shown when there are no rows yet. Defaults to "Press Run to start.". */
|
|
56
|
+
emptyText?: string;
|
|
57
|
+
/** Optional class for the outer container. */
|
|
58
|
+
className?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function safeStringify(value: unknown, space?: number): string {
|
|
62
|
+
// `JSON.stringify(undefined)` returns the JS value `undefined` (not a string),
|
|
63
|
+
// which would render as nothing. Emit the literal "undefined" so a handler/tool
|
|
64
|
+
// that actually returned `undefined` is shown explicitly rather than vanishing.
|
|
65
|
+
if (value === undefined) return "undefined";
|
|
66
|
+
try {
|
|
67
|
+
// `JSON.stringify` throws on BigInt and circular structures; a replacer
|
|
68
|
+
// renders BigInt losslessly as its decimal string so trace payloads that
|
|
69
|
+
// carry engine-native BigInts don't crash serialization.
|
|
70
|
+
return JSON.stringify(
|
|
71
|
+
value,
|
|
72
|
+
(_key, val) => (typeof val === "bigint" ? val.toString() : val),
|
|
73
|
+
space,
|
|
74
|
+
);
|
|
75
|
+
} catch {
|
|
76
|
+
return "[unserializable value]";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function ToolStep({
|
|
81
|
+
activation,
|
|
82
|
+
result,
|
|
83
|
+
labelFor,
|
|
84
|
+
}: {
|
|
85
|
+
activation: TraceRow;
|
|
86
|
+
result: TraceRow | undefined;
|
|
87
|
+
labelFor: (elementId: string) => string;
|
|
88
|
+
}) {
|
|
89
|
+
const elementId = activation.elementId ?? "";
|
|
90
|
+
return (
|
|
91
|
+
<div className="timeline-tool">
|
|
92
|
+
<div className="timeline-tool-head">
|
|
93
|
+
<span className="timeline-badge timeline-badge-info">tool</span>
|
|
94
|
+
<strong>{labelFor(elementId) || elementId}</strong>
|
|
95
|
+
<code>{elementId}</code>
|
|
96
|
+
</div>
|
|
97
|
+
{activation.args !== undefined &&
|
|
98
|
+
Object.keys(activation.args).length > 0 && (
|
|
99
|
+
<div className="timeline-kv">
|
|
100
|
+
<span className="timeline-kv-label">arguments</span>
|
|
101
|
+
<code>{safeStringify(activation.args)}</code>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
<div className="timeline-kv">
|
|
105
|
+
<span className="timeline-kv-label">returned</span>
|
|
106
|
+
<code>
|
|
107
|
+
{result
|
|
108
|
+
? safeStringify(result.result)
|
|
109
|
+
: "— waiting for the job to complete —"}
|
|
110
|
+
</code>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function TurnCard({
|
|
117
|
+
group,
|
|
118
|
+
labelFor,
|
|
119
|
+
}: {
|
|
120
|
+
group: TraceTurnGroup;
|
|
121
|
+
labelFor: (elementId: string) => string;
|
|
122
|
+
}) {
|
|
123
|
+
const reply = group.rows.find((e) => e.kind === "llm");
|
|
124
|
+
const activations = group.rows.filter((e) => e.kind === "agent" && e.elementId);
|
|
125
|
+
const results = group.rows.filter((e) => e.kind === "vars" && e.elementId);
|
|
126
|
+
const decisions = group.rows.filter((e) => e.kind === "agent" && !e.elementId);
|
|
127
|
+
const errors = group.rows.filter((e) => e.kind === "error");
|
|
128
|
+
// Entries a handler's own trace call emits (kind "tool") and any "vars" result
|
|
129
|
+
// that never paired with an activation above would otherwise vanish once
|
|
130
|
+
// stamped with a turn — render them as plain lines within the card, in order.
|
|
131
|
+
const activatedElementIds = new Set(activations.map((a) => a.elementId));
|
|
132
|
+
const loose = group.rows
|
|
133
|
+
.filter(
|
|
134
|
+
(e) =>
|
|
135
|
+
e.kind === "tool" ||
|
|
136
|
+
(e.kind === "vars" &&
|
|
137
|
+
e.elementId &&
|
|
138
|
+
!activatedElementIds.has(e.elementId)),
|
|
139
|
+
)
|
|
140
|
+
.sort((a, b) => a.id - b.id);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div className="timeline-turn">
|
|
144
|
+
<div className="timeline-turn-head">
|
|
145
|
+
<span
|
|
146
|
+
className={`timeline-badge ${
|
|
147
|
+
reply?.pending ? "timeline-badge-warning" : "timeline-badge-neutral"
|
|
148
|
+
}`}
|
|
149
|
+
>
|
|
150
|
+
Turn {group.turn}
|
|
151
|
+
</span>
|
|
152
|
+
{reply?.pending && <span className="timeline-pending">thinking…</span>}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{reply && <blockquote className="timeline-reply">{reply.text}</blockquote>}
|
|
156
|
+
|
|
157
|
+
{decisions.map((d) => (
|
|
158
|
+
<div key={d.key ?? d.id} className="timeline-note">
|
|
159
|
+
{d.text}
|
|
160
|
+
</div>
|
|
161
|
+
))}
|
|
162
|
+
|
|
163
|
+
{activations.map((a) => (
|
|
164
|
+
<ToolStep
|
|
165
|
+
key={a.key ?? a.id}
|
|
166
|
+
activation={a}
|
|
167
|
+
result={results.find((r) => r.elementId === a.elementId)}
|
|
168
|
+
labelFor={labelFor}
|
|
169
|
+
/>
|
|
170
|
+
))}
|
|
171
|
+
|
|
172
|
+
{loose.map((e) => (
|
|
173
|
+
<div key={e.key ?? e.id} className={`log-line log-${e.kind}`}>
|
|
174
|
+
{e.pending ? "⏳ " : ""}
|
|
175
|
+
{e.text}
|
|
176
|
+
</div>
|
|
177
|
+
))}
|
|
178
|
+
|
|
179
|
+
{errors.map((e) => (
|
|
180
|
+
<div key={e.id} className="timeline-error">
|
|
181
|
+
⚠ {e.text}
|
|
182
|
+
</div>
|
|
183
|
+
))}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function TraceTimeline({
|
|
189
|
+
rows,
|
|
190
|
+
elementStats = [],
|
|
191
|
+
incidents = [],
|
|
192
|
+
labelFor = (id) => id,
|
|
193
|
+
title = "Activity",
|
|
194
|
+
description = "Agent turns, model replies, and tool calls — read top to bottom as a story.",
|
|
195
|
+
emptyText = "Press Run to start.",
|
|
196
|
+
className,
|
|
197
|
+
}: TraceTimelineProps) {
|
|
198
|
+
const items: TraceItem[] = useMemo(() => buildTraceItems(rows), [rows]);
|
|
199
|
+
const [copied, setCopied] = useState(false);
|
|
200
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
201
|
+
|
|
202
|
+
// Keep the newest step in view as the run grows, same as the flat logs this
|
|
203
|
+
// replaces.
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
const el = scrollRef.current;
|
|
206
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
207
|
+
}, [items]);
|
|
208
|
+
|
|
209
|
+
const copyJson = () => {
|
|
210
|
+
const payload = {
|
|
211
|
+
log: rows.map(({ id: _id, ...rest }) => rest),
|
|
212
|
+
elementStats,
|
|
213
|
+
incidents,
|
|
214
|
+
};
|
|
215
|
+
const text = safeStringify(payload, 2);
|
|
216
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
217
|
+
navigator.clipboard
|
|
218
|
+
.writeText(text)
|
|
219
|
+
.then(() => {
|
|
220
|
+
setCopied(true);
|
|
221
|
+
setTimeout(() => setCopied(false), 1500);
|
|
222
|
+
})
|
|
223
|
+
.catch(() => {
|
|
224
|
+
// Clipboard access can be denied (permissions policy, insecure
|
|
225
|
+
// context, an embed iframe without a clipboard-write allowance) —
|
|
226
|
+
// fail quietly rather than surfacing an error for a convenience
|
|
227
|
+
// action, not a run-blocking one.
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<div className={className ? `timeline-panel ${className}` : "timeline-panel"}>
|
|
234
|
+
<div className="timeline-header">
|
|
235
|
+
<div className="timeline-title">{title}</div>
|
|
236
|
+
{description && (
|
|
237
|
+
<div className="timeline-description">{description}</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<div className="timeline-toolbar">
|
|
242
|
+
<button type="button" onClick={copyJson}>
|
|
243
|
+
{copied ? "Copied!" : "Copy run as JSON"}
|
|
244
|
+
</button>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div className="timeline" ref={scrollRef}>
|
|
248
|
+
{items.length === 0 ? (
|
|
249
|
+
<div className="log-empty">{emptyText}</div>
|
|
250
|
+
) : (
|
|
251
|
+
items.map((item) =>
|
|
252
|
+
isTraceTurnGroup(item) ? (
|
|
253
|
+
<TurnCard
|
|
254
|
+
key={`turn-${item.turn}-${item.rows[0].key ?? item.rows[0].id}`}
|
|
255
|
+
group={item}
|
|
256
|
+
labelFor={labelFor}
|
|
257
|
+
/>
|
|
258
|
+
) : (
|
|
259
|
+
<div key={item.key ?? item.id} className={`log-line log-${item.kind}`}>
|
|
260
|
+
{item.pending ? "⏳ " : ""}
|
|
261
|
+
{item.text}
|
|
262
|
+
</div>
|
|
263
|
+
),
|
|
264
|
+
)
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{(elementStats.length > 0 || incidents.length > 0) && (
|
|
269
|
+
<div className="timeline-engine-view">
|
|
270
|
+
{elementStats.length > 0 && (
|
|
271
|
+
<div className="timeline-stats">
|
|
272
|
+
<span className="timeline-kv-label">Element completion</span>
|
|
273
|
+
<ul>
|
|
274
|
+
{elementStats
|
|
275
|
+
.filter((s) => s.completed > 0 || (s.active ?? 0) > 0)
|
|
276
|
+
.map((s) => (
|
|
277
|
+
<li key={s.elementId}>
|
|
278
|
+
<code>{labelFor(s.elementId) || s.elementId}</code>{" "}
|
|
279
|
+
completed {s.completed}
|
|
280
|
+
{s.active ? `, ${s.active} active` : ""}
|
|
281
|
+
</li>
|
|
282
|
+
))}
|
|
283
|
+
</ul>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
{incidents.length > 0 && (
|
|
287
|
+
<div className="timeline-incidents">
|
|
288
|
+
<span className="timeline-kv-label">Incidents</span>
|
|
289
|
+
<ul>
|
|
290
|
+
{incidents.map((inc, i) => (
|
|
291
|
+
<li key={`${inc.elementId}-${i}`}>
|
|
292
|
+
<code>{labelFor(inc.elementId) || inc.elementId}</code> —{" "}
|
|
293
|
+
{inc.reason}
|
|
294
|
+
</li>
|
|
295
|
+
))}
|
|
296
|
+
</ul>
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,9 @@ export {
|
|
|
20
20
|
BpmnRuntimeView,
|
|
21
21
|
type BpmnRuntimeViewProps,
|
|
22
22
|
} from "./BpmnRuntimeView.js";
|
|
23
|
+
// The shared activity log (#9). Trace-only imports tree-shake bpmn-js out —
|
|
24
|
+
// TraceTimeline imports only the kit + React, never BpmnRuntimeView.
|
|
25
|
+
export { TraceTimeline, type TraceTimelineProps } from "./TraceTimeline.js";
|
|
23
26
|
export {
|
|
24
27
|
describeRunState,
|
|
25
28
|
markerKey,
|
|
@@ -31,6 +34,10 @@ export {
|
|
|
31
34
|
JobFailure,
|
|
32
35
|
settleReason,
|
|
33
36
|
unhandledJobTypes,
|
|
37
|
+
buildTraceItems,
|
|
38
|
+
isTraceTurnGroup,
|
|
39
|
+
foldEngineEvents,
|
|
40
|
+
traceEntriesToRows,
|
|
34
41
|
type JobHandler,
|
|
35
42
|
type JobResult,
|
|
36
43
|
type AgentHandler,
|
|
@@ -58,4 +65,10 @@ export type {
|
|
|
58
65
|
AgentActivation,
|
|
59
66
|
AgentResult,
|
|
60
67
|
WasmEvent,
|
|
68
|
+
TraceRowKind,
|
|
69
|
+
TraceEntry,
|
|
70
|
+
TraceRow,
|
|
71
|
+
TraceTurnGroup,
|
|
72
|
+
TraceItem,
|
|
73
|
+
TraceAdapter,
|
|
61
74
|
} from "@nanobpm/bojtos-kit";
|