Skip to content
Draft
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/trace-state-dead-field.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Remove a dead `channelKind` field from local trace session state and collapse the per-attempt model and tool context maps into one, keyed by the operation id that already distinguishes them.
1 change: 0 additions & 1 deletion packages/eve/src/harness/instrumentation-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export interface InstrumentationAttemptStartedEvent {
export interface InstrumentationSessionStartedEvent {
readonly type: "session.started";
readonly agentName?: string;
readonly channelKind?: string;
readonly parentTraceContext?: InstrumentationTraceContext;
readonly rootSessionId: string;
readonly sessionId: string;
Expand Down
11 changes: 2 additions & 9 deletions packages/eve/src/tracing/agent-otel-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,8 @@ const TRUNCATED_MESSAGES_KEY = "eve.truncated";
*/
export function contentAttribute(value: unknown, strip = true): string | undefined {
if (value === undefined) return undefined;
const prepared = strip ? stripContentNoise(value, 0) : value;
let json: string | undefined;
try {
json = JSON.stringify(prepared);
} catch {
return undefined;
}
if (json === undefined) return undefined;
return textContentAttribute(json);
const json = stringifyContent(strip ? stripContentNoise(value, 0) : value);
return json === undefined ? undefined : textContentAttribute(json);
}

/**
Expand Down
2 changes: 0 additions & 2 deletions packages/eve/src/tracing/agent-otel-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ async function publishTurnStarted(input: {
const rootSessionId = input.rootSessionId ?? input.sessionId;
await input.hooks.publish({
agentName: "weather",
channelKind: "http",
parentTraceContext: input.parentTraceContext,
rootSessionId,
sessionId: input.sessionId,
Expand Down Expand Up @@ -319,7 +318,6 @@ describe("createAgentOtelInstrumentation", () => {
};
await runtime.hooks.publish({
agentName: "weather",
channelKind: "http",
rootSessionId: "session-1",
sessionId: "session-1",
type: "session.started",
Expand Down
92 changes: 45 additions & 47 deletions packages/eve/src/tracing/agent-otel-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,16 @@ interface ToolSpanState extends SpanState {
/** Sized so an ordinary session stays one trace and only an outsized one rolls. */
export const SESSION_WINDOW_TURN_LIMIT = 200;

/** What opening a session's trace state needs, narrower than the start event. */
interface SessionContextInput {
readonly agentName?: string;
readonly parentTraceContext?: InstrumentationTraceContext;
readonly rootSessionId: string;
readonly sessionId: string;
}

export interface AgentSessionTraceState {
readonly agentName?: string;
readonly channelKind?: string;
readonly context: SpanContext;
readonly rootSessionId: string;
readonly turnsInWindow: number;
Expand Down Expand Up @@ -96,23 +103,23 @@ export class InMemoryAgentTraceStateStore implements AgentTraceStateStore {
}

deleteTurn(sessionId: string, turnId: string): void {
this.#turns.delete(turnKey(sessionId, turnId));
this.#turns.delete(agentTurnStateKey(sessionId, turnId));
}

getSession(sessionId: string): AgentSessionTraceState | undefined {
return this.#sessions.get(sessionId);
}

getTurn(sessionId: string, turnId: string): AgentTurnTraceState | undefined {
return this.#turns.get(turnKey(sessionId, turnId));
return this.#turns.get(agentTurnStateKey(sessionId, turnId));
}

setSession(sessionId: string, state: AgentSessionTraceState): void {
this.#sessions.set(sessionId, state);
}

setTurn(sessionId: string, turnId: string, state: AgentTurnTraceState): void {
this.#turns.set(turnKey(sessionId, turnId), state);
this.#turns.set(agentTurnStateKey(sessionId, turnId), state);
}
}

Expand All @@ -139,15 +146,25 @@ export function createAgentOtelInstrumentation(
input: AgentOtelInstrumentationInput,
): AgentOtelInstrumentation {
const captureContent = input.captureContent ?? true;
const executionContexts = new WeakMap<
InstrumentationAttemptScope,
{ readonly models: Map<string, Context>; readonly tools: Map<string, Context> }
>();
// Keyed by the operation id, which already encodes whether the operation is
// a model call or a tool call (see `ai-sdk-hook-bridge.ts`).
const executionContexts = new WeakMap<InstrumentationAttemptScope, Map<string, Context>>();
// A serverless turn runs inside one `turnStep` "use step" invocation. If
// that worker is lost, Workflow retries the whole step from entry rather
// than resuming this callback sequence in a replacement process.
const steps = new WeakMap<InstrumentationAttemptScope, AttemptSpanState>();

/** Attributes every span within one attempt carries: framework, session, turn, step. */
const attemptScopeAttributes = (scope: InstrumentationAttemptScope) => ({
"agent.framework.name": "eve",
"agent.framework.version": input.frameworkVersion,
"agent.root.session.id": scope.rootSessionId ?? scope.sessionId,
"agent.session.id": scope.sessionId,
"agent.step.attempt": scope.attemptIndex,
"agent.step.index": scope.stepIndex,
"agent.turn.id": scope.turnId,
});

const onSessionStarted = async (event: InstrumentationSessionStartedEvent): Promise<void> => {
await ensureSessionContext(event);
};
Expand All @@ -156,12 +173,9 @@ export function createAgentOtelInstrumentation(
const session = advanceSessionWindow(
event.sessionId,
await ensureSessionContext({
agentName: undefined,
channelKind: undefined,
parentTraceContext: event.parentTraceContext,
rootSessionId: event.rootSessionId,
sessionId: event.sessionId,
type: "session.started",
}),
);
const span = input.tracer.startSpan(
Expand Down Expand Up @@ -202,13 +216,7 @@ export function createAgentOtelInstrumentation(
"agent.step",
{
attributes: {
"agent.session.id": event.scope.sessionId,
"agent.framework.name": "eve",
"agent.framework.version": input.frameworkVersion,
"agent.root.session.id": event.scope.rootSessionId ?? event.scope.sessionId,
"agent.step.attempt": event.scope.attemptIndex,
"agent.step.index": event.scope.stepIndex,
"agent.turn.id": event.scope.turnId,
...attemptScopeAttributes(event.scope),
"agent.name": event.scope.functionId,
},
},
Expand Down Expand Up @@ -323,12 +331,12 @@ export function createAgentOtelInstrumentation(
if (system !== undefined) span.setAttribute("ai.prompt.system", system);
}
const state = { context: trace.setSpan(attempt.operation.context, span), span };
getExecutionContexts(event.scope).models.set(event.id, state.context);
executionContextsFor(event.scope).set(event.id, state.context);
return state;
};

const afterModelCall = (event: InstrumentationModelCallTerminalEvent, state: unknown): void => {
executionContexts.get(event.scope)?.models.delete(event.id);
executionContexts.get(event.scope)?.delete(event.id);
if (!isSpanState(state)) return;
if (event.type === "model.call.failed") {
recordError(state.span, event.error);
Expand Down Expand Up @@ -388,16 +396,10 @@ export function createAgentOtelInstrumentation(
"agent.action",
{
attributes: {
...attemptScopeAttributes(event.scope),
"agent.action.call_id": event.source.toolCall.toolCallId,
"agent.action.kind": "tool",
"agent.action.name": event.source.toolCall.toolName,
"agent.framework.name": "eve",
"agent.framework.version": input.frameworkVersion,
"agent.root.session.id": event.scope.rootSessionId ?? event.scope.sessionId,
"agent.session.id": event.scope.sessionId,
"agent.step.attempt": event.scope.attemptIndex,
"agent.step.index": event.scope.stepIndex,
"agent.turn.id": event.scope.turnId,
},
},
attempt.step.context,
Expand All @@ -423,12 +425,12 @@ export function createAgentOtelInstrumentation(
span: actionSpan,
toolSpan,
};
getExecutionContexts(event.scope).tools.set(event.id, state.context);
executionContextsFor(event.scope).set(event.id, state.context);
return state;
};

const afterToolCall = (event: InstrumentationToolCallTerminalEvent, state: unknown): void => {
executionContexts.get(event.scope)?.tools.delete(event.id);
executionContexts.get(event.scope)?.delete(event.id);
if (!isToolSpanState(state)) return;
if (event.type === "tool.call.failed") {
recordError(state.toolSpan, event.error);
Expand Down Expand Up @@ -473,13 +475,12 @@ export function createAgentOtelInstrumentation(
};

const ensureSessionContext = async (
event: InstrumentationSessionStartedEvent,
event: SessionContextInput,
): Promise<AgentSessionTraceState> => {
let state = await input.stateStore.getSession(event.sessionId);
if (state === undefined) {
state = {
agentName: event.agentName,
channelKind: event.channelKind,
context:
event.parentTraceContext === undefined
? openSessionWindow({
Expand Down Expand Up @@ -551,30 +552,27 @@ export function createAgentOtelInstrumentation(
},
},
runInContext(operation, execute) {
const contexts = executionContexts.get(operation.scope);
const parent =
operation.type === "model.call"
? contexts?.models.get(operation.id)
: contexts?.tools.get(operation.id);
const parent = executionContexts.get(operation.scope)?.get(operation.id);
return parent === undefined ? execute() : context.with(parent, execute);
},
};

function getExecutionContexts(scope: InstrumentationAttemptScope): {
readonly models: Map<string, Context>;
readonly tools: Map<string, Context>;
} {
let state = executionContexts.get(scope);
if (state === undefined) {
state = { models: new Map(), tools: new Map() };
executionContexts.set(scope, state);
function executionContextsFor(scope: InstrumentationAttemptScope): Map<string, Context> {
let contexts = executionContexts.get(scope);
if (contexts === undefined) {
contexts = new Map();
executionContexts.set(scope, contexts);
}
return state;
return contexts;
}
}

function turnKey(sessionId: string, turnId: string): string {
return `${sessionId}:${turnId}`;
/**
* Composite key for one turn's trace state. `\0` separates the parts because
* it cannot occur in either id, so no pair of ids can collide.
*/
export function agentTurnStateKey(sessionId: string, turnId: string): string {
return `${sessionId}\0${turnId}`;
}

function parentLineageAttributes(
Expand Down
14 changes: 6 additions & 8 deletions packages/eve/src/tracing/agent-trace-context-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
AgentTraceStateStore,
AgentTurnTraceState,
} from "#tracing/agent-otel-provider.js";
import { agentTurnStateKey } from "#tracing/agent-otel-provider.js";

interface AgentTraceContextState {
readonly sessions: Readonly<Record<string, AgentSessionTraceState>>;
Expand Down Expand Up @@ -58,7 +59,7 @@ export class ContextAgentTraceStateStore implements AgentTraceStateStore {
deleteTurn(sessionId: string, turnId: string): void {
updateState((state) => {
const turns = { ...state.turns };
delete turns[turnKey(sessionId, turnId)];
delete turns[agentTurnStateKey(sessionId, turnId)];
return { ...state, turns };
});
}
Expand All @@ -68,7 +69,9 @@ export class ContextAgentTraceStateStore implements AgentTraceStateStore {
}

getTurn(sessionId: string, turnId: string): AgentTurnTraceState | undefined {
return contextStorage.getStore()?.get(AgentTraceContextKey)?.turns[turnKey(sessionId, turnId)];
return contextStorage.getStore()?.get(AgentTraceContextKey)?.turns[
agentTurnStateKey(sessionId, turnId)
];
}

setSession(sessionId: string, value: AgentSessionTraceState): void {
Expand All @@ -81,7 +84,7 @@ export class ContextAgentTraceStateStore implements AgentTraceStateStore {
setTurn(sessionId: string, turnId: string, value: AgentTurnTraceState): void {
updateState((state) => ({
...state,
turns: { ...state.turns, [turnKey(sessionId, turnId)]: value },
turns: { ...state.turns, [agentTurnStateKey(sessionId, turnId)]: value },
}));
}
}
Expand All @@ -90,10 +93,6 @@ function updateState(update: (state: AgentTraceContextState) => AgentTraceContex
loadContext().set(AgentTraceContextKey, (state) => update(state ?? { sessions: {}, turns: {} }));
}

function turnKey(sessionId: string, turnId: string): string {
return `${sessionId}\0${turnId}`;
}

function serializeState(state: AgentTraceContextState): unknown {
return {
sessions: Object.fromEntries(
Expand Down Expand Up @@ -127,7 +126,6 @@ function deserializeState(data: unknown): AgentTraceContextState {
if (!isRecord(value) || !isSpanContext(value.context)) return undefined;
return {
agentName: typeof value.agentName === "string" ? value.agentName : undefined,
channelKind: typeof value.channelKind === "string" ? value.channelKind : undefined,
context: value.context,
rootSessionId: typeof value.rootSessionId === "string" ? value.rootSessionId : "",
turnsInWindow: typeof value.turnsInWindow === "number" ? value.turnsInWindow : 0,
Expand Down
Loading