@junejs/server 1.0.0-dev.45 → 1.0.0-dev.47
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-durable.d.ts +4 -0
- package/dist/agent-durable.js +22 -4
- package/dist/agent-native.d.ts +10 -1
- package/dist/agent-native.js +17 -5
- package/dist/app.js +8 -1
- package/dist/core-version.js +1 -1
- package/dist/index.js +1 -1
- package/dist/sqlite-driver.d.ts +1 -0
- package/dist/sqlite-driver.js +17 -4
- package/dist/worker.js +7 -1
- package/package.json +2 -2
- package/src/agent-durable.ts +24 -3
- package/src/agent-native.ts +25 -10
- package/src/app.ts +12 -1
- package/src/core-version.ts +1 -1
- package/src/sqlite-driver.ts +36 -6
- package/src/worker.ts +10 -0
package/dist/agent-durable.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Channel, ChannelFactory } from "@junejs/core/agent-config";
|
|
2
2
|
import { AuthorizeAnswer, ChannelPolicy, InboundEvent, InputAnnouncement, Model, Msg, NoteKind, ProactiveTrigger, SessionStore, Tool, TurnError, TurnEvent, TurnFailurePhase } from "@junejs/core/agent-runtime";
|
|
3
|
+
import { FileStore } from "@junejs/core/agent-files";
|
|
3
4
|
import { Connection } from "@junejs/core/connections";
|
|
4
5
|
import { Resources } from "@junejs/core/resources";
|
|
5
6
|
//#region src/agent-durable.d.ts
|
|
@@ -63,6 +64,7 @@ export type DoAgentDef = {
|
|
|
63
64
|
}) => void | Promise<void>;
|
|
64
65
|
authorizeAnswer?: AuthorizeAnswer;
|
|
65
66
|
onInputAnnouncement?: (a: InputAnnouncement) => void | Promise<void>;
|
|
67
|
+
files?: FileStore;
|
|
66
68
|
};
|
|
67
69
|
export declare class AgentDurableObject {
|
|
68
70
|
private session;
|
|
@@ -83,6 +85,7 @@ export declare class AgentDurableObject {
|
|
|
83
85
|
private readonly authorizeAnswer?;
|
|
84
86
|
private readonly onInputAnnouncement?;
|
|
85
87
|
private readonly storage;
|
|
88
|
+
private readonly files;
|
|
86
89
|
private announceFailures;
|
|
87
90
|
private resolveResources;
|
|
88
91
|
private resolveConnections;
|
|
@@ -140,5 +143,6 @@ export declare function durableChannelSurface(getNamespace: () => DurableObjectN
|
|
|
140
143
|
env: unknown;
|
|
141
144
|
services?: (env: unknown) => unknown;
|
|
142
145
|
waitUntil?: (p: Promise<unknown>) => void;
|
|
146
|
+
files?: FileStore;
|
|
143
147
|
}): (req: Request) => Promise<Response | null>;
|
|
144
148
|
//#endregion
|
package/dist/agent-durable.js
CHANGED
|
@@ -2,7 +2,8 @@ import { observeTurnEvents } from "./turn-events.js";
|
|
|
2
2
|
import { isolateLocal } from "./isolate-local.js";
|
|
3
3
|
import { assertCoreRuntimeVersion } from "./core-version.js";
|
|
4
4
|
import { DeliverUnsupportedError, actionToTool, channelDispatch, resolveChannel } from "@junejs/core/agent-config";
|
|
5
|
-
import { AgentSession, ResumeAuthorizationError, grantAnswer, withSystem } from "@junejs/core/agent-runtime";
|
|
5
|
+
import { AgentSession, FatalToolError, ResumeAuthorizationError, grantAnswer, withSystem } from "@junejs/core/agent-runtime";
|
|
6
|
+
import { blobFileStore } from "@junejs/core/agent-files";
|
|
6
7
|
import { connectAll } from "@junejs/core/connections";
|
|
7
8
|
import { ensureScope, runInScope } from "@junejs/db";
|
|
8
9
|
//#region src/agent-durable.ts
|
|
@@ -111,6 +112,7 @@ var AgentDurableObject = class {
|
|
|
111
112
|
authorizeAnswer;
|
|
112
113
|
onInputAnnouncement;
|
|
113
114
|
storage;
|
|
115
|
+
files;
|
|
114
116
|
announceFailures = 0;
|
|
115
117
|
resolveResources() {
|
|
116
118
|
return this.resourcesOpened ??= Promise.resolve(typeof this.resourcesInput === "function" ? this.resourcesInput(this.doEnv) : this.resourcesInput ?? {});
|
|
@@ -145,6 +147,11 @@ var AgentDurableObject = class {
|
|
|
145
147
|
this.onInputAnnouncement = def.onInputAnnouncement;
|
|
146
148
|
this.storage = state.storage;
|
|
147
149
|
const name = def.name ?? "agent";
|
|
150
|
+
this.files = def.files ?? blobFileStore(async () => {
|
|
151
|
+
const blob = (await this.resolveResources()).blob;
|
|
152
|
+
if (!blob) throw new FatalToolError(`agent "${name}": no file store — declare a blob resource (DoAgentDef.resources) or pass DoAgentDef.files`);
|
|
153
|
+
return blob;
|
|
154
|
+
});
|
|
148
155
|
const sink = new InProcEventSink();
|
|
149
156
|
sink.subscribe((e) => {
|
|
150
157
|
if (e.type === "turn.started") this.armWatchdog();
|
|
@@ -202,6 +209,7 @@ var AgentDurableObject = class {
|
|
|
202
209
|
if (key !== void 0 && key !== "self" && stored === void 0) this.store.setSessionKey(key);
|
|
203
210
|
this.sessionKey = resolved;
|
|
204
211
|
this.session = new AgentSession(this.name, resolved, this.store, this.sink, this.model, this.tools, crossDoUnsupported, this.channelInstructions);
|
|
212
|
+
this.session.files = this.files;
|
|
205
213
|
this.session.onDequeue = (heldTurnId, held) => {
|
|
206
214
|
if (held.hostContext?.deliver && held.event) this.deliverHeld(this.session, heldTurnId, held.event);
|
|
207
215
|
};
|
|
@@ -260,7 +268,10 @@ var AgentDurableObject = class {
|
|
|
260
268
|
this.ready().then((resources) => runInScope({
|
|
261
269
|
resources,
|
|
262
270
|
services: this.services
|
|
263
|
-
}, () => channel.deliver(target, events, {
|
|
271
|
+
}, () => channel.deliver(target, events, {
|
|
272
|
+
session: this.sessionKey,
|
|
273
|
+
files: this.files
|
|
274
|
+
}))).catch(logRenderFailure).finally(() => {
|
|
264
275
|
it.return?.();
|
|
265
276
|
});
|
|
266
277
|
} catch (err) {
|
|
@@ -355,7 +366,10 @@ var AgentDurableObject = class {
|
|
|
355
366
|
Promise.resolve().then(() => runInScope({
|
|
356
367
|
resources,
|
|
357
368
|
services: this.services
|
|
358
|
-
}, () => deliverChannel.deliver(target, events, {
|
|
369
|
+
}, () => deliverChannel.deliver(target, events, {
|
|
370
|
+
session: this.sessionKey,
|
|
371
|
+
files: this.files
|
|
372
|
+
}))).catch(logRenderFailure).finally(() => {
|
|
359
373
|
it.return?.();
|
|
360
374
|
});
|
|
361
375
|
} catch (err) {
|
|
@@ -419,7 +433,10 @@ var AgentDurableObject = class {
|
|
|
419
433
|
Promise.resolve().then(() => runInScope({
|
|
420
434
|
resources,
|
|
421
435
|
services: this.services
|
|
422
|
-
}, () => resumeChannel.deliverResume(target, events, {
|
|
436
|
+
}, () => resumeChannel.deliverResume(target, events, {
|
|
437
|
+
session: this.sessionKey,
|
|
438
|
+
files: this.files
|
|
439
|
+
}))).catch(logRenderFailure).finally(() => {
|
|
423
440
|
it.return?.();
|
|
424
441
|
});
|
|
425
442
|
} catch (err) {
|
|
@@ -612,6 +629,7 @@ function durableChannelSurface(getNamespace, opts) {
|
|
|
612
629
|
},
|
|
613
630
|
services: resolveServices(opts.services, opts.env, opts.agentName),
|
|
614
631
|
waitUntil: opts.waitUntil,
|
|
632
|
+
files: opts.files,
|
|
615
633
|
run: async (message, o) => {
|
|
616
634
|
const namespace = getNamespace();
|
|
617
635
|
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
package/dist/agent-native.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SyncSqlite } from "./sqlite-driver.js";
|
|
2
2
|
import { AgentDefinition, ChannelContext } from "@junejs/core/agent-config";
|
|
3
3
|
import { AgentSession, ChannelPolicy, InputAnnouncement, Model, Runtime, Tool } from "@junejs/core/agent-runtime";
|
|
4
|
+
import { FileStore } from "@junejs/core/agent-files";
|
|
4
5
|
//#region src/agent-native.d.ts
|
|
5
6
|
export type AgentDef = {
|
|
6
7
|
model: Model;
|
|
@@ -12,6 +13,7 @@ export type AgentDef = {
|
|
|
12
13
|
export declare function toAgentDef(agent: AgentDefinition, model: Model): AgentDef;
|
|
13
14
|
export type NativeRuntimeOptions = {
|
|
14
15
|
maxSessions?: number;
|
|
16
|
+
files?: FileStore;
|
|
15
17
|
};
|
|
16
18
|
export declare class NativeRuntime implements Runtime {
|
|
17
19
|
private actors;
|
|
@@ -20,6 +22,7 @@ export declare class NativeRuntime implements Runtime {
|
|
|
20
22
|
private readonly announceRetry;
|
|
21
23
|
private readonly maxSessions;
|
|
22
24
|
private closing;
|
|
25
|
+
readonly files: FileStore | undefined;
|
|
23
26
|
constructor(agents: Record<string, AgentDef>, db: SyncSqlite, opts?: NativeRuntimeOptions);
|
|
24
27
|
agentDef(name: string): AgentDef | undefined;
|
|
25
28
|
session(agent: string, id: string): AgentSession;
|
|
@@ -35,7 +38,10 @@ export declare class MemoryRuntime implements Runtime {
|
|
|
35
38
|
private readonly agents;
|
|
36
39
|
private readonly announceRetry;
|
|
37
40
|
private closed;
|
|
38
|
-
|
|
41
|
+
readonly files: FileStore | undefined;
|
|
42
|
+
constructor(agents: Record<string, AgentDef>, opts?: {
|
|
43
|
+
files?: FileStore;
|
|
44
|
+
});
|
|
39
45
|
agentDef(name: string): AgentDef | undefined;
|
|
40
46
|
session(agent: string, id: string): AgentSession;
|
|
41
47
|
close(): Promise<void>;
|
|
@@ -43,16 +49,19 @@ export declare class MemoryRuntime implements Runtime {
|
|
|
43
49
|
export type AgentBackend = "native" | "memory" | "durable";
|
|
44
50
|
export type InProcessRuntime = Runtime & {
|
|
45
51
|
close(): Promise<void>;
|
|
52
|
+
readonly files?: FileStore;
|
|
46
53
|
};
|
|
47
54
|
export declare function createAgentRuntime(agents: Record<string, AgentDef>, opts?: {
|
|
48
55
|
backend?: AgentBackend;
|
|
49
56
|
path?: string;
|
|
50
57
|
maxSessions?: number;
|
|
58
|
+
files?: FileStore;
|
|
51
59
|
}): Promise<InProcessRuntime>;
|
|
52
60
|
export declare function mountAgent(agent: AgentDefinition, runtime: Runtime, opts?: {
|
|
53
61
|
chatPath?: string;
|
|
54
62
|
channels?: boolean;
|
|
55
63
|
services?: unknown;
|
|
64
|
+
files?: FileStore;
|
|
56
65
|
}): {
|
|
57
66
|
surface: (req: Request) => Promise<Response | null>;
|
|
58
67
|
fetch: (req: Request) => Promise<Response | null>;
|
package/dist/agent-native.js
CHANGED
|
@@ -158,10 +158,12 @@ var NativeRuntime = class {
|
|
|
158
158
|
});
|
|
159
159
|
maxSessions;
|
|
160
160
|
closing;
|
|
161
|
+
files;
|
|
161
162
|
constructor(agents, db, opts = {}) {
|
|
162
163
|
assertCoreRuntimeVersion("NativeRuntime");
|
|
163
164
|
this.agents = agents;
|
|
164
165
|
this.db = db;
|
|
166
|
+
this.files = opts.files;
|
|
165
167
|
const max = opts.maxSessions ?? 1e3;
|
|
166
168
|
if (!(Number.isInteger(max) && max >= 1) && max !== Infinity) throw new RangeError(`NativeRuntime: maxSessions must be an integer >= 1 (or Infinity for no cap), got ${max}`);
|
|
167
169
|
this.maxSessions = max;
|
|
@@ -183,7 +185,9 @@ var NativeRuntime = class {
|
|
|
183
185
|
if (!def) throw new Error(`unknown agent: ${agent}`);
|
|
184
186
|
const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
|
|
185
187
|
const sink = new InProcEventSink();
|
|
186
|
-
const
|
|
188
|
+
const built = new AgentSession(agent, id, new SqliteSessionStore(this.db, key), sink, model, def.tools, this, def.channelInstructions);
|
|
189
|
+
built.files = this.files;
|
|
190
|
+
const session = wireAnnouncements(built, def, this.announceRetry);
|
|
187
191
|
this.evictIdle();
|
|
188
192
|
this.actors.set(key, {
|
|
189
193
|
session,
|
|
@@ -294,9 +298,11 @@ var MemoryRuntime = class {
|
|
|
294
298
|
this.session(agent, id).flushAnnouncements();
|
|
295
299
|
});
|
|
296
300
|
closed = false;
|
|
297
|
-
|
|
301
|
+
files;
|
|
302
|
+
constructor(agents, opts = {}) {
|
|
298
303
|
assertCoreRuntimeVersion("MemoryRuntime");
|
|
299
304
|
this.agents = agents;
|
|
305
|
+
this.files = opts.files;
|
|
300
306
|
}
|
|
301
307
|
agentDef(name) {
|
|
302
308
|
return this.agents[name];
|
|
@@ -311,7 +317,9 @@ var MemoryRuntime = class {
|
|
|
311
317
|
const store = new MemorySessionStore();
|
|
312
318
|
this.stores.set(key, store);
|
|
313
319
|
const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
|
|
314
|
-
|
|
320
|
+
const built = new AgentSession(agent, id, store, new InProcEventSink(), model, def.tools, this, def.channelInstructions);
|
|
321
|
+
built.files = this.files;
|
|
322
|
+
a = wireAnnouncements(built, def, this.announceRetry);
|
|
315
323
|
this.actors.set(key, a);
|
|
316
324
|
}
|
|
317
325
|
return a;
|
|
@@ -325,8 +333,11 @@ var MemoryRuntime = class {
|
|
|
325
333
|
};
|
|
326
334
|
async function createAgentRuntime(agents, opts = {}) {
|
|
327
335
|
const backend = opts.backend ?? "native";
|
|
328
|
-
if (backend === "memory") return new MemoryRuntime(agents);
|
|
329
|
-
if (backend === "native") return createNativeRuntime(agents, opts.path, {
|
|
336
|
+
if (backend === "memory") return new MemoryRuntime(agents, { files: opts.files });
|
|
337
|
+
if (backend === "native") return createNativeRuntime(agents, opts.path, {
|
|
338
|
+
maxSessions: opts.maxSessions,
|
|
339
|
+
files: opts.files
|
|
340
|
+
});
|
|
330
341
|
throw new Error("backend 'durable' is the Cloudflare Durable Object target — construct AgentDurableObject in your worker, not via createAgentRuntime");
|
|
331
342
|
}
|
|
332
343
|
function warnOnDrift(agent, runtime) {
|
|
@@ -346,6 +357,7 @@ function mountAgent(agent, runtime, opts = {}) {
|
|
|
346
357
|
const ctx = {
|
|
347
358
|
agent,
|
|
348
359
|
services: opts.services,
|
|
360
|
+
files: opts.files ?? runtime.files,
|
|
349
361
|
run: (message, o) => runtime.session(agent.name, o?.session ?? "default").turn({
|
|
350
362
|
turnId: o?.turnId,
|
|
351
363
|
userText: message,
|
package/dist/app.js
CHANGED
|
@@ -5,6 +5,7 @@ import { routeProblems, scanRoutes } from "./route-scan.js";
|
|
|
5
5
|
import { createPipeline } from "./pipeline.js";
|
|
6
6
|
import { resolveBoundary } from "./segment.js";
|
|
7
7
|
import { memoizeResources } from "./resources.js";
|
|
8
|
+
import { localBlob } from "./blob.js";
|
|
8
9
|
import { MODULE_STYLES_URL, buildModuleCss, registerCssModules } from "./css-modules.js";
|
|
9
10
|
import { CLIENT_SCRIPT_URL, bundleClientSplit, findClientEntry } from "./client-bundle.js";
|
|
10
11
|
import { generateIslandRegistry } from "./island-registry.js";
|
|
@@ -15,6 +16,7 @@ import { existsSync, lstatSync, realpathSync } from "node:fs";
|
|
|
15
16
|
import { readFile } from "node:fs/promises";
|
|
16
17
|
import { dirname, join, resolve } from "node:path";
|
|
17
18
|
import { pathToFileURL } from "node:url";
|
|
19
|
+
import { blobFileStore, memoryFileStore } from "@junejs/core/agent-files";
|
|
18
20
|
import { routeFromModule } from "@junejs/core/route";
|
|
19
21
|
import { deployOrigin, resolveAgent, resolveClientRouter, resolveSpeculationRules } from "@junejs/core/config";
|
|
20
22
|
import { runWithTrace } from "@junejs/core/instrumentation";
|
|
@@ -139,7 +141,12 @@ function createApp({ appDir: appDirInput, config = {} }) {
|
|
|
139
141
|
const def = await discoverAgent(agentDir);
|
|
140
142
|
const backend = agent.runtime.backend === "durable" ? "native" : agent.runtime.backend;
|
|
141
143
|
const model = anthropic({ model: def.model });
|
|
142
|
-
|
|
144
|
+
let blobOpened;
|
|
145
|
+
const files = backend === "memory" ? memoryFileStore() : blobFileStore(() => blobOpened ??= resources().then(async (r) => r.blob ?? await localBlob().open()));
|
|
146
|
+
const rt = await createAgentRuntime({ [def.name]: toAgentDef(def, model) }, {
|
|
147
|
+
backend,
|
|
148
|
+
files
|
|
149
|
+
});
|
|
143
150
|
agentRuntime = rt;
|
|
144
151
|
const mounted = mountAgent(def, rt, {
|
|
145
152
|
chatPath: agent.runtime.chat.path,
|
package/dist/core-version.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as coreRuntime from "@junejs/core/agent-runtime";
|
|
2
2
|
//#region src/core-version.ts
|
|
3
|
-
const EXPECTED_CORE_RUNTIME_API =
|
|
3
|
+
const EXPECTED_CORE_RUNTIME_API = 7;
|
|
4
4
|
function assertCoreRuntimeVersion(surface) {
|
|
5
5
|
checkCoreRuntimeVersion(surface, coreRuntime.RUNTIME_API_VERSION);
|
|
6
6
|
}
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,8 @@ import { negotiate } from "./negotiate.js";
|
|
|
6
6
|
import { createPipeline } from "./pipeline.js";
|
|
7
7
|
import { d1 } from "./d1.js";
|
|
8
8
|
import { bindWorkerResources, memoizeResources } from "./resources.js";
|
|
9
|
-
import { createApp } from "./app.js";
|
|
10
9
|
import { localBlob, r2 } from "./blob.js";
|
|
10
|
+
import { createApp } from "./app.js";
|
|
11
11
|
import { loadJuneConfig } from "./config-loader.js";
|
|
12
12
|
import { freezeConfig } from "./config-freeze.js";
|
|
13
13
|
import { collection, entry } from "./content.js";
|
package/dist/sqlite-driver.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export declare function nodeSqliteHelp(nodeVersion: string): string;
|
|
|
19
19
|
export declare function isNodeSqliteExperimentalWarning(type: unknown, message: string): boolean;
|
|
20
20
|
type EmitWarning = (warning: string | Error, ...rest: unknown[]) => void;
|
|
21
21
|
export declare function makeWarningFilter(original: EmitWarning): EmitWarning;
|
|
22
|
+
export declare function useDurableJournal(db: SyncSqlite, path: string): SyncSqlite;
|
|
22
23
|
export declare function openLocalSqliteSync(path: string): Promise<SyncSqlite>;
|
|
23
24
|
export declare function openLocalSqlite(path: string): Promise<JuneDb>;
|
|
24
25
|
//#endregion
|
package/dist/sqlite-driver.js
CHANGED
|
@@ -72,10 +72,23 @@ function makeWarningFilter(original) {
|
|
|
72
72
|
original(warning, ...rest);
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
+
function useDurableJournal(db, path) {
|
|
76
|
+
if (path === ":memory:" || path === "") return db;
|
|
77
|
+
try {
|
|
78
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
79
|
+
db.exec("PRAGMA synchronous=FULL");
|
|
80
|
+
} catch (err) {
|
|
81
|
+
try {
|
|
82
|
+
db.close();
|
|
83
|
+
} catch {}
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
return db;
|
|
87
|
+
}
|
|
75
88
|
async function openLocalSqliteSync(path) {
|
|
76
89
|
if (typeof Bun !== "undefined") {
|
|
77
90
|
const { Database } = await import("bun:sqlite");
|
|
78
|
-
return new Database(path, { create: true });
|
|
91
|
+
return useDurableJournal(new Database(path, { create: true }), path);
|
|
79
92
|
}
|
|
80
93
|
const specifier = "node:sqlite";
|
|
81
94
|
let mod;
|
|
@@ -89,14 +102,14 @@ async function openLocalSqliteSync(path) {
|
|
|
89
102
|
process.emitWarning = originalEmitWarning;
|
|
90
103
|
}
|
|
91
104
|
const db = new mod.DatabaseSync(path);
|
|
92
|
-
return {
|
|
105
|
+
return useDurableJournal({
|
|
93
106
|
query: (sql) => db.prepare(sql),
|
|
94
107
|
exec: (sql) => db.exec(sql),
|
|
95
108
|
close: () => db.close()
|
|
96
|
-
};
|
|
109
|
+
}, path);
|
|
97
110
|
}
|
|
98
111
|
async function openLocalSqlite(path) {
|
|
99
112
|
return asyncSqlite(await openLocalSqliteSync(path));
|
|
100
113
|
}
|
|
101
114
|
//#endregion
|
|
102
|
-
export { NODE_SQLITE_MIN_LTS, NODE_SQLITE_MIN_ODD, isNodeSqliteExperimentalWarning, makeWarningFilter, nodeSqliteHelp, openLocalSqlite, openLocalSqliteSync };
|
|
115
|
+
export { NODE_SQLITE_MIN_LTS, NODE_SQLITE_MIN_ODD, isNodeSqliteExperimentalWarning, makeWarningFilter, nodeSqliteHelp, openLocalSqlite, openLocalSqliteSync, useDurableJournal };
|
package/dist/worker.js
CHANGED
|
@@ -3,6 +3,7 @@ import { compareRoutePatterns, parseSegment } from "./route-rank.js";
|
|
|
3
3
|
import { acceptTarget } from "./negotiate.js";
|
|
4
4
|
import { createPipeline } from "./pipeline.js";
|
|
5
5
|
import { RESERVED_PREFIX, contentTypeFor, safeRelativePath } from "./static-files.js";
|
|
6
|
+
import { blobFileStore } from "@junejs/core/agent-files";
|
|
6
7
|
//#region src/worker.ts
|
|
7
8
|
function compilePattern(pattern) {
|
|
8
9
|
const names = [];
|
|
@@ -88,7 +89,12 @@ function createWorker(manifest, opts = {}) {
|
|
|
88
89
|
channels: manifest.agentChannels,
|
|
89
90
|
env: locals?.env,
|
|
90
91
|
services: servicesProvider,
|
|
91
|
-
waitUntil: locals?.ctx?.waitUntil?.bind(locals.ctx)
|
|
92
|
+
waitUntil: locals?.ctx?.waitUntil?.bind(locals.ctx),
|
|
93
|
+
files: provider ? blobFileStore(async () => {
|
|
94
|
+
const blob = (await provider(locals?.env)).blob;
|
|
95
|
+
if (!blob) throw new Error(`agent "${agentName}": no blob resource bound in this isolate — attached files can't be read`);
|
|
96
|
+
return blob;
|
|
97
|
+
}) : void 0
|
|
92
98
|
})(req);
|
|
93
99
|
};
|
|
94
100
|
})() : void 0;
|
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.47",
|
|
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",
|
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
"rolldown": "^1.1.0"
|
|
141
141
|
},
|
|
142
142
|
"peerDependencies": {
|
|
143
|
-
"@junejs/core": ">=0.2.0-dev.
|
|
143
|
+
"@junejs/core": ">=0.2.0-dev.66",
|
|
144
144
|
"react": "^19.2.0",
|
|
145
145
|
"react-dom": "^19.2.0",
|
|
146
146
|
"react-server-dom-webpack": "19.3.0",
|
package/src/agent-durable.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
19
|
AgentSession,
|
|
20
|
+
FatalToolError,
|
|
20
21
|
grantAnswer,
|
|
21
22
|
ResumeAuthorizationError,
|
|
22
23
|
withSystem,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
} from "@junejs/core/agent-runtime";
|
|
40
41
|
import type { Principal } from "@junejs/core/context";
|
|
41
42
|
import type { Resources } from "@junejs/core/resources";
|
|
43
|
+
import { blobFileStore, type FileStore } from "@junejs/core/agent-files";
|
|
42
44
|
import { observeTurnEvents } from "./turn-events";
|
|
43
45
|
import {
|
|
44
46
|
channelDispatch,
|
|
@@ -255,6 +257,10 @@ export type DoAgentDef = {
|
|
|
255
257
|
// Receives this session's input announcements (#260) — run inside the request scope, so it
|
|
256
258
|
// can write the app's cross-session index with the ambient db.
|
|
257
259
|
onInputAnnouncement?: (a: InputAnnouncement) => void | Promise<void>;
|
|
260
|
+
// Where this session's tools store the files they produce (docs/rfc-agent-files.md), and
|
|
261
|
+
// where host-side delivery reads them back. Default: the `blob` of `resources` (an R2
|
|
262
|
+
// bucket), under agent-files/. Without either, a tool's ctx.files.put fails the turn.
|
|
263
|
+
files?: FileStore;
|
|
258
264
|
};
|
|
259
265
|
|
|
260
266
|
// The agent runtime INSIDE a Durable Object. A plain class (constructor takes the
|
|
@@ -300,6 +306,7 @@ export class AgentDurableObject {
|
|
|
300
306
|
private readonly authorizeAnswer?: AuthorizeAnswer;
|
|
301
307
|
private readonly onInputAnnouncement?: (a: InputAnnouncement) => void | Promise<void>;
|
|
302
308
|
private readonly storage: DurableStorage;
|
|
309
|
+
private readonly files: FileStore;
|
|
303
310
|
// Consecutive failed announcement deliveries (#260) — sets the alarm retry's backoff.
|
|
304
311
|
private announceFailures = 0;
|
|
305
312
|
private resolveResources(): Promise<Resources> {
|
|
@@ -347,6 +354,15 @@ export class AgentDurableObject {
|
|
|
347
354
|
this.onInputAnnouncement = def.onInputAnnouncement;
|
|
348
355
|
this.storage = state.storage;
|
|
349
356
|
const name = def.name ?? "agent";
|
|
357
|
+
// The blob resource opens with the rest of resources, lazily — the store is installed
|
|
358
|
+
// on the session before then, and only a tool that stores a file opens it.
|
|
359
|
+
this.files =
|
|
360
|
+
def.files ??
|
|
361
|
+
blobFileStore(async () => {
|
|
362
|
+
const blob = (await this.resolveResources()).blob;
|
|
363
|
+
if (!blob) throw new FatalToolError(`agent "${name}": no file store — declare a blob resource (DoAgentDef.resources) or pass DoAgentDef.files`);
|
|
364
|
+
return blob;
|
|
365
|
+
});
|
|
350
366
|
// Failure observability (#76): a turn that dies after the fast-ACK has no other
|
|
351
367
|
// observable surface on the edge — the webhook already 200'd and runBackground
|
|
352
368
|
// swallows the rejection unless the channel wired onError. Subscribing HERE (the
|
|
@@ -459,6 +475,7 @@ export class AgentDurableObject {
|
|
|
459
475
|
if (key !== undefined && key !== "self" && stored === undefined) this.store.setSessionKey(key);
|
|
460
476
|
this.sessionKey = resolved;
|
|
461
477
|
this.session = new AgentSession(this.name, resolved, this.store, this.sink, this.model, this.tools, crossDoUnsupported, this.channelInstructions);
|
|
478
|
+
this.session.files = this.files;
|
|
462
479
|
// A held turn (#263) that asked for delivery is rendered through its source channel when
|
|
463
480
|
// it finally runs — possibly in a later life of this object, which is why the ask rides
|
|
464
481
|
// in the turn's hostContext and this hook is installed on every rebuild.
|
|
@@ -537,7 +554,7 @@ export class AgentDurableObject {
|
|
|
537
554
|
const events: AsyncIterable<TurnEvent> = { [Symbol.asyncIterator]: () => it };
|
|
538
555
|
const target = { channelId: event.channelId, threadId: event.threadId, recipientUserId: event.user?.id, recipientTeamId: event.teamId };
|
|
539
556
|
this.ready()
|
|
540
|
-
.then((resources) => runInScope({ resources, services: this.services }, () => channel.deliver!(target, events, { session: this.sessionKey })))
|
|
557
|
+
.then((resources) => runInScope({ resources, services: this.services }, () => channel.deliver!(target, events, { session: this.sessionKey, files: this.files })))
|
|
541
558
|
.catch(logRenderFailure)
|
|
542
559
|
.finally(() => { void it.return?.(); });
|
|
543
560
|
} catch (err) {
|
|
@@ -682,7 +699,7 @@ export class AgentDurableObject {
|
|
|
682
699
|
const events: AsyncIterable<TurnEvent> = { [Symbol.asyncIterator]: () => it };
|
|
683
700
|
const target = { channelId: event!.channelId, threadId: event!.threadId, recipientUserId: event!.user?.id, recipientTeamId: event!.teamId };
|
|
684
701
|
Promise.resolve()
|
|
685
|
-
.then(() => runInScope({ resources, services: this.services }, () => deliverChannel!.deliver!(target, events, { session: this.sessionKey })))
|
|
702
|
+
.then(() => runInScope({ resources, services: this.services }, () => deliverChannel!.deliver!(target, events, { session: this.sessionKey, files: this.files })))
|
|
686
703
|
.catch(logRenderFailure)
|
|
687
704
|
.finally(() => { void it.return?.(); });
|
|
688
705
|
} catch (err) {
|
|
@@ -770,7 +787,7 @@ export class AgentDurableObject {
|
|
|
770
787
|
const it = observeTurnEvents(session, turnId)[Symbol.asyncIterator]();
|
|
771
788
|
const events: AsyncIterable<TurnEvent> = { [Symbol.asyncIterator]: () => it };
|
|
772
789
|
Promise.resolve()
|
|
773
|
-
.then(() => runInScope({ resources, services: this.services }, () => resumeChannel!.deliverResume!(target!, events, { session: this.sessionKey })))
|
|
790
|
+
.then(() => runInScope({ resources, services: this.services }, () => resumeChannel!.deliverResume!(target!, events, { session: this.sessionKey, files: this.files })))
|
|
774
791
|
.catch(logRenderFailure)
|
|
775
792
|
.finally(() => { void it.return?.(); });
|
|
776
793
|
} catch (err) {
|
|
@@ -1079,6 +1096,9 @@ export function durableChannelSurface(
|
|
|
1079
1096
|
// agentName must have one provider.
|
|
1080
1097
|
services?: (env: unknown) => unknown;
|
|
1081
1098
|
waitUntil?: (p: Promise<unknown>) => void;
|
|
1099
|
+
// Where the channels read the files a turn attached (docs/rfc-agent-files.md) — the
|
|
1100
|
+
// same storage the Durable Object's tools write, opened from this isolate's env.
|
|
1101
|
+
files?: FileStore;
|
|
1082
1102
|
},
|
|
1083
1103
|
): (req: Request) => Promise<Response | null> {
|
|
1084
1104
|
const resolved = opts.channels.map((c) => resolveChannel(c, opts.env));
|
|
@@ -1088,6 +1108,7 @@ export function durableChannelSurface(
|
|
|
1088
1108
|
agent: { name: opts.agentName, instructions: "", tools: [], skills: [], channels: [], connections: [] } satisfies AgentDefinition,
|
|
1089
1109
|
services: resolveServices(opts.services, opts.env, opts.agentName),
|
|
1090
1110
|
waitUntil: opts.waitUntil,
|
|
1111
|
+
files: opts.files,
|
|
1091
1112
|
run: async (message, o) => {
|
|
1092
1113
|
const namespace = getNamespace();
|
|
1093
1114
|
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
package/src/agent-native.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// SessionStore = a session-scoped view over one shared synchronous SQLite handle
|
|
4
4
|
// (bun:sqlite under Bun, node:sqlite under Node — the same handle openLocalSqlite
|
|
5
5
|
// wraps as the async JuneDb, opened here directly because the durability tx must
|
|
6
|
-
// be synchronous).
|
|
6
|
+
// be synchronous). EventSink = an in-process subscriber set. Turn serialization
|
|
7
7
|
// comes from the AgentSession actor in core. On the edge target this same shape
|
|
8
|
-
// is reimplemented over a Durable Object's ctx.storage.sql (
|
|
8
|
+
// is reimplemented over a Durable Object's ctx.storage.sql (agent-durable.ts).
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
AgentSession,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type Tool,
|
|
23
23
|
} from "@junejs/core/agent-runtime";
|
|
24
24
|
import { buildSystemPrompt, channelFetch, type AgentDefinition, type ChannelContext } from "@junejs/core/agent-config";
|
|
25
|
+
import type { FileStore } from "@junejs/core/agent-files";
|
|
25
26
|
import { openLocalSqliteSync, type SyncSqlite } from "./sqlite-driver";
|
|
26
27
|
import { assertCoreRuntimeVersion } from "./core-version";
|
|
27
28
|
import { observeTurnEvents } from "./turn-events";
|
|
@@ -221,6 +222,10 @@ export type NativeRuntimeOptions = {
|
|
|
221
222
|
// dropped; their state is in SQLite, so the next session() rebuilds them transparently.
|
|
222
223
|
// Busy actors are never dropped, so the count can exceed the cap while they run.
|
|
223
224
|
maxSessions?: number;
|
|
225
|
+
// Where the sessions' tools store the files they produce (docs/rfc-agent-files.md), and
|
|
226
|
+
// where mountAgent's channels read them back to deliver. Absent: ctx.files.put fails the
|
|
227
|
+
// turn naming the missing configuration.
|
|
228
|
+
files?: FileStore;
|
|
224
229
|
};
|
|
225
230
|
|
|
226
231
|
// The native Runtime: a registry of agent definitions over one SQLite handle,
|
|
@@ -238,11 +243,13 @@ export class NativeRuntime implements Runtime {
|
|
|
238
243
|
private readonly announceRetry = new AnnouncementRetry((agent, id) => { void this.session(agent, id).flushAnnouncements(); });
|
|
239
244
|
private readonly maxSessions: number;
|
|
240
245
|
private closing: Promise<void> | undefined; // set by close() (#317)
|
|
246
|
+
readonly files: FileStore | undefined;
|
|
241
247
|
|
|
242
248
|
constructor(agents: Record<string, AgentDef>, db: SyncSqlite, opts: NativeRuntimeOptions = {}) {
|
|
243
249
|
assertCoreRuntimeVersion("NativeRuntime"); // #94: fail power-on, not mid-turn
|
|
244
250
|
this.agents = agents;
|
|
245
251
|
this.db = db;
|
|
252
|
+
this.files = opts.files;
|
|
246
253
|
const max = opts.maxSessions ?? 1000;
|
|
247
254
|
// At least one actor is always retained (the one being handed out), so a cap below 1
|
|
248
255
|
// means nothing, and NaN would compare false and evict every idle actor on each miss.
|
|
@@ -272,7 +279,9 @@ export class NativeRuntime implements Runtime {
|
|
|
272
279
|
if (!def) throw new Error(`unknown agent: ${agent}`);
|
|
273
280
|
const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
|
|
274
281
|
const sink = new InProcEventSink();
|
|
275
|
-
const
|
|
282
|
+
const built = new AgentSession(agent, id, new SqliteSessionStore(this.db, key), sink, model, def.tools, this, def.channelInstructions);
|
|
283
|
+
built.files = this.files;
|
|
284
|
+
const session = wireAnnouncements(built, def, this.announceRetry);
|
|
276
285
|
this.evictIdle();
|
|
277
286
|
this.actors.set(key, { session, sink });
|
|
278
287
|
return session;
|
|
@@ -397,9 +406,11 @@ export class MemoryRuntime implements Runtime {
|
|
|
397
406
|
private readonly agents: Record<string, AgentDef>;
|
|
398
407
|
private readonly announceRetry = new AnnouncementRetry((agent, id) => { void this.session(agent, id).flushAnnouncements(); }); // #260
|
|
399
408
|
private closed = false; // set by close() (#317)
|
|
400
|
-
|
|
409
|
+
readonly files: FileStore | undefined;
|
|
410
|
+
constructor(agents: Record<string, AgentDef>, opts: { files?: FileStore } = {}) {
|
|
401
411
|
assertCoreRuntimeVersion("MemoryRuntime"); // #94: fail power-on, not mid-turn
|
|
402
412
|
this.agents = agents;
|
|
413
|
+
this.files = opts.files;
|
|
403
414
|
}
|
|
404
415
|
agentDef(name: string): AgentDef | undefined { return this.agents[name]; }
|
|
405
416
|
session(agent: string, id: string): AgentSession {
|
|
@@ -413,7 +424,9 @@ export class MemoryRuntime implements Runtime {
|
|
|
413
424
|
this.stores.set(key, store);
|
|
414
425
|
// Same def handling as NativeRuntime: switching backend must not change behavior.
|
|
415
426
|
const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
|
|
416
|
-
|
|
427
|
+
const built = new AgentSession(agent, id, store, new InProcEventSink(), model, def.tools, this, def.channelInstructions);
|
|
428
|
+
built.files = this.files;
|
|
429
|
+
a = wireAnnouncements(built, def, this.announceRetry);
|
|
417
430
|
this.actors.set(key, a);
|
|
418
431
|
}
|
|
419
432
|
return a;
|
|
@@ -440,15 +453,15 @@ export type AgentBackend = "native" | "memory" | "durable";
|
|
|
440
453
|
// target is the DO the worker constructs, not an in-process object) — so the
|
|
441
454
|
// choice is explicit and a mis-selection fails loudly.
|
|
442
455
|
// An in-process runtime: a Runtime the host owns, so it can shut it down (#317).
|
|
443
|
-
export type InProcessRuntime = Runtime & { close(): Promise<void
|
|
456
|
+
export type InProcessRuntime = Runtime & { close(): Promise<void>; readonly files?: FileStore };
|
|
444
457
|
|
|
445
458
|
export async function createAgentRuntime(
|
|
446
459
|
agents: Record<string, AgentDef>,
|
|
447
|
-
opts: { backend?: AgentBackend; path?: string; maxSessions?: number } = {},
|
|
460
|
+
opts: { backend?: AgentBackend; path?: string; maxSessions?: number; files?: FileStore } = {},
|
|
448
461
|
): Promise<InProcessRuntime> {
|
|
449
462
|
const backend = opts.backend ?? "native";
|
|
450
|
-
if (backend === "memory") return new MemoryRuntime(agents);
|
|
451
|
-
if (backend === "native") return createNativeRuntime(agents, opts.path, { maxSessions: opts.maxSessions });
|
|
463
|
+
if (backend === "memory") return new MemoryRuntime(agents, { files: opts.files });
|
|
464
|
+
if (backend === "native") return createNativeRuntime(agents, opts.path, { maxSessions: opts.maxSessions, files: opts.files });
|
|
452
465
|
throw new Error("backend 'durable' is the Cloudflare Durable Object target — construct AgentDurableObject in your worker, not via createAgentRuntime");
|
|
453
466
|
}
|
|
454
467
|
|
|
@@ -485,7 +498,7 @@ function warnOnDrift(agent: AgentDefinition, runtime: Runtime) {
|
|
|
485
498
|
export function mountAgent(
|
|
486
499
|
agent: AgentDefinition,
|
|
487
500
|
runtime: Runtime,
|
|
488
|
-
opts: { chatPath?: string; channels?: boolean; services?: unknown } = {},
|
|
501
|
+
opts: { chatPath?: string; channels?: boolean; services?: unknown; files?: FileStore } = {},
|
|
489
502
|
): {
|
|
490
503
|
surface: (req: Request) => Promise<Response | null>;
|
|
491
504
|
fetch: (req: Request) => Promise<Response | null>;
|
|
@@ -498,6 +511,8 @@ export function mountAgent(
|
|
|
498
511
|
const ctx: ChannelContext = {
|
|
499
512
|
agent,
|
|
500
513
|
services: opts.services, // same DI bag reachable from channel hooks (parity with durableChannelSurface)
|
|
514
|
+
// The runtime's file store, so a channel can deliver the files the turn's tools stored.
|
|
515
|
+
files: opts.files ?? (runtime as { files?: FileStore }).files,
|
|
501
516
|
run: (message, o) =>
|
|
502
517
|
runtime.session(agent.name, o?.session ?? "default").turn({ turnId: o?.turnId, userText: message, event: o?.event, trigger: o?.trigger, replace: o?.replace }),
|
|
503
518
|
// FIRE-AND-FORGET (#77): start() without awaiting the result. Native has no waitUntil
|
package/src/app.ts
CHANGED
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
} from "@junejs/core/config";
|
|
26
26
|
import type { DocumentConfig } from "@junejs/core/document";
|
|
27
27
|
import { runWithTrace, type RequestTrace } from "@junejs/core/instrumentation";
|
|
28
|
+
import { blobFileStore, memoryFileStore } from "@junejs/core/agent-files";
|
|
29
|
+
import type { JuneBlob } from "@junejs/core/resources";
|
|
28
30
|
|
|
29
31
|
import { findMiddlewareFile, isResourceFile, listRoutes, matchRouteTree, resolveNotFound, routeFiles, type SegmentMatch } from "./router";
|
|
30
32
|
import { routeProblems, scanRoutes } from "./route-scan";
|
|
@@ -34,6 +36,7 @@ import { createAgentRuntime, mountAgent, toAgentDef, type InProcessRuntime } fro
|
|
|
34
36
|
import { anthropic } from "@junejs/core/agent-models";
|
|
35
37
|
import { resolveBoundary } from "./segment";
|
|
36
38
|
import { memoizeResources } from "./resources";
|
|
39
|
+
import { localBlob } from "./blob";
|
|
37
40
|
import {
|
|
38
41
|
findClientEntry,
|
|
39
42
|
bundleClientSplit,
|
|
@@ -270,7 +273,15 @@ export function createApp({ appDir: appDirInput, config = {} }: CreateAppOptions
|
|
|
270
273
|
const model = anthropic({ model: def.model });
|
|
271
274
|
// toAgentDef: tools, prompt AND the per-surface policies (#149) from the one
|
|
272
275
|
// definition — channelInstructions was previously dropped on this path (#173).
|
|
273
|
-
|
|
276
|
+
// Agent files (docs/rfc-agent-files.md) live in the app's blob resource; an app that
|
|
277
|
+
// declares none gets the local default directory. The memory backend keeps them in
|
|
278
|
+
// memory, like everything else it holds.
|
|
279
|
+
let blobOpened: Promise<JuneBlob> | undefined;
|
|
280
|
+
const files =
|
|
281
|
+
backend === "memory"
|
|
282
|
+
? memoryFileStore()
|
|
283
|
+
: blobFileStore(() => (blobOpened ??= resources().then(async (r) => r.blob ?? (await localBlob().open()))));
|
|
284
|
+
const rt = await createAgentRuntime({ [def.name]: toAgentDef(def, model) }, { backend, files });
|
|
274
285
|
agentRuntime = rt;
|
|
275
286
|
const mounted = mountAgent(def, rt, { chatPath: agent.runtime.chat.path, channels: agent.runtime.channels });
|
|
276
287
|
agentSurface = (req) => mounted.surface(req);
|
package/src/core-version.ts
CHANGED
|
@@ -15,7 +15,7 @@ import * as coreRuntime from "@junejs/core/agent-runtime";
|
|
|
15
15
|
|
|
16
16
|
// The contract number this server tree is built against. Bump in lockstep with
|
|
17
17
|
// core's RUNTIME_API_VERSION whenever the server↔core runtime contract changes.
|
|
18
|
-
const EXPECTED_CORE_RUNTIME_API =
|
|
18
|
+
const EXPECTED_CORE_RUNTIME_API = 7;
|
|
19
19
|
|
|
20
20
|
export function assertCoreRuntimeVersion(surface: string): void {
|
|
21
21
|
checkCoreRuntimeVersion(surface, (coreRuntime as { RUNTIME_API_VERSION?: number }).RUNTIME_API_VERSION);
|
package/src/sqlite-driver.ts
CHANGED
|
@@ -135,6 +135,33 @@ export function makeWarningFilter(original: EmitWarning): EmitWarning {
|
|
|
135
135
|
// runtime, which is exactly what the typeof guard checks.
|
|
136
136
|
declare const Bun: unknown;
|
|
137
137
|
|
|
138
|
+
// A file database commits durably: WAL, fsynced on every commit. SQLite's default
|
|
139
|
+
// rollback journal (DELETE mode) commits by unlinking the journal, and at
|
|
140
|
+
// synchronous=FULL it does not fsync the directory after that unlink — so a power
|
|
141
|
+
// cut right after a commit can bring the journal back, and the next open rolls the
|
|
142
|
+
// committed transaction back. On a VM power cut that lost the last committed agent
|
|
143
|
+
// turn 5 times out of 5; with WAL + FULL, 0 of 5. FULL is set explicitly: it is a
|
|
144
|
+
// build-time default, and WAL at synchronous=NORMAL skips the per-commit fsync.
|
|
145
|
+
// journal_mode=WAL persists in the file; `:memory:` has no journal to change.
|
|
146
|
+
// A PRAGMA that throws (a locked file mid-switch) closes the handle it was handed,
|
|
147
|
+
// so a retry is not blocked by a descriptor nobody can reach; the original error
|
|
148
|
+
// is what surfaces. Exported for the unit test of that path.
|
|
149
|
+
export function useDurableJournal(db: SyncSqlite, path: string): SyncSqlite {
|
|
150
|
+
if (path === ":memory:" || path === "") return db;
|
|
151
|
+
try {
|
|
152
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
153
|
+
db.exec("PRAGMA synchronous=FULL");
|
|
154
|
+
} catch (err) {
|
|
155
|
+
try {
|
|
156
|
+
db.close();
|
|
157
|
+
} catch {
|
|
158
|
+
// the setup error is the one worth reporting
|
|
159
|
+
}
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
return db;
|
|
163
|
+
}
|
|
164
|
+
|
|
138
165
|
// Open a LOCAL *synchronous* sqlite handle on whichever runtime we're under —
|
|
139
166
|
// bun:sqlite under Bun, node:sqlite under Node. This is the raw synchronous
|
|
140
167
|
// surface: asyncSqlite() wraps it as the edge-safe async JuneDb (below), while
|
|
@@ -148,7 +175,7 @@ export async function openLocalSqliteSync(path: string): Promise<SyncSqlite> {
|
|
|
148
175
|
const { Database } = (await import(specifier)) as {
|
|
149
176
|
Database: new (p: string, o?: { create?: boolean }) => SyncSqlite;
|
|
150
177
|
};
|
|
151
|
-
return new Database(path, { create: true });
|
|
178
|
+
return useDurableJournal(new Database(path, { create: true }), path);
|
|
152
179
|
}
|
|
153
180
|
|
|
154
181
|
// Node: node:sqlite is a builtin, but only flag-free on a recent-enough
|
|
@@ -172,11 +199,14 @@ export async function openLocalSqliteSync(path: string): Promise<SyncSqlite> {
|
|
|
172
199
|
}
|
|
173
200
|
const db = new mod.DatabaseSync(path);
|
|
174
201
|
// Adapt node:sqlite (prepare()) to the query()-shaped SyncSqlite surface.
|
|
175
|
-
return
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
202
|
+
return useDurableJournal(
|
|
203
|
+
{
|
|
204
|
+
query: (sql) => db.prepare(sql),
|
|
205
|
+
exec: (sql) => db.exec(sql),
|
|
206
|
+
close: () => db.close(),
|
|
207
|
+
},
|
|
208
|
+
path,
|
|
209
|
+
);
|
|
180
210
|
}
|
|
181
211
|
|
|
182
212
|
// Open a LOCAL sqlite file (or ":memory:") as the async JuneDb — the single seam
|
package/src/worker.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { I18nConfig } from "@junejs/core/i18n";
|
|
|
18
18
|
import type { Resources } from "@junejs/core/resources";
|
|
19
19
|
|
|
20
20
|
import type { Channel, ChannelFactory } from "@junejs/core/agent-config";
|
|
21
|
+
import { blobFileStore } from "@junejs/core/agent-files";
|
|
21
22
|
|
|
22
23
|
import { createPipeline, type ExtraHandler, type LayoutComponent, type LoadingComponent, type Resolved, type ResolvedResource, type ResourceHandler } from "./pipeline";
|
|
23
24
|
import { durableAgentSurface, durableChannelSurface, type DurableObjectNamespace } from "./agent-durable";
|
|
@@ -219,6 +220,15 @@ export function createWorker(
|
|
|
219
220
|
env: locals?.env,
|
|
220
221
|
services: servicesProvider,
|
|
221
222
|
waitUntil: locals?.ctx?.waitUntil?.bind(locals.ctx),
|
|
223
|
+
// The blob resource the agent's Durable Object stores files in, opened from
|
|
224
|
+
// this isolate's env — the channels read attached files back from it.
|
|
225
|
+
files: provider
|
|
226
|
+
? blobFileStore(async () => {
|
|
227
|
+
const blob = (await provider(locals?.env)).blob;
|
|
228
|
+
if (!blob) throw new Error(`agent "${agentName}": no blob resource bound in this isolate — attached files can't be read`);
|
|
229
|
+
return blob;
|
|
230
|
+
})
|
|
231
|
+
: undefined,
|
|
222
232
|
});
|
|
223
233
|
return channels(req);
|
|
224
234
|
};
|