@kiberon-labs/behave-graph-flow 2.0.0 → 3.0.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.
Files changed (251) hide show
  1. package/.storybook/manager.ts +6 -0
  2. package/.storybook/preview.ts +49 -1
  3. package/.storybook/styles.css +9 -3
  4. package/.turbo/turbo-build.log +1 -1
  5. package/CHANGELOG.md +368 -0
  6. package/dist/AnyControlImpl-Ds-CShIB.js +20 -0
  7. package/dist/AnyControlImpl-Ds-CShIB.js.map +1 -0
  8. package/dist/DocumentationBrowserPanelImpl-deZNzFX8.js +166 -0
  9. package/dist/DocumentationBrowserPanelImpl-deZNzFX8.js.map +1 -0
  10. package/dist/index.css +36 -33
  11. package/dist/index.css.map +1 -1
  12. package/dist/index.d.ts +1865 -550
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +14357 -11221
  15. package/dist/index.js.map +1 -1
  16. package/dist/noteImpl-KkrrWgJd.js +242 -0
  17. package/dist/noteImpl-KkrrWgJd.js.map +1 -0
  18. package/dist/styles.module-CvmpDkZj.css +3 -0
  19. package/dist/styles.module-CvmpDkZj.css.map +1 -0
  20. package/dist/styles.module-DZxg8aW9.js +271 -0
  21. package/dist/styles.module-DZxg8aW9.js.map +1 -0
  22. package/dist/useChangeNodeData-ChQGK7AI.js +23 -0
  23. package/dist/useChangeNodeData-ChQGK7AI.js.map +1 -0
  24. package/docs/protocol.md +43 -20
  25. package/package.json +5 -9
  26. package/src/components/FloatingToolbar/index.module.css +5 -13
  27. package/src/components/FloatingToolbar/index.tsx +9 -9
  28. package/src/components/Flow.tsx +34 -23
  29. package/src/components/contextMenus/DynamicContextMenu.tsx +85 -0
  30. package/src/components/contextMenus/NodePicker.module.css +13 -13
  31. package/src/components/contextMenus/edge.tsx +9 -95
  32. package/src/components/contextMenus/node.tsx +9 -149
  33. package/src/components/contextMenus/selection.tsx +5 -71
  34. package/src/components/controls/any/AnyControlImpl.tsx +14 -0
  35. package/src/components/controls/any/index.tsx +13 -2
  36. package/src/components/edges/index.tsx +75 -69
  37. package/src/components/layoutController/index.module.css +3 -0
  38. package/src/components/layoutController/index.tsx +24 -1
  39. package/src/components/layoutController/utils.ts +46 -3
  40. package/src/components/menubar/defaults.tsx +55 -19
  41. package/src/components/menubar/menuItem.module.css +18 -3
  42. package/src/components/menubar/menuItem.tsx +34 -1
  43. package/src/components/nodes/behave/NodeContainer.module.css +26 -25
  44. package/src/components/nodes/group/index.tsx +3 -3
  45. package/src/components/nodes/wrapper/styles.module.css +6 -32
  46. package/src/components/panels/alignment/index.module.css +0 -10
  47. package/src/components/panels/alignment/index.tsx +4 -4
  48. package/src/components/panels/base/styles.module.css +2 -2
  49. package/src/components/panels/common/PanelHeader.module.css +24 -0
  50. package/src/components/panels/common/PanelHeader.tsx +22 -0
  51. package/src/components/panels/common/SectionTitle.module.css +13 -0
  52. package/src/components/panels/common/SectionTitle.tsx +10 -0
  53. package/src/components/panels/events/EditEventPanel.tsx +14 -5
  54. package/src/components/panels/events/ManageEventsPanel.tsx +11 -8
  55. package/src/components/panels/events/styles.module.css +6 -64
  56. package/src/components/panels/graphProperties/index.tsx +125 -0
  57. package/src/components/panels/history/index.tsx +2 -2
  58. package/src/components/panels/history/styles.module.css +0 -9
  59. package/src/components/panels/keymaps/index.module.css +3 -13
  60. package/src/components/panels/keymaps/index.tsx +1 -2
  61. package/src/components/panels/layers/index.tsx +20 -15
  62. package/src/components/panels/layers/styles.module.css +9 -12
  63. package/src/components/panels/legend/index.tsx +1 -1
  64. package/src/components/panels/logs/index.module.css +25 -19
  65. package/src/components/panels/logs/index.tsx +7 -7
  66. package/src/components/panels/nodeInputs/InputsGroup.tsx +1 -0
  67. package/src/components/panels/nodeInputs/NodeSettings.tsx +2 -2
  68. package/src/components/panels/nodeInputs/NodeTitleEditor.tsx +1 -1
  69. package/src/components/panels/nodeInputs/OutputsGroup.tsx +2 -12
  70. package/src/components/panels/nodeInputs/index.module.css +99 -75
  71. package/src/components/panels/nodeInputs/index.tsx +21 -11
  72. package/src/components/panels/nodeInputs/useNodeHandlers.ts +2 -2
  73. package/src/components/panels/nodeInputs/useNodeInputsData.ts +23 -43
  74. package/src/components/panels/nodePicker/index.tsx +8 -8
  75. package/src/components/panels/panel/index.module.css +7 -7
  76. package/src/components/panels/search/index.module.css +0 -50
  77. package/src/components/panels/search/index.tsx +2 -2
  78. package/src/components/panels/systemSettings/ConversionsSettings.tsx +203 -0
  79. package/src/components/panels/systemSettings/index.tsx +221 -176
  80. package/src/components/panels/systemSettings/styles.module.css +135 -8
  81. package/src/components/panels/traces/GridLines.tsx +1 -1
  82. package/src/components/panels/traces/TimeGrid.tsx +3 -3
  83. package/src/components/panels/traces/TraceLane.tsx +1 -1
  84. package/src/components/panels/traces/index.module.css +1 -8
  85. package/src/components/panels/traces/index.tsx +8 -4
  86. package/src/components/panels/traces/useDerivedSpans.ts +241 -146
  87. package/src/components/panels/traces/utils.ts +8 -0
  88. package/src/components/panels/variables/CreateVariableScreen.tsx +3 -3
  89. package/src/components/panels/variables/ManageVariablesScreen.tsx +12 -9
  90. package/src/components/panels/variables/index.tsx +2 -2
  91. package/src/components/panels/variables/styles.module.css +4 -91
  92. package/src/components/primitives/icon.module.css +4 -4
  93. package/src/components/sockets/input/index.tsx +9 -2
  94. package/src/components/sockets/input/styles.module.css +2 -3
  95. package/src/components/sockets/output/index.tsx +10 -3
  96. package/src/components/sockets/output/styles.module.css +1 -6
  97. package/src/css/notes.css +135 -0
  98. package/src/css/prosemirror.css +3 -3
  99. package/src/css/rc-dock.css +143 -43
  100. package/src/css/rc-menu.css +56 -55
  101. package/src/css/themes/kiberon.css +127 -0
  102. package/src/css/vars.css +197 -13
  103. package/src/css/vscode-elements.css +124 -0
  104. package/src/generators/CallSubgraphGenerator.tsx +136 -0
  105. package/src/generators/CustomEventOnTriggeredGenerator.tsx +2 -2
  106. package/src/generators/GraphBoundaryGenerator.module.css +32 -0
  107. package/src/generators/GraphBoundaryGenerator.tsx +193 -0
  108. package/src/generators/SequenceGenerator.tsx +2 -2
  109. package/src/generators/SwitchOnIntegerGenerator.tsx +2 -2
  110. package/src/generators/SwitchOnStringGenerator.tsx +2 -2
  111. package/src/generators/callSubgraphSync.ts +126 -0
  112. package/src/generators/registerDefaultGenerators.ts +21 -0
  113. package/src/generators/registerDefaults.ts +26 -0
  114. package/src/hooks/useBehaveGraphFlow.ts +2 -2
  115. package/src/hooks/useFlowHandlers.ts +47 -9
  116. package/src/hooks/useWasdPan.ts +26 -4
  117. package/src/index.css +4 -16
  118. package/src/index.ts +17 -0
  119. package/src/manifest/contributionRegistry.ts +93 -0
  120. package/src/manifest/index.ts +4 -0
  121. package/src/manifest/loadManifest.ts +82 -0
  122. package/src/manifest/manifestPlugin.ts +29 -0
  123. package/src/manifest/passthroughValueType.ts +40 -0
  124. package/src/plugin/alignment/index.ts +22 -12
  125. package/src/plugin/autosave/controller.ts +366 -0
  126. package/src/plugin/autosave/index.tsx +114 -0
  127. package/src/plugin/autosave/panel/BackupPanel.tsx +141 -0
  128. package/src/plugin/autosave/panel/index.tsx +1 -0
  129. package/src/plugin/autosave/panel/styles.module.css +56 -0
  130. package/src/plugin/autosave/settings.ts +65 -0
  131. package/src/plugin/autosave/storage.ts +147 -0
  132. package/src/plugin/docs/index.tsx +2 -4
  133. package/src/plugin/docs/panel/DocumentationBrowserPanelImpl.tsx +200 -0
  134. package/src/plugin/docs/panel/index.tsx +15 -194
  135. package/src/plugin/docs/panel/styles.module.css +8 -8
  136. package/src/plugin/graphrunner/actions.ts +258 -185
  137. package/src/plugin/graphrunner/buttons.tsx +34 -26
  138. package/src/plugin/graphrunner/client.ts +4 -1
  139. package/src/plugin/graphrunner/index.tsx +29 -100
  140. package/src/plugin/graphrunner/panel.tsx +2 -2
  141. package/src/plugin/graphrunner/runController.ts +283 -0
  142. package/src/plugin/graphrunner/runner.ts +21 -192
  143. package/src/plugin/graphrunner/store.ts +14 -24
  144. package/src/plugin/graphrunner/styles.module.css +17 -57
  145. package/src/plugin/graphrunner/transport.ts +26 -0
  146. package/src/plugin/graphrunner/types.ts +21 -0
  147. package/src/plugin/graphrunner-local/execution-utils.ts +260 -80
  148. package/src/plugin/graphrunner-local/index.tsx +8 -2
  149. package/src/plugin/graphrunner-local/panel.tsx +131 -175
  150. package/src/plugin/graphrunner-local/styles.module.css +57 -76
  151. package/src/plugin/graphrunner-local/transport.ts +151 -184
  152. package/src/plugin/graphrunner-webworker/graph-executor.worker.ts +2 -0
  153. package/src/plugin/graphrunner-webworker/index.tsx +4 -10
  154. package/src/plugin/graphrunner-webworker/store.ts +9 -0
  155. package/src/plugin/kitchen-sink/index.ts +38 -0
  156. package/src/{layout/dagre.tsx → plugin/layout/dagre.ts} +17 -5
  157. package/src/{layout → plugin/layout}/elk.ts +22 -6
  158. package/src/plugin/layout/index.ts +80 -0
  159. package/src/plugin/notes/FormatToolbar.tsx +200 -0
  160. package/src/plugin/notes/index.tsx +191 -0
  161. package/src/plugin/notes/nodeActions.ts +100 -0
  162. package/src/plugin/notes/note.tsx +20 -0
  163. package/src/plugin/notes/noteImpl.tsx +89 -0
  164. package/src/plugin/realtime/realtimeRunner.ts +58 -4
  165. package/src/specifics/CustomEventOnTriggeredSpecific.tsx +2 -2
  166. package/src/specifics/CustomEventTriggerSpecific.tsx +2 -2
  167. package/src/specifics/VariableGetSpecific.tsx +2 -2
  168. package/src/specifics/VariableSetSpecific.tsx +2 -2
  169. package/src/store/actions.tsx +5 -5
  170. package/src/store/commands.ts +278 -0
  171. package/src/store/contextMenu.ts +192 -0
  172. package/src/store/conversions.ts +47 -0
  173. package/src/store/flow.tsx +23 -38
  174. package/src/store/graphMeta.ts +39 -0
  175. package/src/store/hotKeys.tsx +301 -260
  176. package/src/store/layers.ts +3 -3
  177. package/src/store/registry.ts +12 -4
  178. package/src/store/selection.ts +3 -3
  179. package/src/store/settings.ts +82 -82
  180. package/src/store/settingsSchema.ts +210 -0
  181. package/src/store/tabs.ts +5 -1
  182. package/src/store/traces.ts +3 -3
  183. package/src/system/graph.ts +11 -14
  184. package/src/system/graphSession.ts +172 -0
  185. package/src/system/index.ts +3 -0
  186. package/src/system/notifications.ts +13 -0
  187. package/src/system/persistence.ts +82 -0
  188. package/src/system/plugin.ts +28 -0
  189. package/src/system/provider.tsx +64 -0
  190. package/src/system/system.ts +518 -88
  191. package/src/system/tabLoader.tsx +70 -32
  192. package/src/system/undoRedo.ts +1 -1
  193. package/src/transformers/Uigraph.ts +5 -4
  194. package/src/transformers/contract.ts +87 -0
  195. package/src/transformers/flowToBehave.ts +13 -5
  196. package/src/types/nodes.ts +8 -3
  197. package/src/types.ts +2 -0
  198. package/src/util/autoConvert.ts +200 -0
  199. package/src/util/isValidConnection.ts +23 -2
  200. package/stories/defaults/defaultStoryProvider.tsx +17 -14
  201. package/stories/defaults/systemGenerator.ts +6 -1
  202. package/stories/{components/nodes/comment.stories.tsx → plugins/notes.stories.tsx} +24 -30
  203. package/tests/autoConvert.test.ts +329 -0
  204. package/tests/autosavePlugin.test.ts +204 -0
  205. package/tests/callSubgraphSync.test.ts +148 -0
  206. package/tests/commandRegistry.test.ts +137 -0
  207. package/tests/contract.test.ts +51 -0
  208. package/tests/contractSerialize.test.ts +62 -0
  209. package/tests/deriveSpans.test.ts +71 -0
  210. package/tests/flowToBehave.test.ts +2 -1
  211. package/tests/hotkeys.test.ts +79 -0
  212. package/tests/keepAliveLifecycle.test.ts +167 -0
  213. package/tests/loadManifest.test.ts +113 -0
  214. package/tests/noteMarkdown.test.ts +65 -0
  215. package/tests/notesPlugin.test.ts +162 -0
  216. package/tests/persistence.test.ts +51 -0
  217. package/tests/saveLoad.test.ts +7 -6
  218. package/tests/settings.test.ts +178 -0
  219. package/tests/traceStore.test.ts +46 -0
  220. package/tests/visual/README.md +2 -2
  221. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-conversation-chromium-win32.png +0 -0
  222. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-events-chromium-win32.png +0 -0
  223. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-history-chromium-win32.png +0 -0
  224. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-keymaps-chromium-win32.png +0 -0
  225. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-layers-chromium-win32.png +0 -0
  226. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-legend-chromium-win32.png +0 -0
  227. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-localGraphRunner-chromium-win32.png +0 -0
  228. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-logs-chromium-win32.png +0 -0
  229. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodeInputs-chromium-win32.png +0 -0
  230. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodePicker-chromium-win32.png +0 -0
  231. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-panel-chromium-win32.png +0 -0
  232. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-search-chromium-win32.png +0 -0
  233. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-systemSettings-chromium-win32.png +0 -0
  234. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-variables-chromium-win32.png +0 -0
  235. package/tests/visual/panels.visual.test.tsx +3 -3
  236. package/tests/wasdPan.test.ts +71 -0
  237. package/vitest.config.ts +1 -1
  238. package/vitest.visual.config.ts +7 -0
  239. package/.storybook/vscode.css +0 -814
  240. package/src/components/nodes/comment/FormatToolbar.tsx +0 -118
  241. package/src/components/nodes/comment/comment.tsx +0 -103
  242. package/src/components/nodes/comment/styles.module.css +0 -150
  243. package/src/components/panels/conversation/index.module.css +0 -151
  244. package/src/components/panels/conversation/index.tsx +0 -162
  245. package/src/components/panels/events/CustomEventsEditor.tsx +0 -384
  246. package/src/css/vscode.css +0 -13
  247. package/src/hooks/useDetachNodes.ts +0 -39
  248. package/src/plugin/graphrunner-webworker/types.ts +0 -17
  249. package/src/specifics/registerDefaultSpecifics.ts +0 -5
  250. package/src/store/chat.ts +0 -73
  251. package/src/store/graphRunnerClient.ts +0 -110
