@codemation/core 0.0.18 → 0.2.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 (82) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/EngineRuntimeRegistration.types-0sgV2XL2.d.ts +42 -0
  3. package/dist/EngineWorkflowRunnerService-Dx7bJsJR.d.cts +73 -0
  4. package/dist/InMemoryRunDataFactory-qIYQEar7.d.cts +94 -0
  5. package/dist/{InMemoryLiveWorkflowRepository-DxoualoC.d.ts → RunIntentService-BCvGdOSY.d.ts} +438 -9
  6. package/dist/{RunIntentService-BB4nqX3-.js → RunIntentService-BFA48UpH.js} +308 -71
  7. package/dist/RunIntentService-BFA48UpH.js.map +1 -0
  8. package/dist/{InMemoryLiveWorkflowRepository-orY1VsWG.d.cts → RunIntentService-CV8izV8t.d.cts} +214 -7
  9. package/dist/{RunIntentService-nRx-m0Xs.cjs → RunIntentService-DcxXf_AM.cjs} +318 -69
  10. package/dist/RunIntentService-DcxXf_AM.cjs.map +1 -0
  11. package/dist/bootstrap/index.cjs +14 -1135
  12. package/dist/bootstrap/index.d.cts +7 -60
  13. package/dist/bootstrap/index.d.ts +4 -40
  14. package/dist/bootstrap/index.js +3 -1122
  15. package/dist/bootstrap-D67Sf2BF.js +1136 -0
  16. package/dist/bootstrap-D67Sf2BF.js.map +1 -0
  17. package/dist/bootstrap-DoQHAEQJ.cjs +1203 -0
  18. package/dist/bootstrap-DoQHAEQJ.cjs.map +1 -0
  19. package/dist/{index-B4_ZRTyI.d.ts → index-BHmrZIHp.d.ts} +32 -251
  20. package/dist/index.cjs +98 -223
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +196 -6
  23. package/dist/index.d.ts +3 -3
  24. package/dist/index.js +92 -218
  25. package/dist/index.js.map +1 -1
  26. package/dist/testing.cjs +329 -3
  27. package/dist/testing.cjs.map +1 -1
  28. package/dist/testing.d.cts +181 -4
  29. package/dist/testing.d.ts +181 -3
  30. package/dist/testing.js +319 -2
  31. package/dist/testing.js.map +1 -1
  32. package/dist/workflowActivationPolicy-B8HzTk3o.js +201 -0
  33. package/dist/workflowActivationPolicy-B8HzTk3o.js.map +1 -0
  34. package/dist/workflowActivationPolicy-BzyzXLa_.cjs +231 -0
  35. package/dist/workflowActivationPolicy-BzyzXLa_.cjs.map +1 -0
  36. package/package.json +1 -1
  37. package/src/ai/AgentConnectionNodeCollector.ts +99 -0
  38. package/src/ai/AgentToolFactory.ts +38 -2
  39. package/src/ai/AiHost.ts +1 -1
  40. package/src/browser.ts +11 -0
  41. package/src/contracts/executionPersistenceContracts.ts +186 -0
  42. package/src/contracts/index.ts +1 -0
  43. package/src/contracts/runFinishedAtFactory.ts +5 -2
  44. package/src/contracts/runTypes.ts +10 -0
  45. package/src/contracts/runtimeTypes.ts +6 -2
  46. package/src/contracts/workflowTypes.ts +3 -2
  47. package/src/events/EventPublishingWorkflowExecutionRepository.ts +5 -0
  48. package/src/execution/ActivationEnqueueService.ts +8 -8
  49. package/src/execution/PersistedRunStateTerminalBuilder.ts +3 -0
  50. package/src/index.ts +6 -0
  51. package/src/orchestration/NodeExecutionRequestHandlerService.ts +11 -6
  52. package/src/orchestration/RunContinuationService.ts +94 -24
  53. package/src/planning/CurrentStateFrontierPlanner.ts +24 -1
  54. package/src/runStorage/InMemoryWorkflowExecutionRepository.ts +14 -1
  55. package/src/runtime/RunIntentService.ts +68 -14
  56. package/src/scheduler/DefaultDrivingScheduler.ts +21 -11
  57. package/src/scheduler/InlineDrivingScheduler.ts +17 -21
  58. package/src/testing/CapturingScheduler.ts +15 -0
  59. package/src/testing/EngineTestKitRunIdFactory.ts +24 -0
  60. package/src/testing/InMemoryTriggerSetupStateRepository.ts +21 -0
  61. package/src/testing/PrefixedSequentialIdGenerator.ts +17 -0
  62. package/src/testing/RegistrarEngineTestKit.types.ts +76 -0
  63. package/src/testing/RegistrarEngineTestKitFactory.ts +154 -0
  64. package/src/testing/SubWorkflowRunnerTestNode.ts +83 -0
  65. package/src/testing/WorkflowTestHarnessManualTrigger.ts +39 -0
  66. package/src/testing/WorkflowTestKit.types.ts +9 -0
  67. package/src/testing/WorkflowTestKitBuilder.ts +77 -0
  68. package/src/testing/WorkflowTestKitNodeRegistrationContextFactory.ts +17 -0
  69. package/src/testing/WorkflowTestKitRunNodeWorkflowFactory.ts +26 -0
  70. package/src/testing.ts +19 -0
  71. package/src/types/index.ts +1 -0
  72. package/src/workflow/definition/ConnectionNodeIdFactory.ts +28 -0
  73. package/dist/InMemoryLiveWorkflowRepository-BTzHpQ6e.cjs +0 -151
  74. package/dist/InMemoryLiveWorkflowRepository-BTzHpQ6e.cjs.map +0 -1
  75. package/dist/InMemoryLiveWorkflowRepository-BoLNnVLg.js +0 -139
  76. package/dist/InMemoryLiveWorkflowRepository-BoLNnVLg.js.map +0 -1
  77. package/dist/RunIntentService-BB4nqX3-.js.map +0 -1
  78. package/dist/RunIntentService-ByuUYsAL.d.cts +0 -279
  79. package/dist/RunIntentService-nRx-m0Xs.cjs.map +0 -1
  80. package/dist/WorkflowSnapshotCodec-DSEzKyt3.d.cts +0 -22
  81. package/dist/bootstrap/index.cjs.map +0 -1
  82. package/dist/bootstrap/index.js.map +0 -1
