@codemation/core-nodes-ocr 0.2.2 → 0.2.3

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,12 @@
1
1
  # @codemation/core-nodes-ocr
2
2
 
3
+ ## 0.2.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`3044474`](https://github.com/MadeRelevant/codemation/commit/3044474495525490735510ff74500b53761284b6)]:
8
+ - @codemation/core@0.12.0
9
+
3
10
  ## 0.2.2
4
11
 
5
12
  ### Patch Changes
@@ -8738,12 +8738,22 @@ let AgentToolExecutionCoordinator = class AgentToolExecutionCoordinator$1 {
8738
8738
  this.repairPolicy = repairPolicy;
8739
8739
  }
8740
8740
  async execute(args) {
8741
+ if (args.plannedToolCalls.filter((c) => c.binding.humanApproval !== void 0).length > 0 && args.plannedToolCalls.length > 1) return args.plannedToolCalls.map((c) => ({
8742
+ toolName: c.binding.config.name,
8743
+ toolCallId: c.toolCall.id ?? c.binding.config.name,
8744
+ 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.` },
8745
+ 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.` })
8746
+ }));
8741
8747
  const results = await Promise.allSettled(args.plannedToolCalls.map(async (plannedToolCall) => await this.executePlannedToolCall({
8742
8748
  ...args,
8743
8749
  plannedToolCall
8744
8750
  })));
8745
8751
  const rejected = results.find((result) => result.status === "rejected");
8746
- if (rejected?.status === "rejected") throw rejected.reason instanceof Error ? rejected.reason : new Error(String(rejected.reason));
8752
+ if (rejected?.status === "rejected") {
8753
+ const reason = rejected.reason;
8754
+ if (reason instanceof __codemation_core.SuspensionRequest) throw reason;
8755
+ throw reason instanceof Error ? reason : new Error(String(reason));
8756
+ }
8747
8757
  return results.filter((result) => result.status === "fulfilled").map((result) => result.value);
8748
8758
  }
