@telemetry-dev/eve 0.1.1 → 0.1.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/README.md CHANGED
@@ -10,7 +10,7 @@ lifecycle-hook, and HTTP-client surfaces.
10
10
  npm install @telemetry-dev/eve eve
11
11
  ```
12
12
 
13
- Requires `eve >=0.19.0 <1` and Node.js 24 or newer.
13
+ Requires `eve >=0.47.0 <1` and Node.js 24 or newer.
14
14
 
15
15
  ## Environment
16
16
 
@@ -93,17 +93,21 @@ const client = wrapEveClient(new Client({ host: "http://127.0.0.1:3000" }), {
93
93
  agentName: "support-agent",
94
94
  });
95
95
 
96
- const session = client.session();
97
- const response = await session.send("Summarize this incident.");
96
+ const { response, session } = await client.sessions.create({
97
+ message: "Summarize this incident.",
98
+ });
98
99
  const result = await response.result();
100
+ const followUp = await session.send("What do we do next?");
99
101
  ```
100
102
 
101
- `wrapEveClient()` creates one caller-side `invoke_agent` span per `ClientSession.send()` turn. It
102
- records the outgoing message or HITL input responses as span input, sets `gen_ai.conversation.id` from
103
- Eve's response session id, streams TTFT from the first message/reasoning delta, accumulates usage from
104
- `step.completed`, records final message/result output, and marks failures/cancellations as errors.
103
+ `wrapEveClient()` makes one caller-side `invoke_agent` span for each turn: `sessions.create()`,
104
+ `ClientSession.send()`, and `ClientSession.respond()`. It records the outgoing message or the HITL
105
+ input responses as span input, sets `gen_ai.conversation.id` from
106
+ the session state or Eve's response session id (so all turns of one session share one trace), streams
107
+ TTFT from the first message/reasoning delta, accumulates usage from `step.completed`, records final
108
+ message/result output, and marks failures/cancellations as errors.
105
109
 
106
- `ClientSession.stream()`, `Client.info()`, and `Client.health()` are intentionally not spanned.
110
+ `ClientSession.stream()`, `sessions.attach()`, `Client.info()`, and `Client.health()` are not spanned.
107
111
  Attach-streams are unbounded and the probe routes are not agent turns.
108
112
 
109
113
  ## Trace shape
@@ -120,25 +124,62 @@ A typical turn contains:
120
124
  context and any custom runtime context you return from `step.started`.
121
125
  - Lifecycle logs joined to the same session via `gen_ai.conversation.id`.
122
126
 
123
- Eve's internal workflow-engine spans (`workflow.execute`, `step.execute`, `world.*`, `hook.resume`,
124
- etc., tracer scope `workflow`) are excluded by default so traces stay AI-centric. Pass
125
- `spanFilter: () => true` to export them too.
127
+ By default, only AI spans are exported: tracer scopes `eve`, `eve.agent`, `gen_ai`, and this SDK. Spans from
128
+ other libraries on the same global tracer provider (for example better-auth, Nitro, or eve's
129
+ `workflow` engine) are dropped. Pass `spanFilter: () => true` to export every span.
126
130
 
127
131
  ## Content capture
128
132
 
129
- Server-side model content follows Eve's `recordInputs` / `recordOutputs` settings. Caller-side wrapper
130
- content follows the telemetry.dev SDK `captureInput` / `captureOutput` settings. The SDK mask and
131
- truncation options apply to captured client-wrapper inputs, outputs, and logs.
133
+ Server-side model content depends on Eve's trace policy and the destination capture settings.
134
+ `telemetryDevInstrumentation()` maps `captureInput` / `captureOutput` to `recordInputs` /
135
+ `recordOutputs`. The defaults are `true`, but these flags cannot override Eve's trace policy.
136
+ In Eve 0.47.0 and 0.50.0, the default policy records content only for a `public` audience,
137
+ even if `EVE_DEV=1`. HTTP clients, web chat, and schedules have an `unknown` audience.
138
+
139
+ For approved local content capture, use Eve's provider layout.
140
+ Remove `agent/instrumentation.ts`.
141
+ Set `experimental.instrumentationProviders` to `true` in the Eve configuration.
142
+ Add these two files:
143
+
144
+ ```ts
145
+ // agent/instrumentation/otel.ts
146
+ import { otel } from "eve/instrumentation/otel";
147
+
148
+ export default otel({ tracePolicy: () => true });
149
+ ```
150
+
151
+ ```ts
152
+ // agent/instrumentation/telemetry-dev.ts
153
+ import { telemetryDevOtelIntegration } from "@telemetry-dev/eve";
154
+
155
+ export default telemetryDevOtelIntegration();
156
+ ```
157
+
158
+ This policy selects Eve's audience-aware capture.
159
+ For local `unknown`-audience channels, run `eve dev` or set `EVE_DEV=1` in the Eve server process.
160
+ Without `EVE_DEV=1`, local unknown-audience channels export metadata only.
161
+ Local private channels always export metadata only.
162
+ Destination settings and forwarded parent policies can decrease content capture.
163
+
164
+ The legacy single-file `telemetryDevInstrumentation()` API has no `tracePolicy` option.
165
+ Thus, it cannot record model content for new unknown-audience sessions on these Eve versions.
166
+ The single-file layout remains suitable for metadata-only traces.
167
+ Do not combine the two layouts.
168
+ This package cannot restore content that Eve removed.
169
+
170
+ Caller-side wrapper content follows the telemetry.dev SDK `captureInput` / `captureOutput`
171
+ settings. The SDK mask and truncation options apply to captured client-wrapper inputs, outputs,
172
+ and logs.
132
173
 
133
174
  ## Limitations
134
175
 
135
176
  - Pass options to exactly one entry point in a process. Initialization is one-shot so duplicate entry
136
177
  points do not replace the first configuration.
137
178
  - `ClientSession.stream()` is not spanned.
138
- - Cost is computed server-side by telemetry.dev from usage and pricing data; Eve stream events carry
139
- no cost fields, so the client wrapper records usage only.
140
- - The client wrapper's `invoke_agent` span ends when the response stream is consumed (iteration or
141
- `result()`); an unconsumed response leaves the span unexported.
179
+ - The client wrapper exports a total cost only after an error-free terminal event. Each completed step must report a finite, non-negative `usage.costUsd` value, including zero. Otherwise, the wrapper omits the total but still records token usage. Model information is also necessary for server-side pricing.
180
+ - When caller and server spans share a trace and matching Eve turn metadata, the caller cost replaces the server's main model-call costs. Tool and subagent costs stay separate. Without that match, the two reported totals stay unchanged.
181
+ - The client wrapper ends its `invoke_agent` span after stream use or an abort signal. Without these actions, the span stays open.
182
+ - Eve 0.50 gives `telemetryDevOtelIntegration()` no tracer-provider or parent-context hook. Thus, Eve provider spans cannot join the deterministic session trace.
142
183
  - Inline subagent child streams (`subagent.event`) are not fanned out in full; the hook logs
