@tanstack/ai-client 0.22.0 → 0.23.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/README.md +15 -1
- package/dist/esm/audio-recorder.js +190 -213
- package/dist/esm/audio-recorder.js.map +1 -1
- package/dist/esm/chat-client.d.ts +172 -3
- package/dist/esm/chat-client.js +1656 -1386
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/cleared-stream-tracker.d.ts +23 -0
- package/dist/esm/cleared-stream-tracker.js +97 -0
- package/dist/esm/cleared-stream-tracker.js.map +1 -0
- package/dist/esm/client-persistor.d.ts +25 -12
- package/dist/esm/client-persistor.js +260 -235
- package/dist/esm/client-persistor.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +231 -10
- package/dist/esm/connection-adapters.js +989 -574
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/devtools-noop.d.ts +1 -0
- package/dist/esm/devtools-noop.js +79 -139
- package/dist/esm/devtools-noop.js.map +1 -1
- package/dist/esm/devtools.d.ts +31 -1
- package/dist/esm/devtools.js +977 -1127
- package/dist/esm/devtools.js.map +1 -1
- package/dist/esm/events.js +224 -226
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/generation-client.d.ts +145 -2
- package/dist/esm/generation-client.js +659 -321
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-reconstruct.d.ts +21 -0
- package/dist/esm/generation-reconstruct.js +85 -0
- package/dist/esm/generation-reconstruct.js.map +1 -0
- package/dist/esm/generation-types.d.ts +289 -3
- package/dist/esm/generation-types.js +356 -13
- package/dist/esm/generation-types.js.map +1 -1
- package/dist/esm/index.d.ts +9 -4
- package/dist/esm/index.js +7 -39
- package/dist/esm/interrupt-manager.d.ts +77 -0
- package/dist/esm/interrupt-manager.js +787 -0
- package/dist/esm/interrupt-manager.js.map +1 -0
- package/dist/esm/mcp-app-bridge.js +56 -64
- package/dist/esm/mcp-app-bridge.js.map +1 -1
- package/dist/esm/realtime-client.js +366 -440
- package/dist/esm/realtime-client.js.map +1 -1
- package/dist/esm/response-stream.js +19 -26
- package/dist/esm/response-stream.js.map +1 -1
- package/dist/esm/sse-parser.js +44 -47
- package/dist/esm/sse-parser.js.map +1 -1
- package/dist/esm/sse-utils.js +8 -9
- package/dist/esm/sse-utils.js.map +1 -1
- package/dist/esm/storage-adapters.d.ts +62 -0
- package/dist/esm/storage-adapters.js +174 -0
- package/dist/esm/storage-adapters.js.map +1 -0
- package/dist/esm/types.d.ts +212 -10
- package/dist/esm/types.js +38 -7
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/video-generation-client.d.ts +113 -2
- package/dist/esm/video-generation-client.js +665 -379
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +7 -7
- package/src/chat-client.ts +1079 -61
- package/src/cleared-stream-tracker.ts +151 -0
- package/src/client-persistor.ts +102 -33
- package/src/connection-adapters.ts +1185 -142
- package/src/devtools-noop.ts +4 -3
- package/src/devtools.ts +121 -3
- package/src/generation-client.ts +563 -13
- package/src/generation-reconstruct.ts +121 -0
- package/src/generation-types.ts +727 -3
- package/src/index.ts +56 -1
- package/src/interrupt-manager.ts +1440 -0
- package/src/storage-adapters.ts +242 -0
- package/src/types.ts +301 -9
- package/src/video-generation-client.ts +479 -13
- package/dist/esm/index.js.map +0 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { getChunkRunId } from './connection-adapters'
|
|
2
|
+
import type { StreamChunk } from '@tanstack/ai/client'
|
|
3
|
+
import type { UIMessage } from './types'
|
|
4
|
+
|
|
5
|
+
function getChunkToolCallId(chunk: StreamChunk): string | undefined {
|
|
6
|
+
return 'toolCallId' in chunk && typeof chunk.toolCallId === 'string'
|
|
7
|
+
? chunk.toolCallId
|
|
8
|
+
: undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getChunkMessageId(chunk: StreamChunk): string | undefined {
|
|
12
|
+
return 'messageId' in chunk && typeof chunk.messageId === 'string'
|
|
13
|
+
? chunk.messageId
|
|
14
|
+
: undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getChunkParentMessageId(chunk: StreamChunk): string | undefined {
|
|
18
|
+
return 'parentMessageId' in chunk && typeof chunk.parentMessageId === 'string'
|
|
19
|
+
? chunk.parentMessageId
|
|
20
|
+
: undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Tracks stream chunks that must be ignored after the owning chat is cleared. */
|
|
24
|
+
export class ClearedStreamTracker {
|
|
25
|
+
private readonly clearedMessageIds = new Set<string>()
|
|
26
|
+
private readonly clearedRunIds = new Set<string>()
|
|
27
|
+
private readonly ignoredActiveRunIds = new Set<string>()
|
|
28
|
+
private readonly clearedToolCallIds = new Set<string>()
|
|
29
|
+
private currentRunlessRunId: string | null = null
|
|
30
|
+
|
|
31
|
+
snapshotClear(context: {
|
|
32
|
+
messages: Array<UIMessage>
|
|
33
|
+
activeRunIds: Set<string>
|
|
34
|
+
currentRunId: string | null
|
|
35
|
+
}): void {
|
|
36
|
+
for (const message of context.messages) {
|
|
37
|
+
this.clearedMessageIds.add(message.id)
|
|
38
|
+
}
|
|
39
|
+
for (const runId of context.activeRunIds) {
|
|
40
|
+
this.clearedRunIds.add(runId)
|
|
41
|
+
this.ignoredActiveRunIds.add(runId)
|
|
42
|
+
}
|
|
43
|
+
if (context.currentRunId) {
|
|
44
|
+
this.clearedRunIds.add(context.currentRunId)
|
|
45
|
+
this.ignoredActiveRunIds.add(context.currentRunId)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
shouldIgnoreChunk(chunk: StreamChunk): boolean {
|
|
50
|
+
const runId = getChunkRunId(chunk)
|
|
51
|
+
if (runId && this.clearedRunIds.has(runId)) {
|
|
52
|
+
if (chunk.type === 'RUN_STARTED') {
|
|
53
|
+
this.ignoredActiveRunIds.add(runId)
|
|
54
|
+
this.currentRunlessRunId = runId
|
|
55
|
+
}
|
|
56
|
+
this.markIgnoredChunkIds(chunk)
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (runId && this.ignoredActiveRunIds.has(runId)) {
|
|
61
|
+
this.markIgnoredChunkIds(chunk)
|
|
62
|
+
return true
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (this.isRunlessChunkFromIgnoredRun(chunk)) {
|
|
66
|
+
this.markIgnoredChunkIds(chunk)
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const toolCallId = getChunkToolCallId(chunk)
|
|
71
|
+
if (toolCallId && this.clearedToolCallIds.has(toolCallId)) {
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const parentMessageId = getChunkParentMessageId(chunk)
|
|
76
|
+
if (parentMessageId && this.clearedMessageIds.has(parentMessageId)) {
|
|
77
|
+
if (toolCallId) {
|
|
78
|
+
this.clearedToolCallIds.add(toolCallId)
|
|
79
|
+
}
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const messageId = getChunkMessageId(chunk)
|
|
84
|
+
return Boolean(messageId && this.clearedMessageIds.has(messageId))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
onRunStarted(runId: string): void {
|
|
88
|
+
this.currentRunlessRunId = runId
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
onRunSettled(runId: string): void {
|
|
92
|
+
this.ignoredActiveRunIds.delete(runId)
|
|
93
|
+
this.clearedRunIds.delete(runId)
|
|
94
|
+
if (this.currentRunlessRunId === runId) {
|
|
95
|
+
this.currentRunlessRunId =
|
|
96
|
+
this.ignoredActiveRunIds.values().next().value ?? null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
onSessionRunError(): void {
|
|
101
|
+
this.ignoredActiveRunIds.clear()
|
|
102
|
+
this.currentRunlessRunId = null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
resetActiveRuns(): void {
|
|
106
|
+
this.ignoredActiveRunIds.clear()
|
|
107
|
+
this.currentRunlessRunId = null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
takeRunlessRunId(): string | null {
|
|
111
|
+
const runId = this.currentRunlessRunId
|
|
112
|
+
if (!runId) return null
|
|
113
|
+
this.ignoredActiveRunIds.delete(runId)
|
|
114
|
+
this.clearedRunIds.delete(runId)
|
|
115
|
+
this.currentRunlessRunId =
|
|
116
|
+
this.ignoredActiveRunIds.values().next().value ?? null
|
|
117
|
+
return runId
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private markIgnoredChunkIds(chunk: StreamChunk): void {
|
|
121
|
+
const messageId = getChunkMessageId(chunk)
|
|
122
|
+
if (messageId) {
|
|
123
|
+
this.clearedMessageIds.add(messageId)
|
|
124
|
+
}
|
|
125
|
+
const toolCallId = getChunkToolCallId(chunk)
|
|
126
|
+
if (toolCallId) {
|
|
127
|
+
this.clearedToolCallIds.add(toolCallId)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private isRunlessChunkFromIgnoredRun(chunk: StreamChunk): boolean {
|
|
132
|
+
const runId = getChunkRunId(chunk)
|
|
133
|
+
if (runId || !this.currentRunlessRunId) return false
|
|
134
|
+
if (
|
|
135
|
+
!this.ignoredActiveRunIds.has(this.currentRunlessRunId) &&
|
|
136
|
+
!this.clearedRunIds.has(this.currentRunlessRunId)
|
|
137
|
+
) {
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
return (
|
|
141
|
+
chunk.type === 'TEXT_MESSAGE_START' ||
|
|
142
|
+
chunk.type === 'TEXT_MESSAGE_CONTENT' ||
|
|
143
|
+
chunk.type === 'TOOL_CALL_START' ||
|
|
144
|
+
chunk.type === 'TOOL_CALL_ARGS' ||
|
|
145
|
+
chunk.type === 'TOOL_CALL_END' ||
|
|
146
|
+
chunk.type === 'TOOL_CALL_RESULT' ||
|
|
147
|
+
chunk.type === 'MESSAGES_SNAPSHOT' ||
|
|
148
|
+
chunk.type === 'RUN_ERROR'
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/client-persistor.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { getChunkRunId } from './connection-adapters'
|
|
2
2
|
import type { StreamChunk } from '@tanstack/ai/client'
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ChatClientPersistence,
|
|
5
|
+
ChatPersistedState,
|
|
6
|
+
ChatResumeSnapshot,
|
|
7
|
+
UIMessage,
|
|
8
|
+
} from './types'
|
|
9
|
+
|
|
10
|
+
/** Normalize a raw `getItem` result (legacy bare array or combined record). */
|
|
11
|
+
function normalizePersistedState(
|
|
12
|
+
raw: ChatPersistedState | Array<UIMessage> | null | undefined,
|
|
13
|
+
): ChatPersistedState | undefined {
|
|
14
|
+
if (Array.isArray(raw)) return { messages: raw }
|
|
15
|
+
if (raw && Array.isArray(raw.messages)) return raw
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
4
18
|
|
|
5
19
|
// `StreamChunk` is a discriminated union; `toolCallId` / `messageId` /
|
|
6
20
|
// `parentMessageId` exist on only some members. Narrow with `in` (matching
|
|
@@ -30,10 +44,12 @@ function getChunkParentMessageId(chunk: StreamChunk): string | undefined {
|
|
|
30
44
|
*
|
|
31
45
|
* Two responsibilities live here:
|
|
32
46
|
*
|
|
33
|
-
* 1. **Storage orchestration** — hydrate from `getItem(id)` on creation, save
|
|
34
|
-
* `
|
|
35
|
-
* `removeItem(id)` on clear
|
|
36
|
-
*
|
|
47
|
+
* 1. **Storage orchestration** — hydrate from `getItem(id)` on creation, save a
|
|
48
|
+
* combined `{ messages, resume? }` record via `setItem` on every change
|
|
49
|
+
* through an ordered write queue, and `removeItem(id)` on clear (or when
|
|
50
|
+
* both the transcript and resume pointer are empty). A generation counter
|
|
51
|
+
* discards stale writes when a removal or a newer conversation supersedes
|
|
52
|
+
* an in-flight async operation.
|
|
37
53
|
* 2. **Clear-during-stream suppression** — when a conversation is cleared while a
|
|
38
54
|
* stream is still producing, late chunks for the cleared run(s) must not
|
|
39
55
|
* repopulate the now-empty state. The persistor tracks the cleared ids and
|
|
@@ -51,6 +67,10 @@ export class ChatPersistor {
|
|
|
51
67
|
// Bumped on every message change; lets an in-flight async hydration detect
|
|
52
68
|
// that the message list moved on and avoid clobbering it.
|
|
53
69
|
private messagesGeneration = 0
|
|
70
|
+
// Latest messages + resume snapshot, written together as one combined record
|
|
71
|
+
// so a full page reload restores both from a single adapter key.
|
|
72
|
+
private lastMessages: Array<UIMessage> = []
|
|
73
|
+
private lastResume: ChatResumeSnapshot | null = null
|
|
54
74
|
|
|
55
75
|
// --- clear-during-stream suppression state ---
|
|
56
76
|
private readonly clearedMessageIds = new Set<string>()
|
|
@@ -63,51 +83,91 @@ export class ChatPersistor {
|
|
|
63
83
|
private readonly adapter: ChatClientPersistence,
|
|
64
84
|
private readonly id: string,
|
|
65
85
|
private readonly applyMessages: (messages: Array<UIMessage>) => void,
|
|
86
|
+
private readonly applyResume?: (snapshot: ChatResumeSnapshot) => void,
|
|
66
87
|
) {}
|
|
67
88
|
|
|
89
|
+
/** Persist the current state as one combined `{ messages, resume? }` record. */
|
|
90
|
+
private writeState(): void {
|
|
91
|
+
const messages = [...this.lastMessages]
|
|
92
|
+
// Nothing to persist (no transcript, no resume pointer): remove the key
|
|
93
|
+
// rather than writing an empty `{ messages: [] }`, so a cleared
|
|
94
|
+
// conversation does not leave a stale record behind.
|
|
95
|
+
if (messages.length === 0 && !this.lastResume) {
|
|
96
|
+
const generation = this.generation
|
|
97
|
+
this.runOperation(() => {
|
|
98
|
+
if (generation !== this.generation) {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
return this.adapter.removeItem(this.id)
|
|
102
|
+
})
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
const generation = this.generation
|
|
106
|
+
const state: ChatPersistedState = {
|
|
107
|
+
messages,
|
|
108
|
+
...(this.lastResume ? { resume: this.lastResume } : {}),
|
|
109
|
+
}
|
|
110
|
+
this.runOperation(() => {
|
|
111
|
+
if (generation !== this.generation) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
return this.adapter.setItem(this.id, state)
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
68
118
|
// ---------------------------------------------------------------------------
|
|
69
119
|
// Storage orchestration
|
|
70
120
|
// ---------------------------------------------------------------------------
|
|
71
121
|
|
|
72
122
|
/**
|
|
73
|
-
* Synchronously read the persisted
|
|
74
|
-
* Returns the
|
|
123
|
+
* Synchronously read the persisted state for constructor-time hydration.
|
|
124
|
+
* Returns the normalized combined record, or a promise of it for async stores.
|
|
75
125
|
*/
|
|
76
126
|
readInitial():
|
|
77
|
-
|
|
|
78
|
-
| null
|
|
127
|
+
| ChatPersistedState
|
|
79
128
|
| undefined
|
|
80
|
-
| Promise<
|
|
129
|
+
| Promise<ChatPersistedState | undefined> {
|
|
81
130
|
try {
|
|
82
|
-
|
|
131
|
+
const raw = this.adapter.getItem(this.id)
|
|
132
|
+
if (raw instanceof Promise) {
|
|
133
|
+
return raw.then(normalizePersistedState).catch(() => undefined)
|
|
134
|
+
}
|
|
135
|
+
const state = normalizePersistedState(raw)
|
|
136
|
+
if (state) {
|
|
137
|
+
this.lastMessages = state.messages
|
|
138
|
+
this.lastResume = state.resume ?? null
|
|
139
|
+
}
|
|
140
|
+
return state
|
|
83
141
|
} catch {
|
|
84
142
|
return undefined
|
|
85
143
|
}
|
|
86
144
|
}
|
|
87
145
|
|
|
88
146
|
/**
|
|
89
|
-
* Apply
|
|
147
|
+
* Apply state from an async `getItem` once it resolves, unless the message
|
|
90
148
|
* list has already changed since hydration began.
|
|
91
149
|
*/
|
|
92
150
|
hydrateAsync(
|
|
93
|
-
|
|
94
|
-
|
|
|
95
|
-
| null
|
|
151
|
+
persistedState:
|
|
152
|
+
| ChatPersistedState
|
|
96
153
|
| undefined
|
|
97
|
-
| Promise<
|
|
154
|
+
| Promise<ChatPersistedState | undefined>,
|
|
98
155
|
): void {
|
|
99
|
-
if (!(
|
|
156
|
+
if (!(persistedState instanceof Promise)) {
|
|
100
157
|
return
|
|
101
158
|
}
|
|
102
159
|
|
|
103
160
|
const hydrationGeneration = this.messagesGeneration
|
|
104
|
-
|
|
105
|
-
.then((
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
161
|
+
persistedState
|
|
162
|
+
.then((state) => {
|
|
163
|
+
if (!state || this.messagesGeneration !== hydrationGeneration) {
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
this.lastResume = state.resume ?? null
|
|
167
|
+
this.lastMessages = state.messages
|
|
168
|
+
this.applyMessages(state.messages)
|
|
169
|
+
if (state.resume && this.applyResume) {
|
|
170
|
+
this.applyResume(state.resume)
|
|
111
171
|
}
|
|
112
172
|
})
|
|
113
173
|
.catch(() => {
|
|
@@ -116,28 +176,37 @@ export class ChatPersistor {
|
|
|
116
176
|
}
|
|
117
177
|
|
|
118
178
|
/**
|
|
119
|
-
* Record a message-list change and queue a
|
|
179
|
+
* Record a message-list change and queue a combined write for it. Skips a
|
|
120
180
|
* single write after {@link beginClear} so the clear's empty snapshot isn't
|
|
121
181
|
* persisted between `clearMessages()` and {@link remove}.
|
|
122
182
|
*/
|
|
123
183
|
notifyMessagesChanged(messages: Array<UIMessage>): void {
|
|
124
184
|
this.messagesGeneration++
|
|
185
|
+
this.lastMessages = [...messages]
|
|
125
186
|
if (this.skipNextPersist) {
|
|
126
187
|
this.skipNextPersist = false
|
|
127
188
|
return
|
|
128
189
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
190
|
+
this.writeState()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Record the current resume snapshot (which run to rejoin / which interrupts
|
|
195
|
+
* are pending) and persist it alongside the messages. Pass `null` to clear it
|
|
196
|
+
* once the run reaches a non-interrupt terminal.
|
|
197
|
+
*/
|
|
198
|
+
persistResumeSnapshot(snapshot: ChatResumeSnapshot | null): void {
|
|
199
|
+
this.lastResume = snapshot
|
|
200
|
+
if (this.skipNextPersist) {
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
this.writeState()
|
|
137
204
|
}
|
|
138
205
|
|
|
139
206
|
/** Remove the persisted conversation. Invalidates any queued writes. */
|
|
140
207
|
remove(): void {
|
|
208
|
+
this.lastMessages = []
|
|
209
|
+
this.lastResume = null
|
|
141
210
|
const generation = ++this.generation
|
|
142
211
|
this.runOperation(() => {
|
|
143
212
|
if (generation !== this.generation) {
|