# Build Flue agents

> Author agents with `createAgent`, `defineAgentProfile`, tools, skills, sandboxes, providers, and HTTP route handlers.

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

## 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>
