@codemation/core 0.13.1 → 0.14.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 (71) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/{CostCatalogContract-Dxq1BTyi.d.cts → CostCatalogContract-B9aYIqJu.d.cts} +2 -2
  3. package/dist/{EngineRuntimeRegistration.types-CqcTWexS.d.cts → EngineRuntimeRegistration.types-BYAmGMdS.d.cts} +3 -3
  4. package/dist/{EngineRuntimeRegistration.types-Cr75cSfL.d.ts → EngineRuntimeRegistration.types-CVLI8DsJ.d.ts} +2 -2
  5. package/dist/{InMemoryRunDataFactory-Csy2evr_.d.cts → InMemoryRunDataFactory-C3rIszrW.d.cts} +4 -2
  6. package/dist/{ItemsInputNormalizer-57EdA1ad.cjs → ItemsInputNormalizer-B9SdLG24.cjs} +2 -2
  7. package/dist/{ItemsInputNormalizer-57EdA1ad.cjs.map → ItemsInputNormalizer-B9SdLG24.cjs.map} +1 -1
  8. package/dist/{ItemsInputNormalizer-BkSvmfAW.js → ItemsInputNormalizer-CZEODg94.js} +2 -2
  9. package/dist/{ItemsInputNormalizer-BkSvmfAW.js.map → ItemsInputNormalizer-CZEODg94.js.map} +1 -1
  10. package/dist/{ItemsInputNormalizer-BWtlwdVI.d.ts → ItemsInputNormalizer-DoOawd9R.d.ts} +10 -2
  11. package/dist/{ItemsInputNormalizer-pLrWwUAP.d.cts → ItemsInputNormalizer-UCpn7luX.d.cts} +11 -3
  12. package/dist/{RunIntentService-BitgkKaT.d.cts → RunIntentService-0f3ICjAz.d.cts} +2 -2
  13. package/dist/{RunIntentService-DYpqfu6D.d.ts → RunIntentService-Dx_HHxDX.d.ts} +2 -2
  14. package/dist/{agentMcpTypes-DGIwk6Ue.d.cts → agentMcpTypes-B11B3Hd-.d.cts} +8 -1
  15. package/dist/bootstrap/index.cjs +3 -3
  16. package/dist/bootstrap/index.d.cts +5 -5
  17. package/dist/bootstrap/index.d.ts +5 -5
  18. package/dist/bootstrap/index.js +3 -3
  19. package/dist/{bootstrap-UDyH8OfK.cjs → bootstrap-Be0LB0nh.cjs} +3 -3
  20. package/dist/{bootstrap-UDyH8OfK.cjs.map → bootstrap-Be0LB0nh.cjs.map} +1 -1
  21. package/dist/{bootstrap-DB3jpo8F.js → bootstrap-pSQdsMfa.js} +3 -3
  22. package/dist/{bootstrap-DB3jpo8F.js.map → bootstrap-pSQdsMfa.js.map} +1 -1
  23. package/dist/browser.cjs +2 -2
  24. package/dist/browser.d.cts +3 -3
  25. package/dist/browser.d.ts +2 -2
  26. package/dist/browser.js +2 -2
  27. package/dist/contracts.d.cts +4 -4
  28. package/dist/contracts.d.ts +1 -1
  29. package/dist/{di-D9Mv3kF3.js → di-CEV6wTc4.js} +6 -5
  30. package/dist/di-CEV6wTc4.js.map +1 -0
  31. package/dist/{di-C-2ep8NZ.cjs → di-DhwtDRgs.cjs} +6 -5
  32. package/dist/di-DhwtDRgs.cjs.map +1 -0
  33. package/dist/{executionPersistenceContracts-CN9d7AnL.d.cts → executionPersistenceContracts-CX9Ql8N1.d.cts} +2 -2
  34. package/dist/{index-rllWL4r-.d.ts → index-CbJdbIHe.d.ts} +93 -6
  35. package/dist/{index-C2P-fOAx.d.ts → index-uPnD9EE6.d.ts} +51 -11
  36. package/dist/index.cjs +20 -7
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.cts +135 -16
  39. package/dist/index.d.ts +5 -5
  40. package/dist/index.js +19 -8
  41. package/dist/index.js.map +1 -1
  42. package/dist/{params-DRUr0F5v.d.cts → params-Dwl10Ws9.d.cts} +3 -4
  43. package/dist/{runtime-iHBN1jyD.js → runtime-CSunvf7A.js} +112 -15
  44. package/dist/runtime-CSunvf7A.js.map +1 -0
  45. package/dist/{runtime-rrH8-Ouq.cjs → runtime-n2tqRwaf.cjs} +117 -14
  46. package/dist/runtime-n2tqRwaf.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/ai/AiHost.ts +7 -0
  53. package/src/ai/NodeBackedToolConfig.ts +2 -0
  54. package/src/authoring/defineNode.types.ts +18 -7
  55. package/src/authoring/definePollingTrigger.types.ts +20 -5
  56. package/src/authoring/index.ts +1 -0
  57. package/src/authoring/nodeBaseOptions.types.ts +18 -0
  58. package/src/contracts/itemExpr.ts +15 -11
  59. package/src/contracts/workflowTypes.ts +7 -0
  60. package/src/contracts/workspaceFileTypes.ts +42 -2
  61. package/src/execution/NodeOutputNormalizer.ts +8 -1
  62. package/src/execution/RunnableOutputBehaviorResolver.ts +12 -0
  63. package/src/index.ts +10 -2
  64. package/src/workflow/dsl/ChainCursorResolver.ts +13 -0
  65. package/src/workflow/dsl/WhenBuilder.ts +66 -2
  66. package/src/workflow/dsl/workflowBuilderTypes.ts +29 -0
  67. package/src/workflowSnapshots/WorkflowSnapshotCodec.ts +1 -0
  68. package/dist/di-C-2ep8NZ.cjs.map +0 -1
  69. package/dist/di-D9Mv3kF3.js.map +0 -1
  70. package/dist/runtime-iHBN1jyD.js.map +0 -1
  71. package/dist/runtime-rrH8-Ouq.cjs.map +0 -1
