@junejs/server 1.0.0-dev.14 → 1.0.0-dev.19
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/dist/adapter.d.ts +7 -1
- package/dist/adapter.js +62 -5
- package/dist/agent-compile.d.ts +39 -0
- package/dist/agent-compile.js +145 -0
- package/dist/agent-discover.d.ts +3 -2
- package/dist/agent-discover.js +30 -44
- package/dist/agent-durable.d.ts +13 -4
- package/dist/agent-durable.js +45 -9
- package/dist/build.js +74 -9
- package/dist/client-bundle.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/tsconfig-jsx.d.ts +2 -1
- package/dist/tsconfig-jsx.js +1 -1
- package/dist/worker.d.ts +9 -4
- package/dist/worker.js +28 -9
- package/package.json +13 -3
- package/src/adapter.ts +84 -3
- package/src/agent-compile.ts +271 -0
- package/src/agent-discover.ts +71 -60
- package/src/agent-durable.ts +92 -16
- package/src/build.ts +127 -10
- package/src/index.ts +8 -0
- package/src/tsconfig-jsx.ts +2 -2
- package/src/worker.ts +66 -18
package/src/build.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { localeHref } from "@junejs/core/i18n";
|
|
|
32
32
|
import { routeFromModule, type BrandedRoute } from "@junejs/core/route";
|
|
33
33
|
import { workers, vercel, deno, staticSite, type JuneAdapter, type ResourcePlan } from "./adapter";
|
|
34
34
|
import type { DocumentConfig } from "@junejs/core/document";
|
|
35
|
+
import { generateAgentModule } from "./agent-compile";
|
|
35
36
|
import { generateContentModule } from "./content";
|
|
36
37
|
import { createWorker, type WorkerManifest } from "./worker";
|
|
37
38
|
import { findMiddlewareFile } from "./router";
|
|
@@ -441,6 +442,20 @@ export async function buildManifest(appRoot: string): Promise<WorkerManifest> {
|
|
|
441
442
|
if (typeof mod.default === "function") extra = mod.default as ExtraHandler;
|
|
442
443
|
}
|
|
443
444
|
|
|
445
|
+
// The durable agent's DO address (createWorker routes the chat endpoint by
|
|
446
|
+
// it). Set whenever an agent/ directory exists — the surface is inert without
|
|
447
|
+
// an env.AGENT binding, so in-process consumers (prerender, the parity test)
|
|
448
|
+
// are unaffected.
|
|
449
|
+
let agentName: string | undefined;
|
|
450
|
+
const agentDir = join(appDir, frozen.agent.runtime.dir);
|
|
451
|
+
if (frozen.agent.runtime.enabled && existsSync(agentDir)) {
|
|
452
|
+
const cfgFile = join(agentDir, "agent.ts");
|
|
453
|
+
const cfg = existsSync(cfgFile)
|
|
454
|
+
? ((await import(pathToFileURL(cfgFile).href)).default as { name?: string } | undefined)
|
|
455
|
+
: undefined;
|
|
456
|
+
agentName = cfg?.name ?? basename(agentDir);
|
|
457
|
+
}
|
|
458
|
+
|
|
444
459
|
return {
|
|
445
460
|
routes,
|
|
446
461
|
dynamicRoutes,
|
|
@@ -450,6 +465,7 @@ export async function buildManifest(appRoot: string): Promise<WorkerManifest> {
|
|
|
450
465
|
loadings,
|
|
451
466
|
document: frozen.document,
|
|
452
467
|
agent: frozen.agent,
|
|
468
|
+
agentName,
|
|
453
469
|
i18n: frozen.i18n,
|
|
454
470
|
earlyHints: frozen.earlyHints,
|
|
455
471
|
extra,
|
|
@@ -521,6 +537,52 @@ export async function juneBuild(
|
|
|
521
537
|
config: fullConfig,
|
|
522
538
|
});
|
|
523
539
|
|
|
540
|
+
// ---- durable agent (the F7 mount): compile app/agent → _agent.gen.ts ------
|
|
541
|
+
// The generated entry mounts the compiled module statically (fs discovery
|
|
542
|
+
// can't run on workerd) and exports the per-session Durable Object class.
|
|
543
|
+
// Only a target with Durable Objects can host the session actor — elsewhere
|
|
544
|
+
// the agent surface is skipped with a notice, and an app without an agent/
|
|
545
|
+
// directory emits a byte-identical entry (the parity guarantee).
|
|
546
|
+
const agentDir = join(appDir, frozen.agent.runtime.dir);
|
|
547
|
+
let agentModule: { file: string } | null = null;
|
|
548
|
+
if (frozen.agent.runtime.enabled) {
|
|
549
|
+
const generated = generateAgentModule(agentDir);
|
|
550
|
+
if (generated && adapter.capabilities.durableObjects) {
|
|
551
|
+
agentModule = generated;
|
|
552
|
+
} else if (generated) {
|
|
553
|
+
console.warn(
|
|
554
|
+
`[june build] app/${frozen.agent.runtime.dir}/ found, but the ${adapter.name}() target has no Durable Objects — the durable agent is not mounted.`,
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (agentModule) {
|
|
559
|
+
// The generated DO's model is Claude, and workerd has no runtime module
|
|
560
|
+
// resolution — the SDK must be a real dependency of the app so the entry's
|
|
561
|
+
// STATIC import bundles it (@junejs/core treats it as an optional peer; the
|
|
562
|
+
// adapter's own lazy import can never be bundled). Fail the build with the
|
|
563
|
+
// fix, never ship a worker whose first model turn dies on a bare import.
|
|
564
|
+
// An explicit node_modules walk, not a resolver API: the check must behave
|
|
565
|
+
// identically on every host (Bun.resolveSync missed a symlinked scoped
|
|
566
|
+
// package on Linux CI that it found on macOS), and Rolldown does its own
|
|
567
|
+
// resolution at bundle time anyway — this is a preflight, not the resolver.
|
|
568
|
+
const sdkResolvable = (fromDir: string): boolean => {
|
|
569
|
+
// Absolute first: with a relative root (juneBuild(".")), dirname(".") is
|
|
570
|
+
// still "." and the walk would stop before reaching a hoisted parent.
|
|
571
|
+
for (let d = resolve(fromDir); ; ) {
|
|
572
|
+
if (existsSync(join(d, "node_modules", "@anthropic-ai", "sdk", "package.json"))) return true;
|
|
573
|
+
const parent = dirname(d);
|
|
574
|
+
if (parent === d) return false;
|
|
575
|
+
d = parent;
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
if (!sdkResolvable(appRoot)) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
`app/${frozen.agent.runtime.dir}/ mounts a durable agent whose model is Claude — ` +
|
|
581
|
+
`add the SDK to the app so it bundles for workerd: bun add @anthropic-ai/sdk`,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
524
586
|
// Compile the global stylesheet ONCE and content-hash it: the built worker and
|
|
525
587
|
// prerendered HTML link `/global.<hash>.css`, served immutable (cache forever,
|
|
526
588
|
// a content change ships a new URL → no revalidation, no stale window). Dev
|
|
@@ -691,11 +753,15 @@ export async function juneBuild(
|
|
|
691
753
|
} else if (hasResources) {
|
|
692
754
|
imports.push(`import { bindWorkerResources } from "@junejs/server/resources";`);
|
|
693
755
|
}
|
|
694
|
-
|
|
695
|
-
|
|
756
|
+
// One expression, TWO instances: the pipeline (createWorker) and the durable
|
|
757
|
+
// agent's DO each open their own provider — the DO is a separate isolate whose
|
|
758
|
+
// env must not be captured by the worker-side memoization.
|
|
759
|
+
const resourcesExpr = tursoDb
|
|
760
|
+
? `memoizeResources({ db: turso() })`
|
|
696
761
|
: hasResources
|
|
697
|
-
?
|
|
698
|
-
:
|
|
762
|
+
? `bindWorkerResources(${JSON.stringify(resourceFlags)})`
|
|
763
|
+
: null;
|
|
764
|
+
const resourcesField = resourcesExpr ? `\n resources: ${resourcesExpr},` : "";
|
|
699
765
|
|
|
700
766
|
// Opt-in Tier-3 data layer: import its installDataLayer from the declared module
|
|
701
767
|
// and call it at worker boot — the prod twin of the dev host's dataLayer.install()
|
|
@@ -723,10 +789,52 @@ export async function juneBuild(
|
|
|
723
789
|
}
|
|
724
790
|
const servicesField = servicesModule ? `\n services: __appServices,` : "";
|
|
725
791
|
|
|
792
|
+
// Durable agent: import the compiled agent module + the DO shell pieces, set
|
|
793
|
+
// agentName on the manifest (what activates createWorker's chat routing), and
|
|
794
|
+
// export the DO class the adapter's wrangler binding names. cloudflare:workers
|
|
795
|
+
// is a workerd runtime module — external to the bundle (see the predicate below).
|
|
796
|
+
if (agentModule) {
|
|
797
|
+
imports.push(`import __agentModule from ${JSON.stringify(importPath(genDir, agentModule.file))};`);
|
|
798
|
+
imports.push(`import { assembleDurable } from "@junejs/core/agent-config";`);
|
|
799
|
+
imports.push(`import { anthropic } from "@junejs/core/agent-models";`);
|
|
800
|
+
imports.push(`import { AgentDurableObject } from "@junejs/server/agent-durable";`);
|
|
801
|
+
imports.push(`import { DurableObject } from "cloudflare:workers";`);
|
|
802
|
+
// STATIC so Rolldown bundles the SDK (the adapter's own import is a
|
|
803
|
+
// non-literal lazy specifier — deliberately unbundleable); injected via
|
|
804
|
+
// anthropic({ client }), which skips that lazy path entirely.
|
|
805
|
+
imports.push(`import Anthropic from "@anthropic-ai/sdk";`);
|
|
806
|
+
}
|
|
807
|
+
// Assembled BEFORE createWorker: the manifest consumes the channels (worker-
|
|
808
|
+
// side webhook routing) and the DO class consumes the full definition.
|
|
809
|
+
const agentPreamble = agentModule ? `\nconst __agentDef = assembleDurable(__agentModule);\n` : "";
|
|
810
|
+
const agentNameField = agentModule ? `\n agentName: __agentModule.config.name,\n agentChannels: __agentDef.channels,` : "";
|
|
811
|
+
const doClass = agentModule
|
|
812
|
+
? `
|
|
813
|
+
// The durable agent: one Durable Object per session, the compiled agent/
|
|
814
|
+
// directory as its definition — adapted tools (+ read_skill when skills exist),
|
|
815
|
+
// the assembled system prompt, channel factories resolved with the DO's own
|
|
816
|
+
// env, and the app's declared resources opened from THIS isolate's env (lazily,
|
|
817
|
+
// at the first turn — the same ambient db a tool sees in native dev). Model:
|
|
818
|
+
// Anthropic over a statically bundled client (workerd cannot resolve a bare
|
|
819
|
+
// import at runtime), keyed by the ANTHROPIC_API_KEY secret — a missing secret
|
|
820
|
+
// surfaces as the SDK's own clear construction error on the first agent request.
|
|
821
|
+
export class JuneAgentDO extends DurableObject {
|
|
822
|
+
#agent = new AgentDurableObject(this.ctx, {
|
|
823
|
+
...__agentDef,
|
|
824
|
+
model: anthropic({ model: __agentModule.config.model, client: new Anthropic({ apiKey: (this.env as { ANTHROPIC_API_KEY?: string }).ANTHROPIC_API_KEY }) }),
|
|
825
|
+
env: this.env,${resourcesExpr ? `\n resources: ${resourcesExpr},` : ""}${servicesModule ? "\n services: __appServices(this.env)," : ""}
|
|
826
|
+
});
|
|
827
|
+
fetch(req: Request): Promise<Response> {
|
|
828
|
+
return this.#agent.fetch(req);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
`
|
|
832
|
+
: "";
|
|
833
|
+
|
|
726
834
|
const entry = `// AUTO-GENERATED by \`june build\` — do not edit. Regenerate: june build .
|
|
727
835
|
${adapterEntry.imports.join("\n")}
|
|
728
836
|
${imports.join("\n")}
|
|
729
|
-
${dataLayerBoot}
|
|
837
|
+
${dataLayerBoot}${agentPreamble}
|
|
730
838
|
const pipeline = createWorker({
|
|
731
839
|
routes: {
|
|
732
840
|
${statics.join("\n")}
|
|
@@ -741,12 +849,12 @@ ${chains.join("\n")}
|
|
|
741
849
|
${loadings.join("\n")}
|
|
742
850
|
},
|
|
743
851
|
document: ${JSON.stringify(frozen.document, null, 2).replace(/\n/g, "\n ")},
|
|
744
|
-
agent: ${JSON.stringify(frozen.agent)},${frozen.i18n ? `\n i18n: ${JSON.stringify(frozen.i18n)},` : ""}
|
|
852
|
+
agent: ${JSON.stringify(frozen.agent)},${agentNameField}${frozen.i18n ? `\n i18n: ${JSON.stringify(frozen.i18n)},` : ""}
|
|
745
853
|
earlyHints: ${JSON.stringify(frozen.earlyHints)},${builtExtraFile ? "\n extra," : ""}${resourcesField}${servicesField}
|
|
746
854
|
});
|
|
747
855
|
|
|
748
856
|
${adapterEntry.wrap("pipeline")}
|
|
749
|
-
`;
|
|
857
|
+
${doClass}`;
|
|
750
858
|
const entryFile = join(genDir, "worker-entry.tsx");
|
|
751
859
|
await writeFile(entryFile, entry);
|
|
752
860
|
|
|
@@ -773,6 +881,8 @@ ${adapterEntry.wrap("pipeline")}
|
|
|
773
881
|
external: (id: string) => {
|
|
774
882
|
// Bun built-ins exist only at Bun runtime, never in the workerd graph (see isBunSpecifier).
|
|
775
883
|
if (isBunSpecifier(id)) return true;
|
|
884
|
+
// workerd runtime modules (the durable-agent entry imports cloudflare:workers).
|
|
885
|
+
if (id.startsWith("cloudflare:")) return true;
|
|
776
886
|
// Binary assets stay external — wrangler's CompiledWasm/Data rules own them.
|
|
777
887
|
if (/\.(wasm|ttf|otf|woff2?|png|jpe?g|avif|webp)$/.test(id)) return true;
|
|
778
888
|
// Merge: adapter.buildExternal (target-specific, e.g. workers-og for the
|
|
@@ -960,16 +1070,23 @@ ${adapterEntry.wrap("pipeline")}
|
|
|
960
1070
|
const defaultName = workerName(pkgName ?? basename(appRoot));
|
|
961
1071
|
// A declared D1-backed db (sqlite/d1) → a D1 binding named DB (the name
|
|
962
1072
|
// bindWorkerResources reads). turso() connects from env, not a binding — no plan.
|
|
963
|
-
const plan: ResourcePlan =
|
|
964
|
-
resourcesCfg?.db && resourcesCfg.db.kind !== "turso"
|
|
1073
|
+
const plan: ResourcePlan = {
|
|
1074
|
+
...(resourcesCfg?.db && resourcesCfg.db.kind !== "turso"
|
|
965
1075
|
? { db: { binding: "DB", databaseName: `${defaultName}-db` } }
|
|
966
|
-
: {}
|
|
1076
|
+
: {}),
|
|
1077
|
+
// The durable agent's DO binding — the name createWorker reads (env.AGENT)
|
|
1078
|
+
// and the class the generated entry exports.
|
|
1079
|
+
...(agentModule ? { agent: { binding: "AGENT", className: "JuneAgentDO" } } : {}),
|
|
1080
|
+
};
|
|
967
1081
|
await adapter.emit({ appRoot, outDir, hasAssets, linkHeader, config: fullConfig, plan, defaultName, publicFiles });
|
|
968
1082
|
if (plan.db) {
|
|
969
1083
|
console.log(
|
|
970
1084
|
` ↳ d1 binding "${plan.db.binding}" emitted — set database_id in wrangler.jsonc (wrangler d1 create ${plan.db.databaseName})`,
|
|
971
1085
|
);
|
|
972
1086
|
}
|
|
1087
|
+
if (plan.agent) {
|
|
1088
|
+
console.log(` ↳ durable agent mounted (${plan.agent.className} → env.${plan.agent.binding}; chat at ${frozen.agent.runtime.chat.path})`);
|
|
1089
|
+
}
|
|
973
1090
|
|
|
974
1091
|
return {
|
|
975
1092
|
outFile,
|
package/src/index.ts
CHANGED
|
@@ -91,6 +91,14 @@ export {
|
|
|
91
91
|
freezeConfig,
|
|
92
92
|
type BuildResult,
|
|
93
93
|
} from "./build";
|
|
94
|
+
export {
|
|
95
|
+
generateAgentModule,
|
|
96
|
+
findAgentDir,
|
|
97
|
+
scanAgentDir,
|
|
98
|
+
emitAgentModule,
|
|
99
|
+
AGENT_MODULE_FILE,
|
|
100
|
+
type AgentDirScan,
|
|
101
|
+
} from "./agent-compile";
|
|
94
102
|
export { juneDeploy, type DeployResult } from "./deploy";
|
|
95
103
|
export { migrateD1, wranglerD1, resolveD1Database, type D1Exec } from "./d1-migrate";
|
|
96
104
|
export { startDevServer, type DevServer, type DevServerOptions } from "./dev";
|
package/src/tsconfig-jsx.ts
CHANGED
|
@@ -9,8 +9,8 @@ import { join } from "node:path";
|
|
|
9
9
|
// tsconfig.json is JSONC by convention (comments + trailing commas are idiomatic), and a strict
|
|
10
10
|
// JSON.parse failure here silently degrades to "not declared" → the conflict warning reappears.
|
|
11
11
|
// Strip comments (`//` only at line-start/after-whitespace so "https://…" in strings survives)
|
|
12
|
-
// and trailing commas before parsing.
|
|
13
|
-
const stripJsonc = (s: string): string =>
|
|
12
|
+
// and trailing commas before parsing. Shared: agent-compile's tsconfig sniff parses the same way.
|
|
13
|
+
export const stripJsonc = (s: string): string =>
|
|
14
14
|
s
|
|
15
15
|
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
16
16
|
.replace(/(^|\s)\/\/.*$/gm, "$1")
|
package/src/worker.ts
CHANGED
|
@@ -17,10 +17,19 @@ import type { DocumentConfig } from "@junejs/core/document";
|
|
|
17
17
|
import type { I18nConfig } from "@junejs/core/i18n";
|
|
18
18
|
import type { Resources } from "@junejs/core/resources";
|
|
19
19
|
|
|
20
|
+
import type { Channel, ChannelFactory } from "@junejs/core/agent-config";
|
|
21
|
+
|
|
20
22
|
import { createPipeline, type ExtraHandler, type LayoutComponent, type LoadingComponent, type Resolved, type ResolvedResource, type ResourceHandler } from "./pipeline";
|
|
21
|
-
import { durableAgentSurface, type DurableObjectNamespace } from "./agent-durable";
|
|
23
|
+
import { durableAgentSurface, durableChannelSurface, type DurableObjectNamespace } from "./agent-durable";
|
|
22
24
|
import { contentTypeFor, RESERVED_PREFIX, safeRelativePath } from "./static-files";
|
|
23
25
|
|
|
26
|
+
// The slice of workerd's ExecutionContext the worker threads through: webhook
|
|
27
|
+
// channels ACK fast and finish their work (post the reply out-of-band) on
|
|
28
|
+
// waitUntil — without it a floating promise can be killed when the isolate is
|
|
29
|
+
// reclaimed. Optional end to end; a host without it (tests, other targets)
|
|
30
|
+
// simply doesn't extend the invocation.
|
|
31
|
+
export type WorkerExecutionContext = { waitUntil?(p: Promise<unknown>): void };
|
|
32
|
+
|
|
24
33
|
export type WorkerManifest = {
|
|
25
34
|
// Static paths → route definitions ("/", "/users", ...).
|
|
26
35
|
routes: Record<string, BrandedRoute>;
|
|
@@ -45,6 +54,13 @@ export type WorkerManifest = {
|
|
|
45
54
|
// by the build when an agent/ directory exists; absent → no durable agent on
|
|
46
55
|
// the edge. The DO namespace binding is `env.AGENT`.
|
|
47
56
|
agentName?: string;
|
|
57
|
+
// The agent's channels (Channel or `(env) => Channel` factories, from the
|
|
58
|
+
// compiled agent module) — mounted as webhook routes on the worker via
|
|
59
|
+
// durableChannelSurface when agent.runtime.channels is on. The SAME factories
|
|
60
|
+
// are also wired into the DO (DoAgentDef.channels) by the generated entry, so
|
|
61
|
+
// capability tools and host-side delivery work; this field is the WORKER half
|
|
62
|
+
// (verify the webhook, run the turn on the DO, reply on waitUntil).
|
|
63
|
+
agentChannels?: (Channel | ChannelFactory)[];
|
|
48
64
|
// Locale routing config, frozen from june.config.ts `i18n`. The locales table
|
|
49
65
|
// is plain data; a `resolveLocale` hook survives the in-process freeze (parity
|
|
50
66
|
// test) but not JSON codegen — worker hook support lands with the codegen pass.
|
|
@@ -111,7 +127,7 @@ function compilePattern(pattern: string): { regex: RegExp; names: string[] } {
|
|
|
111
127
|
|
|
112
128
|
export function createWorker(
|
|
113
129
|
manifest: WorkerManifest,
|
|
114
|
-
): { fetch(request: Request, env?: unknown): Promise<Response> } {
|
|
130
|
+
): { fetch(request: Request, env?: unknown, ctx?: WorkerExecutionContext): Promise<Response> } {
|
|
115
131
|
const dynamic: Compiled[] = (manifest.dynamicRoutes ?? []).map((d) => ({
|
|
116
132
|
...compilePattern(d.pattern),
|
|
117
133
|
def: d.def,
|
|
@@ -134,8 +150,14 @@ export function createWorker(
|
|
|
134
150
|
|
|
135
151
|
// The worker's env (D1/KV/R2 bindings) arrives per fetch and is stable across
|
|
136
152
|
// requests in an isolate; we capture the latest and hand it to the env-aware
|
|
137
|
-
// provider, which memoizes the opened handles on first call.
|
|
153
|
+
// provider, which memoizes the opened handles on first call. That "latest"
|
|
154
|
+
// pattern is safe ONLY for per-isolate-stable values — concurrent fetches
|
|
155
|
+
// interleave at await points, so anything per-INVOCATION (the execution
|
|
156
|
+
// context, and env used at a specific moment) must come from request-scoped
|
|
157
|
+
// storage instead: `requestLocals`, keyed by the Request identity that flows
|
|
158
|
+
// unchanged from fetch() through the pipeline to the agent surface.
|
|
138
159
|
let currentEnv: unknown;
|
|
160
|
+
const requestLocals = new WeakMap<Request, { env: unknown; ctx?: WorkerExecutionContext }>();
|
|
139
161
|
const provider = manifest.resources;
|
|
140
162
|
|
|
141
163
|
// App services: the build sets a raw `(env) => services` factory here; memoize it per
|
|
@@ -144,18 +166,43 @@ export function createWorker(
|
|
|
144
166
|
const servicesProvider = manifest.services;
|
|
145
167
|
let servicesCache: { v: unknown } | undefined;
|
|
146
168
|
|
|
147
|
-
// Durable agent (edge): route the chat endpoint
|
|
148
|
-
// env.AGENT. Built only when the app
|
|
149
|
-
// runtime is enabled; the surface is
|
|
150
|
-
// is present, so a worker without
|
|
169
|
+
// Durable agent (edge): route the chat endpoint — and the agent's channel
|
|
170
|
+
// webhooks — to the per-session DO bound at env.AGENT. Built only when the app
|
|
171
|
+
// has an agent (manifest.agentName) and the runtime is enabled; the surface is
|
|
172
|
+
// inert (null → fall through) until env.AGENT is present, so a worker without
|
|
173
|
+
// the DO binding is unaffected.
|
|
151
174
|
const agentSurface =
|
|
152
175
|
manifest.agent.runtime.enabled && manifest.agentName
|
|
153
176
|
? (() => {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
177
|
+
const agentName = manifest.agentName;
|
|
178
|
+
const wantChannels = manifest.agent.runtime.channels !== false && (manifest.agentChannels?.length ?? 0) > 0;
|
|
179
|
+
return async (req: Request): Promise<Response | null> => {
|
|
180
|
+
// REQUEST-scoped, not isolate-scoped: the pipeline awaits between
|
|
181
|
+
// fetch() and this call, so a concurrent fetch may have run since —
|
|
182
|
+
// a module-level "current request" variable here would hand this
|
|
183
|
+
// webhook ANOTHER request's env (wrong AGENT namespace, factories
|
|
184
|
+
// resolved against foreign secrets) and register its background
|
|
185
|
+
// work on another invocation's waitUntil. Everything below reads
|
|
186
|
+
// THIS request's locals.
|
|
187
|
+
const locals = requestLocals.get(req);
|
|
188
|
+
const ns = () => (locals?.env as { AGENT?: DurableObjectNamespace } | undefined)?.AGENT;
|
|
189
|
+
const chat = durableAgentSurface(ns, { agentName, chatPath: manifest.agent.runtime.chat.path });
|
|
190
|
+
const chatRes = await chat(req);
|
|
191
|
+
if (chatRes) return chatRes;
|
|
192
|
+
if (!wantChannels) return null;
|
|
193
|
+
// Constructed per request — cheap by design: resolveChannel is a
|
|
194
|
+
// call, and resolveServices memoizes the app services bag per
|
|
195
|
+
// (env, agentName), exactly for this construction pattern (see
|
|
196
|
+
// durableChannelSurface's services doc).
|
|
197
|
+
const channels = durableChannelSurface(ns, {
|
|
198
|
+
agentName,
|
|
199
|
+
channels: manifest.agentChannels!,
|
|
200
|
+
env: locals?.env,
|
|
201
|
+
services: servicesProvider,
|
|
202
|
+
waitUntil: locals?.ctx?.waitUntil?.bind(locals.ctx),
|
|
203
|
+
});
|
|
204
|
+
return channels(req);
|
|
205
|
+
};
|
|
159
206
|
})()
|
|
160
207
|
: undefined;
|
|
161
208
|
|
|
@@ -224,8 +271,9 @@ export function createWorker(
|
|
|
224
271
|
});
|
|
225
272
|
|
|
226
273
|
return {
|
|
227
|
-
fetch(request, env) {
|
|
228
|
-
currentEnv = env;
|
|
274
|
+
fetch(request, env, ctx) {
|
|
275
|
+
currentEnv = env; // per-isolate-stable consumers only (resources/services)
|
|
276
|
+
requestLocals.set(request, { env, ctx }); // per-invocation consumers (agent surface)
|
|
229
277
|
return pipeline.fetch(request);
|
|
230
278
|
},
|
|
231
279
|
};
|
|
@@ -241,16 +289,16 @@ export function createWorker(
|
|
|
241
289
|
// dynamic routes reach the pipeline. With no ASSETS binding (no prerender), this
|
|
242
290
|
// is a transparent pass-through to the pipeline.
|
|
243
291
|
type AssetEnv = { ASSETS?: { fetch(request: Request): Promise<Response> } };
|
|
244
|
-
type FetchPipeline = { fetch(request: Request, env?: unknown): Promise<Response> };
|
|
292
|
+
type FetchPipeline = { fetch(request: Request, env?: unknown, ctx?: WorkerExecutionContext): Promise<Response> };
|
|
245
293
|
|
|
246
294
|
const estimateTokens = (s: string) => String(Math.ceil(s.length / 4));
|
|
247
295
|
|
|
248
296
|
export function withAssets(
|
|
249
297
|
pipeline: FetchPipeline,
|
|
250
298
|
opts: { link?: string | null } = {},
|
|
251
|
-
): { fetch(request: Request, env?: AssetEnv): Promise<Response> } {
|
|
299
|
+
): { fetch(request: Request, env?: AssetEnv, ctx?: WorkerExecutionContext): Promise<Response> } {
|
|
252
300
|
return {
|
|
253
|
-
async fetch(request, env) {
|
|
301
|
+
async fetch(request, env, ctx) {
|
|
254
302
|
const assets = env?.ASSETS;
|
|
255
303
|
const url = new URL(request.url);
|
|
256
304
|
const isPagePath = !/\.[a-z0-9]+$/i.test(url.pathname); // no file extension
|
|
@@ -296,7 +344,7 @@ export function withAssets(
|
|
|
296
344
|
|
|
297
345
|
// 3. Dynamic routes → the render pipeline (already sets Link + negotiates).
|
|
298
346
|
// Env flows through so the pipeline's resources get the D1/KV/R2 bindings.
|
|
299
|
-
return pipeline.fetch(request, env);
|
|
347
|
+
return pipeline.fetch(request, env, ctx);
|
|
300
348
|
},
|
|
301
349
|
};
|
|
302
350
|
}
|