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/construct-channel-instrumentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": minor
---

Channel audience is now resolved into a delivery decision used to construct harness instrumentation. `tracePolicy` returns an explicit drop or record decision with input/output controls, and audience is no longer exposed through harness lifecycle events or span export policy context.
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`.
2 changes: 2 additions & 0 deletions docs/guides/instrumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ The third configurable surface, [runtime context events](#runtime-context), atta

Built-in messaging channels classify their instrumentation metadata with an `audience`: `public`, `private`, or `unknown`. Slack public channels and Chat SDK workspace-visible threads are public; direct and private conversations are private; platform surfaces without enough visibility evidence remain unknown.

With instrumentation providers enabled, `otel({ tracePolicy })` maps that classification to a decision before the harness runs. eve constructs the harness instrumentation from that decision. Return `{ action: "drop" }` to omit the trace, or `{ action: "record", recordInputs, recordOutputs }` to create it with an explicit content ceiling. The harness receives neither audience nor the decision.

## Channel delivery traces

Instrumentation providers receive `channel.delivery.started` followed by
Expand Down
4 changes: 4 additions & 0 deletions packages/eve/src/context/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type { SandboxAccess } from "#sandbox/state.js";
import type { RunMode } from "#shared/run-mode.js";
import type { RuntimeModelReference } from "#runtime/agent/bootstrap.js";
import type { PreparedRuntimeDelegationTool } from "#runtime/sessions/turn.js";
import type { InstrumentationDecision } from "#shared/instrumentation-decision.js";

// Re-export so consumers don't need a direct channel/ import.
export type { SessionAuthContext, SessionParent, SessionTurn } from "#channel/types.js";
Expand Down Expand Up @@ -89,6 +90,9 @@ export const ActiveChannelDeliveriesKey = new ContextKey<readonly ActiveChannelD
export const ChannelInstrumentationKey = new ContextKey<ChannelInstrumentationProjection>(
"eve.channelInstrumentation",
);
export const InstrumentationDecisionKey = new ContextKey<InstrumentationDecision>(
"eve.instrumentationDecision",
);
export const ModeKey = new ContextKey<RunMode>("eve.mode");
export const ParentSessionKey = new ContextKey<SessionParent>("eve.parentSession");
/** Separate from {@link ParentSessionKey} so it stays out of what extensions read. */
Expand Down
68 changes: 68 additions & 0 deletions packages/eve/src/execution/delivery-instrumentation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, expect, it, vi } from "vitest";

import { ContextContainer } from "#context/container.js";
import { ChannelInstrumentationKey, InstrumentationDecisionKey } from "#context/keys.js";
import { prepareDeliveryInstrumentation } from "#execution/delivery-instrumentation.js";
import { createInstrumentationHooks } from "#harness/instrumentation/lifecycle.js";
import { createInstrumentationRuntime } from "#harness/instrumentation/runtime.js";
import type { InstrumentationRuntime } from "#harness/instrumentation/runtime.js";

function runtime(resolveDecision: InstrumentationRuntime["resolveDecision"]) {
return createInstrumentationRuntime({
forceFlush: async () => undefined,
hooks: createInstrumentationHooks([]),
otelSettings: {
recordInputs: true,
recordOutputs: true,
traceChannelRequests: false,
},
resolveDecision,
runInContext: (_operation, execute) => execute(),
runWithTracingSuppressed: (execute) => execute(),
shutdown: async () => undefined,
});
}

describe("prepareDeliveryInstrumentation", () => {
it("resolves audience before constructing harness capabilities", () => {
const ctx = new ContextContainer();
ctx.set(ChannelInstrumentationKey, {
kind: "channel:test",
metadata: { audience: "public" },
});
const resolveDecision = vi.fn(() => ({
action: "record" as const,
recordInputs: false,
recordOutputs: true,
}));

const constructed = prepareDeliveryInstrumentation({
ctx,
delivery: { kind: "deliver" },
instrumentation: runtime(resolveDecision),
rootSessionId: "session-1",
sessionId: "session-1",
});

expect(resolveDecision).toHaveBeenCalledWith(expect.objectContaining({ audience: "public" }));
expect(ctx.get(InstrumentationDecisionKey)).toEqual({
action: "record",
recordInputs: false,
recordOutputs: true,
});
expect(constructed.harness).not.toHaveProperty("resolveDecision");
});

it("constructs dropped deliveries without provider hooks", () => {
const constructed = prepareDeliveryInstrumentation({
ctx: new ContextContainer(),
delivery: { kind: "deliver" },
instrumentation: runtime(vi.fn(() => ({ action: "drop" as const }))),
rootSessionId: "session-1",
sessionId: "session-1",
});

expect(constructed.harness?.hooks).toBeUndefined();
expect(constructed.harness?.telemetryIntegrations).toEqual([]);
});
});
47 changes: 47 additions & 0 deletions packages/eve/src/execution/delivery-instrumentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { ContextContainer } from "#context/container.js";
import { ChannelInstrumentationKey, InstrumentationDecisionKey } from "#context/keys.js";
import {
getInstrumentationRuntime,
type ConstructedInstrumentation,
type InstrumentationRuntime,
} from "#harness/instrumentation/runtime.js";
import { normalizeChannelAudience } from "#shared/channel-audience.js";

const UNINSTRUMENTED: ConstructedInstrumentation = { run: (execute) => execute() };

/** Resolves audience once, then constructs the only instrumentation visible to the harness. */
export function prepareDeliveryInstrumentation(input: {
readonly agentName?: string;
readonly ctx: ContextContainer;
readonly delivery?: { readonly kind: string };
readonly instrumentation?: InstrumentationRuntime;
readonly rootSessionId: string;
readonly sessionId: string;
}): ConstructedInstrumentation {
const instrumentation = input.instrumentation;
if (instrumentation === undefined) return UNINSTRUMENTED;

let decision = input.ctx.get(InstrumentationDecisionKey);
if (decision === undefined || input.delivery?.kind === "deliver") {
decision = instrumentation.resolveDecision({
agentName: input.agentName,
audience: normalizeChannelAudience(
input.ctx.get(ChannelInstrumentationKey)?.metadata.audience,
),
rootSessionId: input.rootSessionId,
sessionId: input.sessionId,
});
input.ctx.set(InstrumentationDecisionKey, decision);
}
return instrumentation.construct(decision);
}

export function reconstructInstrumentation(
serializedContext: Record<string, unknown>,
): ConstructedInstrumentation {
const instrumentation = getInstrumentationRuntime();
const decision = serializedContext[InstrumentationDecisionKey.name];
return instrumentation !== undefined && typeof decision === "object" && decision !== null
? instrumentation.construct(decision as Parameters<InstrumentationRuntime["construct"]>[0])
: UNINSTRUMENTED;
}
7 changes: 4 additions & 3 deletions packages/eve/src/execution/node-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import type { HarnessToolDefinition } from "#harness/execute-tool.js";
import { LOAD_SKILL_TOOL_NAME } from "#runtime/skills/fragment-context.js";
import { createToolLoopHarness } from "#harness/tool-loop.js";
import type { HandleEventFn, HarnessToolMap, StepFn } from "#harness/types.js";
import type { HarnessInstrumentation } from "#harness/instrumentation/runtime.js";
import { resolveInstalledPackageInfo } from "#internal/application/package.js";
import { getInstrumentationRuntime } from "#harness/instrumentation/runtime.js";
import { createLogger } from "#internal/logging.js";
import type { RuntimeIdentity } from "#protocol/message.js";
import { UNSPECIFIED_INPUT_SCHEMA } from "#shared/tool-schema.js";
Expand Down Expand Up @@ -81,6 +81,7 @@ export interface CreateExecutionNodeStepInput {
readonly handleEvent?: HandleEventFn;
readonly historyProjector?: HistoryViewProjector;
readonly historyView?: PreparedHistoryView;
readonly instrumentation?: HarnessInstrumentation;
readonly mode: RunMode;
readonly modelResolutionScope: RuntimeModelResolutionScope;
readonly node: ResolvedRuntimeAgentNode;
Expand All @@ -105,7 +106,7 @@ export function createExecutionNodeStep(input: CreateExecutionNodeStepInput): St
input.node.turnAgent.dynamicModel,
);
const tools = createNodeHarnessTools({ node: input.node });
const instrumentation = getInstrumentationRuntime();
const instrumentation = input.instrumentation;
const step = createToolLoopHarness({
abortSignal: input.abortSignal,
capabilities: input.capabilities,
Expand Down Expand Up @@ -133,7 +134,7 @@ export function createExecutionNodeStep(input: CreateExecutionNodeStepInput): St
try {
return await step(session, stepInput);
} finally {
await instrumentation.forceFlush();
await instrumentation.forceFlush?.();
}
};
}
Expand Down
8 changes: 4 additions & 4 deletions packages/eve/src/execution/settle-cancelled-turn-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { withContextScope } from "#context/run-step.js";
import { deserializeContext, serializeContext } from "#context/serialize.js";
import { ChannelInstrumentationKey } from "#context/keys.js";
import { setChannelContext } from "#execution/channel-context.js";
import { reconstructInstrumentation } from "#execution/delivery-instrumentation.js";
import {
createDurableSessionState,
type DurableSessionState,
Expand All @@ -28,7 +29,6 @@ import {
import { abandonRunningAgentTurns } from "#harness/handles/transitions.js";
import { clearPendingRuntimeActionBatch } from "#harness/runtime-actions.js";
import { createInstrumentationHandleEvent } from "#harness/instrumentation/native-events.js";
import { getInstrumentationRuntime } from "#harness/instrumentation/runtime.js";
import { getTurnUsageState, toUsage } from "#harness/turn-tag-state.js";
import { clearPendingWorkflowInterrupt } from "#harness/workflow-interrupt-state.js";
import {
Expand Down Expand Up @@ -65,7 +65,7 @@ export async function settleCancelledTurnStep(input: {
const adapterCtx = buildAdapterContext(adapter, ctx);
const bundle = ctx.require(BundleKey);
const effectiveAgent = resolveEffectiveAgentRuntime(bundle, ctx);
const instrumentation = getInstrumentationRuntime();
const instrumentation = reconstructInstrumentation(input.serializedContext);

let session = hydrateDurableSession({
compactionOverrides: {
Expand Down Expand Up @@ -109,7 +109,7 @@ export async function settleCancelledTurnStep(input: {
agentName: bundle.turnAgent.id,
channelKind: ctx.get(ChannelInstrumentationKey)?.kind,
handleEvent: baseEmit,
hooks: instrumentation?.hooks,
hooks: instrumentation.harness?.hooks,
sessionId: session.sessionId,
turnId: activeTurnId(emissionState),
}) ?? baseEmit;
Expand All @@ -121,7 +121,7 @@ export async function settleCancelledTurnStep(input: {
emissionState = scoped.result;
session = scoped.session;
} finally {
await instrumentation?.forceFlush();
await instrumentation.harness?.forceFlush?.();
writer.releaseLock();
}
}
Expand Down
23 changes: 17 additions & 6 deletions packages/eve/src/execution/workflow-steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import { recordSubagentUsageSpans } from "#execution/subagent-usage-span.js";
import { reconcileSessionContinuationToken } from "#execution/reconcile-session-continuation-token.js";
import { hydrateDurableSession, refreshSessionFromTurnAgent } from "#execution/session.js";
import { createExecutionHistoryView } from "#execution/history-view.js";
import { prepareDeliveryInstrumentation } from "#execution/delivery-instrumentation.js";
import { resolveRuntimeCompiledArtifactsVersionedCacheKey } from "#runtime/cache-key.js";
import { createWorkflowRuntime } from "#execution/workflow-runtime.js";
import { isTaskToolAvailable, TASK_UPDATE_TOOL_NAME } from "#runtime/framework-tools/tasks.js";
Expand Down Expand Up @@ -191,14 +192,22 @@ export async function turnStep(rawInput: TurnStepInput): Promise<DurableStepResu
const history = createExecutionHistoryView(initialSession);
const instrumentation = getInstrumentationRuntime();
const initialEmissionState = getHarnessEmissionState(initialSession.state);
const constructedInstrumentation = prepareDeliveryInstrumentation({
agentName: bundle.turnAgent.id,
ctx,
delivery: rawInput.input,
instrumentation,
rootSessionId: initialSession.rootSessionId ?? initialSession.sessionId,
sessionId: initialSession.sessionId,
});

if (rawInput.input?.kind === "deliver") {
await contextStorage.run(ctx, () =>
instrumentChannelDelivery({
agentName: bundle.turnAgent.id,
ctx,
delivery: rawInput.input as DeliverHookPayload,
hooks: instrumentation?.hooks,
hooks: constructedInstrumentation.harness?.hooks,
rootSessionId: initialSession.rootSessionId ?? initialSession.sessionId,
sequence: initialEmissionState.sequence,
sessionId: initialSession.sessionId,
Expand All @@ -213,12 +222,12 @@ export async function turnStep(rawInput: TurnStepInput): Promise<DurableStepResu
ctx,
error,
errorCode: channelDeliveryErrorCode(error),
hooks: instrumentation?.hooks,
hooks: constructedInstrumentation.harness?.hooks,
includeTurn: false,
outcome: "failed",
}),
);
await instrumentation?.forceFlush();
await constructedInstrumentation.harness?.forceFlush?.();
};
const adapterCtx = buildAdapterContext(adapter, ctx);

Expand Down Expand Up @@ -279,12 +288,12 @@ export async function turnStep(rawInput: TurnStepInput): Promise<DurableStepResu
await contextStorage.run(ctx, () =>
instrumentChannelDelivery({
ctx,
hooks: instrumentation?.hooks,
hooks: constructedInstrumentation.harness?.hooks,
includeTurn: false,
outcome: "completed",
}),
);
await instrumentation?.forceFlush();
await constructedInstrumentation.harness?.forceFlush?.();
const rekeyed = reconcileSessionContinuationToken(ctx, initialSession);
const nextSerializedContext = serializeContext(ctx);
const nextState =
Expand Down Expand Up @@ -432,6 +441,7 @@ export async function turnStep(rawInput: TurnStepInput): Promise<DurableStepResu
const traceContext = await prepareWorkflowPreambleTrace({
ctx,
emissionState,
instrumentation: constructedInstrumentation.harness,
runtimeIdentity,
session: schemaSession,
});
Expand Down Expand Up @@ -498,6 +508,7 @@ export async function turnStep(rawInput: TurnStepInput): Promise<DurableStepResu
handleEvent,
historyProjector: history.projector,
historyView: history.prepare(modelSession),
instrumentation: constructedInstrumentation.harness,
mode,
modelResolutionScope: {
moduleMap: bundle.moduleMap,
Expand All @@ -506,7 +517,7 @@ export async function turnStep(rawInput: TurnStepInput): Promise<DurableStepResu
node: effectiveNode,
workflowMaxSubagents: refreshedSession.workflowMaxSubagents,
});
return step(modelSession, stepInput);
return constructedInstrumentation.run(() => step(modelSession, stepInput));
};

return runHarnessStep(schemaSession, resolved);
Expand Down
14 changes: 4 additions & 10 deletions packages/eve/src/execution/workflow-trace-context.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
import type { ContextContainer } from "#context/container.js";
import {
ChannelInstrumentationKey,
ParentSessionKey,
ParentTraceContextKey,
} from "#context/keys.js";
import { ParentSessionKey, ParentTraceContextKey } from "#context/keys.js";
import type { HarnessEmissionState } from "#harness/emission.js";
import { getInstrumentationRuntime } from "#harness/instrumentation/runtime.js";
import type { HarnessInstrumentation } from "#harness/instrumentation/runtime.js";
import { resolveParentLineage } from "#harness/parent-lineage.js";
import { prepareTurnTraceContext } from "#harness/prepare-trace-context.js";
import type { HarnessSession } from "#harness/types.js";
import type { RuntimeIdentity, RuntimeTraceContext } from "#protocol/message.js";
import { ChannelKey } from "#runtime/sessions/runtime-context-keys.js";
import { normalizeChannelAudience } from "#shared/channel-audience.js";

/** Prepares native tracing for workflow-owned preambles emitted outside the tool loop. */
export async function prepareWorkflowPreambleTrace(input: {
readonly ctx: ContextContainer;
readonly emissionState: HarnessEmissionState;
readonly instrumentation?: HarnessInstrumentation;
readonly runtimeIdentity: RuntimeIdentity;
readonly session: HarnessSession;
}): Promise<RuntimeTraceContext | undefined> {
const parent = input.ctx.get(ParentSessionKey);
const channel = input.ctx.get(ChannelInstrumentationKey);
return await prepareTurnTraceContext({
agentName: input.runtimeIdentity.agentName,
channelAudience: normalizeChannelAudience(channel?.metadata.audience),
instrumentation: getInstrumentationRuntime(),
instrumentation: input.instrumentation,
parentLineage: resolveParentLineage(parent, input.ctx.get(ChannelKey)),
parentTraceContext: input.ctx.get(ParentTraceContextKey),
rootSessionId: parent?.rootSessionId ?? input.session.rootSessionId ?? input.session.sessionId,
Expand Down
6 changes: 0 additions & 6 deletions packages/eve/src/harness/channel-delivery-instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { DeliverHookPayload, DeliverPayload } from "#channel/types.js";
import type { AlsContext } from "#context/container.js";
import {
ActiveChannelDeliveriesKey,
ChannelInstrumentationKey,
ParentTraceContextKey,
type ActiveChannelDelivery,
} from "#context/keys.js";
Expand All @@ -12,7 +11,6 @@ import type {
InstrumentationHooks,
} from "#harness/instrumentation/lifecycle.js";
import { channelDeliveryIdempotencyKey } from "#harness/instrumentation/lifecycle.js";
import { normalizeChannelAudience } from "#shared/channel-audience.js";

interface ChannelDeliveryStartInstrumentation {
readonly agentName?: string;
Expand Down Expand Up @@ -70,13 +68,9 @@ export async function instrumentChannelDelivery(
if (input.hooks === undefined || input.delivery.deliveryMetadata === undefined) return;

const active: ActiveChannelDelivery[] = [];
const channelAudience = normalizeChannelAudience(
input.ctx.get(ChannelInstrumentationKey)?.metadata.audience,
);
for (const metadata of input.delivery.deliveryMetadata) {
const payload = input.delivery.payloads[metadata.payloadIndex];
const delivery = {
channelAudience,
channelKind: metadata.channelKind,
channelName: metadata.channelName,
deliveryId: metadata.deliveryId,
Expand Down
Loading
Loading