@codemation/host 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.
Files changed (70) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/{AppConfigFactory-BPp02HMv.js → AppConfigFactory-C4OXGOs2.js} +16 -7
  3. package/dist/{AppConfigFactory-BPp02HMv.js.map → AppConfigFactory-C4OXGOs2.js.map} +1 -1
  4. package/dist/{AppConfigFactory-PFmDg5Sg.d.ts → AppConfigFactory-D3k-R3Ch.d.ts} +338 -6
  5. package/dist/{AppContainerFactory-Cr3JeVmg.js → AppContainerFactory-CKRDz8kQ.js} +409 -92
  6. package/dist/AppContainerFactory-CKRDz8kQ.js.map +1 -0
  7. package/dist/{CodemationAppContext-DP_-56c6.d.ts → CodemationAppContext-YgJRUHWF.d.ts} +2 -2
  8. package/dist/{CodemationAuthoring.types-Cr2QZsUX.d.ts → CodemationAuthoring.types-lUdxXYq-.d.ts} +3 -3
  9. package/dist/{CodemationConfigNormalizer-B8RGUwAe.d.ts → CodemationConfigNormalizer-BWBp7mFB.d.ts} +2 -2
  10. package/dist/{CodemationConsumerConfigLoader-C_QVwcI3.d.ts → CodemationConsumerConfigLoader-Bka3v6lh.d.ts} +2 -2
  11. package/dist/{CodemationPluginListMerger-Bgn1CIX9.d.ts → CodemationPluginListMerger-Oz-GAkxz.d.ts} +17 -5
  12. package/dist/{CredentialServices-95DPogx-.d.ts → CredentialServices-CKXPg5xu.d.ts} +3 -3
  13. package/dist/{PublicFrontendBootstrapFactory-C_iLgPV-.d.ts → PublicFrontendBootstrapFactory-DkQoSYDo.d.ts} +2 -2
  14. package/dist/authoring.d.ts +3 -3
  15. package/dist/consumer.d.ts +4 -4
  16. package/dist/credentials.d.ts +3 -3
  17. package/dist/devServerSidecar.d.ts +1 -1
  18. package/dist/{index-W4eSjdCM.d.ts → index-BxIc_L4D.d.ts} +233 -151
  19. package/dist/index.d.ts +11 -11
  20. package/dist/index.js +4 -4
  21. package/dist/nextServer.d.ts +7 -7
  22. package/dist/nextServer.js +2 -2
  23. package/dist/{persistenceServer-_pqP_0nw.d.ts → persistenceServer-BLG7_6B5.d.ts} +2 -2
  24. package/dist/{persistenceServer-CA0_q0D7.js → persistenceServer-KyHL0u01.js} +2 -2
  25. package/dist/{persistenceServer-CA0_q0D7.js.map → persistenceServer-KyHL0u01.js.map} +1 -1
  26. package/dist/persistenceServer.d.ts +5 -5
  27. package/dist/persistenceServer.js +2 -2
  28. package/dist/{server-Q5uwa6iR.d.ts → server-B0SD6Nvk.d.ts} +5 -5
  29. package/dist/{server-BE4PLhcb.js → server-CMUVhYIc.js} +3 -3
  30. package/dist/{server-BE4PLhcb.js.map → server-CMUVhYIc.js.map} +1 -1
  31. package/dist/server.d.ts +8 -8
  32. package/dist/server.js +4 -4
  33. package/package.json +5 -5
  34. package/prisma/migrations/20260430120000_telemetry_iteration_identity/migration.sql +17 -0
  35. package/prisma/migrations/20260430130000_execution_instance_iteration_identity/migration.sql +11 -0
  36. package/prisma/migrations.sqlite/20260430120000_telemetry_iteration_identity/migration.sql +14 -0
  37. package/prisma/migrations.sqlite/20260430130000_execution_instance_iteration_identity/migration.sql +10 -0
  38. package/prisma/schema.postgresql.prisma +12 -0
  39. package/prisma/schema.sqlite.prisma +12 -0
  40. package/src/application/contracts/IterationCostContracts.ts +11 -0
  41. package/src/application/queries/GetIterationCostQuery.ts +14 -0
  42. package/src/application/queries/GetIterationCostQueryHandler.ts +92 -0
  43. package/src/application/queries/GetWorkflowRunDetailQueryHandler.ts +44 -2
  44. package/src/application/queries/RunIterationProjectionFactory.ts +123 -0
  45. package/src/application/queries/WorkflowQueryHandlers.ts +1 -0
  46. package/src/application/telemetry/OtelExecutionTelemetry.types.ts +3 -0
  47. package/src/application/telemetry/RunEventBusTelemetryReporter.ts +7 -0
  48. package/src/application/telemetry/StoredNodeExecutionTelemetry.ts +14 -0
  49. package/src/application/telemetry/StoredTelemetrySpanScope.ts +90 -1
  50. package/src/bootstrap/AppContainerFactory.ts +5 -0
  51. package/src/domain/telemetry/TelemetryContracts.ts +12 -0
  52. package/src/infrastructure/persistence/InMemoryTelemetryMetricPointStore.ts +3 -0
  53. package/src/infrastructure/persistence/InMemoryTelemetrySpanStore.ts +3 -0
  54. package/src/infrastructure/persistence/InMemoryWorkflowRunRepository.ts +23 -0
  55. package/src/infrastructure/persistence/PrismaTelemetryMetricPointStore.ts +9 -0
  56. package/src/infrastructure/persistence/PrismaTelemetrySpanStore.ts +6 -0
  57. package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +12 -0
  58. package/src/infrastructure/persistence/generated/prisma-postgresql-client/edge.js +15 -6
  59. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index-browser.js +11 -2
  60. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.d.ts +343 -5
  61. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.js +15 -6
  62. package/src/infrastructure/persistence/generated/prisma-postgresql-client/package.json +1 -1
  63. package/src/infrastructure/persistence/generated/prisma-postgresql-client/schema.prisma +12 -0
  64. package/src/infrastructure/persistence/generated/prisma-sqlite-client/edge.js +15 -6
  65. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index-browser.js +11 -2
  66. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.d.ts +343 -5
  67. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.js +15 -6
  68. package/src/infrastructure/persistence/generated/prisma-sqlite-client/package.json +1 -1
  69. package/src/infrastructure/persistence/generated/prisma-sqlite-client/schema.prisma +12 -0
  70. package/dist/AppContainerFactory-Cr3JeVmg.js.map +0 -1