package/dist/testing.cjs CHANGED
@@ -1,8 +1,8 @@
1
- const require_di = require('./di-C-2ep8NZ.cjs');
1
+ const require_di = require('./di-DhwtDRgs.cjs');
2
2
  require('./contracts-CK0x6w_G.cjs');
3
- const require_runtime = require('./runtime-rrH8-Ouq.cjs');
3
+ const require_runtime = require('./runtime-n2tqRwaf.cjs');
4
4
  const require_InMemoryRunEventBusRegistry = require('./InMemoryRunEventBusRegistry-Sa86VxuV.cjs');
5
- const require_bootstrap = require('./bootstrap-UDyH8OfK.cjs');
5
+ const require_bootstrap = require('./bootstrap-Be0LB0nh.cjs');
6
6
  let tsyringe = require("tsyringe");
7
7
  tsyringe = require_di.__toESM(tsyringe);
8
8
 
@@ -1,6 +1,6 @@
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";
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-B11B3Hd-.cjs";
2
+ import { n as InMemoryLiveWorkflowRepository, r as Engine, t as RunIntentService } from "./RunIntentService-0f3ICjAz.cjs";
3
+ import { a as WorkflowSnapshotCodec, i as EngineWorkflowRunnerService, t as EngineRuntimeRegistrationOptions } from "./EngineRuntimeRegistration.types-BYAmGMdS.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 { 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";
1
+ import { An as TypeToken, Ar as RunDataFactory, Cn as WorkflowExecutionRepository, Cr as NodeOffloadPolicy, Kn as RunEventBus, Lr as TriggerNodeConfig, Oo as NodeId, Or as ParentExecutionRef, Pr as RunnableNodeConfig, Ri as NodeExecutionContext, Un as EngineExecutionLimitsPolicy, Vi as NodeExecutionScheduler, Vr as WorkflowDefinition, Xi as RunnableNodeExecuteArgs, Yi as RunnableNode, ea as TriggerNode, ho as CredentialSessionService, ia as TriggerSetupStateRepository, ir as Item, jo as WorkflowId, la as WorkflowRunnerService, mn as RunResult, na as TriggerSetupContext, or as Items, wi as ExecutionContextFactory, wn as Container, wr as NodeOutputs, zi as NodeExecutionRequest } from "./index-CbJdbIHe.js";
2
+ import { i as WorkflowSnapshotCodec, n as InMemoryLiveWorkflowRepository, r as EngineWorkflowRunnerService, t as RunIntentService, u as Engine } from "./RunIntentService-Dx_HHxDX.js";
3
+ import { t as EngineRuntimeRegistrationOptions } from "./EngineRuntimeRegistration.types-CVLI8DsJ.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-D9Mv3kF3.js";
1
+ import { d as CoreTokens } from "./di-CEV6wTc4.js";
2
2
  import "./contracts-DXdfTdpW.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-iHBN1jyD.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-CSunvf7A.js";
4
4
  import { t as InMemoryRunEventBus } from "./InMemoryRunEventBusRegistry-Bwunvt1T.js";
5
- import { a as InMemoryWorkflowExecutionRepository, t as EngineRuntimeRegistrar } from "./bootstrap-DB3jpo8F.js";
5
+ import { a as InMemoryWorkflowExecutionRepository, t as EngineRuntimeRegistrar } from "./bootstrap-pSQdsMfa.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.13.1",
3
+ "version": "0.14.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/ai/AiHost.ts CHANGED
@@ -242,6 +242,13 @@ export type NodeBackedToolConfigOptions<
242
242
  outputSchema: TOutputSchema;
243
243
  mapInput?: NodeBackedToolInputMapper<TNodeConfig, ZodInput<TInputSchema>>;
244
244
  mapOutput?: NodeBackedToolOutputMapper<TNodeConfig, ZodInput<TInputSchema>, ZodOutput<TOutputSchema>>;
245
+ /**
246
+ * Marks THIS tool binding as human-in-the-loop and sets the behavior when a human rejects the
247
+ * approval: `"return"` feeds the rejection back to the agent as a tool result, `"halt"` stops the
248
+ * run. Set per binding, so two tools backed by the same node can reject differently. When set, it
249
+ * takes precedence over any `humanApprovalToolBehavior` marker carried by the backing node.
250
+ */
251
+ onRejected?: "halt" | "return";
245
252
  }>;
246
253
 
