@effect-agent/platform-cloudflare 0.0.1-beta.5 → 0.1.0-beta.6

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.
@@ -29,14 +29,25 @@ import {
29
29
  SubmissionLookupByKey,
30
30
  type DurableSubmitAgent,
31
31
  } from "@effect-agent/session";
32
- import { DurableObject } from "cloudflare:workers";
33
- import { Effect, Layer, ManagedRuntime, Option, Schema, Stream } from "effect";
32
+ import type { DurableObject as CloudflareDurableObject } from "cloudflare:workers";
33
+ import { Effect, Layer, Option, Schema, Stream } from "effect";
34
+ import {
35
+ DurableObject as EffectCfDurableObject,
36
+ DurableObjectState as EffectCfDurableObjectState,
37
+ WorkerEnvironment,
38
+ } from "effect-cf";
34
39
 
35
- import { ConversationMaintenance, DurableAlarmError, DurableAlarmService } from "./alarm.ts";
40
+ import {
41
+ ConversationMaintenance,
42
+ DurableAlarmError,
43
+ DurableAlarmService,
44
+ type MaintenancePassFailure,
45
+ } from "./alarm.ts";
36
46
  import {
37
47
  ConversationObjectIdentity,
38
48
  DurableObjectContext,
39
- conversationNamespaceLayer,
49
+ ConversationObjectNamespace,
50
+ conversationNamespaceFromEnv,
40
51
  type CloudflareBindingError,
41
52
  } from "./bindings.ts";
