@tanstack/ai 0.10.3 → 0.11.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/dist/esm/activities/chat/index.d.ts +4 -0
- package/dist/esm/activities/chat/index.js +50 -16
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/middleware/compose.js +5 -4
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +16 -1
- package/dist/esm/activities/chat/stream/processor.js +120 -21
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/stream/types.d.ts +1 -0
- package/dist/esm/activities/chat/tools/tool-calls.js +3 -1
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.js +6 -0
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/stream-generation-result.d.ts +3 -2
- package/dist/esm/activities/stream-generation-result.js +12 -4
- package/dist/esm/activities/stream-generation-result.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/stream-to-response.d.ts +2 -2
- package/dist/esm/stream-to-response.js +0 -1
- package/dist/esm/stream-to-response.js.map +1 -1
- package/dist/esm/strip-to-spec-middleware.d.ts +18 -0
- package/dist/esm/strip-to-spec-middleware.js +20 -0
- package/dist/esm/strip-to-spec-middleware.js.map +1 -0
- package/dist/esm/types.d.ts +229 -95
- package/package.json +3 -2
- package/src/activities/chat/index.ts +90 -25
- package/src/activities/chat/middleware/compose.ts +6 -4
- package/src/activities/chat/stream/processor.ts +152 -25
- package/src/activities/chat/stream/types.ts +1 -0
- package/src/activities/chat/tools/tool-calls.ts +4 -2
- package/src/activities/generateVideo/index.ts +13 -6
- package/src/activities/stream-generation-result.ts +19 -11
- package/src/stream-to-response.ts +2 -4
- package/src/strip-to-spec-middleware.ts +37 -0
- package/src/types.ts +276 -109
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { devtoolsMiddleware } from '@tanstack/ai-event-client'
|
|
9
|
+
import { stripToSpecMiddleware } from '../../strip-to-spec-middleware'
|
|
9
10
|
import { streamToText } from '../../stream-to-response.js'
|
|
10
11
|
import { LazyToolManager } from './tools/lazy-tool-manager'
|
|
11
12
|
import {
|
|
@@ -104,6 +105,10 @@ export interface TextActivityOptions<
|
|
|
104
105
|
agentLoopStrategy?: TextOptions['agentLoopStrategy']
|
|
105
106
|
/** Unique conversation identifier for tracking */
|
|
106
107
|
conversationId?: TextOptions['conversationId']
|
|
108
|
+
/** Thread/conversation ID for AG-UI protocol. Auto-generated if not provided. */
|
|
109
|
+
threadId?: TextOptions['threadId']
|
|
110
|
+
/** Run ID override for AG-UI protocol. Auto-generated by adapter if not provided. */
|
|
111
|
+
runId?: TextOptions['runId']
|
|
107
112
|
/**
|
|
108
113
|
* Optional Standard Schema for structured output.
|
|
109
114
|
* When provided, the activity will:
|
|
@@ -263,6 +268,10 @@ class TextEngine<
|
|
|
263
268
|
private readonly initialApprovals: Map<string, boolean>
|
|
264
269
|
private readonly initialClientToolResults: Map<string, any>
|
|
265
270
|
|
|
271
|
+
// AG-UI protocol IDs
|
|
272
|
+
private threadId: string
|
|
273
|
+
private runIdOverride?: string
|
|
274
|
+
|
|
266
275
|
// Middleware support
|
|
267
276
|
private readonly middlewareRunner: MiddlewareRunner
|
|
268
277
|
private readonly middlewareCtx: ChatMiddlewareContext
|
|
@@ -307,9 +316,18 @@ class TextEngine<
|
|
|
307
316
|
? { signal: config.params.abortController.signal }
|
|
308
317
|
: undefined
|
|
309
318
|
this.effectiveSignal = config.params.abortController?.signal
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
319
|
+
this.threadId = config.params.threadId || this.createId('thread')
|
|
320
|
+
this.runIdOverride = config.params.runId
|
|
321
|
+
|
|
322
|
+
// Initialize middleware — devtools first, strip-to-spec always last.
|
|
323
|
+
// handleStreamChunk processes raw chunks BEFORE middleware, so internal
|
|
324
|
+
// state management sees extended fields (finishReason, delta, toolCallName, etc.).
|
|
325
|
+
// The strip middleware ensures the yielded public stream is AG-UI spec-compliant.
|
|
326
|
+
const allMiddleware = [
|
|
327
|
+
devtoolsMiddleware(),
|
|
328
|
+
...(config.middleware || []),
|
|
329
|
+
stripToSpecMiddleware(),
|
|
330
|
+
]
|
|
313
331
|
this.middlewareRunner = new MiddlewareRunner(allMiddleware)
|
|
314
332
|
this.middlewareAbortController = new AbortController()
|
|
315
333
|
this.middlewareCtx = {
|
|
@@ -535,6 +553,8 @@ class TextEngine<
|
|
|
535
553
|
request: this.effectiveRequest,
|
|
536
554
|
modelOptions,
|
|
537
555
|
systemPrompts: this.systemPrompts,
|
|
556
|
+
threadId: this.threadId,
|
|
557
|
+
runId: this.runIdOverride,
|
|
538
558
|
})) {
|
|
539
559
|
if (this.isCancelled()) {
|
|
540
560
|
break
|
|
@@ -542,14 +562,17 @@ class TextEngine<
|
|
|
542
562
|
|
|
543
563
|
this.totalChunkCount++
|
|
544
564
|
|
|
545
|
-
//
|
|
565
|
+
// Process the original (unstripped) chunk for internal state management
|
|
566
|
+
// BEFORE middleware, so fields like finishReason, delta, etc. are available
|
|
567
|
+
this.handleStreamChunk(chunk)
|
|
568
|
+
|
|
569
|
+
// Pipe chunk through middleware (devtools middleware observes; strip-to-spec cleans)
|
|
546
570
|
const outputChunks = await this.middlewareRunner.runOnChunk(
|
|
547
571
|
this.middlewareCtx,
|
|
548
572
|
chunk,
|
|
549
573
|
)
|
|
550
574
|
for (const outputChunk of outputChunks) {
|
|
551
575
|
yield outputChunk
|
|
552
|
-
this.handleStreamChunk(outputChunk)
|
|
553
576
|
this.middlewareCtx.chunkIndex++
|
|
554
577
|
}
|
|
555
578
|
|
|
@@ -589,6 +612,18 @@ class TextEngine<
|
|
|
589
612
|
this.handleStepFinishedEvent(chunk)
|
|
590
613
|
break
|
|
591
614
|
|
|
615
|
+
case 'TOOL_CALL_RESULT':
|
|
616
|
+
// Tool result is already added to messages in buildToolResultChunks
|
|
617
|
+
break
|
|
618
|
+
|
|
619
|
+
case 'REASONING_START':
|
|
620
|
+
case 'REASONING_MESSAGE_START':
|
|
621
|
+
case 'REASONING_MESSAGE_CONTENT':
|
|
622
|
+
case 'REASONING_MESSAGE_END':
|
|
623
|
+
case 'REASONING_END':
|
|
624
|
+
// Reasoning events are handled by StreamProcessor
|
|
625
|
+
break
|
|
626
|
+
|
|
592
627
|
default:
|
|
593
628
|
// RUN_STARTED, TEXT_MESSAGE_START, TEXT_MESSAGE_END, STEP_STARTED,
|
|
594
629
|
// STATE_SNAPSHOT, STATE_DELTA, CUSTOM
|
|
@@ -624,7 +659,7 @@ class TextEngine<
|
|
|
624
659
|
|
|
625
660
|
private handleRunFinishedEvent(chunk: RunFinishedEvent): void {
|
|
626
661
|
this.finishedEvent = chunk
|
|
627
|
-
this.lastFinishReason = chunk.finishReason
|
|
662
|
+
this.lastFinishReason = chunk.finishReason ?? null
|
|
628
663
|
}
|
|
629
664
|
|
|
630
665
|
private handleRunErrorEvent(
|
|
@@ -675,7 +710,7 @@ class TextEngine<
|
|
|
675
710
|
undiscoveredLazyResults,
|
|
676
711
|
finishEvent,
|
|
677
712
|
)) {
|
|
678
|
-
yield chunk
|
|
713
|
+
yield* this.pipeThroughMiddleware(chunk)
|
|
679
714
|
}
|
|
680
715
|
}
|
|
681
716
|
|
|
@@ -748,7 +783,7 @@ class TextEngine<
|
|
|
748
783
|
finishEvent,
|
|
749
784
|
argsMap,
|
|
750
785
|
)) {
|
|
751
|
-
yield chunk
|
|
786
|
+
yield* this.pipeThroughMiddleware(chunk)
|
|
752
787
|
}
|
|
753
788
|
}
|
|
754
789
|
|
|
@@ -756,14 +791,14 @@ class TextEngine<
|
|
|
756
791
|
executionResult.needsApproval,
|
|
757
792
|
finishEvent,
|
|
758
793
|
)) {
|
|
759
|
-
yield chunk
|
|
794
|
+
yield* this.pipeThroughMiddleware(chunk)
|
|
760
795
|
}
|
|
761
796
|
|
|
762
797
|
for (const chunk of this.buildClientToolChunks(
|
|
763
798
|
executionResult.needsClientExecution,
|
|
764
799
|
finishEvent,
|
|
765
800
|
)) {
|
|
766
|
-
yield chunk
|
|
801
|
+
yield* this.pipeThroughMiddleware(chunk)
|
|
767
802
|
}
|
|
768
803
|
|
|
769
804
|
this.setToolPhase('wait')
|
|
@@ -777,7 +812,7 @@ class TextEngine<
|
|
|
777
812
|
)
|
|
778
813
|
|
|
779
814
|
for (const chunk of toolResultChunks) {
|
|
780
|
-
yield chunk
|
|
815
|
+
yield* this.pipeThroughMiddleware(chunk)
|
|
781
816
|
}
|
|
782
817
|
|
|
783
818
|
return 'continue'
|
|
@@ -824,7 +859,7 @@ class TextEngine<
|
|
|
824
859
|
undiscoveredLazyResults,
|
|
825
860
|
finishEvt,
|
|
826
861
|
)) {
|
|
827
|
-
yield chunk
|
|
862
|
+
yield* this.pipeThroughMiddleware(chunk)
|
|
828
863
|
}
|
|
829
864
|
}
|
|
830
865
|
|
|
@@ -895,7 +930,7 @@ class TextEngine<
|
|
|
895
930
|
executionResult.results,
|
|
896
931
|
finishEvent,
|
|
897
932
|
)) {
|
|
898
|
-
yield chunk
|
|
933
|
+
yield* this.pipeThroughMiddleware(chunk)
|
|
899
934
|
}
|
|
900
935
|
}
|
|
901
936
|
|
|
@@ -903,14 +938,14 @@ class TextEngine<
|
|
|
903
938
|
executionResult.needsApproval,
|
|
904
939
|
finishEvent,
|
|
905
940
|
)) {
|
|
906
|
-
yield chunk
|
|
941
|
+
yield* this.pipeThroughMiddleware(chunk)
|
|
907
942
|
}
|
|
908
943
|
|
|
909
944
|
for (const chunk of this.buildClientToolChunks(
|
|
910
945
|
executionResult.needsClientExecution,
|
|
911
946
|
finishEvent,
|
|
912
947
|
)) {
|
|
913
|
-
yield chunk
|
|
948
|
+
yield* this.pipeThroughMiddleware(chunk)
|
|
914
949
|
}
|
|
915
950
|
|
|
916
951
|
this.setToolPhase('wait')
|
|
@@ -923,7 +958,7 @@ class TextEngine<
|
|
|
923
958
|
)
|
|
924
959
|
|
|
925
960
|
for (const chunk of toolResultChunks) {
|
|
926
|
-
yield chunk
|
|
961
|
+
yield* this.pipeThroughMiddleware(chunk)
|
|
927
962
|
}
|
|
928
963
|
|
|
929
964
|
// Refresh tools if lazy tools were discovered in this batch
|
|
@@ -1057,7 +1092,7 @@ class TextEngine<
|
|
|
1057
1092
|
needsApproval: true,
|
|
1058
1093
|
},
|
|
1059
1094
|
},
|
|
1060
|
-
})
|
|
1095
|
+
} as StreamChunk)
|
|
1061
1096
|
}
|
|
1062
1097
|
|
|
1063
1098
|
return chunks
|
|
@@ -1080,7 +1115,7 @@ class TextEngine<
|
|
|
1080
1115
|
toolName: clientTool.toolName,
|
|
1081
1116
|
input: clientTool.input,
|
|
1082
1117
|
},
|
|
1083
|
-
})
|
|
1118
|
+
} as StreamChunk)
|
|
1084
1119
|
}
|
|
1085
1120
|
|
|
1086
1121
|
return chunks
|
|
@@ -1105,7 +1140,7 @@ class TextEngine<
|
|
|
1105
1140
|
model: finishEvent.model,
|
|
1106
1141
|
toolCallId: result.toolCallId,
|
|
1107
1142
|
toolName: result.toolName,
|
|
1108
|
-
})
|
|
1143
|
+
} as StreamChunk)
|
|
1109
1144
|
|
|
1110
1145
|
const args = argsMap.get(result.toolCallId) ?? '{}'
|
|
1111
1146
|
chunks.push({
|
|
@@ -1115,7 +1150,7 @@ class TextEngine<
|
|
|
1115
1150
|
toolCallId: result.toolCallId,
|
|
1116
1151
|
delta: args,
|
|
1117
1152
|
args,
|
|
1118
|
-
})
|
|
1153
|
+
} as StreamChunk)
|
|
1119
1154
|
}
|
|
1120
1155
|
|
|
1121
1156
|
chunks.push({
|
|
@@ -1123,9 +1158,21 @@ class TextEngine<
|
|
|
1123
1158
|
timestamp: Date.now(),
|
|
1124
1159
|
model: finishEvent.model,
|
|
1125
1160
|
toolCallId: result.toolCallId,
|
|
1161
|
+
toolCallName: result.toolName,
|
|
1126
1162
|
toolName: result.toolName,
|
|
1127
1163
|
result: content,
|
|
1128
|
-
})
|
|
1164
|
+
} as StreamChunk)
|
|
1165
|
+
|
|
1166
|
+
// AG-UI spec TOOL_CALL_RESULT event
|
|
1167
|
+
chunks.push({
|
|
1168
|
+
type: 'TOOL_CALL_RESULT',
|
|
1169
|
+
timestamp: Date.now(),
|
|
1170
|
+
model: finishEvent.model,
|
|
1171
|
+
messageId: this.createId('tool-result'),
|
|
1172
|
+
toolCallId: result.toolCallId,
|
|
1173
|
+
content,
|
|
1174
|
+
role: 'tool',
|
|
1175
|
+
} as StreamChunk)
|
|
1129
1176
|
|
|
1130
1177
|
this.messages = [
|
|
1131
1178
|
...this.messages,
|
|
@@ -1186,10 +1233,11 @@ class TextEngine<
|
|
|
1186
1233
|
return {
|
|
1187
1234
|
type: 'RUN_FINISHED',
|
|
1188
1235
|
runId: this.createId('pending'),
|
|
1236
|
+
threadId: this.threadId,
|
|
1189
1237
|
model: this.params.model,
|
|
1190
1238
|
timestamp: Date.now(),
|
|
1191
1239
|
finishReason: 'tool_calls',
|
|
1192
|
-
}
|
|
1240
|
+
} as RunFinishedEvent
|
|
1193
1241
|
}
|
|
1194
1242
|
|
|
1195
1243
|
private shouldContinue(): boolean {
|
|
@@ -1256,9 +1304,26 @@ class TextEngine<
|
|
|
1256
1304
|
this.toolPhase = phase
|
|
1257
1305
|
}
|
|
1258
1306
|
|
|
1307
|
+
/**
|
|
1308
|
+
* Pipe a single chunk through the middleware pipeline (strip-to-spec, devtools, etc.)
|
|
1309
|
+
* and yield all resulting output chunks.
|
|
1310
|
+
*/
|
|
1311
|
+
private async *pipeThroughMiddleware(
|
|
1312
|
+
chunk: StreamChunk,
|
|
1313
|
+
): AsyncGenerator<StreamChunk, void, void> {
|
|
1314
|
+
const outputChunks = await this.middlewareRunner.runOnChunk(
|
|
1315
|
+
this.middlewareCtx,
|
|
1316
|
+
chunk,
|
|
1317
|
+
)
|
|
1318
|
+
for (const outputChunk of outputChunks) {
|
|
1319
|
+
yield outputChunk
|
|
1320
|
+
this.middlewareCtx.chunkIndex++
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1259
1324
|
/**
|
|
1260
1325
|
* Drain an executeToolCalls async generator, yielding any CustomEvent chunks
|
|
1261
|
-
* and returning the final ExecuteToolCallsResult.
|
|
1326
|
+
* through the middleware pipeline and returning the final ExecuteToolCallsResult.
|
|
1262
1327
|
*/
|
|
1263
1328
|
private async *drainToolCallGenerator(
|
|
1264
1329
|
generator: AsyncGenerator<
|
|
@@ -1281,7 +1346,7 @@ class TextEngine<
|
|
|
1281
1346
|
> {
|
|
1282
1347
|
let next = await generator.next()
|
|
1283
1348
|
while (!next.done) {
|
|
1284
|
-
yield next.value
|
|
1349
|
+
yield* this.pipeThroughMiddleware(next.value)
|
|
1285
1350
|
next = await generator.next()
|
|
1286
1351
|
}
|
|
1287
1352
|
return next.value
|
|
@@ -1297,7 +1362,7 @@ class TextEngine<
|
|
|
1297
1362
|
model: this.params.model,
|
|
1298
1363
|
name: eventName,
|
|
1299
1364
|
value,
|
|
1300
|
-
}
|
|
1365
|
+
} as CustomEvent
|
|
1301
1366
|
}
|
|
1302
1367
|
|
|
1303
1368
|
private createId(prefix: string): string {
|
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
|
|
18
18
|
/** Check if a middleware should be skipped for instrumentation events. */
|
|
19
19
|
function shouldSkipInstrumentation(mw: ChatMiddleware): boolean {
|
|
20
|
-
return mw.name === 'devtools'
|
|
20
|
+
return mw.name === 'devtools' || mw.name === 'strip-to-spec'
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/** Build the base context for middleware instrumentation events. */
|
|
@@ -132,6 +132,8 @@ export class MiddlewareRunner {
|
|
|
132
132
|
|
|
133
133
|
const nextChunks: Array<StreamChunk> = []
|
|
134
134
|
for (const c of chunks) {
|
|
135
|
+
// Cast: @ag-ui/core Zod passthrough types prevent direct `.type` access
|
|
136
|
+
const chunkType = (c as StreamChunk & { type: string }).type
|
|
135
137
|
const result = await mw.onChunk(ctx, c)
|
|
136
138
|
if (result === null) {
|
|
137
139
|
// Drop this chunk
|
|
@@ -139,7 +141,7 @@ export class MiddlewareRunner {
|
|
|
139
141
|
aiEventClient.emit('middleware:chunk:transformed', {
|
|
140
142
|
...instrumentCtx(ctx),
|
|
141
143
|
middlewareName: mw.name || 'unnamed',
|
|
142
|
-
originalChunkType:
|
|
144
|
+
originalChunkType: chunkType,
|
|
143
145
|
resultCount: 0,
|
|
144
146
|
wasDropped: true,
|
|
145
147
|
})
|
|
@@ -155,7 +157,7 @@ export class MiddlewareRunner {
|
|
|
155
157
|
aiEventClient.emit('middleware:chunk:transformed', {
|
|
156
158
|
...instrumentCtx(ctx),
|
|
157
159
|
middlewareName: mw.name || 'unnamed',
|
|
158
|
-
originalChunkType:
|
|
160
|
+
originalChunkType: chunkType,
|
|
159
161
|
resultCount: result.length,
|
|
160
162
|
wasDropped: false,
|
|
161
163
|
})
|
|
@@ -167,7 +169,7 @@ export class MiddlewareRunner {
|
|
|
167
169
|
aiEventClient.emit('middleware:chunk:transformed', {
|
|
168
170
|
...instrumentCtx(ctx),
|
|
169
171
|
middlewareName: mw.name || 'unnamed',
|
|
170
|
-
originalChunkType:
|
|
172
|
+
originalChunkType: chunkType,
|
|
171
173
|
resultCount: 1,
|
|
172
174
|
wasDropped: false,
|
|
173
175
|
})
|
|
@@ -431,7 +431,7 @@ export class StreamProcessor {
|
|
|
431
431
|
*
|
|
432
432
|
* Central dispatch for all AG-UI events. Each event type maps to a specific
|
|
433
433
|
* handler. Events not listed in the switch are intentionally ignored
|
|
434
|
-
* (
|
|
434
|
+
* (STEP_STARTED, STATE_SNAPSHOT, STATE_DELTA).
|
|
435
435
|
*
|
|
436
436
|
* @see docs/chat-architecture.md#adapter-contract — Expected event types and ordering
|
|
437
437
|
*/
|
|
@@ -445,54 +445,100 @@ export class StreamProcessor {
|
|
|
445
445
|
})
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
-
|
|
448
|
+
// Cast needed: @ag-ui/core Zod passthrough types add `& { [k: string]: unknown }`
|
|
449
|
+
// which prevents TypeScript from narrowing the `type` discriminant in switch.
|
|
450
|
+
const c = chunk as StreamChunk & { type: string }
|
|
451
|
+
switch (c.type) {
|
|
449
452
|
// AG-UI Events
|
|
450
453
|
case 'TEXT_MESSAGE_START':
|
|
451
|
-
this.handleTextMessageStartEvent(
|
|
454
|
+
this.handleTextMessageStartEvent(
|
|
455
|
+
chunk as Extract<StreamChunk, { type: 'TEXT_MESSAGE_START' }>,
|
|
456
|
+
)
|
|
452
457
|
break
|
|
453
458
|
|
|
454
459
|
case 'TEXT_MESSAGE_CONTENT':
|
|
455
|
-
this.handleTextMessageContentEvent(
|
|
460
|
+
this.handleTextMessageContentEvent(
|
|
461
|
+
chunk as Extract<StreamChunk, { type: 'TEXT_MESSAGE_CONTENT' }>,
|
|
462
|
+
)
|
|
456
463
|
break
|
|
457
464
|
|
|
458
465
|
case 'TEXT_MESSAGE_END':
|
|
459
|
-
this.handleTextMessageEndEvent(
|
|
466
|
+
this.handleTextMessageEndEvent(
|
|
467
|
+
chunk as Extract<StreamChunk, { type: 'TEXT_MESSAGE_END' }>,
|
|
468
|
+
)
|
|
460
469
|
break
|
|
461
470
|
|
|
462
471
|
case 'TOOL_CALL_START':
|
|
463
|
-
this.handleToolCallStartEvent(
|
|
472
|
+
this.handleToolCallStartEvent(
|
|
473
|
+
chunk as Extract<StreamChunk, { type: 'TOOL_CALL_START' }>,
|
|
474
|
+
)
|
|
464
475
|
break
|
|
465
476
|
|
|
466
477
|
case 'TOOL_CALL_ARGS':
|
|
467
|
-
this.handleToolCallArgsEvent(
|
|
478
|
+
this.handleToolCallArgsEvent(
|
|
479
|
+
chunk as Extract<StreamChunk, { type: 'TOOL_CALL_ARGS' }>,
|
|
480
|
+
)
|
|
468
481
|
break
|
|
469
482
|
|
|
470
483
|
case 'TOOL_CALL_END':
|
|
471
|
-
this.handleToolCallEndEvent(
|
|
484
|
+
this.handleToolCallEndEvent(
|
|
485
|
+
chunk as Extract<StreamChunk, { type: 'TOOL_CALL_END' }>,
|
|
486
|
+
)
|
|
472
487
|
break
|
|
473
488
|
|
|
474
489
|
case 'RUN_FINISHED':
|
|
475
|
-
this.handleRunFinishedEvent(
|
|
490
|
+
this.handleRunFinishedEvent(
|
|
491
|
+
chunk as Extract<StreamChunk, { type: 'RUN_FINISHED' }>,
|
|
492
|
+
)
|
|
476
493
|
break
|
|
477
494
|
|
|
478
495
|
case 'RUN_ERROR':
|
|
479
|
-
this.handleRunErrorEvent(
|
|
496
|
+
this.handleRunErrorEvent(
|
|
497
|
+
chunk as Extract<StreamChunk, { type: 'RUN_ERROR' }>,
|
|
498
|
+
)
|
|
480
499
|
break
|
|
481
500
|
|
|
482
501
|
case 'STEP_FINISHED':
|
|
483
|
-
this.handleStepFinishedEvent(
|
|
502
|
+
this.handleStepFinishedEvent(
|
|
503
|
+
chunk as Extract<StreamChunk, { type: 'STEP_FINISHED' }>,
|
|
504
|
+
)
|
|
484
505
|
break
|
|
485
506
|
|
|
486
507
|
case 'MESSAGES_SNAPSHOT':
|
|
487
|
-
this.handleMessagesSnapshotEvent(
|
|
508
|
+
this.handleMessagesSnapshotEvent(
|
|
509
|
+
chunk as Extract<StreamChunk, { type: 'MESSAGES_SNAPSHOT' }>,
|
|
510
|
+
)
|
|
488
511
|
break
|
|
489
512
|
|
|
490
513
|
case 'CUSTOM':
|
|
491
|
-
this.handleCustomEvent(
|
|
514
|
+
this.handleCustomEvent(
|
|
515
|
+
chunk as Extract<StreamChunk, { type: 'CUSTOM' }>,
|
|
516
|
+
)
|
|
492
517
|
break
|
|
493
518
|
|
|
494
519
|
case 'RUN_STARTED':
|
|
495
|
-
this.handleRunStartedEvent(
|
|
520
|
+
this.handleRunStartedEvent(
|
|
521
|
+
chunk as Extract<StreamChunk, { type: 'RUN_STARTED' }>,
|
|
522
|
+
)
|
|
523
|
+
break
|
|
524
|
+
|
|
525
|
+
case 'REASONING_START':
|
|
526
|
+
case 'REASONING_MESSAGE_START':
|
|
527
|
+
case 'REASONING_MESSAGE_END':
|
|
528
|
+
case 'REASONING_END':
|
|
529
|
+
// No special handling needed
|
|
530
|
+
break
|
|
531
|
+
|
|
532
|
+
case 'REASONING_MESSAGE_CONTENT':
|
|
533
|
+
this.handleReasoningMessageContentEvent(
|
|
534
|
+
chunk as Extract<StreamChunk, { type: 'REASONING_MESSAGE_CONTENT' }>,
|
|
535
|
+
)
|
|
536
|
+
break
|
|
537
|
+
|
|
538
|
+
case 'TOOL_CALL_RESULT':
|
|
539
|
+
this.handleToolCallResultEvent(
|
|
540
|
+
chunk as Extract<StreamChunk, { type: 'TOOL_CALL_RESULT' }>,
|
|
541
|
+
)
|
|
496
542
|
break
|
|
497
543
|
|
|
498
544
|
default:
|
|
@@ -519,6 +565,7 @@ export class StreamProcessor {
|
|
|
519
565
|
currentSegmentText: '',
|
|
520
566
|
lastEmittedText: '',
|
|
521
567
|
thinkingContent: '',
|
|
568
|
+
hasSeenReasoningEvents: false,
|
|
522
569
|
toolCalls: new Map(),
|
|
523
570
|
toolCallOrder: [],
|
|
524
571
|
hasToolCallsSinceTextStart: false,
|
|
@@ -632,11 +679,11 @@ export class StreamProcessor {
|
|
|
632
679
|
): void {
|
|
633
680
|
const { messageId, role } = chunk
|
|
634
681
|
|
|
635
|
-
// Map 'tool'
|
|
636
|
-
// (UIMessage doesn't support 'tool' role, and lookups like
|
|
682
|
+
// Map 'tool' and 'developer' roles to 'assistant' for both UIMessage and MessageStreamState
|
|
683
|
+
// (UIMessage doesn't support 'tool'/'developer' role, and lookups like
|
|
637
684
|
// getActiveAssistantMessageId() check state.role === 'assistant')
|
|
638
685
|
const uiRole: 'system' | 'user' | 'assistant' =
|
|
639
|
-
role === '
|
|
686
|
+
role === 'user' || role === 'system' ? role : 'assistant'
|
|
640
687
|
|
|
641
688
|
// Case 1: A manual message was created via startAssistantMessage()
|
|
642
689
|
if (this.pendingManualMessageId) {
|
|
@@ -739,7 +786,8 @@ export class StreamProcessor {
|
|
|
739
786
|
chunk: Extract<StreamChunk, { type: 'MESSAGES_SNAPSHOT' }>,
|
|
740
787
|
): void {
|
|
741
788
|
this.resetStreamState()
|
|
742
|
-
|
|
789
|
+
// AG-UI Message[] is compatible with UIMessage[] at runtime
|
|
790
|
+
this.messages = [...chunk.messages] as unknown as Array<UIMessage>
|
|
743
791
|
this.emitMessagesChange()
|
|
744
792
|
}
|
|
745
793
|
|
|
@@ -849,9 +897,11 @@ export class StreamProcessor {
|
|
|
849
897
|
// New tool call starting
|
|
850
898
|
const initialState: ToolCallState = 'awaiting-input'
|
|
851
899
|
|
|
900
|
+
const toolName = chunk.toolCallName
|
|
901
|
+
|
|
852
902
|
const newToolCall: InternalToolCallState = {
|
|
853
903
|
id: chunk.toolCallId,
|
|
854
|
-
name:
|
|
904
|
+
name: toolName,
|
|
855
905
|
arguments: '',
|
|
856
906
|
state: initialState,
|
|
857
907
|
parsedArguments: undefined,
|
|
@@ -867,7 +917,7 @@ export class StreamProcessor {
|
|
|
867
917
|
// Update UIMessage
|
|
868
918
|
this.messages = updateToolCallPart(this.messages, messageId, {
|
|
869
919
|
id: chunk.toolCallId,
|
|
870
|
-
name:
|
|
920
|
+
name: toolName,
|
|
871
921
|
arguments: '',
|
|
872
922
|
state: initialState,
|
|
873
923
|
})
|
|
@@ -1012,6 +1062,44 @@ export class StreamProcessor {
|
|
|
1012
1062
|
}
|
|
1013
1063
|
}
|
|
1014
1064
|
|
|
1065
|
+
/**
|
|
1066
|
+
* Handle TOOL_CALL_RESULT event (AG-UI spec).
|
|
1067
|
+
*
|
|
1068
|
+
* Creates a tool-result part and updates the tool-call output field,
|
|
1069
|
+
* mirroring the logic from TOOL_CALL_END when it carries a result.
|
|
1070
|
+
* This is the spec-compliant path for delivering tool results to the client.
|
|
1071
|
+
*/
|
|
1072
|
+
private handleToolCallResultEvent(
|
|
1073
|
+
chunk: Extract<StreamChunk, { type: 'TOOL_CALL_RESULT' }>,
|
|
1074
|
+
): void {
|
|
1075
|
+
const messageId = this.toolCallToMessage.get(chunk.toolCallId)
|
|
1076
|
+
if (!messageId) return
|
|
1077
|
+
|
|
1078
|
+
// Step 1: Update the tool-call part's output field
|
|
1079
|
+
let output: unknown
|
|
1080
|
+
try {
|
|
1081
|
+
output = JSON.parse(chunk.content)
|
|
1082
|
+
} catch {
|
|
1083
|
+
output = chunk.content
|
|
1084
|
+
}
|
|
1085
|
+
this.messages = updateToolCallWithOutput(
|
|
1086
|
+
this.messages,
|
|
1087
|
+
chunk.toolCallId,
|
|
1088
|
+
output,
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
// Step 2: Create/update the tool-result part
|
|
1092
|
+
const resultState: ToolResultState = 'complete'
|
|
1093
|
+
this.messages = updateToolResultPart(
|
|
1094
|
+
this.messages,
|
|
1095
|
+
messageId,
|
|
1096
|
+
chunk.toolCallId,
|
|
1097
|
+
chunk.content,
|
|
1098
|
+
resultState,
|
|
1099
|
+
)
|
|
1100
|
+
this.emitMessagesChange()
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1015
1103
|
/**
|
|
1016
1104
|
* Handle RUN_STARTED event.
|
|
1017
1105
|
*
|
|
@@ -1037,7 +1125,7 @@ export class StreamProcessor {
|
|
|
1037
1125
|
private handleRunFinishedEvent(
|
|
1038
1126
|
chunk: Extract<StreamChunk, { type: 'RUN_FINISHED' }>,
|
|
1039
1127
|
): void {
|
|
1040
|
-
this.finishReason = chunk.finishReason
|
|
1128
|
+
this.finishReason = chunk.finishReason ?? null
|
|
1041
1129
|
this.activeRuns.delete(chunk.runId)
|
|
1042
1130
|
|
|
1043
1131
|
if (this.activeRuns.size === 0) {
|
|
@@ -1054,13 +1142,17 @@ export class StreamProcessor {
|
|
|
1054
1142
|
chunk: Extract<StreamChunk, { type: 'RUN_ERROR' }>,
|
|
1055
1143
|
): void {
|
|
1056
1144
|
this.hasError = true
|
|
1057
|
-
|
|
1058
|
-
|
|
1145
|
+
const runId = (chunk as any).runId as string | undefined
|
|
1146
|
+
if (runId) {
|
|
1147
|
+
this.activeRuns.delete(runId)
|
|
1059
1148
|
} else {
|
|
1060
1149
|
this.activeRuns.clear()
|
|
1061
1150
|
}
|
|
1062
1151
|
this.ensureAssistantMessage()
|
|
1063
|
-
|
|
1152
|
+
// Prefer spec field `message`; fall back to deprecated `error.message`
|
|
1153
|
+
const errorMessage =
|
|
1154
|
+
chunk.message || chunk.error?.message || 'An error occurred'
|
|
1155
|
+
this.events.onError?.(new Error(errorMessage))
|
|
1064
1156
|
}
|
|
1065
1157
|
|
|
1066
1158
|
/**
|
|
@@ -1078,6 +1170,14 @@ export class StreamProcessor {
|
|
|
1078
1170
|
this.getActiveAssistantMessageId() ?? undefined,
|
|
1079
1171
|
)
|
|
1080
1172
|
|
|
1173
|
+
// During the transition period, adapters emit BOTH STEP_FINISHED and
|
|
1174
|
+
// REASONING_MESSAGE_CONTENT with the same delta. If we've already processed
|
|
1175
|
+
// REASONING_MESSAGE_CONTENT events for this message, skip the duplicate
|
|
1176
|
+
// thinking content from STEP_FINISHED to avoid doubled content.
|
|
1177
|
+
if (state.hasSeenReasoningEvents) {
|
|
1178
|
+
return
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1081
1181
|
const previous = state.thinkingContent
|
|
1082
1182
|
let nextThinking = previous
|
|
1083
1183
|
|
|
@@ -1108,6 +1208,33 @@ export class StreamProcessor {
|
|
|
1108
1208
|
this.events.onThinkingUpdate?.(messageId, state.thinkingContent)
|
|
1109
1209
|
}
|
|
1110
1210
|
|
|
1211
|
+
/**
|
|
1212
|
+
* Handle REASONING_MESSAGE_CONTENT event (AG-UI reasoning protocol).
|
|
1213
|
+
*
|
|
1214
|
+
* Accumulates reasoning delta into thinkingContent and updates the ThinkingPart
|
|
1215
|
+
* in the UIMessage.
|
|
1216
|
+
*/
|
|
1217
|
+
private handleReasoningMessageContentEvent(
|
|
1218
|
+
chunk: Extract<StreamChunk, { type: 'REASONING_MESSAGE_CONTENT' }>,
|
|
1219
|
+
): void {
|
|
1220
|
+
const { messageId, state } = this.ensureAssistantMessage(
|
|
1221
|
+
this.getActiveAssistantMessageId() ?? undefined,
|
|
1222
|
+
)
|
|
1223
|
+
|
|
1224
|
+
state.hasSeenReasoningEvents = true
|
|
1225
|
+
const delta = chunk.delta || ''
|
|
1226
|
+
state.thinkingContent = state.thinkingContent + delta
|
|
1227
|
+
|
|
1228
|
+
this.messages = updateThinkingPart(
|
|
1229
|
+
this.messages,
|
|
1230
|
+
messageId,
|
|
1231
|
+
state.thinkingContent,
|
|
1232
|
+
)
|
|
1233
|
+
this.emitMessagesChange()
|
|
1234
|
+
|
|
1235
|
+
this.events.onThinkingUpdate?.(messageId, state.thinkingContent)
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1111
1238
|
/**
|
|
1112
1239
|
* Handle CUSTOM event.
|
|
1113
1240
|
*
|
|
@@ -1178,7 +1305,7 @@ export class StreamProcessor {
|
|
|
1178
1305
|
if (this.events.onCustomEvent) {
|
|
1179
1306
|
const toolCallId =
|
|
1180
1307
|
chunk.value && typeof chunk.value === 'object'
|
|
1181
|
-
?
|
|
1308
|
+
? chunk.value.toolCallId
|
|
1182
1309
|
: undefined
|
|
1183
1310
|
this.events.onCustomEvent(chunk.name, chunk.value, { toolCallId })
|
|
1184
1311
|
}
|
|
@@ -57,6 +57,7 @@ export interface MessageStreamState {
|
|
|
57
57
|
currentSegmentText: string
|
|
58
58
|
lastEmittedText: string
|
|
59
59
|
thinkingContent: string
|
|
60
|
+
hasSeenReasoningEvents: boolean
|
|
60
61
|
toolCalls: Map<string, InternalToolCallState>
|
|
61
62
|
toolCallOrder: Array<string>
|
|
62
63
|
hasToolCallsSinceTextStart: boolean
|
|
@@ -93,11 +93,12 @@ export class ToolCallManager {
|
|
|
93
93
|
*/
|
|
94
94
|
addToolCallStartEvent(event: ToolCallStartEvent): void {
|
|
95
95
|
const index = event.index ?? this.toolCallsMap.size
|
|
96
|
+
const name = event.toolCallName
|
|
96
97
|
this.toolCallsMap.set(index, {
|
|
97
98
|
id: event.toolCallId,
|
|
98
99
|
type: 'function',
|
|
99
100
|
function: {
|
|
100
|
-
name
|
|
101
|
+
name,
|
|
101
102
|
arguments: '',
|
|
102
103
|
},
|
|
103
104
|
...(event.providerMetadata && {
|
|
@@ -237,11 +238,12 @@ export class ToolCallManager {
|
|
|
237
238
|
yield {
|
|
238
239
|
type: 'TOOL_CALL_END',
|
|
239
240
|
toolCallId: toolCall.id,
|
|
241
|
+
toolCallName: toolCall.function.name,
|
|
240
242
|
toolName: toolCall.function.name,
|
|
241
243
|
model: finishEvent.model,
|
|
242
244
|
timestamp: Date.now(),
|
|
243
245
|
result: toolResultContent,
|
|
244
|
-
}
|
|
246
|
+
} as ToolCallEndEvent
|
|
245
247
|
|
|
246
248
|
// Add tool result message
|
|
247
249
|
toolResults.push({
|