# Channels, Ingress & Conversation Handles

> Eve channels as authored agent/channels/ routes that own continuationToken and auth policy, versus Flue's first-party channel packages (@flue/slack, etc.), blueprints from flue add, and dispatch-based agent admission over a Hono HTTP surface.

- Repository: vercel/eve-with-withastro-flue
- GitHub: https://github.com/vercel/eve
- Human wiki: https://www.grok-wiki.com/public/wiki/vercel-eve-with-withastro-flue-43b600348681
- Complete Markdown: https://www.grok-wiki.com/public/wiki/vercel-eve-with-withastro-flue-43b600348681/llms-full.txt

## Source Files

- `vercel-eve:docs/channels/overview.mdx`
- `vercel-eve:docs/channels/custom.mdx`
- `vercel-eve:apps/frameworks/nuxt/agent/channels/eve.ts`
- `withastro-flue:packages/slack/src/index.ts`
- `withastro-flue:blueprints/channel--slack.md`
- `withastro-flue:packages/runtime/src/runtime/dispatch.ts`
- `withastro-flue:.agents/skills/channel-conformance/SKILL.md`

---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [vercel-eve/docs/channels/overview.mdx](vercel-eve/docs/channels/overview.mdx)
- [vercel-eve/docs/channels/custom.mdx](vercel-eve/docs/channels/custom.mdx)
- [vercel-eve/docs/channels/eve.mdx](vercel-eve/docs/channels/eve.mdx)
- [vercel-eve/apps/frameworks/nuxt/agent/channels/eve.ts](vercel-eve/apps/frameworks/nuxt/agent/channels/eve.ts)
- [vercel-eve/packages/eve/src/public/definitions/defineChannel.ts](vercel-eve/packages/eve/src/public/definitions/defineChannel.ts)
- [vercel-eve/packages/eve/src/public/channels/eve.ts](vercel-eve/packages/eve/src/public/channels/eve.ts)
- [withastro-flue/packages/slack/src/index.ts](withastro-flue/packages/slack/src/index.ts)
- [withastro-flue/blueprints/channel--slack.md](withastro-flue/blueprints/channel--slack.md)
- [withastro-flue/packages/runtime/src/runtime/dispatch.ts](withastro-flue/packages/runtime/src/runtime/dispatch.ts)
- [withastro-flue/packages/runtime/src/runtime/flue-app.ts](withastro-flue/packages/runtime/src/runtime/flue-app.ts)
- [withastro-flue/.agents/skills/channel-conformance/SKILL.md](withastro-flue/.agents/skills/channel-conformance/SKILL.md)

</details>

# Channels, Ingress & Conversation Handles

Channels are the edge between an external platform and your agent runtime. Both Eve and Flue solve the same product problem — turn verified inbound traffic into continuing agent work and route replies back — but they split ownership differently. Eve treats channels as **authored agent modules** that call `send()` and own `continuationToken` semantics end to end. Flue treats channels as **first-party ingress packages** mounted on a Hono HTTP surface, with the application admitting work through `dispatch()` and owning outbound behavior.

Understanding where each framework stops (ingress verification vs. session orchestration vs. delivery) is the key to porting patterns between them or choosing an integration style.

## Mental model at a glance

| Concern | Eve (`vercel-eve`) | Flue (`withastro-flue`) |
| --- | --- | --- |
| Channel location | `agent/channels/<name>.ts` (file stem = channel id) | `channels/<name>.ts` (discovered at build time) |
| Channel export | Default export via `defineChannel()` or `eveChannel()` | Named `channel` binding from `createSlackChannel()` etc. |
| Ingress → agent bridge | `send(message, { auth, continuationToken })` inside route handlers | `dispatch(agent, { id, input })` from verified callbacks |
| Conversation handle | `continuationToken` (channel-local raw token, namespaced by file stem) | `id` on dispatch (often `channel.conversationKey(ref)`) |
| Auth policy | Channel route `auth` (e.g. `eveChannel({ auth: [...] })`) | Application-owned; channel verifies provider signatures only |
| Outbound delivery | Channel `events` handlers (e.g. `"message.completed"`) | Application `defineTool()` + provider SDK/Fetch client |
| HTTP mounting | Eve/Nitro routes declared per channel | Hono `flue()` mounts `/channels/:name/:suffix` |

