@junejs/server 0.1.2-dev.2 → 0.1.2-dev.4
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 +7 -3
- package/dist/agent-durable.js +158 -20
- package/dist/agent-native.js +17 -10
- package/package.json +2 -2
- package/src/agent-durable.ts +182 -20
- package/src/agent-native.ts +16 -11
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, ProactiveTrigger, 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;
|
|
@@ -30,9 +30,10 @@ declare class DoSessionStore implements SessionStore {
|
|
|
30
30
|
private get sql();
|
|
31
31
|
appendMessage(m: Msg): void;
|
|
32
32
|
messages(): Msg[];
|
|
33
|
-
|
|
33
|
+
hasOpeningMessage(turnId: string): boolean;
|
|
34
34
|
getStep(id: string): unknown | undefined;
|
|
35
35
|
putStep(id: string, output: unknown): void;
|
|
36
|
+
delStep(id: string): void;
|
|
36
37
|
getStatus(): string;
|
|
37
38
|
setStatus(s: string): void;
|
|
38
39
|
tx<T>(fn: () => T): T;
|
|
@@ -58,10 +59,13 @@ declare class AgentDurableObject {
|
|
|
58
59
|
turnId?: string;
|
|
59
60
|
userText: string;
|
|
60
61
|
event?: InboundEvent;
|
|
62
|
+
trigger?: ProactiveTrigger;
|
|
61
63
|
}): Promise<string>;
|
|
62
64
|
transcript(): import("@junejs/core/agent-runtime").Turn[];
|
|
63
65
|
fetch(req: Request): Promise<Response>;
|
|
64
66
|
}
|
|
67
|
+
declare function sseTurnFinalText(res: Response): Promise<string>;
|
|
68
|
+
declare function sseTurnEvents(res: Response): AsyncIterable<TurnEvent>;
|
|
65
69
|
declare function durableFetch(namespace: DurableObjectNamespace, agent: string, session: string, req: Request): Promise<Response>;
|
|
66
70
|
declare function durableAgentSurface(getNamespace: () => DurableObjectNamespace | undefined, opts: {
|
|
67
71
|
agentName: string;
|
|
@@ -75,4 +79,4 @@ declare function durableChannelSurface(getNamespace: () => DurableObjectNamespac
|
|
|
75
79
|
waitUntil?: (p: Promise<unknown>) => void;
|
|
76
80
|
}): (req: Request) => Promise<Response | null>;
|
|
77
81
|
//#endregion
|
|
78
|
-
export { AgentDurableObject, DoAgentDef, DoSessionStore, DurableObjectNamespace, DurableObjectState, DurableObjectStub, DurableStorage, SqlStorage, SqlStorageCursor, SqlStorageValue, durableAgentSurface, durableChannelSurface, durableFetch };
|
|
82
|
+
export { AgentDurableObject, DoAgentDef, DoSessionStore, DurableObjectNamespace, DurableObjectState, DurableObjectStub, DurableStorage, SqlStorage, SqlStorageCursor, SqlStorageValue, durableAgentSurface, durableChannelSurface, durableFetch, sseTurnEvents, sseTurnFinalText };
|
package/dist/agent-durable.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { channelDispatch, resolveChannel } from "@junejs/core/agent-config";
|
|
2
|
-
import { AgentSession, withSystem } from "@junejs/core/agent-runtime";
|
|
2
|
+
import { AgentSession, ResumeAuthorizationError, withSystem } from "@junejs/core/agent-runtime";
|
|
3
3
|
import { ensureScope, runInScope } from "@junejs/db";
|
|
4
4
|
//#region src/agent-durable.ts
|
|
5
5
|
var DoSessionStore = class {
|
|
@@ -20,8 +20,8 @@ var DoSessionStore = class {
|
|
|
20
20
|
messages() {
|
|
21
21
|
return this.sql.exec("SELECT body FROM agent_messages ORDER BY seq").toArray().map((r) => JSON.parse(r.body));
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
return this.messages().some((m) => m.role === "user" && m.turnId === turnId);
|
|
23
|
+
hasOpeningMessage(turnId) {
|
|
24
|
+
return this.messages().some((m) => (m.role === "user" || m.role === "trigger") && m.turnId === turnId);
|
|
25
25
|
}
|
|
26
26
|
getStep(id) {
|
|
27
27
|
const rows = this.sql.exec("SELECT output FROM agent_steps WHERE id = ?", id).toArray();
|
|
@@ -30,6 +30,9 @@ var DoSessionStore = class {
|
|
|
30
30
|
putStep(id, output) {
|
|
31
31
|
this.sql.exec("INSERT INTO agent_steps (id, output) VALUES (?, ?)", id, JSON.stringify(output));
|
|
32
32
|
}
|
|
33
|
+
delStep(id) {
|
|
34
|
+
this.sql.exec("DELETE FROM agent_steps WHERE id = ?", id);
|
|
35
|
+
}
|
|
33
36
|
getStatus() {
|
|
34
37
|
const rows = this.sql.exec("SELECT v FROM agent_meta WHERE k = 'status'").toArray();
|
|
35
38
|
return rows.length ? rows[0].v : "new";
|
|
@@ -44,12 +47,12 @@ var DoSessionStore = class {
|
|
|
44
47
|
return this.sql;
|
|
45
48
|
}
|
|
46
49
|
};
|
|
47
|
-
var
|
|
50
|
+
var InProcEventSink = class {
|
|
48
51
|
subs = /* @__PURE__ */ new Set();
|
|
49
|
-
|
|
52
|
+
emit(e) {
|
|
50
53
|
this.subs.forEach((cb) => {
|
|
51
54
|
try {
|
|
52
|
-
cb(
|
|
55
|
+
cb(e);
|
|
53
56
|
} catch {}
|
|
54
57
|
});
|
|
55
58
|
}
|
|
@@ -78,7 +81,7 @@ var AgentDurableObject = class {
|
|
|
78
81
|
seen.add(t.spec.name);
|
|
79
82
|
tools.push(t);
|
|
80
83
|
}
|
|
81
|
-
this.session = new AgentSession(def.name ?? "agent", "self", store, new
|
|
84
|
+
this.session = new AgentSession(def.name ?? "agent", "self", store, new InProcEventSink(), model, tools, crossDoUnsupported, def.channelInstructions);
|
|
82
85
|
}
|
|
83
86
|
async turn(input) {
|
|
84
87
|
await ensureScope();
|
|
@@ -93,17 +96,122 @@ var AgentDurableObject = class {
|
|
|
93
96
|
async fetch(req) {
|
|
94
97
|
const url = new URL(req.url);
|
|
95
98
|
if (req.method === "POST" && url.pathname.endsWith("/turn")) {
|
|
96
|
-
const { userText, turnId, event } = await req.json();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
const { userText, turnId, event, trigger } = await req.json();
|
|
100
|
+
await ensureScope();
|
|
101
|
+
let started;
|
|
102
|
+
try {
|
|
103
|
+
started = runInScope({
|
|
104
|
+
resources: this.resources,
|
|
105
|
+
services: this.services
|
|
106
|
+
}, () => this.session.start({
|
|
107
|
+
userText,
|
|
108
|
+
turnId,
|
|
109
|
+
event,
|
|
110
|
+
trigger
|
|
111
|
+
}));
|
|
112
|
+
} catch (err) {
|
|
113
|
+
return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
|
|
114
|
+
}
|
|
115
|
+
return new Response(sseTurnStream(this.session, started.turnId), { headers: SSE_HEADERS });
|
|
116
|
+
}
|
|
117
|
+
if (req.method === "POST" && url.pathname.endsWith("/resume")) {
|
|
118
|
+
const { turnId, inputId, input, by } = await req.json();
|
|
119
|
+
await ensureScope();
|
|
120
|
+
try {
|
|
121
|
+
runInScope({
|
|
122
|
+
resources: this.resources,
|
|
123
|
+
services: this.services
|
|
124
|
+
}, () => this.session.resume(turnId, inputId, input, { by }));
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
127
|
+
return Response.json({ error: message }, { status: err instanceof ResumeAuthorizationError ? 403 : 409 });
|
|
128
|
+
}
|
|
129
|
+
return new Response(sseTurnStream(this.session, turnId), { headers: SSE_HEADERS });
|
|
102
130
|
}
|
|
103
131
|
if (url.pathname.endsWith("/transcript")) return Response.json({ transcript: this.transcript() });
|
|
104
|
-
return new Response("agent DO — POST /turn or GET /transcript", { status: 404 });
|
|
132
|
+
return new Response("agent DO — POST /turn, POST /resume, or GET /transcript", { status: 404 });
|
|
105
133
|
}
|
|
106
134
|
};
|
|
135
|
+
const SSE_HEADERS = {
|
|
136
|
+
"content-type": "text/event-stream",
|
|
137
|
+
"cache-control": "no-store"
|
|
138
|
+
};
|
|
139
|
+
function sseTurnStream(session, turnId) {
|
|
140
|
+
const enc = new TextEncoder();
|
|
141
|
+
let unsub;
|
|
142
|
+
let heartbeat;
|
|
143
|
+
const stop = () => {
|
|
144
|
+
unsub?.();
|
|
145
|
+
clearInterval(heartbeat);
|
|
146
|
+
};
|
|
147
|
+
return new ReadableStream({
|
|
148
|
+
start(controller) {
|
|
149
|
+
heartbeat = setInterval(() => {
|
|
150
|
+
try {
|
|
151
|
+
controller.enqueue(enc.encode(":hb\n\n"));
|
|
152
|
+
} catch {
|
|
153
|
+
clearInterval(heartbeat);
|
|
154
|
+
}
|
|
155
|
+
}, 2e4);
|
|
156
|
+
unsub = session.observe((e) => {
|
|
157
|
+
controller.enqueue(enc.encode(`data: ${JSON.stringify(e)}\n\n`));
|
|
158
|
+
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "input.requested") {
|
|
159
|
+
stop();
|
|
160
|
+
controller.close();
|
|
161
|
+
}
|
|
162
|
+
}, { turnId });
|
|
163
|
+
},
|
|
164
|
+
cancel() {
|
|
165
|
+
stop();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function sseTurnFinalText(res) {
|
|
170
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
171
|
+
if (!res.body || !ct.includes("text/event-stream")) {
|
|
172
|
+
const detail = res.body ? (await res.text()).slice(0, 200) : "no body";
|
|
173
|
+
throw new Error(`turn stream: expected an SSE response, got ${ct || "no content-type"} (status ${res.status}): ${detail}`);
|
|
174
|
+
}
|
|
175
|
+
const reader = res.body.getReader();
|
|
176
|
+
const dec = new TextDecoder();
|
|
177
|
+
let buf = "";
|
|
178
|
+
for (;;) {
|
|
179
|
+
const { done, value } = await reader.read();
|
|
180
|
+
if (value) buf += dec.decode(value, { stream: true });
|
|
181
|
+
let i;
|
|
182
|
+
while ((i = buf.indexOf("\n\n")) >= 0) {
|
|
183
|
+
const line = buf.slice(0, i).split("\n").find((l) => l.startsWith("data:"));
|
|
184
|
+
buf = buf.slice(i + 2);
|
|
185
|
+
if (!line) continue;
|
|
186
|
+
const e = JSON.parse(line.slice(5).trim());
|
|
187
|
+
if (e.type === "turn.completed") return e.text;
|
|
188
|
+
if (e.type === "turn.failed") throw new Error(e.error.message);
|
|
189
|
+
}
|
|
190
|
+
if (done) break;
|
|
191
|
+
}
|
|
192
|
+
throw new Error("turn stream ended without a terminal event");
|
|
193
|
+
}
|
|
194
|
+
async function* sseTurnEvents(res) {
|
|
195
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
196
|
+
if (!res.body || !ct.includes("text/event-stream")) {
|
|
197
|
+
const detail = res.body ? (await res.text()).slice(0, 200) : "no body";
|
|
198
|
+
throw new Error(`turn stream: expected an SSE response, got ${ct || "no content-type"} (status ${res.status}): ${detail}`);
|
|
199
|
+
}
|
|
200
|
+
const reader = res.body.getReader();
|
|
201
|
+
const dec = new TextDecoder();
|
|
202
|
+
let buf = "";
|
|
203
|
+
for (;;) {
|
|
204
|
+
const { done, value } = await reader.read();
|
|
205
|
+
if (value) buf += dec.decode(value, { stream: true });
|
|
206
|
+
let i;
|
|
207
|
+
while ((i = buf.indexOf("\n\n")) >= 0) {
|
|
208
|
+
const line = buf.slice(0, i).split("\n").find((l) => l.startsWith("data:"));
|
|
209
|
+
buf = buf.slice(i + 2);
|
|
210
|
+
if (line) yield JSON.parse(line.slice(5).trim());
|
|
211
|
+
}
|
|
212
|
+
if (done) break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
107
215
|
function durableFetch(namespace, agent, session, req) {
|
|
108
216
|
return namespace.get(namespace.idFromName(`${agent}:${session}`)).fetch(req);
|
|
109
217
|
}
|
|
@@ -115,10 +223,12 @@ function durableAgentSurface(getNamespace, opts) {
|
|
|
115
223
|
const namespace = getNamespace();
|
|
116
224
|
if (!namespace) return null;
|
|
117
225
|
const { message, session } = await req.json();
|
|
118
|
-
|
|
226
|
+
const res = await durableFetch(namespace, opts.agentName, session ?? "default", new Request("https://do/turn", {
|
|
119
227
|
method: "POST",
|
|
120
228
|
body: JSON.stringify({ userText: message })
|
|
121
229
|
}));
|
|
230
|
+
if (req.headers.get("accept")?.includes("text/event-stream")) return res;
|
|
231
|
+
return Response.json({ text: await sseTurnFinalText(res) });
|
|
122
232
|
};
|
|
123
233
|
}
|
|
124
234
|
function durableChannelSurface(getNamespace, opts) {
|
|
@@ -136,19 +246,47 @@ function durableChannelSurface(getNamespace, opts) {
|
|
|
136
246
|
run: async (message, o) => {
|
|
137
247
|
const namespace = getNamespace();
|
|
138
248
|
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
139
|
-
|
|
249
|
+
return sseTurnFinalText(await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request("https://do/turn", {
|
|
250
|
+
method: "POST",
|
|
251
|
+
body: serializeTurn(message, o)
|
|
252
|
+
})));
|
|
253
|
+
},
|
|
254
|
+
runStream: async function* (message, o) {
|
|
255
|
+
const namespace = getNamespace();
|
|
256
|
+
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
257
|
+
yield* sseTurnEvents(await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request("https://do/turn", {
|
|
140
258
|
method: "POST",
|
|
141
259
|
body: serializeTurn(message, o)
|
|
142
|
-
})))
|
|
143
|
-
|
|
260
|
+
})));
|
|
261
|
+
},
|
|
262
|
+
resumeStream: async function* (o) {
|
|
263
|
+
const namespace = getNamespace();
|
|
264
|
+
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
265
|
+
yield* sseTurnEvents(await durableFetch(namespace, opts.agentName, o.session ?? "default", new Request("https://do/resume", {
|
|
266
|
+
method: "POST",
|
|
267
|
+
body: serializeResume(o)
|
|
268
|
+
})));
|
|
144
269
|
}
|
|
145
270
|
});
|
|
146
271
|
}
|
|
272
|
+
function serializeResume(o) {
|
|
273
|
+
try {
|
|
274
|
+
return JSON.stringify({
|
|
275
|
+
turnId: o.turnId,
|
|
276
|
+
inputId: o.inputId,
|
|
277
|
+
input: o.input,
|
|
278
|
+
by: o.by
|
|
279
|
+
});
|
|
280
|
+
} catch (err) {
|
|
281
|
+
throw new Error(`resumeStream: input is not JSON-serializable (${err.message}) — a resume answer must round-trip to the DO`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
147
284
|
function serializeTurn(userText, o) {
|
|
148
285
|
const payload = {
|
|
149
286
|
userText,
|
|
150
287
|
turnId: o?.turnId,
|
|
151
|
-
event: o?.event
|
|
288
|
+
event: o?.event,
|
|
289
|
+
trigger: o?.trigger
|
|
152
290
|
};
|
|
153
291
|
try {
|
|
154
292
|
return JSON.stringify(payload);
|
|
@@ -164,4 +302,4 @@ function serializeTurn(userText, o) {
|
|
|
164
302
|
}
|
|
165
303
|
}
|
|
166
304
|
//#endregion
|
|
167
|
-
export { AgentDurableObject, DoSessionStore, durableAgentSurface, durableChannelSurface, durableFetch };
|
|
305
|
+
export { AgentDurableObject, DoSessionStore, durableAgentSurface, durableChannelSurface, durableFetch, sseTurnEvents, sseTurnFinalText };
|
package/dist/agent-native.js
CHANGED
|
@@ -20,8 +20,8 @@ var SqliteSessionStore = class {
|
|
|
20
20
|
messages() {
|
|
21
21
|
return this.db.query("SELECT body FROM agent_messages WHERE session_id = ? ORDER BY seq").all(this.sid).map((r) => JSON.parse(r.body));
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
return this.messages().some((m) => m.role === "user" && m.turnId === turnId);
|
|
23
|
+
hasOpeningMessage(turnId) {
|
|
24
|
+
return this.messages().some((m) => (m.role === "user" || m.role === "trigger") && m.turnId === turnId);
|
|
25
25
|
}
|
|
26
26
|
getStep(id) {
|
|
27
27
|
const r = this.db.query("SELECT output FROM agent_steps WHERE session_id = ? AND id = ?").get(this.sid, id);
|
|
@@ -30,6 +30,9 @@ var SqliteSessionStore = class {
|
|
|
30
30
|
putStep(id, output) {
|
|
31
31
|
this.db.query("INSERT INTO agent_steps (session_id, id, output) VALUES (?, ?, ?)").run(this.sid, id, JSON.stringify(output));
|
|
32
32
|
}
|
|
33
|
+
delStep(id) {
|
|
34
|
+
this.db.query("DELETE FROM agent_steps WHERE session_id = ? AND id = ?").run(this.sid, id);
|
|
35
|
+
}
|
|
33
36
|
getStatus() {
|
|
34
37
|
return this.db.query("SELECT status FROM agent_sessions WHERE session_id = ?").get(this.sid)?.status ?? "new";
|
|
35
38
|
}
|
|
@@ -51,12 +54,12 @@ var SqliteSessionStore = class {
|
|
|
51
54
|
return this.db;
|
|
52
55
|
}
|
|
53
56
|
};
|
|
54
|
-
var
|
|
57
|
+
var InProcEventSink = class {
|
|
55
58
|
subs = /* @__PURE__ */ new Set();
|
|
56
|
-
|
|
59
|
+
emit(e) {
|
|
57
60
|
this.subs.forEach((cb) => {
|
|
58
61
|
try {
|
|
59
|
-
cb(
|
|
62
|
+
cb(e);
|
|
60
63
|
} catch {}
|
|
61
64
|
});
|
|
62
65
|
}
|
|
@@ -81,7 +84,7 @@ var NativeRuntime = class {
|
|
|
81
84
|
const def = this.agents[agent];
|
|
82
85
|
if (!def) throw new Error(`unknown agent: ${agent}`);
|
|
83
86
|
const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
|
|
84
|
-
a = new AgentSession(agent, id, new SqliteSessionStore(this.db, key), new
|
|
87
|
+
a = new AgentSession(agent, id, new SqliteSessionStore(this.db, key), new InProcEventSink(), model, def.tools, this, def.channelInstructions);
|
|
85
88
|
this.actors.set(key, a);
|
|
86
89
|
}
|
|
87
90
|
return a;
|
|
@@ -100,8 +103,8 @@ var MemorySessionStore = class {
|
|
|
100
103
|
messages() {
|
|
101
104
|
return this.msgs.slice();
|
|
102
105
|
}
|
|
103
|
-
|
|
104
|
-
return this.msgs.some((m) => m.role === "user" && m.turnId === turnId);
|
|
106
|
+
hasOpeningMessage(turnId) {
|
|
107
|
+
return this.msgs.some((m) => (m.role === "user" || m.role === "trigger") && m.turnId === turnId);
|
|
105
108
|
}
|
|
106
109
|
getStep(id) {
|
|
107
110
|
return this.steps.has(id) ? this.steps.get(id) : void 0;
|
|
@@ -109,6 +112,9 @@ var MemorySessionStore = class {
|
|
|
109
112
|
putStep(id, output) {
|
|
110
113
|
this.steps.set(id, output);
|
|
111
114
|
}
|
|
115
|
+
delStep(id) {
|
|
116
|
+
this.steps.delete(id);
|
|
117
|
+
}
|
|
112
118
|
getStatus() {
|
|
113
119
|
return this.status;
|
|
114
120
|
}
|
|
@@ -135,7 +141,7 @@ var MemoryRuntime = class {
|
|
|
135
141
|
if (!def) throw new Error(`unknown agent: ${agent}`);
|
|
136
142
|
const store = new MemorySessionStore();
|
|
137
143
|
this.stores.set(key, store);
|
|
138
|
-
a = new AgentSession(agent, id, store, new
|
|
144
|
+
a = new AgentSession(agent, id, store, new InProcEventSink(), def.model, def.tools, this);
|
|
139
145
|
this.actors.set(key, a);
|
|
140
146
|
}
|
|
141
147
|
return a;
|
|
@@ -156,7 +162,8 @@ function mountAgent(agent, runtime, opts = {}) {
|
|
|
156
162
|
run: (message, o) => runtime.session(agent.name, o?.session ?? "default").turn({
|
|
157
163
|
turnId: o?.turnId,
|
|
158
164
|
userText: message,
|
|
159
|
-
event: o?.event
|
|
165
|
+
event: o?.event,
|
|
166
|
+
trigger: o?.trigger
|
|
160
167
|
})
|
|
161
168
|
};
|
|
162
169
|
const channels = channelFetch(agent, ctx);
|
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.4",
|
|
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.8",
|
|
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
|
@@ -17,13 +17,16 @@
|
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
19
|
AgentSession,
|
|
20
|
+
ResumeAuthorizationError,
|
|
20
21
|
withSystem,
|
|
21
|
-
type
|
|
22
|
+
type EventSink,
|
|
23
|
+
type TurnEvent,
|
|
22
24
|
type Model,
|
|
23
25
|
type Msg,
|
|
24
26
|
type Runtime,
|
|
25
27
|
type InboundEvent,
|
|
26
28
|
type SessionStore,
|
|
29
|
+
type ProactiveTrigger,
|
|
27
30
|
type Tool,
|
|
28
31
|
} from "@junejs/core/agent-runtime";
|
|
29
32
|
import type { Resources } from "@junejs/core/resources";
|
|
@@ -94,8 +97,8 @@ export class DoSessionStore implements SessionStore {
|
|
|
94
97
|
messages(): Msg[] {
|
|
95
98
|
return this.sql.exec<{ body: string }>("SELECT body FROM agent_messages ORDER BY seq").toArray().map((r) => JSON.parse(r.body));
|
|
96
99
|
}
|
|
97
|
-
|
|
98
|
-
return this.messages().some((m) => m.role === "user" && m.turnId === turnId);
|
|
100
|
+
hasOpeningMessage(turnId: string): boolean {
|
|
101
|
+
return this.messages().some((m) => (m.role === "user" || m.role === "trigger") && m.turnId === turnId);
|
|
99
102
|
}
|
|
100
103
|
getStep(id: string): unknown | undefined {
|
|
101
104
|
const rows = this.sql.exec<{ output: string }>("SELECT output FROM agent_steps WHERE id = ?", id).toArray();
|
|
@@ -104,6 +107,9 @@ export class DoSessionStore implements SessionStore {
|
|
|
104
107
|
putStep(id: string, output: unknown) {
|
|
105
108
|
this.sql.exec("INSERT INTO agent_steps (id, output) VALUES (?, ?)", id, JSON.stringify(output));
|
|
106
109
|
}
|
|
110
|
+
delStep(id: string) {
|
|
111
|
+
this.sql.exec("DELETE FROM agent_steps WHERE id = ?", id);
|
|
112
|
+
}
|
|
107
113
|
getStatus(): string {
|
|
108
114
|
const rows = this.sql.exec<{ v: string }>("SELECT v FROM agent_meta WHERE k = 'status'").toArray();
|
|
109
115
|
return rows.length ? rows[0]!.v : "new";
|
|
@@ -119,10 +125,10 @@ export class DoSessionStore implements SessionStore {
|
|
|
119
125
|
}
|
|
120
126
|
}
|
|
121
127
|
|
|
122
|
-
class
|
|
123
|
-
private subs = new Set<(
|
|
124
|
-
|
|
125
|
-
subscribe(cb: (
|
|
128
|
+
class InProcEventSink implements EventSink {
|
|
129
|
+
private subs = new Set<(e: TurnEvent) => void>();
|
|
130
|
+
emit(e: TurnEvent) { this.subs.forEach((cb) => { try { cb(e); } catch { /* a bad subscriber must not break emit */ } }); }
|
|
131
|
+
subscribe(cb: (e: TurnEvent) => void): () => void { this.subs.add(cb); return () => this.subs.delete(cb); }
|
|
126
132
|
}
|
|
127
133
|
|
|
128
134
|
// A DO can't run in-process subagents; a subagent there is a sibling DO (cross-DO
|
|
@@ -203,7 +209,7 @@ export class AgentDurableObject {
|
|
|
203
209
|
seen.add(t.spec.name);
|
|
204
210
|
tools.push(t);
|
|
205
211
|
}
|
|
206
|
-
this.session = new AgentSession(def.name ?? "agent", "self", store, new
|
|
212
|
+
this.session = new AgentSession(def.name ?? "agent", "self", store, new InProcEventSink(), model, tools, crossDoUnsupported, def.channelInstructions);
|
|
207
213
|
}
|
|
208
214
|
// Run the whole turn inside a request scope seeded from this DO's env, so ambient
|
|
209
215
|
// `db`/`kv`/`blob` and `currentServices()` resolve inside a tool exactly as in a
|
|
@@ -212,23 +218,137 @@ export class AgentDurableObject {
|
|
|
212
218
|
// Juno's batch-loader registry) can't leak across turns on a long-lived DO.
|
|
213
219
|
// ensureScope() lazily wires node:async_hooks (workerd via nodejs_compat), as the
|
|
214
220
|
// pipeline does; without it runInScope is a pass-through and ambient reads throw.
|
|
215
|
-
async turn(input: { turnId?: string; userText: string; event?: InboundEvent }): Promise<string> {
|
|
221
|
+
async turn(input: { turnId?: string; userText: string; event?: InboundEvent; trigger?: ProactiveTrigger }): Promise<string> {
|
|
216
222
|
await ensureScope();
|
|
217
223
|
return runInScope({ resources: this.resources, services: this.services }, () => this.session.turn(input));
|
|
218
224
|
}
|
|
219
225
|
transcript() {
|
|
220
226
|
return this.session.transcript();
|
|
221
227
|
}
|
|
222
|
-
// Default HTTP surface: POST …/turn
|
|
223
|
-
//
|
|
228
|
+
// Default HTTP surface: POST …/turn STREAMS the turn's TurnEvents as SSE (start the
|
|
229
|
+
// turn in-scope, then stream from the session sink); GET …/transcript reads the log.
|
|
230
|
+
// The app can call turn()/transcript() directly instead (turn() awaits the final text).
|
|
224
231
|
async fetch(req: Request): Promise<Response> {
|
|
225
232
|
const url = new URL(req.url);
|
|
226
233
|
if (req.method === "POST" && url.pathname.endsWith("/turn")) {
|
|
227
|
-
const { userText, turnId, event } = (await req.json()) as { userText: string; turnId?: string; event?: InboundEvent };
|
|
228
|
-
|
|
234
|
+
const { userText, turnId, event, trigger } = (await req.json()) as { userText: string; turnId?: string; event?: InboundEvent; trigger?: ProactiveTrigger };
|
|
235
|
+
await ensureScope();
|
|
236
|
+
// start() schedules the turn on the chain WITHIN the scope, so it runs with ambient
|
|
237
|
+
// db/services (ALS propagates to the .then continuation registered here); subscribing
|
|
238
|
+
// happens synchronously right after, before any event can emit.
|
|
239
|
+
let started: { turnId: string };
|
|
240
|
+
try {
|
|
241
|
+
started = runInScope({ resources: this.resources, services: this.services }, () => this.session.start({ userText, turnId, event, trigger }));
|
|
242
|
+
} catch (err) {
|
|
243
|
+
// e.g. the session is suspended awaiting input — a client-resolvable conflict, not a crash
|
|
244
|
+
return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
|
|
245
|
+
}
|
|
246
|
+
return new Response(sseTurnStream(this.session, started.turnId), { headers: SSE_HEADERS });
|
|
247
|
+
}
|
|
248
|
+
// Provide the input a suspended turn is waiting on and stream its continuation as SSE.
|
|
249
|
+
// NOTE: this default surface passes `by` straight from the body — the engine treats it as a
|
|
250
|
+
// VERIFIED identity, so the app must authenticate it upstream (e.g. take the user id from a
|
|
251
|
+
// signature-checked Slack interaction payload), never expose this endpoint raw to clients.
|
|
252
|
+
if (req.method === "POST" && url.pathname.endsWith("/resume")) {
|
|
253
|
+
const { turnId, inputId, input, by } = (await req.json()) as { turnId: string; inputId: string; input: unknown; by?: string };
|
|
254
|
+
await ensureScope();
|
|
255
|
+
try {
|
|
256
|
+
runInScope({ resources: this.resources, services: this.services }, () => this.session.resume(turnId, inputId, input, { by }));
|
|
257
|
+
} catch (err) {
|
|
258
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
259
|
+
// 403: the resumer may not answer; 409: not suspended / wrong turn / wrong input id
|
|
260
|
+
return Response.json({ error: message }, { status: err instanceof ResumeAuthorizationError ? 403 : 409 });
|
|
261
|
+
}
|
|
262
|
+
return new Response(sseTurnStream(this.session, turnId), { headers: SSE_HEADERS });
|
|
229
263
|
}
|
|
230
264
|
if (url.pathname.endsWith("/transcript")) return Response.json({ transcript: this.transcript() });
|
|
231
|
-
return new Response("agent DO — POST /turn or GET /transcript", { status: 404 });
|
|
265
|
+
return new Response("agent DO — POST /turn, POST /resume, or GET /transcript", { status: 404 });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── SSE transport for the turn event stream (crosses the worker→DO isolate) ───
|
|
270
|
+
// The DO streams a turn's TurnEvents as text/event-stream; the worker either pipes
|
|
271
|
+
// that straight to a browser (live chat) or collapses it to the final text (channels).
|
|
272
|
+
// no-store (not no-cache): an intermediary must NOT store chat/user content at all, and it
|
|
273
|
+
// matches the repo's other SSE surface (dev-reload).
|
|
274
|
+
const SSE_HEADERS = { "content-type": "text/event-stream", "cache-control": "no-store" };
|
|
275
|
+
|
|
276
|
+
// A ReadableStream of SSE frames from a session's live event stream, scoped to one turn
|
|
277
|
+
// and closed on its terminal event. Subscribes synchronously (no replay needed — we start
|
|
278
|
+
// the turn then subscribe before any event can emit). A `:hb` comment heartbeat keeps the
|
|
279
|
+
// connection alive across long model/tool gaps (a silent SSE stream gets culled by idle
|
|
280
|
+
// timeouts on hosts/proxies) — cleared on terminal close and on cancel.
|
|
281
|
+
function sseTurnStream(session: AgentSession, turnId: string): ReadableStream<Uint8Array> {
|
|
282
|
+
const enc = new TextEncoder();
|
|
283
|
+
let unsub: (() => void) | undefined;
|
|
284
|
+
let heartbeat: ReturnType<typeof setInterval> | undefined;
|
|
285
|
+
const stop = () => { unsub?.(); clearInterval(heartbeat); };
|
|
286
|
+
return new ReadableStream<Uint8Array>({
|
|
287
|
+
start(controller) {
|
|
288
|
+
heartbeat = setInterval(() => {
|
|
289
|
+
try { controller.enqueue(enc.encode(":hb\n\n")); } catch { clearInterval(heartbeat); }
|
|
290
|
+
}, 20_000);
|
|
291
|
+
unsub = session.observe((e) => {
|
|
292
|
+
controller.enqueue(enc.encode(`data: ${JSON.stringify(e)}\n\n`));
|
|
293
|
+
// terminal for the STREAM: completed, failed, OR suspended (input.requested → the turn
|
|
294
|
+
// parked; the stream ends and a later /resume opens a fresh continuation stream).
|
|
295
|
+
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "input.requested") { stop(); controller.close(); }
|
|
296
|
+
}, { turnId });
|
|
297
|
+
},
|
|
298
|
+
cancel() { stop(); },
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Consume an SSE turn stream to its terminal state: the final text, or throw on failure.
|
|
303
|
+
// The non-streaming path (channels, a JSON chat response) uses this. Guards a non-SSE /
|
|
304
|
+
// body-less upstream response (a misroute or an error) into a clear error, not a TypeError.
|
|
305
|
+
export async function sseTurnFinalText(res: Response): Promise<string> {
|
|
306
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
307
|
+
if (!res.body || !ct.includes("text/event-stream")) {
|
|
308
|
+
const detail = res.body ? (await res.text()).slice(0, 200) : "no body";
|
|
309
|
+
throw new Error(`turn stream: expected an SSE response, got ${ct || "no content-type"} (status ${res.status}): ${detail}`);
|
|
310
|
+
}
|
|
311
|
+
const reader = res.body.getReader();
|
|
312
|
+
const dec = new TextDecoder();
|
|
313
|
+
let buf = "";
|
|
314
|
+
for (;;) {
|
|
315
|
+
const { done, value } = await reader.read();
|
|
316
|
+
if (value) buf += dec.decode(value, { stream: true });
|
|
317
|
+
let i: number;
|
|
318
|
+
while ((i = buf.indexOf("\n\n")) >= 0) {
|
|
319
|
+
const line = buf.slice(0, i).split("\n").find((l) => l.startsWith("data:"));
|
|
320
|
+
buf = buf.slice(i + 2);
|
|
321
|
+
if (!line) continue;
|
|
322
|
+
const e = JSON.parse(line.slice(5).trim()) as TurnEvent;
|
|
323
|
+
if (e.type === "turn.completed") return e.text;
|
|
324
|
+
if (e.type === "turn.failed") throw new Error(e.error.message);
|
|
325
|
+
}
|
|
326
|
+
if (done) break;
|
|
327
|
+
}
|
|
328
|
+
throw new Error("turn stream ended without a terminal event");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Parse an SSE turn response into the stream of TurnEvents (skipping `:hb` heartbeats).
|
|
332
|
+
// The streaming consumer (a channel's render path) iterates this to drive live UI.
|
|
333
|
+
export async function* sseTurnEvents(res: Response): AsyncIterable<TurnEvent> {
|
|
334
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
335
|
+
if (!res.body || !ct.includes("text/event-stream")) {
|
|
336
|
+
const detail = res.body ? (await res.text()).slice(0, 200) : "no body";
|
|
337
|
+
throw new Error(`turn stream: expected an SSE response, got ${ct || "no content-type"} (status ${res.status}): ${detail}`);
|
|
338
|
+
}
|
|
339
|
+
const reader = res.body.getReader();
|
|
340
|
+
const dec = new TextDecoder();
|
|
341
|
+
let buf = "";
|
|
342
|
+
for (;;) {
|
|
343
|
+
const { done, value } = await reader.read();
|
|
344
|
+
if (value) buf += dec.decode(value, { stream: true });
|
|
345
|
+
let i: number;
|
|
346
|
+
while ((i = buf.indexOf("\n\n")) >= 0) {
|
|
347
|
+
const line = buf.slice(0, i).split("\n").find((l) => l.startsWith("data:"));
|
|
348
|
+
buf = buf.slice(i + 2);
|
|
349
|
+
if (line) yield JSON.parse(line.slice(5).trim()) as TurnEvent;
|
|
350
|
+
}
|
|
351
|
+
if (done) break;
|
|
232
352
|
}
|
|
233
353
|
}
|
|
234
354
|
|
|
@@ -257,13 +377,17 @@ export function durableAgentSurface(
|
|
|
257
377
|
const namespace = getNamespace();
|
|
258
378
|
if (!namespace) return null; // no DO binding → not mounted here
|
|
259
379
|
const { message, session } = (await req.json()) as { message: string; session?: string };
|
|
260
|
-
// Forward to the session's DO on its /turn contract
|
|
261
|
-
|
|
380
|
+
// Forward to the session's DO on its /turn contract — which now STREAMS SSE. A client
|
|
381
|
+
// that asks for the stream (Accept: text/event-stream) gets live TurnEvents piped
|
|
382
|
+
// through; otherwise collapse to the final { text } (the prior JSON contract).
|
|
383
|
+
const res = await durableFetch(
|
|
262
384
|
namespace,
|
|
263
385
|
opts.agentName,
|
|
264
386
|
session ?? "default",
|
|
265
387
|
new Request("https://do/turn", { method: "POST", body: JSON.stringify({ userText: message }) }),
|
|
266
388
|
);
|
|
389
|
+
if (req.headers.get("accept")?.includes("text/event-stream")) return res;
|
|
390
|
+
return Response.json({ text: await sseTurnFinalText(res) });
|
|
267
391
|
};
|
|
268
392
|
}
|
|
269
393
|
|
|
@@ -323,8 +447,32 @@ export function durableChannelSurface(
|
|
|
323
447
|
o?.session ?? "default",
|
|
324
448
|
new Request("https://do/turn", { method: "POST", body: serializeTurn(message, o) }),
|
|
325
449
|
);
|
|
326
|
-
|
|
327
|
-
return
|
|
450
|
+
// /turn streams SSE; the simple path collapses it to the final text.
|
|
451
|
+
return sseTurnFinalText(res);
|
|
452
|
+
},
|
|
453
|
+
// The LIVE path: hand the channel the TurnEvent stream so it can render as the turn runs.
|
|
454
|
+
runStream: async function* (message, o) {
|
|
455
|
+
const namespace = getNamespace();
|
|
456
|
+
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
457
|
+
const res = await durableFetch(
|
|
458
|
+
namespace,
|
|
459
|
+
opts.agentName,
|
|
460
|
+
o?.session ?? "default",
|
|
461
|
+
new Request("https://do/turn", { method: "POST", body: serializeTurn(message, o) }),
|
|
462
|
+
);
|
|
463
|
+
yield* sseTurnEvents(res);
|
|
464
|
+
},
|
|
465
|
+
// Resume a parked turn on its session's DO and stream the continuation.
|
|
466
|
+
resumeStream: async function* (o) {
|
|
467
|
+
const namespace = getNamespace();
|
|
468
|
+
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
469
|
+
const res = await durableFetch(
|
|
470
|
+
namespace,
|
|
471
|
+
opts.agentName,
|
|
472
|
+
o.session ?? "default",
|
|
473
|
+
new Request("https://do/resume", { method: "POST", body: serializeResume(o) }),
|
|
474
|
+
);
|
|
475
|
+
yield* sseTurnEvents(res);
|
|
328
476
|
},
|
|
329
477
|
};
|
|
330
478
|
return channelDispatch(resolved, ctx);
|
|
@@ -336,8 +484,22 @@ export function durableChannelSurface(
|
|
|
336
484
|
// run the turn, so on a serialization failure we drop it rather than let an
|
|
337
485
|
// unserializable payload take down turn forwarding entirely. (raw is optional on
|
|
338
486
|
// InboundEvent precisely because it may not survive this boundary.)
|
|
339
|
-
|
|
340
|
-
|
|
487
|
+
// Serialize a /resume RPC body. `input` is `unknown` — the human's answer — so a
|
|
488
|
+
// (third-party) host could hand us something JSON.stringify chokes on (BigInt, circular).
|
|
489
|
+
// Unlike serializeTurn's `raw`, `input` is essential: silently dropping it would resume the
|
|
490
|
+
// turn with the wrong answer. So fail loudly with a clear message rather than corrupt the resume.
|
|
491
|
+
function serializeResume(o: { turnId: string; inputId: string; input: unknown; by?: string }): string {
|
|
492
|
+
try {
|
|
493
|
+
return JSON.stringify({ turnId: o.turnId, inputId: o.inputId, input: o.input, by: o.by });
|
|
494
|
+
} catch (err) {
|
|
495
|
+
throw new Error(`resumeStream: input is not JSON-serializable (${(err as Error).message}) — a resume answer must round-trip to the DO`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// `trigger` is ProactiveTrigger — plain strings by construction — so only event.raw can make
|
|
500
|
+
// the stringify throw; the fallback that strips it stays sufficient.
|
|
501
|
+
function serializeTurn(userText: string, o?: { turnId?: string; event?: InboundEvent; trigger?: ProactiveTrigger }): string {
|
|
502
|
+
const payload = { userText, turnId: o?.turnId, event: o?.event, trigger: o?.trigger };
|
|
341
503
|
try {
|
|
342
504
|
return JSON.stringify(payload);
|
|
343
505
|
} catch {
|
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,
|
|
@@ -45,8 +46,8 @@ class SqliteSessionStore implements SessionStore {
|
|
|
45
46
|
return (this.db.query("SELECT body FROM agent_messages WHERE session_id = ? ORDER BY seq").all(this.sid) as { body: string }[])
|
|
46
47
|
.map((r) => JSON.parse(r.body));
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
-
return this.messages().some((m) => m.role === "user" && m.turnId === turnId);
|
|
49
|
+
hasOpeningMessage(turnId: string): boolean {
|
|
50
|
+
return this.messages().some((m) => (m.role === "user" || m.role === "trigger") && m.turnId === turnId);
|
|
50
51
|
}
|
|
51
52
|
getStep(id: string): unknown | undefined {
|
|
52
53
|
const r = this.db.query("SELECT output FROM agent_steps WHERE session_id = ? AND id = ?").get(this.sid, id) as { output: string } | undefined;
|
|
@@ -55,6 +56,9 @@ class SqliteSessionStore implements SessionStore {
|
|
|
55
56
|
putStep(id: string, output: unknown) {
|
|
56
57
|
this.db.query("INSERT INTO agent_steps (session_id, id, output) VALUES (?, ?, ?)").run(this.sid, id, JSON.stringify(output));
|
|
57
58
|
}
|
|
59
|
+
delStep(id: string) {
|
|
60
|
+
this.db.query("DELETE FROM agent_steps WHERE session_id = ? AND id = ?").run(this.sid, id);
|
|
61
|
+
}
|
|
58
62
|
getStatus(): string {
|
|
59
63
|
return (this.db.query("SELECT status FROM agent_sessions WHERE session_id = ?").get(this.sid) as { status: string } | undefined)?.status ?? "new";
|
|
60
64
|
}
|
|
@@ -77,10 +81,10 @@ class SqliteSessionStore implements SessionStore {
|
|
|
77
81
|
unwrap<H = unknown>(): H { return this.db as unknown as H; }
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
class
|
|
81
|
-
private subs = new Set<(
|
|
82
|
-
|
|
83
|
-
subscribe(cb: (
|
|
84
|
+
class InProcEventSink implements EventSink {
|
|
85
|
+
private subs = new Set<(e: TurnEvent) => void>();
|
|
86
|
+
emit(e: TurnEvent) { this.subs.forEach((cb) => { try { cb(e); } catch { /* a bad subscriber must not break emit */ } }); }
|
|
87
|
+
subscribe(cb: (e: TurnEvent) => void): () => void { this.subs.add(cb); return () => this.subs.delete(cb); }
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
// `instructions` (the agent's system prompt) is injected into the model per turn
|
|
@@ -107,7 +111,7 @@ export class NativeRuntime implements Runtime {
|
|
|
107
111
|
const def = this.agents[agent];
|
|
108
112
|
if (!def) throw new Error(`unknown agent: ${agent}`);
|
|
109
113
|
const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
|
|
110
|
-
a = new AgentSession(agent, id, new SqliteSessionStore(this.db, key), new
|
|
114
|
+
a = new AgentSession(agent, id, new SqliteSessionStore(this.db, key), new InProcEventSink(), model, def.tools, this, def.channelInstructions);
|
|
111
115
|
this.actors.set(key, a);
|
|
112
116
|
}
|
|
113
117
|
return a;
|
|
@@ -135,9 +139,10 @@ class MemorySessionStore implements SessionStore {
|
|
|
135
139
|
private status = "new";
|
|
136
140
|
appendMessage(m: Msg) { this.msgs.push(m); }
|
|
137
141
|
messages(): Msg[] { return this.msgs.slice(); }
|
|
138
|
-
|
|
142
|
+
hasOpeningMessage(turnId: string): boolean { return this.msgs.some((m) => (m.role === "user" || m.role === "trigger") && m.turnId === turnId); }
|
|
139
143
|
getStep(id: string): unknown | undefined { return this.steps.has(id) ? this.steps.get(id) : undefined; }
|
|
140
144
|
putStep(id: string, output: unknown) { this.steps.set(id, output); }
|
|
145
|
+
delStep(id: string) { this.steps.delete(id); }
|
|
141
146
|
getStatus(): string { return this.status; }
|
|
142
147
|
setStatus(s: string) { this.status = s; }
|
|
143
148
|
tx<T>(fn: () => T): T { return fn(); } // no rollback: an in-memory store is not a durability tier
|
|
@@ -159,7 +164,7 @@ export class MemoryRuntime implements Runtime {
|
|
|
159
164
|
if (!def) throw new Error(`unknown agent: ${agent}`);
|
|
160
165
|
const store = new MemorySessionStore();
|
|
161
166
|
this.stores.set(key, store);
|
|
162
|
-
a = new AgentSession(agent, id, store, new
|
|
167
|
+
a = new AgentSession(agent, id, store, new InProcEventSink(), def.model, def.tools, this);
|
|
163
168
|
this.actors.set(key, a);
|
|
164
169
|
}
|
|
165
170
|
return a;
|
|
@@ -208,7 +213,7 @@ export function mountAgent(
|
|
|
208
213
|
agent,
|
|
209
214
|
services: opts.services, // same DI bag reachable from channel hooks (parity with durableChannelSurface)
|
|
210
215
|
run: (message, o) =>
|
|
211
|
-
runtime.session(agent.name, o?.session ?? "default").turn({ turnId: o?.turnId, userText: message, event: o?.event }),
|
|
216
|
+
runtime.session(agent.name, o?.session ?? "default").turn({ turnId: o?.turnId, userText: message, event: o?.event, trigger: o?.trigger }),
|
|
212
217
|
};
|
|
213
218
|
const channels = channelFetch(agent, ctx);
|
|
214
219
|
const surface = async (req: Request): Promise<Response | null> => {
|