@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.
- package/CHANGELOG.md +24 -0
- package/dist/{AppConfigFactory-BPp02HMv.js → AppConfigFactory-C4OXGOs2.js} +16 -7
- package/dist/{AppConfigFactory-BPp02HMv.js.map → AppConfigFactory-C4OXGOs2.js.map} +1 -1
- package/dist/{AppConfigFactory-PFmDg5Sg.d.ts → AppConfigFactory-D3k-R3Ch.d.ts} +338 -6
- package/dist/{AppContainerFactory-Cr3JeVmg.js → AppContainerFactory-CKRDz8kQ.js} +409 -92
- package/dist/AppContainerFactory-CKRDz8kQ.js.map +1 -0
- package/dist/{CodemationAppContext-DP_-56c6.d.ts → CodemationAppContext-YgJRUHWF.d.ts} +2 -2
- package/dist/{CodemationAuthoring.types-Cr2QZsUX.d.ts → CodemationAuthoring.types-lUdxXYq-.d.ts} +3 -3
- package/dist/{CodemationConfigNormalizer-B8RGUwAe.d.ts → CodemationConfigNormalizer-BWBp7mFB.d.ts} +2 -2
- package/dist/{CodemationConsumerConfigLoader-C_QVwcI3.d.ts → CodemationConsumerConfigLoader-Bka3v6lh.d.ts} +2 -2
- package/dist/{CodemationPluginListMerger-Bgn1CIX9.d.ts → CodemationPluginListMerger-Oz-GAkxz.d.ts} +17 -5
- package/dist/{CredentialServices-95DPogx-.d.ts → CredentialServices-CKXPg5xu.d.ts} +3 -3
- package/dist/{PublicFrontendBootstrapFactory-C_iLgPV-.d.ts → PublicFrontendBootstrapFactory-DkQoSYDo.d.ts} +2 -2
- package/dist/authoring.d.ts +3 -3
- package/dist/consumer.d.ts +4 -4
- package/dist/credentials.d.ts +3 -3
- package/dist/devServerSidecar.d.ts +1 -1
- package/dist/{index-W4eSjdCM.d.ts → index-BxIc_L4D.d.ts} +233 -151
- package/dist/index.d.ts +11 -11
- package/dist/index.js +4 -4
- package/dist/nextServer.d.ts +7 -7
- package/dist/nextServer.js +2 -2
- package/dist/{persistenceServer-_pqP_0nw.d.ts → persistenceServer-BLG7_6B5.d.ts} +2 -2
- package/dist/{persistenceServer-CA0_q0D7.js → persistenceServer-KyHL0u01.js} +2 -2
- package/dist/{persistenceServer-CA0_q0D7.js.map → persistenceServer-KyHL0u01.js.map} +1 -1
- package/dist/persistenceServer.d.ts +5 -5
- package/dist/persistenceServer.js +2 -2
- package/dist/{server-Q5uwa6iR.d.ts → server-B0SD6Nvk.d.ts} +5 -5
- package/dist/{server-BE4PLhcb.js → server-CMUVhYIc.js} +3 -3
- package/dist/{server-BE4PLhcb.js.map → server-CMUVhYIc.js.map} +1 -1
- package/dist/server.d.ts +8 -8
- package/dist/server.js +4 -4
- package/package.json +5 -5
- package/prisma/migrations/20260430120000_telemetry_iteration_identity/migration.sql +17 -0
- package/prisma/migrations/20260430130000_execution_instance_iteration_identity/migration.sql +11 -0
- package/prisma/migrations.sqlite/20260430120000_telemetry_iteration_identity/migration.sql +14 -0
- package/prisma/migrations.sqlite/20260430130000_execution_instance_iteration_identity/migration.sql +10 -0
- package/prisma/schema.postgresql.prisma +12 -0
- package/prisma/schema.sqlite.prisma +12 -0
- package/src/application/contracts/IterationCostContracts.ts +11 -0
- package/src/application/queries/GetIterationCostQuery.ts +14 -0
- package/src/application/queries/GetIterationCostQueryHandler.ts +92 -0
- package/src/application/queries/GetWorkflowRunDetailQueryHandler.ts +44 -2
- package/src/application/queries/RunIterationProjectionFactory.ts +123 -0
- package/src/application/queries/WorkflowQueryHandlers.ts +1 -0
- package/src/application/telemetry/OtelExecutionTelemetry.types.ts +3 -0
- package/src/application/telemetry/RunEventBusTelemetryReporter.ts +7 -0
- package/src/application/telemetry/StoredNodeExecutionTelemetry.ts +14 -0
- package/src/application/telemetry/StoredTelemetrySpanScope.ts +90 -1
- package/src/bootstrap/AppContainerFactory.ts +5 -0
- package/src/domain/telemetry/TelemetryContracts.ts +12 -0
- package/src/infrastructure/persistence/InMemoryTelemetryMetricPointStore.ts +3 -0
- package/src/infrastructure/persistence/InMemoryTelemetrySpanStore.ts +3 -0
- package/src/infrastructure/persistence/InMemoryWorkflowRunRepository.ts +23 -0
- package/src/infrastructure/persistence/PrismaTelemetryMetricPointStore.ts +9 -0
- package/src/infrastructure/persistence/PrismaTelemetrySpanStore.ts +6 -0
- package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +12 -0
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/edge.js +15 -6
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/index-browser.js +11 -2
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.d.ts +343 -5
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.js +15 -6
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/package.json +1 -1
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/schema.prisma +12 -0
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/edge.js +15 -6
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/index-browser.js +11 -2
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.d.ts +343 -5
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.js +15 -6
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/package.json +1 -1
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/schema.prisma +12 -0
- 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
|
-
|
|
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";
|
|
@@ -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
|
|