@animalabs/agent-framework 0.26.2 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/README.md +99 -8
  2. package/dist/src/agent.d.ts +291 -23
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +596 -61
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/server.d.ts.map +1 -1
  7. package/dist/src/api/server.js +5 -4
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/auto-continue.d.ts +34 -0
  10. package/dist/src/auto-continue.d.ts.map +1 -0
  11. package/dist/src/auto-continue.js +36 -0
  12. package/dist/src/auto-continue.js.map +1 -0
  13. package/dist/src/code-execution/isolate/scripts.d.ts +11 -0
  14. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  15. package/dist/src/code-execution/isolate/scripts.js +5 -1
  16. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  17. package/dist/src/cut-off-round.d.ts +24 -0
  18. package/dist/src/cut-off-round.d.ts.map +1 -0
  19. package/dist/src/cut-off-round.js +87 -0
  20. package/dist/src/cut-off-round.js.map +1 -0
  21. package/dist/src/framework.d.ts +1038 -193
  22. package/dist/src/framework.d.ts.map +1 -1
  23. package/dist/src/framework.js +5967 -1514
  24. package/dist/src/framework.js.map +1 -1
  25. package/dist/src/gate/event-gate.d.ts +405 -46
  26. package/dist/src/gate/event-gate.d.ts.map +1 -1
  27. package/dist/src/gate/event-gate.js +2646 -399
  28. package/dist/src/gate/event-gate.js.map +1 -1
  29. package/dist/src/gate/index.d.ts +3 -2
  30. package/dist/src/gate/index.d.ts.map +1 -1
  31. package/dist/src/gate/index.js +2 -1
  32. package/dist/src/gate/index.js.map +1 -1
  33. package/dist/src/gate/recipe-reconcile.d.ts +103 -0
  34. package/dist/src/gate/recipe-reconcile.d.ts.map +1 -0
  35. package/dist/src/gate/recipe-reconcile.js +325 -0
  36. package/dist/src/gate/recipe-reconcile.js.map +1 -0
  37. package/dist/src/gate/reply-judge.d.ts +116 -0
  38. package/dist/src/gate/reply-judge.d.ts.map +1 -0
  39. package/dist/src/gate/reply-judge.js +467 -0
  40. package/dist/src/gate/reply-judge.js.map +1 -0
  41. package/dist/src/gate/types.d.ts +229 -8
  42. package/dist/src/gate/types.d.ts.map +1 -1
  43. package/dist/src/heartbeat/heartbeat.d.ts +184 -0
  44. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
  45. package/dist/src/heartbeat/heartbeat.js +752 -0
  46. package/dist/src/heartbeat/heartbeat.js.map +1 -0
  47. package/dist/src/heartbeat/reminder-time.d.ts +5 -0
  48. package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
  49. package/dist/src/heartbeat/reminder-time.js +107 -0
  50. package/dist/src/heartbeat/reminder-time.js.map +1 -0
  51. package/dist/src/index.d.ts +10 -4
  52. package/dist/src/index.d.ts.map +1 -1
  53. package/dist/src/index.js +7 -2
  54. package/dist/src/index.js.map +1 -1
  55. package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
  56. package/dist/src/mcpl/capability-mask.js +3 -2
  57. package/dist/src/mcpl/capability-mask.js.map +1 -1
  58. package/dist/src/mcpl/channel-registry.d.ts +433 -113
  59. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  60. package/dist/src/mcpl/channel-registry.js +1154 -435
  61. package/dist/src/mcpl/channel-registry.js.map +1 -1
  62. package/dist/src/mcpl/conversation-router.d.ts +16 -8
  63. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  64. package/dist/src/mcpl/conversation-router.js +29 -15
  65. package/dist/src/mcpl/conversation-router.js.map +1 -1
  66. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  67. package/dist/src/mcpl/convert-block.js +14 -2
  68. package/dist/src/mcpl/convert-block.js.map +1 -1
  69. package/dist/src/mcpl/hook-orchestrator.d.ts +7 -1
  70. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  71. package/dist/src/mcpl/hook-orchestrator.js +32 -7
  72. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  73. package/dist/src/mcpl/inbound-source.d.ts +26 -10
  74. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  75. package/dist/src/mcpl/inbound-source.js +40 -69
  76. package/dist/src/mcpl/inbound-source.js.map +1 -1
  77. package/dist/src/mcpl/index.d.ts +5 -1
  78. package/dist/src/mcpl/index.d.ts.map +1 -1
  79. package/dist/src/mcpl/index.js +4 -0
  80. package/dist/src/mcpl/index.js.map +1 -1
  81. package/dist/src/mcpl/inference-router.d.ts +14 -2
  82. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  83. package/dist/src/mcpl/inference-router.js +73 -28
  84. package/dist/src/mcpl/inference-router.js.map +1 -1
  85. package/dist/src/mcpl/prose-grammar.d.ts +4 -0
  86. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  87. package/dist/src/mcpl/prose-grammar.js +6 -0
  88. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  89. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  90. package/dist/src/mcpl/protocol-family.js +3 -0
  91. package/dist/src/mcpl/protocol-family.js.map +1 -1
  92. package/dist/src/mcpl/push-coalescer.d.ts +20 -2
  93. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  94. package/dist/src/mcpl/push-coalescer.js +33 -11
  95. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  96. package/dist/src/mcpl/reference-fetcher.d.ts +63 -42
  97. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  98. package/dist/src/mcpl/reference-fetcher.js +177 -145
  99. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  100. package/dist/src/mcpl/references.d.ts +380 -46
  101. package/dist/src/mcpl/references.d.ts.map +1 -1
  102. package/dist/src/mcpl/references.js +736 -77
  103. package/dist/src/mcpl/references.js.map +1 -1
  104. package/dist/src/mcpl/server-connection.d.ts +93 -4
  105. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  106. package/dist/src/mcpl/server-connection.js +176 -11
  107. package/dist/src/mcpl/server-connection.js.map +1 -1
  108. package/dist/src/mcpl/tags.d.ts +9 -2
  109. package/dist/src/mcpl/tags.d.ts.map +1 -1
  110. package/dist/src/mcpl/tags.js +12 -2
  111. package/dist/src/mcpl/tags.js.map +1 -1
  112. package/dist/src/mcpl/tool-lifecycle.d.ts +11 -0
  113. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  114. package/dist/src/mcpl/tool-lifecycle.js +21 -0
  115. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  116. package/dist/src/mcpl/tool-presence.d.ts +311 -0
  117. package/dist/src/mcpl/tool-presence.d.ts.map +1 -0
  118. package/dist/src/mcpl/tool-presence.js +697 -0
  119. package/dist/src/mcpl/tool-presence.js.map +1 -0
  120. package/dist/src/mcpl/tool-result-normalize.d.ts +9 -3
  121. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  122. package/dist/src/mcpl/tool-result-normalize.js +14 -6
  123. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  124. package/dist/src/mcpl/tool-signature.d.ts +46 -0
  125. package/dist/src/mcpl/tool-signature.d.ts.map +1 -0
  126. package/dist/src/mcpl/tool-signature.js +321 -0
  127. package/dist/src/mcpl/tool-signature.js.map +1 -0
  128. package/dist/src/mcpl/toolset-host.d.ts +251 -0
  129. package/dist/src/mcpl/toolset-host.d.ts.map +1 -0
  130. package/dist/src/mcpl/toolset-host.js +720 -0
  131. package/dist/src/mcpl/toolset-host.js.map +1 -0
  132. package/dist/src/mcpl/toolsets-module.d.ts +83 -0
  133. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -0
  134. package/dist/src/mcpl/toolsets-module.js +523 -0
  135. package/dist/src/mcpl/toolsets-module.js.map +1 -0
  136. package/dist/src/mcpl/transport.d.ts +2 -2
  137. package/dist/src/mcpl/transport.d.ts.map +1 -1
  138. package/dist/src/mcpl/transport.js +3 -2
  139. package/dist/src/mcpl/transport.js.map +1 -1
  140. package/dist/src/mcpl/types.d.ts +24 -2
  141. package/dist/src/mcpl/types.d.ts.map +1 -1
  142. package/dist/src/mcpl/types.js.map +1 -1
  143. package/dist/src/mcpl/visible-content.d.ts +7 -9
  144. package/dist/src/mcpl/visible-content.d.ts.map +1 -1
  145. package/dist/src/mcpl/visible-content.js +21 -9
  146. package/dist/src/mcpl/visible-content.js.map +1 -1
  147. package/dist/src/module-registry.d.ts +27 -9
  148. package/dist/src/module-registry.d.ts.map +1 -1
  149. package/dist/src/module-registry.js +77 -17
  150. package/dist/src/module-registry.js.map +1 -1
  151. package/dist/src/modules/discord/index.js +1 -1
  152. package/dist/src/modules/discord/index.js.map +1 -1
  153. package/dist/src/modules/history/index.d.ts +2 -1
  154. package/dist/src/modules/history/index.d.ts.map +1 -1
  155. package/dist/src/modules/history/index.js +73 -28
  156. package/dist/src/modules/history/index.js.map +1 -1
  157. package/dist/src/modules/history/semantic.d.ts +108 -6
  158. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  159. package/dist/src/modules/history/semantic.js +273 -57
  160. package/dist/src/modules/history/semantic.js.map +1 -1
  161. package/dist/src/modules/workspace/answers.d.ts +22 -0
  162. package/dist/src/modules/workspace/answers.d.ts.map +1 -0
  163. package/dist/src/modules/workspace/answers.js +123 -0
  164. package/dist/src/modules/workspace/answers.js.map +1 -0
  165. package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
  166. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  167. package/dist/src/modules/workspace/branch-intent.js +6 -2
  168. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  169. package/dist/src/modules/workspace/direct-write.d.ts +184 -0
  170. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -0
  171. package/dist/src/modules/workspace/direct-write.js +417 -0
  172. package/dist/src/modules/workspace/direct-write.js.map +1 -0
  173. package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
  174. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  175. package/dist/src/modules/workspace/disk-agreement.js +48 -17
  176. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  177. package/dist/src/modules/workspace/edit-text.d.ts +27 -12
  178. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
  179. package/dist/src/modules/workspace/edit-text.js +61 -29
  180. package/dist/src/modules/workspace/edit-text.js.map +1 -1
  181. package/dist/src/modules/workspace/effects.d.ts +10 -0
  182. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  183. package/dist/src/modules/workspace/effects.js +21 -14
  184. package/dist/src/modules/workspace/effects.js.map +1 -1
  185. package/dist/src/modules/workspace/index.d.ts +308 -78
  186. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  187. package/dist/src/modules/workspace/index.js +2103 -720
  188. package/dist/src/modules/workspace/index.js.map +1 -1
  189. package/dist/src/modules/workspace/observe.d.ts +75 -63
  190. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  191. package/dist/src/modules/workspace/observe.js +366 -180
  192. package/dist/src/modules/workspace/observe.js.map +1 -1
  193. package/dist/src/modules/workspace/read-page.d.ts +41 -0
  194. package/dist/src/modules/workspace/read-page.d.ts.map +1 -0
  195. package/dist/src/modules/workspace/read-page.js +219 -0
  196. package/dist/src/modules/workspace/read-page.js.map +1 -0
  197. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  198. package/dist/src/modules/workspace/reconcile.js +26 -16
  199. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  200. package/dist/src/modules/workspace/result-text.d.ts +68 -0
  201. package/dist/src/modules/workspace/result-text.d.ts.map +1 -0
  202. package/dist/src/modules/workspace/result-text.js +94 -0
  203. package/dist/src/modules/workspace/result-text.js.map +1 -0
  204. package/dist/src/modules/workspace/sync.d.ts +9 -0
  205. package/dist/src/modules/workspace/sync.d.ts.map +1 -1
  206. package/dist/src/modules/workspace/sync.js +14 -0
  207. package/dist/src/modules/workspace/sync.js.map +1 -1
  208. package/dist/src/modules/workspace/text.d.ts +29 -12
  209. package/dist/src/modules/workspace/text.d.ts.map +1 -1
  210. package/dist/src/modules/workspace/text.js +35 -33
  211. package/dist/src/modules/workspace/text.js.map +1 -1
  212. package/dist/src/modules/workspace/types.d.ts +137 -60
  213. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  214. package/dist/src/modules/workspace/types.js.map +1 -1
  215. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  216. package/dist/src/modules/workspace/watcher.js +8 -3
  217. package/dist/src/modules/workspace/watcher.js.map +1 -1
  218. package/dist/src/prose-drafts.d.ts +4 -3
  219. package/dist/src/prose-drafts.d.ts.map +1 -1
  220. package/dist/src/prose-drafts.js +3 -2
  221. package/dist/src/prose-drafts.js.map +1 -1
  222. package/dist/src/reasoning-loss-notice.d.ts +23 -0
  223. package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
  224. package/dist/src/reasoning-loss-notice.js +50 -0
  225. package/dist/src/reasoning-loss-notice.js.map +1 -0
  226. package/dist/src/reasoning-loss.d.ts +24 -0
  227. package/dist/src/reasoning-loss.d.ts.map +1 -0
  228. package/dist/src/reasoning-loss.js +102 -0
  229. package/dist/src/reasoning-loss.js.map +1 -0
  230. package/dist/src/send-tools.d.ts +3 -2
  231. package/dist/src/send-tools.d.ts.map +1 -1
  232. package/dist/src/send-tools.js +3 -2
  233. package/dist/src/send-tools.js.map +1 -1
  234. package/dist/src/sent-rows.d.ts +59 -0
  235. package/dist/src/sent-rows.d.ts.map +1 -0
  236. package/dist/src/sent-rows.js +145 -0
  237. package/dist/src/sent-rows.js.map +1 -0
  238. package/dist/src/silent-heartbeat.d.ts +17 -10
  239. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  240. package/dist/src/silent-heartbeat.js.map +1 -1
  241. package/dist/src/stable-hash.d.ts +3 -0
  242. package/dist/src/stable-hash.d.ts.map +1 -0
  243. package/dist/src/stable-hash.js +15 -0
  244. package/dist/src/stable-hash.js.map +1 -0
  245. package/dist/src/stored-injections.d.ts +99 -0
  246. package/dist/src/stored-injections.d.ts.map +1 -0
  247. package/dist/src/stored-injections.js +106 -0
  248. package/dist/src/stored-injections.js.map +1 -0
  249. package/dist/src/text-as-stored.d.ts +4 -3
  250. package/dist/src/text-as-stored.d.ts.map +1 -1
  251. package/dist/src/text-as-stored.js +4 -3
  252. package/dist/src/text-as-stored.js.map +1 -1
  253. package/dist/src/thinking-drops.d.ts +74 -0
  254. package/dist/src/thinking-drops.d.ts.map +1 -0
  255. package/dist/src/thinking-drops.js +96 -0
  256. package/dist/src/thinking-drops.js.map +1 -0
  257. package/dist/src/tool-args.d.ts +12 -0
  258. package/dist/src/tool-args.d.ts.map +1 -1
  259. package/dist/src/tool-args.js +35 -1
  260. package/dist/src/tool-args.js.map +1 -1
  261. package/dist/src/tool-image-ledger.d.ts +9 -8
  262. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  263. package/dist/src/tool-image-ledger.js +9 -8
  264. package/dist/src/tool-image-ledger.js.map +1 -1
  265. package/dist/src/tool-presentation.d.ts.map +1 -1
  266. package/dist/src/tool-presentation.js +3 -1
  267. package/dist/src/tool-presentation.js.map +1 -1
  268. package/dist/src/tool-result-guard.d.ts +12 -6
  269. package/dist/src/tool-result-guard.d.ts.map +1 -1
  270. package/dist/src/tool-result-guard.js +44 -17
  271. package/dist/src/tool-result-guard.js.map +1 -1
  272. package/dist/src/tool-result-history.d.ts +176 -24
  273. package/dist/src/tool-result-history.d.ts.map +1 -1
  274. package/dist/src/tool-result-history.js +411 -91
  275. package/dist/src/tool-result-history.js.map +1 -1
  276. package/dist/src/tune-out/coordinator.d.ts +17 -9
  277. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  278. package/dist/src/tune-out/coordinator.js +84 -37
  279. package/dist/src/tune-out/coordinator.js.map +1 -1
  280. package/dist/src/tune-out/tools.d.ts +4 -1
  281. package/dist/src/tune-out/tools.d.ts.map +1 -1
  282. package/dist/src/types/agent.d.ts +217 -7
  283. package/dist/src/types/agent.d.ts.map +1 -1
  284. package/dist/src/types/events.d.ts +33 -9
  285. package/dist/src/types/events.d.ts.map +1 -1
  286. package/dist/src/types/framework.d.ts +39 -3
  287. package/dist/src/types/framework.d.ts.map +1 -1
  288. package/dist/src/types/module.d.ts +95 -9
  289. package/dist/src/types/module.d.ts.map +1 -1
  290. package/dist/src/types/trace.d.ts +91 -3
  291. package/dist/src/types/trace.d.ts.map +1 -1
  292. package/package.json +5 -4
  293. package/src/agent.ts +661 -67
  294. package/src/api/server.ts +5 -4
  295. package/src/auto-continue.ts +43 -0
  296. package/src/code-execution/isolate/scripts.ts +11 -3
  297. package/src/cut-off-round.ts +98 -0
  298. package/src/framework.ts +6168 -1540
  299. package/src/gate/event-gate.ts +2895 -405
  300. package/src/gate/index.ts +10 -1
  301. package/src/gate/recipe-reconcile.ts +355 -0
  302. package/src/gate/reply-judge.ts +512 -0
  303. package/src/gate/types.ts +234 -9
  304. package/src/heartbeat/heartbeat.ts +804 -0
  305. package/src/heartbeat/reminder-time.ts +107 -0
  306. package/src/index.ts +11 -4
  307. package/src/mcpl/capability-mask.ts +3 -2
  308. package/src/mcpl/channel-registry.ts +1222 -468
  309. package/src/mcpl/conversation-router.ts +34 -16
  310. package/src/mcpl/convert-block.ts +14 -1
  311. package/src/mcpl/hook-orchestrator.ts +33 -6
  312. package/src/mcpl/inbound-source.ts +42 -69
  313. package/src/mcpl/index.ts +6 -1
  314. package/src/mcpl/inference-router.ts +79 -34
  315. package/src/mcpl/prose-grammar.ts +7 -0
  316. package/src/mcpl/protocol-family.ts +2 -0
  317. package/src/mcpl/push-coalescer.ts +51 -13
  318. package/src/mcpl/reference-fetcher.ts +201 -162
  319. package/src/mcpl/references.ts +851 -77
  320. package/src/mcpl/server-connection.ts +189 -11
  321. package/src/mcpl/tags.ts +13 -2
  322. package/src/mcpl/tool-lifecycle.ts +21 -0
  323. package/src/mcpl/tool-presence.ts +716 -0
  324. package/src/mcpl/tool-result-normalize.ts +18 -6
  325. package/src/mcpl/tool-signature.ts +316 -0
  326. package/src/mcpl/toolset-host.ts +727 -0
  327. package/src/mcpl/toolsets-module.ts +545 -0
  328. package/src/mcpl/transport.ts +3 -2
  329. package/src/mcpl/types.ts +25 -2
  330. package/src/mcpl/visible-content.ts +22 -9
  331. package/src/module-registry.ts +85 -26
  332. package/src/modules/discord/index.ts +1 -1
  333. package/src/modules/history/index.ts +70 -30
  334. package/src/modules/history/semantic.ts +308 -54
  335. package/src/modules/workspace/answers.ts +133 -0
  336. package/src/modules/workspace/branch-intent.ts +10 -5
  337. package/src/modules/workspace/direct-write.ts +460 -0
  338. package/src/modules/workspace/disk-agreement.ts +52 -21
  339. package/src/modules/workspace/edit-text.ts +84 -35
  340. package/src/modules/workspace/effects.ts +22 -13
  341. package/src/modules/workspace/index.ts +2142 -723
  342. package/src/modules/workspace/observe.ts +381 -177
  343. package/src/modules/workspace/read-page.ts +228 -0
  344. package/src/modules/workspace/reconcile.ts +27 -15
  345. package/src/modules/workspace/result-text.ts +121 -0
  346. package/src/modules/workspace/sync.ts +16 -0
  347. package/src/modules/workspace/text.ts +58 -32
  348. package/src/modules/workspace/types.ts +137 -60
  349. package/src/modules/workspace/watcher.ts +8 -3
  350. package/src/prose-drafts.ts +4 -3
  351. package/src/reasoning-loss-notice.ts +62 -0
  352. package/src/reasoning-loss.ts +120 -0
  353. package/src/send-tools.ts +3 -2
  354. package/src/sent-rows.ts +162 -0
  355. package/src/silent-heartbeat.ts +17 -10
  356. package/src/stable-hash.ts +14 -0
  357. package/src/stored-injections.ts +131 -0
  358. package/src/text-as-stored.ts +4 -3
  359. package/src/thinking-drops.ts +135 -0
  360. package/src/tool-args.ts +34 -1
  361. package/src/tool-image-ledger.ts +9 -8
  362. package/src/tool-presentation.ts +3 -1
  363. package/src/tool-result-guard.ts +45 -17
  364. package/src/tool-result-history.ts +471 -90
  365. package/src/tune-out/coordinator.ts +80 -37
  366. package/src/tune-out/tools.ts +4 -1
  367. package/src/types/agent.ts +220 -7
  368. package/src/types/events.ts +33 -9
  369. package/src/types/framework.ts +36 -3
  370. package/src/types/module.ts +91 -9
  371. package/src/types/trace.ts +94 -3
