@animalabs/agent-framework 0.21.1 → 0.22.1

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 (348) hide show
  1. package/README.md +54 -4
  2. package/dist/src/agent.d.ts +17 -4
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +105 -43
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/server.d.ts.map +1 -1
  7. package/dist/src/api/server.js +41 -4
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/code-execution/py-runner.d.ts +22 -1
  10. package/dist/src/code-execution/py-runner.d.ts.map +1 -1
  11. package/dist/src/code-execution/py-runner.js +64 -9
  12. package/dist/src/code-execution/py-runner.js.map +1 -1
  13. package/dist/src/code-execution/runtime-py.d.ts +4 -0
  14. package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
  15. package/dist/src/code-execution/runtime-py.js +215 -38
  16. package/dist/src/code-execution/runtime-py.js.map +1 -1
  17. package/dist/src/framework.d.ts +569 -59
  18. package/dist/src/framework.d.ts.map +1 -1
  19. package/dist/src/framework.js +3091 -542
  20. package/dist/src/framework.js.map +1 -1
  21. package/dist/src/gate/event-gate.d.ts +21 -0
  22. package/dist/src/gate/event-gate.d.ts.map +1 -1
  23. package/dist/src/gate/event-gate.js +40 -7
  24. package/dist/src/gate/event-gate.js.map +1 -1
  25. package/dist/src/index.d.ts +7 -2
  26. package/dist/src/index.d.ts.map +1 -1
  27. package/dist/src/index.js +4 -0
  28. package/dist/src/index.js.map +1 -1
  29. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  30. package/dist/src/mcpl/channel-registry.js +26 -3
  31. package/dist/src/mcpl/channel-registry.js.map +1 -1
  32. package/dist/src/mcpl/conversation-router.d.ts +28 -1
  33. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  34. package/dist/src/mcpl/conversation-router.js +17 -0
  35. package/dist/src/mcpl/conversation-router.js.map +1 -1
  36. package/dist/src/mcpl/feature-set-manager.d.ts +11 -0
  37. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  38. package/dist/src/mcpl/feature-set-manager.js +31 -7
  39. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  40. package/dist/src/mcpl/index.d.ts +4 -0
  41. package/dist/src/mcpl/index.d.ts.map +1 -1
  42. package/dist/src/mcpl/index.js +1 -0
  43. package/dist/src/mcpl/index.js.map +1 -1
  44. package/dist/src/mcpl/prose-stream-router.d.ts +27 -6
  45. package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
  46. package/dist/src/mcpl/prose-stream-router.js +85 -22
  47. package/dist/src/mcpl/prose-stream-router.js.map +1 -1
  48. package/dist/src/mcpl/push-coalescer.d.ts +32 -4
  49. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  50. package/dist/src/mcpl/push-coalescer.js +46 -13
  51. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  52. package/dist/src/mcpl/push-handler.d.ts +5 -3
  53. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  54. package/dist/src/mcpl/push-handler.js +41 -11
  55. package/dist/src/mcpl/push-handler.js.map +1 -1
  56. package/dist/src/mcpl/reference-fetcher.d.ts +2 -1
  57. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  58. package/dist/src/mcpl/reference-fetcher.js +20 -9
  59. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  60. package/dist/src/mcpl/references.d.ts +29 -6
  61. package/dist/src/mcpl/references.d.ts.map +1 -1
  62. package/dist/src/mcpl/references.js +57 -16
  63. package/dist/src/mcpl/references.js.map +1 -1
  64. package/dist/src/mcpl/server-connection.d.ts +32 -1
  65. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  66. package/dist/src/mcpl/server-connection.js +76 -11
  67. package/dist/src/mcpl/server-connection.js.map +1 -1
  68. package/dist/src/mcpl/server-registry.d.ts +5 -2
  69. package/dist/src/mcpl/server-registry.d.ts.map +1 -1
  70. package/dist/src/mcpl/server-registry.js +25 -7
  71. package/dist/src/mcpl/server-registry.js.map +1 -1
  72. package/dist/src/mcpl/tool-lifecycle.d.ts +52 -2
  73. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  74. package/dist/src/mcpl/tool-lifecycle.js +47 -1
  75. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  76. package/dist/src/mcpl/tool-pattern-check.d.ts +51 -0
  77. package/dist/src/mcpl/tool-pattern-check.d.ts.map +1 -0
  78. package/dist/src/mcpl/tool-pattern-check.js +79 -0
  79. package/dist/src/mcpl/tool-pattern-check.js.map +1 -0
  80. package/dist/src/mcpl/types.d.ts +15 -5
  81. package/dist/src/mcpl/types.d.ts.map +1 -1
  82. package/dist/src/mcpl/types.js.map +1 -1
  83. package/dist/src/mcpl/visible-content.d.ts +39 -0
  84. package/dist/src/mcpl/visible-content.d.ts.map +1 -0
  85. package/dist/src/mcpl/visible-content.js +54 -0
  86. package/dist/src/mcpl/visible-content.js.map +1 -0
  87. package/dist/src/module-registry.d.ts +90 -6
  88. package/dist/src/module-registry.d.ts.map +1 -1
  89. package/dist/src/module-registry.js +260 -18
  90. package/dist/src/module-registry.js.map +1 -1
  91. package/dist/src/modules/workspace/branch-intent.d.ts +72 -0
  92. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -0
  93. package/dist/src/modules/workspace/branch-intent.js +83 -0
  94. package/dist/src/modules/workspace/branch-intent.js.map +1 -0
  95. package/dist/src/modules/workspace/disk-agreement.d.ts +167 -0
  96. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -0
  97. package/dist/src/modules/workspace/disk-agreement.js +259 -0
  98. package/dist/src/modules/workspace/disk-agreement.js.map +1 -0
  99. package/dist/src/modules/workspace/effects.d.ts +84 -0
  100. package/dist/src/modules/workspace/effects.d.ts.map +1 -0
  101. package/dist/src/modules/workspace/effects.js +368 -0
  102. package/dist/src/modules/workspace/effects.js.map +1 -0
  103. package/dist/src/modules/workspace/index.d.ts +155 -17
  104. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  105. package/dist/src/modules/workspace/index.js +1101 -424
  106. package/dist/src/modules/workspace/index.js.map +1 -1
  107. package/dist/src/modules/workspace/observe.d.ts +288 -0
  108. package/dist/src/modules/workspace/observe.d.ts.map +1 -0
  109. package/dist/src/modules/workspace/observe.js +825 -0
  110. package/dist/src/modules/workspace/observe.js.map +1 -0
  111. package/dist/src/modules/workspace/reconcile.d.ts +238 -0
  112. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -0
  113. package/dist/src/modules/workspace/reconcile.js +909 -0
  114. package/dist/src/modules/workspace/reconcile.js.map +1 -0
  115. package/dist/src/modules/workspace/sync.d.ts +6 -59
  116. package/dist/src/modules/workspace/sync.d.ts.map +1 -1
  117. package/dist/src/modules/workspace/sync.js +6 -257
  118. package/dist/src/modules/workspace/sync.js.map +1 -1
  119. package/dist/src/modules/workspace/types.d.ts +77 -10
  120. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  121. package/dist/src/modules/workspace/types.js.map +1 -1
  122. package/dist/src/operator-log.d.ts +41 -0
  123. package/dist/src/operator-log.d.ts.map +1 -1
  124. package/dist/src/operator-log.js.map +1 -1
  125. package/dist/src/provider-waits.d.ts +216 -0
  126. package/dist/src/provider-waits.d.ts.map +1 -0
  127. package/dist/src/provider-waits.js +482 -0
  128. package/dist/src/provider-waits.js.map +1 -0
  129. package/dist/src/record-journal.d.ts +127 -0
  130. package/dist/src/record-journal.d.ts.map +1 -0
  131. package/dist/src/record-journal.js +202 -0
  132. package/dist/src/record-journal.js.map +1 -0
  133. package/dist/src/refusal-reactions.d.ts +14 -12
  134. package/dist/src/refusal-reactions.d.ts.map +1 -1
  135. package/dist/src/refusal-reactions.js +14 -12
  136. package/dist/src/refusal-reactions.js.map +1 -1
  137. package/dist/src/shutdown.d.ts +13 -0
  138. package/dist/src/shutdown.d.ts.map +1 -0
  139. package/dist/src/shutdown.js +46 -0
  140. package/dist/src/shutdown.js.map +1 -0
  141. package/dist/src/silent-heartbeat.d.ts +74 -0
  142. package/dist/src/silent-heartbeat.d.ts.map +1 -0
  143. package/dist/src/silent-heartbeat.js +77 -0
  144. package/dist/src/silent-heartbeat.js.map +1 -0
  145. package/dist/src/tool-presentation.d.ts +50 -0
  146. package/dist/src/tool-presentation.d.ts.map +1 -0
  147. package/dist/src/tool-presentation.js +235 -0
  148. package/dist/src/tool-presentation.js.map +1 -0
  149. package/dist/src/tool-result-guard.d.ts +142 -6
  150. package/dist/src/tool-result-guard.d.ts.map +1 -1
  151. package/dist/src/tool-result-guard.js +357 -36
  152. package/dist/src/tool-result-guard.js.map +1 -1
  153. package/dist/src/tool-result-history.d.ts +13 -0
  154. package/dist/src/tool-result-history.d.ts.map +1 -1
  155. package/dist/src/tool-result-history.js +21 -1
  156. package/dist/src/tool-result-history.js.map +1 -1
  157. package/dist/src/types/agent.d.ts +18 -2
  158. package/dist/src/types/agent.d.ts.map +1 -1
  159. package/dist/src/types/events.d.ts +16 -0
  160. package/dist/src/types/events.d.ts.map +1 -1
  161. package/dist/src/types/framework.d.ts +12 -0
  162. package/dist/src/types/framework.d.ts.map +1 -1
  163. package/dist/src/types/module.d.ts +26 -4
  164. package/dist/src/types/module.d.ts.map +1 -1
  165. package/dist/src/types/trace.d.ts +87 -3
  166. package/dist/src/types/trace.d.ts.map +1 -1
  167. package/dist/test/batched-wake-locus.test.d.ts +2 -0
  168. package/dist/test/batched-wake-locus.test.d.ts.map +1 -0
  169. package/dist/test/batched-wake-locus.test.js +166 -0
  170. package/dist/test/batched-wake-locus.test.js.map +1 -0
  171. package/dist/test/code-execution.test.js +386 -2
  172. package/dist/test/code-execution.test.js.map +1 -1
  173. package/dist/test/component-defaults.test.d.ts +2 -0
  174. package/dist/test/component-defaults.test.d.ts.map +1 -0
  175. package/dist/test/component-defaults.test.js +54 -0
  176. package/dist/test/component-defaults.test.js.map +1 -0
  177. package/dist/test/conversation-routing.test.js +57 -1
  178. package/dist/test/conversation-routing.test.js.map +1 -1
  179. package/dist/test/discord-awareness-outbox.test.js +3 -0
  180. package/dist/test/discord-awareness-outbox.test.js.map +1 -1
  181. package/dist/test/ephemeral-settle.test.js +3 -0
  182. package/dist/test/ephemeral-settle.test.js.map +1 -1
  183. package/dist/test/event-gate.test.js +118 -0
  184. package/dist/test/event-gate.test.js.map +1 -1
  185. package/dist/test/explicit-prose-routing.test.js +975 -0
  186. package/dist/test/explicit-prose-routing.test.js.map +1 -1
  187. package/dist/test/framework.test.js +160 -1
  188. package/dist/test/framework.test.js.map +1 -1
  189. package/dist/test/helpers/coalescing-fixture.d.ts.map +1 -1
  190. package/dist/test/helpers/coalescing-fixture.js +3 -2
  191. package/dist/test/helpers/coalescing-fixture.js.map +1 -1
  192. package/dist/test/helpers/pytc-stop-rows.d.ts +11 -0
  193. package/dist/test/helpers/pytc-stop-rows.d.ts.map +1 -0
  194. package/dist/test/helpers/pytc-stop-rows.js +143 -0
  195. package/dist/test/helpers/pytc-stop-rows.js.map +1 -0
  196. package/dist/test/inference-exhaustion-rewind.test.js +142 -14
  197. package/dist/test/inference-exhaustion-rewind.test.js.map +1 -1
  198. package/dist/test/inference-failure-bounds.test.d.ts +2 -0
  199. package/dist/test/inference-failure-bounds.test.d.ts.map +1 -0
  200. package/dist/test/inference-failure-bounds.test.js +1201 -0
  201. package/dist/test/inference-failure-bounds.test.js.map +1 -0
  202. package/dist/test/late-events-after-stop.test.d.ts +2 -0
  203. package/dist/test/late-events-after-stop.test.d.ts.map +1 -0
  204. package/dist/test/late-events-after-stop.test.js +122 -0
  205. package/dist/test/late-events-after-stop.test.js.map +1 -0
  206. package/dist/test/mcpl-awareness-barrier.test.js +141 -0
  207. package/dist/test/mcpl-awareness-barrier.test.js.map +1 -1
  208. package/dist/test/mcpl-coalescing.test.js +97 -22
  209. package/dist/test/mcpl-coalescing.test.js.map +1 -1
  210. package/dist/test/mcpl-config-diagnostics.test.d.ts +2 -0
  211. package/dist/test/mcpl-config-diagnostics.test.d.ts.map +1 -0
  212. package/dist/test/mcpl-config-diagnostics.test.js +452 -0
  213. package/dist/test/mcpl-config-diagnostics.test.js.map +1 -0
  214. package/dist/test/mcpl-empty-content.test.d.ts +2 -0
  215. package/dist/test/mcpl-empty-content.test.d.ts.map +1 -0
  216. package/dist/test/mcpl-empty-content.test.js +368 -0
  217. package/dist/test/mcpl-empty-content.test.js.map +1 -0
  218. package/dist/test/mcpl-manifest-changed.test.js +2 -1
  219. package/dist/test/mcpl-manifest-changed.test.js.map +1 -1
  220. package/dist/test/mcpl-reference-fetcher.test.js +232 -4
  221. package/dist/test/mcpl-reference-fetcher.test.js.map +1 -1
  222. package/dist/test/mcpl-reference-fields.test.js +69 -2
  223. package/dist/test/mcpl-reference-fields.test.js.map +1 -1
  224. package/dist/test/mcpl-tool-lifecycle-emitter.test.js +106 -1
  225. package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -1
  226. package/dist/test/mcpl-tool-lifecycle-script.test.d.ts +2 -0
  227. package/dist/test/mcpl-tool-lifecycle-script.test.d.ts.map +1 -0
  228. package/dist/test/mcpl-tool-lifecycle-script.test.js +288 -0
  229. package/dist/test/mcpl-tool-lifecycle-script.test.js.map +1 -0
  230. package/dist/test/mcpl-ws-transport.test.js +103 -0
  231. package/dist/test/mcpl-ws-transport.test.js.map +1 -1
  232. package/dist/test/ops-alert.test.js +2 -1
  233. package/dist/test/ops-alert.test.js.map +1 -1
  234. package/dist/test/placed-reaction-baseline.test.d.ts +2 -0
  235. package/dist/test/placed-reaction-baseline.test.d.ts.map +1 -0
  236. package/dist/test/placed-reaction-baseline.test.js +312 -0
  237. package/dist/test/placed-reaction-baseline.test.js.map +1 -0
  238. package/dist/test/prose-stream-router.test.js +41 -0
  239. package/dist/test/prose-stream-router.test.js.map +1 -1
  240. package/dist/test/provider-acceleration-cooldown.test.js +5 -1
  241. package/dist/test/provider-acceleration-cooldown.test.js.map +1 -1
  242. package/dist/test/provider-waits.test.d.ts +2 -0
  243. package/dist/test/provider-waits.test.d.ts.map +1 -0
  244. package/dist/test/provider-waits.test.js +2196 -0
  245. package/dist/test/provider-waits.test.js.map +1 -0
  246. package/dist/test/reaction-baseline-platform.test.d.ts +2 -0
  247. package/dist/test/reaction-baseline-platform.test.d.ts.map +1 -0
  248. package/dist/test/reaction-baseline-platform.test.js +124 -0
  249. package/dist/test/reaction-baseline-platform.test.js.map +1 -0
  250. package/dist/test/record-journal.test.d.ts +6 -0
  251. package/dist/test/record-journal.test.d.ts.map +1 -0
  252. package/dist/test/record-journal.test.js +269 -0
  253. package/dist/test/record-journal.test.js.map +1 -0
  254. package/dist/test/refusal-reaction-baseline.test.js +10 -3
  255. package/dist/test/refusal-reaction-baseline.test.js.map +1 -1
  256. package/dist/test/save-recent-image.test.js +240 -2
  257. package/dist/test/save-recent-image.test.js.map +1 -1
  258. package/dist/test/shutdown-errors.test.d.ts +2 -0
  259. package/dist/test/shutdown-errors.test.d.ts.map +1 -0
  260. package/dist/test/shutdown-errors.test.js +662 -0
  261. package/dist/test/shutdown-errors.test.js.map +1 -0
  262. package/dist/test/silent-heartbeat-rows.test.d.ts +2 -0
  263. package/dist/test/silent-heartbeat-rows.test.d.ts.map +1 -0
  264. package/dist/test/silent-heartbeat-rows.test.js +451 -0
  265. package/dist/test/silent-heartbeat-rows.test.js.map +1 -0
  266. package/dist/test/silent-heartbeat.test.js +49 -38
  267. package/dist/test/silent-heartbeat.test.js.map +1 -1
  268. package/dist/test/stop-with-work-in-flight.test.d.ts +23 -0
  269. package/dist/test/stop-with-work-in-flight.test.d.ts.map +1 -0
  270. package/dist/test/stop-with-work-in-flight.test.js +672 -0
  271. package/dist/test/stop-with-work-in-flight.test.js.map +1 -0
  272. package/dist/test/surgery-marks-receipt.test.d.ts +2 -0
  273. package/dist/test/surgery-marks-receipt.test.d.ts.map +1 -0
  274. package/dist/test/surgery-marks-receipt.test.js +540 -0
  275. package/dist/test/surgery-marks-receipt.test.js.map +1 -0
  276. package/dist/test/tool-batch-hook.test.d.ts +8 -0
  277. package/dist/test/tool-batch-hook.test.d.ts.map +1 -0
  278. package/dist/test/tool-batch-hook.test.js +275 -0
  279. package/dist/test/tool-batch-hook.test.js.map +1 -0
  280. package/dist/test/tool-presentation.test.d.ts +2 -0
  281. package/dist/test/tool-presentation.test.d.ts.map +1 -0
  282. package/dist/test/tool-presentation.test.js +268 -0
  283. package/dist/test/tool-presentation.test.js.map +1 -0
  284. package/dist/test/tool-result-guard.test.js +2545 -19
  285. package/dist/test/tool-result-guard.test.js.map +1 -1
  286. package/dist/test/tool-result-history.test.js +2 -1
  287. package/dist/test/tool-result-history.test.js.map +1 -1
  288. package/dist/test/turn-trigger-provenance.test.js +35 -0
  289. package/dist/test/turn-trigger-provenance.test.js.map +1 -1
  290. package/dist/test/unconvertible-tool-result.test.d.ts +2 -0
  291. package/dist/test/unconvertible-tool-result.test.d.ts.map +1 -0
  292. package/dist/test/unconvertible-tool-result.test.js +358 -0
  293. package/dist/test/unconvertible-tool-result.test.js.map +1 -0
  294. package/dist/test/workspace-materialize-freshness.test.d.ts +16 -0
  295. package/dist/test/workspace-materialize-freshness.test.d.ts.map +1 -0
  296. package/dist/test/workspace-materialize-freshness.test.js +252 -0
  297. package/dist/test/workspace-materialize-freshness.test.js.map +1 -0
  298. package/dist/test/workspace-three-way.test.d.ts +12 -0
  299. package/dist/test/workspace-three-way.test.d.ts.map +1 -0
  300. package/dist/test/workspace-three-way.test.js +3194 -0
  301. package/dist/test/workspace-three-way.test.js.map +1 -0
  302. package/dist/tsconfig.tsbuildinfo +1 -1
  303. package/package.json +2 -2
  304. package/src/agent.ts +124 -45
  305. package/src/api/server.ts +43 -4
  306. package/src/code-execution/py-runner.ts +66 -9
  307. package/src/code-execution/runtime-py.ts +215 -38
  308. package/src/framework.ts +3191 -544
  309. package/src/gate/event-gate.ts +59 -7
  310. package/src/index.ts +18 -2
  311. package/src/mcpl/channel-registry.ts +24 -3
  312. package/src/mcpl/conversation-router.ts +34 -1
  313. package/src/mcpl/feature-set-manager.ts +36 -11
  314. package/src/mcpl/index.ts +4 -0
  315. package/src/mcpl/prose-stream-router.ts +79 -23
  316. package/src/mcpl/push-coalescer.ts +62 -13
  317. package/src/mcpl/push-handler.ts +41 -12
  318. package/src/mcpl/reference-fetcher.ts +18 -8
  319. package/src/mcpl/references.ts +57 -13
  320. package/src/mcpl/server-connection.ts +95 -13
  321. package/src/mcpl/server-registry.ts +24 -7
  322. package/src/mcpl/tool-lifecycle.ts +77 -2
  323. package/src/mcpl/tool-pattern-check.ts +106 -0
  324. package/src/mcpl/types.ts +15 -5
  325. package/src/mcpl/visible-content.ts +57 -0
  326. package/src/module-registry.ts +269 -25
  327. package/src/modules/workspace/branch-intent.ts +123 -0
  328. package/src/modules/workspace/disk-agreement.ts +325 -0
  329. package/src/modules/workspace/effects.ts +362 -0
  330. package/src/modules/workspace/index.ts +1098 -446
  331. package/src/modules/workspace/observe.ts +865 -0
  332. package/src/modules/workspace/reconcile.ts +1157 -0
  333. package/src/modules/workspace/sync.ts +6 -336
  334. package/src/modules/workspace/types.ts +75 -10
  335. package/src/operator-log.ts +30 -0
  336. package/src/provider-waits.ts +523 -0
  337. package/src/record-journal.ts +246 -0
  338. package/src/refusal-reactions.ts +14 -12
  339. package/src/shutdown.ts +49 -0
  340. package/src/silent-heartbeat.ts +141 -0
  341. package/src/tool-presentation.ts +188 -0
  342. package/src/tool-result-guard.ts +384 -34
  343. package/src/tool-result-history.ts +25 -1
  344. package/src/types/agent.ts +18 -2
  345. package/src/types/events.ts +16 -0
  346. package/src/types/framework.ts +12 -0
  347. package/src/types/module.ts +27 -4
  348. package/src/types/trace.ts +94 -3
