@junejs/server 0.1.2 → 0.2.0

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.
Files changed (130) hide show
  1. package/assets/icon-latin.LICENSE.txt +92 -0
  2. package/assets/icon-latin.ttf +0 -0
  3. package/dist/adapter-DcZ-W1-7.d.ts +62 -0
  4. package/dist/adapter.d.ts +2 -56
  5. package/dist/adapter.js +62 -5
  6. package/dist/agent-compile.d.ts +38 -0
  7. package/dist/agent-compile.js +145 -0
  8. package/dist/agent-discover.d.ts +4 -4
  9. package/dist/agent-discover.js +30 -44
  10. package/dist/agent-durable.d.ts +94 -19
  11. package/dist/agent-durable.js +647 -32
  12. package/dist/agent-native.d.ts +44 -13
  13. package/dist/agent-native.js +264 -27
  14. package/dist/app.d.ts +5 -5
  15. package/dist/app.js +42 -10
  16. package/dist/blob.d.ts +4 -5
  17. package/dist/build.d.ts +8 -29
  18. package/dist/build.js +138 -289
  19. package/dist/client-bundle.d.ts +8 -9
  20. package/dist/client-bundle.js +1 -1
  21. package/dist/config-freeze-CyXvNqi2.d.ts +17 -0
  22. package/dist/config-freeze.d.ts +2 -0
  23. package/dist/config-freeze.js +54 -0
  24. package/dist/config-loader.d.ts +3 -4
  25. package/dist/connection-auth.d.ts +38 -0
  26. package/dist/connection-auth.js +31 -0
  27. package/dist/content-freeze-CyiW2fRK.d.ts +4 -0
  28. package/dist/content-freeze.d.ts +2 -0
  29. package/dist/content-freeze.js +83 -0
  30. package/dist/content.d.ts +22 -9
  31. package/dist/content.js +43 -3
  32. package/dist/core-version.d.ts +4 -0
  33. package/dist/core-version.js +12 -0
  34. package/dist/css-modules-ambient.d.ts +2 -1
  35. package/dist/css-modules.d.ts +10 -11
  36. package/dist/css.d.ts +9 -10
  37. package/dist/d1-migrate.d.ts +6 -7
  38. package/dist/d1-migrate.js +3 -2
  39. package/dist/db.d.ts +2 -2
  40. package/dist/deploy.d.ts +3 -4
  41. package/dist/deploy.js +3 -2
  42. package/dist/dev-reload.d.ts +3 -4
  43. package/dist/dev.d.ts +8 -5
  44. package/dist/dev.js +40 -9
  45. package/dist/favicon.d.ts +37 -0
  46. package/dist/favicon.js +248 -0
  47. package/dist/host.d.ts +6 -5
  48. package/dist/host.js +2 -1
  49. package/dist/icon-letter.d.ts +3 -0
  50. package/dist/icon-letter.js +8 -0
  51. package/dist/index.d.ts +11 -5
  52. package/dist/index.js +9 -3
  53. package/dist/instrumentation.d.ts +2 -3
  54. package/dist/instrumentation.js +2 -1
  55. package/dist/island-registry.d.ts +6 -7
  56. package/dist/isolate-local.d.ts +5 -0
  57. package/dist/isolate-local.js +12 -0
  58. package/dist/kv.d.ts +3 -4
  59. package/dist/llms-links.d.ts +9 -0
  60. package/dist/llms-links.js +65 -0
  61. package/dist/manifest-CwTT1zVs.d.ts +11 -0
  62. package/dist/manifest.d.ts +2 -0
  63. package/dist/manifest.js +113 -0
  64. package/dist/negotiate.d.ts +4 -3
  65. package/dist/negotiate.js +24 -14
  66. package/dist/{pipeline-CCp9idQ4.d.ts → pipeline-BHUjBO0G.d.ts} +13 -1
  67. package/dist/pipeline.d.ts +2 -2
  68. package/dist/pipeline.js +286 -35
  69. package/dist/resources.d.ts +5 -6
  70. package/dist/route-rank.d.ts +10 -0
  71. package/dist/route-rank.js +38 -0
  72. package/dist/route-scan-BMbBzPIB.d.ts +20 -0
  73. package/dist/route-scan.d.ts +2 -0
  74. package/dist/route-scan.js +100 -0
  75. package/dist/router.d.ts +12 -13
  76. package/dist/router.js +58 -44
  77. package/dist/rsc-build.d.ts +6 -7
  78. package/dist/rsc-bundle.d.ts +6 -7
  79. package/dist/rsc-manifest.d.ts +5 -6
  80. package/dist/rsc-runtime/flight-render-routes.d.ts +3 -4
  81. package/dist/rsc-runtime/flight-render.d.ts +2 -3
  82. package/dist/rsc-runtime/flight-to-html.d.ts +2 -3
  83. package/dist/rsc-runtime/flight-to-html.js +3 -2
  84. package/dist/rsc-runtime/rsc-worker.js +11 -8
  85. package/dist/rsc-runtime/rsdw-shims.d.ts +2 -1
  86. package/dist/segment.d.ts +4 -5
  87. package/dist/sqlite-driver.d.ts +11 -11
  88. package/dist/sqlite-driver.js +17 -4
  89. package/dist/static-files.d.ts +4 -5
  90. package/dist/tsconfig-jsx.d.ts +4 -4
  91. package/dist/tsconfig-jsx.js +1 -1
  92. package/dist/turn-events.d.ts +4 -0
  93. package/dist/turn-events.js +46 -0
  94. package/dist/web-manifest.d.ts +5 -0
  95. package/dist/web-manifest.js +42 -0
  96. package/dist/worker-2qbQjj2c.d.ts +64 -0
  97. package/dist/worker.d.ts +2 -56
  98. package/dist/worker.js +121 -103
  99. package/package.json +22 -9
  100. package/src/adapter.ts +84 -3
  101. package/src/agent-compile.ts +271 -0
  102. package/src/agent-discover.ts +77 -63
  103. package/src/agent-durable.ts +998 -30
  104. package/src/agent-native.ts +370 -35
  105. package/src/app.ts +75 -6
  106. package/src/build.ts +237 -444
  107. package/src/config-freeze.ts +109 -0
  108. package/src/connection-auth.ts +101 -0
  109. package/src/content-freeze.ts +151 -0
  110. package/src/content.ts +55 -4
  111. package/src/core-version.ts +34 -0
  112. package/src/dev.ts +41 -8
  113. package/src/favicon.ts +325 -0
  114. package/src/host.ts +4 -3
  115. package/src/icon-letter.ts +14 -0
  116. package/src/index.ts +22 -0
  117. package/src/isolate-local.ts +33 -0
  118. package/src/llms-links.ts +96 -0
  119. package/src/manifest.ts +130 -0
  120. package/src/negotiate.ts +39 -15
  121. package/src/pipeline.ts +494 -44
  122. package/src/route-rank.ts +60 -0
  123. package/src/route-scan.ts +161 -0
  124. package/src/router.ts +75 -58
  125. package/src/rsc-runtime/rsc-worker.tsx +5 -4
  126. package/src/sqlite-driver.ts +36 -6
  127. package/src/tsconfig-jsx.ts +2 -2
  128. package/src/turn-events.ts +43 -0
  129. package/src/web-manifest.ts +38 -0
  130. package/src/worker.ts +197 -117
