Agent-readable docs
Flue & Eve Agent Frameworks Documentation
Source-grounded reference for two TypeScript agent harness frameworks: Flue (programmable harness, multi-target deploy) and Eve (filesystem-first durable agents on Vercel). Covers workspace setup, authoring models, HTTP protocols, CLI commands, and deployment paths.
Pages
- OverviewWhat Flue and Eve expose, how the workspace repos relate, runtime assumptions, and the shortest source-backed paths into each framework.
- InstallationPrerequisites, package managers, Node engine requirements, and first-install commands for Flue and Eve projects.
- Flue quickstartScaffold a Flue project, run `flue dev`, invoke a workflow with `flue run`, and connect to an agent instance with `flue connect`.
- Eve quickstartRun `eve init`, start `eve dev`, inspect discovery with `eve info`, and send the first session message to the stable HTTP API.
- Flue project layoutSource-root resolution (`.flue/` vs `src/`), agent and workflow modules, `app.ts` composition, and discovery boundaries.
- Eve project layoutAuthored slots under `agent/`, path-derived naming, workspace seeding rules, and subagent package boundaries.
- Runtime modelsCompare Flue sessions, workflow runs, and dispatch receipts with Eve sessions, turns, `continuationToken`, and `sessionId` contracts.
- Build Flue agentsAuthor agents with `createAgent`, `defineAgentProfile`, tools, skills, sandboxes, providers, and HTTP route handlers.
- Flue workflowsDefine workflow modules, initialize agents inside `run()`, invoke via CLI or HTTP, and inspect run events with `flue logs`.
- Flue channelsDiscover channel modules, mount verified webhook routes under `/channels/:name`, and dispatch inbound events to agents.
- Flue deployChoose Node or Cloudflare targets, register providers in `app.ts`, build artifacts with `flue build`, and apply blueprint-driven integrations.
- Eve authoring surfacesConfigure `agent.ts`, write instructions, tools, skills, connections, sandbox overrides, subagents, and schedules from the filesystem contract.
Complete Markdown
# Flue & Eve Agent Frameworks Documentation
> Source-grounded reference for two TypeScript agent harness frameworks: Flue (programmable harness, multi-target deploy) and Eve (filesystem-first durable agents on Vercel). Covers workspace setup, authoring models, HTTP protocols, CLI commands, and deployment paths.
## Context Links
- [Agent index](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/llms.txt)
- [Human interactive docs](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6)
- [GitHub repository](https://github.com/withastro/flue)
## Repository Metadata
- Repository: withastro/flue-with-vercel-eve
- Generated: 2026-06-18T19:31:09.220Z
- Updated: 2026-06-18T19:31:14.049Z
- Runtime: Grok CLI
- Format: Documentation
- Pages: 22
## Page Index
- 01. [Overview](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/01-overview.md) - What Flue and Eve expose, how the workspace repos relate, runtime assumptions, and the shortest source-backed paths into each framework.
- 02. [Installation](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/02-installation.md) - Prerequisites, package managers, Node engine requirements, and first-install commands for Flue and Eve projects.
- 03. [Flue quickstart](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/03-flue-quickstart.md) - Scaffold a Flue project, run `flue dev`, invoke a workflow with `flue run`, and connect to an agent instance with `flue connect`.
- 04. [Eve quickstart](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/04-eve-quickstart.md) - Run `eve init`, start `eve dev`, inspect discovery with `eve info`, and send the first session message to the stable HTTP API.
- 05. [Flue project layout](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/05-flue-project-layout.md) - Source-root resolution (`.flue/` vs `src/`), agent and workflow modules, `app.ts` composition, and discovery boundaries.
- 06. [Eve project layout](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/06-eve-project-layout.md) - Authored slots under `agent/`, path-derived naming, workspace seeding rules, and subagent package boundaries.
- 07. [Runtime models](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/07-runtime-models.md) - Compare Flue sessions, workflow runs, and dispatch receipts with Eve sessions, turns, `continuationToken`, and `sessionId` contracts.
- 08. [Build Flue agents](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/08-build-flue-agents.md) - Author agents with `createAgent`, `defineAgentProfile`, tools, skills, sandboxes, providers, and HTTP route handlers.
- 09. [Flue workflows](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/09-flue-workflows.md) - Define workflow modules, initialize agents inside `run()`, invoke via CLI or HTTP, and inspect run events with `flue logs`.
- 10. [Flue channels](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/10-flue-channels.md) - Discover channel modules, mount verified webhook routes under `/channels/:name`, and dispatch inbound events to agents.
- 11. [Flue deploy](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/11-flue-deploy.md) - Choose Node or Cloudflare targets, register providers in `app.ts`, build artifacts with `flue build`, and apply blueprint-driven integrations.
- 12. [Eve authoring surfaces](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/12-eve-authoring-surfaces.md) - Configure `agent.ts`, write instructions, tools, skills, connections, sandbox overrides, subagents, and schedules from the filesystem contract.
- 13. [Eve sessions and streaming](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/13-eve-sessions-and-streaming.md) - Start sessions, stream NDJSON events, send follow-ups with `continuationToken`, and handle HITL, subagent, and compaction events.
- 14. [Eve deployment](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/14-eve-deployment.md) - Build `.eve/` artifacts, configure Vercel env and sandbox backends, prewarm templates, deploy, and verify production routes.
- 15. [Flue CLI reference](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/15-flue-cli-reference.md) - Commands, flags, defaults, and exit behavior for `flue dev`, `run`, `connect`, `build`, `init`, `add`, `update`, `docs`, and `logs`.
- 16. [Flue configuration reference](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/16-flue-configuration-reference.md) - `flue.config.*` keys (`target`, `root`, `output`), source-root precedence, env loading, and CLI override rules.
- 17. [Flue HTTP API reference](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/17-flue-http-api-reference.md) - Mounted routes (`/agents`, `/workflows`, `/runs`, `/channels`, `/openapi.json`), admission semantics, stream reads, and error envelopes.
- 18. [Flue persistence reference](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/18-flue-persistence-reference.md) - `PersistenceAdapter` contract, store interfaces, built-in database adapters, and `db.ts` discovery for durable execution.
- 19. [Eve CLI reference](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/19-eve-cli-reference.md) - Commands and flags for `eve init`, `info`, `build`, `start`, `dev`, `link`, `deploy`, `eval`, and `channels` subcommands.
- 20. [Eve HTTP protocol reference](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/20-eve-http-protocol-reference.md) - Session routes, NDJSON event types, `continuationToken` follow-ups, stream reconnect rules, and client integration entry points.
- 21. [Flue examples](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/21-flue-examples.md) - Copy-pasteable fixtures for hello-world workflows, channel ingress, observability adapters, and target-specific integrations.
- 22. [Eve weather fixture](https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/22-eve-weather-fixture.md) - Walk through the `weather-agent` fixture: instructions, tool schema, skill procedure, and local dev smoke paths.
## Source File Index
- `vercel-eve:.nvmrc`
- `vercel-eve:apps/fixtures/weather-agent/agent/agent.ts`
- `vercel-eve:apps/fixtures/weather-agent/agent/instructions.md`
- `vercel-eve:apps/fixtures/weather-agent/agent/skills/get-weather.md`
- `vercel-eve:apps/fixtures/weather-agent/agent/tools/get_weather.ts`
- `vercel-eve:apps/fixtures/weather-agent/package.json`
- `vercel-eve:apps/fixtures/weather-agent/README.md`
- `vercel-eve:Dockerfile`
- `vercel-eve:docs/agent-config.md`
- `vercel-eve:docs/channels/overview.mdx`
- `vercel-eve:docs/concepts/default-harness.md`
- `vercel-eve:docs/concepts/execution-model-and-durability.md`
- `vercel-eve:docs/concepts/sessions-runs-and-streaming.md`
- `vercel-eve:docs/connections.mdx`
- `vercel-eve:docs/getting-started.mdx`
- `vercel-eve:docs/guides/auth-and-route-protection.md`
- `vercel-eve:docs/guides/deployment.md`
- `vercel-eve:docs/guides/frontend/overview.mdx`
- `vercel-eve:docs/guides/instrumentation.md`
- `vercel-eve:docs/guides/session-context.md`
- `vercel-eve:docs/introduction.md`
- `vercel-eve:docs/meta.json`
- `vercel-eve:docs/README.md`
- `vercel-eve:docs/reference/cli.md`
- `vercel-eve:docs/reference/project-layout.md`
- `vercel-eve:docs/reference/typescript-api.md`
- `vercel-eve:docs/sandbox.mdx`
- `vercel-eve:docs/skills.mdx`
- `vercel-eve:docs/subagents.mdx`
- `vercel-eve:docs/tools.mdx`
- `vercel-eve:package.json`
- `vercel-eve:packages/eve/package.json`
- `vercel-eve:packages/eve/src/public/index.ts`
- `vercel-eve:packages/eve/src/public/tools/index.ts`
- `vercel-eve:pnpm-workspace.yaml`
- `vercel-eve:README.md`
- `vercel-eve:scripts/dev.mjs`
- `vercel-eve:turbo.json`
- `withastro-flue:AGENTS.md`
- `withastro-flue:blueprints/channel.md`
- `withastro-flue:blueprints/database.md`
- `withastro-flue:CHANGELOG.md`
- `withastro-flue:examples/braintrust/README.md`
- `withastro-flue:examples/braintrust/src/workflows/task.ts`
- `withastro-flue:examples/cloudflare/src/workflows/skills-from-r2.ts`
- `withastro-flue:examples/github-channel/src/channels/github.ts`
- `withastro-flue:examples/hello-world/flue.config.ts`
- `withastro-flue:examples/hello-world/src/agents/session-test.ts`
- `withastro-flue:examples/hello-world/src/app.ts`
- `withastro-flue:examples/hello-world/src/workflows/hello.ts`
- `withastro-flue:examples/hello-world/src/workflows/with-subagent.ts`
- `withastro-flue:examples/imported-skill/README.md`
- `withastro-flue:examples/notion-channel/README.md`
- `withastro-flue:examples/react-chat/flue.config.ts`
- `withastro-flue:examples/slack-channel/src/channels/slack.ts`
- `withastro-flue:knip.json`
- `withastro-flue:package.json`
- `withastro-flue:packages/cli/bin/flue.ts`
- `withastro-flue:packages/cli/package.json`
- `withastro-flue:packages/cli/src/lib/build.ts`
- `withastro-flue:packages/cli/src/lib/config-paths.ts`
- `withastro-flue:packages/cli/src/lib/config.ts`
- `withastro-flue:packages/cli/src/lib/dev.ts`
- `withastro-flue:packages/cli/src/lib/env.ts`
- `withastro-flue:packages/cli/src/lib/source-root.ts`
- `withastro-flue:packages/github/src/index.ts`
- `withastro-flue:packages/libsql/README.md`
- `withastro-flue:packages/mysql/README.md`
- `withastro-flue:packages/postgres/README.md`
- `withastro-flue:packages/postgres/src/index.ts`
- `withastro-flue:packages/runtime/src/adapter.ts`
- `withastro-flue:packages/runtime/src/agent-definition.ts`
- `withastro-flue:packages/runtime/src/errors.ts`
- `withastro-flue:packages/runtime/src/index.ts`
- `withastro-flue:packages/runtime/src/runtime/flue-app.ts`
- `withastro-flue:packages/runtime/src/runtime/handle-agent.ts`
- `withastro-flue:packages/runtime/src/runtime/inspect.ts`
- `withastro-flue:packages/runtime/src/tool.ts`
- `withastro-flue:packages/runtime/test/workflow-runs.test.ts`
- `withastro-flue:packages/sdk/src/index.ts`
- `withastro-flue:packages/slack/src/index.ts`
- `withastro-flue:pnpm-workspace.yaml`
- `withastro-flue:README.md`
- `withastro-flue:turbo.jsonc`
---
## 01. Overview
> What Flue and Eve expose, how the workspace repos relate, runtime assumptions, and the shortest source-backed paths into each framework.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/01-overview.md
- Generated: 2026-06-18T19:24:31.383Z
### Source Files
- `withastro-flue:README.md`
- `withastro-flue:AGENTS.md`
- `withastro-flue:packages/runtime/src/index.ts`
- `vercel-eve:README.md`
- `vercel-eve:docs/README.md`
- `vercel-eve:docs/introduction.md`
- `vercel-eve:packages/eve/src/public/index.ts`
---
title: "Overview"
description: "What Flue and Eve expose, how the workspace repos relate, runtime assumptions, and the shortest source-backed paths into each framework."
---
Flue (`withastro/flue`) and Eve (`vercel/eve`) are independent TypeScript agent frameworks checked into this workspace. Flue compiles `agents/`, `workflows/`, and `channels/` modules into a Hono server with routes under `/agents`, `/workflows`, `/runs`, and `/channels`. Eve walks an `agent/` directory tree, compiles discovery output into `.eve/`, and serves a versioned HTTP protocol at `/eve/v1/session` with NDJSON streaming and `continuationToken` follow-ups. Neither repo depends on the other; choose one authoring model and runtime contract per project.
## Workspace layout
| Repository | Package | CLI binary | Primary artifact |
| ---------- | ------- | ---------- | ---------------- |
| `withastro-flue/` | `@flue/runtime`, `@flue/cli`, `@flue/sdk` | `flue` | Built server under `<root>/dist` |
| `vercel-eve/` | `eve` | `eve` | Compiled `.eve/` + host output `.output/` |
`withastro-flue/` is the primary repository in this workspace. `vercel-eve/` ships alongside it for cross-framework comparison and Eve-specific docs pages. Both are BYOC/BYOK: model providers, API keys, and external services are registered or configured in project code—not locked to a single vendor.
## Runtime assumptions
| Requirement | Flue | Eve |
| ----------- | ---- | --- |
| Node.js | `>=22` | `>=24` |
| Package manager | `pnpm >=11` (monorepo) | `pnpm` (monorepo) |
| TypeScript | Required for agent/workflow modules | Required; `agent.ts` and `tools/` are TypeScript |
| Default dev port | `3583` (`flue dev`) | Nitro dev server (fixture scripts use `eve dev`) |
Flue requires an explicit build target (`node` or `cloudflare`) in `flue.config.*` or via `--target`. Eve scaffolds a full app with `eve init` and runs local discovery through `eve dev` / `eve info` without a separate target flag.
## What Flue exposes
Flue is a programmable agent harness: TypeScript modules define agents, workflows, channels, sandboxes, and HTTP composition. The `@flue/runtime` barrel exports `createAgent`, `defineAgentProfile`, `defineTool`, `dispatch`, provider registration, and inspection helpers (`listAgents`, `listRuns`, `getRun`). Routing lives on `@flue/runtime/routing` as `flue()`, a mountable Hono sub-app.
### Authoring surfaces
Flue discovers modules from a resolved source root. Precedence is `<root>/.flue/` when present, else `<root>/src/`, else `<root>/`:
| Path (under source root) | Export contract | HTTP surface |
| ------------------------ | --------------- | ------------ |
| `agents/<name>.ts` | Default export: `createAgent(...)` | `POST/GET /agents/:name/:id` when module exports `route` |
| `workflows/<name>.ts` | Named export `run(ctx)` | `POST /workflows/:name` when module exports `route` |
| `channels/<name>.ts` | Channel module | `ALL /channels/:name` and `/channels/:name/*` |
| `app.ts` | Default export: Hono app mounting `flue()` | Custom routes alongside Flue API |
| `flue.config.ts` | `defineConfig({ target, root, output })` | Build-time only |
Build-time config (`target`, `root`, `output`) stays in `flue.config.*`. Runtime provider registration (`registerProvider`) belongs in `app.ts` because keys often come from `process.env`.
### Terminology
```
Agent profile → defineAgentProfile(...)
Created agent → createAgent(...)
Agent module → agents/<name>.ts
AgentInstance → URL <id>
Harness → init(agent) return value
Session → harness.session(name?)
Operation → session.prompt / skill / task / shell
Turn → one LLM round-trip
Workflow → workflows/<name>.ts; export run(...)
Workflow run → unique ctx.id (runId); /runs and flue logs inspect these only
```
Runs are workflow-only. Direct agent prompts and channel dispatches use persistent sessions and are not runs.
### CLI entry points
```bash
flue dev # long-running watch-mode server
flue run <workflow> # one-shot build + invoke
flue connect <agent> <instance-id> # interactive agent session
flue build # production artifact to dist/
flue init --target <node|cloudflare>
flue logs <runId> # tail/replay workflow run events
```
### HTTP API (via `flue()`)
| Method | Path | Role |
| ------ | ---- | ---- |
| `GET` | `/openapi.json` | OpenAPI spec |
| `POST` | `/agents/:name/:id` | Admit prompt (`?wait=result` for sync JSON) |
| `GET/HEAD` | `/agents/:name/:id` | Durable Streams event read |
| `POST` | `/workflows/:name` | Start workflow run (`?wait=result` optional) |
| `GET/HEAD` | `/runs/:runId` | Workflow run event stream |
| `ALL` | `/channels/:name` | Verified channel webhooks |
Persistence adapters (`PersistenceAdapter`, session stores) are authored against `@flue/runtime/adapter`, separate from the root barrel.
## What Eve exposes
Eve is a filesystem-first framework for durable backend agents. Authored files under `agent/` are the contract; path-derived names replace registries. The published package is `eve`; the CLI binary is `eve`.
### Authoring slots under `agent/`
| Path | Purpose |
| ---- | ------- |
| `agent.ts` | `defineAgent({ model, name, ... })` — additive runtime config |
| `instructions.md` / `instructions.ts` | Always-on system prompt (required on root agent) |
| `tools/` | `defineTool` modules; name from filename |
| `skills/` | On-demand procedures (Markdown or modules) |
| `connections/` | External MCP/OpenAPI connections |
| `channels/` | HTTP and messaging ingress (root-only) |
| `sandbox/` | Per-agent sandbox override and workspace seeds |
| `subagents/` | Specialist child agents under `subagents/<id>/` |
| `schedules/` | Recurring jobs (root-only) |
| `lib/` | Shared import-only code |
Identity comes from paths: `agent/tools/get_weather.ts` resolves to tool `get_weather` with no separate `name` field.
### Runtime split
Eve separates three layers:
- **Channel** — normalizes inbound transport, applies auth/delivery policy, owns `continuationToken`
- **Harness** — executes one unit of AI work, returns `{ session, next }`
- **Runtime** — persists state, follows `next`, streams NDJSON events, owns workflow orchestration
Public HTTP therefore exposes two handles:
- `continuationToken` — send the next user message to the same conversation
- `sessionId` — attach to the event stream and inspect a run
Durability is implemented on the open-source Workflow SDK; Eve checkpoints progress at step boundaries so tools, sandboxes, and subagents feel synchronous inside a durable session.
### CLI entry points
```bash
eve init [target] # scaffold new agent (or add to existing package.json)
eve dev # local runtime + terminal UI
eve info # discovery results and .eve/ artifacts
eve build # compile .eve/ and build host output
eve start # serve built .output/ app
eve link / eve deploy # Vercel project linking and production deploy
eve eval # run eval suites
eve channels add|list # scaffold or list channel integrations
```
Compiled artifacts land under `.eve/discovery/` and `.eve/compile/` (manifests, module map, diagnostics). `eve info` surfaces these paths for inspection.
### HTTP protocol (`/eve/v1`)
| Route | Role |
| ----- | ---- |
| `POST /eve/v1/session` | Create session; returns `sessionId` + `continuationToken` |
| `POST /eve/v1/session/:sessionId` | Follow-up with `continuationToken` (+ message or `inputResponses`) |
| `GET /eve/v1/session/:sessionId/stream` | NDJSON event stream (`application/x-ndjson`) |
| `GET /eve/v1/info` | JSON inspection payload |
| `GET /eve/v1/health` | Health check |
Stream events include `session.started`, `turn.started`, `actions.requested`, `action.result`, `input.requested`, `subagent.called`, `message.appended`, `turn.completed`, `session.waiting`, and terminal failure/completion events. Clients reconnect to the stream by `sessionId`; they resume conversation turns with `continuationToken`.
Public authoring helpers export from `eve` (`defineAgent`) and subpaths such as `eve/tools` (`defineTool`), `eve/skills`, `eve/connections`, and channel packages under `eve/channels/*`.
## How the frameworks differ
```mermaid
flowchart TB
subgraph flue ["Flue (withastro/flue)"]
FC["flue.config.ts<br/>target, root, output"]
FS["Source root<br/>.flue/ → src/ → root"]
AM["agents/*.ts<br/>createAgent"]
WM["workflows/*.ts<br/>run(ctx)"]
CM["channels/*.ts"]
APP["app.ts<br/>Hono + registerProvider"]
API["flue() routes<br/>/agents /workflows /runs /channels"]
FC --> FS
FS --> AM & WM & CM & APP
APP --> API
end
subgraph eve ["Eve (vercel/eve)"]
AG["agent/<br/>filesystem slots"]
DISC["Discovery → .eve/"]
CH["Channel layer<br/>continuationToken"]
HAR["Harness<br/>one AI unit"]
RT["Runtime<br/>sessionId + NDJSON stream"]
HTTP["/eve/v1/session*"]
AG --> DISC --> CH --> HAR --> RT --> HTTP
end
```
| Concern | Flue | Eve |
| ------- | ---- | --- |
| Primary unit of work | Agent session operations; workflow **runs** for scripted automation | Durable **session turns** across HTTP follow-ups |
| Discovery | TypeScript modules by filename under source root | Path-derived slots under `agent/` |
| Instructions | Inline string or imported `SKILL.md` in `createAgent` | `instructions.md` (filesystem contract) |
| Workflow concept | First-class `workflows/<name>.ts` with `run()` and `runId` telemetry | Workflow SDK under the hood; orchestration owned by runtime |
| Streaming | Durable Streams on `/agents/...` and `/runs/:runId` | NDJSON on `/eve/v1/session/:sessionId/stream` |
| Deploy targets | Node.js, Cloudflare Workers (+ ecosystem adapters) | Vercel-first (`eve deploy`); local Nitro host via `eve dev` / `eve start` |
| Client SDK | `@flue/sdk` (`createFlueClient`, `invoke`) | TypeScript SDK and framework bindings (Next.js, Nuxt, SvelteKit) |
Both frameworks support tools, skills, sandboxes, subagents, MCP connections, and channel ingress. Flue adds explicit workflow modules and workflow-only run history; Eve optimizes for markdown-first authoring and a stable session protocol with separated continuation and stream identifiers.
## Shortest paths in
<Steps>
<Step title="Flue: run the hello-world fixture">
From `withastro-flue/examples/hello-world/`:
```bash
pnpm install
flue dev --target node
flue run hello --target node
```
`hello` workflow initializes an agent via `init(agent)`, opens a session, and runs `session.prompt`. Use `flue connect session-test local` for an interactive agent instance.
</Step>
<Step title="Eve: run the weather fixture">
From `vercel-eve/apps/fixtures/weather-agent/`:
```bash
pnpm install
eve dev
```
In another terminal, create a session:
```bash
curl -X POST http://127.0.0.1:3000/eve/v1/session \
-H 'content-type: application/json' \
-d '{"message":"What is the weather in San Francisco?"}'
```
Use the returned `sessionId` to open `GET /eve/v1/session/<sessionId>/stream`. Run `eve info` to inspect discovery and `.eve/` compile output.
</Step>
</Steps>
<Note>
Model strings are provider-neutral in both frameworks. Flue sets `model` in `createAgent(() => ({ model: "provider/model" }))` or per-call prompt options. Eve sets `model` in `defineAgent({ model: "provider/model" })` inside `agent/agent.ts`.
</Note>
## Monorepo packages (reference)
**Flue** core packages in `withastro-flue/packages/`:
| Package | Role |
| ------- | ---- |
| `@flue/runtime` | Harness, sessions, tools, sandbox, `flue()` routing |
| `@flue/cli` | Build graph, discovery, `flue` binary |
| `@flue/sdk` | HTTP client for deployed agents and workflows |
| `@flue/postgres`, `@flue/mysql`, … | Optional `PersistenceAdapter` backends |
| Channel packages (`@flue/slack`, …) | Provider webhook blueprints |
**Eve** ships as a single `eve` package with compiled `dist/`, bundled docs, and framework entry points (`eve/tools`, `eve/channels/slack`, `eve/nuxt`, etc.). Fixtures and templates live under `vercel-eve/apps/`.
## Related pages
<CardGroup>
<Card title="Installation" href="/installation">
Prerequisites, Node engine requirements, and first-install commands for both frameworks.
</Card>
<Card title="Flue quickstart" href="/flue-quickstart">
Scaffold, `flue dev`, `flue run`, and `flue connect`.
</Card>
<Card title="Eve quickstart" href="/eve-quickstart">
`eve init`, `eve dev`, `eve info`, and the first session POST.
</Card>
<Card title="Runtime models" href="/runtime-models">
Compare Flue sessions and workflow runs with Eve sessions, turns, and token contracts.
</Card>
<Card title="Flue project layout" href="/flue-project-layout">
Source-root resolution, agents, workflows, and `app.ts`.
</Card>
<Card title="Eve project layout" href="/eve-project-layout">
Authored slots under `agent/` and path-derived naming.
</Card>
</CardGroup>
---
## 02. Installation
> Prerequisites, package managers, Node engine requirements, and first-install commands for Flue and Eve projects.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/02-installation.md
- Generated: 2026-06-18T19:25:03.273Z
### Source Files
- `withastro-flue:package.json`
- `withastro-flue:pnpm-workspace.yaml`
- `withastro-flue:packages/cli/package.json`
- `vercel-eve:package.json`
- `vercel-eve:pnpm-workspace.yaml`
- `vercel-eve:packages/eve/package.json`
- `vercel-eve:.nvmrc`
---
title: "Installation"
description: "Prerequisites, package managers, Node engine requirements, and first-install commands for Flue and Eve projects."
---
Flue and Eve ship as npm packages (`@flue/runtime`, `@flue/cli`, `eve`) with distinct Node floors, scaffold contracts, and CLI entrypoints. Flue separates runtime (`@flue/runtime`) from build/dev tooling (`@flue/cli`); `flue init` writes `flue.config.ts` only. Eve scaffolds a full agent app through `eve init`, installs dependencies, optionally initializes Git, and starts `eve dev`.
## Prerequisites
Both frameworks are TypeScript-first and provider-neutral: you bring your own model credentials and choose any supported provider prefix (for example `anthropic/...`, `openai/...`, or gateway-routed ids). Neither framework requires a specific hosted connector at install time.
| Requirement | Flue | Eve |
| --- | --- | --- |
| Node.js | `>=22.19.0` on published `@flue/runtime` and `@flue/cli` | `>=24` on published `eve` and the Eve monorepo |
| Package manager (application projects) | npm, pnpm, yarn, or bun — docs use npm | npm, pnpm, yarn, or bun — `eve init` detects the invoking manager |
| Model credentials | At least one provider API key or Cloudflare Workers AI binding | Gateway key (`AI_GATEWAY_API_KEY`), Vercel OIDC (`vercel link`), or direct provider key |
| Git | Optional | `eve init` runs `git init` on fresh scaffolds when Git is available |
<Warning>
The Flue CLI bin rejects Bun and enforces Node `>=22.18` on Node 22, or `>=23.6` on Node 23, for native TypeScript config loading. Published packages still declare `engines.node: ">=22.19.0"`.
</Warning>
### Flue-specific prerequisites
- **LLM access** — A model specifier (for example `anthropic/claude-sonnet-4-6`) and matching provider credentials, or Cloudflare `cloudflare/*` models without API keys when targeting Workers.
- **Target selection** — `flue init` requires `--target node` or `--target cloudflare` before `flue dev`, `flue build`, or `flue run`.
### Eve-specific prerequisites
- **Companion packages** — Application projects depend on `eve`, `ai`, and `zod`. Fresh scaffolds also include `@vercel/connect` for later channel additions.
- **Scaffolded Node pin** — Generated apps declare a single major in `engines.node` (for example `24.x`) derived from the installed Eve release, so deployment platforms resolve a stable major.
## Node engine requirements
```text
Application install Monorepo development
─────────────────────────────────────────────────────────────
Flue Node >=22.19.0 (packages) Node >=22, pnpm >=11 <12
CLI bin: >=22.18 or >=23.6 packageManager: pnpm@11.1.1
Eve Node >=24 (eve package) Node >=24 (.nvmrc: 24)
Scaffold pins e.g. 24.x packageManager: pnpm@11.5.2
```
<Note>
Eve reconciles `engines.node` when adding to an existing project: compatible ranges are preserved; incompatible ranges are replaced with the Eve-selected major and a warning is printed.
</Note>
## Install a Flue application
Flue publishes `@flue/runtime` (harness, sessions, tools) and `@flue/cli` (the `flue` binary and `defineConfig`). Install both in a new project directory, add credentials, then initialize the deployment target.
<Steps>
<Step title="Install packages">
<CodeGroup>
```bash title="npm"
npm install @flue/runtime
npm install --save-dev @flue/cli
```
```bash title="pnpm"
pnpm add @flue/runtime
pnpm add -D @flue/cli
```
```bash title="yarn"
yarn add @flue/runtime
yarn add -D @flue/cli
```
```bash title="bun"
bun add @flue/runtime
bun add -D @flue/cli
```
</CodeGroup>
</Step>
<Step title="Add environment variables">
Create `.env` at the project root with provider credentials. Add `.env` to `.gitignore`.
```bash
echo 'ANTHROPIC_API_KEY="your-api-key"' > .env
```
Flue CLI commands load `<project>/.env` when present; shell-exported values take precedence.
</Step>
<Step title="Initialize configuration">
`flue init` writes `flue.config.ts` and does not scaffold agents, workflows, or `app.ts`.
```bash
npx flue init --target node
# or: npx flue init --target cloudflare
```
<ParamField body="--target" type="'node' | 'cloudflare'" required>
Selects the value written to `flue.config.ts`. Required on first init.
</ParamField>
<ParamField body="--root" type="string">
Directory that receives `flue.config.ts`. Defaults to the current working directory.
</ParamField>
<ParamField body="--force" type="boolean">
Overwrite an existing `flue.config.*` file. Without it, any discovered config blocks generation.
</ParamField>
Generated file:
```ts title="flue.config.ts"
import { defineConfig } from '@flue/cli/config';
export default defineConfig({
target: 'node',
});
```
</Step>
<Step title="Verify">
```bash
node -v # expect >=22.19.0 per package engines
npx flue docs
```
After authoring an agent module under `agents/` or `.flue/agents/`, run `npx flue connect <agent> local --target node` or `npx flue dev --target node`.
</Step>
</Steps>
### Optional Flue packages
| Package | When to install |
| --- | --- |
| `@flue/sdk` | HTTP client for deployed agents and workflows |
| `@flue/postgres`, `@flue/redis`, … | Persistence or channel adapters via `flue add` blueprints |
## Install an Eve application
Eve publishes a single package `eve` with the `eve` CLI binary. The fastest path is `npx eve@latest init`, which scaffolds the project, installs dependencies, initializes Git on fresh projects, and starts the dev server.
<Steps>
<Step title="Scaffold with eve init">
```bash
npx eve@latest init my-agent
```
For an empty directory you already created:
```bash
cd my-empty-dir
npx eve@latest init .
```
`eve init` holds the terminal while `eve dev` runs. Press Ctrl+C to regain the shell before editing files. The command does not create or link a Vercel project.
<ParamField body="target" type="string">
Project name (`my-agent`) creates a child directory. `.`, `./`, or an existing directory with `package.json` adds an agent to that project.
</ParamField>
<ParamField body="--channel-web-nextjs" type="flag">
Adds the Web Chat Next.js application on fresh scaffolds. Rejected when adding to an existing project — use `eve channels add web` instead.
</ParamField>
</Step>
<Step title="Understand scaffold output">
Fresh projects receive:
```text
my-agent/
├── package.json # scripts: dev, build, start, typecheck
├── tsconfig.json
├── pnpm-workspace.yaml # when scaffolded with pnpm
└── agent/
├── agent.ts
├── instructions.md
└── channels/eve.ts
```
`package.json` dependencies include `eve`, `ai`, `zod`, and `@vercel/connect`. `engines.node` is pinned to the Eve release major (for example `24.x`).
Package manager selection on fresh scaffolds:
1. Manager that launched the CLI (`npx` → npm, `pnpm dlx` → pnpm, `yarn dlx` → yarn)
2. Default `pnpm` when the `eve` binary runs directly with no user-agent hint
Existing projects keep their detected manager (`packageManager` field, then lockfile, then `pnpm` default).
</Step>
<Step title="Configure model credentials">
Default model is `anthropic/claude-sonnet-4.6`. Set one of:
- `AI_GATEWAY_API_KEY` for gateway-routed models
- `VERCEL_OIDC_TOKEN` after `vercel link`
- Direct provider key (for example `ANTHROPIC_API_KEY` for `anthropic/...`)
</Step>
<Step title="Verify">
```bash
node -v # expect >=24
cd my-agent
npm run dev # or: npx eve dev
# in another shell:
npx eve info
```
`eve info` confirms discovery, routes, and compiled artifact paths without booting the full dev TUI.
</Step>
</Steps>
### Manual Eve installation
To add Eve without `eve init`, declare Node and install dependencies:
```json
{
"engines": {
"node": "24.x"
}
}
```
```bash
npm install eve@latest ai zod
```
Author `agent/agent.ts` and `agent/instructions.md`, then run `npx eve dev` (no `dev` script is created automatically on the manual path).
### Add Eve to an existing application
From a directory with `package.json` and no `agent/` tree:
```bash
npx eve@latest init .
```
Eve writes `agent/` files, adds missing `eve`, `ai`, and `zod` dependencies, reconciles `engines.node`, and leaves unrelated project configuration untouched. `--channel-web-nextjs` is not supported in this mode.
## Monorepo development
Clone either upstream repository to work on the frameworks themselves. Application installs above do not require monorepo checkout.
<Tabs>
<Tab title="Flue (withastro/flue)">
| Setting | Value |
| --- | --- |
| Node | `>=22` |
| pnpm | `>=11 <12` (pinned `pnpm@11.1.1`) |
| Workspace | `packages/*`, `examples/*`, `apps/*` |
```bash
git clone https://github.com/withastro/flue.git
cd flue
corepack enable
pnpm install
pnpm run build
```
Build order for local CLI work: `packages/runtime` then `packages/cli`. CI builds with:
```bash
pnpm --filter @flue/runtime --filter @flue/sdk --filter @flue/cli build
```
</Tab>
<Tab title="Eve (vercel/eve)">
| Setting | Value |
| --- | --- |
| Node | `>=24` (`.nvmrc` pins `24`) |
| pnpm | pinned `pnpm@11.5.2` via `packageManager` |
| Workspace | `packages/*`, `apps/*`, `e2e/fixtures/*` |
```bash
git clone https://github.com/vercel/eve.git
cd eve
nvm use # or: fnm use
corepack enable
pnpm install
pnpm build
pnpm dev
```
`pnpm dev` watches the `eve` package build and runs the `weather-agent` fixture on an available localhost port.
</Tab>
</Tabs>
## Troubleshooting
<AccordionGroup>
<Accordion title="Flue: Node version rejected at CLI startup">
Upgrade to Node `>=22.19.0` to satisfy package `engines`. If you are on Node 23, use `>=23.6` for the CLI bin guard. Bun is not supported as a Node replacement for the `flue` binary.
</Accordion>
<Accordion title="Flue: flue init refuses to overwrite config">
An existing `flue.config.*` (`.ts`, `.mts`, `.mjs`, `.js`, `.cjs`, `.cts`) blocks generation. Pass `--force` to write `flue.config.ts`; the new `.ts` file takes precedence in discovery.
</Accordion>
<Accordion title="Eve: dependency install fails during eve init">
`eve init` runs the detected package manager with release-age bypass for pinned scaffold versions. On failure, the manager's stderr is replayed. Retry install manually from the project root (`npm install`, `pnpm install`, etc.), then run `npx eve dev`.
</Accordion>
<Accordion title="Eve: cannot add agent to existing project">
`eve init .` requires `package.json`, no pre-existing `agent/` files, and no conflicting paths. For Web Chat on existing apps, run `eve channels add web` after init.
</Accordion>
<Accordion title="Eve: engines.node overridden warning">
When adding Eve to a project whose `engines.node` spans multiple majors, Eve replaces the range with the pinned major for the installed release (for example `24.x`) and prints a warning. Update CI and deployment Node versions to match.
</Accordion>
</AccordionGroup>
## Next
<CardGroup>
<Card title="Flue quickstart" href="/flue-quickstart">
Scaffold agents, run `flue dev`, invoke workflows, and connect to agent instances.
</Card>
<Card title="Eve quickstart" href="/eve-quickstart">
Run `eve init`, start `eve dev`, inspect discovery, and send the first HTTP session message.
</Card>
<Card title="Flue configuration reference" href="/flue-configuration-reference">
`flue.config.*` keys, source-root precedence, and CLI overrides.
</Card>
<Card title="Eve CLI reference" href="/eve-cli-reference">
Full `eve` command surface including `info`, `build`, `deploy`, and `channels`.
</Card>
</CardGroup>
---
## 03. Flue quickstart
> Scaffold a Flue project, run `flue dev`, invoke a workflow with `flue run`, and connect to an agent instance with `flue connect`.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/03-flue-quickstart.md
- Generated: 2026-06-18T19:24:30.065Z
### Source Files
- `withastro-flue:packages/cli/bin/flue.ts`
- `withastro-flue:examples/hello-world/flue.config.ts`
- `withastro-flue:examples/hello-world/src/workflows/hello.ts`
- `withastro-flue:examples/hello-world/src/app.ts`
- `withastro-flue:packages/cli/src/lib/dev.ts`
- `withastro-flue:packages/cli/src/lib/config.ts`
---
title: "Flue quickstart"
description: "Scaffold a Flue project, run `flue dev`, invoke a workflow with `flue run`, and connect to an agent instance with `flue connect`."
---
The Flue CLI (`@flue/cli`) discovers agents and workflows from a project's source root, builds a Node or Cloudflare artifact, and exposes four day-one commands: `flue init` scaffolds `flue.config.ts`, `flue dev` runs a watch-mode HTTP server on port **3583** by default, `flue run <workflow>` performs a one-shot Node workflow invocation, and `flue connect <agent> <instance-id>` opens an interactive stdin prompt loop against a built agent.
## Prerequisites
| Requirement | Detail |
| --- | --- |
| Node.js | `>=22.19.0` (`@flue/cli` engines constraint) |
| Packages | `@flue/runtime` (runtime) and `@flue/cli` (CLI, dev dependency) |
| `target` | Required on the CLI (`--target node` or `--target cloudflare`) or in `flue.config.ts` — no default |
| Model access | A provider API key in `.env` or shell env, or a local OpenAI-compatible server registered in `app.ts` |
| Source layout | One of `.flue/`, `src/`, or project root — first existing directory wins |
<Note>
Provider and model registration is a runtime concern in `app.ts` via `registerProvider(...)`, not in `flue.config.ts`. The config file only sets build-shaped knobs: `target`, `root`, and `output`.
</Note>
## Scaffold a project
<Steps>
<Step title="Install packages">
<Tabs>
<Tab title="npm">
```bash
mkdir my-flue-app && cd my-flue-app
npm init -y
npm install @flue/runtime
npm install --save-dev @flue/cli
```
</Tab>
<Tab title="pnpm">
```bash
mkdir my-flue-app && cd my-flue-app
pnpm init
pnpm add @flue/runtime
pnpm add -D @flue/cli
```
</Tab>
</Tabs>
</Step>
<Step title="Initialize config">
`flue init` writes a starter `flue.config.ts` with the chosen `target`. Re-run with `--force` to overwrite an existing config.
```bash
npx flue init --target node
```
Generated file:
```ts title="flue.config.ts"
import { defineConfig } from '@flue/cli/config';
export default defineConfig({
target: 'node',
});
```
</Step>
<Step title="Add environment variables">
When present, `<project>/.env` loads before config resolution. Shell-exported values override file values.
```bash
echo 'ANTHROPIC_API_KEY="your-api-key"' > .env
```
Add `.env` to `.gitignore`. Do not commit provider credentials.
</Step>
<Step title="Create source modules">
Flue resolves the source root as `<root>/.flue` when that directory exists, otherwise `<root>/src`, otherwise `<root>`. Module names derive from filenames under `agents/` and `workflows/`.
:::files
my-flue-app/
├── flue.config.ts
├── .env
├── package.json
└── src/
├── app.ts # optional Hono composition + registerProvider(...)
├── agents/
│ └── assistant.ts
└── workflows/
└── hello.ts
:::
**Agent module** — default export registers the agent; filename becomes the CLI name:
```ts title="src/agents/assistant.ts"
import { createAgent } from '@flue/runtime';
export default createAgent(() => ({
model: 'anthropic/claude-sonnet-4-6',
instructions: 'You are a concise, helpful assistant.',
}));
```
**Workflow module** — export `run()`; initialize the agent inside it:
```ts title="src/workflows/hello.ts"
import { createAgent, type FlueContext } from '@flue/runtime';
import * as v from 'valibot';
const agent = createAgent(() => ({ model: 'anthropic/claude-sonnet-4-6' }));
export async function run({ init, log }: FlueContext) {
const harness = await init(agent);
const session = await harness.session();
const response = await session.prompt('What is 2 + 2? Return only the number.', {
result: v.object({ answer: v.number() }),
});
log.info('solved arithmetic prompt', { answer: response.data.answer });
return response.data;
}
```
The `hello` workflow is available as `flue run hello`. The `assistant` agent is available as `flue connect assistant local`.
</Step>
</Steps>
<Tip>
For AI-guided scaffolding, fetch `https://flueframework.com/start.md` and follow its agent-first layout rules. `flue init` prints this URL as the recommended next step.
</Tip>
## Run the dev server
`flue dev` performs an initial build, starts the target-specific server, watches the project root (150ms debounce), rebuilds on changes, and prints discovered agents, workflows, and channels.
```bash
npx flue dev --target node
```
<ParamField body="--port" type="number">
Dev server listen port. Default: **3583** (`DEFAULT_DEV_PORT`).
</ParamField>
<ParamField body="--root" type="string">
Project root. Default: current working directory.
</ParamField>
<ParamField body="--config" type="string">
Explicit `flue.config.*` path (relative to cwd). CLI flags override config file values.
</ParamField>
<ParamField body="--env" type="string">
Single alternate `.env`-format file. Without `--env`, commands load `<project>/.env` when present.
</ParamField>
Expected stderr output includes target, server URL, source root, and discovery lists:
```text
flue dev
target node
server http://localhost:3583
source src
...
agents
assistant
workflows
hello
...
connect with: flue connect assistant local
watching for changes; Ctrl+C to stop
```
While `flue dev` is running, workflows are also reachable over HTTP:
```bash
curl -X POST 'http://localhost:3583/workflows/hello?wait=result' \
-H 'Content-Type: application/json' \
-d '{}'
```
<Warning>
`flue run` and `flue connect` are Node-only one-shot invokers. For Cloudflare targets, use `flue dev --target cloudflare` and invoke workflows via HTTP (`POST /workflows/:name`).
</Warning>
## Invoke a workflow with `flue run`
`flue run` builds silently, spawns a local Node child process (`FLUE_MODE=local`), invokes the named workflow once, streams agent events to stderr, prints the JSON return value to stdout, and exits.
```bash
npx flue run hello --target node
```
With a JSON payload (defaults to `{}` when omitted):
```bash
npx flue run hello --target node --payload '{"name": "World"}'
```
<ParamField body="workflow" type="string" required>
Positional workflow name matching a file in `workflows/` (basename without extension).
</ParamField>
<ParamField body="--payload" type="string">
JSON object passed to the workflow `run()` context. Must parse as valid JSON.
</ParamField>
<ParamField body="--target" type="string">
Build target. For `flue run`, only `node` is supported. Cloudflare configs exit with a curl-based workaround hint.
</ParamField>
<RequestExample>
```bash
npx flue run hello --target node
```
</RequestExample>
<ResponseExample>
```text
# stderr (branded header + streamed events)
flue run
workflow hello
target node
run run_01H...
solved arithmetic prompt
# stdout (workflow return value)
{
"answer": 4
}
```
</ResponseExample>
The stderr `run` line carries the generated run ID. Inspect that run while a dev server is active:
```bash
npx flue logs run_01H... --server http://127.0.0.1:3583
```
## Connect to an agent with `flue connect`
`flue connect` builds the project, starts a local agent process, waits for readiness (60s timeout), then reads prompts from stdin one line at a time until EOF (`Ctrl+D`).
```bash
npx flue connect assistant local --target node
```
<ParamField body="agent" type="string" required>
Agent module name (filename under `agents/` without extension).
</ParamField>
<ParamField body="instance-id" type="string" required>
Opaque instance identifier for this connection (for example `local`, `thread-1`, `user-42`).
</ParamField>
<Warning>
`flue connect` does not accept `--payload`. Enter prompts interactively after connecting.
</Warning>
<RequestExample>
```bash
npx flue connect assistant local --target node
```
</RequestExample>
<ResponseExample>
```text
flue connect assistant/local starting...
connected
enter one prompt per line; Ctrl+D to exit
What can you help with?
{
"text": "I can answer questions, draft content, and summarize information."
}
```
</ResponseExample>
Each submitted line sends a `prompt` IPC message; agent events (text deltas, tool calls, thinking, errors) render on stderr. Structured results print as JSON on stdout.
## Command comparison
| Command | Mode | Primary use | Exit behavior |
| --- | --- | --- | --- |
| `flue dev` | Long-running watch server | Local iteration, HTTP API, channel webhooks | Runs until `Ctrl+C` (exit 130/143) |
| `flue run <workflow>` | One-shot Node subprocess | CI scripts, ad-hoc workflow execution | Exits after workflow completes |
| `flue connect <agent> <id>` | Interactive Node subprocess | Manual agent testing, REPL-style prompts | Exits on `Ctrl+D` or child crash |
```text
flue.config.ts + src/{agents,workflows}/
│
▼
flue build (implicit)
│
┌─────┴─────┬─────────────┐
▼ ▼ ▼
flue dev flue run flue connect
(HTTP :3583) (workflow) (agent stdin)
```
## Troubleshooting
<AccordionGroup>
<Accordion title="Missing required `target`">
```
[flue] Missing required `target`. Set it via `--target <node|cloudflare>`
or in `flue.config.ts` as `target: "node"` (or `"cloudflare"`).
```
Pass `--target` on every command or set `target` in `flue.config.ts`.
</Accordion>
<Accordion title="Config already exists during `flue init`">
`flue init` refuses to overwrite unless `--force` is passed. Discovery checks all `flue.config.*` basenames, not only `.ts`.
</Accordion>
<Accordion title="Invalid `--payload` JSON">
`flue run` validates `--payload` at parse time. Quote the JSON for the shell:
```bash
npx flue run hello --target node --payload '{"key": "value"}'
```
</Accordion>
<Accordion title="Cloudflare `flue run` / `flue connect` rejected">
Both commands require a Node.js runtime. Start `flue dev --target cloudflare` and call `POST /workflows/:name` instead.
</Accordion>
<Accordion title="Dev server port in use">
Override the default port:
```bash
npx flue dev --target node --port 8787
```
</Accordion>
</AccordionGroup>
## Next
<CardGroup>
<Card title="Installation" href="/installation">
Prerequisites, package managers, and first-install commands.
</Card>
<Card title="Flue project layout" href="/flue-project-layout">
Source-root resolution, module discovery, and `app.ts` composition.
</Card>
<Card title="Flue workflows" href="/flue-workflows">
Author workflow modules, HTTP invocation, and `flue logs` inspection.
</Card>
<Card title="Build Flue agents" href="/build-flue-agents">
`createAgent`, profiles, tools, skills, sandboxes, and providers.
</Card>
<Card title="Flue CLI reference" href="/flue-cli-reference">
Full command flags, defaults, and exit behavior.
</Card>
<Card title="Flue examples" href="/flue-examples">
Copy-pasteable hello-world and integration fixtures.
</Card>
</CardGroup>
---
## 04. Eve quickstart
> Run `eve init`, start `eve dev`, inspect discovery with `eve info`, and send the first session message to the stable HTTP API.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/04-eve-quickstart.md
- Generated: 2026-06-18T19:24:48.924Z
### Source Files
- `vercel-eve:README.md`
- `vercel-eve:docs/getting-started.mdx`
- `vercel-eve:docs/reference/cli.md`
- `vercel-eve:docs/concepts/sessions-runs-and-streaming.md`
- `vercel-eve:apps/fixtures/weather-agent/agent/agent.ts`
- `vercel-eve:apps/fixtures/weather-agent/agent/tools/get_weather.ts`
---
title: "Eve quickstart"
description: "Run `eve init`, start `eve dev`, inspect discovery with `eve info`, and send the first session message to the stable HTTP API."
---
Eve scaffolds a filesystem-first agent under `agent/`, compiles discovery into `.eve/`, and serves a stable HTTP channel at `/eve/v1/session*` on port `3000` during local development. The `eve` CLI owns project bootstrap, discovery inspection, and the dev runtime; every app exposes the same session create, continue, and NDJSON stream routes whether you call them from `curl`, a client SDK, or the terminal UI.
## Prerequisites
| Requirement | Detail |
| --- | --- |
| Node.js | `24.x` or newer (`engines.node` is pinned on scaffold) |
| Package manager | npm (bundled with Node), or pnpm/yarn/bun if that manager launched the CLI |
| Model credential | Provider-neutral: gateway model ids need `AI_GATEWAY_API_KEY` or `VERCEL_OIDC_TOKEN`; direct provider ids need that provider's env key (for example `ANTHROPIC_API_KEY` for `anthropic/...`) |
<Note>
`eve init` does not create or link a Vercel project. Use `eve link` later to pull gateway credentials into `.env.local`. A running `eve dev` reloads env files automatically after linking.
</Note>
## Scaffold a project
<Steps>
<Step title="Run eve init">
<Tabs>
<Tab title="New directory">
```bash
npx eve@latest init my-agent
```
Creates `my-agent/` with `agent/agent.ts`, `agent/instructions.md`, `agent/channels/eve.ts`, `package.json` scripts (`dev`, `build`, `start`), installs `eve`, `ai`, and `zod`, initializes Git, and starts the dev server.
</Tab>
<Tab title="Current directory">
```bash
mkdir my-agent && cd my-agent
npx eve@latest init .
```
Requires an empty directory (only `.git` and similar dotfiles allowed). Same scaffold as a named target, written in place.
</Tab>
<Tab title="Existing app">
```bash
cd existing-app
npx eve@latest init .
```
Requires an existing `package.json` and no `agent/` directory yet. Adds only the agent files and missing `eve`, `ai`, and `zod` dependencies.
</Tab>
</Tabs>
Pass `--channel-web-nextjs` on a **new** scaffold to add the Web Chat Next.js app. That flag is rejected when adding to an existing project; run `eve channels add web` there instead.
</Step>
<Step title="Stop the dev server to edit files">
`eve init` holds the terminal while `eve dev` runs. Press **Ctrl+C** to return to the shell before editing `agent/`.
On a fresh scaffold, init launches `eve dev --input /model`, which opens the TUI model setup flow. Configure credentials there or in `.env.local` before sending HTTP requests that hit the model.
</Step>
<Step title="Verify the scaffold layout">
:::files
my-agent/
├── package.json
├── tsconfig.json
├── agent/
│ ├── agent.ts
│ ├── instructions.md
│ └── channels/
│ └── eve.ts
└── .eve/ # created after first compile/dev
:::
The default `agent/agent.ts` sets `model: "anthropic/claude-sonnet-4.6"`. Tool names are path-derived: `agent/tools/get_weather.ts` becomes tool `get_weather` for the model.
</Step>
</Steps>
## Start the local runtime
From the project root:
```bash
npm run dev
```
Equivalent to `eve dev`. With no subcommand, `eve` alone also runs `eve dev`.
| Flag | Default | Effect |
| --- | --- | --- |
| `--port` | `$PORT`, then `3000` | Listen port |
| `--host` | all interfaces | Bind address |
| `--no-ui` | UI on | Server only, no terminal UI |
| `--url <url>` | none | Connect UI to a remote deployment instead of starting locally |
<Check>
A healthy local server answers `GET http://127.0.0.1:3000/eve/v1/health` without credentials. Eve writes the active dev PID to `.eve/dev-process.pid`; a second `eve dev` for the same agent exits if that process is still running.
</Check>
## Inspect discovery with eve info
Run discovery and compile without starting the server:
```bash
eve info
```
Human output includes app root, agent root, compile status, diagnostics summary, discovered skills and tools, artifact paths under `.eve/`, and the active messaging contract:
| Label | Example value |
| --- | --- |
| Create | `POST /eve/v1/session` |
| Continue | `POST /eve/v1/session/:id` |
| Stream | `GET /eve/v1/session/:id/stream` |
Machine-readable output:
```bash
eve info --json
```
<ParamField body="--json" type="flag">
Emit the stable JSON contract: `appRoot`, `agentRoot`, `status`, `model`, `tools`, `channels`, `messaging`, `artifacts`, and `diagnostics` counts.
</ParamField>
<Tip>
Run `eve info` after editing files under `agent/` to confirm discovery before debugging runtime behavior. On compile failure, `eve build` prints the full diagnostics report and the path to `.eve/discovery/diagnostics.json`.
</Tip>
## Send the first session message
Every Eve app mounts the Eve HTTP channel. Session routes are enabled by default even when you omit `agent/channels/eve.ts`; the scaffold includes `agent/channels/eve.ts` with `localDev()` and a production auth placeholder.
### Create a session
:::endpoint POST /eve/v1/session
Start a durable session with a user message. Returns immediately with handles for streaming and follow-ups.
:::
<ParamField body="message" type="string | UserContent" required>
Non-empty user message. Plain strings are the common case.
</ParamField>
<RequestExample>
```bash
curl -X POST http://127.0.0.1:3000/eve/v1/session \
-H 'content-type: application/json' \
-d '{"message":"What is the weather in Brooklyn?"}'
```
</RequestExample>
<ResponseExample>
```json
{
"continuationToken": "eve:7f3c…",
"ok": true,
"sessionId": "ses_01h…"
}
```
Response headers include `x-eve-session-id` (same session id) and `cache-control: no-store`. Status is **202 Accepted**.
</ResponseExample>
<ResponseField name="continuationToken" type="string">
Resume handle for the next user message on this conversation. Owned by the channel.
</ResponseField>
<ResponseField name="sessionId" type="string">
Stream-and-inspect handle. Attach to `GET /eve/v1/session/:sessionId/stream`.
</ResponseField>
<Warning>
Do not conflate the two handles. `continuationToken` resumes the conversation; `sessionId` identifies the run for streaming and inspection.
</Warning>
### Stream session events
:::endpoint GET /eve/v1/session/:sessionId/stream
Replay and follow the durable NDJSON event stream for one session.
:::
<RequestExample>
```bash
curl -N http://127.0.0.1:3000/eve/v1/session/ses_01h…/stream
```
</RequestExample>
The response content type is `application/x-ndjson; charset=utf-8`. Each line is one JSON event. A typical tool-using turn emits:
| Event | Meaning |
| --- | --- |
| `session.started` | Durable session created |
| `turn.started` | New turn began |
| `actions.requested` | Model requested tool calls |
| `action.result` | Tool returned |
| `message.completed` | Finalized assistant text |
| `session.waiting` | Session parked, ready for follow-up |
| `session.completed` | Terminal session end |
`reasoning.appended` and `message.appended` are optional incremental deltas; clients that do not render streaming can wait for `reasoning.completed` and `message.completed`.
Reconnect or rewind with `?startIndex=<count>` on the stream URL.
### Send a follow-up
After `session.waiting`, post to the session-specific route with the stored token:
:::endpoint POST /eve/v1/session/:sessionId
Continue an existing session. Requires the current `continuationToken`.
:::
<ParamField body="continuationToken" type="string" required>
Token from the create response (or the latest valid token for this session).
</ParamField>
<ParamField body="message" type="string" required>
Next user message.
</ParamField>
<RequestExample>
```bash
curl -X POST http://127.0.0.1:3000/eve/v1/session/ses_01h… \
-H 'content-type: application/json' \
-d '{"continuationToken":"eve:7f3c…","message":"Now check Queens."}'
```
</RequestExample>
<ResponseExample>
```json
{
"ok": true,
"sessionId": "ses_01h…"
}
```
Status **200**. Reattach to the same stream URL to observe the new turn.
</ResponseExample>
Send one follow-up at a time and wait for `session.waiting` before posting another message to the same session.
## Optional: add a typed tool
The scaffold ships with the default harness (file, shell, web, and delegation tools). For a concrete tool call in the stream, add `agent/tools/get_weather.ts`:
```ts title="agent/tools/get_weather.ts"
import { defineTool } from "eve/tools";
import { z } from "zod";
export default defineTool({
description: "Get the current weather for a city.",
inputSchema: z.object({ city: z.string().min(1) }),
async execute({ city }) {
return { city, condition: "Sunny", temperatureF: 72 };
},
});
```
Run `eve info` to confirm `get_weather` appears under tools, then repeat the HTTP flow. The `weather-agent` fixture in the Eve repo is a fuller example with instructions, skills, and model options.
## Troubleshooting
| Symptom | Check |
| --- | --- |
| Model errors on first message | Credential in `.env.local`; default model `anthropic/claude-sonnet-4.6` needs gateway or Anthropic key. TUI `/model` walks through setup. |
| Tool not called | `eve info` lists discovered tools; filename must be snake_case (`get_weather.ts` → `get_weather`). |
| `400` on follow-up | Body must include non-empty `continuationToken`; stale tokens are rejected. |
| `404` on stream | `sessionId` from create response or `x-eve-session-id` header; server must still be running. |
| Port already in use | Another `eve dev` may be active; check `.eve/dev-process.pid` or pass `--port`. |
| Discovery warnings | `eve info` diagnostics row; inspect `.eve/discovery/diagnostics.json`. |
<AccordionGroup>
<Accordion title="Auth during local development">
`localDev()` on the scaffolded `agent/channels/eve.ts` accepts loopback requests during development. Deployed targets require OIDC or your own auth policy. `GET /eve/v1/health` is always public.
</Accordion>
<Accordion title="Smoke-test a deployment from the CLI">
```bash
eve dev https://your-app.vercel.app
```
Connects the terminal UI to a remote server instead of booting locally.
</Accordion>
</AccordionGroup>
## Related pages
<CardGroup>
<Card title="Installation" href="/installation">
Prerequisites, Node engine requirements, and first-install commands for Eve projects.
</Card>
<Card title="Eve project layout" href="/eve-project-layout">
Authored slots under `agent/`, path-derived naming, and discovery boundaries.
</Card>
<Card title="Eve sessions and streaming" href="/eve-sessions-streaming">
Full NDJSON event set, `continuationToken` lifecycle, HITL, and reconnect rules.
</Card>
<Card title="Eve HTTP protocol reference" href="/eve-http-protocol-reference">
Session routes, event types, and client integration entry points.
</Card>
<Card title="Eve CLI reference" href="/eve-cli-reference">
Flags and defaults for `eve init`, `info`, `dev`, `build`, and related commands.
</Card>
<Card title="Eve weather fixture" href="/eve-weather-fixture">
End-to-end walkthrough of the `weather-agent` fixture with tool and skill authoring.
</Card>
</CardGroup>
---
## 05. Flue project layout
> Source-root resolution (`.flue/` vs `src/`), agent and workflow modules, `app.ts` composition, and discovery boundaries.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/05-flue-project-layout.md
- Generated: 2026-06-18T19:25:47.618Z
### Source Files
- `withastro-flue:AGENTS.md`
- `withastro-flue:packages/cli/src/lib/source-root.ts`
- `withastro-flue:packages/cli/src/lib/config.ts`
- `withastro-flue:examples/hello-world/src/app.ts`
- `withastro-flue:examples/hello-world/src/agents/session-test.ts`
- `withastro-flue:examples/hello-world/src/workflows/hello.ts`
- `withastro-flue:knip.json`
---
title: "Flue project layout"
description: "Source-root resolution (`.flue/` vs `src/`), agent and workflow modules, `app.ts` composition, and discovery boundaries."
---
Flue projects are TypeScript modules under a resolved **source root** (`sourceRoot`). The CLI discovers `agents/`, `workflows/`, and `channels/` there, optionally loads `app.ts`, `db.ts`, and `cloudflare.ts`, and generates a server entry that wires those modules into `@flue/runtime`. Build-time config lives in `flue.config.*` at the project root; runtime provider registration lives in `app.ts`.
## Source root resolution
`resolveSourceRoot(root)` picks the directory where authored Flue modules live. Precedence is fixed:
| Priority | Directory | When selected |
| --- | --- | --- |
| 1 | `<root>/.flue/` | Directory exists |
| 2 | `<root>/src/` | `.flue/` absent and `src/` exists |
| 3 | `<root>/` | Neither `.flue/` nor `src/` exists |
<Warning>
When `.flue/` exists, Flue **ignores** bare `agents/` and `workflows/` at the project root. Modules in `src/` are also outside the active source root in that case.
</Warning>
The resolved path is stored as `flueConfig.sourceRoot` and passed to `flue build` and `flue dev`. Override the project root with `root` in `flue.config.ts` or `--root`; source-root selection always runs against that resolved root.
<ParamField body="root" type="string">
Absolute or relative project root. Defaults to the config file directory (or the search directory when no config is loaded). Relative values in `flue.config.*` resolve from the config directory; inline CLI values resolve from the caller's working directory.
</ParamField>
<ParamField body="output" type="string">
Build output directory. Defaults to `<root>/dist`. Must not resolve to `root` or `sourceRoot` — the CLI rejects that to avoid watcher loops and accidental source deletion.
</ParamField>
:::files
my-flue-project/
├── flue.config.ts # build target, root, output
├── package.json
├── AGENTS.md # runtime context (not CLI-discovered)
├── .agents/skills/ # runtime skills (not CLI-discovered)
│
├── src/ # source root when .flue/ is absent
│ ├── app.ts # optional HTTP composition
│ ├── db.ts # optional persistence (Node only)
│ ├── cloudflare.ts # optional Worker exports (Cloudflare only)
│ ├── agents/
│ │ └── session-test.ts
│ ├── workflows/
│ │ └── hello.ts
│ ├── channels/
│ │ └── slack.ts
│ └── sandboxes/ # imported by agents; not auto-discovered
│ └── daytona.ts
│
└── dist/ # default build output
└── server.mjs # Node target
:::
The Flue monorepo dogfoods the `.flue/` layout: repository workflows such as `pr-redirect` live under `.flue/workflows/` while examples use `src/`.
## Discovery boundaries
Discovery runs at build time inside `sourceRoot`. The dev watcher rebuilds when files under `agents/`, `workflows/`, `channels/`, or top-level `app.*` / `cloudflare.*` change.
### Required modules
At least one agent **or** workflow file must exist. Channels alone do not satisfy the build:
```
[flue] No agent or workflow files found.
Expected at: <sourceRoot>/agents/ or <sourceRoot>/workflows/
```
### Module directories
| Directory | Filename → name | Extensions |
| --- | --- | --- |
| `agents/` | basename without extension | `.ts`, `.mts`, `.js`, `.mjs` |
| `workflows/` | basename without extension | `.ts`, `.mts`, `.js`, `.mjs` |
| `channels/` | basename without extension | `.ts`, `.mts`, `.js`, `.mjs` |
Rules enforced during discovery:
- One file per basename; duplicates throw.
- Agent and channel names must be non-empty and must **not** contain `:`.
- Workflow names must be non-empty (no `:` restriction).
- Declaration files matching `*.d.ts` / `*.d.mts` are skipped.
### Optional top-level entries
| File | Target | Role |
| --- | --- | --- |
| `app.{ts,mts,js,mjs}` | Node, Cloudflare | Custom HTTP app; extension priority `ts` > `mts` > `js` > `mjs` |
| `db.{ts,mts,js,mjs}` | Node only | Default-export `PersistenceAdapter` |
| `cloudflare.{ts,mts,js,mjs}` | Cloudflare only | Non-HTTP Worker handler exports; must not define `fetch` |
### Outside discovery
These paths are **not** scanned by the CLI but are part of typical project layout:
| Path | When used |
| --- | --- |
| `sandboxes/` | Imported by agents after `flue add` sandbox blueprints |
| `tools/`, `skills/` | Imported statically; packaged skills use `import … with { type: 'skill' }` |
| `AGENTS.md`, `.agents/skills/` | Discovered at **runtime** from the session working directory |
| `flue.config.*` | At project root, not under `sourceRoot` |
<Info>
`knip.json` in the Flue repo mirrors the same entry patterns: `.flue/{app,cloudflare,agents/*,workflows/*}` for the monorepo root and `src/{app,cloudflare,agents/*,workflows/*,channels/*}` for examples.
</Info>
## Agent modules
An agent module lives at `agents/<name>.ts`. The filename becomes the agent name exposed over HTTP and CLI (`flue connect <name>`).
**Required default export** — a value from `createAgent(...)`:
```ts title="agents/session-test.ts"
import { type AgentRouteHandler, createAgent, defineAgentProfile } from '@flue/runtime';
export const route: AgentRouteHandler = async (_c, next) => next();
const sessionTest = defineAgentProfile({
instructions: 'You are a test agent for session-oriented message delivery.',
});
export default createAgent(() => ({ profile: sessionTest }));
```
**Optional named exports:**
| Export | Type | Effect |
| --- | --- | --- |
| `route` | `AgentRouteHandler` | Per-agent HTTP middleware before dispatch |
| `description` | non-empty `string` | Listed in agent manifest |
Validation at build time (generated entry):
- Default export must have `__flueCreatedAgent === true` and an `initialize` function.
- Two agent modules cannot default-export the same `createAgent` value.
Terminology from `AGENTS.md`: an **agent profile** is a reusable `defineAgentProfile(...)` value; a **created agent** is the `createAgent(...)` initializer; an **agent instance** gets a URL `<id>` when initialized.
## Workflow modules
A workflow module lives at `workflows/<name>.ts`. Invoke it with `flue run <name>` or `POST /workflows/:name`.
**Required export** — async `run(ctx: FlueContext)`:
```ts title="workflows/hello.ts"
import { createAgent, type FlueContext, type WorkflowRouteHandler } from '@flue/runtime';
import * as v from 'valibot';
export const route: WorkflowRouteHandler = async (_c, next) => next();
const agent = createAgent(() => ({ model: 'anthropic/claude-sonnet-4-6' }));
export async function run({ init, log }: FlueContext) {
const harness = await init(agent);
const session = await harness.session();
const response = await session.prompt('What is 2 + 2? Return only the number.', {
result: v.object({ answer: v.number() }),
});
log.info('solved arithmetic prompt', { answer: response.data.answer });
return response.data;
}
```
**Optional named export:**
| Export | Type | Effect |
| --- | --- | --- |
| `route` | `WorkflowRouteHandler` | HTTP admission middleware; presence also enables HTTP transport in the manifest |
Workflows initialize agents locally via `init(agent)` inside `run()`. Each invocation gets a unique `ctx.id` (run ID). Runs are workflow-only — direct agent HTTP prompts and channel dispatches use sessions, not `/runs`.
## Channel modules
Channel modules live at `channels/<name>.ts` and export a named `channel` binding with a `routes` array. Routes mount under `/channels/:name` at runtime. See the channels guide for webhook verification and dispatch semantics.
Channels are discovered like agents and workflows but cannot be the sole authored module.
## `app.ts` composition
`app.ts` is optional. When present, its **default export** owns the entire HTTP pipeline; the generated entry forwards all requests to `app.fetch`.
```ts title="src/app.ts"
import { registerProvider } from '@flue/runtime';
import { flue } from '@flue/runtime/routing';
import { Hono } from 'hono';
registerProvider('ollama', {
api: 'openai-completions',
baseUrl: 'http://localhost:11434/v1',
});
const app = new Hono();
app.use('*', async (c, next) => {
const started = Date.now();
await next();
console.log(`app: ${c.req.method} ${c.req.path} → ${c.res.status} (${Date.now() - started}ms)`);
});
app.get('/api/ping', (c) => c.json({ pong: true, at: new Date().toISOString() }));
app.route('/', flue());
export default app;
```
<ResponseField name="default export" type="Hono | { fetch(request) }">
Must expose a `fetch` method. On Cloudflare, `fetch(request, env, ctx)` is supported. Invalid exports fail at startup with a clear error.
</ResponseField>
When `app.ts` is absent, the generated entry calls `createDefaultFlueApp()`, which mounts `flue()` at `/` without requiring `hono` in user code.
<Note>
`flue.config.ts` holds build knobs (`target`, `root`, `output`). Provider and model registration belong in `app.ts` because API keys and bindings are runtime concerns.
</Note>
The same `app.ts` shape works on Node and Cloudflare. For Cloudflare-specific provider overrides (for example AI Gateway), register providers in `app.ts` — user imports run before auto-registration.
## Generated server entry
`flue build` does not execute user modules directly. It:
1. Discovers modules under `sourceRoot`.
2. Generates an entry that imports each agent, workflow, and channel.
3. Normalizes exports via `normalizeBuiltModules(...)`.
4. Configures `@flue/runtime` with handlers, middleware, persistence, and sandboxes.
5. Composes or defaults the HTTP app.
6. Writes `dist/server.mjs` (Node) or Cloudflare Vite inputs under `.flue-vite/`.
```mermaid
flowchart TB
subgraph project["Project root"]
config["flue.config.*"]
source["sourceRoot<br/>.flue/ or src/ or root"]
end
subgraph discovered["CLI discovery"]
agents["agents/*.ts"]
workflows["workflows/*.ts"]
channels["channels/*.ts"]
app["app.ts optional"]
db["db.ts optional Node"]
cf["cloudflare.ts optional CF"]
end
subgraph generated["Generated entry"]
norm["normalizeBuiltModules"]
runtime["configureFlueRuntime"]
http["app.fetch or createDefaultFlueApp"]
end
config --> source
source --> discovered
discovered --> norm
norm --> runtime
app --> http
db --> runtime
runtime --> http
http --> artifact["dist/server.mjs or Worker"]
```
## Comparison with Eve layout
In this workspace, **Eve** (`vercel-eve`) uses a single `agent/` slot with path-derived names (`agent/tools/get_weather.ts` → `get_weather`). **Flue** uses plural directories under `sourceRoot` with filename-derived names and separate workflow and channel trees.
| Concern | Flue | Eve |
| --- | --- | --- |
| Authored root | `.flue/`, `src/`, or project root | `agent/` |
| Agents | `agents/<name>.ts` default-export `createAgent` | `agent/agent.ts` + instructions/tools/skills slots |
| Automations | `workflows/<name>.ts` exports `run` | Schedules and session turns |
| Build config | `flue.config.*` at project root | `eve` CLI + compiled `.eve/` |
| HTTP composition | Optional `app.ts` + `flue()` | Stable HTTP session API |
Both frameworks stay provider-neutral: model and tool wiring are project-owned, not locked to a single vendor.
## Scaffold and verify
<Steps>
<Step title="Initialize config">
Run `flue init --target node` (or `cloudflare`) to write `flue.config.ts` with `defineConfig` from `@flue/cli/config`.
</Step>
<Step title="Add modules under source root">
Create `src/agents/` and `src/workflows/` (or `.flue/…` if using that layout). Add at least one agent or workflow file.
</Step>
<Step title="Optional app.ts">
Add `src/app.ts` when you need custom routes, middleware, or `registerProvider` calls.
</Step>
<Step title="Build and inspect discovery">
Run `flue build --target node`. Build output lists `source`, `agents`, `workflows`, and `channels` names discovered from `sourceRoot`.
</Step>
</Steps>
<RequestExample>
```bash
flue build --target node
```
</RequestExample>
<ResponseExample>
```text
flue build
target node
output dist
source src
app src/app.ts
agents
session-test
workflows
hello
with-subagent
...
```
</ResponseExample>
<Tip>
Run `flue dev` to start the dev server with the same discovery rules. Edits under `agents/`, `workflows/`, `channels/`, `app.ts`, or `cloudflare.ts` trigger rebuilds.
</Tip>
## Related pages
<CardGroup>
<Card title="Flue quickstart" href="/flue-quickstart">
Scaffold a project, run `flue dev`, invoke workflows, and connect to agents.
</Card>
<Card title="Configuration reference" href="/flue-configuration-reference">
`flue.config.*` keys, source-root precedence, env loading, and CLI overrides.
</Card>
<Card title="Build Flue agents" href="/build-flue-agents">
`createAgent`, profiles, tools, skills, sandboxes, and route handlers.
</Card>
<Card title="Flue workflows" href="/flue-workflows">
Define `run()`, initialize agents, and inspect runs with `flue logs`.
</Card>
<Card title="Eve project layout" href="/eve-project-layout">
Contrast Eve's `agent/` slot and path-derived naming with Flue's module directories.
</Card>
</CardGroup>
---
## 06. Eve project layout
> Authored slots under `agent/`, path-derived naming, workspace seeding rules, and subagent package boundaries.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/06-eve-project-layout.md
- Generated: 2026-06-18T19:26:07.989Z
### Source Files
- `vercel-eve:docs/reference/project-layout.md`
- `vercel-eve:docs/agent-config.md`
- `vercel-eve:docs/meta.json`
- `vercel-eve:apps/fixtures/weather-agent/agent/instructions.md`
- `vercel-eve:apps/fixtures/weather-agent/agent/skills/get-weather.md`
- `vercel-eve:apps/fixtures/weather-agent/agent/tools/get_weather.ts`
---
title: "Eve project layout"
description: "Authored slots under `agent/`, path-derived naming, workspace seeding rules, and subagent package boundaries."
---
Eve treats the filesystem as the agent contract: discovery walks `agent/` (or a flat app root), classifies each entry into an authored slot, derives stable names from paths, and compiles inspectable artifacts under `.eve/`. The root agent id comes from `package.json` `name` (or the app-root directory name); subagent ids come from their directory names under `subagents/`.
## Nested vs flat layout
Eve resolves two project layouts from the current working directory upward.
| Layout | `appRoot` | `agentRoot` | When to use |
| ------ | --------- | ----------- | ----------- |
| **Nested** (recommended) | Directory with `package.json` or `vercel.json` | `agent/` child of `appRoot` | Keeps npm project metadata separate from the authored surface |
| **Flat** | Same as `agentRoot` | App root containing recognized slot entries | Supported when the app root is also the agent root |
:::files
my-agent/
├── package.json
├── tsconfig.json
├── agent/
│ ├── agent.ts
│ ├── instructions.md
│ ├── tools/
│ ├── skills/
│ ├── connections/
│ ├── sandbox/
│ ├── channels/
│ ├── subagents/
│ ├── schedules/
│ ├── hooks/
│ └── lib/
└── evals/
:::
`evals/` lives at the app root as a sibling of `agent/`, not inside it. A flat layout co-locates slot files at the app root (`agent.ts`, `instructions.md`, `tools/`, etc.) when no `agent/` directory is present.
Compared to Flue, which resolves a configurable source root (`.flue/` or `src/`) and composes agents through `app.ts`, Eve keeps the authored surface in `agent/` slots that discovery walks directly. See [Flue project layout](/flue-project-layout) for the parallel model.
## Path-derived naming
Identity comes from the path. You never write a `name` or `id` field on a `define*` call — the compiler rejects authored `name` fields on tools.
| Authored path | Resolves to | Notes |
| ------------- | ----------- | ----- |
| `agent/tools/get_weather.ts` | tool `get_weather` | Nested paths flatten: `tools/billing/refund.ts` → `billing-refund` |
| `agent/connections/linear.ts` | connection `linear` | Connection tools appear as `connection__<connection>__<tool>` |
| `agent/skills/get-weather.md` | skill `get-weather` | Flat `.md` or module; or packaged `skills/<name>/SKILL.md` |
| `agent/subagents/researcher/agent.ts` | subagent `researcher` | Bare name in the runtime tool namespace — no prefix |
| `agent/hooks/on-turn.ts` | hook `on-turn` | Recursive directories supported |
**Root agent id:** `package.json` `name` when present, otherwise `basename(appRoot)`. In nested layout with `agent/` as the agent root, the package name wins over the directory basename (so CI paths like `/vercel/path0` do not become the agent id).
**Subagent id:** the directory name under `subagents/<id>/`, or the module basename for single-file subagents (`subagents/researcher.ts`).
Subagent tool names share the same namespace as authored tools. A subagent named `researcher` collides with a tool named `researcher`; Eve rejects the build rather than picking a winner.
## Authored slot reference
Each top-level entry under `agent/` (or a subagent package root) maps to one slot. Discovery classifies entries and emits diagnostics for unsupported or misplaced files.
| Slot path | Purpose | Subagents | Root-only |
| --------- | ------- | --------- | --------- |
| `agent.ts` | Runtime config via `defineAgent` (model, compaction, build, experimental) | Yes | — |
| `instructions.md` / `instructions.ts` / `instructions/` | Base system prompt | Optional | Required on root |
| `instrumentation.ts` | OTel exporter and AI SDK span settings | No | Yes |
| `channels/` | HTTP and messaging entrypoints | No | Yes |
| `connections/` | External MCP or OpenAPI connections | Yes | — |
| `hooks/` | Lifecycle and stream-event subscribers | Yes | — |
| `skills/` | On-demand procedures and capability packs | Yes | — |
| `lib/` | Shared helper modules (import-only) | Yes | — |
| `sandbox.ts` or `sandbox/sandbox.ts` | Single sandbox override | Yes | — |
| `sandbox/workspace/**` | Files seeded into the sandbox | Yes | — |
| `tools/` | Typed executable integrations | Yes | — |
| `schedules/` | Recurring jobs (`.ts` module or `.md` with `cron:` frontmatter) | No | Yes |
| `subagents/` | Specialist child agents | Yes | — |
`agent.ts` is optional on the root agent. When omitted, Eve defaults to `anthropic/claude-sonnet-4.6`. When present, `model` is required. Subagents always require `agent.ts` with a `description` so the parent can decide when to delegate.
### Weather-agent fixture
The `weather-agent` fixture shows the minimal nested layout:
```text
weather-agent/
├── package.json # name: "weather-agent"
└── agent/
├── agent.ts # defineAgent({ model: "openai/gpt-5.5", ... })
├── instructions.md # always-on system prompt
├── skills/
│ └── get-weather.md
└── tools/
└── get_weather.ts
```
The skill frontmatter supplies a `description`; the tool uses `defineTool` with a Zod `inputSchema`. Discovery maps `get-weather.md` → skill `get-weather` and `get_weather.ts` → tool `get_weather`.
## Workspace seeding rules
Eve does not mount the entire `agent/` tree into the sandbox. Only two authored sources land in the runtime workspace:
| Source | Sandbox path | When |
| ------ | ------------ | ---- |
| `skills/` files | `/workspace/skills/<skill-id>/...` | Session bootstrap (and prewarm templates) |
| `sandbox/workspace/**` | `/workspace/...` | Session bootstrap |
Everything under `lib/` stays import-only TypeScript and never reaches the workspace. Skills are materialized as packages under `/workspace/skills/<name>/` with at minimum a `SKILL.md` body; the runtime `loadSkill` tool reads from that path and strips YAML frontmatter before returning markdown to the model.
Use top-level `sandbox.ts` for a definition-only sandbox override. Use `sandbox/sandbox.ts` plus `sandbox/workspace/**` when you also need seeded files. When neither is authored, the framework default sandbox applies.
Subagents seed their own skills and workspace files independently — they do not inherit the parent's sandbox contents. Skill seeds differ per agent node, so a declared subagent with its own `skills/` directory gets its own `/workspace/skills/...` tree.
## Subagent package boundaries
A declared subagent lives under `agent/subagents/<id>/` and is discovered as its own agent root. Its location under `subagents/` is the only marker that makes it a subagent.
:::files
agent/subagents/researcher/
├── agent.ts # required — must export description
├── instructions.md # optional (unlike root)
├── connections/
├── hooks/
├── skills/
├── lib/
├── sandbox/
├── tools/
└── subagents/ # nested subagents supported
:::
**Isolation rule:** a declared subagent inherits nothing from the root's authored slots. Discovery treats `subagents/<id>/` as a fresh agent root. An absent slot falls back to the framework default, not the parent's version.
| Slot | Declared subagent | Built-in `agent` tool (copy of parent) |
| ---- | ----------------- | -------------------------------------- |
| Instructions | Own `instructions.{md,ts}`, optional | Inherited |
| Tools, connections, skills, hooks | Own directories | Inherited |
| Sandbox | Own `sandbox/`, else framework default | Shared with parent |
| Channels, schedules | Not supported | Root-only |
| State (`defineState`) | Fresh per child | Fresh per child |
`agent.ts` is required on every declared subagent. The compiler rejects subagents missing `description` on `defineAgent`. Remote subagent definitions (`kind: "remote"`) cannot include local package entries such as `tools/` or `skills/`.
The built-in `agent` tool is the exception: children are copies of the same agent, sharing the parent's sandbox and tools. An authored tool at `agent/tools/agent.ts` takes priority over the built-in.
Nested subagents compile depth-first: each subagent may declare further subagents under its own `subagents/` directory, producing a flat node graph with parent→child edges at compile time.
## Verify discovery
<Steps>
<Step title="Run eve info">
From the app root:
```bash
eve info
```
Lists the discovered surface: layout, model, instructions path, skill names, tool names, channels, and discovery diagnostics.
</Step>
<Step title="Inspect JSON output">
For scripting or CI checks:
```bash
eve info --json
```
Returns `appRoot`, `agentRoot`, `layout`, `skills`, `tools`, `diagnostics`, and paths to artifacts under `.eve/` (`discoveryManifest`, `compiledManifest`, `diagnostics`, `moduleMap`, `metadata`).
</Step>
<Step title="Fix discovery errors">
When a file is not discovered:
1. Confirm it sits in the correct slot per the slot table above.
2. Check root-vs-subagent boundaries (`channels/` and `schedules/` are root-only; `schedules/` inside a subagent is invalid).
3. Ensure the root agent has `instructions.md` (or `.ts` / directory form).
4. Re-run `eve build` or `eve dev` after fixes; inspect `.eve/` artifacts for the full diagnostic list.
</Step>
</Steps>
<ResponseField name="status" type="string">
Compile and discovery status. `unavailable` when artifacts have not been built yet.
</ResponseField>
<ResponseField name="diagnostics" type="object">
Error and warning counts from discovery. Non-zero errors block compilation.
</ResponseField>
## Related pages
<CardGroup cols={2}>
<Card title="Eve quickstart" href="/eve-quickstart">
Run `eve init`, start `eve dev`, and send the first session message.
</Card>
<Card title="Eve authoring surfaces" href="/eve-authoring-surfaces">
Configure `agent.ts`, instructions, tools, skills, connections, sandbox, and subagents from the filesystem contract.
</Card>
<Card title="Eve weather fixture" href="/eve-weather-fixture">
Walk through the `weather-agent` fixture end to end.
</Card>
<Card title="Flue project layout" href="/flue-project-layout">
Compare Eve's `agent/` slots with Flue's source-root resolution and `app.ts` composition.
</Card>
</CardGroup>
---
## 07. Runtime models
> Compare Flue sessions, workflow runs, and dispatch receipts with Eve sessions, turns, `continuationToken`, and `sessionId` contracts.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/07-runtime-models.md
- Generated: 2026-06-18T19:26:10.559Z
### Source Files
- `withastro-flue:AGENTS.md`
- `withastro-flue:packages/runtime/src/runtime/flue-app.ts`
- `withastro-flue:packages/runtime/src/runtime/handle-agent.ts`
- `vercel-eve:docs/concepts/execution-model-and-durability.md`
- `vercel-eve:docs/concepts/sessions-runs-and-streaming.md`
- `vercel-eve:README.md`
---
title: "Runtime models"
description: "Compare Flue sessions, workflow runs, and dispatch receipts with Eve sessions, turns, `continuationToken`, and `sessionId` contracts."
---
Flue and Eve both model long-lived agent work, but they partition identifiers and persistence differently. Flue keeps **workflow runs** (`runId`, `/runs/:runId`) separate from **continuing agent sessions** (`harness.session()`, `instanceId`, `submissionId`, `dispatchId`). Eve unifies durable conversation around a single **`sessionId`** for streaming and a channel-owned **`continuationToken`** for follow-up delivery; each user message runs as a durable **turn** checkpointed at **steps**.
## Side-by-side contracts
| Concept | Flue | Eve |
| --- | --- | --- |
| Long-lived conversation | `AgentInstance` → `Harness` → named `Session` (default `"default"`) | Durable `session` spanning days; `sessionId` is the stream handle |
| One user input unit | `Operation` (`prompt` / `skill` / `task` / `shell`) containing one or more `Turn`s (LLM round-trips) | `turn`: one user message and all model/tool work until the agent responds |
| Durable checkpoint inside a unit | Turn journal inside session history (submission reconciliation) | `step`: one model call and its tool calls; Workflow SDK checkpoints |
| Finite orchestration | `Workflow` → `run()` with unique `ctx.id === runId` | Not authored directly; workflow primitives are runtime-internal |
| Async ingress receipt | `DispatchReceipt` with `dispatchId` (not a `runId`) | N/A — channels deliver via `continuationToken` |
| HTTP prompt admission | `202` with `streamUrl`, `offset`, `submissionId` | `200`/`JSON` with `sessionId`, `continuationToken` |
| Stream protocol | Durable Streams on agent URL or `/runs/:runId` | NDJSON at `/eve/v1/session/:sessionId/stream` |
| Follow-up handle | Same `instanceId` + session name; no token rotation | Current `continuationToken`; stale tokens rejected |
| Terminal “ready for input” | Session returns to idle; no dedicated event name | `session.waiting` on the NDJSON stream |
<Warning>
In Flue, **`runId` is workflow-only**. Direct HTTP prompts, WebSocket traffic, channel dispatch, and `dispatch(...)` operate inside persistent agent sessions and must not be described as runs. `/runs` and `flue logs` inspect workflow runs only.
</Warning>
## Flue runtime model
Flue nests work from reusable profiles down to LLM turns:
```text
Agent profile (defineAgentProfile)
└─ Created agent (createAgent)
└─ AgentInstance (URL :id)
└─ Harness (init(); default name "default")
└─ Session (harness.session(name?); default "default")
└─ Operation (session.prompt | skill | task | shell)
└─ Turn (one LLM round-trip inside pi-agent-core)
Workflow (workflows/<name>.ts, exports run)
└─ Workflow run / invocation (unique ctx.id === runId)
```
### Agent sessions and operations
After `ctx.init(agent)` returns a harness, `harness.session()` get-or-creates named conversation state. Each `session.prompt()`, `skill()`, `task()`, or `shell()` call is one **operation** with a generated `operationId`. Operations may emit multiple **turn** events (`turn_start`, `turn`, tool events) before completing.
Sessions persist history so later operations continue with prior context. With a `db.ts` `PersistenceAdapter`, session rows and submission journals survive process restarts; without one, Node keeps state in process memory.
### Workflow runs
`POST /workflows/:name` admits a new run. The handler receives `FlueContext` where `ctx.id` and `runId` identify the invocation. Admission returns:
<ResponseField name="runId" type="string">
Unique workflow run identifier. On Cloudflare, one workflow Durable Object instance per run — `instanceId` equals `runId`.
</ResponseField>
<ResponseField name="streamUrl" type="string">
Absolute Durable Streams URL under the mount prefix at `/runs/:runId`.
</ResponseField>
<ResponseField name="offset" type="string">
Stream offset captured at admission; reading from it replays the full run.
</ResponseField>
Run lifecycle events are `run_start`, optional `run_resume` (recovery), workflow body events, then terminal `run_end`. `RunStore` records status (`active` | `completed` | `errored`). `GET /runs/:runId?meta` returns `RunRecord` JSON; `GET /runs/:runId` reads the Durable Streams event log.
### Direct HTTP agent prompts
`POST /agents/:name/:id` accepts a JSON body `{ "message": string, "images"?: image[] }`. Default mode returns `202` with stream coordinates on the same URL:
<ResponseField name="streamUrl" type="string">
Agent event stream at the request URL (query stripped).
</ResponseField>
<ResponseField name="offset" type="string">
Current Durable Streams offset; `-1` before the first accepted prompt creates the stream.
</ResponseField>
<ResponseField name="submissionId" type="string">
Durable submission identifier for this admitted prompt.
</ResponseField>
`?wait=result` blocks until the operation completes and returns `{ result, streamUrl, offset, submissionId }`. `GET`/`HEAD` on the same path reads the agent Durable Streams log. Stream reads return `404` until the first prompt is accepted.
### `dispatch(...)` receipts
Application code calls `dispatch(agent, { id, input })` or `dispatch({ agent, id, input })` to queue asynchronous input to a continuing instance:
```ts
interface DispatchReceipt {
dispatchId: string;
acceptedAt: string;
}
```
<ParamField body="id" type="string" required>
Target agent instance id.
</ParamField>
<ParamField body="input" type="unknown" required>
JSON-serializable payload snapshotted at admission. Use `null` for an intentional empty payload.
</ParamField>
`await dispatch(...)` resolves when the runtime **admits and queues** the input — not when the model replies. The returned `dispatchId` identifies delivery; it is **not** a workflow `runId` and does not create run history. Dispatched events may carry `dispatchId` on the agent stream; correlate external side effects with this id for idempotency.
On Node, the default queue is process-lifetime memory. With a durable `db.ts` adapter, dispatches share the same ordered submission lifecycle as direct HTTP prompts. On Cloudflare, admission is durable to the agent Durable Object with at-least-once processing — design external effects to be idempotent.
### Flue event correlation
| Field | Applies to | Meaning |
| --- | --- | --- |
| `runId` | Workflow runs only | Persisted workflow event identity with `eventIndex` |
| `instanceId` | Agent activity | Agent instance URL segment |
| `submissionId` | Durable agent submissions | HTTP prompt or dispatch admission row |
| `dispatchId` | Dispatched input | Delivery identifier (may equal `submissionId` on replay) |
| `session` | Agent activity | Harness session name |
| `harness` | Agent activity | Harness name |
| `operationId` / `turnId` | Agent activity | Generated per operation / LLM turn |
Agent streams and `observe()` deliver live activity; workflow streams persist immutable `runId` + `eventIndex` pairs.
```mermaid
flowchart TB
subgraph flue_agent["Flue — continuing agent"]
HTTP["POST /agents/:name/:id"]
DISP["dispatch(...)"]
Q["Per-instance submission queue"]
SESS["Harness → Session history"]
ASTR["DS stream at /agents/:name/:id"]
HTTP --> Q
DISP --> Q
Q --> SESS
SESS --> ASTR
end
subgraph flue_wf["Flue — workflow run"]
WHTTP["POST /workflows/:name"]
RUN["run(ctx) handler"]
RSTORE["RunStore + run_start/run_end"]
RSTR["DS stream at /runs/:runId"]
WHTTP --> RUN
RUN --> RSTORE
RUN --> RSTR
end
```
## Eve runtime model
Eve treats every conversation as a **durable session** backed by the Workflow SDK. Work nests in three levels:
- **session** — whole durable conversation; survives restarts and redeploys
- **turn** — one user message and all triggered model/tool work until the agent responds
- **step** — durable checkpoint inside a turn (one model call and its tool calls)
Channels normalize transport and own delivery policy. The harness executes one unit of AI work. The runtime persists state, follows workflow `next` hooks, and streams NDJSON events.
```text
Channel (owns continuationToken, auth, delivery)
→ send(message, { continuationToken, auth })
→ Runtime (owns sessionId, event stream, workflow lifecycle)
→ Harness (one turn's tool loop)
→ Step checkpoints (Workflow SDK)
```
### Two handles
Eve deliberately splits resume from inspection:
| Handle | Owner | Job |
| --- | --- | --- |
| `continuationToken` | Channel | Resume handle for the next user message. Namespaced as `<channel>:<rawToken>` (e.g. `eve:7f3c...`). One active continuation per session; stale tokens are rejected. |
| `sessionId` | Runtime | Stream-and-inspect handle. Attach to `/eve/v1/session/:sessionId/stream`. Also referred to as `runId` in docs when describing the active workflow run. |
<Note>
Mixing these handles is the most common integration mistake. POST follow-ups with `continuationToken`; stream and reconnect with `sessionId`.
</Note>
### HTTP session routes
| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/eve/v1/session` | Start a session |
| `POST` | `/eve/v1/session/:sessionId` | Send follow-up (requires current `continuationToken`) |
| `GET` | `/eve/v1/session/:sessionId/stream` | NDJSON event stream (`?startIndex=` for reconnect) |
<RequestExample>
```bash
curl -X POST http://127.0.0.1:3000/eve/v1/session \
-H 'content-type: application/json' \
-d '{"message":"Summarize the latest forecast."}'
```
</RequestExample>
<ResponseExample>
```json
{
"ok": true,
"sessionId": "ses_01h...",
"continuationToken": "eve:7f3c..."
}
```
</ResponseExample>
The response also sets `x-eve-session-id`. After `session.waiting` appears on the stream, send the next message:
<RequestExample>
```bash
curl -X POST http://127.0.0.1:3000/eve/v1/session/ses_01h... \
-H 'content-type: application/json' \
-d '{"continuationToken":"eve:7f3c...","message":"Now send the short version."}'
```
</RequestExample>
### Turns, steps, and parked work
Each turn runs as a durable workflow. Completed steps never re-run; interrupted steps re-run, so non-idempotent side effects need gating or idempotency keys.
When a turn waits on human approval, OAuth, or a subagent, the workflow **parks**. The stream emits `session.waiting`. Delivery to the current `continuationToken` wakes the session and starts the next turn.
<Warning>
Eve does not maintain a durable FIFO message queue per session. `continuationToken` is a workflow resume hook, not a general queue address. For deterministic ordering, send one message at a time and wait for `session.waiting` before the next delivery to the same session.
</Warning>
### Eve stream events (selected)
| Event | Boundary |
| --- | --- |
| `session.started` | Session created |
| `turn.started` / `turn.completed` / `turn.failed` | Turn boundaries |
| `step.started` / `step.completed` / `step.failed` | Step checkpoints |
| `message.received` | Inbound user message accepted |
| `input.requested` | HITL pause |
| `subagent.called` / `subagent.completed` | Delegation (`childSessionId` for child stream) |
| `session.waiting` | Parked, ready for next input |
| `session.completed` / `session.failed` | Terminal session states |
Subagents receive their own durable `sessionId` and stream; the parent emits `subagent.called` with `childSessionId`.
```mermaid
sequenceDiagram
participant Client
participant Channel as Eve channel
participant Runtime
participant Stream as NDJSON stream
Client->>Channel: POST /eve/v1/session {message}
Channel->>Runtime: send(message, {continuationToken})
Runtime-->>Client: sessionId + continuationToken
Client->>Stream: GET /session/:sessionId/stream
Stream-->>Client: turn.started … step.* … session.waiting
Client->>Channel: POST /session/:sessionId {continuationToken, message}
Channel->>Runtime: resume hook
Stream-->>Client: turn.started … session.waiting
```
## Mapping concepts across frameworks
| Integration question | Flue | Eve |
| --- | --- | --- |
| “Where is my conversation?” | `agents/:name/:id` + session name | `sessionId` |
| “How do I send the next message?” | `POST /agents/:name/:id` or `dispatch(...)` to same `id` | `POST /eve/v1/session/:sessionId` with current `continuationToken` |
| “How do I watch progress?” | Durable Streams on agent URL or `/runs/:runId` | `GET /eve/v1/session/:sessionId/stream` |
| “How do I run a one-shot job?” | `POST /workflows/:name` → `runId` | Sessions are conversational; schedules dispatch new `sessionId`s |
| “How do I correlate async ingress?” | `dispatchId` on receipt and events | Channel-local token → namespaced `continuationToken` |
| “When is it safe to send again?” | After prior operation/submission settles (stream idle or `submission_settled`) | After `session.waiting` |
Both frameworks separate **transport admission** from **durable execution**, but the handles differ. Flue exposes `submissionId`/`dispatchId` for agent submissions and reserves `runId` for workflows. Eve rotates `continuationToken` per channel policy while keeping a stable `sessionId` for the event log.
## Durability defaults
| Target | Flue agent sessions | Flue workflow runs | Eve sessions |
| --- | --- | --- | --- |
| Node default | In-memory; optional `db.ts` adapter | `RunStore` + DS stream in-process | Workflow SDK durability when deployed with supported backend |
| Cloudflare | Agent DO SQLite + durable submission queue | Per-run workflow DO | Platform workflow persistence |
Flue agent recovery reconciles submissions conservatively: completed work is recognized, uncertain tool outcomes are not replayed blindly, and `submission_settled` notifies detached stream readers. Eve replays from the last completed step after crash, timeout, or redeploy.
## Related pages
<CardGroup>
<Card title="Flue HTTP API reference" href="/flue-http-api-reference">
Mounted routes, admission envelopes, Durable Streams reads, and error shapes for agents, workflows, and runs.
</Card>
<Card title="Flue persistence reference" href="/flue-persistence-reference">
`PersistenceAdapter`, session stores, and `db.ts` discovery for durable agent execution.
</Card>
<Card title="Flue workflows" href="/flue-workflows">
Author `run()`, invoke via CLI or HTTP, and inspect events with `flue logs`.
</Card>
<Card title="Eve sessions and streaming" href="/eve-sessions-streaming">
NDJSON events, `continuationToken` follow-ups, reconnect rules, and client integration.
</Card>
<Card title="Eve HTTP protocol reference" href="/eve-http-protocol-reference">
Session routes, event types, and stream parameters in full reference form.
</Card>
<Card title="Build Flue agents" href="/build-flue-agents">
`createAgent`, harnesses, sessions, tools, and HTTP route handlers.
</Card>
</CardGroup>
---
## 08. Build Flue agents
> Author agents with `createAgent`, `defineAgentProfile`, tools, skills, sandboxes, providers, and HTTP route handlers.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/08-build-flue-agents.md
- Generated: 2026-06-18T19:26:40.209Z
### Source Files
- `withastro-flue:README.md`
- `withastro-flue:packages/runtime/src/index.ts`
- `withastro-flue:packages/runtime/src/agent-definition.ts`
- `withastro-flue:packages/runtime/src/tool.ts`
- `withastro-flue:examples/hello-world/src/agents/session-test.ts`
- `withastro-flue:examples/hello-world/src/app.ts`
- `withastro-flue:examples/imported-skill/README.md`
---
title: "Build Flue agents"
description: "Author agents with `createAgent`, `defineAgentProfile`, tools, skills, sandboxes, providers, and HTTP route handlers."
---
Flue agents are TypeScript modules under `agents/` that default-export a `createAgent(...)` initializer. The CLI discovers modules at build time, the runtime calls the initializer whenever it prepares a harness, and an optional named `route` export opts the agent into HTTP transport at `/agents/:name/:id`.
## Agent module contract
The build step scans `agents/` for `.ts`, `.js`, `.mts`, or `.mjs` files. Each basename becomes the agent name (for example, `agents/session-test.ts` → `session-test`). Agent names must be non-empty and cannot contain `:`.
<ParamField body="default export" type="CreatedAgent" required>
Must be the return value of `createAgent(...)`. The initializer function is required.
</ParamField>
<ParamField body="route" type="AgentRouteHandler">
Optional Hono middleware (`MiddlewareHandler`). When present, the agent is HTTP-exposed and user middleware runs on every agent request—including stream reads.
</ParamField>
<ParamField body="description" type="string">
Optional non-empty string included in the agent manifest.
</ParamField>
:::files
agents/
assistant.ts # default export: createAgent(...)
session-test.ts # optional route export for HTTP
app.ts # registerProvider, mount flue()
flue.config.ts # build-time target/root/output only
:::
<Note>
A created agent is addressed by its module filename, not a `name` field on the runtime config. Use `defineAgentProfile({ name: '...' })` only for subagent selection via `session.task()`.
</Note>
## `createAgent` and `defineAgentProfile`
`createAgent(initialize)` returns a frozen `CreatedAgent` whose `initialize` function runs whenever the runtime initializes a harness: on workflow `ctx.init()` and when preparing a direct agent interaction. It is not a one-time constructor for a persistent instance id.
`defineAgentProfile(profile)` validates and returns a reusable `AgentProfile`. Profiles serve as baselines for `createAgent(() => ({ profile }))` or as named subagents for `session.task({ agent: '...' })`.
<CodeGroup>
```ts title="Minimal addressable agent"
import { type AgentRouteHandler, createAgent, defineAgentProfile } from '@flue/runtime';
export const route: AgentRouteHandler = async (_c, next) => next();
const sessionTest = defineAgentProfile({
instructions: 'You are a test agent for session-oriented message delivery.',
});
export default createAgent(() => ({ profile: sessionTest }));
```
```ts title="Full harness composition"
import { createAgent } from '@flue/runtime';
import { local } from '@flue/runtime/node';
import triage from '../skills/triage/SKILL.md' with { type: 'skill' };
import * as githubTools from '../tools/github.ts';
export default createAgent(() => ({
model: 'anthropic/claude-sonnet-4-6',
instructions: 'Triage a bug report end-to-end...',
tools: [...githubTools],
skills: [triage],
sandbox: local(),
}));
```
</CodeGroup>
### Runtime config fields
`createAgent` initializers return an `AgentRuntimeConfig`. Top-level fields replace or extend values from an optional `profile`.
| Field | Type | Purpose |
| --- | --- | --- |
| `profile` | `AgentProfile` | Reusable baseline merged with top-level fields |
| `model` | `string \| false` | Default model as `provider-id/model-id`; `false` requires call-site model |
| `instructions` | `string` | Prepended ahead of discovered workspace context |
| `tools` | `ToolDefinition[]` | Custom model-callable tools |
| `skills` | `Skill[]` | Registered skills (imports or inline metadata) |
| `subagents` | `AgentProfile[]` | Named profiles for `session.task()` |
| `thinkingLevel` | `ThinkingLevel` | Default reasoning effort (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`) |
| `compaction` | `false \| CompactionConfig` | Automatic context compaction tuning |
| `durability` | `DurabilityConfig` | Recovery attempts and submission timeout |
| `cwd` | `string` | Working directory inside the sandbox |
| `sandbox` | `SandboxFactory` | Environment factory for shell and filesystem work |
<Warning>
Subagent profiles must not declare `durability`. Delegated task sessions run inside the parent operation; configure durability on the created agent instead.
</Warning>
## Tools
`defineTool({ name, description, parameters, execute })` validates and normalizes a tool definition. Valibot `parameters` must be a top-level `v.object({ ... })`; they are converted to JSON Schema once at definition time. The wrapped `execute` validates model-supplied arguments before your callback runs; validation failures surface as error tool results the model can correct.
```ts
import { defineTool } from '@flue/runtime';
import * as v from 'valibot';
const calculator = defineTool({
name: 'calculator',
description: 'Perform arithmetic. Returns the numeric result as a string.',
parameters: v.object({
expression: v.pipe(v.string(), v.description('A math expression like "2 + 3"')),
}),
execute: async ({ expression }) => String(Function(`"use strict"; return (${expression})`)()),
});
```
Pass tools to the agent config or per-call via `session.prompt(..., { tools: [calculator] })`. Tool names must be unique within an agent's active tool list.
### MCP tools
`connectMcpServer(name, options)` adapts remote MCP tools into ordinary `ToolDefinition` values. Adapted names use `mcp__<server>__<tool>`. Close the returned connection when tools are no longer needed.
## Skills
Skills package reusable expertise. Two registration paths exist:
1. **Imported skill references** — import a `SKILL.md` with `{ type: 'skill' }`. The build packages the permitted skill directory (including sibling files like `CHECKLIST.txt`). Register the reference in `skills: [review]` to expose it to prompts and `session.skill()`.
2. **Workspace discovery** — the runtime discovers `AGENTS.md` and `.agents/skills/` from the session `cwd` at harness initialization.
```ts
import review from '../skills/review/SKILL.md' with { type: 'skill' };
const agent = createAgent(() => ({ model: 'anthropic/claude-haiku-4-5', skills: [review] }));
```
Invoke a named skill at runtime:
```ts
const { data } = await session.skill('greet', {
args: { name: 'World' },
result: v.object({ greeting: v.string() }),
});
```
<Info>
Merely importing an unregistered `SkillReference` does not expose its files to prompts. Add it to `skills` on the agent config or profile.
</Info>
## Subagents
Declare subagents as `AgentProfile` entries with a required `name`. Delegate work with `session.task()`:
```ts
const greeter = defineAgentProfile({
name: 'greeter',
instructions: 'Write one warm, concise greeting.',
});
const agent = createAgent(() => ({ model: 'anthropic/claude-sonnet-4-6', subagents: [greeter] }));
const { data } = await session.task(`Greet the user named "Developer".`, {
agent: 'greeter',
result: v.object({ greeting: v.string() }),
});
```
## Sandboxes
The `sandbox` field accepts a `SandboxFactory` that implements `createSessionEnv({ id })`. One env is created per `init()` call and shared across sessions and task sessions for that harness.
<Tabs>
<Tab title="Node local">
`local()` from `@flue/runtime/node` binds to the host filesystem and `child_process` on the Node target.
```ts
import { local } from '@flue/runtime/node';
createAgent(() => ({ sandbox: local(), model: 'anthropic/claude-sonnet-4-6' }));
```
</Tab>
<Tab title="In-process virtual">
`bash(() => new Bash({ fs }))` wraps a just-bash factory. Useful for deterministic, network-isolated tests.
```ts
import { bash, createAgent } from '@flue/runtime';
import { Bash, InMemoryFs } from 'just-bash';
const fs = new InMemoryFs();
createAgent(() => ({ sandbox: bash(() => new Bash({ fs })), model: false }));
```
</Tab>
<Tab title="Remote container">
Pass a custom `SandboxFactory` adapter (for example, Daytona) that maps a provider SDK into `SessionEnv`.
```ts
import { daytona } from '../sandboxes/daytona';
const sandbox = await client.create();
createAgent(() => ({ sandbox: daytona(sandbox), model: 'anthropic/claude-sonnet-4-6' }));
```
</Tab>
</Tabs>
Set `cwd` on the runtime config to scope relative paths inside the sandbox. Use `harness.fs` or `session.fs` for out-of-band plumbing the model should not see in the transcript.
## Providers
Model specifiers use `provider-id/model-id` (for example, `anthropic/claude-sonnet-4-6`, `ollama/llama3.1:8b`, `cloudflare/@cf/moonshotai/kimi-k2.6`). Provider registration is a runtime concern in `app.ts`, not `flue.config.ts`.
```ts
import { registerProvider } from '@flue/runtime';
import { flue } from '@flue/runtime/routing';
import { Hono } from 'hono';
registerProvider('ollama', {
api: 'openai-completions',
baseUrl: 'http://localhost:11434/v1',
});
if (process.env.ANTHROPIC_GATEWAY_URL) {
registerProvider('anthropic', {
baseUrl: process.env.ANTHROPIC_GATEWAY_URL,
apiKey: process.env.ANTHROPIC_API_KEY,
});
}
const app = new Hono();
app.route('/', flue());
export default app;
```
<ParamField body="registerProvider(providerId, registration)" type="function">
Registers or replaces a provider by ID. Catalog providers layer transport overrides on top of built-in metadata; unknown IDs require `api` and `baseUrl`.
</ParamField>
<ParamField body="registerApiProvider" type="function">
Registers a new wire-protocol handler, then associate it with `registerProvider({ api: '...' })`.
</ParamField>
<Tip>
Flue stays provider-neutral: register any OpenAI-compatible proxy, local inference server, or Cloudflare Workers AI binding without changing agent module code—only the `model` string and `app.ts` registration.
</Tip>
## HTTP route handlers
`AgentRouteHandler` is a Hono `MiddlewareHandler`. Export it as `route` from the agent module to enable HTTP transport and attach auth, rate limiting, or logging before Flue handles the request.
```ts
export const route: AgentRouteHandler = async (c, next) => {
const auth = c.req.header('authorization');
if (!auth) return c.json({ error: 'unauthorized' }, 401);
await next();
};
```
When `app.ts` exists, mount Flue explicitly:
```ts
app.route('/', flue());
```
Without `app.ts`, the generated server mounts `flue()` at `/` automatically.
### Agent HTTP API
:::endpoint POST /agents/:name/:id
Prompt an agent instance. Returns `202` with Durable Streams coordinates by default; add `?wait=result` for a synchronous JSON result.
:::
<ParamField body="name" type="string" required>
Discovered agent module basename.
</ParamField>
<ParamField body="id" type="string" required>
Caller-chosen agent instance id (for example, a thread or session key).
</ParamField>
<RequestExample>
```bash
curl -X POST http://localhost:3000/agents/session-test/thread-1 \
-H 'Content-Type: application/json' \
-d '{"message": "Hello"}'
```
</RequestExample>
<ResponseExample>
```json
{
"streamUrl": "/agents/session-test/thread-1",
"offset": "0"
}
```
</ResponseExample>
Request body shape:
<ParamField body="message" type="string" required>
Prompt text for the agent instance.
</ParamField>
<ParamField body="images" type="image[]">
Optional vision attachments as `{ type: 'image', data: base64, mimeType }`. Each `data` field is capped at 14 MiB.
</ParamField>
Stream events on the same URL via `GET` or `HEAD`. Agents without a `route` export are not HTTP-addressable (use `dispatch()` from application code instead).
## Verify locally
<Steps>
<Step title="Start the dev server">
```bash
flue dev --target node
```
</Step>
<Step title="Connect to an agent instance">
```bash
flue connect session-test local
```
`flue connect` builds and opens an interactive local Node connection. Cloudflare targets are not supported for connect.
</Step>
<Step title="Send an HTTP prompt">
With `route` exported, POST to `/agents/<name>/<id>` and read the event stream at the returned `streamUrl`.
</Step>
</Steps>
## Comparison with Eve authoring
In this workspace, **Flue** agents are TypeScript modules composed with `createAgent`, runtime provider registration, and optional Hono middleware. **Eve** (`vercel/eve`) uses a filesystem contract under `agent/` with `agent.ts`, instructions, tools, and skills as separate authored slots. Both frameworks are provider-neutral at the architecture level; Flue registers providers in `app.ts`, while Eve configures connections from its agent directory layout.
## Related pages
<CardGroup>
<Card title="Flue project layout" href="/flue-project-layout">
Source-root resolution, `agents/` discovery, and `app.ts` composition.
</Card>
<Card title="Flue quickstart" href="/flue-quickstart">
Scaffold, run `flue dev`, and connect with `flue connect`.
</Card>
<Card title="Flue workflows" href="/flue-workflows">
Initialize agents inside workflow `run()` with `ctx.init()`.
</Card>
<Card title="Flue HTTP API reference" href="/flue-http-api-reference">
Full route inventory, stream semantics, and error envelopes.
</Card>
<Card title="Flue deploy" href="/flue-deploy">
Register providers for production targets and build artifacts.
</Card>
<Card title="Eve authoring surfaces" href="/eve-authoring-surfaces">
Filesystem contract for Eve agents in the paired repository.
</Card>
</CardGroup>
---
## 09. Flue workflows
> Define workflow modules, initialize agents inside `run()`, invoke via CLI or HTTP, and inspect run events with `flue logs`.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/09-flue-workflows.md
- Generated: 2026-06-18T19:27:22.433Z
### Source Files
- `withastro-flue:examples/hello-world/src/workflows/hello.ts`
- `withastro-flue:examples/hello-world/src/workflows/with-subagent.ts`
- `withastro-flue:examples/braintrust/src/workflows/task.ts`
- `withastro-flue:packages/runtime/src/runtime/flue-app.ts`
- `withastro-flue:packages/runtime/src/runtime/inspect.ts`
- `withastro-flue:packages/cli/bin/flue.ts`
- `withastro-flue:packages/sdk/src/index.ts`
---
title: "Flue workflows"
description: "Define workflow modules, initialize agents inside `run()`, invoke via CLI or HTTP, and inspect run events with `flue logs`."
---
A Flue workflow is a discovered module under `workflows/` that exports a callable `run(ctx)` handler. Each invocation creates a workflow run with a unique `runId`, persists run metadata and a Durable Streams event log, and returns the handler's return value as the terminal result. Agents are initialized inside `run()` via `ctx.init(agent)`; HTTP exposure is optional and controlled by an exported `route` middleware.
## Module contract
The build discovers one workflow per source file in `workflows/` (or `.flue/workflows/` when that source root is active). The file basename becomes the workflow name: `workflows/hello.ts` registers as `hello`.
| Export | Required | Role |
| --- | --- | --- |
| `run` | Yes | Callable handler invoked for each workflow run. Must be a function. |
| `route` | No | Hono middleware. When present, the workflow opts into HTTP transport at `POST /workflows/:name`. |
Discovery validates the contract at build time. A missing or non-callable `run` export fails the build. A `route` export that is not a callable middleware fails the build.
```ts title="workflows/hello.ts"
import { createAgent, type FlueContext, type WorkflowRouteHandler } from '@flue/runtime';
export const route: WorkflowRouteHandler = async (_c, next) => next();
const agent = createAgent(() => ({ model: 'anthropic/claude-sonnet-4-6' }));
export async function run({ init, log, payload }: FlueContext<{ name?: string }>) {
const harness = await init(agent);
const session = await harness.session();
const { text } = await session.prompt(`Hello, ${payload.name ?? 'Developer'}!`);
log.info('greeting complete', { length: text.length });
return { greeting: text };
}
```
<Note>
Workflow names must be non-empty. Unlike agents and channels, workflow names may contain `:`.
</Note>
Export `route` when callers should reach the workflow over HTTP. The middleware runs before admission and is the right place for authentication, rate limits, or request rejection. On Node and Cloudflare, the same `route` export also guards `GET /runs/:runId` reads for runs owned by that workflow.
## `FlueContext` inside `run()`
`run()` receives a `FlueContext<TPayload, TEnv>` with the current invocation state. Type parameters for `payload` and `env` are compile-time only; Flue does not validate payload shape at runtime.
<ParamField body="id" type="string">
Workflow run ID for this invocation (same value as `runId` on the internal context).
</ParamField>
<ParamField body="payload" type="TPayload">
JSON body from the invoker. Defaults to `{}` when omitted.
</ParamField>
<ParamField body="env" type="TEnv">
Platform environment bindings. `process.env` on Node; Worker `env` on Cloudflare.
</ParamField>
<ParamField body="req" type="Request | undefined">
Standard Fetch `Request` for the current invocation. Undefined outside HTTP contexts. Body is single-use; call `req.clone()` to read it more than once.
</ParamField>
<ParamField body="log" type="FlueLogger">
Emits structured `log` events (`info`, `warn`, `error`) into the run's event stream during workflow execution.
</ParamField>
<ParamField body="init(agent, options?)" type="Promise<FlueHarness>">
Initializes a `createAgent(...)` value for this run. Each harness name may be initialized once per context. Returns a harness with `session()`, `shell()`, and `fs`.
</ParamField>
```ts title="workflows/with-request.ts"
export async function run({ req, init }: FlueContext) {
const authHeader = req?.headers.get('authorization');
if (!authHeader) return { skipped: true, reason: 'no authorization header' };
const harness = await init(agent);
const session = await harness.session();
const { text } = await session.prompt('Say hello in 5 words.');
return { skipped: false, text };
}
```
<Warning>
Do not import a workflow module and call `run()` directly from `app.ts`, schedulers, or channel handlers. That bypasses admission, durable run storage, route middleware, and run inspection APIs. Invoke the mounted `POST /workflows/:name` route, use `flue run`, or extract shared pure logic into a separate module.
</Warning>
## Initialize agents in `run()`
Workflows orchestrate finite agent-backed work. Define agents with `createAgent(() => ({ ... }))` at module scope, then call `init(agent)` inside `run()` to obtain a harness for that invocation.
```ts title="workflows/with-subagent.ts"
const greeter = defineAgentProfile({
name: 'greeter',
instructions: 'Write one warm, concise greeting.',
});
const agent = createAgent(() => ({ model: 'anthropic/claude-sonnet-4-6', subagents: [greeter] }));
export async function run({ init, payload }: FlueContext<{ name?: string }>) {
const harness = await init(agent);
const session = await harness.session();
const { data } = await session.task(`Greet the user named "${payload.name ?? 'Developer'}".`, {
agent: 'greeter',
result: v.object({ greeting: v.string() }),
});
return data;
}
```
Common harness patterns inside workflows:
| Method | Use when |
| --- | --- |
| `harness.session()` | Default conversational context for prompts, skills, and tasks. |
| `session.prompt(...)` | Model-backed work with optional structured `result` schema. |
| `session.task(..., { agent })` | Delegate to a configured subagent profile. |
| `harness.fs` / `harness.shell` | Stage inputs or inspect the sandbox without adding session messages. |
Prefer `init(agent)` and session APIs for model work. Calling a provider SDK directly bypasses Flue's tools, skills, sandbox, operation events, and response handling.
## Run lifecycle
Each invocation allocates a new `runId` (format `run_<ulid>`), records a `RunRecord`, and appends events to a Durable Streams log at `/runs/:runId`.
```mermaid
sequenceDiagram
participant Caller
participant API as flue() HTTP / flue run IPC
participant Admission as handleWorkflowRequest
participant Handler as workflows/:name run()
participant Store as runStore + eventStreamStore
Caller->>API: POST payload
API->>Admission: prepareWorkflowExecution
Admission->>Store: createRun, capture stream offset
Admission-->>Caller: 202 runId, streamUrl, offset
Admission->>Handler: startWorkflowExecution (background)
Handler->>Store: run_start, log/tool/text events
Handler->>Store: run_end + result or error
Caller->>Store: GET /runs/:runId (stream or ?meta)
```
<ResponseField name="status" type="RunStatus">
`active` while the handler runs; `completed` or `errored` after `run_end`.
</ResponseField>
<ResponseField name="result" type="unknown">
Return value from `run()` when `status` is `completed`.
</ResponseField>
<ResponseField name="error" type="unknown">
Terminal error payload when `status` is `errored`.
</ResponseField>
Terminal `run_end` events carry `durationMs`, `isError`, and either `result` or `error`. Agent prompts via `POST /agents/:name/:id` and `dispatch(...)` do not create workflow runs.
## Invoke workflows
### CLI: `flue run`
`flue run` builds the Node target, spawns a short-lived local process, invokes the named workflow once, streams events to stderr, and prints the terminal JSON result to stdout. It does not require HTTP `route` export.
<ParamField body="workflow" type="string" required>
Discovered workflow name (positional argument).
</ParamField>
<ParamField body="--payload" type="json">
JSON object passed as `ctx.payload`. Default: `{}`.
</ParamField>
<ParamField body="--target" type="node">
`flue run` supports Node only. Cloudflare targets must use `flue dev` plus HTTP `curl` or the SDK.
</ParamField>
<ParamField body="--root, --output, --config, --env" type="string">
Project resolution overrides. Same semantics as other application commands.
</ParamField>
<Steps>
<Step title="Start the dev server (HTTP path only)">
```bash
flue dev --target node
```
</Step>
<Step title="Run a workflow locally">
```bash
flue run hello --target node --payload '{"name": "World"}'
```
Expect a `run` line with `runId`, inline event output on stderr, JSON result on stdout, and `workflow completed` on success.
</Step>
<Step title="Inspect the same run against the dev server">
```bash
flue logs <runId>
```
`flue run` history is process-local. `flue logs` reads from a running server's `/runs/:runId` stream (default `http://127.0.0.1:4321`).
</Step>
</Steps>
<Info>
`flue run` uses IPC against `localWorkflowHandlers`, which includes every discovered workflow regardless of HTTP transport. HTTP-only registration applies to `workflowHandlers` exposed by `flue()`.
</Info>
### HTTP: `POST /workflows/:name`
HTTP invocation requires `export const route`. Without it, the workflow is CLI-only.
:::endpoint POST /workflows/:name
Start a workflow run. Default admission returns `202` with stream coordinates. Add `?wait=result` for a synchronous JSON response that includes the terminal result.
:::
<ParamField body="name" type="string" required>
Workflow name from discovery (path parameter).
</ParamField>
<ParamField body="body" type="object">
Workflow-defined JSON payload. Optional; treated as `{}` when omitted.
</ParamField>
<ParamField body="wait" type="query: result">
When set, blocks until the run finishes and returns `200` with `result`.
</ParamField>
<RequestExample>
```bash
curl -X POST http://localhost:4321/workflows/hello \
-H "Content-Type: application/json" \
-d '{"name": "World"}'
```
</RequestExample>
<ResponseExample>
```json
{
"runId": "run_01HXXXXXXXXXXXXXXXXXXXXXX",
"streamUrl": "http://localhost:4321/runs/run_01HXXXXXXXXXXXXXXXXXXXXXX",
"offset": "-1"
}
```
</ResponseExample>
Admission responses set `Location` and `Stream-Next-Offset` headers mirroring Durable Streams conventions. Observe events at `GET /runs/:runId`. Read run metadata with `GET /runs/:runId?meta`.
<CodeGroup>
```bash title="Async admission (202)"
curl -X POST http://localhost:4321/workflows/hello \
-H "Content-Type: application/json" \
-d '{}'
```
```bash title="Sync result (?wait=result)"
curl -X POST "http://localhost:4321/workflows/hello?wait=result" \
-H "Content-Type: application/json" \
-d '{"name": "World"}'
```
```ts title="SDK client"
import { createFlueClient } from '@flue/sdk';
const client = createFlueClient({ baseUrl: 'http://localhost:4321' });
const admitted = await client.workflows.invoke('hello', {
payload: { name: 'World' },
});
const finished = await client.workflows.invoke('hello', {
payload: { name: 'World' },
wait: 'result',
});
```
</CodeGroup>
## Inspect runs with `flue logs`
`flue logs` is read-only. It tails or replays workflow run events from a running Flue server via the Durable Streams protocol at `/runs/:runId`.
<ParamField body="runId" type="string" required>
Opaque workflow run ID printed by `flue run` or returned in HTTP admission.
</ParamField>
<ParamField body="--server" type="url">
Base URL of the running server. Default: `http://127.0.0.1:4321`.
</ParamField>
<ParamField body="--follow / --no-follow / -f" type="boolean">
Follow live events, or replay and exit. Default: follow when run `status` is `active`, otherwise replay.
</ParamField>
<ParamField body="--since" type="offset">
Resume after an opaque Durable Streams offset (for example from ndjson `offset` fields).
</ParamField>
<ParamField body="--types" type="comma-separated list">
Filter to event types such as `log`, `tool`, `run_end`.
</ParamField>
<ParamField body="--limit" type="positive integer">
Cap the number of emitted events (client-side).
</ParamField>
<ParamField body="--format" type="pretty | ndjson">
`pretty` renders human-readable stderr lines; `ndjson` prints one JSON event per stdout line with resume `offset`.
</ParamField>
```bash
flue logs run_01H... --types log,tool,run_end --format ndjson
flue logs run_01H... --no-follow
flue logs run_01H... -f --server http://localhost:4321
```
Pretty output includes `run:start`, `run:end`, operation banners, model text deltas, tool I/O, and `log` lines from `ctx.log`. Exit code `2` when the run ends with `isError: true`; `130` on SIGINT during follow mode.
| Surface | Purpose |
| --- | --- |
| `flue logs <runId>` | CLI tail or replay |
| `GET /runs/<runId>` | Durable Streams event read (catch-up, long-poll, SSE) |
| `GET /runs/<runId>?meta` | Plain JSON `RunRecord` |
| `client.runs.get/stream/events` | Application tooling via `@flue/sdk` |
| `listRuns()` / `getRun()` from `@flue/runtime` | Server-side inspection in custom admin routes |
Run listing and metadata can expose payloads, results, and model activity. Publish listing surfaces only behind authorization appropriate for that data.
## Failure modes
| Symptom | Likely cause |
| --- | --- |
| `Unknown workflow` on `flue run` | Name typo or module missing from `workflows/`. |
| `404` on `POST /workflows/:name` | Workflow exists but lacks `route` export (no HTTP transport). |
| `flue logs` cannot fetch run | Server not running, wrong `--server`, or `runId` from a finished `flue run` child (not published to the dev server). |
| `flue run --target cloudflare` rejected | `flue run` is Node-only; use `flue dev --target cloudflare` and HTTP/SDK invocation. |
| Build error on workflow module | `run` not exported, or `route` is not callable middleware. |
## Related pages
<CardGroup>
<Card title="Flue quickstart" href="/flue-quickstart">
Scaffold a project, run `flue dev`, and invoke your first workflow with `flue run`.
</Card>
<Card title="Flue project layout" href="/flue-project-layout">
Source-root resolution, `workflows/` discovery, and `app.ts` composition.
</Card>
<Card title="Build Flue agents" href="/build-flue-agents">
Author agents with `createAgent`, tools, skills, and subagent profiles used inside workflows.
</Card>
<Card title="Runtime models" href="/runtime-models">
Workflow runs versus agent sessions and dispatch receipts.
</Card>
<Card title="Flue CLI reference" href="/flue-cli-reference">
Full `flue run` and `flue logs` flag reference and exit behavior.
</Card>
<Card title="Flue HTTP API reference" href="/flue-http-api-reference">
`/workflows`, `/runs`, admission envelopes, and stream read semantics.
</Card>
</CardGroup>
---
## 10. Flue channels
> Discover channel modules, mount verified webhook routes under `/channels/:name`, and dispatch inbound events to agents.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/10-flue-channels.md
- Generated: 2026-06-18T19:27:22.840Z
### Source Files
- `withastro-flue:examples/slack-channel/src/channels/slack.ts`
- `withastro-flue:examples/github-channel/src/channels/github.ts`
- `withastro-flue:packages/slack/src/index.ts`
- `withastro-flue:packages/github/src/index.ts`
- `withastro-flue:packages/runtime/src/runtime/flue-app.ts`
- `withastro-flue:blueprints/channel.md`
- `withastro-flue:packages/cli/bin/flue.ts`
---
title: "Flue channels"
description: "Discover channel modules, mount verified webhook routes under `/channels/:name`, and dispatch inbound events to agents."
---
Flue discovers `channels/<name>.ts` modules at build time, registers each module's declared HTTP handlers under `/channels/<name><suffix>`, and routes verified provider webhooks through application callbacks that can call `dispatch(...)` to deliver JSON input into continuing agent sessions.
## Discovery and routing
During `flue build` and `flue dev`, the CLI scans `<source-root>/channels/` for `*.ts`, `*.mts`, `*.js`, or `*.mjs` files. Each basename becomes an immutable channel namespace. Channel names must be non-empty and cannot contain `:`.
:::files
<source-root>/
├── agents/
├── workflows/
└── channels/
├── slack.ts → /channels/slack/*
└── github.ts → /channels/github/*
:::
The source root resolves from `flue.config.*` (see [Flue project layout](/flue-project-layout)): prefer `<project>/.flue/`, then `<project>/src/`, then `<project>/`.
At runtime, `flue()` mounts channel traffic on two patterns:
| Pattern | Behavior |
| --- | --- |
| `* /channels/:name/:suffix` | Match a discovered handler by HTTP method and route suffix |
| `* /channels/:name` | Always `404` — the namespace itself is not an endpoint |
The generated entry normalizes each module's named `channel` export into `channelHandlers[name]`, keyed as `"<METHOD> <suffix>"` (for example `"POST /events"`). Handler lookup is exact: wrong suffix → `404`; wrong method with a matching suffix → `405` with an `Allow` header derived from declared routes.
<Note>
If `app.ts` mounts `flue()` under a prefix, channel URLs receive the same prefix as agents and workflows. Prefixing applies to all Flue routes; one channel cannot be relocated independently.
</Note>
```ts title="src/app.ts"
import { flue } from '@flue/runtime/routing';
import { Hono } from 'hono';
const app = new Hono();
app.route('/api', flue());
export default app;
```
With this composition, a Slack Events API handler declared at `/events` is published at `/api/channels/slack/events`.
## Channel module contract
Every discovered channel file must export a named `channel` binding with a non-empty `routes` array:
```ts title="Minimal custom channel"
import type { Handler } from 'hono';
const webhook: Handler = async (c) => {
const rawBody = await c.req.text();
// Verify signature against rawBody before parsing.
return c.body(null, 200);
};
export const channel = {
routes: [{ method: 'POST', path: '/webhook', handler: webhook }],
};
```
Route validation rules enforced at build time:
| Rule | Constraint |
| --- | --- |
| `method` | Uppercase ASCII letters only (`POST`, `GET`, …) |
| `path` | Absolute suffix starting with `/`, no query or fragment, no `.` or `..` segments |
| `handler` | Callable Hono handler returning a `Response` |
| Duplicates | Same `method + path` within one channel is rejected |
Filename maps to URL namespace:
```txt
channels/acme.ts + /webhook → POST /channels/acme/webhook
channels/slack.ts + /events → POST /channels/slack/events
```
Add a path comment immediately above each application handler documenting the published URL.
First-party packages (`@flue/slack`, `@flue/github`, and others) return a `channel` object that satisfies this contract. Optional protocol surfaces publish routes only when their callback is provided — omitting `events` omits `/events` entirely.
## Scaffold with the CLI
Use `flue add channel` to fetch a blueprint for a coding agent:
<CodeGroup>
```sh title="First-party channel"
flue add channel slack --print | codex
```
```sh title="Custom provider"
flue add channel https://developers.notion.com/reference/webhooks --print | codex
```
</CodeGroup>
Named channels resolve to provider-specific blueprints. A URL selects the generic channel blueprint, which guides signature verification, provider SDK setup, and route declaration without assuming a maintained `@flue/<provider>` package.
Refresh an existing implementation with `flue update channel <name|url>`.
## Ownership boundary
| Concern | Owner |
| --- | --- |
| Request authentication and signature verification | Channel package or authored handler |
| Provider handshakes (URL verification, `ping`, etc.) | Channel package or authored handler |
| Body limits, parsing, typed provider payloads | Channel package or authored handler |
| Routes beneath `/channels/<name>/...` | Flue runtime |
| Provider SDK client and outbound credentials | Application |
| OAuth, installation, token storage | Application |
| Agent tools and authorization policy | Application |
| Delivery deduplication and business persistence | Application |
Channels are inbound HTTP ingress only. Outbound API calls use the provider's established SDK, exported from the same module as application-owned code.
## Verified ingress
First-party channel constructors verify the exact unconsumed request body before invoking application callbacks.
**Slack** (`createSlackChannel`):
<ParamField body="signingSecret" type="string" required>
Secret used to verify exact request bytes. Signed timestamps must be within five minutes of the server clock.
</ParamField>
<ParamField body="bodyLimit" type="number">
Maximum request-body size in bytes. Default: 1 MiB.
</ParamField>
Optional surfaces: `events` → `POST /events`, `interactions` → `POST /interactions`, `commands` → `POST /commands`. URL verification is handled internally; authenticated Events API deliveries forward with Slack's native field names. The channel does not deduplicate Events API retries.
**GitHub** (`createGitHubChannel`):
<ParamField body="webhookSecret" type="string" required>
Secret configured on the GitHub webhook. Verified with `X-Hub-Signature-256` against exact delivered bytes.
</ParamField>
<ParamField body="bodyLimit" type="number">
Maximum request-body size in bytes. Default: 25 MiB.
</ParamField>
Single surface: `webhook` → `POST /webhook`. `ping` deliveries are answered internally. The channel is stateless and does not deduplicate `deliveryId` values — GitHub expects a `2xx` within ten seconds and does not auto-retry.
```mermaid
sequenceDiagram
participant Provider as Provider (Slack/GitHub/…)
participant Flue as flue() /channels/:name
participant Pkg as Channel package
participant App as channels/<name>.ts handler
participant Agent as Agent session
Provider->>Flue: POST /channels/<name>/<suffix>
Flue->>Pkg: Route to verified handler
Pkg->>Pkg: Verify signature, enforce limits
alt Handshake (url_verification / ping)
Pkg-->>Provider: Protocol response
else Application event
Pkg->>App: Typed callback (payload / delivery)
App->>Agent: dispatch(agent, { id, input })
App-->>Pkg: Response / empty 200
Pkg-->>Provider: HTTP response
end
```
## Handle verified events
Callbacks run only after verification and protocol handling complete. Each receives an object with the Hono context `c` plus provider-specific typed data.
```ts title="src/channels/slack.ts"
import { dispatch } from '@flue/runtime';
import { createSlackChannel } from '@flue/slack';
import assistant from '../agents/assistant.ts';
export const channel = createSlackChannel({
signingSecret: process.env.SLACK_SIGNING_SECRET!,
// Path: /channels/slack/events
async events({ payload }) {
if (payload.type !== 'event_callback') return;
switch (payload.event.type) {
case 'app_mention': {
const event = payload.event;
await dispatch(assistant, {
id: channel.conversationKey({
teamId: payload.team_id,
channelId: event.channel,
threadTs: event.thread_ts ?? event.ts,
}),
input: {
type: 'slack.app_mention',
eventId: payload.event_id,
text: event.text,
},
});
return;
}
default:
return;
}
},
});
```
GitHub follows the same pattern with a single `webhook` callback receiving a discriminated `delivery` (`name` narrows `payload`):
```ts title="src/channels/github.ts (abridged)"
async webhook({ delivery }) {
if (delivery.name === 'issue_comment' && delivery.payload.action === 'created') {
const { repository, issue, comment } = delivery.payload;
await dispatch(assistant, {
id: channel.conversationKey({
owner: repository.owner.login,
repo: repository.name,
issueNumber: issue.number,
}),
input: {
type: 'github.issue_comment.created',
deliveryId: delivery.deliveryId,
comment: { id: comment.id, body: comment.body },
},
});
}
}
```
### Return values
Channel callbacks use ordinary Hono and Fetch responses:
- Return nothing → empty `200` acknowledgement
- Return `c.json(...)`, `c.text(...)`, or a `Response` → explicit status, headers, body
- Return a JSON-compatible value (where supported) → serialized as response body
Handlers must return a `Response`; otherwise the runtime throws a `TypeError`.
## Dispatch to agents
`dispatch(...)` admits asynchronous input to a continuing agent session. It resolves after admission, not after model processing.
**Created-agent overload** — pass a default-exported `createAgent(...)` value:
```ts
await dispatch(assistant, {
id: channel.conversationKey(thread),
input: { type: 'slack.app_mention', eventId, text },
});
```
**Named-agent overload** — target by discovered module name:
```ts
await dispatch({
agent: 'assistant',
id: channel.conversationKey(thread),
input: { type: 'slack.app_mention', eventId, text },
});
```
<ParamField body="id" type="string" required>
Target agent instance id. Often a `conversationKey(...)` for the provider thread, issue, or conversation.
</ParamField>
<ParamField body="input" type="unknown" required>
JSON-serializable payload delivered to the session. Use `null` for an intentional empty payload.
</ParamField>
<ResponseField name="dispatchId" type="string">
Generated delivery identifier. This is not a workflow `runId`.
</ResponseField>
<ResponseField name="acceptedAt" type="string">
ISO timestamp assigned when dispatch admission begins.
</ResponseField>
<Warning>
Dispatched input is an agent-session operation, not a workflow run. Do not confuse `dispatchId` with `runId` from `POST /workflows/:name`.
</Warning>
On Node, dispatch admission is durable via SQL; exact replays return the original receipt, conflicting replays throw. On Cloudflare, admission is durable to the agent Durable Object and processing may be at-least-once — design external side effects to be idempotent.
## Conversation keys
First-party channels expose `conversationKey(ref)` and `parseConversationKey(id)` for canonical, namespaced instance identifiers:
| Provider | Key format | Example ref |
| --- | --- | --- |
| Slack | `slack:v1:<teamId>:<channelId>:<threadTs>` | `{ teamId, channelId, threadTs }` |
| GitHub | `github:v1:owner:<owner>:repo:<repo>:issue:<n>` | `{ owner, repo, issueNumber }` |
Conversation keys identify destinations; they are not authorization capabilities. Bind outbound tools to parsed refs in trusted agent initialization:
```ts title="src/agents/assistant.ts"
import { createAgent } from '@flue/runtime';
import { channel, replyInThread } from '../channels/slack.ts';
export default createAgent(({ id }) => ({
model: 'anthropic/claude-haiku-4-5',
instructions: 'Reply in the bound Slack thread when appropriate.',
tools: [replyInThread(channel.parseConversationKey(id))],
}));
```
Direct `POST /agents/:name/:id` routes must authorize caller-selected instance ids before deriving provider destinations from them.
## Application-owned tools
Define narrow `defineTool(...)` helpers that call the exported provider SDK client. Bind credentials and destinations in application code; expose only intentionally variable fields (message text, comment body) to the model.
```ts title="Outbound tool pattern"
import { defineTool } from '@flue/runtime';
import { WebClient } from '@slack/web-api';
export const client = new WebClient(process.env.SLACK_BOT_TOKEN);
export function replyInThread(ref: { channelId: string; threadTs: string }) {
return defineTool({
name: 'reply_in_slack_thread',
description: 'Reply in the Slack thread bound to this agent.',
parameters: {
type: 'object',
properties: { text: { type: 'string', minLength: 1 } },
required: ['text'],
additionalProperties: false,
},
async execute({ text }) {
const result = await client.chat.postMessage({
channel: ref.channelId,
thread_ts: ref.threadTs,
text,
});
return JSON.stringify({ channel: result.channel, ts: result.ts });
},
});
}
```
Keep credentials, raw bodies, webhook response URLs, interaction tokens, and other short-lived capabilities out of dispatched `input`, model context, logs, and durable session history.
## HTTP API surface
:::endpoint POST /channels/:name/:suffix
Serve a discovered channel handler after method and suffix lookup.
**Path parameters**
| Name | Description |
| --- | --- |
| `name` | Channel basename from `channels/<name>.ts` |
| `suffix` | Route suffix declared on `channel.routes[].path` (e.g. `/events`, `/webhook`) |
**Responses**
| Status | When |
| --- | --- |
| `200` | Handler succeeds; empty body when callback returns nothing |
| `400` | Malformed provider payload or failed parsing (provider-specific) |
| `401` | Signature verification failed |
| `404` | Unknown channel, missing suffix, or unregistered route |
| `405` | Wrong HTTP method for an otherwise matching suffix |
| `413` | Body exceeds configured limit |
| `415` | Unsupported `Content-Type` (provider-specific) |
Channel routes are always mounted when a module is discovered; they do not require an HTTP `route` export like agents and workflows.
:::
List channel endpoints alongside agents and workflows at `GET /openapi.json` when mounted through `flue()`. See [Flue HTTP API reference](/flue-http-api-reference) for the full route inventory and error envelopes.
## Retries and idempotency
Channel packages are stateless and do not deduplicate provider deliveries. Providers may retry, duplicate, or reorder events.
Preserve provider delivery or event ids in `input` when useful for tracing. When duplicate admission is unacceptable, claim that id in application-owned durable storage before dispatching or performing external effects.
Handlers await application work such as `dispatch(...)` before acknowledging. Some providers impose response deadlines; a timed-out operation may still complete later, so timeouts do not replace idempotency.
## Node and Cloudflare
First-party channel packages use Fetch and Web Crypto and are tested on Node and workerd. Cloudflare builds enable `nodejs_compat`.
The outbound SDK remains application-owned. Validate that SDK operations your application depends on work on the configured target; provider blueprints select cross-runtime clients where possible.
Long-lived sockets, polling loops, and provider-managed background transports are outside the current channel model. Use verified HTTP delivery or keep those integrations in application-owned infrastructure.
## Verify locally
<Steps>
<Step title="Build the configured target">
```sh
flue build --target node
# or
flue build --target cloudflare
```
</Step>
<Step title="Start the dev server">
```sh
flue dev --target node
```
Confirm discovered channels appear in the build manifest and respond at the expected `/channels/<name>/<suffix>` paths.
</Step>
<Step title="Exercise webhook signatures">
Create representative payloads with valid and invalid signatures. Confirm:
- Correct route returns expected status and body
- Invalid signature returns `401` (or provider-equivalent rejection)
- Wrong HTTP method returns `405` with allowed methods
- Protocol handshakes (Slack URL verification, GitHub `ping`) succeed without reaching application handlers
</Step>
<Step title="Trace dispatch admission">
Dispatch from a handler and confirm the target agent session receives input. On Node, inspect the returned `dispatchId` and agent event stream coordinates from `POST /agents/:name/:id` or `flue connect`.
</Step>
</Steps>
<Warning>
Avoid contacting a live provider during routine verification unless explicitly testing an integration end-to-end.
</Warning>
## Troubleshooting
| Symptom | Likely cause |
| --- | --- |
| `404` on `/channels/<name>` | Namespace-only URL — append the route suffix (`/events`, `/webhook`, …) |
| `404` on `/channels/<name>/events` | Optional callback omitted — enable `events` (or equivalent) in the channel constructor |
| `405` on correct path | HTTP method mismatch — check `channel.routes[].method` |
| `[flue] dispatch() called before runtime was configured` | `dispatch()` invoked outside a Flue-built server entry |
| `[flue] dispatch() target agent "…" is not registered` | Agent module missing, or target name does not match discovered basename |
| `[flue] Channel "…" must export…` | Missing or malformed named `channel` export — rebuild after fixing `routes` |
| Signature failures in production | Clock skew (Slack five-minute window), wrong secret, or body consumed before verification |
## Related pages
<CardGroup>
<Card title="Flue project layout" href="/flue-project-layout">
Source-root resolution, `channels/` discovery boundaries, and `app.ts` composition.
</Card>
<Card title="Build Flue agents" href="/build-flue-agents">
Author agents that receive dispatched channel input and bind provider tools.
</Card>
<Card title="Flue HTTP API reference" href="/flue-http-api-reference">
Full mounted route inventory, admission semantics, and error envelopes.
</Card>
<Card title="Flue examples" href="/flue-examples">
Copy-pasteable channel ingress fixtures for Slack, GitHub, and other providers.
</Card>
<Card title="Runtime models" href="/runtime-models">
How dispatch receipts differ from workflow runs and Eve session turns.
</Card>
<Card title="Flue CLI reference" href="/flue-cli-reference">
`flue add channel`, `flue update channel`, and dev-server flags.
</Card>
</CardGroup>
---
## 11. Flue deploy
> Choose Node or Cloudflare targets, register providers in `app.ts`, build artifacts with `flue build`, and apply blueprint-driven integrations.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/11-flue-deploy.md
- Generated: 2026-06-18T19:27:28.080Z
### Source Files
- `withastro-flue:packages/cli/src/lib/config.ts`
- `withastro-flue:packages/cli/src/lib/build.ts`
- `withastro-flue:examples/cloudflare/src/workflows/skills-from-r2.ts`
- `withastro-flue:examples/hello-world/src/app.ts`
- `withastro-flue:blueprints/database.md`
- `withastro-flue:packages/postgres/README.md`
- `withastro-flue:turbo.jsonc`
---
title: "Flue deploy"
description: "Choose Node or Cloudflare targets, register providers in `app.ts`, build artifacts with `flue build`, and apply blueprint-driven integrations."
---
Flue compiles discovered agents, workflows, and channels into a target-specific deployable artifact: a Node `server.mjs` bundle or a Cloudflare Workers output directory with a generated Wrangler config. The build target is mandatory (`node` or `cloudflare`), provider registration belongs in `app.ts` at runtime, and optional integrations (persistence, sandboxes, channels, observability) are added through blueprint guides fetched by `flue add` and `flue update`.
## Targets at a glance
Flue supports two build targets. Each target changes persistence, bundling, and how you ship the artifact.
| Concern | Node (`target: 'node'`) | Cloudflare (`target: 'cloudflare'`) |
| --- | --- | --- |
| Build output | `dist/server.mjs` (Vite SSR bundle) | `dist/<worker-name>/` with generated `wrangler.json` |
| Default persistence | In-memory SQLite per process | Durable Object SQLite (automatic) |
| Custom `db.ts` | Supported — wired at startup | Rejected at build time |
| Optional modules | `app.ts`, `db.ts` | `app.ts`, `cloudflare.ts` |
| Local one-shot runs | `flue run --target node` | Use `flue dev --target cloudflare` (no `flue run`) |
| Default listen port | `3000` (`PORT` env) | Wrangler dev on `3583` via `flue dev` |
<Tabs>
<Tab title="Node">
Node builds a single ESM server entry. Dependencies from your `package.json` are externalized — ship `node_modules` alongside `dist/server.mjs`. The generated server calls `migrate()` and `connect()` on a `db.ts` adapter when present; otherwise it uses built-in in-memory SQLite.
</Tab>
<Tab title="Cloudflare">
Cloudflare builds through the official Cloudflare Vite integration. Flue generates Durable Object classes, bindings, and a merged Wrangler config from your project-root `wrangler.jsonc`. You own the ordered migration history; Flue does not append migrations automatically. Deploy the **generated** config under `dist/`, not the source-root file.
</Tab>
</Tabs>
## Configure the build target
Set `target` in `flue.config.ts` or pass `--target` on every build command. There is no default — Flue throws if `target` is missing from both places.
<ParamField body="target" type="'node' | 'cloudflare'" required>
Build and development target. Required unless `--target` is passed to the CLI.
</ParamField>
<ParamField body="root" type="string">
Project root. Flue resolves authored source from `<root>/.flue/`, then `<root>/src/`, then `<root>/`. Defaults to the config directory.
</ParamField>
<ParamField body="output" type="string">
Build output directory. Defaults to `<root>/dist`. Must not equal `root` or the resolved source root.
</ParamField>
Precedence is inline CLI flags, then `flue.config.ts`, then defaults.
<CodeGroup>
```ts title="flue.config.ts"
import { defineConfig } from '@flue/cli/config';
export default defineConfig({
target: 'cloudflare',
root: '.',
output: 'dist',
});
```
```bash title="CLI override"
flue build --target node --output ./build
```
</CodeGroup>
Scaffold a starter config with `flue init --target <node|cloudflare>`.
:::files
my-flue-app/
├── flue.config.ts # build-time: target, root, output
├── wrangler.jsonc # Cloudflare only: user-owned bindings + migrations
├── .env # Node local secrets (not bundled into build)
├── .dev.vars # Cloudflare local secrets
├── src/ # or .flue/ — first existing wins
│ ├── app.ts # HTTP composition + provider registration
│ ├── db.ts # Node only: PersistenceAdapter
│ ├── cloudflare.ts # Cloudflare only: DO exports, scheduled handlers
│ ├── agents/
│ ├── workflows/
│ └── channels/
└── dist/ # build output (default)
├── server.mjs # Node target
└── <worker-name>/ # Cloudflare target
└── wrangler.json
:::
## Register providers in `app.ts`
Provider and model routing are **runtime** concerns. They do not belong in `flue.config.ts` — API keys often come from `process.env` or Cloudflare bindings, so registration runs when the built server starts.
Export a Hono app (or compatible `fetch` handler) from `app.ts`. Mount Flue routes with `flue()` and call `registerProvider()` before requests are served.
```ts
import { registerProvider } from '@flue/runtime';
import { flue } from '@flue/runtime/routing';
import { Hono } from 'hono';
// Brand-new provider for a local OpenAI-compatible server.
registerProvider('ollama', {
api: 'openai-completions',
baseUrl: 'http://localhost:11434/v1',
});
// Patch a catalog provider through a gateway.
if (process.env.ANTHROPIC_GATEWAY_URL) {
registerProvider('anthropic', {
baseUrl: process.env.ANTHROPIC_GATEWAY_URL,
apiKey: process.env.ANTHROPIC_API_KEY,
});
}
const app = new Hono();
app.get('/api/ping', (c) => c.json({ pong: true }));
app.route('/', flue());
export default app;
```
On Cloudflare, the build auto-registers the `cloudflare` AI binding provider when you have not already registered it. A user `registerProvider('cloudflare', …)` in `app.ts` wins because ESM import hoisting runs user code first. Use this to customize or disable the default AI Gateway.
Delete `app.ts` and the build falls back to a default Hono app that mounts `flue()` at `/` with no extras.
## Build deployable artifacts
`flue build` discovers modules under the source root, validates the target plugin, and writes artifacts to `output`.
<Steps>
<Step title="Verify discovery">
The build requires at least one agent or workflow under `agents/` or `workflows/`. It also discovers optional `channels/`, `app.ts`, `db.ts` (Node), and `cloudflare.ts` (Cloudflare).
</Step>
<Step title="Run the build">
```bash
flue build --target node
# or
flue build --target cloudflare
```
`flue build` loads project-root `.env` (or one `--env` file) for configuration and build-time evaluation. The built server reads only the environment supplied at start time — credentials are not packaged into the artifact.
</Step>
<Step title="Ship the output">
<Tabs>
<Tab title="Node">
```bash
set -a; source .env; set +a
node dist/server.mjs
```
The server listens on `PORT` (default `3000`). Do not set reserved CLI variables (`FLUE_MODE`, `FLUE_CLI_*`, `FLUE_INTERNAL_CLI_IPC`) in production.
</Tab>
<Tab title="Cloudflare">
```bash
npx wrangler deploy --dry-run --config dist/<worker-name>/wrangler.json
npx wrangler deploy --config dist/<worker-name>/wrangler.json
```
Configure secrets with `wrangler secret put` before deploy. Use the generated Wrangler config so bindings, entrypoint, and Vite output stay in sync.
</Tab>
</Tabs>
</Step>
</Steps>
### Cloudflare-specific authoring
Beyond `app.ts`, Cloudflare projects may include:
- **`cloudflare.ts`** — named exports become top-level Worker exports (application-owned Durable Objects). A default export object may define `scheduled` and other non-HTTP handlers. Do not export a `fetch` handler here.
- **`wrangler.jsonc`** at the project root — bindings, containers, R2 buckets, and **ordered** `migrations`. Append a uniquely tagged `new_sqlite_classes` entry whenever you add an agent or workflow class.
- **Per-module `cloudflare` export** on agents/workflows — `extend({ base, wrap })` from `@flue/runtime/cloudflare` for native Agents SDK lifecycle hooks or integration wrappers.
## Blueprint-driven integrations
`flue add` and `flue update` fetch Markdown implementation guides for a coding agent to apply in your project. Blueprints are not npm packages — the CLI prints the guide; the agent edits source files.
| Kind | Delivers | Primary file |
| --- | --- | --- |
| `sandbox` | Remote execution adapter | `sandboxes/<provider>.ts` |
| `channel` | Verified webhook ingress + client | `channels/<provider>.ts` |
| `database` | `PersistenceAdapter` (Node only) | `db.ts` |
| `tooling` | Observability, evaluation, etc. | Target-specific (e.g. `sentry.ts`) |
```bash
flue add # list available blueprints
flue add database postgres # named blueprint
flue add sandbox daytona
flue add channel slack
flue add sandbox https://e2b.dev # generic guide from URL
flue update channel slack # refresh an existing integration
```
Named guides stamp generated files with a version marker:
```ts
// flue-blueprint: database/postgres@1
```
`flue update` compares your existing implementation against the current guide version and applies mechanical upgrades from the cumulative Upgrade Guide section.
### Target constraints for blueprints
- **Database blueprints** apply only to Node. Cloudflare uses Durable Object SQLite and rejects `db.ts` at build time.
- **Sandbox and channel blueprints** work on both targets but must verify signed payloads and bindings against your configured target.
- **Tooling blueprints** (for example Sentry) install target-specific SDKs — `@sentry/node` on Node, `@sentry/cloudflare` on Cloudflare.
For Postgres-backed durable state on Node:
```bash
flue add database postgres
```
Then default-export the adapter from source-root `db.ts`. `migrate()` runs once at server startup; `connect()` supplies execution, run, and event-stream stores.
## End-to-end deploy workflow
<Steps>
<Step title="Initialize">
```bash
npm init -y
npm install @flue/runtime valibot
npm install -D @flue/cli
flue init --target node # or cloudflare
```
</Step>
<Step title="Author runtime surface">
Create agents and/or workflows under the source root. Add `app.ts` for custom routes and `registerProvider()` calls. On Cloudflare, configure `wrangler.jsonc` migrations before the first deploy.
</Step>
<Step title="Add integrations">
```bash
flue add database postgres # Node: durable persistence
flue add channel github # webhook ingress
flue add tooling sentry # error reporting
```
Pipe blueprint output to your coding agent or use `--print` to inspect the raw Markdown.
</Step>
<Step title="Build and verify locally">
```bash
flue dev --target node # watch + reload on port 3583
flue dev --target cloudflare # Cloudflare Vite dev server
```
For production-style Node verification:
```bash
flue build --target node
node dist/server.mjs
```
</Step>
<Step title="Deploy">
Ship `dist/` to your host (Node) or run `wrangler deploy --config dist/<worker-name>/wrangler.json` (Cloudflare). Confirm HTTP routes: `/workflows/<name>`, `/agents/<name>/<id>`, `/channels/<name>`, and `/runs/<runId>`.
</Step>
</Steps>
## Troubleshooting
<AccordionGroup>
<Accordion title="Missing required target">
```
[flue] Missing required `target`. Set it via `--target <node|cloudflare>`
or in `flue.config.ts` as `target: "node"` (or `"cloudflare"`).
```
Set `target` in config or pass `--target` on every `flue build`, `flue dev`, `flue run`, and `flue connect` invocation.
</Accordion>
<Accordion title="db.ts on Cloudflare">
```
[flue] Custom persistence (db.ts) is not supported on the Cloudflare target.
```
Remove `db.ts` from the source root or switch the project to `target: 'node'` for external database persistence.
</Accordion>
<Accordion title="No agents or workflows found">
The build requires at least one file in `agents/` or `workflows/` under the resolved source root. Check that `root` and source-root precedence (`.flue/` vs `src/`) point at your authored modules.
</Accordion>
<Accordion title="Cloudflare migration errors">
Flue generates class names like `FlueTranslateWorkflow` with bindings `FLUE_TRANSLATE_WORKFLOW`. Keep deployed migration tags in order. When renaming generated classes, append `renamed_classes` entries — do not rewrite history that has already shipped.
</Accordion>
</AccordionGroup>
## Related pages
<Card href="/flue-configuration-reference" title="Flue configuration reference" icon="settings">
`flue.config.*` keys, source-root precedence, env loading, and CLI override rules.
</Card>
<Card href="/flue-cli-reference" title="Flue CLI reference" icon="terminal">
Commands, flags, and exit behavior for `flue build`, `flue dev`, `flue add`, and `flue init`.
</Card>
<Card href="/flue-project-layout" title="Flue project layout" icon="folder">
Source-root resolution, `app.ts` composition, and discovery boundaries.
</Card>
<Card href="/flue-persistence-reference" title="Flue persistence reference" icon="database">
`PersistenceAdapter` contract, built-in adapters, and `db.ts` wiring for Node deploys.
</Card>
<Card href="/flue-channels" title="Flue channels" icon="webhook">
Mount verified webhook routes and dispatch inbound events to agents.
</Card>
<Card href="/flue-examples" title="Flue examples" icon="code">
Target-specific fixtures including Cloudflare R2 hydration and provider registration.
</Card>
---
## 12. Eve authoring surfaces
> Configure `agent.ts`, write instructions, tools, skills, connections, sandbox overrides, subagents, and schedules from the filesystem contract.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/12-eve-authoring-surfaces.md
- Generated: 2026-06-18T19:27:56.383Z
### Source Files
- `vercel-eve:docs/agent-config.md`
- `vercel-eve:docs/tools.mdx`
- `vercel-eve:docs/skills.mdx`
- `vercel-eve:docs/connections.mdx`
- `vercel-eve:docs/sandbox.mdx`
- `vercel-eve:docs/subagents.mdx`
- `vercel-eve:docs/reference/typescript-api.md`
---
title: "Eve authoring surfaces"
description: "Configure `agent.ts`, write instructions, tools, skills, connections, sandbox overrides, subagents, and schedules from the filesystem contract."
---
Eve compiles an agent by walking the `agent/` directory: each subdirectory is an authored slot, and the file path—not a `name` or `id` field—determines runtime identity. Discovery runs at build time, lowers every slot into a compiled manifest under `.eve/`, and exposes the result through `eve info` and the HTTP session API. Model routing stays provider-neutral: pass a gateway model id string or any AI SDK `LanguageModel` instance in `agent.ts`.
## Filesystem contract
Identity is path-derived. A tool at `agent/tools/get_weather.ts` registers as `get_weather`; a connection at `agent/connections/linear.ts` registers as `linear`; a subagent at `agent/subagents/researcher/agent.ts` registers as `researcher`. The root agent name comes from `package.json` `name`, falling back to the app-root directory name.
:::files
my-agent/
├── package.json
├── agent/
│ ├── agent.ts # runtime config (optional on root)
│ ├── instructions.md # required on root agent
│ ├── tools/
│ ├── skills/
│ ├── connections/
│ ├── sandbox/ # or sandbox.ts shorthand
│ ├── schedules/ # root-only
│ ├── subagents/
│ ├── channels/ # root-only
│ ├── hooks/
│ ├── lib/ # import-only helpers
│ └── instrumentation.ts # root-only
└── evals/
:::
| Slot | Authored at | Subagents | Notes |
| --- | --- | --- | --- |
| Runtime config | `agent.ts` | Yes | `model` required when present; `description` required for subagents |
| Instructions | `instructions.md`, `instructions.ts`, or `instructions/` | Optional | Required on root; always-on system prompt |
| Tools | `tools/<name>.ts` | Yes | Run in app runtime; filename is the model-facing name |
| Skills | `skills/<name>.md` or packaged directory | Yes | Loaded on demand via `load_skill` |
| Connections | `connections/<name>.ts` | Yes | MCP or OpenAPI; credentials never reach the model |
| Sandbox | `sandbox.ts` or `sandbox/sandbox.ts` + `workspace/` | Yes | Isolated `/workspace`; framework default when omitted |
| Schedules | `schedules/<name>.ts` or `.md` | No | Root-only; cron-driven task or handler |
| Subagents | `subagents/<id>/` | Yes | Own slots; inherits nothing from root |
<Note>
Compared to Flue's code-first `createAgent` modules under `.flue/` or `src/`, Eve keeps capabilities in filesystem slots that discovery compiles into a manifest. Both frameworks separate always-on prompts from on-demand procedures, but Eve enforces the split through `instructions/` vs `skills/` directories.
</Note>
## `agent.ts` — runtime config
`agent/agent.ts` calls `defineAgent` from `eve`. Omit the file entirely and Eve defaults to `anthropic/claude-sonnet-4.6`. When present, `model` is required.
<CodeGroup>
```ts title="Gateway model id"
import { defineAgent } from "eve";
export default defineAgent({
model: "anthropic/claude-opus-4.8",
});
```
```ts title="Direct provider instance"
import { anthropic } from "@ai-sdk/anthropic";
import { defineAgent } from "eve";
export default defineAgent({
model: anthropic("claude-opus-4.8"),
});
```
</CodeGroup>
<ParamField body="model" type="LanguageModel | string" required>
Primary model for agent turns. Accepts an AI Gateway model id or any AI SDK-compatible `LanguageModel`.
</ParamField>
<ParamField body="modelOptions" type="AgentModelOptionsDefinition">
Provider option overrides forwarded to the model call (for example `providerOptions`).
</ParamField>
<ParamField body="compaction" type="AgentCompactionDefinition">
Conversation compaction config. Defaults to enabled at `thresholdPercent: 0.9`. Lower the threshold to compact sooner.
</ParamField>
<ParamField body="outputSchema" type="StandardJSONSchemaV1 | JsonObject">
Structured return type for task-mode runs (subagent, schedule, remote job). Interactive turns ignore it unless the client supplies a per-message schema.
</ParamField>
<ParamField body="experimental" type="{ codeMode?: boolean }">
Unstable opt-in flags. `codeMode` routes executable tools through a sandboxed code-execution wrapper.
</ParamField>
<ParamField body="build" type="{ externalDependencies?: string[] }">
Hosted-build packaging. Listed packages stay external while Eve compiles authored modules.
</ParamField>
<ParamField body="description" type="string">
Required for subagents. Surfaced to the parent as the lowered subagent tool's description.
</ParamField>
## Instructions
Instructions are the always-on system prompt, prepended to every model call. The root agent requires `agent/instructions.md` (or `instructions.ts` / `instructions/`). Subagents treat instructions as optional.
```md title="agent/instructions.md"
You are a concise assistant. Use tools when they are available.
```
For build-time composition, switch to TypeScript:
```ts title="agent/instructions.ts"
import { defineInstructions } from "eve/instructions";
export default defineInstructions({
markdown: "You are a concise assistant. Use tools when they are available.",
});
```
| Surface | Loaded | Use for |
| --- | --- | --- |
| `instructions.md` / `.ts` | Every turn | Identity, tone, standing rules |
| `agent/skills/*` | On demand via `load_skill` | Situational procedures |
Split large prompts across `agent/instructions/` (non-recursive). Root file content comes first, then directory entries sorted alphabetically. You cannot author both `instructions.md` and `instructions.ts` at the root—that pairing is a build error.
## Tools
Tools are typed actions the agent calls. Filename under `agent/tools/` is the model-facing name. Tools execute in the **app runtime** with full `process.env` access—not inside the sandbox.
```ts title="agent/tools/get_weather.ts"
import { defineTool } from "eve/tools";
import { z } from "zod";
export default defineTool({
description: "Get the current weather for a city.",
inputSchema: z.object({ city: z.string().min(1) }),
async execute({ city }, ctx) {
return { city, condition: "Sunny", temperatureF: 72 };
},
});
```
<ParamField body="description" type="string" required>
What the tool does, written for the model.
</ParamField>
<ParamField body="inputSchema" type="Zod schema | Standard Schema | JsonObject" required>
Input shape. Use `z.object({})` for no input.
</ParamField>
<ParamField body="execute" type="(input, ctx) => T | Promise<T>" required>
Implementation. Receives runtime context while authored code is running.
</ParamField>
<ParamField body="needsApproval" type="ApprovalPredicate">
Human-in-the-loop gate. Helpers from `eve/tools/approval`: `never()`, `once()`, `always()`, or a custom predicate.
</ParamField>
<ParamField body="toModelOutput" type="(output) => ModelOutput">
Project rich return data down to what the model sees. Channels still receive the full output.
</ParamField>
### Runtime context in tools
| Member | Use |
| --- | --- |
| `ctx.session` | Session metadata, turn, auth, parent lineage |
| `ctx.getSandbox()` | Live sandbox handle for `/workspace` |
| `ctx.getSkill(id)` | Read packaged skill metadata and files |
| `ctx.getToken()` | Resolve bearer token for tool `auth` |
| `ctx.requireAuth()` | Force authorization flow (for example after a `401`) |
Eve never runs tools during discovery. Completed steps replay recorded results; interrupted steps re-run—make non-idempotent side effects idempotent or gate them with approval.
## Skills
Skills are on-demand procedures the model loads with the framework-owned `load_skill` tool. Eve advertises each skill's description; the full body enters context only when the model calls `load_skill`.
```md title="agent/skills/forecast.md"
Use the weather tool before answering forecast or temperature questions.
```
Packaged skills use a directory with `SKILL.md` and sibling files:
```md title="agent/skills/research/SKILL.md"
---
description: Research unfamiliar topics before answering with confidence.
---
When the task is novel or ambiguous, gather evidence first.
```
For typed or generated content, use `defineSkill` from `eve/skills`:
```ts title="agent/skills/research.ts"
import { defineSkill } from "eve/skills";
export default defineSkill({
description: "Research unfamiliar topics before answering with confidence.",
markdown: "When the task is novel, gather evidence first.",
files: {
"references/checklist.md": "# Checklist\n\n- Find primary sources.\n",
},
});
```
<Warning>
Skills are scoped per agent. A subagent's `skills/` are invisible to the root agent. Share executable helpers through `lib/`, not a shared-skill mechanism.
</Warning>
Skills add instructions only—never a new execution surface. For typed runtime behavior, author a tool instead.
## Connections
Connections wire external MCP or OpenAPI servers into the agent. Files live under `agent/connections/`; the filename is the connection name. The model never sees URLs or credentials—it discovers tools through `connection__search` and calls them as `connection__<connection>__<tool>`.
```ts title="agent/connections/linear.ts"
import { defineMcpClientConnection } from "eve/connections";
export default defineMcpClientConnection({
url: "https://mcp.linear.app/sse",
description: "Linear workspace: issues, projects, cycles, and comments.",
auth: {
getToken: async () => ({ token: process.env.LINEAR_API_TOKEN! }),
},
});
```
| Connection type | Helper | Import |
| --- | --- | --- |
| MCP (Streamable HTTP or SSE) | `defineMcpClientConnection` | `eve/connections` |
| OpenAPI 3.x | `defineOpenAPIConnection` | `eve/connections` |
Connection-level options mirror tools: `auth.getToken`, `headers`, `tools.allow` / `tools.block` (MCP), `operations.allow` / `operations.block` (OpenAPI), and `approval` from `eve/tools/approval`. Tokens resolve per step and never land in conversation history.
For per-user OAuth, use `connect()` from `@vercel/connect/eve` or `defineInteractiveAuthorization` for self-hosted flows. Throw `ConnectionAuthorizationRequiredError` from `getToken` to start consent; map mid-flight `401` responses to `ctx.requireAuth()` so cached tokens are evicted.
## Sandbox overrides
Every agent has exactly one sandbox—an isolated bash environment rooted at `/workspace`. Built-in `bash`, `read_file`, `write_file`, `glob`, and `grep` already target it. Override only to add setup, seed files, pick a backend, or lock down network.
Seed files by placing them under `agent/sandbox/workspace/`:
```text
agent/sandbox/
sandbox.ts
workspace/
schema.sql → /workspace/schema.sql
scripts/run.sh → /workspace/scripts/run.sh
```
Author `defineSandbox` from `eve/sandbox`:
```ts title="agent/sandbox/sandbox.ts"
import { defineSandbox } from "eve/sandbox";
import { vercel } from "eve/sandbox/vercel";
export default defineSandbox({
backend: vercel({ runtime: "node24", resources: { vcpus: 2 } }),
revalidationKey: () => "repo-bootstrap-v1",
async bootstrap({ use }) {
const sandbox = await use();
await sandbox.run({ command: "apt-get install -y jq" });
},
async onSession({ use }) {
await use({ networkPolicy: "deny-all" });
},
});
```
Two layouts exist: `agent/sandbox.ts` (definition only) and `agent/sandbox/sandbox.ts` (definition + `workspace/` seeding). When both exist, the folder layout wins.
| Backend | Import | Runs |
| --- | --- | --- |
| `vercel()` | `eve/sandbox/vercel` | Vercel Sandbox |
| `docker()` | `eve/sandbox/docker` | Local Docker container |
| `microsandbox()` | `eve/sandbox/microsandbox` | Local lightweight VM |
| `justbash()` | `eve/sandbox/justbash` | Pure-JS simulated bash |
| `defaultBackend()` | `eve/sandbox` | Best available: Vercel → Docker → microsandbox → just-bash |
Lifecycle hooks: `bootstrap({ use })` runs once per template (reusable setup); `onSession({ use, ctx })` runs once per durable session (network policy, per-user credentials). `/workspace` persists across turns for the same session.
## Subagents
Subagents delegate focused subtasks. Two kinds exist:
| Kind | Mechanism | Sandbox | Tools/skills |
| --- | --- | --- | --- |
| Built-in `agent` tool | Copy of the calling agent | Shared with parent | Inherited |
| Declared subagent | `agent/subagents/<id>/` | Own sandbox (or default) | Own slots only |
Declared subagents require `description` in `agent.ts`:
```ts title="agent/subagents/researcher/agent.ts"
import { defineAgent } from "eve";
export default defineAgent({
description: "Investigate ambiguous questions before the parent agent responds.",
model: "anthropic/claude-opus-4.8",
});
```
A declared subagent inherits **nothing** from the root. Discovery treats `subagents/<id>/` as its own agent root with optional `instructions`, `tools/`, `connections/`, `skills/`, `sandbox/`, `hooks/`, and nested `subagents/`. `channels/` and `schedules/` are root-only.
Eve lowers every subagent into a model-visible tool with input `{ message: string; outputSchema?: object }`. A declared subagent's tool name is the bare path-derived id (`researcher`). Subagent names collide with tool names at build time—Eve rejects the build.
<Info>
Use a [skill](/eve-authoring-surfaces) when the agent keeps its identity and needs only an optional procedure. Split a subagent when the task needs a different prompt, narrower tool surface, or isolated runtime context.
</Info>
## Schedules
Schedules start the agent on a cron cadence. Each is a single file under `agent/schedules/` (root-only). Nested directories are supported; the name is path-derived (`agent/schedules/billing/sweep.ts` → `billing/sweep`).
Every schedule provides `cron` and exactly one of `markdown` or `run`:
<ParamField body="cron" type="string" required>
Standard 5-field cron (`minute hour day-of-month month day-of-week`). Vercel evaluates in UTC.
</ParamField>
<ParamField body="markdown" type="string">
Fire-and-forget prompt. Runs in task mode—cannot park for HITL or OAuth.
</ParamField>
<ParamField body="run" type="(args) => void | Promise<void>">
Handler with `receive`, `waitUntil`, and `appAuth` for channel handoffs.
</ParamField>
```ts title="agent/schedules/heartbeat.ts"
import { defineSchedule } from "eve/schedules";
export default defineSchedule({
cron: "*/5 * * * *",
markdown: "Pull open Linear issues and POST a summary to the metrics endpoint.",
});
```
Markdown schedules can also be plain `.md` files with frontmatter:
```md title="agent/schedules/cleanup.md"
---
cron: "0 0 * * 0"
---
Sweep stale workflow state.
```
Handler form delivers to a channel:
```ts title="agent/schedules/daily-digest.ts"
import { defineSchedule } from "eve/schedules";
import slack from "../channels/slack.js";
export default defineSchedule({
cron: "0 9 * * 1-5",
async run({ receive, waitUntil, appAuth }) {
waitUntil(
receive(slack, {
message: "Summarize yesterday's activity and post the digest.",
target: { channelId: "C0123ABC" },
auth: appAuth,
}),
);
},
});
```
`eve dev` never fires schedules on their cron cadence. Trigger one while iterating:
<RequestExample>
```sh
curl -X POST http://localhost:3000/eve/v1/dev/schedules/heartbeat
```
</RequestExample>
<ResponseExample>
```json
{ "scheduleId": "heartbeat", "sessionIds": ["..."] }
```
</ResponseExample>
## Author a complete agent
<Steps>
<Step title="Scaffold the layout">
Create `agent/` with `instructions.md` and optionally `agent.ts`. Run `eve init` if starting from scratch.
</Step>
<Step title="Add capabilities">
Place tools in `agent/tools/`, skills in `agent/skills/`, and external integrations in `agent/connections/`. Seed sandbox files under `agent/sandbox/workspace/` when the agent needs starter files.
</Step>
<Step title="Configure runtime">
Set `model` and optional `compaction`, `modelOptions`, or `outputSchema` in `agent.ts`. Add `defineSandbox` only when the default sandbox is insufficient.
</Step>
<Step title="Add delegation and automation">
Declare specialists under `agent/subagents/<id>/` with required `description`. Add cron jobs under `agent/schedules/` for recurring work.
</Step>
<Step title="Verify discovery">
Run `eve info` (or `eve info --json`) to list discovered tools, skills, subagents, schedules, routes, and diagnostics. Inspect compiled artifacts under `.eve/` when discovery reports errors.
</Step>
</Steps>
## `define*` helper map
| Helper | Import from | Authored at |
| --- | --- | --- |
| `defineAgent` | `eve` | `agent/agent.ts` |
| `defineTool` | `eve/tools` | `agent/tools/<name>.ts` |
| `defineSkill` | `eve/skills` | `agent/skills/<name>.ts` |
| `defineInstructions` | `eve/instructions` | `agent/instructions.ts` |
| `defineMcpClientConnection`, `defineOpenAPIConnection` | `eve/connections` | `agent/connections/<name>.ts` |
| `defineSandbox` | `eve/sandbox` | `agent/sandbox.ts` or `agent/sandbox/sandbox.ts` |
| `defineSchedule` | `eve/schedules` | `agent/schedules/<name>.ts` |
| `defineDynamic` | `eve/tools`, `eve/skills`, `eve/instructions` | Per-session resolution |
## Common discovery failures
| Symptom | Likely cause |
| --- | --- |
| Tool not listed in `eve info` | File outside `agent/tools/` or missing default export |
| Subagent build error | Missing `description` in `subagents/<id>/agent.ts` |
| Name collision | Subagent directory name matches an existing tool name |
| `instructions` build error | Both `instructions.md` and `instructions.ts` at root |
| Schedule not found | File in subagent directory (`schedules/` is root-only) |
| Sandbox seed rejected | `agent/sandbox/workspace/skills/` conflicts with `agent/skills/` |
## Related pages
<CardGroup>
<Card title="Eve project layout" href="/eve-project-layout">
Authored slots under `agent/`, path-derived naming, workspace seeding rules, and subagent package boundaries.
</Card>
<Card title="Eve quickstart" href="/eve-quickstart">
Run `eve init`, start `eve dev`, inspect discovery with `eve info`, and send the first session message.
</Card>
<Card title="Eve sessions and streaming" href="/eve-sessions-streaming">
Start sessions, stream NDJSON events, send follow-ups with `continuationToken`, and handle HITL and subagent events.
</Card>
<Card title="Eve weather fixture" href="/eve-weather-fixture">
Walk through the `weather-agent` fixture: instructions, tool schema, skill procedure, and local dev smoke paths.
</Card>
<Card title="Build Flue agents" href="/build-flue-agents">
Flue's code-first alternative: `createAgent`, `defineAgentProfile`, tools, skills, and sandboxes in TypeScript modules.
</Card>
<Card title="Eve CLI reference" href="/eve-cli-reference">
Commands and flags for `eve init`, `info`, `build`, `dev`, and schedule dispatch during local iteration.
</Card>
</CardGroup>
---
## 13. Eve sessions and streaming
> Start sessions, stream NDJSON events, send follow-ups with `continuationToken`, and handle HITL, subagent, and compaction events.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/13-eve-sessions-and-streaming.md
- Generated: 2026-06-18T19:28:27.518Z
### Source Files
- `vercel-eve:docs/concepts/sessions-runs-and-streaming.md`
- `vercel-eve:docs/concepts/execution-model-and-durability.md`
- `vercel-eve:docs/concepts/default-harness.md`
- `vercel-eve:docs/guides/session-context.md`
- `vercel-eve:docs/guides/auth-and-route-protection.md`
- `vercel-eve:packages/eve/src/public/index.ts`
---
title: "Eve sessions and streaming"
description: "Start sessions, stream NDJSON events, send follow-ups with `continuationToken`, and handle HITL, subagent, and compaction events."
---
Eve exposes a stable HTTP API at `/eve/v1` through the default `eveChannel` in `agent/channels/eve.ts`. A `POST` creates or continues a durable session; a `GET` on `/stream` returns a replayable NDJSON event feed. Clients hold two distinct handles — `continuationToken` for follow-up delivery and `sessionId` for stream attachment — and must not interchange them.
## Session handles
| Handle | Role | Owner |
| --- | --- | --- |
| `continuationToken` | Resume handle for the next user turn or HITL response | Channel |
| `sessionId` | Stream-and-inspect handle for event history | Runtime |
A session has one active continuation at a time. Each follow-up must use the current `continuationToken`; a stale token is rejected. For deterministic ordering, send one user turn at a time and wait for `session.waiting` before posting the next message to the same session.
<Note>
The TypeScript client (`eve/client`) tracks `continuationToken`, `sessionId`, and `streamIndex` as a `SessionState` cursor. Serialize `session.state` after each turn to persist and resume later.
</Note>
## HTTP routes
All three session routes run the channel's ordered `auth` walk before dispatch. `GET /eve/v1/health` is always public.
| Method | Path | Purpose | Success status |
| --- | --- | --- | --- |
| `POST` | `/eve/v1/session` | Create a new durable session | `202` |
| `POST` | `/eve/v1/session/:sessionId` | Continue with `continuationToken` | `200` |
| `GET` | `/eve/v1/session/:sessionId/stream` | Stream NDJSON events | `200` |
:::endpoint POST /eve/v1/session
Create a durable session and start the first turn.
:::
<ParamField body="message" type="string | UserContent" required>
User message. A string or an array of `text` and `file` parts (base64, data URL, or URL).
</ParamField>
<ParamField body="mode" type="'conversation' | 'task'">
Optional run mode for the first turn.
</ParamField>
<ParamField body="outputSchema" type="object">
Optional JSON Schema the harness must satisfy before the turn terminates.
</ParamField>
<ParamField body="clientContext" type="string | string[] | object">
One-turn ephemeral context prepended as user-role messages. Not persisted to durable history.
</ParamField>
<ParamField body="callback" type="object">
Optional terminal session callback configuration.
</ParamField>
<ResponseField name="continuationToken" type="string">
Resume handle for the next delivery to this session.
</ResponseField>
<ResponseField name="sessionId" type="string">
Stream-and-inspect handle. Also returned in the `x-eve-session-id` response header.
</ResponseField>
<RequestExample>
```bash title="Create session"
curl -X POST http://127.0.0.1:3000/eve/v1/session \
-H 'content-type: application/json' \
-d '{"message":"Summarize the latest forecast."}'
```
</RequestExample>
<ResponseExample>
```json title="202 response"
{
"ok": true,
"sessionId": "wrun_01ARYZ6S41TSV4RRFFQ69G5FAV",
"continuationToken": "eve:6c8b1f2e-3d4a-4b9c-8e21-9f0a1b2c3d4e"
}
```
</ResponseExample>
:::
:::endpoint POST /eve/v1/session/:sessionId
Deliver a follow-up message or HITL response to a waiting session.
:::
<ParamField body="continuationToken" type="string" required>
Current resume handle from the previous turn.
</ParamField>
<ParamField body="message" type="string | UserContent">
Optional follow-up user message.
</ParamField>
<ParamField body="inputResponses" type="InputResponse[]">
HITL responses resolving pending `input.requested` events. Each entry requires `requestId` and either `optionId` or `text`.
</ParamField>
<ParamField body="clientContext" type="string | string[] | object">
One-turn ephemeral context for the resumed turn.
</ParamField>
<ParamField body="outputSchema" type="object">
Optional structured-output schema for this turn.
</ParamField>
At least one of `message` or `inputResponses` must be non-empty. Both may be sent together when a resumed turn needs a human answer and follow-up text.
<RequestExample>
```bash title="Follow-up message"
curl -X POST http://127.0.0.1:3000/eve/v1/session/wrun_01ARYZ6S41TSV4RRFFQ69G5FAV \
-H 'content-type: application/json' \
-d '{
"continuationToken": "eve:6c8b1f2e-3d4a-4b9c-8e21-9f0a1b2c3d4e",
"message": "Now send the short version."
}'
```
</RequestExample>
:::
:::endpoint GET /eve/v1/session/:sessionId/stream
Attach to the durable NDJSON event stream for one session.
:::
<ParamField query="startIndex" type="integer">
Non-negative event count to skip before replaying. Omit to start from the beginning; pass the number of events already consumed to reconnect without duplicates.
</ParamField>
Response headers:
| Header | Value |
| --- | --- |
| `content-type` | `application/x-ndjson; charset=utf-8` |
| `x-eve-stream-format` | `ndjson` |
| `x-eve-stream-version` | `15` |
| `x-eve-session-id` | Session ID |
| `cache-control` | `no-store, no-transform` |
<RequestExample>
```bash title="Stream from start"
curl http://127.0.0.1:3000/eve/v1/session/wrun_01ARYZ6S41TSV4RRFFQ69G5FAV/stream
```
```bash title="Reconnect after 42 events"
curl "http://127.0.0.1:3000/eve/v1/session/wrun_01ARYZ6S41TSV4RRFFQ69G5FAV/stream?startIndex=42"
```
</RequestExample>
:::
## Stream lifecycle
```mermaid
sequenceDiagram
participant Client
participant EveChannel as eveChannel /eve/v1
participant Runtime as Durable runtime
participant Stream as Event stream
Client->>EveChannel: POST /session {message}
EveChannel->>Runtime: send() + continuationToken
EveChannel-->>Client: 202 {sessionId, continuationToken}
Client->>EveChannel: GET /session/:id/stream
EveChannel->>Stream: getEventStream()
Stream-->>Client: NDJSON events (one per line)
Stream-->>Client: session.waiting | session.completed | session.failed
Client->>EveChannel: POST /session/:id {continuationToken, message}
EveChannel->>Runtime: resume parked workflow
```
Every event is recorded before a step completes, so the full stream is replayable after disconnects, process restarts, or redeploys. Turn boundaries are `session.waiting`, `session.completed`, and `session.failed`.
## NDJSON event types
Each line is one JSON object with a `type` field. Events carry `data` with `sequence`, `turnId`, and step-scoped fields where applicable.
### Session and turn boundaries
| Event | Meaning |
| --- | --- |
| `session.started` | Durable session created |
| `turn.started` | New turn began |
| `message.received` | Inbound user message accepted |
| `turn.completed` | Turn finished successfully |
| `turn.failed` | Turn failed; carries `{ code, message, details? }` |
| `session.waiting` | Session parked for next input (`data.wait: "next-user-message"`) |
| `session.completed` | Terminal successful end |
| `session.failed` | Terminal session failure; carries `{ code, message, details? }` |
### Model steps and assistant output
| Event | Meaning |
| --- | --- |
| `step.started` | Model step began |
| `step.completed` | Step finished; carries `finishReason` and optional `usage` |
| `step.failed` | Step failed; carries `{ code, message, details? }` |
| `reasoning.appended` | Reasoning delta with `reasoningDelta` and cumulative `reasoningSoFar` |
| `reasoning.completed` | Finalized reasoning block |
| `message.appended` | Assistant text delta with `messageDelta` and cumulative `messageSoFar` |
| `message.completed` | Finalized assistant text; check `data.finishReason` |
| `result.completed` | Structured output when turn requested `outputSchema`; carries `data.result` |
`message.completed` can fire more than once per turn when the model emits interim text before a tool call. Treat `finishReason: "tool-calls"` as non-terminal narration; terminal replies use other finish reasons.
### Tool execution
| Event | Meaning |
| --- | --- |
| `actions.requested` | Model requested tool calls (`data.actions`) |
| `action.result` | Tool call returned with `status` and `result` |
### Human-in-the-loop (HITL)
When a tool requires approval or the harness calls `ask_question`, the turn parks and the stream emits `input.requested` with one or more `InputRequest` objects in `data.requests`.
Each `InputRequest` includes:
| Field | Purpose |
| --- | --- |
| `requestId` | Stable ID echoed in the response |
| `prompt` | Text shown to the user |
| `display` | UX hint: `"confirmation"`, `"select"`, or `"text"` |
| `options` | Selectable choices with `id`, `label`, optional `description` and `style` |
| `allowFreeform` | Whether freeform text is accepted |
| `action` | Underlying tool-call metadata |
Resume the parked turn by posting `inputResponses` to the continue route:
```ts title="HITL approval via eve/client"
const resumed = await session.send({
inputResponses: pendingRequests.map((request) => ({
requestId: request.requestId,
optionId: "approve",
})),
});
await resumed.result();
```
<Warning>
Answer HITL against the current `continuationToken`. A stale token is rejected, preventing double-resume of the same parked turn.
</Warning>
### Subagent delegation
| Event | Meaning |
| --- | --- |
| `subagent.called` | Child workflow started; carries `childSessionId` for stream attachment |
| `subagent.started` | Inline subagent execution began |
| `subagent.event` | Wraps a child stream event under `data.event` |
| `subagent.completed` | Inline subagent finished with `data.output` |
A delegated subagent publishes progress on its own child-session stream. The parent emits only `subagent.called` with `childSessionId` — attach to `GET /eve/v1/session/<childSessionId>/stream` to observe child progress. Child sessions expose `ctx.session.parent` with parent lineage metadata.
### Context compaction
When conversation history crosses the configured threshold (default `thresholdPercent: 0.9`), the harness summarizes older turns:
| Event | Meaning |
| --- | --- |
| `compaction.requested` | Compaction began; carries `modelId`, `sessionId`, `turnId`, `usageInputTokens` |
| `compaction.completed` | Compaction checkpoint written to durable history |
Tune compaction in `agent/agent.ts` under the `compaction` key. Compaction preserves framework tool state (read-before-write tracking, active todo list) automatically.
### Connection authorization
| Event | Meaning |
| --- | --- |
| `authorization.required` | Connection needs OAuth; carries `name`, `description`, and optional `authorization` challenge (`url`, `userCode`, `expiresAt`, `instructions`) |
| `authorization.completed` | Authorization resolved; `data.outcome` is `"authorized"`, `"declined"`, `"failed"`, or `"timed-out"` |
These events surface when a tool or connection with interactive auth suspends the turn. The runtime resumes after the OAuth callback completes.
## Follow-up delivery contract
Eve does not maintain a durable FIFO message queue per session. The `continuationToken` is a resume handle for the session's current workflow hook, not a general queue address.
<Steps>
<Step title="Start or resume">
`POST /eve/v1/session` for the first message, or `POST /eve/v1/session/:sessionId` with the stored `continuationToken` for follow-ups.
</Step>
<Step title="Stream the turn">
`GET /eve/v1/session/:sessionId/stream` (optionally with `?startIndex=N`). Consume events until a turn boundary arrives.
</Step>
<Step title="Wait for session.waiting">
Enable the composer or queue the next user message only after `session.waiting`. Sessions ending in `session.completed` or `session.failed` cannot accept further user input on the same continuation.
</Step>
<Step title="Send the next turn">
Post the next message with the updated `continuationToken` from the most recent successful delivery.
</Step>
</Steps>
If your channel receives bursts while the agent is working, keep a per-session queue in the channel or app layer and deliver after the session parks again. Separate sessions run independently.
## TypeScript client
`eve/client` wraps the HTTP routes with typed session management, automatic stream reconnection, and cursor advancement.
```ts title="Basic turn"
import { Client } from "eve/client";
const client = new Client({ host: "http://127.0.0.1:3000" });
const session = client.session();
const response = await session.send("Summarize the latest forecast.");
// Metadata available immediately after POST
console.log(response.sessionId, response.continuationToken);
const result = await response.result();
console.log(result.status, result.message);
```
### Aggregate vs live streaming
<CodeGroup>
```ts title="Aggregate with result()"
const result = await response.result();
console.log(result.status); // "waiting" | "completed" | "failed"
console.log(result.message); // final assistant text
console.log(result.inputRequests); // pending HITL from this turn
```
```ts title="Live iteration"
for await (const event of response) {
if (event.type === "message.appended") {
process.stdout.write(event.data.messageDelta);
}
if (event.type === "input.requested") {
// pause UI for approval or question
}
}
```
</CodeGroup>
`MessageResponse` is single-use — call either `result()` or `for await`, not both on the same response.
### Reconnection and cursor
| Client option | Default | Behavior |
| --- | --- | --- |
| `maxReconnectAttempts` | `3` | Per-turn reconnects after transient stream disconnects |
| `preserveCompletedSessions` | `false` | Keep `continuationToken` after `session.completed` |
After a turn ending in `session.waiting`, `session.state` preserves `continuationToken`, `sessionId`, and `streamIndex`. After `session.completed` or `session.failed`, the client resets to `{ streamIndex: 0 }` unless `preserveCompletedSessions` is enabled.
```ts title="Resume saved state"
import type { SessionState } from "eve/client";
const saved = await loadSessionState() as SessionState;
const session = client.session(saved);
const response = await session.send("Continue where we left off.");
await response.result();
```
HITL deliveries retry up to 10 times on transient `500` responses matching "target session was not found".
### Manual stream attachment
When you already have a `sessionId` and only need to read events without sending:
```ts
const session = client.session({
continuationToken: "eve:…",
sessionId: "wrun_…",
streamIndex: 10,
});
for await (const event of session.stream()) {
console.log(event.type);
}
```
Pass `{ startIndex: 0 }` to rewind. `stream()` throws if no `sessionId` exists (no message sent yet).
## Error cases
| Condition | Status | Response shape |
| --- | --- | --- |
| Invalid JSON body | `400` | `{ ok: false, error: "…" }` |
| Missing `message` on create | `400` | `{ ok: false, error: "Missing or empty 'message' field." }` |
| Missing `continuationToken` on continue | `400` | `{ ok: false, error: "Missing or empty 'continuationToken' field." }` |
| Neither `message` nor `inputResponses` on continue | `400` | `{ ok: false, error: "Expected a non-empty 'message'…" }` |
| Session not found | `404` | `{ ok: false, error: "Session not found." }` |
| Attachment policy violation | `413` / `415` | `{ ok: false, error, violations }` |
| Route auth failure | `401` / `403` | Framework-shaped unauthorized envelope |
| `onMessage` returns `null` | `204` | Message accepted but not dispatched |
Transport and route errors from `eve/client` throw `ClientError`. A stream ending in `session.failed` returns `status: "failed"` from `result()` without throwing.
## Event dispatch order
Every stream event runs four steps in order:
1. Channel handler — the channel's event handler mutates adapter state
2. Metadata projection — `metadata(state)` re-evaluated and stored
3. Hooks — authored hooks subscribed to the event fire
4. Dynamic resolvers — dynamic tool, skill, and instruction resolvers fire with fresh `ctx.channel.metadata`
By the time a resolver or hook reads channel metadata, the channel has already updated its state.
## Related pages
<CardGroup>
<Card title="Eve HTTP protocol reference" href="/eve-http-protocol-reference">
Full route inventory, event schemas, reconnect rules, and client integration entry points.
</Card>
<Card title="Runtime models" href="/runtime-models">
Compare Eve sessions, turns, and `continuationToken` with Flue sessions and workflow runs.
</Card>
<Card title="Eve quickstart" href="/eve-quickstart">
Run `eve dev`, send the first session message, and verify the HTTP API locally.
</Card>
<Card title="Auth and route protection" href="/page-eve-authoring-surfaces">
Configure `agent/channels/eve.ts` auth before exposing session routes in production.
</Card>
</CardGroup>
---
## 14. Eve deployment
> Build `.eve/` artifacts, configure Vercel env and sandbox backends, prewarm templates, deploy, and verify production routes.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/14-eve-deployment.md
- Generated: 2026-06-18T19:28:45.477Z
### Source Files
- `vercel-eve:docs/guides/deployment.md`
- `vercel-eve:docs/reference/cli.md`
- `vercel-eve:Dockerfile`
- `vercel-eve:docs/guides/instrumentation.md`
- `vercel-eve:docs/guides/auth-and-route-protection.md`
- `vercel-eve:packages/eve/package.json`
- `vercel-eve:turbo.json`
---
title: "Eve deployment"
description: "Build `.eve/` artifacts, configure Vercel env and sandbox backends, prewarm templates, deploy, and verify production routes."
---
`eve build` compiles authored files under `agent/` into inspectable `.eve/` artifacts and a Nitro host bundle under `.output/`. When `VERCEL` is set during a hosted build, the same command also emits `.vercel/output`, runs sandbox template prewarm, and bundles Vercel Workflow functions. Production deploys use `eve link` (or `vercel link` in CI), then `eve deploy` or `vercel deploy`, and expose the same `/eve/v1/*` routes as local `eve dev`.
## Deployment lifecycle
```mermaid
flowchart TB
subgraph Authoring["Authoring layer"]
Agent["agent/ slots"]
end
subgraph Build["eve build"]
EveArtifacts[".eve/ discovery + compile"]
NitroOut[".output/ Nitro server"]
VercelOut[".vercel/output/ (when VERCEL set)"]
Prewarm["Sandbox template prewarm"]
WorkflowFns["Workflow function emit"]
end
subgraph Vercel["Vercel platform"]
Env["Project env vars"]
Sandbox["Vercel Sandbox"]
Runs["Agent Runs / Workflow tags"]
end
subgraph Runtime["Production HTTP"]
Health["GET /eve/v1/health"]
Session["POST /eve/v1/session"]
Stream["GET /eve/v1/session/:id/stream"]
end
Agent --> EveArtifacts
EveArtifacts --> NitroOut
NitroOut --> VercelOut
VercelOut --> Prewarm
Prewarm --> WorkflowFns
WorkflowFns --> Env
Env --> Runtime
Prewarm --> Sandbox
Runtime --> Runs
```
<Note>
Eve requires Node `>=24` (`packages/eve/package.json`). The workspace also contains **withastro/flue**, which ships agents via `flue build` to Node or Cloudflare targets — a different deploy surface documented on the Flue deploy page.
</Note>
## Build output
`eve build` has no flags. It loads `.env` and `.env.local` from the app root, compiles discovery and runtime artifacts, builds the host output, and prints the output directory path.
<CodeGroup>
```bash title="Local build"
eve build
```
```bash title="Expected success line"
built output at .output
```
</CodeGroup>
### `.eve/` artifacts
These files are preserved even on partial discovery failure and are the fastest way to see what a deployment will load.
| Artifact | Purpose |
| --- | --- |
| `.eve/discovery/agent-discovery-manifest.json` | Files Eve discovered on disk |
| `.eve/discovery/diagnostics.json` | Authored-shape errors and warnings |
| `.eve/compile/compiled-agent-manifest.json` | Serialized surface loaded at runtime |
| `.eve/compile/compile-metadata.json` | Build-time metadata and paths |
| `.eve/compile/module-map.mjs` | Compiled module entrypoints imported at runtime |
When discovery errors cause `eve build` to fail, the CLI prints the full diagnostics report (severity, message, source path) and the diagnostics artifact path.
### Host output directories
| Condition | Output |
| --- | --- |
| `VERCEL` unset (local build) | `.output/` Nitro server bundle only |
| `VERCEL` set (Vercel build) | `.output/` plus `.vercel/output/` Build Output API bundle |
On Vercel builds, sandbox prewarm runs **before** workflow function emission so a prewarm failure aborts the build before bundling deploy artifacts that would never ship.
`eve start` serves the previously built `.output/server/index.mjs` bundle. Run `eve build` first; `eve start` errors when that entry is missing.
## Environment variables and secrets
Set credentials in the Vercel project environment — never in source or compiled artifacts. Route-auth secrets are not serialized into discovery or module-map artifacts; the runtime re-materializes them from the authored channel definition at boot.
### Model credentials
<ParamField body="AI Gateway (recommended)" type="env">
Link a Vercel project with `eve link`. Gateway model ids such as `anthropic/claude-opus-4.8` authenticate through Vercel OIDC with no provider API keys to manage. `eve link` pulls `VERCEL_OIDC_TOKEN` or `AI_GATEWAY_API_KEY` into `.env.local` and verifies one is present.
</ParamField>
<ParamField body="Direct provider" type="env">
Set the provider key directly, for example `OPENAI_API_KEY`, when not routing through AI Gateway.
</ParamField>
### Route auth secrets
Route-auth values such as `ROUTE_AUTH_BASIC_PASSWORD` and JWT/OIDC signing keys referenced by `agent/channels/eve.ts` must live in Vercel env vars. Replace scaffolded `placeholderAuth()` before the first production browser request — both the placeholder and the framework default reject production browser traffic (fail closed).
### Preview protection bypass
When a deployment sits behind Vercel preview protection and you want to drive it with `eve dev`, set `VERCEL_AUTOMATION_BYPASS_SECRET` locally before launching:
```bash
eve dev https://<your-app>
```
## Sandbox backend
On Vercel, sandboxes run on [Vercel Sandbox](https://vercel.com/docs/sandbox) infrastructure. Pin the backend explicitly or rely on environment-aware defaults.
<CodeGroup>
```ts title="Explicit Vercel backend"
import { defineSandbox } from "eve/sandbox";
import { vercel } from "eve/sandbox/vercel";
export default defineSandbox({
backend: vercel(),
});
```
```ts title="Environment-aware default"
import { defaultBackend, defineSandbox } from "eve/sandbox";
export default defineSandbox({
backend: defaultBackend(),
});
```
</CodeGroup>
`defaultBackend()` selects backends in priority order: **Vercel Sandbox** when `process.env.VERCEL` is set, then Docker, microsandbox, then just-bash locally. One sandbox definition can serve both local `eve dev` and hosted Vercel without branching on `VERCEL` in authored code.
The repository `Dockerfile` defines the Ubuntu 26.04 base image Eve agents use inside Vercel Sandbox (Node 24, git, ripgrep, python3, jq, sudo).
## Build-time sandbox prewarm
During hosted builds, Eve prewarms reusable Vercel sandbox templates so the first session avoids cold-start template construction.
| Rule | Behavior |
| --- | --- |
| Activation | Runs only when **both** `VERCEL` and `VERCEL_DEPLOYMENT_ID` are set |
| Skip | Sandboxes with no `bootstrap()` and no workspace seed files (`templatePlan.kind === "none"`) |
| Seed-only templates | Keyed by skills and workspace file contents; unchanged seeds reuse across deploys |
| `bootstrap()` templates | Keyed by optional `revalidationKey()`, authored sandbox source hash, and seed contents |
| Build log | Summary line reports reused vs built counts; per-template progress is logged |
| Runtime | `onSession()` still runs once per session; prewarm covers template construction only |
| Failure | **Build fails** — prewarm errors are treated as build failures |
<Warning>
If build-time prewarm fails, the Vercel build fails. Fix sandbox bootstrap or seed configuration before retrying deploy.
</Warning>
## Link and deploy
### Link the Vercel project
<Steps>
<Step title="Link interactively">
```bash
eve link
```
Select a team and project. Eve runs `vercel link`, pulls project environment into `.env.local`, and verifies an AI Gateway credential landed. Re-running `eve link` always shows the pickers again; the new choice wins. A running `eve dev` reloads env files automatically.
</Step>
<Step title="Link in CI">
`eve link` requires an interactive terminal. In CI, use:
```bash
vercel link --project <name> --yes
```
</Step>
</Steps>
### Deploy to production
<Tabs>
<Tab title="eve deploy">
```bash
eve deploy
```
`eve deploy` runs the shared deploy flow: install dependencies if needed, `vercel deploy --prod --yes`, pull env into `.env.local`, and probe the production URL. An unlinked directory prompts through the same team/project pickers when a TTY is present; non-interactive runs without a link exit with guidance to run `eve link` first. `eve deploy` sets `VERCEL_USE_EXPERIMENTAL_FRAMEWORKS=1` during deploy.
</Tab>
<Tab title="vercel deploy">
```bash
vercel deploy
# or
vercel deploy --prod
```
Push to a Git-connected Vercel project or deploy with the Vercel CLI directly. The deployed app serves the same health, session, and stream routes as local dev.
</Tab>
</Tabs>
## Auth before first production traffic
Route auth on `agent/channels/eve.ts` guards:
| Route | Auth |
| --- | --- |
| `POST /eve/v1/session` | Required |
| `POST /eve/v1/session/:sessionId` | Required |
| `GET /eve/v1/session/:sessionId/stream` | Required |
| `GET /eve/v1/health` | Public (no auth walk) |
`eve init` scaffolds `[localDev(), vercelOidc(), placeholderAuth()]`. Swap `placeholderAuth()` for your real policy — or delete the authored channel file to fall back to `[localDev(), vercelOidc()]`, which also rejects production browser traffic.
## Verify production routes
<Steps>
<Step title="Health check">
```bash
curl https://<your-app>/eve/v1/health
```
Expect a successful response suitable for load balancers and uptime monitors.
</Step>
<Step title="Start a session">
<RequestExample>
```bash
curl -X POST https://<your-app>/eve/v1/session \
-H 'content-type: application/json' \
-d '{"message":"Hello from production"}'
```
</RequestExample>
<ResponseExample>
```json
{
"sessionId": "<session-id>"
}
```
</ResponseExample>
The JSON body includes `sessionId` for the new session.
</Step>
<Step title="Attach to the stream">
```bash
curl https://<your-app>/eve/v1/session/<sessionId>/stream
```
Returns NDJSON session events. For authenticated deployments, include the same credentials your route-auth policy expects.
</Step>
<Step title="Interactive smoke test">
```bash
eve dev https://<your-app>
```
Connects the dev TUI to a remote deployment — useful for preview and production smoke tests. Set `VERCEL_AUTOMATION_BYPASS_SECRET` first when preview protection is enabled.
</Step>
<Step title="Run evals against production">
```bash
eve eval --url https://<your-app>
```
Runs discovered evals against the remote target without starting a local server.
</Step>
</Steps>
## Observability after deploy
Once deployed, Vercel auto-detects `eve` as the framework and surfaces an **Agent Runs** tab under the project's **Observability** view. Browse sessions and drill into conversation traces powered by framework-owned `$eve.*` workflow run tags — no `agent/instrumentation.ts` required.
<Info>
The Agent Runs tab is currently gated per Vercel team. If it does not appear, contact your Vercel representative for enablement.
</Info>
OpenTelemetry export configured in `agent/instrumentation.ts` is separate from Agent Runs. Use OTel when you want spans in Braintrust, Datadog, or another third-party backend.
## Host framework co-deploy
An Eve agent can deploy standalone or mount inside a host web framework (Next.js via `withEve`, Nuxt, SvelteKit) that owns marketing pages, dashboards, and other API routes. The host keeps its own routing; Eve's HTTP contract and session routes stay identical either way.
## Pre-deploy checklist
- [ ] `eve build` succeeds locally; on Vercel, `.vercel/output` is written when `VERCEL` is set
- [ ] `eve info` shows the expected tools, skills, channels, and routes with no blocking diagnostics
- [ ] Model credentials and route-auth secrets are set in Vercel env vars (not in repo)
- [ ] Sandbox backend matches the target (`vercel()` or `defaultBackend()`)
- [ ] Build-time prewarm completed without failure (check Vercel build logs)
- [ ] `placeholderAuth()` is replaced with production route auth
- [ ] `eve deploy` or `vercel deploy` succeeds
- [ ] Health, session POST, and stream GET respond on the deployment URL
## Troubleshooting
| Symptom | Likely cause and fix |
| --- | --- |
| `eve build` fails with discovery errors | Read printed diagnostics and `.eve/discovery/diagnostics.json`; fix authored-shape errors under `agent/` |
| Build fails during sandbox prewarm | Fix `bootstrap()` or workspace seeds; prewarm failures abort the Vercel build |
| `401` on session routes in production | Expected fail-closed behavior; replace `placeholderAuth()`, configure `vercelOidc()` or your `AuthFn`, set `VERCEL_PROJECT_ID` |
| `eve deploy` exits before deploy | Directory not linked; run `eve link` or `vercel link --project <name> --yes` in CI |
| `eve start` missing build output | Run `eve build` first; expects `.output/server/index.mjs` |
| Tool not visible to the model | Run `eve info`; confirm file is in the correct slot and default-exports `defineTool(...)` |
| Stuck on `session.waiting` | Turn parked on approval, question, or connection sign-in; answer or POST follow-up with valid `continuationToken` |
## Related pages
<CardGroup>
<Card title="Eve CLI reference" href="/eve-cli-reference">
Commands, flags, and exit behavior for `eve build`, `link`, `deploy`, `start`, and `dev`.
</Card>
<Card title="Eve HTTP protocol reference" href="/eve-http-protocol-reference">
Session routes, NDJSON event types, `continuationToken` follow-ups, and stream reconnect rules.
</Card>
<Card title="Eve authoring surfaces" href="/eve-authoring-surfaces">
Configure `agent.ts`, sandbox overrides, channels, and route auth before shipping.
</Card>
<Card title="Eve sessions and streaming" href="/eve-sessions-streaming">
Start sessions, stream events, and handle HITL and subagent events in production clients.
</Card>
<Card title="Flue deploy" href="/flue-deploy">
Compare Eve's Vercel-centric deploy path with Flue's Node and Cloudflare build targets.
</Card>
</CardGroup>
---
## 15. Flue CLI reference
> Commands, flags, defaults, and exit behavior for `flue dev`, `run`, `connect`, `build`, `init`, `add`, `update`, `docs`, and `logs`.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/15-flue-cli-reference.md
- Generated: 2026-06-18T19:28:44.244Z
### Source Files
- `withastro-flue:packages/cli/bin/flue.ts`
- `withastro-flue:packages/cli/src/lib/config.ts`
- `withastro-flue:packages/cli/src/lib/dev.ts`
- `withastro-flue:packages/cli/src/lib/build.ts`
- `withastro-flue:packages/cli/src/lib/env.ts`
- `withastro-flue:packages/cli/package.json`
---
title: "Flue CLI reference"
description: "Commands, flags, defaults, and exit behavior for `flue dev`, `run`, `connect`, `build`, `init`, `add`, `update`, `docs`, and `logs`."
---
The `flue` executable ships from `@flue/cli` (`bin/flue.mjs` → `dist/flue.js`) and is the primary interface for configuring, developing, exercising, building, and inspecting Flue applications. Commands share a small set of project flags (`--target`, `--root`, `--output`, `--config`, `--env`) resolved through `flue.config.*` with CLI overrides winning. The `target` field is required everywhere it is needed; there is no implicit default.
## Invocation
```bash
flue [--help | -h | help]
flue [--version | -v]
flue <command> [arguments] [flags]
```
| Top-level flag | Exit code | Behavior |
| --- | --- | --- |
| `--help`, `-h`, `help` | `0` | Print full usage to **stdout**. |
| `--version`, `-v` | `0` | Print package version to **stdout**. |
| Unknown command | `1` | Print usage to **stderr**. |
The bin pre-checks Node.js before loading the compiled CLI. Supported runtimes are Node **≥ 22.18** or **≥ 23.6** (native TypeScript config). `package.json` declares `engines.node` as `>=22.19.0`.
## Shared project flags
These flags apply to `dev`, `run`, `connect`, and `build`. Values from the command line override `flue.config.*`; unspecified fields fall through to the config file, then built-in defaults.
<ParamField body="--target" type="node | cloudflare">
Build and runtime target. Required unless set in `flue.config.ts`. `flue run` and `flue connect` only support `node` at execution time; an explicit `--target cloudflare` is rejected at parse time, and a config-only `target: "cloudflare"` is rejected after resolution.
</ParamField>
<ParamField body="--root" type="path">
Project root. Default: directory containing the discovered config file, or the config search directory (cwd when no `--root` is passed). Relative paths resolve from cwd when passed on the CLI. Source discovery prefers `<root>/.flue`, then `<root>/src`, then `<root>`.
</ParamField>
<ParamField body="--output" type="path">
Build artifact directory. Default: `<root>/dist`. Must not resolve to the project root or source root.
</ParamField>
<ParamField body="--config" type="path">
Explicit `flue.config.{ts,mts,mjs,js,cjs,cts}` path, relative to cwd. Without this flag, the CLI searches the root/search directory using basename priority (`flue.config.ts` first).
</ParamField>
<ParamField body="--env" type="path">
Single alternate `.env`-format file. Default: `<config-base>/.env` when present. The path must exist when explicitly set. Only variables not already set in the shell are applied; shell values always win. Passing more than one `--env` is an error.
</ParamField>
Config discovery for `flue dev` also watches `flue.config.*` files. Editing a watched config restarts the dev supervisor (150 ms debounce); invalid config leaves the supervisor waiting for the next valid change.
## Command summary
| Command | Role | Blocks? |
| --- | --- | --- |
| `flue dev` | Watch-mode local server with rebuild/reload | Yes, until SIGINT/SIGTERM |
| `flue run` | One-shot Node workflow invocation | No |
| `flue connect` | Interactive Node agent-instance session | Yes, until EOF or interrupt |
| `flue build` | Production artifact to `output` | No |
| `flue init` | Write starter `flue.config.ts` | No |
| `flue add` | Fetch/list integration blueprints | No |
| `flue update` | Fetch current blueprint for upgrades | No |
| `flue docs` | List, read, or search bundled docs | No |
| `flue logs` | Replay or follow workflow-run events | Follow mode blocks until `run_end` or interrupt |
---
## `flue dev`
Long-running watch-mode development server. Builds the project, serves it locally, watches files, and reloads after relevant changes.
```bash
flue dev [--target <node|cloudflare>] [--root <path>] [--output <path>] [--config <path>] [--port <number>] [--env <path>]
```
<ParamField body="--port" type="number">
Local server port. Default: **3583** (`DEFAULT_DEV_PORT`).
</ParamField>
<Tabs>
<Tab title="Node">
Builds `server.mjs` to `output`, spawns it as a child process, and respawns after every successful rebuild. Env-file changes on Node trigger a forced reload even when build output is unchanged.
</Tab>
<Tab title="Cloudflare">
Runs Vite with the official Cloudflare Workers plugin. Only structural source changes (agents, workflows, channels, `app.ts`, `cloudflare.ts`, Wrangler config) trigger rebuilds. Runtime bindings continue to use `.dev.vars`, `.env`, and `CLOUDFLARE_ENV`.
</Tab>
</Tabs>
Rebuilds are debounced 150 ms. Rebuild errors are logged but do not exit the dev loop.
**Exit behavior:** `SIGINT` → `130`, `SIGTERM` → `143`. Config watcher failure → `1`. Unexpected child exit (non-restart) → child exit code or `1`. Initial build failure → `1`.
<RequestExample>
```bash
flue dev --target node
flue dev --target cloudflare --port 8787
flue dev --env .env.staging
```
</RequestExample>
---
## `flue run`
One-shot Node workflow invocation: silent build → spawn local IPC child → invoke workflow → exit. Does not publish HTTP run-inspection routes; use `flue logs` only for runs owned by a running server.
```bash
flue run <workflow> [--target node] [--payload <json>] [--root <path>] [--output <path>] [--config <path>] [--env <path>]
```
| Argument | Required | Description |
| --- | --- | --- |
| `<workflow>` | Yes | Discovered workflow module name (basename under `workflows/`). |
<ParamField body="--payload" type="JSON string" default="{}">
Workflow input payload. Must be valid JSON; validated at parse time.
</ParamField>
**Output:** Build banner and streamed events on **stderr**. Non-null workflow result as formatted JSON on **stdout**.
**Exit behavior:** Success → `0`. Workflow or build failure → `1`. `SIGINT`/`SIGTERM` → `130`/`143`. Local process readiness timeout is 60 seconds.
Cloudflare targets are unsupported. The CLI prints a curl example against `http://localhost:3583/workflows/<workflow>` and exits `1`.
<RequestExample>
```bash
flue run hello --target node
flue run summarize --payload '{"text":"hello"}' --env .env.staging
```
</RequestExample>
---
## `flue connect`
Builds a Node project and opens an interactive local connection to one agent instance. Enter one prompt per line; Ctrl+D closes the session.
```bash
flue connect <agent> <instance-id> [--target node] [--root <path>] [--output <path>] [--config <path>] [--env <path>]
```
| Argument | Required | Description |
| --- | --- | --- |
| `<agent>` | Yes | Discovered agent module name. |
| `<instance-id>` | Yes | Agent instance identifier (for example `local` or a thread ID). |
`--payload` is rejected; enter prompts interactively after connecting.
**Output:** Connection status and streamed events on **stderr**. Non-null prompt results as JSON on **stdout**.
**Exit behavior:** Build or initial connection failure → `1`. Prompt errors log to stderr but keep the session open. Unexpected child exit before EOF → `exitCode 1`. Normal EOF → `0`. `SIGINT`/`SIGTERM` → `130`/`143`.
Cloudflare targets are unsupported at parse and resolution time.
<RequestExample>
```bash
flue connect assistant customer-123 --target node
flue connect assistant customer-123 --env .env.staging
```
</RequestExample>
---
## `flue build`
Discover agents, workflows, channels, and optional `app.ts` / `db.ts` / `cloudflare.ts` entries, then write target-specific deployment output.
```bash
flue build [--target <node|cloudflare>] [--root <path>] [--output <path>] [--config <path>] [--env <path>]
```
| Target | Primary output |
| --- | --- |
| `node` | `<output>/server.mjs` (Vite SSR bundle) |
| `cloudflare` | Workers-compatible build via Cloudflare Vite integration |
Build fails with exit `1` when no agent or workflow modules are discovered, when `target` is missing, or when `output` equals the project or source root.
<RequestExample>
```bash
flue build --target node
flue build --target cloudflare --root ./my-app
flue build --target node --output ./build
```
</RequestExample>
---
## `flue init`
Write a starter `flue.config.ts`. Does not scaffold agents, workflows, or application code.
```bash
flue init --target <node|cloudflare> [--root <path>] [--force]
```
<ParamField body="--target" type="node | cloudflare" required>
Written into the generated config as `target: '<value>'`.
</ParamField>
<ParamField body="--root" type="path" default="cwd">
Existing directory to receive `flue.config.ts`. Must exist.
</ParamField>
<ParamField body="--force" type="boolean" default="false">
Overwrite when any `flue.config.*` already exists in the target directory. If `--force` writes `flue.config.ts` beside another variant, the new `.ts` file takes precedence; the older file remains on disk with a warning.
</ParamField>
**Exit behavior:** Success → `0`. Missing `--target`, existing config without `--force`, missing target directory, or write failure → `1`.
<ResponseExample>
```ts title="flue.config.ts"
import { defineConfig } from '@flue/cli/config';
export default defineConfig({
target: 'node',
});
```
</ResponseExample>
---
## `flue add` and `flue update`
Fetch Markdown implementation blueprints from `https://flueframework.com/cli/blueprints` (overridable via `FLUE_REGISTRY_URL`). Neither command installs packages or edits project files.
```bash
flue add [<kind> <name|url>] [--print]
flue update <kind> <name|url> [--print]
```
| Kind | Produces |
| --- | --- |
| `sandbox` | Sandbox adapter |
| `channel` | Channel integration |
| `database` | Database persistence adapter |
| `tooling` | Developer tooling integration |
`flue add` with no arguments lists known blueprints on **stderr** and exits `0`. `flue update` requires both `<kind>` and `<name|url>`.
<ParamField body="--print" type="boolean">
Force raw blueprint Markdown to **stdout**, bypassing agent-detection heuristics (`@vercel/detect-agent`).
</ParamField>
**Output modes:** In agent mode (`--print` or detected coding agent), blueprint Markdown goes to **stdout**. Otherwise, human-oriented pipe instructions go to **stderr**.
**Exit behavior:** Success → `0`. Unknown kind, unknown named blueprint, registry HTTP error, or network failure → `1`. `flue update` rejects missing arguments at parse time → `1`.
<RequestExample>
```bash
flue add
flue add channel slack --print | codex
flue add sandbox https://e2b.dev --print | claude
flue update channel slack --print | codex
```
</RequestExample>
---
## `flue docs`
Browse documentation bundled inside the installed `@flue/cli` package. No network access required.
```bash
flue docs
flue docs read <path>
flue docs search <query>
```
| Subcommand | stdout | stderr |
| --- | --- | --- |
| (none) | Page catalog (`path` + `title`) | Usage header |
| `read <path>` | Markdown page body | — |
| `search <query>` | JSON `{ query, results }` (max 8 hits) | Read hint |
`read` accepts page paths (`guide/sandboxes`), website paths (`/docs/guide/sandboxes/`), and full URLs. Unknown pages exit `1`.
**Exit behavior:** Success → `0`. Missing bundled docs tree, unknown page, or invalid subcommand → `1`.
<RequestExample>
```bash
flue docs
flue docs read guide/sandboxes
flue docs search "durable execution"
```
</RequestExample>
---
## `flue logs`
Read-only inspection of workflow-run events from a running Flue server via the Durable Streams protocol. Does not invoke work.
```bash
flue logs <workflowRunId> [--server <url>] [--header 'Name: value'] [--follow|-f|--no-follow] [--since <offset>] [--types <a,b,c>] [--limit <n>] [--format <pretty|ndjson>]
```
| Argument | Required | Description |
| --- | --- | --- |
| `<workflowRunId>` | Yes | Opaque run ID (for example `run_01H...`). |
<ParamField body="--server" type="URL" default="http://127.0.0.1:3583">
Base URL of the running Flue server hosting `/runs/<runId>`.
</ParamField>
<ParamField body="--header" type="Name: value" multiple>
Repeatable curl-style HTTP header. Duplicate names and the reserved `Accept` header are rejected.
</ParamField>
<ParamField body="--follow / -f / --no-follow" type="boolean">
Follow mode control. When neither is set, the CLI probes `GET /runs/<runId>?meta`: **active** runs are followed, **terminal** runs replay once.
</ParamField>
<ParamField body="--since" type="offset" default="-1 (beginning)">
Resume strictly after a Durable Streams offset. NDJSON output includes per-event `offset` derived from `eventIndex`.
</ParamField>
<ParamField body="--types" type="comma-separated list">
Client-side event-type filter.
</ParamField>
<ParamField body="--limit" type="positive integer">
Cap emitted events (client-side, both modes).
</ParamField>
<ParamField body="--format" type="pretty | ndjson" default="pretty">
`pretty` renders human-readable events on **stderr**; `ndjson` writes one JSON object per **stdout** line.
</ParamField>
**Exit behavior:**
| Condition | Exit code |
| --- | --- |
| Success (no failing `run_end`) | `0` |
| Request or fetch failure | `1` |
| Stream interrupted (non-signal) | `1` |
| Consumed `run_end` with `isError: true` | `2` |
| Ctrl+C during follow mode | `130` |
<RequestExample>
```bash
flue logs run_01H...
flue logs run_01H... --no-follow
flue logs run_01H... --types tool,log,run_end --format ndjson
flue logs run_01H... --server https://api.example.com --header "Authorization: Bearer $TOKEN"
```
</RequestExample>
---
## Exit code reference
| Code | Meaning |
| --- | --- |
| `0` | Command completed successfully. |
| `1` | Parse/validation error, config resolution failure, build failure, workflow/agent failure, registry or docs error, stream interruption, or unexpected process exit. |
| `2` | `flue logs` observed a failing `run_end` event. |
| `130` | `SIGINT` (Ctrl+C) — `dev`, `run`, `connect`, `logs` follow mode. |
| `143` | `SIGTERM` — `dev`, `run`, `connect`. |
Commands other than `dev` and `flue logs` follow mode install `SIGINT`/`SIGTERM` handlers that terminate any spawned local IPC child before exiting.
## Typical workflow
<Steps>
<Step title="Initialize and configure">
```bash
flue init --target node
```
Set `target`, `root`, and `output` in `flue.config.ts` or pass flags per command.
</Step>
<Step title="Develop locally">
```bash
flue dev
```
Exercise HTTP routes, or use `flue connect` / `flue run` for private local execution on Node.
</Step>
<Step title="Inspect and ship">
```bash
flue logs run_01H... # runs from the dev server
flue build --target node # production artifact
```
</Step>
</Steps>
## Related pages
<CardGroup>
<Card title="Installation" href="/installation">
Prerequisites, package managers, and first-install commands for Flue projects.
</Card>
<Card title="Flue quickstart" href="/flue-quickstart">
Scaffold, `flue dev`, `flue run`, and `flue connect` in one walkthrough.
</Card>
<Card title="Flue configuration reference" href="/flue-configuration-reference">
`flue.config.*` keys, source-root precedence, and CLI override rules.
</Card>
<Card title="Flue HTTP API reference" href="/flue-http-api-reference">
Routes mounted by `flue dev` and production servers that `flue logs` reads.
</Card>
<Card title="Flue workflows" href="/flue-workflows">
Author workflows and interpret run events.
</Card>
<Card title="Flue deploy" href="/flue-deploy">
Target selection, `flue build` output, and deployment paths.
</Card>
</CardGroup>
---
## 16. Flue configuration reference
> `flue.config.*` keys (`target`, `root`, `output`), source-root precedence, env loading, and CLI override rules.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/16-flue-configuration-reference.md
- Generated: 2026-06-18T19:28:58.078Z
### Source Files
- `withastro-flue:packages/cli/src/lib/config.ts`
- `withastro-flue:packages/cli/src/lib/config-paths.ts`
- `withastro-flue:packages/cli/src/lib/source-root.ts`
- `withastro-flue:examples/hello-world/flue.config.ts`
- `withastro-flue:examples/react-chat/flue.config.ts`
- `withastro-flue:packages/cli/src/lib/env.ts`
---
title: "Flue configuration reference"
description: "`flue.config.*` keys (`target`, `root`, `output`), source-root precedence, env loading, and CLI override rules."
---
`flue.config.{ts,mts,mjs,js,cjs,cts}` is the build-time configuration surface for the Flue CLI. It selects the deployment target, project root, and build output directory. Runtime concerns—provider registration, model selection, and platform bindings—belong in `app.ts`, not in the config file.
## Authoring API
Import `defineConfig` from `@flue/cli/config` for type checking and editor completion:
```ts title="flue.config.ts"
import { defineConfig } from '@flue/cli/config';
export default defineConfig({
target: 'node',
output: 'dist/server',
});
```
<ParamField body="defineConfig(config)" type="UserFlueConfig">
Returns the configuration unchanged. Provides TypeScript types for `flue.config.ts` authoring.
</ParamField>
The public `@flue/cli/config` subpath exports `defineConfig`, `UserFlueConfig`, and `FlueConfig`. Config discovery and resolution are internal to the CLI.
<Note>
TypeScript config files load through Node's native dynamic `import()` with type-stripping. Use erasable TypeScript only—no `enum`, runtime `namespace`, parameter properties, or decorators. Plain `.js` / `.mjs` / `.cjs` variants are also supported.
</Note>
## Configuration file discovery
When no `--config` flag is passed, the CLI scans the search directory for the first matching basename:
| Priority | Basename |
| --- | --- |
| 1 | `flue.config.ts` |
| 2 | `flue.config.mts` |
| 3 | `flue.config.mjs` |
| 4 | `flue.config.js` |
| 5 | `flue.config.cjs` |
| 6 | `flue.config.cts` |
The search directory defaults to the process working directory. Passing `--root <path>` changes the search directory to that path (after resolving it to an absolute path from `cwd`).
<ParamField body="--config <path>" type="string">
Select an explicit config file. The path resolves relative to `cwd`, not `--root`. The file must exist; a missing path throws immediately.
</ParamField>
If no config file is found and no inline values supply required fields, resolution fails with a clear error.
## Config keys
Only three keys are accepted. The schema is strict—unknown fields are rejected at load time.
### `target`
<ParamField body="target" type="'node' | 'cloudflare'">
Build and development target. Required unless supplied via `--target`.
</ParamField>
| Value | Behavior |
| --- | --- |
| `'node'` | Builds a Node.js server artifact. |
| `'cloudflare'` | Builds a Workers-compatible application. |
There is no default. Omitting `target` from both the config file and CLI flags produces:
```
[flue] Missing required `target`. Set it via `--target <node|cloudflare>` or in `flue.config.ts` as `target: "node"` (or `"cloudflare"`).
```
### `root`
<ParamField body="root" type="string">
Project root. Must not be empty. Defaults to the directory containing the loaded config file, or the search directory when no config file exists.
</ParamField>
Relative `root` values in a config file resolve from the directory containing that file. Relative values passed via `--root` resolve from `cwd`. Absolute paths are used as-is.
The resolved `root` is always an absolute path in the internal `FlueConfig` object.
### `output`
<ParamField body="output" type="string">
Build output directory. Must not be empty. Defaults to `<root>/dist`.
</ParamField>
Relative `output` values in a config file resolve from the directory containing that file—not from `root`. Relative values passed via `--output` resolve from `cwd`.
<Warning>
`output` must not resolve to the project `root` or the derived `sourceRoot`. Setting output to either location throws:
```
[flue] `output` resolves to the project root (…) / source root (…). Build artifacts must go in a separate directory, e.g. `dist/`.
```
</Warning>
## Resolved configuration
After discovery, validation, and merging, the CLI produces a `FlueConfig` object consumed by `dev`, `build`, `run`, and `connect`:
<ResponseField name="FlueConfig" type="object">
Fully resolved configuration with absolute paths.
</ResponseField>
| Field | Type | Source |
| --- | --- | --- |
| `target` | `'node' \| 'cloudflare'` | Config file or `--target` |
| `root` | `string` (absolute) | Config file, `--root`, or default |
| `sourceRoot` | `string` (absolute) | Derived from `root` (see below) |
| `output` | `string` (absolute) | Config file, `--output`, or `<root>/dist` |
`sourceRoot` is not authored in `flue.config.*`. The CLI derives it from `root`.
## Source-root precedence
Flue discovers agents, workflows, channels, and `app.ts` from `sourceRoot`, not from `root` directly. Given a resolved `root`, the CLI picks the first existing directory:
```
1. <root>/.flue/
2. <root>/src/
3. <root>/
```
When both `.flue/` and `src/` exist, `.flue/` wins. Bare-layout directories at `root` (for example `agents/`, `workflows/`) are ignored when `.flue/` is present.
<Info>
Use `.flue/` for generated or scaffolded layouts. Use `src/` for conventional TypeScript project structure. Use `root` directly only when neither subdirectory exists.
</Info>
## Merge and precedence
Configuration merges per field on a flat surface—there are no nested objects to deep-merge.
```
CLI flags (--target, --root, --output)
↓ overrides
flue.config.* file values
↓ overrides
Built-in defaults (root, output only)
```
| Field | Default when unset everywhere |
| --- | --- |
| `target` | Error—no default |
| `root` | Config-file directory, or search directory |
| `output` | `<root>/dist` |
CLI flags always win over config-file values for the field they set. Omitting a CLI flag falls through to the file value, then the default.
### Path resolution summary
| Value source | Relative path resolves from |
| --- | --- |
| `root` / `output` in config file | Directory containing the config file |
| `--root` / `--output` on CLI | Process `cwd` |
| `--config <path>` | Process `cwd` |
| Auto-discovery scan | `--root` if set, else `cwd` |
## Environment loading
Commands `flue dev`, `flue build`, `flue run`, and `flue connect` load environment variables before resolving configuration. `flue init`, `flue add`, `flue update`, `flue docs`, and `flue logs` do not use this path.
### Env file selection
The env base directory is the config-file directory when a config file is found, otherwise the search directory (`--root` or `cwd`).
<ParamField body="--env <path>" type="string">
Select one alternate `.env`-format file. Relative paths resolve from the env base directory. The file must exist when explicitly specified.
</ParamField>
Without `--env`, the CLI attempts `<env-base>/.env`. If that file does not exist, env loading is a no-op.
Passing `--env` more than once is an error.
### Precedence
Shell environment wins over file values. The env loader only sets variables that were **not** already defined in `process.env` when the loader was created:
1. Existing shell / process environment (highest)
2. Keys from the selected `.env` file (fill gaps only)
Env is applied before the config module loads, so `process.env` references inside `flue.config.ts` can read file-provided values that the shell did not already set.
### Cloudflare builds
For `target: 'cloudflare'`, the build step additionally loads Vite env prefixes (`CLOUDFLARE_`, `WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_`) from the project root during the build. Local Cloudflare development continues to follow Wrangler conventions for `.dev.vars`, `.env`, and `CLOUDFLARE_ENV`.
## CLI override flags
These flags apply to `flue dev`, `flue build`, `flue run`, and `flue connect`:
| Flag | Config key | Effect |
| --- | --- | --- |
| `--target <node\|cloudflare>` | `target` | Override or supply required target |
| `--root <path>` | `root` | Override project root; also changes config search directory |
| `--output <path>` | `output` | Override build output directory |
| `--config <path>` | — | Select config file (relative to `cwd`) |
| `--env <path>` | — | Select env file (relative to env base directory) |
`flue dev` adds `--port <number>` (default `3583`), which is not a `flue.config.*` key.
`flue init` writes a starter `flue.config.ts` with `--target` (required), `--root`, and `--force`. It does not read env files or merge existing config values.
## What does not belong in `flue.config.*`
Build-time config covers target, root, and output only. Keep runtime registration in application code:
| Concern | Location |
| --- | --- |
| Provider / model registration | `app.ts` via `registerProvider(...)` |
| Agent model defaults | `createAgent(() => ({ model: "…" }))` |
| API keys and secrets | Environment variables or platform bindings |
| HTTP routes and middleware | `app.ts` composition |
Examples in the repository intentionally leave `target` unset so `flue dev --target node` and `flue dev --target cloudflare` work without editing the config file.
## Validation errors
| Condition | Result |
| --- | --- |
| No `target` in file or CLI | `Missing required 'target'` |
| Unknown config field | `Invalid config in …` with field path |
| Invalid `target` value | `Invalid config in …` |
| Empty `root` or `output` | `Path must not be empty.` |
| `output` equals `root` or `sourceRoot` | `output resolves to the project root / source root` |
| Default export is not an object | `must export a config object as the default export` |
| Explicit `--config` path missing | `Config file not found: …` |
| Explicit `--env` path missing | `--env points at a path that doesn't exist` |
| Unsupported TypeScript syntax in config | Hint to use erasable types or plain JS |
## Examples
<CodeGroup>
```ts title="Minimal Node target"
import { defineConfig } from '@flue/cli/config';
export default defineConfig({
target: 'node',
});
```
```ts title="Custom output directory"
import { defineConfig } from '@flue/cli/config';
export default defineConfig({
target: 'node',
output: 'dist/server',
});
```
```ts title="Defer target to CLI"
import { defineConfig } from '@flue/cli/config';
export default defineConfig({});
```
</CodeGroup>
<RequestExample>
```bash
# Override config-file values for one invocation
flue build --target cloudflare --root ./my-app --output ./build
# Staging credentials without editing the config file
flue run summarize --target node --env .env.staging
# Config outside the project root
flue dev --config ../shared/flue.config.ts
```
</RequestExample>
## Related pages
<CardGroup>
<Card title="Flue project layout" href="/flue-project-layout">
Source-root resolution, agent and workflow module placement, and discovery boundaries.
</Card>
<Card title="Flue CLI reference" href="/flue-cli-reference">
Command flags, defaults, and exit behavior for all `flue` subcommands.
</Card>
<Card title="Flue deploy" href="/flue-deploy">
Target-specific build artifacts and deployment integration.
</Card>
<Card title="Flue quickstart" href="/flue-quickstart">
End-to-end workflow from scaffold through `flue dev` and `flue run`.
</Card>
</CardGroup>
---
## 17. Flue HTTP API reference
> Mounted routes (`/agents`, `/workflows`, `/runs`, `/channels`, `/openapi.json`), admission semantics, stream reads, and error envelopes.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/17-flue-http-api-reference.md
- Generated: 2026-06-18T19:29:25.095Z
### Source Files
- `withastro-flue:packages/runtime/src/runtime/flue-app.ts`
- `withastro-flue:packages/runtime/src/runtime/handle-agent.ts`
- `withastro-flue:packages/runtime/src/errors.ts`
- `withastro-flue:packages/runtime/test/workflow-runs.test.ts`
- `withastro-flue:packages/sdk/src/index.ts`
- `withastro-flue:examples/hello-world/src/app.ts`
---
title: "Flue HTTP API reference"
description: "Mounted routes (`/agents`, `/workflows`, `/runs`, `/channels`, `/openapi.json`), admission semantics, stream reads, and error envelopes."
---
`flue()` from `@flue/runtime/routing` returns a mountable Hono sub-app. Mount it at any prefix in `app.ts` (commonly `/` or `/api`); all paths below are relative to that prefix. The generated runtime calls `configureFlueRuntime()` before serving, which wires agent handlers, workflow handlers, channel routes, run stores, and error rendering.
| Route | Methods | Role |
| --- | --- | --- |
| `/openapi.json` | `GET` | OpenAPI 3 document for agent and workflow invocation |
| `/agents/:name/:id` | `POST`, `GET`, `HEAD` | Prompt an agent instance; read its Durable Streams event log |
| `/workflows/:name` | `POST` | Start a workflow run |
| `/runs/:runId` | `GET`, `HEAD` | Read workflow-run events; `?meta` returns run record JSON |
| `/channels/:name/*` | Per-channel | Verified webhook ingress (method + suffix dispatch) |
Only agents and workflows that opt into HTTP transport appear in the manifest and are invocable. Unmatched paths outside `flue()` fall through to your outer Hono app or `createDefaultFlueApp()`'s `RouteNotFoundError` envelope.
## Mounting
```typescript
import { flue } from '@flue/runtime/routing';
import { Hono } from 'hono';
const app = new Hono();
app.route('/', flue()); // routes live at /agents/..., /workflows/..., etc.
export default app;
```
With a prefix mount (`app.route('/api', flue())`), admission responses derive absolute `streamUrl` values under that prefix — for example `http://host/api/runs/run_01…` for workflow runs admitted at `/api/workflows/daily-report`.
<Note>
`createDefaultFlueApp()` mounts `flue()` at `/` and applies canonical error envelopes for unmatched routes. Custom `app.ts` projects compose `flue()` alongside their own routes and middleware.
</Note>
## Invocation modes
Agent and workflow `POST` routes share one query contract:
<ParamField query="wait" type="'result'">
When set to `result`, the server waits for terminal completion and returns **200** with a synchronous JSON envelope. Omit for default **202** admission.
</ParamField>
Invalid `wait` values (for example `?wait=results`) return **400** `invalid_request`.
### Admission (202)
Default mode admits work and returns stream coordinates immediately. The handler continues in the background; the HTTP response does not wait for model output or workflow completion.
<ResponseField name="streamUrl" type="string">
Absolute URL for reading events. Agent prompts stream at the same URL; workflow runs stream at sibling `/runs/:runId` under the mount prefix.
</ResponseField>
<ResponseField name="offset" type="string">
Opaque Durable Streams offset captured at admission. Reading from this offset replays events from the start of the admitted interaction or run. First prompt on a fresh agent instance is typically `-1`.
</ResponseField>
<ResponseField name="runId" type="string">
Present on workflow admissions only. Format: `run_` + ULID (for example `run_01HJKMNP-TV-Z26CHARS`).
</ResponseField>
<ResponseField name="submissionId" type="string">
Present on agent admissions only. Correlates the admitted prompt with attached-agent events.
</ResponseField>
Admission responses also set DS stream-creation headers:
- `Location` — same value as `streamUrl`
- `Stream-Next-Offset` — same value as `offset`
<RequestExample>
```bash
curl -X POST http://localhost:3000/workflows/daily-report \
-H 'Content-Type: application/json' \
-d '{"report":"weekly"}'
```
</RequestExample>
<ResponseExample>
```json
{
"runId": "run_01HJKMNP-TV-Z26CHARS",
"streamUrl": "http://localhost:3000/runs/run_01HJKMNP-TV-Z26CHARS",
"offset": "-1"
}
```
Status **202**. Headers include `Location` and `Stream-Next-Offset`.
</ResponseExample>
### Synchronous (`?wait=result`)
Waits for terminal completion, then returns **200** JSON:
| Field | Agents | Workflows |
| --- | --- | --- |
| `result` | Terminal prompt result (`null` if handler returns `undefined`) | Terminal workflow return value |
| `streamUrl` | Request URL (query stripped) | `/runs/:runId` under mount prefix |
| `offset` | Offset at admission | Offset at admission |
| `runId` | — | Generated run id |
| `submissionId` | Admission receipt | — |
Session-layer `FlueError` subclasses that escape during `?wait=result` (for example `model_not_configured`) render as typed **500** envelopes, not opaque `internal_error`.
Workflow admission requires a configured run store. Missing store returns **501** `run_store_unavailable` before the handler executes.
## Agents
:::endpoint POST /agents/:name/:id
Prompt a persistent agent instance. Body is required JSON. Default returns **202** admission; `?wait=result` blocks until the prompt completes.
:::
<ParamField path="name" type="string" required>
Agent module name from `agents/<name>.ts`. Must be HTTP-exposed in the build manifest.
</ParamField>
<ParamField path="id" type="string" required>
Caller-chosen instance id (for example a user or session key). Empty segments are rejected.
</ParamField>
<ParamField body="message" type="string" required>
Prompt text.
</ParamField>
<ParamField body="images" type="array">
Optional image attachments. Each item: `{ "type": "image", "data": string, "mimeType": string }`. `data` max length 14 MiB characters.
</ParamField>
<RequestExample>
```bash
curl -X POST http://localhost:3000/agents/assistant/customer-123 \
-H 'Content-Type: application/json' \
-d '{"message":"Summarize ticket #42"}'
```
</RequestExample>
<ResponseExample>
```json
{
"streamUrl": "http://localhost:3000/agents/assistant/customer-123",
"offset": "-1",
"submissionId": "submission-abc"
}
```
</ResponseExample>
Allowed methods: `POST`, `GET`, `HEAD`. Other methods return **405** with `Allow: GET, HEAD, POST`.
`agentRouteMiddleware` runs for all three methods, so auth and rate limits apply to stream reads as well as prompts.
<Tip>
Use `dispatch()` from application code for asynchronous delivery to a continuing session without creating workflow-run history. HTTP prompts are attached interactions with durable admission on supported targets.
</Tip>
## Workflows
:::endpoint POST /workflows/:name
Start an HTTP-exposed workflow. Body is optional JSON (empty body treated as `{}`). Default **202** admission; `?wait=result` for synchronous completion.
:::
<ParamField path="name" type="string" required>
Workflow module name. Must be registered and HTTP-exposed.
</ParamField>
<ParamField body="payload" type="object">
Workflow-defined JSON object. Schema is workflow-specific; OpenAPI documents `additionalProperties: true`.
</ParamField>
Only `POST` is allowed. The server generates `runId`, persists run admission, creates the event stream, starts execution in the background, and returns coordinates.
Workflow handlers receive payload and request via `FlueContextInternal` (`ctx.payload`, `ctx.req`). Handler throws in `?wait=result` mode yield **500** `internal_error` to the client while the run record is finalized as `errored`.
## Runs
:::endpoint GET /runs/:runId
Read workflow-run events via the Durable Streams protocol. `HEAD` returns stream metadata without a body.
:::
:::endpoint GET /runs/:runId?meta
Return plain `RunRecord` JSON (`content-type: application/json`). Stream query params (`offset`, `live`, `tail`) are ignored.
:::
`runId` is opaque; owning workflow is resolved through the run store before the response is sent. Workflow `workflowRouteMiddleware` runs **before** existence is disclosed, so auth boundaries apply even for unknown ids.
| Condition | Result |
| --- | --- |
| Run pointer missing or workflow not in current manifest | **404** `run_not_found` |
| Run store not configured | **501** `run_store_unavailable` |
| Stream not yet created | **404** `run_not_found` (run streams) |
Non-`GET`/`HEAD` methods return **405** `Allow: GET, HEAD`.
Run ids match `^run_[0-9A-HJKMNP-TV-Z]{26}
Flue & Eve Agent Frameworks Documentation · Grok Docs
.
## Stream reads
Agent (`GET /agents/:name/:id`) and run (`GET /runs/:runId`) endpoints implement the [Durable Streams protocol](https://github.com/durable-streams/durable-streams/blob/main/PROTOCOL.md): catch-up, long-poll, and SSE. Streams are read-only over HTTP; writes happen internally during execution.
```text
POST /agents/:name/:id ──admit──► events appended to agents/:name/:id
POST /workflows/:name ──admit──► events appended to runs/:runId
GET same agent URL ──read──► DS catch-up / long-poll / SSE
GET /runs/:runId ──read──► DS catch-up / long-poll / SSE
```
<Warning>
Agent streams return **404** `stream_not_found` until the instance receives its first **admitted** prompt. Failed admission does not create a phantom stream.
</Warning>
### Query parameters
<ParamField query="offset" type="string">
Start position. Values: `-1` (beginning), `now` (tail; requires `live`), or `digits_digits` DS offset. Required when `live` is set.
</ParamField>
<ParamField query="live" type="'long-poll' | 'sse'">
Live tailing mode. `long-poll`: **200** with data or **204** empty (30s timeout). `sse`: `text/event-stream` with `event: data` and `event: control` frames.
</ParamField>
<ParamField query="tail" type="integer">
With `offset=-1`, return only the last N events. Must be ≥ 1.
</ParamField>
<ParamField query="cursor" type="string">
Client cursor for long-poll/SSE control events (`Stream-Cursor` header).
</ParamField>
### Response headers
| Header | Meaning |
| --- | --- |
| `Stream-Next-Offset` | Read cursor for the next request |
| `Stream-Up-To-Date` | `true` when client is at tail |
| `Stream-Closed` | `true` when stream is closed (workflow `run_end` finalized) |
| `Stream-Cursor` | Long-poll/SSE cursor token |
| `ETag` | Conditional caching for catch-up reads |
| `X-Content-Type-Options` | `nosniff` (all stream and error responses) |
| `Cross-Origin-Resource-Policy` | `cross-origin` |
Catch-up **GET** returns `application/json` body: JSON array of event objects. Long-poll abort returns **499** with security headers only.
Use `@flue/sdk` `createFlueClient()` for typed invocation and DS-backed `stream()` / `events()` helpers.
## Channels
:::endpoint ALL /channels/:name/:suffix
Dispatch inbound webhooks to channel-defined handlers. Not described in OpenAPI.
:::
Routes map from `channelHandlers[name]` keys shaped as `"METHOD /suffix"` (for example `"POST /events"`, `"HEAD /webhook"`). The HTTP path after `/channels/:name` must match the suffix.
| Request | Outcome |
| --- | --- |
| `/channels/:name` (no suffix) | **404** `route_not_found` |
| Unknown suffix | **404** `route_not_found` |
| Known suffix, wrong method | **405** with `Allow` listing permitted methods |
| Handler returns non-`Response` | **500** `internal_error` |
Handlers receive Hono `Context` and must return a `Response`. Multi-segment suffixes are supported (`/channels/custom/webhooks/retries` → suffix `webhooks/retries`).
## OpenAPI
:::endpoint GET /openapi.json
Machine-readable contract for public invocation routes. Generated from route metadata via `hono-openapi`.
:::
Documents:
- `POST /agents/{name}/{id}` — `invokeAgent`
- `POST /workflows/{name}` — `invokeWorkflow`
Both operations declare `x-flue-invocation-modes: ["accepted", "wait-result"]` and standard error responses (**400**, **404**, **405**, **415**, **500**, **501**). `info.version` reflects the built runtime version. Stream read and channel routes are not included.
## Error envelopes
Every Flue HTTP error returns JSON:
```json
{
"error": {
"type": "agent_not_found",
"message": "Agent \"missing\" is not registered.",
"details": "Verify the agent name is correct.",
"dev": "Available agents: \"assistant\".\n..."
}
}
```
<ResponseField name="error.type" type="string">
Stable machine-readable identifier (snake_case).
</ResponseField>
<ResponseField name="error.message" type="string">
One-sentence caller-safe summary. Always present.
</ResponseField>
<ResponseField name="error.details" type="string">
Longer caller-safe guidance. Always present; may be empty string.
</ResponseField>
<ResponseField name="error.dev" type="string">
Developer-only hints (available siblings, filesystem layout). Present only when `devMode` is enabled **and** the error class populated a non-empty `dev` field.
</ResponseField>
<ResponseField name="error.meta" type="object">
Structured data on select errors. Rare on HTTP routes.
</ResponseField>
Unknown thrown values log server-side and render **500** `internal_error` without leaking internals.
### HTTP error types
| `type` | Status | When |
| --- | --- | --- |
| `invalid_request` | 400 | Malformed path/query/body validation |
| `invalid_json` | 400 | Body present but not valid JSON |
| `agent_not_found` | 404 | Unknown or non-HTTP agent name |
| `workflow_not_found` | 404 | Unknown, non-HTTP, or internal-only workflow |
| `route_not_found` | 404 | Unmatched Flue route (including channels) |
| `run_not_found` | 404 | Unknown run id or missing run stream |
| `stream_not_found` | 404 | Agent stream before first admitted prompt |
| `method_not_allowed` | 405 | Wrong HTTP method; includes `Allow` header |
| `unsupported_media_type` | 415 | Body without `application/json` when body expected |
| `run_store_unavailable` | 501 | Run store not wired for workflow admission or lookup |
| `internal_error` | 500 | Unhandled non-Flue error or admission persistence failure |
Non-HTTP `FlueError` subclasses (session, submission, model configuration) that reach the HTTP layer render with their `type` and message at **500**.
Param/query validation failures from Valibot throw `invalid_request` with flattened issue messages — raw validation objects never reach the wire.
## Client SDK
`createFlueClient({ baseUrl, headers })` from `@flue/sdk` maps to the same routes:
| SDK method | HTTP |
| --- | --- |
| `agents.send(name, id, opts)` | `POST /agents/:name/:id` |
| `agents.prompt(name, id, opts)` | `POST /agents/:name/:id?wait=result` |
| `agents.stream(name, id, opts)` | `GET /agents/:name/:id` (DS client) |
| `workflows.invoke(name, opts)` | `POST /workflows/:name` |
| `workflows.invoke(name, { wait: 'result', ... })` | `POST /workflows/:name?wait=result` |
| `runs.get(runId)` | `GET /runs/:runId?meta` |
| `runs.stream(runId, opts)` | `GET /runs/:runId` (DS client) |
| `runs.events(runId, opts)` | Catch-up loop over `GET /runs/:runId` |
The SDK treats `streamUrl` and `offset` from admission responses as opaque — clients must not construct DS offsets themselves.
## Related pages
<CardGroup>
<Card title="Flue quickstart" href="/flue-quickstart">
Run `flue dev`, invoke workflows, and connect to agent instances from the CLI.
</Card>
<Card title="Build Flue agents" href="/build-flue-agents">
Author agents, HTTP route middleware, and `createAgent` definitions that back `/agents` routes.
</Card>
<Card title="Flue workflows" href="/flue-workflows">
Define workflow modules and interpret run events streamed from `/runs/:runId`.
</Card>
<Card title="Flue channels" href="/flue-channels">
Discover channel modules and mount verified webhook routes under `/channels/:name`.
</Card>
<Card title="Runtime models" href="/runtime-models">
Compare Flue sessions, workflow runs, and dispatch receipts with Eve session contracts.
</Card>
<Card title="Flue persistence reference" href="/flue-persistence-reference">
Run stores, event stream stores, and durable admission adapters behind HTTP execution.
</Card>
</CardGroup>
---
## 18. Flue persistence reference
> `PersistenceAdapter` contract, store interfaces, built-in database adapters, and `db.ts` discovery for durable execution.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/18-flue-persistence-reference.md
- Generated: 2026-06-18T19:30:00.583Z
### Source Files
- `withastro-flue:packages/runtime/src/adapter.ts`
- `withastro-flue:packages/postgres/src/index.ts`
- `withastro-flue:packages/mysql/README.md`
- `withastro-flue:packages/libsql/README.md`
- `withastro-flue:blueprints/database.md`
- `withastro-flue:CHANGELOG.md`
---
title: "Flue persistence reference"
description: "`PersistenceAdapter` contract, store interfaces, built-in database adapters, and `db.ts` discovery for durable execution."
---
Flue persists agent sessions, submission lifecycles, workflow-run records, and event streams through a single `PersistenceAdapter` contract. On the Node target, you configure persistence by default-exporting an adapter from a source-root `db.ts` file; the build discovers it, wires it into the generated server, and calls `migrate()` then `connect()` once at startup. Without `db.ts`, Node falls back to in-memory SQLite that lasts only for the process lifetime.
## What persistence stores
A Flue database holds **runtime state**, not application business data.
| Stored by Flue | Not stored by Flue |
| --- | --- |
| Agent session messages, compaction state, and persisted image chunks | Sandbox files and installed dependencies |
| Accepted direct prompts and `dispatch(...)` submissions, including turn journals, claims, and leases | External API side effects |
| Workflow-run records and persisted run events | Customer records, tickets, payments, or other app-owned data |
| Run indexing for `/runs` lookups and `listRuns()` | Provider credentials or secrets |
Durable persistence enables interruption recovery: accepted work survives process restart, leases coordinate concurrent coordinators, and turn journals back replay-safe recovery. See [Runtime models](/runtime-models) for how sessions, runs, and dispatch receipts relate to durable execution.
## `db.ts` discovery and wiring
`db.ts` is a **Node-target** concern. Cloudflare uses Durable Object SQLite automatically and **rejects** `db.ts` at build time.
### File location
Place exactly one `db.ts` in the project source root. Flue resolves the source root in this order:
1. `<project-root>/.flue/`
2. `<project-root>/src/`
3. `<project-root>/`
Extension priority matches other optional entries: `db.ts` > `db.mts` > `db.js` > `db.mjs`.
The file must **default-export** a `PersistenceAdapter`. Blueprint-generated files include a first-line marker such as `// flue-blueprint: database/postgres@1`.
### Build-time and startup lifecycle
During `flue build`, the Node plugin discovers `db.ts` and imports it into the generated server entry. At server startup:
<Steps>
<Step title="Validate the export">
The generated entry checks that the default export exposes `connect()`.
</Step>
<Step title="Run migrate()">
When present, `migrate()` runs once to create schema idempotently and stamp the schema version.
</Step>
<Step title="Await connect()">
`connect()` returns `{ executionStore, runStore, eventStreamStore }`. An unreachable or misconfigured database fails at boot, not on the first request.
</Step>
<Step title="Release on shutdown">
When present, `close()` runs during graceful shutdown to release pools and file handles.
</Step>
</Steps>
Without `db.ts`, the generated server uses `sqlite()` from `@flue/runtime/node` with an in-memory database — equivalent to omitting a file path.
Verify discovery with `flue build`: the build summary lists the resolved `database` path when `db.ts` is found.
## `PersistenceAdapter` contract
Import types and helpers from `@flue/runtime/adapter`. There is one contract for every backend — no SQL-only or expert tiers.
```ts
interface PersistenceAdapter {
connect(): PersistenceStores | Promise<PersistenceStores>;
migrate?(): void | Promise<void>;
close?(): void | Promise<void>;
}
interface PersistenceStores {
readonly executionStore: AgentExecutionStore;
readonly runStore: RunStore;
readonly eventStreamStore: EventStreamStore;
}
```
<ResponseField name="connect()" type="PersistenceStores | Promise<PersistenceStores>">
Opens the database and returns all three stores. Async pool setup, remote handshakes, and schema-version checks (for adapters without `migrate()`) belong here. Called once at startup.
</ResponseField>
<ResponseField name="migrate()" type="void | Promise<void>">
Optional. Brings the store to the current schema/format version before `connect()`. Creates missing schema idempotently and stamps the version on first creation.
</ResponseField>
<ResponseField name="close()" type="void | Promise<void>">
Optional. Releases connections, pools, or file handles during shutdown.
</ResponseField>
Factory functions in adapter packages (`sqlite()`, `postgres()`, `mysql()`, `libsql()`, and others) return this interface.
## Store interfaces
`PersistenceStores` bundles three stores. Each documents per-method invariants in terms of observable behavior — atomicity, idempotency, and gating — so non-SQL backends are first-class implementations.
### `AgentExecutionStore`
Groups session storage and submission lifecycle storage.
```ts
interface AgentExecutionStore {
readonly sessions: SessionStore;
readonly submissions: AgentSubmissionStore;
}
```
**`SessionStore`** persists complete session records:
| Method | Contract |
| --- | --- |
| `save(id, data)` | Persist the full `SessionData` under the Flue storage key. |
| `load(id)` | Return saved data, or `null` when none exists. |
| `delete(id)` | Remove the stored session record. |
**`AgentSubmissionStore`** owns ordered admission, claims, turn journals, stream chunks, attempt markers, lease renewal, and session deletion coordination for direct prompts and `dispatch(...)` input.
Stability note: `SessionStore`, `RunStore`, and `EventStreamStore` are stable. The `AgentSubmissionStore` turn-journal, stream-chunk, and lease method groups mirror the durable-execution engine and may change before 1.0.
Key invariant groups:
| Group | Behavior |
| --- | --- |
| Admission | `admitDispatch()` is idempotent by dispatch id; same id with different payload returns conflict; `admitDirect()` admits queued submissions with replay idempotency. Both throw while the session is being deleted. |
| Claims | `claimSubmission()` atomically moves queued → running only when the submission is the runnable head of its session. Concurrent claims for the same submission must never both succeed. |
| Turn journal | At most one journal per submission. Phases advance only for uncommitted journals owned by the calling attempt. `replaceTurnJournalAttempt()` is the recovery handoff. |
| Stream chunks | `appendStreamChunkSegment()` is insert-only; duplicate keys return `false` without overwriting. |
| Leases | `renewLeases()` extends expiry for running submissions owned by the coordinator. `listExpiredSubmissions()` returns running submissions past `leaseExpiresAt`. |
| Deletion | `deleteSession()` coordinates three-phase settled-state removal with a durable deletion marker. `listPendingSessionDeletions()` surfaces markers left by crashed processes. |
Default durability constants exported from `@flue/runtime/adapter`:
| Constant | Value |
| --- | --- |
| `DURABILITY_DEFAULT_MAX_ATTEMPTS` | 10 |
| `DURABILITY_DEFAULT_TIMEOUT_MS` | 3,600,000 (1 hour) |
| `LEASE_DURATION_MS` | 30,000 (30 seconds) |
### `RunStore`
Persists workflow-run records and serves lookup and listing for `/runs`, `flue logs`, and inspection primitives. Event payloads live in `EventStreamStore`.
| Method | Contract |
| --- | --- |
| `createRun()` | Persist a new `active` run. Idempotent, first-writer-wins on duplicate `runId`. |
| `endRun()` | Finalize with terminal status, result, or error. No-op when unknown. |
| `getRun()` | Full run record, or `null`. |
| `lookupRun()` | `RunPointer` projection (excludes `payload`, `result`, `error`). |
| `listRuns()` | Newest first, filterable by status/workflow, paginated via opaque cursor. |
Helpers: `encodeRunCursor()`, `decodeRunCursor()`, `DEFAULT_LIST_LIMIT`, `MAX_LIST_LIMIT`.
### `EventStreamStore`
Append-only event streams for agent instances and workflow runs. Paths are typically `agents/<name>/<id>` or `runs/<runId>`.
| Method | Contract |
| --- | --- |
| `createStream()` | Idempotent stream creation. |
| `appendEvent()` | Append and return a new Durable Streams offset. Throws on missing stream. |
| `readEvents()` | Read strictly after `offset`. Missing stream returns empty, up-to-date result (no throw). |
| `closeStream()` | Close the stream. |
| `getStreamMeta()` | Stream metadata, or `null`. |
| `subscribe()` | In-process listener for appends or closure on this store instance. |
Offsets use Durable Streams format (`<readSeq>_<seq>`) plus sentinel `"-1"`. Use `formatOffset()` and `parseOffset()` from `@flue/runtime/adapter`.
## Built-in adapters
<Tabs>
<Tab title="Node built-in">
**`sqlite(path?)`** from `@flue/runtime/node` is the zero-dependency built-in adapter. Omit the path or pass `':memory:'` for process-lifetime storage; pass a file path for restart-surviving state on a single host.
```ts title="src/db.ts"
import { sqlite } from '@flue/runtime/node';
export default sqlite('./data/flue.db');
```
Uses `node:sqlite` with WAL mode for file-backed databases. Shares the SQL implementation with Cloudflare Durable Object SQLite.
</Tab>
<Tab title="SQL packages (BYO driver)">
SQL adapter packages accept a **runner** with three functions — `query`, `transaction`, and `close` — wrapped around your configured driver. Flue does not bundle production drivers.
| Package | Placeholders | Install |
| --- | --- | --- |
| `@flue/postgres` | `$1`, `$2`, … | `flue add database postgres` |
| `@flue/mysql` | `?` | `flue add database mysql` |
| `@flue/libsql` | `?` | `flue add database libsql` or `flue add database turso` |
<CodeGroup>
```ts title="Postgres (porsager postgres)"
import { postgres, type PostgresQuery } from '@flue/postgres';
import sql from 'postgres';
const db = sql(process.env.DATABASE_URL!);
export default postgres({
query: (text, params) => db.unsafe(text, params),
transaction: <T>(fn: (tx: { query: PostgresQuery }) => Promise<T>) =>
db.begin((tx) => fn({ query: (text, params) => tx.unsafe(text, params) })) as Promise<T>,
close: () => db.end(),
});
```
```ts title="MySQL (mysql2)"
import { mysql, type MysqlQuery } from '@flue/mysql';
import mysql2 from 'mysql2/promise';
const pool = mysql2.createPool(process.env.MYSQL_URL!);
const toRows = (result: unknown): Record<string, unknown>[] =>
Array.isArray(result) ? result.map((row) => ({ ...row })) : [];
export default mysql({
query: async (text, params = []) => {
const [result] = await pool.execute(text, params);
return toRows(result);
},
transaction: async <T>(fn: (tx: { query: MysqlQuery }) => Promise<T>) => {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
const result = await fn({
query: async (text, params = []) => {
const [rows] = await connection.execute(text, params);
return toRows(rows);
},
});
await connection.commit();
return result;
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
},
close: () => pool.end(),
});
```
```ts title="libSQL / Turso"
import { libsql } from '@flue/libsql';
import { createClient, type ResultSet } from '@libsql/client';
const client = createClient({
url: process.env.LIBSQL_URL!,
authToken: process.env.LIBSQL_AUTH_TOKEN,
});
const toRows = (rs: ResultSet) =>
rs.rows.map((row) => Object.fromEntries(rs.columns.map((col) => [col, row[col]])));
export default libsql({
query: (text, params = []) => client.execute({ sql: text, args: params }).then(toRows),
transaction: (fn) => client.transaction('write').then(async (tx) => {
try {
const result = await fn({
query: (text, params = []) => tx.execute({ sql: text, args: params }).then(toRows),
});
await tx.commit();
return result;
} catch (error) {
await tx.rollback();
throw error;
} finally {
tx.close();
}
}),
close: () => client.close(),
});
```
</CodeGroup>
MySQL requires MySQL 8 with InnoDB. For local `file:` libSQL databases, serialize operations in the runner to avoid `SQLITE_BUSY` contention.
</Tab>
<Tab title="Non-SQL packages">
Driver-free adapters for backends that are not SQL-shaped:
| Package | Install |
| --- | --- |
| `@flue/redis` | `flue add database redis` |
| `@flue/mongodb` | `flue add database mongodb` |
Blueprints also exist for Supabase (Postgres), Valkey, and custom backends via `flue add database <url>`.
</Tab>
<Tab title="Cloudflare">
No `db.ts`. Generated agent and workflow Durable Objects use SQLite automatically. Run indexing lives in the generated `FlueRegistry` Durable Object. A present `db.ts` causes the Cloudflare build to fail with an explicit error.
</Tab>
</Tabs>
### Choosing an adapter
| Use case | Adapter |
| --- | --- |
| Local dev, restart persistence optional | No `db.ts` (in-memory) or file-backed `sqlite()` |
| Single-host Node deployment | File-backed `sqlite()` |
| Multi-replica Node or host replacement | `@flue/postgres`, `@flue/mysql`, `@flue/libsql`, or hosted Turso |
| Existing operational environment | Match your ops stack (`mysql`, `redis`, `mongodb`, etc.) |
| Cloudflare deployment | Built-in Durable Object SQLite |
## Schema versioning
Every adapter must durably record its schema/format version at store creation and **fail loudly** before reading or writing when opened against an unknown or newer version.
SQL adapters use a one-row `flue_meta` table with key `schema_version`. Non-SQL adapters implement the same obligation natively.
Exports from `@flue/runtime/adapter`:
| Export | Purpose |
| --- | --- |
| `FLUE_SCHEMA_VERSION` | Current version to stamp at creation (currently `1`). |
| `assertSupportedFlueSchemaVersion(storedVersion)` | Throws on mismatch. |
| `PersistedSchemaVersionError` | Error type for version conflicts. |
`migrate()` creates schema idempotently — there is no separate migration CLI command. A database written by a newer Flue version refuses to start after a rollback rather than corrupting state.
## Custom adapters
When built-in adapters do not fit, implement `PersistenceAdapter` directly:
1. Import contract types from `@flue/runtime/adapter` only — not `@flue/runtime/internal`.
2. Honor durable submission invariants with your backend's real primitives (transactions, conditional writes, atomic counters).
3. Run `migrate()` at startup with idempotent schema/collection creation.
4. Return all three stores from `connect()`.
5. Validate with contract suites from `@flue/runtime/test-utils`:
```ts
import {
defineEventStreamStoreContractTests,
defineRunStoreContractTests,
defineStoreContractTests,
} from '@flue/runtime/test-utils';
defineStoreContractTests('MyBackend', {
async create() {
const adapter = myBackend();
await adapter.migrate?.();
const { executionStore } = await adapter.connect();
return executionStore;
},
async cleanup() { /* close connections, delete temp state */ },
});
```
Use `flue add database <url>` to scaffold from the generic database blueprint, or `flue update database <name|url>` to refresh an existing adapter while preserving customizations.
Adapter helpers for storage-key, timestamp, payload-validation, cursor, and offset semantics: `createSessionStorageKey()`, `parseAcceptedAt()`, `isSubmissionPayload()`, `clampLimit()`, and persisted image-chunk utilities.
## Environment and credentials
Read connection strings from the environment — commonly `DATABASE_URL`, `MYSQL_URL`, `LIBSQL_URL` — and load them through project conventions or `flue dev --env <file>` / `flue run --env <file>`. Never commit real credentials. Blueprint-generated `db.ts` files expect you to supply values through env files or your secret manager.
## Troubleshooting
| Symptom | Likely cause |
| --- | --- |
| Build fails on Cloudflare with `db.ts` error | Remove `db.ts` or move it outside the source root; Cloudflare uses DO SQLite. |
| Server fails at startup with persistence error | Database unreachable, wrong credentials, or `connect()` validation failed. Check env vars and network. |
| `PersistedSchemaVersionError` at startup | Database was written by a different Flue version. Align versions or provision a fresh store. |
| Accepted work lost on restart | No `db.ts`, or `sqlite()` without a file path (in-memory). Add file-backed `sqlite()` or a shared adapter. |
| `SQLITE_BUSY` with local libSQL file | Concurrent writes to an embedded file. Serialize runner operations or use hosted Turso/sqld. |
| Session admissions blocked after crash | Orphaned deletion marker. Coordinator resumes via `listPendingSessionDeletions()` at startup. |
## Related pages
<CardGroup>
<Card title="Flue project layout" href="/flue-project-layout">
Source-root resolution (`.flue/` vs `src/`), where `db.ts` lives, and discovery boundaries.
</Card>
<Card title="Runtime models" href="/runtime-models">
Sessions, workflow runs, dispatch receipts, and durable execution semantics.
</Card>
<Card title="Flue configuration reference" href="/flue-configuration-reference">
`flue.config.*` keys, source-root precedence, and env loading.
</Card>
<Card title="Flue workflows" href="/flue-workflows">
Workflow runs, event streams, and `flue logs` inspection.
</Card>
<Card title="Flue HTTP API reference" href="/flue-http-api-reference">
`/runs` routes, stream reads, and run metadata.
</Card>
<Card title="Flue deploy" href="/flue-deploy">
Node vs Cloudflare targets and persistence implications per platform.
</Card>
</CardGroup>
---
## 19. Eve CLI reference
> Commands and flags for `eve init`, `info`, `build`, `start`, `dev`, `link`, `deploy`, `eval`, and `channels` subcommands.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/19-eve-cli-reference.md
- Generated: 2026-06-18T19:30:30.560Z
### Source Files
- `vercel-eve:docs/reference/cli.md`
- `vercel-eve:README.md`
- `vercel-eve:packages/eve/package.json`
- `vercel-eve:scripts/dev.mjs`
- `vercel-eve:docs/reference/typescript-api.md`
- `vercel-eve:docs/getting-started.mdx`
---
title: "Eve CLI reference"
description: "Commands and flags for `eve init`, `info`, `build`, `start`, `dev`, `link`, `deploy`, `eval`, and `channels` subcommands."
---
The `eve` binary ships with the `eve` npm package (`bin: eve`) and runs from your application root — the current working directory. Invoking `eve` with no subcommand runs `eve dev`. Most commands load local environment files before work begins; `eve info` inspects the tree without requiring a running server.
## Command overview
| Command | Description |
| --- | --- |
| `eve init [target]` | Scaffold a new agent, or add one to an existing project |
| `eve info` | Print resolved application details, discovery diagnostics, and artifact paths |
| `eve build` | Compile `.eve/` artifacts and build the Nitro host output |
| `eve start` | Serve the built `.output/` application |
| `eve dev` | Start the local dev server and open the terminal UI |
| `eve dev <url>` | Connect the terminal UI to an existing server URL |
| `eve link` | Link the directory to a Vercel project and pull AI Gateway credentials |
| `eve deploy` | Deploy the agent to Vercel production |
| `eve eval` | Run evals against the local app or a remote target |
| `eve channels add [kind]` | Scaffold a channel (`slack` or `web`) |
| `eve channels list` | List user-authored channels |
## Global behavior
**Application root.** Every command resolves the app root from `process.cwd()`. Run commands from the directory that contains `agent/` and `package.json`.
**Node engine.** The `eve` package requires Node `>=24`.
**Environment files.** `eve dev`, `eve build`, `eve start`, and `eve eval` load development env files from the app root, highest precedence first:
1. `.env.development.local`
2. `.env.local`
3. `.env.development`
4. `.env`
Variables already set in the parent process keep precedence on first load. Subsequent reloads (for example after `eve link` updates `.env.local` during `eve dev`) refresh values from these files. A running `eve dev` reloads env files automatically after linking.
**Default subcommand.** An empty argv list is rewritten to `["dev"]` before parsing.
## `eve init`
Scaffolds a new Eve agent or adds agent files to an existing npm project.
```bash
eve init [target] [--channel-web-nextjs]
```
### Target modes
| Target | Behavior |
| --- | --- |
| `my-agent` (name) | Creates a new subdirectory `my-agent/` with a full scaffold |
| `.` or omitted | Scaffolds in the current directory when empty, or updates it like `eve init .` |
| Existing directory | Adds `agent/` and missing `eve`, `ai`, and `zod` dependencies; requires `package.json` and no existing `agent/` |
Either mode installs dependencies, initializes Git for fresh scaffolds, and hands off to `eve dev` through the detected package manager. When the CLI is launched by a coding agent, it prints the dev command instead of spawning the interactive TUI. Fresh scaffolds pass `--input /model` so the TUI opens the model setup flow.
`eve init` does not create or link a Vercel project.
<ParamField body="--channel-web-nextjs" type="flag">
Add the Web Chat application (a Next.js app) during a fresh scaffold. Rejected when adding to an existing project — run `eve channels add web` there instead.
</ParamField>
## `eve info`
Prints the resolved application: discovered tools, skills, subagents, schedules, channels, HTTP routes, compile status, and artifact paths. Run this first when discovery or routing behaves unexpectedly.
```bash
eve info [--json]
```
<ParamField body="--json" type="flag">
Emit a machine-readable JSON document with `appRoot`, `agentRoot`, `model`, `tools`, `channels`, `messaging` route paths, and `.eve/` artifact locations.
</ParamField>
Human output includes compile status (`ready`, `failed`, or `unavailable`), discovery error and warning counts, and the active session messaging routes (`create`, `continue`, `stream`).
## `eve build`
Compiles authored files under `agent/` into `.eve/` and builds the Nitro host output directory.
```bash
eve build
```
No flags. On success, prints the built output path (typically under `.output/`). On discovery errors, throws with a full diagnostics report listing severity, message, and source path for each item, plus the diagnostics artifact path.
### `.eve/` artifacts
These files are written under `.eve/` and preserved even on partial failure:
| Artifact | Purpose |
| --- | --- |
| `.eve/discovery/agent-discovery-manifest.json` | Filesystem discovery results |
| `.eve/discovery/diagnostics.json` | Authored-shape errors and warnings |
| `.eve/compile/compiled-agent-manifest.json` | Serialized surface the runtime loads |
| `.eve/compile/compile-metadata.json` | Build-time metadata and paths |
| `.eve/compile/module-map.mjs` | Compiled module entrypoints |
## `eve start`
Serves the previously built production output at `.output/server/index.mjs`.
```bash
eve start [--host <host>] [--port <port>]
```
<ParamField body="--host" type="string" default="0.0.0.0">
Host interface to bind.
</ParamField>
<ParamField body="--port" type="number" default="$PORT, then 3000">
Port to listen on. Accepts `0` through `65535`.
</ParamField>
Prints the listening URL and blocks until the server exits or receives `SIGINT`/`SIGTERM`. Exits with an error if `.output/` is missing — run `eve build` first.
## `eve dev`
Starts the local development server or connects the terminal UI to an existing URL.
```bash
eve dev [options]
eve dev https://your-app.vercel.app
```
Pass a bare URL as the only argument (rewritten internally to `--url`) to smoke-test a preview or production deployment without booting a local server.
### UI modes
| Mode | When |
| --- | --- |
| Terminal UI | Interactive TTY and no `--no-ui` |
| Headless | `--no-ui`, or stdin/stdout is not a TTY |
In headless mode the server keeps running and prints `server listening at <url>`. Non-TTY terminals without `--no-ui` receive a warning that the interactive UI is disabled.
### Flags
<ParamField body="--host" type="string" default="127.0.0.1">
Host interface for the local dev server. Cannot be combined with `--url`.
</ParamField>
<ParamField body="--port" type="number" default="$PORT, then 2000">
Port for the local dev server. When omitted, Eve retries up to 10 consecutive ports if the default is in use. Cannot be combined with `--url`.
</ParamField>
<ParamField body="-u, --url" type="string">
Connect the terminal UI to an existing server URL instead of starting one. Incompatible with `--host`, `--port`, and `--no-ui`.
</ParamField>
<ParamField body="--no-ui" type="flag">
Start the server without the interactive terminal UI.
</ParamField>
<ParamField body="--name" type="string" default="humanized app folder name">
Title shown in the terminal UI header. For remote URLs, defaults to the URL host.
</ParamField>
<ParamField body="--input" type="string">
Pre-fill the prompt input after launching the UI. Editable and not auto-submitted. Requires the interactive UI.
</ParamField>
<ParamField body="--tools" type="enum" default="auto-collapsed">
Tool-call rendering: `full`, `collapsed`, `auto-collapsed`, or `hidden`.
</ParamField>
<ParamField body="--reasoning" type="enum" default="full">
Reasoning rendering: `full`, `collapsed`, `auto-collapsed`, or `hidden`.
</ParamField>
<ParamField body="--subagents" type="enum" default="auto-collapsed">
Subagent-section rendering: `full`, `collapsed`, `auto-collapsed`, or `hidden`.
</ParamField>
<ParamField body="--connection-auth" type="enum" default="full">
Connection-authorization rendering: `full`, `collapsed`, `auto-collapsed`, or `hidden`.
</ParamField>
<ParamField body="--assistant-response-stats" type="enum" default="tokensPerSecond">
Assistant header statistic: `tokens` or `tokensPerSecond`.
</ParamField>
<ParamField body="--context-size" type="number">
Model context window size in tokens, shown as a usage percentage in the TUI.
</ParamField>
<ParamField body="--logs" type="enum" default="stderr">
Server and agent logs to surface inline: `all`, `stderr`, `sandbox`, or `none`.
</ParamField>
### Dev server lifecycle
Local dev writes the active server PID to `.eve/dev-process.pid`. Starting a second `eve dev` for the same agent while that process is running exits with the PID and a `kill` command to stop it.
Dev mode keeps immutable runtime snapshots under `.eve/dev-runtime/snapshots/` so in-flight sessions hold a consistent code revision while new prompts pick up rebuilds. On startup, `eve dev` prunes stale runtime snapshots and old local sandbox templates in the background.
## `eve link`
Links the current directory to a Vercel project, then pulls environment variables so an AI Gateway credential (`VERCEL_OIDC_TOKEN` or `AI_GATEWAY_API_KEY`) lands in `.env.local`.
```bash
eve link
```
Interactive only. The command walks team and project pickers; re-linking always runs the pickers and the new choice wins. In CI, use `vercel link --project <name> --yes` instead. Exits with code `1` when the directory is not an Eve project or the terminal is not interactive.
## `eve deploy`
Deploys the agent to Vercel production via `vercel deploy --prod`, installing dependencies first and pulling environment variables after.
```bash
eve deploy
```
| Scenario | Behavior |
| --- | --- |
| Already linked | Deploys immediately, with or without a TTY |
| Unlinked + interactive terminal | Walks the same team/project pickers as `eve link` |
| Unlinked + non-interactive | Exits with guidance to run `eve link` or `vercel link` first |
On success, prints the production URL when available.
## `eve eval`
Runs eval modules discovered under `evals/` (files matching `*.eval.ts`).
```bash
eve eval [evalId...] [--url <url>] [options]
```
With no eval IDs, runs all discovered evals. IDs match exactly or by directory prefix — `eve eval weather` runs everything under `evals/weather/`.
### Exit codes
| Code | Meaning |
| --- | --- |
| `0` | Every eval passed its gate assertions (and soft thresholds when `--strict` is set) |
| `1` | At least one eval failed a check, hit an execution error, or missed a threshold under `--strict` |
| `2` | Configuration error (no evals discovered, no evals matching filters, invalid flag values) |
Without `--url`, eval starts a local dev server on `127.0.0.1` with port `0` (OS-assigned) and tears it down when finished.
<ParamField body="--url" type="string">
Remote agent URL. Skips local host startup.
</ParamField>
<ParamField body="--tag" type="string[]">
Run only evals carrying any of the given tags.
</ParamField>
<ParamField body="--strict" type="flag">
Treat below-threshold scores as failures in addition to gate assertion failures.
</ParamField>
<ParamField body="--list" type="flag">
Print discovered evals without running them.
</ParamField>
<ParamField body="--timeout" type="number">
Per-eval timeout in milliseconds. Must be a non-negative integer.
</ParamField>
<ParamField body="--max-concurrency" type="number" default="8">
Maximum concurrent eval executions. Must be a positive integer. Config-level `maxConcurrency` in eval config is used when this flag is omitted.
</ParamField>
<ParamField body="--json" type="flag">
Output results as JSON (suppresses the console reporter).
</ParamField>
<ParamField body="--junit" type="string">
Write JUnit XML results to the given file path.
</ParamField>
<ParamField body="--skip-report" type="flag">
Skip eval-defined reporters such as Braintrust.
</ParamField>
<ParamField body="--verbose" type="flag">
Stream per-eval `t.log` lines to stdout.
</ParamField>
## `eve channels`
Manage user-authored channel modules under `agent/channels/`.
### `eve channels add`
```bash
eve channels add [kind] [-f] [-y]
```
Scaffolds a channel. With no `kind`, prompts interactively. Pass `slack` or `web` to scaffold directly.
<ParamField body="-f, --force" type="flag">
Overwrite existing channel files.
</ParamField>
<ParamField body="-y, --yes" type="flag">
Assume yes for confirmations. Requires an explicit `kind` and an interactive terminal for kind-less runs.
</ParamField>
### `eve channels list`
```bash
eve channels list [--json]
```
Lists user-authored channels in the current project.
<ParamField body="--json" type="flag">
Output as JSON.
</ParamField>
## Recommended workflow
<Steps>
<Step title="Author under agent/">
Edit instructions, tools, skills, channels, and `agent.ts` configuration.
</Step>
<Step title="Verify discovery">
Run `eve info` to confirm files were discovered and read diagnostics before starting the dev server.
</Step>
<Step title="Iterate locally">
Run `eve dev` and exercise the agent through the terminal UI or HTTP routes.
</Step>
<Step title="Build and smoke-test">
Run `eve build`, then `eve start` to verify the production artifact locally.
</Step>
<Step title="Link and deploy">
Run `eve link` to pull gateway credentials, then `eve deploy` for Vercel production.
</Step>
</Steps>
## Related pages
<CardGroup cols={2}>
<Card title="Installation" href="/installation">
Prerequisites, package managers, and first-install commands for Eve projects.
</Card>
<Card title="Eve quickstart" href="/eve-quickstart">
Run `eve init`, start `eve dev`, inspect with `eve info`, and send the first session message.
</Card>
<Card title="Eve project layout" href="/eve-project-layout">
What `eve info` discovers under `agent/` and how path-derived naming works.
</Card>
<Card title="Eve deployment" href="/eve-deployment">
Build `.eve/` artifacts, configure Vercel env, and verify production routes.
</Card>
<Card title="Eve HTTP protocol reference" href="/eve-http-protocol-reference">
Session routes, NDJSON events, and `continuationToken` follow-ups served by `eve dev` and `eve start`.
</Card>
<Card title="Runtime models" href="/runtime-models">
Compare Eve sessions and turns with Flue sessions and workflow runs.
</Card>
</CardGroup>
---
## 20. Eve HTTP protocol reference
> Session routes, NDJSON event types, `continuationToken` follow-ups, stream reconnect rules, and client integration entry points.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/20-eve-http-protocol-reference.md
- Generated: 2026-06-18T19:30:49.752Z
### Source Files
- `vercel-eve:docs/concepts/sessions-runs-and-streaming.md`
- `vercel-eve:docs/reference/typescript-api.md`
- `vercel-eve:docs/channels/overview.mdx`
- `vercel-eve:docs/guides/frontend/overview.mdx`
- `vercel-eve:packages/eve/src/public/index.ts`
- `vercel-eve:packages/eve/src/public/tools/index.ts`
---
title: "Eve HTTP protocol reference"
description: "Session routes, NDJSON event types, `continuationToken` follow-ups, stream reconnect rules, and client integration entry points."
---
Eve exposes a stable HTTP API under `/eve/v1` through the default Eve channel (`eveChannel()`). Session creation and follow-ups are JSON `POST` requests; progress is a durable NDJSON stream on `GET /eve/v1/session/:sessionId/stream`. The TypeScript client (`eve/client`), browser hooks (`useEveAgent`), and raw `curl` all target the same routes.
## Session handles
Two handles serve different jobs. Mixing them is the most common integration mistake.
| Handle | Role | Owned by |
| --- | --- | --- |
| `continuationToken` | Resume handle for sending the next user turn or HITL response | Channel |
| `sessionId` | Stream-and-inspect handle for attaching to the event feed | Runtime |
A session has one active continuation at a time. Each follow-up must use the current `continuationToken`; a stale token is rejected. For deterministic ordering, send one user turn at a time and wait for `session.waiting` before posting the next message to the same session.
<Note>
The client tracks a third cursor, `streamIndex` — the count of events already consumed. Persist the full `SessionState` object (`sessionId`, `continuationToken`, `streamIndex`) when resuming after a reload.
</Note>
## Route inventory
| Method | Path | Auth | Purpose |
| --- | --- | --- | --- |
| `GET` | `/eve/v1/health` | Public | Liveness probe |
| `GET` | `/eve/v1/info` | `localDev()` / `vercelOidc()` by default | Agent inspection snapshot |
| `POST` | `/eve/v1/session` | Channel `auth` policy | Create session and dispatch first turn |
| `POST` | `/eve/v1/session/:sessionId` | Channel `auth` policy | Continue session with follow-up or HITL response |
| `GET` | `/eve/v1/session/:sessionId/stream` | Channel `auth` policy | Stream durable NDJSON events |
Custom channels mount additional routes under their own paths. The Eve channel is enabled by default even without `agent/channels/eve.ts`.
:::endpoint POST /eve/v1/session
Create a durable session and dispatch the first user turn. Returns immediately; model work streams on the session stream route.
:::
<ParamField body="message" type="string | UserContent[]" required>
Plain text or an array of `text` and `file` parts. File parts require `type`, `mediaType`, and `data` (base64, data URL, or URL). Internal ref schemes (`eve-url:`, `eve-sandbox:`, `eve-attachment:`) are rejected.
</ParamField>
<ParamField body="clientContext" type="string | string[] | object">
Ephemeral page context for this turn only. Strings become user-role context messages prefixed with `Client context:\n`. Objects are JSON-serialized. Never persisted to durable history.
</ParamField>
<ParamField body="outputSchema" type="object">
JSON Schema object requesting a structured turn result. Final payload arrives on `result.completed`.
</ParamField>
<ParamField body="mode" type="'conversation' | 'task'">
Optional run mode for the initial session.
</ParamField>
<ParamField body="callback" type="object">
Optional terminal session callback configuration.
</ParamField>
<ResponseField name="continuationToken" type="string">
Resume handle for follow-up turns on this session.
</ResponseField>
<ResponseField name="sessionId" type="string">
Durable session identifier for stream attachment.
</ResponseField>
<ResponseField name="ok" type="boolean">
Always `true` on success.
</ResponseField>
The response uses status **202 Accepted**. The `x-eve-session-id` header mirrors `sessionId`.
<RequestExample>
```bash title="Start a session"
curl -X POST http://127.0.0.1:3000/eve/v1/session \
-H 'content-type: application/json' \
-d '{"message":"Summarize the latest forecast."}'
```
</RequestExample>
<ResponseExample>
```json title="202 response"
{
"continuationToken": "eve:7f3c4a2b-...",
"ok": true,
"sessionId": "ses_01h..."
}
```
</ResponseExample>
:::
:::endpoint POST /eve/v1/session/:sessionId
Continue an existing session. Requires the current `continuationToken` and at least one of `message` or `inputResponses`.
:::
<ParamField path="sessionId" type="string" required>
Durable session ID from the create response or `x-eve-session-id` header.
</ParamField>
<ParamField body="continuationToken" type="string" required>
Current resume handle. Missing or empty values return 400.
</ParamField>
<ParamField body="message" type="string | UserContent[]">
Follow-up user message. Same shape as the create route.
</ParamField>
<ParamField body="inputResponses" type="InputResponse[]">
HITL responses resolving pending `input.requested` events. Each entry requires `requestId`; include `optionId` for option picks or `text` for freeform answers.
</ParamField>
<ParamField body="clientContext" type="string | string[] | object">
Same ephemeral context semantics as the create route.
</ParamField>
<ParamField body="outputSchema" type="object">
Per-turn structured output schema.
</ParamField>
<ResponseField name="sessionId" type="string">
Confirmed session ID.
</ResponseField>
<ResponseField name="ok" type="boolean">
Always `true` on success.
</ResponseField>
Status **200 OK** on success. The response does not rotate `continuationToken`; retain the token from create (or from client state) for subsequent follow-ups while the session is waiting.
<RequestExample>
```bash title="Send a follow-up"
curl -X POST http://127.0.0.1:3000/eve/v1/session/ses_01h... \
-H 'content-type: application/json' \
-d '{"continuationToken":"eve:7f3c4a2b-...","message":"Now send the short version."}'
```
</RequestExample>
:::
:::endpoint GET /eve/v1/session/:sessionId/stream
Attach to the durable NDJSON event stream for one session. Every event is recorded before a step completes, so the full stream is replayable.
:::
<ParamField path="sessionId" type="string" required>
Session to stream.
</ParamField>
<ParamField query="startIndex" type="integer">
Non-negative event offset. Omit to stream from the beginning; pass the number of events already consumed to reconnect without replaying them.
</ParamField>
<ResponseField name="Content-Type" type="string">
`application/x-ndjson; charset=utf-8`
</ResponseField>
<ResponseField name="x-eve-session-id" type="string">
Session ID echoed on every stream response.
</ResponseField>
<ResponseField name="x-eve-stream-format" type="string">
`ndjson`
</ResponseField>
<ResponseField name="x-eve-stream-version" type="string">
Current stream schema version (`15`).
</ResponseField>
Each line is one JSON event object. The stream sets `cache-control: no-store, no-transform` and `x-accel-buffering: no` to discourage proxy buffering.
<RequestExample>
```bash title="Stream from the start"
curl -N http://127.0.0.1:3000/eve/v1/session/ses_01h.../stream
```
</RequestExample>
<RequestExample>
```bash title="Reconnect after 42 events"
curl -N "http://127.0.0.1:3000/eve/v1/session/ses_01h.../stream?startIndex=42"
```
</RequestExample>
:::
## NDJSON event types
Events share a `type` discriminator. Most carry a `data` object with `sequence`, `turnId`, and step-scoped fields. Optional `meta.at` timestamps mark durable write time.
### Session and turn boundaries
| Event | Meaning |
| --- | --- |
| `session.started` | Durable session created; may include `runtime` identity and subagent `invocation` metadata |
| `turn.started` | New turn began (`turnId`, `sequence`) |
| `message.received` | Inbound user message accepted |
| `turn.completed` | Turn finished successfully |
| `turn.failed` | Turn failed; carries `{ code, message, details? }` |
| `session.waiting` | Session parked for next input (`data.wait: "next-user-message"`) |
| `session.completed` | Terminal session success |
| `session.failed` | Terminal session failure; carries `{ code, message, details? }` |
Turn boundary events for client consumption: `session.waiting`, `session.completed`, and `session.failed`. The TypeScript client stops a turn stream at the first boundary event.
### Model steps and assistant output
| Event | Meaning |
| --- | --- |
| `step.started` | Model step began |
| `step.completed` | Model step finished; carries `finishReason` and optional `usage` |
| `step.failed` | Model step failed; carries `{ code, message, details? }` |
| `reasoning.appended` | Reasoning delta (`reasoningDelta`, `reasoningSoFar`) |
| `reasoning.completed` | Finalized reasoning block |
| `message.appended` | Assistant text delta (`messageDelta`, `messageSoFar`) |
| `message.completed` | Finalized assistant text; `data.finishReason` distinguishes tool-call narration from terminal replies |
| `result.completed` | Structured output when `outputSchema` was requested (`data.result`) |
`message.completed` can fire more than once per turn when the model replies before requesting tools. Treat `finishReason: "tool-calls"` as non-terminal assistant text.
### Tools, HITL, and delegation
| Event | Meaning |
| --- | --- |
| `actions.requested` | Model requested tool calls (`data.actions`) |
| `action.result` | Tool result projected back (`status`, optional `error`) |
| `input.requested` | Run paused for approval or `ask_question`; carries `data.requests` |
| `subagent.called` | Subagent delegated; carries `childSessionId` for attaching to the child stream |
| `subagent.started` | Inline subagent execution began |
| `subagent.event` | Child stream event wrapped for inline subagents |
| `subagent.completed` | Delegated subagent finished |
### Compaction and connections
| Event | Meaning |
| --- | --- |
| `compaction.requested` | Context-window compaction began |
| `compaction.completed` | Compaction checkpoint written |
| `authorization.required` | Connection needs OAuth; `data.authorization` may include `url`, `userCode`, `expiresAt`, `instructions` |
| `authorization.completed` | Authorization resolved; `data.outcome` is `"authorized"`, `"declined"`, `"failed"`, or `"timed-out"` |
```mermaid
sequenceDiagram
participant Client
participant API as POST /eve/v1/session*
participant Stream as GET .../stream
participant Runtime as Durable workflow
Client->>API: POST message (+ continuationToken on follow-up)
API-->>Client: 202/200 JSON (sessionId, continuationToken on create)
Client->>Stream: GET stream (?startIndex=N)
Stream-->>Client: NDJSON turn.started
Stream-->>Client: NDJSON message.appended / actions.requested / ...
Stream-->>Client: NDJSON session.waiting
Client->>API: POST follow-up with continuationToken
```
## Follow-up and HITL protocol
After `session.waiting`, post the next turn to `POST /eve/v1/session/:sessionId` with the stored `continuationToken`.
For human-in-the-loop pauses, the stream emits `input.requested` with `data.requests`. Each `InputRequest` includes `requestId`, `prompt`, optional `options`, and `display` (`"confirmation"`, `"select"`, or `"text"`). Respond with `inputResponses`:
```json title="Approve a tool call"
{
"continuationToken": "eve:7f3c4a2b-...",
"inputResponses": [
{ "requestId": "approval_1", "optionId": "approve" }
]
}
```
HITL-only deliveries retry up to 10 times when the server returns 500 with `target session was not found` (transient propagation window). Message-only deliveries do not retry automatically.
<Warning>
`clientContext` alone cannot dispatch a turn. It must accompany a `message` or `inputResponses` payload.
</Warning>
## Stream reconnect rules
Streams are durable and replayable. Clients reconnect after transient socket disconnects by resuming from the event count already consumed.
| Mechanism | Behavior |
| --- | --- |
| `?startIndex=N` | Server skips the first N recorded events |
| `streamIndex` in `SessionState` | Client-side cursor advanced after each consumed event |
| `maxReconnectAttempts` | Per-turn reconnect budget (default **3** on `Client` and `useEveAgent`) |
| Stream open retries | Up to **12** attempts at **250 ms** for statuses 404, 409, 425, 500, 502, 503, 504 |
| AbortSignal | Caller abort stops reconnect; treat as intentional stop |
On disconnect mid-turn, the client reopens the stream at `startIndex = eventsAlreadyConsumed` and continues until a turn boundary event. `session.stream()` exposes the same reconnect semantics for attaching to an existing session without sending a new message.
<Check>
Wait for `session.waiting` before sending the next user message. Eve does not guarantee FIFO ordering for concurrent deliveries to the same active session.
</Check>
## Error envelopes
Failed requests return JSON with `ok: false` and an `error` string. Common status codes:
| Status | Cause |
| --- | --- |
| 400 | Invalid JSON, missing required fields, bad message parts, invalid `startIndex` |
| 401 | Route auth walk exhausted (no `SessionAuthContext`) |
| 404 | Unknown `sessionId` on continue or stream |
| 413 | Attachment exceeds upload policy size limit |
| 415 | Attachment media type not allowed |
| 500 | `onMessage` handler threw (may include `errorId`) |
| 204 | `onMessage` returned `null` — message accepted but not dispatched |
Upload policy violations include a `violations` array with per-file details.
## Inspection and health routes
:::endpoint GET /eve/v1/health
Public liveness probe. Skips the channel auth walk entirely.
:::
<ResponseField name="ok" type="boolean">
`true` when the workflow is ready.
</ResponseField>
<ResponseField name="status" type="string">
`"ready"`
</ResponseField>
<ResponseField name="workflowId" type="string">
Active workflow identifier.
</ResponseField>
:::
:::endpoint GET /eve/v1/info
JSON inspection snapshot: model, instructions, tools, skills, channels, schedules, subagents, sandbox, connections, hooks, workflow, and workspace metadata. Uses the same default auth chain as the Eve channel (`localDev()`, `vercelOidc()`).
:::
## Client integration entry points
<CardGroup>
<Card title="TypeScript SDK" href="/eve-quickstart">
`eve/client` wraps POST + NDJSON with `Client`, `ClientSession`, and `MessageResponse`. Import stream helpers such as `isCurrentTurnBoundaryEvent` from the same entrypoint.
</Card>
<Card title="Browser hooks" href="/eve-sessions-streaming">
`useEveAgent` from `eve/react`, `eve/vue`, or `eve/svelte` projects events into `{ messages }`, handles reconnects, and persists `session` cursors.
</Card>
<Card title="Framework proxies" href="/eve-quickstart">
`withEve` (Next.js), `eve/nuxt`, and `eveSvelteKit` mount same-origin `/eve/v1/*` routes so browsers avoid cross-origin calls.
</Card>
</CardGroup>
### TypeScript client flow
```ts title="Script or server integration"
import { Client } from "eve/client";
const client = new Client({ host: "http://127.0.0.1:3000" });
const session = client.session();
const response = await session.send("Summarize the latest forecast.");
const result = await response.result();
console.log(result.status, result.message);
// Persist for resume
await save(session.state);
```
`MessageResponse` is single-use: consume it with `for await` or `result()`, not both. After abort, start a new `send()` for the next turn.
### Raw HTTP flow
<Steps>
<Step title="Create session">
`POST /eve/v1/session` with `{ "message": "..." }`. Store `sessionId`, `continuationToken`, and begin streaming.
</Step>
<Step title="Attach stream">
`GET /eve/v1/session/:sessionId/stream` with `curl -N` or a fetch reader. Parse one JSON object per line.
</Step>
<Step title="Wait for park">
Continue reading until `session.waiting`, `session.completed`, or `session.failed`.
</Step>
<Step title="Send follow-up">
If `session.waiting`, `POST /eve/v1/session/:sessionId` with `continuationToken` and the next `message` or `inputResponses`. Reattach to the stream (use `startIndex` to skip events already handled).
</Step>
</Steps>
## Authentication
Route auth is configured on `agent/channels/eve.ts` via `eveChannel({ auth: [...] })`. The ordered walk accepts the first `AuthFn` that returns a `SessionAuthContext`; skipped entries (`null`/`undefined`) fall through; exhaustion yields 401.
Default scaffold uses `[localDev(), vercelOidc()]`. Production browser traffic requires a custom verifier (Clerk, Auth.js, JWT). Pass bearer or basic credentials through `Client` `auth` and `headers`; function values re-resolve before every request including stream reconnects.
## Related pages
<CardGroup>
<Card title="Eve sessions and streaming" href="/eve-sessions-streaming">
Conceptual walkthrough of session cursors, HITL, subagent streams, and compaction events.
</Card>
<Card title="Runtime models" href="/runtime-models">
Compare Eve `continuationToken` and `sessionId` contracts with Flue sessions and workflow runs.
</Card>
<Card title="Eve quickstart" href="/eve-quickstart">
First `eve dev` session against the HTTP API.
</Card>
<Card title="Eve CLI reference" href="/eve-cli-reference">
`eve dev`, `eve info`, and local route discovery.
</Card>
</CardGroup>
---
## 21. Flue examples
> Copy-pasteable fixtures for hello-world workflows, channel ingress, observability adapters, and target-specific integrations.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/21-flue-examples.md
- Generated: 2026-06-18T19:30:43.507Z
### Source Files
- `withastro-flue:examples/hello-world/src/workflows/hello.ts`
- `withastro-flue:examples/slack-channel/src/channels/slack.ts`
- `withastro-flue:examples/braintrust/README.md`
- `withastro-flue:examples/cloudflare/src/workflows/skills-from-r2.ts`
- `withastro-flue:examples/react-chat/flue.config.ts`
- `withastro-flue:examples/notion-channel/README.md`
- `withastro-flue:examples/imported-skill/README.md`
---
title: "Flue examples"
description: "Copy-pasteable fixtures for hello-world workflows, channel ingress, observability adapters, and target-specific integrations."
---
The `withastro-flue/examples/` directory ships 25 self-contained fixtures. Each example is a private workspace package with its own `package.json`, `flue.config.ts` (where needed), and `src/` tree. Fixtures demonstrate one capability end-to-end so you can copy the pattern into a production app without importing example code as a library.
## Example catalog
| Directory | Category | Primary surface |
| --- | --- | --- |
| `hello-world` | Workflow baseline | Prompts, skills, tools, sandboxes, providers |
| `imported-skill` | Skills and sandboxes | `import ... with { type: 'skill' }`, custom `just-bash` |
| `cloudflare` | Cloudflare target | Workers AI binding, R2/git hydration, cf-shell |
| `react-chat` | UI integration | `@flue/react` hooks + Vite static client |
| `braintrust` | Observability | `observe(...)` → Braintrust traces |
| `sentry` | Observability | `observe(...)` → Sentry error captures |
| `chat-sdk` | Channel + SDK | Chat SDK GitHub adapter + `dispatch` |
| `slack-channel`, `notion-channel`, `github-channel`, … | Channel ingress | Verified webhooks at `/channels/:name/*` |
| `assistant` | Deploy fixture | Dockerfile + `wrangler.jsonc` for agent hosting |
Channel examples follow the same project-owned shape: a `src/channels/<provider>.ts` module exports `channel` (ingress) and project-owned SDK clients/tools; `src/agents/assistant.ts` binds outbound tools to the conversation key parsed from the dispatch `id`. Provider packages (`@flue/slack`, `@flue/notion`, `@flue/github`, …) supply signature verification and route mounting — not application policy tools.
<Note>
Build `@flue/runtime` and `@flue/cli` before running examples from a fresh checkout. Most examples declare both as workspace dependencies and consume the local `dist/` artifacts produced by `pnpm run build`.
</Note>
## Hello-world workflows
`examples/hello-world/` is the general runtime integration fixture referenced in the repository `AGENTS.md`. It exports many workflow modules under `src/workflows/` and composes a custom Hono `app.ts` that registers local OpenAI-compatible providers and mounts `flue()` at `/`.
### Minimal prompt workflow
`hello.ts` is the smallest runnable workflow: create an agent, initialize a harness, open a session, prompt with a Valibot `result` schema, and read a workspace file via `session.shell`.
```ts
const agent = createAgent(() => ({ model: 'anthropic/claude-sonnet-4-6' }));
export async function run({ init, log }: FlueContext) {
const harness = await init(agent);
const session = await harness.session();
const response = await session.prompt('What is 2 + 2? Return only the number.', {
result: v.object({ answer: v.number() }),
});
log.info('solved arithmetic prompt', { answer: response.data.answer });
return response.data;
}
```
<Steps>
<Step title="Run the hello workflow">
From `examples/hello-world/`:
```bash
pnpm exec flue run hello --target node --env ../../.env
```
</Step>
<Step title="Verify structured output">
The workflow returns `{ answer: 4 }` and logs token usage. Console output includes `[hello] 2 + 2 =` and a `cat AGENTS.md` read via shell.
</Step>
</Steps>
### Workflow modules in the same fixture
| Workflow | Demonstrates |
| --- | --- |
| `with-skill` | `session.skill('greet', { args, result })` against `.agents/skills/greet/SKILL.md` |
| `with-tools` | `defineTool` passed to `session.prompt`, plus built-in `task` delegation |
| `with-sandbox` | Daytona and other sandbox adapters |
| `with-subagent` | Delegated agent sessions |
| `with-registered-provider` | `registerProvider` for Ollama/LM Studio gateways |
| `with-request` | HTTP request context inside `run()` |
| `compaction-test` | Context compaction boundaries |
| `fs-test`, `fs-surface-test` | Filesystem surfaces |
The `greet` skill lives at `.agents/skills/greet/SKILL.md` with frontmatter `name: greet`. Flue discovers it during `init()` from the standard skills lookup path.
### Custom `app.ts` composition
`hello-world/src/app.ts` shows how runtime concerns stay out of `flue.config.ts`: provider registration (`registerProvider('ollama', …)`), plain Hono middleware, a custom `/api/ping` route, and `app.route('/', flue())` for the built-in agent API.
## Channel ingress fixtures
Channel examples receive verified provider webhooks, derive a canonical conversation instance id, call `dispatch(agent, { id, input })`, and define narrow outbound tools that close over trusted context (thread id, page id, issue number). Agents are intentionally dispatch-only; direct `/agents/:name/:id` routes would need separate authorization.
### Slack (`slack-channel`)
Ingress route: `POST /channels/slack/events`.
<ParamField body="SLACK_SIGNING_SECRET" type="string" required>
Verifies Events API signatures. Requests older than five minutes are rejected.
</ParamField>
<ParamField body="SLACK_BOT_TOKEN" type="string" required>
Powers the project-owned `WebClient` used by `reply_in_slack_thread`.
</ParamField>
```ts
export const channel = createSlackChannel({
signingSecret: requiredEnv('SLACK_SIGNING_SECRET'),
async events({ payload }) {
if (payload.type !== 'event_callback') return;
if (payload.event.type === 'app_mention') {
const thread = { teamId: payload.team_id, channelId: event.channel, threadTs: event.thread_ts ?? event.ts };
await dispatch(assistant, {
id: channel.conversationKey(thread),
input: { type: 'slack.app_mention', eventId: payload.event_id, text: event.text },
});
}
},
});
```
The agent binds `replyInThread(channel.parseConversationKey(id))` as a tool. Optional `/channels/slack/interactions` and `/channels/slack/commands` surfaces are shown commented in the channel module.
<Warning>
Handlers must complete dispatch admission before acknowledging Slack. Slack retries slow or failed Events API deliveries — claim `payload.event_id` in durable storage when uniqueness matters.
</Warning>
### Notion (`notion-channel`)
Ingress route: `POST /channels/notion/webhook`.
<ParamField body="NOTION_TOKEN" type="string" required>
Powers the official `@notionhq/client` v5.22.0 client with injected Fetch transport.
</ParamField>
<ParamField body="NOTION_WEBHOOK_VERIFICATION_TOKEN" type="string">
Set after initial endpoint verification. While unset, uncomment the temporary `verification(...)` callback in the channel module.
</ParamField>
Page lifecycle events (`page.created`, `page.content_updated`, …) dispatch to instance ids with the local `notion-page:` prefix. The `retrieve_notion_page` tool closes over the page id in application code — the model cannot choose another page.
`page.deleted` is intentionally ignored because the retrieval tool cannot read deleted pages.
### GitHub (`github-channel`)
Ingress route: `POST /channels/github/webhook`. Requires `GITHUB_WEBHOOK_SECRET` and `GITHUB_TOKEN`. Webhook content type must be `application/json`. The handler returns `2xx` within ten seconds; GitHub does not auto-retry failed deliveries.
### Other channel fixtures
The same dispatch + project-owned tool pattern appears in `discord-channel`, `telegram-channel`, `teams-channel`, `google-chat-channel`, `linear-channel`, `messenger-channel`, `stripe-channel`, `shopify-channel`, `twilio-channel`, `whatsapp-channel`, `zendesk-channel`, `intercom-channel`, `resend-channel`, and `salesforce-marketing-cloud-channel`. Each README documents required env vars, route paths, and workerd test commands (`pnpm run check:types`, `pnpm run test`, `pnpm run build`, `pnpm run build:cloudflare`).
## Observability adapters
Both observability examples register a global `observe(...)` subscriber in `src/app.ts`. Workflows do not import Braintrust or Sentry.
### Braintrust tracing (`braintrust`)
When `BRAINTRUST_API_KEY` is set, `initLogger` runs and `braintrustFlueObserver` receives compatible `FlueEvent` shapes:
| Flue event | Bridge behavior |
| --- | --- |
| `run_start`, `run_end` | Tracked in `observedRuns` set |
| `tool` | Renamed to `tool_call` for Braintrust 3.17 |
| `run_resume` | Synthesized as `run_start` when the isolate missed the original start |
| `operation_*`, `turn_*`, `task_*`, `compaction_*` | Forwarded as-is |
Trace shape for a tool-using workflow:
```text
workflow:tools
flue.prompt
llm:<model>
tool:lookup_weather
llm:<model>
```
<Info>
When `BRAINTRUST_API_KEY` is absent, the app runs unchanged with no trace export. Braintrust's observer is content-bearing — review masking and retention before enabling on sensitive workloads.
</Info>
Trigger example workflows after `pnpm exec flue dev`:
```bash
curl -X POST 'http://localhost:3583/workflows/prompt?wait=result' \
-H 'content-type: application/json' -d '{"name":"Developer"}'
```
### Sentry error reporting (`sentry`)
`Sentry.init` runs at module scope with `enabled: Boolean(process.env.SENTRY_DSN)`. The bridge captures:
| Flue event | Sentry call |
| --- | --- |
| `run_end` with `isError: true` | `captureException` with `flue.run.id` tag |
| `log` with `level: 'error'` and `attributes.error` | `captureException` |
| `log` with `level: 'error'` without `error` attribute | `captureMessage` |
Pivot on `flue.run.id` in Sentry, then replay the run:
```bash
flue logs run_01HX...
```
<Note>
On Cloudflare, each Durable Object is its own V8 isolate. `app.ts` (and thus `observe`) is evaluated per isolate — each agent reports errors independently. This is the intended shape, not a workaround.
</Note>
Deliberate scope cuts: no spans/traces, no `log.info`/`log.warn` forwarding, no tool-error capture, no AI metrics forwarding.
## Target-specific integrations
### Cloudflare (`cloudflare`)
`flue dev --target cloudflare` serves three workflow/agent surfaces:
| Module | Demonstrates |
| --- | --- |
| `with-cloudflare-binding` | Workers AI binding routing (no API keys) |
| `skills-from-r2` | `hydrateFromBucket` → discovered skill from R2 |
| `skills-from-git` | `createGit` clone into cf-shell `Workspace` |
R2 hydration copies bucket keys verbatim (default `prefix: ''`). Objects under `.agents/skills/<name>/SKILL.md` become registered skills. A `/.hydrated` sentinel short-circuits repeat hydration; bump the sentinel key to force re-hydration after bucket changes.
<Warning>
`skills-from-r2` and `skills-from-git` require a `worker_loaders` binding (beta). Local `wrangler dev` may not expose R2 to the running server — use `wrangler dev --remote` or deploy to a preview environment. Seed R2 with `./seed-r2.sh` before running `skills-from-r2`.
</Warning>
```bash
curl -X POST http://localhost:3583/workflows/skills-from-r2?wait=result \
-H 'Content-Type: application/json' -d '{}'
```
The cf-shell sandbox adapter lives at `src/sandboxes/cloudflare-shell.ts` (conceptually generated by `flue add sandbox @cloudflare/shell`).
### React chat UI (`react-chat`)
`react-chat` composes a Vite-built React client with Flue's HTTP API:
```ts
registerProvider('react-chat-example', { api: 'react-chat-example', baseUrl: '' });
const app = new Hono();
app.route('/api', flue());
app.use('*', serveStatic({ root: './dist/client' }));
```
`flue.config.ts` pins `target: 'node'` and `output: 'dist/server'`. The UI uses `@flue/react` hooks (`useFlueAgent`, `useFlueWorkflow`, `useFlueClient`) against `createFlueClient({ baseUrl: '/api' })`.
<Steps>
<Step title="Build UI and start dev server">
```bash
pnpm run dev # runs build:ui then flue dev
```
</Step>
<Step title="Exercise agent chat and workflow streaming">
Open the served client. Agent messages stream via `useFlueAgent`; the demo workflow logs appear via `useFlueWorkflow({ runId })`.
</Step>
</Steps>
### Chat SDK + GitHub (`chat-sdk`)
`chat-sdk` wires Chat SDK's GitHub adapter for bidirectional issue-comment messaging while Flue owns agent execution. The fixture uses in-memory Chat SDK state and a scripted model provider for deterministic local e2e tests.
```txt
signed GitHub issue_comment webhook
-> Chat SDK GitHub adapter
-> dispatch(assistant, ...)
-> Flue agent tool
-> bot.thread(threadId).post(...)
```
## Skills and custom sandboxes (`imported-skill`)
Two workflow contracts ship in one fixture:
**`with-imported-skill`** — imports `src/skills/review/SKILL.md` with `{ type: 'skill' }`. The import is a lightweight `SkillReference`; the complete skill directory (including `CHECKLIST.txt`) is packaged. Register the reference in `skills: [review]` so files are readable during `session.skill(review)`.
```ts
import review from '../skills/review/SKILL.md' with { type: 'skill' };
const agent = createAgent(() => ({ model: 'anthropic/claude-haiku-4-5', skills: [review] }));
```
**`with-custom-bash`** — imports `Bash` and `InMemoryFs` directly from `just-bash` (declared in app dependencies) and passes `bash(() => new Bash({ fs }))` as the sandbox.
<CodeGroup>
```bash title="Imported skill (Node)"
pnpm exec flue run with-imported-skill --target node --env ../../.env
```
```bash title="Custom bash (Node or Cloudflare)"
pnpm exec flue run with-custom-bash --target node
pnpm exec flue dev --target cloudflare
```
</CodeGroup>
## Shared layout pattern
Most examples follow the same source-root layout:
:::files
examples/<name>/
├── flue.config.ts # target, root, output (build-time only)
├── package.json
├── tsconfig.json
├── AGENTS.md # optional system prompt for init()
└── src/
├── app.ts # Hono composition, providers, observe hooks
├── agents/
│ └── assistant.ts # createAgent default export
├── workflows/ # run() + route exports
├── channels/ # channel ingress (channel examples)
└── sandboxes/ # project-owned adapters (cloudflare, hello-world)
:::
<ParamField body="target" type="'node' | 'cloudflare'">
Selects runtime adapter. Omit in `flue.config.ts` when relying on `--target` CLI overrides (as `hello-world` does).
</ParamField>
<ParamField body="root" type="string">
Source root for agent/workflow discovery. Defaults to `src/` or `.flue/` per project layout rules.
</ParamField>
Provider and model registration intentionally live in `app.ts` via `registerProvider(...)`, not in `flue.config.ts`, because API keys often come from `process.env` or Cloudflare bindings at runtime.
## Verification commands
| Example type | Typical checks |
| --- | --- |
| Workflow baseline | `pnpm exec flue run <workflow> --target node` |
| Channel ingress | `pnpm run check:types && pnpm run test && pnpm run build` |
| Cloudflare target | `pnpm exec flue dev --target cloudflare` or `wrangler dev --remote` |
| Observability | `curl -X POST http://localhost:3583/workflows/<name>?wait=result` then inspect vendor UI |
| React UI | `pnpm run dev` then exercise browser client |
HTTP triggers use port `3583` by default (`flue dev`). Responses include a top-level `runId` for workflow invocations.
## Related pages
<CardGroup>
<Card title="Flue quickstart" href="/flue-quickstart">
Scaffold a project, run `flue dev`, invoke workflows with `flue run`, and connect to an agent instance.
</Card>
<Card title="Flue workflows" href="/flue-workflows">
Define workflow modules, initialize agents inside `run()`, and inspect run events with `flue logs`.
</Card>
<Card title="Flue channels" href="/flue-channels">
Discover channel modules, mount verified webhook routes, and dispatch inbound events to agents.
</Card>
<Card title="Flue deploy" href="/flue-deploy">
Choose Node or Cloudflare targets, build artifacts with `flue build`, and apply blueprint-driven integrations.
</Card>
<Card title="Build Flue agents" href="/build-flue-agents">
Author agents with `createAgent`, tools, skills, sandboxes, and providers.
</Card>
<Card title="Flue project layout" href="/flue-project-layout">
Source-root resolution, agent and workflow modules, and `app.ts` composition boundaries.
</Card>
</CardGroup>
---
## 22. Eve weather fixture
> Walk through the `weather-agent` fixture: instructions, tool schema, skill procedure, and local dev smoke paths.
- Page Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/pages/22-eve-weather-fixture.md
- Generated: 2026-06-18T19:31:09.198Z
### Source Files
- `vercel-eve:apps/fixtures/weather-agent/README.md`
- `vercel-eve:apps/fixtures/weather-agent/agent/agent.ts`
- `vercel-eve:apps/fixtures/weather-agent/agent/instructions.md`
- `vercel-eve:apps/fixtures/weather-agent/agent/tools/get_weather.ts`
- `vercel-eve:apps/fixtures/weather-agent/agent/skills/get-weather.md`
- `vercel-eve:apps/fixtures/weather-agent/package.json`
- `vercel-eve:README.md`
---
title: "Eve weather fixture"
description: "Walk through the `weather-agent` fixture: instructions, tool schema, skill procedure, and local dev smoke paths."
---
The `weather-agent` fixture at `apps/fixtures/weather-agent/` is a minimal, filesystem-authored Eve app used across the Eve monorepo for local development, manual smoke testing, scenario tests, and Nitro bundle analysis. It exposes one typed tool (`get_weather`), one on-demand skill (`get-weather`), always-on instructions, and a model config in `agent/agent.ts`. The agent id resolves to `weather-agent` from `package.json`.
## Fixture role in the monorepo
| Consumer | How it uses `weather-agent` |
| -------- | --------------------------- |
| Root `pnpm dev` | `scripts/dev.mjs` runs `eve` in watch mode and starts `weather-agent` with `eve dev --no-ui --port 0` |
| Manual smokes | Developers boot the fixture directly and exercise the TUI or HTTP session API |
| Scenario tests | `WEATHER_AGENT_DESCRIPTOR` in `packages/eve/src/internal/testing/scenario-apps/weather-agent.ts` mirrors the fixture for dev-server, CLI, and HMR tests |
| Bundle analysis | `nitro-bundle-report` scenario tests target `apps/fixtures/weather-agent` |
Focused end-to-end eval suites live under `e2e/fixtures/`; the weather fixture itself is the canonical small app for repo-local dev rather than a dedicated eval package.
## Authored layout
:::files
weather-agent/
├── package.json # scripts: dev, build, start, info, typecheck
├── tsconfig.json # includes agent/**/* and .eve/**/*.d.ts
├── turbo.json # build depends on eve#build
└── agent/
├── agent.ts # defineAgent model + provider options
├── instructions.md # always-on system prompt
├── tools/
│ └── get_weather.ts
└── skills/
└── get-weather.md
:::
Path-derived naming applies throughout: `agent/tools/get_weather.ts` → tool `get_weather`; `agent/skills/get-weather.md` → skill `get-weather`. No `name` or `id` fields appear on `define*` exports.
```mermaid
flowchart LR
subgraph client["Client surfaces"]
TUI["eve dev TUI"]
HTTP["POST /eve/v1/session"]
end
subgraph runtime["Eve runtime"]
Harness["Harness tool loop"]
SkillRouter["load_skill + skill catalog"]
end
subgraph authored["Authored surface"]
Instr["instructions.md"]
Skill["skills/get-weather.md"]
Tool["tools/get_weather.ts"]
end
TUI --> Harness
HTTP --> Harness
Harness --> Instr
Harness --> SkillRouter
SkillRouter --> Skill
Harness --> Tool
```
## Runtime config (`agent/agent.ts`)
`defineAgent` sets the model and OpenAI provider options for adaptive thinking:
```ts
import { defineAgent } from "eve";
export default defineAgent({
model: "openai/gpt-5.5",
modelOptions: {
providerOptions: {
openai: {
reasoningEffort: "high",
reasoningSummary: "auto",
},
},
},
});
```
`model` is required when `agent.ts` is present. Provider routing stays BYOC/BYOK: configure the model provider through `/model` in the dev TUI, `.env.local`, or a direct provider SDK override—nothing in the fixture hard-codes a hosted connector beyond the gateway model id string.
## Instructions (`agent/instructions.md`)
The always-on prompt is a single line:
```md
You are a weather-focused assistant. Be concise, accurate, and explicit about when you are using the local weather tool.
```
Eve composes this into every turn. Procedural guidance for weather queries lives in the skill, not here.
## Tool: `get_weather`
`defineTool` from `eve/tools` defines a typed, auto-approved lookup with a Zod input schema and a deterministic stub response.
| Field | Value |
| ----- | ----- |
| File | `agent/tools/get_weather.ts` |
| Model-facing name | `get_weather` |
| Approval | `needsApproval: never()` |
| Input | `{ city: string }` |
| Output | `{ city, temperatureF, condition, summary }` |
<ParamField body="city" type="string" required>
City name to look up. Passed through to the response unchanged.
</ParamField>
<ResponseField name="city" type="string">
Echo of the input city.
</ResponseField>
<ResponseField name="temperatureF" type="number">
Fixed stub value `72`.
</ResponseField>
<ResponseField name="condition" type="string">
Fixed stub value `"Sunny"`.
</ResponseField>
<ResponseField name="summary" type="string">
Human-readable summary, e.g. `Sunny in Brooklyn with a light breeze.`
</ResponseField>
```ts
import { defineTool } from "eve/tools";
import { never } from "eve/tools/approval";
import { z } from "zod";
export default defineTool({
needsApproval: never(),
description: "Get the current weather for a city.",
inputSchema: z.object({
city: z.string(),
}),
async execute(input) {
const city = input.city;
await sleep(300); // simulated latency
return {
city,
temperatureF: 72,
condition: "Sunny",
summary: `Sunny in ${city} with a light breeze.`,
};
},
});
```
The tool does not call an external weather API. The 300 ms delay exercises async tool execution and streaming turn timing. Tools run in the app runtime (not the sandbox), so this pattern is suitable for wiring real HTTP clients later by replacing `execute`.
## Skill: `get-weather`
The flat markdown skill at `agent/skills/get-weather.md` carries YAML frontmatter for routing and a short procedure body:
```md
---
description: Use the weather tool before answering forecast or temperature questions.
---
When the user asks about weather, temperature, or forecast conditions, call the `get_weather` tool before answering.
```
Eve advertises the `description` to the model alongside the framework-owned `load_skill` tool. When a turn matches that description, the model loads the skill body into context, then calls `get_weather` before composing a reply. Loading a skill adds instructions only; `get_weather` stays in the active tool set regardless of whether the skill is loaded.
<Note>
Flat skills can omit frontmatter—Eve falls back to the first body line as the routing hint. The fixture includes an explicit `description` so intent-based routing is reliable.
</Note>
## Package scripts
| Script | Command | Purpose |
| ------ | ------- | ------- |
| `dev` | `eve dev` | Local runtime + interactive TUI |
| `build` | `eve build` | Compile `.eve/` and build host output |
| `start` | `eve start` | Serve built `.output/` |
| `info` | `eve info` | Print discovered tools, skills, routes, diagnostics |
| `typecheck` | `tsgo --noEmit -p tsconfig.json` | Type-check authored modules |
Dependencies: `eve` (workspace) and `zod` (catalog).
## Local development
<Steps>
<Step title="Install and build the monorepo">
From the Eve repo root:
```bash
pnpm install
pnpm build
```
</Step>
<Step title="Start the fixture">
<Tabs>
<Tab title="From fixture directory">
```bash
cd apps/fixtures/weather-agent
pnpm dev
```
Starts `eve dev` with the interactive TUI. The fixture README notes that booting the TUI itself does not require credentials.
</Tab>
<Tab title="From monorepo root">
```bash
pnpm dev
```
Runs `eve` package watch mode and `weather-agent` headlessly (`--no-ui --port 0`). The orchestrator prints `server listening at http://127.0.0.1:<port>` when ready.
</Tab>
</Tabs>
</Step>
<Step title="Verify discovery">
```bash
cd apps/fixtures/weather-agent
pnpm run info
```
Confirm `get_weather` (tool), `get-weather` (skill), and the default Eve channel routes appear. Use `--json` for machine-readable output.
</Step>
<Step title="Smoke a weather turn">
Ask a weather question in the TUI, for example:
```text
What is the weather in Brooklyn?
```
Expected turn order: optional `load_skill` for `get-weather`, then `get_weather` with `city="Brooklyn"`, then an assistant reply citing the stub result. The TUI collapses tool calls to a one-line summary (`✓ get_weather city="Brooklyn" → 72°F`).
<Warning>
Completing a model turn requires a linked model provider. The dev TUI surfaces missing provider setup as `⚠ 1 setup issue: model provider not linked · /model` before the first message fails.
</Warning>
</Step>
</Steps>
## HTTP smoke paths
The default Eve channel mounts session routes under `/eve/v1/` without an authored `agent/channels/eve.ts`. `localDev()` accepts loopback requests during development.
:::endpoint GET /eve/v1/health
Liveness check. Returns `{ ok: true, status: "ready" }` when the dev server is up.
:::
:::endpoint POST /eve/v1/session
Start a durable session with a user message.
:::
<RequestExample>
```bash
curl -X POST http://127.0.0.1:<port>/eve/v1/session \
-H 'Content-Type: application/json' \
-d '{"message":"What is the weather in Brooklyn?"}'
```
</RequestExample>
<ResponseExample>
```json
{
"ok": true,
"sessionId": "ses_01h...",
"continuationToken": "eve:7f3c..."
}
```
The response also sets the `x-eve-session-id` header.
</ResponseExample>
:::endpoint GET /eve/v1/session/:sessionId/stream
Stream NDJSON events for the session (`application/x-ndjson; charset=utf-8`).
:::
<RequestExample>
```bash
curl -N http://127.0.0.1:<port>/eve/v1/session/<sessionId>/stream
```
</RequestExample>
For a weather query, expect at minimum:
| Event | Meaning |
| ----- | ------- |
| `session.started` | Session admitted |
| `actions.requested` | Model requested `get_weather` |
| `action.result` | Tool returned stub weather data |
| `message.completed` | Assistant reply finished |
| `session.completed` | Turn closed |
:::endpoint POST /eve/v1/session/:sessionId
Send a follow-up with the `continuationToken` from the prior response.
:::
<RequestExample>
```bash
curl -X POST http://127.0.0.1:<port>/eve/v1/session/<sessionId> \
-H 'Content-Type: application/json' \
-d '{"continuationToken":"<token>","message":"Now do Queens."}'
```
</RequestExample>
Scenario tests in `packages/eve/test/scenarios/dev-server.scenario.test.ts` automate this path: they boot `eve dev --no-ui` against `WEATHER_AGENT_DESCRIPTOR`, hit `/eve/v1/health`, send `"hello world"` through the dev client harness, and assert a `message.completed` event—without requiring live model credentials when `NODE_ENV=test` activates the mock-model adapter.
## Headless and CI-oriented flags
| Flag | Effect |
| ---- | ------ |
| `--no-ui` | Skip the TUI; serve HTTP only (also the default when stdout is not a TTY) |
| `--host 127.0.0.1` | Bind address |
| `--port 0` | Let the OS assign a free port (used by root `pnpm dev`) |
| `--tools auto-collapsed` | Collapse tool call display in the TUI (default) |
## What to copy from this fixture
| Pattern | Fixture file | Reuse for |
| ------- | ------------ | --------- |
| Minimal `defineAgent` with `modelOptions` | `agent/agent.ts` | Model and provider tuning |
| Single-sentence instructions | `agent/instructions.md` | Stable agent identity |
| Typed tool with `never()` approval | `agent/tools/get_weather.ts` | Safe auto-run integrations |
| Flat skill with frontmatter routing | `agent/skills/get-weather.md` | On-demand procedures without prompt bloat |
| Package script wiring | `package.json` | Standard `dev` / `build` / `info` workflow |
## Related pages
<CardGroup>
<Card title="Eve quickstart" href="/eve-quickstart">
Run `eve init`, start `eve dev`, and send the first session message.
</Card>
<Card title="Eve project layout" href="/eve-project-layout">
Authored slots under `agent/` and path-derived naming rules.
</Card>
<Card title="Eve authoring surfaces" href="/eve-authoring-surfaces">
Configure `agent.ts`, instructions, tools, skills, and connections from the filesystem contract.
</Card>
<Card title="Eve sessions and streaming" href="/eve-sessions-streaming">
Session lifecycle, NDJSON events, and `continuationToken` follow-ups.
</Card>
<Card title="Eve HTTP protocol reference" href="/eve-http-protocol-reference">
Full route inventory, event types, and client integration entry points.
</Card>
<Card title="Eve CLI reference" href="/eve-cli-reference">
`eve dev`, `eve info`, `eve build`, and flag defaults.
</Card>
</CardGroup>
---