@effect-agent/platform-cloudflare 0.0.1-beta.5 → 0.0.1-beta.6
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 +120 -35
- package/dist/index.mjs +888 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -7
- package/src/code-mode-executor.ts +710 -0
- package/src/config.ts +3 -0
- package/src/conversation-object.ts +159 -28
- package/src/index.ts +2 -0
- package/src/layers.ts +14 -5
- package/src/telemetry-internal.ts +432 -0
- package/src/telemetry.ts +37 -0
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { Clock, Context, Duration, Effect, Layer, ManagedRuntime, Option, PubSub, Random, Ref, Schema, Stream } from "effect";
|
|
1
|
+
import { Cause, Clock, Context, Duration, Effect, Exit, Fiber, Layer, ManagedRuntime, Option, PubSub, Random, Ref, Schema, Stream } from "effect";
|
|
2
2
|
import { AbortCommand, AbortIntent, AdmissionConflict, AgentBindingResolver, AppendConflict, ApprovalConflict, ApprovalDecisionCommand, ApprovalDecisionIntent, CanonicalRecordEnvelope, CanonicalSequence, ConversationNotMaterialized, ConversationRead, ConversationStore, ConversationStoreError, DEFAULT_OWNERSHIP_LEASE_DURATION, DefinitionDigests, DeploymentId, DigestError, DurableAgentRuntime, DurableRuntimeConfig, DurableRuntimeFailpoint, DurableRuntimeFailpointError, FenceRejected, IdempotencyKey, IntegrityReport, JoinedToHost, LedgerError, ObligationReport, ObligationThresholds, OperationAuthorizationRequest, OperationAuthorizer, OperationDenied, OwnershipLost, PersistedJson, Principal, ProducerId, Receipt, RecoveryExplanation, RecoveryReport, RetryCommand, RetryRefused, RunJournalError, Settlement, SettlementConflict, SubmissionLedger, SubmissionLookupByKey, ToolReconciler, UnknownResolutionCommand, UnknownResolutionConflict, UnknownResolutionIntent, WakeScheduler } from "@effect-agent/session";
|
|
3
3
|
import { ConversationPortTransport, DEFAULT_MAX_STORED_VALUE_BYTES, conversationStoreLayer, handleEncodedPortRequest, portTransportFailure, routedConversationStoreLayer, routedSubmissionLedgerLayer, storageConfigLayer, storageFailpointLayer, submissionLedgerLayer } from "@effect-agent/storage-cloudflare";
|
|
4
4
|
import { AgentId, AgentInputError, ConversationId, SubmissionId } from "@effect-agent/core";
|
|
5
5
|
import { BrowserCrypto } from "@effect/platform-browser";
|
|
6
6
|
import { SqliteClient } from "@effect/sql-sqlite-do";
|
|
7
|
-
import { DurableObject } from "cloudflare:workers";
|
|
7
|
+
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
|
|
8
|
+
import { CodeExecutionHost, CodeExecutionProtocolError, CodeExecutionResourceUse, CodeExecutionResult, CodeExecutionTimeoutError, CodeExecutor, CodeExecutorStartError, CodeExecutorTerminatedError, CodeExecutorUnsupportedError, CodeHostCall, CodeHostCallLimitError, CodeOutputLimitError, CodeProgramFailedError, CodeSourceError, SandboxImplementation } from "@effect-agent/sandbox";
|
|
8
9
|
//#region src/bindings.ts
|
|
9
10
|
/**
|
|
10
11
|
* Cloudflare platform bindings as Effect services (DEPLOY-010: "Cloudflare platform bindings
|
|
@@ -146,6 +147,8 @@ var CloudflareDurableRuntimeConfigValue = class extends Schema.Class("@effect-ag
|
|
|
146
147
|
abortPollInterval: PositiveMillis,
|
|
147
148
|
/** Canonical observation poll cadence of the Durable Object store. */
|
|
148
149
|
observationPollInterval: NonNegativeMillis,
|
|
150
|
+
/** Cooperative background-export budget; native Object delivery never awaits the flush. */
|
|
151
|
+
telemetryFlushTimeout: PositiveMillis,
|
|
149
152
|
/** Per-value byte bound; must stay under the platform's 2 MB SQLite value limit. */
|
|
150
153
|
maxStoredValueBytes: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(2e6)),
|
|
151
154
|
/** Opt-in full payload/digest-chain audit while opening the store. */
|
|
@@ -164,6 +167,7 @@ const CLOUDFLARE_RUNTIME_DEFAULTS = {
|
|
|
164
167
|
leaseRenewalInterval: 1e4,
|
|
165
168
|
abortPollInterval: 500,
|
|
166
169
|
observationPollInterval: 25,
|
|
170
|
+
telemetryFlushTimeout: 2e3,
|
|
167
171
|
maxStoredValueBytes: DEFAULT_MAX_STORED_VALUE_BYTES,
|
|
168
172
|
verifyOnOpen: false,
|
|
169
173
|
maxQueueDepthPerLane: 256,
|
|
@@ -411,6 +415,31 @@ const conversationPortTransportLayer = Layer.effect(ConversationPortTransport)(E
|
|
|
411
415
|
}).pipe(Effect.withSpan("CloudflarePortTransport.call", { attributes: { conversationId } })) });
|
|
412
416
|
}));
|
|
413
417
|
//#endregion
|
|
418
|
+
//#region src/telemetry.ts
|
|
419
|
+
/** A host exporter failed during one coalesced background export attempt. */
|
|
420
|
+
var CloudflareTelemetryExportError = class extends Schema.TaggedError()("CloudflareTelemetryExportError", {
|
|
421
|
+
/** The original exporter failure, retained for host-side diagnostics. */
|
|
422
|
+
cause: Schema.Defect() }) {};
|
|
423
|
+
/**
|
|
424
|
+
* Host-owned exporter lifecycle for one Cloudflare Conversation Object incarnation.
|
|
425
|
+
*
|
|
426
|
+
* The platform package creates measurements but deliberately does not select an exporter or
|
|
427
|
+
* vendor. Native entrypoint instrumentation consumes this service at the Worker composition edge;
|
|
428
|
+
* applications pass its provider as the second `makeConversationObjectClass` argument together
|
|
429
|
+
* with their Effect Logger, Tracer, and Metric layers. That provider may read `DurableObjectContext` or
|
|
430
|
+
* `ConversationObjectNamespace`, and its typed acquisition error remains visible in construction.
|
|
431
|
+
* `flush` must attempt to export every configured signal. After the native span closes, the
|
|
432
|
+
* Conversation Object reserves it into a shared post-settlement batch before `ctx.waitUntil` under
|
|
433
|
+
* a cooperative timeout. Only each pending/trailing/queued batch's first owner registers its
|
|
434
|
+
* shared Promise. Cycles remain capped at one first exporter attempt plus at most one trailing
|
|
435
|
+
* attempt, with one queued cycle while trailing runs; exporter completion never holds endpoint
|
|
436
|
+
* delivery open and exporter failure never replaces endpoint behavior.
|
|
437
|
+
*/
|
|
438
|
+
var CloudflareRuntimeTelemetry = class CloudflareRuntimeTelemetry extends Context.Service()("@effect-agent/platform-cloudflare/CloudflareRuntimeTelemetry") {
|
|
439
|
+
/** Content-free default selected by `makeConversationObjectClass` when no host Layer is passed. */
|
|
440
|
+
static layerNoop = Layer.succeed(CloudflareRuntimeTelemetry)({ flush: Effect.void });
|
|
441
|
+
};
|
|
442
|
+
//#endregion
|
|
414
443
|
//#region src/layers.ts
|
|
415
444
|
/**
|
|
416
445
|
* Owner-side endpoint body for the Conversation Object's `portCall` (plan §1.3): decode,
|
|
@@ -433,6 +462,7 @@ const configFromOptions = (options) => decodeConfigValue({
|
|
|
433
462
|
leaseRenewalInterval: options.leaseRenewalInterval ?? CLOUDFLARE_RUNTIME_DEFAULTS.leaseRenewalInterval,
|
|
434
463
|
abortPollInterval: options.abortPollInterval ?? CLOUDFLARE_RUNTIME_DEFAULTS.abortPollInterval,
|
|
435
464
|
observationPollInterval: options.observationPollInterval ?? CLOUDFLARE_RUNTIME_DEFAULTS.observationPollInterval,
|
|
465
|
+
telemetryFlushTimeout: options.telemetryFlushTimeout ?? CLOUDFLARE_RUNTIME_DEFAULTS.telemetryFlushTimeout,
|
|
436
466
|
maxStoredValueBytes: options.maxStoredValueBytes ?? CLOUDFLARE_RUNTIME_DEFAULTS.maxStoredValueBytes,
|
|
437
467
|
verifyOnOpen: options.verifyOnOpen ?? CLOUDFLARE_RUNTIME_DEFAULTS.verifyOnOpen,
|
|
438
468
|
limits: {
|
|
@@ -470,9 +500,9 @@ const resolveBindings = (source, context) => source === void 0 ? Effect.succeed(
|
|
|
470
500
|
* Storage compatibility is verified during construction: an incompatible database fails the
|
|
471
501
|
* Layer typed (`DoStorageCompatibilityError`) before anything is mutated (DEPLOY-008).
|
|
472
502
|
*
|
|
473
|
-
* Requires
|
|
474
|
-
*
|
|
475
|
-
* (DEPLOY-010).
|
|
503
|
+
* Requires the two binding services (`DurableObjectContext`, `ConversationObjectNamespace`).
|
|
504
|
+
* Host observability belongs to the Worker composition edge because native entrypoint lifecycle
|
|
505
|
+
* instrumentation, rather than this durable runtime assembly, consumes it (DEPLOY-010).
|
|
476
506
|
*/
|
|
477
507
|
var CloudflareDurableRuntime = class {
|
|
478
508
|
static layer(options) {
|
|
@@ -778,6 +808,258 @@ var CloudflareConversationClient = class CloudflareConversationClient extends Co
|
|
|
778
808
|
}));
|
|
779
809
|
};
|
|
780
810
|
//#endregion
|
|
811
|
+
//#region src/telemetry-internal.ts
|
|
812
|
+
/** Bounded typed control signal used only while a native entrypoint span is active. */
|
|
813
|
+
var CloudflareNativeSpanFailure = class extends Error {
|
|
814
|
+
name = "ConversationObjectDeliveryFailed";
|
|
815
|
+
constructor() {
|
|
816
|
+
super("Cloudflare Conversation Object delivery failed");
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
const stripCloudflareNativeSpanFailures = (cause) => {
|
|
820
|
+
if (!cause.reasons.some((reason) => Cause.isFailReason(reason) && reason.error instanceof CloudflareNativeSpanFailure)) return {
|
|
821
|
+
found: false,
|
|
822
|
+
residual: cause
|
|
823
|
+
};
|
|
824
|
+
const reasons = cause.reasons.filter((reason) => {
|
|
825
|
+
if (Cause.isFailReason(reason) && reason.error instanceof CloudflareNativeSpanFailure) return false;
|
|
826
|
+
return true;
|
|
827
|
+
});
|
|
828
|
+
return {
|
|
829
|
+
found: true,
|
|
830
|
+
residual: Cause.fromReasons(reasons)
|
|
831
|
+
};
|
|
832
|
+
};
|
|
833
|
+
/**
|
|
834
|
+
* @internal Replace an entrypoint Cause with a bounded typed marker inside a span, then remove
|
|
835
|
+
* only that marker and restore the exact invocation-local Cause plus any newly composed reasons.
|
|
836
|
+
*/
|
|
837
|
+
const withCloudflareNativeSpanFailure = (effect, trace) => Effect.suspend(() => {
|
|
838
|
+
let original;
|
|
839
|
+
return trace(effect.pipe(Effect.catchCause((cause) => {
|
|
840
|
+
original = cause;
|
|
841
|
+
return Effect.fail(new CloudflareNativeSpanFailure());
|
|
842
|
+
}))).pipe(Effect.catchCause((cause) => {
|
|
843
|
+
const { found, residual } = stripCloudflareNativeSpanFailures(cause);
|
|
844
|
+
if (!found) return Effect.failCause(residual);
|
|
845
|
+
const restored = original === void 0 ? residual : Cause.combine(original, residual);
|
|
846
|
+
return Effect.failCause(restored);
|
|
847
|
+
}));
|
|
848
|
+
});
|
|
849
|
+
const logCloudflareTelemetryCause = (cause) => {
|
|
850
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
851
|
+
for (const reason of cause.reasons) if (Cause.isFailReason(reason)) kinds.add(Cause.isTimeoutError(reason.error) ? "timeout" : "exporter");
|
|
852
|
+
else if (Cause.isDieReason(reason)) kinds.add("defect");
|
|
853
|
+
else kinds.add("interrupted");
|
|
854
|
+
return Effect.forEach(kinds, (kind) => {
|
|
855
|
+
return (kind === "defect" ? Effect.logError("Cloudflare telemetry flush defect") : Effect.logWarning(kind === "exporter" ? "Cloudflare telemetry exporter failed" : kind === "timeout" ? "Cloudflare telemetry flush exceeded its time budget" : "Cloudflare telemetry flush interrupted")).pipe(Effect.annotateLogs({ "effect_agent.cloudflare.telemetry.failure_kind": kind }));
|
|
856
|
+
}, { discard: true });
|
|
857
|
+
};
|
|
858
|
+
/**
|
|
859
|
+
* @internal Cooperative background flush budget. Interruptible exporters stop at the configured
|
|
860
|
+
* timeout; uninterruptible exporters remain bounded by Cloudflare's waitUntil lifecycle instead
|
|
861
|
+
* of holding the native delivery open.
|
|
862
|
+
*/
|
|
863
|
+
const flushCloudflareRuntimeTelemetry = (timeoutMillis) => Effect.flatMap(CloudflareRuntimeTelemetry, ({ flush }) => flush).pipe(Effect.interruptible, Effect.timeout(Duration.millis(timeoutMillis)), Effect.onExit((exit) => Exit.isFailure(exit) ? logCloudflareTelemetryCause(exit.cause) : Effect.void), Effect.asVoid);
|
|
864
|
+
/** @internal Convert the shared exporter result into the always-fulfilled waitUntil bridge. */
|
|
865
|
+
const fulfillCloudflareTelemetryBackground = (result) => result.then(() => void 0, () => void 0);
|
|
866
|
+
const makeFlushBatch = () => {
|
|
867
|
+
let resolveResult = () => void 0;
|
|
868
|
+
let rejectResult = (_cause) => void 0;
|
|
869
|
+
const result = new Promise((resolve, reject) => {
|
|
870
|
+
resolveResult = resolve;
|
|
871
|
+
rejectResult = reject;
|
|
872
|
+
});
|
|
873
|
+
let resolveReady = () => void 0;
|
|
874
|
+
const ready = new Promise((resolve) => {
|
|
875
|
+
resolveReady = resolve;
|
|
876
|
+
});
|
|
877
|
+
return {
|
|
878
|
+
result,
|
|
879
|
+
background: fulfillCloudflareTelemetryBackground(result),
|
|
880
|
+
resolveResult,
|
|
881
|
+
rejectResult,
|
|
882
|
+
ready,
|
|
883
|
+
resolveReady,
|
|
884
|
+
phase: "pending",
|
|
885
|
+
reservations: 0,
|
|
886
|
+
unsettledDeliveries: 0,
|
|
887
|
+
dropDiagnosed: false
|
|
888
|
+
};
|
|
889
|
+
};
|
|
890
|
+
const makeFlushCycle = () => {
|
|
891
|
+
let resolveCycle = () => void 0;
|
|
892
|
+
let rejectCycle = (_cause) => void 0;
|
|
893
|
+
const promise = new Promise((resolve, reject) => {
|
|
894
|
+
resolveCycle = resolve;
|
|
895
|
+
rejectCycle = reject;
|
|
896
|
+
});
|
|
897
|
+
fulfillCloudflareTelemetryBackground(promise);
|
|
898
|
+
return {
|
|
899
|
+
promise,
|
|
900
|
+
resolve: resolveCycle,
|
|
901
|
+
reject: rejectCycle,
|
|
902
|
+
first: makeFlushBatch(),
|
|
903
|
+
phase: "pending",
|
|
904
|
+
trailing: void 0
|
|
905
|
+
};
|
|
906
|
+
};
|
|
907
|
+
/**
|
|
908
|
+
* @internal Coordinate exporter attempts for one Conversation Object incarnation. Each cycle is
|
|
909
|
+
* capped at a first attempt plus one trailing attempt. Each batch is reserved synchronously and
|
|
910
|
+
* waits for every delivery assigned to it before exporting. Only the first owner of the pending,
|
|
911
|
+
* trailing, or queued batch registers that batch's shared background Promise, so a stalled
|
|
912
|
+
* exporter cannot accumulate per-delivery waitUntil registrations. Each batch retains at most
|
|
913
|
+
* `MAX_CLOUDFLARE_TELEMETRY_BATCH_DELIVERIES` settlement Promises; excess deliveries are lossy-
|
|
914
|
+
* coalesced into the already-requested export without settlement ownership and diagnosed once for
|
|
915
|
+
* that batch. Failures remain rejected by the raw cycle after every requested attempt runs; each
|
|
916
|
+
* shared waitUntil bridge always fulfills.
|
|
917
|
+
*/
|
|
918
|
+
const makeCloudflareTelemetryFlushCoordinator = (flush, options = {}) => {
|
|
919
|
+
let active;
|
|
920
|
+
let queued;
|
|
921
|
+
const closeBatch = (batch) => {
|
|
922
|
+
if (batch.phase !== "pending") return;
|
|
923
|
+
batch.phase = "waiting";
|
|
924
|
+
if (batch.unsettledDeliveries === 0) batch.resolveReady();
|
|
925
|
+
};
|
|
926
|
+
const cancelBatch = (batch) => {
|
|
927
|
+
if (batch.phase === "settled" || batch.phase === "cancelled") return;
|
|
928
|
+
batch.phase = "cancelled";
|
|
929
|
+
batch.resolveReady();
|
|
930
|
+
batch.resolveResult();
|
|
931
|
+
};
|
|
932
|
+
const reserveDelivery = (batch, delivery) => {
|
|
933
|
+
if (batch.reservations >= 64) {
|
|
934
|
+
if (!batch.dropDiagnosed) {
|
|
935
|
+
batch.dropDiagnosed = true;
|
|
936
|
+
try {
|
|
937
|
+
options.onReservationDropped?.();
|
|
938
|
+
} catch {}
|
|
939
|
+
}
|
|
940
|
+
return {
|
|
941
|
+
owner: false,
|
|
942
|
+
dropped: true
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
const owner = batch.reservations === 0;
|
|
946
|
+
batch.reservations += 1;
|
|
947
|
+
batch.unsettledDeliveries += 1;
|
|
948
|
+
const settled = () => {
|
|
949
|
+
batch.unsettledDeliveries -= 1;
|
|
950
|
+
if (batch.phase === "waiting" && batch.unsettledDeliveries === 0) batch.resolveReady();
|
|
951
|
+
};
|
|
952
|
+
delivery.then(settled, settled);
|
|
953
|
+
return {
|
|
954
|
+
owner,
|
|
955
|
+
dropped: false
|
|
956
|
+
};
|
|
957
|
+
};
|
|
958
|
+
const runCycle = async (cycle) => {
|
|
959
|
+
const failures = [];
|
|
960
|
+
const runAttempt = async () => {
|
|
961
|
+
try {
|
|
962
|
+
await flush();
|
|
963
|
+
} catch (cause) {
|
|
964
|
+
failures.push(cause);
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
const runBatch = async (batch) => {
|
|
968
|
+
closeBatch(batch);
|
|
969
|
+
await batch.ready;
|
|
970
|
+
if (batch.phase === "cancelled") return;
|
|
971
|
+
batch.phase = "running";
|
|
972
|
+
const failureStart = failures.length;
|
|
973
|
+
await runAttempt();
|
|
974
|
+
batch.phase = "settled";
|
|
975
|
+
if (failures.length === failureStart) batch.resolveResult();
|
|
976
|
+
else batch.rejectResult(failures[failures.length - 1]);
|
|
977
|
+
};
|
|
978
|
+
try {
|
|
979
|
+
cycle.phase = "first";
|
|
980
|
+
await runBatch(cycle.first);
|
|
981
|
+
const trailing = cycle.trailing;
|
|
982
|
+
if (trailing !== void 0) {
|
|
983
|
+
cycle.phase = "trailing";
|
|
984
|
+
await runBatch(trailing);
|
|
985
|
+
}
|
|
986
|
+
cycle.phase = "settling";
|
|
987
|
+
options.onSettling?.();
|
|
988
|
+
} finally {
|
|
989
|
+
const next = queued;
|
|
990
|
+
queued = void 0;
|
|
991
|
+
active = next;
|
|
992
|
+
if (next !== void 0) startCycle(next);
|
|
993
|
+
}
|
|
994
|
+
if (failures.length === 0) return;
|
|
995
|
+
if (failures.length === 1) throw failures[0];
|
|
996
|
+
throw new AggregateError(failures, `${failures.length} Cloudflare telemetry flush attempts failed`);
|
|
997
|
+
};
|
|
998
|
+
const startCycle = (cycle) => {
|
|
999
|
+
Promise.resolve().then(() => runCycle(cycle)).then(cycle.resolve, cycle.reject);
|
|
1000
|
+
};
|
|
1001
|
+
const reserve = (delivery) => {
|
|
1002
|
+
let cycle;
|
|
1003
|
+
let batch;
|
|
1004
|
+
let shouldStart = false;
|
|
1005
|
+
if (active === void 0) {
|
|
1006
|
+
cycle = makeFlushCycle();
|
|
1007
|
+
active = cycle;
|
|
1008
|
+
batch = cycle.first;
|
|
1009
|
+
shouldStart = true;
|
|
1010
|
+
} else if (active.phase === "pending" && active.first.phase !== "cancelled") {
|
|
1011
|
+
cycle = active;
|
|
1012
|
+
batch = active.first;
|
|
1013
|
+
} else if (active.phase === "first") {
|
|
1014
|
+
cycle = active;
|
|
1015
|
+
batch = active.trailing ??= makeFlushBatch();
|
|
1016
|
+
} else {
|
|
1017
|
+
if (queued === void 0) queued = makeFlushCycle();
|
|
1018
|
+
cycle = queued;
|
|
1019
|
+
batch = queued.first;
|
|
1020
|
+
}
|
|
1021
|
+
const { dropped, owner } = reserveDelivery(batch, delivery);
|
|
1022
|
+
if (shouldStart) startCycle(cycle);
|
|
1023
|
+
return {
|
|
1024
|
+
background: batch.background,
|
|
1025
|
+
cycle: cycle.promise,
|
|
1026
|
+
owner,
|
|
1027
|
+
dropped,
|
|
1028
|
+
cancel: () => {
|
|
1029
|
+
if (!owner) return;
|
|
1030
|
+
cancelBatch(batch);
|
|
1031
|
+
if (cycle.trailing === batch && cycle.phase === "first") cycle.trailing = void 0;
|
|
1032
|
+
else if (queued === cycle && active !== cycle) {
|
|
1033
|
+
queued = void 0;
|
|
1034
|
+
cycle.resolve();
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
};
|
|
1039
|
+
return { reserve };
|
|
1040
|
+
};
|
|
1041
|
+
/**
|
|
1042
|
+
* @internal Reserve telemetry before registration without allowing a synchronous platform failure
|
|
1043
|
+
* to replace the already-running native delivery Promise. Only a batch owner calls waitUntil; its
|
|
1044
|
+
* shared bridge is already always fulfilled, and registration failure cancels export for that
|
|
1045
|
+
* unowned batch.
|
|
1046
|
+
*/
|
|
1047
|
+
const registerCloudflareTelemetryAfterNativeSettlement = (waitUntil, delivery, reserve, diagnoseRegistrationFailure) => {
|
|
1048
|
+
const reservation = reserve(delivery);
|
|
1049
|
+
if (!reservation.owner) return delivery;
|
|
1050
|
+
try {
|
|
1051
|
+
waitUntil(reservation.background);
|
|
1052
|
+
} catch (cause) {
|
|
1053
|
+
reservation.cancel();
|
|
1054
|
+
try {
|
|
1055
|
+
diagnoseRegistrationFailure(cause);
|
|
1056
|
+
} catch {}
|
|
1057
|
+
}
|
|
1058
|
+
return delivery;
|
|
1059
|
+
};
|
|
1060
|
+
/** @internal Content-free diagnostic for a rejected Cloudflare waitUntil registration. */
|
|
1061
|
+
const logCloudflareWaitUntilRegistrationFailure = (_cause) => Effect.logError("Cloudflare telemetry waitUntil registration failed").pipe(Effect.annotateLogs({ "effect_agent.cloudflare.telemetry.failure_kind": "wait_until_registration" }));
|
|
1062
|
+
//#endregion
|
|
781
1063
|
//#region src/conversation-object.ts
|
|
782
1064
|
/** Port envelope tags whose owner-side execution durably mutates this Object's lane. */
|
|
783
1065
|
const MUTATING_PORT_TAGS = /* @__PURE__ */ new Set([
|
|
@@ -1017,62 +1299,643 @@ const gateEndpoint = Effect.gen(function* () {
|
|
|
1017
1299
|
yield* (yield* ConversationMaintenance).ensureAlarm;
|
|
1018
1300
|
});
|
|
1019
1301
|
/**
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1022
|
-
|
|
1023
|
-
|
|
1302
|
+
* Owner-side delivery measurement. The exact endpoint Cause is hidden behind a bounded typed
|
|
1303
|
+
* marker while the span closes, then restored together with any newly composed real reasons.
|
|
1304
|
+
*/
|
|
1305
|
+
const observeNativeEntrypoint = (entrypoint, effect) => Effect.gen(function* () {
|
|
1306
|
+
const identity = yield* ConversationObjectIdentity;
|
|
1307
|
+
const config = yield* CloudflareDurableRuntimeConfig;
|
|
1308
|
+
return yield* withCloudflareNativeSpanFailure(effect, (masked) => masked.pipe(Effect.withSpan(`effect_agent.cloudflare.conversation_object.${entrypoint}`, {
|
|
1309
|
+
kind: "server",
|
|
1310
|
+
attributes: {
|
|
1311
|
+
"effect_agent.cloudflare.entrypoint": entrypoint,
|
|
1312
|
+
"effect_agent.deployment.id": config.deploymentId,
|
|
1313
|
+
conversationId: identity.conversationId,
|
|
1314
|
+
producerId: identity.producerId
|
|
1315
|
+
}
|
|
1316
|
+
})));
|
|
1317
|
+
});
|
|
1318
|
+
const flushNativeEntrypointTelemetry = Effect.flatMap(CloudflareDurableRuntimeConfig, (config) => flushCloudflareRuntimeTelemetry(config.telemetryFlushTimeout));
|
|
1319
|
+
/**
|
|
1320
|
+
* Build the application's Conversation Object class (export it from the Worker entry).
|
|
1321
|
+
* `telemetry` is the host composition boundary: it may require the Object context/namespace and
|
|
1322
|
+
* retain a typed acquisition error, while any additional output services remain available to the
|
|
1323
|
+
* cached runtime. `TelemetryError` stays the first explicit generic for source compatibility;
|
|
1324
|
+
* additional outputs infer from the Layer. Omitting it installs the content-free no-op service.
|
|
1325
|
+
* The explicit return type is what makes declaration emit possible: the class body carries a
|
|
1326
|
+
* private runtime field, and TS4094 rejects inferring an exported anonymous class type around it.
|
|
1024
1327
|
*/
|
|
1025
|
-
const makeConversationObjectClass = (options) => {
|
|
1328
|
+
const makeConversationObjectClass = (options, telemetry) => {
|
|
1026
1329
|
class ConversationObject extends DurableObject {
|
|
1027
1330
|
#runtime;
|
|
1331
|
+
#ctx;
|
|
1332
|
+
#telemetryFlush;
|
|
1028
1333
|
constructor(ctx, env) {
|
|
1029
1334
|
super(ctx, env);
|
|
1030
|
-
this.#
|
|
1335
|
+
this.#ctx = ctx;
|
|
1336
|
+
const platform = Layer.mergeAll(DurableObjectContext.layer(ctx, env), conversationNamespaceLayer(env, options.namespaceBinding));
|
|
1337
|
+
const hostTelemetry = (telemetry ?? CloudflareRuntimeTelemetry.layerNoop).pipe(Layer.provideMerge(platform));
|
|
1338
|
+
this.#runtime = ManagedRuntime.make(CloudflareDurableRuntime.layer(options).pipe(Layer.provideMerge(hostTelemetry)));
|
|
1339
|
+
this.#telemetryFlush = makeCloudflareTelemetryFlushCoordinator(() => this.#runtime.runPromise(flushNativeEntrypointTelemetry), { onReservationDropped: () => {
|
|
1340
|
+
this.#runtime.runSyncExit(Effect.logWarning("Cloudflare telemetry delivery coalesced at the batch reservation limit").pipe(Effect.annotateLogs({ "effect_agent.cloudflare.telemetry.failure_kind": "reservation_limit" })));
|
|
1341
|
+
} });
|
|
1031
1342
|
ctx.blockConcurrencyWhile(() => this.#runtime.runPromise(gateEndpoint));
|
|
1032
1343
|
}
|
|
1344
|
+
#runNative(entrypoint, effect) {
|
|
1345
|
+
return registerCloudflareTelemetryAfterNativeSettlement((background) => this.#ctx.waitUntil(background), this.#runtime.runPromise(observeNativeEntrypoint(entrypoint, effect)), this.#telemetryFlush.reserve, (cause) => {
|
|
1346
|
+
this.#runtime.runSyncExit(logCloudflareWaitUntilRegistrationFailure(cause));
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1033
1349
|
async submitEncoded(encoded) {
|
|
1034
|
-
return this.#
|
|
1350
|
+
return this.#runNative("submit", submitEndpoint(encoded));
|
|
1035
1351
|
}
|
|
1036
1352
|
async awaitSettlementEncoded(encoded) {
|
|
1037
|
-
return this.#
|
|
1353
|
+
return this.#runNative("await_settlement", awaitSettlementEndpoint(encoded));
|
|
1038
1354
|
}
|
|
1039
1355
|
async observePage(encoded) {
|
|
1040
|
-
return this.#
|
|
1356
|
+
return this.#runNative("observe", observePageEndpoint(encoded));
|
|
1041
1357
|
}
|
|
1042
1358
|
async abortEncoded(encoded) {
|
|
1043
|
-
return this.#
|
|
1359
|
+
return this.#runNative("abort", abortEndpoint(encoded));
|
|
1044
1360
|
}
|
|
1045
1361
|
async resolveApprovalEncoded(encoded) {
|
|
1046
|
-
return this.#
|
|
1362
|
+
return this.#runNative("resolve_approval", resolveApprovalEndpoint(encoded));
|
|
1047
1363
|
}
|
|
1048
1364
|
async resolveUnknownEncoded(encoded) {
|
|
1049
|
-
return this.#
|
|
1365
|
+
return this.#runNative("resolve_unknown", resolveUnknownEndpoint(encoded));
|
|
1050
1366
|
}
|
|
1051
1367
|
async explainEncoded(encoded) {
|
|
1052
|
-
return this.#
|
|
1368
|
+
return this.#runNative("explain", explainEndpoint(encoded));
|
|
1053
1369
|
}
|
|
1054
1370
|
async verifyEncoded(encoded) {
|
|
1055
|
-
return this.#
|
|
1371
|
+
return this.#runNative("verify", verifyEndpoint(encoded));
|
|
1056
1372
|
}
|
|
1057
1373
|
async retryEncoded(encoded) {
|
|
1058
|
-
return this.#
|
|
1374
|
+
return this.#runNative("retry", retryEndpoint(encoded));
|
|
1059
1375
|
}
|
|
1060
1376
|
async obligationsEncoded(encoded) {
|
|
1061
|
-
return this.#
|
|
1377
|
+
return this.#runNative("obligations", obligationsEndpoint(encoded));
|
|
1062
1378
|
}
|
|
1063
1379
|
async portCall(encoded) {
|
|
1064
|
-
return this.#
|
|
1380
|
+
return this.#runNative("port_call", portCallEndpoint(encoded));
|
|
1065
1381
|
}
|
|
1066
1382
|
async wake() {
|
|
1067
|
-
await this.#
|
|
1383
|
+
await this.#runNative("wake", wakeEndpoint);
|
|
1068
1384
|
}
|
|
1069
1385
|
async alarm() {
|
|
1070
|
-
await this.#
|
|
1386
|
+
await this.#runNative("alarm", alarmEndpoint);
|
|
1071
1387
|
}
|
|
1072
1388
|
}
|
|
1073
1389
|
return ConversationObject;
|
|
1074
1390
|
};
|
|
1075
1391
|
//#endregion
|
|
1076
|
-
|
|
1392
|
+
//#region src/code-mode-executor.ts
|
|
1393
|
+
/**
|
|
1394
|
+
* The Cloudflare Dynamic Worker `CodeExecutor` adapter (C4 of ADR-0017;
|
|
1395
|
+
* DEPLOY-011). Each pass loads one fresh Worker through the Worker Loader
|
|
1396
|
+
* with `globalOutbound: null`, so generated code has no ambient network,
|
|
1397
|
+
* bindings, or secrets; its only authority is the pass-scoped host stub that
|
|
1398
|
+
* routes back to `CodeModeHostEntrypoint` and, from there, into the pass's
|
|
1399
|
+
* `CodeExecutionHost` service. Platform CPU limits stop synchronous runaway
|
|
1400
|
+
* programs; the executor-owned wall-clock deadline interrupts asynchronously
|
|
1401
|
+
* suspended passes. Deployment class `E` only: the adapter records no
|
|
1402
|
+
* persistent state and a later pass may run in a completely different
|
|
1403
|
+
* isolate.
|
|
1404
|
+
*/
|
|
1405
|
+
const dynamicWorkerImplementation = SandboxImplementation.make({
|
|
1406
|
+
isolation: "isolated",
|
|
1407
|
+
identity: "cloudflare-dynamic-worker"
|
|
1408
|
+
});
|
|
1409
|
+
/**
|
|
1410
|
+
* Live passes by identity. Entries are Scope-managed: registered when a pass
|
|
1411
|
+
* opens and removed by its finalizer, so a stale harness (or a forged
|
|
1412
|
+
* `passId`) cannot reach any host authority.
|
|
1413
|
+
*/
|
|
1414
|
+
const passRegistry = /* @__PURE__ */ new Map();
|
|
1415
|
+
/**
|
|
1416
|
+
* The host-side RPC target for dynamic workers. The application exposes it
|
|
1417
|
+
* from its Worker entry (`export { CodeModeHostEntrypoint }`) and hands the
|
|
1418
|
+
* adapter a same-instance stub — `ctx.exports.CodeModeHostEntrypoint()` in
|
|
1419
|
+
* production (a self service binding may reach a different instance and must
|
|
1420
|
+
* not be used there); tests bind it through Miniflare's `kCurrentWorker`.
|
|
1421
|
+
*/
|
|
1422
|
+
var CodeModeHostEntrypoint = class extends WorkerEntrypoint {
|
|
1423
|
+
async call(passId, hostCall) {
|
|
1424
|
+
const pass = passRegistry.get(String(passId));
|
|
1425
|
+
if (pass === void 0) throw new Error("Unknown Code Mode pass");
|
|
1426
|
+
return pass.dispatch(hostCall);
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
/**
|
|
1430
|
+
* The fixed harness loaded as the dynamic worker's main module. The generated
|
|
1431
|
+
* source becomes `program.mjs` (`export default (<expression>);`) — a module,
|
|
1432
|
+
* never `eval`. The harness installs namespace globals and a bounded console,
|
|
1433
|
+
* imports the program, invokes it exactly once, and returns one envelope the
|
|
1434
|
+
* host validates through Effect Schema.
|
|
1435
|
+
*/
|
|
1436
|
+
const HARNESS_MODULE = String.raw`
|
|
1437
|
+
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
1438
|
+
import programDefault from "./program.js";
|
|
1439
|
+
|
|
1440
|
+
const encoder = new TextEncoder();
|
|
1441
|
+
const utf8 = (text) => encoder.encode(text).byteLength;
|
|
1442
|
+
const safeText = (value) => {
|
|
1443
|
+
try {
|
|
1444
|
+
if (value instanceof Error) return (value.name + ": " + value.message).slice(0, 4000);
|
|
1445
|
+
if (typeof value === "string") return value.slice(0, 4000);
|
|
1446
|
+
const encoded = JSON.stringify(value);
|
|
1447
|
+
return (encoded === undefined ? String(value) : encoded).slice(0, 4000);
|
|
1448
|
+
} catch {
|
|
1449
|
+
return "[unserializable value]";
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
const safeJson = (value) => {
|
|
1453
|
+
try {
|
|
1454
|
+
const encoded = JSON.stringify(value);
|
|
1455
|
+
if (encoded !== undefined && encoded.length <= 4000) return JSON.parse(encoded);
|
|
1456
|
+
} catch {}
|
|
1457
|
+
return safeText(value);
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
export default class CodeModeHarness extends WorkerEntrypoint {
|
|
1461
|
+
async run() {
|
|
1462
|
+
const config = JSON.parse(this.env.CODE_MODE_PASS);
|
|
1463
|
+
const host = this.env.CODE_MODE_HOST;
|
|
1464
|
+
const limits = config.limits;
|
|
1465
|
+
const logs = [];
|
|
1466
|
+
let logBytes = 0;
|
|
1467
|
+
let fatal;
|
|
1468
|
+
const boundedLogs = () => logs.slice(0, 4096);
|
|
1469
|
+
const write = (...values) => {
|
|
1470
|
+
const joined = values.map(safeText).join(" ");
|
|
1471
|
+
const line = joined.length > 16000 ? joined.slice(0, 15999) + "…" : joined;
|
|
1472
|
+
const bytes = utf8(line);
|
|
1473
|
+
if (logs.length >= 4096 || logBytes + bytes > limits.maxLogBytes) {
|
|
1474
|
+
fatal = fatal ?? { _tag: "log-limit", observed: logBytes + bytes, logs: boundedLogs() };
|
|
1475
|
+
throw new Error("code-mode log limit exceeded");
|
|
1476
|
+
}
|
|
1477
|
+
logs.push(line);
|
|
1478
|
+
logBytes += bytes;
|
|
1479
|
+
};
|
|
1480
|
+
globalThis.console = { log: write, info: write, warn: write, error: write, debug: write };
|
|
1481
|
+
|
|
1482
|
+
let hostCalls = 0;
|
|
1483
|
+
const makeMethod = (namespace, method) => async (argument) => {
|
|
1484
|
+
hostCalls += 1;
|
|
1485
|
+
if (hostCalls > limits.maxHostCalls) {
|
|
1486
|
+
fatal = fatal ?? { _tag: "host-call-limit", logs: boundedLogs() };
|
|
1487
|
+
throw new Error("code-mode host-call limit exceeded");
|
|
1488
|
+
}
|
|
1489
|
+
let argText;
|
|
1490
|
+
try {
|
|
1491
|
+
argText = JSON.stringify(argument);
|
|
1492
|
+
} catch {}
|
|
1493
|
+
if (argText === undefined || utf8(argText) > limits.maxHostCallArgumentBytes) {
|
|
1494
|
+
fatal = fatal ?? {
|
|
1495
|
+
_tag: "argument-limit",
|
|
1496
|
+
observed: argText === undefined ? 0 : utf8(argText),
|
|
1497
|
+
logs: boundedLogs(),
|
|
1498
|
+
};
|
|
1499
|
+
throw new Error("code-mode host-call argument limit exceeded");
|
|
1500
|
+
}
|
|
1501
|
+
const outcome = await host.call(config.passId, {
|
|
1502
|
+
namespace,
|
|
1503
|
+
method,
|
|
1504
|
+
argument: JSON.parse(argText),
|
|
1505
|
+
});
|
|
1506
|
+
if (outcome !== null && typeof outcome === "object" && outcome._tag === "CodeHostCallSuccess") {
|
|
1507
|
+
return outcome.value;
|
|
1508
|
+
}
|
|
1509
|
+
if (outcome !== null && typeof outcome === "object" && outcome._tag === "CodeHostCallFailure") {
|
|
1510
|
+
throw outcome.error;
|
|
1511
|
+
}
|
|
1512
|
+
fatal = fatal ?? { _tag: "protocol", message: "host returned an unrecognized outcome" };
|
|
1513
|
+
throw new Error("code-mode host protocol violation");
|
|
1514
|
+
};
|
|
1515
|
+
for (const namespace of config.namespaces) {
|
|
1516
|
+
const methods = {};
|
|
1517
|
+
for (const method of namespace.methods) {
|
|
1518
|
+
methods[method] = makeMethod(namespace.name, method);
|
|
1519
|
+
}
|
|
1520
|
+
globalThis[namespace.name] = methods;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// program.js is imported statically at the top of this module, so a
|
|
1524
|
+
// syntactically invalid program fails the whole harness at load (mapped
|
|
1525
|
+
// to a source error by the host). Using a static import keeps this module
|
|
1526
|
+
// free of dynamic-import expressions, which single-script Miniflare hosts
|
|
1527
|
+
// reject. The isolation boundary does NOT depend on the ordering of this
|
|
1528
|
+
// import versus the console/namespace shims installed below: the loaded
|
|
1529
|
+
// Worker has globalOutbound: null and no bindings, secrets, or env from
|
|
1530
|
+
// the Worker Loader config BEFORE any module in the graph evaluates, so
|
|
1531
|
+
// module-level program code has no ambient authority regardless. The
|
|
1532
|
+
// shims below are usability wrappers (bounded console, namespace globals),
|
|
1533
|
+
// and the accepted program is a single async-function expression whose
|
|
1534
|
+
// body runs only when invoked here — after the shims exist.
|
|
1535
|
+
const program = programDefault;
|
|
1536
|
+
if (typeof program !== "function") {
|
|
1537
|
+
return { _tag: "source-not-a-function", actual: typeof program };
|
|
1538
|
+
}
|
|
1539
|
+
try {
|
|
1540
|
+
const value = await program();
|
|
1541
|
+
if (fatal !== undefined) return fatal;
|
|
1542
|
+
let text;
|
|
1543
|
+
try {
|
|
1544
|
+
text = JSON.stringify(value);
|
|
1545
|
+
} catch {}
|
|
1546
|
+
if (text === undefined) {
|
|
1547
|
+
return {
|
|
1548
|
+
_tag: "program-failed",
|
|
1549
|
+
reason: "non-json-result",
|
|
1550
|
+
thrown: null,
|
|
1551
|
+
message: "The program must return a JSON value",
|
|
1552
|
+
logs: boundedLogs(),
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
const resultBytes = utf8(text);
|
|
1556
|
+
if (resultBytes > limits.maxResultBytes) {
|
|
1557
|
+
return { _tag: "result-limit", observed: resultBytes, logs: boundedLogs() };
|
|
1558
|
+
}
|
|
1559
|
+
return {
|
|
1560
|
+
_tag: "completed",
|
|
1561
|
+
value: JSON.parse(text),
|
|
1562
|
+
logs: boundedLogs(),
|
|
1563
|
+
hostCalls,
|
|
1564
|
+
logBytes,
|
|
1565
|
+
resultBytes,
|
|
1566
|
+
};
|
|
1567
|
+
} catch (cause) {
|
|
1568
|
+
if (fatal !== undefined) return fatal;
|
|
1569
|
+
return {
|
|
1570
|
+
_tag: "program-failed",
|
|
1571
|
+
reason: cause instanceof Error ? "threw" : "rejected",
|
|
1572
|
+
thrown: safeJson(cause),
|
|
1573
|
+
message: safeText(cause),
|
|
1574
|
+
logs: boundedLogs(),
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
`;
|
|
1580
|
+
const BoundedLogs = Schema.Array(Schema.String.check(Schema.isMaxLength(16 * 1024))).check(Schema.isMaxLength(4096));
|
|
1581
|
+
const HarnessCompleted = Schema.Struct({
|
|
1582
|
+
_tag: Schema.Literal("completed"),
|
|
1583
|
+
value: Schema.Json,
|
|
1584
|
+
logs: BoundedLogs,
|
|
1585
|
+
hostCalls: Schema.Natural,
|
|
1586
|
+
logBytes: Schema.Natural,
|
|
1587
|
+
resultBytes: Schema.Natural
|
|
1588
|
+
});
|
|
1589
|
+
const HarnessSourceInvalid = Schema.Struct({
|
|
1590
|
+
_tag: Schema.Literal("source-invalid"),
|
|
1591
|
+
message: Schema.String
|
|
1592
|
+
});
|
|
1593
|
+
const HarnessNotAFunction = Schema.Struct({
|
|
1594
|
+
_tag: Schema.Literal("source-not-a-function"),
|
|
1595
|
+
actual: Schema.String
|
|
1596
|
+
});
|
|
1597
|
+
const HarnessProgramFailed = Schema.Struct({
|
|
1598
|
+
_tag: Schema.Literal("program-failed"),
|
|
1599
|
+
reason: Schema.Literals([
|
|
1600
|
+
"threw",
|
|
1601
|
+
"rejected",
|
|
1602
|
+
"non-json-result"
|
|
1603
|
+
]),
|
|
1604
|
+
thrown: Schema.Json,
|
|
1605
|
+
message: Schema.String,
|
|
1606
|
+
logs: BoundedLogs
|
|
1607
|
+
});
|
|
1608
|
+
const HarnessLogLimit = Schema.Struct({
|
|
1609
|
+
_tag: Schema.Literal("log-limit"),
|
|
1610
|
+
observed: Schema.Natural,
|
|
1611
|
+
logs: BoundedLogs
|
|
1612
|
+
});
|
|
1613
|
+
const HarnessArgumentLimit = Schema.Struct({
|
|
1614
|
+
_tag: Schema.Literal("argument-limit"),
|
|
1615
|
+
observed: Schema.Natural,
|
|
1616
|
+
logs: BoundedLogs
|
|
1617
|
+
});
|
|
1618
|
+
const HarnessResultLimit = Schema.Struct({
|
|
1619
|
+
_tag: Schema.Literal("result-limit"),
|
|
1620
|
+
observed: Schema.Natural,
|
|
1621
|
+
logs: BoundedLogs
|
|
1622
|
+
});
|
|
1623
|
+
const HarnessHostCallLimit = Schema.Struct({
|
|
1624
|
+
_tag: Schema.Literal("host-call-limit"),
|
|
1625
|
+
logs: BoundedLogs
|
|
1626
|
+
});
|
|
1627
|
+
const HarnessProtocol = Schema.Struct({
|
|
1628
|
+
_tag: Schema.Literal("protocol"),
|
|
1629
|
+
message: Schema.String
|
|
1630
|
+
});
|
|
1631
|
+
const HarnessOutcome = Schema.Union([
|
|
1632
|
+
HarnessCompleted,
|
|
1633
|
+
HarnessSourceInvalid,
|
|
1634
|
+
HarnessNotAFunction,
|
|
1635
|
+
HarnessProgramFailed,
|
|
1636
|
+
HarnessLogLimit,
|
|
1637
|
+
HarnessArgumentLimit,
|
|
1638
|
+
HarnessResultLimit,
|
|
1639
|
+
HarnessHostCallLimit,
|
|
1640
|
+
HarnessProtocol
|
|
1641
|
+
]);
|
|
1642
|
+
const decodeHarnessOutcome = (value) => {
|
|
1643
|
+
try {
|
|
1644
|
+
return Schema.decodeUnknownOption(HarnessOutcome)(value);
|
|
1645
|
+
} catch {
|
|
1646
|
+
return Option.none();
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
const decodeHostCall = (value) => {
|
|
1650
|
+
try {
|
|
1651
|
+
return Schema.decodeUnknownOption(CodeHostCall)(value);
|
|
1652
|
+
} catch {
|
|
1653
|
+
return Option.none();
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
/**
|
|
1657
|
+
* Project a host outcome to the plain JSON envelope the harness reads. A
|
|
1658
|
+
* `CodeExecutionHost` may return either real `CodeHostCallResult` instances
|
|
1659
|
+
* (the substitute and conformance kit) or plain-object equivalents (the Code
|
|
1660
|
+
* Mode capability's broker route), so this reads the shared fields rather than
|
|
1661
|
+
* `Schema.encodeSync`, which would reject a plain object.
|
|
1662
|
+
*/
|
|
1663
|
+
const hostResultEnvelope = (outcome) => outcome._tag === "CodeHostCallSuccess" ? {
|
|
1664
|
+
_tag: "CodeHostCallSuccess",
|
|
1665
|
+
value: outcome.value
|
|
1666
|
+
} : {
|
|
1667
|
+
_tag: "CodeHostCallFailure",
|
|
1668
|
+
error: outcome.error
|
|
1669
|
+
};
|
|
1670
|
+
const utf8ByteLength = (value) => {
|
|
1671
|
+
let total = 0;
|
|
1672
|
+
for (const character of value) {
|
|
1673
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
1674
|
+
total += codePoint <= 127 ? 1 : codePoint <= 2047 ? 2 : codePoint <= 65535 ? 3 : 4;
|
|
1675
|
+
}
|
|
1676
|
+
return total;
|
|
1677
|
+
};
|
|
1678
|
+
const encodedJsonByteLength = (value) => {
|
|
1679
|
+
try {
|
|
1680
|
+
const encoded = JSON.stringify(value);
|
|
1681
|
+
return encoded === void 0 ? void 0 : utf8ByteLength(encoded);
|
|
1682
|
+
} catch {
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
/** Reserved global names the harness owns inside the dynamic worker. */
|
|
1687
|
+
const reservedHarnessGlobals = /* @__PURE__ */ new Set(["console"]);
|
|
1688
|
+
const passCounterState = { next: 0 };
|
|
1689
|
+
const makeExecute = (options) => Effect.fn("DynamicWorkerCodeExecutor.execute")(function* (request) {
|
|
1690
|
+
if (request.network._tag !== "NetworkDisabled") return yield* CodeExecutorUnsupportedError.make({
|
|
1691
|
+
implementation: dynamicWorkerImplementation,
|
|
1692
|
+
feature: "network",
|
|
1693
|
+
message: "The Dynamic Worker executor denies all egress with globalOutbound: null; an allowlist is not supported in the first slice"
|
|
1694
|
+
});
|
|
1695
|
+
const sourceBytes = utf8ByteLength(request.source);
|
|
1696
|
+
if (sourceBytes > request.limits.maxSourceBytes) return yield* CodeSourceError.make({
|
|
1697
|
+
implementation: dynamicWorkerImplementation,
|
|
1698
|
+
reason: "oversized",
|
|
1699
|
+
message: `Source is ${sourceBytes} bytes; the request allows ${request.limits.maxSourceBytes}`
|
|
1700
|
+
});
|
|
1701
|
+
for (const namespace of request.namespaces) if (reservedHarnessGlobals.has(namespace.name)) return yield* CodeExecutorUnsupportedError.make({
|
|
1702
|
+
implementation: dynamicWorkerImplementation,
|
|
1703
|
+
feature: "namespaces",
|
|
1704
|
+
message: `Namespace ${namespace.name} collides with a harness binding`
|
|
1705
|
+
});
|
|
1706
|
+
const host = yield* CodeExecutionHost;
|
|
1707
|
+
passCounterState.next += 1;
|
|
1708
|
+
const passId = `code-mode-pass-${passCounterState.next}-${crypto.randomUUID()}`;
|
|
1709
|
+
const pending = [];
|
|
1710
|
+
let wake;
|
|
1711
|
+
let issuedHostCalls = 0;
|
|
1712
|
+
const dispatch = (hostCall) => new Promise((resolve, reject) => {
|
|
1713
|
+
issuedHostCalls += 1;
|
|
1714
|
+
if (issuedHostCalls > request.limits.maxHostCalls + 1) {
|
|
1715
|
+
reject(/* @__PURE__ */ new Error("host-call limit exceeded"));
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
pending.push({
|
|
1719
|
+
hostCall,
|
|
1720
|
+
resolve,
|
|
1721
|
+
reject
|
|
1722
|
+
});
|
|
1723
|
+
wake?.();
|
|
1724
|
+
});
|
|
1725
|
+
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
1726
|
+
passRegistry.set(passId, { dispatch });
|
|
1727
|
+
}), () => Effect.sync(() => {
|
|
1728
|
+
passRegistry.delete(passId);
|
|
1729
|
+
}));
|
|
1730
|
+
const nextPending = Effect.suspend(() => {
|
|
1731
|
+
const item = pending.shift();
|
|
1732
|
+
if (item !== void 0) return Effect.succeed(item);
|
|
1733
|
+
return Effect.callback((resume) => {
|
|
1734
|
+
wake = () => {
|
|
1735
|
+
wake = void 0;
|
|
1736
|
+
const next = pending.shift();
|
|
1737
|
+
if (next !== void 0) resume(Effect.succeed(next));
|
|
1738
|
+
};
|
|
1739
|
+
});
|
|
1740
|
+
});
|
|
1741
|
+
const serveHostCalls = Effect.gen(function* () {
|
|
1742
|
+
let served = 0;
|
|
1743
|
+
while (true) {
|
|
1744
|
+
const item = yield* nextPending;
|
|
1745
|
+
served += 1;
|
|
1746
|
+
if (served > request.limits.maxHostCalls) return yield* CodeHostCallLimitError.make({
|
|
1747
|
+
implementation: dynamicWorkerImplementation,
|
|
1748
|
+
limit: request.limits.maxHostCalls,
|
|
1749
|
+
logs: []
|
|
1750
|
+
});
|
|
1751
|
+
const decoded = decodeHostCall(item.hostCall);
|
|
1752
|
+
if (Option.isNone(decoded)) {
|
|
1753
|
+
item.reject(/* @__PURE__ */ new TypeError("host calls must match the CodeHostCall schema"));
|
|
1754
|
+
continue;
|
|
1755
|
+
}
|
|
1756
|
+
const outcome = yield* host.call(decoded.value);
|
|
1757
|
+
if (outcome._tag === "CodeHostCallSuccess") {
|
|
1758
|
+
const bytes = encodedJsonByteLength(outcome.value);
|
|
1759
|
+
if (bytes === void 0 || bytes > request.limits.maxHostCallResultBytes) return yield* CodeOutputLimitError.make({
|
|
1760
|
+
implementation: dynamicWorkerImplementation,
|
|
1761
|
+
surface: "host-call-result",
|
|
1762
|
+
limit: request.limits.maxHostCallResultBytes,
|
|
1763
|
+
observed: bytes ?? 0,
|
|
1764
|
+
logs: []
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
item.resolve(hostResultEnvelope(outcome));
|
|
1768
|
+
}
|
|
1769
|
+
});
|
|
1770
|
+
const workerCode = {
|
|
1771
|
+
compatibilityDate: options.compatibilityDate ?? "2025-05-01",
|
|
1772
|
+
allowExperimental: true,
|
|
1773
|
+
mainModule: "harness.js",
|
|
1774
|
+
modules: {
|
|
1775
|
+
"harness.js": HARNESS_MODULE,
|
|
1776
|
+
"program.js": `export default (\n${request.source}\n);`
|
|
1777
|
+
},
|
|
1778
|
+
env: {
|
|
1779
|
+
CODE_MODE_HOST: options.hostStub,
|
|
1780
|
+
CODE_MODE_PASS: JSON.stringify({
|
|
1781
|
+
passId,
|
|
1782
|
+
namespaces: request.namespaces.map((namespace) => ({
|
|
1783
|
+
name: namespace.name,
|
|
1784
|
+
methods: namespace.methods
|
|
1785
|
+
})),
|
|
1786
|
+
limits: {
|
|
1787
|
+
maxLogBytes: request.limits.maxLogBytes,
|
|
1788
|
+
maxResultBytes: request.limits.maxResultBytes,
|
|
1789
|
+
maxHostCalls: request.limits.maxHostCalls,
|
|
1790
|
+
maxHostCallArgumentBytes: request.limits.maxHostCallArgumentBytes
|
|
1791
|
+
}
|
|
1792
|
+
})
|
|
1793
|
+
},
|
|
1794
|
+
globalOutbound: null,
|
|
1795
|
+
...request.limits.cpuMillis === void 0 ? {} : { limits: {
|
|
1796
|
+
cpuMs: request.limits.cpuMillis,
|
|
1797
|
+
subRequests: request.limits.maxHostCalls + 8
|
|
1798
|
+
} }
|
|
1799
|
+
};
|
|
1800
|
+
const startedAt = yield* Clock.currentTimeMillis;
|
|
1801
|
+
const worker = yield* Effect.acquireRelease(Effect.try({
|
|
1802
|
+
try: () => options.loader.load(workerCode),
|
|
1803
|
+
catch: (cause) => {
|
|
1804
|
+
const text = cause instanceof Error ? cause.message : String(cause);
|
|
1805
|
+
if (/syntaxerror|failed to (compile|parse)/i.test(text)) return CodeSourceError.make({
|
|
1806
|
+
implementation: dynamicWorkerImplementation,
|
|
1807
|
+
reason: "invalid",
|
|
1808
|
+
message: text.slice(0, 8e3)
|
|
1809
|
+
});
|
|
1810
|
+
return CodeExecutorStartError.make({
|
|
1811
|
+
implementation: dynamicWorkerImplementation,
|
|
1812
|
+
message: `The Worker Loader rejected the pass: ${text}`.slice(0, 8e3),
|
|
1813
|
+
cause
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
}), (stub) => Effect.sync(() => {
|
|
1817
|
+
stub[Symbol.dispose]?.();
|
|
1818
|
+
}));
|
|
1819
|
+
const server = yield* serveHostCalls.pipe(Effect.forkScoped);
|
|
1820
|
+
const rpc = Effect.tryPromise({
|
|
1821
|
+
try: async () => {
|
|
1822
|
+
return await worker.getEntrypoint().run();
|
|
1823
|
+
},
|
|
1824
|
+
catch: (cause) => classifyWorkerFailure(cause, request.limits.maxWallTime)
|
|
1825
|
+
});
|
|
1826
|
+
const raw = yield* Effect.raceFirst(rpc, Fiber.join(server)).pipe(Effect.timeoutOrElse({
|
|
1827
|
+
duration: request.limits.maxWallTime,
|
|
1828
|
+
orElse: () => CodeExecutionTimeoutError.make({
|
|
1829
|
+
implementation: dynamicWorkerImplementation,
|
|
1830
|
+
kind: "wall-clock",
|
|
1831
|
+
maxWallTime: request.limits.maxWallTime,
|
|
1832
|
+
logs: []
|
|
1833
|
+
})
|
|
1834
|
+
}), Effect.ensuring(Fiber.interrupt(server)));
|
|
1835
|
+
const finishedAt = yield* Clock.currentTimeMillis;
|
|
1836
|
+
const outcome = decodeHarnessOutcome(raw);
|
|
1837
|
+
if (Option.isNone(outcome)) return yield* CodeExecutionProtocolError.make({
|
|
1838
|
+
implementation: dynamicWorkerImplementation,
|
|
1839
|
+
message: "The dynamic worker returned a value outside the harness envelope schema"
|
|
1840
|
+
});
|
|
1841
|
+
switch (outcome.value._tag) {
|
|
1842
|
+
case "completed": return CodeExecutionResult.make({
|
|
1843
|
+
implementation: dynamicWorkerImplementation,
|
|
1844
|
+
value: outcome.value.value,
|
|
1845
|
+
logs: outcome.value.logs,
|
|
1846
|
+
resourceUse: CodeExecutionResourceUse.make({
|
|
1847
|
+
wallTime: Duration.millis(Math.max(0, finishedAt - startedAt)),
|
|
1848
|
+
hostCalls: outcome.value.hostCalls,
|
|
1849
|
+
logBytes: outcome.value.logBytes,
|
|
1850
|
+
resultBytes: outcome.value.resultBytes
|
|
1851
|
+
})
|
|
1852
|
+
});
|
|
1853
|
+
case "source-invalid": return yield* CodeSourceError.make({
|
|
1854
|
+
implementation: dynamicWorkerImplementation,
|
|
1855
|
+
reason: "invalid",
|
|
1856
|
+
message: outcome.value.message.slice(0, 8e3)
|
|
1857
|
+
});
|
|
1858
|
+
case "source-not-a-function": return yield* CodeSourceError.make({
|
|
1859
|
+
implementation: dynamicWorkerImplementation,
|
|
1860
|
+
reason: "not-a-function",
|
|
1861
|
+
message: `The source expression evaluated to ${outcome.value.actual}; it must evaluate to one async function`
|
|
1862
|
+
});
|
|
1863
|
+
case "program-failed": return yield* CodeProgramFailedError.make({
|
|
1864
|
+
implementation: dynamicWorkerImplementation,
|
|
1865
|
+
reason: outcome.value.reason,
|
|
1866
|
+
thrown: outcome.value.thrown,
|
|
1867
|
+
message: outcome.value.message.slice(0, 8e3),
|
|
1868
|
+
logs: outcome.value.logs
|
|
1869
|
+
});
|
|
1870
|
+
case "log-limit": return yield* CodeOutputLimitError.make({
|
|
1871
|
+
implementation: dynamicWorkerImplementation,
|
|
1872
|
+
surface: "logs",
|
|
1873
|
+
limit: request.limits.maxLogBytes,
|
|
1874
|
+
observed: outcome.value.observed,
|
|
1875
|
+
logs: outcome.value.logs
|
|
1876
|
+
});
|
|
1877
|
+
case "argument-limit": return yield* CodeOutputLimitError.make({
|
|
1878
|
+
implementation: dynamicWorkerImplementation,
|
|
1879
|
+
surface: "host-call-argument",
|
|
1880
|
+
limit: request.limits.maxHostCallArgumentBytes,
|
|
1881
|
+
observed: outcome.value.observed,
|
|
1882
|
+
logs: outcome.value.logs
|
|
1883
|
+
});
|
|
1884
|
+
case "result-limit": return yield* CodeOutputLimitError.make({
|
|
1885
|
+
implementation: dynamicWorkerImplementation,
|
|
1886
|
+
surface: "result",
|
|
1887
|
+
limit: request.limits.maxResultBytes,
|
|
1888
|
+
observed: outcome.value.observed,
|
|
1889
|
+
logs: outcome.value.logs
|
|
1890
|
+
});
|
|
1891
|
+
case "host-call-limit": return yield* CodeHostCallLimitError.make({
|
|
1892
|
+
implementation: dynamicWorkerImplementation,
|
|
1893
|
+
limit: request.limits.maxHostCalls,
|
|
1894
|
+
logs: outcome.value.logs
|
|
1895
|
+
});
|
|
1896
|
+
case "protocol": return yield* CodeExecutionProtocolError.make({
|
|
1897
|
+
implementation: dynamicWorkerImplementation,
|
|
1898
|
+
message: outcome.value.message.slice(0, 8e3)
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
});
|
|
1902
|
+
/**
|
|
1903
|
+
* Expected worker-level failures map into the typed union with bounded
|
|
1904
|
+
* diagnostics; anything unrecognized stays a start/termination error rather
|
|
1905
|
+
* than a fabricated program result.
|
|
1906
|
+
*/
|
|
1907
|
+
const classifyWorkerFailure = (cause, maxWallTime) => {
|
|
1908
|
+
const text = (() => {
|
|
1909
|
+
try {
|
|
1910
|
+
return cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause);
|
|
1911
|
+
} catch {
|
|
1912
|
+
return "[unserializable worker failure]";
|
|
1913
|
+
}
|
|
1914
|
+
})();
|
|
1915
|
+
if (/syntaxerror|failed to (compile|parse)/i.test(text)) return CodeSourceError.make({
|
|
1916
|
+
implementation: dynamicWorkerImplementation,
|
|
1917
|
+
reason: "invalid",
|
|
1918
|
+
message: text.slice(0, 8e3)
|
|
1919
|
+
});
|
|
1920
|
+
if (/cpu/i.test(text)) return CodeExecutionTimeoutError.make({
|
|
1921
|
+
implementation: dynamicWorkerImplementation,
|
|
1922
|
+
kind: "cpu",
|
|
1923
|
+
maxWallTime,
|
|
1924
|
+
logs: []
|
|
1925
|
+
});
|
|
1926
|
+
if (/failed to start worker/i.test(text)) return CodeExecutorStartError.make({
|
|
1927
|
+
implementation: dynamicWorkerImplementation,
|
|
1928
|
+
message: text.slice(0, 8e3),
|
|
1929
|
+
cause
|
|
1930
|
+
});
|
|
1931
|
+
return CodeExecutorTerminatedError.make({
|
|
1932
|
+
implementation: dynamicWorkerImplementation,
|
|
1933
|
+
message: text.slice(0, 8e3)
|
|
1934
|
+
});
|
|
1935
|
+
};
|
|
1936
|
+
/** Layer building the Dynamic Worker `CodeExecutor` from resolved bindings. */
|
|
1937
|
+
const dynamicWorkerCodeExecutorLayer = (options) => Layer.succeed(CodeExecutor)(CodeExecutor.of({ execute: makeExecute(options) }));
|
|
1938
|
+
//#endregion
|
|
1939
|
+
export { AbortRecorded, AdminExplainRequest, AdminFailed, AdminFailure, AdminResponse, AdminVerifyRequest, AdmissionLimitExceeded, ApprovalRecorded, CLOUDFLARE_DATABASE_CAP_BYTES, CLOUDFLARE_RUNTIME_DEFAULTS, CloudflareAdmissionLimitsValue, CloudflareBindingError, CloudflareConversationClient, CloudflareDurableRuntime, CloudflareDurableRuntimeConfig, CloudflareDurableRuntimeConfigValue, CloudflarePlatformConfigError, CloudflareRuntimeTelemetry, CloudflareTelemetryExportError, CodeModeHostEntrypoint, ConversationClientError, ConversationMaintenance, ConversationObjectIdentity, ConversationObjectNamespace, ConversationObjectPorts, DEFAULT_MAX_DATABASE_BYTES, DurableAlarmError, DurableAlarmService, DurableObjectContext, ExplainedRecovery, HostFailed, HostFailure, HostProtocolError, HostResponse, MaintenancePassReport, ObligationsScanned, ObservePageRequest, ObservedPage, RetryExecuted, SettlementReached, SubmitRequest, SubmitSucceeded, UnknownResolutionRecorded, VerifiedIntegrity, boundHostDiagnostic, cloudflareWakeSchedulerLayer, conversationNamespaceFromEnv, conversationNamespaceLayer, conversationPortTransportLayer, decodeAbortCommand, decodeAdminExplainRequest, decodeAdminResponse, decodeAdminVerifyRequest, decodeApprovalDecisionCommand, decodeHostResponse, decodeObligationThresholds, decodeObservePageRequest, decodeReceipt, decodeRetryCommand, decodeSubmitRequest, decodeUnknownResolutionCommand, dynamicWorkerCodeExecutorLayer, dynamicWorkerImplementation, encodeAbortCommand, encodeAdminResponse, encodeApprovalDecisionCommand, encodeHostResponse, encodeObservePageRequest, encodeReceipt, encodeSubmitRequest, encodeUnknownResolutionCommand, makeConversationObjectClass };
|
|
1077
1940
|
|
|
1078
1941
|
//# sourceMappingURL=index.mjs.map
|