@raindrop-ai/eve
v0.0.33
Published
Raindrop integration for Vercel's Eve agent framework (OTel traces to Raindrop + Workshop)
Maintainers
Keywords
Readme
@raindrop-ai/eve
Raindrop integration for Vercel's Eve agent framework.
Drops into agent/instrumentation/raindrop.ts (Eve >= 0.62) or
agent/instrumentation.ts (older Eve) and ships your Eve agent's OpenTelemetry
traces to:
- Raindrop (
https://api.raindrop.ai/v1/tracesby default), and - the local Workshop daemon (
http://localhost:5899) — when one is reachable.
Both destinations receive the same OTLP/HTTP JSON payload, so an agent that
works in production also works in raindrop workshop without changing any code.
Install
pnpm add @raindrop-ai/eve @vercel/otel @opentelemetry/api @opentelemetry/sdk-trace-baseeve is expected to already be in your agent project. @vercel/otel is a peer
dependency on both layouts (install it to silence the peer warning); only the
legacy layout calls its registerOTel. The integration is verified against Eve
0.84.x and keeps the legacy single-file contract (Eve 0.11–0.61) working — a
single defineRaindropInstrumentation() call is all you need on either.
How it works:
- Eve >= 0.62 discovers one instrumentation provider per file under
agent/instrumentation/and assembles the process-wide OpenTelemetry pipeline itself from the destinations those files export. When the installed Eve is >= 0.62 (read from itspackage.json), the object returned bydefineRaindropInstrumentation()is an EveotelIntegration()destination (Raindrop exporter + span processors +runtimeContextresolver) and a provider withsetup. Do not passregisterOTelor call it anywhere — Eve refuses to start when another runtime owns the global tracer provider. Keep theexport defaultun-annotated: the declared return type does not carry Eve's provider/integration symbol markers, so annotating it as Eve'sInstrumentationProviderfails to type-check even though the object is one. - Eve <= 0.61 reads the legacy
agent/instrumentation.tsdefault export (setup,events,recordInputs,recordOutputs,functionId). PassregisterOTelfrom@vercel/otelso Eve's own spans are exported. - Per-step runtime context is authored once via the
events["step.started"]option (returns{ runtimeContext }) and forwarded to Eve throughotelIntegration({ runtimeContext })on >= 0.62 orevents["step.started"]on older Eve; see Identifying users. - Cross-sandbox sub-agent linkage is sourced from the
session.parentlineage Eve exposes on that per-step input; sub-agents carry parent linkage with no extra wiring.
Usage
Eve >= 0.62:
// agent/instrumentation/raindrop.ts
import { otelIntegration } from "eve/instrumentation/otel";
import { defineRaindropInstrumentation } from "@raindrop-ai/eve";
export default defineRaindropInstrumentation({
otelIntegration,
writeKey: process.env.RAINDROP_WRITE_KEY,
});Passing otelIntegration explicitly is required for bundled deploys — eve
build output runs without node_modules, where the runtime fallback lookup
of eve/instrumentation/otel cannot resolve eve.
Process-wide OpenTelemetry settings (functionId, tracePolicy, sampler,
propagators) live in Eve's own otel() declaration — one per project, next to
the Raindrop file:
// agent/instrumentation/otel.ts (optional)
import { otel } from "eve/instrumentation/otel";
export default otel({ functionId: "support-agent" });Eve <= 0.61:
// 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 runs the file at server startup before any agent code. Upgrading
an agent from <= 0.61 to >= 0.62: move the file to
agent/instrumentation/raindrop.ts, drop registerOTel, and move functionId
into otel({ functionId }) if you set it.
Configuration
defineRaindropInstrumentation({
otelIntegration, // Eve >= 0.62, from eve/instrumentation/otel — required for `eve build` deploys
registerOTel, // Eve <= 0.61 only, from @vercel/otel
writeKey: process.env.RAINDROP_WRITE_KEY,
endpoint: "https://api.raindrop.ai/v1/", // default
projectId: "support-prod", // route to a named project; omit for default Production
localWorkshopUrl: undefined, // env + auto-detect by default
serviceName: "support-agent", // defaults to the Eve agent name
staticMetadata: { team: "support", tier: "prod" }, // static, every event
events: { // Eve per-step runtime context
"step.started"(input) {
return {
runtimeContext: {
"raindrop.userId": String(input.channel.metadata.triggeringUserId),
},
};
},
},
recordInputs: false, // default: metadata-only spans. true → ship model inputs (Eve >= 0.62: destination exportPolicy / <= 0.61: forwarded to Eve)
recordOutputs: false, // default: metadata-only spans. true → ship model outputs, likewise
maxTextFieldChars: 1_000_000, // per-attribute cap for exported span text
debug: false, // or RAINDROP_AI_DEBUG=1
});Projects
If your org has multiple projects, route telemetry to a specific one by passing
its slug as projectId (or set RAINDROP_PROJECT_ID):
defineRaindropInstrumentation({
writeKey: process.env.RAINDROP_WRITE_KEY,
projectId: "support-prod",
});This sets the X-Raindrop-Project-Id header on every trace export and AI SDK
event. Omit it (or pass "default") to use your org's default Production
project — the existing behavior. Single-project orgs need nothing new.
Payload size limits
Exported string span attributes (full prompts/completions recorded by the AI
SDK under recordInputs / recordOutputs) are capped at 1,000,000
characters per value by default and truncated with a
...[truncated by raindrop] marker (the result, marker included, never
exceeds the limit). The cap is enforced before the OTLP batch is serialized,
so multi-MB attributes cost the cap — not the payload — and land truncated
instead of being dropped at the ingest size limit. Tune it via
maxTextFieldChars; a stricter OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT env
var is also honored.
staticMetadata is the static authored lane — keys are merged onto every
shipped event. events is Eve's
InstrumentationEvents config: its
step.started(input) callback runs once per model call, returns
{ runtimeContext }, and that runtime context is bridged onto the event too
(see below). A flat metadata: Record<string,string> is still accepted and
treated the same as staticMetadata for convenience.
Identifying users
Static identity. Pass raindrop.userId (and optionally raindrop.convoId)
via staticMetadata and they are attached to every event from this Eve
session:
defineRaindropInstrumentation({
registerOTel,
writeKey: process.env.RAINDROP_WRITE_KEY,
staticMetadata: {
"raindrop.userId": "user_123",
"raindrop.convoId": "convo_456",
team: "support",
},
});Dynamic per-turn identity. Use the events
step.started
callback. Eve invokes it once per model call with the live
channel/session/turn input, so you can derive identity from the
triggering request — e.g. map the Slack user id onto the Raindrop event
userId. On Eve >= 0.84, input.turn is optional (a model call outside a
turn has none), input.step is gone, and input.run carries { id, attempt };
read input.turn?.id. A call without a turn ships the session id but no turn
id, and gets its own event instead of joining the previous turn's.
defineRaindropInstrumentation({
registerOTel,
writeKey: process.env.RAINDROP_WRITE_KEY,
staticMetadata: { app: "demo-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,
},
};
},
},
});The callback's output is both attached to the AI SDK telemetry spans (as
ai.settings.context.*) and routed onto the shipped Raindrop event:
- reserved
raindrop.*control keys set the event identity. A per-stepraindrop.userId(e.g. the Slack user id) wins over the Evesession.idfor the event'suserId;raindrop.convoIdsets theconvoId. - every other key becomes an entry under event
properties.
Precedence for any given key (highest first): per-call metadata on the AI SDK
call itself > the step.started callback > staticMetadata. So a per-step key
always overrides the static default of the same name. When the callback omits
raindrop.userId, the integration falls back to the Eve session.id, then to
opts.userId ?? agentName.
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 oninput.channel.metadata.triggeringUserId. - User's name — already resolved, no API call: read
input.session.auth.current?.attributesand usefull_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 theusers:read.emailscope;conversations.info→channel.name), stash them on the session authattributes, then read them back instep.started(which is synchronous and can't call Slack itself):
// 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). |
| undefined | 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 will start mirroring as soon as the daemon is reachable.
Production-only mode
defineRaindropInstrumentation({
registerOTel,
writeKey: process.env.RAINDROP_WRITE_KEY,
localWorkshopUrl: false,
});Workshop-only mode (no Raindrop account)
Leave writeKey undefined. Spans still flow to Workshop:
defineRaindropInstrumentation({
registerOTel,
localWorkshopUrl: "http://localhost:5899/v1/",
});Sub-agents
Eve runs each sub-agent in its own V8 sandbox. The integration reads the
session.parent lineage Eve exposes on the per-step input (the
otelIntegration({ runtimeContext }) input on Eve >= 0.62, the
events["step.started"] input before that) to detect when the current sandbox was dispatched as a sub-agent, and lifts the
parent's turn identity onto every sub-agent event:
| 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 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).
When Eve exposes no session.parent (a root session), events ship as root
events in the feed with raindrop.agent.role = "root".
What gets traced
Eve automatically creates rich trace hierarchies per turn. Eve >= 0.62 owns
the agent spans and emits OpenTelemetry GenAI semantic-convention attributes
(gen_ai.*):
invoke_agent <agent>
+-- agent.step
+-- chat <model> // model call
+-- execute_tool <tool> // tool execOlder Eve relies on the AI SDK's spans:
eve.turn // session/turn metadata
+-- ai.streamText // step
| +-- ai.streamText.doStream // model call
| +-- ai.toolCall { toolName: search } // tool exec
+-- ai.streamTextBoth flow through Raindrop's standard ingestion and show up in the Raindrop UI
and Workshop with full nesting. On every Eve version the AI SDK telemetry
integration this package registers also ships each model / tool call as a
Raindrop event (track_partial).
Notes
- This package depends on
@raindrop-ai/corefor OTLP serialization and the Workshop mirroring helper. - The exporter implements the
SpanExporterinterface structurally, so it works against both@opentelemetry/sdk-trace-base1.x and 2.x. - On Eve >= 0.62 the session and turn identifiers Eve no longer forwards
natively travel as
raindrop.eve.sessionId/raindrop.eve.turnIdruntime context. They are consumed by this package (per-turnraindrop.eventId, session-scopedraindrop.userId/raindrop.convoId) and never shipped as event metadata. - Span content defaults are unchanged by the upgrade: with
recordInputs/recordOutputsomitted, spans shipped to Raindrop stay metadata-only on both layouts (Eve <= 0.61 read an omitted flag asfalse; on >= 0.62 this package declares a redactingexportPolicyfor its destination). Set a flag totrueto ship that direction; other destinations still follow Eve's owntracePolicy. - On Eve >= 0.62 the
functionIdoption only affects this package's own sub-agent detection; Eve reads the spanfunctionIdfromotel({ functionId }). - Eve requires Node >= 24 (Eve's own
enginessince well before 0.62); whenotelIntegrationis not passed, this package falls back to loadingeve/package.jsonandeve/instrumentation/otelthroughrequire(), which Node 24 supports for ES modules. The fallback needseveresolvable fromnode_modules, so bundledeve builddeploys must pass the option.
Examples
examples/eve-basic— single-agent example with one tool (get_weather) and an end-to-end dashboard verifier.examples/eve-subagents— parent agent dispatchingweatherResearcher+attractionsFindersub-agents, verifying cross-sandbox nesting against the production dashboard.
Application Git identity
Set appGit on defineRaindropInstrumentation (or RaindropEveSpanExporter) to override commitSha, commitDirty, and optional ordinary branch, or pass false to opt out. The revision is unknown by default because ambient deploy/CI and observer working-directory metadata are ignored. Local discovery requires an explicit sourceDirectory (or RAINDROP_GIT_SOURCE_DIRECTORY); explicit RAINDROP_COMMIT_SHA values are honored. Automatic branch discovery requires detectBranch: true. Eve's event feed and independent OTLP exporter share one application Git context.
For advanced direct exporter wiring, install the start processor to freeze automatic metadata when each span begins. On Eve >= 0.62 declare it as a destination in agent/instrumentation/raindrop.ts:
import { otelIntegration } from "eve/instrumentation/otel";
import { RaindropEveSpanExporter, createEveAppGitSpanProcessor } from "@raindrop-ai/eve";
const exporter = new RaindropEveSpanExporter({
writeKey: process.env.RAINDROP_WRITE_KEY,
appGit: { sourceDirectory: "/path/to/application" },
});
export default otelIntegration({
traceExporter: exporter,
spanProcessors: [createEveAppGitSpanProcessor(exporter)],
});On older Eve, keep "auto" to preserve Vercel's default processors; traceExporter retains its normal batch exporter:
import { registerOTel } from "@vercel/otel";
import { RaindropEveSpanExporter, createEveAppGitSpanProcessor } from "@raindrop-ai/eve";
const exporter = new RaindropEveSpanExporter({
writeKey: process.env.RAINDROP_WRITE_KEY,
appGit: { sourceDirectory: "/path/to/application" },
});
registerOTel({
traceExporter: exporter,
spanProcessors: ["auto", createEveAppGitSpanProcessor(exporter)],
});Without this processor, direct exporters attach only explicit client, environment, operation, or span values. Automatic discovery never fills in metadata for a span whose start was not observed. defineRaindropInstrumentation installs the processor automatically.
