basalt-ui 1.10.0 → 1.12.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.
- package/AGENTS.md +21 -21
- package/CHANGELOG.md +25 -0
- package/README.md +36 -27
- package/agent/rules/basalt-content.md +16 -3
- package/dist/agent/adapter.d.ts +146 -0
- package/dist/agent/adapter.d.ts.map +1 -0
- package/dist/agent/adapter.js +473 -0
- package/dist/agent/adapter.test.d.ts +2 -0
- package/dist/agent/adapter.test.d.ts.map +1 -0
- package/dist/agent/ai-sdk-transport.d.ts +3 -52
- package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
- package/dist/agent/ai-sdk-transport.js +108 -24
- package/dist/agent/ai-sdk-transport.test.d.ts +2 -0
- package/dist/agent/ai-sdk-transport.test.d.ts.map +1 -0
- package/dist/agent/coalesce.d.ts +42 -0
- package/dist/agent/coalesce.d.ts.map +1 -0
- package/dist/agent/coalesce.js +137 -0
- package/dist/agent/coalesce.test.d.ts +2 -0
- package/dist/agent/coalesce.test.d.ts.map +1 -0
- package/dist/agent/foreign.d.ts +97 -0
- package/dist/agent/foreign.d.ts.map +1 -0
- package/dist/agent/foreign.js +18 -0
- package/dist/agent/foreign.test.d.ts +2 -0
- package/dist/agent/foreign.test.d.ts.map +1 -0
- package/dist/agent/history.d.ts +2 -0
- package/dist/agent/history.d.ts.map +1 -1
- package/dist/agent/id.d.ts +65 -0
- package/dist/agent/id.d.ts.map +1 -0
- package/dist/agent/id.js +30 -0
- package/dist/agent/id.test.d.ts +2 -0
- package/dist/agent/id.test.d.ts.map +1 -0
- package/dist/agent/index.d.ts +11 -4
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +18 -3
- package/dist/agent/merge.d.ts +8 -0
- package/dist/agent/merge.d.ts.map +1 -0
- package/dist/agent/merge.js +35 -0
- package/dist/agent/merge.test.d.ts +2 -0
- package/dist/agent/merge.test.d.ts.map +1 -0
- package/dist/agent/part-list.d.ts +49 -34
- package/dist/agent/part-list.d.ts.map +1 -1
- package/dist/agent/part-list.js +53 -7
- package/dist/agent/part-list.test.d.ts +2 -0
- package/dist/agent/part-list.test.d.ts.map +1 -0
- package/dist/agent/parts.d.ts +135 -20
- package/dist/agent/parts.d.ts.map +1 -1
- package/dist/agent/parts.js +138 -11
- package/dist/agent/parts.test.d.ts +2 -0
- package/dist/agent/parts.test.d.ts.map +1 -0
- package/dist/agent/thread.d.ts +14 -0
- package/dist/agent/thread.d.ts.map +1 -1
- package/dist/agent/thread.js +8 -2
- package/dist/agent/thread.test.d.ts +2 -0
- package/dist/agent/thread.test.d.ts.map +1 -0
- package/dist/agent/transport.d.ts +39 -3
- package/dist/agent/transport.d.ts.map +1 -1
- package/dist/agent/transport.js +20 -17
- package/dist/agent/transport.test.d.ts +2 -0
- package/dist/agent/transport.test.d.ts.map +1 -0
- package/dist/agent/use-agent-stream.d.ts +2 -1
- package/dist/agent/use-agent-stream.d.ts.map +1 -1
- package/dist/agent/use-agent-stream.js +5 -2
- package/dist/agent/use-agent-stream.test.d.ts +2 -0
- package/dist/agent/use-agent-stream.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.d.ts +9 -3
- package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.foreign.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.foreign.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.js +75 -23
- package/dist/agent/use-agent-thread-runs.resume.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.resume.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.test.d.ts.map +1 -0
- package/dist/agent/use-agent-thread-runs.wedge.test.d.ts +2 -0
- package/dist/agent/use-agent-thread-runs.wedge.test.d.ts.map +1 -0
- package/dist/agent-chat/composer.d.ts +130 -8
- package/dist/agent-chat/composer.d.ts.map +1 -1
- package/dist/agent-chat/composer.js +189 -23
- package/dist/agent-chat/composer.test.d.ts +2 -0
- package/dist/agent-chat/composer.test.d.ts.map +1 -0
- package/dist/agent-chat/index.d.ts +3 -1
- package/dist/agent-chat/index.d.ts.map +1 -1
- package/dist/agent-chat/index.js +2 -0
- package/dist/agent-chat/thread-detail-panel.d.ts.map +1 -1
- package/dist/agent-chat/thread-detail-panel.js +1 -1
- package/dist/agent-chat/thread-message.d.ts +24 -7
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +115 -45
- package/dist/agent-chat/thread-message.test.d.ts +2 -0
- package/dist/agent-chat/thread-message.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-workspace.js +1 -1
- package/dist/agent-chat/tool-chip.d.ts +21 -0
- package/dist/agent-chat/tool-chip.d.ts.map +1 -0
- package/dist/agent-chat/tool-chip.js +120 -0
- package/dist/agent-chat/tool-chip.test.d.ts +2 -0
- package/dist/agent-chat/tool-chip.test.d.ts.map +1 -0
- package/dist/charts/primitives/ChartFrame.test.d.ts +2 -0
- package/dist/charts/primitives/ChartFrame.test.d.ts.map +1 -0
- package/dist/charts/utils/domain.test.d.ts +2 -0
- package/dist/charts/utils/domain.test.d.ts.map +1 -0
- package/dist/cli/check-theme.test.d.ts +2 -0
- package/dist/cli/check-theme.test.d.ts.map +1 -0
- package/dist/cli/help.test.d.ts +2 -0
- package/dist/cli/help.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/init-sync.test.d.ts +2 -0
- package/dist/cli/init-sync.test.d.ts.map +1 -0
- package/dist/cli/placement-engine.test.d.ts +2 -0
- package/dist/cli/placement-engine.test.d.ts.map +1 -0
- package/dist/cli/tarball-parity.test.d.ts +2 -0
- package/dist/cli/tarball-parity.test.d.ts.map +1 -0
- package/dist/cli/template-format.test.d.ts +2 -0
- package/dist/cli/template-format.test.d.ts.map +1 -0
- package/dist/cli/template-ledger.test.d.ts +2 -0
- package/dist/cli/template-ledger.test.d.ts.map +1 -0
- package/dist/cli/tokens-css.test.d.ts +2 -0
- package/dist/cli/tokens-css.test.d.ts.map +1 -0
- package/dist/content/article-actions.test.d.ts +2 -0
- package/dist/content/article-actions.test.d.ts.map +1 -0
- package/dist/content/article-model.test.d.ts +2 -0
- package/dist/content/article-model.test.d.ts.map +1 -0
- package/dist/content/block-split.test.d.ts +2 -0
- package/dist/content/block-split.test.d.ts.map +1 -0
- package/dist/content/fence-block.d.ts +37 -10
- package/dist/content/fence-block.d.ts.map +1 -1
- package/dist/content/fence-block.js +80 -14
- package/dist/content/heading-components.d.ts.map +1 -1
- package/dist/content/heading-components.js +7 -4
- package/dist/content/heading-components.test.d.ts +2 -0
- package/dist/content/heading-components.test.d.ts.map +1 -0
- package/dist/content/index.d.ts +6 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +5 -1
- package/dist/content/markdown.d.ts +101 -14
- package/dist/content/markdown.d.ts.map +1 -1
- package/dist/content/markdown.js +141 -28
- package/dist/content/markdown.test.d.ts +2 -0
- package/dist/content/markdown.test.d.ts.map +1 -0
- package/dist/content/prose.module.css.test.d.ts +2 -0
- package/dist/content/prose.module.css.test.d.ts.map +1 -0
- package/dist/content/sanitize.d.ts +170 -0
- package/dist/content/sanitize.d.ts.map +1 -0
- package/dist/content/sanitize.js +178 -0
- package/dist/content/sanitize.test.d.ts +2 -0
- package/dist/content/sanitize.test.d.ts.map +1 -0
- package/dist/content/slug.test.d.ts +2 -0
- package/dist/content/slug.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.test.d.ts +2 -0
- package/dist/dashboard/stat-card.test.d.ts.map +1 -0
- package/dist/guard/check-source.test.d.ts +2 -0
- package/dist/guard/check-source.test.d.ts.map +1 -0
- package/dist/guard/guard-hook.test.d.ts +2 -0
- package/dist/guard/guard-hook.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +1 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/provider/index.test.d.ts +2 -0
- package/dist/provider/index.test.d.ts.map +1 -0
- package/dist/provider/lab-theme.test.d.ts +2 -0
- package/dist/provider/lab-theme.test.d.ts.map +1 -0
- package/dist/router-tanstack/multi-search-param-store.test.d.ts +2 -0
- package/dist/router-tanstack/multi-search-param-store.test.d.ts.map +1 -0
- package/dist/styles.floor.test.d.ts +2 -0
- package/dist/styles.floor.test.d.ts.map +1 -0
- package/dist/surfaces.d.ts +5 -5
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +13 -4
- package/dist/theme/border-coverage.test.d.ts +18 -0
- package/dist/theme/border-coverage.test.d.ts.map +1 -0
- package/dist/theme/contrast.test.d.ts +2 -0
- package/dist/theme/contrast.test.d.ts.map +1 -0
- package/dist/theme/density-option.test.d.ts +2 -0
- package/dist/theme/density-option.test.d.ts.map +1 -0
- package/dist/theme/density-relations.test.d.ts +2 -0
- package/dist/theme/density-relations.test.d.ts.map +1 -0
- package/dist/theme/derive-theme.test.d.ts +2 -0
- package/dist/theme/derive-theme.test.d.ts.map +1 -0
- package/dist/theme/fonts-option.test.d.ts +2 -0
- package/dist/theme/fonts-option.test.d.ts.map +1 -0
- package/dist/theme/radius-option.test.d.ts +2 -0
- package/dist/theme/radius-option.test.d.ts.map +1 -0
- package/dist/theme/radius.test.d.ts +2 -0
- package/dist/theme/radius.test.d.ts.map +1 -0
- package/dist/theme/shadow-surfaces.test.d.ts +26 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -0
- package/dist/theme/spacing.test.d.ts +2 -0
- package/dist/theme/spacing.test.d.ts.map +1 -0
- package/dist/theme/use-basalt-spacing.test.d.ts +2 -0
- package/dist/theme/use-basalt-spacing.test.d.ts.map +1 -0
- package/dist/theme-lab/derive-controls.test.d.ts +2 -0
- package/dist/theme-lab/derive-controls.test.d.ts.map +1 -0
- package/dist/theme-lab/derive-state.test.d.ts +2 -0
- package/dist/theme-lab/derive-state.test.d.ts.map +1 -0
- package/dist/theme-lab/theme-lab.test.d.ts +2 -0
- package/dist/theme-lab/theme-lab.test.d.ts.map +1 -0
- package/dist/tokens/build-fonts-css.test.d.ts +2 -0
- package/dist/tokens/build-fonts-css.test.d.ts.map +1 -0
- package/dist/tokens/build-radius-css.test.d.ts +2 -0
- package/dist/tokens/build-radius-css.test.d.ts.map +1 -0
- package/dist/tokens/density.test.d.ts +2 -0
- package/dist/tokens/density.test.d.ts.map +1 -0
- package/dist/tokens/derive-radius.test.d.ts +2 -0
- package/dist/tokens/derive-radius.test.d.ts.map +1 -0
- package/dist/tokens/derive.test.d.ts +2 -0
- package/dist/tokens/derive.test.d.ts.map +1 -0
- package/dist/tokens/palette.test.d.ts +2 -0
- package/dist/tokens/palette.test.d.ts.map +1 -0
- package/dist/vite-color-mix.test.d.ts +2 -0
- package/dist/vite-color-mix.test.d.ts.map +1 -0
- package/dist/vite.test.d.ts +2 -0
- package/dist/vite.test.d.ts.map +1 -0
- package/llms.txt +7 -7
- package/package.json +6 -1
- package/src/agent/adapter.test.ts +1403 -0
- package/src/agent/adapter.ts +887 -0
- package/src/agent/ai-sdk-transport.test.ts +380 -55
- package/src/agent/ai-sdk-transport.ts +206 -53
- package/src/agent/coalesce.test.ts +387 -0
- package/src/agent/coalesce.ts +229 -0
- package/src/agent/foreign.test.ts +51 -0
- package/src/agent/foreign.ts +140 -0
- package/src/agent/history.ts +2 -0
- package/src/agent/id.test.ts +75 -0
- package/src/agent/id.ts +91 -0
- package/src/agent/index.ts +34 -3
- package/src/agent/merge.test.ts +206 -0
- package/src/agent/merge.ts +114 -0
- package/src/agent/part-list.test.tsx +112 -0
- package/src/agent/part-list.tsx +108 -35
- package/src/agent/parts.test.ts +516 -57
- package/src/agent/parts.ts +323 -32
- package/src/agent/thread.test.ts +75 -2
- package/src/agent/thread.ts +21 -1
- package/src/agent/transport.test.ts +70 -0
- package/src/agent/transport.ts +72 -20
- package/src/agent/use-agent-stream.test.tsx +53 -19
- package/src/agent/use-agent-stream.ts +13 -3
- package/src/agent/use-agent-thread-runs.foreign.test.tsx +138 -0
- package/src/agent/use-agent-thread-runs.resume.test.tsx +44 -11
- package/src/agent/use-agent-thread-runs.test.tsx +418 -22
- package/src/agent/use-agent-thread-runs.ts +170 -26
- package/src/agent/use-agent-thread-runs.wedge.test.tsx +15 -7
- package/src/agent-chat/composer.test.tsx +924 -0
- package/src/agent-chat/composer.tsx +521 -52
- package/src/agent-chat/index.ts +5 -1
- package/src/agent-chat/thread-detail-panel.tsx +3 -1
- package/src/agent-chat/thread-message.test.tsx +351 -0
- package/src/agent-chat/thread-message.tsx +262 -64
- package/src/agent-chat/thread-workspace.tsx +1 -1
- package/src/agent-chat/tool-chip.test.tsx +139 -0
- package/src/agent-chat/tool-chip.tsx +219 -0
- package/src/cli/index.ts +0 -1
- package/src/cli/template-format.test.ts +2 -2
- package/src/content/fence-block.tsx +242 -15
- package/src/content/heading-components.test.tsx +65 -0
- package/src/content/heading-components.tsx +26 -5
- package/src/content/index.ts +19 -3
- package/src/content/markdown.test.tsx +603 -0
- package/src/content/markdown.tsx +355 -35
- package/src/content/sanitize.test.ts +659 -0
- package/src/content/sanitize.ts +510 -0
- package/src/dashboard/stat-card.test.tsx +6 -1
- package/src/guard/check-source.test.ts +8 -4
- package/src/guard/index.ts +1 -3
- package/src/index.ts +15 -1
- package/src/surfaces.ts +13 -4
- package/src/theme/contrast.test.ts +1 -4
- package/src/theme/density-relations.test.ts +4 -2
- package/src/theme/fonts-option.test.ts +7 -1
- package/src/theme/shadow-surfaces.test.ts +12 -6
- package/src/theme/use-basalt-spacing.test.tsx +2 -1
|
@@ -47,13 +47,22 @@
|
|
|
47
47
|
* resolveOutcome: heuristicOutcome,
|
|
48
48
|
* })
|
|
49
49
|
*/
|
|
50
|
-
import
|
|
51
|
-
import
|
|
50
|
+
import { assertNever } from '../register'
|
|
51
|
+
import { TERMINAL_TOOL_STATES } from './parts'
|
|
52
|
+
import type { AgentPart, AgentPartDraft } from './parts'
|
|
53
|
+
import type { ResumableAgentTransport } from './transport'
|
|
52
54
|
|
|
53
55
|
// ── ai (optional peer) — TYPE-ONLY imports, erased at runtime ────────────────
|
|
54
56
|
// verbatimModuleSyntax-safe: these never trigger a runtime resolution of 'ai'. The actual runtime
|
|
55
57
|
// access goes through the memoized `import('ai')` inside resolveAiSdk() below.
|
|
56
|
-
import type {
|
|
58
|
+
import type {
|
|
59
|
+
DefaultChatTransport,
|
|
60
|
+
DynamicToolUIPart,
|
|
61
|
+
ToolUIPart,
|
|
62
|
+
UIMessage,
|
|
63
|
+
UIMessageChunk,
|
|
64
|
+
readUIMessageStream,
|
|
65
|
+
} from 'ai'
|
|
57
66
|
|
|
58
67
|
/** The element type of an AI SDK UIMessage's `parts` array (defaults applied). */
|
|
59
68
|
type AiPart = UIMessage['parts'][number]
|
|
@@ -76,13 +85,13 @@ export type AiSdkTransportOptions = {
|
|
|
76
85
|
|
|
77
86
|
// ── AiSdkTransport ─────────────────────────────────────────────────────────────
|
|
78
87
|
|
|
79
|
-
export type AiSdkTransport<TPart = AgentPart> =
|
|
88
|
+
export type AiSdkTransport<TPart = AgentPart> = ResumableAgentTransport<TPart, string> & {
|
|
80
89
|
/**
|
|
81
90
|
* Returns an AgentTransport bound to a stable chat id (e.g. a basalt-ui thread id) instead of
|
|
82
91
|
* this transport's own fixed one. Use this for useAgentThreadRuns's per-thread transport-factory
|
|
83
92
|
* form so each thread's turns share one continuous AI SDK chat session.
|
|
84
93
|
*/
|
|
85
|
-
readonly forThread: (chatId: string) =>
|
|
94
|
+
readonly forThread: (chatId: string) => ResumableAgentTransport<TPart, string>
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
// ── Lazy, memoized 'ai' resolution ────────────────────────────────────────────
|
|
@@ -120,80 +129,209 @@ function createAiSdkResolver(options: AiSdkTransportOptions): () => Promise<Reso
|
|
|
120
129
|
|
|
121
130
|
/**
|
|
122
131
|
* Diffs one AI SDK part (by array index) against its previous snapshot at that same index,
|
|
123
|
-
* returning zero or more new AgentPart
|
|
132
|
+
* returning zero or more new AgentPart drafts, each carrying a deterministic id.
|
|
124
133
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
134
|
+
* Index-addressed parts (text, reasoning, source) get `${chatId}#${index}` — stable across
|
|
135
|
+
* replays of the same snapshot sequence, which is what lets `mergePart` rewrite instead of
|
|
136
|
+
* duplicate on a resumed/replayed stream. text/reasoning also carry an authoritative `offset`
|
|
137
|
+
* (the length of the previously-seen text for that part) so a full-length resend at offset 0
|
|
138
|
+
* splices over the existing content instead of appending — the literal mechanism
|
|
139
|
+
* `idempotentReplay: true` asserts.
|
|
140
|
+
*
|
|
141
|
+
* source-document, file, reasoning-file, data-*, step-start, and custom parts have no AgentPart
|
|
142
|
+
* equivalent yet — deliberate v1 gap, skipped rather than inventing new variants.
|
|
127
143
|
*/
|
|
128
|
-
function diffPart(
|
|
144
|
+
function diffPart(
|
|
145
|
+
chatId: string,
|
|
146
|
+
index: number,
|
|
147
|
+
prevPart: AiPart | undefined,
|
|
148
|
+
currPart: AiPart,
|
|
149
|
+
toolStartTimes: Map<string, number>,
|
|
150
|
+
): AgentPartDraft[] {
|
|
129
151
|
switch (currPart.type) {
|
|
130
152
|
case 'text': {
|
|
131
153
|
const prevText = prevPart?.type === 'text' ? prevPart.text : ''
|
|
132
154
|
const delta = currPart.text.slice(prevText.length)
|
|
133
|
-
return delta.length === 0
|
|
155
|
+
return delta.length === 0
|
|
156
|
+
? []
|
|
157
|
+
: [{ id: `${chatId}#${index}`, type: 'text', text: delta, offset: prevText.length }]
|
|
134
158
|
}
|
|
135
159
|
case 'reasoning': {
|
|
136
160
|
const prevText = prevPart?.type === 'reasoning' ? prevPart.text : ''
|
|
137
161
|
const delta = currPart.text.slice(prevText.length)
|
|
138
|
-
return delta.length === 0
|
|
162
|
+
return delta.length === 0
|
|
163
|
+
? []
|
|
164
|
+
: [{ id: `${chatId}#${index}`, type: 'reasoning', text: delta, offset: prevText.length }]
|
|
139
165
|
}
|
|
140
166
|
case 'source-url': {
|
|
141
167
|
// Emit once, the first time this part appears at this index.
|
|
142
168
|
if (prevPart !== undefined) return []
|
|
143
|
-
const source:
|
|
169
|
+
const source: AgentPartDraft = { id: `${chatId}#${index}`, type: 'source', url: currPart.url }
|
|
144
170
|
return [currPart.title !== undefined ? { ...source, title: currPart.title } : source]
|
|
145
171
|
}
|
|
146
172
|
default:
|
|
147
|
-
return currPart
|
|
173
|
+
return isToolLikePart(currPart) ? diffToolPart(prevPart, currPart, toolStartTimes) : []
|
|
148
174
|
}
|
|
149
175
|
}
|
|
150
176
|
|
|
177
|
+
/** True for a `tool-${name}` static part OR a `dynamic-tool` part — the two AI SDK shapes basalt
|
|
178
|
+
* models as one `ToolCallPart`. `'dynamic-tool'` does NOT start with `'tool-'`, so this must be
|
|
179
|
+
* its own check, not a `.startsWith('tool-')` alone (that silently drops every dynamic tool call). */
|
|
180
|
+
function isToolLikePart(part: AiPart): part is ToolUIPart | DynamicToolUIPart {
|
|
181
|
+
return part.type === 'dynamic-tool' || part.type.startsWith('tool-')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Derives the tool's display name — mirrors AI SDK's own `getToolName`/`getStaticToolName`
|
|
185
|
+
* runtime (reimplemented locally rather than value-imported, so this module never resolves 'ai'
|
|
186
|
+
* at runtime; see the module docblock). Static tools carry their name only in the `tool-${name}`
|
|
187
|
+
* discriminator; dynamic tools carry it in `toolName`. */
|
|
188
|
+
function deriveToolName(part: ToolUIPart | DynamicToolUIPart): string {
|
|
189
|
+
return part.type === 'dynamic-tool' ? part.toolName : part.type.split('-').slice(1).join('-')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The `{ output, preliminary }` snapshot of an `output-available` tool part, or undefined for
|
|
193
|
+
* any other state. */
|
|
194
|
+
function outputSnapshot(
|
|
195
|
+
part: ToolUIPart | DynamicToolUIPart,
|
|
196
|
+
): { readonly output: unknown; readonly preliminary: boolean | undefined } | undefined {
|
|
197
|
+
return part.state === 'output-available'
|
|
198
|
+
? { output: part.output, preliminary: part.preliminary }
|
|
199
|
+
: undefined
|
|
200
|
+
}
|
|
201
|
+
|
|
151
202
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
203
|
+
* True when `curr` reports nothing new relative to `prev` — same state AND (for output-available)
|
|
204
|
+
* the same output/preliminary content. A `preliminary: true` result streams several
|
|
205
|
+
* output-available snapshots with the SAME `state` but a MUTATING `output`; naive state-equality
|
|
206
|
+
* would freeze on the first snapshot and drop every refinement. Content (not reference) equality:
|
|
207
|
+
* each snapshot from `readUIMessageStream` is a fresh `structuredClone`, so `===` on nested output
|
|
208
|
+
* objects would never match even when genuinely unchanged.
|
|
155
209
|
*/
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
210
|
+
function isSameToolState(
|
|
211
|
+
prev: ToolUIPart | DynamicToolUIPart,
|
|
212
|
+
curr: ToolUIPart | DynamicToolUIPart,
|
|
213
|
+
): boolean {
|
|
214
|
+
if (prev.state !== curr.state) return false
|
|
215
|
+
const prevOutput = outputSnapshot(prev)
|
|
216
|
+
const currOutput = outputSnapshot(curr)
|
|
217
|
+
if (prevOutput === undefined || currOutput === undefined) return true
|
|
218
|
+
return (
|
|
219
|
+
JSON.stringify(prevOutput.output) === JSON.stringify(currOutput.output) &&
|
|
220
|
+
prevOutput.preliminary === currOutput.preliminary
|
|
221
|
+
)
|
|
163
222
|
}
|
|
164
223
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Diffs one tool part across the seven-state `UIToolInvocation` union into a `ToolCallPart` draft,
|
|
226
|
+
* addressed by `tool#${toolCallId}` — stable across every state transition of one call, so
|
|
227
|
+
* `mergePart` rewrites the same part in place rather than stacking one entry per state.
|
|
228
|
+
*
|
|
229
|
+
* `durationMs` is basalt's own field (the SDK doesn't provide one): wall-clock from this
|
|
230
|
+
* toolCallId's first sighting (recorded in `toolStartTimes`, scoped to ONE stream() / resume()
|
|
231
|
+
* call — never module-global, so two concurrent threads never share timings) to a terminal state.
|
|
232
|
+
*/
|
|
233
|
+
function diffToolPart(
|
|
234
|
+
prevPart: AiPart | undefined,
|
|
235
|
+
currPart: ToolUIPart | DynamicToolUIPart,
|
|
236
|
+
toolStartTimes: Map<string, number>,
|
|
237
|
+
): AgentPartDraft[] {
|
|
238
|
+
if (!toolStartTimes.has(currPart.toolCallId)) {
|
|
239
|
+
toolStartTimes.set(currPart.toolCallId, Date.now())
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const prev = prevPart !== undefined && isToolLikePart(prevPart) ? prevPart : undefined
|
|
243
|
+
if (prev !== undefined && isSameToolState(prev, currPart)) return []
|
|
170
244
|
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// Already emitted this exact state for this tool call index — nothing new to report.
|
|
176
|
-
if (prev?.state === curr.state) return []
|
|
245
|
+
const settled = (TERMINAL_TOOL_STATES as readonly string[]).includes(currPart.state)
|
|
246
|
+
const durationMs = settled
|
|
247
|
+
? Date.now() - (toolStartTimes.get(currPart.toolCallId) ?? Date.now())
|
|
248
|
+
: undefined
|
|
177
249
|
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
type: 'tool',
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
250
|
+
const base = {
|
|
251
|
+
id: `tool#${currPart.toolCallId}`,
|
|
252
|
+
type: 'tool' as const,
|
|
253
|
+
toolCallId: currPart.toolCallId,
|
|
254
|
+
toolName: deriveToolName(currPart),
|
|
255
|
+
...(currPart.providerExecuted !== undefined
|
|
256
|
+
? { providerExecuted: currPart.providerExecuted }
|
|
257
|
+
: {}),
|
|
258
|
+
...(durationMs !== undefined ? { durationMs } : {}),
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
switch (currPart.state) {
|
|
262
|
+
case 'input-streaming':
|
|
263
|
+
return [
|
|
264
|
+
{
|
|
265
|
+
...base,
|
|
266
|
+
state: 'input-streaming',
|
|
267
|
+
...(currPart.input !== undefined ? { input: currPart.input } : {}),
|
|
268
|
+
},
|
|
269
|
+
]
|
|
270
|
+
case 'input-available':
|
|
271
|
+
return [{ ...base, state: 'input-available', input: currPart.input }]
|
|
272
|
+
case 'approval-requested':
|
|
273
|
+
return [
|
|
274
|
+
{
|
|
275
|
+
...base,
|
|
276
|
+
state: 'approval-requested',
|
|
277
|
+
input: currPart.input,
|
|
278
|
+
approval: currPart.approval,
|
|
279
|
+
},
|
|
280
|
+
]
|
|
281
|
+
case 'approval-responded':
|
|
282
|
+
return [
|
|
283
|
+
{
|
|
284
|
+
...base,
|
|
285
|
+
state: 'approval-responded',
|
|
286
|
+
input: currPart.input,
|
|
287
|
+
approval: currPart.approval,
|
|
288
|
+
},
|
|
289
|
+
]
|
|
290
|
+
case 'output-available':
|
|
291
|
+
return [
|
|
292
|
+
{
|
|
293
|
+
...base,
|
|
294
|
+
state: 'output-available',
|
|
295
|
+
input: currPart.input,
|
|
296
|
+
output: currPart.output,
|
|
297
|
+
...(currPart.preliminary !== undefined ? { preliminary: currPart.preliminary } : {}),
|
|
298
|
+
...(currPart.approval !== undefined ? { approval: currPart.approval } : {}),
|
|
299
|
+
},
|
|
300
|
+
]
|
|
301
|
+
case 'output-error': {
|
|
302
|
+
// rawInput only exists on the static ToolUIPart variant — a DynamicToolUIPart's output-error
|
|
303
|
+
// has no such field at all, so this must be read defensively behind the type discriminant.
|
|
304
|
+
const rawInput = currPart.type === 'dynamic-tool' ? undefined : currPart.rawInput
|
|
305
|
+
return [
|
|
306
|
+
{
|
|
307
|
+
...base,
|
|
308
|
+
state: 'output-error',
|
|
309
|
+
...(currPart.input !== undefined ? { input: currPart.input } : {}),
|
|
310
|
+
errorText: currPart.errorText,
|
|
311
|
+
...(rawInput !== undefined ? { rawInput } : {}),
|
|
312
|
+
...(currPart.approval !== undefined ? { approval: currPart.approval } : {}),
|
|
313
|
+
},
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
case 'output-denied':
|
|
317
|
+
return [
|
|
318
|
+
{ ...base, state: 'output-denied', input: currPart.input, approval: currPart.approval },
|
|
319
|
+
]
|
|
320
|
+
default:
|
|
321
|
+
return assertNever(currPart)
|
|
184
322
|
}
|
|
185
|
-
if (curr.state === 'output-available') return [{ ...base, output: curr.output }]
|
|
186
|
-
// output-error: still surface it as a tool part rather than silently dropping the failure — the
|
|
187
|
-
// error text becomes the output payload since ToolCallPart has no dedicated error field.
|
|
188
|
-
if (curr.state === 'output-error') return [{ ...base, output: { error: curr.errorText } }]
|
|
189
|
-
return [base]
|
|
190
323
|
}
|
|
191
324
|
|
|
192
325
|
/** Diffs every index of one UIMessage snapshot against the previous snapshot. */
|
|
193
|
-
function diffMessage(
|
|
194
|
-
|
|
326
|
+
function diffMessage(
|
|
327
|
+
chatId: string,
|
|
328
|
+
prev: UIMessage | undefined,
|
|
329
|
+
curr: UIMessage,
|
|
330
|
+
toolStartTimes: Map<string, number>,
|
|
331
|
+
): AgentPartDraft[] {
|
|
332
|
+
const deltas: AgentPartDraft[] = []
|
|
195
333
|
curr.parts.forEach((part, i) => {
|
|
196
|
-
deltas.push(...diffPart(prev?.parts[i], part))
|
|
334
|
+
deltas.push(...diffPart(chatId, i, prev?.parts[i], part, toolStartTimes))
|
|
197
335
|
})
|
|
198
336
|
return deltas
|
|
199
337
|
}
|
|
@@ -204,19 +342,28 @@ function diffMessage(prev: UIMessage | undefined, curr: UIMessage): AgentPart[]
|
|
|
204
342
|
* built-in AgentPart variants, so TPart must be — or be assignable from — AgentPart for a
|
|
205
343
|
* meaningful result; same documented-cast convention as useAgentThreadRuns' defaultToUserParts).
|
|
206
344
|
*
|
|
345
|
+
* `chatId` is the id-namespace for this diff pass — the caller's chat id on stream(), the
|
|
346
|
+
* resumeToken (which IS the chat id, see the module docblock's CHAT ID BINDING note) on resume() —
|
|
347
|
+
* so index-addressed ids line up with the original run's ids across a reconnect.
|
|
348
|
+
*
|
|
349
|
+
* A fresh `toolStartTimes` map is scoped to ONE diffChunkStream call (one stream()/resume()
|
|
350
|
+
* invocation), never shared across calls or threads.
|
|
351
|
+
*
|
|
207
352
|
* `resume()`'s underlying reconnectToStream call has no AbortSignal parameter in the current AI
|
|
208
353
|
* SDK API (unlike sendMessages), so we can't cancel the in-flight fetch directly — checking
|
|
209
354
|
* `signal.aborted` here at least stops yielding further deltas to an aborted caller promptly.
|
|
210
355
|
*/
|
|
211
356
|
async function* diffChunkStream<TPart>(
|
|
357
|
+
chatId: string,
|
|
212
358
|
chunkStream: ReadableStream<UIMessageChunk>,
|
|
213
359
|
readStream: typeof readUIMessageStream,
|
|
214
360
|
signal?: AbortSignal,
|
|
215
361
|
): AsyncGenerator<TPart> {
|
|
216
362
|
let prev: UIMessage | undefined
|
|
363
|
+
const toolStartTimes = new Map<string, number>()
|
|
217
364
|
for await (const curr of readStream<UIMessage>({ stream: chunkStream })) {
|
|
218
365
|
if (signal?.aborted) return
|
|
219
|
-
for (const part of diffMessage(prev, curr)) {
|
|
366
|
+
for (const part of diffMessage(chatId, prev, curr, toolStartTimes)) {
|
|
220
367
|
yield part as TPart
|
|
221
368
|
}
|
|
222
369
|
prev = curr
|
|
@@ -241,12 +388,16 @@ export function aiSdkTransport<TPart = AgentPart>(
|
|
|
241
388
|
): AiSdkTransport<TPart> {
|
|
242
389
|
const resolveAiSdk = createAiSdkResolver(options)
|
|
243
390
|
|
|
244
|
-
function makeTransport(chatId: string):
|
|
391
|
+
function makeTransport(chatId: string): ResumableAgentTransport<TPart, string> {
|
|
245
392
|
return {
|
|
393
|
+
// Every text/reasoning delta this transport emits carries an authoritative `offset` (see
|
|
394
|
+
// diffPart), so a replayed run rewrites in place instead of duplicating — the literal
|
|
395
|
+
// condition this assertion stands for.
|
|
396
|
+
idempotentReplay: true,
|
|
246
397
|
async *stream(input: string, signal?: AbortSignal): AsyncGenerator<TPart> {
|
|
247
398
|
// Synthesized locally, first — we already know the chat id client-side, so there is no
|
|
248
399
|
// need to wait on the server to hand us a run id (unlike a river-style protocol).
|
|
249
|
-
yield { type: 'start', runId: chatId, resumeToken: chatId } as TPart
|
|
400
|
+
yield { id: `${chatId}#start`, type: 'start', runId: chatId, resumeToken: chatId } as TPart
|
|
250
401
|
|
|
251
402
|
const { httpTransport, readUIMessageStream: readStream } = await resolveAiSdk()
|
|
252
403
|
const userMessage: UIMessage = {
|
|
@@ -261,13 +412,15 @@ export function aiSdkTransport<TPart = AgentPart>(
|
|
|
261
412
|
messageId: undefined,
|
|
262
413
|
abortSignal: signal,
|
|
263
414
|
})
|
|
264
|
-
yield* diffChunkStream<TPart>(chunkStream, readStream, signal)
|
|
415
|
+
yield* diffChunkStream<TPart>(chatId, chunkStream, readStream, signal)
|
|
265
416
|
},
|
|
266
417
|
async *resume(resumeToken: string, signal?: AbortSignal): AsyncGenerator<TPart> {
|
|
267
418
|
const { httpTransport, readUIMessageStream: readStream } = await resolveAiSdk()
|
|
268
419
|
const chunkStream = await httpTransport.reconnectToStream({ chatId: resumeToken })
|
|
269
420
|
if (chunkStream === null) return
|
|
270
|
-
|
|
421
|
+
// resumeToken IS the chat id (see CHAT ID BINDING above) — reusing it as the id-namespace
|
|
422
|
+
// here keeps replayed index-addressed ids identical to the ones the original run minted.
|
|
423
|
+
yield* diffChunkStream<TPart>(resumeToken, chunkStream, readStream, signal)
|
|
271
424
|
},
|
|
272
425
|
}
|
|
273
426
|
}
|