@codemation/core 0.8.1 → 0.10.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 (100) hide show
  1. package/CHANGELOG.md +380 -0
  2. package/dist/{EngineRuntimeRegistration.types-BP6tsaNP.d.ts → EngineRuntimeRegistration.types-D1fyApMI.d.ts} +2 -2
  3. package/dist/{EngineWorkflowRunnerService-DzOCa1BW.d.cts → EngineRuntimeRegistration.types-pB3FnzqR.d.cts} +17 -17
  4. package/dist/{InMemoryRunDataFactory-1iz7_SnO.d.cts → InMemoryRunDataFactory-Xw7v4-sj.d.cts} +31 -29
  5. package/dist/InMemoryRunEventBusRegistry-VM3OWnHo.cjs +47 -0
  6. package/dist/InMemoryRunEventBusRegistry-VM3OWnHo.cjs.map +1 -0
  7. package/dist/InMemoryRunEventBusRegistry-sM4z4n_i.js +41 -0
  8. package/dist/InMemoryRunEventBusRegistry-sM4z4n_i.js.map +1 -0
  9. package/dist/{RunIntentService-BqhmdoA1.d.ts → RunIntentService-BE9CAkbf.d.ts} +966 -471
  10. package/dist/{RunIntentService-S-1lW-gS.d.cts → RunIntentService-siBSjaaY.d.cts} +859 -493
  11. package/dist/bootstrap/index.cjs +5 -2
  12. package/dist/bootstrap/index.d.cts +212 -135
  13. package/dist/bootstrap/index.d.ts +4 -4
  14. package/dist/bootstrap/index.js +3 -3
  15. package/dist/{bootstrap-Bx1u4cbS.cjs → bootstrap-Cm5ruQxx.cjs} +253 -2
  16. package/dist/bootstrap-Cm5ruQxx.cjs.map +1 -0
  17. package/dist/{bootstrap-BoknFKnw.js → bootstrap-D3r505ko.js} +236 -3
  18. package/dist/bootstrap-D3r505ko.js.map +1 -0
  19. package/dist/{index-CVs9rVhl.d.ts → index-DeLl1Tne.d.ts} +632 -230
  20. package/dist/index.cjs +323 -176
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +544 -91
  23. package/dist/index.d.ts +3 -3
  24. package/dist/index.js +299 -166
  25. package/dist/index.js.map +1 -1
  26. package/dist/{runtime-DUW6tIJ1.js → runtime-BGNbRnqs.js} +934 -75
  27. package/dist/runtime-BGNbRnqs.js.map +1 -0
  28. package/dist/{runtime-Dvo2ru5A.cjs → runtime-DKXJwTNv.cjs} +1028 -73
  29. package/dist/runtime-DKXJwTNv.cjs.map +1 -0
  30. package/dist/testing.cjs +4 -4
  31. package/dist/testing.cjs.map +1 -1
  32. package/dist/testing.d.cts +2 -2
  33. package/dist/testing.d.ts +2 -2
  34. package/dist/testing.js +3 -3
  35. package/package.json +7 -2
  36. package/src/ai/AiHost.ts +42 -14
  37. package/src/authoring/DefinedCollectionRegistry.ts +17 -0
  38. package/src/authoring/defineCollection.types.ts +181 -0
  39. package/src/authoring/definePollingTrigger.types.ts +396 -0
  40. package/src/authoring/definePollingTriggerInternals.ts +74 -0
  41. package/src/authoring/index.ts +19 -0
  42. package/src/bootstrap/index.ts +9 -0
  43. package/src/bootstrap/runtime/EngineRuntimeRegistrar.ts +8 -0
  44. package/src/browser.ts +1 -0
  45. package/src/contracts/CodemationTelemetryAttributeNames.ts +6 -0
  46. package/src/contracts/NoOpNodeExecutionTelemetry.ts +2 -11
  47. package/src/contracts/NoOpTelemetrySpanScope.ts +46 -10
  48. package/src/contracts/assertionTypes.ts +63 -0
  49. package/src/contracts/baseTypes.ts +12 -0
  50. package/src/contracts/collectionTypes.ts +44 -0
  51. package/src/contracts/credentialTypes.ts +23 -1
  52. package/src/contracts/executionPersistenceContracts.ts +30 -0
  53. package/src/contracts/index.ts +4 -0
  54. package/src/contracts/runTypes.ts +37 -1
  55. package/src/contracts/runtimeTypes.ts +42 -0
  56. package/src/contracts/telemetryTypes.ts +8 -0
  57. package/src/contracts/testTriggerTypes.ts +66 -0
  58. package/src/contracts/workflowTypes.ts +36 -7
  59. package/src/contracts.ts +59 -0
  60. package/src/events/ConnectionInvocationEventPublisher.ts +46 -0
  61. package/src/events/index.ts +1 -0
  62. package/src/events/runEvents.ts +74 -0
  63. package/src/execution/ChildExecutionScopeFactory.ts +55 -0
  64. package/src/execution/DefaultExecutionContextFactory.ts +6 -0
  65. package/src/execution/ExecutionTelemetryCostTrackingDecoratorFactory.ts +18 -0
  66. package/src/execution/NodeExecutor.ts +10 -2
  67. package/src/execution/NodeInstanceFactory.ts +13 -1
  68. package/src/execution/NodeInstantiationError.ts +16 -0
  69. package/src/execution/NodeRunStateWriter.ts +7 -0
  70. package/src/execution/NodeRunStateWriterFactory.ts +7 -0
  71. package/src/execution/WorkflowRunExecutionContextFactory.ts +3 -0
  72. package/src/execution/index.ts +2 -0
  73. package/src/index.ts +8 -0
  74. package/src/orchestration/AbortControllerFactory.ts +9 -0
  75. package/src/orchestration/NodeExecutionRequestHandlerService.ts +1 -0
  76. package/src/orchestration/RunContinuationService.ts +3 -0
  77. package/src/orchestration/RunStartService.ts +122 -3
  78. package/src/orchestration/TestSuiteOrchestrator.ts +350 -0
  79. package/src/orchestration/TestSuiteRunIdFactory.ts +11 -0
  80. package/src/orchestration/TriggerRuntimeService.ts +34 -7
  81. package/src/orchestration/index.ts +9 -0
  82. package/src/runtime/EngineFactory.ts +12 -0
  83. package/src/triggers/polling/PollingTriggerDedupWindow.ts +23 -0
  84. package/src/triggers/polling/PollingTriggerLogger.ts +18 -0
  85. package/src/triggers/polling/PollingTriggerRuntime.ts +122 -0
  86. package/src/triggers/polling/index.ts +5 -0
  87. package/src/types/index.ts +12 -9
  88. package/src/workflow/definition/NodeIterationIdFactory.ts +26 -0
  89. package/src/workflow/dsl/NodeIdSlugifier.ts +18 -0
  90. package/src/workflow/dsl/WorkflowBuilder.ts +71 -3
  91. package/src/workflow/dsl/WorkflowDefinitionError.ts +15 -0
  92. package/src/workflow/index.ts +3 -0
  93. package/dist/InMemoryRunEventBusRegistry-B0_C4OnP.cjs +0 -262
  94. package/dist/InMemoryRunEventBusRegistry-B0_C4OnP.cjs.map +0 -1
  95. package/dist/InMemoryRunEventBusRegistry-C2U83Hmv.js +0 -238
  96. package/dist/InMemoryRunEventBusRegistry-C2U83Hmv.js.map +0 -1
  97. package/dist/bootstrap-BoknFKnw.js.map +0 -1
  98. package/dist/bootstrap-Bx1u4cbS.cjs.map +0 -1
  99. package/dist/runtime-DUW6tIJ1.js.map +0 -1
  100. package/dist/runtime-Dvo2ru5A.cjs.map +0 -1
@@ -1,3 +1,4 @@
1
+ export { ConnectionInvocationEventPublisher } from "./ConnectionInvocationEventPublisher";
1
2
  export { NodeEventPublisher } from "./NodeEventPublisher";
2
3
  export { InMemoryRunEventBus } from "./InMemoryRunEventBusRegistry";
3
4
  export { EventPublishingWorkflowExecutionRepository } from "./EventPublishingWorkflowExecutionRepository";
@@ -1,5 +1,18 @@
1
+ import type { TestSuiteRunId } from "../contracts/testTriggerTypes";
2
+ import type { ConnectionInvocationRecord } from "../contracts/runTypes";
1
3
  import type { NodeExecutionSnapshot, ParentExecutionRef, PersistedRunState, RunId, WorkflowId } from "../types";
2
4
 
5
+ /**
6
+ * Outcome of a single test case (one workflow run dispatched by the test-suite orchestrator).
7
+ * - `running`: workflow still in flight
8
+ * - `succeeded`: workflow completed AND all assertions passed (or no assertions)
9
+ * - `failed`: workflow failed OR (workflow completed but ≥1 assertion failed)
10
+ * - `errored` / `cancelled`: workflow itself errored or was cancelled
11
+ */
12
+ export type TestCaseRunStatus = "running" | "succeeded" | "failed" | "errored" | "cancelled";
13
+ /** Aggregate outcome of a TestSuiteRun. */
14
+ export type TestSuiteRunStatus = "succeeded" | "failed" | "partial" | "errored" | "cancelled";
15
+
3
16
  export type RunEvent =
4
17
  | Readonly<{ kind: "runCreated"; runId: RunId; workflowId: WorkflowId; parent?: ParentExecutionRef; at: string }>
