@effect-agent/platform-cloudflare 0.0.1-beta.4 → 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
@@ -12,7 +12,7 @@ const PositiveMillis = Schema.Int.check(Schema.isGreaterThan(0));
12
12
  const NonNegativeMillis = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
13
13
 
14
14
  /** The supplied Cloudflare durable runtime configuration failed validation (DEPLOY-003). */
15
- export class CloudflarePlatformConfigError extends Schema.TaggedErrorClass<CloudflarePlatformConfigError>()(
15
+ export class CloudflarePlatformConfigError extends Schema.TaggedError<CloudflarePlatformConfigError>()(
16
16
  "CloudflarePlatformConfigError",
17
17
  {
18
18
  message: Schema.String,
@@ -28,7 +28,7 @@ export class CloudflarePlatformConfigError extends Schema.TaggedErrorClass<Cloud
28
28
  * Conversation Object's submit entry point before `DurableAgentRuntime.submit` runs, and
29
29
  * nothing was admitted or written.
30
30
  */
31
- export class AdmissionLimitExceeded extends Schema.TaggedErrorClass<AdmissionLimitExceeded>()(
31
+ export class AdmissionLimitExceeded extends Schema.TaggedError<AdmissionLimitExceeded>()(
32
32
  "AdmissionLimitExceeded",
33
33
  {
34
34
  limit: Schema.Literals(["queue-depth", "input-bytes", "database-bytes"]),
@@ -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. */
@@ -97,16 +102,35 @@ export interface CloudflareDurableRuntimeOptions {
97
102
  readonly toolReconciler?: Layer.Layer<ToolReconciler> | undefined;
98
103
  /**
99
104
  * Registered worker Bindings resolved at durable claim time (S2, spec/subagents.md §11):
100
- * build each with `DurableWorkerBinding.make(binding, digests)`. An Effect form is accepted
101
- * because the capture is effectful; it runs once per incarnation during Layer construction.
102
- * Defaults to the empty registration (every resolved claim fails closed).
105
+ * build each with `DurableWorkerBinding.make(binding, digests)`. An Effect or callback form is
106
+ * accepted because capture can be effectful. The callback receives the live Object context and
107
+ * derived identities and is evaluated once per incarnation during Layer construction. Defaults
108
+ * to the empty registration (every resolved claim fails closed).
103
109
  */
104
- readonly bindings?:
105
- | ReadonlyArray<ResolvedBinding>
106
- | Effect.Effect<ReadonlyArray<ResolvedBinding>>
107
- | undefined;
110
+ readonly bindings?: CloudflareBindingSource | undefined;
111
+ }
112
+
113
+ /** Per-incarnation host values available while registered worker Bindings are captured. */
114
+ export interface CloudflareBindingSourceContext {
115
+ readonly ctx: DurableObjectState;
116
+ readonly env: unknown;
117
+ readonly conversationId: ConversationId;
118
+ readonly producerId: ProducerId;
108
119
  }
109
120
 
121
+ /**
122
+ * Registered worker Bindings, or a closed Effect/callback that captures them once for each
123
+ * Durable Object incarnation. Callback Effects cannot require services or fail typed.
124
+ */
125
+ export type CloudflareBindingSource =
126
+ | ReadonlyArray<ResolvedBinding>
127
+ | Effect.Effect<ReadonlyArray<ResolvedBinding>, never, never>
128
+ | ((
129
+ context: CloudflareBindingSourceContext,
130
+ ) =>
131
+ | ReadonlyArray<ResolvedBinding>
132
+ | Effect.Effect<ReadonlyArray<ResolvedBinding>, never, never>);
133
+
110
134
  /** Every construction failure of the assembled Cloudflare durable runtime stack. */
111
135
  export type CloudflareDurableRuntimeInitializationError =
112
136
  | CloudflarePlatformConfigError
@@ -161,6 +185,8 @@ const configFromOptions = (
161
185
  abortPollInterval: options.abortPollInterval ?? CLOUDFLARE_RUNTIME_DEFAULTS.abortPollInterval,
162
186
  observationPollInterval:
163
187
  options.observationPollInterval ?? CLOUDFLARE_RUNTIME_DEFAULTS.observationPollInterval,
188
+ telemetryFlushTimeout:
189
+ options.telemetryFlushTimeout ?? CLOUDFLARE_RUNTIME_DEFAULTS.telemetryFlushTimeout,
164
190
  maxStoredValueBytes:
165
191
  options.maxStoredValueBytes ?? CLOUDFLARE_RUNTIME_DEFAULTS.maxStoredValueBytes,
166
192
  verifyOnOpen: options.verifyOnOpen ?? CLOUDFLARE_RUNTIME_DEFAULTS.verifyOnOpen,
@@ -208,13 +234,19 @@ const conversationIdFromState = (
208
234
  );
209
235
 
210
236
  const resolveBindings = (
211
- bindings: CloudflareDurableRuntimeOptions["bindings"],
237
+ source: CloudflareDurableRuntimeOptions["bindings"],
238
+ context: CloudflareBindingSourceContext,
212
239
  ): Effect.Effect<ReadonlyArray<ResolvedBinding>> =>
213
- bindings === undefined
240
+ source === undefined
214
241
  ? Effect.succeed([])
215
- : Effect.isEffect(bindings)
216
- ? bindings
217
- : Effect.succeed(bindings);
242
+ : Effect.isEffect(source)
243
+ ? source
244
+ : typeof source === "function"
245
+ ? Effect.suspend(() => {
246
+ const bindings = source(context);
247
+ return Effect.isEffect(bindings) ? bindings : Effect.succeed(bindings);
248
+ })
249
+ : Effect.succeed(source);
218
250
 
219
251
  /**
220
252
  * The DC Layer assembly (deployment §12: a Layer-assembly library, not an app entrypoint;
@@ -229,9 +261,9 @@ const resolveBindings = (
229
261
  * Storage compatibility is verified during construction: an incompatible database fails the
230
262
  * Layer typed (`DoStorageCompatibilityError`) before anything is mutated (DEPLOY-008).
231
263
  *
232
- * Requires only the two binding services (`DurableObjectContext`,
233
- * `ConversationObjectNamespace`) — platform values enter exclusively through Layers
234
- * (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).
235
267
  */
236
268
  export class CloudflareDurableRuntime {
237
269
  static layer(
@@ -243,7 +275,7 @@ export class CloudflareDurableRuntime {
243
275
  > {
244
276
  return Layer.unwrap(
245
277
  Effect.gen(function* () {
246
- const { ctx } = yield* DurableObjectContext;
278
+ const { ctx, env } = yield* DurableObjectContext;
247
279
  const config = yield* configFromOptions(options);
248
280
  const conversationId = yield* conversationIdFromState(ctx);
249
281
  const producerId = yield* decodeProducerId(
@@ -317,7 +349,10 @@ export class CloudflareDurableRuntime {
317
349
  : Layer.succeed(DurableRuntimeFailpoint)({ hit: options.runtimeFailpoint(ctx) });
318
350
  const reconcilerLayer = options.toolReconciler ?? ToolReconciler.uncertain;
319
351
  const bindingResolverLayer = Layer.effect(AgentBindingResolver)(
320
- Effect.map(resolveBindings(options.bindings), AgentBindingResolver.fromBindings),
352
+ Effect.map(
353
+ resolveBindings(options.bindings, { ctx, env, conversationId, producerId }),
354
+ (bindings) => AgentBindingResolver.fromBindings(bindings),
355
+ ),
321
356
  );
322
357
 
323
358
  const base = Layer.mergeAll(
@@ -337,11 +372,13 @@ export class CloudflareDurableRuntime {
337
372
  Layer.provideMerge(base),
338
373
  );
339
374
 
340
- return Layer.mergeAll(
375
+ const application = Layer.mergeAll(
341
376
  runtimeStack,
342
377
  ConversationMaintenance.layer.pipe(Layer.provide(runtimeStack)),
343
378
  portsEndpointLayer,
344
379
  );
380
+
381
+ return application;
345
382
  }),
346
383
  );
347
384
  }