@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.
- package/README.md +6 -6
- package/dist/Shell/{MUFU35SY.js → SHJAKIOY.js} +3873 -1906
- package/dist/Shell/{GHXECDJV.js → TWTNN4SQ.js} +3909 -1427
- package/dist/dev.js +1 -1
- package/dist/index.js +1 -1
- package/dist/production/Shell/{MUFU35SY.js → SHJAKIOY.js} +3873 -1906
- package/dist/production/Shell/{GHXECDJV.js → TWTNN4SQ.js} +3909 -1427
- package/dist/production/dev.js +1 -1
- package/dist/production/index.js +1 -1
- package/dist/production/server.js +1 -1
- package/dist/server.js +1 -1
- package/package.json +3 -2
- package/src/components/ConversationDetails.tsx +166 -99
- package/src/components/conversation/ConversationHeader.tsx +57 -98
- package/src/components/conversation/ConversationTabs.tsx +12 -14
- package/src/components/conversation/IterationCard.tsx +745 -0
- package/src/components/conversation/IterationTimeline.tsx +424 -0
- package/src/components/conversation/index.ts +3 -0
- package/src/store/ai-context.tsx +485 -25
- package/src/store/ai-store.ts +8 -1
- package/src/styles/use-styles.ts +707 -0
package/src/store/ai-context.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
|
675
|
-
|
|
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 } =
|
|
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
|
-
|
|
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
|
-
|
|
1737
|
+
updateConversation(conversationId, {
|
|
1545
1738
|
status: 'completed',
|
|
1546
1739
|
completedAt: e.payload.timestamp,
|
|
1547
|
-
}
|
|
1740
|
+
})
|
|
1548
1741
|
if (usage) {
|
|
1549
|
-
|
|
1742
|
+
updateConversationUsage(conversationId, requestId, usage)
|
|
1743
|
+
updateMessageUsage(
|
|
1744
|
+
conversationId,
|
|
1745
|
+
e.payload.messageId,
|
|
1746
|
+
usage,
|
|
1747
|
+
requestId,
|
|
1748
|
+
)
|
|
1550
1749
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
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
|
-
|
|
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
|
|
package/src/store/ai-store.ts
CHANGED