@@ -0,0 +1,92 @@
1
+ import { CostTrackingTelemetryAttributeNames, CostTrackingTelemetryMetricNames, inject } from "@codemation/core";
2
+ import { ApplicationTokens } from "../../applicationTokens";
3
+ import type { TelemetryMetricPointRecord, TelemetryMetricPointStore } from "../../domain/telemetry/TelemetryContracts";
4
+ import { HandlesQuery } from "../../infrastructure/di/HandlesQueryRegistry";
5
+ import { QueryHandler } from "../bus/QueryHandler";
6
+ import type { IterationCostRollupDto } from "../contracts/IterationCostContracts";
7
+ import { GetIterationCostQuery } from "./GetIterationCostQuery";
8
+
9
+ interface MutableCostBucket {
10
+ readonly iterationId: string;
11
+ readonly estimatedCostMinorByCurrency: Record<string, number>;
12
+ readonly estimatedCostCurrencyScaleByCurrency: Record<string, number>;
13
+ }
14
+
15
+ @HandlesQuery.for(GetIterationCostQuery)
16
+ export class GetIterationCostQueryHandler extends QueryHandler<
17
+ GetIterationCostQuery,
18
+ ReadonlyArray<IterationCostRollupDto>
19
+ > {
20
+ constructor(
21
+ @inject(ApplicationTokens.TelemetryMetricPointStore)
22
+ private readonly metricPointStore: TelemetryMetricPointStore,
23
+ ) {
24
+ super();
25
+ }
26
+
27
+ async execute(query: GetIterationCostQuery): Promise<ReadonlyArray<IterationCostRollupDto>> {
28
+ const points = await this.metricPointStore.list({
29
+ runId: query.runId,
30
+ metricNames: [CostTrackingTelemetryMetricNames.estimatedCost],
31
+ });
32
+ if (points.length === 0) {
33
+ return [];
34
+ }
35
+ const buckets = new Map<string, MutableCostBucket>();
36
+ for (const point of points) {
37
+ this.accumulate(point, buckets);
38
+ }
39
+ return [...buckets.values()].map((bucket) => ({
40
+ iterationId: bucket.iterationId,
41
+ estimatedCostMinorByCurrency: { ...bucket.estimatedCostMinorByCurrency },
42
+ estimatedCostCurrencyScaleByCurrency: { ...bucket.estimatedCostCurrencyScaleByCurrency },
43
+ }));
44
+ }
45
+
46
+ private accumulate(point: TelemetryMetricPointRecord, buckets: Map<string, MutableCostBucket>): void {
47
+ const iterationId = point.iterationId;
48
+ if (!iterationId || iterationId.length === 0) {
49
+ return;
50
+ }
51
+ const currency = this.readCurrency(point);
52
+ if (!currency) {
53
+ return;
54
+ }
55
+ const currencyScale = this.readCurrencyScale(point);
56
+ const bucket = this.bucketFor(iterationId, buckets);
57
+ bucket.estimatedCostMinorByCurrency[currency] = (bucket.estimatedCostMinorByCurrency[currency] ?? 0) + point.value;
58
+ if (typeof currencyScale === "number") {
59
+ bucket.estimatedCostCurrencyScaleByCurrency[currency] = currencyScale;
60
+ }
61
+ }
62
+
63
+ private bucketFor(iterationId: string, buckets: Map<string, MutableCostBucket>): MutableCostBucket {
64
+ const existing = buckets.get(iterationId);
65
+ if (existing) {
66
+ return existing;
67
+ }
68
+ const bucket: MutableCostBucket = {
69
+ iterationId,
70
+ estimatedCostMinorByCurrency: {},
71
+ estimatedCostCurrencyScaleByCurrency: {},
72
+ };
73
+ buckets.set(iterationId, bucket);
74
+ return bucket;
75
+ }
76
+
77
+ private readCurrency(point: TelemetryMetricPointRecord): string | undefined {
78
+ const fromAttribute = point.dimensions?.[CostTrackingTelemetryAttributeNames.currency];
79
+ if (typeof fromAttribute === "string" && fromAttribute.length > 0) {
80
+ return fromAttribute;
81
+ }
82
+ if (typeof point.unit === "string" && point.unit.length > 0) {
83
+ return point.unit;
84
+ }
85
+ return undefined;
86
+ }
87
+
88
+ private readCurrencyScale(point: TelemetryMetricPointRecord): number | undefined {
89
+ const value = point.dimensions?.[CostTrackingTelemetryAttributeNames.currencyScale];
90
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
91
+ }
92
+ }
@@ -1,10 +1,14 @@
1
- import type { WorkflowRunDetailDto } from "@codemation/core";
1
+ import type { RunIterationDto, WorkflowRunDetailDto } from "@codemation/core";
2
2
  import { inject } from "@codemation/core";
