@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.
@@ -1,11 +1,16 @@
1
1
  import type { AgentId } from "@effect-agent/core";
2
2
  import { SubmissionId } from "@effect-agent/core";
3
+ import {
4
+ decodePortRequest,
5
+ encodePortResponse,
6
+ type PortRequest,
7
+ } from "@effect-agent/storage-cloudflare";
3
8
  import {
4
9
  AppendConflict,
5
- ConversationNotMaterialized,
6
- ConversationRead,
7
- ConversationStore,
8
- ConversationStoreError,
10
+ ThreadNotMaterialized,
11
+ ThreadRead,
12
+ ThreadStore,
13
+ ThreadStoreError,
9
14
  DigestError,
10
15
  DurableAgentRuntime,
11
16
  DurableRuntimeFailpointError,
@@ -29,12 +34,7 @@ import {
29
34
  SubmissionLookupByKey,
30
35
  WakeScheduler,
31
36
  type DurableSubmitAgent,
32
- } from "@effect-agent/session";
33
- import {
34
- decodePortRequest,
35
- encodePortResponse,
36
- type PortRequest,
37
- } from "@effect-agent/storage-cloudflare";
37
+ } from "@effect-agent/thread";
38
38
  import { Effect, Layer, Option, Schema, Stream } from "effect";
