@effect-agent/platform-cloudflare 0.1.0-beta.21 → 0.1.0-beta.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-agent/platform-cloudflare",
3
- "version": "0.1.0-beta.21",
3
+ "version": "0.1.0-beta.23",
4
4
  "exports": {
5
5
  ".": {
6
6
  "types": "./dist/index.d.mts",
@@ -8,11 +8,11 @@
8
8
  }
9
9
  },
10
10
  "dependencies": {
11
- "@effect-agent/core": "0.1.0-beta.21",
12
- "@effect-agent/engine": "0.1.0-beta.21",
13
- "@effect-agent/sandbox": "0.1.0-beta.21",
14
- "@effect-agent/session": "0.1.0-beta.21",
15
- "@effect-agent/storage-cloudflare": "0.1.0-beta.21",
11
+ "@effect-agent/core": "0.1.0-beta.23",
12
+ "@effect-agent/engine": "0.1.0-beta.23",
13
+ "@effect-agent/sandbox": "0.1.0-beta.23",
14
+ "@effect-agent/session": "0.1.0-beta.23",
15
+ "@effect-agent/storage-cloudflare": "0.1.0-beta.23",
16
16
  "@effect/platform-browser": "4.0.0-rc.110",
17
17
  "@effect/sql-sqlite-do": "4.0.0-rc.110",
18
18
  "effect": "4.0.0-rc.110"
@@ -43,8 +43,8 @@
43
43
  "devDependencies": {
44
44
  "@cloudflare/vitest-pool-workers": "0.21.3",
45
45
  "@cloudflare/workers-types": "5.20260813.1",
46
- "@effect-agent/capabilities": "0.1.0-beta.19",
47
- "@effect-agent/testing": "0.1.0-beta.19",
46
+ "@effect-agent/capabilities": "0.1.0-beta.22",
47
+ "@effect-agent/testing": "0.1.0-beta.22",
48
48
  "@effect/vitest": "4.0.0-rc.110",
49
49
  "effect-cf": "0.27.0",
50
50
  "esbuild": "0.28.1",
@@ -19,7 +19,7 @@ import {
19
19
  type CodeExecutionRequest,
20
20
  } from "@effect-agent/sandbox";
21
21
  import { WorkerEntrypoint } from "cloudflare:workers";
22
- import { Cause, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect";
22
+ import { Duration, Effect, Exit, Fiber, Layer, Option, Queue, Schema } from "effect";
23
23
 
24
24
  /**
25
25
  * The Cloudflare Dynamic Worker `CodeExecutor` adapter (C4 of ADR-0017;
@@ -346,23 +346,21 @@ const utf8ByteLength = (value: string): number => {
346
346
  return total;
347
347
  };
348
348
 
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
349
  interface QueuedHostCall {
357
350
  readonly call: CodeHostCall;
358
351
  readonly resolve: (value: unknown) => void;
359
352
  readonly reject: (reason: unknown) => void;
360
353
  }
361
354
 
362
- interface ActiveHostCall {
363
- readonly fiber: Fiber.Fiber<Record<string, unknown>, never>;
364
- readonly settlement: Promise<void>;
365
- }
355
+ type HostWork =
356
+ | { readonly _tag: "call"; readonly queued: QueuedHostCall }
357
+ | { readonly _tag: "limit" };
358
+
359
+ type HostDispatchError =
360
+ | CodeExecutionTimeoutError
361
+ | CodeOutputLimitError
362
+ | CodeExecutionProtocolError
363
+ | CodeHostCallLimitError;
366
364
 
367
365
  /** Reserved global names the harness owns inside the dynamic worker. */
368
366
  const reservedHarnessGlobals = new Set(["console"]);
@@ -426,131 +424,92 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
426
424
  let issuedHostCalls = 0;
427
425
  let passOpen = true;
428
426
  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
- });
427
+ let passFailure: HostDispatchError | undefined;
428
+ const failPass = (error: HostDispatchError): void => {
429
+ if (passFailure === undefined) passFailure = error;
430
+ };
436
431
  const rejectQueuedHostCalls = (reason: Error): void => {
437
432
  for (const queued of queuedHostCalls.splice(0)) {
438
433
  queued.reject(reason);
439
434
  }
440
435
  };
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) =>
436
+ const queue = yield* Queue.unbounded<HostWork>();
437
+
438
+ const deliverHostOutcome = (
439
+ queued: QueuedHostCall,
440
+ outcome: CodeHostCallResult,
441
+ ): Effect.Effect<void, CodeExecutionProtocolError | CodeOutputLimitError> =>
488
442
  Effect.gen(function* () {
489
- propagatedHostDispatchFailure = failure;
490
- return yield* failHostDispatch(failure);
443
+ const decoded = decodeHostCallResult(outcome);
444
+ if (Option.isNone(decoded)) {
445
+ const error = CodeExecutionProtocolError.make({
446
+ implementation: dynamicWorkerImplementation,
447
+ message: "The execution host returned a value outside the CodeHostCallResult schema",
448
+ });
449
+ failPass(error);
450
+ return yield* error;
451
+ }
452
+ const encoded = encodeHostResultPayload(decoded.value);
453
+ if (encoded === undefined || encoded.resultBytes > request.limits.maxHostCallResultBytes) {
454
+ const error = CodeOutputLimitError.make({
455
+ implementation: dynamicWorkerImplementation,
456
+ surface: "host-call-result",
457
+ limit: request.limits.maxHostCallResultBytes,
458
+ observed: encoded?.resultBytes ?? 0,
459
+ logs: [],
460
+ });
461
+ failPass(error);
462
+ return yield* error;
463
+ }
464
+ const normalizedPayload: unknown = JSON.parse(encoded.encodedPayload);
465
+ queued.resolve(
466
+ decoded.value._tag === "CodeHostCallSuccess"
467
+ ? { _tag: "CodeHostCallSuccess", value: normalizedPayload }
468
+ : { _tag: "CodeHostCallFailure", error: normalizedPayload },
469
+ );
491
470
  });