8749
8759
  async executePlannedToolCall(args) {
@@ -8828,6 +8838,34 @@ let AgentToolExecutionCoordinator = class AgentToolExecutionCoordinator$1 {
8828
8838
  result
8829
8839
  };
8830
8840
  } catch (error) {
8841
+ if (error instanceof __codemation_core.SuspensionRequest) {
8842
+ const pendingToolCallId = plannedToolCall.toolCall.id ?? plannedToolCall.binding.config.name;
8843
+ const checkpoint = {
8844
+ conversation: args.conversationSnapshot ? [...args.conversationSnapshot] : [],
8845
+ turnCount: args.turnCount ?? 0,
8846
+ toolCallCount: args.toolCallCount ?? 0,
8847
+ pendingToolCallId,
8848
+ agentName: args.agentName,
8849
+ modelId: args.modelId ?? ""
8850
+ };
8851
+ const agentReasoning = this.extractLastAssistantText(args.conversationSnapshot ?? []);
8852
+ const augmented = new __codemation_core.SuspensionRequest({
8853
+ ...error.request,
8854
+ metadata: {
8855
+ ...error.request.metadata,
8856
+ agentCheckpoint: checkpoint,
8857
+ pendingToolCallId,
8858
+ agentReasoning,
8859
+ onRejected: plannedToolCall.binding.humanApproval?.onRejected ?? "return"
8860
+ }
8861
+ });
8862
+ await span.end({
8863
+ status: "error",
8864
+ statusMessage: "suspended",
8865
+ endedAt: /* @__PURE__ */ new Date()
8866
+ });
8867
+ throw augmented;
8868
+ }
8831
8869
  const classification = this.errorClassifier.classify({
8832
8870
  error,
8833
8871
  toolName: plannedToolCall.binding.config.name,
@@ -9003,6 +9041,24 @@ let AgentToolExecutionCoordinator = class AgentToolExecutionCoordinator$1 {
9003
9041
  extractErrorDetails(error) {
9004
9042
  return error.details;
9005
9043
  }
9044
+ /**
9045
+ * Extracts the text content from the last assistant message in the conversation snapshot.
9046
+ * Used to populate `agentReasoning` in the HITL suspension metadata.
9047
+ */
9048
+ extractLastAssistantText(conversation) {
9049
+ for (let i = conversation.length - 1; i >= 0; i--) {
9050
+ const msg = conversation[i];
9051
+ if (msg?.role !== "assistant") continue;
9052
+ const content = msg.content;
9053
+ if (typeof content === "string") return content;
9054
+ if (Array.isArray(content)) {
9055
+ const textParts = content.filter((part) => typeof part === "object" && part.type === "text").map((part) => part.text);
9056
+ if (textParts.length > 0) return textParts.join("");
9057
+ }
9058
+ break;
9059
+ }
9060
+ return "";
9061
+ }
9006
9062
  serializeIssue(issue) {
9007
9063
  const result = {
9008
9064
  path: [...issue.path],
@@ -15333,6 +15389,7 @@ var AgentItemPortMap = class {
15333
15389
  //#endregion
15334
15390
  //#region ../core-nodes/src/nodes/AIAgentNode.ts
15335
15391
  var _ref, _ref2, _ref3, _ref4, _ref5;
15392
+ 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.";
15336
15393
  let AIAgentNode = class AIAgentNode$1 {
15337
15394
  kind = "node";
15338
15395
  outputPorts = ["main"];
@@ -15350,13 +15407,90 @@ let AIAgentNode = class AIAgentNode$1 {
15350
15407
  this.connectionCredentialExecutionContextFactory = this.executionHelpers.createConnectionCredentialExecutionContextFactory(credentialSessions);
15351
15408
  }
15352
15409
  async execute(args) {
15353
- const prepared = await this.getOrPrepareExecution(args.ctx);
15410
+ const { ctx } = args;
15411
+ if (ctx.resumeContext) return this.executeResumed(args, ctx.resumeContext);
15412
+ const prepared = await this.getOrPrepareExecution(ctx);
15354
15413
  const itemWithMappedJson = {
15355
15414
  ...args.item,
15356
15415
  json: args.input
15357
15416
  };
15358
15417
  return (await this.runAgentForItem(prepared, itemWithMappedJson, args.itemIndex, args.items)).json;
15359
15418
  }
15419
+ /**
15420
+ * Resume path: re-enters the agent loop after a HITL suspension.
15421
+ * Reconstructs the conversation from the checkpoint, injects the human decision
15422
+ * as a tool_result, and continues the loop from where it suspended.
15423
+ */
15424
+ async executeResumed(args, resumeContext) {
15425
+ const { ctx } = args;
15426
+ const taskMetadata = resumeContext.task.metadata ?? {};
15427
+ const checkpoint = taskMetadata["agentCheckpoint"];
15428
+ const onRejected = taskMetadata["onRejected"] ?? "return";
15429
+ if (!checkpoint) {
15430
+ const prepared$1 = await this.getOrPrepareExecution(ctx);
15431
+ const itemWithMappedJson = {
15432
+ ...args.item,
15433
+ json: args.input
15434
+ };
15435
+ return (await this.runAgentForItem(prepared$1, itemWithMappedJson, args.itemIndex, args.items)).json;
15436
+ }
15437
+ if (resumeContext.decision.kind === "decided" && resumeContext.decision.value === null && onRejected === "halt") return;
15438
+ const decision = this.normalizeDecision(resumeContext);
15439
+ if (decision.status === "rejected" && onRejected === "halt") return;
15440
+ const prepared = await this.getOrPrepareExecution(ctx);
15441
+ const item = args.item;
15442
+ const itemInputsByPort = AgentItemPortMap.fromItem(item);
15443
+ const itemScopedTools = this.createItemScopedTools(prepared.resolvedTools, ctx, item, args.itemIndex, args.items);
15444
+ const toolResultEntry = {
15445
+ toolName: checkpoint.pendingToolCallId,
15446
+ toolCallId: checkpoint.pendingToolCallId,
15447
+ result: decision,
15448
+ serialized: JSON.stringify(decision)
15449
+ };
15450
+ const conversation = [...checkpoint.conversation, AgentMessageFactory.createToolResultsMessage([toolResultEntry])];
15451
+ const loopResult = await this.runTurnLoopUntilFinalAnswer({
15452
+ prepared,
15453
+ itemInputsByPort,
15454
+ itemScopedTools,
15455
+ conversation,
15456
+ resumedTurnCount: checkpoint.turnCount,
15457
+ resumedToolCallCount: checkpoint.toolCallCount
15458
+ });
15459
+ await ctx.telemetry.recordMetric({
15460
+ name: __codemation_core.CodemationTelemetryMetricNames.agentTurns,
15461
+ value: loopResult.turnCount
15462
+ });
15463
+ await ctx.telemetry.recordMetric({
15464
+ name: __codemation_core.CodemationTelemetryMetricNames.agentToolCalls,
15465
+ value: loopResult.toolCallCount
15466
+ });
15467
+ const outputJson = await this.resolveFinalOutputJson(prepared, itemInputsByPort, conversation, loopResult.finalText, itemScopedTools.length > 0);
15468
+ return this.buildOutputItem(item, outputJson).json;
15469
+ }
15470
+ /**
15471
+ * Normalizes a {@link ResumeContext} decision into a flat JSON-serializable shape
15472
+ * suitable for injection as a tool_result content.
15473
+ */
15474
+ normalizeDecision(resumeContext) {
15475
+ const { decision } = resumeContext;
15476
+ if (decision.kind === "decided") {
15477
+ const value = decision.value;
15478
+ return {
15479
+ status: (typeof value === "object" && value !== null && "approved" in value ? Boolean(value["approved"]) : true) ? "approved" : "rejected",
15480
+ value: decision.value,
15481
+ actor: decision.actor,
15482
+ decidedAt: decision.decidedAt.toISOString()
15483
+ };
15484
+ }
15485
+ if (decision.kind === "timed_out") return {
15486
+ status: "timed_out",
15487
+ at: decision.at.toISOString()
15488
+ };
15489
+ return {
15490
+ status: "auto_accepted",
15491
+ at: decision.at.toISOString()
15492
+ };
15493
+ }
15360
15494
  async getOrPrepareExecution(ctx) {
15361
15495
  let pending = this.preparedByExecutionContext.get(ctx);
15362
15496
  if (!pending) {
@@ -15477,7 +15611,7 @@ let AIAgentNode = class AIAgentNode$1 {
15477
15611
  const { prepared, itemInputsByPort, itemScopedTools, conversation } = args;
15478
15612
  const { ctx, guardrails, toolLoadingStrategy } = prepared;
15479
15613
  let finalText = "";
15480
- let toolCallCount = 0;
15614
+ let toolCallCount = args.resumedToolCallCount ?? 0;
15481
15615
  let turnCount = 0;
15482
15616
  const repairAttemptsByToolName = /* @__PURE__ */ new Map();
15483
15617
  /** Tool IDs surfaced by find_tools across all prior turns in this item run. */
@@ -15518,11 +15652,17 @@ let AIAgentNode = class AIAgentNode$1 {
15518
15652
  const plannedToolCalls = this.planToolCalls(itemScopedTools, coordinatorCalls, ctx.nodeId);
15519
15653
  toolCallCount += plannedToolCalls.length;
15520
15654
  await this.markQueuedTools(plannedToolCalls, ctx);
15655
+ const assistantMsg = result.assistantMessage ?? AgentMessageFactory.createAssistantWithToolCalls(result.text, result.toolCalls);
15656
+ const conversationWithAssistant = [...conversation, assistantMsg];
15521
15657
  const executed = await this.toolExecutionCoordinator.execute({
15522
15658
  plannedToolCalls,
15523
15659
  ctx,
15524
15660
  agentName: this.getAgentDisplayName(ctx),
15525
- repairAttemptsByToolName
15661
+ repairAttemptsByToolName,
15662
+ conversationSnapshot: conversationWithAssistant,
15663
+ turnCount,
15664
+ toolCallCount,
15665
+ modelId: this.resolveChatModelName(ctx.config.chatModel)
15526
15666
  });
15527
15667
  coordinatorExecutedCalls.push(...executed);
15528
15668
  }
@@ -15584,6 +15724,7 @@ let AIAgentNode = class AIAgentNode$1 {
15584
15724
  connectionNodeId: __codemation_core.ConnectionNodeIdFactory.toolConnectionNodeId(ctx.nodeId, entry.config.name),
15585
15725
  getCredentialRequirements: () => entry.config.getCredentialRequirements?.() ?? []
15586
15726
  });
15727
+ const hitlBehavior = this.resolveHumanApprovalBehavior(entry.config);
15587
15728
  return {
15588
15729
  config: entry.config,
15589
15730
  inputSchema: entry.runtime.inputSchema,
@@ -15598,11 +15739,22 @@ let AIAgentNode = class AIAgentNode$1 {
15598
15739
  items,
15599
15740
  hooks
15600
15741
  });
15601
- }
15742
+ },
15743
+ ...hitlBehavior !== void 0 ? { humanApproval: hitlBehavior } : {}
15602
15744
  };
15603
15745
  });
15604
15746
  }
15605
15747
  /**
15748
+ * Detects whether a tool config is backed by a `defineHumanApprovalNode` marker
15749
+ * and returns the HITL behavior config, or `undefined` when not a HITL tool.
15750
+ */
15751
+ resolveHumanApprovalBehavior(config) {
15752
+ if (!this.isNodeBackedToolConfig(config)) return void 0;
15753
+ const marker$3 = config.node.humanApprovalToolBehavior;
15754
+ if (marker$3 === void 0) return void 0;
15755
+ return { onRejected: marker$3.onRejected ?? "return" };
15756
+ }
15757
+ /**
15606
15758
  * Invoke a text turn using the merged tool set from item-scoped tools (coordinator-managed)
15607
15759
  * and strategy tools (find_tools + discovered MCP tools).
15608
15760
  * Strategy tools take precedence for names that overlap.
@@ -15632,6 +15784,8 @@ let AIAgentNode = class AIAgentNode$1 {
15632
15784
  /**
15633
15785
  * Builds a ToolSet from resolved tools for strategy initialization.
15634
15786
  * The strategy uses this for its "always-included" node-backed tool descriptions.
15787
+ * HITL tools (detected via the `humanApprovalToolBehavior` field set by `defineHumanApprovalNode`) get the solo-constraint sentence
15788
+ * appended to their description.
15635
15789
  */
15636
15790
  buildToolSetFromResolved(resolvedTools) {
15637
15791
  if (resolvedTools.length === 0) return {};
@@ -15641,8 +15795,10 @@ let AIAgentNode = class AIAgentNode$1 {
15641
15795
  schemaName: entry.config.name,
15642
15796
  requireObjectRoot: true
15643
15797
  });
15798
+ const baseDescription = entry.config.description ?? entry.runtime.defaultDescription;
15799
+ const description = this.resolveHumanApprovalBehavior(entry.config) !== void 0 ? `${baseDescription} ${HITL_SOLO_CONSTRAINT_SENTENCE}` : baseDescription;
15644
15800
  toolSet[entry.config.name] = {
15645
- description: entry.config.description ?? entry.runtime.defaultDescription,
15801
+ description,
15646
15802
  inputSchema: jsonSchema(schemaRecord)
15647
15803
  };
15648
15804
  }
@@ -15670,8 +15826,10 @@ let AIAgentNode = class AIAgentNode$1 {
15670
15826
  schemaName: entry.config.name,
15671
15827
  requireObjectRoot: true
15672
15828
  });
15829
+ const baseDescription = entry.config.description;
15830
+ const description = entry.humanApproval !== void 0 && baseDescription !== void 0 ? `${baseDescription} ${HITL_SOLO_CONSTRAINT_SENTENCE}` : entry.humanApproval !== void 0 ? HITL_SOLO_CONSTRAINT_SENTENCE : baseDescription;
15673
15831
  toolSet[entry.config.name] = {
15674
- description: entry.config.description,
15832
+ description,
15675
15833
  inputSchema: jsonSchema(schemaRecord)
15676
15834
  };
15677
15835
  }
@@ -17763,6 +17921,93 @@ const collectionDeleteNode = (0, __codemation_core.defineNode)({
17763
17921
  }
17764
17922
  });
