Skip to content
Merged
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/vercel-session-id-attribute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Agent spans now carry `vercel.session_id` (set to the root session id) when running on Vercel, so traces can be equality-looked-up across all session windows via an indexed column. The attribute is omitted in local `eve dev`.
5 changes: 5 additions & 0 deletions packages/eve/src/tracing/agent-action-instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface AgentActionContext {

/** Builds durable `agent.action` spans around eve's runtime dispatch boundary. */
export function createAgentActionInstrumentation(input: {
readonly emitVercelSessionId?: boolean;
readonly frameworkVersion: string;
readonly idGenerator: AgentSpanIdGenerator;
readonly recordInputs: boolean;
Expand All @@ -55,6 +56,7 @@ export function createAgentActionInstrumentation(input: {
readonly stateStore: AgentTraceStateStore;
readonly tracer: Tracer;
}): AgentActionInstrumentation {
const emitVercelSessionId = input.emitVercelSessionId ?? false;
const byAttempt = new Map<string, Set<string>>();

const onStarted = async (event: InstrumentationActionStartedEvent): Promise<void> => {
Expand Down Expand Up @@ -131,6 +133,9 @@ export function createAgentActionInstrumentation(input: {
"agent.step.attempt": state.attemptIndex,
"agent.step.index": state.stepIndex,
"agent.turn.id": state.turnId,
...(emitVercelSessionId
? { "vercel.session_id": state.rootSessionId }
: {}),
},
startTime: state.startTimeMs,
},
Expand Down
5 changes: 5 additions & 0 deletions packages/eve/src/tracing/agent-approval-instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export function createAgentApprovalInstrumentation(input: {
turnId: string,
callId: string,
) => Promise<AgentActionContext | undefined>;
readonly emitVercelSessionId?: boolean;
readonly frameworkVersion: string;
readonly idGenerator: AgentSpanIdGenerator;
readonly recordInputs: boolean;
Expand All @@ -50,6 +51,7 @@ export function createAgentApprovalInstrumentation(input: {
NonNullable<InstrumentationProviderDefinition["events"]>,
"input.requested" | "input.resolved"
> {
const emitVercelSessionId = input.emitVercelSessionId ?? false;
const onRequested = async (
event: InstrumentationInputRequestedEvent,
ctx: InstrumentationHandlerContext,
Expand Down Expand Up @@ -110,6 +112,9 @@ export function createAgentApprovalInstrumentation(input: {
"agent.step.attempt": state.attemptIndex,
"agent.step.index": state.stepIndex,
"agent.turn.id": state.turnId,
...(emitVercelSessionId
? { "vercel.session_id": state.rootSessionId }
: {}),
},
startTime: state.startTimeMs,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ interface ChannelDeliverySpanState {

/** Builds durable channel delivery spans around the turn that consumes each request. */
export function createAgentChannelDeliveryInstrumentation(input: {
readonly emitVercelSessionId?: boolean;
readonly ensureSessionContext: (
event: InstrumentationSessionStartedEvent,
) => Promise<AgentSessionTraceState>;
Expand All @@ -50,6 +51,7 @@ export function createAgentChannelDeliveryInstrumentation(input: {
| "channel.delivery.failed"
| "channel.delivery.started"
> {
const emitVercelSessionId = input.emitVercelSessionId ?? false;
const onStarted = async (
event: InstrumentationChannelDeliveryStartedEvent,
ctx: InstrumentationHandlerContext,
Expand Down Expand Up @@ -130,6 +132,9 @@ export function createAgentChannelDeliveryInstrumentation(input: {
"agent.session.window": session?.window ?? state.window,
"agent.turn.id": event.turnId,
"agent.turn.sequence": event.sequence,
...(emitVercelSessionId
? { "vercel.session_id": event.rootSessionId }
: {}),
},
kind: SpanKind.CONSUMER,
links:
Expand Down
44 changes: 44 additions & 0 deletions packages/eve/src/tracing/agent-otel-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ interface TestRuntime {
function createRuntime(
stateStore: AgentTraceStateStore = new InMemoryAgentTraceStateStore(),
tracePolicy: TraceCapturePolicy | null = () => true,
options: { readonly emitVercelSessionId?: boolean } = {},
): TestRuntime {
const exporter = new InMemorySpanExporter();
const idGenerator = new AgentSpanIdGenerator();
Expand All @@ -73,6 +74,7 @@ function createRuntime(
const agentOtelInput: Omit<AgentOtelInstrumentationInput, "tracePolicy"> & {
tracePolicy?: TraceCapturePolicy;
} = {
emitVercelSessionId: options.emitVercelSessionId,
frameworkVersion: "test",
idGenerator,
recordInputs: true,
Expand Down Expand Up @@ -1847,4 +1849,46 @@ describe("createAgentOtelInstrumentation", () => {
});
expect(byName(spans, "agent.turn")[0]!.status.code).toBe(SpanStatusCode.UNSET);
});

describe("emitVercelSessionId", () => {
it("emits vercel.session_id on session, turn, step, and action spans when enabled", async () => {
const runtime = createRuntime(undefined, undefined, { emitVercelSessionId: true });
await emitAttempt({
hooks: runtime.hooks,
runInContext: runtime.runInContext,
sessionId: "session-1",
turnId: "turn-1",
turnSequence: 0,
});
await runtime.provider.forceFlush();

const spans = runtime.exporter.getFinishedSpans();
const session = byName(spans, "agent.session")[0]!;
const turn = byName(spans, "agent.turn")[0]!;
const step = byName(spans, "agent.step")[0]!;
const action = byName(spans, "agent.action")[0]!;

expect(session.attributes["vercel.session_id"]).toBe("session-1");
expect(turn.attributes["vercel.session_id"]).toBe("session-1");
expect(step.attributes["vercel.session_id"]).toBe("session-1");
expect(action.attributes["vercel.session_id"]).toBe("session-1");
});

it("does not emit vercel.session_id by default", async () => {
const runtime = createRuntime();
await emitAttempt({
hooks: runtime.hooks,
runInContext: runtime.runInContext,
sessionId: "session-1",
turnId: "turn-1",
turnSequence: 0,
});
await runtime.provider.forceFlush();

const spans = runtime.exporter.getFinishedSpans();
for (const span of spans) {
expect(span.attributes["vercel.session_id"]).toBeUndefined();
}
});
});
});
18 changes: 18 additions & 0 deletions packages/eve/src/tracing/agent-otel-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ export interface AgentOtelInstrumentationInput {
readonly stateStore: AgentTraceStateStore;
readonly tracer: Tracer;
readonly tracePolicy?: TraceCapturePolicy;
/**
* When true, every agent span also carries `vercel.session_id` set to the
* root session id. The VDP trace ingestion materialized view extracts this
* into the indexed `sessionId` column so Agent Runs can equality-lookup a
* session across all trace windows without scanning the attribute map.
* Emitted only on Vercel (not local `eve dev`).
*/
readonly emitVercelSessionId?: boolean;
}

/** OTel event definition and its trusted framework context runner. */
Expand All @@ -93,6 +101,7 @@ export function createAgentOtelInstrumentation(
): AgentOtelInstrumentation {
const recordInputs = input.recordInputs ?? false;
const recordOutputs = input.recordOutputs ?? false;
const emitVercelSessionId = input.emitVercelSessionId ?? false;
const executionContexts = new WeakMap<InstrumentationAttemptScope, Map<string, Context>>();
const attemptScopes = new Map<string, InstrumentationAttemptScope>();
// A serverless turn runs inside one `turnStep` "use step" invocation. If
Expand All @@ -101,6 +110,7 @@ export function createAgentOtelInstrumentation(
const steps = new WeakMap<InstrumentationAttemptScope, AttemptSpanState>();
const modelSpans = new WeakMap<InstrumentationAttemptScope, Map<string, SpanState>>();
const actions = createAgentActionInstrumentation({
emitVercelSessionId,
frameworkVersion: input.frameworkVersion,
idGenerator: input.idGenerator,
recordInputs,
Expand All @@ -114,6 +124,7 @@ export function createAgentOtelInstrumentation(
});
const approvals = createAgentApprovalInstrumentation({
actionContextFor: actions.contextFor,
emitVercelSessionId,
frameworkVersion: input.frameworkVersion,
idGenerator: input.idGenerator,
recordInputs,
Expand Down Expand Up @@ -168,6 +179,9 @@ export function createAgentOtelInstrumentation(
"agent.step.index": event.scope.stepIndex,
"agent.turn.id": event.scope.turnId,
"agent.name": event.scope.functionId,
...(emitVercelSessionId
? { "vercel.session_id": event.scope.rootSessionId ?? event.scope.sessionId }
: {}),
...runtimeContextAttributes(event.runtimeContext),
},
links:
Expand Down Expand Up @@ -274,6 +288,9 @@ export function createAgentOtelInstrumentation(
"agent.session.window": session?.window,
"agent.turn.id": event.turnId,
"agent.turn.sequence": turn.sequence,
...(emitVercelSessionId
? { "vercel.session_id": turn.rootSessionId }
: {}),
},
startTime: turn.startTimeMs,
},
Expand Down Expand Up @@ -411,6 +428,7 @@ export function createAgentOtelInstrumentation(
};

const channelDeliveries = createAgentChannelDeliveryInstrumentation({
emitVercelSessionId,
ensureSessionContext,
frameworkVersion: input.frameworkVersion,
idGenerator: input.idGenerator,
Expand Down
5 changes: 5 additions & 0 deletions packages/eve/src/tracing/agent-otel-session-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from "#tracing/agent-trace-state.js";

interface AgentOtelSessionContextInput {
readonly emitVercelSessionId?: boolean;
readonly frameworkVersion: string;
readonly idGenerator: AgentSpanIdGenerator;
readonly stateStore: AgentTraceStateStore;
Expand All @@ -39,6 +40,7 @@ interface AgentOtelSessionContext {
export function createAgentOtelSessionContext(
input: AgentOtelSessionContextInput,
): AgentOtelSessionContext {
const emitVercelSessionId = input.emitVercelSessionId ?? false;
const openSessionWindow = (window: {
readonly agentName?: string;
readonly channelAudience: ChannelAudience;
Expand All @@ -65,6 +67,9 @@ export function createAgentOtelSessionContext(
"agent.session.id": window.sessionId,
"agent.session.window": window.index,
"agent.trace.schema.version": 1,
...(emitVercelSessionId
? { "vercel.session_id": window.rootSessionId }
: {}),
...(window.previousTraceId === undefined
? {}
: { "agent.session.window.previous.trace.id": window.previousTraceId }),
Expand Down
2 changes: 2 additions & 0 deletions packages/eve/src/tracing/install-instrumentation-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type InstrumentationRuntime,
} from "#harness/instrumentation/runtime.js";
import { createLogger, formatError } from "#internal/logging.js";
import { isEveDevEnvironment } from "#internal/application/dev-environment.js";
import { ContextAgentTraceStateStore } from "#tracing/agent-trace-context-store.js";
import { createAgentOtelInstrumentation } from "#tracing/agent-otel-provider.js";
import { hasSessionRelease, type LocalTracesProcessor } from "#tracing/local-traces.js";
Expand Down Expand Up @@ -48,6 +49,7 @@ export function installInstrumentationRuntime(input: {
serviceName: input.serviceName,
});
const agentOtel = createAgentOtelInstrumentation({
emitVercelSessionId: process.env.VERCEL_ENV !== undefined && !isEveDevEnvironment(),
frameworkVersion: input.frameworkVersion,
idGenerator: otelRuntime.idGenerator,
recordInputs: input.collected.settings.recordInputs,
Expand Down
Loading