@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
package/dist/testing.cjs CHANGED
@@ -1,8 +1,8 @@
1
- const require_di = require('./di-BlEKdoZS.cjs');
1
+ const require_di = require('./di-C-2ep8NZ.cjs');
2
2
  require('./contracts-CK0x6w_G.cjs');
3
- const require_runtime = require('./runtime-DBzq5YBi.cjs');
3
+ const require_runtime = require('./runtime-DjYXgOo0.cjs');
4
4
  const require_InMemoryRunEventBusRegistry = require('./InMemoryRunEventBusRegistry-Sa86VxuV.cjs');
5
- const require_bootstrap = require('./bootstrap-BxuTFTLB.cjs');
5
+ const require_bootstrap = require('./bootstrap-BEu1fJBM.cjs');
6
6
  let tsyringe = require("tsyringe");
7
7
  tsyringe = require_di.__toESM(tsyringe);
8
8
 
@@ -1,6 +1,6 @@
1
- import { $t as TriggerNodeConfig, A as RunResult, B as Container, Bt as NodeOutputs, Ci as CredentialSessionService, Ii as NodeId, Jn as NodeExecutionContext, K as TypeToken, Kt as RunDataFactory, Rn as ExecutionContextFactory, St as Items, Wt as ParentExecutionRef, Xt as RunnableNodeConfig, Yn as NodeExecutionRequest, Zn as NodeExecutionScheduler, _r as WorkflowRunnerService, ar as RunnableNodeExecuteArgs, at as EngineExecutionLimitsPolicy, bt as Item, cr as TriggerNode, ct as RunEventBus, fr as TriggerSetupStateRepository, ir as RunnableNode, rn as WorkflowDefinition, ur as TriggerSetupContext, z as WorkflowExecutionRepository, zi as WorkflowId, zt as NodeOffloadPolicy } from "./agentMcpTypes-ZiNbNsEi.cjs";
2
- import { n as InMemoryLiveWorkflowRepository, r as Engine, t as RunIntentService } from "./RunIntentService-CEF-sFfI.cjs";
3
- import { a as WorkflowSnapshotCodec, i as EngineWorkflowRunnerService, t as EngineRuntimeRegistrationOptions } from "./EngineRuntimeRegistration.types-Cggm5GVY.cjs";
1
+ import { $t as RunnableNodeConfig, Cr as TriggerSetupStateRepository, Ct as Item, H as WorkflowExecutionRepository, Ht as NodeOffloadPolicy, N as RunResult, Or as WorkflowRunnerService, Pi as CredentialSessionService, Tt as Items, U as Container, Un as ExecutionContextFactory, Ut as NodeOutputs, Xi as WorkflowId, Y as TypeToken, Yt as RunDataFactory, ct as EngineExecutionLimitsPolicy, dt as RunEventBus, hr as RunnableNodeExecuteArgs, ir as NodeExecutionRequest, mr as RunnableNode, nn as TriggerNodeConfig, on as WorkflowDefinition, or as NodeExecutionScheduler, qi as NodeId, qt as ParentExecutionRef, rr as NodeExecutionContext, xr as TriggerSetupContext, yr as TriggerNode } from "./agentMcpTypes-DGIwk6Ue.cjs";
2
+ import { n as InMemoryLiveWorkflowRepository, r as Engine, t as RunIntentService } from "./RunIntentService-BitgkKaT.cjs";
3
+ import { a as WorkflowSnapshotCodec, i as EngineWorkflowRunnerService, t as EngineRuntimeRegistrationOptions } from "./EngineRuntimeRegistration.types-CqcTWexS.cjs";
4
4
  import { ZodType } from "zod";
5
5
  import { DependencyContainer, InjectionToken } from "tsyringe";
6
6
 
