@junejs/server 1.0.0-dev.14 → 1.0.0-dev.17

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 CHANGED
@@ -4,6 +4,7 @@ type AdapterCapabilities = {
4
4
  runtime: "edge" | "node" | "serverless" | "native" | "static";
5
5
  persistentConnections: boolean;
6
6
  assets: "platform" | "server" | "none";
7
+ durableObjects?: boolean;
7
8
  };
8
9
  type AdapterEntry = {
9
10
  imports: string[];
@@ -14,6 +15,10 @@ type ResourcePlan = {
14
15
  binding: string;
15
16
  databaseName: string;
16
17
  };
18
+ agent?: {
19
+ binding: string;
20
+ className: string;
21
+ };
17
22
  };
18
23
  type AdapterEmitContext = {
19
24
  appRoot: string;
@@ -39,6 +44,7 @@ interface JuneAdapter {
39
44
  }): AdapterEntry;
40
45
  emit(ctx: AdapterEmitContext): Promise<void>;
41
46
  }
47
+ declare function hasDurableBinding(text: string, file: string, binding: string, className: string): boolean;
42
48
  declare function workers(opts?: {
43
49
  name?: string;
44
50
  domain?: string;
@@ -53,4 +59,4 @@ declare function deno(opts?: {
53
59
  }): JuneAdapter;
54
60
  declare function staticSite(): JuneAdapter;
55
61
  //#endregion
56
- export { AdapterCapabilities, AdapterEmitContext, AdapterEntry, JuneAdapter, ResourcePlan, deno, staticSite, vercel, workers };
62
+ export { AdapterCapabilities, AdapterEmitContext, AdapterEntry, JuneAdapter, ResourcePlan, deno, hasDurableBinding, staticSite, vercel, workers };
package/dist/adapter.js CHANGED
@@ -1,14 +1,51 @@
1
+ import { stripJsonc } from "./tsconfig-jsx.js";
1
2
  import { existsSync } from "node:fs";
2
- import { copyFile, cp, mkdir, readdir, rm, writeFile } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
3
+ import { copyFile, cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, join } from "node:path";
4
5
  //#region src/adapter.ts
6
+ function hasDurableBinding(text, file, binding, className) {
7
+ if (file.endsWith(".toml")) {
8
+ const lines = text.split("\n").map((line) => {
9
+ let inString = false;
10
+ for (let i = 0; i < line.length; i++) {
11
+ const ch = line[i];
12
+ if (ch === "\"" && line[i - 1] !== "\\") inString = !inString;
13
+ else if (ch === "#" && !inString) return line.slice(0, i);
14
+ }
15
+ return line;
16
+ });
17
+ let inBindings = false;
18
+ let name;
19
+ let cls;
20
+ const matches = () => name === binding && cls === className;
21
+ for (const line of lines) {
22
+ const t = line.trim();
23
+ if (t.startsWith("[")) {
24
+ if (inBindings && matches()) return true;
25
+ inBindings = /^\[\[\s*durable_objects\.bindings\s*\]\]$/.test(t);
26
+ name = cls = void 0;
27
+ continue;
28
+ }
29
+ if (!inBindings) continue;
30
+ const kv = t.match(/^(name|class_name)\s*=\s*"([^"]*)"/);
31
+ if (kv) kv[1] === "name" ? name = kv[2] : cls = kv[2];
32
+ }
33
+ return inBindings && matches();
34
+ }
35
+ try {
36
+ return (JSON.parse(stripJsonc(text)).durable_objects?.bindings ?? []).some((b) => b.name === binding && b.class_name === className);
37
+ } catch {
38
+ return text.includes(className);
39
+ }
40
+ }
5
41
  function workers(opts) {
6
42
  return {
7
43
  name: "workers",
8
44
  capabilities: {
9
45
  runtime: "edge",
10
46
  persistentConnections: true,
11
- assets: "platform"
47
+ assets: "platform",
48
+ durableObjects: true
12
49
  },
13
50
  conditions: [
14
51
  "source",
@@ -25,7 +62,17 @@ function workers(opts) {
25
62
  };
26
63
  },
27
64
  async emit({ appRoot, outDir, hasAssets, config, plan, defaultName }) {
28
- if (existsSync(join(appRoot, "wrangler.toml")) || existsSync(join(appRoot, "wrangler.jsonc"))) return;
65
+ const own = ["wrangler.toml", "wrangler.jsonc"].map((f) => join(appRoot, f)).find(existsSync);
66
+ if (own) {
67
+ if (plan.agent) {
68
+ const cls = plan.agent.className;
69
+ if (!hasDurableBinding(await readFile(own, "utf8"), own, plan.agent.binding, cls)) {
70
+ const snippet = own.endsWith(".toml") ? `[[durable_objects.bindings]]\nname = "${plan.agent.binding}"\nclass_name = "${cls}"\n\n[[migrations]]\ntag = "v1"\nnew_sqlite_classes = ["${cls}"]` : `"durable_objects": { "bindings": [{ "name": "${plan.agent.binding}", "class_name": "${cls}" }] },\n"migrations": [{ "tag": "v1", "new_sqlite_classes": ["${cls}"] }]`;
71
+ console.warn(`[june build] durable agent built, but ${basename(own)} doesn't bind it — add:\n${snippet}`);
72
+ }
73
+ }
74
+ return;
75
+ }
29
76
  const deployCfg = config.deploy;
30
77
  await writeFile(join(outDir, "wrangler.jsonc"), JSON.stringify({
31
78
  name: opts?.name ?? deployCfg?.name ?? defaultName,
@@ -42,6 +89,16 @@ function workers(opts) {
42
89
  database_name: plan.db.databaseName,
43
90
  database_id: ""
44
91
  }] } : {},
92
+ ...plan.agent ? {
93
+ durable_objects: { bindings: [{
94
+ name: plan.agent.binding,
95
+ class_name: plan.agent.className
96
+ }] },
97
+ migrations: [{
98
+ tag: "v1",
99
+ new_sqlite_classes: [plan.agent.className]
100
+ }]
101
+ } : {},
45
102
  ...opts?.domain ?? deployCfg?.domain ? { routes: [{
46
103
  pattern: opts?.domain ?? deployCfg?.domain,
47
104
  custom_domain: true
@@ -208,4 +265,4 @@ function staticSite() {
208
265
  };
209
266
  }
210
267
  //#endregion
211
- export { deno, staticSite, vercel, workers };
268
+ export { deno, hasDurableBinding, staticSite, vercel, workers };
@@ -0,0 +1,35 @@
1
+ //#region src/agent-compile.d.ts
2
+ declare const AGENT_MODULE_FILE = "_agent.gen.ts";
3
+ type AgentDirScan = {
4
+ dir: string;
5
+ name: string;
6
+ hasConfig: boolean;
7
+ instructions: string | null;
8
+ tools: string[];
9
+ skills: {
10
+ name: string;
11
+ raw: string;
12
+ }[];
13
+ channels: string[];
14
+ channelOverlays: {
15
+ source: string;
16
+ raw: string;
17
+ }[];
18
+ connections: string[];
19
+ };
20
+ declare function scanAgentDir(dir: string): AgentDirScan | null;
21
+ type EmitOptions = {
22
+ tsExtensions?: boolean;
23
+ };
24
+ declare function emitAgentModule(scan: AgentDirScan, opts?: EmitOptions): string;
25
+ declare function findAgentDir(root: string, dirName?: string): string | null;
26
+ declare function generateAgentModule(dir: string, opts?: {
27
+ check?: boolean;
28
+ } & EmitOptions): {
29
+ file: string;
30
+ code: string;
31
+ stale: boolean;
32
+ written: boolean;
33
+ } | null;
34
+ //#endregion
35
+ export { AGENT_MODULE_FILE, AgentDirScan, EmitOptions, emitAgentModule, findAgentDir, generateAgentModule, scanAgentDir };
@@ -0,0 +1,139 @@
1
+ import { stripJsonc } from "./tsconfig-jsx.js";
2
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ //#region src/agent-compile.ts
5
+ const AGENT_MODULE_FILE = "_agent.gen.ts";
6
+ function list(dir, ext) {
7
+ if (!existsSync(dir)) return [];
8
+ return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(ext) && !e.name.startsWith("_")).map((e) => e.name).sort();
9
+ }
10
+ function scanAgentDir(dir) {
11
+ if (!existsSync(dir)) return null;
12
+ const instructionsFile = join(dir, "instructions.md");
13
+ const scan = {
14
+ dir,
15
+ name: basename(dir),
16
+ hasConfig: existsSync(join(dir, "agent.ts")),
17
+ instructions: existsSync(instructionsFile) ? readFileSync(instructionsFile, "utf8") : null,
18
+ tools: list(join(dir, "tools"), ".ts").map((f) => `./tools/${f}`),
19
+ skills: list(join(dir, "skills"), ".md").map((f) => ({
20
+ name: basename(f, ".md"),
21
+ raw: readFileSync(join(dir, "skills", f), "utf8")
22
+ })),
23
+ channels: list(join(dir, "channels"), ".ts").map((f) => `./channels/${f}`),
24
+ channelOverlays: list(join(dir, "channels"), ".md").map((f) => ({
25
+ source: basename(f, ".md"),
26
+ raw: readFileSync(join(dir, "channels", f), "utf8")
27
+ })),
28
+ connections: list(join(dir, "connections"), ".ts").map((f) => `./connections/${f}`)
29
+ };
30
+ return !scan.hasConfig && scan.instructions === null && !scan.tools.length && !scan.skills.length && !scan.channels.length && !scan.channelOverlays.length && !scan.connections.length ? null : scan;
31
+ }
32
+ function identFor(prefix, spec, taken) {
33
+ const stem = basename(spec).replace(/\.ts$/, "").replace(/[^A-Za-z0-9]/g, "_");
34
+ let id = `${prefix}_${stem}`;
35
+ for (let i = 2; taken.has(id); i++) id = `${prefix}_${stem}_${i}`;
36
+ taken.add(id);
37
+ return id;
38
+ }
39
+ function emitAgentModule(scan, opts) {
40
+ const spec = (s) => JSON.stringify(opts?.tsExtensions ? s : s.replace(/\.ts$/, ""));
41
+ const taken = /* @__PURE__ */ new Set();
42
+ const imports = [];
43
+ const toolIds = scan.tools.map((s) => {
44
+ const id = identFor("tool", s, taken);
45
+ imports.push(`import ${id} from ${spec(s)};`);
46
+ return id;
47
+ });
48
+ const channelIds = scan.channels.map((s) => {
49
+ const id = identFor("channel", s, taken);
50
+ imports.push(`import ${id} from ${spec(s)};`);
51
+ return {
52
+ key: basename(s, ".ts"),
53
+ id
54
+ };
55
+ });
56
+ const connectionIds = scan.connections.map((s) => {
57
+ const id = identFor("connection", s, taken);
58
+ imports.push(`import ${id} from ${spec(s)};`);
59
+ return id;
60
+ });
61
+ if (scan.hasConfig) imports.push(`import config from ${spec("./agent.ts")};`);
62
+ const lines = [
63
+ "// AUTO-GENERATED by June — do not edit. Compiled from this agent/ directory so",
64
+ "// the definition mounts without filesystem discovery (workerd has no fs).",
65
+ "// Regenerate with `june gen` after adding, removing, or editing agent files.",
66
+ ...imports,
67
+ scan.skills.length ? `import { parseSkill, type AgentModule } from "@junejs/core/agent-config";` : `import type { AgentModule } from "@junejs/core/agent-config";`,
68
+ ""
69
+ ];
70
+ if (!scan.hasConfig) lines.push(`const config = { name: ${JSON.stringify(scan.name)} };`, "");
71
+ lines.push("export const agentModule: AgentModule = {", " config,", ` instructions: ${JSON.stringify(scan.instructions ?? "")},`, ` tools: [${toolIds.join(", ")}],`, scan.skills.length ? ` skills: [\n${scan.skills.map((s) => ` parseSkill(${JSON.stringify(s.name)}, ${JSON.stringify(s.raw)}),`).join("\n")}\n ],` : " skills: [],", channelIds.length ? ` channels: { ${channelIds.map((c) => `${JSON.stringify(c.key)}: ${c.id}`).join(", ")} },` : " channels: {},", scan.channelOverlays.length ? ` channelInstructions: {\n${scan.channelOverlays.map((o) => ` ${JSON.stringify(o.source)}: ${JSON.stringify(o.raw)},`).join("\n")}\n },` : " channelInstructions: {},", ` connections: [${connectionIds.join(", ")}],`, "};", "export default agentModule;", "");
72
+ return lines.join("\n");
73
+ }
74
+ function findAgentDir(root, dirName = "agent") {
75
+ for (const candidate of [join(root, "app", dirName), join(root, dirName)]) if (existsSync(candidate)) return candidate;
76
+ return null;
77
+ }
78
+ function readTsconfig(path) {
79
+ try {
80
+ return JSON.parse(stripJsonc(readFileSync(path, "utf8")));
81
+ } catch {
82
+ return;
83
+ }
84
+ }
85
+ function resolveExtends(fromDir, spec) {
86
+ const withJson = (p) => existsSync(p) ? p : !p.endsWith(".json") && existsSync(`${p}.json`) ? `${p}.json` : void 0;
87
+ if (spec.startsWith(".")) return withJson(resolve(fromDir, spec));
88
+ for (let d = fromDir;;) {
89
+ const candidate = withJson(join(d, "node_modules", spec));
90
+ if (candidate) return candidate;
91
+ const parent = dirname(d);
92
+ if (parent === d) return void 0;
93
+ d = parent;
94
+ }
95
+ }
96
+ function resolveAllowTsExtensions(path, depth) {
97
+ if (depth > 8) return void 0;
98
+ const tc = readTsconfig(path);
99
+ if (!tc) return void 0;
100
+ const own = tc.compilerOptions?.allowImportingTsExtensions;
101
+ if (own !== void 0) return own;
102
+ const bases = typeof tc.extends === "string" ? [tc.extends] : [...tc.extends ?? []].reverse();
103
+ for (const spec of bases) {
104
+ const base = resolveExtends(dirname(path), spec);
105
+ const inherited = base ? resolveAllowTsExtensions(base, depth + 1) : void 0;
106
+ if (inherited !== void 0) return inherited;
107
+ }
108
+ }
109
+ function sniffTsExtensions(dir) {
110
+ for (let d = dir;;) {
111
+ const candidate = join(d, "tsconfig.json");
112
+ if (existsSync(candidate)) return resolveAllowTsExtensions(candidate, 0) ?? false;
113
+ const parent = dirname(d);
114
+ if (parent === d) return false;
115
+ d = parent;
116
+ }
117
+ }
118
+ function generateAgentModule(dir, opts) {
119
+ const scan = scanAgentDir(dir);
120
+ if (!scan) return null;
121
+ const code = emitAgentModule(scan, { tsExtensions: opts?.tsExtensions ?? sniffTsExtensions(dir) });
122
+ const file = join(dir, AGENT_MODULE_FILE);
123
+ const stale = (existsSync(file) ? readFileSync(file, "utf8") : null) !== code;
124
+ if (opts?.check) return {
125
+ file,
126
+ code,
127
+ stale,
128
+ written: false
129
+ };
130
+ if (stale) writeFileSync(file, code);
131
+ return {
132
+ file,
133
+ code,
134
+ stale,
135
+ written: stale
136
+ };
137
+ }
138
+ //#endregion
139
+ export { AGENT_MODULE_FILE, emitAgentModule, findAgentDir, generateAgentModule, scanAgentDir };
@@ -1,5 +1,6 @@
1
- import { AgentDefinition } from "@junejs/core/agent-config";
1
+ import { AgentDefinition, AgentModule } from "@junejs/core/agent-config";
2
2
  //#region src/agent-discover.d.ts
3
+ declare function discoverAgentModule(dir: string): Promise<AgentModule>;
3
4
  declare function discoverAgent(dir: string): Promise<AgentDefinition>;
4
5
  //#endregion
5
- export { discoverAgent };
6
+ export { discoverAgent, discoverAgentModule };
@@ -2,42 +2,17 @@ import { existsSync } from "node:fs";
2
2
  import { readFile, readdir } from "node:fs/promises";
3
3
  import { basename, extname, join } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
- import { defineAgent, resolveChannel } from "@junejs/core/agent-config";
6
- import { connectAll } from "@junejs/core/connections";
5
+ import { assembleAgent, parseSkill } from "@junejs/core/agent-config";
7
6
  //#region src/agent-discover.ts
8
7
  async function scan(dir, ext) {
9
8
  if (!existsSync(dir)) return [];
10
- return (await readdir(dir, { withFileTypes: true })).filter((e) => e.isFile() && extname(e.name) === ext).map((e) => join(dir, e.name)).sort();
9
+ return (await readdir(dir, { withFileTypes: true })).filter((e) => e.isFile() && extname(e.name) === ext && !e.name.startsWith("_")).map((e) => join(dir, e.name)).sort();
11
10
  }
12
- function parseSkill(name, text) {
13
- if (text.startsWith("---")) {
14
- const end = text.indexOf("\n---", 3);
15
- if (end !== -1) {
16
- const front = text.slice(3, end);
17
- const body = text.slice(end + 4).replace(/^\s*\n/, "");
18
- const meta = {};
19
- for (const line of front.split("\n")) {
20
- const m = line.match(/^\s*(\w+)\s*:\s*(.+?)\s*$/);
21
- if (m) meta[m[1]] = m[2];
22
- }
23
- return {
24
- name: meta.name ?? name,
25
- description: meta.description ?? "",
26
- body
27
- };
28
- }
29
- }
30
- return {
31
- name,
32
- description: (text.split("\n").find((l) => l.trim()) ?? "").replace(/^#\s*/, ""),
33
- body: text
34
- };
35
- }
36
- async function discoverAgent(dir) {
11
+ async function discoverAgentModule(dir) {
37
12
  const configFile = join(dir, "agent.ts");
38
13
  const config = existsSync(configFile) ? (await import(pathToFileURL(configFile).href)).default : { name: basename(dir) };
39
14
  const instructionsFile = join(dir, "instructions.md");
40
- const instructions = existsSync(instructionsFile) ? await readFile(instructionsFile, "utf8") : config.instructions ?? "";
15
+ const instructions = existsSync(instructionsFile) ? await readFile(instructionsFile, "utf8") : "";
41
16
  const tools = [];
42
17
  for (const f of await scan(join(dir, "tools"), ".ts")) {
43
18
  const mod = await import(pathToFileURL(f).href);
@@ -45,31 +20,30 @@ async function discoverAgent(dir) {
45
20
  }
46
21
  const skills = [];
47
22
  for (const f of await scan(join(dir, "skills"), ".md")) skills.push(parseSkill(basename(f, ".md"), await readFile(f, "utf8")));
48
- const channels = [];
23
+ const channels = {};
49
24
  for (const f of await scan(join(dir, "channels"), ".ts")) {
50
25
  const mod = await import(pathToFileURL(f).href);
51
- if (mod.default) channels.push(resolveChannel(mod.default, process.env));
26
+ if (mod.default) channels[basename(f, ".ts")] = mod.default;
52
27
  }
53
- const connectionDefs = [];
28
+ const channelInstructions = {};
29
+ for (const f of await scan(join(dir, "channels"), ".md")) channelInstructions[basename(f, ".md")] = await readFile(f, "utf8");
30
+ const connections = [];
54
31
  for (const f of await scan(join(dir, "connections"), ".ts")) {
55
32
  const mod = await import(pathToFileURL(f).href);
56
- if (mod.default) connectionDefs.push(mod.default);
33
+ if (mod.default) connections.push(mod.default);
57
34
  }
58
- const { actions: connectionActions, report: connections } = connectionDefs.length ? await connectAll(connectionDefs) : {
59
- actions: [],
60
- report: []
61
- };
62
- tools.push(...connectionActions);
63
- return defineAgent({
64
- name: config.name,
65
- model: config.model,
66
- description: config.description,
35
+ return {
36
+ config,
67
37
  instructions,
68
38
  tools,
69
39
  skills,
70
40
  channels,
41
+ channelInstructions,
71
42
  connections
72
- });
43
+ };
44
+ }
45
+ async function discoverAgent(dir) {
46
+ return assembleAgent(await discoverAgentModule(dir), process.env);
73
47
  }
74
48
  //#endregion
75
- export { discoverAgent };
49
+ export { discoverAgent, discoverAgentModule };
@@ -50,7 +50,7 @@ type DoAgentDef = {
50
50
  channelInstructions?: Record<string, string>;
51
51
  channels?: (Channel | ChannelFactory)[];
52
52
  env?: unknown;
53
- resources?: Resources;
53
+ resources?: Resources | ((env: unknown) => Promise<Resources> | Resources);
54
54
  services?: unknown;
55
55
  onTurnError?: (failure: {
56
56
  turnId: string;
@@ -69,8 +69,11 @@ declare class AgentDurableObject {
69
69
  private readonly tools;
70
70
  private readonly channels;
71
71
  private readonly channelInstructions?;
72
- private readonly resources;
72
+ private readonly resourcesInput;
73
+ private resourcesOpened;
74
+ private readonly doEnv;
73
75
  private readonly services;
76
+ private resolveResources;
74
77
  constructor(state: DurableObjectState, def: DoAgentDef);
75
78
  private resolveSession;
76
79
  turn(input: {
@@ -99,13 +99,19 @@ var AgentDurableObject = class {
99
99
  tools;
100
100
  channels;
101
101
  channelInstructions;
102
- resources;
102
+ resourcesInput;
103
+ resourcesOpened;
104
+ doEnv;
103
105
  services;
106
+ resolveResources() {
107
+ return this.resourcesOpened ??= Promise.resolve(typeof this.resourcesInput === "function" ? this.resourcesInput(this.doEnv) : this.resourcesInput ?? {});
108
+ }
104
109
  constructor(state, def) {
105
110
  assertCoreRuntimeVersion(`AgentDurableObject(${def.name ?? "agent"})`);
106
111
  const store = new DoSessionStore(state.storage);
107
112
  const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
108
- this.resources = def.resources ?? {};
113
+ this.resourcesInput = def.resources;
114
+ this.doEnv = def.env;
109
115
  this.services = def.services;
110
116
  const name = def.name ?? "agent";
111
117
  const sink = new InProcEventSink();
@@ -162,17 +168,19 @@ var AgentDurableObject = class {
162
168
  }
163
169
  async turn(input) {
164
170
  await ensureScope();
171
+ const resources = await this.resolveResources();
165
172
  const session = this.resolveSession(input.session);
166
173
  return runInScope({
167
- resources: this.resources,
174
+ resources,
168
175
  services: this.services
169
176
  }, () => session.turn(input));
170
177
  }
171
178
  async start(input) {
172
179
  await ensureScope();
180
+ const resources = await this.resolveResources();
173
181
  const session = this.resolveSession(input.session);
174
182
  return runInScope({
175
- resources: this.resources,
183
+ resources,
176
184
  services: this.services
177
185
  }, () => session.start(input));
178
186
  }
@@ -198,12 +206,13 @@ var AgentDurableObject = class {
198
206
  deliverChannel = this.channels.find((c) => c.name === event.source);
199
207
  if (!deliverChannel?.deliver) return Response.json({ error: `agent "${this.name}": no deliver()-capable channel named "${event.source}" is wired into this DO (DoAgentDef.channels) — the turn was NOT started` }, { status: 501 });
200
208
  }
209
+ const resources = await this.resolveResources();
201
210
  let session;
202
211
  let started;
203
212
  try {
204
213
  session = this.resolveSession(key);
205
214
  started = runInScope({
206
- resources: this.resources,
215
+ resources,
207
216
  services: this.services
208
217
  }, () => session.start({
209
218
  userText,
@@ -227,7 +236,7 @@ var AgentDurableObject = class {
227
236
  recipientTeamId: event.teamId
228
237
  };
229
238
  Promise.resolve().then(() => runInScope({
230
- resources: this.resources,
239
+ resources,
231
240
  services: this.services
232
241
  }, () => deliverChannel.deliver(target, events, { session: this.sessionKey }))).catch(logRenderFailure).finally(() => {
233
242
  it.return?.();
@@ -250,11 +259,12 @@ var AgentDurableObject = class {
250
259
  resumeChannel = this.channels.find((c) => c.name === source);
251
260
  if (!resumeChannel?.deliverResume) return Response.json({ error: `agent "${this.name}": no deliverResume()-capable channel named "${source}" is wired into this DO (DoAgentDef.channels) — the answer was NOT applied` }, { status: 501 });
252
261
  }
262
+ const resources = await this.resolveResources();
253
263
  let session;
254
264
  try {
255
265
  session = this.resolveSession(key);
256
266
  runInScope({
257
- resources: this.resources,
267
+ resources,
258
268
  services: this.services
259
269
  }, () => session.resume(turnId, inputId, input, { by }));
260
270
  } catch (err) {
@@ -267,7 +277,7 @@ var AgentDurableObject = class {
267
277
  const it = observeTurnEvents(session, turnId)[Symbol.asyncIterator]();
268
278
  const events = { [Symbol.asyncIterator]: () => it };
269
279
  Promise.resolve().then(() => runInScope({
270
- resources: this.resources,
280
+ resources,
271
281
  services: this.services
272
282
  }, () => resumeChannel.deliverResume(target, events, { session: this.sessionKey }))).catch(logRenderFailure).finally(() => {
273
283
  it.return?.();
package/dist/build.js CHANGED
@@ -1,8 +1,9 @@
1
+ import { jsxTransform } from "./tsconfig-jsx.js";
1
2
  import { deno, staticSite, vercel, workers } from "./adapter.js";
3
+ import { generateAgentModule } from "./agent-compile.js";
2
4
  import { findMiddlewareFile } from "./router.js";
3
5
  import { resolveBoundary } from "./segment.js";
4
6
  import { buildModuleCss, registerCssModules, rolldownCssModulesPlugin } from "./css-modules.js";
5
- import { jsxTransform } from "./tsconfig-jsx.js";
6
7
  import { CLIENT_SCRIPT_URL, bundleClientToFile, findClientEntry } from "./client-bundle.js";
7
8
  import { generateIslandRegistry } from "./island-registry.js";
8
9
  import { STYLES_URL, cssTargets, findGlobalCss, globalCssUsesTailwind, minifyCss, processCss } from "./css.js";
@@ -262,6 +263,12 @@ async function buildManifest(appRoot) {
262
263
  const mod = await import(pathToFileURL(extraFile).href);
263
264
  if (typeof mod.default === "function") extra = mod.default;
264
265
  }
266
+ let agentName;
267
+ const agentDir = join(appDir, frozen.agent.runtime.dir);
268
+ if (frozen.agent.runtime.enabled && existsSync(agentDir)) {
269
+ const cfgFile = join(agentDir, "agent.ts");
270
+ agentName = (existsSync(cfgFile) ? (await import(pathToFileURL(cfgFile).href)).default : void 0)?.name ?? basename(agentDir);
271
+ }
265
272
  return {
266
273
  routes,
267
274
  dynamicRoutes,
@@ -271,6 +278,7 @@ async function buildManifest(appRoot) {
271
278
  loadings,
272
279
  document: frozen.document,
273
280
  agent: frozen.agent,
281
+ agentName,
274
282
  i18n: frozen.i18n,
275
283
  earlyHints: frozen.earlyHints,
276
284
  extra
@@ -317,6 +325,24 @@ async function juneBuild(appRoot, options = {}) {
317
325
  } : void 0 },
318
326
  config: fullConfig
319
327
  });
328
+ const agentDir = join(appDir, frozen.agent.runtime.dir);
329
+ let agentModule = null;
330
+ if (frozen.agent.runtime.enabled) {
331
+ const generated = generateAgentModule(agentDir);
332
+ if (generated && adapter.capabilities.durableObjects) agentModule = generated;
333
+ else if (generated) console.warn(`[june build] app/${frozen.agent.runtime.dir}/ found, but the ${adapter.name}() target has no Durable Objects — the durable agent is not mounted.`);
334
+ }
335
+ if (agentModule) {
336
+ const sdkResolvable = (fromDir) => {
337
+ for (let d = resolve(fromDir);;) {
338
+ if (existsSync(join(d, "node_modules", "@anthropic-ai", "sdk", "package.json"))) return true;
339
+ const parent = dirname(d);
340
+ if (parent === d) return false;
341
+ d = parent;
342
+ }
343
+ };
344
+ if (!sdkResolvable(appRoot)) throw new Error(`app/${frozen.agent.runtime.dir}/ mounts a durable agent whose model is Claude — add the SDK to the app so it bundles for workerd: bun add @anthropic-ai/sdk`);
345
+ }
320
346
  const cssOut = await processCss(appDir, { minify: true });
321
347
  let cssAsset = null;
322
348
  if (cssOut !== null) {
@@ -407,7 +433,8 @@ async function juneBuild(appRoot, options = {}) {
407
433
  imports.push(`import { turso } from "@junejs/server/db";`);
408
434
  imports.push(`import { memoizeResources } from "@junejs/server/resources";`);
409
435
  } else if (hasResources) imports.push(`import { bindWorkerResources } from "@junejs/server/resources";`);
410
- const resourcesField = tursoDb ? `\n resources: memoizeResources({ db: turso() }),` : hasResources ? `\n resources: bindWorkerResources(${JSON.stringify(resourceFlags)}),` : "";
436
+ const resourcesExpr = tursoDb ? `memoizeResources({ db: turso() })` : hasResources ? `bindWorkerResources(${JSON.stringify(resourceFlags)})` : null;
437
+ const resourcesField = resourcesExpr ? `\n resources: ${resourcesExpr},` : "";
411
438
  const dataLayerModule = fullConfig.dataLayer?.module;
412
439
  if (dataLayerModule) imports.push(`import { installDataLayer } from ${JSON.stringify(dataLayerModule)};`);
413
440
  const dataLayerBoot = dataLayerModule ? "\ninstallDataLayer();\n" : "";
@@ -417,6 +444,36 @@ async function juneBuild(appRoot, options = {}) {
417
444
  imports.push(`import { services as __appServices } from ${JSON.stringify(spec)};`);
418
445
  }
419
446
  const servicesField = servicesModule ? `\n services: __appServices,` : "";
447
+ if (agentModule) {
448
+ imports.push(`import __agentModule from ${JSON.stringify(importPath(genDir, agentModule.file))};`);
449
+ imports.push(`import { assembleDurable } from "@junejs/core/agent-config";`);
450
+ imports.push(`import { anthropic } from "@junejs/core/agent-models";`);
451
+ imports.push(`import { AgentDurableObject } from "@junejs/server/agent-durable";`);
452
+ imports.push(`import { DurableObject } from "cloudflare:workers";`);
453
+ imports.push(`import Anthropic from "@anthropic-ai/sdk";`);
454
+ }
455
+ const agentNameField = agentModule ? `\n agentName: __agentModule.config.name,` : "";
456
+ const doClass = agentModule ? `
457
+ // The durable agent: one Durable Object per session, the compiled agent/
458
+ // directory as its definition — adapted tools (+ read_skill when skills exist),
459
+ // the assembled system prompt, channel factories resolved with the DO's own
460
+ // env, and the app's declared resources opened from THIS isolate's env (lazily,
461
+ // at the first turn — the same ambient db a tool sees in native dev). Model:
462
+ // Anthropic over a statically bundled client (workerd cannot resolve a bare
463
+ // import at runtime), keyed by the ANTHROPIC_API_KEY secret — a missing secret
464
+ // surfaces as the SDK's own clear construction error on the first agent request.
465
+ const __agentDef = assembleDurable(__agentModule);
466
+ export class JuneAgentDO extends DurableObject {
467
+ #agent = new AgentDurableObject(this.ctx, {
468
+ ...__agentDef,
469
+ model: anthropic({ model: __agentModule.config.model, client: new Anthropic({ apiKey: (this.env as { ANTHROPIC_API_KEY?: string }).ANTHROPIC_API_KEY }) }),
470
+ env: this.env,${resourcesExpr ? `\n resources: ${resourcesExpr},` : ""}${servicesModule ? "\n services: __appServices(this.env)," : ""}
471
+ });
472
+ fetch(req: Request): Promise<Response> {
473
+ return this.#agent.fetch(req);
474
+ }
475
+ }
476
+ ` : "";
420
477
  const entry = `// AUTO-GENERATED by \`june build\` — do not edit. Regenerate: june build .
421
478
  ${adapterEntry.imports.join("\n")}
422
479
  ${imports.join("\n")}
@@ -435,12 +492,12 @@ ${chains.join("\n")}
435
492
  ${loadings.join("\n")}
436
493
  },
437
494
  document: ${JSON.stringify(frozen.document, null, 2).replace(/\n/g, "\n ")},
438
- agent: ${JSON.stringify(frozen.agent)},${frozen.i18n ? `\n i18n: ${JSON.stringify(frozen.i18n)},` : ""}
495
+ agent: ${JSON.stringify(frozen.agent)},${agentNameField}${frozen.i18n ? `\n i18n: ${JSON.stringify(frozen.i18n)},` : ""}
439
496
  earlyHints: ${JSON.stringify(frozen.earlyHints)},${builtExtraFile ? "\n extra," : ""}${resourcesField}${servicesField}
440
497
  });
441
498
 
442
499
  ${adapterEntry.wrap("pipeline")}
443
- `;
500
+ ${doClass}`;
444
501
  const entryFile = join(genDir, "worker-entry.tsx");
445
502
  await writeFile(entryFile, entry);
446
503
  const { rolldown } = await import("rolldown");
@@ -455,6 +512,7 @@ ${adapterEntry.wrap("pipeline")}
455
512
  plugins: [rolldownCssModulesPlugin(cssModuleMaps)],
456
513
  external: (id) => {
457
514
  if (isBunSpecifier(id)) return true;
515
+ if (id.startsWith("cloudflare:")) return true;
458
516
  if (/\.(wasm|ttf|otf|woff2?|png|jpe?g|avif|webp)$/.test(id)) return true;
459
517
  return [...adapter.buildExternal ?? [], ...options.external ?? frozen.buildExternal].some((e) => id === e || id.startsWith(`${e}/`));
460
518
  },
@@ -561,10 +619,16 @@ ${adapterEntry.wrap("pipeline")}
561
619
  }
562
620
  const pkgPath = join(appRoot, "package.json");
563
621
  const defaultName = workerName((existsSync(pkgPath) ? JSON.parse(await Bun.file(pkgPath).text()).name : void 0) ?? basename(appRoot));
564
- const plan = resourcesCfg?.db && resourcesCfg.db.kind !== "turso" ? { db: {
565
- binding: "DB",
566
- databaseName: `${defaultName}-db`
567
- } } : {};
622
+ const plan = {
623
+ ...resourcesCfg?.db && resourcesCfg.db.kind !== "turso" ? { db: {
624
+ binding: "DB",
625
+ databaseName: `${defaultName}-db`
626
+ } } : {},
627
+ ...agentModule ? { agent: {
628
+ binding: "AGENT",
629
+ className: "JuneAgentDO"
630
+ } } : {}
631
+ };
568
632
  await adapter.emit({
569
633
  appRoot,
570
634
  outDir,
@@ -576,6 +640,7 @@ ${adapterEntry.wrap("pipeline")}
576
640
  publicFiles
577
641
  });
578
642
  if (plan.db) console.log(` ↳ d1 binding "${plan.db.binding}" emitted — set database_id in wrangler.jsonc (wrangler d1 create ${plan.db.databaseName})`);
643
+ if (plan.agent) console.log(` ↳ durable agent mounted (${plan.agent.className} → env.${plan.agent.binding}; chat at ${frozen.agent.runtime.chat.path})`);
579
644
  return {
580
645
  outFile,
581
646
  routes: routes.filter((r) => !r.dynamic).map((r) => r.path),