@animalabs/agent-framework 0.26.2 → 0.28.0

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 (371) hide show
  1. package/README.md +99 -8
  2. package/dist/src/agent.d.ts +291 -23
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +596 -61
  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 +5 -4
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/auto-continue.d.ts +34 -0
  10. package/dist/src/auto-continue.d.ts.map +1 -0
  11. package/dist/src/auto-continue.js +36 -0
  12. package/dist/src/auto-continue.js.map +1 -0
  13. package/dist/src/code-execution/isolate/scripts.d.ts +11 -0
  14. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  15. package/dist/src/code-execution/isolate/scripts.js +5 -1
  16. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  17. package/dist/src/cut-off-round.d.ts +24 -0
  18. package/dist/src/cut-off-round.d.ts.map +1 -0
  19. package/dist/src/cut-off-round.js +87 -0
  20. package/dist/src/cut-off-round.js.map +1 -0
  21. package/dist/src/framework.d.ts +1038 -193
  22. package/dist/src/framework.d.ts.map +1 -1
  23. package/dist/src/framework.js +5967 -1514
  24. package/dist/src/framework.js.map +1 -1
  25. package/dist/src/gate/event-gate.d.ts +405 -46
  26. package/dist/src/gate/event-gate.d.ts.map +1 -1
  27. package/dist/src/gate/event-gate.js +2646 -399
  28. package/dist/src/gate/event-gate.js.map +1 -1
  29. package/dist/src/gate/index.d.ts +3 -2
  30. package/dist/src/gate/index.d.ts.map +1 -1
  31. package/dist/src/gate/index.js +2 -1
  32. package/dist/src/gate/index.js.map +1 -1
  33. package/dist/src/gate/recipe-reconcile.d.ts +103 -0
  34. package/dist/src/gate/recipe-reconcile.d.ts.map +1 -0
  35. package/dist/src/gate/recipe-reconcile.js +325 -0
  36. package/dist/src/gate/recipe-reconcile.js.map +1 -0
  37. package/dist/src/gate/reply-judge.d.ts +116 -0
  38. package/dist/src/gate/reply-judge.d.ts.map +1 -0
  39. package/dist/src/gate/reply-judge.js +467 -0
  40. package/dist/src/gate/reply-judge.js.map +1 -0
  41. package/dist/src/gate/types.d.ts +229 -8
  42. package/dist/src/gate/types.d.ts.map +1 -1
  43. package/dist/src/heartbeat/heartbeat.d.ts +184 -0
  44. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
  45. package/dist/src/heartbeat/heartbeat.js +752 -0
  46. package/dist/src/heartbeat/heartbeat.js.map +1 -0
  47. package/dist/src/heartbeat/reminder-time.d.ts +5 -0
  48. package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
  49. package/dist/src/heartbeat/reminder-time.js +107 -0
  50. package/dist/src/heartbeat/reminder-time.js.map +1 -0
  51. package/dist/src/index.d.ts +10 -4
  52. package/dist/src/index.d.ts.map +1 -1
  53. package/dist/src/index.js +7 -2
  54. package/dist/src/index.js.map +1 -1
  55. package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
  56. package/dist/src/mcpl/capability-mask.js +3 -2
  57. package/dist/src/mcpl/capability-mask.js.map +1 -1
  58. package/dist/src/mcpl/channel-registry.d.ts +433 -113
  59. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  60. package/dist/src/mcpl/channel-registry.js +1154 -435
  61. package/dist/src/mcpl/channel-registry.js.map +1 -1
  62. package/dist/src/mcpl/conversation-router.d.ts +16 -8
  63. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  64. package/dist/src/mcpl/conversation-router.js +29 -15
  65. package/dist/src/mcpl/conversation-router.js.map +1 -1
  66. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  67. package/dist/src/mcpl/convert-block.js +14 -2
  68. package/dist/src/mcpl/convert-block.js.map +1 -1
  69. package/dist/src/mcpl/hook-orchestrator.d.ts +7 -1
  70. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  71. package/dist/src/mcpl/hook-orchestrator.js +32 -7
  72. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  73. package/dist/src/mcpl/inbound-source.d.ts +26 -10
  74. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  75. package/dist/src/mcpl/inbound-source.js +40 -69
  76. package/dist/src/mcpl/inbound-source.js.map +1 -1
  77. package/dist/src/mcpl/index.d.ts +5 -1
  78. package/dist/src/mcpl/index.d.ts.map +1 -1
  79. package/dist/src/mcpl/index.js +4 -0
  80. package/dist/src/mcpl/index.js.map +1 -1
  81. package/dist/src/mcpl/inference-router.d.ts +14 -2
  82. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  83. package/dist/src/mcpl/inference-router.js +73 -28
  84. package/dist/src/mcpl/inference-router.js.map +1 -1
  85. package/dist/src/mcpl/prose-grammar.d.ts +4 -0
  86. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  87. package/dist/src/mcpl/prose-grammar.js +6 -0
  88. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  89. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  90. package/dist/src/mcpl/protocol-family.js +3 -0
  91. package/dist/src/mcpl/protocol-family.js.map +1 -1
  92. package/dist/src/mcpl/push-coalescer.d.ts +20 -2
  93. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  94. package/dist/src/mcpl/push-coalescer.js +33 -11
  95. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  96. package/dist/src/mcpl/reference-fetcher.d.ts +63 -42
  97. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  98. package/dist/src/mcpl/reference-fetcher.js +177 -145
  99. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  100. package/dist/src/mcpl/references.d.ts +380 -46
  101. package/dist/src/mcpl/references.d.ts.map +1 -1
  102. package/dist/src/mcpl/references.js +736 -77
  103. package/dist/src/mcpl/references.js.map +1 -1
  104. package/dist/src/mcpl/server-connection.d.ts +93 -4
  105. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  106. package/dist/src/mcpl/server-connection.js +176 -11
  107. package/dist/src/mcpl/server-connection.js.map +1 -1
  108. package/dist/src/mcpl/tags.d.ts +9 -2
  109. package/dist/src/mcpl/tags.d.ts.map +1 -1
  110. package/dist/src/mcpl/tags.js +12 -2
  111. package/dist/src/mcpl/tags.js.map +1 -1
  112. package/dist/src/mcpl/tool-lifecycle.d.ts +11 -0
  113. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  114. package/dist/src/mcpl/tool-lifecycle.js +21 -0
  115. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  116. package/dist/src/mcpl/tool-presence.d.ts +311 -0
  117. package/dist/src/mcpl/tool-presence.d.ts.map +1 -0
  118. package/dist/src/mcpl/tool-presence.js +697 -0
  119. package/dist/src/mcpl/tool-presence.js.map +1 -0
  120. package/dist/src/mcpl/tool-result-normalize.d.ts +9 -3
  121. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  122. package/dist/src/mcpl/tool-result-normalize.js +14 -6
  123. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  124. package/dist/src/mcpl/tool-signature.d.ts +46 -0
  125. package/dist/src/mcpl/tool-signature.d.ts.map +1 -0
  126. package/dist/src/mcpl/tool-signature.js +321 -0
  127. package/dist/src/mcpl/tool-signature.js.map +1 -0
  128. package/dist/src/mcpl/toolset-host.d.ts +251 -0
  129. package/dist/src/mcpl/toolset-host.d.ts.map +1 -0
  130. package/dist/src/mcpl/toolset-host.js +720 -0
  131. package/dist/src/mcpl/toolset-host.js.map +1 -0
  132. package/dist/src/mcpl/toolsets-module.d.ts +83 -0
  133. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -0
  134. package/dist/src/mcpl/toolsets-module.js +523 -0
  135. package/dist/src/mcpl/toolsets-module.js.map +1 -0
  136. package/dist/src/mcpl/transport.d.ts +2 -2
  137. package/dist/src/mcpl/transport.d.ts.map +1 -1
  138. package/dist/src/mcpl/transport.js +3 -2
  139. package/dist/src/mcpl/transport.js.map +1 -1
  140. package/dist/src/mcpl/types.d.ts +24 -2
  141. package/dist/src/mcpl/types.d.ts.map +1 -1
  142. package/dist/src/mcpl/types.js.map +1 -1
  143. package/dist/src/mcpl/visible-content.d.ts +7 -9
  144. package/dist/src/mcpl/visible-content.d.ts.map +1 -1
  145. package/dist/src/mcpl/visible-content.js +21 -9
  146. package/dist/src/mcpl/visible-content.js.map +1 -1
  147. package/dist/src/module-registry.d.ts +27 -9
  148. package/dist/src/module-registry.d.ts.map +1 -1
  149. package/dist/src/module-registry.js +77 -17
  150. package/dist/src/module-registry.js.map +1 -1
  151. package/dist/src/modules/discord/index.js +1 -1
  152. package/dist/src/modules/discord/index.js.map +1 -1
  153. package/dist/src/modules/history/index.d.ts +2 -1
  154. package/dist/src/modules/history/index.d.ts.map +1 -1
  155. package/dist/src/modules/history/index.js +73 -28
  156. package/dist/src/modules/history/index.js.map +1 -1
  157. package/dist/src/modules/history/semantic.d.ts +108 -6
  158. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  159. package/dist/src/modules/history/semantic.js +273 -57
  160. package/dist/src/modules/history/semantic.js.map +1 -1
  161. package/dist/src/modules/workspace/answers.d.ts +22 -0
  162. package/dist/src/modules/workspace/answers.d.ts.map +1 -0
  163. package/dist/src/modules/workspace/answers.js +123 -0
  164. package/dist/src/modules/workspace/answers.js.map +1 -0
  165. package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
  166. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  167. package/dist/src/modules/workspace/branch-intent.js +6 -2
  168. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  169. package/dist/src/modules/workspace/direct-write.d.ts +184 -0
  170. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -0
  171. package/dist/src/modules/workspace/direct-write.js +417 -0
  172. package/dist/src/modules/workspace/direct-write.js.map +1 -0
  173. package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
  174. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  175. package/dist/src/modules/workspace/disk-agreement.js +48 -17
  176. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  177. package/dist/src/modules/workspace/edit-text.d.ts +27 -12
  178. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
  179. package/dist/src/modules/workspace/edit-text.js +61 -29
  180. package/dist/src/modules/workspace/edit-text.js.map +1 -1
  181. package/dist/src/modules/workspace/effects.d.ts +10 -0
  182. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  183. package/dist/src/modules/workspace/effects.js +21 -14
  184. package/dist/src/modules/workspace/effects.js.map +1 -1
  185. package/dist/src/modules/workspace/index.d.ts +308 -78
  186. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  187. package/dist/src/modules/workspace/index.js +2103 -720
  188. package/dist/src/modules/workspace/index.js.map +1 -1
  189. package/dist/src/modules/workspace/observe.d.ts +75 -63
  190. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  191. package/dist/src/modules/workspace/observe.js +366 -180
  192. package/dist/src/modules/workspace/observe.js.map +1 -1
  193. package/dist/src/modules/workspace/read-page.d.ts +41 -0
  194. package/dist/src/modules/workspace/read-page.d.ts.map +1 -0
  195. package/dist/src/modules/workspace/read-page.js +219 -0
  196. package/dist/src/modules/workspace/read-page.js.map +1 -0
  197. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  198. package/dist/src/modules/workspace/reconcile.js +26 -16
  199. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  200. package/dist/src/modules/workspace/result-text.d.ts +68 -0
  201. package/dist/src/modules/workspace/result-text.d.ts.map +1 -0
  202. package/dist/src/modules/workspace/result-text.js +94 -0
  203. package/dist/src/modules/workspace/result-text.js.map +1 -0
  204. package/dist/src/modules/workspace/sync.d.ts +9 -0
  205. package/dist/src/modules/workspace/sync.d.ts.map +1 -1
  206. package/dist/src/modules/workspace/sync.js +14 -0
  207. package/dist/src/modules/workspace/sync.js.map +1 -1
  208. package/dist/src/modules/workspace/text.d.ts +29 -12
  209. package/dist/src/modules/workspace/text.d.ts.map +1 -1
  210. package/dist/src/modules/workspace/text.js +35 -33
  211. package/dist/src/modules/workspace/text.js.map +1 -1
  212. package/dist/src/modules/workspace/types.d.ts +137 -60
  213. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  214. package/dist/src/modules/workspace/types.js.map +1 -1
  215. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  216. package/dist/src/modules/workspace/watcher.js +8 -3
  217. package/dist/src/modules/workspace/watcher.js.map +1 -1
  218. package/dist/src/prose-drafts.d.ts +4 -3
  219. package/dist/src/prose-drafts.d.ts.map +1 -1
  220. package/dist/src/prose-drafts.js +3 -2
  221. package/dist/src/prose-drafts.js.map +1 -1
  222. package/dist/src/reasoning-loss-notice.d.ts +23 -0
  223. package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
  224. package/dist/src/reasoning-loss-notice.js +50 -0
  225. package/dist/src/reasoning-loss-notice.js.map +1 -0
  226. package/dist/src/reasoning-loss.d.ts +24 -0
  227. package/dist/src/reasoning-loss.d.ts.map +1 -0
  228. package/dist/src/reasoning-loss.js +102 -0
  229. package/dist/src/reasoning-loss.js.map +1 -0
  230. package/dist/src/send-tools.d.ts +3 -2
  231. package/dist/src/send-tools.d.ts.map +1 -1
  232. package/dist/src/send-tools.js +3 -2
  233. package/dist/src/send-tools.js.map +1 -1
  234. package/dist/src/sent-rows.d.ts +59 -0
  235. package/dist/src/sent-rows.d.ts.map +1 -0
  236. package/dist/src/sent-rows.js +145 -0
  237. package/dist/src/sent-rows.js.map +1 -0
  238. package/dist/src/silent-heartbeat.d.ts +17 -10
  239. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  240. package/dist/src/silent-heartbeat.js.map +1 -1
  241. package/dist/src/stable-hash.d.ts +3 -0
  242. package/dist/src/stable-hash.d.ts.map +1 -0
  243. package/dist/src/stable-hash.js +15 -0
  244. package/dist/src/stable-hash.js.map +1 -0
  245. package/dist/src/stored-injections.d.ts +99 -0
  246. package/dist/src/stored-injections.d.ts.map +1 -0
  247. package/dist/src/stored-injections.js +106 -0
  248. package/dist/src/stored-injections.js.map +1 -0
  249. package/dist/src/text-as-stored.d.ts +4 -3
  250. package/dist/src/text-as-stored.d.ts.map +1 -1
  251. package/dist/src/text-as-stored.js +4 -3
  252. package/dist/src/text-as-stored.js.map +1 -1
  253. package/dist/src/thinking-drops.d.ts +74 -0
  254. package/dist/src/thinking-drops.d.ts.map +1 -0
  255. package/dist/src/thinking-drops.js +96 -0
  256. package/dist/src/thinking-drops.js.map +1 -0
  257. package/dist/src/tool-args.d.ts +12 -0
  258. package/dist/src/tool-args.d.ts.map +1 -1
  259. package/dist/src/tool-args.js +35 -1
  260. package/dist/src/tool-args.js.map +1 -1
  261. package/dist/src/tool-image-ledger.d.ts +9 -8
  262. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  263. package/dist/src/tool-image-ledger.js +9 -8
  264. package/dist/src/tool-image-ledger.js.map +1 -1
  265. package/dist/src/tool-presentation.d.ts.map +1 -1
  266. package/dist/src/tool-presentation.js +3 -1
  267. package/dist/src/tool-presentation.js.map +1 -1
  268. package/dist/src/tool-result-guard.d.ts +12 -6
  269. package/dist/src/tool-result-guard.d.ts.map +1 -1
  270. package/dist/src/tool-result-guard.js +44 -17
  271. package/dist/src/tool-result-guard.js.map +1 -1
  272. package/dist/src/tool-result-history.d.ts +176 -24
  273. package/dist/src/tool-result-history.d.ts.map +1 -1
  274. package/dist/src/tool-result-history.js +411 -91
  275. package/dist/src/tool-result-history.js.map +1 -1
  276. package/dist/src/tune-out/coordinator.d.ts +17 -9
  277. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  278. package/dist/src/tune-out/coordinator.js +84 -37
  279. package/dist/src/tune-out/coordinator.js.map +1 -1
  280. package/dist/src/tune-out/tools.d.ts +4 -1
  281. package/dist/src/tune-out/tools.d.ts.map +1 -1
  282. package/dist/src/types/agent.d.ts +217 -7
  283. package/dist/src/types/agent.d.ts.map +1 -1
  284. package/dist/src/types/events.d.ts +33 -9
  285. package/dist/src/types/events.d.ts.map +1 -1
  286. package/dist/src/types/framework.d.ts +39 -3
  287. package/dist/src/types/framework.d.ts.map +1 -1
  288. package/dist/src/types/module.d.ts +95 -9
  289. package/dist/src/types/module.d.ts.map +1 -1
  290. package/dist/src/types/trace.d.ts +91 -3
  291. package/dist/src/types/trace.d.ts.map +1 -1
  292. package/package.json +5 -4
  293. package/src/agent.ts +661 -67
  294. package/src/api/server.ts +5 -4
  295. package/src/auto-continue.ts +43 -0
  296. package/src/code-execution/isolate/scripts.ts +11 -3
  297. package/src/cut-off-round.ts +98 -0
  298. package/src/framework.ts +6168 -1540
  299. package/src/gate/event-gate.ts +2895 -405
  300. package/src/gate/index.ts +10 -1
  301. package/src/gate/recipe-reconcile.ts +355 -0
  302. package/src/gate/reply-judge.ts +512 -0
  303. package/src/gate/types.ts +234 -9
  304. package/src/heartbeat/heartbeat.ts +804 -0
  305. package/src/heartbeat/reminder-time.ts +107 -0
  306. package/src/index.ts +11 -4
  307. package/src/mcpl/capability-mask.ts +3 -2
  308. package/src/mcpl/channel-registry.ts +1222 -468
  309. package/src/mcpl/conversation-router.ts +34 -16
  310. package/src/mcpl/convert-block.ts +14 -1
  311. package/src/mcpl/hook-orchestrator.ts +33 -6
  312. package/src/mcpl/inbound-source.ts +42 -69
  313. package/src/mcpl/index.ts +6 -1
  314. package/src/mcpl/inference-router.ts +79 -34
  315. package/src/mcpl/prose-grammar.ts +7 -0
  316. package/src/mcpl/protocol-family.ts +2 -0
  317. package/src/mcpl/push-coalescer.ts +51 -13
  318. package/src/mcpl/reference-fetcher.ts +201 -162
  319. package/src/mcpl/references.ts +851 -77
  320. package/src/mcpl/server-connection.ts +189 -11
  321. package/src/mcpl/tags.ts +13 -2
  322. package/src/mcpl/tool-lifecycle.ts +21 -0
  323. package/src/mcpl/tool-presence.ts +716 -0
  324. package/src/mcpl/tool-result-normalize.ts +18 -6
  325. package/src/mcpl/tool-signature.ts +316 -0
  326. package/src/mcpl/toolset-host.ts +727 -0
  327. package/src/mcpl/toolsets-module.ts +545 -0
  328. package/src/mcpl/transport.ts +3 -2
  329. package/src/mcpl/types.ts +25 -2
  330. package/src/mcpl/visible-content.ts +22 -9
  331. package/src/module-registry.ts +85 -26
  332. package/src/modules/discord/index.ts +1 -1
  333. package/src/modules/history/index.ts +70 -30
  334. package/src/modules/history/semantic.ts +308 -54
  335. package/src/modules/workspace/answers.ts +133 -0
  336. package/src/modules/workspace/branch-intent.ts +10 -5
  337. package/src/modules/workspace/direct-write.ts +460 -0
  338. package/src/modules/workspace/disk-agreement.ts +52 -21
  339. package/src/modules/workspace/edit-text.ts +84 -35
  340. package/src/modules/workspace/effects.ts +22 -13
  341. package/src/modules/workspace/index.ts +2142 -723
  342. package/src/modules/workspace/observe.ts +381 -177
  343. package/src/modules/workspace/read-page.ts +228 -0
  344. package/src/modules/workspace/reconcile.ts +27 -15
  345. package/src/modules/workspace/result-text.ts +121 -0
  346. package/src/modules/workspace/sync.ts +16 -0
  347. package/src/modules/workspace/text.ts +58 -32
  348. package/src/modules/workspace/types.ts +137 -60
  349. package/src/modules/workspace/watcher.ts +8 -3
  350. package/src/prose-drafts.ts +4 -3
  351. package/src/reasoning-loss-notice.ts +62 -0
  352. package/src/reasoning-loss.ts +120 -0
  353. package/src/send-tools.ts +3 -2
  354. package/src/sent-rows.ts +162 -0
  355. package/src/silent-heartbeat.ts +17 -10
  356. package/src/stable-hash.ts +14 -0
  357. package/src/stored-injections.ts +131 -0
  358. package/src/text-as-stored.ts +4 -3
  359. package/src/thinking-drops.ts +135 -0
  360. package/src/tool-args.ts +34 -1
  361. package/src/tool-image-ledger.ts +9 -8
  362. package/src/tool-presentation.ts +3 -1
  363. package/src/tool-result-guard.ts +45 -17
  364. package/src/tool-result-history.ts +471 -90
  365. package/src/tune-out/coordinator.ts +80 -37
  366. package/src/tune-out/tools.ts +4 -1
  367. package/src/types/agent.ts +220 -7
  368. package/src/types/events.ts +33 -9
  369. package/src/types/framework.ts +36 -3
  370. package/src/types/module.ts +91 -9
  371. package/src/types/trace.ts +94 -3
