@tanstack/ai-client 0.32.0 → 0.32.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 +42 -16
- package/dist/esm/byok/client.d.ts +5 -0
- package/dist/esm/byok/client.js +19 -2
- package/dist/esm/byok/client.js.map +1 -1
- package/dist/esm/chat-client.d.ts +4 -0
- package/dist/esm/chat-client.js +37 -6
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/generation-client.js +2 -1
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/video-generation-client.js +2 -1
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +1 -1
- package/src/byok/client.ts +23 -2
- package/src/chat-client.ts +79 -5
- package/src/generation-client.ts +2 -0
- package/src/video-generation-client.ts +2 -0
package/src/chat-client.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
ByokMissingError,
|
|
15
15
|
ByokUnresolvedProviderError,
|
|
16
16
|
} from '@tanstack/ai/byok'
|
|
17
|
+
import { byokFallbackProviderId } from './byok/client'
|
|
17
18
|
import {
|
|
18
19
|
prepareResolvedByokHeaders,
|
|
19
20
|
resolveByokProviderId,
|
|
@@ -331,6 +332,9 @@ const REJOIN_REBUILD_TRIGGERS = new Set<string>([
|
|
|
331
332
|
'MESSAGES_SNAPSHOT',
|
|
332
333
|
])
|
|
333
334
|
|
|
335
|
+
// Cap parent-chain walks so a cyclic replayed parentRunId cannot loop.
|
|
336
|
+
const MAX_RUN_LINEAGE_DEPTH = 64
|
|
337
|
+
|
|
334
338
|
export class ChatClient<
|
|
335
339
|
TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
336
340
|
TContext = unknown,
|
|
@@ -353,6 +357,9 @@ export class ChatClient<
|
|
|
353
357
|
// run, so approvals/client-tool results can be sent back. Cleared when the
|
|
354
358
|
// run terminates. This is STATE (interrupt) resume, not delivery/cursor.
|
|
355
359
|
private lastResume: ChatResumeState | null = null
|
|
360
|
+
// Replay can deliver a parent pause after a descendant already finished.
|
|
361
|
+
private readonly runParents = new Map<string, string>()
|
|
362
|
+
private readonly supersededInterruptRunIds = new Set<string>()
|
|
356
363
|
// The in-flight run id already handed to `resumeInFlightRun`, so a persisted
|
|
357
364
|
// run is rejoined at most once even when both the sync read and the async
|
|
358
365
|
// hydrate surface the same resume pointer.
|
|
@@ -1157,14 +1164,30 @@ export class ChatClient<
|
|
|
1157
1164
|
): void {
|
|
1158
1165
|
if (chunk.type === 'RUN_STARTED') {
|
|
1159
1166
|
const chunkRunId = getChunkRunId(chunk) ?? chunk.runId
|
|
1167
|
+
const parentRunId =
|
|
1168
|
+
'parentRunId' in chunk && typeof chunk.parentRunId === 'string'
|
|
1169
|
+
? chunk.parentRunId
|
|
1170
|
+
: undefined
|
|
1171
|
+
if (parentRunId) {
|
|
1172
|
+
this.runParents.set(chunkRunId, parentRunId)
|
|
1173
|
+
if (this.supersededInterruptRunIds.has(chunkRunId)) {
|
|
1174
|
+
this.markLineageAnswered(parentRunId)
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1160
1177
|
this.activeResumeThreadId =
|
|
1161
1178
|
'threadId' in chunk && typeof chunk.threadId === 'string'
|
|
1162
1179
|
? chunk.threadId
|
|
1163
1180
|
: this.activeResumeThreadId
|
|
1164
1181
|
this.activeResumeRunId = chunkRunId
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1182
|
+
// Late RUN_STARTED for a run that already finished is replay, not a live
|
|
1183
|
+
// turn. A new send can reuse a finished run id while isLoading is true.
|
|
1184
|
+
const isAnsweredReplayStart =
|
|
1185
|
+
this.supersededInterruptRunIds.has(chunkRunId) && !this.isLoading
|
|
1186
|
+
if (!isAnsweredReplayStart) {
|
|
1187
|
+
this.activeRunIds.add(chunkRunId)
|
|
1188
|
+
this.clearedStreamTracker.onRunStarted(chunkRunId)
|
|
1189
|
+
this.setSessionGenerating(true)
|
|
1190
|
+
}
|
|
1168
1191
|
// Persist a live-run resume snapshot so a full page reload can rejoin this
|
|
1169
1192
|
// in-flight run via joinRun. Only a persistor writes it, and a persistor
|
|
1170
1193
|
// exists only in client-authoritative mode; server-authoritative reconnect
|
|
@@ -1172,12 +1195,25 @@ export class ChatClient<
|
|
|
1172
1195
|
// client-cached run pointer (which goes stale the moment a turn spans a
|
|
1173
1196
|
// second run) is ever written. Interrupt/terminal handling overwrites or
|
|
1174
1197
|
// clears it in observeInterruptState.
|
|
1175
|
-
if (
|
|
1198
|
+
if (
|
|
1199
|
+
this.persistor &&
|
|
1200
|
+
this.connection.joinRun &&
|
|
1201
|
+
!this.lastResume &&
|
|
1202
|
+
!isAnsweredReplayStart
|
|
1203
|
+
) {
|
|
1176
1204
|
this.persistResumeSnapshot({
|
|
1177
1205
|
threadId: this.activeResumeThreadId ?? this.threadId,
|
|
1178
1206
|
runId: chunkRunId,
|
|
1179
1207
|
})
|
|
1180
1208
|
}
|
|
1209
|
+
if (
|
|
1210
|
+
this.lastResume &&
|
|
1211
|
+
this.supersededInterruptRunIds.has(this.lastResume.runId)
|
|
1212
|
+
) {
|
|
1213
|
+
this.lastResume = null
|
|
1214
|
+
this.persistor?.persistResumeSnapshot(null)
|
|
1215
|
+
this.interruptManager.reset()
|
|
1216
|
+
}
|
|
1181
1217
|
return
|
|
1182
1218
|
}
|
|
1183
1219
|
|
|
@@ -1222,11 +1258,29 @@ export class ChatClient<
|
|
|
1222
1258
|
? chunk.threadId
|
|
1223
1259
|
: this.activeResumeThreadId
|
|
1224
1260
|
|
|
1261
|
+
// Record answered lineage before hydrating a pause. Intermediate
|
|
1262
|
+
// tool_calls RUN_FINISHED is a mid-turn handoff, not an answered pause.
|
|
1263
|
+
if (
|
|
1264
|
+
runId &&
|
|
1265
|
+
!isIntermediateToolTurn(chunk) &&
|
|
1266
|
+
(chunk.type === 'RUN_ERROR' ||
|
|
1267
|
+
(chunk.type === 'RUN_FINISHED' && chunk.outcome?.type !== 'interrupt'))
|
|
1268
|
+
) {
|
|
1269
|
+
this.markLineageAnswered(runId)
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1225
1272
|
if (chunk.type === 'RUN_FINISHED' && chunk.outcome?.type === 'interrupt') {
|
|
1226
1273
|
// Track the REQUEST run id (what the client sent) so a resume targets the
|
|
1227
1274
|
// same run even when provider events carry their own run id.
|
|
1228
1275
|
const interruptedRunId =
|
|
1229
1276
|
this.currentRunId ?? runId ?? this.activeResumeRunId ?? ''
|
|
1277
|
+
// Replay re-emits a pause that a continuation already answered.
|
|
1278
|
+
if (
|
|
1279
|
+
interruptedRunId &&
|
|
1280
|
+
this.supersededInterruptRunIds.has(interruptedRunId)
|
|
1281
|
+
) {
|
|
1282
|
+
return
|
|
1283
|
+
}
|
|
1230
1284
|
this.lastResume = {
|
|
1231
1285
|
threadId: threadId ?? this.threadId,
|
|
1232
1286
|
runId: interruptedRunId,
|
|
@@ -1271,13 +1325,20 @@ export class ChatClient<
|
|
|
1271
1325
|
chunk.type === 'RUN_FINISHED' &&
|
|
1272
1326
|
chunk.outcome?.type !== 'interrupt',
|
|
1273
1327
|
)
|
|
1328
|
+
// Replay can deliver a parent pause after a descendant already finished,
|
|
1329
|
+
// and this terminal's run id may not match lastResume.
|
|
1330
|
+
const isLineageDescendantTerminal = Boolean(
|
|
1331
|
+
this.lastResume &&
|
|
1332
|
+
this.supersededInterruptRunIds.has(this.lastResume.runId),
|
|
1333
|
+
)
|
|
1274
1334
|
if (
|
|
1275
1335
|
isRunlessSessionError ||
|
|
1276
1336
|
isTrackedRunTerminal ||
|
|
1277
1337
|
isCurrentRunTerminal ||
|
|
1278
1338
|
isActiveStreamRunTerminal ||
|
|
1279
1339
|
isCurrentStreamTerminal ||
|
|
1280
|
-
isActiveInterruptSubmissionTerminal
|
|
1340
|
+
isActiveInterruptSubmissionTerminal ||
|
|
1341
|
+
isLineageDescendantTerminal
|
|
1281
1342
|
) {
|
|
1282
1343
|
this.lastResume = null
|
|
1283
1344
|
// Run settled without an interrupt: drop the durable resume snapshot so a
|
|
@@ -1289,6 +1350,18 @@ export class ChatClient<
|
|
|
1289
1350
|
this.notifyResumeStateChange('live')
|
|
1290
1351
|
}
|
|
1291
1352
|
|
|
1353
|
+
/** Walk parentRunId links and mark this run plus its ancestors as answered. */
|
|
1354
|
+
private markLineageAnswered(runId: string): void {
|
|
1355
|
+
let current: string | undefined = runId
|
|
1356
|
+
let hops = 0
|
|
1357
|
+
while (current !== undefined && hops < MAX_RUN_LINEAGE_DEPTH) {
|
|
1358
|
+
if (this.supersededInterruptRunIds.has(current)) return
|
|
1359
|
+
this.supersededInterruptRunIds.add(current)
|
|
1360
|
+
current = this.runParents.get(current)
|
|
1361
|
+
hops++
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1292
1365
|
/**
|
|
1293
1366
|
* The interrupt-resume state for the active/interrupted run (its run/thread
|
|
1294
1367
|
* ids), or null when there is nothing to resume. Apps can persist this to
|
|
@@ -2350,6 +2423,7 @@ export class ChatClient<
|
|
|
2350
2423
|
const provider = resolveByokProviderId(
|
|
2351
2424
|
this.byokProvider,
|
|
2352
2425
|
mergedBody.provider,
|
|
2426
|
+
byokFallbackProviderId(this.byok),
|
|
2353
2427
|
)
|
|
2354
2428
|
byokHeaders = await prepareResolvedByokHeaders(this.byok, provider)
|
|
2355
2429
|
}
|
package/src/generation-client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ByokBlockedError, ByokMissingError } from '@tanstack/ai/byok'
|
|
2
|
+
import { byokFallbackProviderId } from './byok/client'
|
|
2
3
|
import {
|
|
3
4
|
prepareResolvedByokHeaders,
|
|
4
5
|
resolveByokProviderId,
|
|
@@ -280,6 +281,7 @@ export class GenerationClient<
|
|
|
280
281
|
const provider = resolveByokProviderId(
|
|
281
282
|
this.byokProvider,
|
|
282
283
|
this.body.provider,
|
|
284
|
+
byokFallbackProviderId(this.byok),
|
|
283
285
|
)
|
|
284
286
|
headers = await prepareResolvedByokHeaders(this.byok, provider)
|
|
285
287
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ByokBlockedError, ByokMissingError } from '@tanstack/ai/byok'
|
|
2
|
+
import { byokFallbackProviderId } from './byok/client'
|
|
2
3
|
import {
|
|
3
4
|
prepareResolvedByokHeaders,
|
|
4
5
|
resolveByokProviderId,
|
|
@@ -273,6 +274,7 @@ export class VideoGenerationClient<TOutput = VideoGenerateResult> {
|
|
|
273
274
|
const provider = resolveByokProviderId(
|
|
274
275
|
this.byokProvider,
|
|
275
276
|
this.body.provider,
|
|
277
|
+
byokFallbackProviderId(this.byok),
|
|
276
278
|
)
|
|
277
279
|
headers = await prepareResolvedByokHeaders(this.byok, provider)
|
|
278
280
|
}
|