Sources: [vercel-eve/docs/channels/overview.mdx:6-16](), [vercel-eve/docs/channels/custom.mdx:8-12](), [withastro-flue/.agents/skills/channel-conformance/SKILL.md:17-37](), [withastro-flue/packages/runtime/src/runtime/flue-app.ts:293-294]()

## Eve: authored channels that own the session contract

### Where channels live and how they are identified

Eve channels are files under `agent/channels/` on the **root agent only**. The filename stem becomes the channel id: `agent/channels/intake.ts` is addressed as `intake`. Each file default-exports a channel definition; local subagents do not declare channels.

Scaffolding is available through `eve channels add` (interactive or by kind), but channels can also be authored by hand.

Sources: [vercel-eve/docs/channels/overview.mdx:14-27](), [vercel-eve/packages/eve/src/public/definitions/defineChannel.ts:192-196]()

### The channel contract: normalize, resume, deliver

A channel is the edge adapter between a platform and the agent. It:

1. Normalizes platform input into a user message.
2. Owns the `continuationToken` — the resume handle for a conversation on that surface.
3. Decides delivery: how, where, and whether a response goes back.

Built-in platform channels (Slack, Discord, Teams, etc.) and custom channels share this contract. The default Eve HTTP channel (`eveChannel`) is enabled even when `agent/channels/eve.ts` does not exist; that file is typically added only to override auth or hooks.

Sources: [vercel-eve/docs/channels/overview.mdx:6-31]()

### `defineChannel`: routes, `send`, and event-driven delivery

Custom and built-in channels are built with `defineChannel` from `eve/channels`. A minimal custom channel declares HTTP routes and an `events` map:

```ts
// vercel-eve/docs/channels/custom.mdx (excerpt)
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 });
    }),
  ],
  events: {
    "message.completed"(event, channel, ctx) {
      // deliver completed messages back to the surface
    },
  },
});
```

Route handlers receive helpers including `send`, `getSession`, `receive` (cross-channel hand-off), `params`, `waitUntil`, and `requestIp`. Event handlers receive `(eventData, channel, ctx)` where `channel` carries platform handles plus `continuationToken` and `setContinuationToken`.

Sources: [vercel-eve/docs/channels/custom.mdx:16-56](), [vercel-eve/packages/eve/src/public/definitions/defineChannel.ts:44-86]()

### Continuation tokens: channel-owned, framework-namespaced

Each `send()` call addresses a session by a **channel-local raw token**. The framework prepends the channel name (from the file stem) before handing the token to the runtime — for example, a channel file `stateful.ts` sending with raw token `C1:T1` becomes `stateful:C1:T1` at runtime.

Custom channels define their own token format (Slack uses `channelId:threadTs`, Twilio uses caller/recipient pairs). When identity is not known until later, channels can re-key via `session.setContinuationToken(...)`.

Sources: [vercel-eve/docs/channels/custom.mdx:183-228](), [vercel-eve/packages/eve/src/channel/send.test.ts:167-174]()

### Auth policy on the Eve HTTP channel

The Eve channel exposes canonical session routes under `/eve/v1/session*`. Auth is configured per channel via an ordered `auth` array — the first `AuthFn` returning a `SessionAuthContext` wins; exhaustion rejects with 401.

A production demo might accept anonymous traffic explicitly:

```ts
// vercel-eve/apps/frameworks/nuxt/agent/channels/eve.ts
export default eveChannel({
  auth: [localDev(), none()],
});
```

`eveChannel` runs `routeAuth` on every route before dispatching. `onMessage` can further shape session auth and prepend context strings.

Sources: [vercel-eve/docs/channels/eve.mdx:6-16](), [vercel-eve/docs/channels/eve.mdx:48-57](), [vercel-eve/packages/eve/src/public/channels/eve.ts:84-90](), [vercel-eve/packages/eve/src/public/channels/eve.ts:125-133]()

### Cross-channel hand-off

Eve supports pivoting inbound work from one channel to another via `args.receive(targetChannel, { message, target, auth })`. The target channel's `receive` hook owns continuation-token format; the inbound channel does not also start a session on itself.

Sources: [vercel-eve/docs/channels/custom.mdx:99-136]()

## Flue: first-party ingress packages and dispatch-based admission

### Product boundary: what Flue owns vs. the application

Flue's channel conformance skill defines a strict boundary:

**Flue owns:** authenticated verified HTTP ingress; fixed discovered routes beneath `channels/<name>.ts`; provider-native typed payloads; canonical conversation identity where the provider supplies it; predictable Hono-compatible handler results.

**The application owns:** provider SDK/Fetch clients and credentials; `defineTool()` and authorization policy; installation/OAuth/token storage; webhook registration; deduplication and business persistence.

Conversation keys are identifiers, never authorization capabilities.

Sources: [withastro-flue/.agents/skills/channel-conformance/SKILL.md:17-37]()

### First-party channel packages (`@flue/slack`, etc.)

Provider channels ship as npm packages. `@flue/slack` exposes `createSlackChannel()` which:

- Verifies Slack request signatures over exact bytes (5-minute clock skew).
- Handles URL verification internally.
- Registers optional routes: `/events`, `/interactions`, `/commands` (omitted handlers omit routes).
- Provides `conversationKey(ref)` and `parseConversationKey(id)` for canonical thread identity.

```ts
// withastro-flue/packages/slack/src/index.ts (excerpt)
conversationKey(ref) {
  return `slack:v1:${encodeURIComponent(ref.teamId)}:...`;
}
```

Provider-native payloads pass through without field renaming or Flue-normalized event models. Filtering bots, subtypes, or event families is application policy.

Sources: [withastro-flue/packages/slack/src/index.ts:27-39](), [withastro-flue/packages/slack/src/index.ts:263-270](), [withastro-flue/packages/slack/src/index.ts:328-331](), [withastro-flue/.agents/skills/channel-conformance/SKILL.md:39-71]()

### Blueprints from `flue add`

Channel blueprints (e.g. `blueprints/channel--slack.md`) scaffold a complete integration slice: install `@flue/slack` and `@slack/web-api`, create `channels/slack.ts`, wire `dispatch()` in the events callback, and bind a reply tool using `channel.parseConversationKey(id)`.

Blueprints teach the intended developer experience — export `channel`, export a project-owned `client`, dispatch verified events to an agent, define narrow outbound tools — without prescribing a model provider.

Sources: [withastro-flue/blueprints/channel--slack.md:9-105](), [withastro-flue/blueprints/channel--slack.md:123-133]()

### Dispatch: agent admission over a continuing session

After ingress verification, the application admits work with `dispatch()`:

```ts
// withastro-flue/blueprints/channel--slack.md (excerpt)
await dispatch(assistant, {
  id: channel.conversationKey(thread),
  input: {
    type: 'slack.app_mention',
    eventId: payload.event_id,
    text: event.text,
  },
});
```

`dispatch()` validates the target agent exists, requires a non-empty `id` (agent instance id), requires JSON-serializable `input`, and enqueues admission. It resolves after queuing — not after model processing — returning a `dispatchId` (not a workflow `runId`).

The agent initializer receives `id` via `AgentCreateContext`, enabling per-thread tool binding:

```ts
export default createAgent(({ id }) => ({
  tools: [replyInThread(channel.parseConversationKey(id))],
}));
```

Sources: [withastro-flue/packages/runtime/src/runtime/dispatch.ts:12-47](), [withastro-flue/packages/runtime/src/runtime/flue-app.ts:163-180](), [withastro-flue/packages/runtime/src/types.ts:30-52](), [withastro-flue/packages/runtime/src/types.ts:61-67]()

### Hono HTTP surface and route discovery

At build time, Flue discovers `channels/*.ts` modules and normalizes each channel's `routes` array. At runtime, `flue()` mounts a Hono app with:

- `POST /agents/:name/:id` — direct agent HTTP prompts
- `ALL /channels/:name/:suffix` — channel ingress (e.g. `/channels/slack/events`)

The channel route handler matches `METHOD + path suffix` against the discovered route table. A channel must export a named `channel` binding with at least one route.

Sources: [withastro-flue/packages/cli/src/lib/build.ts:283-316](), [withastro-flue/packages/cli/src/lib/generated-entry-normalization.ts:59-76](), [withastro-flue/packages/runtime/src/runtime/flue-app.ts:283-294](), [withastro-flue/packages/runtime/src/runtime/flue-app.ts:594-628]()

## Architecture comparison

