Skip to content
Closed
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/strict-audience-controls.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-scoped decision used to construct the 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.
4 changes: 4 additions & 0 deletions docs/guides/instrumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ 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 delivery-scoped decision before the harness runs. eve constructs the harness instrumentation from that decision, including its provider set, content-filtered hooks, telemetry, and tracing context. Return `{ action: "drop" }` to omit the trace, or return `{ action: "record", recordInputs, recordOutputs }` to create it with an explicit content ceiling. By default, eve records public deliveries with inputs and outputs and drops private and unknown deliveries. The harness and span export policies receive neither the audience value nor the decision.

This decision governs durable agent and AI telemetry. The optional inbound server span created by `traceChannelRequests: true` is request-scoped, contains no body or session content, and begins before a channel can classify the audience.

## Channel delivery traces

Instrumentation providers receive `channel.delivery.started` followed by
Expand Down
17 changes: 17 additions & 0 deletions packages/eve/extension-contracts/reports/channel/v8.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"kind": "eve-extension-capability-contract",
"capability": "channel",
"epoch": 8,
"sha256": "2a84b0ba011d17f37b9fd48f3dde84ee2a04aa523af53713b41f782847f8e8f9",
"exports": [
"DELETE",
"GET",
"PATCH",
"POST",
"PUT",
"WS",
"createWebSocketUpgradeServer",
"defineChannel",
"isChannel"
]
}
5 changes: 5 additions & 0 deletions packages/eve/src/channel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { ChannelAdapter } from "#channel/adapter.js";
import type { AgentLimitsDefinition } from "#shared/agent-definition.js";
import type { JsonObject } from "#shared/json.js";
import type { TaskView } from "#tasks/types.js";
import type { InstrumentationControls } from "#shared/instrumentation-controls.js";

