@effect-agent/platform-cloudflare 0.0.1-beta.6 → 0.1.0-beta.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Alarm.d.mts +207 -0
- package/dist/Alarm.mjs +578 -0
- package/dist/Alarm.mjs.map +1 -0
- package/dist/BrowserRestCapture.d.mts +34 -0
- package/dist/BrowserRestCapture.mjs +238 -0
- package/dist/BrowserRestCapture.mjs.map +1 -0
- package/dist/BrowserRestCrawl.d.mts +16 -0
- package/dist/BrowserRestCrawl.mjs +244 -0
- package/dist/BrowserRestCrawl.mjs.map +1 -0
- package/dist/CloudflareAiGateway.d.mts +64 -0
- package/dist/CloudflareAiGateway.mjs +70 -0
- package/dist/CloudflareAiGateway.mjs.map +1 -0
- package/dist/CloudflareBindings.d.mts +115 -0
- package/dist/CloudflareBindings.mjs +113 -0
- package/dist/CloudflareBindings.mjs.map +1 -0
- package/dist/CloudflareBrowser-DV6kya1H.mjs +495 -0
- package/dist/CloudflareBrowser-DV6kya1H.mjs.map +1 -0
- package/dist/CloudflareBrowser-Doe1M7R5.d.mts +80 -0
- package/dist/CloudflareBrowser.d.mts +2 -0
- package/dist/CloudflareBrowser.mjs +2 -0
- package/dist/CloudflareCodeMode.d.mts +44 -0
- package/dist/CloudflareCodeMode.mjs +616 -0
- package/dist/CloudflareCodeMode.mjs.map +1 -0
- package/dist/CloudflareConfig-DKXGo8L3.d.mts +106 -0
- package/dist/CloudflareConfig.d.mts +2 -0
- package/dist/CloudflareConfig.mjs +128 -0
- package/dist/CloudflareConfig.mjs.map +1 -0
- package/dist/CloudflareMemory.d.mts +142 -0
- package/dist/CloudflareMemory.mjs +202 -0
- package/dist/CloudflareMemory.mjs.map +1 -0
- package/dist/CloudflareScheduling.d.mts +52 -0
- package/dist/CloudflareScheduling.mjs +396 -0
- package/dist/CloudflareScheduling.mjs.map +1 -0
- package/dist/CloudflareSubscriptions.d.mts +87 -0
- package/dist/CloudflareSubscriptions.mjs +530 -0
- package/dist/CloudflareSubscriptions.mjs.map +1 -0
- package/dist/CloudflareThreadClient.d.mts +1600 -0
- package/dist/CloudflareThreadClient.mjs +382 -0
- package/dist/CloudflareThreadClient.mjs.map +1 -0
- package/dist/InteractiveBrowser-BCS9g0tn.mjs +1180 -0
- package/dist/InteractiveBrowser-BCS9g0tn.mjs.map +1 -0
- package/dist/InteractiveBrowser-BR16UPbF.d.mts +169 -0
- package/dist/InteractiveBrowser.d.mts +2 -0
- package/dist/InteractiveBrowser.mjs +2 -0
- package/dist/ProtectedBrowser.d.mts +123 -0
- package/dist/ProtectedBrowser.mjs +1135 -0
- package/dist/ProtectedBrowser.mjs.map +1 -0
- package/dist/ThreadObject-CMQiW2uX.d.mts +827 -0
- package/dist/ThreadObject-DXavwaU-.mjs +853 -0
- package/dist/ThreadObject-DXavwaU-.mjs.map +1 -0
- package/dist/ThreadObject.d.mts +2 -0
- package/dist/ThreadObject.mjs +3 -0
- package/dist/WakeScheduler.d.mts +23 -0
- package/dist/WakeScheduler.mjs +57 -0
- package/dist/WakeScheduler.mjs.map +1 -0
- package/dist/boundary-BguazVkh.mjs +42 -0
- package/dist/boundary-BguazVkh.mjs.map +1 -0
- package/dist/index.d.mts +13 -1580
- package/dist/index.mjs +13 -1941
- package/dist/prepared-admission-DU0_T-ev.mjs +73 -0
- package/dist/prepared-admission-DU0_T-ev.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -52
- package/src/Alarm.ts +1270 -0
- package/src/BrowserRestCapture.ts +477 -0
- package/src/BrowserRestCrawl.ts +540 -0
- package/src/CloudflareAiGateway.ts +170 -0
- package/src/CloudflareBindings.ts +199 -0
- package/src/CloudflareBrowser.ts +15 -0
- package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +372 -204
- package/src/{config.ts → CloudflareConfig.ts} +19 -15
- package/src/CloudflareMemory.ts +420 -0
- package/src/CloudflareScheduling.ts +747 -0
- package/src/CloudflareSubscriptions.ts +1041 -0
- package/src/CloudflareThreadClient.ts +809 -0
- package/src/InteractiveBrowser.ts +2418 -0
- package/src/ProtectedBrowser.ts +9 -0
- package/src/ThreadObject.ts +1179 -0
- package/src/{wake-scheduler.ts → WakeScheduler.ts} +32 -26
- package/src/index.ts +12 -24
- package/src/internal/boundary.ts +55 -0
- package/src/internal/browser-quick-action.ts +857 -0
- package/src/internal/browser-session-lifecycle.ts +160 -0
- package/src/internal/layers.ts +756 -0
- package/src/internal/message-delivery.ts +181 -0
- package/src/internal/prepared-admission.ts +116 -0
- package/src/internal/progress-wait.ts +121 -0
- package/src/internal/transport.ts +44 -0
- package/src/protected-browser/binding.ts +313 -0
- package/src/protected-browser/host.ts +344 -0
- package/src/protected-browser/inspect-frame.ts +125 -0
- package/src/protected-browser/native.ts +445 -0
- package/src/protected-browser/policy.ts +821 -0
- package/dist/index.mjs.map +0 -1
- package/src/alarm.ts +0 -334
- package/src/bindings.ts +0 -145
- package/src/client.ts +0 -646
- package/src/conversation-object.ts +0 -847
- package/src/layers.ts +0 -385
- package/src/telemetry-internal.ts +0 -432
- package/src/telemetry.ts +0 -37
- package/src/transport.ts +0 -38
|
@@ -1,847 +0,0 @@
|
|
|
1
|
-
import type { AgentId } from "@effect-agent/core";
|
|
2
|
-
import { SubmissionId } from "@effect-agent/core";
|
|
3
|
-
import {
|
|
4
|
-
AppendConflict,
|
|
5
|
-
ConversationNotMaterialized,
|
|
6
|
-
ConversationRead,
|
|
7
|
-
ConversationStore,
|
|
8
|
-
ConversationStoreError,
|
|
9
|
-
DigestError,
|
|
10
|
-
DurableAgentRuntime,
|
|
11
|
-
DurableRuntimeFailpointError,
|
|
12
|
-
FenceRejected,
|
|
13
|
-
IntegrityReport,
|
|
14
|
-
LedgerError,
|
|
15
|
-
ObligationReport,
|
|
16
|
-
ObligationThresholds,
|
|
17
|
-
OperationAuthorizationRequest,
|
|
18
|
-
OperationAuthorizer,
|
|
19
|
-
OperationDenied,
|
|
20
|
-
OwnershipLost,
|
|
21
|
-
PersistedJson,
|
|
22
|
-
RecoveryExplanation,
|
|
23
|
-
RecoveryReport,
|
|
24
|
-
RetryCommand,
|
|
25
|
-
RetryRefused,
|
|
26
|
-
RunJournalError,
|
|
27
|
-
SettlementConflict,
|
|
28
|
-
SubmissionLedger,
|
|
29
|
-
SubmissionLookupByKey,
|
|
30
|
-
type DurableSubmitAgent,
|
|
31
|
-
} from "@effect-agent/session";
|
|
32
|
-
import { DurableObject } from "cloudflare:workers";
|
|
33
|
-
import { Effect, Layer, ManagedRuntime, Option, Schema, Stream } from "effect";
|
|
34
|
-
|
|
35
|
-
import { ConversationMaintenance, DurableAlarmError, DurableAlarmService } from "./alarm.ts";
|
|
36
|
-
import {
|
|
37
|
-
ConversationObjectIdentity,
|
|
38
|
-
DurableObjectContext,
|
|
39
|
-
conversationNamespaceLayer,
|
|
40
|
-
type CloudflareBindingError,
|
|
41
|
-
type ConversationObjectNamespace,
|
|
42
|
-
} from "./bindings.ts";
|
|
43
|
-
import {
|
|
44
|
-
AbortRecorded,
|
|
45
|
-
ApprovalRecorded,
|
|
46
|
-
HostFailed,
|
|
47
|
-
HostProtocolError,
|
|
48
|
-
ObservedPage,
|
|
49
|
-
SettlementReached,
|
|
50
|
-
SubmitSucceeded,
|
|
51
|
-
UnknownResolutionRecorded,
|
|
52
|
-
boundHostDiagnostic,
|
|
53
|
-
decodeAbortCommand,
|
|
54
|
-
decodeApprovalDecisionCommand,
|
|
55
|
-
decodeObservePageRequest,
|
|
56
|
-
decodeReceipt,
|
|
57
|
-
decodeSubmitRequest,
|
|
58
|
-
decodeUnknownResolutionCommand,
|
|
59
|
-
encodeHostResponse,
|
|
60
|
-
type HostFailure,
|
|
61
|
-
type HostResponse,
|
|
62
|
-
} from "./client.ts";
|
|
63
|
-
import { AdmissionLimitExceeded, CloudflareDurableRuntimeConfig } from "./config.ts";
|
|
64
|
-
import {
|
|
65
|
-
CloudflareDurableRuntime,
|
|
66
|
-
ConversationObjectPorts,
|
|
67
|
-
type CloudflareDurableRuntimeInitializationError,
|
|
68
|
-
type CloudflareDurableRuntimeOptions,
|
|
69
|
-
type CloudflareDurableRuntimeServices,
|
|
70
|
-
} from "./layers.ts";
|
|
71
|
-
import {
|
|
72
|
-
flushCloudflareRuntimeTelemetry,
|
|
73
|
-
logCloudflareWaitUntilRegistrationFailure,
|
|
74
|
-
makeCloudflareTelemetryFlushCoordinator,
|
|
75
|
-
registerCloudflareTelemetryAfterNativeSettlement,
|
|
76
|
-
withCloudflareNativeSpanFailure,
|
|
77
|
-
type CloudflareTelemetryFlushCoordinator,
|
|
78
|
-
} from "./telemetry-internal.ts";
|
|
79
|
-
import { CloudflareRuntimeTelemetry } from "./telemetry.ts";
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* `makeConversationObjectClass(options, telemetry?)` — the Conversation Durable Object (plan §1.4,
|
|
83
|
-
* D-P6-1): a factory returning a class that applications export from their Worker entry.
|
|
84
|
-
* One SQLite-backed Object per Conversation is the serialized owner (durability §6); the
|
|
85
|
-
* Object never runs `runResolvedWorker`'s infinite loop — each ingress event or alarm runs
|
|
86
|
-
* ONE bounded `runRecovery` + `processConversationResolved` pass, and the persisted alarm
|
|
87
|
-
* (the single multiplexed slot, D-P6-2) finishes accepted work across evictions WITHOUT any
|
|
88
|
-
* incoming request.
|
|
89
|
-
*
|
|
90
|
-
* Constructor gate (`blockConcurrencyWhile`) is LOCAL-ONLY: schema migration and the
|
|
91
|
-
* exact-version check, configuration decode, and the defensive ensure-alarm half of the
|
|
92
|
-
* alarm invariant. It deliberately does NOT run the recovery pass: parent recovery can
|
|
93
|
-
* require child-Object reads and vice versa, and two Objects blocked in constructor gates
|
|
94
|
-
* awaiting each other's RPC would deadlock (plan §1.4). Instead every pass runs
|
|
95
|
-
* `runRecovery` BEFORE any claim, so reconciliation still strictly precedes new work.
|
|
96
|
-
*/
|
|
97
|
-
|
|
98
|
-
/** Construction options for one deployed Conversation Object class. */
|
|
99
|
-
export interface ConversationObjectOptions extends CloudflareDurableRuntimeOptions {
|
|
100
|
-
/**
|
|
101
|
-
* Name of the Worker `env` binding carrying THIS class's `DurableObjectNamespace` — the
|
|
102
|
-
* Object's route back to sibling Conversation Objects for the WP2 cross-Object port calls
|
|
103
|
-
* and remote wakes (DEPLOY-010: the binding enters through a Layer, never ambiently).
|
|
104
|
-
*/
|
|
105
|
-
readonly namespaceBinding: string;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
type ConversationObjectError<TelemetryError> =
|
|
109
|
-
| CloudflareDurableRuntimeInitializationError
|
|
110
|
-
| CloudflareBindingError
|
|
111
|
-
| TelemetryError;
|
|
112
|
-
|
|
113
|
-
type EndpointServices =
|
|
114
|
-
| CloudflareDurableRuntimeServices
|
|
115
|
-
| DurableObjectContext
|
|
116
|
-
| CloudflareRuntimeTelemetry;
|
|
117
|
-
|
|
118
|
-
/** Port envelope tags whose owner-side execution durably mutates this Object's lane. */
|
|
119
|
-
const MUTATING_PORT_TAGS: ReadonlySet<string> = new Set([
|
|
120
|
-
"LedgerAdmit",
|
|
121
|
-
"LedgerMarkReady",
|
|
122
|
-
"LedgerRequestAbort",
|
|
123
|
-
"LedgerRecordChildSettled",
|
|
124
|
-
"StoreMaterialize",
|
|
125
|
-
"StoreAppend",
|
|
126
|
-
]);
|
|
127
|
-
|
|
128
|
-
const isMutatingPortRequest = (encoded: unknown): boolean =>
|
|
129
|
-
typeof encoded === "object" &&
|
|
130
|
-
encoded !== null &&
|
|
131
|
-
"_tag" in encoded &&
|
|
132
|
-
typeof encoded._tag === "string" &&
|
|
133
|
-
MUTATING_PORT_TAGS.has(encoded._tag);
|
|
134
|
-
|
|
135
|
-
/** The literal encoded `PortFailed(PortProtocolError)` fallback (same shape as WP2's). */
|
|
136
|
-
const encodedPortProtocolFailure = (message: string): unknown => ({
|
|
137
|
-
_tag: "PortFailed",
|
|
138
|
-
failure: { _tag: "PortProtocolError", message: boundHostDiagnostic(message) },
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const protocolFailure = (context: string) => (error: { readonly message: string }) =>
|
|
142
|
-
HostProtocolError.make({
|
|
143
|
-
message: boundHostDiagnostic(`${context}: ${error.message}`),
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
/** Fold one endpoint's typed failures into the uniform `HostResponse` envelope. */
|
|
147
|
-
const respond = <Result extends HostResponse, Failure extends HostFailure>(
|
|
148
|
-
effect: Effect.Effect<Result, Failure, EndpointServices>,
|
|
149
|
-
): Effect.Effect<HostResponse, never, EndpointServices> =>
|
|
150
|
-
effect.pipe(
|
|
151
|
-
Effect.map((result): HostResponse => result),
|
|
152
|
-
Effect.catch((failure) => Effect.succeed<HostResponse>(HostFailed.make({ failure }))),
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
/** Encode the response envelope; an unencodable response degrades to a protocol failure. */
|
|
156
|
-
const encodeResponse = (response: HostResponse): Effect.Effect<unknown> =>
|
|
157
|
-
encodeHostResponse(response).pipe(
|
|
158
|
-
Effect.catch((error) =>
|
|
159
|
-
Effect.succeed<unknown>({
|
|
160
|
-
_tag: "HostFailed",
|
|
161
|
-
failure: {
|
|
162
|
-
_tag: "HostProtocolError",
|
|
163
|
-
message: boundHostDiagnostic(`The host response could not be encoded: ${error.message}`),
|
|
164
|
-
},
|
|
165
|
-
}),
|
|
166
|
-
),
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
const utf8Bytes = (value: unknown): number =>
|
|
170
|
-
new TextEncoder().encode(JSON.stringify(value)).length;
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* The admission-limits gate, BEFORE `runtime.submit` touches the ledger (exit gate
|
|
174
|
-
* "resource limits are checked before admission"; DEPLOY-007). A replayed idempotency key is
|
|
175
|
-
* exempt: its accepted-work obligation already exists, and returning the original Receipt
|
|
176
|
-
* consumes no new quota. Refusals are typed `AdmissionLimitExceeded` and nothing is written.
|
|
177
|
-
*/
|
|
178
|
-
const gateAdmissionLimits = Effect.fn("ConversationObject.gateAdmissionLimits")(
|
|
179
|
-
function* (request: {
|
|
180
|
-
readonly principal: SubmissionLookupByKey["principal"];
|
|
181
|
-
readonly idempotencyKey: SubmissionLookupByKey["idempotencyKey"];
|
|
182
|
-
readonly inputPayload: PersistedJson;
|
|
183
|
-
}) {
|
|
184
|
-
const identity = yield* ConversationObjectIdentity;
|
|
185
|
-
const config = yield* CloudflareDurableRuntimeConfig;
|
|
186
|
-
const ledger = yield* SubmissionLedger;
|
|
187
|
-
const { ctx } = yield* DurableObjectContext;
|
|
188
|
-
|
|
189
|
-
const existing = yield* ledger.lookup(
|
|
190
|
-
SubmissionLookupByKey.make({
|
|
191
|
-
conversationId: identity.conversationId,
|
|
192
|
-
principal: request.principal,
|
|
193
|
-
idempotencyKey: request.idempotencyKey,
|
|
194
|
-
}),
|
|
195
|
-
);
|
|
196
|
-
if (Option.isSome(existing)) return;
|
|
197
|
-
|
|
198
|
-
const inputBytes = utf8Bytes(request.inputPayload);
|
|
199
|
-
if (inputBytes > config.limits.maxInputBytes) {
|
|
200
|
-
return yield* AdmissionLimitExceeded.make({
|
|
201
|
-
limit: "input-bytes",
|
|
202
|
-
actual: inputBytes,
|
|
203
|
-
maximum: config.limits.maxInputBytes,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// One Conversation per Object (durability §5): the local scan IS this lane's queue.
|
|
208
|
-
const nonterminal = yield* Stream.runCollect(ledger.scanNonterminal);
|
|
209
|
-
if (nonterminal.length >= config.limits.maxQueueDepthPerLane) {
|
|
210
|
-
return yield* AdmissionLimitExceeded.make({
|
|
211
|
-
limit: "queue-depth",
|
|
212
|
-
actual: nonterminal.length,
|
|
213
|
-
maximum: config.limits.maxQueueDepthPerLane,
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const databaseBytes = yield* Effect.sync(() => ctx.storage.sql.databaseSize);
|
|
218
|
-
if (databaseBytes > config.limits.maxDatabaseBytes) {
|
|
219
|
-
return yield* AdmissionLimitExceeded.make({
|
|
220
|
-
limit: "database-bytes",
|
|
221
|
-
actual: databaseBytes,
|
|
222
|
-
maximum: config.limits.maxDatabaseBytes,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* The submit-capable projection of an Agent Binding on the OBJECT side: the input arrived
|
|
230
|
-
* already encoded through the real input schema on the Worker side (`client.ts`), so the
|
|
231
|
-
* Object admits the canonical `PersistedJson` payload as-is; the resolved Binding re-derives
|
|
232
|
-
* everything else from the stored `(agentId, agentDigests)` at claim time (SUB-023).
|
|
233
|
-
*/
|
|
234
|
-
const passthroughSubmitAgent = (agentId: AgentId): DurableSubmitAgent<typeof PersistedJson> => ({
|
|
235
|
-
definition: {
|
|
236
|
-
id: agentId,
|
|
237
|
-
input: PersistedJson,
|
|
238
|
-
},
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
const submitEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
242
|
-
decodeSubmitRequest(encoded).pipe(
|
|
243
|
-
Effect.mapError(protocolFailure("The submit request could not be decoded")),
|
|
244
|
-
Effect.flatMap((request) =>
|
|
245
|
-
Effect.gen(function* () {
|
|
246
|
-
const identity = yield* ConversationObjectIdentity;
|
|
247
|
-
const maintenance = yield* ConversationMaintenance;
|
|
248
|
-
const runtime = yield* DurableAgentRuntime;
|
|
249
|
-
yield* gateAdmissionLimits(request);
|
|
250
|
-
// Alarm invariant: the alarm commits BEFORE the admission it will finish (D-P6-2).
|
|
251
|
-
yield* maintenance.preArm;
|
|
252
|
-
const receipt = yield* runtime.submit(
|
|
253
|
-
passthroughSubmitAgent(request.agentId),
|
|
254
|
-
request.inputPayload,
|
|
255
|
-
{
|
|
256
|
-
conversationId: identity.conversationId,
|
|
257
|
-
principal: request.principal,
|
|
258
|
-
idempotencyKey: request.idempotencyKey,
|
|
259
|
-
definitions: request.definitions,
|
|
260
|
-
},
|
|
261
|
-
);
|
|
262
|
-
return SubmitSucceeded.make({ receipt });
|
|
263
|
-
}),
|
|
264
|
-
),
|
|
265
|
-
respond,
|
|
266
|
-
Effect.flatMap(encodeResponse),
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
const awaitSettlementEndpoint = (
|
|
270
|
-
encoded: unknown,
|
|
271
|
-
): Effect.Effect<unknown, never, EndpointServices> =>
|
|
272
|
-
decodeReceipt(encoded).pipe(
|
|
273
|
-
Effect.mapError(protocolFailure("The receipt could not be decoded")),
|
|
274
|
-
Effect.flatMap((receipt) =>
|
|
275
|
-
Effect.gen(function* () {
|
|
276
|
-
const runtime = yield* DurableAgentRuntime;
|
|
277
|
-
const settlement = yield* runtime.awaitSettlement(receipt);
|
|
278
|
-
return SettlementReached.make({ settlement });
|
|
279
|
-
}),
|
|
280
|
-
),
|
|
281
|
-
respond,
|
|
282
|
-
Effect.flatMap(encodeResponse),
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
const observePageEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
286
|
-
decodeObservePageRequest(encoded).pipe(
|
|
287
|
-
Effect.mapError(protocolFailure("The observe request could not be decoded")),
|
|
288
|
-
Effect.flatMap((request) =>
|
|
289
|
-
Effect.gen(function* () {
|
|
290
|
-
const identity = yield* ConversationObjectIdentity;
|
|
291
|
-
const store = yield* ConversationStore;
|
|
292
|
-
// The same fail-closed authorization seam the runtime's `observe` consults (P7 WP1);
|
|
293
|
-
// the default reference preserves the possession behavior.
|
|
294
|
-
const authorizer = yield* OperationAuthorizer;
|
|
295
|
-
yield* authorizer
|
|
296
|
-
.authorize(
|
|
297
|
-
OperationAuthorizationRequest.make({
|
|
298
|
-
operation: "observe",
|
|
299
|
-
conversationId: identity.conversationId,
|
|
300
|
-
}),
|
|
301
|
-
)
|
|
302
|
-
.pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
|
|
303
|
-
const records = yield* Stream.runCollect(
|
|
304
|
-
store.read(
|
|
305
|
-
ConversationRead.make({
|
|
306
|
-
conversationId: identity.conversationId,
|
|
307
|
-
...(request.afterSequence === undefined
|
|
308
|
-
? {}
|
|
309
|
-
: { afterSequence: request.afterSequence }),
|
|
310
|
-
limit: request.limit,
|
|
311
|
-
}),
|
|
312
|
-
),
|
|
313
|
-
);
|
|
314
|
-
return ObservedPage.make({ records: [...records] });
|
|
315
|
-
}),
|
|
316
|
-
),
|
|
317
|
-
respond,
|
|
318
|
-
Effect.flatMap(encodeResponse),
|
|
319
|
-
);
|
|
320
|
-
|
|
321
|
-
const abortEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
322
|
-
decodeAbortCommand(encoded).pipe(
|
|
323
|
-
Effect.mapError(protocolFailure("The abort command could not be decoded")),
|
|
324
|
-
Effect.flatMap((command) =>
|
|
325
|
-
Effect.gen(function* () {
|
|
326
|
-
const maintenance = yield* ConversationMaintenance;
|
|
327
|
-
const runtime = yield* DurableAgentRuntime;
|
|
328
|
-
yield* maintenance.preArm;
|
|
329
|
-
const intent = yield* runtime.abort(command);
|
|
330
|
-
return AbortRecorded.make({ intent });
|
|
331
|
-
}),
|
|
332
|
-
),
|
|
333
|
-
respond,
|
|
334
|
-
Effect.flatMap(encodeResponse),
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* The pre-P7 host protocol's failure union does not carry `OperationDenied` (the Worker client
|
|
339
|
-
* predates the authorizer). This assembly always runs the default possession authorizer — no
|
|
340
|
-
* `CloudflareDurableRuntimeOptions` authorizer lever exists yet — so a denial here is
|
|
341
|
-
* unreachable today; if one ever surfaces it degrades to the protocol failure instead of an
|
|
342
|
-
* out-of-contract throw. The four P7 admin entry points below carry `OperationDenied` typed.
|
|
343
|
-
*/
|
|
344
|
-
const deniedToProtocolFailure = (
|
|
345
|
-
denied: OperationDenied,
|
|
346
|
-
): Effect.Effect<never, HostProtocolError> =>
|
|
347
|
-
Effect.fail(
|
|
348
|
-
HostProtocolError.make({
|
|
349
|
-
message: boundHostDiagnostic(
|
|
350
|
-
`The ${denied.operation} operation was denied: ${denied.reason}`,
|
|
351
|
-
),
|
|
352
|
-
}),
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
const resolveApprovalEndpoint = (
|
|
356
|
-
encoded: unknown,
|
|
357
|
-
): Effect.Effect<unknown, never, EndpointServices> =>
|
|
358
|
-
decodeApprovalDecisionCommand(encoded).pipe(
|
|
359
|
-
Effect.mapError(protocolFailure("The approval command could not be decoded")),
|
|
360
|
-
Effect.flatMap((command) =>
|
|
361
|
-
Effect.gen(function* () {
|
|
362
|
-
const maintenance = yield* ConversationMaintenance;
|
|
363
|
-
const runtime = yield* DurableAgentRuntime;
|
|
364
|
-
yield* maintenance.preArm;
|
|
365
|
-
const intent = yield* runtime
|
|
366
|
-
.resolveApproval(command)
|
|
367
|
-
.pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
|
|
368
|
-
return ApprovalRecorded.make({ intent });
|
|
369
|
-
}),
|
|
370
|
-
),
|
|
371
|
-
respond,
|
|
372
|
-
Effect.flatMap(encodeResponse),
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
const resolveUnknownEndpoint = (
|
|
376
|
-
encoded: unknown,
|
|
377
|
-
): Effect.Effect<unknown, never, EndpointServices> =>
|
|
378
|
-
decodeUnknownResolutionCommand(encoded).pipe(
|
|
379
|
-
Effect.mapError(protocolFailure("The resolution command could not be decoded")),
|
|
380
|
-
Effect.flatMap((command) =>
|
|
381
|
-
Effect.gen(function* () {
|
|
382
|
-
const maintenance = yield* ConversationMaintenance;
|
|
383
|
-
const runtime = yield* DurableAgentRuntime;
|
|
384
|
-
yield* maintenance.preArm;
|
|
385
|
-
const intent = yield* runtime
|
|
386
|
-
.resolveUnknown(command)
|
|
387
|
-
.pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
|
|
388
|
-
return UnknownResolutionRecorded.make({ intent });
|
|
389
|
-
}),
|
|
390
|
-
),
|
|
391
|
-
respond,
|
|
392
|
-
Effect.flatMap(encodeResponse),
|
|
393
|
-
);
|
|
394
|
-
|
|
395
|
-
// ---------------------------------------------------------------------------
|
|
396
|
-
// P7 administrative entry points (plan §3): explain/verify/retry/obligations over the SAME
|
|
397
|
-
// envelope discipline as the host protocol — closed request/response Schema unions, typed
|
|
398
|
-
// failures that re-decode to identical tags, protocol anomalies answered typed. The envelopes
|
|
399
|
-
// live here (not `client.ts`) because no Worker-side client consumption exists yet; `wake`
|
|
400
|
-
// already exists as the `wake()` entry point.
|
|
401
|
-
// ---------------------------------------------------------------------------
|
|
402
|
-
|
|
403
|
-
/** Explain one Submission (`submissionId` present) or every nonterminal lane member. */
|
|
404
|
-
export class AdminExplainRequest extends Schema.Class<AdminExplainRequest>(
|
|
405
|
-
"@effect-agent/platform-cloudflare/AdminExplainRequest",
|
|
406
|
-
)({
|
|
407
|
-
submissionId: Schema.optionalKey(SubmissionId),
|
|
408
|
-
}) {}
|
|
409
|
-
|
|
410
|
-
/** Verify carries no parameters — the addressed Object IS the lane. */
|
|
411
|
-
export class AdminVerifyRequest extends Schema.Class<AdminVerifyRequest>(
|
|
412
|
-
"@effect-agent/platform-cloudflare/AdminVerifyRequest",
|
|
413
|
-
)({}) {}
|
|
414
|
-
|
|
415
|
-
/** Every typed failure of the four admin entry points, plus the protocol's own errors. */
|
|
416
|
-
export const AdminFailure = Schema.Union([
|
|
417
|
-
OperationDenied,
|
|
418
|
-
RetryRefused,
|
|
419
|
-
LedgerError,
|
|
420
|
-
RunJournalError,
|
|
421
|
-
DigestError,
|
|
422
|
-
OwnershipLost,
|
|
423
|
-
SettlementConflict,
|
|
424
|
-
ConversationStoreError,
|
|
425
|
-
ConversationNotMaterialized,
|
|
426
|
-
AppendConflict,
|
|
427
|
-
FenceRejected,
|
|
428
|
-
DurableRuntimeFailpointError,
|
|
429
|
-
DurableAlarmError,
|
|
430
|
-
HostProtocolError,
|
|
431
|
-
]);
|
|
432
|
-
export type AdminFailure = typeof AdminFailure.Type;
|
|
433
|
-
|
|
434
|
-
export class ExplainedRecovery extends Schema.TaggedClass<ExplainedRecovery>(
|
|
435
|
-
"@effect-agent/platform-cloudflare/ExplainedRecovery",
|
|
436
|
-
)("ExplainedRecovery", {
|
|
437
|
-
explanations: Schema.Array(RecoveryExplanation).check(Schema.isMaxLength(1_024)),
|
|
438
|
-
}) {}
|
|
439
|
-
|
|
440
|
-
export class VerifiedIntegrity extends Schema.TaggedClass<VerifiedIntegrity>(
|
|
441
|
-
"@effect-agent/platform-cloudflare/VerifiedIntegrity",
|
|
442
|
-
)("VerifiedIntegrity", {
|
|
443
|
-
report: IntegrityReport,
|
|
444
|
-
}) {}
|
|
445
|
-
|
|
446
|
-
export class RetryExecuted extends Schema.TaggedClass<RetryExecuted>(
|
|
447
|
-
"@effect-agent/platform-cloudflare/RetryExecuted",
|
|
448
|
-
)("RetryExecuted", {
|
|
449
|
-
report: RecoveryReport,
|
|
450
|
-
}) {}
|
|
451
|
-
|
|
452
|
-
export class ObligationsScanned extends Schema.TaggedClass<ObligationsScanned>(
|
|
453
|
-
"@effect-agent/platform-cloudflare/ObligationsScanned",
|
|
454
|
-
)("ObligationsScanned", {
|
|
455
|
-
report: ObligationReport,
|
|
456
|
-
}) {}
|
|
457
|
-
|
|
458
|
-
/** The admin entry point failed TYPED on the Object; the failure re-decodes verbatim. */
|
|
459
|
-
export class AdminFailed extends Schema.TaggedClass<AdminFailed>(
|
|
460
|
-
"@effect-agent/platform-cloudflare/AdminFailed",
|
|
461
|
-
)("AdminFailed", {
|
|
462
|
-
failure: AdminFailure,
|
|
463
|
-
}) {}
|
|
464
|
-
|
|
465
|
-
/** The uniform answer of one admin entry point. Callers narrow by the tag their call implies. */
|
|
466
|
-
export const AdminResponse = Schema.Union([
|
|
467
|
-
ExplainedRecovery,
|
|
468
|
-
VerifiedIntegrity,
|
|
469
|
-
RetryExecuted,
|
|
470
|
-
ObligationsScanned,
|
|
471
|
-
AdminFailed,
|
|
472
|
-
]);
|
|
473
|
-
export type AdminResponse = typeof AdminResponse.Type;
|
|
474
|
-
|
|
475
|
-
export const decodeAdminExplainRequest = Schema.decodeUnknownEffect(AdminExplainRequest);
|
|
476
|
-
export const decodeAdminVerifyRequest = Schema.decodeUnknownEffect(AdminVerifyRequest);
|
|
477
|
-
export const decodeRetryCommand = Schema.decodeUnknownEffect(RetryCommand);
|
|
478
|
-
export const decodeObligationThresholds = Schema.decodeUnknownEffect(ObligationThresholds);
|
|
479
|
-
export const encodeAdminResponse = Schema.encodeEffect(AdminResponse);
|
|
480
|
-
export const decodeAdminResponse = Schema.decodeUnknownEffect(AdminResponse);
|
|
481
|
-
|
|
482
|
-
/** Fold one admin endpoint's typed failures into the uniform `AdminResponse` envelope. */
|
|
483
|
-
const respondAdmin = <Result extends AdminResponse, Failure extends AdminFailure>(
|
|
484
|
-
effect: Effect.Effect<Result, Failure, EndpointServices>,
|
|
485
|
-
): Effect.Effect<AdminResponse, never, EndpointServices> =>
|
|
486
|
-
effect.pipe(
|
|
487
|
-
Effect.map((result): AdminResponse => result),
|
|
488
|
-
Effect.catch((failure) => Effect.succeed<AdminResponse>(AdminFailed.make({ failure }))),
|
|
489
|
-
);
|
|
490
|
-
|
|
491
|
-
/** Encode the admin response envelope; an unencodable response degrades to a protocol failure. */
|
|
492
|
-
const encodeAdminResponseTotal = (response: AdminResponse): Effect.Effect<unknown> =>
|
|
493
|
-
encodeAdminResponse(response).pipe(
|
|
494
|
-
Effect.catch((error) =>
|
|
495
|
-
Effect.succeed<unknown>({
|
|
496
|
-
_tag: "AdminFailed",
|
|
497
|
-
failure: {
|
|
498
|
-
_tag: "HostProtocolError",
|
|
499
|
-
message: boundHostDiagnostic(`The admin response could not be encoded: ${error.message}`),
|
|
500
|
-
},
|
|
501
|
-
}),
|
|
502
|
-
),
|
|
503
|
-
);
|
|
504
|
-
|
|
505
|
-
const explainEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
506
|
-
decodeAdminExplainRequest(encoded).pipe(
|
|
507
|
-
Effect.mapError(protocolFailure("The explain request could not be decoded")),
|
|
508
|
-
Effect.flatMap((request) =>
|
|
509
|
-
Effect.gen(function* () {
|
|
510
|
-
const identity = yield* ConversationObjectIdentity;
|
|
511
|
-
const runtime = yield* DurableAgentRuntime;
|
|
512
|
-
const explanations =
|
|
513
|
-
request.submissionId === undefined
|
|
514
|
-
? yield* runtime.explainConversation(identity.conversationId)
|
|
515
|
-
: [yield* runtime.explain(request.submissionId)];
|
|
516
|
-
return ExplainedRecovery.make({ explanations });
|
|
517
|
-
}),
|
|
518
|
-
),
|
|
519
|
-
respondAdmin,
|
|
520
|
-
Effect.flatMap(encodeAdminResponseTotal),
|
|
521
|
-
);
|
|
522
|
-
|
|
523
|
-
const verifyEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
524
|
-
decodeAdminVerifyRequest(encoded).pipe(
|
|
525
|
-
Effect.mapError(protocolFailure("The verify request could not be decoded")),
|
|
526
|
-
Effect.flatMap(() =>
|
|
527
|
-
Effect.gen(function* () {
|
|
528
|
-
const identity = yield* ConversationObjectIdentity;
|
|
529
|
-
const runtime = yield* DurableAgentRuntime;
|
|
530
|
-
const report = yield* runtime.verify(identity.conversationId);
|
|
531
|
-
return VerifiedIntegrity.make({ report });
|
|
532
|
-
}),
|
|
533
|
-
),
|
|
534
|
-
respondAdmin,
|
|
535
|
-
Effect.flatMap(encodeAdminResponseTotal),
|
|
536
|
-
);
|
|
537
|
-
|
|
538
|
-
const retryEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
539
|
-
decodeRetryCommand(encoded).pipe(
|
|
540
|
-
Effect.mapError(protocolFailure("The retry command could not be decoded")),
|
|
541
|
-
Effect.flatMap((command) =>
|
|
542
|
-
Effect.gen(function* () {
|
|
543
|
-
const maintenance = yield* ConversationMaintenance;
|
|
544
|
-
const runtime = yield* DurableAgentRuntime;
|
|
545
|
-
// Alarm invariant: retry may repair durable state, so the alarm that will finish the
|
|
546
|
-
// lane commits BEFORE the mutation (D-P6-2), exactly like abort.
|
|
547
|
-
yield* maintenance.preArm;
|
|
548
|
-
const report = yield* runtime.retry(command);
|
|
549
|
-
return RetryExecuted.make({ report });
|
|
550
|
-
}),
|
|
551
|
-
),
|
|
552
|
-
respondAdmin,
|
|
553
|
-
Effect.flatMap(encodeAdminResponseTotal),
|
|
554
|
-
);
|
|
555
|
-
|
|
556
|
-
const obligationsEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
557
|
-
decodeObligationThresholds(encoded).pipe(
|
|
558
|
-
Effect.mapError(protocolFailure("The obligation thresholds could not be decoded")),
|
|
559
|
-
Effect.flatMap((thresholds) =>
|
|
560
|
-
Effect.gen(function* () {
|
|
561
|
-
const runtime = yield* DurableAgentRuntime;
|
|
562
|
-
const report = yield* runtime.scanObligations(thresholds);
|
|
563
|
-
return ObligationsScanned.make({ report });
|
|
564
|
-
}),
|
|
565
|
-
),
|
|
566
|
-
respondAdmin,
|
|
567
|
-
Effect.flatMap(encodeAdminResponseTotal),
|
|
568
|
-
);
|
|
569
|
-
|
|
570
|
-
/**
|
|
571
|
-
* Owner-side `portCall`: pre-arm before a mutating envelope (a routed admission committed by
|
|
572
|
-
* THIS Object must already carry the alarm that will finish it), execute on the LOCAL facets
|
|
573
|
-
* (never the routed decorators), then arm an immediate alarm so the mutated lane is
|
|
574
|
-
* processed promptly. Protocol anomalies answer `PortFailed(PortProtocolError)`.
|
|
575
|
-
*/
|
|
576
|
-
const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
|
|
577
|
-
Effect.gen(function* () {
|
|
578
|
-
const ports = yield* ConversationObjectPorts;
|
|
579
|
-
const maintenance = yield* ConversationMaintenance;
|
|
580
|
-
const alarm = yield* DurableAlarmService;
|
|
581
|
-
const mutating = isMutatingPortRequest(encoded);
|
|
582
|
-
if (mutating) {
|
|
583
|
-
const preArmed = yield* maintenance.preArm.pipe(Effect.exit);
|
|
584
|
-
if (preArmed._tag === "Failure") {
|
|
585
|
-
// Without the committed alarm the invariant cannot be promised; refuse the mutation.
|
|
586
|
-
return encodedPortProtocolFailure(
|
|
587
|
-
"The owner Object could not arm its maintenance alarm before the mutation.",
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
const response = yield* ports.handle(encoded);
|
|
592
|
-
if (mutating) {
|
|
593
|
-
// Prompt processing hint; the pre-armed alarm already guarantees convergence.
|
|
594
|
-
yield* alarm.scheduleNow.pipe(
|
|
595
|
-
Effect.catch((error) =>
|
|
596
|
-
Effect.logWarning("ConversationObject.portCall: immediate re-arm failed", error),
|
|
597
|
-
),
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
return response;
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
const wakeEndpoint: Effect.Effect<void, never, EndpointServices> = Effect.gen(function* () {
|
|
604
|
-
const alarm = yield* DurableAlarmService;
|
|
605
|
-
// Wake hints are droppable by contract: a failed alarm write is logged and swallowed; the
|
|
606
|
-
// sender's own alarm/scan pairing (or this Object's next entry point) restores liveness.
|
|
607
|
-
yield* alarm.scheduleNow.pipe(
|
|
608
|
-
Effect.catch((error) => Effect.logWarning("ConversationObject.wake dropped", error)),
|
|
609
|
-
);
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
const alarmEndpoint: Effect.Effect<void, unknown, EndpointServices> = Effect.gen(function* () {
|
|
613
|
-
const maintenance = yield* ConversationMaintenance;
|
|
614
|
-
// Typed pass failures propagate: the rejected promise makes workerd retry the alarm
|
|
615
|
-
// (at-least-once delivery), and the pass's own pre-arm keeps the slot committed meanwhile.
|
|
616
|
-
yield* maintenance.pass;
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
const gateEndpoint: Effect.Effect<void, unknown, EndpointServices> = Effect.gen(function* () {
|
|
620
|
-
// Forcing ConversationMaintenance forces the whole Layer stack: migration + exact-version
|
|
621
|
-
// check + configuration decode (DEPLOY-008 fails typed here, before any mutation), then
|
|
622
|
-
// the defensive local ensure-alarm half of the invariant. LOCAL-ONLY by construction.
|
|
623
|
-
const maintenance = yield* ConversationMaintenance;
|
|
624
|
-
yield* maintenance.ensureAlarm;
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
const NATIVE_ENTRYPOINTS = [
|
|
628
|
-
"submit",
|
|
629
|
-
"await_settlement",
|
|
630
|
-
"observe",
|
|
631
|
-
"abort",
|
|
632
|
-
"resolve_approval",
|
|
633
|
-
"resolve_unknown",
|
|
634
|
-
"explain",
|
|
635
|
-
"verify",
|
|
636
|
-
"retry",
|
|
637
|
-
"obligations",
|
|
638
|
-
"port_call",
|
|
639
|
-
"wake",
|
|
640
|
-
"alarm",
|
|
641
|
-
] as const;
|
|
642
|
-
|
|
643
|
-
type NativeEntrypoint = (typeof NATIVE_ENTRYPOINTS)[number];
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* Owner-side delivery measurement. The exact endpoint Cause is hidden behind a bounded typed
|
|
647
|
-
* marker while the span closes, then restored together with any newly composed real reasons.
|
|
648
|
-
*/
|
|
649
|
-
const observeNativeEntrypoint = <A, E>(
|
|
650
|
-
entrypoint: NativeEntrypoint,
|
|
651
|
-
effect: Effect.Effect<A, E, EndpointServices>,
|
|
652
|
-
): Effect.Effect<A, E, EndpointServices> =>
|
|
653
|
-
Effect.gen(function* () {
|
|
654
|
-
const identity = yield* ConversationObjectIdentity;
|
|
655
|
-
const config = yield* CloudflareDurableRuntimeConfig;
|
|
656
|
-
return yield* withCloudflareNativeSpanFailure(effect, (masked) =>
|
|
657
|
-
masked.pipe(
|
|
658
|
-
Effect.withSpan(`effect_agent.cloudflare.conversation_object.${entrypoint}`, {
|
|
659
|
-
kind: "server",
|
|
660
|
-
attributes: {
|
|
661
|
-
"effect_agent.cloudflare.entrypoint": entrypoint,
|
|
662
|
-
"effect_agent.deployment.id": config.deploymentId,
|
|
663
|
-
conversationId: identity.conversationId,
|
|
664
|
-
producerId: identity.producerId,
|
|
665
|
-
},
|
|
666
|
-
}),
|
|
667
|
-
),
|
|
668
|
-
);
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
const flushNativeEntrypointTelemetry = Effect.flatMap(CloudflareDurableRuntimeConfig, (config) =>
|
|
672
|
-
flushCloudflareRuntimeTelemetry(config.telemetryFlushTimeout),
|
|
673
|
-
);
|
|
674
|
-
|
|
675
|
-
/** The public endpoint surface of one Conversation Object instance. */
|
|
676
|
-
export interface ConversationObjectInstance extends DurableObject {
|
|
677
|
-
submitEncoded(encoded: unknown): Promise<unknown>;
|
|
678
|
-
awaitSettlementEncoded(encoded: unknown): Promise<unknown>;
|
|
679
|
-
observePage(encoded: unknown): Promise<unknown>;
|
|
680
|
-
abortEncoded(encoded: unknown): Promise<unknown>;
|
|
681
|
-
resolveApprovalEncoded(encoded: unknown): Promise<unknown>;
|
|
682
|
-
resolveUnknownEncoded(encoded: unknown): Promise<unknown>;
|
|
683
|
-
explainEncoded(encoded: unknown): Promise<unknown>;
|
|
684
|
-
verifyEncoded(encoded: unknown): Promise<unknown>;
|
|
685
|
-
retryEncoded(encoded: unknown): Promise<unknown>;
|
|
686
|
-
obligationsEncoded(encoded: unknown): Promise<unknown>;
|
|
687
|
-
portCall(encoded: unknown): Promise<unknown>;
|
|
688
|
-
wake(): Promise<void>;
|
|
689
|
-
alarm(): Promise<void>;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/** The constructor shape workerd instantiates for each Conversation Object. */
|
|
693
|
-
export interface ConversationObjectClass {
|
|
694
|
-
new (ctx: DurableObjectState, env: Cloudflare.Env): ConversationObjectInstance;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/**
|
|
698
|
-
* Build the application's Conversation Object class (export it from the Worker entry).
|
|
699
|
-
* `telemetry` is the host composition boundary: it may require the Object context/namespace and
|
|
700
|
-
* retain a typed acquisition error, while any additional output services remain available to the
|
|
701
|
-
* cached runtime. `TelemetryError` stays the first explicit generic for source compatibility;
|
|
702
|
-
* additional outputs infer from the Layer. Omitting it installs the content-free no-op service.
|
|
703
|
-
* The explicit return type is what makes declaration emit possible: the class body carries a
|
|
704
|
-
* private runtime field, and TS4094 rejects inferring an exported anonymous class type around it.
|
|
705
|
-
*/
|
|
706
|
-
export const makeConversationObjectClass = <
|
|
707
|
-
TelemetryError = never,
|
|
708
|
-
AdditionalTelemetryOutputs = never,
|
|
709
|
-
>(
|
|
710
|
-
options: ConversationObjectOptions,
|
|
711
|
-
telemetry?: Layer.Layer<
|
|
712
|
-
CloudflareRuntimeTelemetry | AdditionalTelemetryOutputs,
|
|
713
|
-
TelemetryError,
|
|
714
|
-
DurableObjectContext | ConversationObjectNamespace
|
|
715
|
-
>,
|
|
716
|
-
): ConversationObjectClass => {
|
|
717
|
-
class ConversationObject extends DurableObject {
|
|
718
|
-
readonly #runtime: ManagedRuntime.ManagedRuntime<
|
|
719
|
-
EndpointServices,
|
|
720
|
-
ConversationObjectError<TelemetryError>
|
|
721
|
-
>;
|
|
722
|
-
readonly #ctx: DurableObjectState;
|
|
723
|
-
readonly #telemetryFlush: CloudflareTelemetryFlushCoordinator;
|
|
724
|
-
|
|
725
|
-
constructor(ctx: DurableObjectState, env: Cloudflare.Env) {
|
|
726
|
-
super(ctx, env);
|
|
727
|
-
this.#ctx = ctx;
|
|
728
|
-
const platform = Layer.mergeAll(
|
|
729
|
-
DurableObjectContext.layer(ctx, env),
|
|
730
|
-
conversationNamespaceLayer(env, options.namespaceBinding),
|
|
731
|
-
);
|
|
732
|
-
// Host observability is composed at the Worker edge, not hidden in runtime options. Its
|
|
733
|
-
// typed acquisition failure remains in the ManagedRuntime error, while the Object context
|
|
734
|
-
// and namespace are available to Layers that derive exporter configuration from `env`.
|
|
735
|
-
const hostTelemetry = (telemetry ?? CloudflareRuntimeTelemetry.layerNoop).pipe(
|
|
736
|
-
Layer.provideMerge(platform),
|
|
737
|
-
);
|
|
738
|
-
this.#runtime = ManagedRuntime.make(
|
|
739
|
-
CloudflareDurableRuntime.layer(options).pipe(
|
|
740
|
-
// Supplying the host Layer outermost makes its Logger/Tracer/Metric runtime
|
|
741
|
-
// configuration observe application Layer acquisition and every native endpoint.
|
|
742
|
-
Layer.provideMerge(hostTelemetry),
|
|
743
|
-
),
|
|
744
|
-
);
|
|
745
|
-
this.#telemetryFlush = makeCloudflareTelemetryFlushCoordinator(
|
|
746
|
-
() => this.#runtime.runPromise(flushNativeEntrypointTelemetry),
|
|
747
|
-
{
|
|
748
|
-
onReservationDropped: () => {
|
|
749
|
-
void this.#runtime.runSyncExit(
|
|
750
|
-
Effect.logWarning(
|
|
751
|
-
"Cloudflare telemetry delivery coalesced at the batch reservation limit",
|
|
752
|
-
).pipe(
|
|
753
|
-
Effect.annotateLogs({
|
|
754
|
-
"effect_agent.cloudflare.telemetry.failure_kind": "reservation_limit",
|
|
755
|
-
}),
|
|
756
|
-
),
|
|
757
|
-
);
|
|
758
|
-
},
|
|
759
|
-
},
|
|
760
|
-
);
|
|
761
|
-
// The constructor gate: local-only checks; never the recovery pass (deadlock argument,
|
|
762
|
-
// plan §1.4). A failure here fails every delivery with the typed construction error.
|
|
763
|
-
ctx.blockConcurrencyWhile(() => this.#runtime.runPromise(gateEndpoint));
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
#runNative<A, E>(
|
|
767
|
-
entrypoint: NativeEntrypoint,
|
|
768
|
-
effect: Effect.Effect<A, E, EndpointServices>,
|
|
769
|
-
): Promise<A> {
|
|
770
|
-
const delivery = this.#runtime.runPromise(observeNativeEntrypoint(entrypoint, effect));
|
|
771
|
-
// Reserve synchronously with the current native delivery, but do not await export from the
|
|
772
|
-
// RPC/alarm Promise. Each pending/trailing/queued batch waits for its assigned deliveries to
|
|
773
|
-
// settle and only its first owner registers the shared background Promise. Even an
|
|
774
|
-
// uninterruptible exporter therefore cannot delay a caller, suppress alarm retry, accumulate
|
|
775
|
-
// concurrent exporter work, an unbounded waitUntil registration queue, or unbounded retained
|
|
776
|
-
// delivery Promises. Excess arrivals are diagnosed and lossy-coalesced at the batch cap.
|
|
777
|
-
// Registration failure and every asynchronous flush failure remain isolated from delivery.
|
|
778
|
-
return registerCloudflareTelemetryAfterNativeSettlement(
|
|
779
|
-
(background) => this.#ctx.waitUntil(background),
|
|
780
|
-
delivery,
|
|
781
|
-
this.#telemetryFlush.reserve,
|
|
782
|
-
(cause) => {
|
|
783
|
-
// Native entrypoints run only after blockConcurrencyWhile has built the cached runtime.
|
|
784
|
-
// Effect Logger invocation is synchronous; runSyncExit captures a broken logger without
|
|
785
|
-
// starting an unscoped fiber or changing the delivery Promise. The exact platform value
|
|
786
|
-
// stays confined to this registration callback; the automatic Logger diagnostic is
|
|
787
|
-
// deliberately content-free.
|
|
788
|
-
void this.#runtime.runSyncExit(logCloudflareWaitUntilRegistrationFailure(cause));
|
|
789
|
-
},
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
async submitEncoded(encoded: unknown): Promise<unknown> {
|
|
794
|
-
return this.#runNative("submit", submitEndpoint(encoded));
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
async awaitSettlementEncoded(encoded: unknown): Promise<unknown> {
|
|
798
|
-
return this.#runNative("await_settlement", awaitSettlementEndpoint(encoded));
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
async observePage(encoded: unknown): Promise<unknown> {
|
|
802
|
-
return this.#runNative("observe", observePageEndpoint(encoded));
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
async abortEncoded(encoded: unknown): Promise<unknown> {
|
|
806
|
-
return this.#runNative("abort", abortEndpoint(encoded));
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
async resolveApprovalEncoded(encoded: unknown): Promise<unknown> {
|
|
810
|
-
return this.#runNative("resolve_approval", resolveApprovalEndpoint(encoded));
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
async resolveUnknownEncoded(encoded: unknown): Promise<unknown> {
|
|
814
|
-
return this.#runNative("resolve_unknown", resolveUnknownEndpoint(encoded));
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
async explainEncoded(encoded: unknown): Promise<unknown> {
|
|
818
|
-
return this.#runNative("explain", explainEndpoint(encoded));
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
async verifyEncoded(encoded: unknown): Promise<unknown> {
|
|
822
|
-
return this.#runNative("verify", verifyEndpoint(encoded));
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
async retryEncoded(encoded: unknown): Promise<unknown> {
|
|
826
|
-
return this.#runNative("retry", retryEndpoint(encoded));
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
async obligationsEncoded(encoded: unknown): Promise<unknown> {
|
|
830
|
-
return this.#runNative("obligations", obligationsEndpoint(encoded));
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
async portCall(encoded: unknown): Promise<unknown> {
|
|
834
|
-
return this.#runNative("port_call", portCallEndpoint(encoded));
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
async wake(): Promise<void> {
|
|
838
|
-
await this.#runNative("wake", wakeEndpoint);
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
override async alarm(): Promise<void> {
|
|
842
|
-
await this.#runNative("alarm", alarmEndpoint);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
return ConversationObject;
|
|
847
|
-
};
|