492
471
 
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)),
472
+ // Workers RPC into the loader isolate cannot settle on the fiber blocked
473
+ // in `entrypoint.run()`. A Scope-owned sibling fiber keeps that
474
+ // independence while inheriting the pass Context and dying with the Scope.
475
+ const serveHostCalls = Effect.gen(function* () {
476
+ while (true) {
477
+ const work = yield* Queue.take(queue);
478
+ if (work._tag === "limit") {
479
+ const error = CodeHostCallLimitError.make({
480
+ implementation: dynamicWorkerImplementation,
481
+ limit: request.limits.maxHostCalls,
482
+ logs: [],
483
+ });
484
+ failPass(error);
485
+ return yield* error;
486
+ }
487
+ const queued = work.queued;
488
+ yield* host.call(queued.call).pipe(
505
489
  Effect.timeoutOrElse({
506
490
  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,
491
+ orElse: () => {
492
+ const error = CodeExecutionTimeoutError.make({
493
+ implementation: dynamicWorkerImplementation,
494
+ kind: "wall-clock",
495
+ maxWallTime: request.limits.maxWallTime,
496
+ logs: [],
527
497
  });
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 };
498
+ failPass(error);
499
+ return error;
500
+ },
534
501
  }),
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
- };
502
+ Effect.flatMap((outcome) => deliverHostOutcome(queued, outcome)),
503
+ Effect.tapError(() =>
504
+ Effect.sync(() => queued.reject(new Error("Code Mode host call failed"))),
505
+ ),
506
+ Effect.onInterrupt(() =>
507
+ Effect.sync(() => queued.reject(new Error("Code Mode pass is closing"))),
508
+ ),
509
+ );
510
+ }
511
+ });
512
+ const server = yield* serveHostCalls.pipe(Effect.forkScoped);
554
513
 
555
514
  const dispatch = (hostCall: unknown): Promise<unknown> => {
556
515
  if (!passOpen) {
@@ -558,7 +517,13 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
558
517
  }
559
518
  issuedHostCalls += 1;
560
519
  if (issuedHostCalls > request.limits.maxHostCalls) {
561
- recordHostDispatchFailure({ _tag: "host-call-limit" });
520
+ const error = CodeHostCallLimitError.make({
521
+ implementation: dynamicWorkerImplementation,
522
+ limit: request.limits.maxHostCalls,
523
+ logs: [],
524
+ });
525
+ failPass(error);
526
+ Queue.offerUnsafe(queue, { _tag: "limit" });
562
527
  return Promise.reject(new Error("host-call limit exceeded"));
563
528
  }
564
529
  const decoded = decodeHostCall(hostCall);
@@ -566,45 +531,23 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
566
531
  return Promise.reject(new TypeError("host calls must match the CodeHostCall schema"));
567
532
  }
568
533
  return new Promise((resolve, reject) => {
569
- queuedHostCalls.push({ call: decoded.value, resolve, reject });
570
- startNextHostCall();
534
+ const queued = { call: decoded.value, resolve, reject };
535
+ queuedHostCalls.push(queued);
536
+ Queue.offerUnsafe(queue, { _tag: "call", queued });
571
537
  });
572
538
  };
573
539
 
