@effect-agent/platform-cloudflare 0.1.0-beta.13 → 0.1.0-beta.15

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.
@@ -246,17 +246,15 @@ const submitEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoin
246
246
  const maintenance = yield* ConversationMaintenance;
247
247
  const runtime = yield* DurableAgentRuntime;
248
248
  yield* gateAdmissionLimits(request);
249
- // Alarm invariant: the alarm commits BEFORE the admission it will finish (D-P6-2).
250
- yield* maintenance.preArm;
251
- const receipt = yield* runtime.submit(
252
- passthroughSubmitAgent(request.agentId),
253
- request.inputPayload,
254
- {
249
+ // Alarm invariant: the generation + alarm commit BEFORE the admission, and maintenance
250
+ // cannot acknowledge that generation until this mutation leaves its public RPC seam.
251
+ const receipt = yield* maintenance.withMutation(
252
+ runtime.submit(passthroughSubmitAgent(request.agentId), request.inputPayload, {
255
253
  conversationId: identity.conversationId,
256
254
  principal: request.principal,
257
255
  idempotencyKey: request.idempotencyKey,
258
256
  definitions: request.definitions,
259
- },
257
+ }),
260
258
  );
261
259
  return SubmitSucceeded.make({ receipt });
262
260
  }),
@@ -324,8 +322,7 @@ const abortEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoint
324
322
  Effect.gen(function* () {
325
323
  const maintenance = yield* ConversationMaintenance;
326
324
  const runtime = yield* DurableAgentRuntime;
327
- yield* maintenance.preArm;
328
- const intent = yield* runtime.abort(command);
325
+ const intent = yield* maintenance.withMutation(runtime.abort(command));
329
326
  return AbortRecorded.make({ intent });
330
327
  }),
331
328
  ),
@@ -360,10 +357,11 @@ const resolveApprovalEndpoint = (
360
357
  Effect.gen(function* () {
361
358
  const maintenance = yield* ConversationMaintenance;
362
359
  const runtime = yield* DurableAgentRuntime;
363
- yield* maintenance.preArm;
364
- const intent = yield* runtime
365
- .resolveApproval(command)
366
- .pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
360
+ const intent = yield* maintenance.withMutation(
361
+ runtime
362
+ .resolveApproval(command)
363
+ .pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure)),
364
+ );
367
365
  return ApprovalRecorded.make({ intent });
368
366
  }),
369
367
  ),
@@ -380,10 +378,11 @@ const resolveUnknownEndpoint = (
380
378
  Effect.gen(function* () {
381
379
  const maintenance = yield* ConversationMaintenance;
382
380
  const runtime = yield* DurableAgentRuntime;
383
- yield* maintenance.preArm;
384
- const intent = yield* runtime
385
- .resolveUnknown(command)
386
- .pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure));
381
+ const intent = yield* maintenance.withMutation(
382
+ runtime
383
+ .resolveUnknown(command)
384
+ .pipe(Effect.catchTag("OperationDenied", deniedToProtocolFailure)),
385
+ );
387
386
  return UnknownResolutionRecorded.make({ intent });
388
387
  }),
389
388
  ),
@@ -541,10 +540,8 @@ const retryEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoint
541
540
  Effect.gen(function* () {
542
541
  const maintenance = yield* ConversationMaintenance;
543
542
  const runtime = yield* DurableAgentRuntime;
544
- // Alarm invariant: retry may repair durable state, so the alarm that will finish the
545
- // lane commits BEFORE the mutation (D-P6-2), exactly like abort.
546
- yield* maintenance.preArm;
547
- const report = yield* runtime.retry(command);
543
+ // Retry may repair durable state, so its generation + alarm commit before the mutation.
544
+ const report = yield* maintenance.withMutation(runtime.retry(command));
548
545
  return RetryExecuted.make({ report });
549
546
  }),
550
547
  ),
@@ -567,10 +564,11 @@ const obligationsEndpoint = (encoded: unknown): Effect.Effect<unknown, never, En
567
564
  );
568
565
 
569
566
  /**
570
- * Owner-side `portCall`: pre-arm before a mutating envelope (a routed admission committed by
571
- * THIS Object must already carry the alarm that will finish it), execute on the LOCAL facets
572
- * (never the routed decorators), then arm an immediate alarm so the mutated lane is
573
- * processed promptly. Protocol anomalies answer `PortFailed(PortProtocolError)`.
567
+ * Owner-side `portCall`: wrap a mutating envelope in the same pre-armed generation protocol as
568
+ * public RPC (a routed mutation committed by THIS Object must already carry the alarm that will
569
+ * finish it), execute on the LOCAL facets (never the routed decorators), then arm an immediate
570
+ * alarm so the mutated lane is processed promptly. Protocol anomalies answer
571
+ * `PortFailed(PortProtocolError)`.
574
572
  */
575
573
  const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, EndpointServices> =>
576
574
  Effect.gen(function* () {
@@ -578,16 +576,17 @@ const portCallEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpo
578
576
  const maintenance = yield* ConversationMaintenance;
579
577
  const alarm = yield* DurableAlarmService;
580
578
  const mutating = isMutatingPortRequest(encoded);
581
- if (mutating) {
582
- const preArmed = yield* maintenance.preArm.pipe(Effect.exit);
583
- if (preArmed._tag === "Failure") {
584
- // Without the committed alarm the invariant cannot be promised; refuse the mutation.
585
- return encodedPortProtocolFailure(
586
- "The owner Object could not arm its maintenance alarm before the mutation.",
587
- );
588
- }
579
+ const handled = yield* (
580
+ mutating ? maintenance.withMutation(ports.handle(encoded)) : ports.handle(encoded)
581
+ ).pipe(Effect.exit);
582
+ if (handled._tag === "Failure") {
583
+ // Without the committed generation/alarm the invariant cannot be promised; refuse before
584
+ // the port mutation runs. `ports.handle` itself is total, so this is the maintenance error.
585
+ return encodedPortProtocolFailure(
586
+ "The owner Object could not arm its maintenance alarm before the mutation.",
587
+ );
589
588
  }
590
- const response = yield* ports.handle(encoded);
589
+ const response = handled.value;
591
590
  if (mutating) {
592
591
  // Prompt processing hint; the pre-armed alarm already guarantees convergence.
593
592
  yield* alarm.scheduleNow.pipe(
@@ -612,7 +611,7 @@ const alarmEndpoint: Effect.Effect<void, MaintenancePassFailure, EndpointService
612
611
  function* () {
613
612
  const maintenance = yield* ConversationMaintenance;
614
613
  // 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.
614
+ // (at-least-once delivery), and the dirty generation retains a committed slot meanwhile.
616
615
  yield* maintenance.pass;
617
616
  },
618
617
  );
package/src/index.ts CHANGED
@@ -6,8 +6,9 @@
6
6
  * `makeConversationObjectClass` builds the class applications export from their Worker,
7
7
  * `CloudflareDurableRuntime.layer` assembles the coordinator over the storage-cloudflare
8
8
  * adapters and the WP2 cross-Object routing, `DurableAlarmService`/`ConversationMaintenance`
9
- * multiplex every cadence into the Object's single alarm slot (nonterminal work implies a
10
- * committed alarm, so eviction recovers without an incoming request), and
9
+ * multiplex every cadence into the Object's single alarm slot (dirty or autonomously
10
+ * actionable work retains a committed alarm; stable external waits quiesce until their next
11
+ * durably pre-armed mutation), and
11
12
  * `CloudflareConversationClient` is the Worker-side ingress. Platform bindings enter ONLY
12
13
  * through the `bindings.ts` Layers (DEPLOY-010). This is the only workspace package allowed
13
14
  * to import the `cloudflare:workers` runtime module.
package/src/layers.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import { ConversationId } from "@effect-agent/core";
2
2
  import {
3
3
  AgentBindingResolver,
4
- ConversationStore,
5
4
  DurableAgentRuntime,
6
5
  DurableRuntimeConfig,
7
6
  DurableRuntimeFailpoint,
8
7
  ProducerId,
9
- SubmissionLedger,
10
8
  ToolReconciler,
11
- WakeScheduler,
9
+ type ConversationStore,
12
10
  type DurableRuntimeFailpointHandler,
13
11
  type ResolvedBinding,
12
+ type SubmissionLedger,
13
+ type WakeScheduler,
14
14
  } from "@effect-agent/session";
15
15
  import {
16
16
  conversationStoreLayer,
@@ -28,11 +28,16 @@ import { BrowserCrypto } from "@effect/platform-browser";
28
28
  import { SqliteClient } from "@effect/sql-sqlite-do";
29
29
  import { Context, Duration, Effect, Layer, Schema } from "effect";
30
30
 
31
- import { ConversationMaintenance, DurableAlarmService } from "./alarm.ts";
31
+ import {
32
+ ConversationMaintenance,
33
+ ConversationMaintenanceFailpoint,
34
+ DurableAlarmService,
35
+ type ConversationMaintenanceFailpointHandler,
36
+ } from "./alarm.ts";
32
37
  import {
33
38
  ConversationObjectIdentity,
34
- ConversationObjectNamespace,
35
39
  DurableObjectContext,
40
+ type ConversationObjectNamespace,
36
41
  } from "./bindings.ts";
37
42
  import {
38
43
  CLOUDFLARE_RUNTIME_DEFAULTS,
@@ -89,6 +94,10 @@ export interface CloudflareDurableRuntimeOptions {
89
94
  readonly runtimeFailpoint?:
90
95
  | ((ctx: DurableObjectState) => DurableRuntimeFailpointHandler)
91
96
  | undefined;
97
+ /** Conversation-maintenance generation/alarm fault injection; default none. */
98
+ readonly maintenanceFailpoint?:
99
+ | ((ctx: DurableObjectState) => ConversationMaintenanceFailpointHandler)
100
+ | undefined;
92
101
  /**
93
102
  * Reconciliation policy consulted for open ordinary Tool Calls before an Unknown Outcome
94
103
  * is recorded (durability §10, DUR-009). Defaults to the fail-closed
@@ -340,6 +349,12 @@ export class CloudflareDurableRuntime {
340
349
  options.runtimeFailpoint === undefined
341
350
  ? DurableRuntimeFailpoint.layer
342
351
  : Layer.succeed(DurableRuntimeFailpoint)({ hit: options.runtimeFailpoint(ctx) });
352
+ const maintenanceFailpointLayer =
353
+ options.maintenanceFailpoint === undefined
354
+ ? ConversationMaintenanceFailpoint.layer
355
+ : Layer.succeed(ConversationMaintenanceFailpoint)({
356
+ hit: options.maintenanceFailpoint(ctx),
357
+ });
343
358
  const reconcilerLayer = options.toolReconciler ?? ToolReconciler.uncertain;
344
359
  const bindingResolverLayer = Layer.effect(AgentBindingResolver)(
345
360
  Effect.map(
@@ -352,6 +367,7 @@ export class CloudflareDurableRuntime {
352
367
  identityLayer,
353
368
  cloudflareConfigLayer,
354
369
  DurableAlarmService.layer,
370
+ maintenanceFailpointLayer,
355
371
  );
356
372
 
357
373
  const runtimeStack = DurableAgentRuntime.layer.pipe(