@effect-agent/platform-cloudflare 0.1.0-beta.13 → 0.1.0-beta.131

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 (106) hide show
  1. package/dist/Alarm.d.mts +244 -0
  2. package/dist/Alarm.mjs +947 -0
  3. package/dist/Alarm.mjs.map +1 -0
  4. package/dist/BrowserCredentials.d.mts +91 -0
  5. package/dist/BrowserCredentials.mjs +120 -0
  6. package/dist/BrowserCredentials.mjs.map +1 -0
  7. package/dist/BrowserRestCapture.d.mts +34 -0
  8. package/dist/BrowserRestCapture.mjs +238 -0
  9. package/dist/BrowserRestCapture.mjs.map +1 -0
  10. package/dist/BrowserRestCrawl.d.mts +16 -0
  11. package/dist/BrowserRestCrawl.mjs +244 -0
  12. package/dist/BrowserRestCrawl.mjs.map +1 -0
  13. package/dist/BrowserSession-DP6b-cpL.d.mts +125 -0
  14. package/dist/BrowserSession-DWoRC_iM.mjs +505 -0
  15. package/dist/BrowserSession-DWoRC_iM.mjs.map +1 -0
  16. package/dist/BrowserSession.d.mts +3 -0
  17. package/dist/BrowserSession.mjs +4 -0
  18. package/dist/CloudflareAiGateway.d.mts +64 -0
  19. package/dist/CloudflareAiGateway.mjs +70 -0
  20. package/dist/CloudflareAiGateway.mjs.map +1 -0
  21. package/dist/CloudflareBindings.d.mts +115 -0
  22. package/dist/CloudflareBindings.mjs +113 -0
  23. package/dist/CloudflareBindings.mjs.map +1 -0
  24. package/dist/CloudflareBrowser-DV6kya1H.mjs +495 -0
  25. package/dist/CloudflareBrowser-DV6kya1H.mjs.map +1 -0
  26. package/dist/CloudflareBrowser-Doe1M7R5.d.mts +80 -0
  27. package/dist/CloudflareBrowser.d.mts +2 -0
  28. package/dist/CloudflareBrowser.mjs +2 -0
  29. package/dist/CloudflareCodeMode.d.mts +44 -0
  30. package/dist/CloudflareCodeMode.mjs +616 -0
  31. package/dist/CloudflareCodeMode.mjs.map +1 -0
  32. package/dist/CloudflareConfig-DKXGo8L3.d.mts +106 -0
  33. package/dist/CloudflareConfig.d.mts +2 -0
  34. package/dist/CloudflareConfig.mjs +128 -0
  35. package/dist/CloudflareConfig.mjs.map +1 -0
  36. package/dist/CloudflareMemory.d.mts +142 -0
  37. package/dist/CloudflareMemory.mjs +202 -0
  38. package/dist/CloudflareMemory.mjs.map +1 -0
  39. package/dist/CloudflareScheduling.d.mts +52 -0
  40. package/dist/CloudflareScheduling.mjs +396 -0
  41. package/dist/CloudflareScheduling.mjs.map +1 -0
  42. package/dist/CloudflareSubscriptions.d.mts +87 -0
  43. package/dist/CloudflareSubscriptions.mjs +530 -0
  44. package/dist/CloudflareSubscriptions.mjs.map +1 -0
  45. package/dist/CloudflareThreadClient.d.mts +1752 -0
  46. package/dist/CloudflareThreadClient.mjs +382 -0
  47. package/dist/CloudflareThreadClient.mjs.map +1 -0
  48. package/dist/InteractiveBrowser-BnqY_Y-F.d.mts +228 -0
  49. package/dist/InteractiveBrowser-CxFT-Nwm.mjs +1923 -0
  50. package/dist/InteractiveBrowser-CxFT-Nwm.mjs.map +1 -0
  51. package/dist/InteractiveBrowser.d.mts +2 -0
  52. package/dist/InteractiveBrowser.mjs +2 -0
  53. package/dist/ThreadObject-CVhZC-7l.mjs +849 -0
  54. package/dist/ThreadObject-CVhZC-7l.mjs.map +1 -0
  55. package/dist/ThreadObject-D8GzJxJB.d.mts +839 -0
  56. package/dist/ThreadObject.d.mts +2 -0
  57. package/dist/ThreadObject.mjs +3 -0
  58. package/dist/WakeScheduler.d.mts +23 -0
  59. package/dist/WakeScheduler.mjs +57 -0
  60. package/dist/WakeScheduler.mjs.map +1 -0
  61. package/dist/boundary-BguazVkh.mjs +42 -0
  62. package/dist/boundary-BguazVkh.mjs.map +1 -0
  63. package/dist/index.d.mts +15 -1550
  64. package/dist/index.mjs +15 -1716
  65. package/dist/prepared-admission-DU0_T-ev.mjs +73 -0
  66. package/dist/prepared-admission-DU0_T-ev.mjs.map +1 -0
  67. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  68. package/package.json +1 -56
  69. package/src/Alarm.ts +1979 -0
  70. package/src/BrowserCredentials.ts +146 -0
  71. package/src/BrowserRestCapture.ts +477 -0
  72. package/src/BrowserRestCrawl.ts +540 -0
  73. package/src/BrowserSession.ts +624 -0
  74. package/src/CloudflareAiGateway.ts +170 -0
  75. package/src/CloudflareBindings.ts +199 -0
  76. package/src/CloudflareBrowser.ts +15 -0
  77. package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +308 -283
  78. package/src/{config.ts → CloudflareConfig.ts} +19 -12
  79. package/src/CloudflareMemory.ts +420 -0
  80. package/src/CloudflareScheduling.ts +747 -0
  81. package/src/CloudflareSubscriptions.ts +1041 -0
  82. package/src/CloudflareThreadClient.ts +809 -0
  83. package/src/InteractiveBrowser.ts +3246 -0
  84. package/src/ThreadObject.ts +1190 -0
  85. package/src/{wake-scheduler.ts → WakeScheduler.ts} +32 -26
  86. package/src/index.ts +14 -23
  87. package/src/internal/boundary.ts +55 -0
  88. package/src/internal/browser-binding.ts +322 -0
  89. package/src/internal/browser-credentials.ts +362 -0
  90. package/src/internal/browser-failure.ts +92 -0
  91. package/src/internal/browser-file-selection.ts +126 -0
  92. package/src/internal/browser-quick-action.ts +857 -0
  93. package/src/internal/browser-readonly-live-view.ts +111 -0
  94. package/src/internal/browser-session-lifecycle.ts +193 -0
  95. package/src/internal/layers.ts +773 -0
  96. package/src/internal/message-delivery.ts +131 -0
  97. package/src/internal/prepared-admission.ts +116 -0
  98. package/src/internal/progress-wait.ts +121 -0
  99. package/src/internal/transport.ts +44 -0
  100. package/dist/index.mjs.map +0 -1
  101. package/src/alarm.ts +0 -334
  102. package/src/bindings.ts +0 -145
  103. package/src/client.ts +0 -646
  104. package/src/conversation-object.ts +0 -768
  105. package/src/layers.ts +0 -376
  106. package/src/transport.ts +0 -38