3
3
  import { ApplicationTokens } from "../../applicationTokens";
4
4
  import type { WorkflowRunRepository } from "../../domain/runs/WorkflowRunRepository";
5
5
  import { HandlesQuery } from "../../infrastructure/di/HandlesQueryRegistry";
6
6
  import { QueryHandler } from "../bus/QueryHandler";
7
+ import type { IterationCostRollupDto } from "../contracts/IterationCostContracts";
8
+ import { GetIterationCostQuery } from "./GetIterationCostQuery";
9
+ import { GetIterationCostQueryHandler } from "./GetIterationCostQueryHandler";
7
10
  import { GetWorkflowRunDetailQuery } from "./GetWorkflowRunDetailQuery";
11
+ import { RunIterationProjectionFactory } from "./RunIterationProjectionFactory";
8
12
 
9
13
  @HandlesQuery.for(GetWorkflowRunDetailQuery)
10
14
  export class GetWorkflowRunDetailQueryHandler extends QueryHandler<
@@ -14,11 +18,49 @@ export class GetWorkflowRunDetailQueryHandler extends QueryHandler<
14
18
  constructor(
15
19
  @inject(ApplicationTokens.WorkflowRunRepository)
16
20
  private readonly workflowRunRepository: WorkflowRunRepository,
21
+ @inject(RunIterationProjectionFactory)
22
+ private readonly runIterationProjectionFactory: RunIterationProjectionFactory,
23
+ @inject(GetIterationCostQueryHandler)
24
+ private readonly getIterationCostQueryHandler: GetIterationCostQueryHandler,
17
25
  ) {
18
26
  super();
19
27
  }
20
28
 
21
29
  async execute(query: GetWorkflowRunDetailQuery): Promise<WorkflowRunDetailDto | undefined> {
22
- return await this.workflowRunRepository.loadRunDetail?.(query.runId);
30
+ const detail = await this.workflowRunRepository.loadRunDetail?.(query.runId);
31
+ if (!detail) {
32
+ return undefined;
33
+ }
34
+ const baseIterations = this.runIterationProjectionFactory.project(detail.executionInstances);
35
+ const iterations = await this.joinIterationCosts(query.runId, baseIterations);
36
+ return { ...detail, iterations };
37
+ }
38
+
39
+ private async joinIterationCosts(
40
+ runId: string,
41
+ iterations: ReadonlyArray<RunIterationDto>,
42
+ ): Promise<ReadonlyArray<RunIterationDto>> {
43
+ if (iterations.length === 0) {
44
+ return iterations;
45
+ }
46
+ const rollups = await this.getIterationCostQueryHandler.execute(new GetIterationCostQuery(runId));
47
+ if (rollups.length === 0) {
48
+ return iterations;
49
+ }
50
+ const rollupsByIterationId = new Map<string, IterationCostRollupDto>();
51
+ for (const rollup of rollups) {
52
+ rollupsByIterationId.set(rollup.iterationId, rollup);
53
+ }
54
+ return iterations.map((iteration) => {
55
+ const rollup = rollupsByIterationId.get(iteration.iterationId);
56
+ if (!rollup) {
57
+ return iteration;
58
+ }
59
+ return {
60
+ ...iteration,
61
+ estimatedCostMinorByCurrency: rollup.estimatedCostMinorByCurrency,
62
+ estimatedCostCurrencyScaleByCurrency: rollup.estimatedCostCurrencyScaleByCurrency,
63
+ };
64
+ });
23
65
  }
24
66
  }