@@ -5,6 +5,8 @@ import type {
5
5
  NodeId,
6
6
  NodeInputsByPort,
7
7
  NodeOutputs,
8
+ PendingNodeExecution,
9
+ PersistedRunSchedulingState,
8
10
  PersistedRunState,
9
11
  RunDataFactory,
10
12
  RunId,
@@ -31,7 +33,6 @@ import { NodeEventPublisher } from "../events/NodeEventPublisher";
31
33
 
32
34
  import { ActivationEnqueueService } from "../execution/ActivationEnqueueService";
33
35
  import { NodeExecutionSnapshotFactory } from "../execution/NodeExecutionSnapshotFactory";
34
- import { NodeInputsByPortFactory } from "../execution/NodeInputsByPortFactory";
35
36
  import { NodeRunStateWriterFactory } from "../execution/NodeRunStateWriterFactory";
36
37
  import { NodeActivationRequestComposer } from "../execution/NodeActivationRequestComposer";
37
38
  import { PersistedRunStateTerminalBuilder } from "../execution/PersistedRunStateTerminalBuilder";
@@ -65,9 +66,13 @@ export class RunContinuationService {
65
66
  nodeId: NodeId;
66
67
  inputsByPort: NodeInputsByPort;
67
68
  }): Promise<void> {
68
- const state = await this.workflowExecutionRepository.load(args.runId);
69
- if (!state?.pending) return;
70
- if (state.pending.activationId !== args.activationId || state.pending.nodeId !== args.nodeId) return;
69
+ const [state, schedulingState] = await Promise.all([
70
+ this.workflowExecutionRepository.load(args.runId),
71
+ this.workflowExecutionRepository.loadSchedulingState(args.runId),
72
+ ]);
73
+ const pendingExecution = schedulingState?.pending;
74
+ if (!state || !pendingExecution) return;
75
+ if (pendingExecution.activationId !== args.activationId || pendingExecution.nodeId !== args.nodeId) return;
71
76
 
72
77
  const startedAt = new Date().toISOString();
73
78
  const previous = state.nodeSnapshotsByNodeId?.[args.nodeId];
@@ -98,12 +103,21 @@ export class RunContinuationService {
98
103
  nodeId: NodeId;
99
104
  outputs: NodeOutputs;
100
105
  }): Promise<RunResult> {
101
- const state = await this.workflowExecutionRepository.load(args.runId);
106
+ const [state, schedulingState] = await Promise.all([
107
+ this.workflowExecutionRepository.load(args.runId),
108
+ this.workflowExecutionRepository.loadSchedulingState(args.runId),
109
+ ]);
102
110
  if (!state) throw new Error(`Unknown runId: ${args.runId}`);
103
- if (state.status !== "pending" || !state.pending) throw new Error(`Run ${args.runId} is not pending`);
104
- if (state.pending.activationId !== args.activationId)
111
+ const pendingExecution = this.requirePendingExecution(
112
+ args.runId,
113
+ args.activationId,
114
+ args.nodeId,
115
+ state,
116
+ schedulingState,
117
+ );
118
+ if (pendingExecution.activationId !== args.activationId)
105
119
  throw new Error(`activationId mismatch for run ${args.runId}`);
106
- if (state.pending.nodeId !== args.nodeId) throw new Error(`nodeId mismatch for run ${args.runId}`);
120
+ if (pendingExecution.nodeId !== args.nodeId) throw new Error(`nodeId mismatch for run ${args.runId}`);
107
121
 
108
122
  const wf = this.resolvePersistedWorkflow(state);
109
123
  if (!wf) throw new Error(`Unknown workflowId: ${state.workflowId}`);
@@ -135,7 +149,7 @@ export class RunContinuationService {
135
149
  activationId: args.activationId,
136
150
  parent: state.parent,
137
151
  finishedAt: completedAt,
138
- inputsByPort: state.pending.inputsByPort,
152
+ inputsByPort: pendingExecution.inputsByPort,
139
153
  outputs: args.outputs,
140
154
  });
