effect-agent 0.1.0-beta.152 → 0.1.0-beta.153

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.
Files changed (131) hide show
  1. package/dist/{Admin-CKIdJoP8.d.mts → Admin-BjmupLN3.d.mts} +36 -17
  2. package/dist/{Agent-Cjd_ZveM.d.mts → Agent-BCyrGaWg.d.mts} +2 -2
  3. package/dist/{AgentPolicy-BqDoSXO6.d.mts → AgentPolicy-CnXWWwdg.d.mts} +5 -1
  4. package/dist/{AgentRuntime-sXpKcyxu.d.mts → AgentRuntime-BAjT5gXl.d.mts} +10 -10
  5. package/dist/{AgentRuntime-D8U9gl9Q.mjs → AgentRuntime-IF_1GoUf.mjs} +54 -11
  6. package/dist/AgentRuntime-IF_1GoUf.mjs.map +1 -0
  7. package/dist/{ContextCompactor-WwSIG_Z2.d.mts → ContextCompactor-CPlStZRc.d.mts} +2 -2
  8. package/dist/{Digest-U69rDHuO.d.mts → Digest-CaJ2RIsF.d.mts} +2 -2
  9. package/dist/{DurableAgentRuntime-DQFV26SB.mjs → DurableAgentRuntime-BQzYkFVy.mjs} +185 -88
  10. package/dist/DurableAgentRuntime-BQzYkFVy.mjs.map +1 -0
  11. package/dist/{EventSource-CMbXXmwe.d.mts → EventSource-C8CKwQy5.d.mts} +3 -3
  12. package/dist/InMemory.d.mts +1 -1
  13. package/dist/{Messaging-ccQURKCf.d.mts → Messaging-B1zumorg.d.mts} +3 -3
  14. package/dist/{MessagingHost-D04wE-7W.d.mts → MessagingHost-B4jqlJmx.d.mts} +3 -3
  15. package/dist/{Output-CFD1CPfU.d.mts → Output-WL7JexCm.d.mts} +2 -2
  16. package/dist/{PreparedInputAdmission-Bt6rgtVu.d.mts → PreparedInputAdmission-CU1PzUVD.d.mts} +4 -4
  17. package/dist/{Records-B5GFWCpn.d.mts → Records-BrEpibum.d.mts} +17 -6
  18. package/dist/{RunEvent-DIrzkjLr.d.mts → RunEvent-DV_llBcq.d.mts} +18 -4
  19. package/dist/{RunJournal-Gd83HYqG.mjs → RunJournal-ixFRCyW3.mjs} +29 -7
  20. package/dist/RunJournal-ixFRCyW3.mjs.map +1 -0
  21. package/dist/{RunOptions-Bv9NpkPZ.d.mts → RunOptions-CkRYnlJa.d.mts} +22 -8
  22. package/dist/{RunPolicyUsage-ClZxyR46.d.mts → RunPolicyUsage-CUudVtg_.d.mts} +3 -1
  23. package/dist/{Schedule-CtYREFnz.d.mts → Schedule-B_z7PMg-.d.mts} +2 -2
  24. package/dist/{Subagent-hG9lJ7tJ.mjs → Subagent-CmFtZaeI.mjs} +2 -2
  25. package/dist/{Subagent-hG9lJ7tJ.mjs.map → Subagent-CmFtZaeI.mjs.map} +1 -1
  26. package/dist/{Subagent-VHrANXFy.d.mts → Subagent-Dld4W1BY.d.mts} +7 -7
  27. package/dist/{SubagentHost-C-4A7tA2.d.mts → SubagentHost--HBuY8fF.d.mts} +4 -4
  28. package/dist/{Subscription-BJAzYNw1.d.mts → Subscription-BomcQ9_W.d.mts} +5 -5
  29. package/dist/{SubscriptionInput-CkBJt0AS.d.mts → SubscriptionInput-gpZSR9vB.d.mts} +3 -3
  30. package/dist/{ThreadHistory-tS1KwxHR.d.mts → ThreadHistory-ZEu_ja6L.d.mts} +2 -2
  31. package/dist/{Worker-fwWkBg12.d.mts → Worker-2r2zGDkh.d.mts} +2 -2
  32. package/dist/{WorkerAdmission-CJ_oWRuS.mjs → WorkerAdmission-CNbm4o_x.mjs} +3 -3
  33. package/dist/{WorkerAdmission-CJ_oWRuS.mjs.map → WorkerAdmission-CNbm4o_x.mjs.map} +1 -1
  34. package/dist/{agent-registration-CKNsrpwo.mjs → agent-registration-U3kmV7nW.mjs} +18 -18
  35. package/dist/agent-registration-U3kmV7nW.mjs.map +1 -0
  36. package/dist/capabilities/Commands.d.mts +1 -1
  37. package/dist/capabilities/Messaging.d.mts +1 -1
  38. package/dist/capabilities/Redaction.d.mts +1 -1
  39. package/dist/capabilities/RunHooks.d.mts +1 -1
  40. package/dist/capabilities/Subagent.d.mts +2 -2
  41. package/dist/capabilities/Subagent.mjs +1 -1
  42. package/dist/core/Agent.d.mts +1 -1
  43. package/dist/core/AgentPolicy.d.mts +1 -1
  44. package/dist/core/AgentPolicy.mjs +2 -0
  45. package/dist/core/AgentPolicy.mjs.map +1 -1
  46. package/dist/core/RunEvent.d.mts +2 -2
  47. package/dist/core/RunEvent.mjs +10 -1
  48. package/dist/core/RunEvent.mjs.map +1 -1
  49. package/dist/core/RunPolicyUsage.d.mts +1 -1
  50. package/dist/core/RunPolicyUsage.mjs +6 -1
  51. package/dist/core/RunPolicyUsage.mjs.map +1 -1
  52. package/dist/core/Worker.d.mts +1 -1
  53. package/dist/durable/ActivityStore.d.mts +1 -1
  54. package/dist/durable/Admin.d.mts +1 -1
  55. package/dist/durable/AgentRegistration.d.mts +2 -2
  56. package/dist/durable/AgentRegistration.mjs +3 -2
  57. package/dist/durable/AgentRegistration.mjs.map +1 -1
  58. package/dist/durable/Certification.d.mts +1 -1
  59. package/dist/durable/CommittedActivity.d.mts +3 -3
  60. package/dist/durable/Digest.d.mts +1 -1
  61. package/dist/durable/DurableAgentRuntime.d.mts +1 -1
  62. package/dist/durable/DurableAgentRuntime.mjs +2 -2
  63. package/dist/durable/EventSource.d.mts +1 -1
  64. package/dist/durable/GitHubWorkflowSource.d.mts +3 -3
  65. package/dist/durable/LifecyclePublication.d.mts +1 -1
  66. package/dist/durable/MessageDelivery.d.mts +5 -5
  67. package/dist/durable/MessageDeliveryStoreConformance.d.mts +24 -24
  68. package/dist/durable/PersistentHistory.d.mts +2 -2
  69. package/dist/durable/PersistentHistory.mjs +1 -1
  70. package/dist/durable/PreparedInputAdmission.d.mts +1 -1
  71. package/dist/durable/Records.d.mts +2 -2
  72. package/dist/durable/Records.mjs +16 -1
  73. package/dist/durable/Records.mjs.map +1 -1
  74. package/dist/durable/Recovery.d.mts +1 -1
  75. package/dist/durable/RunJournal.d.mts +1 -1
  76. package/dist/durable/RunJournal.mjs +1 -1
  77. package/dist/durable/Schedule.d.mts +1 -1
  78. package/dist/durable/ScheduleStoreConformance.d.mts +1 -1
  79. package/dist/durable/ScheduleTransition.d.mts +1 -1
  80. package/dist/durable/Scheduling.d.mts +3 -3
  81. package/dist/durable/SubmissionLedger.d.mts +1 -1
  82. package/dist/durable/SubmissionLedgerConformance.d.mts +2 -2
  83. package/dist/durable/SubmissionLedgerConformance.mjs +1 -1
  84. package/dist/durable/SubmissionStatus.d.mts +1 -1
  85. package/dist/durable/Subscription.d.mts +1 -1
  86. package/dist/durable/SubscriptionInput.d.mts +1 -1
  87. package/dist/durable/SubscriptionInput.mjs +1 -1
  88. package/dist/durable/SubscriptionStoreConformance.d.mts +1 -1
  89. package/dist/durable/SubscriptionTools.d.mts +4 -4
  90. package/dist/durable/SubscriptionTransition.d.mts +1 -1
  91. package/dist/durable/Subscriptions.d.mts +6 -6
  92. package/dist/durable/ThreadContextHistory.d.mts +1 -1
  93. package/dist/durable/ThreadContextHistoryProjection.d.mts +1 -1
  94. package/dist/durable/ThreadInvariants.d.mts +2 -2
  95. package/dist/durable/ThreadInvariants.mjs +1 -1
  96. package/dist/durable/ThreadProjection.d.mts +1 -1
  97. package/dist/durable/ThreadProjectionMaintenance.d.mts +1 -1
  98. package/dist/durable/ThreadStore.d.mts +1 -1
  99. package/dist/durable/ThreadStore.mjs +1 -1
  100. package/dist/durable/ThreadStoreConformance.d.mts +1 -1
  101. package/dist/durable/ThreadStoreConformance.mjs +1 -1
  102. package/dist/durable/WorkerAdmission.d.mts +3 -3
  103. package/dist/durable/WorkerAdmission.mjs +1 -1
  104. package/dist/durable/WorkerHost.d.mts +5 -5
  105. package/dist/engine/AgentRuntime.d.mts +1 -1
  106. package/dist/engine/AgentRuntime.mjs +1 -1
  107. package/dist/engine/Compaction.d.mts +1 -1
  108. package/dist/engine/ContextCompactor.d.mts +1 -1
  109. package/dist/engine/MessagingHost.d.mts +1 -1
  110. package/dist/engine/Output.d.mts +1 -1
  111. package/dist/engine/RunOptions.d.mts +1 -1
  112. package/dist/engine/RunOptions.mjs.map +1 -1
  113. package/dist/engine/SubagentHost.d.mts +1 -1
  114. package/dist/engine/ThreadHistory.d.mts +1 -1
  115. package/dist/index.d.mts +22 -22
  116. package/dist/index.mjs +5 -5
  117. package/package.json +1 -1
  118. package/src/core/AgentPolicy.ts +2 -0
  119. package/src/core/RunEvent.ts +9 -0
  120. package/src/core/RunPolicyUsage.ts +2 -0
  121. package/src/durable/AgentRegistration.ts +2 -0
  122. package/src/durable/DurableAgentRuntime.ts +320 -137
  123. package/src/durable/Records.ts +12 -0
  124. package/src/durable/RunJournal.ts +88 -27
  125. package/src/durable/internal/agent-registration.ts +58 -35
  126. package/src/engine/RunOptions.ts +17 -2
  127. package/src/engine/internal/agent-runtime.ts +115 -8
  128. package/dist/AgentRuntime-D8U9gl9Q.mjs.map +0 -1
  129. package/dist/DurableAgentRuntime-DQFV26SB.mjs.map +0 -1
  130. package/dist/RunJournal-Gd83HYqG.mjs.map +0 -1
  131. package/dist/agent-registration-CKNsrpwo.mjs.map +0 -1
