flue-framework
NewFlue Agent Framework v0.9.1+ — TypeScript harness-driven agent framework. Covers createAgent, init, defineTool, defineAgentProfile, routing, SSE streaming, production deployment, and all known pitfalls.
Overview
Flue Framework Skill
Flue is a TypeScript framework for building AI agents using the harness-driven architecture. This skill provides comprehensive guidance for creating, developing, and deploying Flue agents (tested with v0.9.1).
Quick Start
1. Initialize a Flue Project
npm install @flue/runtime valibot@^1.0.0
npm install --save-dev @flue/cli typescript
npx flue init --target node2. Create Your First Agent
Create src/agents/hello-world.ts:
import { createAgent } from '@flue/runtime';
export default createAgent(() => ({
model: 'anthropic/claude-sonnet-4-6',
instructions: 'Tell a funny hello world engineering joke.',
}));3. Run the Agent
npx flue connect hello-world --target node --env .envCore Concepts
Agent equals Model plus Harness. The model generates responses while the harness provides sessions for conversation context, tools for custom functions, skills for reusable knowledge, and a sandbox for isolated execution.
Two Agent Patterns (Critical)
Flue has two distinct patterns for creating agents. Using the wrong one causes errors.
Pattern 1: Interactive Agent (for web chat, SSE, WebSocket)
Uses createAgent() directly. The agent is long-lived with sessions and routing.
// src/agents/social-search.ts
import { createAgent, defineTool, Type, local } from '@flue/runtime';
import type { AgentRouteHandler, AgentWebSocketHandler } from '@flue/runtime';
const myTool = defineTool({
name: 'my_tool',
description: 'Does something useful',
parameters: Type.Object({
query: Type.String({ description: 'Search query' }),
}),
execute: async ({ query }) => {
return JSON.stringify({ result: query });
},
});
const agent = createAgent(() => ({
model: 'deepseek/deepseek-v4-flash',
instructions: 'You are a helpful assistant.',
sandbox: local(),
tools: [myTool],
}));
// These exports are REQUIRED for HTTP/WS access
export const route: AgentRouteHandler = agent.route;
export const websocket: AgentWebSocketHandler = agent.websocket;
export default agent;Pattern 2: Workflow Agent (for CLI flue run)
Uses a handler function with FlueContext. Must call createAgent() then init(agent).
// src/workflows/keyword-search.ts
import { createAgent, type FlueContext } from '@flue/runtime';
import * as v from 'valibot';
async function handler({ init, payload, env }: FlueContext) {
const agent = createAgent(() => ({
model: env.MODEL || 'deepseek/deepseek-v4-flash',
instructions: 'Search for keywords and return structured results.',
}));
const harness = await init(agent);
const session = await harness.session();
const result = await session.prompt(
`Search for: ${payload.keyword}`,
{ result: v.object({ items: v.array(v.string()) }) },
);
return result;
}
export default handler;⚠️ CRITICAL: init() API
// ❌ WRONG — init() does NOT accept a plain config object
const harness = await init({
model: 'deepseek/deepseek-v4-flash',
instructions: '...',
providers: { deepseek: { apiKey: '...' } },
});
// ✅ CORRECT — init() requires a createAgent() result
const agent = createAgent(() => ({
model: 'deepseek/deepseek-v4-flash',
instructions: '...',
}));
const harness = await init(agent);
// ✅ init() second parameter is AgentHarnessOptions { name, tools, skills, subagents }
const harness = await init(agent, {
tools: [myTool],
subagents: [researcherProfile],
});The init() function does NOT accept providers, instructions, model, or sandbox in its second parameter. Those belong in createAgent().
Provider Configuration
Built-in Providers
| Provider ID | Environment Variable | Example Model Specifier |
|---|---|---|
anthropic | ANTHROPIC_API_KEY | anthropic/claude-sonnet-4-6 |
openai | OPENAI_API_KEY | openai/gpt-4.1 |
deepseek | DEEPSEEK_API_KEY | deepseek/deepseek-v4-flash |
openrouter | OPENROUTER_API_KEY | openrouter/anthropic/claude-sonnet-4-6 |
Using DeepSeek
DeepSeek is a built-in provider. Set the env var and use the model specifier — no provider config needed:
# .env
DEEPSEEK_API_KEY=sk-your-key
MODEL=deepseek/deepseek-v4-flash// In createAgent — just use the model specifier
const agent = createAgent(() => ({
model: env.MODEL || 'deepseek/deepseek-v4-flash',
instructions: '...',
}));Overriding Provider Settings (baseUrl, apiKey)
Use init({ providers: {...} }) ONLY when you need to override transport settings (custom baseUrl, different apiKey). The providers option goes in the first argument (createAgent config), NOT in init():
// ✅ Provider overrides go in createAgent
const agent = createAgent(() => ({
model: 'anthropic/claude-haiku-4-5',
instructions: '...',
// providers config is NOT available here in current version
}));
// For DeepSeek via Anthropic-compatible endpoint, set env vars:
// ANTHROPIC_API_KEY=sk-your-deepseek-key
// ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropicThird-Party OpenAI-Compatible Providers
Register in src/app.ts (NOT in agent files — build discards top-level side effects):
import { registerProvider } from '@flue/runtime';
registerProvider('my-provider', {
api: 'openai-completions',
baseUrl: 'https://my-provider.example.com/v1',
apiKey: process.env.MY_PROVIDER_API_KEY,
models: { 'my-model': { maxTokens: 8192 } },
});Custom Tools with defineTool()
⚠️ Tool Parameters Use TypeBox, NOT Valibot
import { defineTool, Type } from '@flue/runtime';
const myTool = defineTool({
name: 'http_request',
description: 'Make an HTTP request',
parameters: Type.Object({
method: Type.Union([Type.Literal('GET'), Type.Literal('POST')]),
url: Type.String({ description: 'Target URL' }),
body: Type.Optional(Type.String({ description: 'POST body as JSON string' })),
}),
execute: async ({ method, url, body }) => {
// execute() must return Promise<string>
const result = await fetch(url, { method, body });
return JSON.stringify(await result.json());
},
});Key points:
- •Import
Typefrom@flue/runtime(TypeBox), not from valibot - •
execute()must returnPromise<string>(return JSON.stringify for objects) - •
execute()runs in Node.js — you can use any Node.js API (https, child_process, fs, etc.) - •No need for
harness.env.exec()insideexecute()— use Node.js APIs directly
Tools Can Use Node.js APIs Directly
// ✅ Direct Node.js HTTP in defineTool — no sandbox needed
import https from 'node:https';
execute: async ({ path }) => {
return new Promise((resolve) => {
const req = https.request({ hostname: 'api.example.com', path }, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => resolve(data));
});
req.end();
});
}Sub-Agents with defineAgentProfile()
import { defineAgentProfile } from '@flue/runtime';
export const researcher = defineAgentProfile({
name: 'researcher',
description: 'Deep data collection agent with pagination support',
instructions: 'You are a data collection specialist...',
});Use in agent:
import { researcher } from '../shared/profiles/researcher';
const agent = createAgent(() => ({
model: 'deepseek/deepseek-v4-flash',
instructions: '...',
subagents: [researcher],
}));Sandbox
local() — Host Machine Access
import { local } from '@flue/runtime/node';
const agent = createAgent(() => ({
model: 'deepseek/deepseek-v4-flash',
instructions: '...',
sandbox: local(), // Access host filesystem, Python, curl, etc.
}));Use local() when the agent needs to run shell commands, access host Python, or interact with the real filesystem. Import from @flue/runtime/node.
Project Layout
Flue discovers agents and workflows from a source directory (priority: .flue/ > src/ > project root):
my-project/
├─ src/
│ ├─ agents/
│ │ └─ social-search.ts ← agent "social-search"
│ ├─ workflows/
│ │ └─ keyword-search.ts ← workflow "keyword-search"
│ ├─ shared/
│ │ ├─ tools/ ← defineTool modules
│ │ ├─ profiles/ ← defineAgentProfile modules
│ │ └─ constants.ts
│ └─ app.ts ← Entry point (Hono + flue routes)
├─ public/
│ └─ index.html ← Web chat frontend
├─ .env
├─ package.json
└─ flue.config.ts- •
flue run <name>→ discovers fromagents/only - •Workflows → invoked via HTTP
POST /workflows/<name> - •
.flue/takes priority oversrc/— if both exist,src/is ignored
Application Entry Point (app.ts)
Custom routes can be added before mounting Flue:
import { flue } from '@flue/runtime/routing';
import { Hono } from 'hono';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
const app = new Hono();
// Custom routes BEFORE flue()
app.get('/', (c) => {
const html = readFileSync(resolve(process.cwd(), 'public', 'index.html'), 'utf-8');
return c.html(html);
});
// Mount Flue agent routes
app.route('/', flue());
export default app;HTTP API & SSE Streaming
Agent Endpoints
| Endpoint | Method | Description |
|---|---|---|
/agents/<name>/<session-id> | POST | Send message ({ "message": "..." }) |
/agents/<name>/<session-id> | GET | WebSocket upgrade |
/openapi.json | GET | OpenAPI schema |
SSE Streaming (Real-time Intermediate Events)
Add Accept: text/event-stream header to get streaming events:
curl -N -X POST http://localhost:3583/agents/social-search/my-session \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"message":"微博热搜Top3"}'SSE events:
| Event | Description |
|---|---|
thinking_start / thinking_delta / thinking_end | Agent reasoning process |
tool_execution_start | Tool starts (contains toolName, tool.input) |
tool_execution_end | Tool finishes (contains result/output) |
tool_call | Tool call summary (contains durationMs) |
text_delta | Streaming text fragment of final response |
agent_end / operation | Agent finishes (may contain result.text) |
SSE format: each line is data: <JSON>\n\n.
Response Format (non-SSE)
{
"result": { "text": "## 微博热搜 Top 5\n..." },
"usage": { "input": 9598, "output": 717, "totalTokens": 23627 },
"model": { "provider": "deepseek", "id": "deepseek-v4-flash" }
}Session API
session.prompt()
// Without result → returns { text: string }
const response = await session.prompt('Hello');
console.log(response.text);
// With result → returns parsed valibot data DIRECTLY (not wrapped)
const result = await session.prompt('2+2?', {
result: v.object({ answer: v.number() }),
});
console.log(result.answer); // Direct access, NOT result.data.answerDeepSeek Structured Output Hint
DeepSeek needs explicit JSON instructions in the prompt:
// ✅ Include JSON format hint for DeepSeek
await session.prompt(
'What is 2 + 2? Respond as JSON: {"answer": 4}.',
{ result: v.object({ answer: v.number() }) },
);Add to agent instructions: "When asked for structured data, always respond with valid JSON matching the requested schema."
session.task()
// Delegate to sub-agent — also returns data directly with result schema
const data = await session.task('Research this topic', {
agent: 'researcher',
result: v.object({ findings: v.array(v.string()) }),
});Harness API
⚠️ harness.fs Does NOT Exist — Use harness.env
// ❌ WRONG
await harness.fs.writeFile('/file.md', content);
// ✅ CORRECT
await harness.env.writeFile('/file.md', content);
await harness.env.readFile('/file.md');
await harness.env.exists('/file.md');
await harness.env.mkdir('/dir', { recursive: true });
await harness.env.readdir('/dir');
await harness.env.rm('/file');
await harness.env.cwd;Production Deployment
Build & Start
npm run build # Build to dist/
npm start # Start production serverProduction Start Script
The production server needs env vars loaded and port set explicitly (defaults to 3000):
{
"scripts": {
"start": "bash -c 'set -a && source .env && set +a && PORT=3583 node dist/server.mjs'"
}
}Dev vs Production
| Mode | Command | Behavior |
|---|---|---|
| Dev | flue dev --target node --env .env | Watches ALL root files, hot reloads, kills active connections on any file change |
| Production | npm start | No hot reload, stable connections |
⚠️ Dev server watches ALL files in project root — changes to .md, .txt, scripts/, etc. trigger rebuilds and restart the server, dropping active SSE connections. Use production mode for stable serving.
Dependency Requirements
| Dependency | Version | Why |
|---|---|---|
| Node.js | >= 22.18.0 | Engine requirement |
| valibot | ^1.0.0 | v0.x causes TypeError: Cannot read properties of undefined (reading 'flatMap') |
node --version # Must be >= 22.18.0
npm ls valibot # Must show 1.x.xTroubleshooting
init() requires an agent created with createAgent(...)
Cause: Passing a plain config object to init() instead of a createAgent() result.
Fix: Always use const agent = createAgent(() => ({...})); init(agent).
Invalid model "xxx". Use "provider/model-id" format
Fix: Use provider/model-id format: deepseek/deepseek-v4-flash not deepseek-v4-flash.
403 Forbidden with Anthropic provider + DeepSeek API
Cause: DeepSeek default endpoint is OpenAI-compatible, not Anthropic-compatible.
Fix: Use built-in deepseek provider: model: 'deepseek/deepseek-v4-flash', or use baseUrl: 'https://api.deepseek.com/anthropic'.
registerProvider() not working in agent files
Cause: Flue build extracts only the handler function from agents. Top-level side effects are discarded.
Fix: Place registerProvider() in src/app.ts.
Agent "xxx" not found with flue run
Cause: File is in workflows/. flue run only discovers from agents/.
Fix: Move to agents/ or use HTTP routes for workflows.
Dev server keeps dropping connections
Cause: flue dev watches all root files and restarts on any change.
Fix: Use production mode (npm start) or remove non-essential files from project root.
CLI Commands
| Command | Description |
|---|---|
flue init | Initialize project configuration |
flue dev | Start dev server (hot reload, unstable for active connections) |
flue build | Build to dist/ |
flue run <agent> | Execute an agent (discovers from agents/ only) |
flue connect <agent> | Interactive agent session |
flue run
npx flue run <agent-name> --target node --payload '<json>' --env ".env"Note: --id flag is NOT supported in current version.
Complete Example: Web Chat Agent
Agent (src/agents/social-search.ts)
import { createAgent, defineTool, Type, local } from '@flue/runtime';
import type { AgentRouteHandler, AgentWebSocketHandler } from '@flue/runtime';
const apiCall = defineTool({
name: 'api_call',
description: 'Call an external API',
parameters: Type.Object({
method: Type.Union([Type.Literal('GET'), Type.Literal('POST')]),
path: Type.String({ description: 'API path' }),
params: Type.Optional(Type.Record(Type.String(), Type.String())),
}),
execute: async ({ method, path, params }) => {
// Direct Node.js https request — no Python/shell needed
return JSON.stringify({ method, path, params });
},
});
const agent = createAgent(() => ({
model: 'deepseek/deepseek-v4-flash',
instructions: `You are a social media search assistant.
Analyze user intent, call appropriate APIs, and return results.
When asked for structured data, always respond with valid JSON.`,
sandbox: local(),
tools: [apiCall],
}));
export const route: AgentRouteHandler = agent.route;
export const websocket: AgentWebSocketHandler = agent.websocket;
export default agent;Frontend SSE Consumer (excerpt)
const res = await fetch(`/agents/social-search/${sessionId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
},
body: JSON.stringify({ message: userInput }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split('\n');
buf = lines.pop();
for (const line of lines) {
if (line.startsWith('data: ')) {
const evt = JSON.parse(line.slice(6));
// evt.type: thinking_start, tool_execution_start, text_delta, etc.
handleEvent(evt);
}
}
}Documentation Reference
Core Docs
Deployment
- •Node.js | Cloudflare | Render
Install & Usage
mkdir -p .claude/skillsmkdir -p .claude/skills && curl -o .claude/skills/flue-framework.md https://raw.githubusercontent.com/liangdabiao/flue-framework-skill/main/SKILL.md/flue-frameworkFrequently Asked Questions
What is flue-framework?
Flue Agent Framework v0.9.1+ — TypeScript harness-driven agent framework. Covers createAgent, init, defineTool, defineAgentProfile, routing, SSE streaming, production deployment, and all known pitfalls.
How to install flue-framework?
To install flue-framework, create the .claude/skills directory in your project, then run the curl command to download the skill file. Once installed, invoke it in Claude Code with /flue-framework.
What is flue-framework best for?
flue-framework is a community categorized under General. It is designed for: deployment, agent, typescript. Created by liangdabiao.