Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eve-instrumentation-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"braintrust": minor
---

feat: Add eve instrumentation provider for eve's lifecycle event bus
2 changes: 2 additions & 0 deletions e2e/scenarios/eve-instrumentation/agent/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { defineInstrumentation } from "eve/instrumentation";
export default defineInstrumentation(
braintrustEveInstrumentation({
defineState,
recordInputs: true,
recordOutputs: true,
setup: ({ agentName }) => {
initLogger({
projectName: process.env.BRAINTRUST_E2E_PROJECT_NAME || agentName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { braintrustEveInstrumentation, initLogger } from "braintrust";
import { defineState } from "eve/context";
import { defineInstrumentation } from "eve/instrumentation";

export default defineInstrumentation(
braintrustEveInstrumentation({
defineState,
recordInputs: true,
recordOutputs: true,
setup: ({ agentName }) => {
initLogger({
projectName: process.env.BRAINTRUST_E2E_PROJECT_NAME || agentName,
});
},
}),
);
2 changes: 1 addition & 1 deletion e2e/scenarios/eve-instrumentation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@openrouter/ai-sdk-provider": "3.0.0",
"ai": "7.0.34",
"eve-v0": "npm:eve@0.22.1",
"eve-v0-latest": "npm:eve@0.27.6",
"eve-v0-latest": "npm:eve@0.36.0",
"zod": "4.3.6"
}
}
19 changes: 13 additions & 6 deletions e2e/scenarios/eve-instrumentation/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions e2e/scenarios/eve-instrumentation/pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minimumReleaseAgeExclude:
- eve@0.36.0
29 changes: 27 additions & 2 deletions e2e/scenarios/eve-instrumentation/scenario.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import { once } from "node:events";
import { readFile, symlink, unlink } from "node:fs/promises";
import { readFile, rm, symlink, unlink, writeFile } from "node:fs/promises";
import net from "node:net";
import path from "node:path";
import { runMain } from "../../helpers/scenario-runtime";
Expand All @@ -14,7 +14,7 @@ async function main() {
);
const evePackage = JSON.parse(
await readFile(path.join(evePackageDir, "package.json"), "utf8"),
) as { bin?: string | Record<string, string> };
) as { bin?: string | Record<string, string>; version: string };
const eveBinPath =
typeof evePackage.bin === "string" ? evePackage.bin : evePackage.bin?.eve;
if (!eveBinPath) {
Expand All @@ -32,6 +32,31 @@ async function main() {
await symlink(evePackageDir, eveModuleDir, "dir");

const eveBin = path.join(evePackageDir, eveBinPath);
const agentDir = path.join(process.cwd(), "agent");
const [eveMajor, eveMinor] = evePackage.version.split(".").map(Number);
const usesProviders = eveMajor > 0 || (eveMajor === 0 && eveMinor >= 34);

if (usesProviders) {
// New eve: use the directory layout + experimental flag
await rm(path.join(agentDir, "instrumentation.ts"), { force: true });
const agentPath = path.join(agentDir, "agent.ts");
const agentSrc = await readFile(agentPath, "utf8");
if (!agentSrc.includes("instrumentationProviders")) {
await writeFile(
agentPath,
agentSrc.replace(
"export default defineAgent({",
"export default defineAgent({\n experimental: { instrumentationProviders: true },",
),
);
}
} else {
// Old eve: use the single-file layout, no experimental flag
await rm(path.join(agentDir, "instrumentation"), {
recursive: true,
force: true,
});
}

await runProcess(process.execPath, [eveBin, "build"], 90_000);

Expand Down
5 changes: 5 additions & 0 deletions js/src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ export {
braintrustEveHook,
braintrustEveInstrumentation,
} from "./instrumentation/plugins/eve-plugin";
export {
braintrustEveProvider,
type EveProviderDefinition,
type EveContentOptions,
} from "./instrumentation/plugins/eve-provider";
export { collectAnthropicSession } from "./wrappers/anthropic-session-collector";
export { wrapAnthropic } from "./wrappers/anthropic";
export {
Expand Down
5 changes: 5 additions & 0 deletions js/src/instrumentation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export {
braintrustEveHook,
braintrustEveInstrumentation,
} from "./plugins/eve-plugin";
export {
braintrustEveProvider,
type EveProviderDefinition,
type EveContentOptions,
} from "./plugins/eve-provider";

// Re-export core types for external instrumentation packages
export type {
Expand Down
117 changes: 98 additions & 19 deletions js/src/instrumentation/plugins/eve-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { createRequire } from "node:module";
import { debugLogger } from "../../debug-logger";
import {
NOOP_SPAN,
_internalStartSpanWithInitialMerge,
flush,
initLogger,
updateSpan,
withCurrent,
} from "../../logger";
Expand All @@ -14,6 +16,7 @@ import {
} from "../../span-origin";
import { SpanTypeAttribute, isObject } from "../../../util/index";
import { getCurrentUnixTimestamp } from "../../util";
import { braintrustEveProvider } from "./eve-provider";
import type {
EveAssistantStepFinishReason,
EveActionResultError,
Expand Down Expand Up @@ -119,11 +122,52 @@ type CapturedEveModelMessage = {

type CapturedEveModelInput = readonly CapturedEveModelMessage[];

/** Manual hook instrumentation for eve runtime stream events. */
export function braintrustEveHook(options: {
type EveInstrumentationOptions = {
defineState: EveDefineState;
recordInputs?: boolean;
recordOutputs?: boolean;
setup?: EveInstrumentationDefinition["setup"];
};

type EveHookOptions = {
defineState: EveDefineState;
metadata?: Record<string, unknown>;
}): EveHookDefinition {
};

function defaultSetup({ agentName }: { agentName: string }): void {
initLogger({
projectName: agentName,
apiKey: process.env.BRAINTRUST_API_KEY,
});
}

function resolveSetup(
setup: EveInstrumentationOptions["setup"],
): NonNullable<EveInstrumentationOptions["setup"]> {
return setup ?? defaultSetup;
}

function legacyEveInstrumentation(
options: EveInstrumentationOptions,
): EveInstrumentationDefinition {
const state = options.defineState(EVE_TRACE_STATE_KEY, emptyEveTraceState);
return {
events: {
"step.started": (input: EveInstrumentationStepStartedEventInput) => {
try {
captureEveModelInput(state, input);
} catch (error) {
debugLogger.warn("Error in Eve LLM input capture:", error);
}
},
},
recordInputs: options.recordInputs === true,
recordOutputs: options.recordOutputs === true,
setup: resolveSetup(options.setup),
};
}

function legacyEveHook(options: EveHookOptions): EveHookDefinition {
const state = options.defineState(EVE_TRACE_STATE_KEY, emptyEveTraceState);
const bridge = new EveBridge(state);
return {
Expand All @@ -135,26 +179,61 @@ export function braintrustEveHook(options: {
};
}

/** Eve instrumentation helper for logger setup and durable LLM input capture. */
let cachedEveVersion: string | undefined | null = undefined;

function detectEveVersion(): string | undefined {
if (cachedEveVersion !== undefined) {
return cachedEveVersion === null ? undefined : cachedEveVersion;
}
try {
const req = createRequire(`file://${process.cwd()}/`);
cachedEveVersion = req("eve/package.json").version as string;
} catch {
cachedEveVersion = null;
}
return cachedEveVersion === null ? undefined : cachedEveVersion;
}

function eveSupportsInstrumentationProviders(): boolean {
const version = detectEveVersion();
if (!version) return false;
const [major, minor] = version.split(".").map(Number);
return major > 0 || (major === 0 && minor >= 34);
}

let braintrustEveInstrumentationImpl: (
options: EveInstrumentationOptions,
) => EveInstrumentationDefinition;

let braintrustEveHookImpl: (options: EveHookOptions) => EveHookDefinition;

if (eveSupportsInstrumentationProviders()) {
braintrustEveInstrumentationImpl = (options) =>
braintrustEveProvider({
recordInputs: options.recordInputs,
recordOutputs: options.recordOutputs,
setup: resolveSetup(options.setup),
}) as unknown as EveInstrumentationDefinition;
braintrustEveHookImpl = () => ({ events: {} });
} else {
braintrustEveInstrumentationImpl = legacyEveInstrumentation;
braintrustEveHookImpl = legacyEveHook;
}

export function braintrustEveHook(options: {
defineState: EveDefineState;
metadata?: Record<string, unknown>;
}): EveHookDefinition {
return braintrustEveHookImpl(options);
}

export function braintrustEveInstrumentation(options: {
defineState: EveDefineState;
recordInputs?: boolean;
recordOutputs?: boolean;
setup?: EveInstrumentationDefinition["setup"];
}): EveInstrumentationDefinition {
const state = options.defineState(EVE_TRACE_STATE_KEY, emptyEveTraceState);
return {
events: {
"step.started": (input: EveInstrumentationStepStartedEventInput) => {
try {
captureEveModelInput(state, input);
} catch (error) {
debugLogger.warn("Error in Eve LLM input capture:", error);
}
},
},
recordInputs: false,
recordOutputs: false,
setup: options.setup,
};
return braintrustEveInstrumentationImpl(options);
}

function isEveHandleMessageStreamEvent(
Expand Down
Loading
Loading