@animalabs/agent-framework 0.27.0 → 0.29.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 (344) hide show
  1. package/README.md +145 -41
  2. package/dist/src/agent.d.ts +29 -2
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +73 -12
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  7. package/dist/src/api/mcp-server-core.js +3 -12
  8. package/dist/src/api/mcp-server-core.js.map +1 -1
  9. package/dist/src/api/server.d.ts.map +1 -1
  10. package/dist/src/api/server.js +11 -5
  11. package/dist/src/api/server.js.map +1 -1
  12. package/dist/src/auto-continue.d.ts +5 -2
  13. package/dist/src/auto-continue.d.ts.map +1 -1
  14. package/dist/src/auto-continue.js +9 -5
  15. package/dist/src/auto-continue.js.map +1 -1
  16. package/dist/src/code-execution/isolate/runner.d.ts +7 -5
  17. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
  18. package/dist/src/code-execution/isolate/runner.js +7 -5
  19. package/dist/src/code-execution/isolate/runner.js.map +1 -1
  20. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  21. package/dist/src/code-execution/isolate/scripts.js +3 -2
  22. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  23. package/dist/src/cut-off-round.d.ts +24 -0
  24. package/dist/src/cut-off-round.d.ts.map +1 -0
  25. package/dist/src/cut-off-round.js +87 -0
  26. package/dist/src/cut-off-round.js.map +1 -0
  27. package/dist/src/framework.d.ts +468 -125
  28. package/dist/src/framework.d.ts.map +1 -1
  29. package/dist/src/framework.js +3651 -1111
  30. package/dist/src/framework.js.map +1 -1
  31. package/dist/src/gate/event-gate.d.ts +133 -13
  32. package/dist/src/gate/event-gate.d.ts.map +1 -1
  33. package/dist/src/gate/event-gate.js +654 -59
  34. package/dist/src/gate/event-gate.js.map +1 -1
  35. package/dist/src/gate/index.d.ts +3 -2
  36. package/dist/src/gate/index.d.ts.map +1 -1
  37. package/dist/src/gate/index.js +2 -1
  38. package/dist/src/gate/index.js.map +1 -1
  39. package/dist/src/gate/reply-judge.d.ts +116 -0
  40. package/dist/src/gate/reply-judge.d.ts.map +1 -0
  41. package/dist/src/gate/reply-judge.js +467 -0
  42. package/dist/src/gate/reply-judge.js.map +1 -0
  43. package/dist/src/gate/types.d.ts +173 -2
  44. package/dist/src/gate/types.d.ts.map +1 -1
  45. package/dist/src/heartbeat/heartbeat.d.ts +202 -0
  46. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
  47. package/dist/src/heartbeat/heartbeat.js +778 -0
  48. package/dist/src/heartbeat/heartbeat.js.map +1 -0
  49. package/dist/src/heartbeat/reminder-time.d.ts +5 -0
  50. package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
  51. package/dist/src/heartbeat/reminder-time.js +107 -0
  52. package/dist/src/heartbeat/reminder-time.js.map +1 -0
  53. package/dist/src/held-writes.d.ts +3 -3
  54. package/dist/src/held-writes.js +3 -3
  55. package/dist/src/host-presentation.d.ts +170 -0
  56. package/dist/src/host-presentation.d.ts.map +1 -0
  57. package/dist/src/host-presentation.js +561 -0
  58. package/dist/src/host-presentation.js.map +1 -0
  59. package/dist/src/index.d.ts +5 -1
  60. package/dist/src/index.d.ts.map +1 -1
  61. package/dist/src/index.js +3 -1
  62. package/dist/src/index.js.map +1 -1
  63. package/dist/src/line-reader.d.ts +4 -0
  64. package/dist/src/line-reader.d.ts.map +1 -0
  65. package/dist/src/line-reader.js +59 -0
  66. package/dist/src/line-reader.js.map +1 -0
  67. package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
  68. package/dist/src/mcpl/capability-mask.js +3 -2
  69. package/dist/src/mcpl/capability-mask.js.map +1 -1
  70. package/dist/src/mcpl/channel-registry.d.ts +34 -6
  71. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  72. package/dist/src/mcpl/channel-registry.js +133 -54
  73. package/dist/src/mcpl/channel-registry.js.map +1 -1
  74. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  75. package/dist/src/mcpl/conversation-router.js +2 -1
  76. package/dist/src/mcpl/conversation-router.js.map +1 -1
  77. package/dist/src/mcpl/convert-block.d.ts +1 -0
  78. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  79. package/dist/src/mcpl/convert-block.js +16 -4
  80. package/dist/src/mcpl/convert-block.js.map +1 -1
  81. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  82. package/dist/src/mcpl/hook-orchestrator.js +4 -3
  83. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  84. package/dist/src/mcpl/inbound-source.d.ts +29 -11
  85. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  86. package/dist/src/mcpl/inbound-source.js +43 -70
  87. package/dist/src/mcpl/inbound-source.js.map +1 -1
  88. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  89. package/dist/src/mcpl/modern-connection.js +11 -7
  90. package/dist/src/mcpl/modern-connection.js.map +1 -1
  91. package/dist/src/mcpl/protocol-family.d.ts +3 -2
  92. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  93. package/dist/src/mcpl/protocol-family.js +25 -3
  94. package/dist/src/mcpl/protocol-family.js.map +1 -1
  95. package/dist/src/mcpl/push-handler.js +2 -2
  96. package/dist/src/mcpl/push-handler.js.map +1 -1
  97. package/dist/src/mcpl/references.d.ts +12 -40
  98. package/dist/src/mcpl/references.d.ts.map +1 -1
  99. package/dist/src/mcpl/references.js +29 -23
  100. package/dist/src/mcpl/references.js.map +1 -1
  101. package/dist/src/mcpl/server-connection.d.ts +18 -8
  102. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  103. package/dist/src/mcpl/server-connection.js +70 -30
  104. package/dist/src/mcpl/server-connection.js.map +1 -1
  105. package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
  106. package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
  107. package/dist/src/mcpl/skip-reply-wake.js +25 -0
  108. package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
  109. package/dist/src/mcpl/tags.d.ts +9 -2
  110. package/dist/src/mcpl/tags.d.ts.map +1 -1
  111. package/dist/src/mcpl/tags.js +12 -2
  112. package/dist/src/mcpl/tags.js.map +1 -1
  113. package/dist/src/mcpl/tool-presence.d.ts +66 -14
  114. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  115. package/dist/src/mcpl/tool-presence.js +150 -80
  116. package/dist/src/mcpl/tool-presence.js.map +1 -1
  117. package/dist/src/mcpl/tool-signature.d.ts +10 -3
  118. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  119. package/dist/src/mcpl/tool-signature.js +60 -30
  120. package/dist/src/mcpl/tool-signature.js.map +1 -1
  121. package/dist/src/mcpl/toolset-host.d.ts +88 -55
  122. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  123. package/dist/src/mcpl/toolset-host.js +220 -111
  124. package/dist/src/mcpl/toolset-host.js.map +1 -1
  125. package/dist/src/mcpl/toolsets-module.d.ts +27 -35
  126. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  127. package/dist/src/mcpl/toolsets-module.js +313 -91
  128. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  129. package/dist/src/mcpl/transport.d.ts +1 -1
  130. package/dist/src/mcpl/transport.d.ts.map +1 -1
  131. package/dist/src/mcpl/transport.js +16 -16
  132. package/dist/src/mcpl/transport.js.map +1 -1
  133. package/dist/src/mcpl/types.d.ts +31 -13
  134. package/dist/src/mcpl/types.d.ts.map +1 -1
  135. package/dist/src/mcpl/types.js.map +1 -1
  136. package/dist/src/module-registry.d.ts +10 -8
  137. package/dist/src/module-registry.d.ts.map +1 -1
  138. package/dist/src/module-registry.js +15 -9
  139. package/dist/src/module-registry.js.map +1 -1
  140. package/dist/src/modules/api/index.d.ts.map +1 -1
  141. package/dist/src/modules/api/index.js +4 -2
  142. package/dist/src/modules/api/index.js.map +1 -1
  143. package/dist/src/modules/discord/index.d.ts.map +1 -1
  144. package/dist/src/modules/discord/index.js +33 -25
  145. package/dist/src/modules/discord/index.js.map +1 -1
  146. package/dist/src/modules/history/index.d.ts.map +1 -1
  147. package/dist/src/modules/history/index.js +30 -17
  148. package/dist/src/modules/history/index.js.map +1 -1
  149. package/dist/src/modules/sender-name.d.ts +7 -0
  150. package/dist/src/modules/sender-name.d.ts.map +1 -0
  151. package/dist/src/modules/sender-name.js +10 -0
  152. package/dist/src/modules/sender-name.js.map +1 -0
  153. package/dist/src/modules/workspace/answers.d.ts +26 -0
  154. package/dist/src/modules/workspace/answers.d.ts.map +1 -0
  155. package/dist/src/modules/workspace/answers.js +138 -0
  156. package/dist/src/modules/workspace/answers.js.map +1 -0
  157. package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
  158. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  159. package/dist/src/modules/workspace/branch-intent.js +6 -2
  160. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  161. package/dist/src/modules/workspace/direct-write.d.ts +3 -0
  162. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
  163. package/dist/src/modules/workspace/direct-write.js +59 -24
  164. package/dist/src/modules/workspace/direct-write.js.map +1 -1
  165. package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
  166. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  167. package/dist/src/modules/workspace/disk-agreement.js +48 -17
  168. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  169. package/dist/src/modules/workspace/edit-text.d.ts +2 -1
  170. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
  171. package/dist/src/modules/workspace/edit-text.js +4 -2
  172. package/dist/src/modules/workspace/edit-text.js.map +1 -1
  173. package/dist/src/modules/workspace/effects.d.ts +4 -2
  174. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  175. package/dist/src/modules/workspace/effects.js +67 -50
  176. package/dist/src/modules/workspace/effects.js.map +1 -1
  177. package/dist/src/modules/workspace/index.d.ts +63 -51
  178. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  179. package/dist/src/modules/workspace/index.js +980 -547
  180. package/dist/src/modules/workspace/index.js.map +1 -1
  181. package/dist/src/modules/workspace/observe.d.ts +125 -82
  182. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  183. package/dist/src/modules/workspace/observe.js +465 -241
  184. package/dist/src/modules/workspace/observe.js.map +1 -1
  185. package/dist/src/modules/workspace/read-page.d.ts.map +1 -1
  186. package/dist/src/modules/workspace/read-page.js +7 -1
  187. package/dist/src/modules/workspace/read-page.js.map +1 -1
  188. package/dist/src/modules/workspace/reconcile.d.ts +7 -1
  189. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  190. package/dist/src/modules/workspace/reconcile.js +156 -50
  191. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  192. package/dist/src/modules/workspace/result-text.d.ts +1 -0
  193. package/dist/src/modules/workspace/result-text.d.ts.map +1 -1
  194. package/dist/src/modules/workspace/result-text.js.map +1 -1
  195. package/dist/src/modules/workspace/sync.d.ts +9 -0
  196. package/dist/src/modules/workspace/sync.d.ts.map +1 -1
  197. package/dist/src/modules/workspace/sync.js +14 -0
  198. package/dist/src/modules/workspace/sync.js.map +1 -1
  199. package/dist/src/modules/workspace/types.d.ts +96 -43
  200. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  201. package/dist/src/modules/workspace/types.js.map +1 -1
  202. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  203. package/dist/src/modules/workspace/watcher.js +8 -3
  204. package/dist/src/modules/workspace/watcher.js.map +1 -1
  205. package/dist/src/notice-tags.d.ts +79 -0
  206. package/dist/src/notice-tags.d.ts.map +1 -0
  207. package/dist/src/notice-tags.js +83 -0
  208. package/dist/src/notice-tags.js.map +1 -0
  209. package/dist/src/prose-drafts.d.ts +4 -3
  210. package/dist/src/prose-drafts.d.ts.map +1 -1
  211. package/dist/src/prose-drafts.js +3 -2
  212. package/dist/src/prose-drafts.js.map +1 -1
  213. package/dist/src/reasoning-loss-notice.d.ts +23 -0
  214. package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
  215. package/dist/src/reasoning-loss-notice.js +52 -0
  216. package/dist/src/reasoning-loss-notice.js.map +1 -0
  217. package/dist/src/reasoning-loss.d.ts +24 -0
  218. package/dist/src/reasoning-loss.d.ts.map +1 -0
  219. package/dist/src/reasoning-loss.js +102 -0
  220. package/dist/src/reasoning-loss.js.map +1 -0
  221. package/dist/src/send-tools.d.ts +3 -2
  222. package/dist/src/send-tools.d.ts.map +1 -1
  223. package/dist/src/send-tools.js +3 -2
  224. package/dist/src/send-tools.js.map +1 -1
  225. package/dist/src/sent-rows.js +1 -1
  226. package/dist/src/silent-heartbeat.d.ts +1 -53
  227. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  228. package/dist/src/silent-heartbeat.js +56 -3
  229. package/dist/src/silent-heartbeat.js.map +1 -1
  230. package/dist/src/supplied-text.d.ts +35 -0
  231. package/dist/src/supplied-text.d.ts.map +1 -0
  232. package/dist/src/supplied-text.js +141 -0
  233. package/dist/src/supplied-text.js.map +1 -0
  234. package/dist/src/thinking-drops.d.ts +4 -11
  235. package/dist/src/thinking-drops.d.ts.map +1 -1
  236. package/dist/src/thinking-drops.js +7 -14
  237. package/dist/src/thinking-drops.js.map +1 -1
  238. package/dist/src/tool-args.d.ts +12 -0
  239. package/dist/src/tool-args.d.ts.map +1 -1
  240. package/dist/src/tool-args.js +39 -4
  241. package/dist/src/tool-args.js.map +1 -1
  242. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  243. package/dist/src/tool-image-ledger.js +2 -1
  244. package/dist/src/tool-image-ledger.js.map +1 -1
  245. package/dist/src/tool-presentation.d.ts.map +1 -1
  246. package/dist/src/tool-presentation.js +5 -4
  247. package/dist/src/tool-presentation.js.map +1 -1
  248. package/dist/src/tool-result-guard.d.ts +8 -4
  249. package/dist/src/tool-result-guard.d.ts.map +1 -1
  250. package/dist/src/tool-result-guard.js +23 -11
  251. package/dist/src/tool-result-guard.js.map +1 -1
  252. package/dist/src/tune-out/coordinator.d.ts +3 -2
  253. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  254. package/dist/src/tune-out/coordinator.js +24 -26
  255. package/dist/src/tune-out/coordinator.js.map +1 -1
  256. package/dist/src/tune-out/tools.d.ts +4 -1
  257. package/dist/src/tune-out/tools.d.ts.map +1 -1
  258. package/dist/src/types/agent.d.ts +63 -18
  259. package/dist/src/types/agent.d.ts.map +1 -1
  260. package/dist/src/types/events.d.ts +18 -8
  261. package/dist/src/types/events.d.ts.map +1 -1
  262. package/dist/src/types/framework.d.ts +58 -0
  263. package/dist/src/types/framework.d.ts.map +1 -1
  264. package/dist/src/types/module.d.ts +27 -13
  265. package/dist/src/types/module.d.ts.map +1 -1
  266. package/dist/src/types/trace.d.ts +34 -1
  267. package/dist/src/types/trace.d.ts.map +1 -1
  268. package/package.json +3 -3
  269. package/src/agent.ts +76 -12
  270. package/src/api/mcp-server-core.ts +3 -14
  271. package/src/api/server.ts +11 -5
  272. package/src/auto-continue.ts +12 -5
  273. package/src/code-execution/isolate/runner.ts +7 -5
  274. package/src/code-execution/isolate/scripts.ts +3 -2
  275. package/src/cut-off-round.ts +98 -0
  276. package/src/framework.ts +3772 -1138
  277. package/src/gate/event-gate.ts +705 -60
  278. package/src/gate/index.ts +9 -1
  279. package/src/gate/reply-judge.ts +512 -0
  280. package/src/gate/types.ts +176 -3
  281. package/src/heartbeat/heartbeat.ts +843 -0
  282. package/src/heartbeat/reminder-time.ts +107 -0
  283. package/src/held-writes.ts +3 -3
  284. package/src/host-presentation.ts +605 -0
  285. package/src/index.ts +5 -1
  286. package/src/line-reader.ts +58 -0
  287. package/src/mcpl/capability-mask.ts +3 -2
  288. package/src/mcpl/channel-registry.ts +139 -53
  289. package/src/mcpl/conversation-router.ts +2 -1
  290. package/src/mcpl/convert-block.ts +17 -4
  291. package/src/mcpl/hook-orchestrator.ts +4 -3
  292. package/src/mcpl/inbound-source.ts +45 -70
  293. package/src/mcpl/modern-connection.ts +11 -7
  294. package/src/mcpl/protocol-family.ts +24 -3
  295. package/src/mcpl/push-handler.ts +2 -2
  296. package/src/mcpl/references.ts +31 -23
  297. package/src/mcpl/server-connection.ts +76 -34
  298. package/src/mcpl/skip-reply-wake.ts +35 -0
  299. package/src/mcpl/tags.ts +13 -2
  300. package/src/mcpl/tool-presence.ts +149 -82
  301. package/src/mcpl/tool-signature.ts +64 -32
  302. package/src/mcpl/toolset-host.ts +212 -110
  303. package/src/mcpl/toolsets-module.ts +277 -91
  304. package/src/mcpl/transport.ts +18 -14
  305. package/src/mcpl/types.ts +32 -13
  306. package/src/module-registry.ts +14 -9
  307. package/src/modules/api/index.ts +4 -2
  308. package/src/modules/discord/index.ts +37 -25
  309. package/src/modules/history/index.ts +30 -18
  310. package/src/modules/sender-name.ts +9 -0
  311. package/src/modules/workspace/answers.ts +151 -0
  312. package/src/modules/workspace/branch-intent.ts +10 -5
  313. package/src/modules/workspace/direct-write.ts +59 -23
  314. package/src/modules/workspace/disk-agreement.ts +52 -21
  315. package/src/modules/workspace/edit-text.ts +7 -5
  316. package/src/modules/workspace/effects.ts +69 -50
  317. package/src/modules/workspace/index.ts +966 -556
  318. package/src/modules/workspace/observe.ts +497 -242
  319. package/src/modules/workspace/read-page.ts +7 -1
  320. package/src/modules/workspace/reconcile.ts +184 -49
  321. package/src/modules/workspace/result-text.ts +2 -0
  322. package/src/modules/workspace/sync.ts +16 -0
  323. package/src/modules/workspace/types.ts +96 -43
  324. package/src/modules/workspace/watcher.ts +8 -3
  325. package/src/notice-tags.ts +84 -0
  326. package/src/prose-drafts.ts +4 -3
  327. package/src/reasoning-loss-notice.ts +64 -0
  328. package/src/reasoning-loss.ts +120 -0
  329. package/src/send-tools.ts +3 -2
  330. package/src/sent-rows.ts +1 -1
  331. package/src/silent-heartbeat.ts +4 -3
  332. package/src/supplied-text.ts +154 -0
  333. package/src/thinking-drops.ts +10 -16
  334. package/src/tool-args.ts +38 -4
  335. package/src/tool-image-ledger.ts +2 -1
  336. package/src/tool-presentation.ts +5 -4
  337. package/src/tool-result-guard.ts +26 -11
  338. package/src/tune-out/coordinator.ts +25 -27
  339. package/src/tune-out/tools.ts +4 -1
  340. package/src/types/agent.ts +63 -18
  341. package/src/types/events.ts +18 -8
  342. package/src/types/framework.ts +59 -0
  343. package/src/types/module.ts +28 -13
  344. package/src/types/trace.ts +36 -1