@@ -21,7 +21,8 @@ import type {
21
21
  RunStatus,
22
22
  ServerGraphRunnerMessage,
23
23
  ServerVariable,
24
- ServerEvent
24
+ ServerEvent,
25
+ TraceBatchEvent
25
26
  } from '../graphrunner/types.js';
26
27
  import { sleep } from '@kiberon-labs/behave-graph';
27
28
 
@@ -41,6 +42,12 @@ export interface ActiveRun {
41
42
  isPaused: boolean;
42
43
  executionPhase: 'start' | 'tick' | 'end' | 'completed';
43
44
  currentTick: number;
45
+ /**
46
+ * Flushes any trace events still buffered by {@link setupTracing}. Set when
47
+ * tracing is enabled; call before emitting `completed`/`stopped` so the
48
+ * client receives the tail of the trace while the run id is still routable.
49
+ */
50
+ flushTracing?: () => void;
44
51
  }
45
52
 
46
53
  export interface MessageContext {
@@ -188,20 +195,59 @@ export function handleGetCapabilities(ctx: MessageContext): void {
188
195
  });
189
196
  }
190
197
 
198
+ /** Flush buffered trace events roughly once per frame. */
199
+ const TRACE_FLUSH_INTERVAL_MS = 16;
200
+ /** Safety valve: flush early if a single window buffers this many events. */
201
+ const TRACE_FLUSH_MAX_EVENTS = 2048;
202
+
191
203
  /**
192
- * Setup tracing for a run
204
+ * Setup tracing for a run.
205
+ *
206
+ * Node execution events are buffered and flushed as a single `traceBatch`
207
+ * message per flush window instead of one `trace` message per event. A graph
208
+ * ticking at display rate executes every node twice per frame (start + end);
209
+ * sending each event individually made the message pipeline (store updates,
210
+ * postMessage for the worker runner) the dominant per-frame cost.
193
211
  */