247
254
  export interface AgentNodeConfig<TInputJson = unknown, TOutputJson = unknown> extends RunnableNodeConfig<
@@ -22,6 +22,7 @@ export class NodeBackedToolConfig<
22
22
  readonly toolKind = "nodeBacked" as const;
23
23
  readonly description?: string;
24
24
  readonly presentation?: AgentCanvasPresentation;
25
+ readonly onRejected?: "halt" | "return";
25
26
  private readonly inputSchemaValue: TInputSchema;
26
27
  private readonly outputSchemaValue: TOutputSchema;
27
28
  private readonly mapInputValue?: NodeBackedToolInputMapper<TNodeConfig, ZodInput<TInputSchema>>;
@@ -39,6 +40,7 @@ export class NodeBackedToolConfig<
39
40
  this.type = node.type;
40
41
  this.description = options.description;
41
42
  this.presentation = options.presentation;
43
+ this.onRejected = options.onRejected;
42
44
  this.inputSchemaValue = options.inputSchema;
43
45
  this.outputSchemaValue = options.outputSchema;
44
46
  this.mapInputValue = options.mapInput;
@@ -12,6 +12,7 @@ import { node as persistedNode } from "../runtime-types/runtimeTypeDecorators.ty
12
12
  import type { ZodType } from "zod";
13
13
  import { z } from "zod";
14
14
  import { DefinedNodeRegistry } from "./DefinedNodeRegistry";
15
+ import type { NodeBaseOptions } from "./nodeBaseOptions.types";
15
16
 
16
17
  type MaybePromise<TValue> = TValue | Promise<TValue>;
17
18
 
@@ -87,7 +88,7 @@ export interface DefinedNode<
87
88
  create<TConfigItemJson = TInputJson>(
88
89
  config: DefinedNodeConfigInput<TConfig, TConfigItemJson>,
89
90
  name?: string,
90
- id?: string,
91
+ idOrOptions?: string | NodeBaseOptions,
91
92
  ): RunnableNodeConfig<TInputJson, TOutputJson>;
92
93
  register(context: { registerNode<TValue>(token: TypeToken<TValue>, implementation?: TypeToken<TValue>): void }): void;
93
94
  }
@@ -271,12 +272,17 @@ export function defineNode<
271
272
  readonly icon = options.icon;
272
273
  readonly inputSchema = options.inputSchema;
273
274
  readonly keepBinaries = options.keepBinaries ?? false;
275
+ readonly id?: string;
276
+ readonly description?: string;
274
277
 
275
278
  constructor(
276
279
  public readonly name: string,
277
280
  config: DefinedNodeConfigInput<TConfig, unknown>,
278
- public readonly id?: string,
281
+ idOrOptions?: string | NodeBaseOptions,
279
282
  ) {
283
+ const resolved = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
284
+ this.id = resolved?.id;
285
+ this.description = resolved?.description;
280
286
  this.config = config as unknown as TConfig;
281
287
  }
282
288
 
@@ -299,9 +305,9 @@ export function defineNode<
299
305
  create<TConfigItemJson = TInputJson>(
300
306
  config: DefinedNodeConfigInput<TConfig, TConfigItemJson>,
301
307
  name = options.title,
302
- id?: string,
308
+ idOrOptions?: string | NodeBaseOptions,
303
309
  ) {
304
- return new DefinedRunnableNodeConfig(name, config as DefinedNodeConfigInput<TConfig, unknown>, id);
310
+ return new DefinedRunnableNodeConfig(name, config as DefinedNodeConfigInput<TConfig, unknown>, idOrOptions);
305
311
  },
