@codemation/core-nodes 1.0.1 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemation/core-nodes",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -32,7 +32,7 @@
32
32
  "@ai-sdk/provider": "^3.0.8",
33
33
  "ai": "^6.0.168",
34
34
  "lucide-react": "^0.577.0",
35
- "@codemation/core": "1.0.0"
35
+ "@codemation/core": "1.0.1"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^25.3.5",
@@ -356,7 +356,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
356
356
  return {
357
357
  config: entry.config,
358
358
  inputSchema: entry.runtime.inputSchema,
359
- execute: async (input: unknown): Promise<unknown> => {
359
+ execute: async (input, hooks): Promise<unknown> => {
360
360
  const validated = entry.runtime.inputSchema.parse(input) as unknown;
361
361
  return await entry.runtime.execute({
362
362
  config: entry.config,
@@ -365,6 +365,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
365
365
  item,
366
366
  itemIndex,
367
367
  items,
368
+ hooks,
368
369
  });
369
370
  },
370
371
  } satisfies ItemScopedToolBinding;
@@ -421,11 +422,36 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
421
422
  activationId: ctx.activationId,
422
423
  inputsByPort: itemInputsByPort,
423
424
  });
425
+ await ctx.nodeState?.appendConnectionInvocation({
426
+ invocationId,
427
+ connectionNodeId: languageModelConnectionNodeId,
428
+ parentAgentNodeId: ctx.nodeId,
429
+ parentAgentActivationId: ctx.activationId,
430
+ status: "queued",
431
+ managedInput: summarizedInput,
432
+ queuedAt: startedAt.toISOString(),
433
+ iterationId: ctx.iterationId,
434
+ itemIndex: ctx.itemIndex,
435
+ parentInvocationId: ctx.parentInvocationId,
436
+ });
424
437
  await ctx.nodeState?.markRunning({
425
438
  nodeId: languageModelConnectionNodeId,
426
439
  activationId: ctx.activationId,
427
440
  inputsByPort: itemInputsByPort,
428
441
  });
442
+ await ctx.nodeState?.appendConnectionInvocation({
443
+ invocationId,
444
+ connectionNodeId: languageModelConnectionNodeId,
445
+ parentAgentNodeId: ctx.nodeId,
446
+ parentAgentActivationId: ctx.activationId,
447
+ status: "running",
448
+ managedInput: summarizedInput,
449
+ queuedAt: startedAt.toISOString(),
450
+ startedAt: startedAt.toISOString(),
451
+ iterationId: ctx.iterationId,
452
+ itemIndex: ctx.itemIndex,
453
+ parentInvocationId: ctx.parentInvocationId,
454
+ });
429
455
  try {
430
456
  const tools = this.buildToolSet(itemScopedTools);
431
457
  const callOptions = this.resolveCallOptions(model, guardrails.modelInvocationOptions);
@@ -441,11 +467,12 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
441
467
  });
442
468
  const turnResult = this.extractTurnResult(result as AnyGenerateTextResult);
443
469
  const finishedAt = new Date();
470
+ const managedOutput = this.summarizeTurnOutput(turnResult);
444
471
  await ctx.nodeState?.markCompleted({
445
472
  nodeId: languageModelConnectionNodeId,
446
473
  activationId: ctx.activationId,
447
474
  inputsByPort: itemInputsByPort,
448
- outputs: AgentOutputFactory.fromUnknown({ content: turnResult.text }),
475
+ outputs: AgentOutputFactory.fromUnknown(managedOutput),
449
476
  });
450
477
  await span.attachArtifact({ kind: "ai.messages", contentType: "application/json", previewJson: summarizedInput });
451
478
  await span.attachArtifact({ kind: "ai.response", contentType: "application/json", previewJson: turnResult.text });
@@ -458,10 +485,13 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
458
485
  parentAgentActivationId: ctx.activationId,
459
486
  status: "completed",
460
487
  managedInput: summarizedInput,
461
- managedOutput: turnResult.text,
488
+ managedOutput,
462
489
  queuedAt: startedAt.toISOString(),
463
490
  startedAt: startedAt.toISOString(),
464
491
  finishedAt: finishedAt.toISOString(),
492
+ iterationId: ctx.iterationId,
493
+ itemIndex: ctx.itemIndex,
494
+ parentInvocationId: ctx.parentInvocationId,
465
495
  });
