diff --git a/.changeset/eve-instrumentation-provider.md b/.changeset/eve-instrumentation-provider.md new file mode 100644 index 000000000..8a939fa6e --- /dev/null +++ b/.changeset/eve-instrumentation-provider.md @@ -0,0 +1,5 @@ +--- +"braintrust": minor +--- + +feat: Add eve instrumentation provider for eve's lifecycle event bus diff --git a/e2e/scenarios/eve-instrumentation/agent/instrumentation.ts b/e2e/scenarios/eve-instrumentation/agent/instrumentation.ts index fff8a737d..cdac61d70 100644 --- a/e2e/scenarios/eve-instrumentation/agent/instrumentation.ts +++ b/e2e/scenarios/eve-instrumentation/agent/instrumentation.ts @@ -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, diff --git a/e2e/scenarios/eve-instrumentation/agent/instrumentation/braintrust.ts b/e2e/scenarios/eve-instrumentation/agent/instrumentation/braintrust.ts new file mode 100644 index 000000000..cdac61d70 --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/agent/instrumentation/braintrust.ts @@ -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, + }); + }, + }), +); diff --git a/e2e/scenarios/eve-instrumentation/package.json b/e2e/scenarios/eve-instrumentation/package.json index 202841885..8c0bbbe54 100644 --- a/e2e/scenarios/eve-instrumentation/package.json +++ b/e2e/scenarios/eve-instrumentation/package.json @@ -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" } } diff --git a/e2e/scenarios/eve-instrumentation/pnpm-lock.yaml b/e2e/scenarios/eve-instrumentation/pnpm-lock.yaml index c3cbc645d..a97f00e63 100644 --- a/e2e/scenarios/eve-instrumentation/pnpm-lock.yaml +++ b/e2e/scenarios/eve-instrumentation/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: npm:eve@0.22.1 version: eve@0.22.1(ai@7.0.34(zod@4.3.6)) eve-v0-latest: - specifier: npm:eve@0.27.6 - version: eve@0.27.6(ai@7.0.34(zod@4.3.6)) + specifier: npm:eve@0.36.0 + version: eve@0.36.0(ai@7.0.34(zod@4.3.6)) zod: specifier: 4.3.6 version: 4.3.6 @@ -257,13 +257,13 @@ packages: microsandbox: optional: true - eve@0.27.6: - resolution: {integrity: sha512-t2Xr/nlVGc0+Fy9fav8p/CJrwMSQM0zyuOGY1F1XapN0UFssGYBGa6Ml/Q5OnowvWwsGH4m0Tf2QAccayVCvtw==} + eve@0.36.0: + resolution: {integrity: sha512-dkUR2mg/FmIR84vvavx4XYsrMuGPi+EO3r0k94KSMHjoUi6G/DmJaNtm62dbD6FGDnRViO7uwSYOdVp5+mJhYQ==} engines: {node: '>=24'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.0.0 - ai: ^7.0.34 + ai: ^7.0.58 braintrust: ^3.0.0 just-bash: ^3.0.0 microsandbox: ^0.5.0 @@ -365,6 +365,10 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + undici@8.9.0: + resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==} + engines: {node: '>=22.19.0'} + unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} @@ -624,10 +628,11 @@ snapshots: - xml2js - zephyr-agent - eve@0.27.6(ai@7.0.34(zod@4.3.6)): + eve@0.36.0(ai@7.0.34(zod@4.3.6)): dependencies: ai: 7.0.34(zod@4.3.6) nitro: 3.0.260610-beta + undici: 8.9.0 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -773,6 +778,8 @@ snapshots: tslib@2.8.1: optional: true + undici@8.9.0: {} + unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 diff --git a/e2e/scenarios/eve-instrumentation/pnpm-workspace.yaml b/e2e/scenarios/eve-instrumentation/pnpm-workspace.yaml new file mode 100644 index 000000000..f8aab5c1b --- /dev/null +++ b/e2e/scenarios/eve-instrumentation/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +minimumReleaseAgeExclude: + - eve@0.36.0 diff --git a/e2e/scenarios/eve-instrumentation/scenario.ts b/e2e/scenarios/eve-instrumentation/scenario.ts index 48efc7bea..9625da5e6 100644 --- a/e2e/scenarios/eve-instrumentation/scenario.ts +++ b/e2e/scenarios/eve-instrumentation/scenario.ts @@ -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"; @@ -14,7 +14,7 @@ async function main() { ); const evePackage = JSON.parse( await readFile(path.join(evePackageDir, "package.json"), "utf8"), - ) as { bin?: string | Record }; + ) as { bin?: string | Record; version: string }; const eveBinPath = typeof evePackage.bin === "string" ? evePackage.bin : evePackage.bin?.eve; if (!eveBinPath) { @@ -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); diff --git a/js/src/exports.ts b/js/src/exports.ts index 8a7ec1364..29e81427c 100644 --- a/js/src/exports.ts +++ b/js/src/exports.ts @@ -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 { diff --git a/js/src/instrumentation/index.ts b/js/src/instrumentation/index.ts index 338d62abd..124903086 100644 --- a/js/src/instrumentation/index.ts +++ b/js/src/instrumentation/index.ts @@ -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 { diff --git a/js/src/instrumentation/plugins/eve-plugin.ts b/js/src/instrumentation/plugins/eve-plugin.ts index ef18f8913..105ccce27 100644 --- a/js/src/instrumentation/plugins/eve-plugin.ts +++ b/js/src/instrumentation/plugins/eve-plugin.ts @@ -1,8 +1,10 @@ +import { createRequire } from "node:module"; import { debugLogger } from "../../debug-logger"; import { NOOP_SPAN, _internalStartSpanWithInitialMerge, flush, + initLogger, updateSpan, withCurrent, } from "../../logger"; @@ -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, @@ -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; -}): EveHookDefinition { +}; + +function defaultSetup({ agentName }: { agentName: string }): void { + initLogger({ + projectName: agentName, + apiKey: process.env.BRAINTRUST_API_KEY, + }); +} + +function resolveSetup( + setup: EveInstrumentationOptions["setup"], +): NonNullable { + 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 { @@ -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; +}): 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( diff --git a/js/src/instrumentation/plugins/eve-provider.ts b/js/src/instrumentation/plugins/eve-provider.ts new file mode 100644 index 000000000..1edeb4e83 --- /dev/null +++ b/js/src/instrumentation/plugins/eve-provider.ts @@ -0,0 +1,865 @@ +import { debugLogger } from "../../debug-logger"; +import { + NOOP_SPAN, + _internalStartSpanWithInitialMerge, + flush, + withCurrent, +} from "../../logger"; +import type { Span } from "../../logger"; +import { LRUCache } from "../../lru-cache"; +import { + INSTRUMENTATION_NAMES, + withSpanInstrumentationName, +} from "../../span-origin"; +import { SpanTypeAttribute, isObject } from "../../../util/index"; +import { getCurrentUnixTimestamp } from "../../util"; +import type { + EveContentOptions, + EveInstrumentationActionCompletedEvent, + EveInstrumentationActionFailedEvent, + EveInstrumentationActionKind, + EveInstrumentationActionStartedEvent, + EveInstrumentationCapture, + EveInstrumentationContentPart, + EveInstrumentationModelCallCompletedEvent, + EveInstrumentationModelCallFailedEvent, + EveInstrumentationModelCallStartedEvent, + EveInstrumentationModelInput, + EveProviderContext, + EveProviderDefinition, + EveProviderSetupContext, + EveInstrumentationSessionFailedEvent, + EveInstrumentationSessionSettledEvent, + EveInstrumentationSessionStartedEvent, + EveInstrumentationStepAttemptCompletedEvent, + EveInstrumentationStepAttemptFailedEvent, + EveInstrumentationStepAttemptMetadataEvent, + EveInstrumentationStepAttemptStartedEvent, + EveInstrumentationTurnFailedEvent, + EveInstrumentationTurnSettledEvent, + EveInstrumentationTurnStartedEvent, + EveInstrumentationUsage, +} from "../../vendor-sdk-types/eve-provider"; + +type EveSpan = Pick; + +type SpanState = { + metadata: Record; + span: EveSpan; +}; + +type StepState = SpanState & { + input?: unknown; + metrics: Record; + output?: unknown; +}; + +type ActionState = SpanState & { + endedByTurn?: boolean; + turnKey: string; +}; + +type EveEntityKind = "action" | "step" | "turn"; + +const MAX_EVE_CACHE_ENTRIES = 10_000; + +/** + * Braintrust instrumentation provider for eve's lifecycle bus. + * + * Consume this from `agent/instrumentation/braintrust.ts` (the provider + * directory layout) when the eve version supports + * `experimental.instrumentationProviders`. For older eve, keep using + * {@link braintrustEveInstrumentation} with the single-file + * `agent/instrumentation.ts` layout. + * + * The provider receives structured lifecycle events directly from eve's + * instrumentation bus — no stream-event parsing, no `defineState`, and no + * manual LLM-input capture. eve assigns replay-stable idempotency keys + * and durable per-operation state through `ctx.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. + * + * Requires eve >= 0.34.0 with `experimental.instrumentationProviders` on. + */ +export function braintrustEveProvider(options: { + recordInputs?: boolean; + recordOutputs?: boolean; + setup?: (context: EveProviderSetupContext) => void | PromiseLike; +}): EveProviderDefinition { + const recordInputs = options.recordInputs === true; + const recordOutputs = options.recordOutputs === true; + const capture: EveInstrumentationCapture = + recordInputs || recordOutputs ? "content" : "metadata"; + const bridge = new EveProviderBridge({ recordInputs, recordOutputs }); + return { + capture, + events: { + "session.started": (event, ctx) => + bridge.handleSessionStarted(event, ctx), + "session.completed": (event, ctx) => + bridge.handleSessionSettled(event, ctx), + "session.waiting": (event, ctx) => + bridge.handleSessionSettled(event, ctx), + "session.failed": (event, ctx) => bridge.handleSessionFailed(event, ctx), + "turn.started": (event, ctx) => bridge.handleTurnStarted(event, ctx), + "turn.completed": (event, ctx) => bridge.handleTurnSettled(event, ctx), + "turn.cancelled": (event, ctx) => bridge.handleTurnSettled(event, ctx), + "turn.failed": (event, ctx) => bridge.handleTurnFailed(event, ctx), + "step.attempt.started": (event, ctx) => + bridge.handleStepAttemptStarted(event, ctx), + "step.attempt.completed": (event, ctx) => + bridge.handleStepAttemptCompleted(event, ctx), + "step.attempt.failed": (event, ctx) => + bridge.handleStepAttemptFailed(event, ctx), + "step.attempt.metadata": (event, ctx) => + bridge.handleStepAttemptMetadata(event, ctx), + "model.call.started": (event, ctx) => + bridge.handleModelCallStarted(event, ctx), + "model.call.completed": (event, ctx) => + bridge.handleModelCallCompleted(event, ctx), + "model.call.failed": (event, ctx) => + bridge.handleModelCallFailed(event, ctx), + "action.started": (event, ctx) => bridge.handleActionStarted(event, ctx), + "action.completed": (event, ctx) => + bridge.handleActionCompleted(event, ctx), + "action.failed": (event, ctx) => bridge.handleActionFailed(event, ctx), + } satisfies EveProviderDefinition["events"], + flush: async () => { + await bridge.flush(); + }, + shutdown: async () => { + await bridge.flush(); + }, + setup: options.setup, + }; +} + +class EveProviderBridge { + private readonly recordInputs: boolean; + private readonly recordOutputs: boolean; + + constructor(content: EveContentOptions) { + this.recordInputs = content.recordInputs === true; + this.recordOutputs = content.recordOutputs === true; + } + + private turnsByKey = new LRUCache< + string, + SpanState & { key: string; sessionId: string; turnId: string } + >({ + max: MAX_EVE_CACHE_ENTRIES, + }); + private stepsByKey = new LRUCache({ + max: MAX_EVE_CACHE_ENTRIES, + }); + private actionsByKey = new LRUCache({ + max: MAX_EVE_CACHE_ENTRIES, + }); + private sessionMetadata = new Map>(); + + // --- Session --- + + handleSessionStarted( + event: EveInstrumentationSessionStartedEvent, + _ctx: EveProviderContext, + ): void { + const metadata: Record = { + "eve.session_id": event.sessionId, + }; + if (event.agentName) metadata["eve.agent_name"] = event.agentName; + if (event.channelKind) metadata["eve.channel_kind"] = event.channelKind; + + this.sessionMetadata.set(event.sessionId, metadata); + + for (const [, turn] of this.turnsByKey) { + if (turn.sessionId !== event.sessionId) continue; + turn.metadata = { ...turn.metadata, ...metadata }; + turn.span.log({ metadata: turn.metadata }); + } + } + + handleSessionSettled( + event: EveInstrumentationSessionSettledEvent, + _ctx: EveProviderContext, + ): void { + if (event.type === "session.waiting") { + this.sessionMetadata.delete(event.sessionId); + } else { + this.finalizeSession(event.sessionId, { endTime: undefined }); + } + if (event.type === "session.completed") { + void this.flush(); + } + } + + handleSessionFailed( + event: EveInstrumentationSessionFailedEvent, + ctx: EveProviderContext, + ): void { + const error = normalizeError(event.error); + this.finalizeSession(event.sessionId, { endTime: undefined, error }); + void this.flush(); + } + + // --- Turn --- + + handleTurnStarted( + event: EveInstrumentationTurnStartedEvent, + ctx: EveProviderContext, + ): void { + const key = event.idempotencyKey; + if (this.turnsByKey.has(key)) { + const existing = this.turnsByKey.get(key)!; + existing.span.log({ metadata: existing.metadata }); + return; + } + + const sessionId = event.sessionId; + const sessionMeta = this.sessionMetadata.get(sessionId) ?? {}; + const metadata: Record = { + ...sessionMeta, + "eve.session_id": sessionId, + "eve.turn_sequence": event.sequence, + }; + + const rootSpanId = deterministicId( + "eve:root", + event.rootSessionId, + event.turnId, + ); + const parentSpanId = event.parentLineage + ? deterministicId( + "eve:subagent", + event.parentLineage.sessionId, + event.parentLineage.callId, + ) + : undefined; + + const spanId = deterministicId("eve:turn", sessionId, event.turnId); + const rowId = event.idempotencyKey; + const span = this.startSpan({ + event: { id: rowId, metadata }, + name: "eve.turn", + parentSpanIds: parentSpanId + ? { rootSpanId, spanId: parentSpanId } + : { parentSpanIds: [], rootSpanId }, + spanAttributes: { type: SpanTypeAttribute.TASK }, + spanId, + }); + span.log({ metadata }); + + const turnState = { key, metadata, sessionId, span, turnId: event.turnId }; + this.turnsByKey.set(key, turnState); + this.storeDurableRef(ctx, "turn", rowId, span, { + sessionId, + turnId: event.turnId, + }); + } + + handleTurnSettled( + event: EveInstrumentationTurnSettledEvent, + _ctx: EveProviderContext, + ): void { + const turn = this.turnsByKey.get(event.idempotencyKey); + if (!turn) return; + this.finalizeTurn(event.idempotencyKey, { endTime: undefined }); + } + + handleTurnFailed( + event: EveInstrumentationTurnFailedEvent, + _ctx: EveProviderContext, + ): void { + const turn = this.turnsByKey.get(event.idempotencyKey); + if (!turn) return; + this.finalizeTurn(event.idempotencyKey, { + endTime: undefined, + error: normalizeError(event.error), + }); + } + + // --- Step attempt --- + + handleStepAttemptStarted( + event: EveInstrumentationStepAttemptStartedEvent, + ctx: EveProviderContext, + ): void { + const key = event.idempotencyKey; + const turnKey = turnIdempotencyKey( + event.scope.sessionId, + event.scope.turnId, + ); + const turn = this.turnsByKey.get(turnKey); + if (!turn) return; + + // Close any existing step for this attempt (retry replaces previous) + const existing = this.stepsByKey.get(key); + if (existing) { + existing.span.log({ + ...(this.recordInputs && existing.input !== undefined + ? { input: existing.input } + : {}), + metadata: existing.metadata, + metrics: existing.metrics, + ...(this.recordOutputs && existing.output !== undefined + ? { output: existing.output } + : {}), + }); + existing.span.end(); + } + + const modelMeta = modelMetadataFromOperation(event.operation); + const metadata: Record = { + ...turn.metadata, + ...modelMeta, + "eve.step_index": event.scope.stepIndex, + "eve.attempt_index": event.scope.attemptIndex, + }; + + const spanId = deterministicId("eve:step", event.scope.attemptId); + const span = this.startChildSpan(turn.span, { + event: { id: event.idempotencyKey, metadata }, + name: "eve.step", + spanAttributes: { type: SpanTypeAttribute.LLM }, + spanId, + }); + span.log({ metadata }); + + this.stepsByKey.set(key, { + metadata, + metrics: {}, + span, + }); + } + + handleStepAttemptMetadata( + event: EveInstrumentationStepAttemptMetadataEvent, + _ctx: EveProviderContext, + ): void { + const step = this.stepsByKey.get(event.idempotencyKey); + if (!step) return; + const gateway = event.providerMetadata?.gateway; + if (isObject(gateway) && typeof gateway["generationId"] === "string") { + step.metadata["eve.gateway_generation_id"] = gateway["generationId"]; + step.span.log({ metadata: step.metadata }); + } + } + + handleStepAttemptCompleted( + event: EveInstrumentationStepAttemptCompletedEvent, + _ctx: EveProviderContext, + ): void { + this.finalizeStep(event.idempotencyKey, { endTime: undefined }); + } + + handleStepAttemptFailed( + event: EveInstrumentationStepAttemptFailedEvent, + _ctx: EveProviderContext, + ): void { + this.finalizeStep(event.idempotencyKey, { + endTime: undefined, + error: normalizeError(event.error), + }); + } + + // --- Model call (logs to the step span, does not create a separate span) --- + + handleModelCallStarted( + event: EveInstrumentationModelCallStartedEvent, + _ctx: EveProviderContext, + ): void { + if (!this.recordInputs) return; + const step = this.stepsByKey.get(event.idempotencyKey); + if (!step || !event.input) return; + const captured = captureModelInput(event.input); + if (captured) { + step.input = captured; + step.span.log({ input: captured }); + } + } + + handleModelCallCompleted( + event: EveInstrumentationModelCallCompletedEvent, + _ctx: EveProviderContext, + ): void { + const step = this.stepsByKey.get(event.idempotencyKey); + if (!step) return; + + if (this.recordOutputs) { + const output = contentPartsToOutput(event.content, event.finishReason); + if (output !== undefined) { + step.output = output; + } + } + const metrics = usageToMetrics(event.usage); + if (Object.keys(metrics).length > 0) { + step.metrics = { ...step.metrics, ...metrics }; + } + step.span.log({ + ...(this.recordInputs && step.input !== undefined + ? { input: step.input } + : {}), + metadata: step.metadata, + ...(Object.keys(metrics).length > 0 ? { metrics } : {}), + ...(step.output !== undefined ? { output: step.output } : {}), + }); + } + + handleModelCallFailed( + event: EveInstrumentationModelCallFailedEvent, + _ctx: EveProviderContext, + ): void { + const step = this.stepsByKey.get(event.idempotencyKey); + if (!step) return; + step.span.log({ + error: normalizeError(event.error), + metadata: step.metadata, + }); + } + + // --- Action (tool, subagent, etc.) --- + + handleActionStarted( + event: EveInstrumentationActionStartedEvent, + ctx: EveProviderContext, + ): void { + const key = event.idempotencyKey; + if (this.actionsByKey.has(key)) return; + + const turnKey = turnIdempotencyKey( + event.scope.sessionId, + event.scope.turnId, + ); + const turn = this.turnsByKey.get(turnKey); + if (!turn) return; + + const name = actionSpanName(event.kind, event.name); + const metadata = actionMetadata(turn.metadata, event.kind); + const spanId = deterministicId( + "eve:action", + event.scope.sessionId, + event.scope.turnId, + event.callId, + ); + + const span = this.startChildSpan(turn.span, { + event: { + id: event.idempotencyKey, + ...(this.recordInputs && event.input !== undefined + ? { input: event.input } + : {}), + metadata, + }, + name, + spanAttributes: { type: SpanTypeAttribute.TOOL }, + spanId, + }); + span.log({ + ...(this.recordInputs && event.input !== undefined + ? { input: event.input } + : {}), + metadata, + }); + + this.actionsByKey.set(key, { + metadata, + span, + turnKey, + }); + } + + handleActionCompleted( + event: EveInstrumentationActionCompletedEvent, + _ctx: EveProviderContext, + ): void { + const action = this.actionsByKey.get(event.idempotencyKey); + if (!action) return; + const output = this.recordOutputs + ? actionOutputToValue(event.output) + : undefined; + const metrics = event.usage ? usageToMetrics(event.usage) : {}; + action.span.log({ + metadata: action.metadata, + ...(Object.keys(metrics).length > 0 ? { metrics } : {}), + ...(output !== undefined ? { output } : {}), + }); + const endTime = event.acceptedAtMs ? event.acceptedAtMs / 1000 : undefined; + action.span.end(endTime === undefined ? undefined : { endTime }); + this.actionsByKey.delete(event.idempotencyKey); + } + + handleActionFailed( + event: EveInstrumentationActionFailedEvent, + _ctx: EveProviderContext, + ): void { + const action = this.actionsByKey.get(event.idempotencyKey); + if (!action) return; + action.span.log({ + error: normalizeError(event.error), + metadata: action.metadata, + }); + const endTime = event.acceptedAtMs ? event.acceptedAtMs / 1000 : undefined; + action.span.end(endTime === undefined ? undefined : { endTime }); + this.actionsByKey.delete(event.idempotencyKey); + } + + // --- Flush / shutdown --- + + async flush(): Promise { + try { + await flush(); + return true; + } catch (error) { + debugLogger.warn("Error in Eve provider flush:", error); + return false; + } + } + + // --- Internals --- + + private startSpan( + args: Parameters[0], + ): EveSpan { + const rowId = args?.event?.id; + if (typeof rowId === "string") { + // Try to find a durable reference for resumption + } + const span = withCurrent(NOOP_SPAN, () => + _internalStartSpanWithInitialMerge( + withSpanInstrumentationName( + { ...args, startTime: args?.startTime ?? getCurrentUnixTimestamp() }, + INSTRUMENTATION_NAMES.EVE, + ), + ), + ); + return span; + } + + private startChildSpan( + parent: EveSpan, + args: Parameters[0], + ): EveSpan { + return this.startSpan({ + ...args, + parentSpanIds: { + rootSpanId: parent.rootSpanId, + spanId: parent.spanId, + }, + }); + } + + private storeDurableRef( + ctx: EveProviderContext, + kind: EveEntityKind, + rowId: string, + span: EveSpan, + owner: { sessionId: string; turnId?: string }, + ): void { + try { + void span.export().then((exported) => { + ctx.state.set({ + exported, + kind, + rootSpanId: span.rootSpanId, + rowId, + sessionId: owner.sessionId, + spanId: span.spanId, + ...(owner.turnId ? { turnId: owner.turnId } : {}), + } as unknown as Parameters[0]); + }); + } catch (error) { + debugLogger.warn("Error storing durable Eve span ref:", error); + } + } + + private finalizeStep( + key: string, + args: { endTime: number | undefined; error?: Error }, + ): void { + const step = this.stepsByKey.get(key); + if (!step) return; + if (args.error) { + step.span.log({ error: args.error }); + } else { + step.span.log({ + ...(this.recordInputs && step.input !== undefined + ? { input: step.input } + : {}), + metadata: step.metadata, + metrics: step.metrics, + ...(this.recordOutputs && step.output !== undefined + ? { output: step.output } + : {}), + }); + } + step.span.end( + args.endTime === undefined ? undefined : { endTime: args.endTime }, + ); + this.stepsByKey.delete(key); + } + + private finalizeTurn( + key: string, + args: { endTime: number | undefined; error?: Error }, + ): void { + const turn = this.turnsByKey.get(key); + if (!turn) return; + + // Close any open steps + for (const [stepKey, step] of this.stepsByKey) { + if (!stepKey.startsWith(`step:${turn.sessionId}:${turn.turnId}:`)) + continue; + step.span.log({ + ...(this.recordInputs && step.input !== undefined + ? { input: step.input } + : {}), + metadata: step.metadata, + metrics: step.metrics, + ...(this.recordOutputs && step.output !== undefined + ? { output: step.output } + : {}), + }); + step.span.end( + args.endTime === undefined ? undefined : { endTime: args.endTime }, + ); + this.stepsByKey.delete(stepKey); + } + + // Close any open actions for this turn + for (const [, action] of this.actionsByKey) { + if (action.turnKey !== key || action.endedByTurn) continue; + action.span.log({ metadata: action.metadata }); + action.span.end( + args.endTime === undefined ? undefined : { endTime: args.endTime }, + ); + action.endedByTurn = true; + } + + if (args.error) { + turn.span.log({ error: args.error }); + } else { + turn.span.log({ metadata: turn.metadata }); + } + turn.span.end( + args.endTime === undefined ? undefined : { endTime: args.endTime }, + ); + this.turnsByKey.delete(key); + } + + private finalizeSession( + sessionId: string, + args: { endTime: number | undefined; error?: Error }, + ): void { + for (const [, turn] of this.turnsByKey) { + if (turn.sessionId !== sessionId) continue; + this.finalizeTurn(turn.key, args); + } + // Clean up any remaining actions + for (const [key, action] of this.actionsByKey) { + if (!key.includes(`${sessionId}:`)) continue; + if (!action.endedByTurn) { + action.span.log({ metadata: action.metadata }); + action.span.end( + args.endTime === undefined ? undefined : { endTime: args.endTime }, + ); + } + this.actionsByKey.delete(key); + } + this.sessionMetadata.delete(sessionId); + } +} + +// --- Helpers --- + +function turnIdempotencyKey(sessionId: string, turnId: string): string { + return `turn:${sessionId}:${turnId}`; +} + +function deterministicId(...parts: string[]): string { + // Synchronous SHA-256 is not available in all environments; use a + // simple but stable hash for span IDs. This only needs to be + // deterministic, not cryptographically secure. + const text = parts.map((p) => `${p.length}:${p}`).join("\0"); + let h1 = 0x811c9dc5; + let h2 = 0x1000193; + for (let i = 0; i < text.length; i++) { + const c = text.charCodeAt(i); + h1 = Math.imul(h1 ^ c, 0x01000193); + h2 = Math.imul(h2 ^ c, 0x01000193); + } + const hex = + (h1 >>> 0).toString(16).padStart(8, "0") + + (h2 >>> 0).toString(16).padStart(8, "0"); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(4, 8)}-${hex.slice(0, 4)}-${hex.slice(0, 12)}`; +} + +function modelMetadataFromOperation(operation: { + readonly modelId: string; + readonly provider: string; +}): Record { + const modelId = operation.modelId; + if (!modelId || modelId.trim().startsWith("dynamic:")) return {}; + return modelMetadataFromModelId(modelId); +} + +function modelMetadataFromModelId(modelId: string): Record { + const normalized = modelId.trim(); + if (!normalized) return {}; + const slashIndex = normalized.indexOf("/"); + if (slashIndex > 0 && slashIndex < normalized.length - 1) { + return { + model: normalized.slice(slashIndex + 1), + provider: normalized.slice(0, slashIndex), + }; + } + return { model: normalized }; +} + +function actionSpanName( + kind: EveInstrumentationActionKind, + name: string, +): string { + switch (kind) { + case "subagent-call": + return name || "agent"; + default: + return name; + } +} + +function actionMetadata( + turnMetadata: Record, + kind: EveInstrumentationActionKind, +): Record { + const { model: _model, provider: _provider, ...rest } = turnMetadata; + void _model; + void _provider; + return { + ...rest, + "eve.action_kind": kind, + }; +} + +function usageToMetrics( + usage: EveInstrumentationUsage, +): Record { + const metrics: Record = {}; + if (typeof usage.inputTokens === "number" && usage.inputTokens >= 0) { + metrics.prompt_tokens = usage.inputTokens; + } + if (typeof usage.outputTokens === "number" && usage.outputTokens >= 0) { + metrics.completion_tokens = usage.outputTokens; + } + if ( + metrics.prompt_tokens !== undefined && + metrics.completion_tokens !== undefined + ) { + metrics.tokens = metrics.prompt_tokens + metrics.completion_tokens; + } + const cacheRead = usage.inputTokenDetails?.cacheReadTokens; + if (typeof cacheRead === "number" && cacheRead >= 0) { + metrics.prompt_cached_tokens = cacheRead; + } + const cacheWrite = usage.inputTokenDetails?.cacheWriteTokens; + if (typeof cacheWrite === "number" && cacheWrite >= 0) { + metrics.prompt_cache_creation_tokens = cacheWrite; + } + return metrics; +} + +function captureModelInput( + input: EveInstrumentationModelInput, +): unknown | undefined { + try { + const value: unknown[] = []; + if (input.instructions !== undefined) { + value.push(input.instructions); + } + value.push(...input.messages); + return JSON.parse(JSON.stringify(value)); + } catch { + return undefined; + } +} + +function contentPartsToOutput( + content: readonly EveInstrumentationContentPart[] | undefined, + finishReason: string, +): unknown | undefined { + if (!content || content.length === 0) return undefined; + const textParts: string[] = []; + const reasoningParts: string[] = []; + const toolCalls: unknown[] = []; + for (const part of content) { + switch (part.type) { + case "text": + textParts.push(part.text); + break; + case "reasoning": + reasoningParts.push(part.text); + break; + case "tool-call": + toolCalls.push({ + function: { + arguments: JSON.stringify(part.input), + name: part.toolName, + }, + id: part.callId, + type: "function", + }); + break; + // tool-result and tool-error are not part of model output + } + } + const message: Record = { + content: textParts.length > 0 ? textParts.join("") : null, + role: "assistant", + }; + if (reasoningParts.length > 0) { + message["reasoning"] = reasoningParts.map((content) => ({ content })); + } + if (toolCalls.length > 0) { + message["tool_calls"] = toolCalls; + } + return [ + { + finish_reason: finishReason, + index: 0, + message, + }, + ]; +} + +function actionOutputToValue( + output: + | { readonly type: "result"; readonly output?: unknown } + | { readonly type: "error"; readonly error?: unknown }, +): unknown | undefined { + if (output.type === "result") return output.output; + if (output.type === "error") return output.error; + return undefined; +} + +function normalizeError(error: unknown): Error | undefined { + if (error === undefined || error === null) return undefined; + if (error instanceof Error) return error; + if (typeof error === "string") return new Error(error); + if (isObject(error)) { + const message = + typeof error["message"] === "string" ? error["message"] : "Eve error"; + const code = typeof error["code"] === "string" ? error["code"] : undefined; + const result = new Error(code ? `${code}: ${message}` : message); + if (error["details"] !== undefined) { + result.cause = error["details"]; + } + return result; + } + return new Error(String(error)); +} + +export type { EveProviderDefinition, EveContentOptions }; diff --git a/js/src/vendor-sdk-types/eve-provider.ts b/js/src/vendor-sdk-types/eve-provider.ts new file mode 100644 index 000000000..deed1201e --- /dev/null +++ b/js/src/vendor-sdk-types/eve-provider.ts @@ -0,0 +1,303 @@ +/** + * Vendored types for eve's instrumentation provider contract. + * + * These mirror the public types exported from `eve/instrumentation` when the + * `experimental.instrumentationProviders` flag is on. Keep this surface + * intentionally narrow — only the fields the Braintrust provider reads, + * correlates, or logs. + */ + +export type EveInstrumentationCapture = "content" | "metadata"; + +/** + * What this provider records of the conversation itself. + * + * Mirrors eve's `ContentOptions`: declining is per provider, not per process. + * Content is only projected by eve when at least one provider asks, and a + * provider that declined never receives it. Defaults to `false` for both, + * matching eve's convention. + */ +export interface EveContentOptions { + /** Record model prompts and tool call inputs. Defaults to `false`. */ + readonly recordInputs?: boolean; + /** Record model responses and tool call outputs. Defaults to `false`. */ + readonly recordOutputs?: boolean; +} + +export type EveInstrumentationEnvironment = + | "development" + | "preview" + | "production"; + +export interface EveProviderSetupContext { + readonly agentName: string; + readonly environment?: EveInstrumentationEnvironment; + readonly frameworkVersion?: string; +} + +export interface EveProviderState { + get(): unknown; + set(value: unknown): void; +} + +export interface EveProviderContext { + readonly state: EveProviderState; +} + +export interface EveInstrumentationAttemptScope { + readonly attemptId: string; + readonly attemptIndex: number; + readonly functionId?: string; + readonly rootSessionId?: string; + readonly sessionId: string; + readonly stepIndex: number; + readonly turnId: string; +} + +export interface EveInstrumentationOperationRef { + readonly modelId: string; + readonly operationId: string; + readonly provider: string; +} + +export interface EveInstrumentationModelRef { + readonly modelId: string; + readonly provider: string; +} + +export interface EveInstrumentationUsage { + readonly inputTokenDetails?: { + readonly cacheReadTokens?: number; + readonly cacheWriteTokens?: number; + }; + readonly inputTokens?: number; + readonly outputTokens?: number; +} + +export interface EveInstrumentationModelInput { + readonly instructions?: unknown; + readonly messages: readonly unknown[]; +} + +export type EveInstrumentationContentPart = + | { readonly type: "text"; readonly text: string } + | { readonly type: "reasoning"; readonly text: string } + | { + readonly type: "tool-call"; + readonly callId: string; + readonly input: unknown; + readonly toolName: string; + } + | { + readonly type: "tool-result"; + readonly callId: string; + readonly input: unknown; + readonly output: unknown; + readonly toolName: string; + } + | { + readonly type: "tool-error"; + readonly callId: string; + readonly error: unknown; + readonly input: unknown; + readonly toolName: string; + }; + +export type EveInstrumentationActionKind = + | "load-skill" + | "remote-agent-call" + | "subagent-call" + | "tool-call"; + +export type EveInstrumentationActionOutput = + | { readonly type: "result"; readonly output?: unknown } + | { readonly type: "error"; readonly error?: unknown }; + +export type EveInstrumentationActionOutcome = + | "abandoned" + | "cancelled" + | "completed" + | "failed" + | "rejected"; + +export interface EveInstrumentationParentLineage { + readonly callId: string; + readonly sessionId: string; + readonly subagentName?: string; + readonly turnId: string; +} + +export interface EveInstrumentationTraceContext { + readonly traceparent?: string; + readonly tracestate?: string; + readonly baggage?: string; +} + +// --- Events --- + +export interface EveInstrumentationSessionStartedEvent { + readonly type: "session.started"; + readonly agentName?: string; + readonly channelKind?: string; + readonly idempotencyKey: string; + readonly parentTraceContext?: EveInstrumentationTraceContext; + readonly rootSessionId: string; + readonly sessionId: string; +} + +export interface EveInstrumentationSessionSettledEvent { + readonly type: "session.completed" | "session.waiting"; + readonly idempotencyKey: string; + readonly sessionId: string; + readonly turnId?: string; +} + +export interface EveInstrumentationSessionFailedEvent { + readonly type: "session.failed"; + readonly error?: unknown; + readonly idempotencyKey: string; + readonly sessionId: string; + readonly turnId?: string; +} + +export interface EveInstrumentationTurnStartedEvent { + readonly type: "turn.started"; + readonly idempotencyKey: string; + readonly parentLineage?: EveInstrumentationParentLineage; + readonly parentTraceContext?: EveInstrumentationTraceContext; + readonly rootSessionId: string; + readonly sequence: number; + readonly sessionId: string; + readonly turnId: string; +} + +export interface EveInstrumentationTurnSettledEvent { + readonly type: "turn.cancelled" | "turn.completed"; + readonly idempotencyKey: string; + readonly sessionId: string; + readonly turnId: string; +} + +export interface EveInstrumentationTurnFailedEvent { + readonly type: "turn.failed"; + readonly error?: unknown; + readonly idempotencyKey: string; + readonly sessionId: string; + readonly turnId: string; +} + +export interface EveInstrumentationStepAttemptStartedEvent { + readonly type: "step.attempt.started"; + readonly idempotencyKey: string; + readonly operation: EveInstrumentationOperationRef; + readonly scope: EveInstrumentationAttemptScope; +} + +export interface EveInstrumentationStepAttemptCompletedEvent { + readonly type: "step.attempt.completed"; + readonly idempotencyKey: string; + readonly scope: EveInstrumentationAttemptScope; +} + +export interface EveInstrumentationStepAttemptFailedEvent { + readonly type: "step.attempt.failed"; + readonly error?: unknown; + readonly idempotencyKey: string; + readonly scope: EveInstrumentationAttemptScope; +} + +export interface EveInstrumentationStepAttemptMetadataEvent { + readonly type: "step.attempt.metadata"; + readonly idempotencyKey: string; + readonly scope: EveInstrumentationAttemptScope; + readonly providerMetadata: Readonly>; +} + +export interface EveInstrumentationModelCallStartedEvent { + readonly type: "model.call.started"; + readonly idempotencyKey: string; + readonly input?: EveInstrumentationModelInput; + readonly model: EveInstrumentationModelRef; + readonly scope: EveInstrumentationAttemptScope; +} + +export interface EveInstrumentationModelCallCompletedEvent { + readonly type: "model.call.completed"; + readonly content?: readonly EveInstrumentationContentPart[]; + readonly finishReason: string; + readonly idempotencyKey: string; + readonly scope: EveInstrumentationAttemptScope; + readonly usage: EveInstrumentationUsage; +} + +export interface EveInstrumentationModelCallFailedEvent { + readonly type: "model.call.failed"; + readonly error?: unknown; + readonly idempotencyKey: string; + readonly scope: EveInstrumentationAttemptScope; +} + +export interface EveInstrumentationActionStartedEvent { + readonly type: "action.started"; + readonly callId: string; + readonly idempotencyKey: string; + readonly input?: unknown; + readonly kind: EveInstrumentationActionKind; + readonly name: string; + readonly scope: EveInstrumentationAttemptScope; +} + +export interface EveInstrumentationActionCompletedEvent { + readonly type: "action.completed"; + readonly acceptedAtMs?: number; + readonly idempotencyKey: string; + readonly outcome: "completed"; + readonly output: EveInstrumentationActionOutput; + readonly scope: EveInstrumentationAttemptScope; + readonly usage?: EveInstrumentationUsage; +} + +export interface EveInstrumentationActionFailedEvent { + readonly type: "action.failed"; + readonly acceptedAtMs?: number; + readonly error?: unknown; + readonly errorCode?: string; + readonly idempotencyKey: string; + readonly outcome: Exclude; + readonly scope: EveInstrumentationAttemptScope; +} + +type EveInstrumentationEvent = + | EveInstrumentationSessionStartedEvent + | EveInstrumentationSessionSettledEvent + | EveInstrumentationSessionFailedEvent + | EveInstrumentationTurnStartedEvent + | EveInstrumentationTurnSettledEvent + | EveInstrumentationTurnFailedEvent + | EveInstrumentationStepAttemptStartedEvent + | EveInstrumentationStepAttemptCompletedEvent + | EveInstrumentationStepAttemptFailedEvent + | EveInstrumentationStepAttemptMetadataEvent + | EveInstrumentationModelCallStartedEvent + | EveInstrumentationModelCallCompletedEvent + | EveInstrumentationModelCallFailedEvent + | EveInstrumentationActionStartedEvent + | EveInstrumentationActionCompletedEvent + | EveInstrumentationActionFailedEvent; + +export type EveProviderEvents = { + readonly [TType in EveInstrumentationEvent["type"]]?: ( + event: Extract, + ctx: EveProviderContext, + ) => void | PromiseLike; +}; + +export interface EveProviderDefinition { + readonly capture?: EveInstrumentationCapture; + readonly events?: EveProviderEvents; + readonly setup?: ( + context: EveProviderSetupContext, + ) => void | PromiseLike; + readonly flush?: () => void | PromiseLike; + readonly shutdown?: () => void | PromiseLike; +} diff --git a/js/tsup.config.ts b/js/tsup.config.ts index 14bca6b36..b44b85b80 100644 --- a/js/tsup.config.ts +++ b/js/tsup.config.ts @@ -20,7 +20,7 @@ export default defineConfig([ }, format: ["cjs", "esm"], outDir: "dist", - external: ["zod"], + external: ["node:module", "zod"], removeNodeProtocol: false, dts: { // Split DTS generation to reduce memory usage @@ -86,7 +86,7 @@ export default defineConfig([ }, format: ["cjs", "esm"], outDir: "dist", - external: ["zod"], + external: ["node:module", "zod"], removeNodeProtocol: false, platform: "browser", splitting: false, @@ -98,7 +98,7 @@ export default defineConfig([ entry: ["src/instrumentation/index.ts"], format: ["cjs", "esm"], outDir: "dist/instrumentation", - external: ["@braintrust/instrumentation-core", "zod"], + external: ["@braintrust/instrumentation-core", "node:module", "zod"], dts: { compilerOptions: { skipLibCheck: true,