Skip to content

feat: Add eve first party eve instrumentation#2203

Open
Luca Forstner (lforst) wants to merge 11 commits into
mainfrom
lforst/eve-agent-instrumentation
Open

feat: Add eve first party eve instrumentation#2203
Luca Forstner (lforst) wants to merge 11 commits into
mainfrom
lforst/eve-agent-instrumentation

Conversation

@lforst

@lforst Luca Forstner (lforst) commented Jul 6, 2026

Copy link
Copy Markdown
Member

Ok this instrumentation is a bit interesting:

  • Eve already has docs for braintrust but the traces it generates are not great. They're otel traces with wild disconnection issues.
  • Eve is most likely deployed to Vercel workflows very often which means each step runs in it's own "function invocation", meaning we cannot rely on ALS.
  • To combat the "can't use ALS" situation, we use the session/turn-IDs that eve gives us to produce hashes that we then use for our trace and span IDs.
  • Also, because the turns are detached runtime-wise, we have a hard time creating a trace for each turn, which is arguably not what we want in any case (?) - so instead we attach all turns, tools, subagents, llm calls to a singular trace as they happen.

The way someone instruments their app is via:

// agent/instrumentation.ts
import { braintrustEveInstrumentation, initLogger } from "braintrust";
import { defineState } from "eve/context";
import { defineInstrumentation } from "eve/instrumentation";

export default defineInstrumentation(
  braintrustEveInstrumentation({
    defineState,
    setup: ({ agentName }) => {
      initLogger({
        projectName: 'project',
      });
    },
  }),
);

plus

// agent/hooks/braintrust.ts
import { braintrustEveHook } from "braintrust";
import { defineHook } from "eve/hooks";

export default defineHook(braintrustEveHook());

@lforst Luca Forstner (lforst) marked this pull request as ready for review July 9, 2026 16:15
@lforst Luca Forstner (lforst) changed the title WIP feat: Add eve first party eve instrumentation feat: Add eve first party eve instrumentation Jul 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no orchestrion config? Or iitm setup?

Comment on lines +130 to +131
recordInputs: false,
recordOutputs: false,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are these false? Should we make them user configurable?

Comment on lines +174 to +177
private sessionsById = new Map<string, SessionState>();
private completedToolKeys = new Set<string>();
private toolsByCallKey = new Map<string, ToolState>();
private turnsByKey = new Map<string, TurnState>();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we bound? Like use an LRU to manage memory?

this.state.update((current) => ({
...normalizeEveTraceState(current),
stepStarts: [
...state.stepStarts,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we use current here? Ditto with some other places where we reference state instead of current.

}
try {
await this.handleEvent(event, ctx, hookMetadata);
await this.flushInstrumentation();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we only call flushInstrumentation for events we handle?

@@ -0,0 +1,5 @@
---
"braintrust": patch

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be a minor!

}
}

class EveBridge {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kind feels weird there is no createTurn and endTurn methods on the bridge here.

turnId: string,
stepIndex: number,
): Promise<string> {
return deterministicEveId("eve:step", sessionId, turnId, String(stepIndex));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can have

type EveEntityKind = "session" | "step" | "subagent" | "tool" | "turn";

async function generateEveIds(
  kind: EveEntityKind,
  ...parts: string[]
): Promise<{ rowId: string; spanId: string }> {
  const [rowId, spanId] = await Promise.all([
    deterministicEveId(`eve:row:${kind}`, ...parts),
    deterministicEveId(`eve:${kind}`, ...parts),
  ]);
  return { rowId, spanId };
}

and take care of most of the use cases, and then we just need rootSpanIdForSession and spanIdForSubagent?

Comment on lines +58 to +63
type ToolState = SpanState & {
endedByTurn?: boolean;
kind: "subagent" | "tool";
stepIndex?: number;
turnKey: string;
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clanker tells me ToolState.kind and ToolState.stepIndex were written at every creation site but never read.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants