@effect-agent/platform-cloudflare 0.1.0-beta.8 → 0.1.0-beta.81

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.
Files changed (100) hide show
  1. package/dist/Alarm.d.mts +184 -0
  2. package/dist/Alarm.mjs +451 -0
  3. package/dist/Alarm.mjs.map +1 -0
  4. package/dist/BrowserRestCapture.d.mts +35 -0
  5. package/dist/BrowserRestCapture.mjs +238 -0
  6. package/dist/BrowserRestCapture.mjs.map +1 -0
  7. package/dist/BrowserRestCrawl.d.mts +17 -0
  8. package/dist/BrowserRestCrawl.mjs +244 -0
  9. package/dist/BrowserRestCrawl.mjs.map +1 -0
  10. package/dist/CloudflareAiGateway.d.mts +64 -0
  11. package/dist/CloudflareAiGateway.mjs +70 -0
  12. package/dist/CloudflareAiGateway.mjs.map +1 -0
  13. package/dist/CloudflareBindings.d.mts +112 -0
  14. package/dist/CloudflareBindings.mjs +101 -0
  15. package/dist/CloudflareBindings.mjs.map +1 -0
  16. package/dist/CloudflareBrowser-D4YqaTAl.mjs +495 -0
  17. package/dist/CloudflareBrowser-D4YqaTAl.mjs.map +1 -0
  18. package/dist/CloudflareBrowser-DJq53niJ.d.mts +80 -0
  19. package/dist/CloudflareBrowser.d.mts +2 -0
  20. package/dist/CloudflareBrowser.mjs +2 -0
  21. package/dist/CloudflareCodeMode.d.mts +44 -0
  22. package/dist/CloudflareCodeMode.mjs +616 -0
  23. package/dist/CloudflareCodeMode.mjs.map +1 -0
  24. package/dist/CloudflareConfig-f3CqTel1.d.mts +101 -0
  25. package/dist/CloudflareConfig.d.mts +2 -0
  26. package/dist/CloudflareConfig.mjs +122 -0
  27. package/dist/CloudflareConfig.mjs.map +1 -0
  28. package/dist/CloudflareMemory.d.mts +142 -0
  29. package/dist/CloudflareMemory.mjs +195 -0
  30. package/dist/CloudflareMemory.mjs.map +1 -0
  31. package/dist/CloudflareScheduling.d.mts +52 -0
  32. package/dist/CloudflareScheduling.mjs +389 -0
  33. package/dist/CloudflareScheduling.mjs.map +1 -0
  34. package/dist/CloudflareSubscriptions.d.mts +87 -0
  35. package/dist/CloudflareSubscriptions.mjs +524 -0
  36. package/dist/CloudflareSubscriptions.mjs.map +1 -0
  37. package/dist/CloudflareThreadClient.d.mts +1286 -0
  38. package/dist/CloudflareThreadClient.mjs +386 -0
  39. package/dist/CloudflareThreadClient.mjs.map +1 -0
  40. package/dist/InteractiveBrowser.d.mts +144 -0
  41. package/dist/InteractiveBrowser.mjs +1090 -0
  42. package/dist/InteractiveBrowser.mjs.map +1 -0
  43. package/dist/ProtectedBrowser.d.mts +65 -0
  44. package/dist/ProtectedBrowser.mjs +859 -0
  45. package/dist/ProtectedBrowser.mjs.map +1 -0
  46. package/dist/ThreadObject-BpwkEc5E.mjs +836 -0
  47. package/dist/ThreadObject-BpwkEc5E.mjs.map +1 -0
  48. package/dist/ThreadObject-NbFOWK1I.d.mts +816 -0
  49. package/dist/ThreadObject.d.mts +2 -0
  50. package/dist/ThreadObject.mjs +3 -0
  51. package/dist/WakeScheduler.d.mts +23 -0
  52. package/dist/WakeScheduler.mjs +60 -0
  53. package/dist/WakeScheduler.mjs.map +1 -0
  54. package/dist/boundary-BguazVkh.mjs +42 -0
  55. package/dist/boundary-BguazVkh.mjs.map +1 -0
  56. package/dist/browser-session-lifecycle-CUbVfEgP.mjs +84 -0
  57. package/dist/browser-session-lifecycle-CUbVfEgP.mjs.map +1 -0
  58. package/dist/browser-session-lifecycle-DJpqLXy_.d.mts +21 -0
  59. package/dist/index.d.mts +13 -1547
  60. package/dist/index.mjs +13 -1636
  61. package/dist/prepared-admission-DSi55W-7.mjs +73 -0
  62. package/dist/prepared-admission-DSi55W-7.mjs.map +1 -0
  63. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  64. package/package.json +1 -53
  65. package/src/Alarm.ts +967 -0
  66. package/src/BrowserRestCapture.ts +477 -0
  67. package/src/BrowserRestCrawl.ts +540 -0
  68. package/src/CloudflareAiGateway.ts +170 -0
  69. package/src/CloudflareBindings.ts +180 -0
  70. package/src/CloudflareBrowser.ts +15 -0
  71. package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +372 -204
  72. package/src/{config.ts → CloudflareConfig.ts} +9 -8
  73. package/src/CloudflareMemory.ts +406 -0
  74. package/src/CloudflareScheduling.ts +733 -0
  75. package/src/CloudflareSubscriptions.ts +1045 -0
  76. package/src/CloudflareThreadClient.ts +807 -0
  77. package/src/InteractiveBrowser.ts +2357 -0
  78. package/src/ProtectedBrowser.ts +2 -0
  79. package/src/ThreadObject.ts +1179 -0
  80. package/src/{wake-scheduler.ts → WakeScheduler.ts} +23 -23
  81. package/src/index.ts +12 -23
  82. package/src/internal/boundary.ts +55 -0
  83. package/src/internal/browser-quick-action.ts +857 -0
  84. package/src/internal/browser-session-lifecycle.ts +160 -0
  85. package/src/internal/layers.ts +751 -0
  86. package/src/internal/message-delivery.ts +148 -0
  87. package/src/internal/prepared-admission.ts +116 -0
  88. package/src/internal/progress-wait.ts +121 -0
  89. package/src/internal/transport.ts +42 -0
  90. package/src/protected-browser/binding.ts +187 -0
  91. package/src/protected-browser/inspect-frame.ts +125 -0
  92. package/src/protected-browser/native.ts +429 -0
  93. package/src/protected-browser/policy.ts +719 -0
  94. package/dist/index.mjs.map +0 -1
  95. package/src/alarm.ts +0 -334
  96. package/src/bindings.ts +0 -145
  97. package/src/client.ts +0 -646
  98. package/src/conversation-object.ts +0 -768
  99. package/src/layers.ts +0 -376
  100. package/src/transport.ts +0 -38
