@raindrop-ai/eve
v0.0.19
Published
Raindrop integration for Vercel's Eve agent framework (OTel traces to Raindrop + Workshop)
Keywords
Readme
@raindrop-ai/eve
Raindrop integration for Vercel's Eve agent framework.
Drops into agent/instrumentation.ts 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. The integration tracks
Eve's fast-moving releases and is verified against the current latest
(0.11.x), hooking Eve's defineInstrumentation surface from
eve/instrumentation — so a single defineRaindropInstrumentation() call is all
you need.
How it works:
- Eve enables telemetry by the mere presence of an
agent/instrumentation.tsdefault export. The object returned bydefineRaindropInstrumentation()is that export: Eve reads itssetup,events,recordInputs,recordOutputs, andfunctionIdfields and ignores the rest. - Per-step runtime context is resolved through Eve's
events["step.started"]hook (returns{ runtimeContext }); see Identifying users. - Cross-sandbox sub-agent linkage is sourced from the
step.startedsession.parentlineage Eve exposes; sub-agents carry parent linkage with no extra wiring.
Usage
// 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. The framework auto-discovers agent/instrumentation.ts and runs it
at server startup before any agent code. Pass registerOTel from @vercel/otel
so Eve's own OTel spans (workflow/step/fetch) are exported — without it the
integration still wires up Raindrop's AI SDK telemetry but skips the underlying
OTel registration.
Configuration
defineRaindropInstrumentation({
registerOTel, // 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: true, // forwarded to Eve
recordOutputs: true, // forwarded to Eve
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({
registerOTel,
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/step input, so you can derive identity from the
triggering request — e.g. map the Slack user id onto the Raindrop event
userId:
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 events["step.started"] input 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.turn // session/turn metadata
+-- ai.streamText // step
| +-- ai.streamText.doStream // model call
| +-- ai.toolCall { toolName: search } // tool exec
+-- ai.streamTextThese flow through Raindrop's standard ingestion and show up in both the Raindrop UI and Workshop with full nesting + AI SDK attributes.
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.
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.
