@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
@@ -43,6 +43,41 @@ export function readSkipReplyWake(input: unknown): SkipReplyWake {
43
43
  return { kind: 'wake', seconds: Math.floor(Math.min(SKIP_REPLY_WAKE_MAX_SECONDS, seconds)) };
44
44
  }
45
45
 
46
+ /**
47
+ * A wake the heartbeat has due before a skip_reply's self-wake: its next tick
48
+ * or a reminder, one the agent's rules would let wake it as they stand when
49
+ * the self-wake is armed. `what` names it to the agent ("your heartbeat
50
+ * (…)"); `inSeconds` is how far off it is then.
51
+ */
52
+ export interface SkipReplyEarlierWake {
53
+ inSeconds: number;
54
+ what: string;
55
+ }
56
+
57
+ /**
58
+ * The note a skip_reply's result carries: that the turn ended, and, when the
59
+ * agent set a wake, when it fires unless something wakes the agent first.
60
+ * When the heartbeat has a wake due sooner, the note adds it, and what it
61
+ * would mean: any turn start cancels a pending self-wake
62
+ * (EventGate.onInferenceStarted), so a timer the agent still wants after the
63
+ * heartbeat's turn is one it sets again then, for the time that was left.
64
+ *
65
+ * It says when the heartbeat is due, not that it will wake the agent: the
66
+ * rules can change before then, a rate limit can have no token left, a
67
+ * gate.js or a host's inference policy can hold the wake back, and a batch or
68
+ * a queued request can wake the agent sooner. Each of those leaves the note
69
+ * true.
70
+ */
71
+ export function skipReplyNote(selfWakeSeconds: number | undefined, earlier?: SkipReplyEarlierWake): string {
72
+ if (selfWakeSeconds === undefined) return 'Turn ended; nothing sent.';
73
+ const note = `Turn ended; nothing sent. Self-wake in ~${selfWakeSeconds}s unless something wakes you first.`;
74
+ if (earlier === undefined) return note;
75
+ const what = earlier.what.charAt(0).toUpperCase() + earlier.what.slice(1);
76
+ const left = Math.max(1, selfWakeSeconds - earlier.inSeconds);
77
+ return `${note} ${what} is due sooner, in ~${earlier.inSeconds}s; if it wakes you, the self-wake is cancelled, `
78
+ + `so set it again for the ~${left}s left if you still want it.`;
79
+ }
80
+
46
81
  /** The refusal the model reads when its host has no EventGate to set a timer. */
47
82
  export const SKIP_REPLY_WAKE_NO_GATE_REFUSAL =
48
83
  "Your turn didn't end, and no wake was set: this host can't wake you on a timer, so skip_reply can't take wake_in_seconds here. To end the turn and wait for the next message, call skip_reply without it. Anything you meant to come back to, do now.";
