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

# Eve (Beta)

> Automatic observability for Vercel's Eve agent framework. Drop in one file and every turn, sub-agent dispatch, model call, and tool call shows up in Raindrop and Workshop.

The `@raindrop-ai/eve` package instruments [Vercel's Eve agent framework](https://beta.eve.dev/docs) using its native `agent/instrumentation.ts` entry point. No wrapping, no per-call setup — Eve auto-discovers the integration at server startup and every agent turn flows to Raindrop.

**What gets tracked:**

* Every agent turn as a single Raindrop event, grouped by the Eve session
* Multi-step `streamText` / `generateText` / `generateObject` calls with full attribute capture
* Tool calls (inputs, outputs, durations, errors)
* Token usage per step and per turn
* Sub-agent dispatches as nested `agent.subagent` spans — even across Eve's V8 sandbox boundary
* Cross-sandbox parent linkage (`raindrop.parent.eventId` / `sessionId` / `turnId` / `turnSequence`)
* Model name, finish reason, errors, and full AI SDK telemetry attributes
* Automatic mirroring to the local [Workshop](/docs/workshop/overview) daemon during development

## Installation

```bash theme={null}
pnpm add @raindrop-ai/eve @vercel/otel @opentelemetry/api @opentelemetry/sdk-trace-base
```

<Info>
  `eve` is expected to already be in your agent project. The integration declares `eve >= 0.11.0` as an optional peer dependency and reads only Eve's documented runtime surface, so a single `defineRaindropInstrumentation()` call works without any per-version configuration.

  * **Cross-sandbox sub-agent linkage** is sourced from the `events["step.started"]` `session.parent` lineage Eve forwards on every dispatch.
  * **Per-turn dynamic metadata** (the `step.started` callback — see [Identifying Users](#identifying-users)) is wired through Eve's `events` config and runs once per model call.
</Info>

## Quick Start

Drop a single file into your Eve project at `agent/instrumentation.ts`:

```typescript theme={null}
// agent/instrumentation.ts
import { registerOTel } from "@vercel/otel";
import { defineRaindropInstrumentation } from "@raindrop-ai/eve";

export default defineRaindropInstrumentation({
  registerOTel,
  writeKey: process.env.RAINDROP_WRITE_KEY,
});
```

That's it. Eve auto-discovers `agent/instrumentation.ts` and runs it at server startup before any agent code. Every turn from every session in your agent will land in Raindrop.

***

## How It Works

The package does two things:

1. **Implements a `SpanExporter`** (`RaindropEveSpanExporter`) that serializes OTel spans to OTLP/HTTP JSON and ships them to `https://api.raindrop.ai/v1/traces`. It also mirrors every export to a local Workshop daemon when one is reachable (`http://localhost:5899/`), so the same agent run shows up in `raindrop workshop` AND hosted Raindrop without changing code.

2. **Registers a Raindrop `TelemetryIntegration`** with Vercel AI SDK v7's global registry so every `streamText` / `generateText` / tool call in your agent becomes a proper Raindrop LLM / tool span — and a `track_partial` semantic event — instead of opaque AI SDK spans. This is what hydrates the [Workshop Overview](/docs/workshop/overview) tab and the hosted Raindrop event feed. All AI SDK calls within one Eve turn are grouped under a single `eventId` derived from Eve's active OTel trace, so a single agent turn shows up as a single Raindrop event in both UIs.

For users who'd rather wire `@vercel/otel` themselves, `RaindropEveSpanExporter` is exported separately for use with any `registerOTel({ traceExporter })` call.

***

## Sub-Agents

Eve runs each sub-agent in its own V8 sandbox. The integration detects when the current sandbox was dispatched as a sub-agent and lifts the parent's turn identity onto every sub-agent event, sourcing the lineage from the `events["step.started"]` `session.parent` Eve forwards on dispatch:

| Attribute                      | Description                                                                                                                                                                                                                                            |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `raindrop.agent.role`          | `"subagent"` when the current sandbox is a sub-agent dispatched by another agent; `"root"` otherwise.                                                                                                                                                  |
| `raindrop.subagent.name`       | The sub-agent's `agentName` (e.g. `weatherResearcher`).                                                                                                                                                                                                |
| `raindrop.parent.sessionId`    | The parent agent's (bare) Eve session id.                                                                                                                                                                                                              |
| `raindrop.parent.eventId`      | **The cross-sandbox link key.** The dispatching parent turn's per-turn `raindrop.eventId` (the `eve:<sessionId>:<turnId>` composite); the dashboard stitches a sub-agent under its parent by matching this against that turn's own `raindrop.eventId`. |
| `raindrop.parent.turnId`       | The parent's turn id that dispatched this sub-agent.                                                                                                                                                                                                   |
| `raindrop.parent.turnSequence` | The parent's turn sequence number.                                                                                                                                                                                                                     |

These attributes power the **AGENT block** in Workshop's Overview tab and let the Raindrop dashboard stitch sub-agent events under the parent turn that dispatched them (matching on `raindrop.parent.eventId`).

***

## Configuration

```typescript theme={null}
defineRaindropInstrumentation({
  registerOTel,
  writeKey: process.env.RAINDROP_WRITE_KEY,
  endpoint: "https://api.raindrop.ai/v1/",   // optional, default
  projectId: "support-prod",                   // optional, route events to a specific project (slug)
  localWorkshopUrl: undefined,                 // optional, see below
  serviceName: "support-agent",                // optional, defaults to the Eve agent name
  staticMetadata: { team: "support", tier: "prod" }, // optional, static, attached to every event
  events: {                                    // optional, per-step callback
    "step.started"(input) {
      return {
        runtimeContext: {
          "raindrop.userId": String(input.session.id),
        },
      };
    },
  },
  recordInputs: true,                          // optional, forwarded to Eve
  recordOutputs: true,                         // optional, forwarded to Eve
  debug: false,                                // optional, or set RAINDROP_AI_DEBUG=1
});
```

**Two ways to attach metadata:**

| Field                    | When it runs                           | Use it for                                                                                              |
| ------------------------ | -------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `staticMetadata`         | Set once at startup                    | Values that are the same on every event — `team`, `tier`, environment, or a fixed `raindrop.userId`.    |
| `events["step.started"]` | Eve callback, runs once per model call | Values that change per turn — e.g. the user who triggered *this* request. Returns `{ runtimeContext }`. |

Both feed the same event fields — the `events` callback just lets you compute them per turn.

### Projects

If your org has multiple [projects](/docs/platform/projects), route this agent's events to a specific one by passing its slug as `projectId` (or via `RAINDROP_PROJECT_ID`):

```typescript theme={null}
defineRaindropInstrumentation({
  registerOTel,
  writeKey: process.env.RAINDROP_WRITE_KEY,
  projectId: "support-prod",
});
```

This sets the `X-Raindrop-Project-Id` header on every cloud export. Omit it (or pass `"default"`) to use your org's default **Production** project, which is the existing behavior. Single-project orgs need nothing new.

<Warning>
  **Eve is one instrumentation per process by design — run one project per process.** `defineRaindropInstrumentation()` installs its OpenTelemetry exporter behind a process-global symbol gate (the first call wins) and registers its AI SDK telemetry integration on `globalThis` (the last call wins). Two `defineRaindropInstrumentation()` calls with different `writeKey`s or `projectId`s in the same process therefore split-brain — the exporter belongs to the first call, the AI SDK capture to the last — and spans land in the wrong project. To report several agents to different projects, run one instrumented Eve process per project rather than calling `defineRaindropInstrumentation()` more than once.
</Warning>

### Identifying Users

Raindrop sets each event's user and conversation from two reserved keys: `raindrop.userId` and `raindrop.convoId`. Put them on `staticMetadata` if they never change, or return them from the `events["step.started"]` callback to compute them per turn.

**Same user every time** — put the keys on `staticMetadata`:

```typescript theme={null}
defineRaindropInstrumentation({
  registerOTel,
  writeKey: process.env.RAINDROP_WRITE_KEY,
  staticMetadata: {
    "raindrop.userId": "user_123",
    "raindrop.convoId": "convo_456",
    team: "support",
  },
});
```

**Different user per turn** — return the keys from the [`events["step.started"]`](https://beta.eve.dev/docs/agent-config) callback. Eve runs it once per model call with the live `input`, so you can derive identity from the request that triggered the turn — e.g. map the Slack user onto the Raindrop event `userId`:

```typescript theme={null}
defineRaindropInstrumentation({
  registerOTel,
  writeKey: process.env.RAINDROP_WRITE_KEY,
  staticMetadata: { app: "clay-data-agent", team: "data" },
  events: {
    "step.started"(input) {
      const m = input.channel.metadata;
      return {
        runtimeContext: {
          // reserved keys -> Raindrop event identity
          "raindrop.userId": m.triggeringUserId, // becomes the event userId
          "raindrop.convoId": m.threadTs,        // becomes the event convoId
          // any other key -> Raindrop event properties
          "slack.user_id": m.triggeringUserId,
          "slack.channel_id": m.channelId,
          "slack.team_id": m.teamId,
          "slack.thread_ts": m.threadTs,
        },
      };
    },
  },
});
```

**How the keys map onto the event:**

* `raindrop.userId` → the event's `userId`; `raindrop.convoId` → the event's `convoId`.
* Every other key → the event's `properties`.

The same values are also attached to the AI SDK spans (as `ai.settings.context.*`).

<Note>
  When the same key is set in more than one place, the most specific value wins: per-call AI SDK metadata > the `events["step.started"]` callback > `staticMetadata`. If no `raindrop.userId` is set anywhere, Raindrop falls back to the Eve `session.id`.
</Note>

#### Using the Slack user's name / email / channel name

Whatever string you return as `raindrop.userId` becomes the event's `userId` — so to make Raindrop track e.g. the Slack email instead of the user id, just return that string. What's reachable from `step.started`:

* **User id** (`Uxxxx`) — directly on `input.channel.metadata.triggeringUserId`.
* **User's name** — already resolved, no API call: read `input.session.auth.current?.attributes` and use `full_name` / `user_name` (Eve's default Slack auth puts them there).
* **Email / channel name** — *not* in any Slack mention payload, so resolve them once on the async inbound side (`onAppMention`) via the Slack Web API (`users.info` → `user.profile.email`, needs the `users:read.email` scope; `conversations.info` → `channel.name`), stash them on the session auth `attributes`, then read them back in `step.started` (which is synchronous and can't call Slack itself):

```typescript theme={null}
// agent/channels/slack.ts — runs on the inbound webhook (async ok)
export default slackChannel({
  async onAppMention(ctx, message) {
    const auth = defaultSlackAuth(message, ctx);
    const who = await ctx.slack.request("users.info", { user: message.author.userId });
    return auth && {
      auth: { ...auth, attributes: { ...auth.attributes, email: who?.user?.profile?.email ?? "" } },
    };
  },
});

// instrumentation.ts — synchronous, just reads what you stashed
"step.started"(input) {
  const a = input.session.auth.current?.attributes ?? {};
  return { runtimeContext: { "raindrop.userId": String(a.email || a.user_id || "") } }; // userId will be the email
},
```

### Workshop / Production Mirroring

`localWorkshopUrl` controls Workshop mirroring:

| Value                 | Behavior                                                                                              |
| --------------------- | ----------------------------------------------------------------------------------------------------- |
| `string`              | Force-enable; mirror every export to that URL.                                                        |
| `false`               | Opt out entirely (skip env vars and auto-detect).                                                     |
| `null`                | Same as `false`.                                                                                      |
| `undefined` (default) | Honor `RAINDROP_WORKSHOP` / `RAINDROP_LOCAL_DEBUGGER`, then auto-detect localhost during development. |

Run `raindrop workshop` locally to get a daemon at `http://localhost:5899` — the integration starts mirroring as soon as the daemon is reachable.

### Production-Only Mode

```typescript theme={null}
defineRaindropInstrumentation({
  registerOTel,
  writeKey: process.env.RAINDROP_WRITE_KEY,
  localWorkshopUrl: false,
});
```

### Workshop-Only Mode (no Raindrop account)

Leave `writeKey` undefined. Spans still flow to Workshop:

```typescript theme={null}
defineRaindropInstrumentation({
  registerOTel,
  localWorkshopUrl: "http://localhost:5899/v1/",
});
```

***

## Captured Trace Hierarchy

A typical Eve turn produces this trace shape:

```
ai.eve.turn                                   // session / turn metadata
  ai.streamText                               // step
    ai.streamText.doStream                    // model call
    ai.toolCall  { toolName: get_weather }    // tool exec
  ai.streamText
```

When a sub-agent is dispatched, the sub-agent's sandbox opens its own OTel trace. The integration tags the sub-agent's events with `raindrop.parent.eventId` (plus `sessionId` / `turnId` / `turnSequence`) so the Raindrop dashboard renders them nested under the parent's turn — matching on `raindrop.parent.eventId` — even though they are on a different OTel trace.

***

## Captured Properties

The following properties land on every event:

| Property                       | Description                                                                                   |
| ------------------------------ | --------------------------------------------------------------------------------------------- |
| `eve.session.id`               | Eve session id.                                                                               |
| `eve.turn.id`                  | Eve turn id (one per assistant response).                                                     |
| `eve.turn.sequence`            | Monotonic turn counter within the session.                                                    |
| `raindrop.agent.role`          | `"root"` or `"subagent"`.                                                                     |
| `raindrop.subagent.name`       | Sub-agent `agentName` (sub-agent turns only).                                                 |
| `raindrop.parent.sessionId`    | Parent's (bare) session id (sub-agent turns only).                                            |
| `raindrop.parent.eventId`      | Parent turn's `raindrop.eventId` link key, `eve:<sessionId>:<turnId>` (sub-agent turns only). |
| `raindrop.parent.turnId`       | Parent's dispatching turn id (sub-agent turns only).                                          |
| `raindrop.parent.turnSequence` | Parent's dispatching turn sequence (sub-agent turns only).                                    |
| `ai.usage.prompt_tokens`       | Aggregate input token count for the turn.                                                     |
| `ai.usage.completion_tokens`   | Aggregate output token count for the turn.                                                    |

Any keys you set on `staticMetadata` (or return from the `events["step.started"]` callback) are also forwarded to `event.properties`, except the reserved `raindrop.*` identity keys.

***

## Flush & Shutdown

Eve's `BatchSpanProcessor` flushes spans automatically at process exit, and the integration registers a `shutdown()` hook that drains the internal Raindrop client buffer before the process exits. Most users don't need to call anything manually.

If you're embedding `RaindropEveSpanExporter` directly, call `await exporter.shutdown()` before exit.

***

## Known Limitations

* **Streaming**: The integration captures aggregate token usage and the final outputs of each step. Individual token-by-token deltas are not traced separately.
* **OTel provider access**: The integration calls `addSpanProcessor` on the `NodeTracerProvider` returned by `@vercel/otel`. Custom OTel setups that hide the underlying provider behind a non-standard proxy will gracefully skip the `ai.eve.turn` session-id enrichment path and fall back to the `events["step.started"]` `session` input only.