574
- const closeHostDispatch = Effect.gen(function* () {
540
+ const closeAdmission = Effect.sync(() => {
575
541
  passOpen = false;
576
542
  passRegistry.delete(passId);
577
543
  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
544
  });
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
545
 
603
546
  yield* Effect.acquireRelease(
604
547
  Effect.sync(() => {
605
548
  passRegistry.set(passId, { dispatch });
606
549
  }),
607
- () => releaseHostDispatch,
550
+ () => closeAdmission.pipe(Effect.andThen(Fiber.interrupt(server))),
608
551
  );
609
552
 
610
553
  // No `allowExperimental`: the runtime only accepts it when the CALLING
@@ -683,7 +626,7 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
683
626
  catch: (cause) => classifyWorkerFailure(cause, request.limits.maxWallTime),
684
627
  });
685
628
 
686
- const raw = yield* Effect.raceFirst(
629
+ const exit = yield* Effect.raceFirst(
687
630
  rpc.pipe(
688
631
  Effect.timeoutOrElse({
689
632
  duration: remainingPassWallTime(),
@@ -696,16 +639,18 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
696
639
  }),
697
640
  }),
698
641
  ),
699
- Effect.promise(() => hostDispatchFailureSignal).pipe(
700
- Effect.flatMap(propagateHostDispatchFailure),
701
- ),
702
- );
703
- yield* stopHostDispatch;
704
- const finishedAt = performance.now();
705
-
706
- if (hostDispatchFailure !== undefined) {
707
- return yield* propagateHostDispatchFailure(hostDispatchFailure);
642
+ Fiber.join(server),
643
+ ).pipe(Effect.exit);
644
+ yield* closeAdmission;
645
+ yield* Fiber.interrupt(server);
646
+ if (passFailure !== undefined) {
647
+ return yield* passFailure;
648
+ }
649
+ if (Exit.isFailure(exit)) {
650
+ return yield* Effect.failCause(exit.cause);
708
651
  }
652
+ const raw = exit.value;
653
+ const finishedAt = performance.now();
709
654
 
710
655
  const outcome = decodeHarnessOutcome(raw);
711
656
  if (Option.isNone(outcome)) {
package/src/layers.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ConversationId } from "@effect-agent/core";
2
- import { RunContextPreparation, RunContextPreparationPassthrough } from "@effect-agent/engine";
2
+ import type { RunContextPreparation } from "@effect-agent/engine";
3
+ import { RunContextPreparationPassthrough } from "@effect-agent/engine";
3
4
  import {
4
5
  AgentBindingResolver,
5
6
  DurableAgentRuntime,
@@ -29,7 +30,8 @@ import {
29
30
  } from "@effect-agent/storage-cloudflare";
30
31
  import { BrowserCrypto } from "@effect/platform-browser";
31
32
  import { SqliteClient } from "@effect/sql-sqlite-do";
32
- import { Context, Crypto, Duration, Effect, Layer, Schema } from "effect";
33
+ import type { Crypto } from "effect";
34
+ import { Context, Duration, Effect, Layer, Schema } from "effect";
33
35
 
34
36
  import {
35
37
  ConversationMaintenance,
@@ -119,9 +121,11 @@ export interface CloudflareDurableRuntimeOptions {
119
121
  */
120
122
  readonly bindings?: CloudflareBindingSource | undefined;
121
123
  /**
122
- * Generic model-context preparation acquired with this Durable Object incarnation. The Layer
123
- * may depend only on `Crypto.Crypto`, which this platform supplies with `BrowserCrypto`; hosts
124
- * must close every application-specific service before passing it here. Default absent.
124
+ * Generic Run context acquired with this Durable Object incarnation. It may prepare model
125
+ * context and/or authorize exact model-declared application Tool Calls at action time. The
126
+ * Layer may depend
127
+ * only on `Crypto.Crypto`, which this platform supplies with `BrowserCrypto`; hosts must close
128
+ * every application-specific service before passing it here. Default absent.
125
129
  */
126
130
  readonly runContext?: CloudflareRunContextSource | undefined;
127
131
  }
@@ -150,7 +154,7 @@ export type CloudflareBindingSource =
150
154
  | ReadonlyArray<ResolvedBinding>
151
155
  | Effect.Effect<ReadonlyArray<ResolvedBinding>, never, never>);
152
156
 
153
- /** A closed context-preparation service whose only remaining requirement is platform Crypto. */
157
+ /** A closed Run-context service whose only remaining requirement is platform Crypto. */
154
158
  export type CloudflareRunContextLayer = Layer.Layer<RunContextPreparation, never, Crypto.Crypto>;
155
159
 
156
160
  /** One Layer or a per-incarnation factory over explicit Cloudflare host values. */