@codemation/core 0.11.1 → 0.13.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 (93) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/{CostCatalogContract-DZgcUBE4.d.cts → CostCatalogContract-Dxq1BTyi.d.cts} +2 -2
  3. package/dist/{EngineRuntimeRegistration.types-Cggm5GVY.d.cts → EngineRuntimeRegistration.types-CqcTWexS.d.cts} +3 -3
  4. package/dist/{EngineRuntimeRegistration.types-BQbS9_gs.d.ts → EngineRuntimeRegistration.types-Cr75cSfL.d.ts} +2 -2
  5. package/dist/InMemoryRunDataFactory-Csy2evr_.d.cts +205 -0
  6. package/dist/{ItemsInputNormalizer-CwdOhSAK.cjs → ItemsInputNormalizer-57EdA1ad.cjs} +2 -2
  7. package/dist/{ItemsInputNormalizer-CwdOhSAK.cjs.map → ItemsInputNormalizer-57EdA1ad.cjs.map} +1 -1
  8. package/dist/{ItemsInputNormalizer-_Mfcd3YU.d.ts → ItemsInputNormalizer-BWtlwdVI.d.ts} +2 -2
  9. package/dist/{ItemsInputNormalizer-D-MH8MBs.js → ItemsInputNormalizer-BkSvmfAW.js} +2 -2
  10. package/dist/{ItemsInputNormalizer-D-MH8MBs.js.map → ItemsInputNormalizer-BkSvmfAW.js.map} +1 -1
  11. package/dist/{ItemsInputNormalizer-C_dpn76M.d.cts → ItemsInputNormalizer-pLrWwUAP.d.cts} +3 -3
  12. package/dist/{RunIntentService-CEF-sFfI.d.cts → RunIntentService-BitgkKaT.d.cts} +18 -4
  13. package/dist/{RunIntentService-BVur7x9n.d.ts → RunIntentService-DYpqfu6D.d.ts} +18 -4
  14. package/dist/{agentMcpTypes-ZiNbNsEi.d.cts → agentMcpTypes-DGIwk6Ue.d.cts} +201 -4
  15. package/dist/bootstrap/index.cjs +3 -3
  16. package/dist/bootstrap/index.d.cts +63 -7
  17. package/dist/bootstrap/index.d.ts +5 -5
  18. package/dist/bootstrap/index.js +3 -3
  19. package/dist/{bootstrap-BxuTFTLB.cjs → bootstrap-BEu1fJBM.cjs} +175 -4
  20. package/dist/bootstrap-BEu1fJBM.cjs.map +1 -0
  21. package/dist/{bootstrap-D_Yyi0wL.js → bootstrap-CSeInbj1.js} +173 -4
  22. package/dist/bootstrap-CSeInbj1.js.map +1 -0
  23. package/dist/browser.cjs +4 -2
  24. package/dist/browser.d.cts +4 -4
  25. package/dist/browser.d.ts +3 -3
  26. package/dist/browser.js +3 -3
  27. package/dist/contracts.d.cts +5 -5
  28. package/dist/contracts.d.ts +2 -2
  29. package/dist/{di-BlEKdoZS.cjs → di-C-2ep8NZ.cjs} +44 -1
  30. package/dist/di-C-2ep8NZ.cjs.map +1 -0
  31. package/dist/{di-0Wop7z1y.js → di-D9Mv3kF3.js} +33 -2
  32. package/dist/di-D9Mv3kF3.js.map +1 -0
  33. package/dist/{executionPersistenceContracts-BgZMRsTa.d.cts → executionPersistenceContracts-CN9d7AnL.d.cts} +2 -2
  34. package/dist/{index-62Ba9f7D.d.ts → index-CqZeNGAp.d.ts} +343 -101
  35. package/dist/{index-zWGtEhrf.d.ts → index-rllWL4r-.d.ts} +459 -5
  36. package/dist/index.cjs +91 -161
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.cts +458 -97
  39. package/dist/index.d.ts +5 -5
  40. package/dist/index.js +74 -159
  41. package/dist/index.js.map +1 -1
  42. package/dist/{params-B5SENSzZ.d.cts → params-DRUr0F5v.d.cts} +2 -2
  43. package/dist/{runtime-cxmUkk0l.js → runtime-6-U2Cou5.js} +690 -18
  44. package/dist/runtime-6-U2Cou5.js.map +1 -0
  45. package/dist/{runtime-DBzq5YBi.cjs → runtime-DjYXgOo0.cjs} +749 -17
  46. package/dist/runtime-DjYXgOo0.cjs.map +1 -0
  47. package/dist/testing.cjs +3 -3
  48. package/dist/testing.d.cts +3 -3
  49. package/dist/testing.d.ts +3 -3
  50. package/dist/testing.js +3 -3
  51. package/package.json +1 -1
  52. package/src/authoring/defineHumanApprovalNode.types.ts +379 -0
  53. package/src/authoring/index.ts +6 -0
  54. package/src/binaries/DefaultExecutionBinaryServiceFactory.ts +27 -2
  55. package/src/binaries/DefaultNodeBinaryAttachmentServiceFactory.ts +14 -0
  56. package/src/binaries/boundedReadBinary.types.ts +90 -0
  57. package/src/bootstrap/runtime/EngineRuntimeRegistrar.ts +29 -0
  58. package/src/contracts/CodemationTelemetryAttributeNames.ts +10 -0
  59. package/src/contracts/credentialTypes.ts +10 -0
  60. package/src/contracts/hitlSeamTypes.ts +34 -0
  61. package/src/contracts/humanTaskStoreTypes.ts +48 -0
  62. package/src/contracts/inboxChannelTypes.ts +58 -0
  63. package/src/contracts/index.ts +3 -0
  64. package/src/contracts/runTypes.ts +61 -3
  65. package/src/contracts/runtimeTypes.ts +131 -0
  66. package/src/contracts/workspaceFileTypes.ts +73 -0
  67. package/src/credentials/CredentialMaterialProvider.types.ts +61 -0
  68. package/src/credentials/ManagedCredentialMaterialWriteError.ts +14 -0
  69. package/src/credentials/ManagedMaterialFetchError.ts +16 -0
  70. package/src/execution/ActivationEnqueueService.ts +16 -0
  71. package/src/execution/DefaultExecutionContextFactory.ts +11 -0
  72. package/src/execution/NodeExecutionSnapshotFactory.ts +7 -1
  73. package/src/execution/NodeExecutor.ts +60 -1
  74. package/src/execution/NodeExecutorFactory.ts +12 -2
  75. package/src/execution/NodeSuspensionHandler.ts +220 -0
  76. package/src/execution/PersistedRunStateTerminalBuilder.ts +5 -2
  77. package/src/execution/RunStateSemantics.ts +5 -0
  78. package/src/execution/RunSuspendedError.ts +21 -0
  79. package/src/index.ts +42 -0
  80. package/src/orchestration/Engine.ts +12 -2
  81. package/src/orchestration/EngineWaiters.ts +1 -1
  82. package/src/orchestration/NodeExecutionRequestHandlerService.ts +25 -2
  83. package/src/orchestration/RunContinuationService.ts +226 -2
  84. package/src/orchestration/TestSuiteOrchestrator.ts +5 -4
  85. package/src/runtime/RunIntentService.ts +3 -0
  86. package/src/workflow/dsl/ChainCursorResolver.ts +36 -0
  87. package/dist/InMemoryRunDataFactory-C7YItvHG.d.cts +0 -123
  88. package/dist/bootstrap-BxuTFTLB.cjs.map +0 -1
  89. package/dist/bootstrap-D_Yyi0wL.js.map +0 -1
  90. package/dist/di-0Wop7z1y.js.map +0 -1
  91. package/dist/di-BlEKdoZS.cjs.map +0 -1
  92. package/dist/runtime-DBzq5YBi.cjs.map +0 -1
  93. package/dist/runtime-cxmUkk0l.js.map +0 -1