@@ -1,3 +1,5 @@
1
+ import { RpcTarget } from "cloudflare:workers";
2
+ import { Clock, Duration, Effect, Exit, Fiber, Layer, Option, Queue, Schema } from "effect";
1
3
  import {
2
4
  CodeExecutionHost,
3
5
  CodeExecutionResourceUse,
@@ -14,60 +16,54 @@ import {
14
16
  CodeOutputLimitError,
15
17
  CodeProgramFailedError,
16
18
  CodeSourceError,
17
- SandboxImplementation,
18
19
  type CodeExecutorExecute,
19
20
  type CodeExecutionRequest,
20
- } from "@effect-agent/sandbox";
21
- import { WorkerEntrypoint } from "cloudflare:workers";
22
- import { Cause, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect";
21
+ } from "effect-agent/code-executor";
22
+ import { SandboxImplementation } from "effect-agent/sandbox";
23
+
24
+ import { safeCauseDiagnostic, safeCauseMessage } from "./internal/boundary.ts";
23
25
 
24
26
  /**
25
27
  * The Cloudflare Dynamic Worker `CodeExecutor` adapter (C4 of ADR-0017;
26
28
  * DEPLOY-011). Each pass loads one fresh Worker through the Worker Loader
27
29
  * with `globalOutbound: null`, so generated code has no ambient network,
28
- * bindings, or secrets; its only authority is the pass-scoped host stub that
29
- * routes back to `CodeModeHostEntrypoint` and, from there, into the pass's
30
- * `CodeExecutionHost` service. Platform CPU limits stop synchronous runaway
31
- * programs; the executor-owned wall-clock deadline interrupts asynchronously
32
- * suspended passes. Deployment class `E` only: the adapter records no
33
- * persistent state and a later pass may run in a completely different
34
- * isolate.
30
+ * bindings, or secrets; its only authority is the pass-scoped RPC target that
31
+ * routes back into the owning event's `CodeExecutionHost` service. Platform
32
+ * CPU limits stop synchronous runaway programs; the executor-owned wall-clock
33
+ * deadline interrupts asynchronously suspended passes. Deployment class `E`
34
+ * only: the adapter records no persistent state and a later pass may run in a
35
+ * completely different isolate.
35
36
  */
36
37
  export const dynamicWorkerImplementation = SandboxImplementation.make({
37
38
  isolation: "isolated",
38
39
  identity: "cloudflare-dynamic-worker",
39
40
  });
40
41
 
41
- /** The narrow host stub the dynamic worker receives (Workers RPC). */
42
- export interface CodeModeHostStub {
43
- readonly call: (passId: string, hostCall: unknown) => Promise<unknown>;
42
+ interface CodeModePassHost extends Rpc.RpcTargetBranded {
43
+ readonly call: (hostCall: unknown) => Promise<unknown>;
44
44
  }
45
45
 
46
- interface RegisteredPass {
47
- readonly dispatch: (hostCall: unknown) => Promise<unknown>;
46
+ interface CodeModeHarnessEntrypoint extends Rpc.WorkerEntrypointBranded {
47
+ readonly run: (host: CodeModePassHost) => Promise<unknown>;
48
48
  }
49
49
 
50
50
  /**
51
- * Live passes by identity. Entries are Scope-managed: registered when a pass
52
- * opens and removed by its finalizer, so a stale harness (or a forged
53
- * `passId`) cannot reach any host authority.
51
+ * One object-capability endpoint for one execution pass. Workers RPC invokes
52
+ * the target in the request context where it was created, so the native
53
+ * Promise returned by `dispatch` and the Effect fiber that settles it share
54
+ * one I/O owner. Passing the target as `run()`'s argument also scopes the
55
+ * remote stub to that RPC call; no request state lives at module scope.
54
56
  */
55
- const passRegistry = new Map<string, RegisteredPass>();
57
+ class CodeModePassHostTarget extends RpcTarget implements CodeModePassHost {
58
+ readonly #dispatch: (hostCall: unknown) => Promise<unknown>;
56
59
 
57
- /**
58
- * The host-side RPC target for dynamic workers. The application exposes it
59
- * from its Worker entry (`export { CodeModeHostEntrypoint }`) and hands the
60
- * adapter a same-instance stub — `ctx.exports.CodeModeHostEntrypoint()` in
61
- * production (a self service binding may reach a different instance and must
62
- * not be used there); tests bind it through Miniflare's `kCurrentWorker`.
63
- */
64
- export class CodeModeHostEntrypoint extends WorkerEntrypoint {
65
- async call(passId: unknown, hostCall: unknown): Promise<unknown> {
66
- const pass = passRegistry.get(String(passId));
67
- if (pass === undefined) {
68
- throw new Error("Unknown Code Mode pass");
69
- }
70
- return pass.dispatch(hostCall);
60
+ constructor(dispatch: (hostCall: unknown) => Promise<unknown>) {
61
+ super();
62
+ this.#dispatch = dispatch;
63
+ }
64
+
65
+ call(hostCall: unknown): Promise<unknown> {
66
+ return this.#dispatch(hostCall);
71
67
  }
72
68
  }
73
69
 
@@ -103,9 +99,8 @@ const safeJson = (value) => {
103
99
  };
104
100
 
105
101
  export default class CodeModeHarness extends WorkerEntrypoint {
106
- async run() {
107
- const config = JSON.parse(this.env.CODE_MODE_PASS);
108
- const host = this.env.CODE_MODE_HOST;
102
+ async run(host) {
103
+ const config = this.env.CODE_MODE_PASS;
109
104
  const limits = config.limits;
110
105
  const logs = [];
111
106
  let logBytes = 0;
@@ -143,7 +138,7 @@ export default class CodeModeHarness extends WorkerEntrypoint {
143
138
  };
144
139
  throw new Error("code-mode host-call argument limit exceeded");
145
140
  }
146
- const outcome = await host.call(config.passId, {
141
+ const outcome = await host.call({
147
142
  namespace,
148
143
  method,
149
144
  argument: JSON.parse(argText),
@@ -227,55 +222,50 @@ const BoundedLogs = Schema.Array(Schema.String.check(Schema.isMaxLength(16 * 102
227
222
  Schema.isMaxLength(4_096),
228
223
  );
229
224
 
230
- const HarnessCompleted = Schema.Struct({
231
- _tag: Schema.Literal("completed"),
225
+ const HarnessCompleted = Schema.TaggedStruct("completed", {
232
226
  value: Schema.Json,
233
227
  logs: BoundedLogs,
234
228
  hostCalls: Schema.Natural,
235
229
  logBytes: Schema.Natural,
236
230
  resultBytes: Schema.Natural,
237
231
  });
238
- const HarnessSourceInvalid = Schema.Struct({
239
- _tag: Schema.Literal("source-invalid"),
240
- message: Schema.String,
241
- });
242
- const HarnessNotAFunction = Schema.Struct({
243
- _tag: Schema.Literal("source-not-a-function"),
232
+
233
+ const HarnessNotAFunction = Schema.TaggedStruct("source-not-a-function", {
244
234
  actual: Schema.String,
245
235
  });
246
- const HarnessProgramFailed = Schema.Struct({
247
- _tag: Schema.Literal("program-failed"),
236
+
237
+ const HarnessProgramFailed = Schema.TaggedStruct("program-failed", {
248
238
  reason: Schema.Literals(["threw", "rejected", "non-json-result"]),
249
239
  thrown: Schema.Json,
250
240
  message: Schema.String,
251
241
  logs: BoundedLogs,
252
242
  });
253
- const HarnessLogLimit = Schema.Struct({
254
- _tag: Schema.Literal("log-limit"),
243
+
244
+ const HarnessLogLimit = Schema.TaggedStruct("log-limit", {
255
245
  observed: Schema.Natural,
256
246
  logs: BoundedLogs,
257
247
  });
258
- const HarnessArgumentLimit = Schema.Struct({
259
- _tag: Schema.Literal("argument-limit"),
248
+
249
+ const HarnessArgumentLimit = Schema.TaggedStruct("argument-limit", {
260
250
  observed: Schema.Natural,
261
251
  logs: BoundedLogs,
262
252
  });
263
- const HarnessResultLimit = Schema.Struct({
264
- _tag: Schema.Literal("result-limit"),
253
+
254
+ const HarnessResultLimit = Schema.TaggedStruct("result-limit", {
265
255
  observed: Schema.Natural,
266
256
  logs: BoundedLogs,
267
257
  });
268
- const HarnessHostCallLimit = Schema.Struct({
269
- _tag: Schema.Literal("host-call-limit"),
258
+
259
+ const HarnessHostCallLimit = Schema.TaggedStruct("host-call-limit", {
270
260
  logs: BoundedLogs,
271
261
  });
272
- const HarnessProtocol = Schema.Struct({
273
- _tag: Schema.Literal("protocol"),
262
+
263
+ const HarnessProtocol = Schema.TaggedStruct("protocol", {
274
264
  message: Schema.String,
275
265
  });
266
+
276
267
  const HarnessOutcome = Schema.Union([
277
268
  HarnessCompleted,
278
- HarnessSourceInvalid,
279
269
  HarnessNotAFunction,
280
270
  HarnessProgramFailed,
281
271
  HarnessLogLimit,
@@ -285,6 +275,25 @@ const HarnessOutcome = Schema.Union([
285
275
  HarnessProtocol,
286
276
  ]);
287
277
 
278
+ const HarnessPassConfig = Schema.Struct({
279
+ namespaces: Schema.Array(
280
+ Schema.Struct({
281
+ name: Schema.NonEmptyString,
282
+ methods: Schema.Array(Schema.NonEmptyString).check(Schema.isMaxLength(64)),
283
+ }),
284
+ ).check(Schema.isMaxLength(32)),
285
+ limits: Schema.Struct({
286
+ maxLogBytes: Schema.Natural,
287
+ maxResultBytes: Schema.Natural,
288
+ maxHostCalls: Schema.Natural,
289
+ maxHostCallArgumentBytes: Schema.Natural,
290
+ }),
291
+ });
292
+
293
+ const encodeHarnessPassConfig = Schema.encodeSync(HarnessPassConfig);
294
+ const encodeJsonPayload = Schema.encodeSync(Schema.fromJsonString(Schema.Json));
295
+ const decodeJsonPayload = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json));
296
+
288
297
  const decodeHarnessOutcome = (value: unknown) => {
289
298
  try {
290
299
  return Schema.decodeUnknownOption(HarnessOutcome)(value);
@@ -309,6 +318,28 @@ const decodeHostCallResult = (value: unknown) => {
309
318
  }
310
319
  };
311
320
 
321
+ /** Dispose a Cloudflare RPC handle when the runtime supplies its untyped disposal hook. @internal */
322
+ export const disposeRpcHandle = (handle: unknown): Effect.Effect<void> =>
323
+ Effect.try({
324
+ try: () => {
325
+ if ((typeof handle !== "object" && typeof handle !== "function") || handle === null) return;
326
+ if (!(Symbol.dispose in handle)) return;
327
+ const dispose = Reflect.get(handle, Symbol.dispose);
328
+
329
+ if (typeof dispose === "function") {
330
+ Reflect.apply(dispose, handle, []);
331
+ }
332
+ },
333
+ catch: (cause) =>
334
+ safeCauseDiagnostic(cause, "The Cloudflare RPC disposal hook failed without a diagnostic"),
335
+ }).pipe(
336
+ Effect.catch((diagnostic) =>
337
+ Effect.logWarning(`Cloudflare RPC handle disposal failed: ${diagnostic}`).pipe(
338
+ Effect.ignoreCause,
339
+ ),
340
+ ),
341
+ );
342
+
312
343
  /**
313
344
  * Project a host outcome to the plain JSON envelope the harness reads. A
314
345
  * `CodeExecutionHost` may return either real `CodeHostCallResult` instances
@@ -326,8 +357,8 @@ const encodeHostResultPayload = (
326
357
  ): EncodedHostResultPayload | undefined => {
327
358
  try {
328
359
  const payload = outcome._tag === "CodeHostCallSuccess" ? outcome.value : outcome.error;
329
- const encodedPayload = JSON.stringify(payload);
330
- if (encodedPayload === undefined) return undefined;
360
+ const encodedPayload = encodeJsonPayload(payload);
361
+
331
362
  return {
332
363
  encodedPayload,
333
364
  resultBytes: utf8ByteLength(encodedPayload),
@@ -339,30 +370,31 @@ const encodeHostResultPayload = (
339
370
 
340
371
  const utf8ByteLength = (value: string): number => {
341
372
  let total = 0;
373
+
342
374
  for (const character of value) {
343
375
  const codePoint = character.codePointAt(0) ?? 0;
376
+
344
377
  total += codePoint <= 0x7f ? 1 : codePoint <= 0x7ff ? 2 : codePoint <= 0xffff ? 3 : 4;
345
378
  }
379
+
346
380
  return total;
347
381
  };
348
382
 
349
- type HostDispatchFailure =
350
- | { readonly _tag: "host-call-limit" }
351
- | { readonly _tag: "host-call-result-limit"; readonly observed: number }
352
- | { readonly _tag: "host-call-protocol" }
353
- | { readonly _tag: "wall-clock-timeout" }
354
- | { readonly _tag: "host-call-defect"; readonly cause: Cause.Cause<never> };
355
-
356
383
  interface QueuedHostCall {
357
384
  readonly call: CodeHostCall;
358
385
  readonly resolve: (value: unknown) => void;
359
386
  readonly reject: (reason: unknown) => void;
360
387
  }
361
388
 
362
- interface ActiveHostCall {
363
- readonly fiber: Fiber.Fiber<Record<string, unknown>, never>;
364
- readonly settlement: Promise<void>;
365
- }
389
+ type HostWork =
390
+ | { readonly _tag: "call"; readonly queued: QueuedHostCall }
391
+ | { readonly _tag: "limit" };
392
+
393
+ type HostDispatchError =
394
+ | CodeExecutionTimeoutError
395
+ | CodeOutputLimitError
396
+ | CodeExecutionProtocolError
397
+ | CodeHostCallLimitError;
366
398
 
367
399
  /** Reserved global names the harness owns inside the dynamic worker. */
368
400
  const reservedHarnessGlobals = new Set(["console"]);
@@ -370,19 +402,14 @@ const reservedHarnessGlobals = new Set(["console"]);
370
402
  export interface DynamicWorkerCodeExecutorOptions {
371
403
  /** The `worker_loader` binding. */
372
404
  readonly loader: WorkerLoader;
373
- /**
374
- * A SAME-INSTANCE stub of `CodeModeHostEntrypoint`. In production create it
375
- * with `ctx.exports.CodeModeHostEntrypoint()`; a cross-instance stub would
376
- * dispatch host calls into an isolate without this pass's registry entry.
377
- */
378
- readonly hostStub: CodeModeHostStub;
379
405
  /** Compatibility date for dynamic workers; defaults to `2025-05-01`. */
380
406
  readonly compatibilityDate?: string | undefined;
381
407
  }
382
408
 
383
- const passCounterState = { next: 0 };
384
-
385
- const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExecute =>
409
+ const makeExecute = (
410
+ options: DynamicWorkerCodeExecutorOptions,
411
+ clock: Clock.Clock,
412
+ ): CodeExecutorExecute =>
386
413
  Effect.fn("DynamicWorkerCodeExecutor.execute")(function* (request: CodeExecutionRequest) {
387
414
  if (request.network._tag !== "NetworkDisabled") {
388
415
  return yield* CodeExecutorUnsupportedError.make({
@@ -393,6 +420,7 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
393
420
  });
394
421
  }
395
422
  const sourceBytes = utf8ByteLength(request.source);
423
+
396
424
  if (sourceBytes > request.limits.maxSourceBytes) {
397
425
  return yield* CodeSourceError.make({
398
426
  implementation: dynamicWorkerImplementation,
@@ -409,148 +437,140 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
409
437
  });
410
438
  }
411
439
  }
412
-
413
440
  const host = yield* CodeExecutionHost;
414
- passCounterState.next += 1;
415
- // The pass id is the only credential a loaded program presents to reach
416
- // its host authority, so it must be unguessable: a program cannot forge
417
- // another pass's id even if two passes were ever concurrent (the broker
418
- // keeps them sequential, but the id must not rely on that). The counter
419
- // prefix keeps ids debuggable; the random suffix makes them unforgeable.
420
- const passId = `code-mode-pass-${passCounterState.next}-${crypto.randomUUID()}`;
421
-
422
- const startedAt = performance.now();
423
- const passDeadline = startedAt + Duration.toMillis(request.limits.maxWallTime);
424
- const remainingPassWallTime = (): Duration.Duration =>
425
- Duration.millis(Math.max(0, passDeadline - performance.now()));
441
+
442
+ // This synchronous clock access is confined to callbacks that must compute a timeout
443
+ // immediately. The Clock service remains the authority, so tests and hosts can replace it.
444
+ const startedAt = clock.monotonicTimeNanosUnsafe();
445
+ const passDeadline = startedAt + Duration.toNanosUnsafe(request.limits.maxWallTime);
446
+
447
+ const remainingPassWallTime = (): Duration.Duration => {
448
+ const now = clock.monotonicTimeNanosUnsafe();
449
+
450
+ return Duration.nanos(passDeadline > now ? passDeadline - now : 0n);
451
+ };
452
+
426
453
  let issuedHostCalls = 0;
427
454
  let passOpen = true;
428
455
  const queuedHostCalls: Array<QueuedHostCall> = [];
429
- let activeHostCall: ActiveHostCall | undefined;
430
- let hostDispatchFailure: HostDispatchFailure | undefined;
431
- let propagatedHostDispatchFailure: HostDispatchFailure | undefined;
432
- let signalHostDispatchFailure = (_failure: HostDispatchFailure): void => undefined;
433
- const hostDispatchFailureSignal = new Promise<HostDispatchFailure>((resolve) => {
434
- signalHostDispatchFailure = resolve;
435
- });
456
+ let passFailure: HostDispatchError | undefined;
457
+
458
+ const failPass = (error: HostDispatchError): void => {
459
+ if (passFailure === undefined) passFailure = error;
460
+ };
461
+
436
462
  const rejectQueuedHostCalls = (reason: Error): void => {
437
463
  for (const queued of queuedHostCalls.splice(0)) {
438
464
  queued.reject(reason);
439
465
  }
440
466
  };
441
- const recordHostDispatchFailure = (failure: HostDispatchFailure): void => {
442
- if (hostDispatchFailure !== undefined) return;
443
- hostDispatchFailure = failure;
444
- signalHostDispatchFailure(failure);
445
- rejectQueuedHostCalls(new Error("Code Mode pass failed"));
446
- };
447
- const failHostDispatch = (failure: HostDispatchFailure) => {
448
- switch (failure._tag) {
449
- case "host-call-limit":
450
- return Effect.fail(
451
- CodeHostCallLimitError.make({
452
- implementation: dynamicWorkerImplementation,
453
- limit: request.limits.maxHostCalls,
454
- logs: [],
455
- }),
456
- );
457
- case "host-call-result-limit":
458
- return Effect.fail(
459
- CodeOutputLimitError.make({
460
- implementation: dynamicWorkerImplementation,
461
- surface: "host-call-result",
462
- limit: request.limits.maxHostCallResultBytes,
463
- observed: failure.observed,
464
- logs: [],
465
- }),
466
- );
467
- case "host-call-protocol":
468
- return Effect.fail(
469
- CodeExecutionProtocolError.make({
470
- implementation: dynamicWorkerImplementation,
471
- message: "The execution host returned a value outside the CodeHostCallResult schema",
472
- }),
473
- );
474
- case "wall-clock-timeout":
475
- return Effect.fail(
476
- CodeExecutionTimeoutError.make({
477
- implementation: dynamicWorkerImplementation,
478
- kind: "wall-clock",
479
- maxWallTime: request.limits.maxWallTime,
480
- logs: [],
481
- }),
482
- );
483
- case "host-call-defect":
484
- return Effect.failCause(failure.cause);
485
- }
486
- };
487
- const propagateHostDispatchFailure = (failure: HostDispatchFailure) =>
467
+
468
+ const queue = yield* Queue.unbounded<HostWork>();
469
+
470
+ const deliverHostOutcome = (
471
+ queued: QueuedHostCall,
472
+ outcome: CodeHostCallResult,
473
+ ): Effect.Effect<void, CodeExecutionProtocolError | CodeOutputLimitError> =>
488
474
  Effect.gen(function* () {
489
- propagatedHostDispatchFailure = failure;
490
- return yield* failHostDispatch(failure);
475
+ const decoded = decodeHostCallResult(outcome);
476
+
477
+ if (Option.isNone(decoded)) {
478
+ const error = CodeExecutionProtocolError.make({
479
+ implementation: dynamicWorkerImplementation,
480
+ message: "The execution host returned a value outside the CodeHostCallResult schema",
481
+ });
482
+
483
+ failPass(error);
484
+
485
+ return yield* error;
486
+ }
487
+ const encoded = encodeHostResultPayload(decoded.value);
488
+
489
+ if (encoded === undefined || encoded.resultBytes > request.limits.maxHostCallResultBytes) {
490
+ const error = CodeOutputLimitError.make({
491
+ implementation: dynamicWorkerImplementation,
492
+ surface: "host-call-result",
493
+ limit: request.limits.maxHostCallResultBytes,
494
+ observed: encoded?.resultBytes ?? 0,
495
+ logs: [],
496
+ });
497
+
498
+ failPass(error);
499
+
500
+ return yield* error;
501
+ }
502
+ const normalizedPayload = decodeJsonPayload(encoded.encodedPayload);
503
+
504
+ if (Option.isNone(normalizedPayload)) {
505
+ const error = CodeExecutionProtocolError.make({
506
+ implementation: dynamicWorkerImplementation,
507
+ message: "The execution host returned a result that could not cross the JSON boundary",
508
+ });
509
+
510
+ failPass(error);
511
+
512
+ return yield* error;
513
+ }
514
+ queued.resolve(
515
+ decoded.value._tag === "CodeHostCallSuccess"
516
+ ? { _tag: "CodeHostCallSuccess", value: normalizedPayload.value }
517
+ : { _tag: "CodeHostCallFailure", error: normalizedPayload.value },
518
+ );
491
519
  });
492
520
 
493
- const startNextHostCall = (): void => {
494
- if (!passOpen || hostDispatchFailure !== undefined || activeHostCall !== undefined) return;
495
- const queued = queuedHostCalls.shift();
496
- if (queued === undefined) return;
497
-
498
- // A Dynamic Worker callback is a new Workers RPC into the loader isolate. Running the
499
- // complete host call on an independent root fiber breaks its dependency on the still-open
500
- // guest RPC. Retaining the handle and starting one call at a time preserves bounded,
501
- // serialized execution and lets pass teardown interrupt and await the active call.
502
- const fiber = Effect.runFork(
503
- Effect.yieldNow.pipe(
504
- Effect.andThen(host.call(queued.call)),
521
+ // Workers RPC into the loader isolate cannot settle on the fiber blocked
522
+ // in `entrypoint.run()`. A Scope-owned sibling fiber keeps that
523
+ // independence while inheriting the pass Context and dying with the Scope.
524
+ const serveHostCalls = Effect.gen(function* () {
525
+ while (true) {
526
+ const work = yield* Queue.take(queue);
527
+
528
+ if (work._tag === "limit") {
529
+ const error = CodeHostCallLimitError.make({
530
+ implementation: dynamicWorkerImplementation,
531
+ limit: request.limits.maxHostCalls,
532
+ logs: [],
533
+ });
534
+
535
+ failPass(error);
536
+
537
+ return yield* error;
538
+ }
539
+ const queued = work.queued;
540
+
541
+ yield* host.call(queued.call).pipe(
505
542
  Effect.timeoutOrElse({
506
543
  duration: remainingPassWallTime(),
507
- orElse: () =>
508
- Effect.sync(() => {
509
- recordHostDispatchFailure({ _tag: "wall-clock-timeout" });
510
- throw new Error("code-mode host call exceeded the pass wall-clock limit");
511
- }),
512
- }),
513
- Effect.map((outcome) => {
514
- const decoded = decodeHostCallResult(outcome);
515
- if (Option.isNone(decoded)) {
516
- recordHostDispatchFailure({ _tag: "host-call-protocol" });
517
- throw new Error("host-call protocol violation");
518
- }
519
- const encoded = encodeHostResultPayload(decoded.value);
520
- if (
521
- encoded === undefined ||
522
- encoded.resultBytes > request.limits.maxHostCallResultBytes
523
- ) {
524
- recordHostDispatchFailure({
525
- _tag: "host-call-result-limit",
526
- observed: encoded?.resultBytes ?? 0,
544
+ orElse: () => {
545
+ const error = CodeExecutionTimeoutError.make({
546
+ implementation: dynamicWorkerImplementation,
547
+ kind: "wall-clock",
548
+ maxWallTime: request.limits.maxWallTime,
549
+ logs: [],
527
550
  });
528
- throw new Error("host-call result limit exceeded");
529
- }
530
- const normalizedPayload: unknown = JSON.parse(encoded.encodedPayload);
531
- return decoded.value._tag === "CodeHostCallSuccess"
532
- ? { _tag: "CodeHostCallSuccess", value: normalizedPayload }
533
- : { _tag: "CodeHostCallFailure", error: normalizedPayload };
551
+
552
+ failPass(error);
553
+
554
+ return error;
555
+ },
534
556
  }),
535
- ),
536
- );
537
- const settlement = Effect.runPromise(Fiber.await(fiber))
538
- .then((exit) => {
539
- if (Exit.isSuccess(exit)) {
540
- queued.resolve(exit.value);
541
- return;
542
- }
543
- if (!Cause.hasInterruptsOnly(exit.cause)) {
544
- recordHostDispatchFailure({ _tag: "host-call-defect", cause: exit.cause });
545
- }
546
- queued.reject(new Error("Code Mode host call failed"));
547
- })
548
- .finally(() => {
549
- if (activeHostCall?.fiber === fiber) activeHostCall = undefined;
550
- startNextHostCall();
551
- });
552
- activeHostCall = { fiber, settlement };
553
- };
557
+ Effect.flatMap((outcome) => deliverHostOutcome(queued, outcome)),
558
+ Effect.tapError(() =>
559
+ Effect.sync(() => queued.reject(new Error("Code Mode host call failed"))),
560
+ ),
561
+ Effect.onInterrupt(() =>
562
+ Effect.sync(() => queued.reject(new Error("Code Mode pass is closing"))),
563
+ ),
564
+ );
565
+ }
566
+ });
567
+
568
+ const concurrency = request.limits.maxHostCallConcurrency ?? 4;
569
+
570
+ const server = yield* Effect.all(
571
+ Array.from({ length: concurrency }, () => serveHostCalls),
572
+ { concurrency, discard: true },
573
+ ).pipe(Effect.andThen(Effect.never), Effect.forkScoped);
554
574
 
555
575
  const dispatch = (hostCall: unknown): Promise<unknown> => {
556
576
  if (!passOpen) {
@@ -558,67 +578,51 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
558
578
  }
559
579
  issuedHostCalls += 1;
560
580
  if (issuedHostCalls > request.limits.maxHostCalls) {
561
- recordHostDispatchFailure({ _tag: "host-call-limit" });
581
+ const error = CodeHostCallLimitError.make({
582
+ implementation: dynamicWorkerImplementation,
583
+ limit: request.limits.maxHostCalls,
584
+ logs: [],
585
+ });
586
+
587
+ failPass(error);
588
+ Queue.offerUnsafe(queue, { _tag: "limit" });
589
+
562
590
  return Promise.reject(new Error("host-call limit exceeded"));
563
591
  }
564
592
  const decoded = decodeHostCall(hostCall);
593
+
565
594
  if (Option.isNone(decoded)) {
566
595
  return Promise.reject(new TypeError("host calls must match the CodeHostCall schema"));
567
596
  }
597
+
568
598
  return new Promise((resolve, reject) => {
569
- queuedHostCalls.push({ call: decoded.value, resolve, reject });
570
- startNextHostCall();
599
+ const queued = { call: decoded.value, resolve, reject };
600
+
601
+ queuedHostCalls.push(queued);
602
+ Queue.offerUnsafe(queue, { _tag: "call", queued });
571
603
  });
572
604
  };
573
605
 
574
- const closeHostDispatch = Effect.gen(function* () {
606
+ const closeAdmission = Effect.sync(() => {
575
607
  passOpen = false;
576
- passRegistry.delete(passId);
577
608
  rejectQueuedHostCalls(new Error("Code Mode pass is closing"));
578
- const active = activeHostCall;
579
- if (active !== undefined) {
580
- yield* Fiber.interrupt(active.fiber);
581
- yield* Effect.promise(() => active.settlement);
582
- if (activeHostCall === active) activeHostCall = undefined;
583
- }
584
- return hostDispatchFailure;
585
609
  });
586
- const stopHostDispatch = closeHostDispatch.pipe(
587
- Effect.flatMap((failure) =>
588
- failure !== undefined && propagatedHostDispatchFailure !== failure
589
- ? propagateHostDispatchFailure(failure)
590
- : Effect.void,
591
- ),
592
- );
593
- const releaseHostDispatch = closeHostDispatch.pipe(
594
- Effect.flatMap((failure) => {
595
- if (failure?._tag !== "host-call-defect" || propagatedHostDispatchFailure === failure) {
596
- return Effect.void;
597
- }
598
- propagatedHostDispatchFailure = failure;
599
- return Effect.failCause(failure.cause);
600
- }),
601
- );
602
610
 
603
- yield* Effect.acquireRelease(
604
- Effect.sync(() => {
605
- passRegistry.set(passId, { dispatch });
606
- }),
607
- () => releaseHostDispatch,
608
- );
611
+ yield* Effect.addFinalizer(() => closeAdmission.pipe(Effect.andThen(Fiber.interrupt(server))));
609
612
 
610
- const workerCode = {
613
+ // No `allowExperimental`: the runtime only accepts it when the CALLING
614
+ // worker carries the `experimental` compatibility flag, which deployed
615
+ // consumers cannot set — the option would reject every pass in
616
+ // production. The harness needs no experimental runtime features.
617
+ const workerCode: WorkerLoaderWorkerCode = {
611
618
  compatibilityDate: options.compatibilityDate ?? "2025-05-01",
612
- allowExperimental: true,
613
619
  mainModule: "harness.js",
614
620
  modules: {
615
621
  "harness.js": HARNESS_MODULE,
616
622
  "program.js": `export default (\n${request.source}\n);`,
617
623
  },
618
624
  env: {
619
- CODE_MODE_HOST: options.hostStub,
620
- CODE_MODE_PASS: JSON.stringify({
621
- passId,
625
+ CODE_MODE_PASS: encodeHarnessPassConfig({
622
626
  namespaces: request.namespaces.map((namespace) => ({
623
627
  name: namespace.name,
624
628
  methods: namespace.methods,
@@ -644,9 +648,10 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
644
648
 
645
649
  const worker = yield* Effect.acquireRelease(
646
650
  Effect.try({
647
- try: () => options.loader.load(workerCode as never),
651
+ try: () => options.loader.load(workerCode),
648
652
  catch: (cause) => {
649
- const text = cause instanceof Error ? cause.message : String(cause);
653
+ const text = safeCauseMessage(cause, "The Worker Loader failed without a diagnostic");
654
+
650
655
  // Blame the program's source ONLY on a genuine compile diagnostic;
651
656
  // any other load rejection is an infrastructure start failure, not
652
657
  // the model's fault (see classifyWorkerFailure for the same split).
@@ -657,6 +662,7 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
657
662
  message: text.slice(0, 8_000),
658
663
  });
659
664
  }
665
+
660
666
  return CodeExecutorStartError.make({
661
667
  implementation: dynamicWorkerImplementation,
662
668
  message: `The Worker Loader rejected the pass: ${text}`.slice(0, 8_000),
@@ -664,23 +670,23 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
664
670
  });
665
671
  },
666
672
  }),
667
- (stub) =>
668
- Effect.sync(() => {
669
- (stub as Partial<Record<typeof Symbol.dispose, () => void>>)[Symbol.dispose]?.();
670
- }),
673
+ disposeRpcHandle,
674
+ );
675
+
676
+ const entrypoint = yield* Effect.acquireRelease(
677
+ Effect.try({
678
+ try: () => worker.getEntrypoint<CodeModeHarnessEntrypoint>(),
679
+ catch: (cause) => classifyWorkerFailure(cause, request.limits.maxWallTime),
680
+ }),
681
+ disposeRpcHandle,
671
682
  );
672
683
 
673
684
  const rpc = Effect.tryPromise({
674
- try: async () => {
675
- const entrypoint = worker.getEntrypoint() as unknown as {
676
- run(): Promise<unknown>;
677
- };
678
- return await entrypoint.run();
679
- },
685
+ try: () => entrypoint.run(new CodeModePassHostTarget(dispatch)),
680
686
  catch: (cause) => classifyWorkerFailure(cause, request.limits.maxWallTime),
681
687
  });
682
688
 
683
- const raw = yield* Effect.raceFirst(
689
+ const exit = yield* Effect.raceFirst(
684
690
  rpc.pipe(
685
691
  Effect.timeoutOrElse({
686
692
  duration: remainingPassWallTime(),
@@ -693,18 +699,22 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
693
699
  }),
694
700
  }),
695
701
  ),
696
- Effect.promise(() => hostDispatchFailureSignal).pipe(
697
- Effect.flatMap(propagateHostDispatchFailure),
698
- ),
699
- );
700
- yield* stopHostDispatch;
701
- const finishedAt = performance.now();
702
+ Fiber.join(server),
703
+ ).pipe(Effect.exit);
702
704
 
703
- if (hostDispatchFailure !== undefined) {
704
- return yield* propagateHostDispatchFailure(hostDispatchFailure);
705
+ yield* closeAdmission;
706
+ yield* Fiber.interrupt(server);
707
+ if (passFailure !== undefined) {
708
+ return yield* passFailure;
709
+ }
710
+ if (Exit.isFailure(exit)) {
711
+ return yield* Effect.failCause(exit.cause);
705
712
  }
713
+ const raw = exit.value;
714
+ const finishedAt = clock.monotonicTimeNanosUnsafe();
706
715
 
707
716
  const outcome = decodeHarnessOutcome(raw);
717
+
708
718
  if (Option.isNone(outcome)) {
709
719
  return yield* CodeExecutionProtocolError.make({
710
720
  implementation: dynamicWorkerImplementation,
@@ -718,20 +728,13 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
718
728
  value: outcome.value.value,
719
729
  logs: outcome.value.logs,
720
730
  resourceUse: CodeExecutionResourceUse.make({
721
- wallTime: Duration.millis(Math.max(0, finishedAt - startedAt)),
731
+ wallTime: Duration.nanos(finishedAt > startedAt ? finishedAt - startedAt : 0n),
722
732
  hostCalls: outcome.value.hostCalls,
723
733
  logBytes: outcome.value.logBytes,
724
734
  resultBytes: outcome.value.resultBytes,
725
735
  }),
726
736
  });
727
737
  }
728
- case "source-invalid": {
729
- return yield* CodeSourceError.make({
730
- implementation: dynamicWorkerImplementation,
731
- reason: "invalid",
732
- message: outcome.value.message.slice(0, 8_000),
733
- });
734
- }
735
738
  case "source-not-a-function": {
736
739
  return yield* CodeSourceError.make({
737
740
  implementation: dynamicWorkerImplementation,
@@ -804,13 +807,8 @@ const classifyWorkerFailure = (
804
807
  | CodeExecutorTerminatedError
805
808
  | CodeExecutorStartError
806
809
  | CodeSourceError => {
807
- const text = (() => {
808
- try {
809
- return cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause);
810
- } catch {
811
- return "[unserializable worker failure]";
812
- }
813
- })();
810
+ const text = safeCauseDiagnostic(cause, "[unserializable worker failure]");
811
+
814
812
  // `WorkerLoader.load()` is lazy, so a module-compile error in the generated
815
813
  // program surfaces here at first use. Blame the program's source ONLY on a
816
814
  // genuine compile diagnostic (a `SyntaxError` or an explicit compile
@@ -840,6 +838,7 @@ const classifyWorkerFailure = (
840
838
  cause,
841
839
  });
842
840
  }
841
+
843
842
  return CodeExecutorTerminatedError.make({
844
843
  implementation: dynamicWorkerImplementation,
845
844
  message: text.slice(0, 8_000),
@@ -850,4 +849,30 @@ const classifyWorkerFailure = (
850
849
  export const dynamicWorkerCodeExecutorLayer = (
851
850
  options: DynamicWorkerCodeExecutorOptions,
852
851
  ): Layer.Layer<CodeExecutor> =>
853
- Layer.succeed(CodeExecutor)(CodeExecutor.of({ execute: makeExecute(options) }));
852
+ Layer.effect(
853
+ CodeExecutor,
854
+ Effect.gen(function* () {
855
+ const clock = yield* Clock.Clock;
856
+
857
+ return CodeExecutor.of({ execute: makeExecute(options, clock) });
858
+ }),
859
+ );
860
+
861
+ /** Assemble Code Mode handlers with the isolated Dynamic Worker executor. */
862
+ export const CloudflareCodeMode = {
863
+ /**
864
+ * Provide the selected tool handlers at construction, where Code Mode captures them.
865
+ * Their errors and remaining dependencies stay visible. The definition still owns the
866
+ * allowlist and limits; the runtime supplies the live Tool broker for each scoped pass.
867
+ */
868
+ layer: <A, E, R, Handlers, HandlerError, HandlerRequirements>(
869
+ definition: { readonly handlers: Layer.Layer<A, E, R> },
870
+ options: DynamicWorkerCodeExecutorOptions & {
871
+ readonly handlers: Layer.Layer<Handlers, HandlerError, HandlerRequirements>;
872
+ },
873
+ ) =>
874
+ definition.handlers.pipe(
875
+ Layer.provide(options.handlers),
876
+ Layer.provide(dynamicWorkerCodeExecutorLayer(options)),
877
+ ),
878
+ };