@codemation/core 0.11.0 → 0.12.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 (101) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/CostCatalogContract-DD7fQ4FF.d.cts +19 -0
  3. package/dist/{EngineRuntimeRegistration.types-MPYWsEM0.d.cts → EngineRuntimeRegistration.types-DTV5_7Jw.d.cts} +3 -2
  4. package/dist/{EngineRuntimeRegistration.types-BZ_1XWAJ.d.ts → EngineRuntimeRegistration.types-Dl92Hdoi.d.ts} +2 -2
  5. package/dist/InMemoryRunDataFactory-qMiYjhCK.d.cts +202 -0
  6. package/dist/{InMemoryRunEventBusRegistry-sM4z4n_i.js → InMemoryRunEventBusRegistry-Bwunvt1T.js} +1 -1
  7. package/dist/{InMemoryRunEventBusRegistry-sM4z4n_i.js.map → InMemoryRunEventBusRegistry-Bwunvt1T.js.map} +1 -1
  8. package/dist/{InMemoryRunEventBusRegistry-VM3OWnHo.cjs → InMemoryRunEventBusRegistry-Sa86VxuV.cjs} +1 -1
  9. package/dist/{InMemoryRunEventBusRegistry-VM3OWnHo.cjs.map → InMemoryRunEventBusRegistry-Sa86VxuV.cjs.map} +1 -1
  10. package/dist/ItemsInputNormalizer-BhuxvZh5.js +36 -0
  11. package/dist/ItemsInputNormalizer-BhuxvZh5.js.map +1 -0
  12. package/dist/ItemsInputNormalizer-C09a7iFP.d.ts +321 -0
  13. package/dist/ItemsInputNormalizer-DLaD6rTl.d.cts +407 -0
  14. package/dist/ItemsInputNormalizer-Div-fb6a.cjs +43 -0
  15. package/dist/ItemsInputNormalizer-Div-fb6a.cjs.map +1 -0
  16. package/dist/RunIntentService-BOSGwmqn.d.ts +299 -0
  17. package/dist/RunIntentService-CWMMrAP4.d.cts +220 -0
  18. package/dist/{RunIntentService-MUHJ1bhO.d.cts → agentMcpTypes-DUmniLOY.d.cts} +183 -206
  19. package/dist/bootstrap/index.cjs +4 -2
  20. package/dist/bootstrap/index.d.cts +63 -5
  21. package/dist/bootstrap/index.d.ts +5 -4
  22. package/dist/bootstrap/index.js +4 -2
  23. package/dist/{bootstrap-Dgzsjoj7.js → bootstrap-CKTMMNmL.js} +174 -3
  24. package/dist/bootstrap-CKTMMNmL.js.map +1 -0
  25. package/dist/{bootstrap-dVmpU1ju.cjs → bootstrap-D460dCgS.cjs} +209 -36
  26. package/dist/bootstrap-D460dCgS.cjs.map +1 -0
  27. package/dist/browser.cjs +17 -0
  28. package/dist/browser.d.cts +4 -0
  29. package/dist/browser.d.ts +3 -0
  30. package/dist/browser.js +4 -0
  31. package/dist/contracts-CK0x6w_G.cjs +74 -0
  32. package/dist/contracts-CK0x6w_G.cjs.map +1 -0
  33. package/dist/contracts-DXdfTdpW.js +50 -0
  34. package/dist/contracts-DXdfTdpW.js.map +1 -0
  35. package/dist/contracts.cjs +6 -0
  36. package/dist/contracts.d.cts +5 -0
  37. package/dist/contracts.d.ts +2 -0
  38. package/dist/contracts.js +3 -0
  39. package/dist/di-DdsgWfVy.js +405 -0
  40. package/dist/di-DdsgWfVy.js.map +1 -0
  41. package/dist/di-tO6R7VJV.cjs +524 -0
  42. package/dist/di-tO6R7VJV.cjs.map +1 -0
  43. package/dist/executionPersistenceContracts-DenJJK2T.d.cts +275 -0
  44. package/dist/{index-Bes88mxT.d.ts → index-BZDhEQ6W.d.ts} +278 -415
  45. package/dist/{RunIntentService-BrEq6Jm6.d.ts → index-CSKKuK60.d.ts} +441 -286
  46. package/dist/index.cjs +97 -250
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.d.cts +395 -803
  49. package/dist/index.d.ts +5 -3
  50. package/dist/index.js +58 -224
  51. package/dist/index.js.map +1 -1
  52. package/dist/params-DqRvku2h.d.cts +44 -0
  53. package/dist/{runtime-Duf3ClPw.js → runtime-BPZgnZ9G.js} +591 -382
  54. package/dist/runtime-BPZgnZ9G.js.map +1 -0
  55. package/dist/{runtime-vH0EeZzH.cjs → runtime-CyW9c9XM.cjs} +651 -500
  56. package/dist/runtime-CyW9c9XM.cjs.map +1 -0
  57. package/dist/testing.cjs +23 -21
  58. package/dist/testing.cjs.map +1 -1
  59. package/dist/testing.d.cts +3 -2
  60. package/dist/testing.d.ts +3 -2
  61. package/dist/testing.js +5 -3
  62. package/dist/testing.js.map +1 -1
  63. package/package.json +9 -5
  64. package/src/ai/AgentConnectionNodeCollector.ts +1 -1
  65. package/src/authoring/defineHumanApprovalNode.types.ts +379 -0
  66. package/src/authoring/index.ts +6 -0
  67. package/src/bootstrap/runtime/EngineRuntimeRegistrar.ts +29 -0
  68. package/src/contracts/CodemationTelemetryAttributeNames.ts +10 -0
  69. package/src/contracts/credentialTypes.ts +10 -0
  70. package/src/contracts/hitlSeamTypes.ts +34 -0
  71. package/src/contracts/humanTaskStoreTypes.ts +48 -0
  72. package/src/contracts/inboxChannelTypes.ts +58 -0
  73. package/src/contracts/index.ts +3 -0
  74. package/src/contracts/runTypes.ts +61 -3
  75. package/src/contracts/runtimeTypes.ts +112 -0
  76. package/src/credentials/CredentialMaterialProvider.types.ts +61 -0
  77. package/src/credentials/ManagedCredentialMaterialWriteError.ts +14 -0
  78. package/src/credentials/ManagedMaterialFetchError.ts +16 -0
  79. package/src/execution/ActivationEnqueueService.ts +16 -0
  80. package/src/execution/DefaultExecutionContextFactory.ts +11 -0
  81. package/src/execution/NodeExecutionSnapshotFactory.ts +7 -1
  82. package/src/execution/NodeExecutor.ts +60 -1
  83. package/src/execution/NodeExecutorFactory.ts +12 -2
  84. package/src/execution/NodeSuspensionHandler.ts +220 -0
  85. package/src/execution/PersistedRunStateTerminalBuilder.ts +5 -2
  86. package/src/execution/RunStateSemantics.ts +5 -0
  87. package/src/execution/RunSuspendedError.ts +21 -0
  88. package/src/index.ts +40 -0
  89. package/src/orchestration/Engine.ts +12 -2
  90. package/src/orchestration/EngineWaiters.ts +1 -1
  91. package/src/orchestration/NodeExecutionRequestHandlerService.ts +25 -2
  92. package/src/orchestration/RunContinuationService.ts +226 -2
  93. package/src/orchestration/TestSuiteOrchestrator.ts +5 -4
  94. package/src/runtime/RunIntentService.ts +3 -0
  95. package/src/workflow/dsl/ChainCursorResolver.ts +36 -0
  96. package/tsdown.config.ts +1 -1
  97. package/dist/InMemoryRunDataFactory-hmkh0lzR.d.cts +0 -138
  98. package/dist/bootstrap-Dgzsjoj7.js.map +0 -1
  99. package/dist/bootstrap-dVmpU1ju.cjs.map +0 -1
  100. package/dist/runtime-Duf3ClPw.js.map +0 -1
  101. package/dist/runtime-vH0EeZzH.cjs.map +0 -1