42
53
  import {
@@ -69,7 +80,8 @@ import {
69
80
  } from "./layers.ts";
70
81
 
71
82
  /**
72
- * `makeConversationObjectClass(options)` — the Conversation Durable Object (plan §1.4,
83
+ * `makeConversationObjectClass(options, observability?)` — the Conversation Durable Object
84
+ * (plan §1.4,
73
85
  * D-P6-1): a factory returning a class that applications export from their Worker entry.
74
86
  * One SQLite-backed Object per Conversation is the serialized owner (durability §6); the
75
87
  * Object never runs `runResolvedWorker`'s infinite loop — each ingress event or alarm runs
@@ -95,9 +107,12 @@ export interface ConversationObjectOptions extends CloudflareDurableRuntimeOptio
95
107
  readonly namespaceBinding: string;
96
108
  }
97
109
 
98
- type ConversationObjectError = CloudflareDurableRuntimeInitializationError | CloudflareBindingError;
99
-
100
110
  type EndpointServices = CloudflareDurableRuntimeServices | DurableObjectContext;
111
+ type RuntimeServices = EndpointServices | ConversationObjectNamespace;
112
+ type ConversationObjectInitializationError =
113
+ | CloudflareDurableRuntimeInitializationError
114
+ | CloudflareBindingError
115
+ | MaintenancePassFailure;
101
116
 
102
117
  /** Port envelope tags whose owner-side execution durably mutates this Object's lane. */
103
118
  const MUTATING_PORT_TAGS: ReadonlySet<string> = new Set([
@@ -593,23 +608,56 @@ const wakeEndpoint: Effect.Effect<void, never, EndpointServices> = Effect.gen(fu
593
608
  );
594
609
  });
595
610
 
596
- const alarmEndpoint: Effect.Effect<void, unknown, EndpointServices> = Effect.gen(function* () {
597
- const maintenance = yield* ConversationMaintenance;
598
- // Typed pass failures propagate: the rejected promise makes workerd retry the alarm
599
- // (at-least-once delivery), and the pass's own pre-arm keeps the slot committed meanwhile.
600
- yield* maintenance.pass;
601
- });
611
+ const alarmEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointServices> = Effect.gen(
612
+ function* () {
613
+ const maintenance = yield* ConversationMaintenance;
614
+ // Typed pass failures propagate: the rejected promise makes workerd retry the alarm
615
+ // (at-least-once delivery), and the pass's own pre-arm keeps the slot committed meanwhile.
616
+ yield* maintenance.pass;
617
+ },
618
+ );
602
619
 
603
- const gateEndpoint: Effect.Effect<void, unknown, EndpointServices> = Effect.gen(function* () {
604
- // Forcing ConversationMaintenance forces the whole Layer stack: migration + exact-version
605
- // check + configuration decode (DEPLOY-008 fails typed here, before any mutation), then
606
- // the defensive local ensure-alarm half of the invariant. LOCAL-ONLY by construction.
607
- const maintenance = yield* ConversationMaintenance;
608
- yield* maintenance.ensureAlarm;
609
- });
620
+ const gateEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointServices> = Effect.gen(
621
+ function* () {
622
+ // Forcing ConversationMaintenance forces the whole Layer stack: migration + exact-version
623
+ // check + configuration decode (DEPLOY-008 fails typed here, before any mutation), then
624
+ // the defensive local ensure-alarm half of the invariant. LOCAL-ONLY by construction.
625
+ const maintenance = yield* ConversationMaintenance;
626
+ yield* maintenance.ensureAlarm;
627
+ },
628
+ );
629
+
630
+ /**
631
+ * Adapter from effect-cf's native Durable Object services to Effect Agent's existing platform
632
+ * ports. effect-cf owns the cached ManagedRuntime and supplies these values once per Object
633
+ * incarnation; the durable runtime continues to depend only on the narrow services below.
634
+ */
635
+ const effectCfPlatformLayer = (
636
+ namespaceBinding: string,
637
+ ): Layer.Layer<
638
+ DurableObjectContext | ConversationObjectNamespace,
639
+ CloudflareBindingError,
640
+ EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
641
+ > => {
642
+ const context = Layer.effect(DurableObjectContext)(
643
+ Effect.gen(function* () {
644
+ const state = yield* EffectCfDurableObjectState.DurableObjectState;
645
+ const env = yield* WorkerEnvironment;
646
+ return DurableObjectContext.of({ ctx: state.raw, env });
647
+ }),
648
+ );
649
+ const namespace = Layer.effect(ConversationObjectNamespace)(
650
+ Effect.gen(function* () {
651
+ const env = yield* WorkerEnvironment;
652
+ const binding = yield* conversationNamespaceFromEnv(env, namespaceBinding);
653
+ return ConversationObjectNamespace.of({ namespace: binding });
654
+ }),
655
+ );
656
+ return Layer.merge(context, namespace);
657
+ };
610
658
 
611
659
  /** The public endpoint surface of one Conversation Object instance. */
612
- export interface ConversationObjectInstance extends DurableObject {
660
+ export interface ConversationObjectInstance extends CloudflareDurableObject {
613
661
  submitEncoded(encoded: unknown): Promise<unknown>;
614
662
  awaitSettlementEncoded(encoded: unknown): Promise<unknown>;
615
663
  observePage(encoded: unknown): Promise<unknown>;
@@ -622,7 +670,7 @@ export interface ConversationObjectInstance extends DurableObject {
622
670
  obligationsEncoded(encoded: unknown): Promise<unknown>;
623
671
  portCall(encoded: unknown): Promise<unknown>;
624
672
  wake(): Promise<void>;
625
- alarm(): Promise<void>;
673
+ alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void;
626
674
  }
627
675
 
628
676
  /** The constructor shape workerd instantiates for each Conversation Object. */
@@ -631,84 +679,88 @@ export interface ConversationObjectClass {
631
679
  }
632
680
 
633
681
  /**
634
- * Build the application's Conversation Object class (export it from the
635
- * Worker entry). The explicit return type is what makes declaration emit
636
- * possible: the class body carries a private runtime field, and TS4094
637
- * rejects inferring an exported anonymous class type around it.
682
+ * Build the application's Conversation Object class (export it from the Worker entry).
683
+ * effect-cf owns the cached ManagedRuntime, native RPC methods, event scopes, and post-handler
684
+ * OTLP flush scheduling for RPC and alarm events. The optional outer Layer is built per native
685
+ * event, so a host can install Tracer/Logger/Metric services and `OtlpExporter.Flusher` without
686
+ * Effect Agent owning exporter lifecycle machinery.
638
687
  */
639
- export const makeConversationObjectClass = (
688
+ export const makeConversationObjectClass = <EventLayerError = never, EventServices = never>(
640
689
  options: ConversationObjectOptions,
690
+ observability?: Layer.Layer<
691
+ EventServices,
692
+ EventLayerError,
693
+ | DurableObjectContext
694
+ | ConversationObjectNamespace
695
+ | EffectCfDurableObjectState.DurableObjectState
696
+ | WorkerEnvironment
697
+ >,
641
698
  ): ConversationObjectClass => {
642
- class ConversationObject extends DurableObject {
643
- readonly #runtime: ManagedRuntime.ManagedRuntime<EndpointServices, ConversationObjectError>;
644
-
645
- constructor(ctx: DurableObjectState, env: Cloudflare.Env) {
646
- super(ctx, env);
647
- this.#runtime = ManagedRuntime.make(
648
- CloudflareDurableRuntime.layer(options).pipe(
649
- Layer.provideMerge(
650
- Layer.mergeAll(
651
- DurableObjectContext.layer(ctx, env),
652
- conversationNamespaceLayer(env, options.namespaceBinding),
653
- ),
654
- ),
655
- ),
656
- );
657
- // The constructor gate: local-only checks; never the recovery pass (deadlock argument,
658
- // plan §1.4). A failure here fails every delivery with the typed construction error.
659
- ctx.blockConcurrencyWhile(() => this.#runtime.runPromise(gateEndpoint));
660
- }
661
-
662
- async submitEncoded(encoded: unknown): Promise<unknown> {
663
- return this.#runtime.runPromise(submitEndpoint(encoded));
664
- }
665
-
666
- async awaitSettlementEncoded(encoded: unknown): Promise<unknown> {
667
- return this.#runtime.runPromise(awaitSettlementEndpoint(encoded));
668
- }
669
-
670
- async observePage(encoded: unknown): Promise<unknown> {
671
- return this.#runtime.runPromise(observePageEndpoint(encoded));
672
- }
673
-
674
- async abortEncoded(encoded: unknown): Promise<unknown> {
675
- return this.#runtime.runPromise(abortEndpoint(encoded));
676
- }
677
-
678
- async resolveApprovalEncoded(encoded: unknown): Promise<unknown> {
679
- return this.#runtime.runPromise(resolveApprovalEndpoint(encoded));
680
- }
681
-
682
- async resolveUnknownEncoded(encoded: unknown): Promise<unknown> {
683
- return this.#runtime.runPromise(resolveUnknownEndpoint(encoded));
684
- }
685
-
686
- async explainEncoded(encoded: unknown): Promise<unknown> {
687
- return this.#runtime.runPromise(explainEndpoint(encoded));
688
- }
689
-
690
- async verifyEncoded(encoded: unknown): Promise<unknown> {
691
- return this.#runtime.runPromise(verifyEndpoint(encoded));
692
- }
693
-
694
- async retryEncoded(encoded: unknown): Promise<unknown> {
695
- return this.#runtime.runPromise(retryEndpoint(encoded));
696
- }
697
-
698
- async obligationsEncoded(encoded: unknown): Promise<unknown> {
699
- return this.#runtime.runPromise(obligationsEndpoint(encoded));
700
- }
699
+ const application: Layer.Layer<
700
+ RuntimeServices,
701
+ CloudflareDurableRuntimeInitializationError | CloudflareBindingError,
702
+ EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
703
+ > = CloudflareDurableRuntime.layer(options).pipe(
704
+ Layer.provideMerge(effectCfPlatformLayer(options.namespaceBinding)),
705
+ );
701
706
 
702
- async portCall(encoded: unknown): Promise<unknown> {
703
- return this.#runtime.runPromise(portCallEndpoint(encoded));
704
- }
707
+ // The storage/config Layer must acquire inside Cloudflare's constructor gate. effect-cf owns
708
+ // the ManagedRuntime, while this effectContext ensures its first Layer build enters the gate
709
+ // before migration, compatibility checks, or alarm inspection touch Object storage.
710
+ const runtime: Layer.Layer<
711
+ RuntimeServices,
712
+ ConversationObjectInitializationError,
713
+ EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
714
+ > = Layer.effectContext(
715
+ Effect.gen(function* () {
716
+ const state = yield* EffectCfDurableObjectState.DurableObjectState;
717
+ const scope = yield* Effect.scope;
718
+ return yield* state.blockConcurrencyWhile(
719
+ Effect.gen(function* () {
720
+ const services = yield* Layer.buildWithScope(application, scope);
721
+ yield* gateEndpoint.pipe(Effect.provide(services));
722
+ return services;
723
+ }),
724
+ );
725
+ }),
726
+ );
705
727
 
706
- async wake(): Promise<void> {
707
- await this.#runtime.runPromise(wakeEndpoint);
708
- }
728
+ const rpc = {
729
+ submitEncoded: (encoded: unknown) => submitEndpoint(encoded),
730
+ awaitSettlementEncoded: (encoded: unknown) => awaitSettlementEndpoint(encoded),
731
+ observePage: (encoded: unknown) => observePageEndpoint(encoded),
732
+ abortEncoded: (encoded: unknown) => abortEndpoint(encoded),
733
+ resolveApprovalEncoded: (encoded: unknown) => resolveApprovalEndpoint(encoded),
734
+ resolveUnknownEncoded: (encoded: unknown) => resolveUnknownEndpoint(encoded),
735
+ explainEncoded: (encoded: unknown) => explainEndpoint(encoded),
736
+ verifyEncoded: (encoded: unknown) => verifyEndpoint(encoded),
737
+ retryEncoded: (encoded: unknown) => retryEndpoint(encoded),
738
+ obligationsEncoded: (encoded: unknown) => obligationsEndpoint(encoded),
739
+ portCall: (encoded: unknown) => portCallEndpoint(encoded),
740
+ wake: () => wakeEndpoint,
741
+ } satisfies EffectCfDurableObject.DurableObjectRpc<RuntimeServices | EventServices>;
742
+
743
+ const EffectCfConversationObject = EffectCfDurableObject.make<
744
+ RuntimeServices,
745
+ ConversationObjectInitializationError,
746
+ EventServices,
747
+ EventLayerError,
748
+ typeof rpc
749
+ >(runtime, {
750
+ ...(observability === undefined ? {} : { eventLayer: observability }),
751
+ // Force the gated runtime Layer when Cloudflare loads this Object incarnation. Recovery stays
752
+ // in each bounded pass so cross-Object initialization cannot deadlock.
753
+ initialize: Effect.void,
754
+ rpc,
755
+ alarm: () => alarmEndpoint,
756
+ });
709
757
 
710
- override async alarm(): Promise<void> {
711
- await this.#runtime.runPromise(alarmEndpoint);
758
+ // effect-cf's class type keeps `alarm` optional even when the handler option is present. This
759
+ // concrete override reflects this factory's stronger contract while delegating execution to
760
+ // the effect-cf runtime unchanged.
761
+ class ConversationObject extends EffectCfConversationObject {
762
+ override alarm(alarmInfo?: AlarmInvocationInfo): Promise<void> | void {
763
+ return super.alarm?.(alarmInfo);
712
764
  }
713
765
  }
714
766
 
package/src/index.ts CHANGED
@@ -20,3 +20,4 @@ export * from "./transport.ts";
20
20
  export * from "./layers.ts";
21
21
  export * from "./conversation-object.ts";
22
22
  export * from "./client.ts";
23
+ export * from "./code-mode-executor.ts";