@@ -20,14 +20,13 @@
20
20
  * retires).
21
21
  *
22
22
  * The ReferenceRegistry is the host-private reference record (§6.2): raw
23
- * URIs and testimony live here and never in model-visible content. Fetching
23
+ * URIs and testimony live here, and model-visible content shows a uri only
24
+ * as a tool result's link line shows one (shownLinkUri). Fetching
24
25
  * (§6/§7 origin binding, ceilings, redirects) is deliberately not here yet —
25
26
  * the registry is what makes a future host-mediated fetcher and the
26
27
  * code-execution materialization hook possible.
27
28
  */
28
29
 
29
- import { NON_STRING_TEXT_PLACEHOLDER } from './visible-content.js';
30
-
31
30
  export const REFERENCE_LIMITS = { uri: 4096, mimeType: 255, name: 255, expiresAt: 64 } as const;
32
31
  export const DIGEST_PATTERN = /^sha256:[A-Za-z0-9_-]{43}$/;
33
32
  const MAX_SAFE = 9007199254740991; // 2^53 - 1
@@ -49,15 +48,30 @@ export interface ReferenceTestimony {
49
48
  disposition?: ReferenceDisposition;
50
49
  rejectedFields: string[];
51
50
  truncatedFields: string[];
51
+ /** Set when the testimony is an MCP resource_link's, which claims no
52
+ * digest, expiry or disposition. A link is registered but never fetched
53
+ * on the side, and its testimony never replaces a record's (register). */
54
+ fromLink?: true;
52
55
  }