5
18
  | Readonly<{
@@ -41,6 +54,67 @@ export type RunEvent =
41
54
  parent?: ParentExecutionRef;
42
55
  at: string;
43
56
  snapshot: NodeExecutionSnapshot;
57
+ }>
58
+ | Readonly<{
59
+ kind: "connectionInvocationStarted";
60
+ runId: RunId;
61
+ workflowId: WorkflowId;
62
+ parent?: ParentExecutionRef;
63
+ at: string;
64
+ record: ConnectionInvocationRecord;
65
+ }>
66
+ | Readonly<{
67
+ kind: "connectionInvocationCompleted";
68
+ runId: RunId;
69
+ workflowId: WorkflowId;
70
+ parent?: ParentExecutionRef;
71
+ at: string;
72
+ record: ConnectionInvocationRecord;
73
+ }>
74
+ | Readonly<{
75
+ kind: "connectionInvocationFailed";
76
+ runId: RunId;
77
+ workflowId: WorkflowId;
78
+ parent?: ParentExecutionRef;
79
+ at: string;
80
+ record: ConnectionInvocationRecord;
81
+ }>
82
+ | Readonly<{
83
+ kind: "testSuiteStarted";
84
+ testSuiteRunId: TestSuiteRunId;
85
+ workflowId: WorkflowId;
86
+ triggerNodeId: string;
87
+ triggerNodeName?: string;
88
+ concurrency: number;
89
+ at: string;
90
+ }>
91
+ | Readonly<{
92
+ kind: "testSuiteFinished";
93
+ testSuiteRunId: TestSuiteRunId;
94
+ workflowId: WorkflowId;
95
+ status: TestSuiteRunStatus;
96
+ totalCases: number;
97
+ passedCases: number;
98
+ failedCases: number;
99
+ at: string;
100
+ }>
101
+ | Readonly<{
102
+ kind: "testCaseStarted";
103
+ testSuiteRunId: TestSuiteRunId;
104
+ testCaseIndex: number;
105
+ runId: RunId;
106
+ workflowId: WorkflowId;
107
+ testCaseLabel?: string;
108
+ at: string;
109
+ }>
110
+ | Readonly<{
111
+ kind: "testCaseCompleted";
112
+ testSuiteRunId: TestSuiteRunId;
113
+ testCaseIndex: number;
114
+ runId: RunId;
115
+ workflowId: WorkflowId;
116
+ status: TestCaseRunStatus;
117
+ at: string;
44
118
  }>;
45
119
 
46
120
  export interface RunEventSubscription {
@@ -0,0 +1,55 @@
1
+ import type {
2
+ ActivationIdFactory,
3
+ ConnectionInvocationId,
4
+ NodeExecutionContext,
5
+ NodeId,
6
+ RunnableNodeConfig,
7
+ TelemetrySpanScope,
8
+ } from "../types";
9
+
10
+ /**
11
+ * Builds a re-rooted child execution context for sub-agent (and other deeply-nested) invocations.
12
+ *
13
+ * At the orchestrator's `agent.tool.call` boundary the inner runtime needs a ctx whose:
14
+ * - `nodeId` is the tool's connection node id (so inner LLM/tool connection ids derive correctly),
15
+ * - `activationId` is fresh (so its connection-invocation rows are uniquely identifiable),
16
+ * - `telemetry` parents children under the tool-call span (not the orchestrator's node span),
17
+ * - `binary` is scoped to the new (nodeId, activationId),
18
+ * - `parentInvocationId` points back to the tool-call invocation for downstream lineage.
19
+ *
20
+ * Registered via factory in {@link EngineRuntimeRegistrar} so constructors stay free of parameter
21
+ * decorators (Next/SWC and coverage tooling cannot parse them on in-repo sources).
22
+ */
23
+ export class ChildExecutionScopeFactory {
24
+ constructor(private readonly activationIdFactory: ActivationIdFactory) {}
25
+
26
+ forSubAgent<TConfig extends RunnableNodeConfig<any, any>>(
27
+ args: Readonly<{
28
+ parentCtx: NodeExecutionContext<TConfig>;
29
+ childNodeId: NodeId;
30
+ childConfig: TConfig;
31
+ parentInvocationId: ConnectionInvocationId;
32
+ parentSpan: TelemetrySpanScope;
33
+ }>,
34
+ ): NodeExecutionContext<TConfig> {
35
+ const childActivationId = this.activationIdFactory.makeActivationId();
36
+ const childTelemetry = args.parentSpan.asNodeTelemetry({
37
+ nodeId: args.childNodeId,
38
+ activationId: childActivationId,
39
+ });
40
+ const childBinary = args.parentCtx.binary.forNode({
41
+ nodeId: args.childNodeId,
42
+ activationId: childActivationId,
43
+ });
44
+ return {
45
+ ...args.parentCtx,
46
+ nodeId: args.childNodeId,
47
+ activationId: childActivationId,
48
+ config: args.childConfig,
49
+ telemetry: childTelemetry,
50
+ binary: childBinary,
51
+ parentInvocationId: args.parentInvocationId,
52
+ iterationId: undefined,
53
+ };
54
+ }
55
+ }
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  BinaryStorage,
3
+ CollectionsContext,
3
4
  CostTrackingTelemetryFactory,
4
5
  ExecutionContext,
5
6
  ExecutionContextFactory,
@@ -8,6 +9,7 @@ import type {
8
9
  ParentExecutionRef,
9
10
  RunDataSnapshot,
10
11
  RunId,
12
+ RunTestContext,
11
13
  WorkflowId,
12
14
  } from "../types";
13
15
  import { NoOpCostTrackingTelemetryFactory, NoOpExecutionTelemetryFactory } from "../types";
@@ -26,6 +28,7 @@ export class DefaultExecutionContextFactory implements ExecutionContextFactory {
26
28
  private readonly telemetryFactory: ExecutionTelemetryFactory = new NoOpExecutionTelemetryFactory(),
27
29
  private readonly costTrackingFactory: CostTrackingTelemetryFactory = new NoOpCostTrackingTelemetryFactory(),
28
30
  private readonly currentDate: () => Date = () => new Date(),
31
+ private readonly collections?: CollectionsContext,
29
32
  ) {}
30
33
 
31
34
  create(args: {
@@ -40,6 +43,7 @@ export class DefaultExecutionContextFactory implements ExecutionContextFactory {
40
43
  nodeState?: NodeExecutionStatePublisher;
41
44
  telemetry?: ExecutionContext["telemetry"];
42
45
  getCredential<TSession = unknown>(slotKey: string): Promise<TSession>;
46
+ testContext?: RunTestContext;
43
47
  }): ExecutionContext {
44
48
  const baseTelemetry =
45
49
  args.telemetry ??
@@ -66,6 +70,8 @@ export class DefaultExecutionContextFactory implements ExecutionContextFactory {
66
70
  telemetry,
67
71
  binary: new DefaultExecutionBinaryService(this.binaryStorage, args.workflowId, args.runId, this.currentDate),
68
72
  getCredential: args.getCredential,
73
+ testContext: args.testContext,
74
+ collections: this.collections,
69
75
  };
70
76
  }
71
77
  }
@@ -62,6 +62,15 @@ export class ExecutionTelemetryCostTrackingDecoratorFactory {
62
62
  costTracking: args.costTracking.forScope(nodeTelemetry),
63
63
  });