143
184
  `subagent.started`, `subagent.called`, `subagent.completed`, and child failure events.
144
185
  - Eve workflow `$eve.*` run tags are a Vercel-dashboard surface and are not visible to OpenTelemetry,
package/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
- import { ClientOverrides, ClientOverrides as ClientOverrides$1, TelemetryOptions } from "@telemetry-dev/sdk";
1
+ import { ClientOverrides, ClientOverrides as ClientOverrides$1, TelemetryOptions, TelemetrySpanProcessorOptions } from "@telemetry-dev/sdk";
2
2
  import { Client } from "eve/client";
3
+ import { OtelIntegration } from "eve/instrumentation/otel";
3
4
  import { HookDefinition } from "eve/hooks";
4
5
  import { InstrumentationDefinition, InstrumentationEvents, InstrumentationRuntimeContext } from "eve/instrumentation";
5
6
 
@@ -34,4 +35,16 @@ interface TelemetryDevInstrumentationOptions extends TelemetryDevEveOptions {
34
35
  }
35
36
  declare function telemetryDevInstrumentation(options?: TelemetryDevInstrumentationOptions, overrides?: ClientOverrides$1): InstrumentationDefinition;
36
37
  //#endregion
37
- export { type TelemetryDevEveOptions, type TelemetryDevInstrumentationOptions, type WrapEveClientOptions, telemetryDevHook, telemetryDevInstrumentation, wrapEveClient };
38
+ //#region src/otel.d.ts
39
+ interface TelemetryDevOtelIntegrationOptions extends Omit<TelemetrySpanProcessorOptions, "spanExporter"> {
40
+ recordInputs?: boolean;
41
+ recordOutputs?: boolean;
42
+ }
43
+ /**
44
+ * telemetry.dev as a destination in eve's `agent/instrumentation/` provider layout
45
+ * (`experimental.instrumentationProviders`). Export it from one file in that directory.
46
+ * eve owns the tracer provider there, so this attaches a processor instead of registering one.
47
+ */
48
+ declare function telemetryDevOtelIntegration(options?: TelemetryDevOtelIntegrationOptions): OtelIntegration;
49
+ //#endregion
50
+ export { type TelemetryDevEveOptions, type TelemetryDevInstrumentationOptions, type TelemetryDevOtelIntegrationOptions, type WrapEveClientOptions, telemetryDevHook, telemetryDevInstrumentation, telemetryDevOtelIntegration, wrapEveClient };
package/dist/index.mjs CHANGED
@@ -1,13 +1,21 @@
1
- import { init, log, startSpan } from "@telemetry-dev/sdk";
1
+ import { SCOPE_NAME, TelemetrySpanProcessor, init, log, startSpan } from "@telemetry-dev/sdk";
2
2
  import { MessageResponse, isCurrentTurnBoundaryEvent } from "eve/client";
3
+ import { otelIntegration } from "eve/instrumentation/otel";
3
4
  //#region src/config.ts
4
5
  let initialized = false;
6
+ const AI_SCOPES = {
7
+ eve: true,
8
+ "eve.agent": true,
9
+ gen_ai: true,
10
+ [SCOPE_NAME]: true
11
+ };
12
+ const isAiScope = (scope) => Object.hasOwn(AI_SCOPES, scope);
5
13
  /**
6
14
  * Initializes the telemetry.dev SDK exactly once per process for this integration.
7
- * registerGlobal:true lets eve's tracers (scopes "eve" and "gen_ai") resolve to
8
- * the SDK provider. spanFilter defaults to exporting everything except eve's
9
- * internal workflow-engine spans (scope "workflow"); pass spanFilter: () => true
10
- * to export those too.
15
+ * registerGlobal:true lets eve's tracers resolve to the SDK provider. spanFilter defaults to
16
+ * exporting only AI scopes (see AI_SCOPES); pass spanFilter: () => true to export every span.
17
+ * eve starts each `ai.eve.turn` under a workflow-engine span of its own trace, so the turn is
18
+ * re-rooted under the session parent and every turn of a session lands in one trace.
11
19
  */
12
20
  function ensureInit(options = {}, overrides) {
13
21
  if (initialized) return;
@@ -16,7 +24,11 @@ function ensureInit(options = {}, overrides) {
16
24
  ...options,
17
25
  sdkName: "@telemetry-dev/eve",
18
26
  registerGlobal: true,
19
- spanFilter: options.spanFilter ?? ((span) => span.instrumentationScope.name !== "workflow")
27
+ spanFilter: options.spanFilter ?? ((span) => isAiScope(span.instrumentationScope.name)),
28
+ sessionRootOf: (name, attributes) => {
29
+ const sessionId = attributes["eve.session.id"];
30
+ return name === "ai.eve.turn" && typeof sessionId === "string" ? sessionId : void 0;
31
+ }
20
32
  }, overrides);
21
33
  }
22
34
  //#endregion
@@ -52,9 +64,6 @@ function failureError(code, message) {
52
64
  if (code) error.name = code;
53
65
  return error;
54
66
  }
55
- function inputForSpan(payload) {
56
- return payload.message ?? payload.inputResponses;
57
- }
58
67
  function bindOrReturn(target, prop) {
59
68
  const value = target[prop];
60
69
  return value instanceof Function ? value.bind(target) : value;
@@ -85,16 +94,21 @@ function addLifecycleEvent(span, event) {
85
94
  default: return;
86
95
  }
87
96
  }
