eve 0.81.0 → 0.81.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/src/compiled/.vendor-stamp.json +2 -2
- package/dist/src/compiled/@eve/catalog/index.js +1 -1
- package/dist/src/eve-channel/create-request.d.ts +2 -1
- package/dist/src/eve-channel/index.js +1 -1
- package/dist/src/eve-channel/request.d.ts +2 -1
- package/dist/src/eve-channel/request.js +1 -2
- package/dist/src/eve-channel/support.d.ts +9 -1
- package/dist/src/eve-channel/support.js +2 -1
- package/dist/src/eve-channel/types.d.ts +18 -0
- package/dist/src/execution/dynamic-workflow/tool.js +1 -1
- package/dist/src/execution/hook-ownership.d.ts +8 -0
- package/dist/src/execution/hook-ownership.js +1 -1
- package/dist/src/execution/session/entry-input.d.ts +19 -5
- package/dist/src/execution/session/entry-input.js +1 -1
- package/dist/src/execution/session/entry.js +1 -1
- package/dist/src/execution/session/handoff-steps.d.ts +75 -13
- package/dist/src/execution/session/handoff-steps.js +1 -1
- package/dist/src/execution/session/handoff.d.ts +69 -27
- package/dist/src/execution/session/handoff.js +1 -1
- package/dist/src/execution/session/program.js +1 -1
- package/dist/src/execution/session/run-watch.d.ts +22 -0
- package/dist/src/execution/session/run-watch.js +1 -0
- package/dist/src/execution/session-inbox/address.d.ts +4 -1
- package/dist/src/execution/session-inbox/end-stranded-session.js +1 -1
- package/dist/src/execution/session-inbox/hook-release.d.ts +9 -2
- package/dist/src/execution/session-inbox/hook-release.js +1 -1
- package/dist/src/execution/session-inbox/inbox.d.ts +66 -6
- package/dist/src/execution/session-inbox/inbox.js +1 -1
- package/dist/src/execution/session-inbox/resume.d.ts +4 -3
- package/dist/src/execution/tools/agent-router-workflow.js +1 -1
- package/dist/src/execution/workflow-runtime.d.ts +6 -11
- package/dist/src/execution/workflow-runtime.js +1 -1
- package/dist/src/harness/model-call/call.js +1 -1
- package/dist/src/instrumentation/content.d.ts +1 -1
- package/dist/src/instrumentation/content.js +1 -1
- package/dist/src/instrumentation/lifecycle.d.ts +4 -2
- package/dist/src/internal/application/stamped-package-version.js +1 -1
- package/dist/src/internal/authored-definition/connection.js +1 -1
- package/dist/src/internal/mcp/skills.js +1 -1
- package/dist/src/internal/workflow/development-world-client.js +1 -1
- package/dist/src/public/channels/chat-sdk/messageToUserContent.d.ts +2 -1
- package/dist/src/public/channels/chat-sdk/messageToUserContent.js +1 -1
- package/dist/src/public/channels/photon/attachments.d.ts +5 -2
- package/dist/src/public/channels/photon/attachments.js +1 -1
- package/dist/src/public/channels/photon/photonIMessageChannel.js +1 -1
- package/dist/src/public/channels/slack/api.d.ts +8 -1
- package/dist/src/public/channels/slack/api.js +1 -1
- package/dist/src/public/channels/slack/slackChannel.js +1 -1
- package/dist/src/setup/scaffold/create/project.js +1 -1
- package/dist/src/shared/gateway-routing.d.ts +20 -0
- package/dist/src/shared/gateway-routing.js +1 -0
- package/dist/src/shared/skill-package.js +1 -1
- package/dist/src/tools/provided/ask-question-workflow.js +1 -1
- package/dist/src/tools/provided/sleep-workflow.js +1 -1
- package/dist/src/tracing/eve/agent-otel-provider.js +1 -1
- package/dist/src/tracing/eve/gateway.d.ts +5 -0
- package/dist/src/tracing/eve/gateway.js +1 -1
- package/dist/src/tracing/eve/operation-input.d.ts +0 -7
- package/dist/src/tracing/eve/operation-input.js +1 -1
- package/dist/src/tracing/eve/policy.d.ts +1 -1
- package/dist/src/tracing/eve/policy.js +1 -1
- package/dist/src/tracing/eve/session.d.ts +2 -0
- package/dist/src/tracing/eve/session.js +1 -1
- package/docs/README.md +1 -0
- package/docs/agent-config.md +22 -0
- package/docs/channels/chat-sdk.mdx +4 -2
- package/docs/channels/eve.mdx +27 -0
- package/docs/channels/photon.mdx +4 -2
- package/docs/channels/slack.mdx +2 -0
- package/docs/concepts/execution-model-and-durability.mdx +16 -7
- package/docs/guides/instrumentation/otel.mdx +20 -0
- package/docs/patterns/meta.json +1 -0
- package/docs/patterns/voice-notes.md +67 -0
- package/docs/sandbox/index.mdx +2 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isObject}from"#shared/guards.js";import{isUserMessageKind}from"#harness/messages.js";import{resolveInstrumentationEnvironment}from"#internal/application/dev-environment.js";import{isSampledTrace}from"#shared/trace-policy.js";import{ROOT_CONTEXT,context,trace}from"#compiled/@opentelemetry/api/index.js";import{gatewayCostAttributes,readGatewayCostData}from"#tracing/eve/gateway.js";import{createAgentToolInstrumentation}from"#tracing/eve/agent-tool-instrumentation.js";import{withChannelAudience}from"#tracing/eve/channel-audience-context.js";import{createEveTracing}from"#tracing/eve/operation-input.js";import{createAgentApprovalInstrumentation}from"#tracing/eve/agent-approval-instrumentation.js";import{createAgentChannelDeliveryInstrumentation}from"#tracing/eve/agent-channel-delivery-instrumentation.js";import{eveActivationMetadata}from"#tracing/eve/metadata.js";import{createAgentMemoryInstrumentation}from"#tracing/eve/agent-memory-instrumentation.js";import{createEveSessionTracing}from"#tracing/eve/session.js";import{createEveCapturePolicy}from"#tracing/eve/policy.js";function createAgentOtelInstrumentation(e){let t=e.environment??resolveInstrumentationEnvironment(),n=e.recordInputs??!1,r=e.recordOutputs??!1,i=createEveTracing(e),a=new Map,
|
|
1
|
+
import{isObject}from"#shared/guards.js";import{isUserMessageKind}from"#harness/messages.js";import{resolveInstrumentationEnvironment}from"#internal/application/dev-environment.js";import{isSampledTrace}from"#shared/trace-policy.js";import{ROOT_CONTEXT,context,trace}from"#compiled/@opentelemetry/api/index.js";import{gatewayCostAttributes,gatewayRoutingAttributes,readGatewayCostData}from"#tracing/eve/gateway.js";import{createAgentToolInstrumentation}from"#tracing/eve/agent-tool-instrumentation.js";import{withChannelAudience}from"#tracing/eve/channel-audience-context.js";import{createEveTracing}from"#tracing/eve/operation-input.js";import{createAgentApprovalInstrumentation}from"#tracing/eve/agent-approval-instrumentation.js";import{createAgentChannelDeliveryInstrumentation}from"#tracing/eve/agent-channel-delivery-instrumentation.js";import{eveActivationMetadata}from"#tracing/eve/metadata.js";import{createAgentMemoryInstrumentation}from"#tracing/eve/agent-memory-instrumentation.js";import{createEveSessionTracing}from"#tracing/eve/session.js";import{createEveCapturePolicy}from"#tracing/eve/policy.js";function createAgentOtelInstrumentation(e){let t=e.environment??resolveInstrumentationEnvironment(),n=e.recordInputs??!1,r=e.recordOutputs??!1,i=createEveTracing(e),a=createEveCapturePolicy({stateStore:e.stateStore,environment:t,recordInputs:n,recordOutputs:r}),o=new Map,s=new Map,c=createEveSessionTracing({...e,environment:t,tracing:i,capturePolicy:a}),{prepareSessionTrace:l,prepareTurnTrace:u}=c;async function attemptFor(e){let t=o.get(e.attemptId);if(t!==void 0)return t;let n=await c.turnFor(e.sessionId,e.turnId,withChannelAudience(ROOT_CONTEXT,e.channelAudience)),r={stepIndex:e.stepIndex,attempt:e.attemptIndex};return n?.findAttempt(r)??(n?.finished===!1?n.attempt(r):void 0)}let d=createAgentToolInstrumentation({tracing:i,attemptFor,stepFor:e=>o.get(e.attemptId),recordInputs:n,recordOutputs:r,stateStore:e.stateStore}),f=createAgentApprovalInstrumentation({tracing:i,actionStateFor:d.stateFor}),p=createAgentMemoryInstrumentation({...e,environment:t,tracing:i});function turnState(t){return e.stateStore.get(`turn`,JSON.stringify([t.sessionId,t.turnId]))}let onSessionStarted=async e=>{await l(e)},onTurnStarted=async e=>{await u(e)},onStepStarted=async t=>{let n=await turnState(t.scope);if(n===void 0||!isSampledTrace(n.context))return;let r=await e.stateStore.get(`session`,t.scope.sessionId),i=await c.turnFor(t.scope.sessionId,t.scope.turnId,withChannelAudience(ROOT_CONTEXT,t.scope.channelAudience));if(i===void 0||i.finished)return;let a=trace.getSpan(context.active())?.spanContext(),s=await i.attempt({stepIndex:t.scope.stepIndex,attempt:t.scope.attemptIndex,runtimeContext:t.runtimeContext,channel:eveActivationMetadata({session:r,turn:n,sessionId:t.scope.sessionId}).channel,links:a===void 0||a.traceId===n.context.traceId?void 0:[{relationship:`execution.delivery`,context:a}]});o.set(t.scope.attemptId,s)},onStepTerminal=async e=>{let t=e.type===`step.attempt.failed`?{error:e.error}:void 0;await drainOpenSpans(e),await d.drain(e.scope.attemptId,t);let n=o.get(e.scope.attemptId)??await attemptFor(e.scope);o.delete(e.scope.attemptId),t===void 0?await n?.complete():(await n?.fail(t.error),await d.forgetAttempt(e.scope))},onSessionTransition=async t=>{await c.sessionTransition(t),(t.type===`session.completed`||t.type===`session.failed`)&&(await d.deleteForSession(t.sessionId),await e.stateStore.delete(`session`,t.sessionId))},onModelCallStarted=async e=>{let t=o.get(e.scope.attemptId);if(t===void 0)return;let r=await t.modelCall({provider:e.model.provider,modelId:e.model.modelId,messages:n?e.input?.messages.map(e=>isObject(e)&&e.role===`user`?{...e,kind:isUserMessageKind(e.kind)?e.kind:void 0}:e):void 0,instructions:n?e.input?.instructions:void 0,tools:n?e.input?.tools:void 0,runtimeContext:e.runtimeContext},e.idempotencyKey),i=s.get(e.scope.attemptId);i===void 0&&s.set(e.scope.attemptId,i=new Map),i.set(e.idempotencyKey,r)},onModelCallTerminal=async e=>{let t=s.get(e.scope.attemptId),n=t?.get(e.idempotencyKey);t?.delete(e.idempotencyKey),n!==void 0&&(e.type===`model.call.failed`?await n.fail(e.error):await n.complete({outcome:`completed`,result:{...e,content:r?e.content:void 0}}))},m=createAgentChannelDeliveryInstrumentation({recordInputs:n,stateStore:e.stateStore}),onStepMetadata=e=>{let t=o.get(e.scope.attemptId);if(t===void 0)return;let n=readGatewayCostData(e.providerMetadata);n!==void 0&&t.attributes(gatewayCostAttributes(n));let r=gatewayRoutingAttributes(e.providerMetadata);r!==void 0&&t.attributes(r)};return{hook:{events:{...m,...f,"step.attempt.completed":onStepTerminal,"step.attempt.failed":onStepTerminal,"step.attempt.metadata":onStepMetadata,"step.attempt.started":onStepStarted,"model.call.completed":onModelCallTerminal,"model.call.failed":onModelCallTerminal,"model.call.started":onModelCallStarted,"session.completed":onSessionTransition,"session.failed":onSessionTransition,"session.started":onSessionStarted,"session.waiting":onSessionTransition,...d.events,"turn.cancelled":c.turnTerminal,"turn.completed":c.turnTerminal,"turn.failed":c.turnTerminal,"turn.started":onTurnStarted},name:`eve.otel`,projectEvent:a.projectEvent,tracePolicy:()=>({emit:!0,recordInputs:n,recordOutputs:r})},prepareSessionTrace:l,prepareTurnTrace:u,async runInContext(t,n){if(t.type===`memory.operation`)return p.runInContext(t,n);let r={emit:!0,...await a.forOperation(t.scope)},inTurn=async()=>{let i=await c.turnFor(t.scope.sessionId,t.scope.turnId,withChannelAudience(ROOT_CONTEXT,t.scope.channelAudience));if(i!==void 0&&!i.finished)return i.run(n,r);let{sessionId:a,turnId:o}=t.scope;return e.telemetry.suppressed!==void 0&&await c.dropped(a,o)?e.telemetry.suppressed(n):n()};if(t.type===`tool.call`)return d.runInContext(t,n,r,inTurn);let i=s.get(t.scope.attemptId)?.get(t.idempotencyKey);return i!==void 0&&!i.finished?i.run(n,r):inTurn()}};async function drainOpenSpans(e){for(let t of s.get(e.scope.attemptId)?.values()??[])e.type===`step.attempt.failed`?await t.fail(e.error):await t.complete({outcome:`completed`,result:{finishReason:`unknown`,usage:{}}});s.delete(e.scope.attemptId)}}export{createAgentOtelInstrumentation};
|
|
@@ -15,3 +15,8 @@ export declare function readGatewayCostData(providerMetadata: Readonly<Record<st
|
|
|
15
15
|
outputCost: number | undefined;
|
|
16
16
|
generationId: string | undefined;
|
|
17
17
|
} | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Converts Gateway session affinity metadata to span attributes, so a step can
|
|
20
|
+
* be matched to its session in the Gateway logs Sessions view.
|
|
21
|
+
*/
|
|
22
|
+
export declare function gatewayRoutingAttributes(providerMetadata: Readonly<Record<string, unknown>> | undefined): Attributes | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{stripEveTraceBaggage}from"#protocol/baggage.js";import{context,createContextKey,propagation,trace}from"#compiled/@opentelemetry/api/index.js";const SUPPRESS_TRACING_KEY=createContextKey(`OpenTelemetry SDK Context Key SUPPRESS_TRACING`),traceContextSetter={set(e,t,n){e[t]=n}};function createGatewayTraceContextHeaders(){let e=context.active();if(e.getValue(SUPPRESS_TRACING_KEY)===!0||trace.getSpan(e)===void 0)return;let t={};return propagation.inject(e,t,traceContextSetter),stripEveTraceBaggage(t),Object.keys(t).length===0?void 0:t}function gatewayCostAttributes(e){return{"gen_ai.usage.cost":e.cost,"gen_ai.usage.gateway_cost":e.gatewayCost,"gen_ai.usage.input_cost":e.inputCost,"gen_ai.usage.output_cost":e.outputCost,"gen_ai.generation.id":e.generationId}}function readGatewayCostData(e){if(e===void 0)return;let t=e.gateway;if(typeof t!=`object`||!t||Array.isArray(t))return;let n=t;return{cost:readUsd(n.cost),gatewayCost:readUsd(n.gatewayCost),inputCost:readUsd(n.inputInferenceCost),outputCost:readUsd(n.outputInferenceCost),generationId:typeof n.generationId==`string`&&n.generationId.length>0?n.generationId:void 0}}function readUsd(e){if(typeof e!=`string`||e.trim()===``)return;let t=Number(e);return Number.isFinite(t)?t:void 0}export{createGatewayTraceContextHeaders,gatewayCostAttributes,readGatewayCostData};
|
|
1
|
+
import{isObject}from"#shared/guards.js";import{stripEveTraceBaggage}from"#protocol/baggage.js";import{context,createContextKey,propagation,trace}from"#compiled/@opentelemetry/api/index.js";import{readGatewayRouting}from"#shared/gateway-routing.js";const SUPPRESS_TRACING_KEY=createContextKey(`OpenTelemetry SDK Context Key SUPPRESS_TRACING`),traceContextSetter={set(e,t,n){e[t]=n}};function createGatewayTraceContextHeaders(){let e=context.active();if(e.getValue(SUPPRESS_TRACING_KEY)===!0||trace.getSpan(e)===void 0)return;let t={};return propagation.inject(e,t,traceContextSetter),stripEveTraceBaggage(t),Object.keys(t).length===0?void 0:t}function gatewayCostAttributes(e){return{"gen_ai.usage.cost":e.cost,"gen_ai.usage.gateway_cost":e.gatewayCost,"gen_ai.usage.input_cost":e.inputCost,"gen_ai.usage.output_cost":e.outputCost,"gen_ai.generation.id":e.generationId}}function readGatewayCostData(e){if(e===void 0)return;let t=e.gateway;if(typeof t!=`object`||!t||Array.isArray(t))return;let n=t;return{cost:readUsd(n.cost),gatewayCost:readUsd(n.gatewayCost),inputCost:readUsd(n.inputInferenceCost),outputCost:readUsd(n.outputInferenceCost),generationId:typeof n.generationId==`string`&&n.generationId.length>0?n.generationId:void 0}}function gatewayRoutingAttributes(e){let t=e?.gateway,n=readGatewayRouting(isObject(t)?t.routing:void 0);if(n!==void 0)return{"vercel.ai_gateway.client_session_id":n.clientSessionId,"vercel.ai_gateway.client_session_id_source":n.clientSessionIdSource,"vercel.ai_gateway.affinity.outcome":n.affinity?.outcome,"vercel.ai_gateway.affinity.pinned_provider":n.affinity?.pinnedProvider,"vercel.ai_gateway.affinity.displaced_provider":n.affinity?.displacedProvider}}function readUsd(e){if(typeof e!=`string`||e.trim()===``)return;let t=Number(e);return Number.isFinite(t)?t:void 0}export{createGatewayTraceContextHeaders,gatewayCostAttributes,gatewayRoutingAttributes,readGatewayCostData};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { SpanContext } from "#compiled/@opentelemetry/api/index.js";
|
|
2
1
|
import type { AgentTraceStateStore } from "#tracing/eve/agent-trace-state.js";
|
|
3
2
|
import { type AgentTelemetry, type AgentRun } from "#compiled/@vercel/agent-runs/index.js";
|
|
4
3
|
export declare function traceSessionIdOf(scope: {
|
|
@@ -17,12 +16,6 @@ export declare function eveTurnIdentity(input: {
|
|
|
17
16
|
runId: string;
|
|
18
17
|
turnId: string;
|
|
19
18
|
};
|
|
20
|
-
/** Content flags are recorded by eve's content processor, not withheld by the library. */
|
|
21
|
-
export declare function eveCapture(reference: SpanContext | undefined): {
|
|
22
|
-
emit: boolean;
|
|
23
|
-
recordInputs: boolean;
|
|
24
|
-
recordOutputs: boolean;
|
|
25
|
-
};
|
|
26
19
|
/** eve's agent tracing, checkpointed in its workflow-context trace state. */
|
|
27
20
|
export declare function createEveTracing(input: {
|
|
28
21
|
readonly telemetry: AgentTelemetry;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveConversationId}from"#shared/conversation-identity.js";import{createAgentRun}from"#compiled/@vercel/agent-runs/index.js";function traceSessionIdOf(e){return e.traceSessionId??e.rootSessionId??e.sessionId}function checkpointContent(e){if(e!==void 0)try{return JSON.parse(e)}catch{return}}function eveTurnIdentity(e){return{conversationId:resolveConversationId(e.rootSessionId??e.sessionId),runId:e.sessionId,turnId:e.turnId}}function
|
|
1
|
+
import{resolveConversationId}from"#shared/conversation-identity.js";import{createAgentRun}from"#compiled/@vercel/agent-runs/index.js";function traceSessionIdOf(e){return e.traceSessionId??e.rootSessionId??e.sessionId}function checkpointContent(e){if(e!==void 0)try{return JSON.parse(e)}catch{return}}function eveTurnIdentity(e){return{conversationId:resolveConversationId(e.rootSessionId??e.sessionId),runId:e.sessionId,turnId:e.turnId}}function createEveTracing(e){return createAgentRun({telemetry:e.telemetry,checkpointer:{get:t=>e.stateStore.get(`checkpoint`,t),set:(t,n)=>e.stateStore.set(`checkpoint`,t,n),delete:t=>e.stateStore.delete(`checkpoint`,t)}})}export{checkpointContent,createEveTracing,eveTurnIdentity,traceSessionIdOf};
|
|
@@ -8,7 +8,7 @@ export declare function createEveCapturePolicy(input: {
|
|
|
8
8
|
readonly recordOutputs: boolean;
|
|
9
9
|
}): {
|
|
10
10
|
projectEvent(event: InstrumentationEvent): Promise<InstrumentationEvent>;
|
|
11
|
-
forOperation(scope: InstrumentationAttemptScope): Promise<{
|
|
11
|
+
forOperation(scope: Pick<InstrumentationAttemptScope, "sessionId" | "channelAudience">): Promise<{
|
|
12
12
|
recordInputs: boolean;
|
|
13
13
|
recordOutputs: boolean;
|
|
14
14
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{normalizeChannelAudience}from"#shared/channel-audience.js";import{applyLiveDeliveryAudienceCeiling,resolveForwardedTraceSeed}from"#shared/forwarded-trace-policy.js";import{contextStorage}from"#context/container.js";import{SessionTraceSeedKey}from"#context/keys.js";import{isSampledTrace,resolveTracePolicyDecision}from"#shared/trace-policy.js";import{withoutInstrumentationContent}from"#instrumentation/content.js";import{readInstrumentationDecision}from"#shared/instrumentation-decision.js";import{instrumentationEventForTraceDecision}from"#instrumentation/content-policy.js";function createEveCapturePolicy(e){return{async projectEvent(t){let n=await e.stateStore.get(`session`,`scope`in t?t.scope.sessionId:t.sessionId),r=audienceForEvent(t,n?.channelAudience),i=`traceSeed`in t?t.traceSeed:void 0,a=contextStorage.getStore()?.get(SessionTraceSeedKey),o=resolveForwardedTraceSeed(a),s=resolveForwardedTraceSeed(i,o?.forwardedTracePolicy),decisionForTrace=t=>t===void 0?void 0:resolveTracePolicyDecision(isSampledTrace(t),{audience:r,environment:e.environment}),c=s?.decision??o?.decision??readInstrumentationDecision(n?.decision)??decisionForTrace(i)??decisionForTrace(a)??decisionForTrace(n?.context);if(c===void 0)return withoutInstrumentationContent(t);let l=s===void 0||!(`traceSeed`in t)||t.traceSeed===void 0?t:{...t,traceSeed:{...t.traceSeed,decision:s.decision,traceFlags:s.traceFlags}};return instrumentationEventForTraceDecision(l,applyLiveDeliveryAudienceCeiling(c.action===`drop`?c:{...c,recordInputs:e.recordInputs&&c.recordInputs,recordOutputs:e.recordOutputs&&c.recordOutputs},r,s?.forwardedTracePolicy??o?.forwardedTracePolicy,e.environment),{audience:r,environment:e.environment},{applyAudienceCeiling:!1})},async forOperation(t){let n=await e.stateStore.get(`session`,t.sessionId),r=
|
|
1
|
+
import{normalizeChannelAudience}from"#shared/channel-audience.js";import{ConversationContextKey}from"#shared/conversation-context.js";import{applyLiveDeliveryAudienceCeiling,resolveForwardedTraceSeed}from"#shared/forwarded-trace-policy.js";import{contextStorage}from"#context/container.js";import{SessionTraceSeedKey}from"#context/keys.js";import{isSampledTrace,resolveTracePolicyDecision}from"#shared/trace-policy.js";import{withoutInstrumentationContent}from"#instrumentation/content.js";import{readInstrumentationDecision}from"#shared/instrumentation-decision.js";import{instrumentationEventForTraceDecision}from"#instrumentation/content-policy.js";function createEveCapturePolicy(e){return{async projectEvent(t){let n=await e.stateStore.get(`session`,`scope`in t?t.scope.sessionId:t.sessionId),r=audienceForEvent(t,n?.channelAudience),i=`traceSeed`in t?t.traceSeed:void 0,a=contextStorage.getStore()?.get(SessionTraceSeedKey),o=resolveForwardedTraceSeed(a),s=resolveForwardedTraceSeed(i,o?.forwardedTracePolicy),decisionForTrace=t=>t===void 0?void 0:resolveTracePolicyDecision(isSampledTrace(t),{audience:r,environment:e.environment}),c=s?.decision??o?.decision??readInstrumentationDecision(n?.decision)??decisionForTrace(i)??decisionForTrace(a)??decisionForTrace(n?.context);if(c===void 0)return withoutInstrumentationContent(t);let l=s===void 0||!(`traceSeed`in t)||t.traceSeed===void 0?t:{...t,traceSeed:{...t.traceSeed,decision:s.decision,traceFlags:s.traceFlags}};return instrumentationEventForTraceDecision(l,applyLiveDeliveryAudienceCeiling(c.action===`drop`?c:{...c,recordInputs:e.recordInputs&&c.recordInputs,recordOutputs:e.recordOutputs&&c.recordOutputs},r,s?.forwardedTracePolicy??o?.forwardedTracePolicy,e.environment),{audience:r,environment:e.environment},{applyAudienceCeiling:!1})},async forOperation(t){let n=await e.stateStore.get(`session`,t.sessionId),r=contextStorage.getStore(),i=resolveForwardedTraceSeed(r?.get(SessionTraceSeedKey)),a=i?.decision??n?.decision,o=a===void 0?void 0:applyLiveDeliveryAudienceCeiling(a,normalizeChannelAudience(t.channelAudience??r?.get(ConversationContextKey)?.audience??n?.channelAudience),i?.forwardedTracePolicy,e.environment);return{recordInputs:e.recordInputs&&o?.action===`record`&&o.recordInputs,recordOutputs:e.recordOutputs&&o?.action===`record`&&o.recordOutputs}}}}function audienceForEvent(e,t){return`delivery`in e?normalizeChannelAudience(e.delivery.channelAudience):`scope`in e&&e.scope.channelAudience!==void 0?normalizeChannelAudience(e.scope.channelAudience):e.type===`session.started`?normalizeChannelAudience(e.channelAudience):normalizeChannelAudience(t)}export{createEveCapturePolicy};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type InstrumentationSessionStartedEvent, type InstrumentationTraceSeed, type InstrumentationTurnStartedEvent, type InstrumentationTurnTerminalEvent, type InstrumentationSessionTransitionEvent } from "#instrumentation/lifecycle.js";
|
|
2
2
|
import type { TraceCapturePolicy } from "#tracing/eve/otel-declaration.js";
|
|
3
3
|
import type { AgentSessionTraceState, AgentTraceStateStore } from "#tracing/eve/agent-trace-state.js";
|
|
4
|
+
import type { createEveCapturePolicy } from "#tracing/eve/policy.js";
|
|
4
5
|
import type { ConversationEnvironment } from "#shared/conversation-context.js";
|
|
5
6
|
import type { AgentSpanIdGenerator, AgentRun, ExecutionContext, TurnOperation } from "#compiled/@vercel/agent-runs/index.js";
|
|
6
7
|
interface EveSessionTracingInput {
|
|
@@ -10,6 +11,7 @@ interface EveSessionTracingInput {
|
|
|
10
11
|
readonly frameworkVersion: string;
|
|
11
12
|
readonly stateStore: AgentTraceStateStore;
|
|
12
13
|
readonly tracePolicy?: TraceCapturePolicy;
|
|
14
|
+
readonly capturePolicy: ReturnType<typeof createEveCapturePolicy>;
|
|
13
15
|
}
|
|
14
16
|
type SessionMetadata = Omit<InstrumentationSessionStartedEvent, "idempotencyKey" | "type">;
|
|
15
17
|
interface EveSessionTracing {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{normalizeChannelAudience}from"#shared/channel-audience.js";import{normalizeInstrumentationChannelKind}from"#internal/instrumentation.js";import{withOperationConversation}from"#tracing/eve/conversation-context.js";import{isSampledTrace,resolveTracePolicy,resolveTracePolicyDecision}from"#shared/trace-policy.js";import{ROOT_CONTEXT}from"#compiled/@opentelemetry/api/index.js";import"#instrumentation/lifecycle.js";import{readInstrumentationDecision}from"#shared/instrumentation-decision.js";import{applyPrincipalTraceDecision}from"#instrumentation/principal-summary.js";import{withChannelAudience}from"#tracing/eve/channel-audience-context.js";import{
|
|
1
|
+
import{normalizeChannelAudience}from"#shared/channel-audience.js";import{normalizeInstrumentationChannelKind}from"#internal/instrumentation.js";import{withOperationConversation}from"#tracing/eve/conversation-context.js";import{isSampledTrace,resolveTracePolicy,resolveTracePolicyDecision}from"#shared/trace-policy.js";import{ROOT_CONTEXT}from"#compiled/@opentelemetry/api/index.js";import"#instrumentation/lifecycle.js";import{readInstrumentationDecision}from"#shared/instrumentation-decision.js";import{applyPrincipalTraceDecision}from"#instrumentation/principal-summary.js";import{withChannelAudience}from"#tracing/eve/channel-audience-context.js";import{eveTurnIdentity,traceSessionIdOf}from"#tracing/eve/operation-input.js";import{eveActivationMetadata}from"#tracing/eve/metadata.js";function createEveSessionTracing(e){let ensureSessionContext=async t=>{let n=await e.stateStore.get(`session`,t.sessionId);if(n===void 0){let r=normalizeChannelAudience(t.channelAudience),i=resolveSessionTraceDecision(t,r,e.environment,e.tracePolicy);n={agentName:t.agentName,channelAudience:r,channelKind:t.channelKind,channelType:t.channelType,decision:i,context:initialSessionContext(e,t,i),parentLineage:t.parentLineage,rootSessionId:t.rootSessionId,traceSessionId:traceSessionIdOf(t),scheduleId:t.scheduleId,title:t.title},await e.stateStore.set(`session`,t.sessionId,n)}return n},prepareSessionTrace=async e=>{let t=await ensureSessionContext(e);return portableSpanContext(t.context,t.decision)},turnKey=(e,t)=>JSON.stringify([e,t]);async function turnFor(t,n,r){let i=await e.stateStore.get(`turn`,turnKey(t,n));if(i===void 0||!isSampledTrace(i.context))return;let a=await e.stateStore.get(`session`,t);return e.tracing.resume({identity:eveTurnIdentity({sessionId:t,rootSessionId:i.rootSessionId,turnId:n}),context:r??hostContext(a?.channelAudience,{sessionId:t,...i})})}async function dropped(t,n){let r=await e.stateStore.get(`turn`,turnKey(t,n));return r!==void 0&&!isSampledTrace(r.context)}let prepareTurnTrace=async t=>{let n=await e.stateStore.get(`turn`,turnKey(t.sessionId,t.turnId));if(n!==void 0){let r=await e.stateStore.get(`session`,t.sessionId);return portableSpanContext(n.context,r?.decision)}let r=await ensureSessionContext(t),i=t.sequence===0,a=i?t.parentTraceContext:void 0,o=r.decision?.action===`record`,s=a!==void 0&&!(`isRemote`in a&&a.isRemote===!0),c=i?{...r.context,traceId:s?a.traceId:r.context.traceId,isRemote:!1}:{traceId:e.idGenerator.deriveTraceId(`turn:${t.idempotencyKey}`),spanId:e.idGenerator.deriveSpanId(`turn:${t.idempotencyKey}`),traceFlags:+!!o,isRemote:!1},l=r.agentName??(t.parentLineage??r.parentLineage)?.subagentName,u={agentName:l,caller:a===void 0?void 0:adoptedSpanContext(a),context:c,currentPrincipal:applyPrincipalTraceDecision(t.currentPrincipal,r.decision),initiatorPrincipal:applyPrincipalTraceDecision(t.initiatorPrincipal,r.decision),parentLineage:t.parentLineage??r.parentLineage,rootSessionId:t.rootSessionId,traceSessionId:traceSessionIdOf(t),sequence:t.sequence,startTimeMs:Date.now(),subagentName:(t.parentLineage??r.parentLineage)?.subagentName};if(isSampledTrace(c)){let n=await e.tracing.turn({...eveActivationMetadata({session:r,turn:u,sessionId:t.sessionId}),agentName:l,identity:eveTurnIdentity({...t,sessionId:t.sessionId}),framework:{name:`eve`,version:e.frameworkVersion},capture:{emit:!0,...await e.capturePolicy.forOperation(t)},reference:c,parent:s?adoptedSpanContext(a):void 0,links:activationLinks(u),startTimeMs:u.startTimeMs,context:hostContext(r.channelAudience,t)});u={...u,context:{...c,traceFlags:n.reference.traceFlags}}}return await e.stateStore.set(`turn`,turnKey(t.sessionId,t.turnId),u),portableSpanContext(u.context,r.decision)};async function recordTerminal(t,n,r,i){await e.stateStore.update(`turn`,turnKey(t,n),e=>({...e,terminal:r.outcome===`failed`?{outcome:`failed`,errorName:i instanceof Error?i.name:void 0,errorMessage:i instanceof Error?i.message:void 0}:r}))}return{ensureSessionContext,prepareSessionTrace,prepareTurnTrace,turnFor,dropped,async turnTerminal(e){await recordTerminal(e.sessionId,e.turnId,{outcome:e.type===`turn.failed`?`failed`:e.type===`turn.cancelled`?`cancelled`:`completed`},e.type===`turn.failed`?e.error:void 0)},async sessionTransition(t){if(t.turnId===void 0)return;t.type===`session.failed`&&await recordTerminal(t.sessionId,t.turnId,{outcome:`failed`},t.error);let n=await e.stateStore.get(`turn`,turnKey(t.sessionId,t.turnId));if(n===void 0)return;let r=await e.stateStore.get(`session`,t.sessionId),i=await turnFor(t.sessionId,t.turnId);if(i!==void 0){let e=eveActivationMetadata({session:r,turn:n,sessionId:t.sessionId});i.attributes(e.attributes??{}),i.links(activationLinks(n)??[]);let a=n.terminal;if(a===void 0)await i.complete({outcomeUnknown:!0});else if(a.outcome===`failed`){let e=Error(a.errorMessage);e.name=a.errorName??`Error`,await i.complete({outcome:`failed`,failed:!0,error:e,errorType:a.errorName})}else await i.complete({outcome:a.outcome})}await e.stateStore.delete(`turn`,turnKey(t.sessionId,t.turnId))}}}function hostContext(e,t){return withOperationConversation(withChannelAudience(ROOT_CONTEXT,e),t)}function activationLinks(e){let t=[];return e.caller?.isRemote===!0&&t.push({context:e.caller,relationship:`agent.dispatch`}),e.channelDelivery?.requestTraceContext!==void 0&&t.push({context:e.channelDelivery.requestTraceContext,relationship:`channel.request`}),t.length===0?void 0:t}function portableSpanContext(e,t){return{decision:t,spanId:e.spanId,traceFlags:e.traceFlags,traceId:e.traceId}}function adoptedSpanContext(e){return{isRemote:`isRemote`in e&&e.isRemote===!0,spanId:e.spanId,traceFlags:e.traceFlags,traceId:e.traceId}}function initialSessionContext(e,t,n){let r=t.traceSeed;if(r!==void 0)return{...adoptedSpanContext(r),traceFlags:n.action===`drop`?0:r.traceFlags};let i=`session:${t.sessionId}`,a=t.parentTraceContext;return{traceId:a!==void 0&&!(`isRemote`in a&&a.isRemote===!0)?a.traceId:e.idGenerator.deriveTraceId(i),spanId:e.idGenerator.deriveSpanId(i),traceFlags:+(n.action===`record`)}}function resolveSessionTraceDecision(e,t,n,r){let i={audience:t,environment:n};return e.parentTraceContext!==void 0&&!isSampledTrace(e.parentTraceContext)?{action:`drop`}:e.traceSeed?.decision===void 0?e.traceSeed===void 0?e.parentTraceContext===void 0?e.agentName===void 0?r===void 0?resolveTracePolicyDecision(!0,i):{action:`drop`}:resolveTracePolicy(r,{agentName:e.agentName,audience:t,channel:{kind:normalizeInstrumentationChannelKind(e.channelKind??e.channelType)},environment:n,principalType:`unknown`}):resolveTracePolicyDecision(isSampledTrace(e.parentTraceContext),i):resolveTracePolicyDecision(isSampledTrace(e.traceSeed),i):readInstrumentationDecision(e.traceSeed.decision)??{action:`drop`}}export{createEveSessionTracing};
|
package/docs/README.md
CHANGED
|
@@ -25,6 +25,7 @@ Important naming note:
|
|
|
25
25
|
| Package a procedure the agent loads only when it applies | [Skills](./skills.mdx) |
|
|
26
26
|
| Carry state across turns, or shape what the model sees | [State](./concepts/state.md), [Context Control](./concepts/context-control.md) |
|
|
27
27
|
| Run commands or untrusted code in isolation | [Sandboxes](./sandbox/index.mdx) |
|
|
28
|
+
| Answer voice notes and other audio attachments | [Voice Notes](./patterns/voice-notes.md) |
|
|
28
29
|
| Delegate work to a specialist child agent | [Subagents](./subagents/index.mdx) |
|
|
29
30
|
| Run work on a recurring schedule | [Schedules](./schedules.mdx) |
|
|
30
31
|
| Install an existing integration instead of writing one | [Add Integrations](./install-integrations.mdx) |
|
package/docs/agent-config.md
CHANGED
|
@@ -67,6 +67,28 @@ Gateway, share that ID so their Gateway generations can be found together.
|
|
|
67
67
|
Direct-provider calls do not receive the option. An authored `gateway.sessionId`
|
|
68
68
|
takes precedence; AI Gateway hashes IDs longer than 256 characters.
|
|
69
69
|
|
|
70
|
+
AI Gateway also uses this ID for
|
|
71
|
+
[session affinity](https://vercel.com/docs/ai-gateway/models-and-providers/session-affinity),
|
|
72
|
+
which tries the provider that served the session's previous call first, among
|
|
73
|
+
the providers your routing settings allow, so that provider can read the prompt
|
|
74
|
+
from its cache. The conversation
|
|
75
|
+
appears as one session in the AI Gateway logs Sessions view. Affinity can move
|
|
76
|
+
the pinned provider ahead of your `order`. To keep `order` exactly, set
|
|
77
|
+
`sessionAffinity: false`:
|
|
78
|
+
|
|
79
|
+
```ts title="agent/agent.ts"
|
|
80
|
+
import { defineAgent } from "eve";
|
|
81
|
+
|
|
82
|
+
export default defineAgent({
|
|
83
|
+
model: "anthropic/claude-opus-5.5",
|
|
84
|
+
modelOptions: {
|
|
85
|
+
providerOptions: {
|
|
86
|
+
gateway: { order: ["bedrock", "anthropic"], sessionAffinity: false },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
70
92
|
For direct OpenAI and `chatgpt()` model calls, eve sets
|
|
71
93
|
`providerOptions.openai.promptCacheKey` to a SHA-256 hash of the session ID,
|
|
72
94
|
which helps OpenAI route a session's calls to the cache that holds its prompt
|
|
@@ -140,7 +140,7 @@ This policy controls overlapping eve turns. Chat SDK's separate `concurrency` op
|
|
|
140
140
|
|
|
141
141
|
The default handlers post the agent's reply back to the thread — no `events` override required. Completed assistant messages are posted as markdown (`{ markdown: … }`) so adapters render rich text and email HTML rather than a raw string.
|
|
142
142
|
|
|
143
|
-
Streaming is on by default: the channel posts an initial message and edits it as tokens arrive (`message.appended`), throttled by `streamingEditIntervalMs` (default `1000`). Set `streaming: false` for surfaces that deliver one message per turn — email, for example — so the reply posts once on completion instead of editing:
|
|
143
|
+
Streaming is on by default: the channel posts an initial message and edits it as tokens arrive (`message.appended`), throttled by `streamingEditIntervalMs` (default `1000`). Set `streaming: false` for surfaces that deliver one message per turn or can't edit sent messages — email and WhatsApp, for example — so the reply posts once on completion instead of editing:
|
|
144
144
|
|
|
145
145
|
```ts
|
|
146
146
|
chatSdkChannel({
|
|
@@ -245,7 +245,9 @@ bot.onNewMention(async (thread, message) => {
|
|
|
245
245
|
});
|
|
246
246
|
```
|
|
247
247
|
|
|
248
|
-
When the adapter can download an attachment (it sets `fetchData`, as the Slack, Google Chat, WhatsApp, and Twilio adapters do), eve downloads it with the adapter's credentials when the turn runs, not in the webhook, and passes the bytes. Platform file URLs usually need those credentials, so the model provider couldn't open them. eve rebuilds the download with the adapter's `rehydrateAttachment`; an adapter without one can't download after the webhook returns, so its files reach the agent as a short note. A failed download, or a file over 25 MB, also becomes a note. An attachment with only a URL is downloaded by eve when the URL is public `https:`; otherwise it becomes a note.
|
|
248
|
+
When the adapter can download an attachment (it sets `fetchData`, as the Slack, Google Chat, WhatsApp, and Twilio adapters do), eve downloads it with the adapter's credentials when the turn runs, not in the webhook, and passes the bytes. Platform file URLs usually need those credentials, so the model provider couldn't open them. eve rebuilds the download with the adapter's `rehydrateAttachment`; an adapter without one can't download after the webhook returns, so its files reach the agent as a short note. A failed download, or a file over 25 MB, also becomes a note. An attachment with only a URL is downloaded by eve when the URL is public `https:`; otherwise it becomes a note. An attachment with neither a download nor a URL also becomes a note, so the agent still knows a file was sent.
|
|
249
|
+
|
|
250
|
+
Audio attachments, such as voice notes, reach the model as a file label, not as sound. To answer them, give the agent a transcription tool; see [Voice notes](../patterns/voice-notes).
|
|
249
251
|
|
|
250
252
|
See [File uploads](./custom#file-uploads) for how eve stages remote file URLs before the model call.
|
|
251
253
|
|
package/docs/channels/eve.mdx
CHANGED
|
@@ -341,6 +341,33 @@ export default eveChannel({
|
|
|
341
341
|
|
|
342
342
|
`onMessage` must return an auth result. Return `title` alongside `auth` to set the title when the dispatch creates a session or sends the first message to a prewarmed session. A successful canonical eve HTTP message always dispatches and therefore always produces or continues a session.
|
|
343
343
|
|
|
344
|
+
### Turn context
|
|
345
|
+
|
|
346
|
+
`context` strings become user messages in durable session history. For context that applies to one turn only, return `clientContext` instead. It replaces the request's [`clientContext`](../guides/client/messages) for that turn: it reaches every model call in the turn and is not kept in session history.
|
|
347
|
+
|
|
348
|
+
`ctx.eve.clientContext` holds the request's `clientContext` exactly as the client sent it, or `undefined` when it sent none. Use it to check or correct client-supplied state before the model sees it:
|
|
349
|
+
|
|
350
|
+
```ts title="agent/channels/eve.ts"
|
|
351
|
+
import { eveChannel, defaultEveAuth } from "eve/channels/eve";
|
|
352
|
+
import { vercelOidc } from "eve/channels/auth";
|
|
353
|
+
|
|
354
|
+
import { getVerifiedPlan } from "../lib/billing";
|
|
355
|
+
|
|
356
|
+
export default eveChannel({
|
|
357
|
+
auth: [vercelOidc()],
|
|
358
|
+
async onMessage(ctx) {
|
|
359
|
+
const auth = defaultEveAuth(ctx);
|
|
360
|
+
const plan = await getVerifiedPlan(auth);
|
|
361
|
+
return { auth, clientContext: [`The user's plan is ${plan}.`] };
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
- Omit `clientContext` to keep the request's context. Return `[]` to drop it.
|
|
367
|
+
- The returned value takes the same shape as request `clientContext` (`ClientContextValue`, exported from `eve/channels/eve`): a string, an array of strings, or a JSON object. eve turns it into model messages the same way.
|
|
368
|
+
- eve labels client-supplied entries `Client context:` for the model. Returned context is sent without the label.
|
|
369
|
+
- `onMessage` runs only for requests with a message. An input response keeps the context its request sent.
|
|
370
|
+
|
|
344
371
|
## Clients
|
|
345
372
|
|
|
346
373
|
The browser side of this API lives in the [Frontend](../guides/frontend/overview) docs, where `useEveAgent` drives the eve channel from React UI.
|
package/docs/channels/photon.mdx
CHANGED
|
@@ -34,8 +34,10 @@ Set `turnPolicy: "queue"` when every response should finish before Photon starts
|
|
|
34
34
|
|
|
35
35
|
Images and other files people send reach the agent as file parts. Photon's
|
|
36
36
|
webhook lists each file without its bytes, so eve downloads it from Photon by
|
|
37
|
-
its attachment id with the project's credentials.
|
|
38
|
-
|
|
37
|
+
its attachment id with the project's credentials. The webhook omits the id
|
|
38
|
+
for voice notes, so eve refetches those messages from Photon to get it. A
|
|
39
|
+
failed download, a file over 25 MB, or a file eve can't find an id for reaches
|
|
40
|
+
the agent as a short note.
|
|
39
41
|
|
|
40
42
|
Customize inbound dispatch with `onMessage`. Return `null` to ignore a message, or return `title` alongside `auth` to set the title when the dispatch starts a run:
|
|
41
43
|
|
package/docs/channels/slack.mdx
CHANGED
|
@@ -750,6 +750,8 @@ The agent receives the files attached to the message that reaches it. When a men
|
|
|
750
750
|
|
|
751
751
|
Images, PDFs, audio, video, and other files all reach the agent. See [Inbound attachments](../sandbox#inbound-attachments) for which files the model reads directly and which it opens with tools. Files shared through an integration such as Google Drive or Dropbox are links to the other service, not Slack files, so eve skips them; their link stays in the message text. A download that fails or takes longer than 30 seconds reaches the model as a note.
|
|
752
752
|
|
|
753
|
+
Slack voice clips reach the model as an audio file label, not as sound. To answer them, give the agent a transcription tool; see [Voice notes](../patterns/voice-notes).
|
|
754
|
+
|
|
753
755
|
Private file downloads resolve credentials from the session's app installation workspace and require the Slack bot token's `files:read` scope. Add the scope and reinstall the Slack app before sending attachments.
|
|
754
756
|
|
|
755
757
|
Downloads cover Slack's own file hosts plus URLs under a configured [`api.fileBaseUrl`](#point-the-channel-at-a-different-slack-api-host), and travel on `api.fetch` when one is configured.
|
|
@@ -27,7 +27,11 @@ When a Vercel production deployment changes, a new delivery to an idle session h
|
|
|
27
27
|
|
|
28
28
|
Before taking ownership, the target deployment upgrades the checkpoint when an older eve release wrote it, then checks that no pending work remains. A deployment accepts checkpoints from eve 0.66.0 and later, so upgrading eve does not leave long-lived sessions, such as a Slack thread, on their old deployment. An older release cannot read a newer checkpoint: after a rollback, a session that already moved to the newer deployment stays there. Extension state saved before eve 0.69 moves to its mount when exactly one mount of that extension package defines that state. State that the target deployment no longer defines, such as a removed `defineState` key, is dropped, and the target deployment's runtime logs show a `dropping unknown context key during deserialization` warning. When two or more mounts of one extension package define that state, eve cannot choose an owner, and the session stays on its current deployment. Idle subagent sessions that eve 0.66–0.68 kept for later reuse are stopped after the handoff. Authored session state remains opaque to this validation.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Messages that arrive while a handoff is in progress are accepted right away and are not lost. The new owner processes them after the triggering delivery, in the order they arrived, whichever run accepted them.
|
|
31
|
+
|
|
32
|
+
The previous owner forwards the messages it accepted as soon as the new owner has taken over the session. Until they arrive, the new owner holds back everything it receives itself, including replies to tool runs and subagents that its first turn started, and processes it afterwards. A cancel still stops the new owner's running turn as soon as it arrives. If either run ends before the handoff settles, the other stops waiting for it.
|
|
33
|
+
|
|
34
|
+
If the target deployment rejects the session, the previous owner keeps it and processes the triggering delivery, followed by any messages that arrived during the attempt. When the target deployment cannot read the checkpoint, its Vercel runtime logs show a `session handoff refused` warning with the checkpoint version and the reason. Owners on this release or later also write a `session handoff failed` warning to their own runtime logs, with the target deployment and the reason.
|
|
31
35
|
|
|
32
36
|
eve does not move live work between deployments. A session with a running turn, pending human input or authorization, queued messages, or active tasks and subagents stays on its current deployment. After that work settles, a later delivery can move the idle session; the earlier deployment signal is not remembered.
|
|
33
37
|
|
|
@@ -35,7 +39,9 @@ eve does not move live work between deployments. A session with a running turn,
|
|
|
35
39
|
|
|
36
40
|
Every workflow run records each step it executes, so a session that ran on one run for weeks would carry its whole history in that run. To bound this, eve moves a session to a fresh run on its current deployment after [compaction](./default-harness#compaction) rewrites the conversation history. This applies to automatic compaction during a turn and to a client or channel compaction request.
|
|
37
41
|
|
|
38
|
-
The move happens once the turn settles and nothing else is waiting: no queued messages, running tasks or subagents, or pending human input. The new run receives the compacted history and settled state, then waits for the next message.
|
|
42
|
+
The move happens once the turn settles and nothing else is waiting: no queued messages, running tasks or subagents, or pending human input. The new run receives the compacted history and settled state, then waits for the next message. A message that is already waiting when the turn settles runs on the current run first, and the session moves at the next point where nothing is waiting. A message that arrives after the move starts is accepted right away and runs once, on the new run. If work is still pending, eve tries again after a later turn settles. The session keeps its ID, event stream, continuation addresses, and deadline; compaction never extends [`sessionTimeoutMs`](../agent-config#runtime-limits). If the new run cannot take ownership, the current run keeps the session, writes a `session handoff failed` warning to its runtime logs, and tries again later.
|
|
43
|
+
|
|
44
|
+
Both kinds of handoff need a Workflow world that can move a session's addresses from one run to another without releasing them, and can keep a released address reserved for a while (the `hookForceClaim` and `hookRetention` capabilities). Vercel Workflow, the local world, and `@workflow/world-postgres` 5.0.4 and later provide both. On a world without them, sessions never move between runs: deployment and compaction handoffs are skipped, and a session's run history grows for as long as the session lives. The first skipped attempt writes a `session handoff unavailable` warning to the runtime logs.
|
|
39
45
|
|
|
40
46
|
### Stranded sessions
|
|
41
47
|
|
|
@@ -253,11 +259,14 @@ the returned handle. That identity lookup does not block workflow resumption.
|
|
|
253
259
|
ID-addressed delivery already knows the session ID and needs no hook metadata
|
|
254
260
|
lookup. An ambiguous delivery failure does not trigger session creation.
|
|
255
261
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
delivery
|
|
259
|
-
|
|
260
|
-
|
|
262
|
+
A handoff never leaves the session's addresses without an owner. The previous
|
|
263
|
+
owner keeps every address until the new run takes all of them over, so a
|
|
264
|
+
delivery during a handoff is accepted immediately and is never reported as an
|
|
265
|
+
inactive session. An owner still running an earlier eve release gives up its
|
|
266
|
+
addresses briefly during a handoff and leaves a short-lived marker for each
|
|
267
|
+
one; a delivery that lands in that interval retries until the successor
|
|
268
|
+
claims the address. Either way, a channel never starts a replacement session
|
|
269
|
+
for an address that is changing owners.
|
|
261
270
|
|
|
262
271
|
When a session is waiting, a delivery through its ID or any claimed channel address wakes it and starts the next turn. Message sends default to `turnPolicy: "steer"`. Before assistant output begins, the session owner signals pending model generation to stop, then applies the correction within the same turn and with the same turn ID. This includes model requests that perform provider-managed search before answering; interrupted search work may run again. Reasoning and search progress do not count as assistant output.
|
|
263
272
|
|
|
@@ -188,6 +188,26 @@ Gateway-routed model calls propagate the configured OpenTelemetry context at
|
|
|
188
188
|
each provider request. Eve-owned baggage is removed before forwarding. Gateway
|
|
189
189
|
controls whether its spans are sampled and which destinations receive them.
|
|
190
190
|
|
|
191
|
+
When AI Gateway reports
|
|
192
|
+
[session affinity](https://vercel.com/docs/ai-gateway/models-and-providers/session-affinity)
|
|
193
|
+
for a model call, the `agent.step` span records it. These attributes appear even
|
|
194
|
+
when the trace content policy omits outputs.
|
|
195
|
+
|
|
196
|
+
`vercel.ai_gateway.client_session_id` is the session ID AI Gateway used for
|
|
197
|
+
affinity. It is not the eve session ID that `vercel.session_id` records. It
|
|
198
|
+
equals `gen_ai.conversation.id` unless you set `gateway.sessionId` yourself or
|
|
199
|
+
the ID is longer than 256 characters, in which case AI Gateway hashes it. An
|
|
200
|
+
authored `gateway.sessionId` is recorded verbatim, so don't put personal data in
|
|
201
|
+
it.
|
|
202
|
+
|
|
203
|
+
| Attribute | Value |
|
|
204
|
+
| ----------------------------------------------- | --------------------------------------------------------------- |
|
|
205
|
+
| `vercel.ai_gateway.client_session_id` | Session ID AI Gateway used |
|
|
206
|
+
| `vercel.ai_gateway.client_session_id_source` | `explicit`, `agent`, or `fingerprint` |
|
|
207
|
+
| `vercel.ai_gateway.affinity.outcome` | What affinity did, such as `promoted`, `confirmed`, or `no_pin` |
|
|
208
|
+
| `vercel.ai_gateway.affinity.pinned_provider` | Provider the session's pin named |
|
|
209
|
+
| `vercel.ai_gateway.affinity.displaced_provider` | Provider that would have gone first, when the pin moved it |
|
|
210
|
+
|
|
191
211
|
## Local traces
|
|
192
212
|
|
|
193
213
|
`eve dev` records local traces by default. Omitting
|
package/docs/patterns/meta.json
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Voice Notes"
|
|
3
|
+
description: "Transcribe voice notes, voice messages, and other audio attachments with an AI Gateway transcription model so an eve agent can answer them."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Voice notes and other audio files reach your agent, but by default the model does not transcribe them. To give your agent the ability to answer a voice note, you will need to add a tool that transcribes the staged file.
|
|
7
|
+
|
|
8
|
+
This works for any channel that delivers audio as a file, such as iMessage voice notes through [Photon](../channels/photon), [Slack](../channels/slack#attachments) voice clips, and files sent through the [eve](../channels/eve) and [Chat SDK](../channels/chat-sdk#attachments) channels. See [Inbound attachments](../sandbox#inbound-attachments) for how eve stages files.
|
|
9
|
+
|
|
10
|
+
## Add a transcription tool
|
|
11
|
+
|
|
12
|
+
```ts title="agent/tools/transcribe_audio.ts"
|
|
13
|
+
import { posix } from "node:path";
|
|
14
|
+
import { transcribe } from "ai";
|
|
15
|
+
import { defineTool } from "eve/tools";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
const TRANSCRIPTION_MODEL = "openai/whisper-1";
|
|
19
|
+
const ATTACHMENTS_DIR = "/workspace/.eve/attachments/";
|
|
20
|
+
|
|
21
|
+
export default defineTool({
|
|
22
|
+
description:
|
|
23
|
+
"Transcribe an audio attachment, such as a voice note, to text. Pass the path from its `Attached file` label.",
|
|
24
|
+
inputSchema: z.object({
|
|
25
|
+
path: z.string().describe(`Attachment path under ${ATTACHMENTS_DIR}`),
|
|
26
|
+
}),
|
|
27
|
+
outputSchema: z.object({ transcript: z.string() }),
|
|
28
|
+
async execute({ path }, ctx) {
|
|
29
|
+
const file = posix.normalize(path);
|
|
30
|
+
if (!file.startsWith(ATTACHMENTS_DIR)) {
|
|
31
|
+
throw new Error(`Only attachments under ${ATTACHMENTS_DIR} can be transcribed.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const sandbox = await ctx.getSandbox();
|
|
35
|
+
const audio = await sandbox.readBinaryFile({ path: file, abortSignal: ctx.abortSignal });
|
|
36
|
+
if (!audio) {
|
|
37
|
+
throw new Error(`Attachment not found: ${file}. Ask the user to send the voice note again.`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const { text } = await transcribe({
|
|
42
|
+
model: TRANSCRIPTION_MODEL,
|
|
43
|
+
audio,
|
|
44
|
+
abortSignal: ctx.abortSignal,
|
|
45
|
+
});
|
|
46
|
+
return { transcript: text.trim() };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Transcription failed (${reason}). Tell the user you couldn't understand the voice note and ask them to type it.`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`transcribe()` uses the same credentials as your eve agent.
|
|
58
|
+
|
|
59
|
+
## Tell the agent when to transcribe
|
|
60
|
+
|
|
61
|
+
```md title="agent/instructions.md"
|
|
62
|
+
When a message has an audio attachment (`Attached file ... (audio/...)`), such as a voice note, call `transcribe_audio` with its path first and treat the transcript as what the user said. Do not mention the transcription step unless it fails.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## When transcription fails
|
|
66
|
+
|
|
67
|
+
When `execute` throws, eve gives the error to the model as a tool error, which it can relay to the user. See [When a tool throws](../tools#when-a-tool-throws). A file over the channel's upload limit, 25 MB by default, or a file eve can't download reaches the agent as a short note instead of a file.
|
package/docs/sandbox/index.mdx
CHANGED
|
@@ -162,6 +162,8 @@ Before every model call, eve reads the referenced files back from the sandbox. E
|
|
|
162
162
|
|
|
163
163
|
Other files, such as CSV, HEIC, audio, and video, reach the model as the label alone. eve checks image and PDF media types against the file's bytes when it stages the file. A file whose bytes don't match its declared image or PDF type is staged as `application/octet-stream`, so a provider never receives bytes it would reject.
|
|
164
164
|
|
|
165
|
+
To let the agent answer voice notes and other audio, add a tool that transcribes the staged file. See [Voice notes](/docs/patterns/voice-notes).
|
|
166
|
+
|
|
165
167
|
If a referenced file is missing, or its bytes no longer match what eve staged because something in the sandbox overwrote it, the model receives a `FileNotFound` note in its place.
|
|
166
168
|
|
|
167
169
|
A file that eve can't stage never enters session history. When a channel's `fetchFile` fails, or a file part's `data` is not bytes, base64, a `data:` URL, or a URL, the model receives a note that names the file instead.
|