Use Cases

eve

Source Code
Export one evlog wide event per eve agent turn — token usage, tool executions, business context, drains, enrichers, and tail sampling alongside Agent Runs and OpenTelemetry.

eve ships built-in observability: Agent Runs on Vercel and optional OpenTelemetry spans via agent/instrumentation.ts. evlog/eve adds a third layer — exportable wide events per turn with your full evlog pipeline (drains, enrichers, tail sampling, audit).

evlog/eve requires eve 0.30 or later. eve is still in beta and stream event shapes may change before GA, so pin eve and evlog versions in production agents.

Add evlog wide events to my eve agent

When to use what

NeedUse
Debug a session in Verceleve Agent Runs (automatic)
Span-level traces in Datadog / Honeycombeve agent/instrumentation.ts + OTel exporter
Wide events to Axiom / Better Stack / FS, billing, audit, tail samplingevlog/eve
Jumping from a span to its wide event, and backdefineEvlogInstrumentation()below

Quick Start

1. Install

pnpm add evlog eve

2. Add the hook

Create agent/hooks/evlog.ts:

agent/hooks/evlog.ts
import { defineEvlogHook } from 'evlog/eve'
import { createAxiomDrain } from 'evlog/axiom'

export default defineEvlogHook({
  init: { env: { service: 'my-agent' } },
  drain: createAxiomDrain(),
  enrich: (ctx) => {
    ctx.event.region = process.env.VERCEL_REGION
  },
})

eve auto-discovers hook files under agent/hooks/. No HTTP middleware — the unit of work is an agent turn, not a request.

3. Log business context from tools

defineEvlogHook() binds the turn logger via AsyncLocalStorage on turn.started. Inside tool execute() handlers, call useLogger() with no arguments — same ergonomics as useLogger(event) in Nuxt or Hono.

In the example agent, support tools attach customer and order context as the agent works a refund:

agent/tools/lookup_order.ts
import { defineTool } from 'eve/tools'
import { useLogger } from 'evlog/eve'
import { z } from 'zod'

export default defineTool({
  description: 'Look up an order by id.',
  inputSchema: z.object({ orderId: z.string() }),
  async execute({ orderId }) {
    const order = await fetchOrder(orderId)
    const log = useLogger()
    log.set({ order: { id: order.id, amount: order.amount, status: order.status } })
    return order
  },
})
Fallback: if useLogger() throws outside a turn, pass eve tool ctx: useLogger(ctx). This covers separate hook/tool bundles or runtimes where AsyncLocalStorage does not propagate. With a standard eve agent layout and agent/hooks/evlog.ts registered, you should not need it.

4. Next.js web chat (optional)

