@effect-agent/platform-cloudflare 0.1.0-beta.33 → 0.1.0-beta.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.ts CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  type DurableSubmitOptions,
32
32
  } from "@effect-agent/session";
33
33
  import { Context, Crypto, Duration, Effect, Layer, Schema } from "effect";
34
+ import { RpcTracing } from "effect-cf";
34
35
 
35
36
  import { DurableAlarmError } from "./alarm.ts";
36
37
  import { ConversationObjectNamespace, type ConversationObjectRpc } from "./bindings.ts";
@@ -318,6 +319,17 @@ const outOfContract = (
318
319
  ),
319
320
  });
320
321
 
322
+ const hostRpcMethods = {
323
+ submit: "submitEncoded",
324
+ awaitSettlement: "awaitSettlementEncoded",
325
+ awaitProgress: "awaitProgressEncoded",
326
+ cancelProgress: "cancelProgressEncoded",
327
+ observePage: "observePage",
328
+ abort: "abortEncoded",
329
+ resolveApproval: "resolveApprovalEncoded",
330
+ resolveUnknown: "resolveUnknownEncoded",
331
+ } as const satisfies Record<string, keyof ConversationObjectRpc>;
332
+
321
333
  /** Worker-side client over the Conversation Object namespace (DEPLOY-010). */
322
334
  export class CloudflareConversationClient extends Context.Service<
323
335
  CloudflareConversationClient,
@@ -379,45 +391,54 @@ export class CloudflareConversationClient extends Context.Service<
379
391
  ConversationObjectNamespace | Crypto.Crypto
380
392
  > = Layer.effect(CloudflareConversationClient)(
381
393
  Effect.gen(function* () {
382
- const { namespace } = yield* ConversationObjectNamespace;
394
+ const { namespace, rpcTracing } = yield* ConversationObjectNamespace;
383
395
  const crypto = yield* Crypto.Crypto;
384
396
 
385
- const call = (
386
- conversationId: string,
387
- operation: string,
388
- invoke: (stub: DurableObjectStub<ConversationObjectRpc>) => Promise<unknown>,
389
- ): Effect.Effect<HostResponse, ConversationClientError | HostProtocolError> =>
390
- Effect.tryPromise({
391
- try: () => invoke(namespace.get(namespace.idFromName(conversationId))),
392
- catch: (cause) =>
393
- ConversationClientError.make({
394
- conversationId,
395
- message: boundHostDiagnostic(
396
- `${operation} did not reach the Conversation Object: ${safeCauseMessage(
397
- cause,
398
- "the RPC failed without a diagnostic",
399
- )}`,
400
- ),
401
- cause,
402
- ...cloudflareFailureSignals(cause),
403
- }),
404
- }).pipe(
405
- Effect.flatMap((raw) =>
406
- decodeHostResponse(raw).pipe(
407
- Effect.mapError(
408
- (error): HostProtocolError =>
409
- HostProtocolError.make({
410
- message: boundHostDiagnostic(
411
- `The ${operation} answer could not be decoded: ${error.message}`,
412
- ),
413
- }),
414
- ),
397
+ const call = Effect.fn(
398
+ function* (
399
+ conversationId: string,
400
+ operation: keyof typeof hostRpcMethods,
401
+ encoded: unknown,
402
+ ): Effect.fn.Return<HostResponse, ConversationClientError | HostProtocolError> {
403
+ // Empty arguments preserve native arity. Passing `undefined` still adds an argument.
404
+ const traceArgs =
405
+ rpcTracing === undefined ? [] : yield* RpcTracing.withRpcTraceContext([]);
406
+ const raw = yield* Effect.tryPromise({
407
+ try: () => {
408
+ const stub = namespace.get(namespace.idFromName(conversationId));
409
+ return stub[hostRpcMethods[operation]](encoded, ...traceArgs);
410
+ },
411
+ catch: (cause) =>
412
+ ConversationClientError.make({
413
+ conversationId,
414
+ message: boundHostDiagnostic(
415
+ `${operation} did not reach the Conversation Object: ${safeCauseMessage(
416
+ cause,
417
+ "the RPC failed without a diagnostic",
418
+ )}`,
419
+ ),
420
+ cause,
421
+ ...cloudflareFailureSignals(cause),
422
+ }),
423
+ });
424
+ return yield* decodeHostResponse(raw).pipe(
425
+ Effect.mapError(
426
+ (error): HostProtocolError =>
427
+ HostProtocolError.make({
428
+ message: boundHostDiagnostic(
429
+ `The ${operation} answer could not be decoded: ${error.message}`,
430
+ ),
431
+ }),
415
432
  ),
416
- ),
417
- Effect.withSpan("CloudflareConversationClient.call", {
418
- attributes: { conversationId, operation },
419
- }),
420
- );
433
+ );
434
+ },
435
+ (effect, conversationId, operation) =>
436
+ rpcTracing === undefined
437
+ ? Effect.withSpan(effect, "CloudflareConversationClient.call", {
438
+ attributes: { conversationId, operation },
439
+ })
440
+ : RpcTracing.withRpcClientSpan(effect, rpcTracing, hostRpcMethods[operation]),
441
+ );
421
442
 
422
443
  const expect = <ResultSchema extends Schema.Top, FailureSchema extends Schema.Top>(
423
444
  conversationId: string,
@@ -464,9 +485,7 @@ export class CloudflareConversationClient extends Context.Service<
464
485
  }),
465
486
  ),
466
487
  );
467
- const response = yield* call(conversationId, "observePage", (stub) =>
468
- stub.observePage(encoded),
469
- );
488
+ const response = yield* call(conversationId, "observePage", encoded);
470
489
  const page = yield* expect(
471
490
  conversationId,
472
491
  "observePage",
@@ -482,9 +501,7 @@ export class CloudflareConversationClient extends Context.Service<
482
501
  ): Effect.Effect<void> =>
483
502
  encodeCancelProgressRequest(CancelProgressRequest.make({ waiterId })).pipe(
484
503
  Effect.mapError(() => undefined),
485
- Effect.flatMap((encoded) =>
486
- call(conversationId, "cancelProgress", (stub) => stub.cancelProgressEncoded(encoded)),
487
- ),
504
+ Effect.flatMap((encoded) => call(conversationId, "cancelProgress", encoded)),
488
505
  Effect.asVoid,
489
506
  Effect.ignore,
490
507
  );
@@ -526,9 +543,7 @@ export class CloudflareConversationClient extends Context.Service<
526
543
  }),