53
56
 
54
57
  export type BlockClassification =
55
58
  | { kind: 'text' }
56
59
  | { kind: 'inline'; contradiction: boolean }
57
60
  | { kind: 'reference'; testimony: ReferenceTestimony }
61
+ /** MCP's embedded resource (embeddedResourceContents): each lane shows
62
+ * its contents as it shows the block they make. */
63
+ | { kind: 'embedded'; contents: EmbeddedContents }
58
64
  /** `shown` is what the model-visible lanes put in the block's place. */
59
65
  | { kind: 'invalid'; shown: string };
60
66
 
67
+ /** What an embedded resource's contents are shown as: the ordinary block
68
+ * they make, withheld inline data, or a marker saying why they aren't
69
+ * shown. */
70
+ export type EmbeddedContents =
71
+ | { kind: 'block'; block: { type: 'text'; text: string } | { type: 'image' | 'audio'; data: string; mimeType: string } }
72
+ | { kind: 'withheld' }
73
+ | { kind: 'marker'; text: string };
74
+
61
75
  function isRecord(x: unknown): x is Record<string, unknown> {
62
76
  return typeof x === 'object' && x !== null && !Array.isArray(x);
63
77
  }
@@ -92,29 +106,72 @@ export function classifyBlock(block: unknown): BlockClassification {
92
106
  : { kind: 'invalid', shown: NON_STRING_TEXT_PLACEHOLDER };
93
107
  case 'image':
94
108
  case 'audio': {
95
- const hasData = typeof block.data === 'string';
96
- const hasUri = typeof block.uri === 'string';
97
- if (hasData) {
98
- // Inline form claiming bulk disposition (or smuggling a uri beside
109
+ // An empty uri is no link, as the push lane's converter reads it.
110
+ const hasLink = typeof block.uri === 'string' && block.uri !== '';
111
+ if (typeof block.data === 'string') {
112
+ // Inline form claiming bulk disposition (or smuggling a link beside
99
113
  // the data) is the vector-2 contradiction: fail closed, withhold.
100
- // A uri of '' counts here too, though it's no link: until the
101
- // tool-result lane gives every inline shape a marker of its own,
102
- // showing that data would put some of it into history as raw JSON.
103
- // Once that lane does, an empty link beside data reads as no link.
104
- return { kind: 'inline', contradiction: block.disposition !== undefined || hasUri };
114
+ return { kind: 'inline', contradiction: block.disposition !== undefined || hasLink };
105
115
  }
106
- // With no data, an empty uri is no link, as the push lane's converter
107
- // reads it.
108
- if (hasUri && block.uri !== '') return parseReferenceFields(block);
116
+ if (hasLink) return parseReferenceFields(block);
109
117
  return { kind: 'invalid', shown: block.type === 'image' ? IMAGE_NO_DATA_TEXT : AUDIO_NO_DATA_TEXT };
110
118
  }
111
- case 'resource':
112
- return parseReferenceFields(block);
119
+ case 'resource': {
120
+ const contents = embeddedResourceContents(block);
121
+ return contents ? { kind: 'embedded', contents } : parseReferenceFields(block);
122
+ }
123
+ case 'resource_link':
124
+ // MCP's link to a resource is a reference too. RFC-005 §8.1 maps it
125
+ // onto the reference record field for field, its `size` as `sizeBytes`,
126
+ // with no disposition: the host's default policy, as for any reference
127
+ // that claims none. Its title, description and annotations aren't
128
+ // testimony.
129
+ // A link's size is MCP's `size`; the script lane's object for a link
130
+ // names it `sizeBytes`, so given again it reads the same, where
131
+ // sanitizeLabel left its uri as it was. Read on every lane, so a
132
+ // peer's link that names a `sizeBytes` is labelled with it too.
133
+ return linkTestimony(parseReferenceFields({ uri: block.uri, name: block.name, mimeType: block.mimeType, sizeBytes: block.size ?? block.sizeBytes }));
113
134
  default:
114
135
  return { kind: 'invalid', shown: unknownBlockText(block) };
115
136
  }
116
137
  }
117
138
 
