@junejs/server 0.1.2-dev.2 → 0.1.2-dev.3

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.
@@ -1,5 +1,5 @@
1
1
  import { Channel, ChannelFactory } from "@junejs/core/agent-config";
2
- import { InboundEvent, Model, Msg, SessionStore, Tool } from "@junejs/core/agent-runtime";
2
+ import { InboundEvent, Model, Msg, SessionStore, Tool, TurnEvent } from "@junejs/core/agent-runtime";
3
3
  import { Resources } from "@junejs/core/resources";
4
4
  //#region src/agent-durable.d.ts
5
5
  type SqlStorageValue = ArrayBuffer | string | number | null;
@@ -62,6 +62,8 @@ declare class AgentDurableObject {
62
62
  transcript(): import("@junejs/core/agent-runtime").Turn[];
63
63
  fetch(req: Request): Promise<Response>;
64
64
  }
65
+ declare function sseTurnFinalText(res: Response): Promise<string>;
66
+ declare function sseTurnEvents(res: Response): AsyncIterable<TurnEvent>;
65
67
  declare function durableFetch(namespace: DurableObjectNamespace, agent: string, session: string, req: Request): Promise<Response>;
66
68
  declare function durableAgentSurface(getNamespace: () => DurableObjectNamespace | undefined, opts: {
67
69
  agentName: string;
@@ -75,4 +77,4 @@ declare function durableChannelSurface(getNamespace: () => DurableObjectNamespac
75
77
  waitUntil?: (p: Promise<unknown>) => void;
76
78
  }): (req: Request) => Promise<Response | null>;
77
79
  //#endregion
78
- export { AgentDurableObject, DoAgentDef, DoSessionStore, DurableObjectNamespace, DurableObjectState, DurableObjectStub, DurableStorage, SqlStorage, SqlStorageCursor, SqlStorageValue, durableAgentSurface, durableChannelSurface, durableFetch };
80
+ export { AgentDurableObject, DoAgentDef, DoSessionStore, DurableObjectNamespace, DurableObjectState, DurableObjectStub, DurableStorage, SqlStorage, SqlStorageCursor, SqlStorageValue, durableAgentSurface, durableChannelSurface, durableFetch, sseTurnEvents, sseTurnFinalText };
@@ -44,12 +44,12 @@ var DoSessionStore = class {
44
44
  return this.sql;
45
45
  }
46
46
  };
