@effect-agent/platform-cloudflare 0.0.1-beta.5 → 0.0.1-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.
package/src/config.ts CHANGED
@@ -103,6 +103,8 @@ export class CloudflareDurableRuntimeConfigValue extends Schema.Class<Cloudflare
103
103
  abortPollInterval: PositiveMillis,
104
104
  /** Canonical observation poll cadence of the Durable Object store. */
105
105
  observationPollInterval: NonNegativeMillis,
106
+ /** Cooperative background-export budget; native Object delivery never awaits the flush. */
107
+ telemetryFlushTimeout: PositiveMillis,
106
108
  /** Per-value byte bound; must stay under the platform's 2 MB SQLite value limit. */
107
109
  maxStoredValueBytes: Schema.Int.check(
108
110
  Schema.isGreaterThan(0),
@@ -129,6 +131,7 @@ export const CLOUDFLARE_RUNTIME_DEFAULTS = {
129
131
  leaseRenewalInterval: 10_000,
130
132
  abortPollInterval: 500,
131
133
  observationPollInterval: 25,
134
+ telemetryFlushTimeout: 2_000,
132
135
  maxStoredValueBytes: DEFAULT_MAX_STORED_VALUE_BYTES,
133
136
  verifyOnOpen: false,
134
137
  maxQueueDepthPerLane: 256,
@@ -38,6 +38,7 @@ import {
38
38
  DurableObjectContext,
39
39
  conversationNamespaceLayer,
40
40
  type CloudflareBindingError,
41
+ type ConversationObjectNamespace,
41
42
  } from "./bindings.ts";
42
43
  import {
43
44
  AbortRecorded,
@@ -67,9 +68,18 @@ import {
67
68
  type CloudflareDurableRuntimeOptions,
68
69
  type CloudflareDurableRuntimeServices,
69
70
  } from "./layers.ts";
71
+ import {
72
+ flushCloudflareRuntimeTelemetry,
73
+ logCloudflareWaitUntilRegistrationFailure,
74
+ makeCloudflareTelemetryFlushCoordinator,
75
+ registerCloudflareTelemetryAfterNativeSettlement,
76
+ withCloudflareNativeSpanFailure,
77
+ type CloudflareTelemetryFlushCoordinator,
78
+ } from "./telemetry-internal.ts";
79
+ import { CloudflareRuntimeTelemetry } from "./telemetry.ts";
70
80
 
71
81
  /**
72
- * `makeConversationObjectClass(options)` — the Conversation Durable Object (plan §1.4,
82
+ * `makeConversationObjectClass(options, telemetry?)` — the Conversation Durable Object (plan §1.4,
73
83
  * D-P6-1): a factory returning a class that applications export from their Worker entry.
74
84
  * One SQLite-backed Object per Conversation is the serialized owner (durability §6); the
75
85
  * Object never runs `runResolvedWorker`'s infinite loop — each ingress event or alarm runs
@@ -95,9 +105,15 @@ export interface ConversationObjectOptions extends CloudflareDurableRuntimeOptio
95
105
  readonly namespaceBinding: string;
96
106
  }
97
107
 
98
- type ConversationObjectError = CloudflareDurableRuntimeInitializationError | CloudflareBindingError;
108
+ type ConversationObjectError<TelemetryError> =
109
+ | CloudflareDurableRuntimeInitializationError
110
+ | CloudflareBindingError
111
+ | TelemetryError;
99
112
 
100
- type EndpointServices = CloudflareDurableRuntimeServices | DurableObjectContext;
113
+ type EndpointServices =
114
+ | CloudflareDurableRuntimeServices
115
+ | DurableObjectContext
116
+ | CloudflareRuntimeTelemetry;
101
117
 
102
118
  /** Port envelope tags whose owner-side execution durably mutates this Object's lane. */
103
119
  const MUTATING_PORT_TAGS: ReadonlySet<string> = new Set([
@@ -608,6 +624,54 @@ const gateEndpoint: Effect.Effect<void, unknown, EndpointServices> = Effect.gen(
608
624
  yield* maintenance.ensureAlarm;
609
625
  });
610
626
 
627
+ const NATIVE_ENTRYPOINTS = [
628
+ "submit",
629
+ "await_settlement",
630
+ "observe",
631
+ "abort",
632
+ "resolve_approval",
633
+ "resolve_unknown",
634
+ "explain",
635
+ "verify",
636
+ "retry",
637
+ "obligations",
638
+ "port_call",
639
+ "wake",
640
+ "alarm",
641
+ ] as const;
642
+
643
+ type NativeEntrypoint = (typeof NATIVE_ENTRYPOINTS)[number];
644
+
645
+ /**
646
+ * Owner-side delivery measurement. The exact endpoint Cause is hidden behind a bounded typed
647
+ * marker while the span closes, then restored together with any newly composed real reasons.
648
+ */
649
+ const observeNativeEntrypoint = <A, E>(
650
+ entrypoint: NativeEntrypoint,
651
+ effect: Effect.Effect<A, E, EndpointServices>,
652
+ ): Effect.Effect<A, E, EndpointServices> =>
653
+ Effect.gen(function* () {
654
+ const identity = yield* ConversationObjectIdentity;
655
+ const config = yield* CloudflareDurableRuntimeConfig;
656
+ return yield* withCloudflareNativeSpanFailure(effect, (masked) =>
657
+ masked.pipe(
658
+ Effect.withSpan(`effect_agent.cloudflare.conversation_object.${entrypoint}`, {
659
+ kind: "server",
660
+ attributes: {
661
+ "effect_agent.cloudflare.entrypoint": entrypoint,
662
+ "effect_agent.deployment.id": config.deploymentId,
663
+ conversationId: identity.conversationId,
664
+ producerId: identity.producerId,
665
+ },
666
+ }),
667
+ ),
668
+ );
669
+ });
670
+
671
+ const flushNativeEntrypointTelemetry = Effect.flatMap(CloudflareDurableRuntimeConfig, (config) =>
672
+ flushCloudflareRuntimeTelemetry(config.telemetryFlushTimeout),
673
+ );
674
+
611
675
  /** The public endpoint surface of one Conversation Object instance. */
612
676
  export interface ConversationObjectInstance extends DurableObject {
613
677
  submitEncoded(encoded: unknown): Promise<unknown>;
@@ -631,84 +695,151 @@ export interface ConversationObjectClass {
631
695
  }
632
696
 
633
697
  /**
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.
698
+ * Build the application's Conversation Object class (export it from the Worker entry).
699
+ * `telemetry` is the host composition boundary: it may require the Object context/namespace and
700
+ * retain a typed acquisition error, while any additional output services remain available to the
701
+ * cached runtime. `TelemetryError` stays the first explicit generic for source compatibility;
702
+ * additional outputs infer from the Layer. Omitting it installs the content-free no-op service.
703
+ * The explicit return type is what makes declaration emit possible: the class body carries a
704
+ * private runtime field, and TS4094 rejects inferring an exported anonymous class type around it.
638
705
  */
639
- export const makeConversationObjectClass = (
706
+ export const makeConversationObjectClass = <
707
+ TelemetryError = never,
708
+ AdditionalTelemetryOutputs = never,
709
+ >(
640
710
  options: ConversationObjectOptions,
711
+ telemetry?: Layer.Layer<
712
+ CloudflareRuntimeTelemetry | AdditionalTelemetryOutputs,
713
+ TelemetryError,
714
+ DurableObjectContext | ConversationObjectNamespace
715
+ >,
641
716
  ): ConversationObjectClass => {
642
717
  class ConversationObject extends DurableObject {
643
- readonly #runtime: ManagedRuntime.ManagedRuntime<EndpointServices, ConversationObjectError>;
718
+ readonly #runtime: ManagedRuntime.ManagedRuntime<
719
+ EndpointServices,
720
+ ConversationObjectError<TelemetryError>
721
+ >;
722
+ readonly #ctx: DurableObjectState;
723
+ readonly #telemetryFlush: CloudflareTelemetryFlushCoordinator;
644
724
 
645
725
  constructor(ctx: DurableObjectState, env: Cloudflare.Env) {
646
726
  super(ctx, env);
727
+ this.#ctx = ctx;
728
+ const platform = Layer.mergeAll(
729
+ DurableObjectContext.layer(ctx, env),
730
+ conversationNamespaceLayer(env, options.namespaceBinding),
731
+ );
732
+ // Host observability is composed at the Worker edge, not hidden in runtime options. Its
733
+ // typed acquisition failure remains in the ManagedRuntime error, while the Object context
734
+ // and namespace are available to Layers that derive exporter configuration from `env`.
735
+ const hostTelemetry = (telemetry ?? CloudflareRuntimeTelemetry.layerNoop).pipe(
736
+ Layer.provideMerge(platform),
737
+ );
647
738
  this.#runtime = ManagedRuntime.make(
648
739
  CloudflareDurableRuntime.layer(options).pipe(
649
- Layer.provideMerge(
650
- Layer.mergeAll(
651
- DurableObjectContext.layer(ctx, env),
652
- conversationNamespaceLayer(env, options.namespaceBinding),
653
- ),
654
- ),
740
+ // Supplying the host Layer outermost makes its Logger/Tracer/Metric runtime
741
+ // configuration observe application Layer acquisition and every native endpoint.
742
+ Layer.provideMerge(hostTelemetry),
655
743
  ),
656
744
  );
745
+ this.#telemetryFlush = makeCloudflareTelemetryFlushCoordinator(
746
+ () => this.#runtime.runPromise(flushNativeEntrypointTelemetry),
747
+ {
748
+ onReservationDropped: () => {
749
+ void this.#runtime.runSyncExit(
750
+ Effect.logWarning(
751
+ "Cloudflare telemetry delivery coalesced at the batch reservation limit",
752
+ ).pipe(
753
+ Effect.annotateLogs({
754
+ "effect_agent.cloudflare.telemetry.failure_kind": "reservation_limit",
755
+ }),
756
+ ),
757
+ );
758
+ },
759
+ },
760
+ );
657
761
  // The constructor gate: local-only checks; never the recovery pass (deadlock argument,
658
762
  // plan §1.4). A failure here fails every delivery with the typed construction error.
659
763
  ctx.blockConcurrencyWhile(() => this.#runtime.runPromise(gateEndpoint));
660
764
  }
661
765
 
766
+ #runNative<A, E>(
767
+ entrypoint: NativeEntrypoint,
768
+ effect: Effect.Effect<A, E, EndpointServices>,
769
+ ): Promise<A> {
770
+ const delivery = this.#runtime.runPromise(observeNativeEntrypoint(entrypoint, effect));
771
+ // Reserve synchronously with the current native delivery, but do not await export from the
772
+ // RPC/alarm Promise. Each pending/trailing/queued batch waits for its assigned deliveries to
773
+ // settle and only its first owner registers the shared background Promise. Even an
774
+ // uninterruptible exporter therefore cannot delay a caller, suppress alarm retry, accumulate
775
+ // concurrent exporter work, an unbounded waitUntil registration queue, or unbounded retained
776
+ // delivery Promises. Excess arrivals are diagnosed and lossy-coalesced at the batch cap.
777
+ // Registration failure and every asynchronous flush failure remain isolated from delivery.
778
+ return registerCloudflareTelemetryAfterNativeSettlement(
779
+ (background) => this.#ctx.waitUntil(background),
780
+ delivery,
781
+ this.#telemetryFlush.reserve,
782
+ (cause) => {
783
+ // Native entrypoints run only after blockConcurrencyWhile has built the cached runtime.
784
+ // Effect Logger invocation is synchronous; runSyncExit captures a broken logger without
785
+ // starting an unscoped fiber or changing the delivery Promise. The exact platform value
786
+ // stays confined to this registration callback; the automatic Logger diagnostic is
787
+ // deliberately content-free.
788
+ void this.#runtime.runSyncExit(logCloudflareWaitUntilRegistrationFailure(cause));
789
+ },
790
+ );
791
+ }
792
+
662
793
  async submitEncoded(encoded: unknown): Promise<unknown> {
663
- return this.#runtime.runPromise(submitEndpoint(encoded));
794
+ return this.#runNative("submit", submitEndpoint(encoded));
664
795
  }
665
796
 
666
797
  async awaitSettlementEncoded(encoded: unknown): Promise<unknown> {
667
- return this.#runtime.runPromise(awaitSettlementEndpoint(encoded));
798
+ return this.#runNative("await_settlement", awaitSettlementEndpoint(encoded));
668
799
  }
669
800
 
670
801
  async observePage(encoded: unknown): Promise<unknown> {
671
- return this.#runtime.runPromise(observePageEndpoint(encoded));
802
+ return this.#runNative("observe", observePageEndpoint(encoded));
672
803
  }
673
804
 
674
805
  async abortEncoded(encoded: unknown): Promise<unknown> {
675
- return this.#runtime.runPromise(abortEndpoint(encoded));
806
+ return this.#runNative("abort", abortEndpoint(encoded));
676
807
  }
677
808
 
678
809
  async resolveApprovalEncoded(encoded: unknown): Promise<unknown> {
679
- return this.#runtime.runPromise(resolveApprovalEndpoint(encoded));
810
+ return this.#runNative("resolve_approval", resolveApprovalEndpoint(encoded));
680
811
  }
681
812
 
682
813
  async resolveUnknownEncoded(encoded: unknown): Promise<unknown> {
683
- return this.#runtime.runPromise(resolveUnknownEndpoint(encoded));
814
+ return this.#runNative("resolve_unknown", resolveUnknownEndpoint(encoded));
684
815
  }
685
816
 
686
817
  async explainEncoded(encoded: unknown): Promise<unknown> {
687
- return this.#runtime.runPromise(explainEndpoint(encoded));
818
+ return this.#runNative("explain", explainEndpoint(encoded));
688
819
  }
689
820
 
690
821
  async verifyEncoded(encoded: unknown): Promise<unknown> {
691
- return this.#runtime.runPromise(verifyEndpoint(encoded));
822
+ return this.#runNative("verify", verifyEndpoint(encoded));
692
823
  }
693
824
 
694
825
  async retryEncoded(encoded: unknown): Promise<unknown> {
695
- return this.#runtime.runPromise(retryEndpoint(encoded));
826
+ return this.#runNative("retry", retryEndpoint(encoded));
696
827
  }
697
828
 
698
829
  async obligationsEncoded(encoded: unknown): Promise<unknown> {
699
- return this.#runtime.runPromise(obligationsEndpoint(encoded));
830
+ return this.#runNative("obligations", obligationsEndpoint(encoded));
700
831
  }
701
832
 
702
833
  async portCall(encoded: unknown): Promise<unknown> {
703
- return this.#runtime.runPromise(portCallEndpoint(encoded));
834
+ return this.#runNative("port_call", portCallEndpoint(encoded));
704
835
  }
705
836
 
706
837
  async wake(): Promise<void> {
707
- await this.#runtime.runPromise(wakeEndpoint);
838
+ await this.#runNative("wake", wakeEndpoint);
708
839
  }
709
840
 
710
841
  override async alarm(): Promise<void> {
711
- await this.#runtime.runPromise(alarmEndpoint);
842
+ await this.#runNative("alarm", alarmEndpoint);
712
843
  }
713
844
  }
714
845
 
package/src/index.ts CHANGED
@@ -17,6 +17,8 @@ export * from "./config.ts";
17
17
  export * from "./alarm.ts";
18
18
  export * from "./wake-scheduler.ts";
19
19
  export * from "./transport.ts";
20
+ export * from "./telemetry.ts";
20
21
  export * from "./layers.ts";
21
22
  export * from "./conversation-object.ts";
22
23
  export * from "./client.ts";
24
+ export * from "./code-mode-executor.ts";
package/src/layers.ts CHANGED
@@ -31,8 +31,8 @@ import { Context, Duration, Effect, Layer, Schema } from "effect";
31
31
  import { ConversationMaintenance, DurableAlarmService } from "./alarm.ts";
32
32
  import {
33
33
  ConversationObjectIdentity,
34
- ConversationObjectNamespace,
35
34
  DurableObjectContext,
35
+ type ConversationObjectNamespace,
36
36
  } from "./bindings.ts";
37
37
  import {
38
38
  CLOUDFLARE_RUNTIME_DEFAULTS,
@@ -69,6 +69,11 @@ export interface CloudflareDurableRuntimeOptions {
69
69
  readonly abortPollInterval?: number | undefined;
70
70
  /** Milliseconds; default 25. */
71
71
  readonly observationPollInterval?: number | undefined;
72
+ /**
73
+ * Milliseconds; default 2000. Cooperative budget for the background exporter flush registered
74
+ * after each native RPC, wake, or alarm span. Delivery never awaits the flush.
75
+ */
76
+ readonly telemetryFlushTimeout?: number | undefined;
72
77
  /** Bytes; default just under the 2 MB platform value limit. */
73
78
  readonly maxStoredValueBytes?: number | undefined;
74
79
  /** Default false. */
@@ -180,6 +185,8 @@ const configFromOptions = (
180
185
  abortPollInterval: options.abortPollInterval ?? CLOUDFLARE_RUNTIME_DEFAULTS.abortPollInterval,
181
186
  observationPollInterval:
182
187
  options.observationPollInterval ?? CLOUDFLARE_RUNTIME_DEFAULTS.observationPollInterval,
188
+ telemetryFlushTimeout:
189
+ options.telemetryFlushTimeout ?? CLOUDFLARE_RUNTIME_DEFAULTS.telemetryFlushTimeout,
183
190
  maxStoredValueBytes:
184
191
  options.maxStoredValueBytes ?? CLOUDFLARE_RUNTIME_DEFAULTS.maxStoredValueBytes,
185
192
  verifyOnOpen: options.verifyOnOpen ?? CLOUDFLARE_RUNTIME_DEFAULTS.verifyOnOpen,
@@ -254,9 +261,9 @@ const resolveBindings = (
254
261
  * Storage compatibility is verified during construction: an incompatible database fails the
255
262
  * Layer typed (`DoStorageCompatibilityError`) before anything is mutated (DEPLOY-008).
256
263
  *
257
- * Requires only the two binding services (`DurableObjectContext`,
258
- * `ConversationObjectNamespace`) — platform values enter exclusively through Layers
259
- * (DEPLOY-010).
264
+ * Requires the two binding services (`DurableObjectContext`, `ConversationObjectNamespace`).
265
+ * Host observability belongs to the Worker composition edge because native entrypoint lifecycle
266
+ * instrumentation, rather than this durable runtime assembly, consumes it (DEPLOY-010).
260
267
  */
261
268
  export class CloudflareDurableRuntime {
262
269
  static layer(
@@ -365,11 +372,13 @@ export class CloudflareDurableRuntime {
365
372
  Layer.provideMerge(base),
366
373
  );
367
374
 
368
- return Layer.mergeAll(
375
+ const application = Layer.mergeAll(
369
376
  runtimeStack,
370
377
  ConversationMaintenance.layer.pipe(Layer.provide(runtimeStack)),
371
378
  portsEndpointLayer,
372
379
  );
380
+
381
+ return application;
373
382
  }),
374
383
  );
375
384
  }