export type { ContextAccessor } from "#context/key.js";
export type { ChannelInstrumentationProjection } from "#channel/instrumentation.js";
Expand Down Expand Up @@ -162,6 +163,8 @@ export interface DeliverPayload {
readonly message?: string | UserContent;
readonly context?: readonly string[];
readonly outputSchema?: JsonObject;
/** Framework-only instrumentation ceiling ferried to local child sessions. */
readonly instrumentationControls?: InstrumentationControls;
/** Framework-only task envelopes consumed before adapter/model delivery. */
readonly task?: {
/** Task HITL input-request batches for the parent's pre-model router. */
Expand Down Expand Up @@ -470,6 +473,8 @@ export interface RunInput {
* (root session behavior).
*/
readonly initiatorAuth?: SessionAuthContext | null;
/** Framework-owned instrumentation ceiling inherited by local subagents. */
readonly instrumentationControls?: InstrumentationControls;
readonly input: {
readonly message: string | UserContent;
readonly context?: readonly string[];
Expand Down
8 changes: 7 additions & 1 deletion packages/eve/src/compiler/extension-compatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ const EXTENSION_CAPABILITY_CONTRACTS = {
supported: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18],
dropped: {},
},
channel: { current: 7, supported: [1, 2, 3, 4, 5, 6, 7], dropped: {} },
channel: {
current: 8,
supported: [1, 2, 3, 4, 5, 6, 8],
dropped: {
7: "Instrumentation callbacks no longer expose channel audience; eve maps it to internal controls at delivery.",
},
},
schedule: { current: 3, supported: [1, 2, 3], dropped: {} },
subagent: { current: 2, supported: [1, 2], dropped: {} },
connection: { current: 5, supported: [1, 2, 3, 4, 5], dropped: {} },
Expand Down
2 changes: 1 addition & 1 deletion packages/eve/src/context/dynamic-resolve-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe("buildResolveContext", () => {
ctx.set(ChannelKey, { kind: "http" });
ctx.set(ChannelInstrumentationKey, {
kind: "channel:slack",
metadata: { threadTs: "1234.5678", userId: "U123" },
metadata: { audience: "private", threadTs: "1234.5678", userId: "U123" },
});

const resolveCtx = buildResolveContext(ctx, []);
Expand Down
6 changes: 5 additions & 1 deletion packages/eve/src/context/dynamic-resolve-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "#context/keys.js";
import { ChannelKey } from "#runtime/sessions/runtime-context-keys.js";
import { getAdapterKind } from "#channel/adapter.js";
import { withoutChannelAudience } from "#shared/channel-audience.js";

type ReadableContext = Pick<AlsContext, "get">;

Expand Down Expand Up @@ -42,7 +43,10 @@ export function buildResolveContext(
channel: {
kind: channelAdapter !== undefined ? getAdapterKind(channelAdapter) : undefined,
continuationToken,
metadata: channelInstrumentation?.metadata,
metadata:
channelInstrumentation === undefined
? undefined
: withoutChannelAudience(channelInstrumentation.metadata),
},
messages,
};
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 { InstrumentationControls } from "#shared/instrumentation-controls.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 InstrumentationControlsKey = new ContextKey<InstrumentationControls>(
"eve.instrumentationControls",
);
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
8 changes: 7 additions & 1 deletion packages/eve/src/execution/agent-handle-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* dead (handle deleted) or retryable (handle restored to `parked`).
*/

import type { SessionAuthContext } from "#channel/types.js";
import type { DeliverPayload, SessionAuthContext } from "#channel/types.js";
import { AGENT_BUSY, AGENT_MISMATCH, AGENT_UNREACHABLE } from "#harness/agent-handle-errors.js";
import { deriveAgentOperationId } from "#harness/handles/operation-id.js";
import {
Expand Down Expand Up @@ -106,6 +106,7 @@ export async function dispatchToAgentHandle(input: {
readonly auth: SessionAuthContext | null;
readonly bundle: CompiledBundle;
readonly currentSession: RuntimeSession;
readonly instrumentationControls?: DeliverPayload["instrumentationControls"];
readonly parentToken: string;
readonly parentTurnId: string;
}): Promise<DispatchOutcome> {
Expand Down Expand Up @@ -185,6 +186,7 @@ export async function dispatchToAgentHandle(input: {
auth: input.auth,
bundle,
identity: handle.identity,
instrumentationControls: input.instrumentationControls,
parentToken: input.parentToken,
});
if (!delivery.ok) {
Expand Down Expand Up @@ -233,6 +235,7 @@ export async function dispatchToTaskAgentAddress(input: {
readonly auth: SessionAuthContext | null;
readonly bundle: CompiledBundle;
readonly currentSession: RuntimeSession;
readonly instrumentationControls?: DeliverPayload["instrumentationControls"];
readonly parentToken: string;
}): Promise<DispatchOutcome> {
const { action, agentId } = input;
Expand Down Expand Up @@ -271,6 +274,7 @@ export async function dispatchToTaskAgentAddress(input: {
auth: input.auth,
bundle: input.bundle,
identity: record.identity,
instrumentationControls: input.instrumentationControls,
parentToken: input.parentToken,
});
if (!delivery.ok) {
Expand Down Expand Up @@ -324,6 +328,7 @@ async function deliverToAgentAddress(input: {
readonly auth: SessionAuthContext | null;
readonly bundle: CompiledBundle;
readonly identity: AgentIdentity;
readonly instrumentationControls?: DeliverPayload["instrumentationControls"];
readonly parentToken: string;
}): Promise<
Result<
Expand Down Expand Up @@ -390,6 +395,7 @@ async function deliverToAgentAddress(input: {
},
kind: "send",
payload: {
instrumentationControls: input.instrumentationControls,
message: readSubagentMessage(action),
outputSchema: normalizeRequestedOutputSchema(action.input.outputSchema),
},
Expand Down
10 changes: 10 additions & 0 deletions packages/eve/src/execution/cancel-descendant-turns-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { createLogger, logError } from "#internal/logging.js";
import type { RuntimeSubagentRegistry } from "#runtime/subagents/registry.js";
import { getDynamicSubagentSelection } from "#context/dynamic-subagent-lifecycle.js";
import type { ContextContainer } from "#context/container.js";
import { constructSerializedInstrumentation } from "#execution/instrumentation-controls.js";

// Retry through transient world contention (queue wakes, hook-claim
// conflicts), then log loudly: a silently dropped cancel leaves the child
Expand All @@ -31,6 +32,15 @@ export async function cancelDescendantTurnsStep(input: {
}): Promise<void> {
"use step";

return await constructSerializedInstrumentation(input.serializedContext).run(() =>
cancelDescendantTurns(input),
);
}

async function cancelDescendantTurns(input: {
readonly serializedContext: Record<string, unknown>;
readonly sessionState: DurableSessionState;
}): Promise<void> {
let running: readonly RunningAgentHandle[];
try {
const session = await readDurableSession(input.sessionState);
Expand Down
21 changes: 13 additions & 8 deletions packages/eve/src/execution/channel-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ export function setChannelContext(
readonly channelName?: string;
} = {},
): void {
const existing = ctx.get(ChannelInstrumentationKey);
const projection = buildChannelInstrumentationProjection({
adapter,
channelName: options.channelName,
existingKind: existing?.kind,
});
ctx.set(ChannelKey, adapter);
ctx.set(
ChannelInstrumentationKey,
buildChannelInstrumentationProjection({
adapter,
channelName: options.channelName,
existingKind: ctx.get(ChannelInstrumentationKey)?.kind,
}),
);
ctx.set(ChannelInstrumentationKey, {
...projection,
metadata:
projection.kind === "subagent" && existing !== undefined
? existing.metadata
: projection.metadata,
});
}
24 changes: 24 additions & 0 deletions packages/eve/src/execution/delegated-parent-notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { AgentTurnOutcome } from "#shared/agent-turn-outcome.js";
import { toErrorMessage } from "#shared/errors.js";
import { parseJsonValue } from "#shared/json.js";
import type { TokenUsage } from "#shared/token-usage.js";
import { constructSerializedInstrumentation } from "#execution/instrumentation-controls.js";
import { resumeHook } from "#internal/workflow/runtime.js";
import { postSessionCallbackRequest } from "#execution/session-callback-request.js";
import type { TaskInboundTurnStarted } from "#tasks/types.js";
Expand All @@ -43,6 +44,16 @@ export async function notifyDelegatedParentStep(input: {
}): Promise<void> {
"use step";

return await constructSerializedInstrumentation(input.serializedContext).run(() =>
notifyDelegatedParent(input),
);
}

async function notifyDelegatedParent(input: {
readonly result: RuntimeSubagentChildResult | undefined;
readonly serializedContext: Record<string, unknown>;
readonly usage?: TokenUsage;
}): Promise<void> {
if (input.result === undefined) {
return;
}
Expand Down Expand Up @@ -104,11 +115,24 @@ const ZERO_TOKEN_USAGE: TokenUsage = {
export async function notifyTurnCallerStep(input: {
readonly caller: TurnCaller | undefined;
readonly lifecycle: AgentTurnOutcome["kind"];
readonly serializedContext?: Record<string, unknown>;
readonly sessionId: string;
readonly settled: SettledTurnNotification;
}): Promise<void> {
"use step";

return await constructSerializedInstrumentation(input.serializedContext ?? {}).run(() =>
notifyTurnCaller(input),
);
}

async function notifyTurnCaller(input: {
readonly caller: TurnCaller | undefined;
readonly lifecycle: AgentTurnOutcome["kind"];
readonly serializedContext?: Record<string, unknown>;
readonly sessionId: string;
readonly settled: SettledTurnNotification;
}): Promise<void> {
if (input.caller === undefined) {
return;
}
Expand Down
9 changes: 9 additions & 0 deletions packages/eve/src/execution/dispatch-runtime-actions-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
CapabilitiesKey,
ChannelInstrumentationKey,
InitiatorAuthKey,
InstrumentationControlsKey,
SandboxKey,
} from "#context/keys.js";
import { type AlsContext, ContextContainer } from "#context/container.js";
Expand Down Expand Up @@ -154,6 +155,9 @@ export interface PreparedRuntimeActionDispatch {
*/
readonly fanoutSize: number;
readonly initiatorAuth: Parameters<typeof buildSubagentRunInput>[0]["initiatorAuth"];
readonly instrumentationControls: Parameters<
typeof buildSubagentRunInput
>[0]["instrumentationControls"];
readonly parentTraceContext: Parameters<typeof buildSubagentRunInput>[0]["parentTraceContext"];
readonly sandboxSessionId: string;
readonly serializedContext: Record<string, unknown>;
Expand Down Expand Up @@ -280,6 +284,7 @@ async function prepareActionDispatch(input: {
input.fanoutSize ??
plan.filter((entry) => entry.kind === "start" && entry.target.kind === "local").length,
initiatorAuth: ctx.get(InitiatorAuthKey) ?? null,
instrumentationControls: ctx.get(InstrumentationControlsKey),
parentTraceContext: readSessionTraceContext(input.serializedContext, session.sessionId),
plan,
sandboxSessionId,
Expand Down Expand Up @@ -542,6 +547,9 @@ export async function startSubagent(input: {
readonly currentSession: RuntimeSession;
readonly fanoutSize: number;
readonly initiatorAuth: Parameters<typeof buildSubagentRunInput>[0]["initiatorAuth"];
readonly instrumentationControls: Parameters<
typeof buildSubagentRunInput
>[0]["instrumentationControls"];
readonly parentContinuationToken: string | undefined;
readonly parentTraceContext: Parameters<typeof buildSubagentRunInput>[0]["parentTraceContext"];
readonly persistentSessions: boolean;
Expand Down Expand Up @@ -572,6 +580,7 @@ export async function startSubagent(input: {
dynamicSubagentAgentConfig: input.target.dynamicSubagentAgentConfig,
fanoutSize: input.fanoutSize,
initiatorAuth: input.initiatorAuth,
instrumentationControls: input.instrumentationControls,
parentContinuationToken: input.parentContinuationToken,
parentTraceContext,
persistentSessions: input.persistentSessions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
CapabilitiesKey,
ChannelInstrumentationKey,
InitiatorAuthKey,
InstrumentationControlsKey,
SessionIdKey,
SessionKey,
} from "#context/keys.js";
Expand All @@ -41,6 +42,7 @@ import type {
import type { RuntimeSandboxRegistry } from "#runtime/sandbox/registry.js";
import type { ResolvedSandboxDefinition } from "#runtime/types.js";
import { mockSandbox } from "#internal/testing/mocks/mock-sandbox.js";
import type { InstrumentationControls } from "#shared/instrumentation-controls.js";

const mocks = vi.hoisted(() => ({
continueRemoteAgentSession: vi.fn(),
Expand Down Expand Up @@ -850,7 +852,11 @@ describe("dispatchRuntimeActionsStep agent delivery", () => {
handle: LOCAL_PARKED_HANDLE,
agentId: LOCAL_PARKED_HANDLE.identity.id,
});
installContext(session);
installContext(session, undefined, false, null, {
action: "drop",
recordInputs: false,
recordOutputs: false,
});
const writes: Uint8Array[] = [];

const result = await dispatchRuntimeActionsStep({
Expand All @@ -871,6 +877,11 @@ describe("dispatchRuntimeActionsStep agent delivery", () => {
kind: "send",
auth: null,
payload: {
instrumentationControls: {
action: "drop",
recordInputs: false,
recordOutputs: false,
},
message: "continue with raw input",
outputSchema: undefined,
},
Expand Down Expand Up @@ -1271,6 +1282,7 @@ function installContext(
remote?: { readonly definition: unknown; readonly nodeId: string },
tasks = false,
auth: SessionAuthContext | null = null,
instrumentationControls?: InstrumentationControls,
): void {
const subagentsByNodeId = new Map<string, { definition: unknown }>();
if (remote !== undefined) {
Expand All @@ -1295,6 +1307,7 @@ function installContext(
[CapabilitiesKey, undefined],
[ChannelInstrumentationKey, undefined],
[InitiatorAuthKey, null],
[InstrumentationControlsKey, instrumentationControls],
[ChannelKey, ADAPTER],
]);
mocks.deserializeContext.mockResolvedValue({
Expand Down
Loading
Loading