Wrap next.config.ts with withEve() from eve/next and use useEveAgent() from eve/react in your app. eve starts alongside next dev and proxies /eve/v1/* on the same origin — tool calls, approvals, and ask_question work out of the box. See the example agent.

Wide event shape

Each completed turn emits one event:

Wide Event — turn awaiting approval
{
  "method": "EVE",
  "path": "/sessions/sess_abc/turns/turn_0",
  "status": 200,
  "duration": "7.9s",
  "durationMs": 7903,
  "service": "clearbill-support-agent",
  "eve": {
    "sessionId": "sess_abc",
    "turnId": "turn_0",
    "turnSequence": 0,
    "phase": "awaiting-approval",
    "sessionTurns": 1
  },
  "customer": { "slug": "acme-corp", "plan": "enterprise" },
  "order": { "id": "4821", "amount": 890, "currency": "USD" },
  "approval": { "status": "pending", "tool": "issue_refund" },
  "ai": {
    "calls": 2,
    "steps": 2,
    "inputTokens": 11724,
    "outputTokens": 282,
    "finishReason": "tool-calls",
    "tools": [
      { "name": "lookup_customer", "durationMs": 26, "success": true },
      { "name": "lookup_order", "durationMs": 13, "success": true }
    ]
  }
}

After approval, the next turn carries the outcome:

Wide Event — refund completed
{
  "path": "/sessions/sess_abc/turns/turn_1",
  "eve": {
    "sessionId": "sess_abc",
    "turnId": "turn_1",
    "turnSequence": 1,
    "sessionTurns": 2
  },
  "refund": { "orderId": "4821", "amount": 890, "status": "refunded" },
  "audit": { "action": "refund.issued", "target": { "type": "order", "id": "4821" } },
  "approval": { "status": "approved", "tool": "issue_refund" },
  "ai": {
    "tools": [{ "name": "issue_refund", "durationMs": 993, "success": true }],
    "finishReason": "stop"
  }
}

Token usage and tool executions are accumulated from eve stream events (step.completed, actions.requested, action.result). Business fields set via useLogger() carry across turns in the same session. Link turns in analytics with eve.sessionId + eve.turnSequence — each event stays self-contained. eve.phase is only set for non-routine endings: awaiting-approval, awaiting-authorization, rejected, cancelled, failed.

Everything the turn can report

Beyond the identifiers above, a turn records what eve reports about it. Every field is optional — it appears only when the turn produced it.

FieldWhat it tells you
eve.runtimeeve version, agent id, model, and the deployed gitSha / gitBranch / deployedAt
eve.parentParent and root session ids for a subagent run — rebuild the delegation tree with rootSessionId
eve.authorizationsConnection sign-ins with their outcome, reason and duration
eve.compactionHow many compactions ran, on which model, and inputTokensAtTrigger — how full the context was when the first one fired
eve.stepFailures / eve.failedStepsModel calls that failed, including on a turn that then succeeded on retry
eve.subagentsDelegations with their status and duration
eve.reasoningblocks and chars — the size of the model's thinking, never its content
eve.resultThe structured result, for an agent with an output schema
eve.contextClearedThe durable history was wiped during this turn
message.responseCharsLength of the agent's response, recorded whatever the message mode
ai.costUsdCost as reported by eve. ai.estimatedCost is the fallback computed from cost

Correlate traces with wide events

A wide event and an Agent Runs span describe the same turn, but nothing joins them on its own. defineEvlogInstrumentation() stamps evlog's turn identity onto eve's AI SDK spans:

agent/instrumentation.ts
import { defineEvlogInstrumentation } from 'evlog/eve'

export default defineEvlogInstrumentation()

Every model-call span — and its children — then carries evlog.request_id and evlog.session_id, the same values the wide event reports as requestId and eve.sessionId. Jump from a trace in Braintrust, Datadog or Agent Runs straight to the event in your drain, and back.

Without setup, OpenTelemetry export is untouched: eve keeps writing its local traces, readable with eve traces. Pass one to export elsewhere:

agent/instrumentation.ts
import { defineEvlogInstrumentation } from 'evlog/eve'
import { registerOTel } from '@vercel/otel'

export default defineEvlogInstrumentation({
  setup: ({ agentName }) => registerOTel({ serviceName: agentName }),
})

functionId, recordInputs, recordOutputs and traceChannelRequests pass straight through to eve's defineInstrumentation.

Production

Long-running eve agents should disable terminal pretty-printing and use a non-blocking drain:

agent/hooks/evlog.ts
import { defineEvlogHook } from 'evlog/eve'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'

const drain = createDrainPipeline({ batch: { size: 50, intervalMs: 5000 } })(
  createAxiomDrain(),
)

export default defineEvlogHook({
  init: {
    env: { service: 'my-agent', environment: 'production' },
    pretty: false,
    sampling: { rates: { info: 10 } },
  },
  drain,
  maxSessions: 256,
})
ConcernRecommendation
Terminal outputinit.pretty: false — pretty-print is for local dev only
Drain latencyBatch or async HTTP drains; never block the turn on I/O
Head samplinginit.sampling.rates — eve emits one event per turn, not per token
MemorymaxSessions (default 256) evicts oldest idle session state
LoadHook handlers are O(1) per stream event; cost is dominated by your drain

Options

OptionDescription
initPassed to initLogger() on first hook invocation
drain / enrich / keep / pluginsSame as HTTP integrations (plugins)
message'omit' (default), 'preview' or 'full' — how much of the user message and the agent response to record
messagePreviewLengthCharacters kept in 'preview' mode (default 500)
sessionEventtrue emits one extra event per session, rolling up its turns
cost / modelFallback token pricing (ModelCost from evlog/ai) → ai.estimatedCost, used only when eve reports no cost
maxSessionsIn-memory session cap for context carry-over (default 256)
include / excludeRoute-style filters on turn paths (/sessions/*/turns/*)

Tail sampling

Use keep to force-keep turns with failed tools or high token usage:

agent/hooks/evlog.ts
export default defineEvlogHook({
  drain: createAxiomDrain(),
  keep: (ctx) => {
    const ai = ctx.context.ai as {
      inputTokens?: number
      outputTokens?: number
      tools?: Array<{ success: boolean }>
    } | undefined
    const totalTokens = (ai?.inputTokens ?? 0) + (ai?.outputTokens ?? 0)
    if (totalTokens > 10_000) ctx.shouldKeep = true
    if (ai?.tools?.some(t => !t.success)) ctx.shouldKeep = true
  },
})

Audit logs

Combine with Audit Logs: register auditEnricher() via init.plugins or a global plugin, and call log.audit() inside tools when a human approval gate fires. Tool rejections surface on action.result with status: "rejected".

Run locally

git clone https://github.com/HugoRCD/evlog
cd evlog
pnpm install
pnpm example eve

The examples/eve project is a support refund copilot (Clearbill SaaS): lookup customer → lookup order → issue refund, with eve approvals when amount > $100. Run pnpm example eve, open http://localhost:3000, and click a starter prompt.