@tanstack/ai-devtools-core 0.3.9 → 0.3.12

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.
@@ -1,6 +1,6 @@
1
1
  import { batch, createContext, onCleanup, onMount, useContext } from 'solid-js'
2
2
  import { createStore, produce } from 'solid-js/store'
3
- import { aiEventClient } from '@tanstack/ai/event-client'
3
+ import { aiEventClient } from '@tanstack/ai-event-client'
4
4
  import type { ContentPartSource } from '@tanstack/ai'
5
5
  import type { ParentComponent } from 'solid-js'
6
6
 
@@ -60,6 +60,8 @@ export interface Message {
60
60
  thinkingContent?: string
61
61
  /** Source of the message: 'client' for aggregated client-side data, 'server' for individual server chunks */
62
62
  source?: 'client' | 'server'
63
+ /** The requestId this message belongs to (for scoping usage calculations) */
64
+ requestId?: string
63
65
  }
64
66
 
65
67
  /**
@@ -100,6 +102,38 @@ export interface Chunk {
100
102
  isClientTool?: boolean
101
103
  }
102
104
 
105
+ export interface MiddlewareEvent {
106
+ id: string
107
+ middlewareName: string
108
+ hookName: string
109
+ timestamp: number
110
+ duration?: number
111
+ hasTransform: boolean
112
+ configChanges?: Record<string, unknown>
113
+ originalChunkType?: string
114
+ resultCount?: number
115
+ wasDropped?: boolean
116
+ }
117
+
118
+ export interface Iteration {
119
+ /** The requestId this iteration belongs to (unique per chat() call) */
120
+ requestId?: string
121
+ index: number
122
+ messageId: string
123
+ startedAt: number
124
+ completedAt?: number
125
+ model?: string
126
+ provider?: string
127
+ systemPrompts?: Array<string>
128
+ toolNames?: Array<string>
129
+ options?: Record<string, unknown>
130
+ modelOptions?: Record<string, unknown>
131
+ finishReason?: string
132
+ usage?: TokenUsage
133
+ middlewareEvents: Array<MiddlewareEvent>
134
+ messageIds: Array<string>
135
+ }
136
+
103
137
  export interface SummarizeOperation {
104
138
  id: string
105
139
  model: string
@@ -129,6 +163,7 @@ export interface Conversation {
129
163
  startedAt: number
130
164
  completedAt?: number
131
165
  usage?: TokenUsage
166
+ iterations: Array<Iteration>
132
167
  iterationCount?: number
133
168
  toolNames?: Array<string>
134
169
  options?: Record<string, unknown>
@@ -178,6 +213,8 @@ export const AIProvider: ParentComponent = (props) => {
178
213
 
179
214
  const streamToConversation = new Map<string, string>()
180
215
  const requestToConversation = new Map<string, string>()
216
+ /** Track max cumulative usage per requestId per conversation for correct totals */
217
+ const requestUsageByConversation = new Map<string, Map<string, TokenUsage>>()
181
218
 
182
219
  // Batching system for high-frequency chunk updates with consolidated chunk merging
183
220
  // Stores: conversationId -> { chunks to merge, totalNewChunks count }
@@ -366,6 +403,7 @@ export const AIProvider: ParentComponent = (props) => {
366
403
  label,
367
404
  messages: [],
368
405
  chunks: [],
406
+ iterations: [],
369
407
  imageEvents: [],
370
408
  speechEvents: [],
371
409
  transcriptionEvents: [],
@@ -441,6 +479,7 @@ export const AIProvider: ParentComponent = (props) => {
441
479
  conversationId: string,
442
480
  messageId: string | undefined,
443
481
  cumulativeUsage: TokenUsage,
482
+ requestId?: string,
444
483
  ): void {
445
484
  const conv = state.conversations[conversationId]
446
485
  if (!conv) return
@@ -467,10 +506,12 @@ export const AIProvider: ParentComponent = (props) => {
467
506
 
468
507
  if (targetMessageIndex === -1) return
469
508
 
470
- // Sum up usage from all previous assistant messages
509
+ // Sum up usage from previous assistant messages in the SAME request only.
510
+ // Cumulative usage is per-request, so mixing requests gives wrong deltas.
471
511
  for (let i = 0; i < targetMessageIndex; i++) {
472
512
  const msg = conv.messages[i]
473
513
  if (msg?.role === 'assistant' && msg.usage) {
514
+ if (requestId && msg.requestId !== requestId) continue
474
515
  previousPromptTokens += msg.usage.promptTokens
475
516
  previousCompletionTokens += msg.usage.completionTokens
476
517
  }
@@ -501,12 +542,50 @@ export const AIProvider: ParentComponent = (props) => {
501
542
  )
502
543
  }
503
544
 
545
+ /**
546
+ * Update conversation-level usage by tracking max cumulative per request.
547
+ * Usage events report cumulative totals per-request, so we keep the highest
548
+ * value seen for each requestId and sum across all requests for the total.
549
+ */
550
+ function updateConversationUsage(
551
+ conversationId: string,
552
+ requestId: string | undefined,
553
+ usage: TokenUsage,
554
+ ): void {
555
+ if (!state.conversations[conversationId]) return
556
+ const key = requestId || '__default__'
557
+ let requestMap = requestUsageByConversation.get(conversationId)
558
+ if (!requestMap) {
559
+ requestMap = new Map()
560
+ requestUsageByConversation.set(conversationId, requestMap)
561
+ }
562
+ const existing = requestMap.get(key)
563
+ if (!existing || usage.totalTokens > existing.totalTokens) {
564
+ requestMap.set(key, usage)
565
+ }
566
+ // Sum across all requests
567
+ let prompt = 0
568
+ let completion = 0
569
+ for (const v of requestMap.values()) {
570
+ prompt += v.promptTokens
571
+ completion += v.completionTokens
572
+ }
573
+ updateConversation(conversationId, {
574
+ usage: {
575
+ promptTokens: prompt,
576
+ completionTokens: completion,
577
+ totalTokens: prompt + completion,
578
+ },
579
+ })
580
+ }
581
+
504
582
  // Public actions
505
583
  function clearAllConversations() {
506
584
  setState('conversations', {})
507
585
  setState('activeConversationId', null)
508
586
  streamToConversation.clear()
509
587
  requestToConversation.clear()
588
+ requestUsageByConversation.clear()
510
589
  pendingConversationChunks.clear()
511
590
  pendingMessageChunks.clear()
512
591
  }
@@ -671,8 +750,15 @@ export const AIProvider: ParentComponent = (props) => {
671
750
 
672
751
  cleanupFns.push(
673
752
  aiEventClient.on('text:message:created', (e) => {
674
- const { clientId, streamId, messageId, role, content, timestamp } =
675
- e.payload
753
+ const {
754
+ clientId,
755
+ streamId,
756
+ messageId,
757
+ role,
758
+ content,
759
+ timestamp,
760
+ requestId,
761
+ } = e.payload
676
762
  const conversationId =
677
763
  clientId ||
678
764
  (streamId ? streamToConversation.get(streamId) : undefined)
@@ -781,6 +867,7 @@ export const AIProvider: ParentComponent = (props) => {
781
867
  parts,
782
868
  toolCalls,
783
869
  source,
870
+ requestId,
784
871
  }
785
872
 
786
873
  if (existingIndex >= 0) {
@@ -789,13 +876,46 @@ export const AIProvider: ParentComponent = (props) => {
789
876
  addMessage(conversationId, messagePayload)
790
877
  }
791
878
 
879
+ // Track messageId in the correct iteration (scoped by requestId)
880
+ if (conv.iterations.length > 0) {
881
+ let iterIndex = -1
882
+ if (requestId) {
883
+ // Find the latest iteration for this specific request
884
+ for (let i = conv.iterations.length - 1; i >= 0; i--) {
885
+ if (conv.iterations[i]?.requestId === requestId) {
886
+ iterIndex = i
887
+ break
888
+ }
889
+ }
890
+ } else {
891
+ // Fallback: use latest iteration
892
+ iterIndex = conv.iterations.length - 1
893
+ }
894
+ if (iterIndex >= 0) {
895
+ const iter = conv.iterations[iterIndex]
896
+ if (iter && !iter.messageIds.includes(messageId)) {
897
+ setState(
898
+ 'conversations',
899
+ conversationId,
900
+ 'iterations',
901
+ iterIndex,
902
+ 'messageIds',
903
+ produce((arr: Array<string>) => {
904
+ arr.push(messageId)
905
+ }),
906
+ )
907
+ }
908
+ }
909
+ }
910
+
792
911
  updateConversation(conversationId, { status: 'active', hasChat: true })
793
912
  }),
794
913
  )
795
914
 
796
915
  cleanupFns.push(
797
916
  aiEventClient.on('text:message:user', (e) => {
798
- const { clientId, streamId, messageId, content, timestamp } = e.payload
917
+ const { clientId, streamId, messageId, content, timestamp, requestId } =
918
+ e.payload
799
919
  const conversationId =
800
920
  clientId ||
801
921
  (streamId ? streamToConversation.get(streamId) : undefined)
@@ -822,6 +942,7 @@ export const AIProvider: ParentComponent = (props) => {
822
942
  content,
823
943
  timestamp,
824
944
  source,
945
+ requestId,
825
946
  })
826
947
  }),
827
948
  )
@@ -1312,17 +1433,6 @@ export const AIProvider: ParentComponent = (props) => {
1312
1433
  const conv = state.conversations[conversationId]
1313
1434
  if (conv?.type === 'client') {
1314
1435
  addChunkToMessage(conversationId, chunk)
1315
-
1316
- if (e.payload.messageId) {
1317
- const messageIndex = conv.messages.findIndex(
1318
- (msg) => msg.id === e.payload.messageId,
1319
- )
1320
- if (messageIndex !== -1) {
1321
- updateMessage(conversationId, messageIndex, {
1322
- thinkingContent: e.payload.content,
1323
- })
1324
- }
1325
- }
1326
1436
  } else {
1327
1437
  ensureMessageForChunk(
1328
1438
  conversationId,
@@ -1331,6 +1441,18 @@ export const AIProvider: ParentComponent = (props) => {
1331
1441
  )
1332
1442
  addChunk(conversationId, chunk)
1333
1443
  }
1444
+
1445
+ // Update thinkingContent on the message for all conversation types
1446
+ if (e.payload.messageId && conv) {
1447
+ const messageIndex = conv.messages.findIndex(
1448
+ (msg) => msg.id === e.payload.messageId,
1449
+ )
1450
+ if (messageIndex !== -1) {
1451
+ updateMessage(conversationId, messageIndex, {
1452
+ thinkingContent: e.payload.content,
1453
+ })
1454
+ }
1455
+ }
1334
1456
  }),
1335
1457
  )
1336
1458
 
@@ -1350,11 +1472,16 @@ export const AIProvider: ParentComponent = (props) => {
1350
1472
  }
1351
1473
 
1352
1474
  if (e.payload.usage) {
1353
- updateConversation(conversationId, { usage: e.payload.usage })
1475
+ updateConversationUsage(
1476
+ conversationId,
1477
+ e.payload.requestId,
1478
+ e.payload.usage,
1479
+ )
1354
1480
  updateMessageUsage(
1355
1481
  conversationId,
1356
1482
  e.payload.messageId,
1357
1483
  e.payload.usage,
1484
+ e.payload.requestId,
1358
1485
  )
1359
1486
  }
1360
1487
 
@@ -1370,6 +1497,41 @@ export const AIProvider: ParentComponent = (props) => {
1370
1497
  addChunk(conversationId, chunk)
1371
1498
  }
1372
1499
 
1500
+ // Mark the current iteration as completed when the LLM finishes generating.
1501
+ // This is critical for iterations that end with tool_calls — the
1502
+ // text:iteration:completed event only fires when the NEXT iteration starts,
1503
+ // so without this the iteration appears stuck in "streaming" during tool execution.
1504
+ if (e.payload.finishReason) {
1505
+ const convForIter = state.conversations[conversationId]
1506
+ if (convForIter) {
1507
+ for (let i = convForIter.iterations.length - 1; i >= 0; i--) {
1508
+ const iter = convForIter.iterations[i]
1509
+ const msgId = e.payload.messageId
1510
+ if (
1511
+ iter &&
1512
+ !iter.completedAt &&
1513
+ msgId &&
1514
+ (iter.messageId === msgId || iter.messageIds.includes(msgId))
1515
+ ) {
1516
+ const iterIdx = i
1517
+ setState(
1518
+ 'conversations',
1519
+ conversationId,
1520
+ 'iterations',
1521
+ iterIdx,
1522
+ produce((it: Iteration) => {
1523
+ it.completedAt = e.payload.timestamp
1524
+ if (!it.finishReason)
1525
+ it.finishReason = e.payload.finishReason || undefined
1526
+ if (e.payload.usage && !it.usage) it.usage = e.payload.usage
1527
+ }),
1528
+ )
1529
+ break
1530
+ }
1531
+ }
1532
+ }
1533
+ }
1534
+
1373
1535
  updateConversation(conversationId, {
1374
1536
  status: 'completed',
1375
1537
  completedAt: e.payload.timestamp,
@@ -1404,6 +1566,37 @@ export const AIProvider: ParentComponent = (props) => {
1404
1566
  addChunk(conversationId, chunk)
1405
1567
  }
1406
1568
 
1569
+ // Mark any active iterations as completed with error
1570
+ const convForError = state.conversations[conversationId]
1571
+ if (convForError) {
1572
+ const errorRequestId = e.payload.requestId
1573
+ const errorMsgId = e.payload.messageId
1574
+ for (let i = convForError.iterations.length - 1; i >= 0; i--) {
1575
+ const iter = convForError.iterations[i]
1576
+ if (iter && !iter.completedAt) {
1577
+ // Scope to matching requestId or messageId when available
1578
+ const matchesRequest =
1579
+ !errorRequestId || iter.requestId === errorRequestId
1580
+ const matchesMessage =
1581
+ !errorMsgId ||
1582
+ iter.messageId === errorMsgId ||
1583
+ iter.messageIds.includes(errorMsgId)
1584
+ if (matchesRequest || matchesMessage) {
1585
+ setState(
1586
+ 'conversations',
1587
+ conversationId,
1588
+ 'iterations',
1589
+ i,
1590
+ produce((it: Iteration) => {
1591
+ it.completedAt = e.payload.timestamp
1592
+ if (!it.finishReason) it.finishReason = 'error'
1593
+ }),
1594
+ )
1595
+ }
1596
+ }
1597
+ }
1598
+ }
1599
+
1407
1600
  updateConversation(conversationId, {
1408
1601
  status: 'error',
1409
1602
  completedAt: e.payload.timestamp,
@@ -1541,16 +1734,47 @@ export const AIProvider: ParentComponent = (props) => {
1541
1734
 
1542
1735
  const conversationId = requestToConversation.get(requestId)
1543
1736
  if (conversationId && state.conversations[conversationId]) {
1544
- const updates: Partial<Conversation> = {
1737
+ updateConversation(conversationId, {
1545
1738
  status: 'completed',
1546
1739
  completedAt: e.payload.timestamp,
1547
- }
1740
+ })
1548
1741
  if (usage) {
1549
- updates.usage = usage
1742
+ updateConversationUsage(conversationId, requestId, usage)
1743
+ updateMessageUsage(
1744
+ conversationId,
1745
+ e.payload.messageId,
1746
+ usage,
1747
+ requestId,
1748
+ )
1550
1749
  }
1551
- updateConversation(conversationId, updates)
1552
- if (usage) {
1553
- updateMessageUsage(conversationId, e.payload.messageId, usage)
1750
+
1751
+ // Failsafe: mark any remaining active iterations FOR THIS REQUEST as completed.
1752
+ // Only scope to this requestId to avoid touching other requests' iterations.
1753
+ const conv = state.conversations[conversationId]
1754
+ for (let i = 0; i < conv.iterations.length; i++) {
1755
+ const iter = conv.iterations[i]
1756
+ if (
1757
+ iter &&
1758
+ !iter.completedAt &&
1759
+ (!requestId || iter.requestId === requestId)
1760
+ ) {
1761
+ const iterIdx = i
1762
+ setState(
1763
+ 'conversations',
1764
+ conversationId,
1765
+ 'iterations',
1766
+ iterIdx,
1767
+ produce((it: Iteration) => {
1768
+ it.completedAt = e.payload.timestamp
1769
+ if (!it.finishReason) {
1770
+ it.finishReason = e.payload.finishReason || 'stop'
1771
+ }
1772
+ if (!it.usage && usage) {
1773
+ it.usage = usage
1774
+ }
1775
+ }),
1776
+ )
1777
+ }
1554
1778
  }
1555
1779
  }
1556
1780
  }),
@@ -1562,9 +1786,245 @@ export const AIProvider: ParentComponent = (props) => {
1562
1786
 
1563
1787
  const conversationId = requestToConversation.get(requestId)
1564
1788
  if (conversationId && state.conversations[conversationId]) {
1565
- updateConversation(conversationId, { usage })
1566
- updateMessageUsage(conversationId, messageId, usage)
1789
+ updateConversationUsage(conversationId, requestId, usage)
1790
+ updateMessageUsage(conversationId, messageId, usage, requestId)
1791
+ }
1792
+ }),
1793
+ )
1794
+
1795
+ // ============= Iteration Events =============
1796
+
1797
+ cleanupFns.push(
1798
+ aiEventClient.on('text:iteration:started', (e) => {
1799
+ const { requestId, streamId, clientId, iteration, messageId } =
1800
+ e.payload
1801
+
1802
+ const conversationId =
1803
+ clientId ||
1804
+ (streamId ? streamToConversation.get(streamId) : undefined) ||
1805
+ requestToConversation.get(requestId)
1806
+ if (!conversationId || !state.conversations[conversationId]) return
1807
+
1808
+ // Failsafe: when a new iteration starts, any previous uncompleted
1809
+ // iterations for the same request must have ended (with tool_calls).
1810
+ // This covers edge cases where text:chunk:done didn't match by messageId.
1811
+ const convForFailsafe = state.conversations[conversationId]
1812
+ for (let i = 0; i < convForFailsafe.iterations.length; i++) {
1813
+ const iter = convForFailsafe.iterations[i]
1814
+ if (iter && !iter.completedAt && iter.requestId === requestId) {
1815
+ setState(
1816
+ 'conversations',
1817
+ conversationId,
1818
+ 'iterations',
1819
+ i,
1820
+ produce((it: Iteration) => {
1821
+ it.completedAt = e.payload.timestamp
1822
+ if (!it.finishReason) it.finishReason = 'tool_calls'
1823
+ }),
1824
+ )
1825
+ }
1826
+ }
1827
+
1828
+ // Guard against duplicate iteration events (e.g. middleware registered twice)
1829
+ const existingConv = state.conversations[conversationId]
1830
+ if (
1831
+ existingConv.iterations.some(
1832
+ (it) => it.index === iteration && it.requestId === requestId,
1833
+ )
1834
+ ) {
1835
+ return
1836
+ }
1837
+
1838
+ const newIteration: Iteration = {
1839
+ requestId,
1840
+ index: iteration,
1841
+ messageId,
1842
+ startedAt: e.payload.timestamp,
1843
+ model: e.payload.model,
1844
+ provider: e.payload.provider,
1845
+ systemPrompts: e.payload.systemPrompts,
1846
+ toolNames: e.payload.toolNames,
1847
+ options: e.payload.options,
1848
+ modelOptions: e.payload.modelOptions,
1849
+ middlewareEvents: [],
1850
+ messageIds: [messageId],
1851
+ }
1852
+
1853
+ setState(
1854
+ 'conversations',
1855
+ conversationId,
1856
+ 'iterations',
1857
+ produce((arr: Array<Iteration>) => {
1858
+ arr.push(newIteration)
1859
+ }),
1860
+ )
1861
+ setState(
1862
+ 'conversations',
1863
+ conversationId,
1864
+ 'iterationCount',
1865
+ iteration + 1,
1866
+ )
1867
+ }),
1868
+ )
1869
+
1870
+ cleanupFns.push(
1871
+ aiEventClient.on('text:iteration:completed', (e) => {
1872
+ const { requestId, streamId, clientId, iteration } = e.payload
1873
+
1874
+ const conversationId =
1875
+ clientId ||
1876
+ (streamId ? streamToConversation.get(streamId) : undefined) ||
1877
+ requestToConversation.get(requestId)
1878
+ if (!conversationId || !state.conversations[conversationId]) return
1879
+
1880
+ const conv = state.conversations[conversationId]
1881
+ // Find the iteration by BOTH requestId and index to avoid cross-request pollution.
1882
+ // Without requestId scoping, request 2's iteration 0 would match request 1's iteration 0.
1883
+ const iterIndex = conv.iterations.findIndex(
1884
+ (it) =>
1885
+ it.index === iteration &&
1886
+ (!requestId || it.requestId === requestId),
1887
+ )
1888
+ if (iterIndex === -1) return
1889
+
1890
+ setState(
1891
+ 'conversations',
1892
+ conversationId,
1893
+ 'iterations',
1894
+ iterIndex,
1895
+ produce((it: Iteration) => {
1896
+ it.completedAt = e.payload.timestamp
1897
+ it.finishReason = e.payload.finishReason
1898
+ if (e.payload.usage) {
1899
+ it.usage = e.payload.usage
1900
+ }
1901
+ }),
1902
+ )
1903
+ }),
1904
+ )
1905
+
1906
+ // ============= Middleware Events =============
1907
+
1908
+ /** Find the latest iteration for a given requestId, or the very latest iteration as fallback */
1909
+ function findLatestIterationIndex(
1910
+ conv: Conversation,
1911
+ reqId?: string,
1912
+ ): number {
1913
+ if (reqId) {
1914
+ for (let i = conv.iterations.length - 1; i >= 0; i--) {
1915
+ if (conv.iterations[i]?.requestId === reqId) return i
1916
+ }
1917
+ }
1918
+ return conv.iterations.length - 1
1919
+ }
1920
+
1921
+ cleanupFns.push(
1922
+ aiEventClient.on('middleware:hook:executed', (e) => {
1923
+ const { requestId, streamId, clientId } = e.payload
1924
+
1925
+ const conversationId =
1926
+ clientId ||
1927
+ (streamId ? streamToConversation.get(streamId) : undefined) ||
1928
+ requestToConversation.get(requestId)
1929
+ if (!conversationId || !state.conversations[conversationId]) return
1930
+
1931
+ const conv = state.conversations[conversationId]
1932
+ const iterIndex = findLatestIterationIndex(conv, requestId)
1933
+ if (iterIndex < 0) return
1934
+
1935
+ const mwEvent: MiddlewareEvent = {
1936
+ id: `mw-${Date.now()}-${Math.random()}`,
1937
+ middlewareName: e.payload.middlewareName,
1938
+ hookName: e.payload.hookName,
1939
+ timestamp: e.payload.timestamp,
1940
+ duration: e.payload.duration,
1941
+ hasTransform: e.payload.hasTransform,
1942
+ }
1943
+
1944
+ setState(
1945
+ 'conversations',
1946
+ conversationId,
1947
+ 'iterations',
1948
+ iterIndex,
1949
+ 'middlewareEvents',
1950
+ produce((arr: Array<MiddlewareEvent>) => {
1951
+ arr.push(mwEvent)
1952
+ }),
1953
+ )
1954
+ }),
1955
+ )
1956
+
1957
+ cleanupFns.push(
1958
+ aiEventClient.on('middleware:config:transformed', (e) => {
1959
+ const { requestId, streamId, clientId } = e.payload
1960
+
1961
+ const conversationId =
1962
+ clientId ||
1963
+ (streamId ? streamToConversation.get(streamId) : undefined) ||
1964
+ requestToConversation.get(requestId)
1965
+ if (!conversationId || !state.conversations[conversationId]) return
1966
+
1967
+ const conv = state.conversations[conversationId]
1968
+ const iterIndex = findLatestIterationIndex(conv, requestId)
1969
+ if (iterIndex < 0) return
1970
+
1971
+ const mwEvent: MiddlewareEvent = {
1972
+ id: `mw-cfg-${Date.now()}-${Math.random()}`,
1973
+ middlewareName: e.payload.middlewareName,
1974
+ hookName: 'onConfig',
1975
+ timestamp: e.payload.timestamp,
1976
+ hasTransform: true,
1977
+ configChanges: e.payload.changes,
1567
1978
  }
1979
+
1980
+ setState(
1981
+ 'conversations',
1982
+ conversationId,
1983
+ 'iterations',
1984
+ iterIndex,
1985
+ 'middlewareEvents',
1986
+ produce((arr: Array<MiddlewareEvent>) => {
1987
+ arr.push(mwEvent)
1988
+ }),
1989
+ )
1990
+ }),
1991
+ )
1992
+
1993
+ cleanupFns.push(
1994
+ aiEventClient.on('middleware:chunk:transformed', (e) => {
1995
+ const { requestId, streamId, clientId } = e.payload
1996
+
1997
+ const conversationId =
1998
+ clientId ||
1999
+ (streamId ? streamToConversation.get(streamId) : undefined) ||
2000
+ requestToConversation.get(requestId)
2001
+ if (!conversationId || !state.conversations[conversationId]) return
2002
+
2003
+ const conv = state.conversations[conversationId]
2004
+ const iterIndex = findLatestIterationIndex(conv, requestId)
2005
+ if (iterIndex < 0) return
2006
+
2007
+ const mwEvent: MiddlewareEvent = {
2008
+ id: `mw-chunk-${Date.now()}-${Math.random()}`,
2009
+ middlewareName: e.payload.middlewareName,
2010
+ hookName: 'onChunk',
2011
+ timestamp: e.payload.timestamp,
2012
+ hasTransform: true,
2013
+ originalChunkType: e.payload.originalChunkType,
2014
+ resultCount: e.payload.resultCount,
2015
+ wasDropped: e.payload.wasDropped,
2016
+ }
2017
+
2018
+ setState(
2019
+ 'conversations',
2020
+ conversationId,
2021
+ 'iterations',
2022
+ iterIndex,
2023
+ 'middlewareEvents',
2024
+ produce((arr: Array<MiddlewareEvent>) => {
2025
+ arr.push(mwEvent)
2026
+ }),
2027
+ )
1568
2028
  }),
1569
2029
  )
1570
2030
 
@@ -1,2 +1,9 @@
1
1
  // Re-export types from ai-context for backward compatibility
2
- export type { ToolCall, Message, Chunk, Conversation } from './ai-context'
2
+ export type {
3
+ ToolCall,
4
+ Message,
5
+ Chunk,
6
+ Conversation,
7
+ Iteration,
8
+ MiddlewareEvent,
9
+ } from './ai-context'