@codemation/core-nodes 0.8.1 → 0.10.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/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",
@@ -4352,6 +4389,59 @@ var OpenAiChatModelPresets = class {
4352
4389
  };
4353
4390
  const openAiChatModelPresets = new OpenAiChatModelPresets();
4354
4391
 
4392
+ //#endregion
4393
+ //#region src/chatModels/ManagedHmacSignerFactory.types.ts
4394
+ /**
4395
+ * Creates an HMAC-signing fetch wrapper that authenticates requests to
4396
+ * Codemation managed services (LLM broker, doc-scanner) with the
4397
+ * Codemation-Hmac v=1 scheme.
4398
+ *
4399
+ * Mirrors HmacRequestSigner from @codemation/host/pairing without importing
4400
+ * that package (which would create a circular dependency since @codemation/host
4401
+ * depends on @codemation/core-nodes).
4402
+ *
4403
+ * @param workspaceId - Workspace identifier injected by the CP provisioner.
4404
+ * @param pairingSecret - Base64-encoded 32-byte HMAC key injected by the provisioner.
4405
+ * @param options - Optional behaviour flags and test seams.
4406
+ */
4407
+ function managedHmacFetchFactory(workspaceId, pairingSecret, options) {
4408
+ const signBody = options?.signBody ?? true;
4409
+ return async (input, init) => {
4410
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
4411
+ const method = init?.method ?? "POST";
4412
+ let bodyForSigning = "";
4413
+ if (signBody && init?.body !== void 0 && init.body !== null) bodyForSigning = typeof init.body === "string" ? init.body : await new Response(init.body).text();
4414
+ const authHeader = buildHmacAuthHeader(workspaceId, pairingSecret, method, url, bodyForSigning, options);
4415
+ const headers = new Headers(init?.headers);
4416
+ headers.set("Authorization", authHeader);
4417
+ const outgoingBody = signBody ? bodyForSigning || init?.body : init?.body;
4418
+ return fetch(input, {
4419
+ ...init,
4420
+ body: outgoingBody,
4421
+ headers
4422
+ });
4423
+ };
4424
+ }
4425
+ /**
4426
+ * Produces a Codemation-Hmac v=1 Authorization header value.
4427
+ * Algorithm must match HmacVerifier.computeSignature() in the control-plane.
4428
+ */
4429
+ function buildHmacAuthHeader(workspaceId, pairingSecret, method, url, body, overrides) {
4430
+ const ts = overrides?.now ? overrides.now() : Math.floor(Date.now() / 1e3);
4431
+ const nonce = overrides?.nonce ? overrides.nonce() : (0, node_crypto.randomBytes)(16).toString("base64");
4432
+ const parsed = new URL(url);
4433
+ const path = (parsed.pathname + parsed.search).toLowerCase();
4434
+ const bodyHash = (0, node_crypto.createHash)("sha256").update(body, "utf8").digest("hex");
4435
+ const baseString = [
4436
+ method.toUpperCase(),
4437
+ path,
4438
+ ts,
4439
+ nonce,
4440
+ bodyHash
4441
+ ].join("\n");
4442
+ return `Codemation-Hmac v=1,workspaceId=${workspaceId},ts=${ts},nonce=${nonce},sig=${(0, node_crypto.createHmac)("sha256", Buffer.from(pairingSecret, "base64")).update(baseString, "utf8").digest("base64")}`;
4443
+ }
4444
+
4355
4445
  //#endregion
4356
4446
  //#region src/chatModels/CodemationChatModelFactory.ts