47
- var InProcBroadcaster = class {
47
+ var InProcEventSink = class {
48
48
  subs = /* @__PURE__ */ new Set();
49
- publish(turnId) {
49
+ emit(e) {
50
50
  this.subs.forEach((cb) => {
51
51
  try {
52
- cb(turnId);
52
+ cb(e);
53
53
  } catch {}
54
54
  });
55
55
  }
@@ -78,7 +78,7 @@ var AgentDurableObject = class {
78
78
  seen.add(t.spec.name);
79
79
  tools.push(t);
80
80
  }
81
- this.session = new AgentSession(def.name ?? "agent", "self", store, new InProcBroadcaster(), model, tools, crossDoUnsupported, def.channelInstructions);
81
+ this.session = new AgentSession(def.name ?? "agent", "self", store, new InProcEventSink(), model, tools, crossDoUnsupported, def.channelInstructions);
82
82
  }
83
83
  async turn(input) {
84
84
  await ensureScope();
@@ -94,16 +94,101 @@ var AgentDurableObject = class {
94
94
  const url = new URL(req.url);
95
95
  if (req.method === "POST" && url.pathname.endsWith("/turn")) {
96
96
  const { userText, turnId, event } = await req.json();
97
- return Response.json({ text: await this.turn({
97
+ await ensureScope();
98
+ const started = runInScope({
99
+ resources: this.resources,
100
+ services: this.services
101
+ }, () => this.session.start({
98
102
  userText,
99
103
  turnId,
100
104
  event
101
- }) });
105
+ }));
106
+ return new Response(sseTurnStream(this.session, started.turnId), { headers: SSE_HEADERS });
102
107
  }
103
108
  if (url.pathname.endsWith("/transcript")) return Response.json({ transcript: this.transcript() });
104
109
  return new Response("agent DO — POST /turn or GET /transcript", { status: 404 });
105
110
  }
106
111
  };
112
+ const SSE_HEADERS = {
113
+ "content-type": "text/event-stream",
114
+ "cache-control": "no-store"
115
+ };
116
+ function sseTurnStream(session, turnId) {
117
+ const enc = new TextEncoder();
118
+ let unsub;
119
+ let heartbeat;
120
+ const stop = () => {
121
+ unsub?.();
122
+ clearInterval(heartbeat);
123
+ };
124
+ return new ReadableStream({
125
+ start(controller) {
126
+ heartbeat = setInterval(() => {
127
+ try {
128
+ controller.enqueue(enc.encode(":hb\n\n"));
129
+ } catch {
130
+ clearInterval(heartbeat);
131
+ }
132
+ }, 2e4);
133
+ unsub = session.observe((e) => {
134
+ controller.enqueue(enc.encode(`data: ${JSON.stringify(e)}\n\n`));
135
+ if (e.type === "turn.completed" || e.type === "turn.failed") {
136
+ stop();
137
+ controller.close();
138
+ }
139
+ }, { turnId });
140
+ },
141
+ cancel() {
142
+ stop();
143
+ }
144
+ });
145
+ }
146
+ async function sseTurnFinalText(res) {
147
+ const ct = res.headers.get("content-type") ?? "";
148
+ if (!res.body || !ct.includes("text/event-stream")) {
149
+ const detail = res.body ? (await res.text()).slice(0, 200) : "no body";
150
+ throw new Error(`turn stream: expected an SSE response, got ${ct || "no content-type"} (status ${res.status}): ${detail}`);
151
+ }
152
+ const reader = res.body.getReader();
153
+ const dec = new TextDecoder();
154
+ let buf = "";
155
+ for (;;) {
156
+ const { done, value } = await reader.read();
157
+ if (value) buf += dec.decode(value, { stream: true });
158
+ let i;
159
+ while ((i = buf.indexOf("\n\n")) >= 0) {
160
+ const line = buf.slice(0, i).split("\n").find((l) => l.startsWith("data:"));
161
+ buf = buf.slice(i + 2);
162
+ if (!line) continue;
163
+ const e = JSON.parse(line.slice(5).trim());
164
+ if (e.type === "turn.completed") return e.text;
165
+ if (e.type === "turn.failed") throw new Error(e.error.message);
166
+ }
167
+ if (done) break;
168
+ }
169
+ throw new Error("turn stream ended without a terminal event");
170
+ }
171
+ async function* sseTurnEvents(res) {
172
+ const ct = res.headers.get("content-type") ?? "";
173
+ if (!res.body || !ct.includes("text/event-stream")) {
174
+ const detail = res.body ? (await res.text()).slice(0, 200) : "no body";
175
+ throw new Error(`turn stream: expected an SSE response, got ${ct || "no content-type"} (status ${res.status}): ${detail}`);
176
+ }
177
+ const reader = res.body.getReader();
178
+ const dec = new TextDecoder();
179
+ let buf = "";
180
+ for (;;) {
181
+ const { done, value } = await reader.read();
182
+ if (value) buf += dec.decode(value, { stream: true });
183
+ let i;
184
+ while ((i = buf.indexOf("\n\n")) >= 0) {
185
+ const line = buf.slice(0, i).split("\n").find((l) => l.startsWith("data:"));
186
+ buf = buf.slice(i + 2);
187
+ if (line) yield JSON.parse(line.slice(5).trim());
188
+ }
189
+ if (done) break;
190
+ }
191
+ }
107
192
  function durableFetch(namespace, agent, session, req) {
108
193
  return namespace.get(namespace.idFromName(`${agent}:${session}`)).fetch(req);
109
194
  }
@@ -115,10 +200,12 @@ function durableAgentSurface(getNamespace, opts) {
115
200
  const namespace = getNamespace();
116
201
  if (!namespace) return null;
117
202
  const { message, session } = await req.json();
118
- return durableFetch(namespace, opts.agentName, session ?? "default", new Request("https://do/turn", {
203
+ const res = await durableFetch(namespace, opts.agentName, session ?? "default", new Request("https://do/turn", {
119
204
  method: "POST",
120
205
  body: JSON.stringify({ userText: message })
121
206
  }));
207
+ if (req.headers.get("accept")?.includes("text/event-stream")) return res;
208
+ return Response.json({ text: await sseTurnFinalText(res) });
122
209
  };
123
210
  }
124
211
  function durableChannelSurface(getNamespace, opts) {
@@ -136,11 +223,18 @@ function durableChannelSurface(getNamespace, opts) {
136
223
  run: async (message, o) => {
137
224
  const namespace = getNamespace();
138
225
  if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
139
- const { text } = await (await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request("https://do/turn", {
226
+ return sseTurnFinalText(await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request("https://do/turn", {
227
+ method: "POST",
228
+ body: serializeTurn(message, o)
229
+ })));
230
+ },
231
+ runStream: async function* (message, o) {
232
+ const namespace = getNamespace();
233
+ if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
234
+ yield* sseTurnEvents(await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request("https://do/turn", {
140
235
  method: "POST",
141
236
  body: serializeTurn(message, o)
142
- }))).json();
143
- return text;
237
+ })));
144
238
  }
145
239
  });
146
240
  }