@@ -1,13 +1,115 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { isDeepStrictEqual } from 'node:util';
3
- import type { ContextManager, MessageId } from '@animalabs/context-manager';
3
+ import type { ContextManager, MessageId, MessageMetadata } from '@animalabs/context-manager';
4
4
  import type { ContentBlock, NormalizedMessage, ToolResult } from '@animalabs/membrane';
5
5
  import type { CompletedToolCall } from './types/index.js';
6
6
  import { isStateExistsError } from './module-registry.js';
7
+ import { toolResultDataToHistoryString } from './tool-result-history.js';
8
+ import { safeSlice } from './safe-slice.js';
7
9
 
8
10
  export const TOOL_RESULT_GUARD_NOTICE = 'Tool result withheld by the guard. The tool has already executed.';
9
11
  export const TOOL_RESULT_GUARD_AUDIT_STATE = 'framework/tool-result-guard';
10
12
 
13
+ /** Where a withheld result's original went: a workspace path; the path a
14
+ * write to it failed at (`error`); the path a write was still under way to
15
+ * when the host stopped waiting for it (`unfinished`); no write, because the
16
+ * workspace isn't answering (`workspace_unanswered`: an earlier write is
17
+ * still under way past the host's deadline) or because the workspace had
18
+ * left the stub's branch when the file would have been committed
19
+ * (`branch_changed`), or because the batch used its deadline before this
20
+ * write could start (`batch_deadline`); or null when no file is written at all (no writable
21
+ * workspace, or the host is stopping). */
22
+ export type WithheldSpill =
23
+ | { path: string; error?: string; unfinished?: true }
24
+ | { skipped: 'workspace_unanswered' | 'branch_changed' | 'batch_deadline' }
25
+ | null;
26
+
27
+ /**
28
+ * Writes one withheld original as a workspace file, using what remains of
29
+ * the batch's absolute deadline. A write still running then is unfinished.
30
+ * Once a write reaches that deadline, later spills are workspace_unanswered
31
+ * until all overdue writes settle. Other batches can start before that gate
32
+ * closes. An exhausted batch starts no write (batch_deadline).
33
+ * The tree entry lands on branch or the write is declined (branch_changed).
34
+ * The mount's disk directory is shared across branches.
35
+ */
36
+ export type WithheldSpiller = (label: string, text: string, branch: string, deadline: number) => Promise<WithheldSpill>;
37
+
38
+ /** What a host gives a guard for its withheld results: where originals are
39
+ * written, where each annotation is registered, so the host's stop can
40
+ * wait for it even after the agent itself was disposed, and where the
41
+ * guard says whether it has work left. */
42
+ export interface WithheldHost {
43
+ spill: WithheldSpiller;
44
+ /** Absolute deadline for one batch (Date.now() time), taken when the batch is queued. */
45
+ deadline(): number;
46
+ track(annotation: Promise<void>): void;
47
+ /** Whether the guard is idle (see ToolResultGuard.idle), told after each
48
+ * change that can alter it: when an annotation starts or ends, and after
49
+ * each pass over the storage queue. A host keeps a guard that isn't idle
50
+ * within reach for retries, whatever has become of its agent, and lets
51
+ * an idle one go. */
52
+ idle(idle: boolean): void;
53
+ }
54
+
55
+ /**
56
+ * The stub a withheld result settles to: #159's neutral notice, then where
57
+ * the original is, then, when its tool marked any, the fields kept from its
58
+ * result (`kept`, from keptWhenWithheld). It names no refusal and no
59
+ * category (#159 keeps those in operational logs), so reading the rest back
60
+ * is the agent's deliberate choice (agent-framework #277). The place holds
61
+ * the result as stored, which for one over the inline cap is its capped
62
+ * text, whose own note names the file with the whole output; so the stub
63
+ * says "its result", not its full result. A failed write's own error text
64
+ * stays in the audit record and the logs, not here. The audit record is
65
+ * said to keep the original only when it was saved: a staged record whose
66
+ * save failed is still queued in memory, and a restart before its retry
67
+ * loses it.
68
+ */
69
+ export function withheldResultNotice(spill: WithheldSpill, auditDurable: boolean, kept?: string): string {
70
+ const audit = `the guard's audit record (${TOOL_RESULT_GUARD_AUDIT_STATE}), for an operator`;
71
+ const keptIn = (only: string) => auditDurable
72
+ ? `is kept ${only}in ${audit}`
73
+ : `went ${only}to ${audit}, though saving that record had failed when the result arrived`;
74
+ const place = !spill || 'skipped' in spill
75
+ ? `Its result ${keptIn('')}.`
76
+ : spill.unfinished
77
+ ? `Its result was still being written to workspace file ${spill.path} when this note was made; ` +
78
+ `if that file is missing, it ${keptIn('')}.`
79
+ : spill.error === undefined
80
+ ? `Its result is in workspace file ${spill.path}.`
81
+ : `Writing its result to workspace file ${spill.path} failed, so it ${keptIn('only ')}.`;
82
+ const fields = kept === undefined ? '' : ` Kept from its result: ${kept}`;
83
+ return `${TOOL_RESULT_GUARD_NOTICE} ${place}${fields}`;
84
+ }
85
+
86
+ /**
87
+ * What a withheld result's stub keeps of it: the fields of its data that its
88
+ * tool marked as saying what it acted on (ToolResult.keepWhenWithheld), in
89
+ * the order marked, serialized as a stored result is. Past the inline cap
90
+ * they're cut, with a note that says so and where the whole result is, since
91
+ * a later field can then be cut away entirely. Undefined when it keeps
92
+ * nothing, as for a failed result (agent-framework #277). A write's file
93
+ * list is evidence of what it did, rarely what drew a refusal, and keeping it
94
+ * is what shows a clobbered file when the receipt is withheld.
95
+ */
96
+ export function keptWhenWithheld(result: CompletedToolCall['result'], maxChars: number | undefined): string | undefined {
97
+ const { data, keepWhenWithheld: keys } = result;
98
+ if (!result.success || result.isError || !keys?.length || !data || typeof data !== 'object' || Array.isArray(data)) {
99
+ return undefined;
100
+ }
101
+ const fields = data as Record<string, unknown>;
102
+ const kept = Object.fromEntries(keys.filter((key) => Object.hasOwn(fields, key)).map((key) => [key, fields[key]]));
103
+ if (Object.keys(kept).length === 0) return undefined;
104
+ const whole = toolResultDataToHistoryString(kept);
105
+ if (!maxChars || whole.length <= maxChars) return whole;
106
+ // Never half a surrogate pair: the stub goes to the provider as text, so
107
+ // the cut can be one character short of the cap.
108
+ const head = safeSlice(whole, 0, maxChars);
109
+ return `${head} [cut at the inline cap: ${head.length} of these fields' ${whole.length} ` +
110
+ 'characters; the whole result is where this note says]';
111
+ }
112
+
11
113
  interface PendingBatch {
12
114
  id: string;
13
115
  messageId: MessageId;
@@ -18,6 +120,9 @@ interface PendingBatch {
18
120
  submitted: boolean;
19
121
  /** False when the audit sync failed: originals then never go on the wire. */
20
122
  durable: boolean;
123
+ /** What each result's stub keeps if the batch is withheld, by its index in
124
+ * content: the fields its tool marked (keptWhenWithheld). */
125
+ kept: Map<number, string>;
21
126
  }
22
127
 
23
128
  interface AuditOperation {
@@ -43,6 +148,13 @@ export class ToolResultGuard {
43
148
  private override: boolean | undefined;
44
149
  /** True until a recovery produces a clean response/new tool round. */
45
150
  recovering = false;
151
+ /** Writes withheld originals where the agent can read them; without it,
152
+ * a stub names only the audit record. */
153
+ private host: WithheldHost | undefined;
154
+ /** Annotations of withheld stubs, chained in settlement order. */
155
+ private annotating: Promise<void> | undefined;
156
+ /** Annotations started so far, so a read can tell one began during it. */
157
+ private annotationsStarted = 0;
46
158
 
47
159
  constructor(
48
160
  private readonly agentName: string,
@@ -51,6 +163,51 @@ export class ToolResultGuard {
51
163
  ) {}
52
164
 
53
165
  get enabled(): boolean { return this.override ?? this.configured; }
166
+ setHost(host: WithheldHost | undefined): void { this.host = host; }
167
+ /** Settles once every annotation started so far has finished: each of
168
+ * those stubs has been edited to say where its original is, or its edit
169
+ * failed and waits in the storage queue, retried in order until it lands
170
+ * or an operator's change supersedes it (until then the stub is the bare
171
+ * notice, kept out of summaries by its compression hold, and the audit
172
+ * record names the place). It never rejects. Each batch has one deadline
173
+ * for all its workspace writes; queued batches are annotated in order.
174
+ * A request may carry an unfinished-write notice once that deadline passes. */
175
+ whenAnnotated(): Promise<void> { return this.annotating ?? Promise.resolve(); }
176
+
177
+ /**
178
+ * Runs `read` (a compile of the context) once no withheld stub is still
179
+ * being annotated, and again if an annotation began while it ran, since a
180
+ * settlement can land during the compile's own awaits, such as a previous
181
+ * frame's teardown abandoning its batch. The framework's sent-request
182
+ * compiles go through it: the agent's activations, its direct
183
+ * API calls and their refusal retry, and a conversation fork's seed from its
184
+ * template. A request preview doesn't, so it never waits on a
185
+ * workspace write.
186
+ *
187
+ * Before `read`, a stub edit that failed gets another try with the rest of
188
+ * the storage queue; a store that is still failing then compiles the bare
189
+ * notice, which its hold keeps out of summaries.
190
+ */
191
+ async afterAnnotations<T>(read: () => Promise<T>): Promise<T> {
192
+ for (;;) {
193
+ // Counted before the wait, so an annotation that starts while this one
194
+ // is awaited is waited for too.
195
+ const started = this.annotationsStarted;
196
+ await this.whenAnnotated();
197
+ if (this.annotationsStarted !== started) continue;
198
+ try {
199
+ this.flushUnrecorded();
200
+ } catch (error) {
201
+ console.error(`[tool-result-guard] agent=${this.agentName} storage retry before a compile failed:`, error);
202
+ }
203
+ const result = await read();
204
+ if (this.annotationsStarted === started) return result;
205
+ }
206
+ }
207
+
208
+ /** No annotation is running and no storage work is queued: nothing of this
209
+ * guard is left for a host to wait on or retry. */
210
+ get idle(): boolean { return this.annotating === undefined && this.unrecorded.length === 0; }
54
211
  setOverride(value: boolean | undefined): void { this.override = value; }
55
212
  get settingOverride(): boolean | undefined { return this.override; }
56
213
  get hasPending(): boolean { return this.pending !== undefined; }
@@ -81,39 +238,52 @@ export class ToolResultGuard {
81
238
 
82
239
  /** Ordered storage work, separate from batches awaiting a provider verdict.
83
240
  * A failed acceptance edit retains both the payload and its compression
84
- * hold here; it can never re-arm refusal handling or tool submission. */
241
+ * hold here; it can never re-arm refusal handling or tool submission.
242
+ * Any failed head, including an audit-only record, blocks later queued
243
+ * edits and their holds until that operation succeeds. */
85
244
  private unrecorded: AuditOperation[] = [];
86
245
 
87
- /** Retry edits and audit writes before a new activation, audit write,
88
- * maintenance pass, or shutdown. A failed operation stays at the head. */
246
+ /** Retry edits and audit writes before a new activation, a compile of a
247
+ * request that is sent (afterAnnotations), an audit write, a maintenance
248
+ * pass, or shutdown. A failed operation stays at the head. */
89
249
  flushUnrecorded(): void {
90
- while (this.unrecorded.length > 0) {
91
- const operation = this.unrecorded[0];
92
- try {
93
- if (operation.admission) {
94
- const { messageId, content, expected, branch } = operation.admission;
95
- const sameBranch = this.cm.currentBranch().name === branch;
96
- const current = sameBranch ? this.cm.getMessage(messageId) : null;
97
- // A retry must not undo an operator's later edit/removal/rollback.
98
- // Reapplying our own payload is safe if an earlier edit wrote the
99
- // message and then failed while propagating to context entries.
100
- // Chronicle normalizes object-key order and drops undefined fields.
101
- if (!sameBranch || !current ||
102
- (!isDeepStrictEqual(current.content, JSON.parse(JSON.stringify(expected)))
103
- && !isDeepStrictEqual(current.content, JSON.parse(JSON.stringify(content))))) {
104
- operation.record.historyUpdate = 'superseded';
105
- } else {
106
- this.cm.editMessage(messageId, content);
107
- }
108
- // An audit-only retry must not repeat an edit that already succeeded.
109
- operation.admission = undefined;
250
+ try {
251
+ while (this.unrecorded.length > 0) {
252
+ const operation = this.unrecorded[0];
253
+ try {
254
+ this.applyEdit(operation);
255
+ this.write(operation.record);
256
+ this.unrecorded.shift();
257
+ } finally {
258
+ if (!operation.admission) this.releaseOperationHold(operation);
110
259
  }
111
- this.write(operation.record);
112
- this.unrecorded.shift();
113
- } finally {
114
- if (!operation.admission) this.releaseOperationHold(operation);
115
260
  }
261
+ } finally {
262
+ this.host?.idle(this.idle);
263
+ }
264
+ }
265
+
266
+ /** Make an operation's history edit, if it still has one: once it is made,
267
+ * or superseded by an operator's change, only its record is left to
268
+ * write, and its hold can go. */
269
+ private applyEdit(operation: AuditOperation): void {
270
+ if (!operation.admission) return;
271
+ const { messageId, content, expected, branch } = operation.admission;
272
+ const sameBranch = this.cm.currentBranch().name === branch;
273
+ const current = sameBranch ? this.cm.getMessage(messageId) : null;
274
+ // A retry must not undo an operator's later edit/removal/rollback.
275
+ // Reapplying our own payload is safe if an earlier edit wrote the
276
+ // message and then failed while propagating to context entries.
277
+ // Chronicle normalizes object-key order and drops undefined fields.
278
+ if (!sameBranch || !current ||
279
+ (!isDeepStrictEqual(current.content, JSON.parse(JSON.stringify(expected)))
280
+ && !isDeepStrictEqual(current.content, JSON.parse(JSON.stringify(content))))) {
281
+ operation.record.historyUpdate = 'superseded';
282
+ } else {
283
+ this.cm.editMessage(messageId, content);
116
284
  }
285
+ // An audit-only retry must not repeat an edit that already succeeded.
286
+ operation.admission = undefined;
117
287
  }
118
288
 
119
289
  private append(record: Record<string, unknown>): void {
@@ -144,8 +314,17 @@ export class ToolResultGuard {
144
314
  : JSON.parse(json);
145
315
  }
146
316
 
147
- storeResults(content: ContentBlock[], wireResults: ToolResult[], originals: CompletedToolCall[]): MessageId {
148
- if (!this.enabled) return this.cm.addMessage('user', content);
317
+ /** `inlineCap` is the cap the caller stored `content` under; what a stub
318
+ * keeps of a withheld result is cut at it too. */
319
+ storeResults(
320
+ content: ContentBlock[],
321
+ wireResults: ToolResult[],
322
+ originals: CompletedToolCall[],
323
+ metadata: MessageMetadata | undefined,
324
+ /** The cap the results were stored under: what a stub keeps is cut there too. */
325
+ inlineCap: number | undefined,
326
+ ): MessageId {
327
+ if (!this.enabled) return this.cm.addMessage('user', content, metadata);
149
328
  if (this.pending) {
150
329
  // Idempotent for the same not-yet-submitted batch: a caller that
151
330
  // retries after a failure between staging and submission (e.g. a
@@ -171,13 +350,22 @@ export class ToolResultGuard {
171
350
  const withheld: ContentBlock[] = content.map((block) => block.type === 'tool_result'
172
351
  ? { type: 'tool_result', toolUseId: block.toolUseId, content: TOOL_RESULT_GUARD_NOTICE, isError: block.isError }
173
352
  : block);
353
+ // What each stub keeps if the batch is withheld, matched to its tool's
354
+ // result by tool use id, keyed by index as the stubs and files are.
355
+ const results = new Map(originals.map((call) => [call.id, call.result]));
356
+ const kept = new Map<number, string>();
357
+ for (const [index, block] of content.entries()) {
358
+ const result = block.type === 'tool_result' ? results.get(block.toolUseId) : undefined;
359
+ const text = result ? keptWhenWithheld(result, inlineCap) : undefined;
360
+ if (text !== undefined) kept.set(index, text);
361
+ }
174
362
  // Compression hold (CM >= 0.12): placed BEFORE onNewMessage fires, so no
175
363
  // chunk is ever summarized from the placeholder; released after the
176
364
  // settlement edit. Holds are in-memory only — a reopened manager has none,
177
365
  // which is correct since a reopened guard has no pending batch.
178
- const messageId = this.cm.addMessage('user', withheld, undefined, undefined, { holdCompression: true });
366
+ const messageId = this.cm.addMessage('user', withheld, metadata, undefined, { holdCompression: true });
179
367
  this.pending = { id, messageId, content, withheld, wireResults,
180
- branch: this.cm.currentBranch().name, submitted: false, durable: false };
368
+ branch: this.cm.currentBranch().name, submitted: false, durable: false, kept };
181
369
  // Durability barrier: the audit must reach Chronicle's chain heads before
182
370
  // the originals can go to a provider. On a failed sync the batch fails
183
371
  // CLOSED: the placeholders go on the wire instead (the turn continues,
@@ -285,7 +473,9 @@ export class ToolResultGuard {
285
473
 
286
474
  /** Transfer ownership from provider admission to an ordered storage job.
287
475
  * Failure retains the accepted payload and its hold until the edit succeeds.
288
- * Withheld content is final already, so audit failures cannot keep it held. */
476
+ * A withheld batch's hold passes to its annotation instead, and from there
477
+ * to its stub's edit, so no summary is made of a stub that doesn't yet say
478
+ * where its original went. */
289
479
  private settle(pending: PendingBatch, record: Record<string, unknown>, admit: boolean): void {
290
480
  this.pending = undefined;
291
481
  const operation: AuditOperation = {
@@ -293,8 +483,9 @@ export class ToolResultGuard {
293
483
  batchId: pending.id, messageId: pending.messageId, sourceBranch: pending.branch },
294
484
  ...(admit ? { admission: { messageId: pending.messageId, content: pending.content,
295
485
  expected: pending.withheld, branch: pending.branch } } : {}),
296
- heldMessageId: pending.messageId,
486
+ heldMessageId: admit ? pending.messageId : undefined,
297
487
  };
488
+ if (!admit) this.annotate(pending);
298
489
  this.unrecorded.push(operation);
299
490
  try {
300
491
  this.flushUnrecorded();
@@ -305,6 +496,165 @@ export class ToolResultGuard {
305
496
  }
306
497
  }
307
498
 
499
+ /**
500
+ * Turn a withheld batch's notices into stubs that say where each original
501
+ * is (agent-framework #277): written to the workspace when a writable mount
502
+ * takes it, else kept in the audit record. A stub also keeps the file
503
+ * lists its tool marked. Its own unedited placeholder is
504
+ * the only thing it edits: an operator's later edit, removal or rollback
505
+ * stands.
506
+ *
507
+ * Where each original went is queued first, on its own ('annotated'), so
508
+ * a stub edit that fails can't take the record of a written file with it.
509
+ * The edit is then made at once, so no storage work queued ahead of it
510
+ * holds it, not even that record if its write is failing, and its record
511
+ * ('stub_updated') is written in order after the others. An
512
+ * edit that fails joins the ordered storage queue like an admission's, with
513
+ * the batch's compression hold: retried before the next activation, compile,
514
+ * maintenance pass or stop until it lands or an operator's change supersedes
515
+ * it. Until then the stub is the bare notice, which the hold keeps out of
516
+ * summaries, and the audit record names the file.
517
+ */
518
+ private annotate(pending: PendingBatch): void {
519
+ const previous = this.annotating ?? Promise.resolve();
520
+ this.annotationsStarted += 1;
521
+ // The batch's deadline runs from when it's queued, not from when the
522
+ // batches ahead of it finish, so a request waiting on a chain of batches
523
+ // waits at most one deadline past the latest one's arrival.
524
+ let deadline = Infinity;
525
+ try { deadline = this.host?.deadline() ?? Infinity; } catch { /* the spill then takes its own deadline, rather than losing the record */ }
526
+ const run = previous.then(async () => {
527
+ // Set once the stub's edit is queued with the hold: the queue releases
528
+ // it when the edit lands or is superseded.
529
+ let holdQueued = false;
530
+ try {
531
+ const date = new Date().toISOString().slice(0, 10);
532
+ // A file is named by its batch and its place in the batch, ahead of
533
+ // anything the writer may cut or substitute: results of one batch
534
+ // never share a file, and results of two batches share one only if 48
535
+ // random bits of their batch ids agree. The tool use id after them is
536
+ // for whoever lists the directory.
537
+ const batch = pending.id.replace(/-/g, '').slice(0, 12);
538
+ // Keyed by the result's index in the batch, which the stub and the
539
+ // original share.
540
+ const spills = new Map<number, WithheldSpill>();
541
+ // A spill that threw, by index: its error, for the record.
542
+ const thrown = new Map<number, string>();
543
+ // Set once the branch has changed: no more is written.
544
+ let branchLeft = false;
545
+ let deadlineSpent = false;
546
+ for (const [index, block] of pending.content.entries()) {
547
+ if (block.type !== 'tool_result') continue;
548
+ // Once the store has left the batch's branch (a rollback, an undo, a
549
+ // host's switch), write no more. This result and every later one is
550
+ // skipped, and its original stays in the audit record. Nothing
551
+ // yields from here to the edit's own branch check, so while the
552
+ // store is away the stub stays as it is. A write already under way
553
+ // when the branch changes is refused before its tree entry is set,
554
+ // since it names the batch's branch. Once set, that entry stays on
555
+ // its branch; its disk copy uses the mount's shared directory.
556
+ if (branchLeft || this.cm.currentBranch().name !== pending.branch) {
557
+ branchLeft = true;
558
+ spills.set(index, { skipped: 'branch_changed' });
559
+ continue;
560
+ }
561
+ // The result as stored. Both paths that store a tool result store
562
+ // its text; content stored as blocks would be written as their JSON.
563
+ const original = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
564
+ let spill: WithheldSpill = null;
565
+ if (this.host) {
566
+ try {
567
+ spill = await this.host.spill(`${date}-withheld-${batch}-${index}-${block.toolUseId}`, original, pending.branch, deadline);
568
+ } catch (error) {
569
+ // The host reports a failed write rather than throwing; a throw
570
+ // is recorded all the same, without a path.
571
+ thrown.set(index, (error instanceof Error ? error.message : String(error)).slice(0, 200));
572
+ console.error(`[tool-result-guard] agent=${this.agentName} could not spill withheld result ` +
573
+ `${block.toolUseId}:`, error);
574
+ }
575
+ }
576
+ // Declined where it would have committed, since the workspace had
577
+ // left the batch's branch. Nothing was written, and no more are, as
578
+ // for the results the branch check above stops. If the branch is
579
+ // back by the edit, as after an undo and a redo, the stubs of this
580
+ // result and the later ones name the audit record.
581
+ if (spill && 'skipped' in spill && spill.skipped === 'branch_changed') branchLeft = true;
582
+ // A batch that used its deadline is said once, here, where the
583
+ // batch is known; the host returns the skip without a line.
584
+ if (spill && 'skipped' in spill && spill.skipped === 'batch_deadline' && !deadlineSpent) {
585
+ deadlineSpent = true;
586
+ console.error(`[tool-result-guard] agent=${this.agentName} batch=${pending.id} used its deadline before all its originals were written; the rest name the audit record (batch_deadline)`);
587
+ }
588
+ spills.set(index, spill);
589
+ }
590
+ const annotated: ContentBlock[] = pending.withheld.map((block, index) => block.type === 'tool_result'
591
+ ? { ...block, content: withheldResultNotice(spills.get(index) ?? null, pending.durable, pending.kept.get(index)) }
592
+ : block);
593
+ try {
594
+ this.append({
595
+ type: 'annotated', batchId: pending.id, messageId: pending.messageId, sourceBranch: pending.branch,
596
+ results: pending.content.flatMap((block, index): Array<Record<string, string | true>> => {
597
+ if (block.type !== 'tool_result') return [];
598
+ if (thrown.has(index)) return [{ toolUseId: block.toolUseId, error: thrown.get(index)! }];
599
+ const spill = spills.get(index) ?? null;
600
+ // No file was written: no writable workspace, or the framework
601
+ // was stopping.
602
+ if (spill === null) return [{ toolUseId: block.toolUseId, skipped: 'no_workspace_file' }];
603
+ if ('skipped' in spill) return [{ toolUseId: block.toolUseId, skipped: spill.skipped }];
604
+ return [{ toolUseId: block.toolUseId, path: spill.path,
605
+ ...(spill.error !== undefined ? { error: spill.error } : {}),
606
+ ...(spill.unfinished ? { unfinished: true as const } : {}) }];
607
+ }),
608
+ });
609
+ } catch (error) {
610
+ // It stays queued and is retried in order with the storage work
611
+ // ahead of it; the stub's record is written after it.
612
+ console.error(`[tool-result-guard] agent=${this.agentName} batch=${pending.id} annotation record queued for retry:`, error);
613
+ }
614
+ const stub: AuditOperation = {
615
+ record: { agentName: this.agentName, timestamp: Date.now(), type: 'stub_updated',
616
+ batchId: pending.id, messageId: pending.messageId, sourceBranch: pending.branch },
617
+ admission: { messageId: pending.messageId, content: annotated, expected: pending.withheld, branch: pending.branch },
618
+ heldMessageId: pending.messageId,
619
+ };
620
+ try {
621
+ this.applyEdit(stub);
622
+ } catch (error) {
623
+ console.error(`[tool-result-guard] agent=${this.agentName} batch=${pending.id} stub edit queued for retry:`, error);
624
+ }
625
+ this.unrecorded.push(stub);
626
+ holdQueued = true;
627
+ try {
628
+ // A made edit's hold goes now, whatever storage work is still
629
+ // queued ahead of its record.
630
+ if (!stub.admission) this.releaseOperationHold(stub);
631
+ this.flushUnrecorded();
632
+ } catch (error) {
633
+ console.error(`[tool-result-guard] agent=${this.agentName} batch=${pending.id} stub's storage work queued for retry:`, error);
634
+ }
635
+ } catch (error) {
636
+ console.error(`[tool-result-guard] agent=${this.agentName} batch=${pending.id} could not annotate withheld results:`, error);
637
+ } finally {
638
+ // Never rejects: a compile and stop() wait on this chain. With no
639
+ // stub edit queued, nothing else holds the batch.
640
+ if (!holdQueued) {
641
+ try {
642
+ this.releaseHold(pending.messageId);
643
+ } catch (error) {
644
+ console.error(`[tool-result-guard] agent=${this.agentName} could not release a withheld batch's hold:`, error);
645
+ }
646
+ }
647
+ }
648
+ });
649
+ this.annotating = run;
650
+ void run.finally(() => {
651
+ if (this.annotating === run) this.annotating = undefined;
652
+ this.host?.idle(this.idle);
653
+ });
654
+ this.host?.track(run);
655
+ this.host?.idle(false);
656
+ }
657
+
308
658
  private releaseOperationHold(operation: AuditOperation): void {
309
659
  if (operation.heldMessageId !== undefined) {
310
660
  this.releaseHold(operation.heldMessageId);
@@ -52,10 +52,34 @@ export function toolResultDataToHistoryString(
52
52
  options?: HistorySerializeOptions,
53
53
  ): string {
54
54
  const fromArray = tryHistoryStringFromContentArray(data, options);
55
- const str = fromArray ?? JSON.stringify(data);
55
+ // JSON leaves out undefined, a function and a symbol: a result with no data
56
+ // is stored as no text, as an empty content array is.
57
+ const str = fromArray ?? (JSON.stringify(data) as string | undefined) ?? '';
56
58
  return maxChars ? truncateForHistory(str, maxChars) : str;
57
59
  }
58
60
 
61
+ /**
62
+ * The text of a tool result that isn't an error result: its data, as history
63
+ * stores it, or, when it has no data, its error text. A tool can answer
64
+ * `{ success: false, error, isError: false }`, a failure it doesn't flag as
65
+ * an error for the model, and the model still reads what failed.
66
+ *
67
+ * Each field is read once: the direct Agent API passes its caller's own
68
+ * result here, unchecked, and a getter needn't answer the same way twice.
69
+ */
70
+ export function nonErrorResultText(
71
+ result: { data?: unknown; error?: string },
72
+ maxChars?: number,
73
+ options?: HistorySerializeOptions,
74
+ ): string {
75
+ const data = result.data;
76
+ if (data === undefined || data === null) {
77
+ const error = result.error;
78
+ if (typeof error === 'string' && error) return maxChars ? truncateForHistory(error, maxChars) : error;
79
+ }
80
+ return toolResultDataToHistoryString(data, maxChars, options);
81
+ }
82
+
59
83
  /** Bounded copy of an arbitrary string with the standard truncation notice.
60
84
  * Image slots past the cut are re-appended, never dropped (issue #104). */
61
85
  export function truncateForHistory(str: string, maxChars: number): string {
@@ -1,6 +1,7 @@
1
1
  import type { ContentBlock, YieldingStream } from '@animalabs/membrane';
2
2
  import type { ContextStrategy } from '@animalabs/context-manager';
3
3
  import type { ToolCallId, ToolResult, ToolCall } from './events.js';
4
+ import type { SilentHeartbeatTick } from '../silent-heartbeat.js';
4
5
 
5
6
  export type SameRoundThinkTextPolicy = 'public' | 'private';
6
7
  export type SameRoundThinkTextPolicySource =
@@ -12,6 +13,7 @@ export type SameRoundThinkTextPolicySource =
12
13
  * Configuration for an agent.
13
14
  */
14
15
  export interface AgentConfig {
16
+ toolPresentation?: import("../tool-presentation.js").ToolPresentationConfig;
15
17
  /** Unique name for this agent */
16
18
  name: string;
17
19
 
@@ -305,6 +307,14 @@ export interface InferenceRequest {
305
307
  /** Host-owned identity for withdrawal of an unconsumed coalesced wake. */
306
308
  coalescingSubject?: string;
307
309
  coalescingEventId?: string;
310
+ /** Host-owned: this wake's cause is an RFC-006 deferred batch, which has
311
+ * no model-visible content until it renders at the turn's assembly. */
312
+ coalescingBatch?: boolean;
313
+ /** Host-owned, set on a turn's trigger when EVERY request batched into it
314
+ * was a deferred-batch wake: these are their subjects. If assembly settles
315
+ * all of them without materializing content, the turn has no cause left
316
+ * and does not run. */
317
+ coalescingBatchSubjects?: string[];
308
318
  agentName: string;
309
319
  reason: string;
310
320
  source: string;
@@ -339,8 +349,10 @@ export interface InferenceRequest {
339
349
  * Where the wake came from, for telemetry ONLY — never a speech locus.
340
350
  * Set by the EventGate for batched wakes (composite channel id of the
341
351
  * chosen event when the event carried one; push-event raw ids are not
342
- * used). Routing keeps reading `channelId`, which only the direct channel
343
- * paths set from normalized ids — a gate wake leaves it unset, as before.
352
+ * used). Routing keeps reading `channelId`, set from registered ids: by
353
+ * the direct channel paths, and by a gate wake only when its chosen event
354
+ * addressed the agent and the host resolved that event's registered
355
+ * channel (ambient-only batches leave it unset).
344
356
  */
345
357
  wakeChannelId?: string;
346
358
  /** Timestamp of the event the wake provenance was taken from (ms). */
@@ -351,4 +363,8 @@ export interface InferenceRequest {
351
363
  /** Ephemeral system-position prompt for this turn only. Never written to
352
364
  * Chronicle; callers must supply bounded non-secret control text. */
353
365
  ephemeralSystemPrompt?: string;
366
+ /** The authenticated silent heartbeat tick this turn answers. Every row the
367
+ * turn stores is stamped `metadata.silentHeartbeat` with it, and request
368
+ * builds render a request-only separator before the tick's first row. */
369
+ silentHeartbeat?: SilentHeartbeatTick;
354
370
  }
@@ -92,6 +92,10 @@ export interface ToolResultEvent {
92
92
  agentName: string;
93
93
  /** Module that handled the call */
94
94
  moduleName: string;
95
+ /** The tool's name, as its call named it. The framework's own
96
+ * producers set it, so a result nothing can take any more (one arriving
97
+ * after stop()) is still logged by tool. */
98
+ toolName?: string;
95
99
  /** Result of the tool call */
96
100
  result: ToolResult;
97
101
  }
@@ -195,6 +199,18 @@ export interface ToolResult {
195
199
  * Adapted from Anarchid/agent-framework@mcpl-module-proto.
196
200
  */
197
201
  endTurn?: boolean;
202
+ /**
203
+ * Fields of `data` that say what the tool acted on, such as the paths a
204
+ * write wrote, removed or took from disk, the path it deleted, or a mount
205
+ * whose root it accepted, in the order they should be kept. When the
206
+ * tool-result guard withholds this result, these fields stay in its stub,
207
+ * so the agent still sees what the tool did; the result as stored goes
208
+ * where every withheld original goes (agent-framework #277). Past the
209
+ * inline cap they're cut, in this order, so a later field can be cut away
210
+ * entirely; put the short, telling ones first. Only data's own fields can
211
+ * be kept, and a failed result (success false) keeps nothing.
212
+ */
213
+ keepWhenWithheld?: string[];
198
214
  }
199
215
 
200
216
  /**
@@ -237,6 +237,11 @@ export interface FrameworkConfig {
237
237
  * effective class (first matching pattern wins); replaces, never merges
238
238
  * with, what the providing server declared. Use it to correct or tighten a
239
239
  * misclassed tool.
240
+ *
241
+ * MCPL tools are named `<toolPrefix>--<tool>`, and toolPrefix defaults to
242
+ * `mcpl--<serverId>`: server `search` without a toolPrefix needs
243
+ * `mcpl--search--*`, not `search--*`. A pattern that matches no tool is
244
+ * reported once its servers have listed their tools.
240
245
  */
241
246
  toolClassOverrides?: Record<string, string[]>;
242
247
 
@@ -274,6 +279,13 @@ export interface FrameworkConfig {
274
279
  * messages route to per-channel fork agents spawned from the template
275
280
  * agent instead of the primary conversation. If omitted, behavior is
276
281
  * unchanged (all messages go to the primary agent).
282
+ *
283
+ * @deprecated Per-channel conversation routing is deprecated and will be
284
+ * removed (anima-research/agent-framework#235). Its 'mention' bind/trigger
285
+ * rule reads `metadata.mentioned`, which not every channel server sets
286
+ * (discord-mcpl does not), so on those channels an @-mention neither binds
287
+ * a fork nor triggers a bound one. Routing still works for now; the
288
+ * framework logs one `[deprecated]` line at creation when this is set.
277
289
  */
278
290
  conversations?: ConversationRouterConfig;
279
291
  }