UNPKG

eve

Version:

Filesystem-first framework for durable backend AI agents that run anywhere.

210 lines (154 loc) 11 kB
--- title: "Chat SDK" description: "Bridge any Vercel Chat SDK adapter — Slack, Discord, Telegram, WhatsApp, email, and more — to your agent through one channel, using your own credentials and state store." --- The Chat SDK channel connects your agent to any [Vercel Chat SDK](https://chat-sdk.dev) adapter. You pick an adapter (`@chat-adapter/slack`, `@resend/chat-sdk-adapter`, and so on), register handlers for the messages you care about, and call `send` to hand each turn to eve. Use it to reach a surface eve does not ship a first-class channel for, or when you want to manage credentials and state with the Chat SDK's own primitives rather than [Vercel Connect](../guides/auth-and-route-protection). See [Channels](./overview) for the contract this builds on. You supply an adapter and a state store: the adapter owns provider auth, webhook verification, and delivery, while eve owns session dispatch, streaming, typing, and human-in-the-loop. First-class channels such as [Slack](./slack) manage credentials through Vercel Connect insteadreach for whichever fits the surface you are targeting. ## Install Add eve, the Chat SDK core (`chat`), an adapter, and a state adapter. The example below uses the Resend email adapter with the in-memory state store: ```bash npm install eve@latest chat @resend/chat-sdk-adapter @chat-adapter/state-memory ``` Swap in whichever adapter matches your surface — `@chat-adapter/slack`, `@chat-adapter/discord`, `@chat-adapter/telegram`, and so on. Any Chat SDK adapter works. ## Add the channel `chatSdkChannel` returns `{ bot, channel, send }`. Register Chat SDK handlers on `bot`, call `send` from those handlers to start or resume an eve session, and export `channel` as the module default: ```ts title="agent/channels/resend.ts" import { createMemoryState } from "@chat-adapter/state-memory"; import { createResendAdapter } from "@resend/chat-sdk-adapter"; import type { Message, Thread } from "chat"; import { chatSdkChannel } from "eve/channels/chat-sdk"; export const { bot, channel, send } = chatSdkChannel({ userName: "Resend Bot", adapters: { resend: createResendAdapter({ fromAddress: "hello@example.com", fromName: "Resend Bot", }), }, state: createMemoryState(), streaming: false, }); bot.onNewMention(async (thread: Thread, message: Message) => { await thread.subscribe(); await send(message.text, { thread }); }); bot.onSubscribedMessage(async (thread: Thread, message: Message) => { await send(message.text, { thread }); }); export default channel; ``` `adapters` is a map of adapter name to adapter instance; each entry mounts its own webhook (see below). `state` takes any Chat SDK state adapter: `createMemoryState()` is fine for local development, but use a durable adapter (Redis, Upstash, etc.) in production so thread subscriptions and inbound deduplication survive restarts. `send` accepts a plain string, an AI SDK `UserContent` array, or a `SendPayload`, and must be called from inside a Chat SDK handler — it dispatches the turn on the webhook that is currently running. Deploy once the channel file is in place: ```bash eve deploy ``` `eve deploy` links the project if needed and deploys to Vercel production. ## Configure the webhook route Each adapter mounts one `POST` route at `/eve/v1/{adapterName}`, so the `resend` adapter above is served at `/eve/v1/resend`. Point your provider's webhook (the Resend inbound address, the Slack Event Subscriptions URL, etc.) at that path. Override the base path for every adapter with `route`, or pin an individual adapter's path with `routes`: ```ts export const { bot, channel, send } = chatSdkChannel({ userName: "Resend Bot", adapters: { resend: createResendAdapter({ fromAddress: "hello@example.com" }) }, state: createMemoryState(), route: "/webhooks", // resend now mounts at /webhooks/resend routes: { resend: "/webhooks/inbound-email" }, //or pin it exactly }); ``` Use `routes` when a provider requires a fixed URL or when you are migrating an existing endpoint without changing the provider's settings. ## How the channel handles messages ### Dispatch You choose which Chat SDK events start a turn by registering handlers on `bot` and calling `send`: - `bot.onNewMention(thread, message)` fires on a fresh `@mention` (or, for surfaces like email, a new inbound thread). Call `thread.subscribe()` when you want later replies in the same thread to keep reaching the agent. - `bot.onSubscribedMessage(thread, message)` fires on subsequent messages in a subscribed thread. - `bot.onAction`, `bot.onReaction`, and `bot.onSlashCommand` are available for adapters that emit them. `send(input, options)` starts or resumes the eve session. The `thread` you pass determines the continuation token and the persisted channel state, so replies land back on the originating thread: ```ts bot.onNewMention(async (thread, message) => { await send(message.text, { thread, title: "Support request" }); }); ``` `options` accepts `{ thread, auth?, title?, mode?, callback?, adapterName? }`. `title` sets the eve session's display title without changing the model message; `auth` attaches an authenticated principal to the turn. ### Delivery The default handlers post the agent's reply back to the thread — no `events` override required. Completed assistant messages are posted as markdown (`{ markdown: … }`) so adapters render rich text and email HTML rather than a raw string. Streaming is on by default: the channel posts an initial message and edits it as tokens arrive (`message.appended`), throttled by `streamingEditIntervalMs` (default `1000`). Set `streaming: false` for surfaces that deliver one message per turn — email, for example — so the reply posts once on completion instead of editing: ```ts chatSdkChannel({ userName: "Resend Bot", adapters: { resend: createResendAdapter({ fromAddress: "hello@example.com" }) }, state: createMemoryState(), streaming: false, }); ``` Typing indicators post automatically where the adapter supports them: `Working…` on `turn.started`, and tool status on `actions.requested`. ### Optional capabilities degrade gracefully Adapters do not all implement every operation. When an adapter's `startTyping` or `editMessage` throws a `NotImplementedError` (or an error with code `NOT_IMPLEMENTED`), the channel swallows it: typing indicators are skipped, and a streaming edit falls back to a single final post for the rest of the session. You never have to guard optional capabilities in your own handlers. The same predicate is exported as `isNotImplemented` if you want it in custom `events`: ```ts import { isNotImplemented } from "eve/channels/chat-sdk"; ``` Override any default by passing `events`. Handlers receive `(eventData, channel, ctx)`, with the rebuilt Chat SDK thread on `channel.thread`: ```ts chatSdkChannel({ userName: "Resend Bot", adapters: { resend: createResendAdapter({ fromAddress: "hello@example.com" }) }, state: createMemoryState(), events: { "message.completed"(eventData, channel) { if (eventData.finishReason === "tool-calls" || !eventData.message || !channel.thread) return; return channel.thread.post({ markdown: eventData.message }); }, }, }); ``` ### Human-in-the-loop (HITL) HITL prompts render as a Chat SDK `Card` with buttons. Button clicks resume the parked session automatically — the channel wires `bot.onAction` for you. Change the action-id prefix with `inputActionPrefix` (default `eve_input:`) if your app already uses it, and provide `resolveInputAuth` to carry user or tenant auth across the resume: ```ts chatSdkChannel({ userName: "Support Bot", adapters: { slack: createSlackAdapter() }, state: createMemoryState(), resolveInputAuth: (event) => ({ authenticator: "slack", principalType: "user", principalId: event.user?.userId ?? "unknown", attributes: {}, }), }); ``` ### Proactive sessions Start a session without an inbound webhook through `channel.receive({ message, target, auth })` — from a schedule `run` handler, or `args.receive(channel, ...)` from another channel. The target is a serialized Chat SDK thread, or `{ threadId, adapterName }` when you only have a provider-native thread id: ```ts await channel.receive({ message: "Your weekly digest is ready.", target: { adapterName: "resend", threadId: "resend:user@example.com" }, auth: null, }); ``` ### Attachments `send` takes plain text or an AI SDK `UserContent` array. To forward a Chat SDK message's attachments, convert it with `messageToUserContent`, which returns `message.text` when there are no attachments and a `UserContent` array (text plus one file part per attachment URL) when there are: ```ts import { messageToUserContent } from "eve/channels/chat-sdk"; bot.onNewMention(async (thread, message) => { await send(messageToUserContent(message), { thread }); }); ``` See [File uploads](./custom#file-uploads) for how eve stages remote file URLs before the model call. ## Configuration reference | Option | Default | Purpose | | ------------------------- | ------------ | ------------------------------------------------------------------------ | | `adapters` | — | Map of adapter name to Chat SDK adapter instance. One webhook per entry. | | `state` | — | Chat SDK state adapter for subscriptions, locks, and dedupe. | | `userName` | — | Display name for the bot (a standard Chat SDK `ChatConfig` field). | | `route` | `/eve/v1` | Base path for generated adapter webhooks (`{route}/{adapter}`). | | `routes` | — | Per-adapter path overrides for fixed or migrated webhook URLs. | | `streaming` | `true` | Post-then-edit streaming. Set `false` for one-message-per-turn surfaces. | | `streamingEditIntervalMs` | `1000` | Minimum interval between streaming edits. | | `events` | built-in | Per-event handlers. A supplied handler replaces that built-in default. | | `inputActionPrefix` | `eve_input:` | Prefix for default HITL button action ids. | | `resolveInputAuth` | `null` | Auth resolver applied when a HITL button click resumes a session. | | `webhook` | — | Extra Chat SDK webhook options (eve owns `waitUntil`). | Any other Chat SDK `ChatConfig` field (for example `concurrency`) is accepted and passed through. ## What to read next - [Channels overview](./overview): the channel contract and every built-in channel - [Custom channels](./custom): build a channel for any surface with `defineChannel` - [Auth & route protection](../guides/auth-and-route-protection): authenticating inbound traffic