@effect-agent/storage-cloudflare 0.1.0-beta.40 → 0.1.0-beta.42
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/index.d.mts +19902 -2
- package/dist/index.mjs +197 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/do-memory-store.ts +51 -0
- package/src/index.ts +2 -0
- package/src/memory-protocol.ts +262 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-agent/storage-cloudflare",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.42",
|
|
4
4
|
"exports": {
|
|
5
5
|
".": {
|
|
6
6
|
"types": "./dist/index.d.mts",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@effect-agent/
|
|
15
|
+
"@effect-agent/core": "0.1.0-beta.42",
|
|
16
|
+
"@effect-agent/thread": "0.1.0-beta.42",
|
|
16
17
|
"@effect/platform-browser": "4.0.0-rc.111",
|
|
17
18
|
"@effect/sql-sqlite-do": "4.0.0-rc.111"
|
|
18
19
|
},
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@cloudflare/vitest-pool-workers": "0.21.3",
|
|
43
44
|
"@cloudflare/workers-types": "5.20260825.1",
|
|
44
|
-
"@effect-agent/testing": "0.1.0-beta.
|
|
45
|
+
"@effect-agent/testing": "0.1.0-beta.40",
|
|
45
46
|
"@effect/vitest": "4.0.0-rc.111",
|
|
46
47
|
"effect": "4.0.0-rc.111",
|
|
47
48
|
"typescript": "7.0.2",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { MemoryStorageError, MemoryMutationFailpoint } from "@effect-agent/core";
|
|
2
|
+
import { memoryStoreLayerWithFailpoints, SqlMemoryLimits } from "@effect-agent/thread/sql-memory";
|
|
3
|
+
import { SqliteClient } from "@effect/sql-sqlite-do";
|
|
4
|
+
import { Effect, Layer, Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
export class DoMemoryStorageLimits extends Schema.Class<DoMemoryStorageLimits>(
|
|
7
|
+
"@effect-agent/storage-cloudflare/DoMemoryStorageLimits",
|
|
8
|
+
)({
|
|
9
|
+
maxRowBytes: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1_900_000 })),
|
|
10
|
+
maxStorageBytes: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 536_870_912 })),
|
|
11
|
+
maxDocuments: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100_000 })),
|
|
12
|
+
maxReceipts: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 1_000_000 })),
|
|
13
|
+
}) {}
|
|
14
|
+
|
|
15
|
+
export const defaultDoMemoryStorageLimits = DoMemoryStorageLimits.make({
|
|
16
|
+
maxRowBytes: 1_900_000,
|
|
17
|
+
maxStorageBytes: 536_870_912,
|
|
18
|
+
maxDocuments: 10_000,
|
|
19
|
+
maxReceipts: 100_000,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Local memory only, without Thread tables. Keep one SQL client per owner and pass the
|
|
24
|
+
* full storage handle: sql-only handles cannot provide atomic receipts and revisions.
|
|
25
|
+
* Byte limits conservatively count encoded rows, not SQLite page/index overhead.
|
|
26
|
+
*/
|
|
27
|
+
export const doMemoryStoreLayerWithFailpoints = (
|
|
28
|
+
storage: NonNullable<SqliteClient.SqliteClientConfig["storage"]>,
|
|
29
|
+
limits: DoMemoryStorageLimits = defaultDoMemoryStorageLimits,
|
|
30
|
+
) =>
|
|
31
|
+
Layer.unwrap(
|
|
32
|
+
Schema.decodeUnknownEffect(DoMemoryStorageLimits)(limits).pipe(
|
|
33
|
+
Effect.mapError(() =>
|
|
34
|
+
MemoryStorageError.make({ operation: "memory storage limits", reason: "invalid-input" }),
|
|
35
|
+
),
|
|
36
|
+
Effect.map((validated) =>
|
|
37
|
+
memoryStoreLayerWithFailpoints.pipe(
|
|
38
|
+
Layer.provide(Layer.succeed(SqlMemoryLimits, validated)),
|
|
39
|
+
Layer.provide(SqliteClient.layer({ storage })),
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
export const doMemoryStoreLayer = (
|
|
46
|
+
storage: NonNullable<SqliteClient.SqliteClientConfig["storage"]>,
|
|
47
|
+
limits: DoMemoryStorageLimits = defaultDoMemoryStorageLimits,
|
|
48
|
+
) =>
|
|
49
|
+
doMemoryStoreLayerWithFailpoints(storage, limits).pipe(
|
|
50
|
+
Layer.provide(MemoryMutationFailpoint.layer),
|
|
51
|
+
);
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import type { MemoryReader } from "@effect-agent/core";
|
|
2
|
+
import {
|
|
3
|
+
MemoryAccess,
|
|
4
|
+
MemoryConflict,
|
|
5
|
+
MemoryDocument,
|
|
6
|
+
MemoryLookup,
|
|
7
|
+
MemoryMutationFailure,
|
|
8
|
+
MemoryNamespace,
|
|
9
|
+
MemoryOperationConflict,
|
|
10
|
+
MemoryRecallError,
|
|
11
|
+
MemoryRecallLimits,
|
|
12
|
+
MemoryStorageError,
|
|
13
|
+
MemoryWithdrawn,
|
|
14
|
+
MemoryWrite,
|
|
15
|
+
MemoryWriter,
|
|
16
|
+
revalidateMemoryLookup,
|
|
17
|
+
MemoryIndexSearch,
|
|
18
|
+
MemoryIndexError,
|
|
19
|
+
SemanticMemoryError,
|
|
20
|
+
SemanticMemoryProfile,
|
|
21
|
+
SemanticCandidateLimits,
|
|
22
|
+
SemanticCandidateResult,
|
|
23
|
+
revalidateSemanticMemoryCandidates,
|
|
24
|
+
} from "@effect-agent/core";
|
|
25
|
+
import { Principal } from "@effect-agent/thread";
|
|
26
|
+
import { Clock, Context, Effect, Encoding, Schema } from "effect";
|
|
27
|
+
|
|
28
|
+
export class MemoryRpcError extends Schema.TaggedError<MemoryRpcError>()("MemoryRpcError", {
|
|
29
|
+
reason: Schema.Literals(["denied", "protocol", "budget", "timeout", "unavailable"]),
|
|
30
|
+
}) {}
|
|
31
|
+
|
|
32
|
+
export class MemoryRpcLimits extends Schema.Class<MemoryRpcLimits>(
|
|
33
|
+
"@effect-agent/storage-cloudflare/MemoryRpcLimits",
|
|
34
|
+
)({
|
|
35
|
+
maxRequestBytes: Schema.Int.check(Schema.isBetween({ minimum: 256, maximum: 4_194_304 })),
|
|
36
|
+
maxResponseBytes: Schema.Int.check(Schema.isBetween({ minimum: 256, maximum: 16_777_216 })),
|
|
37
|
+
maxSourceBytes: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 67_108_864 })),
|
|
38
|
+
maxSources: MemoryRecallLimits.fields.maxSources,
|
|
39
|
+
timeoutMillis: MemoryRecallLimits.fields.timeoutMillis,
|
|
40
|
+
}) {}
|
|
41
|
+
|
|
42
|
+
export const defaultMemoryRpcLimits = MemoryRpcLimits.make({
|
|
43
|
+
maxRequestBytes: 1_048_576,
|
|
44
|
+
maxResponseBytes: 4_194_304,
|
|
45
|
+
maxSourceBytes: 16_777_216,
|
|
46
|
+
maxSources: 16,
|
|
47
|
+
timeoutMillis: 10_000,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const RequestFields = {
|
|
51
|
+
version: Schema.Literal(1),
|
|
52
|
+
access: MemoryAccess.Wire,
|
|
53
|
+
/** Host-authenticated identity, never copied from model input. The owner still authorizes it. */
|
|
54
|
+
principal: Principal,
|
|
55
|
+
deadlineMillis: Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const RevalidateRequest = Schema.TaggedStruct("Revalidate", {
|
|
59
|
+
...RequestFields,
|
|
60
|
+
lookup: MemoryLookup,
|
|
61
|
+
limits: MemoryRecallLimits,
|
|
62
|
+
});
|
|
63
|
+
const ChangeRequest = Schema.TaggedStruct("Change", { ...RequestFields, write: MemoryWrite.Wire });
|
|
64
|
+
const SemanticRequest: Schema.TaggedStruct<
|
|
65
|
+
"RevalidateSemantic",
|
|
66
|
+
typeof RequestFields & {
|
|
67
|
+
readonly found: typeof MemoryIndexSearch.Wire;
|
|
68
|
+
readonly profile: typeof SemanticMemoryProfile;
|
|
69
|
+
readonly limits: typeof SemanticCandidateLimits;
|
|
70
|
+
}
|
|
71
|
+
> = Schema.TaggedStruct("RevalidateSemantic", {
|
|
72
|
+
...RequestFields,
|
|
73
|
+
found: MemoryIndexSearch.Wire,
|
|
74
|
+
profile: SemanticMemoryProfile,
|
|
75
|
+
limits: SemanticCandidateLimits,
|
|
76
|
+
});
|
|
77
|
+
export const MemoryOwnerRequest: Schema.Union<
|
|
78
|
+
[typeof RevalidateRequest, typeof ChangeRequest, typeof SemanticRequest]
|
|
79
|
+
> = Schema.Union([RevalidateRequest, ChangeRequest, SemanticRequest]);
|
|
80
|
+
export type MemoryOwnerRequest = typeof MemoryOwnerRequest.Type;
|
|
81
|
+
|
|
82
|
+
export const MemoryOwnerFailure = Schema.Union([
|
|
83
|
+
MemoryRpcError,
|
|
84
|
+
MemoryStorageError,
|
|
85
|
+
MemoryRecallError,
|
|
86
|
+
MemoryConflict,
|
|
87
|
+
MemoryWithdrawn,
|
|
88
|
+
MemoryOperationConflict,
|
|
89
|
+
MemoryMutationFailure,
|
|
90
|
+
MemoryIndexError,
|
|
91
|
+
SemanticMemoryError,
|
|
92
|
+
]);
|
|
93
|
+
export type MemoryOwnerFailure = typeof MemoryOwnerFailure.Type;
|
|
94
|
+
|
|
95
|
+
export const MemoryOwnerResponse = Schema.Union([
|
|
96
|
+
Schema.TaggedStruct("Lookup", { access: MemoryAccess.Wire, lookup: MemoryLookup }),
|
|
97
|
+
Schema.TaggedStruct("Changed", { access: MemoryAccess.Wire, document: MemoryDocument.Wire }),
|
|
98
|
+
Schema.TaggedStruct("Semantic", { access: MemoryAccess.Wire, result: SemanticCandidateResult }),
|
|
99
|
+
Schema.TaggedStruct("Failed", { failure: MemoryOwnerFailure }),
|
|
100
|
+
]);
|
|
101
|
+
export type MemoryOwnerResponse = typeof MemoryOwnerResponse.Type;
|
|
102
|
+
|
|
103
|
+
/** Fail-closed application policy. Authorize the namespace, principal, scope, and full command. */
|
|
104
|
+
export class MemoryOwnerAuthorizer extends Context.Service<
|
|
105
|
+
MemoryOwnerAuthorizer,
|
|
106
|
+
{
|
|
107
|
+
readonly authorize: (request: MemoryOwnerRequest) => Effect.Effect<void, MemoryRpcError>;
|
|
108
|
+
}
|
|
109
|
+
>()("@effect-agent/storage-cloudflare/MemoryOwnerAuthorizer") {}
|
|
110
|
+
|
|
111
|
+
/** Canonical namespace derived from this object's idFromName identity, never its request. */
|
|
112
|
+
export class MemoryOwnerIdentity extends Context.Service<
|
|
113
|
+
MemoryOwnerIdentity,
|
|
114
|
+
{
|
|
115
|
+
readonly namespace: MemoryNamespace.Any;
|
|
116
|
+
}
|
|
117
|
+
>()("@effect-agent/storage-cloudflare/MemoryOwnerIdentity") {}
|
|
118
|
+
|
|
119
|
+
export const memoryWireBytes = (text: string): number => Encoding.encodeHex(text).length / 2;
|
|
120
|
+
|
|
121
|
+
export const decodeMemoryWire = Effect.fn("decodeMemoryWire")(function* <A, I>(
|
|
122
|
+
schema: Schema.Codec<A, I, never>,
|
|
123
|
+
raw: unknown,
|
|
124
|
+
maxBytes: number,
|
|
125
|
+
) {
|
|
126
|
+
const text = yield* Schema.decodeUnknownEffect(Schema.String)(raw).pipe(
|
|
127
|
+
Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
|
|
128
|
+
);
|
|
129
|
+
if (text.length > maxBytes || memoryWireBytes(text) > maxBytes)
|
|
130
|
+
return yield* MemoryRpcError.make({ reason: "budget" });
|
|
131
|
+
return yield* Schema.decodeEffect(Schema.fromJsonString(schema))(text).pipe(
|
|
132
|
+
Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
export const encodeMemoryWire = Effect.fn("encodeMemoryWire")(function* <A, I>(
|
|
137
|
+
schema: Schema.Codec<A, I, never, never>,
|
|
138
|
+
value: A,
|
|
139
|
+
maxBytes: number,
|
|
140
|
+
) {
|
|
141
|
+
const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(schema))(value).pipe(
|
|
142
|
+
Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
|
|
143
|
+
);
|
|
144
|
+
if (encoded.length > maxBytes || memoryWireBytes(encoded) > maxBytes)
|
|
145
|
+
return yield* MemoryRpcError.make({ reason: "budget" });
|
|
146
|
+
return encoded;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/** One local read per distinct candidate source, with no per-document network calls. */
|
|
150
|
+
export const handleMemoryOwnerRequest = Effect.fn("MemoryOwner.handleRequest")(function* (
|
|
151
|
+
raw: unknown,
|
|
152
|
+
limits: MemoryRpcLimits = defaultMemoryRpcLimits,
|
|
153
|
+
): Effect.fn.Return<
|
|
154
|
+
string,
|
|
155
|
+
never,
|
|
156
|
+
MemoryOwnerIdentity | MemoryOwnerAuthorizer | MemoryReader | MemoryWriter
|
|
157
|
+
> {
|
|
158
|
+
const result = yield* Effect.gen(function* (): Effect.fn.Return<
|
|
159
|
+
MemoryOwnerResponse,
|
|
160
|
+
MemoryOwnerFailure,
|
|
161
|
+
MemoryOwnerIdentity | MemoryOwnerAuthorizer | MemoryReader | MemoryWriter
|
|
162
|
+
> {
|
|
163
|
+
limits = yield* Schema.decodeUnknownEffect(MemoryRpcLimits)(limits).pipe(
|
|
164
|
+
Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
|
|
165
|
+
);
|
|
166
|
+
const request = yield* decodeMemoryWire(MemoryOwnerRequest, raw, limits.maxRequestBytes);
|
|
167
|
+
const { namespace } = yield* MemoryOwnerIdentity;
|
|
168
|
+
if (
|
|
169
|
+
!MemoryNamespace.equals(namespace, request.access.namespace) ||
|
|
170
|
+
(request._tag === "Change" &&
|
|
171
|
+
!MemoryNamespace.equals(namespace, request.write.key.namespace)) ||
|
|
172
|
+
(request._tag === "RevalidateSemantic" &&
|
|
173
|
+
request.found.candidates.some(
|
|
174
|
+
(candidate) => !MemoryNamespace.equals(namespace, candidate.key.namespace),
|
|
175
|
+
))
|
|
176
|
+
)
|
|
177
|
+
return yield* MemoryRpcError.make({ reason: "denied" });
|
|
178
|
+
const remaining = Math.min(
|
|
179
|
+
limits.timeoutMillis,
|
|
180
|
+
request.deadlineMillis - (yield* Clock.currentTimeMillis),
|
|
181
|
+
);
|
|
182
|
+
if (remaining <= 0) return yield* MemoryRpcError.make({ reason: "timeout" });
|
|
183
|
+
return yield* Effect.gen(function* (): Effect.fn.Return<
|
|
184
|
+
MemoryOwnerResponse,
|
|
185
|
+
MemoryOwnerFailure,
|
|
186
|
+
MemoryOwnerAuthorizer | MemoryReader | MemoryWriter
|
|
187
|
+
> {
|
|
188
|
+
const authorizer = yield* MemoryOwnerAuthorizer;
|
|
189
|
+
yield* authorizer.authorize(request);
|
|
190
|
+
if (request._tag === "Change") {
|
|
191
|
+
const writer = yield* MemoryWriter;
|
|
192
|
+
return {
|
|
193
|
+
_tag: "Changed",
|
|
194
|
+
access: request.access,
|
|
195
|
+
document: yield* writer.change(request.write),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (request._tag === "RevalidateSemantic") {
|
|
199
|
+
if (
|
|
200
|
+
new Set(request.found.candidates.map((candidate) => candidate.key.id)).size >
|
|
201
|
+
limits.maxSources
|
|
202
|
+
)
|
|
203
|
+
return yield* MemoryRpcError.make({ reason: "budget" });
|
|
204
|
+
const result = yield* revalidateSemanticMemoryCandidates(
|
|
205
|
+
request.found,
|
|
206
|
+
request.access,
|
|
207
|
+
request.profile,
|
|
208
|
+
{
|
|
209
|
+
...request.limits,
|
|
210
|
+
maxSourceBytes: Math.min(
|
|
211
|
+
limits.maxSourceBytes,
|
|
212
|
+
request.limits.maxSourceBytes ?? 16_777_216,
|
|
213
|
+
),
|
|
214
|
+
maxOutputBytes: Math.min(
|
|
215
|
+
limits.maxResponseBytes,
|
|
216
|
+
request.limits.maxOutputBytes ?? 16_777_216,
|
|
217
|
+
),
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
return { _tag: "Semantic", access: request.access, result };
|
|
221
|
+
}
|
|
222
|
+
const count =
|
|
223
|
+
request.lookup._tag === "Found"
|
|
224
|
+
? new Set(request.lookup.passages.map((passage) => passage.source.id)).size
|
|
225
|
+
: 0;
|
|
226
|
+
if (count > Math.min(limits.maxSources, request.limits.maxSources))
|
|
227
|
+
return yield* MemoryRpcError.make({ reason: "budget" });
|
|
228
|
+
const lookup = yield* revalidateMemoryLookup(request.lookup, request.access, {
|
|
229
|
+
maxSourceBytes: limits.maxSourceBytes,
|
|
230
|
+
maxInputBytes: Math.min(limits.maxSourceBytes, request.limits.maxInputBytes ?? 16_777_216),
|
|
231
|
+
});
|
|
232
|
+
return { _tag: "Lookup", access: request.access, lookup };
|
|
233
|
+
}).pipe(
|
|
234
|
+
Effect.scoped,
|
|
235
|
+
Effect.timeoutOrElse({
|
|
236
|
+
duration: remaining,
|
|
237
|
+
orElse: () => Effect.fail(MemoryRpcError.make({ reason: "timeout" })),
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
}).pipe(
|
|
241
|
+
Effect.flatMap((response) =>
|
|
242
|
+
encodeMemoryWire(MemoryOwnerResponse, response, limits.maxResponseBytes),
|
|
243
|
+
),
|
|
244
|
+
Effect.result,
|
|
245
|
+
);
|
|
246
|
+
if (result._tag === "Success") return result.success;
|
|
247
|
+
return yield* encodeMemoryWire(
|
|
248
|
+
MemoryOwnerResponse,
|
|
249
|
+
{
|
|
250
|
+
_tag: "Failed",
|
|
251
|
+
failure: result.failure,
|
|
252
|
+
},
|
|
253
|
+
limits.maxResponseBytes,
|
|
254
|
+
).pipe(
|
|
255
|
+
Effect.catch(() =>
|
|
256
|
+
Schema.encodeEffect(Schema.fromJsonString(MemoryOwnerResponse))({
|
|
257
|
+
_tag: "Failed",
|
|
258
|
+
failure: MemoryRpcError.make({ reason: "budget" }),
|
|
259
|
+
}).pipe(Effect.orDie),
|
|
260
|
+
),
|
|
261
|
+
);
|
|
262
|
+
});
|