```mermaid
flowchart TB
  subgraph Eve["Eve — agent/channels/&lt;name&gt;.ts"]
    PlatformE[External platform]
    RouteE[Channel routes GET/POST/WS]
    SendE["send(message, { auth, continuationToken })"]
    RuntimeE[Eve session runtime]
    EventsE["events: message.completed → deliver"]
    PlatformE --> RouteE --> SendE --> RuntimeE --> EventsE --> PlatformE
  end

  subgraph Flue["Flue — channels/&lt;name&gt;.ts + @flue/*"]
    PlatformF[External platform]
    HonoF["flue() Hono /channels/:name/:suffix"]
    VerifyF["@flue/slack verify + forward payload"]
    DispatchF["dispatch(agent, { id, input })"]
    QueueF[Dispatch queue]
    AgentF[createAgent initializer by id]
    ToolsF["defineTool() + WebClient"]
    PlatformF --> HonoF --> VerifyF --> DispatchF --> QueueF --> AgentF
    AgentF --> ToolsF --> PlatformF
  end
```

## Conversation handles side by side

| Property | Eve `continuationToken` | Flue dispatch `id` |
| --- | --- | --- |
| Who defines format | Channel author (raw token); framework adds `<channelName>:` prefix | Channel package `conversationKey()` (e.g. `slack:v1:...`) |
| Where it is set | `send({ continuationToken })` or `setContinuationToken()` in events | Passed to `dispatch({ id })`; agent sees it as `context.id` |
| Session coupling | Directly parks/resumes Eve sessions | Selects agent instance within a continuing session |
| Auth coupling | Paired with `auth` on each `send()` | Identifier only; auth is separate application policy |
| Re-keying | `channel.setContinuationToken()` at event boundaries | Application manages id stability via `conversationKey` inputs |

Sources: [vercel-eve/docs/channels/custom.mdx:183-197](), [withastro-flue/packages/slack/src/index.ts:266-269](), [withastro-flue/packages/runtime/src/types.ts:31-33]()

## Ingress verification vs. route auth

Both frameworks verify inbound provider traffic, but auth policy placement differs:

**Eve** folds route auth into the channel definition. `eveChannel({ auth: [...] })` decides who may call `/eve/v1/session*`. Platform channels (Slack signatures, etc.) are implemented inside Eve's built-in channel adapters, and custom channels implement verification in route handlers.

**Flue** keeps verification inside `@flue/*` packages (signature checks, body limits, protocol handshakes) and explicitly leaves workspace allowlists, tenant authorization, and outbound credentials to application code. Short-lived provider capabilities (`trigger_id`, `response_url`) must not be copied into dispatch input or durable session data.

Sources: [vercel-eve/packages/eve/src/public/channels/eve.ts:84-90](), [withastro-flue/.agents/skills/channel-conformance/SKILL.md:55-71](), [withastro-flue/blueprints/channel--slack.md:119-121]()

## Portable patterns

Several ideas transfer cleanly across both models:

1. **Filename-derived channel namespace** — both use the channel file stem as the URL/logical namespace segment (Eve prepends it to tokens; Flue mounts `/channels/<stem>/...`).
2. **Provider-native payloads** — neither normalizes Slack/Discord/etc. into a universal event schema at the channel boundary; application code filters and interprets.
3. **Thread identity as a stable key** — Eve's `slackContinuationToken(channelId, threadTs)` and Flue's `conversationKey({ teamId, channelId, threadTs })` solve the same addressing problem with different string formats.
4. **Separation of ingress and outbound** — Flue makes this explicit (channel verifies in, tools send out). Eve combines them in one module but still separates route ingress from `events`-driven delivery.

What does **not** port directly: Eve's `send()` + `events` delivery loop has no Flue equivalent — Flue requires explicit `dispatch()` admission and application-owned reply tools. Eve's `receive()` cross-channel hand-off is a first-class framework feature; Flue would implement similar logic by dispatching to a different agent `id` or calling another channel callback from application code.

## Summary

Eve channels are **full session adapters** authored in `agent/channels/`: they normalize input, call `send()` with channel-local `continuationToken` values (namespaced by the framework), enforce route `auth`, and deliver responses through lifecycle `events`. Flue channels are **verified ingress libraries** (`@flue/slack`, etc.) wired in `channels/` and mounted on a Hono `/channels/*` surface; they forward provider-native payloads to application callbacks that admit work via `dispatch(agent, { id, input })`, using canonical `conversationKey` strings as agent instance ids. Eve centralizes conversation continuity in the channel module; Flue centralizes it in dispatch admission and agent initialization — with outbound behavior, credentials, and authorization always remaining application-owned.
