Skip to main content
Eve is Vercel’s filesystem-first framework for durable backend AI agents: you define an agent as files under an agent/ directory and Eve runs it on Vercel Functions. In this guide you’ll scaffold Eve’s hello-world weather agent, wire it to Arize AX with a single instrumentation file, run a session, and explore the resulting trace — the per-turn breakdown of model calls and tool executions. Eve emits Vercel AI SDK OpenTelemetry spans, and Arize AX ingests them through the @arizeai/openinference-vercel span processor — the same processor used by the Vercel AI SDK integration.

Prerequisites

  • Node.js 24+ (Eve’s CLI requires it)
  • An Arize AX account (sign up) — copy your Space ID and API Key from Space Settings
  • An AI_GATEWAY_API_KEY for Eve’s model routing (or run vercel link to use a VERCEL_OIDC_TOKEN)

Step 1: Scaffold the agent

Create a new Eve agent. The CLI scaffolds the project, installs dependencies, initializes Git, and starts a dev server:
npx eve@latest init weather-agent
This generates an agent/ directory — instructions.md (the system prompt), agent.ts (runtime config), tools/ (one file per tool), and channels/eve.ts (the built-in HTTP channel). The minimal agent is just two files. agent/instructions.md:
You are a concise assistant. Use tools when they are available.
agent/agent.ts — the scaffold writes a default model (currently anthropic/claude-sonnet-4.6):
import { defineAgent } from "eve";

export default defineAgent({
  model: "anthropic/claude-sonnet-4.6",
});
Eve resolves the model string through AI Gateway, so you authenticate once with your gateway credential instead of managing provider keys. Any AI Gateway model works — swap in openai/gpt-5.4-mini or another provider/model if you prefer. Add a tool so the agent has something to call. Each file in agent/tools/ is one tool, and the runtime tool name comes from the filename — agent/tools/get_weather.ts:
import { defineTool } from "eve/tools";
import { z } from "zod";

export default defineTool({
  description: "Get the current weather for a city.",
  inputSchema: z.object({
    city: z.string().min(1),
  }),
  async execute({ city }) {
    return { city, condition: "Sunny", temperatureF: 72 };
  },
});

Step 2: Add Arize AX observability

Tracing in Eve is configured in a single file: agent/instrumentation.ts. Eve looks for that exact path and, if it exists, runs it once when the server starts, before any agent or tool code executes. That ordering is the whole reason the file exists separately: OpenTelemetry has to be registered before the AI SDK that Eve runs on is imported, otherwise its spans are never captured. Because Eve owns this startup hook, there’s no per-call telemetry flag to set — unlike the raw Vercel AI SDK where you pass experimental_telemetry on every call. The file’s presence is what turns tracing on. You write the file with defineInstrumentation from eve/instrumentation. Its setup callback runs at startup and receives the resolved agent name; inside it you register an OpenTelemetry provider. This guide uses @vercel/otel’s registerOTel to wire an OTLP exporter that ships Eve’s spans to Arize AX through an OpenInference span processor. Install the OpenInference processor and the OpenTelemetry packages:
npm install @arizeai/openinference-vercel \
  @vercel/otel \
  @opentelemetry/api \
  @opentelemetry/sdk-trace-base \
  @opentelemetry/exporter-trace-otlp-proto
Eve nests each turn’s spans under its own ai.eve.turn workflow span, which in turn hangs off Vercel Workflow spans that aren’t OpenInference spans. A plain isOpenInferenceSpan filter drops all of those, orphaning every span on the Traces tab. To get a clean trace tree, add a small span processor that keeps ai.eve.turn and promotes it to the trace root by clearing its parent ID. Create agent/root-aware-processor.ts:
import { Context } from "@opentelemetry/api";
import { Span, SpanExporter } from "@opentelemetry/sdk-trace-base";
import {
  OpenInferenceSimpleSpanProcessor,
  isOpenInferenceSpan,
} from "@arizeai/openinference-vercel";

// Eve wraps each turn's OpenInference spans under its own `ai.eve.turn`
// workflow span, which in turn hangs off Vercel Workflow spans that are
// filtered. Keep `ai.eve.turn` and promote it to the trace root so the
// gen_ai spans have a single, un-orphaned parent. Eve emits one trace per
// turn, so there is exactly one `ai.eve.turn` to promote per trace.
const EVE_ROOT_SPAN_NAME = "ai.eve.turn";

function isEveSpan(span: Span): boolean {
  return isOpenInferenceSpan(span) || span.name === EVE_ROOT_SPAN_NAME;
}

