@telemetry-dev/eve 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/dist/index.d.mts +37 -0
- package/dist/index.mjs +465 -0
- package/package.json +63 -0
- package/src/client.ts +277 -0
- package/src/config.ts +37 -0
- package/src/hook.ts +313 -0
- package/src/index.ts +7 -0
- package/src/instrumentation.ts +82 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 telemetry.dev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# @telemetry-dev/eve
|
|
2
|
+
|
|
3
|
+
Eve telemetry integration for [telemetry.dev](https://telemetry.dev). It wires Vercel's
|
|
4
|
+
[Eve](https://eve.dev) agent framework to telemetry.dev through Eve's native OpenTelemetry,
|
|
5
|
+
lifecycle-hook, and HTTP-client surfaces.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @telemetry-dev/eve eve
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires `eve >=0.19.0 <1` and Node.js 24 or newer.
|
|
14
|
+
|
|
15
|
+
## Environment
|
|
16
|
+
|
|
17
|
+
| Variable | Required | Default | Notes |
|
|
18
|
+
| --------------------------- | -------- | ------------------------------ | ----------------------------------------------------------------------- |
|
|
19
|
+
| `TELEMETRY_DEV_API_KEY` | yes | — | Ingest key (`td_live_…`). No key ⇒ the integration is a complete no-op. |
|
|
20
|
+
| `TELEMETRY_DEV_BASE_URL` | no | `https://ingest.telemetry.dev` | Trailing slashes are stripped. |
|
|
21
|
+
| `TELEMETRY_DEV_ENVIRONMENT` | no | `production` | Environment label on every trace/log. |
|
|
22
|
+
| `OTEL_SERVICE_NAME` | no | SDK default | Server instrumentation falls back to the Eve agent name after this env. |
|
|
23
|
+
|
|
24
|
+
All four are also settable through SDK options. Pass SDK options to exactly one entry point in a
|
|
25
|
+
process; whichever initializes first wins. If you only use `telemetryDevHook()` or
|
|
26
|
+
`wrapEveClient()`, pass `serviceName` explicitly or set `OTEL_SERVICE_NAME`.
|
|
27
|
+
|
|
28
|
+
## Server instrumentation
|
|
29
|
+
|
|
30
|
+
Create `agent/instrumentation.ts`:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { telemetryDevInstrumentation } from "@telemetry-dev/eve";
|
|
34
|
+
|
|
35
|
+
export default telemetryDevInstrumentation();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This registers telemetry.dev as the global OpenTelemetry provider during Eve startup, so Eve's tracer
|
|
39
|
+
scope (`"eve"`) and the AI SDK GenAI tracer scope (`"gen_ai"`) export to telemetry.dev.
|
|
40
|
+
|
|
41
|
+
Useful options:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
export default telemetryDevInstrumentation({
|
|
45
|
+
functionId: "support-agent",
|
|
46
|
+
recordInputs: false,
|
|
47
|
+
recordOutputs: true,
|
|
48
|
+
runtimeContext: { "team.id": "support" },
|
|
49
|
+
stepStarted(input) {
|
|
50
|
+
return { runtimeContext: { "channel.kind": input.channel.kind } };
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`recordInputs` defaults to `captureInput ?? true`; `recordOutputs` defaults to
|
|
56
|
+
`captureOutput ?? true`. Runtime context is attached by Eve to model-call spans under the
|
|
57
|
+
`ai.settings.context.` prefix. The integration also adds `user.id` when Eve auth exposes a principal.
|
|
58
|
+
|
|
59
|
+
## Lifecycle logs
|
|
60
|
+
|
|
61
|
+
Create `agent/hooks/telemetry-dev.ts`:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { telemetryDevHook } from "@telemetry-dev/eve";
|
|
65
|
+
|
|
66
|
+
export default telemetryDevHook();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The hook emits bounded lifecycle logs for:
|
|
70
|
+
|
|
71
|
+
- session start/completion/failure;
|
|
72
|
+
- turn start/completion/failure;
|
|
73
|
+
- user message receipt without message content;
|
|
74
|
+
- step completion/failure with usage where available;
|
|
75
|
+
- non-completed tool results;
|
|
76
|
+
- HITL input requests and authorization outcomes;
|
|
77
|
+
- subagent calls/completions;
|
|
78
|
+
- compaction requests/completions.
|
|
79
|
+
|
|
80
|
+
Every log includes `gen_ai.conversation.id`, `gen_ai.agent.name`, Eve turn/step ids when present, and
|
|
81
|
+
`eventName` equal to the Eve stream-event type. Hook handlers are fail-open; instrumentation errors are
|
|
82
|
+
routed to `onError` and never thrown back into Eve.
|
|
83
|
+
|
|
84
|
+
## Client wrapper
|
|
85
|
+
|
|
86
|
+
For apps that call an Eve agent over HTTP:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { wrapEveClient } from "@telemetry-dev/eve";
|
|
90
|
+
import { Client } from "eve/client";
|
|
91
|
+
|
|
92
|
+
const client = wrapEveClient(new Client({ host: "http://127.0.0.1:3000" }), {
|
|
93
|
+
agentName: "support-agent",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const session = client.session();
|
|
97
|
+
const response = await session.send("Summarize this incident.");
|
|
98
|
+
const result = await response.result();
|
|
99
|
+
```
|
|
100
|
+
|
|
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.
|
|
105
|
+
|
|
106
|
+
`ClientSession.stream()`, `Client.info()`, and `Client.health()` are intentionally not spanned.
|
|
107
|
+
Attach-streams are unbounded and the probe routes are not agent turns.
|
|
108
|
+
|
|
109
|
+
## Trace shape
|
|
110
|
+
|
|
111
|
+
A typical turn contains:
|
|
112
|
+
|
|
113
|
+
- Eve server root span `ai.eve.turn`, normalized by telemetry.dev as `invoke_agent`.
|
|
114
|
+
- Nested AI SDK GenAI spans: `chat {modelId}`, `step N`, provider `chat {modelId}` client spans, and
|
|
115
|
+
`execute_tool {toolName}` spans.
|
|
116
|
+
- Usage attributes such as `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, cache token
|
|
117
|
+
counts, finish reasons, response ids, TTFT, and time-per-output-chunk when Eve's AI SDK integration
|
|
118
|
+
emits them.
|
|
119
|
+
- Runtime context attributes under `ai.settings.context.*`, including Eve session/turn/step/channel
|
|
120
|
+
context and any custom runtime context you return from `step.started`.
|
|
121
|
+
- Lifecycle logs joined to the same session via `gen_ai.conversation.id`.
|
|
122
|
+
|
|
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.
|
|
126
|
+
|
|
127
|
+
## Content capture
|
|
128
|
+
|
|
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.
|
|
132
|
+
|
|
133
|
+
## Limitations
|
|
134
|
+
|
|
135
|
+
- Pass options to exactly one entry point in a process. Initialization is one-shot so duplicate entry
|
|
136
|
+
points do not replace the first configuration.
|
|
137
|
+
- `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.
|
|
142
|
+
- Inline subagent child streams (`subagent.event`) are not fanned out in full; the hook logs
|
|
143
|
+
`subagent.started`, `subagent.called`, `subagent.completed`, and child failure events.
|
|
144
|
+
- Eve workflow `$eve.*` run tags are a Vercel-dashboard surface and are not visible to OpenTelemetry,
|
|
145
|
+
so this package does not capture them.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ClientOverrides, ClientOverrides as ClientOverrides$1, TelemetryOptions } from "@telemetry-dev/sdk";
|
|
2
|
+
import { Client } from "eve/client";
|
|
3
|
+
import { HookDefinition } from "eve/hooks";
|
|
4
|
+
import { InstrumentationDefinition, InstrumentationEvents, InstrumentationRuntimeContext } from "eve/instrumentation";
|
|
5
|
+
|
|
6
|
+
//#region src/config.d.ts
|
|
7
|
+
/** Options accepted by every @telemetry-dev/eve entry point. */
|
|
8
|
+
type TelemetryDevEveOptions = Omit<TelemetryOptions, "registerGlobal">;
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/client.d.ts
|
|
11
|
+
interface WrapEveClientOptions extends TelemetryDevEveOptions {
|
|
12
|
+
/** gen_ai.agent.name on turn spans. */
|
|
13
|
+
agentName?: string;
|
|
14
|
+
/** Span name for each send() turn. Default "invoke_agent". */
|
|
15
|
+
spanName?: string;
|
|
16
|
+
}
|
|
17
|
+
declare function wrapEveClient<C extends Client>(client: C, options?: WrapEveClientOptions, overrides?: ClientOverrides): C;
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/hook.d.ts
|
|
20
|
+
declare function telemetryDevHook(options?: TelemetryDevEveOptions, overrides?: ClientOverrides$1): HookDefinition;
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/instrumentation.d.ts
|
|
23
|
+
interface TelemetryDevInstrumentationOptions extends TelemetryDevEveOptions {
|
|
24
|
+
/** ai.telemetry.functionId override; eve defaults it to the agent name. */
|
|
25
|
+
functionId?: string;
|
|
26
|
+
/** Record model inputs on spans. Default: captureInput ?? true. */
|
|
27
|
+
recordInputs?: boolean;
|
|
28
|
+
/** Record model outputs on spans. Default: captureOutput ?? true. */
|
|
29
|
+
recordOutputs?: boolean;
|
|
30
|
+
/** Static runtime context merged into every model-call span. */
|
|
31
|
+
runtimeContext?: InstrumentationRuntimeContext;
|
|
32
|
+
/** User step.started callback, composed with the integration's own. */
|
|
33
|
+
stepStarted?: InstrumentationEvents["step.started"];
|
|
34
|
+
}
|
|
35
|
+
declare function telemetryDevInstrumentation(options?: TelemetryDevInstrumentationOptions, overrides?: ClientOverrides$1): InstrumentationDefinition;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { type TelemetryDevEveOptions, type TelemetryDevInstrumentationOptions, type WrapEveClientOptions, telemetryDevHook, telemetryDevInstrumentation, wrapEveClient };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { init, log, startSpan } from "@telemetry-dev/sdk";
|
|
2
|
+
import { MessageResponse, isCurrentTurnBoundaryEvent } from "eve/client";
|
|
3
|
+
//#region src/config.ts
|
|
4
|
+
let initialized = false;
|
|
5
|
+
/**
|
|
6
|
+
* 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.
|
|
11
|
+
*/
|
|
12
|
+
function ensureInit(options = {}, overrides) {
|
|
13
|
+
if (initialized) return;
|
|
14
|
+
initialized = true;
|
|
15
|
+
init({
|
|
16
|
+
...options,
|
|
17
|
+
registerGlobal: true,
|
|
18
|
+
spanFilter: options.spanFilter ?? ((span) => span.instrumentationScope.name !== "workflow")
|
|
19
|
+
}, overrides);
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/client.ts
|
|
23
|
+
function asRecord$1(value) {
|
|
24
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function stringField$1(record, key) {
|
|
28
|
+
const value = record?.[key];
|
|
29
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
30
|
+
}
|
|
31
|
+
function numberField$1(record, key) {
|
|
32
|
+
const value = record?.[key];
|
|
33
|
+
return typeof value === "number" ? value : void 0;
|
|
34
|
+
}
|
|
35
|
+
function eventData$1(event) {
|
|
36
|
+
if (!("data" in event)) return {};
|
|
37
|
+
return asRecord$1(event.data) ?? {};
|
|
38
|
+
}
|
|
39
|
+
function eventAttributes(attributes) {
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [key, value] of Object.entries(attributes)) if (value !== void 0) out[key] = value;
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
function addUsage(usage, key, value) {
|
|
45
|
+
if (value === void 0) return false;
|
|
46
|
+
usage[key] = (usage[key] ?? 0) + value;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
function failureError(code, message) {
|
|
50
|
+
const error = new Error(message ?? code ?? "error");
|
|
51
|
+
if (code) error.name = code;
|
|
52
|
+
return error;
|
|
53
|
+
}
|
|
54
|
+
function inputForSpan(payload) {
|
|
55
|
+
return payload.message ?? payload.inputResponses;
|
|
56
|
+
}
|
|
57
|
+
function bindOrReturn(target, prop) {
|
|
58
|
+
const value = Reflect.get(target, prop, target);
|
|
59
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
60
|
+
}
|
|
61
|
+
function addLifecycleEvent(span, event) {
|
|
62
|
+
const data = eventData$1(event);
|
|
63
|
+
switch (event.type) {
|
|
64
|
+
case "subagent.called": {
|
|
65
|
+
const remote = asRecord$1(data.remote);
|
|
66
|
+
span.span.addEvent(event.type, eventAttributes({
|
|
67
|
+
"eve.subagent.name": stringField$1(data, "name"),
|
|
68
|
+
"gen_ai.tool.name": stringField$1(data, "toolName"),
|
|
69
|
+
"gen_ai.tool.call.id": stringField$1(data, "callId"),
|
|
70
|
+
"eve.child.session_id": stringField$1(data, "childSessionId"),
|
|
71
|
+
"eve.workflow.id": stringField$1(data, "workflowId"),
|
|
72
|
+
"eve.remote.url": stringField$1(remote, "url")
|
|
73
|
+
}));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
case "input.requested": {
|
|
77
|
+
const requests = Array.isArray(data.requests) ? data.requests : void 0;
|
|
78
|
+
span.span.addEvent(event.type, eventAttributes({ "eve.input.request_count": requests?.length }));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
case "authorization.required":
|
|
82
|
+
span.span.addEvent(event.type, eventAttributes({ "eve.authorization.name": stringField$1(data, "name") }));
|
|
83
|
+
return;
|
|
84
|
+
default: return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function* instrumentedStream(response, span, start, payload) {
|
|
88
|
+
const usage = {};
|
|
89
|
+
let sawUsage = false;
|
|
90
|
+
let output;
|
|
91
|
+
let finishReason;
|
|
92
|
+
let timeToFirstChunkMs;
|
|
93
|
+
let error;
|
|
94
|
+
let sawTerminalEvent = false;
|
|
95
|
+
const finish = () => {
|
|
96
|
+
if (!sawTerminalEvent && error === void 0) if (payload.signal?.aborted) {
|
|
97
|
+
error = failureError("cancelled", "cancelled");
|
|
98
|
+
finishReason ??= "cancelled";
|
|
99
|
+
} else {
|
|
100
|
+
error = failureError("stream_incomplete", "stream ended before a terminal event");
|
|
101
|
+
finishReason ??= "error";
|
|
102
|
+
}
|
|
103
|
+
span.end({
|
|
104
|
+
usage: sawUsage ? usage : void 0,
|
|
105
|
+
finishReason,
|
|
106
|
+
output,
|
|
107
|
+
timeToFirstChunkMs,
|
|
108
|
+
error
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
try {
|
|
112
|
+
for await (const event of response) {
|
|
113
|
+
const data = eventData$1(event);
|
|
114
|
+
if (isCurrentTurnBoundaryEvent(event)) sawTerminalEvent = true;
|
|
115
|
+
if (timeToFirstChunkMs === void 0 && (event.type === "message.appended" || event.type === "reasoning.appended")) timeToFirstChunkMs = performance.now() - start;
|
|
116
|
+
if (event.type === "step.completed") {
|
|
117
|
+
const eventUsage = asRecord$1(data.usage);
|
|
118
|
+
sawUsage = addUsage(usage, "inputTokens", numberField$1(eventUsage, "inputTokens")) || sawUsage;
|
|
119
|
+
sawUsage = addUsage(usage, "outputTokens", numberField$1(eventUsage, "outputTokens")) || sawUsage;
|
|
120
|
+
sawUsage = addUsage(usage, "cacheReadInputTokens", numberField$1(eventUsage, "cacheReadTokens")) || sawUsage;
|
|
121
|
+
sawUsage = addUsage(usage, "cacheCreationInputTokens", numberField$1(eventUsage, "cacheWriteTokens")) || sawUsage;
|
|
122
|
+
finishReason = stringField$1(data, "finishReason") ?? finishReason;
|
|
123
|
+
}
|
|
124
|
+
if (event.type === "message.completed" && stringField$1(data, "finishReason") !== "tool-calls") output = data.message;
|
|
125
|
+
if (event.type === "result.completed") output = data.result;
|
|
126
|
+
if (event.type === "turn.failed" || event.type === "session.failed" || event.type === "step.failed") {
|
|
127
|
+
if (error === void 0) {
|
|
128
|
+
const code = stringField$1(data, "code");
|
|
129
|
+
error = failureError(code, stringField$1(data, "message"));
|
|
130
|
+
if (code) span.span.setAttribute("error.code", code);
|
|
131
|
+
}
|
|
132
|
+
finishReason = "error";
|
|
133
|
+
}
|
|
134
|
+
addLifecycleEvent(span, event);
|
|
135
|
+
yield event;
|
|
136
|
+
}
|
|
137
|
+
} catch (caught) {
|
|
138
|
+
if (payload.signal?.aborted) {
|
|
139
|
+
error = failureError("cancelled", "cancelled");
|
|
140
|
+
finishReason ??= "cancelled";
|
|
141
|
+
} else error = caught;
|
|
142
|
+
throw caught;
|
|
143
|
+
} finally {
|
|
144
|
+
finish();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function wrapSession(session, options) {
|
|
148
|
+
return new Proxy(session, { get(target, prop) {
|
|
149
|
+
if (prop === "send") {
|
|
150
|
+
const wrappedSend = async (input) => {
|
|
151
|
+
const payload = typeof input === "string" ? { message: input } : input;
|
|
152
|
+
const span = startSpan(options.spanName ?? "invoke_agent", {
|
|
153
|
+
type: "agent",
|
|
154
|
+
agentName: options.agentName,
|
|
155
|
+
input: inputForSpan(payload)
|
|
156
|
+
});
|
|
157
|
+
const start = performance.now();
|
|
158
|
+
let response;
|
|
159
|
+
try {
|
|
160
|
+
response = await target.send(input);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (payload.signal?.aborted) span.end({
|
|
163
|
+
error: failureError("cancelled", "cancelled"),
|
|
164
|
+
finishReason: "cancelled"
|
|
165
|
+
});
|
|
166
|
+
else span.end({ error });
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
span.span.setAttribute("gen_ai.conversation.id", response.sessionId);
|
|
170
|
+
return new MessageResponse({
|
|
171
|
+
continuationToken: response.continuationToken,
|
|
172
|
+
sessionId: response.sessionId,
|
|
173
|
+
createStream: () => instrumentedStream(response, span, start, payload)
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
return wrappedSend;
|
|
177
|
+
}
|
|
178
|
+
return bindOrReturn(target, prop);
|
|
179
|
+
} });
|
|
180
|
+
}
|
|
181
|
+
function wrapEveClient(client, options = {}, overrides) {
|
|
182
|
+
const { agentName: _agentName, spanName: _spanName, ...sdkOptions } = options;
|
|
183
|
+
ensureInit(sdkOptions, overrides);
|
|
184
|
+
return new Proxy(client, { get(target, prop) {
|
|
185
|
+
if (prop === "session") return (state) => wrapSession(target.session(state), options);
|
|
186
|
+
return bindOrReturn(target, prop);
|
|
187
|
+
} });
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/hook.ts
|
|
191
|
+
function asRecord(value) {
|
|
192
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
function eventData(event) {
|
|
196
|
+
if (!("data" in event)) return {};
|
|
197
|
+
return asRecord(event.data) ?? {};
|
|
198
|
+
}
|
|
199
|
+
function stringField(record, key) {
|
|
200
|
+
const value = record?.[key];
|
|
201
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
202
|
+
}
|
|
203
|
+
function numberField(record, key) {
|
|
204
|
+
const value = record?.[key];
|
|
205
|
+
return typeof value === "number" ? value : void 0;
|
|
206
|
+
}
|
|
207
|
+
function jsonField(record, key) {
|
|
208
|
+
const value = record?.[key];
|
|
209
|
+
if (value === void 0) return void 0;
|
|
210
|
+
try {
|
|
211
|
+
return JSON.stringify(value);
|
|
212
|
+
} catch {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function baseAttributes(event, ctx) {
|
|
217
|
+
const data = eventData(event);
|
|
218
|
+
return {
|
|
219
|
+
"gen_ai.conversation.id": ctx.session.id,
|
|
220
|
+
"gen_ai.agent.name": ctx.agent.name,
|
|
221
|
+
"eve.channel.kind": ctx.channel.kind,
|
|
222
|
+
"eve.turn.id": stringField(data, "turnId"),
|
|
223
|
+
"eve.step.index": numberField(data, "stepIndex"),
|
|
224
|
+
"eve.turn.sequence": numberField(data, "sequence")
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function toolAttrs(result) {
|
|
228
|
+
const record = asRecord(result);
|
|
229
|
+
const subagentName = stringField(record, "subagentName");
|
|
230
|
+
return {
|
|
231
|
+
"gen_ai.tool.name": stringField(record, "toolName") ?? (subagentName ? `eve:subagent:${subagentName}` : void 0),
|
|
232
|
+
"gen_ai.tool.call.id": stringField(record, "callId"),
|
|
233
|
+
"eve.subagent.name": subagentName
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function reportError$1(onError, error) {
|
|
237
|
+
try {
|
|
238
|
+
onError?.(error);
|
|
239
|
+
} catch {}
|
|
240
|
+
}
|
|
241
|
+
function emit(event, ctx, level, message, attributes = {}, baseEvent = event) {
|
|
242
|
+
const at = event.meta?.at;
|
|
243
|
+
log(message, {
|
|
244
|
+
level,
|
|
245
|
+
eventName: event.type,
|
|
246
|
+
timestamp: at ? new Date(at) : void 0,
|
|
247
|
+
attributes: {
|
|
248
|
+
...baseAttributes(baseEvent, ctx),
|
|
249
|
+
...attributes
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
function stepCompletedMessage(data) {
|
|
254
|
+
const finishReason = stringField(data, "finishReason") ?? "unknown";
|
|
255
|
+
const usage = asRecord(data.usage);
|
|
256
|
+
const inputTokens = numberField(usage, "inputTokens");
|
|
257
|
+
const outputTokens = numberField(usage, "outputTokens");
|
|
258
|
+
return `Step completed (${finishReason})${inputTokens !== void 0 && outputTokens !== void 0 ? `: ${inputTokens} in / ${outputTokens} out tokens` : ""}`;
|
|
259
|
+
}
|
|
260
|
+
function logEvent(event, ctx) {
|
|
261
|
+
const data = eventData(event);
|
|
262
|
+
switch (event.type) {
|
|
263
|
+
case "session.started": {
|
|
264
|
+
const runtime = asRecord(data.runtime);
|
|
265
|
+
const invocation = asRecord(data.invocation);
|
|
266
|
+
emit(event, ctx, "info", "Session started", {
|
|
267
|
+
"eve.version": stringField(runtime, "eveVersion"),
|
|
268
|
+
"gen_ai.request.model": stringField(runtime, "modelId"),
|
|
269
|
+
"eve.agent.id": stringField(runtime, "agentId"),
|
|
270
|
+
"eve.parent.session_id": stringField(invocation, "parentSessionId"),
|
|
271
|
+
"eve.parent.call_id": stringField(invocation, "parentCallId"),
|
|
272
|
+
"eve.parent.turn_id": stringField(invocation, "parentTurnId"),
|
|
273
|
+
"eve.subagent.name": stringField(invocation, "name")
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
case "turn.started":
|
|
278
|
+
emit(event, ctx, "debug", "Turn started");
|
|
279
|
+
return;
|
|
280
|
+
case "message.received":
|
|
281
|
+
emit(event, ctx, "debug", "User message received");
|
|
282
|
+
return;
|
|
283
|
+
case "step.completed": {
|
|
284
|
+
const usage = asRecord(data.usage);
|
|
285
|
+
emit(event, ctx, "info", stepCompletedMessage(data), {
|
|
286
|
+
"gen_ai.usage.input_tokens": numberField(usage, "inputTokens"),
|
|
287
|
+
"gen_ai.usage.output_tokens": numberField(usage, "outputTokens"),
|
|
288
|
+
"eve.usage.cache_read_tokens": numberField(usage, "cacheReadTokens"),
|
|
289
|
+
"eve.usage.cache_write_tokens": numberField(usage, "cacheWriteTokens")
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
case "step.failed":
|
|
294
|
+
emit(event, ctx, "error", `Step failed: ${stringField(data, "message") ?? ""}`, {
|
|
295
|
+
"error.code": stringField(data, "code"),
|
|
296
|
+
"eve.error.details": jsonField(data, "details")
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
case "action.result": {
|
|
300
|
+
const status = stringField(data, "status");
|
|
301
|
+
if (status === "completed") return;
|
|
302
|
+
const error = asRecord(data.error);
|
|
303
|
+
emit(event, ctx, status === "failed" ? "error" : "warn", `Tool ${status ?? "failed"}: ${stringField(error, "message") ?? ""}`, {
|
|
304
|
+
"error.code": stringField(error, "code"),
|
|
305
|
+
...toolAttrs(data.result)
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
case "input.requested":
|
|
310
|
+
emit(event, ctx, "info", "Input requested (HITL)", { "eve.input.request_count": (Array.isArray(data.requests) ? data.requests : void 0)?.length });
|
|
311
|
+
return;
|
|
312
|
+
case "authorization.required":
|
|
313
|
+
emit(event, ctx, "warn", `Authorization required: ${stringField(data, "name") ?? ""}`, { "eve.authorization.name": stringField(data, "name") });
|
|
314
|
+
return;
|
|
315
|
+
case "authorization.completed": {
|
|
316
|
+
const outcome = stringField(data, "outcome");
|
|
317
|
+
emit(event, ctx, outcome === "authorized" ? "info" : "warn", `Authorization ${outcome ?? "completed"}: ${stringField(data, "name") ?? ""}`, {
|
|
318
|
+
"eve.authorization.name": stringField(data, "name"),
|
|
319
|
+
"eve.authorization.outcome": outcome,
|
|
320
|
+
"eve.authorization.reason": stringField(data, "reason")
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
case "subagent.started":
|
|
325
|
+
emit(event, ctx, "info", "Subagent started", {
|
|
326
|
+
"eve.subagent.name": stringField(data, "subagentName"),
|
|
327
|
+
"gen_ai.tool.call.id": stringField(data, "callId")
|
|
328
|
+
});
|
|
329
|
+
return;
|
|
330
|
+
case "subagent.event": {
|
|
331
|
+
const child = asRecord(data.event);
|
|
332
|
+
const childData = asRecord(child?.data);
|
|
333
|
+
const childType = stringField(child, "type");
|
|
334
|
+
const attrs = {
|
|
335
|
+
"eve.subagent.name": stringField(data, "subagentName"),
|
|
336
|
+
"gen_ai.tool.call.id": stringField(data, "callId")
|
|
337
|
+
};
|
|
338
|
+
if (childType === "step.failed") {
|
|
339
|
+
emit(event, ctx, "error", `Subagent step failed: ${stringField(childData, "message") ?? ""}`, {
|
|
340
|
+
...attrs,
|
|
341
|
+
"error.code": stringField(childData, "code"),
|
|
342
|
+
"eve.error.details": jsonField(childData, "details")
|
|
343
|
+
}, child);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (childType === "turn.failed") {
|
|
347
|
+
emit(event, ctx, "error", `Subagent turn failed: ${stringField(childData, "message") ?? ""}`, {
|
|
348
|
+
...attrs,
|
|
349
|
+
"error.code": stringField(childData, "code"),
|
|
350
|
+
"eve.error.details": jsonField(childData, "details")
|
|
351
|
+
}, child);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (childType === "session.failed") emit(event, ctx, "error", `Subagent session failed: ${stringField(childData, "message") ?? ""}`, {
|
|
355
|
+
...attrs,
|
|
356
|
+
"error.code": stringField(childData, "code"),
|
|
357
|
+
"eve.error.details": jsonField(childData, "details")
|
|
358
|
+
}, child);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
case "subagent.called": {
|
|
362
|
+
const remote = asRecord(data.remote);
|
|
363
|
+
emit(event, ctx, "info", `Subagent called: ${stringField(data, "name") ?? ""}`, {
|
|
364
|
+
"eve.subagent.name": stringField(data, "name"),
|
|
365
|
+
"gen_ai.tool.name": stringField(data, "toolName"),
|
|
366
|
+
"gen_ai.tool.call.id": stringField(data, "callId"),
|
|
367
|
+
"eve.child.session_id": stringField(data, "childSessionId"),
|
|
368
|
+
"eve.workflow.id": stringField(data, "workflowId"),
|
|
369
|
+
"eve.remote.url": stringField(remote, "url")
|
|
370
|
+
});
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
case "subagent.completed":
|
|
374
|
+
emit(event, ctx, "info", "Subagent completed", {
|
|
375
|
+
"eve.subagent.name": stringField(data, "subagentName"),
|
|
376
|
+
"gen_ai.tool.call.id": stringField(data, "callId")
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
case "compaction.requested":
|
|
380
|
+
emit(event, ctx, "info", `Compaction requested (${stringField(data, "modelId") ?? ""})`, {
|
|
381
|
+
"gen_ai.request.model": stringField(data, "modelId"),
|
|
382
|
+
"gen_ai.usage.input_tokens": numberField(data, "usageInputTokens")
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
case "compaction.completed":
|
|
386
|
+
emit(event, ctx, "debug", "Compaction completed", { "gen_ai.request.model": stringField(data, "modelId") });
|
|
387
|
+
return;
|
|
388
|
+
case "turn.completed":
|
|
389
|
+
emit(event, ctx, "info", "Turn completed");
|
|
390
|
+
return;
|
|
391
|
+
case "turn.failed":
|
|
392
|
+
emit(event, ctx, "error", `Turn failed: ${stringField(data, "message") ?? ""}`, {
|
|
393
|
+
"error.code": stringField(data, "code"),
|
|
394
|
+
"eve.error.details": jsonField(data, "details")
|
|
395
|
+
});
|
|
396
|
+
return;
|
|
397
|
+
case "session.waiting":
|
|
398
|
+
emit(event, ctx, "debug", "Session waiting");
|
|
399
|
+
return;
|
|
400
|
+
case "session.completed":
|
|
401
|
+
emit(event, ctx, "info", "Session completed");
|
|
402
|
+
return;
|
|
403
|
+
case "session.failed":
|
|
404
|
+
emit(event, ctx, "error", `Session failed: ${stringField(data, "message") ?? ""}`, {
|
|
405
|
+
"error.code": stringField(data, "code"),
|
|
406
|
+
"eve.error.details": jsonField(data, "details")
|
|
407
|
+
});
|
|
408
|
+
return;
|
|
409
|
+
default: return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function telemetryDevHook(options = {}, overrides) {
|
|
413
|
+
return { events: { "*"(event, ctx) {
|
|
414
|
+
try {
|
|
415
|
+
ensureInit(options, overrides);
|
|
416
|
+
logEvent(event, ctx);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
reportError$1(options.onError, error);
|
|
419
|
+
}
|
|
420
|
+
} } };
|
|
421
|
+
}
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/instrumentation.ts
|
|
424
|
+
const envServiceName = () => typeof process !== "undefined" ? process.env.OTEL_SERVICE_NAME : void 0;
|
|
425
|
+
function reportError(onError, error) {
|
|
426
|
+
try {
|
|
427
|
+
onError?.(error);
|
|
428
|
+
} catch {}
|
|
429
|
+
}
|
|
430
|
+
function telemetryDevInstrumentation(options = {}, overrides) {
|
|
431
|
+
const { functionId, recordInputs, recordOutputs, runtimeContext, stepStarted, ...sdkOptions } = options;
|
|
432
|
+
const definition = {
|
|
433
|
+
recordInputs: recordInputs ?? sdkOptions.captureInput ?? true,
|
|
434
|
+
recordOutputs: recordOutputs ?? sdkOptions.captureOutput ?? true,
|
|
435
|
+
setup({ agentName }) {
|
|
436
|
+
try {
|
|
437
|
+
ensureInit({
|
|
438
|
+
...sdkOptions,
|
|
439
|
+
serviceName: sdkOptions.serviceName ?? envServiceName() ?? agentName
|
|
440
|
+
}, overrides);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
reportError(sdkOptions.onError, error);
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
events: { "step.started"(input) {
|
|
446
|
+
const ctx = { ...runtimeContext };
|
|
447
|
+
const userId = input.session.auth.initiator?.principalId ?? input.session.auth.current?.principalId;
|
|
448
|
+
if (userId) ctx["user.id"] = userId;
|
|
449
|
+
try {
|
|
450
|
+
const userResult = stepStarted?.(input);
|
|
451
|
+
if (userResult?.runtimeContext) Object.assign(ctx, userResult.runtimeContext);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
reportError(sdkOptions.onError, error);
|
|
454
|
+
}
|
|
455
|
+
return Object.keys(ctx).length > 0 ? { runtimeContext: ctx } : void 0;
|
|
456
|
+
} }
|
|
457
|
+
};
|
|
458
|
+
if (functionId !== void 0) return {
|
|
459
|
+
...definition,
|
|
460
|
+
functionId
|
|
461
|
+
};
|
|
462
|
+
return definition;
|
|
463
|
+
}
|
|
464
|
+
//#endregion
|
|
465
|
+
export { telemetryDevHook, telemetryDevInstrumentation, wrapEveClient };
|