@zhivex-ai/core 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/dist/agent-handoff-contracts.d.ts +12 -0
- package/dist/agent-handoff-contracts.d.ts.map +1 -0
- package/dist/agent-handoff-contracts.js +18 -0
- package/dist/agent-handoff-contracts.js.map +1 -0
- package/dist/agent-handoff.d.ts +2 -11
- package/dist/agent-handoff.d.ts.map +1 -1
- package/dist/agent-handoff.js +1 -18
- package/dist/agent-handoff.js.map +1 -1
- package/dist/agent-trace.d.ts +1 -5
- package/dist/agent-trace.d.ts.map +1 -1
- package/dist/agent-trace.js.map +1 -1
- package/dist/agent.d.ts +1 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +358 -215
- package/dist/agent.js.map +1 -1
- package/dist/api-stability.d.ts.map +1 -1
- package/dist/api-stability.js +36 -25
- package/dist/api-stability.js.map +1 -1
- package/dist/artifact.d.ts +32 -9
- package/dist/artifact.d.ts.map +1 -1
- package/dist/artifact.js +428 -82
- package/dist/artifact.js.map +1 -1
- package/dist/catalog.d.ts +42 -3
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +269 -7
- package/dist/catalog.js.map +1 -1
- package/dist/contracts.d.ts +8 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +2 -0
- package/dist/contracts.js.map +1 -0
- package/dist/generate-text.d.ts.map +1 -1
- package/dist/generate-text.js +6 -3
- package/dist/generate-text.js.map +1 -1
- package/dist/google.d.ts +1 -60
- package/dist/google.d.ts.map +1 -1
- package/dist/google.js +0 -198
- package/dist/google.js.map +1 -1
- package/dist/index.d.ts +13 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/live-agent.js +1 -1
- package/dist/live-agent.js.map +1 -1
- package/dist/middleware.d.ts +16 -0
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +476 -110
- package/dist/middleware.js.map +1 -1
- package/dist/model-capabilities.d.ts +66 -0
- package/dist/model-capabilities.d.ts.map +1 -0
- package/dist/model-capabilities.js +136 -0
- package/dist/model-capabilities.js.map +1 -0
- package/dist/node.d.ts +8 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +8 -0
- package/dist/node.js.map +1 -0
- package/dist/observability.d.ts +57 -10
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +535 -114
- package/dist/observability.js.map +1 -1
- package/dist/pricing.d.ts +7 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +2 -0
- package/dist/pricing.js.map +1 -0
- package/dist/provider-resources.d.ts +61 -0
- package/dist/provider-resources.d.ts.map +1 -0
- package/dist/provider-resources.js +199 -0
- package/dist/provider-resources.js.map +1 -0
- package/dist/runner.js +1 -1
- package/dist/runner.js.map +1 -1
- package/dist/runtime-entry.d.ts +15 -0
- package/dist/runtime-entry.d.ts.map +1 -0
- package/dist/runtime-entry.js +14 -0
- package/dist/runtime-entry.js.map +1 -0
- package/dist/runtime.d.ts +9 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +35 -8
- package/dist/runtime.js.map +1 -1
- package/dist/secure-id-node.d.ts +2 -0
- package/dist/secure-id-node.d.ts.map +1 -0
- package/dist/secure-id-node.js +3 -0
- package/dist/secure-id-node.js.map +1 -0
- package/dist/secure-id.d.ts.map +1 -1
- package/dist/secure-id.js +6 -2
- package/dist/secure-id.js.map +1 -1
- package/dist/testing.d.ts +4 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +4 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +74 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/ui-entry.d.ts +6 -0
- package/dist/ui-entry.d.ts.map +1 -0
- package/dist/ui-entry.js +5 -0
- package/dist/ui-entry.js.map +1 -0
- package/dist/ui.js +1 -1
- package/dist/ui.js.map +1 -1
- package/dist/workflow-observability.d.ts +13 -0
- package/dist/workflow-observability.d.ts.map +1 -0
- package/dist/workflow-observability.js +186 -0
- package/dist/workflow-observability.js.map +1 -0
- package/dist/workflow-state-contracts.d.ts +76 -0
- package/dist/workflow-state-contracts.d.ts.map +1 -0
- package/dist/workflow-state-contracts.js +49 -0
- package/dist/workflow-state-contracts.js.map +1 -0
- package/dist/workflow-state-service.d.ts +3 -34
- package/dist/workflow-state-service.d.ts.map +1 -1
- package/dist/workflow-state-service.js +2 -2
- package/dist/workflow-state-service.js.map +1 -1
- package/dist/workflow.d.ts +53 -42
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +258 -125
- package/dist/workflow.js.map +1 -1
- package/dist/workflows-entry.d.ts +9 -0
- package/dist/workflows-entry.d.ts.map +1 -0
- package/dist/workflows-entry.js +5 -0
- package/dist/workflows-entry.js.map +1 -0
- package/package.json +42 -1
- package/secure-id-internal.d.ts +1 -0
package/dist/observability.js
CHANGED
|
@@ -1,137 +1,445 @@
|
|
|
1
1
|
import { createTelemetryMiddleware } from "./middleware.js";
|
|
2
|
+
/** Zhivex's stable, privacy-first mapping contract for GenAI telemetry. */
|
|
3
|
+
export const OTEL_GENAI_CONTRACT_VERSION = 1;
|
|
4
|
+
/** Upstream Development revision used to audit the v1 mapping. */
|
|
5
|
+
export const OTEL_GENAI_SEMCONV_REVISION = "a685613a207a580163353b8e48a7ad88967e7b42";
|
|
2
6
|
const OTEL_API_MODULE = "@opentelemetry/api";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
const GEN_AI = {
|
|
8
|
+
agentName: "gen_ai.agent.name",
|
|
9
|
+
operationName: "gen_ai.operation.name",
|
|
10
|
+
providerName: "gen_ai.provider.name",
|
|
11
|
+
requestMaxTokens: "gen_ai.request.max_tokens",
|
|
12
|
+
requestModel: "gen_ai.request.model",
|
|
13
|
+
requestReasoningLevel: "gen_ai.request.reasoning.level",
|
|
14
|
+
requestStream: "gen_ai.request.stream",
|
|
15
|
+
requestTemperature: "gen_ai.request.temperature",
|
|
16
|
+
responseFinishReasons: "gen_ai.response.finish_reasons",
|
|
17
|
+
responseTimeToFirstChunk: "gen_ai.response.time_to_first_chunk",
|
|
18
|
+
tokenType: "gen_ai.token.type",
|
|
19
|
+
toolCallId: "gen_ai.tool.call.id",
|
|
20
|
+
toolName: "gen_ai.tool.name",
|
|
21
|
+
toolType: "gen_ai.tool.type",
|
|
22
|
+
usageCacheCreationInputTokens: "gen_ai.usage.cache_creation.input_tokens",
|
|
23
|
+
usageCacheReadInputTokens: "gen_ai.usage.cache_read.input_tokens",
|
|
24
|
+
usageInputTokens: "gen_ai.usage.input_tokens",
|
|
25
|
+
usageOutputTokens: "gen_ai.usage.output_tokens",
|
|
26
|
+
usageReasoningOutputTokens: "gen_ai.usage.reasoning.output_tokens"
|
|
27
|
+
};
|
|
28
|
+
const GEN_AI_OPERATION = {
|
|
29
|
+
chat: "chat",
|
|
30
|
+
executeTool: "execute_tool",
|
|
31
|
+
generateContent: "generate_content",
|
|
32
|
+
invokeAgent: "invoke_agent"
|
|
33
|
+
};
|
|
34
|
+
const PROVIDER_NAMES = {
|
|
35
|
+
"azure-openai": "azure.ai.openai",
|
|
36
|
+
bedrock: "aws.bedrock",
|
|
37
|
+
gemini: "gcp.gemini",
|
|
38
|
+
google: "gcp.gen_ai",
|
|
39
|
+
kimi: "moonshot_ai",
|
|
40
|
+
vertex: "gcp.vertex_ai",
|
|
41
|
+
xai: "x_ai"
|
|
11
42
|
};
|
|
12
|
-
const
|
|
13
|
-
|
|
43
|
+
const DURATION_BUCKETS = [
|
|
44
|
+
0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.56, 5.12, 10.24, 20.48, 40.96, 81.92
|
|
45
|
+
];
|
|
46
|
+
const TOKEN_BUCKETS = [
|
|
47
|
+
1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864
|
|
48
|
+
];
|
|
49
|
+
const AGENT_DURATION_BUCKETS = [0.1, 0.2, 0.4, 0.8, 1.6, 3.2, 6.4, 12.8, 25.6, 51.2, 102.4, 204.8, 409.6];
|
|
50
|
+
const CALL_COUNT_BUCKETS = [1, 2, 4, 8, 16, 32, 64, 128];
|
|
51
|
+
let optionalOtelApiPromise;
|
|
52
|
+
// OpenTelemetry stays optional for consumers that never instantiate these helpers.
|
|
53
|
+
const loadOptionalOtelApi = () => {
|
|
54
|
+
optionalOtelApiPromise ??= import(OTEL_API_MODULE).catch(() => undefined);
|
|
55
|
+
return optionalOtelApiPromise;
|
|
56
|
+
};
|
|
57
|
+
const toProviderName = (provider) => PROVIDER_NAMES[provider] ?? provider;
|
|
58
|
+
const toModelOperationName = (provider) => provider === "gemini" || provider === "google" || provider === "vertex"
|
|
59
|
+
? GEN_AI_OPERATION.generateContent
|
|
60
|
+
: GEN_AI_OPERATION.chat;
|
|
61
|
+
const toAttributeValue = (value) => {
|
|
62
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
14
63
|
return value;
|
|
15
|
-
}
|
|
16
64
|
if (Array.isArray(value)) {
|
|
17
|
-
const normalized = value
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
65
|
+
const normalized = value.filter((item) => item !== null && item !== undefined);
|
|
66
|
+
if (!normalized.length)
|
|
67
|
+
return undefined;
|
|
68
|
+
if (normalized.every((item) => typeof item === "string"))
|
|
69
|
+
return normalized;
|
|
70
|
+
if (normalized.every((item) => typeof item === "number"))
|
|
71
|
+
return normalized;
|
|
72
|
+
if (normalized.every((item) => typeof item === "boolean"))
|
|
73
|
+
return normalized;
|
|
74
|
+
return normalized.map((item) => {
|
|
75
|
+
if (typeof item === "string")
|
|
76
|
+
return item;
|
|
77
|
+
try {
|
|
78
|
+
return JSON.stringify(item) ?? String(item);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return String(item);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
21
84
|
}
|
|
22
|
-
if (value === undefined)
|
|
85
|
+
if (value === null || value === undefined)
|
|
23
86
|
return undefined;
|
|
24
|
-
}
|
|
25
87
|
try {
|
|
26
|
-
return JSON.stringify(value);
|
|
88
|
+
return JSON.stringify(value) ?? String(value);
|
|
27
89
|
}
|
|
28
90
|
catch {
|
|
29
91
|
return String(value);
|
|
30
92
|
}
|
|
31
93
|
};
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.filter((entry) => entry[1] !== undefined);
|
|
36
|
-
return Object.fromEntries(entries);
|
|
37
|
-
};
|
|
94
|
+
const toAttributes = (attributes) => Object.fromEntries(Object.entries(attributes ?? {})
|
|
95
|
+
.map(([key, value]) => [key, toAttributeValue(value)])
|
|
96
|
+
.filter((entry) => entry[1] !== undefined));
|
|
38
97
|
const safeSetAttributes = (span, attributes) => {
|
|
39
|
-
for (const [key, value] of Object.entries(
|
|
98
|
+
for (const [key, value] of Object.entries(toAttributes(attributes)))
|
|
40
99
|
span.setAttribute?.(key, value);
|
|
41
|
-
}
|
|
42
100
|
};
|
|
43
101
|
const safeAddEvent = (span, name, attributes) => {
|
|
44
|
-
|
|
102
|
+
span?.addEvent?.(name, toAttributes(attributes));
|
|
103
|
+
};
|
|
104
|
+
const GUARDRAIL_METADATA_ALLOWLIST = {
|
|
105
|
+
actual: "actual",
|
|
106
|
+
budgetLimit: "budget_limit",
|
|
107
|
+
includeChildRuns: "include_child_runs",
|
|
108
|
+
limit: "limit",
|
|
109
|
+
operation: "operation",
|
|
110
|
+
remaining: "remaining",
|
|
111
|
+
required: "required"
|
|
112
|
+
};
|
|
113
|
+
const truncateTelemetryString = (value, maxLength) => value.length <= maxLength ? value : `${value.slice(0, maxLength)}…`;
|
|
114
|
+
const safeGuardrailMetadataAttributes = (metadata) => {
|
|
115
|
+
const attributes = {};
|
|
116
|
+
for (const [sourceKey, attributeKey] of Object.entries(GUARDRAIL_METADATA_ALLOWLIST)) {
|
|
117
|
+
const value = metadata?.[sourceKey];
|
|
118
|
+
if (typeof value === "string") {
|
|
119
|
+
attributes[`zhivex.guardrail.metadata.${attributeKey}`] = truncateTelemetryString(value, 128);
|
|
120
|
+
}
|
|
121
|
+
else if (typeof value === "number" && Number.isFinite(value)) {
|
|
122
|
+
attributes[`zhivex.guardrail.metadata.${attributeKey}`] = value;
|
|
123
|
+
}
|
|
124
|
+
else if (typeof value === "boolean") {
|
|
125
|
+
attributes[`zhivex.guardrail.metadata.${attributeKey}`] = value;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return attributes;
|
|
129
|
+
};
|
|
130
|
+
const errorType = (error) => error.name || error.constructor?.name || "_OTHER";
|
|
131
|
+
const seconds = (milliseconds) => Math.max(0, milliseconds) / 1_000;
|
|
132
|
+
const usageAttributes = (usage) => ({
|
|
133
|
+
[GEN_AI.usageInputTokens]: usage?.inputTokens,
|
|
134
|
+
[GEN_AI.usageOutputTokens]: usage?.outputTokens,
|
|
135
|
+
[GEN_AI.usageCacheReadInputTokens]: usage?.cachedInputTokens,
|
|
136
|
+
[GEN_AI.usageCacheCreationInputTokens]: usage?.cacheWriteTokens,
|
|
137
|
+
[GEN_AI.usageReasoningOutputTokens]: usage?.reasoningTokens
|
|
138
|
+
});
|
|
139
|
+
const isStreamTelemetry = (event) => event.type.startsWith("stream-");
|
|
140
|
+
const requestAttributes = (event) => ({
|
|
141
|
+
[GEN_AI.operationName]: toModelOperationName(event.model.provider),
|
|
142
|
+
[GEN_AI.providerName]: toProviderName(event.model.provider),
|
|
143
|
+
[GEN_AI.requestModel]: event.model.modelId,
|
|
144
|
+
[GEN_AI.requestStream]: isStreamTelemetry(event) ? true : undefined,
|
|
145
|
+
[GEN_AI.requestTemperature]: event.input.temperature,
|
|
146
|
+
[GEN_AI.requestMaxTokens]: event.input.maxTokens,
|
|
147
|
+
[GEN_AI.requestReasoningLevel]: event.input.reasoning?.effort,
|
|
148
|
+
"zhivex.provider": event.model.provider,
|
|
149
|
+
"zhivex.model_id": event.model.modelId
|
|
150
|
+
});
|
|
151
|
+
const clientMetricAttributes = (event, error) => ({
|
|
152
|
+
[GEN_AI.operationName]: toModelOperationName(event.model.provider),
|
|
153
|
+
[GEN_AI.providerName]: toProviderName(event.model.provider),
|
|
154
|
+
[GEN_AI.requestModel]: event.model.modelId,
|
|
155
|
+
...(error ? { "error.type": errorType(error) } : {})
|
|
156
|
+
});
|
|
157
|
+
const responseAttributes = (options) => ({
|
|
158
|
+
[GEN_AI.responseFinishReasons]: options.finishReason ? [options.finishReason] : undefined,
|
|
159
|
+
...usageAttributes(options.usage),
|
|
160
|
+
"zhivex.finish_reason": options.finishReason,
|
|
161
|
+
"zhivex.provider_finish_reason": options.providerFinishReason,
|
|
162
|
+
"zhivex.output_chunk_count": options.outputChunkCount,
|
|
163
|
+
"zhivex.latency_ms": options.latencyMs
|
|
164
|
+
});
|
|
165
|
+
const histogram = (meter, name, unit, description, explicitBucketBoundaries) => meter.createHistogram(name, {
|
|
166
|
+
unit,
|
|
167
|
+
description,
|
|
168
|
+
advice: { explicitBucketBoundaries: [...explicitBucketBoundaries] }
|
|
169
|
+
});
|
|
170
|
+
const createGenAiMetrics = (meter) => ({
|
|
171
|
+
clientTokenUsage: histogram(meter, "gen_ai.client.token.usage", "{token}", "Number of input and output tokens used.", TOKEN_BUCKETS),
|
|
172
|
+
clientOperationDuration: histogram(meter, "gen_ai.client.operation.duration", "s", "GenAI operation duration.", DURATION_BUCKETS),
|
|
173
|
+
clientTimeToFirstChunk: histogram(meter, "gen_ai.client.operation.time_to_first_chunk", "s", "Time to receive the first chunk, measured from when the client issues the generation request to when the first chunk is received in the response stream.", DURATION_BUCKETS),
|
|
174
|
+
clientTimePerOutputChunk: histogram(meter, "gen_ai.client.operation.time_per_output_chunk", "s", "Time per output chunk, recorded for each chunk received after the first one, measured as the time elapsed from the end of the previous chunk to the end of the current chunk.", DURATION_BUCKETS),
|
|
175
|
+
invokeAgentDuration: histogram(meter, "gen_ai.invoke_agent.duration", "s", "The end-to-end duration of a single in-process agent invocation, from the moment the invocation starts until the agent emits the last chunk of its final response or terminates with an error.", AGENT_DURATION_BUCKETS),
|
|
176
|
+
invokeAgentInferenceCalls: histogram(meter, "gen_ai.invoke_agent.inference_calls", "{inference_call}", "The number of inference (model) calls a GenAI agent makes during a single invocation.", CALL_COUNT_BUCKETS),
|
|
177
|
+
invokeAgentToolCalls: histogram(meter, "gen_ai.invoke_agent.tool_calls", "{tool_call}", "The number of tool calls an agent makes during a single invocation.", CALL_COUNT_BUCKETS),
|
|
178
|
+
executeToolDuration: histogram(meter, "gen_ai.execute_tool.duration", "s", "The duration of a single tool execution.", DURATION_BUCKETS)
|
|
179
|
+
});
|
|
180
|
+
const safeRecord = (instrument, value, attributes) => {
|
|
181
|
+
if (value === undefined || !Number.isFinite(value) || value < 0)
|
|
45
182
|
return;
|
|
183
|
+
try {
|
|
184
|
+
instrument?.record(value, toAttributes(attributes));
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Exporters are observational and cannot own the instrumented operation outcome.
|
|
46
188
|
}
|
|
47
|
-
|
|
189
|
+
};
|
|
190
|
+
const recordTokenUsage = (metrics, usage, attributes) => {
|
|
191
|
+
safeRecord(metrics?.clientTokenUsage, usage?.inputTokens, { ...attributes, [GEN_AI.tokenType]: "input" });
|
|
192
|
+
safeRecord(metrics?.clientTokenUsage, usage?.outputTokens, { ...attributes, [GEN_AI.tokenType]: "output" });
|
|
193
|
+
};
|
|
194
|
+
const resolveMeter = async (options) => {
|
|
195
|
+
if (options.meter)
|
|
196
|
+
return options.meter;
|
|
197
|
+
if (options.meterProvider) {
|
|
198
|
+
return options.meterProvider.getMeter(options.meterName ?? options.tracerName ?? "zhivex-ai", options.version);
|
|
199
|
+
}
|
|
200
|
+
const otel = await loadOptionalOtelApi();
|
|
201
|
+
return otel?.metrics.getMeter(options.meterName ?? options.tracerName ?? "zhivex-ai", options.version);
|
|
48
202
|
};
|
|
49
203
|
export class OTelSpanHandle {
|
|
50
204
|
span;
|
|
51
205
|
statusSetter;
|
|
206
|
+
endPromise;
|
|
52
207
|
constructor(span, statusSetter) {
|
|
53
208
|
this.span = span;
|
|
54
209
|
this.statusSetter = statusSetter;
|
|
55
210
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
211
|
+
get ended() {
|
|
212
|
+
return this.endPromise !== undefined;
|
|
213
|
+
}
|
|
214
|
+
end(options = {}) {
|
|
215
|
+
this.endPromise ??= this.finish(options);
|
|
216
|
+
return this.endPromise;
|
|
217
|
+
}
|
|
218
|
+
async finish(options) {
|
|
219
|
+
try {
|
|
220
|
+
safeSetAttributes(this.span, {
|
|
221
|
+
...(options.error ? { "error.type": errorType(options.error) } : {}),
|
|
222
|
+
...options.attributes
|
|
223
|
+
});
|
|
224
|
+
if (options.error) {
|
|
225
|
+
this.span.recordException?.(options.error);
|
|
226
|
+
await this.statusSetter?.(this.span, options.error);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
this.span.end(options.endTime);
|
|
61
231
|
}
|
|
62
|
-
this.span.end();
|
|
63
232
|
}
|
|
64
233
|
}
|
|
65
234
|
export class OTelObserver {
|
|
66
235
|
tracer;
|
|
67
236
|
statusSetter;
|
|
68
|
-
|
|
237
|
+
contextFor;
|
|
238
|
+
spanKindFor;
|
|
239
|
+
runWithSpan;
|
|
240
|
+
constructor(tracer, statusSetter, contextFor, spanKindFor, runWithSpan) {
|
|
69
241
|
this.tracer = tracer;
|
|
70
242
|
this.statusSetter = statusSetter;
|
|
243
|
+
this.contextFor = contextFor;
|
|
244
|
+
this.spanKindFor = spanKindFor;
|
|
245
|
+
this.runWithSpan = runWithSpan;
|
|
71
246
|
}
|
|
72
|
-
startSpan(name, attributes) {
|
|
247
|
+
startSpan(name, attributes, options = {}) {
|
|
73
248
|
const span = this.tracer.startSpan(name, {
|
|
74
|
-
attributes:
|
|
75
|
-
|
|
249
|
+
attributes: toAttributes(attributes),
|
|
250
|
+
kind: options.kind ? this.spanKindFor?.(options.kind) : undefined,
|
|
251
|
+
startTime: options.startTime
|
|
252
|
+
}, this.contextFor?.(options.parent?.span));
|
|
76
253
|
return new OTelSpanHandle(span, this.statusSetter);
|
|
77
254
|
}
|
|
255
|
+
async withSpan(handle, callback) {
|
|
256
|
+
if (!this.runWithSpan)
|
|
257
|
+
return callback();
|
|
258
|
+
return this.runWithSpan(handle.span, callback);
|
|
259
|
+
}
|
|
78
260
|
}
|
|
79
|
-
const createDefaultStatusSetter =
|
|
80
|
-
|
|
81
|
-
if (!otel) {
|
|
261
|
+
const createDefaultStatusSetter = (otel) => {
|
|
262
|
+
if (!otel)
|
|
82
263
|
return undefined;
|
|
83
|
-
}
|
|
84
264
|
return (span, error) => {
|
|
85
|
-
span.setStatus?.({
|
|
86
|
-
code: otel.SpanStatusCode.ERROR,
|
|
87
|
-
message: error.message
|
|
88
|
-
});
|
|
265
|
+
span.setStatus?.({ code: otel.SpanStatusCode.ERROR, message: error.message });
|
|
89
266
|
};
|
|
90
267
|
};
|
|
91
268
|
export const createOtelObserver = async (options = {}) => {
|
|
92
|
-
const statusSetter = await createDefaultStatusSetter();
|
|
93
|
-
if (options.tracer) {
|
|
94
|
-
return new OTelObserver(options.tracer, statusSetter);
|
|
95
|
-
}
|
|
96
269
|
const otel = await loadOptionalOtelApi();
|
|
97
|
-
if (!otel) {
|
|
270
|
+
if (!options.tracer && !options.tracerProvider && !otel) {
|
|
98
271
|
throw new Error('OpenTelemetry is not installed. Install "@opentelemetry/api" to use OTEL observability helpers.', { cause: new Error(`Missing optional dependency: ${OTEL_API_MODULE}`) });
|
|
99
272
|
}
|
|
100
|
-
|
|
273
|
+
const tracer = options.tracer
|
|
274
|
+
?? options.tracerProvider?.getTracer(options.tracerName ?? "zhivex-ai", options.version)
|
|
275
|
+
?? otel.trace.getTracer(options.tracerName ?? "zhivex-ai", options.version);
|
|
276
|
+
return new OTelObserver(tracer, createDefaultStatusSetter(otel), otel ? (parent) => parent ? otel.trace.setSpan(otel.context.active(), parent) : otel.context.active() : undefined, otel ? (kind) => kind === "client" ? otel.SpanKind.CLIENT : otel.SpanKind.INTERNAL : undefined, otel
|
|
277
|
+
? (span, callback) => otel.context.with(otel.trace.setSpan(otel.context.active(), span), callback)
|
|
278
|
+
: undefined);
|
|
279
|
+
};
|
|
280
|
+
const settleSpanEnds = async (handles, attributes, endTime) => {
|
|
281
|
+
const results = await Promise.allSettled(Array.from(handles, (handle) => handle.end({ attributes, endTime })));
|
|
282
|
+
const rejection = results.find((result) => result.status === "rejected");
|
|
283
|
+
if (rejection)
|
|
284
|
+
throw rejection.reason;
|
|
285
|
+
};
|
|
286
|
+
const agentStatusError = (event) => {
|
|
287
|
+
if (event.status === "failed")
|
|
288
|
+
return new Error(event.state.error?.message ?? "Agent run failed.");
|
|
289
|
+
if (event.status === "timed_out") {
|
|
290
|
+
return Object.assign(new Error("Agent run timed out."), { name: "TimeoutError" });
|
|
291
|
+
}
|
|
292
|
+
return undefined;
|
|
101
293
|
};
|
|
102
294
|
export const createOtelAgentObserver = async (options = {}) => {
|
|
103
|
-
const observer =
|
|
295
|
+
const [observer, meter] = await Promise.all([
|
|
296
|
+
options.observer ? Promise.resolve(options.observer) : createOtelObserver(options),
|
|
297
|
+
resolveMeter(options)
|
|
298
|
+
]);
|
|
299
|
+
const metrics = meter ? createGenAiMetrics(meter) : undefined;
|
|
104
300
|
const runSpans = new Map();
|
|
105
301
|
const stepSpans = new Map();
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
302
|
+
const metricStates = new Map();
|
|
303
|
+
const queues = new Map();
|
|
304
|
+
const prefix = options.spanNamePrefix?.trim();
|
|
305
|
+
const runSpanName = (agentName) => prefix ? `${prefix}.run` : `${GEN_AI_OPERATION.invokeAgent}${agentName ? ` ${agentName}` : ""}`;
|
|
306
|
+
const stepSpanName = () => prefix ? `${prefix}.step` : "agent_step";
|
|
307
|
+
const closeSteps = async (runId, attributes, endTime) => {
|
|
308
|
+
const spans = stepSpans.get(runId);
|
|
309
|
+
stepSpans.delete(runId);
|
|
310
|
+
if (spans)
|
|
311
|
+
await settleSpanEnds(spans.values(), attributes, endTime);
|
|
312
|
+
};
|
|
313
|
+
const recordAgentMetrics = (runId, endTime, error) => {
|
|
314
|
+
const state = metricStates.get(runId);
|
|
315
|
+
metricStates.delete(runId);
|
|
316
|
+
if (!state)
|
|
317
|
+
return;
|
|
318
|
+
const nameAttributes = { [GEN_AI.agentName]: state.agentName };
|
|
319
|
+
safeRecord(metrics?.invokeAgentDuration, seconds(endTime - state.startedAt), {
|
|
320
|
+
...nameAttributes,
|
|
321
|
+
[GEN_AI.requestModel]: state.modelId,
|
|
322
|
+
...(error ? { "error.type": errorType(error) } : {})
|
|
323
|
+
});
|
|
324
|
+
safeRecord(metrics?.invokeAgentInferenceCalls, state.inferenceCalls, nameAttributes);
|
|
325
|
+
safeRecord(metrics?.invokeAgentToolCalls, state.toolCalls, nameAttributes);
|
|
326
|
+
};
|
|
327
|
+
const closeRun = async (runId, attributes, error, endTime = Date.now()) => {
|
|
328
|
+
const run = runSpans.get(runId);
|
|
329
|
+
runSpans.delete(runId);
|
|
330
|
+
let firstError;
|
|
331
|
+
try {
|
|
332
|
+
await closeSteps(runId, {
|
|
333
|
+
"zhivex.step_status": "interrupted",
|
|
334
|
+
"zhivex.run_status": attributes["zhivex.status"]
|
|
335
|
+
}, endTime);
|
|
336
|
+
}
|
|
337
|
+
catch (caught) {
|
|
338
|
+
firstError = caught;
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
await run?.end({ attributes, error, endTime });
|
|
342
|
+
}
|
|
343
|
+
catch (caught) {
|
|
344
|
+
firstError ??= caught;
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
recordAgentMetrics(runId, endTime, error);
|
|
348
|
+
}
|
|
349
|
+
catch (caught) {
|
|
350
|
+
firstError ??= caught;
|
|
351
|
+
}
|
|
352
|
+
if (firstError)
|
|
353
|
+
throw firstError;
|
|
354
|
+
};
|
|
355
|
+
const startRun = async (event) => {
|
|
356
|
+
const existing = runSpans.get(event.runId);
|
|
357
|
+
if (existing) {
|
|
358
|
+
const metricState = metricStates.get(event.runId);
|
|
359
|
+
if (metricState) {
|
|
360
|
+
metricState.agentName = event.agentName ?? metricState.agentName;
|
|
361
|
+
metricState.modelId = event.modelId;
|
|
362
|
+
}
|
|
363
|
+
safeSetAttributes(existing.span, {
|
|
364
|
+
[GEN_AI.agentName]: event.agentName,
|
|
365
|
+
[GEN_AI.requestModel]: event.modelId,
|
|
111
366
|
"zhivex.agent_id": event.agentId,
|
|
112
367
|
"zhivex.provider": event.provider,
|
|
113
368
|
"zhivex.model_id": event.modelId,
|
|
114
369
|
"zhivex.max_steps": event.maxSteps
|
|
115
|
-
})
|
|
370
|
+
});
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const startedAt = event.startedAt ?? Date.now();
|
|
374
|
+
await closeRun(event.runId, { "zhivex.status": "replaced" }, undefined, startedAt);
|
|
375
|
+
metricStates.set(event.runId, {
|
|
376
|
+
startedAt,
|
|
377
|
+
agentName: event.agentName,
|
|
378
|
+
modelId: event.modelId,
|
|
379
|
+
inferenceCalls: 0,
|
|
380
|
+
toolCalls: 0
|
|
381
|
+
});
|
|
382
|
+
runSpans.set(event.runId, observer.startSpan(runSpanName(event.agentName), {
|
|
383
|
+
[GEN_AI.operationName]: GEN_AI_OPERATION.invokeAgent,
|
|
384
|
+
[GEN_AI.agentName]: event.agentName,
|
|
385
|
+
[GEN_AI.requestModel]: event.modelId,
|
|
386
|
+
"zhivex.run_id": event.runId,
|
|
387
|
+
"zhivex.agent_id": event.agentId,
|
|
388
|
+
"zhivex.provider": event.provider,
|
|
389
|
+
"zhivex.model_id": event.modelId,
|
|
390
|
+
"zhivex.max_steps": event.maxSteps
|
|
391
|
+
}, { kind: "internal", startTime: startedAt }));
|
|
392
|
+
};
|
|
393
|
+
const processEvent = async (event) => {
|
|
394
|
+
if (event.type === "run-start") {
|
|
395
|
+
await startRun(event);
|
|
116
396
|
return;
|
|
117
397
|
}
|
|
118
398
|
if (event.type === "step-start") {
|
|
119
|
-
|
|
399
|
+
const metricState = metricStates.get(event.runId);
|
|
400
|
+
if (metricState)
|
|
401
|
+
metricState.inferenceCalls += 1;
|
|
402
|
+
const spans = stepSpans.get(event.runId) ?? new Map();
|
|
403
|
+
const previous = spans.get(event.stepIndex);
|
|
404
|
+
spans.delete(event.stepIndex);
|
|
405
|
+
await previous?.end({ attributes: { "zhivex.step_status": "replaced" }, endTime: event.startedAt });
|
|
406
|
+
spans.set(event.stepIndex, observer.startSpan(stepSpanName(), {
|
|
407
|
+
[GEN_AI.agentName]: event.agentName ?? metricState?.agentName,
|
|
120
408
|
"zhivex.run_id": event.runId,
|
|
121
409
|
"zhivex.agent_id": event.agentId,
|
|
122
410
|
"zhivex.step_index": event.stepIndex
|
|
411
|
+
}, {
|
|
412
|
+
kind: "internal",
|
|
413
|
+
parent: runSpans.get(event.runId),
|
|
414
|
+
startTime: event.startedAt
|
|
123
415
|
}));
|
|
416
|
+
stepSpans.set(event.runId, spans);
|
|
124
417
|
return;
|
|
125
418
|
}
|
|
126
419
|
if (event.type === "step-finish") {
|
|
127
|
-
const
|
|
128
|
-
const handle =
|
|
129
|
-
|
|
420
|
+
const spans = stepSpans.get(event.runId);
|
|
421
|
+
const handle = spans?.get(event.step.index);
|
|
422
|
+
spans?.delete(event.step.index);
|
|
423
|
+
if (spans?.size === 0)
|
|
424
|
+
stepSpans.delete(event.runId);
|
|
130
425
|
await handle?.end({
|
|
131
426
|
attributes: {
|
|
132
427
|
"zhivex.step_status": event.step.status,
|
|
133
428
|
"zhivex.tool_results": event.step.toolResults.length
|
|
134
|
-
}
|
|
429
|
+
},
|
|
430
|
+
error: event.step.status === "failed" ? new Error(event.step.error?.message ?? "Agent step failed.") : undefined,
|
|
431
|
+
endTime: event.step.finishedAt
|
|
432
|
+
});
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (event.type === "tool-start") {
|
|
436
|
+
const metricState = metricStates.get(event.runId);
|
|
437
|
+
if (metricState)
|
|
438
|
+
metricState.toolCalls += 1;
|
|
439
|
+
safeAddEvent(runSpans.get(event.runId)?.span, "tool-start", {
|
|
440
|
+
[GEN_AI.toolName]: event.toolCall.name,
|
|
441
|
+
[GEN_AI.toolCallId]: event.toolCall.id,
|
|
442
|
+
"zhivex.step_index": event.stepIndex
|
|
135
443
|
});
|
|
136
444
|
return;
|
|
137
445
|
}
|
|
@@ -154,6 +462,8 @@ export const createOtelAgentObserver = async (options = {}) => {
|
|
|
154
462
|
}
|
|
155
463
|
if (event.type === "tool-approval") {
|
|
156
464
|
safeAddEvent(runSpans.get(event.runId)?.span, "tool-approval", {
|
|
465
|
+
[GEN_AI.toolName]: event.toolCall.name,
|
|
466
|
+
[GEN_AI.toolCallId]: event.toolCall.id,
|
|
157
467
|
"zhivex.tool_name": event.toolCall.name,
|
|
158
468
|
"zhivex.tool_call_id": event.toolCall.id,
|
|
159
469
|
"zhivex.approved": event.approved,
|
|
@@ -162,16 +472,14 @@ export const createOtelAgentObserver = async (options = {}) => {
|
|
|
162
472
|
return;
|
|
163
473
|
}
|
|
164
474
|
if (event.type === "memory-loaded") {
|
|
165
|
-
safeAddEvent(runSpans.get(event.runId)?.span, "memory-loaded", {
|
|
166
|
-
"zhivex.message_count": event.messageCount
|
|
167
|
-
});
|
|
475
|
+
safeAddEvent(runSpans.get(event.runId)?.span, "memory-loaded", { "zhivex.message_count": event.messageCount });
|
|
168
476
|
return;
|
|
169
477
|
}
|
|
170
478
|
if (event.type === "guardrail-triggered") {
|
|
171
479
|
safeAddEvent(runSpans.get(event.runId)?.span, "guardrail-triggered", {
|
|
172
480
|
"zhivex.stage": event.stage,
|
|
173
|
-
"zhivex.reason": event.reason,
|
|
174
|
-
...event.metadata
|
|
481
|
+
"zhivex.reason": event.reason ? truncateTelemetryString(event.reason, 256) : undefined,
|
|
482
|
+
...safeGuardrailMetadataAttributes(event.metadata)
|
|
175
483
|
});
|
|
176
484
|
return;
|
|
177
485
|
}
|
|
@@ -183,97 +491,210 @@ export const createOtelAgentObserver = async (options = {}) => {
|
|
|
183
491
|
});
|
|
184
492
|
return;
|
|
185
493
|
}
|
|
186
|
-
if (event.type === "
|
|
187
|
-
safeAddEvent(runSpans.get(event.runId)?.span, "
|
|
188
|
-
"zhivex.
|
|
494
|
+
if (event.type === "subagent-start") {
|
|
495
|
+
safeAddEvent(runSpans.get(event.runId)?.span, "subagent-start", {
|
|
496
|
+
"zhivex.child_agent_id": event.childAgentId,
|
|
497
|
+
"zhivex.tool_name": event.toolName
|
|
189
498
|
});
|
|
190
499
|
return;
|
|
191
500
|
}
|
|
192
|
-
if (event.type === "
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"zhivex.status": event.status,
|
|
198
|
-
"zhivex.current_step": event.state.currentStep,
|
|
199
|
-
"zhivex.pending_approvals": event.state.pendingApprovals.length,
|
|
200
|
-
"zhivex.finish_reason": event.state.finishReason,
|
|
201
|
-
"zhivex.provider_finish_reason": event.state.providerFinishReason
|
|
202
|
-
},
|
|
203
|
-
error: event.status === "failed" && event.state.error?.message
|
|
204
|
-
? new Error(event.state.error.message)
|
|
205
|
-
: undefined
|
|
501
|
+
if (event.type === "subagent-finish") {
|
|
502
|
+
safeAddEvent(runSpans.get(event.runId)?.span, "subagent-finish", {
|
|
503
|
+
"zhivex.child_run_id": event.childRun.runId,
|
|
504
|
+
"zhivex.child_agent_id": event.childRun.agentId,
|
|
505
|
+
"zhivex.status": event.childRun.status
|
|
206
506
|
});
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (event.type === "state-saved") {
|
|
510
|
+
safeAddEvent(runSpans.get(event.runId)?.span, "state-saved", { "zhivex.status": event.status });
|
|
511
|
+
return;
|
|
207
512
|
}
|
|
513
|
+
if (event.type === "run-finish") {
|
|
514
|
+
const error = agentStatusError(event);
|
|
515
|
+
const finishedAt = event.finishedAt ?? event.state.updatedAt ?? Date.now();
|
|
516
|
+
await closeRun(event.runId, {
|
|
517
|
+
"zhivex.status": event.status,
|
|
518
|
+
"zhivex.current_step": event.state.currentStep,
|
|
519
|
+
"zhivex.pending_approvals": event.state.pendingApprovals.length,
|
|
520
|
+
"zhivex.finish_reason": event.state.finishReason,
|
|
521
|
+
"zhivex.provider_finish_reason": event.state.providerFinishReason,
|
|
522
|
+
"zhivex.usage_input_tokens": event.state.usage?.inputTokens,
|
|
523
|
+
"zhivex.usage_output_tokens": event.state.usage?.outputTokens,
|
|
524
|
+
"zhivex.usage_total_tokens": event.state.usage?.totalTokens
|
|
525
|
+
}, error, finishedAt);
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
const enqueue = (runId, operation) => {
|
|
529
|
+
const previous = queues.get(runId) ?? Promise.resolve();
|
|
530
|
+
const task = previous.catch(() => undefined).then(operation);
|
|
531
|
+
queues.set(runId, task);
|
|
532
|
+
return task.finally(() => {
|
|
533
|
+
if (queues.get(runId) === task)
|
|
534
|
+
queues.delete(runId);
|
|
535
|
+
});
|
|
208
536
|
};
|
|
537
|
+
const telemetryObserver = (event) => enqueue(event.runId, () => processEvent(event));
|
|
538
|
+
telemetryObserver.startInvocation = (event) => enqueue(event.runId, () => startRun(event));
|
|
539
|
+
telemetryObserver.finishInvocation = (event) => enqueue(event.runId, () => closeRun(event.runId, { "zhivex.status": event.status }, event.error, event.finishedAt));
|
|
540
|
+
telemetryObserver.withRunContext = async (runId, callback) => {
|
|
541
|
+
const handle = runSpans.get(runId);
|
|
542
|
+
return handle ? observer.withSpan(handle, callback) : callback();
|
|
543
|
+
};
|
|
544
|
+
telemetryObserver.withStepContext = async (runId, stepIndex, callback) => {
|
|
545
|
+
const handle = stepSpans.get(runId)?.get(stepIndex) ?? runSpans.get(runId);
|
|
546
|
+
return handle ? observer.withSpan(handle, callback) : callback();
|
|
547
|
+
};
|
|
548
|
+
return telemetryObserver;
|
|
209
549
|
};
|
|
210
550
|
export const createOtelTelemetryMiddleware = async (options = {}) => {
|
|
211
|
-
const observer =
|
|
551
|
+
const [observer, meter] = await Promise.all([
|
|
552
|
+
options.observer ? Promise.resolve(options.observer) : createOtelObserver(options),
|
|
553
|
+
resolveMeter(options)
|
|
554
|
+
]);
|
|
555
|
+
const metrics = meter ? createGenAiMetrics(meter) : undefined;
|
|
212
556
|
const spans = new Map();
|
|
213
|
-
const
|
|
557
|
+
const inputIds = new WeakMap();
|
|
558
|
+
let nextInputId = 1;
|
|
559
|
+
const prefix = options.spanNamePrefix?.trim();
|
|
560
|
+
const inputId = (input) => {
|
|
561
|
+
const existing = inputIds.get(input);
|
|
562
|
+
if (existing !== undefined)
|
|
563
|
+
return existing;
|
|
564
|
+
const id = nextInputId;
|
|
565
|
+
nextInputId += 1;
|
|
566
|
+
inputIds.set(input, id);
|
|
567
|
+
return id;
|
|
568
|
+
};
|
|
214
569
|
const keyFor = (event) => {
|
|
215
570
|
if (event.type === "generate-start" || event.type === "generate-finish" || event.type === "generate-error") {
|
|
216
|
-
return
|
|
571
|
+
return event.generateId === undefined
|
|
572
|
+
? `generate:${event.model.provider}:${event.model.modelId}:${inputId(event.input)}:${event.startedAt}`
|
|
573
|
+
: `generate:${event.generateId}`;
|
|
217
574
|
}
|
|
218
|
-
if (event.type === "stream-start" || event.type === "stream-finish" || event.type === "stream-error") {
|
|
219
|
-
return
|
|
575
|
+
if (event.type === "stream-start" || event.type === "stream-chunk" || event.type === "stream-finish" || event.type === "stream-error") {
|
|
576
|
+
return event.streamId === undefined
|
|
577
|
+
? `stream:${event.model.provider}:${event.model.modelId}:${inputId(event.input)}:${event.startedAt}`
|
|
578
|
+
: `stream:${event.streamId}`;
|
|
220
579
|
}
|
|
221
|
-
return `tool:${event.model.provider}:${event.model.modelId}:${event.step}:${event.toolCall.id}:${event.startedAt}`;
|
|
580
|
+
return `tool:${event.model.provider}:${event.model.modelId}:${inputId(event.input)}:${event.step}:${event.toolCall.id}:${event.startedAt}`;
|
|
222
581
|
};
|
|
582
|
+
const pushSpan = (key, handle) => {
|
|
583
|
+
const queued = spans.get(key) ?? [];
|
|
584
|
+
queued.push(handle);
|
|
585
|
+
spans.set(key, queued);
|
|
586
|
+
};
|
|
587
|
+
const peekSpan = (key) => spans.get(key)?.[0];
|
|
588
|
+
const shiftSpan = (key) => {
|
|
589
|
+
const queued = spans.get(key);
|
|
590
|
+
const handle = queued?.shift();
|
|
591
|
+
if (queued?.length === 0)
|
|
592
|
+
spans.delete(key);
|
|
593
|
+
return handle;
|
|
594
|
+
};
|
|
595
|
+
const modelSpanName = (modelId, provider, operation) => prefix ? `${prefix}.${operation}` : `${toModelOperationName(provider)} ${modelId}`;
|
|
596
|
+
const toolSpanName = (toolName) => prefix ? `${prefix}.tool` : `${GEN_AI_OPERATION.executeTool} ${toolName}`;
|
|
223
597
|
return createTelemetryMiddleware({
|
|
224
598
|
onEvent: async (event) => {
|
|
225
599
|
if (event.type === "generate-start" || event.type === "stream-start") {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
"zhivex.
|
|
229
|
-
|
|
600
|
+
pushSpan(keyFor(event), observer.startSpan(modelSpanName(event.model.modelId, event.model.provider, event.type === "generate-start" ? "generate" : "stream"), {
|
|
601
|
+
...requestAttributes(event),
|
|
602
|
+
"zhivex.operation_type": event.type === "generate-start" ? "generate" : "stream",
|
|
603
|
+
"zhivex.generate_id": event.type === "generate-start" ? event.generateId : undefined,
|
|
604
|
+
"zhivex.stream_id": event.type === "stream-start" ? event.streamId : undefined
|
|
605
|
+
}, { kind: "client", startTime: event.startedAt }));
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (event.type === "stream-chunk") {
|
|
609
|
+
const attributes = clientMetricAttributes(event);
|
|
610
|
+
if (event.chunkIndex === 1 && event.timeToFirstChunkMs !== undefined) {
|
|
611
|
+
const duration = seconds(event.timeToFirstChunkMs);
|
|
612
|
+
safeRecord(metrics?.clientTimeToFirstChunk, duration, attributes);
|
|
613
|
+
const span = peekSpan(keyFor(event))?.span;
|
|
614
|
+
if (span)
|
|
615
|
+
safeSetAttributes(span, { [GEN_AI.responseTimeToFirstChunk]: duration });
|
|
616
|
+
}
|
|
617
|
+
if (event.timeSincePreviousChunkMs !== undefined) {
|
|
618
|
+
safeRecord(metrics?.clientTimePerOutputChunk, seconds(event.timeSincePreviousChunkMs), attributes);
|
|
619
|
+
}
|
|
230
620
|
return;
|
|
231
621
|
}
|
|
232
622
|
if (event.type === "tool-execution-start") {
|
|
233
|
-
|
|
623
|
+
pushSpan(keyFor(event), observer.startSpan(toolSpanName(event.toolCall.name), {
|
|
624
|
+
[GEN_AI.operationName]: GEN_AI_OPERATION.executeTool,
|
|
625
|
+
[GEN_AI.toolName]: event.toolCall.name,
|
|
626
|
+
[GEN_AI.toolCallId]: event.toolCall.id,
|
|
627
|
+
[GEN_AI.toolType]: "function",
|
|
234
628
|
"zhivex.provider": event.model.provider,
|
|
235
629
|
"zhivex.model_id": event.model.modelId,
|
|
236
630
|
"zhivex.step": event.step,
|
|
237
631
|
"zhivex.tool_name": event.toolCall.name,
|
|
238
632
|
"zhivex.tool_call_id": event.toolCall.id
|
|
239
|
-
}));
|
|
633
|
+
}, { kind: "internal", startTime: event.startedAt }));
|
|
240
634
|
return;
|
|
241
635
|
}
|
|
242
|
-
const handle =
|
|
243
|
-
spans.delete(keyFor(event));
|
|
636
|
+
const handle = shiftSpan(keyFor(event));
|
|
244
637
|
if (event.type === "generate-finish") {
|
|
638
|
+
const attributes = clientMetricAttributes(event);
|
|
639
|
+
safeRecord(metrics?.clientOperationDuration, seconds(event.latencyMs), attributes);
|
|
640
|
+
recordTokenUsage(metrics, event.output.usage, attributes);
|
|
245
641
|
await handle?.end({
|
|
246
|
-
attributes: {
|
|
247
|
-
|
|
248
|
-
|
|
642
|
+
attributes: responseAttributes({
|
|
643
|
+
finishReason: event.output.finishReason,
|
|
644
|
+
providerFinishReason: event.output.providerFinishReason,
|
|
645
|
+
usage: event.output.usage,
|
|
646
|
+
latencyMs: event.latencyMs
|
|
647
|
+
}),
|
|
648
|
+
endTime: event.finishedAt
|
|
249
649
|
});
|
|
250
650
|
return;
|
|
251
651
|
}
|
|
252
652
|
if (event.type === "stream-finish") {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
"zhivex.latency_ms": event.latencyMs
|
|
258
|
-
}
|
|
259
|
-
});
|
|
653
|
+
const attributes = clientMetricAttributes(event);
|
|
654
|
+
safeRecord(metrics?.clientOperationDuration, seconds(event.latencyMs), attributes);
|
|
655
|
+
recordTokenUsage(metrics, event.usage, attributes);
|
|
656
|
+
await handle?.end({ attributes: responseAttributes(event), endTime: event.finishedAt });
|
|
260
657
|
return;
|
|
261
658
|
}
|
|
262
659
|
if (event.type === "tool-execution-finish") {
|
|
660
|
+
const error = event.toolResult.isError ? new Error(event.toolResult.error?.message ?? "Tool execution failed.") : undefined;
|
|
661
|
+
safeRecord(metrics?.executeToolDuration, seconds(event.latencyMs), {
|
|
662
|
+
[GEN_AI.toolName]: event.toolCall.name,
|
|
663
|
+
[GEN_AI.toolType]: "function",
|
|
664
|
+
...(error ? { "error.type": errorType(error) } : {})
|
|
665
|
+
});
|
|
263
666
|
await handle?.end({
|
|
264
667
|
attributes: {
|
|
265
668
|
"zhivex.latency_ms": event.latencyMs,
|
|
266
669
|
"zhivex.tool_error": event.toolResult.isError
|
|
267
|
-
}
|
|
670
|
+
},
|
|
671
|
+
error,
|
|
672
|
+
endTime: event.finishedAt
|
|
268
673
|
});
|
|
269
674
|
return;
|
|
270
675
|
}
|
|
271
|
-
if (event.type === "generate-error" || event.type === "stream-error"
|
|
676
|
+
if (event.type === "generate-error" || event.type === "stream-error") {
|
|
677
|
+
safeRecord(metrics?.clientOperationDuration, seconds(event.latencyMs), clientMetricAttributes(event, event.error));
|
|
272
678
|
await handle?.end({
|
|
273
679
|
attributes: {
|
|
274
|
-
"zhivex.latency_ms": event.latencyMs
|
|
680
|
+
"zhivex.latency_ms": event.latencyMs,
|
|
681
|
+
...(event.type === "stream-error" ? { "zhivex.output_chunk_count": event.outputChunkCount } : {})
|
|
275
682
|
},
|
|
276
|
-
error: event.error
|
|
683
|
+
error: event.error,
|
|
684
|
+
endTime: event.finishedAt
|
|
685
|
+
});
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (event.type === "tool-execution-error") {
|
|
689
|
+
safeRecord(metrics?.executeToolDuration, seconds(event.latencyMs), {
|
|
690
|
+
[GEN_AI.toolName]: event.toolCall.name,
|
|
691
|
+
[GEN_AI.toolType]: "function",
|
|
692
|
+
"error.type": errorType(event.error)
|
|
693
|
+
});
|
|
694
|
+
await handle?.end({
|
|
695
|
+
attributes: { "zhivex.latency_ms": event.latencyMs },
|
|
696
|
+
error: event.error,
|
|
697
|
+
endTime: event.finishedAt
|
|
277
698
|
});
|
|
278
699
|
}
|
|
279
700
|
}
|