@tanstack/ai-client 0.22.1 → 0.23.1
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,242 @@
|
|
|
1
|
+
import type { ChatPersistedState, ChatStorageAdapter } from './types'
|
|
2
|
+
|
|
3
|
+
export interface WebStoragePersistenceOptions {
|
|
4
|
+
keyPrefix?: string
|
|
5
|
+
/**
|
|
6
|
+
* Defaults to `JSON.stringify`. Override only for values JSON can't
|
|
7
|
+
* round-trip losslessly (a `Map`, a `bigint`, a `Date` you need back as a
|
|
8
|
+
* `Date` rather than an ISO string).
|
|
9
|
+
*/
|
|
10
|
+
serialize?: (value: ChatPersistedState) => string
|
|
11
|
+
/** Defaults to `JSON.parse`. */
|
|
12
|
+
deserialize?: (value: string) => ChatPersistedState
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface IndexedDBPersistenceOptions {
|
|
16
|
+
databaseName?: string
|
|
17
|
+
objectStoreName?: string
|
|
18
|
+
keyPrefix?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type StorageName = 'localStorage' | 'sessionStorage' | 'indexedDB'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Thrown by a storage adapter when its backing store is absent — most commonly
|
|
25
|
+
* during server-side rendering, where `localStorage` / `sessionStorage` /
|
|
26
|
+
* `indexedDB` do not exist on `globalThis`. The adapters check availability
|
|
27
|
+
* lazily, **per operation**, so constructing an adapter never throws; the error
|
|
28
|
+
* surfaces from `getItem` / `setItem` / `removeItem` (rejected promise for
|
|
29
|
+
* IndexedDB). The chat persistence layer treats adapter failures as best-effort
|
|
30
|
+
* (storage errors never break chat setup or streaming).
|
|
31
|
+
*/
|
|
32
|
+
export class StorageUnavailableError extends Error {
|
|
33
|
+
constructor(storageName: StorageName) {
|
|
34
|
+
super(`${storageName} is not available in this environment.`)
|
|
35
|
+
this.name = 'StorageUnavailableError'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function stringifyJson(value: ChatPersistedState): string {
|
|
40
|
+
const stringify: (input: unknown) => unknown = JSON.stringify
|
|
41
|
+
const serialized = stringify(value)
|
|
42
|
+
if (typeof serialized !== 'string') {
|
|
43
|
+
throw new TypeError('The value is not JSON serializable.')
|
|
44
|
+
}
|
|
45
|
+
return serialized
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createWebStoragePersistence(
|
|
49
|
+
storageName: 'localStorage' | 'sessionStorage',
|
|
50
|
+
options: WebStoragePersistenceOptions,
|
|
51
|
+
): ChatStorageAdapter<ChatPersistedState> {
|
|
52
|
+
const keyPrefix = options.keyPrefix ?? 'tanstack-ai:'
|
|
53
|
+
const serialize = options.serialize ?? stringifyJson
|
|
54
|
+
const deserialize = options.deserialize ?? JSON.parse
|
|
55
|
+
const key = (id: string) => `${keyPrefix}${id}`
|
|
56
|
+
|
|
57
|
+
const getStorage = (): Storage => {
|
|
58
|
+
const browserGlobals: {
|
|
59
|
+
localStorage?: Storage
|
|
60
|
+
sessionStorage?: Storage
|
|
61
|
+
} = globalThis
|
|
62
|
+
const storage = browserGlobals[storageName]
|
|
63
|
+
if (!storage) {
|
|
64
|
+
throw new StorageUnavailableError(storageName)
|
|
65
|
+
}
|
|
66
|
+
return storage
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
getItem(id) {
|
|
71
|
+
const item = getStorage().getItem(key(id))
|
|
72
|
+
return item === null ? null : deserialize(item)
|
|
73
|
+
},
|
|
74
|
+
setItem(id, value) {
|
|
75
|
+
getStorage().setItem(key(id), serialize(value))
|
|
76
|
+
},
|
|
77
|
+
removeItem(id) {
|
|
78
|
+
getStorage().removeItem(key(id))
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A `ChatStorageAdapter` backed by `window.localStorage` (persists across
|
|
85
|
+
* reloads and browser restarts). Keys are namespaced with `keyPrefix`, which
|
|
86
|
+
* defaults to `tanstack-ai:`. Every operation reads `localStorage` lazily and
|
|
87
|
+
* throws {@link StorageUnavailableError} when it is absent (e.g. SSR), so the
|
|
88
|
+
* adapter can be constructed safely on the server.
|
|
89
|
+
*
|
|
90
|
+
* The `serialize` / `deserialize` codec defaults to `JSON.stringify` /
|
|
91
|
+
* `JSON.parse`, so the common case needs no codec.
|
|
92
|
+
*/
|
|
93
|
+
export function localStoragePersistence(
|
|
94
|
+
options: WebStoragePersistenceOptions = {},
|
|
95
|
+
): ChatStorageAdapter<ChatPersistedState> {
|
|
96
|
+
return createWebStoragePersistence('localStorage', options)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A `ChatStorageAdapter` backed by `window.sessionStorage` (scoped to the tab
|
|
101
|
+
* and cleared when it closes). Identical to {@link localStoragePersistence} in
|
|
102
|
+
* every other respect: the `tanstack-ai:` default `keyPrefix`, lazy
|
|
103
|
+
* per-operation {@link StorageUnavailableError} on SSR, and a JSON codec that
|
|
104
|
+
* defaults to `JSON.stringify` / `JSON.parse`.
|
|
105
|
+
*/
|
|
106
|
+
export function sessionStoragePersistence(
|
|
107
|
+
options: WebStoragePersistenceOptions = {},
|
|
108
|
+
): ChatStorageAdapter<ChatPersistedState> {
|
|
109
|
+
return createWebStoragePersistence('sessionStorage', options)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A `ChatStorageAdapter` backed by IndexedDB, for values too large for Web
|
|
114
|
+
* Storage or that benefit from structured-clone storage. All operations are
|
|
115
|
+
* async and the database opens lazily on first use; keys are namespaced with
|
|
116
|
+
* `keyPrefix` (default `tanstack-ai:`). When IndexedDB is unavailable (e.g.
|
|
117
|
+
* SSR) each operation rejects with {@link StorageUnavailableError}.
|
|
118
|
+
*
|
|
119
|
+
* No serialize/deserialize codec is needed or accepted — values are stored via
|
|
120
|
+
* IndexedDB's native structured clone, so `Date`, `Map`, `ArrayBuffer`, etc.
|
|
121
|
+
* round-trip without a JSON step.
|
|
122
|
+
*/
|
|
123
|
+
export function indexedDBPersistence(
|
|
124
|
+
options: IndexedDBPersistenceOptions = {},
|
|
125
|
+
): ChatStorageAdapter<ChatPersistedState> {
|
|
126
|
+
const databaseName = options.databaseName ?? 'tanstack-ai'
|
|
127
|
+
const objectStoreName = options.objectStoreName ?? 'persistence'
|
|
128
|
+
const keyPrefix = options.keyPrefix ?? 'tanstack-ai:'
|
|
129
|
+
let databasePromise: Promise<IDBDatabase> | undefined
|
|
130
|
+
|
|
131
|
+
const openDatabase = (): Promise<IDBDatabase> => {
|
|
132
|
+
if (databasePromise) {
|
|
133
|
+
return databasePromise
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
databasePromise = new Promise<IDBDatabase>((resolve, reject) => {
|
|
137
|
+
const browserGlobals: { indexedDB?: IDBFactory } = globalThis
|
|
138
|
+
const factory = browserGlobals.indexedDB
|
|
139
|
+
if (!factory) {
|
|
140
|
+
reject(new StorageUnavailableError('indexedDB'))
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let request: IDBOpenDBRequest
|
|
145
|
+
let openFailed = false
|
|
146
|
+
try {
|
|
147
|
+
request = factory.open(databaseName)
|
|
148
|
+
} catch (error) {
|
|
149
|
+
reject(error)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
request.onupgradeneeded = () => {
|
|
154
|
+
if (!request.result.objectStoreNames.contains(objectStoreName)) {
|
|
155
|
+
request.result.createObjectStore(objectStoreName)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
request.onerror = () => {
|
|
159
|
+
openFailed = true
|
|
160
|
+
reject(request.error ?? new Error(`Failed to open ${databaseName}.`))
|
|
161
|
+
}
|
|
162
|
+
request.onblocked = () => {
|
|
163
|
+
openFailed = true
|
|
164
|
+
reject(
|
|
165
|
+
new Error(
|
|
166
|
+
`Opening IndexedDB database "${databaseName}" was blocked.`,
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
request.onsuccess = () => {
|
|
171
|
+
const database = request.result
|
|
172
|
+
if (openFailed) {
|
|
173
|
+
database.close()
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
database.onversionchange = () => {
|
|
177
|
+
database.close()
|
|
178
|
+
databasePromise = undefined
|
|
179
|
+
}
|
|
180
|
+
resolve(database)
|
|
181
|
+
}
|
|
182
|
+
}).catch((error: unknown) => {
|
|
183
|
+
databasePromise = undefined
|
|
184
|
+
throw error
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
return databasePromise
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const runRequest = async <TResult>(
|
|
191
|
+
mode: IDBTransactionMode,
|
|
192
|
+
createRequest: (store: IDBObjectStore) => IDBRequest<TResult>,
|
|
193
|
+
): Promise<TResult> => {
|
|
194
|
+
const database = await openDatabase()
|
|
195
|
+
return new Promise<TResult>((resolve, reject) => {
|
|
196
|
+
let request: IDBRequest<TResult>
|
|
197
|
+
let result: TResult
|
|
198
|
+
try {
|
|
199
|
+
const transaction = database.transaction(objectStoreName, mode)
|
|
200
|
+
request = createRequest(transaction.objectStore(objectStoreName))
|
|
201
|
+
request.onsuccess = () => {
|
|
202
|
+
result = request.result
|
|
203
|
+
}
|
|
204
|
+
request.onerror = () => {
|
|
205
|
+
reject(request.error ?? new Error('IndexedDB request failed.'))
|
|
206
|
+
}
|
|
207
|
+
transaction.oncomplete = () => {
|
|
208
|
+
resolve(result)
|
|
209
|
+
}
|
|
210
|
+
transaction.onerror = () => {
|
|
211
|
+
reject(
|
|
212
|
+
transaction.error ?? new Error('IndexedDB transaction failed.'),
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
transaction.onabort = () => {
|
|
216
|
+
reject(
|
|
217
|
+
transaction.error ?? new Error('IndexedDB transaction aborted.'),
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
} catch (error) {
|
|
221
|
+
reject(error)
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const key = (id: string) => `${keyPrefix}${id}`
|
|
227
|
+
return {
|
|
228
|
+
getItem(id) {
|
|
229
|
+
return runRequest('readonly', (store) => store.get(key(id)))
|
|
230
|
+
},
|
|
231
|
+
setItem(id, value) {
|
|
232
|
+
return runRequest('readwrite', (store) => store.put(value, key(id))).then(
|
|
233
|
+
() => undefined,
|
|
234
|
+
)
|
|
235
|
+
},
|
|
236
|
+
removeItem(id) {
|
|
237
|
+
return runRequest('readwrite', (store) => store.delete(key(id))).then(
|
|
238
|
+
() => undefined,
|
|
239
|
+
)
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AnyClientTool,
|
|
3
|
+
ApprovalCapabilityOf,
|
|
4
|
+
ApprovalSchemaOf,
|
|
3
5
|
AudioPart,
|
|
6
|
+
BatchInterruptError,
|
|
4
7
|
ChunkStrategy,
|
|
5
8
|
ContentPart,
|
|
6
9
|
DocumentPart,
|
|
7
10
|
ImagePart,
|
|
11
|
+
InferSchemaType,
|
|
8
12
|
InferToolInput,
|
|
9
13
|
InferToolOutput,
|
|
14
|
+
InputSchemaOf,
|
|
15
|
+
Interrupt,
|
|
16
|
+
InterruptBinding,
|
|
17
|
+
ItemInterruptError,
|
|
10
18
|
ModelMessage,
|
|
19
|
+
NoSchema,
|
|
20
|
+
RunAgentResumeItem,
|
|
21
|
+
SchemaInput,
|
|
11
22
|
StreamChunk,
|
|
12
23
|
StructuredOutputPart,
|
|
13
24
|
UIResourcePart,
|
|
@@ -17,7 +28,181 @@ import type { ConnectionAdapter } from './connection-adapters'
|
|
|
17
28
|
import type { AIDevtoolsClientMetadata } from './devtools'
|
|
18
29
|
import type { ChatDevtoolsBridgeFactory } from './devtools-noop'
|
|
19
30
|
|
|
20
|
-
export type { StructuredOutputPart }
|
|
31
|
+
export type { StructuredOutputPart }
|
|
32
|
+
|
|
33
|
+
export interface ChatResumeState {
|
|
34
|
+
threadId: string
|
|
35
|
+
runId: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ChatPendingInterrupt = Interrupt
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The durable pointer a chat keeps for the run it may need to rejoin, plus any
|
|
42
|
+
* interrupt that run is waiting on.
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
export interface ChatResumeSnapshot {
|
|
47
|
+
resumeState: ChatResumeState
|
|
48
|
+
pendingInterrupts?: Array<ChatPendingInterrupt>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type InterruptItemStatus =
|
|
52
|
+
| 'pending'
|
|
53
|
+
| 'validating'
|
|
54
|
+
| 'staged'
|
|
55
|
+
| 'submitting'
|
|
56
|
+
| 'error'
|
|
57
|
+
|
|
58
|
+
export interface BoundInterruptBase {
|
|
59
|
+
readonly id: string
|
|
60
|
+
readonly interruptId: string
|
|
61
|
+
readonly reason: string
|
|
62
|
+
readonly message?: string
|
|
63
|
+
readonly responseSchema?: Readonly<Record<string, unknown>>
|
|
64
|
+
readonly expiresAt?: string
|
|
65
|
+
readonly metadata?: Readonly<Record<string, unknown>>
|
|
66
|
+
readonly threadId: string
|
|
67
|
+
readonly interruptedRunId: string
|
|
68
|
+
readonly generation: number
|
|
69
|
+
readonly status: InterruptItemStatus
|
|
70
|
+
readonly errors: ReadonlyArray<ItemInterruptError>
|
|
71
|
+
/** @deprecated Use `errors[0]`. */
|
|
72
|
+
readonly error?: ItemInterruptError
|
|
73
|
+
/**
|
|
74
|
+
* Whether the binding/schema allows resolution at hydrate time.
|
|
75
|
+
* Does not flip on submit/expiry — gate UI on `status`, `resuming`, and
|
|
76
|
+
* `errors` for those lifecycle states.
|
|
77
|
+
*/
|
|
78
|
+
readonly canResolve: boolean
|
|
79
|
+
cancel: () => void
|
|
80
|
+
clearResolution: () => void
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface GenericAGUIInterrupt extends BoundInterruptBase {
|
|
84
|
+
readonly kind: 'generic'
|
|
85
|
+
readonly binding: Readonly<Extract<InterruptBinding, { kind: 'generic' }>>
|
|
86
|
+
resolveInterrupt: (payload: unknown) => void
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* An interrupt that arrived on the stream carrying no resume binding this
|
|
91
|
+
* client understands — no `tanstack:interruptBinding`, or one written at a
|
|
92
|
+
* protocol version we don't recognise.
|
|
93
|
+
*
|
|
94
|
+
* These are surfaced rather than hidden so a UI can show that the run is
|
|
95
|
+
* paused, but they are never resolvable here: something else owns them. A
|
|
96
|
+
* workflow engine's durable approval projected into the same AG-UI stream
|
|
97
|
+
* lands in this bucket, and resolving it through the chat resume path would
|
|
98
|
+
* send an answer no one is waiting for. Render it, or route it to whatever
|
|
99
|
+
* actually owns the pause.
|
|
100
|
+
*/
|
|
101
|
+
export interface UnboundInterrupt extends BoundInterruptBase {
|
|
102
|
+
readonly kind: 'unbound'
|
|
103
|
+
readonly binding?: undefined
|
|
104
|
+
readonly canResolve: false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type ApprovalBranchSchema<TTool, TBranch extends 'approve' | 'reject'> =
|
|
108
|
+
ApprovalSchemaOf<TTool> extends infer TApproval
|
|
109
|
+
? TApproval extends { approve?: SchemaInput; reject?: SchemaInput }
|
|
110
|
+
? Exclude<TApproval[TBranch], undefined>
|
|
111
|
+
: TApproval extends SchemaInput
|
|
112
|
+
? TApproval
|
|
113
|
+
: never
|
|
114
|
+
: never
|
|
115
|
+
|
|
116
|
+
type ApprovalEdits<TTool> =
|
|
117
|
+
InputSchemaOf<TTool> extends NoSchema
|
|
118
|
+
? { editedArgs?: never }
|
|
119
|
+
: { editedArgs?: InferToolInput<TTool> }
|
|
120
|
+
|
|
121
|
+
type ApprovalPayload<TSchema> = [TSchema] extends [never]
|
|
122
|
+
? { payload?: never }
|
|
123
|
+
: TSchema extends SchemaInput
|
|
124
|
+
? { payload: InferSchemaType<TSchema> }
|
|
125
|
+
: { payload?: never }
|
|
126
|
+
|
|
127
|
+
type ApproveArguments<TTool> = [
|
|
128
|
+
ApprovalBranchSchema<TTool, 'approve'>,
|
|
129
|
+
] extends [never]
|
|
130
|
+
? InputSchemaOf<TTool> extends NoSchema
|
|
131
|
+
? [options?: never]
|
|
132
|
+
: [options?: ApprovalEdits<TTool> & { payload?: never }]
|
|
133
|
+
: [
|
|
134
|
+
options: ApprovalEdits<TTool> &
|
|
135
|
+
ApprovalPayload<ApprovalBranchSchema<TTool, 'approve'>>,
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
type RejectArguments<TTool> = [ApprovalBranchSchema<TTool, 'reject'>] extends [
|
|
139
|
+
never,
|
|
140
|
+
]
|
|
141
|
+
? [options?: never]
|
|
142
|
+
: [
|
|
143
|
+
options: { editedArgs?: never } & ApprovalPayload<
|
|
144
|
+
ApprovalBranchSchema<TTool, 'reject'>
|
|
145
|
+
>,
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
export type ToolApprovalInterrupt<TTool extends AnyClientTool = AnyClientTool> =
|
|
149
|
+
TTool extends AnyClientTool
|
|
150
|
+
? BoundInterruptBase & {
|
|
151
|
+
readonly kind: 'tool-approval'
|
|
152
|
+
readonly binding: Readonly<
|
|
153
|
+
Extract<InterruptBinding, { kind: 'tool-approval' }>
|
|
154
|
+
>
|
|
155
|
+
readonly toolName: TTool['name']
|
|
156
|
+
readonly toolCallId: string
|
|
157
|
+
readonly originalArgs: InferToolInput<TTool>
|
|
158
|
+
// A single generic call signature — not two overloads. Overloads break
|
|
159
|
+
// editor autocomplete: a half-typed options literal (e.g.
|
|
160
|
+
// `resolveInterrupt(true, { payload: {` ) satisfies neither overload,
|
|
161
|
+
// so TS resolves no signature and offers no contextual completions.
|
|
162
|
+
// Making `approved` a generic discriminant lets TS infer it from the
|
|
163
|
+
// first argument and pick the matching branch for the rest params, so
|
|
164
|
+
// `payload` / `editedArgs` / the correct schema's fields complete
|
|
165
|
+
// per-branch (a plain union-of-tuples would offer both branches'
|
|
166
|
+
// fields) while still enforcing the right shape.
|
|
167
|
+
resolveInterrupt: <TApproved extends boolean>(
|
|
168
|
+
approved: TApproved,
|
|
169
|
+
...args: TApproved extends true
|
|
170
|
+
? ApproveArguments<TTool>
|
|
171
|
+
: RejectArguments<TTool>
|
|
172
|
+
) => void
|
|
173
|
+
}
|
|
174
|
+
: never
|
|
175
|
+
|
|
176
|
+
type ApprovalInterrupts<TTools extends ReadonlyArray<AnyClientTool>> =
|
|
177
|
+
TTools[number] extends infer TTool
|
|
178
|
+
? TTool extends AnyClientTool
|
|
179
|
+
? ApprovalCapabilityOf<TTool> extends true
|
|
180
|
+
? ToolApprovalInterrupt<TTool>
|
|
181
|
+
: never
|
|
182
|
+
: never
|
|
183
|
+
: never
|
|
184
|
+
|
|
185
|
+
// Client tools resolve through their `.client()` implementation (auto-run) or
|
|
186
|
+
// `addToolResult` — never as a bound interrupt. The `client-tool-execution`
|
|
187
|
+
// pause is handled internally and is intentionally absent from this public
|
|
188
|
+
// union.
|
|
189
|
+
export type ChatInterrupt<
|
|
190
|
+
TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,
|
|
191
|
+
> = GenericAGUIInterrupt | UnboundInterrupt | ApprovalInterrupts<TTools>
|
|
192
|
+
|
|
193
|
+
export type BoundInterrupts<
|
|
194
|
+
TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,
|
|
195
|
+
> = ReadonlyArray<ChatInterrupt<TTools>>
|
|
196
|
+
|
|
197
|
+
export interface ChatInterruptState<
|
|
198
|
+
TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,
|
|
199
|
+
> {
|
|
200
|
+
readonly interrupts: BoundInterrupts<TTools>
|
|
201
|
+
/** @deprecated Use `interrupts`. Same snapshot today. */
|
|
202
|
+
readonly pendingInterrupts: BoundInterrupts<TTools>
|
|
203
|
+
readonly interruptErrors: ReadonlyArray<BatchInterruptError>
|
|
204
|
+
readonly resuming: boolean
|
|
205
|
+
}
|
|
21
206
|
|
|
22
207
|
/**
|
|
23
208
|
* `messages` is the full UIMessage history (not a delta). `data` is the
|
|
@@ -31,6 +216,8 @@ export interface ChatFetcherInput {
|
|
|
31
216
|
data?: Record<string, unknown>
|
|
32
217
|
threadId: string
|
|
33
218
|
runId: string
|
|
219
|
+
parentRunId?: string
|
|
220
|
+
resume?: Array<RunAgentResumeItem>
|
|
34
221
|
}
|
|
35
222
|
|
|
36
223
|
export interface ChatFetcherOptions {
|
|
@@ -363,23 +550,84 @@ export interface UIMessage<
|
|
|
363
550
|
createdAt?: Date
|
|
364
551
|
}
|
|
365
552
|
|
|
553
|
+
/**
|
|
554
|
+
* A generic key/value storage adapter. `getItem` may be sync or async; the
|
|
555
|
+
* chat persistence layer treats every call as best-effort. The provided
|
|
556
|
+
* `localStoragePersistence` / `sessionStoragePersistence` / `indexedDBPersistence`
|
|
557
|
+
* factories return one of these, and `ChatStorageAdapter<ChatPersistedState>`
|
|
558
|
+
* is assignable to {@link ChatClientPersistence}.
|
|
559
|
+
*/
|
|
560
|
+
export interface ChatStorageAdapter<TValue> {
|
|
561
|
+
getItem: (
|
|
562
|
+
id: string,
|
|
563
|
+
) => TValue | null | undefined | Promise<TValue | null | undefined>
|
|
564
|
+
setItem: (id: string, value: TValue) => void | Promise<void>
|
|
565
|
+
removeItem: (id: string) => void | Promise<void>
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* The single record a `ChatClientPersistence` adapter stores per chat. It folds
|
|
570
|
+
* the two things that must survive a full page reload into one blob under one
|
|
571
|
+
* key: the message transcript and the optional resume snapshot (which run to
|
|
572
|
+
* rejoin / which interrupts to rehydrate). One adapter, one key — see
|
|
573
|
+
* {@link ChatClientPersistence}.
|
|
574
|
+
*/
|
|
575
|
+
export interface ChatPersistedState<
|
|
576
|
+
TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
577
|
+
> {
|
|
578
|
+
messages: Array<UIMessage<TTools>>
|
|
579
|
+
/** Present while a run is in flight or paused on an interrupt; absent otherwise. */
|
|
580
|
+
resume?: ChatResumeSnapshot
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Storage adapter for durable chat state. A single adapter persists both the
|
|
585
|
+
* message transcript and the resume snapshot as one {@link ChatPersistedState}
|
|
586
|
+
* record, so a full page reload restores the conversation AND can rejoin an
|
|
587
|
+
* in-flight run / rehydrate pending interrupts.
|
|
588
|
+
*
|
|
589
|
+
* For backward compatibility `getItem` may also return a bare `UIMessage[]`
|
|
590
|
+
* (the legacy messages-only format); the client normalizes it to
|
|
591
|
+
* `{ messages }`. `setItem` always writes the combined record.
|
|
592
|
+
*/
|
|
366
593
|
export interface ChatClientPersistence<
|
|
367
594
|
TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
368
595
|
> {
|
|
369
596
|
getItem: (
|
|
370
597
|
id: string,
|
|
371
598
|
) =>
|
|
599
|
+
| ChatPersistedState<TTools>
|
|
372
600
|
| Array<UIMessage<TTools>>
|
|
373
601
|
| null
|
|
374
602
|
| undefined
|
|
375
|
-
| Promise<
|
|
603
|
+
| Promise<
|
|
604
|
+
ChatPersistedState<TTools> | Array<UIMessage<TTools>> | null | undefined
|
|
605
|
+
>
|
|
376
606
|
setItem: (
|
|
377
607
|
id: string,
|
|
378
|
-
|
|
608
|
+
state: ChatPersistedState<TTools>,
|
|
379
609
|
) => void | Promise<void>
|
|
380
610
|
removeItem: (id: string) => void | Promise<void>
|
|
381
611
|
}
|
|
382
612
|
|
|
613
|
+
/**
|
|
614
|
+
* The `persistence` option for a chat.
|
|
615
|
+
*
|
|
616
|
+
* - `false` (default): ephemeral. Messages live in memory only; a reload starts
|
|
617
|
+
* from empty.
|
|
618
|
+
* - `true`: server-authoritative. Nothing is cached in the browser. On mount the
|
|
619
|
+
* client hydrates the thread from the server by its `threadId` (paints the
|
|
620
|
+
* stored transcript and tails any run still generating), so a reload or the
|
|
621
|
+
* same thread opened on another device both just resume. Requires a connection
|
|
622
|
+
* with a `hydrate` handler.
|
|
623
|
+
* - a {@link ChatClientPersistence} adapter: client-authoritative. The combined
|
|
624
|
+
* {@link ChatPersistedState} record (transcript plus resume pointer) is cached
|
|
625
|
+
* in the browser and restored on reload with no network.
|
|
626
|
+
*/
|
|
627
|
+
export type ChatPersistenceOption<
|
|
628
|
+
TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
629
|
+
> = boolean | ChatClientPersistence<TTools>
|
|
630
|
+
|
|
383
631
|
type IsUnknown<T> = unknown extends T
|
|
384
632
|
? [T] extends [unknown]
|
|
385
633
|
? true
|
|
@@ -471,22 +719,49 @@ export interface ChatClientBaseOptions<
|
|
|
471
719
|
initialMessages?: Array<UIMessage<TTools>>
|
|
472
720
|
|
|
473
721
|
/**
|
|
474
|
-
*
|
|
722
|
+
* How this chat persists across reloads. See {@link ChatPersistenceOption}.
|
|
723
|
+
*
|
|
724
|
+
* - Omit or `false`: ephemeral, in-memory only.
|
|
725
|
+
* - `true`: server-authoritative. The client caches nothing and hydrates the
|
|
726
|
+
* thread from the server by its `threadId` on mount (needs a connection with
|
|
727
|
+
* a `hydrate` handler). Big transcripts never touch the browser, and the same
|
|
728
|
+
* thread opens the same way on another device.
|
|
729
|
+
* - a {@link ChatClientPersistence} adapter: client-authoritative. The combined
|
|
730
|
+
* {@link ChatPersistedState} record (transcript plus resume pointer) is cached
|
|
731
|
+
* in the browser, restoring the transcript, pending interrupts, and an
|
|
732
|
+
* in-flight run on reload.
|
|
733
|
+
*
|
|
734
|
+
* Use `initialResumeSnapshot` for a host-supplied in-memory rehydrate instead.
|
|
475
735
|
*/
|
|
476
|
-
persistence?:
|
|
736
|
+
persistence?: ChatPersistenceOption<TTools>
|
|
477
737
|
|
|
478
738
|
/**
|
|
479
|
-
*
|
|
480
|
-
*
|
|
739
|
+
* Optional storage-key override for this chat instance, and the devtools
|
|
740
|
+
* instance id. Persistence keys on `threadId` by default; set `id` only when
|
|
741
|
+
* you need the persisted record keyed separately from the wire thread.
|
|
742
|
+
* Prefer a stable `threadId` for the common case.
|
|
743
|
+
*
|
|
744
|
+
* The framework hooks (`useChat` / `createChat`) do NOT expose `id`: a hook's
|
|
745
|
+
* identity is its `threadId`. This lower-level escape hatch exists only for
|
|
746
|
+
* direct `ChatClient` construction.
|
|
481
747
|
*/
|
|
482
748
|
id?: string
|
|
483
749
|
|
|
484
750
|
/**
|
|
485
|
-
*
|
|
486
|
-
* the
|
|
751
|
+
* The conversation id for this chat, stable across sends and reloads. It is
|
|
752
|
+
* the AG-UI thread key on the wire AND the key client persistence stores the
|
|
753
|
+
* conversation under, so set a stable `threadId` to have a reload restore the
|
|
754
|
+
* same conversation. If omitted, a unique thread id is generated per session.
|
|
487
755
|
*/
|
|
488
756
|
threadId?: string
|
|
489
757
|
|
|
758
|
+
/**
|
|
759
|
+
* Initial resumable run state, useful when rehydrating a persisted client
|
|
760
|
+
* after a full page reload. This restores the client-side interrupt
|
|
761
|
+
* descriptors needed to send AG-UI resume entries.
|
|
762
|
+
*/
|
|
763
|
+
initialResumeSnapshot?: ChatResumeSnapshot
|
|
764
|
+
|
|
490
765
|
/**
|
|
491
766
|
* Arbitrary client-controlled JSON forwarded to the server in the
|
|
492
767
|
* AG-UI `RunAgentInput.forwardedProps` field. Use this for per-session
|
|
@@ -591,6 +866,23 @@ export interface ChatClientBaseOptions<
|
|
|
591
866
|
*/
|
|
592
867
|
onQueueChange?: (queue: Array<QueuedMessage>) => void
|
|
593
868
|
|
|
869
|
+
/**
|
|
870
|
+
* Callback when resumable run state or pending interrupts change.
|
|
871
|
+
*/
|
|
872
|
+
onResumeStateChange?: (
|
|
873
|
+
resumeState: ChatResumeState | null,
|
|
874
|
+
pendingInterrupts: BoundInterrupts<TTools>,
|
|
875
|
+
) => void
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Callback when the id of the run this client has in flight changes: the new
|
|
879
|
+
* id when a run starts (a send, or a `joinRun` rejoin), `null` when it settles.
|
|
880
|
+
*/
|
|
881
|
+
onRunIdChange?: (runId: string | null) => void
|
|
882
|
+
|
|
883
|
+
/** Callback when the immutable interrupt state snapshot changes. */
|
|
884
|
+
onInterruptStateChange?: (state: ChatInterruptState<TTools>) => void
|
|
885
|
+
|
|
594
886
|
/**
|
|
595
887
|
* Callback when a custom event is received from a server-side tool.
|
|
596
888
|
* Custom events are emitted by tools using `context.emitCustomEvent()` during execution.
|