@yielded/agent 0.1.0-beta.173 → 0.1.0-beta.175

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 (266) hide show
  1. package/dist/{Admin-BX9ns28E.d.mts → Admin-Qa3I1oA5.d.mts} +61 -21
  2. package/dist/{Agent-9aBoIi74.d.mts → Agent-B39jRnFv.d.mts} +2 -2
  3. package/dist/{Agent-0WvBWWbM.mjs → Agent-BiRbr73L.mjs} +7 -6
  4. package/dist/Agent-BiRbr73L.mjs.map +1 -0
  5. package/dist/{AgentRuntime-BfP14534.d.mts → AgentRuntime-CHDyRbSf.d.mts} +12 -12
  6. package/dist/{AgentRuntime-CQ-qcETl.mjs → AgentRuntime-DaDKMasp.mjs} +290 -273
  7. package/dist/AgentRuntime-DaDKMasp.mjs.map +1 -0
  8. package/dist/{Digest-DCuPlhNk.mjs → Digest-C3EODOee.mjs} +42 -15
  9. package/dist/Digest-C3EODOee.mjs.map +1 -0
  10. package/dist/{DurableAgentRuntime-BAmSkWYr.mjs → DurableAgentRuntime-CPlw5_7e.mjs} +1557 -1458
  11. package/dist/DurableAgentRuntime-CPlw5_7e.mjs.map +1 -0
  12. package/dist/{EventSource-BwliwQz9.d.mts → EventSource-BTu68yDW.d.mts} +2 -2
  13. package/dist/InMemory.d.mts +2 -2
  14. package/dist/{MessageDelivery-B943ebI_.d.mts → MessageDelivery-DjAZJ4Fd.d.mts} +42 -42
  15. package/dist/{Messaging-C_6OOg_s.d.mts → Messaging-SVu6rWV8.d.mts} +12 -6
  16. package/dist/{MessagingHost-B8oKM7AC.d.mts → MessagingHost-C6SOAcoG.d.mts} +2 -2
  17. package/dist/{MessagingHost-CRUK4JBi.mjs → MessagingHost-CZCyrnvJ.mjs} +27 -26
  18. package/dist/MessagingHost-CZCyrnvJ.mjs.map +1 -0
  19. package/dist/{Output-Dsx6k52t.d.mts → Output-CRamN1Fy.d.mts} +2 -2
  20. package/dist/{PreparedInputAdmission-CxSJSiTl.d.mts → PreparedInputAdmission-wgY4JMZY.d.mts} +3 -3
  21. package/dist/{Records-BiuJDWNe.mjs → Records-DzL4Kbtw.mjs} +2 -2
  22. package/dist/{Records-BiuJDWNe.mjs.map → Records-DzL4Kbtw.mjs.map} +1 -1
  23. package/dist/{RunContinuation-FHM7XPjq.d.mts → RunContinuation-3jB5pULC.d.mts} +13 -7
  24. package/dist/{RunContinuation-DHYNAQFi.mjs → RunContinuation-BiyKJK32.mjs} +173 -82
  25. package/dist/RunContinuation-BiyKJK32.mjs.map +1 -0
  26. package/dist/{RunJournal-kfKVxJj7.mjs → RunJournal-CPMKcfd0.mjs} +106 -73
  27. package/dist/RunJournal-CPMKcfd0.mjs.map +1 -0
  28. package/dist/{RunOptions-D8zxFIrZ.d.mts → RunOptions-Be77lKVZ.d.mts} +2 -2
  29. package/dist/{RunStorage-uqPxps09.mjs → RunStorage-CzutHJBE.mjs} +10 -6
  30. package/dist/RunStorage-CzutHJBE.mjs.map +1 -0
  31. package/dist/{Scheduling-CVyfXlBR.mjs → Scheduling-07_dsGED.mjs} +12 -12
  32. package/dist/Scheduling-07_dsGED.mjs.map +1 -0
  33. package/dist/{Subagent-vpffemql.mjs → Subagent-CexndZSp.mjs} +53 -40
  34. package/dist/Subagent-CexndZSp.mjs.map +1 -0
  35. package/dist/{Subagent-E6tvd7-k.d.mts → Subagent-xhN_aD1r.d.mts} +20 -9
  36. package/dist/{SubagentHost-B6w2m-SI.d.mts → SubagentHost-D1xBgn9g.d.mts} +2 -2
  37. package/dist/{Subscription-DONig54Q.d.mts → Subscription-DmIHlOJK.d.mts} +2 -2
  38. package/dist/{SubscriptionInput-mJIRxWJM.d.mts → SubscriptionInput-DgcQhvbe.d.mts} +2 -2
  39. package/dist/{Thread-dk-QiNml.d.mts → Thread-DzdJAY4J.d.mts} +4 -3
  40. package/dist/{ThreadHistory-ByxS8HU1.d.mts → ThreadHistory-KPPBJLJb.d.mts} +12 -2
  41. package/dist/{ThreadImport-DckUMEqJ.mjs → ThreadImport-CjlG57vH.mjs} +9 -9
  42. package/dist/ThreadImport-CjlG57vH.mjs.map +1 -0
  43. package/dist/{ThreadImport-DpHxjBrA.d.mts → ThreadImport-DP-vOk2F.d.mts} +5 -5
  44. package/dist/{ThreadStore-CEekRa80.mjs → ThreadStore-EuUilthA.mjs} +162 -85
  45. package/dist/ThreadStore-EuUilthA.mjs.map +1 -0
  46. package/dist/{ThreadTransfer-B4gTQLIl.mjs → ThreadTransfer-DB_zsrtJ.mjs} +4 -4
  47. package/dist/{ThreadTransfer-B4gTQLIl.mjs.map → ThreadTransfer-DB_zsrtJ.mjs.map} +1 -1
  48. package/dist/{WorkerAdmission-DFVbbVh2.mjs → WorkerAdmission-eBeXt_mu.mjs} +5 -5
  49. package/dist/{WorkerAdmission-DFVbbVh2.mjs.map → WorkerAdmission-eBeXt_mu.mjs.map} +1 -1
  50. package/dist/{agent-registration-ByMZ_cK5.mjs → agent-registration-a6QYaG3h.mjs} +70 -57
  51. package/dist/agent-registration-a6QYaG3h.mjs.map +1 -0
  52. package/dist/capabilities/Approval.mjs +2 -2
  53. package/dist/capabilities/Approval.mjs.map +1 -1
  54. package/dist/capabilities/BrowserUse.d.mts +102 -1867
  55. package/dist/capabilities/BrowserUse.mjs +74 -798
  56. package/dist/capabilities/BrowserUse.mjs.map +1 -1
  57. package/dist/capabilities/Budget.mjs +3 -1
  58. package/dist/capabilities/Budget.mjs.map +1 -1
  59. package/dist/capabilities/CodeMode.d.mts +1 -1
  60. package/dist/capabilities/CodeMode.mjs +16 -9
  61. package/dist/capabilities/CodeMode.mjs.map +1 -1
  62. package/dist/capabilities/Commands.d.mts +1 -1
  63. package/dist/capabilities/McpClient.mjs +1 -1
  64. package/dist/capabilities/McpClient.mjs.map +1 -1
  65. package/dist/capabilities/Messaging.d.mts +1 -1
  66. package/dist/capabilities/Messaging.mjs +5 -5
  67. package/dist/capabilities/Messaging.mjs.map +1 -1
  68. package/dist/capabilities/ModelContext.d.mts +1 -1
  69. package/dist/capabilities/ModelContext.mjs +7 -5
  70. package/dist/capabilities/ModelContext.mjs.map +1 -1
  71. package/dist/capabilities/Redaction.mjs +49 -19
  72. package/dist/capabilities/Redaction.mjs.map +1 -1
  73. package/dist/capabilities/RunHooks.d.mts +2 -2
  74. package/dist/capabilities/RunHooks.mjs +1 -1
  75. package/dist/capabilities/RunHooks.mjs.map +1 -1
  76. package/dist/capabilities/Subagent.d.mts +1 -1
  77. package/dist/capabilities/Subagent.mjs +1 -1
  78. package/dist/capabilities/SubagentReservations.mjs +3 -1
  79. package/dist/capabilities/SubagentReservations.mjs.map +1 -1
  80. package/dist/capabilities/WebCapture.mjs.map +1 -1
  81. package/dist/capabilities/WebSearch.mjs +7 -7
  82. package/dist/capabilities/WebSearch.mjs.map +1 -1
  83. package/dist/core/Agent.d.mts +1 -1
  84. package/dist/core/Agent.mjs +1 -1
  85. package/dist/core/AgentUpdates.mjs +8 -8
  86. package/dist/core/AgentUpdates.mjs.map +1 -1
  87. package/dist/core/Thread.d.mts +2 -2
  88. package/dist/core/Thread.mjs +6 -4
  89. package/dist/core/Thread.mjs.map +1 -1
  90. package/dist/core/Usage.mjs +2 -2
  91. package/dist/core/Usage.mjs.map +1 -1
  92. package/dist/durable/ActivityStore.mjs +1 -1
  93. package/dist/durable/Admin.d.mts +1 -1
  94. package/dist/durable/Admin.mjs +1 -1
  95. package/dist/durable/AgentRegistration.d.mts +1 -1
  96. package/dist/durable/AgentRegistration.mjs +1 -1
  97. package/dist/durable/Certification.d.mts +1 -1
  98. package/dist/durable/CommittedActivity.d.mts +1 -1
  99. package/dist/durable/CommittedActivity.mjs +3 -3
  100. package/dist/durable/Digest.mjs +1 -1
  101. package/dist/durable/DurableAgentRuntime.d.mts +1 -1
  102. package/dist/durable/DurableAgentRuntime.mjs +2 -2
  103. package/dist/durable/EventSource.d.mts +1 -1
  104. package/dist/durable/EventSource.mjs +7 -7
  105. package/dist/durable/EventSource.mjs.map +1 -1
  106. package/dist/durable/GitHubWorkflowSource.d.mts +3 -3
  107. package/dist/durable/LifecyclePublication.d.mts +1 -1
  108. package/dist/durable/LifecyclePublication.mjs +2 -2
  109. package/dist/durable/MessageDelivery.d.mts +1 -1
  110. package/dist/durable/MessageDelivery.mjs +2 -2
  111. package/dist/durable/MessageDeliveryStoreConformance.d.mts +40 -40
  112. package/dist/durable/MessageDeliveryStoreConformance.mjs +2 -2
  113. package/dist/durable/PersistentHistory.d.mts +2 -2
  114. package/dist/durable/PersistentHistory.mjs +4 -4
  115. package/dist/durable/PreparedInputAdmission.d.mts +1 -1
  116. package/dist/durable/RecordFormat.mjs +3 -3
  117. package/dist/durable/RecordFormat.mjs.map +1 -1
  118. package/dist/durable/Records.mjs +1 -1
  119. package/dist/durable/Recovery.d.mts +1 -1
  120. package/dist/durable/Recovery.mjs +1 -1
  121. package/dist/durable/RunContinuation.d.mts +2 -2
  122. package/dist/durable/RunContinuation.mjs +3 -3
  123. package/dist/durable/RunJournal.d.mts +2 -2
  124. package/dist/durable/RunJournal.mjs +2 -2
  125. package/dist/durable/RunStorage.d.mts +1 -1
  126. package/dist/durable/RunStorage.mjs +1 -1
  127. package/dist/durable/Schedule.mjs +1 -1
  128. package/dist/durable/ScheduleStoreConformance.mjs +1 -1
  129. package/dist/durable/ScheduleTransition.mjs +1 -1
  130. package/dist/durable/Scheduling.d.mts +1 -1
  131. package/dist/durable/Scheduling.mjs +1 -1
  132. package/dist/durable/SettlementPublisher.d.mts +1 -1
  133. package/dist/durable/SettlementPublisher.mjs +3 -3
  134. package/dist/durable/SqlMemoryStore.mjs +5 -3
  135. package/dist/durable/SqlMemoryStore.mjs.map +1 -1
  136. package/dist/durable/SubmissionLedger.d.mts +1 -1
  137. package/dist/durable/SubmissionLedger.mjs +11 -3
  138. package/dist/durable/SubmissionLedger.mjs.map +1 -1
  139. package/dist/durable/SubmissionLedgerConformance.d.mts +1 -1
  140. package/dist/durable/SubmissionLedgerConformance.mjs +6 -6
  141. package/dist/durable/SubmissionStatus.d.mts +1 -1
  142. package/dist/durable/Subscription.d.mts +1 -1
  143. package/dist/durable/Subscription.mjs +2 -2
  144. package/dist/durable/SubscriptionInput.d.mts +1 -1
  145. package/dist/durable/SubscriptionInput.mjs +4 -4
  146. package/dist/durable/SubscriptionInput.mjs.map +1 -1
  147. package/dist/durable/SubscriptionStoreConformance.d.mts +1 -1
  148. package/dist/durable/SubscriptionStoreConformance.mjs +3 -3
  149. package/dist/durable/SubscriptionStoreConformance.mjs.map +1 -1
  150. package/dist/durable/SubscriptionTools.d.mts +2 -2
  151. package/dist/durable/SubscriptionTransition.d.mts +1 -1
  152. package/dist/durable/SubscriptionTransition.mjs +1 -1
  153. package/dist/durable/Subscriptions.d.mts +5 -5
  154. package/dist/durable/Subscriptions.mjs +3 -3
  155. package/dist/durable/Subscriptions.mjs.map +1 -1
  156. package/dist/durable/ThreadArchiveRange.d.mts +1 -1
  157. package/dist/durable/ThreadArchiveRange.mjs +1 -1
  158. package/dist/durable/ThreadContextHistory.d.mts +1 -1
  159. package/dist/durable/ThreadContextHistory.mjs +2 -2
  160. package/dist/durable/ThreadContextHistoryProjection.mjs +1 -1
  161. package/dist/durable/ThreadImport.d.mts +1 -1
  162. package/dist/durable/ThreadImport.mjs +1 -1
  163. package/dist/durable/ThreadInvariants.d.mts +3 -3
  164. package/dist/durable/ThreadInvariants.mjs +5 -5
  165. package/dist/durable/ThreadInvariants.mjs.map +1 -1
  166. package/dist/durable/ThreadProjection.mjs +1 -1
  167. package/dist/durable/ThreadProjectionMaintenance.d.mts +1 -1
  168. package/dist/durable/ThreadReader.d.mts +1 -1
  169. package/dist/durable/ThreadReader.mjs +1 -1
  170. package/dist/durable/ThreadStore.d.mts +1 -1
  171. package/dist/durable/ThreadStore.mjs +2 -2
  172. package/dist/durable/ThreadStoreConformance.d.mts +1 -1
  173. package/dist/durable/ThreadStoreConformance.mjs +4 -4
  174. package/dist/durable/ThreadTransfer.d.mts +1 -1
  175. package/dist/durable/ThreadTransfer.mjs +1 -1
  176. package/dist/durable/ThreadWork.d.mts +2 -2
  177. package/dist/durable/ThreadWork.mjs +12 -6
  178. package/dist/durable/ThreadWork.mjs.map +1 -1
  179. package/dist/durable/ToolReconciler.mjs +1 -1
  180. package/dist/durable/WakeScheduler.mjs +23 -39
  181. package/dist/durable/WakeScheduler.mjs.map +1 -1
  182. package/dist/durable/WorkerAdmission.d.mts +1 -1
  183. package/dist/durable/WorkerAdmission.mjs +1 -1
  184. package/dist/durable/WorkerHost.d.mts +2 -2
  185. package/dist/engine/AgentRuntime.d.mts +1 -1
  186. package/dist/engine/AgentRuntime.mjs +1 -1
  187. package/dist/engine/MessagingHost.d.mts +1 -1
  188. package/dist/engine/MessagingHost.mjs +1 -1
  189. package/dist/engine/Output.d.mts +1 -1
  190. package/dist/engine/Output.mjs +1 -1
  191. package/dist/engine/RunOptions.d.mts +1 -1
  192. package/dist/engine/SubagentHost.d.mts +1 -1
  193. package/dist/engine/ThreadHistory.d.mts +1 -1
  194. package/dist/engine/ThreadHistory.mjs +3 -2
  195. package/dist/engine/ThreadHistory.mjs.map +1 -1
  196. package/dist/index.d.mts +21 -21
  197. package/dist/index.mjs +16 -16
  198. package/dist/{output-contract-CMIhu4Cw.mjs → output-contract-DCnLYCdp.mjs} +12 -15
  199. package/dist/{output-contract-CMIhu4Cw.mjs.map → output-contract-DCnLYCdp.mjs.map} +1 -1
  200. package/dist/{run-context-reader-OcsZr4bk.d.mts → run-context-reader-DVhJZ1Qe.d.mts} +2 -2
  201. package/package.json +1 -1
  202. package/src/capabilities/Approval.ts +58 -60
  203. package/src/capabilities/BrowserUse.ts +89 -944
  204. package/src/capabilities/Budget.ts +8 -1
  205. package/src/capabilities/CodeMode.ts +57 -33
  206. package/src/capabilities/McpClient.ts +2 -2
  207. package/src/capabilities/Messaging.ts +51 -55
  208. package/src/capabilities/ModelContext.ts +5 -2
  209. package/src/capabilities/Redaction.ts +64 -34
  210. package/src/capabilities/RunHooks.ts +52 -55
  211. package/src/capabilities/Subagent.ts +33 -33
  212. package/src/capabilities/SubagentReservations.ts +9 -1
  213. package/src/capabilities/WebCapture.ts +6 -0
  214. package/src/capabilities/WebSearch.ts +10 -7
  215. package/src/capabilities/internal/subagent-background.ts +202 -158
  216. package/src/core/Agent.ts +11 -6
  217. package/src/core/AgentUpdates.ts +8 -8
  218. package/src/core/Thread.ts +142 -146
  219. package/src/core/Usage.ts +7 -8
  220. package/src/core/internal/agent-updates.ts +1 -0
  221. package/src/durable/DurableAgentRuntime.ts +1024 -829
  222. package/src/durable/EventSource.ts +15 -9
  223. package/src/durable/RecordFormat.ts +5 -3
  224. package/src/durable/RunContinuation.ts +316 -113
  225. package/src/durable/RunJournal.ts +106 -114
  226. package/src/durable/RunStorage.ts +129 -124
  227. package/src/durable/Scheduling.ts +639 -651
  228. package/src/durable/SqlMemoryStore.ts +8 -6
  229. package/src/durable/SubmissionLedger.ts +11 -0
  230. package/src/durable/SubscriptionInput.ts +5 -3
  231. package/src/durable/SubscriptionStoreConformance.ts +23 -23
  232. package/src/durable/Subscriptions.ts +18 -19
  233. package/src/durable/ThreadImport.ts +18 -19
  234. package/src/durable/ThreadInvariants.ts +2 -1
  235. package/src/durable/ThreadStore.ts +217 -111
  236. package/src/durable/ThreadWork.ts +11 -2
  237. package/src/durable/WakeScheduler.ts +34 -66
  238. package/src/durable/internal/StorageOwner.ts +10 -4
  239. package/src/durable/internal/agent-registration.ts +96 -89
  240. package/src/durable/internal/canonical-json.ts +61 -20
  241. package/src/durable/internal/prompt-history-cache.ts +13 -4
  242. package/src/durable/internal/record-encoding.ts +120 -64
  243. package/src/durable/internal/schedule-time.ts +6 -6
  244. package/src/durable/internal/usage-totals.ts +35 -0
  245. package/src/durable/internal/worker-host.ts +69 -74
  246. package/src/engine/MessagingHost.ts +4 -2
  247. package/src/engine/ThreadHistory.ts +47 -48
  248. package/src/engine/internal/agent-runtime.ts +1298 -1217
  249. package/src/engine/internal/output-contract.ts +15 -15
  250. package/src/engine/internal/primitive-delta.ts +2 -3
  251. package/src/engine/internal/provisional-text.ts +12 -10
  252. package/src/engine/internal/tool-exposure.ts +22 -18
  253. package/src/engine/internal/tool-telemetry.ts +15 -12
  254. package/dist/Agent-0WvBWWbM.mjs.map +0 -1
  255. package/dist/AgentRuntime-CQ-qcETl.mjs.map +0 -1
  256. package/dist/Digest-DCuPlhNk.mjs.map +0 -1
  257. package/dist/DurableAgentRuntime-BAmSkWYr.mjs.map +0 -1
  258. package/dist/MessagingHost-CRUK4JBi.mjs.map +0 -1
  259. package/dist/RunContinuation-DHYNAQFi.mjs.map +0 -1
  260. package/dist/RunJournal-kfKVxJj7.mjs.map +0 -1
  261. package/dist/RunStorage-uqPxps09.mjs.map +0 -1
  262. package/dist/Scheduling-CVyfXlBR.mjs.map +0 -1
  263. package/dist/Subagent-vpffemql.mjs.map +0 -1
  264. package/dist/ThreadImport-DckUMEqJ.mjs.map +0 -1
  265. package/dist/ThreadStore-CEekRa80.mjs.map +0 -1
  266. package/dist/agent-registration-ByMZ_cK5.mjs.map +0 -1