4357
4447
  let CodemationChatModelFactory = class CodemationChatModelFactory$1 {
@@ -4361,7 +4451,7 @@ let CodemationChatModelFactory = class CodemationChatModelFactory$1 {
4361
4451
  const workspaceId = process.env["WORKSPACE_ID"];
4362
4452
  const pairingSecret = process.env["WORKSPACE_PAIRING_SECRET"];
4363
4453
  if (!workspaceId || !pairingSecret) throw new Error("Codemation managed AI not available in this environment (workspace pairing is not configured).");
4364
- const hmacFetch = this.buildHmacSignedFetch(workspaceId, pairingSecret);
4454
+ const hmacFetch = managedHmacFetchFactory(workspaceId, pairingSecret);
4365
4455
  const languageModel = (0, __ai_sdk_openai.createOpenAI)({
4366
4456
  baseURL: `${gatewayUrl}/v1`,
4367
4457
  apiKey: "codemation-managed",
@@ -4377,52 +4467,6 @@ let CodemationChatModelFactory = class CodemationChatModelFactory$1 {
4377
4467
  }
4378
4468
  });
4379
4469
  }
4380
- /**
4381
- * Creates an HMAC-signed fetch wrapper for use with AI SDK's createOpenAI.
4382
- * Each call signs the request body with the workspace pairing secret so the
4383
- * LLM broker can authenticate the workspace without a user-managed API key.
4384
- *
4385
- * Mirrors HmacRequestSigner from @codemation/host/pairing without importing
4386
- * that package (which would create a circular dependency since @codemation/host
4387
- * depends on @codemation/core-nodes).
4388
- */
4389
- buildHmacSignedFetch(workspaceId, pairingSecret) {
4390
- return async (input, init) => {
4391
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
4392
- const method = init?.method ?? "POST";
4393
- let bodyString = "";
4394
- if (init?.body !== void 0 && init.body !== null) if (typeof init.body === "string") bodyString = init.body;
4395
- else bodyString = await new Response(init.body).text();
4396
- const authHeader = this.buildHmacAuthHeader(workspaceId, pairingSecret, method, url, bodyString);
4397
- const headers = new Headers(init?.headers);
4398
- headers.set("Authorization", authHeader);
4399
- const effectiveBody = bodyString || init?.body;
4400
- return fetch(input, {
4401
- ...init,
4402
- body: effectiveBody,
4403
- headers
4404
- });
4405
- };
4406
- }
4407
- /**
4408
- * Produces a Codemation-Hmac v1 Authorization header value.
4409
- * The algorithm must match HmacVerifier.computeSignature() in the control-plane.
4410
- */
4411
- buildHmacAuthHeader(workspaceId, pairingSecret, method, url, body) {
4412
- const ts = Math.floor(Date.now() / 1e3);
4413
- const nonce = (0, node_crypto.randomBytes)(16).toString("base64");
4414
- const parsed = new URL(url);
4415
- const path = (parsed.pathname + parsed.search).toLowerCase();
4416
- const bodyHash = (0, node_crypto.createHash)("sha256").update(body, "utf8").digest("hex");
4417
- const baseString = [
4418
- method.toUpperCase(),
4419
- path,
4420
- ts,
4421
- nonce,
4422
- bodyHash
4423
- ].join("\n");
4424
- return `Codemation-Hmac v=1,workspaceId=${workspaceId},ts=${ts},nonce=${nonce},sig=${(0, node_crypto.createHmac)("sha256", Buffer.from(pairingSecret, "base64")).update(baseString, "utf8").digest("base64")}`;
4425
- }
4426
4470
  };
4427
4471
  CodemationChatModelFactory = __decorate([(0, __codemation_core.chatModel)({ packageName: "@codemation/core-nodes" })], CodemationChatModelFactory);
4428
4472
 
@@ -4925,6 +4969,14 @@ const ZodNumberFormat = /* @__PURE__ */ $constructor("ZodNumberFormat", (inst, d
4925
4969
  function int(params) {
4926
4970
  return _int(ZodNumberFormat, params);
4927
4971
  }
4972
+ const ZodBoolean = /* @__PURE__ */ $constructor("ZodBoolean", (inst, def) => {
4973
+ $ZodBoolean.init(inst, def);
4974
+ ZodType.init(inst, def);
4975
+ inst._zod.processJSONSchema = (ctx, json, params) => booleanProcessor(inst, ctx, json, params);
4976
+ });
4977
+ function boolean(params) {
4978
+ return _boolean(ZodBoolean, params);
4979
+ }
4928
4980
  const ZodUnknown = /* @__PURE__ */ $constructor("ZodUnknown", (inst, def) => {
4929
4981
  $ZodUnknown.init(inst, def);
4930
4982
  ZodType.init(inst, def);
@@ -5236,6 +5288,9 @@ const ZodCustom = /* @__PURE__ */ $constructor("ZodCustom", (inst, def) => {
5236
5288
  ZodType.init(inst, def);
5237
5289
  inst._zod.processJSONSchema = (ctx, json, params) => customProcessor(inst, ctx, json, params);
5238
5290
  });
5291
+ function custom(fn, _params) {
5292
+ return _custom(ZodCustom, fn ?? (() => true), _params);
5293
+ }
5239
5294
  function refine(fn, _params = {}) {
5240
5295
  return _refine(ZodCustom, fn, _params);
5241
5296
  }
@@ -5485,12 +5540,22 @@ let AgentToolExecutionCoordinator = class AgentToolExecutionCoordinator$1 {
5485
5540
  this.repairPolicy = repairPolicy;
5486
5541
  }
5487
5542
  async execute(args) {
5543
+ if (args.plannedToolCalls.filter((c) => c.binding.humanApproval !== void 0).length > 0 && args.plannedToolCalls.length > 1) return args.plannedToolCalls.map((c) => ({
5544
+ toolName: c.binding.config.name,
5545
+ toolCallId: c.toolCall.id ?? c.binding.config.name,
5546
+ 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.` },
5547
+ 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.` })
5548
+ }));
5488
5549
  const results = await Promise.allSettled(args.plannedToolCalls.map(async (plannedToolCall) => await this.executePlannedToolCall({
5489
5550
  ...args,
5490
5551
  plannedToolCall
5491
5552
  })));
5492
5553
  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));
5554
+ if (rejected?.status === "rejected") {
5555
+ const reason = rejected.reason;
5556
+ if (reason instanceof __codemation_core.SuspensionRequest) throw reason;
5557
+ throw reason instanceof Error ? reason : new Error(String(reason));
5558
+ }
5494
5559
  return results.filter((result) => result.status === "fulfilled").map((result) => result.value);
5495
5560
  }
