@effect-agent/platform-cloudflare 0.1.0-beta.37 → 0.1.0-beta.39

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/src/layers.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ConversationId } from "@effect-agent/core";
1
+ import { ThreadId } from "@effect-agent/core";
2
2
  import type {
3
3
  RunContextPreparation,
4
4
  RunCostEstimator,
@@ -7,27 +7,13 @@ import type {
7
7
  import {
8
8
  CurrentToolFailureObserver,
9
9
  RunContextPreparationPassthrough,
10
+ RunToolAuthorization,
10
11
  toolFailureObserverLayer,
11
12
  } from "@effect-agent/engine";
12
13
  import {
13
- AgentBindingResolver,
14
- DurableAgentRuntime,
15
- DurableRuntimeConfig,
16
- DurableRuntimeFailpoint,
17
- ProducerId,
18
- operationAuthorizerLayer,
19
- ToolReconciler,
20
- type ConversationStore,
21
- type DurableRuntimeFailpointHandler,
22
- type OperationAuthorizerService,
23
- type ResolvedBinding,
24
- type SubmissionLedger,
25
- type WakeScheduler,
26
- } from "@effect-agent/session";
27
- import {
28
- conversationStoreLayer,
14
+ threadStoreLayer,
29
15
  executePortRequest,
30
- routedConversationStoreLayer,
16
+ routedThreadStoreLayer,
31
17
  routedSubmissionLedgerLayer,
32
18
  storageConfigLayer,
33
19
  storageFailpointLayer,
@@ -35,24 +21,42 @@ import {
35
21
  type DoStorageFailpointHandler,
36
22
  type DoStorageInitializationError,
37
23
  type DoStorageOptions,
24
+ type DoStorageFailpoint,
38
25
  type PortRequest,
39
26
  type PortResponse,
40
27
  } from "@effect-agent/storage-cloudflare";
28
+ import {
29
+ compileRegistrations,
30
+ DurableAgentRuntime,
31
+ DurableRuntimeConfig,
32
+ DurableRuntimeFailpoint,
33
+ ProducerId,
34
+ operationAuthorizerLayer,
35
+ ToolReconciler,
36
+ type AgentRegistration,
37
+ type ThreadStore,
38
+ type DigestError,
39
+ type DurableRuntimeFailpointHandler,
40
+ type OperationAuthorizerService,
41
+ type ResolvedBinding,
42
+ type SubmissionLedger,
43
+ type WakeScheduler,
44
+ } from "@effect-agent/thread";
41
45
  import { BrowserCrypto } from "@effect/platform-browser";
42
46
  import { SqliteClient } from "@effect/sql-sqlite-do";
43
47
  import type { Crypto } from "effect";
44
48
  import { Context, Duration, Effect, Layer, Schema } from "effect";
45
49
 
46
50
  import {
47
- ConversationMaintenance,
48
- ConversationMaintenanceFailpoint,
51
+ ThreadMaintenance,
52
+ ThreadMaintenanceFailpoint,
49
53
  DurableAlarmService,
50
- type ConversationMaintenanceFailpointHandler,
54
+ type ThreadMaintenanceFailpointHandler,
51
55
  } from "./alarm.ts";
52
56
  import {
53
- ConversationObjectIdentity,
57
+ ThreadObjectIdentity,
54
58
  DurableObjectContext,
55
- type ConversationObjectNamespace,
59
+ type ThreadObjectNamespace,
56
60
  } from "./bindings.ts";
57
61
  import {
58
62
  CLOUDFLARE_RUNTIME_DEFAULTS,
@@ -61,18 +65,18 @@ import {
61
65
  CloudflarePlatformConfigError,
62
66
  } from "./config.ts";
63
67
  import { ProgressWaitRegistry } from "./progress-wait.ts";
64
- import { conversationPortTransportLayer } from "./transport.ts";
68
+ import { threadPortTransportLayer } from "./transport.ts";
65
69
  import { cloudflareWakeSchedulerLayer } from "./wake-scheduler.ts";
66
70
 
67
71
  /**
68
- * Raw (unvalidated) construction options for `CloudflareDurableRuntime.layer`, mirroring
72
+ * Raw (unvalidated) construction options for `ThreadObject.make`, mirroring
69
73
  * `NodeDurableRuntimeOptions`. Optional fields default to the documented production values
70
74
  * (`CLOUDFLARE_RUNTIME_DEFAULTS`); everything is schema-decoded into
71
75
  * `CloudflareDurableRuntimeConfigValue` before any resource opens (deployment §5 gate 1).
72
76
  */
73
77
  export interface CloudflareDurableRuntimeOptions {
74
78
  readonly deploymentId: string;
75
- /** Head of the minted producer identity `{producerPrefix}:{conversationId}`. */
79
+ /** Head of the minted producer identity `{producerPrefix}:{threadId}`. */
76
80
  readonly producerPrefix: string;
77
81
  /** Milliseconds; default 30s (D5). */
78
82
  readonly ownershipLeaseDuration?: number | undefined;
@@ -114,9 +118,9 @@ export interface CloudflareDurableRuntimeOptions {
114
118
  readonly runtimeFailpoint?:
115
119
  | ((ctx: DurableObjectState) => DurableRuntimeFailpointHandler)
116
120
  | undefined;
117
- /** Conversation-maintenance generation/alarm fault injection; default none. */
121
+ /** Thread-maintenance generation/alarm fault injection; default none. */
118
122
  readonly maintenanceFailpoint?:
119
- | ((ctx: DurableObjectState) => ConversationMaintenanceFailpointHandler)
123
+ | ((ctx: DurableObjectState) => ThreadMaintenanceFailpointHandler)
120
124
  | undefined;
121
125
  /** Host-supplied fail-closed authorization policy; defaults to service possession. */
122
126
  readonly operationAuthorizer?: OperationAuthorizerService | undefined;
@@ -126,65 +130,36 @@ export interface CloudflareDurableRuntimeOptions {
126
130
  * `ToolReconciler.uncertain`.
127
131
  */
128
132
  readonly toolReconciler?: Layer.Layer<ToolReconciler> | undefined;
129
- /**
130
- * Registered worker Bindings resolved at durable claim time:
131
- * build each with `DurableWorkerBinding.make(binding, digests)`. The callback receives the live
132
- * Object context and derived identities and is evaluated once per incarnation during Layer
133
- * construction. Defaults to the empty registration (every resolved claim fails closed).
134
- */
135
- readonly bindings?: CloudflareBindingSource | undefined;
136
- /**
137
- * Generic Run context acquired with this Durable Object incarnation. It may prepare model
138
- * context and/or authorize exact model-declared application Tool Calls at action time. The
139
- * Layer may depend
140
- * only on `Crypto.Crypto`, which this platform supplies with `BrowserCrypto`; hosts must close
141
- * every application-specific service before passing it here. Default absent.
142
- */
143
- readonly runContext?: CloudflareRunContextSource | undefined;
144
- }
145
-
146
- /** Per-incarnation host values available to Effect-native runtime extension factories. */
147
- export interface CloudflareRuntimeSourceContext {
148
- readonly ctx: DurableObjectState;
149
- readonly env: unknown;
150
- readonly conversationId: ConversationId;
151
- readonly producerId: ProducerId;
152
133
  }
153
134
 
154
- /** Per-incarnation host values available while registered worker Bindings are captured. */
155
- export interface CloudflareBindingSourceContext extends CloudflareRuntimeSourceContext {}
156
-
157
- /** Captures registered worker Bindings once for each Durable Object incarnation. */
158
- export type CloudflareBindingSource = (
159
- context: CloudflareBindingSourceContext,
160
- ) => Effect.Effect<ReadonlyArray<ResolvedBinding>, never, never>;
161
-
162
- /** A closed Run-context service whose only remaining requirement is platform Crypto. */
163
- export type CloudflareRunContextLayer = Layer.Layer<RunContextPreparation, never, Crypto.Crypto>;
164
-
165
- /** One Layer or a per-incarnation factory over explicit Cloudflare host values. */
166
- export type CloudflareRunContextSource =
167
- | CloudflareRunContextLayer
168
- | ((context: CloudflareRuntimeSourceContext) => CloudflareRunContextLayer);
135
+ /** Services supplied before the application graph is built, including its dependencies. */
136
+ export type CloudflareBootstrapServices =
137
+ | CloudflareDurableRuntimeConfig
138
+ | ThreadObjectIdentity
139
+ | DurableRuntimeConfig
140
+ | Crypto.Crypto
141
+ | DoStorageFailpoint
142
+ | DurableRuntimeFailpoint
143
+ | ThreadMaintenanceFailpoint
144
+ | RunContextPreparation
145
+ | RunToolAuthorization
146
+ | ToolReconciler;
169
147
 
170
148
  /** Every construction failure of the assembled Cloudflare durable runtime stack. */
171
149
  export type CloudflareDurableRuntimeInitializationError =
172
150
  | CloudflarePlatformConfigError
151
+ | DigestError
173
152
  | DoStorageInitializationError;
174
153
 
175
- /** The services `CloudflareDurableRuntime.layer` provides. */
154
+ /** The services `ThreadObject.layer` provides. */
176
155
  export type CloudflareDurableRuntimeServices =
177
156
  | DurableAgentRuntime
178
157
  | SubmissionLedger
179
- | ConversationStore
158
+ | ThreadStore
180
159
  | WakeScheduler
181
- | DurableRuntimeConfig
182
- | AgentBindingResolver
183
- | CloudflareDurableRuntimeConfig
184
- | ConversationObjectIdentity
185
160
  | DurableAlarmService
186
- | ConversationMaintenance
187
- | ConversationObjectPorts
161
+ | ThreadMaintenance
162
+ | ThreadObjectPorts
188
163
  | ProgressWaitRegistry;
189
164
 
190
165
  /**
@@ -193,15 +168,15 @@ export type CloudflareDurableRuntimeServices =
193
168
  * request cannot bounce between Objects — and returns the typed response for the endpoint to
194
169
  * encode.
195
170
  */
196
- export class ConversationObjectPorts extends Context.Service<
197
- ConversationObjectPorts,
171
+ export class ThreadObjectPorts extends Context.Service<
172
+ ThreadObjectPorts,
198
173
  {
199
174
  readonly handle: (request: PortRequest) => Effect.Effect<PortResponse>;
200
175
  }
201
- >()("@effect-agent/platform-cloudflare/ConversationObjectPorts") {}
176
+ >()("@effect-agent/platform-cloudflare/ThreadObjectPorts") {}
202
177
 
203
178
  const decodeConfigValue = Schema.decodeUnknownEffect(CloudflareDurableRuntimeConfigValue);
204
- const decodeConversationId = Schema.decodeUnknownEffect(ConversationId);
179
+ const decodeThreadId = Schema.decodeUnknownEffect(ThreadId);
205
180
  const decodeProducerId = Schema.decodeUnknownEffect(ProducerId);
206
181
 
207
182
  const configFromOptions = (
@@ -244,203 +219,149 @@ const configFromOptions = (
244
219
  );
245
220
 
246
221
  /**
247
- * The Conversation this Object owns, from the Object identity rule (plan §1.2): Conversation
248
- * Objects are addressed exclusively by `idFromName(conversationId)`, so `ctx.id.name` IS the
249
- * Conversation ID. An unnamed Object (from `newUniqueId`) is a deployment error, not a lane.
222
+ * The Thread this Object owns, from the Object identity rule (plan §1.2): Thread
223
+ * Objects are addressed exclusively by `idFromName(threadId)`, so `ctx.id.name` IS the
224
+ * Thread ID. An unnamed Object (from `newUniqueId`) is a deployment error, not a lane.
250
225
  */
251
- const conversationIdFromState = (
226
+ const threadIdFromState = (
252
227
  ctx: DurableObjectState,
253
- ): Effect.Effect<ConversationId, CloudflarePlatformConfigError> =>
228
+ ): Effect.Effect<ThreadId, CloudflarePlatformConfigError> =>
254
229
  ctx.id.name === undefined
255
230
  ? Effect.fail(
256
231
  CloudflarePlatformConfigError.make({
257
232
  message:
258
- "This Durable Object was not created via idFromName(conversationId); Conversation " +
259
- "Objects must be addressed by their Conversation identity (plan §1.2).",
233
+ "This Durable Object was not created via idFromName(threadId); Thread " +
234
+ "Objects must be addressed by their Thread identity (plan §1.2).",
260
235
  }),
261
236
  )
262
- : decodeConversationId(ctx.id.name).pipe(
237
+ : decodeThreadId(ctx.id.name).pipe(
263
238
  Effect.mapError((error) =>
264
239
  CloudflarePlatformConfigError.make({
265
- message: `The Durable Object name is not a valid ConversationId: ${error.message}`,
240
+ message: `The Durable Object name is not a valid ThreadId: ${error.message}`,
266
241
  cause: error,
267
242
  }),
268
243
  ),
269
244
  );
270
245
 
271
- const resolveBindings = (
272
- source: CloudflareDurableRuntimeOptions["bindings"],
273
- context: CloudflareBindingSourceContext,
274
- ): Effect.Effect<ReadonlyArray<ResolvedBinding>> =>
275
- source === undefined ? Effect.succeed([]) : Effect.suspend(() => source(context));
276
-
277
- const resolveRunContext = (
278
- source: CloudflareRunContextSource,
279
- context: CloudflareRuntimeSourceContext,
280
- ): CloudflareRunContextLayer => (typeof source === "function" ? source(context) : source);
281
-
282
246
  /**
283
- * The DC Layer assembly (deployment §12: a Layer-assembly library, not an app entrypoint;
284
- * plan §1.4). `layer(options)` decodes the configuration, derives this Object's Conversation
285
- * and producer identities, opens the Object's private SQLite database through
286
- * `@effect/sql-sqlite-do` for BOTH the Conversation Log and the Submission Ledger (so claims
287
- * fence the same producer epochs — ADR-0011 D7 transposed), wraps the local port facets with
288
- * the WP2 cross-Object routing decorators over the Durable Object RPC transport, wires the
289
- * alarm-backed wake scheduler and the maintenance pass, defaults reconciliation to the
290
- * fail-closed `ToolReconciler.uncertain`, and provides a ready `DurableAgentRuntime` on top.
291
- *
292
- * Storage compatibility is verified during construction: an incompatible database fails the
293
- * Layer typed (`DoStorageCompatibilityError`) before anything is mutated (DEPLOY-008).
294
- *
295
- * Requires only the two binding services (`DurableObjectContext`,
296
- * `ConversationObjectNamespace`) — platform values enter exclusively through Layers
297
- * (DEPLOY-010).
247
+ * Validate deployment settings and derive services before building application dependencies.
248
+ * The native class factory builds this Layer inside its constructor gate. Custom Effect hosts
249
+ * can provide it around the complete application Layer with the native context already supplied.
298
250
  */
299
- export class CloudflareDurableRuntime {
300
- static layer(
301
- options: CloudflareDurableRuntimeOptions,
302
- ): Layer.Layer<
303
- CloudflareDurableRuntimeServices,
304
- CloudflareDurableRuntimeInitializationError,
305
- DurableObjectContext | ConversationObjectNamespace
306
- > {
307
- return Layer.unwrap(
308
- Effect.gen(function* () {
309
- const { ctx, env } = yield* DurableObjectContext;
310
- const config = yield* configFromOptions(options);
311
- const conversationId = yield* conversationIdFromState(ctx);
312
- const producerId = yield* decodeProducerId(
313
- `${config.producerPrefix}:${conversationId}`,
314
- ).pipe(
315
- Effect.mapError((error) =>
316
- CloudflarePlatformConfigError.make({
317
- message: `The minted producer identity is invalid: ${error.message}`,
318
- cause: error,
319
- }),
320
- ),
321
- );
322
-
323
- const identityLayer = Layer.succeed(ConversationObjectIdentity)({
324
- conversationId,
325
- producerId,
326
- });
327
- const cloudflareConfigLayer = Layer.succeed(CloudflareDurableRuntimeConfig)(config);
328
-
329
- const storageOptions: DoStorageOptions = {
330
- storage: ctx.storage,
331
- observationPollInterval: config.observationPollInterval,
332
- ownershipLeaseDuration: config.ownershipLeaseDuration,
333
- maxStoredValueBytes: config.maxStoredValueBytes,
334
- verifyOnOpen: config.verifyOnOpen,
335
- failpoint: options.storageFailpoint?.(ctx),
336
- };
337
- const infrastructure = Layer.mergeAll(
338
- storageConfigLayer(storageOptions),
339
- storageFailpointLayer(storageOptions),
340
- SqliteClient.layer({ storage: ctx.storage }),
341
- BrowserCrypto.layer,
342
- );
343
-
344
- /**
345
- * ONE local-facet instance (a shared Layer value) serves both the routed decorators
346
- * and the owner-side `portCall` executor — the executor must never see the routed
347
- * ports (plan §1.3: re-routing could bounce a request between Objects).
348
- */
349
- const localPorts = Layer.mergeAll(conversationStoreLayer, submissionLedgerLayer).pipe(
350
- Layer.provide(infrastructure),
351
- );
352
-
353
- const portsEndpointLayer = Layer.effect(ConversationObjectPorts)(
354
- Effect.gen(function* () {
355
- const local = yield* Effect.context<SubmissionLedger | ConversationStore>();
356
- return ConversationObjectPorts.of({
357
- handle: (request) => executePortRequest(request).pipe(Effect.provide(local)),
358
- });
359
- }),
360
- ).pipe(Layer.provide(localPorts));
361
-
362
- const routedPorts = Layer.mergeAll(
363
- routedSubmissionLedgerLayer({ localConversationId: conversationId }),
364
- routedConversationStoreLayer({ localConversationId: conversationId }),
365
- ).pipe(Layer.provide(localPorts), Layer.provide(conversationPortTransportLayer));
366
-
367
- const runtimeConfigLayer = Layer.succeed(DurableRuntimeConfig)(
368
- DurableRuntimeConfig.make({
369
- deploymentId: config.deploymentId,
370
- producerId,
371
- settlementPollInterval: Duration.millis(config.settlementPollInterval),
372
- leaseRenewalInterval: Duration.millis(config.leaseRenewalInterval),
373
- abortPollInterval: Duration.millis(config.abortPollInterval),
374
- ...(options.estimateCostMicrousd === undefined
375
- ? {}
376
- : { estimateCostMicrousd: options.estimateCostMicrousd }),
251
+ export const layerConfig = (
252
+ options: CloudflareDurableRuntimeOptions,
253
+ ): Layer.Layer<CloudflareBootstrapServices, CloudflarePlatformConfigError, DurableObjectContext> =>
254
+ Layer.unwrap(
255
+ Effect.gen(function* () {
256
+ const { ctx } = yield* DurableObjectContext;
257
+ const config = yield* configFromOptions(options);
258
+ const threadId = yield* threadIdFromState(ctx);
259
+ const producerId = yield* decodeProducerId(`${config.producerPrefix}:${threadId}`).pipe(
260
+ Effect.mapError((error) =>
261
+ CloudflarePlatformConfigError.make({
262
+ message: `The minted producer identity is invalid: ${error.message}`,
263
+ cause: error,
377
264
  }),
378
- );
379
-
380
- const runtimeFailpointLayer =
381
- options.runtimeFailpoint === undefined
382
- ? DurableRuntimeFailpoint.layer
383
- : Layer.succeed(DurableRuntimeFailpoint)({ hit: options.runtimeFailpoint(ctx) });
384
- const maintenanceFailpointLayer =
385
- options.maintenanceFailpoint === undefined
386
- ? ConversationMaintenanceFailpoint.layer
387
- : Layer.succeed(ConversationMaintenanceFailpoint)({
388
- hit: options.maintenanceFailpoint(ctx),
389
- });
390
- const reconcilerLayer = options.toolReconciler ?? ToolReconciler.uncertain;
391
- const authorizerLayer =
392
- options.operationAuthorizer === undefined
393
- ? Layer.empty
394
- : operationAuthorizerLayer(options.operationAuthorizer);
395
- const observerLayer =
396
- options.toolFailureObserver === undefined
397
- ? Layer.succeed(CurrentToolFailureObserver)(undefined)
398
- : toolFailureObserverLayer(options.toolFailureObserver);
399
- const bindingResolverLayer = Layer.effect(AgentBindingResolver)(
400
- Effect.map(
401
- resolveBindings(options.bindings, { ctx, env, conversationId, producerId }),
402
- (bindings) => AgentBindingResolver.fromBindings(bindings),
403
- ),
404
- );
405
- const runContextLayer =
406
- options.runContext === undefined
407
- ? RunContextPreparationPassthrough
408
- : resolveRunContext(options.runContext, { ctx, env, conversationId, producerId }).pipe(
409
- Layer.provide(BrowserCrypto.layer),
410
- );
265
+ ),
266
+ );
267
+ return Layer.mergeAll(
268
+ Layer.succeed(CloudflareDurableRuntimeConfig, config),
269
+ Layer.succeed(ThreadObjectIdentity, { threadId, producerId }),
270
+ DurableRuntimeConfig.layer({
271
+ deploymentId: config.deploymentId,
272
+ producerId,
273
+ settlementPollInterval: Duration.millis(config.settlementPollInterval),
274
+ leaseRenewalInterval: Duration.millis(config.leaseRenewalInterval),
275
+ abortPollInterval: Duration.millis(config.abortPollInterval),
276
+ ...(options.estimateCostMicrousd === undefined
277
+ ? {}
278
+ : { estimateCostMicrousd: options.estimateCostMicrousd }),
279
+ }),
280
+ BrowserCrypto.layer,
281
+ storageFailpointLayer({ storage: ctx.storage, failpoint: options.storageFailpoint?.(ctx) }),
282
+ options.runtimeFailpoint === undefined
283
+ ? DurableRuntimeFailpoint.layer
284
+ : Layer.succeed(DurableRuntimeFailpoint, { hit: options.runtimeFailpoint(ctx) }),
285
+ options.maintenanceFailpoint === undefined
286
+ ? ThreadMaintenanceFailpoint.layer
287
+ : Layer.succeed(ThreadMaintenanceFailpoint, {
288
+ hit: options.maintenanceFailpoint(ctx),
289
+ }),
290
+ options.toolReconciler ?? ToolReconciler.uncertain,
291
+ options.operationAuthorizer === undefined
292
+ ? Layer.empty
293
+ : operationAuthorizerLayer(options.operationAuthorizer),
294
+ options.toolFailureObserver === undefined
295
+ ? Layer.succeed(CurrentToolFailureObserver, undefined)
296
+ : toolFailureObserverLayer(options.toolFailureObserver),
297
+ RunContextPreparationPassthrough,
298
+ RunToolAuthorization.allowAll,
299
+ );
300
+ }),
301
+ );
411
302
 
412
- const base = Layer.mergeAll(
413
- identityLayer,
414
- cloudflareConfigLayer,
415
- DurableAlarmService.layer,
416
- maintenanceFailpointLayer,
417
- ProgressWaitRegistry.layer,
418
- );
303
+ /**
304
+ * Register typed Agents and version declarations. Hashing and dependency capture happen in
305
+ * this Layer's Scope, after application Layers have been provided. Every Agent's instruction,
306
+ * Tool, Schema, and model requirements remain visible until satisfied by Layer composition.
307
+ * Use Layer.unwrap for registration values that need effectful application setup.
308
+ */
309
+ export const layer = <const Entries extends ReadonlyArray<AgentRegistration>>(
310
+ registrations: Entries,
311
+ ) => Layer.unwrap(Effect.map(compileRegistrations(registrations), layerFromBindings));
419
312
 
420
- const runtimeStack = DurableAgentRuntime.layerWithContext.pipe(
421
- Layer.provideMerge(routedPorts),
422
- Layer.provideMerge(cloudflareWakeSchedulerLayer),
423
- Layer.provideMerge(runtimeConfigLayer),
424
- Layer.provideMerge(bindingResolverLayer),
425
- Layer.provide(
426
- Layer.mergeAll(
427
- runtimeFailpointLayer,
428
- reconcilerLayer,
429
- authorizerLayer,
430
- observerLayer,
431
- runContextLayer,
432
- BrowserCrypto.layer,
433
- ),
434
- ),
435
- Layer.provideMerge(base),
436
- );
313
+ /** Internal assembly shared by registration compilation and low-level adapter fixtures. */
314
+ export const layerFromBindings = (
315
+ bindings: ReadonlyArray<ResolvedBinding>,
316
+ ): Layer.Layer<
317
+ CloudflareDurableRuntimeServices,
318
+ DoStorageInitializationError,
319
+ DurableObjectContext | ThreadObjectNamespace | CloudflareBootstrapServices
320
+ > =>
321
+ Layer.unwrap(
322
+ Effect.gen(function* () {
323
+ const { ctx } = yield* DurableObjectContext;
324
+ const config = yield* CloudflareDurableRuntimeConfig;
325
+ const { threadId } = yield* ThreadObjectIdentity;
326
+ const storageOptions: DoStorageOptions = {
327
+ storage: ctx.storage,
328
+ observationPollInterval: config.observationPollInterval,
329
+ ownershipLeaseDuration: config.ownershipLeaseDuration,
330
+ maxStoredValueBytes: config.maxStoredValueBytes,
331
+ verifyOnOpen: config.verifyOnOpen,
332
+ };
333
+ const infrastructure = Layer.mergeAll(
334
+ storageConfigLayer(storageOptions),
335
+ SqliteClient.layer({ storage: ctx.storage }),
336
+ );
437
337
 
438
- return Layer.mergeAll(
439
- runtimeStack,
440
- ConversationMaintenance.layer.pipe(Layer.provide(runtimeStack)),
441
- portsEndpointLayer,
442
- );
443
- }),
444
- );
445
- }
446
- }
338
+ // The same local ports serve routed decorators and owner-side RPC execution.
339
+ // The RPC executor must never receive routed ports and bounce requests between Objects.
340
+ const localPorts = Layer.mergeAll(threadStoreLayer, submissionLedgerLayer).pipe(
341
+ Layer.provide(infrastructure),
342
+ );
343
+ const portsEndpointLayer = Layer.effect(ThreadObjectPorts)(
344
+ Effect.gen(function* () {
345
+ const local = yield* Effect.context<SubmissionLedger | ThreadStore>();
346
+ return ThreadObjectPorts.of({
347
+ handle: (request) => executePortRequest(request).pipe(Effect.provide(local)),
348
+ });
349
+ }),
350
+ ).pipe(Layer.provide(localPorts));
351
+ const routedPorts = Layer.mergeAll(
352
+ routedSubmissionLedgerLayer({ localThreadId: threadId }),
353
+ routedThreadStoreLayer({ localThreadId: threadId }),
354
+ ).pipe(Layer.provide(localPorts), Layer.provide(threadPortTransportLayer));
355
+ const base = Layer.mergeAll(DurableAlarmService.layer, ProgressWaitRegistry.layer);
356
+ const runtimeStack = DurableAgentRuntime.layerWithServices.pipe(
357
+ Layer.provideMerge(routedPorts),
358
+ Layer.provideMerge(cloudflareWakeSchedulerLayer),
359
+ Layer.provideMerge(base),
360
+ );
361
+ return Layer.mergeAll(
362
+ runtimeStack,
363
+ ThreadMaintenance.layer(bindings).pipe(Layer.provide(runtimeStack)),
364
+ portsEndpointLayer,
365
+ );
366
+ }),
367
+ );
@@ -0,0 +1,90 @@
1
+ import type { AgentId } from "@effect-agent/core";
2
+ import {
3
+ type DurableSubmitAgent,
4
+ PersistedJson,
5
+ type PreparedInput,
6
+ PreparedInputAdmission,
7
+ type ScheduledEnvelope,
8
+ ScheduledInputAdmission,
9
+ ScheduledInputRetryable,
10
+ ScheduleStorageError,
11
+ } from "@effect-agent/thread";
12
+ import { Effect, Layer } from "effect";
13
+
14
+ import { CloudflareThreadClient, type ThreadClientError } from "./client.ts";
15
+
16
+ const passthroughAgent = (agentId: AgentId): DurableSubmitAgent<typeof PersistedJson> => ({
17
+ definition: { id: agentId, input: PersistedJson },
18
+ });
19
+
20
+ /** Ordinary prepared admission through one freshly addressed Thread Object call. */
21
+ export const cloudflarePreparedInputAdmissionLayer: Layer.Layer<
22
+ PreparedInputAdmission,
23
+ never,
24
+ CloudflareThreadClient
25
+ > = Layer.effect(
26
+ PreparedInputAdmission,
27
+ Effect.gen(function* () {
28
+ const client = yield* CloudflareThreadClient;
29
+ return PreparedInputAdmission.of({
30
+ submit: (envelope) =>
31
+ client
32
+ .submit(passthroughAgent(envelope.agentId), envelope.input, {
33
+ threadId: envelope.threadId,
34
+ principal: envelope.deliveryPrincipal,
35
+ idempotencyKey: envelope.admissionKey,
36
+ definitions: envelope.definitions,
37
+ })
38
+ .pipe(
39
+ Effect.catchTags({
40
+ AdmissionConflict: () =>
41
+ ScheduleStorageError.make({ operation: "prepared admission", reason: "corrupt" }),
42
+ AdmissionLimitExceeded: () => ScheduledInputRetryable.make({ reason: "capacity" }),
43
+ ThreadClientError: (error: ThreadClientError) =>
44
+ ScheduledInputRetryable.make({
45
+ reason: error.overloaded === true ? "capacity" : "transport",
46
+ }),
47
+ HostProtocolError: () => ScheduledInputRetryable.make({ reason: "ambiguous" }),
48
+ LedgerError: () => ScheduledInputRetryable.make({ reason: "storage" }),
49
+ ThreadStoreError: () => ScheduledInputRetryable.make({ reason: "storage" }),
50
+ DurableAlarmError: () => ScheduledInputRetryable.make({ reason: "storage" }),
51
+ AgentInputError: () =>
52
+ ScheduleStorageError.make({ operation: "prepared admission", reason: "corrupt" }),
53
+ DigestError: () =>
54
+ ScheduleStorageError.make({ operation: "prepared admission", reason: "corrupt" }),
55
+ ThreadNotMaterialized: () => ScheduledInputRetryable.make({ reason: "storage" }),
56
+ AppendConflict: () => ScheduledInputRetryable.make({ reason: "ambiguous" }),
57
+ FenceRejected: () => ScheduledInputRetryable.make({ reason: "ambiguous" }),
58
+ DurableRuntimeFailpointError: () =>
59
+ ScheduledInputRetryable.make({ reason: "ambiguous" }),
60
+ }),
61
+ ),
62
+ });
63
+ }),
64
+ );
65
+
66
+ const preparedFromSchedule = (envelope: ScheduledEnvelope): PreparedInput => ({
67
+ schemaVersion: 1,
68
+ threadId: envelope.threadId,
69
+ deliveryPrincipal: envelope.deliveryPrincipal,
70
+ agentId: envelope.agentId,
71
+ definitions: envelope.definitions,
72
+ input: envelope.input,
73
+ inputDigest: envelope.inputDigest,
74
+ admissionKey: envelope.admissionKey,
75
+ authorization: envelope.authorization,
76
+ });
77
+
78
+ /** Compatibility adapter retaining Scheduling's public admission port. */
79
+ export const cloudflareScheduledInputAdmissionLayer: Layer.Layer<
80
+ ScheduledInputAdmission,
81
+ never,
82
+ PreparedInputAdmission
83
+ > = Layer.effect(
84
+ ScheduledInputAdmission,
85
+ Effect.map(PreparedInputAdmission, (admission) =>
86
+ ScheduledInputAdmission.of({
87
+ submit: (envelope) => admission.submit(preparedFromSchedule(envelope)),
88
+ }),
89
+ ),
90
+ );