@junejs/server 0.1.2-dev.3 → 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 +4 -2
- package/dist/agent-durable.js +59 -15
- package/dist/agent-native.js +12 -5
- package/package.json +2 -2
- package/src/agent-durable.ts +64 -9
- package/src/agent-native.ts +8 -4
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, TurnEvent } 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,6 +59,7 @@ 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>;
|
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";
|
|
@@ -93,20 +96,40 @@ 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();
|
|
99
|
+
const { userText, turnId, event, trigger } = await req.json();
|
|
97
100
|
await ensureScope();
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
}
|
|
106
115
|
return new Response(sseTurnStream(this.session, started.turnId), { headers: SSE_HEADERS });
|
|
107
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 });
|
|
130
|
+
}
|
|
108
131
|
if (url.pathname.endsWith("/transcript")) return Response.json({ transcript: this.transcript() });
|
|
109
|
-
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 });
|
|
110
133
|
}
|
|
111
134
|
};
|
|
112
135
|
const SSE_HEADERS = {
|
|
@@ -132,7 +155,7 @@ function sseTurnStream(session, turnId) {
|
|
|
132
155
|
}, 2e4);
|
|
133
156
|
unsub = session.observe((e) => {
|
|
134
157
|
controller.enqueue(enc.encode(`data: ${JSON.stringify(e)}\n\n`));
|
|
135
|
-
if (e.type === "turn.completed" || e.type === "turn.failed") {
|
|
158
|
+
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "input.requested") {
|
|
136
159
|
stop();
|
|
137
160
|
controller.close();
|
|
138
161
|
}
|
|
@@ -235,14 +258,35 @@ function durableChannelSurface(getNamespace, opts) {
|
|
|
235
258
|
method: "POST",
|
|
236
259
|
body: serializeTurn(message, o)
|
|
237
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
|
+
})));
|
|
238
269
|
}
|
|
239
270
|
});
|
|
240
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
|
+
}
|
|
241
284
|
function serializeTurn(userText, o) {
|
|
242
285
|
const payload = {
|
|
243
286
|
userText,
|
|
244
287
|
turnId: o?.turnId,
|
|
245
|
-
event: o?.event
|
|
288
|
+
event: o?.event,
|
|
289
|
+
trigger: o?.trigger
|
|
246
290
|
};
|
|
247
291
|
try {
|
|
248
292
|
return JSON.stringify(payload);
|
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
|
}
|
|
@@ -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
|
}
|
|
@@ -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,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
19
|
AgentSession,
|
|
20
|
+
ResumeAuthorizationError,
|
|
20
21
|
withSystem,
|
|
21
22
|
type EventSink,
|
|
22
23
|
type TurnEvent,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
type Runtime,
|
|
26
27
|
type InboundEvent,
|
|
27
28
|
type SessionStore,
|
|
29
|
+
type ProactiveTrigger,
|
|
28
30
|
type Tool,
|
|
29
31
|
} from "@junejs/core/agent-runtime";
|
|
30
32
|
import type { Resources } from "@junejs/core/resources";
|
|
@@ -95,8 +97,8 @@ export class DoSessionStore implements SessionStore {
|
|
|
95
97
|
messages(): Msg[] {
|
|
96
98
|
return this.sql.exec<{ body: string }>("SELECT body FROM agent_messages ORDER BY seq").toArray().map((r) => JSON.parse(r.body));
|
|
97
99
|
}
|
|
98
|
-
|
|
99
|
-
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);
|
|
100
102
|
}
|
|
101
103
|
getStep(id: string): unknown | undefined {
|
|
102
104
|
const rows = this.sql.exec<{ output: string }>("SELECT output FROM agent_steps WHERE id = ?", id).toArray();
|
|
@@ -105,6 +107,9 @@ export class DoSessionStore implements SessionStore {
|
|
|
105
107
|
putStep(id: string, output: unknown) {
|
|
106
108
|
this.sql.exec("INSERT INTO agent_steps (id, output) VALUES (?, ?)", id, JSON.stringify(output));
|
|
107
109
|
}
|
|
110
|
+
delStep(id: string) {
|
|
111
|
+
this.sql.exec("DELETE FROM agent_steps WHERE id = ?", id);
|
|
112
|
+
}
|
|
108
113
|
getStatus(): string {
|
|
109
114
|
const rows = this.sql.exec<{ v: string }>("SELECT v FROM agent_meta WHERE k = 'status'").toArray();
|
|
110
115
|
return rows.length ? rows[0]!.v : "new";
|
|
@@ -213,7 +218,7 @@ export class AgentDurableObject {
|
|
|
213
218
|
// Juno's batch-loader registry) can't leak across turns on a long-lived DO.
|
|
214
219
|
// ensureScope() lazily wires node:async_hooks (workerd via nodejs_compat), as the
|
|
215
220
|
// pipeline does; without it runInScope is a pass-through and ambient reads throw.
|
|
216
|
-
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> {
|
|
217
222
|
await ensureScope();
|
|
218
223
|
return runInScope({ resources: this.resources, services: this.services }, () => this.session.turn(input));
|
|
219
224
|
}
|
|
@@ -226,16 +231,38 @@ export class AgentDurableObject {
|
|
|
226
231
|
async fetch(req: Request): Promise<Response> {
|
|
227
232
|
const url = new URL(req.url);
|
|
228
233
|
if (req.method === "POST" && url.pathname.endsWith("/turn")) {
|
|
229
|
-
const { userText, turnId, event } = (await req.json()) as { userText: string; turnId?: string; event?: InboundEvent };
|
|
234
|
+
const { userText, turnId, event, trigger } = (await req.json()) as { userText: string; turnId?: string; event?: InboundEvent; trigger?: ProactiveTrigger };
|
|
230
235
|
await ensureScope();
|
|
231
236
|
// start() schedules the turn on the chain WITHIN the scope, so it runs with ambient
|
|
232
237
|
// db/services (ALS propagates to the .then continuation registered here); subscribing
|
|
233
238
|
// happens synchronously right after, before any event can emit.
|
|
234
|
-
|
|
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
|
+
}
|
|
235
246
|
return new Response(sseTurnStream(this.session, started.turnId), { headers: SSE_HEADERS });
|
|
236
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 });
|
|
263
|
+
}
|
|
237
264
|
if (url.pathname.endsWith("/transcript")) return Response.json({ transcript: this.transcript() });
|
|
238
|
-
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 });
|
|
239
266
|
}
|
|
240
267
|
}
|
|
241
268
|
|
|
@@ -263,7 +290,9 @@ function sseTurnStream(session: AgentSession, turnId: string): ReadableStream<Ui
|
|
|
263
290
|
}, 20_000);
|
|
264
291
|
unsub = session.observe((e) => {
|
|
265
292
|
controller.enqueue(enc.encode(`data: ${JSON.stringify(e)}\n\n`));
|
|
266
|
-
|
|
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(); }
|
|
267
296
|
}, { turnId });
|
|
268
297
|
},
|
|
269
298
|
cancel() { stop(); },
|
|
@@ -433,6 +462,18 @@ export function durableChannelSurface(
|
|
|
433
462
|
);
|
|
434
463
|
yield* sseTurnEvents(res);
|
|
435
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);
|
|
476
|
+
},
|
|
436
477
|
};
|
|
437
478
|
return channelDispatch(resolved, ctx);
|
|
438
479
|
}
|
|
@@ -443,8 +484,22 @@ export function durableChannelSurface(
|
|
|
443
484
|
// run the turn, so on a serialization failure we drop it rather than let an
|
|
444
485
|
// unserializable payload take down turn forwarding entirely. (raw is optional on
|
|
445
486
|
// InboundEvent precisely because it may not survive this boundary.)
|
|
446
|
-
|
|
447
|
-
|
|
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 };
|
|
448
503
|
try {
|
|
449
504
|
return JSON.stringify(payload);
|
|
450
505
|
} catch {
|
package/src/agent-native.ts
CHANGED
|
@@ -46,8 +46,8 @@ class SqliteSessionStore implements SessionStore {
|
|
|
46
46
|
return (this.db.query("SELECT body FROM agent_messages WHERE session_id = ? ORDER BY seq").all(this.sid) as { body: string }[])
|
|
47
47
|
.map((r) => JSON.parse(r.body));
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
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);
|
|
51
51
|
}
|
|
52
52
|
getStep(id: string): unknown | undefined {
|
|
53
53
|
const r = this.db.query("SELECT output FROM agent_steps WHERE session_id = ? AND id = ?").get(this.sid, id) as { output: string } | undefined;
|
|
@@ -56,6 +56,9 @@ class SqliteSessionStore implements SessionStore {
|
|
|
56
56
|
putStep(id: string, output: unknown) {
|
|
57
57
|
this.db.query("INSERT INTO agent_steps (session_id, id, output) VALUES (?, ?, ?)").run(this.sid, id, JSON.stringify(output));
|
|
58
58
|
}
|
|
59
|
+
delStep(id: string) {
|
|
60
|
+
this.db.query("DELETE FROM agent_steps WHERE session_id = ? AND id = ?").run(this.sid, id);
|
|
61
|
+
}
|
|
59
62
|
getStatus(): string {
|
|
60
63
|
return (this.db.query("SELECT status FROM agent_sessions WHERE session_id = ?").get(this.sid) as { status: string } | undefined)?.status ?? "new";
|
|
61
64
|
}
|
|
@@ -136,9 +139,10 @@ class MemorySessionStore implements SessionStore {
|
|
|
136
139
|
private status = "new";
|
|
137
140
|
appendMessage(m: Msg) { this.msgs.push(m); }
|
|
138
141
|
messages(): Msg[] { return this.msgs.slice(); }
|
|
139
|
-
|
|
142
|
+
hasOpeningMessage(turnId: string): boolean { return this.msgs.some((m) => (m.role === "user" || m.role === "trigger") && m.turnId === turnId); }
|
|
140
143
|
getStep(id: string): unknown | undefined { return this.steps.has(id) ? this.steps.get(id) : undefined; }
|
|
141
144
|
putStep(id: string, output: unknown) { this.steps.set(id, output); }
|
|
145
|
+
delStep(id: string) { this.steps.delete(id); }
|
|
142
146
|
getStatus(): string { return this.status; }
|
|
143
147
|
setStatus(s: string) { this.status = s; }
|
|
144
148
|
tx<T>(fn: () => T): T { return fn(); } // no rollback: an in-memory store is not a durability tier
|
|
@@ -209,7 +213,7 @@ export function mountAgent(
|
|
|
209
213
|
agent,
|
|
210
214
|
services: opts.services, // same DI bag reachable from channel hooks (parity with durableChannelSurface)
|
|
211
215
|
run: (message, o) =>
|
|
212
|
-
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 }),
|
|
213
217
|
};
|
|
214
218
|
const channels = channelFetch(agent, ctx);
|
|
215
219
|
const surface = async (req: Request): Promise<Response | null> => {
|