@@ -151,6 +151,7 @@ import {
151
151
  type ResolvedBinding,
152
152
  resolveDefinitionBinding,
153
153
  resolveWorkerBinding,
154
+ CurrentBindingSelection,
154
155
  } from "./internal/agent-registration.ts";
155
156
  import { makeAgentUpdateRuntime } from "./internal/agent-updates.ts";
156
157
  import { inspectForeignDiagnostic, safeUnknownString } from "./internal/foreign-diagnostic.ts";
@@ -184,6 +185,7 @@ import {
184
185
  CompactionCreated,
185
186
  DefinitionDigests,
186
187
  ModelResponseInterrupted,
188
+ ModelCallAborted,
187
189
  PersistedJson,
188
190
  ProducerEpoch,
189
191
  RecordEnvelope,
@@ -240,6 +242,7 @@ import {
240
242
  modelResponseRecordId,
241
243
  projectRunJournalStream,
242
244
  type JournalBoundary,
245
+ type RunJournalProjection,
243
246
  runCompletedRecordId,
244
247
  runCompletionDigest,
245
248
  runIdForSubmission,
@@ -296,6 +299,7 @@ import {
296
299
  ClaimHandoff,
297
300
  SubmissionScheduling,
298
301
  ClaimJoiningRequest,
302
+ type JoiningClaim,
299
303
  ClaimRequest,
300
304
  IdempotencyKey,
301
305
  LedgerError,
@@ -1257,11 +1261,25 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
1257
1261
  bindings: ReadonlyArray<ResolvedBinding>,
1258
1262
  ) {
1259
1263
  const registeredBindings = [...bindings];
1264
+ const bindingSelection = yield* CurrentBindingSelection;
1260
1265
  const workerAdmissionPort = yield* WorkerAdmissionPort;
1261
1266
  const ledger = yield* SubmissionLedger;
1262
1267
  const submissionScheduling = yield* SubmissionScheduling;
1263
1268
  const store = yield* ThreadStore;
1264
1269
 
1270
+ // Disposable: canonical tail and owner identify the exact projection, never ownership.
1271
+ // Retain only the most recent view; persisted checkpoints still govern cold recovery.
1272
+ let projectedJournal:
1273
+ | {
1274
+ readonly threadId: ThreadId;
1275
+ readonly through: CanonicalSequence;
1276
+ readonly runId: RunId;
1277
+ readonly seedThrough: CanonicalSequence | undefined;
1278
+ readonly journal: RunJournalProjection;
1279
+ readonly boundaries: ReadonlyArray<JournalBoundary>;
1280
+ }
1281
+ | undefined;
1282
+
1265
1283
  const wake = yield* WakeScheduler;
1266
1284
  const failpoint = yield* DurableRuntimeFailpoint;
1267
1285
  const config = yield* DurableRuntimeConfig;
@@ -1301,20 +1319,11 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
1301
1319
  Effect.provideService(effect, Crypto.Crypto, crypto);
1302
1320
 
1303
1321
  const contractsFor = (definition: Agent.AnyDefinition) => {
1304
- const registered = registeredBindings.filter((binding) => binding.agentId === definition.id);
1305
- const binding = registered.length === 1 ? registered[0] : undefined;
1306
-
1307
- // Attempts may restore the accepted Run policy on a Definition copy. Replay contracts
1308
- // describe Tools, so retain the registration's semantic versions while all declarations
1309
- // that affect those contracts are unchanged. Replaced Tools or completion declarations
1310
- // must not inherit another executable's replay authority.
1311
- const current =
1312
- binding?.definition.toolkit === definition.toolkit &&
1313
- binding.definition.completion === definition.completion &&
1314
- binding.definition.completionFromTools === definition.completionFromTools
1315
- ? binding
1316
- : undefined;
1322
+ const registered = registeredBindings.filter(
1323
+ (binding) => binding.agentId === definition.id && Object.is(binding.definition, definition),
1324
+ );
1317
1325
 
1326
+ const current = registered.length === 1 ? registered[0] : undefined;
1318
1327
  const contracts = current?.digests.replay?.tools;
1319
1328
 
1320
1329
  return contracts === undefined
@@ -1324,9 +1333,15 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
1324
1333
  : Effect.succeed(contracts);
1325
1334
  };
1326
1335
 
1327
- const currentOperationsFor = Effect.fnUntraced(function* (agentId: AgentId) {
1328
- const registered = registeredBindings.filter((binding) => binding.agentId === agentId);
1329
- const binding = registered.length === 1 ? registered[0] : undefined;
1336
+ const resolveCurrentBinding = (submission: SubmissionSnapshot) =>
1337
+ resolveWorkerBinding(registeredBindings, submission).pipe(
1338
+ Effect.provideService(CurrentBindingSelection, bindingSelection),
1339
+ );
1340
+
1341
+ const currentOperationsFor = Effect.fnUntraced(function* (submission: SubmissionSnapshot) {
1342
+ const binding = yield* resolveCurrentBinding(submission).pipe(
1343
+ Effect.catchTag("BindingUnavailable", () => Effect.succeed(undefined)),
1344
+ );
1330
1345
 
1331
1346
  return binding === undefined
1332
1347
  ? undefined
@@ -4551,6 +4566,7 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
4551
4566
  journalMetadata: JournalMetadata | undefined,
4552
4567
  lineage: AttemptLineage,
4553
4568
  approvalDecisions: ReadonlyArray<ApprovalDecisionIntent>,
4569
+ currentContracts: Readonly<Record<string, Digest>>,
4554
4570
  runTiming: { readonly startedAt: DateTime.Utc; readonly deadline: DateTime.Utc },
4555
4571
  yieldAfter?: DateTime.Utc,
4556
4572
  ) =>
@@ -4560,13 +4576,31 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
4560
4576
  const runId = runIdForSubmission(submissionId);
4561
4577
  const boundaries: Array<JournalBoundary> = [];
4562
4578
 
4563
- const journal = yield* projectRunJournalStream(
4564
- canonical,
4579
+ const cached = projectedJournal;
4580
+
4581
+ const journal =
4582
+ cached !== undefined &&
4583
+ cached.threadId === ctx.threadId &&
4584
+ cached.through === canonicalThrough &&
4585
+ cached.runId === runId &&
4586
+ cached.seedThrough === journalSeed?.throughSequence
4587
+ ? (boundaries.push(...cached.boundaries), cached.journal)
4588
+ : yield* projectRunJournalStream(
4589
+ canonical,
4590
+ runId,
4591
+ (boundary) => boundaries.push(boundary),
4592
+ journalSeed,
4593
+ journalMetadata,
4594
+ );
4595
+
4596
+ projectedJournal = {
4597
+ threadId: ctx.threadId,
4598
+ through: canonicalThrough,
4565
4599
  runId,
4566
- (boundary) => boundaries.push(boundary),
4567
- journalSeed,
4568
- journalMetadata,
4569
- );
4600
+ seedThrough: journalSeed?.throughSequence,
4601
+ journal,
4602
+ boundaries,
4603
+ };
4570
4604
 
4571
4605
  const saveRecoveryCheckpoint = Effect.fn("DurableAgentRuntime.saveRecoveryCheckpoint")(
4572
4606
  function* (compactionId: RecordId): Effect.fn.Return<void, DurableWorkerFailure> {
@@ -4873,8 +4907,6 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
4873
4907
  journalSeed,
4874
4908
  );
4875
4909
 
4876
- const currentContracts = yield* contractsFor(agent.definition);
4877
-
4878
4910
  const rolloverOperation =
4879
4911
  journal.pendingContextToolCallId === undefined
4880
4912
  ? undefined
@@ -5009,15 +5041,43 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
5009
5041
 
5010
5042
  // Staging also backs the cumulative summary; keep it intact and track canonical prefixes.
5011
5043
  const committedUsageLengths = new Map<number, number>();
5044
+ const committedUnobservedCalls = new Map<number, number>();
5045
+
5046
+ const usageForCommit = (turn: number) => {
5047
+ const staged = stagedUsage.get(turn);
5048
+
5049
+ if (staged === undefined) return undefined;
5050
+ const modelUsage = staged.modelUsage.slice(committedUsageLengths.get(turn) ?? 0);
5051
+
5052
+ if (modelUsage.length === 0) return undefined;
5053
+
5054
+ return {
5055
+ modelUsage,
5056
+ inputTokens: modelUsage.reduce((sum, call) => sum + call.inputTokens.total, 0),
5057
+ outputTokens: modelUsage.reduce((sum, call) => sum + call.outputTokens.total, 0),
5058
+ costMicrousd: modelUsage.reduce((sum, call) => sum + call.costMicrousd, 0),
5059
+ };
5060
+ };
5061
+
5062
+ const unobservedForCommit = (turn: number) =>
5063
+ (stagedUnobservedCalls.get(turn) ?? 0) - (committedUnobservedCalls.get(turn) ?? 0);
5012
5064
 
5013
5065
  const recordCommittedUsage = (batch: CanonicalBatch) => {
5014
5066
  for (const record of batch.records) {
5015
- if (record.payload._tag !== "ModelResponseRecorded") continue;
5067
+ if (
5068
+ record.payload._tag !== "ModelResponseRecorded" &&
5069
+ record.payload._tag !== "ModelCallAborted"
5070
+ )
5071
+ continue;
5016
5072
  const payload = record.payload;
5017
5073
 
5018
5074
  committedUsageLengths.set(
5019
5075
  payload.turn,
5020
- Math.max(committedUsageLengths.get(payload.turn) ?? 0, payload.modelUsage?.length ?? 0),
5076
+ (committedUsageLengths.get(payload.turn) ?? 0) + (payload.modelUsage?.length ?? 0),
5077
+ );
5078
+ committedUnobservedCalls.set(
5079
+ payload.turn,
5080
+ (committedUnobservedCalls.get(payload.turn) ?? 0) + (payload.unobservedModelCalls ?? 0),
5021
5081
  );
5022
5082
  }
5023
5083
  };
@@ -5430,7 +5490,9 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
5430
5490
  completedExhausted: undefined,
5431
5491
  });
5432
5492
 
5433
- const turnCounter = yield* Ref.make(journal.committedTurns);
5493
+ const turnCounter = yield* Ref.make(
5494
+ journal.committedTurns + (journal.policyUsage.modelRestarts ?? 0),
5495
+ );
5434
5496
 
5435
5497
  const idGenerator: (typeof IdGenerator)["Service"] = {
5436
5498
  nextThreadId: Effect.succeed(submission.threadId),
@@ -5543,6 +5605,37 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
5543
5605
  };
5544
5606
 
5545
5607
  const durability: RunDurabilityHook<CoordinatorHalt | CompactionError, never> = {
5608
+ commitModelRestart: (restart) =>
5609
+ recordHalt(
5610
+ Effect.gen(function* () {
5611
+ if (knownIds.has(modelResponseRecordId(runId, restart.turn)))
5612
+ return yield* RunJournalError.make({
5613
+ message: "Cannot restart a canonical model response",
5614
+ });
5615
+ const recordId = decodeRecordIdSync(`model-aborted:${runId}:${restart.restart}`);
5616
+
5617
+ const record = yield* makeEnvelope(
5618
+ recordId,
5619
+ ModelCallAborted.make({
5620
+ runId,
5621
+ ...restart,
5622
+ reason: "joined-input",
5623
+ modelUsage: usageForCommit(restart.turn)?.modelUsage ?? [],
5624
+ unobservedModelCalls: unobservedForCommit(restart.turn),
5625
+ }),
5626
+ );
5627
+
5628
+ const batch = CanonicalBatch.make({
5629
+ batchId: decodeBatchIdSync(recordId),
5630
+ producerId: config.producerId,
5631
+ records: [record],
5632
+ });
5633
+
5634
+ yield* appendBatch(ctx, batch);
5635
+ recordCommittedUsage(batch);
5636
+ knownIds.add(recordId);
5637
+ }),
5638
+ ),
5546
5639
  noteToolExposure: (turn, snapshot) =>
5547
5640
  Effect.sync(() => {
5548
5641
  stagedToolExposure.set(turn, snapshot);
@@ -5624,8 +5717,8 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
5624
5717
  ...(canonicalTurn === 1 && pendingSlice.length > 0
5625
5718
  ? { runScopedPrefixLength: pendingSlice.length }
5626
5719
  : {}),
5627
- usage: stagedUsage.get(canonicalTurn),
5628
- unobservedModelCalls: stagedUnobservedCalls.get(canonicalTurn),
5720
+ usage: usageForCommit(canonicalTurn),
5721
+ unobservedModelCalls: unobservedForCommit(canonicalTurn),
5629
5722
  }),
5630
5723
  );
5631
5724
 
@@ -6210,10 +6303,114 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
6210
6303
  yield* commitPendingTurn;
6211
6304
  });
6212
6305
 
6306
+ // Claims are authority, wakes are hints. A cancelled waiter retains every claim
6307
+ // for the seam drain; it never appends input while an old response can still commit.
6308
+ type PreparedJoin = {
6309
+ readonly claim: JoiningClaim;
6310
+ readonly input: Pick<UserInputRecorded, "input" | "messageAdmission">;
6311
+ readonly rendered: Prompt.RawInput;
6312
+ };
6313
+
6314
+ const pendingJoinClaims: Array<{ readonly claim: JoiningClaim; prepared?: PreparedJoin }> =
6315
+ [];
6316
+
6317
+ const claimInputs = Effect.fnUntraced(function* (maxCount: number) {
6318
+ const ownershipToken = yield* Ref.get(tokenRef);
6319
+
6320
+ const claims = yield* ledger.claimJoining(
6321
+ ClaimJoiningRequest.make({
6322
+ threadId: submission.threadId,
6323
+ hostSubmissionId: submissionId,
6324
+ ownershipToken,
6325
+ maxCount,
6326
+ }),
6327
+ );
6328
+
6329
+ pendingJoinClaims.push(...claims.map((claim) => ({ claim })));
6330
+
6331
+ return claims.length;
6332
+ }, Effect.uninterruptible);
6333
+
6334
+ const revertClaims = Effect.fnUntraced(function* (
6335
+ claims: ReadonlyArray<(typeof pendingJoinClaims)[number]>,
6336
+ ) {
6337
+ for (const pending of claims) {
6338
+ yield* ledger.revertJoining(
6339
+ RevertJoiningRequest.make({ submissionId: pending.claim.submissionId }),
6340
+ );
6341
+ pendingJoinClaims.splice(pendingJoinClaims.indexOf(pending), 1);
6342
+ }
6343
+ }, Effect.uninterruptible);
6344
+
6345
+ // Preparation remains interruptible; a completed render stays with its claim so
6346
+ // cancellation of the waiter cannot lose it or invoke the callback again at the seam.
6347
+ const prepareInputs = Effect.fnUntraced(function* (limit: number, firstReady = false) {
6348
+ if (
6349
+ pendingJoinClaims.length < limit &&
6350
+ (yield* claimInputs(limit - pendingJoinClaims.length)) > 0
6351
+ )
6352
+ yield* hit("join:after-claim");
6353
+ const prepared: Array<PreparedJoin> = [];
6354
+
6355
+ for (const pending of pendingJoinClaims.slice(0, limit)) {
6356
+ const claim = pending.claim;
6357
+
6358
+ const snapshot = yield* ledger.loadRecoverySnapshot(
6359
+ RecoverySnapshotRequest.make({ submissionId: claim.submissionId }),
6360
+ );
6361
+
6362
+ if (snapshot.abortIntent !== undefined) {
6363
+ yield* revertClaims([pending]);
6364
+ continue;
6365
+ }
6366
+ if (pending.prepared === undefined) {
6367
+ const input = {
6368
+ input: claim.inputPayload,
6369
+ ...(snapshot.submission.messageAdmission === undefined
6370
+ ? {}
6371
+ : { messageAdmission: snapshot.submission.messageAdmission }),
6372
+ };
6373
+
6374
+ const rendered = yield* Effect.result(renderJoinedInput(input));
6375
+
6376
+ if (Result.isFailure(rendered)) {
6377
+ // A rejected prompt and its suffix retain their own Runs in queue order.
6378
+ yield* revertClaims(pendingJoinClaims.slice(pendingJoinClaims.indexOf(pending)));
6379
+ break;
6380
+ }
6381
+ pending.prepared = { claim, input, rendered: rendered.success };
6382
+ }
6383
+ prepared.push(pending.prepared);
6384
+ if (firstReady) break;
6385
+ }
6386
+
6387
+ return prepared;
6388
+ });
6389
+
6390
+ const awaitJoin = recordHalt(
6391
+ Effect.gen(function* () {
6392
+ while (true) {
6393
+ const ready = yield* Effect.scoped(
6394
+ Effect.gen(function* () {
6395
+ const notified = yield* wake.subscribe(submission.threadId);
6396
+
6397
+ if ((yield* prepareInputs(MAX_JOIN_DRAIN, true)).length > 0) return true;
6398
+ yield* notified;
6399
+
6400
+ return false;
6401
+ }),
6402
+ );
6403
+
6404
+ if (ready) return;
6405
+ }
6406
+ }),
6407
+ );
6408
+
6213
6409
  const input: RunInputHook<
6214
6410
  CoordinatorHalt | Agent.Failure<typeof agent>,
6215
6411
  Agent.DefinitionRequirements<(typeof agent)["definition"]>
6216
6412
  > = {
6413
+ awaitJoin,
6217
6414
  drain: (policy) =>
6218
6415
  Effect.gen(function* () {
6219
6416
  const joinedInputs = yield* recordHalt(
@@ -6233,8 +6430,9 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
6233
6430
 
6234
6431
  joinBacklog = hostSnapshot.joins;
6235
6432
  }
6433
+ // These inputs were already consumed by an earlier attempt. Restore the
6434
+ // entire uncovered prompt; only fresh claims are subject to the drain bound.
6236
6435
  for (const join of joinBacklog) {
6237
- if (joinedInputs.length >= limit) break;
6238
6436
  const joinId = join.submissionId;
6239
6437
 
6240
6438
  if (deliveredJoinInputs.has(joinId)) continue;
@@ -6269,109 +6467,62 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
6269
6467
  yield* preserveToolResults;
6270
6468
  joinedInputs.push(payload);
6271
6469
  }
6272
- if (joinedInputs.length < limit) {
6273
- const ownershipToken = yield* Ref.get(tokenRef);
6274
-
6275
- const claims = yield* ledger.claimJoining(
6276
- ClaimJoiningRequest.make({
6277
- threadId: submission.threadId,
6278
- hostSubmissionId: submissionId,
6279
- ownershipToken,
6280
- maxCount: limit - joinedInputs.length,
6281
- }),
6282
- );
6470
+ const claims = yield* prepareInputs(limit);
6283
6471
 
6284
- if (claims.length > 0) {
6285
- yield* hit("join:after-claim");
6286
- }
6287
- for (const claim of claims) {
6288
- const claimSnapshot = yield* ledger.loadRecoverySnapshot(
6289
- RecoverySnapshotRequest.make({ submissionId: claim.submissionId }),
6472
+ pendingJoinClaims.splice(0, claims.length);
6473
+ for (const { claim, input: payload, rendered } of claims) {
6474
+ yield* preserveToolResults;
6475
+ const recordId = submissionInputRecordId(claim.submissionId);
6476
+ let sequence: CanonicalSequence;
6477
+ const existing = joinedInputEnvelopes.get(claim.submissionId);
6478
+
6479
+ if (existing !== undefined) {
6480
+ // Defensive reattach: the exact record is already canonical, so only the
6481
+ // marker and the delivery remain (DUR-016 — never a duplicate append).
6482
+ sequence = existing.sequence;
6483
+ } else {
6484
+ const envelope = yield* makeEnvelope(
6485
+ recordId,
6486
+ UserInputRecorded.make({
6487
+ submissionId: claim.submissionId,
6488
+ kind: "steering",
6489
+ runId,
6490
+ input: claim.inputPayload,
6491
+ ...(payload.messageAdmission === undefined
6492
+ ? {}
6493
+ : { messageAdmission: payload.messageAdmission }),
6494
+ }),
6290
6495
  );
6291
6496
 
6292
- if (claimSnapshot.abortIntent !== undefined) {
6293
- // Aborted before the host consumed the input: honor the intent by
6294
- // returning the claim to ready (revert-then-abort, plan §2.5); it settles
6295
- // aborted once it heads the lane.
6296
- yield* ledger.revertJoining(
6297
- RevertJoiningRequest.make({ submissionId: claim.submissionId }),
6298
- );
6299
- continue;
6300
- }
6301
-
6302
- const payload = {
6303
- input: claim.inputPayload,
6304
- ...(claimSnapshot.submission.messageAdmission === undefined
6305
- ? {}
6306
- : { messageAdmission: claimSnapshot.submission.messageAdmission }),
6307
- };
6308
-
6309
- const rendered = yield* Effect.result(renderJoinedInput(payload));
6310
-
6311
- if (Result.isFailure(rendered)) {
6312
- // The rejecting input gets its own Run and normal typed failure.
6313
- // Leave it and the unconsumed suffix ready, preserving queue order
6314
- // and preventing one invalid prompt from failing unrelated receipts.
6315
- for (const remaining of claims.slice(claims.indexOf(claim))) {
6316
- yield* ledger.revertJoining(
6317
- RevertJoiningRequest.make({ submissionId: remaining.submissionId }),
6318
- );
6319
- }
6320
- break;
6321
- }
6322
- yield* preserveToolResults;
6323
- const recordId = submissionInputRecordId(claim.submissionId);
6324
- let sequence: CanonicalSequence;
6325
- const existing = joinedInputEnvelopes.get(claim.submissionId);
6326
-
6327
- if (existing !== undefined) {
6328
- // Defensive reattach: the exact record is already canonical, so only the
6329
- // marker and the delivery remain (DUR-016 — never a duplicate append).
6330
- sequence = existing.sequence;
6331
- } else {
6332
- const envelope = yield* makeEnvelope(
6333
- recordId,
6334
- UserInputRecorded.make({
6335
- submissionId: claim.submissionId,
6336
- kind: "steering",
6337
- runId,
6338
- input: claim.inputPayload,
6339
- ...(claimSnapshot.submission.messageAdmission === undefined
6340
- ? {}
6341
- : { messageAdmission: claimSnapshot.submission.messageAdmission }),
6342
- }),
6343
- );
6344
-
6345
- const result = yield* appendBatch(
6346
- ctx,
6347
- CanonicalBatch.make({
6348
- batchId: submissionInputBatchId(claim.submissionId),
6349
- producerId: config.producerId,
6350
- records: [envelope],
6351
- }),
6352
- );
6353
-
6354
- sequence = result.firstSequence;
6355
- knownIds.add(recordId);
6356
- yield* hit("join:after-canonical-append");
6357
- }
6358
- // Re-read the token: the concurrent lease renewal may rotate it mid-batch.
6359
- const markToken = yield* Ref.get(tokenRef);
6360
-
6361
- yield* ledger.markJoined(
6362
- MarkJoinedRequest.make({
6363
- submissionId: claim.submissionId,
6364
- ownershipToken: markToken,
6365
- recordId,
6366
- sequence,
6497
+ const result = yield* appendBatch(
6498
+ ctx,
6499
+ CanonicalBatch.make({
6500
+ batchId: submissionInputBatchId(claim.submissionId),
6501
+ producerId: config.producerId,
6502
+ records: [envelope],
6367
6503
  }),
6368
6504
  );
6369
- deliveredJoinInputs.add(claim.submissionId);
6370
- joinedInputs.push({
6371
- ...payload,
6372
- rendered: rendered.success,
6373
- });
6505
+
6506
+ sequence = result.firstSequence;
6507
+ knownIds.add(recordId);
6508
+ yield* hit("join:after-canonical-append");
6374
6509
  }
6510
+ // Re-read the token: the concurrent lease renewal may rotate it mid-batch.
6511
+ const markToken = yield* Ref.get(tokenRef);
6512
+
6513
+ yield* ledger.markJoined(
6514
+ MarkJoinedRequest.make({
6515
+ submissionId: claim.submissionId,
6516
+ ownershipToken: markToken,
6517
+ recordId,
6518
+ sequence,
6519
+ }),
6520
+ );
6521
+ deliveredJoinInputs.add(claim.submissionId);
6522
+ joinedInputs.push({
6523
+ ...payload,
6524
+ rendered,
6525
+ });
6375
6526
  }
6376
6527
 
6377
6528
  return joinedInputs;
@@ -7306,8 +7457,8 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
7306
7457
  createdAt,
7307
7458
  ...(runScopedPrefixLength > 0 ? { runScopedPrefixLength } : {}),
7308
7459
  ...(completedRun === undefined ? {} : { runCompletion: completedRun }),
7309
- usage: stagedUsage.get(canonicalTurn),
7310
- unobservedModelCalls: stagedUnobservedCalls.get(canonicalTurn),
7460
+ usage: usageForCommit(canonicalTurn),
7461
+ unobservedModelCalls: unobservedForCommit(canonicalTurn),
7311
7462
  }),
7312
7463
  );
7313
7464
 
@@ -7373,6 +7524,11 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
7373
7524
 
7374
7525
  const handleEvent = (event: RunEvent): Effect.Effect<void, DurableWorkerFailure> => {
7375
7526
  switch (event._tag) {
7527
+ case "ModelRestarted": {
7528
+ // A finish part can arrive before the provider stream closes. Its TurnCompleted
7529
+ // only staged this disposable Turn; never let the next seam commit its prefix.
7530
+ return Ref.update(stateRef, (state) => ({ ...state, pendingTurn: undefined }));
7531
+ }
7376
7532
  case "TurnStarted": {
7377
7533
  // Suspension owns only this Turn's siblings. Earlier results are already canonical
7378
7534
  // under their original Turn and must never be re-recorded with a later Turn id.
@@ -8004,6 +8160,10 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
8004
8160
  definition: { ...registeredAgent.definition, policy: childPolicy },
8005
8161
  };
8006
8162
 
8163
+ // Restoring the accepted child policy changes no Tool declarations. Retain the
8164
+ // selected registration's contracts instead of resolving the copied Definition.
8165
+ const currentContracts = yield* contractsFor(registeredAgent.definition);
8166
+
8007
8167
  const evidence = yield* evidenceFor(records, submissionId, true, snapshot.hostSubmissionId);
8008
8168
  const knownIds = knownRecordIdsOf(records);
8009
8169
 
@@ -8099,7 +8259,7 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
8099
8259
  reconciliationRecords,
8100
8260
  reconciliationEvidence.openToolCalls,
8101
8261
  knownIds,
8102
- { definition: agent.definition, contracts: yield* contractsFor(agent.definition) },
8262
+ { definition: agent.definition, contracts: currentContracts },
8103
8263
  );
8104
8264
 
8105
8265
  if (review.uncertain.length > 0 || review.unproven.length > 0) {
@@ -8139,7 +8299,7 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
8139
8299
  if (continuation !== undefined) {
8140
8300
  const current = {
8141
8301
  definition: agent.definition,
8142
- contracts: yield* contractsFor(agent.definition),
8302
+ contracts: currentContracts,
8143
8303
  };
8144
8304
 
8145
8305
  const operations = operationsFor(continuationRecords, runIdForSubmission(submissionId));
@@ -8314,6 +8474,7 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
8314
8474
  journalMetadata?.snapshot(),
8315
8475
  lineage,
8316
8476
  approvalDecisionIntents,
8477
+ currentContracts,
8317
8478
  runTiming,
8318
8479
  yieldAfter,
8319
8480
  );
@@ -8759,9 +8920,6 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
8759
8920
  );
8760
8921
  });
8761
8922
 
8762
- const resolveCurrentBinding = (submission: SubmissionSnapshot) =>
8763
- resolveWorkerBinding(registeredBindings, submission.agentId);
8764
-
8765
8923
  const processThreadResolvedImpl = (
8766
8924
  threadId: ThreadId,
8767
8925
  ): Effect.Effect<ReadonlyArray<Settlement>, DurableWorkerFailure | DurableBindingFailure> =>
@@ -9011,7 +9169,7 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
9011
9169
  records,
9012
9170
  evidence.openToolCalls.filter((call) => markableIds.has(call.toolCallId)),
9013
9171
  knownIds,
9014
- yield* currentOperationsFor(submission.agentId),
9172
+ yield* currentOperationsFor(submission),
9015
9173
  );
9016
9174
 
9017
9175
  let disposition: "repaired" | "deferred" | "unknown";
@@ -9050,7 +9208,7 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
9050
9208
  if (!snapshot.unknownResolutions.some((intent) => intent.resolution._tag === "SafeToRetry"))
9051
9209
  return false;
9052
9210
  const submission = snapshot.submission;
9053
- const current = yield* currentOperationsFor(submission.agentId);
9211
+ const current = yield* currentOperationsFor(submission);
9054
9212
  const operations = operationsFor(records, runIdForSubmission(submission.submissionId));
9055
9213
 
9056
9214
  for (const intent of snapshot.unknownResolutions) {
@@ -10988,6 +11146,29 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
10988
11146
  }).pipe(Effect.provideService(WorkerRuntime, workerRuntime));
10989
11147
 
10990
11148
  return DurableAgentRuntime.of({
11149
+ bindingRegistryKey: yield* withCrypto(
11150
+ digestJson({
11151
+ selection: bindingSelection?.key ?? null,
11152
+ bindings: (yield* Effect.forEach(registeredBindings, (binding) =>
11153
+ withCrypto(
11154
+ digestJson({
11155
+ agentId: binding.agentId,
11156
+ agent: binding.digests.agent,
11157
+ model: binding.digests.model,
11158
+ tools: binding.digests.tools,
11159
+ replay:
11160
+ binding.digests.replay === undefined
11161
+ ? null
11162
+ : {
11163
+ agent: binding.digests.replay.agent,
11164
+ agentBehavior: binding.digests.replay.agentBehavior ?? null,
11165
+ tools: binding.digests.replay.tools,
11166
+ },
11167
+ }),
11168
+ ),
11169
+ ).pipe(Effect.orDie)).sort(),
11170
+ }),
11171
+ ).pipe(Effect.orDie),
10991
11172
  workerHost: workerRuntime.acquire,
10992
11173
  messagingHost: messagingRuntime.acquire,
10993
11174
  submitRegistered,
@@ -11079,6 +11260,8 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
11079
11260
  export class DurableAgentRuntime extends Context.Service<
11080
11261
  DurableAgentRuntime,
11081
11262
  {
11263
+ /** Stable registration identity for hosts parking unavailable bindings; never an admission authority. */
11264
+ readonly bindingRegistryKey: string;
11082
11265
  /** Acquire an authenticated source Thread facet; its References confer no authority. */
11083
11266
  readonly workerHost: (request: {
11084
11267
  readonly sourceThreadId: ThreadId;
@@ -11330,7 +11513,7 @@ export class DurableAgentRuntime extends Context.Service<
11330
11513
  >()("@effect-agent/thread/DurableAgentRuntime") {
11331
11514
  /**
11332
11515
  * Resolve typed registrations and capture their services once in the runtime's Layer Scope.
11333
- * Every claimed head must match an exact registered identity and digest triple. Worker calls
11516
+ * Every claimed head must select exactly one current registered Definition. Worker calls
11334
11517
  * cannot replace these registrations or their captured services. Tool authorization is required.
11335
11518
  */
11336
11519
  static layerRegistered<const Entries extends ReadonlyArray<AgentRegistration>>(