@@ -0,0 +1,123 @@
1
+ import {
2
+ injectable,
3
+ type ExecutionInstanceDto,
4
+ type NodeExecutionStatus,
5
+ type RunIterationDto,
6
+ } from "@codemation/core";
7
+
8
+ /**
9
+ * Builds the per-iteration projection from a run's connection invocations.
10
+ *
11
+ * One iteration represents a single item being processed by an agent within an activation. All
12
+ * invocations (LLM rounds, tool calls) emitted while handling that item share the same iterationId
13
+ * and project into one {@link RunIterationDto}.
14
+ *
15
+ * Old runs (persisted before iteration ids existed) fall back to grouping by the agent
16
+ * activationId so the UI still sees coherent groups instead of a flat list.
17
+ */
18
+ @injectable()
19
+ export class RunIterationProjectionFactory {
20
+ project(executionInstances: ReadonlyArray<ExecutionInstanceDto>): ReadonlyArray<RunIterationDto> {
21
+ const invocations = executionInstances.filter((row) => row.kind === "connectionInvocation");
22
+ if (invocations.length === 0) {
23
+ return [];
24
+ }
25
+ const grouped = new Map<string, ExecutionInstanceDto[]>();
26
+ for (const invocation of invocations) {
27
+ const key = this.iterationKey(invocation);
28
+ if (!key) {
29
+ continue;
30
+ }
31
+ const bucket = grouped.get(key);
32
+ if (bucket) {
33
+ bucket.push(invocation);
34
+ } else {
35
+ grouped.set(key, [invocation]);
36
+ }
37
+ }
38
+ const iterations: RunIterationDto[] = [];
39
+ for (const [key, group] of grouped.entries()) {
40
+ iterations.push(this.toIteration(key, group));
41
+ }
42
+ iterations.sort((left, right) => this.compareIterations(left, right));
43
+ return iterations;
44
+ }
45
+
46
+ private iterationKey(invocation: ExecutionInstanceDto): string | undefined {
47
+ if (invocation.iterationId) {
48
+ return invocation.iterationId;
49
+ }
50
+ if (invocation.activationId) {
51
+ return `legacy::${invocation.workflowNodeId}::${invocation.activationId}::${invocation.itemIndex ?? 0}`;
52
+ }
53
+ return undefined;
54
+ }
55
+
56
+ private toIteration(iterationKey: string, group: ReadonlyArray<ExecutionInstanceDto>): RunIterationDto {
57
+ const sorted = [...group].sort((a, b) => this.compareInvocations(a, b));
58
+ const first = sorted[0]!;
59
+ const status = this.aggregateStatus(sorted);
60
+ const iterationId = first.iterationId ?? iterationKey;
61
+ return {
62
+ iterationId,
63
+ agentNodeId: first.workflowNodeId,
64
+ activationId: first.activationId ?? "synthetic",
65
+ itemIndex: first.itemIndex ?? 0,
66
+ status,
67
+ startedAt: this.minIso(sorted.map((row) => row.startedAt)),
68
+ finishedAt: status === "running" ? undefined : this.maxIso(sorted.map((row) => row.finishedAt)),
69
+ invocationIds: sorted.map((row) => row.instanceId),
70
+ parentInvocationId: first.parentInvocationId,
71
+ };
72
+ }
73
+
74
+ private aggregateStatus(group: ReadonlyArray<ExecutionInstanceDto>): NodeExecutionStatus {
75
+ if (group.some((row) => row.status === "failed")) {
76
+ return "failed";
77
+ }
78
+ if (group.every((row) => row.status === "completed")) {
79
+ return "completed";
80
+ }
81
+ return "running";
82
+ }
83
+
84
+ private minIso(values: ReadonlyArray<string | undefined>): string | undefined {
85
+ let min: string | undefined;
86
+ for (const value of values) {
87
+ if (!value) continue;
88
+ if (!min || value < min) {
89
+ min = value;
90
+ }
91
+ }
92
+ return min;
93
+ }
94
+
95
+ private maxIso(values: ReadonlyArray<string | undefined>): string | undefined {
96
+ let max: string | undefined;
97
+ for (const value of values) {
98
+ if (!value) continue;
99
+ if (!max || value > max) {
100
+ max = value;
101
+ }
102
+ }
103
+ return max;
104
+ }
105
+
106
+ private compareInvocations(left: ExecutionInstanceDto, right: ExecutionInstanceDto): number {
107
+ const leftStart = left.startedAt ?? left.queuedAt ?? "";
108
+ const rightStart = right.startedAt ?? right.queuedAt ?? "";
109
+ if (leftStart !== rightStart) {
110
+ return leftStart.localeCompare(rightStart);
111
+ }
112
+ return left.runIndex - right.runIndex;
113
+ }
114
+
115
+ private compareIterations(left: RunIterationDto, right: RunIterationDto): number {
116
+ if (left.itemIndex !== right.itemIndex) {
117
+ return left.itemIndex - right.itemIndex;
118
+ }
119
+ const leftStart = left.startedAt ?? "";
120
+ const rightStart = right.startedAt ?? "";
121
+ return leftStart.localeCompare(rightStart);
122
+ }
123
+ }
@@ -5,6 +5,7 @@ export { GetTelemetryDashboardRunsQueryHandler } from "./GetTelemetryDashboardRu
5
5
  export { GetTelemetryRunTraceQueryHandler } from "./GetTelemetryRunTraceQueryHandler";
6
6
  export { GetTelemetryDashboardSummaryQueryHandler } from "./GetTelemetryDashboardSummaryQueryHandler";
7
7
  export { GetTelemetryDashboardTimeseriesQueryHandler } from "./GetTelemetryDashboardTimeseriesQueryHandler";
