@junejs/server 1.0.0-dev.13 → 1.0.0-dev.14
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 -0
- package/dist/agent-durable.js +53 -8
- package/dist/agent-native.js +35 -3
- package/package.json +2 -2
- package/src/agent-durable.ts +80 -10
- package/src/agent-native.ts +38 -2
package/dist/agent-durable.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ declare class DoSessionStore implements SessionStore {
|
|
|
39
39
|
getSessionKey(): string | undefined;
|
|
40
40
|
setSessionKey(key: string): void;
|
|
41
41
|
tx<T>(fn: () => T): T;
|
|
42
|
+
reset(): number;
|
|
42
43
|
unwrap<H = unknown>(): H;
|
|
43
44
|
}
|
|
44
45
|
type DoAgentDef = {
|
|
@@ -88,6 +89,12 @@ declare class AgentDurableObject {
|
|
|
88
89
|
}): Promise<{
|
|
89
90
|
turnId: string;
|
|
90
91
|
}>;
|
|
92
|
+
reset(opts?: {
|
|
93
|
+
session?: string;
|
|
94
|
+
}): Promise<{
|
|
95
|
+
previousSession: string;
|
|
96
|
+
generation: number;
|
|
97
|
+
}>;
|
|
91
98
|
transcript(): import("@junejs/core/agent-runtime").Turn[];
|
|
92
99
|
fetch(req: Request): Promise<Response>;
|
|
93
100
|
}
|
package/dist/agent-durable.js
CHANGED
|
@@ -52,6 +52,22 @@ var DoSessionStore = class {
|
|
|
52
52
|
tx(fn) {
|
|
53
53
|
return this.storage.transactionSync(fn);
|
|
54
54
|
}
|
|
55
|
+
reset() {
|
|
56
|
+
return this.storage.transactionSync(() => {
|
|
57
|
+
const sql = this.sql;
|
|
58
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS agent_messages_archive (generation INTEGER, seq INTEGER, body TEXT)`);
|
|
59
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS agent_steps_archive (generation INTEGER, id TEXT, output TEXT)`);
|
|
60
|
+
const rows = this.sql.exec("SELECT v FROM agent_meta WHERE k = 'generation'").toArray();
|
|
61
|
+
const generation = rows.length ? Number(rows[0].v) : 0;
|
|
62
|
+
sql.exec("INSERT INTO agent_messages_archive (generation, seq, body) SELECT ?, seq, body FROM agent_messages", generation);
|
|
63
|
+
sql.exec("DELETE FROM agent_messages");
|
|
64
|
+
sql.exec("INSERT INTO agent_steps_archive (generation, id, output) SELECT ?, id, output FROM agent_steps", generation);
|
|
65
|
+
sql.exec("DELETE FROM agent_steps");
|
|
66
|
+
this.setStatus("new");
|
|
67
|
+
sql.exec("INSERT INTO agent_meta (k, v) VALUES ('generation', ?) ON CONFLICT(k) DO UPDATE SET v = ?", String(generation + 1), String(generation + 1));
|
|
68
|
+
return generation;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
55
71
|
unwrap() {
|
|
56
72
|
return this.sql;
|
|
57
73
|
}
|
|
@@ -160,6 +176,9 @@ var AgentDurableObject = class {
|
|
|
160
176
|
services: this.services
|
|
161
177
|
}, () => session.start(input));
|
|
162
178
|
}
|
|
179
|
+
reset(opts) {
|
|
180
|
+
return this.resolveSession(opts?.session).reset();
|
|
181
|
+
}
|
|
163
182
|
transcript() {
|
|
164
183
|
if (this.session) return this.session.transcript();
|
|
165
184
|
if (this.store.getSessionKey() !== void 0) return this.resolveSession().transcript();
|
|
@@ -171,6 +190,7 @@ var AgentDurableObject = class {
|
|
|
171
190
|
if (req.method === "POST" && url.pathname.endsWith("/turn")) {
|
|
172
191
|
const { userText, turnId, event, trigger } = await req.json();
|
|
173
192
|
await ensureScope();
|
|
193
|
+
const replace = url.searchParams.get("replace") === "1";
|
|
174
194
|
const wantsDeliver = url.searchParams.get("deliver") === "1";
|
|
175
195
|
let deliverChannel;
|
|
176
196
|
if (wantsDeliver) {
|
|
@@ -189,7 +209,8 @@ var AgentDurableObject = class {
|
|
|
189
209
|
userText,
|
|
190
210
|
turnId,
|
|
191
211
|
event,
|
|
192
|
-
trigger
|
|
212
|
+
trigger,
|
|
213
|
+
replace
|
|
193
214
|
}));
|
|
194
215
|
} catch (err) {
|
|
195
216
|
return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
|
|
@@ -258,6 +279,19 @@ var AgentDurableObject = class {
|
|
|
258
279
|
}
|
|
259
280
|
return new Response(sseTurnStream(session, turnId), { headers: SSE_HEADERS });
|
|
260
281
|
}
|
|
282
|
+
if (req.method === "POST" && url.pathname.endsWith("/reset")) {
|
|
283
|
+
let session;
|
|
284
|
+
try {
|
|
285
|
+
session = this.resolveSession(key);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
return Response.json(await session.reset());
|
|
291
|
+
} catch (err) {
|
|
292
|
+
return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
261
295
|
if (url.pathname.endsWith("/transcript")) {
|
|
262
296
|
if (key !== void 0) try {
|
|
263
297
|
return Response.json({ transcript: this.resolveSession(key).transcript() });
|
|
@@ -266,7 +300,7 @@ var AgentDurableObject = class {
|
|
|
266
300
|
}
|
|
267
301
|
return Response.json({ transcript: this.transcript() });
|
|
268
302
|
}
|
|
269
|
-
return new Response("agent DO — POST /turn, POST /resume, or GET /transcript", { status: 404 });
|
|
303
|
+
return new Response("agent DO — POST /turn, POST /resume, POST /reset, or GET /transcript", { status: 404 });
|
|
270
304
|
}
|
|
271
305
|
};
|
|
272
306
|
const SSE_HEADERS = {
|
|
@@ -292,7 +326,7 @@ function sseTurnStream(session, turnId) {
|
|
|
292
326
|
}, 2e4);
|
|
293
327
|
unsub = session.observe((e) => {
|
|
294
328
|
controller.enqueue(enc.encode(`data: ${JSON.stringify(e)}\n\n`));
|
|
295
|
-
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "input.requested") {
|
|
329
|
+
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "turn.cancelled" || e.type === "input.requested") {
|
|
296
330
|
stop();
|
|
297
331
|
controller.close();
|
|
298
332
|
}
|
|
@@ -309,7 +343,7 @@ function observeTurnEvents(session, turnId) {
|
|
|
309
343
|
let notify;
|
|
310
344
|
const unsub = session.observe((e) => {
|
|
311
345
|
queue.push(e);
|
|
312
|
-
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "input.requested") terminal = true;
|
|
346
|
+
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "turn.cancelled" || e.type === "input.requested") terminal = true;
|
|
313
347
|
notify?.();
|
|
314
348
|
}, { turnId });
|
|
315
349
|
return { [Symbol.asyncIterator]() {
|
|
@@ -366,6 +400,7 @@ async function sseTurnFinalText(res) {
|
|
|
366
400
|
const e = JSON.parse(line.slice(5).trim());
|
|
367
401
|
if (e.type === "turn.completed") return e.text;
|
|
368
402
|
if (e.type === "turn.failed") throw new Error(e.error.message, { cause: e.error });
|
|
403
|
+
if (e.type === "turn.cancelled") throw new Error(`turn ${e.turnId} was cancelled (${e.reason})`);
|
|
369
404
|
}
|
|
370
405
|
if (done) break;
|
|
371
406
|
}
|
|
@@ -451,7 +486,7 @@ function durableChannelSurface(getNamespace, opts) {
|
|
|
451
486
|
run: async (message, o) => {
|
|
452
487
|
const namespace = getNamespace();
|
|
453
488
|
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
454
|
-
return sseTurnFinalText(await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(
|
|
489
|
+
return sseTurnFinalText(await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(`https://do/turn${o?.replace ? "?replace=1" : ""}`, {
|
|
455
490
|
method: "POST",
|
|
456
491
|
body: serializeTurn(message, o)
|
|
457
492
|
})));
|
|
@@ -459,7 +494,7 @@ function durableChannelSurface(getNamespace, opts) {
|
|
|
459
494
|
runDetached: async (message, o) => {
|
|
460
495
|
const namespace = getNamespace();
|
|
461
496
|
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
462
|
-
const res = await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(
|
|
497
|
+
const res = await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(`https://do/turn?detach=1${o?.replace ? "&replace=1" : ""}`, {
|
|
463
498
|
method: "POST",
|
|
464
499
|
body: serializeTurn(message, o)
|
|
465
500
|
}));
|
|
@@ -472,7 +507,7 @@ function durableChannelSurface(getNamespace, opts) {
|
|
|
472
507
|
runDelivered: async (message, o) => {
|
|
473
508
|
const namespace = getNamespace();
|
|
474
509
|
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
475
|
-
const res = await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(
|
|
510
|
+
const res = await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(`https://do/turn?deliver=1${o?.replace ? "&replace=1" : ""}`, {
|
|
476
511
|
method: "POST",
|
|
477
512
|
body: serializeTurn(message, o)
|
|
478
513
|
}));
|
|
@@ -486,7 +521,7 @@ function durableChannelSurface(getNamespace, opts) {
|
|
|
486
521
|
runStream: async function* (message, o) {
|
|
487
522
|
const namespace = getNamespace();
|
|
488
523
|
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
489
|
-
yield* sseTurnEvents(await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(
|
|
524
|
+
yield* sseTurnEvents(await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request(`https://do/turn${o?.replace ? "?replace=1" : ""}`, {
|
|
490
525
|
method: "POST",
|
|
491
526
|
body: serializeTurn(message, o)
|
|
492
527
|
})));
|
|
@@ -512,6 +547,16 @@ function durableChannelSurface(getNamespace, opts) {
|
|
|
512
547
|
throw new Error(`resumeDelivered: resume was not accepted (status ${res.status}): ${detail}`);
|
|
513
548
|
}
|
|
514
549
|
return await res.json();
|
|
550
|
+
},
|
|
551
|
+
resetSession: async (o) => {
|
|
552
|
+
const namespace = getNamespace();
|
|
553
|
+
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
554
|
+
const res = await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request("https://do/reset", { method: "POST" }));
|
|
555
|
+
if (!res.ok) {
|
|
556
|
+
const detail = (await res.text()).slice(0, 200);
|
|
557
|
+
throw new Error(`resetSession: reset was not accepted (status ${res.status}): ${detail}`);
|
|
558
|
+
}
|
|
559
|
+
return await res.json();
|
|
515
560
|
}
|
|
516
561
|
});
|
|
517
562
|
}
|
package/dist/agent-native.js
CHANGED
|
@@ -51,6 +51,21 @@ var SqliteSessionStore = class {
|
|
|
51
51
|
throw e;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
reset() {
|
|
55
|
+
return this.tx(() => {
|
|
56
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS agent_messages_archive (session_id TEXT, generation INTEGER, seq INTEGER, body TEXT)`);
|
|
57
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS agent_steps_archive (session_id TEXT, generation INTEGER, id TEXT, output TEXT)`);
|
|
58
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS agent_session_generations (session_id TEXT PRIMARY KEY, generation INTEGER)`);
|
|
59
|
+
const generation = this.db.query("SELECT generation FROM agent_session_generations WHERE session_id = ?").get(this.sid)?.generation ?? 0;
|
|
60
|
+
this.db.query("INSERT INTO agent_messages_archive (session_id, generation, seq, body) SELECT session_id, ?, seq, body FROM agent_messages WHERE session_id = ?").run(generation, this.sid);
|
|
61
|
+
this.db.query("DELETE FROM agent_messages WHERE session_id = ?").run(this.sid);
|
|
62
|
+
this.db.query("INSERT INTO agent_steps_archive (session_id, generation, id, output) SELECT session_id, ?, id, output FROM agent_steps WHERE session_id = ?").run(generation, this.sid);
|
|
63
|
+
this.db.query("DELETE FROM agent_steps WHERE session_id = ?").run(this.sid);
|
|
64
|
+
this.db.query("INSERT INTO agent_sessions (session_id, status) VALUES (?, 'new') ON CONFLICT(session_id) DO UPDATE SET status = 'new'").run(this.sid);
|
|
65
|
+
this.db.query("INSERT INTO agent_session_generations (session_id, generation) VALUES (?, ?) ON CONFLICT(session_id) DO UPDATE SET generation = ?").run(this.sid, generation + 1, generation + 1);
|
|
66
|
+
return generation;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
54
69
|
unwrap() {
|
|
55
70
|
return this.db;
|
|
56
71
|
}
|
|
@@ -99,6 +114,8 @@ var MemorySessionStore = class {
|
|
|
99
114
|
msgs = [];
|
|
100
115
|
steps = /* @__PURE__ */ new Map();
|
|
101
116
|
status = "new";
|
|
117
|
+
generation = 0;
|
|
118
|
+
archives = [];
|
|
102
119
|
appendMessage(m) {
|
|
103
120
|
this.msgs.push(m);
|
|
104
121
|
}
|
|
@@ -126,6 +143,18 @@ var MemorySessionStore = class {
|
|
|
126
143
|
tx(fn) {
|
|
127
144
|
return fn();
|
|
128
145
|
}
|
|
146
|
+
reset() {
|
|
147
|
+
const generation = this.generation++;
|
|
148
|
+
this.archives.push({
|
|
149
|
+
generation,
|
|
150
|
+
msgs: this.msgs,
|
|
151
|
+
steps: this.steps
|
|
152
|
+
});
|
|
153
|
+
this.msgs = [];
|
|
154
|
+
this.steps = /* @__PURE__ */ new Map();
|
|
155
|
+
this.status = "new";
|
|
156
|
+
return generation;
|
|
157
|
+
}
|
|
129
158
|
unwrap() {}
|
|
130
159
|
};
|
|
131
160
|
var MemoryRuntime = class {
|
|
@@ -166,14 +195,17 @@ function mountAgent(agent, runtime, opts = {}) {
|
|
|
166
195
|
turnId: o?.turnId,
|
|
167
196
|
userText: message,
|
|
168
197
|
event: o?.event,
|
|
169
|
-
trigger: o?.trigger
|
|
198
|
+
trigger: o?.trigger,
|
|
199
|
+
replace: o?.replace
|
|
170
200
|
}),
|
|
171
201
|
runDetached: async (message, o) => runtime.session(agent.name, o?.session ?? "default").start({
|
|
172
202
|
turnId: o?.turnId,
|
|
173
203
|
userText: message,
|
|
174
204
|
event: o?.event,
|
|
175
|
-
trigger: o?.trigger
|
|
176
|
-
|
|
205
|
+
trigger: o?.trigger,
|
|
206
|
+
replace: o?.replace
|
|
207
|
+
}),
|
|
208
|
+
resetSession: (o) => runtime.session(agent.name, o?.session ?? "default").reset()
|
|
177
209
|
};
|
|
178
210
|
const channels = channelFetch(agent, ctx);
|
|
179
211
|
const surface = async (req) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junejs/server",
|
|
3
|
-
"version": "1.0.0-dev.
|
|
3
|
+
"version": "1.0.0-dev.14",
|
|
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",
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"rolldown": "^1.1.0"
|
|
133
133
|
},
|
|
134
134
|
"peerDependencies": {
|
|
135
|
-
"@junejs/core": ">=0.2.0-dev.
|
|
135
|
+
"@junejs/core": ">=0.2.0-dev.32",
|
|
136
136
|
"react": "^19.2.0",
|
|
137
137
|
"react-dom": "^19.2.0",
|
|
138
138
|
"react-server-dom-webpack": "19.2.7",
|
package/src/agent-durable.ts
CHANGED
|
@@ -137,6 +137,28 @@ export class DoSessionStore implements SessionStore {
|
|
|
137
137
|
tx<T>(fn: () => T): T {
|
|
138
138
|
return this.storage.transactionSync(fn);
|
|
139
139
|
}
|
|
140
|
+
// Session reset (#129): ARCHIVE messages/steps under the current generation (the audit
|
|
141
|
+
// trail — never deleted), clear the live tables, status → "new". The session key in
|
|
142
|
+
// agent_meta is deliberately untouched: the DO's identity survives its history. Archive
|
|
143
|
+
// tables + the generation counter are created lazily, so existing objects stay
|
|
144
|
+
// untouched until their first reset. seq keeps counting across generations (DELETE
|
|
145
|
+
// doesn't reset AUTOINCREMENT), so an archived (generation, seq) stays unique.
|
|
146
|
+
reset(): number {
|
|
147
|
+
return this.storage.transactionSync(() => {
|
|
148
|
+
const sql = this.sql;
|
|
149
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS agent_messages_archive (generation INTEGER, seq INTEGER, body TEXT)`);
|
|
150
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS agent_steps_archive (generation INTEGER, id TEXT, output TEXT)`);
|
|
151
|
+
const rows = this.sql.exec<{ v: string }>("SELECT v FROM agent_meta WHERE k = 'generation'").toArray();
|
|
152
|
+
const generation = rows.length ? Number(rows[0]!.v) : 0;
|
|
153
|
+
sql.exec("INSERT INTO agent_messages_archive (generation, seq, body) SELECT ?, seq, body FROM agent_messages", generation);
|
|
154
|
+
sql.exec("DELETE FROM agent_messages");
|
|
155
|
+
sql.exec("INSERT INTO agent_steps_archive (generation, id, output) SELECT ?, id, output FROM agent_steps", generation);
|
|
156
|
+
sql.exec("DELETE FROM agent_steps");
|
|
157
|
+
this.setStatus("new");
|
|
158
|
+
sql.exec("INSERT INTO agent_meta (k, v) VALUES ('generation', ?) ON CONFLICT(k) DO UPDATE SET v = ?", String(generation + 1), String(generation + 1));
|
|
159
|
+
return generation;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
140
162
|
unwrap<H = unknown>(): H {
|
|
141
163
|
return this.sql as unknown as H;
|
|
142
164
|
}
|
|
@@ -358,6 +380,11 @@ export class AgentDurableObject {
|
|
|
358
380
|
const session = this.resolveSession(input.session);
|
|
359
381
|
return runInScope({ resources: this.resources, services: this.services }, () => session.start(input));
|
|
360
382
|
}
|
|
383
|
+
// Session reset (#129) for custom shells — the direct sibling of the /reset route. No
|
|
384
|
+
// scope needed: reset touches only the store (no tools run).
|
|
385
|
+
reset(opts?: { session?: string }): Promise<{ previousSession: string; generation: number }> {
|
|
386
|
+
return this.resolveSession(opts?.session).reset();
|
|
387
|
+
}
|
|
361
388
|
// Read-only: folds the durable log. When an identity exists (live, or persisted from a
|
|
362
389
|
// prior life) the session resolves and caches like any other path. Only a read on a
|
|
363
390
|
// NEVER-keyed object stays non-committal — caching there would bake "self" in as the
|
|
@@ -378,6 +405,11 @@ export class AgentDurableObject {
|
|
|
378
405
|
if (req.method === "POST" && url.pathname.endsWith("/turn")) {
|
|
379
406
|
const { userText, turnId, event, trigger } = (await req.json()) as { userText: string; turnId?: string; event?: InboundEvent; trigger?: ProactiveTrigger };
|
|
380
407
|
await ensureScope();
|
|
408
|
+
// CANCEL-AND-REPLACE (#129): supersede every unfinished turn on this session before
|
|
409
|
+
// queuing this one (debounce). A mode flag like deliver/detach, so it rides the query
|
|
410
|
+
// string and composes with both. A suspended session still 409s below — replace
|
|
411
|
+
// never cancels a parked approval.
|
|
412
|
+
const replace = url.searchParams.get("replace") === "1";
|
|
381
413
|
// DELIVERED: like detach=1, the caller gets a 202 and goes away — but the reply is NOT
|
|
382
414
|
// dropped: this DO renders the turn's event stream through the source channel's own
|
|
383
415
|
// deliver() (the inbound renderer), under the DO's lifetime. Capability is checked
|
|
@@ -405,7 +437,7 @@ export class AgentDurableObject {
|
|
|
405
437
|
let started: { turnId: string };
|
|
406
438
|
try {
|
|
407
439
|
session = this.resolveSession(key);
|
|
408
|
-
started = runInScope({ resources: this.resources, services: this.services }, () => session.start({ userText, turnId, event, trigger }));
|
|
440
|
+
started = runInScope({ resources: this.resources, services: this.services }, () => session.start({ userText, turnId, event, trigger, replace }));
|
|
409
441
|
} catch (err) {
|
|
410
442
|
// e.g. the session is suspended awaiting input, or the key mis-matches this
|
|
411
443
|
// object's identity — a client-resolvable conflict, not a crash
|
|
@@ -516,6 +548,25 @@ export class AgentDurableObject {
|
|
|
516
548
|
}
|
|
517
549
|
return new Response(sseTurnStream(session, turnId), { headers: SSE_HEADERS });
|
|
518
550
|
}
|
|
551
|
+
// SESSION RESET (#129): terminally retire this session's history — supersede unfinished
|
|
552
|
+
// turns, archive messages/steps under the current generation (the audit trail), and
|
|
553
|
+
// answer with the audit handle. The response waits for in-flight turns to unwind at
|
|
554
|
+
// their next checkpoint boundary (the reset op runs on the turn chain), which is fine:
|
|
555
|
+
// a DO stays alive while it has pending work.
|
|
556
|
+
if (req.method === "POST" && url.pathname.endsWith("/reset")) {
|
|
557
|
+
let session: AgentSession;
|
|
558
|
+
try {
|
|
559
|
+
session = this.resolveSession(key);
|
|
560
|
+
} catch (err) {
|
|
561
|
+
return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
return Response.json(await session.reset());
|
|
565
|
+
} catch (err) {
|
|
566
|
+
// e.g. a store without reset() — a caller-resolvable refusal, not a crash
|
|
567
|
+
return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
519
570
|
if (url.pathname.endsWith("/transcript")) {
|
|
520
571
|
// A keyed read still conflict-checks (and may commit the key — it is authentic,
|
|
521
572
|
// idFromName routed it here); a key-less read stays non-committal via transcript().
|
|
@@ -528,7 +579,7 @@ export class AgentDurableObject {
|
|
|
528
579
|
}
|
|
529
580
|
return Response.json({ transcript: this.transcript() });
|
|
530
581
|
}
|
|
531
|
-
return new Response("agent DO — POST /turn, POST /resume, or GET /transcript", { status: 404 });
|
|
582
|
+
return new Response("agent DO — POST /turn, POST /resume, POST /reset, or GET /transcript", { status: 404 });
|
|
532
583
|
}
|
|
533
584
|
}
|
|
534
585
|
|
|
@@ -556,9 +607,9 @@ function sseTurnStream(session: AgentSession, turnId: string): ReadableStream<Ui
|
|
|
556
607
|
}, 20_000);
|
|
557
608
|
unsub = session.observe((e) => {
|
|
558
609
|
controller.enqueue(enc.encode(`data: ${JSON.stringify(e)}\n\n`));
|
|
559
|
-
// terminal for the STREAM: completed, failed, OR suspended (input.requested
|
|
560
|
-
// parked; the stream ends and a later /resume opens a fresh continuation stream).
|
|
561
|
-
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "input.requested") { stop(); controller.close(); }
|
|
610
|
+
// terminal for the STREAM: completed, failed, cancelled, OR suspended (input.requested
|
|
611
|
+
// → the turn parked; the stream ends and a later /resume opens a fresh continuation stream).
|
|
612
|
+
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "turn.cancelled" || e.type === "input.requested") { stop(); controller.close(); }
|
|
562
613
|
}, { turnId });
|
|
563
614
|
},
|
|
564
615
|
cancel() { stop(); },
|
|
@@ -578,7 +629,7 @@ function observeTurnEvents(session: AgentSession, turnId: string): AsyncIterable
|
|
|
578
629
|
let notify: (() => void) | undefined;
|
|
579
630
|
const unsub = session.observe((e) => {
|
|
580
631
|
queue.push(e);
|
|
581
|
-
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "input.requested") terminal = true;
|
|
632
|
+
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "turn.cancelled" || e.type === "input.requested") terminal = true;
|
|
582
633
|
notify?.();
|
|
583
634
|
}, { turnId });
|
|
584
635
|
return {
|
|
@@ -627,6 +678,7 @@ export async function sseTurnFinalText(res: Response): Promise<string> {
|
|
|
627
678
|
const e = JSON.parse(line.slice(5).trim()) as TurnEvent;
|
|
628
679
|
if (e.type === "turn.completed") return e.text;
|
|
629
680
|
if (e.type === "turn.failed") throw new Error(e.error.message, { cause: e.error }); // full TurnError rides along
|
|
681
|
+
if (e.type === "turn.cancelled") throw new Error(`turn ${e.turnId} was cancelled (${e.reason})`);
|
|
630
682
|
}
|
|
631
683
|
if (done) break;
|
|
632
684
|
}
|
|
@@ -827,7 +879,7 @@ export function durableChannelSurface(
|
|
|
827
879
|
namespace,
|
|
828
880
|
opts.agentName,
|
|
829
881
|
o?.session ?? "default",
|
|
830
|
-
new Request(
|
|
882
|
+
new Request(`https://do/turn${o?.replace ? "?replace=1" : ""}`, { method: "POST", body: serializeTurn(message, o) }),
|
|
831
883
|
);
|
|
832
884
|
// /turn streams SSE; the simple path collapses it to the final text.
|
|
833
885
|
return sseTurnFinalText(res);
|
|
@@ -843,7 +895,7 @@ export function durableChannelSurface(
|
|
|
843
895
|
namespace,
|
|
844
896
|
opts.agentName,
|
|
845
897
|
o?.session ?? "default",
|
|
846
|
-
new Request(
|
|
898
|
+
new Request(`https://do/turn?detach=1${o?.replace ? "&replace=1" : ""}`, { method: "POST", body: serializeTurn(message, o) }),
|
|
847
899
|
);
|
|
848
900
|
if (res.status !== 202) {
|
|
849
901
|
const detail = (await res.text()).slice(0, 200);
|
|
@@ -864,7 +916,7 @@ export function durableChannelSurface(
|
|
|
864
916
|
namespace,
|
|
865
917
|
opts.agentName,
|
|
866
918
|
o?.session ?? "default",
|
|
867
|
-
new Request(
|
|
919
|
+
new Request(`https://do/turn?deliver=1${o?.replace ? "&replace=1" : ""}`, { method: "POST", body: serializeTurn(message, o) }),
|
|
868
920
|
);
|
|
869
921
|
if (res.status === 501) {
|
|
870
922
|
throw new DeliverUnsupportedError(`runDelivered: ${((await res.json().catch(() => undefined)) as { error?: string } | undefined)?.error ?? "the turn host cannot deliver this turn"}`);
|
|
@@ -883,7 +935,7 @@ export function durableChannelSurface(
|
|
|
883
935
|
namespace,
|
|
884
936
|
opts.agentName,
|
|
885
937
|
o?.session ?? "default",
|
|
886
|
-
new Request(
|
|
938
|
+
new Request(`https://do/turn${o?.replace ? "?replace=1" : ""}`, { method: "POST", body: serializeTurn(message, o) }),
|
|
887
939
|
);
|
|
888
940
|
yield* sseTurnEvents(res);
|
|
889
941
|
},
|
|
@@ -924,6 +976,24 @@ export function durableChannelSurface(
|
|
|
924
976
|
}
|
|
925
977
|
return (await res.json()) as { turnId: string };
|
|
926
978
|
},
|
|
979
|
+
// SESSION RESET (#129): retire the session's history on its DO. The response carries
|
|
980
|
+
// the audit handle ({ previousSession, generation }); a 409 (store without reset(),
|
|
981
|
+
// key mismatch) surfaces as an ordinary error — there is no fallback to invent.
|
|
982
|
+
resetSession: async (o) => {
|
|
983
|
+
const namespace = getNamespace();
|
|
984
|
+
if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
|
|
985
|
+
const res = await durableFetch(
|
|
986
|
+
namespace,
|
|
987
|
+
opts.agentName,
|
|
988
|
+
o?.session ?? "default",
|
|
989
|
+
new Request("https://do/reset", { method: "POST" }),
|
|
990
|
+
);
|
|
991
|
+
if (!res.ok) {
|
|
992
|
+
const detail = (await res.text()).slice(0, 200);
|
|
993
|
+
throw new Error(`resetSession: reset was not accepted (status ${res.status}): ${detail}`);
|
|
994
|
+
}
|
|
995
|
+
return (await res.json()) as { previousSession: string; generation: number };
|
|
996
|
+
},
|
|
927
997
|
};
|
|
928
998
|
return channelDispatch(resolved, ctx);
|
|
929
999
|
}
|
package/src/agent-native.ts
CHANGED
|
@@ -79,6 +79,26 @@ class SqliteSessionStore implements SessionStore {
|
|
|
79
79
|
throw e;
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
// Session reset (#129): ARCHIVE this session's messages/steps under the current
|
|
83
|
+
// generation (audit trail — never deleted), clear the live rows, status → "new".
|
|
84
|
+
// Archive tables + the generation counter are created lazily here, so existing
|
|
85
|
+
// databases stay untouched until the first reset.
|
|
86
|
+
reset(): number {
|
|
87
|
+
return this.tx(() => {
|
|
88
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS agent_messages_archive (session_id TEXT, generation INTEGER, seq INTEGER, body TEXT)`);
|
|
89
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS agent_steps_archive (session_id TEXT, generation INTEGER, id TEXT, output TEXT)`);
|
|
90
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS agent_session_generations (session_id TEXT PRIMARY KEY, generation INTEGER)`);
|
|
91
|
+
const r = this.db.query("SELECT generation FROM agent_session_generations WHERE session_id = ?").get(this.sid) as { generation: number } | undefined;
|
|
92
|
+
const generation = r?.generation ?? 0;
|
|
93
|
+
this.db.query("INSERT INTO agent_messages_archive (session_id, generation, seq, body) SELECT session_id, ?, seq, body FROM agent_messages WHERE session_id = ?").run(generation, this.sid);
|
|
94
|
+
this.db.query("DELETE FROM agent_messages WHERE session_id = ?").run(this.sid);
|
|
95
|
+
this.db.query("INSERT INTO agent_steps_archive (session_id, generation, id, output) SELECT session_id, ?, id, output FROM agent_steps WHERE session_id = ?").run(generation, this.sid);
|
|
96
|
+
this.db.query("DELETE FROM agent_steps WHERE session_id = ?").run(this.sid);
|
|
97
|
+
this.db.query("INSERT INTO agent_sessions (session_id, status) VALUES (?, 'new') ON CONFLICT(session_id) DO UPDATE SET status = 'new'").run(this.sid);
|
|
98
|
+
this.db.query("INSERT INTO agent_session_generations (session_id, generation) VALUES (?, ?) ON CONFLICT(session_id) DO UPDATE SET generation = ?").run(this.sid, generation + 1, generation + 1);
|
|
99
|
+
return generation;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
82
102
|
unwrap<H = unknown>(): H { return this.db as unknown as H; }
|
|
83
103
|
}
|
|
84
104
|
|
|
@@ -139,6 +159,10 @@ class MemorySessionStore implements SessionStore {
|
|
|
139
159
|
private msgs: Msg[] = [];
|
|
140
160
|
private steps = new Map<string, unknown>();
|
|
141
161
|
private status = "new";
|
|
162
|
+
private generation = 0;
|
|
163
|
+
// Retired generations (#129) — kept for the audit contract even in memory, so the
|
|
164
|
+
// backend behaves like the durable tiers within one process lifetime.
|
|
165
|
+
private readonly archives: { generation: number; msgs: Msg[]; steps: Map<string, unknown> }[] = [];
|
|
142
166
|
appendMessage(m: Msg) { this.msgs.push(m); }
|
|
143
167
|
messages(): Msg[] { return this.msgs.slice(); }
|
|
144
168
|
hasOpeningMessage(turnId: string): boolean { return this.msgs.some((m) => (m.role === "user" || m.role === "trigger") && m.turnId === turnId); }
|
|
@@ -148,6 +172,14 @@ class MemorySessionStore implements SessionStore {
|
|
|
148
172
|
getStatus(): string { return this.status; }
|
|
149
173
|
setStatus(s: string) { this.status = s; }
|
|
150
174
|
tx<T>(fn: () => T): T { return fn(); } // no rollback: an in-memory store is not a durability tier
|
|
175
|
+
reset(): number {
|
|
176
|
+
const generation = this.generation++;
|
|
177
|
+
this.archives.push({ generation, msgs: this.msgs, steps: this.steps });
|
|
178
|
+
this.msgs = [];
|
|
179
|
+
this.steps = new Map();
|
|
180
|
+
this.status = "new";
|
|
181
|
+
return generation;
|
|
182
|
+
}
|
|
151
183
|
unwrap<H = unknown>(): H { return undefined as unknown as H; }
|
|
152
184
|
}
|
|
153
185
|
|
|
@@ -216,12 +248,16 @@ export function mountAgent(
|
|
|
216
248
|
agent,
|
|
217
249
|
services: opts.services, // same DI bag reachable from channel hooks (parity with durableChannelSurface)
|
|
218
250
|
run: (message, o) =>
|
|
219
|
-
runtime.session(agent.name, o?.session ?? "default").turn({ turnId: o?.turnId, userText: message, event: o?.event, trigger: o?.trigger }),
|
|
251
|
+
runtime.session(agent.name, o?.session ?? "default").turn({ turnId: o?.turnId, userText: message, event: o?.event, trigger: o?.trigger, replace: o?.replace }),
|
|
220
252
|
// FIRE-AND-FORGET (#77): start() without awaiting the result. Native has no waitUntil
|
|
221
253
|
// ceiling (Node keeps floating promises alive), but the seam is the same so a channel
|
|
222
254
|
// written against ctx.runDetached behaves identically on both targets.
|
|
223
255
|
runDetached: async (message, o) =>
|
|
224
|
-
runtime.session(agent.name, o?.session ?? "default").start({ turnId: o?.turnId, userText: message, event: o?.event, trigger: o?.trigger }),
|
|
256
|
+
runtime.session(agent.name, o?.session ?? "default").start({ turnId: o?.turnId, userText: message, event: o?.event, trigger: o?.trigger, replace: o?.replace }),
|
|
257
|
+
// SESSION RESET (#129): same seam as durableChannelSurface — the in-process runtimes'
|
|
258
|
+
// stores implement archival, so a channel written against ctx.resetSession behaves
|
|
259
|
+
// identically on both targets.
|
|
260
|
+
resetSession: (o) => runtime.session(agent.name, o?.session ?? "default").reset(),
|
|
225
261
|
};
|
|
226
262
|
const channels = channelFetch(agent, ctx);
|
|
227
263
|
const surface = async (req: Request): Promise<Response | null> => {
|