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).

eve is currently in beta; hook and stream event shapes may change before GA. 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

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",
  "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, rejected, failed).

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)
redactMessageDefault true — omits user message text from the wide event
cost / modelOptional token pricing (ModelCost from evlog/ai) → ai.estimatedCost
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 run 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 run example:eve, open http://localhost:3000, and click a starter prompt.