@effect-agent/platform-cloudflare 0.1.0-beta.50 → 0.1.0-beta.51

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.
@@ -13,6 +13,7 @@ import {
13
13
  type SubscriptionAuthorizer,
14
14
  SubscriptionConfiguration,
15
15
  SubscriptionDeliverySnapshot,
16
+ SubscriptionDeliveryKey,
16
17
  SubscriptionError,
17
18
  SubscriptionFailpointError,
18
19
  SubscriptionKey,
@@ -30,7 +31,7 @@ import {
30
31
  } from "@effect-agent/thread/Subscriptions";
31
32
  import { BrowserCrypto } from "@effect/platform-browser";
32
33
  import { SqliteClient } from "@effect/sql-sqlite-do";
33
- import { Cause, Clock, Context, DateTime, Effect, Layer, Schema } from "effect";
34
+ import { Cause, Clock, Context, DateTime, Effect, Layer, Schema, type Scope } from "effect";
34
35
  import {
35
36
  DurableObject as EffectCfDurableObject,
36
37
  DurableObjectAlarm,
@@ -45,6 +46,8 @@ import { cloudflarePreparedInputAdmissionLayer } from "./internal/prepared-admis
45
46
  const SUBSCRIPTION_ALARM_TAG = "effect-agent/SubscriptionPartitionWake";
46
47
  const SUBSCRIPTION_ALARM_ID = "driver";
47
48
  const MAX_ALARM_WALL_MILLIS = 12 * 60_000;
49
+ const MAX_ALARMS_PER_INVOCATION = 16;
50
+ const MAX_ANCILLARY_ALARM_MILLIS = 30_000;
48
51
 
49
52
  const SubscriptionAlarmPayload = Schema.Struct({
50
53
  schemaVersion: Schema.Literal(1),
@@ -56,6 +59,86 @@ export class SubscriptionAlarmProtocolError extends Schema.TaggedError<Subscript
56
59
  { message: Schema.String },
57
60
  ) {}
58
61
 
62
+ /** Bounded host diagnostic; credentials and provider responses do not belong in alarm failures. */
63
+ export class SubscriptionAlarmExtensionError extends Schema.TaggedError<SubscriptionAlarmExtensionError>()(
64
+ "SubscriptionAlarmExtensionError",
65
+ { code: Schema.NonEmptyString.check(Schema.isMaxLength(128)) },
66
+ ) {}
67
+
68
+ export interface SubscriptionPartitionAlarmHandler {
69
+ readonly tag: string;
70
+ readonly handle: (
71
+ event: DurableObjectAlarm.DurableObjectAlarmEvent,
72
+ ) => Effect.Effect<void, SubscriptionAlarmProtocolError | SubscriptionAlarmExtensionError>;
73
+ }
74
+
75
+ /** Host-only handlers; the framework reserves its namespace and rejects every unknown tag. */
76
+ export const SubscriptionPartitionAlarmExtension = Context.Reference<{
77
+ readonly handlers: ReadonlyArray<SubscriptionPartitionAlarmHandler>;
78
+ }>("@effect-agent/platform-cloudflare/SubscriptionPartitionAlarmExtension", {
79
+ defaultValue: () => ({ handlers: [] }),
80
+ });
81
+
82
+ /** Capture host services once; each invocation owns its codec/handler Scope and timeout.
83
+ * Callback failures stay typed. Defects and interruption reach the native alarm multiplexer.
84
+ * The host owns durable idempotency, prearming and external-effect uncertainty.
85
+ */
86
+ export const makeSubscriptionPartitionAlarmHandler = Effect.fn(
87
+ "makeSubscriptionPartitionAlarmHandler",
88
+ )(function* <Payload extends Schema.Top, R>(options: {
89
+ readonly tag: string;
90
+ readonly payload: Payload;
91
+ readonly timeoutMillis: number;
92
+ readonly handle: (
93
+ event: Omit<DurableObjectAlarm.DurableObjectAlarmEvent, "payload"> & {
94
+ readonly payload: Payload["Type"];
95
+ },
96
+ ) => Effect.Effect<void, SubscriptionAlarmExtensionError, R>;
97
+ }): Effect.fn.Return<
98
+ SubscriptionPartitionAlarmHandler,
99
+ SubscriptionAlarmProtocolError,
100
+ Exclude<R | Payload["DecodingServices"], Scope.Scope>
101
+ > {
102
+ if (
103
+ options.tag.length === 0 ||
104
+ options.tag.length > 128 ||
105
+ options.tag.startsWith("effect-agent/") ||
106
+ !Number.isSafeInteger(options.timeoutMillis) ||
107
+ options.timeoutMillis < 1 ||
108
+ options.timeoutMillis > MAX_ANCILLARY_ALARM_MILLIS
109
+ )
110
+ return yield* SubscriptionAlarmProtocolError.make({
111
+ message: "Invalid ancillary alarm tag or timeout",
112
+ });
113
+ const services = yield* Effect.context<Exclude<R | Payload["DecodingServices"], Scope.Scope>>();
114
+
115
+ return {
116
+ tag: options.tag,
117
+ handle: (event) =>
118
+ Effect.gen(function* () {
119
+ if (event.tag !== options.tag)
120
+ return yield* SubscriptionAlarmProtocolError.make({
121
+ message: "Ancillary alarm tag mismatch",
122
+ });
123
+
124
+ const payload = yield* Schema.decodeUnknownEffect(options.payload)(event.payload).pipe(
125
+ Effect.mapError(() =>
126
+ SubscriptionAlarmProtocolError.make({ message: "Invalid ancillary alarm payload" }),
127
+ ),
128
+ );
129
+
130
+ yield* options.handle({ ...event, payload });
131
+ }).pipe(
132
+ Effect.scoped,
133
+ Effect.timeout(options.timeoutMillis),
134
+ Effect.catchTag("TimeoutError", () =>
135
+ SubscriptionAlarmExtensionError.make({ code: "timeout" }),
136
+ ),
137
+ Effect.provideContext(services),
138
+ ),
139
+ };
140
+ });
141
+
59
142
  export class SubscriptionPartitionProtocolError extends Schema.TaggedError<SubscriptionPartitionProtocolError>()(
60
143
  "SubscriptionPartitionProtocolError",
61
144
  { message: Schema.String.check(Schema.isMaxLength(4_096)) },
@@ -69,9 +152,11 @@ export class CloudflareSubscriptionConfigError extends Schema.TaggedError<Cloudf
69
152
  /** Reject limits whose four bounded phases could exceed the safe Durable Object alarm budget. */
70
153
  export const validateCloudflareSubscriptionLimits = (
71
154
  limits: SubscriptionLimits,
155
+ options: { readonly ancillaryAlarms?: boolean } = {},
72
156
  ): Effect.Effect<void, CloudflareSubscriptionConfigError> => {
73
157
  const worstCaseMillis =
74
- 4 * Math.ceil(limits.batchSize / limits.concurrency) * limits.operationTimeoutMillis;
158
+ 4 * Math.ceil(limits.batchSize / limits.concurrency) * limits.operationTimeoutMillis +
159
+ (options.ancillaryAlarms === true ? MAX_ALARMS_PER_INVOCATION * MAX_ANCILLARY_ALARM_MILLIS : 0);
75
160
 
76
161
  return worstCaseMillis <= MAX_ALARM_WALL_MILLIS
77
162
  ? Effect.void
@@ -96,14 +181,32 @@ const SubscribeRequest = Schema.TaggedStruct("Subscribe", {
96
181
  parameters: PersistedJson,
97
182
  context: PersistedJson,
98
183
  mode: Schema.Literals(["once", "continuous"]),
99
- expiresAtMillis: Schema.Number,
184
+ expiresAtMillis: Schema.NullOr(Schema.Number),
100
185
  destination: SubscriptionConfiguration.fields.destination,
101
186
  deliveryPrincipal: SubscriptionConfiguration.fields.deliveryPrincipal,
187
+ admissionGroup: SubscriptionConfiguration.fields.admissionGroup,
188
+ admissionFence: SubscriptionConfiguration.fields.admissionFence,
102
189
  agentId: SubscriptionConfiguration.fields.agentId,
103
190
  definitions: SubscriptionConfiguration.fields.definitions,
104
191
  }),
105
192
  });
106
193
 
194
+ const UpdateSubscriptionRequest = Schema.TaggedStruct("UpdateSubscription", {
195
+ schemaVersion: Schema.Literal(1),
196
+ scope: SubscribeRequest.fields.scope,
197
+ key: SubscriptionKey,
198
+ expectedRevision: Schema.Int.check(Schema.isGreaterThan(0)),
199
+ options: SubscribeRequest.fields.options.mapFields(({ subscriptionId: _, ...fields }) => fields),
200
+ });
201
+
202
+ const SubscriptionStateRequest = Schema.Struct({
203
+ _tag: Schema.Literals(["PauseSubscription", "ResumeSubscription", "RecoverSubscription"]),
204
+ schemaVersion: Schema.Literal(1),
205
+ scope: SubscribeRequest.fields.scope,
206
+ key: SubscriptionKey,
207
+ expectedRevision: Schema.Int.check(Schema.isGreaterThan(0)),
208
+ });
209
+
107
210
  const ListSubscriptionsRequest = Schema.TaggedStruct("ListSubscriptions", {
108
211
  schemaVersion: Schema.Literal(1),
109
212
  scope: SubscribeRequest.fields.scope,
@@ -112,11 +215,25 @@ const ListSubscriptionsRequest = Schema.TaggedStruct("ListSubscriptions", {
112
215
  });
113
216
 
114
217
  const CancelSubscriptionRequest = Schema.TaggedStruct("CancelSubscription", {
218
+ expectedRevision: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
115
219
  schemaVersion: Schema.Literal(1),
116
220
  scope: SubscribeRequest.fields.scope,
117
221
  key: SubscriptionKey,
118
222
  });
119
223
 
224
+ const GetSubscriptionRequest = Schema.TaggedStruct("GetSubscription", {
225
+ schemaVersion: Schema.Literal(1),
226
+ scope: SubscriptionScope,
227
+ key: SubscriptionKey,
228
+ });
229
+
230
+ const RecoverDeliveryRequest = Schema.TaggedStruct("RecoverDelivery", {
231
+ expectedGeneration: Schema.Natural,
232
+ schemaVersion: Schema.Literal(1),
233
+ scope: SubscribeRequest.fields.scope,
234
+ key: SubscriptionDeliveryKey,
235
+ });
236
+
120
237
  const ListDeliveriesRequest = Schema.TaggedStruct("ListDeliveries", {
121
238
  schemaVersion: Schema.Literal(1),
122
239
  scope: SubscribeRequest.fields.scope,
@@ -143,9 +260,13 @@ const StatusRequest = Schema.TaggedStruct("Status", {
143
260
 
144
261
  const SubscriptionPartitionRequest = Schema.Union([
145
262
  SubscribeRequest,
263
+ GetSubscriptionRequest,
264
+ UpdateSubscriptionRequest,
265
+ SubscriptionStateRequest,
146
266
  ListSubscriptionsRequest,
147
267
  CancelSubscriptionRequest,
148
268
  ListDeliveriesRequest,
269
+ RecoverDeliveryRequest,
149
270
  AcceptRequest,
150
271
  StatusRequest,
151
272
  ]);
@@ -182,6 +303,7 @@ const SubscriptionPartitionResponse = Schema.Union([
182
303
  Schema.TaggedStruct("Snapshot", { value: SubscriptionSnapshot }),
183
304
  Schema.TaggedStruct("SubscriptionPage", { value: SubscriptionPage }),
184
305
  Schema.TaggedStruct("DeliveryPage", { value: DeliveryPage }),
306
+ Schema.TaggedStruct("DeliverySnapshot", { value: SubscriptionDeliverySnapshot }),
185
307
  Schema.TaggedStruct("Acknowledgement", { value: EventAcknowledgement }),
186
308
  Schema.TaggedStruct("Status", { value: IntakeStatus }),
187
309
  Schema.TaggedStruct("Failed", { failure: SubscriptionPartitionFailure }),
@@ -261,7 +383,88 @@ export class CloudflareSubscriptionsClient {
261
383
  const asProtocolError = (): SubscriptionError =>
262
384
  corruptFailure("subscription-partition-protocol");
263
385
 
386
+ const changeState = Effect.fn("CloudflareSubscriptions.changeState")(function* (
387
+ scope: typeof SubscriptionScope.Type,
388
+ key: typeof SubscriptionKey.Type,
389
+ expectedRevision: number,
390
+ _tag: "PauseSubscription" | "ResumeSubscription" | "RecoverSubscription",
391
+ ) {
392
+ const response = yield* call(scope.partition, {
393
+ _tag,
394
+ schemaVersion: 1,
395
+ scope,
396
+ key,
397
+ expectedRevision,
398
+ });
399
+
400
+ if (response._tag === "Snapshot") return response.value;
401
+ const failure = failed(response);
402
+
403
+ return yield* failure._tag === "SubscriptionError" ||
404
+ failure._tag === "SubscriptionFailpointError"
405
+ ? failure
406
+ : asProtocolError();
407
+ });
408
+
264
409
  const subscriptions = Subscriptions.of({
410
+ recoverSubscription: (scope, key, revision) =>
411
+ changeState(scope, key, revision, "RecoverSubscription"),
412
+ getSubscription: (scope, key) =>
413
+ Effect.gen(function* () {
414
+ const response = yield* call(scope.partition, {
415
+ _tag: "GetSubscription",
416
+ schemaVersion: 1,
417
+ scope,
418
+ key,
419
+ });
420
+
421
+ if (response._tag === "Snapshot") return response.value;
422
+ const failure = failed(response);
423
+
424
+ return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
425
+ }),
426
+ recoverDelivery: (scope, key, expectedGeneration) =>
427
+ Effect.gen(function* () {
428
+ const response = yield* call(scope.partition, {
429
+ _tag: "RecoverDelivery",
430
+ expectedGeneration,
431
+ schemaVersion: 1,
432
+ scope,
433
+ key,
434
+ });
435
+
436
+ if (response._tag === "DeliverySnapshot") return response.value;
437
+ const failure = failed(response);
438
+
439
+ return yield* failure._tag === "SubscriptionError" ||
440
+ failure._tag === "SubscriptionFailpointError"
441
+ ? failure
442
+ : asProtocolError();
443
+ }),
444
+ updateSubscription: (scope, key, expectedRevision, options) =>
445
+ Effect.gen(function* () {
446
+ const response = yield* call(scope.partition, {
447
+ _tag: "UpdateSubscription",
448
+ schemaVersion: 1,
449
+ scope,
450
+ key,
451
+ expectedRevision,
452
+ options,
453
+ });
454
+
455
+ if (response._tag === "Snapshot") return response.value;
456
+ const failure = failed(response);
457
+
458
+ return yield* failure._tag === "SubscriptionError" ||
459
+ failure._tag === "SubscriptionFailpointError" ||
460
+ failure._tag === "SubscriptionSourceError"
461
+ ? failure
462
+ : asProtocolError();
463
+ }),
464
+ pauseSubscription: (scope, key, revision) =>
465
+ changeState(scope, key, revision, "PauseSubscription"),
466
+ resumeSubscription: (scope, key, revision) =>
467
+ changeState(scope, key, revision, "ResumeSubscription"),
265
468
  subscribe: (scope, options) =>
266
469
  Effect.gen(function* () {
267
470
  const response = yield* call(scope.partition, {
@@ -298,10 +501,11 @@ export class CloudflareSubscriptionsClient {
298
501
 
299
502
  return yield* failure._tag === "SubscriptionError" ? failure : asProtocolError();
300
503
  }),
301
- cancelSubscription: (scope, key) =>
504
+ cancelSubscription: (scope, key, expectedRevision) =>
302
505
  Effect.gen(function* () {
303
506
  const response = yield* call(scope.partition, {
304
507
  _tag: "CancelSubscription",
508
+ ...(expectedRevision === undefined ? {} : { expectedRevision }),
305
509
  schemaVersion: 1,
306
510
  scope,
307
511
  key,
@@ -461,11 +665,53 @@ const handleRequest = Effect.fn("SubscriptionPartition.handleRequest")(function*
461
665
  SubscriptionPartitionFailure
462
666
  > {
463
667
  switch (request._tag) {
668
+ case "GetSubscription":
669
+ return {
670
+ _tag: "Snapshot",
671
+ value: yield* subscriptions.getSubscription(request.scope, request.key),
672
+ };
464
673
  case "Subscribe":
465
674
  return {
466
675
  _tag: "Snapshot",
467
676
  value: yield* subscriptions.subscribe(request.scope, request.options),
468
677
  };
678
+ case "UpdateSubscription":
679
+ return {
680
+ _tag: "Snapshot",
681
+ value: yield* subscriptions.updateSubscription(
682
+ request.scope,
683
+ request.key,
684
+ request.expectedRevision,
685
+ request.options,
686
+ ),
687
+ };
688
+ case "RecoverSubscription":
689
+ return {
690
+ _tag: "Snapshot",
691
+ value: yield* subscriptions.recoverSubscription(
692
+ request.scope,
693
+ request.key,
694
+ request.expectedRevision,
695
+ ),
696
+ };
697
+ case "PauseSubscription":
698
+ return {
699
+ _tag: "Snapshot",
700
+ value: yield* subscriptions.pauseSubscription(
701
+ request.scope,
702
+ request.key,
703
+ request.expectedRevision,
704
+ ),
705
+ };
706
+ case "ResumeSubscription":
707
+ return {
708
+ _tag: "Snapshot",
709
+ value: yield* subscriptions.resumeSubscription(
710
+ request.scope,
711
+ request.key,
712
+ request.expectedRevision,
713
+ ),
714
+ };
469
715
  case "ListSubscriptions":
470
716
  return {
471
717
  _tag: "SubscriptionPage",
@@ -478,7 +724,20 @@ const handleRequest = Effect.fn("SubscriptionPartition.handleRequest")(function*
478
724
  case "CancelSubscription":
479
725
  return {
480
726
  _tag: "Snapshot",
481
- value: yield* subscriptions.cancelSubscription(request.scope, request.key),
727
+ value: yield* subscriptions.cancelSubscription(
728
+ request.scope,
729
+ request.key,
730
+ request.expectedRevision,
731
+ ),
732
+ };
733
+ case "RecoverDelivery":
734
+ return {
735
+ _tag: "DeliverySnapshot",
736
+ value: yield* subscriptions.recoverDelivery(
737
+ request.scope,
738
+ request.key,
739
+ request.expectedGeneration,
740
+ ),
482
741
  };
483
742
  case "ListDeliveries":
484
743
  return {
@@ -566,7 +825,29 @@ const alarmHandler = (limits: SubscriptionLimits) =>
566
825
  DurableObjectAlarm.processDue(
567
826
  (event) =>
568
827
  Effect.gen(function* () {
569
- if (event.tag !== SUBSCRIPTION_ALARM_TAG || event.id !== SUBSCRIPTION_ALARM_ID) {
828
+ if (event.tag !== SUBSCRIPTION_ALARM_TAG) {
829
+ const { handlers } = yield* SubscriptionPartitionAlarmExtension;
830
+ const matches = handlers.filter((handler) => handler.tag === event.tag);
831
+ const handler = matches[0];
832
+
833
+ if (
834
+ event.tag.startsWith("effect-agent/") ||
835
+ matches.length !== 1 ||
836
+ handler === undefined
837
+ )
838
+ return yield* SubscriptionAlarmProtocolError.make({
839
+ message: "Unknown or ambiguous ancillary alarm tag",
840
+ });
841
+
842
+ return yield* handler.handle(event).pipe(
843
+ Effect.scoped,
844
+ Effect.timeout(MAX_ANCILLARY_ALARM_MILLIS),
845
+ Effect.catchTag("TimeoutError", () =>
846
+ SubscriptionAlarmExtensionError.make({ code: "timeout" }),
847
+ ),
848
+ );
849
+ }
850
+ if (event.id !== SUBSCRIPTION_ALARM_ID) {
570
851
  return yield* SubscriptionAlarmProtocolError.make({
571
852
  message: `Unsupported Subscription Partition alarm ${event.tag}/${event.id}`,
572
853
  });
@@ -600,7 +881,12 @@ const alarmHandler = (limits: SubscriptionLimits) =>
600
881
  yield* alarmControl.reconcile;
601
882
  }
602
883
  }),
603
- { mode: "ordered" },
884
+ {
885
+ mode: "isolated",
886
+ limit: MAX_ALARMS_PER_INVOCATION,
887
+ retryFailedAfter: limits.retryMillis,
888
+ onFailure: () => Effect.logWarning("Subscription partition alarm retained for retry"),
889
+ },
604
890
  ).pipe(Effect.asVoid);
605
891
 
606
892
  type SubscriptionRuntimeServices =
@@ -669,6 +955,29 @@ export const makeSubscriptionPartitionObjectClass = <E>(
669
955
  SubscriptionIntake.layer(limits),
670
956
  SubscriptionDriver.layer(limits),
671
957
  cloudflareLimitsLayer,
958
+ // Capture the host reference before its scoped input Layer is hidden from the runtime.
959
+ Layer.effect(
960
+ SubscriptionPartitionAlarmExtension,
961
+ Effect.gen(function* () {
962
+ const extension = yield* SubscriptionPartitionAlarmExtension;
963
+ const tags = extension.handlers.map((handler) => handler.tag);
964
+
965
+ if (
966
+ tags.length > MAX_ALARMS_PER_INVOCATION ||
967
+ new Set(tags).size !== tags.length ||
968
+ tags.some(
969
+ (tag) => tag.length === 0 || tag.length > 128 || tag.startsWith("effect-agent/"),
970
+ )
971
+ )
972
+ return yield* SubscriptionAlarmProtocolError.make({
973
+ message: "Invalid ancillary alarm handler registry",
974
+ });
975
+
976
+ yield* validateCloudflareSubscriptionLimits(limits, { ancillaryAlarms: tags.length > 0 });
977
+
978
+ return extension;
979
+ }),
980
+ ),
672
981
  ).pipe(
673
982
  Layer.provideMerge(partitionStore),
674
983
  Layer.provide(
@@ -682,7 +991,11 @@ export const makeSubscriptionPartitionObjectClass = <E>(
682
991
 
683
992
  const runtime: Layer.Layer<
684
993
  SubscriptionRuntimeServices,
685
- E | SubscriptionError | SubscriptionPartitionProtocolError | CloudflareSubscriptionConfigError,
994
+ | E
995
+ | SubscriptionError
996
+ | SubscriptionPartitionProtocolError
997
+ | CloudflareSubscriptionConfigError
998
+ | SubscriptionAlarmProtocolError,
686
999
  EffectCfDurableObjectState.DurableObjectState | WorkerEnvironment
687
1000
  > = Layer.effectContext(
688
1001
  Effect.gen(function* () {
@@ -15,9 +15,12 @@ import {
15
15
  PersistedJson,
16
16
  } from "@effect-agent/thread/Records";
17
17
  import {
18
+ AdmissionFence,
19
+ AdmissionGroup,
18
20
  AbortCommand,
19
21
  AbortIntent,
20
22
  AdmissionConflict,
23
+ AdmissionPolicyError,
21
24
  ApprovalConflict,
22
25
  ApprovalDecisionCommand,
23
26
  ApprovalDecisionIntent,
@@ -31,6 +34,7 @@ import {
31
34
  UnknownResolutionConflict,
32
35
  UnknownResolutionIntent,
33
36
  } from "@effect-agent/thread/SubmissionLedger";
37
+ import { SubmissionStatus } from "@effect-agent/thread/SubmissionStatus";
34
38
  import {
35
39
  AppendConflict,
36
40
  ThreadNotMaterialized,
@@ -107,6 +111,8 @@ export class SubmitRequest extends Schema.Class<SubmitRequest>(
107
111
  agentId: AgentId,
108
112
  principal: Principal,
109
113
  idempotencyKey: IdempotencyKey,
114
+ admissionGroup: Schema.optionalKey(AdmissionGroup),
115
+ admissionFence: Schema.optionalKey(AdmissionFence),
110
116
  definitions: DefinitionDigests,
111
117
  inputPayload: PersistedJson,
112
118
  }) {}
@@ -148,6 +154,7 @@ export const HostFailure = Schema.Union([
148
154
  AgentInputError,
149
155
  DigestError,
150
156
  AdmissionConflict,
157
+ AdmissionPolicyError,
151
158
  SettlementConflict,
152
159
  ApprovalConflict,
153
160
  UnknownResolutionConflict,
@@ -172,6 +179,10 @@ export class SubmitSucceeded extends Schema.TaggedClass<SubmitSucceeded>(
172
179
  receipt: Receipt,
173
180
  }) {}
174
181
 
182
+ export class SubmissionStatusResponse extends Schema.TaggedClass<SubmissionStatusResponse>(
183
+ "@effect-agent/platform-cloudflare/SubmissionStatusResponse",
184
+ )("SubmissionStatusResponse", { status: SubmissionStatus }) {}
185
+
175
186
  export class SettlementReached extends Schema.TaggedClass<SettlementReached>(
176
187
  "@effect-agent/platform-cloudflare/SettlementReached",
177
188
  )("SettlementReached", {
@@ -222,6 +233,7 @@ export class HostFailed extends Schema.TaggedClass<HostFailed>(
222
233
  export const HostResponse = Schema.Union([
223
234
  SubmitSucceeded,
224
235
  SettlementReached,
236
+ SubmissionStatusResponse,
225
237
  ObservedPage,
226
238
  ProgressObserved,
227
239
  ProgressCancelled,
@@ -265,6 +277,7 @@ const ClientSubmitHostFailure = Schema.Union([
265
277
  AgentInputError,
266
278
  DigestError,
267
279
  AdmissionConflict,
280
+ AdmissionPolicyError,
268
281
  LedgerError,
269
282
  ThreadStoreError,
270
283
  ThreadNotMaterialized,
@@ -339,6 +352,7 @@ const outOfContract = (threadId: string, operation: string, observed: string): T
339
352
  const hostRpcMethods = {
340
353
  submit: "submitEncoded",
341
354
  awaitSettlement: "awaitSettlementEncoded",
355
+ submissionStatus: "submissionStatusEncoded",
342
356
  awaitProgress: "awaitProgressEncoded",
343
357
  cancelProgress: "cancelProgressEncoded",
344
358
  observePage: "observePage",
@@ -358,6 +372,9 @@ export class CloudflareThreadClient extends Context.Service<
358
372
  options: DurableSubmitOptions,
359
373
  ) => Effect.Effect<Receipt, ClientSubmitFailure, InputSchema["EncodingServices"]>;
360
374
  /** Wake-hinted, poll-guaranteed settlement wait executed inside the owning Object. */
375
+ readonly submissionStatus: (
376
+ receipt: Receipt,
377
+ ) => Effect.Effect<SubmissionStatus, ClientAwaitFailure>;
361
378
  readonly awaitSettlement: (receipt: Receipt) => Effect.Effect<Settlement, ClientAwaitFailure>;
362
379
  /**
363
380
  * Wait without polling until progress after `afterSequence` is already durable or hinted.
@@ -572,6 +589,12 @@ export class CloudflareThreadClient extends Context.Service<
572
589
  agentId: agent.definition.id,
573
590
  principal: options.principal,
574
591
  idempotencyKey: options.idempotencyKey,
592
+ ...(options.admissionGroup === undefined
593
+ ? {}
594
+ : { admissionGroup: options.admissionGroup }),
595
+ ...(options.admissionFence === undefined
596
+ ? {}
597
+ : { admissionFence: options.admissionFence }),
575
598
  definitions: options.definitions,
576
599
  inputPayload,
577
600
  });
@@ -596,6 +619,23 @@ export class CloudflareThreadClient extends Context.Service<
596
619
  return succeeded.receipt;
597
620
  }),
598
621
 
622
+ submissionStatus: (receipt) =>
623
+ Effect.gen(function* () {
624
+ const encoded = yield* encodeReceipt(receipt).pipe(
625
+ Effect.mapError(() => HostProtocolError.make({ message: "Invalid receipt" })),
626
+ );
627
+
628
+ const response = yield* call(receipt.threadId, "submissionStatus", encoded);
629
+
630
+ const result = yield* expect(
631
+ receipt.threadId,
632
+ "submissionStatus",
633
+ SubmissionStatusResponse,
634
+ ClientAwaitHostFailure,
635
+ )(response);
636
+
637
+ return result.status;
638
+ }),
599
639
  awaitSettlement: (receipt) =>
600
640
  Effect.gen(function* () {
601
641
  const encoded = yield* encodeReceipt(receipt).pipe(
@@ -28,6 +28,7 @@ import {
28
28
  import { PersistedJson } from "@effect-agent/thread/Records";
29
29
  import { RunJournalError } from "@effect-agent/thread/RunJournal";
30
30
  import {
31
+ AdmissionPolicyError,
31
32
  LedgerError,
32
33
  OwnershipLost,
33
34
  SettlementConflict,
@@ -73,6 +74,7 @@ import {
73
74
  ProgressObserved,
74
75
  ProgressCancelled,
75
76
  SettlementReached,
77
+ SubmissionStatusResponse,
76
78
  SubmitSucceeded,
77
79
  UnknownResolutionRecorded,
78
80
  boundHostDiagnostic,
@@ -308,6 +310,12 @@ const submitEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoin
308
310
  threadId: identity.threadId,
309
311
  principal: request.principal,
310
312
  idempotencyKey: request.idempotencyKey,
313
+ ...(request.admissionGroup === undefined
314
+ ? {}
315
+ : { admissionGroup: request.admissionGroup }),
316
+ ...(request.admissionFence === undefined
317
+ ? {}
318
+ : { admissionFence: request.admissionFence }),
311
319
  definitions: request.definitions,
312
320
  }),
313
321
  );
@@ -319,6 +327,22 @@ const submitEndpoint = (encoded: unknown): Effect.Effect<unknown, never, Endpoin
319
327
  Effect.flatMap(encodeResponse),
320
328
  );
321
329
 
330
+ const submissionStatusEndpoint = (
331
+ encoded: unknown,
332
+ ): Effect.Effect<unknown, never, EndpointServices> =>
333
+ decodeReceipt(encoded).pipe(
334
+ Effect.mapError(protocolFailure("The receipt could not be decoded")),
335
+ Effect.flatMap((receipt) =>
336
+ Effect.gen(function* () {
337
+ const runtime = yield* DurableAgentRuntime;
338
+
339
+ return SubmissionStatusResponse.make({ status: yield* runtime.submissionStatus(receipt) });
340
+ }),
341
+ ),
342
+ respond,
343
+ Effect.flatMap(encodeResponse),
344
+ );
345
+
322
346
  const awaitSettlementEndpoint = (
323
347
  encoded: unknown,
324
348
  ): Effect.Effect<unknown, never, EndpointServices> =>
@@ -492,6 +516,7 @@ export class AdminVerifyRequest extends Schema.Class<AdminVerifyRequest>(
492
516
 
493
517
  /** Every typed failure of the four admin entry points, plus the protocol's own errors. */
494
518
  export const AdminFailure = Schema.Union([
519
+ AdmissionPolicyError,
495
520
  OperationDenied,
496
521
  RetryRefused,
497
522
  LedgerError,
@@ -781,6 +806,7 @@ export interface Instance<EventServices = never> extends InstanceType<
781
806
  EffectCfDurableObject.DurableObjectClass<Record<never, never>, RuntimeServices | EventServices>
782
807
  > {
783
808
  submitEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
809
+ submissionStatusEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
784
810
  awaitSettlementEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
785
811
  awaitProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
786
812
  cancelProgressEncoded(encoded: unknown, traceContext?: unknown): Promise<unknown>;
@@ -859,6 +885,7 @@ export const make = <
859
885
 
860
886
  const rpc = {
861
887
  submitEncoded: (encoded: unknown) => submitEndpoint(encoded),
888
+ submissionStatusEncoded: (encoded: unknown) => submissionStatusEndpoint(encoded),
862
889
  awaitSettlementEncoded: (encoded: unknown) => awaitSettlementEndpoint(encoded),
863
890
  awaitProgressEncoded: (encoded: unknown) => awaitProgressEndpoint(encoded),
864
891
  cancelProgressEncoded: (encoded: unknown) => cancelProgressEndpoint(encoded),