@flue/sdk 0.3.5 → 0.3.7
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 +33 -3
- package/dist/{agent-BB4lwAd5.mjs → agent-BTB0809P.mjs} +1 -1
- package/dist/client.d.mts +3 -3
- package/dist/client.mjs +33 -12
- package/dist/cloudflare/index.d.mts +2 -2
- package/dist/cloudflare/index.mjs +5 -3
- package/dist/{command-helpers-DdAfbnom.d.mts → command-helpers-5DpOaRIB.d.mts} +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +199 -92
- package/dist/internal.d.mts +265 -4
- package/dist/internal.mjs +438 -6
- package/dist/{mcp-DOgMtp8y.mjs → mcp-B13ZPduG.mjs} +4 -3
- package/dist/{mcp-BVF-sOBZ.d.mts → mcp-CKMPhMDe.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-CNOAfV45.mjs} +14 -8
- package/dist/{types-T8pE1xIS.d.mts → types-CKcp6T-y.d.mts} +64 -16
- package/package.json +1 -1
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,12 +433,11 @@ const skills = {};
|
|
|
417
433
|
const systemPrompt = '';
|
|
418
434
|
const manifest = ${manifest};
|
|
419
435
|
|
|
420
|
-
//
|
|
421
|
-
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
const model = undefined;
|
|
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))});
|
|
426
441
|
|
|
427
442
|
// ─── Sandbox Environments ───────────────────────────────────────────────────
|
|
428
443
|
|
|
@@ -527,7 +542,7 @@ function createContextForRequest(id, payload, doInstance) {
|
|
|
527
542
|
payload,
|
|
528
543
|
env: doInstance?.env ?? {},
|
|
529
544
|
agentConfig: {
|
|
530
|
-
systemPrompt, skills, roles, model, resolveModel,
|
|
545
|
+
systemPrompt, skills, roles, model: undefined, resolveModel,
|
|
531
546
|
},
|
|
532
547
|
createDefaultEnv,
|
|
533
548
|
createLocalEnv,
|
|
@@ -597,19 +612,16 @@ async function handleAgentRequest(request, doInstance, agentName, handler) {
|
|
|
597
612
|
// Agent id is the DO "room name" set by routeAgentRequest
|
|
598
613
|
const id = doInstance.name;
|
|
599
614
|
|
|
600
|
-
// Parse payload
|
|
601
|
-
let payload;
|
|
602
615
|
try {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
616
|
+
// Parse the request body. Throws on invalid Content-Type or malformed
|
|
617
|
+
// JSON; returns {} for genuinely empty bodies (so no-payload agents
|
|
618
|
+
// still work).
|
|
619
|
+
const payload = await parseJsonBody(request);
|
|
607
620
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
621
|
+
const accept = request.headers.get('accept') || '';
|
|
622
|
+
const isWebhook = request.headers.get('x-webhook') === 'true';
|
|
623
|
+
const isSSE = accept.includes('text/event-stream') && !isWebhook;
|
|
611
624
|
|
|
612
|
-
try {
|
|
613
625
|
// Fire-and-forget (webhook mode)
|
|
614
626
|
if (isWebhook) {
|
|
615
627
|
const requestId = crypto.randomUUID();
|
|
@@ -628,7 +640,13 @@ async function handleAgentRequest(request, doInstance, agentName, handler) {
|
|
|
628
640
|
});
|
|
629
641
|
}
|
|
630
642
|
|
|
631
|
-
// SSE streaming mode
|
|
643
|
+
// SSE streaming mode. Two error regimes meet here:
|
|
644
|
+
// - Pre-stream errors (body parsing, etc.) have already thrown above
|
|
645
|
+
// and are caught by the outer try/catch — rendered as plain HTTP
|
|
646
|
+
// responses by toHttpResponse, since headers haven't been sent yet.
|
|
647
|
+
// - Errors during agent execution surface as in-stream \`error\`
|
|
648
|
+
// events with the canonical envelope (via toSseData), since by
|
|
649
|
+
// then the 200 + text/event-stream headers are already on the wire.
|
|
632
650
|
if (isSSE) {
|
|
633
651
|
const { readable, writable } = new TransformStream();
|
|
634
652
|
const writer = writable.getWriter();
|
|
@@ -640,7 +658,7 @@ async function handleAgentRequest(request, doInstance, agentName, handler) {
|
|
|
640
658
|
const lines = [];
|
|
641
659
|
if (event) lines.push('event: ' + event);
|
|
642
660
|
lines.push('id: ' + eventId++);
|
|
643
|
-
lines.push('data: ' + JSON.stringify(data));
|
|
661
|
+
lines.push('data: ' + (typeof data === 'string' ? data : JSON.stringify(data)));
|
|
644
662
|
lines.push('', '');
|
|
645
663
|
await writer.write(encoder.encode(lines.join('\\n')));
|
|
646
664
|
};
|
|
@@ -662,10 +680,7 @@ async function handleAgentRequest(request, doInstance, agentName, handler) {
|
|
|
662
680
|
'result',
|
|
663
681
|
);
|
|
664
682
|
} catch (err) {
|
|
665
|
-
await writeSSE(
|
|
666
|
-
{ type: 'error', error: String(err) },
|
|
667
|
-
'error',
|
|
668
|
-
);
|
|
683
|
+
await writeSSE(toSseData(err), 'error');
|
|
669
684
|
if (!isIdle) {
|
|
670
685
|
await writeSSE({ type: 'idle' }, 'idle');
|
|
671
686
|
}
|
|
@@ -696,11 +711,10 @@ async function handleAgentRequest(request, doInstance, agentName, handler) {
|
|
|
696
711
|
ctx.setEventCallback(undefined);
|
|
697
712
|
}
|
|
698
713
|
} catch (err) {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
);
|
|
714
|
+
// toHttpResponse logs unknowns via flueLog.error — no extra console.error
|
|
715
|
+
// needed here. The agentName tag is captured in the wrapped error's
|
|
716
|
+
// server-side log line via flueLog's prefix.
|
|
717
|
+
return toHttpResponse(err);
|
|
704
718
|
}
|
|
705
719
|
}
|
|
706
720
|
|
|
@@ -710,38 +724,77 @@ ${agentClasses}
|
|
|
710
724
|
|
|
711
725
|
// ─── User-declared Sandbox re-exports ──────────────────────────────────────
|
|
712
726
|
// One line per DO binding in the user's wrangler.jsonc whose class_name
|
|
713
|
-
//
|
|
727
|
+
// ends with "Sandbox". Flue aliases the single \`Sandbox\` class shipped by
|
|
714
728
|
// \`@cloudflare/sandbox\` so each user-chosen class_name resolves at the
|
|
715
729
|
// bundle's top level. The binding + container image configuration is owned
|
|
716
730
|
// by the user's wrangler.jsonc.
|
|
717
|
-
${
|
|
731
|
+
${sandboxReExports}
|
|
718
732
|
|
|
719
733
|
// ─── Worker Fetch Handler ───────────────────────────────────────────────────
|
|
720
734
|
|
|
721
735
|
export default {
|
|
722
736
|
async fetch(request, env) {
|
|
723
|
-
|
|
737
|
+
try {
|
|
738
|
+
const url = new URL(request.url);
|
|
739
|
+
const method = request.method;
|
|
724
740
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
741
|
+
// Health check
|
|
742
|
+
if (url.pathname === '/health') {
|
|
743
|
+
return new Response(JSON.stringify({ status: 'ok' }), {
|
|
744
|
+
headers: { 'content-type': 'application/json' },
|
|
745
|
+
});
|
|
746
|
+
}
|
|
731
747
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
748
|
+
// Agent manifest
|
|
749
|
+
if (url.pathname === '/agents' && method === 'GET') {
|
|
750
|
+
return new Response(JSON.stringify(manifest), {
|
|
751
|
+
headers: { 'content-type': 'application/json' },
|
|
752
|
+
});
|
|
753
|
+
}
|
|
738
754
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
755
|
+
// Webhook agent route: /agents/<name>/<id>
|
|
756
|
+
//
|
|
757
|
+
// We pre-check method and agent registration here, BEFORE delegating to
|
|
758
|
+
// routeAgentRequest. Without this, partyserver (the transitive
|
|
759
|
+
// dispatcher behind routeAgentRequest) returns a text/plain
|
|
760
|
+
// "Invalid request" 400 for unknown agent namespaces — visibly
|
|
761
|
+
// inconsistent with the rest of the API and with the Node target.
|
|
762
|
+
// Pre-routing means every error path in the agent route flows through
|
|
763
|
+
// the same JSON envelope.
|
|
764
|
+
const agentRouteMatch = url.pathname.match(/^\\/agents\\/([^/]+)\\/([^/]+)\\/?$/);
|
|
765
|
+
if (agentRouteMatch) {
|
|
766
|
+
if (method !== 'POST') {
|
|
767
|
+
throw new MethodNotAllowedError({ method, allowed: ['POST'] });
|
|
768
|
+
}
|
|
769
|
+
const name = decodeURIComponent(agentRouteMatch[1]);
|
|
770
|
+
const id = decodeURIComponent(agentRouteMatch[2]);
|
|
771
|
+
if (name.trim() === '' || id.trim() === '') {
|
|
772
|
+
throw new InvalidRequestError({
|
|
773
|
+
reason: 'Webhook URLs must have the shape /agents/<name>/<id> with non-empty segments.',
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
if (!webhookAgentNames.has(name)) {
|
|
777
|
+
throw new AgentNotFoundError({
|
|
778
|
+
name,
|
|
779
|
+
available: Array.from(webhookAgentNames),
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
// All gating passed. Delegate to the Agents SDK / partyserver, which
|
|
783
|
+
// dispatches into the per-agent DO's onRequest → handleAgentRequest.
|
|
784
|
+
// routeAgentRequest may still return null for shape mismatches we
|
|
785
|
+
// didn't anticipate; treat that as a route_not_found with a hint.
|
|
786
|
+
const response = await routeAgentRequest(request, env);
|
|
787
|
+
if (response) return response;
|
|
788
|
+
throw new RouteNotFoundError({ method, path: url.pathname });
|
|
789
|
+
}
|
|
743
790
|
|
|
744
|
-
|
|
791
|
+
// Anything else: canonical 404 envelope.
|
|
792
|
+
throw new RouteNotFoundError({ method, path: url.pathname });
|
|
793
|
+
} catch (err) {
|
|
794
|
+
// toHttpResponse logs unknowns via flueLog.error — no extra
|
|
795
|
+
// console.error needed at this layer.
|
|
796
|
+
return toHttpResponse(err);
|
|
797
|
+
}
|
|
745
798
|
},
|
|
746
799
|
};
|
|
747
800
|
`;
|
|
@@ -766,7 +819,7 @@ export default {
|
|
|
766
819
|
const sandboxClassNames = detectSandboxBindings(userConfig);
|
|
767
820
|
if (sandboxClassNames.length > 0) {
|
|
768
821
|
assertSandboxPackageInstalled(sandboxClassNames, [ctx.outputDir, ctx.workspaceDir]);
|
|
769
|
-
for (const className of sandboxClassNames) console.log(`[flue]
|
|
822
|
+
for (const className of sandboxClassNames) console.log(`[flue] Auto-wiring DO binding "${className}" to @cloudflare/sandbox's Sandbox class.`);
|
|
770
823
|
}
|
|
771
824
|
const merged = mergeFlueAdditions(userConfig, additions);
|
|
772
825
|
stripNoisyWranglerDefaults(merged);
|
|
@@ -810,6 +863,11 @@ import {
|
|
|
810
863
|
InMemorySessionStore,
|
|
811
864
|
bashFactoryToSessionEnv,
|
|
812
865
|
resolveModel,
|
|
866
|
+
parseJsonBody,
|
|
867
|
+
validateAgentRequest,
|
|
868
|
+
toHttpResponse,
|
|
869
|
+
toSseData,
|
|
870
|
+
RouteNotFoundError,
|
|
813
871
|
} from '@flue/sdk/internal';
|
|
814
872
|
import { randomUUID } from 'node:crypto';
|
|
815
873
|
|
|
@@ -827,7 +885,10 @@ const handlers = {
|
|
|
827
885
|
${agents.map((a) => ` ${JSON.stringify(a.name)}: ${agentVarName(a.name)},`).join("\n")}
|
|
828
886
|
};
|
|
829
887
|
|
|
830
|
-
|
|
888
|
+
// Set of webhook-accessible agent names. Named distinctly from the
|
|
889
|
+
// validateAgentRequest \`webhookAgents\` parameter to keep the call site
|
|
890
|
+
// below readable.
|
|
891
|
+
const webhookAgentSet = new Set(${JSON.stringify(webhookAgents.map((a) => a.name))});
|
|
831
892
|
|
|
832
893
|
// When the CLI starts this server via \`flue run\`, it sets FLUE_MODE=local.
|
|
833
894
|
// In local mode the HTTP route accepts any registered agent (including
|
|
@@ -841,13 +902,6 @@ const manifest = ${JSON.stringify({ agents: agents.map((a) => ({
|
|
|
841
902
|
triggers: a.triggers
|
|
842
903
|
})) }, null, 2)};
|
|
843
904
|
|
|
844
|
-
// ─── Infrastructure ─────────────────────────────────────────────────────────
|
|
845
|
-
|
|
846
|
-
// No build-time model default. The user sets model at runtime via
|
|
847
|
-
// \`init({ model: "provider/model-id" })\` for an agent default, or via
|
|
848
|
-
// \`{ model: "provider/model-id" }\` on any individual prompt/skill/task call.
|
|
849
|
-
const model = undefined;
|
|
850
|
-
|
|
851
905
|
// ─── Sandbox Environments ───────────────────────────────────────────────────
|
|
852
906
|
|
|
853
907
|
/**
|
|
@@ -887,7 +941,7 @@ function createContextForRequest(id, payload) {
|
|
|
887
941
|
payload,
|
|
888
942
|
env: process.env,
|
|
889
943
|
agentConfig: {
|
|
890
|
-
systemPrompt, skills, roles, model, resolveModel,
|
|
944
|
+
systemPrompt, skills, roles, model: undefined, resolveModel,
|
|
891
945
|
},
|
|
892
946
|
createDefaultEnv,
|
|
893
947
|
createLocalEnv,
|
|
@@ -902,31 +956,29 @@ const app = new Hono();
|
|
|
902
956
|
app.get('/health', (c) => c.json({ status: 'ok' }));
|
|
903
957
|
app.get('/agents', (c) => c.json(manifest));
|
|
904
958
|
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}, 400);
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
app.post('/agents/:name/:id', async (c) => {
|
|
959
|
+
// Catch any method on the agent route so non-POSTs become 405 (instead of
|
|
960
|
+
// Hono's default 404 for unmatched method). Throws are translated by the
|
|
961
|
+
// onError handler into the canonical error envelope.
|
|
962
|
+
app.all('/agents/:name/:id', async (c) => {
|
|
913
963
|
const name = c.req.param('name');
|
|
914
964
|
const id = c.req.param('id');
|
|
915
965
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
966
|
+
// Validate method, name shape, registration, webhook-accessibility.
|
|
967
|
+
// Throws FlueHttpError on any failure; caught by app.onError below.
|
|
968
|
+
validateAgentRequest({
|
|
969
|
+
method: c.req.method,
|
|
970
|
+
name,
|
|
971
|
+
id,
|
|
972
|
+
registeredAgents: Object.keys(handlers),
|
|
973
|
+
webhookAgents: Array.from(webhookAgentSet),
|
|
974
|
+
allowNonWebhook: isLocalMode,
|
|
975
|
+
});
|
|
922
976
|
|
|
923
977
|
const handler = handlers[name];
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
payload = {};
|
|
929
|
-
}
|
|
978
|
+
|
|
979
|
+
// Parse the request body. Throws on invalid Content-Type or malformed JSON;
|
|
980
|
+
// returns {} for genuinely empty bodies (so no-payload agents still work).
|
|
981
|
+
const payload = await parseJsonBody(c.req.raw);
|
|
930
982
|
|
|
931
983
|
const accept = c.req.header('accept') || '';
|
|
932
984
|
const isWebhook = c.req.header('x-webhook') === 'true';
|
|
@@ -949,7 +1001,13 @@ app.post('/agents/:name/:id', async (c) => {
|
|
|
949
1001
|
return c.json({ status: 'accepted', requestId }, 202);
|
|
950
1002
|
}
|
|
951
1003
|
|
|
952
|
-
// SSE streaming mode
|
|
1004
|
+
// SSE streaming mode. Two error regimes meet here:
|
|
1005
|
+
// - Pre-stream errors (validation, body parsing, agent lookup) have
|
|
1006
|
+
// already thrown above and are rendered as plain HTTP responses by
|
|
1007
|
+
// app.onError — headers haven't been sent yet, so this works.
|
|
1008
|
+
// - Errors during agent execution surface as in-stream \`error\` events
|
|
1009
|
+
// with the canonical envelope (via toSseData), since by then the
|
|
1010
|
+
// 200 + text/event-stream headers are already on the wire.
|
|
953
1011
|
if (isSSE) {
|
|
954
1012
|
return streamSSE(c, async (stream) => {
|
|
955
1013
|
let eventId = 0;
|
|
@@ -973,7 +1031,7 @@ app.post('/agents/:name/:id', async (c) => {
|
|
|
973
1031
|
});
|
|
974
1032
|
} catch (err) {
|
|
975
1033
|
await stream.writeSSE({
|
|
976
|
-
data:
|
|
1034
|
+
data: toSseData(err),
|
|
977
1035
|
event: 'error',
|
|
978
1036
|
id: String(eventId++),
|
|
979
1037
|
});
|
|
@@ -987,18 +1045,29 @@ app.post('/agents/:name/:id', async (c) => {
|
|
|
987
1045
|
});
|
|
988
1046
|
}
|
|
989
1047
|
|
|
990
|
-
// Sync mode (default)
|
|
1048
|
+
// Sync mode (default). Errors propagate to app.onError.
|
|
1049
|
+
const ctx = createContextForRequest(id, payload);
|
|
991
1050
|
try {
|
|
992
|
-
const ctx = createContextForRequest(id, payload);
|
|
993
1051
|
const result = await handler(ctx);
|
|
994
|
-
ctx.setEventCallback(undefined);
|
|
995
1052
|
return c.json({ result: result !== undefined ? result : null });
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
return c.json({ error: String(err) }, 500);
|
|
1053
|
+
} finally {
|
|
1054
|
+
ctx.setEventCallback(undefined);
|
|
999
1055
|
}
|
|
1000
1056
|
});
|
|
1001
1057
|
|
|
1058
|
+
// 404 handler — fires for any URL that didn't match a registered route.
|
|
1059
|
+
app.notFound((c) => {
|
|
1060
|
+
// Throw rather than return so the onError handler is the single source of
|
|
1061
|
+
// truth for error-envelope shaping.
|
|
1062
|
+
throw new RouteNotFoundError({ method: c.req.method, path: new URL(c.req.url).pathname });
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
// Single-source-of-truth error renderer. Every thrown FlueError (and every
|
|
1066
|
+
// thrown unknown) is converted to the canonical JSON envelope here.
|
|
1067
|
+
// toHttpResponse takes care of logging unknowns — no extra console.error
|
|
1068
|
+
// needed at this layer.
|
|
1069
|
+
app.onError((err) => toHttpResponse(err));
|
|
1070
|
+
|
|
1002
1071
|
// ─── Start ──────────────────────────────────────────────────────────────────
|
|
1003
1072
|
|
|
1004
1073
|
const port = parseInt(process.env.PORT || '3000', 10);
|
|
@@ -1594,6 +1663,29 @@ var CloudflareReloader = class {
|
|
|
1594
1663
|
port;
|
|
1595
1664
|
configPath;
|
|
1596
1665
|
envFiles;
|
|
1666
|
+
/**
|
|
1667
|
+
* Stable container build ID for the lifetime of this reloader instance.
|
|
1668
|
+
*
|
|
1669
|
+
* `unstable_startWorker` does NOT default this field — only wrangler's CLI
|
|
1670
|
+
* path does, via `generateContainerBuildId()`. When the user's wrangler
|
|
1671
|
+
* config declares `containers[]` (e.g. via `@cloudflare/sandbox`), the
|
|
1672
|
+
* first `onBundleComplete` calls `getImageNameFromDOClassName(...)` which
|
|
1673
|
+
* asserts that `options.containerBuildId` is set; without this, the
|
|
1674
|
+
* assertion throws, the `ProxyController` never gets `reloadComplete`,
|
|
1675
|
+
* and every request hangs (including `/health`). See issue #22.
|
|
1676
|
+
*
|
|
1677
|
+
* We generate it once per reloader and reuse it across reloads so that
|
|
1678
|
+
* wrangler's container-prep cache hits when nothing about the image
|
|
1679
|
+
* changed. Format matches wrangler's own helper: an 8-char UUID slice.
|
|
1680
|
+
*/
|
|
1681
|
+
containerBuildId;
|
|
1682
|
+
/**
|
|
1683
|
+
* Bound listener for `DevEnv` `'error'` events. Stored so we can detach
|
|
1684
|
+
* it on `disposeWorker()` — the underlying `EventEmitter` outlives the
|
|
1685
|
+
* worker handle, so if the listener stayed attached we'd leak (and
|
|
1686
|
+
* double-fire) across reloads.
|
|
1687
|
+
*/
|
|
1688
|
+
errorListener = null;
|
|
1597
1689
|
url;
|
|
1598
1690
|
constructor(wrangler, opts) {
|
|
1599
1691
|
this.wrangler = wrangler;
|
|
@@ -1601,6 +1693,7 @@ var CloudflareReloader = class {
|
|
|
1601
1693
|
this.port = opts.port;
|
|
1602
1694
|
this.envFiles = opts.envFiles;
|
|
1603
1695
|
this.configPath = path.join(this.outputDir, "dist", "wrangler.jsonc");
|
|
1696
|
+
this.containerBuildId = randomUUID().slice(0, 8);
|
|
1604
1697
|
}
|
|
1605
1698
|
async start() {
|
|
1606
1699
|
await this.startWorker();
|
|
@@ -1659,9 +1752,18 @@ var CloudflareReloader = class {
|
|
|
1659
1752
|
port: this.port
|
|
1660
1753
|
},
|
|
1661
1754
|
watch: false,
|
|
1662
|
-
logLevel: "info"
|
|
1755
|
+
logLevel: "info",
|
|
1756
|
+
containerBuildId: this.containerBuildId
|
|
1663
1757
|
}
|
|
1664
1758
|
});
|
|
1759
|
+
this.errorListener = (event) => {
|
|
1760
|
+
const reason = event?.reason ?? "unknown error";
|
|
1761
|
+
const cause = event?.cause;
|
|
1762
|
+
const causeMsg = cause && typeof cause === "object" && "message" in cause ? cause.message : void 0;
|
|
1763
|
+
console.error(`[flue] Wrangler error (${event?.source ?? "unknown"}): ${reason}`);
|
|
1764
|
+
if (causeMsg) console.error(`[flue] ${causeMsg}`);
|
|
1765
|
+
};
|
|
1766
|
+
this.worker.raw.on("error", this.errorListener);
|
|
1665
1767
|
try {
|
|
1666
1768
|
this.url = (await this.worker.url).toString().replace(/\/$/, "");
|
|
1667
1769
|
} catch {
|
|
@@ -1670,8 +1772,13 @@ var CloudflareReloader = class {
|
|
|
1670
1772
|
}
|
|
1671
1773
|
async disposeWorker() {
|
|
1672
1774
|
const worker = this.worker;
|
|
1775
|
+
const listener = this.errorListener;
|
|
1673
1776
|
this.worker = null;
|
|
1777
|
+
this.errorListener = null;
|
|
1674
1778
|
if (!worker) return;
|
|
1779
|
+
if (listener) try {
|
|
1780
|
+
worker.raw.off("error", listener);
|
|
1781
|
+
} catch {}
|
|
1675
1782
|
try {
|
|
1676
1783
|
await worker.dispose();
|
|
1677
1784
|
} catch (err) {
|