5496
5561
  async executePlannedToolCall(args) {
@@ -5575,6 +5640,34 @@ let AgentToolExecutionCoordinator = class AgentToolExecutionCoordinator$1 {
5575
5640
  result
5576
5641
  };
5577
5642
  } catch (error) {
5643
+ if (error instanceof __codemation_core.SuspensionRequest) {
5644
+ const pendingToolCallId = plannedToolCall.toolCall.id ?? plannedToolCall.binding.config.name;
5645
+ const checkpoint = {
5646
+ conversation: args.conversationSnapshot ? [...args.conversationSnapshot] : [],
5647
+ turnCount: args.turnCount ?? 0,
5648
+ toolCallCount: args.toolCallCount ?? 0,
5649
+ pendingToolCallId,
5650
+ agentName: args.agentName,
5651
+ modelId: args.modelId ?? ""
5652
+ };
5653
+ const agentReasoning = this.extractLastAssistantText(args.conversationSnapshot ?? []);
5654
+ const augmented = new __codemation_core.SuspensionRequest({
5655
+ ...error.request,
5656
+ metadata: {
5657
+ ...error.request.metadata,
5658
+ agentCheckpoint: checkpoint,
5659
+ pendingToolCallId,
5660
+ agentReasoning,
5661
+ onRejected: plannedToolCall.binding.humanApproval?.onRejected ?? "return"
5662
+ }
5663
+ });
5664
+ await span.end({
5665
+ status: "error",
5666
+ statusMessage: "suspended",
5667
+ endedAt: /* @__PURE__ */ new Date()
5668
+ });
5669
+ throw augmented;
5670
+ }
5578
5671
  const classification = this.errorClassifier.classify({
5579
5672
  error,
5580
5673
  toolName: plannedToolCall.binding.config.name,
@@ -5750,6 +5843,24 @@ let AgentToolExecutionCoordinator = class AgentToolExecutionCoordinator$1 {
5750
5843
  extractErrorDetails(error) {
5751
5844
  return error.details;
5752
5845
  }
5846
+ /**
5847
+ * Extracts the text content from the last assistant message in the conversation snapshot.
5848
+ * Used to populate `agentReasoning` in the HITL suspension metadata.
5849
+ */
5850
+ extractLastAssistantText(conversation) {
5851
+ for (let i = conversation.length - 1; i >= 0; i--) {
5852
+ const msg = conversation[i];
5853
+ if (msg?.role !== "assistant") continue;
5854
+ const content = msg.content;
5855
+ if (typeof content === "string") return content;
5856
+ if (Array.isArray(content)) {
5857
+ const textParts = content.filter((part) => typeof part === "object" && part.type === "text").map((part) => part.text);
5858
+ if (textParts.length > 0) return textParts.join("");
5859
+ }
5860
+ break;
5861
+ }
5862
+ return "";
5863
+ }
5753
5864
  serializeIssue(issue$1) {
5754
5865
  const result = {
5755
5866
  path: [...issue$1.path],
@@ -6103,6 +6214,7 @@ var AgentItemPortMap = class {
6103
6214
  //#endregion
6104
6215
  //#region src/nodes/AIAgentNode.ts
6105
6216
  var _ref, _ref2, _ref3, _ref4, _ref5;
6217
+ 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
6218
  let AIAgentNode = class AIAgentNode$1 {
6107
6219
  kind = "node";
6108
6220
  outputPorts = ["main"];
@@ -6120,13 +6232,90 @@ let AIAgentNode = class AIAgentNode$1 {
6120
6232
  this.connectionCredentialExecutionContextFactory = this.executionHelpers.createConnectionCredentialExecutionContextFactory(credentialSessions);
6121
6233
  }
6122
6234
  async execute(args) {
6123
- const prepared = await this.getOrPrepareExecution(args.ctx);
6235
+ const { ctx } = args;
6236
+ if (ctx.resumeContext) return this.executeResumed(args, ctx.resumeContext);
6237
+ const prepared = await this.getOrPrepareExecution(ctx);
6124
6238
  const itemWithMappedJson = {
6125
6239
  ...args.item,
6126
6240
  json: args.input
6127
6241
  };
6128
6242
  return (await this.runAgentForItem(prepared, itemWithMappedJson, args.itemIndex, args.items)).json;
6129
6243
  }
6244
+ /**
6245
+ * Resume path: re-enters the agent loop after a HITL suspension.
6246
+ * Reconstructs the conversation from the checkpoint, injects the human decision
6247
+ * as a tool_result, and continues the loop from where it suspended.
6248
+ */
6249
+ async executeResumed(args, resumeContext) {
6250
+ const { ctx } = args;
6251
+ const taskMetadata = resumeContext.task.metadata ?? {};
6252
+ const checkpoint = taskMetadata["agentCheckpoint"];
6253
+ const onRejected = taskMetadata["onRejected"] ?? "return";
6254
+ if (!checkpoint) {
6255
+ const prepared$1 = await this.getOrPrepareExecution(ctx);
6256
+ const itemWithMappedJson = {
6257
+ ...args.item,
6258
+ json: args.input
6259
+ };
6260
+ return (await this.runAgentForItem(prepared$1, itemWithMappedJson, args.itemIndex, args.items)).json;
6261
+ }
6262
+ if (resumeContext.decision.kind === "decided" && resumeContext.decision.value === null && onRejected === "halt") return;
6263
+ const decision = this.normalizeDecision(resumeContext);
6264
+ if (decision.status === "rejected" && onRejected === "halt") return;
6265
+ const prepared = await this.getOrPrepareExecution(ctx);
6266
+ const item = args.item;
6267
+ const itemInputsByPort = AgentItemPortMap.fromItem(item);
6268
+ const itemScopedTools = this.createItemScopedTools(prepared.resolvedTools, ctx, item, args.itemIndex, args.items);
6269
+ const toolResultEntry = {
6270
+ toolName: checkpoint.pendingToolCallId,
6271
+ toolCallId: checkpoint.pendingToolCallId,
6272
+ result: decision,
6273
+ serialized: JSON.stringify(decision)
6274
+ };
6275
+ const conversation = [...checkpoint.conversation, AgentMessageFactory.createToolResultsMessage([toolResultEntry])];
6276
+ const loopResult = await this.runTurnLoopUntilFinalAnswer({
6277
+ prepared,
6278
+ itemInputsByPort,
6279
+ itemScopedTools,
6280
+ conversation,
6281
+ resumedTurnCount: checkpoint.turnCount,
6282
+ resumedToolCallCount: checkpoint.toolCallCount
6283
+ });
6284
+ await ctx.telemetry.recordMetric({
6285
+ name: __codemation_core.CodemationTelemetryMetricNames.agentTurns,
6286
+ value: loopResult.turnCount
6287
+ });
6288
+ await ctx.telemetry.recordMetric({
6289
+ name: __codemation_core.CodemationTelemetryMetricNames.agentToolCalls,
6290
+ value: loopResult.toolCallCount
6291
+ });
6292
+ const outputJson = await this.resolveFinalOutputJson(prepared, itemInputsByPort, conversation, loopResult.finalText, itemScopedTools.length > 0);
6293
+ return this.buildOutputItem(item, outputJson).json;
6294
+ }
6295
+ /**
6296
+ * Normalizes a {@link ResumeContext} decision into a flat JSON-serializable shape
6297
+ * suitable for injection as a tool_result content.
6298
+ */
6299
+ normalizeDecision(resumeContext) {
6300
+ const { decision } = resumeContext;
6301
+ if (decision.kind === "decided") {
6302
+ const value = decision.value;
6303
+ return {
6304
+ status: (typeof value === "object" && value !== null && "approved" in value ? Boolean(value["approved"]) : true) ? "approved" : "rejected",
6305
+ value: decision.value,
6306
+ actor: decision.actor,
6307
+ decidedAt: decision.decidedAt.toISOString()
6308
+ };
6309
+ }
6310
+ if (decision.kind === "timed_out") return {
6311
+ status: "timed_out",
6312
+ at: decision.at.toISOString()
6313
+ };
6314
+ return {
6315
+ status: "auto_accepted",
6316
+ at: decision.at.toISOString()
6317
+ };
6318
+ }
6130
6319
  async getOrPrepareExecution(ctx) {
6131
6320
  let pending = this.preparedByExecutionContext.get(ctx);
6132
6321
  if (!pending) {
@@ -6247,7 +6436,7 @@ let AIAgentNode = class AIAgentNode$1 {
6247
6436
  const { prepared, itemInputsByPort, itemScopedTools, conversation } = args;
6248
6437
  const { ctx, guardrails, toolLoadingStrategy } = prepared;
6249
6438
  let finalText = "";
6250
- let toolCallCount = 0;
6439
+ let toolCallCount = args.resumedToolCallCount ?? 0;
6251
6440
  let turnCount = 0;
6252
6441
  const repairAttemptsByToolName = /* @__PURE__ */ new Map();
6253
6442
  /** Tool IDs surfaced by find_tools across all prior turns in this item run. */
@@ -6288,11 +6477,17 @@ let AIAgentNode = class AIAgentNode$1 {
6288
6477
  const plannedToolCalls = this.planToolCalls(itemScopedTools, coordinatorCalls, ctx.nodeId);
6289
6478
  toolCallCount += plannedToolCalls.length;
6290
6479
  await this.markQueuedTools(plannedToolCalls, ctx);
6480
+ const assistantMsg = result.assistantMessage ?? AgentMessageFactory.createAssistantWithToolCalls(result.text, result.toolCalls);
6481
+ const conversationWithAssistant = [...conversation, assistantMsg];
6291
6482
  const executed = await this.toolExecutionCoordinator.execute({
6292
6483
  plannedToolCalls,
6293
6484
  ctx,
6294
6485
  agentName: this.getAgentDisplayName(ctx),
6295
- repairAttemptsByToolName
6486
+ repairAttemptsByToolName,
6487
+ conversationSnapshot: conversationWithAssistant,
6488
+ turnCount,
6489
+ toolCallCount,
6490
+ modelId: this.resolveChatModelName(ctx.config.chatModel)
6296
6491
  });
6297
6492
  coordinatorExecutedCalls.push(...executed);
6298
6493
  }
@@ -6354,6 +6549,7 @@ let AIAgentNode = class AIAgentNode$1 {
6354
6549
  connectionNodeId: __codemation_core.ConnectionNodeIdFactory.toolConnectionNodeId(ctx.nodeId, entry.config.name),
6355
6550
  getCredentialRequirements: () => entry.config.getCredentialRequirements?.() ?? []
6356
6551
  });
6552
+ const hitlBehavior = this.resolveHumanApprovalBehavior(entry.config);
6357
6553
  return {
6358
6554
  config: entry.config,
6359
6555
  inputSchema: entry.runtime.inputSchema,
@@ -6368,11 +6564,22 @@ let AIAgentNode = class AIAgentNode$1 {
6368
6564
  items,
6369
6565
  hooks
6370
6566
  });
6371
- }
6567
+ },
6568
+ ...hitlBehavior !== void 0 ? { humanApproval: hitlBehavior } : {}
6372
6569
  };
6373
6570
  });
6374
6571
  }
6375
6572
  /**
6573
+ * Detects whether a tool config is backed by a `defineHumanApprovalNode` marker
6574
+ * and returns the HITL behavior config, or `undefined` when not a HITL tool.
6575
+ */
6576
+ resolveHumanApprovalBehavior(config$1) {
6577
+ if (!this.isNodeBackedToolConfig(config$1)) return void 0;
6578
+ const marker = config$1.node.humanApprovalToolBehavior;
6579
+ if (marker === void 0) return void 0;
6580
+ return { onRejected: marker.onRejected ?? "return" };
6581
+ }
6582
+ /**
6376
6583
  * Invoke a text turn using the merged tool set from item-scoped tools (coordinator-managed)
6377
6584
  * and strategy tools (find_tools + discovered MCP tools).
6378
6585
  * Strategy tools take precedence for names that overlap.
@@ -6402,6 +6609,8 @@ let AIAgentNode = class AIAgentNode$1 {
6402
6609
  /**
6403
6610
  * Builds a ToolSet from resolved tools for strategy initialization.
6404
6611
  * The strategy uses this for its "always-included" node-backed tool descriptions.
6612
+ * HITL tools (detected via the `humanApprovalToolBehavior` field set by `defineHumanApprovalNode`) get the solo-constraint sentence
6613
+ * appended to their description.
6405
6614
  */
6406
6615
  buildToolSetFromResolved(resolvedTools) {
6407
6616
  if (resolvedTools.length === 0) return {};
@@ -6411,8 +6620,10 @@ let AIAgentNode = class AIAgentNode$1 {
6411
6620
  schemaName: entry.config.name,
6412
6621
  requireObjectRoot: true
6413
6622
  });
6623
+ const baseDescription = entry.config.description ?? entry.runtime.defaultDescription;
6624
+ const description = this.resolveHumanApprovalBehavior(entry.config) !== void 0 ? `${baseDescription} ${HITL_SOLO_CONSTRAINT_SENTENCE}` : baseDescription;
6414
6625
  toolSet[entry.config.name] = {
6415
- description: entry.config.description ?? entry.runtime.defaultDescription,
6626
+ description,
6416
6627
  inputSchema: (0, ai.jsonSchema)(schemaRecord)
6417
6628
  };
6418
6629
  }
@@ -6440,8 +6651,10 @@ let AIAgentNode = class AIAgentNode$1 {
6440
6651
  schemaName: entry.config.name,
6441
6652
  requireObjectRoot: true
6442
6653
  });
6654
+ const baseDescription = entry.config.description;
6655
+ const description = entry.humanApproval !== void 0 && baseDescription !== void 0 ? `${baseDescription} ${HITL_SOLO_CONSTRAINT_SENTENCE}` : entry.humanApproval !== void 0 ? HITL_SOLO_CONSTRAINT_SENTENCE : baseDescription;
6443
6656
  toolSet[entry.config.name] = {
6444
- description: entry.config.description,
6657
+ description,
6445
6658
  inputSchema: (0, ai.jsonSchema)(schemaRecord)
6446
6659
  };
6447
6660
  }
@@ -6609,7 +6822,11 @@ let AIAgentNode = class AIAgentNode$1 {
6609
6822
  });