141
155
 
@@ -154,6 +168,7 @@ export class RunContinuationService {
154
168
  ...(state.nodeSnapshotsByNodeId ?? {}),
155
169
  [args.nodeId]: completedSnapshot,
156
170
  },
171
+ finishedAtIso: completedAt,
157
172
  });
158
173
  await this.workflowExecutionRepository.save(completedState);
159
174
  await this.nodeEventPublisher.publish("nodeCompleted", completedSnapshot);
@@ -174,8 +189,8 @@ export class RunContinuationService {
174
189
  return result;
175
190
  }
176
191
 
177
- const batchId = state.pending.batchId ?? "batch_1";
178
- const queue: RunQueueEntry[] = (state.queue ?? []).map((q) => ({ ...q, batchId: q.batchId ?? batchId }));
192
+ const batchId = pendingExecution.batchId ?? "batch_1";
193
+ const queue: RunQueueEntry[] = (schedulingState?.queue ?? []).map((q) => ({ ...q, batchId: q.batchId ?? batchId }));
179
194
  const nextNodeSnapshotsByNodeId = {
180
195
  ...(state.nodeSnapshotsByNodeId ?? {}),
181
196
  [args.nodeId]: completedSnapshot,
@@ -220,6 +235,7 @@ export class RunContinuationService {
220
235
  queue: [],
221
236
  outputsByNode: data.dump(),
222
237
  nodeSnapshotsByNodeId: nextNodeSnapshotsByNodeId,
238
+ finishedAtIso: completedAt,
223
239
  });
224
240
  await this.workflowExecutionRepository.save(completedState);
225
241
  await this.nodeEventPublisher.publish("nodeCompleted", completedSnapshot);
@@ -250,6 +266,7 @@ export class RunContinuationService {
250
266
  queue: queue.map((q) => ({ ...q })),
251
267
  outputsByNode: data.dump(),
252
268
  nodeSnapshotsByNodeId: nextNodeSnapshotsByNodeId,
269
+ finishedAtIso: completedAt,
253
270
  });
254
271
  await this.workflowExecutionRepository.save(failedState);
255
272
  await this.nodeEventPublisher.publish("nodeCompleted", completedSnapshot);
