@yielded/agent-platform-cloudflare 0.1.0-beta.167 → 0.1.0-beta.168

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 (97) hide show
  1. package/dist/{Alarm-B0ODr1NE.mjs → Alarm-DxODgAZZ.mjs} +259 -159
  2. package/dist/Alarm-DxODgAZZ.mjs.map +1 -0
  3. package/dist/Alarm.d.mts +27 -36
  4. package/dist/Alarm.mjs +2 -2
  5. package/dist/BrowserCredentials-C5IWfi7f.d.mts +91 -0
  6. package/dist/BrowserCredentials.d.mts +2 -91
  7. package/dist/BrowserRestCapture.mjs +3 -3
  8. package/dist/BrowserRestCapture.mjs.map +1 -1
  9. package/dist/BrowserRestCrawl.mjs +6 -6
  10. package/dist/BrowserRestCrawl.mjs.map +1 -1
  11. package/dist/{BrowserSession-RfIzxGCH.mjs → BrowserSession-BfNmyayH.mjs} +5 -5
  12. package/dist/BrowserSession-BfNmyayH.mjs.map +1 -0
  13. package/dist/{BrowserSession-B969Xg-2.d.mts → BrowserSession-Bx0V1-6_.d.mts} +6 -3
  14. package/dist/BrowserSession.d.mts +1 -1
  15. package/dist/BrowserSession.mjs +2 -2
  16. package/dist/BrowserUse-AstK0jKX.mjs +1467 -0
  17. package/dist/BrowserUse-AstK0jKX.mjs.map +1 -0
  18. package/dist/BrowserUse.d.mts +83 -0
  19. package/dist/BrowserUse.mjs +2 -0
  20. package/dist/CloudflareBindings.d.mts +8 -1
  21. package/dist/CloudflareBindings.mjs +2 -2
  22. package/dist/CloudflareBindings.mjs.map +1 -1
  23. package/dist/{CloudflareBrowser-5an6m-Qb.mjs → CloudflareBrowser-Vt8qTdYW.mjs} +11 -13
  24. package/dist/CloudflareBrowser-Vt8qTdYW.mjs.map +1 -0
  25. package/dist/CloudflareBrowser.mjs +1 -1
  26. package/dist/CloudflareCodeMode.mjs +1 -1
  27. package/dist/CloudflareCodeMode.mjs.map +1 -1
  28. package/dist/{CloudflareConfig-qDrRLofL.d.mts → CloudflareConfig-C9MvPbX8.d.mts} +9 -11
  29. package/dist/CloudflareConfig.d.mts +1 -1
  30. package/dist/CloudflareConfig.mjs +9 -12
  31. package/dist/CloudflareConfig.mjs.map +1 -1
  32. package/dist/CloudflareMemory.d.mts +3 -25
  33. package/dist/CloudflareMemory.mjs +33 -31
  34. package/dist/CloudflareMemory.mjs.map +1 -1
  35. package/dist/CloudflareScheduling.mjs +8 -15
  36. package/dist/CloudflareScheduling.mjs.map +1 -1
  37. package/dist/CloudflareSubscriptions.mjs +9 -16
  38. package/dist/CloudflareSubscriptions.mjs.map +1 -1
  39. package/dist/CloudflareThreadClient-CZm0TvUe.mjs +588 -0
  40. package/dist/CloudflareThreadClient-CZm0TvUe.mjs.map +1 -0
  41. package/dist/CloudflareThreadClient-QsVzLGGZ.d.mts +2033 -0
  42. package/dist/CloudflareThreadClient.d.mts +2 -1796
  43. package/dist/CloudflareThreadClient.mjs +2 -382
  44. package/dist/{InteractiveBrowser-BbG3kGA9.mjs → InteractiveBrowser-LdKzQeLR.mjs} +18 -19
  45. package/dist/InteractiveBrowser-LdKzQeLR.mjs.map +1 -0
  46. package/dist/InteractiveBrowser.mjs +1 -1
  47. package/dist/{ThreadObject-BcfXbzSO.d.mts → ThreadObject-7s5XnlH-.d.mts} +103 -79
  48. package/dist/{ThreadObject-DHjrnlX4.mjs → ThreadObject-C0vrcDrw.mjs} +265 -66
  49. package/dist/ThreadObject-C0vrcDrw.mjs.map +1 -0
  50. package/dist/ThreadObject.d.mts +2 -2
  51. package/dist/ThreadObject.mjs +2 -3
  52. package/dist/WakeScheduler.d.mts +6 -5
  53. package/dist/WakeScheduler.mjs +15 -11
  54. package/dist/WakeScheduler.mjs.map +1 -1
  55. package/dist/index.d.mts +7 -6
  56. package/dist/index.mjs +7 -6
  57. package/dist/page-function-CmUCcyNP.mjs +11 -0
  58. package/dist/page-function-CmUCcyNP.mjs.map +1 -0
  59. package/dist/{prepared-admission-DMX5p9dC.mjs → prepared-admission-3UUq0zth.mjs} +3 -3
  60. package/dist/{prepared-admission-DMX5p9dC.mjs.map → prepared-admission-3UUq0zth.mjs.map} +1 -1
  61. package/package.json +1 -1
  62. package/src/Alarm.ts +510 -279
  63. package/src/BrowserRestCapture.ts +2 -2
  64. package/src/BrowserRestCrawl.ts +10 -14
  65. package/src/BrowserSession.ts +10 -2
  66. package/src/BrowserUse.ts +1541 -0
  67. package/src/CloudflareBindings.ts +13 -4
  68. package/src/CloudflareCodeMode.ts +1 -1
  69. package/src/CloudflareConfig.ts +9 -12
  70. package/src/CloudflareMemory.ts +21 -28
  71. package/src/CloudflareScheduling.ts +7 -15
  72. package/src/CloudflareSubscriptions.ts +11 -20
  73. package/src/CloudflareThreadClient.ts +289 -9
  74. package/src/InteractiveBrowser.ts +325 -323
  75. package/src/ThreadObject.ts +172 -52
  76. package/src/WakeScheduler.ts +29 -18
  77. package/src/index.ts +1 -0
  78. package/src/internal/browser-dom.ts +845 -0
  79. package/src/internal/browser-file-selection.ts +8 -1
  80. package/src/internal/browser-jev-dom.ts +344 -0
  81. package/src/internal/browser-quick-action.ts +7 -8
  82. package/src/internal/browser-session-lifecycle.ts +2 -5
  83. package/src/internal/crypto.ts +41 -0
  84. package/src/internal/due-queue.ts +5 -69
  85. package/src/internal/layers.ts +100 -17
  86. package/src/internal/live-text-protocol.ts +51 -0
  87. package/src/internal/live-text.ts +256 -0
  88. package/src/internal/page-function.ts +12 -0
  89. package/src/internal/prepared-admission.ts +1 -1
  90. package/src/internal/progress-wait.ts +19 -24
  91. package/src/internal/transport.ts +1 -1
  92. package/dist/Alarm-B0ODr1NE.mjs.map +0 -1
  93. package/dist/BrowserSession-RfIzxGCH.mjs.map +0 -1
  94. package/dist/CloudflareBrowser-5an6m-Qb.mjs.map +0 -1
  95. package/dist/CloudflareThreadClient.mjs.map +0 -1
  96. package/dist/InteractiveBrowser-BbG3kGA9.mjs.map +0 -1
  97. package/dist/ThreadObject-DHjrnlX4.mjs.map +0 -1