@@ -1,8 +1,36 @@
1
- import { S as resolveItemExprsForExecution, _ as AgentConnectionNodeCollector, a as injectable, g as NodeIterationIdFactory, h as CredentialUnboundError, w as ConnectionNodeIdFactory, y as AgentConfigInspector } from "./di-0Wop7z1y.js";
1
+ import { E as ConnectionNodeIdFactory, _ as CredentialUnboundError, a as injectable, g as SuspensionRequest, h as BINARY_DEFAULT_MAX_BYTES, v as NodeIterationIdFactory, w as resolveItemExprsForExecution, x as AgentConfigInspector, y as AgentConnectionNodeCollector } from "./di-D9Mv3kF3.js";
2
2
  import { ZodError, z } from "zod";
3
3
  import { ReadableStream } from "node:stream/web";
4
4
  import { createHash } from "node:crypto";
5
5
 
6
+ //#region src/contracts/humanTaskStoreTypes.ts
7
+ const HumanTaskStoreToken = Symbol.for("codemation.core.HumanTaskStore");
8
+
9
+ //#endregion
10
+ //#region src/contracts/hitlSeamTypes.ts
11
+ const HitlResumeTokenSignerToken = Symbol.for("codemation.core.HitlResumeTokenSigner");
12
+ const HitlTimeoutJobSchedulerToken = Symbol.for("codemation.core.HitlTimeoutJobScheduler");
13
+ /**
14
+ * Optional workspace ID injected into NodeSuspensionHandler in managed mode (T7 security fix).
15
+ * Allows the handler to stamp the workspaceId on each HumanTaskRecord so HitlCallbackHandler
16
+ * can assert workspace identity independently of the HMAC middleware.
17
+ * Not registered in non-managed mode; NodeSuspensionHandler defaults to null.
18
+ */
19
+ const HitlWorkspaceIdToken = Symbol.for("codemation.core.HitlWorkspaceId");
20
+
21
+ //#endregion
22
+ //#region src/authoring/DefinedNodeRegistry.ts
23
+ var DefinedNodeRegistry = class {
24
+ static definitions = /* @__PURE__ */ new Map();
25
+ static register(definition) {
26
+ this.definitions.set(definition.key, definition);
27
+ }
28
+ static resolve(key) {
29
+ return this.definitions.get(key);
30
+ }
31
+ };
32
+
33
+ //#endregion
6
34
  //#region src/runtime-types/persistedRuntimeTypeModelRegistry.ts
7
35
  /** Shared metadata key used to attach persisted runtime-type information to decorated classes. */
8
36
  const persistedRuntimeTypeMetadataKey = Symbol.for("codemation.core.persistedRuntimeTypeMetadata");
@@ -111,6 +139,305 @@ function chatModel(options = {}) {
111
139
  return InjectableRuntimeDecoratorComposer.compose("chatModel", options, import.meta.url);
112
140
  }
113
141
 