package/dist/testing.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { $a as NodeId, Ba as CredentialSessionService, Di as TriggerSetupContext, Ln as Item, Pi as WorkflowRunnerService, Si as RunnableNodeExecuteArgs, Sn as EngineExecutionLimitsPolicy, Ti as TriggerNode, Tn as RunEventBus, br as WorkflowDefinition, di as NodeExecutionContext, fi as NodeExecutionRequest, gr as TriggerNodeConfig, ki as TriggerSetupStateRepository, ln as TypeToken, lr as RunDataFactory, mi as NodeExecutionScheduler, nn as WorkflowExecutionRepository, no as WorkflowId, nr as NodeOffloadPolicy, pr as RunnableNodeConfig, qt as RunResult, rn as Container, rr as NodeOutputs, sr as ParentExecutionRef, ti as ExecutionContextFactory, xi as RunnableNode, zn as Items } from "./index-zWGtEhrf.js";
2
- import { i as WorkflowSnapshotCodec, n as InMemoryLiveWorkflowRepository, r as EngineWorkflowRunnerService, t as RunIntentService, u as Engine } from "./RunIntentService-BVur7x9n.js";
3
- import { t as EngineRuntimeRegistrationOptions } from "./EngineRuntimeRegistration.types-BQbS9_gs.js";
1
+ import { Eo as NodeId, Er as ParentExecutionRef, Fr as TriggerNodeConfig, Ii as NodeExecutionContext, Ji as RunnableNodeExecuteArgs, Li as NodeExecutionRequest, Mr as RunnableNodeConfig, On as TypeToken, Or as RunDataFactory, Qi as TriggerNode, Si as ExecutionContextFactory, Sn as Container, Sr as NodeOutputs, Vn as EngineExecutionLimitsPolicy, Wn as RunEventBus, ea as TriggerSetupContext, fn as RunResult, ir as Items, ko as WorkflowId, na as TriggerSetupStateRepository, nr as Item, po as CredentialSessionService, qi as RunnableNode, sa as WorkflowRunnerService, xn as WorkflowExecutionRepository, xr as NodeOffloadPolicy, zi as NodeExecutionScheduler, zr as WorkflowDefinition } from "./index-rllWL4r-.js";
2
+ import { i as WorkflowSnapshotCodec, n as InMemoryLiveWorkflowRepository, r as EngineWorkflowRunnerService, t as RunIntentService, u as Engine } from "./RunIntentService-DYpqfu6D.js";
3
+ import { t as EngineRuntimeRegistrationOptions } from "./EngineRuntimeRegistration.types-Cr75cSfL.js";
4
4
  import { DependencyContainer, InjectionToken } from "tsyringe";
5
5
  import { ZodType } from "zod";
6
6
 
package/dist/testing.js CHANGED
@@ -1,8 +1,8 @@
1
- import { d as CoreTokens } from "./di-0Wop7z1y.js";
1
+ import { d as CoreTokens } from "./di-D9Mv3kF3.js";
2
2
  import "./contracts-DXdfTdpW.js";
3
- import { A as PersistedWorkflowTokenRegistry, B as DefaultExecutionContextFactory, C as DefaultDrivingScheduler, N as NodeExecutor, O as NodeInstanceFactory, R as InProcessRetryRunner, S as HintOnlyOffloadPolicy, W as AllWorkflowsActiveWorkflowActivationPolicy, a as InMemoryLiveWorkflowRepository, i as RunIntentService, k as WorkflowSnapshotCodec, l as Engine, ot as DefaultAsyncSleeper, p as InMemoryRunDataFactory, rt as emitPorts, vt as WorkflowBuilder, x as InlineDrivingScheduler } from "./runtime-cxmUkk0l.js";
3
+ import { A as PersistedWorkflowTokenRegistry, C as DefaultDrivingScheduler, G as AllWorkflowsActiveWorkflowActivationPolicy, N as NodeExecutor, O as NodeInstanceFactory, S as HintOnlyOffloadPolicy, V as DefaultExecutionContextFactory, a as InMemoryLiveWorkflowRepository, i as RunIntentService, it as emitPorts, k as WorkflowSnapshotCodec, l as Engine, p as InMemoryRunDataFactory, st as DefaultAsyncSleeper, x as InlineDrivingScheduler, yt as WorkflowBuilder, z as InProcessRetryRunner } from "./runtime-6-U2Cou5.js";
4
4
  import { t as InMemoryRunEventBus } from "./InMemoryRunEventBusRegistry-Bwunvt1T.js";
5
- import { a as InMemoryWorkflowExecutionRepository, t as EngineRuntimeRegistrar } from "./bootstrap-D_Yyi0wL.js";
5
+ import { a as InMemoryWorkflowExecutionRepository, t as EngineRuntimeRegistrar } from "./bootstrap-CSeInbj1.js";
6
6
  import { container } from "tsyringe";
7
7
 
8
8
  //#region src/testing/RejectingCredentialSessionService.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemation/core",