466
496
  return turnResult;
467
497
  } catch (error) {
@@ -504,11 +534,36 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
504
534
  activationId: ctx.activationId,
505
535
  inputsByPort: itemInputsByPort,
506
536
  });
537
+ await ctx.nodeState?.appendConnectionInvocation({
538
+ invocationId,
539
+ connectionNodeId: languageModelConnectionNodeId,
540
+ parentAgentNodeId: ctx.nodeId,
541
+ parentAgentActivationId: ctx.activationId,
542
+ status: "queued",
543
+ managedInput: summarizedInput,
544
+ queuedAt: startedAt.toISOString(),
545
+ iterationId: ctx.iterationId,
546
+ itemIndex: ctx.itemIndex,
547
+ parentInvocationId: ctx.parentInvocationId,
548
+ });
507
549
  await ctx.nodeState?.markRunning({
508
550
  nodeId: languageModelConnectionNodeId,
509
551
  activationId: ctx.activationId,
510
552
  inputsByPort: itemInputsByPort,
511
553
  });
554
+ await ctx.nodeState?.appendConnectionInvocation({
555
+ invocationId,
556
+ connectionNodeId: languageModelConnectionNodeId,
557
+ parentAgentNodeId: ctx.nodeId,
558
+ parentAgentActivationId: ctx.activationId,
559
+ status: "running",
560
+ managedInput: summarizedInput,
561
+ queuedAt: startedAt.toISOString(),
562
+ startedAt: startedAt.toISOString(),
563
+ iterationId: ctx.iterationId,
564
+ itemIndex: ctx.itemIndex,
565
+ parentInvocationId: ctx.parentInvocationId,
566
+ });
512
567
  try {
513
568
  const callOptions = this.resolveCallOptions(model, guardrails.modelInvocationOptions);
514
569
  const outputSchema =
@@ -551,6 +606,9 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
551
606
  queuedAt: startedAt.toISOString(),
552
607
  startedAt: startedAt.toISOString(),
553
608
  finishedAt: finishedAt.toISOString(),
609
+ iterationId: ctx.iterationId,
610
+ itemIndex: ctx.itemIndex,
611
+ parentInvocationId: ctx.parentInvocationId,
554
612
  });
555
613
  return result.experimental_output;
556
614
  } catch (error) {
@@ -589,6 +647,22 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
589
647
  };
590
648
  }
591
649
 
650
+ /**
651
+ * Build a no-code-friendly output payload for an LLM round.
652
+ *
653
+ * Always includes `content` (matching the canvas snapshot shape used elsewhere) and adds a
654
+ * `toolCalls` array when the round produced tool calls so the execution inspector surfaces the
655
+ * planned calls instead of just an empty `""` for tool-only rounds.
656
+ */
657
+ private summarizeTurnOutput(turnResult: TurnResult): JsonValue {
658
+ if (turnResult.toolCalls.length === 0) return { content: turnResult.text };
659
+ const toolCalls = turnResult.toolCalls.map((toolCall) => ({
660
+ name: toolCall.name,
661
+ args: this.resultToJsonValue(toolCall.input) ?? null,
662
+ }));
663
+ return { content: turnResult.text, toolCalls };
664
+ }
665
+
592
666
  private extractTurnResult(result: AnyGenerateTextResult): TurnResult {
593
667
  const usage = this.extractUsageFromResult(result);
594
668
  const text = result.text;
@@ -642,6 +716,13 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
642
716
  [CodemationTelemetryAttributeNames.connectionInvocationId]: invocationId,
643
717
  [GenAiTelemetryAttributeNames.operationName]: "chat",
644
718
  [GenAiTelemetryAttributeNames.requestModel]: this.resolveChatModelName(ctx.config.chatModel),
719
+ ...(ctx.iterationId ? { [CodemationTelemetryAttributeNames.iterationId]: ctx.iterationId } : {}),
720
+ ...(typeof ctx.itemIndex === "number"
721
+ ? { [CodemationTelemetryAttributeNames.iterationIndex]: ctx.itemIndex }
722
+ : {}),
723
+ ...(ctx.parentInvocationId
724
+ ? { [CodemationTelemetryAttributeNames.parentInvocationId]: ctx.parentInvocationId }
725
+ : {}),
645
726
  },
646
727
  });
647
728
  }
