@effect-agent/platform-cloudflare 0.1.0-beta.40 → 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 +215 -56
- package/dist/index.mjs +148 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/index.ts +1 -0
- package/src/memory.ts +317 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-agent/platform-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",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@effect-agent/core": "0.1.0-beta.
|
|
28
|
-
"@effect-agent/engine": "0.1.0-beta.
|
|
29
|
-
"@effect-agent/sandbox": "0.1.0-beta.
|
|
30
|
-
"@effect-agent/storage-cloudflare": "0.1.0-beta.
|
|
31
|
-
"@effect-agent/thread": "0.1.0-beta.
|
|
27
|
+
"@effect-agent/core": "0.1.0-beta.41",
|
|
28
|
+
"@effect-agent/engine": "0.1.0-beta.41",
|
|
29
|
+
"@effect-agent/sandbox": "0.1.0-beta.41",
|
|
30
|
+
"@effect-agent/storage-cloudflare": "0.1.0-beta.41",
|
|
31
|
+
"@effect-agent/thread": "0.1.0-beta.41",
|
|
32
32
|
"@effect/platform-browser": "4.0.0-rc.111",
|
|
33
33
|
"@effect/sql-sqlite-do": "4.0.0-rc.111"
|
|
34
34
|
},
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"@cloudflare/puppeteer": "1.1.0",
|
|
61
61
|
"@cloudflare/vitest-pool-workers": "0.21.3",
|
|
62
62
|
"@cloudflare/workers-types": "5.20260825.1",
|
|
63
|
-
"@effect-agent/capabilities": "0.1.0-beta.
|
|
64
|
-
"@effect-agent/testing": "0.1.0-beta.
|
|
63
|
+
"@effect-agent/capabilities": "0.1.0-beta.40",
|
|
64
|
+
"@effect-agent/testing": "0.1.0-beta.40",
|
|
65
65
|
"@effect/platform-node": "4.0.0-rc.111",
|
|
66
66
|
"@effect/sql-d1": "4.0.0-rc.111",
|
|
67
67
|
"@effect/vitest": "4.0.0-rc.111",
|
package/src/index.ts
CHANGED
package/src/memory.ts
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type MemoryLookup,
|
|
3
|
+
type MemoryReader,
|
|
4
|
+
type MemoryWrite,
|
|
5
|
+
Memory,
|
|
6
|
+
MemoryAccess,
|
|
7
|
+
MemoryDocument,
|
|
8
|
+
MemoryMutationFailpoint,
|
|
9
|
+
MemoryNamespace,
|
|
10
|
+
MemoryNamespaceAddress,
|
|
11
|
+
MemoryRecallLimits,
|
|
12
|
+
MemoryStorageError,
|
|
13
|
+
MemoryWriter,
|
|
14
|
+
type MemoryIndexSearch,
|
|
15
|
+
type SemanticCandidateLimits,
|
|
16
|
+
type SemanticMemoryProfile,
|
|
17
|
+
} from "@effect-agent/core";
|
|
18
|
+
import {
|
|
19
|
+
type DoMemoryStorageLimits,
|
|
20
|
+
type MemoryOwnerAuthorizer,
|
|
21
|
+
decodeMemoryWire,
|
|
22
|
+
defaultDoMemoryStorageLimits,
|
|
23
|
+
defaultMemoryRpcLimits,
|
|
24
|
+
doMemoryStoreLayerWithFailpoints,
|
|
25
|
+
encodeMemoryWire,
|
|
26
|
+
handleMemoryOwnerRequest,
|
|
27
|
+
MemoryOwnerIdentity,
|
|
28
|
+
MemoryOwnerRequest,
|
|
29
|
+
MemoryOwnerResponse,
|
|
30
|
+
MemoryRpcError,
|
|
31
|
+
MemoryRpcLimits,
|
|
32
|
+
type MemoryOwnerFailure,
|
|
33
|
+
} from "@effect-agent/storage-cloudflare";
|
|
34
|
+
import { Principal } from "@effect-agent/thread";
|
|
35
|
+
import { Clock, Context, Effect, Layer, Schema } from "effect";
|
|
36
|
+
import {
|
|
37
|
+
DurableObject as EffectCfDurableObject,
|
|
38
|
+
DurableObjectState,
|
|
39
|
+
type WorkerEnvironment,
|
|
40
|
+
} from "effect-cf";
|
|
41
|
+
|
|
42
|
+
export interface MemoryObjectRpc extends Rpc.DurableObjectBranded {
|
|
43
|
+
memory(encoded: string): Promise<string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class MemoryObjectNamespace extends Context.Service<
|
|
47
|
+
MemoryObjectNamespace,
|
|
48
|
+
{
|
|
49
|
+
readonly namespace: DurableObjectNamespace<MemoryObjectRpc>;
|
|
50
|
+
}
|
|
51
|
+
>()("@effect-agent/platform-cloudflare/MemoryObjectNamespace") {}
|
|
52
|
+
|
|
53
|
+
/** Namespace version and identity are already canonicalized by MemoryNamespace. */
|
|
54
|
+
export const memoryObjectName = (namespace: MemoryNamespace.Any): string => namespace.address;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Effect-native, host-bound memory client. Recall revalidates the entire admitted lookup
|
|
58
|
+
* in one RPC and renders it locally. No retries or per-source splitting occur here.
|
|
59
|
+
* Interrupted callers stop waiting; the owner has its own deadline. A timed-out write
|
|
60
|
+
* may have committed: reconcile by sending the identical operation ID and command.
|
|
61
|
+
*/
|
|
62
|
+
const makeMemoryClient = Effect.fn("CloudflareMemoryClient.make")(function* <
|
|
63
|
+
Namespace extends MemoryNamespace.Any,
|
|
64
|
+
>(
|
|
65
|
+
access: MemoryAccess<Namespace>,
|
|
66
|
+
principal: Principal,
|
|
67
|
+
rpcLimits: MemoryRpcLimits = defaultMemoryRpcLimits,
|
|
68
|
+
) {
|
|
69
|
+
const validated = yield* Schema.decodeUnknownEffect(MemoryRpcLimits)(rpcLimits).pipe(
|
|
70
|
+
Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
|
|
71
|
+
);
|
|
72
|
+
const bound = yield* Schema.decodeUnknownEffect(MemoryAccess.Wire)(access).pipe(
|
|
73
|
+
Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
|
|
74
|
+
);
|
|
75
|
+
principal = yield* Schema.decodeUnknownEffect(Principal)(principal).pipe(
|
|
76
|
+
Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
|
|
77
|
+
);
|
|
78
|
+
const { namespace } = yield* MemoryObjectNamespace;
|
|
79
|
+
const call = Effect.fn("CloudflareMemoryClient.call")(function* (request: MemoryOwnerRequest) {
|
|
80
|
+
const decoded = yield* Schema.decodeUnknownEffect(MemoryOwnerRequest)(request).pipe(
|
|
81
|
+
Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
|
|
82
|
+
);
|
|
83
|
+
const encoded = yield* encodeMemoryWire(MemoryOwnerRequest, decoded, validated.maxRequestBytes);
|
|
84
|
+
const raw = yield* Effect.tryPromise({
|
|
85
|
+
try: () =>
|
|
86
|
+
namespace.get(namespace.idFromName(memoryObjectName(bound.namespace))).memory(encoded),
|
|
87
|
+
catch: () => MemoryRpcError.make({ reason: "unavailable" }),
|
|
88
|
+
});
|
|
89
|
+
const response = yield* decodeMemoryWire(MemoryOwnerResponse, raw, validated.maxResponseBytes);
|
|
90
|
+
if (response._tag === "Failed") return yield* response.failure;
|
|
91
|
+
if (
|
|
92
|
+
!MemoryNamespace.equals(response.access.namespace, bound.namespace) ||
|
|
93
|
+
response.access.scope !== bound.scope
|
|
94
|
+
)
|
|
95
|
+
return yield* MemoryRpcError.make({ reason: "protocol" });
|
|
96
|
+
return response;
|
|
97
|
+
});
|
|
98
|
+
const withinDeadline = <A, E, R>(effect: Effect.Effect<A, E, R>, timeoutMillis: number) =>
|
|
99
|
+
effect.pipe(
|
|
100
|
+
Effect.timeoutOrElse({
|
|
101
|
+
duration: timeoutMillis,
|
|
102
|
+
orElse: () => Effect.fail(MemoryRpcError.make({ reason: "timeout" })),
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
const revalidate = Effect.fn("CloudflareMemoryClient.revalidate")(function* (
|
|
106
|
+
lookup: MemoryLookup,
|
|
107
|
+
limits: MemoryRecallLimits,
|
|
108
|
+
) {
|
|
109
|
+
limits = yield* Schema.decodeUnknownEffect(MemoryRecallLimits)(limits).pipe(
|
|
110
|
+
Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })),
|
|
111
|
+
);
|
|
112
|
+
const timeoutMillis = Math.min(validated.timeoutMillis, limits.timeoutMillis);
|
|
113
|
+
return yield* Effect.gen(function* () {
|
|
114
|
+
const response = yield* call({
|
|
115
|
+
_tag: "Revalidate",
|
|
116
|
+
version: 1,
|
|
117
|
+
access: bound,
|
|
118
|
+
principal,
|
|
119
|
+
lookup,
|
|
120
|
+
limits,
|
|
121
|
+
deadlineMillis: (yield* Clock.currentTimeMillis) + timeoutMillis,
|
|
122
|
+
});
|
|
123
|
+
if (response._tag !== "Lookup") return yield* MemoryRpcError.make({ reason: "protocol" });
|
|
124
|
+
return response.lookup;
|
|
125
|
+
}).pipe((effect) => withinDeadline(effect, timeoutMillis));
|
|
126
|
+
});
|
|
127
|
+
const change = Effect.fn("CloudflareMemoryClient.change")(function* (
|
|
128
|
+
write: MemoryWrite<Namespace>,
|
|
129
|
+
) {
|
|
130
|
+
if (!MemoryNamespace.equals(write.key.namespace, bound.namespace))
|
|
131
|
+
return yield* MemoryRpcError.make({ reason: "denied" });
|
|
132
|
+
return yield* Effect.gen(function* () {
|
|
133
|
+
const response = yield* call({
|
|
134
|
+
_tag: "Change",
|
|
135
|
+
version: 1,
|
|
136
|
+
access: bound,
|
|
137
|
+
principal,
|
|
138
|
+
write,
|
|
139
|
+
deadlineMillis: (yield* Clock.currentTimeMillis) + validated.timeoutMillis,
|
|
140
|
+
});
|
|
141
|
+
if (response._tag !== "Changed" || response.document.key.id !== write.key.id)
|
|
142
|
+
return yield* MemoryRpcError.make({ reason: "protocol" });
|
|
143
|
+
return yield* MemoryDocument.restore(access.namespace, response.document);
|
|
144
|
+
}).pipe((effect) => withinDeadline(effect, validated.timeoutMillis));
|
|
145
|
+
});
|
|
146
|
+
const revalidateSemantic = Effect.fn("CloudflareMemoryClient.revalidateSemantic")(function* (
|
|
147
|
+
found: MemoryIndexSearch<Namespace>,
|
|
148
|
+
profile: SemanticMemoryProfile,
|
|
149
|
+
limits: SemanticCandidateLimits,
|
|
150
|
+
) {
|
|
151
|
+
return yield* Effect.gen(function* () {
|
|
152
|
+
const response = yield* call({
|
|
153
|
+
_tag: "RevalidateSemantic",
|
|
154
|
+
version: 1,
|
|
155
|
+
access: bound,
|
|
156
|
+
principal,
|
|
157
|
+
found,
|
|
158
|
+
profile,
|
|
159
|
+
limits,
|
|
160
|
+
deadlineMillis: (yield* Clock.currentTimeMillis) + validated.timeoutMillis,
|
|
161
|
+
});
|
|
162
|
+
if (response._tag !== "Semantic") return yield* MemoryRpcError.make({ reason: "protocol" });
|
|
163
|
+
return response.result;
|
|
164
|
+
}).pipe((effect) => withinDeadline(effect, validated.timeoutMillis));
|
|
165
|
+
});
|
|
166
|
+
/**
|
|
167
|
+
* Revalidate in one owner RPC, then render whole passages within the caller's budget.
|
|
168
|
+
* The bound source is essential: unavailable/stale results and matches that cannot fit
|
|
169
|
+
* fail instead of silently producing empty context. No-match remains successful.
|
|
170
|
+
* The single outcome has sourceId "memory". No embedding or candidate search is performed.
|
|
171
|
+
* Use revalidate with Memory.recall for multiple readers sharing one output budget.
|
|
172
|
+
*/
|
|
173
|
+
const recall = Effect.fn("CloudflareMemoryClient.recall")(function* (
|
|
174
|
+
lookup: MemoryLookup,
|
|
175
|
+
limits: MemoryRecallLimits,
|
|
176
|
+
estimateTokens?: (text: string) => number,
|
|
177
|
+
) {
|
|
178
|
+
return yield* Memory.recall(
|
|
179
|
+
[{ id: "memory", essential: true, read: revalidate(lookup, limits) }],
|
|
180
|
+
limits,
|
|
181
|
+
estimateTokens,
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
return { recall, revalidate, revalidateSemantic, change };
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
export const CloudflareMemoryClient = {
|
|
188
|
+
/** Bind access and principal using the MemoryObjectNamespace supplied by the application. */
|
|
189
|
+
make: makeMemoryClient,
|
|
190
|
+
/** Use a resolved Worker or Durable Object binding without manual service provisioning. */
|
|
191
|
+
fromBinding: Effect.fn("CloudflareMemoryClient.fromBinding")(function* <
|
|
192
|
+
Namespace extends MemoryNamespace.Any,
|
|
193
|
+
>(
|
|
194
|
+
binding: DurableObjectNamespace<MemoryObjectRpc>,
|
|
195
|
+
options: {
|
|
196
|
+
readonly access: MemoryAccess<Namespace>;
|
|
197
|
+
readonly principal: Principal;
|
|
198
|
+
readonly rpcLimits?: MemoryRpcLimits;
|
|
199
|
+
},
|
|
200
|
+
) {
|
|
201
|
+
return yield* makeMemoryClient(options.access, options.principal, options.rpcLimits).pipe(
|
|
202
|
+
Effect.provideService(MemoryObjectNamespace, { namespace: binding }),
|
|
203
|
+
);
|
|
204
|
+
}),
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Optional activity-processor destination. Keeps domain write errors intact; transport,
|
|
209
|
+
* authorization and deadline failures become the existing MemoryStorageError contract.
|
|
210
|
+
* Receipts remain authoritative, including after caller interruption or lost replies.
|
|
211
|
+
*/
|
|
212
|
+
export const cloudflareMemoryWriterLayer = (
|
|
213
|
+
access: MemoryAccess,
|
|
214
|
+
principal: Principal,
|
|
215
|
+
limits: MemoryRpcLimits = defaultMemoryRpcLimits,
|
|
216
|
+
) =>
|
|
217
|
+
Layer.effect(
|
|
218
|
+
MemoryWriter,
|
|
219
|
+
Effect.gen(function* () {
|
|
220
|
+
const client = yield* CloudflareMemoryClient.make(access, principal, limits);
|
|
221
|
+
return MemoryWriter.fromAdapter({
|
|
222
|
+
change: (write) =>
|
|
223
|
+
client.change(write).pipe(
|
|
224
|
+
Effect.catchTag("MemoryRpcError", (error) =>
|
|
225
|
+
Effect.fail(
|
|
226
|
+
MemoryStorageError.make({
|
|
227
|
+
operation: `memory RPC ${error.reason}`,
|
|
228
|
+
reason:
|
|
229
|
+
error.reason === "unavailable" || error.reason === "timeout"
|
|
230
|
+
? "unavailable"
|
|
231
|
+
: "invalid-input",
|
|
232
|
+
}),
|
|
233
|
+
),
|
|
234
|
+
),
|
|
235
|
+
Effect.catchTag(["MemoryRecallError", "MemoryIndexError", "SemanticMemoryError"], () =>
|
|
236
|
+
Effect.fail(
|
|
237
|
+
MemoryStorageError.make({ operation: "memory RPC response", reason: "corrupt" }),
|
|
238
|
+
),
|
|
239
|
+
),
|
|
240
|
+
),
|
|
241
|
+
});
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
type OwnerServices = MemoryReader | MemoryWriter | MemoryOwnerAuthorizer | MemoryOwnerIdentity;
|
|
246
|
+
export interface MemoryObjectInstance extends InstanceType<
|
|
247
|
+
EffectCfDurableObject.DurableObjectClass<Record<never, never>, OwnerServices>
|
|
248
|
+
> {
|
|
249
|
+
memory(encoded: string): Promise<string>;
|
|
250
|
+
}
|
|
251
|
+
export interface MemoryObjectClass {
|
|
252
|
+
new (ctx: globalThis.DurableObjectState, env: Cloudflare.Env): MemoryObjectInstance;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Dedicated SQLite owner, independent of Thread lifetimes. The host binds authorization
|
|
257
|
+
* after restoring its namespace definition from MemoryOwnerIdentity. Do not retain
|
|
258
|
+
* cleanup-scoped resources in the host Layer; it lives for the DO incarnation.
|
|
259
|
+
*/
|
|
260
|
+
const makeMemoryObject = <E>(
|
|
261
|
+
host: Layer.Layer<
|
|
262
|
+
MemoryOwnerAuthorizer,
|
|
263
|
+
E,
|
|
264
|
+
MemoryOwnerIdentity | DurableObjectState.DurableObjectState | WorkerEnvironment
|
|
265
|
+
>,
|
|
266
|
+
options: {
|
|
267
|
+
readonly storageLimits?: DoMemoryStorageLimits;
|
|
268
|
+
readonly rpcLimits?: MemoryRpcLimits;
|
|
269
|
+
readonly failpoints?: Layer.Layer<
|
|
270
|
+
MemoryMutationFailpoint,
|
|
271
|
+
never,
|
|
272
|
+
DurableObjectState.DurableObjectState
|
|
273
|
+
>;
|
|
274
|
+
} = {},
|
|
275
|
+
): MemoryObjectClass => {
|
|
276
|
+
const identity = Layer.effect(
|
|
277
|
+
MemoryOwnerIdentity,
|
|
278
|
+
Effect.gen(function* () {
|
|
279
|
+
const state = yield* DurableObjectState.DurableObjectState;
|
|
280
|
+
const address = yield* Schema.decodeUnknownEffect(MemoryNamespaceAddress)(
|
|
281
|
+
state.raw.id.name,
|
|
282
|
+
).pipe(Effect.mapError(() => MemoryRpcError.make({ reason: "denied" })));
|
|
283
|
+
return { namespace: MemoryNamespace.Any.make({ address }) };
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
const store = Layer.unwrap(
|
|
287
|
+
Effect.map(DurableObjectState.DurableObjectState, (state) =>
|
|
288
|
+
doMemoryStoreLayerWithFailpoints(
|
|
289
|
+
state.raw.storage,
|
|
290
|
+
options.storageLimits ?? defaultDoMemoryStorageLimits,
|
|
291
|
+
),
|
|
292
|
+
),
|
|
293
|
+
).pipe(Layer.provide(options.failpoints ?? MemoryMutationFailpoint.layer));
|
|
294
|
+
const application = Layer.merge(store, host).pipe(Layer.provideMerge(identity));
|
|
295
|
+
const runtime: Layer.Layer<
|
|
296
|
+
OwnerServices,
|
|
297
|
+
E | MemoryOwnerFailure,
|
|
298
|
+
DurableObjectState.DurableObjectState | WorkerEnvironment
|
|
299
|
+
> = Layer.effectContext(
|
|
300
|
+
Effect.gen(function* () {
|
|
301
|
+
const state = yield* DurableObjectState.DurableObjectState;
|
|
302
|
+
const scope = yield* Effect.scope;
|
|
303
|
+
yield* Schema.decodeUnknownEffect(MemoryRpcLimits)(
|
|
304
|
+
options.rpcLimits ?? defaultMemoryRpcLimits,
|
|
305
|
+
).pipe(Effect.mapError(() => MemoryRpcError.make({ reason: "protocol" })));
|
|
306
|
+
return yield* state.blockConcurrencyWhile(Layer.buildWithScope(application, scope));
|
|
307
|
+
}),
|
|
308
|
+
);
|
|
309
|
+
return EffectCfDurableObject.make(runtime, {
|
|
310
|
+
rpc: { memory: (encoded: string) => handleMemoryOwnerRequest(encoded, options.rpcLimits) },
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
export const MemoryObject = {
|
|
315
|
+
/** Build the SQLite Durable Object class with the application's owner authorization Layer. */
|
|
316
|
+
make: makeMemoryObject,
|
|
317
|
+
};
|