39
39
  import {
40
40
  DurableObject as EffectCfDurableObject,
@@ -43,16 +43,16 @@ import {
43
43
  } from "effect-cf";
44
44
 
45
45
  import {
46
- ConversationMaintenance,
46
+ ThreadMaintenance,
47
47
  DurableAlarmError,
48
48
  DurableAlarmService,
49
49
  type MaintenancePassFailure,
50
50
  } from "./alarm.ts";
51
51
  import {
52
- ConversationObjectIdentity,
52
+ ThreadObjectIdentity,
53
53
  DurableObjectContext,
54
- ConversationObjectNamespace,
55
- conversationNamespaceFromEnv,
54
+ ThreadObjectNamespace,
55
+ threadNamespaceFromEnv,
56
56
  type CloudflareBindingError,
57
57
  } from "./bindings.ts";
58
58
  import {
@@ -81,21 +81,31 @@ import {
81
81
  } from "./client.ts";
82
82
  import { AdmissionLimitExceeded, CloudflareDurableRuntimeConfig } from "./config.ts";
83
83
  import {
84
- CloudflareDurableRuntime,
85
- ConversationObjectPorts,
84
+ layerConfig,
85
+ ThreadObjectPorts,
86
86
  type CloudflareDurableRuntimeInitializationError,
87
87
  type CloudflareDurableRuntimeOptions,
88
88
  type CloudflareDurableRuntimeServices,
89
+ type CloudflareBootstrapServices,
89
90
  } from "./layers.ts";
90
91
  import { ProgressWaitRegistry } from "./progress-wait.ts";
91
92
 
93
+ export {
94
+ layer,
95
+ layerConfig,
96
+ type CloudflareDurableRuntimeOptions as RuntimeOptions,
97
+ type CloudflareDurableRuntimeServices as Services,
98
+ type CloudflareDurableRuntimeInitializationError as InitializationError,
99
+ type CloudflareBootstrapServices as BootstrapServices,
100
+ } from "./layers.ts";
101
+
92
102
  /**
93
- * `makeConversationObjectClass(options, observability?)` — the Conversation Durable Object
103
+ * `ThreadObject.make(application, options)` — the Thread Durable Object
94
104
  * (plan §1.4,
95
105
  * D-P6-1): a factory returning a class that applications export from their Worker entry.
96
- * One SQLite-backed Object per Conversation is the serialized owner (durability §6); the
106
+ * One SQLite-backed Object per Thread is the serialized owner (durability §6); the
97
107
  * Object never runs `runResolvedWorker`'s infinite loop — each ingress event or alarm runs
98
- * ONE bounded `runRecovery` + `processConversationResolved` pass, and the persisted alarm
108
+ * ONE bounded `runRecovery` + `processThreadResolved` pass, and the persisted alarm
99
109
  * (the single multiplexed slot, D-P6-2) finishes accepted work across evictions WITHOUT any
100
110
  * incoming request.
101
111
  *
@@ -107,21 +117,37 @@ import { ProgressWaitRegistry } from "./progress-wait.ts";
107
117
  * `runRecovery` BEFORE any claim, so reconciliation still strictly precedes new work.
108
118
  */
109
119
 
110
- /** Construction options for one deployed Conversation Object class. */
111
- export interface ConversationObjectOptions extends CloudflareDurableRuntimeOptions {
120
+ /** Construction options for one deployed Thread Object class. */
121
+ export interface Options<
122
+ ApplicationServices = never,
123
+ EventServices = never,
124
+ EventLayerError = never,
125
+ > extends CloudflareDurableRuntimeOptions {
112
126
  /** Accept transient native RPC tracing through effect-cf; disabled by default. */
113
127
  readonly rpcTracing?: boolean;
114
128
  /**
115
129
  * Name of the Worker `env` binding carrying THIS class's `DurableObjectNamespace` — the
116
- * Object's route back to sibling Conversation Objects for the WP2 cross-Object port calls
130
+ * Object's route back to sibling Thread Objects for the WP2 cross-Object port calls
117
131
  * and remote wakes (DEPLOY-010: the binding enters through a Layer, never ambiently).
118
132
  */
119
133
  readonly namespaceBinding: string;
134
+ /** Acquired and finalized per native event, with access to the complete application runtime. */
135
+ readonly eventLayer?: Layer.Layer<
136
+ EventServices,
137
+ EventLayerError,
138
+ | RuntimeServices
139
+ | ApplicationServices
140
+ | EffectCfDurableObjectState.DurableObjectState
141
+ | WorkerEnvironment
142
+ >;
120
143
  }
121
144
 
122
- type EndpointServices = CloudflareDurableRuntimeServices | DurableObjectContext;
123
- type RuntimeServices = EndpointServices | ConversationObjectNamespace;
124
- type ConversationObjectInitializationError =
145
+ type EndpointServices =
146
+ | CloudflareDurableRuntimeServices
147
+ | CloudflareBootstrapServices
148
+ | DurableObjectContext;
149
+ type RuntimeServices = EndpointServices | ThreadObjectNamespace;
150
+ type ThreadObjectInitializationError =
125
151
  | CloudflareDurableRuntimeInitializationError
126
152
  | CloudflareBindingError
127
153
  | MaintenancePassFailure;
@@ -190,55 +216,53 @@ const utf8Bytes = (value: PersistedJson): number =>
190
216
  * exempt: its accepted-work obligation already exists, and returning the original Receipt
191
217
  * consumes no new quota. Refusals are typed `AdmissionLimitExceeded` and nothing is written.
192
218
  */
193
- const gateAdmissionLimits = Effect.fn("ConversationObject.gateAdmissionLimits")(
194
- function* (request: {
195
- readonly principal: SubmissionLookupByKey["principal"];
196
- readonly idempotencyKey: SubmissionLookupByKey["idempotencyKey"];
197
- readonly inputPayload: PersistedJson;
198
- }) {
199
- const identity = yield* ConversationObjectIdentity;
200
- const config = yield* CloudflareDurableRuntimeConfig;
201
- const ledger = yield* SubmissionLedger;
202
- const { ctx } = yield* DurableObjectContext;
203
-
204
- const existing = yield* ledger.lookup(
205
- SubmissionLookupByKey.make({
206
- conversationId: identity.conversationId,
207
- principal: request.principal,
208
- idempotencyKey: request.idempotencyKey,
209
- }),
210
- );
211
- if (Option.isSome(existing)) return;
212
-
213
- const inputBytes = utf8Bytes(request.inputPayload);
214
- if (inputBytes > config.limits.maxInputBytes) {
215
- return yield* AdmissionLimitExceeded.make({
216
- limit: "input-bytes",
217
- actual: inputBytes,
218
- maximum: config.limits.maxInputBytes,
219
- });
220
- }
219
+ const gateAdmissionLimits = Effect.fn("ThreadObject.gateAdmissionLimits")(function* (request: {
220
+ readonly principal: SubmissionLookupByKey["principal"];
221
+ readonly idempotencyKey: SubmissionLookupByKey["idempotencyKey"];
222
+ readonly inputPayload: PersistedJson;
223
+ }) {
224
+ const identity = yield* ThreadObjectIdentity;
225
+ const config = yield* CloudflareDurableRuntimeConfig;
226
+ const ledger = yield* SubmissionLedger;
227
+ const { ctx } = yield* DurableObjectContext;
228
+
229
+ const existing = yield* ledger.lookup(
230
+ SubmissionLookupByKey.make({
231
+ threadId: identity.threadId,
232
+ principal: request.principal,
233
+ idempotencyKey: request.idempotencyKey,
234
+ }),
235
+ );
236
+ if (Option.isSome(existing)) return;
237
+
238
+ const inputBytes = utf8Bytes(request.inputPayload);
239
+ if (inputBytes > config.limits.maxInputBytes) {
240
+ return yield* AdmissionLimitExceeded.make({
241
+ limit: "input-bytes",
242
+ actual: inputBytes,
243
+ maximum: config.limits.maxInputBytes,
244
+ });
245
+ }
221
246
 
222
- // One Conversation per Object (durability §5): the local scan IS this lane's queue.
223
- const nonterminal = yield* Stream.runCollect(ledger.scanNonterminal);
224
- if (nonterminal.length >= config.limits.maxQueueDepthPerLane) {
225
- return yield* AdmissionLimitExceeded.make({
226
- limit: "queue-depth",
227
- actual: nonterminal.length,
228
- maximum: config.limits.maxQueueDepthPerLane,
229
- });
230
- }
247
+ // One Thread per Object (durability §5): the local scan IS this lane's queue.
248
+ const nonterminal = yield* Stream.runCollect(ledger.scanNonterminal);
249
+ if (nonterminal.length >= config.limits.maxQueueDepthPerLane) {
250
+ return yield* AdmissionLimitExceeded.make({
251
+ limit: "queue-depth",
252
+ actual: nonterminal.length,
253
+ maximum: config.limits.maxQueueDepthPerLane,
254
+ });
255
+ }
231
256
 
232
- const databaseBytes = yield* Effect.sync(() => ctx.storage.sql.databaseSize);
233
- if (databaseBytes > config.limits.maxDatabaseBytes) {
234
- return yield* AdmissionLimitExceeded.make({
235
- limit: "database-bytes",
236
- actual: databaseBytes,
237
- maximum: config.limits.maxDatabaseBytes,
238
- });
239
- }
240
- },
241
- );
257
+ const databaseBytes = yield* Effect.sync(() => ctx.storage.sql.databaseSize);
258
+ if (databaseBytes > config.limits.maxDatabaseBytes) {
259
+ return yield* AdmissionLimitExceeded.make({
260
+ limit: "database-bytes",
261
+ actual: databaseBytes,
262
+ maximum: config.limits.maxDatabaseBytes,
263
+ });
264
+ }
265
+ });
242
266
 
243
267
  /**
244
268
  * The submit-capable projection of an Agent Binding on the OBJECT side: the input arrived
@@ -258,15 +282,15 @@ const submitEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoin
258
282
  Effect.mapError(protocolFailure("The submit request could not be decoded")),
259
283
  Effect.flatMap((request) =>
260
284
  Effect.gen(function* () {
261
- const identity = yield* ConversationObjectIdentity;
262
- const maintenance = yield* ConversationMaintenance;
285
+ const identity = yield* ThreadObjectIdentity;
286
+ const maintenance = yield* ThreadMaintenance;
263
287
  const runtime = yield* DurableAgentRuntime;
264
288
  yield* gateAdmissionLimits(request);
265
289
  // Alarm invariant: the generation + alarm commit BEFORE the admission, and maintenance
266
290
  // cannot acknowledge that generation until this mutation leaves its public RPC seam.
267
291
  const receipt = yield* maintenance.withMutation(
268
292
  runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
269
- conversationId: identity.conversationId,
293
+ threadId: identity.threadId,
270
294
  principal: request.principal,
271
295
  idempotencyKey: request.idempotencyKey,
272
296
  definitions: request.definitions,
@@ -300,14 +324,14 @@ const awaitProgressEndpoint = (encoded: unknown): Effect.Effect<unknown, never,
300
324
  Effect.mapError(protocolFailure("The progress request could not be decoded")),
301
325
  Effect.flatMap((request) =>
302
326
  Effect.gen(function* () {
303
- const identity = yield* ConversationObjectIdentity;
327
+ const identity = yield* ThreadObjectIdentity;
304
328
  const runtime = yield* DurableAgentRuntime;
305
329
  const registry = yield* ProgressWaitRegistry;
306
330
  yield* Effect.scoped(
307
331
  Effect.gen(function* () {
308
332
  const cancelled = yield* registry.subscribe(request.waiterId);
309
333
  yield* Effect.raceFirst(
310
- runtime.awaitProgress(identity.conversationId, request.afterSequence),
334
+ runtime.awaitProgress(identity.threadId, request.afterSequence),
311
335
  cancelled,
312
336
  );
313
337
  }),
@@ -340,21 +364,21 @@ const observePageEndpoint = (encoded: unknown): Effect.Effect<unknown, never, En
340
364
  Effect.mapError(protocolFailure("The observe request could not be decoded")),
341
365
  Effect.flatMap((request) =>
342
366
  Effect.gen(function* () {
343
- const identity = yield* ConversationObjectIdentity;
344
- const store = yield* ConversationStore;
367
+ const identity = yield* ThreadObjectIdentity;
368
+ const store = yield* ThreadStore;
345
369
  // The same fail-closed authorization seam the runtime's `observe` consults (P7 WP1);
346
370
  // the default reference preserves the possession behavior.
347
371
  const authorizer = yield* OperationAuthorizer;
348
372
  yield* authorizer.authorize(
349
373
  OperationAuthorizationRequest.make({
350
374
  operation: "observe",
351
- conversationId: identity.conversationId,
375
+ threadId: identity.threadId,
352
376
  }),
353
377
  );
354
378
  const records = yield* Stream.runCollect(
355
379
  store.read(
356
- ConversationRead.make({
357
- conversationId: identity.conversationId,
380
+ ThreadRead.make({
381
+ threadId: identity.threadId,
358
382
  ...(request.afterSequence === undefined
359
383
  ? {}
360
384
  : { afterSequence: request.afterSequence }),
@@ -374,7 +398,7 @@ const abortEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoint
374
398
  Effect.mapError(protocolFailure("The abort command could not be decoded")),
375
399
  Effect.flatMap((command) =>
376
400
  Effect.gen(function* () {
377
- const maintenance = yield* ConversationMaintenance;
401
+ const maintenance = yield* ThreadMaintenance;
378
402
  const runtime = yield* DurableAgentRuntime;
379
403
  const intent = yield* maintenance.withMutation(runtime.abort(command));
380
404
  return AbortRecorded.make({ intent });
@@ -391,7 +415,7 @@ const resolveApprovalEndpoint = (
391
415
  Effect.mapError(protocolFailure("The approval command could not be decoded")),
392
416
  Effect.flatMap((command) =>
393
417
  Effect.gen(function* () {
394
- const maintenance = yield* ConversationMaintenance;
418
+ const maintenance = yield* ThreadMaintenance;
395
419
  const runtime = yield* DurableAgentRuntime;
396
420
  const intent = yield* maintenance.withMutation(runtime.resolveApproval(command));
397
421
  return ApprovalRecorded.make({ intent });
@@ -408,7 +432,7 @@ const resolveUnknownEndpoint = (
408
432
  Effect.mapError(protocolFailure("The resolution command could not be decoded")),
409
433
  Effect.flatMap((command) =>
410
434
  Effect.gen(function* () {
411
- const maintenance = yield* ConversationMaintenance;
435
+ const maintenance = yield* ThreadMaintenance;
412
436
  const runtime = yield* DurableAgentRuntime;
413
437
  const intent = yield* maintenance.withMutation(runtime.resolveUnknown(command));
414
438
  return UnknownResolutionRecorded.make({ intent });
@@ -447,8 +471,8 @@ export const AdminFailure = Schema.Union([
447
471
  DigestError,
448
472
  OwnershipLost,
449
473
  SettlementConflict,
450
- ConversationStoreError,
451
- ConversationNotMaterialized,
474
+ ThreadStoreError,
475
+ ThreadNotMaterialized,
452
476
  AppendConflict,
453
477
  FenceRejected,
454
478
  DurableRuntimeFailpointError,
@@ -533,11 +557,11 @@ const explainEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoi
533
557
  Effect.mapError(protocolFailure("The explain request could not be decoded")),
534
558
  Effect.flatMap((request) =>
535
559
  Effect.gen(function* () {
536
- const identity = yield* ConversationObjectIdentity;
560
+ const identity = yield* ThreadObjectIdentity;
537
561
  const runtime = yield* DurableAgentRuntime;
538
562
  const explanations =
539
563
  request.submissionId === undefined
540
- ? yield* runtime.explainConversation(identity.conversationId)
564
+ ? yield* runtime.explainThread(identity.threadId)
541
565
  : [yield* runtime.explain(request.submissionId)];
542
566
  return ExplainedRecovery.make({ explanations });
543
567
  }),
@@ -551,9 +575,9 @@ const verifyEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoin
551
575
  Effect.mapError(protocolFailure("The verify request could not be decoded")),
552
576
  Effect.flatMap(() =>
553
577
  Effect.gen(function* () {
554
- const identity = yield* ConversationObjectIdentity;
578
+ const identity = yield* ThreadObjectIdentity;
555
579
  const runtime = yield* DurableAgentRuntime;
556
- const report = yield* runtime.verify(identity.conversationId);
580
+ const report = yield* runtime.verify(identity.threadId);
557
581
  return VerifiedIntegrity.make({ report });
558
582
  }),
559
583
  ),
@@ -566,7 +590,7 @@ const retryEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoint
566
590
  Effect.mapError(protocolFailure("The retry command could not be decoded")),
567
591
  Effect.flatMap((command) =>
568
592
  Effect.gen(function* () {
569
- const maintenance = yield* ConversationMaintenance;
593
+ const maintenance = yield* ThreadMaintenance;
570
594
  const runtime = yield* DurableAgentRuntime;
571
595
  // Retry may repair durable state, so its generation + alarm commit before the mutation.
572
596
  const report = yield* maintenance.withMutation(runtime.retry(command));
@@ -600,8 +624,8 @@ const obligationsEndpoint = (encoded: unknown): Effect.Effect<unknown, never, En
600
624
  */
601
625
  const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
602
626
  Effect.gen(function* () {
603
- const ports = yield* ConversationObjectPorts;
604
- const maintenance = yield* ConversationMaintenance;
627
+ const ports = yield* ThreadObjectPorts;
628
+ const maintenance = yield* ThreadMaintenance;
605
629
  const alarm = yield* DurableAlarmService;
606
630
  const decoded = yield* decodePortRequest(encoded).pipe(
607
631
  Effect.map((request) => ({ _tag: "success" as const, request })),
@@ -636,7 +660,7 @@ const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpo
636
660
  // Prompt processing hint; the pre-armed alarm already guarantees convergence.
637
661
  yield* alarm.scheduleNow.pipe(
638
662
  Effect.catch((error) =>
639
- Effect.logWarning("ConversationObject.portCall: immediate re-arm failed", error),
663
+ Effect.logWarning("ThreadObject.portCall: immediate re-arm failed", error),
640
664
  ),
641
665
  );
642
666
  }
@@ -644,16 +668,16 @@ const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpo
644
668
  });
645
669
 
646
670
  const wakeEndpoint: Effect.Effect<void, never, EndpointServices> = Effect.gen(function* () {
647
- const identity = yield* ConversationObjectIdentity;
671
+ const identity = yield* ThreadObjectIdentity;
648
672
  const wake = yield* WakeScheduler;
649
673
  // Route the remote hint through this incarnation's scheduler so scoped progress waiters and
650
674
  // the alarm receive the same hint. Delivery remains droppable; canonical storage is authority.
651
- yield* wake.notify(identity.conversationId);
675
+ yield* wake.notify(identity.threadId);
652
676
  });
653
677
 
654
678
  const alarmEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointServices> = Effect.gen(
655
679
  function* () {
656
- const maintenance = yield* ConversationMaintenance;
680
+ const maintenance = yield* ThreadMaintenance;
657
681
  // Typed pass failures propagate: the rejected promise makes workerd retry the alarm
658
682
  // (at-least-once delivery), and the dirty generation retains a committed slot meanwhile.
659
683
  yield* maintenance.pass;
@@ -662,10 +686,10 @@ const alarmEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointService
662
686
 
663
687
  const gateEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointServices> = Effect.gen(
664
688
  function* () {
665
- // Forcing ConversationMaintenance forces the whole Layer stack: migration + exact-version
689
+ // Forcing ThreadMaintenance forces the whole Layer stack: migration + exact-version
666
690
  // check + configuration decode (DEPLOY-008 fails typed here, before any mutation), then
667
691
  // the defensive local ensure-alarm half of the invariant. LOCAL-ONLY by construction.
668
- const maintenance = yield* ConversationMaintenance;
692
+ const maintenance = yield* ThreadMaintenance;
669
693
  yield* maintenance.ensureAlarm;
670
694
  },
671
695
  );
@@ -679,7 +703,7 @@ const effectCfPlatformLayer = (
679
703
  namespaceBinding: string,
680
704
  rpcTracing = false,
681
705
  ): Layer.Layer<
682
- DurableObjectContext | ConversationObjectNamespace,
706
+ DurableObjectContext | ThreadObjectNamespace,
683
707
  CloudflareBindingError,
684
708
  EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
685
709
  > => {
@@ -690,11 +714,11 @@ const effectCfPlatformLayer = (
690
714
  return DurableObjectContext.of({ ctx: state.raw, env });
691
715
  }),
692
716
  );
693
- const namespace = Layer.effect(ConversationObjectNamespace)(
717
+ const namespace = Layer.effect(ThreadObjectNamespace)(
694
718
  Effect.gen(function* () {
695
719
  const env = yield* WorkerEnvironment;
696
- const binding = yield* conversationNamespaceFromEnv(env, namespaceBinding);
697
- return ConversationObjectNamespace.of({
720
+ const binding = yield* threadNamespaceFromEnv(env, namespaceBinding);
721
+ return ThreadObjectNamespace.of({
698
722
  namespace: binding,
699
723
  ...(rpcTracing === true ? { rpcTracing: namespaceBinding } : {}),
700
724
  });
@@ -703,8 +727,8 @@ const effectCfPlatformLayer = (
703
727
  return Layer.merge(context, namespace);
704
728
  };
705
729
 
706
- /** The public endpoints and effect-cf invocation hook of one Conversation Object instance. */
707
- export interface ConversationObjectInstance<EventServices = never> extends InstanceType<
730
+ /** The public endpoints and effect-cf invocation hook of one Thread Object instance. */
731
+ export interface Instance<EventServices = never> extends InstanceType<
708
732
  EffectCfDurableObject.DurableObjectClass<Record<never, never>, RuntimeServices | EventServices>
709
733
  > {
710
734
  submitEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
@@ -724,34 +748,39 @@ export interface ConversationObjectInstance<EventServices = never> extends Insta
724
748
  alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void;
725
749
  }
726
750
 
727
- /** The constructor shape workerd instantiates for each Conversation Object. */
728
- export interface ConversationObjectClass<EventServices = never> {
729
- new (ctx: DurableObjectState, env: Cloudflare.Env): ConversationObjectInstance<EventServices>;
751
+ /** The constructor shape workerd instantiates for each Thread Object. */
752
+ export interface Class<EventServices = never> {
753
+ new (ctx: DurableObjectState, env: Cloudflare.Env): Instance<EventServices>;
730
754
  }
731
755
 
732
756
  /**
733
- * Build the application's Conversation Object class (export it from the Worker entry).
734
- * effect-cf owns the cached ManagedRuntime, native RPC methods, event scopes, and post-handler
735
- * OTLP flush scheduling for RPC and alarm events. The optional outer Layer is built per native
736
- * event, so a host can install Tracer/Logger/Metric services and `OtlpExporter.Flusher` without
737
- * Effect Agent owning exporter lifecycle machinery.
757
+ * Export a composed application Layer as a native Durable Object class.
758
+ * Bootstrap services are provided to the whole graph before it acquires, so application Layers
759
+ * can yield effect-cf's WorkerEnvironment and DurableObjectState, derived identity, and Crypto.
760
+ * Application dependencies remain visible until Layer.provide satisfies them. effect-cf owns the
761
+ * cached ManagedRuntime, native RPC methods, event scopes, and telemetry flushing.
762
+ * Initialization is local and bounded inside the constructor gate. Cloudflare eviction does not
763
+ * guarantee finalizers; put resources requiring timely release in scoped operations or eventLayer.
738
764
  */
739
- export const makeConversationObjectClass = <EventLayerError = never, EventServices = never>(
740
- options: ConversationObjectOptions,
741
- observability?: Layer.Layer<
742
- EventServices,
743
- EventLayerError,
744
- | DurableObjectContext
745
- | ConversationObjectNamespace
765
+ export const make = <
766
+ ApplicationServices,
767
+ ApplicationError,
768
+ EventServices = never,
769
+ EventLayerError = never,
770
+ >(
771
+ applicationLayer: Layer.Layer<
772
+ CloudflareDurableRuntimeServices | ApplicationServices,
773
+ ApplicationError,
774
+ | CloudflareBootstrapServices
746
775
  | EffectCfDurableObjectState.DurableObjectState
747
776
  | WorkerEnvironment
777
+ | DurableObjectContext
778
+ | ThreadObjectNamespace
748
779
  >,
749
- ): ConversationObjectClass<EventServices> => {
750
- const application: Layer.Layer<
751
- RuntimeServices,
752
- CloudflareDurableRuntimeInitializationError | CloudflareBindingError,
753
- EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
754
- > = CloudflareDurableRuntime.layer(options).pipe(
780
+ options: Options<ApplicationServices, EventServices, EventLayerError>,
781
+ ): Class<ApplicationServices | EventServices> => {
782
+ const application = applicationLayer.pipe(
783
+ Layer.provideMerge(layerConfig(options)),
755
784
  Layer.provideMerge(effectCfPlatformLayer(options.namespaceBinding, options.rpcTracing)),
756
785
  );
757
786
 
@@ -759,8 +788,8 @@ export const makeConversationObjectClass = <EventLayerError = never, EventServic
759
788
  // the ManagedRuntime, while this effectContext ensures its first Layer build enters the gate
760
789
  // before migration, compatibility checks, or alarm inspection touch Object storage.
761
790
  const runtime: Layer.Layer<
762
- RuntimeServices,
763
- ConversationObjectInitializationError,
791
+ RuntimeServices | ApplicationServices,
792
+ ThreadObjectInitializationError | ApplicationError,
764
793
  EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
765
794
  > = Layer.effectContext(
766
795
  Effect.gen(function* () {
@@ -791,17 +820,19 @@ export const makeConversationObjectClass = <EventLayerError = never, EventServic
791
820
  obligationsEncoded: (encoded: unknown) => obligationsEndpoint(encoded),
792
821
  portCall: (encoded: unknown) => portCallEndpoint(encoded),
793
822
  wake: () => wakeEndpoint,
794
- } satisfies EffectCfDurableObject.DurableObjectRpc<RuntimeServices | EventServices>;
823
+ } satisfies EffectCfDurableObject.DurableObjectRpc<
824
+ RuntimeServices | ApplicationServices | EventServices
825
+ >;
795
826
 
796
- const EffectCfConversationObject = EffectCfDurableObject.make<
797
- RuntimeServices,
798
- ConversationObjectInitializationError,
827
+ const EffectCfThreadObject = EffectCfDurableObject.make<
828
+ RuntimeServices | ApplicationServices,
829
+ ThreadObjectInitializationError | ApplicationError,
799
830
  EventServices,
800
831
  EventLayerError,
801
832
  typeof rpc
802
833
  >(runtime, {
803
834
  ...(options.rpcTracing === true ? { rpcTracing: { service: options.namespaceBinding } } : {}),
804
- ...(observability === undefined ? {} : { eventLayer: observability }),
835
+ ...(options.eventLayer === undefined ? {} : { eventLayer: options.eventLayer }),
805
836
  // Force the gated runtime Layer when Cloudflare loads this Object incarnation. Recovery stays
806
837
  // in each bounded pass so cross-Object initialization cannot deadlock.
807
838
  initialize: Effect.void,
@@ -812,11 +843,11 @@ export const makeConversationObjectClass = <EventLayerError = never, EventServic
812
843
  // effect-cf's class type keeps `alarm` optional even when the handler option is present. This
813
844
  // concrete override reflects this factory's stronger contract while delegating execution to
814
845
  // the effect-cf runtime unchanged.
815
- class ConversationObject extends EffectCfConversationObject {
846
+ class ThreadObject extends EffectCfThreadObject {
816
847
  override alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void {
817
848
  return super.alarm?.(alarmInfo);
818
849
  }
819
850
  }
820
851
 
821
- return ConversationObject;
852
+ return ThreadObject;
822
853
  };
package/src/transport.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { ConversationPortTransport, portTransportFailure } from "@effect-agent/storage-cloudflare";
1
+ import { ThreadPortTransport, portTransportFailure } from "@effect-agent/storage-cloudflare";
2
2
  import { Effect, Layer } from "effect";
3
3
 
4
- import { ConversationObjectNamespace } from "./bindings.ts";
4
+ import { ThreadObjectNamespace } from "./bindings.ts";
5
5
 
6
6
  /**
7
- * `ConversationPortTransport` over native Durable Object JS RPC (decision D-P6-3): one
8
- * `portCall(envelope)` on the stub of the Object that owns the addressed Conversation
9
- * (`namespace.idFromName(conversationId)` — the identity rule, plan §1.2). The envelopes are
7
+ * `ThreadPortTransport` over native Durable Object JS RPC (decision D-P6-3): one
8
+ * `portCall(envelope)` on the stub of the Object that owns the addressed Thread
9
+ * (`namespace.idFromName(threadId)` — the identity rule, plan §1.2). The envelopes are
10
10
  * already Schema-encoded JSON, so the RPC boundary carries only structured-cloneable values;
11
11
  * the protocol module stays transport-agnostic and fetch-with-JSON remains the documented
12
12
  * fallback carrier.
@@ -16,21 +16,21 @@ import { ConversationObjectNamespace } from "./bindings.ts";
16
16
  * when present) and NEVER as a fabricated answer: on `resolveAdmission` the routing layer
17
17
  * turns exactly this error into `AdmissionIndeterminate` (SUB-031).
18
18
  */
19
- export const conversationPortTransportLayer: Layer.Layer<
20
- ConversationPortTransport,
19
+ export const threadPortTransportLayer: Layer.Layer<
20
+ ThreadPortTransport,
21
21
  never,
22
- ConversationObjectNamespace
23
- > = Layer.effect(ConversationPortTransport)(
22
+ ThreadObjectNamespace
23
+ > = Layer.effect(ThreadPortTransport)(
24
24
  Effect.gen(function* () {
25
- const { namespace } = yield* ConversationObjectNamespace;
26
- return ConversationPortTransport.of({
27
- call: (conversationId, request) =>
25
+ const { namespace } = yield* ThreadObjectNamespace;
26
+ return ThreadPortTransport.of({
27
+ call: (threadId, request) =>
28
28
  Effect.tryPromise({
29
- try: () => namespace.get(namespace.idFromName(conversationId)).portCall(request),
30
- catch: (cause) => portTransportFailure(conversationId, cause),
29
+ try: () => namespace.get(namespace.idFromName(threadId)).portCall(request),
30
+ catch: (cause) => portTransportFailure(threadId, cause),
31
31
  }).pipe(
32
32
  Effect.withSpan("CloudflarePortTransport.call", {
33
- attributes: { conversationId },
33
+ attributes: { threadId },
34
34
  }),
35
35
  ),
36
36
  });