@animalabs/agent-framework 0.26.1 → 0.27.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 (276) hide show
  1. package/README.md +57 -7
  2. package/dist/src/agent.d.ts +263 -22
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +531 -54
  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 +1 -2
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/auto-continue.d.ts +33 -0
  10. package/dist/src/auto-continue.d.ts.map +1 -0
  11. package/dist/src/auto-continue.js +35 -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/framework.d.ts +829 -156
  18. package/dist/src/framework.d.ts.map +1 -1
  19. package/dist/src/framework.js +3906 -1008
  20. package/dist/src/framework.js.map +1 -1
  21. package/dist/src/gate/event-gate.d.ts +309 -46
  22. package/dist/src/gate/event-gate.d.ts.map +1 -1
  23. package/dist/src/gate/event-gate.js +2117 -395
  24. package/dist/src/gate/event-gate.js.map +1 -1
  25. package/dist/src/gate/index.d.ts +2 -2
  26. package/dist/src/gate/index.d.ts.map +1 -1
  27. package/dist/src/gate/index.js +1 -1
  28. package/dist/src/gate/index.js.map +1 -1
  29. package/dist/src/gate/recipe-reconcile.d.ts +103 -0
  30. package/dist/src/gate/recipe-reconcile.d.ts.map +1 -0
  31. package/dist/src/gate/recipe-reconcile.js +325 -0
  32. package/dist/src/gate/recipe-reconcile.js.map +1 -0
  33. package/dist/src/gate/types.d.ts +68 -8
  34. package/dist/src/gate/types.d.ts.map +1 -1
  35. package/dist/src/index.d.ts +6 -4
  36. package/dist/src/index.d.ts.map +1 -1
  37. package/dist/src/index.js +5 -2
  38. package/dist/src/index.js.map +1 -1
  39. package/dist/src/mcpl/channel-registry.d.ts +433 -113
  40. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  41. package/dist/src/mcpl/channel-registry.js +1152 -434
  42. package/dist/src/mcpl/channel-registry.js.map +1 -1
  43. package/dist/src/mcpl/conversation-router.d.ts +16 -8
  44. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  45. package/dist/src/mcpl/conversation-router.js +29 -15
  46. package/dist/src/mcpl/conversation-router.js.map +1 -1
  47. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  48. package/dist/src/mcpl/convert-block.js +14 -2
  49. package/dist/src/mcpl/convert-block.js.map +1 -1
  50. package/dist/src/mcpl/hook-orchestrator.d.ts +7 -1
  51. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  52. package/dist/src/mcpl/hook-orchestrator.js +32 -7
  53. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  54. package/dist/src/mcpl/index.d.ts +5 -1
  55. package/dist/src/mcpl/index.d.ts.map +1 -1
  56. package/dist/src/mcpl/index.js +4 -0
  57. package/dist/src/mcpl/index.js.map +1 -1
  58. package/dist/src/mcpl/inference-router.d.ts +14 -2
  59. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  60. package/dist/src/mcpl/inference-router.js +73 -28
  61. package/dist/src/mcpl/inference-router.js.map +1 -1
  62. package/dist/src/mcpl/prose-grammar.d.ts +4 -0
  63. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  64. package/dist/src/mcpl/prose-grammar.js +6 -0
  65. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  66. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  67. package/dist/src/mcpl/protocol-family.js +3 -0
  68. package/dist/src/mcpl/protocol-family.js.map +1 -1
  69. package/dist/src/mcpl/push-coalescer.d.ts +20 -2
  70. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  71. package/dist/src/mcpl/push-coalescer.js +33 -11
  72. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  73. package/dist/src/mcpl/reference-fetcher.d.ts +63 -42
  74. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  75. package/dist/src/mcpl/reference-fetcher.js +177 -145
  76. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  77. package/dist/src/mcpl/references.d.ts +380 -46
  78. package/dist/src/mcpl/references.d.ts.map +1 -1
  79. package/dist/src/mcpl/references.js +737 -78
  80. package/dist/src/mcpl/references.js.map +1 -1
  81. package/dist/src/mcpl/server-connection.d.ts +82 -3
  82. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  83. package/dist/src/mcpl/server-connection.js +132 -7
  84. package/dist/src/mcpl/server-connection.js.map +1 -1
  85. package/dist/src/mcpl/tool-lifecycle.d.ts +11 -0
  86. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  87. package/dist/src/mcpl/tool-lifecycle.js +21 -0
  88. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  89. package/dist/src/mcpl/tool-presence.d.ts +259 -0
  90. package/dist/src/mcpl/tool-presence.d.ts.map +1 -0
  91. package/dist/src/mcpl/tool-presence.js +628 -0
  92. package/dist/src/mcpl/tool-presence.js.map +1 -0
  93. package/dist/src/mcpl/tool-result-normalize.d.ts +9 -3
  94. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  95. package/dist/src/mcpl/tool-result-normalize.js +14 -6
  96. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  97. package/dist/src/mcpl/tool-signature.d.ts +39 -0
  98. package/dist/src/mcpl/tool-signature.d.ts.map +1 -0
  99. package/dist/src/mcpl/tool-signature.js +292 -0
  100. package/dist/src/mcpl/tool-signature.js.map +1 -0
  101. package/dist/src/mcpl/toolset-host.d.ts +200 -0
  102. package/dist/src/mcpl/toolset-host.d.ts.map +1 -0
  103. package/dist/src/mcpl/toolset-host.js +621 -0
  104. package/dist/src/mcpl/toolset-host.js.map +1 -0
  105. package/dist/src/mcpl/toolsets-module.d.ts +80 -0
  106. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -0
  107. package/dist/src/mcpl/toolsets-module.js +431 -0
  108. package/dist/src/mcpl/toolsets-module.js.map +1 -0
  109. package/dist/src/mcpl/transport.d.ts +2 -2
  110. package/dist/src/mcpl/transport.d.ts.map +1 -1
  111. package/dist/src/mcpl/transport.js +3 -2
  112. package/dist/src/mcpl/transport.js.map +1 -1
  113. package/dist/src/mcpl/types.d.ts +21 -1
  114. package/dist/src/mcpl/types.d.ts.map +1 -1
  115. package/dist/src/mcpl/types.js.map +1 -1
  116. package/dist/src/mcpl/visible-content.d.ts +7 -9
  117. package/dist/src/mcpl/visible-content.d.ts.map +1 -1
  118. package/dist/src/mcpl/visible-content.js +21 -9
  119. package/dist/src/mcpl/visible-content.js.map +1 -1
  120. package/dist/src/module-registry.d.ts +23 -8
  121. package/dist/src/module-registry.d.ts.map +1 -1
  122. package/dist/src/module-registry.js +68 -15
  123. package/dist/src/module-registry.js.map +1 -1
  124. package/dist/src/modules/discord/index.js +1 -1
  125. package/dist/src/modules/discord/index.js.map +1 -1
  126. package/dist/src/modules/history/index.d.ts +2 -1
  127. package/dist/src/modules/history/index.d.ts.map +1 -1
  128. package/dist/src/modules/history/index.js +51 -17
  129. package/dist/src/modules/history/index.js.map +1 -1
  130. package/dist/src/modules/history/semantic.d.ts +108 -6
  131. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  132. package/dist/src/modules/history/semantic.js +273 -57
  133. package/dist/src/modules/history/semantic.js.map +1 -1
  134. package/dist/src/modules/workspace/direct-write.d.ts +181 -0
  135. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -0
  136. package/dist/src/modules/workspace/direct-write.js +384 -0
  137. package/dist/src/modules/workspace/direct-write.js.map +1 -0
  138. package/dist/src/modules/workspace/edit-text.d.ts +26 -12
  139. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
  140. package/dist/src/modules/workspace/edit-text.js +58 -28
  141. package/dist/src/modules/workspace/edit-text.js.map +1 -1
  142. package/dist/src/modules/workspace/effects.d.ts +10 -0
  143. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  144. package/dist/src/modules/workspace/effects.js +1 -1
  145. package/dist/src/modules/workspace/effects.js.map +1 -1
  146. package/dist/src/modules/workspace/index.d.ts +246 -27
  147. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  148. package/dist/src/modules/workspace/index.js +1216 -236
  149. package/dist/src/modules/workspace/index.js.map +1 -1
  150. package/dist/src/modules/workspace/read-page.d.ts +41 -0
  151. package/dist/src/modules/workspace/read-page.d.ts.map +1 -0
  152. package/dist/src/modules/workspace/read-page.js +213 -0
  153. package/dist/src/modules/workspace/read-page.js.map +1 -0
  154. package/dist/src/modules/workspace/result-text.d.ts +67 -0
  155. package/dist/src/modules/workspace/result-text.d.ts.map +1 -0
  156. package/dist/src/modules/workspace/result-text.js +94 -0
  157. package/dist/src/modules/workspace/result-text.js.map +1 -0
  158. package/dist/src/modules/workspace/text.d.ts +29 -12
  159. package/dist/src/modules/workspace/text.d.ts.map +1 -1
  160. package/dist/src/modules/workspace/text.js +35 -33
  161. package/dist/src/modules/workspace/text.js.map +1 -1
  162. package/dist/src/modules/workspace/types.d.ts +41 -17
  163. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  164. package/dist/src/modules/workspace/types.js.map +1 -1
  165. package/dist/src/sent-rows.d.ts +59 -0
  166. package/dist/src/sent-rows.d.ts.map +1 -0
  167. package/dist/src/sent-rows.js +145 -0
  168. package/dist/src/sent-rows.js.map +1 -0
  169. package/dist/src/silent-heartbeat.d.ts +17 -10
  170. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  171. package/dist/src/silent-heartbeat.js.map +1 -1
  172. package/dist/src/stable-hash.d.ts +3 -0
  173. package/dist/src/stable-hash.d.ts.map +1 -0
  174. package/dist/src/stable-hash.js +15 -0
  175. package/dist/src/stable-hash.js.map +1 -0
  176. package/dist/src/stored-injections.d.ts +99 -0
  177. package/dist/src/stored-injections.d.ts.map +1 -0
  178. package/dist/src/stored-injections.js +106 -0
  179. package/dist/src/stored-injections.js.map +1 -0
  180. package/dist/src/text-as-stored.d.ts +4 -3
  181. package/dist/src/text-as-stored.d.ts.map +1 -1
  182. package/dist/src/text-as-stored.js +4 -3
  183. package/dist/src/text-as-stored.js.map +1 -1
  184. package/dist/src/thinking-drops.d.ts +81 -0
  185. package/dist/src/thinking-drops.d.ts.map +1 -0
  186. package/dist/src/thinking-drops.js +103 -0
  187. package/dist/src/thinking-drops.js.map +1 -0
  188. package/dist/src/tool-image-ledger.d.ts +9 -8
  189. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  190. package/dist/src/tool-image-ledger.js +9 -8
  191. package/dist/src/tool-image-ledger.js.map +1 -1
  192. package/dist/src/tool-presentation.d.ts.map +1 -1
  193. package/dist/src/tool-presentation.js +3 -1
  194. package/dist/src/tool-presentation.js.map +1 -1
  195. package/dist/src/tool-result-guard.d.ts +4 -2
  196. package/dist/src/tool-result-guard.d.ts.map +1 -1
  197. package/dist/src/tool-result-guard.js +21 -6
  198. package/dist/src/tool-result-guard.js.map +1 -1
  199. package/dist/src/tool-result-history.d.ts +186 -21
  200. package/dist/src/tool-result-history.d.ts.map +1 -1
  201. package/dist/src/tool-result-history.js +428 -89
  202. package/dist/src/tool-result-history.js.map +1 -1
  203. package/dist/src/tune-out/coordinator.d.ts +17 -9
  204. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  205. package/dist/src/tune-out/coordinator.js +84 -37
  206. package/dist/src/tune-out/coordinator.js.map +1 -1
  207. package/dist/src/types/agent.d.ts +186 -7
  208. package/dist/src/types/agent.d.ts.map +1 -1
  209. package/dist/src/types/events.d.ts +19 -2
  210. package/dist/src/types/events.d.ts.map +1 -1
  211. package/dist/src/types/framework.d.ts +24 -3
  212. package/dist/src/types/framework.d.ts.map +1 -1
  213. package/dist/src/types/module.d.ts +90 -7
  214. package/dist/src/types/module.d.ts.map +1 -1
  215. package/dist/src/types/trace.d.ts +57 -2
  216. package/dist/src/types/trace.d.ts.map +1 -1
  217. package/package.json +4 -3
  218. package/src/agent.ts +593 -60
  219. package/src/api/server.ts +1 -2
  220. package/src/auto-continue.ts +42 -0
  221. package/src/code-execution/isolate/scripts.ts +11 -3
  222. package/src/framework.ts +4004 -1026
  223. package/src/gate/event-gate.ts +2315 -389
  224. package/src/gate/index.ts +2 -1
  225. package/src/gate/recipe-reconcile.ts +355 -0
  226. package/src/gate/types.ts +70 -8
  227. package/src/index.ts +7 -4
  228. package/src/mcpl/channel-registry.ts +1220 -467
  229. package/src/mcpl/conversation-router.ts +34 -16
  230. package/src/mcpl/convert-block.ts +14 -1
  231. package/src/mcpl/hook-orchestrator.ts +33 -6
  232. package/src/mcpl/index.ts +6 -1
  233. package/src/mcpl/inference-router.ts +79 -34
  234. package/src/mcpl/prose-grammar.ts +7 -0
  235. package/src/mcpl/protocol-family.ts +2 -0
  236. package/src/mcpl/push-coalescer.ts +51 -13
  237. package/src/mcpl/reference-fetcher.ts +201 -162
  238. package/src/mcpl/references.ts +852 -78
  239. package/src/mcpl/server-connection.ts +145 -7
  240. package/src/mcpl/tool-lifecycle.ts +21 -0
  241. package/src/mcpl/tool-presence.ts +650 -0
  242. package/src/mcpl/tool-result-normalize.ts +18 -6
  243. package/src/mcpl/tool-signature.ts +285 -0
  244. package/src/mcpl/toolset-host.ts +637 -0
  245. package/src/mcpl/toolsets-module.ts +451 -0
  246. package/src/mcpl/transport.ts +3 -2
  247. package/src/mcpl/types.ts +22 -1
  248. package/src/mcpl/visible-content.ts +22 -9
  249. package/src/module-registry.ts +77 -24
  250. package/src/modules/discord/index.ts +1 -1
  251. package/src/modules/history/index.ts +48 -18
  252. package/src/modules/history/semantic.ts +308 -54
  253. package/src/modules/workspace/direct-write.ts +425 -0
  254. package/src/modules/workspace/edit-text.ts +82 -35
  255. package/src/modules/workspace/effects.ts +1 -1
  256. package/src/modules/workspace/index.ts +1274 -241
  257. package/src/modules/workspace/read-page.ts +222 -0
  258. package/src/modules/workspace/result-text.ts +119 -0
  259. package/src/modules/workspace/text.ts +58 -32
  260. package/src/modules/workspace/types.ts +41 -17
  261. package/src/sent-rows.ts +162 -0
  262. package/src/silent-heartbeat.ts +17 -10
  263. package/src/stable-hash.ts +14 -0
  264. package/src/stored-injections.ts +131 -0
  265. package/src/text-as-stored.ts +4 -3
  266. package/src/thinking-drops.ts +141 -0
  267. package/src/tool-image-ledger.ts +9 -8
  268. package/src/tool-presentation.ts +3 -1
  269. package/src/tool-result-guard.ts +19 -6
  270. package/src/tool-result-history.ts +488 -88
  271. package/src/tune-out/coordinator.ts +80 -37
  272. package/src/types/agent.ts +189 -7
  273. package/src/types/events.ts +19 -2
  274. package/src/types/framework.ts +21 -3
  275. package/src/types/module.ts +86 -7
  276. package/src/types/trace.ts +58 -2