142
+ //#endregion
143
+ //#region src/authoring/defineNode.types.ts
144
+ const definedNodeCredentialRequirementFactory = {
145
+ create(bindings) {
146
+ if (!bindings) return [];
147
+ return Object.entries(bindings).map(([slotKey, binding]) => {
148
+ if (typeof binding === "string" || this.isCredentialType(binding)) return {
149
+ slotKey,
150
+ label: this.humanize(slotKey),
151
+ acceptedTypes: [this.resolveTypeId(binding)]
152
+ };
153
+ const types = Array.isArray(binding.type) ? binding.type : [binding.type];
154
+ return {
155
+ slotKey,
156
+ label: binding.label ?? this.humanize(slotKey),
157
+ acceptedTypes: types.map((entry) => this.resolveTypeId(entry)),
158
+ optional: binding.optional,
159
+ helpText: binding.helpText,
160
+ helpUrl: binding.helpUrl
161
+ };
162
+ });
163
+ },
164
+ isCredentialType(value) {
165
+ return Boolean(value) && typeof value === "object" && "definition" in value && typeof value.definition?.typeId === "string";
166
+ },
167
+ resolveTypeId(type) {
168
+ return typeof type === "string" ? type : type.definition.typeId;
169
+ },
170
+ humanize(key) {
171
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[-_.]+/g, " ").replace(/\s+/g, " ").trim().replace(/^./, (character) => character.toUpperCase());
172
+ }
173
+ };
174
+ const definedNodeCredentialAccessorFactory = { create(bindings, ctx) {
175
+ if (!bindings) return {};
176
+ const entries = Object.keys(bindings).map((slotKey) => [slotKey, () => ctx.getCredential(slotKey)]);
177
+ return Object.fromEntries(entries);
178
+ } };
179
+ function defineNode(options) {
180
+ const credentialRequirements = definedNodeCredentialRequirementFactory.create(options.credentials);
181
+ const DefinedNodeRuntime = class {
182
+ kind = "node";
183
+ outputPorts = ["main"];
184
+ inputSchema = options.inputSchema;
185
+ async execute(args) {
186
+ const ctx = args.ctx;
187
+ const context = {
188
+ config: ctx.config.config,
189
+ credentials: definedNodeCredentialAccessorFactory.create(options.credentials, ctx),
190
+ execution: ctx
191
+ };
192
+ const payload = {
193
+ input: args.input,
194
+ item: args.item,
195
+ itemIndex: args.itemIndex,
196
+ items: args.items,
197
+ ctx
198
+ };
199
+ return await options.execute(payload, context);
200
+ }
201
+ };
202
+ node({ name: options.key })(DefinedNodeRuntime);
203
+ const DefinedRunnableNodeConfig = class {
204
+ kind = "node";
205
+ type = DefinedNodeRuntime;
206
+ icon = options.icon;
207
+ inputSchema = options.inputSchema;
208
+ keepBinaries = options.keepBinaries ?? false;
209
+ constructor(name, config, id) {
210
+ this.name = name;
211
+ this.id = id;
212
+ this.config = config;
213
+ }
214
+ config;
215
+ getCredentialRequirements() {
216
+ return credentialRequirements;
217
+ }
218
+ inspectorSummary() {
219
+ return options.inspectorSummary?.({ config: this.config });
220
+ }
221
+ };
222
+ const definition = {
223
+ kind: "defined-node",
224
+ key: options.key,
225
+ title: options.title,
226
+ description: options.description,
227
+ create(config, name = options.title, id) {
228
+ return new DefinedRunnableNodeConfig(name, config, id);
229
+ },
230
+ register(context) {
231
+ context.registerNode(DefinedNodeRuntime);
232
+ }
233
+ };
234
+ DefinedNodeRegistry.register(definition);
235
+ return definition;
236
+ }
237
+ function defineBatchNode(options) {
238
+ const credentialRequirements = definedNodeCredentialRequirementFactory.create(options.credentials);
239
+ const DefinedNodeRuntime = class {
240
+ kind = "node";
241
+ outputPorts = ["main"];
242
+ async execute(args) {
243
+ if (args.itemIndex !== args.items.length - 1) return [];
244
+ const ctx = args.ctx;
245
+ const context = {
246
+ config: ctx.config.config,
247
+ credentials: definedNodeCredentialAccessorFactory.create(options.credentials, ctx),
248
+ execution: ctx
249
+ };
250
+ return [...await options.run(args.items.map((item) => item.json), context)];
251
+ }
252
+ };
253
+ node({ name: options.key })(DefinedNodeRuntime);
254
+ const DefinedRunnableNodeConfig = class {
255
+ kind = "node";
256
+ type = DefinedNodeRuntime;
257
+ icon = options.icon;
258
+ constructor(name, config, id) {
259
+ this.name = name;
260
+ this.id = id;
261
+ this.config = config;
262
+ }
263
+ config;
264
+ getCredentialRequirements() {
265
+ return credentialRequirements;
266
+ }
267
+ inspectorSummary() {
268
+ return options.inspectorSummary?.({ config: this.config });
269
+ }
270
+ };
271
+ const definition = {
272
+ kind: "defined-node",
273
+ key: options.key,
274
+ title: options.title,
275
+ description: options.description,
276
+ create(config, name = options.title, id) {
277
+ return new DefinedRunnableNodeConfig(name, config, id);
278
+ },
279
+ register(context) {
280
+ context.registerNode(DefinedNodeRuntime);
281
+ }
282
+ };
283
+ DefinedNodeRegistry.register(definition);
284
+ return definition;
285
+ }
286
+
287
+ //#endregion
288
+ //#region src/authoring/defineHumanApprovalNode.types.ts
289
+ /**
290
+ * Returns `true` when `node` was created by {@link defineHumanApprovalNode}.
291
+ * Uses the `humanApprovalToolBehavior` typed field as the discriminant.
292
+ */
293
+ function isHumanApprovalNode(node$1) {
294
+ return typeof node$1 === "object" && node$1 !== null && "humanApprovalToolBehavior" in node$1 && typeof node$1.humanApprovalToolBehavior === "object";
295
+ }
296
+ /**
297
+ * Authoring helper that compiles a HITL approval channel down to a regular
298
+ * {@link defineNode}-backed node with `SuspensionRequest` semantics.
299
+ *
300
+ * **Fast-forward decision semantics:**
301
+ * - On the first `execute` call (no `ctx.resumeContext`): throws a `SuspensionRequest`
302
+ * that calls the author's `deliver`. The engine persists the suspension and continues.
303
+ * - On resume (`ctx.resumeContext` set): calls `onDecision`/`onTimeout` as appropriate,
304
+ * merges a `decision` key into `item.json`, and returns an item with the original
305
+ * `binary` map passed by reference (no copy).
306
+ *
307
+ * **Output shape per item:**
308
+ * ```ts
309
+ * // Input: { json: { invoiceId: 42 }, binary?: {...} }
310
+ * // Output: { json: { invoiceId: 42, decision: { status: "approved", actor, decidedAt } }, binary: <unchanged> }
311
+ * ```
312
+ * If `item.json` already has a `decision` key it is **overwritten**. Namespace as
313
+ * needed if your schema reserves that key for another purpose.
314
+ *
315
+ * **Predicate persistence:**
316
+ * The `approvedPredicate` function is NOT serialized to the suspension record (except
317
+ * as an audit-only string via `toString()`). On resume, the workflow definition is
318
+ * reloaded from code at process start and the predicate closure is rebuilt naturally.
319
+ * If a deploy ships a changed predicate between suspend and resume, the *new* predicate
320
+ * runs — document this in your runbook when the predicate carries business logic that
321
+ * may change across deploys.
322
+ *
323
+ * @example
324
+ * ```ts
325
+ * export const slackApprovalNode = defineHumanApprovalNode({
326
+ * key: "my-plugin.slackApproval",
327
+ * title: "Slack Approval",
328
+ * channel: "slack",
329
+ * configSchema: z.object({ channel: z.string(), message: z.string() }),
330
+ * decisionSchema: z.object({ approved: z.boolean(), note: z.string().optional() }),
331
+ *
332
+ * async deliver({ task, config, item }, ctx) {
333
+ * const ts = await postSlackMessage(config.channel, `Approve? <${task.resumeUrl}>`);
334
+ * return { channel: config.channel, ts };
335
+ * },
336
+ *
337
+ * async onDecision({ decision, actor, delivery }, ctx) {
338
+ * await updateSlackMessage(delivery.channel, delivery.ts, decision.approved ? "✅" : "❌");
339
+ * },
340
+ * });
341
+ * ```
342
+ */
343
+ function defineHumanApprovalNode(opts) {
344
+ const resolvedPredicate = resolveApprovedPredicate(opts.decisionSchema, opts.approvedPredicate);
345
+ const timeout = opts.defaultTimeout ?? "24h";
346
+ const onTimeout = opts.defaultOnTimeout ?? "halt";
347
+ const inner = defineNode({
348
+ key: opts.key,
349
+ title: opts.title,
350
+ description: opts.description,
351
+ icon: opts.icon,
352
+ configSchema: opts.configSchema,
353
+ inputSchema: opts.inputSchema,
354
+ credentials: opts.credentials,
355
+ inspectorSummary: opts.inspectorSummary ? ({ config }) => opts.inspectorSummary(config) : void 0,
356
+ async execute(args, { config, execution: ctx }) {
357
+ if (!ctx.resumeContext) {
358
+ const subject = buildSubject(opts.title, args.item, ctx);
359
+ throw new SuspensionRequest({
360
+ decisionSchema: opts.decisionSchema,
361
+ timeout,
362
+ onTimeout,
363
+ subject,
364
+ metadata: {
365
+ channel: opts.channel,
366
+ nodeKey: opts.key,
367
+ approvedPredicateSource: opts.approvedPredicate?.toString() ?? null
368
+ },
369
+ deliver: (handle) => opts.deliver({
370
+ task: handle,
371
+ config,
372
+ input: args.input,
373
+ item: args.item
374
+ }, ctx)
375
+ });
376
+ }
377
+ return await handleResume(args.item, ctx.resumeContext, opts.decisionSchema, resolvedPredicate, opts.onDecision, opts.onTimeout, ctx);
378
+ }
379
+ });
380
+ return Object.assign(inner, { humanApprovalToolBehavior: { onRejected: "return" } });
381
+ }
382
+ function resolveApprovedPredicate(schema, predicate) {
383
+ if (predicate) return predicate;
384
+ const shape = schema.shape;
385
+ if (shape && typeof shape === "object" && "approved" in shape) return (d) => d.approved === true;
386
+ throw new Error("defineHumanApprovalNode: decisionSchema has no \"approved\" field and no approvedPredicate was provided. Either add { approved: z.boolean() } to the decision schema or supply approvedPredicate explicitly.");
387
+ }
388
+ function buildSubject(title, item, ctx) {
389
+ return {
390
+ title,
391
+ summary: "",
392
+ attributes: {
393
+ workflowId: ctx.workflowId,
394
+ nodeId: ctx.nodeId,
395
+ item: item.json
396
+ }
397
+ };
398
+ }
399
+ function mergeDecision(item, decision) {
400
+ return {
401
+ json: {
402
+ ...item.json,
403
+ decision
404
+ },
405
+ binary: item.binary,
406
+ meta: item.meta
407
+ };
408
+ }
409
+ async function handleResume(item, resumeContext, decisionSchema, resolvedPredicate, onDecision, onTimeoutCb, ctx) {
410
+ const { decision: dec, delivery, task } = resumeContext;
411
+ if (dec.kind === "timed_out" || dec.kind === "auto_accepted") {
412
+ const policy = dec.kind === "auto_accepted" ? "auto-accept" : "halt";
413
+ await onTimeoutCb?.({
414
+ task,
415
+ delivery,
416
+ item,
417
+ policy
418
+ }, ctx);
419
+ return mergeDecision(item, {
420
+ status: dec.kind === "auto_accepted" ? "auto-accepted" : "timed-out",
421
+ decidedAt: dec.at
422
+ });
423
+ }
424
+ const parsed = decisionSchema.parse(dec.value);
425
+ await onDecision?.({
426
+ decision: parsed,
427
+ actor: dec.actor,
428
+ task,
429
+ delivery,
430
+ item
431
+ }, ctx);
432
+ return mergeDecision(item, {
433
+ status: resolvedPredicate(parsed) ? "approved" : "rejected",
434
+ actor: dec.actor,
435
+ decidedAt: dec.decidedAt,
436
+ note: parsed.note,
437
+ payload: parsed
438
+ });
439
+ }
440
+
114
441
  //#endregion
115
442
  //#region src/workflow/dsl/WhenBuilder.ts
116
443
  var WhenBuilder = class WhenBuilder {
@@ -237,6 +564,25 @@ var ChainCursor = class ChainCursor {
237
564
  }
238
565
  return new ChainCursor(this.wf, nextEndpoints);
239
566
  }