@@ -164,4 +258,4 @@ function serializeTurn(userText, o) {
164
258
  }
165
259
  }
166
260
  //#endregion
167
- export { AgentDurableObject, DoSessionStore, durableAgentSurface, durableChannelSurface, durableFetch };
261
+ export { AgentDurableObject, DoSessionStore, durableAgentSurface, durableChannelSurface, durableFetch, sseTurnEvents, sseTurnFinalText };
@@ -51,12 +51,12 @@ var SqliteSessionStore = class {
51
51
  return this.db;
52
52
  }
53
53
  };
54
- var InProcBroadcaster = class {
54
+ var InProcEventSink = class {
55
55
  subs = /* @__PURE__ */ new Set();
56
- publish(turnId) {
56
+ emit(e) {
57
57
  this.subs.forEach((cb) => {
58
58
  try {
59
- cb(turnId);
59
+ cb(e);
60
60
  } catch {}
61
61
  });
62
62
  }
@@ -81,7 +81,7 @@ var NativeRuntime = class {
81
81
  const def = this.agents[agent];
82
82
  if (!def) throw new Error(`unknown agent: ${agent}`);
83
83
  const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
84
- a = new AgentSession(agent, id, new SqliteSessionStore(this.db, key), new InProcBroadcaster(), model, def.tools, this, def.channelInstructions);
84
+ a = new AgentSession(agent, id, new SqliteSessionStore(this.db, key), new InProcEventSink(), model, def.tools, this, def.channelInstructions);
85
85
  this.actors.set(key, a);
86
86
  }
87
87
  return a;
@@ -135,7 +135,7 @@ var MemoryRuntime = class {
135
135
  if (!def) throw new Error(`unknown agent: ${agent}`);
136
136
  const store = new MemorySessionStore();
137
137
  this.stores.set(key, store);
138
- a = new AgentSession(agent, id, store, new InProcBroadcaster(), def.model, def.tools, this);
138
+ a = new AgentSession(agent, id, store, new InProcEventSink(), def.model, def.tools, this);
139
139
  this.actors.set(key, a);
140
140
  }
141
141
  return a;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junejs/server",
3
- "version": "0.1.2-dev.2",
3
+ "version": "0.1.2-dev.3",
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",
@@ -125,7 +125,7 @@
125
125
  "build": "tsdown"
126
126
  },
127
127
  "dependencies": {
128
- "@junejs/core": ">=0.1.1-dev.4",
128
+ "@junejs/core": ">=0.1.1-dev.5",
129
129
  "@junejs/db": ">=0.0.34-dev.0",
130
130
  "@momiji-rs/sparkdown": "^0.0.4",
131
131
  "lightningcss": "^1.27.0",
@@ -18,7 +18,8 @@
18
18
  import {
19
19
  AgentSession,
20
20
  withSystem,
21
- type Broadcaster,
21
+ type EventSink,
22
+ type TurnEvent,
22
23
  type Model,
23
24
  type Msg,
24
25
  type Runtime,
@@ -119,10 +120,10 @@ export class DoSessionStore implements SessionStore {
119
120
  }
120
121
  }
121
122
 
122
- class InProcBroadcaster implements Broadcaster {
123
- private subs = new Set<(t: string) => void>();
124
- publish(turnId: string) { this.subs.forEach((cb) => { try { cb(turnId); } catch { /* a bad subscriber must not break publish */ } }); }
125
- subscribe(cb: (t: string) => void): () => void { this.subs.add(cb); return () => this.subs.delete(cb); }
123
+ class InProcEventSink implements EventSink {
124
+ private subs = new Set<(e: TurnEvent) => void>();
125
+ emit(e: TurnEvent) { this.subs.forEach((cb) => { try { cb(e); } catch { /* a bad subscriber must not break emit */ } }); }
126
+ subscribe(cb: (e: TurnEvent) => void): () => void { this.subs.add(cb); return () => this.subs.delete(cb); }
126
127
  }
127
128
 
128
129
  // A DO can't run in-process subagents; a subagent there is a sibling DO (cross-DO
@@ -203,7 +204,7 @@ export class AgentDurableObject {
203
204
  seen.add(t.spec.name);
204
205
  tools.push(t);
205
206
  }
206
- this.session = new AgentSession(def.name ?? "agent", "self", store, new InProcBroadcaster(), model, tools, crossDoUnsupported, def.channelInstructions);
207
+ this.session = new AgentSession(def.name ?? "agent", "self", store, new InProcEventSink(), model, tools, crossDoUnsupported, def.channelInstructions);
207
208
  }
208
209
  // Run the whole turn inside a request scope seeded from this DO's env, so ambient
209
210
  // `db`/`kv`/`blob` and `currentServices()` resolve inside a tool exactly as in a
@@ -219,19 +220,109 @@ export class AgentDurableObject {
219
220
  transcript() {
220
221
  return this.session.transcript();
221
222
  }
222
- // Default HTTP surface: POST …/turn runs a durable turn; GET …/transcript reads
223
- // the log. The app can call turn()/transcript() directly instead.
223
+ // Default HTTP surface: POST …/turn STREAMS the turn's TurnEvents as SSE (start the
224
+ // turn in-scope, then stream from the session sink); GET …/transcript reads the log.
225
+ // The app can call turn()/transcript() directly instead (turn() awaits the final text).
224
226
  async fetch(req: Request): Promise<Response> {
225
227
  const url = new URL(req.url);
226
228
  if (req.method === "POST" && url.pathname.endsWith("/turn")) {
227
229
  const { userText, turnId, event } = (await req.json()) as { userText: string; turnId?: string; event?: InboundEvent };
228
- return Response.json({ text: await this.turn({ userText, turnId, event }) });
230
+ await ensureScope();
231
+ // start() schedules the turn on the chain WITHIN the scope, so it runs with ambient
232
+ // db/services (ALS propagates to the .then continuation registered here); subscribing
233
+ // happens synchronously right after, before any event can emit.
234
+ const started = runInScope({ resources: this.resources, services: this.services }, () => this.session.start({ userText, turnId, event }));
235
+ return new Response(sseTurnStream(this.session, started.turnId), { headers: SSE_HEADERS });
229
236
  }
230
237
  if (url.pathname.endsWith("/transcript")) return Response.json({ transcript: this.transcript() });
231
238
  return new Response("agent DO — POST /turn or GET /transcript", { status: 404 });
232
239
  }
233
240
  }
234
241
 
242
+ // ── SSE transport for the turn event stream (crosses the worker→DO isolate) ───
243
+ // The DO streams a turn's TurnEvents as text/event-stream; the worker either pipes
244
+ // that straight to a browser (live chat) or collapses it to the final text (channels).
245
+ // no-store (not no-cache): an intermediary must NOT store chat/user content at all, and it
246
+ // matches the repo's other SSE surface (dev-reload).
247
+ const SSE_HEADERS = { "content-type": "text/event-stream", "cache-control": "no-store" };
248
+
249
+ // A ReadableStream of SSE frames from a session's live event stream, scoped to one turn
250
+ // and closed on its terminal event. Subscribes synchronously (no replay needed — we start
251
+ // the turn then subscribe before any event can emit). A `:hb` comment heartbeat keeps the
252
+ // connection alive across long model/tool gaps (a silent SSE stream gets culled by idle
253
+ // timeouts on hosts/proxies) — cleared on terminal close and on cancel.
254
+ function sseTurnStream(session: AgentSession, turnId: string): ReadableStream<Uint8Array> {
255
+ const enc = new TextEncoder();
256
+ let unsub: (() => void) | undefined;
257
+ let heartbeat: ReturnType<typeof setInterval> | undefined;
258
+ const stop = () => { unsub?.(); clearInterval(heartbeat); };
259
+ return new ReadableStream<Uint8Array>({
260
+ start(controller) {
261
+ heartbeat = setInterval(() => {
262
+ try { controller.enqueue(enc.encode(":hb\n\n")); } catch { clearInterval(heartbeat); }
263
+ }, 20_000);
264
+ unsub = session.observe((e) => {
265
+ controller.enqueue(enc.encode(`data: ${JSON.stringify(e)}\n\n`));
266
+ if (e.type === "turn.completed" || e.type === "turn.failed") { stop(); controller.close(); }
267
+ }, { turnId });
268
+ },
269
+ cancel() { stop(); },
270
+ });
271
+ }
272
+
273
+ // Consume an SSE turn stream to its terminal state: the final text, or throw on failure.
274
+ // The non-streaming path (channels, a JSON chat response) uses this. Guards a non-SSE /
275
+ // body-less upstream response (a misroute or an error) into a clear error, not a TypeError.
276
+ export async function sseTurnFinalText(res: Response): Promise<string> {
277
+ const ct = res.headers.get("content-type") ?? "";
278
+ if (!res.body || !ct.includes("text/event-stream")) {
279
+ const detail = res.body ? (await res.text()).slice(0, 200) : "no body";
280
+ throw new Error(`turn stream: expected an SSE response, got ${ct || "no content-type"} (status ${res.status}): ${detail}`);
281
+ }
282
+ const reader = res.body.getReader();
283
+ const dec = new TextDecoder();
284
+ let buf = "";
285
+ for (;;) {
286
+ const { done, value } = await reader.read();
287
+ if (value) buf += dec.decode(value, { stream: true });
288
+ let i: number;
289
+ while ((i = buf.indexOf("\n\n")) >= 0) {
290
+ const line = buf.slice(0, i).split("\n").find((l) => l.startsWith("data:"));
291
+ buf = buf.slice(i + 2);
292
+ if (!line) continue;
293
+ const e = JSON.parse(line.slice(5).trim()) as TurnEvent;
294
+ if (e.type === "turn.completed") return e.text;
295
+ if (e.type === "turn.failed") throw new Error(e.error.message);
296
+ }
297
+ if (done) break;
298
+ }
299
+ throw new Error("turn stream ended without a terminal event");
300
+ }
301
+
302
+ // Parse an SSE turn response into the stream of TurnEvents (skipping `:hb` heartbeats).
303
+ // The streaming consumer (a channel's render path) iterates this to drive live UI.
304
+ export async function* sseTurnEvents(res: Response): AsyncIterable<TurnEvent> {
305
+ const ct = res.headers.get("content-type") ?? "";
306
+ if (!res.body || !ct.includes("text/event-stream")) {
307
+ const detail = res.body ? (await res.text()).slice(0, 200) : "no body";
308
+ throw new Error(`turn stream: expected an SSE response, got ${ct || "no content-type"} (status ${res.status}): ${detail}`);
309
+ }
310
+ const reader = res.body.getReader();
311
+ const dec = new TextDecoder();
312
+ let buf = "";
313
+ for (;;) {
314
+ const { done, value } = await reader.read();
315
+ if (value) buf += dec.decode(value, { stream: true });
316
+ let i: number;
317
+ while ((i = buf.indexOf("\n\n")) >= 0) {
318
+ const line = buf.slice(0, i).split("\n").find((l) => l.startsWith("data:"));
319
+ buf = buf.slice(i + 2);
320
+ if (line) yield JSON.parse(line.slice(5).trim()) as TurnEvent;
321
+ }
322
+ if (done) break;
323
+ }
324
+ }
325
+
235
326
  // Worker-side routing: address a session's DO by (agent, session) and forward the
236
327
  // request to it. `env.AGENT` is the DO namespace binding.
237
328
  export function durableFetch(namespace: DurableObjectNamespace, agent: string, session: string, req: Request): Promise<Response> {
@@ -257,13 +348,17 @@ export function durableAgentSurface(
257
348
  const namespace = getNamespace();
258
349
  if (!namespace) return null; // no DO binding → not mounted here
259
350
  const { message, session } = (await req.json()) as { message: string; session?: string };
260
- // Forward to the session's DO on its /turn contract (AgentDurableObject.fetch).
261
- return durableFetch(
351
+ // Forward to the session's DO on its /turn contract — which now STREAMS SSE. A client
352
+ // that asks for the stream (Accept: text/event-stream) gets live TurnEvents piped
353
+ // through; otherwise collapse to the final { text } (the prior JSON contract).
354
+ const res = await durableFetch(
262
355
  namespace,
263
356
  opts.agentName,
264
357
  session ?? "default",
265
358
  new Request("https://do/turn", { method: "POST", body: JSON.stringify({ userText: message }) }),
266
359
  );
360
+ if (req.headers.get("accept")?.includes("text/event-stream")) return res;
361
+ return Response.json({ text: await sseTurnFinalText(res) });
267
362
  };
268
363
  }
269
364
 
@@ -323,8 +418,20 @@ export function durableChannelSurface(
323
418
  o?.session ?? "default",
324
419
  new Request("https://do/turn", { method: "POST", body: serializeTurn(message, o) }),
325
420
  );
326
- const { text } = (await res.json()) as { text: string };
327
- return text;
421
+ // /turn streams SSE; the simple path collapses it to the final text.
422
+ return sseTurnFinalText(res);
423
+ },
424
+ // The LIVE path: hand the channel the TurnEvent stream so it can render as the turn runs.
425
+ runStream: async function* (message, o) {
426
+ const namespace = getNamespace();
427
+ if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
428
+ const res = await durableFetch(
429
+ namespace,
430
+ opts.agentName,
431
+ o?.session ?? "default",
432
+ new Request("https://do/turn", { method: "POST", body: serializeTurn(message, o) }),
433
+ );
434
+ yield* sseTurnEvents(res);
328
435
  },
329
436
  };
330
437
  return channelDispatch(resolved, ctx);
@@ -10,7 +10,8 @@
10
10
  import {
11
11
  AgentSession,
12
12
  withSystem,
13
- type Broadcaster,
13
+ type EventSink,
14
+ type TurnEvent,
14
15
  type Model,
15
16
  type Msg,
16
17
  type Runtime,
@@ -77,10 +78,10 @@ class SqliteSessionStore implements SessionStore {
77
78
  unwrap<H = unknown>(): H { return this.db as unknown as H; }
78
79
  }
79
80
 
80
- class InProcBroadcaster implements Broadcaster {
81
- private subs = new Set<(t: string) => void>();
82
- publish(turnId: string) { this.subs.forEach((cb) => { try { cb(turnId); } catch { /* a bad subscriber must not break publish */ } }); }
83
- subscribe(cb: (t: string) => void): () => void { this.subs.add(cb); return () => this.subs.delete(cb); }
81
+ class InProcEventSink implements EventSink {
82
+ private subs = new Set<(e: TurnEvent) => void>();
83
+ emit(e: TurnEvent) { this.subs.forEach((cb) => { try { cb(e); } catch { /* a bad subscriber must not break emit */ } }); }
84
+ subscribe(cb: (e: TurnEvent) => void): () => void { this.subs.add(cb); return () => this.subs.delete(cb); }
84
85
  }
85
86
 
86
87
  // `instructions` (the agent's system prompt) is injected into the model per turn
@@ -107,7 +108,7 @@ export class NativeRuntime implements Runtime {
107
108
  const def = this.agents[agent];
108
109
  if (!def) throw new Error(`unknown agent: ${agent}`);
109
110
  const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
110
- a = new AgentSession(agent, id, new SqliteSessionStore(this.db, key), new InProcBroadcaster(), model, def.tools, this, def.channelInstructions);
111
+ a = new AgentSession(agent, id, new SqliteSessionStore(this.db, key), new InProcEventSink(), model, def.tools, this, def.channelInstructions);
111
112
  this.actors.set(key, a);
112
113
  }
113
114
  return a;
@@ -159,7 +160,7 @@ export class MemoryRuntime implements Runtime {
159
160
  if (!def) throw new Error(`unknown agent: ${agent}`);
160
161
  const store = new MemorySessionStore();
161
162
  this.stores.set(key, store);
162
- a = new AgentSession(agent, id, store, new InProcBroadcaster(), def.model, def.tools, this);
163
+ a = new AgentSession(agent, id, store, new InProcEventSink(), def.model, def.tools, this);
163
164
  this.actors.set(key, a);
164
165
  }
165
166
  return a;