@effect-agent/platform-cloudflare 0.1.0-beta.120 → 0.1.0-beta.122

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.
@@ -705,20 +705,17 @@ declare const encodeAdminResponse: (input: AdminFailed | ExplainedRecovery | Obl
705
705
  } | {
706
706
  readonly _tag: "AdminFailed";
707
707
  readonly failure: {
708
- readonly _tag: "ThreadNotMaterialized";
709
- readonly threadId: string;
710
- } | {
711
- readonly _tag: "ThreadStoreError";
708
+ readonly _tag: "DurableAlarmError";
712
709
  readonly operation: string;
713
710
  readonly message: string;
714
711
  readonly cause?: Schema.Json | undefined;
715
- readonly diagnostic?: {
716
- readonly causeTag: string;
717
- readonly operation: string;
718
- readonly decoder?: string | undefined;
719
- readonly issueTag?: string | undefined;
720
- readonly sequence?: number | undefined;
721
- } | undefined;
712
+ } | {
713
+ readonly _tag: "HostProtocolError";
714
+ readonly message: string;
715
+ } | {
716
+ readonly _tag: "DigestError";
717
+ readonly message: string;
718
+ readonly cause?: Schema.Json | undefined;
722
719
  } | {
723
720
  readonly _tag: "AdmissionPolicyError";
724
721
  readonly reason: "occupied" | "refused" | "unavailable";
@@ -734,6 +731,21 @@ declare const encodeAdminResponse: (input: AdminFailed | ExplainedRecovery | Obl
734
731
  readonly operation: string;
735
732
  readonly message: string;
736
733
  readonly cause?: import("effect-agent/failure-diagnostic").Diagnostic | undefined;
734
+ } | {
735
+ readonly _tag: "ThreadStoreError";
736
+ readonly operation: string;
737
+ readonly message: string;
738
+ readonly cause?: Schema.Json | undefined;
739
+ readonly diagnostic?: {
740
+ readonly causeTag: string;
741
+ readonly operation: string;
742
+ readonly decoder?: string | undefined;
743
+ readonly issueTag?: string | undefined;
744
+ readonly sequence?: number | undefined;
745
+ } | undefined;
746
+ } | {
747
+ readonly _tag: "ThreadNotMaterialized";
748
+ readonly threadId: string;
737
749
  } | {
738
750
  readonly _tag: "AppendConflict";
739
751
  readonly threadId: string;
@@ -746,30 +758,18 @@ declare const encodeAdminResponse: (input: AdminFailed | ExplainedRecovery | Obl
746
758
  readonly threadId: string;
747
759
  readonly actualEpoch: number;
748
760
  readonly attemptedEpoch: number;
749
- } | {
750
- readonly _tag: "DurableAlarmError";
751
- readonly operation: string;
752
- readonly message: string;
753
- readonly cause?: Schema.Json | undefined;
754
- } | {
755
- readonly _tag: "HostProtocolError";
756
- readonly message: string;
757
- } | {
758
- readonly _tag: "DigestError";
759
- readonly message: string;
760
- readonly cause?: Schema.Json | undefined;
761
761
  } | {
762
762
  readonly _tag: "DurableRuntimeFailpointError";
763
763
  readonly location: "abort:after-intent" | "approval:after-request-append" | "approval:after-suspend" | "checkpoint:after-save" | "checkpoint:before-save" | "claim:after-claim" | "compaction:after-canonical-append" | "compaction:before-canonical-append" | "input:after-canonical-append" | "join:after-canonical-append" | "join:after-claim" | "policy:after-reservation-append" | "policy:before-reservation-append" | "resolve:after-intent" | "run:after-duration-append" | "run:after-start-append" | "run:before-duration-append" | "run:before-start-append" | "step:after-step-append" | "subagent:after-admit" | "subagent:after-child-abort-intent" | "subagent:after-child-ready" | "subagent:after-join-append" | "subagent:after-release" | "subagent:after-release-pending" | "subagent:after-request-append" | "subagent:after-reserve" | "subagent:after-sibling-settle" | "subagent:after-start-append" | "subagent:after-suspend" | "subagent:before-join-append" | "submit:after-admit" | "submit:after-materialize" | "terminalize:after-canonical-append" | "terminalize:after-reserve" | "tools:after-prepared-append" | "tools:after-unavailable-append" | "tools:before-prepared-append" | "tools:before-unavailable-append" | "turn:after-canonical-append" | "turn:after-response-append" | "turn:after-results-append" | "update:after-canonical-append" | "update:after-delivery-insert" | "update:before-canonical-append" | "update:before-delivery-insert" | "worker:after-completion-append" | "worker:after-origin-append" | "worker:after-report-append" | "worker:after-report-delivery" | "worker:after-source-append" | "worker:after-stop-append" | "worker:after-stop-seal" | "worker:after-subtree-append" | "worker:before-completion-append" | "worker:before-origin-append" | "worker:before-report-append" | "worker:before-report-delivery" | "worker:before-source-append" | "worker:before-stop-append" | "worker:before-stop-seal" | "worker:before-subtree-append";
764
- } | {
765
- readonly _tag: "ApprovalSuspensionError";
766
- readonly cause: Schema.Json;
767
764
  } | {
768
765
  readonly _tag: "OperationDenied";
769
766
  readonly operation: "abort" | "awaitSettlement" | "explain" | "observe" | "resolveApproval" | "resolveUnknown" | "retry" | "scanObligations" | "verify" | "wake";
770
767
  readonly reason: string;
771
768
  readonly threadId?: string | undefined;
772
769
  readonly submissionId?: string | undefined;
770
+ } | {
771
+ readonly _tag: "ApprovalSuspensionError";
772
+ readonly cause: Schema.Json;
773
773
  } | {
774
774
  readonly _tag: "RetryRefused";
775
775
  readonly submissionId: string;
@@ -836,4 +836,4 @@ interface Class<EventServices = never> {
836
836
  declare const make: <ApplicationServices, ApplicationError, EventServices = never, EventLayerError = never>(applicationLayer: Layer.Layer<CloudflareDurableRuntimeServices | ApplicationServices, ApplicationError, CloudflareBootstrapServices | DurableObjectState$1.DurableObjectState | WorkerEnvironment | DurableObjectContext | ThreadObjectNamespace>, options: Options<ApplicationServices, EventServices, EventLayerError>) => Class<ApplicationServices | EventServices>;
837
837
  //#endregion
838
838
  export { ThreadPublicationOptions as A, portCall as C, CloudflareDurableRuntimeOptions as D, CloudflareDurableRuntimeInitializationError as E, layerConfig as M, layerHostConfig as N, CloudflareDurableRuntimeServices as O, layerInHost as P, make as S, CloudflareBootstrapServices as T, decodeAdminVerifyRequest as _, AdminVerifyRequest as a, encodeAdminResponse as b, Instance as c, RetryExecuted as d, ThreadObject_d_exports as f, decodeAdminResponse as g, decodeAdminExplainRequest as h, AdminResponse as i, layer as j, ThreadObjectPorts as k, ObligationsScanned as l, VerifiedIntegrity as m, AdminFailed as n, Class as o, ThreadRpcOperation as p, AdminFailure as r, ExplainedRecovery as s, AdminExplainRequest as t, Options as u, decodeObligationThresholds as v, submit as w, handleRpc as x, decodeRetryCommand as y };
839
- //# sourceMappingURL=ThreadObject-9IVibJM7.d.mts.map
839
+ //# sourceMappingURL=ThreadObject-mt0uxvZZ.d.mts.map
@@ -1,2 +1,2 @@
1
- import { A as ThreadPublicationOptions, C as portCall, D as CloudflareDurableRuntimeOptions, E as CloudflareDurableRuntimeInitializationError, M as layerConfig, N as layerHostConfig, O as CloudflareDurableRuntimeServices, P as layerInHost, S as make, T as CloudflareBootstrapServices, _ as decodeAdminVerifyRequest, a as AdminVerifyRequest, b as encodeAdminResponse, c as Instance, d as RetryExecuted, g as decodeAdminResponse, h as decodeAdminExplainRequest, i as AdminResponse, j as layer, k as ThreadObjectPorts, l as ObligationsScanned, m as VerifiedIntegrity, n as AdminFailed, o as Class, p as ThreadRpcOperation, r as AdminFailure, s as ExplainedRecovery, t as AdminExplainRequest, u as Options, v as decodeObligationThresholds, w as submit, x as handleRpc, y as decodeRetryCommand } from "./ThreadObject-9IVibJM7.mjs";
1
+ import { A as ThreadPublicationOptions, C as portCall, D as CloudflareDurableRuntimeOptions, E as CloudflareDurableRuntimeInitializationError, M as layerConfig, N as layerHostConfig, O as CloudflareDurableRuntimeServices, P as layerInHost, S as make, T as CloudflareBootstrapServices, _ as decodeAdminVerifyRequest, a as AdminVerifyRequest, b as encodeAdminResponse, c as Instance, d as RetryExecuted, g as decodeAdminResponse, h as decodeAdminExplainRequest, i as AdminResponse, j as layer, k as ThreadObjectPorts, l as ObligationsScanned, m as VerifiedIntegrity, n as AdminFailed, o as Class, p as ThreadRpcOperation, r as AdminFailure, s as ExplainedRecovery, t as AdminExplainRequest, u as Options, v as decodeObligationThresholds, w as submit, x as handleRpc, y as decodeRetryCommand } from "./ThreadObject-mt0uxvZZ.mjs";
2
2
  export { AdminExplainRequest, AdminFailed, AdminFailure, AdminResponse, AdminVerifyRequest, type CloudflareBootstrapServices as BootstrapServices, Class, ExplainedRecovery, type CloudflareDurableRuntimeInitializationError as InitializationError, Instance, ObligationsScanned, Options, type ThreadPublicationOptions as PublicationOptions, RetryExecuted, type CloudflareDurableRuntimeOptions as RuntimeOptions, type CloudflareDurableRuntimeServices as Services, ThreadObjectPorts, ThreadRpcOperation, VerifiedIntegrity, decodeAdminExplainRequest, decodeAdminResponse, decodeAdminVerifyRequest, decodeObligationThresholds, decodeRetryCommand, encodeAdminResponse, handleRpc, layer, layerConfig, layerHostConfig, layerInHost, make, portCall, submit };
package/dist/index.d.mts CHANGED
@@ -8,6 +8,6 @@ import { t as CloudflareMemory_d_exports } from "./CloudflareMemory.mjs";
8
8
  import { t as CloudflareScheduling_d_exports } from "./CloudflareScheduling.mjs";
9
9
  import { t as CloudflareSubscriptions_d_exports } from "./CloudflareSubscriptions.mjs";
10
10
  import { t as CloudflareThreadClient_d_exports } from "./CloudflareThreadClient.mjs";
11
- import { f as ThreadObject_d_exports } from "./ThreadObject-9IVibJM7.mjs";
11
+ import { f as ThreadObject_d_exports } from "./ThreadObject-mt0uxvZZ.mjs";
12
12
  import { t as WakeScheduler_d_exports } from "./WakeScheduler.mjs";
13
13
  export { Alarm_d_exports as Alarm, CloudflareAiGateway_d_exports as CloudflareAiGateway, CloudflareBindings_d_exports as CloudflareBindings, CloudflareBrowser_d_exports as CloudflareBrowser, CloudflareCodeMode_d_exports as CloudflareCodeMode, CloudflareConfig_d_exports as CloudflareConfig, CloudflareMemory_d_exports as CloudflareMemory, CloudflareScheduling_d_exports as CloudflareScheduling, CloudflareSubscriptions_d_exports as CloudflareSubscriptions, CloudflareThreadClient_d_exports as CloudflareThreadClient, ThreadObject_d_exports as ThreadObject, WakeScheduler_d_exports as WakeScheduler };
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@effect-agent/platform-cloudflare","version":"0.1.0-beta.120","dependencies":{"@effect-agent/storage-cloudflare":"0.1.0-beta.120","@effect/platform-browser":"4.0.0-rc.116","@effect/sql-sqlite-do":"4.0.0-rc.116","effect-agent":"0.1.0-beta.120"},"devDependencies":{"@cloudflare/puppeteer":"1.1.0","@cloudflare/vitest-pool-workers":"0.21.3","@cloudflare/workers-types":"5.20260825.1","@effect-agent/testing":"0.1.0-beta.120","@effect/platform-node":"4.0.0-rc.116","@effect/sql-d1":"4.0.0-rc.116","@effect/vitest":"4.0.0-rc.116","effect":"4.0.0-rc.116","effect-cf":"0.44.1","esbuild":"0.28.1","miniflare":"5.20260811.1-alpha","typescript":"7.0.2","vite-plus":"0.3.2","vitest":"4.1.11"},"peerDependencies":{"@cloudflare/puppeteer":"^1.1.0","effect":"^4.0.0-rc.116","effect-cf":"^0.44.1 || ^0.45.0"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./alarm":{"types":"./dist/Alarm.d.mts","default":"./dist/Alarm.mjs"},"./browser-rest-capture":{"types":"./dist/BrowserRestCapture.d.mts","default":"./dist/BrowserRestCapture.mjs"},"./browser-rest-crawl":{"types":"./dist/BrowserRestCrawl.d.mts","default":"./dist/BrowserRestCrawl.mjs"},"./cloudflare-bindings":{"types":"./dist/CloudflareBindings.d.mts","default":"./dist/CloudflareBindings.mjs"},"./cloudflare-browser":{"types":"./dist/CloudflareBrowser.d.mts","default":"./dist/CloudflareBrowser.mjs"},"./cloudflare-code-mode":{"types":"./dist/CloudflareCodeMode.d.mts","default":"./dist/CloudflareCodeMode.mjs"},"./cloudflare-config":{"types":"./dist/CloudflareConfig.d.mts","default":"./dist/CloudflareConfig.mjs"},"./cloudflare-memory":{"types":"./dist/CloudflareMemory.d.mts","default":"./dist/CloudflareMemory.mjs"},"./cloudflare-scheduling":{"types":"./dist/CloudflareScheduling.d.mts","default":"./dist/CloudflareScheduling.mjs"},"./cloudflare-subscriptions":{"types":"./dist/CloudflareSubscriptions.d.mts","default":"./dist/CloudflareSubscriptions.mjs"},"./cloudflare-thread-client":{"types":"./dist/CloudflareThreadClient.d.mts","default":"./dist/CloudflareThreadClient.mjs"},"./interactive-browser":{"types":"./dist/InteractiveBrowser.d.mts","default":"./dist/InteractiveBrowser.mjs"},"./protected-browser":{"types":"./dist/ProtectedBrowser.d.mts","default":"./dist/ProtectedBrowser.mjs"},"./thread-object":{"types":"./dist/ThreadObject.d.mts","default":"./dist/ThreadObject.mjs"},"./wake-scheduler":{"types":"./dist/WakeScheduler.d.mts","default":"./dist/WakeScheduler.mjs"},"./cloudflare-ai-gateway":{"types":"./dist/CloudflareAiGateway.d.mts","default":"./dist/CloudflareAiGateway.mjs"}},"description":"Cloudflare Layer assembly for Effect Agent: Durable Objects and Browser Run adapters.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/danieljvdm/effect-agent.git","directory":"packages/platform-cloudflare"},"files":["dist","src"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json"},"peerDependenciesMeta":{"@cloudflare/puppeteer":{"optional":true}}}
1
+ {"name":"@effect-agent/platform-cloudflare","version":"0.1.0-beta.122","dependencies":{"@effect-agent/storage-cloudflare":"0.1.0-beta.122","@effect/platform-browser":"4.0.0-rc.116","@effect/sql-sqlite-do":"4.0.0-rc.116","effect-agent":"0.1.0-beta.122","puppeteer-core":"24.29.0"},"devDependencies":{"@cloudflare/vitest-pool-workers":"0.21.3","@cloudflare/workers-types":"5.20260825.1","@effect-agent/testing":"0.1.0-beta.122","@effect/platform-node":"4.0.0-rc.116","@effect/sql-d1":"4.0.0-rc.116","@effect/vitest":"4.0.0-rc.116","effect":"4.0.0-rc.116","effect-cf":"0.44.1","esbuild":"0.28.1","miniflare":"5.20260811.1-alpha","typescript":"7.0.2","vite-plus":"0.3.2","vitest":"4.1.11"},"peerDependencies":{"effect":"^4.0.0-rc.116","effect-cf":"^0.44.1 || ^0.45.0"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./alarm":{"types":"./dist/Alarm.d.mts","default":"./dist/Alarm.mjs"},"./browser-rest-capture":{"types":"./dist/BrowserRestCapture.d.mts","default":"./dist/BrowserRestCapture.mjs"},"./browser-rest-crawl":{"types":"./dist/BrowserRestCrawl.d.mts","default":"./dist/BrowserRestCrawl.mjs"},"./cloudflare-bindings":{"types":"./dist/CloudflareBindings.d.mts","default":"./dist/CloudflareBindings.mjs"},"./cloudflare-browser":{"types":"./dist/CloudflareBrowser.d.mts","default":"./dist/CloudflareBrowser.mjs"},"./cloudflare-code-mode":{"types":"./dist/CloudflareCodeMode.d.mts","default":"./dist/CloudflareCodeMode.mjs"},"./cloudflare-config":{"types":"./dist/CloudflareConfig.d.mts","default":"./dist/CloudflareConfig.mjs"},"./cloudflare-memory":{"types":"./dist/CloudflareMemory.d.mts","default":"./dist/CloudflareMemory.mjs"},"./cloudflare-scheduling":{"types":"./dist/CloudflareScheduling.d.mts","default":"./dist/CloudflareScheduling.mjs"},"./cloudflare-subscriptions":{"types":"./dist/CloudflareSubscriptions.d.mts","default":"./dist/CloudflareSubscriptions.mjs"},"./cloudflare-thread-client":{"types":"./dist/CloudflareThreadClient.d.mts","default":"./dist/CloudflareThreadClient.mjs"},"./interactive-browser":{"types":"./dist/InteractiveBrowser.d.mts","default":"./dist/InteractiveBrowser.mjs"},"./protected-browser":{"types":"./dist/ProtectedBrowser.d.mts","default":"./dist/ProtectedBrowser.mjs"},"./thread-object":{"types":"./dist/ThreadObject.d.mts","default":"./dist/ThreadObject.mjs"},"./wake-scheduler":{"types":"./dist/WakeScheduler.d.mts","default":"./dist/WakeScheduler.mjs"},"./cloudflare-ai-gateway":{"types":"./dist/CloudflareAiGateway.d.mts","default":"./dist/CloudflareAiGateway.mjs"}},"description":"Cloudflare Layer assembly for Effect Agent: Durable Objects and Browser Run adapters.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/danieljvdm/effect-agent.git","directory":"packages/platform-cloudflare"},"files":["dist","src"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json"}}
@@ -1,12 +1,5 @@
1
1
  /// <reference types="@cloudflare/workers-types" />
2
2
 
3
- import {
4
- type Browser,
5
- type BrowserContext,
6
- type CDPSession,
7
- type HTTPRequest,
8
- type Page,
9
- } from "@cloudflare/puppeteer";
10
3
  import {
11
4
  Cause,
12
5
  Context,
@@ -47,6 +40,13 @@ import {
47
40
  } from "effect-agent/interactive-browser";
48
41
  import { PageScreenshotResult } from "effect-agent/page-screenshot";
49
42
  import { SandboxImplementation } from "effect-agent/sandbox";
43
+ import {
44
+ type Browser,
45
+ type BrowserContext,
46
+ type CDPSession,
47
+ type HTTPRequest,
48
+ type Page,
49
+ } from "puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js";
50
50
 
51
51
  import { BrowserRunBinding } from "./internal/browser-binding.ts";
52
52
  import {
@@ -380,7 +380,10 @@ export class BrowserRunInteractiveBinding extends Context.Service<
380
380
  BrowserRunInteractiveBinding,
381
381
  {
382
382
  readonly acquire: (keepAliveMillis: number) => Promise<unknown>;
383
- readonly connect: (sessionId: string) => Promise<BrowserRunInteractiveBrowser>;
383
+ readonly connect: (
384
+ sessionId: string,
385
+ signal?: AbortSignal,
386
+ ) => Promise<BrowserRunInteractiveBrowser>;
384
387
  /** Success proves whole-browser termination or exact-session absence. */
385
388
  readonly closeSession: (
386
389
  sessionId: Redacted.Redacted<string>,
@@ -406,9 +409,10 @@ export class BrowserRunInteractiveBinding extends Context.Service<
406
409
  return {
407
410
  acquire: async (keepAliveMillis: number) =>
408
411
  binding.acquire(keepAliveMillis, "interactive.acquire"),
409
- connect: async (sessionId: string) =>
412
+ connect: async (sessionId: string, signal?: AbortSignal) =>
410
413
  makeProductionBrowser(
411
- await binding.connect(sessionId, "interactive.connect"),
414
+ await binding.connect(sessionId, "interactive.connect", signal).browser,
415
+ sessionId,
412
416
  viewport,
413
417
  ),
414
418
  closeSession: (sessionId: Redacted.Redacted<string>) =>
@@ -1385,6 +1389,7 @@ const makeProductionContext = (
1385
1389
 
1386
1390
  const makeProductionBrowser = (
1387
1391
  browser: Browser,
1392
+ sessionId: string,
1388
1393
  viewport?: BrowserRunViewport,
1389
1394
  ): BrowserRunInteractiveBrowser => ({
1390
1395
  detach: () => browser.disconnect(),
@@ -1417,7 +1422,7 @@ const makeProductionBrowser = (
1417
1422
  if (!isRemoteClosure(cause)) throw cause;
1418
1423
  }
1419
1424
  },
1420
- sessionId: () => browser.sessionId(),
1425
+ sessionId: () => sessionId,
1421
1426
  isConnected: () => browser.isConnected(),
1422
1427
  onDisconnected: (listener) => {
1423
1428
  browser.on("disconnected", listener);
@@ -2673,7 +2678,7 @@ const makeHostService = (
2673
2678
  signal,
2674
2679
  closeLateAcquisition(
2675
2680
  signal,
2676
- () => binding.connect(sessionIdValue),
2681
+ () => binding.connect(sessionIdValue, signal),
2677
2682
  (acquired) =>
2678
2683
  runCleanup(
2679
2684
  resume === undefined
@@ -1,11 +1,39 @@
1
1
  /// <reference types="@cloudflare/workers-types" />
2
- import puppeteer, { type Browser } from "@cloudflare/puppeteer";
3
- import { Context, Layer, Schema } from "effect";
2
+ import { Cause, Context, Effect, Layer, Schema } from "effect";
3
+ import puppeteer, {
4
+ type Browser,
5
+ type ConnectionTransport,
6
+ } from "puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js";
4
7
 
5
- import { browserFailure, BrowserRunFailure } from "./browser-failure.ts";
8
+ import {
9
+ browserFailure,
10
+ BrowserRunFailure,
11
+ reportBrowserCause,
12
+ reportedBrowserError,
13
+ } from "./browser-failure.ts";
6
14
 
7
15
  const Acquired = Schema.Struct({ sessionId: Schema.String.check(Schema.isUUID()) });
8
16
 
17
+ const VersionReply = Schema.fromJsonString(
18
+ Schema.Struct({
19
+ id: Schema.optionalKey(Schema.Int),
20
+ result: Schema.optionalKey(Schema.Unknown),
21
+ error: Schema.optionalKey(Schema.Unknown),
22
+ }),
23
+ );
24
+
25
+ const Version = Schema.Struct({
26
+ product: Schema.NonEmptyString,
27
+ protocolVersion: Schema.NonEmptyString,
28
+ });
29
+
30
+ /** Private attachment ownership is available before asynchronous SDK initialization completes. */
31
+ export interface BrowserRunAttachment {
32
+ readonly browser: Promise<Browser>;
33
+ /** Fence SDK dispatch/callbacks and await local socket closure; never terminate the provider. */
34
+ readonly retire: Effect.Effect<void, BrowserRunFailure>;
35
+ }
36
+
9
37
  /** Native transport port shared by both scoped adapters; the Layer owns the foreign binding. */
10
38
  export class BrowserRunBinding extends Context.Service<
11
39
  BrowserRunBinding,
@@ -14,10 +42,42 @@ export class BrowserRunBinding extends Context.Service<
14
42
  keepAliveMillis: number,
15
43
  operation: "protected.acquire" | "interactive.acquire",
16
44
  ) => Promise<string>;
17
- readonly connect: (sessionId: string, operation: string) => Promise<Browser>;
45
+ readonly connect: (
46
+ sessionId: string,
47
+ operation: string,
48
+ signal?: AbortSignal,
49
+ ) => BrowserRunAttachment;
50
+ readonly keepAlive: (sessionId: string) => Effect.Effect<void, BrowserRunFailure>;
18
51
  }
19
52
  >()("@effect-agent/platform-cloudflare/internal/BrowserRunBinding") {
20
53
  static layer(browser: Pick<BrowserRun, "fetch">) {
54
+ const upgrade = async (sessionId: string, operation: string, signal?: AbortSignal) => {
55
+ const response = await browser.fetch(
56
+ `https://browser-rendering.cloudflare.com/v1/devtools/browser/${sessionId}`,
57
+ { headers: { Upgrade: "websocket" }, ...(signal === undefined ? {} : { signal }) },
58
+ );
59
+
60
+ if (response.status !== 101) {
61
+ const failure = new BrowserRunFailure({
62
+ operation,
63
+ reason: "provider",
64
+ status: response.status,
65
+ });
66
+
67
+ try {
68
+ await response.body?.cancel();
69
+ } catch {
70
+ // Preserve the provider refusal when local response release also fails.
71
+ }
72
+ throw failure;
73
+ }
74
+ const socket = response.webSocket;
75
+
76
+ if (socket === null) throw new BrowserRunFailure({ operation, reason: "malformed" });
77
+
78
+ return socket;
79
+ };
80
+
21
81
  return Layer.succeed(this)({
22
82
  // One allocation path, with recording explicitly disabled for private sessions.
23
83
  acquire: async (keepAliveMillis, operation) => {
@@ -43,40 +103,218 @@ export class BrowserRunBinding extends Context.Service<
43
103
 
44
104
  return Schema.decodeUnknownSync(Acquired)(await response.json()).sessionId;
45
105
  },
46
- // The SDK embeds bodies and session IDs in errors; retain status before that loss.
47
- connect: async (sessionId, operation) => {
48
- let failure: BrowserRunFailure | undefined;
106
+ connect: (sessionId, operation, signal) => {
107
+ let socket: WebSocket | undefined;
108
+ let retired = false;
109
+ let closedByAbort = false;
110
+ let retirementFailure: BrowserRunFailure | undefined;
49
111
 
50
- try {
51
- return await puppeteer.connect(
52
- {
53
- fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
112
+ const disconnectOperation =
113
+ operation === "protected.connect" ? "protected.disconnect" : "interactive.disconnect";
114
+
115
+ const transport: ConnectionTransport = {
116
+ send: (message) => {
117
+ if (retired || socket === undefined)
118
+ throw new BrowserRunFailure({ operation, reason: "provider" });
119
+ socket.send(message);
120
+ },
121
+ close: () => retireNow(),
122
+ };
123
+
124
+ const onMessage = (event: MessageEvent) => {
125
+ if (!retired) transport.onmessage?.(event.data);
126
+ };
127
+
128
+ const fence = () => {
129
+ if (retired) return;
130
+ retired = true;
131
+ const notify = transport.onclose;
132
+
133
+ transport.onmessage = undefined;
134
+ transport.onclose = undefined;
135
+ // Notify the SDK before touching the socket: disposal must reject pending CDP calls
136
+ // even when the raw transport cannot close. The send fence also covers queued work.
137
+ try {
138
+ notify?.();
139
+ } finally {
140
+ socket?.removeEventListener("message", onMessage);
141
+ }
142
+ };
143
+
144
+ const retireNow = () => {
145
+ try {
146
+ fence();
147
+ } catch (cause) {
148
+ retirementFailure ??= browserFailure(disconnectOperation, cause);
149
+ }
150
+ try {
151
+ if (socket !== undefined && socket.readyState < WebSocket.CLOSING) socket.close();
152
+ } catch (cause) {
153
+ retirementFailure ??= browserFailure(disconnectOperation, cause);
154
+ }
155
+ if (retirementFailure !== undefined) throw retirementFailure;
156
+ };
157
+
158
+ const abort = () => {
159
+ closedByAbort = socket?.readyState === WebSocket.OPEN;
160
+ try {
161
+ retireNow();
162
+ } catch {
163
+ // The attachment owner observes this separately from the initiating failure.
164
+ }
165
+ };
166
+
167
+ signal?.addEventListener("abort", abort, { once: true });
168
+
169
+ const initialized = (async () => {
170
+ try {
171
+ signal?.throwIfAborted();
172
+ socket = await upgrade(sessionId, operation, signal);
173
+ socket.accept();
174
+ if (retired || signal?.aborted) {
175
+ // No SDK command may escape a late upgrade. Preserve a late local-close
176
+ // failure independently of the cancellation already returned to the owner.
177
+ retireNow();
178
+ signal?.throwIfAborted();
179
+ throw new BrowserRunFailure({ operation, reason: "provider" });
180
+ }
181
+ socket.addEventListener("message", onMessage);
182
+ socket.addEventListener(
183
+ "close",
184
+ () => {
54
185
  try {
55
- const response = await browser.fetch(input, init);
56
-
57
- if (response.status !== 101) {
58
- failure = new BrowserRunFailure({
59
- operation,
60
- reason: "provider",
61
- status: response.status,
62
- });
63
- await response.body?.cancel();
64
- throw failure;
65
- }
66
-
67
- return response;
186
+ fence();
68
187
  } catch (cause) {
69
- failure ??= browserFailure(operation, cause);
70
- throw failure;
188
+ retirementFailure ??= browserFailure(disconnectOperation, cause);
71
189
  }
72
190
  },
73
- },
74
- sessionId,
75
- );
76
- } catch (cause) {
77
- throw failure ?? browserFailure(operation, cause);
78
- }
191
+ { once: true },
192
+ );
193
+
194
+ return await puppeteer.connect({ transport });
195
+ } catch (cause) {
196
+ try {
197
+ retireNow();
198
+ } catch {
199
+ // Preserve both facts: initialization failed, and retirement may be unconfirmed.
200
+ }
201
+ const failure = browserFailure(operation, cause);
202
+
203
+ const localCancellation =
204
+ cause instanceof Error &&
205
+ signal?.aborted === true &&
206
+ (cause === signal.reason || (closedByAbort && cause.name === "TargetCloseError"));
207
+
208
+ throw localCancellation ? reportedBrowserError(failure) : failure;
209
+ } finally {
210
+ signal?.removeEventListener("abort", abort);
211
+ }
212
+ })();
213
+
214
+ return {
215
+ browser: initialized,
216
+ retire: Effect.callback<void, BrowserRunFailure>((resume) => {
217
+ const connected = socket;
218
+ const onClose = () => resume(Effect.void);
219
+
220
+ try {
221
+ retireNow();
222
+ if (connected === undefined || connected.readyState === WebSocket.CLOSED)
223
+ resume(Effect.void);
224
+ else connected.addEventListener("close", onClose, { once: true });
225
+ } catch (cause) {
226
+ resume(Effect.fail(browserFailure(disconnectOperation, cause)));
227
+ }
228
+
229
+ return Effect.sync(() => connected?.removeEventListener("close", onClose));
230
+ }),
231
+ };
79
232
  },
233
+ keepAlive: Effect.fnUntraced(function* (sessionId: string) {
234
+ const operation = "protected.keepAlive";
235
+ const runReport = Effect.runPromiseWith(yield* Effect.context<never>());
236
+
237
+ const release = (socket: WebSocket) =>
238
+ Effect.sync(() => socket.close()).pipe(
239
+ Effect.catchCause((cause) => reportBrowserCause(operation, cause)),
240
+ );
241
+
242
+ const socket = yield* Effect.acquireRelease(
243
+ Effect.tryPromise({
244
+ try: async (signal) => {
245
+ let acquired: WebSocket | undefined;
246
+
247
+ try {
248
+ acquired = await upgrade(sessionId, operation, signal);
249
+ acquired.accept();
250
+ // The binding may resolve an upgrade after the Effect was interrupted.
251
+ signal.throwIfAborted();
252
+
253
+ return acquired;
254
+ } catch (cause) {
255
+ if (acquired !== undefined) await runReport(release(acquired));
256
+ if (signal.aborted && cause !== signal.reason)
257
+ await runReport(reportBrowserCause(operation, Cause.fail(cause)));
258
+ throw cause;
259
+ }
260
+ },
261
+ catch: (cause) => browserFailure(operation, cause),
262
+ }),
263
+ release,
264
+ { interruptible: true },
265
+ );
266
+
267
+ yield* Effect.callback<void, BrowserRunFailure>((resume) => {
268
+ const finish = (result: Effect.Effect<void, BrowserRunFailure>) => {
269
+ removeListeners();
270
+ resume(result);
271
+ };
272
+
273
+ const fail = (reason: BrowserRunFailure["reason"]) =>
274
+ finish(Effect.fail(new BrowserRunFailure({ operation, reason })));
275
+
276
+ const onMessage = (event: MessageEvent) => {
277
+ try {
278
+ if (typeof event.data !== "string" || event.data.length > 16_384) {
279
+ fail("malformed");
280
+
281
+ return;
282
+ }
283
+ const reply = Schema.decodeSync(VersionReply)(event.data);
284
+
285
+ if (reply.id !== 1) return;
286
+ if (reply.error !== undefined) {
287
+ fail("provider");
288
+
289
+ return;
290
+ }
291
+ Schema.decodeUnknownSync(Version)(reply.result);
292
+ finish(Effect.void);
293
+ } catch {
294
+ fail("malformed");
295
+ }
296
+ };
297
+
298
+ const onClose = () => fail("provider");
299
+
300
+ const removeListeners = () => {
301
+ socket.removeEventListener("message", onMessage);
302
+ socket.removeEventListener("close", onClose);
303
+ socket.removeEventListener("error", onClose);
304
+ };
305
+
306
+ socket.addEventListener("message", onMessage);
307
+ socket.addEventListener("close", onClose);
308
+ socket.addEventListener("error", onClose);
309
+ try {
310
+ socket.send(JSON.stringify({ id: 1, method: "Browser.getVersion" }));
311
+ } catch (cause) {
312
+ finish(Effect.fail(browserFailure(operation, cause)));
313
+ }
314
+
315
+ return Effect.sync(removeListeners);
316
+ });
317
+ }, Effect.scoped),
80
318
  });
81
319
  }
82
320
  }
@@ -1,6 +1,6 @@
1
- import type { Page } from "@cloudflare/puppeteer";
2
1
  import { Encoding } from "effect";
3
2
  import type { BrowserSelectFileRequest } from "effect-agent/interactive-browser";
3
+ import type { Page } from "puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js";
4
4
 
5
5
  type Element = NonNullable<Awaited<ReturnType<Page["$"]>>>;
6
6