88
- async function* instrumentedStream(response, span, start, payload) {
97
+ async function* instrumentedStream(response, span, start, signal, endCancelled) {
89
98
  const usage = {};
90
99
  let sawUsage = false;
91
100
  let output;
92
101
  let finishReason;
102
+ let costUsd;
103
+ let allCostsKnown = true;
93
104
  let timeToFirstChunkMs;
94
105
  let error;
106
+ let done = false;
95
107
  let sawTerminalEvent = false;
96
108
  const finish = () => {
97
- if (!sawTerminalEvent && error === void 0) if (payload.signal?.aborted) {
109
+ if (done) return;
110
+ done = true;
111
+ if (!sawTerminalEvent && error === void 0) if (signal?.aborted) {
98
112
  error = failureError("cancelled", "cancelled");
99
113
  finishReason ??= "cancelled";
100
114
  } else {
@@ -104,6 +118,7 @@ async function* instrumentedStream(response, span, start, payload) {
104
118
  span.end({
105
119
  usage: sawUsage ? usage : void 0,
106
120
  finishReason,
121
+ costUsd: sawTerminalEvent && error === void 0 && allCostsKnown && Number.isFinite(costUsd) ? costUsd : void 0,
107
122
  output,
108
123
  timeToFirstChunkMs,
109
124
  error: error instanceof Error ? error : error === void 0 ? void 0 : /* @__PURE__ */ new Error("error")
@@ -112,6 +127,16 @@ async function* instrumentedStream(response, span, start, payload) {
112
127
  try {
113
128
  for await (const event of response) {
114
129
  const data = eventData$1(event);
130
+ if (event.type === "turn.started") {
131
+ const turnTrace = asRecord$1(data.trace);
132
+ const traceId = stringField$1(turnTrace, "traceId");
133
+ const spanId = stringField$1(turnTrace, "spanId");
134
+ const turnId = stringField$1(data, "turnId");
135
+ if (traceId && spanId && turnId) {
136
+ span.span.setAttribute("td.eve.turn_root", `${traceId}/${spanId}`);
137
+ span.span.setAttribute("eve.turn.id", turnId);
138
+ }
139
+ }
115
140
  if (isCurrentTurnBoundaryEvent(event)) sawTerminalEvent = true;
116
141
  if (timeToFirstChunkMs === void 0 && (event.type === "message.appended" || event.type === "reasoning.appended")) timeToFirstChunkMs = performance.now() - start;
117
142
  if (event.type === "step.completed") {
@@ -120,6 +145,9 @@ async function* instrumentedStream(response, span, start, payload) {
120
145
  sawUsage = addUsage(usage, "outputTokens", numberField$1(eventUsage, "outputTokens")) || sawUsage;
121
146
  sawUsage = addUsage(usage, "cacheReadInputTokens", numberField$1(eventUsage, "cacheReadTokens")) || sawUsage;
122
147
  sawUsage = addUsage(usage, "cacheCreationInputTokens", numberField$1(eventUsage, "cacheWriteTokens")) || sawUsage;
148
+ const stepCost = numberField$1(eventUsage, "costUsd");
149
+ if (stepCost === void 0 || !Number.isFinite(stepCost) || stepCost < 0) allCostsKnown = false;
150
+ else costUsd = (costUsd ?? 0) + stepCost;
123
151
  finishReason = stringField$1(data, "finishReason") ?? finishReason;
124
152
  }
125
153
  if (event.type === "message.completed" && stringField$1(data, "finishReason") !== "tool-calls") output = data.message;
@@ -132,49 +160,104 @@ async function* instrumentedStream(response, span, start, payload) {
132
160
  }
133
161
  finishReason = "error";
134
162
  }
163
+ if (event.type === "turn.cancelled") {
164
+ error = failureError("cancelled", "cancelled");
165
+ finishReason = "cancelled";
166
+ }
135
167
  addLifecycleEvent(span, event);
136
168
  yield event;
137
169
  }
138
170
  } catch (caught) {
139
- if (payload.signal?.aborted) {
171
+ if (signal?.aborted) {
140
172
  error = failureError("cancelled", "cancelled");
141
173
  finishReason ??= "cancelled";
142
174
  } else error = caught instanceof Error ? caught : new Error(String(caught));
143
175
  throw caught;
144
176
  } finally {
145
177
  finish();
178
+ if (endCancelled) signal?.removeEventListener("abort", endCancelled);
179
+ }
180
+ }
181
+ async function tracedTurn(options, input, turnOptions, knownSessionId, send) {
182
+ const startTime = /* @__PURE__ */ new Date();
183
+ const start = performance.now();
184
+ const signal = turnOptions?.signal;
185
+ const spanFor = (sessionId) => startSpan(options.spanName ?? "invoke_agent", {
186
+ type: "agent",
187
+ agentName: options.agentName,
188
+ input,
189
+ startTime,
190
+ attributes: sessionId === void 0 ? void 0 : { "gen_ai.conversation.id": sessionId }
191
+ });
192
+ let response;
193
+ try {
194
+ response = await send();
195
+ } catch (error) {
196
+ const span = spanFor(knownSessionId);
197
+ if (signal?.aborted) span.end({
198
+ error: failureError("cancelled", "cancelled"),
199
+ finishReason: "cancelled"
200
+ });
201
+ else span.end({ error: error instanceof Error ? error : new Error(String(error)) });
202
+ throw error;
146
203
  }
204
+ const span = spanFor(response.sessionId);
205
+ let streamStarted = false;
206
+ let endedBeforeStream = false;
207
+ const endCancelled = () => {
208
+ if (streamStarted) return;
209
+ endedBeforeStream = true;
210
+ span.end({
211
+ error: failureError("cancelled", "cancelled"),
212
+ finishReason: "cancelled"
213
+ });
214
+ };
215
+ if (signal?.aborted) endCancelled();
216
+ else signal?.addEventListener("abort", endCancelled, { once: true });
217
+ return new MessageResponse({
218
+ cancelTurn: () => response.cancel(),
219
+ createStream: () => {
220
+ streamStarted = true;
221
+ if (endedBeforeStream) return (async function* () {
222
+ yield* response;
223
+ })();
224
+ return instrumentedStream(response, span, start, signal, endCancelled);
225
+ },
226
+ sessionId: response.sessionId
227
+ });
147
228
  }
148
229
  function wrapSession(session, options) {
149
230
  return new Proxy(session, { get(target, prop) {
150
231
  if (prop === "send") {
151
- const wrappedSend = async (input) => {
152
- const turnPayload = input instanceof String || input?.constructor === String ? { message: String(input) } : input;
153
- const span = startSpan(options.spanName ?? "invoke_agent", {
154
- type: "agent",
155
- agentName: options.agentName,
156
- input: inputForSpan(turnPayload)
157
- });
158
- const start = performance.now();
159
- let response;
160
- try {
161
- response = await target.send(input);
162
- } catch (error) {
163
- if (turnPayload.signal?.aborted) span.end({
164
- error: failureError("cancelled", "cancelled"),
165
- finishReason: "cancelled"
166
- });
167
- else span.end({ error: error instanceof Error ? error : new Error(String(error)) });
168
- throw error;
169
- }
170
- span.span.setAttribute("gen_ai.conversation.id", response.sessionId);
171
- return new MessageResponse({
172
- continuationToken: response.continuationToken,
173
- sessionId: response.sessionId,
174
- createStream: () => instrumentedStream(response, span, start, turnPayload)
175
- });
232
+ const send = (message, turnOptions) => tracedTurn(options, message, turnOptions, target.state.sessionId, () => target.send(message, turnOptions));
233
+ return send;
234
+ }
235
+ if (prop === "respond") {
236
+ const respond = (inputResponses, turnOptions) => tracedTurn(options, inputResponses, turnOptions, target.state.sessionId, () => target.respond(inputResponses, turnOptions));
237
+ return respond;
238
+ }
239
+ return bindOrReturn(target, prop);
240
+ } });
241
+ }
242
+ function wrapSessions(sessions, options) {
243
+ return new Proxy(sessions, { get(target, prop) {
244
+ if (prop === "create") {
245
+ const create = async (input) => {
246
+ let session;
247
+ return {
248
+ response: await tracedTurn(options, input.message, input, void 0, async () => {
249
+ const created = await target.create(input);
250
+ session = created.session;
251
+ return created.response;
252
+ }),
253
+ session: wrapSession(session, options)
254
+ };
176
255
  };
177
- return wrappedSend;
256
+ return create;
257
+ }
258
+ if (prop === "attach") {
259
+ const attach = (sessionId, attachOptions) => wrapSession(target.attach(sessionId, attachOptions), options);
260
+ return attach;
178
261
  }
179
262
  return bindOrReturn(target, prop);
180
263
  } });
@@ -182,8 +265,9 @@ function wrapSession(session, options) {
182
265
  function wrapEveClient(client, options = {}, overrides) {
183
266
  const { agentName: _agentName, spanName: _spanName, ...sdkOptions } = options;
184
267
  ensureInit(sdkOptions, overrides);
268
+ const sessions = wrapSessions(client.sessions, options);
185
269
  return new Proxy(client, { get(target, prop) {
186
- if (prop === "session") return (state) => wrapSession(target.session(state), options);
270
+ if (prop === "sessions") return sessions;
187
271
  return bindOrReturn(target, prop);
188
272
  } });
189
273
  }
@@ -240,7 +324,7 @@ function reportError$1(onError, error) {
240
324
  } catch {}
241
325
  }
242
326
  function emit(event, ctx, level, message, attributes = {}, baseEvent = event) {
243
- const at = event.meta?.at;
327
+ const at = event.meta.at;
244
328
  log(message, {
245
329
  level,
246
330
  eventName: event.type,
@@ -264,10 +348,11 @@ function logEvent(event, ctx) {
264
348
  case "session.started": {
265
349
  const runtime = asRecord(data.runtime);
266
350
  const invocation = asRecord(data.invocation);
351
+ const trace = asRecord(data.trace);
267
352
  emit(event, ctx, "info", "Session started", {
268
353
  "eve.version": stringField(runtime, "eveVersion"),
269
- "gen_ai.request.model": stringField(runtime, "modelId"),
270
354
  "eve.agent.id": stringField(runtime, "agentId"),
355
+ "eve.trace.id": stringField(trace, "traceId"),
271
356
  "eve.parent.session_id": stringField(invocation, "parentSessionId"),
272
357
  "eve.parent.call_id": stringField(invocation, "parentCallId"),
273
358
  "eve.parent.turn_id": stringField(invocation, "parentTurnId"),
@@ -281,6 +366,9 @@ function logEvent(event, ctx) {
281
366
  case "message.received":
282
367
  emit(event, ctx, "debug", "User message received");
283
368
  return;
369
+ case "step.started":
370
+ emit(event, ctx, "debug", "Step started", { "gen_ai.request.model": stringField(data, "modelId") });
371
+ return;
284
372
  case "step.completed": {
285
373
  const usage = asRecord(data.usage);
286
374
  emit(event, ctx, "info", stepCompletedMessage(data), {
@@ -336,31 +424,7 @@ function logEvent(event, ctx) {
336
424
  "eve.subagent.name": stringField(data, "subagentName"),
337
425
  "gen_ai.tool.call.id": stringField(data, "callId")
338
426
  };
339
- if (childType === "step.failed") {
340
- emit(event, ctx, "error", `Subagent step failed: ${stringField(childData, "message") ?? ""}`, {
341
- ...attrs,
342
- "error.code": stringField(childData, "code"),
343
- "eve.error.details": jsonField(childData, "details")
344
- }, {
345
- ...child,
346
- data: childData,
347
- type: childType
348
- });
349
- return;
350
- }
351
- if (childType === "turn.failed") {
352
- emit(event, ctx, "error", `Subagent turn failed: ${stringField(childData, "message") ?? ""}`, {
353
- ...attrs,
354
- "error.code": stringField(childData, "code"),
355
- "eve.error.details": jsonField(childData, "details")
356
- }, {
357
- ...child,
358
- data: childData,
359
- type: childType
360
- });
361
- return;
362
- }
363
- if (childType === "session.failed") emit(event, ctx, "error", `Subagent session failed: ${stringField(childData, "message") ?? ""}`, {
427
+ if (childType === "step.failed" || childType === "turn.failed" || childType === "session.failed") emit(event, ctx, "error", `Subagent ${childType.replace(".", " ")}: ${stringField(childData, "message") ?? ""}`, {
364
428
  ...attrs,
365
429
  "error.code": stringField(childData, "code"),
366
430
  "eve.error.details": jsonField(childData, "details")
@@ -401,6 +465,9 @@ function logEvent(event, ctx) {
401
465
  case "turn.completed":
402
466
  emit(event, ctx, "info", "Turn completed");
403
467
  return;
468
+ case "turn.cancelled":
469
+ emit(event, ctx, "warn", "Turn cancelled");
470
+ return;
404
471
  case "turn.failed":
405
472
  emit(event, ctx, "error", `Turn failed: ${stringField(data, "message") ?? ""}`, {
406
473
  "error.code": stringField(data, "code"),
@@ -478,4 +545,22 @@ function telemetryDevInstrumentation(options = {}, overrides) {
478
545
  return definition;
479
546
  }
480
547
  //#endregion
481
- export { telemetryDevHook, telemetryDevInstrumentation, wrapEveClient };
548
+ //#region src/otel.ts
549
+ /**
550
+ * telemetry.dev as a destination in eve's `agent/instrumentation/` provider layout
551
+ * (`experimental.instrumentationProviders`). Export it from one file in that directory.
552
+ * eve owns the tracer provider there, so this attaches a processor instead of registering one.
553
+ */
554
+ function telemetryDevOtelIntegration(options = {}) {
555
+ const { recordInputs, recordOutputs, ...processorOptions } = options;
556
+ return otelIntegration({
557
+ recordInputs,
558
+ recordOutputs,
559
+ spanProcessors: [new TelemetrySpanProcessor({
560
+ ...processorOptions,
561
+ spanFilter: processorOptions.spanFilter ?? ((span) => isAiScope(span.instrumentationScope.name))
562
+ })]
563
+ });
564
+ }
565
+ //#endregion
566
+ export { telemetryDevHook, telemetryDevInstrumentation, telemetryDevOtelIntegration, wrapEveClient };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telemetry-dev/eve",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Eve (Vercel agent framework) telemetry integration for telemetry.dev.",
5
5
  "keywords": [
6
6
  "agents",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@opentelemetry/api": "^1.9.1",
40
- "@telemetry-dev/sdk": "^0.1.1"
40
+ "@telemetry-dev/sdk": "^0.1.2"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@opentelemetry/sdk-logs": "^0.218.0",
@@ -45,14 +45,14 @@
45
45
  "@types/node": "^25.5.0",
46
46
  "@typescript/native-preview": "7.0.0-dev.20260328.1",
47
47
  "ai": "7.0.0",
48
- "eve": "0.19.0",
48
+ "eve": "0.50.0",
49
49
  "typescript": "^6.0.2",
50
50
  "vite-plus": "0.1.20",
51
51
  "vitest": "npm:@voidzero-dev/vite-plus-test@0.1.20",
52
52
  "zod": "^4.4.3"
53
53
  },
54
54
  "peerDependencies": {
55
- "eve": ">=0.19.0 <1"
55
+ "eve": ">=0.47.0 <1"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "pnpm exec vp pack",
package/src/client.ts CHANGED
@@ -8,10 +8,11 @@ import {
8
8
  import type {
9
9
  Client,
10
10
  ClientSession,
11
- HandleMessageStreamEvent,
11
+ ClientSessions,
12
+ InputResponse,
13
+ MessageStreamEvent,
12
14
  SendTurnInput,
13
- SendTurnPayload,
14
- SessionState,
15
+ SendTurnOptions,
15
16
  } from "eve/client";
16
17
  import { isCurrentTurnBoundaryEvent, MessageResponse } from "eve/client";
17
18
 
@@ -48,7 +49,7 @@ function numberField(record: EventRecord | undefined, key: string): number | und
48
49
  return value?.constructor === Number ? value : undefined;
49
50
  }
50
51
 
51
- function eventData(event: HandleMessageStreamEvent): EventRecord {
52
+ function eventData(event: MessageStreamEvent): EventRecord {
52
53
  if (!("data" in event)) return {};
53
54
  return asRecord(event.data as EventValue) ?? {};
54
55
  }
@@ -75,16 +76,12 @@ function failureError(code: string | undefined, message: string | undefined): Er
75
76
  return error;
76
77
  }
77
78
 
78
- function inputForSpan<TOutput>(payload: SendTurnPayload<TOutput>) {
79
- return payload.message ?? payload.inputResponses;
80
- }
81
-
82
79
  function bindOrReturn<T extends object>(target: T, prop: string | symbol) {
83
80
  const value = target[prop as keyof T];
84
81
  return value instanceof Function ? value.bind(target) : value;
85
82
  }
86
83
 
87
- function addLifecycleEvent(span: SpanHandle, event: HandleMessageStreamEvent): void {
84
+ function addLifecycleEvent(span: SpanHandle, event: MessageStreamEvent): void {
88
85
  const data = eventData(event);
89
86
  switch (event.type) {
90
87
  case "subagent.called": {
@@ -125,19 +122,25 @@ async function* instrumentedStream<TOutput>(
125
122
  response: MessageResponse<TOutput>,
126
123
  span: SpanHandle,
127
124
  start: number,
128
- payload: SendTurnPayload<TOutput>,
129
- ): AsyncGenerator<HandleMessageStreamEvent> {
125
+ signal: AbortSignal | undefined,
126
+ endCancelled: (() => void) | undefined,
127
+ ): AsyncGenerator<MessageStreamEvent> {
130
128
  const usage: TokenUsage = {};
131
129
  let sawUsage = false;
132
130
  let output: unknown;
133
131
  let finishReason: string | undefined;
132
+ let costUsd: number | undefined;
133
+ let allCostsKnown = true;
134
134
  let timeToFirstChunkMs: number | undefined;
135
135
  let error: unknown;
136
+ let done = false;
136
137
  let sawTerminalEvent = false;
137
138
 
138
139
  const finish = (): void => {
140
+ if (done) return;
141
+ done = true;
139
142
  if (!sawTerminalEvent && error === undefined) {
140
- if (payload.signal?.aborted) {
143
+ if (signal?.aborted) {
141
144
  error = failureError("cancelled", "cancelled");
142
145
  finishReason ??= "cancelled";
143
146
  } else {
@@ -148,6 +151,10 @@ async function* instrumentedStream<TOutput>(
148
151
  span.end({
149
152
  usage: sawUsage ? usage : undefined,
150
153
  finishReason,
154
+ costUsd:
155
+ sawTerminalEvent && error === undefined && allCostsKnown && Number.isFinite(costUsd)
156
+ ? costUsd
157
+ : undefined,
151
158
  output,
152
159
  timeToFirstChunkMs,
153
160
  error: error instanceof Error ? error : error === undefined ? undefined : new Error("error"),
@@ -157,6 +164,16 @@ async function* instrumentedStream<TOutput>(
157
164
  try {
158
165
  for await (const event of response) {
159
166
  const data = eventData(event);
167
+ if (event.type === "turn.started") {
168
+ const turnTrace = asRecord(data.trace);
169
+ const traceId = stringField(turnTrace, "traceId");
170
+ const spanId = stringField(turnTrace, "spanId");
171
+ const turnId = stringField(data, "turnId");
172
+ if (traceId && spanId && turnId) {
173
+ span.span.setAttribute("td.eve.turn_root", `${traceId}/${spanId}`);
174
+ span.span.setAttribute("eve.turn.id", turnId);
175
+ }
176
+ }
160
177
  if (isCurrentTurnBoundaryEvent(event)) sawTerminalEvent = true;
161
178
  if (
162
179
  timeToFirstChunkMs === undefined &&
@@ -180,6 +197,12 @@ async function* instrumentedStream<TOutput>(
180
197
  "cacheCreationInputTokens",
181
198
  numberField(eventUsage, "cacheWriteTokens"),
182
199
  ) || sawUsage;
200
+ const stepCost = numberField(eventUsage, "costUsd");
201
+ if (stepCost === undefined || !Number.isFinite(stepCost) || stepCost < 0) {
202
+ allCostsKnown = false;
203
+ } else {
204
+ costUsd = (costUsd ?? 0) + stepCost;
205
+ }
183
206
  finishReason = stringField(data, "finishReason") ?? finishReason;
184
207
  }
185
208
 
@@ -204,12 +227,16 @@ async function* instrumentedStream<TOutput>(
204
227
  }
205
228
  finishReason = "error";
206
229
  }
230
+ if (event.type === "turn.cancelled") {
231
+ error = failureError("cancelled", "cancelled");
232
+ finishReason = "cancelled";
233
+ }
207
234
 
208
235
  addLifecycleEvent(span, event);
209
236
  yield event;
210
237
  }
211
238
  } catch (caught) {
212
- if (payload.signal?.aborted) {
239
+ if (signal?.aborted) {
213
240
  error = failureError("cancelled", "cancelled");
214
241
  finishReason ??= "cancelled";
215
242
  } else {
@@ -218,52 +245,116 @@ async function* instrumentedStream<TOutput>(
218
245
  throw caught;
219
246
  } finally {
220
247
  finish();
248
+ if (endCancelled) signal?.removeEventListener("abort", endCancelled);
249
+ }
250
+ }
251
+
252
+ async function tracedTurn<TOutput>(
253
+ options: WrapEveClientOptions,
254
+ input: SendTurnInput["message"] | readonly InputResponse[],
255
+ turnOptions: SendTurnOptions<TOutput> | undefined,
256
+ knownSessionId: string | undefined,
257
+ send: () => Promise<MessageResponse<TOutput>>,
258
+ ): Promise<MessageResponse<TOutput>> {
259
+ // The session id is only known after the POST (first turn), so the span starts once the
260
+ // request resolves with the original start time; that lets it join the session trace.
261
+ const startTime = new Date();
262
+ const start = performance.now();
263
+ const signal = turnOptions?.signal;
264
+ const spanFor = (sessionId: string | undefined) =>
265
+ startSpan(options.spanName ?? "invoke_agent", {
266
+ type: "agent",
267
+ agentName: options.agentName,
268
+ input,
269
+ startTime,
270
+ attributes: sessionId === undefined ? undefined : { "gen_ai.conversation.id": sessionId },
271
+ });
272
+ let response: MessageResponse<TOutput>;
273
+ try {
274
+ response = await send();
275
+ } catch (error) {
276
+ const span = spanFor(knownSessionId);
277
+ if (signal?.aborted) {
278
+ span.end({ error: failureError("cancelled", "cancelled"), finishReason: "cancelled" });
279
+ } else {
280
+ span.end({ error: error instanceof Error ? error : new Error(String(error)) });
281
+ }
282
+ throw error;
283
+ }
284
+ const span = spanFor(response.sessionId);
285
+ let streamStarted = false;
286
+ let endedBeforeStream = false;
287
+ const endCancelled = () => {
288
+ if (streamStarted) return;
289
+ endedBeforeStream = true;
290
+ span.end({ error: failureError("cancelled", "cancelled"), finishReason: "cancelled" });
291
+ };
292
+ if (signal?.aborted) {
293
+ endCancelled();
294
+ } else {
295
+ signal?.addEventListener("abort", endCancelled, { once: true });
221
296
  }
297
+ // MessageResponse's constructor is @internal in eve; re-wrapping the stream has no
298
+ // public alternative. cancel() forwards to the original response, whose turn id
299
+ // resolves because instrumentedStream consumes it.
300
+ return new MessageResponse<TOutput>({
301
+ cancelTurn: () => response.cancel(),
302
+ createStream: () => {
303
+ streamStarted = true;
304
+ // The span already ended on abort; hand back the raw stream instead of ending it twice.
305
+ if (endedBeforeStream)
306
+ return (async function* () {
307
+ yield* response;
308
+ })();
309
+ return instrumentedStream(response, span, start, signal, endCancelled);
310
+ },
311
+ sessionId: response.sessionId,
312
+ });
222
313
  }
223
314
 
224
315
  function wrapSession(session: ClientSession, options: WrapEveClientOptions): ClientSession {
225
316
  return new Proxy(session, {
226
317
  get(target, prop) {
227
318
  if (prop === "send") {
228
- const wrappedSend: ClientSession["send"] = async <TOutput = unknown>(
319
+ const send: ClientSession["send"] = (message, turnOptions) =>
320
+ tracedTurn(options, message, turnOptions, target.state.sessionId, () =>
321
+ target.send(message, turnOptions),
322
+ );
323
+ return send;
324
+ }
325
+ if (prop === "respond") {
326
+ const respond: ClientSession["respond"] = (inputResponses, turnOptions) =>
327
+ tracedTurn(options, inputResponses, turnOptions, target.state.sessionId, () =>
328
+ target.respond(inputResponses, turnOptions),
329
+ );
330
+ return respond;
331
+ }
332
+ return bindOrReturn(target, prop);
333
+ },
334
+ });
335
+ }
336
+
337
+ function wrapSessions(sessions: ClientSessions, options: WrapEveClientOptions): ClientSessions {
338
+ return new Proxy(sessions, {
339
+ get(target, prop) {
340
+ if (prop === "create") {
341
+ const create: ClientSessions["create"] = async <TOutput = unknown>(
229
342
  input: SendTurnInput<TOutput>,
230
343
  ) => {
231
- const payload =
232
- input instanceof String || input?.constructor === String
233
- ? { message: String(input) }
234
- : input;
235
- const turnPayload = payload as SendTurnPayload<TOutput>;
236
- const span = startSpan(options.spanName ?? "invoke_agent", {
237
- type: "agent",
238
- agentName: options.agentName,
239
- input: inputForSpan(turnPayload),
240
- });
241
- const start = performance.now();
242
- let response: MessageResponse<TOutput>;
243
- try {
244
- response = await target.send(input);
245
- } catch (error) {
246
- if (turnPayload.signal?.aborted) {
247
- span.end({
248
- error: failureError("cancelled", "cancelled"),
249
- finishReason: "cancelled",
250
- });
251
- } else {
252
- span.end({ error: error instanceof Error ? error : new Error(String(error)) });
253
- }
254
- throw error;
255
- }
256
-
257
- span.span.setAttribute("gen_ai.conversation.id", response.sessionId);
258
- // MessageResponse's constructor is @internal in eve; re-wrapping the stream has no
259
- // public alternative. Revisit on eve upgrades.
260
- return new MessageResponse<TOutput>({
261
- continuationToken: response.continuationToken,
262
- sessionId: response.sessionId,
263
- createStream: () => instrumentedStream(response, span, start, turnPayload),
344
+ let session: ClientSession | undefined;
345
+ const response = await tracedTurn(options, input.message, input, undefined, async () => {
346
+ const created = await target.create(input);
347
+ session = created.session;
348
+ return created.response;
264
349
  });
350
+ return { response, session: wrapSession(session!, options) };
265
351
  };
266
- return wrappedSend;
352
+ return create;
353
+ }
354
+ if (prop === "attach") {
355
+ const attach: ClientSessions["attach"] = (sessionId, attachOptions) =>
356
+ wrapSession(target.attach(sessionId, attachOptions), options);
357
+ return attach;
267
358
  }
268
359
  return bindOrReturn(target, prop);
269
360
  },
@@ -277,11 +368,10 @@ export function wrapEveClient<C extends Client>(
277
368
  ): C {
278
369
  const { agentName: _agentName, spanName: _spanName, ...sdkOptions } = options;
279
370
  ensureInit(sdkOptions, overrides);
371
+ const sessions = wrapSessions(client.sessions, options);
280
372
  return new Proxy(client, {
281
373
  get(target, prop) {
282
- if (prop === "session") {
283
- return (state?: SessionState | string) => wrapSession(target.session(state), options);
284
- }
374
+ if (prop === "sessions") return sessions;
285
375
  return bindOrReturn(target, prop);
286
376
  },
287
377
  }) as C;
package/src/config.ts CHANGED
@@ -1,4 +1,10 @@
1
- import { init, shutdown, type ClientOverrides, type TelemetryOptions } from "@telemetry-dev/sdk";
1
+ import {
2
+ init,
3
+ SCOPE_NAME,
4
+ shutdown,
5
+ type ClientOverrides,
6
+ type TelemetryOptions,
7
+ } from "@telemetry-dev/sdk";
2
8
 
3
9
  /** Options accepted by every @telemetry-dev/eve entry point. */
4
10
  export type TelemetryDevEveOptions = Omit<TelemetryOptions, "registerGlobal" | "sdkName">;
@@ -7,12 +13,25 @@ export type { ClientOverrides };
7
13
 
8
14
  let initialized = false;
9
15
 
16
+ // eve's agent spans ("eve" in agent/instrumentation.ts, "eve.agent" in the provider layout), the
17
+ // AI SDK's model-call and tool spans ("gen_ai", emitted by @ai-sdk/otel), and this SDK's own
18
+ // client-wrapper spans. Everything else on the global provider (better-auth, Nitro, eve's
19
+ // "workflow" engine) is not AI telemetry.
20
+ const AI_SCOPES = {
21
+ eve: true,
22
+ "eve.agent": true,
23
+ gen_ai: true,
24
+ [SCOPE_NAME]: true,
25
+ } satisfies Record<string, true>;
26
+
27
+ export const isAiScope = (scope: string): boolean => Object.hasOwn(AI_SCOPES, scope);
28
+
10
29
  /**
11
30
  * Initializes the telemetry.dev SDK exactly once per process for this integration.
12
- * registerGlobal:true lets eve's tracers (scopes "eve" and "gen_ai") resolve to
13
- * the SDK provider. spanFilter defaults to exporting everything except eve's
14
- * internal workflow-engine spans (scope "workflow"); pass spanFilter: () => true
15
- * to export those too.
31
+ * registerGlobal:true lets eve's tracers resolve to the SDK provider. spanFilter defaults to
32
+ * exporting only AI scopes (see AI_SCOPES); pass spanFilter: () => true to export every span.
33
+ * eve starts each `ai.eve.turn` under a workflow-engine span of its own trace, so the turn is
34
+ * re-rooted under the session parent and every turn of a session lands in one trace.
16
35
  */
17
36
  export function ensureInit(
18
37
  options: TelemetryDevEveOptions = {},
@@ -25,7 +44,11 @@ export function ensureInit(
25
44
  ...options,
26
45
  sdkName: "@telemetry-dev/eve",
27
46
  registerGlobal: true,
28
- spanFilter: options.spanFilter ?? ((span) => span.instrumentationScope.name !== "workflow"),
47
+ spanFilter: options.spanFilter ?? ((span) => isAiScope(span.instrumentationScope.name)),
48
+ sessionRootOf: (name, attributes) => {
49
+ const sessionId = attributes["eve.session.id"];
50
+ return name === "ai.eve.turn" && typeof sessionId === "string" ? sessionId : undefined;
51
+ },
29
52
  },
30
53
  overrides,
31
54
  );
package/src/hook.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { log, type LogLevel } from "@telemetry-dev/sdk";
2
- import type { HandleMessageStreamEvent } from "eve/client";
2
+ import type { MessageStreamEvent } from "eve/client";
3
3
  import type { HookContext, HookDefinition } from "eve/hooks";
4
4
 
5
5
  import { ensureInit, type ClientOverrides, type TelemetryDevEveOptions } from "./config.ts";
@@ -12,7 +12,7 @@ function asRecord(value: Value): Attrs | undefined {
12
12
  return value as Attrs;
13
13
  }
14
14
 
15
- function eventData(event: HandleMessageStreamEvent): Attrs {
15
+ function eventData(event: MessageStreamEvent): Attrs {
16
16
  if (!("data" in event)) return {};
17
17
  return asRecord(event.data as Value) ?? {};
18
18
  }
@@ -37,7 +37,7 @@ function jsonField(record: Attrs | undefined, key: string): string | undefined {
37
37
  }
38
38
  }
39
39
 
40
- function baseAttributes(event: HandleMessageStreamEvent, ctx: HookContext) {
40
+ function baseAttributes(event: MessageStreamEvent, ctx: HookContext) {
41
41
  const data = eventData(event);
42
42
  return {
43
43
  "gen_ai.conversation.id": ctx.session.id,
@@ -69,14 +69,14 @@ function reportError(onError: ((error: Error) => void) | undefined, error: Error
69
69
  }
70
70
 
71
71
  function emit(
72
- event: HandleMessageStreamEvent,
72
+ event: MessageStreamEvent,
73
73
  ctx: HookContext,
74
74
  level: LogLevel,
75
75
  message: string,
76
76
  attributes: Attrs = {},
77
- baseEvent: HandleMessageStreamEvent = event,
77
+ baseEvent: MessageStreamEvent = event,
78
78
  ): void {
79
- const at = event.meta?.at;
79
+ const at = event.meta.at;
80
80
  log(message, {
81
81
  level,
82
82
  eventName: event.type,
@@ -97,17 +97,18 @@ function stepCompletedMessage(data: Attrs): string {
97
97
  return `Step completed (${finishReason})${suffix}`;
98
98
  }
99
99
 
100
- function logEvent(event: HandleMessageStreamEvent, ctx: HookContext): void {
100
+ function logEvent(event: MessageStreamEvent, ctx: HookContext): void {
101
101
  const data = eventData(event);
102
102
 
103
103
  switch (event.type) {
104
104
  case "session.started": {
105
105
  const runtime = asRecord(data.runtime);
106
106
  const invocation = asRecord(data.invocation);
107
+ const trace = asRecord(data.trace);
107
108
  emit(event, ctx, "info", "Session started", {
108
109
  "eve.version": stringField(runtime, "eveVersion"),
109
- "gen_ai.request.model": stringField(runtime, "modelId"),
110
110
  "eve.agent.id": stringField(runtime, "agentId"),
111
+ "eve.trace.id": stringField(trace, "traceId"),
111
112
  "eve.parent.session_id": stringField(invocation, "parentSessionId"),
112
113
  "eve.parent.call_id": stringField(invocation, "parentCallId"),
113
114
  "eve.parent.turn_id": stringField(invocation, "parentTurnId"),
@@ -121,6 +122,11 @@ function logEvent(event: HandleMessageStreamEvent, ctx: HookContext): void {
121
122
  case "message.received":
122
123
  emit(event, ctx, "debug", "User message received");
123
124
  return;
125
+ case "step.started":
126
+ emit(event, ctx, "debug", "Step started", {
127
+ "gen_ai.request.model": stringField(data, "modelId"),
128
+ });
129
+ return;
124
130
  case "step.completed": {
125
131
  const usage = asRecord(data.usage);
126
132
  emit(event, ctx, "info", stepCompletedMessage(data), {
@@ -194,31 +200,16 @@ function logEvent(event: HandleMessageStreamEvent, ctx: HookContext): void {
194
200
  "eve.subagent.name": stringField(data, "subagentName"),
195
201
  "gen_ai.tool.call.id": stringField(data, "callId"),
196
202
  };
197
- if (childType === "step.failed") {
198
- emit(
199
- event,
200
- ctx,
201
- "error",
202
- `Subagent step failed: ${stringField(childData, "message") ?? ""}`,
203
- {
204
- ...attrs,
205
- "error.code": stringField(childData, "code"),
206
- "eve.error.details": jsonField(childData, "details"),
207
- },
208
- {
209
- ...child,
210
- data: childData,
211
- type: childType,
212
- } as HandleMessageStreamEvent,
213
- );
214
- return;
215
- }
216
- if (childType === "turn.failed") {
203
+ if (
204
+ childType === "step.failed" ||
205
+ childType === "turn.failed" ||
206
+ childType === "session.failed"
207
+ ) {
217
208
  emit(
218
209
  event,
219
210
  ctx,
220
211
  "error",
221
- `Subagent turn failed: ${stringField(childData, "message") ?? ""}`,
212
+ `Subagent ${childType.replace(".", " ")}: ${stringField(childData, "message") ?? ""}`,
222
213
  {
223
214
  ...attrs,
224
215
  "error.code": stringField(childData, "code"),
@@ -228,26 +219,7 @@ function logEvent(event: HandleMessageStreamEvent, ctx: HookContext): void {
228
219
  ...child,
229
220
  data: childData,
230
221
  type: childType,
231
- } as HandleMessageStreamEvent,
232
- );
233
- return;
234
- }
235
- if (childType === "session.failed") {
236
- emit(
237
- event,
238
- ctx,
239
- "error",
240
- `Subagent session failed: ${stringField(childData, "message") ?? ""}`,
241
- {
242
- ...attrs,
243
- "error.code": stringField(childData, "code"),
244
- "eve.error.details": jsonField(childData, "details"),
245
- },
246
- {
247
- ...child,
248
- data: childData,
249
- type: childType,
250
- } as HandleMessageStreamEvent,
222
+ } as MessageStreamEvent,
251
223
  );
252
224
  }
253
225
  return;
@@ -284,6 +256,9 @@ function logEvent(event: HandleMessageStreamEvent, ctx: HookContext): void {
284
256
  case "turn.completed":
285
257
  emit(event, ctx, "info", "Turn completed");
286
258
  return;
259
+ case "turn.cancelled":
260
+ emit(event, ctx, "warn", "Turn cancelled");
261
+ return;
287
262
  case "turn.failed":
288
263
  emit(event, ctx, "error", `Turn failed: ${stringField(data, "message") ?? ""}`, {
289
264
  "error.code": stringField(data, "code"),
package/src/index.ts CHANGED
@@ -5,3 +5,4 @@ export {
5
5
  telemetryDevInstrumentation,
6
6
  type TelemetryDevInstrumentationOptions,
7
7
  } from "./instrumentation.ts";
8
+ export { telemetryDevOtelIntegration, type TelemetryDevOtelIntegrationOptions } from "./otel.ts";
package/src/otel.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { TelemetrySpanProcessor, type TelemetrySpanProcessorOptions } from "@telemetry-dev/sdk";
2
+ import { otelIntegration, type OtelIntegration } from "eve/instrumentation/otel";
3
+
4
+ import { isAiScope } from "./config.ts";
5
+
6
+ export interface TelemetryDevOtelIntegrationOptions extends Omit<
7
+ TelemetrySpanProcessorOptions,
8
+ "spanExporter"
9
+ > {
10
+ recordInputs?: boolean;
11
+ recordOutputs?: boolean;
12
+ }
13
+
14
+ /**
15
+ * telemetry.dev as a destination in eve's `agent/instrumentation/` provider layout
16
+ * (`experimental.instrumentationProviders`). Export it from one file in that directory.
17
+ * eve owns the tracer provider there, so this attaches a processor instead of registering one.
18
+ */
19
+ export function telemetryDevOtelIntegration(
20
+ options: TelemetryDevOtelIntegrationOptions = {},
21
+ ): OtelIntegration {
22
+ const { recordInputs, recordOutputs, ...processorOptions } = options;
23
+ return otelIntegration({
24
+ recordInputs,
25
+ recordOutputs,
26
+ spanProcessors: [
27
+ new TelemetrySpanProcessor({
28
+ ...processorOptions,
29
+ spanFilter:
30
+ processorOptions.spanFilter ?? ((span) => isAiScope(span.instrumentationScope.name)),
31
+ }),
32
+ ],
33
+ });
34
+ }