@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 +59 -18
- package/dist/index.d.mts +15 -2
- package/dist/index.mjs +152 -67
- package/package.json +4 -4
- package/src/client.ts +141 -51
- package/src/config.ts +29 -6
- package/src/hook.ts +24 -49
- package/src/index.ts +1 -0
- package/src/otel.ts +34 -0
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.
|
|
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.
|
|
97
|
-
|
|
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()`
|
|
102
|
-
records the outgoing message or HITL
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
`spanFilter: () => true` to export
|
|
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
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
-
|
|
139
|
-
|
|
140
|
-
- The client wrapper
|
|
141
|
-
|
|
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
|
-
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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,
|
|
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 (
|
|
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 (
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
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 === "
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
11
|
+
ClientSessions,
|
|
12
|
+
InputResponse,
|
|
13
|
+
MessageStreamEvent,
|
|
12
14
|
SendTurnInput,
|
|
13
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
129
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
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 === "
|
|
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 {
|
|
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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
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 {
|
|
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:
|
|
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:
|
|
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:
|
|
72
|
+
event: MessageStreamEvent,
|
|
73
73
|
ctx: HookContext,
|
|
74
74
|
level: LogLevel,
|
|
75
75
|
message: string,
|
|
76
76
|
attributes: Attrs = {},
|
|
77
|
-
baseEvent:
|
|
77
|
+
baseEvent: MessageStreamEvent = event,
|
|
78
78
|
): void {
|
|
79
|
-
const at = event.meta
|
|
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:
|
|
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 (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
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
|
|
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
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
|
+
}
|