package/src/mcpl/tags.ts CHANGED
@@ -21,8 +21,10 @@ const CORE_IMPLIES: Readonly<Record<string, readonly string[]>> = {
21
21
  /**
22
22
  * RFC-001 core tags that mark a message as being ABOUT the conversation
23
23
  * rather than part of it: reactions (added or removed), edits and deletions
24
- * of an existing message. These are markers, not turns — they must not clear
25
- * explicit-send suppression, count as a new conversational round, or
24
+ * of an existing message, and (revision 3) `chat:system` notices from the
25
+ * platform or the place: a join, a pin, the weather changing in a world.
26
+ * These are markers, not turns — they must not clear explicit-send
27
+ * suppression, count as a new conversational round, or
26
28
  * retarget the default publish channel. An edit can also be the first
27
29
  * version of a message the agent sees, and then counts as that message:
28
30
  * isMarkerTagged reads the host's mark for the case the coalescer knows
@@ -36,6 +38,7 @@ export const NON_CONVERSATIONAL_TAGS: ReadonlySet<string> = new Set([
36
38
  'chat:reaction-remove',
37
39
  'chat:edited',
38
40
  'chat:deleted',
41
+ 'chat:system',
39
42
  ]);
40
43
 
41
44
  /** True when a (possibly untyped) tag list carries a non-conversational marker tag. */
@@ -54,11 +57,19 @@ export function isMarkerTagged(tags: unknown, firstView: boolean): boolean {
54
57
  return hasNonConversationalTag(firstView && Array.isArray(tags) ? tags.filter((t) => t !== 'chat:edited') : tags);
55
58
  }
56
59
 
60
+ /** True when a (possibly untyped) tag list marks a `chat:system` notice: an
61
+ * event about the place, not a message anyone sent (RFC-001 rev 3). */
62
+ export function isSystemNotice(tags: unknown): boolean {
63
+ return Array.isArray(tags) && tags.includes('chat:system');
64
+ }
65
+
57
66
  /**
58
67
  * Expand a raw tag list over the normative core closure, then resolve the
59
68
  * §16.2 mutual exclusion: `chat:addressed` and `chat:ambient` cannot both
60
69
  * stand after expansion, and addressed wins — a message that is both
61
70
  * specifically-to-you and background noise is specifically-to-you.
71
+ * `chat:system` is another facet (kind, not addressing) and stands beside
72
+ * either: a pin in a DM is addressed by its place and still a notice.
62
73
  *
63
74
  * Pure; input order is preserved for the tags that survive, with implied
64
75
  * tags appended in first-cause order. Unknown and producer-namespaced tags
@@ -25,7 +25,8 @@
25
25
  *
26
26
  * Ported from mcpl-cc-bridge's presence.ts, the reference host.
27
27
  */
28
- import { objectSignature, renderToolLines, toolDigest, type HintedTool } from './tool-signature.js';
28
+ import { objectSignature, renderToolLines, toolDigest, type HintedTool, type Shown } from './tool-signature.js';
29
+ import { nameToTypeBack, quotedValue, suppliedValue } from '../supplied-text.js';
29
30
 
30
31
  export interface ToolsetDecl {
31
32
  title?: string;
@@ -69,15 +70,20 @@ export interface Change {
69
70
  /** The visible tools after the change. */
70
71
  now: HintedTool[];
71
72
  }
72
- export interface ChangeSet { changes: Change[]; suggest: Suggestion[]; notes: string[] }
73
+ export interface ChangeSet {
74
+ changes: Change[];
75
+ suggest: Suggestion[];
76
+ notes: string[];
77
+ /** Ended records' notes, by id: rendered while the id is untold, told once the set reaches an agent (markTold). */
78
+ ended?: Array<{ id: string; note: string }>;
79
+ /** false when its taker showed the change without its blocks (a look that restates the present shows the live catalog instead): told() then counts none of its definitions shown, while a notice that tells it still shows them. */
80
+ blocksShown?: false;
81
+ }
73
82
 
74
83
  export const emptyChange = (): ChangeSet => ({ changes: [], suggest: [], notes: [] });
75
- export const isEmptyChange = (c: ChangeSet): boolean => !c.changes.length && !c.suggest.length && !c.notes.length;
76
- export const joinChanges = (...sets: ChangeSet[]): ChangeSet => ({
77
- changes: sets.flatMap((s) => s.changes),
78
- suggest: sets.flatMap((s) => s.suggest),
79
- notes: sets.flatMap((s) => s.notes),
80
- });
84
+ export const isEmptyChange = (c: ChangeSet): boolean => !c.changes.length && !c.suggest.length && !c.notes.length && !c.ended?.length;
85
+ /** Whether a change to one key shows that key's tools in a block: what render prints and markShown counts as shown. */
86
+ const showsBlock = (c: Change): boolean => !c.closed && (c.opened || c.appeared.length > 0 || c.redefined.length > 0 || c.gone.length > 0);
81
87
 
82
88
  const isObj = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
83
89
  const arr = (v: unknown): unknown[] => (Array.isArray(v) ? v : []);
@@ -90,7 +96,7 @@ const str = (v: unknown): string | undefined => (typeof v === 'string' && v ? v
90
96
  * the last + 1 (a message was lost unread): recover.
91
97
  * behind: at or below the last, not applied: recover.
92
98
  */
93
- export interface ApplyOutcome { applied: boolean; skipped: boolean; behind: boolean; suggest: Suggestion[] }
99
+ export interface ApplyOutcome { applied: boolean; skipped: boolean; behind: boolean; suggest: Suggestion[]; unlisted: boolean }
94
100
 
95
101
  /** What a hint `definition` may change (RFC §6.1): everything else stays the listed tool's. */
96
102
  interface DefOverride { description?: string; inputSchema?: Record<string, unknown>; annotations?: Record<string, unknown> }
@@ -117,10 +123,10 @@ export class ToolPresence {
117
123
  private visible = new Map<string, Set<string>>();
118
124
  /** Definition overrides from hints: per key (an instance's own, or a toolset's), per tool. */
119
125
  private defs = new Map<string, Map<string, DefOverride>>();
120
- /** `ended` ids told to the agent: once per id, across reconnects (RFC §5.3). */
126
+ /** `ended` ids told to the agent, kept while the server still reports them (RFC §5.3). */
121
127
  private toldEnded = new Set<string>();
128
+ /** Moves a tools/list made that the model's tool list doesn't show yet: told once a rebuild has published them. */
122
129
  private pendingMoves = new Map<string, Presence>();
123
- private moved = new Map<string, Presence>();
124
130
  /** Digests of definitions the model has been shown with their first sentence. */
125
131
  readonly shown = new Set<string>();
126
132
 
@@ -132,6 +138,8 @@ export class ToolPresence {
132
138
  private readonly allowed: (tool: string) => boolean = () => true,
133
139
  /** How a static tool is named in the API tool list, for `look`. */
134
140
  private readonly listedName: (tool: string) => string = (tool) => tool,
141
+ /** Where the model's published tool list last put a tool, if it's there (shownAs). */
142
+ private readonly published: (tool: string) => Presence | undefined = () => undefined,
135
143
  ) {}
136
144
 
137
145
  // ── connection lifecycle ──
@@ -166,6 +174,7 @@ export class ToolPresence {
166
174
  }
167
175
  }
168
176
  this.declared = decl;
177
+ for (const name of Object.keys(decl)) this.groups.delete(name);
169
178
  this.surface = mcpl && isObj(mcpl.surface) ? { title: str(mcpl.surface.title), summary: str(mcpl.surface.summary) } : null;
170
179
  // RFC §5.1: removing a toolset, or turning its instances on or off, ends
171
180
  // its open instances; one that becomes undeclared starts closed under the
@@ -190,21 +199,35 @@ export class ToolPresence {
190
199
  this.visible.clear();
191
200
  this.defs.clear();
192
201
  this.pendingMoves.clear();
193
- this.moved.clear();
194
202
  }
195
203
 
196
204
  /** tools/list: every definition, visible or not (RFC §4.1). A tool the host's policy refuses is left out. */
197
205
  setTools(list: HintedTool[]): void {
198
- this.tools = new Map(list
199
- .filter((t) => isObj(t) && typeof t.name === 'string' && this.allowed(t.name))
200
- .map((t) => [t.name, t]));
206
+ const seen = new Map([...this.tools.keys()].map((name) => [name, this.presenceOf(name)]));
207
+ const named = list.filter((t) => isObj(t) && typeof t.name === 'string');
208
+ this.tools = new Map();
209
+ for (const t of named) if (this.allowed(t.name) && !this.tools.has(t.name)) this.tools.set(t.name, t);
201
210
  for (const set of this.visible.values()) for (const n of [...set]) if (!this.tools.has(n)) set.delete(n);
211
+ // RFC §4.1: the list states each tool's presence, and presence follows
212
+ // it. A changed presence is a move the model's tool list shows at its
213
+ // next rebuild (§4.2; applyMoves); one the list reverses before then
214
+ // never reached that list.
215
+ for (const name of [...this.pendingMoves.keys()]) if (!this.tools.has(name)) this.pendingMoves.delete(name);
216
+ for (const name of this.active ? this.tools.keys() : []) {
217
+ const was = seen.get(name);
218
+ const listed = this.listedPresence(name);
219
+ if (this.pendingMoves.has(name)) {
220
+ if (this.pendingMoves.get(name) !== listed) this.pendingMoves.delete(name);
221
+ } else if (was !== undefined && was !== listed) {
222
+ this.pendingMoves.set(name, listed);
223
+ }
224
+ }
202
225
  // The list's `mcpl/visible` is the server's word only where nothing else
203
226
  // speaks: toolset-less dynamic tools and undeclared groups. For a declared
204
227
  // toolset it's an aggregate ("visible in some open instance"); per-instance
205
228
  // state comes from toolsets/list and hint changes (RFC §4.1, §5.3).
206
229
  for (const t of this.tools.values()) {
207
- if (this.presenceOf(t.name) !== 'dynamic') continue;
230
+ if (this.listedPresence(t.name) !== 'dynamic') continue;
208
231
  const ts = this.toolsetOf(t.name);
209
232
  if (ts && this.isDeclared(ts)) continue;
210
233
  this.setVisible(keyOf(ts ?? NO_TOOLSET), t.name, t._meta?.['mcpl/visible'] !== false);
@@ -234,12 +257,23 @@ export class ToolPresence {
234
257
  return m && typeof m['mcpl/toolset'] === 'string' && m['mcpl/toolset'] ? (m['mcpl/toolset'] as string) : undefined;
235
258
  }
236
259
 
237
- /** static unless hinted dynamic; a toolset wins over any "static" (RFC §4.1); a move applies only at a boundary (applyMoves). */
260
+ /** As the list states it: a toolset's tool is dynamic whatever a hint says (RFC §4.1). */
238
261
  presenceOf(name: string): Presence {
239
262
  if (!this.active) return 'static';
263
+ return this.listedPresence(name);
264
+ }
265
+
266
+ /** RFC §4.2: "Until it applies the move, the tool keeps working where it
267
+ * is." Whether a tool is at `place` in the tool list last published, or
268
+ * in the server's latest tools/list (its presence). Until a rebuild
269
+ * applies a move the two differ, and a call is taken at either. */
270
+ shownAs(name: string, place: Presence): boolean {
271
+ if (!this.active) return place === 'static';
272
+ return this.published(name) === place || this.listedPresence(name) === place;
273
+ }
274
+
275
+ private listedPresence(name: string): Presence {
240
276
  if (this.toolsetOf(name)) return 'dynamic';
241
- const moved = this.moved.get(name);
242
- if (moved) return moved;
243
277
  return this.tools.get(name)?._meta?.['mcpl/presence'] === 'dynamic' ? 'dynamic' : 'static';
244
278
  }
245
279
 
@@ -332,12 +366,12 @@ export class ToolPresence {
332
366
 
333
367
  /** Apply a HintChanges (RFC §6.1) from any of its carriers, as it is read (see ApplyOutcome). */
334
368
  apply(h: unknown): ApplyOutcome {
335
- const none: ApplyOutcome = { applied: false, skipped: false, behind: false, suggest: [] };
369
+ const none: ApplyOutcome = { applied: false, skipped: false, behind: false, suggest: [], unlisted: false };
336
370
  if (!isObj(h) || typeof h.rev !== 'number' || !Number.isFinite(h.rev)) return none;
337
371
  if (h.rev <= this.rev) return { ...none, behind: true };
338
372
  const skipped = h.rev !== this.rev + 1;
339
373
  this.rev = h.rev;
340
- return { ...none, applied: true, skipped, suggest: this.applyBody(h) };
374
+ return { ...none, applied: true, skipped, ...this.applyBody(h) };
341
375
  }
342
376
 
343
377
  /**
@@ -352,7 +386,8 @@ export class ToolPresence {
352
386
  return lost;
353
387
  }
354
388
 
355
- private applyBody(h: Record<string, unknown>): Suggestion[] {
389
+ private applyBody(h: Record<string, unknown>): { suggest: Suggestion[]; unlisted: boolean } {
390
+ let unlisted = false;
356
391
  for (const e of arr(h.toolsets)) {
357
392
  if (!isObj(e) || !str(e.toolset) || !this.isDeclared(e.toolset as string)) continue; // undeclared toolsets are host-local
358
393
  const ts = e.toolset as string;
@@ -366,20 +401,20 @@ export class ToolPresence {
366
401
  else if (e.state === 'closed') this.dropKey(key);
367
402
  }
368
403
  for (const e of arr(h.tools)) {
369
- if (!isObj(e) || !str(e.name) || !this.tools.has(e.name as string)) continue;
404
+ if (!isObj(e) || !str(e.name)) continue;
370
405
  const name = e.name as string;
371
- // A toolset's tool is dynamic whatever a hint says (RFC §4.1).
372
- if ((e.presence === 'static' || e.presence === 'dynamic') && !this.toolsetOf(name)) {
373
- // RFC §4.2: a move applies at a boundary the host chooses; until then the tool stays where it is.
374
- if (e.presence !== this.presenceOf(name)) this.pendingMoves.set(name, e.presence);
375
- else this.pendingMoves.delete(name);
406
+ if (!this.tools.has(name)) {
407
+ if (this.allowed(name)) unlisted = true;
408
+ continue;
376
409
  }
377
- if (isObj(e.definition) && this.presenceOf(name) === 'dynamic') {
410
+ // A hint's presence stages nothing: a hinted move takes effect when
411
+ // the server's tools/list states it (RFC §4.1), as the rebuild's own
412
+ // list does. A definition it gives is the tool's current one, kept
413
+ // whatever its presence now, for when it's shown dynamic (§6.1).
414
+ if (isObj(e.definition)) {
378
415
  const key = this.defKeyFor(name, str(e.instance));
379
- if (key !== null) {
380
- if (!this.defs.has(key)) this.defs.set(key, new Map());
381
- this.defs.get(key)!.set(name, overrideOf(e.definition));
382
- }
416
+ if (!this.defs.has(key)) this.defs.set(key, new Map());
417
+ this.defs.get(key)!.set(name, overrideOf(e.definition));
383
418
  }
384
419
  if (typeof e.visible === 'boolean') for (const key of this.keysFor(name, str(e.instance))) this.setVisible(key, name, e.visible);
385
420
  }
@@ -388,14 +423,14 @@ export class ToolPresence {
388
423
  if (!isObj(s) || !str(s.toolset) || (s.action !== 'open' && s.action !== 'close')) continue;
389
424
  suggest.push({ toolset: s.toolset as string, ...(str(s.instance) ? { instance: str(s.instance) } : {}), action: s.action, reason: str(s.reason) ?? '' });
390
425
  }
391
- return suggest;
426
+ return { suggest, unlisted };
392
427
  }
393
428
 
394
429
  /**
395
430
  * Where a definition lands (RFC §6.1): with `instance`, that instance's own;
396
431
  * without, the toolset's (for a toolset without instances, its only one's).
397
432
  */
398
- private defKeyFor(name: string, instance: string | undefined): string | null {
433
+ private defKeyFor(name: string, instance: string | undefined): string {
399
434
  const ts = this.toolsetOf(name);
400
435
  if (!ts) return keyOf(NO_TOOLSET);
401
436
  return keyOf(ts, this.declared[ts]?.instances ? instance : undefined);
@@ -445,7 +480,7 @@ export class ToolPresence {
445
480
  * Returns what was lost, what the server says ended while no connection was
446
481
  * told, its suggestions, and whether its rev skipped one.
447
482
  */
448
- adopt(result: unknown): { lost: OpenRecord[]; ended: Ended[]; suggest: Suggestion[]; skipped: boolean } {
483
+ adopt(result: unknown): { lost: OpenRecord[]; ended: Ended[]; suggest: Suggestion[]; skipped: boolean; unlisted: boolean } {
449
484
  const had = [...this.open.values()];
450
485
  // A full statement: a dynamic tool not stated visible is invisible.
451
486
  // Undeclared groups stay as the host has them open or closed.
@@ -463,9 +498,10 @@ export class ToolPresence {
463
498
  }
464
499
  let suggest: Suggestion[] = [];
465
500
  let skipped = false;
501
+ let unlisted = false;
466
502
  const h = isObj(r._meta) ? r._meta['mcpl/hints'] : undefined;
467
503
  if (isObj(h)) {
468
- suggest = this.applyBody(h);
504
+ ({ suggest, unlisted } = this.applyBody(h));
469
505
  // Its own rev, at its place in order (RFC §6.1).
470
506
  if (typeof h.rev === 'number' && Number.isFinite(h.rev)) {
471
507
  skipped = h.rev !== this.rev + 1;
@@ -477,29 +513,51 @@ export class ToolPresence {
477
513
  if (!isObj(e) || !str(e.toolset)) continue;
478
514
  ended.push({ ...(str(e.id) ? { id: str(e.id) } : {}), toolset: e.toolset as string, ...(str(e.instance) ? { instance: str(e.instance) } : {}), reason: str(e.reason) ?? '' });
479
515
  }
480
- return { lost: had.filter((o) => !this.open.has(keyOf(o.toolset, o.instance))), ended, suggest, skipped };
516
+ // RFC §5.3: a record is reported on every toolsets/list until it expires, and its id is never
517
+ // reused, so an id the server no longer reports needn't be remembered.
518
+ const reported = new Set(ended.flatMap((e) => (e.id ? [e.id] : [])));
519
+ for (const id of [...this.toldEnded]) if (!reported.has(id)) this.toldEnded.delete(id);
520
+ return { lost: had.filter((o) => !this.open.has(keyOf(o.toolset, o.instance))), ended, suggest, skipped, unlisted };
481
521
  }
482
522
 
483
523
  /**
484
524
  * What the agent should hear about an adoption: what ended while it was
485
- * away, each `ended` id once however often the server repeats it (RFC
486
- * §5.3), and what is closed now.
525
+ * away, each `ended` id until it has been told (RFC §5.3; markTold), and
526
+ * what is closed now. An ended record with an id is in `ended`, rendered
527
+ * while its id is untold.
487
528
  */
488
- adoptionNotes(lost: OpenRecord[], ended: Ended[]): string[] {
529
+ adoptionNotes(lost: OpenRecord[], ended: Ended[]): { notes: string[]; ended: Array<{ id: string; note: string }> } {
489
530
  const notes: string[] = [];
490
- const endedKeys = new Set(ended.map((e) => keyOf(e.toolset, e.instance)));
531
+ const told: Array<{ id: string; note: string }> = [];
532
+ const endedKeys = new Set<string>();
491
533
  for (const e of ended) {
534
+ if (!e.id || !this.toldEnded.has(e.id)) endedKeys.add(keyOf(e.toolset, e.instance));
535
+ const note = `${this.label(keyOf(e.toolset, e.instance))} ended${e.reason ? `: ${suppliedValue(e.reason)}` : ''}.`;
492
536
  if (e.id) {
493
- if (this.toldEnded.has(e.id)) continue;
494
- this.toldEnded.add(e.id);
495
- }
496
- notes.push(`${this.label(keyOf(e.toolset, e.instance))} ended${e.reason ? `: ${e.reason}` : ''}.`);
537
+ if (!this.toldEnded.has(e.id)) told.push({ id: e.id, note });
538
+ } else notes.push(note);
497
539
  }
498
540
  for (const o of lost) {
499
541
  if (endedKeys.has(keyOf(o.toolset, o.instance))) continue;
500
542
  notes.push(`${this.label(keyOf(o.toolset, o.instance))} is closed: ${this.server} no longer has it open. ${this.openHint(o.toolset)} starts a new one.`);
501
543
  }
502
- return notes;
544
+ return { notes, ended: told };
545
+ }
546
+
547
+ /** The agent has been told these ended records (a notice written, a result it reads); the next statement forgets any it no longer reports. */
548
+ markTold(ended: ChangeSet['ended']): void {
549
+ for (const { id } of ended ?? []) this.toldEnded.add(id);
550
+ }
551
+
552
+ /** The definitions a change set's tool blocks describe have reached the model. */
553
+ markShown(cs: ChangeSet): void {
554
+ for (const c of cs.changes) if (showsBlock(c)) for (const t of c.now) this.shown.add(toolDigest(t));
555
+ }
556
+
557
+ /** A set's ended notes whose ids are still untold, each once. */
558
+ untoldNotes(cs: ChangeSet): string[] {
559
+ const seen = new Set<string>();
560
+ return (cs.ended ?? []).filter(({ id }) => !this.toldEnded.has(id) && !seen.has(id) && !!seen.add(id)).map(({ note }) => note);
503
561
  }
504
562
 
505
563
  // ── host-made opens of undeclared groups (RFC §5.4) ──
@@ -514,15 +572,15 @@ export class ToolPresence {
514
572
 
515
573
  // ── boundaries ──
516
574
 
517
- /** Apply pending static↔dynamic moves (RFC §4.2) at a boundary the host chooses. Returns what moved. */
518
- applyMoves(): Array<{ name: string; to: Presence }> {
519
- const out: Array<{ name: string; to: Presence }> = [];
520
- for (const [name, to] of this.pendingMoves) {
521
- if (!this.tools.has(name)) continue;
522
- this.moved.set(name, to);
523
- out.push({ name, to });
524
- }
525
- this.pendingMoves.clear();
575
+ /** The static↔dynamic moves the lists made since the last rebuild, still to be shown. */
576
+ moves(): Array<{ name: string; to: Presence }> {
577
+ return [...this.pendingMoves].map(([name, to]) => ({ name, to }));
578
+ }
579
+
580
+ /** At a rebuild of the model's tool list (RFC §4.2): those of `moves` still pending, now shown. */
581
+ applyMoves(moves = this.moves()): Array<{ name: string; to: Presence }> {
582
+ const out = moves.filter(({ name, to }) => this.pendingMoves.get(name) === to);
583
+ for (const { name } of out) this.pendingMoves.delete(name);
526
584
  return out;
527
585
  }
528
586
 
@@ -531,60 +589,67 @@ export class ToolPresence {
531
589
  /** The marker a key's announcements begin with, `[server/toolset:instance]`, which a reminder looks for. */
532
590
  label(key: string): string {
533
591
  const { toolset, instance } = splitKey(key);
534
- return `[${this.server}${toolset ? `/${toolset}` : ''}${instance ? `:${instance}` : ''}]`;
592
+ return `[${this.server}${toolset ? `/${nameToTypeBack(toolset)}` : ''}${instance ? `:${nameToTypeBack(instance)}` : ''}]`;
535
593
  }
536
594
 
537
595
  callHint(key: string): string {
538
596
  const { instance } = splitKey(key);
539
- return `${this.names.act} {server:"${this.server}"${instance ? `, instance:"${instance}"` : ''}, tool, args}`;
597
+ return `${this.names.act} {server:"${this.server}"${instance ? `, instance:${quotedValue(instance)}` : ''}, tool, args}`;
540
598
  }
541
599
 
542
600
  closeHint(key: string): string | null {
543
601
  const { toolset, instance } = splitKey(key);
544
602
  if (!toolset) return null;
545
- return `${this.names.close} {server:"${this.server}", toolset:"${toolset}"${instance ? `, instance:"${instance}"` : ''}}`;
603
+ return `${this.names.close} {server:"${this.server}", toolset:${quotedValue(toolset)}${instance ? `, instance:${quotedValue(instance)}` : ''}}`;
546
604
  }
547
605
 
548
606
  openHint(toolset: string, instance?: string): string {
549
- return `${this.names.open} {server:"${this.server}", toolset:"${toolset}"${instance ? `, instance:"${instance}"` : ''}}`;
607
+ return `${this.names.open} {server:"${this.server}", toolset:${quotedValue(toolset)}${instance ? `, instance:${quotedValue(instance)}` : ''}}`;
550
608
  }
551
609
 
552
610
  /** The tool block for one key: its label, how to call, and the visible tools (new or changed ones described). */
553
- toolBlock(key: string, tools: HintedTool[], opts: { describeAll?: boolean; headed?: boolean; title?: string } = {}): string[] {
554
- const head = opts.headed === false ? '' : `${this.label(key)}${opts.title ? ` ${opts.title}` : ''} `;
611
+ toolBlock(key: string, tools: HintedTool[], opts: { describeAll?: boolean; headed?: boolean; title?: string; seen?: Shown } = {}): string[] {
612
+ const head = opts.headed === false ? '' : `${this.label(key)}${opts.title ? ` ${suppliedValue(opts.title)}` : ''} `;
555
613
  if (!tools.length) return [`${head}no tools available right now.`];
556
- return [`${head}tools now — ${this.callHint(key)}:`, ...renderToolLines(tools, this.shown, { describeAll: opts.describeAll })];
614
+ return [`${head}tools now — ${this.callHint(key)}:`, ...renderToolLines(tools, opts.seen ?? this.shown, { describeAll: opts.describeAll })];
557
615
  }
558
616
 
559
- /** What the model reads for a change set. `bare`: a key whose header the caller already printed. */
560
- render(cs: ChangeSet, opts: { bare?: string } = {}): string {
617
+ /**
618
+ * What the model reads for a change set. `bare`: a key whose header the
619
+ * caller already printed. `mark: false` renders without counting the
620
+ * definitions it describes as shown, for text that may not reach the model
621
+ * (markShown counts them once it has). `describeAll`: every tool with its
622
+ * first sentence, as for a result that may reach no agent.
623
+ */
624
+ render(cs: ChangeSet, opts: { bare?: string; mark?: boolean; describeAll?: boolean } = {}): string {
625
+ const local = new Set<string>();
626
+ const seen: Shown = opts.mark === false ? { has: (d) => this.shown.has(d) || local.has(d), add: (d) => { local.add(d); } } : this.shown;
561
627
  const lines: string[] = [];
562
628
  for (const c of cs.changes) {
563
629
  const bare = opts.bare === c.key;
564
630
  if (c.closed) {
565
- lines.push(`${this.label(c.key)} closed.${c.gone.length ? ` Now unavailable: ${c.gone.join(', ')}.` : ''}`);
631
+ lines.push(`${this.label(c.key)} closed.${c.gone.length ? ` Now unavailable: ${c.gone.map(suppliedValue).join(', ')}.` : ''}`);
566
632
  continue;
567
633
  }
568
634
  const title = (c.opened || c.retitled) && !bare ? c.title : undefined;
569
- const setChanged = c.opened || c.appeared.length > 0 || c.redefined.length > 0 || c.gone.length > 0;
570
- if (setChanged) lines.push(...this.toolBlock(c.key, c.now, { headed: !bare, title }));
571
- else if (title) lines.push(`${this.label(c.key)} ${title}`);
572
- if (c.gone.length) lines.push(`now unavailable: ${c.gone.join(', ')}`);
635
+ if (showsBlock(c)) lines.push(...this.toolBlock(c.key, c.now, { headed: !bare, title, seen, describeAll: opts.describeAll }));
636
+ else if (title) lines.push(`${this.label(c.key)} ${suppliedValue(title)}`);
637
+ if (c.gone.length) lines.push(`now unavailable: ${c.gone.map(suppliedValue).join(', ')}`);
573
638
  if (c.opened && !bare) {
574
639
  const close = this.closeHint(c.key);
575
640
  if (close) lines.push(`${close} closes it.`);
576
641
  }
577
642
  }
578
643
  for (const s of cs.suggest) lines.push(this.renderSuggestion(s));
579
- lines.push(...cs.notes);
644
+ lines.push(...this.untoldNotes(cs), ...cs.notes);
580
645
  return lines.join('\n');
581
646
  }
582
647
 
583
648
  /** RFC §6.2: a suggestion is shown, never acted on. */
584
649
  renderSuggestion(s: Suggestion): string {
585
- const what = `${s.toolset}${s.instance ? `:${s.instance}` : ''}`;
650
+ const what = suppliedValue(`${s.toolset}${s.instance ? `:${s.instance}` : ''}`);
586
651
  const how = s.action === 'open' ? this.openHint(s.toolset, s.instance) : this.closeHint(keyOf(s.toolset, s.instance));
587
- return `${this.server} suggests ${s.action === 'open' ? 'opening' : 'closing'} ${what}${s.reason ? `: ${s.reason}` : ''} — ${how} if you want to (nothing was done).`;
652
+ return `${this.server} suggests ${s.action === 'open' ? 'opening' : 'closing'} ${what}${s.reason ? `: ${suppliedValue(s.reason)}` : ''} — ${how} if you want to (nothing was done).`;
588
653
  }
589
654
 
590
655
  /** The keys a reminder restates: every open key of this server's view. */
@@ -605,14 +670,16 @@ export class ToolPresence {
605
670
  return lines.join('\n');
606
671
  }
607
672
 
608
- /** look naming this server: what is open (with tools) and what can be opened. */
609
- catalog(): string {
673
+ /** look naming this server: what is open (with tools) and what can be
674
+ * opened. `mark: false`, as for render, for a look whose result may
675
+ * reach no agent. */
676
+ catalog(opts: { mark?: boolean } = {}): string {
610
677
  const lines: string[] = [];
611
- lines.push(this.surface ? `${this.server} — ${[this.surface.title, this.surface.summary].filter(Boolean).join(': ')}` : this.server);
678
+ lines.push(this.surface ? `${this.server} — ${[this.surface.title, this.surface.summary].filter(Boolean).map(suppliedValue).join(': ')}` : this.server);
612
679
  const v = this.view();
613
680
  if (v.size) {
614
681
  for (const [key, e] of v) {
615
- lines.push(...this.toolBlock(key, [...e.tools.values()], { describeAll: true, title: e.title }).map((l) => ` ${l}`));
682
+ lines.push(...this.toolBlock(key, [...e.tools.values()], { describeAll: true, title: e.title, seen: opts.mark === false ? new Set<string>() : undefined }).map((l) => ` ${l}`));
616
683
  const close = this.closeHint(key);
617
684
  if (close) lines.push(` ${close} closes it.`);
618
685
  }
@@ -623,7 +690,7 @@ export class ToolPresence {
623
690
  for (const [name, d] of Object.entries(this.declared)) {
624
691
  if (!d.instances && this.isOpen(name)) continue;
625
692
  openable.push(
626
- ` ${name}${d.title ? ` — ${d.title}` : ''}${d.summary ? `: ${d.summary}` : ''}` +
693
+ ` ${nameToTypeBack(name)}${d.title ? ` — ${suppliedValue(d.title)}` : ''}${d.summary ? `: ${suppliedValue(d.summary)}` : ''}` +
627
694
  (d.instances ? ' (several can be open at once; name each with instance)' : '') +
628
695
  (d.options ? ` · options ${objectSignature(d.options)}` : ''),
629
696
  );
@@ -631,7 +698,7 @@ export class ToolPresence {
631
698
  for (const g of this.undeclaredGroups) {
632
699
  if (this.groups.has(g)) continue;
633
700
  const names = this.allTools.filter((t) => this.toolsetOf(t.name) === g).map((t) => t.name);
634
- openable.push(` ${g}: ${names.join(', ')}`);
701
+ openable.push(` ${nameToTypeBack(g)}: ${names.map(nameToTypeBack).join(', ')}`);
635
702
  }
636
703
  if (openable.length) lines.push(`can open — ${this.names.open} {server:"${this.server}", toolset, instance?, options?}:`, ...openable);
637
704
  return lines.join('\n');
@@ -639,12 +706,12 @@ export class ToolPresence {
639
706
 
640
707
  /** Where a tool can be called from now, for `look` naming it. */
641
708
  toolStatus(tool: string): string {
642
- if (this.presenceOf(tool) === 'static') return `static — in your tool list as ${this.listedName(tool)}`;
709
+ if (this.presenceOf(tool) === 'static') return `static — called by name, as ${nameToTypeBack(this.listedName(tool))}`;
643
710
  const where = [...this.view().entries()].filter(([, e]) => e.tools.has(tool)).map(([k]) => k);
644
711
  const ts = this.toolsetOf(tool);
645
712
  return where.length
646
- ? `available now in ${where.map((k) => this.label(k)).join(', ')} — ${this.names.act} {server:"${this.server}", tool:"${tool}"${where.length > 1 ? ', instance' : ''}, args}`
647
- : `not available right now${ts ? ` (toolset ${ts})` : ''}`;
713
+ ? `available now in ${where.map((k) => this.label(k)).join(', ')} — ${this.names.act} {server:"${this.server}", tool:${quotedValue(tool)}${where.length > 1 ? ', instance' : ''}, args}`
714
+ : `not available right now${ts ? ` (toolset ${nameToTypeBack(ts)})` : ''}`;
648
715
  }
649
716
 
650
717
  }