@@ -0,0 +1,220 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+
4
+ import type { HitlResumeTokenSignerSeam, HitlTimeoutJobSchedulerSeam } from "../contracts/hitlSeamTypes";
5
+ import type { HumanTaskRecord, HumanTaskStore } from "../contracts/humanTaskStoreTypes";
6
+ import type {
7
+ HumanTaskHandle,
8
+ NodeActivationId,
9
+ NodeId,
10
+ PersistedRunState,
11
+ PersistedSuspensionEntry,
12
+ RunId,
13
+ SuspensionRequest,
14
+ WorkflowExecutionRepository,
15
+ } from "../types";
16
+ import type { TelemetryScope } from "../contracts/telemetryTypes";
17
+ import { CodemationTelemetryAttributeNames } from "../contracts/CodemationTelemetryAttributeNames";
18
+
19
+ import { RunSuspendedError } from "./RunSuspendedError";
20
+ export { RunSuspendedError };
21
+
22
+ /**
23
+ * Handles per-item `SuspensionRequest` catches in the engine's item execution loop.
24
+ *
25
+ * Responsibilities:
26
+ * 1. Generate a `taskId` (UUID v4).
27
+ * 2. Persist a `HumanTask` row via `HumanTaskStore.create`.
28
+ * 3. Sign a resume URL via `HitlResumeTokenSigner.sign`.
29
+ * 4. Enqueue a delayed BullMQ timeout job via `HitlTimeoutJobScheduler.enqueue`.
30
+ * 5. Build a `HumanTaskHandle` and call `deliver`.
31
+ * 6. Append a `PersistedSuspensionEntry` to the run state and flip status to `"suspended"`.
32
+ * 7. Persist via `WorkflowExecutionRepository.save`.
33
+ * 8. Throw `RunSuspendedError` so the caller can exit cleanly.
34
+ *
35
+ * If `deliver` throws, the error propagates up to `NodeExecutionRequestHandlerService`
36
+ * which routes it through `resumeFromNodeError` → run status becomes `"failed"`.
37
+ *
38
+ * `humanTaskStore`, `tokenSigner`, and `timeoutScheduler` are optional —
39
+ * when not registered (e.g. in unit tests), the handler still suspends the run but
40
+ * skips persistence, token signing, and job scheduling.
41
+ */
42
+ export class NodeSuspensionHandler {
43
+ constructor(
44
+ private readonly workflowExecutionRepository: WorkflowExecutionRepository,
45
+ private readonly humanTaskStore?: HumanTaskStore,
46
+ private readonly tokenSigner?: HitlResumeTokenSignerSeam,
47
+ private readonly timeoutScheduler?: HitlTimeoutJobSchedulerSeam,
48
+ /** Workspace ID to stamp on HumanTaskRecord in managed mode (T7 security fix). Null in non-managed mode. */
49
+ private readonly workspaceId?: string,
50
+ ) {}
51
+
52
+ async handle(args: {
53
+ runId: RunId;
54
+ nodeId: NodeId;
55
+ activationId: NodeActivationId;
56
+ itemIndex: number;
57
+ suspensionRequest: SuspensionRequest;
58
+ state: PersistedRunState;
59
+ /** Telemetry scope of the node's per-item span. Used to emit `hitl.task.*` span events. */
60
+ telemetry?: TelemetryScope;
61
+ }): Promise<never> {
62
+ const taskId = `htask_${globalThis.crypto.randomUUID()}`;
63
+ const { timeout, onTimeout, deliver, decisionSchema, subject, metadata } = args.suspensionRequest.request;
64
+
65
+ const timeoutMs = this.parseDurationMs(timeout);
66
+ const expiresAt = new Date(Date.now() + timeoutMs);
67
+
68
+ const decisionSchemaHash = this.hashSchema(decisionSchema);
69
+ const decisionSchemaJson = this.schemaToJson(decisionSchema);
70
+
71
+ // Build resume token (when signer is available)
72
+ let resumeUrl = "";
73
+ let resumeTokenHash = "";
74
+ if (this.tokenSigner) {
75
+ const token = this.tokenSigner.sign({ taskId, expiresAt, schemaHash: decisionSchemaHash });
76
+ resumeUrl = token; // callers (deliver) receive the raw token; inbox layers wrap into a URL
77
+ resumeTokenHash = this.tokenSigner.hashToken(token);
78
+ }
79
+
80
+ const handle: HumanTaskHandle = {
81
+ taskId,
82
+ runId: args.runId,
83
+ nodeId: args.nodeId,
84
+ expiresAt,
85
+ resumeUrl,
86
+ ...(metadata !== undefined ? { metadata } : {}),
87
+ };
88
+
89
+ // Emit hitl.task.created before calling deliver.
90
+ const channel = (metadata as Record<string, unknown> | undefined)?.["channel"];
91
+ await args.telemetry?.addSpanEvent?.({
92
+ name: "hitl.task.created",
93
+ attributes: {
94
+ [CodemationTelemetryAttributeNames.hitlTaskId]: taskId,
95
+ [CodemationTelemetryAttributeNames.hitlChannel]: typeof channel === "string" ? channel : "unknown",
96
+ [CodemationTelemetryAttributeNames.runId]: args.runId,
97
+ [CodemationTelemetryAttributeNames.nodeId]: args.nodeId,
98
+ expiresAt: expiresAt.toISOString(),
99
+ },
100
+ });
101
+
102
+ // D5: deliver throws → emit hitl.task.delivery_failed, then propagate upward;
103
+ // caller routes to resumeFromNodeError → "failed"
104
+ let deliveryRef: Awaited<ReturnType<typeof deliver>>;
105
+ try {
106
+ deliveryRef = await deliver(handle);
107
+ } catch (deliverError) {
108
+ await args.telemetry?.addSpanEvent?.({
109
+ name: "hitl.task.delivery_failed",
110
+ attributes: {
111
+ [CodemationTelemetryAttributeNames.hitlTaskId]: taskId,
112
+ [CodemationTelemetryAttributeNames.hitlChannel]: typeof channel === "string" ? channel : "unknown",
113
+ error: deliverError instanceof Error ? deliverError.message : String(deliverError),
114
+ },
115
+ });
116
+ throw deliverError;
117
+ }
118
+
119
+ // Persist HumanTask row
120
+ if (this.humanTaskStore) {
121
+ const record: HumanTaskRecord = {
122
+ id: taskId,
123
+ runId: args.runId,
124
+ workflowId: args.state.workflowId,
125
+ // T7: stamp workspaceId in managed mode so HitlCallbackHandler can assert workspace identity.
126
+ // Non-managed mode leaves this undefined (null in DB) — the check in HitlCallbackHandler
127
+ // is guarded by `task.workspaceId !== undefined` and is a no-op when null.
128
+ workspaceId: this.workspaceId ?? undefined,
129
+ nodeId: args.nodeId,
130
+ activationId: args.activationId,
131
+ itemIndex: args.itemIndex,
132
+ status: "pending",
133
+ channel: "local",
134
+ subject,
135
+ metadata: (metadata as Record<string, import("../contracts/workflowTypes").JsonValue>) ?? {},
136
+ decisionSchemaJson,
137
+ decisionSchemaHash,
138
+ onTimeout,
139
+ deliveryRef,
140
+ resumeTokenHash: resumeTokenHash || "no-token",
141
+ expiresAt,
142
+ createdAt: new Date(),
143
+ };
144
+ await this.humanTaskStore.create(record);
145
+ }
146
+
147
+ // Enqueue timeout job
148
+ if (this.timeoutScheduler) {
149
+ await this.timeoutScheduler.enqueueTimeoutJob({ taskId, expiresAt });
150
+ }
151
+
152
+ const entry: PersistedSuspensionEntry = {
153
+ taskId,
154
+ nodeId: args.nodeId,
155
+ activationId: args.activationId,
156
+ itemIndex: args.itemIndex,
157
+ decisionSchemaHash,
158
+ deliveryRef,
159
+ timeoutAt: expiresAt.toISOString(),
160
+ onTimeout,
161
+ };
162
+
163
+ const existingSuspensions = args.state.suspension ?? [];
164
+ const updatedState: PersistedRunState = {
165
+ ...args.state,
166
+ status: "suspended",
167
+ suspension: [...existingSuspensions, entry],
168
+ };
169
+
170
+ await this.workflowExecutionRepository.save(updatedState);
171
+
172
+ throw new RunSuspendedError(args.runId, taskId);
173
+ }
174
+
175
+ /**
176
+ * Parse a duration string into milliseconds.
177
+ * Accepts ISO 8601 durations ("PT24H", "PT30M") and shorthand ("24h", "30m", "1d").
178
+ * Throws for unrecognised formats.
179
+ */
180
+ private parseDurationMs(duration: string): number {
181
+ // Shorthand: "24h", "30m", "7d", "3600s"
182
+ const shorthand = /^(\d+(?:\.\d+)?)(s|m|h|d)$/i.exec(duration);
183
+ if (shorthand) {
184
+ const value = parseFloat(shorthand[1]!);
185
+ const unit = shorthand[2]!.toLowerCase();
186
+ const multipliers: Record<string, number> = {
187
+ s: 1_000,
188
+ m: 60_000,
189
+ h: 3_600_000,
190
+ d: 86_400_000,
191
+ };
192
+ return value * multipliers[unit]!;
193
+ }
194
+ // ISO 8601 duration subset: PTnHnMnS (days handled via P1D)
195
+ const iso = /^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/.exec(duration);
196
+ if (iso) {
197
+ const days = parseFloat(iso[1] ?? "0");
198
+ const hours = parseFloat(iso[2] ?? "0");
199
+ const minutes = parseFloat(iso[3] ?? "0");
200
+ const seconds = parseFloat(iso[4] ?? "0");
201
+ return (days * 86_400 + hours * 3_600 + minutes * 60 + seconds) * 1_000;
202
+ }
203
+ throw new Error(`NodeSuspensionHandler: unrecognised duration format: "${duration}"`);
204
+ }
205
+
206
+ private hashSchema(schema: unknown): string {
207
+ const json = this.schemaToJson(schema);
208
+ return createHash("sha256").update(json).digest("hex");
209
+ }
210
+
211
+ private schemaToJson(schema: unknown): string {
212
+ if (schema instanceof z.ZodType) {
213
+ return JSON.stringify(z.toJSONSchema(schema));
214
+ }
215
+ if (typeof (schema as { toJSON?: unknown }).toJSON === "function") {
216
+ return JSON.stringify((schema as { toJSON: () => unknown }).toJSON());
217
+ }
218
+ return JSON.stringify(schema);
219
+ }
220
+ }
@@ -1,4 +1,4 @@
1
- import type { EngineRunCounters, PersistedRunState, RunQueueEntry } from "../types";
1
+ import type { EngineRunCounters, PersistedRunState, RunHaltReason, RunQueueEntry } from "../types";
2
2
 