6610
6823
  try {
6611
6824
  const callOptions = this.resolveCallOptions(model, guardrails.modelInvocationOptions);
6612
- const outputSchema = structuredOptions?.strict && !this.isZodSchema(schema) ? ai.Output.object({ schema: (0, ai.jsonSchema)(schema) }) : ai.Output.object({ schema });
6825
+ const schemaRecord = this.isZodSchema(schema) ? this.executionHelpers.createJsonSchemaRecord(schema, {
6826
+ schemaName: structuredOptions?.schemaName ?? "structured_output",
6827
+ requireObjectRoot: true
6828
+ }) : schema;
6829
+ const outputSchema = ai.Output.object({ schema: (0, ai.jsonSchema)(schemaRecord) });
6613
6830
  const result = await (0, ai.generateText)({
6614
6831
  model: model.languageModel,
6615
6832
  messages: [...messages],
@@ -8758,6 +8975,173 @@ const collectionDeleteNode = (0, __codemation_core.defineNode)({
8758
8975
  }
8759
8976
  });
8760
8977
 
8978
+ //#endregion
8979
+ //#region src/nodes/InboxApprovalNode.types.ts
8980
+ function resolveSubjectField(field, item) {
8981
+ return typeof field === "function" ? field({ item }) : field;
8982
+ }
8983
+ /**
8984
+ * Auto-detecting inbox approval node.
8985
+ *
8986
+ * Uses `ctx.resolve(InboxChannelResolverToken)` to pick the right inbox channel
8987
+ * at runtime:
8988
+ * - In managed mode (PairingConfig present): routes to the control-plane inbox.
8989
+ * - Otherwise: routes to the local inbox.
8990
+ *
8991
+ * Authors use this node directly; no extra wiring needed per deployment mode.
8992
+ */
8993
+ const inboxApproval = (0, __codemation_core.defineHumanApprovalNode)({
8994
+ key: "inbox.approval",
8995
+ title: "Inbox Approval",
8996
+ description: "Suspend and wait for a human reviewer to approve or reject.",
8997
+ icon: "lucide:inbox",
8998
+ channel: "inbox",
8999
+ configSchema: object({
9000
+ title: custom((v) => typeof v === "string" || typeof v === "function"),
9001
+ body: custom((v) => typeof v === "string" || typeof v === "function"),
9002
+ priority: _enum([
9003
+ "low",
9004
+ "normal",
9005
+ "high"
9006
+ ]).default("normal"),
9007
+ timeout: string().default("24h"),
9008
+ onTimeout: _enum(["halt", "auto-accept"]).default("halt")
9009
+ }),
9010
+ decisionSchema: object({
9011
+ approved: boolean(),
9012
+ note: string().optional()
9013
+ }),
9014
+ defaultTimeout: "24h",
9015
+ defaultOnTimeout: "halt",
9016
+ async deliver({ task, config: config$1, item }, ctx) {
9017
+ const resolver = ctx.resolve(__codemation_core.InboxChannelResolverToken);
9018
+ if (!resolver) throw new Error("inboxApproval: no InboxChannelResolver registered. Ensure the host DI container is wired.");
9019
+ const { channel, workspaceId } = resolver.resolve();
9020
+ const subject = {
9021
+ title: resolveSubjectField(config$1.title, item),
9022
+ summary: resolveSubjectField(config$1.body, item),
9023
+ attributes: {
9024
+ workflowId: ctx.workflowId,
9025
+ item: item.json
9026
+ }
9027
+ };
9028
+ const delivery = await channel.deliver({
9029
+ task,
9030
+ subject,
9031
+ priority: config$1.priority,
9032
+ item,
9033
+ workspaceId
9034
+ });
9035
+ ctx.telemetry.addSpanEvent({
9036
+ name: "hitl.task.delivered",
9037
+ attributes: {
9038
+ taskId: task.taskId,
9039
+ channel: channel.kind
9040
+ }
9041
+ });
9042
+ return delivery;
9043
+ },
9044
+ async onDecision({ decision, actor, delivery }, ctx) {
9045
+ const resolver = ctx.resolve(__codemation_core.InboxChannelResolverToken);
9046
+ if (!resolver) return;
9047
+ const { channel } = resolver.resolve();
9048
+ await channel.updateOnDecision?.({
9049
+ delivery,
9050
+ decision,
9051
+ actor
9052
+ });
9053
+ },
9054
+ async onTimeout({ delivery, policy }, ctx) {
9055
+ const resolver = ctx.resolve(__codemation_core.InboxChannelResolverToken);
9056
+ if (!resolver) return;
9057
+ const { channel } = resolver.resolve();
9058
+ await channel.updateOnTimeout?.({
9059
+ delivery,
9060
+ policy
9061
+ });
9062
+ }
9063
+ });
9064
+
9065
+ //#endregion
9066
+ //#region src/nodes/codemationDocumentScannerNode.ts
9067
+ const ANALYZER_TYPES = [
9068
+ "document",
9069
+ "invoice",
9070
+ "image",
9071
+ "auto"
9072
+ ];
9073
+ const codemationDocumentScannerNode = (0, __codemation_core.defineNode)({
9074
+ key: "codemation.document-scanner",
9075
+ title: "Codemation Document Scanner",
9076
+ description: "Analyzes a binary attachment (document or image) via the managed Codemation document-scanning service and returns markdown text plus structured fields. No Azure credential required — auth uses the workspace pairing secret. Enable includeConfidence to get per-field confidence scores (0–1).",
9077
+ icon: "lucide:scan-text",
9078
+ input: {
9079
+ binaryField: "data",
9080
+ analyzerType: "auto",
9081
+ contentType: void 0,
9082
+ includeConfidence: false,
9083
+ maxBytes: void 0
9084
+ },
9085
+ configSchema: object({
9086
+ binaryField: string().optional(),
9087
+ analyzerType: _enum(ANALYZER_TYPES).optional(),
9088
+ contentType: string().optional(),
9089
+ includeConfidence: boolean().optional(),
9090
+ maxBytes: number().int().positive().optional()
9091
+ }),
9092
+ inspectorSummary({ config: config$1 }) {
9093
+ const cfg = config$1;
9094
+ const rows = [{
9095
+ label: "Analyzer type",
9096
+ value: cfg.analyzerType ?? "auto"
9097
+ }];
9098
+ const binaryField = cfg.binaryField ?? "data";
9099
+ if (binaryField !== "data") rows.push({
9100
+ label: "Binary field",
9101
+ value: binaryField
9102
+ });
9103
+ if (cfg.includeConfidence) rows.push({
9104
+ label: "Confidence",
9105
+ value: "enabled"
9106
+ });
9107
+ if (cfg.contentType) rows.push({
9108
+ label: "Content type",
9109
+ value: cfg.contentType
9110
+ });
9111
+ return rows;
9112
+ },
9113
+ async execute({ item, ctx }, { config: rawConfig }) {
9114
+ const config$1 = rawConfig;
9115
+ const gatewayUrl = process.env["DOC_SCANNER_GATEWAY_URL"];
9116
+ if (!gatewayUrl) throw new Error("Codemation Document Scanner not available in this environment (DOC_SCANNER_GATEWAY_URL is not set).");
9117
+ const workspaceId = process.env["WORKSPACE_ID"];
9118
+ const pairingSecret = process.env["WORKSPACE_PAIRING_SECRET"];
9119
+ if (!workspaceId || !pairingSecret) throw new Error("Codemation Document Scanner not available (workspace pairing is not configured).");
9120
+ const binaryField = config$1.binaryField ?? "data";
9121
+ const attachment = item.binary?.[binaryField];
9122
+ if (!attachment) throw new Error(`Codemation Document Scanner: no binary attachment at key "${binaryField}".`);
9123
+ const body = await ctx.binary.getBytes(attachment, config$1.maxBytes);
9124
+ const contentType = config$1.contentType ?? attachment.mimeType ?? "application/octet-stream";
9125
+ const analyzerType = config$1.analyzerType ?? "auto";
9126
+ const confidenceSuffix = config$1.includeConfidence ?? false ? "&confidence=true" : "";
9127
+ const url = `${gatewayUrl}/analyze?type=${encodeURIComponent(analyzerType)}${confidenceSuffix}`;
9128
+ const response = await managedHmacFetchFactory(workspaceId, pairingSecret, { signBody: false })(url, {
9129
+ method: "POST",
9130
+ body: body.buffer,
9131
+ headers: {
9132
+ "Content-Type": contentType,
9133
+ "Content-Length": String(body.byteLength),
9134
+ "X-Codemation-Caller": "workflow-node"
9135
+ }
9136
+ });
9137
+ if (!response.ok) {
9138
+ const text = await response.text().catch(() => "(unreadable)");
9139
+ throw new Error(`Codemation Document Scanner: service responded ${response.status} ${response.statusText} — ${text}`);
9140
+ }
9141
+ return await response.json();
9142
+ }
9143
+ });
9144
+
8761
9145
  //#endregion
8762
9146
  exports.AIAgent = AIAgent;
8763
9147
  exports.AIAgentConnectionWorkflowExpander = AIAgentConnectionWorkflowExpander;
@@ -8986,6 +9370,7 @@ exports.WorkflowChain = WorkflowChain;
8986
9370
  exports.apiKeyCredentialType = apiKeyCredentialType;
8987
9371
  exports.basicAuthCredentialType = basicAuthCredentialType;
8988
9372
  exports.bearerTokenCredentialType = bearerTokenCredentialType;
9373
+ exports.codemationDocumentScannerNode = codemationDocumentScannerNode;
8989
9374
  exports.collectionDeleteNode = collectionDeleteNode;
8990
9375
  exports.collectionFindOneNode = collectionFindOneNode;
8991
9376
  exports.collectionGetNode = collectionGetNode;
@@ -8994,6 +9379,7 @@ exports.collectionListNode = collectionListNode;
8994
9379
  exports.collectionUpdateNode = collectionUpdateNode;
8995
9380
  exports.createWorkflowBuilder = createWorkflowBuilder;
8996
9381
  exports.defineRestNode = defineRestNode;
9382
+ exports.inboxApproval = inboxApproval;
8997
9383
  exports.oauth2ClientCredentialsType = oauth2ClientCredentialsType;
8998
9384
  exports.openAiChatModelPresets = openAiChatModelPresets;
8999
9385
  exports.registerCoreNodes = registerCoreNodes;