@effect-agent/storage-memory 0.1.0-beta.9 → 0.1.0-beta.90
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/MemoryMessageDeliveryStore.d.mts +15 -0
- package/dist/MemoryMessageDeliveryStore.mjs +147 -0
- package/dist/MemoryMessageDeliveryStore.mjs.map +1 -0
- package/dist/MemoryScheduleStore.d.mts +10 -0
- package/dist/MemoryScheduleStore.mjs +169 -0
- package/dist/MemoryScheduleStore.mjs.map +1 -0
- package/dist/MemorySemanticIndex.d.mts +21 -0
- package/dist/MemorySemanticIndex.mjs +222 -0
- package/dist/MemorySemanticIndex.mjs.map +1 -0
- package/dist/MemorySubmissionLedger.d.mts +24 -0
- package/dist/MemorySubmissionLedger.mjs +1058 -0
- package/dist/MemorySubmissionLedger.mjs.map +1 -0
- package/dist/MemorySubscriptionStore.d.mts +9 -0
- package/dist/MemorySubscriptionStore.mjs +849 -0
- package/dist/MemorySubscriptionStore.mjs.map +1 -0
- package/dist/MemoryThreadStore.d.mts +17 -0
- package/dist/MemoryThreadStore.mjs +377 -0
- package/dist/MemoryThreadStore.mjs.map +1 -0
- package/dist/index.d.mts +7 -30
- package/dist/index.mjs +7 -1298
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -45
- package/src/MemoryMessageDeliveryStore.ts +293 -0
- package/src/MemoryScheduleStore.ts +328 -0
- package/src/MemorySemanticIndex.ts +357 -0
- package/src/{memory-ledger.ts → MemorySubmissionLedger.ts} +478 -86
- package/src/MemorySubscriptionStore.ts +1549 -0
- package/src/MemoryThreadStore.ts +766 -0
- package/src/index.ts +6 -2
- package/dist/index.mjs.map +0 -1
- package/dist/testing.d.mts +0 -2
- package/dist/testing.mjs +0 -2
- package/src/memory-storage.ts +0 -614
- package/src/testing.ts +0 -10
package/src/memory-storage.ts
DELETED
|
@@ -1,614 +0,0 @@
|
|
|
1
|
-
import { ConversationId } from "@effect-agent/core";
|
|
2
|
-
import {
|
|
3
|
-
AppendConflict,
|
|
4
|
-
AppendResult,
|
|
5
|
-
CanonicalRecordEnvelope,
|
|
6
|
-
CanonicalSequence,
|
|
7
|
-
CheckpointRejected,
|
|
8
|
-
ConversationExportRequest,
|
|
9
|
-
ConversationCheckpoint,
|
|
10
|
-
ConversationExport,
|
|
11
|
-
ConversationMaterialization,
|
|
12
|
-
ConversationNotMaterialized,
|
|
13
|
-
ConversationObservation,
|
|
14
|
-
ConversationRead,
|
|
15
|
-
ConversationStore,
|
|
16
|
-
ConversationStoreError,
|
|
17
|
-
ConversationTail,
|
|
18
|
-
ConversationTailRequest,
|
|
19
|
-
digestCanonicalBatch,
|
|
20
|
-
EMPTY_TAIL_DIGEST,
|
|
21
|
-
FenceRejected,
|
|
22
|
-
FencedAppendRequest,
|
|
23
|
-
LoadCheckpointRequest,
|
|
24
|
-
ObservationOffset,
|
|
25
|
-
ProducerEpoch,
|
|
26
|
-
RecordId,
|
|
27
|
-
SaveCheckpointRequest,
|
|
28
|
-
type BatchId,
|
|
29
|
-
type Digest,
|
|
30
|
-
} from "@effect-agent/session";
|
|
31
|
-
import { Crypto, Effect, Encoding, Layer, Option, PubSub, Ref, Schema, Stream } from "effect";
|
|
32
|
-
|
|
33
|
-
const MAX_CONVERSATIONS = 256;
|
|
34
|
-
const MAX_RECORDS_PER_CONVERSATION = 65_536;
|
|
35
|
-
const MAX_CHECKPOINTS_PER_CONVERSATION = 1_024;
|
|
36
|
-
|
|
37
|
-
interface StoredBatch {
|
|
38
|
-
readonly digest: Digest;
|
|
39
|
-
readonly result: AppendResult;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface StoredConversation {
|
|
43
|
-
readonly producerEpoch: ProducerEpoch;
|
|
44
|
-
readonly tailSequence: CanonicalSequence;
|
|
45
|
-
readonly tailDigest: Digest;
|
|
46
|
-
readonly records: ReadonlyArray<CanonicalRecordEnvelope>;
|
|
47
|
-
readonly recordIds: ReadonlySet<RecordId>;
|
|
48
|
-
readonly batches: ReadonlyMap<BatchId, StoredBatch>;
|
|
49
|
-
readonly tailDigests: ReadonlyMap<CanonicalSequence, Digest>;
|
|
50
|
-
readonly checkpoints: ReadonlyMap<CanonicalSequence, ConversationCheckpoint>;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface MemoryState {
|
|
54
|
-
readonly conversations: ReadonlyMap<ConversationId, StoredConversation>;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
type AppendDecision =
|
|
58
|
-
| {
|
|
59
|
-
readonly _tag: "failure";
|
|
60
|
-
readonly error:
|
|
61
|
-
| ConversationStoreError
|
|
62
|
-
| ConversationNotMaterialized
|
|
63
|
-
| AppendConflict
|
|
64
|
-
| FenceRejected;
|
|
65
|
-
}
|
|
66
|
-
| {
|
|
67
|
-
readonly _tag: "success";
|
|
68
|
-
readonly result: AppendResult;
|
|
69
|
-
readonly records: ReadonlyArray<CanonicalRecordEnvelope>;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
type MaterializeDecision =
|
|
73
|
-
| { readonly _tag: "failure"; readonly error: ConversationStoreError | FenceRejected }
|
|
74
|
-
| { readonly _tag: "success" };
|
|
75
|
-
|
|
76
|
-
type CheckpointDecision =
|
|
77
|
-
| {
|
|
78
|
-
readonly _tag: "failure";
|
|
79
|
-
readonly error: ConversationNotMaterialized | ConversationStoreError | CheckpointRejected;
|
|
80
|
-
}
|
|
81
|
-
| { readonly _tag: "success" };
|
|
82
|
-
|
|
83
|
-
const storeError = (operation: string, message: string, cause?: unknown): ConversationStoreError =>
|
|
84
|
-
cause === undefined
|
|
85
|
-
? ConversationStoreError.make({ operation, message })
|
|
86
|
-
: ConversationStoreError.make({ operation, message, cause });
|
|
87
|
-
|
|
88
|
-
const validate = Effect.fn("MemoryConversationStore.validate")(
|
|
89
|
-
<A, I>(
|
|
90
|
-
schema: Schema.Codec<A, I>,
|
|
91
|
-
operation: string,
|
|
92
|
-
value: unknown,
|
|
93
|
-
): Effect.Effect<A, ConversationStoreError> =>
|
|
94
|
-
Schema.encodeUnknownEffect(schema)(value).pipe(
|
|
95
|
-
Effect.flatMap(Schema.decodeUnknownEffect(schema)),
|
|
96
|
-
Effect.mapError((error) => storeError(operation, `Invalid ${operation} request`, error)),
|
|
97
|
-
),
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
const decodeCanonicalSequence = Schema.decodeSync(CanonicalSequence);
|
|
101
|
-
const ZERO_CANONICAL_SEQUENCE = decodeCanonicalSequence(0);
|
|
102
|
-
|
|
103
|
-
const offsetSequence = Effect.fn("MemoryConversationStore.offsetSequence")((
|
|
104
|
-
conversationId: ConversationId,
|
|
105
|
-
offset: ObservationOffset | undefined,
|
|
106
|
-
): Effect.Effect<CanonicalSequence, ConversationStoreError> => {
|
|
107
|
-
if (offset === undefined) return Effect.succeed(ZERO_CANONICAL_SEQUENCE);
|
|
108
|
-
const prefix = `memory:v1:${Encoding.encodeBase64(conversationId)}:`;
|
|
109
|
-
const encodedSequence = offset.startsWith(prefix) ? offset.slice(prefix.length) : "";
|
|
110
|
-
if (!/^\d+$/.test(encodedSequence)) {
|
|
111
|
-
return Effect.fail(storeError("observe", "Malformed observation offset"));
|
|
112
|
-
}
|
|
113
|
-
const sequence = Number(encodedSequence);
|
|
114
|
-
return Number.isSafeInteger(sequence)
|
|
115
|
-
? Schema.decodeUnknownEffect(CanonicalSequence)(sequence).pipe(
|
|
116
|
-
Effect.mapError(() => storeError("observe", "Malformed observation offset")),
|
|
117
|
-
)
|
|
118
|
-
: Effect.fail(storeError("observe", "Malformed observation offset"));
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const observationOffset = (
|
|
122
|
-
conversationId: ConversationId,
|
|
123
|
-
sequence: CanonicalSequence,
|
|
124
|
-
): ObservationOffset =>
|
|
125
|
-
Schema.decodeSync(ObservationOffset)(
|
|
126
|
-
`memory:v1:${Encoding.encodeBase64(conversationId)}:${sequence}`,
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
const findConversation = Effect.fn("MemoryConversationStore.findConversation")((
|
|
130
|
-
state: MemoryState,
|
|
131
|
-
conversationId: ConversationId,
|
|
132
|
-
): Effect.Effect<StoredConversation, ConversationNotMaterialized> => {
|
|
133
|
-
const conversation = state.conversations.get(conversationId);
|
|
134
|
-
return conversation === undefined
|
|
135
|
-
? Effect.fail(ConversationNotMaterialized.make({ conversationId }))
|
|
136
|
-
: Effect.succeed(conversation);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const CheckpointVersionEnvelope = Schema.Struct({
|
|
140
|
-
checkpoint: Schema.Struct({
|
|
141
|
-
conversationId: ConversationId,
|
|
142
|
-
schemaVersion: Schema.Natural,
|
|
143
|
-
}),
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const validateCheckpointVersion = Effect.fn("MemoryConversationStore.validateCheckpointVersion")(
|
|
147
|
-
function* (value: unknown): Effect.fn.Return<void, ConversationStoreError | CheckpointRejected> {
|
|
148
|
-
const envelope = yield* Schema.decodeUnknownEffect(CheckpointVersionEnvelope)(value).pipe(
|
|
149
|
-
Effect.mapError(() => storeError("saveCheckpoint", "Invalid saveCheckpoint request")),
|
|
150
|
-
);
|
|
151
|
-
if (envelope.checkpoint.schemaVersion !== 1) {
|
|
152
|
-
return yield* CheckpointRejected.make({
|
|
153
|
-
conversationId: envelope.checkpoint.conversationId,
|
|
154
|
-
reason: "unsupported-version",
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
const makeConversationStore = Effect.gen(function* () {
|
|
161
|
-
const crypto = yield* Crypto.Crypto;
|
|
162
|
-
const state = yield* Ref.make<MemoryState>({ conversations: new Map() });
|
|
163
|
-
const updates = yield* PubSub.sliding<void>(1);
|
|
164
|
-
yield* Effect.addFinalizer(() => PubSub.shutdown(updates));
|
|
165
|
-
|
|
166
|
-
const materialize: ConversationStore["Service"]["materialize"] = Effect.fn(
|
|
167
|
-
"MemoryConversationStore.materialize",
|
|
168
|
-
)((unvalidated) =>
|
|
169
|
-
Effect.gen(function* () {
|
|
170
|
-
const request = yield* validate(ConversationMaterialization, "materialize", unvalidated);
|
|
171
|
-
const decision = yield* Ref.modify(
|
|
172
|
-
state,
|
|
173
|
-
(current): readonly [MaterializeDecision, MemoryState] => {
|
|
174
|
-
const existing = current.conversations.get(request.conversationId);
|
|
175
|
-
if (existing !== undefined) {
|
|
176
|
-
if (request.producerEpoch < existing.producerEpoch) {
|
|
177
|
-
return [
|
|
178
|
-
{
|
|
179
|
-
_tag: "failure",
|
|
180
|
-
error: FenceRejected.make({
|
|
181
|
-
conversationId: request.conversationId,
|
|
182
|
-
actualEpoch: existing.producerEpoch,
|
|
183
|
-
attemptedEpoch: request.producerEpoch,
|
|
184
|
-
}),
|
|
185
|
-
},
|
|
186
|
-
current,
|
|
187
|
-
];
|
|
188
|
-
}
|
|
189
|
-
if (request.producerEpoch === existing.producerEpoch) {
|
|
190
|
-
return [{ _tag: "success" }, current];
|
|
191
|
-
}
|
|
192
|
-
const conversations = new Map(current.conversations);
|
|
193
|
-
conversations.set(request.conversationId, {
|
|
194
|
-
...existing,
|
|
195
|
-
producerEpoch: request.producerEpoch,
|
|
196
|
-
});
|
|
197
|
-
return [{ _tag: "success" }, { conversations }];
|
|
198
|
-
}
|
|
199
|
-
if (current.conversations.size >= MAX_CONVERSATIONS) {
|
|
200
|
-
return [
|
|
201
|
-
{
|
|
202
|
-
_tag: "failure",
|
|
203
|
-
error: storeError(
|
|
204
|
-
"materialize",
|
|
205
|
-
`In-memory conversation limit ${MAX_CONVERSATIONS} exceeded`,
|
|
206
|
-
),
|
|
207
|
-
},
|
|
208
|
-
current,
|
|
209
|
-
];
|
|
210
|
-
}
|
|
211
|
-
const conversations = new Map(current.conversations);
|
|
212
|
-
conversations.set(request.conversationId, {
|
|
213
|
-
producerEpoch: request.producerEpoch,
|
|
214
|
-
tailSequence: ZERO_CANONICAL_SEQUENCE,
|
|
215
|
-
tailDigest: EMPTY_TAIL_DIGEST,
|
|
216
|
-
records: [],
|
|
217
|
-
recordIds: new Set(),
|
|
218
|
-
batches: new Map(),
|
|
219
|
-
tailDigests: new Map([[ZERO_CANONICAL_SEQUENCE, EMPTY_TAIL_DIGEST]]),
|
|
220
|
-
checkpoints: new Map(),
|
|
221
|
-
});
|
|
222
|
-
return [{ _tag: "success" }, { conversations }];
|
|
223
|
-
},
|
|
224
|
-
);
|
|
225
|
-
if (decision._tag === "failure") return yield* decision.error;
|
|
226
|
-
}),
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
const append: ConversationStore["Service"]["append"] = Effect.fn(
|
|
230
|
-
"MemoryConversationStore.append",
|
|
231
|
-
)((unvalidated) =>
|
|
232
|
-
Effect.gen(function* () {
|
|
233
|
-
const request = yield* validate(FencedAppendRequest, "append", unvalidated);
|
|
234
|
-
const digest = yield* digestCanonicalBatch(request.expectedTailDigest, request.batch).pipe(
|
|
235
|
-
Effect.provideService(Crypto.Crypto, crypto),
|
|
236
|
-
Effect.mapError((error) => storeError("append", error.message, error)),
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
const decision = yield* Effect.uninterruptible(
|
|
240
|
-
Ref.modify(state, (current): readonly [AppendDecision, MemoryState] => {
|
|
241
|
-
const conversation = current.conversations.get(request.conversationId);
|
|
242
|
-
if (conversation === undefined) {
|
|
243
|
-
return [
|
|
244
|
-
{
|
|
245
|
-
_tag: "failure",
|
|
246
|
-
error: ConversationNotMaterialized.make({
|
|
247
|
-
conversationId: request.conversationId,
|
|
248
|
-
}),
|
|
249
|
-
},
|
|
250
|
-
current,
|
|
251
|
-
];
|
|
252
|
-
}
|
|
253
|
-
if (request.producerEpoch !== conversation.producerEpoch) {
|
|
254
|
-
return [
|
|
255
|
-
{
|
|
256
|
-
_tag: "failure",
|
|
257
|
-
error: FenceRejected.make({
|
|
258
|
-
conversationId: request.conversationId,
|
|
259
|
-
actualEpoch: conversation.producerEpoch,
|
|
260
|
-
attemptedEpoch: request.producerEpoch,
|
|
261
|
-
}),
|
|
262
|
-
},
|
|
263
|
-
current,
|
|
264
|
-
];
|
|
265
|
-
}
|
|
266
|
-
const previous = conversation.batches.get(request.batch.batchId);
|
|
267
|
-
if (previous !== undefined) {
|
|
268
|
-
if (previous.digest !== digest) {
|
|
269
|
-
return [
|
|
270
|
-
{
|
|
271
|
-
_tag: "failure",
|
|
272
|
-
error: AppendConflict.make({
|
|
273
|
-
conversationId: request.conversationId,
|
|
274
|
-
batchId: request.batch.batchId,
|
|
275
|
-
reason: "batch-digest",
|
|
276
|
-
}),
|
|
277
|
-
},
|
|
278
|
-
current,
|
|
279
|
-
];
|
|
280
|
-
}
|
|
281
|
-
return [
|
|
282
|
-
{
|
|
283
|
-
_tag: "success",
|
|
284
|
-
result: AppendResult.make({
|
|
285
|
-
firstSequence: previous.result.firstSequence,
|
|
286
|
-
lastSequence: previous.result.lastSequence,
|
|
287
|
-
tailDigest: previous.result.tailDigest,
|
|
288
|
-
replayed: true,
|
|
289
|
-
}),
|
|
290
|
-
records: [],
|
|
291
|
-
},
|
|
292
|
-
current,
|
|
293
|
-
];
|
|
294
|
-
}
|
|
295
|
-
if (
|
|
296
|
-
request.expectedTailSequence !== conversation.tailSequence ||
|
|
297
|
-
request.expectedTailDigest !== conversation.tailDigest
|
|
298
|
-
) {
|
|
299
|
-
return [
|
|
300
|
-
{
|
|
301
|
-
_tag: "failure",
|
|
302
|
-
error: AppendConflict.make({
|
|
303
|
-
conversationId: request.conversationId,
|
|
304
|
-
batchId: request.batch.batchId,
|
|
305
|
-
reason: "tail",
|
|
306
|
-
actualTailSequence: conversation.tailSequence,
|
|
307
|
-
actualTailDigest: conversation.tailDigest,
|
|
308
|
-
}),
|
|
309
|
-
},
|
|
310
|
-
current,
|
|
311
|
-
];
|
|
312
|
-
}
|
|
313
|
-
if (
|
|
314
|
-
conversation.records.length + request.batch.records.length >
|
|
315
|
-
MAX_RECORDS_PER_CONVERSATION
|
|
316
|
-
) {
|
|
317
|
-
return [
|
|
318
|
-
{
|
|
319
|
-
_tag: "failure",
|
|
320
|
-
error: storeError(
|
|
321
|
-
"append",
|
|
322
|
-
`In-memory record limit ${MAX_RECORDS_PER_CONVERSATION} exceeded`,
|
|
323
|
-
),
|
|
324
|
-
},
|
|
325
|
-
current,
|
|
326
|
-
];
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const batchRecordIds = new Set<RecordId>();
|
|
330
|
-
for (const record of request.batch.records) {
|
|
331
|
-
if (
|
|
332
|
-
conversation.recordIds.has(record.recordId) ||
|
|
333
|
-
batchRecordIds.has(record.recordId)
|
|
334
|
-
) {
|
|
335
|
-
return [
|
|
336
|
-
{
|
|
337
|
-
_tag: "failure",
|
|
338
|
-
error: AppendConflict.make({
|
|
339
|
-
conversationId: request.conversationId,
|
|
340
|
-
batchId: request.batch.batchId,
|
|
341
|
-
reason: "record-identity",
|
|
342
|
-
}),
|
|
343
|
-
},
|
|
344
|
-
current,
|
|
345
|
-
];
|
|
346
|
-
}
|
|
347
|
-
batchRecordIds.add(record.recordId);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const records = request.batch.records.map((record, index) => {
|
|
351
|
-
const sequence = decodeCanonicalSequence(conversation.tailSequence + index + 1);
|
|
352
|
-
return CanonicalRecordEnvelope.make({
|
|
353
|
-
conversationId: request.conversationId,
|
|
354
|
-
batchId: request.batch.batchId,
|
|
355
|
-
sequence,
|
|
356
|
-
offset: observationOffset(request.conversationId, sequence),
|
|
357
|
-
record,
|
|
358
|
-
});
|
|
359
|
-
});
|
|
360
|
-
const lastSequence = decodeCanonicalSequence(conversation.tailSequence + records.length);
|
|
361
|
-
const result = AppendResult.make({
|
|
362
|
-
firstSequence: decodeCanonicalSequence(conversation.tailSequence + 1),
|
|
363
|
-
lastSequence,
|
|
364
|
-
tailDigest: digest,
|
|
365
|
-
replayed: false,
|
|
366
|
-
});
|
|
367
|
-
const batches = new Map(conversation.batches);
|
|
368
|
-
batches.set(request.batch.batchId, { digest, result });
|
|
369
|
-
const recordIds = new Set(conversation.recordIds);
|
|
370
|
-
for (const recordId of batchRecordIds) recordIds.add(recordId);
|
|
371
|
-
const tailDigests = new Map(conversation.tailDigests);
|
|
372
|
-
tailDigests.set(lastSequence, digest);
|
|
373
|
-
const conversations = new Map(current.conversations);
|
|
374
|
-
conversations.set(request.conversationId, {
|
|
375
|
-
...conversation,
|
|
376
|
-
tailSequence: lastSequence,
|
|
377
|
-
tailDigest: digest,
|
|
378
|
-
records: [...conversation.records, ...records],
|
|
379
|
-
recordIds,
|
|
380
|
-
batches,
|
|
381
|
-
tailDigests,
|
|
382
|
-
});
|
|
383
|
-
return [{ _tag: "success", result, records }, { conversations }];
|
|
384
|
-
}).pipe(
|
|
385
|
-
Effect.tap((decision) =>
|
|
386
|
-
decision._tag === "success" && decision.records.length > 0
|
|
387
|
-
? PubSub.publish(updates, undefined)
|
|
388
|
-
: Effect.void,
|
|
389
|
-
),
|
|
390
|
-
),
|
|
391
|
-
);
|
|
392
|
-
if (decision._tag === "failure") return yield* decision.error;
|
|
393
|
-
return decision.result;
|
|
394
|
-
}),
|
|
395
|
-
);
|
|
396
|
-
|
|
397
|
-
const readSnapshot = Effect.fn("MemoryConversationStore.readSnapshot")(
|
|
398
|
-
(conversationId: ConversationId, afterSequence: CanonicalSequence | undefined, limit: number) =>
|
|
399
|
-
Ref.get(state).pipe(
|
|
400
|
-
Effect.flatMap((current) => findConversation(current, conversationId)),
|
|
401
|
-
Effect.map((conversation) =>
|
|
402
|
-
conversation.records
|
|
403
|
-
.filter((record) => record.sequence > (afterSequence ?? ZERO_CANONICAL_SEQUENCE))
|
|
404
|
-
.slice(0, limit),
|
|
405
|
-
),
|
|
406
|
-
),
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
const read: ConversationStore["Service"]["read"] = (unvalidated) =>
|
|
410
|
-
Stream.unwrap(
|
|
411
|
-
Effect.gen(function* () {
|
|
412
|
-
const request = yield* validate(ConversationRead, "read", unvalidated);
|
|
413
|
-
const records = yield* readSnapshot(
|
|
414
|
-
request.conversationId,
|
|
415
|
-
request.afterSequence,
|
|
416
|
-
request.limit,
|
|
417
|
-
);
|
|
418
|
-
return Stream.fromIterable(records);
|
|
419
|
-
}),
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
const observe: ConversationStore["Service"]["observe"] = (unvalidated) =>
|
|
423
|
-
Stream.unwrap(
|
|
424
|
-
Effect.gen(function* () {
|
|
425
|
-
const request = yield* validate(ConversationObservation, "observe", unvalidated);
|
|
426
|
-
const afterSequence = yield* offsetSequence(request.conversationId, request.afterOffset);
|
|
427
|
-
return Stream.unwrap(
|
|
428
|
-
Effect.gen(function* () {
|
|
429
|
-
const subscription = yield* PubSub.subscribe(updates);
|
|
430
|
-
const initial = yield* readSnapshot(
|
|
431
|
-
request.conversationId,
|
|
432
|
-
afterSequence,
|
|
433
|
-
MAX_RECORDS_PER_CONVERSATION,
|
|
434
|
-
);
|
|
435
|
-
const highWater =
|
|
436
|
-
initial.length === 0 ? afterSequence : (initial.at(-1)?.sequence ?? afterSequence);
|
|
437
|
-
const live = Stream.fromEffectRepeat(PubSub.take(subscription)).pipe(
|
|
438
|
-
Stream.mapAccumEffect(
|
|
439
|
-
() => highWater,
|
|
440
|
-
(lastSequence) =>
|
|
441
|
-
readSnapshot(
|
|
442
|
-
request.conversationId,
|
|
443
|
-
lastSequence,
|
|
444
|
-
MAX_RECORDS_PER_CONVERSATION,
|
|
445
|
-
).pipe(
|
|
446
|
-
Effect.map(
|
|
447
|
-
(records) => [records.at(-1)?.sequence ?? lastSequence, records] as const,
|
|
448
|
-
),
|
|
449
|
-
),
|
|
450
|
-
),
|
|
451
|
-
);
|
|
452
|
-
return Stream.fromIterable(initial).pipe(Stream.concat(live));
|
|
453
|
-
}),
|
|
454
|
-
);
|
|
455
|
-
}),
|
|
456
|
-
);
|
|
457
|
-
|
|
458
|
-
const exportConversation: ConversationStore["Service"]["export"] = Effect.fn(
|
|
459
|
-
"MemoryConversationStore.export",
|
|
460
|
-
)((unvalidated) =>
|
|
461
|
-
Effect.gen(function* () {
|
|
462
|
-
const request = yield* validate(ConversationExportRequest, "export", unvalidated);
|
|
463
|
-
const conversation = yield* Ref.get(state).pipe(
|
|
464
|
-
Effect.flatMap((current) => findConversation(current, request.conversationId)),
|
|
465
|
-
);
|
|
466
|
-
return ConversationExport.make({
|
|
467
|
-
format: "effect-agent/conversation@1",
|
|
468
|
-
conversationId: request.conversationId,
|
|
469
|
-
tailSequence: conversation.tailSequence,
|
|
470
|
-
tailDigest: conversation.tailDigest,
|
|
471
|
-
records: conversation.records,
|
|
472
|
-
});
|
|
473
|
-
}),
|
|
474
|
-
);
|
|
475
|
-
|
|
476
|
-
const inspectTail: ConversationStore["Service"]["inspectTail"] = Effect.fn(
|
|
477
|
-
"MemoryConversationStore.inspectTail",
|
|
478
|
-
)((unvalidated) =>
|
|
479
|
-
Effect.gen(function* () {
|
|
480
|
-
const request = yield* validate(ConversationTailRequest, "inspectTail", unvalidated);
|
|
481
|
-
const conversation = yield* Ref.get(state).pipe(
|
|
482
|
-
Effect.flatMap((current) => findConversation(current, request.conversationId)),
|
|
483
|
-
);
|
|
484
|
-
return ConversationTail.make({
|
|
485
|
-
conversationId: request.conversationId,
|
|
486
|
-
tailSequence: conversation.tailSequence,
|
|
487
|
-
tailDigest: conversation.tailDigest,
|
|
488
|
-
producerEpoch: conversation.producerEpoch,
|
|
489
|
-
});
|
|
490
|
-
}),
|
|
491
|
-
);
|
|
492
|
-
|
|
493
|
-
const saveCheckpoint: ConversationStore["Service"]["saveCheckpoint"] = Effect.fn(
|
|
494
|
-
"MemoryConversationStore.saveCheckpoint",
|
|
495
|
-
)((unvalidated) =>
|
|
496
|
-
Effect.gen(function* () {
|
|
497
|
-
yield* validateCheckpointVersion(unvalidated);
|
|
498
|
-
const request = yield* validate(SaveCheckpointRequest, "saveCheckpoint", unvalidated);
|
|
499
|
-
const decision = yield* Ref.modify(
|
|
500
|
-
state,
|
|
501
|
-
(current): readonly [CheckpointDecision, MemoryState] => {
|
|
502
|
-
const checkpoint = request.checkpoint;
|
|
503
|
-
const conversation = current.conversations.get(checkpoint.conversationId);
|
|
504
|
-
if (conversation === undefined) {
|
|
505
|
-
return [
|
|
506
|
-
{
|
|
507
|
-
_tag: "failure",
|
|
508
|
-
error: ConversationNotMaterialized.make({
|
|
509
|
-
conversationId: checkpoint.conversationId,
|
|
510
|
-
}),
|
|
511
|
-
},
|
|
512
|
-
current,
|
|
513
|
-
];
|
|
514
|
-
}
|
|
515
|
-
if (checkpoint.throughSequence > conversation.tailSequence) {
|
|
516
|
-
return [
|
|
517
|
-
{
|
|
518
|
-
_tag: "failure",
|
|
519
|
-
error: CheckpointRejected.make({
|
|
520
|
-
conversationId: checkpoint.conversationId,
|
|
521
|
-
reason: "ahead-of-tail",
|
|
522
|
-
}),
|
|
523
|
-
},
|
|
524
|
-
current,
|
|
525
|
-
];
|
|
526
|
-
}
|
|
527
|
-
if (conversation.tailDigests.get(checkpoint.throughSequence) !== checkpoint.tailDigest) {
|
|
528
|
-
return [
|
|
529
|
-
{
|
|
530
|
-
_tag: "failure",
|
|
531
|
-
error: CheckpointRejected.make({
|
|
532
|
-
conversationId: checkpoint.conversationId,
|
|
533
|
-
reason: "digest-mismatch",
|
|
534
|
-
}),
|
|
535
|
-
},
|
|
536
|
-
current,
|
|
537
|
-
];
|
|
538
|
-
}
|
|
539
|
-
if (
|
|
540
|
-
!conversation.checkpoints.has(checkpoint.throughSequence) &&
|
|
541
|
-
conversation.checkpoints.size >= MAX_CHECKPOINTS_PER_CONVERSATION
|
|
542
|
-
) {
|
|
543
|
-
return [
|
|
544
|
-
{
|
|
545
|
-
_tag: "failure",
|
|
546
|
-
error: storeError(
|
|
547
|
-
"saveCheckpoint",
|
|
548
|
-
`In-memory checkpoint limit ${MAX_CHECKPOINTS_PER_CONVERSATION} exceeded`,
|
|
549
|
-
),
|
|
550
|
-
},
|
|
551
|
-
current,
|
|
552
|
-
];
|
|
553
|
-
}
|
|
554
|
-
const checkpoints = new Map(conversation.checkpoints);
|
|
555
|
-
checkpoints.set(checkpoint.throughSequence, checkpoint);
|
|
556
|
-
const conversations = new Map(current.conversations);
|
|
557
|
-
conversations.set(checkpoint.conversationId, { ...conversation, checkpoints });
|
|
558
|
-
return [{ _tag: "success" }, { conversations }];
|
|
559
|
-
},
|
|
560
|
-
);
|
|
561
|
-
if (decision._tag === "failure") return yield* decision.error;
|
|
562
|
-
}),
|
|
563
|
-
);
|
|
564
|
-
|
|
565
|
-
const loadCheckpoint: ConversationStore["Service"]["loadCheckpoint"] = Effect.fn(
|
|
566
|
-
"MemoryConversationStore.loadCheckpoint",
|
|
567
|
-
)((unvalidated) =>
|
|
568
|
-
Effect.gen(function* () {
|
|
569
|
-
const request = yield* validate(LoadCheckpointRequest, "loadCheckpoint", unvalidated);
|
|
570
|
-
const conversation = yield* Ref.get(state).pipe(
|
|
571
|
-
Effect.flatMap((current) => findConversation(current, request.conversationId)),
|
|
572
|
-
);
|
|
573
|
-
const maximum = request.atOrBeforeSequence ?? conversation.tailSequence;
|
|
574
|
-
let selected: ConversationCheckpoint | undefined;
|
|
575
|
-
for (const [sequence, checkpoint] of conversation.checkpoints) {
|
|
576
|
-
if (
|
|
577
|
-
sequence <= maximum &&
|
|
578
|
-
(selected === undefined || sequence > selected.throughSequence)
|
|
579
|
-
) {
|
|
580
|
-
selected = checkpoint;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
if (
|
|
584
|
-
selected !== undefined &&
|
|
585
|
-
conversation.tailDigests.get(selected.throughSequence) !== selected.tailDigest
|
|
586
|
-
) {
|
|
587
|
-
return yield* CheckpointRejected.make({
|
|
588
|
-
conversationId: request.conversationId,
|
|
589
|
-
reason: "digest-mismatch",
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
return Option.fromNullishOr(selected);
|
|
593
|
-
}),
|
|
594
|
-
);
|
|
595
|
-
|
|
596
|
-
return ConversationStore.of({
|
|
597
|
-
materialize,
|
|
598
|
-
append,
|
|
599
|
-
read,
|
|
600
|
-
observe,
|
|
601
|
-
export: exportConversation,
|
|
602
|
-
inspectTail,
|
|
603
|
-
saveCheckpoint,
|
|
604
|
-
loadCheckpoint,
|
|
605
|
-
});
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
export const MemoryConversationStoreLive = Layer.effect(ConversationStore, makeConversationStore);
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* In-memory canonical Conversation persistence. Durable accepted work is served by the separate
|
|
612
|
-
* SubmissionLedger port; this Layer deliberately provides only the ConversationStore.
|
|
613
|
-
*/
|
|
614
|
-
export const MemoryStorageLive = MemoryConversationStoreLive;
|
package/src/testing.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
conversationStoreConformanceCases,
|
|
3
|
-
ConversationStoreConformanceViolation,
|
|
4
|
-
submissionLedgerConformanceCases,
|
|
5
|
-
SubmissionLedgerConformanceViolation,
|
|
6
|
-
type ConversationStoreConformanceCase,
|
|
7
|
-
type ConversationStoreConformanceFailure,
|
|
8
|
-
type SubmissionLedgerConformanceCase,
|
|
9
|
-
type SubmissionLedgerConformanceFailure,
|
|
10
|
-
} from "@effect-agent/session";
|