Skip to content

Commit b02fa2b

Browse files
committed
feat(eve): add braintrustEveProvider for eve's instrumentation bridge
Add a new Braintrust instrumentation provider that consumes eve's lifecycle event bus (session/turn/step/model-call/action events) instead of parsing stream events manually. The provider declares capture: "content" or "metadata" and maps lifecycle events directly to Braintrust spans, using eve's replay-stable idempotency keys for deterministic span IDs and ctx.state for durable per-operation state. Content redaction follows eve's per-destination ContentOptions model: recordInputs and recordOutputs default to false, so the provider captures metadata only (structure, identity, usage, timing) unless the caller opts in. When both are false, the provider declares capture: "metadata" so eve never builds the content projection at all. When either is true, the provider declares capture: "content" and respects its own redaction policy in each handler. The version branching is internal to the SDK. braintrustEveInstrumentation detects eve >= 0.34.0 at module scope and delegates to the provider implementation; braintrustEveHook becomes a no-op on new eve since the provider handles everything. The user's code does not change — they keep importing braintrustEveInstrumentation and braintrustEveHook and passing defineState, and the SDK uses the right path automatically. New files: - js/src/vendor-sdk-types/eve-provider.ts: vendored lifecycle event types - js/src/instrumentation/plugins/eve-provider.ts: provider implementation Signed-off-by: Chad Hietala <chadhietala@gmail.com>
1 parent ae76882 commit b02fa2b

6 files changed

Lines changed: 1285 additions & 4 deletions

File tree

‎e2e/scenarios/eve-instrumentation/agent/instrumentation.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { defineInstrumentation } from "eve/instrumentation";
55
export default defineInstrumentation(
66
braintrustEveInstrumentation({
77
defineState,
8+
recordInputs: true,
9+
recordOutputs: true,
810
setup: ({ agentName }) => {
911
initLogger({
1012
projectName: process.env.BRAINTRUST_E2E_PROJECT_NAME || agentName,

‎js/src/exports.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,11 @@ export {
200200
braintrustEveHook,
201201
braintrustEveInstrumentation,
202202
} from "./instrumentation/plugins/eve-plugin";
203+
export {
204+
braintrustEveProvider,
205+
type EveProviderDefinition,
206+
type EveContentOptions,
207+
} from "./instrumentation/plugins/eve-provider";
203208
export { collectAnthropicSession } from "./wrappers/anthropic-session-collector";
204209
export { wrapAnthropic } from "./wrappers/anthropic";
205210
export {

‎js/src/instrumentation/index.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ export {
2727
braintrustEveHook,
2828
braintrustEveInstrumentation,
2929
} from "./plugins/eve-plugin";
30+
export {
31+
braintrustEveProvider,
32+
type EveProviderDefinition,
33+
type EveContentOptions,
34+
} from "./plugins/eve-provider";
3035

3136
// Re-export core types for external instrumentation packages
3237
export type {

‎js/src/instrumentation/plugins/eve-plugin.ts‎

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from "../../span-origin";
1515
import { SpanTypeAttribute, isObject } from "../../../util/index";
1616
import { getCurrentUnixTimestamp } from "../../util";
17+
import { braintrustEveProvider } from "./eve-provider";
1718
import type {
1819
EveAssistantStepFinishReason,
1920
EveActionResultError,
@@ -112,18 +113,56 @@ const MAX_STORED_REASONING_BLOCKS = 100;
112113
const MAX_STORED_SPAN_REFERENCES = 10_000;
113114
const MAX_STORED_STEP_STARTS = 10_000;
114115

116+
let cachedEveVersion: string | undefined | null = undefined;
117+
118+
function detectEveVersion(): string | undefined {
119+
if (cachedEveVersion !== undefined) {
120+
return cachedEveVersion === null ? undefined : cachedEveVersion;
121+
}
122+
try {
123+
let req: NodeRequire;
124+
try {
125+
const { createRequire } =
126+
require("node:module") as typeof import("node:module");
127+
req = createRequire(`file://${process.cwd()}/`);
128+
} catch {
129+
req = require;
130+
}
131+
cachedEveVersion = req("eve/package.json").version as string;
132+
} catch {
133+
cachedEveVersion = null;
134+
}
135+
return cachedEveVersion === null ? undefined : cachedEveVersion;
136+
}
137+
138+
function eveSupportsInstrumentationProviders(): boolean {
139+
const version = detectEveVersion();
140+
if (!version) return false;
141+
const [major, minor] = version.split(".").map(Number);
142+
return major > 0 || (major === 0 && minor >= 34);
143+
}
144+
115145
type CapturedEveModelMessage = {
116146
content: string | readonly Record<string, unknown>[];
117147
role: EveModelMessage["role"];
118148
};
119149

120150
type CapturedEveModelInput = readonly CapturedEveModelMessage[];
121151

122-
/** Manual hook instrumentation for eve runtime stream events. */
152+
/**
153+
* Manual hook instrumentation for eve runtime stream events.
154+
*
155+
* On eve >= 0.34.0 with `experimental.instrumentationProviders` enabled, the
156+
* provider bus supersedes stream-event hooks. This function returns a no-op
157+
* hook so the user's `agent/hooks/braintrust.ts` stays valid without changes.
158+
*/
123159
export function braintrustEveHook(options: {
124160
defineState: EveDefineState;
125161
metadata?: Record<string, unknown>;
126162
}): EveHookDefinition {
163+
if (eveSupportsInstrumentationProviders()) {
164+
return { events: {} };
165+
}
127166
const state = options.defineState(EVE_TRACE_STATE_KEY, emptyEveTraceState);
128167
const bridge = new EveBridge(state);
129168
return {
@@ -135,11 +174,33 @@ export function braintrustEveHook(options: {
135174
};
136175
}
137176

138-
/** Eve instrumentation helper for logger setup and durable LLM input capture. */
177+
/**
178+
* Eve instrumentation helper for logger setup and durable LLM input capture.
179+
*
180+
* On eve >= 0.34.0 with `experimental.instrumentationProviders` enabled, this
181+
* internally delegates to {@link braintrustEveProvider} and returns a provider
182+
* definition that consumes eve's lifecycle event bus. `defineState` is ignored
183+
* on that path — it remains required so the call site does not change across
184+
* versions.
185+
*
186+
* `recordInputs` and `recordOutputs` default to `false` (metadata only). Set
187+
* either to `true` to capture prompts, responses, and tool payloads.
188+
*/
139189
export function braintrustEveInstrumentation(options: {
140190
defineState: EveDefineState;
191+
recordInputs?: boolean;
192+
recordOutputs?: boolean;
141193
setup?: EveInstrumentationDefinition["setup"];
142194
}): EveInstrumentationDefinition {
195+
if (eveSupportsInstrumentationProviders()) {
196+
return braintrustEveProvider({
197+
recordInputs: options.recordInputs,
198+
recordOutputs: options.recordOutputs,
199+
setup: options.setup as Parameters<
200+
typeof braintrustEveProvider
201+
>[0]["setup"],
202+
}) as unknown as EveInstrumentationDefinition;
203+
}
143204
const state = options.defineState(EVE_TRACE_STATE_KEY, emptyEveTraceState);
144205
return {
145206
events: {
@@ -151,8 +212,8 @@ export function braintrustEveInstrumentation(options: {
151212
}
152213
},
153214
},
154-
recordInputs: false,
155-
recordOutputs: false,
215+
recordInputs: options.recordInputs === true,
216+
recordOutputs: options.recordOutputs === true,
156217
setup: options.setup,
157218
};
158219
}

0 commit comments

Comments
 (0)