567
+ /**
568
+ * Chainable shorthand for `.then(node.create(config, metadata?.name, metadata?.nodeId))`.
569
+ *
570
+ * Signals to readers that this step suspends the run and waits for a human decision.
571
+ * Throws at workflow-build time if `node` was not created via `defineHumanApprovalNode`.
572
+ *
573
+ * @example
574
+ * ```ts
575
+ * workflow
576
+ * .trigger(...)
577
+ * .humanApproval(inboxApproval, { title: "Approve?", body: "...", priority: "normal" })
578
+ * .then(nextStep.create(...))
579
+ * .build();
580
+ * ```
581
+ */
582
+ humanApproval(node$1, config, metadata) {
583
+ if (!isHumanApprovalNode(node$1)) throw new Error(`.humanApproval() requires a node created via defineHumanApprovalNode (got '${node$1.key ?? String(node$1)}').`);
584
+ return this.then(node$1.create(config, metadata?.name, metadata?.nodeId));
585
+ }
240
586
  build() {
241
587
  return this.wf.build();
242
588
  }
@@ -544,6 +890,58 @@ var NodeEventPublisher = class {
544
890
  }
545
891
  };
546
892
 
893
+ //#endregion
894
+ //#region src/binaries/boundedReadBinary.types.ts
895
+ /**
896
+ * Reads all bytes from an already-opened binary stream into a contiguous `Uint8Array`.
897
+ *
898
+ * Safety contract:
899
+ * - `attachment.size` is checked against `maxBytes` *before* any allocation (no OOM).
900
+ * - A single buffer of exactly `attachment.size` is pre-allocated; the stream fills it
901
+ * directly — no chunks array, no doubling.
902
+ * - A byte-count mismatch between the declared size and actual stream content is an error.
903
+ *
904
+ * This is the single canonical implementation; `ExecutionBinaryService.getBytes`,
905
+ * `getText`, and `getJson` all delegate here. The per-package `readBinaryBody` helpers
906
+ * in `core-nodes` and `core-nodes-ocr` have been removed in favour of this function.
907
+ */
908
+ async function boundedReadBinary(result, attachment, maxBytes = BINARY_DEFAULT_MAX_BYTES) {
909
+ if (attachment.size > maxBytes) throw new Error(`Binary attachment size ${attachment.size} bytes exceeds maxBytes ${maxBytes}. Raise the node's maxBytes setting if this document is expected to be larger.`);
910
+ const out = new Uint8Array(attachment.size);
911
+ const reader = result.body.getReader();
912
+ let offset = 0;
913
+ while (true) {
914
+ const { done, value } = await reader.read();
915
+ if (done) break;
916
+ if (!value) continue;
917
+ if (offset + value.byteLength > out.byteLength) throw new Error(`Binary stream produced more bytes than the attachment's declared size (${attachment.size}).`);
918
+ out.set(value, offset);
919
+ offset += value.byteLength;
920
+ }
921
+ if (offset !== out.byteLength) throw new Error(`Binary stream produced ${offset} bytes but attachment declared size ${attachment.size}.`);
922
+ return out;
923
+ }
924
+ /** Shared implementation of `getBytes` used by both binary-service classes. */
925
+ async function readBinaryAsBytes(storage, attachment, maxBytes) {
926
+ const result = await storage.openReadStream(attachment.storageKey);
927
+ if (!result) throw new Error("Binary attachment stream is unavailable.");
928
+ return boundedReadBinary(result, attachment, maxBytes);
929
+ }
930
+ /** Shared implementation of `getText` used by both binary-service classes. */
931
+ async function readBinaryAsText(storage, attachment, maxBytes) {
932
+ const bytes = await readBinaryAsBytes(storage, attachment, maxBytes);
933
+ return new TextDecoder().decode(bytes);
934
+ }
935
+ /** Shared implementation of `getJson` used by both binary-service classes. */
936
+ async function readBinaryAsJson(storage, attachment, maxBytes) {
937
+ const text = await readBinaryAsText(storage, attachment, maxBytes);
938
+ try {
939
+ return JSON.parse(text);
940
+ } catch (cause) {
941
+ throw new SyntaxError(`Binary attachment at storage key "${attachment.storageKey}" is not valid JSON: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
942
+ }
943
+ }
944
+
547
945
  //#endregion
548
946
  //#region src/binaries/DefaultNodeBinaryAttachmentServiceFactory.ts
549
947
  var DefaultNodeBinaryAttachmentService = class DefaultNodeBinaryAttachmentService {
@@ -594,6 +992,15 @@ var DefaultNodeBinaryAttachmentService = class DefaultNodeBinaryAttachmentServic
594
992
  async openReadStream(attachment) {
595
993
  return await this.storage.openReadStream(attachment.storageKey);
596
994
  }
995
+ async getBytes(attachment, maxBytes) {
996
+ return readBinaryAsBytes(this.storage, attachment, maxBytes);
997
+ }
998
+ async getText(attachment, maxBytes) {
999
+ return readBinaryAsText(this.storage, attachment, maxBytes);
1000
+ }
1001
+ async getJson(attachment, maxBytes) {
1002
+ return readBinaryAsJson(this.storage, attachment, maxBytes);
1003
+ }
597
1004
  createAttachmentId() {
598
1005
  return DefaultNodeBinaryAttachmentService.createAttachmentIdValue(`${this.activationId}-${this.now().getTime()}`);
599
1006
  }
@@ -657,8 +1064,24 @@ var DefaultExecutionBinaryService = class {
657
1064
  forNode(args) {
658
1065
  return new DefaultNodeBinaryAttachmentService(this.storage, this.workflowId, this.runId, args.nodeId, args.activationId, this.now);
659
1066
  }
660
- async openReadStream(attachment) {
661
- return await this.storage.openReadStream(attachment.storageKey);
1067
+ openReadStream(attachment) {
1068
+ return this.storage.openReadStream(attachment.storageKey);
1069
+ }
1070
+ async getBytes(attachment, maxBytes) {
1071
+ const stream = await this.openReadStream(attachment);
1072
+ if (!stream) throw new Error("Binary attachment stream is unavailable.");
1073
+ return boundedReadBinary(stream, attachment, maxBytes);
1074
+ }
1075
+ async getText(attachment, maxBytes) {
1076
+ return new TextDecoder().decode(await this.getBytes(attachment, maxBytes));
1077
+ }
1078
+ async getJson(attachment, maxBytes) {
1079
+ const text = await this.getText(attachment, maxBytes);
1080
+ try {
1081
+ return JSON.parse(text);
1082
+ } catch (cause) {
1083
+ throw new SyntaxError(`Binary attachment at storage key "${attachment.storageKey}" is not valid JSON: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
1084
+ }
662
1085
  }
663
1086
  };
664
1087
 
@@ -703,7 +1126,7 @@ var NodeExecutionSnapshotFactory = class {
703
1126
  nodeId: args.nodeId,
704
1127
  activationId: args.activationId,
705
1128
  parent: args.parent,
706
- status: "completed",
1129
+ status: args.hitlStatus ?? "completed",
707
1130
  queuedAt: args.previous?.queuedAt,
708
1131
  startedAt,
709
1132
  finishedAt: args.finishedAt,
@@ -830,7 +1253,9 @@ var ActivationEnqueueService = class {
830
1253
  nodeSnapshotsByNodeId: {
831
1254
  ...args.previousNodeSnapshotsByNodeId,
832
1255
  [args.request.nodeId]: queuedSnapshot
833
- }
1256
+ },
1257
+ ...args.suspension !== void 0 ? { suspension: args.suspension } : {},
1258
+ ...args.pendingResume !== void 0 ? { pendingResume: args.pendingResume } : {}
834
1259
  });
835
1260
  await this.dispatchPreparedActivation(preparedDispatch);