306
312
  register(context) {
307
313
  context.registerNode(DefinedNodeRuntime);
@@ -358,12 +364,17 @@ export function defineBatchNode<
358
364
  readonly kind = "node" as const;
359
365
  readonly type: TypeToken<unknown> = DefinedNodeRuntime;
360
366
  readonly icon = options.icon;
367
+ readonly id?: string;
368
+ readonly description?: string;
361
369
 
362
370
  constructor(
363
371
  public readonly name: string,
364
372
  config: DefinedNodeConfigInput<TConfig, unknown>,
365
- public readonly id?: string,
373
+ idOrOptions?: string | NodeBaseOptions,
366
374
  ) {
375
+ const resolved = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
376
+ this.id = resolved?.id;
377
+ this.description = resolved?.description;
367
378
  this.config = config as unknown as TConfig;
368
379
  }
369
380
 
@@ -386,9 +397,9 @@ export function defineBatchNode<
386
397
  create<TConfigItemJson = TInputJson>(
387
398
  config: DefinedNodeConfigInput<TConfig, TConfigItemJson>,
388
399
  name = options.title,
389
- id?: string,
400
+ idOrOptions?: string | NodeBaseOptions,
390
401
  ) {
391
- return new DefinedRunnableNodeConfig(name, config as DefinedNodeConfigInput<TConfig, unknown>, id);
402
+ return new DefinedRunnableNodeConfig(name, config as DefinedNodeConfigInput<TConfig, unknown>, idOrOptions);
392
403
  },
393
404
  register(context) {
394
405
  context.registerNode(DefinedNodeRuntime);
@@ -21,6 +21,7 @@ import type {
21
21
  } from "..";
22
22
  import type { CredentialJsonRecord, CredentialRequirement } from "../contracts/credentialTypes";
23
23
  import type { DefinedNodeCredentialAccessors, DefinedNodeCredentialBindings } from "./defineNode.types";
24
+ import type { NodeBaseOptions } from "./nodeBaseOptions.types";
24
25
  import { node as persistedNode } from "../runtime-types/runtimeTypeDecorators.types";
25
26
  import {
26
27
  definedNodeCredentialRequirementFactory,
@@ -170,9 +171,14 @@ export interface DefinedPollingTrigger<
170
171
  * Create the trigger config for use in workflow definitions.
171
172
  * @param cfg - User-facing trigger configuration.
172
173
  * @param name - Display name (defaults to `title`).
173
- * @param id - Optional stable node id.
174
+ * @param idOrOptions - Optional stable node id, or `{ id?, description? }` authoring options
175
+ * (a bare string id still works — back-compat).
174
176
  */
175
- create(cfg: TConfig, name?: string, id?: string): DefinedPollingTriggerConfig<TConfig, TItemJson>;
177
+ create(
178
+ cfg: TConfig,
179
+ name?: string,
180
+ idOrOptions?: string | NodeBaseOptions,
181
+ ): DefinedPollingTriggerConfig<TConfig, TItemJson>;
176
182
  /**
177
183
  * Test seam: call `poll` directly without starting the runtime.
178
184
  * Returns `{ items, nextState }` just like the real runtime receives.
@@ -203,6 +209,8 @@ export class DefinedPollingTriggerConfig<TConfig extends CredentialJsonRecord, T
203
209
  readonly kind = "trigger" as const;
204
210
  readonly type: TypeToken<unknown>;
205
211
  readonly icon: string | undefined;
212
+ readonly id?: string;
213
+ readonly description?: string;
206
214
 
207
215
  constructor(
208
216
  public readonly name: string,
@@ -210,13 +218,16 @@ export class DefinedPollingTriggerConfig<TConfig extends CredentialJsonRecord, T
210
218
  typeToken: TypeToken<unknown>,
211
219
  icon: string | undefined,
212
220
  private readonly credentialRequirements: ReadonlyArray<CredentialRequirement>,
213
- public readonly id?: string,
221
+ idOrOptions?: string | NodeBaseOptions,
214
222
  private readonly inspectorSummaryFn?: (
215
223
  args: Readonly<{ config: TConfig }>,
216
224
  ) => ReadonlyArray<NodeInspectorSummaryRow> | undefined,
217
225
  ) {
218
226
  this.type = typeToken;
219
227
  this.icon = icon;
228
+ const resolved = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
229
+ this.id = resolved?.id;
230
+ this.description = resolved?.description;
220
231
  }
221
232
 
222
233
  getCredentialRequirements(): ReadonlyArray<CredentialRequirement> {
@@ -385,14 +396,18 @@ export function definePollingTrigger<
385
396
  title: options.title,
386
397
  description: options.description,
387
398
 
388
- create(cfg: TConfig, name = options.title, id?: string): DefinedPollingTriggerConfig<TConfig, TItemJson> {
399
+ create(
400
+ cfg: TConfig,
401
+ name = options.title,
402
+ idOrOptions?: string | NodeBaseOptions,
403
+ ): DefinedPollingTriggerConfig<TConfig, TItemJson> {
389
404
  return new DefinedPollingTriggerConfig<TConfig, TItemJson>(
390
405
  name,
391
406
  cfg,
392
407
  DefinedPollingTriggerRuntime,
393
408
  options.icon,
394
409
  credentialRequirements,
395
- id,
410
+ idOrOptions,
396
411
  options.inspectorSummary as
397
412
  | ((args: Readonly<{ config: TConfig }>) => ReadonlyArray<NodeInspectorSummaryRow> | undefined)
398
413
  | undefined,
@@ -1,4 +1,5 @@
1
1
  export { DefinedNodeRegistry } from "./DefinedNodeRegistry";
2
+ export type { NodeBaseOptions } from "./nodeBaseOptions.types";
2
3
  export type {
3
4
  DefinedNode,
4
5
  DefinedNodeConfigInput,
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Core-local copy of the per-instance authoring options every `define*` factory's `create(...)`
3
+ * accepts in its trailing argument: a stable `id` plus a plain-language `description` (the
4
+ * non-technical "what does this step do" line surfaced in the node sidebar).
5
+ *
6
+ * `description` is a first-class option — passed inline exactly like `id` — and is threaded onto
7
+ * the config instance as an OWN ENUMERABLE field so it flows into the persisted workflow snapshot
8
+ * (`WorkflowSnapshotCodec` serializes via `JSON.parse(JSON.stringify(config))`, which only captures
9
+ * own enumerable properties — never a getter).
10
+ *
11
+ * `@codemation/core-nodes` declares an identical `NodeBaseOptions` for its bare-id built-in nodes;
12
+ * core keeps its own copy because core must not depend on `@codemation/core-nodes` (dependency
13
+ * direction). The two types are structurally identical by design.
14
+ */
15
+ export interface NodeBaseOptions {
16
+ readonly id?: string;
17
+ readonly description?: string;
18
+ }
@@ -1,7 +1,11 @@
1
1
  import type { NodeExecutionContext } from "./runtimeTypes";
2
2
  import type { Item, Items, NodeActivationId, NodeId, RunDataSnapshot, RunId, WorkflowId } from "./workflowTypes";
3
3
 
4
- const ITEM_EXPR_BRAND = Symbol.for("codemation.itemExpr");
4
+ // Structural string-literal brand (NOT a `unique symbol`): independently-bundled copies of
5
+ // `ItemExpr` across node packages must be STRUCTURALLY identical to stay cross-assignable. A
6
+ // `unique symbol` makes each bundled copy a nominally-distinct type, which breaks passing an
7
+ // `itemExpr()` into a node-config field whose package resolved a different (duplicated) copy of
8
+ // core. A plain string-literal property is identical across copies and also survives JSON.
5
9
 
6
10
  export type ItemExprResolvedContext = Readonly<{
7
11
  runId: RunId;
@@ -26,12 +30,12 @@ export type ItemExprArgs<TItemJson = unknown> = Readonly<{
26
30
  export type ItemExprCallback<T, TItemJson = unknown> = (args: ItemExprArgs<TItemJson>) => T | Promise<T>;
27
31
 
28
32
  export type ItemExpr<T, TItemJson = unknown> = Readonly<{
29
- readonly [ITEM_EXPR_BRAND]: true;
33
+ readonly __codemationItemExpr: "codemation.itemExpr";
30
34
  readonly fn: ItemExprCallback<T, TItemJson>;
31
35
  }>;
32
36
 
33
37
  export function itemExpr<T, TItemJson = unknown>(fn: ItemExprCallback<T, TItemJson>): ItemExpr<T, TItemJson> {
34
- return { [ITEM_EXPR_BRAND]: true, fn };
38
+ return { __codemationItemExpr: "codemation.itemExpr", fn };
35
39
  }
36
40
 
37
41
  export function isItemExpr<T, TItemJson = unknown>(value: unknown): value is ItemExpr<T, TItemJson> {
@@ -39,21 +43,21 @@ export function isItemExpr<T, TItemJson = unknown>(value: unknown): value is Ite
39
43
  return false;
40
44
  }
41
45
  const v = value as Record<PropertyKey, unknown>;
42
- if (v[ITEM_EXPR_BRAND] === true) {
43
- return true;
44
- }
45
- // Support snapshot-hydrated itemExpr wrappers where the symbol brand was lost but the callback survived.
46
- // Workflow snapshot hydration currently restores function-valued fields (like `fn`) but may drop symbol-keyed brands.
47
- // We treat the minimal `{ fn: Function }` shape as an itemExpr wrapper to keep runnable configs working.
48
- const keys = Object.keys(v);
49
- if (keys.length === 1 && keys[0] === "fn" && typeof (v as { fn?: unknown }).fn === "function") {
46
+ // Current structural brand (a plain string property; survives JSON serialization).
47
+ if (v["__codemationItemExpr"] === "codemation.itemExpr" && typeof v["fn"] === "function") {
50
48
  return true;
51
49
  }
50
+ // Legacy: the old `Symbol.for("codemation.itemExpr")` brand (configs from an older core build).
52
51
  for (const sym of Object.getOwnPropertySymbols(v)) {
53
52
  if (sym.description === "codemation.itemExpr" && v[sym] === true) {
54
53
  return true;
55
54
  }
56
55
  }
56
+ // Snapshot-hydrated wrappers where the brand was dropped but the callback survived.
57
+ const keys = Object.keys(v);
58
+ if (keys.length === 1 && keys[0] === "fn" && typeof (v as { fn?: unknown }).fn === "function") {
59
+ return true;
60
+ }
57
61
  return false;
58
62
  }
59
63
 
@@ -76,6 +76,13 @@ export interface NodeConfigBase {
76
76
  readonly name?: string;
77
77
  readonly id?: NodeId;
78
78
  readonly icon?: string;
79
+ /**
80
+ * Plain-language, non-technical explanation of what this node does, surfaced in the workflow
81
+ * inspector / node properties sidebar. A first-class config option every authorable node accepts
82
+ * directly (alongside `id`), so it flows into the persisted config the mappers read. Distinct from
83
+ * {@link inspectorSummary} (config-derived label/value rows).
84
+ */
85
+ readonly description?: string;
79
86
  readonly execution?: Readonly<{ hint?: "local" | "worker"; queue?: string }>;
80
87
  /** In-process execute retries (runnable nodes). Triggers typically omit this. */
81
88
  readonly retryPolicy?: RetryPolicySpec;
@@ -19,11 +19,11 @@ export interface WorkspaceFileMetadata {
19
19
  }
20
20
 
21
21
  /**
22
- * Read-only, workspace-scoped port for accessing the shared workspace file pool.
22
+ * Workspace-scoped port for accessing the shared workspace file pool.
23
23
  * Implemented in `@codemation/host`; nodes reach it via `ctx.resolve(WorkspaceFileStorageToken)`.
24
24
  *
25
25
  * Key scheme: `<workspaceId>/files/<fileId>` — but nodes never construct raw keys.
26
- * Use the workspace-level helpers (`listFiles`, `getFileByName`, `getFileById`) instead.
26
+ * Use the workspace-level helpers (`listFiles`, `getFileByName`, `getFileById`, `writeFile`) instead.
27
27
  *
28
28
  * This adapter is SEPARATE from the run-scoped BinaryStorage — do not confuse the two.
29
29
  */
@@ -52,6 +52,25 @@ export interface IWorkspaceFileStorage {
52
52
  * @throws WorkspaceFileNotFoundError when the key does not exist.
53
53
  */
54
54
  getStream(key: string): Promise<ReadableStream<Uint8Array>>;
55
+
56
+ /**
57
+ * Writes a file into the workspace's shared file pool.
58
+ *
59
+ * Generates a new fileId internally; the caller never constructs a raw key.
60
+ * Stamps the filename into object metadata so read-side adapters can resolve
61
+ * files by name without querying a registry.
62
+ *
63
+ * Returns the stored file's metadata (fileId, key, filename, contentType, size,
64
+ * lastModified). Callers that need concierge visibility must also register a
65
+ * WorkspaceFile row on the CP side via the HMAC-paired
66
+ * `POST /internal/workspace-files/register` endpoint — see the host-side
67
+ * `WorkspaceFileRegistrarClient`.
68
+ *
69
+ * @param filename Original filename; stored in object metadata.
70
+ * @param body Bytes to write (contiguous — required for presigned PUT Content-Length).
71
+ * @param contentType MIME type.
72
+ */
73
+ writeFile(filename: string, body: Uint8Array, contentType: string): Promise<WorkspaceFileMetadata>;
55
74
  }
56
75
 
57
76
  /**
@@ -64,6 +83,17 @@ export class WorkspaceFileNotFoundError extends Error {
64
83
  }
65
84
  }
66
85
 
86
+ /**
87
+ * Port for registering a workflow-written file in the CP's WorkspaceFile table.
88
+ * Optional: only wired when the host is paired with a control plane.
89
+ * Implemented by `WorkspaceFileRegistrarClient` in `@codemation/host`.
90
+ * Nodes call this after `writeFile` so the concierge can see the file via
91
+ * `list_files` / `get_file`.
92
+ */
93
+ export interface IWorkspaceFileRegistrar {
94
+ register(meta: WorkspaceFileMetadata): Promise<void>;
95
+ }
96
+
67
97
  /**
68
98
  * DI token for the workspace-scoped file storage adapter.
69
99
  * Registered by `@codemation/host`; resolved by workspace-file nodes via `ctx.resolve(...)`.
@@ -71,3 +101,13 @@ export class WorkspaceFileNotFoundError extends Error {
71
101
  export const WorkspaceFileStorageToken = Symbol.for("codemation.core.WorkspaceFileStorage") as TypeToken<
72
102
  IWorkspaceFileStorage | undefined
73
103
  >;
104
+
105
+ /**
106
+ * DI token for the optional CP registry hook.
107
+ * Present only when the host is paired (managed mode). Standalone / local-fs
108
+ * deployments leave this undefined — workflow-written files will be in S3/local
109
+ * but will not appear in the concierge's list_files.
110
+ */
111
+ export const WorkspaceFileRegistrarToken = Symbol.for("codemation.core.WorkspaceFileRegistrar") as TypeToken<
112
+ IWorkspaceFileRegistrar | undefined
113
+ >;
@@ -73,10 +73,17 @@ export class NodeOutputNormalizer {
73
73
  return typeof value === "object" && value !== null && "json" in value;
74
74
  }
75
75
 
76
+ private isPlainJsonObject(value: unknown): value is Record<string, unknown> {
77
+ return typeof value === "object" && value !== null && !Array.isArray(value);
78
+ }
79
+
76
80
  private applyOutput(baseItem: Item, next: Item, behavior: RunnableOutputBehavior): Item {
77
81
  const explicitBinary = next.binary;
78
82
  return {
79
- json: next.json,
83
+ json:
84
+ behavior.mergeJson && this.isPlainJsonObject(baseItem.json) && this.isPlainJsonObject(next.json)
85
+ ? { ...baseItem.json, ...next.json }
86
+ : next.json,
80
87
  ...(explicitBinary !== undefined
81
88
  ? { binary: explicitBinary }
82
89
  : behavior.keepBinaries && baseItem.binary
@@ -5,14 +5,21 @@ type BinaryKeepingRunnableNodeConfig = RunnableNodeConfig &
5
5
  keepBinaries?: boolean;
6
6
  }>;
7
7
 
8
+ type MergeJsonRunnableNodeConfig = RunnableNodeConfig &
9
+ Readonly<{
10
+ mergeJson?: boolean;
11
+ }>;
12
+
8
13
  export type RunnableOutputBehavior = Readonly<{
9
14
  keepBinaries: boolean;
15
+ mergeJson: boolean;
10
16
  }>;
11
17
 
12
18
  export class RunnableOutputBehaviorResolver {
13
19
  resolve(config: RunnableNodeConfig): RunnableOutputBehavior {
14
20
  return {
15
21
  keepBinaries: this.isKeepBinariesEnabled(config),
22
+ mergeJson: this.isMergeJsonEnabled(config),
16
23
  };
17
24
  }
18
25
 
@@ -20,4 +27,9 @@ export class RunnableOutputBehaviorResolver {
20
27
  const candidate = config as BinaryKeepingRunnableNodeConfig;
21
28
  return candidate.keepBinaries === true;
22
29
  }
30
+
31
+ private isMergeJsonEnabled(config: RunnableNodeConfig): boolean {
32
+ const candidate = config as MergeJsonRunnableNodeConfig;
33
+ return candidate.mergeJson === true;
34
+ }
23
35
  }
package/src/index.ts CHANGED
@@ -84,5 +84,13 @@ export type {
84
84
  export { IllegalMaterialSourceError } from "./credentials/CredentialMaterialProvider.types";
85
85
  export { ManagedCredentialMaterialWriteError } from "./credentials/ManagedCredentialMaterialWriteError";
86
86
  export { ManagedMaterialFetchError } from "./credentials/ManagedMaterialFetchError";
87
- export type { IWorkspaceFileStorage, WorkspaceFileMetadata } from "./contracts/workspaceFileTypes";
88
- export { WorkspaceFileNotFoundError, WorkspaceFileStorageToken } from "./contracts/workspaceFileTypes";
87
+ export type {
88
+ IWorkspaceFileStorage,
89
+ IWorkspaceFileRegistrar,
90
+ WorkspaceFileMetadata,
91
+ } from "./contracts/workspaceFileTypes";
92
+ export {
93
+ WorkspaceFileNotFoundError,
94
+ WorkspaceFileStorageToken,
95
+ WorkspaceFileRegistrarToken,
96
+ } from "./contracts/workspaceFileTypes";
@@ -19,6 +19,7 @@ import type {
19
19
  BranchStepsArg,
20
20
  StepSequenceOutput,
21
21
  } from "./workflowBuilderTypes";
22
+ import { mergeForward } from "./workflowBuilderTypes";
22
23
 
23
24
  type ChainCursorEndpoint = Readonly<{ node: NodeRef; output: OutputPortKey; inputPortHint?: InputPortKey }>;
24
25
 
@@ -56,6 +57,18 @@ export class ChainCursor<TCurrentJson> {
56
57
  ]);
57
58
  }
58
59
 
60
+ /**
61
+ * Append a step whose output is MERGED onto `item.json` (shallow, output-wins) instead of
62
+ * replacing it — so earlier fields (e.g. trigger metadata) survive a transform/OCR/extraction
63
+ * node. Use for any node in a pipeline where you need data from before it.
64
+ */
65
+ thenMerge<TOutputJson, TConfig extends RunnableNodeConfig<TCurrentJson, TOutputJson>>(
66
+ config: TConfig,
67
+ ): ChainCursor<TCurrentJson & RunnableNodeOutputJson<TConfig>> {
68
+ mergeForward(config);
69
+ return this.then(config) as unknown as ChainCursor<TCurrentJson & RunnableNodeOutputJson<TConfig>>;
70
+ }
71
+
59
72
  thenIntoInputHints<TOutputJson, TConfig extends RunnableNodeConfig<any, TOutputJson>>(
60
73
  config: TConfig,
61
74
  ): ChainCursor<RunnableNodeOutputJson<TConfig>> {
@@ -1,13 +1,33 @@
1
- import type { NodeId, NodeRef, OutputPortKey, UpstreamRefPlaceholder, WorkflowDefinition } from "../../types";
1
+ import type {
2
+ InputPortKey,
3
+ NodeId,
4
+ NodeRef,
5
+ OutputPortKey,
6
+ RunnableNodeConfig,
7
+ RunnableNodeOutputJson,
8
+ UpstreamRefPlaceholder,
9
+ WorkflowDefinition,
10
+ } from "../../types";
2
11
 
12
+ import type { DefinedNodeCredentialBindings } from "../../authoring/defineNode.types";
13
+ import type { DefinedHumanApprovalNode, HumanApprovalOutputJson } from "../../authoring/defineHumanApprovalNode.types";
3
14
  import { WorkflowBuilder } from "./WorkflowBuilder";
15
+ import { ChainCursor } from "./ChainCursorResolver";
4
16
  import type { AnyRunnableNodeConfig, BooleanWhenOverloads, ValidStepSequence } from "./workflowBuilderTypes";
5
17
 
18
+ /** Structurally identical to ChainCursorResolver's (unexported) ChainCursorEndpoint. */
19
+ type WhenEndpoint = Readonly<{ node: NodeRef; output: OutputPortKey; inputPortHint?: InputPortKey }>;
20
+
6
21
  export class WhenBuilder<TCurrentJson> {
22
+ /** Tail endpoint of the arm this builder added (set by addBranch). */
23
+ private armEndpoint: WhenEndpoint | undefined;
24
+
7
25
  constructor(
8
26
  private readonly wf: WorkflowBuilder,
9
27
  private readonly from: NodeRef,
10
28
  private readonly branchPort: OutputPortKey,
29
+ /** Tails of arms added by earlier `.when(...)` calls in this chain. */
30
+ private readonly priorEndpoints: ReadonlyArray<WhenEndpoint> = [],
11
31
  ) {}
12
32
 
13
33
  addBranch<TSteps extends ReadonlyArray<AnyRunnableNodeConfig>>(
@@ -36,6 +56,12 @@ export class WhenBuilder<TCurrentJson> {
36
56
  });
37
57
  }
38
58
 
59
+ // An empty arm rejoins straight from the `from` node's branch port (mirrors the
60
+ // object-form `buildBranch` returning `{ end: cursor, endOutput: port }`).
61
+ this.armEndpoint = prev
62
+ ? { node: prev, output: "main", inputPortHint: this.branchPort }
63
+ : { node: this.from, output: this.branchPort, inputPortHint: this.branchPort };
64
+
39
65
  return this;
40
66
  }
41
67
 
@@ -46,12 +72,50 @@ export class WhenBuilder<TCurrentJson> {
46
72
  ): WhenBuilder<TCurrentJson> => {
47
73
  const list = Array.isArray(steps) ? steps : [steps, ...more];
48
74
  const port: OutputPortKey = branch ? "true" : "false";
49
- const b = new WhenBuilder<TCurrentJson>(this.wf, this.from, port);
75
+ const b = new WhenBuilder<TCurrentJson>(this.wf, this.from, port, this.accumulatedEndpoints);
50
76
  b.addBranch(list);
51
77
  return b;
52
78
  };
53
79
 
80
+ /**
81
+ * Continue the trunk after a boolean `.when(...)` branch chain, auto-merging EVERY branch
82
+ * tail accumulated across the chain into the next node — the same fan-in the object form
83
+ * produces. Typed as `ChainCursor<TCurrentJson>` (the pre-branch item type): boolean arms
84
+ * carry no output guard so the merged item type is underdetermined — use the object form
85
+ * `.when({ true: [...], false: [...] })` when you need a precise merged type inline.
86
+ */
87
+ then<TOutputJson, TConfig extends RunnableNodeConfig<TCurrentJson, TOutputJson>>(
88
+ config: TConfig,
89
+ ): ChainCursor<RunnableNodeOutputJson<TConfig>> {
90
+ return this.toCursor().then(config);
91
+ }
92
+
93
+ /**
94
+ * Chainable human-approval step after a boolean `.when(...)` branch chain — merges every
95
+ * branch tail into the approval node. Mirrors `ChainCursor.humanApproval`.
96
+ */
97
+ humanApproval<
98
+ TKey extends string,
99
+ TConfig extends Record<string, unknown>,
100
+ TBindings extends DefinedNodeCredentialBindings | undefined = undefined,
101
+ >(
102
+ node: DefinedHumanApprovalNode<TKey, TConfig, TCurrentJson & Record<string, unknown>, TBindings>,
103
+ config: TConfig,
104
+ metadata?: { name?: string; nodeId?: string },
105
+ ): ChainCursor<HumanApprovalOutputJson<TCurrentJson & Record<string, unknown>>> {
106
+ return this.toCursor().humanApproval(node, config, metadata);
107
+ }
108
+
54
109
  build(): WorkflowDefinition {
55
110
  return this.wf.build();
56
111
  }
112
+
113
+ /** Endpoints of every arm added so far in this chain (prior arms + this one). */
114
+ private get accumulatedEndpoints(): ReadonlyArray<WhenEndpoint> {
115
+ return this.armEndpoint ? [...this.priorEndpoints, this.armEndpoint] : this.priorEndpoints;
116
+ }
117
+
118
+ private toCursor(): ChainCursor<TCurrentJson> {
119
+ return new ChainCursor<TCurrentJson>(this.wf, this.accumulatedEndpoints);
120
+ }
57
121
  }
@@ -1,5 +1,34 @@
1
1
  import type { RunnableNodeConfig, RunnableNodeOutputJson, TriggerNodeConfig } from "../../types";
2
2
 
3
+ /**
4
+ * Flags a node's config so its output is MERGED onto `item.json` (shallow, output-wins) instead of
5
+ * replacing it — the config-level twin of {@link ChainCursor.thenMerge}.
6
+ *
7
+ * Unlike `.thenMerge` (a cursor method that only works on the trunk), `mergeForward` operates on a
8
+ * bare config, so it is usable in ANY position — including inside a `.when({ true: [...] })` branch
9
+ * arm, where the steps are a flat array of configs (not a cursor). Wrap a payload-REPLACING node
10
+ * (e.g. an extractor) in `mergeForward(node.create(...))` so prior fields survive into the next step.
11
+ *
12
+ * The OUTPUT type becomes `TIn & TOut` (the intersection), mirroring `.thenMerge`'s return type at
13
+ * the config level — so the next step in the branch array sees both the prior fields and the node's
14
+ * output. The INPUT type stays `TIn` (the node still receives the pre-merge item).
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * workflow
19
+ * .trigger(...)
20
+ * .when({
21
+ * true: [mergeForward(extractor.create(...))], // output merges onto item.json; { a } survives
22
+ * false: [...],
23
+ * })
24
+ * .build();
25
+ * ```
26
+ */
27
+ export function mergeForward<TIn, TOut>(config: RunnableNodeConfig<TIn, TOut>): RunnableNodeConfig<TIn, TIn & TOut> {
28
+ (config as { mergeJson?: boolean }).mergeJson = true;
29
+ return config as unknown as RunnableNodeConfig<TIn, TIn & TOut>;
30
+ }
31
+
3
32
  export type AnyRunnableNodeConfig = RunnableNodeConfig<any, any>;
4
33
 
5
34
  export type AnyTriggerNodeConfig = TriggerNodeConfig<any>;