@@ -707,12 +788,25 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
707
788
  plannedToolCalls: ReadonlyArray<PlannedToolCall>,
708
789
  ctx: NodeExecutionContext<AIAgent<any, any>>,
709
790
  ): Promise<void> {
791
+ const queuedAt = new Date().toISOString();
710
792
  for (const plannedToolCall of plannedToolCalls) {
711
793
  await ctx.nodeState?.markQueued({
712
794
  nodeId: plannedToolCall.nodeId,
713
795
  activationId: ctx.activationId,
714
796
  inputsByPort: AgentToolCallPortMap.fromInput(plannedToolCall.toolCall.input ?? {}),
715
797
  });
798
+ await ctx.nodeState?.appendConnectionInvocation({
799
+ invocationId: plannedToolCall.invocationId,
800
+ connectionNodeId: plannedToolCall.nodeId,
801
+ parentAgentNodeId: ctx.nodeId,
802
+ parentAgentActivationId: ctx.activationId,
803
+ status: "queued",
804
+ managedInput: this.resultToJsonValue(plannedToolCall.toolCall.input),
805
+ queuedAt,
806
+ iterationId: ctx.iterationId,
807
+ itemIndex: ctx.itemIndex,
808
+ parentInvocationId: ctx.parentInvocationId,
809
+ });
716
810
  }
717
811
  }
718
812
 
@@ -732,6 +826,7 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
732
826
  toolCall,
733
827
  invocationIndex,
734
828
  nodeId: ConnectionNodeIdFactory.toolConnectionNodeId(parentNodeId, binding.config.name),
829
+ invocationId: ConnectionInvocationIdFactory.create(),
735
830
  } satisfies PlannedToolCall;
736
831
  });
737
832
  }
@@ -771,6 +866,9 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
771
866
  queuedAt: args.startedAt.toISOString(),
772
867
  startedAt: args.startedAt.toISOString(),
773
868
  finishedAt: finishedAt.toISOString(),
869
+ iterationId: args.ctx.iterationId,
870
+ itemIndex: args.ctx.itemIndex,
871
+ parentInvocationId: args.ctx.parentInvocationId,
774
872
  });
775
873
  return effectiveError;
776
874
  }
@@ -1,5 +1,5 @@
1
1
  import type { JsonValue, NodeExecutionContext } from "@codemation/core";
2
- import { CodemationTelemetryAttributeNames, ConnectionInvocationIdFactory, inject, injectable } from "@codemation/core";
2
+ import { CodemationTelemetryAttributeNames, inject, injectable } from "@codemation/core";
3
3
 
4
4
  import type { AIAgent } from "./AIAgentConfig";
5
5
  import { AgentOutputFactory } from "./AgentOutputFactory";
