basalt-ui 1.11.0 → 1.13.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 (301) hide show
  1. package/AGENTS.md +21 -21
  2. package/CHANGELOG.md +32 -0
  3. package/README.md +63 -51
  4. package/agent/rules/basalt-agent.md +14 -0
  5. package/agent/rules/basalt-content.md +16 -3
  6. package/configs/oxlint-plugin.js +7 -3
  7. package/configs/oxlint-plugin.test.ts +34 -5
  8. package/configs/oxlint.json +4 -4
  9. package/dist/agent/adapter.d.ts +150 -0
  10. package/dist/agent/adapter.d.ts.map +1 -0
  11. package/dist/agent/adapter.js +482 -0
  12. package/dist/agent/adapter.test.d.ts +2 -0
  13. package/dist/agent/adapter.test.d.ts.map +1 -0
  14. package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
  15. package/dist/agent/ai-sdk-transport.js +6 -2
  16. package/dist/agent/ai-sdk-transport.test.d.ts +2 -0
  17. package/dist/agent/ai-sdk-transport.test.d.ts.map +1 -0
  18. package/dist/agent/coalesce.d.ts +20 -4
  19. package/dist/agent/coalesce.d.ts.map +1 -1
  20. package/dist/agent/coalesce.js +76 -8
  21. package/dist/agent/coalesce.test.d.ts +2 -0
  22. package/dist/agent/coalesce.test.d.ts.map +1 -0
  23. package/dist/agent/foreign.test.d.ts +2 -0
  24. package/dist/agent/foreign.test.d.ts.map +1 -0
  25. package/dist/agent/id.d.ts +78 -0
  26. package/dist/agent/id.d.ts.map +1 -1
  27. package/dist/agent/id.js +30 -0
  28. package/dist/agent/id.test.d.ts +2 -0
  29. package/dist/agent/id.test.d.ts.map +1 -0
  30. package/dist/agent/index.d.ts +2 -0
  31. package/dist/agent/index.d.ts.map +1 -1
  32. package/dist/agent/index.js +3 -0
  33. package/dist/agent/merge.d.ts.map +1 -1
  34. package/dist/agent/merge.js +19 -7
  35. package/dist/agent/merge.test.d.ts +2 -0
  36. package/dist/agent/merge.test.d.ts.map +1 -0
  37. package/dist/agent/part-list.d.ts +49 -34
  38. package/dist/agent/part-list.d.ts.map +1 -1
  39. package/dist/agent/part-list.js +42 -6
  40. package/dist/agent/part-list.test.d.ts +2 -0
  41. package/dist/agent/part-list.test.d.ts.map +1 -0
  42. package/dist/agent/parts.d.ts +46 -10
  43. package/dist/agent/parts.d.ts.map +1 -1
  44. package/dist/agent/parts.js +16 -6
  45. package/dist/agent/parts.test.d.ts +2 -0
  46. package/dist/agent/parts.test.d.ts.map +1 -0
  47. package/dist/agent/thread.d.ts +18 -0
  48. package/dist/agent/thread.d.ts.map +1 -1
  49. package/dist/agent/thread.js +8 -2
  50. package/dist/agent/thread.test.d.ts +2 -0
  51. package/dist/agent/thread.test.d.ts.map +1 -0
  52. package/dist/agent/transport.test.d.ts +2 -0
  53. package/dist/agent/transport.test.d.ts.map +1 -0
  54. package/dist/agent/use-agent-stream.d.ts.map +1 -1
  55. package/dist/agent/use-agent-stream.js +2 -2
  56. package/dist/agent/use-agent-stream.test.d.ts +2 -0
  57. package/dist/agent/use-agent-stream.test.d.ts.map +1 -0
  58. package/dist/agent/use-agent-thread-runs.d.ts +24 -3
  59. package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
  60. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts +2 -0
  61. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts.map +1 -0
  62. package/dist/agent/use-agent-thread-runs.js +55 -15
  63. package/dist/agent/use-agent-thread-runs.resume.test.d.ts +2 -0
  64. package/dist/agent/use-agent-thread-runs.resume.test.d.ts.map +1 -0
  65. package/dist/agent/use-agent-thread-runs.test.d.ts +2 -0
  66. package/dist/agent/use-agent-thread-runs.test.d.ts.map +1 -0
  67. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts +2 -0
  68. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts.map +1 -0
  69. package/dist/agent-chat/composer.d.ts +130 -8
  70. package/dist/agent-chat/composer.d.ts.map +1 -1
  71. package/dist/agent-chat/composer.js +189 -23
  72. package/dist/agent-chat/composer.test.d.ts +2 -0
  73. package/dist/agent-chat/composer.test.d.ts.map +1 -0
  74. package/dist/agent-chat/index.d.ts +5 -1
  75. package/dist/agent-chat/index.d.ts.map +1 -1
  76. package/dist/agent-chat/index.js +2 -0
  77. package/dist/agent-chat/message-affordances.d.ts +59 -0
  78. package/dist/agent-chat/message-affordances.d.ts.map +1 -0
  79. package/dist/agent-chat/message-affordances.js +7 -0
  80. package/dist/agent-chat/relative-time.d.ts +27 -0
  81. package/dist/agent-chat/relative-time.d.ts.map +1 -0
  82. package/dist/agent-chat/relative-time.js +27 -0
  83. package/dist/agent-chat/relative-time.test.d.ts +2 -0
  84. package/dist/agent-chat/relative-time.test.d.ts.map +1 -0
  85. package/dist/agent-chat/thread-detail-panel.d.ts.map +1 -1
  86. package/dist/agent-chat/thread-detail-panel.js +1 -1
  87. package/dist/agent-chat/thread-feed-row.d.ts +65 -0
  88. package/dist/agent-chat/thread-feed-row.d.ts.map +1 -0
  89. package/dist/agent-chat/thread-feed-row.js +156 -0
  90. package/dist/agent-chat/thread-feed-row.test.d.ts +2 -0
  91. package/dist/agent-chat/thread-feed-row.test.d.ts.map +1 -0
  92. package/dist/agent-chat/thread-feed.d.ts +34 -5
  93. package/dist/agent-chat/thread-feed.d.ts.map +1 -1
  94. package/dist/agent-chat/thread-feed.js +52 -19
  95. package/dist/agent-chat/thread-feed.test.d.ts +2 -0
  96. package/dist/agent-chat/thread-feed.test.d.ts.map +1 -0
  97. package/dist/agent-chat/thread-message.d.ts +183 -2
  98. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  99. package/dist/agent-chat/thread-message.js +329 -37
  100. package/dist/agent-chat/thread-message.test.d.ts +2 -0
  101. package/dist/agent-chat/thread-message.test.d.ts.map +1 -0
  102. package/dist/agent-chat/thread-outcome-card.d.ts.map +1 -1
  103. package/dist/agent-chat/thread-outcome-card.js +1 -16
  104. package/dist/agent-chat/thread-workspace.d.ts +14 -2
  105. package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
  106. package/dist/agent-chat/thread-workspace.js +30 -4
  107. package/dist/agent-chat/thread-workspace.test.d.ts +2 -0
  108. package/dist/agent-chat/thread-workspace.test.d.ts.map +1 -0
  109. package/dist/agent-chat/tool-chip.test.d.ts +2 -0
  110. package/dist/agent-chat/tool-chip.test.d.ts.map +1 -0
  111. package/dist/agent-chat/virtualize.d.ts +123 -0
  112. package/dist/agent-chat/virtualize.d.ts.map +1 -0
  113. package/dist/agent-chat/virtualize.js +9 -0
  114. package/dist/agent-chat/virtualize.type-guard.test.d.ts +2 -0
  115. package/dist/agent-chat/virtualize.type-guard.test.d.ts.map +1 -0
  116. package/dist/charts/primitives/ChartFrame.test.d.ts +2 -0
  117. package/dist/charts/primitives/ChartFrame.test.d.ts.map +1 -0
  118. package/dist/charts/utils/domain.test.d.ts +2 -0
  119. package/dist/charts/utils/domain.test.d.ts.map +1 -0
  120. package/dist/cli/check-theme.test.d.ts +2 -0
  121. package/dist/cli/check-theme.test.d.ts.map +1 -0
  122. package/dist/cli/help.test.d.ts +2 -0
  123. package/dist/cli/help.test.d.ts.map +1 -0
  124. package/dist/cli/index.d.ts.map +1 -1
  125. package/dist/cli/init-sync.test.d.ts +2 -0
  126. package/dist/cli/init-sync.test.d.ts.map +1 -0
  127. package/dist/cli/placement-engine.test.d.ts +2 -0
  128. package/dist/cli/placement-engine.test.d.ts.map +1 -0
  129. package/dist/cli/tarball-parity.test.d.ts +2 -0
  130. package/dist/cli/tarball-parity.test.d.ts.map +1 -0
  131. package/dist/cli/template-format.test.d.ts +2 -0
  132. package/dist/cli/template-format.test.d.ts.map +1 -0
  133. package/dist/cli/template-ledger.test.d.ts +2 -0
  134. package/dist/cli/template-ledger.test.d.ts.map +1 -0
  135. package/dist/cli/tokens-css.test.d.ts +2 -0
  136. package/dist/cli/tokens-css.test.d.ts.map +1 -0
  137. package/dist/content/article-actions.test.d.ts +2 -0
  138. package/dist/content/article-actions.test.d.ts.map +1 -0
  139. package/dist/content/article-model.test.d.ts +2 -0
  140. package/dist/content/article-model.test.d.ts.map +1 -0
  141. package/dist/content/block-split.test.d.ts +2 -0
  142. package/dist/content/block-split.test.d.ts.map +1 -0
  143. package/dist/content/fence-block.d.ts +37 -10
  144. package/dist/content/fence-block.d.ts.map +1 -1
  145. package/dist/content/fence-block.js +80 -14
  146. package/dist/content/heading-components.d.ts.map +1 -1
  147. package/dist/content/heading-components.js +7 -4
  148. package/dist/content/heading-components.test.d.ts +2 -0
  149. package/dist/content/heading-components.test.d.ts.map +1 -0
  150. package/dist/content/index.d.ts +6 -3
  151. package/dist/content/index.d.ts.map +1 -1
  152. package/dist/content/index.js +5 -1
  153. package/dist/content/markdown.d.ts +101 -14
  154. package/dist/content/markdown.d.ts.map +1 -1
  155. package/dist/content/markdown.js +141 -28
  156. package/dist/content/markdown.test.d.ts +2 -0
  157. package/dist/content/markdown.test.d.ts.map +1 -0
  158. package/dist/content/prose.module.css.test.d.ts +2 -0
  159. package/dist/content/prose.module.css.test.d.ts.map +1 -0
  160. package/dist/content/sanitize.d.ts +170 -0
  161. package/dist/content/sanitize.d.ts.map +1 -0
  162. package/dist/content/sanitize.js +178 -0
  163. package/dist/content/sanitize.test.d.ts +2 -0
  164. package/dist/content/sanitize.test.d.ts.map +1 -0
  165. package/dist/content/slug.test.d.ts +2 -0
  166. package/dist/content/slug.test.d.ts.map +1 -0
  167. package/dist/dashboard/stat-card.test.d.ts +2 -0
  168. package/dist/dashboard/stat-card.test.d.ts.map +1 -0
  169. package/dist/data/index.d.ts +5 -1
  170. package/dist/data/index.d.ts.map +1 -1
  171. package/dist/data/virtual-list.d.ts.map +1 -1
  172. package/dist/data/virtual.d.ts +2 -1
  173. package/dist/data/virtual.d.ts.map +1 -1
  174. package/dist/guard/check-source.test.d.ts +2 -0
  175. package/dist/guard/check-source.test.d.ts.map +1 -0
  176. package/dist/guard/guard-hook.test.d.ts +2 -0
  177. package/dist/guard/guard-hook.test.d.ts.map +1 -0
  178. package/dist/index.d.ts +4 -4
  179. package/dist/index.d.ts.map +1 -1
  180. package/dist/index.js +11 -1
  181. package/dist/provider/index.test.d.ts +2 -0
  182. package/dist/provider/index.test.d.ts.map +1 -0
  183. package/dist/provider/lab-theme.test.d.ts +2 -0
  184. package/dist/provider/lab-theme.test.d.ts.map +1 -0
  185. package/dist/router-tanstack/multi-search-param-store.test.d.ts +2 -0
  186. package/dist/router-tanstack/multi-search-param-store.test.d.ts.map +1 -0
  187. package/dist/styles.floor.test.d.ts +2 -0
  188. package/dist/styles.floor.test.d.ts.map +1 -0
  189. package/dist/surfaces.d.ts +5 -5
  190. package/dist/surfaces.d.ts.map +1 -1
  191. package/dist/surfaces.js +14 -4
  192. package/dist/theme/border-coverage.test.d.ts +18 -0
  193. package/dist/theme/border-coverage.test.d.ts.map +1 -0
  194. package/dist/theme/contrast.test.d.ts +2 -0
  195. package/dist/theme/contrast.test.d.ts.map +1 -0
  196. package/dist/theme/density-option.test.d.ts +2 -0
  197. package/dist/theme/density-option.test.d.ts.map +1 -0
  198. package/dist/theme/density-relations.test.d.ts +2 -0
  199. package/dist/theme/density-relations.test.d.ts.map +1 -0
  200. package/dist/theme/derive-theme.test.d.ts +2 -0
  201. package/dist/theme/derive-theme.test.d.ts.map +1 -0
  202. package/dist/theme/fonts-option.test.d.ts +2 -0
  203. package/dist/theme/fonts-option.test.d.ts.map +1 -0
  204. package/dist/theme/radius-option.test.d.ts +2 -0
  205. package/dist/theme/radius-option.test.d.ts.map +1 -0
  206. package/dist/theme/radius.test.d.ts +2 -0
  207. package/dist/theme/radius.test.d.ts.map +1 -0
  208. package/dist/theme/shadow-surfaces.test.d.ts +26 -0
  209. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -0
  210. package/dist/theme/spacing.test.d.ts +2 -0
  211. package/dist/theme/spacing.test.d.ts.map +1 -0
  212. package/dist/theme/use-basalt-spacing.test.d.ts +2 -0
  213. package/dist/theme/use-basalt-spacing.test.d.ts.map +1 -0
  214. package/dist/theme-lab/derive-controls.test.d.ts +2 -0
  215. package/dist/theme-lab/derive-controls.test.d.ts.map +1 -0
  216. package/dist/theme-lab/derive-state.test.d.ts +2 -0
  217. package/dist/theme-lab/derive-state.test.d.ts.map +1 -0
  218. package/dist/theme-lab/theme-lab.test.d.ts +2 -0
  219. package/dist/theme-lab/theme-lab.test.d.ts.map +1 -0
  220. package/dist/tokens/build-fonts-css.test.d.ts +2 -0
  221. package/dist/tokens/build-fonts-css.test.d.ts.map +1 -0
  222. package/dist/tokens/build-radius-css.test.d.ts +2 -0
  223. package/dist/tokens/build-radius-css.test.d.ts.map +1 -0
  224. package/dist/tokens/density.test.d.ts +2 -0
  225. package/dist/tokens/density.test.d.ts.map +1 -0
  226. package/dist/tokens/derive-radius.test.d.ts +2 -0
  227. package/dist/tokens/derive-radius.test.d.ts.map +1 -0
  228. package/dist/tokens/derive.test.d.ts +2 -0
  229. package/dist/tokens/derive.test.d.ts.map +1 -0
  230. package/dist/tokens/palette.test.d.ts +2 -0
  231. package/dist/tokens/palette.test.d.ts.map +1 -0
  232. package/dist/vite-color-mix.test.d.ts +2 -0
  233. package/dist/vite-color-mix.test.d.ts.map +1 -0
  234. package/dist/vite.test.d.ts +2 -0
  235. package/dist/vite.test.d.ts.map +1 -0
  236. package/llms.txt +9 -9
  237. package/package.json +7 -2
  238. package/src/agent/adapter.test.ts +1640 -0
  239. package/src/agent/adapter.ts +945 -0
  240. package/src/agent/ai-sdk-transport.test.ts +117 -26
  241. package/src/agent/ai-sdk-transport.ts +8 -2
  242. package/src/agent/coalesce.test.ts +202 -9
  243. package/src/agent/coalesce.ts +126 -15
  244. package/src/agent/foreign.test.ts +5 -1
  245. package/src/agent/id.test.ts +101 -3
  246. package/src/agent/id.ts +118 -0
  247. package/src/agent/index.ts +8 -0
  248. package/src/agent/merge.test.ts +87 -0
  249. package/src/agent/merge.ts +57 -10
  250. package/src/agent/part-list.test.tsx +31 -1
  251. package/src/agent/part-list.tsx +77 -29
  252. package/src/agent/parts.test.ts +121 -4
  253. package/src/agent/parts.ts +78 -17
  254. package/src/agent/thread.test.ts +75 -2
  255. package/src/agent/thread.ts +25 -1
  256. package/src/agent/use-agent-stream.test.tsx +31 -5
  257. package/src/agent/use-agent-stream.ts +6 -3
  258. package/src/agent/use-agent-thread-runs.foreign.test.tsx +5 -0
  259. package/src/agent/use-agent-thread-runs.resume.test.tsx +16 -11
  260. package/src/agent/use-agent-thread-runs.test.tsx +356 -17
  261. package/src/agent/use-agent-thread-runs.ts +159 -23
  262. package/src/agent/use-agent-thread-runs.wedge.test.tsx +93 -7
  263. package/src/agent-chat/composer.test.tsx +924 -0
  264. package/src/agent-chat/composer.tsx +521 -52
  265. package/src/agent-chat/index.ts +13 -1
  266. package/src/agent-chat/message-affordances.ts +61 -0
  267. package/src/agent-chat/relative-time.test.ts +95 -0
  268. package/src/agent-chat/relative-time.ts +55 -0
  269. package/src/agent-chat/thread-detail-panel.tsx +3 -1
  270. package/src/agent-chat/thread-feed-row.test.tsx +510 -0
  271. package/src/agent-chat/thread-feed-row.tsx +274 -0
  272. package/src/agent-chat/thread-feed.test.tsx +241 -0
  273. package/src/agent-chat/thread-feed.tsx +119 -18
  274. package/src/agent-chat/thread-message.test.tsx +1078 -4
  275. package/src/agent-chat/thread-message.tsx +795 -42
  276. package/src/agent-chat/thread-outcome-card.tsx +1 -25
  277. package/src/agent-chat/thread-workspace.test.tsx +169 -0
  278. package/src/agent-chat/thread-workspace.tsx +70 -6
  279. package/src/agent-chat/virtualize.ts +132 -0
  280. package/src/agent-chat/virtualize.type-guard.test.ts +49 -0
  281. package/src/cli/index.ts +0 -1
  282. package/src/cli/template-format.test.ts +2 -2
  283. package/src/content/fence-block.tsx +242 -15
  284. package/src/content/heading-components.test.tsx +65 -0
  285. package/src/content/heading-components.tsx +26 -5
  286. package/src/content/index.ts +19 -3
  287. package/src/content/markdown.test.tsx +603 -0
  288. package/src/content/markdown.tsx +355 -35
  289. package/src/content/sanitize.test.ts +659 -0
  290. package/src/content/sanitize.ts +510 -0
  291. package/src/dashboard/stat-card.test.tsx +6 -1
  292. package/src/data/index.ts +5 -1
  293. package/src/data/virtual-list.tsx +2 -1
  294. package/src/data/virtual.ts +2 -1
  295. package/src/index.ts +18 -1
  296. package/src/surfaces.ts +13 -3
  297. package/src/theme/contrast.test.ts +1 -4
  298. package/src/theme/density-relations.test.ts +4 -2
  299. package/src/theme/fonts-option.test.ts +7 -1
  300. package/src/theme/shadow-surfaces.test.ts +21 -6
  301. package/src/theme/use-basalt-spacing.test.tsx +2 -1