package/src/agent.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { NOTICE_TAG } from './notice-tags.js';
2
+ import { heartbeatToolAlias } from './heartbeat/heartbeat.js';
1
3
  import type { Membrane, NormalizedMessage, NormalizedRequest, ContentBlock, ThinkingBindingEntry, YieldingStream } from '@animalabs/membrane';
2
4
  import { isAbortedResponse } from '@animalabs/membrane';
3
5
  import type { CacheWireReceipt, KvUnifiedRequestHooks } from './kv-unified-wire.js';
@@ -97,10 +99,15 @@ export interface StartStreamResult {
97
99
  /** The compile's `thinkingRecordedAfter`, when it gave one: a reply stored at or below it predates the branch's
98
100
  * thinking-binding record, so the provider may drop its thinking with no strip having missed. */
99
101
  thinkingRecordedAfter?: number;
102
+ /** The key (`branchKey`) of the branch the compile read, when its provenance names one. */
103
+ thinkingBranch?: string;
100
104
  /** Tells the context manager what the stream's later rounds carried after the compiled messages, every round so
101
105
  * far in sent order, with this request's head (context-manager's `noteRequestSent`). Present only where the
102
106
  * model binds its thinking and the context manager keeps a record it can be told of. */
103
107
  noteRequestSent?: (rows: readonly SentRow[]) => boolean;
108
+ /** The stored bodies the request's compile carried, by each body's first message id (its provenance's raw bodies).
109
+ * Absent when the compile reports no provenance. */
110
+ carriedBodies?: ReadonlySet<MessageId>;
104
111
  }