64
64
  },
65
+ asNodeTelemetry: (
66
+ rescope: Readonly<{ nodeId: NodeId; activationId: NodeActivationId }>,
67
+ ): NodeExecutionTelemetry => {
68
+ const nodeTelemetry = args.telemetry.asNodeTelemetry(rescope);
69
+ return this.decorateNodeExecutionTelemetry({
70
+ telemetry: nodeTelemetry,
71
+ costTracking: args.costTracking.forScope(nodeTelemetry),
72
+ });
73
+ },
65
74
  };
66
75
  }
67
76
 
@@ -79,6 +88,15 @@ export class ExecutionTelemetryCostTrackingDecoratorFactory {
79
88
  artifact: TelemetryArtifactAttachment,
80
89
  ): Promise<TelemetryArtifactReference> | TelemetryArtifactReference => args.scope.attachArtifact(artifact),
81
90
  end: (endArgs?: TelemetrySpanEnd) => args.scope.end(endArgs),
91
+ asNodeTelemetry: (
92
+ rescope: Readonly<{ nodeId: NodeId; activationId: NodeActivationId }>,
93
+ ): NodeExecutionTelemetry => {
94
+ const nodeTelemetry = args.scope.asNodeTelemetry(rescope);
95
+ return this.decorateNodeExecutionTelemetry({
96
+ telemetry: nodeTelemetry,
97
+ costTracking: args.costTracking.forScope(nodeTelemetry),
98
+ });
99
+ },
82
100
  };
83
101
  }
84
102
  }
@@ -13,6 +13,7 @@ import type {
13
13
  TriggerNode,
14
14
  WorkflowNodeInstanceFactory,
15
15
  } from "../types";
16
+ import { NodeIterationIdFactory } from "../workflow/definition/NodeIterationIdFactory";
16
17
 
17
18
  import { FanInMergeByOriginMerger } from "./FanInMergeByOriginMerger";
18
19
  import { ItemExprResolver } from "./ItemExprResolver";