@@ -1,5 +1,10 @@
1
- import { channelDispatch, resolveChannel } from "@junejs/core/agent-config";
2
- import { AgentSession, withSystem } from "@junejs/core/agent-runtime";
1
+ import { observeTurnEvents } from "./turn-events.js";
2
+ import { isolateLocal } from "./isolate-local.js";
3
+ import { assertCoreRuntimeVersion } from "./core-version.js";
4
+ import { DeliverUnsupportedError, actionToTool, channelDispatch, resolveChannel } from "@junejs/core/agent-config";
5
+ import { AgentSession, FatalToolError, ResumeAuthorizationError, grantAnswer, withSystem } from "@junejs/core/agent-runtime";
6
+ import { blobFileStore } from "@junejs/core/agent-files";
7
+ import { connectAll } from "@junejs/core/connections";
3
8
  import { ensureScope, runInScope } from "@junejs/db";
4
9
  //#region src/agent-durable.ts
5
10
  var DoSessionStore = class {
@@ -20,8 +25,8 @@ var DoSessionStore = class {
20
25
  messages() {
21
26
  return this.sql.exec("SELECT body FROM agent_messages ORDER BY seq").toArray().map((r) => JSON.parse(r.body));
22
27
  }
23
- hasUserTurn(turnId) {
24
- return this.messages().some((m) => m.role === "user" && m.turnId === turnId);
28
+ hasOpeningMessage(turnId) {
29
+ return this.messages().some((m) => (m.role === "user" || m.role === "trigger") && m.turnId === turnId);
25
30
  }
26
31
  getStep(id) {
27
32
  const rows = this.sql.exec("SELECT output FROM agent_steps WHERE id = ?", id).toArray();
@@ -30,6 +35,9 @@ var DoSessionStore = class {
30
35
  putStep(id, output) {
31
36
  this.sql.exec("INSERT INTO agent_steps (id, output) VALUES (?, ?)", id, JSON.stringify(output));
32
37
  }
38
+ delStep(id) {
39
+ this.sql.exec("DELETE FROM agent_steps WHERE id = ?", id);
40
+ }
33
41
  getStatus() {
34
42
  const rows = this.sql.exec("SELECT v FROM agent_meta WHERE k = 'status'").toArray();
35
43
  return rows.length ? rows[0].v : "new";
@@ -37,19 +45,43 @@ var DoSessionStore = class {
37
45
  setStatus(s) {
38
46
  this.sql.exec("INSERT INTO agent_meta (k, v) VALUES ('status', ?) ON CONFLICT(k) DO UPDATE SET v = ?", s, s);
39
47
  }
48
+ getSessionKey() {
49
+ const rows = this.sql.exec("SELECT v FROM agent_meta WHERE k = 'session_key'").toArray();
50
+ return rows.length ? rows[0].v : void 0;
51
+ }
52
+ setSessionKey(key) {
53
+ this.sql.exec("INSERT INTO agent_meta (k, v) VALUES ('session_key', ?) ON CONFLICT(k) DO UPDATE SET v = ?", key, key);
54
+ }
40
55
  tx(fn) {
41
56
  return this.storage.transactionSync(fn);
42
57
  }
58
+ reset(inTx) {
59
+ return this.storage.transactionSync(() => {
60
+ const sql = this.sql;
61
+ sql.exec(`CREATE TABLE IF NOT EXISTS agent_messages_archive (generation INTEGER, seq INTEGER, body TEXT)`);
62
+ sql.exec(`CREATE TABLE IF NOT EXISTS agent_steps_archive (generation INTEGER, id TEXT, output TEXT)`);
63
+ const rows = this.sql.exec("SELECT v FROM agent_meta WHERE k = 'generation'").toArray();
64
+ const generation = rows.length ? Number(rows[0].v) : 0;
65
+ sql.exec("INSERT INTO agent_messages_archive (generation, seq, body) SELECT ?, seq, body FROM agent_messages", generation);
66
+ sql.exec("DELETE FROM agent_messages");
67
+ sql.exec("INSERT INTO agent_steps_archive (generation, id, output) SELECT ?, id, output FROM agent_steps", generation);
68
+ sql.exec("DELETE FROM agent_steps");
69
+ this.setStatus("new");
70
+ sql.exec("INSERT INTO agent_meta (k, v) VALUES ('generation', ?) ON CONFLICT(k) DO UPDATE SET v = ?", String(generation + 1), String(generation + 1));
71
+ inTx?.();
72
+ return generation;
73
+ });
74
+ }
43
75
  unwrap() {
44
76
  return this.sql;
45
77
  }
46
78
  };
47
- var InProcBroadcaster = class {
79
+ var InProcEventSink = class {
48
80
  subs = /* @__PURE__ */ new Set();
49
- publish(turnId) {
81
+ emit(e) {
50
82
  this.subs.forEach((cb) => {
51
83
  try {
52
- cb(turnId);
84
+ cb(e);
53
85
  } catch {}
54
86
  });
55
87
  }
@@ -63,40 +95,493 @@ const crossDoUnsupported = { session() {
63
95
  } };
64
96
  var AgentDurableObject = class {
65
97
  session;
66
- resources;
98
+ sessionKey;
99
+ name;
100
+ store;
101
+ sink;
102
+ model;
103
+ tools;
104
+ channels;
105
+ channelInstructions;
106
+ resourcesInput;
107
+ resourcesOpened;
108
+ connectionsInput;
109
+ connectionsOpened;
110
+ doEnv;
67
111
  services;
112
+ authorizeAnswer;
113
+ onInputAnnouncement;
114
+ storage;
115
+ files;
116
+ announceFailures = 0;
117
+ resolveResources() {
118
+ return this.resourcesOpened ??= Promise.resolve(typeof this.resourcesInput === "function" ? this.resourcesInput(this.doEnv) : this.resourcesInput ?? {});
119
+ }
120
+ resolveConnections() {
121
+ return this.connectionsOpened ??= (async () => {
122
+ if (!this.connectionsInput?.length) return;
123
+ const { actions, report } = await connectAll(this.connectionsInput);
124
+ for (const r of report) if (r.error) console.error(`[june] agent "${this.name}": connection "${r.name}" (${r.url}) failed: ${r.error}`);
125
+ const seen = new Set(this.tools.map((t) => t.spec.name));
126
+ for (const a of actions) {
127
+ const tool = actionToTool(a);
128
+ 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.`);
129
+ seen.add(tool.spec.name);
130
+ this.tools.push(tool);
131
+ }
132
+ })();
133
+ }
134
+ async ready() {
135
+ const [resources] = await Promise.all([this.resolveResources(), this.resolveConnections()]);
136
+ return resources;
137
+ }
68
138
  constructor(state, def) {
139
+ assertCoreRuntimeVersion(`AgentDurableObject(${def.name ?? "agent"})`);
69
140
  const store = new DoSessionStore(state.storage);
70
141
  const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
71
- this.resources = def.resources ?? {};
142
+ this.resourcesInput = def.resources;
143
+ this.connectionsInput = def.connections;
144
+ this.doEnv = def.env;
72
145
  this.services = def.services;
73
- this.session = new AgentSession(def.name ?? "agent", "self", store, new InProcBroadcaster(), model, def.tools, crossDoUnsupported);
146
+ this.authorizeAnswer = def.authorizeAnswer;
147
+ this.onInputAnnouncement = def.onInputAnnouncement;
148
+ this.storage = state.storage;
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
+ });
155
+ const sink = new InProcEventSink();
156
+ sink.subscribe((e) => {
157
+ if (e.type === "turn.started") this.armWatchdog();
158
+ });
159
+ sink.subscribe((e) => {
160
+ if (e.type !== "turn.failed") return;
161
+ const defaultLog = () => console.error(`[june] agent "${name}" turn ${e.turnId} failed${e.step ? ` at ${e.step}` : ""}: ${e.error.stack ?? e.error.message}${e.error.causeChain?.length ? `\ncaused by: ${e.error.causeChain.join(" ← ")}` : ""}`);
162
+ if (def.onTurnError) try {
163
+ const out = def.onTurnError({
164
+ turnId: e.turnId,
165
+ error: e.error,
166
+ phase: e.phase,
167
+ step: e.step
168
+ });
169
+ if (out && typeof out.then === "function") out.then(void 0, (hookErr) => {
170
+ console.error(`[june] agent "${name}": onTurnError hook rejected:`, hookErr);
171
+ defaultLog();
172
+ });
173
+ return;
174
+ } catch (hookErr) {
175
+ console.error(`[june] agent "${name}": onTurnError hook threw:`, hookErr);
176
+ }
177
+ defaultLog();
178
+ });
179
+ const channels = (def.channels ?? []).map((c) => resolveChannel(c, def.env));
180
+ const channelTools = channels.flatMap((c) => c.tools?.() ?? []);
181
+ const tools = [...def.tools];
182
+ const seen = new Set(tools.map((t) => t.spec.name));
183
+ for (const t of channelTools) {
184
+ if (seen.has(t.spec.name)) throw new Error(`AgentDurableObject(${def.name ?? "agent"}): duplicate tool name "${t.spec.name}" from a channel — rename so dispatch is unambiguous.`);
185
+ seen.add(t.spec.name);
186
+ tools.push(t);
187
+ }
188
+ this.name = name;
189
+ this.store = store;
190
+ this.sink = sink;
191
+ this.model = model;
192
+ this.tools = tools;
193
+ this.channels = channels;
194
+ this.channelInstructions = def.channelInstructions;
195
+ if (def.channelInstructions) {
196
+ const names = new Set(channels.map((c) => c.name));
197
+ 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.`);
198
+ }
199
+ }
200
+ resolveSession(key) {
201
+ if (key !== void 0) assertSessionKey(key);
202
+ if (this.session) {
203
+ if (key !== void 0 && this.sessionKey !== key) throw new Error(this.sessionKey === "self" ? `agent "${this.name}": request session "${key}" arrived, but this session is bound to the placeholder "self" (first used without a real key) — carry the key on every path (route through durableFetch, or pass turn({ session }) on the direct API)` : `agent "${this.name}": request session "${key}" does not match this object's session "${this.sessionKey}" — one DO is one session; address each session by its own key (durableFetch, or turn({ session }) on the direct API)`);
204
+ return this.session;
205
+ }
206
+ const stored = this.store.getSessionKey();
207
+ if (key !== void 0 && stored !== void 0 && key !== stored) throw new Error(`agent "${this.name}": request session "${key}" does not match this object's session "${stored}" — one DO is one session; address each session by its own key (durableFetch, or turn({ session }) on the direct API)`);
208
+ const resolved = key ?? stored ?? "self";
209
+ if (key !== void 0 && key !== "self" && stored === void 0) this.store.setSessionKey(key);
210
+ this.sessionKey = resolved;
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;
213
+ this.session.onDequeue = (heldTurnId, held) => {
214
+ if (held.hostContext?.deliver && held.event) this.deliverHeld(this.session, heldTurnId, held.event);
215
+ };
216
+ const announce = this.onInputAnnouncement;
217
+ if (announce) {
218
+ this.session.onAnnounce = async (a) => {
219
+ await ensureScope();
220
+ const resources = await this.ready();
221
+ try {
222
+ await runInScope({
223
+ resources,
224
+ services: this.services
225
+ }, () => announce(a));
226
+ } catch (err) {
227
+ this.scheduleAlarm(Math.min(5e3 * 2 ** this.announceFailures, 3e5));
228
+ this.announceFailures++;
229
+ throw err;
230
+ }
231
+ this.announceFailures = 0;
232
+ };
233
+ this.session.flushAnnouncements();
234
+ }
235
+ return this.session;
236
+ }
237
+ async alarm() {
238
+ if (!this.onInputAnnouncement) return;
239
+ await this.resolveSession().flushAnnouncements();
240
+ }
241
+ armWatchdog() {
242
+ if (this.onInputAnnouncement) this.scheduleAlarm(6e4);
243
+ }
244
+ scheduleAlarm(inMs) {
245
+ const failed = (err) => console.error(`[june] agent "${this.name}": could not schedule the announcement alarm — delivery waits for the next announcement, turn or rebuild:`, err);
246
+ try {
247
+ Promise.resolve(this.storage.setAlarm?.(Date.now() + inMs)).catch(failed);
248
+ } catch (err) {
249
+ failed(err);
250
+ }
251
+ }
252
+ deliverHeld(session, turnId, event) {
253
+ const logRenderFailure = (err) => console.error(`[june] agent "${this.name}": delivered render for held turn ${turnId} failed:`, err);
254
+ try {
255
+ const channel = this.channels.find((c) => c.name === event.source);
256
+ if (!channel?.deliver) {
257
+ logRenderFailure(/* @__PURE__ */ new Error(`no deliver()-capable channel named "${event.source}" is wired into this DO — the held turn runs, but its reply is not rendered`));
258
+ return;
259
+ }
260
+ const it = observeTurnEvents(session, turnId)[Symbol.asyncIterator]();
261
+ const events = { [Symbol.asyncIterator]: () => it };
262
+ const target = {
263
+ channelId: event.channelId,
264
+ threadId: event.threadId,
265
+ recipientUserId: event.user?.id,
266
+ recipientTeamId: event.teamId
267
+ };
268
+ this.ready().then((resources) => runInScope({
269
+ resources,
270
+ services: this.services
271
+ }, () => channel.deliver(target, events, {
272
+ session: this.sessionKey,
273
+ files: this.files
274
+ }))).catch(logRenderFailure).finally(() => {
275
+ it.return?.();
276
+ });
277
+ } catch (err) {
278
+ logRenderFailure(err);
279
+ }
74
280
  }
75
281
  async turn(input) {
76
282
  await ensureScope();
283
+ const resources = await this.ready();
284
+ const session = this.resolveSession(input.session);
77
285
  return runInScope({
78
- resources: this.resources,
286
+ resources,
79
287
  services: this.services
80
- }, () => this.session.turn(input));
288
+ }, () => session.turn(input));
289
+ }
290
+ async start(input) {
291
+ await ensureScope();
292
+ const resources = await this.ready();
293
+ const session = this.resolveSession(input.session);
294
+ return runInScope({
295
+ resources,
296
+ services: this.services
297
+ }, () => session.start(input));
298
+ }
299
+ note(input) {
300
+ const { session, ...note } = input;
301
+ return this.resolveSession(session).note(note);
302
+ }
303
+ reset(opts) {
304
+ const session = this.resolveSession(opts?.session);
305
+ this.armWatchdog();
306
+ return session.reset();
81
307
  }
82
308
  transcript() {
83
- return this.session.transcript();
309
+ if (this.session) return this.session.transcript();
310
+ if (this.store.getSessionKey() !== void 0) return this.resolveSession().transcript();
311
+ return new AgentSession(this.name, "self", this.store, this.sink, this.model, this.tools, crossDoUnsupported, this.channelInstructions).transcript();
84
312
  }
85
313
  async fetch(req) {
86
314
  const url = new URL(req.url);
315
+ const key = req.headers.get("x-june-session") ?? void 0;
87
316
  if (req.method === "POST" && url.pathname.endsWith("/turn")) {
88
- const { userText, turnId } = await req.json();
89
- return Response.json({ text: await this.turn({
90
- userText,
91
- turnId
92
- }) });
317
+ const { userText, turnId, event, trigger, ifSuspended } = await req.json();
318
+ await ensureScope();
319
+ const replace = url.searchParams.get("replace") === "1";
320
+ const wantsDeliver = url.searchParams.get("deliver") === "1";
321
+ const detached = url.searchParams.get("detach") === "1";
322
+ if (ifSuspended === "queue" && !wantsDeliver && !detached) return Response.json({ error: `ifSuspended "queue" needs deliver=1 or detach=1: a held turn runs later, when no one is streaming it` }, { status: 400 });
323
+ if (ifSuspended === "queue" && replace) return Response.json({ error: `ifSuspended "queue" and replace=1 cannot be combined — replacing would drop held turns that were accepted` }, { status: 400 });
324
+ let deliverChannel;
325
+ if (wantsDeliver) {
326
+ if (!event?.channelId) return Response.json({ error: "deliver=1 needs an inbound event (event.channelId names the reply target)" }, { status: 400 });
327
+ deliverChannel = this.channels.find((c) => c.name === event.source);
328
+ 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 });
329
+ }
330
+ const resources = await this.ready();
331
+ let session;
332
+ let started;
333
+ try {
334
+ session = this.resolveSession(key);
335
+ this.armWatchdog();
336
+ started = runInScope({
337
+ resources,
338
+ services: this.services
339
+ }, () => session.start({
340
+ userText,
341
+ turnId,
342
+ event,
343
+ trigger,
344
+ replace,
345
+ ifSuspended,
346
+ hostContext: wantsDeliver ? { deliver: true } : void 0
347
+ }));
348
+ } catch (err) {
349
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
350
+ }
351
+ if (started.queued) return Response.json({
352
+ turnId: started.turnId,
353
+ queued: true
354
+ }, { status: 202 });
355
+ if (wantsDeliver) {
356
+ const logRenderFailure = (err) => console.error(`[june] agent "${this.name}": delivered render for turn ${started.turnId} failed:`, err);
357
+ try {
358
+ const it = observeTurnEvents(session, started.turnId)[Symbol.asyncIterator]();
359
+ const events = { [Symbol.asyncIterator]: () => it };
360
+ const target = {
361
+ channelId: event.channelId,
362
+ threadId: event.threadId,
363
+ recipientUserId: event.user?.id,
364
+ recipientTeamId: event.teamId
365
+ };
366
+ Promise.resolve().then(() => runInScope({
367
+ resources,
368
+ services: this.services
369
+ }, () => deliverChannel.deliver(target, events, {
370
+ session: this.sessionKey,
371
+ files: this.files
372
+ }))).catch(logRenderFailure).finally(() => {
373
+ it.return?.();
374
+ });
375
+ } catch (err) {
376
+ logRenderFailure(err);
377
+ }
378
+ return Response.json({ turnId: started.turnId }, { status: 202 });
379
+ }
380
+ if (detached) return Response.json({ turnId: started.turnId }, { status: 202 });
381
+ return new Response(sseTurnStream(session, started.turnId), { headers: SSE_HEADERS });
93
382
  }
94
- if (url.pathname.endsWith("/transcript")) return Response.json({ transcript: this.transcript() });
95
- return new Response("agent DO — POST /turn or GET /transcript", { status: 404 });
383
+ if (req.method === "POST" && url.pathname.endsWith("/resume")) {
384
+ const { turnId, inputId, input, by, principal, source, target } = await req.json();
385
+ await ensureScope();
386
+ const wantsDeliver = url.searchParams.get("deliver") === "1";
387
+ let resumeChannel;
388
+ if (wantsDeliver) {
389
+ if (!source || !target?.channelId || !target.messageTs) return Response.json({ error: "resume deliver=1 needs `source` (the channel name) and `target` (channelId + messageTs of the prompt message)" }, { status: 400 });
390
+ resumeChannel = this.channels.find((c) => c.name === source);
391
+ 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 });
392
+ }
393
+ const resources = await this.ready();
394
+ let session;
395
+ try {
396
+ session = this.resolveSession(key);
397
+ } catch (err) {
398
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
399
+ }
400
+ let granted;
401
+ try {
402
+ granted = await runInScope({
403
+ resources,
404
+ services: this.services
405
+ }, () => grantAnswer(session, {
406
+ turnId,
407
+ inputId,
408
+ by,
409
+ principal
410
+ }, this.authorizeAnswer));
411
+ } catch (err) {
412
+ console.error(`[june] agent "${this.name}": authorizeAnswer failed for input "${inputId}" of turn ${turnId}:`, err);
413
+ return Response.json({ error: `authorizeAnswer failed: ${err instanceof Error ? err.message : String(err)} — the answer was NOT applied` }, { status: 500 });
414
+ }
415
+ this.armWatchdog();
416
+ try {
417
+ runInScope({
418
+ resources,
419
+ services: this.services
420
+ }, () => session.resume(turnId, inputId, input, {
421
+ by,
422
+ granted
423
+ }));
424
+ } catch (err) {
425
+ const message = err instanceof Error ? err.message : String(err);
426
+ return Response.json({ error: message }, { status: err instanceof ResumeAuthorizationError ? 403 : 409 });
427
+ }
428
+ if (wantsDeliver) {
429
+ const logRenderFailure = (err) => console.error(`[june] agent "${this.name}": delivered resume render for turn ${turnId} failed:`, err);
430
+ try {
431
+ const it = observeTurnEvents(session, turnId)[Symbol.asyncIterator]();
432
+ const events = { [Symbol.asyncIterator]: () => it };
433
+ Promise.resolve().then(() => runInScope({
434
+ resources,
435
+ services: this.services
436
+ }, () => resumeChannel.deliverResume(target, events, {
437
+ session: this.sessionKey,
438
+ files: this.files
439
+ }))).catch(logRenderFailure).finally(() => {
440
+ it.return?.();
441
+ });
442
+ } catch (err) {
443
+ logRenderFailure(err);
444
+ }
445
+ return Response.json({ turnId }, { status: 202 });
446
+ }
447
+ return new Response(sseTurnStream(session, turnId), { headers: SSE_HEADERS });
448
+ }
449
+ if (req.method === "POST" && url.pathname.endsWith("/reset")) {
450
+ let session;
451
+ try {
452
+ session = this.resolveSession(key);
453
+ } catch (err) {
454
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
455
+ }
456
+ this.armWatchdog();
457
+ try {
458
+ return Response.json(await session.reset());
459
+ } catch (err) {
460
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
461
+ }
462
+ }
463
+ if (req.method === "POST" && url.pathname.endsWith("/note")) {
464
+ let input;
465
+ try {
466
+ input = await req.json();
467
+ } catch {
468
+ return Response.json({ error: "note: the body must be JSON { by, kind, text }" }, { status: 400 });
469
+ }
470
+ let session;
471
+ try {
472
+ session = this.resolveSession(key);
473
+ } catch (err) {
474
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
475
+ }
476
+ try {
477
+ return Response.json(await session.note(input));
478
+ } catch (err) {
479
+ if (err instanceof TypeError) return Response.json({ error: err.message }, { status: 400 });
480
+ throw err;
481
+ }
482
+ }
483
+ if (url.pathname.endsWith("/transcript")) {
484
+ if (key !== void 0) try {
485
+ return Response.json({ transcript: this.resolveSession(key).transcript() });
486
+ } catch (err) {
487
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
488
+ }
489
+ return Response.json({ transcript: this.transcript() });
490
+ }
491
+ return new Response("agent DO — POST /turn, POST /resume, POST /reset, POST /note, or GET /transcript", { status: 404 });
96
492
  }
97
493
  };
494
+ const SSE_HEADERS = {
495
+ "content-type": "text/event-stream",
496
+ "cache-control": "no-store"
497
+ };
498
+ function sseTurnStream(session, turnId) {
499
+ const enc = new TextEncoder();
500
+ let unsub;
501
+ let heartbeat;
502
+ const stop = () => {
503
+ unsub?.();
504
+ clearInterval(heartbeat);
505
+ };
506
+ return new ReadableStream({
507
+ start(controller) {
508
+ heartbeat = setInterval(() => {
509
+ try {
510
+ controller.enqueue(enc.encode(":hb\n\n"));
511
+ } catch {
512
+ clearInterval(heartbeat);
513
+ }
514
+ }, 2e4);
515
+ unsub = session.observe((e) => {
516
+ controller.enqueue(enc.encode(`data: ${JSON.stringify(e)}\n\n`));
517
+ if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "turn.cancelled" || e.type === "input.requested") {
518
+ stop();
519
+ controller.close();
520
+ }
521
+ }, { turnId });
522
+ },
523
+ cancel() {
524
+ stop();
525
+ }
526
+ });
527
+ }
528
+ async function sseTurnFinalText(res) {
529
+ const ct = res.headers.get("content-type") ?? "";
530
+ if (!res.body || !ct.includes("text/event-stream")) {
531
+ const detail = res.body ? (await res.text()).slice(0, 200) : "no body";
532
+ throw new Error(`turn stream: expected an SSE response, got ${ct || "no content-type"} (status ${res.status}): ${detail}`);
533
+ }
534
+ const reader = res.body.getReader();
535
+ const dec = new TextDecoder();
536
+ let buf = "";
537
+ for (;;) {
538
+ const { done, value } = await reader.read();
539
+ if (value) buf += dec.decode(value, { stream: true });
540
+ let i;
541
+ while ((i = buf.indexOf("\n\n")) >= 0) {
542
+ const line = buf.slice(0, i).split("\n").find((l) => l.startsWith("data:"));
543
+ buf = buf.slice(i + 2);
544
+ if (!line) continue;
545
+ const e = JSON.parse(line.slice(5).trim());
546
+ if (e.type === "turn.completed") return e.text;
547
+ if (e.type === "turn.failed") throw new Error(e.error.message, { cause: e.error });
548
+ if (e.type === "turn.cancelled") throw new Error(`turn ${e.turnId} was cancelled (${e.reason})`);
549
+ }
550
+ if (done) break;
551
+ }
552
+ throw new Error("turn stream ended without a terminal event");
553
+ }
554
+ async function* sseTurnEvents(res) {
555
+ const ct = res.headers.get("content-type") ?? "";
556
+ if (!res.body || !ct.includes("text/event-stream")) {
557
+ const detail = res.body ? (await res.text()).slice(0, 200) : "no body";
558
+ throw new Error(`turn stream: expected an SSE response, got ${ct || "no content-type"} (status ${res.status}): ${detail}`);
559
+ }
560
+ const reader = res.body.getReader();
561
+ const dec = new TextDecoder();
562
+ let buf = "";
563
+ for (;;) {
564
+ const { done, value } = await reader.read();
565
+ if (value) buf += dec.decode(value, { stream: true });
566
+ let i;
567
+ while ((i = buf.indexOf("\n\n")) >= 0) {
568
+ const line = buf.slice(0, i).split("\n").find((l) => l.startsWith("data:"));
569
+ buf = buf.slice(i + 2);
570
+ if (line) yield JSON.parse(line.slice(5).trim());
571
+ }
572
+ if (done) break;
573
+ }
574
+ }
575
+ const SESSION_HEADER = "x-june-session";
576
+ const SESSION_KEY_RE = /^[\x20-\x7E]+$/;
577
+ function assertSessionKey(session) {
578
+ if (typeof session !== "string" || !SESSION_KEY_RE.test(session)) throw new Error(`invalid session key ${JSON.stringify(session)} — a session key must be a non-empty printable-ASCII string (it rides an HTTP header and the DO name)`);
579
+ }
98
580
  function durableFetch(namespace, agent, session, req) {
99
- return namespace.get(namespace.idFromName(`${agent}:${session}`)).fetch(req);
581
+ assertSessionKey(session);
582
+ const headers = new Headers(req.headers);
583
+ headers.set(SESSION_HEADER, session);
584
+ return namespace.get(namespace.idFromName(`${agent}:${session}`)).fetch(new Request(req, { headers }));
100
585
  }
101
586
  function durableAgentSurface(getNamespace, opts) {
102
587
  const chatPath = opts.chatPath ?? "/message";
@@ -106,14 +591,34 @@ function durableAgentSurface(getNamespace, opts) {
106
591
  const namespace = getNamespace();
107
592
  if (!namespace) return null;
108
593
  const { message, session } = await req.json();
109
- return durableFetch(namespace, opts.agentName, session ?? "default", new Request("https://do/turn", {
594
+ if (session !== void 0) try {
595
+ assertSessionKey(session);
596
+ } catch (err) {
597
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 400 });
598
+ }
599
+ const res = await durableFetch(namespace, opts.agentName, session ?? "default", new Request("https://do/turn", {
110
600
  method: "POST",
111
601
  body: JSON.stringify({ userText: message })
112
602
  }));
603
+ if (req.headers.get("accept")?.includes("text/event-stream")) return res;
604
+ return Response.json({ text: await sseTurnFinalText(res) });
113
605
  };
114
606
  }
607
+ function resolveServices(factory, env, agentName) {
608
+ if (!factory) return void 0;
609
+ if (typeof env !== "object" || env === null) return factory(env);
610
+ const cache = isolateLocal("june.durableChannelSurface.services", () => /* @__PURE__ */ new WeakMap());
611
+ let perAgent = cache.get(env);
612
+ if (!perAgent) {
613
+ perAgent = /* @__PURE__ */ new Map();
614
+ cache.set(env, perAgent);
615
+ }
616
+ if (!perAgent.has(agentName)) perAgent.set(agentName, factory(env));
617
+ return perAgent.get(agentName);
618
+ }
115
619
  function durableChannelSurface(getNamespace, opts) {
116
- return channelDispatch(opts.channels.map((c) => resolveChannel(c, opts.env)), {
620
+ const resolved = opts.channels.map((c) => resolveChannel(c, opts.env));
621
+ const ctx = {
117
622
  agent: {
118
623
  name: opts.agentName,
119
624
  instructions: "",
@@ -122,20 +627,130 @@ function durableChannelSurface(getNamespace, opts) {
122
627
  channels: [],
123
628
  connections: []
124
629
  },
630
+ services: resolveServices(opts.services, opts.env, opts.agentName),
125
631
  waitUntil: opts.waitUntil,
632
+ files: opts.files,
126
633
  run: async (message, o) => {
127
634
  const namespace = getNamespace();
128
635
  if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
129
- const { text } = await (await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request("https://do/turn", {
636
+ return sseTurnFinalText(await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(`https://do/turn${o?.replace ? "?replace=1" : ""}`, {
637
+ method: "POST",
638
+ body: serializeTurn(message, o)
639
+ })));
640
+ },
641
+ runDetached: async (message, o) => {
642
+ const namespace = getNamespace();
643
+ if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
644
+ const res = await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(`https://do/turn?detach=1${o?.replace ? "&replace=1" : ""}`, {
645
+ method: "POST",
646
+ body: serializeTurn(message, o)
647
+ }));
648
+ if (res.status !== 202) {
649
+ const detail = (await res.text()).slice(0, 200);
650
+ throw new Error(`runDetached: turn was not accepted (status ${res.status}): ${detail}`);
651
+ }
652
+ return await res.json();
653
+ },
654
+ runDelivered: async (message, o) => {
655
+ const namespace = getNamespace();
656
+ if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
657
+ const res = await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(`https://do/turn?deliver=1${o?.replace ? "&replace=1" : ""}`, {
658
+ method: "POST",
659
+ body: serializeTurn(message, o)
660
+ }));
661
+ if (res.status === 501) throw new DeliverUnsupportedError(`runDelivered: ${(await res.json().catch(() => void 0))?.error ?? "the turn host cannot deliver this turn"}`);
662
+ if (res.status !== 202) {
663
+ const detail = (await res.text()).slice(0, 200);
664
+ throw new Error(`runDelivered: delivered turn was not accepted (status ${res.status}): ${detail}`);
665
+ }
666
+ return await res.json();
667
+ },
668
+ runStream: async function* (message, o) {
669
+ const namespace = getNamespace();
670
+ if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
671
+ yield* sseTurnEvents(await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(`https://do/turn${o?.replace ? "?replace=1" : ""}`, {
672
+ method: "POST",
673
+ body: serializeTurn(message, o)
674
+ })));
675
+ },
676
+ resumeStream: async function* (o) {
677
+ const namespace = getNamespace();
678
+ if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
679
+ yield* sseTurnEvents(await durableFetch(namespace, opts.agentName, o.session ?? "default", new Request("https://do/resume", {
130
680
  method: "POST",
131
- body: JSON.stringify({
132
- userText: message,
133
- turnId: o?.turnId
134
- })
135
- }))).json();
136
- return text;
681
+ body: serializeResume(o)
682
+ })));
683
+ },
684
+ resumeDelivered: async (o) => {
685
+ const namespace = getNamespace();
686
+ if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
687
+ const res = await durableFetch(namespace, opts.agentName, o.session ?? "default", new Request("https://do/resume?deliver=1", {
688
+ method: "POST",
689
+ body: serializeResume(o)
690
+ }));
691
+ if (res.status === 501) throw new DeliverUnsupportedError(`resumeDelivered: ${(await res.json().catch(() => void 0))?.error ?? "the turn host cannot deliver this continuation"}`);
692
+ if (res.status !== 202) {
693
+ const detail = (await res.text()).slice(0, 200);
694
+ throw new Error(`resumeDelivered: resume was not accepted (status ${res.status}): ${detail}`);
695
+ }
696
+ return await res.json();
697
+ },
698
+ resetSession: async (o) => {
699
+ const namespace = getNamespace();
700
+ if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
701
+ const res = await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request("https://do/reset", { method: "POST" }));
702
+ if (!res.ok) {
703
+ const detail = (await res.text()).slice(0, 200);
704
+ throw new Error(`resetSession: reset was not accepted (status ${res.status}): ${detail}`);
705
+ }
706
+ return await res.json();
137
707
  }
138
- });
708
+ };
709
+ return channelDispatch(resolved, ctx);
710
+ }
711
+ function serializeResume(o) {
712
+ try {
713
+ return JSON.stringify({
714
+ turnId: o.turnId,
715
+ inputId: o.inputId,
716
+ input: o.input,
717
+ by: o.by,
718
+ principal: o.principal,
719
+ source: o.source,
720
+ target: o.target
721
+ });
722
+ } catch (err) {
723
+ const culprit = ["input", "principal"].find((field) => {
724
+ try {
725
+ JSON.stringify(o[field]);
726
+ return false;
727
+ } catch {
728
+ return true;
729
+ }
730
+ }) ?? "payload";
731
+ throw new Error(`resume: ${culprit} is not JSON-serializable (${err.message}) — a resume must round-trip to the DO`);
732
+ }
733
+ }
734
+ function serializeTurn(userText, o) {
735
+ const payload = {
736
+ userText,
737
+ turnId: o?.turnId,
738
+ event: o?.event,
739
+ trigger: o?.trigger,
740
+ ifSuspended: o?.ifSuspended
741
+ };
742
+ try {
743
+ return JSON.stringify(payload);
744
+ } catch {
745
+ const event = o?.event ? {
746
+ ...o.event,
747
+ raw: void 0
748
+ } : void 0;
749
+ return JSON.stringify({
750
+ ...payload,
751
+ event
752
+ });
753
+ }
139
754
  }
140
755
  //#endregion
141
- export { AgentDurableObject, DoSessionStore, durableAgentSurface, durableChannelSurface, durableFetch };
756
+ export { AgentDurableObject, DoSessionStore, SESSION_HEADER, durableAgentSurface, durableChannelSurface, durableFetch, sseTurnEvents, sseTurnFinalText };