@@ -763,8 +763,8 @@ const acceptTurnCommit = <E, R>(
763
763
  Effect.andThen(() => durability?.commitTurn(commit) ?? Effect.succeed("committed" as const)),
764
764
  );
765
765
 
766
- return accepted.pipe(
767
- Effect.flatMap((receipt) => {
766
+ return Effect.onExitPrimitive(
767
+ Effect.flatMap(accepted, (receipt) => {
768
768
  if (receipt === "deferred")
769
769
  return commit._tag === "Response" && commit.defer === true
770
770
  ? Effect.void
@@ -774,21 +774,19 @@ const acceptTurnCommit = <E, R>(
774
774
  }),
775
775
  );
776
776
 
777
- return Effect.sync(() => {
778
- if (commit._tag !== "Partial") {
779
- if (commit.response !== undefined) {
780
- context.pendingCommitInputs.splice(0, trace.commitInputCount ?? 0);
781
- trace.responseCommitted = true;
782
- }
783
- if (commit._tag === "Settled") trace.resultsCommitted = true;
777
+ if (commit._tag !== "Partial") {
778
+ if (commit.response !== undefined) {
779
+ context.pendingCommitInputs.splice(0, trace.commitInputCount ?? 0);
780
+ trace.responseCommitted = true;
784
781
  }
785
- });
782
+ if (commit._tag === "Settled") trace.resultsCommitted = true;
783
+ }
784
+
785
+ return Effect.void;
786
786
  }),
787
- Effect.onExit((exit) =>
788
- Effect.sync(() => {
789
- if (Exit.isFailure(exit)) trace.commitFailed = true;
790
- }),
791
- ),
787
+ (exit) => {
788
+ if (Exit.isFailure(exit)) trace.commitFailed = true;
789
+ },
792
790
  );
793
791
  });
794
792
 
