@smithers-orchestrator/devtools 0.16.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.
@@ -0,0 +1,146 @@
1
+ import { buildSnapshot } from "./buildSnapshot.js";
2
+ import { collectTasks } from "./collectTasks.js";
3
+ import { DevToolsRunStore } from "./DevToolsRunStore.js";
4
+ import { findNodeById } from "./findNodeById.js";
5
+ import { printTree } from "./printTree.js";
6
+
7
+ /** @typedef {import("./DevToolsEngineEvent.ts").DevToolsEngineEvent} DevToolsEngineEvent */
8
+ /** @typedef {import("./DevToolsEventBus.ts").DevToolsEventBus} DevToolsEventBus */
9
+ /** @typedef {import("./DevToolsNode.ts").DevToolsNode} DevToolsNode */
10
+ /** @typedef {import("./DevToolsSnapshot.ts").DevToolsSnapshot} DevToolsSnapshot */
11
+ /** @typedef {import("./RunExecutionState.ts").RunExecutionState} RunExecutionState */
12
+ /** @typedef {import("./SmithersDevToolsOptions.ts").SmithersDevToolsOptions} SmithersDevToolsOptions */
13
+ /** @typedef {import("./TaskExecutionState.ts").TaskExecutionState} TaskExecutionState */
14
+
15
+ export class SmithersDevToolsCore {
16
+ /** @type {SmithersDevToolsOptions} */
17
+ options;
18
+ /** @type {DevToolsSnapshot | null} */
19
+ _lastSnapshot = null;
20
+ /** @type {DevToolsRunStore} */
21
+ _runStore;
22
+ /**
23
+ * @param {SmithersDevToolsOptions} [options]
24
+ */
25
+ constructor(options = {}) {
26
+ this.options = options;
27
+ this._runStore = new DevToolsRunStore(options);
28
+ }
29
+ /**
30
+ * @param {DevToolsNode | null} tree
31
+ * @returns {DevToolsSnapshot}
32
+ */
33
+ captureSnapshot(tree) {
34
+ const snapshot = buildSnapshot(tree);
35
+ this._lastSnapshot = snapshot;
36
+ return snapshot;
37
+ }
38
+ /**
39
+ * @param {DevToolsSnapshot} [snapshot]
40
+ * @returns {DevToolsSnapshot}
41
+ */
42
+ emitCommit(snapshot = this._lastSnapshot ?? buildSnapshot(null)) {
43
+ this.options.onCommit?.("commit", snapshot);
44
+ return snapshot;
45
+ }
46
+ /**
47
+ * @param {DevToolsNode | null} tree
48
+ * @returns {DevToolsSnapshot}
49
+ */
50
+ captureCommit(tree) {
51
+ const snapshot = this.captureSnapshot(tree);
52
+ this.emitCommit(snapshot);
53
+ return snapshot;
54
+ }
55
+ /**
56
+ * @param {DevToolsSnapshot} [snapshot]
57
+ * @returns {DevToolsSnapshot}
58
+ */
59
+ emitUnmount(snapshot = this._lastSnapshot ?? buildSnapshot(null)) {
60
+ this.options.onCommit?.("unmount", snapshot);
61
+ return snapshot;
62
+ }
63
+ /**
64
+ * @param {DevToolsEventBus} bus
65
+ * @returns {this}
66
+ */
67
+ attachEventBus(bus) {
68
+ this._runStore.attachEventBus(bus);
69
+ return this;
70
+ }
71
+ /** @returns {void} */
72
+ detachEventBuses() {
73
+ this._runStore.detachEventBuses();
74
+ }
75
+ /**
76
+ * @param {DevToolsEngineEvent} event
77
+ * @returns {void}
78
+ */
79
+ processEngineEvent(event) {
80
+ this._runStore.processEngineEvent(event);
81
+ }
82
+ /**
83
+ * @param {string} runId
84
+ * @returns {RunExecutionState | undefined}
85
+ */
86
+ getRun(runId) {
87
+ return this._runStore.getRun(runId);
88
+ }
89
+ /**
90
+ * @returns {Map<string, RunExecutionState>}
91
+ */
92
+ get runs() {
93
+ return this._runStore.runs;
94
+ }
95
+ /**
96
+ * @param {string} runId
97
+ * @param {string} nodeId
98
+ * @param {number} [iteration]
99
+ * @returns {TaskExecutionState | undefined}
100
+ */
101
+ getTaskState(runId, nodeId, iteration) {
102
+ return this._runStore.getTaskState(runId, nodeId, iteration);
103
+ }
104
+ /**
105
+ * Get the last captured snapshot.
106
+ * @returns {DevToolsSnapshot | null}
107
+ */
108
+ get snapshot() {
109
+ return this._lastSnapshot;
110
+ }
111
+ /**
112
+ * Get the current tree (shorthand).
113
+ * @returns {DevToolsNode | null}
114
+ */
115
+ get tree() {
116
+ return this._lastSnapshot?.tree ?? null;
117
+ }
118
+ /**
119
+ * Pretty-print the current tree to a string.
120
+ * @returns {string}
121
+ */
122
+ printTree() {
123
+ if (!this._lastSnapshot?.tree)
124
+ return "(no tree captured yet)";
125
+ return printTree(this._lastSnapshot.tree);
126
+ }
127
+ /**
128
+ * Find a node by task nodeId.
129
+ * @param {string} nodeId
130
+ * @returns {DevToolsNode | null}
131
+ */
132
+ findTask(nodeId) {
133
+ if (!this._lastSnapshot?.tree)
134
+ return null;
135
+ return findNodeById(this._lastSnapshot.tree, nodeId);
136
+ }
137
+ /**
138
+ * List all tasks in the current tree.
139
+ * @returns {DevToolsNode[]}
140
+ */
141
+ listTasks() {
142
+ if (!this._lastSnapshot?.tree)
143
+ return [];
144
+ return collectTasks(this._lastSnapshot.tree);
145
+ }
146
+ }
@@ -0,0 +1,11 @@
1
+ import type { DevToolsEngineEvent } from "./DevToolsEngineEvent.ts";
2
+ import type { DevToolsEventHandler } from "./DevToolsEventHandler.ts";
3
+
4
+ export type SmithersDevToolsOptions = {
5
+ /** Called on every renderer commit that touches the Smithers tree */
6
+ onCommit?: DevToolsEventHandler;
7
+ /** Called on every SmithersEvent from an attached EventBus */
8
+ onEngineEvent?: (event: DevToolsEngineEvent) => void;
9
+ /** Enable verbose console logging */
10
+ verbose?: boolean;
11
+ };
@@ -0,0 +1,17 @@
1
+ export type SmithersNodeType =
2
+ | "workflow"
3
+ | "task"
4
+ | "sequence"
5
+ | "parallel"
6
+ | "merge-queue"
7
+ | "branch"
8
+ | "loop"
9
+ | "worktree"
10
+ | "approval"
11
+ | "timer"
12
+ | "subflow"
13
+ | "wait-for-event"
14
+ | "saga"
15
+ | "try-catch"
16
+ | "fragment"
17
+ | "unknown";
@@ -0,0 +1,7 @@
1
+ import type { SnapshotSerializerWarning } from "./SnapshotSerializerWarning.ts";
2
+
3
+ export type SnapshotSerializerOptions = {
4
+ maxDepth?: number;
5
+ maxEntries?: number;
6
+ onWarning?: (warning: SnapshotSerializerWarning) => void;
7
+ };
@@ -0,0 +1,9 @@
1
+ export type SnapshotSerializerWarning = {
2
+ code:
3
+ | "CircularReference"
4
+ | "MaxDepthExceeded"
5
+ | "MaxEntriesExceeded"
6
+ | "UnsupportedType";
7
+ path: string;
8
+ detail?: string;
9
+ };
@@ -0,0 +1,21 @@
1
+ /** Execution state for a task, derived from SmithersEvent stream */
2
+ export type TaskExecutionState = {
3
+ nodeId: string;
4
+ iteration: number;
5
+ status:
6
+ | "pending"
7
+ | "started"
8
+ | "finished"
9
+ | "failed"
10
+ | "cancelled"
11
+ | "skipped"
12
+ | "waiting-approval"
13
+ | "waiting-event"
14
+ | "waiting-timer"
15
+ | "retrying";
16
+ attempt: number;
17
+ startedAt?: number;
18
+ finishedAt?: number;
19
+ error?: unknown;
20
+ toolCalls: Array<{ name: string; seq: number; status?: "success" | "error" }>;
21
+ };
@@ -0,0 +1,115 @@
1
+ /** @typedef {import("./DevToolsNode.ts").DevToolsNode} DevToolsNode */
2
+ /** @typedef {import("./DevToolsDelta.ts").DevToolsDelta} DevToolsDelta */
3
+ /** @typedef {import("./DevToolsSnapshotV1.ts").DevToolsSnapshotV1} DevToolsSnapshotV1 */
4
+
5
+ import { InvalidDeltaError } from "./InvalidDeltaError.js";
6
+
7
+ /**
8
+ * @param {unknown} value
9
+ * @returns {unknown}
10
+ */
11
+ function cloneValue(value) {
12
+ if (typeof structuredClone === "function") {
13
+ return structuredClone(value);
14
+ }
15
+ return JSON.parse(JSON.stringify(value));
16
+ }
17
+
18
+ /**
19
+ * @param {DevToolsNode} root
20
+ * @param {number} id
21
+ * @returns {{ node: DevToolsNode; parent: DevToolsNode | null; index: number } | null}
22
+ */
23
+ function findNode(root, id) {
24
+ /** @type {Array<{ node: DevToolsNode; parent: DevToolsNode | null; index: number }>} */
25
+ const stack = [{ node: root, parent: null, index: -1 }];
26
+ while (stack.length > 0) {
27
+ const current = stack.pop();
28
+ if (!current) {
29
+ continue;
30
+ }
31
+ if (current.node.id === id) {
32
+ return current;
33
+ }
34
+ for (let index = current.node.children.length - 1; index >= 0; index -= 1) {
35
+ const child = current.node.children[index];
36
+ stack.push({ node: child, parent: current.node, index });
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+
42
+ /**
43
+ * Apply a delta to a snapshot. Throws `InvalidDeltaError` for malformed ops.
44
+ *
45
+ * @param {DevToolsSnapshotV1} snapshot
46
+ * @param {DevToolsDelta} delta
47
+ * @returns {DevToolsSnapshotV1}
48
+ */
49
+ export function applyDelta(snapshot, delta) {
50
+ if (delta.version !== 1) {
51
+ throw new InvalidDeltaError(`Unsupported delta version: ${String(delta.version)}`);
52
+ }
53
+ if (delta.baseSeq !== snapshot.seq) {
54
+ throw new InvalidDeltaError(`Delta base seq ${delta.baseSeq} does not match snapshot seq ${snapshot.seq}.`);
55
+ }
56
+ /** @type {DevToolsSnapshotV1} */
57
+ const next = {
58
+ ...snapshot,
59
+ frameNo: delta.seq,
60
+ seq: delta.seq,
61
+ root: /** @type {DevToolsNode} */ (cloneValue(snapshot.root)),
62
+ };
63
+ for (const op of delta.ops) {
64
+ if (op.op === "replaceRoot") {
65
+ if (!op.node || typeof op.node !== "object") {
66
+ throw new InvalidDeltaError("replaceRoot requires a node.");
67
+ }
68
+ next.root = /** @type {DevToolsNode} */ (cloneValue(op.node));
69
+ continue;
70
+ }
71
+ if (op.op === "removeNode") {
72
+ if (op.id === next.root.id) {
73
+ throw new InvalidDeltaError("Cannot remove the root node.");
74
+ }
75
+ const target = findNode(next.root, op.id);
76
+ if (!target || !target.parent) {
77
+ throw new InvalidDeltaError(`Unknown node id: ${op.id}`);
78
+ }
79
+ target.parent.children.splice(target.index, 1);
80
+ continue;
81
+ }
82
+ if (op.op === "addNode") {
83
+ const parent = findNode(next.root, op.parentId);
84
+ if (!parent) {
85
+ throw new InvalidDeltaError(`Unknown parent id: ${op.parentId}`);
86
+ }
87
+ const index = Math.max(0, Math.min(op.index, parent.node.children.length));
88
+ parent.node.children.splice(index, 0, /** @type {DevToolsNode} */ (cloneValue(op.node)));
89
+ continue;
90
+ }
91
+ if (op.op === "updateProps") {
92
+ const target = findNode(next.root, op.id);
93
+ if (!target) {
94
+ throw new InvalidDeltaError(`Unknown node id: ${op.id}`);
95
+ }
96
+ target.node.props = /** @type {Record<string, unknown>} */ (cloneValue(op.props));
97
+ continue;
98
+ }
99
+ if (op.op === "updateTask") {
100
+ const target = findNode(next.root, op.id);
101
+ if (!target) {
102
+ throw new InvalidDeltaError(`Unknown node id: ${op.id}`);
103
+ }
104
+ if (op.task === undefined) {
105
+ delete target.node.task;
106
+ }
107
+ else {
108
+ target.node.task = /** @type {DevToolsNode["task"]} */ (cloneValue(op.task));
109
+ }
110
+ continue;
111
+ }
112
+ throw new InvalidDeltaError(`Unknown op: ${String(op?.op)}`);
113
+ }
114
+ return next;
115
+ }
@@ -0,0 +1,15 @@
1
+ import { countNodes } from "./countNodes.js";
2
+ /** @typedef {import("./DevToolsNode.ts").DevToolsNode} DevToolsNode */
3
+ /** @typedef {import("./DevToolsSnapshot.ts").DevToolsSnapshot} DevToolsSnapshot */
4
+
5
+ /**
6
+ * @param {DevToolsNode | null} root
7
+ * @returns {DevToolsSnapshot}
8
+ */
9
+ export function buildSnapshot(root) {
10
+ if (!root) {
11
+ return { tree: null, nodeCount: 0, taskCount: 0, timestamp: Date.now() };
12
+ }
13
+ const { nodes, tasks } = countNodes(root);
14
+ return { tree: root, nodeCount: nodes, taskCount: tasks, timestamp: Date.now() };
15
+ }
@@ -0,0 +1,15 @@
1
+
2
+ /** @typedef {import("./DevToolsNode.ts").DevToolsNode} DevToolsNode */
3
+ /**
4
+ * @param {DevToolsNode} node
5
+ * @param {DevToolsNode[]} [out]
6
+ * @returns {DevToolsNode[]}
7
+ */
8
+ export function collectTasks(node, out = []) {
9
+ if (node.type === "task")
10
+ out.push(node);
11
+ for (const child of node.children) {
12
+ collectTasks(child, out);
13
+ }
14
+ return out;
15
+ }
@@ -0,0 +1,16 @@
1
+
2
+ /** @typedef {import("./DevToolsNode.ts").DevToolsNode} DevToolsNode */
3
+ /**
4
+ * @param {DevToolsNode} node
5
+ * @returns {{ nodes: number; tasks: number }}
6
+ */
7
+ export function countNodes(node) {
8
+ let nodes = 1;
9
+ let tasks = node.type === "task" ? 1 : 0;
10
+ for (const child of node.children) {
11
+ const c = countNodes(child);
12
+ nodes += c.nodes;
13
+ tasks += c.tasks;
14
+ }
15
+ return { nodes, tasks };
16
+ }
@@ -0,0 +1,211 @@
1
+ /** @typedef {import("./DevToolsNode.ts").DevToolsNode} DevToolsNode */
2
+ /** @typedef {import("./DevToolsDelta.ts").DevToolsDelta} DevToolsDelta */
3
+ /** @typedef {import("./DevToolsDeltaOp.ts").DevToolsDeltaOp} DevToolsDeltaOp */
4
+ /** @typedef {import("./DevToolsSnapshotV1.ts").DevToolsSnapshotV1} DevToolsSnapshotV1 */
5
+
6
+ /**
7
+ * @param {unknown} value
8
+ * @returns {unknown}
9
+ */
10
+ function cloneValue(value) {
11
+ if (typeof structuredClone === "function") {
12
+ return structuredClone(value);
13
+ }
14
+ return JSON.parse(JSON.stringify(value));
15
+ }
16
+
17
+ /**
18
+ * @param {unknown} value
19
+ * @returns {boolean}
20
+ */
21
+ function isRecord(value) {
22
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
23
+ }
24
+
25
+ /**
26
+ * @param {unknown} left
27
+ * @param {unknown} right
28
+ * @returns {boolean}
29
+ */
30
+ function deepEqual(left, right) {
31
+ if (Object.is(left, right)) {
32
+ return true;
33
+ }
34
+ if (typeof left !== typeof right) {
35
+ return false;
36
+ }
37
+ if (left === null || right === null) {
38
+ return left === right;
39
+ }
40
+ if (Array.isArray(left) && Array.isArray(right)) {
41
+ if (left.length !== right.length) {
42
+ return false;
43
+ }
44
+ for (let index = 0; index < left.length; index += 1) {
45
+ if (!deepEqual(left[index], right[index])) {
46
+ return false;
47
+ }
48
+ }
49
+ return true;
50
+ }
51
+ if (isRecord(left) && isRecord(right)) {
52
+ const leftKeys = Object.keys(left);
53
+ const rightKeys = Object.keys(right);
54
+ if (leftKeys.length !== rightKeys.length) {
55
+ return false;
56
+ }
57
+ leftKeys.sort();
58
+ rightKeys.sort();
59
+ for (let index = 0; index < leftKeys.length; index += 1) {
60
+ if (leftKeys[index] !== rightKeys[index]) {
61
+ return false;
62
+ }
63
+ }
64
+ for (const key of leftKeys) {
65
+ if (!deepEqual(left[key], right[key])) {
66
+ return false;
67
+ }
68
+ }
69
+ return true;
70
+ }
71
+ return false;
72
+ }
73
+
74
+ /**
75
+ * @param {DevToolsNode} root
76
+ * @returns {Map<number, { node: DevToolsNode; parentId: number | null; index: number }>}
77
+ */
78
+ function indexTree(root) {
79
+ /** @type {Map<number, { node: DevToolsNode; parentId: number | null; index: number }>} */
80
+ const indexed = new Map();
81
+ /** @type {Array<{ node: DevToolsNode; parentId: number | null; index: number }>} */
82
+ const stack = [{ node: root, parentId: null, index: 0 }];
83
+ while (stack.length > 0) {
84
+ const current = stack.pop();
85
+ if (!current) {
86
+ continue;
87
+ }
88
+ indexed.set(current.node.id, current);
89
+ for (let childIndex = current.node.children.length - 1; childIndex >= 0; childIndex -= 1) {
90
+ const child = current.node.children[childIndex];
91
+ stack.push({
92
+ node: child,
93
+ parentId: current.node.id,
94
+ index: childIndex,
95
+ });
96
+ }
97
+ }
98
+ return indexed;
99
+ }
100
+
101
+ /**
102
+ * @param {DevToolsNode} left
103
+ * @param {DevToolsNode} right
104
+ * @returns {boolean}
105
+ */
106
+ function sameNodeShape(left, right) {
107
+ return left.type === right.type &&
108
+ left.name === right.name &&
109
+ left.depth === right.depth;
110
+ }
111
+
112
+ /**
113
+ * Compute a delta from snapshot `a` to snapshot `b`.
114
+ *
115
+ * @param {DevToolsSnapshotV1} a
116
+ * @param {DevToolsSnapshotV1} b
117
+ * @returns {DevToolsDelta}
118
+ */
119
+ export function diffSnapshots(a, b) {
120
+ if (a.runId !== b.runId) {
121
+ throw new Error("Cannot diff snapshots from different runs.");
122
+ }
123
+ // Root replacement: if the root's identity (id) changed, or its shape
124
+ // changed, emit a single replaceRoot op rather than trying to remove + add
125
+ // the root in place (the root cannot be removed).
126
+ if (a.root.id !== b.root.id || !sameNodeShape(a.root, b.root)) {
127
+ return {
128
+ version: 1,
129
+ baseSeq: a.seq,
130
+ seq: b.seq,
131
+ ops: [{
132
+ op: "replaceRoot",
133
+ node: /** @type {DevToolsNode} */ (cloneValue(b.root)),
134
+ }],
135
+ };
136
+ }
137
+ const from = indexTree(a.root);
138
+ const to = indexTree(b.root);
139
+ /** @type {Set<number>} */
140
+ const removeSet = new Set();
141
+ /** @type {Set<number>} */
142
+ const addSet = new Set();
143
+ /** @type {DevToolsDeltaOp[]} */
144
+ const updateOps = [];
145
+ for (const [id, fromEntry] of from.entries()) {
146
+ const toEntry = to.get(id);
147
+ if (!toEntry) {
148
+ removeSet.add(id);
149
+ continue;
150
+ }
151
+ const moved = fromEntry.parentId !== toEntry.parentId || fromEntry.index !== toEntry.index;
152
+ const replaced = !sameNodeShape(fromEntry.node, toEntry.node);
153
+ if (moved || replaced) {
154
+ removeSet.add(id);
155
+ addSet.add(id);
156
+ continue;
157
+ }
158
+ if (!deepEqual(fromEntry.node.props, toEntry.node.props)) {
159
+ updateOps.push({
160
+ op: "updateProps",
161
+ id,
162
+ props: /** @type {Record<string, unknown>} */ (cloneValue(toEntry.node.props)),
163
+ });
164
+ }
165
+ if (!deepEqual(fromEntry.node.task, toEntry.node.task)) {
166
+ updateOps.push({
167
+ op: "updateTask",
168
+ id,
169
+ task: /** @type {DevToolsNode["task"]} */ (cloneValue(toEntry.node.task)),
170
+ });
171
+ }
172
+ }
173
+ for (const [id] of to.entries()) {
174
+ if (!from.has(id)) {
175
+ addSet.add(id);
176
+ }
177
+ }
178
+ /** @type {DevToolsDeltaOp[]} */
179
+ const removeOps = [...removeSet]
180
+ .sort((leftId, rightId) => (from.get(rightId)?.node.depth ?? 0) - (from.get(leftId)?.node.depth ?? 0))
181
+ .map((id) => ({ op: "removeNode", id }));
182
+ const topLevelAddIds = [...addSet].filter((id) => {
183
+ const parentId = to.get(id)?.parentId;
184
+ return parentId === null || !addSet.has(parentId);
185
+ });
186
+ /** @type {DevToolsDeltaOp[]} */
187
+ const addOps = topLevelAddIds
188
+ .sort((leftId, rightId) => (to.get(leftId)?.node.depth ?? 0) - (to.get(rightId)?.node.depth ?? 0))
189
+ .map((id) => {
190
+ const entry = to.get(id);
191
+ if (!entry || entry.parentId === null) {
192
+ return /** @type {DevToolsDeltaOp} */ ({
193
+ op: "updateProps",
194
+ id,
195
+ props: /** @type {Record<string, unknown>} */ (cloneValue(entry?.node.props ?? {})),
196
+ });
197
+ }
198
+ return /** @type {DevToolsDeltaOp} */ ({
199
+ op: "addNode",
200
+ parentId: entry.parentId,
201
+ index: entry.index,
202
+ node: /** @type {DevToolsNode} */ (cloneValue(entry.node)),
203
+ });
204
+ });
205
+ return {
206
+ version: 1,
207
+ baseSeq: a.seq,
208
+ seq: b.seq,
209
+ ops: [...removeOps, ...addOps, ...updateOps],
210
+ };
211
+ }
@@ -0,0 +1,17 @@
1
+
2
+ /** @typedef {import("./DevToolsNode.ts").DevToolsNode} DevToolsNode */
3
+ /**
4
+ * @param {DevToolsNode} node
5
+ * @param {string} nodeId
6
+ * @returns {DevToolsNode | null}
7
+ */
8
+ export function findNodeById(node, nodeId) {
9
+ if (node.task?.nodeId === nodeId)
10
+ return node;
11
+ for (const child of node.children) {
12
+ const found = findNodeById(child, nodeId);
13
+ if (found)
14
+ return found;
15
+ }
16
+ return null;
17
+ }