@junejs/server 1.0.0-dev.17 → 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/agent-compile.d.ts +4 -0
- package/dist/agent-compile.js +8 -2
- package/dist/agent-discover.js +13 -1
- package/dist/agent-durable.d.ts +8 -2
- package/dist/agent-durable.js +31 -5
- package/dist/build.js +3 -3
- package/dist/worker.d.ts +9 -4
- package/dist/worker.js +28 -9
- package/package.json +2 -2
- package/src/agent-compile.ts +26 -0
- package/src/agent-discover.ts +37 -11
- package/src/agent-durable.ts +63 -8
- package/src/build.ts +5 -3
- package/src/worker.ts +66 -18
package/dist/agent-compile.d.ts
CHANGED
package/dist/agent-compile.js
CHANGED
|
@@ -15,6 +15,10 @@ function scanAgentDir(dir) {
|
|
|
15
15
|
name: basename(dir),
|
|
16
16
|
hasConfig: existsSync(join(dir, "agent.ts")),
|
|
17
17
|
instructions: existsSync(instructionsFile) ? readFileSync(instructionsFile, "utf8") : null,
|
|
18
|
+
surfaceVariants: readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name.match(/^instructions\.([a-z0-9_-]+)\.md$/)).filter((m) => m !== null).sort((a, b) => a[1].localeCompare(b[1])).map((m) => ({
|
|
19
|
+
source: m[1],
|
|
20
|
+
raw: readFileSync(join(dir, m[0]), "utf8")
|
|
21
|
+
})),
|
|
18
22
|
tools: list(join(dir, "tools"), ".ts").map((f) => `./tools/${f}`),
|
|
19
23
|
skills: list(join(dir, "skills"), ".md").map((f) => ({
|
|
20
24
|
name: basename(f, ".md"),
|
|
@@ -27,7 +31,7 @@ function scanAgentDir(dir) {
|
|
|
27
31
|
})),
|
|
28
32
|
connections: list(join(dir, "connections"), ".ts").map((f) => `./connections/${f}`)
|
|
29
33
|
};
|
|
30
|
-
return !scan.hasConfig && scan.instructions === null && !scan.tools.length && !scan.skills.length && !scan.channels.length && !scan.channelOverlays.length && !scan.connections.length ? null : scan;
|
|
34
|
+
return !scan.hasConfig && scan.instructions === null && !scan.tools.length && !scan.skills.length && !scan.channels.length && !scan.surfaceVariants.length && !scan.channelOverlays.length && !scan.connections.length ? null : scan;
|
|
31
35
|
}
|
|
32
36
|
function identFor(prefix, spec, taken) {
|
|
33
37
|
const stem = basename(spec).replace(/\.ts$/, "").replace(/[^A-Za-z0-9]/g, "_");
|
|
@@ -68,7 +72,7 @@ function emitAgentModule(scan, opts) {
|
|
|
68
72
|
""
|
|
69
73
|
];
|
|
70
74
|
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;", "");
|
|
75
|
+
lines.push("export const agentModule: AgentModule = {", " config,", ` instructions: ${JSON.stringify(scan.instructions ?? "")},`, scan.surfaceVariants.length ? ` surfaceInstructions: {\n${scan.surfaceVariants.map((v) => ` ${JSON.stringify(v.source)}: ${JSON.stringify(v.raw)},`).join("\n")}\n },` : " surfaceInstructions: {},", ` 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
76
|
return lines.join("\n");
|
|
73
77
|
}
|
|
74
78
|
function findAgentDir(root, dirName = "agent") {
|
|
@@ -118,6 +122,8 @@ function sniffTsExtensions(dir) {
|
|
|
118
122
|
function generateAgentModule(dir, opts) {
|
|
119
123
|
const scan = scanAgentDir(dir);
|
|
120
124
|
if (!scan) return null;
|
|
125
|
+
for (const o of scan.channelOverlays) console.warn(`[june] deprecated: channels/${o.source}.md — move it to instructions.${o.source}.md at the agent root (per-surface behavior is agent-level; see junebuild/june#149).`);
|
|
126
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) if (e.isFile() && /^instructions\..+\.md$/.test(e.name) && !/^instructions\.[a-z0-9_-]+\.md$/.test(e.name)) console.warn(`[june] ${e.name}: unrecognized instructions variant — only single-segment sources (instructions.<source>.md) are parsed today; locale variants are a planned follow-up (junebuild/june#149). This file is ignored.`);
|
|
121
127
|
const code = emitAgentModule(scan, { tsExtensions: opts?.tsExtensions ?? sniffTsExtensions(dir) });
|
|
122
128
|
const file = join(dir, AGENT_MODULE_FILE);
|
|
123
129
|
const stale = (existsSync(file) ? readFileSync(file, "utf8") : null) !== code;
|
package/dist/agent-discover.js
CHANGED
|
@@ -13,6 +13,13 @@ async function discoverAgentModule(dir) {
|
|
|
13
13
|
const config = existsSync(configFile) ? (await import(pathToFileURL(configFile).href)).default : { name: basename(dir) };
|
|
14
14
|
const instructionsFile = join(dir, "instructions.md");
|
|
15
15
|
const instructions = existsSync(instructionsFile) ? await readFile(instructionsFile, "utf8") : "";
|
|
16
|
+
const surfaceInstructions = {};
|
|
17
|
+
if (existsSync(dir)) for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
18
|
+
if (!e.isFile() || !/^instructions\..+\.md$/.test(e.name)) continue;
|
|
19
|
+
const m = e.name.match(/^instructions\.([a-z0-9_-]+)\.md$/);
|
|
20
|
+
if (m) surfaceInstructions[m[1]] = await readFile(join(dir, e.name), "utf8");
|
|
21
|
+
else console.warn(`[june] ${e.name}: unrecognized instructions variant — only single-segment sources (instructions.<source>.md) are parsed today; locale variants are a planned follow-up (junebuild/june#149). This file is ignored.`);
|
|
22
|
+
}
|
|
16
23
|
const tools = [];
|
|
17
24
|
for (const f of await scan(join(dir, "tools"), ".ts")) {
|
|
18
25
|
const mod = await import(pathToFileURL(f).href);
|
|
@@ -26,7 +33,11 @@ async function discoverAgentModule(dir) {
|
|
|
26
33
|
if (mod.default) channels[basename(f, ".ts")] = mod.default;
|
|
27
34
|
}
|
|
28
35
|
const channelInstructions = {};
|
|
29
|
-
for (const f of await scan(join(dir, "channels"), ".md"))
|
|
36
|
+
for (const f of await scan(join(dir, "channels"), ".md")) {
|
|
37
|
+
const source = basename(f, ".md");
|
|
38
|
+
console.warn(`[june] deprecated: channels/${source}.md — move it to instructions.${source}.md at the agent root (per-surface behavior is agent-level; see junebuild/june#149).`);
|
|
39
|
+
channelInstructions[source] = await readFile(f, "utf8");
|
|
40
|
+
}
|
|
30
41
|
const connections = [];
|
|
31
42
|
for (const f of await scan(join(dir, "connections"), ".ts")) {
|
|
32
43
|
const mod = await import(pathToFileURL(f).href);
|
|
@@ -35,6 +46,7 @@ async function discoverAgentModule(dir) {
|
|
|
35
46
|
return {
|
|
36
47
|
config,
|
|
37
48
|
instructions,
|
|
49
|
+
surfaceInstructions,
|
|
38
50
|
tools,
|
|
39
51
|
skills,
|
|
40
52
|
channels,
|
package/dist/agent-durable.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Channel, ChannelFactory } from "@junejs/core/agent-config";
|
|
2
|
-
import { InboundEvent, Model, Msg, ProactiveTrigger, SessionStore, Tool, TurnError, TurnEvent, TurnFailurePhase } from "@junejs/core/agent-runtime";
|
|
2
|
+
import { ChannelPolicy, InboundEvent, Model, Msg, ProactiveTrigger, SessionStore, Tool, TurnError, TurnEvent, TurnFailurePhase } from "@junejs/core/agent-runtime";
|
|
3
|
+
import { Connection } from "@junejs/core/connections";
|
|
3
4
|
import { Resources } from "@junejs/core/resources";
|
|
4
5
|
//#region src/agent-durable.d.ts
|
|
5
6
|
type SqlStorageValue = ArrayBuffer | string | number | null;
|
|
@@ -47,10 +48,11 @@ type DoAgentDef = {
|
|
|
47
48
|
model: Model;
|
|
48
49
|
tools: Tool[];
|
|
49
50
|
instructions?: string;
|
|
50
|
-
channelInstructions?: Record<string, string>;
|
|
51
|
+
channelInstructions?: Record<string, string | ChannelPolicy>;
|
|
51
52
|
channels?: (Channel | ChannelFactory)[];
|
|
52
53
|
env?: unknown;
|
|
53
54
|
resources?: Resources | ((env: unknown) => Promise<Resources> | Resources);
|
|
55
|
+
connections?: Connection[];
|
|
54
56
|
services?: unknown;
|
|
55
57
|
onTurnError?: (failure: {
|
|
56
58
|
turnId: string;
|
|
@@ -71,9 +73,13 @@ declare class AgentDurableObject {
|
|
|
71
73
|
private readonly channelInstructions?;
|
|
72
74
|
private readonly resourcesInput;
|
|
73
75
|
private resourcesOpened;
|
|
76
|
+
private readonly connectionsInput;
|
|
77
|
+
private connectionsOpened;
|
|
74
78
|
private readonly doEnv;
|
|
75
79
|
private readonly services;
|
|
76
80
|
private resolveResources;
|
|
81
|
+
private resolveConnections;
|
|
82
|
+
private ready;
|
|
77
83
|
constructor(state: DurableObjectState, def: DoAgentDef);
|
|
78
84
|
private resolveSession;
|
|
79
85
|
turn(input: {
|
package/dist/agent-durable.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { isolateLocal } from "./isolate-local.js";
|
|
2
2
|
import { assertCoreRuntimeVersion } from "./core-version.js";
|
|
3
|
-
import { DeliverUnsupportedError, channelDispatch, resolveChannel } from "@junejs/core/agent-config";
|
|
3
|
+
import { DeliverUnsupportedError, actionToTool, channelDispatch, resolveChannel } from "@junejs/core/agent-config";
|
|
4
4
|
import { AgentSession, ResumeAuthorizationError, withSystem } from "@junejs/core/agent-runtime";
|
|
5
|
+
import { connectAll } from "@junejs/core/connections";
|
|
5
6
|
import { ensureScope, runInScope } from "@junejs/db";
|
|
6
7
|
//#region src/agent-durable.ts
|
|
7
8
|
var DoSessionStore = class {
|
|
@@ -101,16 +102,37 @@ var AgentDurableObject = class {
|
|
|
101
102
|
channelInstructions;
|
|
102
103
|
resourcesInput;
|
|
103
104
|
resourcesOpened;
|
|
105
|
+
connectionsInput;
|
|
106
|
+
connectionsOpened;
|
|
104
107
|
doEnv;
|
|
105
108
|
services;
|
|
106
109
|
resolveResources() {
|
|
107
110
|
return this.resourcesOpened ??= Promise.resolve(typeof this.resourcesInput === "function" ? this.resourcesInput(this.doEnv) : this.resourcesInput ?? {});
|
|
108
111
|
}
|
|
112
|
+
resolveConnections() {
|
|
113
|
+
return this.connectionsOpened ??= (async () => {
|
|
114
|
+
if (!this.connectionsInput?.length) return;
|
|
115
|
+
const { actions, report } = await connectAll(this.connectionsInput);
|
|
116
|
+
for (const r of report) if (r.error) console.error(`[june] agent "${this.name}": connection "${r.name}" (${r.url}) failed: ${r.error}`);
|
|
117
|
+
const seen = new Set(this.tools.map((t) => t.spec.name));
|
|
118
|
+
for (const a of actions) {
|
|
119
|
+
const tool = actionToTool(a);
|
|
120
|
+
if (seen.has(tool.spec.name)) throw new Error(`AgentDurableObject(${this.name}): duplicate tool name "${tool.spec.name}" from a connection — rename so dispatch is unambiguous.`);
|
|
121
|
+
seen.add(tool.spec.name);
|
|
122
|
+
this.tools.push(tool);
|
|
123
|
+
}
|
|
124
|
+
})();
|
|
125
|
+
}
|
|
126
|
+
async ready() {
|
|
127
|
+
const [resources] = await Promise.all([this.resolveResources(), this.resolveConnections()]);
|
|
128
|
+
return resources;
|
|
129
|
+
}
|
|
109
130
|
constructor(state, def) {
|
|
110
131
|
assertCoreRuntimeVersion(`AgentDurableObject(${def.name ?? "agent"})`);
|
|
111
132
|
const store = new DoSessionStore(state.storage);
|
|
112
133
|
const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
|
|
113
134
|
this.resourcesInput = def.resources;
|
|
135
|
+
this.connectionsInput = def.connections;
|
|
114
136
|
this.doEnv = def.env;
|
|
115
137
|
this.services = def.services;
|
|
116
138
|
const name = def.name ?? "agent";
|
|
@@ -151,6 +173,10 @@ var AgentDurableObject = class {
|
|
|
151
173
|
this.tools = tools;
|
|
152
174
|
this.channels = channels;
|
|
153
175
|
this.channelInstructions = def.channelInstructions;
|
|
176
|
+
if (def.channelInstructions) {
|
|
177
|
+
const names = new Set(channels.map((c) => c.name));
|
|
178
|
+
for (const source of Object.keys(def.channelInstructions)) if (!names.has(source)) console.warn(`[june] agent "${name}": surface policy "${source}" matches no channel wired into this DO (${[...names].join(", ") || "none"}) — it will never fire.`);
|
|
179
|
+
}
|
|
154
180
|
}
|
|
155
181
|
resolveSession(key) {
|
|
156
182
|
if (key !== void 0) assertSessionKey(key);
|
|
@@ -168,7 +194,7 @@ var AgentDurableObject = class {
|
|
|
168
194
|
}
|
|
169
195
|
async turn(input) {
|
|
170
196
|
await ensureScope();
|
|
171
|
-
const resources = await this.
|
|
197
|
+
const resources = await this.ready();
|
|
172
198
|
const session = this.resolveSession(input.session);
|
|
173
199
|
return runInScope({
|
|
174
200
|
resources,
|
|
@@ -177,7 +203,7 @@ var AgentDurableObject = class {
|
|
|
177
203
|
}
|
|
178
204
|
async start(input) {
|
|
179
205
|
await ensureScope();
|
|
180
|
-
const resources = await this.
|
|
206
|
+
const resources = await this.ready();
|
|
181
207
|
const session = this.resolveSession(input.session);
|
|
182
208
|
return runInScope({
|
|
183
209
|
resources,
|
|
@@ -206,7 +232,7 @@ var AgentDurableObject = class {
|
|
|
206
232
|
deliverChannel = this.channels.find((c) => c.name === event.source);
|
|
207
233
|
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 });
|
|
208
234
|
}
|
|
209
|
-
const resources = await this.
|
|
235
|
+
const resources = await this.ready();
|
|
210
236
|
let session;
|
|
211
237
|
let started;
|
|
212
238
|
try {
|
|
@@ -259,7 +285,7 @@ var AgentDurableObject = class {
|
|
|
259
285
|
resumeChannel = this.channels.find((c) => c.name === source);
|
|
260
286
|
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 });
|
|
261
287
|
}
|
|
262
|
-
const resources = await this.
|
|
288
|
+
const resources = await this.ready();
|
|
263
289
|
let session;
|
|
264
290
|
try {
|
|
265
291
|
session = this.resolveSession(key);
|
package/dist/build.js
CHANGED
|
@@ -452,7 +452,8 @@ async function juneBuild(appRoot, options = {}) {
|
|
|
452
452
|
imports.push(`import { DurableObject } from "cloudflare:workers";`);
|
|
453
453
|
imports.push(`import Anthropic from "@anthropic-ai/sdk";`);
|
|
454
454
|
}
|
|
455
|
-
const
|
|
455
|
+
const agentPreamble = agentModule ? `\nconst __agentDef = assembleDurable(__agentModule);\n` : "";
|
|
456
|
+
const agentNameField = agentModule ? `\n agentName: __agentModule.config.name,\n agentChannels: __agentDef.channels,` : "";
|
|
456
457
|
const doClass = agentModule ? `
|
|
457
458
|
// The durable agent: one Durable Object per session, the compiled agent/
|
|
458
459
|
// directory as its definition — adapted tools (+ read_skill when skills exist),
|
|
@@ -462,7 +463,6 @@ async function juneBuild(appRoot, options = {}) {
|
|
|
462
463
|
// Anthropic over a statically bundled client (workerd cannot resolve a bare
|
|
463
464
|
// import at runtime), keyed by the ANTHROPIC_API_KEY secret — a missing secret
|
|
464
465
|
// surfaces as the SDK's own clear construction error on the first agent request.
|
|
465
|
-
const __agentDef = assembleDurable(__agentModule);
|
|
466
466
|
export class JuneAgentDO extends DurableObject {
|
|
467
467
|
#agent = new AgentDurableObject(this.ctx, {
|
|
468
468
|
...__agentDef,
|
|
@@ -477,7 +477,7 @@ export class JuneAgentDO extends DurableObject {
|
|
|
477
477
|
const entry = `// AUTO-GENERATED by \`june build\` — do not edit. Regenerate: june build .
|
|
478
478
|
${adapterEntry.imports.join("\n")}
|
|
479
479
|
${imports.join("\n")}
|
|
480
|
-
${dataLayerBoot}
|
|
480
|
+
${dataLayerBoot}${agentPreamble}
|
|
481
481
|
const pipeline = createWorker({
|
|
482
482
|
routes: {
|
|
483
483
|
${statics.join("\n")}
|
package/dist/worker.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { l as ResourceHandler, n as LayoutComponent, r as LoadingComponent, t as ExtraHandler } from "./pipeline-cCR6MGNK.js";
|
|
2
|
+
import { Channel, ChannelFactory } from "@junejs/core/agent-config";
|
|
2
3
|
import { BrandedRoute } from "@junejs/core/route";
|
|
3
4
|
import { AgentConfig } from "@junejs/core/config";
|
|
4
5
|
import React from "react";
|
|
@@ -6,6 +7,9 @@ import { DocumentConfig } from "@junejs/core/document";
|
|
|
6
7
|
import { I18nConfig } from "@junejs/core/i18n";
|
|
7
8
|
import { Resources } from "@junejs/core/resources";
|
|
8
9
|
//#region src/worker.d.ts
|
|
10
|
+
type WorkerExecutionContext = {
|
|
11
|
+
waitUntil?(p: Promise<unknown>): void;
|
|
12
|
+
};
|
|
9
13
|
type WorkerManifest = {
|
|
10
14
|
routes: Record<string, BrandedRoute>;
|
|
11
15
|
dynamicRoutes?: Array<{
|
|
@@ -25,6 +29,7 @@ type WorkerManifest = {
|
|
|
25
29
|
document: DocumentConfig;
|
|
26
30
|
agent: AgentConfig;
|
|
27
31
|
agentName?: string;
|
|
32
|
+
agentChannels?: (Channel | ChannelFactory)[];
|
|
28
33
|
i18n?: I18nConfig;
|
|
29
34
|
earlyHints?: string[];
|
|
30
35
|
htmlCacheControl?: string;
|
|
@@ -36,7 +41,7 @@ type WorkerManifest = {
|
|
|
36
41
|
services?: (env?: unknown) => unknown;
|
|
37
42
|
};
|
|
38
43
|
declare function createWorker(manifest: WorkerManifest): {
|
|
39
|
-
fetch(request: Request, env?: unknown): Promise<Response>;
|
|
44
|
+
fetch(request: Request, env?: unknown, ctx?: WorkerExecutionContext): Promise<Response>;
|
|
40
45
|
};
|
|
41
46
|
type AssetEnv = {
|
|
42
47
|
ASSETS?: {
|
|
@@ -44,13 +49,13 @@ type AssetEnv = {
|
|
|
44
49
|
};
|
|
45
50
|
};
|
|
46
51
|
type FetchPipeline = {
|
|
47
|
-
fetch(request: Request, env?: unknown): Promise<Response>;
|
|
52
|
+
fetch(request: Request, env?: unknown, ctx?: WorkerExecutionContext): Promise<Response>;
|
|
48
53
|
};
|
|
49
54
|
declare function withAssets(pipeline: FetchPipeline, opts?: {
|
|
50
55
|
link?: string | null;
|
|
51
56
|
}): {
|
|
52
|
-
fetch(request: Request, env?: AssetEnv): Promise<Response>;
|
|
57
|
+
fetch(request: Request, env?: AssetEnv, ctx?: WorkerExecutionContext): Promise<Response>;
|
|
53
58
|
};
|
|
54
59
|
declare function withDenoAssets(pipeline: FetchPipeline): (request: Request) => Promise<Response>;
|
|
55
60
|
//#endregion
|
|
56
|
-
export { WorkerManifest, createWorker, withAssets, withDenoAssets };
|
|
61
|
+
export { WorkerExecutionContext, WorkerManifest, createWorker, withAssets, withDenoAssets };
|
package/dist/worker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { durableAgentSurface } from "./agent-durable.js";
|
|
1
|
+
import { durableAgentSurface, durableChannelSurface } from "./agent-durable.js";
|
|
2
2
|
import { createPipeline } from "./pipeline.js";
|
|
3
3
|
import { RESERVED_PREFIX, contentTypeFor, safeRelativePath } from "./static-files.js";
|
|
4
4
|
//#region src/worker.ts
|
|
@@ -61,15 +61,30 @@ function createWorker(manifest) {
|
|
|
61
61
|
const loadingFor = (key) => manifest.loadings?.[key];
|
|
62
62
|
const boundaryFor = (key) => manifest.layoutBoundaries?.[key] ?? null;
|
|
63
63
|
let currentEnv;
|
|
64
|
+
const requestLocals = /* @__PURE__ */ new WeakMap();
|
|
64
65
|
const provider = manifest.resources;
|
|
65
66
|
const servicesProvider = manifest.services;
|
|
66
67
|
let servicesCache;
|
|
67
68
|
const agentSurface = manifest.agent.runtime.enabled && manifest.agentName ? (() => {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
const agentName = manifest.agentName;
|
|
70
|
+
const wantChannels = manifest.agent.runtime.channels !== false && (manifest.agentChannels?.length ?? 0) > 0;
|
|
71
|
+
return async (req) => {
|
|
72
|
+
const locals = requestLocals.get(req);
|
|
73
|
+
const ns = () => (locals?.env)?.AGENT;
|
|
74
|
+
const chatRes = await durableAgentSurface(ns, {
|
|
75
|
+
agentName,
|
|
76
|
+
chatPath: manifest.agent.runtime.chat.path
|
|
77
|
+
})(req);
|
|
78
|
+
if (chatRes) return chatRes;
|
|
79
|
+
if (!wantChannels) return null;
|
|
80
|
+
return durableChannelSurface(ns, {
|
|
81
|
+
agentName,
|
|
82
|
+
channels: manifest.agentChannels,
|
|
83
|
+
env: locals?.env,
|
|
84
|
+
services: servicesProvider,
|
|
85
|
+
waitUntil: locals?.ctx?.waitUntil?.bind(locals.ctx)
|
|
86
|
+
})(req);
|
|
87
|
+
};
|
|
73
88
|
})() : void 0;
|
|
74
89
|
const pipeline = createPipeline({
|
|
75
90
|
docConfig: manifest.document,
|
|
@@ -130,14 +145,18 @@ function createWorker(manifest) {
|
|
|
130
145
|
return null;
|
|
131
146
|
}
|
|
132
147
|
});
|
|
133
|
-
return { fetch(request, env) {
|
|
148
|
+
return { fetch(request, env, ctx) {
|
|
134
149
|
currentEnv = env;
|
|
150
|
+
requestLocals.set(request, {
|
|
151
|
+
env,
|
|
152
|
+
ctx
|
|
153
|
+
});
|
|
135
154
|
return pipeline.fetch(request);
|
|
136
155
|
} };
|
|
137
156
|
}
|
|
138
157
|
const estimateTokens = (s) => String(Math.ceil(s.length / 4));
|
|
139
158
|
function withAssets(pipeline, opts = {}) {
|
|
140
|
-
return { async fetch(request, env) {
|
|
159
|
+
return { async fetch(request, env, ctx) {
|
|
141
160
|
const assets = env?.ASSETS;
|
|
142
161
|
const url = new URL(request.url);
|
|
143
162
|
const isPagePath = !/\.[a-z0-9]+$/i.test(url.pathname);
|
|
@@ -177,7 +196,7 @@ function withAssets(pipeline, opts = {}) {
|
|
|
177
196
|
return a;
|
|
178
197
|
}
|
|
179
198
|
}
|
|
180
|
-
return pipeline.fetch(request, env);
|
|
199
|
+
return pipeline.fetch(request, env, ctx);
|
|
181
200
|
} };
|
|
182
201
|
}
|
|
183
202
|
function withDenoAssets(pipeline) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junejs/server",
|
|
3
|
-
"version": "1.0.0-dev.
|
|
3
|
+
"version": "1.0.0-dev.19",
|
|
4
4
|
"description": "June host adapters + dev server. Host-coupled; depends on the pure @junejs/core contract layer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"rolldown": "^1.1.0"
|
|
139
139
|
},
|
|
140
140
|
"peerDependencies": {
|
|
141
|
-
"@junejs/core": ">=0.2.0-dev.
|
|
141
|
+
"@junejs/core": ">=0.2.0-dev.36",
|
|
142
142
|
"react": "^19.2.0",
|
|
143
143
|
"react-dom": "^19.2.0",
|
|
144
144
|
"react-server-dom-webpack": "19.2.7",
|
package/src/agent-compile.ts
CHANGED
|
@@ -31,6 +31,8 @@ export type AgentDirScan = {
|
|
|
31
31
|
name: string;
|
|
32
32
|
hasConfig: boolean;
|
|
33
33
|
instructions: string | null;
|
|
34
|
+
// instructions.<source>.md at the agent root — per-surface variants (#149).
|
|
35
|
+
surfaceVariants: { source: string; raw: string }[];
|
|
34
36
|
tools: string[]; // relative specifiers, e.g. "./tools/create_order.ts"
|
|
35
37
|
skills: { name: string; raw: string }[];
|
|
36
38
|
channels: string[]; // "./channels/slack.ts"
|
|
@@ -59,6 +61,12 @@ export function scanAgentDir(dir: string): AgentDirScan | null {
|
|
|
59
61
|
name: basename(dir),
|
|
60
62
|
hasConfig: existsSync(join(dir, "agent.ts")),
|
|
61
63
|
instructions: existsSync(instructionsFile) ? readFileSync(instructionsFile, "utf8") : null,
|
|
64
|
+
surfaceVariants: readdirSync(dir, { withFileTypes: true })
|
|
65
|
+
.filter((e) => e.isFile())
|
|
66
|
+
.map((e) => e.name.match(/^instructions\.([a-z0-9_-]+)\.md$/))
|
|
67
|
+
.filter((m): m is RegExpMatchArray => m !== null)
|
|
68
|
+
.sort((a, b) => a[1]!.localeCompare(b[1]!))
|
|
69
|
+
.map((m) => ({ source: m[1]!, raw: readFileSync(join(dir, m[0]!), "utf8") })),
|
|
62
70
|
tools: list(join(dir, "tools"), ".ts").map((f) => `./tools/${f}`),
|
|
63
71
|
skills: list(join(dir, "skills"), ".md").map((f) => ({
|
|
64
72
|
name: basename(f, ".md"),
|
|
@@ -77,6 +85,7 @@ export function scanAgentDir(dir: string): AgentDirScan | null {
|
|
|
77
85
|
!scan.tools.length &&
|
|
78
86
|
!scan.skills.length &&
|
|
79
87
|
!scan.channels.length &&
|
|
88
|
+
!scan.surfaceVariants.length &&
|
|
80
89
|
!scan.channelOverlays.length &&
|
|
81
90
|
!scan.connections.length;
|
|
82
91
|
return empty ? null : scan;
|
|
@@ -139,6 +148,9 @@ export function emitAgentModule(scan: AgentDirScan, opts?: EmitOptions): string
|
|
|
139
148
|
"export const agentModule: AgentModule = {",
|
|
140
149
|
" config,",
|
|
141
150
|
` instructions: ${JSON.stringify(scan.instructions ?? "")},`,
|
|
151
|
+
scan.surfaceVariants.length
|
|
152
|
+
? ` surfaceInstructions: {\n${scan.surfaceVariants.map((v) => ` ${JSON.stringify(v.source)}: ${JSON.stringify(v.raw)},`).join("\n")}\n },`
|
|
153
|
+
: " surfaceInstructions: {},",
|
|
142
154
|
` tools: [${toolIds.join(", ")}],`,
|
|
143
155
|
scan.skills.length
|
|
144
156
|
? ` skills: [\n${scan.skills.map((s) => ` parseSkill(${JSON.stringify(s.name)}, ${JSON.stringify(s.raw)}),`).join("\n")}\n ],`
|
|
@@ -235,6 +247,20 @@ export function generateAgentModule(
|
|
|
235
247
|
): { file: string; code: string; stale: boolean; written: boolean } | null {
|
|
236
248
|
const scan = scanAgentDir(dir);
|
|
237
249
|
if (!scan) return null;
|
|
250
|
+
for (const o of scan.channelOverlays) {
|
|
251
|
+
console.warn(
|
|
252
|
+
`[june] deprecated: channels/${o.source}.md — move it to instructions.${o.source}.md at the agent root (per-surface behavior is agent-level; see junebuild/june#149).`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
// A variant-shaped file the grammar doesn't parse yet must never be silently
|
|
256
|
+
// inert (the same failure class this whole convention exists to kill).
|
|
257
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
258
|
+
if (e.isFile() && /^instructions\..+\.md$/.test(e.name) && !/^instructions\.[a-z0-9_-]+\.md$/.test(e.name)) {
|
|
259
|
+
console.warn(
|
|
260
|
+
`[june] ${e.name}: unrecognized instructions variant — only single-segment sources (instructions.<source>.md) are parsed today; locale variants are a planned follow-up (junebuild/june#149). This file is ignored.`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
238
264
|
const code = emitAgentModule(scan, { tsExtensions: opts?.tsExtensions ?? sniffTsExtensions(dir) });
|
|
239
265
|
const file = join(dir, AGENT_MODULE_FILE);
|
|
240
266
|
const existing = existsSync(file) ? readFileSync(file, "utf8") : null;
|
package/src/agent-discover.ts
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
// An agent is a DIRECTORY, not a config object. Location determines function:
|
|
4
4
|
//
|
|
5
5
|
// agent/
|
|
6
|
-
// agent.ts
|
|
7
|
-
// instructions.md
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// channels/*.
|
|
12
|
-
//
|
|
6
|
+
// agent.ts → default-exports a plain config { name, model?, surfaces? }
|
|
7
|
+
// instructions.md → base system prompt
|
|
8
|
+
// instructions.<source>.md → per-surface instruction variant (#149)
|
|
9
|
+
// tools/*.ts → each default-exports a defineAction (a tool)
|
|
10
|
+
// skills/*.md → each a procedure, loaded on demand (progressive disclosure)
|
|
11
|
+
// channels/*.ts → each default-exports a Channel (an inbound edge — pure transport)
|
|
12
|
+
// channels/*.md → DEPRECATED overlay location (warned; use instructions.<source>.md)
|
|
13
|
+
// connections/*.ts → each default-exports a Connection (an outbound tool source)
|
|
13
14
|
//
|
|
14
15
|
// There is no central registry to keep in sync — the directory IS the manifest.
|
|
15
16
|
// Because tools are `defineAction`s, the SAME directory is also an MCP server and
|
|
@@ -51,6 +52,26 @@ export async function discoverAgentModule(dir: string): Promise<AgentModule> {
|
|
|
51
52
|
const instructionsFile = join(dir, "instructions.md");
|
|
52
53
|
const instructions = existsSync(instructionsFile) ? await readFile(instructionsFile, "utf8") : "";
|
|
53
54
|
|
|
55
|
+
// instructions.<source>.md — the agent's per-surface instruction variants
|
|
56
|
+
// (#149): applied to turns arriving through that source, composed per
|
|
57
|
+
// config.surfaces (append by default, "replace" to stand alone).
|
|
58
|
+
const surfaceInstructions: Record<string, string> = {};
|
|
59
|
+
if (existsSync(dir)) {
|
|
60
|
+
for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
61
|
+
if (!e.isFile() || !/^instructions\..+\.md$/.test(e.name)) continue;
|
|
62
|
+
const m = e.name.match(/^instructions\.([a-z0-9_-]+)\.md$/);
|
|
63
|
+
if (m) {
|
|
64
|
+
surfaceInstructions[m[1]!] = await readFile(join(dir, e.name), "utf8");
|
|
65
|
+
} else {
|
|
66
|
+
// A variant-shaped file the grammar doesn't parse yet (e.g. a locale
|
|
67
|
+
// suffix, instructions.slack.zh-TW.md) must never be SILENTLY inert.
|
|
68
|
+
console.warn(
|
|
69
|
+
`[june] ${e.name}: unrecognized instructions variant — only single-segment sources (instructions.<source>.md) are parsed today; locale variants are a planned follow-up (junebuild/june#149). This file is ignored.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
54
75
|
const tools: AnyAction[] = [];
|
|
55
76
|
for (const f of await scan(join(dir, "tools"), ".ts")) {
|
|
56
77
|
const mod = await import(pathToFileURL(f).href);
|
|
@@ -71,11 +92,16 @@ export async function discoverAgentModule(dir: string): Promise<AgentModule> {
|
|
|
71
92
|
if (mod.default) channels[basename(f, ".ts")] = mod.default as Channel | ChannelFactory;
|
|
72
93
|
}
|
|
73
94
|
|
|
74
|
-
// channels/<source>.md —
|
|
75
|
-
//
|
|
95
|
+
// channels/<source>.md — the DEPRECATED overlay convention (#149): behavior
|
|
96
|
+
// prose belongs at the agent level (instructions.<source>.md), not beside
|
|
97
|
+
// transport code. Still honored for one dev-series, loudly.
|
|
76
98
|
const channelInstructions: Record<string, string> = {};
|
|
77
99
|
for (const f of await scan(join(dir, "channels"), ".md")) {
|
|
78
|
-
|
|
100
|
+
const source = basename(f, ".md");
|
|
101
|
+
console.warn(
|
|
102
|
+
`[june] deprecated: channels/${source}.md — move it to instructions.${source}.md at the agent root (per-surface behavior is agent-level; see junebuild/june#149).`,
|
|
103
|
+
);
|
|
104
|
+
channelInstructions[source] = await readFile(f, "utf8");
|
|
79
105
|
}
|
|
80
106
|
|
|
81
107
|
// connections/*.ts — outbound: definitions of external MCP/OpenAPI servers;
|
|
@@ -86,7 +112,7 @@ export async function discoverAgentModule(dir: string): Promise<AgentModule> {
|
|
|
86
112
|
if (mod.default) connections.push(mod.default as Connection);
|
|
87
113
|
}
|
|
88
114
|
|
|
89
|
-
return { config, instructions, tools, skills, channels, channelInstructions, connections };
|
|
115
|
+
return { config, instructions, surfaceInstructions, tools, skills, channels, channelInstructions, connections };
|
|
90
116
|
}
|
|
91
117
|
|
|
92
118
|
// Discover an agent from its directory, returning the assembled AgentDefinition
|
package/src/agent-durable.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
AgentSession,
|
|
20
20
|
ResumeAuthorizationError,
|
|
21
21
|
withSystem,
|
|
22
|
+
type ChannelPolicy,
|
|
22
23
|
type EventSink,
|
|
23
24
|
type TurnEvent,
|
|
24
25
|
type Model,
|
|
@@ -35,6 +36,7 @@ import type { Resources } from "@junejs/core/resources";
|
|
|
35
36
|
import {
|
|
36
37
|
channelDispatch,
|
|
37
38
|
DeliverUnsupportedError,
|
|
39
|
+
actionToTool,
|
|
38
40
|
resolveChannel,
|
|
39
41
|
type AgentDefinition,
|
|
40
42
|
type Channel,
|
|
@@ -42,6 +44,7 @@ import {
|
|
|
42
44
|
type ChannelFactory,
|
|
43
45
|
type ResumeDeliveryTarget,
|
|
44
46
|
} from "@junejs/core/agent-config";
|
|
47
|
+
import { connectAll, type Connection } from "@junejs/core/connections";
|
|
45
48
|
import { ensureScope, runInScope } from "@junejs/db";
|
|
46
49
|
import { isolateLocal } from "./isolate-local";
|
|
47
50
|
import { assertCoreRuntimeVersion } from "./core-version";
|
|
@@ -198,9 +201,12 @@ export type DoAgentDef = {
|
|
|
198
201
|
model: Model;
|
|
199
202
|
tools: Tool[];
|
|
200
203
|
instructions?: string;
|
|
201
|
-
// Per-channel-source
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
+
// Per-channel-source turn policies (see AgentDefinition.channelInstructions):
|
|
205
|
+
// an overlay string, or a ChannelPolicy carrying overlay/overlayMode/denyTools
|
|
206
|
+
// (#149 — derived from agent-level instructions.<source>.md + surfaces config
|
|
207
|
+
// by assembleDurable). The shared agent branches by real inbound source, no
|
|
208
|
+
// userText marker.
|
|
209
|
+
channelInstructions?: Record<string, string | ChannelPolicy>;
|
|
204
210
|
// Channels whose CAPABILITY tools should be available to this session's turns. Their
|
|
205
211
|
// tools are built HERE, in the DO isolate, from `env` — because a channel's tool `run`
|
|
206
212
|
// is a closure (over the bot token, etc.) that can't cross the worker→DO RPC. This is
|
|
@@ -213,6 +219,12 @@ export type DoAgentDef = {
|
|
|
213
219
|
// constructor cannot await, so a provider is resolved lazily at the first
|
|
214
220
|
// turn (once per DO instance) with this def's `env`.
|
|
215
221
|
resources?: Resources | ((env: unknown) => Promise<Resources> | Resources);
|
|
222
|
+
// Outbound connection DEFINITIONS (external MCP/OpenAPI servers). Wiring them
|
|
223
|
+
// is network I/O (connectAll), which a DO constructor cannot await — resolved
|
|
224
|
+
// lazily at the first turn, their tools merged into the agent's tool set
|
|
225
|
+
// before any session is constructed. The native equivalent happens at
|
|
226
|
+
// assembly (assembleAgent → connectAll).
|
|
227
|
+
connections?: Connection[];
|
|
216
228
|
services?: unknown;
|
|
217
229
|
// Called on every turn.failed for this session — the app's seam for routing turn
|
|
218
230
|
// failures to its own telemetry (Sentry, a ledger, …). Providing it REPLACES the
|
|
@@ -256,13 +268,15 @@ export class AgentDurableObject {
|
|
|
256
268
|
private readonly model: Model;
|
|
257
269
|
private readonly tools: Tool[];
|
|
258
270
|
private readonly channels: Channel[];
|
|
259
|
-
private readonly channelInstructions?: Record<string, string>;
|
|
271
|
+
private readonly channelInstructions?: Record<string, string | ChannelPolicy>;
|
|
260
272
|
// A bag is used as-is; a provider (bindWorkerResources' shape) is resolved
|
|
261
273
|
// lazily at the first turn with the def's env — opening a D1 handle is async
|
|
262
274
|
// and this constructor cannot await. Resolved once, shared across turns (env
|
|
263
275
|
// is stable per isolate, like bindWorkerResources memoizes per worker isolate).
|
|
264
276
|
private readonly resourcesInput: DoAgentDef["resources"];
|
|
265
277
|
private resourcesOpened: Promise<Resources> | undefined;
|
|
278
|
+
private readonly connectionsInput: Connection[] | undefined;
|
|
279
|
+
private connectionsOpened: Promise<void> | undefined;
|
|
266
280
|
private readonly doEnv: unknown;
|
|
267
281
|
private readonly services: unknown;
|
|
268
282
|
private resolveResources(): Promise<Resources> {
|
|
@@ -270,11 +284,40 @@ export class AgentDurableObject {
|
|
|
270
284
|
typeof this.resourcesInput === "function" ? this.resourcesInput(this.doEnv) : (this.resourcesInput ?? {}),
|
|
271
285
|
));
|
|
272
286
|
}
|
|
287
|
+
// Wire outbound connections once (network I/O the constructor can't await):
|
|
288
|
+
// their tools merge into this.tools BEFORE any session is constructed — the
|
|
289
|
+
// ready() callers hoist this ahead of resolveSession. A failed connection is
|
|
290
|
+
// reported and skipped (connectAll never throws), matching native assembly.
|
|
291
|
+
private resolveConnections(): Promise<void> {
|
|
292
|
+
return (this.connectionsOpened ??= (async () => {
|
|
293
|
+
if (!this.connectionsInput?.length) return;
|
|
294
|
+
const { actions, report } = await connectAll(this.connectionsInput);
|
|
295
|
+
for (const r of report) {
|
|
296
|
+
if (r.error) console.error(`[june] agent "${this.name}": connection "${r.name}" (${r.url}) failed: ${r.error}`);
|
|
297
|
+
}
|
|
298
|
+
const seen = new Set(this.tools.map((t) => t.spec.name));
|
|
299
|
+
for (const a of actions) {
|
|
300
|
+
const tool = actionToTool(a);
|
|
301
|
+
if (seen.has(tool.spec.name)) {
|
|
302
|
+
throw new Error(`AgentDurableObject(${this.name}): duplicate tool name "${tool.spec.name}" from a connection — rename so dispatch is unambiguous.`);
|
|
303
|
+
}
|
|
304
|
+
seen.add(tool.spec.name);
|
|
305
|
+
this.tools.push(tool);
|
|
306
|
+
}
|
|
307
|
+
})());
|
|
308
|
+
}
|
|
309
|
+
// Everything a turn needs opened BEFORE its synchronous scope-and-subscribe
|
|
310
|
+
// section: resources (possibly an async provider) and outbound connections.
|
|
311
|
+
private async ready(): Promise<Resources> {
|
|
312
|
+
const [resources] = await Promise.all([this.resolveResources(), this.resolveConnections()]);
|
|
313
|
+
return resources;
|
|
314
|
+
}
|
|
273
315
|
constructor(state: DurableObjectState, def: DoAgentDef) {
|
|
274
316
|
assertCoreRuntimeVersion(`AgentDurableObject(${def.name ?? "agent"})`); // #94: fail power-on, not mid-turn
|
|
275
317
|
const store = new DoSessionStore(state.storage);
|
|
276
318
|
const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
|
|
277
319
|
this.resourcesInput = def.resources;
|
|
320
|
+
this.connectionsInput = def.connections;
|
|
278
321
|
this.doEnv = def.env;
|
|
279
322
|
this.services = def.services;
|
|
280
323
|
const name = def.name ?? "agent";
|
|
@@ -335,6 +378,18 @@ export class AgentDurableObject {
|
|
|
335
378
|
this.tools = tools;
|
|
336
379
|
this.channels = channels;
|
|
337
380
|
this.channelInstructions = def.channelInstructions;
|
|
381
|
+
// Orphan-policy check (#149): assembly can't validate surface keys against
|
|
382
|
+
// factory channels (they resolve only here, with env) — so warn now, once,
|
|
383
|
+
// when a policy's source matches no resolved channel. A policy that never
|
|
384
|
+
// fires is the old silent failure; a warning is its floor.
|
|
385
|
+
if (def.channelInstructions) {
|
|
386
|
+
const names = new Set(channels.map((c) => c.name));
|
|
387
|
+
for (const source of Object.keys(def.channelInstructions)) {
|
|
388
|
+
if (!names.has(source)) {
|
|
389
|
+
console.warn(`[june] agent "${name}": surface policy "${source}" matches no channel wired into this DO (${[...names].join(", ") || "none"}) — it will never fire.`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
338
393
|
}
|
|
339
394
|
// Resolve THE session for this DO: explicit key (from a routed request) → persisted
|
|
340
395
|
// key (a prior life learned it) → "self". A key that contradicts the persisted or
|
|
@@ -382,7 +437,7 @@ export class AgentDurableObject {
|
|
|
382
437
|
// pipeline does; without it runInScope is a pass-through and ambient reads throw.
|
|
383
438
|
async turn(input: { turnId?: string; userText: string; event?: InboundEvent; trigger?: ProactiveTrigger; session?: string }): Promise<string> {
|
|
384
439
|
await ensureScope();
|
|
385
|
-
const resources = await this.
|
|
440
|
+
const resources = await this.ready();
|
|
386
441
|
const session = this.resolveSession(input.session);
|
|
387
442
|
return runInScope({ resources, services: this.services }, () => session.turn(input));
|
|
388
443
|
}
|
|
@@ -392,7 +447,7 @@ export class AgentDurableObject {
|
|
|
392
447
|
// the default turn-failure log / onTurnError (#76).
|
|
393
448
|
async start(input: { turnId?: string; userText: string; event?: InboundEvent; trigger?: ProactiveTrigger; session?: string }): Promise<{ turnId: string }> {
|
|
394
449
|
await ensureScope();
|
|
395
|
-
const resources = await this.
|
|
450
|
+
const resources = await this.ready();
|
|
396
451
|
const session = this.resolveSession(input.session);
|
|
397
452
|
return runInScope({ resources, services: this.services }, () => session.start(input));
|
|
398
453
|
}
|
|
@@ -450,7 +505,7 @@ export class AgentDurableObject {
|
|
|
450
505
|
// db/services (ALS propagates to the .then continuation registered here); subscribing
|
|
451
506
|
// happens synchronously right after, before any event can emit. Resources resolve
|
|
452
507
|
// BEFORE this block — the section from start() to the subscription stays synchronous.
|
|
453
|
-
const resources = await this.
|
|
508
|
+
const resources = await this.ready();
|
|
454
509
|
let session: AgentSession;
|
|
455
510
|
let started: { turnId: string };
|
|
456
511
|
try {
|
|
@@ -535,7 +590,7 @@ export class AgentDurableObject {
|
|
|
535
590
|
}
|
|
536
591
|
// Same discipline as /turn: resolve resources before the synchronous
|
|
537
592
|
// resume-then-subscribe section.
|
|
538
|
-
const resources = await this.
|
|
593
|
+
const resources = await this.ready();
|
|
539
594
|
let session: AgentSession;
|
|
540
595
|
try {
|
|
541
596
|
session = this.resolveSession(key);
|
package/src/build.ts
CHANGED
|
@@ -804,7 +804,10 @@ export async function juneBuild(
|
|
|
804
804
|
// anthropic({ client }), which skips that lazy path entirely.
|
|
805
805
|
imports.push(`import Anthropic from "@anthropic-ai/sdk";`);
|
|
806
806
|
}
|
|
807
|
-
|
|
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,` : "";
|
|
808
811
|
const doClass = agentModule
|
|
809
812
|
? `
|
|
810
813
|
// The durable agent: one Durable Object per session, the compiled agent/
|
|
@@ -815,7 +818,6 @@ export async function juneBuild(
|
|
|
815
818
|
// Anthropic over a statically bundled client (workerd cannot resolve a bare
|
|
816
819
|
// import at runtime), keyed by the ANTHROPIC_API_KEY secret — a missing secret
|
|
817
820
|
// surfaces as the SDK's own clear construction error on the first agent request.
|
|
818
|
-
const __agentDef = assembleDurable(__agentModule);
|
|
819
821
|
export class JuneAgentDO extends DurableObject {
|
|
820
822
|
#agent = new AgentDurableObject(this.ctx, {
|
|
821
823
|
...__agentDef,
|
|
@@ -832,7 +834,7 @@ export class JuneAgentDO extends DurableObject {
|
|
|
832
834
|
const entry = `// AUTO-GENERATED by \`june build\` — do not edit. Regenerate: june build .
|
|
833
835
|
${adapterEntry.imports.join("\n")}
|
|
834
836
|
${imports.join("\n")}
|
|
835
|
-
${dataLayerBoot}
|
|
837
|
+
${dataLayerBoot}${agentPreamble}
|
|
836
838
|
const pipeline = createWorker({
|
|
837
839
|
routes: {
|
|
838
840
|
${statics.join("\n")}
|
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
|
}
|