17765
17923
 
17924
+ //#endregion
17925
+ //#region ../core-nodes/src/nodes/InboxApprovalNode.types.ts
17926
+ function resolveSubjectField(field, item) {
17927
+ return typeof field === "function" ? field({ item }) : field;
17928
+ }
17929
+ /**
17930
+ * Auto-detecting inbox approval node.
17931
+ *
17932
+ * Uses `ctx.resolve(InboxChannelResolverToken)` to pick the right inbox channel
17933
+ * at runtime:
17934
+ * - In managed mode (PairingConfig present): routes to the control-plane inbox.
17935
+ * - Otherwise: routes to the local inbox.
17936
+ *
17937
+ * Authors use this node directly; no extra wiring needed per deployment mode.
17938
+ */
17939
+ const inboxApproval = (0, __codemation_core.defineHumanApprovalNode)({
17940
+ key: "inbox.approval",
17941
+ title: "Inbox Approval",
17942
+ description: "Suspend and wait for a human reviewer to approve or reject.",
17943
+ icon: "lucide:inbox",
17944
+ channel: "inbox",
17945
+ configSchema: zod.z.object({
17946
+ title: zod.z.custom((v$1) => typeof v$1 === "string" || typeof v$1 === "function"),
17947
+ body: zod.z.custom((v$1) => typeof v$1 === "string" || typeof v$1 === "function"),
17948
+ priority: zod.z.enum([
17949
+ "low",
17950
+ "normal",
17951
+ "high"
17952
+ ]).default("normal"),
17953
+ timeout: zod.z.string().default("24h"),
17954
+ onTimeout: zod.z.enum(["halt", "auto-accept"]).default("halt")
17955
+ }),
17956
+ decisionSchema: zod.z.object({
17957
+ approved: zod.z.boolean(),
17958
+ note: zod.z.string().optional()
17959
+ }),
17960
+ defaultTimeout: "24h",
17961
+ defaultOnTimeout: "halt",
17962
+ async deliver({ task, config, item }, ctx) {
17963
+ const resolver = ctx.resolve(__codemation_core.InboxChannelResolverToken);
17964
+ if (!resolver) throw new Error("inboxApproval: no InboxChannelResolver registered. Ensure the host DI container is wired.");
17965
+ const { channel, workspaceId } = resolver.resolve();
17966
+ const subject = {
17967
+ title: resolveSubjectField(config.title, item),
17968
+ summary: resolveSubjectField(config.body, item),
17969
+ attributes: {
17970
+ workflowId: ctx.workflowId,
17971
+ item: item.json
17972
+ }
17973
+ };
17974
+ const delivery = await channel.deliver({
17975
+ task,
17976
+ subject,
17977
+ priority: config.priority,
17978
+ item,
17979
+ workspaceId
17980
+ });
17981
+ ctx.telemetry.addSpanEvent({
17982
+ name: "hitl.task.delivered",
17983
+ attributes: {
17984
+ taskId: task.taskId,
17985
+ channel: channel.kind
17986
+ }
17987
+ });
17988
+ return delivery;
17989
+ },
17990
+ async onDecision({ decision, actor, delivery }, ctx) {
17991
+ const resolver = ctx.resolve(__codemation_core.InboxChannelResolverToken);
17992
+ if (!resolver) return;
17993
+ const { channel } = resolver.resolve();
17994
+ await channel.updateOnDecision?.({
17995
+ delivery,
17996
+ decision,
17997
+ actor
17998
+ });
17999
+ },
18000
+ async onTimeout({ delivery, policy }, ctx) {
18001
+ const resolver = ctx.resolve(__codemation_core.InboxChannelResolverToken);
18002
+ if (!resolver) return;
18003
+ const { channel } = resolver.resolve();
18004
+ await channel.updateOnTimeout?.({
18005
+ delivery,
18006
+ policy
18007
+ });
18008
+ }
18009
+ });
18010
+
17766
18011
  //#endregion
17767
18012
  //#region ../host/src/presentation/config/CodemationAuthoring.types.ts
17768
18013
  var CodemationAuthoringConfigFactory = class {