@@ -312,12 +329,21 @@ export class RunContinuationService {
312
329
  nodeId: NodeId;
313
330
  error: Error;
314
331
  }): Promise<RunResult> {
315
- const state = await this.workflowExecutionRepository.load(args.runId);
332
+ const [state, schedulingState] = await Promise.all([
333
+ this.workflowExecutionRepository.load(args.runId),
334
+ this.workflowExecutionRepository.loadSchedulingState(args.runId),
335
+ ]);
316
336
  if (!state) throw new Error(`Unknown runId: ${args.runId}`);
317
- if (state.status !== "pending" || !state.pending) throw new Error(`Run ${args.runId} is not pending`);
318
- if (state.pending.activationId !== args.activationId)
337
+ const pendingExecution = this.requirePendingExecution(
338
+ args.runId,
339
+ args.activationId,
340
+ args.nodeId,
341
+ state,
342
+ schedulingState,
343
+ );
344
+ if (pendingExecution.activationId !== args.activationId)
319
345
  throw new Error(`activationId mismatch for run ${args.runId}`);
320
- if (state.pending.nodeId !== args.nodeId) throw new Error(`nodeId mismatch for run ${args.runId}`);
346
+ if (pendingExecution.nodeId !== args.nodeId) throw new Error(`nodeId mismatch for run ${args.runId}`);
321
347
 
322
348
  const wf = this.resolvePersistedWorkflow(state);
323
349
  if (!wf) throw new Error(`Unknown workflowId: ${state.workflowId}`);
@@ -327,15 +353,28 @@ export class RunContinuationService {
327
353
  ? this.asWebhookControlSignal(args.error)
328
354
  : undefined;
329
355
  if (webhookControlSignal) {
330
- return await this.resumeFromWebhookControl({ state, workflow: wf, args, signal: webhookControlSignal });
356
+ return await this.resumeFromWebhookControl({
357
+ state,
358
+ schedulingState,
359
+ pendingExecution,
360
+ workflow: wf,
361
+ args,
362
+ signal: webhookControlSignal,
363
+ });
331
364
  }
332
365
 
333
366
  if (failedDefinition && failedDefinition.kind === "node") {
334
367
  const nodeHandler = this.policyErrorServices.resolveNodeErrorHandler(failedDefinition.config.nodeErrorHandler);
335
368
  if (nodeHandler) {
336
369
  try {
337
- const ctx = this.buildNodeExecutionContextForPending(state, wf, failedDefinition, args.nodeId);
338
- const inputsByPort = state.pending.inputsByPort;
370
+ const ctx = this.buildNodeExecutionContextForPending(
371
+ state,
372
+ pendingExecution,
373
+ wf,
374
+ failedDefinition,
375
+ args.nodeId,
376
+ );
377
+ const inputsByPort = pendingExecution.inputsByPort;
339
378
  const portKeys = Object.keys(inputsByPort);
340
379
  const kind = portKeys.length === 1 && portKeys[0] === "in" ? ("single" as const) : ("multi" as const);
341
380
  const items = inputsByPort.in ?? [];
@@ -368,19 +407,20 @@ export class RunContinuationService {
368
407
  activationId: args.activationId,
369
408
  parent: state.parent,
370
409
  finishedAt,
371
- inputsByPort: state.pending.inputsByPort,
410
+ inputsByPort: pendingExecution.inputsByPort,
372
411
  error: args.error,
373
412
  });
374
413
  const failedState = this.persistedRunStateTerminalBuilder.mergeTerminal({
375
414
  state,
376
415
  engineCounters: state.engineCounters ?? { completedNodeActivations: 0 },
377
416
  status: "failed",
378
- queue: (state.queue ?? []).map((q) => ({ ...q })),
417
+ queue: (schedulingState?.queue ?? []).map((q) => ({ ...q })),
379
418
  outputsByNode: state.outputsByNode,
380
419
  nodeSnapshotsByNodeId: {
381
420
  ...(state.nodeSnapshotsByNodeId ?? {}),
382
421
  [args.nodeId]: failedSnapshot,
383
422
  },
423
+ finishedAtIso: finishedAt,
384
424
  });
385
425
  await this.workflowExecutionRepository.save(failedState);
386
426
  await this.nodeEventPublisher.publish("nodeFailed", failedSnapshot);
@@ -474,6 +514,8 @@ export class RunContinuationService {
474
514
 
475
515
  private async resumeFromWebhookControl(args: {
476
516
  state: NonNullable<Awaited<ReturnType<WorkflowExecutionRepository["load"]>>>;
517
+ schedulingState: PersistedRunSchedulingState | undefined;
518
+ pendingExecution: PendingNodeExecution;
477
519
  workflow: WorkflowDefinition;
478
520
  args: { runId: RunId; activationId: NodeActivationId; nodeId: NodeId; error: Error };
479
521
  signal: WebhookControlSignal;
@@ -495,7 +537,7 @@ export class RunContinuationService {
495
537
  activationId: args.args.activationId,
496
538
  parent: args.state.parent,
497
539
  finishedAt: new Date().toISOString(),
498
- inputsByPort: args.state.pending?.inputsByPort ?? NodeInputsByPortFactory.empty(),
540
+ inputsByPort: args.pendingExecution.inputsByPort,
499
541
  outputs: triggerOutputs,
500
542
  });
501
543
 
@@ -514,6 +556,7 @@ export class RunContinuationService {
514
556
  ...(args.state.nodeSnapshotsByNodeId ?? {}),
515
557
  [args.args.nodeId]: completedSnapshot,
516
558
  },
559
+ finishedAtIso: completedSnapshot.finishedAt ?? completedSnapshot.updatedAt,
517
560
  });
518
561
  await this.workflowExecutionRepository.save(completedState);
519
562
  await this.nodeEventPublisher.publish("nodeCompleted", completedSnapshot);
@@ -552,6 +595,7 @@ export class RunContinuationService {
552
595
  ...(args.state.nodeSnapshotsByNodeId ?? {}),
553
596
  [args.args.nodeId]: completedSnapshot,
554
597
  },
598
+ finishedAtIso: completedSnapshot.finishedAt ?? completedSnapshot.updatedAt,
555
599
  });
556
600
  await this.workflowExecutionRepository.save(completedState);
557
601
  await this.nodeEventPublisher.publish("nodeCompleted", completedSnapshot);
@@ -580,8 +624,8 @@ export class RunContinuationService {
580
624
  return result;
581
625
  }
582
626
 
583
- const batchId = args.state.pending?.batchId ?? "batch_1";
584
- const queue: RunQueueEntry[] = (args.state.queue ?? []).map((entry) => ({
627
+ const batchId = args.pendingExecution.batchId ?? "batch_1";
628
+ const queue: RunQueueEntry[] = (args.schedulingState?.queue ?? []).map((entry) => ({
585
629
  ...entry,
586
630
  batchId: entry.batchId ?? batchId,
587
631
  }));
@@ -603,6 +647,7 @@ export class RunContinuationService {
603
647
  ...(args.state.nodeSnapshotsByNodeId ?? {}),
604
648
  [args.args.nodeId]: completedSnapshot,
605
649
  },
650
+ finishedAtIso: completedSnapshot.finishedAt ?? completedSnapshot.updatedAt,
606
651
  });
607
652
  await this.workflowExecutionRepository.save(completedState);
608
653
  await this.nodeEventPublisher.publish("nodeCompleted", completedSnapshot);
@@ -643,6 +688,7 @@ export class RunContinuationService {
643
688
  ...(args.state.nodeSnapshotsByNodeId ?? {}),
644
689
  [args.args.nodeId]: completedSnapshot,
645
690
  },
691
+ finishedAtIso: completedSnapshot.finishedAt ?? completedSnapshot.updatedAt,
646
692
  });
647
693
  await this.workflowExecutionRepository.save(failedState);
648
694
  await this.nodeEventPublisher.publish("nodeCompleted", completedSnapshot);
@@ -750,6 +796,7 @@ export class RunContinuationService {
750
796
 
751
797
  private buildNodeExecutionContextForPending(
752
798
  state: NonNullable<Awaited<ReturnType<WorkflowExecutionRepository["load"]>>>,
799
+ pendingExecution: PendingNodeExecution,
753
800
  wf: WorkflowDefinition,
754
801
  def: Readonly<{ id: NodeId; config: NodeExecutionContext["config"] }>,
755
802
  nodeId: NodeId,
@@ -767,7 +814,7 @@ export class RunContinuationService {
767
814
  data,
768
815
  nodeState: this.nodeStatePublisherFactory.create(state.runId, state.workflowId, state.parent),
769
816
  });
770
- const activationId = state.pending!.activationId;
817
+ const activationId = pendingExecution.activationId;
771
818
  return {
772
819
  ...base,
773
820
  data,
@@ -779,6 +826,29 @@ export class RunContinuationService {
779
826
  };
780
827
  }
781
828
 
829
+ private requirePendingExecution(
830
+ runId: RunId,
831
+ activationId: NodeActivationId,
832
+ nodeId: NodeId,
833
+ state: PersistedRunState,
834
+ schedulingState?: PersistedRunSchedulingState,
835
+ ): PendingNodeExecution {
836
+ if (state.status !== "pending") {
837
+ throw new Error(`Run ${runId} is not pending`);
838
+ }
839
+ const pendingExecution = schedulingState?.pending;
840
+ if (!pendingExecution) {
841
+ throw new Error(`Run ${runId} is not pending`);
842
+ }
843
+ if (pendingExecution.activationId !== activationId) {
844
+ throw new Error(`activationId mismatch for run ${runId}`);
845
+ }
846
+ if (pendingExecution.nodeId !== nodeId) {
847
+ throw new Error(`nodeId mismatch for run ${runId}`);
848
+ }
849
+ return pendingExecution;
850
+ }
851
+
782
852
  private resolveEngineLimitsFromState(state: PersistedRunState): {
783
853
  engineMaxNodeActivations: number;
784
854
  engineMaxSubworkflowDepth: number;
@@ -303,7 +303,17 @@ export class CurrentStateFrontierPlanner {
303
303
  if (!incomingEdge) {
304
304
  return false;
305
305
  }
306
- return this.hasOutputPort(currentState, incomingEdge.from.nodeId, incomingEdge.from.output);
306
+ if (!this.hasOutputPort(currentState, incomingEdge.from.nodeId, incomingEdge.from.output)) {
307
+ return false;
308
+ }
309
+ if (this.usesCollect(nodeId)) {
310
+ return true;
311
+ }
312
+ const items = this.resolveOutputItems(currentState, incomingEdge.from.nodeId, incomingEdge.from.output);
313
+ if (items.length > 0) {
314
+ return true;
315
+ }
316
+ return this.shouldContinueAfterEmptyOutputFromSource(incomingEdge.from.nodeId);
307
317
  }
308
318
 
309
319
  private resolveInput(currentState: RunCurrentState, nodeId: NodeId, input: InputPortKey): Items {
@@ -335,6 +345,19 @@ export class CurrentStateFrontierPlanner {
335
345
  return currentState.outputsByNode[nodeId]?.[output] ?? [];
336
346
  }
337
347
 
348
+ private usesCollect(nodeId: NodeId): boolean {
349
+ const expectedInputs = this.topology.expectedInputsByNode.get(nodeId) ?? [];
350
+ return expectedInputs.length !== 1 || expectedInputs[0] !== "in";
351
+ }
352
+
353
+ private shouldContinueAfterEmptyOutputFromSource(nodeId: NodeId): boolean {
354
+ const definition = this.topology.defsById.get(nodeId);
355
+ if (!definition) {
356
+ return false;
357
+ }
358
+ return definition.config.continueWhenEmptyOutput === true;
359
+ }
360
+
338
361
  private getPinnedOutputs(currentState: RunCurrentState, nodeId: NodeId): NodeOutputs | undefined {
339
362
  return currentState.mutableState?.nodesById?.[nodeId]?.pinnedOutputsByPort;
340
363
  }
@@ -3,6 +3,7 @@ import type {
3
3
  NodeId,
4
4
  NodeOutputs,
5
5
  ParentExecutionRef,
6
+ PersistedRunSchedulingState,
6
7
  PersistedRunState,
7
8
  RunId,
8
9
  RunSummary,
@@ -36,6 +37,7 @@ export class InMemoryWorkflowExecutionRepository
36
37
  runId: args.runId,
37
38
  workflowId: args.workflowId,
38
39
  startedAt: args.startedAt,
40
+ revision: 0,
39
41
  parent: args.parent,
40
42
  executionOptions: args.executionOptions,
41
43
  control: args.control,
@@ -55,8 +57,19 @@ export class InMemoryWorkflowExecutionRepository
55
57
  return this.runs.get(runId);
56
58
  }
57
59
 
60
+ async loadSchedulingState(runId: RunId): Promise<PersistedRunSchedulingState | undefined> {
61
+ const state = this.runs.get(runId);
62
+ if (!state) {
63
+ return undefined;
64
+ }
65
+ return {
66
+ pending: state.pending ? { ...state.pending } : undefined,
67
+ queue: state.queue.map((entry) => ({ ...entry })),
68
+ };
69
+ }
70
+
58
71
  async save(state: PersistedRunState): Promise<void> {
59
- this.runs.set(state.runId, state);
72
+ this.runs.set(state.runId, { ...state, revision: (state.revision ?? 0) + 1 });
60
73
  }
61
74
 
62
75
  async deleteRun(runId: RunId): Promise<void> {
@@ -20,6 +20,7 @@ export type StartWorkflowIntent = {
20
20
  workflow: WorkflowDefinition;
21
21
  startAt?: string;
22
22
  items: Items;
23
+ synthesizeTriggerItems?: boolean;
23
24
  parent?: CurrentStateExecutionRequest["parent"];
24
25
  executionOptions?: RunExecutionOptions;
25
26
  workflowSnapshot?: CurrentStateExecutionRequest["workflowSnapshot"];
@@ -34,6 +35,7 @@ export type RerunFromNodeIntent = {
34
35
  nodeId: NodeId;
35
36
  currentState: RunCurrentState;
36
37
  items?: Items;
38
+ synthesizeTriggerItems?: boolean;
37
39
  parent?: CurrentStateExecutionRequest["parent"];
38
40
  executionOptions?: RunExecutionOptions;
39
41
  workflowSnapshot?: CurrentStateExecutionRequest["workflowSnapshot"];
@@ -58,22 +60,16 @@ export class RunIntentService {
58
60
  ) {}
59
61
 
60
62
  async startWorkflow(args: StartWorkflowIntent): Promise<RunResult> {
63
+ const items = await this.resolveStartWorkflowItems(args);
61
64
  if (args.startAt && !args.currentState && !args.stopCondition && !args.reset) {
62
- return await this.engine.runWorkflow(
63
- args.workflow,
64
- args.startAt,
65
- args.items,
66
- args.parent,
67
- args.executionOptions,
68
- {
69
- workflowSnapshot: args.workflowSnapshot,
70
- mutableState: args.mutableState,
71
- },
72
- );
65
+ return await this.engine.runWorkflow(args.workflow, args.startAt, items, args.parent, args.executionOptions, {
66
+ workflowSnapshot: args.workflowSnapshot,
67
+ mutableState: args.mutableState,
68
+ });
73
69
  }
74
70
  return await this.engine.runWorkflowFromState({
75
71
  workflow: args.workflow,
76
- items: args.items,
72
+ items,
77
73
  parent: args.parent,
78
74
  executionOptions: args.executionOptions,
79
75
  workflowSnapshot: args.workflowSnapshot,
@@ -85,8 +81,9 @@ export class RunIntentService {
85
81
  }
86
82
 
87
83
  async rerunFromNode(args: RerunFromNodeIntent): Promise<RunResult> {
88
- if (args.items) {
89
- return await this.engine.runWorkflow(args.workflow, args.nodeId, args.items, args.parent, args.executionOptions, {
84
+ const items = await this.resolveRerunItems(args);
85
+ if (items) {
86
+ return await this.engine.runWorkflow(args.workflow, args.nodeId, items, args.parent, args.executionOptions, {
90
87
  workflowSnapshot: args.workflowSnapshot,
91
88
  mutableState: args.mutableState,
92
89
  });
@@ -103,6 +100,63 @@ export class RunIntentService {
103
100
  });
104
101
  }
105
102
 
103
+ private async resolveStartWorkflowItems(args: StartWorkflowIntent): Promise<Items> {
104
+ if (this.hasNonEmptyItems(args.items)) {
105
+ return args.items;
106
+ }
107
+ const triggerNodeId = this.resolveStartWorkflowTriggerNodeId(args);
108
+ if (!triggerNodeId) {
109
+ return args.items;
110
+ }
111
+ return (await this.engine.createTriggerTestItems({ workflow: args.workflow, nodeId: triggerNodeId })) ?? args.items;
112
+ }
113
+
114
+ private async resolveRerunItems(args: RerunFromNodeIntent): Promise<Items | undefined> {
115
+ if (this.hasNonEmptyItems(args.items)) {
116
+ return args.items;
117
+ }
118
+ const triggerNodeId = this.resolveRerunTriggerNodeId(args);
119
+ if (!triggerNodeId) {
120
+ return args.items;
121
+ }
122
+ return (await this.engine.createTriggerTestItems({ workflow: args.workflow, nodeId: triggerNodeId })) ?? args.items;
123
+ }
124
+
125
+ private resolveStartWorkflowTriggerNodeId(args: StartWorkflowIntent): NodeId | undefined {
126
+ if (args.stopCondition?.kind === "nodeCompleted" && this.isTriggerNode(args.workflow, args.stopCondition.nodeId)) {
127
+ return args.stopCondition.nodeId;
128
+ }
129
+ if (!args.synthesizeTriggerItems) {
130
+ return undefined;
131
+ }
132
+ if (args.startAt && this.isTriggerNode(args.workflow, args.startAt)) {
133
+ return args.startAt;
134
+ }
135
+ return this.firstTriggerNodeId(args.workflow);
136
+ }
137
+
138
+ private resolveRerunTriggerNodeId(args: RerunFromNodeIntent): NodeId | undefined {
139
+ if (this.isTriggerNode(args.workflow, args.nodeId)) {
140
+ return args.nodeId;
141
+ }
142
+ if (!args.synthesizeTriggerItems) {
143
+ return undefined;
144
+ }
145
+ return this.firstTriggerNodeId(args.workflow);
146
+ }
147
+
148
+ private firstTriggerNodeId(workflow: WorkflowDefinition): NodeId | undefined {
149
+ return workflow.nodes.find((node) => node.kind === "trigger")?.id;
150
+ }
151
+
152
+ private isTriggerNode(workflow: WorkflowDefinition, nodeId: string): boolean {
153
+ return workflow.nodes.find((node) => node.id === nodeId)?.kind === "trigger";
154
+ }
155
+
156
+ private hasNonEmptyItems(items: Items | undefined): boolean {
157
+ return (items?.length ?? 0) > 0;
158
+ }
159
+
106
160
  resolveWebhookTrigger(args: { endpointPath: string; method: HttpMethod }): WebhookTriggerResolution {
107
161
  return this.engine.resolveWebhookTrigger(args);
108
162
  }
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  NodeActivationContinuation,
3
+ PreparedNodeActivationDispatch,
3
4
  NodeActivationReceipt,
4
5
  NodeActivationRequest,
5
6
  NodeActivationScheduler,
@@ -26,7 +27,7 @@ export class DefaultDrivingScheduler implements NodeActivationScheduler {
26
27
  this.inline.setContinuation(continuation);
27
28
  }
28
29
 
29
- async enqueue(request: NodeActivationRequest): Promise<NodeActivationReceipt> {
30
+ async prepareDispatch(request: NodeActivationRequest): Promise<PreparedNodeActivationDispatch> {
30
31
  const selection = await this.selectScheduler(request);
31
32
  if (selection.mode === "worker") {
32
33
  if (request.kind === "multi") {
@@ -44,15 +45,19 @@ export class DefaultDrivingScheduler implements NodeActivationScheduler {
44
45
  executionOptions: request.executionOptions,
45
46
  };
46
47
 
47
- const receipt = await this.workerScheduler.enqueue(workerRequest);
48
- return { receiptId: receipt.receiptId, mode: "worker", queue: selection.queue };
48
+ return {
49
+ receipt: {
50
+ receiptId: request.activationId,
51
+ mode: "worker",
52
+ queue: selection.queue,
53
+ },
54
+ dispatch: async () => {
55
+ await this.workerScheduler.enqueue(workerRequest);
56
+ },
57
+ };
49
58
  }
50
59
 
51
- return await this.enqueueInline(request);
52
- }
53
-
54
- notifyPendingStatePersisted(runId: string): void {
55
- this.inline.notifyPendingStatePersisted(runId);
60
+ return await this.prepareInlineDispatch(request);
56
61
  }
57
62
 
58
63
  /**
@@ -93,8 +98,13 @@ export class DefaultDrivingScheduler implements NodeActivationScheduler {
93
98
  return request.ctx.config.execution?.hint !== undefined || request.ctx.config.execution?.queue !== undefined;
94
99
  }
95
100
 
96
- private async enqueueInline(request: NodeActivationRequest): Promise<NodeActivationReceipt> {
97
- const receipt = await this.inline.enqueue(request);
98
- return { ...receipt, mode: "local" };
101
+ private async prepareInlineDispatch(request: NodeActivationRequest): Promise<PreparedNodeActivationDispatch> {
102
+ const prepared = await this.inline.prepareDispatch(request);
103
+ return {
104
+ receipt: { ...prepared.receipt, mode: "local" },
105
+ dispatch: async () => {
106
+ await prepared.dispatch();
107
+ },
108
+ };
99
109
  }
100
110
  }
@@ -1,21 +1,20 @@
1
1
  import type {
2
2
  NodeActivationContinuation,
3
+ PreparedNodeActivationDispatch,
3
4
  NodeActivationReceipt,
4
5
  NodeActivationRequest,
5
6
  NodeActivationScheduler,
6
- RunId,
7
7
  } from "../types";
8
8
  import { NodeExecutor } from "../execution/NodeExecutor";
9
9
 
10
10
  export class InlineDrivingScheduler implements NodeActivationScheduler {
11
11
  private continuation: NodeActivationContinuation | undefined;
12
- private readonly drainingRuns = new Set<RunId>();
12
+ private readonly drainingRuns = new Set<string>();
13
13
  private readonly queuesByRunId = new Map<
14
- RunId,
14
+ string,
15
15
  Array<Readonly<{ request: NodeActivationRequest; receipt: NodeActivationReceipt }>>
16
16
  >();
17
- private readonly scheduledRuns = new Set<RunId>();
18
- private seq = 0;
17
+ private readonly scheduledRuns = new Set<string>();
19
18
 
20
19
  constructor(private readonly nodeExecutor: NodeExecutor) {}
21
20
 
@@ -23,23 +22,20 @@ export class InlineDrivingScheduler implements NodeActivationScheduler {
23
22
  this.continuation = continuation;
24
23
  }
25
24
 
26
- async enqueue(request: NodeActivationRequest): Promise<NodeActivationReceipt> {
27
- const receipt: NodeActivationReceipt = { receiptId: `inline_${++this.seq}`, mode: "local" };
28
- const q = this.queuesByRunId.get(request.runId) ?? [];
29
- q.push({ request, receipt });
30
- this.queuesByRunId.set(request.runId, q);
31
-
32
- return receipt;
33
- }
34
-
35
- notifyPendingStatePersisted(runId: RunId): void {
36
- if ((this.queuesByRunId.get(runId)?.length ?? 0) === 0) {
37
- return;
38
- }
39
- this.scheduleDrain(runId);
25
+ async prepareDispatch(request: NodeActivationRequest): Promise<PreparedNodeActivationDispatch> {
26
+ const receipt: NodeActivationReceipt = { receiptId: request.activationId, mode: "local" };
27
+ return {
28
+ receipt,
29
+ dispatch: async () => {
30
+ const queue = this.queuesByRunId.get(request.runId) ?? [];
31
+ queue.push({ request, receipt });
32
+ this.queuesByRunId.set(request.runId, queue);
33
+ this.scheduleDrain(request.runId);
34
+ },
35
+ };
40
36
  }
41
37
 
42
- private async drainRun(runId: RunId): Promise<void> {
38
+ private async drainRun(runId: string): Promise<void> {
43
39
  if (this.drainingRuns.has(runId)) return;
44
40
  this.drainingRuns.add(runId);
45
41
  this.scheduledRuns.delete(runId);
@@ -78,7 +74,7 @@ export class InlineDrivingScheduler implements NodeActivationScheduler {
78
74
  }
79
75
  }
80
76
 
81
- private scheduleDrain(runId: RunId): void {
77
+ private scheduleDrain(runId: string): void {
82
78
  if (this.drainingRuns.has(runId) || this.scheduledRuns.has(runId)) {
83
79
  return;
84
80
  }
@@ -0,0 +1,15 @@
1
+ import type { NodeExecutionRequest, NodeExecutionScheduler } from "../types";
2
+
3
+ /**
4
+ * Test scheduler that records enqueue requests without executing a real queue.
5
+ */
6
+ export class CapturingScheduler implements NodeExecutionScheduler {
7
+ lastRequest: NodeExecutionRequest | undefined;
8
+ requests: NodeExecutionRequest[] = [];
9
+
10
+ async enqueue(request: NodeExecutionRequest): Promise<{ receiptId: string }> {
11
+ this.lastRequest = request;
12
+ this.requests.push(request);
13
+ return { receiptId: `receipt_${this.requests.length}` };
14
+ }
15
+ }
@@ -0,0 +1,24 @@
1
+ import type { RunIdFactory } from "../types";
2
+
3
+ /**
4
+ * @internal Test harness id factory shared by registrar kit wiring.
5
+ */
6
+ export class EngineTestKitRunIdFactory implements RunIdFactory {
7
+ private runCounter = 0;
8
+ private activationCounter = 0;
9
+
10
+ constructor(
11
+ private readonly makeRunIdValue: () => string,
12
+ private readonly makeActivationIdValue: () => string,
13
+ ) {}
14
+
15
+ makeRunId(): string {
16
+ this.runCounter += 1;
17
+ return this.makeRunIdValue();
18
+ }
19
+
20
+ makeActivationId(): string {
21
+ this.activationCounter += 1;
22
+ return this.makeActivationIdValue();
23
+ }
24
+ }
@@ -0,0 +1,21 @@
1
+ import type { PersistedTriggerSetupState, TriggerSetupStateRepository } from "../types";
2
+
3
+ export class InMemoryTriggerSetupStateRepository implements TriggerSetupStateRepository {
4
+ private readonly statesByKey = new Map<string, PersistedTriggerSetupState>();
5
+
6
+ async load(trigger: { workflowId: string; nodeId: string }): Promise<PersistedTriggerSetupState | undefined> {
7
+ return this.statesByKey.get(this.toKey(trigger));
8
+ }
9
+
10
+ async save(state: PersistedTriggerSetupState): Promise<void> {
11
+ this.statesByKey.set(this.toKey(state.trigger), state);
12
+ }
13
+
14
+ async delete(trigger: { workflowId: string; nodeId: string }): Promise<void> {
15
+ this.statesByKey.delete(this.toKey(trigger));
16
+ }
17
+
18
+ private toKey(trigger: { workflowId: string; nodeId: string }): string {
19
+ return `${trigger.workflowId}:${trigger.nodeId}`;
20
+ }
21
+ }