@@ -9,30 +9,201 @@
9
9
  * `maxChars` slice and re-introduces, one turn deferred, exactly the silent
10
10
  * hallucination this feature exists to kill.
11
11
  *
12
- * For MCP-shaped content arrays, this helper keeps text blocks verbatim,
13
- * apart from a lone UTF-16 surrogate, which becomes U+FFFD (#695), and
14
- * replaces image blocks with a short `[image: mimeType, ~NKB]` placeholder.
15
- * A string is already text and is kept as it is. For anything else it falls
16
- * back to JSON, written by RFC-005's rules at every depth (historyJson): a
17
- * reference is shown as its stub wherever it sits in the data, so its uri
18
- * never reaches history, and inline data that claims a uri or a disposition
19
- * is withheld.
12
+ * A content array (toolResultBlocks) is shown block by block, each block as
13
+ * both copies show it: text verbatim, apart from a lone UTF-16 surrogate,
14
+ * which becomes U+FFFD (#695), an image as a short
15
+ * `[image: mimeType, ~NKB]` placeholder, a reference as its stub, and any
16
+ * other block as a marker saying what it was and why it isn't shown. Anything
17
+ * else falls back to JSON, written by RFC-005's rules at every depth
18
+ * (historyJson): a reference is shown as its stub wherever it sits in the
19
+ * data, so its uri reaches history only as a link's line shows it where the
20
+ * host can't fetch the link and its uri can't carry a credential
21
+ * (shownLinkUri), and inline data that claims a uri or a disposition is
22
+ * withheld. A string is already text and is kept as it is.
20
23
  */
21
24
 
22
- import { INLINE_WITHHELD_TEXT, isInlineContradiction, referenceBlockForScript, referenceStubOrNull } from './mcpl/references.js';
23
- import { NON_STRING_TEXT_PLACEHOLDER } from './mcpl/visible-content.js';
25
+ import {
26
+ AUDIO_NO_DATA_TEXT,
27
+ AUDIO_NO_MEDIA_TYPE_TEXT,
28
+ IMAGE_NO_DATA_TEXT,
29
+ IMAGE_NO_MEDIA_TYPE_TEXT,
30
+ INLINE_WITHHELD_TEXT,
31
+ buildReferenceStub,
32
+ classifyBlock,
33
+ contentClaims,
34
+ inlineNeverUri,
35
+ referenceBlockForScript,
36
+ referenceStubOrNull,
37
+ referenceTypeLabel,
38
+ resultClaims,
39
+ type ReferenceTestimony,
40
+ type ResultClaims,
41
+ type ScriptLine,
42
+ } from './mcpl/references.js';
24
43
  import { formatToolImagePlaceholder, toolResultCutAt } from './tool-image-ledger.js';
25
44
 
26
45
  /** Where one block of a content array sits in its history text, and what
27
46
  * kind of block it was: text, an inline image (its placeholder), an RFC-005
28
- * reference (its stub), or inline data withheld for a contradiction. */
47
+ * reference (its stub), inline data withheld for a contradiction, or a
48
+ * marker in place of a block the lane can't show. */
29
49
  export interface HistoryBlockSpan {
30
- kind: 'text' | 'image' | 'reference' | 'withheld';
50
+ kind: ToolResultBlock['kind'];
31
51
  start: number;
32
52
  end: number;
33
53
  }
34
54
 
55
+ /**
56
+ * Whether a tool result's live copy goes as native blocks, an image among
57
+ * them, rather than as its stored text. It does when the result holds an
58
+ * image and its links' blocks and markers come to no more than `maxChars`
59
+ * together. Past that, the live copy is the stored text, cut at the cap, so
60
+ * no number of links and markers carries a result past it. A reference's
61
+ * stub and a withheld line go whole beside an image whatever their length,
62
+ * and past its cut the stored copy keeps the newest 64 image slots the live
63
+ * copy showed (#403). Text blocks don't count either: the native copy cuts
64
+ * each at the cap on its own. Both copies decide by this, from each block's
65
+ * kind and the length of its text.
66
+ */
67
+ export function goesNative(
68
+ blocks: ReadonlyArray<{ kind: ToolResultBlock['kind']; length: number }>,
69
+ maxChars: number | undefined,
70
+ ): boolean {
71
+ if (!blocks.some((block) => block.kind === 'image')) return false;
72
+ if (!maxChars) return true;
73
+ let bounded = 0;
74
+ for (const block of blocks) if (block.kind === 'link' || block.kind === 'marker') bounded += block.length;
75
+ return bounded <= maxChars;
76
+ }
77
+
78
+ /**
79
+ * One block of a tool result's content array as both of its copies show it
80
+ * (toolResultBlocks): a text block's text, an inline image, which the stored
81
+ * copy shows as its placeholder and the live copy as itself, or the text that
82
+ * stands in its place, the same in both. That text is a reference's stub, a
83
+ * link's stub or line (MCP's resource_link, `link`), inline data withheld for
84
+ * a contradiction, or a marker saying what kind of block it was and why the
85
+ * lane can't show it.
86
+ */
87
+ export type ToolResultBlock =
88
+ | { kind: 'text'; text: string }
89
+ | { kind: 'image'; data: string; mimeType: string }
90
+ | { kind: 'reference' | 'link' | 'withheld' | 'marker'; text: string };
91
+
92
+ /** The block types MCP defines for a tool result's content. */
93
+ const MCP_BLOCK_TYPES = new Set(['text', 'image', 'audio', 'resource', 'resource_link']);
94
+
95
+ /**
96
+ * Each element of `data` as both copies of a tool result show it, or null
97
+ * when `data` isn't a content array, so the result is data, written as JSON.
98
+ * An array is content when the result says so (`dataIsContent`, which the
99
+ * framework sets on an MCPL tool's result: every element of a protocol
100
+ * result's content is a block, of a type the host shows or not). Otherwise
101
+ * it's content only when every element is a block of one of MCP's own types,
102
+ * so a list of records of other types stays data. A module's array of
103
+ * records that all carry MCP's type names, such as `[{ type: 'image', url }]`,
104
+ * is read as content, and shows a marker for each record that isn't a block
105
+ * the lane can show.
106
+ */
107
+ export function toolResultBlocks(data: unknown, dataIsContent?: boolean, claims?: ResultClaims): ToolResultBlock[] | null {
108
+ if (!isContentArray(data, dataIsContent)) return null;
109
+ // The result's claims, read as one (ResultClaims): what any of its
110
+ // references says of a uri (a `disposition: never`, an expiry) holds for
111
+ // each block that names it, whatever their order.
112
+ const read = claims ?? contentClaims(data);
113
+ // Each text well formed, as the context store makes it when it writes, so
114
+ // the copy sent and the copy stored carry the same text (#695): a stub's
115
+ // labels and a link's uri come from the block too.
116
+ return data.map((raw) => {
117
+ const block = toolResultBlock(raw, read);
118
+ return 'text' in block ? { ...block, text: block.text.toWellFormed() } : block;
119
+ });
120
+ }
121
+
122
+ /** Whether a tool result's data is a content array, which toolResultBlocks
123
+ * shows block by block. */
124
+ export function isContentArray(data: unknown, dataIsContent?: boolean): data is unknown[] {
125
+ return Array.isArray(data)
126
+ && (dataIsContent === true || data.every((element) => isPlainRecord(element) && MCP_BLOCK_TYPES.has(element.type as string)));
127
+ }
128
+
129
+ /**
130
+ * The claims of a tool result's data (ResultClaims), as showing it reads
131
+ * them: a content array's from its elements (contentClaims), and any other
132
+ * data's by the walk serialization takes (claimsInHistory). For a decision
133
+ * made about the result outside showing it, such as which of its references
134
+ * to fetch, or for showing part of it as the rest of it claims. Null where a
135
+ * value stops the walk, so what the data claims is unknown (claimsInHistory).
136
+ */
137
+ export function resultClaimsOf(data: unknown, dataIsContent?: boolean): ResultClaims | null {
138
+ return isContentArray(data, dataIsContent) ? contentClaims(data) : claimsInHistory(data);
139
+ }
140
+
141
+ /** resultClaimsOf, throwing what stops the walk, for a caller that answers
142
+ * a stop itself: a script's text route, which fails then, as its view does
143
+ * (Ada-1017, room-203 #200374). */
144
+ export function resultClaimsOrThrow(data: unknown, dataIsContent?: boolean): ResultClaims {
145
+ return isContentArray(data, dataIsContent) ? contentClaims(data) : claimsInHistoryOrThrow(data);
146
+ }
147
+
148
+ /** One element of a content array, as toolResultBlocks gives it. Only an
149
+ * image with data and a media type is shown as itself, as the push lane's
150
+ * converter shows one, and only text and images can be: this host's tool
151
+ * results carry nothing else, so inline audio is a marker here. */
152
+ function toolResultBlock(raw: unknown, claims: ResultClaims): ToolResultBlock {
153
+ const c = classifyBlock(raw);
154
+ switch (c.kind) {
155
+ case 'text':
156
+ return { kind: 'text', text: (raw as { text: string }).text };
157
+ case 'reference':
158
+ // A bounded stub, never the block's JSON, which would show its uri. A
159
+ // stub that shows an image type occupies a slot in save_recent_image's
160
+ // inventory (a failing one: its bytes are behind fetch_reference), so
161
+ // the index never slides past an image the resident saw. A link the
162
+ // host can't fetch, shown by its uri, is no slot, as no link was before
163
+ // links were references, when the result's JSON showed it. A link's
164
+ // block, stub or line, is `link`, counted toward the cap (goesNative).
165
+ return {
166
+ kind: c.testimony.fromLink ? 'link' : 'reference',
167
+ text: buildReferenceStub(c.testimony, 'from tool result', undefined, { unfetchedLinkUri: true, claims }),
168
+ };
169
+ case 'invalid':
170
+ return { kind: 'marker', text: c.shown };
171
+ case 'embedded':
172
+ // MCP's embedded resource shows as the block its contents make would
173
+ // show here. Its uri never shows.
174
+ if (c.contents.kind === 'block') return toolResultBlock(c.contents.block, claims);
175
+ return c.contents.kind === 'withheld'
176
+ ? { kind: 'withheld', text: INLINE_WITHHELD_TEXT }
177
+ : { kind: 'marker', text: c.contents.text };
178
+ case 'inline': {
179
+ if (c.contradiction) return { kind: 'withheld', text: INLINE_WITHHELD_TEXT };
180
+ const b = raw as { type: 'image' | 'audio'; data: string; mimeType?: unknown };
181
+ const image = b.type === 'image';
182
+ if (b.data === '') return { kind: 'marker', text: image ? IMAGE_NO_DATA_TEXT : AUDIO_NO_DATA_TEXT };
183
+ if (typeof b.mimeType !== 'string' || b.mimeType === '') {
184
+ return { kind: 'marker', text: image ? IMAGE_NO_MEDIA_TYPE_TEXT : AUDIO_NO_MEDIA_TYPE_TEXT };
185
+ }
186
+ if (image) return { kind: 'image', data: b.data, mimeType: b.mimeType };
187
+ return { kind: 'marker', text: toolResultAudioText(b.mimeType, b.data) };
188
+ }
189
+ }
190
+ }
191
+
192
+ /** What a tool result's inline audio shows as: its type and size, and that
193
+ * this host's tool results carry only text and images. */
194
+ function toolResultAudioText(mimeType: string, data: string): string {
195
+ const what = [referenceTypeLabel(mimeType), formatSize(Math.floor(data.length * 3 / 4))].filter(Boolean).join(', ');
196
+ return `[Audio not shown: this host's tool results carry only text and images (${what}).]`;
197
+ }
198
+
35
199
  export interface HistorySerializeOptions {
200
+ /**
201
+ * Whether the result's data is a protocol result's content
202
+ * (ToolResult.dataIsContent), so an array is shown block by block whatever
203
+ * its blocks' types (toolResultBlocks). nonErrorResultText takes it from
204
+ * the result instead, whatever this says.
205
+ */
206
+ dataIsContent?: boolean;
36
207
  /**
37
208
  * Called for every image block the serializer replaces with a placeholder
38
209
  * (issue #104). Returns the retention ref to embed in the placeholder —
@@ -43,10 +214,23 @@ export interface HistorySerializeOptions {
43
214
  /**
44
215
  * Called once a content array has been serialized block by block, rather
45
216
  * than given up on for the fallback, with where each block sits in the
46
- * joined text, before any cut at `maxChars`. The framework reads from it
47
- * what a result's live copy showed (#403).
217
+ * joined text, before any cut at `maxChars`, and the blocks themselves as
218
+ * this text was made from them. The framework reads from it what a
219
+ * result's live copy shows (#403), and builds that copy from these very
220
+ * blocks, so the two copies come from one classification.
48
221
  */
49
- onBlocks?: (spans: HistoryBlockSpan[]) => void;
222
+ onBlocks?: (spans: HistoryBlockSpan[], blocks: ToolResultBlock[]) => void;
223
+ /**
224
+ * The claims to read (ResultClaims) in place of the data's own, where the
225
+ * data shown is part of a larger result whose claims these are
226
+ * (resultClaimsOf), such as the fields a withheld result keeps.
227
+ */
228
+ claims?: ResultClaims;
229
+ /**
230
+ * Whose result this is, as nonErrorResultText's log line names it where it
231
+ * shows UNCHECKED_RESULT_TEXT for a result it couldn't read or show.
232
+ */
233
+ toolName?: string;
50
234
  }
51
235
 
52
236
  /**
@@ -85,7 +269,7 @@ export function toolResultDataToHistoryString(
85
269
  // RFC-005 shows only objects otherwise (shownInHistory), so a string has
86
270
  // nothing for historyJson to stub or withhold.
87
271
  const str = fromArray
88
- ?? (typeof data === 'string' ? data.toWellFormed() : historyJson(data))
272
+ ?? (typeof data === 'string' ? data.toWellFormed() : historyJson(data, options?.claims))
89
273
  ?? '';
90
274
  return maxChars ? truncateForHistory(str, maxChars) : str;
91
275
  }
@@ -98,6 +282,11 @@ export function toolResultDataToHistoryString(
98
282
  *
99
283
  * Each field is read once: the direct Agent API passes its caller's own
100
284
  * result here, unchecked, and a getter needn't answer the same way twice.
285
+ * Whatever stops reading or showing the data is UNCHECKED_RESULT_TEXT, with
286
+ * a log line naming the tool (`options.toolName`) and the error: the model's
287
+ * lane has no failure answer that doesn't wedge the round.
288
+ * Whether an array is content is the result's own `dataIsContent`, read for
289
+ * its truth as a result's flags are, whatever `options` says.
101
290
  *
102
291
  * Each text a tool result's copies carry is made well formed where it's
103
292
  * taken from the result: a lone UTF-16 surrogate becomes U+FFFD, as the
@@ -108,7 +297,44 @@ export function toolResultDataToHistoryString(
108
297
  * as before; a cut at a lone high half no longer steps back over it.
109
298
  */
110
299
  export function nonErrorResultText(
111
- result: { data?: unknown; error?: string },
300
+ result: { data?: unknown; error?: string; dataIsContent?: boolean },
301
+ maxChars?: number,
302
+ options?: HistorySerializeOptions,
303
+ ): string {
304
+ // The blocks go to the caller only once the text is made from them, so a
305
+ // result that shows the notice has no live copy of its own: the notice is
306
+ // both copies' text.
307
+ let blocks: [HistoryBlockSpan[], ToolResultBlock[]] | undefined;
308
+ const onBlocks = options?.onBlocks;
309
+ try {
310
+ // The result's own fields are read inside the try too: the direct Agent
311
+ // API's result is its caller's own object, whose `data` can be a getter.
312
+ const text = nonErrorResultTextOrThrow(result, maxChars, {
313
+ ...options,
314
+ ...(onBlocks ? { onBlocks: (spans: HistoryBlockSpan[], made: ToolResultBlock[]) => { blocks = [spans, made]; } } : {}),
315
+ });
316
+ if (onBlocks && blocks) onBlocks(blocks[0], blocks[1]);
317
+ return text;
318
+ } catch (error) {
319
+ // The model's lane has no failure answer that doesn't wedge the round, so
320
+ // whatever stops reading or showing a result, such as a getter that
321
+ // throws in a content array's block or a render deeper than the claims
322
+ // walk got (a script's routes call the serializer directly, and fail),
323
+ // is the notice, with a line naming the tool and the error, since it can
324
+ // also be a fault of this host's (Ada-1017, room-203 #201013).
325
+ console.error(`[tool-results] ${options?.toolName ? `tool '${options.toolName}'` : 'a tool'}: its result couldn't be read or shown, so the model reads that it was withheld: ${thrownLine(error)}`);
326
+ return maxChars ? truncateForHistory(UNCHECKED_RESULT_TEXT, maxChars) : UNCHECKED_RESULT_TEXT;
327
+ }
328
+ }
329
+
330
+ /**
331
+ * nonErrorResultText's text, throwing whatever stops reading or showing the
332
+ * result, such as a stopped claims walk (historyJson), for a caller that
333
+ * answers a stop itself: a script's routes, which fail naming the tool
334
+ * (Ada-1017, room-203 #200374), never the model lane's notice.
335
+ */
336
+ export function nonErrorResultTextOrThrow(
337
+ result: { data?: unknown; error?: string; dataIsContent?: boolean },
112
338
  maxChars?: number,
113
339
  options?: HistorySerializeOptions,
114
340
  ): string {
@@ -120,7 +346,16 @@ export function nonErrorResultText(
120
346
  return maxChars ? truncateForHistory(text, maxChars) : text;
121
347
  }
122
348
  }
123
- return toolResultDataToHistoryString(data, maxChars, options);
349
+ return toolResultDataToHistoryString(data, maxChars, { ...options, dataIsContent: Boolean(result.dataIsContent) });
350
+ }
351
+
352
+ /** The first line of what was thrown, for a log line. */
353
+ function thrownLine(error: unknown): string {
354
+ try {
355
+ return (error instanceof Error ? error.message : String(error)).split('\n')[0] || 'no reason given';
356
+ } catch {
357
+ return 'a value that can\'t be read';
358
+ }
124
359
  }
125
360
 
126
361
  /** Bounded copy of an arbitrary string with the standard truncation notice,
@@ -137,46 +372,18 @@ export function truncateForHistory(str: string, maxChars: number): string {
137
372
  }
138
373
 
139
374
  /**
140
- * If `data` is an MCP content array whose every block has a shape we
141
- * recognize, return a history-safe string (images → placeholders). Otherwise
142
- * return `null` to defer to the fallback, historyJson, which shows a
143
- * reference anywhere in the data as its stub.
375
+ * A content array's history text (toolResultBlocks): each block's text, an
376
+ * image as its placeholder, joined by newlines. Null when `data` isn't a
377
+ * content array, for the fallback, historyJson.
144
378
  */
145
379
  function tryHistoryStringFromContentArray(data: unknown, options?: HistorySerializeOptions): string | null {
146
- if (!Array.isArray(data)) return null;
147
- const parts: Array<{ kind: HistoryBlockSpan['kind']; text: string }> = [];
148
- for (const [blockIndex, raw] of data.entries()) {
149
- if (!raw || typeof raw !== 'object') return null;
150
- const b = raw as { type?: unknown; text?: unknown; data?: unknown; mimeType?: unknown };
151
- if (b.type === 'text') {
152
- // A text that isn't a string shows as the converters' placeholder,
153
- // rather than sending the whole array to the JSON fallback.
154
- // Read once: a direct Agent API result is unchecked.
155
- const text = b.text;
156
- parts.push({ kind: 'text', text: typeof text === 'string' ? text.toWellFormed() : NON_STRING_TEXT_PLACEHOLDER });
157
- } else if (isInlineContradiction(b)) {
158
- // RFC-005 vector 2: checked before the data branch so inline data
159
- // claiming bulk disposition is withheld, not rendered.
160
- parts.push({ kind: 'withheld', text: INLINE_WITHHELD_TEXT });
161
- } else if (b.type === 'image' && typeof b.data === 'string' && typeof b.mimeType === 'string') {
162
- // Decoded byte estimate from base64 length (3/4 ratio, rounded).
163
- const approxBytes = Math.floor(b.data.length * 3 / 4);
164
- const ref = options?.imageRef?.(blockIndex, { data: b.data, mimeType: b.mimeType }) ?? null;
165
- parts.push({ kind: 'image', text: formatToolImagePlaceholder(b.mimeType, formatSize(approxBytes), ref) });
166
- } else if (b.type === 'resource' || ((b.type === 'image' || b.type === 'audio') && typeof (b as { uri?: unknown }).uri === 'string')) {
167
- // RFC-005 reference block: a bounded stub, never the raw block JSON
168
- // (which used to inline the URI — and the whole array — into history).
169
- // A stub that shows an image type occupies a slot in
170
- // save_recent_image's inventory (a failing one — bytes live behind
171
- // fetch_reference), so the index never slides past an image the
172
- // resident saw.
173
- const stub = referenceStubOrNull(b, 'from tool result');
174
- if (stub === null) return null;
175
- parts.push({ kind: 'reference', text: stub });
176
- } else {
177
- return null;
178
- }
179
- }
380
+ const blocks = toolResultBlocks(data, options?.dataIsContent, options?.claims);
381
+ if (blocks === null) return null;
382
+ const parts = blocks.map((block, blockIndex): { kind: HistoryBlockSpan['kind']; text: string } => {
383
+ if (block.kind !== 'image') return block;
384
+ const ref = options?.imageRef?.(blockIndex, { data: block.data, mimeType: block.mimeType }) ?? null;
385
+ return { kind: 'image', text: storedText(block, ref) };
386
+ });
180
387
  if (options?.onBlocks) {
181
388
  const spans: HistoryBlockSpan[] = [];
182
389
  let start = 0;
@@ -184,23 +391,47 @@ function tryHistoryStringFromContentArray(data: unknown, options?: HistorySerial
184
391
  spans.push({ kind: part.kind, start, end: start + part.text.length });
185
392
  start += part.text.length + 1; // the newline that joins them
186
393
  }
187
- options.onBlocks(spans);
394
+ options.onBlocks(spans, blocks);
188
395
  }
189
396
  return parts.map((part) => part.text).join('\n');
190
397
  }
191
398
 
192
399
  /**
193
400
  * Whether RFC-005 governs a value of a tool result's data, wherever it sits.
194
- * A reference is a `resource` with a uri, at its top level or under
195
- * `resource` (where MCP embeds a resource's uri), or an `image` or `audio`
196
- * given by uri; inline data is an `image` or `audio` whose data is a string.
197
- * A uri that is null, absent or empty counts as none, and data that isn't a
198
- * string isn't inline data, so a record that only shares one of those type
199
- * names is data. A uri of any other value, string or not, counts, so a
200
- * credential fails closed.
401
+ * A reference is a `resource` with a uri at its top level, or an `image` or
402
+ * `audio` given by uri; inline data is an `image` or `audio` whose data is a
403
+ * string, or MCP's embedded resource, a `resource` with a uri under
404
+ * `resource` beside its contents. There a uri that is null, absent or empty
405
+ * counts as none, except under `resource`, where a string, an empty one
406
+ * included, tells an embedded resource, as the classifier tells it. Data
407
+ * that isn't a string isn't inline data, so a record that only shares one of
408
+ * those type names is data. A uri of any other value, string or not, counts,
409
+ * so a credential fails closed. MCP's `resource_link` (RFC-005 §8.1) is
410
+ * governed whatever value its uri has, so one whose uri isn't usable is its
411
+ * marker; one that names no uri is data here, where as a block of a content
412
+ * array, which the classifier reads, it's the marker.
201
413
  */
202
414
  function governedByRfc005(b: Record<string, unknown>): boolean {
203
- if (b.type === 'resource') return hasUri(b.uri) || (isPlainRecord(b.resource) && hasUri(b.resource.uri));
415
+ if (b.type === 'resource') {
416
+ // MCP's embedded resource is told by a string uri under `resource`, ''
417
+ // included, as classifyBlock tells it; a uri of any other value there
418
+ // counts too, failing closed.
419
+ return hasUri(b.uri) || (isPlainRecord(b.resource) && (typeof b.resource.uri === 'string' || hasUri(b.resource.uri)));
420
+ }
421
+ // A link that names a uri is governed whatever its value, so one without
422
+ // a usable uri (empty, null, not a string) is its marker here, as on the
423
+ // content lanes, never its fields. One that names none is data at depth,
424
+ // on the model's lanes and a script's, as main has every link at depth:
425
+ // its fields hold no uri RFC-005 governs, though a value in them, such as
426
+ // an untyped object's `uri`, is data as anywhere else. As a block of a
427
+ // content array on the model's line, the classifier reads it, and there
428
+ // it's the marker; a script's view of a module's array, which reads every
429
+ // block here, shows its fields, as module data reached a script on main
430
+ // (Ada-1017, room-203 #202138).
431
+ // That's the object the script lane makes of a link whose uri it leaves
432
+ // out (referenceBlockForScript), which a script's view, given it again,
433
+ // leaves as it is, as it leaves a reference's stub.
434
+ if (b.type === 'resource_link') return b.uri !== undefined;
204
435
  if (b.type === 'image' || b.type === 'audio') return typeof b.data === 'string' || hasUri(b.uri);
205
436
  return false;
206
437
  }
@@ -217,66 +448,235 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
217
448
  * A value of a tool result's data as history shows it, by RFC-005's rules,
218
449
  * wherever it sits in the data. A value RFC-005 governs (governedByRfc005) is
219
450
  * shown as the reference lanes show it: a reference as its stub, an invalid
220
- * one (such as a uri that isn't a string, or MCP's embedded resource, whose
221
- * uri isn't at its top level) as an unrecognized-block line, and inline data
222
- * that also claims a uri or a disposition as withheld, so no uri is ever
223
- * shown. Inline data that claims neither is itself, as is any other value:
224
- * a block of another type keeps its fields, a `resource_link`'s uri among
451
+ * one (such as a uri that isn't a string) as the marker saying why it isn't
452
+ * shown, MCP's embedded resource as the text its stored copy shows (its
453
+ * text, an image's placeholder or a marker), never its fields, which carry
454
+ * its uri, and inline data that also claims a uri or a disposition as
455
+ * withheld. So of the uris RFC-005 governs, the only one shown is a link's,
456
+ * in its line, where the host can't fetch the link and its uri can't carry a
457
+ * credential (shownLinkUri). Inline data that claims neither is itself, as is
458
+ * any other value: a block of another type keeps its fields, a url among
225
459
  * them, since RFC-005 governs references.
226
460
  */
227
- function shownInHistory(value: unknown): unknown {
461
+ function shownInHistory(value: unknown, claims?: ResultClaims): unknown {
462
+ return walkedInHistory(value, (b) => {
463
+ // MCP's embedded resource is the text its stored copy shows: its text,
464
+ // an image's placeholder or its marker. As JSON it would carry its uri.
465
+ // Made well formed, as a content array's texts are (toolResultBlocks),
466
+ // since at the root this text is the result's whole text, as it's sent.
467
+ if (classifyBlock(b).kind === 'embedded') return storedText(toolResultBlock(b, claims ?? resultClaims([]))).toWellFormed();
468
+ return referenceStubOrNull(b, 'from tool result', undefined, { unfetchedLinkUri: true, claims });
469
+ });
470
+ }
471
+
472
+ /**
473
+ * The one walk of a tool result's data that showing it and reading its
474
+ * claims both take: a value RFC-005 governs is text, from `asText`, except
475
+ * inline data that claims no uri or disposition, which is itself, as is any
476
+ * other value, so serialization descends into the same values each time, so
477
+ * long as each value's toJSON and getters answer the same each time they're
478
+ * read. `asText` gives a governed value's text, or null where it's itself.
479
+ */
480
+ function walkedInHistory(value: unknown, asText: (b: Record<string, unknown>) => string | null): unknown {
228
481
  if (!value || typeof value !== 'object' || Array.isArray(value)) return value;
229
482
  const b = value as Record<string, unknown>;
230
483
  if (!governedByRfc005(b)) return value;
231
484
  if ((b.type === 'image' || b.type === 'audio') && typeof b.data === 'string') {
232
- // JSON writes every field, so here inline data beside a uri of any value
485
+ // JSON writes every field, so here inline data beside a uri of any type
233
486
  // is withheld, not only beside a string one as in the lanes, which show
234
- // no uri: an object can carry a credential. An empty uri carries none.
487
+ // no uri: an object can carry a credential. A null, absent or empty uri
488
+ // carries none (hasUri).
235
489
  return hasUri(b.uri) || b.disposition !== undefined ? INLINE_WITHHELD_TEXT : value;
236
490
  }
237
- return referenceStubOrNull(b, 'from tool result') ?? value;
491
+ return asText(b) ?? value;
492
+ }
493
+
494
+ /**
495
+ * What a tool result's data claims of each uri its references name
496
+ * (ResultClaims), read by the walk serialization takes (walkedInHistory),
497
+ * through toJSON, the fields of inline data and every depth it shows: so
498
+ * every reference whose stub the result shows counts, and nothing it doesn't
499
+ * show does. A value the walk can't read or write, such as a cycle or a
500
+ * getter that throws, stops it, and then it's null, since what the rest of
501
+ * the data claims is unknown. It's a walk of its own before the one that
502
+ * shows the data, so a value's toJSON and getters run in both: one that
503
+ * answers differently the second time can claim what the shown text doesn't
504
+ * show, or show a reference the claims never read. Only data no check has
505
+ * read through JSON can do either: a module's result is parsed JSON from its
506
+ * dispatch, and an MCPL peer's blocks are parsed wire JSON, so this is the
507
+ * direct Agent API's, whose results a host gives unchecked.
508
+ */
509
+ function claimsInHistory(data: unknown): ResultClaims | null {
510
+ try {
511
+ return claimsInHistoryOrThrow(data);
512
+ } catch {
513
+ // What the rest of the data claims is unknown, so a withheld result's
514
+ // kept fields keep nothing (keptWhenWithheld).
515
+ return null;
516
+ }
517
+ }
518
+
519
+ /** claimsInHistory's walk, which throws what stops it. */
520
+ function claimsInHistoryOrThrow(data: unknown): ResultClaims {
521
+ const testimonies: ReferenceTestimony[] = [];
522
+ // Inline data's never, which walkedInHistory withholds before it asks
523
+ // `read`, and an embedded resource's, which `read` notes (inlineNeverUri).
524
+ const nevers: string[] = [];
525
+ const noteInline = (value: unknown): void => {
526
+ if (!isPlainRecord(value) || (value.type !== 'image' && value.type !== 'audio')) return;
527
+ const never = inlineNeverUri(value);
528
+ if (never !== null) nevers.push(never);
529
+ };
530
+ const read = (b: Record<string, unknown>): string | null => {
531
+ const c = classifyBlock(b);
532
+ if (c.kind === 'reference') testimonies.push(c.testimony);
533
+ const never = inlineNeverUri(b, c);
534
+ if (never !== null) nevers.push(never);
535
+ // Text ('') wherever showing gives text (shownInHistory), an embedded
536
+ // resource included, so the walk stops where showing stops, never
537
+ // reading such a value's fields, and descends only where showing does.
538
+ // Inline data never reaches here: walkedInHistory decides it first.
539
+ return c.kind === 'text' ? null : '';
540
+ };
541
+ noteInline(data);
542
+ const root = walkedInHistory(data, read);
543
+ // Its text is thrown away, so a string is written as 0, which keeps a
544
+ // long one from being copied for nothing; the walk descends as before.
545
+ if (root === data || typeof root !== 'string') {
546
+ JSON.stringify(data, (_key, value: unknown) => {
547
+ if (value !== data) noteInline(value);
548
+ const walked = walkedInHistory(value, read);
549
+ return typeof walked === 'string' ? 0 : walked;
550
+ });
551
+ }
552
+ return resultClaims(testimonies, nevers);
238
553
  }
239
554
 
240
555
  /**
241
556
  * A value of a tool result's data as a script reads it, by history's rules
242
557
  * (shownInHistory), as a JSON replacer applies them at every depth: what
243
558
  * RFC-005 governs is shown as the script lane shows it, a reference as its
244
- * stub's facts without its uri and an invalid one as the notice
245
- * (referenceBlockForScript), and inline data that also claims a uri of any
246
- * value or a disposition as the withheld notice, each as a text block. Any
247
- * other value is itself. What it gives is either a stub or a notice, which
248
- * RFC-005 doesn't govern, or the value itself, which it gives again, so
249
- * applying it twice gives what applying it once gave.
559
+ * stub's facts, a link's uri among them only where the model's line for it
560
+ * would show it, an invalid one as the notice, and MCP's embedded resource as
561
+ * the block its contents make (referenceBlockForScript), and inline data
562
+ * that also claims a uri of any type or a disposition as the withheld
563
+ * notice, each notice as a text block. A null, absent or empty uri claims
564
+ * none (hasUri), so inline data beside one is itself. Any other value is
565
+ * itself. `claims` are those of everything the script's view shows
566
+ * (scriptViewClaims), and `line` the model's line for the result, its claims
567
+ * and its records as it reads them (ScriptLine), which decide where a link's
568
+ * uri is shown. What it gives is either a stub, a notice or a text block,
569
+ * which RFC-005 doesn't govern, or inline data that claims nothing, or the
570
+ * value itself, each of which it gives again, so applying it twice gives
571
+ * what applying it once gave.
250
572
  */
251
- export function shownToScript(value: unknown): unknown {
573
+ export function shownToScript(value: unknown, claims?: ResultClaims, line?: ScriptLine): unknown {
252
574
  if (!value || typeof value !== 'object' || Array.isArray(value)) return value;
253
575
  const b = value as Record<string, unknown>;
254
576
  if (!governedByRfc005(b)) return value;
255
577
  if ((b.type === 'image' || b.type === 'audio') && typeof b.data === 'string') {
256
578
  return hasUri(b.uri) || b.disposition !== undefined ? { type: 'text', text: INLINE_WITHHELD_TEXT } : value;
257
579
  }
258
- return referenceBlockForScript(b);
580
+ return referenceBlockForScript(b, undefined, claims, line);
581
+ }
582
+
583
+ /**
584
+ * The claims (ResultClaims) of everything a script's view of a content array
585
+ * shows, nested fields included (Ada-1017, room-203 #200094), by the walk
586
+ * history's JSON takes (claimsInHistory): a script is shown the references
587
+ * nested in a block's fields, each as its stub, so a `never` among them
588
+ * withholds that uri from every block in the view. A link's uri is still
589
+ * shown only where the model's line shows it, by the claims the line reads
590
+ * (contentClaims, which reads a block and not its fields): reading more
591
+ * claims can loosen as well as tighten, since an expiry makes a link one the
592
+ * host can't fetch (referenceBlockForScript's `line`). An MCPL peer's
593
+ * top-level block the classifier rejects is
594
+ * its notice, its fields unshown, so they don't count (`fromServer`). It
595
+ * throws what stops the walk, such as a value too deep to convert or a
596
+ * getter that throws, and the script's view then isn't written: the
597
+ * treatment never runs without the claims it reads (Ada-1017, room-203
598
+ * #200250), whether or not its own walk, with other replacers and so other
599
+ * stack depths, would stop at the same value.
600
+ */
601
+ export function scriptViewClaims(content: readonly unknown[], fromServer: boolean): ResultClaims {
602
+ return claimsInHistoryOrThrow(fromServer ? content.filter((block) => classifyBlock(block).kind !== 'invalid') : content);
603
+ }
604
+
605
+ /**
606
+ * A value nested in an MCPL peer's content block as a script reads it, as a
607
+ * JSON replacer applies it inside the blocks: what RFC-005 governs
608
+ * (governedByRfc005, history's guard, since the classifier would make a
609
+ * notice of an object that isn't a content block, such as a peer's own
610
+ * nested data) gets the treatment the script lane gives a
611
+ * peer's block (referenceBlockForScript), its reference bound to the server
612
+ * that sent it, and any other value is itself. A stub or a notice isn't
613
+ * governed, so a block the top-level pass already treated is left as it is.
614
+ * `claims` are those of the script's whole view (scriptViewClaims).
615
+ */
616
+ export function shownToScriptFromServer(value: unknown, serverId: string, claims?: ResultClaims, line?: ScriptLine): unknown {
617
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return value;
618
+ const b = value as Record<string, unknown>;
619
+ return governedByRfc005(b) ? referenceBlockForScript(b, serverId, claims, line) : value;
259
620
  }
260
621
 
261
622
  /**
262
623
  * The text RFC-005 shows a value of a tool result's data as, when it shows it
263
624
  * as text rather than as its fields: a reference's stub, an invalid one's
264
- * unrecognized-block line, or inline data withheld. Null for any other value.
625
+ * marker, inline data withheld, or what an embedded resource's stored copy
626
+ * shows. Null for any other value.
265
627
  */
266
628
  export function rfc005TextOf(value: unknown): string | null {
629
+ // A single value claims only what it says itself, which its stub reads.
267
630
  const shown = shownInHistory(value);
268
631
  return shown !== value && typeof shown === 'string' ? shown : null;
269
632
  }
270
633
 
634
+ /**
635
+ * What the model's lane shows for a tool result it couldn't read through,
636
+ * check for references and show (nonErrorResultText, the lane's one catch,
637
+ * which logs the tool and the error): the tool ran, and its result isn't
638
+ * shown, rather than shown with fewer claims, where a link could show a uri a
639
+ * reference in the result withholds, and rather than a throw, which would
640
+ * leave the round unstored and unsent. Where the walk of its claims stops
641
+ * (historyJson throws then), the claims walk meets data no JSON can hold,
642
+ * such as a cycle or a BigInt, or data too deep to write, before the render
643
+ * does, so such data shows this, where the render threw (Desmond-2579's
644
+ * run); so does a render that stops after the walk, or a getter that throws
645
+ * (Ada-1017, room-203 #200344, #201013). The round completes, as a withheld
646
+ * result's kept fields keep nothing then (keptWhenWithheld). A script never
647
+ * gets it: its routes call the serializer, or nonErrorResultTextOrThrow,
648
+ * directly, whose walk throws where it stops, and fail naming the tool
649
+ * (#200374). An error result's own error field is read as before, and so
650
+ * are the fields the tool-result guard keeps of a result it withholds.
651
+ */
652
+ export const UNCHECKED_RESULT_TEXT =
653
+ "[Result not shown: the tool ran, but its result couldn't be read through, checked for references and shown, so this host withheld it.]";
654
+
271
655
  /**
272
656
  * JSON of a tool result's data with each value in it shown as history shows
273
- * it (shownInHistory), at any depth. Data that is itself shown as text, such
274
- * as a bare reference, is that text.
657
+ * it (shownInHistory), at any depth, by the claims `given` or else its own
658
+ * (claimsInHistoryOrThrow), throwing where that walk stops. Data that is
659
+ * itself shown as text, such as a bare reference, is that text.
275
660
  */
276
- function historyJson(data: unknown): string | undefined {
277
- const root = shownInHistory(data);
661
+ function historyJson(data: unknown, given?: ResultClaims): string | undefined {
662
+ // A walk that stopped can't say what the data claims, and showing it with
663
+ // no claims would let a link show a uri a reference withholds, should the
664
+ // render's own walk, with other replacers, get past the same value.
665
+ // Where the walk stops this throws: the model's lane answers with
666
+ // UNCHECKED_RESULT_TEXT and a log line (nonErrorResultText), a script's
667
+ // routes with a failure naming the tool.
668
+ const claims = given ?? claimsInHistoryOrThrow(data);
669
+ const root = shownInHistory(data, claims);
278
670
  if (root !== data && typeof root === 'string') return root;
279
- return JSON.stringify(data, (_key, value: unknown) => shownInHistory(value)) as string | undefined;
671
+ return JSON.stringify(data, (_key, value: unknown) => shownInHistory(value, claims)) as string | undefined;
672
+ }
673
+
674
+ /** A block's text in the stored copy: an image as its placeholder, with the
675
+ * retention ref given, and any other block as its text. */
676
+ function storedText(block: ToolResultBlock, ref: string | null = null): string {
677
+ if (block.kind !== 'image') return block.text;
678
+ // Decoded byte estimate from base64 length (3/4 ratio, rounded).
679
+ return formatToolImagePlaceholder(block.mimeType, formatSize(Math.floor(block.data.length * 3 / 4)), ref);
280
680
  }
281
681
 
282
682
  function formatSize(bytes: number): string {