export class RootAwareOpenInferenceProcessor
  extends OpenInferenceSimpleSpanProcessor {
  constructor(exporter: SpanExporter) {
    super({ exporter, spanFilter: isEveSpan });
  }

  onStart(span: Span, parentContext: Context): void {
    if (span.name === EVE_ROOT_SPAN_NAME) {
      (span as unknown as { parentSpanId?: string }).parentSpanId = undefined;
      (span as unknown as { parentSpanContext?: unknown })
        .parentSpanContext = undefined;
      span.setAttribute("openinference.span.kind", "AGENT");
    }
    super.onStart(span, parentContext);
  }
}
Now create agent/instrumentation.ts — the file Eve auto-discovers and runs at startup. Inside the setup callback, registerOTel builds the OpenTelemetry provider: it sets the model_id resource attribute (Arize routes spans to a project by it — without it the OTLP endpoint rejects spans), points the OTLP exporter at Arize with your space and API key, and registers the RootAwareOpenInferenceProcessor you just wrote as the span processor:
import { defineInstrumentation } from "eve/instrumentation";
import { registerOTel } from "@vercel/otel";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
import { RootAwareOpenInferenceProcessor } from "./root-aware-processor";

export default defineInstrumentation({
  setup: ({ agentName }) =>
    registerOTel({
      serviceName: agentName,
      // Arize routes spans to a project by the `model_id` resource attribute —
      // without it the OTLP endpoint rejects spans with an InvalidArgument error.
      attributes: { model_id: agentName },
      spanProcessors: [
        new RootAwareOpenInferenceProcessor(
          new OTLPTraceExporter({
            url: "https://otlp.arize.com/v1/traces",
            headers: {
              "arize-space-id": process.env.ARIZE_SPACE_ID ?? "",
              "arize-api-key": process.env.ARIZE_API_KEY ?? "",
            },
          }),
        ),
      ],
    }),
});
RootAwareOpenInferenceProcessor extends OpenInferenceSimpleSpanProcessor, which exports each span as it ends — so there’s no forceFlush to call on exit, even on the short-lived serverless functions Eve runs on.

Step 3: Run a session

Set your credentials and start the dev server:
export ARIZE_SPACE_ID="<your-space-id>"
export ARIZE_API_KEY="<your-api-key>"
export AI_GATEWAY_API_KEY="<your-ai-gateway-key>"

npm run dev
The dev server listens on http://127.0.0.1:2000 by default (pass --port to change it). Open a session against the built-in HTTP channel. The agent should call your get_weather tool and answer (for example, “The weather in Brooklyn is sunny and 72°F.”):
curl -X POST http://127.0.0.1:2000/eve/v1/session \
  -H 'content-type: application/json' \
  -d '{"message":"What is the weather in Brooklyn?"}'
The POST is asynchronous — it returns 202 with the session id (sessionId in the body, also the x-eve-session-id response header) and a continuationToken, but not the model’s reply; Eve runs the turn in the background. Stream the session to watch it run and see the answer:
curl -N http://127.0.0.1:2000/eve/v1/session/<sessionId>/stream
This emits NDJSON lifecycle events — session.started, the get_weather actions.requested and action.result, streaming message.appended deltas, and finally a message.completed event with the full reply:
{"type":"message.completed","data":{"finishReason":"stop","message":"The current weather in Brooklyn is sunny and 72°F. It's a nice sunny day!"}}
Send a follow-up on the same session to produce a second turn (stream it the same way to see its reply):
curl -X POST http://127.0.0.1:2000/eve/v1/session/<sessionId> \
  -H 'content-type: application/json' \
  -d '{"continuationToken":"<token>","message":"Now do Queens."}'

Step 4: Explore the trace in Arize AX

Open your Arize AX space and select the project named after your agent (weather-agent). Eve runs on the AI SDK’s GenAI-convention spans, so every span arrives named gen_ai (or gen_ai.client for the model request) rather than ai.streamText/ai.toolCall — the OpenInference processor classifies them by span kind (agent, llm, tool), which is what you read in the UI. Each turn is one trace, parented by Eve’s ai.eve.turn workflow span — which the RootAwareOpenInferenceProcessor tags as an agent span so the per-turn root reads cleanly in the UI; per step, an agent span wraps an llm span, which parents the gen_ai.client model request and any gen_ai tool spans:
ai.eve.turn  [agent]                         turn
  ├─ gen_ai  [agent]                         step 0
  │    └─ gen_ai  [llm]
  │         ├─ gen_ai.client  [llm]          model request (prompt, response, tokens)
  │         └─ gen_ai  [tool]  tool.name=get_weather
  └─ gen_ai  [agent]                         step 1
       └─ gen_ai  [llm]
            └─ gen_ai.client  [llm]          final text
Arize AX trace view of a Vercel Eve agent turn, showing the ai.eve.turn agent root span with nested agent, llm, and tool spans for each step
Because you wired in the RootAwareOpenInferenceProcessor in Step 2, ai.eve.turn is promoted to the trace root — so each turn is a single clean tree with no orphaned spans. The integration guide’s Span filter section explains the processor and the simpler filter-only alternative.
From the trace you can inspect:
  • The llm span (gen_ai.client) with the prompt, the model’s response, and input/output token counts.
  • Each tool span (gen_ai, carrying tool.name) with its arguments and returned result.
  • Session grouping — Eve attaches ai.settings.context.eve.session.id, ai.settings.context.eve.turn.id, ai.settings.context.eve.step.index, and ai.settings.context.eve.channel.kind to the spans, so the two turns from your follow-up message group under the same session.

Next steps