@effect-agent/platform-cloudflare 0.1.0-beta.12 → 0.1.0-beta.14
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 +10 -8
- package/dist/index.mjs +159 -79
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/code-mode-executor.ts +243 -100
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-agent/platform-cloudflare",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.14",
|
|
4
4
|
"exports": {
|
|
5
5
|
".": {
|
|
6
6
|
"types": "./dist/index.d.mts",
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
}
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@effect-agent/core": "0.1.0-beta.
|
|
12
|
-
"@effect-agent/sandbox": "0.1.0-beta.
|
|
13
|
-
"@effect-agent/session": "0.1.0-beta.
|
|
14
|
-
"@effect-agent/storage-cloudflare": "0.1.0-beta.
|
|
11
|
+
"@effect-agent/core": "0.1.0-beta.14",
|
|
12
|
+
"@effect-agent/sandbox": "0.1.0-beta.14",
|
|
13
|
+
"@effect-agent/session": "0.1.0-beta.14",
|
|
14
|
+
"@effect-agent/storage-cloudflare": "0.1.0-beta.14",
|
|
15
15
|
"@effect/platform-browser": "4.0.0-beta.107",
|
|
16
16
|
"@effect/sql-sqlite-do": "4.0.0-beta.107",
|
|
17
17
|
"effect": "4.0.0-beta.107"
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CodeExecutionHost,
|
|
3
|
-
CodeExecutionRequest,
|
|
4
3
|
CodeExecutionResourceUse,
|
|
5
4
|
CodeExecutionResult,
|
|
6
5
|
CodeExecutionTimeoutError,
|
|
@@ -17,9 +16,10 @@ import {
|
|
|
17
16
|
CodeSourceError,
|
|
18
17
|
SandboxImplementation,
|
|
19
18
|
type CodeExecutorExecute,
|
|
19
|
+
type CodeExecutionRequest,
|
|
20
20
|
} from "@effect-agent/sandbox";
|
|
21
21
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
22
|
-
import {
|
|
22
|
+
import { Cause, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect";
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* The Cloudflare Dynamic Worker `CodeExecutor` adapter (C4 of ADR-0017;
|
|
@@ -301,6 +301,14 @@ const decodeHostCall = (value: unknown) => {
|
|
|
301
301
|
}
|
|
302
302
|
};
|
|
303
303
|
|
|
304
|
+
const decodeHostCallResult = (value: unknown) => {
|
|
305
|
+
try {
|
|
306
|
+
return Schema.decodeUnknownOption(CodeHostCallResult)(value);
|
|
307
|
+
} catch {
|
|
308
|
+
return Option.none<CodeHostCallResult>();
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
304
312
|
/**
|
|
305
313
|
* Project a host outcome to the plain JSON envelope the harness reads. A
|
|
306
314
|
* `CodeExecutionHost` may return either real `CodeHostCallResult` instances
|
|
@@ -308,10 +316,26 @@ const decodeHostCall = (value: unknown) => {
|
|
|
308
316
|
* Mode capability's broker route), so this reads the shared fields rather than
|
|
309
317
|
* `Schema.encodeSync`, which would reject a plain object.
|
|
310
318
|
*/
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
319
|
+
interface EncodedHostResultPayload {
|
|
320
|
+
readonly encodedPayload: string;
|
|
321
|
+
readonly resultBytes: number;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const encodeHostResultPayload = (
|
|
325
|
+
outcome: CodeHostCallResult,
|
|
326
|
+
): EncodedHostResultPayload | undefined => {
|
|
327
|
+
try {
|
|
328
|
+
const payload = outcome._tag === "CodeHostCallSuccess" ? outcome.value : outcome.error;
|
|
329
|
+
const encodedPayload = JSON.stringify(payload);
|
|
330
|
+
if (encodedPayload === undefined) return undefined;
|
|
331
|
+
return {
|
|
332
|
+
encodedPayload,
|
|
333
|
+
resultBytes: utf8ByteLength(encodedPayload),
|
|
334
|
+
};
|
|
335
|
+
} catch {
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
315
339
|
|
|
316
340
|
const utf8ByteLength = (value: string): number => {
|
|
317
341
|
let total = 0;
|
|
@@ -322,21 +346,24 @@ const utf8ByteLength = (value: string): number => {
|
|
|
322
346
|
return total;
|
|
323
347
|
};
|
|
324
348
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
};
|
|
349
|
+
type HostDispatchFailure =
|
|
350
|
+
| { readonly _tag: "host-call-limit" }
|
|
351
|
+
| { readonly _tag: "host-call-result-limit"; readonly observed: number }
|
|
352
|
+
| { readonly _tag: "host-call-protocol" }
|
|
353
|
+
| { readonly _tag: "wall-clock-timeout" }
|
|
354
|
+
| { readonly _tag: "host-call-defect"; readonly cause: Cause.Cause<never> };
|
|
333
355
|
|
|
334
|
-
interface
|
|
335
|
-
readonly
|
|
356
|
+
interface QueuedHostCall {
|
|
357
|
+
readonly call: CodeHostCall;
|
|
336
358
|
readonly resolve: (value: unknown) => void;
|
|
337
359
|
readonly reject: (reason: unknown) => void;
|
|
338
360
|
}
|
|
339
361
|
|
|
362
|
+
interface ActiveHostCall {
|
|
363
|
+
readonly fiber: Fiber.Fiber<Record<string, unknown>, never>;
|
|
364
|
+
readonly settlement: Promise<void>;
|
|
365
|
+
}
|
|
366
|
+
|
|
340
367
|
/** Reserved global names the harness owns inside the dynamic worker. */
|
|
341
368
|
const reservedHarnessGlobals = new Set(["console"]);
|
|
342
369
|
|
|
@@ -392,83 +419,193 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
392
419
|
// prefix keeps ids debuggable; the random suffix makes them unforgeable.
|
|
393
420
|
const passId = `code-mode-pass-${passCounterState.next}-${crypto.randomUUID()}`;
|
|
394
421
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const pending: Array<PendingHostCall> = [];
|
|
400
|
-
let wake: (() => void) | undefined;
|
|
422
|
+
const startedAt = performance.now();
|
|
423
|
+
const passDeadline = startedAt + Duration.toMillis(request.limits.maxWallTime);
|
|
424
|
+
const remainingPassWallTime = (): Duration.Duration =>
|
|
425
|
+
Duration.millis(Math.max(0, passDeadline - performance.now()));
|
|
401
426
|
let issuedHostCalls = 0;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
wake?.();
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
yield* Effect.acquireRelease(
|
|
414
|
-
Effect.sync(() => {
|
|
415
|
-
passRegistry.set(passId, { dispatch });
|
|
416
|
-
}),
|
|
417
|
-
() =>
|
|
418
|
-
Effect.sync(() => {
|
|
419
|
-
passRegistry.delete(passId);
|
|
420
|
-
}),
|
|
421
|
-
);
|
|
422
|
-
|
|
423
|
-
const nextPending = Effect.suspend(() => {
|
|
424
|
-
const item = pending.shift();
|
|
425
|
-
if (item !== undefined) {
|
|
426
|
-
return Effect.succeed(item);
|
|
427
|
-
}
|
|
428
|
-
return Effect.callback<PendingHostCall>((resume) => {
|
|
429
|
-
wake = () => {
|
|
430
|
-
wake = undefined;
|
|
431
|
-
const next = pending.shift();
|
|
432
|
-
if (next !== undefined) {
|
|
433
|
-
resume(Effect.succeed(next));
|
|
434
|
-
}
|
|
435
|
-
};
|
|
436
|
-
});
|
|
427
|
+
let passOpen = true;
|
|
428
|
+
const queuedHostCalls: Array<QueuedHostCall> = [];
|
|
429
|
+
let activeHostCall: ActiveHostCall | undefined;
|
|
430
|
+
let hostDispatchFailure: HostDispatchFailure | undefined;
|
|
431
|
+
let propagatedHostDispatchFailure: HostDispatchFailure | undefined;
|
|
432
|
+
let signalHostDispatchFailure = (_failure: HostDispatchFailure): void => undefined;
|
|
433
|
+
const hostDispatchFailureSignal = new Promise<HostDispatchFailure>((resolve) => {
|
|
434
|
+
signalHostDispatchFailure = resolve;
|
|
437
435
|
});
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
436
|
+
const rejectQueuedHostCalls = (reason: Error): void => {
|
|
437
|
+
for (const queued of queuedHostCalls.splice(0)) {
|
|
438
|
+
queued.reject(reason);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
const recordHostDispatchFailure = (failure: HostDispatchFailure): void => {
|
|
442
|
+
if (hostDispatchFailure !== undefined) return;
|
|
443
|
+
hostDispatchFailure = failure;
|
|
444
|
+
signalHostDispatchFailure(failure);
|
|
445
|
+
rejectQueuedHostCalls(new Error("Code Mode pass failed"));
|
|
446
|
+
};
|
|
447
|
+
const failHostDispatch = (failure: HostDispatchFailure) => {
|
|
448
|
+
switch (failure._tag) {
|
|
449
|
+
case "host-call-limit":
|
|
450
|
+
return Effect.fail(
|
|
451
|
+
CodeHostCallLimitError.make({
|
|
452
|
+
implementation: dynamicWorkerImplementation,
|
|
453
|
+
limit: request.limits.maxHostCalls,
|
|
454
|
+
logs: [],
|
|
455
|
+
}),
|
|
456
|
+
);
|
|
457
|
+
case "host-call-result-limit":
|
|
458
|
+
return Effect.fail(
|
|
459
|
+
CodeOutputLimitError.make({
|
|
461
460
|
implementation: dynamicWorkerImplementation,
|
|
462
461
|
surface: "host-call-result",
|
|
463
462
|
limit: request.limits.maxHostCallResultBytes,
|
|
464
|
-
observed:
|
|
463
|
+
observed: failure.observed,
|
|
465
464
|
logs: [],
|
|
466
|
-
})
|
|
465
|
+
}),
|
|
466
|
+
);
|
|
467
|
+
case "host-call-protocol":
|
|
468
|
+
return Effect.fail(
|
|
469
|
+
CodeExecutionProtocolError.make({
|
|
470
|
+
implementation: dynamicWorkerImplementation,
|
|
471
|
+
message: "The execution host returned a value outside the CodeHostCallResult schema",
|
|
472
|
+
}),
|
|
473
|
+
);
|
|
474
|
+
case "wall-clock-timeout":
|
|
475
|
+
return Effect.fail(
|
|
476
|
+
CodeExecutionTimeoutError.make({
|
|
477
|
+
implementation: dynamicWorkerImplementation,
|
|
478
|
+
kind: "wall-clock",
|
|
479
|
+
maxWallTime: request.limits.maxWallTime,
|
|
480
|
+
logs: [],
|
|
481
|
+
}),
|
|
482
|
+
);
|
|
483
|
+
case "host-call-defect":
|
|
484
|
+
return Effect.failCause(failure.cause);
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
const propagateHostDispatchFailure = (failure: HostDispatchFailure) =>
|
|
488
|
+
Effect.gen(function* () {
|
|
489
|
+
propagatedHostDispatchFailure = failure;
|
|
490
|
+
return yield* failHostDispatch(failure);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const startNextHostCall = (): void => {
|
|
494
|
+
if (!passOpen || hostDispatchFailure !== undefined || activeHostCall !== undefined) return;
|
|
495
|
+
const queued = queuedHostCalls.shift();
|
|
496
|
+
if (queued === undefined) return;
|
|
497
|
+
|
|
498
|
+
// A Dynamic Worker callback is a new Workers RPC into the loader isolate. Running the
|
|
499
|
+
// complete host call on an independent root fiber breaks its dependency on the still-open
|
|
500
|
+
// guest RPC. Retaining the handle and starting one call at a time preserves bounded,
|
|
501
|
+
// serialized execution and lets pass teardown interrupt and await the active call.
|
|
502
|
+
const fiber = Effect.runFork(
|
|
503
|
+
Effect.yieldNow.pipe(
|
|
504
|
+
Effect.andThen(host.call(queued.call)),
|
|
505
|
+
Effect.timeoutOrElse({
|
|
506
|
+
duration: remainingPassWallTime(),
|
|
507
|
+
orElse: () =>
|
|
508
|
+
Effect.sync(() => {
|
|
509
|
+
recordHostDispatchFailure({ _tag: "wall-clock-timeout" });
|
|
510
|
+
throw new Error("code-mode host call exceeded the pass wall-clock limit");
|
|
511
|
+
}),
|
|
512
|
+
}),
|
|
513
|
+
Effect.map((outcome) => {
|
|
514
|
+
const decoded = decodeHostCallResult(outcome);
|
|
515
|
+
if (Option.isNone(decoded)) {
|
|
516
|
+
recordHostDispatchFailure({ _tag: "host-call-protocol" });
|
|
517
|
+
throw new Error("host-call protocol violation");
|
|
518
|
+
}
|
|
519
|
+
const encoded = encodeHostResultPayload(decoded.value);
|
|
520
|
+
if (
|
|
521
|
+
encoded === undefined ||
|
|
522
|
+
encoded.resultBytes > request.limits.maxHostCallResultBytes
|
|
523
|
+
) {
|
|
524
|
+
recordHostDispatchFailure({
|
|
525
|
+
_tag: "host-call-result-limit",
|
|
526
|
+
observed: encoded?.resultBytes ?? 0,
|
|
527
|
+
});
|
|
528
|
+
throw new Error("host-call result limit exceeded");
|
|
529
|
+
}
|
|
530
|
+
const normalizedPayload: unknown = JSON.parse(encoded.encodedPayload);
|
|
531
|
+
return decoded.value._tag === "CodeHostCallSuccess"
|
|
532
|
+
? { _tag: "CodeHostCallSuccess", value: normalizedPayload }
|
|
533
|
+
: { _tag: "CodeHostCallFailure", error: normalizedPayload };
|
|
534
|
+
}),
|
|
535
|
+
),
|
|
536
|
+
);
|
|
537
|
+
const settlement = Effect.runPromise(Fiber.await(fiber))
|
|
538
|
+
.then((exit) => {
|
|
539
|
+
if (Exit.isSuccess(exit)) {
|
|
540
|
+
queued.resolve(exit.value);
|
|
541
|
+
return;
|
|
467
542
|
}
|
|
468
|
-
|
|
469
|
-
|
|
543
|
+
if (!Cause.hasInterruptsOnly(exit.cause)) {
|
|
544
|
+
recordHostDispatchFailure({ _tag: "host-call-defect", cause: exit.cause });
|
|
545
|
+
}
|
|
546
|
+
queued.reject(new Error("Code Mode host call failed"));
|
|
547
|
+
})
|
|
548
|
+
.finally(() => {
|
|
549
|
+
if (activeHostCall?.fiber === fiber) activeHostCall = undefined;
|
|
550
|
+
startNextHostCall();
|
|
551
|
+
});
|
|
552
|
+
activeHostCall = { fiber, settlement };
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const dispatch = (hostCall: unknown): Promise<unknown> => {
|
|
556
|
+
if (!passOpen) {
|
|
557
|
+
return Promise.reject(new Error("Code Mode pass is closing"));
|
|
558
|
+
}
|
|
559
|
+
issuedHostCalls += 1;
|
|
560
|
+
if (issuedHostCalls > request.limits.maxHostCalls) {
|
|
561
|
+
recordHostDispatchFailure({ _tag: "host-call-limit" });
|
|
562
|
+
return Promise.reject(new Error("host-call limit exceeded"));
|
|
563
|
+
}
|
|
564
|
+
const decoded = decodeHostCall(hostCall);
|
|
565
|
+
if (Option.isNone(decoded)) {
|
|
566
|
+
return Promise.reject(new TypeError("host calls must match the CodeHostCall schema"));
|
|
470
567
|
}
|
|
568
|
+
return new Promise((resolve, reject) => {
|
|
569
|
+
queuedHostCalls.push({ call: decoded.value, resolve, reject });
|
|
570
|
+
startNextHostCall();
|
|
571
|
+
});
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const closeHostDispatch = Effect.gen(function* () {
|
|
575
|
+
passOpen = false;
|
|
576
|
+
passRegistry.delete(passId);
|
|
577
|
+
rejectQueuedHostCalls(new Error("Code Mode pass is closing"));
|
|
578
|
+
const active = activeHostCall;
|
|
579
|
+
if (active !== undefined) {
|
|
580
|
+
yield* Fiber.interrupt(active.fiber);
|
|
581
|
+
yield* Effect.promise(() => active.settlement);
|
|
582
|
+
if (activeHostCall === active) activeHostCall = undefined;
|
|
583
|
+
}
|
|
584
|
+
return hostDispatchFailure;
|
|
471
585
|
});
|
|
586
|
+
const stopHostDispatch = closeHostDispatch.pipe(
|
|
587
|
+
Effect.flatMap((failure) =>
|
|
588
|
+
failure !== undefined && propagatedHostDispatchFailure !== failure
|
|
589
|
+
? propagateHostDispatchFailure(failure)
|
|
590
|
+
: Effect.void,
|
|
591
|
+
),
|
|
592
|
+
);
|
|
593
|
+
const releaseHostDispatch = closeHostDispatch.pipe(
|
|
594
|
+
Effect.flatMap((failure) => {
|
|
595
|
+
if (failure?._tag !== "host-call-defect" || propagatedHostDispatchFailure === failure) {
|
|
596
|
+
return Effect.void;
|
|
597
|
+
}
|
|
598
|
+
propagatedHostDispatchFailure = failure;
|
|
599
|
+
return Effect.failCause(failure.cause);
|
|
600
|
+
}),
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
yield* Effect.acquireRelease(
|
|
604
|
+
Effect.sync(() => {
|
|
605
|
+
passRegistry.set(passId, { dispatch });
|
|
606
|
+
}),
|
|
607
|
+
() => releaseHostDispatch,
|
|
608
|
+
);
|
|
472
609
|
|
|
473
610
|
const workerCode = {
|
|
474
611
|
compatibilityDate: options.compatibilityDate ?? "2025-05-01",
|
|
@@ -505,7 +642,6 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
505
642
|
}),
|
|
506
643
|
};
|
|
507
644
|
|
|
508
|
-
const startedAt = yield* Clock.currentTimeMillis;
|
|
509
645
|
const worker = yield* Effect.acquireRelease(
|
|
510
646
|
Effect.try({
|
|
511
647
|
try: () => options.loader.load(workerCode as never),
|
|
@@ -534,8 +670,6 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
534
670
|
}),
|
|
535
671
|
);
|
|
536
672
|
|
|
537
|
-
const server = yield* serveHostCalls.pipe(Effect.forkScoped);
|
|
538
|
-
|
|
539
673
|
const rpc = Effect.tryPromise({
|
|
540
674
|
try: async () => {
|
|
541
675
|
const entrypoint = worker.getEntrypoint() as unknown as {
|
|
@@ -546,20 +680,29 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
546
680
|
catch: (cause) => classifyWorkerFailure(cause, request.limits.maxWallTime),
|
|
547
681
|
});
|
|
548
682
|
|
|
549
|
-
const raw = yield* Effect.raceFirst(
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
683
|
+
const raw = yield* Effect.raceFirst(
|
|
684
|
+
rpc.pipe(
|
|
685
|
+
Effect.timeoutOrElse({
|
|
686
|
+
duration: remainingPassWallTime(),
|
|
687
|
+
orElse: () =>
|
|
688
|
+
CodeExecutionTimeoutError.make({
|
|
689
|
+
implementation: dynamicWorkerImplementation,
|
|
690
|
+
kind: "wall-clock",
|
|
691
|
+
maxWallTime: request.limits.maxWallTime,
|
|
692
|
+
logs: [],
|
|
693
|
+
}),
|
|
694
|
+
}),
|
|
695
|
+
),
|
|
696
|
+
Effect.promise(() => hostDispatchFailureSignal).pipe(
|
|
697
|
+
Effect.flatMap(propagateHostDispatchFailure),
|
|
698
|
+
),
|
|
561
699
|
);
|
|
562
|
-
|
|
700
|
+
yield* stopHostDispatch;
|
|
701
|
+
const finishedAt = performance.now();
|
|
702
|
+
|
|
703
|
+
if (hostDispatchFailure !== undefined) {
|
|
704
|
+
return yield* propagateHostDispatchFailure(hostDispatchFailure);
|
|
705
|
+
}
|
|
563
706
|
|
|
564
707
|
const outcome = decodeHarnessOutcome(raw);
|
|
565
708
|
if (Option.isNone(outcome)) {
|