@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,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";
@@ -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
  });
@@ -19,4 +19,14 @@ export class CodemationTelemetryAttributeNames {
19
19
  static readonly mcpServerId = "mcp.server_id";
20
20
  /** MCP tool name on spans created for callTool invocations. */
21
21
  static readonly mcpToolName = "mcp.tool_name";
22
+ /** Terminal node-execution status (e.g. `"hitl-approved"`, `"hitl-rejected"`) on HITL outcome spans. */
23
+ static readonly nodeExecutionStatus = "codemation.node.execution_status";
24
+ /** Populated on run-halted spans; discriminates the halt reason (e.g. `"hitl-rejected"`). */
25
+ static readonly runHaltReason = "codemation.run.halt_reason";
26
+ /** Human task ID on `hitl.task.*` span events. */
27
+ static readonly hitlTaskId = "codemation.hitl.task_id";
28
+ /** HITL channel name (e.g. `"inbox"`, `"control-plane-inbox"`) on `hitl.task.*` span events. */
29
+ static readonly hitlChannel = "codemation.hitl.channel";
30
+ /** Decision outcome (e.g. `"approved"`, `"rejected"`) on `hitl.task.decided` span events. */
31
+ static readonly hitlDecisionStatus = "codemation.hitl.decision_status";
22
32
  }
@@ -162,6 +162,16 @@ export type CredentialInstanceRecord<TPublicConfig extends CredentialJsonRecord
162
162
  setupStatus: CredentialSetupStatus;
163
163
  createdAt: string;
164
164
  updatedAt: string;
165
+ /**
166
+ * Pointer to where the credential material bytes live. For OSS / standalone
167
+ * rows this is `{source: "local", ref: instanceId}` and the bytes co-locate
168
+ * with the row in the workspace DB. For managed-mode rows this is
169
+ * `{source: "control-plane", ref: <cp_id>}` and the bytes live at CP.
170
+ *
171
+ * The seam is read through `CredentialMaterialProvider`. See
172
+ * `docs/design/credentials-oauth-unification.md` ("Material provider seam").
173
+ */
174
+ material: Readonly<{ source: "local" | "control-plane"; ref: string }>;
165
175
  }>;
166
176
 