@@ -53,7 +53,7 @@ export class AgentToolExecutionCoordinator {
53
53
  ): Promise<ExecutedToolCall> {
54
54
  const { plannedToolCall, ctx } = args;
55
55
  const toolCallInputsByPort = AgentToolCallPortMap.fromInput(plannedToolCall.toolCall.input ?? {});
56
- const invocationId = ConnectionInvocationIdFactory.create();
56
+ const invocationId = plannedToolCall.invocationId;
57
57
  const startedAt = new Date();
58
58
  const span = ctx.telemetry.startChildSpan({
59
59
  name: "agent.tool.call",
@@ -62,6 +62,13 @@ export class AgentToolExecutionCoordinator {
62
62
  attributes: {
63
63
  [CodemationTelemetryAttributeNames.connectionInvocationId]: invocationId,
64
64
  [CodemationTelemetryAttributeNames.toolName]: plannedToolCall.binding.config.name,
65
+ ...(ctx.iterationId ? { [CodemationTelemetryAttributeNames.iterationId]: ctx.iterationId } : {}),
66
+ ...(typeof ctx.itemIndex === "number"
67
+ ? { [CodemationTelemetryAttributeNames.iterationIndex]: ctx.itemIndex }
68
+ : {}),
69
+ ...(ctx.parentInvocationId
70
+ ? { [CodemationTelemetryAttributeNames.parentInvocationId]: ctx.parentInvocationId }
71
+ : {}),
65
72
  },
66
73
  });
67
74
  await ctx.nodeState?.markRunning({
@@ -69,9 +76,24 @@ export class AgentToolExecutionCoordinator {
69
76
  activationId: ctx.activationId,
70
77
  inputsByPort: toolCallInputsByPort,
71
78
  });
79
+ await ctx.nodeState?.appendConnectionInvocation({
80
+ invocationId,
81
+ connectionNodeId: plannedToolCall.nodeId,
82
+ parentAgentNodeId: ctx.nodeId,
83
+ parentAgentActivationId: ctx.activationId,
84
+ status: "running",
85
+ managedInput: this.toJsonValue(plannedToolCall.toolCall.input),
86
+ queuedAt: startedAt.toISOString(),
87
+ startedAt: startedAt.toISOString(),
88
+ iterationId: ctx.iterationId,
89
+ parentInvocationId: ctx.parentInvocationId,
90
+ });
72
91
 
73
92
  try {
74
- const result = await plannedToolCall.binding.execute(plannedToolCall.toolCall.input ?? {});
93
+ const result = await plannedToolCall.binding.execute(plannedToolCall.toolCall.input ?? {}, {
94
+ parentSpan: span,
95
+ parentInvocationId: invocationId,
96
+ });
75
97
  const serialized = typeof result === "string" ? result : JSON.stringify(result);
76
98
  const finishedAt = new Date();
77
99
  await ctx.nodeState?.markCompleted({
@@ -102,6 +124,8 @@ export class AgentToolExecutionCoordinator {
102
124
  queuedAt: startedAt.toISOString(),
103
125
  startedAt: startedAt.toISOString(),
104
126
  finishedAt: finishedAt.toISOString(),
127
+ iterationId: ctx.iterationId,
128
+ parentInvocationId: ctx.parentInvocationId,
105
129
  });
106
130
  return {
107
131
  toolName: plannedToolCall.binding.config.name,
@@ -262,6 +286,8 @@ export class AgentToolExecutionCoordinator {
262
286
  queuedAt: args.startedAt.toISOString(),
263
287
  startedAt: args.startedAt.toISOString(),
264
288
  finishedAt: finishedAt.toISOString(),
289
+ iterationId: args.ctx.iterationId,
290
+ parentInvocationId: args.ctx.parentInvocationId,
265
291
  });
266
292
  }
267
293
 
@@ -11,6 +11,8 @@ import type {
11
11
  ZodSchemaAny,
12
12
  } from "@codemation/core";
13
13
  import {
14
+ AgentConfigInspector,
15
+ ChildExecutionScopeFactory,
14
16
  CoreTokens,
15
17
  inject,
16
18
  injectable,
@@ -31,6 +33,8 @@ export class NodeBackedToolRuntime {
31
33
  private readonly outputNormalizer: NodeOutputNormalizer,
32
34
  @inject(RunnableOutputBehaviorResolver)
33
35
  private readonly outputBehaviorResolver: RunnableOutputBehaviorResolver,
36
+ @inject(ChildExecutionScopeFactory)
37
+ private readonly childExecutionScopeFactory: ChildExecutionScopeFactory,
34
38
  ) {}
35
39
 
36
40
  async execute(
@@ -45,10 +49,7 @@ export class NodeBackedToolRuntime {
45
49
  ctx: args.ctx,
46
50
  node: config.node,
47
51
  });
48
- const nodeCtx = {
49
- ...args.ctx,
50
- config: config.node,
51
- } as NodeExecutionContext<RunnableNodeConfig>;
52
+ const nodeCtx = this.resolveNodeCtx(config, args);
52
53
  const resolvedNode = this.nodeResolver.resolve(config.node.type);
53
54
  const outputs = await this.executeResolvedNode(resolvedNode, nodeInput, nodeCtx);
54
55
  return config.toToolOutput({
@@ -62,6 +63,41 @@ export class NodeBackedToolRuntime {
62
63
  });
63
64
  }
64
65
 
66
+ /**
67
+ * Returns a re-rooted child ctx for nested-agent tools (so their LLM/tool connection ids derive
68
+ * from the tool connection node, telemetry parents under the tool-call span, and connection
69
+ * invocations carry `parentInvocationId`). Plain runnable tools (non-agent) keep the orchestrator
70
+ * ctx with only `config` swapped — no nesting concern.
71
+ *
72
+ * The caller (`AIAgentNode.createItemScopedTools`) already wraps the orchestrator ctx via
73
+ * `ConnectionCredentialExecutionContextFactory.forConnectionNode`, so `args.ctx.nodeId` is the
74
+ * tool's own connection node id (e.g. `AIAgentNode:2__conn__tool__searchInMail`). We pass that
75
+ * through as the sub-agent's `nodeId`; deriving another `toolConnectionNodeId(args.ctx.nodeId,
76
+ * config.name)` here would prepend a duplicate `__conn__tool__<name>` segment and exponentially
77
+ * deepen ids on each invocation, which also breaks credential resolution because user-provided
78
+ * bindings sit on the single-level connection node id.
79
+ */
80
+ private resolveNodeCtx(
81
+ config: NodeBackedToolConfig<any, ZodSchemaAny, ZodSchemaAny>,
82
+ args: ToolExecuteArgs,
83
+ ): NodeExecutionContext<RunnableNodeConfig> {
84
+ const isNestedAgent = AgentConfigInspector.isAgentNodeConfig(config.node);
85
+ const hooks = args.hooks;
86
+ if (!isNestedAgent || !hooks?.parentSpan || !hooks.parentInvocationId) {
87
+ return {
88
+ ...args.ctx,
89
+ config: config.node,
90
+ } as NodeExecutionContext<RunnableNodeConfig>;
91
+ }
92
+ return this.childExecutionScopeFactory.forSubAgent({
93
+ parentCtx: args.ctx as NodeExecutionContext<RunnableNodeConfig>,
94
+ childNodeId: args.ctx.nodeId,
95
+ childConfig: config.node as unknown as RunnableNodeConfig,
96
+ parentInvocationId: hooks.parentInvocationId,
97
+ parentSpan: hooks.parentSpan,
98
+ });
99
+ }
100
+
65
101
  private async executeResolvedNode(
66
102
  resolvedNode: unknown,
67
103
  nodeInput: ToolExecuteArgs["item"],
@@ -1,7 +1,9 @@
1
1
  import type {
2
2
  AgentToolCall,
3
+ ConnectionInvocationId,
3
4
  Item,
4
5
  NodeInputsByPort,
6
+ TelemetrySpanScope,
5
7
  ToolConfig,
6
8
  ToolExecuteArgs,
7
9
  ZodSchemaAny,
@@ -24,13 +26,24 @@ export type ResolvedTool = Readonly<{
24
26
 
25
27
  /**
26
28
  * Per-item binding of a tool: the user config plus the resolved runtime and a snapshot of the
27
- * original Zod `inputSchema` used to convert to AI SDK `Tool` + OpenAI-strict JSON Schema for
28
- * repair prompts.
29
+ * original Zod `inputSchema`.
30
+ *
31
+ * `execute` accepts optional `hooks` so the agent coordinator can pass the live `agent.tool.call`
32
+ * span and the planned tool-call's `invocationId`. Node-backed sub-agent tools use these hooks
33
+ * via {@link ChildExecutionScopeFactory} to re-root their runtime ctx under the tool-call boundary
34
+ * (fresh activationId, telemetry parented at the tool-call span, `parentInvocationId` set).
29
35
  */
30
36
  export type ItemScopedToolBinding = Readonly<{
31
37
  config: ToolConfig;
32
38
  inputSchema: ZodSchemaAny;
33
- execute(input: unknown): Promise<unknown>;
39
+ execute(input: unknown, hooks?: ItemScopedToolCallHooks): Promise<unknown>;
40
+ }>;
41
+
42
+ export type ItemScopedToolCallHooks = Readonly<{
43
+ /** Live agent.tool.call span (used to parent sub-agent telemetry). */
44
+ parentSpan?: TelemetrySpanScope;
45
+ /** invocationId of the parent tool call (used to thread `parentInvocationId` through ctx). */
46
+ parentInvocationId?: ConnectionInvocationId;
34
47
  }>;
35
48
 
36
49
  export type PlannedToolCall = Readonly<{
@@ -38,6 +51,8 @@ export type PlannedToolCall = Readonly<{
38
51
  toolCall: AgentToolCall;
39
52
  invocationIndex: number;
40
53
  nodeId: string;
54
+ /** Stable id reused across queued / running / completed connection invocation rows for this tool call. */
55
+ invocationId: string;
41
56
  }>;
42
57
 
43
58
  export type ExecutedToolCall = Readonly<{