8
+ export { GetIterationCostQueryHandler } from "./GetIterationCostQueryHandler";
8
9
  export { GetWorkflowRunDetailQueryHandler } from "./GetWorkflowRunDetailQueryHandler";
9
10
  export { GetWorkflowDebuggerOverlayQueryHandler } from "./GetWorkflowDebuggerOverlayQueryHandler";
10
11
  export { GetWorkflowDetailQueryHandler } from "./GetWorkflowDetailQueryHandler";
@@ -38,4 +38,7 @@ export type StoredSpanScopeArgs = StoredExecutionTelemetryDeps &
38
38
  initialStartTime?: Date;
39
39
  connectionInvocationId?: string;
40
40
  modelName?: string;
41
+ iterationId?: string;
42
+ itemIndex?: number;
43
+ parentInvocationId?: string;
41
44
  }>;
@@ -67,6 +67,13 @@ export class RunEventBusTelemetryReporter {
67
67
  case "nodeFailed":
68
68
  await this.handleNodeSnapshot(event);
69
69
  return;
70
+ case "connectionInvocationStarted":
71
+ case "connectionInvocationCompleted":
72
+ case "connectionInvocationFailed":
73
+ // Per-invocation events are surfaced to the realtime UI via the websocket bridge;
74
+ // they do not need additional span/metric persistence here because the underlying
75
+ // child spans are already produced by the engine's telemetry scopes.
76
+ return;
70
77
  }
71
78
  }
72
79
 
@@ -13,6 +13,13 @@ export class StoredNodeExecutionTelemetry extends StoredTelemetrySpanScope imple
13
13
  }
14
14
 
15
15
  startChildSpan(args: TelemetryChildSpanStart): TelemetrySpanScope {
16
+ // Iteration / parent-invocation identity is read from the attribute bag when present (the
17
+ // engine passes them in for runnable per-item loops and sub-agent boundaries) and falls back
18
+ // to the parent scope's identity otherwise (so spans started outside the iteration loop still
19
+ // inherit a non-iteration scope).
20
+ const iterationIdFromAttrs = this.toStringAttribute(args.attributes?.["codemation.iteration.id"]);
21
+ const itemIndexFromAttrs = this.toNumberAttribute(args.attributes?.["codemation.iteration.index"]);
22
+ const parentInvocationIdFromAttrs = this.toStringAttribute(args.attributes?.["codemation.parent.invocation_id"]);
16
23
  // eslint-disable-next-line codemation/no-manual-di-new
17
24
  const span = new StoredTelemetrySpanScope({
18
25
  ...this.deps,
@@ -28,8 +35,15 @@ export class StoredNodeExecutionTelemetry extends StoredTelemetrySpanScope imple
28
35
  args.attributes?.["codemation.connection.invocation_id"] ?? args.attributes?.["connection.invocation_id"],
29
36
  ),
30
37
  modelName: this.toStringAttribute(args.attributes?.[GenAiTelemetryAttributeNames.requestModel]),
38
+ iterationId: iterationIdFromAttrs ?? this.iterationId,
39
+ itemIndex: itemIndexFromAttrs ?? this.itemIndex,
40
+ parentInvocationId: parentInvocationIdFromAttrs ?? this.parentInvocationId,
31
41
  });
32
42
  void span.markStarted();
33
43
  return span;
34
44
  }
45
+
46
+ private toNumberAttribute(value: unknown): number | undefined {
47
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
48
+ }
35
49
  }