@@ -1,4 +1,3 @@
1
- import { BrowserCrypto } from "@effect/platform-browser";
2
1
  import { SqliteClient } from "@effect/sql-sqlite-do";
3
2
  import { doMessageDeliveryStoreLayer } from "@yielded/agent-storage-cloudflare/do-message-delivery-store";
4
3
  import {
@@ -24,6 +23,7 @@ import {
24
23
  routedMessageDeliveryStoreLayer,
25
24
  routedThreadStoreLayer,
26
25
  routedSubmissionLedgerLayer,
26
+ routedSettlementPublisherLayer,
27
27
  routedWorkerAdmissionLayer,
28
28
  } from "@yielded/agent-storage-cloudflare/port-routing";
29
29
  import {
@@ -70,6 +70,9 @@ import {
70
70
  RunToolAuthorization,
71
71
  toolFailureObserverLayer,
72
72
  } from "@yielded/agent/run-options";
73
+ import type { RunStorage } from "@yielded/agent/run-storage";
74
+ import { layer as runStorageLayer, StorageOwner } from "@yielded/agent/run-storage";
75
+ import { SettlementPublisher, validatePublication } from "@yielded/agent/settlement-publisher";
73
76
  import { SqlStorageOwner } from "@yielded/agent/sql-memory-store";
74
77
  import {
75
78
  LedgerError,
@@ -80,7 +83,8 @@ import {
80
83
  type WorkerStopCommand,
81
84
  } from "@yielded/agent/submission-ledger";
82
85
  import { ThreadProjectionMaintenance } from "@yielded/agent/thread-projection-maintenance";
83
- import { ThreadStoreError, ThreadStore } from "@yielded/agent/thread-store";
86
+ import type { ThreadReader } from "@yielded/agent/thread-store";
87
+ import { AppendResult, ThreadStoreError, ThreadStore } from "@yielded/agent/thread-store";
84
88
  import { ToolReconciler } from "@yielded/agent/tool-reconciler";
85
89
  import { type WakeScheduler } from "@yielded/agent/wake-scheduler";
86
90
  import {
@@ -125,7 +129,9 @@ import {
125
129
  } from "../CloudflareConfig.ts";
126
130
  import { CloudflareThreadClient } from "../CloudflareThreadClient.ts";
127
131
  import { cloudflareWakeSchedulerLayer } from "../WakeScheduler.ts";
132
+ import { cloudflareCryptoLayer } from "./crypto.ts";
128
133
  import * as DueQueue from "./due-queue.ts";
134
+ import { LiveTextHub } from "./live-text.ts";
129
135
  import {
130
136
  guardedMessageDeliveryStoreLayer,
131
137
  threadMessageDeliveryLayer,
@@ -144,16 +150,12 @@ export interface CloudflareDurableRuntimeOptions {
144
150
  readonly deploymentId: string;
145
151
  /** Head of the minted producer identity `{producerPrefix}:{threadId}`. */
146
152
  readonly producerPrefix: string;
147
- /** Milliseconds; default 30s (D5). */
148
- readonly ownershipLeaseDuration?: number | undefined;
149
153
  /** Milliseconds; default 100. */
150
154
  readonly alarmBackoffBase?: number | undefined;
151
155
  /** Failed/no-progress exponential backoff ceiling in milliseconds; default 5000. */
152
156
  readonly alarmBackoffCap?: number | undefined;
153
157
  /** Milliseconds; default 500. */
154
158
  readonly settlementPollInterval?: number | undefined;
155
- /** Milliseconds; default 10000. */
156
- readonly leaseRenewalInterval?: number | undefined;
157
159
  /** Milliseconds; default 500. */
158
160
  readonly abortPollInterval?: number | undefined;
159
161
  /** Deployment-owned pricing authority used by durable cost budgets and settlements. */
@@ -210,7 +212,8 @@ export type CloudflareBootstrapServices =
210
212
  | ThreadMaintenanceFailpoint
211
213
  | RunContextPreparation
212
214
  | RunToolAuthorization
213
- | ToolReconciler;
215
+ | ToolReconciler
216
+ | LiveTextHub;
214
217
 
215
218
  /** Every construction failure of the assembled Cloudflare durable runtime stack. */
216
219
  export type CloudflareDurableRuntimeInitializationError =
@@ -228,6 +231,10 @@ export type CloudflareDurableRuntimeServices =
228
231
  | DurableAgentRuntime
229
232
  | SubmissionLedger
230
233
  | ThreadStore
234
+ | ThreadReader
235
+ | RunStorage
236
+ | StorageOwner
237
+ | SettlementPublisher
231
238
  | MessageDeliveryStore
232
239
  | WakeScheduler
233
240
  | DurableAlarmService
@@ -269,14 +276,10 @@ const configFromOptions = (
269
276
  decodeConfigValue({
270
277
  deploymentId: options.deploymentId,
271
278
  producerPrefix: options.producerPrefix,
272
- ownershipLeaseDuration:
273
- options.ownershipLeaseDuration ?? CLOUDFLARE_RUNTIME_DEFAULTS.ownershipLeaseDuration,
274
279
  alarmBackoffBase: options.alarmBackoffBase ?? CLOUDFLARE_RUNTIME_DEFAULTS.alarmBackoffBase,
275
280
  alarmBackoffCap: options.alarmBackoffCap ?? CLOUDFLARE_RUNTIME_DEFAULTS.alarmBackoffCap,
276
281
  settlementPollInterval:
277
282
  options.settlementPollInterval ?? CLOUDFLARE_RUNTIME_DEFAULTS.settlementPollInterval,
278
- leaseRenewalInterval:
279
- options.leaseRenewalInterval ?? CLOUDFLARE_RUNTIME_DEFAULTS.leaseRenewalInterval,
280
283
  abortPollInterval: options.abortPollInterval ?? CLOUDFLARE_RUNTIME_DEFAULTS.abortPollInterval,
281
284
  observationPollInterval:
282
285
  options.observationPollInterval ?? CLOUDFLARE_RUNTIME_DEFAULTS.observationPollInterval,
@@ -353,13 +356,13 @@ const runtimeConfigLayer = (
353
356
  deploymentId: config.deploymentId,
354
357
  producerId,
355
358
  settlementPollInterval: Duration.millis(config.settlementPollInterval),
356
- leaseRenewalInterval: Duration.millis(config.leaseRenewalInterval),
357
359
  abortPollInterval: Duration.millis(config.abortPollInterval),
358
360
  ...(options.estimateCostMicrousd === undefined
359
361
  ? {}
360
362
  : { estimateCostMicrousd: options.estimateCostMicrousd }),
361
363
  }),
362
- BrowserCrypto.layer,
364
+ cloudflareCryptoLayer,
365
+ LiveTextHub.layer,
363
366
  storageFailpointLayer({ storage: ctx.storage, failpoint: options.storageFailpoint?.(ctx) }),
364
367
  options.runtimeFailpoint === undefined
365
368
  ? DurableRuntimeFailpoint.layer
@@ -405,7 +408,10 @@ export const layerConfig = (
405
408
  return Layer.mergeAll(
406
409
  runtimeConfigLayer(options, producerId),
407
410
  Layer.succeed(ThreadObjectIdentity, { threadId, producerId }),
408
- Layer.succeed(ThreadObjectPlacement, { ownsThread: (target) => target === threadId }),
411
+ Layer.succeed(ThreadObjectPlacement, {
412
+ threadId,
413
+ ownsThread: (target) => target === threadId,
414
+ }),
409
415
  );
410
416
  }),
411
417
  );
@@ -609,12 +615,12 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
609
615
  Effect.gen(function* () {
610
616
  const { ctx } = yield* DurableObjectContext;
611
617
  const config = yield* CloudflareDurableRuntimeConfig;
612
- const { ownsThread } = yield* ThreadObjectPlacement;
618
+ const placement = yield* ThreadObjectPlacement;
619
+ const { ownsThread } = placement;
613
620
 
614
621
  const storageOptions: DoStorageOptions = {
615
622
  storage: ctx.storage,
616
623
  observationPollInterval: config.observationPollInterval,
617
- ownershipLeaseDuration: config.ownershipLeaseDuration,
618
624
  maxStoredValueBytes: config.maxStoredValueBytes,
619
625
  verifyOnOpen: config.verifyOnOpen,
620
626
  };
@@ -748,6 +754,8 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
748
754
  Effect.gen(function* () {
749
755
  const store = yield* ThreadStore;
750
756
  const ledger = yield* SubmissionLedger;
757
+ const storageOwner = yield* StorageOwner;
758
+ const settlementPublisher = yield* SettlementPublisher;
751
759
  const mutations = yield* ThreadMutationGate;
752
760
  const index = yield* ThreadProjectionMaintenance;
753
761
  const publish = yield* Effect.context<ThreadPublication>();
@@ -879,6 +887,64 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
879
887
  const stopWorker = ledger.stopWorker;
880
888
 
881
889
  return Context.make(ThreadStore, observedStore).pipe(
890
+ Context.add(StorageOwner, storageOwner),
891
+ Context.add(SettlementPublisher, {
892
+ publish: (input) =>
893
+ Effect.gen(function* () {
894
+ const { request } = yield* validatePublication(input);
895
+
896
+ return yield* mutations
897
+ .withMutation(
898
+ sourceCommits
899
+ .withPermit(
900
+ settlementPublisher.publish(request).pipe(
901
+ Effect.tap((result) =>
902
+ result.replayed
903
+ ? Effect.void
904
+ : index
905
+ .applyCommitted(
906
+ request.append,
907
+ AppendResult.make({
908
+ firstSequence: result.tailSequence,
909
+ lastSequence: result.tailSequence,
910
+
911
+ replayed: false,
912
+ }),
913
+ )
914
+ .pipe(
915
+ Effect.catchCause((cause) =>
916
+ Cause.hasInterrupts(cause)
917
+ ? Effect.interrupt
918
+ : Effect.logError(
919
+ "Thread projection deferred after settlement publication",
920
+ cause,
921
+ ),
922
+ ),
923
+ ),
924
+ ),
925
+ ),
926
+ )
927
+ .pipe(Effect.tap(() => afterCommit)),
928
+ {
929
+ lanes: [
930
+ ...(options.projection === undefined ? [] : [DueQueue.Projection]),
931
+ ...(options.lifecyclePublication === undefined
932
+ ? []
933
+ : [DueQueue.Lifecycle]),
934
+ ],
935
+ },
936
+ )
937
+ .pipe(
938
+ Effect.catchTag("DurableAlarmError", (cause) =>
939
+ ThreadStoreError.make({
940
+ operation: "prearm settlement publication",
941
+ message: cause.message,
942
+ cause,
943
+ }),
944
+ ),
945
+ );
946
+ }),
947
+ }),
882
948
  Context.add(SubmissionLedger, {
883
949
  ...ledger,
884
950
  ...(options.hostLanesForMutation === undefined
@@ -936,6 +1002,7 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
936
1002
  Effect.gen(function* () {
937
1003
  const local = yield* Effect.context<
938
1004
  | SubmissionLedger
1005
+ | SettlementPublisher
939
1006
  | ThreadStore
940
1007
  | MessageDeliveryStore
941
1008
  | WakeScheduler
@@ -953,7 +1020,9 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
953
1020
  ).pipe(Layer.provide(localPorts), Layer.provide(messageStore), Layer.provide(wakes));
954
1021
 
955
1022
  const routedPorts = Layer.mergeAll(
1023
+ Layer.effect(StorageOwner, StorageOwner),
956
1024
  routedSubmissionLedgerLayer({ ownsThread }),
1025
+ routedSettlementPublisherLayer({ ownsThread }),
957
1026
  routedThreadStoreLayer({ ownsThread }),
958
1027
  routedWorkerAdmissionLayer({ ownsThread }),
959
1028
  ).pipe(Layer.provide(localPorts), Layer.provide(threadPortTransportLayer));
@@ -964,6 +1033,7 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
964
1033
  );
965
1034
 
966
1035
  const runtimeStack = application.pipe(
1036
+ Layer.provideMerge(runStorageLayer()),
967
1037
  Layer.provide(
968
1038
  cloudflarePreparedInputAdmissionLayer.pipe(Layer.provide(CloudflareThreadClient.layer)),
969
1039
  ),
@@ -986,7 +1056,7 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
986
1056
  const storage = store.lifecyclePublications;
987
1057
 
988
1058
  const context = yield* Effect.context<
989
- LifecyclePublicationHandler | ThreadStore | SubmissionLedger
1059
+ LifecyclePublicationHandler | ThreadStore | ThreadReader | SubmissionLedger
990
1060
  >();
991
1061
 
992
1062
  const failure = (cause: unknown) => {
@@ -1050,6 +1120,19 @@ const sharedLayer = <A, E, R, PE = never, PR = never>(
1050
1120
  maintenanceRuntime,
1051
1121
  ThreadMaintenance.layer.pipe(
1052
1122
  Layer.provide(options.recoveryEvents ?? Layer.empty),
1123
+ Layer.provide(
1124
+ Layer.effect(ThreadNativeMaintenance)(
1125
+ Effect.gen(function* () {
1126
+ const previous = yield* ThreadNativeMaintenance;
1127
+ const runtime = yield* DurableAgentRuntime;
1128
+
1129
+ return {
1130
+ lanes: previous.lanes,
1131
+ ...(placement.threadId === undefined ? {} : { singleThreadRuntime: runtime }),
1132
+ };
1133
+ }),
1134
+ ),
1135
+ ),
1053
1136
  Layer.provide(maintenanceRuntime),
1054
1137
  Layer.provide(messageRecovery),
1055
1138
  ),
@@ -0,0 +1,51 @@
1
+ import { ThreadId } from "@yielded/agent/identifiers";
2
+ import { Event } from "@yielded/agent/provisional-text";
3
+ import { Schema } from "effect";
4
+
5
+ export const MAX_FRAME_BYTES = 32 * 1_024;
6
+ export const MAX_ID_LENGTH = 256;
7
+ export const MAX_DELTA_LENGTH = 4_096;
8
+ export const BUFFER_SIZE = 32;
9
+ export const MAX_OBSERVERS = 8;
10
+ export const MAX_DRAFT_PARTS = 32;
11
+ export const MAX_DRAFT_LENGTH = 2 * 1_024 * 1_024;
12
+
13
+ const streamId = Schema.NonEmptyString.check(Schema.isMaxLength(MAX_ID_LENGTH));
14
+
15
+ export const Request = Schema.Struct({ schemaVersion: Schema.Literal(1) });
16
+
17
+ /**
18
+ * Reset replaces all drafts with a snapshot, streamed as native Text parts before live deltas.
19
+ * Each snapshot starts a fresh streamId. Its sequences are unrelated to canonical sequences.
20
+ */
21
+ export const Frame = Schema.Union([
22
+ Schema.TaggedStruct("Reset", {
23
+ schemaVersion: Schema.Literal(1),
24
+ streamId,
25
+ threadId: ThreadId,
26
+ sequence: Schema.Literal(0),
27
+ }),
28
+ Schema.TaggedStruct("Event", {
29
+ schemaVersion: Schema.Literal(1),
30
+ streamId,
31
+ sequence: Schema.Int.check(Schema.isGreaterThan(0)),
32
+ event: Event,
33
+ }),
34
+ ]);
35
+
36
+ export type Frame = typeof Frame.Type;
37
+
38
+ /** Bounds are checked before queueing; provider metadata is deliberately absent. */
39
+ export const isBoundedEvent = (event: Event): boolean => {
40
+ const ids: string[] = [event.threadId, event.submissionId, event.attemptId];
41
+
42
+ if (event._tag !== "AttemptEnded") ids.push(event.runId, event.turnId);
43
+ if (ids.some((id) => id.length > MAX_ID_LENGTH)) return false;
44
+
45
+ return (
46
+ event._tag !== "Text" ||
47
+ (event.part.id.length <= MAX_ID_LENGTH &&
48
+ Object.keys(event.part.metadata).length === 0 &&
49
+ (event.part.type !== "text-delta" || event.part.delta.length <= MAX_DELTA_LENGTH))
50
+ );
51
+ };
@@ -0,0 +1,256 @@
1
+ import { type ThreadId } from "@yielded/agent/identifiers";
2
+ import { Publisher, type Event } from "@yielded/agent/provisional-text";
3
+ import { Cause, Context, Effect, Exit, Layer, Queue, Schema, Scope, Stream } from "effect";
4
+ import * as Response from "effect/ai/Response";
5
+
6
+ import { HostProtocolError } from "../CloudflareThreadClient.ts";
7
+ import {
8
+ BUFFER_SIZE,
9
+ Frame,
10
+ isBoundedEvent,
11
+ MAX_DELTA_LENGTH,
12
+ MAX_DRAFT_LENGTH,
13
+ MAX_DRAFT_PARTS,
14
+ MAX_FRAME_BYTES,
15
+ MAX_ID_LENGTH,
16
+ MAX_OBSERVERS,
17
+ } from "./live-text-protocol.ts";
18
+
19
+ type TextEvent = Extract<Event, { readonly _tag: "Text" }>;
20
+
21
+ interface Draft {
22
+ readonly start: TextEvent;
23
+ text: string;
24
+ ended: boolean;
25
+ }
26
+
27
+ interface Observer {
28
+ readonly threadId: ThreadId;
29
+ readonly queue: Queue.Queue<Event | undefined, HostProtocolError>;
30
+ snapshot: boolean;
31
+ streamId: string;
32
+ sequence: number;
33
+ }
34
+
35
+ /** One Object incarnation; owns drafts and subscriptions independently of RPC event scopes. */
36
+ export class LiveTextHub extends Context.Service<
37
+ LiveTextHub,
38
+ {
39
+ readonly open: (
40
+ threadId: ThreadId,
41
+ ) => Effect.Effect<ReadableStream<Uint8Array>, HostProtocolError>;
42
+ }
43
+ >()("@effect-agent/platform-cloudflare/LiveTextHub") {
44
+ static readonly layer: Layer.Layer<LiveTextHub> = Layer.effectContext(
45
+ Effect.gen(function* () {
46
+ const owner = yield* Effect.scope;
47
+ const observers = new Set<Observer>();
48
+ const drafts = new Map<string, Draft>();
49
+ let length = 0;
50
+
51
+ let unavailable:
52
+ | { readonly error: HostProtocolError; readonly attemptId: Event["attemptId"] }
53
+ | undefined;
54
+
55
+ const encode = Schema.encodeEffect(Schema.fromJsonString(Frame));
56
+ const encoder = new TextEncoder();
57
+
58
+ yield* Effect.addFinalizer(() => Effect.sync(() => drafts.clear()));
59
+
60
+ const fail = (attemptId: Event["attemptId"]) => {
61
+ const error = HostProtocolError.make({
62
+ message:
63
+ "Provisional text exceeds its draft bounds or is invalid; read canonical records",
64
+ });
65
+
66
+ unavailable = { error, attemptId };
67
+ drafts.clear();
68
+ length = 0;
69
+ for (const observer of observers) {
70
+ observer.snapshot = true;
71
+ Queue.failCauseUnsafe(observer.queue, Cause.fail(error));
72
+ Queue.shutdownUnsafe(observer.queue);
73
+ }
74
+
75
+ return false;
76
+ };
77
+
78
+ const offerUnsafe = (event: Event): boolean => {
79
+ // Retain drafts even without subscribers. Invalid input disables previews until an
80
+ // offending Attempt ends; canonical execution never depends on the observer's state.
81
+ if (event._tag === "AttemptEnded" && event.attemptId === unavailable?.attemptId)
82
+ unavailable = undefined;
83
+ if (unavailable !== undefined) return false;
84
+ if (!isBoundedEvent(event)) {
85
+ // Drop an oversized terminal frame without disabling an already-ended Attempt.
86
+ return event._tag === "AttemptEnded" ? false : fail(event.attemptId);
87
+ }
88
+
89
+ if (event._tag === "Text") {
90
+ const key = JSON.stringify([
91
+ event.threadId,
92
+ event.submissionId,
93
+ event.attemptId,
94
+ event.runId,
95
+ event.turnId,
96
+ event.generation,
97
+ event.part.id,
98
+ ]);
99
+
100
+ const draft = drafts.get(key);
101
+
102
+ if (event.part.type === "text-start") {
103
+ if (draft !== undefined || drafts.size >= MAX_DRAFT_PARTS) return fail(event.attemptId);
104
+ drafts.set(key, { start: event, text: "", ended: false });
105
+ } else {
106
+ // After invalidation, never expose an orphaned suffix as a complete prefix.
107
+ if (draft === undefined) return false;
108
+ if (draft.ended) return fail(event.attemptId);
109
+ if (event.part.type === "text-delta") {
110
+ if (length + event.part.delta.length > MAX_DRAFT_LENGTH) return fail(event.attemptId);
111
+ draft.text += event.part.delta;
112
+ length += event.part.delta.length;
113
+ } else {
114
+ draft.ended = true;
115
+ }
116
+ }
117
+ } else {
118
+ for (const [key, draft] of drafts) {
119
+ const start = draft.start;
120
+
121
+ if (
122
+ start.threadId === event.threadId &&
123
+ start.submissionId === event.submissionId &&
124
+ start.attemptId === event.attemptId &&
125
+ (event._tag === "AttemptEnded" ||
126
+ (start.runId === event.runId &&
127
+ start.turnId === event.turnId &&
128
+ start.generation === event.generation))
129
+ ) {
130
+ drafts.delete(key);
131
+ length -= draft.text.length;
132
+ }
133
+ }
134
+ }
135
+
136
+ // No snapshot copying, encoding, I/O or awaited work on the producer's path.
137
+ for (const observer of observers) {
138
+ if (observer.threadId !== event.threadId) continue;
139
+ if (!Queue.offerUnsafe(observer.queue, event)) observer.snapshot = true;
140
+ }
141
+
142
+ return true;
143
+ };
144
+
145
+ const open = Effect.fnUntraced(function* (threadId: ThreadId) {
146
+ if (unavailable !== undefined) return yield* unavailable.error;
147
+ if (threadId.length > MAX_ID_LENGTH || observers.size >= MAX_OBSERVERS)
148
+ return yield* HostProtocolError.make({
149
+ message: "Provisional text observation capacity exceeded",
150
+ });
151
+ const scope = yield* Scope.fork(owner);
152
+ const queue = yield* Queue.dropping<Event | undefined, HostProtocolError>(BUFFER_SIZE);
153
+
154
+ const observer: Observer = {
155
+ threadId,
156
+ queue,
157
+ snapshot: true,
158
+ streamId: "",
159
+ sequence: 0,
160
+ };
161
+
162
+ const close = Scope.close(scope, Exit.void);
163
+
164
+ yield* Scope.addFinalizer(
165
+ scope,
166
+ Effect.sync(() => {
167
+ observer.snapshot = true;
168
+ observers.delete(observer);
169
+ Queue.shutdownUnsafe(queue);
170
+ }),
171
+ );
172
+ observers.add(observer);
173
+ Queue.offerUnsafe(queue, undefined);
174
+
175
+ const frame = (event: Event): Frame => ({
176
+ _tag: "Event",
177
+ schemaVersion: 1,
178
+ streamId: observer.streamId,
179
+ sequence: ++observer.sequence,
180
+ event,
181
+ });
182
+
183
+ const snapshot = function* (): Generator<Frame> {
184
+ // Capture immutable strings once, then encode bounded slices on consumer pulls.
185
+ // Queue drain and capture are synchronous so no delta can straddle the snapshot.
186
+ while (Queue.sizeUnsafe(queue) > 0) Queue.takeUnsafe(queue);
187
+
188
+ const current = Array.from(drafts.values())
189
+ .filter((draft) => draft.start.threadId === threadId)
190
+ .map((draft) => ({ ...draft }));
191
+
192
+ observer.snapshot = false;
193
+ observer.streamId = crypto.randomUUID();
194
+ observer.sequence = 0;
195
+ yield {
196
+ _tag: "Reset",
197
+ schemaVersion: 1,
198
+ streamId: observer.streamId,
199
+ threadId,
200
+ sequence: 0,
201
+ };
202
+ for (const draft of current) {
203
+ yield frame(draft.start);
204
+ for (let offset = 0; offset < draft.text.length; offset += MAX_DELTA_LENGTH) {
205
+ yield frame({
206
+ ...draft.start,
207
+ part: Response.makePart("text-delta", {
208
+ id: draft.start.part.id,
209
+ delta: draft.text.slice(offset, offset + MAX_DELTA_LENGTH),
210
+ }),
211
+ });
212
+ }
213
+ if (draft.ended)
214
+ yield frame({
215
+ ...draft.start,
216
+ part: Response.makePart("text-end", { id: draft.start.part.id }),
217
+ });
218
+ }
219
+ };
220
+
221
+ const toBytes = Effect.fnUntraced(function* (value: Frame) {
222
+ const json = yield* encode(value).pipe(
223
+ Effect.mapError(() =>
224
+ HostProtocolError.make({ message: "Provisional text frame is invalid" }),
225
+ ),
226
+ );
227
+
228
+ const bytes = encoder.encode(json + "\n");
229
+
230
+ if (bytes.byteLength > MAX_FRAME_BYTES)
231
+ return yield* HostProtocolError.make({
232
+ message: "Provisional text frame exceeds its wire bound",
233
+ });
234
+
235
+ return bytes;
236
+ });
237
+
238
+ const frames = Stream.fromEffectRepeat(Queue.take(queue)).pipe(
239
+ Stream.flatMap((event) =>
240
+ observer.snapshot || event === undefined
241
+ ? Stream.fromIterable(snapshot(), { chunkSize: 1 }).pipe(
242
+ Stream.takeWhile(() => !observer.snapshot),
243
+ )
244
+ : Stream.succeed(frame(event)),
245
+ ),
246
+ Stream.mapEffect(toBytes),
247
+ Stream.ensuring(close),
248
+ );
249
+
250
+ return yield* Stream.toReadableStreamEffect(frames);
251
+ });
252
+
253
+ return Context.make(LiveTextHub, { open }).pipe(Context.add(Publisher, { offerUnsafe }));
254
+ }),
255
+ );
256
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Puppeteer sends a page function's source to the browser. Bundlers that keep function names,
3
+ * including Wrangler by default, insert `__name(...)` calls into that source, and the page has no
4
+ * such helper. The returned function's source declares a local no-op `__name` first.
5
+ */
6
+ export const pageFunction = <A extends ReadonlyArray<unknown>, R>(
7
+ fn: (...args: A) => R,
8
+ ): ((...args: A) => R) =>
9
+ Object.assign((...args: A) => fn(...args), {
10
+ toString: () =>
11
+ `(...args) => { const __name = (target) => target; return (${Function.prototype.toString.call(fn)})(...args); }`,
12
+ });
@@ -78,7 +78,7 @@ export const cloudflarePreparedInputAdmissionLayer: Layer.Layer<
78
78
  ScheduleStorageError.make({ operation: "prepared admission", reason: "corrupt" }),
79
79
  ThreadNotMaterialized: () => ScheduledInputRetryable.make({ reason: "storage" }),
80
80
  AppendConflict: () => ScheduledInputRetryable.make({ reason: "ambiguous" }),
81
- FenceRejected: () => ScheduledInputRetryable.make({ reason: "ambiguous" }),
81
+ OwnershipLost: () => ScheduledInputRetryable.make({ reason: "ambiguous" }),
82
82
  DurableRuntimeFailpointError: () =>
83
83
  ScheduledInputRetryable.make({ reason: "ambiguous" }),
84
84
  }),
@@ -48,39 +48,34 @@ export class ProgressWaitRegistry extends Context.Service<
48
48
  return next;
49
49
  });
50
50
 
51
- const subscribe = Effect.fn("ProgressWaitRegistry.subscribe")(
52
- (waiterId: string): Effect.Effect<Effect.Effect<void>, never, Scope.Scope> =>
53
- Effect.gen(function* () {
54
- const deferred = yield* Deferred.make<void>();
51
+ const subscribe = Effect.fnUntraced(function* (
52
+ waiterId: string,
53
+ ): Effect.fn.Return<Effect.Effect<void>, never, Scope.Scope> {
54
+ const deferred = yield* Deferred.make<void>();
55
55
 
56
- yield* Effect.addFinalizer(() => remove(waiterId, deferred));
56
+ yield* Effect.addFinalizer(() => remove(waiterId, deferred));
57
57
 
58
- const cancelled = yield* Ref.modify(registrations, (current) => {
59
- const existing = current.get(waiterId);
60
- const next = new Map(current);
58
+ const cancelled = yield* Ref.modify(registrations, (current) => {
59
+ const existing = current.get(waiterId);
60
+ const next = new Map(current);
61
61
 
62
- if (existing === "cancelled") {
63
- return [true, current] as const;
64
- }
65
- const active = new Set(existing ?? []);
62
+ if (existing === "cancelled") {
63
+ return [true, current] as const;
64
+ }
65
+ const active = new Set(existing ?? []);
66
66
 
67
- active.add(deferred);
68
- next.set(waiterId, active);
67
+ active.add(deferred);
68
+ next.set(waiterId, active);
69
69
 
70
- return [false, next] as const;
71
- });
70
+ return [false, next] as const;
71
+ });
72
72
 
73
- return { cancelled, deferred };
74
- }).pipe(
75
- Effect.map(({ cancelled, deferred }) =>
76
- cancelled ? Effect.void : Deferred.await(deferred),
77
- ),
78
- ),
79
- );
73
+ return cancelled ? Effect.void : Deferred.await(deferred);
74
+ });
80
75
 
81
76
  // Updating the registry and completing captured signals form one nonblocking operation;
82
77
  // interruption between them must not strand attempts removed from the active registry.
83
- const cancel = Effect.fn("ProgressWaitRegistry.cancel")(function* (waiterId: string) {
78
+ const cancel = Effect.fnUntraced(function* (waiterId: string) {
84
79
  const waiters = yield* Ref.modify(
85
80
  registrations,
86
81
  (current): readonly [ReadonlyArray<Deferred.Deferred<void>>, Registrations] => {
@@ -33,7 +33,7 @@ export const threadPortTransportLayer: Layer.Layer<
33
33
  const { rpcTracing } = namespace;
34
34
 
35
35
  return ThreadPortTransport.of({
36
- call: Effect.fn(
36
+ call: Effect.fnUntraced(
37
37
  function* (threadId: ThreadId, request: unknown) {
38
38
  const traceArgs =
39
39
  rpcTracing === undefined ? [] : yield* RpcTracing.withRpcTraceContext([]);