@codemation/core-nodes 0.8.0 → 0.9.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @codemation/core-nodes
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#167](https://github.com/MadeRelevant/codemation/pull/167) [`3044474`](https://github.com/MadeRelevant/codemation/commit/3044474495525490735510ff74500b53761284b6) Thanks [@cblokland90](https://github.com/cblokland90)! - feat(hitl): Human-in-the-Loop — engine suspend/resume, inbox approval node + channels (local + control-plane), agent-as-tool, decision/timeout handling, inbox decision UX (toast + node status icons + "waiting for approval"), plus the consolidated dev/canvas/host fixes shipped alongside.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [[`3044474`](https://github.com/MadeRelevant/codemation/commit/3044474495525490735510ff74500b53761284b6)]:
12
+ - @codemation/core@0.12.0
13
+
14
+ ## 0.8.1
15
+
16
+ ### Patch Changes
17
+
18
+ - [#157](https://github.com/MadeRelevant/codemation/pull/157) [`3025b86`](https://github.com/MadeRelevant/codemation/commit/3025b8685b0d7ad60c506b5a0f21967e681a25ea) Thanks [@cblokland90](https://github.com/cblokland90)! - Shrink workspace-host Docker image by decoupling CLI from next-host at runtime.
19
+
20
+ `@codemation/cli`: demote `@codemation/next-host` from `dependencies` to `devDependencies`. The CLI's
21
+ non-headless serve path resolves the next-host package at runtime via `require.resolve()`; the
22
+ headless path (used by workspace-host pods) never touches it. Consumers that install `@codemation/cli`
23
+ from the registry and need the UI shell must add `@codemation/next-host` as a direct dependency.
24
+
25
+ `@codemation/core-nodes`: demote `lucide-react` from `dependencies` to `devDependencies`. The package
26
+ only references lucide icon names as strings (e.g. `"lucide:bot"`); it never imports the react library
27
+ at runtime. This removes ~46 MB from runtime installs of `@codemation/core-nodes`.
28
+
29
+ `@codemation/host`: promote `execa` and `dotenv` from `devDependencies` to `dependencies`. Both are
30
+ required at Dockerfile build time by `scripts/generate-prisma-clients.mjs` (imports `execaSync` from
31
+ `execa`) and `prisma.config.ts` (imports `dotenv/config`). These files run during `prisma:generate`
32
+ which executes in the production builder stage with `--prod` install (no devDeps available).
33
+
34
+ - Updated dependencies [[`e0933eb`](https://github.com/MadeRelevant/codemation/commit/e0933ebc51806a9593f94758860c591b8346a7a5)]:
35
+ - @codemation/core@0.11.1
36
+
3
37
  ## 0.8.0
4
38
 
5
39
  ### Minor Changes
package/LICENSE CHANGED
@@ -1 +1,37 @@
1
- ../../LICENSE
1
+ Codemation Pre-Stable License
2
+
3
+ Copyright (c) Made Relevant B.V. All rights reserved.
4
+
5
+ 1. Definitions
6
+
7
+ "Software" means the Codemation source code, documentation, and artifacts in this repository and any published npm packages in the Codemation monorepo.
8
+
9
+ "Stable Version" means the first published release of the package `@codemation/core` on the public npm registry with version 1.0.0 or higher.
10
+
11
+ 2. Permitted use (before Stable Version)
12
+
13
+ Until a Stable Version exists, you may use, copy, modify, and distribute the Software only for non-commercial purposes, including personal learning, research, evaluation, and internal use within your organization that does not charge third parties for access to the Software or a product or service whose primary value is the Software.
14
+
15
+ 3. Restrictions (before Stable Version)
16
+
17
+ Until a Stable Version exists, you must not:
18
+
19
+ a) Sell, rent, lease, or sublicense the Software or a derivative work for a fee;
20
+
21
+ b) Offer the Software or a derivative work as part of a paid product or service (including hosting, support, or consulting) where the Software is a material part of the offering;
22
+
23
+ c) Use the Software or a derivative work primarily to generate revenue or commercial advantage for you or others.
24
+
25
+ These restrictions apply to all versions published before a Stable Version, even if a later Stable Version is released under different terms.
26
+
27
+ 4. After Stable Version
28
+
29
+ The maintainers may publish a Stable Version under different license terms. If they do, those terms apply only to that Stable Version and subsequent releases they designate; they do not automatically apply to earlier pre-stable versions.
30
+
31
+ 5. No warranty
32
+
33
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
34
+
35
+ 6. Third-party components
36
+
37
+ The Software may include third-party components under their own licenses. Those licenses govern those components.
package/dist/index.cjs CHANGED
@@ -1309,6 +1309,7 @@ const string$1 = (params) => {
1309
1309
  };