527
544
  ),
528
545
  );
529
- const response = yield* call(options.conversationId, "submit", (stub) =>
530
- stub.submitEncoded(encoded),
531
- );
546
+ const response = yield* call(options.conversationId, "submit", encoded);
532
547
  const succeeded = yield* expect(
533
548
  options.conversationId,
534
549
  "submit",
@@ -547,9 +562,7 @@ export class CloudflareConversationClient extends Context.Service<
547
562
  }),
548
563
  ),
549
564
  );
550
- const response = yield* call(receipt.conversationId, "awaitSettlement", (stub) =>
551
- stub.awaitSettlementEncoded(encoded),
552
- );
565
+ const response = yield* call(receipt.conversationId, "awaitSettlement", encoded);
553
566
  const settled = yield* expect(
554
567
  receipt.conversationId,
555
568
  "awaitSettlement",
@@ -582,9 +595,7 @@ export class CloudflareConversationClient extends Context.Service<
582
595
  );
583
596
 
584
597
  const attempt = (retry: number): Effect.Effect<void, ClientProgressFailure> =>
585
- call(conversationId, "awaitProgress", (stub) =>
586
- stub.awaitProgressEncoded(encoded),
587
- ).pipe(
598
+ call(conversationId, "awaitProgress", encoded).pipe(
588
599
  Effect.flatMap(
589
600
  expect(
590
601
  conversationId,
@@ -632,9 +643,7 @@ export class CloudflareConversationClient extends Context.Service<
632
643
  }),
633
644
  ),
634
645
  );
635
- const response = yield* call(conversationId, "abort", (stub) =>
636
- stub.abortEncoded(encoded),
637
- );
646
+ const response = yield* call(conversationId, "abort", encoded);
638
647
  const recorded = yield* expect(
639
648
  conversationId,
640
649
  "abort",
@@ -653,9 +662,7 @@ export class CloudflareConversationClient extends Context.Service<
653
662
  }),
654
663
  ),
655
664
  );
656
- const response = yield* call(conversationId, "resolveApproval", (stub) =>
657
- stub.resolveApprovalEncoded(encoded),
658
- );
665
+ const response = yield* call(conversationId, "resolveApproval", encoded);
659
666
  const recorded = yield* expect(
660
667
  conversationId,
661
668
  "resolveApproval",
@@ -676,9 +683,7 @@ export class CloudflareConversationClient extends Context.Service<
676
683
  }),
