@flue/sdk 0.3.5 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/{agent-BB4lwAd5.mjs → agent-BTB0809P.mjs} +1 -1
- package/dist/client.d.mts +2 -2
- package/dist/client.mjs +3 -3
- package/dist/cloudflare/index.d.mts +2 -2
- package/dist/cloudflare/index.mjs +5 -3
- package/dist/{command-helpers-DdAfbnom.d.mts → command-helpers-CXzopT_-.d.mts} +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +198 -77
- package/dist/internal.d.mts +264 -3
- package/dist/internal.mjs +420 -4
- package/dist/{mcp-DOgMtp8y.mjs → mcp-DTFRe9vh.mjs} +4 -3
- package/dist/{mcp-BVF-sOBZ.d.mts → mcp-EZy-Vb5M.d.mts} +1 -1
- package/dist/node/index.d.mts +2 -2
- package/dist/sandbox.d.mts +2 -1
- package/dist/sandbox.mjs +31 -5
- package/dist/{session-DukL3zwF.mjs → session-BaaSQTWS.mjs} +6 -4
- package/dist/{types-T8pE1xIS.d.mts → types-CItTrBsU.d.mts} +9 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
# Flue
|
|
6
6
|
|
|
7
|
-
Flue is **The
|
|
7
|
+
Flue is **The Agent Harness Framework.** If you know how to use Claude Code (or OpenCode, Codex, Gemini, etc)... then you already know the basics of how to build agents with Flue.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Flue is a TypeScript framework for building the next generation of agents, designed around a built-in **agent harness**. It's like Claude Code, but 100% headless and programmable. There's no baked-in assumption like requiring a human operator to function. No TUI. No GUI. Just TypeScript.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
But using Flue feels like using Claude Code. The agents you build act autonomously to solve problems and complete tasks. They require very little code to run — most of the "logic" lives in Markdown: skills, context, and `AGENTS.md`.
|
|
12
|
+
|
|
13
|
+
Flue isn't another AI SDK. It's a proper runtime-agnostic framework — think Astro or Next.js, but for agents. Write once, build, and deploy your agents anywhere (Node.js, Cloudflare, GitHub Actions, GitLab CI/CD, etc).
|
|
12
14
|
|
|
13
15
|
## Packages
|
|
14
16
|
|
|
@@ -287,7 +287,7 @@ function createBashTool(env) {
|
|
|
287
287
|
}),
|
|
288
288
|
async execute(_toolCallId, params, signal) {
|
|
289
289
|
throwIfAborted(signal);
|
|
290
|
-
return formatBashResult(await env.exec(params.command), params.command);
|
|
290
|
+
return formatBashResult(await env.exec(params.command, { timeout: params.timeout }), params.command);
|
|
291
291
|
}
|
|
292
292
|
};
|
|
293
293
|
}
|
package/dist/client.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as TaskOptions, C as SessionEnv, D as ShellResult, E as ShellOptions, M as ToolParameters, S as SessionData, T as SessionStore, _ as FlueSessions, a as BashLike, d as FileStat, f as FlueAgent, g as FlueSession, h as FlueEventCallback, i as BashFactory, j as ToolDef, k as SkillOptions, l as Command, m as FlueEvent, p as FlueContext, r as AgentInit, t as AgentConfig, v as PromptOptions, w as SessionOptions, x as SandboxFactory, y as PromptResponse } from "./types-
|
|
2
|
-
import { i as connectMcpServer, n as McpServerOptions, r as McpTransport, t as McpServerConnection } from "./mcp-
|
|
1
|
+
import { A as TaskOptions, C as SessionEnv, D as ShellResult, E as ShellOptions, M as ToolParameters, S as SessionData, T as SessionStore, _ as FlueSessions, a as BashLike, d as FileStat, f as FlueAgent, g as FlueSession, h as FlueEventCallback, i as BashFactory, j as ToolDef, k as SkillOptions, l as Command, m as FlueEvent, p as FlueContext, r as AgentInit, t as AgentConfig, v as PromptOptions, w as SessionOptions, x as SandboxFactory, y as PromptResponse } from "./types-CItTrBsU.mjs";
|
|
2
|
+
import { i as connectMcpServer, n as McpServerOptions, r as McpTransport, t as McpServerConnection } from "./mcp-EZy-Vb5M.mjs";
|
|
3
3
|
import { Type } from "@mariozechner/pi-ai";
|
|
4
4
|
|
|
5
5
|
//#region src/client.d.ts
|
package/dist/client.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { r as discoverSessionContext } from "./agent-
|
|
2
|
-
import { a as assertRoleExists } from "./session-
|
|
1
|
+
import { r as discoverSessionContext } from "./agent-BTB0809P.mjs";
|
|
2
|
+
import { a as assertRoleExists } from "./session-BaaSQTWS.mjs";
|
|
3
3
|
import { bashFactoryToSessionEnv, createCwdSessionEnv } from "./sandbox.mjs";
|
|
4
|
-
import { n as AgentClient, t as connectMcpServer } from "./mcp-
|
|
4
|
+
import { n as AgentClient, t as connectMcpServer } from "./mcp-DTFRe9vh.mjs";
|
|
5
5
|
import { Type } from "@mariozechner/pi-ai";
|
|
6
6
|
|
|
7
7
|
//#region src/client.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as SessionEnv, T as SessionStore, l as Command } from "../types-
|
|
2
|
-
import { t as CommandExecutor } from "../command-helpers-
|
|
1
|
+
import { C as SessionEnv, T as SessionStore, l as Command } from "../types-CItTrBsU.mjs";
|
|
2
|
+
import { t as CommandExecutor } from "../command-helpers-CXzopT_-.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/cloudflare/virtual-sandbox.d.ts
|
|
5
5
|
interface VirtualSandboxOptions {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import "../agent-
|
|
2
|
-
import "../session-
|
|
1
|
+
import "../agent-BTB0809P.mjs";
|
|
2
|
+
import "../session-BaaSQTWS.mjs";
|
|
3
3
|
import { createSandboxSessionEnv } from "../sandbox.mjs";
|
|
4
4
|
import { t as normalizeExecutor } from "../command-helpers-hTZKWK13.mjs";
|
|
5
5
|
import { Workspace, WorkspaceFileSystem } from "@cloudflare/shell";
|
|
@@ -195,9 +195,11 @@ async function cfSandboxToSessionEnv(sandbox, cwd = "/workspace") {
|
|
|
195
195
|
} else await sandbox.deleteFile(path);
|
|
196
196
|
},
|
|
197
197
|
async exec(command, execOpts) {
|
|
198
|
+
const timeoutMs = typeof execOpts?.timeout === "number" ? execOpts.timeout * 1e3 : void 0;
|
|
198
199
|
const result = await sandbox.exec(command, {
|
|
199
200
|
cwd: execOpts?.cwd,
|
|
200
|
-
env: execOpts?.env
|
|
201
|
+
env: execOpts?.env,
|
|
202
|
+
timeout: timeoutMs
|
|
201
203
|
});
|
|
202
204
|
return {
|
|
203
205
|
stdout: result.stdout ?? "",
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as TaskOptions, C as SessionEnv, D as ShellResult, E as ShellOptions, M as ToolParameters, O as Skill, S as SessionData, T as SessionStore, _ as FlueSessions, a as BashLike, b as Role, c as BuildPlugin, d as FileStat, f as FlueAgent, g as FlueSession, h as FlueEventCallback, i as BashFactory, j as ToolDef, k as SkillOptions, l as Command, m as FlueEvent, n as AgentInfo, o as BuildContext, p as FlueContext, r as AgentInit, s as BuildOptions, t as AgentConfig, u as CommandDef, v as PromptOptions, w as SessionOptions, x as SandboxFactory, y as PromptResponse } from "./types-
|
|
1
|
+
import { A as TaskOptions, C as SessionEnv, D as ShellResult, E as ShellOptions, M as ToolParameters, O as Skill, S as SessionData, T as SessionStore, _ as FlueSessions, a as BashLike, b as Role, c as BuildPlugin, d as FileStat, f as FlueAgent, g as FlueSession, h as FlueEventCallback, i as BashFactory, j as ToolDef, k as SkillOptions, l as Command, m as FlueEvent, n as AgentInfo, o as BuildContext, p as FlueContext, r as AgentInit, s as BuildOptions, t as AgentConfig, u as CommandDef, v as PromptOptions, w as SessionOptions, x as SandboxFactory, y as PromptResponse } from "./types-CItTrBsU.mjs";
|
|
2
2
|
import { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
3
3
|
|
|
4
4
|
//#region src/build.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { a as parseFrontmatterFile, n as createTools, t as BUILTIN_TOOL_NAMES } from "./agent-
|
|
1
|
+
import { a as parseFrontmatterFile, n as createTools, t as BUILTIN_TOOL_NAMES } from "./agent-BTB0809P.mjs";
|
|
2
2
|
import * as esbuild from "esbuild";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { packageUpSync } from "package-up";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
7
8
|
import { parseEnv } from "node:util";
|
|
8
9
|
|
|
9
10
|
//#region src/cloudflare-wrangler-merge.ts
|
|
@@ -267,11 +268,11 @@ function stripNoisyWranglerDefaults(merged) {
|
|
|
267
268
|
}
|
|
268
269
|
/**
|
|
269
270
|
* Return the list of `class_name`s declared in the user's wrangler
|
|
270
|
-
* `durable_objects.bindings` that
|
|
271
|
+
* `durable_objects.bindings` that end with the literal suffix `Sandbox`
|
|
271
272
|
* (case-sensitive).
|
|
272
273
|
*
|
|
273
274
|
* This is Flue's convention for wiring `@cloudflare/sandbox`: any DO binding
|
|
274
|
-
* whose class name
|
|
275
|
+
* whose class name ends with `Sandbox` triggers an automatic re-export in the
|
|
275
276
|
* generated Worker entry:
|
|
276
277
|
*
|
|
277
278
|
* export { Sandbox as <class_name> } from '@cloudflare/sandbox';
|
|
@@ -281,6 +282,13 @@ function stripNoisyWranglerDefaults(merged) {
|
|
|
281
282
|
* `@cloudflare/sandbox` package. Each distinct `class_name` can be paired with
|
|
282
283
|
* a different container image in the user's `containers[]` config.
|
|
283
284
|
*
|
|
285
|
+
* The match is intentionally a suffix (not substring) so that user-defined
|
|
286
|
+
* classes whose names merely contain "Sandbox" mid-word — e.g. `MySandboxV2`,
|
|
287
|
+
* `MySandboxedAgent`, `LegacySandboxedThing` — are not silently overridden
|
|
288
|
+
* by the `@cloudflare/sandbox` re-export. Note that classes whose names
|
|
289
|
+
* still end in `Sandbox` (e.g. `MockSandbox`, `NotASandbox`) will match;
|
|
290
|
+
* to opt out, rename the class to not end in `Sandbox`.
|
|
291
|
+
*
|
|
284
292
|
* Returns unique, sorted class names. Non-object bindings or bindings without
|
|
285
293
|
* a string `class_name` are ignored.
|
|
286
294
|
*/
|
|
@@ -294,7 +302,7 @@ function detectSandboxBindings(userConfig) {
|
|
|
294
302
|
if (typeof entry !== "object" || entry === null) continue;
|
|
295
303
|
const className = entry.class_name;
|
|
296
304
|
if (typeof className !== "string") continue;
|
|
297
|
-
if (className.
|
|
305
|
+
if (className.endsWith("Sandbox")) found.add(className);
|
|
298
306
|
}
|
|
299
307
|
return Array.from(found).sort();
|
|
300
308
|
}
|
|
@@ -328,7 +336,7 @@ function assertSandboxPackageInstalled(sandboxClassNames, searchDirs) {
|
|
|
328
336
|
current = path.dirname(current);
|
|
329
337
|
}
|
|
330
338
|
}
|
|
331
|
-
throw new Error(`[flue] Your wrangler config declares DO binding(s) whose class_name
|
|
339
|
+
throw new Error(`[flue] Your wrangler config declares DO binding(s) whose class_name ends with "Sandbox" (${sandboxClassNames.join(", ")}), but @cloudflare/sandbox is not in your package.json. Install it: \`npm install @cloudflare/sandbox\`.`);
|
|
332
340
|
}
|
|
333
341
|
/**
|
|
334
342
|
* Write the wrangler deploy-redirect file at `<outputDir>/.wrangler/deploy/config.json`
|
|
@@ -396,6 +404,7 @@ var CloudflarePlugin = class {
|
|
|
396
404
|
}`;
|
|
397
405
|
}).join("\n\n");
|
|
398
406
|
const { config: userConfig } = await this.getUserConfig(ctx.outputDir);
|
|
407
|
+
const sandboxReExports = detectSandboxBindings(userConfig).map((name) => `export { Sandbox as ${name} } from '@cloudflare/sandbox';`).join("\n");
|
|
399
408
|
return `
|
|
400
409
|
// Auto-generated by @flue/sdk build (cloudflare)
|
|
401
410
|
import { Agent, routeAgentRequest } from 'agents';
|
|
@@ -405,6 +414,13 @@ import {
|
|
|
405
414
|
InMemorySessionStore,
|
|
406
415
|
bashFactoryToSessionEnv,
|
|
407
416
|
resolveModel,
|
|
417
|
+
parseJsonBody,
|
|
418
|
+
toHttpResponse,
|
|
419
|
+
toSseData,
|
|
420
|
+
AgentNotFoundError,
|
|
421
|
+
MethodNotAllowedError,
|
|
422
|
+
RouteNotFoundError,
|
|
423
|
+
InvalidRequestError,
|
|
408
424
|
} from '@flue/sdk/internal';
|
|
409
425
|
import { runWithCloudflareContext, cfSandboxToSessionEnv } from '@flue/sdk/cloudflare';
|
|
410
426
|
|
|
@@ -417,6 +433,12 @@ const skills = {};
|
|
|
417
433
|
const systemPrompt = '';
|
|
418
434
|
const manifest = ${manifest};
|
|
419
435
|
|
|
436
|
+
// Set of webhook-accessible agent names (raw form, as used in URL segments).
|
|
437
|
+
// Used by the worker fetch handler to pre-route requests and reject unknown
|
|
438
|
+
// agents with a JSON 404 envelope before the request hits the partyserver
|
|
439
|
+
// dispatcher (which would otherwise return text/plain "Invalid request").
|
|
440
|
+
const webhookAgentNames = new Set(${JSON.stringify(webhookAgents.map((a) => a.name))});
|
|
441
|
+
|
|
420
442
|
// ─── Infrastructure ─────────────────────────────────────────────────────────
|
|
421
443
|
|
|
422
444
|
// No build-time model default. The user sets model at runtime via
|
|
@@ -597,19 +619,16 @@ async function handleAgentRequest(request, doInstance, agentName, handler) {
|
|
|
597
619
|
// Agent id is the DO "room name" set by routeAgentRequest
|
|
598
620
|
const id = doInstance.name;
|
|
599
621
|
|
|
600
|
-
// Parse payload
|
|
601
|
-
let payload;
|
|
602
622
|
try {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
623
|
+
// Parse the request body. Throws on invalid Content-Type or malformed
|
|
624
|
+
// JSON; returns {} for genuinely empty bodies (so no-payload agents
|
|
625
|
+
// still work).
|
|
626
|
+
const payload = await parseJsonBody(request);
|
|
607
627
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
628
|
+
const accept = request.headers.get('accept') || '';
|
|
629
|
+
const isWebhook = request.headers.get('x-webhook') === 'true';
|
|
630
|
+
const isSSE = accept.includes('text/event-stream') && !isWebhook;
|
|
611
631
|
|
|
612
|
-
try {
|
|
613
632
|
// Fire-and-forget (webhook mode)
|
|
614
633
|
if (isWebhook) {
|
|
615
634
|
const requestId = crypto.randomUUID();
|
|
@@ -628,7 +647,13 @@ async function handleAgentRequest(request, doInstance, agentName, handler) {
|
|
|
628
647
|
});
|
|
629
648
|
}
|
|
630
649
|
|
|
631
|
-
// SSE streaming mode
|
|
650
|
+
// SSE streaming mode. Two error regimes meet here:
|
|
651
|
+
// - Pre-stream errors (body parsing, etc.) have already thrown above
|
|
652
|
+
// and are caught by the outer try/catch — rendered as plain HTTP
|
|
653
|
+
// responses by toHttpResponse, since headers haven't been sent yet.
|
|
654
|
+
// - Errors during agent execution surface as in-stream \`error\`
|
|
655
|
+
// events with the canonical envelope (via toSseData), since by
|
|
656
|
+
// then the 200 + text/event-stream headers are already on the wire.
|
|
632
657
|
if (isSSE) {
|
|
633
658
|
const { readable, writable } = new TransformStream();
|
|
634
659
|
const writer = writable.getWriter();
|
|
@@ -640,7 +665,7 @@ async function handleAgentRequest(request, doInstance, agentName, handler) {
|
|
|
640
665
|
const lines = [];
|
|
641
666
|
if (event) lines.push('event: ' + event);
|
|
642
667
|
lines.push('id: ' + eventId++);
|
|
643
|
-
lines.push('data: ' + JSON.stringify(data));
|
|
668
|
+
lines.push('data: ' + (typeof data === 'string' ? data : JSON.stringify(data)));
|
|
644
669
|
lines.push('', '');
|
|
645
670
|
await writer.write(encoder.encode(lines.join('\\n')));
|
|
646
671
|
};
|
|
@@ -662,10 +687,7 @@ async function handleAgentRequest(request, doInstance, agentName, handler) {
|
|
|
662
687
|
'result',
|
|
663
688
|
);
|
|
664
689
|
} catch (err) {
|
|
665
|
-
await writeSSE(
|
|
666
|
-
{ type: 'error', error: String(err) },
|
|
667
|
-
'error',
|
|
668
|
-
);
|
|
690
|
+
await writeSSE(toSseData(err), 'error');
|
|
669
691
|
if (!isIdle) {
|
|
670
692
|
await writeSSE({ type: 'idle' }, 'idle');
|
|
671
693
|
}
|
|
@@ -696,11 +718,10 @@ async function handleAgentRequest(request, doInstance, agentName, handler) {
|
|
|
696
718
|
ctx.setEventCallback(undefined);
|
|
697
719
|
}
|
|
698
720
|
} catch (err) {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
);
|
|
721
|
+
// toHttpResponse logs unknowns via flueLog.error — no extra console.error
|
|
722
|
+
// needed here. The agentName tag is captured in the wrapped error's
|
|
723
|
+
// server-side log line via flueLog's prefix.
|
|
724
|
+
return toHttpResponse(err);
|
|
704
725
|
}
|
|
705
726
|
}
|
|
706
727
|
|
|
@@ -710,38 +731,77 @@ ${agentClasses}
|
|
|
710
731
|
|
|
711
732
|
// ─── User-declared Sandbox re-exports ──────────────────────────────────────
|
|
712
733
|
// One line per DO binding in the user's wrangler.jsonc whose class_name
|
|
713
|
-
//
|
|
734
|
+
// ends with "Sandbox". Flue aliases the single \`Sandbox\` class shipped by
|
|
714
735
|
// \`@cloudflare/sandbox\` so each user-chosen class_name resolves at the
|
|
715
736
|
// bundle's top level. The binding + container image configuration is owned
|
|
716
737
|
// by the user's wrangler.jsonc.
|
|
717
|
-
${
|
|
738
|
+
${sandboxReExports}
|
|
718
739
|
|
|
719
740
|
// ─── Worker Fetch Handler ───────────────────────────────────────────────────
|
|
720
741
|
|
|
721
742
|
export default {
|
|
722
743
|
async fetch(request, env) {
|
|
723
|
-
|
|
744
|
+
try {
|
|
745
|
+
const url = new URL(request.url);
|
|
746
|
+
const method = request.method;
|
|
724
747
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
748
|
+
// Health check
|
|
749
|
+
if (url.pathname === '/health') {
|
|
750
|
+
return new Response(JSON.stringify({ status: 'ok' }), {
|
|
751
|
+
headers: { 'content-type': 'application/json' },
|
|
752
|
+
});
|
|
753
|
+
}
|
|
731
754
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
755
|
+
// Agent manifest
|
|
756
|
+
if (url.pathname === '/agents' && method === 'GET') {
|
|
757
|
+
return new Response(JSON.stringify(manifest), {
|
|
758
|
+
headers: { 'content-type': 'application/json' },
|
|
759
|
+
});
|
|
760
|
+
}
|
|
738
761
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
762
|
+
// Webhook agent route: /agents/<name>/<id>
|
|
763
|
+
//
|
|
764
|
+
// We pre-check method and agent registration here, BEFORE delegating to
|
|
765
|
+
// routeAgentRequest. Without this, partyserver (the transitive
|
|
766
|
+
// dispatcher behind routeAgentRequest) returns a text/plain
|
|
767
|
+
// "Invalid request" 400 for unknown agent namespaces — visibly
|
|
768
|
+
// inconsistent with the rest of the API and with the Node target.
|
|
769
|
+
// Pre-routing means every error path in the agent route flows through
|
|
770
|
+
// the same JSON envelope.
|
|
771
|
+
const agentRouteMatch = url.pathname.match(/^\\/agents\\/([^/]+)\\/([^/]+)\\/?$/);
|
|
772
|
+
if (agentRouteMatch) {
|
|
773
|
+
if (method !== 'POST') {
|
|
774
|
+
throw new MethodNotAllowedError({ method, allowed: ['POST'] });
|
|
775
|
+
}
|
|
776
|
+
const name = decodeURIComponent(agentRouteMatch[1]);
|
|
777
|
+
const id = decodeURIComponent(agentRouteMatch[2]);
|
|
778
|
+
if (name.trim() === '' || id.trim() === '') {
|
|
779
|
+
throw new InvalidRequestError({
|
|
780
|
+
reason: 'Webhook URLs must have the shape /agents/<name>/<id> with non-empty segments.',
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
if (!webhookAgentNames.has(name)) {
|
|
784
|
+
throw new AgentNotFoundError({
|
|
785
|
+
name,
|
|
786
|
+
available: Array.from(webhookAgentNames),
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
// All gating passed. Delegate to the Agents SDK / partyserver, which
|
|
790
|
+
// dispatches into the per-agent DO's onRequest → handleAgentRequest.
|
|
791
|
+
// routeAgentRequest may still return null for shape mismatches we
|
|
792
|
+
// didn't anticipate; treat that as a route_not_found with a hint.
|
|
793
|
+
const response = await routeAgentRequest(request, env);
|
|
794
|
+
if (response) return response;
|
|
795
|
+
throw new RouteNotFoundError({ method, path: url.pathname });
|
|
796
|
+
}
|
|
743
797
|
|
|
744
|
-
|
|
798
|
+
// Anything else: canonical 404 envelope.
|
|
799
|
+
throw new RouteNotFoundError({ method, path: url.pathname });
|
|
800
|
+
} catch (err) {
|
|
801
|
+
// toHttpResponse logs unknowns via flueLog.error — no extra
|
|
802
|
+
// console.error needed at this layer.
|
|
803
|
+
return toHttpResponse(err);
|
|
804
|
+
}
|
|
745
805
|
},
|
|
746
806
|
};
|
|
747
807
|
`;
|
|
@@ -766,7 +826,7 @@ export default {
|
|
|
766
826
|
const sandboxClassNames = detectSandboxBindings(userConfig);
|
|
767
827
|
if (sandboxClassNames.length > 0) {
|
|
768
828
|
assertSandboxPackageInstalled(sandboxClassNames, [ctx.outputDir, ctx.workspaceDir]);
|
|
769
|
-
for (const className of sandboxClassNames) console.log(`[flue]
|
|
829
|
+
for (const className of sandboxClassNames) console.log(`[flue] Auto-wiring DO binding "${className}" to @cloudflare/sandbox's Sandbox class.`);
|
|
770
830
|
}
|
|
771
831
|
const merged = mergeFlueAdditions(userConfig, additions);
|
|
772
832
|
stripNoisyWranglerDefaults(merged);
|
|
@@ -810,6 +870,11 @@ import {
|
|
|
810
870
|
InMemorySessionStore,
|
|
811
871
|
bashFactoryToSessionEnv,
|
|
812
872
|
resolveModel,
|
|
873
|
+
parseJsonBody,
|
|
874
|
+
validateAgentRequest,
|
|
875
|
+
toHttpResponse,
|
|
876
|
+
toSseData,
|
|
877
|
+
RouteNotFoundError,
|
|
813
878
|
} from '@flue/sdk/internal';
|
|
814
879
|
import { randomUUID } from 'node:crypto';
|
|
815
880
|
|
|
@@ -827,7 +892,10 @@ const handlers = {
|
|
|
827
892
|
${agents.map((a) => ` ${JSON.stringify(a.name)}: ${agentVarName(a.name)},`).join("\n")}
|
|
828
893
|
};
|
|
829
894
|
|
|
830
|
-
|
|
895
|
+
// Set of webhook-accessible agent names. Named distinctly from the
|
|
896
|
+
// validateAgentRequest \`webhookAgents\` parameter to keep the call site
|
|
897
|
+
// below readable.
|
|
898
|
+
const webhookAgentSet = new Set(${JSON.stringify(webhookAgents.map((a) => a.name))});
|
|
831
899
|
|
|
832
900
|
// When the CLI starts this server via \`flue run\`, it sets FLUE_MODE=local.
|
|
833
901
|
// In local mode the HTTP route accepts any registered agent (including
|
|
@@ -902,31 +970,29 @@ const app = new Hono();
|
|
|
902
970
|
app.get('/health', (c) => c.json({ status: 'ok' }));
|
|
903
971
|
app.get('/agents', (c) => c.json(manifest));
|
|
904
972
|
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}, 400);
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
app.post('/agents/:name/:id', async (c) => {
|
|
973
|
+
// Catch any method on the agent route so non-POSTs become 405 (instead of
|
|
974
|
+
// Hono's default 404 for unmatched method). Throws are translated by the
|
|
975
|
+
// onError handler into the canonical error envelope.
|
|
976
|
+
app.all('/agents/:name/:id', async (c) => {
|
|
913
977
|
const name = c.req.param('name');
|
|
914
978
|
const id = c.req.param('id');
|
|
915
979
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
980
|
+
// Validate method, name shape, registration, webhook-accessibility.
|
|
981
|
+
// Throws FlueHttpError on any failure; caught by app.onError below.
|
|
982
|
+
validateAgentRequest({
|
|
983
|
+
method: c.req.method,
|
|
984
|
+
name,
|
|
985
|
+
id,
|
|
986
|
+
registeredAgents: Object.keys(handlers),
|
|
987
|
+
webhookAgents: Array.from(webhookAgentSet),
|
|
988
|
+
allowNonWebhook: isLocalMode,
|
|
989
|
+
});
|
|
922
990
|
|
|
923
991
|
const handler = handlers[name];
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
payload = {};
|
|
929
|
-
}
|
|
992
|
+
|
|
993
|
+
// Parse the request body. Throws on invalid Content-Type or malformed JSON;
|
|
994
|
+
// returns {} for genuinely empty bodies (so no-payload agents still work).
|
|
995
|
+
const payload = await parseJsonBody(c.req.raw);
|
|
930
996
|
|
|
931
997
|
const accept = c.req.header('accept') || '';
|
|
932
998
|
const isWebhook = c.req.header('x-webhook') === 'true';
|
|
@@ -949,7 +1015,13 @@ app.post('/agents/:name/:id', async (c) => {
|
|
|
949
1015
|
return c.json({ status: 'accepted', requestId }, 202);
|
|
950
1016
|
}
|
|
951
1017
|
|
|
952
|
-
// SSE streaming mode
|
|
1018
|
+
// SSE streaming mode. Two error regimes meet here:
|
|
1019
|
+
// - Pre-stream errors (validation, body parsing, agent lookup) have
|
|
1020
|
+
// already thrown above and are rendered as plain HTTP responses by
|
|
1021
|
+
// app.onError — headers haven't been sent yet, so this works.
|
|
1022
|
+
// - Errors during agent execution surface as in-stream \`error\` events
|
|
1023
|
+
// with the canonical envelope (via toSseData), since by then the
|
|
1024
|
+
// 200 + text/event-stream headers are already on the wire.
|
|
953
1025
|
if (isSSE) {
|
|
954
1026
|
return streamSSE(c, async (stream) => {
|
|
955
1027
|
let eventId = 0;
|
|
@@ -973,7 +1045,7 @@ app.post('/agents/:name/:id', async (c) => {
|
|
|
973
1045
|
});
|
|
974
1046
|
} catch (err) {
|
|
975
1047
|
await stream.writeSSE({
|
|
976
|
-
data:
|
|
1048
|
+
data: toSseData(err),
|
|
977
1049
|
event: 'error',
|
|
978
1050
|
id: String(eventId++),
|
|
979
1051
|
});
|
|
@@ -987,18 +1059,29 @@ app.post('/agents/:name/:id', async (c) => {
|
|
|
987
1059
|
});
|
|
988
1060
|
}
|
|
989
1061
|
|
|
990
|
-
// Sync mode (default)
|
|
1062
|
+
// Sync mode (default). Errors propagate to app.onError.
|
|
1063
|
+
const ctx = createContextForRequest(id, payload);
|
|
991
1064
|
try {
|
|
992
|
-
const ctx = createContextForRequest(id, payload);
|
|
993
1065
|
const result = await handler(ctx);
|
|
994
|
-
ctx.setEventCallback(undefined);
|
|
995
1066
|
return c.json({ result: result !== undefined ? result : null });
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
return c.json({ error: String(err) }, 500);
|
|
1067
|
+
} finally {
|
|
1068
|
+
ctx.setEventCallback(undefined);
|
|
999
1069
|
}
|
|
1000
1070
|
});
|
|
1001
1071
|
|
|
1072
|
+
// 404 handler — fires for any URL that didn't match a registered route.
|
|
1073
|
+
app.notFound((c) => {
|
|
1074
|
+
// Throw rather than return so the onError handler is the single source of
|
|
1075
|
+
// truth for error-envelope shaping.
|
|
1076
|
+
throw new RouteNotFoundError({ method: c.req.method, path: new URL(c.req.url).pathname });
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
// Single-source-of-truth error renderer. Every thrown FlueError (and every
|
|
1080
|
+
// thrown unknown) is converted to the canonical JSON envelope here.
|
|
1081
|
+
// toHttpResponse takes care of logging unknowns — no extra console.error
|
|
1082
|
+
// needed at this layer.
|
|
1083
|
+
app.onError((err) => toHttpResponse(err));
|
|
1084
|
+
|
|
1002
1085
|
// ─── Start ──────────────────────────────────────────────────────────────────
|
|
1003
1086
|
|
|
1004
1087
|
const port = parseInt(process.env.PORT || '3000', 10);
|
|
@@ -1594,6 +1677,29 @@ var CloudflareReloader = class {
|
|
|
1594
1677
|
port;
|
|
1595
1678
|
configPath;
|
|
1596
1679
|
envFiles;
|
|
1680
|
+
/**
|
|
1681
|
+
* Stable container build ID for the lifetime of this reloader instance.
|
|
1682
|
+
*
|
|
1683
|
+
* `unstable_startWorker` does NOT default this field — only wrangler's CLI
|
|
1684
|
+
* path does, via `generateContainerBuildId()`. When the user's wrangler
|
|
1685
|
+
* config declares `containers[]` (e.g. via `@cloudflare/sandbox`), the
|
|
1686
|
+
* first `onBundleComplete` calls `getImageNameFromDOClassName(...)` which
|
|
1687
|
+
* asserts that `options.containerBuildId` is set; without this, the
|
|
1688
|
+
* assertion throws, the `ProxyController` never gets `reloadComplete`,
|
|
1689
|
+
* and every request hangs (including `/health`). See issue #22.
|
|
1690
|
+
*
|
|
1691
|
+
* We generate it once per reloader and reuse it across reloads so that
|
|
1692
|
+
* wrangler's container-prep cache hits when nothing about the image
|
|
1693
|
+
* changed. Format matches wrangler's own helper: an 8-char UUID slice.
|
|
1694
|
+
*/
|
|
1695
|
+
containerBuildId;
|
|
1696
|
+
/**
|
|
1697
|
+
* Bound listener for `DevEnv` `'error'` events. Stored so we can detach
|
|
1698
|
+
* it on `disposeWorker()` — the underlying `EventEmitter` outlives the
|
|
1699
|
+
* worker handle, so if the listener stayed attached we'd leak (and
|
|
1700
|
+
* double-fire) across reloads.
|
|
1701
|
+
*/
|
|
1702
|
+
errorListener = null;
|
|
1597
1703
|
url;
|
|
1598
1704
|
constructor(wrangler, opts) {
|
|
1599
1705
|
this.wrangler = wrangler;
|
|
@@ -1601,6 +1707,7 @@ var CloudflareReloader = class {
|
|
|
1601
1707
|
this.port = opts.port;
|
|
1602
1708
|
this.envFiles = opts.envFiles;
|
|
1603
1709
|
this.configPath = path.join(this.outputDir, "dist", "wrangler.jsonc");
|
|
1710
|
+
this.containerBuildId = randomUUID().slice(0, 8);
|
|
1604
1711
|
}
|
|
1605
1712
|
async start() {
|
|
1606
1713
|
await this.startWorker();
|
|
@@ -1659,9 +1766,18 @@ var CloudflareReloader = class {
|
|
|
1659
1766
|
port: this.port
|
|
1660
1767
|
},
|
|
1661
1768
|
watch: false,
|
|
1662
|
-
logLevel: "info"
|
|
1769
|
+
logLevel: "info",
|
|
1770
|
+
containerBuildId: this.containerBuildId
|
|
1663
1771
|
}
|
|
1664
1772
|
});
|
|
1773
|
+
this.errorListener = (event) => {
|
|
1774
|
+
const reason = event?.reason ?? "unknown error";
|
|
1775
|
+
const cause = event?.cause;
|
|
1776
|
+
const causeMsg = cause && typeof cause === "object" && "message" in cause ? cause.message : void 0;
|
|
1777
|
+
console.error(`[flue] Wrangler error (${event?.source ?? "unknown"}): ${reason}`);
|
|
1778
|
+
if (causeMsg) console.error(`[flue] ${causeMsg}`);
|
|
1779
|
+
};
|
|
1780
|
+
this.worker.raw.on("error", this.errorListener);
|
|
1665
1781
|
try {
|
|
1666
1782
|
this.url = (await this.worker.url).toString().replace(/\/$/, "");
|
|
1667
1783
|
} catch {
|
|
@@ -1670,8 +1786,13 @@ var CloudflareReloader = class {
|
|
|
1670
1786
|
}
|
|
1671
1787
|
async disposeWorker() {
|
|
1672
1788
|
const worker = this.worker;
|
|
1789
|
+
const listener = this.errorListener;
|
|
1673
1790
|
this.worker = null;
|
|
1791
|
+
this.errorListener = null;
|
|
1674
1792
|
if (!worker) return;
|
|
1793
|
+
if (listener) try {
|
|
1794
|
+
worker.raw.off("error", listener);
|
|
1795
|
+
} catch {}
|
|
1675
1796
|
try {
|
|
1676
1797
|
await worker.dispose();
|
|
1677
1798
|
} catch (err) {
|