@@ -1,4 +1,7 @@
1
1
  import type {
2
+ NodeActivationId,
3
+ NodeExecutionTelemetry,
4
+ NodeId,
2
5
  TelemetryArtifactAttachment,
3
6
  TelemetryArtifactReference,
4
7
  TelemetryAttributes,
@@ -25,6 +28,9 @@ export class StoredTelemetrySpanScope implements TelemetrySpanScope {
25
28
  private readonly initialStartTime: Date | undefined;
26
29
  private readonly connectionInvocationId: string | undefined;
27
30
  private readonly modelName: string | undefined;
31
+ protected readonly iterationId: string | undefined;
32
+ protected readonly itemIndex: number | undefined;
33
+ protected readonly parentInvocationId: string | undefined;
28
34
 
29
35
  constructor(args: StoredSpanScopeArgs) {
30
36
  this.deps = args;
@@ -39,6 +45,9 @@ export class StoredTelemetrySpanScope implements TelemetrySpanScope {
39
45
  this.initialStartTime = args.initialStartTime;
40
46
  this.connectionInvocationId = args.connectionInvocationId;
41
47
  this.modelName = args.modelName;
48
+ this.iterationId = args.iterationId;
49
+ this.itemIndex = args.itemIndex;
50
+ this.parentInvocationId = args.parentInvocationId;
42
51
  }
43
52
 
44
53
  async addSpanEvent(args: TelemetrySpanEventRecord): Promise<void> {
@@ -66,6 +75,9 @@ export class StoredTelemetrySpanScope implements TelemetrySpanScope {
66
75
  nodeType: enrichment.nodeType,
67
76
  nodeRole: enrichment.nodeRole,
68
77
  modelName: this.modelName,
78
+ iterationId: this.iterationId,
79
+ itemIndex: this.itemIndex,
80
+ parentInvocationId: this.parentInvocationId,
69
81
  retentionExpiresAt: this.deps.telemetryRetentionTimestampFactory.createMetricExpiry(
70
82
  this.deps.policySnapshot,
71
83
  observedAt,
@@ -121,16 +133,93 @@ export class StoredTelemetrySpanScope implements TelemetrySpanScope {
121
133
  });
122
134
  }
123
135
 
136
+ asNodeTelemetry(args: Readonly<{ nodeId: NodeId; activationId: NodeActivationId }>): NodeExecutionTelemetry {
137
+ return this.buildNodeTelemetryView(args);
138
+ }
139
+
140
+ private buildNodeTelemetryView(
141
+ args: Readonly<{ nodeId: NodeId; activationId: NodeActivationId }>,
142
+ ): NodeExecutionTelemetry {
143
+ // Returns a NodeExecutionTelemetry view of THIS span: children created via the returned
144
+ // telemetry's `startChildSpan` parent under this span (e.g. agent.tool.call) and inherit the
145
+ // child execution scope's nodeId/activationId. Used at the sub-agent boundary so nested
146
+ // runtime telemetry parents under the tool-call span instead of the orchestrator's node span.
147
+ const buildChildScope = (
148
+ childName: string,
149
+ childKind: "internal" | "client",
150
+ childAttrs?: TelemetryAttributes,
151
+ childStart?: Date,
152
+ ): StoredTelemetrySpanScope => {
153
+ // eslint-disable-next-line codemation/no-manual-di-new
154
+ const child = new StoredTelemetrySpanScope({
155
+ ...this.deps,
156
+ spanId: this.deps.otelIdentityFactory.createEphemeralSpanId(),
157
+ parentSpanId: this.spanId,
158
+ nodeId: args.nodeId,
159
+ activationId: args.activationId,
160
+ spanName: childName,
161
+ spanKind: childKind,
162
+ initialAttributes: childAttrs,
163
+ initialStartTime: childStart,
164
+ connectionInvocationId: this.toStringAttribute(
165
+ childAttrs?.["codemation.connection.invocation_id"] ?? childAttrs?.["connection.invocation_id"],
166
+ ),
167
+ modelName: this.toStringAttribute(childAttrs?.["gen_ai.request.model"]),
168
+ iterationId: this.iterationId,
169
+ itemIndex: this.itemIndex,
170
+ parentInvocationId: this.parentInvocationId,
171
+ });
172
+ void child.markStarted();
173
+ return child;
174
+ };
175
+ const view: NodeExecutionTelemetry = {
176
+ traceId: this.traceId,
177
+ spanId: this.spanId,
178
+ addSpanEvent: (event) => this.addSpanEvent(event),
179
+ recordMetric: (metric) => this.recordMetric(metric),
180
+ attachArtifact: (artifact) => this.attachArtifact(artifact),
181
+ end: (endArgs) => this.end(endArgs),
182
+ asNodeTelemetry: (rescope) => this.asNodeTelemetry(rescope),
183
+ forNode: () => view,
184
+ startChildSpan: (childArgs) =>
185
+ buildChildScope(childArgs.name, childArgs.kind ?? "internal", childArgs.attributes, childArgs.startedAt),
186
+ };
187
+ return view;
188
+ }
189
+
124
190
  async markStarted(): Promise<void> {
125
191
  await this.upsert({
126
192
  status: "running",
127
193
  startTime: (this.initialStartTime ?? new Date()).toISOString(),
128
- attributes: this.initialAttributes,
194
+ attributes: this.attributesWithIdentity(this.initialAttributes),
129
195
  modelName: this.modelName,
130
196
  connectionInvocationId: this.connectionInvocationId,
197
+ iterationId: this.iterationId,
198
+ itemIndex: this.itemIndex,
199
+ parentInvocationId: this.parentInvocationId,
131
200
  });
132
201
  }
133
202
 
203
+ /**
204
+ * Stamps `codemation.iteration.id`, `codemation.iteration.index`, and
205
+ * `codemation.parent.invocation_id` onto the attribute bag so dashboards/queries can filter by
206
+ * iteration without joining on the dedicated columns. The dedicated columns are still the
207
+ * authoritative source — these attributes are convenience for downstream consumers.
208
+ */
209
+ protected attributesWithIdentity(attrs: TelemetryAttributes | undefined): TelemetryAttributes | undefined {
210
+ const base: Record<string, TelemetryAttributes[string]> = { ...(attrs ?? {}) };
211
+ if (typeof this.iterationId === "string" && this.iterationId.length > 0) {
212
+ base["codemation.iteration.id"] = this.iterationId;
213
+ }
214
+ if (typeof this.itemIndex === "number") {
215
+ base["codemation.iteration.index"] = this.itemIndex;
216
+ }
217
+ if (typeof this.parentInvocationId === "string" && this.parentInvocationId.length > 0) {
218
+ base["codemation.parent.invocation_id"] = this.parentInvocationId;
219
+ }
220
+ return Object.keys(base).length > 0 ? base : undefined;
221
+ }
222
+
134
223
  protected async upsert(update: Partial<TelemetrySpanUpsert>): Promise<void> {
135
224
  const enrichment = await this.resolveEnrichment();
136
225
  const observedAt = this.resolveObservedAt(update);
@@ -62,6 +62,7 @@ import {
62
62
  VerifyUserInviteQueryHandler,
63
63
  } from "../application/queries/UserAccountQueryHandlers";
64
64
  import {
65
+ GetIterationCostQueryHandler,
65
66
  GetRunBinaryAttachmentQueryHandler,
66
67
  GetTelemetryDashboardDimensionsQueryHandler,
67
68
  GetTelemetryDashboardRunsQueryHandler,
@@ -76,6 +77,7 @@ import {
76
77
  GetWorkflowSummariesQueryHandler,
77
78
  ListWorkflowRunsQueryHandler,
78
79
  } from "../application/queries/WorkflowQueryHandlers";
80
+ import { RunIterationProjectionFactory } from "../application/queries/RunIterationProjectionFactory";
79
81
  import { OpenAiApiKeyCredentialHealthTester } from "../infrastructure/credentials/OpenAiApiKeyCredentialHealthTester";
80
82
  import { OpenAiApiKeyCredentialTypeFactory } from "../infrastructure/credentials/OpenAiApiKeyCredentialTypeFactory";
81
83
  import { CodemationPluginRegistrar } from "../infrastructure/config/CodemationPluginRegistrar";
@@ -222,6 +224,7 @@ export class AppContainerFactory {
222
224
  ListCredentialTypesQueryHandler,
223
225
  ListUserAccountsQueryHandler,
224
226
  VerifyUserInviteQueryHandler,
227
+ GetIterationCostQueryHandler,
225
228
  GetRunBinaryAttachmentQueryHandler,
226
229
  GetTelemetryDashboardDimensionsQueryHandler,
227
230
  GetTelemetryDashboardRunsQueryHandler,
@@ -442,6 +445,8 @@ export class AppContainerFactory {
442
445
  }),
443
446
  });
444
447
  container.registerSingleton(PrismaClientFactory, PrismaClientFactory);
448
+ container.registerSingleton(RunIterationProjectionFactory, RunIterationProjectionFactory);
449
+ container.registerSingleton(GetIterationCostQueryHandler, GetIterationCostQueryHandler);
445
450
  container.registerSingleton(WorkflowPolicyUiPresentationFactory, WorkflowPolicyUiPresentationFactory);
446
451
  container.registerSingleton(WorkflowDefinitionMapper, WorkflowDefinitionMapper);
447
452
  container.registerSingleton(RequestToWebhookItemMapper, RequestToWebhookItemMapper);
@@ -40,6 +40,9 @@ export interface TelemetrySpanRecord {
40
40
  readonly attributes?: TelemetryAttributes;
41
41
  readonly events?: ReadonlyArray<TelemetrySpanEventRecord>;
42
42
  readonly retentionExpiresAt?: string;
43
+ readonly iterationId?: string;
44
+ readonly itemIndex?: number;
45
+ readonly parentInvocationId?: string;
43
46
  }
44
47
 
45
48
  export interface TelemetrySpanUpsert {
@@ -64,6 +67,9 @@ export interface TelemetrySpanUpsert {
64
67
  readonly attributes?: TelemetryAttributes;
65
68
  readonly events?: ReadonlyArray<TelemetrySpanEventRecord>;
66
69
  readonly retentionExpiresAt?: string;
70
+ readonly iterationId?: string;
71
+ readonly itemIndex?: number;
72
+ readonly parentInvocationId?: string;
67
73
  }
68
74
 
69
75
  export interface TelemetryArtifactRecord {
@@ -115,6 +121,9 @@ export interface TelemetryMetricPointRecord {
115
121
  readonly modelName?: string;
116
122
  readonly dimensions?: TelemetryAttributes;
117
123
  readonly retentionExpiresAt?: string;
124
+ readonly iterationId?: string;
125
+ readonly itemIndex?: number;
126
+ readonly parentInvocationId?: string;
118
127
  }
119
128
 
120
129
  export interface TelemetryMetricPointWrite extends TelemetryMetricRecord {
@@ -130,6 +139,9 @@ export interface TelemetryMetricPointWrite extends TelemetryMetricRecord {
130
139
  readonly nodeRole?: string;
131
140
  readonly modelName?: string;
132
141
  readonly retentionExpiresAt?: string;
142
+ readonly iterationId?: string;
143
+ readonly itemIndex?: number;
144
+ readonly parentInvocationId?: string;
133
145
  }
134
146
 
135
147
  export interface TelemetrySpanListQuery {
@@ -32,6 +32,9 @@ export class InMemoryTelemetryMetricPointStore implements TelemetryMetricPointSt
32
32
  modelName: record.modelName,
33
33
  dimensions: record.attributes,
34
34
  retentionExpiresAt: record.retentionExpiresAt,
35
+ iterationId: record.iterationId,
36
+ itemIndex: record.itemIndex,
37
+ parentInvocationId: record.parentInvocationId,
35
38
  };
36
39
  this.rows.set(created.metricPointId, created);
37
40
  return created;
@@ -63,6 +63,9 @@ export class InMemoryTelemetrySpanStore implements TelemetrySpanStore {
63
63
  },
64
64
  events: [...(existing?.events ?? []), ...(update.events ?? [])],
65
65
  retentionExpiresAt: update.retentionExpiresAt ?? existing?.retentionExpiresAt,
66
+ iterationId: update.iterationId ?? existing?.iterationId,
67
+ itemIndex: update.itemIndex ?? existing?.itemIndex,
68
+ parentInvocationId: update.parentInvocationId ?? existing?.parentInvocationId,
66
69
  };
67
70
  }
68
71
 
@@ -121,6 +121,29 @@ export class InMemoryWorkflowRunRepository implements WorkflowRunRepository, Wor
121
121
  error: snapshot.error,
122
122
  }),
123
123
  );
124
+ const invocationInstances: ExecutionInstanceDto[] = (state.connectionInvocations ?? []).map(
125
+ (invocation, index) => ({
126
+ instanceId: invocation.invocationId,
127
+ slotNodeId: invocation.connectionNodeId,
128
+ workflowNodeId: invocation.parentAgentNodeId,
129
+ kind: "connectionInvocation",
130
+ runIndex: index,
131
+ batchId: state.pending?.batchId ?? "batch_1",
132
+ activationId: invocation.parentAgentActivationId,
133
+ status: invocation.status,
134
+ queuedAt: invocation.queuedAt,
135
+ startedAt: invocation.startedAt,
136
+ finishedAt: invocation.finishedAt,
137
+ itemCount: 0,
138
+ inputJson: invocation.managedInput as never,
139
+ outputJson: invocation.managedOutput as never,
140
+ error: invocation.error,
141
+ iterationId: invocation.iterationId,
142
+ itemIndex: invocation.itemIndex,
143
+ parentInvocationId: invocation.parentInvocationId,
144
+ }),
145
+ );
146
+ executionInstances.push(...invocationInstances);
124
147
  return {
125
148
  runId: state.runId,
126
149
  workflowId: state.workflowId,
@@ -38,6 +38,9 @@ export class PrismaTelemetryMetricPointStore implements TelemetryMetricPointStor
38
38
  modelName: record.modelName ?? null,
39
39
  dimensionsJson: record.attributes ? JSON.stringify(record.attributes) : null,
40
40
  retentionExpiresAt: record.retentionExpiresAt ?? null,
41
+ iterationId: record.iterationId ?? null,
42
+ itemIndex: record.itemIndex ?? null,
43
+ parentInvocationId: record.parentInvocationId ?? null,
41
44
  },
42
45
  });