3
3
  /**
4
4
  * Merges common terminal-run fields onto a loaded {@link PersistedRunState} without repeating object literals.
@@ -7,7 +7,9 @@ export class PersistedRunStateTerminalBuilder {
7
7
  mergeTerminal(args: {
8
8
  state: PersistedRunState;
9
9
  engineCounters: EngineRunCounters;
10
- status: "completed" | "failed";
10
+ status: "completed" | "failed" | "halted";
11
+ /** Populated when `status === "halted"`. */
12
+ reason?: RunHaltReason;
11
13
  queue: RunQueueEntry[];
12
14
  outputsByNode: PersistedRunState["outputsByNode"];
13
15
  nodeSnapshotsByNodeId: NonNullable<PersistedRunState["nodeSnapshotsByNodeId"]>;
@@ -18,6 +20,7 @@ export class PersistedRunStateTerminalBuilder {
18
20
  ...args.state,
19
21
  engineCounters: args.engineCounters,
20
22
  status: args.status,
23
+ reason: args.reason,
21
24
  pending: undefined,
22
25
  queue: args.queue,
23
26
  outputsByNode: args.outputsByNode,
@@ -2,6 +2,7 @@ import type {
2
2
  Items,
3
3
  NodeActivationId,
4
4
  NodeExecutionSnapshot,
5
+ NodeExecutionStatus,
5
6
  NodeId,
6
7
  NodeInputsByPort,
7
8
  NodeOutputs,
@@ -139,6 +140,10 @@ export class RunStateSemantics {
139
140
  finishedAt: string;
140
141
  inputsByPort: NodeInputsByPort;
141
142
  outputs: NodeOutputs;
143
+ hitlStatus?: Extract<
144
+ NodeExecutionStatus,
145
+ "hitl-approved" | "hitl-rejected" | "hitl-timeout" | "hitl-auto-accepted" | "hitl-cancelled"
146
+ >;
142
147
  }): NodeExecutionSnapshot {
143
148
  const definition = args.workflow.nodes.find((node) => node.id === args.nodeId);
144
149
  if (this.missingRuntimeExecutionMarker.isMarked(definition?.config)) {
@@ -0,0 +1,21 @@
1
+ import type { RunId } from "../types";
2
+
3
+ /**
4
+ * Internal sentinel thrown by {@link NodeSuspensionHandler} after persisting a suspension
5
+ * entry. `NodeExecutionRequestHandlerService` catches this specifically and returns cleanly —
6
+ * no continuation call, preventing `resumeFromNodeResult` / `resumeFromNodeError` from
7
+ * overwriting the `"suspended"` run status.
8
+ *
9
+ * The `Error` suffix satisfies the ESLint `no-manual-di-new` allowlist. This is NOT a
10
+ * user-facing error — it is an engine-internal control-flow primitive and should NOT be
11
+ * exported from the public barrel.
12
+ */
13
+ export class RunSuspendedError extends Error {
14
+ constructor(
15
+ readonly runId: RunId,
16
+ readonly taskId: string,
17
+ ) {
18
+ super(`RunSuspendedError: run ${runId} suspended on task ${taskId}`);
19
+ this.name = "RunSuspendedError";
20
+ }
21
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,35 @@
1
1
  export { SystemClock, type Clock } from "./contracts/Clock";
2
+ export {
3
+ SuspensionRequest,
4
+ type HumanTaskHandle,
5
+ type HumanTaskSubject,
6
+ type HumanTaskActor,
7
+ type HumanTaskId,
8
+ type Duration,
9
+ type ResumeContext,
10
+ } from "./contracts/runtimeTypes";
11
+ export type { PersistedSuspensionEntry, PendingResumeEntry, RunHaltReason } from "./contracts/runTypes";
12
+ export type { HumanTaskRecord, HumanTaskStatus, HumanTaskStore } from "./contracts/humanTaskStoreTypes";
13
+ export { HumanTaskStoreToken } from "./contracts/humanTaskStoreTypes";
14
+ export type { HitlResumeTokenSignerSeam, HitlTimeoutJobSchedulerSeam } from "./contracts/hitlSeamTypes";
15
+ export {
16
+ HitlResumeTokenSignerToken,
17
+ HitlTimeoutJobSchedulerToken,
18
+ HitlWorkspaceIdToken,
19
+ } from "./contracts/hitlSeamTypes";
20
+ export type {
21
+ InboxChannel,
22
+ InboxChannelResolverSeam,
23
+ InboxDeliverArgs,
24
+ InboxDelivery,
25
+ InboxOnDecisionArgs,
26
+ InboxOnTimeoutArgs,
27
+ } from "./contracts/inboxChannelTypes";
28
+ export {
29
+ InboxChannelResolverToken,
30
+ LocalInboxChannelToken,
31
+ ControlPlaneInboxChannelToken,
32
+ } from "./contracts/inboxChannelTypes";
2
33
  export * from "./authoring";
3
34
  export * from "./ai/AiHost";
4
35
  export { AgentConnectionNodeCollector } from "./ai/AgentConnectionNodeCollector";
@@ -44,3 +75,12 @@ export type {
44
75
  OAuthMaterial,
45
76
  OAuthFlowExecutor,
46
77
  } from "./credentials/OAuthFlowExecutor.types";
78
+ export type {
79
+ CredentialMaterialProvider,
80
+ CredentialMaterialRef,
81
+ MaterialBundle,
82
+ CallerContext,
83
+ } from "./credentials/CredentialMaterialProvider.types";
84
+ export { IllegalMaterialSourceError } from "./credentials/CredentialMaterialProvider.types";
85
+ export { ManagedCredentialMaterialWriteError } from "./credentials/ManagedCredentialMaterialWriteError";
86
+ export { ManagedMaterialFetchError } from "./credentials/ManagedMaterialFetchError";
@@ -11,6 +11,7 @@ import type {
11
11
  NodeOutputs,
12
12
  ParentExecutionRef,
13
13
  PersistedWorkflowTokenRegistryLike,
14
+ ResumeContext,
14
15
  RunExecutionOptions,
15
16
  RunId,
16
17
  RunResult,
@@ -77,8 +78,9 @@ interface EngineRunContinuationService {
77
78
  nodeId: NodeId;
78
79
  error: Error;
79
80
  }): Promise<RunResult>;
80
- waitForCompletion(runId: RunId): Promise<Extract<RunResult, { status: "completed" | "failed" }>>;
81
+ waitForCompletion(runId: RunId): Promise<Extract<RunResult, { status: "completed" | "failed" | "halted" }>>;
81
82
  waitForWebhookResponse(runId: RunId): Promise<WebhookRunResult>;
83
+ resumeRun(args: { runId: RunId; taskId: string; resumeContext: ResumeContext }): Promise<RunResult>;
82
84
  }
83
85
 
84
86
  interface EngineNodeExecutionRequestHandler {
@@ -226,7 +228,7 @@ export class Engine implements NodeActivationContinuation, NodeExecutionRequestH
226
228
  return await this.deps.runContinuationService.resumeFromStepError(args);
227
229
  }
228
230
 
229
- async waitForCompletion(runId: RunId): Promise<Extract<RunResult, { status: "completed" | "failed" }>> {
231
+ async waitForCompletion(runId: RunId): Promise<Extract<RunResult, { status: "completed" | "failed" | "halted" }>> {
230
232
  return await this.deps.runContinuationService.waitForCompletion(runId);
231
233
  }
232
234
 
@@ -234,6 +236,14 @@ export class Engine implements NodeActivationContinuation, NodeExecutionRequestH
234
236
  return await this.deps.runContinuationService.waitForWebhookResponse(runId);
235
237
  }
236
238
 
239
+ /**
240
+ * Re-activate a suspended run item with a human decision (HITL).
241
+ * The HTTP resume endpoint calls this; this method exposes the engine primitive.
242
+ */
243
+ async resumeRun(args: { runId: RunId; taskId: string; resumeContext: ResumeContext }): Promise<RunResult> {
244
+ return await this.deps.runContinuationService.resumeRun(args);
245
+ }
246
+
237
247
  async handleNodeExecutionRequest(request: NodeExecutionRequest): Promise<void> {
238
248
  await this.deps.nodeExecutionRequestHandler.handleNodeExecutionRequest(request);
239
249
  }
@@ -21,7 +21,7 @@ export class EngineWaiters {
21
21
  }
22
22
 
23
23
  resolveRunCompletion(result: RunResult): void {
24
- if (result.status !== "completed" && result.status !== "failed") return;
24
+ if (result.status !== "completed" && result.status !== "failed" && result.status !== "halted") return;
25
25
  const list = this.completionWaiters.get(result.runId);
26
26
  if (!list || list.length === 0) return;
27
27
  this.completionWaiters.delete(result.runId);
@@ -4,12 +4,14 @@ import type {
4
4
  NodeExecutionRequest,
5
5
  NodeExecutionRequestHandler,
6
6
  PersistedRunState,
7
+ ResumeContext,
7
8
  RunDataFactory,
8
9
  WorkflowDefinition,
9
10
  WorkflowExecutionRepository,
10
11
  WorkflowSnapshotResolver,
11
12
  } from "../types";
12
13
  import type { EngineExecutionLimitsPolicy } from "../policies/executionLimits/EngineExecutionLimitsPolicy";
14
+ import { RunSuspendedError } from "../execution/RunSuspendedError";
13
15
  import { NodeActivationRequestComposer } from "../execution/NodeActivationRequestComposer";
14
16
  import { NodeRunStateWriterFactory } from "../execution/NodeRunStateWriterFactory";
15
17
  import { WorkflowRunExecutionContextFactory } from "../execution/WorkflowRunExecutionContextFactory";
@@ -84,6 +86,14 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
84
86
  const portKeys = Object.keys(inputsByPort);
85
87
  const kind = portKeys.length === 1 && portKeys[0] === "in" ? ("single" as const) : ("multi" as const);
86
88
  const batchId = pendingExecution.batchId ?? "batch_1";
89
+ // Splice resumeContext from pendingResume if this activation is a HITL resume.
90
+ const pendingResume = state.pendingResume;
91
+ const resumeContext: ResumeContext | undefined =
92
+ pendingResume?.activationId === request.activationId && pendingResume?.nodeId === request.nodeId
93
+ ? (pendingResume.resumeContext as ResumeContext)
94
+ : undefined;
95
+ const baseWithResume = resumeContext != null ? { ...base, resumeContext } : base;
96
+
87
97
  const activationRequest =
88
98
  kind === "multi"
89
99
  ? this.nodeActivationRequestComposer.createMultiFromDefinitionWithActivation({
@@ -92,7 +102,7 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
92
102
  workflowId: request.workflowId,
93
103
  parent: resolvedParent,
94
104
  executionOptions: request.executionOptions ?? state.executionOptions,
95
- base,
105
+ base: baseWithResume,
96
106
  data,
97
107
  definition: {
98
108
  id: definition.id,
@@ -107,7 +117,7 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
107
117
  workflowId: request.workflowId,
108
118
  parent: resolvedParent,
109
119
  executionOptions: request.executionOptions ?? state.executionOptions,
110
- base,
120
+ base: baseWithResume,
111
121
  data,
112
122
  definition: {
113
123
  id: definition.id,
@@ -117,6 +127,14 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
117
127
  input: inputsByPort.in ?? request.input ?? [],
118
128
  });
119
129
 
130
+ // Clear pendingResume from state now that we have consumed it.
131
+ if (resumeContext != null) {
132
+ const clearedState = await this.workflowExecutionRepository.load(request.runId);
133
+ if (clearedState?.pendingResume?.activationId === request.activationId) {
134
+ await this.workflowExecutionRepository.save({ ...clearedState, pendingResume: undefined });
135
+ }
136
+ }
137
+
120
138
  await this.continuation.markNodeRunning({
121
139
  runId: activationRequest.runId,
122
140
  activationId: activationRequest.activationId,
@@ -128,6 +146,11 @@ export class NodeExecutionRequestHandlerService implements NodeExecutionRequestH
128
146
  try {
129
147
  outputs = await this.nodeExecutor.execute(activationRequest);
130
148
  } catch (error) {
149
+ if (error instanceof RunSuspendedError) {
150
+ // The node threw SuspensionRequest; NodeSuspensionHandler already persisted the
151
+ // suspension entry and flipped status to "suspended". Nothing more to do here.
152
+ return;
153
+ }
131
154
  await this.resumeAfterExecutionError(activationRequest, this.asError(error));
132
155
  return;
133
156
  }