3
- "version": "0.11.1",
3
+ "version": "0.13.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -0,0 +1,379 @@
1
+ import type {
2
+ Duration,
3
+ ExecutionContext,
4
+ HumanTaskActor,
5
+ HumanTaskHandle,
6
+ HumanTaskSubject,
7
+ NodeExecutionContext,
8
+ ResumeContext,
9
+ } from "../contracts/runtimeTypes";
10
+ import type { Item, JsonValue, NodeInspectorSummaryRow, NodeConfigBase } from "../contracts/workflowTypes";
11
+ import type { CredentialJsonRecord } from "../contracts/credentialTypes";
12
+ import type { ZodObject, ZodType } from "zod";
13
+ import type { DefinedNodeCredentialBindings } from "./defineNode.types";
14
+ import { SuspensionRequest } from "../contracts/runtimeTypes";
15
+ import { defineNode } from "./defineNode.types";
16
+ import type { DefinedNode } from "./defineNode.types";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Public types
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Decision shape merged into `item.json` after a HITL approval task resolves.
24
+ *
25
+ * - `"approved"` / `"rejected"` — from a human decision (uses `approvedPredicate`).
26
+ * - `"timed-out"` — timeout fired with `onTimeout: "halt"`.
27
+ * - `"auto-accepted"` — timeout fired with `onTimeout: "auto-accept"`.
28
+ */
29
+ export interface HumanApprovalDecisionResult {
30
+ readonly status: "approved" | "rejected" | "timed-out" | "auto-accepted";
31
+ /** Identity of the person who decided; absent for automated outcomes. */
32
+ readonly actor?: HumanTaskActor;
33
+ /** ISO 8601 timestamp of the decision. */
34
+ readonly decidedAt?: Date;
35
+ /** Optional free-text note from the reviewer. */
36
+ readonly note?: string;
37
+ /**
38
+ * Full raw decision payload (only present for `"approved"` / `"rejected"`).
39
+ * Shape is determined by the channel's `decisionSchema`.
40
+ */
41
+ readonly payload?: Record<string, unknown>;
42
+ }
43
+
44
+ /**
45
+ * Output item shape emitted by a `defineHumanApprovalNode`-based node.
46
+ * Original `item.json` fields are preserved and `decision` is merged in.
47
+ * If the input `item.json` already contained a `decision` key it is **overwritten**.
48
+ */
49
+ export type HumanApprovalOutputJson<TInputJson extends Record<string, unknown>> = TInputJson & {
50
+ readonly decision: HumanApprovalDecisionResult;
51
+ };
52
+
53
+ /**
54
+ * Extends {@link DefinedNode} with the `humanApprovalToolBehavior` metadata marker.
55
+ * Story 10 reads this field when attaching the node as an agent tool.
56
+ */
57
+ export interface DefinedHumanApprovalNode<
58
+ TKey extends string,
59
+ TConfig extends CredentialJsonRecord,
60
+ TInputJson extends Record<string, unknown>,
61
+ TBindings extends DefinedNodeCredentialBindings | undefined = undefined,
62
+ > extends DefinedNode<TKey, TConfig, TInputJson, HumanApprovalOutputJson<TInputJson>, TBindings> {
63
+ /**
64
+ * Behavior hint consumed by the agent runtime (story 10) when this node is attached as a tool.
65
+ * `"return"` (default) — return the rejection to the agent as a tool result.
66
+ * `"halt"` — halt the agent run on rejection.
67
+ *
68
+ * Standalone DSL usage ignores this field.
69
+ */
70
+ readonly humanApprovalToolBehavior: { onRejected: "return" | "halt" };
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // isHumanApprovalNode predicate
75
+ // ---------------------------------------------------------------------------
76
+
77
+ /**
78
+ * Returns `true` when `node` was created by {@link defineHumanApprovalNode}.
79
+ * Uses the `humanApprovalToolBehavior` typed field as the discriminant.
80
+ */
81
+ export function isHumanApprovalNode(
82
+ node: unknown,
83
+ ): node is DefinedHumanApprovalNode<string, Record<string, unknown>, Record<string, unknown>, undefined> {
84
+ return (
85
+ typeof node === "object" &&
86
+ node !== null &&
87
+ "humanApprovalToolBehavior" in node &&
88
+ typeof (node as { humanApprovalToolBehavior: unknown }).humanApprovalToolBehavior === "object"
89
+ );
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // defineHumanApprovalNode
94
+ // ---------------------------------------------------------------------------
95
+
96
+ /**
97
+ * Authoring helper that compiles a HITL approval channel down to a regular
98
+ * {@link defineNode}-backed node with `SuspensionRequest` semantics.
99
+ *
100
+ * **Fast-forward decision semantics:**
101
+ * - On the first `execute` call (no `ctx.resumeContext`): throws a `SuspensionRequest`
102
+ * that calls the author's `deliver`. The engine persists the suspension and continues.
103
+ * - On resume (`ctx.resumeContext` set): calls `onDecision`/`onTimeout` as appropriate,
104
+ * merges a `decision` key into `item.json`, and returns an item with the original
105
+ * `binary` map passed by reference (no copy).
106
+ *
107
+ * **Output shape per item:**
108
+ * ```ts
109
+ * // Input: { json: { invoiceId: 42 }, binary?: {...} }
110
+ * // Output: { json: { invoiceId: 42, decision: { status: "approved", actor, decidedAt } }, binary: <unchanged> }
111
+ * ```
112
+ * If `item.json` already has a `decision` key it is **overwritten**. Namespace as
113
+ * needed if your schema reserves that key for another purpose.
114
+ *
115
+ * **Predicate persistence:**
116
+ * The `approvedPredicate` function is NOT serialized to the suspension record (except
117
+ * as an audit-only string via `toString()`). On resume, the workflow definition is
118
+ * reloaded from code at process start and the predicate closure is rebuilt naturally.
119
+ * If a deploy ships a changed predicate between suspend and resume, the *new* predicate
120
+ * runs — document this in your runbook when the predicate carries business logic that
121
+ * may change across deploys.
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * export const slackApprovalNode = defineHumanApprovalNode({
126
+ * key: "my-plugin.slackApproval",
127
+ * title: "Slack Approval",
128
+ * channel: "slack",
129
+ * configSchema: z.object({ channel: z.string(), message: z.string() }),
130
+ * decisionSchema: z.object({ approved: z.boolean(), note: z.string().optional() }),
131
+ *
132
+ * async deliver({ task, config, item }, ctx) {
133
+ * const ts = await postSlackMessage(config.channel, `Approve? <${task.resumeUrl}>`);
134
+ * return { channel: config.channel, ts };
135
+ * },
136
+ *
137
+ * async onDecision({ decision, actor, delivery }, ctx) {
138
+ * await updateSlackMessage(delivery.channel, delivery.ts, decision.approved ? "✅" : "❌");
139
+ * },
140
+ * });
141
+ * ```
142
+ */
143
+ export function defineHumanApprovalNode<
144
+ TKey extends string,
145
+ TConfig extends CredentialJsonRecord,
146
+ TInputJson extends Record<string, unknown>,
147
+ TDecision extends Record<string, unknown>,
148
+ TDelivery extends JsonValue,
149
+ TBindings extends DefinedNodeCredentialBindings | undefined = undefined,
150
+ >(opts: {
151
+ key: TKey;
152
+ title: string;
153
+ description?: string;
154
+ icon?: string;
155
+ channel: string;
156
+
157
+ configSchema: ZodType<TConfig>;
158
+ inputSchema?: ZodType<TInputJson>;
159
+ decisionSchema: ZodType<TDecision>;
160
+ credentials?: TBindings;
161
+
162
+ /**
163
+ * Custom predicate that decides whether a decision counts as "approved".
164
+ * When omitted, the helper checks if `decisionSchema` is a Zod object with an
165
+ * `approved: boolean` field; if so it uses `decision.approved === true`.
166
+ * If neither holds, `defineHumanApprovalNode` throws at **definition time** (not runtime).
167
+ */
168
+ approvedPredicate?: (decision: TDecision) => boolean;
169
+ /** Default suspension timeout. Defaults to `"24h"`. */
170
+ defaultTimeout?: Duration;
171
+ /** What to do when the task times out. Defaults to `"halt"`. */
172
+ defaultOnTimeout?: "halt" | "auto-accept";
173
+
174
+ inspectorSummary?: (config: TConfig) => ReadonlyArray<NodeInspectorSummaryRow> | undefined;
175
+
176
+ deliver: (
177
+ args: {
178
+ task: HumanTaskHandle;
179
+ config: TConfig;
180
+ input: TInputJson;
181
+ item: Item;
182
+ },
183
+ ctx: ExecutionContext,
184
+ ) => Promise<TDelivery>;
185
+
186
+ onDecision?: (
187
+ args: {
188
+ decision: TDecision;
189
+ actor: HumanTaskActor;
190
+ task: HumanTaskHandle;
191
+ delivery: TDelivery;
192
+ item: Item;
193
+ },
194
+ ctx: ExecutionContext,
195
+ ) => Promise<void>;
196
+
197
+ onTimeout?: (
198
+ args: {
199
+ task: HumanTaskHandle;
200
+ delivery: TDelivery;
201
+ item: Item;
202
+ policy: "halt" | "auto-accept";
203
+ },
204
+ ctx: ExecutionContext,
205
+ ) => Promise<void>;
206
+ }): DefinedHumanApprovalNode<TKey, TConfig, TInputJson, TBindings> {
207
+ // Resolve the approved predicate at definition time so we throw early when
208
+ // the schema is ambiguous.
209
+ const resolvedPredicate = resolveApprovedPredicate(opts.decisionSchema, opts.approvedPredicate);
210
+
211
+ const timeout = opts.defaultTimeout ?? "24h";
212
+ const onTimeout = opts.defaultOnTimeout ?? "halt";
213
+
214
+ // TOutputJson is `unknown` here because `execute` returns an Item-shaped object
215
+ // that the engine's NodeOutputNormalizer converts to the proper output. The public
216
+ // interface's DefinedHumanApprovalNode carries the correct output type for DSL use.
217
+ const inner = defineNode<TKey, TConfig, TInputJson, unknown, TBindings>({
218
+ key: opts.key,
219
+ title: opts.title,
220
+ description: opts.description,
221
+ icon: opts.icon,
222
+ configSchema: opts.configSchema,
223
+ inputSchema: opts.inputSchema,
224
+ credentials: opts.credentials,
225
+ inspectorSummary: opts.inspectorSummary ? ({ config }) => opts.inspectorSummary!(config) : undefined,
226
+
227
+ async execute(args, { config, execution: ctx }) {
228
+ if (!ctx.resumeContext) {
229
+ // First pass — suspend.
230
+ const subject = buildSubject(opts.title, args.item, ctx);
231
+ throw new SuspensionRequest({
232
+ decisionSchema: opts.decisionSchema,
233
+ timeout,
234
+ onTimeout,
235
+ subject,
236
+ metadata: {
237
+ channel: opts.channel,
238
+ nodeKey: opts.key,
239
+ // Stored for audit only; never re-evaluated. See JSDoc on defineHumanApprovalNode.
240
+ approvedPredicateSource: opts.approvedPredicate?.toString() ?? null,
241
+ },
242
+ deliver: (handle: HumanTaskHandle) =>
243
+ opts.deliver(
244
+ {
245
+ task: handle,
246
+ config,
247
+ input: args.input as TInputJson,
248
+ item: args.item,
249
+ },
250
+ ctx,
251
+ ),
252
+ });
253
+ }
254
+
255
+ // Resume pass.
256
+ return await handleResume(
257
+ args.item,
258
+ ctx.resumeContext,
259
+ opts.decisionSchema,
260
+ resolvedPredicate,
261
+ opts.onDecision,
262
+ opts.onTimeout,
263
+ ctx,
264
+ );
265
+ },
266
+ });
267
+
268
+ return Object.assign(inner, {
269
+ humanApprovalToolBehavior: { onRejected: "return" as const },
270
+ }) as unknown as DefinedHumanApprovalNode<TKey, TConfig, TInputJson, TBindings>;
271
+ }
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // Internal helpers (module-private)
275
+ // ---------------------------------------------------------------------------
276
+
277
+ function resolveApprovedPredicate<TDecision extends Record<string, unknown>>(
278
+ schema: ZodType<TDecision>,
279
+ predicate: ((d: TDecision) => boolean) | undefined,
280
+ ): (d: TDecision) => boolean {
281
+ if (predicate) {
282
+ return predicate;
283
+ }
284
+ // Zod 4: ZodObject exposes `.shape` directly as an object (not a function).
285
+ const shape = (schema as unknown as ZodObject<Record<string, ZodType>>).shape;
286
+ if (shape && typeof shape === "object" && "approved" in shape) {
287
+ return (d) => (d as { approved?: unknown }).approved === true;
288
+ }
289
+ throw new Error(
290
+ `defineHumanApprovalNode: decisionSchema has no "approved" field and no approvedPredicate was provided. ` +
291
+ `Either add { approved: z.boolean() } to the decision schema or supply approvedPredicate explicitly.`,
292
+ );
293
+ }
294
+
295
+ function buildSubject(title: string, item: Item, ctx: NodeExecutionContext<NodeConfigBase>): HumanTaskSubject {
296
+ return {
297
+ title,
298
+ summary: "",
299
+ attributes: {
300
+ workflowId: ctx.workflowId,
301
+ nodeId: ctx.nodeId,
302
+ item: item.json as JsonValue,
303
+ },
304
+ };
305
+ }
306
+
307
+ function mergeDecision(
308
+ item: Item,
309
+ decision: HumanApprovalDecisionResult,
310
+ ): { json: Record<string, unknown>; binary: Item["binary"]; meta: Item["meta"] } {
311
+ return {
312
+ json: { ...(item.json as Record<string, unknown>), decision },
313
+ // binary is passed by reference — no copy. See defineHumanApprovalNode JSDoc.
314
+ binary: item.binary,
315
+ meta: item.meta,
316
+ };
317
+ }
318
+
319
+ async function handleResume<TDecision extends Record<string, unknown>, TDelivery extends JsonValue>(
320
+ item: Item,
321
+ resumeContext: ResumeContext,
322
+ decisionSchema: ZodType<TDecision>,
323
+ resolvedPredicate: (d: TDecision) => boolean,
324
+ onDecision:
325
+ | ((
326
+ args: {
327
+ decision: TDecision;
328
+ actor: HumanTaskActor;
329
+ task: HumanTaskHandle;
330
+ delivery: TDelivery;
331
+ item: Item;
332
+ },
333
+ ctx: ExecutionContext,
334
+ ) => Promise<void>)
335
+ | undefined,
336
+ onTimeoutCb:
337
+ | ((
338
+ args: {
339
+ task: HumanTaskHandle;
340
+ delivery: TDelivery;
341
+ item: Item;
342
+ policy: "halt" | "auto-accept";
343
+ },
344
+ ctx: ExecutionContext,
345
+ ) => Promise<void>)
346
+ | undefined,
347
+ ctx: ExecutionContext,
348
+ ): Promise<{ json: Record<string, unknown>; binary: Item["binary"]; meta: Item["meta"] }> {
349
+ const { decision: dec, delivery, task } = resumeContext;
350
+
351
+ if (dec.kind === "timed_out" || dec.kind === "auto_accepted") {
352
+ const policy: "halt" | "auto-accept" = dec.kind === "auto_accepted" ? "auto-accept" : "halt";
353
+ await onTimeoutCb?.({ task, delivery: delivery as TDelivery, item, policy }, ctx);
354
+ const status = dec.kind === "auto_accepted" ? "auto-accepted" : "timed-out";
355
+ return mergeDecision(item, { status, decidedAt: dec.at });
356
+ }
357
+
358
+ // dec.kind === "decided"
359
+ const parsed = decisionSchema.parse(dec.value);
360
+ await onDecision?.(
361
+ {
362
+ decision: parsed,
363
+ actor: dec.actor,
364
+ task,
365
+ delivery: delivery as TDelivery,
366
+ item,
367
+ },
368
+ ctx,
369
+ );
370
+
371
+ const isApproved = resolvedPredicate(parsed);
372
+ return mergeDecision(item, {
373
+ status: isApproved ? "approved" : "rejected",
374
+ actor: dec.actor,
375
+ decidedAt: dec.decidedAt,
376
+ note: (parsed as { note?: string }).note,
377
+ payload: parsed,
378
+ });
379
+ }
@@ -11,6 +11,12 @@ export type {
11
11
  DefineNodeOptions,
12
12
  } from "./defineNode.types";
13
13
  export { defineBatchNode, defineNode } from "./defineNode.types";
14
+ export type {
15
+ DefinedHumanApprovalNode,
16
+ HumanApprovalDecisionResult,
17
+ HumanApprovalOutputJson,
18
+ } from "./defineHumanApprovalNode.types";
19
+ export { defineHumanApprovalNode, isHumanApprovalNode } from "./defineHumanApprovalNode.types";
14
20
  export type { DefineCredentialOptions } from "./defineCredential.types";
15
21
  export { defineCredential } from "./defineCredential.types";
16
22
  export { callableTool } from "./callableTool.types";
@@ -8,6 +8,7 @@ import type {
8
8
  } from "../types";
9
9
 
10
10
  import { DefaultNodeBinaryAttachmentService } from "./DefaultNodeBinaryAttachmentServiceFactory";
11
+ import { boundedReadBinary } from "./boundedReadBinary.types";
11
12
 
12
13
  export class DefaultExecutionBinaryService implements ExecutionBinaryService {
13
14
  constructor(
@@ -28,8 +29,32 @@ export class DefaultExecutionBinaryService implements ExecutionBinaryService {
28
29
  );
29
30
  }
30
31
 
31
- async openReadStream(attachment: BinaryAttachment): Promise<BinaryStorageReadResult | undefined> {
32
- return await this.storage.openReadStream(attachment.storageKey);
32
+ openReadStream(attachment: BinaryAttachment): Promise<BinaryStorageReadResult | undefined> {
33
+ return this.storage.openReadStream(attachment.storageKey);
34
+ }
35
+
36
+ async getBytes(attachment: BinaryAttachment, maxBytes?: number): Promise<Uint8Array> {
37
+ const stream = await this.openReadStream(attachment);
38
+ if (!stream) {
39
+ throw new Error("Binary attachment stream is unavailable.");
40
+ }
41
+ return boundedReadBinary(stream, attachment, maxBytes);
42
+ }
43
+
44
+ async getText(attachment: BinaryAttachment, maxBytes?: number): Promise<string> {
45
+ return new TextDecoder().decode(await this.getBytes(attachment, maxBytes));
46
+ }
47
+
48
+ async getJson<T = unknown>(attachment: BinaryAttachment, maxBytes?: number): Promise<T> {
49
+ const text = await this.getText(attachment, maxBytes);
50
+ try {
51
+ return JSON.parse(text) as T;
52
+ } catch (cause) {
53
+ throw new SyntaxError(
54
+ `Binary attachment at storage key "${attachment.storageKey}" is not valid JSON: ${cause instanceof Error ? cause.message : String(cause)}`,
55
+ { cause },
56
+ );
57
+ }
33
58
  }
34
59
  }
35
60
 
@@ -7,6 +7,8 @@ import type {
7
7
  NodeBinaryAttachmentService,
8
8
  } from "../types";
9
9
 
10
+ import { readBinaryAsBytes, readBinaryAsJson, readBinaryAsText } from "./boundedReadBinary.types";
11
+
10
12
  export class DefaultNodeBinaryAttachmentService implements NodeBinaryAttachmentService {
11
13
  constructor(
12
14
  private readonly storage: BinaryStorage,
@@ -67,6 +69,18 @@ export class DefaultNodeBinaryAttachmentService implements NodeBinaryAttachmentS
67
69
  return await this.storage.openReadStream(attachment.storageKey);
68
70
  }
69
71
 
72
+ async getBytes(attachment: BinaryAttachment, maxBytes?: number): Promise<Uint8Array> {
73
+ return readBinaryAsBytes(this.storage, attachment, maxBytes);
74
+ }
75
+
76
+ async getText(attachment: BinaryAttachment, maxBytes?: number): Promise<string> {
77
+ return readBinaryAsText(this.storage, attachment, maxBytes);
78
+ }
79
+
80
+ async getJson<T = unknown>(attachment: BinaryAttachment, maxBytes?: number): Promise<T> {
81
+ return readBinaryAsJson<T>(this.storage, attachment, maxBytes);
82
+ }
83
+
70
84
  private createAttachmentId(): string {
71
85
  return DefaultNodeBinaryAttachmentService.createAttachmentIdValue(`${this.activationId}-${this.now().getTime()}`);
72
86
  }
@@ -0,0 +1,90 @@
1
+ import type { BinaryAttachment, BinaryStorage } from "../types";
2
+ import type { BinaryStorageReadResult } from "../types";
3
+ import { BINARY_DEFAULT_MAX_BYTES } from "../contracts/runtimeTypes";
4
+
5
+ /**
6
+ * Reads all bytes from an already-opened binary stream into a contiguous `Uint8Array`.
7
+ *
8
+ * Safety contract:
9
+ * - `attachment.size` is checked against `maxBytes` *before* any allocation (no OOM).
10
+ * - A single buffer of exactly `attachment.size` is pre-allocated; the stream fills it
11
+ * directly — no chunks array, no doubling.
12
+ * - A byte-count mismatch between the declared size and actual stream content is an error.
13
+ *
14
+ * This is the single canonical implementation; `ExecutionBinaryService.getBytes`,
15
+ * `getText`, and `getJson` all delegate here. The per-package `readBinaryBody` helpers
16
+ * in `core-nodes` and `core-nodes-ocr` have been removed in favour of this function.
17
+ */
18
+ export async function boundedReadBinary(
19
+ result: BinaryStorageReadResult,
20
+ attachment: BinaryAttachment,
21
+ maxBytes: number = BINARY_DEFAULT_MAX_BYTES,
22
+ ): Promise<Uint8Array> {
23
+ if (attachment.size > maxBytes) {
24
+ throw new Error(
25
+ `Binary attachment size ${attachment.size} bytes exceeds maxBytes ${maxBytes}. ` +
26
+ `Raise the node's maxBytes setting if this document is expected to be larger.`,
27
+ );
28
+ }
29
+ const out = new Uint8Array(attachment.size);
30
+ const reader = result.body.getReader();
31
+ let offset = 0;
32
+ while (true) {
33
+ const { done, value } = await reader.read();
34
+ if (done) {
35
+ break;
36
+ }
37
+ if (!value) {
38
+ continue;
39
+ }
40
+ if (offset + value.byteLength > out.byteLength) {
41
+ throw new Error(`Binary stream produced more bytes than the attachment's declared size (${attachment.size}).`);
42
+ }
43
+ out.set(value, offset);
44
+ offset += value.byteLength;
45
+ }
46
+ if (offset !== out.byteLength) {
47
+ throw new Error(`Binary stream produced ${offset} bytes but attachment declared size ${attachment.size}.`);
48
+ }
49
+ return out;
50
+ }
51
+
52
+ /** Shared implementation of `getBytes` used by both binary-service classes. */
53
+ export async function readBinaryAsBytes(
54
+ storage: BinaryStorage,
55
+ attachment: BinaryAttachment,
56
+ maxBytes?: number,
57
+ ): Promise<Uint8Array> {
58
+ const result = await storage.openReadStream(attachment.storageKey);
59
+ if (!result) {
60
+ throw new Error("Binary attachment stream is unavailable.");
61
+ }
62
+ return boundedReadBinary(result, attachment, maxBytes);
63
+ }
64
+
65
+ /** Shared implementation of `getText` used by both binary-service classes. */
66
+ export async function readBinaryAsText(
67
+ storage: BinaryStorage,
68
+ attachment: BinaryAttachment,
69
+ maxBytes?: number,
70
+ ): Promise<string> {
71
+ const bytes = await readBinaryAsBytes(storage, attachment, maxBytes);
72
+ return new TextDecoder().decode(bytes);
73
+ }
74
+
75
+ /** Shared implementation of `getJson` used by both binary-service classes. */
76
+ export async function readBinaryAsJson<T = unknown>(
77
+ storage: BinaryStorage,
78
+ attachment: BinaryAttachment,
79
+ maxBytes?: number,
80
+ ): Promise<T> {
81
+ const text = await readBinaryAsText(storage, attachment, maxBytes);
82
+ try {
83
+ return JSON.parse(text) as T;
84
+ } catch (cause) {
85
+ throw new SyntaxError(
86
+ `Binary attachment at storage key "${attachment.storageKey}" is not valid JSON: ${cause instanceof Error ? cause.message : String(cause)}`,
87
+ { cause },
88
+ );
89
+ }
90
+ }
@@ -13,6 +13,13 @@ import {
13
13
  NodeOutputNormalizer,
14
14
  RunnableOutputBehaviorResolver,
15
15
  } from "../../execution";
16
+ import { NodeSuspensionHandler } from "../../execution/NodeSuspensionHandler";
17
+ import { HumanTaskStoreToken } from "../../contracts/humanTaskStoreTypes";
18
+ import {
19
+ HitlResumeTokenSignerToken,
20
+ HitlTimeoutJobSchedulerToken,
21
+ HitlWorkspaceIdToken,
22
+ } from "../../contracts/hitlSeamTypes";
16
23
  import {
17
24
  EngineFactory,
18
25
  EngineWorkflowRunnerServiceFactory,
@@ -119,12 +126,34 @@ export class EngineRuntimeRegistrar {
119
126
  const retryRunner = dependencyContainer
120
127
  .resolve(InProcessRetryRunnerFactory)
121
128
  .create(dependencyContainer.resolve(DefaultAsyncSleeper));
129
+ const workflowExecutionRepository = dependencyContainer.resolve(CoreTokens.WorkflowExecutionRepository);
130
+ const humanTaskStore = dependencyContainer.isRegistered(HumanTaskStoreToken, true)
131
+ ? dependencyContainer.resolve(HumanTaskStoreToken)
132
+ : undefined;
133
+ const tokenSigner = dependencyContainer.isRegistered(HitlResumeTokenSignerToken, true)
134
+ ? dependencyContainer.resolve(HitlResumeTokenSignerToken)
135
+ : undefined;
136
+ const timeoutScheduler = dependencyContainer.isRegistered(HitlTimeoutJobSchedulerToken, true)
137
+ ? dependencyContainer.resolve(HitlTimeoutJobSchedulerToken)
138
+ : undefined;
139
+ const workspaceId = dependencyContainer.isRegistered(HitlWorkspaceIdToken, true)
140
+ ? dependencyContainer.resolve(HitlWorkspaceIdToken)
141
+ : undefined;
142
+ const suspensionHandler = new NodeSuspensionHandler(
143
+ workflowExecutionRepository,
144
+ humanTaskStore ?? undefined,
145
+ tokenSigner ?? undefined,
146
+ timeoutScheduler ?? undefined,
147
+ workspaceId ?? undefined,
148
+ );
122
149
  return dependencyContainer
123
150
  .resolve(NodeExecutorFactory)
124
151
  .create(
125
152
  dependencyContainer.resolve(CoreTokens.WorkflowNodeInstanceFactory),
126
153
  retryRunner,
127
154
  dependencyContainer.resolve(RunnableOutputBehaviorResolver),
155
+ suspensionHandler,
156
+ (runId) => workflowExecutionRepository.load(runId),
128
157
  );
129
158
  }),
130
159
  });