105
112
  import type {
106
113
  ContextManager,
@@ -112,6 +119,7 @@ import type {
112
119
  MessageId,
113
120
  MessageMetadata,
114
121
  RequestIdentity,
122
+ ThinkingStrip,
115
123
  } from '@animalabs/context-manager';
116
124
  import type {
117
125
  AgentConfig,
@@ -257,8 +265,7 @@ export function resolveAutoContinue(config: AgentConfig): { maxConsecutive: numb
257
265
  return { maxConsecutive: max };
258
266
  }
259
267
 
260
- /** A branch's key in Agent.unsentSince: its id and when it was created. */
261
- function branchKey(branch: { readonly id: string; readonly created: number }): string {
268
+ export function branchKey(branch: { readonly id: string; readonly created: number }): string {
262
269
  return `${branch.id}\u0000${branch.created}`;
263
270
  }
264
271
 
@@ -493,7 +500,13 @@ export class Agent {
493
500
  if (this.allowedTools === 'all') {
494
501
  return true;
495
502
  }
496
- return this.allowedTools.includes(toolName);
503
+ if (this.allowedTools.includes(toolName)) return true;
504
+ // A heartbeat tool is one tool under either prefix: the framework's
505
+ // `heartbeat--x` and heartbeat-mcpl's `mcpl--heartbeat--x`, which an
506
+ // allowedTools written for the server names. Only one of them is ever
507
+ // offered, since the framework refuses the server beside its heartbeat.
508
+ const alias = heartbeatToolAlias(toolName);
509
+ return alias !== undefined && this.allowedTools.includes(alias);
497
510
  }
498
511
 
499
512
  /**
@@ -1176,8 +1189,9 @@ export class Agent {
1176
1189
  // Tolerates partial context-manager doubles (tests build Agents over
1177
1190
  // stubs). Without a watermark, a reader that looks for one takes its own
1178
1191
  // side: the coalescer counts the message as read, and the framework's
1179
- // unreadNoticesForPrimary counts it as unread. A deferred write's wake
1180
- // reads the answered watermark instead (deferredWriteDone).
1192
+ // unreadNoticesForPrimary counts it as unread. A wake for a deferred
1193
+ // write or a stored message reads the answered watermark instead
1194
+ // (deferredWriteDone, storedMessageDone).
1181
1195
  this.consumedWatermark = this.pendingWatermark();
1182
1196
  this.watermarkMarks++;
1183
1197
  this.shownWatermark = this.consumedWatermark;
@@ -1225,6 +1239,21 @@ export class Agent {
1225
1239
  this.watermarkMarks++;
1226
1240
  }
1227
1241
 
1242
+ /**
1243
+ * The branch now (`branchKey`) and its head, as `pendingWatermark` reads it: a message stored on that branch later
1244
+ * has a larger sequence. Sequences are numbered per branch, so one compares with another only on the same branch.
1245
+ */
1246
+ branchHead(): { branch: string; sequence: number } | undefined {
1247
+ const head = this.pendingWatermark();
1248
+ const cm = this.contextManager as Partial<ContextManager>;
1249
+ if (!head || typeof cm.currentBranchRef !== 'function') return undefined;
1250
+ try {
1251
+ return { branch: branchKey(cm.currentBranchRef()), sequence: head.sequence };
1252
+ } catch {
1253
+ return undefined;
1254
+ }
1255
+ }
1256
+
1228
1257
  /** The watermark a successful compile will establish (taken before, applied after). */
1229
1258
  private pendingWatermark(): { branch: string; sequence: number } | null {
1230
1259
  const cm = this.contextManager as Partial<ContextManager>;
@@ -1313,6 +1342,15 @@ export class Agent {
1313
1342
  */
1314
1343
  onCompiledToSend?: (messages: readonly unknown[]) => void;
1315
1344
 
1345
+ /**
1346
+ * A host's hook, called with each committing compile's `thinkingStrip` (never a dry run's) and the key of the
1347
+ * branch it read (`branchKey`), as the compile returns, whether or not its request is then sent, since the
1348
+ * context manager's binding record takes the strip either way. One that throws is logged. The framework sets it
1349
+ * at the agent's first stream unless a host has set one, so a host's own observer stands in for the framework's,
1350
+ * whose `[reasoning]` line for the agent then isn't told.
1351
+ */
1352
+ onThinkingStrip?: (strip: ThinkingStrip, branch: string) => void;
1353
+
1316
1354
  /**
1317
1355
  * The newest content of `namespace` among this agent's rows stored since
1318
1356
  * its last compile built to send (unsentSince), and whether every part of
@@ -1516,6 +1554,13 @@ export class Agent {
1516
1554
  throw error;
1517
1555
  }
1518
1556
  if (watermark) this.applyCompileWatermark(watermark, marksBefore);
1557
+ if (!opts?.dryRun && result.thinkingStrip && this.onThinkingStrip) {
1558
+ try {
1559
+ this.onThinkingStrip(result.thinkingStrip, result.provenance ? branchKey(result.provenance.branch) : '');
1560
+ } catch (err) {
1561
+ console.error(`[agent] ${this.name}: the host's thinking-strip observer failed:`, err);
1562
+ }
1563
+ }
1519
1564
  return result;
1520
1565
  }
1521
1566
 
@@ -2064,10 +2109,10 @@ export class Agent {
2064
2109
  // ran. The stream driver collects it on that stream's terminal event, or
2065
2110
  // at the loop's end when the iterator closed without one, while the
2066
2111
  // stream still owns the agent. This clear is for the records no driver
2067
- // collects: a dropped turn's, whose stream no driver iterates; one whose
2068
- // stream's terminal came after another stream took the agent; or a
2069
- // cancel after the stream's complete event. A fresh stream must not
2070
- // inherit one and read its own later error as a deliberate stop.
2112
+ // collects: a cancel of a stream no driver iterates; one whose stream's
2113
+ // terminal came after another stream took the agent; or a cancel after
2114
+ // the stream's complete event. A fresh stream must not inherit one and
2115
+ // read its own later error as a deliberate stop.
2071
2116
  this._pendingCancel = undefined;
2072
2117
  this._inferenceStartedAt = Date.now();
2073
2118
  this.lastStreamInputTokens = 0;
@@ -2146,7 +2191,11 @@ export class Agent {
2146
2191
  // Read against the messages the stream sends, which membrane's report indexes (RoundReport's coordinates).
2147
2192
  placeDrops: (entries) => placeThinkingDrops(entries, request.messages, compiled),
2148
2193
  ...this.requestNotes(binding, compiled),
2194
+ ...(compiled.provenance
2195
+ ? { carriedBodies: new Set(compiled.provenance.messages.flatMap((m) => m.kind === 'raw' ? m.bodies.map((b) => b.messageId) : [])) }
2196
+ : {}),
2149
2197
  ...(compiled.recordedAfter !== undefined ? { thinkingRecordedAfter: compiled.recordedAfter } : {}),
2198
+ ...(compiled.provenance ? { thinkingBranch: branchKey(compiled.provenance.branch) } : {}),
2150
2199
  ...(kvEnabled
2151
2200
  ? {
2152
2201
  takeKvSubmission: () => kvQueue.shift(),
@@ -2261,12 +2310,24 @@ export class Agent {
2261
2310
  * one is still ahead; otherwise the next start drops it. `reason` is
2262
2311
  * provenance for the framework's inference:aborted trace and marker
2263
2312
  * metadata (e.g. 'zombie_reclaim', 'subagent_cancel'); it never reaches
2264
- * the provider.
2313
+ * the provider. When the stream's cancel() throws, the throw goes on to
2314
+ * the caller, and the agent still holds the stream, its cancel record as
2315
+ * it was before the call.
2265
2316
  */
2266
2317
  cancelStream(reason?: string): void {
2267
2318
  if ('stream' in this._state && this._state.stream) {
2319
+ // A cancel that throws leaves the stream going and the agent holding
2320
+ // it, so this call's record is taken back, leaving any earlier one: a
2321
+ // later end of that stream, an `error` the provider reports included,
2322
+ // is not read as this stop's.
2323
+ const earlier = this._pendingCancel;
2268
2324
  this._pendingCancel = { reason };
2269
- this._state.stream.cancel();
2325
+ try {
2326
+ this._state.stream.cancel();
2327
+ } catch (error) {
2328
+ this._pendingCancel = earlier;
2329
+ throw error;
2330
+ }
2270
2331
  } else {
2271
2332
  this._stopWithoutStream = { reason };
2272
2333
  }
@@ -2338,7 +2399,9 @@ export class Agent {
2338
2399
  }
2339
2400
 
2340
2401
  const injectedText = systemInjections.map((block) =>
2341
- block.type === 'text' ? block.text : `[${block.type} block not shown: the system prompt carries text only.]`);
2402
+ block.type === 'text'
2403
+ ? block.text
2404
+ : `[${NOTICE_TAG.blockNotShown}: ${/^[aeiou]/i.test(block.type) ? 'an' : 'a'} ${block.type} block; the system prompt carries text only.]`);
2342
2405
 
2343
2406
  return this.systemPrompt + '\n' + injectedText.join('\n');
2344
2407
  }
@@ -2442,6 +2505,7 @@ export class Agent {
2442
2505
  id: block.id,
2443
2506
  name: block.name,
2444
2507
  input: block.input,
2508
+ ...(block.unparseableInput !== undefined ? { unparseableInput: block.unparseableInput } : {}),
2445
2509
  });
2446
2510
  } else if (block.type === 'text') {
2447
2511
  speechContent.push(block);
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import WebSocket from 'ws';
15
- import { createInterface } from 'node:readline';
15
+ import { escapeLineSeparators, readLines } from '../line-reader.js';
16
16
  import type { ApiRequest, ApiResponse, ApiEvent } from './types.js';
17
17
 
18
18
  // ============================================================================
@@ -86,18 +86,7 @@ export class McpServer {
86
86
  // Connect to WebSocket API
87
87
  await this.connectWs();
88
88
 
89
- // Set up stdio interface
90
- const rl = createInterface({
91
- input: process.stdin,
92
- output: process.stdout,
93
- terminal: false,
94
- });
95
-
96
- rl.on('line', (line) => {
97
- this.handleInput(line);
98
- });
99
-
100
- rl.on('close', () => {
89
+ readLines(process.stdin, (line) => this.handleInput(line), () => {
101
90
  this.close();
102
91
  process.exit(0);
103
92
  });
@@ -667,7 +656,7 @@ export class McpServer {
667
656
  }
668
657
 
669
658
  private send(message: JsonRpcResponse | JsonRpcNotification): void {
670
- process.stdout.write(JSON.stringify(message) + '\n');
659
+ process.stdout.write(escapeLineSeparators(JSON.stringify(message)) + '\n');
671
660
  }
672
661
 
673
662
  close(): void {
package/src/api/server.ts CHANGED
@@ -489,9 +489,13 @@ export class ApiServer {
489
489
  if (!params.participant || !params.content) {
490
490
  throw new Error('participant and content are required');
491
491
  }
492
- // ApiModule stores the message as one text block, which context-manager
493
- // refuses unless its text is a string, so content that isn't text is
494
- // refused here, where the caller hears it, rather than at dispatch.
492
+ // ApiModule stores the participant as a name, and the message as one text
493
+ // block, which context-manager refuses unless its text is a string. So a
494
+ // participant or content that isn't text is refused here, where the
495
+ // caller hears it, rather than at dispatch.
496
+ if (typeof params.participant !== 'string') {
497
+ throw new Error('participant must be text');
498
+ }
495
499
  if (typeof params.content !== 'string') {
496
500
  throw new Error('content must be text');
497
501
  }
@@ -649,7 +653,8 @@ export class ApiServer {
649
653
  throw new Error('agentName is required');
650
654
  }
651
655
 
652
- const result = this.framework.undoLastTurn(params.agentName);
656
+ const { settled, ...result } = this.framework.undoLastTurn(params.agentName);
657
+ await settled;
653
658
 
654
659
  if (result.undone) {
655
660
  this.broadcast('branch:switched', {
@@ -667,7 +672,8 @@ export class ApiServer {
667
672
  throw new Error('agentName is required');
668
673
  }
669
674
 
670
- const result = this.framework.redo(params.agentName);
675
+ const { settled, ...result } = this.framework.redo(params.agentName);
676
+ await settled;
671
677
 
672
678
  if (result.redone) {
673
679
  this.broadcast('branch:switched', {
@@ -5,6 +5,8 @@
5
5
  * row the next turn reads.
6
6
  */
7
7
 
8
+ import { NOTICE_TAG } from './notice-tags.js';
9
+
8
10
  /** The reason on a continuation's wake (InferenceRequest.reason). */
9
11
  export const AUTO_CONTINUE_REASON = 'auto-continue';
10
12
 
@@ -21,8 +23,9 @@ export const AUTO_CONTINUE_STOP_REASONS: ReadonlySet<string> = new Set(['end_tur
21
23
  * one shared message slot), says another turn starts, says how to stop
22
24
  * (`stop`, an imperative such as "call skip_reply") and that anything written
23
25
  * without it is sent as usual, and, on the last continuation of a run, that
24
- * the agent waits for the next event after it. When the last turn's closing
25
- * words were held because that turn had already sent, it says so. Short,
26
+ * the agent waits for the next event after it. When some or all of the last
27
+ * turn's closing words were held because a send that turn may already have
28
+ * posted where they were going, it says so. Short,
26
29
  * neutral and event-shaped on purpose: it rides as a system row in the
27
30
  * window, where instruction blocks about output have tripped classifiers,
28
31
  * and wording that reads as a lapse or a budget invites a published reply.
@@ -32,11 +35,15 @@ export function autoContinueNotice(opts: {
32
35
  stop: string;
33
36
  last: boolean;
34
37
  heldAfterSend?: boolean;
38
+ /** The run began with a heartbeat's turn, whose plain speech is held as drafts. */
39
+ heartbeat?: boolean;
35
40
  }): string {
36
- return `[continue] ${opts.agentName}, another turn starts now.` +
41
+ return `[${NOTICE_TAG.continue}] ${opts.agentName}, another turn starts now.` +
37
42
  (opts.heldAfterSend
38
- ? ' Your closing words last turn were held, not sent: that turn had already sent with a send tool.'
43
+ ? ' Some or all of your closing words last turn were held, not sent: a send that turn may already have posted where they were going.'
39
44
  : '') +
40
- ` If you're done, ${opts.stop}; anything you write without it is sent as usual.` +
45
+ (opts.heartbeat
46
+ ? ` If you're done, ${opts.stop}; plain words you write without it and with no destination are held as drafts, as on the heartbeat's turn, and an explicit send reaches a channel.`
47
+ : ` If you're done, ${opts.stop}; anything you write without it is sent as usual.`) +
41
48
  (opts.last ? ' This is the last continuation in a row: after this turn you wait for the next event either way.' : '');
42
49
  }
@@ -4,11 +4,13 @@
4
4
  * which ends with it, so nothing a script held stays after it.
5
5
  *
6
6
  * Every frame from the child is decoded and checked before the parent acts on
7
- * it, since the child is untrusted. A frame that breaks the protocol, a child
8
- * that exits without saying how the script ended, or one that doesn't stop
9
- * when asked, ends the run as crashed. A tool call goes to `onToolCall`, the
10
- * bridge to the framework's dispatcher, and its outcome goes into the run's
11
- * call record at dispatch, as running until it's answered.
7
+ * it, since the child is untrusted. A frame that breaks the protocol, or a
8
+ * child that exits without saying how the script ended when no stop was
9
+ * asked, ends the run as crashed. One that doesn't stop when asked is ended
10
+ * once the stop's grace has passed, and the run ends stopped, marked short.
11
+ * A tool call goes to `onToolCall`, the bridge to the framework's dispatcher,
12
+ * and its outcome goes into the run's call record at dispatch, as running
13
+ * until it's answered.
12
14
  *
13
15
  * What the host holds for a script is bounded: at most MAX_CALLS_IN_FLIGHT of
14
16
  * its calls wait for answers at once, and at most MAX_CALLS_IN_FLIGHT are out
@@ -26,6 +26,7 @@
26
26
  * dispatcher a script's calls go through, and how to wake an agent.
27
27
  */
28
28
 
29
+ import { NOTICE_TAG } from '../../notice-tags.js';
29
30
  import { ChildCapacity, ScriptRun, type CallAnswer, type Ending, type ScriptRunOptions, type ToolAnswer } from './runner.js';
30
31
  import type { CallSummary } from './calls.js';
31
32
  import type { OutputBounds, OutputWindow } from './output.js';
@@ -478,10 +479,10 @@ function leftEndingText(scriptId: string, ending: Ending): string {
478
479
  : ending.how === 'stopped'
479
480
  ? ending.why ? `was stopped: ${ending.why}` : 'was stopped at its limit'
480
481
  : 'ended';
481
- return `[script ${scriptId}] Your script ${how}. {"action": "wait", "script_id": "${scriptId}"} gives its result.`;
482
+ return `[${NOTICE_TAG.script} ${scriptId}] Your script ${how}. {"action": "wait", "script_id": "${scriptId}"} gives its result.`;
482
483
  }
483
484
 
484
485
  function wakeText(scriptId: string, payloadJson: string, n: number, max: number, ageMs: number): string {
485
486
  const minutes = Math.round(ageMs / 60_000);
486
- return `[script ${scriptId}] Woke you with wake_agent(), ${minutes} min after it started (wake ${n} of ${max}): ${payloadJson}`;
487
+ return `[${NOTICE_TAG.script} ${scriptId}] Woke you with wake_agent(), ${minutes} min after it started (wake ${n} of ${max}): ${payloadJson}`;
487
488
  }
@@ -0,0 +1,98 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { ContentBlock, NormalizedResponse } from '@animalabs/membrane';
3
+
4
+ export interface CutOffCall {
5
+ id: string;
6
+ name: string;
7
+ input: Record<string, unknown>;
8
+ unparseableInput?: string;
9
+ }
10
+
11
+ export interface CutOffRound {
12
+ content: ContentBlock[];
13
+ start: number;
14
+ calls: CutOffCall[];
15
+ unnamed: boolean;
16
+ xml: boolean;
17
+ }
18
+
19
+ /**
20
+ * The last round of a response that ended at the output limit, when it holds
21
+ * calls that never ran, or, in XML tool mode, a tool-call block cut off before
22
+ * any call in it was named (`unnamed`). Membrane yields a round's calls only
23
+ * at its tool_use stop, or in XML tool mode at its `</function_calls>` stop,
24
+ * and the round begins after the last result that answers one of the
25
+ * response's calls.
26
+ */
27
+ export function cutOffRound(response: NormalizedResponse): CutOffRound | undefined {
28
+ if (response.stopReason !== 'max_tokens') return undefined;
29
+ const callIds = new Set<string>();
30
+ let start = 0;
31
+ response.content.forEach((block, index) => {
32
+ if (block.type === 'tool_use') callIds.add(block.id);
33
+ else if (block.type === 'tool_result' && callIds.has(block.toolUseId)) start = index + 1;
34
+ });
35
+ const cut = response.details?.stop?.unclosedToolBlock ? cutToolBlock(response.content, start) : undefined;
36
+ const content = cut ?? response.content;
37
+ const uses = content.slice(start).filter((block): block is ContentBlock & { type: 'tool_use' } => block.type === 'tool_use');
38
+ const calls = uses.map(({ id, name, input, unparseableInput }) => ({
39
+ id, name, input,
40
+ ...(unparseableInput !== undefined ? { unparseableInput } : {}),
41
+ }));
42
+ const unnamed = !!cut && calls.length === 0;
43
+ const xml = !!cut || uses.some((block) => (block as { rawXml?: string }).rawXml !== undefined);
44
+ return calls.length > 0 || unnamed ? { content, start, calls, unnamed, xml } : undefined;
45
+ }
46
+
47
+ /**
48
+ * Membrane 0.8.0's grammar (unchanged from 0.7.2) for the anthropic-xml
49
+ * formatter's XML tool mode. Membrane exports two of its predicates,
50
+ * hasUnclosedToolBlock and endsWithPartialToolBlock, but not the grammar
51
+ * itself (dist/utils/tool-parser.js, INVOKE_REGEX): a block opens
52
+ * with `<function_calls>` and each call with `<invoke name="…">`, either tag
53
+ * optionally `antml:` prefixed, and a closer ends the innermost block still
54
+ * open. Its parser takes spans out of an unclosed block's text, which leaves
55
+ * that text in several text blocks: thinking, which stays with the round, a
56
+ * whole inner block its stop sequence didn't end, whose calls are the round's
57
+ * too, and results, which don't stay. Only a block's opener
58
+ * and each call's name head are read, nothing after a name, and a name that
59
+ * isn't a tool name's characters isn't taken as one: the formatter writes a
60
+ * stored call's name into the next request as it is.
61
+ */
62
+ const OPENER = /<(antml:)?function_calls(>|[^>]*$)/g;
63
+ const CLOSER = /<\/(antml:)?function_calls>/g;
64
+ const NAME_HEAD = /<(antml:)?invoke\s+name=(["'])((?:(?!\2).)+)\2\s*\/?>/g;
65
+ const TOOL_NAME = /^[A-Za-z0-9_.:-]{1,128}$/;
66
+
67
+ function cutToolBlock(content: ContentBlock[], start: number): ContentBlock[] | undefined {
68
+ const texts = content.map((block, index) => ({ block, index }))
69
+ .filter((entry): entry is { block: ContentBlock & { type: 'text' }; index: number } => entry.index >= start && entry.block.type === 'text');
70
+ const joined = texts.map(({ block }) => block.text).join('');
71
+ const tags = [
72
+ ...[...joined.matchAll(OPENER)].map((match) => ({ index: match.index, opens: true })),
73
+ ...[...joined.matchAll(CLOSER)].map((match) => ({ index: match.index, opens: false })),
74
+ ].sort((a, b) => a.index - b.index);
75
+ const open: number[] = [];
76
+ for (const tag of tags) {
77
+ if (tag.opens) open.push(tag.index);
78
+ else open.pop();
79
+ }
80
+ if (open.length === 0) return undefined;
81
+ const opener = open[0]!;
82
+ let at = opener;
83
+ const holder = texts.find(({ block }) => {
84
+ if (at < block.text.length) return true;
85
+ at -= block.text.length;
86
+ return false;
87
+ })!;
88
+ const blockText = joined.slice(opener);
89
+ const heads = [...blockText.matchAll(NAME_HEAD)].filter((head) => TOOL_NAME.test(head[3]!));
90
+ const calls: ContentBlock[] = heads.map((head, index) => ({
91
+ type: 'tool_use', id: `tool_${randomUUID()}`, name: head[3]!, input: {},
92
+ unparseableInput: blockText.slice(head.index, heads[index + 1]?.index),
93
+ }));
94
+ const prose: ContentBlock[] = at > 0 ? [{ type: 'text', text: holder.block.text.slice(0, at) }] : [];
95
+ const kept = content.slice(holder.index + 1)
96
+ .filter((block) => block.type === 'thinking' || block.type === 'redacted_thinking' || block.type === 'tool_use');
97
+ return [...content.slice(0, holder.index), ...prose, ...kept, ...calls];
98
+ }