194
212
  export function setupTracing(
195
213
  run: ActiveRun,
196
214
  graphId: string,
197
215
  ctx: MessageContext
198
216
  ): void {
199
- run.engine.onNodeExecutionStart.addListener((node) => {
200
- run.performance.nodesExecuted++;
217
+ let buffer: TraceBatchEvent[] = [];
218
+ let flushTimer: ReturnType<typeof setTimeout> | undefined;
219
+
220
+ const flush = () => {
221
+ if (flushTimer !== undefined) {
222
+ clearTimeout(flushTimer);
223
+ flushTimer = undefined;
224
+ }
225
+ if (buffer.length === 0) return;
226
+ const events = buffer;
227
+ buffer = [];
201
228
  ctx.sendMessage({
202
- type: 'trace',
229
+ type: 'traceBatch',
203
230
  runId: run.runId,
204
231
  graphId,
232
+ events
233
+ });
234
+ };
235
+ run.flushTracing = flush;
236
+
237
+ const push = (event: TraceBatchEvent) => {
238
+ buffer.push(event);
239
+ if (buffer.length >= TRACE_FLUSH_MAX_EVENTS) {
240
+ flush();
241
+ return;
242
+ }
243
+ if (flushTimer === undefined) {
244
+ flushTimer = setTimeout(flush, TRACE_FLUSH_INTERVAL_MS);
245
+ }
246
+ };
247
+
248
+ run.engine.onNodeExecutionStart.addListener((node) => {
249
+ run.performance.nodesExecuted++;
250
+ push({
205
251
  nodeId: node.id,
206
252
  event: 'start',
207
253
  data: { typeName: node.description.typeName },
@@ -210,10 +256,7 @@ export function setupTracing(
210
256
  });
211
257
 
212
258
  run.engine.onNodeExecutionEnd.addListener((node) => {
213
- ctx.sendMessage({
214
- type: 'trace',
215
- runId: run.runId,
216
- graphId,
259
+ push({
217
260
  nodeId: node.id,
218
261
  event: 'end',
219
262
  data: { typeName: node.description.typeName },
@@ -262,91 +305,228 @@ export function setupVariableChangeTracking(
262
305
  /**
263
306
  * Execute a graph through its lifecycle phases
264
307
  */
265
- export async function executeGraphLifecycle(
308
+ /**
309
+ * The single graph-execution lifecycle shared by both runners (the in-browser
310
+ * local transport and the web-worker runner). It drives the start → tick → end →
311
+ * completed phase machine and emits the `completed` / error messages; runner-
312
+ * specific behaviour (how fibers are stepped, tick timing, and what to do on
313
+ * completion/error) is injected via {@link ExecuteGraphLifecycleOptions} hooks so
314
+ * neither runner keeps its own copy of this logic.
315
+ */
316
+ export interface ExecuteGraphLifecycleOptions {
317
+ /** Tick timing when no {@link tickStrategy} is given. Defaults to 50ms. */
318
+ tickInterval?: number;
319
+ /**
320
+ * When true, the run finalizes (end phase, `completed` message, engine
321
+ * dispose) once its flows drain. Defaults to false: the run idles in the
322
+ * tick phase, keeping event-node subscriptions live and draining any fibers
323
+ * they commit, until it is explicitly stopped.
324
+ */
325
+ autoEnd?: boolean;
326
+ /**
327
+ * Run the engine's pending fibers for the current phase. Defaults to
328
+ * `run.engine.executeAllAsync()`; the local runner injects a pause-aware
329
+ * executor that also honours its step-delay / speed settings.
330
+ */
331
+ executeStep?: () => Promise<void>;
332
+ /** Timing between tick iterations. Defaults to `sleep(tickInterval)`. */
333
+ tickStrategy?: () => Promise<void>;
334
+ /** Invoked after each tick iteration. */
335
+ onStepComplete?: () => Promise<void>;
336
+ /**
337
+ * Invoked after a natural completion — the run is marked completed, the
338
+ * `completed` message has been sent, and the engine disposed. Lets a runner
339
+ * run session hooks and sync its own state (e.g. the local panel's status).
340
+ */
341
+ onComplete?: () => void | Promise<void>;
342
+ /**
343
+ * Invoked on error (after the run is marked errored, before the engine is
344
+ * disposed and the error rethrown). Replaces the default `sendError`.
345
+ */
346
+ onError?: (error: Error) => void | Promise<void>;
347
+ }
348
+
349
+ /** True when a lifecycle event has at least one listener attached. */
350
+ function hasListeners(event?: { listenerCount: number }): boolean {
351
+ return !!event && event.listenerCount > 0;
352
+ }
353
+
354
+ /**
355
+ * Run the `start` lifecycle phase: emit the start event (if anyone is
356
+ * listening) and drain the fibers it commits, then advance to the tick phase.
357
+ */
358
+ async function runStartPhase(
266
359
  run: ActiveRun,
267
- graphId: string,
268
- ctx: MessageContext,
269
- options?: {
270
- tickInterval?: number;
271
- onStepComplete?: () => Promise<void>;
272
- autoEnd?: boolean;
273
- }
360
+ eventEmitter: ILifecycleEventEmitter | undefined,
361
+ executeStep: () => Promise<unknown>
274
362
  ): Promise<void> {
275
- try {
276
- const eventEmitter = run.registry.dependencies?.ILifecycleEventEmitter as
277
- | ILifecycleEventEmitter
278
- | undefined;
363
+ if (run.executionPhase !== 'start') return;
364
+ if (hasListeners(eventEmitter?.startEvent)) {
365
+ eventEmitter!.startEvent.emit();
366
+ await executeStep();
367
+ }
368
+ run.executionPhase = 'tick';
369
+ }
279
370
 
280
- // Execute start event
281
- if (run.executionPhase === 'start') {
282
- if (
283
- eventEmitter?.startEvent &&
284
- eventEmitter.startEvent.listenerCount > 0
285
- ) {
286
- eventEmitter.startEvent.emit();
287
- await run.engine.executeAllAsync();
288
- }
289
- run.executionPhase = 'tick';
371
+ /**
372
+ * Run the `tick` phase until the run is paused or stopped. Returns `true` when
373
+ * the loop yielded control mid-tick (paused/stopped) so the caller should bail
374
+ * out without finalizing.
375
+ *
376
+ * A run stays alive here even with no tick listeners: completing it would
377
+ * dispose the engine and tear down event-node subscriptions (ai/onToolCall,
378
+ * ai/onMessage, custom triggers) that fire out-of-band, after the start flow has
379
+ * drained. The loop keeps draining fibers those events commit. Pass
380
+ * `autoEnd: true` to restore the finalize-when-drained behaviour for
381
+ * fire-and-forget runs.
382
+ */
383
+ async function runTickPhase(
384
+ run: ActiveRun,
385
+ eventEmitter: ILifecycleEventEmitter | undefined,
386
+ executeStep: () => Promise<unknown>,
387
+ tickStrategy: () => Promise<unknown>,
388
+ options?: ExecuteGraphLifecycleOptions
389
+ ): Promise<boolean> {
390
+ if (run.executionPhase !== 'tick') return false;
391
+
392
+ const hasTickListeners = hasListeners(eventEmitter?.tickEvent);
393
+ const autoEnd = options?.autoEnd ?? false;
394
+ if (!hasTickListeners && autoEnd) {
395
+ run.executionPhase = 'end';
396
+ return false;
397
+ }
398
+
399
+ while (!run.isPaused && run.status === 'running') {
400
+ if (hasTickListeners) {
401
+ eventEmitter!.tickEvent.emit();
402
+ run.currentTick++;
290
403
  }
404
+ await executeStep();
291
405
 
292
- // Execute tick events
293
- if (run.executionPhase === 'tick') {
294
- if (eventEmitter?.tickEvent && eventEmitter.tickEvent.listenerCount > 0) {
295
- while (!run.isPaused && run.status === 'running') {
296
- eventEmitter.tickEvent.emit();
297
- await run.engine.executeAllAsync();
298
- run.currentTick++;
299
-
300
- if (options?.onStepComplete) {
301
- await options.onStepComplete();
302
- }
303
-
304
- if (run.isPaused || run.status !== 'running') {
305
- return;
306
- }
307
-
308
- await sleep((options?.tickInterval ?? 50) / 1000);
309
- }
310
- } else {
311
- run.executionPhase = 'end';
312
- }
406
+ if (options?.onStepComplete) {
407
+ await options.onStepComplete();
313
408
  }
314
409
 
315
- // Execute end event
316
- if (run.executionPhase === 'end' && !run.isPaused) {
317
- if (eventEmitter?.endEvent && eventEmitter.endEvent.listenerCount > 0) {
318
- eventEmitter.endEvent.emit();
319
- await run.engine.executeAllAsync();
320
- }
321
- run.executionPhase = 'completed';
410
+ if (run.isPaused || run.status !== 'running') {
411
+ return true;
322
412
  }
323
413
 
324
- // Complete if not paused
325
- if (!run.isPaused && !options?.autoEnd) {
326
- run.status = 'completed';
327
- const elapsedMs = Date.now() - run.startedAt;
414
+ await tickStrategy();
415
+ }
416
+ return false;
417
+ }
328
418
 
329
- ctx.sendMessage({
330
- type: 'completed',
331
- runId: run.runId,
332
- graphId,
333
- completedAt: Date.now(),
334
- elapsedMs,
335
- result: null,
336
- performance: run.performance
337
- });
419
+ /**
420
+ * Run the `end` lifecycle phase: emit the end event (if listened) and drain,
421
+ * then mark the run as reaching the `completed` phase.
422
+ */
423
+ async function runEndPhase(
424
+ run: ActiveRun,
425
+ eventEmitter: ILifecycleEventEmitter | undefined,
426
+ executeStep: () => Promise<unknown>
427
+ ): Promise<void> {
428
+ if (run.executionPhase !== 'end' || run.isPaused) return;
429
+ if (hasListeners(eventEmitter?.endEvent)) {
430
+ eventEmitter!.endEvent.emit();
431
+ await executeStep();
432
+ }
433
+ run.executionPhase = 'completed';
434
+ }
338
435
 
339
- run.engine.dispose();
340
- }
341
- } catch (error) {
342
- run.status = 'error';
343
- const errorMessage = error instanceof Error ? error.message : String(error);
344
- ctx.sendError('NODE_EXECUTION_ERROR', errorMessage, {
436
+ /**
437
+ * Finalize a run that ran out of fibers and isn't paused. Only autoEnd runs
438
+ * advance this far; by default a run idles in the tick phase until stopped.
439
+ */
440
+ async function finalizeCompletedRun(
441
+ run: ActiveRun,
442
+ graphId: string,
443
+ ctx: MessageContext,
444
+ options?: ExecuteGraphLifecycleOptions
445
+ ): Promise<void> {
446
+ if (
447
+ run.executionPhase !== 'completed' ||
448
+ run.isPaused ||
449
+ !(options?.autoEnd ?? false)
450
+ ) {
451
+ return;
452
+ }
453
+
454
+ run.status = 'completed';
455
+ const elapsedMs = Date.now() - run.startedAt;
456
+
457
+ // Deliver any buffered trace events before `completed` , the client
458
+ // unregisters the run id on completion and would drop a late batch.
459
+ run.flushTracing?.();
460
+
461
+ ctx.sendMessage({
462
+ type: 'completed',
463
+ runId: run.runId,
464
+ graphId,
465
+ completedAt: Date.now(),
466
+ elapsedMs,
467
+ result: null,
468
+ performance: run.performance
469
+ });
470
+
471
+ run.engine.dispose();
472
+ await options?.onComplete?.();
473
+ }
474
+
475
+ /** Mark the run errored, notify, dispose the engine, and rethrow. */
476
+ async function handleLifecycleError(
477
+ run: ActiveRun,
478
+ graphId: string,
479
+ ctx: MessageContext,
480
+ error: unknown,
481
+ options?: ExecuteGraphLifecycleOptions
482
+ ): Promise<never> {
483
+ run.status = 'error';
484
+ run.flushTracing?.();
485
+ const err = error instanceof Error ? error : new Error(String(error));
486
+ if (options?.onError) {
487
+ await options.onError(err);
488
+ } else {
489
+ ctx.sendError('NODE_EXECUTION_ERROR', err.message, {
345
490
  runId: run.runId,
346
491
  graphId
347
492
  });
348
- run.engine.dispose();
349
- throw error;
493
+ }
494
+ run.engine.dispose();
495
+ throw error;
496
+ }
497
+
498
+ export async function executeGraphLifecycle(
499
+ run: ActiveRun,
500
+ graphId: string,
501
+ ctx: MessageContext,
502
+ options?: ExecuteGraphLifecycleOptions
503
+ ): Promise<void> {
504
+ const executeStep =
505
+ options?.executeStep ?? (() => run.engine.executeAllAsync());
506
+ const tickStrategy =
507
+ options?.tickStrategy ??
508
+ (() => sleep((options?.tickInterval ?? 50) / 1000));
509
+
510
+ try {
511
+ const eventEmitter = run.registry.dependencies?.ILifecycleEventEmitter as
512
+ | ILifecycleEventEmitter
513
+ | undefined;
514
+
515
+ await runStartPhase(run, eventEmitter, executeStep);
516
+
517
+ const yielded = await runTickPhase(
518
+ run,
519
+ eventEmitter,
520
+ executeStep,
521
+ tickStrategy,
522
+ options
523
+ );
524
+ if (yielded) return;
525
+
526
+ await runEndPhase(run, eventEmitter, executeStep);
527
+ await finalizeCompletedRun(run, graphId, ctx, options);
528
+ } catch (error) {
529
+ await handleLifecycleError(run, graphId, ctx, error, options);
350
530
  }
351
531
  }
352
532
 
@@ -6,6 +6,7 @@
6
6
  import type { System } from '../../system/system.js';
7
7
  import { plugin } from '../../system/plugin.js';
8
8
  import type { IRegistry } from '@kiberon-labs/behave-graph';
9
+ import { buildUIGraphJSON } from '../../transformers/Uigraph.js';
9
10
  import { GraphRunnerClient } from '../graphrunner/client.js';
10
11
  import { LocalTransport } from './transport.js';
11
12
  import {
@@ -86,11 +87,16 @@ export async function localGraphRunnerPluginLoader(
86
87
  tickStrategy
87
88
  });
88
89
 
89
- // Create local transport with access to the node registry and store
90
+ // Create local transport with access to the node registry and store.
91
+ // `resolveGraph` lets Call Subgraph nodes run other open graphs by id.
90
92
  const transport = new LocalTransport(options.registry, {
91
93
  ...options,
92
94
  store: localStore,
93
- sessionFactory
95
+ sessionFactory,
96
+ resolveGraph: (id) => {
97
+ const target = system.activeGraph.getState().sessions[id];
98
+ return target ? buildUIGraphJSON(target).flow : undefined;
99
+ }
94
100
  });
95
101
 
96
102
  // Create client with the local transport and message activity tracking