Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/vercel/eve/llms.txt

Use this file to discover all available pages before exploring further.

When eve doesn’t ship a channel for your surface, you build one. Custom channels expose HTTP or WebSocket endpoints, parse incoming requests, start or resume sessions, observe runtime events, and deliver responses back to your platform.

File location and identity

Custom channels live in agent/channels/ at the root agent. The file stem becomes the channel id, so agent/channels/internal-webhook.ts is addressed as internal-webhook. Export the channel definition as the module’s default export.

Defining a channel

Import defineChannel and the route verb helpers from eve/channels:
agent/channels/my-channel.ts
import { defineChannel, GET, POST } from "eve/channels";

export default defineChannel({
  routes: [
    POST("/message", async (req, { send }) => {
      const body = await req.json();
      const session = await send(body.message, {
        auth: null,
        continuationToken: body.token,
      });

      return Response.json({ sessionId: session.id });
    }),

    GET("/sessions/:sessionId/stream", async (_req, { getSession, params }) => {
      const session = getSession(params.sessionId);
      const stream = await session.getEventStream();

      return new Response(stream, {
        headers: { "content-type": "application/x-ndjson; charset=utf-8" },
      });
    }),
  ],

  events: {
    "message.completed"(event, channel, ctx) {
      // deliver completed messages back to the surface that owns this channel
    },
  },
});

Route verbs

The following route verb helpers are available from eve/channels:
VerbHelperUse case
GETGET()Streaming endpoints, health checks
POSTPOST()Webhook receivers, message ingestion
PUTPUT()Full-replace update webhooks
PATCHPATCH()Partial-update webhooks
DELETEDELETE()Deletion notifications
WSWS()Real-time bidirectional connections
Each route handler receives the raw Request and a helpers object:
HelperDescription
send()Starts or resumes a session. Returns a Session.
getSession()Looks up an existing session by id. The returned Session exposes getEventStream().
receive()Hands inbound work to a different channel (cross-channel hand-off).
paramsRoute parameters extracted from the path pattern.
waitUntil()Extends the request lifetime for background work.
requestIpThe client IP address, or null when the host cannot provide it.

The events map

Event handlers are declared under the events key and receive (eventData, channel, ctx):
  • eventData — the event payload
  • channel — platform handles and session continuation operations
  • ctx — the eve SessionContext
The one exception is session.failed, which receives only (eventData, channel) with no ctx.

WebSocket routes

Use WS() when a custom channel needs a WebSocket endpoint. The route handler runs once per upgrade request and returns lifecycle hooks for that connection:
agent/channels/voice.ts
import { defineChannel, WS } from "eve/channels";

export default defineChannel({
  routes: [
    WS("/voice/ws", async (_req, { send }) => ({
      async message(_peer, message) {
        await send(message.text(), {
          auth: null,
          continuationToken: "voice-demo",
        });
      },
    })),
  ],
});
WS() handlers receive the same helpers as HTTP route handlers: send, getSession, receive, params, waitUntil, and requestIp. The returned hooks are compatible with Nitro/H3 websocket routing and include upgrade, open, message, close, and error.

Node upgrade server escape hatch

When a third-party SDK expects to bind directly to a Node http.Server via server.on("upgrade", ...), use createWebSocketUpgradeServer():
agent/channels/vendor.ts
import { defineChannel, WS, createWebSocketUpgradeServer } from "eve/channels";

const bridge = createWebSocketUpgradeServer();

thirdPartySdk.attach(bridge.server);

export default defineChannel({
  routes: [WS("/vendor/ws", bridge.route)],
});
The bridge server does not listen on its own port. It receives only upgrade events that matched the eve route. Treat it as a compatibility adapter for libraries with server-binding APIs, not the primary way to build WebSocket channels in eve.

Cross-channel hand-off

Route handlers can start a session on a different channel via args.receive(channel, ...). Use this when an inbound request on one channel should pivot the conversation onto another — for example, an incident webhook that opens an investigation thread in Slack:
agent/channels/incident-webhook.ts
import { defineChannel, POST } from "eve/channels";
import slack from "./slack.js";

export default defineChannel({
  routes: [
    POST("/incident", async (req, args) => {
      const incident = await req.json();

      args.waitUntil(
        args.receive(slack, {
          message: `Investigate ${incident.reference}: ${incident.title}`,
          target: { channelId: "C0123ABC" },
          auth: {
            authenticator: "incidentio",
            principalType: "service",
            principalId: incident.actor.id,
            attributes: {
              reference: incident.reference,
              severity: incident.severity,
            },
          },
        }),
      );

      return new Response("ok");
    }),
  ],
});
Key semantics:
  • The first argument to args.receive(...) is the target channel module’s default export — import it directly from agent/channels/<name>.ts.
  • auth flows through to session.auth.initiator so the target’s handlers and the agent’s tools can read who started the session.
  • Calling args.receive(...) does not also start a session on the current channel.

Channel metadata

A channel can project a subset of its adapter state as metadata, available to instrumentation resolvers, dynamic tool resolvers, and dynamic skill or instruction resolvers. Define a metadata(state) function on the channel config:
agent/channels/my-channel.ts
import { defineChannel, POST } from "eve/channels";