@@ -158,13 +159,20 @@ export class NodeExecutor {
158
159
  const parsed = inputSchema.parse(item.json);
159
160
  const runnableCtx = request.ctx as NodeExecutionContext<RunnableNodeConfig>;
160
161
  const resolvedCtx = await this.itemExprResolver.resolveConfigForItem(runnableCtx, item, i, inputBatch);
161
- const ctx = this.pickExecutionContext(runnableCtx, resolvedCtx);
162
+ const baseCtx = this.pickExecutionContext(runnableCtx, resolvedCtx);
163
+ // Mint a per-item iteration id and stamp it (with the item index) onto the ctx so connection
164
+ // invocations and telemetry written from inside `node.execute` carry the per-item identity.
165
+ const iterationCtx = {
166
+ ...baseCtx,
167
+ iterationId: NodeIterationIdFactory.create(),
168
+ itemIndex: i,
169
+ } as NodeExecutionContext<RunnableNodeConfig>;
162
170
  const args: RunnableNodeExecuteArgs = {
163
171
  input: parsed,
164
172
  item,
165
173
  itemIndex: i,
166
174
  items: inputBatch,
167
- ctx,
175
+ ctx: iterationCtx,
168
176
  };
169
177
  const raw = await Promise.resolve(node.execute(args));
170
178
  const normalized = this.outputNormalizer.normalizeExecuteResult({
@@ -4,6 +4,7 @@ import type { NodeId, NodeResolver, WorkflowDefinition, WorkflowNodeInstanceFact
4
4
  import { MissingRuntimeNode, MissingRuntimeTrigger } from "../workflowSnapshots";
5
5
  import { MissingRuntimeNodeToken } from "../workflowSnapshots/MissingRuntimeNodeToken";
6
6
  import { MissingRuntimeTriggerToken } from "../workflowSnapshots/MissingRuntimeTriggerToken";
7
+ import { NodeInstantiationError } from "./NodeInstantiationError";
7
8
 
8
9
  export class NodeInstanceFactory implements WorkflowNodeInstanceFactory {
9
10
  constructor(private readonly nodeResolver: NodeResolver) {}
@@ -11,7 +12,18 @@ export class NodeInstanceFactory implements WorkflowNodeInstanceFactory {
11
12
  createNodes(workflow: WorkflowDefinition): Map<NodeId, unknown> {
12
13
  const nodeInstances = new Map<NodeId, unknown>();
13
14
  for (const definition of workflow.nodes) {
14
- nodeInstances.set(definition.id, this.createNode(definition));
15
+ try {
16
+ nodeInstances.set(definition.id, this.createNode(definition));
17
+ } catch (err) {
18
+ if (err instanceof NodeInstantiationError) {
19
+ throw err;
20
+ }
21
+ throw new NodeInstantiationError(
22
+ definition.id,
23
+ typeof definition.type === "function" ? definition.type.name : String(definition.type),
24
+ err instanceof Error ? err : new Error(String(err)),
25
+ );
26
+ }
15
27
  }
16
28
  return nodeInstances;
17
29
  }
@@ -0,0 +1,16 @@
1
+ import type { NodeId } from "../types";
2
+
3
+ export class NodeInstantiationError extends Error {
4
+ readonly name = "NodeInstantiationError";
5
+ readonly originalError: Error;
6
+
7
+ constructor(
8
+ readonly nodeId: NodeId,
9
+ readonly nodeType: string,
10
+ originalError: Error,
11
+ ) {
12
+ super(`Failed to instantiate node "${nodeId}" (type ${nodeType}): ${originalError.message}`);
13
+ this.originalError = originalError;
14
+ this.stack = originalError.stack;
15
+ }
16
+ }
@@ -28,6 +28,7 @@ export class NodeRunStateWriter implements NodeExecutionStatePublisher {
28
28
  kind: "nodeQueued" | "nodeStarted" | "nodeCompleted" | "nodeFailed",
29
29
  snapshot: NodeExecutionSnapshot,
30
30
  ) => Promise<void>,
31
+ private readonly publishConnectionInvocationEvent?: (record: ConnectionInvocationRecord) => Promise<void>,
31
32
  ) {}
32
33
 
33
34
  markQueued(args: {
@@ -148,11 +149,17 @@ export class NodeRunStateWriter implements NodeExecutionStatePublisher {
148
149
  startedAt: args.startedAt,
149
150
  finishedAt: args.finishedAt,
150
151
  updatedAt,
152
+ iterationId: args.iterationId,
153
+ itemIndex: args.itemIndex,
154
+ parentInvocationId: args.parentInvocationId,
151
155
  };
152
156
  await this.workflowExecutionRepository.save({
153
157
  ...state,
154
158
  connectionInvocations: [...(state.connectionInvocations ?? []), record],
155
159
  });
160
+ if (this.publishConnectionInvocationEvent) {
161
+ await this.publishConnectionInvocationEvent(record);
162
+ }
156
163
  });
157
164
  }
158
165
 
@@ -6,16 +6,20 @@ import type {
6
6
  WorkflowId,
7
7
  } from "../types";
8
8
 
9
+ import { ConnectionInvocationEventPublisher } from "../events/ConnectionInvocationEventPublisher";
9
10
  import { NodeEventPublisher } from "../events/NodeEventPublisher";
11
+ import type { RunEventBus } from "../events/runEvents";
10
12
  import { NodeRunStateWriter } from "./NodeRunStateWriter";
11
13
 
12
14
  export class NodeRunStateWriterFactory {
13
15
  constructor(
14
16
  private readonly workflowExecutionRepository: WorkflowExecutionRepository,
15
17
  private readonly nodeEventPublisher: NodeEventPublisher,
18
+ private readonly eventBus?: RunEventBus,
16
19
  ) {}
17
20
 
18
21
  create(runId: RunId, workflowId: WorkflowId, parent: ParentExecutionRef | undefined): NodeExecutionStatePublisher {
22
+ const connectionInvocationEventPublisher = new ConnectionInvocationEventPublisher(this.eventBus, parent);
19
23
  return new NodeRunStateWriter(
20
24
  this.workflowExecutionRepository,
21
25
  runId,
@@ -24,6 +28,9 @@ export class NodeRunStateWriterFactory {
24
28
  async (kind, snapshot) => {
25
29
  await this.nodeEventPublisher.publish(kind, snapshot);
26
30
  },
31
+ async (record) => {
32
+ await connectionInvocationEventPublisher.publish(record);
33
+ },
27
34
  );
28
35
  }
29
36
  }
@@ -5,6 +5,7 @@ import type {
5
5
  ParentExecutionRef,
6
6
  RunDataFactory,
7
7
  RunId,
8
+ RunTestContext,
8
9
  WorkflowId,
9
10
  } from "../types";
10
11
 
@@ -30,6 +31,7 @@ export class WorkflowRunExecutionContextFactory {
30
31
  engineMaxSubworkflowDepth: number;
31
32
  data: ReturnType<RunDataFactory["create"]>;
32
33
  nodeState?: NodeExecutionStatePublisher;
34
+ testContext?: RunTestContext;
33
35
  }) {
34
36
  return this.executionContextFactory.create({
35
37
  runId: args.runId,
@@ -42,6 +44,7 @@ export class WorkflowRunExecutionContextFactory {
42
44
  data: args.data,
43
45
  nodeState: args.nodeState,
44
46
  getCredential: this.credentialResolverFactory.create(args.workflowId, args.nodeId),
47
+ testContext: args.testContext,
45
48
  });
46
49
  }
47
50
  }
@@ -1,6 +1,8 @@
1
1
  export { ActivationEnqueueService } from "./ActivationEnqueueService";
2
+ export { ChildExecutionScopeFactory } from "./ChildExecutionScopeFactory";
2
3
  export { NodeActivationRequestInputPreparer } from "./NodeActivationRequestInputPreparer";
3
4
  export { NodeInputContractError } from "./NodeInputContractError";
5
+ export { NodeInstantiationError } from "./NodeInstantiationError";
4
6
  export { CredentialResolverFactory } from "./CredentialResolverFactory";
5
7
  export { DefaultAsyncSleeper } from "./DefaultAsyncSleeper";
6
8
  export { DefaultExecutionContextFactory } from "./DefaultExecutionContextFactory";
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export * from "./runtime-types/runtimeTypeDecorators.types";
14
14
  export * from "./serialization/ItemsInputNormalizer";
15
15
  export { DefaultExecutionBinaryService, UnavailableBinaryStorage } from "./binaries";
16
16
  export {
17
+ ChildExecutionScopeFactory,
17
18
  CredentialResolverFactory,
18
19
  DefaultAsyncSleeper,
19
20
  DefaultExecutionContextFactory,
@@ -26,3 +27,10 @@ export { EngineExecutionLimitsPolicy, type EngineExecutionLimitsPolicyConfig } f
26
27
  export { InMemoryBinaryStorage, InMemoryRunDataFactory } from "./runStorage";
27
28
  export { InMemoryLiveWorkflowRepository, RunIntentService } from "./runtime";
28
29
  export * from "./types";
30
+ export { PollingTriggerRuntime, PollingTriggerDedupWindow, NoOpPollingTriggerLogger } from "./triggers/polling";
31
+ export type {
32
+ PollingTriggerLogger,
33
+ PollingRunCycleArgs,
34
+ PollingRunCycleResult,
35
+ PollingTriggerStartArgs,
36
+ } from "./triggers/polling";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Mints fresh {@link AbortController}s. Injected (rather than direct `new`) to honor the
3
+ * codebase's no-direct-construction rule and to give tests a seam for substituting a fake.
4
+ */
5
+ export class AbortControllerFactory {
6
+ create(): AbortController {
7
+ return new AbortController();
8
+ }
9
+ }
@@ -77,6 +77,7 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
77
77
  engineMaxSubworkflowDepth: limits.engineMaxSubworkflowDepth,
78
78
  data,
79
79
  nodeState: this.nodeStatePublisherFactory.create(state.runId, state.workflowId, resolvedParent),
80
+ testContext: state.executionOptions?.testContext,
80
81
  });
81
82
 
82
83
  const inputsByPort = pendingExecution.inputsByPort;
@@ -141,6 +141,7 @@ export class RunContinuationService {
141
141
  engineMaxSubworkflowDepth: limits.engineMaxSubworkflowDepth,
142
142
  data,
143
143
  nodeState: this.nodeStatePublisherFactory.create(state.runId, state.workflowId, state.parent),
144
+ testContext: state.executionOptions?.testContext,
144
145
  });
145
146
 
146
147
  data.setOutputs(args.nodeId, args.outputs);
@@ -755,6 +756,7 @@ export class RunContinuationService {
755
756
  engineMaxSubworkflowDepth: webhookLimits.engineMaxSubworkflowDepth,
756
757
  data,
757
758
  nodeState: this.nodeStatePublisherFactory.create(args.state.runId, args.state.workflowId, args.state.parent),
759
+ testContext: args.state.executionOptions?.testContext,
758
760
  });
759
761
  const request = this.nodeActivationRequestComposer.createFromPlannedActivation({
760
762
  next,
@@ -859,6 +861,7 @@ export class RunContinuationService {
859
861
  engineMaxSubworkflowDepth: limits.engineMaxSubworkflowDepth,
860
862
  data,
861
863
  nodeState: this.nodeStatePublisherFactory.create(state.runId, state.workflowId, state.parent),
864
+ testContext: state.executionOptions?.testContext,
862
865
  });
863
866
  const activationId = pendingExecution.activationId;
864
867
  return {
@@ -34,8 +34,12 @@ import { RunPolicySnapshotFactory } from "../policies/storage/RunPolicySnapshotF
34
34
  import { ActivationEnqueueService } from "../execution/ActivationEnqueueService";
35
35
  import { NodeRunStateWriterFactory } from "../execution/NodeRunStateWriterFactory";
36
36
  import { NodeActivationRequestComposer } from "../execution/NodeActivationRequestComposer";
37
+ import { NodeExecutionSnapshotFactory } from "../execution/NodeExecutionSnapshotFactory";
38
+ import { NodeInstantiationError } from "../execution/NodeInstantiationError";
39
+ import { PersistedRunStateTerminalBuilder } from "../execution/PersistedRunStateTerminalBuilder";
37
40
  import { RunStateSemantics } from "../execution/RunStateSemantics";
38
41
  import { WorkflowRunExecutionContextFactory } from "../execution/WorkflowRunExecutionContextFactory";
42
+ import { NodeEventPublisher } from "../events/NodeEventPublisher";
39
43
 
40
44
  export class RunStartService {
41
45
  constructor(
@@ -52,6 +56,8 @@ export class RunStartService {
52
56
  private readonly waiters: EngineWaiters,
53
57
  private readonly workflowPolicyRuntimeDefaults: WorkflowPolicyRuntimeDefaults | undefined,
54
58
  private readonly executionLimitsPolicy: EngineExecutionLimitsPolicy,
59
+ private readonly nodeEventPublisher: NodeEventPublisher,
60
+ private readonly persistedRunStateTerminalBuilder: PersistedRunStateTerminalBuilder,
55
61
  ) {}
56
62
 
57
63
  async runWorkflow(
@@ -96,8 +102,29 @@ export class RunStartService {
96
102
  engineMaxSubworkflowDepth: mergedExecutionOptions.maxSubworkflowDepth!,
97
103
  data,
98
104
  nodeState: this.nodeStatePublisherFactory.create(runId, workflow.id, parent),
105
+ testContext: mergedExecutionOptions.testContext,
99
106
  });
100
- const { topology, planner } = this.planningFactory.create(workflow);
107
+ let planning: Readonly<ReturnType<EngineWorkflowPlanningFactory["create"]>>;
108
+ try {
109
+ planning = this.planningFactory.create(workflow);
110
+ } catch (err) {
111
+ if (err instanceof NodeInstantiationError) {
112
+ return await this.failRunDuringPlanning({
113
+ runId,
114
+ workflowId: workflow.id,
115
+ startedAt,
116
+ parent,
117
+ executionOptions: mergedExecutionOptions,
118
+ control: undefined,
119
+ workflowSnapshot,
120
+ mutableState,
121
+ policySnapshot,
122
+ err,
123
+ });
124
+ }
125
+ throw err;
126
+ }
127
+ const { topology, planner } = planning;
101
128
  const startDefinition = topology.defsById.get(startAt);
102
129
  if (!startDefinition) {
103
130
  throw new Error(`Unknown start nodeId: ${startAt}`);
@@ -183,7 +210,27 @@ export class RunStartService {
183
210
  engineCounters: { completedNodeActivations: 0 },
184
211
  });
185
212
 
186
- const { topology, planner } = this.planningFactory.create(request.workflow);
213
+ let planningFromState: Readonly<ReturnType<EngineWorkflowPlanningFactory["create"]>>;
214
+ try {
215
+ planningFromState = this.planningFactory.create(request.workflow);
216
+ } catch (err) {
217
+ if (err instanceof NodeInstantiationError) {
218
+ return await this.failRunDuringPlanning({
219
+ runId,
220
+ workflowId: request.workflow.id,
221
+ startedAt,
222
+ parent: request.parent,
223
+ executionOptions: mergedExecutionOptions,
224
+ control,
225
+ workflowSnapshot,
226
+ mutableState,
227
+ policySnapshot,
228
+ err,
229
+ });
230
+ }
231
+ throw err;
232
+ }
233
+ const { topology, planner } = planningFromState;
187
234
  const plan = CurrentStateFrontierPlanner.createFromTopology(topology).plan({
188
235
  currentState: this.createRunCurrentState(request.currentState, mutableState),
189
236
  stopCondition: control.stopCondition,
@@ -206,6 +253,7 @@ export class RunStartService {
206
253
  engineMaxSubworkflowDepth: mergedExecutionOptions.maxSubworkflowDepth!,
207
254
  data,
208
255
  nodeState: this.nodeStatePublisherFactory.create(runId, request.workflow.id, request.parent),
256
+ testContext: mergedExecutionOptions.testContext,
209
257
  });
210
258
 
211
259
  return await this.scheduleInitialPlan({
@@ -229,10 +277,17 @@ export class RunStartService {
229
277
  currentState: RunCurrentState | undefined,
230
278
  mutableState: NonNullable<Awaited<ReturnType<WorkflowExecutionRepository["load"]>>>["mutableState"],
231
279
  ): RunCurrentState {
280
+ // Each `ConnectionInvocationRecord` represents a single, auditable LLM/tool call
281
+ // that must belong to exactly one run. Reruns start from a fresh invocation
282
+ // ledger so the new run only records what it actually invokes. The prior run's
283
+ // invocations remain queryable on that run's persisted state (their true owner).
284
+ // Carrying them over here would write duplicate `ExecutionInstance` rows whose
285
+ // primary key (`invocationId`) already exists under the previous run, causing a
286
+ // `Unique constraint failed on the fields: (instance_id)` violation on first save.
232
287
  return {
233
288
  outputsByNode: { ...(currentState?.outputsByNode ?? {}) },
234
289
  nodeSnapshotsByNodeId: { ...(currentState?.nodeSnapshotsByNodeId ?? {}) },
235
- connectionInvocations: currentState?.connectionInvocations ? [...currentState.connectionInvocations] : undefined,
290
+ connectionInvocations: [],
236
291
  mutableState: mutableState ?? currentState?.mutableState,
237
292
  };
238
293
  }
@@ -448,4 +503,68 @@ export class RunStartService {
448
503
  this.waiters.resolveRunCompletion(result);
449
504
  return result;
450
505
  }
506
+
507
+ private async failRunDuringPlanning(args: {
508
+ runId: RunId;
509
+ workflowId: WorkflowId;
510
+ startedAt: string;
511
+ parent?: ParentExecutionRef;
512
+ executionOptions?: RunExecutionOptions;
513
+ control?: PersistedRunControlState;
514
+ workflowSnapshot: NonNullable<Awaited<ReturnType<WorkflowExecutionRepository["load"]>>>["workflowSnapshot"];
515
+ mutableState: NonNullable<Awaited<ReturnType<WorkflowExecutionRepository["load"]>>>["mutableState"];
516
+ policySnapshot: NonNullable<Awaited<ReturnType<WorkflowExecutionRepository["load"]>>>["policySnapshot"];
517
+ err: NodeInstantiationError;
518
+ }): Promise<RunResult> {
519
+ const finishedAt = new Date().toISOString();
520
+ const failedSnapshot = NodeExecutionSnapshotFactory.failed({
521
+ previous: undefined,
522
+ runId: args.runId,
523
+ workflowId: args.workflowId,
524
+ nodeId: args.err.nodeId,
525
+ activationId: "planning_failure",
526
+ parent: args.parent,
527
+ finishedAt,
528
+ inputsByPort: {},
529
+ error: args.err,
530
+ });
531
+ const failedState = this.persistedRunStateTerminalBuilder.mergeTerminal({
532
+ state: {
533
+ runId: args.runId,
534
+ workflowId: args.workflowId,
535
+ startedAt: args.startedAt,
536
+ parent: args.parent,
537
+ executionOptions: args.executionOptions,
538
+ control: args.control,
539
+ workflowSnapshot: args.workflowSnapshot,
540
+ mutableState: args.mutableState,
541
+ policySnapshot: args.policySnapshot,
542
+ engineCounters: { completedNodeActivations: 0 },
543
+ status: "pending",
544
+ pending: undefined,
545
+ queue: [],
546
+ outputsByNode: {},
547
+ nodeSnapshotsByNodeId: {},
548
+ connectionInvocations: [],
549
+ },
550
+ engineCounters: { completedNodeActivations: 0 },
551
+ status: "failed",
552
+ queue: [],
553
+ outputsByNode: {},
554
+ nodeSnapshotsByNodeId: { [args.err.nodeId]: failedSnapshot },
555
+ finishedAtIso: finishedAt,
556
+ });
557
+ await this.workflowExecutionRepository.save(failedState);
558
+ await this.nodeEventPublisher.publish("nodeFailed", failedSnapshot);
559
+
560
+ const result: RunResult = {
561
+ runId: args.runId,
562
+ workflowId: args.workflowId,
563
+ startedAt: args.startedAt,
564
+ status: "failed",
565
+ error: { message: args.err.message },
566
+ };
567
+ this.waiters.resolveRunCompletion(result);
568
+ return result;
569
+ }
451
570
  }