1310
1310
  const integer = /^-?\d+$/;
1311
1311
  const number$1 = /^-?\d+(?:\.\d+)?$/;
1312
+ const boolean$1 = /^(?:true|false)$/i;
1312
1313
  const lowercase = /^[^A-Z]*$/;
1313
1314
  const uppercase = /^[^a-z]*$/;
1314
1315
 
@@ -2087,6 +2088,24 @@ const $ZodNumberFormat = /* @__PURE__ */ $constructor("$ZodNumberFormat", (inst,
2087
2088
  $ZodCheckNumberFormat.init(inst, def);
2088
2089
  $ZodNumber.init(inst, def);
2089
2090
  });
2091
+ const $ZodBoolean = /* @__PURE__ */ $constructor("$ZodBoolean", (inst, def) => {
2092
+ $ZodType.init(inst, def);
2093
+ inst._zod.pattern = boolean$1;
2094
+ inst._zod.parse = (payload, _ctx) => {
2095
+ if (def.coerce) try {
2096
+ payload.value = Boolean(payload.value);
2097
+ } catch (_) {}
2098
+ const input = payload.value;
2099
+ if (typeof input === "boolean") return payload;
2100
+ payload.issues.push({
2101
+ expected: "boolean",
2102
+ code: "invalid_type",
2103
+ input,
2104
+ inst
2105
+ });
2106
+ return payload;
2107
+ };
2108
+ });
2090
2109
  const $ZodUnknown = /* @__PURE__ */ $constructor("$ZodUnknown", (inst, def) => {
2091
2110
  $ZodType.init(inst, def);
2092
2111
  inst._zod.parse = (payload) => payload;
@@ -3163,6 +3182,13 @@ function _int(Class, params) {
3163
3182
  });
3164
3183
  }
3165
3184
  /* @__NO_SIDE_EFFECTS__ */
3185
+ function _boolean(Class, params) {
3186
+ return new Class({
3187
+ type: "boolean",
3188
+ ...normalizeParams(params)
3189
+ });
3190
+ }
3191
+ /* @__NO_SIDE_EFFECTS__ */
3166
3192
  function _unknown(Class) {
3167
3193
  return new Class({ type: "unknown" });
3168
3194
  }
@@ -3329,6 +3355,17 @@ function _array(Class, element, params) {
3329
3355
  });
3330
3356
  }
3331
3357
  /* @__NO_SIDE_EFFECTS__ */