677
684
  ),
678
685
  );
679
- const response = yield* call(conversationId, "resolveUnknown", (stub) =>
680
- stub.resolveUnknownEncoded(encoded),
681
- );
686
+ const response = yield* call(conversationId, "resolveUnknown", encoded);
682
687
  const recorded = yield* expect(
683
688
  conversationId,
684
689
  "resolveUnknown",
@@ -35,7 +35,6 @@ import {
35
35
  encodePortResponse,
36
36
  type PortRequest,
37
37
  } from "@effect-agent/storage-cloudflare";
38
- import type { DurableObject as CloudflareDurableObject } from "cloudflare:workers";
39
38
  import { Effect, Layer, Option, Schema, Stream } from "effect";
40
39
  import {
41
40
  DurableObject as EffectCfDurableObject,
@@ -110,6 +109,8 @@ import { ProgressWaitRegistry } from "./progress-wait.ts";
110
109
 
111
110
  /** Construction options for one deployed Conversation Object class. */
112
111
  export interface ConversationObjectOptions extends CloudflareDurableRuntimeOptions {
112
+ /** Accept transient native RPC tracing through effect-cf; disabled by default. */
113
+ readonly rpcTracing?: boolean;
113
114
  /**
114
115
  * Name of the Worker `env` binding carrying THIS class's `DurableObjectNamespace` — the
115
116
  * Object's route back to sibling Conversation Objects for the WP2 cross-Object port calls
@@ -676,6 +677,7 @@ const gateEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointServices
676
677
  */
677
678
  const effectCfPlatformLayer = (
678
679
  namespaceBinding: string,
680
+ rpcTracing = false,
679
681
  ): Layer.Layer<
680
682
  DurableObjectContext | ConversationObjectNamespace,
681
683
  CloudflareBindingError,
@@ -692,22 +694,27 @@ const effectCfPlatformLayer = (
692
694
  Effect.gen(function* () {
693
695
  const env = yield* WorkerEnvironment;
694
696
  const binding = yield* conversationNamespaceFromEnv(env, namespaceBinding);
695
- return ConversationObjectNamespace.of({ namespace: binding });
697
+ return ConversationObjectNamespace.of({
698
+ namespace: binding,
699
+ ...(rpcTracing === true ? { rpcTracing: namespaceBinding } : {}),
700
+ });
696
701
  }),
697
702
  );
698
703
  return Layer.merge(context, namespace);
699
704
  };
700
705
 
701
- /** The public endpoint surface of one Conversation Object instance. */
702
- export interface ConversationObjectInstance extends CloudflareDurableObject {
703
- submitEncoded(encoded: unknown): Promise<unknown>;
704
- awaitSettlementEncoded(encoded: unknown): Promise<unknown>;
705
- awaitProgressEncoded(encoded: unknown): Promise<unknown>;
706
- cancelProgressEncoded(encoded: unknown): Promise<unknown>;
707
- observePage(encoded: unknown): Promise<unknown>;
708
- abortEncoded(encoded: unknown): Promise<unknown>;
709
- resolveApprovalEncoded(encoded: unknown): Promise<unknown>;
710
- resolveUnknownEncoded(encoded: unknown): Promise<unknown>;
706
+ /** The public endpoints and effect-cf invocation hook of one Conversation Object instance. */
707
+ export interface ConversationObjectInstance<EventServices = never> extends InstanceType<
708
+ EffectCfDurableObject.DurableObjectClass<Record<never, never>, RuntimeServices | EventServices>
709
+ > {
710
+ submitEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
711
+ awaitSettlementEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
712
+ awaitProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
713
+ cancelProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
714
+ observePage(encoded: unknown, traceContext?: unknown): Promise<unknown>;
715
+ abortEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
716
+ resolveApprovalEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
717
+ resolveUnknownEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
711
718
  explainEncoded(encoded: unknown): Promise<unknown>;
712
719
  verifyEncoded(encoded: unknown): Promise<unknown>;
713
720
  retryEncoded(encoded: unknown): Promise<unknown>;
@@ -718,8 +725,8 @@ export interface ConversationObjectInstance extends CloudflareDurableObject {
718
725
  }
719
726
 
720
727
  /** The constructor shape workerd instantiates for each Conversation Object. */
721
- export interface ConversationObjectClass {
722
- new (ctx: DurableObjectState, env: Cloudflare.Env): ConversationObjectInstance;
728
+ export interface ConversationObjectClass<EventServices = never> {
729
+ new (ctx: DurableObjectState, env: Cloudflare.Env): ConversationObjectInstance<EventServices>;
723
730
  }
724
731
 
725
732
  /**
@@ -739,13 +746,13 @@ export const makeConversationObjectClass = <EventLayerError = never, EventServic
739
746
  | EffectCfDurableObjectState.DurableObjectState
740
747
  | WorkerEnvironment
741
748
  >,
742
- ): ConversationObjectClass => {
749
+ ): ConversationObjectClass<EventServices> => {
743
750
  const application: Layer.Layer<
744
751
  RuntimeServices,
745
752
  CloudflareDurableRuntimeInitializationError | CloudflareBindingError,
746
753
  EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
747
754
  > = CloudflareDurableRuntime.layer(options).pipe(
748
- Layer.provideMerge(effectCfPlatformLayer(options.namespaceBinding)),
755
+ Layer.provideMerge(effectCfPlatformLayer(options.namespaceBinding, options.rpcTracing)),
749
756
  );
750
757
 
751
758
  // The storage/config Layer must acquire inside Cloudflare's constructor gate. effect-cf owns
@@ -793,6 +800,7 @@ export const makeConversationObjectClass = <EventLayerError = never, EventServic
793
800
  EventLayerError,
794
801
  typeof rpc
795
802
  >(runtime, {
803
+ ...(options.rpcTracing === true ? { rpcTracing: { service: options.namespaceBinding } } : {}),
796
804
  ...(observability === undefined ? {} : { eventLayer: observability }),
797
805
  // Force the gated runtime Layer when Cloudflare loads this Object incarnation. Recovery stays
798
806
  // in each bounded pass so cross-Object initialization cannot deadlock.
@@ -22,10 +22,13 @@ import {
22
22
  InteractiveBrowserPolicy,
23
23
  InteractiveBrowserPolicyDeniedError,
24
24
  InteractiveBrowserProtocolError,
25
+ InteractiveBrowserTargetUrl,
26
+ InteractiveBrowserUnsupportedError,
25
27
  PageScreenshotResult,
26
28
  SandboxImplementation,
27
29
  type BrowserHandle,
28
30
  type InteractiveBrowserError,
31
+ type InteractiveBrowserNetworkPolicy,
29
32
  } from "@effect-agent/sandbox";
30
33
  import {
31
34
  Context,
@@ -170,7 +173,7 @@ type BrowserFailure = typeof InteractiveBrowserError.Type;
170
173
  type BrowserOperation = InteractiveBrowserActionError["operation"];
171
174
 
172
175
  interface InteractiveBrowserPolicySnapshot {
173
- readonly allowedHosts: ReadonlyArray<string>;
176
+ readonly network: Exclude<InteractiveBrowserNetworkPolicy, { readonly _tag: "PublicWeb" }>;
174
177
  readonly maxActions: number;
175
178
  readonly maxElapsedMillis: number;
176
179
  readonly maxReturnedBytes: number;
@@ -443,29 +446,46 @@ const isRemoteClosure = (cause: unknown): boolean =>
443
446
  causeText(cause),
444
447
  );
445
448
 
446
- const snapshotPolicy = (
449
+ const snapshotPolicy = Effect.fn("BrowserRunInteractive.snapshotPolicy")(function* (
447
450
  input: InteractiveBrowserPolicy,
448
- ): Effect.Effect<InteractiveBrowserPolicySnapshot, InteractiveBrowserPolicyDeniedError> =>
449
- Schema.decodeUnknownEffect(InteractiveBrowserPolicy)(input).pipe(
451
+ ): Effect.fn.Return<
452
+ InteractiveBrowserPolicySnapshot,
453
+ InteractiveBrowserPolicyDeniedError | InteractiveBrowserUnsupportedError
454
+ > {
455
+ const decoded = yield* Schema.decodeUnknownEffect(InteractiveBrowserPolicy)(input).pipe(
450
456
  Effect.mapError(() => policyError("The interactive browser policy is malformed")),
451
- Effect.map((decoded) =>
452
- Object.freeze({
453
- allowedHosts: Object.freeze([...decoded.allowedHosts]),
454
- maxActions: decoded.maxActions,
455
- maxElapsedMillis: decoded.maxElapsedMillis,
456
- maxReturnedBytes: decoded.maxReturnedBytes,
457
- }),
458
- ),
459
457
  );
458
+ if (decoded.network._tag === "PublicWeb") {
459
+ return yield* InteractiveBrowserUnsupportedError.make({
460
+ implementation: browserRunInteractiveImplementation,
461
+ feature: "policy",
462
+ message:
463
+ "Cloudflare Browser Run cannot enforce the PublicWeb network policy for all session traffic",
464
+ });
465
+ }
466
+ return Object.freeze({
467
+ network:
468
+ decoded.network._tag === "ExactHosts"
469
+ ? Object.freeze({
470
+ _tag: decoded.network._tag,
471
+ allowedHosts: Object.freeze([...decoded.network.allowedHosts]),
472
+ })
473
+ : Object.freeze({ _tag: decoded.network._tag }),
474
+ maxActions: decoded.maxActions,
475
+ maxElapsedMillis: decoded.maxElapsedMillis,
476
+ maxReturnedBytes: decoded.maxReturnedBytes,
477
+ });
478
+ });
460
479
 
461
480
  const hostAllowed = (policy: InteractiveBrowserPolicySnapshot, value: string): boolean => {
481
+ if (policy.network._tag === "Unrestricted") return true;
462
482
  try {
463
483
  const url = new URL(value);
464
484
  return (
465
485
  url.protocol === "https:" &&
466
486
  url.username === "" &&
467
487
  url.password === "" &&
468
- policy.allowedHosts.some((host) => host === url.host)
488
+ policy.network.allowedHosts.some((host) => host === url.host)
469
489
  );
470
490
  } catch {
471
491
  return false;
@@ -756,7 +776,7 @@ const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (
756
776
  return yield* decodeNavigationResult(page, policy);
757
777
  }),
758
778
  Effect.suspend(() =>
759
- hostAllowed(policy, request.url)
779
+ Schema.is(InteractiveBrowserTargetUrl)(request.url) && hostAllowed(policy, request.url)
760
780
  ? Effect.void
761
781
  : Effect.fail(policyError("The navigation URL is outside the browser policy")),
762
782
  ),
package/src/layers.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import { ConversationId } from "@effect-agent/core";
2
- import type { RunContextPreparation, RunCostEstimator } from "@effect-agent/engine";
3
- import { RunContextPreparationPassthrough } from "@effect-agent/engine";
2
+ import type {
3
+ RunContextPreparation,
4
+ RunCostEstimator,
5
+ RunToolFailureObserver,
6
+ } from "@effect-agent/engine";
7
+ import {
8
+ CurrentToolFailureObserver,
9
+ RunContextPreparationPassthrough,
10
+ toolFailureObserverLayer,
11
+ } from "@effect-agent/engine";
4
12
  import {
5
13
  AgentBindingResolver,
6
14
  DurableAgentRuntime,
@@ -82,6 +90,8 @@ export interface CloudflareDurableRuntimeOptions {
82
90
  readonly abortPollInterval?: number | undefined;
83
91
  /** Deployment-owned pricing authority used by durable cost budgets and settlements. */
84
92
  readonly estimateCostMicrousd?: RunCostEstimator | undefined;
93
+ /** Closed trusted Tool failure reporting. Omission masks ambient observers at construction. */
94
+ readonly toolFailureObserver?: RunToolFailureObserver | undefined;
85
95
  /** Milliseconds; default 25. */
86
96
  readonly observationPollInterval?: number | undefined;
87
97
  /** Bytes; default just under the 2 MB platform value limit. */
@@ -382,6 +392,10 @@ export class CloudflareDurableRuntime {
382
392
  options.operationAuthorizer === undefined
383
393
  ? Layer.empty
384
394
  : operationAuthorizerLayer(options.operationAuthorizer);
395
+ const observerLayer =
396
+ options.toolFailureObserver === undefined
397
+ ? Layer.succeed(CurrentToolFailureObserver)(undefined)
398
+ : toolFailureObserverLayer(options.toolFailureObserver);
385
399
  const bindingResolverLayer = Layer.effect(AgentBindingResolver)(
386
400
  Effect.map(
387
401
  resolveBindings(options.bindings, { ctx, env, conversationId, producerId }),
@@ -413,6 +427,7 @@ export class CloudflareDurableRuntime {
413
427
  runtimeFailpointLayer,
414
428
  reconcilerLayer,
415
429
  authorizerLayer,
430
+ observerLayer,
416
431
  runContextLayer,
417
432
  BrowserCrypto.layer,
418
433
  ),