flue
无头式 Agent 编排框架,类似 Claude Code 的可编程版本——用 TypeScript 定义 agent 工作流,支持 HTTP/WebSocket 驱动、MCP 工具接入和远程沙箱(Daytona 等),可在 Node.js 或 Cloudflare Workers 上部署。亮点是 harness + session 抽象让多轮对话和状态管理开箱即用,且依赖标准 Markdown(AGENTS.md)而非复杂配置。实验性项目,API 可能变更。
README
Experimental(实验性) — Flue 正在积极开发中。API 可能发生变化。
正在寻找
v0.0.x?在此处查看。
Flue
Flue 是 Agent 控制框架(The Agent Harness Framework)。 如果你知道如何使用 Claude Code(或 Codex、OpenCode、Pi 等),那么你已经掌握了使用 Flue 构建 agent 的基础。
Flue 是一个 TypeScript 框架,用于构建下一代 agent,其设计围绕内置的 agent 控制(agent harness)。它就像 Claude Code,但 100% 无头且可编程。没有内置的假设,例如需要人类操作员来运行。没有 TUI。没有 GUI。只有 TypeScript。
但使用 Flue 的感觉就像使用 Claude Code。你构建的 agent 自主行动以解决问题和完成任务。它们运行所需的代码很少——大部分“逻辑”存在于 Markdown 中:技能(skills)、上下文(context)和 AGENTS.md。
Flue 不是另一个 AI SDK。它是一个真正的运行时无关框架——把它想象成 Astro 或 Next.js,但用于 agent。编写一次,构建,然后将你的 agent 部署到任何地方(Node.js、Cloudflare、GitHub Actions、GitLab CI/CD 等)。
包
| 包 | 描述 |
|---|---|
@flue/runtime |
运行时:控制(harness)、会话、工具、沙箱 |
@flue/cli |
CLI + 构建/开发工具(flue 二进制文件) |
示例
消息驱动的 agent 在 /agents/:name/:id 接收直接的 HTTP 或 WebSocket 消息;应用拥有的集成可以调用 dispatch(...) 将异步输入传递给 agent 会话。关于这些接口,请参见 消息驱动 Agent。可运行的 WebSocket 示例可在 Node 和 Cloudflare 中找到。
对于外部追踪、指标和错误报告,请参见 可观测性、官方 @flue/opentelemetry 适配器、基于公共 observe(...) 的 Braintrust 追踪示例 以及 Sentry 错误报告示例。
快速开始
最简单的 agent——没有容器,没有工具,只有一个提示词和一个类型化的结果。
除非你选择初始化一个完整的容器沙箱,否则 Flue 将为每个 agent 默认使用一个虚拟沙箱,该沙箱由 just-bash 驱动。与为每个 agent 运行完整容器相比,虚拟沙箱将显著更快、更便宜、更具可扩展性,这使其非常适合构建高流量/高规模的 agent。
// .flue/workflows/hello-world.ts
import { createAgent, type FlueContext, type WorkflowRouteHandler } from '@flue/runtime';
import * as v from 'valibot';
// 公共 HTTP 暴露由导出的 Hono 中间件启用。
export const route: WorkflowRouteHandler = async (_c, next) => next();
const translator = createAgent(() => ({ model: 'anthropic/claude-sonnet-4-6' }));
// 工作流处理程序。工作流编排所在处。
export async function run({ init, payload }: FlueContext) {
// `harness` -- 你初始化的控制,包括沙箱、工具、技能等。
const harness = await init(translator);
const session = await harness.session();
// prompt() 在会话中发送一条消息,触发动作。
const { data } = await session.prompt(
`将以下内容翻译成 ${payload.language}:"${payload.text}"`,
{
// 传递 `result` 以从 agent 获取类型化、经过 schema 验证的数据。
result: v.object({
translation: v.string(),
confidence: v.picklist(['low', 'medium', 'high']),
}),
},
);
return data;
}
支持 Agent
一个支持 agent 也可以在 Cloudflare 上运行,无需容器,使用 Flue 的默认虚拟沙箱。将其文件系统填充 agent 所需的上下文,然后它可以使用内置的 grep、glob 和 read 工具搜索该内容。
由于此 agent 部署在 Cloudflare 上,消息历史和会话状态会自动为你持久化。因此,你(或你的客户)可以在数天、数周或数年后重新访问此支持会话,并精确地从上次中断的地方继续。
// .flue/workflows/support.ts
import { createAgent, type FlueContext, type WorkflowRouteHandler } from '@flue/runtime';
export const route: WorkflowRouteHandler = async (_c, next) => next();
const support = createAgent(() => ({ model: 'openrouter/moonshotai/kimi-k2.6' }));
export async function run({ init, payload }: FlueContext) {
const harness = await init(support);
const session = await harness.session();
await session.fs.mkdir('/workspace/articles', { recursive: true });
await session.fs.writeFile(
'/workspace/articles/reset-password.md',
'# 重置密码\n\n使用账户设置页面请求重置密码的电子邮件。',
);
return await session.prompt(
`你是一个支持 agent。在工作区中搜索与此请求相关的文章,
然后编写一个有用的回复。\n\n客户:${payload.message}`,
);
}
这使用了 Flue 内置的、由 just-bash 驱动的虚拟沙箱;无需连接器或容器。
问题分类(CI)
一个分类 agent,在 GitHub 上打开问题时在 CI 中运行。local() 沙箱使 agent 可以直接访问宿主文件系统和 shell——非常适合 CI 运行器,其中 gh、git 和 npm 已经在 $PATH 中,并且运行器本身是你的隔离边界。
// .flue/workflows/triage.ts
import { createAgent, type FlueContext } from '@flue/runtime';
import { local } from '@flue/runtime/node';
import * as v from 'valibot';
// 因为我们在 CI 中运行它,所以不需要将其暴露为 HTTP 端点。
// CLI 可以从命令行运行任何工作流:`flue run triage ...`
export async function run({ init, payload }: FlueContext) {
// `local()` 使 agent 可以直接访问宿主文件系统和 shell。
// agent 的 bash 工具可以直接运行 `gh`、`git`、`npm`。
// 技能(Skills)和 AGENTS.md 从 process.cwd() 发现。
//
// 默认情况下,只有一小部分 shell 必需的环境变量(PATH、HOME、
// locale 等)从 process.env 继承。传递 `env: { GH_TOKEN: process.env.GH_TOKEN }` 以暴露更多。
//
// `model` 设置此 agent 中每个 prompt/skill 调用的默认模型。
// 通过 prompt()/skill() 上的 `{ model: '...' }` 覆盖每个调用。
const agent = createAgent(() => ({
sandbox: local({
env: { GH_TOKEN: process.env.GH_TOKEN },
}),
model: 'anthropic/claude-opus-4-7',
}));
const harness = await init(agent);
const session = await harness.session();
// 工作区发现的技能通过其 frontmatter `name:` 激活。
// 静态导入的打包技能也可以通过传递其引用来激活。
const { data } = await session.skill('triage', {
// 向任何 prompt 或 skill 传递参数。
args: { issueNumber: payload.issueNumber },
// 结果 schema 非常适合根据 prompt 或 skill 调用返回的
// 结构化 `data` 进行行动/编排。
result: v.object({
severity: v.picklist(['low', 'medium', 'high', 'critical']),
reproducible: v.boolean(),
summary: v.string(),
fix_applied: v.boolean(),
}),
});
return data;
}
编码 Agent(远程沙箱)
上面的示例都在轻量级虚拟沙箱上运行——无需容器。但对于一个完整的编码 agent,你想要一个真正的 Linux 环境,包含 git、Node.js、浏览器和一个已克隆的仓库,随时可用。
Daytona 的声明式镜像构建器允许你用代码定义环境。镜像在第一次构建后缓存,因此后续会话可以立即启动。
使用 flue add daytona | <your-agent>(例如 claude、opencode、codex、cursor-agent)安装 Daytona 连接器。它会在你的项目中写入一个小的 connectors/daytona.ts 适配器,你可以直接导入。
// .flue/workflows/code.ts
import {
Type,
createAgent,
defineTool,
type FlueContext,
type WorkflowRouteHandler,
} from '@flue/runtime';
import { Daytona } from '@daytona/sdk';
import { daytona } from '../connectors/daytona';
export const route: WorkflowRouteHandler = async (_c, next) => next();
export async function run({ init, payload, env }: FlueContext) {
// 每个 agent 通过 Daytona 获得一个真实容器。该容器具有
// 完整的 Linux 环境,包含持久化文件系统和 shell。
//
// 为简单起见,我们在此处总是创建一个新的沙箱。你也可以
// 首先检查 agent 实例 ID 是否存在现有沙箱,并重用该沙箱
// 以最好地继续上次对话中断的地方。
const client = new Daytona({ apiKey: env.DAYTONA_API_KEY });
const sandbox = await client.create();
const setupAgent = createAgent(() => ({
sandbox: daytona(sandbox),
model: 'openai/gpt-5.5',
}));
const setupHarness = await init(setupAgent, { name: 'setup' });
const setup = await setupHarness.session();
// 为简单起见,我们在此处将目标仓库克隆到沙箱中。
// 你也可以将它们烘焙到容器镜像快照中,以获得
// 更快/接近即时的启动速度。
await setup.shell(`git clone ${payload.repo} /workspace/project`);
await setup.shell('npm install', { cwd: '/workspace/project' });
// 在克隆的仓库中启动第二个控制。它共享相同的沙箱,但
// 从 /workspace/project 发现 AGENTS.md 和技能(skills)。
const projectAgent = createAgent(() => ({
sandbox: daytona(sandbox),
cwd: '/workspace/project',
model: 'openai/gpt-5.5',
}));
const projectHarness = await init(projectAgent, { name: 'project' });
const session = await projectHarness.session();
// 编码 agent 不会向用户隐藏 agent DX,因此无需
// 包装用户提示词。直接将其发送给 agent,
// 然后流式返回进度和最终结果。
return await session.prompt(payload.prompt);
}
远程 MCP 工具
MCP 作为运行时工具适配器可用。在可信代码中连接到远程 MCP 服务器,将其工具传递给 init(),并将秘密保存在 env 中,而不是文件系统上下文或提示词中。
// .flue/workflows/assistant.ts
import {
connectMcpServer,
createAgent,
type FlueContext,
type WorkflowRouteHandler,
} from '@flue/runtime';
export const route: WorkflowRouteHandler = async (_c, next) => next();
export async function run({ init, payload, env }: FlueContext) {
const github = await connectMcpServer('github', {
url: 'https://mcp.github.com/mcp',
headers: {
Authorization: `Bearer ${env.GITHUB_TOKEN}`,
},
});
try {
const agent = createAgent(() => ({
model: 'anthropic/claude-sonnet-4-6',
tools: github.tools,
}));
const harness = await init(agent);
const session = await harness.session();
return await session.prompt(payload.prompt);
} finally {
await github.close();
}
}
connectMcpServer() 默认为现代可流式 HTTP。对于传统的 SSE 服务器,传递 transport: 'sse'。Flue 在此版本中不会自动检测传输、生成本地 stdio MCP 服务器或处理 OAuth 回调。
Agent、控制和会话
一个 agent 是 agents/<name>.ts 中的源文件。对于附加的 HTTP 或 WebSocket agent,URL 的 <id> 段标识 agent 实例:一个客户、一个仓库、一个对话空间或其他调用者定义边界的持久运行时范围。
POST /agents/<agent-name>/<id>
GET /agents/<agent-name>/<id> (Upgrade: websocket)
在 agent 模块中,导入 type AgentWebSocketHandler 并导出 const websocket: AgentWebSocketHandler = async (_c, next) => next(); 以打开与该稳定实例的长连接 SDK 连接;它可以在调用 next() 之前添加身份验证。一个 agent socket 可以发出顺序的 prompt 并为每个 prompt 选择一个会话;工作流 socket 是一个连接一次调用。
在工作流中,init(createdAgent) 创建一个控制(harness):一个用于模型默认值、工具、沙箱、文件系统和会话的配置化句柄。当一个工作流需要多个隔离的控制作用域时,传递 init(createdAgent, { name })。在 agent 模块中,运行时在消息到达时初始化模块的默认 createAgent(...) 导出。
默认情况下,harness.session() 打开该 agent 实例的默认控制内的默认会话。重用相同的 URL <id> 以继续相同的 agent 实例。使用新的 URL <id> 以重新开始。
运行(runs)仅属于工作流。直接 HTTP 和 WebSocket prompt 是与 agent 会话的附加交互。异步 agent 交付使用 dispatch(...),其 dispatchId 标识交付而不是运行。Agent 交互不会出现在 /runs 中,也不会使用 flue logs 检查。
# 开始对话(端口 3583 是 `flue dev` 的默认端口)
curl http://localhost:3583/agents/hello/session-abc \
-H "Content-Type: application/json" \
-d '{"message": "Hello, Alice"}'
# 继续那个对话
curl http://localhost:3583/agents/hello/session-abc \
-H "Content-Type: application/json" \
-d '{"message": "我刚才说了什么?"}'
# 开始另一个单独的对话
curl http://localhost:3583/agents/hello/session-xyz \
-H "Content-Type: application/json" \
-d '{"message": "来自另一个对话的问候"}'
Agent 实例拥有沙箱状态,例如跨 prompt 和分派输入交互写入的文件。控制在实例内对相关的会话状态进行分组。会话在控制内持久化消息历史和对话元数据。在 Cloudflare 上,会话数据由 Durable Objects 支持,并在请求之间持久化。在 Node.js 上,会话默认存储在内存中,除非你提供自定义存储。
在生产中,为你想要保留的 agent 实例生成一个稳定的 URL <id>。当你需要在同一个控制内进行多个对话时,使用 harness.session(threadName)。
任务
使用 session.task() 在一个分离的会话中运行一个聚焦的、一次性的子 agent。任务共享相同的沙箱/文件系统,但拥有自己的消息历史,并从其工作目录发现 AGENTS.md 和 .agents/skills/。相同的 task 工具在 prompt() 和 skill() 调用期间也可供 LLM 使用,因此 agent 可以自行委派并行研究或探索工作。
const session = await harness.session();
const research = await session.task('研究认证流程并总结关键文件。', {
cwd: '/workspace/project',
agent: 'researcher',
});
const answer = await session.prompt(
`使用这些研究来草拟实施计划:\n\n${research.text}`,
);
使用 defineAgentProfile() 声明可重用的子 agent 配置,在 createAgent(...) 中配置它们,并使用 task({ agent }) 选择它们进行分离式委派。
const researcher = defineAgentProfile({ name: 'researcher', instructions: '仔细研究。' });
const workflowAgent = createAgent(() => ({
model: 'anthropic/claude-sonnet-4-6',
subagents: [researcher],
}));
const harness = await init(workflowAgent);
const session = await harness.session('review-thread');
await session.prompt('审查最近的更改。');
await session.task('研究相关的问题。', { agent: 'researcher' });
提供者设置
当模型流量需要提供者特定的运行时设置时,使用 configureProvider(...),例如企业 API 网关、兼容提供者的代理、自定义端点或网关特定的凭据。这对于托管凭据、审计日志、流量路由或自托管的 OpenAI 兼容提供者很常见。
在 app.ts 中使用来自模型值的提供者 ID 配置这些设置。它们适用于通过该提供者解析模型的每个控制和会话。
// .flue/app.ts
import { configureProvider } from '@flue/runtime';
import { flue } from '@flue/runtime/routing';
export default {
fetch(req, env, ctx) {
configureProvider('anthropic', {
baseUrl: env.ANTHROPIC_BASE_URL,
headers: { 'X-Custom-Auth': env.GATEWAY_KEY },
// 当代理期望一个合成或网关特定的密钥时使用此选项。
apiKey: 'dummy',
});
return flue().fetch(req, env, ctx);
},
};
导入的 Agent 技能
位于 <cwd>/.agents/skills/<name>/SKILL.md 的工作区技能在运行时被发现,并通过 session.skill('name') 按名称激活。静态技能导入是打包的构建依赖项:
import review from '../skills/review/SKILL.md' with { type: 'skill' };
const agent = createAgent(() => ({ model: 'anthropic/claude-sonnet-4-6', skills: [review] }));
const harness = await init(agent);
const session = await harness.session();
await session.skill('review');
// 或者直接激活导入的引用:await session.skill(review);
静态导入暴露了一个轻量级的 SkillReference,而不是技能内容。Vite 会验证符合规范的 SKILL.md,并打包其技能目录中所有允许的支持文件,不仅仅是 scripts/、references/ 和 assets/。打包的文件对于直接引用激活和对在 skills 中注册了引用的 agent 进行的操作是可读的;仅导入未注册的引用不会将其内容暴露给普通提示词。可能包含密钥或私钥的文件(包括 .env*、.dev.vars*、凭据文件、密钥文件、.aws/、.ssh/ 和 .gnupg/)会拒绝构建而不会被部署。将凭据保留在技能目录之外。
自定义虚拟沙箱
对于大多数 agent,使用内置的虚拟沙箱或 sandbox: local()(仅限 Node 目标)。生成的默认沙箱由 @flue/runtime 提供;应用程序不需要声明 just-bash,除非编写的应用程序代码直接导入它。如果你需要直接自定义 just-bash,请添加 just-bash 作为应用程序依赖项并传递一个 Bash 工厂。工厂必须每次返回一个新鲜的类似 Bash 的运行时;在闭包中共享文件系统对象,以在会话和提示词之间持久化文件。
import { Bash, InMemoryFs } from 'just-bash';
const fs = new InMemoryFs();
const agent = createAgent(() => ({
sandbox: () => new Bash({ fs, cwd: '/workspace', python: true }),
model: 'anthropic/claude-sonnet-4-6',
}));
const harness = await init(agent);
const session = await harness.session();
连接器
连接器(Connectors)将第三方服务(沙箱提供商等)适配到 Flue 中。它们不是一个 npm 包——它们是托管在 https://flueframework.com/cli/connectors/ 的 markdown 安装说明,并由你的 AI 编码 agent 应用到你的项目中。
flue add # 列出可用的连接器
flue add daytona | claude # 管道到你的编码 agent(claude、opencode、codex、cursor-agent 等)
flue add https://e2b.dev --category sandbox | claude # 从头开始构建一个——将提供者的文档 URL 作为 agent 的起点
CLI 会获取指定连接器的 markdown 并将其打印到标准输出(当由 agent 运行时,或使用 --print),或者在人类在终端中运行时显示一个短的、可复制的 flue add ... | <agent> 配方。你的 agent 会读取 markdown,并在选定的源目录下写入一个小型的 TypeScript 适配器:首先是 .flue/,然后是 src/,最后是项目根目录。
运行和连接
本地开发(flue dev)
长期运行的监视模式开发服务器。在文件更改时重建并重新加载——编辑 agent,重新运行 curl,看到你的更改。
flue dev --target node # Node.js 开发服务器
flue dev --target cloudflare # Cloudflare Vite/workerd 开发服务器
默认端口为 3583(手机键盘上的 "FLUE")。使用 --port 覆盖。
import { createFlueClient } from '@flue/sdk';
const client = createFlueClient({ baseUrl: 'http://localhost:3583' });
const chat = client.agents.connect('chat', 'customer-123');
await chat.ready;
await chat.prompt('Hello', { session: 'support' });
chat.close();
const job = client.workflows.connect('summarize');
await job.ready;
await job.invoke({ text: 'Summarize me' });
导出的 websocket 中间件可以对其自己的 agent 或工作流 socket 端点进行身份验证。使用自定义 app.ts 进行集中式身份验证或挂载前缀,在 app.route('/api', flue()) 之前应用普通的 Hono 中间件;相同的路由模型适用于 Node 和 Cloudflare。在 baseUrl: 'https://example.com/api' 中包含自定义挂载点,并使用 websocketUrl: (url) => { url.searchParams.set('token', socketToken); return url; } 进行 URL 携带或签名的握手身份验证。HTTP token 和 headers 选项不会自动应用于 WebSocket 升级;浏览器应使用 cookie 或应用程序设计的 URL 身份验证,而需要实现特定标头的 Node 客户端可以提供自定义的 websocket 工厂。避免在 WebSocket 升级路由周围使用会修改标头的中间件。flue dev --target cloudflare 要求 wrangler 作为项目中的对等依赖项(npm install --save-dev wrangler)。
加载环境变量
flue build、flue dev、flue run 和 flue connect 在配置时加载项目根目录的 .env 文件(如果存在):
flue dev --target node
使用 --env <path> 选择一个替代文件;shell 设置的值优先。flue build 可能在评估配置时使用加载的值,但生成的产物在部署运行时不会加载 .env。对于 Cloudflare 开发,Worker 运行时绑定继续使用官方的 .dev.vars 或 .env 文件以及 CLOUDFLARE_ENV 约定。
从 CLI 触发(flue run)
构建并本地运行任何工作流,非常适合 CI 或一次性脚本调用。CLI 通过私有的子进程通信调用构建的 Node 产物,独立于公共工作流路由和中间件。
flue run hello --target node \
--payload '{"text": "Hello world", "language": "French"}'
连接到 Agent 实例(flue connect)
打开一个与 agent 实例的交互式本地会话。构建的 Node 子进程在连接期间保持存活,因此重复的提示词可以共享内存中的会话状态。
flue connect chat customer-123 --target node --session support
从 HTTP 端点触发(flue build)
构建并将你的 agent 部署为 Web 服务器,非常适合托管的 agent。
flue build 构建到 ./dist 目录,然后你可以部署。目前支持 Cloudflare 和任何 Node.js 主机,未来将支持更多。
flue build --target node # Node.js 服务器(单个捆绑的 .mjs 文件)
flue build --target cloudflare # Cloudflare Workers + Durable Objects
对于 Cloudflare,flue build 和 flue dev 通过官方的 Cloudflare Vite/workerd 集成运行。生产构建会生成可由 wrangler deploy 消费的可部署输出。