@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.
- package/dist/{Alarm-B0ODr1NE.mjs → Alarm-DxODgAZZ.mjs} +259 -159
- package/dist/Alarm-DxODgAZZ.mjs.map +1 -0
- package/dist/Alarm.d.mts +27 -36
- package/dist/Alarm.mjs +2 -2
- package/dist/BrowserCredentials-C5IWfi7f.d.mts +91 -0
- package/dist/BrowserCredentials.d.mts +2 -91
- package/dist/BrowserRestCapture.mjs +3 -3
- package/dist/BrowserRestCapture.mjs.map +1 -1
- package/dist/BrowserRestCrawl.mjs +6 -6
- package/dist/BrowserRestCrawl.mjs.map +1 -1
- package/dist/{BrowserSession-RfIzxGCH.mjs → BrowserSession-BfNmyayH.mjs} +5 -5
- package/dist/BrowserSession-BfNmyayH.mjs.map +1 -0
- package/dist/{BrowserSession-B969Xg-2.d.mts → BrowserSession-Bx0V1-6_.d.mts} +6 -3
- package/dist/BrowserSession.d.mts +1 -1
- package/dist/BrowserSession.mjs +2 -2
- package/dist/BrowserUse-AstK0jKX.mjs +1467 -0
- package/dist/BrowserUse-AstK0jKX.mjs.map +1 -0
- package/dist/BrowserUse.d.mts +83 -0
- package/dist/BrowserUse.mjs +2 -0
- package/dist/CloudflareBindings.d.mts +8 -1
- package/dist/CloudflareBindings.mjs +2 -2
- package/dist/CloudflareBindings.mjs.map +1 -1
- package/dist/{CloudflareBrowser-5an6m-Qb.mjs → CloudflareBrowser-Vt8qTdYW.mjs} +11 -13
- package/dist/CloudflareBrowser-Vt8qTdYW.mjs.map +1 -0
- package/dist/CloudflareBrowser.mjs +1 -1
- package/dist/CloudflareCodeMode.mjs +1 -1
- package/dist/CloudflareCodeMode.mjs.map +1 -1
- package/dist/{CloudflareConfig-qDrRLofL.d.mts → CloudflareConfig-C9MvPbX8.d.mts} +9 -11
- package/dist/CloudflareConfig.d.mts +1 -1
- package/dist/CloudflareConfig.mjs +9 -12
- package/dist/CloudflareConfig.mjs.map +1 -1
- package/dist/CloudflareMemory.d.mts +3 -25
- package/dist/CloudflareMemory.mjs +33 -31
- package/dist/CloudflareMemory.mjs.map +1 -1
- package/dist/CloudflareScheduling.mjs +8 -15
- package/dist/CloudflareScheduling.mjs.map +1 -1
- package/dist/CloudflareSubscriptions.mjs +9 -16
- package/dist/CloudflareSubscriptions.mjs.map +1 -1
- package/dist/CloudflareThreadClient-CZm0TvUe.mjs +588 -0
- package/dist/CloudflareThreadClient-CZm0TvUe.mjs.map +1 -0
- package/dist/CloudflareThreadClient-QsVzLGGZ.d.mts +2033 -0
- package/dist/CloudflareThreadClient.d.mts +2 -1796
- package/dist/CloudflareThreadClient.mjs +2 -382
- package/dist/{InteractiveBrowser-BbG3kGA9.mjs → InteractiveBrowser-LdKzQeLR.mjs} +18 -19
- package/dist/InteractiveBrowser-LdKzQeLR.mjs.map +1 -0
- package/dist/InteractiveBrowser.mjs +1 -1
- package/dist/{ThreadObject-BcfXbzSO.d.mts → ThreadObject-7s5XnlH-.d.mts} +103 -79
- package/dist/{ThreadObject-DHjrnlX4.mjs → ThreadObject-C0vrcDrw.mjs} +265 -66
- package/dist/ThreadObject-C0vrcDrw.mjs.map +1 -0
- package/dist/ThreadObject.d.mts +2 -2
- package/dist/ThreadObject.mjs +2 -3
- package/dist/WakeScheduler.d.mts +6 -5
- package/dist/WakeScheduler.mjs +15 -11
- package/dist/WakeScheduler.mjs.map +1 -1
- package/dist/index.d.mts +7 -6
- package/dist/index.mjs +7 -6
- package/dist/page-function-CmUCcyNP.mjs +11 -0
- package/dist/page-function-CmUCcyNP.mjs.map +1 -0
- package/dist/{prepared-admission-DMX5p9dC.mjs → prepared-admission-3UUq0zth.mjs} +3 -3
- package/dist/{prepared-admission-DMX5p9dC.mjs.map → prepared-admission-3UUq0zth.mjs.map} +1 -1
- package/package.json +1 -1
- package/src/Alarm.ts +510 -279
- package/src/BrowserRestCapture.ts +2 -2
- package/src/BrowserRestCrawl.ts +10 -14
- package/src/BrowserSession.ts +10 -2
- package/src/BrowserUse.ts +1541 -0
- package/src/CloudflareBindings.ts +13 -4
- package/src/CloudflareCodeMode.ts +1 -1
- package/src/CloudflareConfig.ts +9 -12
- package/src/CloudflareMemory.ts +21 -28
- package/src/CloudflareScheduling.ts +7 -15
- package/src/CloudflareSubscriptions.ts +11 -20
- package/src/CloudflareThreadClient.ts +289 -9
- package/src/InteractiveBrowser.ts +325 -323
- package/src/ThreadObject.ts +172 -52
- package/src/WakeScheduler.ts +29 -18
- package/src/index.ts +1 -0
- package/src/internal/browser-dom.ts +845 -0
- package/src/internal/browser-file-selection.ts +8 -1
- package/src/internal/browser-jev-dom.ts +344 -0
- package/src/internal/browser-quick-action.ts +7 -8
- package/src/internal/browser-session-lifecycle.ts +2 -5
- package/src/internal/crypto.ts +41 -0
- package/src/internal/due-queue.ts +5 -69
- package/src/internal/layers.ts +100 -17
- package/src/internal/live-text-protocol.ts +51 -0
- package/src/internal/live-text.ts +256 -0
- package/src/internal/page-function.ts +12 -0
- package/src/internal/prepared-admission.ts +1 -1
- package/src/internal/progress-wait.ts +19 -24
- package/src/internal/transport.ts +1 -1
- package/dist/Alarm-B0ODr1NE.mjs.map +0 -1
- package/dist/BrowserSession-RfIzxGCH.mjs.map +0 -1
- package/dist/CloudflareBrowser-5an6m-Qb.mjs.map +0 -1
- package/dist/CloudflareThreadClient.mjs.map +0 -1
- package/dist/InteractiveBrowser-BbG3kGA9.mjs.map +0 -1
- package/dist/ThreadObject-DHjrnlX4.mjs.map +0 -1
package/src/internal/layers.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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, {
|
|
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
|
|
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
|
-
|
|
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.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
56
|
+
yield* Effect.addFinalizer(() => remove(waiterId, deferred));
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
const cancelled = yield* Ref.modify(registrations, (current) => {
|
|
59
|
+
const existing = current.get(waiterId);
|
|
60
|
+
const next = new Map(current);
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
if (existing === "cancelled") {
|
|
63
|
+
return [true, current] as const;
|
|
64
|
+
}
|
|
65
|
+
const active = new Set(existing ?? []);
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
active.add(deferred);
|
|
68
|
+
next.set(waiterId, active);
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
return [false, next] as const;
|
|
71
|
+
});
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
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.
|
|
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.
|
|
36
|
+
call: Effect.fnUntraced(
|
|
37
37
|
function* (threadId: ThreadId, request: unknown) {
|
|
38
38
|
const traceArgs =
|
|
39
39
|
rpcTracing === undefined ? [] : yield* RpcTracing.withRpcTraceContext([]);
|