@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.
- package/dist/agent-durable.d.ts +4 -2
- package/dist/agent-durable.js +105 -11
- package/dist/agent-native.js +5 -5
- package/package.json +2 -2
- package/src/agent-durable.ts +120 -13
- package/src/agent-native.ts +8 -7
package/dist/agent-durable.d.ts
CHANGED
|
@@ -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 };
|
package/dist/agent-durable.js
CHANGED
|
@@ -44,12 +44,12 @@ var DoSessionStore = class {
|
|
|
44
44
|
return this.sql;
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
|
-
var
|
|
47
|
+
var InProcEventSink = class {
|
|
48
48
|
subs = /* @__PURE__ */ new Set();
|
|
49
|
-
|
|
49
|
+
emit(e) {
|
|
50
50
|
this.subs.forEach((cb) => {
|
|
51
51
|
try {
|
|
52
|
-
cb(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
})))
|
|
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 };
|
package/dist/agent-native.js
CHANGED
|
@@ -51,12 +51,12 @@ var SqliteSessionStore = class {
|
|
|
51
51
|
return this.db;
|
|
52
52
|
}
|
|
53
53
|
};
|
|
54
|
-
var
|
|
54
|
+
var InProcEventSink = class {
|
|
55
55
|
subs = /* @__PURE__ */ new Set();
|
|
56
|
-
|
|
56
|
+
emit(e) {
|
|
57
57
|
this.subs.forEach((cb) => {
|
|
58
58
|
try {
|
|
59
|
-
cb(
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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",
|
package/src/agent-durable.ts
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
import {
|
|
19
19
|
AgentSession,
|
|
20
20
|
withSystem,
|
|
21
|
-
type
|
|
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
|
|
123
|
-
private subs = new Set<(
|
|
124
|
-
|
|
125
|
-
subscribe(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
|
|
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
|
|
223
|
-
//
|
|
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
|
-
|
|
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
|
|
261
|
-
|
|
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
|
-
|
|
327
|
-
return
|
|
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);
|
package/src/agent-native.ts
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
import {
|
|
11
11
|
AgentSession,
|
|
12
12
|
withSystem,
|
|
13
|
-
type
|
|
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
|
|
81
|
-
private subs = new Set<(
|
|
82
|
-
|
|
83
|
-
subscribe(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
|
|
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
|
|
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;
|