@effect-agent/platform-cloudflare 0.1.0-beta.23 → 0.1.0-beta.25
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/index.d.mts +36 -32
- package/dist/index.mjs +234 -171
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/alarm.ts +2 -1
- package/src/bindings.ts +31 -30
- package/src/boundary.ts +51 -0
- package/src/client.ts +86 -187
- package/src/code-mode-executor.ts +128 -62
- package/src/conversation-object.ts +46 -20
- package/src/layers.ts +17 -33
- package/src/wake-scheduler.ts +2 -1
|
@@ -18,8 +18,11 @@ import {
|
|
|
18
18
|
type CodeExecutorExecute,
|
|
19
19
|
type CodeExecutionRequest,
|
|
20
20
|
} from "@effect-agent/sandbox";
|
|
21
|
+
import { BrowserCrypto } from "@effect/platform-browser";
|
|
21
22
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
22
|
-
import { Duration, Effect, Exit, Fiber, Layer, Option, Queue, Schema } from "effect";
|
|
23
|
+
import { Clock, Crypto, Duration, Effect, Exit, Fiber, Layer, Option, Queue, Schema } from "effect";
|
|
24
|
+
|
|
25
|
+
import { safeCauseDiagnostic, safeCauseMessage } from "./boundary.ts";
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
28
|
* The Cloudflare Dynamic Worker `CodeExecutor` adapter (C4 of ADR-0017;
|
|
@@ -43,6 +46,10 @@ export interface CodeModeHostStub {
|
|
|
43
46
|
readonly call: (passId: string, hostCall: unknown) => Promise<unknown>;
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
interface CodeModeHarnessEntrypoint extends Rpc.WorkerEntrypointBranded {
|
|
50
|
+
readonly run: () => Promise<unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
interface RegisteredPass {
|
|
47
54
|
readonly dispatch: (hostCall: unknown) => Promise<unknown>;
|
|
48
55
|
}
|
|
@@ -63,7 +70,10 @@ const passRegistry = new Map<string, RegisteredPass>();
|
|
|
63
70
|
*/
|
|
64
71
|
export class CodeModeHostEntrypoint extends WorkerEntrypoint {
|
|
65
72
|
async call(passId: unknown, hostCall: unknown): Promise<unknown> {
|
|
66
|
-
|
|
73
|
+
if (typeof passId !== "string") {
|
|
74
|
+
throw new TypeError("Code Mode pass identities must be strings");
|
|
75
|
+
}
|
|
76
|
+
const pass = passRegistry.get(passId);
|
|
67
77
|
if (pass === undefined) {
|
|
68
78
|
throw new Error("Unknown Code Mode pass");
|
|
69
79
|
}
|
|
@@ -104,7 +114,7 @@ const safeJson = (value) => {
|
|
|
104
114
|
|
|
105
115
|
export default class CodeModeHarness extends WorkerEntrypoint {
|
|
106
116
|
async run() {
|
|
107
|
-
const config =
|
|
117
|
+
const config = this.env.CODE_MODE_PASS;
|
|
108
118
|
const host = this.env.CODE_MODE_HOST;
|
|
109
119
|
const limits = config.limits;
|
|
110
120
|
const logs = [];
|
|
@@ -227,50 +237,41 @@ const BoundedLogs = Schema.Array(Schema.String.check(Schema.isMaxLength(16 * 102
|
|
|
227
237
|
Schema.isMaxLength(4_096),
|
|
228
238
|
);
|
|
229
239
|
|
|
230
|
-
const HarnessCompleted = Schema.
|
|
231
|
-
_tag: Schema.Literal("completed"),
|
|
240
|
+
const HarnessCompleted = Schema.TaggedStruct("completed", {
|
|
232
241
|
value: Schema.Json,
|
|
233
242
|
logs: BoundedLogs,
|
|
234
243
|
hostCalls: Schema.Natural,
|
|
235
244
|
logBytes: Schema.Natural,
|
|
236
245
|
resultBytes: Schema.Natural,
|
|
237
246
|
});
|
|
238
|
-
const HarnessSourceInvalid = Schema.
|
|
239
|
-
_tag: Schema.Literal("source-invalid"),
|
|
247
|
+
const HarnessSourceInvalid = Schema.TaggedStruct("source-invalid", {
|
|
240
248
|
message: Schema.String,
|
|
241
249
|
});
|
|
242
|
-
const HarnessNotAFunction = Schema.
|
|
243
|
-
_tag: Schema.Literal("source-not-a-function"),
|
|
250
|
+
const HarnessNotAFunction = Schema.TaggedStruct("source-not-a-function", {
|
|
244
251
|
actual: Schema.String,
|
|
245
252
|
});
|
|
246
|
-
const HarnessProgramFailed = Schema.
|
|
247
|
-
_tag: Schema.Literal("program-failed"),
|
|
253
|
+
const HarnessProgramFailed = Schema.TaggedStruct("program-failed", {
|
|
248
254
|
reason: Schema.Literals(["threw", "rejected", "non-json-result"]),
|
|
249
255
|
thrown: Schema.Json,
|
|
250
256
|
message: Schema.String,
|
|
251
257
|
logs: BoundedLogs,
|
|
252
258
|
});
|
|
253
|
-
const HarnessLogLimit = Schema.
|
|
254
|
-
_tag: Schema.Literal("log-limit"),
|
|
259
|
+
const HarnessLogLimit = Schema.TaggedStruct("log-limit", {
|
|
255
260
|
observed: Schema.Natural,
|
|
256
261
|
logs: BoundedLogs,
|
|
257
262
|
});
|
|
258
|
-
const HarnessArgumentLimit = Schema.
|
|
259
|
-
_tag: Schema.Literal("argument-limit"),
|
|
263
|
+
const HarnessArgumentLimit = Schema.TaggedStruct("argument-limit", {
|
|
260
264
|
observed: Schema.Natural,
|
|
261
265
|
logs: BoundedLogs,
|
|
262
266
|
});
|
|
263
|
-
const HarnessResultLimit = Schema.
|
|
264
|
-
_tag: Schema.Literal("result-limit"),
|
|
267
|
+
const HarnessResultLimit = Schema.TaggedStruct("result-limit", {
|
|
265
268
|
observed: Schema.Natural,
|
|
266
269
|
logs: BoundedLogs,
|
|
267
270
|
});
|
|
268
|
-
const HarnessHostCallLimit = Schema.
|
|
269
|
-
_tag: Schema.Literal("host-call-limit"),
|
|
271
|
+
const HarnessHostCallLimit = Schema.TaggedStruct("host-call-limit", {
|
|
270
272
|
logs: BoundedLogs,
|
|
271
273
|
});
|
|
272
|
-
const HarnessProtocol = Schema.
|
|
273
|
-
_tag: Schema.Literal("protocol"),
|
|
274
|
+
const HarnessProtocol = Schema.TaggedStruct("protocol", {
|
|
274
275
|
message: Schema.String,
|
|
275
276
|
});
|
|
276
277
|
const HarnessOutcome = Schema.Union([
|
|
@@ -285,6 +286,26 @@ const HarnessOutcome = Schema.Union([
|
|
|
285
286
|
HarnessProtocol,
|
|
286
287
|
]);
|
|
287
288
|
|
|
289
|
+
const HarnessPassConfig = Schema.Struct({
|
|
290
|
+
passId: Schema.NonEmptyString,
|
|
291
|
+
namespaces: Schema.Array(
|
|
292
|
+
Schema.Struct({
|
|
293
|
+
name: Schema.NonEmptyString,
|
|
294
|
+
methods: Schema.Array(Schema.NonEmptyString).check(Schema.isMaxLength(64)),
|
|
295
|
+
}),
|
|
296
|
+
).check(Schema.isMaxLength(32)),
|
|
297
|
+
limits: Schema.Struct({
|
|
298
|
+
maxLogBytes: Schema.Natural,
|
|
299
|
+
maxResultBytes: Schema.Natural,
|
|
300
|
+
maxHostCalls: Schema.Natural,
|
|
301
|
+
maxHostCallArgumentBytes: Schema.Natural,
|
|
302
|
+
}),
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const encodeHarnessPassConfig = Schema.encodeSync(HarnessPassConfig);
|
|
306
|
+
const encodeJsonPayload = Schema.encodeSync(Schema.fromJsonString(Schema.Json));
|
|
307
|
+
const decodeJsonPayload = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json));
|
|
308
|
+
|
|
288
309
|
const decodeHarnessOutcome = (value: unknown) => {
|
|
289
310
|
try {
|
|
290
311
|
return Schema.decodeUnknownOption(HarnessOutcome)(value);
|
|
@@ -309,6 +330,27 @@ const decodeHostCallResult = (value: unknown) => {
|
|
|
309
330
|
}
|
|
310
331
|
};
|
|
311
332
|
|
|
333
|
+
/** Dispose a Cloudflare RPC handle when the runtime supplies its untyped disposal hook. @internal */
|
|
334
|
+
export const disposeRpcHandle = (handle: unknown): Effect.Effect<void> =>
|
|
335
|
+
Effect.try({
|
|
336
|
+
try: () => {
|
|
337
|
+
if ((typeof handle !== "object" && typeof handle !== "function") || handle === null) return;
|
|
338
|
+
if (!(Symbol.dispose in handle)) return;
|
|
339
|
+
const dispose = Reflect.get(handle, Symbol.dispose);
|
|
340
|
+
if (typeof dispose === "function") {
|
|
341
|
+
Reflect.apply(dispose, handle, []);
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
catch: (cause) =>
|
|
345
|
+
safeCauseDiagnostic(cause, "The Cloudflare RPC disposal hook failed without a diagnostic"),
|
|
346
|
+
}).pipe(
|
|
347
|
+
Effect.catch((diagnostic) =>
|
|
348
|
+
Effect.logWarning(`Cloudflare RPC handle disposal failed: ${diagnostic}`).pipe(
|
|
349
|
+
Effect.ignoreCause,
|
|
350
|
+
),
|
|
351
|
+
),
|
|
352
|
+
);
|
|
353
|
+
|
|
312
354
|
/**
|
|
313
355
|
* Project a host outcome to the plain JSON envelope the harness reads. A
|
|
314
356
|
* `CodeExecutionHost` may return either real `CodeHostCallResult` instances
|
|
@@ -326,8 +368,7 @@ const encodeHostResultPayload = (
|
|
|
326
368
|
): EncodedHostResultPayload | undefined => {
|
|
327
369
|
try {
|
|
328
370
|
const payload = outcome._tag === "CodeHostCallSuccess" ? outcome.value : outcome.error;
|
|
329
|
-
const encodedPayload =
|
|
330
|
-
if (encodedPayload === undefined) return undefined;
|
|
371
|
+
const encodedPayload = encodeJsonPayload(payload);
|
|
331
372
|
return {
|
|
332
373
|
encodedPayload,
|
|
333
374
|
resultBytes: utf8ByteLength(encodedPayload),
|
|
@@ -378,9 +419,11 @@ export interface DynamicWorkerCodeExecutorOptions {
|
|
|
378
419
|
readonly compatibilityDate?: string | undefined;
|
|
379
420
|
}
|
|
380
421
|
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
422
|
+
const makeExecute = (
|
|
423
|
+
options: DynamicWorkerCodeExecutorOptions,
|
|
424
|
+
crypto: Crypto.Crypto,
|
|
425
|
+
clock: Clock.Clock,
|
|
426
|
+
): CodeExecutorExecute =>
|
|
384
427
|
Effect.fn("DynamicWorkerCodeExecutor.execute")(function* (request: CodeExecutionRequest) {
|
|
385
428
|
if (request.network._tag !== "NetworkDisabled") {
|
|
386
429
|
return yield* CodeExecutorUnsupportedError.make({
|
|
@@ -409,18 +452,32 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
409
452
|
}
|
|
410
453
|
|
|
411
454
|
const host = yield* CodeExecutionHost;
|
|
412
|
-
passCounterState.next += 1;
|
|
413
455
|
// The pass id is the only credential a loaded program presents to reach
|
|
414
456
|
// its host authority, so it must be unguessable: a program cannot forge
|
|
415
457
|
// another pass's id even if two passes were ever concurrent (the broker
|
|
416
|
-
// keeps them sequential, but the id must not rely on that).
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
458
|
+
// keeps them sequential, but the id must not rely on that).
|
|
459
|
+
const passId = yield* crypto.randomUUIDv4.pipe(
|
|
460
|
+
Effect.mapError((cause) =>
|
|
461
|
+
CodeExecutorStartError.make({
|
|
462
|
+
implementation: dynamicWorkerImplementation,
|
|
463
|
+
message: `Could not mint the Code Mode pass identity: ${safeCauseMessage(
|
|
464
|
+
cause,
|
|
465
|
+
"the crypto service failed without a diagnostic",
|
|
466
|
+
)}`.slice(0, 8_000),
|
|
467
|
+
cause,
|
|
468
|
+
}),
|
|
469
|
+
),
|
|
470
|
+
Effect.map((uuid) => `code-mode-pass-${uuid}`),
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
// This synchronous clock access is confined to callbacks that must compute a timeout
|
|
474
|
+
// immediately. The Clock service remains the authority, so tests and hosts can replace it.
|
|
475
|
+
const startedAt = clock.monotonicTimeNanosUnsafe();
|
|
476
|
+
const passDeadline = startedAt + Duration.toNanosUnsafe(request.limits.maxWallTime);
|
|
477
|
+
const remainingPassWallTime = (): Duration.Duration => {
|
|
478
|
+
const now = clock.monotonicTimeNanosUnsafe();
|
|
479
|
+
return Duration.nanos(passDeadline > now ? passDeadline - now : 0n);
|
|
480
|
+
};
|
|
424
481
|
let issuedHostCalls = 0;
|
|
425
482
|
let passOpen = true;
|
|
426
483
|
const queuedHostCalls: Array<QueuedHostCall> = [];
|
|
@@ -461,11 +518,19 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
461
518
|
failPass(error);
|
|
462
519
|
return yield* error;
|
|
463
520
|
}
|
|
464
|
-
const normalizedPayload
|
|
521
|
+
const normalizedPayload = decodeJsonPayload(encoded.encodedPayload);
|
|
522
|
+
if (Option.isNone(normalizedPayload)) {
|
|
523
|
+
const error = CodeExecutionProtocolError.make({
|
|
524
|
+
implementation: dynamicWorkerImplementation,
|
|
525
|
+
message: "The execution host returned a result that could not cross the JSON boundary",
|
|
526
|
+
});
|
|
527
|
+
failPass(error);
|
|
528
|
+
return yield* error;
|
|
529
|
+
}
|
|
465
530
|
queued.resolve(
|
|
466
531
|
decoded.value._tag === "CodeHostCallSuccess"
|
|
467
|
-
? { _tag: "CodeHostCallSuccess", value: normalizedPayload }
|
|
468
|
-
: { _tag: "CodeHostCallFailure", error: normalizedPayload },
|
|
532
|
+
? { _tag: "CodeHostCallSuccess", value: normalizedPayload.value }
|
|
533
|
+
: { _tag: "CodeHostCallFailure", error: normalizedPayload.value },
|
|
469
534
|
);
|
|
470
535
|
});
|
|
471
536
|
|
|
@@ -554,7 +619,7 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
554
619
|
// worker carries the `experimental` compatibility flag, which deployed
|
|
555
620
|
// consumers cannot set — the option would reject every pass in
|
|
556
621
|
// production. The harness needs no experimental runtime features.
|
|
557
|
-
const workerCode = {
|
|
622
|
+
const workerCode: WorkerLoaderWorkerCode = {
|
|
558
623
|
compatibilityDate: options.compatibilityDate ?? "2025-05-01",
|
|
559
624
|
mainModule: "harness.js",
|
|
560
625
|
modules: {
|
|
@@ -563,7 +628,7 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
563
628
|
},
|
|
564
629
|
env: {
|
|
565
630
|
CODE_MODE_HOST: options.hostStub,
|
|
566
|
-
CODE_MODE_PASS:
|
|
631
|
+
CODE_MODE_PASS: encodeHarnessPassConfig({
|
|
567
632
|
passId,
|
|
568
633
|
namespaces: request.namespaces.map((namespace) => ({
|
|
569
634
|
name: namespace.name,
|
|
@@ -590,9 +655,9 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
590
655
|
|
|
591
656
|
const worker = yield* Effect.acquireRelease(
|
|
592
657
|
Effect.try({
|
|
593
|
-
try: () => options.loader.load(workerCode
|
|
658
|
+
try: () => options.loader.load(workerCode),
|
|
594
659
|
catch: (cause) => {
|
|
595
|
-
const text = cause
|
|
660
|
+
const text = safeCauseMessage(cause, "The Worker Loader failed without a diagnostic");
|
|
596
661
|
// Blame the program's source ONLY on a genuine compile diagnostic;
|
|
597
662
|
// any other load rejection is an infrastructure start failure, not
|
|
598
663
|
// the model's fault (see classifyWorkerFailure for the same split).
|
|
@@ -610,19 +675,19 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
610
675
|
});
|
|
611
676
|
},
|
|
612
677
|
}),
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
678
|
+
disposeRpcHandle,
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
const entrypoint = yield* Effect.acquireRelease(
|
|
682
|
+
Effect.try({
|
|
683
|
+
try: () => worker.getEntrypoint<CodeModeHarnessEntrypoint>(),
|
|
684
|
+
catch: (cause) => classifyWorkerFailure(cause, request.limits.maxWallTime),
|
|
685
|
+
}),
|
|
686
|
+
disposeRpcHandle,
|
|
617
687
|
);
|
|
618
688
|
|
|
619
689
|
const rpc = Effect.tryPromise({
|
|
620
|
-
try:
|
|
621
|
-
const entrypoint = worker.getEntrypoint() as unknown as {
|
|
622
|
-
run(): Promise<unknown>;
|
|
623
|
-
};
|
|
624
|
-
return await entrypoint.run();
|
|
625
|
-
},
|
|
690
|
+
try: () => entrypoint.run(),
|
|
626
691
|
catch: (cause) => classifyWorkerFailure(cause, request.limits.maxWallTime),
|
|
627
692
|
});
|
|
628
693
|
|
|
@@ -650,7 +715,7 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
650
715
|
return yield* Effect.failCause(exit.cause);
|
|
651
716
|
}
|
|
652
717
|
const raw = exit.value;
|
|
653
|
-
const finishedAt =
|
|
718
|
+
const finishedAt = clock.monotonicTimeNanosUnsafe();
|
|
654
719
|
|
|
655
720
|
const outcome = decodeHarnessOutcome(raw);
|
|
656
721
|
if (Option.isNone(outcome)) {
|
|
@@ -666,7 +731,7 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
666
731
|
value: outcome.value.value,
|
|
667
732
|
logs: outcome.value.logs,
|
|
668
733
|
resourceUse: CodeExecutionResourceUse.make({
|
|
669
|
-
wallTime: Duration.
|
|
734
|
+
wallTime: Duration.nanos(finishedAt > startedAt ? finishedAt - startedAt : 0n),
|
|
670
735
|
hostCalls: outcome.value.hostCalls,
|
|
671
736
|
logBytes: outcome.value.logBytes,
|
|
672
737
|
resultBytes: outcome.value.resultBytes,
|
|
@@ -752,13 +817,7 @@ const classifyWorkerFailure = (
|
|
|
752
817
|
| CodeExecutorTerminatedError
|
|
753
818
|
| CodeExecutorStartError
|
|
754
819
|
| CodeSourceError => {
|
|
755
|
-
const text = (
|
|
756
|
-
try {
|
|
757
|
-
return cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause);
|
|
758
|
-
} catch {
|
|
759
|
-
return "[unserializable worker failure]";
|
|
760
|
-
}
|
|
761
|
-
})();
|
|
820
|
+
const text = safeCauseDiagnostic(cause, "[unserializable worker failure]");
|
|
762
821
|
// `WorkerLoader.load()` is lazy, so a module-compile error in the generated
|
|
763
822
|
// program surfaces here at first use. Blame the program's source ONLY on a
|
|
764
823
|
// genuine compile diagnostic (a `SyntaxError` or an explicit compile
|
|
@@ -798,4 +857,11 @@ const classifyWorkerFailure = (
|
|
|
798
857
|
export const dynamicWorkerCodeExecutorLayer = (
|
|
799
858
|
options: DynamicWorkerCodeExecutorOptions,
|
|
800
859
|
): Layer.Layer<CodeExecutor> =>
|
|
801
|
-
Layer.
|
|
860
|
+
Layer.effect(
|
|
861
|
+
CodeExecutor,
|
|
862
|
+
Effect.gen(function* () {
|
|
863
|
+
const crypto = yield* Crypto.Crypto;
|
|
864
|
+
const clock = yield* Clock.Clock;
|
|
865
|
+
return CodeExecutor.of({ execute: makeExecute(options, crypto, clock) });
|
|
866
|
+
}),
|
|
867
|
+
).pipe(Layer.provide(BrowserCrypto.layer));
|
|
@@ -30,6 +30,11 @@ import {
|
|
|
30
30
|
WakeScheduler,
|
|
31
31
|
type DurableSubmitAgent,
|
|
32
32
|
} from "@effect-agent/session";
|
|
33
|
+
import {
|
|
34
|
+
decodePortRequest,
|
|
35
|
+
encodePortResponse,
|
|
36
|
+
type PortRequest,
|
|
37
|
+
} from "@effect-agent/storage-cloudflare";
|
|
33
38
|
import type { DurableObject as CloudflareDurableObject } from "cloudflare:workers";
|
|
34
39
|
import { Effect, Layer, Option, Schema, Stream } from "effect";
|
|
35
40
|
import {
|
|
@@ -120,22 +125,26 @@ type ConversationObjectInitializationError =
|
|
|
120
125
|
| CloudflareBindingError
|
|
121
126
|
| MaintenancePassFailure;
|
|
122
127
|
|
|
123
|
-
/**
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
128
|
+
/** Classify only a decoded port request so new protocol members cannot bypass pre-arming. */
|
|
129
|
+
const isMutatingPortRequest = (request: PortRequest): boolean => {
|
|
130
|
+
switch (request._tag) {
|
|
131
|
+
case "LedgerAdmit":
|
|
132
|
+
case "LedgerMarkReady":
|
|
133
|
+
case "LedgerRequestAbort":
|
|
134
|
+
case "LedgerRecordChildSettled":
|
|
135
|
+
case "StoreMaterialize":
|
|
136
|
+
case "StoreAppend":
|
|
137
|
+
return true;
|
|
138
|
+
case "LedgerLookup":
|
|
139
|
+
case "LedgerResolveAdmission":
|
|
140
|
+
case "StoreReadPage":
|
|
141
|
+
case "StoreInspectTail":
|
|
142
|
+
case "StoreExport":
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
request satisfies never;
|
|
146
|
+
return false;
|
|
147
|
+
};
|
|
139
148
|
|
|
140
149
|
/** The literal encoded `PortFailed(PortProtocolError)` fallback (same shape as WP2's). */
|
|
141
150
|
const encodedPortProtocolFailure = (message: string): unknown => ({
|
|
@@ -171,7 +180,7 @@ const encodeResponse = (response: HostResponse): Effect.Effect<unknown> =>
|
|
|
171
180
|
),
|
|
172
181
|
);
|
|
173
182
|
|
|
174
|
-
const utf8Bytes = (value:
|
|
183
|
+
const utf8Bytes = (value: PersistedJson): number =>
|
|
175
184
|
new TextEncoder().encode(JSON.stringify(value)).length;
|
|
176
185
|
|
|
177
186
|
/**
|
|
@@ -593,9 +602,20 @@ const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpo
|
|
|
593
602
|
const ports = yield* ConversationObjectPorts;
|
|
594
603
|
const maintenance = yield* ConversationMaintenance;
|
|
595
604
|
const alarm = yield* DurableAlarmService;
|
|
596
|
-
const
|
|
605
|
+
const decoded = yield* decodePortRequest(encoded).pipe(
|
|
606
|
+
Effect.map((request) => ({ _tag: "success" as const, request })),
|
|
607
|
+
Effect.catch((error) => Effect.succeed({ _tag: "failure" as const, message: error.message })),
|
|
608
|
+
);
|
|
609
|
+
if (decoded._tag === "failure") {
|
|
610
|
+
return encodedPortProtocolFailure(
|
|
611
|
+
`The port request could not be decoded: ${decoded.message}`,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
const mutating = isMutatingPortRequest(decoded.request);
|
|
597
615
|
const handled = yield* (
|
|
598
|
-
mutating
|
|
616
|
+
mutating
|
|
617
|
+
? maintenance.withMutation(ports.handle(decoded.request))
|
|
618
|
+
: ports.handle(decoded.request)
|
|
599
619
|
).pipe(Effect.exit);
|
|
600
620
|
if (handled._tag === "Failure") {
|
|
601
621
|
// Without the committed generation/alarm the invariant cannot be promised; refuse before
|
|
@@ -604,7 +624,13 @@ const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpo
|
|
|
604
624
|
"The owner Object could not arm its maintenance alarm before the mutation.",
|
|
605
625
|
);
|
|
606
626
|
}
|
|
607
|
-
const response = handled.value
|
|
627
|
+
const response = yield* encodePortResponse(handled.value).pipe(
|
|
628
|
+
Effect.catch((error) =>
|
|
629
|
+
Effect.succeed(
|
|
630
|
+
encodedPortProtocolFailure(`The port response could not be encoded: ${error.message}`),
|
|
631
|
+
),
|
|
632
|
+
),
|
|
633
|
+
);
|
|
608
634
|
if (mutating) {
|
|
609
635
|
// Prompt processing hint; the pre-armed alarm already guarantees convergence.
|
|
610
636
|
yield* alarm.scheduleNow.pipe(
|
package/src/layers.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from "@effect-agent/session";
|
|
19
19
|
import {
|
|
20
20
|
conversationStoreLayer,
|
|
21
|
-
|
|
21
|
+
executePortRequest,
|
|
22
22
|
routedConversationStoreLayer,
|
|
23
23
|
routedSubmissionLedgerLayer,
|
|
24
24
|
storageConfigLayer,
|
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
type DoStorageFailpointHandler,
|
|
28
28
|
type DoStorageInitializationError,
|
|
29
29
|
type DoStorageOptions,
|
|
30
|
+
type PortRequest,
|
|
31
|
+
type PortResponse,
|
|
30
32
|
} from "@effect-agent/storage-cloudflare";
|
|
31
33
|
import { BrowserCrypto } from "@effect/platform-browser";
|
|
32
34
|
import { SqliteClient } from "@effect/sql-sqlite-do";
|
|
@@ -114,10 +116,9 @@ export interface CloudflareDurableRuntimeOptions {
|
|
|
114
116
|
readonly toolReconciler?: Layer.Layer<ToolReconciler> | undefined;
|
|
115
117
|
/**
|
|
116
118
|
* Registered worker Bindings resolved at durable claim time (S2, spec/subagents.md §11):
|
|
117
|
-
* build each with `DurableWorkerBinding.make(binding, digests)`.
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* to the empty registration (every resolved claim fails closed).
|
|
119
|
+
* build each with `DurableWorkerBinding.make(binding, digests)`. The callback receives the live
|
|
120
|
+
* Object context and derived identities and is evaluated once per incarnation during Layer
|
|
121
|
+
* construction. Defaults to the empty registration (every resolved claim fails closed).
|
|
121
122
|
*/
|
|
122
123
|
readonly bindings?: CloudflareBindingSource | undefined;
|
|
123
124
|
/**
|
|
@@ -141,18 +142,10 @@ export interface CloudflareRuntimeSourceContext {
|
|
|
141
142
|
/** Per-incarnation host values available while registered worker Bindings are captured. */
|
|
142
143
|
export interface CloudflareBindingSourceContext extends CloudflareRuntimeSourceContext {}
|
|
143
144
|
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
export type CloudflareBindingSource =
|
|
149
|
-
| ReadonlyArray<ResolvedBinding>
|
|
150
|
-
| Effect.Effect<ReadonlyArray<ResolvedBinding>, never, never>
|
|
151
|
-
| ((
|
|
152
|
-
context: CloudflareBindingSourceContext,
|
|
153
|
-
) =>
|
|
154
|
-
| ReadonlyArray<ResolvedBinding>
|
|
155
|
-
| Effect.Effect<ReadonlyArray<ResolvedBinding>, never, never>);
|
|
145
|
+
/** Captures registered worker Bindings once for each Durable Object incarnation. */
|
|
146
|
+
export type CloudflareBindingSource = (
|
|
147
|
+
context: CloudflareBindingSourceContext,
|
|
148
|
+
) => Effect.Effect<ReadonlyArray<ResolvedBinding>, never, never>;
|
|
156
149
|
|
|
157
150
|
/** A closed Run-context service whose only remaining requirement is platform Crypto. */
|
|
158
151
|
export type CloudflareRunContextLayer = Layer.Layer<RunContextPreparation, never, Crypto.Crypto>;
|
|
@@ -183,15 +176,15 @@ export type CloudflareDurableRuntimeServices =
|
|
|
183
176
|
| ProgressWaitRegistry;
|
|
184
177
|
|
|
185
178
|
/**
|
|
186
|
-
* Owner-side
|
|
187
|
-
*
|
|
188
|
-
* request cannot bounce between Objects — and
|
|
189
|
-
*
|
|
179
|
+
* Owner-side execution port for a `portCall` request the wire endpoint has already decoded.
|
|
180
|
+
* It executes against THIS Object's LOCAL port facets — never the routed decorators, so a
|
|
181
|
+
* request cannot bounce between Objects — and returns the typed response for the endpoint to
|
|
182
|
+
* encode.
|
|
190
183
|
*/
|
|
191
184
|
export class ConversationObjectPorts extends Context.Service<
|
|
192
185
|
ConversationObjectPorts,
|
|
193
186
|
{
|
|
194
|
-
readonly handle: (
|
|
187
|
+
readonly handle: (request: PortRequest) => Effect.Effect<PortResponse>;
|
|
195
188
|
}
|
|
196
189
|
>()("@effect-agent/platform-cloudflare/ConversationObjectPorts") {}
|
|
197
190
|
|
|
@@ -267,16 +260,7 @@ const resolveBindings = (
|
|
|
267
260
|
source: CloudflareDurableRuntimeOptions["bindings"],
|
|
268
261
|
context: CloudflareBindingSourceContext,
|
|
269
262
|
): Effect.Effect<ReadonlyArray<ResolvedBinding>> =>
|
|
270
|
-
source === undefined
|
|
271
|
-
? Effect.succeed([])
|
|
272
|
-
: Effect.isEffect(source)
|
|
273
|
-
? source
|
|
274
|
-
: typeof source === "function"
|
|
275
|
-
? Effect.suspend(() => {
|
|
276
|
-
const bindings = source(context);
|
|
277
|
-
return Effect.isEffect(bindings) ? bindings : Effect.succeed(bindings);
|
|
278
|
-
})
|
|
279
|
-
: Effect.succeed(source);
|
|
263
|
+
source === undefined ? Effect.succeed([]) : Effect.suspend(() => source(context));
|
|
280
264
|
|
|
281
265
|
const resolveRunContext = (
|
|
282
266
|
source: CloudflareRunContextSource,
|
|
@@ -358,7 +342,7 @@ export class CloudflareDurableRuntime {
|
|
|
358
342
|
Effect.gen(function* () {
|
|
359
343
|
const local = yield* Effect.context<SubmissionLedger | ConversationStore>();
|
|
360
344
|
return ConversationObjectPorts.of({
|
|
361
|
-
handle: (
|
|
345
|
+
handle: (request) => executePortRequest(request).pipe(Effect.provide(local)),
|
|
362
346
|
});
|
|
363
347
|
}),
|
|
364
348
|
).pipe(Layer.provide(localPorts));
|
package/src/wake-scheduler.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { Effect, Layer, PubSub, Schema, Stream } from "effect";
|
|
|
4
4
|
|
|
5
5
|
import { DurableAlarmService } from "./alarm.ts";
|
|
6
6
|
import { ConversationObjectIdentity, ConversationObjectNamespace } from "./bindings.ts";
|
|
7
|
+
import { safeCauseMessage } from "./boundary.ts";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Bounded in-memory wake buffer for same-incarnation `awaitSettlement` subscribers. Wake
|
|
@@ -63,7 +64,7 @@ export const cloudflareWakeSchedulerLayer: Layer.Layer<
|
|
|
63
64
|
catch: (cause) =>
|
|
64
65
|
RemoteWakeDropped.make({
|
|
65
66
|
conversationId,
|
|
66
|
-
message: cause
|
|
67
|
+
message: safeCauseMessage(cause, "The remote wake failed without a diagnostic"),
|
|
67
68
|
cause,
|
|
68
69
|
}),
|
|
69
70
|
}).pipe(
|