export default defineChannel({
  state: {
    topic: null as string | null,
    contextMessages: [] as string[],
    internalCounter: 0,
  },

  metadata(state) {
    return {
      topic: state.topic,
      contextMessages: state.contextMessages,
    };
  },

  routes: [
    POST("/start", async (req, { send }) => {
      const body = await req.json();
      await send(body.message, {
        auth: null,
        continuationToken: body.token,
        state: {
          topic: body.topic,
          contextMessages: body.context,
          internalCounter: 0,
        },
      });

      return new Response("ok");
    }),
  ],

  events: {
    "turn.started"(eventData, channel) {
      channel.state.internalCounter += 1;
    },
  },
});
The projection is re-evaluated whenever adapter state changes after channel event handlers run. Dynamic tool resolvers read it via ctx.channel.metadata and narrow it with isChannel. When a parent agent dispatches a subagent, the framework forwards the parent’s channel metadata projection to the child.

Continuation tokens

Each call to send(message, { auth, continuationToken }) addresses a session by its channel-local raw token. The framework prepends the channel name (derived from the file stem) before handing the token to the runtime. Built-in channels export helpers that construct the correct format:
import { slackContinuationToken } from "eve/channels/slack";
import { twilioContinuationToken } from "eve/channels/twilio";

slackContinuationToken("C0123ABC", "1800000000.001234");
// → "C0123ABC:1800000000.001234"

twilioContinuationToken("+15551234567", "+15557654321");
// → "+15551234567:+15557654321"
Custom channels write their own function that joins the relevant identity fields. The framework derives nothing for you — the channel owns its token format.

Late-bound continuation tokens

When the identity that should address a session is not known until later, re-key the parked session by calling session.setContinuationToken(...). Pass the channel-local raw token; the runtime preserves the current channel namespace:
agent/channels/my-channel.ts
import { defineChannel } from "eve/channels";
import { mintRef } from "./refs.js";

defineChannel<{ ref: string | null }>({
  state: { ref: null },

  context(state, session) {
    return {
      state,
      registerAnchor(ref: string) {
        state.ref = ref;
        session.setContinuationToken(ref);
      },
    };
  },

  events: {
    "message.completed"(eventData, channel) {
      if (!channel.state.ref) channel.registerAnchor(mintRef());
    },
  },

  routes: [
    /* ... */
  ],
});
After a successful re-key, inbound deliveries addressed to the old token are dropped. Coordinate with your senders to use the new token. If another active session already owns the new token, the re-keying session fails instead of taking it over.

File uploads

send() accepts string | UserContent. To include file attachments, pass a UserContent array mixing text and file parts:
agent/channels/my-channel.ts
await send(
  [
    { type: "text", text: body.message },
    { type: "file", data: imageBytes, mediaType: "image/png" },
  ],
  { auth, continuationToken },
);

Authenticated file URLs

For platforms like Slack where files sit behind authenticated URLs, put a URL object in FilePart.data and declare fetchFile on the channel config. The staging pipeline calls fetchFile with the URL serialized as a string (url.href):
agent/channels/my-channel.ts
import { defineChannel, POST } from "eve/channels";

export default defineChannel({
  fetchFile(url) {
    if (!url.startsWith("https://files.slack.com/")) return null;
    return fetch(url, { headers: { authorization: `Bearer ${token}` } })
      .then((r) => r.arrayBuffer())
      .then((b) => ({ bytes: Buffer.from(b) }));
  },

  routes: [
    POST("/webhook", async (req, { send }) => {
      await send(
        [
          { type: "text", text: message.text },
          ...message.attachments.map((a) => ({
            type: "file" as const,
            data: new URL(a.url),
            mediaType: a.mediaType,
          })),
        ],
        { auth, continuationToken, state },
      );
    }),
  ],
});
Return bytes to stage the file to the sandbox, or null to let the URL pass through to the model provider. The framework handles staging, enforcing upload policy, hydrating files for the model call, and reconstituting URL objects after queue serialization.

Auth helpers

The eve/channels/auth module provides helpers for the eve HTTP channel’s route auth policy:
agent/channels/eve.ts
import { eveChannel } from "eve/channels/eve";
import { localDev, vercelOidc, placeholderAuth } from "eve/channels/auth";

export default eveChannel({
  auth: [localDev(), vercelOidc(), placeholderAuth()],
});
HelperDescription
localDev()Accepts requests during local development
vercelOidc()Allows the local CLI and Vercel-issued deployment tokens from the same team
placeholderAuth()Returns a setup-focused 401 in production until replaced with real auth
localDev() and vercelOidc() are for trusted infrastructure only. For browser users or external clients, wire in your own auth (Clerk, Auth.js, OIDC/JWT, API-key verifier, or a custom AuthFn).

Channels Overview

Understand the channel contract, the eve HTTP channel default, and how to scaffold channel files.

Platform Channels

Ready-made adapters for Slack, Discord, Teams, Telegram, Twilio, and GitHub.

Build docs developers (and LLMs) love