@@ -5,11 +5,14 @@
5
5
  * `output` field, so the old `part.state === 'output-available'`-only body left a failed call
6
6
  * rendering as name+input and nothing else — the failure was silently invisible. Same gap for
7
7
  * `output-denied`, indistinguishable from a still-pending call.
8
+ *
9
+ * Plus the `settled` flag's forwarding contract — the field that lets a text renderer drop out of
10
+ * streaming mode once the message it belongs to has finished.
8
11
  */
9
12
  import { cleanup, render, screen } from '@testing-library/react'
10
13
  import { afterEach, describe, expect, test } from 'bun:test'
11
14
  import { PartList } from './part-list'
12
- import type { ToolCallPart } from './parts'
15
+ import type { AgentPart, ToolCallPart } from './parts'
13
16
 
14
17
  afterEach(cleanup)
15
18
 
@@ -80,3 +83,30 @@ describe('DefaultToolCall — terminal states stay visible', () => {
80
83
  expect(screen.queryByText(/Denied/)).toBeNull()
81
84
  })
82
85
  })
86
+
87
+ describe('settled — forwarded to every renderer', () => {
88
+ const TEXT: AgentPart = { id: 't1', type: 'text', text: 'hello' }
89
+
90
+ function settledProbe() {
91
+ const seen: boolean[] = []
92
+ const components = {
93
+ text: ({ settled }: { settled: boolean }) => {
94
+ seen.push(settled)
95
+ return <div data-testid="probe">{String(settled)}</div>
96
+ },
97
+ }
98
+ return { seen, components }
99
+ }
100
+
101
+ test('defaults to true — a caller that knows nothing about a run renders a finished list', () => {
102
+ const { seen, components } = settledProbe()
103
+ render(<PartList parts={[TEXT]} components={components} />)
104
+ expect(seen).toEqual([true])
105
+ })
106
+
107
+ test('an explicit settled={false} reaches the renderer', () => {
108
+ const { seen, components } = settledProbe()
109
+ render(<PartList parts={[TEXT]} components={components} settled={false} />)
110
+ expect(seen).toEqual([false])
111
+ })
112
+ })
@@ -19,7 +19,9 @@
19
19
  * <PartList