139
+ /**
140
+ * MCP's EmbeddedResource: a `resource` block with no uri of its own whose
141
+ * `resource` is a record with a string `uri`. RFC-005 §8.1 reads it as "an
142
+ * ordinary inline block, nothing new", so its contents show as the block
143
+ * they make: with exactly one of a string `text` or a string `blob`
144
+ * (base64), its text as text, and its blob as the inline image or audio its
145
+ * media type makes it, or as a marker for any other type; with both or
146
+ * neither, the marker saying its contents can't be read. Its uri is never shown, since
147
+ * no lane RFC-005 governs showed it before and nothing here can tell a
148
+ * capability in a uri from a location. A disposition on it is inline
149
+ * content claiming bulk disposition (RFC-005 vector 2), so it's withheld.
150
+ * Null for any other block, among them MCPL's reference, whose uri is its
151
+ * own.
152
+ */
153
+ function embeddedResourceContents(block: Record<string, unknown>): EmbeddedContents | null {
154
+ if (block.uri !== undefined && block.uri !== null && block.uri !== '') return null;
155
+ if (!isRecord(block.resource) || typeof block.resource.uri !== 'string') return null;
156
+ const { text, blob, mimeType } = block.resource;
157
+ if (block.disposition !== undefined) return { kind: 'withheld' };
158
+ if ((typeof text === 'string') === (typeof blob === 'string')) return { kind: 'marker', text: EMBEDDED_RESOURCE_UNREADABLE_TEXT };
159
+ if (typeof text === 'string') return { kind: 'block', block: { type: 'text', text } };
160
+ const data = blob as string;
161
+ const type = typeof mimeType === 'string' ? mimeType : '';
162
+ // image/* or audio/*, with a subtype: a bare `image` is no media type a
163
+ // block can carry, so it reads as any other type does.
164
+ const family = /^(image|audio)\/[^/]/i.exec(type)?.[1]?.toLowerCase() as 'image' | 'audio' | undefined;
165
+ if (family) return { kind: 'block', block: { type: family, data, mimeType: type } };
166
+ if (type === '') return { kind: 'marker', text: EMBEDDED_RESOURCE_NO_MEDIA_TYPE_TEXT };
167
+ return { kind: 'marker', text: embeddedBinaryText(type, data) };
168
+ }
169
+
170
+ /** A link's classification, its testimony marked as a link's (fromLink). */
171
+ function linkTestimony(c: BlockClassification): BlockClassification {
172
+ return c.kind === 'reference' ? { kind: 'reference', testimony: { ...c.testimony, fromLink: true } } : c;
173
+ }
174
+
118
175
  function parseReferenceFields(block: Record<string, unknown>): BlockClassification {
119
176
  if (typeof block.uri !== 'string' || block.uri.length === 0) {
120
177
  return { kind: 'invalid', shown: REFERENCE_NO_LINK_TEXT };
@@ -170,6 +227,30 @@ export const AUDIO_NO_MEDIA_TYPE_TEXT = '[Audio not shown: its data arrived with
170
227
  * inline data, so a plain link isn't enough either. (A tool result's audio
171
228
  * with a link is a reference, as any block with one is there.) */
172
229
  export const AUDIO_NO_DATA_TEXT = '[Audio not shown: it arrived with no usable inline data.]';
230
+ /** MCP's embedded resource whose contents are neither text nor a blob, or
231
+ * both. */
232
+ export const EMBEDDED_RESOURCE_UNREADABLE_TEXT = '[Resource not shown: its contents arrived as neither text nor data, or as both.]';
233
+ /** MCP's embedded resource whose blob has no media type, so it can't be
234
+ * shown as an image or audio. */
235
+ export const EMBEDDED_RESOURCE_NO_MEDIA_TYPE_TEXT = '[Resource not shown: its data arrived without a media type.]';
236
+
237
+ /** MCP's embedded resource whose blob is of a type no lane can show: what
238
+ * it was, by its type and size. Made well formed (a lone UTF-16 surrogate
239
+ * becomes U+FFFD), as a stub is (#695), since it names the block's media
240
+ * type, which sanitizeLabel leaves as it is: every lane then shows the same
241
+ * text. */
242
+ export function embeddedBinaryText(mimeType: string, blob: string): string {
243
+ const what = [referenceTypeLabel(mimeType), formatSizeBytes(Math.floor(blob.length * 3 / 4))].filter(Boolean).join(', ');
244
+ return `[Resource not shown: this host can't display its data (${what}).]`.toWellFormed();
245
+ }
246
+ /**
247
+ * What the converter (convertBlock, on every lane) shows for a text
248
+ * block whose text isn't a string, which only a nonconformant sender writes,
249
+ * so the agent learns that a block was there. It is the host's word, not the
250
+ * sender's, so it never makes content visible (isVisiblyEmptyConversion in
251
+ * visible-content.ts, which re-exports it).
252
+ */
253
+ export const NON_STRING_TEXT_PLACEHOLDER = '[Text not shown: a text part arrived without readable text.]';
173
254
  export const REFERENCE_NO_LINK_TEXT = '[Reference not shown: it arrived without a link.]';
174
255
  export const REFERENCE_LINK_TOO_LONG_TEXT = '[Reference not shown: its link is longer than this host accepts.]';
175
256
  const UNREADABLE_BLOCK_TEXT = "[Content not shown: a part arrived in a form this host can't read.]";
@@ -222,22 +303,349 @@ export const EXPIRY_REASON = {
222
303
  * only through a server that delivered the reference. */
223
304
  export const UNBOUND_REASON = "not tied to any server, so there's nowhere to fetch it from";
224
305
 
225
- /** The fetcher's refusal for a server it has no address to dial, one the
226
- * host doesn't connect to over a network, such as a stdio server, as a
227
- * clause that completes "couldn't fetch <ref>: " and says so of the server;
228
- * the fetcher dials only through a server's address. save_recent_image gives
229
- * it too. A stub can't know it, since only the framework holds its servers'
230
- * configs. */
306
+ /** The refusal for a server the host has no address to dial, one it doesn't
307
+ * connect to over a network, such as a stdio server, as a clause that
308
+ * completes "couldn't fetch <ref>: " and says so of the server: the fetcher
309
+ * dials only through a server's address. A reference's stub, fetch_reference
310
+ * and save_recent_image all give it (dialCheck). */
231
311
  export function noAddressReason(serverId: string): string {
232
312
  return `this host doesn't connect to the "${serverId}" server over a network, so there's nowhere to fetch it from`;
233
313
  }
234
314
 
235
- /** Strip control characters (C0, DEL, C1) and every bidi control (marks,
236
- * embeddings, overrides, isolates, and the Arabic letter mark), trim the
237
- * whitespace (a BOM included) left at either end, and bound the length.
238
- * Labels are labels — storage naming is host-generated elsewhere (§7.3). */
315
+ /** A server's id as the agent reads it in a stub or a refusal: as a label
316
+ * is, bounded and with its controls removed. */
317
+ export function shownServerId(serverId: string): string {
318
+ return sanitizeLabel(serverId, STUB_FIELD_CHARS);
319
+ }
320
+
321
+ /**
322
+ * Query and fragment parameters that carry a credential, by name, compared
323
+ * by their letters and digits alone, without letter case, so `access_token`,
324
+ * `accessToken`, `access-token` and `access_token[]` are one name. RFC-005 §5: "Signed URLs and query capabilities are
325
+ * bearer credentials that look like locations", so a uri is shown only where
326
+ * the host's policy classifies it as non-capability-bearing, which here is
327
+ * one with no userinfo and none of these parameters, nor any uri in a
328
+ * parameter's value that has them (uriMayCarryCredential).
329
+ * For example:
330
+ * - AWS's signed URLs: `https://b.s3.amazonaws.com/k?X-Amz-Algorithm=…&X-Amz-Credential=…&X-Amz-Security-Token=…&X-Amz-Signature=…`
331
+ * - Google Cloud Storage's: `https://storage.googleapis.com/b/k?X-Goog-Algorithm=…&X-Goog-Credential=…&X-Goog-Signature=…`
332
+ * - Azure's shared access signatures: `https://a.blob.core.windows.net/c/b?sv=…&se=…&sp=r&sig=…`
333
+ * - CloudFront's: `https://d1.cloudfront.net/k?Expires=…&Signature=…&Key-Pair-Id=…`, or `?Policy=…&Signature=…&Key-Pair-Id=…`
334
+ * - Discord's CDN: `https://cdn.discordapp.com/attachments/1/2/f.png?ex=…&is=…&hm=…`
335
+ * - Backblaze B2's: `https://f000.backblazeb2.com/file/b/k?Authorization=…`
336
+ * - OAuth's: `#access_token=…&token_type=bearer`, `#id_token=…`, `?refresh_token=…`, `?client_secret=…`,
337
+ * and OAuth 1.0a's `?oauth_token=…&oauth_signature=…`
338
+ * - the generic: `?token=…`, `?auth=…`, `?auth_token=…`, `?key=…`, `?apikey=…`, `?api_key=…`, `?api_token=…`,
339
+ * `?x-api-key=…`, `?private_token=…`, `?secret=…`, `?password=…`, `?signature=…`
340
+ * - a session's id, a bearer credential too: `?session_id=…`, `?jsessionid=…` (in a path, as
341
+ * `;jsessionid=…` usually rides, it isn't read)
342
+ * A credential named otherwise, or carried in a path, such as a webhook's
343
+ * `https://discord.com/api/webhooks/<id>/<token>`, isn't caught.
344
+ */
345
+ const CREDENTIAL_PARAMETERS: ReadonlySet<string> = new Set([
346
+ 'x-amz-signature', 'x-amz-credential', 'x-amz-security-token',
347
+ 'x-goog-signature', 'x-goog-credential',
348
+ 'sig',
349
+ 'signature', 'key-pair-id', 'policy',
350
+ 'hm',
351
+ 'authorization',
352
+ 'access_token', 'id_token', 'refresh_token', 'client_secret',
353
+ 'oauth_token', 'oauth_signature',
354
+ 'token', 'auth', 'auth_token', 'key', 'api_key', 'api_token', 'x-api-key', 'private_token', 'secret', 'password',
355
+ 'session_id', 'jsessionid',
356
+ ].map(credentialName));
357
+
358
+ /** A parameter's name as CREDENTIAL_PARAMETERS compares it: its letters and
359
+ * digits alone, in lower case. */
360
+ function credentialName(name: string): string {
361
+ return name.toLowerCase().replace(/[^a-z0-9]/g, '');
362
+ }
363
+
364
+ /**
365
+ * Whether a uri may carry a credential: userinfo, or a query or fragment
366
+ * parameter named on CREDENTIAL_PARAMETERS with a value, a fragment read as
367
+ * `name=value&…` as a query is. A name with no value carries nothing, as an
368
+ * anchor (`#token`) or `?token=` doesn't. A `?` or `;` inside either may
369
+ * separate parameters too, as in a route kept in a fragment
370
+ * (`#/reset?token=…`), so the parameters are read both with and without them
371
+ * as separators, and one with a value in either reading counts. A
372
+ * parameter's value that is itself a uri is read the same way
373
+ * (nestedMayCarry): an absolute one, as an image proxy's `?url=<a
374
+ * percent-encoded signed URL>` is once decoded; a protocol-relative one, or
375
+ * a relative one with a query or fragment, as a login's
376
+ * `?next=%2Freset%3Ftoken%3D…` is; and one encoded again inside the value,
377
+ * as a redirector that encodes an already encoded url leaves it, each read
378
+ * and each decode a level of its own. That goes up to CREDENTIAL_NESTING
379
+ * levels deep, and a uri reached deeper may carry one, since a bound that
380
+ * let the next level through would be the same gap one level down. A uri
381
+ * carried in another encoding, such as base64, isn't read. A uri that
382
+ * doesn't parse, even as a relative reference, may, since its parts can't be
383
+ * read.
384
+ */
385
+ export function uriMayCarryCredential(uri: string): boolean {
386
+ let u: URL;
387
+ try {
388
+ // Absolute first: against an http base, `http:user:pass@host` would read
389
+ // as a relative path and hide its userinfo.
390
+ u = new URL(uri);
391
+ } catch {
392
+ try {
393
+ u = new URL(uri, RELATIVE_BASE);
394
+ } catch {
395
+ return true;
396
+ }
397
+ }
398
+ return urlMayCarryCredential(u, 0);
399
+ }
400
+
401
+ /** The base a relative uri is read against: it supplies only a scheme and a
402
+ * path, and a credential lives in the uri's own authority, query or
403
+ * fragment. */
404
+ const RELATIVE_BASE = 'http://relative.invalid/';
405
+
406
+ /** How deep uriMayCarryCredential reads uris nested in parameters' values,
407
+ * each one a parameter's value in the uri before it. */
408
+ const CREDENTIAL_NESTING = 4;
409
+
410
+ function urlMayCarryCredential(u: URL, depth: number): boolean {
411
+ if (u.username !== '' || u.password !== '') return true;
412
+ // Read both ways: with '&' alone separating parameters, as a server
413
+ // reading the query as WHATWG does sees them, and with '?' and ';' too, so
414
+ // a value that opens with one (`token=;…`) still counts in the first.
415
+ const named = (params: string) => [params, params.replace(/[?;]/g, '&')].some((read) =>
416
+ [...new URLSearchParams(read).entries()].some(([name, value]) => value !== ''
417
+ && (CREDENTIAL_PARAMETERS.has(credentialName(name)) || nestedMayCarry(value, depth))));
418
+ return named(u.search) || named(u.hash.slice(1));
419
+ }
420
+
421
+ /** Whether a parameter's value is a uri that may carry a credential, read in
422
+ * this order: as an absolute uri; else, where its form is a relative
423
+ * reference's (it starts with `/` or `\`, or holds `?` or `#`), resolved
424
+ * against RELATIVE_BASE, never against the uri it sits in, which may have
425
+ * no path to resolve against, as a `mailto:` hasn't; else, where decoding
426
+ * it once more changes it, decoded and read again from the start. Each read
427
+ * or decode is a level, and a decode is lenient, as the parameter's own
428
+ * was, so a stray `%` doesn't stop it. A value that is none of these, such
429
+ * as a word, answers false; within the bound a decode alone never
430
+ * withholds, and past it a value that would still decode does. */
431
+ function nestedMayCarry(value: string, depth: number): boolean {
432
+ let inner: URL | null = null;
433
+ try {
434
+ inner = new URL(value);
435
+ } catch {
436
+ // Its form as the URL parser reads it, which first trims C0 controls and
437
+ // spaces, so `\t//user:pass@…` is the protocol-relative uri it resolves
438
+ // as. The tabs and newlines it also drops can't change this form once the
439
+ // lead is trimmed: none is a `/`, `\\`, `?` or `#`.
440
+ if (/^[\u0000-\u0020]*[/\\]|[?#]/.test(value)) {
441
+ try {
442
+ inner = new URL(value, RELATIVE_BASE);
443
+ } catch {
444
+ // Against a hierarchical base only a value that is protocol-relative
445
+ // as the parser reads it (`//`, or `\\` as a special scheme reads it)
446
+ // fails to resolve, on its authority, and it may carry one, as a uri
447
+ // that doesn't parse may: its authority can't be read. So an
448
+ // ordinary value such as `q=// note` is withheld too.
449
+ return true;
450
+ }
451
+ }
452
+ }
453
+ if (inner !== null) return depth >= CREDENTIAL_NESTING || urlMayCarryCredential(inner, depth + 1);
454
+ // Decoded as the parameter itself was, leniently: a stray `%` stays as it
455
+ // is rather than stopping the reading, as it doesn't stop a server's.
456
+ // `+` and `&` are escaped first, so they come back as themselves.
457
+ const decoded = new URLSearchParams(`v=${value.replace(/[+&]/g, encodeURIComponent)}`).get('v') ?? value;
458
+ if (decoded === value) return false;
459
+ // Past the bound, a value that would still decode is withheld: the check's
460
+ // work stops where its reading does, and a uri found deeper would be.
461
+ return depth >= CREDENTIAL_NESTING || nestedMayCarry(decoded, depth + 1);
462
+ }
463
+
464
+ /** How the host reaches a server, as far as fetching its references goes:
465
+ * the WebSocket url it dials an MCPL server at, or none: for a server it
466
+ * spawns, such as a stdio one, or a modern MCP server over HTTP, whose
467
+ * results carry no references (RFC-005 governs MCPL peers' alone), or one
468
+ * whose url the transport won't dial. A framework records each of its
469
+ * servers' in the registry (ReferenceRegistry.setServerAddress), where a
470
+ * stub reads it; its fetcher reads the same from the framework's own
471
+ * configs, beside each server's credential. */
472
+ export type ServerAddress = { url: string } | 'no-address';
473
+
474
+ /** Why a fetch of a reference failed, or would be refused before it dials. */
475
+ export interface Refusal {
476
+ /** What happened, as a clause that completes "couldn't fetch <ref>: ",
477
+ * whatever it's about: fetch_reference's sentence, save_recent_image's,
478
+ * and the eager and script-demand fetches' log lines read it. */
479
+ error: string;
480
+ /** What calling again can do, as a sentence. fetch_reference and
481
+ * save_recent_image show it; the eager and script-demand fetches log the
482
+ * error alone, and a reference's stub never shows it. */
483
+ remedy: string;
484
+ /** How the reference's stub words it, where the clause names parts of the
485
+ * uri the stub never shows. A stub shows this or the clause. */
486
+ inStub?: string;
487
+ }
488
+
489
+ // What calling again can do, for the refusals a reference's stub shares with
490
+ // a fetch; the fetcher has these and its own. None names a tool: a fetch
491
+ // doesn't know where its reference came from.
492
+ export const NOTHING_TO_DO = "Calling again won't change that.";
493
+ /** The registry keys a record by its uri, so a fresh reference to the same
494
+ * uri arrives as this very id and refreshes its record. */
495
+ export const FRESH_REFERENCE = "Once a fresh reference to it arrives, fetching may work; until then, fetching it again won't.";
496
+ /** A server's address is read from the host's configs as they are at each
497
+ * fetch. */
498
+ export const SERVER_BACK = "Calling again won't change that unless whoever runs this host adds that server back.";
499
+ const SERVER_ADDRESS = "Calling again won't change that unless whoever runs this host corrects the server's address.";
500
+
501
+ /** ws→http / wss→https with default ports elided, for origin comparison. */
502
+ export function normalizedOrigin(url: string): string | null {
503
+ try {
504
+ const u = new URL(url);
505
+ const scheme = u.protocol === 'ws:' ? 'http:' : u.protocol === 'wss:' ? 'https:' : u.protocol;
506
+ const defaultPort = scheme === 'https:' ? '443' : '80';
507
+ const port = u.port === '' || u.port === defaultPort ? '' : `:${u.port}`;
508
+ return `${scheme}//${u.hostname.toLowerCase()}${port}`;
509
+ } catch {
510
+ return null;
511
+ }
512
+ }
513
+
514
+ /** A server's address as it's configured, its scheme and host, for a
515
+ * clause: never its userinfo, path or query. URL's own origin is 'null' for
516
+ * a scheme that isn't special, which normalizedOrigin accepts. The caller
517
+ * has already parsed it. */
518
+ function configuredOrigin(url: string): string {
519
+ const u = new URL(url);
520
+ return `${u.protocol}//${u.host}`;
521
+ }
522
+
523
+ /** The §6.1/§7.1 policy check of a uri against the url a server is dialed
524
+ * at: null where a fetch through that server may fetch it. */
525
+ function originRefusal(uri: string, serverUrl: string, serverId: string): Refusal | null {
526
+ const refOrigin = normalizedOrigin(uri);
527
+ if (!refOrigin) return { error: "its address doesn't parse as a URL", remedy: NOTHING_TO_DO };
528
+ // A server's address that doesn't parse leaves nothing to dial. The
529
+ // framework's resolver gives only an address its transport dials, so this
530
+ // guards other resolvers.
531
+ const dialedOrigin = normalizedOrigin(serverUrl);
532
+ if (!dialedOrigin) {
533
+ return {
534
+ error: `the "${shownServerId(serverId)}" server's address doesn't parse as a URL, so there's nowhere to fetch it from`,
535
+ remedy: SERVER_ADDRESS,
536
+ };
537
+ }
538
+ const refUrl = new URL(uri);
539
+ // A user name or password in the uri is a credential embedded in it,
540
+ // which RFC-005 §6.1 forbids a server to mint and lets a host refuse
541
+ // (vector 6). This host neither sends nor repeats it: fetch refuses such
542
+ // a url with a message that holds it whole.
543
+ if (refUrl.username !== '' || refUrl.password !== '') {
544
+ return { error: "its address carries a user name or password, which this host doesn't send", remedy: NOTHING_TO_DO };
545
+ }
546
+ // The uri's own scheme, not its origin's: an origin reads ws: as http:
547
+ // and wss: as https:, and fetch refuses a ws: or wss: uri ('unknown
548
+ // scheme') before connecting, which would read as a failed request a
549
+ // later try can mend.
550
+ const scheme = refUrl.protocol;
551
+ if (scheme !== 'https:' && scheme !== 'http:') {
552
+ return {
553
+ error: `its address uses ${sanitizeLabel(scheme, STUB_FIELD_CHARS)}, and this host fetches only http: and https: addresses`,
554
+ remedy: NOTHING_TO_DO,
555
+ inStub: 'scheme not allowed: this host fetches only http and https uris',
556
+ };
557
+ }
558
+ // Origins compare with their schemes, so this also refuses an http:
559
+ // reference from a server dialed over TLS (wss://).
560
+ if (refOrigin !== dialedOrigin) {
561
+ return {
562
+ error: `it points to ${refOrigin}, not to the "${shownServerId(serverId)}" server's own address (${configuredOrigin(serverUrl)}), ` +
563
+ 'and this host fetches only from the server itself',
564
+ remedy: NOTHING_TO_DO,
565
+ inStub: "third-party origin: it isn't on its server's own address, so this host doesn't fetch it",
566
+ };
567
+ }
568
+ return null;
569
+ }
570
+
571
+ /**
572
+ * The server a fetch of a reference would dial: its servers, in the order
573
+ * they delivered it, the first whose address is on the uri's origin (§6.2).
574
+ * When none is, the first server's refusal (unknown to this host, with no
575
+ * address, or the origin check's), and for a record no server delivered,
576
+ * that it's unbound, which a later delivery of the uri by a server mends.
577
+ */
578
+ export function dialServer(
579
+ record: ReferenceRecord,
580
+ addressOf: (serverId: string) => ServerAddress | undefined,
581
+ ): { serverId: string } | { refusal: Refusal } {
582
+ let refusal: Refusal | undefined;
583
+ let dialed: string | undefined;
584
+ for (const serverId of record.serverIds) {
585
+ const address = addressOf(serverId);
586
+ if (address === undefined) {
587
+ refusal ??= { error: `this host no longer has the "${shownServerId(serverId)}" server it came from`, remedy: SERVER_BACK };
588
+ } else if (address === 'no-address') {
589
+ refusal ??= { error: noAddressReason(shownServerId(serverId)), remedy: NOTHING_TO_DO };
590
+ } else {
591
+ const offOrigin = originRefusal(record.testimony.uri, address.url, serverId);
592
+ if (offOrigin === null) {
593
+ dialed = serverId;
594
+ break;
595
+ }
596
+ refusal ??= offOrigin;
597
+ }
598
+ }
599
+ return dialed === undefined
600
+ ? { refusal: refusal ?? { error: `it's ${UNBOUND_REASON}`, remedy: FRESH_REFERENCE } }
601
+ : { serverId: dialed };
602
+ }
603
+
604
+ /**
605
+ * Where a fetch of a reference would dial, or why it would be refused before
606
+ * dialing, whatever the fetch: dialServer's server, then the reference's
607
+ * expiry. A pure function of the record and the servers' addresses: a
608
+ * reference's stub asks it of the registry's (unfetchableReason), and the
609
+ * fetcher's pre-dial check of its own framework's configs, whose credential
610
+ * goes with the address it checks. In a process with one running framework,
611
+ * or with running frameworks that agree on each id they share, the two
612
+ * sources hold the same addresses, so a stub promises the server the fetch
613
+ * dials. A stub reads an expiry with its result's claims, and the fetch with
614
+ * the record's latest testimony, so where one result names a uri both
615
+ * expired and fresh, its stub says expired while a fetch can succeed. A
616
+ * stopped framework's addresses are out of the registry, so its stubs read
617
+ * another running framework's entry for the same id where there is one, and
618
+ * otherwise say the server is unknown, while its configs still resolve it.
619
+ * Where two
620
+ * frameworks configure one id differently, only a stub's words can be wrong,
621
+ * never where a token goes. A fetch's own limits, its budget and ceiling,
622
+ * come after it.
623
+ */
624
+ export function dialCheck(
625
+ record: ReferenceRecord,
626
+ addressOf: (serverId: string) => ServerAddress | undefined,
627
+ nowMs = Date.now(),
628
+ /** For a stub or a script's object, the claims of the result it's shown
629
+ * in, which decide where its references name the record's uri, the
630
+ * record's own testimony deciding otherwise (claimedExpiry). A fetch
631
+ * reads the record alone. */
632
+ claims?: ResultClaims,
633
+ ): { serverId: string } | { refusal: Refusal } {
634
+ const server = dialServer(record, addressOf);
635
+ if ('refusal' in server) return server;
636
+ const expiry = claimedExpiry(record, claims, nowMs);
637
+ return expiry ? { refusal: { error: `the reference is ${EXPIRY_REASON[expiry]}`, remedy: FRESH_REFERENCE } } : server;
638
+ }
639
+
640
+ /** Strip control characters (C0, DEL, C1), every bidi control (marks,
641
+ * embeddings, overrides, isolates, and the Arabic letter mark) and the line
642
+ * and paragraph separators (U+2028, U+2029), which a regex reads as line
643
+ * ends, so a label can't start a line of its own, such as one the image
644
+ * ledger would read as a slot. Then trim the whitespace (a BOM included)
645
+ * left at either end, and bound the length. Labels are labels — storage
646
+ * naming is host-generated elsewhere (§7.3). */
239
647
  export function sanitizeLabel(s: string, maxChars: number): string {
240
- const cleaned = s.replace(/[\p{Cc}\p{Bidi_Control}]/gu, '').trim();
648
+ const cleaned = s.replace(/[\p{Cc}\p{Bidi_Control}\u2028\u2029]/gu, '').trim();
241
649
  if (cleaned.length <= maxChars) return cleaned;
242
650
  let cut = cleaned.slice(0, Math.max(0, maxChars - 1));
243
651
  // never split a surrogate pair at the truncation boundary
@@ -271,6 +679,13 @@ export interface ReferenceRecord {
271
679
  verifiedBytes?: number;
272
680
  verifiedMimeType?: string;
273
681
  digestVerified?: boolean;
682
+ /** Set when the eager default's fetch of it failed, so the eager default
683
+ * doesn't choose it again; fetch_reference still fetches it. A refreshed
684
+ * testimony leaves it set, since a server can re-issue a uri at every
685
+ * result, and it lasts as long as the record. A record fetched later, by a
686
+ * save that landed after the eager pass stopped waiting or by
687
+ * fetch_reference, can carry it too: fetchedPath is read first. */
688
+ eagerFailed?: boolean;
274
689
  }
275
690
 
276
691
  const MAX_RECORDS = 2000;
@@ -290,7 +705,7 @@ const MAX_RECORDS = 2000;
290
705
  export class ReferenceRegistry {
291
706
  private records = new Map<string, ReferenceRecord>();
292
707
  /** uri → refId. The uri ALONE is the identity key: a tool result's stubs
293
- * (tool-result-history.ts's, and framework.ts's tryNativeToolResultContent)
708
+ * (tool-result-history.ts's, which both copies of a tool result take)
294
709
  * and a script's demand fetch register with no server context,
295
710
  * while the registrations that know the server carry its id: a tool
296
711
  * result's, before it's shown, whether the agent's dispatch or an
@@ -307,8 +722,51 @@ export class ReferenceRegistry {
307
722
  * its own origin, for a uri it delivered. */
308
723
  private byKey = new Map<string, string>();
309
724
  private counter = 0;
725
+ /** Each open journal's copies (openJournal), by uri. */
726
+ private journals = new Set<Map<string, ReferenceRecord | null>>();
727
+
728
+ /**
729
+ * Keep, until `close`, each record as it stood when this opened: a copy
730
+ * taken the first time a registration names its uri, whatever it changes,
731
+ * or evicts its record, or null where the uri had none then. `before(uri)`
732
+ * gives that copy, or, where no registration has named the uri since, the
733
+ * record as it is, which is the same. A fetch that saves a copy while it's
734
+ * open sets its record's saved path without registering: a record no
735
+ * registration has named since is read with it, and one a registration
736
+ * named first is read as it stood then, saved or not. For a record a fetch
737
+ * saved meanwhile, both readings are of one the host can fetch, since a
738
+ * fetch saves only what passed the checks a link's line reads too, except
739
+ * where two frameworks configure one server id differently, which a stub's
740
+ * words can't tell apart either (setServerAddress). Each
741
+ * open has its own copies, so lanes that overlap keep theirs apart, and
742
+ * closing one lets its copies go.
743
+ */
744
+ openJournal(): RegistryJournal {
745
+ const copies = new Map<string, ReferenceRecord | null>();
746
+ this.journals.add(copies);
747
+ return {
748
+ before: (uri) => (copies.has(uri) ? copies.get(uri) ?? undefined : this.findByUri(uri)),
749
+ close: () => { this.journals.delete(copies); },
750
+ };
751
+ }
752
+
753
+ /** Copy a uri's record into each open journal that has none for it yet,
754
+ * a copy of its own for each. */
755
+ private keepFirst(uri: string): void {
756
+ if (this.journals.size === 0) return;
757
+ const record = this.findByUri(uri);
758
+ for (const copies of this.journals) {
759
+ if (!copies.has(uri)) copies.set(uri, record ? { ...record, serverIds: [...record.serverIds] } : null);
760
+ }
761
+ }
762
+
763
+ /** How many journals are open: none once every lane has closed its own. */
764
+ get journalCount(): number {
765
+ return this.journals.size;
766
+ }
310
767
 
311
768
  register(testimony: ReferenceTestimony, serverId?: string): ReferenceRecord {
769
+ this.keepFirst(testimony.uri);
312
770
  const existingId = this.byKey.get(testimony.uri);
313
771
  if (existingId !== undefined) {
314
772
  const existing = this.records.get(existingId);
@@ -319,8 +777,9 @@ export class ReferenceRegistry {
319
777
  // Refresh testimony while unfetched: a re-issued reference with a new
320
778
  // digest/expiry supersedes the stale claims. Once fetched, the record
321
779
  // describes verified bytes on disk and keeps the testimony they were
322
- // verified against.
323
- if (!existing.fetchedPath) existing.testimony = testimony;
780
+ // verified against. A link claims no digest, expiry or disposition,
781
+ // so its testimony never replaces a record's claims.
782
+ if (!existing.fetchedPath && !testimony.fromLink) existing.testimony = testimony;
324
783
  // Newest again, so a uri just mentioned is the last to be evicted.
325
784
  this.records.delete(existingId);
326
785
  this.records.set(existingId, existing);
@@ -335,6 +794,7 @@ export class ReferenceRegistry {
335
794
  const oldest = this.records.keys().next().value;
336
795
  if (oldest !== undefined) {
337
796
  const old = this.records.get(oldest);
797
+ if (old) this.keepFirst(old.testimony.uri);
338
798
  this.records.delete(oldest);
339
799
  if (old) this.byKey.delete(old.testimony.uri);
340
800
  }
@@ -346,6 +806,38 @@ export class ReferenceRegistry {
346
806
  return this.records.get(refId);
347
807
  }
348
808
 
809
+ /** How the host reaches each server, by id, as each framework in the process
810
+ * configured it, which a reference's stub reads (unfetchableReason). A
811
+ * fetch reads its own framework's configs instead, so a server's token only
812
+ * ever goes to the origin its own config names. Keyed by id as a record's
813
+ * servers are: where two frameworks configure one id, the latest entry is
814
+ * in force, and a stub's words can be wrong for the other one's fetch. */
815
+ private addresses = new Map<string, Array<{ owner: object; address: ServerAddress }>>();
816
+
817
+ /** Record how the host reaches a server, as a framework (`owner`) does for
818
+ * each server it configures. Its entry for the id is then in force. */
819
+ setServerAddress(serverId: string, address: ServerAddress, owner: object): void {
820
+ const entries = (this.addresses.get(serverId) ?? []).filter((e) => e.owner !== owner);
821
+ entries.push({ owner, address });
822
+ this.addresses.set(serverId, entries);
823
+ }
824
+
825
+ /** Take back a framework's entry for a server, as it does for a server it
826
+ * removes, and for each of its servers when it stops. Another framework's
827
+ * entry for the id, if any, is in force again; with none, a stub reads the
828
+ * id's references as an unknown server's. After a removal, that framework's
829
+ * fetch refuses them too. A stopped framework keeps its configs, so its
830
+ * fetcher, called before it starts again, can still dial them. */
831
+ forgetServerAddress(serverId: string, owner: object): void {
832
+ const entries = (this.addresses.get(serverId) ?? []).filter((e) => e.owner !== owner);
833
+ if (entries.length > 0) this.addresses.set(serverId, entries);
834
+ else this.addresses.delete(serverId);
835
+ }
836
+
837
+ serverAddress(serverId: string): ServerAddress | undefined {
838
+ return this.addresses.get(serverId)?.at(-1)?.address;
839
+ }
840
+
349
841
  /** Find the record for a uri. */
350
842
  findByUri(uri: string): ReferenceRecord | undefined {
351
843
  const id = this.byKey.get(uri);
@@ -355,6 +847,23 @@ export class ReferenceRegistry {
355
847
 
356
848
  export const referenceRegistry = new ReferenceRegistry();
357
849
 
850
+ /** A registry's records as they stood when it opened (openJournal). */
851
+ export interface RegistryJournal {
852
+ before(uri: string): ReferenceRecord | undefined;
853
+ close(): void;
854
+ }
855
+
856
+ /**
857
+ * The model's line for a result as a script's link object reads it: the
858
+ * claims that line reads, and each record as it stood when the script lane
859
+ * began to treat the result (a journal's `before`), whatever the treatment
860
+ * has registered, rebound or evicted since, which the line never sees.
861
+ */
862
+ export interface ScriptLine {
863
+ claims: ResultClaims;
864
+ recordBefore(uri: string): ReferenceRecord | undefined;
865
+ }
866
+
358
867
  // ============================================================================
359
868
  // Stub building (§5)
360
869
  // ============================================================================
@@ -366,13 +875,15 @@ export function referenceTypeLabel(mimeType: string | undefined): string {
366
875
  }
367
876
 
368
877
  /**
369
- * The longest line buildReferenceStub writes, in UTF-16 code units, from its
878
+ * The longest stub buildReferenceStub writes, in UTF-16 code units, from its
370
879
  * own bounds: the longest id the registry mints, then the name, the type with
371
880
  * the largest size it shows, and the origin, each at its bound, and the
372
881
  * longest ending, a saved copy's path, with their separators. The other
373
882
  * endings, the fetch hint and the reasons a fetch would be refused, are
374
883
  * shorter. A line shaped like a stub but longer than this is quoted text the
375
- * writer can't have written, and no image slot (tool-image-ledger.ts).
884
+ * writer can't have written, and no image slot (tool-image-ledger.ts). A
885
+ * tool result's link line (StubOptions.unfetchedLinkUri) isn't shaped like a
886
+ * stub, so it's no slot whatever its length.
376
887
  */
377
888
  export const REFERENCE_STUB_MAX_CHARS =
378
889
  `[ref_${MAX_SAFE.toString(36)}_] `.length + REF_ID_SUFFIX_CHARS
@@ -381,15 +892,211 @@ export const REFERENCE_STUB_MAX_CHARS =
381
892
  + ' — '.length + STUB_FIELD_CHARS
382
893
  + ' — '.length + 'saved: '.length + SAVED_PATH_CHARS;
383
894
 
895
+ /** How a lane shows a reference beyond its stub. */
896
+ export interface StubOptions {
897
+ /**
898
+ * Show a link (MCP's resource_link) the host can't fetch as its name, uri
899
+ * and type, unless its uri may carry a credential (uriMayCarryCredential).
900
+ * For a tool result's links, whose JSON showed every link's uri before
901
+ * this host read links as references: there the uri is the agent's only
902
+ * handle on a link the host can't fetch, and showing it is §5's MAY for a
903
+ * reference the host's policy classifies as non-capability-bearing. No
904
+ * other lane showed a link's uri, so none shows one now.
905
+ */
906
+ unfetchedLinkUri?: boolean;
907
+ /** The claims of the result this block is shown in (resultClaims), which
908
+ * decide what the stub says where its references name the record's uri,
909
+ * the record's own testimony deciding otherwise. */
910
+ claims?: ResultClaims;
911
+ }
912
+
913
+ /**
914
+ * What one tool result's references claim of each uri they name, each uri's
915
+ * testimonies read as one (UriClaims): the strictest claim wins, so a uri any
916
+ * of them marks `disposition: never` is withheld, and one any of them gives
917
+ * an expiry that counts is expired (claimedExpiry), whatever the order of the
918
+ * blocks and whatever the registry evicted while it was read (Ada-1017's
919
+ * call). A decision about a record reads its result's claims where the
920
+ * result's references name its uri, a plain one included, and otherwise the
921
+ * record's own testimony, from an earlier result. A reference naming the uri
922
+ * is that result's word on it, the latest, as the registry keeps it
923
+ * (register() replaces a record's testimony with it until the record is
924
+ * fetched), so an earlier result's `never` or expiry gives way to it
925
+ * (Ada-1017's call). A link claims no disposition or expiry, so it adds
926
+ * nothing here, and a result that names a uri only in links leaves its
927
+ * record's testimony in force.
928
+ */
929
+ export type ResultClaims = ReadonlyMap<string, UriClaims>;
930
+
384
931
  /**
385
- * Build the model-visible stub for a reference. Output length is bounded by
386
- * host constants and independent of every server-supplied field length.
387
- * Under `disposition:"never"` the uri is omitted unconditionally; under
388
- * `ref`/absent it is omitted too — this host's default policy is the opaque
389
- * id everywhere (the safe default of §5), with retrieval via the record.
932
+ * A uri as a link shows it: cleaned as sanitizeLabel cleans a label (controls
933
+ * and bidi marks dropped, surrounding space trimmed; a uri past
934
+ * REFERENCE_LIMITS.uri is no reference, so none is shortened), and well
935
+ * formed, as every copy is before it's shown, a lone surrogate showing as
936
+ * U+FFFD. A `never` is about what a link shows, so it's found by this text
937
+ * (neverShown): a claim of `never` on a uri withholds every link that shows
938
+ * the same text, two uris that clean alike sharing it. Every other claim is about the record,
939
+ * which the fetch reads by its raw uri, so it's kept by the raw uri (Ada-1017,
940
+ * room-203 #202239).
390
941
  */
391
- export function buildReferenceStub(t: ReferenceTestimony, provenance?: string, serverId?: string): string {
942
+ export function shownUri(uri: string): string {
943
+ return sanitizeLabel(uri, REFERENCE_LIMITS.uri).toWellFormed();
944
+ }
945
+
946
+ const NEVER_SHOWN = new WeakMap<ResultClaims, ReadonlySet<string>>();
947
+
948
+ /** The texts a link shows (shownUri) for the uris a result's claims mark
949
+ * `never`, read once per claims. A ResultClaims isn't changed once made. */
950
+ function neverShown(claims: ResultClaims): ReadonlySet<string> {
951
+ let shown = NEVER_SHOWN.get(claims);
952
+ if (!shown) {
953
+ shown = new Set([...claims].filter(([, c]) => c.never).map(([uri]) => shownUri(uri)));
954
+ NEVER_SHOWN.set(claims, shown);
955
+ }
956
+ return shown;
957
+ }
958
+
959
+ /** What a result's references say of one uri, read as one. */
960
+ export interface UriClaims {
961
+ /** Whether any of them marks it `disposition: never`. */
962
+ never: boolean;
963
+ /** Whether any of them gives an expiry rejected as a field. */
964
+ rejected: boolean;
965
+ /** Whether any of them gives an expiry that doesn't parse. */
966
+ unparseable: boolean;
967
+ /** The earliest expiry any of them gives, in ms since the epoch, or null. */
968
+ earliestMs: number | null;
969
+ }
970
+
971
+ /** A result's claims (ResultClaims), from the testimonies of its references,
972
+ * each read once, as referenceExpiry reads an expiry. */
973
+ export function resultClaims(testimonies: Iterable<ReferenceTestimony>, nevers: Iterable<string> = []): ResultClaims {
974
+ const claims = new Map<string, UriClaims>();
975
+ // A uri inline content marks `disposition: never` (inlineNeverUri), inline
976
+ // data beside its data, the classifier's contradiction, or an embedded
977
+ // resource: withheld unconditionally (RFC-005 §5), though the block is no
978
+ // reference. Only its never counts, since its other claims could loosen
979
+ // (Ada-1017, room-203 #201013, #206688).
980
+ const entry = (uri: string): UriClaims => {
981
+ let c = claims.get(uri);
982
+ if (!c) claims.set(uri, c = { never: false, rejected: false, unparseable: false, earliestMs: null });
983
+ return c;
984
+ };
985
+ for (const uri of nevers) entry(uri).never = true;
986
+ for (const t of testimonies) {
987
+ if (t.fromLink) continue;
988
+ const c = entry(t.uri);
989
+ if (t.disposition === 'never') c.never = true;
990
+ if (t.rejectedFields.includes('expiresAt')) {
991
+ c.rejected = true;
992
+ } else if (t.expiresAt !== undefined) {
993
+ const ms = Date.parse(t.expiresAt);
994
+ if (Number.isNaN(ms)) c.unparseable = true;
995
+ else if (c.earliestMs === null || ms < c.earliestMs) c.earliestMs = ms;
996
+ }
997
+ }
998
+ return claims;
999
+ }
1000
+
1001
+ /** The claims of a content array's references, read from its elements as
1002
+ * the array shows them: an element's own fields, such as an image's
1003
+ * `_meta`, aren't shown, so a reference inside them doesn't count. */
1004
+ export function contentClaims(blocks: readonly unknown[]): ResultClaims {
1005
+ const testimonies: ReferenceTestimony[] = [];
1006
+ const nevers: string[] = [];
1007
+ for (const block of blocks) {
1008
+ const c = classifyBlock(block);
1009
+ if (c.kind === 'reference') testimonies.push(c.testimony);
1010
+ const never = inlineNeverUri(block, c);
1011
+ if (never !== null) nevers.push(never);
1012
+ }
1013
+ return resultClaims(testimonies, nevers);
1014
+ }
1015
+
1016
+ /** The uri inline content marks `disposition: never`, which counts for that
1017
+ * uri's claims (resultClaims) though the block is no reference: inline data
1018
+ * beside its uri, the classifier's contradiction, and MCP's embedded
1019
+ * resource, whose uri sits under `resource` (Ada-1017, room-203 #206688).
1020
+ * Null for any other block. A uri that isn't a non-empty string names
1021
+ * nothing a claim is kept for. */
1022
+ export function inlineNeverUri(block: unknown, c: BlockClassification = classifyBlock(block)): string | null {
1023
+ // Either kind is a record, so its fields can be read.
1024
+ if (!(c.kind === 'inline' && c.contradiction) && c.kind !== 'embedded') return null;
1025
+ const b = block as { uri?: unknown; disposition?: unknown; resource: { uri?: unknown } };
1026
+ const uri = c.kind === 'embedded' ? b.resource.uri : b.uri;
1027
+ return b.disposition === 'never' && typeof uri === 'string' && uri !== '' ? uri : null;
1028
+ }
1029
+
1030
+ /** The claims a decision about a record reads: its result's for its uri,
1031
+ * where the result's references name it, and otherwise the record's own
1032
+ * testimony. */
1033
+ function uriClaims(record: ReferenceRecord, claims?: ResultClaims): UriClaims | undefined {
1034
+ return claims?.get(record.testimony.uri) ?? resultClaims([record.testimony]).get(record.testimony.uri);
1035
+ }
1036
+
1037
+ /** Whether a record's uri is withheld: marked `disposition: never` by its
1038
+ * result's references, for any uri a link shows as the same text
1039
+ * (shownUri), or by its own testimony where none of them names it. */
1040
+ export function claimedNever(record: ReferenceRecord, claims?: ResultClaims): boolean {
1041
+ if (claims && neverShown(claims).has(shownUri(record.testimony.uri))) return true;
1042
+ return uriClaims(record, claims)?.never === true;
1043
+ }
1044
+
1045
+ /** The expiry a record counts, as of `nowMs`: the first of EXPIRY_REASON's
1046
+ * cases its claims give (uriClaims), so the order of the blocks doesn't
1047
+ * choose the words. Null when none does. */
1048
+ export function claimedExpiry(record: ReferenceRecord, claims: ResultClaims | undefined, nowMs: number): keyof typeof EXPIRY_REASON | null {
1049
+ const c = uriClaims(record, claims);
1050
+ if (!c) return null;
1051
+ const given: Record<keyof typeof EXPIRY_REASON, boolean> = {
1052
+ passed: c.earliestMs !== null && c.earliestMs <= nowMs,
1053
+ unparseable: c.unparseable,
1054
+ rejected: c.rejected,
1055
+ };
1056
+ return (Object.keys(EXPIRY_REASON) as Array<keyof typeof EXPIRY_REASON>).find((reason) => given[reason]) ?? null;
1057
+ }
1058
+
1059
+ /**
1060
+ * The uri a tool result's link shows, or null where it shows none: a link
1061
+ * (fromLink) the host can't fetch, whose uri can't carry a credential, and
1062
+ * that no reference withholds with `disposition: never`, keeps its uri, the
1063
+ * agent's only handle on it. A `data:` uri holds its content rather than
1064
+ * naming where it lies, so it's never shown: a link's line isn't a way for
1065
+ * inline data to reach context. A tool result's link line (buildReferenceStub,
1066
+ * unfetchedLinkUri) and a script's object for the link
1067
+ * (referenceBlockForScript) both ask this, so the model and a script, whose
1068
+ * output comes back to the model, see a link's uri alike.
1069
+ *
1070
+ * The credential check reads the uri as it would be shown, with what
1071
+ * sanitizeLabel strips gone, so a uri split by a control character is read
1072
+ * as it would read: `ht\u0000tps://user:pass@…` parses as a path, and shows
1073
+ * as userinfo. A `disposition: "never"` for the uri keeps it out whatever
1074
+ * the link says: any reference's in the same result (claims), for this uri
1075
+ * or one shown as the same text, or, where no reference in the result names
1076
+ * the uri, the record's.
1077
+ */
1078
+ export function shownLinkUri(t: ReferenceTestimony, record: ReferenceRecord, nowMs = Date.now(), claims?: ResultClaims): string | null {
1079
+ if (!t.fromLink || claimedNever(record, claims)) return null;
1080
+ if (unfetchableReason(record, nowMs, claims) === null) return null;
1081
+ const uri = shownUri(t.uri);
1082
+ return uriMayCarryCredential(uri) || /^data:/i.test(uri) ? null : uri;
1083
+ }
1084
+
1085
+ /**
1086
+ * Build the model-visible stub for a reference. Its length is bounded by
1087
+ * host constants and independent of every server-supplied field's length:
1088
+ * REFERENCE_STUB_MAX_CHARS. Under `disposition:"never"` the uri is omitted
1089
+ * unconditionally; under `ref`/absent it is omitted too — this host's default
1090
+ * policy is the opaque id everywhere (the safe default of §5), with retrieval
1091
+ * via the record. The one exception is a tool result's link the host can't
1092
+ * fetch (StubOptions.unfetchedLinkUri), which is no stub but a line of its
1093
+ * own, its uri bounded by REFERENCE_LIMITS.uri.
1094
+ */
1095
+ export function buildReferenceStub(t: ReferenceTestimony, provenance?: string, serverId?: string, options?: StubOptions): string {
392
1096
  const record = referenceRegistry.register(t, serverId);
1097
+ // One clock reading for the whole stub, so whether it shows a link's uri
1098
+ // and the reason it gives can't disagree across an expiry.
1099
+ const nowMs = Date.now();
393
1100
  const parts: string[] = [];
394
1101
  // A label is shown only when something is left of it once sanitized.
395
1102
  const name = t.name ? sanitizeLabel(t.name, STUB_FIELD_CHARS) : '';
@@ -406,29 +1113,40 @@ export function buildReferenceStub(t: ReferenceTestimony, provenance?: string, s
406
1113
  // workspace path (host-generated, §7.3) so existing tools just work.
407
1114
  parts.push(`saved: ${sanitizeLabel(record.fetchedPath, SAVED_PATH_CHARS)}`);
408
1115
  } else {
409
- // A reference whose record shows the fetcher would refuse it says why,
410
- // rather than inviting a fetch_reference call.
411
- parts.push(unfetchableReason(record) ?? 'fetch with fetch_reference');
1116
+ // A reference the fetcher would refuse says why, rather than inviting a
1117
+ // fetch_reference call.
1118
+ if (options?.unfetchedLinkUri) {
1119
+ const uri = shownLinkUri(t, record, nowMs, options.claims);
1120
+ if (uri !== null) return unfetchedLinkText(t, uri);
1121
+ }
1122
+ parts.push(unfetchableReason(record, nowMs, options?.claims) ?? 'fetch with fetch_reference');
412
1123
  }
413
1124
  return `[${record.refId}] ${parts.join(' — ') || 'referenced content'}`;
414
1125
  }
415
1126
 
416
1127
  /**
417
- * Why the fetcher would refuse a reference, as far as its record alone shows,
418
- * for its stub: no server registered for it, which the fetcher checks first,
419
- * or its expiry. Null for a saved copy, which the fetcher serves before any
420
- * check, and when the record shows neither. The fetcher's other refusals rest
421
- * on what a record doesn't hold: whether any of its servers is known to the
422
- * host, has an address and is on the uri's origin, the dialed server's fetch
423
- * budget spent, or a claimed size over the ceiling a fetch is given. The
424
- * fetcher gives those in its own words, and save_recent_image, in the
425
- * framework where it can ask the fetcher, says them too.
1128
+ * What a tool result's link the host can't fetch shows, where its uri can't
1129
+ * carry a credential: its name, its uri and its type, as a standard MCP
1130
+ * server's link shows (StubOptions.unfetchedLinkUri).
1131
+ */
1132
+ function unfetchedLinkText(t: ReferenceTestimony, uri: string): string {
1133
+ const name = t.name ? sanitizeLabel(t.name, STUB_FIELD_CHARS) : '';
1134
+ const mimeType = referenceTypeLabel(t.mimeType);
1135
+ return `[resource link${name ? ` "${name}"` : ''}: ${uri}${mimeType ? `, ${mimeType}` : ''}; not fetched]`;
1136
+ }
1137
+
1138
+ /**
1139
+ * Why the fetcher would refuse a reference before it dials, whatever the
1140
+ * fetch, in its stub's words: the registry's dialCheck, from its record and
1141
+ * its servers' addresses. Null for a saved copy, which the fetcher serves
1142
+ * before any check, and where a fetch would dial. A fetch's own limits, the
1143
+ * server's spent budget or a claimed size over the ceiling it's given, the
1144
+ * fetcher gives in its own words, and save_recent_image says them too.
426
1145
  */
427
- export function unfetchableReason(record: ReferenceRecord, nowMs = Date.now()): string | null {
1146
+ export function unfetchableReason(record: ReferenceRecord, nowMs = Date.now(), claims?: ResultClaims): string | null {
428
1147
  if (record.fetchedPath) return null;
429
- if (record.serverIds.length === 0) return UNBOUND_REASON;
430
- const expiry = referenceExpiry(record.testimony, nowMs);
431
- return expiry ? EXPIRY_REASON[expiry] : null;
1148
+ const dial = dialCheck(record, (serverId) => referenceRegistry.serverAddress(serverId), nowMs, claims);
1149
+ return 'refusal' in dial ? dial.refusal.inStub ?? dial.refusal.error : null;
432
1150
  }
433
1151
 
434
1152
  /**
@@ -437,15 +1155,20 @@ export function unfetchableReason(record: ReferenceRecord, nowMs = Date.now()):
437
1155
  * its existing (inline/text) handling. A reference's stub carries text taken
438
1156
  * from the block's own fields, so it's made well formed (a lone UTF-16
439
1157
  * surrogate becomes U+FFFD), as the context store would make it: a tool
440
- * result's copies then carry the same text (#695).
1158
+ * result's copies then carry the same text (#695). For MCP's embedded
1159
+ * resource, it's the text for contents that can't be shown (its marker is
1160
+ * well formed where it's made), and null where they can: the caller shows the
1161
+ * block they make (classifyBlock's `embedded`).
441
1162
  */
442
- export function referenceStubOrNull(block: unknown, provenance?: string, serverId?: string): string | null {
1163
+ export function referenceStubOrNull(block: unknown, provenance?: string, serverId?: string, options?: StubOptions): string | null {
443
1164
  const c = classifyBlock(block);
444
1165
  switch (c.kind) {
445
1166
  case 'text': return null;
446
1167
  case 'inline':
447
1168
  return c.contradiction ? INLINE_WITHHELD_TEXT : null;
448
- case 'reference': return buildReferenceStub(c.testimony, provenance, serverId).toWellFormed();
1169
+ case 'reference': return buildReferenceStub(c.testimony, provenance, serverId, options).toWellFormed();
1170
+ case 'embedded':
1171
+ return c.contents.kind === 'withheld' ? INLINE_WITHHELD_TEXT : c.contents.kind === 'marker' ? c.contents.text : null;
449
1172
  case 'invalid': return c.shown;
450
1173
  }
451
1174
  }
@@ -455,26 +1178,39 @@ export function referenceStubOrNull(block: unknown, provenance?: string, serverI
455
1178
  * result, which carries blocks rather than text. It makes the decision
456
1179
  * referenceStubOrNull makes for the model's lanes, since what a script prints
457
1180
  * comes back to the model, except that a script reads every field, so inline
458
- * data beside a uri of any type but null or absent is withheld, where the
459
- * model's lanes, which show no uri, withhold it beside a string one. A reference becomes the stub's facts as data,
460
- * without its URI: the host's reference id, what the server said about it in
461
- * this block (its labels stripped and bounded as in the stub), and `savedTo`,
462
- * the workspace path its payload was fetched to, or null when it wasn't
463
- * (`fetch_reference` takes the id as `ref_id`). An unsaved reference that
464
- * counts as expired also carries `expired`, the reason the stub gives in
465
- * place of advising fetch_reference. The reference is registered bound to
466
- * `serverId`, the server whose tool returned it, as the model's lanes bind
467
- * a tool result's references, so the fetch its id offers can be made. With
468
- * no `serverId`, as for a module's result, it's registered as the model's
1181
+ * data beside a uri of any type but null, absent or empty is withheld, where
1182
+ * the model's lanes, which show no uri, withhold it beside a string one that
1183
+ * isn't empty. A reference becomes the stub's facts as data: the host's
1184
+ * reference id, what the server said about it in this block (its labels
1185
+ * stripped and bounded as in the stub), and `savedTo`, the workspace path
1186
+ * its payload was fetched to, or null when it wasn't (`fetch_reference`
1187
+ * takes the id as `ref_id`). Its uri is left out, except that a link (MCP's
1188
+ * resource_link) carries `uri` where the model's line for it shows the uri
1189
+ * (shownLinkUri). An unsaved reference that counts as expired also carries
1190
+ * `expired`, and one the fetcher would refuse carries `unfetchable`, the
1191
+ * words a stub gives in place of advising fetch_reference
1192
+ * (unfetchableReason). Both give why a fetch_reference with the object's
1193
+ * `refId` would be refused, as the result's references claim it, so a link
1194
+ * whose line shows its uri, which the host can't fetch, carries them too: its
1195
+ * line has no id to fetch with. The fetch itself reads the record, which
1196
+ * keeps the latest testimony registered until it's fetched, and then the one
1197
+ * its saved copy was verified against (Ada-1017's call). `claims` are those
1198
+ * of everything the script's view shows (scriptViewClaims), so its other
1199
+ * references count (ResultClaims). The reference is registered bound to
1200
+ * `serverId`, the server whose tool returned it, as the model's lanes bind a
1201
+ * tool result's references, so the fetch its id offers can be made. With no
1202
+ * `serverId`, as for a module's result, it's registered as the model's
469
1203
  * history registers a module's: the record its uri already has, bound where
470
1204
  * it arrived (a module's ctx.callTool registers an MCPL peer's references),
471
1205
  * or a new one bound to no server. A block the classifier
472
1206
  * rejects becomes the notice the model's lanes show, and inline data that
473
- * also claims a bulk disposition, or a uri of any type but null or absent
474
- * (an empty string included), the withheld notice, each as a text block.
1207
+ * also claims a bulk disposition, or a uri of any type but null, absent or
1208
+ * empty, the withheld notice, each as a text block. MCP's embedded resource
1209
+ * becomes the block its contents make, as the model's lanes show it, never
1210
+ * its uri.
475
1211
  * A text block, or inline data that claims nothing, is returned as sent.
476
1212
  */
477
- export function referenceBlockForScript(block: unknown, serverId?: string): unknown {
1213
+ export function referenceBlockForScript(block: unknown, serverId?: string, claims?: ResultClaims, line?: ScriptLine): unknown {
478
1214
  const c = classifyBlock(block);
479
1215
  switch (c.kind) {
480
1216
  case 'text': return block;
@@ -482,22 +1218,59 @@ export function referenceBlockForScript(block: unknown, serverId?: string): unkn
482
1218
  // A script reads every field, as history's JSON writes it, so inline
483
1219
  // data beside a uri of any type is withheld here, not only beside a
484
1220
  // string one as in the model's lanes, which show no uri: an object can
485
- // carry a credential. An absent or null uri carries none; a string one,
486
- // an empty one included, is already the classifier's contradiction.
1221
+ // carry a credential. An absent, null or empty uri carries none, as at
1222
+ // any depth (hasUri), so a peer's and a module's agree (Ada-1017,
1223
+ // room-203 #200094); one beside a disposition is the classifier's
1224
+ // contradiction, a reference's claim.
487
1225
  const uri = (block as { uri?: unknown }).uri;
488
- return c.contradiction || (uri !== undefined && uri !== null)
1226
+ return c.contradiction || (uri !== undefined && uri !== null && uri !== '')
489
1227
  ? { type: 'text', text: INLINE_WITHHELD_TEXT }
490
1228
  : block;
491
1229
  }
492
1230
  case 'invalid': return { type: 'text', text: c.shown };
1231
+ case 'embedded':
1232
+ // The block its contents make, as the model's lanes show it, and never
1233
+ // its uri or its `resource` record (Ada-1017, room-203 #206688): text,
1234
+ // or inline data that claims nothing, as such a block reaches a script;
1235
+ // the withheld notice where it claims a disposition; its marker
1236
+ // otherwise. Given again, each is as it was.
1237
+ if (c.contents.kind === 'block') return c.contents.block;
1238
+ return { type: 'text', text: c.contents.kind === 'withheld' ? INLINE_WITHHELD_TEXT : c.contents.text };
493
1239
  case 'reference': {
494
1240
  const t = c.testimony;
495
1241
  const record = referenceRegistry.register(t, serverId);
496
- // As the stub judges it: a saved copy is still served.
497
- const expiry = record.fetchedPath ? null : referenceExpiry(record.testimony);
1242
+ // One clock reading for the whole object, so its words can't disagree
1243
+ // with each other across an expiry.
1244
+ const nowMs = Date.now();
1245
+ // A link's uri where the model's line for the result shows it, by the
1246
+ // claims the line reads and its record as the line reads it (`line`,
1247
+ // where they differ from the view's), and never where the view's
1248
+ // claims withhold it: reading more claims can loosen as well as
1249
+ // tighten, since an expiry makes a link one the host can't fetch, whose
1250
+ // uri the line shows (Ada-1017, room-203 #201013). The line's record is
1251
+ // the registry's as the model's line leaves it, before the script
1252
+ // lane's own registrations (#202683, #203011), since the treatment
1253
+ // registers, rebinds and evicts as it goes, which the line never sees;
1254
+ // where there was none, it's the one the link's own registration
1255
+ // makes. A script never sees a uri the model's line
1256
+ // withholds, through its blocks; structured content goes as sent, as
1257
+ // on main.
1258
+ const lineRecord = line
1259
+ ? line.recordBefore(t.uri) ?? { refId: record.refId, serverIds: serverId === undefined ? [] : [serverId], testimony: t, receivedAt: record.receivedAt }
1260
+ : record;
1261
+ const lineShows = shownLinkUri(t, lineRecord, nowMs, line ? line.claims : claims);
1262
+ const uri = lineShows !== null && line !== undefined && claimedNever(record, claims) ? null : lineShows;
1263
+ // Why a fetch_reference with this object's refId would be refused, in
1264
+ // the words a stub gives, whether or not the model's line for it is a
1265
+ // stub: the line has no id to fetch with, and the object does. As the
1266
+ // stub judges it, a saved copy is still served, and the result's own
1267
+ // claims decide where its references name the uri (uriClaims).
1268
+ const expiry = record.fetchedPath ? null : claimedExpiry(record, claims, nowMs);
1269
+ const unfetchable = unfetchableReason(record, nowMs, claims);
498
1270
  return {
499
1271
  type: (block as { type: string }).type,
500
1272
  refId: record.refId,
1273
+ ...(uri !== null ? { uri } : {}),
501
1274
  ...(t.name !== undefined ? { name: sanitizeLabel(t.name, STUB_FIELD_CHARS) } : {}),
502
1275
  ...(t.mimeType !== undefined ? { mimeType: sanitizeLabel(t.mimeType, STUB_FIELD_CHARS) } : {}),
503
1276
  ...(t.sizeBytes !== undefined ? { sizeBytes: t.sizeBytes } : {}),
@@ -505,6 +1278,7 @@ export function referenceBlockForScript(block: unknown, serverId?: string): unkn
505
1278
  ...(t.expiresAt !== undefined ? { expiresAt: sanitizeLabel(t.expiresAt, STUB_FIELD_CHARS) } : {}),
506
1279
  savedTo: record.fetchedPath ?? null,
507
1280
  ...(expiry ? { expired: EXPIRY_REASON[expiry] } : {}),
1281
+ ...(unfetchable !== null ? { unfetchable } : {}),
508
1282
  };
509
1283
  }
510
1284
  }