package/src/agent.ts CHANGED
@@ -1,11 +1,15 @@
1
- import type { Membrane, NormalizedMessage, NormalizedRequest, ContentBlock, YieldingStream } from '@animalabs/membrane';
1
+ import { heartbeatToolAlias } from './heartbeat/heartbeat.js';
2
+ import type { Membrane, NormalizedMessage, NormalizedRequest, ContentBlock, ThinkingBindingEntry, YieldingStream } from '@animalabs/membrane';
2
3
  import { isAbortedResponse } from '@animalabs/membrane';
3
- import { createHash } from 'node:crypto';
4
4
  import type { CacheWireReceipt, KvUnifiedRequestHooks } from './kv-unified-wire.js';
5
5
  import { ToolResultGuard } from './tool-result-guard.js';
6
6
  import { withoutTokenEstimates } from './thinking-token-stamp.js';
7
+ import { placeThinkingDrops, type CompiledForRequest, type PlacedDrop } from './thinking-drops.js';
8
+ import { carriedBlock, type SentRow } from './sent-rows.js';
7
9
  import { injectionsAsStored } from './text-as-stored.js';
8
- import { assertBooleanSetting, holdsSettings } from './config-checks.js';
10
+ import { stableHash } from './stable-hash.js';
11
+ import { hasRenderedContent, storedInjectionMarkOf } from './stored-injections.js';
12
+ import { assertBooleanSetting, holdsSettings, kindOf } from './config-checks.js';
9
13
  import {
10
14
  CONTINUE_ROW,
11
15
  continueTurn,
@@ -33,6 +37,45 @@ export interface ActivationRequestOptions {
33
37
  /** A request that will never be sent (a preview): compile it as a dry
34
38
  * run (see `compileWithInjections` for what that leaves alone). */
35
39
  dryRun?: boolean;
40
+ /** Messages added to the compile's: a preview's stand-in for the context
41
+ * injections a start would store before its compile
42
+ * (stored-injections.ts), put where that compile would render them, as
43
+ * the window's newest rows. A preview stores nothing, so the compile
44
+ * doesn't weigh them against the budget or shape them as it would the
45
+ * stored rows. */
46
+ appendedMessages?: NormalizedMessage[];
47
+ /** `false`: the caller retries this stream's failures itself, so membrane
48
+ * sends a failed round once and hands the error up. The framework's turns
49
+ * pass it (its errorPolicy is their retry layer); a direct caller of
50
+ * startStream keeps membrane's retries. */
51
+ membraneRetries?: boolean;
52
+ }
53
+
54
+ /** A compile's render stats, as context-manager's `getRenderStats()` types
55
+ * them; each compile carries its own, where the strategy records them, in
56
+ * `CompileResult.renderStats`. */
57
+ export type RenderStats = NonNullable<ReturnType<ContextManager['getRenderStats']>>;
58
+
59
+ /** The render stats of the dry compile behind each request a dry run
60
+ * assembled (`assembleActivationRequest` with `dryRun`), by that request.
61
+ * A request that is sent has none here. Read through the framework's
62
+ * `getRequestCompileStats`. */
63
+ const dryCompileStats = new WeakMap<object, RenderStats | undefined>();
64
+
65
+ /** The render stats of the dry compile that built `request`, or null: see
66
+ * `AgentFramework.getRequestCompileStats`. */
67
+ export function dryCompileStatsOf(request: object): RenderStats | null {
68
+ return dryCompileStats.get(request) ?? null;
69
+ }
70
+
71
+ /**
72
+ * What a compile is told when the agent's model binds its thinking: `compile()`'s options for a context manager
73
+ * that keeps a thinking-binding record, as agent-framework holds the binding (with the participant whose messages
74
+ * are the agent's own replies) and as digests of the request's head (bindingCompileOptions).
75
+ */
76
+ export interface BindingCompileOptions {
77
+ thinkingBinding?: { binds: boolean; dropBlock: boolean; participant: string };
78
+ identity?: RequestIdentity;
36
79
  }
37
80
 
38
81
  export interface StartStreamResult {
@@ -40,13 +83,30 @@ export interface StartStreamResult {
40
83
  request: NormalizedRequest;
41
84
  /** The request-only `[Continue]` prompt this request's reply follows, if
42
85
  * any, read from the request: a new one when it ends on `[Continue]`;
43
- * else, when it ends on tool results, the prompt of the reply that called
86
+ * else, when it ends on tool results, or on tool results and then
87
+ * stored context injections alone, the prompt of the reply that called
44
88
  * those tools (`toolLoopPrompt`); else none. The framework stamps the
45
89
  * reply's rows with it (`metadata.promptedBy`), for later compiles to
46
90
  * render it before them. */
47
91
  continuePrompt?: RequestOnlyPrompt;
48
92
  takeKvSubmission?: () => { submissionId: string; wireReceipt: CacheWireReceipt } | undefined;
49
93
  drainKvSubmissionIds?: () => string[];
94
+ /** Where each block a call of this stream reports the provider dropped came from: a stored reply this
95
+ * request's compile rendered, a round the stream appended, or neither (placeThinkingDrops). Optional, so a
96
+ * stand-in for the stream start needn't supply it; without it, no drop can be placed. */
97
+ placeDrops?: (entries: readonly ThinkingBindingEntry[]) => PlacedDrop[];
98
+ /** The compile's `thinkingRecordedAfter`, when it gave one: a reply stored at or below it predates the branch's
99
+ * thinking-binding record, so the provider may drop its thinking with no strip having missed. */
100
+ thinkingRecordedAfter?: number;
101
+ /** The key (`branchKey`) of the branch the compile read, when its provenance names one. */
102
+ thinkingBranch?: string;
103
+ /** Tells the context manager what the stream's later rounds carried after the compiled messages, every round so
104
+ * far in sent order, with this request's head (context-manager's `noteRequestSent`). Present only where the
105
+ * model binds its thinking and the context manager keeps a record it can be told of. */
106
+ noteRequestSent?: (rows: readonly SentRow[]) => boolean;
107
+ /** The stored bodies the request's compile carried, by each body's first message id (its provenance's raw bodies).
108
+ * Absent when the compile reports no provenance. */
109
+ carriedBodies?: ReadonlySet<MessageId>;
50
110
  }
51
111
  import type {
52
112
  ContextManager,
@@ -55,10 +115,14 @@ import type {
55
115
  CompileResult,
56
116
  HotContextSettingsStatus,
57
117
  HotContextSettingsUpdate,
118
+ MessageId,
58
119
  MessageMetadata,
120
+ RequestIdentity,
121
+ ThinkingStrip,
59
122
  } from '@animalabs/context-manager';
60
123
  import type {
61
124
  AgentConfig,
125
+ AutoContinueConfig,
62
126
  AgentState,
63
127
  SameRoundThinkTextPolicy,
64
128
  SameRoundThinkTextPolicySource,
@@ -76,16 +140,22 @@ import type {
76
140
  AgentRuntimeSettingsOverrides,
77
141
  } from './types/index.js';
78
142
 
79
- function stableHash(value: unknown): string {
80
- const encode = (item: unknown): string => {
81
- if (Array.isArray(item)) return `[${item.map(encode).join(',')}]`;
82
- if (item && typeof item === 'object') {
83
- const record = item as Record<string, unknown>;
84
- return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${encode(record[key])}`).join(',')}}`;
85
- }
86
- return JSON.stringify(item) ?? 'null';
87
- };
88
- return createHash('sha256').update(encode(value)).digest('hex');
143
+
144
+ /**
145
+ * Whether a request's messages, as sanitized, end on `agentName`'s own
146
+ * message, apart from `injectionTail` stored context injections after it
147
+ * (counted over the messages the sanitizer keeps).
148
+ */
149
+ function endsOnOwnMessage(messages: NormalizedMessage[], agentName: string, injectionTail: number): boolean {
150
+ const own = messages[messages.length - 1 - injectionTail];
151
+ return own !== undefined && own.participant === agentName
152
+ && messages.slice(messages.length - injectionTail).every((m) => m.participant.startsWith('system_context:'));
153
+ }
154
+
155
+ /** A digest of a value as a request's JSON carries it (a field whose value is undefined left out, as the wire leaves
156
+ * it out), key order aside: the head digests the thinking binding compares, so a change on the wire changes one. */
157
+ function wireDigest(value: unknown): string {
158
+ return stableHash(JSON.parse(JSON.stringify(value) ?? 'null'));
89
159
  }
90
160
 
91
161
  const DEFAULT_CONTEXT_BUDGET_TOKENS = 100_000;
@@ -94,14 +164,16 @@ const DEFAULT_SAME_ROUND_THINK_TEXT_POLICY: SameRoundThinkTextPolicy = 'public';
94
164
 
95
165
  /**
96
166
  * Refuse an agent config whose `promptCaching`, `toolResultGuard`,
97
- * `toolWrapperProseGuard`, `thinking.enabled` or `refusalHandling.autoRewind`
98
- * holds anything but true or false (assertBooleanSetting), or whose `thinking`
99
- * or `refusalHandling` is anything but an object, null or false
100
- * (holdsSettings). The constructor runs it first, so the rule holds wherever
101
- * an Agent is built. `create()` also runs it for each of its agents before
102
- * anything opens, and `createEphemeralAgent` before it reserves the name, so a
103
- * refusal there leaves nothing behind. Membrane reads `thinking.enabled`, and
104
- * the refusal is here because the setting is AgentConfig's.
167
+ * `toolWrapperProseGuard`, `thinking.enabled`, `thinkingBinding.dropBlock`,
168
+ * `thinkingBinding.binds` or `refusalHandling.autoRewind` holds anything but true or false
169
+ * (assertBooleanSetting, where null counts as left out), or whose `thinking`, `thinkingBinding` or
170
+ * `refusalHandling` is anything but an object, null or false (holdsSettings),
171
+ * or whose `autoContinue` resolveAutoContinue refuses. The constructor runs it
172
+ * first, so the rule holds wherever an Agent is built. `create()` also runs it
173
+ * for each of its agents before anything opens, and `createEphemeralAgent`
174
+ * before it reserves the name, so a refusal there leaves nothing behind.
175
+ * Membrane reads `thinking.enabled`, and the refusal is here because the
176
+ * setting is AgentConfig's.
105
177
  */
106
178
  export function assertAgentSwitches(config: AgentConfig): void {
107
179
  const agent = `Agent "${config.name}"`;
@@ -117,6 +189,20 @@ export function assertAgentSwitches(config: AgentConfig): void {
117
189
  'true asks the provider for extended thinking, and false or leaving it out doesn\'t',
118
190
  );
119
191
  }
192
+ if (holdsSettings(`${agent} thinkingBinding`, config.thinkingBinding, '{ binds: true, dropBlock: true }')) {
193
+ assertBooleanSetting(
194
+ `${agent} thinkingBinding.dropBlock`,
195
+ config.thinkingBinding.dropBlock,
196
+ 'true asks the provider to drop a signed thinking block whose earlier conversation changed rather than refuse the request, ' +
197
+ 'and false or leaving it out sends nothing, so the account\'s default applies',
198
+ );
199
+ assertBooleanSetting(
200
+ `${agent} thinkingBinding.binds`,
201
+ config.thinkingBinding.binds,
202
+ 'true says the model binds its signed thinking to the conversation before it, so each compile tells the context manager so, ' +
203
+ 'and false or leaving it out compiles as before',
204
+ );
205
+ }
120
206
  if (holdsSettings(`${agent} refusalHandling`, config.refusalHandling, '{ autoRewind: true }')) {
121
207
  assertBooleanSetting(
122
208
  `${agent} refusalHandling.autoRewind`,
@@ -137,6 +223,49 @@ export function assertAgentSwitches(config: AgentConfig): void {
137
223
  'true keeps a response whose whole prose is a tool-call-shaped wrapper from running or entering the agent\'s history, ' +
138
224
  'and false or leaving it out doesn\'t',
139
225
  );
226
+ resolveAutoContinue(config);
227
+ }
228
+
229
+ /** `AutoContinueConfig.maxConsecutive` when left out. */
230
+ export const DEFAULT_AUTO_CONTINUE_MAX = 3;
231
+ /** The most continuations in a row a config may ask for. */
232
+ const MAX_AUTO_CONTINUE_MAX = 20;
233
+
234
+ /**
235
+ * `AgentConfig.autoContinue` resolved: undefined when off (left out, null or
236
+ * false), else its cap. `true` takes the default cap; an object takes
237
+ * `maxConsecutive`, a whole number from 1 to 20 (the default when left out).
238
+ * Any other value is refused.
239
+ */
240
+ export function resolveAutoContinue(config: AgentConfig): { maxConsecutive: number } | undefined {
241
+ const setting = `Agent "${config.name}" autoContinue`;
242
+ const value: unknown = config.autoContinue;
243
+ if (value === true) return { maxConsecutive: DEFAULT_AUTO_CONTINUE_MAX };
244
+ if (value !== undefined && value !== null && value !== false && (typeof value !== 'object' || Array.isArray(value))) {
245
+ throw new Error(
246
+ `${setting} must be true, false or an object such as { maxConsecutive: 3 }, not ${kindOf(value)}` +
247
+ (typeof value === 'string' ? ' (true and false go unquoted)' : ''),
248
+ );
249
+ }
250
+ if (!holdsSettings(setting, value as AgentConfig['autoContinue'], '{ maxConsecutive: 3 }')) return undefined;
251
+ const max = (value as AutoContinueConfig).maxConsecutive;
252
+ if (max === undefined) return { maxConsecutive: DEFAULT_AUTO_CONTINUE_MAX };
253
+ if (typeof max !== 'number' || !Number.isInteger(max) || max < 1 || max > MAX_AUTO_CONTINUE_MAX) {
254
+ // The value's kind, never the value (config-checks): a `${VAR}` that
255
+ // lands here could be a credential.
256
+ const kind = typeof max !== 'number' ? kindOf(max)
257
+ : !Number.isInteger(max) ? 'a number that isn\'t whole'
258
+ : max < 1 ? 'a number below 1' : `a number above ${MAX_AUTO_CONTINUE_MAX}`;
259
+ throw new Error(
260
+ `${setting}.maxConsecutive must be a whole number from 1 to ${MAX_AUTO_CONTINUE_MAX}, not ${kind}: ` +
261
+ 'the most turns in a row that continue without skip_reply before the agent waits for the next event',
262
+ );
263
+ }
264
+ return { maxConsecutive: max };
265
+ }
266
+
267
+ export function branchKey(branch: { readonly id: string; readonly created: number }): string {
268
+ return `${branch.id}\u0000${branch.created}`;
140
269
  }
141
270
 
142
271
  /**
@@ -162,6 +291,12 @@ export class Agent {
162
291
  * blocks are preserved through Chronicle.
163
292
  */
164
293
  readonly thinking: AgentConfig['thinking'];
294
+ /** Whether requests ask the provider to drop signed thinking whose earlier
295
+ * conversation changed (see AgentConfig.thinkingBinding.dropBlock). */
296
+ private readonly thinkingDropBlock: boolean;
297
+ /** Whether the model binds its signed thinking to the conversation before
298
+ * it (see AgentConfig.thinkingBinding.binds). */
299
+ private readonly thinkingBinds: boolean;
165
300
  /** Refusal auto-rewind policy (see AgentConfig.refusalHandling). */
166
301
  readonly refusalHandling: AgentConfig['refusalHandling'];
167
302
  readonly toolResultGuard: ToolResultGuard;
@@ -169,6 +304,9 @@ export class Agent {
169
304
  readonly proseRouting: 'locus' | 'explicit' | 'hybrid' | 'disabled';
170
305
  /** Exact whole-response known-tool wrapper containment (default off). */
171
306
  readonly toolWrapperProseGuard: boolean;
307
+ /** Continuation after a turn the model ends itself (see
308
+ * AgentConfig.autoContinue); undefined when off. */
309
+ readonly autoContinue: { maxConsecutive: number } | undefined;
172
310
  /** Clock rows' interval and precision (see AgentConfig.clock); undefined
173
311
  * when the agent writes none of its own (a resident still reads those
174
312
  * another resident writes into the shared slot). */
@@ -298,10 +436,13 @@ export class Agent {
298
436
  this.maxTokens = config.maxTokens ?? 4096;
299
437
  this.temperature = config.temperature;
300
438
  this.thinking = config.thinking;
439
+ this.thinkingDropBlock = config.thinkingBinding?.dropBlock === true;
440
+ this.thinkingBinds = config.thinkingBinding?.binds === true;
301
441
  this.refusalHandling = config.refusalHandling;
302
442
  this.toolResultGuard = new ToolResultGuard(config.name, contextManager, config.toolResultGuard === true);
303
443
  this.proseRouting = config.proseRouting ?? 'locus';
304
444
  this.toolWrapperProseGuard = config.toolWrapperProseGuard === true;
445
+ this.autoContinue = resolveAutoContinue(config);
305
446
  this.clock = resolveClockConfig(config.name, config.clock);
306
447
  this.cacheTtl = config.cacheTtl ?? '1h';
307
448
  this.promptCaching = config.promptCaching !== false;
@@ -309,7 +450,12 @@ export class Agent {
309
450
  this.providerParams = config.providerParams;
310
451
  this.sameRoundThinkTextPolicy = config.sameRoundThinkTextPolicy;
311
452
  this.maxStreamTokens = config.maxStreamTokens ?? 150_000;
312
- this.physicalWindowTokens = config.physicalWindowTokens;
453
+ // The window size converted as the projection's comparison (`projected > window`) converts it, so a string such
454
+ // as "200000" from a JSON or YAML config is its number, for every rule that reads it. A value whose number isn't
455
+ // finite is no window: null, NaN and Infinity, which the comparison never passed, and -Infinity, which it always
456
+ // did, a restart at every boundary.
457
+ const window = config.physicalWindowTokens == null ? NaN : Number(config.physicalWindowTokens);
458
+ this.physicalWindowTokens = Number.isFinite(window) ? window : undefined;
313
459
  this.contextBudgetTokens = config.contextBudgetTokens;
314
460
  this.configuredContextBudgetTokens = config.contextBudgetTokens;
315
461
  this.contextManager = contextManager;
@@ -353,7 +499,13 @@ export class Agent {
353
499
  if (this.allowedTools === 'all') {
354
500
  return true;
355
501
  }
356
- return this.allowedTools.includes(toolName);
502
+ if (this.allowedTools.includes(toolName)) return true;
503
+ // A heartbeat tool is one tool under either prefix: the framework's
504
+ // `heartbeat--x` and heartbeat-mcpl's `mcpl--heartbeat--x`, which an
505
+ // allowedTools written for the server names. Only one of them is ever
506
+ // offered, since the framework refuses the server beside its heartbeat.
507
+ const alias = heartbeatToolAlias(toolName);
508
+ return alias !== undefined && this.allowedTools.includes(alias);
357
509
  }
358
510
 
359
511
  /**
@@ -978,6 +1130,57 @@ export class Agent {
978
1130
  return this.shownWatermark;
979
1131
  }
980
1132
 
1133
+ /**
1134
+ * The newest consumed watermark a request carried that was answered: its
1135
+ * round's tool calls, or its completion, came back. A message a boundary
1136
+ * injected counts as heard only once a request carrying it is answered;
1137
+ * the consumed watermark moves when the stream is handed it, before its
1138
+ * request runs, so a request that fails, ending its stream with an error,
1139
+ * leaves this as it was.
1140
+ */
1141
+ private answeredWatermark: { branch: string; sequence: number } | null = null;
1142
+
1143
+ getAnsweredWatermark(): { branch: string; sequence: number } | null {
1144
+ return this.answeredWatermark;
1145
+ }
1146
+
1147
+ /** A request that carried `watermark` was answered. Requests are answered
1148
+ * in the order they're sent, and the consumed watermark only advances
1149
+ * within a branch, so the latest is the newest. */
1150
+ markAnswered(watermark: { branch: string; sequence: number } | null): void {
1151
+ if (watermark) this.answeredWatermark = watermark;
1152
+ }
1153
+
1154
+ /**
1155
+ * Per destination (a server's channel, by its key), the newest consumed
1156
+ * watermark the agent had read when it published there by a route the
1157
+ * framework sees (its prose, a sleep's announcement, a channel_publish, a
1158
+ * draft resend, or a send tool whose channel the framework records), on
1159
+ * the branch it was taken on. A wake that carries a channel, unless it's
1160
+ * marked ambient (InferenceRequest.ambient), keeps its turn unless the
1161
+ * agent spoke there after reading the write the wake is for. It
1162
+ * lives as long as this Agent: an agent made again starts it empty, and
1163
+ * such a wake then stays.
1164
+ */
1165
+ private publishedThrough = new Map<string, { branch: string; sequence: number }>();
1166
+
1167
+ /** Note a publish into `destination` (its key), made having read through
1168
+ * `readThrough`: the consumed watermark when its words were written, or
1169
+ * its call made. */
1170
+ notePublished(destination: string, readThrough: { branch: string; sequence: number } | null): void {
1171
+ if (!readThrough) return;
1172
+ const noted = this.publishedThrough.get(destination);
1173
+ if (noted && noted.branch === readThrough.branch && noted.sequence >= readThrough.sequence) return;
1174
+ this.publishedThrough.set(destination, readThrough);
1175
+ }
1176
+
1177
+ /** Whether the agent published into `destination` (its key) having read
1178
+ * the row stored at `sequence` on `branch`. */
1179
+ publishedAfterReading(destination: string, branch: string, sequence: number): boolean {
1180
+ const noted = this.publishedThrough.get(destination);
1181
+ return !!noted && noted.branch === branch && noted.sequence >= sequence;
1182
+ }
1183
+
981
1184
  /** Mark everything currently stored as consumed (the agent's creation, at
982
1185
  * boot or later, and a tool boundary that injected messages into a live
983
1186
  * stream declaring it carries them; see consumedWatermark). */
@@ -985,7 +1188,9 @@ export class Agent {
985
1188
  // Tolerates partial context-manager doubles (tests build Agents over
986
1189
  // stubs). Without a watermark, a reader that looks for one takes its own
987
1190
  // side: the coalescer counts the message as read, and the framework's
988
- // deferredWriteReadOrGone and unreadNoticesForPrimary count it as unread.
1191
+ // unreadNoticesForPrimary counts it as unread. A wake for a deferred
1192
+ // write or a stored message reads the answered watermark instead
1193
+ // (deferredWriteDone, storedMessageDone).
989
1194
  this.consumedWatermark = this.pendingWatermark();
990
1195
  this.watermarkMarks++;
991
1196
  this.shownWatermark = this.consumedWatermark;
@@ -1033,6 +1238,21 @@ export class Agent {
1033
1238
  this.watermarkMarks++;
1034
1239
  }
1035
1240
 
1241
+ /**
1242
+ * The branch now (`branchKey`) and its head, as `pendingWatermark` reads it: a message stored on that branch later
1243
+ * has a larger sequence. Sequences are numbered per branch, so one compares with another only on the same branch.
1244
+ */
1245
+ branchHead(): { branch: string; sequence: number } | undefined {
1246
+ const head = this.pendingWatermark();
1247
+ const cm = this.contextManager as Partial<ContextManager>;
1248
+ if (!head || typeof cm.currentBranchRef !== 'function') return undefined;
1249
+ try {
1250
+ return { branch: branchKey(cm.currentBranchRef()), sequence: head.sequence };
1251
+ } catch {
1252
+ return undefined;
1253
+ }
1254
+ }
1255
+
1036
1256
  /** The watermark a successful compile will establish (taken before, applied after). */
1037
1257
  private pendingWatermark(): { branch: string; sequence: number } | null {
1038
1258
  const cm = this.contextManager as Partial<ContextManager>;
@@ -1057,7 +1277,10 @@ export class Agent {
1057
1277
  * manager commits what it commits for one, the consumed watermark moves,
1058
1278
  * and a prepared budget change settles. To inspect what the agent would
1059
1279
  * see, use the framework's `previewActivation`, which compiles as a dry
1060
- * run. */
1280
+ * run. It isn't told the agent's thinking binding (`thinkingBinding.binds`),
1281
+ * so a context manager that keeps a thinking-binding record neither reads
1282
+ * nor writes the record here, and what it returns keeps thinking whose
1283
+ * earlier conversation changed. */
1061
1284
  async compileContext(budget?: TokenBudget): Promise<CompileResult> {
1062
1285
  // RFC-006 §3.3: consumption is determined at assembly — a compile that
1063
1286
  // fails assembled nothing, so the watermark moves only on success.
@@ -1077,6 +1300,211 @@ export class Agent {
1077
1300
  return result;
1078
1301
  }
1079
1302
 
1303
+ /**
1304
+ * The stored context injections of this agent's that the last compile it
1305
+ * built to send rendered (stored-injections.ts), the framework's activation
1306
+ * request's or the direct API's: for each namespace and content, the parts
1307
+ * the compile held, and the branch it read. A compile built to send whose
1308
+ * request then isn't sent (a stop, a failure) still records: its rows stay
1309
+ * in the window all the same. Undefined before the first, as after a
1310
+ * restart. The framework's same-content skip reads it (renderedInjection).
1311
+ */
1312
+ private renderedInjections?: {
1313
+ branch: { readonly id: string; readonly created: number };
1314
+ /** Per namespace, per content hash: the parts any copy of that content
1315
+ * holds, how many it has, and its newest copy's sequence. */
1316
+ byNamespace: Map<string, Map<string, { parts: number; held: Set<number>; sequence: number }>>;
1317
+ };
1318
+
1319
+ /**
1320
+ * Per branch (by id and created), the point a stored injection on it has
1321
+ * to come after to have been in no request yet (unsentInjection): the
1322
+ * newest row of this agent's window on that branch when the first plan
1323
+ * there since its last compile built to send (a start's, or a preview's,
1324
+ * which stores nothing) asked about a unit, before that plan's start
1325
+ * stored anything. Each compile built to send that records what it
1326
+ * held unsets its branch's, a restart unsets them all, and a plan asks
1327
+ * here before it stores a unit, so every one of this agent's stored
1328
+ * injections on a branch after its point was stored since that compile.
1329
+ * Kept per branch, so a start or preview on another branch doesn't move
1330
+ * this one's past a copy no request has had.
1331
+ */
1332
+ private readonly unsentSince = new Map<string, number>();
1333
+
1334
+ /**
1335
+ * A host's hook, called with the messages of each compile built to send
1336
+ * (never a dry run), as the compile is recorded: what the request carries,
1337
+ * after compression and the window have done their work. The framework
1338
+ * uses it to tell when an MCPL toolset's announcement has left the context
1339
+ * (RFC-014 §8.3 reminders). It must not change the messages; one that
1340
+ * throws is logged, and the start goes on.
1341
+ */
1342
+ onCompiledToSend?: (messages: readonly unknown[]) => void;
1343
+
1344
+ /**
1345
+ * A host's hook, called with each committing compile's `thinkingStrip` (never a dry run's) and the key of the
1346
+ * branch it read (`branchKey`), as the compile returns, whether or not its request is then sent, since the
1347
+ * context manager's binding record takes the strip either way. One that throws is logged. The framework sets it
1348
+ * at the agent's first stream unless a host has set one, so a host's own observer stands in for the framework's,
1349
+ * whose `[reasoning]` line for the agent then isn't told.
1350
+ */
1351
+ onThinkingStrip?: (strip: ThinkingStrip, branch: string) => void;
1352
+
1353
+ /**
1354
+ * The newest content of `namespace` among this agent's rows stored since
1355
+ * its last compile built to send (unsentSince), and whether every part of
1356
+ * it is among them: rows a start stored whose compile then didn't record
1357
+ * (it threw, or the start failed before it), which no request has had
1358
+ * yet, and which are newer than anything renderedInjection knows. Undefined when there are none, as at every
1359
+ * start whose last compile was built and recorded. A fold between that
1360
+ * start and the next (a host's own committing compile, or the compression
1361
+ * an over-budget retry waits for) can summarize them all the same: the
1362
+ * next request then has the summary's word for them, and the start after
1363
+ * it stores them again, as after any fold. The window is read back 64
1364
+ * rows at a time from the newest, stopping at that point.
1365
+ */
1366
+ unsentInjection(namespace: string): { contentHash: string; whole: boolean } | undefined {
1367
+ let newest: { contentHash: string; parts: number; held: Set<number> } | undefined;
1368
+ try {
1369
+ const branch = branchKey(this.contextManager.currentBranchRef());
1370
+ const since = this.unsentSince.get(branch);
1371
+ if (since === undefined) {
1372
+ const count = this.contextManager.getMessageCount();
1373
+ const top = count > 0 ? this.contextManager.getMessageWindow(count - 1, 1, { resolveBlobs: false }).messages[0]?.sequence ?? 0 : 0;
1374
+ this.unsentSince.set(branch, top);
1375
+ return undefined;
1376
+ }
1377
+ scan: for (let end = this.contextManager.getMessageCount(); end > 0;) {
1378
+ const span = Math.min(end, 64);
1379
+ const { messages } = this.contextManager.getMessageWindow(end - span, span, { resolveBlobs: false });
1380
+ for (let i = messages.length - 1; i >= 0; i--) {
1381
+ const row = messages[i]!;
1382
+ if (row.sequence <= since) break scan;
1383
+ const mark = storedInjectionMarkOf(row.metadata);
1384
+ if (mark?.agent !== this.name || mark.namespace !== namespace) continue;
1385
+ // Read from the newest back, the first copy met is the newest
1386
+ // content; its other parts pool with it.
1387
+ newest ??= { contentHash: mark.contentHash, parts: mark.parts, held: new Set() };
1388
+ if (mark.contentHash === newest.contentHash) newest.held.add(mark.part);
1389
+ }
1390
+ end -= span;
1391
+ }
1392
+ } catch (err) {
1393
+ // Where the rows read before the failure held a copy, the newest of
1394
+ // them is still the newest content (any unread row is older), and a
1395
+ // part it lacks may be unread, so it's stored again at worst. Where
1396
+ // they held none, the last request's record decides, as it did before
1397
+ // this check: a copy a failed start stored is then stored again, or,
1398
+ // where the source has changed back to what that request held, left
1399
+ // newest for one more request. Either way the start goes on, where
1400
+ // failing here would lose its turn.
1401
+ console.error(`[injection] ${this.name}: could not read its window for copies stored since its last request, so ${newest ? 'the rows it read decide' : "that request's record decides"}:`, err);
1402
+ if (!newest) return undefined;
1403
+ }
1404
+ return newest && { contentHash: newest.contentHash, whole: newest.held.size === newest.parts };
1405
+ }
1406
+
1407
+ /** Record which of this agent's stored injections a compile built to send
1408
+ * rendered. Another agent's stored rows never reach this agent's compile,
1409
+ * since every compile of a shared store leaves them out by its view
1410
+ * filter (isAnotherAgentsInjection): a compile of a shared store without
1411
+ * that filter would mix them in here. */
1412
+ private noteRenderedInjections(result: CompileResult): void {
1413
+ // The host's look at what this request carries (onCompiledToSend).
1414
+ if (this.onCompiledToSend) {
1415
+ try {
1416
+ this.onCompiledToSend(result.messages);
1417
+ } catch (err) {
1418
+ console.error(`[agent] ${this.name}: the host's compile observer failed:`, err);
1419
+ }
1420
+ }
1421
+ const provenance = result.provenance;
1422
+ // The rows stored before this compile on its branch have been compiled
1423
+ // to send, whether the request held them or a fold or the strategy's
1424
+ // window took them (the record below says which): the first plan there
1425
+ // after it, a start's or a preview's, sets the point again
1426
+ // (unsentInjection). Without provenance, which branch isn't known, so
1427
+ // every branch's point goes, rather than one surviving a compile that
1428
+ // held its rows.
1429
+ if (provenance) this.unsentSince.delete(branchKey(provenance.branch));
1430
+ else this.unsentSince.clear();
1431
+ if (!provenance) {
1432
+ this.renderedInjections = undefined;
1433
+ return;
1434
+ }
1435
+ const byNamespace = new Map<string, Map<string, { parts: number; held: Set<number>; sequence: number }>>();
1436
+ for (const [i, source] of provenance.messages.entries()) {
1437
+ // Only a message rendered as an injection's participant can be one, so
1438
+ // the store is read for those alone.
1439
+ if (source.kind !== 'raw' || !result.messages[i]?.participant.startsWith('system_context:')) continue;
1440
+ for (const body of source.bodies) {
1441
+ const mark = this.storedMarkOf(body.messageId);
1442
+ if (!mark) continue;
1443
+ // A part counts as held when all its shards are there. One the
1444
+ // strategy altered (cut at its per-message cap, an image stripped)
1445
+ // counts too: a new copy would meet the same, so storing again
1446
+ // would only add copies, every start. A part missing shards still
1447
+ // says which content is newest.
1448
+ const held = body.complete || (body.missing !== undefined && !body.missing.includes('shards'));
1449
+ const contents = byNamespace.get(mark.namespace) ?? new Map();
1450
+ byNamespace.set(mark.namespace, contents);
1451
+ // Copies of the same content in view pool what they hold: the agent
1452
+ // has each part that one of them holds.
1453
+ const known = contents.get(mark.contentHash) ?? { parts: mark.parts, held: new Set<number>(), sequence: body.sequence };
1454
+ if (held) known.held.add(mark.part);
1455
+ known.sequence = Math.max(known.sequence, body.sequence);
1456
+ contents.set(mark.contentHash, known);
1457
+ }
1458
+ }
1459
+ this.renderedInjections = { branch: provenance.branch, byNamespace };
1460
+ }
1461
+
1462
+ /**
1463
+ * The newest content of `namespace` among this agent's stored injections
1464
+ * that the last compile built to send rendered, by its newest copy: its
1465
+ * content hash, and whether every part of that content was there in some
1466
+ * copy, every shard, however the strategy shaped it. Undefined when none
1467
+ * was, when this agent hasn't built a request since it started, or when
1468
+ * the window's branch has changed since.
1469
+ */
1470
+ renderedInjection(namespace: string): { contentHash: string; whole: boolean } | undefined {
1471
+ const rendered = this.renderedInjections;
1472
+ const branch = this.contextManager.currentBranchRef();
1473
+ if (!rendered || rendered.branch.id !== branch.id || rendered.branch.created !== branch.created) return undefined;
1474
+ let newest: [string, { parts: number; held: Set<number>; sequence: number }] | undefined;
1475
+ for (const entry of rendered.byNamespace.get(namespace) ?? []) {
1476
+ if (!newest || entry[1].sequence > newest[1].sequence) newest = entry;
1477
+ }
1478
+ return newest ? { contentHash: newest[0], whole: newest[1].held.size === newest[1].parts } : undefined;
1479
+ }
1480
+
1481
+ /**
1482
+ * Whether compiled message `i` of `result` is a row of this agent's stored
1483
+ * context injections (stored-injections.ts): rendered as an injection's
1484
+ * participant, and each of its stored rows, by the compile's provenance,
1485
+ * carrying this agent's mark. A speaker merely named like an injection's
1486
+ * participant isn't one. Only such a message's rows are read, so a start
1487
+ * reads the store for those alone.
1488
+ */
1489
+ private isStoredInjectionRow(result: CompileResult, i: number): boolean {
1490
+ const source = result.provenance?.messages[i];
1491
+ return source?.kind === 'raw' && source.bodies.length > 0
1492
+ && !!result.messages[i]?.participant.startsWith('system_context:')
1493
+ && source.bodies.every((b) => this.storedMarkOf(b.messageId)?.agent === this.name);
1494
+ }
1495
+
1496
+ /** A stored row's injection mark, or undefined for any other row, and for
1497
+ * one the store can't read just now, which is logged: a start doesn't fail
1498
+ * on it (the row then counts as no stored injection). */
1499
+ private storedMarkOf(id: MessageId): ReturnType<typeof storedInjectionMarkOf> {
1500
+ try {
1501
+ return storedInjectionMarkOf(this.contextManager.getMessage(id)?.metadata);
1502
+ } catch (err) {
1503
+ console.error(`[injection] ${this.name}: could not read stored row ${id}, so it counts as no stored injection:`, err);
1504
+ return undefined;
1505
+ }
1506
+ }
1507
+
1080
1508
  /**
1081
1509
  * Compile the context with injections (e.g. from MCPL servers), which the
1082
1510
  * context manager merges into the compiled messages. Without
@@ -1088,13 +1516,12 @@ export class Agent {
1088
1516
  async compileWithInjections(
1089
1517
  budget?: TokenBudget,
1090
1518
  injections?: ContextInjection[],
1091
- opts?: { kvUnifiedImmutablePrefixHash?: string; dryRun?: boolean },
1519
+ opts?: { kvUnifiedImmutablePrefixHash?: string; dryRun?: boolean } & BindingCompileOptions,
1092
1520
  ): Promise<CompileResult> {
1093
1521
  // A dry run is a compile that will never be sent. The context manager
1094
1522
  // persists no fold resolutions for it and queues none of the compression
1095
- // or merge work its fold plan calls for. Its thinking binding
1096
- // (context-manager#160, not yet released) will commit stamps and add a
1097
- // compile awaiting acceptance for a committing compile only. Here nothing
1523
+ // or merge work its fold plan calls for, and, with the thinking binding
1524
+ // (`thinkingBinding.binds`), writes no binding record. Here nothing
1098
1525
  // moves either: the agent never read what a dry run assembled, so the
1099
1526
  // consumed watermark stays. A dry run proves nothing about the budget
1100
1527
  // either, so it goes straight to the compile, around
@@ -1126,6 +1553,13 @@ export class Agent {
1126
1553
  throw error;
1127
1554
  }
1128
1555
  if (watermark) this.applyCompileWatermark(watermark, marksBefore);
1556
+ if (!opts?.dryRun && result.thinkingStrip && this.onThinkingStrip) {
1557
+ try {
1558
+ this.onThinkingStrip(result.thinkingStrip, result.provenance ? branchKey(result.provenance.branch) : '');
1559
+ } catch (err) {
1560
+ console.error(`[agent] ${this.name}: the host's thinking-strip observer failed:`, err);
1561
+ }
1562
+ }
1129
1563
  return result;
1130
1564
  }
1131
1565
 
@@ -1135,7 +1569,13 @@ export class Agent {
1135
1569
 
1136
1570
  /**
1137
1571
  * Run inference and return result with tool calls and speech content.
1138
- * Updates agent state during execution.
1572
+ * Updates agent state during execution. Its compile commits. With
1573
+ * `thinkingBinding.binds` it also writes the context manager's
1574
+ * thinking-binding record, so then call it only while none of this agent's
1575
+ * requests is streaming: a stream open meanwhile then has its later notes
1576
+ * write nothing, and its replies go without their thinking at the next
1577
+ * compile. Its own reply is never noted, so with `binds` its thinking is
1578
+ * left out at the compile after it's stored.
1139
1579
  */
1140
1580
  async runInference(
1141
1581
  availableTools: ToolDefinition[],
@@ -1147,7 +1587,9 @@ export class Agent {
1147
1587
 
1148
1588
  /**
1149
1589
  * Run inference with context injections.
1150
- * Same as runInference but passes injections through to compile.
1590
+ * Same as runInference but passes injections through to compile. Its
1591
+ * compile commits, and with `thinkingBinding.binds` writes the
1592
+ * thinking-binding record, as runInference's does (see there).
1151
1593
  */
1152
1594
  async runInferenceWithInjections(
1153
1595
  availableTools: ToolDefinition[],
@@ -1191,9 +1633,14 @@ export class Agent {
1191
1633
  // been retried; one whose store still fails is compiled as the bare
1192
1634
  // notice.
1193
1635
  let unguardedResults: ReturnType<Agent['buildToolResultMessages']> | undefined;
1636
+ const binding = this.bindingCompileOptions(tools, (injections ?? [])
1637
+ .filter((injection) => injection.position === 'system')
1638
+ .flatMap((injection) => injection.content));
1194
1639
  const assemble = async (): Promise<NormalizedRequest> => {
1195
1640
  const compiled = await this.toolResultGuard.afterAnnotations(
1196
- () => this.compileWithInjections(budget, injections));
1641
+ () => this.compileWithInjections(budget, injections, binding.thinkingBinding ? binding : undefined));
1642
+ // What it holds of the stored injections is what the agent last saw.
1643
+ this.noteRenderedInjections(compiled);
1197
1644
  const { systemInjections } = compiled;
1198
1645
  // The framework's own pricing stamp never goes out (thinking-token-stamp.ts).
1199
1646
  const messages = withoutTokenEstimates(compiled.messages);
@@ -1322,7 +1769,10 @@ export class Agent {
1322
1769
  * watermark); a preview passes `options.dryRun`, so its compile commits
1323
1770
  * none of that (see `compileWithInjections` for what a dry run still
1324
1771
  * leaves). Either way the context manager is handed the compression tool
1325
- * definitions. Safe to call regardless of the agent's current status.
1772
+ * definitions. Safe to call regardless of the agent's current status,
1773
+ * except that with `thinkingBinding.binds` a compile that isn't a dry run
1774
+ * writes the context manager's binding record, so while one of the
1775
+ * agent's requests streams, a call to look passes `options.dryRun`.
1326
1776
  * It doesn't wait for the tool-result guard's annotations, so a preview
1327
1777
  * never waits on a workspace write (rollback, undo and redo report one), and
1328
1778
  * may show a withheld stub before its place is added; the activation waits
@@ -1334,7 +1784,7 @@ export class Agent {
1334
1784
  budget?: TokenBudget,
1335
1785
  compressionTools: ToolDefinition[] = availableTools,
1336
1786
  options: ActivationRequestOptions = {},
1337
- ): Promise<{ request: NormalizedRequest; continuePrompt?: RequestOnlyPrompt }> {
1787
+ ): Promise<{ request: NormalizedRequest; continuePrompt?: RequestOnlyPrompt; compiled: CompiledForRequest; binding: BindingCompileOptions }> {
1338
1788
  // Unstored, so they leave for the request here (text-as-stored.ts), the
1339
1789
  // layout hash below included.
1340
1790
  injections = injectionsAsStored(injections);
@@ -1363,16 +1813,63 @@ export class Agent {
1363
1813
  system: this.buildSystemPrompt(prospectiveSystemInjections),
1364
1814
  })
1365
1815
  : undefined;
1366
- let { messages, systemInjections } = await this.compileWithInjections(
1816
+ const binding = this.bindingCompileOptions(availableTools, prospectiveSystemInjections);
1817
+ const compiledResult = await this.compileWithInjections(
1367
1818
  budget,
1368
1819
  injections,
1369
- immutablePrefixHash || options.dryRun
1820
+ immutablePrefixHash || options.dryRun || binding.thinkingBinding
1370
1821
  ? {
1371
1822
  ...(immutablePrefixHash ? { kvUnifiedImmutablePrefixHash: immutablePrefixHash } : {}),
1372
1823
  ...(options.dryRun ? { dryRun: true } : {}),
1824
+ ...binding,
1373
1825
  }
1374
1826
  : undefined,
1375
1827
  );
1828
+ let { messages } = compiledResult;
1829
+ const { systemInjections } = compiledResult;
1830
+ // It counts what the compile selected, so a preview's stand-ins
1831
+ // (appendedMessages, below) aren't in it.
1832
+ const { renderStats } = compiledResult;
1833
+ // What the request is built from, as the compile returned it: a block the provider drops is found again in
1834
+ // it, through the copies below, by its signature (placeThinkingDrops).
1835
+ const recordedAfter = compiledResult.thinkingRecordedAfter;
1836
+ const compiled: CompiledForRequest = {
1837
+ messages: compiledResult.messages,
1838
+ ...(compiledResult.provenance ? { provenance: compiledResult.provenance } : {}),
1839
+ ...(recordedAfter !== undefined ? { recordedAfter } : {}),
1840
+ };
1841
+ // Each message's place in the compile, or a preview's stand-in (null),
1842
+ // for the count of stored rows the request ends on, below.
1843
+ let origin: Array<number | null> = messages.map((_, i) => i);
1844
+ // A request that's sent says which stored injections the agent now has in
1845
+ // view, which the next start's same-content skip reads (stored-injections.ts).
1846
+ if (!options.dryRun) this.noteRenderedInjections(compiledResult);
1847
+ const appended = options.appendedMessages ?? [];
1848
+ if (appended.length > 0) {
1849
+ // Where a start's compile would render them, as the window's newest
1850
+ // rows: last, unless the window has no user message, where the compile
1851
+ // places the injections it's given at the end, after the window.
1852
+ const sources = compiledResult.provenance?.messages;
1853
+ let at = messages.length;
1854
+ if (sources && !messages.some((m, i) => sources[i]?.kind !== 'injection' && m.participant.toLowerCase() === 'user')) {
1855
+ while (at > 0 && sources[at - 1]?.kind === 'injection') at--;
1856
+ }
1857
+ origin = [...origin.slice(0, at), ...appended.map(() => null), ...origin.slice(at)];
1858
+ messages = [...messages.slice(0, at), ...appended, ...messages.slice(at)];
1859
+ }
1860
+ // The stored context injections the request ends on, counted over the
1861
+ // messages the sanitizer below keeps (hasRenderedContent), since it drops
1862
+ // a message it leaves empty, a placed blank injection among them.
1863
+ // [Continue] goes after them, and a tool loop's prompt is read from
1864
+ // before them: they're context, not what woke the agent. An injection
1865
+ // compile() places ends the tail, as on main, where every one was placed.
1866
+ let injectionTail = 0;
1867
+ for (let i = messages.length - 1; i >= 0; i--) {
1868
+ if (!hasRenderedContent(messages[i]!.content)) continue;
1869
+ const from = origin[i] ?? null;
1870
+ if (from !== null && !this.isStoredInjectionRow(compiledResult, from)) break;
1871
+ injectionTail++;
1872
+ }
1376
1873
 
1377
1874
  // Sanitize: strip empty/whitespace text blocks and drop messages left with
1378
1875
  // no content. The Anthropic API rejects empty text blocks with 400
@@ -1385,15 +1882,10 @@ export class Agent {
1385
1882
  // 2026-07-10 on a resident agent: one empty block muted the agent's live
1386
1883
  // path entirely; twin of context-manager's stripEmptyTextBlocks on the
1387
1884
  // compression path.)
1885
+ // The same pass a tool boundary applies to what it notes as sent
1886
+ // (sent-rows.ts), so the two can't drift.
1388
1887
  messages = messages
1389
- .map((m) => ({
1390
- ...m,
1391
- content: m.content.filter(
1392
- // typeof guard is deliberate runtime defense: history loaded from
1393
- // disk can carry a non-string `text` despite what the types claim.
1394
- (b: ContentBlock) => !(b.type === "text" && (typeof b.text !== "string" || b.text.trim() === "")),
1395
- ),
1396
- }))
1888
+ .map((m) => ({ ...m, content: m.content.filter(carriedBlock) }))
1397
1889
  .filter((m) => m.content.length > 0);
1398
1890
  // The framework's own pricing stamp never goes out (thinking-token-stamp.ts).
1399
1891
  messages = withoutTokenEstimates(messages);
@@ -1419,21 +1911,29 @@ export class Agent {
1419
1911
 
1420
1912
  // The [Continue] prompt this request's reply follows, read from the
1421
1913
  // request itself: a new one when it ends on [Continue]; else, when it
1422
- // ends on tool results, the prompt of the reply that called those tools,
1423
- // whose tool loop this reply continues; else none. A request that ends
1914
+ // ends on tool results (apart from stored context injections after
1915
+ // them), the prompt of the reply that called those tools, whose tool
1916
+ // loop this reply continues; else none. A request that ends
1424
1917
  // on anything else (a message that arrived during a cooldown, say) gets
1425
1918
  // a reply that follows that message and carries no prompt.
1426
1919
  let continuePrompt: RequestOnlyPrompt | undefined;
1427
1920
  if (openingTick) {
1428
1921
  messages = [...messages, silentHeartbeatSeparatorTurn()];
1429
- } else if (messages.length > 0 && messages[messages.length - 1]!.participant === this.name) {
1922
+ } else if (endsOnOwnMessage(messages, this.name, injectionTail)) {
1430
1923
  // Safety: ensure messages don't end with an assistant message.
1431
1924
  // Some models reject trailing assistant messages ("prefill not supported"),
1432
1925
  // and after context compression a stale assistant turn can end up last.
1926
+ // Where stored context injections follow the agent's message, as a
1927
+ // changed one does on a wake with nothing new, [Continue] goes after
1928
+ // them, so the cue stays last: the block isn't taken for what woke
1929
+ // the agent. Its reply's promptedBy puts it there in later compiles too,
1930
+ // where they can recognise the reply (renderRequestOnlyRows).
1433
1931
  messages = [...messages, continueTurn()];
1434
1932
  continuePrompt = { turn: randomUUID(), text: CONTINUE_ROW };
1435
1933
  } else {
1436
- continuePrompt = toolLoopPrompt(messages.at(-1), index);
1934
+ // What the request ends on apart from the stored injections after it,
1935
+ // which aren't something the reply answers.
1936
+ continuePrompt = toolLoopPrompt(messages.at(-1 - injectionTail), index);
1437
1937
  }
1438
1938
 
1439
1939
  const request: NormalizedRequest = {
@@ -1443,25 +1943,27 @@ export class Agent {
1443
1943
  ...this.requestSettings(),
1444
1944
  ...(kvUnified ? { cacheMarkers: 'cm-owned' as const } : {}),
1445
1945
  };
1446
- return { request, ...(continuePrompt ? { continuePrompt } : {}) };
1946
+ if (options.dryRun) dryCompileStats.set(request, renderStats);
1947
+ return { request, ...(continuePrompt ? { continuePrompt } : {}), compiled, binding };
1447
1948
  }
1448
1949
 
1449
1950
  /**
1450
1951
  * The request fields that come from this agent's own settings: its model
1451
- * config, prompt caching and cache TTL, prefill message, provider
1452
- * parameters and participant name. Every request the agent sends takes
1453
- * them from here, a framework turn's (assembleActivationRequest) and the
1454
- * direct API's (runInferenceWithInjections), so a setting reaches both or
1455
- * neither.
1952
+ * config, thinking binding, prompt caching and cache TTL, prefill message,
1953
+ * provider parameters and participant name. Every request the agent sends
1954
+ * takes them from here, a framework turn's (assembleActivationRequest) and
1955
+ * the direct API's (runInferenceWithInjections), so a setting reaches both
1956
+ * or neither.
1456
1957
  */
1457
1958
  private requestSettings(): Pick<NormalizedRequest,
1458
1959
  'config' | 'promptCaching' | 'cacheTtl' | 'prefillUserMessage' | 'providerParams' | 'assistantParticipant'> {
1960
+ const thinking = this.requestThinking();
1459
1961
  return {
1460
1962
  config: {
1461
1963
  model: this.model,
1462
1964
  maxTokens: this.maxTokens,
1463
1965
  ...(this.temperature !== undefined && { temperature: this.temperature }),
1464
- ...(this.thinking !== undefined && { thinking: this.thinking }),
1966
+ ...(thinking !== undefined && { thinking }),
1465
1967
  },
1466
1968
  promptCaching: this.promptCaching,
1467
1969
  cacheTtl: this.cacheTtl,
@@ -1471,6 +1973,71 @@ export class Agent {
1471
1973
  };
1472
1974
  }
1473
1975
 
1976
+ /**
1977
+ * The thinking config a request carries: the agent's own, with the binding
1978
+ * control added when `thinkingBinding.dropBlock` asks for it. A model whose
1979
+ * thinking is always on takes the control whatever `enabled` says, so an
1980
+ * agent with no thinking config of its own still sends one, holding only
1981
+ * the control and `enabled: false`. Membrane sends nothing from it to any
1982
+ * other model, since thinking is off there.
1983
+ */
1984
+ private requestThinking(): NonNullable<NormalizedRequest['config']['thinking']> | undefined {
1985
+ if (!this.thinkingDropBlock) return this.thinking;
1986
+ const own = this.thinking !== null && typeof this.thinking === 'object' ? this.thinking : { enabled: false };
1987
+ return { ...own, blockBinding: { prefixMismatchBehavior: 'drop_block' } };
1988
+ }
1989
+
1990
+ /** The stream's way to tell the context manager what its later rounds sent, with the head its compile was given:
1991
+ * where the agent's model binds its thinking (context-manager's `noteRequestSent`). */
1992
+ private requestNotes(binding: BindingCompileOptions, compiled: CompiledForRequest): Pick<StartStreamResult, 'noteRequestSent'> {
1993
+ const identity = binding.identity;
1994
+ // Each note names the compile the stream was built from, so a note can't vouch for this stream under a record a
1995
+ // later committing compile with binds wrote. A compile that reports no provenance gives the stream nothing to name, and the
1996
+ // stream notes nothing: its replies go without their thinking at the next compile, never refused.
1997
+ const compileId = compiled.provenance?.compileId;
1998
+ if (!identity || compileId === undefined) return {};
1999
+ const cm = this.contextManager;
2000
+ return { noteRequestSent: (rows) => cm.noteRequestSent(rows, identity, compileId) };
2001
+ }
2002
+
2003
+ /**
2004
+ * The binding a compile of this agent is told of (bindingCompileOptions), and a preview of its context too: the
2005
+ * participant whose messages are its replies is its own name. Undefined where the model doesn't bind.
2006
+ * @internal For the framework's preview and its budget rule; not a host API.
2007
+ */
2008
+ thinkingBindingOption(): { binds: true; dropBlock: boolean; participant: string } | undefined {
2009
+ return this.thinkingBinds ? { binds: true, dropBlock: this.thinkingDropBlock, participant: this.name } : undefined;
2010
+ }
2011
+
2012
+ /**
2013
+ * What a compile the agent sends tells the context manager when the model binds its thinking
2014
+ * (`thinkingBinding.binds`): the binding as set, with the agent's own name as the participant whose messages
2015
+ * are its replies (an agent stores its replies under its name), and a digest of each part of the request ahead of
2016
+ * its messages, so a changed one can stand for a first difference where the context manager keeps a
2017
+ * thinking-binding record. The system text is the one the request will carry, built from the system-position
2018
+ * injections given to the compile as the kv-unified prefix hash builds it; the tools are the ones it will carry, in
2019
+ * their order; `request` covers the thinking config, `maxTokens` and `providerParams`, where a host puts its own
2020
+ * thinking config and `tool_choice`, and `prefillUserMessage`, which membrane sends as a leading user turn ahead
2021
+ * of a conversation that would begin with the assistant. Nothing without `binds`: the compile is as it was.
2022
+ */
2023
+ private bindingCompileOptions(tools: readonly ToolDefinition[], systemInjections: ContentBlock[]): BindingCompileOptions {
2024
+ const thinkingBinding = this.thinkingBindingOption();
2025
+ if (!thinkingBinding) return {};
2026
+ const { config, providerParams, prefillUserMessage } = this.requestSettings();
2027
+ return {
2028
+ thinkingBinding,
2029
+ identity: {
2030
+ system: wireDigest(this.buildSystemPrompt(systemInjections)),
2031
+ tools: wireDigest(tools),
2032
+ model: wireDigest(this.model),
2033
+ request: wireDigest({
2034
+ thinking: config.thinking ?? null, maxTokens: config.maxTokens, providerParams: providerParams ?? null,
2035
+ prefillUserMessage: prefillUserMessage ?? null,
2036
+ }),
2037
+ },
2038
+ };
2039
+ }
2040
+
1474
2041
  /** The request `assembleActivationRequest` builds (see there), for preview
1475
2042
  * and debug tooling: what this agent's next activation would send. */
1476
2043
  async buildActivationRequest(
@@ -1541,10 +2108,10 @@ export class Agent {
1541
2108
  // ran. The stream driver collects it on that stream's terminal event, or
1542
2109
  // at the loop's end when the iterator closed without one, while the
1543
2110
  // stream still owns the agent. This clear is for the records no driver
1544
- // collects: a dropped turn's, whose stream no driver iterates; one whose
1545
- // stream's terminal came after another stream took the agent; or a
1546
- // cancel after the stream's complete event. A fresh stream must not
1547
- // inherit one and read its own later error as a deliberate stop.
2111
+ // collects: a cancel of a stream no driver iterates; one whose stream's
2112
+ // terminal came after another stream took the agent; or a cancel after
2113
+ // the stream's complete event. A fresh stream must not inherit one and
2114
+ // read its own later error as a deliberate stop.
1548
2115
  this._pendingCancel = undefined;
1549
2116
  this._inferenceStartedAt = Date.now();
1550
2117
  this.lastStreamInputTokens = 0;
@@ -1563,7 +2130,7 @@ export class Agent {
1563
2130
  // Compiled only once each withheld stub's annotation has finished,
1564
2131
  // including one that began during the compile, and after a stub edit that
1565
2132
  // failed has been retried (see afterAnnotations).
1566
- const { request, continuePrompt } = await this.toolResultGuard.afterAnnotations(
2133
+ const { request, continuePrompt, compiled, binding } = await this.toolResultGuard.afterAnnotations(
1567
2134
  () => this.assembleActivationRequest(availableTools, injections, budget, compressionTools, options));
1568
2135
  request.messages = this.toolResultGuard.prepareRequest(request.messages, true);
1569
2136
 
@@ -1595,6 +2162,12 @@ export class Agent {
1595
2162
  emitTokens: true,
1596
2163
  emitBlocks: false,
1597
2164
  emitUsage: true,
2165
+ // A caller that retries itself (the framework's turns: errorPolicy,
2166
+ // provider holds, stated waits, each attempt a fresh compile a stop can
2167
+ // cancel) has membrane send a failed round once and hand the error up:
2168
+ // since membrane 0.7 its own schedules, ten minutes at a 529, would
2169
+ // otherwise run inside each of the caller's attempts, out of its sight.
2170
+ ...(options.membraneRetries === false ? { retry: false } : {}),
1598
2171
  // Retry a content-policy refusal at the provider seam rather than at
1599
2172
  // the framework level: membrane replays the SAME request immediately
1600
2173
  // (cache-warm), where a framework-level requeue would recompile and
@@ -1614,6 +2187,14 @@ export class Agent {
1614
2187
  stream,
1615
2188
  request,
1616
2189
  ...(continuePrompt ? { continuePrompt } : {}),
2190
+ // Read against the messages the stream sends, which membrane's report indexes (RoundReport's coordinates).
2191
+ placeDrops: (entries) => placeThinkingDrops(entries, request.messages, compiled),
2192
+ ...this.requestNotes(binding, compiled),
2193
+ ...(compiled.provenance
2194
+ ? { carriedBodies: new Set(compiled.provenance.messages.flatMap((m) => m.kind === 'raw' ? m.bodies.map((b) => b.messageId) : [])) }
2195
+ : {}),
2196
+ ...(compiled.recordedAfter !== undefined ? { thinkingRecordedAfter: compiled.recordedAfter } : {}),
2197
+ ...(compiled.provenance ? { thinkingBranch: branchKey(compiled.provenance.branch) } : {}),
1617
2198
  ...(kvEnabled
1618
2199
  ? {
1619
2200
  takeKvSubmission: () => kvQueue.shift(),
@@ -1667,7 +2248,7 @@ export class Agent {
1667
2248
  * Add an assistant response to context.
1668
2249
  * Called by framework when stream completes.
1669
2250
  */
1670
- addAssistantResponse(content: ContentBlock[], metadata?: MessageMetadata): void {
2251
+ addAssistantResponse(content: ContentBlock[], metadata?: MessageMetadata): MessageId | undefined {
1671
2252
  // A turn whose entire output is thinking blocks produced NOTHING: no
1672
2253
  // speech, no tool call. That is what a refusal looks like on the wire —
1673
2254
  // the provider returns signed thinking (often with empty text, the
@@ -1691,9 +2272,9 @@ export class Agent {
1691
2272
  `message (${content.length} block(s), no text and no tool_use) — the turn produced no ` +
1692
2273
  `output. See the refusal/inference-failed record for why.`,
1693
2274
  );
1694
- return;
2275
+ return undefined;
1695
2276
  }
1696
- this.contextManager.addMessage(this.name, content, metadata);
2277
+ return this.contextManager.addMessage(this.name, content, metadata);
1697
2278
  }
1698
2279
 
1699
2280
  /**
@@ -1728,12 +2309,24 @@ export class Agent {
1728
2309
  * one is still ahead; otherwise the next start drops it. `reason` is
1729
2310
  * provenance for the framework's inference:aborted trace and marker
1730
2311
  * metadata (e.g. 'zombie_reclaim', 'subagent_cancel'); it never reaches
1731
- * the provider.
2312
+ * the provider. When the stream's cancel() throws, the throw goes on to
2313
+ * the caller, and the agent still holds the stream, its cancel record as
2314
+ * it was before the call.
1732
2315
  */
1733
2316
  cancelStream(reason?: string): void {
1734
2317
  if ('stream' in this._state && this._state.stream) {
2318
+ // A cancel that throws leaves the stream going and the agent holding
2319
+ // it, so this call's record is taken back, leaving any earlier one: a
2320
+ // later end of that stream, an `error` the provider reports included,
2321
+ // is not read as this stop's.
2322
+ const earlier = this._pendingCancel;
1735
2323
  this._pendingCancel = { reason };
1736
- this._state.stream.cancel();
2324
+ try {
2325
+ this._state.stream.cancel();
2326
+ } catch (error) {
2327
+ this._pendingCancel = earlier;
2328
+ throw error;
2329
+ }
1737
2330
  } else {
1738
2331
  this._stopWithoutStream = { reason };
1739
2332
  }
@@ -1909,6 +2502,7 @@ export class Agent {
1909
2502
  id: block.id,
1910
2503
  name: block.name,
1911
2504
  input: block.input,
2505
+ ...(block.unparseableInput !== undefined ? { unparseableInput: block.unparseableInput } : {}),
1912
2506
  });
1913
2507
  } else if (block.type === 'text') {
1914
2508
  speechContent.push(block);
@@ -1938,7 +2532,7 @@ export class Agent {
1938
2532
  toolUseId: r.id,
1939
2533
  content: isError
1940
2534
  ? truncateForHistory(typeof error === 'string' ? error.toWellFormed() : error as string, DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS)
1941
- : nonErrorResultText(r.result, DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS),
2535
+ : nonErrorResultText(r.result, DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS, { toolName: r.name }),
1942
2536
  isError,
1943
2537
  };
1944
2538
  });