836
1261
  return {
@@ -1297,6 +1722,16 @@ var CodemationTelemetryAttributeNames = class {
1297
1722
  static mcpServerId = "mcp.server_id";
1298
1723
  /** MCP tool name on spans created for callTool invocations. */
1299
1724
  static mcpToolName = "mcp.tool_name";
1725
+ /** Terminal node-execution status (e.g. `"hitl-approved"`, `"hitl-rejected"`) on HITL outcome spans. */
1726
+ static nodeExecutionStatus = "codemation.node.execution_status";
1727
+ /** Populated on run-halted spans; discriminates the halt reason (e.g. `"hitl-rejected"`). */
1728
+ static runHaltReason = "codemation.run.halt_reason";
1729
+ /** Human task ID on `hitl.task.*` span events. */
1730
+ static hitlTaskId = "codemation.hitl.task_id";
1731
+ /** HITL channel name (e.g. `"inbox"`, `"control-plane-inbox"`) on `hitl.task.*` span events. */
1732
+ static hitlChannel = "codemation.hitl.channel";
1733
+ /** Decision outcome (e.g. `"approved"`, `"rejected"`) on `hitl.task.decided` span events. */
1734
+ static hitlDecisionStatus = "codemation.hitl.decision_status";
1300
1735
  };
1301
1736
 
1302
1737
  //#endregion
@@ -1395,12 +1830,13 @@ var ExecutionTelemetryCostTrackingDecoratorFactory = class {
1395
1830
  //#region src/execution/DefaultExecutionContextFactory.ts
1396
1831
  var DefaultExecutionContextFactory = class {
1397
1832
  telemetryDecoratorFactory = new ExecutionTelemetryCostTrackingDecoratorFactory();
1398
- constructor(binaryStorage = new UnavailableBinaryStorage(), telemetryFactory = new NoOpExecutionTelemetryFactory(), costTrackingFactory = new NoOpCostTrackingTelemetryFactory(), currentDate = () => /* @__PURE__ */ new Date(), collections) {
1833
+ constructor(binaryStorage = new UnavailableBinaryStorage(), telemetryFactory = new NoOpExecutionTelemetryFactory(), costTrackingFactory = new NoOpCostTrackingTelemetryFactory(), currentDate = () => /* @__PURE__ */ new Date(), collections, nodeResolver) {
1399
1834
  this.binaryStorage = binaryStorage;
1400
1835
  this.telemetryFactory = telemetryFactory;
1401
1836
  this.costTrackingFactory = costTrackingFactory;
1402
1837
  this.currentDate = currentDate;
1403
1838
  this.collections = collections;
1839
+ this.nodeResolver = nodeResolver;
1404
1840
  }
1405
1841
  create(args) {
1406
1842
  const baseTelemetry = args.telemetry ?? this.telemetryFactory.create({
@@ -1427,7 +1863,11 @@ var DefaultExecutionContextFactory = class {
1427
1863
  binary: new DefaultExecutionBinaryService(this.binaryStorage, args.workflowId, args.runId, this.currentDate),
1428
1864
  getCredential: args.getCredential,
1429
1865
  testContext: args.testContext,
1430
- collections: this.collections
1866
+ collections: this.collections,
1867
+ resolve: (token) => {
1868
+ if (!this.nodeResolver) throw new Error("ExecutionContext.resolve() is not available: no NodeResolver was provided to DefaultExecutionContextFactory.");
1869
+ return this.nodeResolver.resolve(token);
1870
+ }
1431
1871
  };
1432
1872
  }
1433
1873
  };
@@ -1769,6 +2209,27 @@ var NodeActivationRequestComposer = class {
1769
2209
  }
1770
2210
  };
1771
2211
 
2212
+ //#endregion
2213
+ //#region src/execution/RunSuspendedError.ts
2214
+ /**
2215
+ * Internal sentinel thrown by {@link NodeSuspensionHandler} after persisting a suspension
2216
+ * entry. `NodeExecutionRequestHandlerService` catches this specifically and returns cleanly —
2217
+ * no continuation call, preventing `resumeFromNodeResult` / `resumeFromNodeError` from
2218
+ * overwriting the `"suspended"` run status.
2219
+ *
2220
+ * The `Error` suffix satisfies the ESLint `no-manual-di-new` allowlist. This is NOT a
2221
+ * user-facing error — it is an engine-internal control-flow primitive and should NOT be
2222
+ * exported from the public barrel.
2223
+ */
2224
+ var RunSuspendedError = class extends Error {
2225
+ constructor(runId, taskId) {
2226
+ super(`RunSuspendedError: run ${runId} suspended on task ${taskId}`);
2227
+ this.runId = runId;
2228
+ this.taskId = taskId;
2229
+ this.name = "RunSuspendedError";
2230
+ }
2231
+ };
2232
+
1772
2233
  //#endregion
1773
2234
  //#region src/execution/NodeExecutor.ts
1774
2235
  var NodeExecutor = class {
@@ -1776,9 +2237,11 @@ var NodeExecutor = class {
1776
2237
  outputNormalizer = new NodeOutputNormalizer();
1777
2238
  itemExprResolver;
1778
2239
  outputBehaviorResolver;
1779
- constructor(nodeInstanceFactory, retryRunner, itemExprResolver, outputBehaviorResolver) {
2240
+ constructor(nodeInstanceFactory, retryRunner, itemExprResolver, outputBehaviorResolver, suspensionHandler, loadRunState) {
1780
2241
  this.nodeInstanceFactory = nodeInstanceFactory;
1781
2242
  this.retryRunner = retryRunner;
2243
+ this.suspensionHandler = suspensionHandler;
2244
+ this.loadRunState = loadRunState;
1782
2245
  this.itemExprResolver = itemExprResolver ?? new ItemExprResolver();
1783
2246
  this.outputBehaviorResolver = outputBehaviorResolver ?? new RunnableOutputBehaviorResolver();
1784
2247
  }
@@ -1878,6 +2341,7 @@ var NodeExecutor = class {
1878
2341
  });
1879
2342
  }
1880
2343
  const byPort = {};
2344
+ let hasSuspension = false;
1881
2345
  for (let i = 0; i < inputBatch.length; i++) {
1882
2346
  const item = inputBatch[i];
1883
2347
  this.assertItemJsonNotTopLevelArray(request.nodeId, item);
@@ -1896,7 +2360,28 @@ var NodeExecutor = class {
1896
2360
  items: inputBatch,
1897
2361
  ctx: iterationCtx
1898
2362
  };
1899
- const raw = await Promise.resolve(node$1.execute(args));
2363
+ let raw;
2364
+ try {
2365
+ raw = await Promise.resolve(node$1.execute(args));
2366
+ } catch (e) {
2367
+ if (e instanceof SuspensionRequest || e instanceof Error && e.name === "SuspensionRequest" && typeof e.request === "object") {
2368
+ if (!this.suspensionHandler || !this.loadRunState) throw new Error(`Node ${request.nodeId} threw SuspensionRequest but this NodeExecutor has no suspensionHandler configured.`, { cause: e });
2369
+ const state = await this.loadRunState(request.runId);
2370
+ if (!state) throw new Error(`NodeExecutor: run state not found for runId ${request.runId} during suspension`, { cause: e });
2371
+ await this.suspensionHandler.handle({
2372
+ runId: request.runId,
2373
+ nodeId: request.nodeId,
2374
+ activationId: request.activationId,
2375
+ itemIndex: i,
2376
+ suspensionRequest: e,
2377
+ state,
2378
+ telemetry: iterationCtx.telemetry
2379
+ });
2380
+ hasSuspension = true;
2381
+ continue;
2382
+ }
2383
+ throw e;
2384
+ }
1900
2385
  const normalized = this.outputNormalizer.normalizeExecuteResult({
1901
2386
  baseItem: item,
1902
2387
  raw,
@@ -1909,6 +2394,7 @@ var NodeExecutor = class {
1909
2394
  byPort[port] = list;
1910
2395
  }
1911
2396
  }
2397
+ if (hasSuspension) throw new RunSuspendedError(request.runId, "unknown");
1912
2398
  return byPort;
1913
2399
  }
1914
2400
  /** Use resolver ctx only when {@link NodeExecutionContext.config} is non-nullish. */
@@ -1935,8 +2421,8 @@ var NodeExecutor = class {
1935
2421
  //#endregion
1936
2422
  //#region src/execution/NodeExecutorFactory.ts
1937
2423
  var NodeExecutorFactory = class {
1938
- create(workflowNodeInstanceFactory, retryRunner, outputBehaviorResolver) {
1939
- return new NodeExecutor(workflowNodeInstanceFactory, retryRunner, void 0, outputBehaviorResolver);
2424
+ create(workflowNodeInstanceFactory, retryRunner, outputBehaviorResolver, suspensionHandler, loadRunState) {
2425
+ return new NodeExecutor(workflowNodeInstanceFactory, retryRunner, void 0, outputBehaviorResolver, suspensionHandler, loadRunState);
1940
2426
  }
1941
2427
  };
1942
2428
 
@@ -2529,6 +3015,7 @@ var PersistedRunStateTerminalBuilder = class {
2529
3015
  ...args.state,
2530
3016
  engineCounters: args.engineCounters,
2531
3017
  status: args.status,
3018
+ reason: args.reason,
2532
3019
  pending: void 0,
2533
3020
  queue: args.queue,
2534
3021
  outputsByNode: args.outputsByNode,
@@ -2811,6 +3298,7 @@ var RunContinuationService = class {
2811
3298
  });
2812
3299
  data.setOutputs(args.nodeId, args.outputs);
2813
3300
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
3301
+ const hitlResolution = this.resolveHitlStatus(args.outputs);
2814
3302
  const completedSnapshot = this.semantics.createFinishedSnapshot({
2815
3303
  workflow: wf,
2816
3304
  previous: state.nodeSnapshotsByNodeId?.[args.nodeId],
@@ -2821,8 +3309,41 @@ var RunContinuationService = class {
2821
3309
  parent: state.parent,
2822
3310
  finishedAt: completedAt,
2823
3311
  inputsByPort: pendingExecution.inputsByPort,
2824
- outputs: args.outputs
3312
+ outputs: args.outputs,
3313
+ hitlStatus: hitlResolution?.nodeStatus
2825
3314
  });
3315
+ if (hitlResolution?.halt) {
3316
+ const haltedState = this.persistedRunStateTerminalBuilder.mergeTerminal({
3317
+ state,
3318
+ engineCounters: state.engineCounters ?? { completedNodeActivations: 0 },
3319
+ status: "halted",
3320
+ reason: hitlResolution.reason,
3321
+ queue: [],
3322
+ outputsByNode: data.dump(),
3323
+ nodeSnapshotsByNodeId: {
3324
+ ...state.nodeSnapshotsByNodeId ?? {},
3325
+ [args.nodeId]: completedSnapshot
3326
+ },
3327
+ finishedAtIso: completedAt
3328
+ });
3329
+ await this.workflowExecutionRepository.save(haltedState);
3330
+ await this.nodeEventPublisher.publish("nodeCompleted", completedSnapshot);
3331
+ await this.terminalPersistence.maybeDeleteAfterTerminalState({
3332
+ workflow: wf,
3333
+ state: haltedState,
3334
+ finalStatus: "failed",
3335
+ finishedAt: completedAt
3336
+ });
3337
+ const result = {
3338
+ runId: state.runId,
3339
+ workflowId: state.workflowId,
3340
+ startedAt: state.startedAt,
3341
+ status: "halted",
3342
+ reason: hitlResolution.reason
3343
+ };
3344
+ this.waiters.resolveRunCompletion(result);
3345
+ return result;
3346
+ }
2826
3347
  const completedActivations = (state.engineCounters?.completedNodeActivations ?? 0) + 1;
2827
3348
  const engineCounters = { completedNodeActivations: completedActivations };
2828
3349
  const maxNodeActivations = state.executionOptions?.maxNodeActivations ?? this.executionLimitsPolicy.createRootExecutionOptions().maxNodeActivations;
@@ -3123,13 +3644,114 @@ var RunContinuationService = class {
3123
3644
  status: "failed",
3124
3645
  error: { message: "Run failed" }
3125
3646
  };
3647
+ if (existing?.status === "halted") return {
3648
+ runId: existing.runId,
3649
+ workflowId: existing.workflowId,
3650
+ startedAt: existing.startedAt,
3651
+ status: "halted",
3652
+ reason: existing.reason ?? "hitl-rejected"
3653
+ };
3126
3654
  const result = await this.waiters.waitForCompletion(runId);
3127
- if (result.status !== "completed" && result.status !== "failed") throw new Error(`Unexpected run completion status: ${result.status}`);
3655
+ if (result.status !== "completed" && result.status !== "failed" && result.status !== "halted") throw new Error(`Unexpected run completion status: ${result.status}`);
3128
3656
  return result;
3129
3657
  }
3130
3658
  async waitForWebhookResponse(runId) {
3131
3659
  return await this.waiters.waitForWebhookResponse(runId);
3132
3660
  }
3661
+ /**
3662
+ * Re-activate a previously suspended run item with a human decision.
3663
+ *
3664
+ * Called by the HITL resume endpoint. This method:
3665
+ * 1. Loads `PersistedRunState` and locates the suspension entry by `taskId`.
3666
+ * 2. Removes the entry from the `suspension` array; if empty, run stays `"suspended"` until
3667
+ * enqueue flips it to `"pending"`.
3668
+ * 3. Writes `pendingResume` onto the state so `NodeExecutionRequestHandlerService` can
3669
+ * splice `resumeContext` into the node's execution context.
3670
+ * 4. Reconstructs the original input from `outputsByNode` of the upstream node and
3671
+ * enqueues a new activation via `activationEnqueueService`.
3672
+ *
3673
+ * @throws if the run is not found, not suspended, or the `taskId` is unknown.
3674
+ */
3675
+ async resumeRun(args) {
3676
+ const state = await this.workflowExecutionRepository.load(args.runId);
3677
+ if (!state) throw new Error(`Unknown runId: ${args.runId}`);
3678
+ if (state.status !== "suspended") throw new Error(`Run ${args.runId} is not suspended (status: ${state.status})`);
3679
+ const suspensionEntry = (state.suspension ?? []).find((s) => s.taskId === args.taskId);
3680
+ if (!suspensionEntry) throw new Error(`No suspension entry with taskId "${args.taskId}" found on run ${args.runId}`);
3681
+ const wf = this.resolvePersistedWorkflow(state);
3682
+ if (!wf) throw new Error(`Unknown workflowId: ${state.workflowId}`);
3683
+ const { topology, planner } = this.planningFactory.create(wf);
3684
+ const def = topology.defsById.get(suspensionEntry.nodeId);
3685
+ if (!def || def.kind !== "node") throw new Error(`Node ${suspensionEntry.nodeId} is not a runnable node`);
3686
+ const data = this.runDataFactory.create(state.outputsByNode);
3687
+ const limits = this.resolveEngineLimitsFromState(state);
3688
+ const base = this.runExecutionContextFactory.create({
3689
+ runId: state.runId,
3690
+ workflowId: state.workflowId,
3691
+ nodeId: suspensionEntry.nodeId,
3692
+ parent: state.parent,
3693
+ policySnapshot: state.policySnapshot,
3694
+ subworkflowDepth: state.executionOptions?.subworkflowDepth ?? 0,
3695
+ engineMaxNodeActivations: limits.engineMaxNodeActivations,
3696
+ engineMaxSubworkflowDepth: limits.engineMaxSubworkflowDepth,
3697
+ data,
3698
+ nodeState: this.nodeStatePublisherFactory.create(state.runId, state.workflowId, state.parent),
3699
+ testContext: state.executionOptions?.testContext
3700
+ });
3701
+ const parentEdges = wf.edges.filter((e) => e.to.nodeId === suspensionEntry.nodeId);
3702
+ const parentNodeId = parentEdges[0]?.from.nodeId;
3703
+ const parentOutputPort = parentEdges[0]?.from.output ?? "main";
3704
+ const allParentItems = parentNodeId ? data.getOutputItems(parentNodeId, parentOutputPort) ?? [] : [];
3705
+ const resumeInput = allParentItems.length > suspensionEntry.itemIndex ? [allParentItems[suspensionEntry.itemIndex]] : allParentItems;
3706
+ const newActivationId = this.activationIdFactory.makeActivationId();
3707
+ const pendingResume = {
3708
+ activationId: newActivationId,
3709
+ nodeId: suspensionEntry.nodeId,
3710
+ resumeContext: args.resumeContext
3711
+ };
3712
+ const remainingSuspensions = (state.suspension ?? []).filter((s) => s.taskId !== args.taskId);
3713
+ const baseWithResume = {
3714
+ ...base,
3715
+ resumeContext: args.resumeContext
3716
+ };
3717
+ const batchId = `resume_${newActivationId}`;
3718
+ const request = this.nodeActivationRequestComposer.createSingleFromDefinitionWithActivation({
3719
+ activationId: newActivationId,
3720
+ runId: state.runId,
3721
+ workflowId: state.workflowId,
3722
+ parent: state.parent,
3723
+ executionOptions: state.executionOptions,
3724
+ base: baseWithResume,
3725
+ data,
3726
+ definition: {
3727
+ id: suspensionEntry.nodeId,
3728
+ config: def.config
3729
+ },
3730
+ batchId,
3731
+ input: resumeInput
3732
+ });
3733
+ const { result, queuedSnapshot } = await this.activationEnqueueService.enqueueActivationWithSnapshot({
3734
+ runId: state.runId,
3735
+ workflowId: state.workflowId,
3736
+ startedAt: state.startedAt,
3737
+ parent: state.parent,
3738
+ executionOptions: state.executionOptions,
3739
+ control: state.control,
3740
+ workflowSnapshot: state.workflowSnapshot,
3741
+ mutableState: state.mutableState,
3742
+ policySnapshot: state.policySnapshot,
3743
+ pendingQueue: [],
3744
+ request,
3745
+ previousNodeSnapshotsByNodeId: state.nodeSnapshotsByNodeId ?? {},
3746
+ planner,
3747
+ engineCounters: state.engineCounters,
3748
+ connectionInvocations: state.connectionInvocations ?? [],
3749
+ suspension: remainingSuspensions.length > 0 ? remainingSuspensions : void 0,
3750
+ pendingResume
3751
+ });
3752
+ await this.nodeEventPublisher.publish("nodeQueued", queuedSnapshot);
3753
+ return result;
3754
+ }
3133
3755
  async resumeFromWebhookControl(args) {
3134
3756
  const data = this.runDataFactory.create(args.state.outputsByNode);
3135
3757
  const { topology, planner } = this.planningFactory.create(args.workflow);
@@ -3516,6 +4138,34 @@ var RunContinuationService = class {
3516
4138
  this.waiters.resolveRunCompletion(result);
3517
4139
  return result;
3518
4140
  }
4141
+ /**
4142
+ * Inspects node outputs for a `decision.status` written by `defineHumanApprovalNode`.
4143
+ * Returns the first-class HITL node status and halt classification, or `undefined`
4144
+ * when the node is not a HITL approval node.
4145
+ */
4146
+ resolveHitlStatus(outputs) {
4147
+ const firstItem = outputs?.main?.[0];
4148
+ const decisionStatus = firstItem && typeof firstItem === "object" && "json" in firstItem && firstItem.json && typeof firstItem.json === "object" && "decision" in firstItem.json && firstItem.json.decision && typeof firstItem.json.decision === "object" && "status" in firstItem.json.decision ? firstItem.json.decision.status : void 0;
4149
+ if (!decisionStatus) return void 0;
4150
+ if (decisionStatus === "approved") return {
4151
+ nodeStatus: "hitl-approved",
4152
+ halt: false
4153
+ };
4154
+ if (decisionStatus === "auto-accepted") return {
4155
+ nodeStatus: "hitl-auto-accepted",
4156
+ halt: false
4157
+ };
4158
+ if (decisionStatus === "rejected") return {
4159
+ nodeStatus: "hitl-rejected",
4160
+ halt: true,
4161
+ reason: "hitl-rejected"
4162
+ };
4163
+ if (decisionStatus === "timed-out") return {
4164
+ nodeStatus: "hitl-timeout",
4165
+ halt: true,
4166
+ reason: "hitl-timeout"
4167
+ };
4168
+ }
3519
4169
  formatNodeLabel(args) {
3520
4170
  const tokenName = typeof args.definition?.type === "function" ? args.definition.type.name : "Node";
3521
4171
  return args.definition?.name ? `"${args.definition.name}" (${tokenName}:${args.nodeId})` : `${tokenName}:${args.nodeId}`;
@@ -4868,13 +5518,19 @@ var NodeExecutionRequestHandlerService = class {
4868
5518
  const portKeys = Object.keys(inputsByPort);
4869
5519
  const kind = portKeys.length === 1 && portKeys[0] === "in" ? "single" : "multi";
4870
5520
  const batchId = pendingExecution.batchId ?? "batch_1";
5521
+ const pendingResume = state.pendingResume;
5522
+ const resumeContext = pendingResume?.activationId === request.activationId && pendingResume?.nodeId === request.nodeId ? pendingResume.resumeContext : void 0;
5523
+ const baseWithResume = resumeContext != null ? {
5524
+ ...base,
5525
+ resumeContext
5526
+ } : base;
4871
5527
  const activationRequest = kind === "multi" ? this.nodeActivationRequestComposer.createMultiFromDefinitionWithActivation({
4872
5528
  activationId: request.activationId,
4873
5529
  runId: request.runId,
4874
5530
  workflowId: request.workflowId,
4875
5531
  parent: resolvedParent,
4876
5532
  executionOptions: request.executionOptions ?? state.executionOptions,
4877
- base,
5533
+ base: baseWithResume,
4878
5534
  data,
4879
5535
  definition: {
4880
5536
  id: definition.id,
@@ -4888,7 +5544,7 @@ var NodeExecutionRequestHandlerService = class {
4888
5544
  workflowId: request.workflowId,
4889
5545
  parent: resolvedParent,
4890
5546
  executionOptions: request.executionOptions ?? state.executionOptions,
4891
- base,
5547
+ base: baseWithResume,
4892
5548
  data,
4893
5549
  definition: {
4894
5550
  id: definition.id,
@@ -4897,6 +5553,13 @@ var NodeExecutionRequestHandlerService = class {
4897
5553
  batchId,
4898
5554
  input: inputsByPort.in ?? request.input ?? []
4899
5555
  });
5556
+ if (resumeContext != null) {
5557
+ const clearedState = await this.workflowExecutionRepository.load(request.runId);
5558
+ if (clearedState?.pendingResume?.activationId === request.activationId) await this.workflowExecutionRepository.save({
5559
+ ...clearedState,
5560
+ pendingResume: void 0
5561
+ });
5562
+ }
4900
5563
  await this.continuation.markNodeRunning({
4901
5564
  runId: activationRequest.runId,
4902
5565
  activationId: activationRequest.activationId,
@@ -4907,6 +5570,7 @@ var NodeExecutionRequestHandlerService = class {
4907
5570
  try {
4908
5571
  outputs = await this.nodeExecutor.execute(activationRequest);
4909
5572
  } catch (error) {
5573
+ if (error instanceof RunSuspendedError) return;
4910
5574
  await this.resumeAfterExecutionError(activationRequest, this.asError(error));
4911
5575
  return;
4912
5576
  }
@@ -5467,7 +6131,7 @@ var EngineWaiters = class {
5467
6131
  });
5468
6132
  }
5469
6133
  resolveRunCompletion(result) {
5470
- if (result.status !== "completed" && result.status !== "failed") return;
6134
+ if (result.status !== "completed" && result.status !== "failed" && result.status !== "halted") return;
5471
6135
  const list = this.completionWaiters.get(result.runId);
5472
6136
  if (!list || list.length === 0) return;
5473
6137
  this.completionWaiters.delete(result.runId);
@@ -5561,6 +6225,13 @@ var Engine = class {
5561
6225
  async waitForWebhookResponse(runId) {
5562
6226
  return await this.deps.runContinuationService.waitForWebhookResponse(runId);
5563
6227
  }
6228
+ /**
6229
+ * Re-activate a suspended run item with a human decision (HITL).
6230
+ * The HTTP resume endpoint calls this; this method exposes the engine primitive.
6231
+ */
6232
+ async resumeRun(args) {
6233
+ return await this.deps.runContinuationService.resumeRun(args);
6234
+ }
5564
6235
  async handleNodeExecutionRequest(request) {
5565
6236
  await this.deps.nodeExecutionRequestHandler.handleNodeExecutionRequest(request);
5566
6237
  }
@@ -5768,6 +6439,7 @@ var RunIntentService = class {
5768
6439
  };
5769
6440
  return await Promise.race([this.engine.waitForWebhookResponse(scheduled.runId), this.engine.waitForCompletion(scheduled.runId).then((completed) => {
5770
6441
  if (completed.status === "failed") throw new Error(completed.error.message);
6442
+ if (completed.status === "halted") throw new Error(`Run halted: ${completed.reason}`);
5771
6443
  return {
5772
6444
  runId: completed.runId,
5773
6445
  workflowId: completed.workflowId,
@@ -5892,5 +6564,5 @@ var WorkflowRepositoryWebhookTriggerMatcherFactory = class {
5892
6564
  };
5893
6565
 
5894
6566
  //#endregion
5895
- export { CostTrackingTelemetryMetricNames as $, PersistedWorkflowTokenRegistry as A, PersistedRuntimeTypeNameResolver as At, DefaultExecutionContextFactory as B, DefaultDrivingScheduler as C, chatModel as Ct, NodeInstanceFactoryFactory as D, InjectableRuntimeDecoratorComposer as Dt, StaticCostCatalog as E, tool as Et, RunnableOutputBehaviorResolver as F, NoOpExecutionTelemetryFactory as G, GenAiTelemetryAttributeNames as H, NodeOutputNormalizer as I, NoOpTelemetrySpanScope as J, NoOpExecutionTelemetry as K, ItemExprResolver as L, NodeExecutorFactory as M, NodeExecutor as N, NodeInstanceFactory as O, PersistedRuntimeTypeMetadataStore as Ot, InProcessRetryRunnerFactory as P, CostTrackingTelemetryAttributeNames as Q, InProcessRetryRunner as R, HintOnlyOffloadPolicy as S, WhenBuilder as St, RunPolicySnapshotFactory as T, node as Tt, CodemationTelemetryAttributeNames as U, CodemationTelemetryMetricNames as V, AllWorkflowsActiveWorkflowActivationPolicy as W, NoOpCostTrackingTelemetryFactory as X, NoOpTelemetryArtifactReference as Y, NoOpCostTrackingTelemetry as Z, RunTerminalPersistenceCoordinator as _, ConnectionInvocationIdFactory as _t, InMemoryLiveWorkflowRepository as a, isUnbrandedPortsEmissionShape as at, LocalOnlyScheduler as b, NodeIdSlugifier as bt, EngineFactory as c, getOriginIndexFromItem as ct, PollingTriggerRuntime as d, UnavailableBinaryStorage as dt, ExpRetryPolicy as et, PollingTriggerDedupWindow as f, NodeEventPublisher as ft, WorkflowPolicyErrorServices as g, WorkflowExecutableNodeClassifier as gt, WorkflowStoragePolicyEvaluator as h, WorkflowExecutableNodeClassifierFactory as ht, RunIntentService as i, isPortsEmission as it, MissingRuntimeTriggerToken as j, WorkflowSnapshotCodec as k, StackTraceCallSitePathResolver as kt, Engine as l, ChildExecutionScopeFactory as lt, InMemoryBinaryStorage as m, DefaultWorkflowGraphFactory as mt, WorkflowRepositoryWebhookTriggerMatcher as n, NoRetryPolicy as nt, EngineWorkflowRunnerServiceFactory as o, DefaultAsyncSleeper as ot, InMemoryRunDataFactory as p, ConnectionInvocationEventPublisher as pt, NoOpNodeExecutionTelemetry as q, RunIntentServiceFactory as r, emitPorts as rt, EngineWorkflowRunnerService as s, CredentialResolverFactory as st, WorkflowRepositoryWebhookTriggerMatcherFactory as t, RetryPolicy as tt, NoOpPollingTriggerLogger as u, DefaultExecutionBinaryService as ut, ENGINE_EXECUTION_LIMITS_DEFAULTS as v, WorkflowBuilder as vt, ConfigDrivenOffloadPolicy as w, getPersistedRuntimeTypeMetadata as wt, InlineDrivingScheduler as x, ChainCursor as xt, EngineExecutionLimitsPolicy as y, WorkflowDefinitionError as yt, CatalogBackedCostTrackingTelemetryFactory as z };
5896
- //# sourceMappingURL=runtime-cxmUkk0l.js.map
6567
+ export { CostTrackingTelemetryAttributeNames as $, PersistedWorkflowTokenRegistry as A, node as At, CatalogBackedCostTrackingTelemetryFactory as B, HumanTaskStoreToken as Bt, DefaultDrivingScheduler as C, WhenBuilder as Ct, NodeInstanceFactoryFactory as D, defineNode as Dt, StaticCostCatalog as E, defineBatchNode as Et, InProcessRetryRunnerFactory as F, PersistedRuntimeTypeNameResolver as Ft, AllWorkflowsActiveWorkflowActivationPolicy as G, CodemationTelemetryMetricNames as H, RunnableOutputBehaviorResolver as I, DefinedNodeRegistry as It, NoOpNodeExecutionTelemetry as J, NoOpExecutionTelemetryFactory as K, NodeOutputNormalizer as L, HitlResumeTokenSignerToken as Lt, NodeExecutorFactory as M, InjectableRuntimeDecoratorComposer as Mt, NodeExecutor as N, PersistedRuntimeTypeMetadataStore as Nt, NodeInstanceFactory as O, chatModel as Ot, RunSuspendedError as P, StackTraceCallSitePathResolver as Pt, NoOpCostTrackingTelemetry as Q, ItemExprResolver as R, HitlTimeoutJobSchedulerToken as Rt, HintOnlyOffloadPolicy as S, ChainCursor as St, RunPolicySnapshotFactory as T, isHumanApprovalNode as Tt, GenAiTelemetryAttributeNames as U, DefaultExecutionContextFactory as V, CodemationTelemetryAttributeNames as W, NoOpTelemetryArtifactReference as X, NoOpTelemetrySpanScope as Y, NoOpCostTrackingTelemetryFactory as Z, RunTerminalPersistenceCoordinator as _, WorkflowExecutableNodeClassifier as _t, InMemoryLiveWorkflowRepository as a, isPortsEmission as at, LocalOnlyScheduler as b, WorkflowDefinitionError as bt, EngineFactory as c, CredentialResolverFactory as ct, PollingTriggerRuntime as d, DefaultExecutionBinaryService as dt, CostTrackingTelemetryMetricNames as et, PollingTriggerDedupWindow as f, UnavailableBinaryStorage as ft, WorkflowPolicyErrorServices as g, WorkflowExecutableNodeClassifierFactory as gt, WorkflowStoragePolicyEvaluator as h, DefaultWorkflowGraphFactory as ht, RunIntentService as i, emitPorts as it, MissingRuntimeTriggerToken as j, tool as jt, WorkflowSnapshotCodec as k, getPersistedRuntimeTypeMetadata as kt, Engine as l, getOriginIndexFromItem as lt, InMemoryBinaryStorage as m, ConnectionInvocationEventPublisher as mt, WorkflowRepositoryWebhookTriggerMatcher as n, RetryPolicy as nt, EngineWorkflowRunnerServiceFactory as o, isUnbrandedPortsEmissionShape as ot, InMemoryRunDataFactory as p, NodeEventPublisher as pt, NoOpExecutionTelemetry as q, RunIntentServiceFactory as r, NoRetryPolicy as rt, EngineWorkflowRunnerService as s, DefaultAsyncSleeper as st, WorkflowRepositoryWebhookTriggerMatcherFactory as t, ExpRetryPolicy as tt, NoOpPollingTriggerLogger as u, ChildExecutionScopeFactory as ut, ENGINE_EXECUTION_LIMITS_DEFAULTS as v, ConnectionInvocationIdFactory as vt, ConfigDrivenOffloadPolicy as w, defineHumanApprovalNode as wt, InlineDrivingScheduler as x, NodeIdSlugifier as xt, EngineExecutionLimitsPolicy as y, WorkflowBuilder as yt, InProcessRetryRunner as z, HitlWorkspaceIdToken as zt };
6568
+ //# sourceMappingURL=runtime-6-U2Cou5.js.map