@@ -961,24 +959,20 @@ const schemaClassToolPartPreflight = <Tools extends Record<string, Tool.Any>>(
961
959
  try {
962
960
  if (part === null || typeof part !== "object") return undefined;
963
961
 
964
- const read = (key: string): unknown => {
962
+ const read = (key: string): PropertyDescriptor | undefined => {
965
963
  const descriptor = Object.getOwnPropertyDescriptor(part, key);
966
964
 
967
- if (descriptor === undefined || !("value" in descriptor)) {
968
- throw new TypeError(`response part ${key} must be an own data property`);
969
- }
970
-
971
- return descriptor.value;
965
+ return descriptor !== undefined && "value" in descriptor ? descriptor : undefined;
972
966
  };
973
967
 
974
- const type = read("type");
968
+ const type = read("type")?.value;
975
969
 
976
970
  if (type !== "tool-call" && type !== "tool-result") return undefined;
977
- const name = read("name");
971
+ const name = read("name")?.value;
978
972
 
979
973
  if (typeof name !== "string" || !hasTool(toolkit.tools, name)) return undefined;
980
974
  const tool = toolkit.tools[name];
981
- const payload = read(type === "tool-call" ? "params" : "result");
975
+ const payload: unknown = read(type === "tool-call" ? "params" : "result")?.value;
982
976
 
983
977
  if (payload === null || typeof payload !== "object") return undefined;
984
978
  const payloadPrototype = Object.getPrototypeOf(payload);
@@ -1007,7 +1001,10 @@ const schemaClassToolPartPreflight = <Tools extends Record<string, Tool.Any>>(
1007
1001
  ];
1008
1002
 
1009
1003
  for (const key of retainedKeys) {
1010
- preflight[key] = read(key);
1004
+ const descriptor = read(key);
1005
+
1006
+ if (descriptor === undefined) return undefined;
1007
+ preflight[key] = descriptor.value;
1011
1008
  }
1012
1009
 
1013
1010
  return preflight;
@@ -1170,11 +1167,14 @@ const captureModelResponsePartGeneral = Effect.fnUntraced(function* <
1170
1167
 
1171
1168
  const retainedBytes = yield* inspectModelResponsePartCapacity(usage, encoded, limits);
1172
1169
 
1170
+ if (typeof structuredCloneFunction !== "function") {
1171
+ return yield* ModelProtocolError.make({
1172
+ message: "Model response part could not be converted into engine-owned data",
1173
+ });
1174
+ }
1175
+
1173
1176
  const ownedEncoded = yield* Effect.try({
1174
1177
  try: () => {
1175
- if (typeof structuredCloneFunction !== "function") {
1176
- throw new TypeError("structuredClone is unavailable");
1177
- }
1178
1178
  const cloned: unknown = Reflect.apply(structuredCloneFunction, globalThis, [encoded]);
1179
1179
 
1180
1180
  return cloned;
@@ -1188,53 +1188,67 @@ const captureModelResponsePartGeneral = Effect.fnUntraced(function* <
1188
1188
  return { encodedPart: ownedEncoded, retainedBytes };
1189
1189
  });
1190
1190
 
1191
- const captureModelResponsePart = <Tools extends Record<string, Tool.Any>>(
1192
- part: unknown,
1193
- toolkit: Toolkit.Toolkit<Tools>,
1194
- usage: ModelResponseBufferUsage,
1195
- limits: EffectiveRunBufferLimits,
1196
- ): ReturnType<typeof captureModelResponsePartGeneral> =>
1197
- Effect.suspend((): ReturnType<typeof captureModelResponsePartGeneral> => {
1198
- const primitive =
1199
- usage.responsePartCount < limits.maxModelResponseParts
1200
- ? capturePrimitiveTextPart(part, limits.maxModelResponseBytes - usage.responsePartBytes)
1201
- : undefined;
1202
-
1203
- return primitive === undefined
1204
- ? captureModelResponsePartGeneral(part, toolkit, usage, limits)
1205
- : Effect.succeed(primitive);
1206
- });
1207
-
1208
1191
  interface OwnedModelResponsePart {
1209
1192
  readonly ownedPart: Response.AnyPart;
1210
1193
  readonly retainedBytes: number;
1211
1194
  }
1212
1195
 
1196
+ const decodePrimitiveModelResponsePart = Schema.decodeUnknownEffect(
1197
+ Schema.toType(
1198
+ Schema.Union([
1199
+ Response.TextStartPart,
1200
+ Response.TextDeltaPart,
1201
+ Response.TextEndPart,
1202
+ Response.ReasoningStartPart,
1203
+ Response.ReasoningDeltaPart,
1204
+ Response.ReasoningEndPart,
1205
+ ]),
1206
+ ),
1207
+ );
1208
+
1213
1209
  /** Reject trailing content before its capture can run an application codec or suspend. */
1214
- const ownModelResponsePart = Effect.fnUntraced(function* <Tools extends Record<string, Tool.Any>>(
1210
+ const ownModelResponsePart = <Tools extends Record<string, Tool.Any>>(
1215
1211
  part: unknown,
1216
1212
  toolkit: Toolkit.Toolkit<Tools>,
1217
1213
  usage: ModelResponseBufferUsage,
1218
1214
  limits: EffectiveRunBufferLimits,
1219
1215
  finished: boolean,
1220
- ): Effect.fn.Return<OwnedModelResponsePart, ModelProtocolError> {
1221
- if (finished) {
1222
- return yield* ModelProtocolError.make({
1223
- message: "Model response emitted content after its finish part",
1224
- });
1225
- }
1226
- const captured = yield* captureModelResponsePart(part, toolkit, usage, limits);
1216
+ ): Effect.Effect<OwnedModelResponsePart, ModelProtocolError> =>
1217
+ Effect.suspend((): Effect.Effect<OwnedModelResponsePart, ModelProtocolError> => {
1218
+ if (finished) {
1219
+ return Effect.fail(
1220
+ ModelProtocolError.make({
1221
+ message: "Model response emitted content after its finish part",
1222
+ }),
1223
+ );
1224
+ }
1227
1225
 
1228
- const ownedPart = yield* modelResponseParsersFor(toolkit)
1229
- .decode(captured.encodedPart)
1230
- .pipe(
1231
- Effect.mapError(() =>
1232
- ModelProtocolError.make({ message: "Model response part failed canonical decoding" }),
1233
- ),
1234
- );
1226
+ const primitive =
1227
+ usage.responsePartCount < limits.maxModelResponseParts
1228
+ ? capturePrimitiveTextPart(part, limits.maxModelResponseBytes - usage.responsePartBytes)
1229
+ : undefined;
1235
1230
 
1236
- return { ownedPart, retainedBytes: captured.retainedBytes };
1237
- });
1231
+ // Complete primitive snapshots need validation, but no defaults or transformation.
1232
+ // Schema.toType retains every constraint from the native Response part schemas.
1233
+ if (primitive !== undefined) {
1234
+ return Effect.mapBothEager(decodePrimitiveModelResponsePart(primitive.part), {
1235
+ onFailure: () =>
1236
+ ModelProtocolError.make({ message: "Model response part failed canonical decoding" }),
1237
+ onSuccess: (ownedPart) => ({ ownedPart, retainedBytes: primitive.retainedBytes }),
1238
+ });
1239
+ }
1240
+
1241
+ // Pending application codecs keep normal Effect execution in this same consumer fiber.
1242
+ return Effect.flatMapEager(
1243
+ captureModelResponsePartGeneral(part, toolkit, usage, limits),
1244
+ (captured) =>
1245
+ Effect.mapBothEager(modelResponseParsersFor(toolkit).decode(captured.encodedPart), {
1246
+ onFailure: () =>
1247
+ ModelProtocolError.make({ message: "Model response part failed canonical decoding" }),
1248
+ onSuccess: (ownedPart) => ({ ownedPart, retainedBytes: captured.retainedBytes }),
1249
+ }),
1250
+ );
1251
+ });
1238
1252
 
1239
1253
  type PartLifecycle = "open" | "closed";
1240
1254
 
@@ -1244,7 +1258,10 @@ interface PreparedToolCall<Tools extends Record<string, Tool.Any>> {
1244
1258
  readonly call: Response.ToolCallPart<string, Schema.Json>;
1245
1259
  readonly name: keyof Tools & string;
1246
1260
  readonly toolCallId: ToolCallId;
1247
- readonly validation: Result.Result<Tool.Parameters<ToolUnion<Tools>>, ToolParameterRejection>;
1261
+ readonly validation: Result.Result<
1262
+ Option.Option<Tool.Parameters<ToolUnion<Tools>>>,
1263
+ ToolParameterRejection
1264
+ >;
1248
1265
  readonly tool: ToolUnion<Tools>;
1249
1266
  readonly declarationIndex: number;
1250
1267
  }
@@ -1375,9 +1392,15 @@ const decodeToolCallParameters = <Tools extends Record<string, Tool.Any>>(
1375
1392
  );
1376
1393
  };
1377
1394
 
1395
+ /** Native identity proves encoding does not transform the already validated parameters. */
1396
+ const reusesValidatedToolParameters = (tool: Tool.Any): boolean =>
1397
+ (tool.needsApproval === undefined || tool.needsApproval === false) &&
1398
+ SchemaAST.toEncoded(tool.parametersSchema.ast) === tool.parametersSchema.ast;
1399
+
1378
1400
  /**
1379
- * Decode executable parameters for approval while retaining their encoded form for
1380
- * Toolkit.handle. A recorded rejection is a terminal failure, never an executable call.
1401
+ * Fresh responses and resumed batches validate every call before reaching this boundary.
1402
+ * Identity encodings with no approval need no second decoded value. Other codecs retain the
1403
+ * complete batch preflight, including validation of their canonical round trip, before dispatch.
1381
1404
  */
1382
1405
  const prepareToolCall = <Tools extends Record<string, Tool.Any>>(
1383
1406
  toolkit: Toolkit.WithHandler<Tools>,
@@ -1398,17 +1421,26 @@ const prepareToolCall = <Tools extends Record<string, Tool.Any>>(
1398
1421
  }
1399
1422
  const tool = toolkit.tools[name] as ToolUnion<Tools>;
1400
1423
 
1401
- return Effect.gen(function* () {
1402
- const toolCallId = yield* decodeToolCallId(call.id);
1424
+ return Effect.flatMap(decodeToolCallId(call.id), (toolCallId) => {
1425
+ const prepared = (
1426
+ validation: PreparedToolCall<Tools>["validation"],
1427
+ ): PreparedToolCall<Tools> => ({
1428
+ call,
1429
+ name,
1430
+ toolCallId,
1431
+ validation,
1432
+ tool,
1433
+ declarationIndex,
1434
+ });
1403
1435
 
1404
- const validation: PreparedToolCall<Tools>["validation"] =
1405
- rejection === undefined
1406
- ? Result.succeed(
1407
- yield* decodeToolCallParameters<Tools>(tool, call.name, copyJson(call.params)),
1408
- )
1409
- : Result.fail(rejection);
1436
+ if (rejection !== undefined) return Effect.succeed(prepared(Result.fail(rejection)));
1437
+ if (reusesValidatedToolParameters(tool))
1438
+ return Effect.succeed(prepared(Result.succeed(Option.none())));
1410
1439
 
1411
- return { call, name, toolCallId, validation, tool, declarationIndex };
1440
+ return Effect.map(
1441
+ decodeToolCallParameters<Tools>(tool, call.name, copyJson(call.params)),
1442
+ (parameters) => prepared(Result.succeed(Option.some(parameters))),
1443
+ );
1412
1444
  });
1413
1445
  };
1414
1446
 
@@ -1453,47 +1485,56 @@ const effectiveRunBounds = (
1453
1485
  maxToolCalls: boundedAllowance(policy.maxToolCalls, options.toolCallAllowance),
1454
1486
  });
1455
1487
 
1456
- const decodeResumedSettledCall = Effect.fnUntraced(function* (input: unknown) {
1457
- const raw = yield* Effect.try({
1458
- try: () => {
1459
- if (input === null || typeof input !== "object") {
1460
- throw new TypeError("settled Tool Call must be an object");
1461
- }
1462
-
1463
- const readOwnDataProperty = (key: "id" | "result" | "isFailure"): unknown => {
1464
- const descriptor = Object.getOwnPropertyDescriptor(input, key);
1465
-
1466
- if (descriptor === undefined || !("value" in descriptor)) {
1467
- throw new TypeError(`settled Tool Call ${key} must be an own data property`);
1468
- }
1488
+ const readSnapshotProperty = Effect.fnUntraced(function* (
1489
+ input: object,
1490
+ key: string,
1491
+ invalid: () => ModelProtocolError,
1492
+ optional = false,
1493
+ ): Effect.fn.Return<unknown, ModelProtocolError> {
1494
+ const descriptor = yield* Effect.try({
1495
+ try: () => Object.getOwnPropertyDescriptor(input, key),
1496
+ catch: invalid,
1497
+ });
1469
1498
 
1470
- return descriptor.value;
1471
- };
1499
+ if (descriptor === undefined && optional) return undefined;
1500
+ if (descriptor === undefined || !("value" in descriptor)) return yield* invalid();
1472
1501
 
1473
- const selection = Object.getOwnPropertyDescriptor(input, "toolSelection");
1502
+ return descriptor.value;
1503
+ });
1474
1504
 
1475
- if (selection !== undefined && !("value" in selection))
1476
- throw new TypeError("settled Tool selection must be an own data property");
1477
- const rejected = Object.getOwnPropertyDescriptor(input, "budgetRejected");
1505
+ const decodeResumedSettledCall = Effect.fnUntraced(function* (input: unknown) {
1506
+ const invalid = () =>
1507
+ ModelProtocolError.make({
1508
+ message: "Turn resume contains an invalid settled Tool Call",
1509
+ });
1478
1510
 
1479
- if (rejected !== undefined && !("value" in rejected)) {
1480
- throw new TypeError("settled Tool Call budgetRejected must be an own data property");
1481
- }
1511
+ if (input === null || typeof input !== "object") return yield* invalid();
1512
+ const id = yield* readSnapshotProperty(input, "id", invalid);
1513
+ const resultValue = yield* readSnapshotProperty(input, "result", invalid);
1514
+ const isFailure = yield* readSnapshotProperty(input, "isFailure", invalid);
1482
1515
 
1483
- return {
1484
- id: readOwnDataProperty("id"),
1485
- result: readOwnDataProperty("result"),
1486
- isFailure: readOwnDataProperty("isFailure"),
1487
- ...(rejected === undefined ? {} : { budgetRejected: rejected.value }),
1488
- ...(selection === undefined ? {} : { toolSelection: selection.value }),
1489
- };
1490
- },
1491
- catch: () =>
1492
- ModelProtocolError.make({
1493
- message: "Turn resume contains an invalid settled Tool Call",
1494
- }),
1516
+ const { selection, rejected } = yield* Effect.try({
1517
+ try: () => ({
1518
+ selection: Object.getOwnPropertyDescriptor(input, "toolSelection"),
1519
+ rejected: Object.getOwnPropertyDescriptor(input, "budgetRejected"),
1520
+ }),
1521
+ catch: invalid,
1495
1522
  });
1496
1523
 
1524
+ if (
1525
+ (selection !== undefined && !("value" in selection)) ||
1526
+ (rejected !== undefined && !("value" in rejected))
1527
+ )
1528
+ return yield* invalid();
1529
+
1530
+ const raw = {
1531
+ id,
1532
+ result: resultValue,
1533
+ isFailure,
1534
+ ...(rejected === undefined ? {} : { budgetRejected: rejected.value }),
1535
+ ...(selection === undefined ? {} : { toolSelection: selection.value }),
1536
+ };
1537
+
1497
1538
  const result = boundedCanonicalJsonSnapshot(raw.result, MAX_RESUMED_RESULT_BYTES);
1498
1539
 
1499
1540
  if (result === undefined) {
@@ -1514,106 +1555,73 @@ const decodeResumedSettledCall = Effect.fnUntraced(function* (input: unknown) {
1514
1555
  );
1515
1556
  });
1516
1557
 
1517
- const snapshotResumedSettledCalls = (resume: unknown, maximum: number) =>
1518
- Effect.try({
1519
- try: () => {
1520
- if (resume === null || typeof resume !== "object") {
1521
- throw new TypeError("Turn resume must be an object");
1522
- }
1523
- const settledDescriptor = Object.getOwnPropertyDescriptor(resume, "settled");
1524
-
1525
- if (settledDescriptor === undefined || !("value" in settledDescriptor)) {
1526
- throw new TypeError("Turn resume settled must be an own data property");
1527
- }
1528
- const settled = settledDescriptor.value;
1558
+ const snapshotResumedSettledCalls = Effect.fnUntraced(function* (resume: unknown, maximum: number) {
1559
+ const invalid = () =>
1560
+ ModelProtocolError.make({
1561
+ message: "Turn resume contains an invalid settled Tool Call collection",
1562
+ });
1529
1563
 
1530
- if (!Array.isArray(settled)) {
1531
- throw new TypeError("Turn resume settled must be an array");
1532
- }
1533
- const lengthDescriptor = Object.getOwnPropertyDescriptor(settled, "length");
1564
+ if (resume === null || typeof resume !== "object") return yield* invalid();
1565
+ const settled = yield* readSnapshotProperty(resume, "settled", invalid);
1566
+ const isArray = yield* Effect.try({ try: () => Array.isArray(settled), catch: invalid });
1534
1567
 
1535
- if (
1536
- lengthDescriptor === undefined ||
1537
- !("value" in lengthDescriptor) ||
1538
- !Number.isSafeInteger(lengthDescriptor.value) ||
1539
- lengthDescriptor.value < 0 ||
1540
- lengthDescriptor.value > maximum
1541
- ) {
1542
- throw new TypeError("Turn resume settled has an invalid length");
1543
- }
1544
- const snapshot: Array<unknown> = [];
1568
+ if (!isArray || settled === null || typeof settled !== "object") return yield* invalid();
1569
+ const length = yield* readSnapshotProperty(settled, "length", invalid);
1545
1570
 
1546
- for (let index = 0; index < lengthDescriptor.value; index += 1) {
1547
- const entryDescriptor = Object.getOwnPropertyDescriptor(settled, String(index));
1571
+ if (
1572
+ typeof length !== "number" ||
1573
+ !Number.isSafeInteger(length) ||
1574
+ length < 0 ||
1575
+ length > maximum
1576
+ ) {
1577
+ return yield* invalid();
1578
+ }
1579
+ const snapshot: Array<unknown> = [];
1548
1580
 
1549
- if (entryDescriptor === undefined || !("value" in entryDescriptor)) {
1550
- throw new TypeError("Turn resume settled entries must be own data properties");
1551
- }
1552
- snapshot.push(entryDescriptor.value);
1553
- }
1581
+ for (let index = 0; index < length; index += 1) {
1582
+ snapshot.push(yield* readSnapshotProperty(settled, String(index), invalid));
1583
+ }
1554
1584
 
1555
- return snapshot;
1556
- },
1557
- catch: () =>
1558
- ModelProtocolError.make({
1559
- message: "Turn resume contains an invalid settled Tool Call collection",
1560
- }),
1561
- });
1585
+ return snapshot;
1586
+ });
1562
1587
 
1563
1588
  const decodeResumeUsage = Effect.fnUntraced(function* (input: unknown) {
1564
- const snapshot = yield* Effect.try({
1565
- try: () => {
1566
- if (input === null || typeof input !== "object") {
1567
- throw new TypeError("Run resume usage must be an object");
1568
- }
1569
-
1570
- const read = (key: keyof RunResumeUsage, optional = false): unknown => {
1571
- const descriptor = Object.getOwnPropertyDescriptor(input, key);
1572
-
1573
- if (descriptor === undefined && optional) return undefined;
1574
- if (descriptor === undefined || !("value" in descriptor)) {
1575
- throw new TypeError(`Run resume usage ${key} must be an own data property`);
1576
- }
1589
+ const invalid = () =>
1590
+ ModelProtocolError.make({
1591
+ message:
1592
+ "Run resume usage requires own data properties with non-negative safe-integer totals and last-call tokens no greater than their cumulative totals",
1593
+ });
1577
1594
 
1578
- return descriptor.value;
1579
- };
1595
+ if (input === null || typeof input !== "object") return yield* invalid();
1596
+ const snapshot: Record<string, unknown> = {};
1580
1597
 
1581
- const optional = Object.fromEntries(
1582
- (
1583
- [
1584
- "usageStatus",
1585
- "pricingStatus",
1586
- "unobservedModelCalls",
1587
- "children",
1588
- "modelRestarts",
1589
- "webSearchCalls",
1590
- ] as const
1591
- )
1592
- .map((key) => [key, read(key, true)])
1593
- .filter(([, value]) => value !== undefined),
1594
- );
1598
+ for (const key of [
1599
+ "usageStatus",
1600
+ "pricingStatus",
1601
+ "unobservedModelCalls",
1602
+ "children",
1603
+ "modelRestarts",
1604
+ "webSearchCalls",
1605
+ ] satisfies ReadonlyArray<keyof RunResumeUsage>) {
1606
+ const value = yield* readSnapshotProperty(input, key, invalid, true);
1595
1607
 
1596
- return {
1597
- ...optional,
1598
- modelCalls: read("modelCalls"),
1599
- inputTokens: read("inputTokens"),
1600
- outputTokens: read("outputTokens"),
1601
- lastInputTokens: read("lastInputTokens"),
1602
- lastOutputTokens: read("lastOutputTokens"),
1603
- costMicrousd: read("costMicrousd"),
1604
- committedTurns: read("committedTurns"),
1605
- toolCalls: read("toolCalls"),
1606
- programmaticToolCalls: read("programmaticToolCalls"),
1607
- consecutiveToolFailures: read("consecutiveToolFailures"),
1608
- finalizationUsed: read("finalizationUsed"),
1609
- };
1610
- },
1611
- catch: () =>
1612
- ModelProtocolError.make({
1613
- message:
1614
- "Run resume usage requires own data properties with non-negative safe-integer totals and last-call tokens no greater than their cumulative totals",
1615
- }),
1616
- });
1608
+ if (value !== undefined) snapshot[key] = value;
1609
+ }
1610
+ for (const key of [
1611
+ "modelCalls",
1612
+ "inputTokens",
1613
+ "outputTokens",
1614
+ "lastInputTokens",
1615
+ "lastOutputTokens",
1616
+ "costMicrousd",
1617
+ "committedTurns",
1618
+ "toolCalls",
1619
+ "programmaticToolCalls",
1620
+ "consecutiveToolFailures",
1621
+ "finalizationUsed",
1622
+ ] satisfies ReadonlyArray<keyof RunResumeUsage>) {
1623
+ snapshot[key] = yield* readSnapshotProperty(input, key, invalid);
1624
+ }
1617
1625
 
1618
1626
  return yield* Schema.decodeUnknownEffect(RunResumeUsageSchema)(snapshot).pipe(
1619
1627
  Effect.mapError(() =>
@@ -1802,7 +1810,8 @@ const prepareApprovalRequest = <Tools extends Record<string, Tool.Any>>(
1802
1810
  context,
1803
1811
  Effect.gen(function* () {
1804
1812
  if (Result.isFailure(prepared.validation)) return undefined;
1805
- const decodedParams = prepared.validation.success;
1813
+ if (Option.isNone(prepared.validation.success)) return undefined;
1814
+ const decodedParams = prepared.validation.success.value;
1806
1815
  const approval = prepared.tool.needsApproval;
1807
1816
 
1808
1817
  const required =
@@ -2169,6 +2178,7 @@ const executePreparedToolCall = Effect.fnUntraced(function* <
2169
2178
  | AiError.AiError
2170
2179
  | Tool.HandlerError<ToolUnion<Tools>>;
2171
2180
  const call = prepared.call;
2181
+ const toolCallId = prepared.toolCallId;
2172
2182
  const observer = context.toolFailureObserver;
2173
2183
  const telemetryToolCallId = isTelemetryToolCallId(call.id) ? call.id : undefined;
2174
2184
  const executionClass = getToolExecutionClass(prepared.tool);
@@ -2208,8 +2218,6 @@ const executePreparedToolCall = Effect.fnUntraced(function* <
2208
2218
  const started = Result.isFailure(prepared.validation)
2209
2219
  ? Effect.void
2210
2220
  : Effect.gen(function* () {
2211
- const toolCallId = yield* decodeToolCallId(call.id);
2212
-
2213
2221
  yield* Effect.logDebug("agent tool handler started").pipe(
2214
2222
  Effect.annotateLogs({
2215
2223
  agentId: context.agentId,
@@ -2268,7 +2276,6 @@ const executePreparedToolCall = Effect.fnUntraced(function* <
2268
2276
  return yield* ModelProtocolError.make({
2269
2277
  message: `Tool Call ${call.id} produced more than one terminal result`,
2270
2278
  });
2271
- const toolCallId = yield* decodeToolCallId(call.id);
2272
2279
  const encodedResult = toolResultForJson(prepared.tool, result);
2273
2280
 
2274
2281
  if (result.preliminary) {
@@ -2305,7 +2312,6 @@ const executePreparedToolCall = Effect.fnUntraced(function* <
2305
2312
  message: `Tool Call ${call.id} completed without a terminal result`,
2306
2313
  });
2307
2314
  const result = terminalResult;
2308
- const toolCallId = yield* decodeToolCallId(call.id);
2309
2315
  let toolSelection: Selection | undefined;
2310
2316
 
2311
2317
  if (!result.isFailure && Context.get(prepared.tool.annotations, DiscoveryTool)) {
@@ -2321,7 +2327,7 @@ const executePreparedToolCall = Effect.fnUntraced(function* <
2321
2327
  context.toolSchemaTransformer,
2322
2328
  );
2323
2329
  }
2324
- const encodedResult = boundEncodedToolResult(result.encodedResult, resultBounds);
2330
+ const encodedResult = yield* boundEncodedToolResult(result.encodedResult, resultBounds);
2325
2331
 
2326
2332
  // The ordinary JSON boundary remains required; only the public Class projection is optional.
2327
2333
  const successResult = result.isFailure
@@ -2392,22 +2398,21 @@ const executePreparedToolCall = Effect.fnUntraced(function* <
2392
2398
  if (result.isFailure) return yield* failureMarker();
2393
2399
  });
2394
2400
 
2395
- const failTerminalResult = (cause: Cause.Cause<ToolExecutionError>) =>
2396
- Effect.gen(function* () {
2397
- terminal = true;
2398
- terminalResult = undefined;
2399
- propagatedFailure = cause;
2400
- // The raw failure has no canonical success/result, matching the prior propagated path.
2401
- yield* decodeToolCallId(call.id);
2402
- trace.finalToolResultIds.add(call.id);
2403
- terminalResultCommitted = true;
2404
- yield* terminalTelemetry("failure", "propagated");
2405
- yield* publishEvent(context, () =>
2406
- makeToolFailedEvent(context, turnId, call, Cause.squash(cause), "propagated"),
2407
- );
2401
+ const failTerminalResult = Effect.fnUntraced(function* (cause: Cause.Cause<ToolExecutionError>) {
2402
+ terminal = true;
2403
+ terminalResult = undefined;
2404
+ propagatedFailure = cause;
2405
+ // The raw failure has no canonical success/result, matching the prior propagated path.
2406
+ yield* decodeToolCallId(call.id);
2407
+ trace.finalToolResultIds.add(call.id);
2408
+ terminalResultCommitted = true;
2409
+ yield* terminalTelemetry("failure", "propagated");
2410
+ yield* publishEvent(context, () =>
2411
+ makeToolFailedEvent(context, turnId, call, Cause.squash(cause), "propagated"),
2412
+ );
2408
2413
 
2409
- return yield* failureMarker();
2410
- });
2414
+ return yield* failureMarker();
2415
+ });
2411
2416
 
2412
2417
  const measured = started.pipe(
2413
2418
  Effect.andThen(results),
@@ -2491,6 +2496,7 @@ const executeToolBatch = Effect.fnUntraced(function* <
2491
2496
  | AgentUpdateAcceptance
2492
2497
  | ToolSpanTelemetry
2493
2498
  | ProgrammaticToolAuthorization
2499
+ | Scope.Scope
2494
2500
  | Tool.HandlerServices<ToolUnion<Tools>>
2495
2501
  > {
2496
2502
  const exposed = context.toolExposure?.exposedToolNames;
@@ -2513,22 +2519,38 @@ const executeToolBatch = Effect.fnUntraced(function* <
2513
2519
  message: "Tool batch calls a Tool outside its request exposure or current visibility",
2514
2520
  });
2515
2521
 
2516
- const prepared = yield* prepareWithinDeadline(
2517
- context,
2518
- Effect.forEach(
2519
- calls.flatMap((call, declarationIndex) =>
2520
- settledCallIds?.has(call.id) ? [] : [{ call, declarationIndex }],
2521
- ),
2522
- ({ call, declarationIndex }) =>
2523
- prepareToolCall(
2524
- toolkit,
2525
- call,
2526
- declarationIndex,
2527
- trace.toolParameterRejections.get(call.id),
2528
- ),
2522
+ const prepareCalls = Effect.forEach(
2523
+ calls.flatMap((call, declarationIndex) =>
2524
+ settledCallIds?.has(call.id) ? [] : [{ call, declarationIndex }],
2529
2525
  ),
2526
+ ({ call, declarationIndex }) =>
2527
+ prepareToolCall(toolkit, call, declarationIndex, trace.toolParameterRejections.get(call.id)),
2528
+ );
2529
+
2530
+ const needsParameterDecoding = calls.some(
2531
+ (call) =>
2532
+ !settledCallIds?.has(call.id) &&
2533
+ !trace.toolParameterRejections.has(call.id) &&
2534
+ hasTool(toolkit.tools, call.name) &&
2535
+ !reusesValidatedToolParameters(toolkit.tools[call.name]),
2530
2536
  );
2531
2537
 
2538
+ const prepared = yield* needsParameterDecoding
2539
+ ? prepareWithinDeadline(context, prepareCalls)
2540
+ : Effect.flatMap(
2541
+ Clock.currentTimeMillis,
2542
+ (
2543
+ now,
2544
+ ): Effect.Effect<
2545
+ Array<PreparedToolCall<Tools>>,
2546
+ ModelProtocolError | AgentPolicyError,
2547
+ Tool.HandlerServices<ToolUnion<Tools>>
2548
+ > =>
2549
+ context.durationDeadlineMillis - now <= 0
2550
+ ? Effect.fail(context.durationFailure)
2551
+ : prepareCalls,
2552
+ );
2553
+
2532
2554
  if (
2533
2555
  prepared.some((call) => Context.get(call.tool.annotations, ContextRolloverTool)) &&
2534
2556
  calls.length !== 1
@@ -2692,7 +2714,7 @@ const executeToolBatch = Effect.fnUntraced(function* <
2692
2714
  "SubagentInterrupted",
2693
2715
  "SubagentJoined",
2694
2716
  ].includes(payload._tag)
2695
- ? Effect.fail(new RunEventSinkClosedError({ message: "Unsupported Tool event payload" }))
2717
+ ? Effect.fail(RunEventSinkClosedError.make({ message: "Unsupported Tool event payload" }))
2696
2718
  : emitPayload(payload),
2697
2719
  };
2698
2720
 
@@ -2702,28 +2724,28 @@ const executeToolBatch = Effect.fnUntraced(function* <
2702
2724
  emit: (request) =>
2703
2725
  context.updatePermits.withPermit(
2704
2726
  Effect.gen(function* () {
2705
- if (!updatesOpen) return yield* new UpdateError({ reason: "unavailable" });
2727
+ if (!updatesOpen) return yield* UpdateError.make({ reason: "unavailable" });
2706
2728
  if (
2707
2729
  request.target.id !== context.agentId ||
2708
2730
  request.target.updates !== context.definition.updates ||
2709
2731
  context.definition.updates === undefined
2710
2732
  )
2711
- return yield* new UpdateError({ reason: "identity" });
2733
+ return yield* UpdateError.make({ reason: "identity" });
2712
2734
 
2713
2735
  const updateId = yield* Schema.decodeEffect(IdempotencyKey)(request.updateId).pipe(
2714
- Effect.mapError(() => new UpdateError({ reason: "validation" })),
2736
+ Effect.mapError(() => UpdateError.make({ reason: "validation" })),
2715
2737
  );
2716
2738
 
2717
2739
  const value = yield* Schema.decodeEffect(Schema.fromJsonString(Schema.Json))(
2718
2740
  JSON.stringify(request.value),
2719
- ).pipe(Effect.mapError(() => new UpdateError({ reason: "validation" })));
2741
+ ).pipe(Effect.mapError(() => UpdateError.make({ reason: "validation" })));
2720
2742
 
2721
2743
  yield* context.validateUpdate(value);
2722
2744
  const existing = context.updates.get(updateId);
2723
2745
 
2724
2746
  if (existing !== undefined) {
2725
2747
  if (!Schema.toEquivalence(Schema.Json)(existing.value, value))
2726
- return yield* new UpdateError({ reason: "conflict" });
2748
+ return yield* UpdateError.make({ reason: "conflict" });
2727
2749
 
2728
2750
  return existing;
2729
2751
  }
@@ -2739,11 +2761,11 @@ const executeToolBatch = Effect.fnUntraced(function* <
2739
2761
  context.updates.size >= maxCount ||
2740
2762
  context.updateBytes + bytes > maxBytes
2741
2763
  )
2742
- return yield* new UpdateError({ reason: "capacity" });
2764
+ return yield* UpdateError.make({ reason: "capacity" });
2743
2765
 
2744
2766
  const snapshot = boundedCanonicalJsonSnapshot(value, bytes);
2745
2767
 
2746
- if (snapshot === undefined) return yield* new UpdateError({ reason: "validation" });
2768
+ if (snapshot === undefined) return yield* UpdateError.make({ reason: "validation" });
2747
2769
 
2748
2770
  const accepted = yield* updateAcceptance.accept(
2749
2771
  Object.freeze(
@@ -2760,7 +2782,7 @@ const executeToolBatch = Effect.fnUntraced(function* <
2760
2782
  );
2761
2783
 
2762
2784
  const validated = yield* Schema.decodeEffect(Update)(accepted).pipe(
2763
- Effect.mapError(() => new UpdateError({ reason: "validation" })),
2785
+ Effect.mapError(() => UpdateError.make({ reason: "validation" })),
2764
2786
  );
2765
2787
 
2766
2788
  if (
@@ -2770,7 +2792,7 @@ const executeToolBatch = Effect.fnUntraced(function* <
2770
2792
  validated.updateId !== request.updateId ||
2771
2793
  !Schema.toEquivalence(Schema.Json)(validated.value, value)
2772
2794
  )
2773
- return yield* new UpdateError({ reason: "identity" });
2795
+ return yield* UpdateError.make({ reason: "identity" });
2774
2796
 
2775
2797
  // Keep acknowledgements, replay, and retries on the same owned value even when
2776
2798
  // the accepting host retains its own mutable Update instance.
@@ -2780,7 +2802,7 @@ const executeToolBatch = Effect.fnUntraced(function* <
2780
2802
  context.updateBytes += bytes;
2781
2803
 
2782
2804
  yield* emitPayload({ _tag: "AgentUpdateEmitted", update }).pipe(
2783
- Effect.mapError(() => new UpdateError({ reason: "unavailable" })),
2805
+ Effect.mapError(() => UpdateError.make({ reason: "unavailable" })),
2784
2806
  );
2785
2807
 
2786
2808
  return update;
@@ -2788,9 +2810,9 @@ const executeToolBatch = Effect.fnUntraced(function* <
2788
2810
  ),
2789
2811
  };
2790
2812
 
2791
- // One finite permit owner; a call's own fiber fully exits before the supervisor releases it.
2792
- // The extra scoped child is required even for a singleton group: it closes handler descendants
2793
- // before another group can enter and retains cleanup failures in that call's Cause.
2813
+ // The permit covers the child's complete lifetime, including descendant cleanup. The call
2814
+ // owns its resource Scope and its fiber belongs to the enclosing Turn Scope. The native bracket
2815
+ // joins or interrupts it before releasing the permit, without another supervisor Scope.
2794
2816
  const permits = yield* Semaphore.make(concurrency);
2795
2817
 
2796
2818
  const handlers = Effect.gen(function* () {
@@ -2799,9 +2821,9 @@ const executeToolBatch = Effect.fnUntraced(function* <
2799
2821
  group,
2800
2822
  (call) =>
2801
2823
  permits.withPermit(
2802
- Effect.scoped(
2803
- Effect.gen(function* () {
2804
- const callBody = Effect.scoped(
2824
+ Effect.acquireUseRelease(
2825
+ Effect.forkScoped(
2826
+ Effect.scoped(
2805
2827
  Effect.gen(function* () {
2806
2828
  yield* checkpointExecution(context, durability);
2807
2829
  const subagentHost = yield* SubagentHost.forTool;
@@ -2827,6 +2849,11 @@ const executeToolBatch = Effect.fnUntraced(function* <
2827
2849
  hookServices,
2828
2850
  });
2829
2851
 
2852
+ const step = stepServiceFor(call);
2853
+ const subagent = subagentHost(source);
2854
+ const messaging = messagingHost(source);
2855
+ const catalog = { entries: context.toolCatalog };
2856
+
2830
2857
  return yield* executePreparedToolCall(
2831
2858
  context,
2832
2859
  turnId,
@@ -2838,30 +2865,52 @@ const executeToolBatch = Effect.fnUntraced(function* <
2838
2865
  waitingByDeclaration.set(call.declarationIndex, waiting);
2839
2866
  },
2840
2867
  ).pipe(
2841
- Effect.provideService(DurableStep, stepServiceFor(call)),
2842
- Effect.provideService(SubagentHost, subagentHost(source)),
2843
- Effect.provideService(MessagingHost, messagingHost(source)),
2844
- Effect.provideService(ToolBroker, broker.service),
2845
- Effect.provideService(CurrentToolCatalog, { entries: context.toolCatalog }),
2868
+ // Match the original providers' outer-to-inner order with one restoration.
2869
+ Effect.updateContext(
2870
+ (
2871
+ services: Context.Context<
2872
+ ToolSpanTelemetry | Tool.HandlerServices<ToolUnion<Tools>>
2873
+ >,
2874
+ ) =>
2875
+ services.pipe(
2876
+ Context.add(CurrentToolCatalog, catalog),
2877
+ Context.add(ToolBroker, broker.service),
2878
+ Context.add(MessagingHost, messaging),
2879
+ Context.add(SubagentHost, subagent),
2880
+ Context.add(DurableStep, step),
2881
+ ),
2882
+ ),
2846
2883
  Effect.ensuring(Effect.sync(() => broker.close())),
2847
2884
  );
2848
2885
  }),
2849
- );
2850
-
2851
- const fiber = yield* Effect.forkScoped(callBody);
2852
-
2853
- return yield* Fiber.join(fiber);
2854
- }),
2886
+ ),
2887
+ ),
2888
+ Fiber.join,
2889
+ Fiber.interrupt,
2855
2890
  ),
2856
2891
  ),
2857
- { concurrency: "unbounded", discard: true },
2892
+ { concurrency: group.length === 1 ? 1 : "unbounded", discard: true },
2858
2893
  );
2859
- yield* checkpointExecution(context, durability);
2894
+ // Each following call checks before dispatch; the batch owner checks again
2895
+ // after the final group, its cleanup, and the optional budget guard.
2860
2896
  }
2861
2897
  }).pipe(
2862
- Effect.provideService(RunEventSink, batchSink),
2863
- Effect.provideService(Emitter, updateEmitter),
2864
- Effect.provideService(SubagentDurability, batchSubagentDurability),
2898
+ Effect.updateContext(
2899
+ (
2900
+ services: Context.Context<
2901
+ | HookRequirements
2902
+ | ToolSpanTelemetry
2903
+ | ProgrammaticToolAuthorization
2904
+ | Scope.Scope
2905
+ | Tool.HandlerServices<ToolUnion<Tools>>
2906
+ >,
2907
+ ) =>
2908
+ services.pipe(
2909
+ Context.add(SubagentDurability, batchSubagentDurability),
2910
+ Context.add(Emitter, updateEmitter),
2911
+ Context.add(RunEventSink, batchSink),
2912
+ ),
2913
+ ),
2865
2914
  Effect.ensuring(
2866
2915
  Effect.sync(() => {
2867
2916
  updatesOpen = false;
@@ -3118,13 +3167,17 @@ const toolResultForJson = (tool: Tool.Any, result: Tool.HandlerResult<Tool.Any>)
3118
3167
  return result.encodedResult;
3119
3168
  };
3120
3169
 
3170
+ const decodeBoundedToolResult = Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown));
3171
+
3121
3172
  /**
3122
3173
  * RUN-022: bound one application Tool result at the settle seam. The bounded
3123
3174
  * value is what official history, the settled record, and the success event
3124
- * all carry, so every downstream surface agrees; a within-bounds result
3125
- * passes through with its identity preserved.
3175
+ * all carry, so every downstream surface agrees on the measured representation.
3126
3176
  */
3127
- const boundEncodedToolResult = (encodedResult: unknown, bounds: ToolResultBounds): unknown => {
3177
+ const boundEncodedToolResult = (
3178
+ encodedResult: unknown,
3179
+ bounds: ToolResultBounds,
3180
+ ): Effect.Effect<unknown> => {
3128
3181
  let text: string | undefined;
3129
3182
 
3130
3183
  try {
@@ -3132,10 +3185,10 @@ const boundEncodedToolResult = (encodedResult: unknown, bounds: ToolResultBounds
3132
3185
  } catch (cause) {
3133
3186
  // Fail closed (runtime spec §9): an unserializable result is unbounded by
3134
3187
  // construction, so the sentinel replaces it before history or records.
3135
- return unserializableToolResult(cause);
3188
+ return Effect.succeed(unserializableToolResult(cause));
3136
3189
  }
3137
3190
  if (text === undefined) {
3138
- return unserializableToolResult("the encoded result is not a JSON value");
3191
+ return Effect.succeed(unserializableToolResult("the encoded result is not a JSON value"));
3139
3192
  }
3140
3193
  const bounded = applyToolResultBounds(text, bounds);
3141
3194
 
@@ -3143,12 +3196,8 @@ const boundEncodedToolResult = (encodedResult: unknown, bounds: ToolResultBounds
3143
3196
  // truncated and unmodified cases: returning the original object would let a
3144
3197
  // stateful `toJSON` pass the byte check small and expand or throw on later
3145
3198
  // canonical serialization, carrying unchecked state past the boundary.
3146
- try {
3147
- return Schema.decodeSync(Schema.fromJsonString(Schema.Unknown))(bounded);
3148
- } catch (cause) {
3149
- // Defensive only: `applyToolResultBounds` always emits valid JSON.
3150
- return unserializableToolResult(cause);
3151
- }
3199
+ // Stay in the caller's Effect; a synchronous decoder would start another runtime fiber.
3200
+ return decodeBoundedToolResult(bounded).pipe(Effect.orElseSucceed(unserializableToolResult));
3152
3201
  };
3153
3202
 
3154
3203
  const durableResultBounds = <E, R>(
@@ -3337,7 +3386,7 @@ const consumeUsage = <AgentValue extends Agent.Any, HookError, HookRequirements>
3337
3386
  // them separately. Separate that overlap before constructing the canonical,
3338
3387
  // additive input components, but retain the raw provider usage for pricing.
3339
3388
  const reportedInputTotal = providerUsage.inputTokens.total;
3340
- const reportedInputWithoutWrite = yield* decodeProviderUsageTotal(reportedUncached + cacheRead);
3389
+ const reportedInputWithoutWrite = reportedUncached + cacheRead;
3341
3390
 
3342
3391
  const cacheWriteOverlapsUncached =
3343
3392
  reportedInputTotal !== undefined &&
@@ -3350,9 +3399,7 @@ const consumeUsage = <AgentValue extends Agent.Any, HookError, HookRequirements>
3350
3399
  const disjointReportedUncached =
3351
3400
  reportedUncached - (cacheWriteOverlapsUncached ? cacheWrite : 0);
3352
3401
 
3353
- const reportedInputComponents = yield* decodeProviderUsageTotal(
3354
- disjointReportedUncached + cacheRead + cacheWrite,
3355
- );
3402
+ const reportedInputComponents = disjointReportedUncached + cacheRead + cacheWrite;
3356
3403
 
3357
3404
  const allInputComponentsReported =
3358
3405
  providerUsage.inputTokens.uncached !== undefined &&
@@ -3367,7 +3414,7 @@ const consumeUsage = <AgentValue extends Agent.Any, HookError, HookRequirements>
3367
3414
  return yield* invalidProviderUsage();
3368
3415
  }
3369
3416
  const inputTokens = reportedInputTotal ?? reportedInputComponents;
3370
- const reportedOutputComponents = yield* decodeProviderUsageTotal(reportedText + reasoning);
3417
+ const reportedOutputComponents = reportedText + reasoning;
3371
3418
  const reportedOutputTotal = providerUsage.outputTokens.total;
3372
3419
 
3373
3420
  const allOutputComponentsReported =
@@ -3418,6 +3465,20 @@ const consumeUsage = <AgentValue extends Agent.Any, HookError, HookRequirements>
3418
3465
  ? outputRemainder
3419
3466
  : 0);
3420
3467
 
3468
+ // Validate the complete additive components before pricing or staging usage.
3469
+ const inputUsage = yield* InputTokenUsage.makeEffect({
3470
+ total: inputTokens,
3471
+ uncached,
3472
+ cacheRead: normalizedCacheRead,
3473
+ cacheWrite: normalizedCacheWrite,
3474
+ }).pipe(Effect.mapError(() => invalidProviderUsage()));
3475
+
3476
+ const outputUsage = yield* OutputTokenUsage.makeEffect({
3477
+ total: outputTokens,
3478
+ text,
3479
+ reasoning: normalizedReasoning,
3480
+ }).pipe(Effect.mapError(() => invalidProviderUsage()));
3481
+
3421
3482
  const totalTokens = yield* decodeProviderUsageTotal(inputTokens + outputTokens);
3422
3483
  const provider = yield* Model.ProviderName;
3423
3484
  const model = yield* Model.ModelName;
@@ -3485,17 +3546,8 @@ const consumeUsage = <AgentValue extends Agent.Any, HookError, HookRequirements>
3485
3546
  pricingStatus,
3486
3547
  ...(serviceTier === undefined ? {} : { serviceTier }),
3487
3548
  ...(pricingVersion === undefined ? {} : { pricingVersion }),
3488
- inputTokens: InputTokenUsage.make({
3489
- total: inputTokens,
3490
- uncached,
3491
- cacheRead: normalizedCacheRead,
3492
- cacheWrite: normalizedCacheWrite,
3493
- }),
3494
- outputTokens: OutputTokenUsage.make({
3495
- total: outputTokens,
3496
- text,
3497
- reasoning: normalizedReasoning,
3498
- }),
3549
+ inputTokens: inputUsage,
3550
+ outputTokens: outputUsage,
3499
3551
  costMicrousd,
3500
3552
  });
3501
3553
 
@@ -4127,76 +4179,75 @@ const compactContext = <AgentValue extends Agent.Any, HookError, HookRequirement
4127
4179
  const textParts = new Map<string, PartLifecycle>();
4128
4180
  const reasoningParts = new Map<string, PartLifecycle>();
4129
4181
 
4130
- const consumeSummaryPart = (owned: OwnedModelResponsePart) =>
4131
- Effect.gen(function* () {
4132
- responseUsage.responsePartCount++;
4133
- responseUsage.responsePartBytes += owned.retainedBytes;
4134
- const ownedPart = owned.ownedPart;
4135
-
4136
- if (ownedPart.type === "text-delta") {
4137
- pieces.push(ownedPart.delta);
4138
- } else if (ownedPart.type === "finish") {
4139
- if (summaryUsage === undefined) {
4140
- summaryUsage = ownedPart.usage;
4141
- summaryFinishMetadata = ownedPart.metadata;
4142
- }
4143
- } else if (ownedPart.type === "response-metadata") {
4144
- summaryResponse = yield* responseIdentity(ownedPart, summaryResponse);
4182
+ const consumeSummaryPart = Effect.fnUntraced(function* (owned: OwnedModelResponsePart) {
4183
+ responseUsage.responsePartCount++;
4184
+ responseUsage.responsePartBytes += owned.retainedBytes;
4185
+ const ownedPart = owned.ownedPart;
4186
+
4187
+ if (ownedPart.type === "text-delta") {
4188
+ pieces.push(ownedPart.delta);
4189
+ } else if (ownedPart.type === "finish") {
4190
+ if (summaryUsage === undefined) {
4191
+ summaryUsage = ownedPart.usage;
4192
+ summaryFinishMetadata = ownedPart.metadata;
4145
4193
  }
4146
- // Drain malformed responses within the buffer bounds so reported usage is charged.
4147
- yield* Effect.gen(function* () {
4148
- switch (ownedPart.type) {
4149
- case "text-start":
4150
- return yield* startPart(textParts, ownedPart.id, "compaction text");
4151
- case "text-delta":
4152
- return yield* continuePart(textParts, ownedPart.id, "compaction text delta");
4153
- case "text-end":
4154
- return yield* endPart(textParts, ownedPart.id, "compaction text");
4155
- case "reasoning-start":
4156
- return yield* startPart(reasoningParts, ownedPart.id, "compaction reasoning");
4157
- case "reasoning-delta":
4158
- return yield* continuePart(
4159
- reasoningParts,
4160
- ownedPart.id,
4161
- "compaction reasoning delta",
4162
- );
4163
- case "reasoning-end":
4164
- return yield* endPart(reasoningParts, ownedPart.id, "compaction reasoning");
4165
- case "finish": {
4166
- summaryFinished = true;
4167
- if (
4168
- ownedPart.reason !== "stop" ||
4169
- [...textParts.values(), ...reasoningParts.values()].includes("open")
4170
- ) {
4171
- return yield* ModelProtocolError.make({
4172
- message:
4173
- "Compaction response did not finish with complete text and a stop reason",
4174
- });
4175
- }
4176
-
4177
- return;
4178
- }
4179
- case "tool-params-start":
4180
- case "tool-params-delta":
4181
- case "tool-params-end":
4182
- case "tool-approval-request":
4183
- case "error":
4194
+ } else if (ownedPart.type === "response-metadata") {
4195
+ summaryResponse = yield* responseIdentity(ownedPart, summaryResponse);
4196
+ }
4197
+ // Drain malformed responses within the buffer bounds so reported usage is charged.
4198
+ yield* Effect.gen(function* () {
4199
+ switch (ownedPart.type) {
4200
+ case "text-start":
4201
+ return yield* startPart(textParts, ownedPart.id, "compaction text");
4202
+ case "text-delta":
4203
+ return yield* continuePart(textParts, ownedPart.id, "compaction text delta");
4204
+ case "text-end":
4205
+ return yield* endPart(textParts, ownedPart.id, "compaction text");
4206
+ case "reasoning-start":
4207
+ return yield* startPart(reasoningParts, ownedPart.id, "compaction reasoning");
4208
+ case "reasoning-delta":
4209
+ return yield* continuePart(
4210
+ reasoningParts,
4211
+ ownedPart.id,
4212
+ "compaction reasoning delta",
4213
+ );
4214
+ case "reasoning-end":
4215
+ return yield* endPart(reasoningParts, ownedPart.id, "compaction reasoning");
4216
+ case "finish": {
4217
+ summaryFinished = true;
4218
+ if (
4219
+ ownedPart.reason !== "stop" ||
4220
+ [...textParts.values(), ...reasoningParts.values()].includes("open")
4221
+ ) {
4184
4222
  return yield* ModelProtocolError.make({
4185
- message: `Compaction response contained an unusable ${ownedPart.type} part`,
4223
+ message:
4224
+ "Compaction response did not finish with complete text and a stop reason",
4186
4225
  });
4187
- case "file":
4188
- case "response-metadata":
4189
- case "source":
4190
- return;
4226
+ }
4227
+
4228
+ return;
4191
4229
  }
4192
- }).pipe(
4193
- Effect.catch((error) =>
4194
- Effect.sync(() => {
4195
- summaryFailure ??= error;
4196
- }),
4197
- ),
4198
- );
4199
- });
4230
+ case "tool-params-start":
4231
+ case "tool-params-delta":
4232
+ case "tool-params-end":
4233
+ case "tool-approval-request":
4234
+ case "error":
4235
+ return yield* ModelProtocolError.make({
4236
+ message: `Compaction response contained an unusable ${ownedPart.type} part`,
4237
+ });
4238
+ case "file":
4239
+ case "response-metadata":
4240
+ case "source":
4241
+ return;
4242
+ }
4243
+ }).pipe(
4244
+ Effect.catch((error) =>
4245
+ Effect.sync(() => {
4246
+ summaryFailure ??= error;
4247
+ }),
4248
+ ),
4249
+ );
4250
+ });
4200
4251
 
4201
4252
  const summaryExit = yield* enforceDurationDeadline(
4202
4253
  guardBudgetStream(LanguageModel.streamText({ prompt: summarizerPrompt }), options.budget),
@@ -4449,7 +4500,7 @@ const decodeInput = <AgentValue extends Agent.Any>(
4449
4500
  AgentInputDecodeError,
4450
4501
  AgentValue["definition"]["input"]["DecodingServices"]
4451
4502
  > =>
4452
- Schema.decodeUnknownEffect(agent.definition.input)(input).pipe(
4503
+ Schema.decodeUnknownEffect<AgentValue["definition"]["input"]>(agent.definition.input)(input).pipe(
4453
4504
  Effect.mapError((cause) =>
4454
4505
  AgentInputDecodeError.make({
4455
4506
  message: cause.message,
@@ -4475,7 +4526,7 @@ const encodeInput = <AgentValue extends Agent.Any>(
4475
4526
  AgentInputError,
4476
4527
  AgentValue["definition"]["input"]["EncodingServices"]
4477
4528
  > =>
4478
- Schema.encodeEffect(agent.definition.input)(input).pipe(
4529
+ Schema.encodeEffect<AgentValue["definition"]["input"]>(agent.definition.input)(input).pipe(
4479
4530
  Effect.mapError((cause) =>
4480
4531
  AgentInputError.make({
4481
4532
  message: `Unable to encode Agent input: ${cause.message}`,
@@ -4489,20 +4540,21 @@ const renderInputPromptEffect = <Input, Error, Services>(
4489
4540
  encodedInput: unknown,
4490
4541
  ): Effect.Effect<Prompt.RawInput, Error | AgentInputError, Services> =>
4491
4542
  inputPrompt === undefined
4492
- ? Effect.try({
4493
- try: () => {
4494
- const encoded = JSON.stringify(encodedInput);
4543
+ ? Effect.gen(function* () {
4544
+ const encoded = yield* Effect.try({
4545
+ try: () => JSON.stringify(encodedInput),
4546
+ catch: (cause) =>
4547
+ AgentInputError.make({
4548
+ message: `Unable to materialize Agent input: ${errorMessage(cause)}`,
4549
+ }),
4550
+ });
4495
4551
 
4496
- if (encoded === undefined) {
4497
- throw new Error("Agent input cannot be represented as JSON");
4498
- }
4552
+ if (encoded === undefined)
4553
+ return yield* AgentInputError.make({
4554
+ message: "Unable to materialize Agent input: Agent input cannot be represented as JSON",
4555
+ });
4499
4556
 
4500
- return encoded;
4501
- },
4502
- catch: (cause) =>
4503
- AgentInputError.make({
4504
- message: `Unable to materialize Agent input: ${errorMessage(cause)}`,
4505
- }),
4557
+ return encoded;
4506
4558
  })
4507
4559
  : Effect.suspend(() => {
4508
4560
  const result = inputPrompt(decodedInput);
@@ -4524,11 +4576,11 @@ export function renderInputPrompt<
4524
4576
  InputPromptRequirementsOf<InputPromptValue, Input>
4525
4577
  >;
4526
4578
 
4527
- export function renderInputPrompt<Input>(
4528
- inputPrompt: InputPromptSource<Input, unknown, unknown> | undefined,
4579
+ export function renderInputPrompt<Input, Error, Requirements>(
4580
+ inputPrompt: InputPromptSource<Input, Error, Requirements> | undefined,
4529
4581
  decodedInput: Input,
4530
4582
  encodedInput: unknown,
4531
- ): Effect.Effect<Prompt.RawInput, unknown, unknown> {
4583
+ ): Effect.Effect<Prompt.RawInput, Error | AgentInputError, Requirements> {
4532
4584
  return renderInputPromptEffect(inputPrompt, decodedInput, encodedInput);
4533
4585
  }
4534
4586
 
@@ -4579,30 +4631,26 @@ const decodeToolCallId = (id: string) =>
4579
4631
  );
4580
4632
 
4581
4633
  const decodeProviderToolCallId = (id: string) =>
4582
- Effect.suspend(() =>
4583
- decodeProviderToolCallIdValue(id).pipe(
4584
- Effect.mapError(() =>
4585
- ModelProtocolError.make({
4586
- message:
4587
- "Model supplied an invalid Tool Call ID; expected 1-128 ASCII letters, digits, dots, underscores, colons, or hyphens",
4588
- }),
4589
- ),
4634
+ decodeProviderToolCallIdValue(id).pipe(
4635
+ Effect.mapErrorEager(() =>
4636
+ ModelProtocolError.make({
4637
+ message:
4638
+ "Model supplied an invalid Tool Call ID; expected 1-128 ASCII letters, digits, dots, underscores, colons, or hyphens",
4639
+ }),
4590
4640
  ),
4591
4641
  );
4592
4642
 
4593
4643
  const decodeProviderResponsePartId = (id: string) =>
4594
- Effect.suspend(() =>
4595
- decodeProviderResponsePartIdValue(id).pipe(
4596
- Effect.mapError(() =>
4597
- ModelProtocolError.make({
4598
- message:
4599
- "Model supplied an invalid response part ID; expected 1-128 ASCII letters, digits, dots, underscores, colons, or hyphens",
4600
- }),
4601
- ),
4644
+ decodeProviderResponsePartIdValue(id).pipe(
4645
+ Effect.mapErrorEager(() =>
4646
+ ModelProtocolError.make({
4647
+ message:
4648
+ "Model supplied an invalid response part ID; expected 1-128 ASCII letters, digits, dots, underscores, colons, or hyphens",
4649
+ }),
4602
4650
  ),
4603
4651
  );
4604
4652
 
4605
- const validateProviderPartIdentifiers = Effect.fnUntraced(function* (part: Response.AnyPart) {
4653
+ const validateProviderPartIdentifiers = Effect.fnUntracedEager(function* (part: Response.AnyPart) {
4606
4654
  switch (part.type) {
4607
4655
  case "text-start":
4608
4656
  case "text-delta":
@@ -4735,10 +4783,10 @@ const promptFromTurnParts = (trace: TurnTrace): Prompt.Prompt => {
4735
4783
  return Prompt.fromMessages(messages);
4736
4784
  };
4737
4785
 
4738
- // `Effect.fnUntraced`: this dispatcher runs for every streamed Response Part,
4739
- // so a named span here would emit one span per TextDelta/ReasoningDelta. The
4740
- // enclosing model request keeps its `AgentRuntime.model` stream span.
4741
- const processModelPart = Effect.fnUntraced(function* <Tools extends Record<string, Tool.Any>>(
4786
+ // Yield the returned Effect immediately and exactly once from the executing stream
4787
+ // consumer: construction runs the synchronous prefix, mutating trace counters and maps.
4788
+ // Application codecs and events still suspend. The enclosing model request owns the span.
4789
+ const processModelPart = Effect.fnUntracedEager(function* <Tools extends Record<string, Tool.Any>>(
4742
4790
  context: RunContext,
4743
4791
  turnId: TurnId,
4744
4792
  turn: number,
@@ -4908,7 +4956,7 @@ const processModelPart = Effect.fnUntraced(function* <Tools extends Record<strin
4908
4956
  const error = AiError.make({
4909
4957
  module: "Toolkit",
4910
4958
  method: `${part.name}.handle`,
4911
- reason: new AiError.ToolParameterValidationError({
4959
+ reason: AiError.ToolParameterValidationError.make({
4912
4960
  toolName: part.name,
4913
4961
  description: decoded.failure.message,
4914
4962
  }),
@@ -5142,7 +5190,9 @@ const decodeFinalOutput = Effect.fnUntraced(function* <AgentValue extends Agent.
5142
5190
 
5143
5191
  const candidateOutput: unknown = eventJson;
5144
5192
 
5145
- const decoded = yield* Schema.decodeUnknownEffect(agent.definition.output)(candidateOutput).pipe(
5193
+ const decoded = yield* Schema.decodeUnknownEffect<AgentValue["definition"]["output"]>(
5194
+ agent.definition.output,
5195
+ )(candidateOutput).pipe(
5146
5196
  Effect.mapError((cause) =>
5147
5197
  AgentOutputError.make({
5148
5198
  message: cause.message,
@@ -5157,7 +5207,9 @@ const encodeOutputCandidate = Effect.fnUntraced(function* <AgentValue extends Ag
5157
5207
  agent: AgentValue,
5158
5208
  candidate: unknown,
5159
5209
  ) {
5160
- const encoded = yield* Schema.encodeUnknownEffect(agent.definition.output)(candidate).pipe(
5210
+ const encoded = yield* Schema.encodeUnknownEffect<AgentValue["definition"]["output"]>(
5211
+ agent.definition.output,
5212
+ )(candidate).pipe(
5161
5213
  Effect.mapError((cause) =>
5162
5214
  AgentOutputError.make({
5163
5215
  message: `Completion Tool output failed Schema encoding: ${cause.message}`,
@@ -5165,7 +5217,9 @@ const encodeOutputCandidate = Effect.fnUntraced(function* <AgentValue extends Ag
5165
5217
  ),
5166
5218
  );
5167
5219
 
5168
- const decoded = yield* Schema.decodeUnknownEffect(agent.definition.output)(encoded).pipe(
5220
+ const decoded = yield* Schema.decodeUnknownEffect<AgentValue["definition"]["output"]>(
5221
+ agent.definition.output,
5222
+ )(encoded).pipe(
5169
5223
  Effect.mapError((cause) =>
5170
5224
  AgentOutputError.make({
5171
5225
  message: `Completion Tool output failed canonical decoding: ${cause.message}`,
@@ -5194,7 +5248,8 @@ const projectToolResult = Effect.fnUntraced(function* <AgentValue extends Agent.
5194
5248
  AgentOutputError | ModelProtocolError,
5195
5249
  AgentCompletionProjectionRequirements<AgentValue>
5196
5250
  > {
5197
- const tool = agent.definition.toolkit.tools[declaration.tool];
5251
+ const tools: AgentValue["definition"]["toolkit"]["tools"] = agent.definition.toolkit.tools;
5252
+ const tool = tools[declaration.tool];
5198
5253
 
5199
5254
  if (tool === undefined) {
5200
5255
  return yield* ModelProtocolError.make({
@@ -5202,9 +5257,9 @@ const projectToolResult = Effect.fnUntraced(function* <AgentValue extends Agent.
5202
5257
  });
5203
5258
  }
5204
5259
 
5205
- const decodedParameters = yield* Schema.decodeUnknownEffect(tool.parametersSchema)(
5206
- parameters,
5207
- ).pipe(
5260
+ const decodedParameters = yield* Schema.decodeUnknownEffect<
5261
+ AgentValue["definition"]["toolkit"]["tools"][string]["parametersSchema"]
5262
+ >(tool.parametersSchema)(parameters).pipe(
5208
5263
  Effect.mapError((cause) =>
5209
5264
  AgentOutputError.make({
5210
5265
  message: `Completion Tool parameters failed canonical decoding: ${cause.message}`,
@@ -5212,7 +5267,9 @@ const projectToolResult = Effect.fnUntraced(function* <AgentValue extends Agent.
5212
5267
  ),
5213
5268
  );
5214
5269
 
5215
- const decodedResult = yield* Schema.decodeUnknownEffect(tool.successSchema)(result).pipe(
5270
+ const decodedResult = yield* Schema.decodeUnknownEffect<
5271
+ AgentValue["definition"]["toolkit"]["tools"][string]["successSchema"]
5272
+ >(tool.successSchema)(result).pipe(
5216
5273
  Effect.mapError((cause) =>
5217
5274
  AgentOutputError.make({
5218
5275
  message: `Completion Tool result failed canonical decoding: ${cause.message}`,
@@ -5512,10 +5569,10 @@ const makeTurn = <
5512
5569
  : yield* prepareWithinDeadline(context, options.context.prepare(contextRequest));
5513
5570
 
5514
5571
  if (context.resolvedModel && modelContext.modelCall !== undefined) {
5515
- return yield* new AiError.AiError({
5572
+ return yield* AiError.AiError.make({
5516
5573
  module: "AgentRuntime",
5517
5574
  method: "resolveModel",
5518
- reason: new AiError.InvalidRequestError({
5575
+ reason: AiError.InvalidRequestError.make({
5519
5576
  description: "Context preparation cannot replace a thread-resolved model",
5520
5577
  }),
5521
5578
  });
@@ -6337,13 +6394,20 @@ const makeTurn = <
6337
6394
  if (Exit.isFailure(exit)) failedUsageCause = exit.cause;
6338
6395
  });
6339
6396
 
6340
- const attempt = (basis: Prompt.Prompt) =>
6397
+ const attempt = (
6398
+ basis: Prompt.Prompt,
6399
+ ): Effect.Effect<
6400
+ void,
6401
+ AgentRuntimeFailure<typeof agent, HookError, InstructionError>,
6402
+ | InterpreterRequirements<typeof agent, HookRequirements, InstructionRequirements>
6403
+ | Scope.Scope
6404
+ > =>
6341
6405
  Effect.flatten(
6342
6406
  outgoingModelPrompt(
6343
6407
  policy,
6344
6408
  context,
6345
6409
  prepareModelPrompt(
6346
- Prompt.fromMessages([...basis.content, ...transientContext.content]),
6410
+ Prompt.concat(basis, transientContext),
6347
6411
  outputContract._tag === "rendered" ? outputContract.part : undefined,
6348
6412
  systemMessagesInHistory,
6349
6413
  staticInstructions,
@@ -6430,6 +6494,8 @@ const makeTurn = <
6430
6494
 
6431
6495
  return yield* trackContext.pipe(
6432
6496
  Effect.as(
6497
+ // Transport-only tools retain existential schema services; the enclosing attempt keeps the Agent's declared requirements.
6498
+ // @effect-diagnostics-next-line anyUnknownInErrorContext:off
6433
6499
  guardBudgetStream(
6434
6500
  LanguageModel.streamText({
6435
6501
  // Every attempt, including an overflow retry, places the
@@ -6475,43 +6541,47 @@ const makeTurn = <
6475
6541
  activeProvisionalText = provisionalText;
6476
6542
  }),
6477
6543
  ),
6478
- Stream.runForEach((part) =>
6544
+ Stream.runForEachArray((parts) =>
6479
6545
  Effect.gen(function* () {
6480
- const owned = yield* ownModelResponsePart(
6481
- part,
6482
- agent.definition.toolkit,
6483
- trace,
6484
- context.bufferLimits,
6485
- trace.finished,
6486
- );
6487
-
6488
- const events = context.publish === undefined ? undefined : [];
6489
-
6490
- yield* processModelPart(
6491
- context,
6492
- turnId,
6493
- turn,
6494
- agent.definition.toolkit.tools,
6495
- trace,
6496
- owned.ownedPart,
6497
- owned.retainedBytes,
6498
- events,
6499
- );
6500
- if (provisionalText !== undefined) {
6501
- const part = owned.ownedPart;
6502
-
6503
- if (
6504
- part.type === "text-start" ||
6505
- part.type === "text-delta" ||
6506
- part.type === "text-end"
6507
- )
6508
- provisionalText.offerUnsafe(part);
6546
+ for (const part of parts) {
6547
+ const owned = yield* ownModelResponsePart(
6548
+ part,
6549
+ agent.definition.toolkit,
6550
+ trace,
6551
+ context.bufferLimits,
6552
+ trace.finished,
6553
+ );
6554
+
6555
+ const events = context.publish === undefined ? undefined : [];
6556
+
6557
+ yield* processModelPart(
6558
+ context,
6559
+ turnId,
6560
+ turn,
6561
+ agent.definition.toolkit.tools,
6562
+ trace,
6563
+ owned.ownedPart,
6564
+ owned.retainedBytes,
6565
+ events,
6566
+ );
6567
+ if (provisionalText !== undefined) {
6568
+ const part = owned.ownedPart;
6569
+
6570
+ if (
6571
+ part.type === "text-start" ||
6572
+ part.type === "text-delta" ||
6573
+ part.type === "text-end"
6574
+ )
6575
+ provisionalText.offerUnsafe(part);
6576
+ }
6577
+ // Publish before accepting the next part. Backpressure stays interruptible.
6578
+ if (events !== undefined) yield* publishEvents(context, events);
6509
6579
  }
6510
- // Publish before accepting the next part. Backpressure stays interruptible.
6511
- if (events !== undefined) yield* publishEvents(context, events);
6512
6580
  }),
6513
6581
  ),
6514
- (effect) => prepareWithinDeadline(context, effect),
6582
+ <Value, Error, Requirements>(
6583
+ effect: Effect.Effect<Value, Error, Requirements>,
6584
+ ) => prepareWithinDeadline(context, effect),
6515
6585
  Effect.onExit((exit) => {
6516
6586
  if (Exit.isSuccess(exit)) return Effect.void;
6517
6587
  provisionalText?.discard();
@@ -6529,63 +6599,71 @@ const makeTurn = <
6529
6599
  // Retry the closed native provider operation. Admitted content makes the response
6530
6600
  // ineligible for retry; canonical acceptance belongs to the continuation below.
6531
6601
  const attemptWithRetries = Effect.suspend(() => attempt(compactedOutgoing())).pipe(
6532
- Effect.retry(($) =>
6533
- $(Schedule.exponential(MODEL_RETRY_BASE)).pipe(
6534
- Schedule.upTo({ times: policy.modelRetries ?? 0 }),
6535
- Schedule.while(({ input: error }) => {
6536
- const lastPart = trace.parts.at(-1);
6537
-
6538
- return (
6539
- canRepeatModelCall &&
6540
- !hasStreamedContent(trace) &&
6541
- ((lastPart?.type === "error" && isTransientErrorPayload(lastPart.error)) ||
6542
- (AiError.isAiError(error) && error.isRetryable))
6543
- );
6544
- }),
6545
- Schedule.modifyDelay(({ input: error, duration }) =>
6546
- Effect.gen(function* () {
6547
- const backoff = Duration.min(duration, MODEL_RETRY_MAX_DELAY);
6548
- const retryAfter = AiError.isAiError(error) ? error.retryAfter : undefined;
6602
+ (operation) =>
6603
+ (policy.modelRetries ?? 0) === 0
6604
+ ? operation
6605
+ : Effect.retry(operation, ($) =>
6606
+ $(Schedule.exponential(MODEL_RETRY_BASE)).pipe(
6607
+ Schedule.upTo({ times: policy.modelRetries ?? 0 }),
6608
+ Schedule.while(({ input: error }) => {
6609
+ const lastPart = trace.parts.at(-1);
6610
+ const providerError: unknown = error;
6611
+
6612
+ return (
6613
+ canRepeatModelCall &&
6614
+ !hasStreamedContent(trace) &&
6615
+ ((lastPart?.type === "error" && isTransientErrorPayload(lastPart.error)) ||
6616
+ (AiError.isAiError(providerError) && providerError.isRetryable))
6617
+ );
6618
+ }),
6619
+ Schedule.modifyDelay(({ input: error, duration }) =>
6620
+ Effect.gen(function* () {
6621
+ const backoff = Duration.min(duration, MODEL_RETRY_MAX_DELAY);
6622
+ const providerError: unknown = error;
6549
6623
 
6550
- const requested =
6551
- retryAfter === undefined ? backoff : Duration.max(backoff, retryAfter);
6624
+ const retryAfter = AiError.isAiError(providerError)
6625
+ ? providerError.retryAfter
6626
+ : undefined;
6552
6627
 
6553
- // Expiration is rejected by the next native attempt. Capping only this delay
6554
- // keeps failed-attempt accounting and canonical hooks outside interruption timers.
6555
- const remaining = Math.max(
6556
- 0,
6557
- context.durationDeadlineMillis - (yield* Clock.currentTimeMillis),
6558
- );
6628
+ const requested =
6629
+ retryAfter === undefined ? backoff : Duration.max(backoff, retryAfter);
6559
6630
 
6560
- return Duration.min(requested, Duration.millis(remaining));
6561
- }),
6562
- ),
6563
- Schedule.tap(({ input: error, attempt: retry, duration }) =>
6564
- Effect.gen(function* () {
6565
- trace.parts.length = 0;
6566
- trace.responsePartCount = 0;
6567
- trace.responsePartBytes = 0;
6568
- // The failed attempt's usage was already retained on exit.
6569
- trace.usage = undefined;
6570
- delete trace.response;
6571
- delete trace.finishMetadata;
6572
- yield* Effect.logWarning("agent model call retrying").pipe(
6573
- Effect.annotateLogs({
6574
- runId: context.runId,
6575
- turnId,
6576
- retry,
6577
- delayMillis: Duration.toMillis(duration),
6578
- error: errorMessage(error),
6579
- }),
6580
- );
6581
- }),
6582
- ),
6583
- ),
6584
- ),
6585
- );
6631
+ // Expiration is rejected by the next native attempt. Capping only this delay
6632
+ // keeps failed-attempt accounting and canonical hooks outside interruption timers.
6633
+ const remaining = Math.max(
6634
+ 0,
6635
+ context.durationDeadlineMillis - (yield* Clock.currentTimeMillis),
6636
+ );
6586
6637
 
6587
- // RUN-027: one summarize-and-retry for a classified provider context
6588
- // overflow when compaction is configured; every other provider error
6638
+ return Duration.min(requested, Duration.millis(remaining));
6639
+ }),
6640
+ ),
6641
+ Schedule.tap(({ input: error, attempt: retry, duration }) =>
6642
+ Effect.gen(function* () {
6643
+ trace.parts.length = 0;
6644
+ trace.responsePartCount = 0;
6645
+ trace.responsePartBytes = 0;
6646
+ // The failed attempt's usage was already retained on exit.
6647
+ trace.usage = undefined;
6648
+ delete trace.response;
6649
+ delete trace.finishMetadata;
6650
+ yield* Effect.logWarning("agent model call retrying").pipe(
6651
+ Effect.annotateLogs({
6652
+ runId: context.runId,
6653
+ turnId,
6654
+ retry,
6655
+ delayMillis: Duration.toMillis(duration),
6656
+ error: errorMessage(error),
6657
+ }),
6658
+ );
6659
+ }),
6660
+ ),
6661
+ ),
6662
+ ),
6663
+ );
6664
+
6665
+ // RUN-027: one summarize-and-retry for a classified provider context
6666
+ // overflow when compaction is configured; every other provider error
6589
6667
  // propagates unchanged. A response that already streamed parts mutated
6590
6668
  // the trace, so it is never retried.
6591
6669
  const response = attemptWithRetries.pipe(
@@ -7031,16 +7109,15 @@ const makeTurn = <
7031
7109
  * steering at the safe seam, and start the next Turn. The `maxTurns`
7032
7110
  * guard already ran for these Tool Call sites.
7033
7111
  */
7034
- const continueTurn = (history: Prompt.Prompt) =>
7035
- Effect.gen(function* () {
7036
- trace.historyAccepted = true;
7037
- yield* advanceHistory(context, history, options);
7038
- yield* settleTurn(context, trace, turn, turnId, options);
7039
- const steering = yield* drainInputs(context, options);
7040
- const nextPrompt = yield* appendInputs(context, history, steering, options);
7041
-
7042
- return nextTurn(nextPrompt, turn + 1, toolCalls);
7043
- });
7112
+ const continueTurn = Effect.fnUntraced(function* (history: Prompt.Prompt) {
7113
+ trace.historyAccepted = true;
7114
+ yield* advanceHistory(context, history, options);
7115
+ yield* settleTurn(context, trace, turn, turnId, options);
7116
+ const steering = yield* drainInputs(context, options);
7117
+ const nextPrompt = yield* appendInputs(context, history, steering, options);
7118
+
7119
+ return nextTurn(nextPrompt, turn + 1, toolCalls);
7120
+ });
7044
7121
 
7045
7122
  /**
7046
7123
  * The otherwise-stop seam: advance official history, drain steering,
@@ -7048,86 +7125,81 @@ const makeTurn = <
7048
7125
  * under the `maxTurns` and repeated-failure Stop Policy bounds or
7049
7126
  * complete the Run with the Turn's decoded final output.
7050
7127
  */
7051
- const settleOrFollowUp = (history: Prompt.Prompt) =>
7052
- Effect.gen(function* () {
7053
- trace.historyAccepted = true;
7054
- yield* advanceHistory(context, history, options);
7128
+ const settleOrFollowUp = Effect.fnUntraced(function* (history: Prompt.Prompt) {
7129
+ trace.historyAccepted = true;
7130
+ yield* advanceHistory(context, history, options);
7055
7131
 
7056
- // Do not consume durable receipts that this completed Turn cannot cover.
7057
- // Final-answer mode permits one grace Turn, but never a second finalization.
7058
- const turnsBlocked =
7059
- policy.onExhaustion === "fail" ? turn >= bounds.maxTurns : turn > bounds.maxTurns;
7132
+ // Do not consume durable receipts that this completed Turn cannot cover.
7133
+ // Final-answer mode permits one grace Turn, but never a second finalization.
7134
+ const turnsBlocked =
7135
+ policy.onExhaustion === "fail" ? turn >= bounds.maxTurns : turn > bounds.maxTurns;
7060
7136
 
7061
- const steering =
7062
- turnsBlocked || context.finalizationUsed
7063
- ? []
7064
- : yield* drainInputs(context, options);
7065
-
7066
- const queued =
7067
- steering.length > 0
7068
- ? steering
7069
- : takeFollowUps(context, options.commandDrainPolicy ?? "one");
7070
-
7071
- if (queued.length > 0) {
7072
- // Final-answer mode admits exactly one grace Turn past
7073
- // `maxTurns` (RUN-019): `turn > maxTurns` can only be
7074
- // `maxTurns + 1`, so a second grace is structurally
7075
- // impossible.
7076
- if (turnsBlocked) {
7077
- return failExecution(
7078
- AgentPolicyError.make({
7079
- limit: "turns",
7080
- message: `Agent exceeded its ${bounds.maxTurns} Turn limit`,
7081
- }),
7082
- );
7083
- }
7084
- yield* applyRepeatedFailurePolicy(
7085
- context,
7086
- trace,
7087
- agent.definition.policy.repeatedFailureLimit,
7088
- );
7089
- yield* settleTurn(context, trace, turn, turnId, options);
7090
- const nextPrompt = yield* appendInputs(context, history, queued, options);
7137
+ const steering =
7138
+ turnsBlocked || context.finalizationUsed ? [] : yield* drainInputs(context, options);
7091
7139
 
7092
- return nextTurn(nextPrompt, turn + 1, toolCalls);
7093
- }
7140
+ const queued =
7141
+ steering.length > 0
7142
+ ? steering
7143
+ : takeFollowUps(context, options.commandDrainPolicy ?? "one");
7094
7144
 
7095
- const output = yield* prepareWithinDeadline(
7145
+ if (queued.length > 0) {
7146
+ // Final-answer mode admits exactly one grace Turn past
7147
+ // `maxTurns` (RUN-019): `turn > maxTurns` can only be
7148
+ // `maxTurns + 1`, so a second grace is structurally
7149
+ // impossible.
7150
+ if (turnsBlocked) {
7151
+ return failExecution(
7152
+ AgentPolicyError.make({
7153
+ limit: "turns",
7154
+ message: `Agent exceeded its ${bounds.maxTurns} Turn limit`,
7155
+ }),
7156
+ );
7157
+ }
7158
+ yield* applyRepeatedFailurePolicy(
7096
7159
  context,
7097
- decodeFinalOutput(agent, trace.text.join("")),
7160
+ trace,
7161
+ agent.definition.policy.repeatedFailureLimit,
7098
7162
  );
7163
+ yield* settleTurn(context, trace, turn, turnId, options);
7164
+ const nextPrompt = yield* appendInputs(context, history, queued, options);
7165
+
7166
+ return nextTurn(nextPrompt, turn + 1, toolCalls);
7167
+ }
7099
7168
 
7100
- const declaration = agent.definition.runDisposition;
7169
+ const output = yield* prepareWithinDeadline(
7170
+ context,
7171
+ decodeFinalOutput(agent, trace.text.join("")),
7172
+ );
7101
7173
 
7102
- const runDisposition =
7103
- finalAnswerOnly || declaration === undefined
7104
- ? undefined
7105
- : yield* prepareWithinDeadline(
7106
- context,
7107
- encodeRunDisposition(agent, output.decoded),
7108
- );
7174
+ const declaration = agent.definition.runDisposition;
7109
7175
 
7110
- const completionUsage = yield* usageReportOf(context);
7111
-
7112
- return Effect.map(eventBase(context), (base) =>
7113
- RunCompleted.make({
7114
- ...base,
7115
- output: output.encoded,
7116
- ...(runDisposition === undefined ? {} : { runDisposition }),
7117
- turns: turn,
7118
- // A Run settled under the final-answer constraint reports
7119
- // the exhaustion honestly (RUN-011), never a plain model
7120
- // stop, and carries the dimension that bound.
7121
- finishReason: finalAnswerOnly ? "budget-exhausted" : "model-stop",
7122
- ...(finalAnswerOnly && context.exhaustedDimension !== undefined
7123
- ? { exhausted: context.exhaustedDimension }
7124
- : {}),
7125
- ...completionUsage,
7126
- }),
7127
- ).pipe(
7128
- Effect.tap((event) => settleTurn(context, trace, turn, turnId, options, event)),
7129
- );
7130
- });
7176
+ const runDisposition =
7177
+ finalAnswerOnly || declaration === undefined
7178
+ ? undefined
7179
+ : yield* prepareWithinDeadline(
7180
+ context,
7181
+ encodeRunDisposition(agent, output.decoded),
7182
+ );
7183
+
7184
+ const completionUsage = yield* usageReportOf(context);
7185
+
7186
+ return Effect.map(eventBase(context), (base) =>
7187
+ RunCompleted.make({
7188
+ ...base,
7189
+ output: output.encoded,
7190
+ ...(runDisposition === undefined ? {} : { runDisposition }),
7191
+ turns: turn,
7192
+ // A Run settled under the final-answer constraint reports
7193
+ // the exhaustion honestly (RUN-011), never a plain model
7194
+ // stop, and carries the dimension that bound.
7195
+ finishReason: finalAnswerOnly ? "budget-exhausted" : "model-stop",
7196
+ ...(finalAnswerOnly && context.exhaustedDimension !== undefined
7197
+ ? { exhausted: context.exhaustedDimension }
7198
+ : {}),
7199
+ ...completionUsage,
7200
+ }),
7201
+ ).pipe(Effect.tap((event) => settleTurn(context, trace, turn, turnId, options, event)));
7202
+ });
7131
7203
 
7132
7204
  if (providerOnly && trace.finishReason === "stop") {
7133
7205
  if (agent.definition.completion?.required === true) {
@@ -7889,7 +7961,7 @@ const makeResumeTurn = <
7889
7961
  id: call.id,
7890
7962
  isFailure: true,
7891
7963
  result: yield* decodeEventJson(
7892
- boundEncodedToolResult(
7964
+ yield* boundEncodedToolResult(
7893
7965
  rejection.error,
7894
7966
  durableResultBounds(agent.definition.policy.toolResultBounds, options, turn),
7895
7967
  ),
@@ -8533,10 +8605,9 @@ function executeWithCompletion<
8533
8605
 
8534
8606
  // Normalize the selected host policy at the Run boundary. Inner calls project typed
8535
8607
  // override failures into broker outcomes; native calls retain their original E channel.
8536
- const programmaticAuthorization = Layer.effect(
8537
- ProgrammaticToolAuthorization,
8538
- Effect.gen(function* () {
8539
- const services = yield* Effect.context<HookRequirements>();
8608
+ const programmaticAuthorization = Effect.map(
8609
+ Effect.context<HookRequirements>(),
8610
+ (services) => {
8540
8611
  const selected = options.toolAuthorization;
8541
8612
 
8542
8613
  return ProgrammaticToolAuthorization.of({
@@ -8549,7 +8620,7 @@ function executeWithCompletion<
8549
8620
  ),
8550
8621
  ),
8551
8622
  });
8552
- }),
8623
+ },
8553
8624
  );
8554
8625
 
8555
8626
  const interpreted = Effect.flatten(
@@ -8601,12 +8672,12 @@ function executeWithCompletion<
8601
8672
 
8602
8673
  const validateUpdate =
8603
8674
  updateSchema === undefined
8604
- ? () => Effect.fail(new UpdateError({ reason: "unavailable" }))
8675
+ ? () => Effect.fail(UpdateError.make({ reason: "unavailable" }))
8605
8676
  : (value: Schema.Json) =>
8606
8677
  Schema.decodeEffect(updateSchema)(value).pipe(
8607
8678
  Effect.provide(updateContext),
8608
8679
  Effect.asVoid,
8609
- Effect.mapError(() => new UpdateError({ reason: "validation" })),
8680
+ Effect.mapError(() => UpdateError.make({ reason: "validation" })),
8610
8681
  );
8611
8682
 
8612
8683
  const languageModel = yield* LanguageModel.LanguageModel;
@@ -8961,8 +9032,13 @@ function executeWithCompletion<
8961
9032
  ),
8962
9033
  );
8963
9034
 
8964
- yield* checkpointExecution(context, options.durability);
8965
- if (output._tag !== "NextTurn") return output;
9035
+ if (output._tag !== "NextTurn") {
9036
+ yield* checkpointExecution(context, options.durability);
9037
+
9038
+ return output;
9039
+ }
9040
+ // Both Turn entry points check before any hook or dispatch. The
9041
+ // preceding Scope has closed, so that check also sees finalizer failures.
8966
9042
  request = output;
8967
9043
  }
8968
9044
 
@@ -8983,15 +9059,14 @@ function executeWithCompletion<
8983
9059
  const selectionPrompt = yield* Schema.encodeEffect(
8984
9060
  Schema.fromJsonString(Prompt.Prompt),
8985
9061
  )(pending.prompt).pipe(
8986
- Effect.mapError(
8987
- (cause) =>
8988
- new AiError.AiError({
8989
- module: "AgentRuntime",
8990
- method: "resolveModel",
8991
- reason: new AiError.InvalidRequestError({
8992
- description: `Cannot encode model selection context: ${cause.message}`,
8993
- }),
9062
+ Effect.mapError((cause) =>
9063
+ AiError.AiError.make({
9064
+ module: "AgentRuntime",
9065
+ method: "resolveModel",
9066
+ reason: AiError.InvalidRequestError.make({
9067
+ description: `Cannot encode model selection context: ${cause.message}`,
8994
9068
  }),
9069
+ }),
8995
9070
  ),
8996
9071
  );
8997
9072
 
@@ -9030,34 +9105,36 @@ function executeWithCompletion<
9030
9105
  agent.definition.updates === undefined || nativeUpdateTool === undefined
9031
9106
  ? Context.empty()
9032
9107
  : yield* Toolkit.make(nativeUpdateTool).toHandlers({
9033
- emit_update: (parameters: unknown, call: Toolkit.HandlerContext<Tool.Any>) =>
9034
- Effect.gen(function* () {
9035
- if (call.toolCallId === undefined)
9036
- return yield* new UpdateError({ reason: "unavailable" });
9108
+ emit_update: Effect.fnUntraced(function* (
9109
+ parameters: unknown,
9110
+ call: Toolkit.HandlerContext<Tool.Any>,
9111
+ ) {
9112
+ if (call.toolCallId === undefined)
9113
+ return yield* UpdateError.make({ reason: "unavailable" });
9037
9114
 
9038
- const decoded = yield* Schema.decodeUnknownEffect(
9039
- Schema.Struct({ value: Schema.Unknown }),
9040
- )(parameters).pipe(
9041
- Effect.mapError(() => new UpdateError({ reason: "validation" })),
9042
- );
9115
+ const decoded = yield* Schema.decodeUnknownEffect(
9116
+ Schema.Struct({ value: Schema.Unknown }),
9117
+ )(parameters).pipe(
9118
+ Effect.mapError(() => UpdateError.make({ reason: "validation" })),
9119
+ );
9043
9120
 
9044
- const value = yield* Schema.encodeUnknownEffect(agent.definition.updates!)(
9045
- decoded.value,
9046
- ).pipe(
9047
- Effect.flatMap(Schema.decodeUnknownEffect(Schema.Json)),
9048
- Effect.mapError(() => new UpdateError({ reason: "validation" })),
9049
- );
9121
+ const value = yield* Schema.encodeUnknownEffect(agent.definition.updates!)(
9122
+ decoded.value,
9123
+ ).pipe(
9124
+ Effect.flatMap(Schema.decodeUnknownEffect(Schema.Json)),
9125
+ Effect.mapError(() => UpdateError.make({ reason: "validation" })),
9126
+ );
9050
9127
 
9051
- const updateId = yield* Schema.decodeEffect(IdempotencyKey)(
9052
- `tool:${context.runId}:${call.toolCallId}`,
9053
- ).pipe(Effect.mapError(() => new UpdateError({ reason: "validation" })));
9128
+ const updateId = yield* Schema.decodeEffect(IdempotencyKey)(
9129
+ `tool:${context.runId}:${call.toolCallId}`,
9130
+ ).pipe(Effect.mapError(() => UpdateError.make({ reason: "validation" })));
9054
9131
 
9055
- const emitter = yield* Emitter;
9132
+ const emitter = yield* Emitter;
9056
9133
 
9057
- yield* emitter.emit({ target: agent.definition, updateId, value });
9134
+ yield* emitter.emit({ target: agent.definition, updateId, value });
9058
9135
 
9059
- return { emitted: true };
9060
- }),
9136
+ return { emitted: true };
9137
+ }),
9061
9138
  });
9062
9139
 
9063
9140
  const engineToolServices = Context.make(
@@ -9114,7 +9191,7 @@ function executeWithCompletion<
9114
9191
  }),
9115
9192
  Context.merge(updateHandlers),
9116
9193
  Context.add(Emitter, {
9117
- emit: () => Effect.fail(new UpdateError({ reason: "unavailable" })),
9194
+ emit: () => Effect.fail(UpdateError.make({ reason: "unavailable" })),
9118
9195
  }),
9119
9196
  Context.add(CurrentToolCatalog, { entries: [] }),
9120
9197
  Context.add(RunToolVisibility, visibility),
@@ -9208,7 +9285,8 @@ function executeWithCompletion<
9208
9285
  const events = modeled.pipe(
9209
9286
  // The engine composition boundary owns span-lifecycle isolation while preserving the host's
9210
9287
  // ambient Tracer/Logger configuration. Individual Tool executions consume this capability.
9211
- Effect.provide(ToolSpanTelemetry.layer.pipe(Layer.provideMerge(programmaticAuthorization))),
9288
+ Effect.provideServiceEffect(ToolSpanTelemetry, ToolSpanTelemetry.make),
9289
+ Effect.provideServiceEffect(ProgrammaticToolAuthorization, programmaticAuthorization),
9212
9290
  );
9213
9291
 
9214
9292
  // Closing execution first joins every owned child and finalizer before validation,
@@ -9330,9 +9408,9 @@ const completeRun = <AgentValue extends Agent.Any, Error, Requirements>(
9330
9408
  Effect.gen(function* () {
9331
9409
  const candidateOutput: unknown = completed.output;
9332
9410
 
9333
- const output = yield* Schema.decodeUnknownEffect(agent.definition.output)(
9334
- candidateOutput,
9335
- ).pipe(
9411
+ const output = yield* Schema.decodeUnknownEffect<AgentValue["definition"]["output"]>(
9412
+ agent.definition.output,
9413
+ )(candidateOutput).pipe(
9336
9414
  Effect.mapError((cause) =>
9337
9415
  AgentOutputError.make({
9338
9416
  message: cause.message,
@@ -9577,9 +9655,11 @@ function runUnknown<H = never, R = never>(
9577
9655
  input: unknown,
9578
9656
  options: RunOptions<H, R> = {},
9579
9657
  ) {
9580
- const program = "definition" in agent ? agent : { definition: agent };
9658
+ const program = { definition: "definition" in agent ? agent.definition : agent };
9581
9659
 
9582
9660
  return runProgram(program, (onCompleted) =>
9661
+ // The overload retains each Agent's R; this implementation executes an existential model binding without narrowing its services.
9662
+ // @effect-diagnostics-next-line anyUnknownInErrorContext:off
9583
9663
  executeWithCompletion(agent, input, options, onCompleted).pipe(
9584
9664
  Effect.provide(
9585
9665
  Layer.mergeAll(ModelUsageAccounting.layerEphemeral, AgentUpdateAcceptance.layerEphemeral),
@@ -9614,11 +9694,13 @@ function startUnknown<H = never, R = never>(
9614
9694
  input: unknown,
9615
9695
  options: RunOptions<H, R> = {},
9616
9696
  ) {
9617
- const program = "definition" in agent ? agent : { definition: agent };
9697
+ const program = { definition: "definition" in agent ? agent.definition : agent };
9618
9698
 
9619
9699
  return startProgram(
9620
9700
  program,
9621
9701
  (executionOptions, onCompleted, onUsage, publish) =>
9702
+ // The overload retains each Agent's R; detached execution captures the same existential binding inside the caller's Scope.
9703
+ // @effect-diagnostics-next-line anyUnknownInErrorContext:off
9622
9704
  executeWithCompletion(agent, input, executionOptions, onCompleted, onUsage, publish).pipe(
9623
9705
  Effect.provide(
9624
9706
  Layer.mergeAll(ModelUsageAccounting.layerEphemeral, AgentUpdateAcceptance.layerEphemeral),
@@ -9888,588 +9970,588 @@ const makeToolBrokerService = Effect.fnUntraced(function* <HookError, HookRequir
9888
9970
  observer === undefined ? undefined : { observer, permits: Semaphore.makeUnsafe(1) };
9889
9971
 
9890
9972
  const service: ToolBrokerService = {
9891
- openPass: (toolkit, passOptions) =>
9892
- Effect.gen(function* () {
9893
- const inheritedAuthority = yield* CurrentSubagentAuthority;
9973
+ openPass: Effect.fnUntraced(function* <Tools extends Record<string, Tool.Any>>(
9974
+ toolkit: Toolkit.WithHandler<Tools>,
9975
+ passOptions: Parameters<ToolBrokerService["openPass"]>[1],
9976
+ ) {
9977
+ const inheritedAuthority = yield* CurrentSubagentAuthority;
9894
9978
 
9895
- if (lifecycle.closed) {
9896
- return yield* ToolBrokerUnavailableError.make({
9897
- message: "The outer Tool Call for this broker has already settled",
9898
- });
9899
- }
9900
- // A malformed result bound would fail open (`NaN` defeats every
9901
- // comparison), so it is rejected typed before the pass opens.
9902
- if (
9903
- passOptions === undefined ||
9904
- !Number.isSafeInteger(passOptions.maxResultBytes) ||
9905
- passOptions.maxResultBytes <= 0
9906
- ) {
9907
- return yield* ToolBrokerConfigurationError.make({
9908
- message: `maxResultBytes must be a positive safe integer; received ${String(passOptions?.maxResultBytes)}`,
9909
- });
9910
- }
9911
- // Capture the handler services present at the pass edge once; nothing
9912
- // inside business execution can substitute them per invocation. The
9913
- // same private-assertion contract as `provideHookServices` applies.
9914
- const handlerServices = (yield* Effect.context<never>()) as Context.Context<unknown>;
9915
- const concurrency = passOptions.concurrency ?? 4;
9916
-
9917
- if (!Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 64) {
9918
- return yield* ToolBrokerConfigurationError.make({
9919
- message: "concurrency must be an integer between 1 and 64",
9920
- });
9921
- }
9922
- const permits = yield* Semaphore.make(concurrency);
9923
- const records: Array<ProgrammaticCallRecord> = [];
9924
-
9925
- const preflightFailure = (
9926
- input: ProgrammaticToolInput,
9927
- kind: "infrastructure" | "protocol",
9928
- tag: string,
9929
- message: string,
9930
- cause?: Cause.Cause<unknown>,
9931
- ): Effect.Effect<ProgrammaticCallOutcome> => {
9932
- const outcome = programmaticOutcomeError(undefined, tag, message);
9933
-
9934
- if (preflightObserver === undefined) return Effect.succeed(outcome);
9935
-
9936
- return deliverToolFailure(preflightObserver.observer, {
9937
- _tag: "ProgrammaticPreflightFailure",
9938
- agentId: binding.context.agentId,
9939
- threadId: binding.context.threadId,
9940
- runId: binding.context.runId,
9941
- turnId: binding.turnId,
9942
- parentToolCallId: binding.outerToolCallId,
9943
- toolName: input.toolName,
9944
- ...(hasTool(toolkit.tools, input.toolName)
9945
- ? { executionClass: getToolExecutionClass(toolkit.tools[input.toolName]) }
9946
- : {}),
9947
- kind,
9948
- tag,
9949
- message: toolFailureMessage(message),
9950
- ...(cause === undefined ? {} : { cause }),
9951
- }).pipe(preflightObserver.permits.withPermits(1), Effect.as(outcome));
9952
- };
9979
+ if (lifecycle.closed) {
9980
+ return yield* ToolBrokerUnavailableError.make({
9981
+ message: "The outer Tool Call for this broker has already settled",
9982
+ });
9983
+ }
9984
+ // A malformed result bound would fail open (`NaN` defeats every
9985
+ // comparison), so it is rejected typed before the pass opens.
9986
+ if (
9987
+ passOptions === undefined ||
9988
+ !Number.isSafeInteger(passOptions.maxResultBytes) ||
9989
+ passOptions.maxResultBytes <= 0
9990
+ ) {
9991
+ return yield* ToolBrokerConfigurationError.make({
9992
+ message: `maxResultBytes must be a positive safe integer; received ${String(passOptions?.maxResultBytes)}`,
9993
+ });
9994
+ }
9995
+ // Capture the declared handler services at the pass boundary once.
9996
+ const handlerServices = yield* Effect.context<Tool.HandlerServices<Tools[keyof Tools]>>();
9997
+
9998
+ // Toolkit.handle decodes unknown parameters itself. Widen only that input boundary;
9999
+ // retain its declared schema failures and handler services for this exact Toolkit.
10000
+ const handle = toolkit.handle as (
10001
+ name: keyof Tools,
10002
+ parameters: unknown,
10003
+ id: string,
10004
+ ) => ReturnType<Toolkit.WithHandler<Tools>["handle"]>;
10005
+
10006
+ const concurrency = passOptions.concurrency ?? 4;
10007
+
10008
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 64) {
10009
+ return yield* ToolBrokerConfigurationError.make({
10010
+ message: "concurrency must be an integer between 1 and 64",
10011
+ });
10012
+ }
10013
+ const permits = yield* Semaphore.make(concurrency);
10014
+ const records: Array<ProgrammaticCallRecord> = [];
10015
+
10016
+ const preflightFailure = (
10017
+ input: ProgrammaticToolInput,
10018
+ kind: "infrastructure" | "protocol",
10019
+ tag: string,
10020
+ message: string,
10021
+ cause?: Cause.Cause<unknown>,
10022
+ ): Effect.Effect<ProgrammaticCallOutcome> => {
10023
+ const outcome = programmaticOutcomeError(undefined, tag, message);
10024
+
10025
+ if (preflightObserver === undefined) return Effect.succeed(outcome);
10026
+
10027
+ return deliverToolFailure(preflightObserver.observer, {
10028
+ _tag: "ProgrammaticPreflightFailure",
10029
+ agentId: binding.context.agentId,
10030
+ threadId: binding.context.threadId,
10031
+ runId: binding.context.runId,
10032
+ turnId: binding.turnId,
10033
+ parentToolCallId: binding.outerToolCallId,
10034
+ toolName: input.toolName,
10035
+ ...(hasTool(toolkit.tools, input.toolName)
10036
+ ? { executionClass: getToolExecutionClass(toolkit.tools[input.toolName]) }
10037
+ : {}),
10038
+ kind,
10039
+ tag,
10040
+ message: toolFailureMessage(message),
10041
+ ...(cause === undefined ? {} : { cause }),
10042
+ }).pipe(preflightObserver.permits.withPermits(1), Effect.as(outcome));
10043
+ };
9953
10044
 
9954
- const body = (
9955
- input: ProgrammaticToolInput,
9956
- sequenceIndex: number,
9957
- ): Effect.Effect<ProgrammaticCallOutcome> =>
9958
- Effect.gen(function* () {
9959
- if (!hasTool(toolkit.tools, input.toolName)) {
9960
- return yield* preflightFailure(
9961
- input,
9962
- "infrastructure",
9963
- "ProgrammaticToolUnknownError",
9964
- `Tool ${input.toolName} is not part of this pass's allowlisted Toolkit`,
9965
- );
9966
- }
9967
- const tool = toolkit.tools[input.toolName] as Tool.Any;
10045
+ const body = (
10046
+ input: ProgrammaticToolInput,
10047
+ sequenceIndex: number,
10048
+ ): Effect.Effect<ProgrammaticCallOutcome> =>
10049
+ Effect.gen(function* () {
10050
+ if (!hasTool(toolkit.tools, input.toolName)) {
10051
+ return yield* preflightFailure(
10052
+ input,
10053
+ "infrastructure",
10054
+ "ProgrammaticToolUnknownError",
10055
+ `Tool ${input.toolName} is not part of this pass's allowlisted Toolkit`,
10056
+ );
10057
+ }
10058
+ const tool = toolkit.tools[input.toolName];
9968
10059
 
9969
- const advertised = Object.values(binding.context.definition.toolkit.tools).some(
9970
- (outer) =>
9971
- Context.get(outer.annotations, AdditionalToolCatalog).some(
9972
- (entry) => entry.tool.name === input.toolName,
9973
- ),
10060
+ const advertised = Object.values(binding.context.definition.toolkit.tools).some((outer) =>
10061
+ Context.get(outer.annotations, AdditionalToolCatalog).some(
10062
+ (entry) => entry.tool.name === input.toolName,
10063
+ ),
10064
+ );
10065
+
10066
+ if (
10067
+ advertised &&
10068
+ !binding.context.toolCatalog.some(
10069
+ (entry) => entry.kind === "code-mode" && entry.tool.name === input.toolName,
10070
+ )
10071
+ )
10072
+ return yield* preflightFailure(
10073
+ input,
10074
+ "infrastructure",
10075
+ "ProgrammaticToolAuthorizationDenied",
10076
+ "Tool is excluded by the current host visibility policy",
9974
10077
  );
9975
10078
 
9976
- if (
9977
- advertised &&
9978
- !binding.context.toolCatalog.some(
9979
- (entry) => entry.kind === "code-mode" && entry.tool.name === input.toolName,
9980
- )
10079
+ if (
10080
+ !isSubagentToolAllowed(
10081
+ inheritedAuthority?.grant,
10082
+ inheritedAuthority?.depth ?? 0,
10083
+ input.toolName,
10084
+ tool.annotations,
9981
10085
  )
9982
- return yield* preflightFailure(
9983
- input,
9984
- "infrastructure",
9985
- "ProgrammaticToolAuthorizationDenied",
9986
- "Tool is excluded by the current host visibility policy",
9987
- );
10086
+ ) {
10087
+ return yield* preflightFailure(
10088
+ input,
10089
+ "infrastructure",
10090
+ "ProgrammaticToolAuthorizationDenied",
10091
+ "Tool exceeds the inherited subagent grant",
10092
+ );
10093
+ }
9988
10094
 
9989
- if (
9990
- !isSubagentToolAllowed(
9991
- inheritedAuthority?.grant,
9992
- inheritedAuthority?.depth ?? 0,
9993
- input.toolName,
9994
- tool.annotations,
9995
- )
9996
- ) {
9997
- return yield* preflightFailure(
9998
- input,
9999
- "infrastructure",
10000
- "ProgrammaticToolAuthorizationDenied",
10001
- "Tool exceeds the inherited subagent grant",
10002
- );
10003
- }
10095
+ if (Context.get(tool.annotations, ContextRolloverTool)) {
10096
+ return yield* preflightFailure(
10097
+ input,
10098
+ "infrastructure",
10099
+ "ProgrammaticContextRolloverUnsupportedError",
10100
+ "Context rollover requires a direct, singleton model Tool Call",
10101
+ );
10102
+ }
10103
+ const approval = tool.needsApproval;
10104
+
10105
+ if (approval !== undefined && approval !== false) {
10106
+ return yield* preflightFailure(
10107
+ input,
10108
+ "infrastructure",
10109
+ "ProgrammaticApprovalUnsupportedError",
10110
+ `Tool ${input.toolName} requires approval; approval-requiring Tools never start programmatically in the ephemeral slice`,
10111
+ );
10112
+ }
10004
10113
 
10005
- if (Context.get(tool.annotations, ContextRolloverTool)) {
10006
- return yield* preflightFailure(
10007
- input,
10008
- "infrastructure",
10009
- "ProgrammaticContextRolloverUnsupportedError",
10010
- "Context rollover requires a direct, singleton model Tool Call",
10011
- );
10012
- }
10013
- const approval = tool.needsApproval;
10114
+ // Validate the encoded arguments against the owning parameter
10115
+ // Schema before the handler can start; the pinned `Toolkit.handle`
10116
+ // decodes internally, so the validated encoded form passes through
10117
+ // (the same inversion as `prepareToolCall`).
10118
+ const decodeParameters = Schema.decodeUnknownEffect(tool.parametersSchema) as (
10119
+ value: unknown,
10120
+ ) => Effect.Effect<unknown, Schema.SchemaError>;
10014
10121
 
10015
- if (approval !== undefined && approval !== false) {
10016
- return yield* preflightFailure(
10122
+ const invalidParameters = yield* decodeParameters(input.encodedArguments).pipe(
10123
+ Effect.map(() => undefined),
10124
+ Effect.catchCauseFilter(Cause.findError, (error, cause) =>
10125
+ preflightFailure(
10017
10126
  input,
10018
- "infrastructure",
10019
- "ProgrammaticApprovalUnsupportedError",
10020
- `Tool ${input.toolName} requires approval; approval-requiring Tools never start programmatically in the ephemeral slice`,
10021
- );
10022
- }
10127
+ "protocol",
10128
+ "ModelProtocolError",
10129
+ `Invalid parameters for Tool ${input.toolName}: ${error.message}`,
10130
+ cause,
10131
+ ),
10132
+ ),
10133
+ );
10134
+
10135
+ if (invalidParameters !== undefined) {
10136
+ return invalidParameters;
10137
+ }
10138
+
10139
+ const handleId = ToolCallId.make(`${binding.outerToolCallId}#${sequenceIndex}`);
10023
10140
 
10024
- // Validate the encoded arguments against the owning parameter
10025
- // Schema before the handler can start; the pinned `Toolkit.handle`
10026
- // decodes internally, so the validated encoded form passes through
10027
- // (the same inversion as `prepareToolCall`).
10028
- const decodeParameters = Schema.decodeUnknownEffect(tool.parametersSchema) as (
10029
- value: unknown,
10030
- ) => Effect.Effect<unknown, Schema.SchemaError>;
10031
-
10032
- const invalidParameters = yield* decodeParameters(input.encodedArguments).pipe(
10033
- Effect.map(() => undefined),
10034
- Effect.catchCauseFilter(Cause.findError, (error, cause) =>
10141
+ const denied = yield* authorization
10142
+ .authorize({
10143
+ threadId: binding.context.threadId,
10144
+ runId: binding.context.runId,
10145
+ turnId: binding.turnId,
10146
+ turn: binding.turn,
10147
+ input: binding.context.input,
10148
+ programmatic: { parentToolCallId: binding.outerToolCallId, sequenceIndex },
10149
+ call: {
10150
+ toolCallId: handleId,
10151
+ toolName: input.toolName,
10152
+ parameters: input.encodedArguments,
10153
+ executionClass: getToolExecutionClass(tool),
10154
+ executionKind: getToolExecutionKind(tool.annotations),
10155
+ },
10156
+ })
10157
+ .pipe(
10158
+ Effect.flatMap((decision) =>
10159
+ decision._tag === "allowed"
10160
+ ? Effect.succeed(undefined)
10161
+ : preflightFailure(
10162
+ input,
10163
+ "infrastructure",
10164
+ "ProgrammaticToolAuthorizationDenied",
10165
+ decision.reason,
10166
+ ),
10167
+ ),
10168
+ Effect.catch((failure) =>
10035
10169
  preflightFailure(
10036
10170
  input,
10037
- "protocol",
10038
- "ModelProtocolError",
10039
- `Invalid parameters for Tool ${input.toolName}: ${error.message}`,
10040
- cause,
10171
+ "infrastructure",
10172
+ errorTag(failure.error),
10173
+ errorMessage(failure.error),
10174
+ failure.cause,
10041
10175
  ),
10042
10176
  ),
10043
10177
  );
10044
10178
 
10045
- if (invalidParameters !== undefined) {
10046
- return invalidParameters;
10047
- }
10179
+ if (denied !== undefined) return denied;
10048
10180
 
10049
- const handleId = ToolCallId.make(`${binding.outerToolCallId}#${sequenceIndex}`);
10050
-
10051
- const denied = yield* authorization
10052
- .authorize({
10053
- threadId: binding.context.threadId,
10054
- runId: binding.context.runId,
10055
- turnId: binding.turnId,
10056
- turn: binding.turn,
10057
- input: binding.context.input,
10058
- programmatic: { parentToolCallId: binding.outerToolCallId, sequenceIndex },
10059
- call: {
10060
- toolCallId: handleId,
10061
- toolName: input.toolName,
10062
- parameters: input.encodedArguments,
10063
- executionClass: getToolExecutionClass(tool),
10064
- executionKind: getToolExecutionKind(tool.annotations),
10065
- },
10066
- })
10067
- .pipe(
10068
- Effect.flatMap((decision) =>
10069
- decision._tag === "allowed"
10070
- ? Effect.succeed(undefined)
10071
- : preflightFailure(
10072
- input,
10073
- "infrastructure",
10074
- "ProgrammaticToolAuthorizationDenied",
10075
- decision.reason,
10076
- ),
10077
- ),
10078
- Effect.catch((failure) =>
10079
- preflightFailure(
10080
- input,
10081
- "infrastructure",
10082
- errorTag(failure.error),
10083
- errorMessage(failure.error),
10084
- failure.cause,
10085
- ),
10086
- ),
10087
- );
10088
-
10089
- if (denied !== undefined) return denied;
10181
+ // Serialize admission and durable reservation across outer handlers.
10182
+ // Once reserved, a slot is never refunded: ownership may be lost
10183
+ // after the append but before the Handler starts.
10184
+ const rejected = yield* binding.context.policyReservations.withPermit(
10185
+ Effect.gen(function* () {
10186
+ const used = binding.declaredToolCalls + binding.context.programmaticToolCalls;
10090
10187
 
10091
- // Serialize admission and durable reservation across outer handlers.
10092
- // Once reserved, a slot is never refunded: ownership may be lost
10093
- // after the append but before the Handler starts.
10094
- const rejected = yield* binding.context.policyReservations.withPermit(
10095
- Effect.gen(function* () {
10096
- const used = binding.declaredToolCalls + binding.context.programmaticToolCalls;
10188
+ if (used + 1 > binding.maxToolCalls) {
10189
+ return yield* preflightFailure(
10190
+ input,
10191
+ "infrastructure",
10192
+ "AgentPolicyError",
10193
+ `Agent exceeded its ${binding.maxToolCalls} Tool Call limit`,
10194
+ );
10195
+ }
10196
+ if (binding.budget !== undefined) {
10197
+ const exhausted = yield* provideHookServices(
10198
+ binding.budget.consume({
10199
+ modelCalls: 0,
10200
+ inputTokens: 0,
10201
+ outputTokens: 0,
10202
+ totalTokens: 0,
10203
+ toolCalls: 1,
10204
+ costMicrousd: 0,
10205
+ usage: emptyProgrammaticUsage,
10206
+ }),
10207
+ binding.hookServices,
10208
+ ).pipe(
10209
+ Effect.map(() => undefined),
10210
+ Effect.catchCauseFilter(Cause.findError, (error, cause) =>
10211
+ Effect.succeed({ error, cause }),
10212
+ ),
10213
+ );
10097
10214
 
10098
- if (used + 1 > binding.maxToolCalls) {
10215
+ if (exhausted !== undefined) {
10099
10216
  return yield* preflightFailure(
10100
10217
  input,
10101
10218
  "infrastructure",
10102
- "AgentPolicyError",
10103
- `Agent exceeded its ${binding.maxToolCalls} Tool Call limit`,
10219
+ errorTag(exhausted.error),
10220
+ errorMessage(exhausted.error),
10221
+ exhausted.cause,
10104
10222
  );
10105
10223
  }
10106
- if (binding.budget !== undefined) {
10107
- const exhausted = yield* provideHookServices(
10108
- binding.budget.consume({
10109
- modelCalls: 0,
10110
- inputTokens: 0,
10111
- outputTokens: 0,
10112
- totalTokens: 0,
10113
- toolCalls: 1,
10114
- costMicrousd: 0,
10115
- usage: emptyProgrammaticUsage,
10116
- }),
10117
- binding.hookServices,
10118
- ).pipe(
10119
- Effect.map(() => undefined),
10120
- Effect.catchCauseFilter(Cause.findError, (error, cause) =>
10121
- Effect.succeed({ error, cause }),
10122
- ),
10123
- );
10124
-
10125
- if (exhausted !== undefined) {
10126
- return yield* preflightFailure(
10224
+ }
10225
+ binding.context.programmaticToolCalls += 1;
10226
+ if (binding.reservePolicyUsage !== undefined) {
10227
+ return yield* provideHookServices(
10228
+ binding.reservePolicyUsage({
10229
+ programmaticToolCalls: binding.context.programmaticToolCalls,
10230
+ finalizationUsed: binding.context.finalizationUsed,
10231
+ }),
10232
+ binding.hookServices,
10233
+ ).pipe(
10234
+ Effect.as(undefined),
10235
+ Effect.catchCauseFilter(Cause.findError, (error, cause) =>
10236
+ preflightFailure(
10127
10237
  input,
10128
10238
  "infrastructure",
10129
- errorTag(exhausted.error),
10130
- errorMessage(exhausted.error),
10131
- exhausted.cause,
10132
- );
10133
- }
10134
- }
10135
- binding.context.programmaticToolCalls += 1;
10136
- if (binding.reservePolicyUsage !== undefined) {
10137
- return yield* provideHookServices(
10138
- binding.reservePolicyUsage({
10139
- programmaticToolCalls: binding.context.programmaticToolCalls,
10140
- finalizationUsed: binding.context.finalizationUsed,
10141
- }),
10142
- binding.hookServices,
10143
- ).pipe(
10144
- Effect.as(undefined),
10145
- Effect.catchCauseFilter(Cause.findError, (error, cause) =>
10146
- preflightFailure(
10147
- input,
10148
- "infrastructure",
10149
- errorTag(error),
10150
- errorMessage(error),
10151
- cause,
10152
- ),
10239
+ errorTag(error),
10240
+ errorMessage(error),
10241
+ cause,
10153
10242
  ),
10154
- );
10155
- }
10156
-
10157
- return undefined;
10158
- }),
10159
- );
10160
-
10161
- if (rejected !== undefined) return rejected;
10243
+ ),
10244
+ );
10245
+ }
10162
10246
 
10163
- const index = sequenceIndex;
10247
+ return undefined;
10248
+ }),
10249
+ );
10164
10250
 
10165
- records[sequenceIndex] = {
10166
- sequenceIndex,
10167
- toolName: input.toolName,
10168
- status: "uncertain",
10169
- };
10170
- const executionClass = getToolExecutionClass(tool);
10171
- let failureObservation: ProgrammaticToolFailure | undefined;
10172
-
10173
- const startedFailure = (
10174
- kind: "infrastructure" | "protocol",
10175
- tag: string,
10176
- message: string,
10177
- ): Effect.Effect<ProgrammaticCallOutcome> => {
10178
- const outcome = programmaticOutcomeError(index, tag, message);
10179
-
10180
- if (observer === undefined) return Effect.succeed(outcome);
10181
- failureObservation = {
10182
- _tag: "ProgrammaticToolFailure",
10183
- agentId: binding.context.agentId,
10184
- threadId: binding.context.threadId,
10185
- runId: binding.context.runId,
10186
- turnId: binding.turnId,
10187
- toolCallId: handleId,
10188
- parentToolCallId: binding.outerToolCallId,
10189
- sequenceIndex: index,
10190
- toolName: input.toolName,
10191
- executionClass,
10192
- kind,
10193
- tag,
10194
- message: toolFailureMessage(message),
10195
- };
10251
+ if (rejected !== undefined) return rejected;
10196
10252
 
10197
- return Effect.succeed(outcome);
10198
- };
10253
+ const index = sequenceIndex;
10199
10254
 
10200
- const telemetryDescriptor: ToolTelemetryDescriptor = {
10201
- context: binding.context,
10255
+ records[sequenceIndex] = {
10256
+ sequenceIndex,
10257
+ toolName: input.toolName,
10258
+ status: "uncertain",
10259
+ };
10260
+ const executionClass = getToolExecutionClass(tool);
10261
+ let failureObservation: ProgrammaticToolFailure | undefined;
10262
+
10263
+ const startedFailure = (
10264
+ kind: "infrastructure" | "protocol",
10265
+ tag: string,
10266
+ message: string,
10267
+ ): Effect.Effect<ProgrammaticCallOutcome> => {
10268
+ const outcome = programmaticOutcomeError(index, tag, message);
10269
+
10270
+ if (observer === undefined) return Effect.succeed(outcome);
10271
+ failureObservation = {
10272
+ _tag: "ProgrammaticToolFailure",
10273
+ agentId: binding.context.agentId,
10274
+ threadId: binding.context.threadId,
10275
+ runId: binding.context.runId,
10202
10276
  turnId: binding.turnId,
10203
10277
  toolCallId: handleId,
10204
- toolName: input.toolName,
10205
- executionClass,
10206
- invocationKind: "programmatic",
10207
- failureMode: tool.failureMode,
10208
10278
  parentToolCallId: binding.outerToolCallId,
10209
10279
  sequenceIndex: index,
10280
+ toolName: input.toolName,
10281
+ executionClass,
10282
+ kind,
10283
+ tag,
10284
+ message: toolFailureMessage(message),
10210
10285
  };
10211
10286
 
10212
- const execution = measureProgrammaticToolCall(
10213
- toolSpanTelemetry,
10214
- telemetryDescriptor,
10215
- Effect.gen(function* () {
10216
- yield* Effect.logDebug("agent programmatic tool handler started").pipe(
10217
- Effect.annotateLogs({
10287
+ return Effect.succeed(outcome);
10288
+ };
10289
+
10290
+ const telemetryDescriptor: ToolTelemetryDescriptor = {
10291
+ context: binding.context,
10292
+ turnId: binding.turnId,
10293
+ toolCallId: handleId,
10294
+ toolName: input.toolName,
10295
+ executionClass,
10296
+ invocationKind: "programmatic",
10297
+ failureMode: tool.failureMode,
10298
+ parentToolCallId: binding.outerToolCallId,
10299
+ sequenceIndex: index,
10300
+ };
10301
+
10302
+ const execution = measureProgrammaticToolCall(
10303
+ toolSpanTelemetry,
10304
+ telemetryDescriptor,
10305
+ Effect.gen(function* () {
10306
+ yield* Effect.logDebug("agent programmatic tool handler started").pipe(
10307
+ Effect.annotateLogs({
10308
+ agentId: binding.context.agentId,
10309
+ runId: binding.context.runId,
10310
+ turnId: binding.turnId,
10311
+ toolCallId: handleId,
10312
+ parentToolCallId: binding.outerToolCallId,
10313
+ toolName: input.toolName,
10314
+ sequenceIndex: index,
10315
+ }),
10316
+ );
10317
+ yield* Metric.update(toolCounter, 1);
10318
+
10319
+ let terminal:
10320
+ | {
10321
+ readonly encodedResult: unknown;
10322
+ readonly isFailure: boolean;
10323
+ readonly tag: string | undefined;
10324
+ }
10325
+ | undefined;
10326
+
10327
+ let resultAfterTerminal = false;
10328
+
10329
+ const handlerFailed = yield* Stream.unwrap(
10330
+ toolSpanTelemetry.isolateToolkitHandle(
10331
+ handle(input.toolName, input.encodedArguments, handleId),
10332
+ ),
10333
+ ).pipe(
10334
+ Stream.runForEach((result) =>
10335
+ Effect.sync(() => {
10336
+ // The direct path rejects a second result after the terminal
10337
+ // one; the broker preserves that protocol violation instead
10338
+ // of silently keeping the last value.
10339
+ if (terminal !== undefined) {
10340
+ resultAfterTerminal = true;
10341
+
10342
+ return;
10343
+ }
10344
+ if (!result.preliminary) {
10345
+ terminal = {
10346
+ encodedResult: toolResultForJson(tool, result),
10347
+ isFailure: result.isFailure,
10348
+ tag:
10349
+ observer === undefined || !result.isFailure
10350
+ ? undefined
10351
+ : errorTag(result.result),
10352
+ };
10353
+ }
10354
+ }),
10355
+ ),
10356
+ Effect.map(() => undefined),
10357
+ Effect.catchCauseFilter(Cause.findError, (error, cause) =>
10358
+ Effect.succeed(new BrokerCallFailure(error, cause)),
10359
+ ),
10360
+ );
10361
+
10362
+ if (handlerFailed instanceof BrokerCallFailure) {
10363
+ const tag = errorTag(handlerFailed.error);
10364
+
10365
+ if (observer !== undefined) {
10366
+ failureObservation = {
10367
+ _tag: "ProgrammaticToolFailure",
10218
10368
  agentId: binding.context.agentId,
10369
+ threadId: binding.context.threadId,
10219
10370
  runId: binding.context.runId,
10220
10371
  turnId: binding.turnId,
10221
10372
  toolCallId: handleId,
10222
10373
  parentToolCallId: binding.outerToolCallId,
10223
- toolName: input.toolName,
10224
10374
  sequenceIndex: index,
10225
- }),
10375
+ toolName: input.toolName,
10376
+ executionClass,
10377
+ kind: "handler-error",
10378
+ tag,
10379
+ cause: handlerFailed.cause,
10380
+ };
10381
+ }
10382
+
10383
+ return programmaticOutcomeError(index, tag, errorMessage(handlerFailed.error));
10384
+ }
10385
+ if (resultAfterTerminal) {
10386
+ return yield* startedFailure(
10387
+ "protocol",
10388
+ "ModelProtocolError",
10389
+ `Tool Call ${handleId} produced more than one terminal result`,
10390
+ );
10391
+ }
10392
+ if (terminal === undefined) {
10393
+ return yield* startedFailure(
10394
+ "protocol",
10395
+ "ModelProtocolError",
10396
+ `Tool Call ${handleId} completed without a terminal result`,
10226
10397
  );
10227
- yield* Metric.update(toolCounter, 1);
10398
+ }
10399
+ if (terminal.isFailure) {
10400
+ if (observer !== undefined) {
10401
+ failureObservation = {
10402
+ _tag: "ProgrammaticToolFailure",
10403
+ agentId: binding.context.agentId,
10404
+ threadId: binding.context.threadId,
10405
+ runId: binding.context.runId,
10406
+ turnId: binding.turnId,
10407
+ toolCallId: handleId,
10408
+ parentToolCallId: binding.outerToolCallId,
10409
+ sequenceIndex: index,
10410
+ toolName: input.toolName,
10411
+ executionClass,
10412
+ kind: "declared-failure",
10413
+ tag: terminal.tag ?? "UnknownError",
10414
+ };
10415
+ }
10228
10416
 
10229
- let terminal:
10230
- | {
10231
- readonly encodedResult: unknown;
10232
- readonly isFailure: boolean;
10233
- readonly tag: string | undefined;
10234
- }
10235
- | undefined;
10236
-
10237
- let resultAfterTerminal = false;
10238
-
10239
- const handlerFailed = yield* Stream.unwrap(
10240
- toolSpanTelemetry.isolateToolkitHandle(
10241
- (
10242
- toolkit.handle as (
10243
- name: string,
10244
- params: unknown,
10245
- id: string,
10246
- ) => Effect.Effect<
10247
- Stream.Stream<Tool.HandlerResult<Tool.Any>, unknown, unknown>,
10248
- unknown,
10249
- unknown
10250
- >
10251
- )(input.toolName, input.encodedArguments, handleId),
10252
- ),
10253
- ).pipe(
10254
- Stream.runForEach((result) =>
10255
- Effect.sync(() => {
10256
- // The direct path rejects a second result after the terminal
10257
- // one; the broker preserves that protocol violation instead
10258
- // of silently keeping the last value.
10259
- if (terminal !== undefined) {
10260
- resultAfterTerminal = true;
10261
-
10262
- return;
10263
- }
10264
- if (!result.preliminary) {
10265
- terminal = {
10266
- encodedResult: toolResultForJson(tool, result),
10267
- isFailure: result.isFailure,
10268
- tag:
10269
- observer === undefined || !result.isFailure
10270
- ? undefined
10271
- : errorTag(result.result),
10272
- };
10273
- }
10274
- }),
10275
- ),
10276
- Effect.map(() => undefined),
10277
- Effect.catchCauseFilter(Cause.findError, (error, cause) =>
10278
- Effect.succeed(new BrokerCallFailure(error, cause)),
10279
- ),
10417
+ return {
10418
+ _tag: "ProgrammaticCallFailure",
10419
+ index,
10420
+ encodedResult: terminal.encodedResult,
10421
+ } as const;
10422
+ }
10423
+ if (Option.isNone(brokerDecodeJson(terminal.encodedResult))) {
10424
+ return yield* startedFailure(
10425
+ "protocol",
10426
+ "ModelProtocolError",
10427
+ `Tool ${input.toolName} produced a success encoding outside JSON`,
10280
10428
  );
10429
+ }
10430
+ let encodedResult = terminal.encodedResult;
10281
10431
 
10282
- if (handlerFailed instanceof BrokerCallFailure) {
10283
- const tag = errorTag(handlerFailed.error);
10284
-
10285
- if (observer !== undefined) {
10286
- failureObservation = {
10287
- _tag: "ProgrammaticToolFailure",
10288
- agentId: binding.context.agentId,
10289
- threadId: binding.context.threadId,
10290
- runId: binding.context.runId,
10291
- turnId: binding.turnId,
10292
- toolCallId: handleId,
10293
- parentToolCallId: binding.outerToolCallId,
10294
- sequenceIndex: index,
10295
- toolName: input.toolName,
10296
- executionClass,
10297
- kind: "handler-error",
10298
- tag,
10299
- cause: handlerFailed.cause,
10300
- };
10301
- }
10432
+ if (passOptions.redactResult !== undefined) {
10433
+ // A redactor is a substitution point: its replacement re-crosses
10434
+ // the JSON boundary or the call fails closed.
10435
+ const redacted = brokerDecodeJson(yield* passOptions.redactResult(encodedResult));
10302
10436
 
10303
- return programmaticOutcomeError(index, tag, errorMessage(handlerFailed.error));
10304
- }
10305
- if (resultAfterTerminal) {
10306
- return yield* startedFailure(
10307
- "protocol",
10308
- "ModelProtocolError",
10309
- `Tool Call ${handleId} produced more than one terminal result`,
10310
- );
10311
- }
10312
- if (terminal === undefined) {
10437
+ if (Option.isNone(redacted)) {
10313
10438
  return yield* startedFailure(
10314
10439
  "protocol",
10315
10440
  "ModelProtocolError",
10316
- `Tool Call ${handleId} completed without a terminal result`,
10441
+ `The redacted result for Tool ${input.toolName} is outside the JSON surface`,
10317
10442
  );
10318
10443
  }
10319
- if (terminal.isFailure) {
10320
- if (observer !== undefined) {
10321
- failureObservation = {
10322
- _tag: "ProgrammaticToolFailure",
10323
- agentId: binding.context.agentId,
10324
- threadId: binding.context.threadId,
10325
- runId: binding.context.runId,
10326
- turnId: binding.turnId,
10327
- toolCallId: handleId,
10328
- parentToolCallId: binding.outerToolCallId,
10329
- sequenceIndex: index,
10330
- toolName: input.toolName,
10331
- executionClass,
10332
- kind: "declared-failure",
10333
- tag: terminal.tag ?? "UnknownError",
10334
- };
10335
- }
10444
+ encodedResult = redacted.value;
10445
+ }
10446
+ const snapshot = brokerSerializeJson(encodedResult);
10336
10447
 
10337
- return {
10338
- _tag: "ProgrammaticCallFailure",
10339
- index,
10340
- encodedResult: terminal.encodedResult,
10341
- } as const;
10342
- }
10343
- if (Option.isNone(brokerDecodeJson(terminal.encodedResult))) {
10344
- return yield* startedFailure(
10345
- "protocol",
10346
- "ModelProtocolError",
10347
- `Tool ${input.toolName} produced a success encoding outside JSON`,
10348
- );
10349
- }
10350
- let encodedResult = terminal.encodedResult;
10351
-
10352
- if (passOptions.redactResult !== undefined) {
10353
- // A redactor is a substitution point: its replacement re-crosses
10354
- // the JSON boundary or the call fails closed.
10355
- const redacted = brokerDecodeJson(yield* passOptions.redactResult(encodedResult));
10356
-
10357
- if (Option.isNone(redacted)) {
10358
- return yield* startedFailure(
10359
- "protocol",
10360
- "ModelProtocolError",
10361
- `The redacted result for Tool ${input.toolName} is outside the JSON surface`,
10362
- );
10363
- }
10364
- encodedResult = redacted.value;
10365
- }
10366
- const snapshot = brokerSerializeJson(encodedResult);
10448
+ if (snapshot === undefined || snapshot.bytes > passOptions.maxResultBytes) {
10449
+ return yield* startedFailure(
10450
+ "infrastructure",
10451
+ "ProgrammaticResultLimitError",
10452
+ `Tool ${input.toolName} result of ${snapshot?.bytes ?? "unencodable"} bytes exceeds the ${passOptions.maxResultBytes}-byte broker bound`,
10453
+ );
10454
+ }
10367
10455
 
10368
- if (snapshot === undefined || snapshot.bytes > passOptions.maxResultBytes) {
10369
- return yield* startedFailure(
10370
- "infrastructure",
10371
- "ProgrammaticResultLimitError",
10372
- `Tool ${input.toolName} result of ${snapshot?.bytes ?? "unencodable"} bytes exceeds the ${passOptions.maxResultBytes}-byte broker bound`,
10373
- );
10374
- }
10456
+ // Retain the exact representation admitted above. Reusing the handler or redactor
10457
+ // value would let later mutation or stateful getters escape the byte bound.
10458
+ const owned = Schema.decodeOption(Schema.fromJsonString(Schema.Json))(snapshot.text);
10375
10459
 
10376
- // Retain the exact representation admitted above. Reusing the handler or redactor
10377
- // value would let later mutation or stateful getters escape the byte bound.
10378
- const owned = Schema.decodeOption(Schema.fromJsonString(Schema.Json))(
10379
- snapshot.text,
10460
+ if (Option.isNone(owned)) {
10461
+ return yield* startedFailure(
10462
+ "protocol",
10463
+ "ModelProtocolError",
10464
+ `Tool ${input.toolName} produced a success encoding outside JSON`,
10380
10465
  );
10466
+ }
10381
10467
 
10382
- if (Option.isNone(owned)) {
10383
- return yield* startedFailure(
10384
- "protocol",
10385
- "ModelProtocolError",
10386
- `Tool ${input.toolName} produced a success encoding outside JSON`,
10387
- );
10388
- }
10468
+ return {
10469
+ _tag: "ProgrammaticCallSuccess",
10470
+ index,
10471
+ encodedResult: owned.value,
10472
+ } as const;
10473
+ }),
10474
+ );
10389
10475
 
10390
- return {
10391
- _tag: "ProgrammaticCallSuccess",
10392
- index,
10393
- encodedResult: owned.value,
10394
- } as const;
10476
+ // Fix the broker outcome and terminal telemetry before delivery. Interruption of an
10477
+ // observer cannot turn a settled inner failure into an interrupted Handler attempt.
10478
+ // This still runs inline under the outer call's permit, before pass.invoke returns.
10479
+ const recorded = execution.pipe(
10480
+ Effect.tap((outcome) =>
10481
+ Effect.sync(() => {
10482
+ records[sequenceIndex] = {
10483
+ sequenceIndex,
10484
+ toolName: input.toolName,
10485
+ status:
10486
+ outcome._tag === "ProgrammaticCallSuccess"
10487
+ ? "succeeded"
10488
+ : outcome._tag === "ProgrammaticCallFailure"
10489
+ ? "failed"
10490
+ : "uncertain",
10491
+ ...(outcome._tag === "ProgrammaticCallError"
10492
+ ? { errorTag: outcome.errorTag }
10493
+ : {}),
10494
+ };
10395
10495
  }),
10396
- );
10496
+ ),
10497
+ );
10397
10498
 
10398
- // Fix the broker outcome and terminal telemetry before delivery. Interruption of an
10399
- // observer cannot turn a settled inner failure into an interrupted Handler attempt.
10400
- // This still runs inline under the outer call's permit, before pass.invoke returns.
10401
- const recorded = execution.pipe(
10402
- Effect.tap((outcome) =>
10403
- Effect.sync(() => {
10404
- records[sequenceIndex] = {
10405
- sequenceIndex,
10406
- toolName: input.toolName,
10407
- status:
10408
- outcome._tag === "ProgrammaticCallSuccess"
10409
- ? "succeeded"
10410
- : outcome._tag === "ProgrammaticCallFailure"
10411
- ? "failed"
10412
- : "uncertain",
10413
- ...(outcome._tag === "ProgrammaticCallError"
10414
- ? { errorTag: outcome.errorTag }
10415
- : {}),
10416
- };
10417
- }),
10418
- ),
10419
- );
10499
+ return yield* observer === undefined
10500
+ ? recorded
10501
+ : recorded.pipe(
10502
+ Effect.tap(() =>
10503
+ failureObservation === undefined
10504
+ ? Effect.void
10505
+ : deliverToolFailure(observer, failureObservation),
10506
+ ),
10507
+ );
10508
+ }).pipe(
10509
+ Effect.updateContext((current: Context.Context<never>) =>
10510
+ Context.merge(current, handlerServices),
10511
+ ),
10512
+ );
10420
10513
 
10421
- return yield* observer === undefined
10422
- ? recorded
10423
- : recorded.pipe(
10424
- Effect.tap(() =>
10425
- failureObservation === undefined
10426
- ? Effect.void
10427
- : deliverToolFailure(observer, failureObservation),
10428
- ),
10429
- );
10430
- }).pipe(Effect.provideContext(handlerServices)) as Effect.Effect<ProgrammaticCallOutcome>;
10431
-
10432
- const pass: ToolBrokerPass = {
10433
- snapshot: Effect.sync(() => records.map((record) => ({ ...record }))),
10434
- invoke: (input) =>
10435
- Effect.suspend(() => {
10436
- const sequenceIndex = records.length;
10437
-
10438
- records.push({ sequenceIndex, toolName: input.toolName, status: "not-started" });
10439
-
10440
- // Check after acquiring the permit too: queued work cannot start after its owner exits.
10441
- return permits
10442
- .withPermit(
10443
- Effect.suspend(() =>
10444
- lifecycle.closed
10445
- ? preflightFailure(
10446
- input,
10447
- "infrastructure",
10448
- "ToolBrokerUnavailableError",
10449
- "The outer Tool Call for this pass has already settled",
10450
- )
10451
- : body(input, sequenceIndex),
10452
- ),
10453
- )
10454
- .pipe(
10455
- Effect.tap((outcome) =>
10456
- Effect.sync(() => {
10457
- if (outcome._tag === "ProgrammaticCallError" && outcome.index === undefined) {
10458
- records[sequenceIndex] = {
10459
- sequenceIndex,
10460
- toolName: input.toolName,
10461
- status: "not-started",
10462
- errorTag: outcome.errorTag,
10463
- };
10464
- }
10465
- }),
10466
- ),
10467
- );
10468
- }),
10469
- };
10514
+ const pass: ToolBrokerPass = {
10515
+ snapshot: Effect.sync(() => records.map((record) => ({ ...record }))),
10516
+ invoke: (input) =>
10517
+ Effect.suspend(() => {
10518
+ const sequenceIndex = records.length;
10470
10519
 
10471
- return pass;
10472
- }),
10520
+ records.push({ sequenceIndex, toolName: input.toolName, status: "not-started" });
10521
+
10522
+ // Check after acquiring the permit too: queued work cannot start after its owner exits.
10523
+ return permits
10524
+ .withPermit(
10525
+ Effect.suspend(() =>
10526
+ lifecycle.closed
10527
+ ? preflightFailure(
10528
+ input,
10529
+ "infrastructure",
10530
+ "ToolBrokerUnavailableError",
10531
+ "The outer Tool Call for this pass has already settled",
10532
+ )
10533
+ : body(input, sequenceIndex),
10534
+ ),
10535
+ )
10536
+ .pipe(
10537
+ Effect.tap((outcome) =>
10538
+ Effect.sync(() => {
10539
+ if (outcome._tag === "ProgrammaticCallError" && outcome.index === undefined) {
10540
+ records[sequenceIndex] = {
10541
+ sequenceIndex,
10542
+ toolName: input.toolName,
10543
+ status: "not-started",
10544
+ errorTag: outcome.errorTag,
10545
+ };
10546
+ }
10547
+ }),
10548
+ ),
10549
+ );
10550
+ }),
10551
+ };
10552
+
10553
+ return pass;
10554
+ }),
10473
10555
  };
10474
10556
 
10475
10557
  return {
@@ -10553,98 +10635,97 @@ const makeDurableStepService = <HookError, HookRequirements>(
10553
10635
  const usedNames = new Set<string>();
10554
10636
 
10555
10637
  return {
10556
- do: <Output extends Schema.Top, BodyError, BodyServices>(
10638
+ do: Effect.fnUntraced(function* <Output extends Schema.Top, BodyError, BodyServices>(
10557
10639
  name: string,
10558
10640
  output: Output,
10559
10641
  execute: Effect.Effect<Output["Type"], BodyError, BodyServices>,
10560
- ) =>
10561
- Effect.gen(function* () {
10562
- if (usedNames.has(name)) {
10563
- return yield* DurableStepError.make({
10642
+ ) {
10643
+ if (usedNames.has(name)) {
10644
+ return yield* DurableStepError.make({
10645
+ toolCallId,
10646
+ stepName: name,
10647
+ reason: "duplicate-step-name",
10648
+ message: "Durable Step name was reused within one Tool Call",
10649
+ });
10650
+ }
10651
+ usedNames.add(name);
10652
+ const key: RunStepKey = { toolCallId, stepName: name };
10653
+
10654
+ const recorded = yield* provideHookServices(hook.lookup(key), hookServices).pipe(
10655
+ Effect.mapError((cause) =>
10656
+ DurableStepError.make({
10564
10657
  toolCallId,
10565
10658
  stepName: name,
10566
- reason: "duplicate-step-name",
10567
- message: "Durable Step name was reused within one Tool Call",
10568
- });
10569
- }
10570
- usedNames.add(name);
10571
- const key: RunStepKey = { toolCallId, stepName: name };
10659
+ reason: "lookup-failed",
10660
+ message: "Durable Step lookup failed",
10661
+ cause,
10662
+ }),
10663
+ ),
10664
+ );
10572
10665
 
10573
- const recorded = yield* provideHookServices(hook.lookup(key), hookServices).pipe(
10574
- Effect.mapError((cause) =>
10666
+ if (Option.isSome(recorded)) {
10667
+ return yield* Schema.decodeUnknownEffect(output)(recorded.value.encodedOutput).pipe(
10668
+ Effect.mapError(() =>
10575
10669
  DurableStepError.make({
10576
10670
  toolCallId,
10577
10671
  stepName: name,
10578
- reason: "lookup-failed",
10579
- message: "Durable Step lookup failed",
10580
- cause,
10672
+ reason: "recorded-result-invalid",
10673
+ message: "Recorded Durable Step result failed the declared output Schema",
10581
10674
  }),
10582
10675
  ),
10583
10676
  );
10677
+ }
10584
10678
 
10585
- if (Option.isSome(recorded)) {
10586
- return yield* Schema.decodeUnknownEffect(output)(recorded.value.encodedOutput).pipe(
10587
- Effect.mapError(() =>
10588
- DurableStepError.make({
10589
- toolCallId,
10590
- stepName: name,
10591
- reason: "recorded-result-invalid",
10592
- message: "Recorded Durable Step result failed the declared output Schema",
10593
- }),
10594
- ),
10595
- );
10596
- }
10597
-
10598
- return yield* Effect.acquireUseRelease(
10599
- provideHookServices(hook.reserve(key), hookServices).pipe(
10600
- Effect.mapError((cause) =>
10601
- DurableStepError.make({
10602
- toolCallId,
10603
- stepName: name,
10604
- reason:
10605
- cause instanceof AgentPersistenceCapacityError
10606
- ? "capacity-exhausted"
10607
- : "reservation-failed",
10608
- message:
10609
- cause instanceof AgentPersistenceCapacityError
10610
- ? "Durable Step has no room for a valid saved result"
10611
- : "Durable Step capacity reservation failed",
10612
- cause,
10613
- }),
10614
- ),
10679
+ return yield* Effect.acquireUseRelease(
10680
+ provideHookServices(hook.reserve(key), hookServices).pipe(
10681
+ Effect.mapError((cause) =>
10682
+ DurableStepError.make({
10683
+ toolCallId,
10684
+ stepName: name,
10685
+ reason:
10686
+ cause instanceof AgentPersistenceCapacityError
10687
+ ? "capacity-exhausted"
10688
+ : "reservation-failed",
10689
+ message:
10690
+ cause instanceof AgentPersistenceCapacityError
10691
+ ? "Durable Step has no room for a valid saved result"
10692
+ : "Durable Step capacity reservation failed",
10693
+ cause,
10694
+ }),
10615
10695
  ),
10616
- () =>
10617
- Effect.gen(function* () {
10618
- const value = yield* execute;
10619
-
10620
- const encodedOutput = yield* Schema.encodeEffect(output)(value).pipe(
10621
- Effect.mapError(() =>
10622
- DurableStepError.make({
10623
- toolCallId,
10624
- stepName: name,
10625
- reason: "output-encoding-failed",
10626
- message: "Durable Step output failed the declared output Schema",
10627
- }),
10628
- ),
10629
- );
10696
+ ),
10697
+ () =>
10698
+ Effect.gen(function* () {
10699
+ const value = yield* execute;
10700
+
10701
+ const encodedOutput = yield* Schema.encodeEffect(output)(value).pipe(
10702
+ Effect.mapError(() =>
10703
+ DurableStepError.make({
10704
+ toolCallId,
10705
+ stepName: name,
10706
+ reason: "output-encoding-failed",
10707
+ message: "Durable Step output failed the declared output Schema",
10708
+ }),
10709
+ ),
10710
+ );
10630
10711
 
10631
- yield* provideHookServices(hook.commit(key, encodedOutput), hookServices).pipe(
10632
- Effect.mapError((cause) =>
10633
- DurableStepError.make({
10634
- toolCallId,
10635
- stepName: name,
10636
- reason: "commit-failed",
10637
- message: "Durable Step commit failed",
10638
- cause,
10639
- }),
10640
- ),
10641
- );
10712
+ yield* provideHookServices(hook.commit(key, encodedOutput), hookServices).pipe(
10713
+ Effect.mapError((cause) =>
10714
+ DurableStepError.make({
10715
+ toolCallId,
10716
+ stepName: name,
10717
+ reason: "commit-failed",
10718
+ message: "Durable Step commit failed",
10719
+ cause,
10720
+ }),
10721
+ ),
10722
+ );
10642
10723
 
10643
- return value;
10644
- }),
10645
- (release) => release,
10646
- );
10647
- }),
10724
+ return value;
10725
+ }),
10726
+ (release) => release,
10727
+ );
10728
+ }),
10648
10729
  };
10649
10730
  };
10650
10731