@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
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Text the framework didn't write (what a participant or a server supplied),
3
+ * put beside structure of the framework's own: the reply judge's document,
4
+ * and notices the framework writes. A value that can end its line can start
5
+ * a line that reads as the framework's own.
6
+ */
7
+
8
+ import { CONTEXT_MANAGER_TAGS, MEMBRANE_TAGS, NOTICE_TAG } from './notice-tags.js';
9
+
10
+ /** Every form of line break a reader might take for one: CR LF, CR, LF,
11
+ * vertical tab, form feed, NEL, and the Unicode line and paragraph separators. */
12
+ const LINE_BREAK = /\r\n|[\r\n\u000b\u000c\u0085\u2028\u2029]/g;
13
+
14
+ /** Text with each line break, in whatever form, as `\n`. */
15
+ export function normalizeLineBreaks(text: string): string {
16
+ return text.replace(LINE_BREAK, '\n');
17
+ }
18
+
19
+ /** A value kept to one line: each run of line breaks, in whatever form, as one
20
+ * space, so it can't end the line it's put in or start one of its own. */
21
+ export function oneLine(text: string): string {
22
+ return normalizeLineBreaks(text).replace(/\n+/g, ' ');
23
+ }
24
+
25
+ const INVISIBLE = '\\p{Cf}\\p{Default_Ignorable_Code_Point}';
26
+ const SKIPPED = `[\\s\\p{Cc}${INVISIBLE}]`;
27
+ const tagWord = (word: string): string =>
28
+ [...word].map((c) => (/[\\^$.*+?()[\]{}|/]/.test(c) ? `\\${c}` : c)).join(`${SKIPPED}*`);
29
+ const NOTICE_OPENING = new RegExp(
30
+ `\\[(?=${SKIPPED}*(?:${[...Object.values(NOTICE_TAG), ...CONTEXT_MANAGER_TAGS, ...MEMBRANE_TAGS].map((tag) => tag.split(' ').map(tagWord).join(`${SKIPPED}+`)).join('|')})(?![\\p{L}\\p{N}]))`,
31
+ 'giu',
32
+ );
33
+
34
+ /**
35
+ * Texts read as one string, joined by line breaks, with a backslash put
36
+ * before the bracket of each opening of a listed tag (NOTICE_TAG,
37
+ * CONTEXT_MANAGER_TAGS, MEMBRANE_TAGS) that hasn't one before it in its own
38
+ * text. A text `marks` says false of is read and never changed.
39
+ *
40
+ * Every opening, not only one that starts a line: membrane's XML formatter
41
+ * writes `participant: ` before each message, so the framework's own
42
+ * openings stand mid-line there too. It joins a message's text blocks with
43
+ * line breaks and sets its images apart from that text, while the native
44
+ * formatter sends each block on its own, so an opening split across a
45
+ * message's text blocks is one either way. In any case, by Unicode's case folding, and
46
+ * through whitespace, control and invisible characters anywhere inside the
47
+ * bracket, which a model reads past.
48
+ */
49
+ export function markedTexts(texts: readonly string[], marks: readonly boolean[] = []): string[] {
50
+ const starts: number[] = [];
51
+ let at = 0;
52
+ for (const text of texts) {
53
+ starts.push(at);
54
+ at += text.length + 1;
55
+ }
56
+ const offsets: number[][] = texts.map(() => []);
57
+ for (const match of texts.join('\n').matchAll(NOTICE_OPENING)) {
58
+ let holder = texts.length - 1;
59
+ while (starts[holder]! > match.index) holder--;
60
+ const offset = match.index - starts[holder]!;
61
+ if (marks[holder] !== false && texts[holder]![offset - 1] !== '\\') offsets[holder]!.push(offset);
62
+ }
63
+ return texts.map((text, i) => withMarks(text, offsets[i]!));
64
+ }
65
+
66
+ function withMarks(text: string, offsets: readonly number[]): string {
67
+ let marked = '';
68
+ let from = 0;
69
+ for (const offset of offsets) {
70
+ marked += `${text.slice(from, offset)}\\`;
71
+ from = offset;
72
+ }
73
+ return marked + text.slice(from);
74
+ }
75
+
76
+ function closedWithin(text: string, at: number): boolean {
77
+ let depth = 0;
78
+ for (let i = at; i < text.length; i++) {
79
+ if (text[i] === '[') depth++;
80
+ else if (text[i] === ']' && --depth === 0) return true;
81
+ }
82
+ return false;
83
+ }
84
+
85
+ function unmarkedOpenings(text: string): number[] {
86
+ return [...text.matchAll(NOTICE_OPENING)].map((match) => match.index).filter((at) => text[at - 1] !== '\\');
87
+ }
88
+
89
+ export function markedText(text: string): string {
90
+ return markedTexts([text])[0]!;
91
+ }
92
+
93
+ export function markNoticeOpenings<T extends object>(blocks: readonly T[], marks: (index: number) => boolean = () => true): T[] {
94
+ const at: number[] = [];
95
+ blocks.forEach((block, index) => {
96
+ const { type, text } = (block ?? {}) as { type?: unknown; text?: unknown };
97
+ if (type === 'text' && typeof text === 'string') at.push(index);
98
+ });
99
+ const textOf = (index: number): string => (blocks[index] as unknown as { text: string }).text;
100
+ const marked = markedTexts(at.map(textOf), at.map(marks));
101
+ const out = [...blocks];
102
+ at.forEach((index, i) => {
103
+ if (marked[i] !== textOf(index)) out[index] = { ...blocks[index]!, text: marked[i] };
104
+ });
105
+ return out;
106
+ }
107
+
108
+ export function suppliedValue(value: unknown): string {
109
+ return markedText(oneLine(String(value)));
110
+ }
111
+
112
+ export function cutToFit(text: string, length: number): string {
113
+ let end = Math.max(0, Math.floor(length));
114
+ if (end >= text.length) return text;
115
+ for (;;) {
116
+ const last = text.charCodeAt(end - 1);
117
+ if (end < text.length && last >= 0xd800 && last <= 0xdbff) end--;
118
+ else if (text[end - 1] === '\\' && text[end] === '[') end--;
119
+ else {
120
+ const cut = text.slice(0, end);
121
+ const open = unmarkedOpenings(cut).find((at) => !closedWithin(cut, at));
122
+ if (open === undefined) return cut;
123
+ end = open;
124
+ }
125
+ }
126
+ }
127
+
128
+ export function showsAsItself(value: string): boolean {
129
+ return value !== '' && suppliedValue(value) === value && value.isWellFormed() && !EDGE_OR_QUOTE.test(value);
130
+ }
131
+
132
+ const EDGE_OR_QUOTE = /^\s|\s$|["\\]/;
133
+
134
+ export function quotedValue(value: unknown, space?: number): string {
135
+ return (JSON.stringify(value, null, space) ?? 'null').replace(/"(?:[^"\\]|\\.)*"/g, (literal) => quotedString(JSON.parse(literal)));
136
+ }
137
+
138
+ function quotedString(text: string): string {
139
+ let quoted = '';
140
+ let from = 0;
141
+ for (const match of text.matchAll(NOTICE_OPENING)) {
142
+ quoted += `${jsonText(text.slice(from, match.index))}\\u005b`;
143
+ from = match.index + 1;
144
+ }
145
+ return `"${quoted}${jsonText(text.slice(from))}"`;
146
+ }
147
+
148
+ // JSON.stringify leaves NEL, U+2028 and U+2029 as they are.
149
+ const jsonText = (text: string): string =>
150
+ JSON.stringify(text).slice(1, -1).replace(/[\u0085\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
151
+
152
+ export function nameToTypeBack(name: string): string {
153
+ return showsAsItself(name) ? name : quotedValue(name);
154
+ }
@@ -29,10 +29,11 @@ export interface CompiledForRequest {
29
29
  recordedAfter?: number;
30
30
  }
31
31
 
32
- /** What a stream's driver reads a call's dropped blocks against (`StartStreamResult`'s two fields, together). */
32
+ /** What a stream's driver reads a call's dropped blocks against (`StartStreamResult`'s fields, together). */
33
33
  export interface StreamThinkingDrops {
34
34
  place: (entries: readonly ThinkingBindingEntry[]) => PlacedDrop[];
35
35
  recordedAfter?: number;
36
+ branch?: string;
36
37
  }
37
38
 
38
39
  /** A signed block's identity on the wire: a thinking block's signature, a redacted block's data. */
@@ -112,26 +113,19 @@ export function splitThinkingDrops(
112
113
  /** Binding failures the context manager's record doesn't track: the account's, not the conversation's. */
113
114
  const UNTRACKED_REASONS = new Set(['organization_binding_mismatch', 'end_user_binding_mismatch']);
114
115
 
115
- /**
116
- * When an agent's unexpected drops call for an operator alert: at the first call that shows any, and again only
117
- * when a call shows more than the alert last counted. Each call of a stream, and each stream until a strip takes
118
- * them out, drops the same blocks again, so an unchanged count isn't news. A call that shows no unexpected drop, its
119
- * report empty, absent or naming only expected ones, ends the incident: the blocks it counted no longer drop. The next
120
- * one alerts afresh. It's held in memory, so a restart mid-incident alerts once more, and an agent's removal ends its
121
- * incident (`forget`).
122
- */
123
116
  export class ThinkingLossIncidents {
124
- private readonly alerted = new Map<string, number>();
117
+ private readonly alerted = new Map<string, { count: number; scope: string | undefined }>();
125
118
 
126
- /** A call's report of `unexpected` drops for `agent`: the count to alert with, or undefined for no alert. */
127
- observe(agent: string, unexpected: number): number | undefined {
128
- if (unexpected === 0) {
119
+ /** A call's report of `count` drops for `agent` in `scope`: the count to alert with, or undefined for no alert. */
120
+ observe(agent: string, count: number, scope?: string): number | undefined {
121
+ const open = this.alerted.get(agent);
122
+ if (count === 0) {
129
123
  this.alerted.delete(agent);
130
124
  return undefined;
131
125
  }
132
- if (unexpected <= (this.alerted.get(agent) ?? 0)) return undefined;
133
- this.alerted.set(agent, unexpected);
134
- return unexpected;
126
+ if (open && open.scope === scope && count <= open.count) return undefined;
127
+ this.alerted.set(agent, { count, scope });
128
+ return count;
135
129
  }
136
130
 
137
131
  /** The agent is gone: its incident, if one is open, ends with it. */
package/src/tool-args.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { nameToTypeBack, quotedValue } from './supplied-text.js';
1
2
  import type { ToolDefinition } from './types/index.js';
2
3
 
3
4
  /** A run of 16 or more digits, which membrane's XML parser passes as text for
@@ -67,6 +68,39 @@ function shown(value: unknown): string {
67
68
  return head;
68
69
  }
69
70
 
71
+ function notAnObject(input: unknown, field = 'input'): string {
72
+ return `its ${field} must be an object, and this call gave ${withArticle(kindOf(input))}`;
73
+ }
74
+
75
+ export function markedInputRefusal(call: { name: string; unparseableInput?: string }): string | undefined {
76
+ const text = call.unparseableInput;
77
+ if (text === undefined) return undefined;
78
+ // When a stream ends before a call's block stops, membrane marks the call
79
+ // with what had arrived of its arguments: nothing, a part, or a whole
80
+ // object. A block still open when a stream breaks the API's block order
81
+ // is marked the same way, and one that had stopped with its object's JSON,
82
+ // so the arguments may be another call's (ToolUseContent.unparseableInput).
83
+ if (/^[ \t\n\r]*$/.test(text)) return `Nothing was done: ${nameToTypeBack(call.name)} its arguments never arrived; call it again with them whole.`;
84
+ let parsed: unknown;
85
+ try {
86
+ parsed = JSON.parse(text);
87
+ } catch {
88
+ return `Nothing was done: ${nameToTypeBack(call.name)} its arguments must be one JSON object, and this call gave text that isn't JSON, which may have been cut off: ${shown(text)}; call it again with one JSON object.`;
89
+ }
90
+ if (parsed === null) return undefined;
91
+ if (typeof parsed !== 'object' || Array.isArray(parsed)) return `Nothing was done: ${nameToTypeBack(call.name)} ${notAnObject(parsed)}.`;
92
+ return `Nothing was done: ${nameToTypeBack(call.name)} its arguments came in a response that didn't arrive whole and in order, so they can't be taken as this call's; call it again.`;
93
+ }
94
+
95
+ export function cutOffRefusal(call: { name: string }): string {
96
+ return `Nothing was done: ${nameToTypeBack(call.name)} didn't run, because the response it was in was cut off at the output limit. ` +
97
+ 'To make the call, send less in one response: shorter text before it, shorter arguments, or fewer calls at once.';
98
+ }
99
+
100
+ export function asCalled<T extends { name: string; input?: unknown; unparseableInput?: string }>(call: T): T {
101
+ return call.unparseableInput !== undefined && markedInputRefusal(call) === undefined ? { ...call, input: {} } : call;
102
+ }
103
+
70
104
  /** A list in prose: `a`, `a or b`, `a, b or c`. */
71
105
  function oneOf(items: string[]): string {
72
106
  return items.length <= 1 ? items.join('') : `${items.slice(0, -1).join(', ')} or ${items[items.length - 1]}`;
@@ -169,7 +203,7 @@ export function toolArgsProblems(
169
203
  if (!schema) return [];
170
204
  const absent = input === undefined || input === null;
171
205
  if (!absent && (typeof input !== 'object' || Array.isArray(input))) {
172
- return [`its ${options.field ?? 'input'} must be an object, and this call gave ${withArticle(kindOf(input))}`];
206
+ return [notAnObject(input, options.field)];
173
207
  }
174
208
  const obj = (absent ? {} : input) as Record<string, unknown>;
175
209
  const properties = (isMap(schema.properties) ? schema.properties : {}) as Record<string, { type?: unknown; nullable?: unknown; enum?: unknown } | undefined>;
@@ -183,7 +217,7 @@ export function toolArgsProblems(
183
217
  const kinds = admittedKinds(declared(key));
184
218
  if (value !== undefined && (value !== null || takesNull(declared(key)))) continue;
185
219
  missing.add(key);
186
- const wanted = kinds.length > 0 ? `"${key}" (${oneOf(kinds.map(withArticle))})` : `"${key}"`;
220
+ const wanted = kinds.length > 0 ? `${quotedValue(key)} (${oneOf(kinds.map(withArticle))})` : quotedValue(key);
187
221
  problems.push(`needs ${wanted}, and this call gave ${value === null ? 'null' : 'none'}`);
188
222
  }
189
223
  if (missing.size > 0 && declaresKeysPlainly(schema as Record<string, unknown>)) {
@@ -202,11 +236,11 @@ export function toolArgsProblems(
202
236
  const kind = kindOf(value);
203
237
  const largeInteger = kind === 'string' && kinds.includes('number') && LARGE_INTEGER_TEXT.test(value as string);
204
238
  if (kinds.length > 0 && !kinds.includes(kind) && !largeInteger) {
205
- problems.push(`"${key}" must be ${oneOf(kinds.map(withArticle))}, and this call gave ${withArticle(kind)}`);
239
+ problems.push(`${quotedValue(key)} must be ${oneOf(kinds.map(withArticle))}, and this call gave ${withArticle(kind)}`);
206
240
  continue;
207
241
  }
208
242
  if (Array.isArray(param.enum) && !param.enum.includes(value)) {
209
- problems.push(`"${key}" must be one of ${oneOf(param.enum.map(shown))}, and this call gave ${shown(value)}`);
243
+ problems.push(`${quotedValue(key)} must be one of ${oneOf(param.enum.map((e) => quotedValue(e)))}, and this call gave ${shown(value)}`);
210
244
  }
211
245
  }
212
246
  return problems;
@@ -26,6 +26,7 @@
26
26
  * of bytes from another surface is the failure this exists to end.
27
27
  */
28
28
 
29
+ import { NOTICE_TAG } from './notice-tags.js';
29
30
  import { createHash, randomBytes } from 'node:crypto';
30
31
  import { REFERENCE_STUB_MAX_CHARS, referenceTypeLabel } from './mcpl/references.js';
31
32
  import { safeSlice } from './safe-slice.js';
@@ -256,7 +257,7 @@ export type ParsedImagePlaceholder =
256
257
  const PLACEHOLDER_RE = /\[image: ([a-z0-9!#$&^_.+-]{1,127}\/[a-z0-9!#$&^_.+-]{1,127}), (~?[\d.]{1,16}(?:B|KB|MB))(?:, ref (img_[0-9a-z]{1,16}_\d{1,16}))?\]/g;
257
258
  /** `[ref_1_ab3d] cam.png — image/png, 1.2MB claimed — from tool result — …`
258
259
  * (one line, at most REFERENCE_STUB_MAX_CHARS long: see parseImagePlaceholders). */
259
- const REFERENCE_STUB_RE = /^\[(ref_[0-9a-z]+_[0-9a-z]+)\] ([^\n]*)$/gm;
260
+ const REFERENCE_STUB_RE = new RegExp(`^\\[(${NOTICE_TAG.referenceId}_[0-9a-z]+_[0-9a-z]+)\\] ([^\\n]*)$`, 'gm');
260
261
  const IMAGE_MIME_RE = /\bimage\/[a-z0-9!#$&^_.+-]+/i;
261
262
 
262
263
  /**
@@ -32,7 +32,8 @@ const MAX_BYTES = 256 * 1024;
32
32
  */
33
33
  const NONBLOCK = typeof constants.O_NONBLOCK === 'number' ? constants.O_NONBLOCK : 0;
34
34
  const MAX_DESCRIPTION_CHARS = 32768;
35
- const protectedNames = new Set(['set_tool_visibility', 'set_tool_description', 'workspace--read']);
35
+ // The toolsets meta-tools are how a hidden tool is reached again, so they stay.
36
+ const protectedNames = new Set(['set_tool_visibility', 'set_tool_description', 'workspace--read', 'toolsets--open', 'toolsets--act', 'toolsets--look', 'toolsets--close']);
36
37
  export const isPresentationTool = (name: string): boolean => name === 'set_tool_visibility' || name === 'set_tool_description';
37
38
  const hash = (value: string): string => createHash('sha256').update(value).digest('hex');
38
39
 
@@ -175,7 +176,7 @@ function lockLeftBehind(error: unknown): string {
175
176
  }
176
177
 
177
178
  export function presentationTools(path: string): ToolDefinition[] {
178
- const signpost = ` Read the generated catalogue with workspace--read {"path":${JSON.stringify(path)},"limit":140}. Hidden tools remain available; changes affect the next newly compiled request, not an in-flight stream.`;
179
+ const signpost = ` Read the generated catalogue with workspace--read {"path":${JSON.stringify(path)},"limit":140}. A hidden tool moves out of your tool list into the host's "settings" toolset (an MCPL server's tool, into that server's tab): toolsets--look lists it there, and toolsets--act calls it once that toolset is open. Changes affect the next newly compiled request, not an in-flight stream.`;
179
180
  return [
180
181
  {name:'set_tool_visibility', description:'Show or hide a tool definition. Hiding does not revoke permission.' + signpost,
181
182
  inputSchema:{type:'object',properties:{name:{type:'string'},visible:{type:'boolean'}},required:['name','visible'],additionalProperties:false}},
@@ -341,7 +342,7 @@ export class ToolPresentation {
341
342
  if (field === 'description' && typeof args.description === 'string' && characters(args.description) > MAX_DESCRIPTION_CHARS)
342
343
  throw new PresentationRefusal(`Nothing was changed: the description is ${count(characters(args.description))} characters, over the ${count(MAX_DESCRIPTION_CHARS)} allowed. Shorten it and call set_tool_description again.`);
343
344
  if (field === 'visible' && args.visible === false && protectedNames.has(name))
344
- throw new PresentationRefusal(`Nothing was changed: ${name} can't be hidden, since workspace--read, set_tool_visibility and set_tool_description are how you read the catalogue and undo these changes. Calling this again won't change that.`);
345
+ throw new PresentationRefusal(`Nothing was changed: ${name} can't be hidden, since workspace--read, set_tool_visibility, set_tool_description and the toolsets-- tools are how you read the catalogue, reach hidden tools and undo these changes. Calling this again won't change that.`);
345
346
  return name;
346
347
  }
347
348
  }
@@ -356,7 +357,7 @@ export function renderCatalogue(snapshot: PresentationSnapshot): string {
356
357
  '# Tool catalogue — generated from currently available tools', '',
357
358
  `Revision: ${snapshot.revision}`, '',
358
359
  `${snapshot.entries.length} tools: ${visible} visible, ${snapshot.entries.length-visible} hidden.`,
359
- 'Visible = supplied in new requests. Hidden = stored away, still available.',
360
+ 'Visible = not hidden by you: in your tool list, unless the host keeps it in its "settings" toolset or a server\'s tab (toolsets--look shows those). Hidden = moved into the "settings" toolset, or its MCPL server\'s tab: toolsets--look shows it, toolsets--act calls it once that toolset is open.',
360
361
  'These are your visibility choices, not fixed primary/secondary rankings.',
361
362
  'Copy exact names from the index; similar-sounding names may not exist.',
362
363
  'Show a hidden tool: set_tool_visibility {"name":"EXACT_NAME","visible":true}.',
@@ -9,6 +9,14 @@ import { resultClaimsOf, rfc005TextOf, toolResultDataToHistoryString } from './t
9
9
  import { safeSlice } from './safe-slice.js';
10
10
 
11
11
  export const TOOL_RESULT_GUARD_NOTICE = 'Tool result withheld by the guard. The tool has already executed.';
12
+ export type WithheldNoticeKind = 'ran' | 'refused' | 'error';
13
+ const GUARD_NOTICES: Record<WithheldNoticeKind, string> = {
14
+ ran: TOOL_RESULT_GUARD_NOTICE,
15
+ refused: "Tool result withheld by the guard. The tool didn't run.",
16
+ error: "Tool result withheld by the guard. The call returned an error, and this notice can't say whether the tool ran.",
17
+ };
18
+ const noticeKind = (result: CompletedToolCall['result'] | undefined): WithheldNoticeKind =>
19
+ result?.refused === true ? 'refused' : result?.isError === true ? 'error' : 'ran';
12
20
  export const TOOL_RESULT_GUARD_AUDIT_STATE = 'framework/tool-result-guard';
13
21
 
14
22
  /** Where a withheld result's original went: a workspace path; the path a
@@ -54,9 +62,12 @@ export interface WithheldHost {
54
62
  }
55
63
 
56
64
  /**
57
- * The stub a withheld result settles to: #159's neutral notice, then where
58
- * the original is, then, when its tool marked any, the fields kept from its
59
- * result (`kept`, from keptWhenWithheld). It names no refusal and no
65
+ * The stub a withheld result settles to: #159's neutral notice for its
66
+ * `kind` (a result that isn't an error, a refused call's, or another error's),
67
+ * then where the original is, then, for an error whose original is in a file
68
+ * known written, a line telling the agent to read it, then, when its tool
69
+ * marked any, the fields kept from its result (`kept`, from
70
+ * keptWhenWithheld). It names no refusal and no
60
71
  * category (#159 keeps those in operational logs), so reading the rest back
61
72
  * is the agent's deliberate choice (agent-framework #277). The place holds
62
73
  * the result as stored, which for one over the inline cap is its capped
@@ -67,7 +78,7 @@ export interface WithheldHost {
67
78
  * save failed is still queued in memory, and a restart before its retry
68
79
  * loses it.
69
80
  */
70
- export function withheldResultNotice(spill: WithheldSpill, auditDurable: boolean, kept?: string): string {
81
+ export function withheldResultNotice(spill: WithheldSpill, auditDurable: boolean, kept?: string, kind: WithheldNoticeKind = 'ran'): string {
71
82
  const audit = `the guard's audit record (${TOOL_RESULT_GUARD_AUDIT_STATE}), for an operator`;
72
83
  const keptIn = (only: string) => auditDurable
73
84
  ? `is kept ${only}in ${audit}`
@@ -80,8 +91,10 @@ export function withheldResultNotice(spill: WithheldSpill, auditDurable: boolean
80
91
  : spill.error === undefined
81
92
  ? `Its result is in workspace file ${spill.path}.`
82
93
  : `Writing its result to workspace file ${spill.path} failed, so it ${keptIn('only ')}.`;
94
+ const readable = kind === 'error' && !!spill && 'path' in spill && spill.error === undefined && !spill.unfinished;
95
+ const readFirst = readable ? ' Read it before calling again.' : '';
83
96
  const fields = kept === undefined ? '' : ` Kept from its result: ${kept}`;
84
- return `${TOOL_RESULT_GUARD_NOTICE} ${place}${fields}`;
97
+ return `${GUARD_NOTICES[kind]} ${place}${readFirst}${fields}`;
85
98
  }
86
99
 
87
100
  /**
@@ -140,6 +153,7 @@ interface PendingBatch {
140
153
  /** What each result's stub keeps if the batch is withheld, by its index in
141
154
  * content: the fields its tool marked (keptWhenWithheld). */
142
155
  kept: Map<number, string>;
156
+ notices: Map<string, WithheldNoticeKind>;
143
157
  }
144
158
 
145
159
  interface AuditOperation {
@@ -248,7 +262,7 @@ export class ToolResultGuard {
248
262
  if (block.type === 'image') images += 1;
249
263
  else chars += JSON.stringify(block).length;
250
264
  }
251
- chars -= TOOL_RESULT_GUARD_NOTICE.length;
265
+ chars -= GUARD_NOTICES[pending.notices.get(result.toolUseId) ?? 'ran'].length;
252
266
  }
253
267
  return Math.max(0, Math.ceil(chars / 4) + images * 1600);
254
268
  }
@@ -361,12 +375,13 @@ export class ToolResultGuard {
361
375
  // with a placeholder if the audit cannot become durable below.
362
376
  console.error(`[tool-result-guard] agent=${this.agentName} staged audit write queued:`, error);
363
377
  }
378
+ const results = new Map(originals.map((call) => [call.id, call.result]));
379
+ const notices = new Map([...results].map(([id, result]) => [id, noticeKind(result)]));
364
380
  const withheld: ContentBlock[] = content.map((block) => block.type === 'tool_result'
365
- ? { type: 'tool_result', toolUseId: block.toolUseId, content: TOOL_RESULT_GUARD_NOTICE, isError: block.isError }
381
+ ? { type: 'tool_result', toolUseId: block.toolUseId, content: GUARD_NOTICES[notices.get(block.toolUseId) ?? 'ran'], isError: block.isError }
366
382
  : block);
367
383
  // What each stub keeps if the batch is withheld, matched to its tool's
368
384
  // result by tool use id, keyed by index as the stubs and files are.
369
- const results = new Map(originals.map((call) => [call.id, call.result]));
370
385
  const kept = new Map<number, string>();
371
386
  for (const [index, block] of content.entries()) {
372
387
  const result = block.type === 'tool_result' ? results.get(block.toolUseId) : undefined;
@@ -379,7 +394,7 @@ export class ToolResultGuard {
379
394
  // which is correct since a reopened guard has no pending batch.
380
395
  const messageId = this.cm.addMessage('user', withheld, metadata, undefined, { holdCompression: true });
381
396
  this.pending = { id, messageId, content, withheld, wireResults,
382
- branch: this.cm.currentBranch().name, submitted: false, durable: false, kept };
397
+ branch: this.cm.currentBranch().name, submitted: false, durable: false, kept, notices };
383
398
  // Durability barrier: the audit must reach Chronicle's chain heads before
384
399
  // the originals can go to a provider. On a failed sync the batch fails
385
400
  // CLOSED: the placeholders go on the wire instead (the turn continues,
@@ -406,7 +421,7 @@ export class ToolResultGuard {
406
421
  if (pending.durable) { pending.submitted = true; return results; }
407
422
  const ids = new Set(pending.wireResults.map((result) => result.toolUseId));
408
423
  return results.map((result) => ids.has(result.toolUseId)
409
- ? { ...result, content: TOOL_RESULT_GUARD_NOTICE } : result);
424
+ ? { ...result, content: GUARD_NOTICES[pending.notices.get(result.toolUseId) ?? 'ran'] } : result);
410
425
  }
411
426
 
412
427
  /** The stream carrying this batch ended without a clean round (abort or
@@ -603,7 +618,7 @@ export class ToolResultGuard {
603
618
  spills.set(index, spill);
604
619
  }
605
620
  const annotated: ContentBlock[] = pending.withheld.map((block, index) => block.type === 'tool_result'
606
- ? { ...block, content: withheldResultNotice(spills.get(index) ?? null, pending.durable, pending.kept.get(index)) }
621
+ ? { ...block, content: withheldResultNotice(spills.get(index) ?? null, pending.durable, pending.kept.get(index), pending.notices.get(block.toolUseId)) }
607
622
  : block);
608
623
  try {
609
624
  this.append({
@@ -15,6 +15,8 @@
15
15
  * lifecycle facts (the [Gate: …] precedent).
16
16
  */
17
17
 
18
+ import { NOTICE_TAG } from '../notice-tags.js';
19
+ import { cutToFit, suppliedValue } from '../supplied-text.js';
18
20
  import { randomUUID } from 'node:crypto';
19
21
  import type { ContentBlock } from '@animalabs/membrane';
20
22
  import type { StoredMessage } from '@animalabs/context-manager';
@@ -38,14 +40,14 @@ const WAKE_COALESCE_MS = 2_000;
38
40
  const EXPIRY_RETRY_MS = 60_000;
39
41
 
40
42
  /** Ends a backlog line cut to fit in one of the residents' messages. */
41
- const CUT_MARK = " [cut to fit; the whole message is in the channel's history]";
43
+ const CUT_MARK = ` [${NOTICE_TAG.cutToFit}; the whole message is in the channel's history]`;
42
44
 
43
45
  /**
44
46
  * tune_out's refusal of a channel already tuned out: its settings, then what
45
47
  * its transport is now where the repeat tried to open it again (`transport`,
46
48
  * from the framework's dispatch), then how to change the settings.
47
49
  */
48
- export function alreadyTunedOutText(shown: string, p: TuneOutParams, transport?: string): string {
50
+ export function alreadyTunedOutText(quoted: string, p: TuneOutParams, transport?: string): string {
49
51
  // A duration so long its end is past what a Date can hold has no time to show.
50
52
  const expires = p.expiresAtMs !== undefined ? new Date(p.expiresAtMs) : undefined;
51
53
  const until = expires && Number.isFinite(expires.getTime())
@@ -53,21 +55,13 @@ export function alreadyTunedOutText(shown: string, p: TuneOutParams, transport?:
53
55
  : '';
54
56
  // A thrown error's words may end without a stop.
55
57
  const now = transport === undefined ? '' : /[.!?]$/.test(transport) ? `${transport} ` : `${transport}. `;
56
- return `Nothing changed${transport !== undefined ? ' in the tune-out' : ''}: ${JSON.stringify(shown)} is already `
58
+ return `Nothing changed${transport !== undefined ? ' in the tune-out' : ''}: ${quoted} is already `
57
59
  + `tuned out, with cadenceSeconds ${p.cadenceSeconds}, backlogCap ${p.backlogCap} and maxWakes ${p.maxWakes}${until}. `
58
60
  + now
59
61
  + 'To change those, cancel it first with tune_out mode "cancel", which delivers its backlog to you now, then call '
60
62
  + 'tune_out again with the new settings.';
61
63
  }
62
64
 
63
- /** The first `length` code units of `text`, one fewer where the last would be
64
- * the first half of a surrogate pair. */
65
- function prefixOf(text: string, length: number): string {
66
- const cut = text.slice(0, Math.max(0, length));
67
- const last = cut.charCodeAt(cut.length - 1);
68
- return last >= 0xd800 && last <= 0xdbff ? cut.slice(0, -1) : cut;
69
- }
70
-
71
65
  /** What a thrown value says, without throwing: an Error's message, or the
72
66
  * value as text, which String() can't give for an object with no prototype. */
73
67
  function thrownReason(error: unknown): string {
@@ -153,6 +147,10 @@ export class TuneOutCoordinator {
153
147
  return this.channelRegistry.nameShown({ serverId, channelId });
154
148
  }
155
149
 
150
+ private quotedShown(serverId: string, channelId: string): string {
151
+ return this.channelRegistry.quotedShown({ serverId, channelId });
152
+ }
153
+
156
154
  /** Restart cadence + expiry timers and re-anchor after a process restart. */
157
155
  resumeActiveEpochs(): void {
158
156
  let oldestAnchor: number | null = null;
@@ -244,14 +242,14 @@ export class TuneOutCoordinator {
244
242
  if (this.hooks.isForkBound({ serverId, channelId })) {
245
243
  return {
246
244
  ok: false,
247
- error: `Nothing changed: a conversation is bound to ${JSON.stringify(this.shown(serverId, channelId))}, so its messages go to that `
245
+ error: `Nothing changed: a conversation is bound to ${this.quotedShown(serverId, channelId)}, so its messages go to that `
248
246
  + "conversation, and tune_out won't divert a channel while one is. Calling tune_out on it again won't change "
249
247
  + 'that until the conversation ends, which happens only once the channel has been quiet for a while. Tune '
250
248
  + 'out a different channel, or ask whoever runs this host if this one should be diverted.',
251
249
  };
252
250
  }
253
251
  const current = this.channelRegistry.getTuneOutState(serverId, channelId);
254
- if (current) return { ok: false, error: alreadyTunedOutText(this.shown(serverId, channelId), current.params), alreadyTunedOut: true };
252
+ if (current) return { ok: false, error: alreadyTunedOutText(this.quotedShown(serverId, channelId), current.params), alreadyTunedOut: true };
255
253
  // A tool's input reaches here as the model wrote it, and the store can't
256
254
  // keep a NaN or an Infinity: its append would throw after the registry
257
255
  // had marked the channel tuned out. So each option is read as a number
@@ -337,7 +335,7 @@ export class TuneOutCoordinator {
337
335
  const ended = this.channelRegistry.cancelTuneOut(serverId, channelId, 'open', source);
338
336
  if (!ended) {
339
337
  // Its reader is the resident's tune_out: cancel_tuneout finds the channel first.
340
- return { ok: false, error: this.notTunedOut(this.shown(serverId, channelId), 'you') };
338
+ return { ok: false, error: this.notTunedOut(this.quotedShown(serverId, channelId), 'you') };
341
339
  }
342
340
  this.clearTimers(serverId, channelId);
343
341
  this.divertedSinceInvocation.delete(this.key(serverId, channelId));
@@ -360,7 +358,7 @@ export class TuneOutCoordinator {
360
358
  // start at 0 and take them all.
361
359
  const cap = Math.floor(ended.params.backlogCap);
362
360
  const authorOf = (m: Pick<StoredMessage, 'participant' | 'metadata'>): string =>
363
- (m.metadata as { author?: { name?: string } } | undefined)?.author?.name ?? m.participant;
361
+ suppliedValue((m.metadata as { author?: { name?: string } } | undefined)?.author?.name ?? m.participant);
364
362
  const lineOf = (m: Pick<StoredMessage, 'participant' | 'content' | 'metadata'>): string => {
365
363
  const text = m.content
366
364
  .filter((b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text')
@@ -370,10 +368,10 @@ export class TuneOutCoordinator {
370
368
  };
371
369
  // The channel as the resident and the subconscious are shown it.
372
370
  const name = this.shown(serverId, channelId);
373
- const lifecycle = `[Tune-out: ${name} cancelled — ${reason}, ${ended.wakeCount} wake${ended.wakeCount === 1 ? '' : 's'}]`;
371
+ const lifecycle = `[${NOTICE_TAG.tuneOut}: ${name} cancelled — ${reason}, ${ended.wakeCount} wake${ended.wakeCount === 1 ? '' : 's'}]`;
374
372
  const render = (lines: string[], truncated: number): string => {
375
373
  const attrs = [
376
- `channel="${name}"`,
374
+ `channel=${this.quotedShown(serverId, channelId)}`,
377
375
  `messages=${backlog.length}`,
378
376
  ...(truncated > 0 ? [`truncated=${truncated} (oldest; fetch_history covers the rest)`] : []),
379
377
  ];
@@ -417,7 +415,7 @@ export class TuneOutCoordinator {
417
415
  // What of the newest line's text fits beside the frame and the mark. A
418
416
  // cut shows some of it or none: a room under one character, or one
419
417
  // that would split a surrogate pair, leaves the head empty.
420
- const head = prefixOf(lines.at(-1) ?? '', room - frameShowing(1) - CUT_MARK.length);
418
+ const head = cutToFit(lines.at(-1) ?? '', room - frameShowing(1) - CUT_MARK.length);
421
419
  if (kept === 0 && head !== '') {
422
420
  lines = [head + CUT_MARK];
423
421
  shown = shown.slice(-1);
@@ -480,7 +478,7 @@ export class TuneOutCoordinator {
480
478
  : '';
481
479
  this.hooks.addMessage(
482
480
  'user',
483
- [{ type: 'text', text: `[Tune-out cancelled: ${this.shown(serverId, channelId)} — ${reason}.${truncNote}${heldNote}]` }],
481
+ [{ type: 'text', text: `[${NOTICE_TAG.tuneOutCancelled}: ${this.shown(serverId, channelId)} — ${reason}.${truncNote}${heldNote}]` }],
484
482
  { system: true, kind: 'tune-out-cancel-report', channelId },
485
483
  subName,
486
484
  );
@@ -664,8 +662,8 @@ export class TuneOutCoordinator {
664
662
 
665
663
  const shown = this.shown(serverId, channelId);
666
664
  const notice = trigger === 'wake'
667
- ? `[Tune-out wake: ${shown} — ${count} addressed message${count === 1 ? '' : 's'}]`
668
- : `[Tune-out cadence: ${shown} — ${count} diverted message${count === 1 ? '' : 's'} since your last look]`;
665
+ ? `[${NOTICE_TAG.tuneOutWake}: ${shown} — ${count} addressed message${count === 1 ? '' : 's'}]`
666
+ : `[${NOTICE_TAG.tuneOutCadence}: ${shown} — ${count} diverted message${count === 1 ? '' : 's'} since your last look]`;
669
667
  this.hooks.addMessage(
670
668
  'user',
671
669
  [{ type: 'text', text: notice }],
@@ -709,7 +707,7 @@ export class TuneOutCoordinator {
709
707
  position: 'system',
710
708
  content: [{
711
709
  type: 'text',
712
- text: `[Standing dispositions]\n${entries.map(([k, v]) => `- ${k}: ${v}`).join('\n')}`,
710
+ text: `[${NOTICE_TAG.standingDispositions}]\n${entries.map(([k, v]) => `- ${k}: ${v}`).join('\n')}`,
713
711
  }],
714
712
  };
715
713
  }
@@ -809,7 +807,7 @@ export class TuneOutCoordinator {
809
807
  if (shared) return { success: false, error: shared, isError: true };
810
808
  const channel = named[0];
811
809
  if (!channel || !this.channelRegistry.getTuneOutState(channel.serverId, channel.channelId)) {
812
- return { success: false, error: this.notTunedOut(name, 'the resident'), isError: true };
810
+ return { success: false, error: this.notTunedOut(JSON.stringify(name), 'the resident'), isError: true };
813
811
  }
814
812
  const note = typeof input.text === 'string' && input.text.trim() ? input.text.trim() : undefined;
815
813
  const result = this.cancel(channel.serverId, channel.channelId, 'subconscious', 'subconscious judgment', note);
@@ -874,7 +872,7 @@ export class TuneOutCoordinator {
874
872
  }
875
873
 
876
874
  /**
877
- * A cancel of a channel that isn't tuned out, by the name the caller gave
875
+ * A cancel of a channel that isn't tuned out, by the name, quoted, the caller gave
878
876
  * or knows it by: which channels are, so the next call can name one, and
879
877
  * how a tune-out ends without one. `backlogTo`
880
878
  * names the resident as the message's reader knows it: "you" to the
@@ -884,11 +882,11 @@ export class TuneOutCoordinator {
884
882
  * neither this list nor the name's lookup (channelsNamed, which reads the
885
883
  * registered channels too), and the text claims nothing about it.
886
884
  */
887
- private notTunedOut(name: string, backlogTo: 'you' | 'the resident'): string {
888
- const active = this.listActive().map((e) => JSON.stringify(this.shown(e.serverId, e.channelId)));
885
+ private notTunedOut(quoted: string, backlogTo: 'you' | 'the resident'): string {
886
+ const active = this.listActive().map((e) => this.quotedShown(e.serverId, e.channelId));
889
887
  const opening = active.length === 0
890
888
  ? 'no connected channel is tuned out now.'
891
- : `no connected channel ${JSON.stringify(name)} is tuned out. Connected channels tuned out now: `
889
+ : `no connected channel ${quoted} is tuned out. Connected channels tuned out now: `
892
890
  + `${active.join(', ')}.`;
893
891
  return `Nothing was cancelled: ${opening} A tune-out also ends by itself when its duration runs out, or at the `
894
892
  + `first wake past its wake budget, and its backlog goes to ${backlogTo} then.`;
@@ -113,7 +113,10 @@ export interface SubconsciousConfig {
113
113
  * agent-prefixed identifier.
114
114
  */
115
115
  name?: string;
116
- /** Model id; defaults to the primary agent's (same-model side-process). */
116
+ /** Model id; defaults to the primary agent's (same-model side-process).
117
+ * Another id, compared exactly, makes the subconscious leave out the
118
+ * fields that describe the primary's model, its window and its thinking
119
+ * binding (SUBCONSCIOUS_MODEL_FIELDS); nothing here sets its own. */
117
120
  model?: string;
118
121
  /**
119
122
  * The voice/criteria mode block — recipe-side and co-authored with the