@tanstack/ai 0.47.3 → 0.48.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 +2 -1
- package/dist/esm/activities/chat/adapter.d.ts +5 -4
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/index.d.ts +4 -7
- package/dist/esm/activities/chat/index.js +201 -243
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +115 -25
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +38 -17
- package/dist/esm/activities/chat/stream/processor.js +186 -101
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +3 -2
- package/dist/esm/activities/chat/tools/tool-calls.js +15 -10
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.js +6 -6
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/stream-generation-result.js +7 -8
- package/dist/esm/activities/stream-generation-result.js.map +1 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.js +56 -53
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/adapter-internals.d.ts +2 -0
- package/dist/esm/adapter-internals.js +3 -1
- package/dist/esm/client.d.ts +8 -1
- package/dist/esm/client.js +7 -2
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.js +6 -2
- package/dist/esm/middlewares/otel.js +11 -4
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/stream-to-response.js +8 -5
- package/dist/esm/stream-to-response.js.map +1 -1
- package/dist/esm/stream-to-websocket.js +4 -2
- package/dist/esm/stream-to-websocket.js.map +1 -1
- package/dist/esm/strip-to-spec-middleware.d.ts +10 -13
- package/dist/esm/strip-to-spec-middleware.js +24 -22
- package/dist/esm/strip-to-spec-middleware.js.map +1 -1
- package/dist/esm/types.d.ts +82 -305
- package/dist/esm/utilities/adapter-yield-chunk.d.ts +31 -0
- package/dist/esm/utilities/ag-ui-usage.d.ts +24 -0
- package/dist/esm/utilities/ag-ui-usage.js +66 -0
- package/dist/esm/utilities/ag-ui-usage.js.map +1 -0
- package/dist/esm/utilities/ag-ui-wire.d.ts +14 -7
- package/dist/esm/utilities/ag-ui-wire.js +71 -30
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/chat-params.d.ts +3 -3
- package/dist/esm/utilities/chat-params.js +10 -30
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/dist/esm/utilities/chunk-ids.d.ts +5 -0
- package/dist/esm/utilities/chunk-ids.js +25 -0
- package/dist/esm/utilities/chunk-ids.js.map +1 -0
- package/dist/esm/utilities/merge-metadata.d.ts +14 -0
- package/dist/esm/utilities/merge-metadata.js +43 -0
- package/dist/esm/utilities/merge-metadata.js.map +1 -0
- package/dist/esm/utilities/normalize-stream-chunk.d.ts +3 -0
- package/dist/esm/utilities/normalize-stream-chunk.js +100 -0
- package/dist/esm/utilities/normalize-stream-chunk.js.map +1 -0
- package/dist/esm/utilities/reasoning-encrypted-value.d.ts +8 -0
- package/dist/esm/utilities/reasoning-encrypted-value.js +16 -0
- package/dist/esm/utilities/reasoning-encrypted-value.js.map +1 -0
- package/dist/esm/utilities/restore-inbound-chunk.d.ts +15 -0
- package/dist/esm/utilities/restore-inbound-chunk.js +43 -0
- package/dist/esm/utilities/restore-inbound-chunk.js.map +1 -0
- package/dist/esm/utilities/spec-event-keys.d.ts +2 -0
- package/dist/esm/utilities/spec-event-keys.js +56 -0
- package/dist/esm/utilities/spec-event-keys.js.map +1 -0
- package/dist/esm/utilities/structured-output-events.d.ts +3 -3
- package/dist/esm/utilities/structured-output-events.js.map +1 -1
- package/package.json +2 -2
- package/src/activities/chat/adapter.ts +4 -4
- package/src/activities/chat/index.ts +398 -400
- package/src/activities/chat/messages.ts +174 -34
- package/src/activities/chat/stream/processor.ts +289 -218
- package/src/activities/chat/tools/tool-calls.ts +23 -22
- package/src/activities/generateVideo/index.ts +7 -6
- package/src/activities/stream-generation-result.ts +8 -12
- package/src/activities/summarize/chat-stream-summarize.ts +94 -70
- package/src/adapter-internals.ts +2 -0
- package/src/client.ts +13 -0
- package/src/index.ts +6 -0
- package/src/middlewares/otel.ts +16 -8
- package/src/stream-to-response.ts +11 -4
- package/src/stream-to-websocket.ts +3 -1
- package/src/strip-to-spec-middleware.ts +48 -24
- package/src/types.ts +109 -393
- package/src/utilities/adapter-yield-chunk.ts +30 -0
- package/src/utilities/ag-ui-usage.test.ts +194 -0
- package/src/utilities/ag-ui-usage.ts +148 -0
- package/src/utilities/ag-ui-wire.ts +149 -31
- package/src/utilities/chat-params.ts +22 -39
- package/src/utilities/chunk-ids.ts +24 -0
- package/src/utilities/merge-metadata.test.ts +117 -0
- package/src/utilities/merge-metadata.ts +59 -0
- package/src/utilities/normalize-stream-chunk.test.ts +423 -0
- package/src/utilities/normalize-stream-chunk.ts +186 -0
- package/src/utilities/reasoning-encrypted-value.ts +18 -0
- package/src/utilities/restore-inbound-chunk.test.ts +133 -0
- package/src/utilities/restore-inbound-chunk.ts +72 -0
- package/src/utilities/spec-event-keys.test.ts +34 -0
- package/src/utilities/spec-event-keys.ts +74 -0
- package/src/utilities/structured-output-events.ts +3 -3
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { normalizeToolResult } from '../../../utilities/tool-result'
|
|
2
|
+
import { tanstackMetadata } from '../../../utilities/merge-metadata'
|
|
3
|
+
import type { AdapterYieldChunk } from '../../../utilities/adapter-yield-chunk'
|
|
2
4
|
import { isStandardSchema, parseWithStandardSchema } from './schema-converter'
|
|
3
5
|
import type { ToolApprovalResolution } from '../../../interrupts'
|
|
4
6
|
import type {
|
|
@@ -231,10 +233,8 @@ export class ToolCallManager<
|
|
|
231
233
|
* Add a TOOL_CALL_START event to begin tracking a tool call (AG-UI)
|
|
232
234
|
*/
|
|
233
235
|
addToolCallStartEvent(event: ToolCallStartEvent): void {
|
|
234
|
-
const index = event.index ?? this.toolCallsMap.size
|
|
235
|
-
const
|
|
236
|
-
Pick<ToolCallStartEvent, 'toolName'>
|
|
237
|
-
const name = runtimeEvent.toolCallName ?? runtimeEvent.toolName
|
|
236
|
+
const index = (event as AdapterYieldChunk).index ?? this.toolCallsMap.size
|
|
237
|
+
const name = event.toolCallName ?? event.toolName
|
|
238
238
|
this.toolCallsMap.set(index, {
|
|
239
239
|
id: event.toolCallId,
|
|
240
240
|
type: 'function',
|
|
@@ -250,10 +250,14 @@ export class ToolCallManager<
|
|
|
250
250
|
* Add a TOOL_CALL_ARGS event to accumulate arguments (AG-UI)
|
|
251
251
|
*/
|
|
252
252
|
addToolCallArgsEvent(event: ToolCallArgsEvent): void {
|
|
253
|
-
|
|
253
|
+
const extra = event as AdapterYieldChunk
|
|
254
254
|
for (const [, toolCall] of this.toolCallsMap.entries()) {
|
|
255
255
|
if (toolCall.id === event.toolCallId) {
|
|
256
|
-
|
|
256
|
+
if (typeof extra.args === 'string' && extra.args !== '') {
|
|
257
|
+
toolCall.function.arguments = extra.args
|
|
258
|
+
} else {
|
|
259
|
+
toolCall.function.arguments += event.delta
|
|
260
|
+
}
|
|
257
261
|
break
|
|
258
262
|
}
|
|
259
263
|
}
|
|
@@ -264,16 +268,13 @@ export class ToolCallManager<
|
|
|
264
268
|
* Called when TOOL_CALL_END is received
|
|
265
269
|
*/
|
|
266
270
|
completeToolCall(event: ToolCallEndEvent): void {
|
|
267
|
-
for (const
|
|
268
|
-
if (toolCall.id
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
break
|
|
276
|
-
}
|
|
271
|
+
for (const toolCall of this.toolCallsMap.values()) {
|
|
272
|
+
if (toolCall.id !== event.toolCallId) continue
|
|
273
|
+
if (event.input === undefined) return
|
|
274
|
+
const normalized =
|
|
275
|
+
event.input && typeof event.input === 'object' ? event.input : {}
|
|
276
|
+
toolCall.function.arguments = JSON.stringify(normalized)
|
|
277
|
+
return
|
|
277
278
|
}
|
|
278
279
|
}
|
|
279
280
|
|
|
@@ -301,7 +302,7 @@ export class ToolCallManager<
|
|
|
301
302
|
async *executeTools(
|
|
302
303
|
finishEvent: RunFinishedEvent,
|
|
303
304
|
...contextArgs: ExecuteToolsContextArgs<TContext>
|
|
304
|
-
): AsyncGenerator<
|
|
305
|
+
): AsyncGenerator<AdapterYieldChunk, Array<ModelMessage>, void> {
|
|
305
306
|
const toolCallsArray = this.getToolCalls()
|
|
306
307
|
const toolResults: Array<ModelMessage> = []
|
|
307
308
|
const hasRuntimeContext = contextArgs.length > 0
|
|
@@ -313,9 +314,6 @@ export class ToolCallManager<
|
|
|
313
314
|
let toolResultContent: string | Array<ContentPart>
|
|
314
315
|
let toolResultState: ToolOutputState | undefined
|
|
315
316
|
// Holds the parsed/validated execution output before serialization.
|
|
316
|
-
// Surfaced on the emitted `TOOL_CALL_END` event as `output` so
|
|
317
|
-
// consumers can read it typed (via `TypedStreamChunk` distribution
|
|
318
|
-
// over the tools array) without re-parsing `result`.
|
|
319
317
|
// Stays `undefined` when the tool has no `execute` (client-only
|
|
320
318
|
// tools) or when execution throws.
|
|
321
319
|
let toolOutput: unknown
|
|
@@ -397,7 +395,10 @@ export class ToolCallManager<
|
|
|
397
395
|
toolCallId: toolCall.id,
|
|
398
396
|
toolCallName: toolCall.function.name,
|
|
399
397
|
toolName: toolCall.function.name,
|
|
400
|
-
model:
|
|
398
|
+
model: (() => {
|
|
399
|
+
const model = tanstackMetadata(finishEvent)?.model
|
|
400
|
+
return typeof model === 'string' ? model : undefined
|
|
401
|
+
})(),
|
|
401
402
|
timestamp: Date.now(),
|
|
402
403
|
// Typed parsed output (undefined for failed exec / client-only tools).
|
|
403
404
|
...(toolOutput !== undefined ? { output: toolOutput } : {}),
|
|
@@ -433,7 +434,7 @@ export interface ToolResult {
|
|
|
433
434
|
duration?: number
|
|
434
435
|
/**
|
|
435
436
|
* Parsed tool input (after JSON parse + optional Standard Schema validation).
|
|
436
|
-
*
|
|
437
|
+
* Parsed tool input after JSON parse + optional Standard Schema validation.
|
|
437
438
|
*/
|
|
438
439
|
input?: unknown
|
|
439
440
|
/**
|
|
@@ -33,6 +33,8 @@ import type {
|
|
|
33
33
|
GenerationMiddlewareContext,
|
|
34
34
|
} from '../middleware/types'
|
|
35
35
|
import type { VideoAdapter } from './adapter'
|
|
36
|
+
import { normalizeStreamChunk } from '../../utilities/normalize-stream-chunk'
|
|
37
|
+
import type { AdapterYieldChunk } from '../../utilities/adapter-yield-chunk'
|
|
36
38
|
import type {
|
|
37
39
|
MediaPrompt,
|
|
38
40
|
MediaPromptFor,
|
|
@@ -728,13 +730,13 @@ async function* runStreamingVideoGeneration<
|
|
|
728
730
|
timestamp: Date.now(),
|
|
729
731
|
}
|
|
730
732
|
|
|
731
|
-
yield {
|
|
733
|
+
yield* normalizeStreamChunk({
|
|
732
734
|
type: 'RUN_FINISHED',
|
|
733
735
|
runId,
|
|
734
736
|
threadId: wireThreadId,
|
|
735
737
|
finishReason: 'stop',
|
|
736
738
|
timestamp: Date.now(),
|
|
737
|
-
} as
|
|
739
|
+
} as AdapterYieldChunk)
|
|
738
740
|
return
|
|
739
741
|
}
|
|
740
742
|
|
|
@@ -768,15 +770,14 @@ async function* runStreamingVideoGeneration<
|
|
|
768
770
|
code: payload.code,
|
|
769
771
|
source: 'generateVideo',
|
|
770
772
|
})
|
|
771
|
-
yield {
|
|
773
|
+
yield* normalizeStreamChunk({
|
|
772
774
|
type: 'RUN_ERROR',
|
|
773
775
|
runId,
|
|
774
776
|
threadId: wireThreadId,
|
|
775
777
|
message: payload.message,
|
|
776
|
-
code: payload.code,
|
|
777
|
-
error: payload,
|
|
778
|
+
...(payload.code !== undefined ? { code: payload.code } : {}),
|
|
778
779
|
timestamp: Date.now(),
|
|
779
|
-
} as
|
|
780
|
+
} as AdapterYieldChunk)
|
|
780
781
|
} finally {
|
|
781
782
|
abortControls.clear()
|
|
782
783
|
if (!settled) {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { EventType } from '@ag-ui/core'
|
|
8
8
|
import { toRunErrorPayload } from './error-payload'
|
|
9
9
|
import type { StreamChunk } from '../types'
|
|
10
|
+
import { normalizeStreamChunk } from '../utilities/normalize-stream-chunk'
|
|
10
11
|
|
|
11
12
|
function createId(prefix: string): string {
|
|
12
13
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
|
@@ -74,31 +75,26 @@ export async function* streamGenerationResult<TResult>(
|
|
|
74
75
|
timestamp: Date.now(),
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
yield {
|
|
78
|
+
yield* normalizeStreamChunk({
|
|
78
79
|
type: EventType.RUN_FINISHED,
|
|
79
80
|
runId,
|
|
80
81
|
threadId,
|
|
81
82
|
finishReason: 'stop',
|
|
82
83
|
timestamp: Date.now(),
|
|
83
|
-
}
|
|
84
|
+
})
|
|
84
85
|
} catch (error: unknown) {
|
|
85
86
|
const payload = toRunErrorPayload(error, 'Generation failed')
|
|
86
87
|
// `code` is omitted entirely when undefined so the event matches the
|
|
87
|
-
// AG-UI `code?: string` shape under `exactOptionalPropertyTypes`.
|
|
88
|
-
// deprecated nested `error` form preserves the same conditional
|
|
89
|
-
// structure for backward compatibility.
|
|
88
|
+
// AG-UI `code?: string` shape under `exactOptionalPropertyTypes`.
|
|
90
89
|
const codeFields =
|
|
91
90
|
payload.code !== undefined ? { code: payload.code } : undefined
|
|
92
|
-
yield {
|
|
91
|
+
yield* normalizeStreamChunk({
|
|
93
92
|
type: EventType.RUN_ERROR,
|
|
93
|
+
runId,
|
|
94
|
+
threadId,
|
|
94
95
|
message: payload.message,
|
|
95
96
|
...codeFields,
|
|
96
|
-
// Deprecated nested form for backward compatibility
|
|
97
|
-
error: {
|
|
98
|
-
message: payload.message,
|
|
99
|
-
...codeFields,
|
|
100
|
-
},
|
|
101
97
|
timestamp: Date.now(),
|
|
102
|
-
}
|
|
98
|
+
})
|
|
103
99
|
}
|
|
104
100
|
}
|
|
@@ -1,14 +1,58 @@
|
|
|
1
1
|
import { EventType } from '@ag-ui/core'
|
|
2
2
|
import { toRunErrorPayload } from '../error-payload'
|
|
3
3
|
import { MAX_TOKENS_KEYS } from '../../utilities/sampling-keys'
|
|
4
|
+
import { rebuildTokenUsage } from '../../utilities/ag-ui-usage'
|
|
5
|
+
import type { AdapterYieldChunk } from '../../utilities/adapter-yield-chunk'
|
|
6
|
+
import { tanstackMetadata } from '../../utilities/merge-metadata'
|
|
7
|
+
import { normalizeStreamChunk } from '../../utilities/normalize-stream-chunk'
|
|
4
8
|
import { BaseSummarizeAdapter } from './adapter'
|
|
5
9
|
import type {
|
|
6
10
|
StreamChunk,
|
|
7
11
|
SummarizationOptions,
|
|
8
12
|
SummarizationResult,
|
|
9
13
|
TextOptions,
|
|
14
|
+
TokenUsage,
|
|
10
15
|
} from '../../types'
|
|
11
16
|
|
|
17
|
+
function consumeSpecSummarizeChunk(
|
|
18
|
+
chunk: StreamChunk,
|
|
19
|
+
state: { summary: string; model: string; usage: TokenUsage },
|
|
20
|
+
): void {
|
|
21
|
+
if (chunk.type === EventType.TEXT_MESSAGE_CONTENT) {
|
|
22
|
+
if (chunk.delta) state.summary += chunk.delta
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const tanstack = tanstackMetadata(chunk)
|
|
27
|
+
if (
|
|
28
|
+
(chunk.type === EventType.RUN_STARTED ||
|
|
29
|
+
chunk.type === EventType.RUN_FINISHED ||
|
|
30
|
+
chunk.type === EventType.TEXT_MESSAGE_START) &&
|
|
31
|
+
typeof tanstack?.model === 'string'
|
|
32
|
+
) {
|
|
33
|
+
state.model = tanstack.model
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (chunk.type === EventType.RUN_FINISHED) {
|
|
37
|
+
const rebuilt = rebuildTokenUsage(chunk.usage, tanstack?.usage)
|
|
38
|
+
if (rebuilt) state.usage = rebuilt
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function throwRunError(
|
|
43
|
+
chunk: Extract<StreamChunk, { type: 'RUN_ERROR' }>,
|
|
44
|
+
): never {
|
|
45
|
+
const message =
|
|
46
|
+
typeof chunk.message === 'string' && chunk.message.length > 0
|
|
47
|
+
? chunk.message
|
|
48
|
+
: 'Summarization failed'
|
|
49
|
+
const err = new Error(message)
|
|
50
|
+
if (typeof chunk.code === 'string') {
|
|
51
|
+
;(err as Error & { code?: string }).code = chunk.code
|
|
52
|
+
}
|
|
53
|
+
throw err
|
|
54
|
+
}
|
|
55
|
+
|
|
12
56
|
/**
|
|
13
57
|
* Minimal contract for a text adapter that supports `chatStream`. Lets
|
|
14
58
|
* `ChatStreamSummarizeAdapter` work with any text adapter without coupling
|
|
@@ -21,7 +65,7 @@ import type {
|
|
|
21
65
|
* `SummarizationOptions<TProviderOptions>` on the wrapper itself.
|
|
22
66
|
*/
|
|
23
67
|
export interface ChatStreamCapable {
|
|
24
|
-
chatStream: (options: TextOptions<any>) => AsyncIterable<
|
|
68
|
+
chatStream: (options: TextOptions<any>) => AsyncIterable<AdapterYieldChunk>
|
|
25
69
|
}
|
|
26
70
|
|
|
27
71
|
/**
|
|
@@ -201,10 +245,12 @@ export class ChatStreamSummarizeAdapter<
|
|
|
201
245
|
): Promise<SummarizationResult> {
|
|
202
246
|
const systemPrompt = this.buildSummarizationPrompt(options)
|
|
203
247
|
|
|
204
|
-
let summary = ''
|
|
205
248
|
const id = this.generateId()
|
|
206
|
-
|
|
207
|
-
|
|
249
|
+
const state = {
|
|
250
|
+
summary: '',
|
|
251
|
+
model: options.model,
|
|
252
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
253
|
+
}
|
|
208
254
|
|
|
209
255
|
options.logger.request(
|
|
210
256
|
`activity=summarize provider=${this.name} model=${options.model} text-length=${options.text.length} maxLength=${options.maxLength ?? 'unset'}`,
|
|
@@ -212,41 +258,15 @@ export class ChatStreamSummarizeAdapter<
|
|
|
212
258
|
)
|
|
213
259
|
|
|
214
260
|
try {
|
|
215
|
-
for await (const
|
|
261
|
+
for await (const raw of this.textAdapter.chatStream(
|
|
216
262
|
this.buildTextOptions(options, systemPrompt),
|
|
217
263
|
)) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
summary += chunk.delta
|
|
225
|
-
}
|
|
226
|
-
model = chunk.model || model
|
|
227
|
-
}
|
|
228
|
-
if (chunk.type === 'RUN_FINISHED') {
|
|
229
|
-
if (chunk.usage) {
|
|
230
|
-
usage = chunk.usage
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
// Surface failures: the underlying chatStream emits RUN_ERROR instead
|
|
234
|
-
// of throwing, so without this branch summarize() would return an
|
|
235
|
-
// empty summary and pretend a failed run succeeded.
|
|
236
|
-
if (chunk.type === 'RUN_ERROR') {
|
|
237
|
-
const message =
|
|
238
|
-
(chunk.error && typeof chunk.error.message === 'string'
|
|
239
|
-
? chunk.error.message
|
|
240
|
-
: null) ?? 'Summarization failed'
|
|
241
|
-
const code =
|
|
242
|
-
chunk.error && typeof chunk.error.code === 'string'
|
|
243
|
-
? chunk.error.code
|
|
244
|
-
: undefined
|
|
245
|
-
const err = new Error(message)
|
|
246
|
-
if (code) {
|
|
247
|
-
;(err as Error & { code?: string }).code = code
|
|
248
|
-
}
|
|
249
|
-
throw err
|
|
264
|
+
for (const chunk of normalizeStreamChunk(raw as AdapterYieldChunk)) {
|
|
265
|
+
// Surface failures: the underlying chatStream emits RUN_ERROR instead
|
|
266
|
+
// of throwing, so without this branch summarize() would return an
|
|
267
|
+
// empty summary and pretend a failed run succeeded.
|
|
268
|
+
if (chunk.type === EventType.RUN_ERROR) throwRunError(chunk)
|
|
269
|
+
consumeSpecSummarizeChunk(chunk, state)
|
|
250
270
|
}
|
|
251
271
|
}
|
|
252
272
|
} catch (error: unknown) {
|
|
@@ -259,7 +279,12 @@ export class ChatStreamSummarizeAdapter<
|
|
|
259
279
|
throw error
|
|
260
280
|
}
|
|
261
281
|
|
|
262
|
-
return {
|
|
282
|
+
return {
|
|
283
|
+
id,
|
|
284
|
+
model: state.model,
|
|
285
|
+
summary: state.summary,
|
|
286
|
+
usage: state.usage,
|
|
287
|
+
}
|
|
263
288
|
}
|
|
264
289
|
|
|
265
290
|
override async *summarizeStream(
|
|
@@ -273,46 +298,45 @@ export class ChatStreamSummarizeAdapter<
|
|
|
273
298
|
)
|
|
274
299
|
|
|
275
300
|
const id = this.generateId()
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
301
|
+
const state = {
|
|
302
|
+
summary: '',
|
|
303
|
+
model: options.model,
|
|
304
|
+
usage: {
|
|
305
|
+
promptTokens: 0,
|
|
306
|
+
completionTokens: 0,
|
|
307
|
+
totalTokens: 0,
|
|
308
|
+
} satisfies SummarizationResult['usage'],
|
|
282
309
|
}
|
|
283
310
|
|
|
284
311
|
try {
|
|
285
|
-
for await (const
|
|
312
|
+
for await (const raw of this.textAdapter.chatStream(
|
|
286
313
|
this.buildTextOptions(options, systemPrompt),
|
|
287
314
|
)) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
315
|
+
for (const chunk of normalizeStreamChunk(raw as AdapterYieldChunk)) {
|
|
316
|
+
// Accumulate the same way `summarize()` does so consumers see deltas
|
|
317
|
+
// AND the terminal `generation:result` event below carries the same
|
|
318
|
+
// final summary that non-streaming returns.
|
|
319
|
+
consumeSpecSummarizeChunk(chunk, state)
|
|
320
|
+
|
|
321
|
+
// Emit the GenerationClient-shaped result event just before the
|
|
322
|
+
// terminal RUN_FINISHED so subscribers (useSummarize) populate
|
|
323
|
+
// `result` before flipping `status` to success.
|
|
324
|
+
if (chunk.type === EventType.RUN_FINISHED) {
|
|
325
|
+
yield {
|
|
326
|
+
type: EventType.CUSTOM,
|
|
327
|
+
name: 'generation:result',
|
|
328
|
+
value: {
|
|
329
|
+
id,
|
|
330
|
+
model: state.model,
|
|
331
|
+
summary: state.summary,
|
|
332
|
+
usage: state.usage,
|
|
333
|
+
} satisfies SummarizationResult,
|
|
334
|
+
timestamp: Date.now(),
|
|
335
|
+
}
|
|
296
336
|
}
|
|
297
|
-
if (chunk.model) model = chunk.model
|
|
298
|
-
}
|
|
299
337
|
|
|
300
|
-
|
|
301
|
-
// terminal RUN_FINISHED so subscribers (useSummarize) populate
|
|
302
|
-
// `result` before flipping `status` to success.
|
|
303
|
-
if (chunk.type === 'RUN_FINISHED') {
|
|
304
|
-
if (chunk.usage) usage = chunk.usage
|
|
305
|
-
if (chunk.model) model = chunk.model
|
|
306
|
-
yield {
|
|
307
|
-
type: EventType.CUSTOM,
|
|
308
|
-
name: 'generation:result',
|
|
309
|
-
value: { id, model, summary, usage } satisfies SummarizationResult,
|
|
310
|
-
model,
|
|
311
|
-
timestamp: Date.now(),
|
|
312
|
-
}
|
|
338
|
+
yield chunk
|
|
313
339
|
}
|
|
314
|
-
|
|
315
|
-
yield chunk
|
|
316
340
|
}
|
|
317
341
|
} catch (error: unknown) {
|
|
318
342
|
options.logger.errors(`${this.name}.summarizeStream fatal`, {
|
package/src/adapter-internals.ts
CHANGED
package/src/client.ts
CHANGED
|
@@ -297,6 +297,17 @@ export type {
|
|
|
297
297
|
} from './activities/chat/stream/index'
|
|
298
298
|
|
|
299
299
|
export { uiMessagesToWire } from './utilities/ag-ui-wire'
|
|
300
|
+
export {
|
|
301
|
+
mergeMetadata,
|
|
302
|
+
tanstackMetadata,
|
|
303
|
+
withTanstackMetadata,
|
|
304
|
+
} from './utilities/merge-metadata'
|
|
305
|
+
export { fromSpecTokenUsage, toSpecTokenUsage } from './utilities/ag-ui-usage'
|
|
306
|
+
export type { SpecTokenUsage } from './utilities/ag-ui-usage'
|
|
307
|
+
export { normalizeStreamChunk } from './utilities/normalize-stream-chunk'
|
|
308
|
+
export { restoreInboundChunk } from './utilities/restore-inbound-chunk'
|
|
309
|
+
export type { AdapterYieldChunk } from './utilities/adapter-yield-chunk'
|
|
310
|
+
export { getChunkRunId, getChunkThreadId } from './utilities/chunk-ids'
|
|
300
311
|
export type { WireMessage } from './utilities/ag-ui-wire'
|
|
301
312
|
|
|
302
313
|
export type {
|
|
@@ -326,6 +337,8 @@ export type {
|
|
|
326
337
|
StreamChunk,
|
|
327
338
|
StructuredOutputPart,
|
|
328
339
|
TextPart,
|
|
340
|
+
TanStackMessageMetadata,
|
|
341
|
+
TanStackRunMetadata,
|
|
329
342
|
ThinkingPart,
|
|
330
343
|
ToolCall,
|
|
331
344
|
ToolCallPart,
|
package/src/index.ts
CHANGED
|
@@ -459,6 +459,12 @@ export { generationParamsFromBody, generationParamsFromRequest } from './client'
|
|
|
459
459
|
|
|
460
460
|
// AG-UI wire serialization (used internally by @tanstack/ai-client)
|
|
461
461
|
export { uiMessagesToWire } from './utilities/ag-ui-wire'
|
|
462
|
+
export { mergeMetadata, withTanstackMetadata } from './utilities/merge-metadata'
|
|
463
|
+
export { fromSpecTokenUsage, toSpecTokenUsage } from './utilities/ag-ui-usage'
|
|
464
|
+
export type { SpecTokenUsage } from './utilities/ag-ui-usage'
|
|
465
|
+
export { normalizeStreamChunk } from './utilities/normalize-stream-chunk'
|
|
466
|
+
export type { AdapterYieldChunk } from './utilities/adapter-yield-chunk'
|
|
467
|
+
export { getChunkRunId, getChunkThreadId } from './utilities/chunk-ids'
|
|
462
468
|
export type { WireMessage } from './utilities/ag-ui-wire'
|
|
463
469
|
export {
|
|
464
470
|
isContentPart,
|
package/src/middlewares/otel.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
} from '../utilities/sampling-keys'
|
|
11
11
|
import { firstNumber } from '../utilities/numbers'
|
|
12
12
|
import { errorMessage, errorTypeName } from '../utilities/errors'
|
|
13
|
+
import { rebuildTokenUsage } from '../utilities/ag-ui-usage'
|
|
14
|
+
import { tanstackMetadata } from '../utilities/merge-metadata'
|
|
13
15
|
import { usageAttributes } from './usage-attributes'
|
|
14
16
|
import type {
|
|
15
17
|
AttributeValue,
|
|
@@ -631,26 +633,32 @@ export function otelMiddleware(
|
|
|
631
633
|
}
|
|
632
634
|
|
|
633
635
|
if (chunk.type !== 'RUN_FINISHED') return
|
|
636
|
+
const tanstack = tanstackMetadata(chunk)
|
|
637
|
+
const extra = chunk as {
|
|
638
|
+
finishReason?: string | null
|
|
639
|
+
model?: string
|
|
640
|
+
}
|
|
641
|
+
const finishReason = extra.finishReason ?? tanstack?.finishReason
|
|
642
|
+
const model = extra.model ?? tanstack?.model
|
|
634
643
|
// Capture for the root-span finish_reasons attribute set in onFinish,
|
|
635
644
|
// which receives base-shaped info without a finishReason field.
|
|
636
|
-
if (
|
|
645
|
+
if (finishReason) state.lastFinishReason = finishReason
|
|
637
646
|
const span = state.currentIterationSpan
|
|
638
647
|
if (!span) return
|
|
639
648
|
|
|
640
|
-
if (
|
|
641
|
-
span.setAttribute('gen_ai.response.finish_reasons', [
|
|
642
|
-
chunk.finishReason,
|
|
643
|
-
])
|
|
649
|
+
if (finishReason) {
|
|
650
|
+
span.setAttribute('gen_ai.response.finish_reasons', [finishReason])
|
|
644
651
|
}
|
|
645
|
-
if (
|
|
652
|
+
if (model) span.setAttribute('gen_ai.response.model', model)
|
|
646
653
|
|
|
647
654
|
// Set usage attributes on the iteration span directly from the chunk
|
|
648
655
|
// so they're available before `onUsage` fires. Histogram recording is
|
|
649
656
|
// deliberately NOT done here — the chat runner always invokes
|
|
650
657
|
// `runOnUsage` when `chunk.usage` is present, and `onUsage` is the
|
|
651
658
|
// canonical place for the metric. Recording in both would double-count.
|
|
652
|
-
|
|
653
|
-
|
|
659
|
+
const tokenUsage = rebuildTokenUsage(chunk.usage, tanstack?.usage)
|
|
660
|
+
if (tokenUsage) {
|
|
661
|
+
span.setAttributes(usageAttributes(tokenUsage))
|
|
654
662
|
}
|
|
655
663
|
|
|
656
664
|
if (captureContent && state.assistantTextBuffer.length > 0) {
|
|
@@ -8,6 +8,7 @@ import { wasRunDetached } from './delivery-detach'
|
|
|
8
8
|
import { notifyRunDisconnected } from './delivery-disconnect'
|
|
9
9
|
import { resolveResumeRunId } from './stream-durability'
|
|
10
10
|
import { EventType } from './types'
|
|
11
|
+
import { toWireChunk } from './strip-to-spec-middleware'
|
|
11
12
|
import { resolveDebugOption } from './logger/resolve'
|
|
12
13
|
import { runErrorEventToError } from './utilities/errors'
|
|
13
14
|
import type { LockStore } from './activities/chat/middleware/locks'
|
|
@@ -287,10 +288,13 @@ function sseEncoders(
|
|
|
287
288
|
encodeChunk: (chunk, index) => {
|
|
288
289
|
const id = getId?.(chunk, index)
|
|
289
290
|
const idLine = id === undefined ? '' : `id: ${id}\n`
|
|
290
|
-
|
|
291
|
+
const wire = toWireChunk(chunk)
|
|
292
|
+
return encoder.encode(`${idLine}data: ${JSON.stringify(wire)}\n\n`)
|
|
291
293
|
},
|
|
292
294
|
encodeError: (error) =>
|
|
293
|
-
encoder.encode(
|
|
295
|
+
encoder.encode(
|
|
296
|
+
`data: ${JSON.stringify(toWireChunk(runErrorChunk(error)))}\n\n`,
|
|
297
|
+
),
|
|
294
298
|
}
|
|
295
299
|
}
|
|
296
300
|
|
|
@@ -1069,12 +1073,15 @@ function ndjsonEncoders(
|
|
|
1069
1073
|
return {
|
|
1070
1074
|
encodeChunk: (chunk, index) => {
|
|
1071
1075
|
const id = getId?.(chunk, index)
|
|
1076
|
+
const wire = toWireChunk(chunk)
|
|
1072
1077
|
const line =
|
|
1073
|
-
id === undefined
|
|
1078
|
+
id === undefined
|
|
1079
|
+
? JSON.stringify(wire)
|
|
1080
|
+
: JSON.stringify({ id, chunk: wire })
|
|
1074
1081
|
return encoder.encode(`${line}\n`)
|
|
1075
1082
|
},
|
|
1076
1083
|
encodeError: (error) =>
|
|
1077
|
-
encoder.encode(`${JSON.stringify(runErrorChunk(error))}\n`),
|
|
1084
|
+
encoder.encode(`${JSON.stringify(toWireChunk(runErrorChunk(error)))}\n`),
|
|
1078
1085
|
}
|
|
1079
1086
|
}
|
|
1080
1087
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { chatParamsFromRequestBody } from './utilities/chat-params'
|
|
2
2
|
import { durableStreamSource, runErrorChunk } from './stream-to-response'
|
|
3
|
+
import { toWireChunk } from './strip-to-spec-middleware'
|
|
3
4
|
import { resolveDebugOption } from './logger/resolve'
|
|
4
5
|
import type { StreamDurability } from './stream-durability'
|
|
5
6
|
import type { DebugOption } from './logger/types'
|
|
@@ -35,7 +36,8 @@ export function encodeWsFrame(
|
|
|
35
36
|
chunk: StreamChunk,
|
|
36
37
|
id: string | undefined,
|
|
37
38
|
): string {
|
|
38
|
-
|
|
39
|
+
const wire = toWireChunk(chunk)
|
|
40
|
+
return JSON.stringify(id === undefined ? wire : { id, chunk: wire })
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
|
@@ -1,34 +1,58 @@
|
|
|
1
|
-
import type { ChatMiddleware } from './activities/chat/middleware/types'
|
|
2
1
|
import type { StreamChunk } from './types'
|
|
2
|
+
import { EventType } from './types'
|
|
3
|
+
import type { AdapterYieldChunk } from './utilities/adapter-yield-chunk'
|
|
4
|
+
import { isTanstackUsage, toSpecTokenUsage } from './utilities/ag-ui-usage'
|
|
5
|
+
import {
|
|
6
|
+
tanstackMetadata,
|
|
7
|
+
withTanstackMetadata,
|
|
8
|
+
} from './utilities/merge-metadata'
|
|
9
|
+
import { normalizeStreamChunk } from './utilities/normalize-stream-chunk'
|
|
10
|
+
import { isSpecTopLevelKey } from './utilities/spec-event-keys'
|
|
3
11
|
|
|
4
12
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* All other fields pass through unchanged. @ag-ui/core's BaseEventSchema
|
|
9
|
-
* uses `.passthrough()`, so extra fields (model, content, usage,
|
|
10
|
-
* finishReason, toolName, stepId, etc.) are allowed and won't break
|
|
11
|
-
* spec validation or verifyEvents.
|
|
13
|
+
* Delete unknown top-level keys from a stream chunk.
|
|
14
|
+
* Keep only AG-UI spec keys for this event type.
|
|
15
|
+
* Convert TanStack TokenUsage objects to the spec `usage[]` array.
|
|
12
16
|
*/
|
|
13
|
-
export function stripToSpec(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
export function stripToSpec(
|
|
18
|
+
chunk: StreamChunk | AdapterYieldChunk,
|
|
19
|
+
): StreamChunk {
|
|
20
|
+
const out: Record<string, unknown> = {}
|
|
21
|
+
for (const [key, value] of Object.entries(chunk)) {
|
|
22
|
+
if (isSpecTopLevelKey(chunk.type, key) && value !== undefined) {
|
|
23
|
+
out[key] = value
|
|
24
|
+
}
|
|
18
25
|
}
|
|
19
|
-
|
|
26
|
+
|
|
27
|
+
if (
|
|
28
|
+
(chunk.type === EventType.RUN_FINISHED ||
|
|
29
|
+
chunk.type === EventType.RUN_ERROR) &&
|
|
30
|
+
isTanstackUsage(out.usage)
|
|
31
|
+
) {
|
|
32
|
+
const model = tanstackMetadata(chunk)?.model
|
|
33
|
+
const { usage, leftover } = toSpecTokenUsage(out.usage, {
|
|
34
|
+
model: typeof model === 'string' ? model : undefined,
|
|
35
|
+
})
|
|
36
|
+
out.usage = usage
|
|
37
|
+
if (leftover !== undefined) {
|
|
38
|
+
return withTanstackMetadata(out as StreamChunk, {
|
|
39
|
+
usage: leftover,
|
|
40
|
+
}) as StreamChunk
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return out as StreamChunk
|
|
20
45
|
}
|
|
21
46
|
|
|
22
47
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
48
|
+
* Move TanStack extras into `metadata.tanstack`, then keep only spec keys.
|
|
49
|
+
* Custom servers that skip `chat()` still round-trip `finishReason` on SSE/HTTP/WS.
|
|
50
|
+
* Fan-out extras (encrypted-value, TOOL_CALL_RESULT) stay on the `chat()` path;
|
|
51
|
+
* this encoder is 1:1 with the durability log offset.
|
|
26
52
|
*/
|
|
27
|
-
export function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
},
|
|
33
|
-
}
|
|
53
|
+
export function toWireChunk(
|
|
54
|
+
chunk: StreamChunk | AdapterYieldChunk,
|
|
55
|
+
): StreamChunk {
|
|
56
|
+
const [normalized] = normalizeStreamChunk(chunk)
|
|
57
|
+
return stripToSpec(normalized ?? chunk)
|
|
34
58
|
}
|