@tanstack/ai-persistence 0.2.0 → 0.4.0
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/esm/capabilities.d.ts +6 -0
- package/dist/esm/capabilities.js +3 -1
- package/dist/esm/capabilities.js.map +1 -1
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.js +2 -2
- package/dist/esm/memory.js +41 -1
- package/dist/esm/memory.js.map +1 -1
- package/dist/esm/middleware.js +291 -53
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/testkit/conformance.js +63 -10
- package/dist/esm/testkit/conformance.js.map +1 -1
- package/dist/esm/types.d.ts +35 -1
- package/dist/esm/types.js.map +1 -1
- package/package.json +3 -3
- package/skills/ai-persistence/SKILL.md +7 -0
- package/skills/ai-persistence/build-cloudflare-artifact-store/SKILL.md +47 -25
- package/skills/ai-persistence/server/SKILL.md +17 -10
- package/src/capabilities.ts +10 -0
- package/src/index.ts +5 -0
- package/src/memory.ts +78 -1
- package/src/middleware.ts +537 -83
- package/src/testkit/conformance.ts +77 -12
- package/src/types.ts +38 -1
package/dist/esm/middleware.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { validateChatPersistenceStores, validateGenerationPersistenceStores } from "./types.js";
|
|
2
|
-
import { InterruptsCapability, PersistenceCapability, provideInterrupts, providePersistence } from "./capabilities.js";
|
|
2
|
+
import { InterruptsCapability, PersistenceCapability, PersistenceCompletionCapability, provideInterrupts, providePersistence, providePersistenceCompletion } from "./capabilities.js";
|
|
3
3
|
import { artifactBlobKey } from "./retrieve.js";
|
|
4
|
-
import { defineChatMiddleware, getDetachableRun, wasCancelRequested } from "@tanstack/ai";
|
|
5
|
-
import { providePendingTurn } from "@tanstack/ai/adapter-internals";
|
|
4
|
+
import { InterruptResumeValidationError, defineChatMiddleware, getDetachableRun, readInterruptBinding, validateInterruptResumeBatch, wasCancelRequested } from "@tanstack/ai";
|
|
5
|
+
import { createInterruptBinding, getGenericInterruptDefinitionRegistry, providePendingTurn, rehydrateInterruptRequest } from "@tanstack/ai/adapter-internals";
|
|
6
6
|
import { base64ToUint8Array } from "@tanstack/ai-utils";
|
|
7
7
|
//#region src/middleware.ts
|
|
8
8
|
/**
|
|
@@ -23,30 +23,116 @@ var DEFAULT_ARTIFACT_FETCH_TIMEOUT_MS = 3e4;
|
|
|
23
23
|
var DEFAULT_MAX_ARTIFACT_BYTES = 1073741824;
|
|
24
24
|
var runState = /* @__PURE__ */ new WeakMap();
|
|
25
25
|
var validResumeStatuses = /* @__PURE__ */ new Set(["resolved", "cancelled"]);
|
|
26
|
-
function
|
|
26
|
+
function mergeMaps(left, right) {
|
|
27
|
+
if (!left && !right) return void 0;
|
|
28
|
+
return new Map([...left ?? [], ...right ?? []]);
|
|
29
|
+
}
|
|
30
|
+
function mergeSets(left, right) {
|
|
31
|
+
if (!left && !right) return void 0;
|
|
32
|
+
return /* @__PURE__ */ new Set([...left ?? [], ...right ?? []]);
|
|
33
|
+
}
|
|
34
|
+
function mergeResumeToolState(left, right) {
|
|
35
|
+
if (!left) return right;
|
|
36
|
+
if (!right) return left;
|
|
37
|
+
return {
|
|
38
|
+
approvals: mergeMaps(left.approvals, right.approvals),
|
|
39
|
+
clientToolResults: mergeMaps(left.clientToolResults, right.clientToolResults),
|
|
40
|
+
genericInterrupts: mergeMaps(left.genericInterrupts, right.genericInterrupts),
|
|
41
|
+
genericInterruptRequests: mergeMaps(left.genericInterruptRequests, right.genericInterruptRequests),
|
|
42
|
+
deniedToolResults: mergeMaps(left.deniedToolResults, right.deniedToolResults),
|
|
43
|
+
cancelledToolCallIds: mergeSets(left.cancelledToolCallIds, right.cancelledToolCallIds)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function rejectMixedRunPending(pending, ctx) {
|
|
47
|
+
if (new Set(pending.map((interrupt) => interrupt.runId)).size <= 1) return;
|
|
48
|
+
throw new InterruptResumeValidationError([{
|
|
49
|
+
scope: "batch",
|
|
50
|
+
threadId: ctx.threadId,
|
|
51
|
+
interruptedRunId: ctx.runId,
|
|
52
|
+
generation: 0,
|
|
53
|
+
interruptIds: pending.map((interrupt) => interrupt.interruptId),
|
|
54
|
+
code: "stale",
|
|
55
|
+
message: "Thread has pending interrupts from more than one run.",
|
|
56
|
+
source: "server",
|
|
57
|
+
retryable: false
|
|
58
|
+
}]);
|
|
59
|
+
}
|
|
60
|
+
function validatePendingResumes(pending, resume, ctx) {
|
|
61
|
+
const interruptedRunId = pending[0]?.runId ?? ctx.runId;
|
|
62
|
+
const failure = (interruptId, code, message) => {
|
|
63
|
+
throw new InterruptResumeValidationError([{
|
|
64
|
+
scope: "item",
|
|
65
|
+
threadId: ctx.threadId,
|
|
66
|
+
interruptedRunId,
|
|
67
|
+
generation: 0,
|
|
68
|
+
interruptId,
|
|
69
|
+
code,
|
|
70
|
+
message,
|
|
71
|
+
source: "client",
|
|
72
|
+
retryable: false
|
|
73
|
+
}, {
|
|
74
|
+
scope: "batch",
|
|
75
|
+
threadId: ctx.threadId,
|
|
76
|
+
interruptedRunId,
|
|
77
|
+
generation: 0,
|
|
78
|
+
interruptIds: pending.map((interrupt) => interrupt.interruptId),
|
|
79
|
+
code: code === "conflict" ? "conflict" : "incomplete-batch",
|
|
80
|
+
message: "Resume entries must resolve or cancel the complete interrupt batch.",
|
|
81
|
+
source: "client",
|
|
82
|
+
retryable: false
|
|
83
|
+
}]);
|
|
84
|
+
};
|
|
27
85
|
const pendingInterruptIds = new Set(pending.map((interrupt) => interrupt.interruptId));
|
|
28
|
-
const resumeByInterruptId = new Map(
|
|
86
|
+
const resumeByInterruptId = /* @__PURE__ */ new Map();
|
|
87
|
+
for (const entry of resume ?? []) {
|
|
88
|
+
if (resumeByInterruptId.has(entry.interruptId)) return failure(entry.interruptId, "conflict", `Interrupt ${entry.interruptId} has duplicate resume entries.`);
|
|
89
|
+
resumeByInterruptId.set(entry.interruptId, entry);
|
|
90
|
+
}
|
|
29
91
|
if (pending.length === 0) {
|
|
30
92
|
const staleEntry = resume?.[0];
|
|
31
|
-
if (staleEntry)
|
|
93
|
+
if (staleEntry) return failure(staleEntry.interruptId, "unknown-interrupt", `Resume entry references non-pending interrupt ${staleEntry.interruptId}.`);
|
|
32
94
|
return resumeByInterruptId;
|
|
33
95
|
}
|
|
34
|
-
|
|
96
|
+
const firstPending = pending[0];
|
|
97
|
+
if (firstPending === void 0) return resumeByInterruptId;
|
|
98
|
+
if (!resume || resume.length === 0) return failure(firstPending.interruptId, "unknown-interrupt", `Thread has pending interrupts; resume is required before accepting new input.`);
|
|
35
99
|
for (const interrupt of pending) {
|
|
36
100
|
const entry = resumeByInterruptId.get(interrupt.interruptId);
|
|
37
|
-
if (!entry)
|
|
38
|
-
if (!validResumeStatuses.has(entry.status))
|
|
101
|
+
if (!entry) return failure(interrupt.interruptId, "unknown-interrupt", `Missing resume entry for pending interrupt ${interrupt.interruptId}.`);
|
|
102
|
+
if (!validResumeStatuses.has(entry.status)) return failure(interrupt.interruptId, "unknown-interrupt", `Invalid resume status for pending interrupt ${interrupt.interruptId}: ${entry.status}.`);
|
|
39
103
|
}
|
|
40
|
-
for (const entry of resume) if (!pendingInterruptIds.has(entry.interruptId))
|
|
104
|
+
for (const entry of resume) if (!pendingInterruptIds.has(entry.interruptId)) return failure(entry.interruptId, "unknown-interrupt", `Resume entry references non-pending interrupt ${entry.interruptId}.`);
|
|
41
105
|
return resumeByInterruptId;
|
|
42
106
|
}
|
|
43
107
|
async function applyPendingResumes(pending, resumeByInterruptId, interrupts) {
|
|
108
|
+
const entries = [];
|
|
44
109
|
for (const interrupt of pending) {
|
|
45
110
|
const entry = resumeByInterruptId.get(interrupt.interruptId);
|
|
46
111
|
if (!entry) continue;
|
|
47
|
-
if (entry.status === "resolved")
|
|
48
|
-
|
|
112
|
+
if (entry.status === "resolved") entries.push({
|
|
113
|
+
interruptId: interrupt.interruptId,
|
|
114
|
+
status: "resolved",
|
|
115
|
+
response: entry.payload
|
|
116
|
+
});
|
|
117
|
+
else entries.push({
|
|
118
|
+
interruptId: interrupt.interruptId,
|
|
119
|
+
status: "cancelled"
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (interrupts.commitBatch) {
|
|
123
|
+
await interrupts.commitBatch(entries);
|
|
124
|
+
return;
|
|
49
125
|
}
|
|
126
|
+
const ids = /* @__PURE__ */ new Set();
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
if (ids.has(entry.interruptId)) throw new Error(`Interrupt batch contains duplicate id: ${entry.interruptId}.`);
|
|
129
|
+
ids.add(entry.interruptId);
|
|
130
|
+
const existing = await interrupts.get(entry.interruptId);
|
|
131
|
+
if (!existing) throw new Error(`Interrupt batch references missing id: ${entry.interruptId}.`);
|
|
132
|
+
if (existing.status !== "pending") throw new Error(`Interrupt batch references non-pending id: ${entry.interruptId}.`);
|
|
133
|
+
}
|
|
134
|
+
for (const entry of entries) if (entry.status === "resolved") await interrupts.resolve(entry.interruptId, entry.response);
|
|
135
|
+
else await interrupts.cancel(entry.interruptId);
|
|
50
136
|
}
|
|
51
137
|
/**
|
|
52
138
|
* Commit the resumes stashed in `onConfig`, marking each resumed interrupt
|
|
@@ -72,6 +158,147 @@ function interruptKind(interrupt) {
|
|
|
72
158
|
const metadata = objectValue(interrupt.payload.metadata);
|
|
73
159
|
return metadata ? stringField(metadata, "kind") : void 0;
|
|
74
160
|
}
|
|
161
|
+
function hasReservedInterruptBinding(payload) {
|
|
162
|
+
const metadata = objectValue(objectValue(payload)?.metadata);
|
|
163
|
+
return !!metadata && "tanstack:interruptBinding" in metadata;
|
|
164
|
+
}
|
|
165
|
+
function isPersistedInterruptDescriptor(value) {
|
|
166
|
+
const record = objectValue(value);
|
|
167
|
+
return !!record && typeof record.id === "string" && typeof record.reason === "string" && typeof record.message === "string";
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Does this pending record belong to the TanStack chat resume protocol?
|
|
171
|
+
*
|
|
172
|
+
* An external system can persist an AG-UI descriptor in the same durable
|
|
173
|
+
* thread. A descriptor without a TanStack binding or legacy tool marker stays
|
|
174
|
+
* pending for its owner, but it does not make this resume incomplete. Older
|
|
175
|
+
* opaque records remain owned because their provenance cannot be known.
|
|
176
|
+
*/
|
|
177
|
+
function isChatOwnedPendingInterrupt(interrupt) {
|
|
178
|
+
const kind = interruptKind(interrupt);
|
|
179
|
+
return !isPersistedInterruptDescriptor(interrupt.payload) || stringField(interrupt.payload, "toolCallId") !== void 0 || kind === "approval" || kind === "client_tool" || hasReservedInterruptBinding(interrupt.payload);
|
|
180
|
+
}
|
|
181
|
+
function durableGenericFailure(ctx, persisted, message) {
|
|
182
|
+
return new InterruptResumeValidationError([{
|
|
183
|
+
scope: "item",
|
|
184
|
+
threadId: ctx.threadId,
|
|
185
|
+
interruptedRunId: persisted.runId || ctx.runId,
|
|
186
|
+
generation: 0,
|
|
187
|
+
interruptId: persisted.interruptId,
|
|
188
|
+
code: "stale",
|
|
189
|
+
message,
|
|
190
|
+
source: "server",
|
|
191
|
+
retryable: false
|
|
192
|
+
}, {
|
|
193
|
+
scope: "batch",
|
|
194
|
+
threadId: ctx.threadId,
|
|
195
|
+
interruptedRunId: persisted.runId || ctx.runId,
|
|
196
|
+
generation: 0,
|
|
197
|
+
interruptIds: [persisted.interruptId],
|
|
198
|
+
code: "item-validation-failed",
|
|
199
|
+
message: "One or more persisted interrupt records are invalid.",
|
|
200
|
+
source: "server",
|
|
201
|
+
retryable: false
|
|
202
|
+
}]);
|
|
203
|
+
}
|
|
204
|
+
async function durableGenericResumeState(ctx, pending, resume, tools) {
|
|
205
|
+
const registry = getGenericInterruptDefinitionRegistry(ctx, { optional: true });
|
|
206
|
+
const records = [];
|
|
207
|
+
for (const persisted of pending) {
|
|
208
|
+
if (!isPersistedInterruptDescriptor(persisted.payload)) {
|
|
209
|
+
if (hasReservedInterruptBinding(persisted.payload)) throw durableGenericFailure(ctx, persisted, `Persisted interrupt ${persisted.interruptId} has an invalid binding descriptor.`);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const descriptor = persisted.payload;
|
|
213
|
+
const binding = readInterruptBinding(descriptor);
|
|
214
|
+
if (!binding) {
|
|
215
|
+
if (hasReservedInterruptBinding(descriptor)) throw durableGenericFailure(ctx, persisted, `Persisted interrupt ${persisted.interruptId} has an invalid or incomplete binding.`);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (descriptor.id !== persisted.interruptId || binding.interruptId !== persisted.interruptId || binding.interruptedRunId !== persisted.runId || binding.generation !== 0) throw durableGenericFailure(ctx, persisted, `Persisted interrupt ${persisted.interruptId} has stale correlation metadata.`);
|
|
219
|
+
if (binding.kind !== "generic") {
|
|
220
|
+
records.push({
|
|
221
|
+
interruptId: persisted.interruptId,
|
|
222
|
+
payload: descriptor,
|
|
223
|
+
binding
|
|
224
|
+
});
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (!binding.definitionId || !binding.key || binding.batchIndex === void 0) {
|
|
228
|
+
records.push({
|
|
229
|
+
interruptId: persisted.interruptId,
|
|
230
|
+
payload: descriptor,
|
|
231
|
+
binding
|
|
232
|
+
});
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (!registry) throw durableGenericFailure(ctx, persisted, `Persisted generic interrupt ${persisted.interruptId} cannot be restored because no interrupt registry is available.`);
|
|
236
|
+
const definition = registry.definitions.get(binding.definitionId);
|
|
237
|
+
if (!definition) throw durableGenericFailure(ctx, persisted, `Persisted generic interrupt definition ${binding.definitionId} is unavailable.`);
|
|
238
|
+
const payload = objectValue(descriptor.metadata)?.["tanstack:interruptPayload"];
|
|
239
|
+
let request;
|
|
240
|
+
try {
|
|
241
|
+
request = rehydrateInterruptRequest(definition, {
|
|
242
|
+
key: binding.key,
|
|
243
|
+
reason: descriptor.reason,
|
|
244
|
+
message: descriptor.message,
|
|
245
|
+
...descriptor.expiresAt !== void 0 ? { expiresAt: descriptor.expiresAt } : {},
|
|
246
|
+
...payload !== void 0 ? { payload } : {}
|
|
247
|
+
});
|
|
248
|
+
} catch (error) {
|
|
249
|
+
throw durableGenericFailure(ctx, persisted, `Persisted generic interrupt ${persisted.interruptId} is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
250
|
+
}
|
|
251
|
+
const emitted = createInterruptBinding(request, { batchIndex: binding.batchIndex });
|
|
252
|
+
if (emitted.descriptor.responseSchemaHash !== binding.responseSchemaHash || emitted.descriptor.payloadSchemaHash !== binding.payloadSchemaHash || binding.interruptId !== persisted.interruptId) throw durableGenericFailure(ctx, persisted, `Persisted generic interrupt ${persisted.interruptId} is stale.`);
|
|
253
|
+
records.push({
|
|
254
|
+
interruptId: persisted.interruptId,
|
|
255
|
+
payload: descriptor,
|
|
256
|
+
binding,
|
|
257
|
+
genericRequest: request
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
const firstRecord = records[0];
|
|
261
|
+
if (firstRecord === void 0) return void 0;
|
|
262
|
+
const interruptedRunId = firstRecord.binding.interruptedRunId;
|
|
263
|
+
const generation = firstRecord.binding.generation;
|
|
264
|
+
const validated = await validateInterruptResumeBatch({
|
|
265
|
+
threadId: ctx.threadId,
|
|
266
|
+
interruptedRunId,
|
|
267
|
+
generation,
|
|
268
|
+
pending: records,
|
|
269
|
+
resume: resume.filter((entry) => records.some((record) => record.interruptId === entry.interruptId)),
|
|
270
|
+
tools
|
|
271
|
+
});
|
|
272
|
+
if (validated.errors.length > 0 || !validated.resumeToolState) throw new InterruptResumeValidationError(validated.errors);
|
|
273
|
+
const isGenericRecord = (record) => record.binding.kind === "generic" && record.genericRequest !== void 0;
|
|
274
|
+
const genericRecords = [];
|
|
275
|
+
const batchIndexes = /* @__PURE__ */ new Set();
|
|
276
|
+
for (const record of records) {
|
|
277
|
+
if (!isGenericRecord(record)) continue;
|
|
278
|
+
const batchIndex = record.binding.batchIndex;
|
|
279
|
+
if (batchIndex === void 0 || batchIndexes.has(batchIndex)) throw new InterruptResumeValidationError([{
|
|
280
|
+
scope: "batch",
|
|
281
|
+
threadId: ctx.threadId,
|
|
282
|
+
interruptedRunId,
|
|
283
|
+
generation,
|
|
284
|
+
interruptIds: records.map((item) => item.interruptId),
|
|
285
|
+
code: "stale",
|
|
286
|
+
message: "Persisted generic interrupts have duplicate or invalid batch indexes.",
|
|
287
|
+
source: "server",
|
|
288
|
+
retryable: false
|
|
289
|
+
}]);
|
|
290
|
+
batchIndexes.add(batchIndex);
|
|
291
|
+
genericRecords.push({
|
|
292
|
+
record,
|
|
293
|
+
batchIndex
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
genericRecords.sort((left, right) => left.batchIndex - right.batchIndex);
|
|
297
|
+
return {
|
|
298
|
+
...validated.resumeToolState,
|
|
299
|
+
genericInterruptRequests: new Map(genericRecords.flatMap(({ record }) => record.genericRequest ? [[record.interruptId, record.genericRequest]] : []))
|
|
300
|
+
};
|
|
301
|
+
}
|
|
75
302
|
function resolvedApprovalDecision(entry) {
|
|
76
303
|
if (entry.status === "cancelled") return false;
|
|
77
304
|
const payload = objectValue(entry.payload);
|
|
@@ -109,30 +336,6 @@ function resumeToolStateFromPending(pending, resumeByInterruptId) {
|
|
|
109
336
|
cancelledToolCallIds
|
|
110
337
|
};
|
|
111
338
|
}
|
|
112
|
-
/**
|
|
113
|
-
* Build the transcript to persist when a run finishes successfully.
|
|
114
|
-
*
|
|
115
|
-
* The chat engine appends an assistant message to the middleware message list
|
|
116
|
-
* only when that turn carries tool calls (to feed the agent loop); a run's
|
|
117
|
-
* terminal *text* reply is never appended. So `ctx.messages` at `onFinish` is
|
|
118
|
-
* missing the assistant's final answer. Reattach it from the finish info —
|
|
119
|
-
* `info.content` is the last turn's accumulated text (reset each cycle) — so
|
|
120
|
-
* the stored thread is the complete conversation a server-authoritative client
|
|
121
|
-
* hydrates on load. A guard avoids duplicating a terminal assistant turn should
|
|
122
|
-
* the engine ever start appending it itself.
|
|
123
|
-
*/
|
|
124
|
-
function finishedTranscript(messages, info, messageId, createdAt) {
|
|
125
|
-
const transcript = [...messages];
|
|
126
|
-
const last = transcript[transcript.length - 1];
|
|
127
|
-
const alreadyPresent = last?.role === "assistant" && last.toolCalls === void 0 && last.content === info.content;
|
|
128
|
-
if (info.content && !alreadyPresent) transcript.push({
|
|
129
|
-
role: "assistant",
|
|
130
|
-
content: info.content,
|
|
131
|
-
...messageId ? { id: messageId } : {},
|
|
132
|
-
...createdAt ? { createdAt } : {}
|
|
133
|
-
});
|
|
134
|
-
return transcript;
|
|
135
|
-
}
|
|
136
339
|
function interruptPayload(interrupt) {
|
|
137
340
|
return interrupt && typeof interrupt === "object" ? { ...interrupt } : { value: interrupt };
|
|
138
341
|
}
|
|
@@ -760,16 +963,33 @@ function withPersistence(persistence, options = {}) {
|
|
|
760
963
|
const { wantsInterrupts, runs } = resolvePersistencePlan(persistence);
|
|
761
964
|
const messageStore = persistence.stores.messages;
|
|
762
965
|
if (!messageStore) throw new Error("Chat persistence requires stores.messages.");
|
|
763
|
-
const provides = [
|
|
966
|
+
const provides = [
|
|
967
|
+
PersistenceCapability,
|
|
968
|
+
PersistenceCompletionCapability,
|
|
969
|
+
...wantsInterrupts ? [InterruptsCapability] : []
|
|
970
|
+
];
|
|
764
971
|
return defineChatMiddleware({
|
|
765
972
|
name: "chat-persistence",
|
|
766
973
|
provides,
|
|
767
974
|
setup(ctx) {
|
|
768
975
|
providePersistence(ctx, persistence);
|
|
976
|
+
let resolveCompletion = () => void 0;
|
|
977
|
+
let rejectCompletion = () => void 0;
|
|
978
|
+
const completion = new Promise((resolve, reject) => {
|
|
979
|
+
resolveCompletion = resolve;
|
|
980
|
+
rejectCompletion = reject;
|
|
981
|
+
});
|
|
982
|
+
completion.catch(() => void 0);
|
|
769
983
|
runState.set(ctx, {
|
|
770
984
|
merged: false,
|
|
771
|
-
interrupted: false
|
|
985
|
+
interrupted: false,
|
|
986
|
+
completion: {
|
|
987
|
+
promise: completion,
|
|
988
|
+
resolve: resolveCompletion,
|
|
989
|
+
reject: rejectCompletion
|
|
990
|
+
}
|
|
772
991
|
});
|
|
992
|
+
providePersistenceCompletion(ctx, { waitForRunCompletion: () => completion });
|
|
773
993
|
if (wantsInterrupts && persistence.stores.interrupts) provideInterrupts(ctx, persistence.stores.interrupts);
|
|
774
994
|
providePendingTurn(ctx, { snapshot: async () => {
|
|
775
995
|
const stored = await messageStore.loadThread(ctx.threadId);
|
|
@@ -781,16 +1001,18 @@ function withPersistence(persistence, options = {}) {
|
|
|
781
1001
|
if (ctx.phase !== "init") return;
|
|
782
1002
|
const patch = {};
|
|
783
1003
|
if (wantsInterrupts && persistence.stores.interrupts) {
|
|
784
|
-
const
|
|
785
|
-
|
|
1004
|
+
const ownedPending = (await persistence.stores.interrupts.listPending(ctx.threadId)).filter(isChatOwnedPendingInterrupt);
|
|
1005
|
+
rejectMixedRunPending(ownedPending, ctx);
|
|
1006
|
+
const resumeByInterruptId = validatePendingResumes(ownedPending, config.resume, ctx);
|
|
786
1007
|
if ((config.resume?.length ?? 0) > 0) {
|
|
787
|
-
const resumeToolState = resumeToolStateFromPending(
|
|
1008
|
+
const resumeToolState = resumeToolStateFromPending(ownedPending, resumeByInterruptId);
|
|
1009
|
+
const genericResumeState = await durableGenericResumeState(ctx, ownedPending, config.resume ?? [], config.tools);
|
|
788
1010
|
patch.resume = [];
|
|
789
|
-
if (resumeToolState) patch.resumeToolState = resumeToolState;
|
|
1011
|
+
if (resumeToolState || genericResumeState) patch.resumeToolState = mergeResumeToolState(resumeToolState, genericResumeState);
|
|
790
1012
|
}
|
|
791
1013
|
const state = runState.get(ctx);
|
|
792
|
-
if (state &&
|
|
793
|
-
pending,
|
|
1014
|
+
if (state && ownedPending.length > 0) state.pendingResumes = {
|
|
1015
|
+
pending: ownedPending,
|
|
794
1016
|
resumeByInterruptId
|
|
795
1017
|
};
|
|
796
1018
|
}
|
|
@@ -810,7 +1032,7 @@ function withPersistence(persistence, options = {}) {
|
|
|
810
1032
|
} catch {}
|
|
811
1033
|
},
|
|
812
1034
|
async onChunk(ctx, chunk) {
|
|
813
|
-
if (ctx.phase === "modelStream") {
|
|
1035
|
+
if (snapshotStreaming && ctx.phase === "modelStream") {
|
|
814
1036
|
const s = runState.get(ctx);
|
|
815
1037
|
if (s && chunk.type === "TEXT_MESSAGE_START") {
|
|
816
1038
|
s.streamingMessageId = typeof chunk.messageId === "string" && chunk.messageId !== "" ? chunk.messageId : void 0;
|
|
@@ -866,19 +1088,35 @@ function withPersistence(persistence, options = {}) {
|
|
|
866
1088
|
async onFinish(ctx, info) {
|
|
867
1089
|
const state = runState.get(ctx);
|
|
868
1090
|
if (state?.interrupted) return;
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1091
|
+
try {
|
|
1092
|
+
await messageStore.saveThread(ctx.threadId, [...ctx.messages]);
|
|
1093
|
+
await commitPendingResumes(state, persistence.stores.interrupts);
|
|
1094
|
+
await completeRun(runs, ctx.runId, state?.usage ?? info.usage);
|
|
1095
|
+
state?.completion?.resolve();
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
try {
|
|
1098
|
+
await failRun(runs, ctx.runId, error, state?.usage);
|
|
1099
|
+
} finally {
|
|
1100
|
+
state?.completion?.reject(error);
|
|
1101
|
+
}
|
|
1102
|
+
throw error;
|
|
1103
|
+
}
|
|
872
1104
|
},
|
|
873
1105
|
async onError(ctx, info) {
|
|
874
|
-
|
|
1106
|
+
try {
|
|
1107
|
+
await failRun(runs, ctx.runId, info.error, runState.get(ctx)?.usage);
|
|
1108
|
+
} finally {
|
|
1109
|
+
runState.get(ctx)?.completion?.reject(info.error);
|
|
1110
|
+
}
|
|
875
1111
|
},
|
|
876
1112
|
async onAbort(ctx, info) {
|
|
877
|
-
const cancelled = info.cancelRequested === true || runs !== void 0 && await wasCancelRequested(runs, ctx.runId);
|
|
878
1113
|
const state = runState.get(ctx);
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1114
|
+
let terminal = false;
|
|
1115
|
+
try {
|
|
1116
|
+
terminal = info.cancelRequested === true || runs !== void 0 && await wasCancelRequested(runs, ctx.runId) || !detachableRun(ctx) && state?.interrupted !== true;
|
|
1117
|
+
if (terminal) await abortRun(runs, ctx.runId, state?.usage);
|
|
1118
|
+
} finally {
|
|
1119
|
+
if (terminal) state?.completion?.reject(info.reason);
|
|
882
1120
|
}
|
|
883
1121
|
}
|
|
884
1122
|
});
|