@effect-agent/storage-cloudflare 0.1.0-beta.39 → 0.1.0-beta.41
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 +19903 -8
- package/dist/index.mjs +211 -22
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -5
- package/src/do-memory-store.ts +51 -0
- package/src/index.ts +2 -0
- package/src/memory-protocol.ts +262 -0
- package/src/migrations.ts +3 -10
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.41",
|
|
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.41",
|
|
16
|
+
"@effect-agent/thread": "0.1.0-beta.41",
|
|
16
17
|
"@effect/platform-browser": "4.0.0-rc.111",
|
|
17
18
|
"@effect/sql-sqlite-do": "4.0.0-rc.111"
|
|
18
19
|
},
|
|
@@ -41,11 +42,11 @@
|
|
|
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",
|
|
48
|
-
"vite-plus": "0.
|
|
49
|
-
"vitest": "4.1.
|
|
49
|
+
"vite-plus": "0.3.0",
|
|
50
|
+
"vitest": "4.1.11"
|
|
50
51
|
}
|
|
51
52
|
}
|
|
@@ -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
|
+
});
|
package/src/migrations.ts
CHANGED
|
@@ -2,19 +2,12 @@ import { SqliteMigrator } from "@effect/sql-sqlite-do";
|
|
|
2
2
|
import { Effect } from "effect";
|
|
3
3
|
import * as SqlClient from "effect/unstable/sql/SqlClient";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* The exact-or-fresh storage version recorded in `effect_agent_meta`. Cloudflare is a fresh
|
|
7
|
-
* platform, so there is exactly ONE migration carrying the complete current schema — no
|
|
8
|
-
* v1→v4 history to replay (deployment spec §9: no rolling data-version promise during
|
|
9
|
-
* private development).
|
|
10
|
-
*/
|
|
5
|
+
/** The current storage version recorded in `effect_agent_meta`. */
|
|
11
6
|
export const CurrentDoStorageVersion = 2;
|
|
12
7
|
|
|
13
8
|
/**
|
|
14
|
-
* The Thread Durable Object schema
|
|
15
|
-
*
|
|
16
|
-
* into their final shape) so the shared conformance suites and crash-matrix rows address
|
|
17
|
-
* identical durable state. Two DC-specific additions:
|
|
9
|
+
* The Thread Durable Object schema shares its thread and ledger tables with Node/SQLite.
|
|
10
|
+
* Schedules and subscriptions use separate Durable Objects. Two DC-specific additions:
|
|
18
11
|
*
|
|
19
12
|
* 1. `effect_agent_meta` replaces `PRAGMA user_version` as the exact-or-fresh version gate —
|
|
20
13
|
* a meta table is portable regardless of which PRAGMAs Durable Object SQL storage allows.
|