> ## Documentation Index
> Fetch the complete documentation index at: https://arize-ax.mintlify.site/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Tracing a Vercel Eve Agent

> Scaffold a Vercel Eve agent, add Arize AX observability through OpenTelemetry, and explore the turn, model, and tool spans it produces.

[Eve](https://eve.dev/) 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](https://github.com/vercel/ai) OpenTelemetry spans, and Arize AX ingests them through the [`@arizeai/openinference-vercel`](https://github.com/Arize-ai/openinference/tree/main/js/packages/openinference-vercel) span processor — the same processor used by the [Vercel AI SDK integration](/ax/integrations/ts-js-agent-frameworks/vercel/vercel-ai-sdk-tracing).

## Prerequisites

* Node.js 24+ (Eve's CLI requires it)
* An Arize AX account ([sign up](https://arize.com/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:

```bash theme={null}
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`:

```md theme={null}
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`):

```typescript theme={null}
import { defineAgent } from "eve";

export default defineAgent({
  model: "anthropic/claude-sonnet-4.6",
});
```

Eve resolves the model string through [AI Gateway](https://vercel.com/docs/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`:

```typescript theme={null}
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:

```bash theme={null}
npm install @arizeai/openinference-vercel@^3 \
  @vercel/otel \
  @opentelemetry/api \
  @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. Filtering with `isOpenInferenceSpan` drops those non-AI spans — which would orphan every surviving span on the **Traces** tab, since their parent is gone. Set `reparentOrphanedSpans: true` on the processor to fix this: it re-roots any AI span whose dropped parent was a non-AI span, and recognizes Eve's `ai.eve.turn` wrapper as an AI-like root, tagging it `openinference.span.kind = AGENT` so it survives the filter as a clean per-turn **agent** root.

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 OpenInference span processor with `spanFilter` and `reparentOrphanedSpans` set:

```typescript theme={null}
import { defineInstrumentation } from "eve/instrumentation";
import { registerOTel } from "@vercel/otel";
import {
  isOpenInferenceSpan,
  OpenInferenceSimpleSpanProcessor,
} from "@arizeai/openinference-vercel";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";

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 OpenInferenceSimpleSpanProcessor({
          exporter: 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 ?? "",
            },
          }),
          spanFilter: isOpenInferenceSpan,
          reparentOrphanedSpans: true,
        }),
      ],
    }),
});
```

<Note>
  `OpenInferenceSimpleSpanProcessor` 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. `reparentOrphanedSpans` runs statelessly at span start, so it adds no buffering.
</Note>

## Step 3: Run a session

Set your credentials and start the dev server:

```bash theme={null}
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."*):

```bash theme={null}
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:

```bash theme={null}
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:

```text wrap theme={null}
{"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):

```bash theme={null}
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 `reparentOrphanedSpans` re-roots and 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:

```text theme={null}
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
```

<Frame caption="The full Vercel Eve turn trace in Arize AX, with the ai.eve.turn agent span as the root over the per-step agent, llm, and tool spans">
  ![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](https://storage.googleapis.com/arize-phoenix-assets/assets/images/arize-docs-images/cookbooks/vercel-eve-trace.png)
</Frame>

<Note>
  Because you set `reparentOrphanedSpans: true` in Step 2, `ai.eve.turn` is re-rooted and tagged as an agent span — so each turn is a single clean tree with no orphaned spans. The [integration guide's Span filter section](/ax/integrations/ts-js-agent-frameworks/vercel/eve-tracing#span-filter) explains the option in more detail.
</Note>

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

* Add more tools or a [skill](https://vercel.com/docs/eve/concepts#tools-and-skills) and watch the new `gen_ai` **tool** spans appear per turn.
* Set up an [LLM-as-a-judge evaluator](/ax/evaluate/create-evaluators) over the captured spans to score the agent's responses.
* For the underlying processor and version-pinning details, see the [Vercel Eve integration guide](/ax/integrations/ts-js-agent-frameworks/vercel/eve-tracing).
