@zhivex-ai/core 1.4.0 → 1.6.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.
Files changed (50) hide show
  1. package/README.md +3 -0
  2. package/dist/agent-control-plane.d.ts +31 -2
  3. package/dist/agent-control-plane.d.ts.map +1 -1
  4. package/dist/agent-control-plane.js +545 -30
  5. package/dist/agent-control-plane.js.map +1 -1
  6. package/dist/agent.d.ts +1 -0
  7. package/dist/agent.d.ts.map +1 -1
  8. package/dist/agent.js +320 -193
  9. package/dist/agent.js.map +1 -1
  10. package/dist/api-stability.d.ts.map +1 -1
  11. package/dist/api-stability.js +65 -47
  12. package/dist/api-stability.js.map +1 -1
  13. package/dist/artifact.d.ts +32 -9
  14. package/dist/artifact.d.ts.map +1 -1
  15. package/dist/artifact.js +427 -81
  16. package/dist/artifact.js.map +1 -1
  17. package/dist/catalog.d.ts +34 -2
  18. package/dist/catalog.d.ts.map +1 -1
  19. package/dist/catalog.js +262 -7
  20. package/dist/catalog.js.map +1 -1
  21. package/dist/index.d.ts +11 -7
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +5 -3
  24. package/dist/index.js.map +1 -1
  25. package/dist/middleware.d.ts.map +1 -1
  26. package/dist/middleware.js +138 -52
  27. package/dist/middleware.js.map +1 -1
  28. package/dist/observability.d.ts +57 -10
  29. package/dist/observability.d.ts.map +1 -1
  30. package/dist/observability.js +535 -114
  31. package/dist/observability.js.map +1 -1
  32. package/dist/types.d.ts +71 -3
  33. package/dist/types.d.ts.map +1 -1
  34. package/dist/workflow-evaluation-gate.d.ts +130 -0
  35. package/dist/workflow-evaluation-gate.d.ts.map +1 -0
  36. package/dist/workflow-evaluation-gate.js +337 -0
  37. package/dist/workflow-evaluation-gate.js.map +1 -0
  38. package/dist/workflow-evaluation.d.ts +8 -0
  39. package/dist/workflow-evaluation.d.ts.map +1 -1
  40. package/dist/workflow-evaluation.js +14 -1
  41. package/dist/workflow-evaluation.js.map +1 -1
  42. package/dist/workflow-observability.d.ts +13 -0
  43. package/dist/workflow-observability.d.ts.map +1 -0
  44. package/dist/workflow-observability.js +186 -0
  45. package/dist/workflow-observability.js.map +1 -0
  46. package/dist/workflow.d.ts +49 -0
  47. package/dist/workflow.d.ts.map +1 -1
  48. package/dist/workflow.js +255 -78
  49. package/dist/workflow.js.map +1 -1
  50. package/package.json +9 -1
@@ -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
- // Keep OpenTelemetry optional for consumers that never use the OTEL helpers.
4
- const loadOptionalOtelApi = async () => {
5
- try {
6
- return await import(OTEL_API_MODULE);
7
- }
8
- catch {
9
- return undefined;
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 toSpanAttributeValue = (value) => {
13
- if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
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
- .map((item) => (item === null || typeof item === "string" || typeof item === "number" || typeof item === "boolean" ? item : JSON.stringify(item)))
19
- .filter((item) => item !== undefined);
20
- return normalized.length ? normalized : undefined;
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 toSpanAttributes = (attributes) => {
33
- const entries = Object.entries(attributes ?? {})
34
- .map(([key, value]) => [key, toSpanAttributeValue(value)])
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(toSpanAttributes(attributes))) {
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
- if (!span?.addEvent) {
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
- span.addEvent(name, toSpanAttributes(attributes));
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
- async end(options = {}) {
57
- safeSetAttributes(this.span, options.attributes);
58
- if (options.error) {
59
- this.span.recordException?.(options.error);
60
- await this.statusSetter?.(this.span, options.error);
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
- constructor(tracer, statusSetter) {
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: toSpanAttributes(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 = async () => {
80
- const otel = await loadOptionalOtelApi();
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
- return new OTelObserver(otel.trace.getTracer(options.tracerName ?? "zhivex-ai", options.version), statusSetter);
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 = options.observer ?? (await createOtelObserver(options));
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 prefix = options.spanNamePrefix ?? "zhivex.agent";
107
- return async (event) => {
108
- if (event.type === "run-start") {
109
- runSpans.set(event.runId, observer.startSpan(`${prefix}.run`, {
110
- "zhivex.run_id": event.runId,
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
- stepSpans.set(`${event.runId}:${event.stepIndex}`, observer.startSpan(`${prefix}.step`, {
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 key = `${event.runId}:${event.step.index}`;
128
- const handle = stepSpans.get(key);
129
- stepSpans.delete(key);
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 === "state-saved") {
187
- safeAddEvent(runSpans.get(event.runId)?.span, "state-saved", {
188
- "zhivex.status": event.status
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 === "run-finish") {
193
- const handle = runSpans.get(event.runId);
194
- runSpans.delete(event.runId);
195
- await handle?.end({
196
- attributes: {
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 = options.observer ?? (await createOtelObserver(options));
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 prefix = options.spanNamePrefix ?? "zhivex.model";
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 `generate:${event.model.provider}:${event.model.modelId}:${event.startedAt}`;
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 `stream:${event.model.provider}:${event.model.modelId}:${event.startedAt}`;
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
- spans.set(keyFor(event), observer.startSpan(`${prefix}.${event.type === "generate-start" ? "generate" : "stream"}`, {
227
- "zhivex.provider": event.model.provider,
228
- "zhivex.model_id": event.model.modelId
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
- spans.set(keyFor(event), observer.startSpan(`${prefix}.tool`, {
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 = spans.get(keyFor(event));
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
- "zhivex.latency_ms": event.latencyMs
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
- await handle?.end({
254
- attributes: {
255
- "zhivex.finish_reason": event.finishReason,
256
- "zhivex.provider_finish_reason": event.providerFinishReason,
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" || event.type === "tool-execution-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
  }