3358
+ function _custom(Class, fn, _params) {
3359
+ const norm = normalizeParams(_params);
3360
+ norm.abort ?? (norm.abort = true);
3361
+ return new Class({
3362
+ type: "custom",
3363
+ check: "custom",
3364
+ fn,
3365
+ ...norm
3366
+ });
3367
+ }
3368
+ /* @__NO_SIDE_EFFECTS__ */
3332
3369
  function _refine(Class, fn, _params) {
3333
3370
  return new Class({
3334
3371
  type: "custom",
@@ -4925,6 +4962,14 @@ const ZodNumberFormat = /* @__PURE__ */ $constructor("ZodNumberFormat", (inst, d
4925
4962
  function int(params) {
4926
4963
  return _int(ZodNumberFormat, params);
4927
4964
  }
4965
+ const ZodBoolean = /* @__PURE__ */ $constructor("ZodBoolean", (inst, def) => {
4966
+ $ZodBoolean.init(inst, def);
4967
+ ZodType.init(inst, def);
4968
+ inst._zod.processJSONSchema = (ctx, json, params) => booleanProcessor(inst, ctx, json, params);
4969
+ });
4970
+ function boolean(params) {
4971
+ return _boolean(ZodBoolean, params);
4972
+ }
4928
4973
  const ZodUnknown = /* @__PURE__ */ $constructor("ZodUnknown", (inst, def) => {
4929
4974
  $ZodUnknown.init(inst, def);
4930
4975
  ZodType.init(inst, def);
@@ -5236,6 +5281,9 @@ const ZodCustom = /* @__PURE__ */ $constructor("ZodCustom", (inst, def) => {
5236
5281
  ZodType.init(inst, def);
5237
5282
  inst._zod.processJSONSchema = (ctx, json, params) => customProcessor(inst, ctx, json, params);
5238
5283
  });
5284
+ function custom(fn, _params) {
5285
+ return _custom(ZodCustom, fn ?? (() => true), _params);
5286
+ }
5239
5287
  function refine(fn, _params = {}) {
5240
5288
  return _refine(ZodCustom, fn, _params);
5241
5289
  }
@@ -5485,12 +5533,22 @@ let AgentToolExecutionCoordinator = class AgentToolExecutionCoordinator$1 {
5485
5533
  this.repairPolicy = repairPolicy;
5486
5534
  }
5487
5535
  async execute(args) {
5536
+ if (args.plannedToolCalls.filter((c) => c.binding.humanApproval !== void 0).length > 0 && args.plannedToolCalls.length > 1) return args.plannedToolCalls.map((c) => ({
5537
+ toolName: c.binding.config.name,
5538
+ toolCallId: c.toolCall.id ?? c.binding.config.name,
5539
+ result: { error: c.binding.humanApproval !== void 0 ? `HITL tool '${c.binding.config.name}' cannot be called alongside other tools in the same turn; call it alone.` : `deferred: a HITL tool in the same turn blocked execution. Retry this tool alone in the next turn.` },
5540
+ serialized: JSON.stringify({ error: c.binding.humanApproval !== void 0 ? `HITL tool '${c.binding.config.name}' cannot be called alongside other tools in the same turn; call it alone.` : `deferred: a HITL tool in the same turn blocked execution. Retry this tool alone in the next turn.` })
5541
+ }));
5488
5542
  const results = await Promise.allSettled(args.plannedToolCalls.map(async (plannedToolCall) => await this.executePlannedToolCall({
5489
5543
  ...args,
5490
5544
  plannedToolCall
5491
5545
  })));
5492
5546
  const rejected = results.find((result) => result.status === "rejected");
5493
- if (rejected?.status === "rejected") throw rejected.reason instanceof Error ? rejected.reason : new Error(String(rejected.reason));
5547
+ if (rejected?.status === "rejected") {
5548
+ const reason = rejected.reason;
5549
+ if (reason instanceof __codemation_core.SuspensionRequest) throw reason;
5550
+ throw reason instanceof Error ? reason : new Error(String(reason));
5551
+ }
5494
5552
  return results.filter((result) => result.status === "fulfilled").map((result) => result.value);
5495
5553
  }
5496
5554
  async executePlannedToolCall(args) {
@@ -5575,6 +5633,34 @@ let AgentToolExecutionCoordinator = class AgentToolExecutionCoordinator$1 {
5575
5633
  result
5576
5634
  };
5577
5635
  } catch (error) {
5636
+ if (error instanceof __codemation_core.SuspensionRequest) {
5637
+ const pendingToolCallId = plannedToolCall.toolCall.id ?? plannedToolCall.binding.config.name;
5638
+ const checkpoint = {
5639
+ conversation: args.conversationSnapshot ? [...args.conversationSnapshot] : [],
5640
+ turnCount: args.turnCount ?? 0,
5641
+ toolCallCount: args.toolCallCount ?? 0,
5642
+ pendingToolCallId,
5643
+ agentName: args.agentName,
5644
+ modelId: args.modelId ?? ""
5645
+ };
5646
+ const agentReasoning = this.extractLastAssistantText(args.conversationSnapshot ?? []);
5647
+ const augmented = new __codemation_core.SuspensionRequest({
5648
+ ...error.request,
5649
+ metadata: {
5650
+ ...error.request.metadata,
5651
+ agentCheckpoint: checkpoint,
5652
+ pendingToolCallId,
5653
+ agentReasoning,
5654
+ onRejected: plannedToolCall.binding.humanApproval?.onRejected ?? "return"
5655
+ }
5656
+ });
5657
+ await span.end({
5658
+ status: "error",
5659
+ statusMessage: "suspended",
5660
+ endedAt: /* @__PURE__ */ new Date()
5661
+ });
5662
+ throw augmented;
5663
+ }
5578
5664
  const classification = this.errorClassifier.classify({
5579
5665
  error,
5580
5666
  toolName: plannedToolCall.binding.config.name,
@@ -5750,6 +5836,24 @@ let AgentToolExecutionCoordinator = class AgentToolExecutionCoordinator$1 {
5750
5836
  extractErrorDetails(error) {
5751
5837
  return error.details;
5752
5838
  }
5839
+ /**
5840
+ * Extracts the text content from the last assistant message in the conversation snapshot.
5841
+ * Used to populate `agentReasoning` in the HITL suspension metadata.
5842
+ */
5843
+ extractLastAssistantText(conversation) {
5844
+ for (let i = conversation.length - 1; i >= 0; i--) {
5845
+ const msg = conversation[i];
5846
+ if (msg?.role !== "assistant") continue;
5847
+ const content = msg.content;
5848
+ if (typeof content === "string") return content;
5849
+ if (Array.isArray(content)) {
5850
+ const textParts = content.filter((part) => typeof part === "object" && part.type === "text").map((part) => part.text);
5851
+ if (textParts.length > 0) return textParts.join("");
5852
+ }
5853
+ break;
5854
+ }
5855
+ return "";
5856
+ }
5753
5857
  serializeIssue(issue$1) {
5754
5858
  const result = {
5755
5859
  path: [...issue$1.path],
@@ -6103,6 +6207,7 @@ var AgentItemPortMap = class {
6103
6207
  //#endregion
6104
6208
  //#region src/nodes/AIAgentNode.ts
6105
6209
  var _ref, _ref2, _ref3, _ref4, _ref5;
6210
+ const HITL_SOLO_CONSTRAINT_SENTENCE = "This tool requires human approval and may take time. Call it alone — do not invoke other tools in the same turn. Your turn will be paused until a decision is made.";
6106
6211
  let AIAgentNode = class AIAgentNode$1 {
6107
6212
  kind = "node";
6108
6213
  outputPorts = ["main"];
@@ -6120,13 +6225,90 @@ let AIAgentNode = class AIAgentNode$1 {
6120
6225
  this.connectionCredentialExecutionContextFactory = this.executionHelpers.createConnectionCredentialExecutionContextFactory(credentialSessions);
6121
6226
  }
6122
6227
  async execute(args) {
6123
- const prepared = await this.getOrPrepareExecution(args.ctx);
6228
+ const { ctx } = args;
6229
+ if (ctx.resumeContext) return this.executeResumed(args, ctx.resumeContext);
6230
+ const prepared = await this.getOrPrepareExecution(ctx);
6124
6231
  const itemWithMappedJson = {
6125
6232
  ...args.item,
6126
6233
  json: args.input
6127
6234
  };
6128
6235
  return (await this.runAgentForItem(prepared, itemWithMappedJson, args.itemIndex, args.items)).json;
6129
6236
  }
6237
+ /**
6238
+ * Resume path: re-enters the agent loop after a HITL suspension.
6239
+ * Reconstructs the conversation from the checkpoint, injects the human decision
6240
+ * as a tool_result, and continues the loop from where it suspended.
6241
+ */
6242
+ async executeResumed(args, resumeContext) {
6243
+ const { ctx } = args;
6244
+ const taskMetadata = resumeContext.task.metadata ?? {};
6245
+ const checkpoint = taskMetadata["agentCheckpoint"];
6246
+ const onRejected = taskMetadata["onRejected"] ?? "return";
6247
+ if (!checkpoint) {
6248
+ const prepared$1 = await this.getOrPrepareExecution(ctx);
6249
+ const itemWithMappedJson = {
6250
+ ...args.item,
6251
+ json: args.input
6252
+ };
6253
+ return (await this.runAgentForItem(prepared$1, itemWithMappedJson, args.itemIndex, args.items)).json;
6254
+ }
6255
+ if (resumeContext.decision.kind === "decided" && resumeContext.decision.value === null && onRejected === "halt") return;
6256
+ const decision = this.normalizeDecision(resumeContext);
6257
+ if (decision.status === "rejected" && onRejected === "halt") return;
6258
+ const prepared = await this.getOrPrepareExecution(ctx);
6259
+ const item = args.item;
6260
+ const itemInputsByPort = AgentItemPortMap.fromItem(item);
6261
+ const itemScopedTools = this.createItemScopedTools(prepared.resolvedTools, ctx, item, args.itemIndex, args.items);
6262
+ const toolResultEntry = {
6263
+ toolName: checkpoint.pendingToolCallId,
6264
+ toolCallId: checkpoint.pendingToolCallId,
6265
+ result: decision,
6266
+ serialized: JSON.stringify(decision)
6267
+ };
6268
+ const conversation = [...checkpoint.conversation, AgentMessageFactory.createToolResultsMessage([toolResultEntry])];
6269
+ const loopResult = await this.runTurnLoopUntilFinalAnswer({
6270
+ prepared,
6271
+ itemInputsByPort,
6272
+ itemScopedTools,
6273
+ conversation,
6274
+ resumedTurnCount: checkpoint.turnCount,
6275
+ resumedToolCallCount: checkpoint.toolCallCount
6276
+ });
6277
+ await ctx.telemetry.recordMetric({
6278
+ name: __codemation_core.CodemationTelemetryMetricNames.agentTurns,
6279
+ value: loopResult.turnCount
6280
+ });
6281
+ await ctx.telemetry.recordMetric({
6282
+ name: __codemation_core.CodemationTelemetryMetricNames.agentToolCalls,
6283
+ value: loopResult.toolCallCount
6284
+ });
6285
+ const outputJson = await this.resolveFinalOutputJson(prepared, itemInputsByPort, conversation, loopResult.finalText, itemScopedTools.length > 0);
6286
+ return this.buildOutputItem(item, outputJson).json;
6287
+ }
6288
+ /**
6289
+ * Normalizes a {@link ResumeContext} decision into a flat JSON-serializable shape
6290
+ * suitable for injection as a tool_result content.
6291
+ */
6292
+ normalizeDecision(resumeContext) {
6293
+ const { decision } = resumeContext;
6294
+ if (decision.kind === "decided") {
6295
+ const value = decision.value;
6296
+ return {
6297
+ status: (typeof value === "object" && value !== null && "approved" in value ? Boolean(value["approved"]) : true) ? "approved" : "rejected",
6298
+ value: decision.value,
6299
+ actor: decision.actor,
6300
+ decidedAt: decision.decidedAt.toISOString()
6301
+ };
6302
+ }
6303
+ if (decision.kind === "timed_out") return {
6304
+ status: "timed_out",
6305
+ at: decision.at.toISOString()
6306
+ };
6307
+ return {
6308
+ status: "auto_accepted",
6309
+ at: decision.at.toISOString()
6310
+ };
6311
+ }
6130
6312
  async getOrPrepareExecution(ctx) {
6131
6313
  let pending = this.preparedByExecutionContext.get(ctx);
6132
6314
  if (!pending) {
@@ -6247,7 +6429,7 @@ let AIAgentNode = class AIAgentNode$1 {
6247
6429
  const { prepared, itemInputsByPort, itemScopedTools, conversation } = args;
6248
6430
  const { ctx, guardrails, toolLoadingStrategy } = prepared;
6249
6431
  let finalText = "";
6250
- let toolCallCount = 0;
6432
+ let toolCallCount = args.resumedToolCallCount ?? 0;
6251
6433
  let turnCount = 0;
6252
6434
  const repairAttemptsByToolName = /* @__PURE__ */ new Map();
6253
6435
  /** Tool IDs surfaced by find_tools across all prior turns in this item run. */
@@ -6288,11 +6470,17 @@ let AIAgentNode = class AIAgentNode$1 {
6288
6470
  const plannedToolCalls = this.planToolCalls(itemScopedTools, coordinatorCalls, ctx.nodeId);
6289
6471
  toolCallCount += plannedToolCalls.length;
6290
6472
  await this.markQueuedTools(plannedToolCalls, ctx);
6473
+ const assistantMsg = result.assistantMessage ?? AgentMessageFactory.createAssistantWithToolCalls(result.text, result.toolCalls);
6474
+ const conversationWithAssistant = [...conversation, assistantMsg];
6291
6475
  const executed = await this.toolExecutionCoordinator.execute({
6292
6476
  plannedToolCalls,
6293
6477
  ctx,
6294
6478
  agentName: this.getAgentDisplayName(ctx),
6295
- repairAttemptsByToolName
6479
+ repairAttemptsByToolName,
6480
+ conversationSnapshot: conversationWithAssistant,
6481
+ turnCount,
6482
+ toolCallCount,
6483
+ modelId: this.resolveChatModelName(ctx.config.chatModel)
6296
6484
  });
6297
6485
  coordinatorExecutedCalls.push(...executed);
6298
6486
  }
@@ -6354,6 +6542,7 @@ let AIAgentNode = class AIAgentNode$1 {
6354
6542
  connectionNodeId: __codemation_core.ConnectionNodeIdFactory.toolConnectionNodeId(ctx.nodeId, entry.config.name),
6355
6543
  getCredentialRequirements: () => entry.config.getCredentialRequirements?.() ?? []
6356
6544
  });
6545
+ const hitlBehavior = this.resolveHumanApprovalBehavior(entry.config);
6357
6546
  return {
6358
6547
  config: entry.config,
6359
6548
  inputSchema: entry.runtime.inputSchema,
@@ -6368,11 +6557,22 @@ let AIAgentNode = class AIAgentNode$1 {
6368
6557
  items,
6369
6558
  hooks
6370
6559
  });
6371
- }
6560
+ },
6561
+ ...hitlBehavior !== void 0 ? { humanApproval: hitlBehavior } : {}
6372
6562
  };
6373
6563
  });
6374
6564
  }
6375
6565
  /**
6566
+ * Detects whether a tool config is backed by a `defineHumanApprovalNode` marker
6567
+ * and returns the HITL behavior config, or `undefined` when not a HITL tool.
6568
+ */
6569
+ resolveHumanApprovalBehavior(config$1) {
6570
+ if (!this.isNodeBackedToolConfig(config$1)) return void 0;
6571
+ const marker = config$1.node.humanApprovalToolBehavior;
6572
+ if (marker === void 0) return void 0;
6573
+ return { onRejected: marker.onRejected ?? "return" };
6574
+ }
6575
+ /**
6376
6576
  * Invoke a text turn using the merged tool set from item-scoped tools (coordinator-managed)
6377
6577
  * and strategy tools (find_tools + discovered MCP tools).
6378
6578
  * Strategy tools take precedence for names that overlap.
@@ -6402,6 +6602,8 @@ let AIAgentNode = class AIAgentNode$1 {
6402
6602
  /**
6403
6603
  * Builds a ToolSet from resolved tools for strategy initialization.
6404
6604
  * The strategy uses this for its "always-included" node-backed tool descriptions.
6605
+ * HITL tools (detected via the `humanApprovalToolBehavior` field set by `defineHumanApprovalNode`) get the solo-constraint sentence
6606
+ * appended to their description.
6405
6607
  */
6406
6608
  buildToolSetFromResolved(resolvedTools) {
6407
6609
  if (resolvedTools.length === 0) return {};
@@ -6411,8 +6613,10 @@ let AIAgentNode = class AIAgentNode$1 {
6411
6613
  schemaName: entry.config.name,
6412
6614
  requireObjectRoot: true
6413
6615
  });
6616
+ const baseDescription = entry.config.description ?? entry.runtime.defaultDescription;
6617
+ const description = this.resolveHumanApprovalBehavior(entry.config) !== void 0 ? `${baseDescription} ${HITL_SOLO_CONSTRAINT_SENTENCE}` : baseDescription;
6414
6618
  toolSet[entry.config.name] = {
6415
- description: entry.config.description ?? entry.runtime.defaultDescription,
6619
+ description,
6416
6620
  inputSchema: (0, ai.jsonSchema)(schemaRecord)
6417
6621
  };
6418
6622
  }
@@ -6440,8 +6644,10 @@ let AIAgentNode = class AIAgentNode$1 {
6440
6644
  schemaName: entry.config.name,
6441
6645
  requireObjectRoot: true
6442
6646
  });
6647
+ const baseDescription = entry.config.description;
6648
+ const description = entry.humanApproval !== void 0 && baseDescription !== void 0 ? `${baseDescription} ${HITL_SOLO_CONSTRAINT_SENTENCE}` : entry.humanApproval !== void 0 ? HITL_SOLO_CONSTRAINT_SENTENCE : baseDescription;
6443
6649
  toolSet[entry.config.name] = {
6444
- description: entry.config.description,
6650
+ description,
6445
6651
  inputSchema: (0, ai.jsonSchema)(schemaRecord)
6446
6652
  };
6447
6653
  }
@@ -8758,6 +8964,93 @@ const collectionDeleteNode = (0, __codemation_core.defineNode)({
8758
8964
  }
8759
8965
  });
8760
8966
 
8967
+ //#endregion
8968
+ //#region src/nodes/InboxApprovalNode.types.ts
8969
+ function resolveSubjectField(field, item) {
8970
+ return typeof field === "function" ? field({ item }) : field;
8971
+ }
8972
+ /**
8973
+ * Auto-detecting inbox approval node.
8974
+ *
8975
+ * Uses `ctx.resolve(InboxChannelResolverToken)` to pick the right inbox channel
8976
+ * at runtime:
8977
+ * - In managed mode (PairingConfig present): routes to the control-plane inbox.
8978
+ * - Otherwise: routes to the local inbox.
8979
+ *
8980
+ * Authors use this node directly; no extra wiring needed per deployment mode.
8981
+ */
8982
+ const inboxApproval = (0, __codemation_core.defineHumanApprovalNode)({
8983
+ key: "inbox.approval",
8984
+ title: "Inbox Approval",
8985
+ description: "Suspend and wait for a human reviewer to approve or reject.",
8986
+ icon: "lucide:inbox",
8987
+ channel: "inbox",
8988
+ configSchema: object({
8989
+ title: custom((v) => typeof v === "string" || typeof v === "function"),
8990
+ body: custom((v) => typeof v === "string" || typeof v === "function"),
8991
+ priority: _enum([
8992
+ "low",
8993
+ "normal",
8994
+ "high"
8995
+ ]).default("normal"),
8996
+ timeout: string().default("24h"),
8997
+ onTimeout: _enum(["halt", "auto-accept"]).default("halt")
8998
+ }),
8999
+ decisionSchema: object({
9000
+ approved: boolean(),
9001
+ note: string().optional()
9002
+ }),
9003
+ defaultTimeout: "24h",
9004
+ defaultOnTimeout: "halt",
9005
+ async deliver({ task, config: config$1, item }, ctx) {
9006
+ const resolver = ctx.resolve(__codemation_core.InboxChannelResolverToken);
9007
+ if (!resolver) throw new Error("inboxApproval: no InboxChannelResolver registered. Ensure the host DI container is wired.");
9008
+ const { channel, workspaceId } = resolver.resolve();
9009
+ const subject = {
9010
+ title: resolveSubjectField(config$1.title, item),
9011
+ summary: resolveSubjectField(config$1.body, item),
9012
+ attributes: {
9013
+ workflowId: ctx.workflowId,
9014
+ item: item.json
9015
+ }
9016
+ };
9017
+ const delivery = await channel.deliver({
9018
+ task,
9019
+ subject,
9020
+ priority: config$1.priority,
9021
+ item,
9022
+ workspaceId
9023
+ });
9024
+ ctx.telemetry.addSpanEvent({
9025
+ name: "hitl.task.delivered",
9026
+ attributes: {
9027
+ taskId: task.taskId,
9028
+ channel: channel.kind
9029
+ }
9030
+ });
9031
+ return delivery;
9032
+ },
9033
+ async onDecision({ decision, actor, delivery }, ctx) {
9034
+ const resolver = ctx.resolve(__codemation_core.InboxChannelResolverToken);
9035
+ if (!resolver) return;
9036
+ const { channel } = resolver.resolve();
9037
+ await channel.updateOnDecision?.({
9038
+ delivery,
9039
+ decision,
9040
+ actor
9041
+ });
9042
+ },
9043
+ async onTimeout({ delivery, policy }, ctx) {
9044
+ const resolver = ctx.resolve(__codemation_core.InboxChannelResolverToken);
9045
+ if (!resolver) return;
9046
+ const { channel } = resolver.resolve();
9047
+ await channel.updateOnTimeout?.({
9048
+ delivery,
9049
+ policy
9050
+ });
9051
+ }
9052
+ });
9053
+
8761
9054
  //#endregion
8762
9055
  exports.AIAgent = AIAgent;
8763
9056
  exports.AIAgentConnectionWorkflowExpander = AIAgentConnectionWorkflowExpander;
@@ -8994,6 +9287,7 @@ exports.collectionListNode = collectionListNode;
8994
9287
  exports.collectionUpdateNode = collectionUpdateNode;
8995
9288
  exports.createWorkflowBuilder = createWorkflowBuilder;
8996
9289
  exports.defineRestNode = defineRestNode;
9290
+ exports.inboxApproval = inboxApproval;
8997
9291
  exports.oauth2ClientCredentialsType = oauth2ClientCredentialsType;
8998
9292
  exports.openAiChatModelPresets = openAiChatModelPresets;
8999
9293
  exports.registerCoreNodes = registerCoreNodes;