package/src/alarm.ts DELETED
@@ -1,334 +0,0 @@
1
- import {
2
- AgentBindingResolver,
3
- DurableAgentRuntime,
4
- SubmissionLedger,
5
- type DurableBindingFailure,
6
- type DurableWorkerFailure,
7
- type RecoveryReport,
8
- } from "@effect-agent/session";
9
- import { Clock, Context, Effect, Layer, Option, Random, Ref, Schema, Stream } from "effect";
10
-
11
- import { ConversationObjectIdentity, DurableObjectContext } from "./bindings.ts";
12
- import { CloudflareDurableRuntimeConfig } from "./config.ts";
13
-
14
- /**
15
- * The single multiplexed Durable Object alarm (decision D-P6-2). A Durable Object has ONE
16
- * alarm slot; every cadence the Node host ran on fibers (wake scan, lease expiry, settlement
17
- * and abort re-checks, retry backoff) multiplexes into one idempotent maintenance pass, and
18
- * the slot always holds the EARLIEST deadline any caller asked for.
19
- *
20
- * The alarm invariant (plan §1.4): committed nonterminal work implies a committed alarm.
21
- * It is established by pre-arming — every mutating entry point and every pass arms the alarm
22
- * BEFORE its durable mutations — so an eviction at any failpoint leaves a persisted alarm
23
- * that workerd re-delivers to a fresh incarnation WITHOUT any incoming request. Spurious
24
- * alarms are harmless by design: a pass over an all-settled lane simply deletes the slot.
25
- */
26
-
27
- /** The Durable Object alarm API failed; surfaces on host entry points as a typed refusal. */
28
- export class DurableAlarmError extends Schema.TaggedError<DurableAlarmError>()(
29
- "DurableAlarmError",
30
- {
31
- operation: Schema.String,
32
- message: Schema.String,
33
- cause: Schema.optionalKey(Schema.Defect()),
34
- },
35
- ) {}
36
-
37
- const alarmFailure =
38
- (operation: string) =>
39
- (cause: unknown): DurableAlarmError =>
40
- DurableAlarmError.make({
41
- operation,
42
- message: cause instanceof Error ? cause.message : String(cause),
43
- cause,
44
- });
45
-
46
- /** `ctx.storage` alarm slot as an Effect service; storage is truth, never a memory field. */
47
- export class DurableAlarmService extends Context.Service<
48
- DurableAlarmService,
49
- {
50
- /** The scheduled deadline in epoch milliseconds, if any. */
51
- readonly scheduled: Effect.Effect<Option.Option<number>, DurableAlarmError>;
52
- /** Replace the slot with this deadline. */
53
- readonly scheduleAt: (epochMillis: number) => Effect.Effect<void, DurableAlarmError>;
54
- /** Keep the EARLIER of the existing deadline and this one (the multiplexing rule). */
55
- readonly ensureScheduledBy: (epochMillis: number) => Effect.Effect<void, DurableAlarmError>;
56
- /**
57
- * Arm an immediate alarm (the durable, coalescing local wake) — DEFERRED while a
58
- * maintenance pass is executing. Workerd cancels an in-flight alarm handler when a new
59
- * EARLIER deadline is written during its execution (`requestScheduledAlarm`), and the
60
- * maintenance pass runs INSIDE the alarm handler: an immediate wake landing mid-pass
61
- * (a routed port mutation, a sibling's `wake()`, the coordinator's own local notify)
62
- * would kill the running Attempt — manufacturing an ownership loss no real eviction
63
- * caused, and routing open uncertain-class Tool Calls into spurious Unknown Outcomes.
64
- * Deferral is contract-safe: wakes are droppable hints, every mutating entry point
65
- * pre-arms BEFORE its first durable mutation (the alarm invariant never rests on this
66
- * call), and the deferred wake is flushed when the pass completes.
67
- */
68
- readonly scheduleNow: Effect.Effect<void, DurableAlarmError>;
69
- /**
70
- * Run one maintenance pass with wake deferral (see `scheduleNow`): `scheduleNow` calls
71
- * while `body` executes coalesce into one flag, flushed as an immediate re-arm after the
72
- * pass — failures of the flush are logged and swallowed (hints are droppable; the pass's
73
- * own re-arm policy already bounded the next delivery).
74
- */
75
- readonly withWakesDeferred: <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
76
- /** Clear the slot; only the maintenance pass does this, and only when all work settled. */
77
- readonly cancel: Effect.Effect<void, DurableAlarmError>;
78
- }
79
- >()("@effect-agent/platform-cloudflare/DurableAlarmService") {
80
- static readonly layer: Layer.Layer<DurableAlarmService, never, DurableObjectContext> =
81
- Layer.effect(DurableAlarmService)(
82
- Effect.gen(function* () {
83
- const { ctx } = yield* DurableObjectContext;
84
- /**
85
- * In-memory pass bookkeeping — a pure CACHE, never state: a fresh incarnation has no
86
- * running pass, and a deferred wake lost to eviction was only ever a promptness hint
87
- * on top of the already-committed pre-armed alarm.
88
- */
89
- const runningPasses = yield* Ref.make(0);
90
- const wakeDeferred = yield* Ref.make(false);
91
- const scheduled = Effect.tryPromise({
92
- try: () => ctx.storage.getAlarm(),
93
- catch: alarmFailure("get alarm"),
94
- }).pipe(
95
- Effect.map((deadline) =>
96
- deadline === null ? Option.none<number>() : Option.some(deadline),
97
- ),
98
- );
99
- const scheduleAt = (epochMillis: number) =>
100
- Effect.tryPromise({
101
- try: () => ctx.storage.setAlarm(epochMillis),
102
- catch: alarmFailure("set alarm"),
103
- });
104
- const ensureScheduledBy = (epochMillis: number) =>
105
- scheduled.pipe(
106
- Effect.flatMap((existing) =>
107
- Option.isSome(existing) && existing.value <= epochMillis
108
- ? Effect.void
109
- : scheduleAt(epochMillis),
110
- ),
111
- );
112
- const armNow = Clock.currentTimeMillis.pipe(
113
- Effect.flatMap((now) => ensureScheduledBy(now)),
114
- );
115
- const scheduleNow = Ref.get(runningPasses).pipe(
116
- Effect.flatMap((passes) => (passes > 0 ? Ref.set(wakeDeferred, true) : armNow)),
117
- );
118
- /**
119
- * Flush after the LAST concurrent pass: the re-arm lands at the very end of the alarm
120
- * handler, where a superseding cancellation only re-delivers to the already-idempotent
121
- * pass. Runs inside `Effect.ensuring`, so flush failures are logged, never raised.
122
- */
123
- const flushDeferredWake = Ref.get(runningPasses).pipe(
124
- Effect.flatMap((passes) =>
125
- passes > 0
126
- ? Effect.void
127
- : Ref.getAndSet(wakeDeferred, false).pipe(
128
- Effect.flatMap((wanted) => (wanted ? armNow : Effect.void)),
129
- ),
130
- ),
131
- Effect.catch((error) =>
132
- Effect.logWarning("DurableAlarmService: deferred wake flush failed", error),
133
- ),
134
- );
135
- const withWakesDeferred = <A, E, R>(body: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
136
- Ref.update(runningPasses, (passes) => passes + 1).pipe(
137
- Effect.andThen(body),
138
- Effect.ensuring(
139
- Ref.update(runningPasses, (passes) => passes - 1).pipe(
140
- Effect.andThen(flushDeferredWake),
141
- ),
142
- ),
143
- );
144
- const cancel = Effect.tryPromise({
145
- try: () => ctx.storage.deleteAlarm(),
146
- catch: alarmFailure("delete alarm"),
147
- });
148
- return DurableAlarmService.of({
149
- scheduled,
150
- scheduleAt,
151
- ensureScheduledBy,
152
- scheduleNow,
153
- withWakesDeferred,
154
- cancel,
155
- });
156
- }),
157
- );
158
- }
159
-
160
- /** What one maintenance pass did — auditable evidence mirroring `NodeDurableHost`'s report. */
161
- export class MaintenancePassReport extends Schema.Class<MaintenancePassReport>(
162
- "@effect-agent/platform-cloudflare/MaintenancePassReport",
163
- )({
164
- /** Recovery decisions executed (or deferred) BEFORE any new claim in this pass. */
165
- recovered: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
166
- /** Settlements the drain pass finalized. */
167
- settled: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
168
- /** Submissions still nonterminal after the pass (suspended/unknown lanes stay honest). */
169
- nonterminal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
170
- /** `rearmed` while nonterminal work remains, `cleared` once everything settled. */
171
- alarm: Schema.Literals(["rearmed", "cleared"]),
172
- }) {}
173
-
174
- export type MaintenancePassFailure =
175
- | DurableWorkerFailure
176
- | DurableBindingFailure
177
- | DurableAlarmError;
178
-
179
- /**
180
- * The idempotent maintenance pass and the alarm-invariant helpers (plan §1.4, D-P6-1/2).
181
- *
182
- * `pass` = pre-arm → `runRecovery` → `processConversationResolved` → re-arm-or-clear:
183
- *
184
- * 1. **Pre-arm**: the alarm is re-armed at `now + wakeScanInterval` BEFORE any work, so an
185
- * eviction (or thrown failure → workerd alarm retry) at any point of the pass leaves a
186
- * committed alarm and the fresh incarnation converges without an incoming request.
187
- * 2. **Reconcile before new work** (exit gate): every pass classifies and repairs every
188
- * nonterminal Submission before the drain claims anything; the pure classifier and the
189
- * repair executors are idempotent, so at-least-once alarm delivery re-runs them safely.
190
- * 3. **Drain**: one bounded `processConversationResolved` pass over this Object's lane — the
191
- * D-P6-1 shape; no infinite `runResolvedWorker` loop ever pins the Object.
192
- * 4. **Re-arm policy**: nonterminal work re-arms at `now + min(backoff-with-jitter,
193
- * wakeScanInterval)` — the scan interval bounds every wait (lease expiry of a dead
194
- * incarnation included, since claims retry each pass) and the backoff (reset on progress,
195
- * grown otherwise) keeps stuck lanes from busy-spinning; all settled clears the slot.
196
- *
197
- * Every step is a durability-protocol step that already tolerates re-execution, which is
198
- * what makes double-fired alarms harmless (the alarm.test.ts gate).
199
- */
200
- export class ConversationMaintenance extends Context.Service<
201
- ConversationMaintenance,
202
- {
203
- /** One idempotent maintenance pass; failures propagate so workerd retries the alarm. */
204
- readonly pass: Effect.Effect<MaintenancePassReport, MaintenancePassFailure>;
205
- /**
206
- * Defensive half of the alarm invariant, run by the constructor gate: if any local
207
- * Submission is nonterminal and no alarm is scheduled, arm one within the scan interval.
208
- * Local-only (one ledger scan + the alarm slot) — never a recovery pass, never transport.
209
- */
210
- readonly ensureAlarm: Effect.Effect<void, MaintenancePassFailure>;
211
- /**
212
- * The pre-arm every mutating entry point runs BEFORE its first durable mutation: with
213
- * the alarm committed first, a committed admission (or any other committed nonterminal
214
- * transition) can never be observed without the alarm that will finish it.
215
- */
216
- readonly preArm: Effect.Effect<void, DurableAlarmError>;
217
- }
218
- >()("@effect-agent/platform-cloudflare/ConversationMaintenance") {
219
- static readonly layer: Layer.Layer<
220
- ConversationMaintenance,
221
- never,
222
- | DurableAgentRuntime
223
- | AgentBindingResolver
224
- | SubmissionLedger
225
- | DurableAlarmService
226
- | CloudflareDurableRuntimeConfig
227
- | ConversationObjectIdentity
228
- > = Layer.effect(ConversationMaintenance)(
229
- Effect.gen(function* () {
230
- const runtime = yield* DurableAgentRuntime;
231
- const resolver = yield* AgentBindingResolver;
232
- const ledger = yield* SubmissionLedger;
233
- const alarm = yield* DurableAlarmService;
234
- const config = yield* CloudflareDurableRuntimeConfig;
235
- const identity = yield* ConversationObjectIdentity;
236
-
237
- /**
238
- * Consecutive no-progress passes — an in-memory CACHE, not state: a fresh incarnation
239
- * restarts at zero and merely re-arms sooner than a long-lived one would have.
240
- */
241
- const stalls = yield* Ref.make(0);
242
-
243
- const preArm = Clock.currentTimeMillis.pipe(
244
- Effect.flatMap((now) => alarm.ensureScheduledBy(now + config.wakeScanInterval)),
245
- );
246
-
247
- const countNonterminal = Stream.runCollect(ledger.scanNonterminal).pipe(
248
- Effect.map((snapshots) => snapshots.length),
249
- );
250
-
251
- const rearmDelay = Effect.fn("ConversationMaintenance.rearmDelay")(function* (
252
- progressed: boolean,
253
- ) {
254
- const priorStalls = yield* Ref.getAndUpdate(stalls, (count) =>
255
- progressed ? 0 : count + 1,
256
- );
257
- if (progressed) return config.alarmBackoffBase;
258
- const exponent = Math.min(priorStalls, 30);
259
- const backoff = Math.min(config.alarmBackoffCap, config.alarmBackoffBase * 2 ** exponent);
260
- const jitter = yield* Random.next;
261
- // Full jitter over [backoff/2, backoff]: desynchronizes retry storms without ever
262
- // waiting longer than the deterministic bound.
263
- const jittered = Math.ceil(backoff / 2 + (backoff / 2) * jitter);
264
- return Math.min(jittered, config.wakeScanInterval);
265
- });
266
-
267
- const pass = Effect.fn("ConversationMaintenance.pass")(function* (): Effect.fn.Return<
268
- MaintenancePassReport,
269
- MaintenancePassFailure
270
- > {
271
- // Step 1 — pre-arm: an abort or throw anywhere below leaves a committed alarm.
272
- yield* preArm;
273
- // Step 2 — reconciliation strictly precedes new work in this pass (exit gate).
274
- const recovered: ReadonlyArray<RecoveryReport> = yield* runtime.runRecovery;
275
- // Step 3 — one bounded drain pass over this Object's own lane.
276
- const settlements = yield* runtime
277
- .processConversationResolved(identity.conversationId)
278
- .pipe(Effect.provideService(AgentBindingResolver, resolver));
279
- // Step 4 — re-arm or clear.
280
- const nonterminal = yield* countNonterminal;
281
- if (nonterminal === 0) {
282
- yield* alarm.cancel;
283
- yield* Ref.set(stalls, 0);
284
- // Close the cancel/admission interleaving window: a submission committing between
285
- // the count and the cancel (Durable Object events interleave at storage-operation
286
- // boundaries) must not be left without its alarm. The recount re-arms if anything
287
- // appeared; the submit entry's own `scheduleNow` covers commits after the recount.
288
- const appeared = yield* countNonterminal;
289
- if (appeared > 0) {
290
- yield* preArm;
291
- return MaintenancePassReport.make({
292
- recovered: recovered.length,
293
- settled: settlements.length,
294
- nonterminal: appeared,
295
- alarm: "rearmed",
296
- });
297
- }
298
- return MaintenancePassReport.make({
299
- recovered: recovered.length,
300
- settled: settlements.length,
301
- nonterminal,
302
- alarm: "cleared",
303
- });
304
- }
305
- const progressed =
306
- settlements.length > 0 || recovered.some((report) => report.disposition === "repaired");
307
- const delay = yield* rearmDelay(progressed);
308
- const now = yield* Clock.currentTimeMillis;
309
- yield* alarm.ensureScheduledBy(now + delay);
310
- return MaintenancePassReport.make({
311
- recovered: recovered.length,
312
- settled: settlements.length,
313
- nonterminal,
314
- alarm: "rearmed",
315
- });
316
- });
317
-
318
- const ensureAlarm = Effect.fn("ConversationMaintenance.ensureAlarm")(function* () {
319
- const nonterminal = yield* countNonterminal;
320
- if (nonterminal === 0) return;
321
- yield* preArm;
322
- });
323
-
324
- return ConversationMaintenance.of({
325
- // Wake deferral (see `DurableAlarmService.scheduleNow`): an immediate wake written
326
- // while THIS handler runs would make workerd cancel it mid-Attempt; deferring keeps
327
- // the running pass alive and flushes the hint as the pass's final re-arm.
328
- pass: alarm.withWakesDeferred(pass()),
329
- ensureAlarm: ensureAlarm(),
330
- preArm,
331
- });
332
- }),
333
- );
334
- }
package/src/bindings.ts DELETED
@@ -1,145 +0,0 @@
1
- import type { ConversationId } from "@effect-agent/core";
2
- import type { ProducerId } from "@effect-agent/session";
3
- import { Context, Effect, Layer, Schema } from "effect";
4
-
5
- /**
6
- * Cloudflare platform bindings as Effect services (DEPLOY-010: "Cloudflare platform bindings
7
- * are supplied as Effect services/Layers"). Application code never reads `env` or touches a
8
- * `DurableObjectState` directly — the Conversation Object class constructs these Layers once
9
- * per incarnation and everything downstream consumes the services.
10
- */
11
-
12
- /** A Cloudflare platform binding was missing or carried the wrong shape (DEPLOY-003/010). */
13
- export class CloudflareBindingError extends Schema.TaggedError<CloudflareBindingError>()(
14
- "CloudflareBindingError",
15
- {
16
- binding: Schema.String,
17
- message: Schema.String,
18
- },
19
- ) {}
20
-
21
- /**
22
- * The RPC surface one Conversation Durable Object exposes to Workers and to sibling
23
- * Conversation Objects. `makeConversationObjectClass` implements it; the Worker-side client
24
- * and the cross-Object transport call it through `DurableObjectNamespace` stubs. Every
25
- * `encoded` value is a Schema-encoded envelope (`client.ts` wire schemas for host entry
26
- * points, `@effect-agent/storage-cloudflare` port envelopes for `portCall`), so the RPC
27
- * boundary carries only structured-cloneable JSON.
28
- */
29
- export interface ConversationObjectRpc extends Rpc.DurableObjectBranded {
30
- /** Admission-limits gate + `DurableAgentRuntime.submit`; answers a `SubmitResponse`. */
31
- submitEncoded(encoded: unknown): Promise<unknown>;
32
- /** Wake-hinted, poll-guaranteed settlement wait; answers an `AwaitSettlementResponse`. */
33
- awaitSettlementEncoded(encoded: unknown): Promise<unknown>;
34
- /** One bounded page of canonical records; answers an `ObservePageResponse`. */
35
- observePage(encoded: unknown): Promise<unknown>;
36
- /** Durable abort intent; answers an `AbortResponse`. */
37
- abortEncoded(encoded: unknown): Promise<unknown>;
38
- /** Durable approval decision (plan §2.6); answers a `ResolveApprovalResponse`. */
39
- resolveApprovalEncoded(encoded: unknown): Promise<unknown>;
40
- /** Authorized DUR-017 Unknown-Outcome resolution; answers a `ResolveUnknownResponse`. */
41
- resolveUnknownEncoded(encoded: unknown): Promise<unknown>;
42
- /** Owner-side cross-Object port endpoint (WP2 envelopes, executed on LOCAL facets). */
43
- portCall(encoded: unknown): Promise<unknown>;
44
- /** Droppable liveness hint from another Object: arms an immediate alarm. */
45
- wake(): Promise<void>;
46
- }
47
-
48
- /**
49
- * The `DurableObjectNamespace` binding that addresses Conversation Objects. The Object
50
- * identity rule is `namespace.idFromName(conversationId)` (plan §1.2): Conversation IDs are
51
- * globally unique, so the mapping is total and deterministic and no directory service exists.
52
- */
53
- export class ConversationObjectNamespace extends Context.Service<
54
- ConversationObjectNamespace,
55
- {
56
- readonly namespace: DurableObjectNamespace<ConversationObjectRpc>;
57
- }
58
- >()("@effect-agent/platform-cloudflare/ConversationObjectNamespace") {
59
- static layer(
60
- namespace: DurableObjectNamespace<ConversationObjectRpc>,
61
- ): Layer.Layer<ConversationObjectNamespace> {
62
- return Layer.succeed(ConversationObjectNamespace)({ namespace });
63
- }
64
- }
65
-
66
- /**
67
- * Narrow one `env` member to a `DurableObjectNamespace`. `env` is an untyped platform value,
68
- * and a namespace binding is a host object no Schema can decode, so this is the documented
69
- * narrowest-boundary check (structural probe for the namespace surface the transport uses);
70
- * a missing or misshaped binding fails typed before any Layer is built.
71
- */
72
- export const conversationNamespaceFromEnv = (
73
- env: unknown,
74
- binding: string,
75
- ): Effect.Effect<DurableObjectNamespace<ConversationObjectRpc>, CloudflareBindingError> =>
76
- Effect.suspend(() => {
77
- if (typeof env !== "object" || env === null) {
78
- return Effect.fail(
79
- CloudflareBindingError.make({
80
- binding,
81
- message: "The Worker environment is not an object; no bindings are available.",
82
- }),
83
- );
84
- }
85
- const candidate: unknown = (env as Record<string, unknown>)[binding];
86
- if (
87
- typeof candidate === "object" &&
88
- candidate !== null &&
89
- "idFromName" in candidate &&
90
- typeof candidate.idFromName === "function" &&
91
- "get" in candidate &&
92
- typeof candidate.get === "function"
93
- ) {
94
- // The structural probe above is the entire runtime contract this package relies on;
95
- // the assertion records that `idFromName`/`get` name a DurableObjectNamespace.
96
- return Effect.succeed(candidate as DurableObjectNamespace<ConversationObjectRpc>);
97
- }
98
- return Effect.fail(
99
- CloudflareBindingError.make({
100
- binding,
101
- message:
102
- `env.${binding} is not a DurableObjectNamespace binding; declare the Conversation ` +
103
- "Object class under this binding in the Worker configuration.",
104
- }),
105
- );
106
- });
107
-
108
- /** `ConversationObjectNamespace` built from the untyped Worker `env` (fails typed). */
109
- export const conversationNamespaceLayer = (
110
- env: unknown,
111
- binding: string,
112
- ): Layer.Layer<ConversationObjectNamespace, CloudflareBindingError> =>
113
- Layer.effect(ConversationObjectNamespace)(
114
- Effect.map(conversationNamespaceFromEnv(env, binding), (namespace) => ({ namespace })),
115
- );
116
-
117
- /**
118
- * The live Durable Object execution context of THIS incarnation. Only Layer construction and
119
- * the alarm service consume it; important state never lives on it (`ctx.storage` is truth,
120
- * everything in memory is a cache — deployment spec §11).
121
- */
122
- export class DurableObjectContext extends Context.Service<
123
- DurableObjectContext,
124
- {
125
- readonly ctx: DurableObjectState;
126
- readonly env: unknown;
127
- }
128
- >()("@effect-agent/platform-cloudflare/DurableObjectContext") {
129
- static layer(ctx: DurableObjectState, env: unknown): Layer.Layer<DurableObjectContext> {
130
- return Layer.succeed(DurableObjectContext)({ ctx, env });
131
- }
132
- }
133
-
134
- /**
135
- * The Conversation identity this Object serializes and the producer identity its Attempts
136
- * write with (`{producerPrefix}:{conversationId}`, plan §1.4). Derived once per incarnation
137
- * from `ctx.id.name` — the Object identity rule guarantees the name IS the Conversation ID.
138
- */
139
- export class ConversationObjectIdentity extends Context.Service<
140
- ConversationObjectIdentity,
141
- {
142
- readonly conversationId: ConversationId;
143
- readonly producerId: ProducerId;
144
- }
145
- >()("@effect-agent/platform-cloudflare/ConversationObjectIdentity") {}