20
20
  * parts={parts}
21
21
  * components={{
22
- * text: ({ part }) => <Markdown streaming density="chat">{part.text}</Markdown>,
22
+ * text: ({ part, settled }) => (
23
+ * <Markdown streaming={!settled} contentTrust="untrusted" density="chat">{part.text}</Markdown>
24
+ * ),
23
25
  * }}
24
26
  * />
25
27
  */
@@ -38,37 +40,48 @@ import type {
38
40
 
39
41
  // ── Per-type renderer signatures ──────────────────────────────────────────────
40
42
 
41
- export type TextPartRenderer<TPart extends AgentPart = AgentPart> = (props: {
42
- part: Extract<TPart, { type: 'text' }>
43
+ /**
44
+ * The per-renderer argument. `settled` is `false` only while the message these parts belong to is
45
+ * still streaming — it converges this map toward the `PartRenderContext` a foreign renderer
46
+ * already receives (`./foreign`), rather than adding a third shape. Additive and
47
+ * source-compatible: an existing `({ part }) => …` renderer simply ignores it.
48
+ *
49
+ * Settlement is MONOTONE — a message that has settled never un-settles.
50
+ */
51
+ type PartRenderArgs<TPart> = {
52
+ part: TPart
43
53
  index: number
44
- }) => JSX.Element | null
54
+ settled: boolean
55
+ }
45
56
 
46
- export type ReasoningPartRenderer<TPart extends AgentPart = AgentPart> = (props: {
47
- part: Extract<TPart, { type: 'reasoning' }>
48
- index: number
49
- }) => JSX.Element | null
57
+ export type TextPartRenderer<TPart extends AgentPart = AgentPart> = (
58
+ props: PartRenderArgs<Extract<TPart, { type: 'text' }>>,
59
+ ) => JSX.Element | null
50
60
 
51
- export type ToolCallPartRenderer<TPart extends AgentPart = AgentPart> = (props: {
52
- part: Extract<TPart, { type: 'tool' }>
53
- index: number
54
- }) => JSX.Element | null
61
+ export type ReasoningPartRenderer<TPart extends AgentPart = AgentPart> = (
62
+ props: PartRenderArgs<Extract<TPart, { type: 'reasoning' }>>,
63
+ ) => JSX.Element | null
55
64
 
56
- export type SourcePartRenderer<TPart extends AgentPart = AgentPart> = (props: {
57
- part: Extract<TPart, { type: 'source' }>
58
- index: number
59
- }) => JSX.Element | null
65
+ export type ToolCallPartRenderer<TPart extends AgentPart = AgentPart> = (
66
+ props: PartRenderArgs<Extract<TPart, { type: 'tool' }>>,
67
+ ) => JSX.Element | null
60
68
 
61
- export type ErrorPartRenderer<TPart extends AgentPart = AgentPart> = (props: {
62
- part: Extract<TPart, { type: 'error' }>
63
- index: number
64
- }) => JSX.Element | null
69
+ export type SourcePartRenderer<TPart extends AgentPart = AgentPart> = (
70
+ props: PartRenderArgs<Extract<TPart, { type: 'source' }>>,
71
+ ) => JSX.Element | null
72
+
73
+ export type ErrorPartRenderer<TPart extends AgentPart = AgentPart> = (
74
+ props: PartRenderArgs<Extract<TPart, { type: 'error' }>>,
75
+ ) => JSX.Element | null
65
76
 
66
77
  /**
67
78
  * Partial map of per-type renderers. Any omitted key falls back to the headless default.
68
79
  *
69
80
  * @example
70
81
  * const renderers: Partial<AgentPartRenderers> = {
71
- * text: ({ part }) => <Markdown streaming density="chat">{part.text}</Markdown>,
82
+ * text: ({ part, settled }) => (
83
+ * <Markdown streaming={!settled} contentTrust="untrusted" density="chat">{part.text}</Markdown>
84
+ * ),
72
85
  * }
73
86
  */
74
87
  export type AgentPartRenderers<TPart extends AgentPart = AgentPart> = {
@@ -231,10 +244,17 @@ export type PartListProps<TPart extends AgentPart = AgentPart> = {
231
244
  readonly parts: TPart[]
232
245
  /**
233
246
  * Override individual part renderers. Omitted keys fall back to the headless defaults.
234
- * Pass `{ text: ({ part }) => <Markdown streaming density="chat">{part.text}</Markdown> }` to
235
- * enable rich markdown rendering.
247
+ * Pass `{ text: ({ part, settled }) => <Markdown streaming={!settled} contentTrust="untrusted" density="chat">{part.text}</Markdown> }`
248
+ * to enable rich markdown rendering.
236
249
  */
237
250
  readonly components?: Partial<AgentPartRenderers<TPart>>
251
+ /**
252
+ * `false` while the message these parts belong to is still streaming — forwarded to every
253
+ * renderer so a text renderer can drop out of streaming mode (and a fenced mermaid/diagram can
254
+ * upgrade) once the turn finishes. Defaults to `true`: a caller that knows nothing about a run
255
+ * is rendering a finished list, and un-settled must be opted into by the thing that does know.
256
+ */
257
+ readonly settled?: boolean
238
258
  }
239
259
 
240
260
  /**
@@ -251,11 +271,19 @@ export type PartListProps<TPart extends AgentPart = AgentPart> = {
251
271
  * // With basalt's own Markdown for the text renderer:
252
272
  * import { PartList } from 'basalt-ui/agent'
253
273
  * import { Markdown } from 'basalt-ui/content'
254
- * <PartList parts={parts} components={{ text: ({ part }) => <Markdown streaming density="chat">{part.text}</Markdown> }} />
274
+ * <PartList
275
+ * parts={parts}
276
+ * components={{
277
+ * text: ({ part, settled }) => (
278
+ * <Markdown streaming={!settled} contentTrust="untrusted" density="chat">{part.text}</Markdown>
279
+ * ),
280
+ * }}
281
+ * />
255
282
  */
256
283
  export function PartList<TPart extends AgentPart = AgentPart>({
257
284
  parts,
258
285
  components,
286
+ settled = true,
259
287
  }: PartListProps<TPart>): JSX.Element {
260
288
  // Memoised to avoid rebuilding the renderer map on every streaming re-render (hot path).
261
289
  // Cast required because DEFAULT_RENDERERS is typed for the base AgentPart, not the generic TPart.
@@ -277,7 +305,11 @@ export function PartList<TPart extends AgentPart = AgentPart>({
277
305
  const Render = renderers.text
278
306
  return (
279
307
  <Fragment key={part.id}>
280
- <Render part={part as Extract<TPart, { type: 'text' }>} index={index} />
308
+ <Render
309
+ part={part as Extract<TPart, { type: 'text' }>}
310
+ index={index}
311
+ settled={settled}
312
+ />
281
313
  </Fragment>
282
314
  )
283
315
  }
@@ -285,7 +317,11 @@ export function PartList<TPart extends AgentPart = AgentPart>({
285
317
  const Render = renderers.reasoning
286
318
  return (
287
319
  <Fragment key={part.id}>
288
- <Render part={part as Extract<TPart, { type: 'reasoning' }>} index={index} />
320
+ <Render
321
+ part={part as Extract<TPart, { type: 'reasoning' }>}
322
+ index={index}
323
+ settled={settled}
324
+ />
289
325
  </Fragment>
290
326
  )
291
327
  }
@@ -293,7 +329,11 @@ export function PartList<TPart extends AgentPart = AgentPart>({
293
329
  const Render = renderers.tool
294
330
  return (
295
331
  <Fragment key={part.id}>
296
- <Render part={part as Extract<TPart, { type: 'tool' }>} index={index} />
332
+ <Render
333
+ part={part as Extract<TPart, { type: 'tool' }>}
334
+ index={index}
335
+ settled={settled}
336
+ />
297
337
  </Fragment>
298
338
  )
299
339
  }
@@ -301,7 +341,11 @@ export function PartList<TPart extends AgentPart = AgentPart>({
301
341
  const Render = renderers.source
302
342
  return (
303
343
  <Fragment key={part.id}>
304
- <Render part={part as Extract<TPart, { type: 'source' }>} index={index} />
344
+ <Render
345
+ part={part as Extract<TPart, { type: 'source' }>}
346
+ index={index}
347
+ settled={settled}
348
+ />
305
349
  </Fragment>
306
350
  )
307
351
  }
@@ -309,7 +353,11 @@ export function PartList<TPart extends AgentPart = AgentPart>({
309
353
  const Render = renderers.error
310
354
  return (
311
355
  <Fragment key={part.id}>
312
- <Render part={part as Extract<TPart, { type: 'error' }>} index={index} />
356
+ <Render
357
+ part={part as Extract<TPart, { type: 'error' }>}
358
+ index={index}
359
+ settled={settled}
360
+ />
313
361
  </Fragment>
314
362
  )
315
363
  }
@@ -3,14 +3,16 @@
3
3
  * AgentPart wire format, post-1.11.0 part identity + the seven-state ToolCallPart union.
4
4
  *
5
5
  * Every AgentPart variant below is validated against parts.ts's `parseAgentPart`, `isStartPart`,
6
- * and `isToolCallSettled` as they read right now: `id` is required on every variant, and `tool`
7
- * parts are validated against the seven-state discriminated union mirroring AI SDK v7's
6
+ * and `isToolCallSettled` as they read right now: `id` is required on every variant, `toolCallId`
7
+ * is required on the three in-flight tool states and optional on the four settled ones (see the
8
+ * per-state identity block below), and `tool` parts are validated against the seven-state
9
+ * discriminated union mirroring AI SDK v7's
8
10
  * `UIToolInvocation` (see parts.ts's ToolCallPart doc for the ground-truth corrections against the
9
11
  * design spec).
10
12
  */
11
13
  import { describe, expect, test } from 'bun:test'
12
14
  import { isStartPart, isToolCallSettled, parseAgentPart } from './parts'
13
- import type { AgentPart, ToolCallPart } from './parts'
15
+ import type { AgentPart, ToolCallPart, ToolCallState } from './parts'
14
16
 
15
17
  /** JSON round-trip a value through JSON.stringify → JSON.parse, mirroring wire transport. */
16
18
  function roundTrip(value: unknown): unknown {
@@ -109,10 +111,11 @@ describe('parseAgentPart', () => {
109
111
  })
110
112
 
111
113
  describe('tool', () => {
112
- test('rejects a missing toolCallId', () => {
114
+ test('rejects a non-string toolCallId', () => {
113
115
  const raw = {
114
116
  id: 'p1',
115
117
  type: 'tool',
118
+ toolCallId: 42,
116
119
  toolName: 'search',
117
120
  state: 'input-available',
118
121
  input: {},
@@ -120,6 +123,120 @@ describe('parseAgentPart', () => {
120
123
  expect(parseAgentPart(raw)).toBeNull()
121
124
  })
122
125
 
126
+ // ── toolCallId identity, decided per state ──────────────────────────────
127
+ // An empty string is not a value any state accepts: '' normalizes to absent right here, so the
128
+ // pre-1.12.0 sentinel can never reach the type or a consumer of it. What an ABSENT id then
129
+ // means is a per-state decision (see Identified / MaybeIdentified in parts.ts):
130
+ // in-flight (input-streaming, input-available, approval-requested) → REJECTED. The part
131
+ // exists only to be closed by a later state, which re-supplies its toolName and input;
132
+ // unidentified it could never be closed and would render as a forever-pending chip.
133
+ // settled (approval-responded + the three terminal states) → KEPT, unidentified.
134
+ // It carries the OUTCOME; dropping it would erase how the call ended (silent data loss),
135
+ // while keeping it costs only the fold — coalesceParts renders it as its own block.
136
+ //
137
+ // The `satisfies Record<ToolCallState, …>` below is an authoring-time exhaustiveness gate: add
138
+ // a state to the union and this fixture stops typechecking until its identity behaviour is
139
+ // decided here too. It is an EDITOR/tsc guard only — the package tsconfig excludes `*.test.ts`
140
+ // from `bun run typecheck`, and `bun test` type-strips rather than typechecks — so the
141
+ // "covers every state exactly once" test below backs it with a runtime check of the same claim.
142
+
143
+ const UNIDENTIFIED_RAW = {
144
+ 'input-streaming': { toolName: 'search', state: 'input-streaming' },
145
+ 'input-available': { toolName: 'search', state: 'input-available', input: { q: 'x' } },
146
+ 'approval-requested': {
147
+ toolName: 'search',
148
+ state: 'approval-requested',
149
+ input: { q: 'x' },
150
+ approval: { id: 'appr-1' },
151
+ },
152
+ 'approval-responded': {
153
+ toolName: 'search',
154
+ state: 'approval-responded',
155
+ input: { q: 'x' },
156
+ approval: { id: 'appr-1', approved: true },
157
+ },
158
+ 'output-available': {
159
+ toolName: 'search',
160
+ state: 'output-available',
161
+ input: { q: 'x' },
162
+ output: { hits: 3 },
163
+ },
164
+ 'output-error': { toolName: 'search', state: 'output-error', errorText: 'boom' },
165
+ 'output-denied': {
166
+ toolName: 'search',
167
+ state: 'output-denied',
168
+ input: { q: 'x' },
169
+ approval: { id: 'appr-1', approved: false },
170
+ },
171
+ } satisfies Record<ToolCallState, Record<string, unknown>>
172
+
173
+ const IN_FLIGHT_STATES = [
174
+ 'input-streaming',
175
+ 'input-available',
176
+ 'approval-requested',
177
+ ] as const satisfies readonly ToolCallState[]
178
+
179
+ const SETTLED_STATES = [
180
+ 'approval-responded',
181
+ 'output-available',
182
+ 'output-error',
183
+ 'output-denied',
184
+ ] as const satisfies readonly ToolCallState[]
185
+
186
+ test('the in-flight/settled split covers every tool state exactly once', () => {
187
+ // Object.keys() is always typed string[] regardless of the object's key type — cast back to
188
+ // ToolCallState[], which every key of UNIDENTIFIED_RAW genuinely is (used as
189
+ // UNIDENTIFIED_RAW[state] elsewhere in this file).
190
+ expect([...IN_FLIGHT_STATES, ...SETTLED_STATES].toSorted()).toEqual(
191
+ (Object.keys(UNIDENTIFIED_RAW) as ToolCallState[]).toSorted(),
192
+ )
193
+ })
194
+
195
+ for (const state of IN_FLIGHT_STATES) {
196
+ test(`${state}: rejects a missing toolCallId — an in-flight part that could never be closed`, () => {
197
+ expect(parseAgentPart({ id: 'p1', type: 'tool', ...UNIDENTIFIED_RAW[state] })).toBeNull()
198
+ })
199
+
200
+ test(`${state}: rejects an empty-string toolCallId identically (it means absent)`, () => {
201
+ const raw = { id: 'p1', type: 'tool', toolCallId: '', ...UNIDENTIFIED_RAW[state] }
202
+ expect(parseAgentPart(raw)).toBeNull()
203
+ })
204
+ }
205
+
206
+ for (const state of SETTLED_STATES) {
207
+ test(`${state}: a missing toolCallId keeps the part UNIDENTIFIED — it is never dropped`, () => {
208
+ const parsed = parseAgentPart({ id: 'p1', type: 'tool', ...UNIDENTIFIED_RAW[state] })
209
+ expect(parsed).not.toBeNull()
210
+ expect((parsed as ToolCallPart).state).toBe(state)
211
+ expect(Object.hasOwn(parsed as object, 'toolCallId')).toBe(false)
212
+ })
213
+
214
+ test(`${state}: an empty-string toolCallId normalizes to absent, never to ""`, () => {
215
+ const raw = { id: 'p1', type: 'tool', toolCallId: '', ...UNIDENTIFIED_RAW[state] }
216
+ const parsed = parseAgentPart(raw)
217
+ expect(parsed).not.toBeNull()
218
+ expect(Object.hasOwn(parsed as object, 'toolCallId')).toBe(false)
219
+ })
220
+
221
+ test(`${state}: a real toolCallId still carries through`, () => {
222
+ const raw = { id: 'p1', type: 'tool', toolCallId: 'call-1', ...UNIDENTIFIED_RAW[state] }
223
+ expect((parseAgentPart(raw) as ToolCallPart | null)?.toolCallId).toBe('call-1')
224
+ })
225
+ }
226
+
227
+ test('approval-responded: a real toolCallId round-trips through JSON', () => {
228
+ const part: ToolCallPart = {
229
+ id: 'p1',
230
+ type: 'tool',
231
+ toolCallId: 'call-1',
232
+ toolName: 'delete_file',
233
+ state: 'approval-responded',
234
+ input: { path: '/tmp/x' },
235
+ approval: { id: 'appr-1', approved: true },
236
+ }
237
+ expect(parseAgentPart(roundTrip(part))).toEqual(part)
238
+ })
239
+
123
240
  test('rejects a missing toolName', () => {
124
241
  const raw = {
125
242
  id: 'p1',
@@ -3,7 +3,10 @@
3
3
  *
4
4
  * Exhaustiveness is enforced at every switch via assertNever (imported from '../register').
5
5
  * Adding a new variant without a matching case is a tsc error — see the fixture in
6
- * apps/playground/src/demo/agent-part.type-guard.ts.
6
+ * apps/playground/src/demo/agent-part.type-guard.ts. One caveat for code that folds WIRE data on
7
+ * the render path (`coalesceParts`): keep the compile-time guarantee — bind the value at `never` —
8
+ * but do NOT let the runtime throw, or one malformed chunk kills the whole transcript. See
9
+ * `mergeToolPart`'s default branch for that shape.
7
10
  *
8
11
  * Eden #231 doctrine: the server-side Elysia route MUST declare `: AsyncGenerator<AgentPart>`
9
12
  * explicitly and carry NO t.Object/t.Union response schema. Validate at yield-time. See
@@ -78,6 +81,10 @@ export type ReasoningPart = PartBase & {
78
81
  // discriminator `tool-${NAME}`. It IS a required field here (ours, not the SDK's) because the chip
79
82
  // needs it; a transport mapping from the SDK must DERIVE it from the discriminator, not read it off
80
83
  // the invocation.
84
+ //
85
+ // `toolCallId` is required on the three IN-FLIGHT states and OPTIONAL on the four SETTLED ones —
86
+ // see `Identified` / `MaybeIdentified` below. The absence is modelled in the type, never as a
87
+ // sentinel value.
81
88
 
82
89
  /** The nested approval envelope — mirrors the SDK's shape verbatim, never flattened. */
83
90
  type ToolApproval = {
@@ -90,35 +97,66 @@ type ToolApproval = {
90
97
 
91
98
  type ToolCallBase = PartBase & {
92
99
  readonly type: 'tool'
93
- /** Required on all seven states (was optional — see parts.ts history). */
94
- readonly toolCallId: string
95
100
  readonly toolName: string
96
101
  /** Wall-clock time from first sighting of this toolCallId to its terminal state. */
97
102
  readonly durationMs?: number
98
103
  readonly providerExecuted?: boolean
99
104
  }
100
105
 
106
+ /**
107
+ * The three IN-FLIGHT states (`input-streaming`, `input-available`, `approval-requested`) carry an
108
+ * authoritative `toolCallId` — the merge key, and for them it is structural. An in-flight part
109
+ * exists only to be superseded by the settled part for the same call; with no merge key it can
110
+ * never be closed, so it would render as a forever-pending chip standing beside the real result.
111
+ * Nothing is lost by rejecting an unidentified one either: the `toolName` and `input` it carries
112
+ * are re-supplied on the settled part. `parseToolCallPart` therefore rejects these when the id is
113
+ * missing or empty.
114
+ */
115
+ type Identified = ToolCallBase & { readonly toolCallId: string }
116
+
117
+ /**
118
+ * The four SETTLED states (`approval-responded` plus the three terminal ones) may legitimately
119
+ * arrive without a `toolCallId`, for two different reasons.
120
+ *
121
+ * `approval-responded` is where the WIRE omits it: the SDK's `tool-approval-response` chunk
122
+ * carries none, and it is resolved by reverse lookup on `approval.id` (coalesce.ts keeps the
123
+ * approvalId -> toolCallId side index that does it).
124
+ *
125
+ * The three terminal states admit the absence for a different reason — tolerance, not wire shape.
126
+ * They are self-contained: each carries the OUTCOME of the call, so a missing id costs only the
127
+ * fold (`coalesceParts` keeps an unresolvable part as its own block and never discards it).
128
+ * Rejecting one instead would delete the only record of how the call ended — silent data loss in a
129
+ * transcript, strictly worse than an unfolded block.
130
+ *
131
+ * The absence is modelled here rather than smuggled through an empty-string sentinel: the union
132
+ * previously declared an always-meaningful `string` that the `approval-responded` wire shape
133
+ * cannot supply, which forced a `''` sentinel into the type and an `as unknown as` cast into every
134
+ * test that built one. A `''` toolCallId is not a value any state accepts; `parseToolCallPart`
135
+ * normalizes one to absent at the wire boundary, so it never reaches the type or a consumer.
136
+ */
137
+ type MaybeIdentified = ToolCallBase & { readonly toolCallId?: string }
138
+
101
139
  export type ToolCallPart =
102
- | (ToolCallBase & { readonly state: 'input-streaming'; readonly input?: unknown })
103
- | (ToolCallBase & { readonly state: 'input-available'; readonly input: unknown })
104
- | (ToolCallBase & {
140
+ | (Identified & { readonly state: 'input-streaming'; readonly input?: unknown })
141
+ | (Identified & { readonly state: 'input-available'; readonly input: unknown })
142
+ | (Identified & {
105
143
  readonly state: 'approval-requested'
106
144
  readonly input: unknown
107
145
  readonly approval: ToolApproval & { readonly approved?: never; readonly reason?: never }
108
146
  })
109
- | (ToolCallBase & {
147
+ | (MaybeIdentified & {
110
148
  readonly state: 'approval-responded'
111
149
  readonly input: unknown
112
150
  readonly approval: ToolApproval & { readonly approved: boolean }
113
151
  })
114
- | (ToolCallBase & {
152
+ | (MaybeIdentified & {
115
153
  readonly state: 'output-available'
116
154
  readonly input: unknown
117
155
  readonly output: unknown
118
156
  readonly preliminary?: boolean
119
157
  readonly approval?: ToolApproval & { readonly approved?: true }
120
158
  })
121
- | (ToolCallBase & {
159
+ | (MaybeIdentified & {
122
160
  readonly state: 'output-error'
123
161
  readonly input?: unknown
124
162
  /** The SDK's field name — there is no field named `error` anywhere in the union. */
@@ -127,7 +165,7 @@ export type ToolCallPart =
127
165
  readonly rawInput?: unknown
128
166
  readonly approval?: ToolApproval & { readonly approved?: true }
129
167
  })
130
- | (ToolCallBase & {
168
+ | (MaybeIdentified & {
131
169
  readonly state: 'output-denied'
132
170
  readonly input: unknown
133
171
  readonly approval: ToolApproval & { readonly approved: false }
@@ -236,35 +274,53 @@ export function parseAgentPart(raw: unknown): AgentPart | null {
236
274
  }
237
275
 
238
276
  /** Validates the state-discriminated `tool` part shape. Rejects the pre-1.11.0 flat shape
239
- * (`{type:'tool', output}` with no `state`) and any unrecognized `state` value. */
277
+ * (`{type:'tool', output}` with no `state`) and any unrecognized `state` value.
278
+ *
279
+ * An empty-string `toolCallId` is normalized to absent here — the one place the wire's degenerate
280
+ * shape is decoded — so `''` never reaches the type or any consumer of it. What an ABSENT id then
281
+ * means is decided per state, exactly as the union declares it (`Identified` /
282
+ * `MaybeIdentified`): fatal on the three in-flight states, which exist only to be closed by a
283
+ * later one and are re-supplied by it; survivable on the four settled states, which carry the
284
+ * outcome and are kept UNIDENTIFIED rather than dropped. Dropping a settled part would be silent
285
+ * data loss — the transcript would simply forget how the call ended. */
240
286
  function parseToolCallPart(id: string, obj: Record<string, unknown>): ToolCallPart | null {
241
- if (typeof obj['toolCallId'] !== 'string') return null
242
287
  if (typeof obj['toolName'] !== 'string') return null
243
288
  if (typeof obj['state'] !== 'string') return null
244
289
 
245
- const { toolCallId, toolName } = obj as { toolCallId: string; toolName: string }
290
+ const rawToolCallId = obj['toolCallId']
291
+ if (rawToolCallId !== undefined && typeof rawToolCallId !== 'string') return null
292
+ const toolCallId =
293
+ typeof rawToolCallId === 'string' && rawToolCallId.length > 0 ? rawToolCallId : undefined
294
+
295
+ const toolName = obj['toolName']
246
296
  const common = {
247
297
  id,
248
298
  type: 'tool' as const,
249
- toolCallId,
250
299
  toolName,
251
300
  ...(typeof obj['durationMs'] === 'number' ? { durationMs: obj['durationMs'] } : {}),
252
301
  ...(typeof obj['providerExecuted'] === 'boolean'
253
302
  ? { providerExecuted: obj['providerExecuted'] }
254
303
  : {}),
255
304
  }
305
+ // The three in-flight states below require an identified call; the four settled ones spread
306
+ // `identity` instead, which is simply empty when the wire supplied no usable id.
307
+ const identified = toolCallId !== undefined ? { ...common, toolCallId } : null
308
+ const identity = toolCallId !== undefined ? { toolCallId } : {}
256
309
 
257
310
  switch (obj['state']) {
258
311
  case 'input-streaming':
312
+ if (identified === null) return null
259
313
  return {
260
- ...common,
314
+ ...identified,
261
315
  state: 'input-streaming',
262
316
  ...('input' in obj ? { input: obj['input'] } : {}),
263
317
  }
264
318
  case 'input-available':
319
+ if (identified === null) return null
265
320
  if (!('input' in obj)) return null
266
- return { ...common, state: 'input-available', input: obj['input'] }
321
+ return { ...identified, state: 'input-available', input: obj['input'] }
267
322
  case 'approval-requested': {
323
+ if (identified === null) return null
268
324
  if (!('input' in obj)) return null
269
325
  const approval = parseApproval(obj['approval'])
270
326
  // The type declares this state's approval as `{ approved?: never; reason?: never }` — a
@@ -274,7 +330,7 @@ function parseToolCallPart(id: string, obj: Record<string, unknown>): ToolCallPa
274
330
  return null
275
331
  }
276
332
  return {
277
- ...common,
333
+ ...identified,
278
334
  state: 'approval-requested',
279
335
  input: obj['input'],
280
336
  approval: approval as ToolApproval & { approved?: never; reason?: never },
@@ -284,8 +340,10 @@ function parseToolCallPart(id: string, obj: Record<string, unknown>): ToolCallPa
284
340
  if (!('input' in obj)) return null
285
341
  const approval = parseApproval(obj['approval'])
286
342
  if (approval === null || typeof approval.approved !== 'boolean') return null
343
+ // The state the WIRE ships unidentified — `approval.id` is then its only addressing key.
287
344
  return {
288
345
  ...common,
346
+ ...identity,
289
347
  state: 'approval-responded',
290
348
  input: obj['input'],
291
349
  approval: approval as ToolApproval & { approved: boolean },
@@ -297,6 +355,7 @@ function parseToolCallPart(id: string, obj: Record<string, unknown>): ToolCallPa
297
355
  if (approval === null || approval.approved !== false) return null
298
356
  return {
299
357
  ...common,
358
+ ...identity,
300
359
  state: 'output-denied',
301
360
  input: obj['input'],
302
361
  approval: approval as ToolApproval & { approved: false },
@@ -315,6 +374,7 @@ function parseToolCallPart(id: string, obj: Record<string, unknown>): ToolCallPa
315
374
  }
316
375
  return {
317
376
  ...common,
377
+ ...identity,
318
378
  state: 'output-available',
319
379
  input: obj['input'],
320
380
  output: obj['output'],
@@ -332,6 +392,7 @@ function parseToolCallPart(id: string, obj: Record<string, unknown>): ToolCallPa
332
392
  }
333
393
  return {
334
394
  ...common,
395
+ ...identity,
335
396
  state: 'output-error',
336
397
  errorText: obj['errorText'],
337
398
  ...('input' in obj ? { input: obj['input'] } : {}),