167
177
  /**
@@ -0,0 +1,34 @@
1
+ import type { TypeToken } from "../di";
2
+
3
+ /**
4
+ * Seam interfaces for HITL collaborators that are implemented in `@codemation/host`
5
+ * and injected into `NodeSuspensionHandler` at runtime. Core defines the interface only —
6
+ * no HTTP, vendor SDK, or Prisma dependencies here.
7
+ */
8
+
9
+ /** Signs and hashes a HITL resume token. Core only needs the sign and hash operations. */
10
+ export interface HitlResumeTokenSignerSeam {
11
+ sign(args: { taskId: string; expiresAt: Date; schemaHash: string }): string;
12
+ hashToken(token: string): string;
13
+ }
14
+
15
+ /** Schedules a delayed BullMQ job that drives the timeout path. */
16
+ export interface HitlTimeoutJobSchedulerSeam {
17
+ enqueueTimeoutJob(args: { taskId: string; expiresAt: Date }): Promise<void>;
18
+ }
19
+
20
+ export const HitlResumeTokenSignerToken = Symbol.for("codemation.core.HitlResumeTokenSigner") as TypeToken<
21
+ HitlResumeTokenSignerSeam | undefined
22
+ >;
23
+
24
+ export const HitlTimeoutJobSchedulerToken = Symbol.for("codemation.core.HitlTimeoutJobScheduler") as TypeToken<
25
+ HitlTimeoutJobSchedulerSeam | undefined
26
+ >;
27
+
28
+ /**
29
+ * Optional workspace ID injected into NodeSuspensionHandler in managed mode (T7 security fix).
30
+ * Allows the handler to stamp the workspaceId on each HumanTaskRecord so HitlCallbackHandler
31
+ * can assert workspace identity independently of the HMAC middleware.
32
+ * Not registered in non-managed mode; NodeSuspensionHandler defaults to null.
33
+ */
34
+ export const HitlWorkspaceIdToken = Symbol.for("codemation.core.HitlWorkspaceId") as TypeToken<string | undefined>;
@@ -0,0 +1,48 @@
1
+ import type { TypeToken } from "../di";
2
+ import type { HumanTaskActor, HumanTaskSubject } from "./runtimeTypes";
3
+ import type { JsonValue } from "./workflowTypes";
4
+
5
+ export type HumanTaskStatus = "pending" | "decided" | "timed_out" | "auto_accepted" | "cancelled";
6
+
7
+ /** Persisted record for a single HITL task instance. */
8
+ export interface HumanTaskRecord {
9
+ readonly id: string;
10
+ readonly runId: string;
11
+ readonly workflowId: string;
12
+ readonly workspaceId?: string;
13
+ readonly nodeId: string;
14
+ readonly activationId: string;
15
+ readonly itemIndex: number;
16
+ readonly status: HumanTaskStatus;
17
+ readonly channel: string;
18
+ readonly subject: HumanTaskSubject;
19
+ readonly metadata: Record<string, JsonValue>;
20
+ readonly decisionSchemaJson: string;
21
+ readonly decisionSchemaHash: string;
22
+ readonly onTimeout: "halt" | "auto-accept";
23
+ readonly deliveryRef?: JsonValue;
24
+ readonly decision?: JsonValue;
25
+ readonly decidedAt?: Date;
26
+ readonly decidedBy?: HumanTaskActor;
27
+ readonly resumeTokenHash: string;
28
+ readonly expiresAt: Date;
29
+ readonly createdAt: Date;
30
+ }
31
+
32
+ export interface HumanTaskStore {
33
+ create(record: HumanTaskRecord): Promise<void>;
34
+ findById(taskId: string): Promise<HumanTaskRecord | undefined>;
35
+ findByResumeTokenHash(tokenHash: string): Promise<HumanTaskRecord | undefined>;
36
+ findPendingForWorkspace(workspaceId: string): Promise<ReadonlyArray<HumanTaskRecord>>;
37
+ /** Returns all pending tasks regardless of workspace. Used by the local dev inbox (non-managed mode). */
38
+ findAllPending(): Promise<ReadonlyArray<HumanTaskRecord>>;
39
+ markDecided(args: { taskId: string; decision: JsonValue; decidedBy: HumanTaskActor; decidedAt: Date }): Promise<void>;
40
+ markTimedOut(taskId: string): Promise<void>;
41
+ markAutoAccepted(taskId: string): Promise<void>;
42
+ markCancelled(taskId: string): Promise<void>;
43
+ cancelPendingForRun(runId: string): Promise<void>;
44
+ }
45
+
46
+ export const HumanTaskStoreToken = Symbol.for("codemation.core.HumanTaskStore") as TypeToken<
47
+ HumanTaskStore | undefined
48
+ >;
@@ -0,0 +1,58 @@
1
+ import type { TypeToken } from "../di";
2
+ import type { HumanTaskActor, HumanTaskHandle, HumanTaskSubject } from "./runtimeTypes";
3
+ import type { Item } from "./workflowTypes";
4
+
5
+ /**
6
+ * A single inbox delivery channel.
7
+ * Implementations: `LocalInboxChannel`, `ControlPlaneInboxChannel`.
8
+ */
9
+ export interface InboxChannel {
10
+ readonly kind: "local" | "control-plane-inbox";
11
+ deliver(args: InboxDeliverArgs): Promise<InboxDelivery>;
12
+ updateOnDecision?(args: InboxOnDecisionArgs): Promise<void>;
13
+ updateOnTimeout?(args: InboxOnTimeoutArgs): Promise<void>;
14
+ }
15
+
16
+ export type InboxDeliverArgs = Readonly<{
17
+ task: HumanTaskHandle;
18
+ subject: HumanTaskSubject;
19
+ priority: "low" | "normal" | "high";
20
+ item: Item;
21
+ /** Present in managed mode (from `PairingConfig.workspaceId`). */
22
+ workspaceId?: string;
23
+ }>;
24
+
25
+ export type InboxDelivery =
26
+ | { kind: "local"; inboxItemId: string }
27
+ | { kind: "cp"; inboxItemId: string; workspaceId: string };
28
+
29
+ export type InboxOnDecisionArgs = Readonly<{
30
+ delivery: InboxDelivery;
31
+ decision: { approved: boolean; note?: string };
32
+ actor: HumanTaskActor;
33
+ }>;
34
+
35
+ export type InboxOnTimeoutArgs = Readonly<{
36
+ delivery: InboxDelivery;
37
+ policy: "halt" | "auto-accept";
38
+ }>;
39
+
40
+ /**
41
+ * Resolves the correct `InboxChannel` for the current deployment mode
42
+ * (local dev vs. managed/CP). Implemented in `@codemation/host`.
43
+ */
44
+ export interface InboxChannelResolverSeam {
45
+ resolve(): { channel: InboxChannel; workspaceId?: string };
46
+ }
47
+
48
+ export const InboxChannelResolverToken = Symbol.for("codemation.core.InboxChannelResolver") as TypeToken<
49
+ InboxChannelResolverSeam | undefined
50
+ >;
51
+
52
+ export const LocalInboxChannelToken = Symbol.for("codemation.core.LocalInboxChannel") as TypeToken<
53
+ InboxChannel | undefined
54
+ >;
55
+
56
+ export const ControlPlaneInboxChannelToken = Symbol.for("codemation.core.ControlPlaneInboxChannel") as TypeToken<
57
+ InboxChannel | undefined
58
+ >;
@@ -1,5 +1,8 @@
1
1
  export * from "./AgentBindError";
2
2
  export * from "./agentMcpTypes";
3
+ export * from "./hitlSeamTypes";
4
+ export * from "./humanTaskStoreTypes";
5
+ export * from "./inboxChannelTypes";
3
6
  export * from "./NoOpAgentMcpIntegration";
4
7
  export * from "./baseTypes";
5
8
  export * from "./assertionTypes";