43
46
  return {
@@ -58,6 +61,9 @@ export class PrismaTelemetryMetricPointStore implements TelemetryMetricPointStor
58
61
  modelName: record.modelName,
59
62
  dimensions: record.attributes,
60
63
  retentionExpiresAt: record.retentionExpiresAt,
64
+ iterationId: record.iterationId,
65
+ itemIndex: record.itemIndex,
66
+ parentInvocationId: record.parentInvocationId,
61
67
  };
62
68
  }
63
69
 
@@ -96,6 +102,9 @@ export class PrismaTelemetryMetricPointStore implements TelemetryMetricPointStor
96
102
  modelName: row.modelName ?? undefined,
97
103
  dimensions: this.parseJson<TelemetryAttributes>(row.dimensionsJson),
98
104
  retentionExpiresAt: row.retentionExpiresAt ?? undefined,
105
+ iterationId: row.iterationId ?? undefined,
106
+ itemIndex: row.itemIndex ?? undefined,
107
+ parentInvocationId: row.parentInvocationId ?? undefined,
99
108
  }));
100
109
  }
101
110
 
@@ -49,6 +49,9 @@ export class PrismaTelemetrySpanStore implements TelemetrySpanStore {
49
49
  attributesJson: attributes ? JSON.stringify(attributes) : null,
50
50
  eventsJson: events.length > 0 ? JSON.stringify(events) : null,
51
51
  retentionExpiresAt: record.retentionExpiresAt ?? existing?.retentionExpiresAt ?? null,
52
+ iterationId: record.iterationId ?? existing?.iterationId ?? null,
53
+ itemIndex: record.itemIndex ?? existing?.itemIndex ?? null,
54
+ parentInvocationId: record.parentInvocationId ?? existing?.parentInvocationId ?? null,
52
55
  updatedAt: new Date().toISOString(),
53
56
  };
54
57
  await this.prisma.telemetrySpan.upsert({
@@ -100,6 +103,9 @@ export class PrismaTelemetrySpanStore implements TelemetrySpanStore {
100
103
  attributes: this.parseJson<TelemetryAttributes>(row.attributesJson),
101
104
  events: this.parseJson<ReadonlyArray<TelemetrySpanEventRecord>>(row.eventsJson) ?? [],
102
105
  retentionExpiresAt: row.retentionExpiresAt ?? undefined,
106
+ iterationId: row.iterationId ?? undefined,
107
+ itemIndex: row.itemIndex ?? undefined,
108
+ parentInvocationId: row.parentInvocationId ?? undefined,
103
109
  }));
104
110
  }
105
111