@tanstack/ai-devtools-core 0.3.38 → 0.4.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/Shell/DKCFIT6H.js +12142 -0
- package/dist/Shell/SLOX2TPU.js +12119 -0
- package/dist/Shell/Z6HLJOVZ.js +10170 -0
- package/dist/dev.js +1 -1
- package/dist/index.js +1 -1
- package/dist/production/Shell/DKCFIT6H.js +12142 -0
- package/dist/production/Shell/SLOX2TPU.js +12119 -0
- package/dist/production/Shell/Z6HLJOVZ.js +10170 -0
- 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 +4 -4
- package/src/components/Shell.tsx +74 -30
- package/src/components/conversation/IterationCard.tsx +261 -12
- package/src/components/conversation/IterationTimeline.tsx +66 -16
- package/src/components/conversation/index.ts +0 -9
- package/src/components/hooks/FixtureNamePopover.tsx +69 -0
- package/src/components/hooks/GenerationPanel.tsx +847 -0
- package/src/components/hooks/HookDashboard.tsx +292 -0
- package/src/components/hooks/HookDetails.tsx +1757 -0
- package/src/components/hooks/ToolFixtureForm.tsx +347 -0
- package/src/components/hooks/hook-dashboard-model.ts +134 -0
- package/src/components/hooks/index.ts +4 -0
- package/src/components/hooks/preview-messages.ts +117 -0
- package/src/components/hooks/preview-model.ts +195 -0
- package/src/components/utils/format.ts +0 -58
- package/src/store/ai-context.tsx +1165 -159
- package/src/store/ai-store.ts +0 -1
- package/src/store/hook-registry.ts +815 -0
- package/src/store/message-event-utils.ts +60 -0
- package/src/styles/use-styles.ts +1558 -99
- package/dist/Shell/MRWZEHAJ.js +0 -6225
- package/dist/Shell/RRRNZJ5E.js +0 -7560
- package/dist/production/Shell/MRWZEHAJ.js +0 -6225
- package/dist/production/Shell/RRRNZJ5E.js +0 -7560
- package/src/components/ConversationDetails.tsx +0 -216
- package/src/components/ConversationsList.tsx +0 -21
- package/src/components/conversation/ActivityEventsTab.tsx +0 -57
- package/src/components/conversation/ChunkBadges.tsx +0 -51
- package/src/components/conversation/ChunkItem.tsx +0 -273
- package/src/components/conversation/ChunksCollapsible.tsx +0 -56
- package/src/components/conversation/ChunksTab.tsx +0 -87
- package/src/components/conversation/ConversationHeader.tsx +0 -106
- package/src/components/conversation/ConversationTabs.tsx +0 -205
- package/src/components/conversation/MessageCard.tsx +0 -174
- package/src/components/conversation/MessageGroup.tsx +0 -101
- package/src/components/conversation/MessagesTab.tsx +0 -30
- package/src/components/conversation/SummariesTab.tsx +0 -91
- package/src/components/conversation/ToolCallDisplay.tsx +0 -99
- package/src/components/list/ConversationRow.tsx +0 -85
- package/src/components/list/index.ts +0 -1
package/src/store/ai-context.tsx
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import { batch, createContext, onCleanup, onMount, useContext } from 'solid-js'
|
|
2
2
|
import { createStore, produce } from 'solid-js/store'
|
|
3
3
|
import { aiEventClient } from '@tanstack/ai-event-client'
|
|
4
|
+
import {
|
|
5
|
+
addSavedFixture,
|
|
6
|
+
applyHookEvent,
|
|
7
|
+
clearHookRegistry,
|
|
8
|
+
createHookRegistryState,
|
|
9
|
+
removeSavedFixture,
|
|
10
|
+
replaceSavedFixtures,
|
|
11
|
+
setActiveHook,
|
|
12
|
+
} from './hook-registry'
|
|
13
|
+
import {
|
|
14
|
+
createClientToolCallMessage,
|
|
15
|
+
shouldSkipClientAssistantPlaceholder,
|
|
16
|
+
} from './message-event-utils'
|
|
4
17
|
import type { ContentPartSource } from '@tanstack/ai'
|
|
18
|
+
import type {
|
|
19
|
+
DevtoolsToolFixtureApplyEvent,
|
|
20
|
+
RunLifecycleEvent,
|
|
21
|
+
} from '@tanstack/ai-event-client'
|
|
22
|
+
import type { HookRegistryState, ToolFixtureRecord } from './hook-registry'
|
|
5
23
|
import type { ParentComponent } from 'solid-js'
|
|
6
24
|
|
|
7
25
|
interface MessagePart {
|
|
@@ -14,13 +32,25 @@ interface MessagePart {
|
|
|
14
32
|
| 'audio'
|
|
15
33
|
| 'video'
|
|
16
34
|
| 'document'
|
|
35
|
+
| 'structured-output'
|
|
17
36
|
content?: string
|
|
37
|
+
status?: 'streaming' | 'complete' | 'error'
|
|
38
|
+
raw?: string
|
|
39
|
+
partial?: unknown
|
|
40
|
+
data?: unknown
|
|
41
|
+
reasoning?: string
|
|
42
|
+
errorMessage?: string
|
|
18
43
|
toolCallId?: string
|
|
19
44
|
toolName?: string
|
|
20
45
|
arguments?: string
|
|
21
46
|
state?: string
|
|
22
47
|
output?: unknown
|
|
23
48
|
error?: string
|
|
49
|
+
approval?: {
|
|
50
|
+
id?: string
|
|
51
|
+
needsApproval?: boolean
|
|
52
|
+
approved?: boolean
|
|
53
|
+
}
|
|
24
54
|
// Multimodal content fields
|
|
25
55
|
source?: ContentPartSource
|
|
26
56
|
metadata?: unknown
|
|
@@ -34,6 +64,7 @@ export interface ToolCall {
|
|
|
34
64
|
result?: unknown
|
|
35
65
|
approvalRequired?: boolean
|
|
36
66
|
approvalId?: string
|
|
67
|
+
approvalApproved?: boolean
|
|
37
68
|
/** Duration of tool execution in milliseconds */
|
|
38
69
|
duration?: number
|
|
39
70
|
}
|
|
@@ -78,6 +109,7 @@ export interface Chunk {
|
|
|
78
109
|
| 'error'
|
|
79
110
|
| 'approval'
|
|
80
111
|
| 'thinking'
|
|
112
|
+
| 'structured_output'
|
|
81
113
|
timestamp: number
|
|
82
114
|
messageId?: string
|
|
83
115
|
/** Accumulated content from all merged chunks */
|
|
@@ -89,6 +121,7 @@ export interface Chunk {
|
|
|
89
121
|
finishReason?: string
|
|
90
122
|
error?: string
|
|
91
123
|
approvalId?: string
|
|
124
|
+
approved?: boolean
|
|
92
125
|
input?: unknown
|
|
93
126
|
/** Tool arguments for tool_call chunks */
|
|
94
127
|
arguments?: string
|
|
@@ -100,6 +133,12 @@ export interface Chunk {
|
|
|
100
133
|
chunkCount: number
|
|
101
134
|
/** Whether this is a client-side tool execution */
|
|
102
135
|
isClientTool?: boolean
|
|
136
|
+
structuredStatus?: 'streaming' | 'complete' | 'error'
|
|
137
|
+
raw?: string
|
|
138
|
+
partial?: unknown
|
|
139
|
+
data?: unknown
|
|
140
|
+
reasoning?: string
|
|
141
|
+
errorMessage?: string
|
|
103
142
|
}
|
|
104
143
|
|
|
105
144
|
export interface MiddlewareEvent {
|
|
@@ -162,6 +201,7 @@ export interface Conversation {
|
|
|
162
201
|
status: 'active' | 'completed' | 'error'
|
|
163
202
|
startedAt: number
|
|
164
203
|
completedAt?: number
|
|
204
|
+
runIds?: Array<string>
|
|
165
205
|
usage?: TokenUsage
|
|
166
206
|
iterations: Array<Iteration>
|
|
167
207
|
iterationCount?: number
|
|
@@ -173,12 +213,14 @@ export interface Conversation {
|
|
|
173
213
|
hasChat?: boolean
|
|
174
214
|
hasSummarize?: boolean
|
|
175
215
|
hasImage?: boolean
|
|
216
|
+
hasAudio?: boolean
|
|
176
217
|
hasSpeech?: boolean
|
|
177
218
|
hasTranscription?: boolean
|
|
178
219
|
hasVideo?: boolean
|
|
179
220
|
/** Summarize operations in this conversation */
|
|
180
221
|
summaries?: Array<SummarizeOperation>
|
|
181
222
|
imageEvents?: Array<ActivityEvent>
|
|
223
|
+
audioEvents?: Array<ActivityEvent>
|
|
182
224
|
speechEvents?: Array<ActivityEvent>
|
|
183
225
|
transcriptionEvents?: Array<ActivityEvent>
|
|
184
226
|
videoEvents?: Array<ActivityEvent>
|
|
@@ -187,12 +229,18 @@ export interface Conversation {
|
|
|
187
229
|
interface AIStoreState {
|
|
188
230
|
conversations: Record<string, Conversation>
|
|
189
231
|
activeConversationId: string | null
|
|
232
|
+
hooks: HookRegistryState
|
|
190
233
|
}
|
|
191
234
|
|
|
192
235
|
interface AIContextValue {
|
|
193
236
|
state: AIStoreState
|
|
194
237
|
clearAllConversations: () => void
|
|
195
238
|
selectConversation: (id: string) => void
|
|
239
|
+
clearHooks: () => void
|
|
240
|
+
selectHook: (id: string | null) => void
|
|
241
|
+
saveToolFixture: (fixture: ToolFixtureRecord) => void
|
|
242
|
+
deleteToolFixture: (fixtureId: string) => void
|
|
243
|
+
applyToolFixture: (fixture: ToolFixtureRecord) => void
|
|
196
244
|
}
|
|
197
245
|
|
|
198
246
|
const AIContext = createContext<AIContextValue>()
|
|
@@ -209,20 +257,19 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
209
257
|
const [state, setState] = createStore<AIStoreState>({
|
|
210
258
|
conversations: {},
|
|
211
259
|
activeConversationId: null,
|
|
260
|
+
hooks: createHookRegistryState(),
|
|
212
261
|
})
|
|
213
262
|
|
|
214
263
|
const streamToConversation = new Map<string, string>()
|
|
215
264
|
const requestToConversation = new Map<string, string>()
|
|
216
265
|
/** Track max cumulative usage per requestId per conversation for correct totals */
|
|
217
266
|
const requestUsageByConversation = new Map<string, Map<string, TokenUsage>>()
|
|
267
|
+
const fixturesStorageKey = 'tanstack-ai-devtools:tool-fixtures'
|
|
218
268
|
|
|
219
|
-
// Batching system for high-frequency chunk updates with consolidated chunk merging
|
|
220
|
-
// Stores: conversationId -> { chunks to merge, totalNewChunks count }
|
|
221
269
|
const pendingConversationChunks = new Map<
|
|
222
270
|
string,
|
|
223
271
|
{ chunks: Array<Chunk>; newChunkCount: number }
|
|
224
272
|
>()
|
|
225
|
-
// Stores: conversationId -> messageIndex -> { chunks to merge, totalNewChunks count }
|
|
226
273
|
const pendingMessageChunks = new Map<
|
|
227
274
|
string,
|
|
228
275
|
Map<number, { chunks: Array<Chunk>; newChunkCount: number }>
|
|
@@ -235,29 +282,24 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
235
282
|
queueMicrotask(flushPendingChunks)
|
|
236
283
|
}
|
|
237
284
|
|
|
238
|
-
/** Check if a chunk type can be merged with consecutive same-type chunks */
|
|
239
285
|
function isMergeableChunkType(type: Chunk['type']): boolean {
|
|
240
286
|
return type === 'content' || type === 'thinking'
|
|
241
287
|
}
|
|
242
288
|
|
|
243
|
-
/** Merge pending chunks into existing chunks array, consolidating consecutive same-type chunks */
|
|
244
289
|
function mergeChunks(existing: Array<Chunk>, pending: Array<Chunk>): void {
|
|
245
290
|
for (const chunk of pending) {
|
|
246
291
|
const lastChunk = existing[existing.length - 1]
|
|
247
292
|
|
|
248
|
-
// If last chunk exists, is the same type, and both are mergeable types, merge them
|
|
249
293
|
if (
|
|
250
294
|
lastChunk &&
|
|
251
295
|
lastChunk.type === chunk.type &&
|
|
252
296
|
isMergeableChunkType(chunk.type) &&
|
|
253
297
|
lastChunk.messageId === chunk.messageId
|
|
254
298
|
) {
|
|
255
|
-
// Merge: use the new chunk's content (it's already accumulated), update delta, increment count
|
|
256
299
|
lastChunk.content = chunk.content || lastChunk.content
|
|
257
300
|
lastChunk.delta = chunk.delta
|
|
258
301
|
lastChunk.chunkCount += chunk.chunkCount
|
|
259
302
|
} else {
|
|
260
|
-
// Different type or not mergeable - add as new entry
|
|
261
303
|
existing.push(chunk)
|
|
262
304
|
}
|
|
263
305
|
}
|
|
@@ -267,7 +309,6 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
267
309
|
batchScheduled = false
|
|
268
310
|
|
|
269
311
|
batch(() => {
|
|
270
|
-
// Flush conversation-level chunks
|
|
271
312
|
for (const [
|
|
272
313
|
conversationId,
|
|
273
314
|
{ chunks, newChunkCount },
|
|
@@ -286,7 +327,6 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
286
327
|
}
|
|
287
328
|
pendingConversationChunks.clear()
|
|
288
329
|
|
|
289
|
-
// Flush message-level chunks
|
|
290
330
|
for (const [conversationId, messageMap] of pendingMessageChunks) {
|
|
291
331
|
const conv = state.conversations[conversationId]
|
|
292
332
|
if (!conv) continue
|
|
@@ -294,7 +334,6 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
294
334
|
for (const [messageIndex, { chunks, newChunkCount }] of messageMap) {
|
|
295
335
|
const message = conv.messages[messageIndex]
|
|
296
336
|
if (message) {
|
|
297
|
-
// Update chunks array with merging
|
|
298
337
|
setState(
|
|
299
338
|
'conversations',
|
|
300
339
|
conversationId,
|
|
@@ -302,15 +341,11 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
302
341
|
messageIndex,
|
|
303
342
|
'chunks',
|
|
304
343
|
produce((arr: Array<Chunk> | undefined) => {
|
|
305
|
-
if (!arr)
|
|
306
|
-
// First time - just set the chunks (they're already consolidated in pending)
|
|
307
|
-
return chunks
|
|
308
|
-
}
|
|
344
|
+
if (!arr) return chunks
|
|
309
345
|
mergeChunks(arr, chunks)
|
|
310
346
|
return arr
|
|
311
347
|
}),
|
|
312
348
|
)
|
|
313
|
-
// Update total chunk count
|
|
314
349
|
const currentTotal = message.totalChunkCount || 0
|
|
315
350
|
setState(
|
|
316
351
|
'conversations',
|
|
@@ -334,7 +369,6 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
334
369
|
pendingConversationChunks.set(conversationId, pending)
|
|
335
370
|
}
|
|
336
371
|
|
|
337
|
-
// Pre-merge in pending buffer to reduce array operations during flush
|
|
338
372
|
const lastPending = pending.chunks[pending.chunks.length - 1]
|
|
339
373
|
if (
|
|
340
374
|
lastPending &&
|
|
@@ -342,8 +376,6 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
342
376
|
isMergeableChunkType(chunk.type) &&
|
|
343
377
|
lastPending.messageId === chunk.messageId
|
|
344
378
|
) {
|
|
345
|
-
// Merge into pending buffer - only append delta (incremental content)
|
|
346
|
-
// Use the new chunk's content as the accumulated content (it's already accumulated)
|
|
347
379
|
lastPending.content = chunk.content || lastPending.content
|
|
348
380
|
lastPending.delta = chunk.delta
|
|
349
381
|
lastPending.chunkCount += chunk.chunkCount
|
|
@@ -370,7 +402,6 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
370
402
|
messageMap.set(messageIndex, pending)
|
|
371
403
|
}
|
|
372
404
|
|
|
373
|
-
// Pre-merge in pending buffer
|
|
374
405
|
const lastPending = pending.chunks[pending.chunks.length - 1]
|
|
375
406
|
if (
|
|
376
407
|
lastPending &&
|
|
@@ -378,8 +409,6 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
378
409
|
isMergeableChunkType(chunk.type) &&
|
|
379
410
|
lastPending.messageId === chunk.messageId
|
|
380
411
|
) {
|
|
381
|
-
// Merge into pending buffer - only append delta (incremental content)
|
|
382
|
-
// Use the new chunk's content as the accumulated content (it's already accumulated)
|
|
383
412
|
lastPending.content = chunk.content || lastPending.content
|
|
384
413
|
lastPending.delta = chunk.delta
|
|
385
414
|
lastPending.chunkCount += chunk.chunkCount
|
|
@@ -390,7 +419,6 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
390
419
|
scheduleBatchFlush()
|
|
391
420
|
}
|
|
392
421
|
|
|
393
|
-
// Optimized helper functions using path-based updates
|
|
394
422
|
function getOrCreateConversation(
|
|
395
423
|
id: string,
|
|
396
424
|
type: 'client' | 'server',
|
|
@@ -405,6 +433,7 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
405
433
|
chunks: [],
|
|
406
434
|
iterations: [],
|
|
407
435
|
imageEvents: [],
|
|
436
|
+
audioEvents: [],
|
|
408
437
|
speechEvents: [],
|
|
409
438
|
transcriptionEvents: [],
|
|
410
439
|
videoEvents: [],
|
|
@@ -417,6 +446,18 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
417
446
|
}
|
|
418
447
|
}
|
|
419
448
|
|
|
449
|
+
function attachRunToConversation(
|
|
450
|
+
conversationId: string,
|
|
451
|
+
runId: string | undefined,
|
|
452
|
+
): void {
|
|
453
|
+
if (!runId) return
|
|
454
|
+
const conv = state.conversations[conversationId]
|
|
455
|
+
if (!conv || conv.runIds?.includes(runId)) return
|
|
456
|
+
updateConversation(conversationId, {
|
|
457
|
+
runIds: [...(conv.runIds ?? []), runId],
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
|
|
420
461
|
function addMessage(conversationId: string, message: Message): void {
|
|
421
462
|
const conv = state.conversations[conversationId]
|
|
422
463
|
if (!conv) return
|
|
@@ -594,6 +635,181 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
594
635
|
setState('activeConversationId', id)
|
|
595
636
|
}
|
|
596
637
|
|
|
638
|
+
function clearHooks() {
|
|
639
|
+
setState(
|
|
640
|
+
'hooks',
|
|
641
|
+
produce((hooks: HookRegistryState) => {
|
|
642
|
+
clearHookRegistry(hooks)
|
|
643
|
+
}),
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function selectHook(id: string | null) {
|
|
648
|
+
setState(
|
|
649
|
+
'hooks',
|
|
650
|
+
produce((hooks: HookRegistryState) => {
|
|
651
|
+
setActiveHook(hooks, id)
|
|
652
|
+
}),
|
|
653
|
+
)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function saveToolFixture(fixture: ToolFixtureRecord) {
|
|
657
|
+
const fixtures = mergeFixtures(fixture, state.hooks.fixtures)
|
|
658
|
+
setState(
|
|
659
|
+
'hooks',
|
|
660
|
+
produce((hooks: HookRegistryState) => {
|
|
661
|
+
addSavedFixture(hooks, fixture)
|
|
662
|
+
}),
|
|
663
|
+
)
|
|
664
|
+
persistFixtures(fixtures)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function deleteToolFixture(fixtureId: string) {
|
|
668
|
+
const fixtures = state.hooks.fixtures.filter(
|
|
669
|
+
(fixture) => fixture.id !== fixtureId,
|
|
670
|
+
)
|
|
671
|
+
setState(
|
|
672
|
+
'hooks',
|
|
673
|
+
produce((hooks: HookRegistryState) => {
|
|
674
|
+
removeSavedFixture(hooks, fixtureId)
|
|
675
|
+
}),
|
|
676
|
+
)
|
|
677
|
+
persistFixtures(fixtures)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function applyToolFixture(fixture: ToolFixtureRecord) {
|
|
681
|
+
const timestamp = Date.now()
|
|
682
|
+
const payload: DevtoolsToolFixtureApplyEvent = {
|
|
683
|
+
eventId: `devtools:tool-fixture:apply:${fixture.id}:${timestamp}:${Math.random()
|
|
684
|
+
.toString(36)
|
|
685
|
+
.slice(2)}`,
|
|
686
|
+
fixtureId: fixture.id,
|
|
687
|
+
timestamp,
|
|
688
|
+
source: 'devtools',
|
|
689
|
+
visibility: 'devtools-action',
|
|
690
|
+
...(fixture.hookId ? { hookId: fixture.hookId } : {}),
|
|
691
|
+
...(fixture.threadId ? { threadId: fixture.threadId } : {}),
|
|
692
|
+
...(fixture.runId ? { runId: fixture.runId } : {}),
|
|
693
|
+
toolName: fixture.toolName,
|
|
694
|
+
input: fixture.input,
|
|
695
|
+
output: fixture.output,
|
|
696
|
+
...(fixture.execute !== undefined ? { execute: fixture.execute } : {}),
|
|
697
|
+
...(fixture.message ? { message: fixture.message } : {}),
|
|
698
|
+
...(fixture.errorText ? { errorText: fixture.errorText } : {}),
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
aiEventClient.emit('devtools:tool-fixture:apply', payload)
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function mergeFixtures(
|
|
705
|
+
fixture: ToolFixtureRecord,
|
|
706
|
+
fixtures: Array<ToolFixtureRecord>,
|
|
707
|
+
): Array<ToolFixtureRecord> {
|
|
708
|
+
return [
|
|
709
|
+
fixture,
|
|
710
|
+
...fixtures.filter((existing) => existing.id !== fixture.id),
|
|
711
|
+
].slice(0, 50)
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function persistFixtures(fixtures: Array<ToolFixtureRecord>) {
|
|
715
|
+
if (typeof localStorage === 'undefined') return
|
|
716
|
+
localStorage.setItem(fixturesStorageKey, JSON.stringify(fixtures))
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function loadFixtures() {
|
|
720
|
+
if (typeof localStorage === 'undefined') return
|
|
721
|
+
const raw = localStorage.getItem(fixturesStorageKey)
|
|
722
|
+
if (!raw) return
|
|
723
|
+
|
|
724
|
+
let parsed: unknown
|
|
725
|
+
try {
|
|
726
|
+
parsed = JSON.parse(raw)
|
|
727
|
+
} catch (err) {
|
|
728
|
+
// Surface the bad payload instead of silently nuking it — keep the raw
|
|
729
|
+
// value in storage so the user can recover it manually if needed.
|
|
730
|
+
console.error(
|
|
731
|
+
'[ai-devtools] failed to parse saved tool fixtures; keeping raw payload for inspection',
|
|
732
|
+
err,
|
|
733
|
+
)
|
|
734
|
+
return
|
|
735
|
+
}
|
|
736
|
+
if (!Array.isArray(parsed)) {
|
|
737
|
+
console.error(
|
|
738
|
+
'[ai-devtools] saved tool fixtures payload is not an array; ignoring',
|
|
739
|
+
parsed,
|
|
740
|
+
)
|
|
741
|
+
return
|
|
742
|
+
}
|
|
743
|
+
const fixtures = parsed.filter(isToolFixtureRecord)
|
|
744
|
+
setState(
|
|
745
|
+
'hooks',
|
|
746
|
+
produce((hooks: HookRegistryState) => {
|
|
747
|
+
replaceSavedFixtures(hooks, fixtures)
|
|
748
|
+
}),
|
|
749
|
+
)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function isToolFixtureRecord(value: unknown): value is ToolFixtureRecord {
|
|
753
|
+
if (typeof value !== 'object' || value === null) return false
|
|
754
|
+
const candidate = value as Partial<ToolFixtureRecord>
|
|
755
|
+
return (
|
|
756
|
+
typeof candidate.id === 'string' &&
|
|
757
|
+
typeof candidate.createdAt === 'number' &&
|
|
758
|
+
(candidate.name === undefined || typeof candidate.name === 'string') &&
|
|
759
|
+
typeof candidate.toolName === 'string' &&
|
|
760
|
+
(candidate.execute === undefined ||
|
|
761
|
+
typeof candidate.execute === 'boolean') &&
|
|
762
|
+
'input' in candidate &&
|
|
763
|
+
'output' in candidate &&
|
|
764
|
+
(candidate.message === undefined ||
|
|
765
|
+
isToolFixtureMessage(candidate.message))
|
|
766
|
+
)
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function isToolFixtureMessage(
|
|
770
|
+
value: unknown,
|
|
771
|
+
): value is NonNullable<ToolFixtureRecord['message']> {
|
|
772
|
+
if (typeof value !== 'object' || value === null) return false
|
|
773
|
+
const candidate = value as {
|
|
774
|
+
id?: unknown
|
|
775
|
+
role?: unknown
|
|
776
|
+
parts?: unknown
|
|
777
|
+
createdAt?: unknown
|
|
778
|
+
}
|
|
779
|
+
return (
|
|
780
|
+
typeof candidate.id === 'string' &&
|
|
781
|
+
(candidate.role === 'system' ||
|
|
782
|
+
candidate.role === 'user' ||
|
|
783
|
+
candidate.role === 'assistant') &&
|
|
784
|
+
Array.isArray(candidate.parts) &&
|
|
785
|
+
(candidate.createdAt === undefined ||
|
|
786
|
+
typeof candidate.createdAt === 'number' ||
|
|
787
|
+
typeof candidate.createdAt === 'string')
|
|
788
|
+
)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function normalizeMessageSource(
|
|
792
|
+
source: unknown,
|
|
793
|
+
fallback: 'client' | 'server',
|
|
794
|
+
): 'client' | 'server' {
|
|
795
|
+
return source === 'client' || source === 'server' ? source : fallback
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function stringifyToolArguments(value: unknown): string {
|
|
799
|
+
if (typeof value === 'string') return value
|
|
800
|
+
try {
|
|
801
|
+
return JSON.stringify(value ?? {})
|
|
802
|
+
} catch (error) {
|
|
803
|
+
console.error(
|
|
804
|
+
'[ai-devtools] failed to JSON.stringify tool call arguments; saved fixture replay will be malformed.',
|
|
805
|
+
{ error, value },
|
|
806
|
+
)
|
|
807
|
+
return `[ai-devtools] unserializable tool arguments: ${
|
|
808
|
+
error instanceof Error ? error.message : String(error)
|
|
809
|
+
}`
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
597
813
|
// Additional optimized helper functions
|
|
598
814
|
function updateConversation(
|
|
599
815
|
conversationId: string,
|
|
@@ -648,6 +864,202 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
648
864
|
)
|
|
649
865
|
}
|
|
650
866
|
|
|
867
|
+
function findToolCallLocation(
|
|
868
|
+
conversationId: string,
|
|
869
|
+
input: { toolCallId?: string; approvalId?: string },
|
|
870
|
+
): { messageIndex: number; toolCallIndex: number } | undefined {
|
|
871
|
+
const conv = state.conversations[conversationId]
|
|
872
|
+
if (!conv) return undefined
|
|
873
|
+
|
|
874
|
+
for (
|
|
875
|
+
let messageIndex = conv.messages.length - 1;
|
|
876
|
+
messageIndex >= 0;
|
|
877
|
+
messageIndex--
|
|
878
|
+
) {
|
|
879
|
+
const message = conv.messages[messageIndex]
|
|
880
|
+
if (!message?.toolCalls) continue
|
|
881
|
+
|
|
882
|
+
const toolCallIndex = message.toolCalls.findIndex((toolCall) => {
|
|
883
|
+
if (input.toolCallId && toolCall.id === input.toolCallId) return true
|
|
884
|
+
if (input.approvalId && toolCall.approvalId === input.approvalId) {
|
|
885
|
+
return true
|
|
886
|
+
}
|
|
887
|
+
return false
|
|
888
|
+
})
|
|
889
|
+
if (toolCallIndex >= 0) {
|
|
890
|
+
return { messageIndex, toolCallIndex }
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return undefined
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function getToolEventConversationIds(input: {
|
|
898
|
+
clientId?: string
|
|
899
|
+
threadId?: string
|
|
900
|
+
streamId?: string
|
|
901
|
+
toolCallId?: string
|
|
902
|
+
approvalId?: string
|
|
903
|
+
}): Array<string> {
|
|
904
|
+
const ids: Array<string> = []
|
|
905
|
+
const add = (id: string | undefined) => {
|
|
906
|
+
if (id && !ids.includes(id)) ids.push(id)
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
add(input.clientId)
|
|
910
|
+
add(input.streamId ? streamToConversation.get(input.streamId) : undefined)
|
|
911
|
+
add(input.threadId)
|
|
912
|
+
|
|
913
|
+
for (const conversationId of Object.keys(state.conversations)) {
|
|
914
|
+
if (
|
|
915
|
+
findToolCallLocation(conversationId, {
|
|
916
|
+
toolCallId: input.toolCallId,
|
|
917
|
+
approvalId: input.approvalId,
|
|
918
|
+
})
|
|
919
|
+
) {
|
|
920
|
+
add(conversationId)
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return ids
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function getStructuredOutputConversationIds(input: {
|
|
928
|
+
clientId?: string
|
|
929
|
+
threadId?: string
|
|
930
|
+
streamId?: string
|
|
931
|
+
messageId?: string
|
|
932
|
+
}): Array<string> {
|
|
933
|
+
const ids: Array<string> = []
|
|
934
|
+
const add = (id: string | undefined) => {
|
|
935
|
+
if (id && !ids.includes(id)) ids.push(id)
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
add(input.clientId)
|
|
939
|
+
add(input.streamId ? streamToConversation.get(input.streamId) : undefined)
|
|
940
|
+
add(input.threadId)
|
|
941
|
+
|
|
942
|
+
if (input.messageId) {
|
|
943
|
+
for (const conversation of Object.values(state.conversations)) {
|
|
944
|
+
if (
|
|
945
|
+
conversation.messages.some(
|
|
946
|
+
(message) => message.id === input.messageId,
|
|
947
|
+
)
|
|
948
|
+
) {
|
|
949
|
+
add(conversation.id)
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return ids
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function upsertStructuredOutputPart(
|
|
958
|
+
conversationId: string,
|
|
959
|
+
input: {
|
|
960
|
+
messageId: string
|
|
961
|
+
timestamp: number
|
|
962
|
+
source: 'client' | 'server'
|
|
963
|
+
requestId?: string
|
|
964
|
+
status: 'streaming' | 'complete' | 'error'
|
|
965
|
+
raw: string
|
|
966
|
+
partial?: unknown
|
|
967
|
+
data?: unknown
|
|
968
|
+
reasoning?: string
|
|
969
|
+
errorMessage?: string
|
|
970
|
+
},
|
|
971
|
+
): number | undefined {
|
|
972
|
+
const conv = state.conversations[conversationId]
|
|
973
|
+
if (!conv) return undefined
|
|
974
|
+
|
|
975
|
+
const part: MessagePart = {
|
|
976
|
+
type: 'structured-output',
|
|
977
|
+
status: input.status,
|
|
978
|
+
raw: input.raw,
|
|
979
|
+
...(input.partial !== undefined ? { partial: input.partial } : {}),
|
|
980
|
+
...(input.data !== undefined ? { data: input.data } : {}),
|
|
981
|
+
...(input.reasoning !== undefined ? { reasoning: input.reasoning } : {}),
|
|
982
|
+
...(input.errorMessage !== undefined
|
|
983
|
+
? { errorMessage: input.errorMessage }
|
|
984
|
+
: {}),
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
let messageIndex = conv.messages.findIndex(
|
|
988
|
+
(message) => message.id === input.messageId,
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
if (messageIndex === -1) {
|
|
992
|
+
messageIndex = conv.messages.length
|
|
993
|
+
addMessage(conversationId, {
|
|
994
|
+
id: input.messageId,
|
|
995
|
+
role: 'assistant',
|
|
996
|
+
content: '',
|
|
997
|
+
timestamp: input.timestamp,
|
|
998
|
+
parts: [part],
|
|
999
|
+
source: input.source,
|
|
1000
|
+
...(input.requestId ? { requestId: input.requestId } : {}),
|
|
1001
|
+
})
|
|
1002
|
+
} else {
|
|
1003
|
+
setState(
|
|
1004
|
+
'conversations',
|
|
1005
|
+
conversationId,
|
|
1006
|
+
'messages',
|
|
1007
|
+
messageIndex,
|
|
1008
|
+
'parts',
|
|
1009
|
+
produce((parts: Array<MessagePart> | undefined) => {
|
|
1010
|
+
const next = parts ? [...parts] : []
|
|
1011
|
+
const existingIndex = next.findIndex(
|
|
1012
|
+
(candidate) => candidate.type === 'structured-output',
|
|
1013
|
+
)
|
|
1014
|
+
if (existingIndex >= 0) {
|
|
1015
|
+
next[existingIndex] = part
|
|
1016
|
+
} else {
|
|
1017
|
+
next.push(part)
|
|
1018
|
+
}
|
|
1019
|
+
return next
|
|
1020
|
+
}),
|
|
1021
|
+
)
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return messageIndex
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function attachMessageToLatestIteration(
|
|
1028
|
+
conversationId: string,
|
|
1029
|
+
messageId: string,
|
|
1030
|
+
requestId: string | undefined,
|
|
1031
|
+
): void {
|
|
1032
|
+
const conv = state.conversations[conversationId]
|
|
1033
|
+
if (!conv || conv.iterations.length === 0) return
|
|
1034
|
+
|
|
1035
|
+
let iterIndex = -1
|
|
1036
|
+
if (requestId) {
|
|
1037
|
+
for (let i = conv.iterations.length - 1; i >= 0; i--) {
|
|
1038
|
+
if (conv.iterations[i]?.requestId === requestId) {
|
|
1039
|
+
iterIndex = i
|
|
1040
|
+
break
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (iterIndex === -1) {
|
|
1045
|
+
iterIndex = conv.iterations.length - 1
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const iteration = conv.iterations[iterIndex]
|
|
1049
|
+
if (!iteration || iteration.messageIds.includes(messageId)) return
|
|
1050
|
+
|
|
1051
|
+
setState(
|
|
1052
|
+
'conversations',
|
|
1053
|
+
conversationId,
|
|
1054
|
+
'iterations',
|
|
1055
|
+
iterIndex,
|
|
1056
|
+
'messageIds',
|
|
1057
|
+
produce((arr: Array<string>) => {
|
|
1058
|
+
arr.push(messageId)
|
|
1059
|
+
}),
|
|
1060
|
+
)
|
|
1061
|
+
}
|
|
1062
|
+
|
|
651
1063
|
function setToolCalls(
|
|
652
1064
|
conversationId: string,
|
|
653
1065
|
messageIndex: number,
|
|
@@ -673,6 +1085,7 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
673
1085
|
conversationId: string,
|
|
674
1086
|
activity:
|
|
675
1087
|
| 'imageEvents'
|
|
1088
|
+
| 'audioEvents'
|
|
676
1089
|
| 'speechEvents'
|
|
677
1090
|
| 'transcriptionEvents'
|
|
678
1091
|
| 'videoEvents',
|
|
@@ -693,6 +1106,7 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
693
1106
|
|
|
694
1107
|
const activityFlagMap: Record<typeof activity, keyof Conversation> = {
|
|
695
1108
|
imageEvents: 'hasImage',
|
|
1109
|
+
audioEvents: 'hasAudio',
|
|
696
1110
|
speechEvents: 'hasSpeech',
|
|
697
1111
|
transcriptionEvents: 'hasTranscription',
|
|
698
1112
|
videoEvents: 'hasVideo',
|
|
@@ -731,8 +1145,112 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
731
1145
|
// Register all event listeners on mount
|
|
732
1146
|
onMount(() => {
|
|
733
1147
|
const cleanupFns: Array<() => void> = []
|
|
1148
|
+
loadFixtures()
|
|
1149
|
+
|
|
1150
|
+
cleanupFns.push(
|
|
1151
|
+
aiEventClient.on('hook:registered', (e) => {
|
|
1152
|
+
setState(
|
|
1153
|
+
'hooks',
|
|
1154
|
+
produce((hooks: HookRegistryState) => {
|
|
1155
|
+
applyHookEvent(hooks, 'hook:registered', e.payload)
|
|
1156
|
+
}),
|
|
1157
|
+
)
|
|
1158
|
+
}),
|
|
1159
|
+
)
|
|
1160
|
+
|
|
1161
|
+
cleanupFns.push(
|
|
1162
|
+
aiEventClient.on('hook:updated', (e) => {
|
|
1163
|
+
setState(
|
|
1164
|
+
'hooks',
|
|
1165
|
+
produce((hooks: HookRegistryState) => {
|
|
1166
|
+
applyHookEvent(hooks, 'hook:updated', e.payload)
|
|
1167
|
+
}),
|
|
1168
|
+
)
|
|
1169
|
+
}),
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1172
|
+
cleanupFns.push(
|
|
1173
|
+
aiEventClient.on('hook:unregistered', (e) => {
|
|
1174
|
+
setState(
|
|
1175
|
+
'hooks',
|
|
1176
|
+
produce((hooks: HookRegistryState) => {
|
|
1177
|
+
applyHookEvent(hooks, 'hook:unregistered', e.payload)
|
|
1178
|
+
}),
|
|
1179
|
+
)
|
|
1180
|
+
}),
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
cleanupFns.push(
|
|
1184
|
+
aiEventClient.on('hook:state-snapshot', (e) => {
|
|
1185
|
+
setState(
|
|
1186
|
+
'hooks',
|
|
1187
|
+
produce((hooks: HookRegistryState) => {
|
|
1188
|
+
applyHookEvent(hooks, 'hook:state-snapshot', e.payload)
|
|
1189
|
+
}),
|
|
1190
|
+
)
|
|
1191
|
+
}),
|
|
1192
|
+
)
|
|
734
1193
|
|
|
735
|
-
|
|
1194
|
+
const recordRunEvent = (
|
|
1195
|
+
eventName:
|
|
1196
|
+
| 'run:created'
|
|
1197
|
+
| 'run:started'
|
|
1198
|
+
| 'run:updated'
|
|
1199
|
+
| 'run:completed'
|
|
1200
|
+
| 'run:errored'
|
|
1201
|
+
| 'run:cancelled',
|
|
1202
|
+
payload: RunLifecycleEvent,
|
|
1203
|
+
) => {
|
|
1204
|
+
setState(
|
|
1205
|
+
'hooks',
|
|
1206
|
+
produce((hooks: HookRegistryState) => {
|
|
1207
|
+
applyHookEvent(hooks, eventName, payload)
|
|
1208
|
+
}),
|
|
1209
|
+
)
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
cleanupFns.push(
|
|
1213
|
+
aiEventClient.on('run:created', (e) => {
|
|
1214
|
+
recordRunEvent('run:created', e.payload)
|
|
1215
|
+
}),
|
|
1216
|
+
aiEventClient.on('run:started', (e) => {
|
|
1217
|
+
recordRunEvent('run:started', e.payload)
|
|
1218
|
+
}),
|
|
1219
|
+
aiEventClient.on('run:updated', (e) => {
|
|
1220
|
+
recordRunEvent('run:updated', e.payload)
|
|
1221
|
+
}),
|
|
1222
|
+
aiEventClient.on('run:completed', (e) => {
|
|
1223
|
+
recordRunEvent('run:completed', e.payload)
|
|
1224
|
+
}),
|
|
1225
|
+
aiEventClient.on('run:errored', (e) => {
|
|
1226
|
+
recordRunEvent('run:errored', e.payload)
|
|
1227
|
+
}),
|
|
1228
|
+
aiEventClient.on('run:cancelled', (e) => {
|
|
1229
|
+
recordRunEvent('run:cancelled', e.payload)
|
|
1230
|
+
}),
|
|
1231
|
+
)
|
|
1232
|
+
|
|
1233
|
+
cleanupFns.push(
|
|
1234
|
+
aiEventClient.on('tools:registered', (e) => {
|
|
1235
|
+
setState(
|
|
1236
|
+
'hooks',
|
|
1237
|
+
produce((hooks: HookRegistryState) => {
|
|
1238
|
+
applyHookEvent(hooks, 'tools:registered', e.payload)
|
|
1239
|
+
}),
|
|
1240
|
+
)
|
|
1241
|
+
}),
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
cleanupFns.push(
|
|
1245
|
+
aiEventClient.on('devtools:tool-fixture:apply', (e) => {
|
|
1246
|
+
setState(
|
|
1247
|
+
'hooks',
|
|
1248
|
+
produce((hooks: HookRegistryState) => {
|
|
1249
|
+
applyHookEvent(hooks, 'devtools:tool-fixture:apply', e.payload)
|
|
1250
|
+
}),
|
|
1251
|
+
)
|
|
1252
|
+
}),
|
|
1253
|
+
)
|
|
736
1254
|
|
|
737
1255
|
cleanupFns.push(
|
|
738
1256
|
aiEventClient.on('client:created', (e) => {
|
|
@@ -746,8 +1264,6 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
746
1264
|
}),
|
|
747
1265
|
)
|
|
748
1266
|
|
|
749
|
-
// ============= Message Events =============
|
|
750
|
-
|
|
751
1267
|
cleanupFns.push(
|
|
752
1268
|
aiEventClient.on('text:message:created', (e) => {
|
|
753
1269
|
const {
|
|
@@ -764,13 +1280,23 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
764
1280
|
(streamId ? streamToConversation.get(streamId) : undefined)
|
|
765
1281
|
|
|
766
1282
|
if (!conversationId) return
|
|
1283
|
+
if (clientId && streamId) {
|
|
1284
|
+
streamToConversation.set(streamId, clientId)
|
|
1285
|
+
}
|
|
767
1286
|
if (role === 'tool' || role === 'system') return
|
|
768
1287
|
|
|
1288
|
+
const source = normalizeMessageSource(
|
|
1289
|
+
e.payload.source,
|
|
1290
|
+
clientId ? 'client' : 'server',
|
|
1291
|
+
)
|
|
1292
|
+
const conversationType =
|
|
1293
|
+
clientId && source !== 'server' ? 'client' : 'server'
|
|
1294
|
+
|
|
769
1295
|
if (!state.conversations[conversationId]) {
|
|
770
1296
|
getOrCreateConversation(
|
|
771
1297
|
conversationId,
|
|
772
|
-
|
|
773
|
-
|
|
1298
|
+
conversationType,
|
|
1299
|
+
conversationType === 'client'
|
|
774
1300
|
? `Client Chat (${conversationId.substring(0, 8)})`
|
|
775
1301
|
: `Server Chat (${conversationId.substring(0, 8)})`,
|
|
776
1302
|
)
|
|
@@ -783,76 +1309,111 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
783
1309
|
(message) => message.id === messageId,
|
|
784
1310
|
)
|
|
785
1311
|
|
|
786
|
-
const parts =
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
if (part.type === 'tool-call') {
|
|
792
|
-
return {
|
|
793
|
-
type: 'tool-call',
|
|
794
|
-
toolCallId: part.id,
|
|
795
|
-
toolName: part.name,
|
|
796
|
-
arguments: part.arguments,
|
|
797
|
-
state: part.state,
|
|
798
|
-
output: part.output,
|
|
799
|
-
content: part.approval
|
|
800
|
-
? JSON.stringify(part.approval)
|
|
801
|
-
: undefined,
|
|
1312
|
+
const parts =
|
|
1313
|
+
e.payload.parts
|
|
1314
|
+
?.map((part): MessagePart | null => {
|
|
1315
|
+
if (part.type === 'text') {
|
|
1316
|
+
return { type: 'text', content: part.content }
|
|
802
1317
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1318
|
+
if (part.type === 'tool-call') {
|
|
1319
|
+
return {
|
|
1320
|
+
type: 'tool-call',
|
|
1321
|
+
toolCallId: part.id,
|
|
1322
|
+
toolName: part.name,
|
|
1323
|
+
arguments: part.arguments,
|
|
1324
|
+
state: part.state,
|
|
1325
|
+
output: part.output,
|
|
1326
|
+
approval: part.approval,
|
|
1327
|
+
content: part.approval
|
|
1328
|
+
? JSON.stringify(part.approval)
|
|
1329
|
+
: undefined,
|
|
1330
|
+
}
|
|
811
1331
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1332
|
+
if (part.type === 'tool-result') {
|
|
1333
|
+
return {
|
|
1334
|
+
type: 'tool-result',
|
|
1335
|
+
toolCallId: part.toolCallId,
|
|
1336
|
+
content: part.content,
|
|
1337
|
+
state: part.state,
|
|
1338
|
+
error: part.error,
|
|
1339
|
+
}
|
|
817
1340
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
part.type === 'audio' ||
|
|
824
|
-
part.type === 'video'
|
|
825
|
-
) {
|
|
826
|
-
return {
|
|
827
|
-
type: part.type,
|
|
828
|
-
source: part.source,
|
|
829
|
-
metadata: part.metadata,
|
|
1341
|
+
if (part.type === 'thinking') {
|
|
1342
|
+
return {
|
|
1343
|
+
type: 'thinking',
|
|
1344
|
+
content: part.content,
|
|
1345
|
+
}
|
|
830
1346
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1347
|
+
if (part.type === 'structured-output') {
|
|
1348
|
+
return {
|
|
1349
|
+
type: 'structured-output',
|
|
1350
|
+
status: part.status,
|
|
1351
|
+
raw: part.raw,
|
|
1352
|
+
partial: part.partial,
|
|
1353
|
+
data: part.data,
|
|
1354
|
+
reasoning: part.reasoning,
|
|
1355
|
+
errorMessage: part.errorMessage,
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
// Handle multimodal parts (image, audio, video)
|
|
1359
|
+
// These have a source property instead of content
|
|
1360
|
+
if (
|
|
1361
|
+
part.type === 'image' ||
|
|
1362
|
+
part.type === 'audio' ||
|
|
1363
|
+
part.type === 'video'
|
|
1364
|
+
) {
|
|
1365
|
+
return {
|
|
1366
|
+
type: part.type,
|
|
1367
|
+
source: part.source,
|
|
1368
|
+
metadata: part.metadata,
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
// Fallback for any unknown part types - skip them
|
|
1372
|
+
return null
|
|
1373
|
+
})
|
|
1374
|
+
.filter((part): part is MessagePart => part !== null) ?? []
|
|
836
1375
|
|
|
837
|
-
const
|
|
1376
|
+
const toolCallsFromPayload = e.payload.toolCalls?.map((toolCall) => ({
|
|
838
1377
|
id: toolCall.id,
|
|
839
1378
|
name: toolCall.function.name,
|
|
840
1379
|
arguments: toolCall.function.arguments,
|
|
841
1380
|
state: 'input-complete',
|
|
842
1381
|
}))
|
|
843
|
-
|
|
844
|
-
|
|
1382
|
+
const toolCallsFromParts = parts
|
|
1383
|
+
.filter((part) => part.type === 'tool-call')
|
|
1384
|
+
.map((part) => ({
|
|
1385
|
+
id: part.toolCallId ?? `${messageId}:${part.toolName ?? 'tool'}`,
|
|
1386
|
+
name: part.toolName ?? 'tool',
|
|
1387
|
+
arguments: part.arguments ?? stringifyToolArguments(part.output),
|
|
1388
|
+
state: part.state ?? 'input-complete',
|
|
1389
|
+
...(part.output !== undefined ? { result: part.output } : {}),
|
|
1390
|
+
...(part.approval?.needsApproval !== undefined
|
|
1391
|
+
? { approvalRequired: part.approval.needsApproval }
|
|
1392
|
+
: {}),
|
|
1393
|
+
...(part.approval?.id ? { approvalId: part.approval.id } : {}),
|
|
1394
|
+
...(part.approval?.approved !== undefined
|
|
1395
|
+
? { approvalApproved: part.approval.approved }
|
|
1396
|
+
: {}),
|
|
1397
|
+
}))
|
|
1398
|
+
const toolCalls =
|
|
1399
|
+
toolCallsFromPayload && toolCallsFromPayload.length > 0
|
|
1400
|
+
? toolCallsFromPayload
|
|
1401
|
+
: toolCallsFromParts.length > 0
|
|
1402
|
+
? toolCallsFromParts
|
|
1403
|
+
: undefined
|
|
845
1404
|
|
|
846
1405
|
if (role === 'user' && conv.type === 'client' && source === 'server') {
|
|
847
1406
|
return
|
|
848
1407
|
}
|
|
849
1408
|
|
|
850
|
-
// Skip empty assistant messages from client - the real content comes from server
|
|
851
1409
|
if (
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1410
|
+
shouldSkipClientAssistantPlaceholder({
|
|
1411
|
+
role,
|
|
1412
|
+
source,
|
|
1413
|
+
content,
|
|
1414
|
+
toolCalls,
|
|
1415
|
+
parts,
|
|
1416
|
+
})
|
|
856
1417
|
) {
|
|
857
1418
|
return
|
|
858
1419
|
}
|
|
@@ -928,7 +1489,10 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
928
1489
|
|
|
929
1490
|
if (existingIndex >= 0) return
|
|
930
1491
|
|
|
931
|
-
const source =
|
|
1492
|
+
const source = normalizeMessageSource(
|
|
1493
|
+
e.payload.source,
|
|
1494
|
+
clientId ? 'client' : 'server',
|
|
1495
|
+
)
|
|
932
1496
|
|
|
933
1497
|
if (conv.type === 'client' && source === 'server') {
|
|
934
1498
|
return
|
|
@@ -1003,8 +1567,6 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
1003
1567
|
}),
|
|
1004
1568
|
)
|
|
1005
1569
|
|
|
1006
|
-
// ============= Tool Events =============
|
|
1007
|
-
|
|
1008
1570
|
cleanupFns.push(
|
|
1009
1571
|
aiEventClient.on('tools:result:added', (e) => {
|
|
1010
1572
|
const {
|
|
@@ -1068,31 +1630,59 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
1068
1630
|
|
|
1069
1631
|
cleanupFns.push(
|
|
1070
1632
|
aiEventClient.on('tools:approval:responded', (e) => {
|
|
1071
|
-
const {
|
|
1633
|
+
const {
|
|
1634
|
+
clientId,
|
|
1635
|
+
threadId,
|
|
1636
|
+
streamId,
|
|
1637
|
+
toolCallId,
|
|
1638
|
+
approvalId,
|
|
1639
|
+
approved,
|
|
1640
|
+
timestamp,
|
|
1641
|
+
} = e.payload
|
|
1072
1642
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1643
|
+
const conversationIds = getToolEventConversationIds({
|
|
1644
|
+
clientId,
|
|
1645
|
+
threadId,
|
|
1646
|
+
streamId,
|
|
1647
|
+
toolCallId,
|
|
1648
|
+
approvalId,
|
|
1649
|
+
})
|
|
1075
1650
|
|
|
1076
|
-
const
|
|
1651
|
+
for (const conversationId of conversationIds) {
|
|
1652
|
+
const conv = state.conversations[conversationId]
|
|
1653
|
+
if (!conv) continue
|
|
1077
1654
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
const message = conv.messages[messageIndex]
|
|
1084
|
-
if (!message?.toolCalls) continue
|
|
1655
|
+
const location = findToolCallLocation(conversationId, {
|
|
1656
|
+
toolCallId,
|
|
1657
|
+
approvalId,
|
|
1658
|
+
})
|
|
1659
|
+
if (!location) continue
|
|
1085
1660
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1661
|
+
updateToolCall(
|
|
1662
|
+
conversationId,
|
|
1663
|
+
location.messageIndex,
|
|
1664
|
+
location.toolCallIndex,
|
|
1665
|
+
{
|
|
1091
1666
|
state: approved ? 'approved' : 'denied',
|
|
1092
1667
|
approvalRequired: false,
|
|
1093
|
-
|
|
1094
|
-
|
|
1668
|
+
approvalId,
|
|
1669
|
+
approvalApproved: approved,
|
|
1670
|
+
},
|
|
1671
|
+
)
|
|
1672
|
+
|
|
1673
|
+
const message = conv.messages[location.messageIndex]
|
|
1674
|
+
const chunk: Chunk = {
|
|
1675
|
+
id: `chunk-${Date.now()}-${Math.random()}`,
|
|
1676
|
+
type: 'approval',
|
|
1677
|
+
...(message?.id ? { messageId: message.id } : {}),
|
|
1678
|
+
toolCallId,
|
|
1679
|
+
approvalId,
|
|
1680
|
+
approved,
|
|
1681
|
+
timestamp,
|
|
1682
|
+
chunkCount: 1,
|
|
1095
1683
|
}
|
|
1684
|
+
|
|
1685
|
+
addChunkToMessage(conversationId, chunk)
|
|
1096
1686
|
}
|
|
1097
1687
|
}),
|
|
1098
1688
|
)
|
|
@@ -1113,12 +1703,40 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
1113
1703
|
clientId ||
|
|
1114
1704
|
(streamId ? streamToConversation.get(streamId) : undefined)
|
|
1115
1705
|
if (!conversationId || !state.conversations[conversationId]) return
|
|
1706
|
+
if (clientId && streamId) {
|
|
1707
|
+
streamToConversation.set(streamId, clientId)
|
|
1708
|
+
}
|
|
1116
1709
|
|
|
1117
1710
|
const conv = state.conversations[conversationId]
|
|
1118
1711
|
const messageIndex = conv.messages.findIndex(
|
|
1119
1712
|
(m: Message) => m.id === messageId,
|
|
1120
1713
|
)
|
|
1121
|
-
if (messageIndex === -1)
|
|
1714
|
+
if (messageIndex === -1) {
|
|
1715
|
+
const source = normalizeMessageSource(
|
|
1716
|
+
e.payload.source,
|
|
1717
|
+
clientId ? 'client' : 'server',
|
|
1718
|
+
)
|
|
1719
|
+
addMessage(
|
|
1720
|
+
conversationId,
|
|
1721
|
+
createClientToolCallMessage({
|
|
1722
|
+
messageId,
|
|
1723
|
+
toolCallId,
|
|
1724
|
+
toolName,
|
|
1725
|
+
arguments: args,
|
|
1726
|
+
state: toolCallState,
|
|
1727
|
+
timestamp: e.payload.timestamp,
|
|
1728
|
+
source,
|
|
1729
|
+
...(e.payload.requestId
|
|
1730
|
+
? { requestId: e.payload.requestId }
|
|
1731
|
+
: {}),
|
|
1732
|
+
}),
|
|
1733
|
+
)
|
|
1734
|
+
updateConversation(conversationId, {
|
|
1735
|
+
status: 'active',
|
|
1736
|
+
hasChat: true,
|
|
1737
|
+
})
|
|
1738
|
+
return
|
|
1739
|
+
}
|
|
1122
1740
|
|
|
1123
1741
|
const message = conv.messages[messageIndex]
|
|
1124
1742
|
if (!message) return
|
|
@@ -1253,13 +1871,146 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
1253
1871
|
}),
|
|
1254
1872
|
)
|
|
1255
1873
|
|
|
1256
|
-
|
|
1874
|
+
const recordStructuredOutput = (payload: {
|
|
1875
|
+
clientId?: string
|
|
1876
|
+
threadId?: string
|
|
1877
|
+
runId?: string
|
|
1878
|
+
streamId: string
|
|
1879
|
+
messageId: string
|
|
1880
|
+
requestId?: string
|
|
1881
|
+
timestamp: number
|
|
1882
|
+
source?: string
|
|
1883
|
+
status: 'streaming' | 'complete' | 'error'
|
|
1884
|
+
raw?: string
|
|
1885
|
+
partial?: unknown
|
|
1886
|
+
data?: unknown
|
|
1887
|
+
reasoning?: string
|
|
1888
|
+
errorMessage?: string
|
|
1889
|
+
delta?: string
|
|
1890
|
+
}) => {
|
|
1891
|
+
const {
|
|
1892
|
+
clientId,
|
|
1893
|
+
threadId,
|
|
1894
|
+
runId,
|
|
1895
|
+
streamId,
|
|
1896
|
+
messageId,
|
|
1897
|
+
requestId,
|
|
1898
|
+
timestamp,
|
|
1899
|
+
} = payload
|
|
1900
|
+
|
|
1901
|
+
if (clientId && streamId) {
|
|
1902
|
+
streamToConversation.set(streamId, clientId)
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
const source = normalizeMessageSource(
|
|
1906
|
+
payload.source,
|
|
1907
|
+
clientId ? 'client' : 'server',
|
|
1908
|
+
)
|
|
1909
|
+
const conversationIds = getStructuredOutputConversationIds({
|
|
1910
|
+
clientId,
|
|
1911
|
+
threadId,
|
|
1912
|
+
streamId,
|
|
1913
|
+
messageId,
|
|
1914
|
+
})
|
|
1915
|
+
|
|
1916
|
+
for (const conversationId of conversationIds) {
|
|
1917
|
+
if (!state.conversations[conversationId]) {
|
|
1918
|
+
getOrCreateConversation(
|
|
1919
|
+
conversationId,
|
|
1920
|
+
conversationId === clientId && source === 'client'
|
|
1921
|
+
? 'client'
|
|
1922
|
+
: 'server',
|
|
1923
|
+
conversationId === clientId && source === 'client'
|
|
1924
|
+
? `Client Chat (${conversationId.substring(0, 8)})`
|
|
1925
|
+
: `Server Chat (${conversationId.substring(0, 8)})`,
|
|
1926
|
+
)
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
attachRunToConversation(conversationId, runId)
|
|
1930
|
+
const messageIndex = upsertStructuredOutputPart(conversationId, {
|
|
1931
|
+
messageId,
|
|
1932
|
+
timestamp,
|
|
1933
|
+
source:
|
|
1934
|
+
conversationId === clientId && source === 'client'
|
|
1935
|
+
? 'client'
|
|
1936
|
+
: 'server',
|
|
1937
|
+
...(requestId ? { requestId } : {}),
|
|
1938
|
+
status: payload.status,
|
|
1939
|
+
raw: payload.raw ?? '',
|
|
1940
|
+
...(payload.partial !== undefined
|
|
1941
|
+
? { partial: payload.partial }
|
|
1942
|
+
: {}),
|
|
1943
|
+
...(payload.data !== undefined ? { data: payload.data } : {}),
|
|
1944
|
+
...(payload.reasoning !== undefined
|
|
1945
|
+
? { reasoning: payload.reasoning }
|
|
1946
|
+
: {}),
|
|
1947
|
+
...(payload.errorMessage !== undefined
|
|
1948
|
+
? { errorMessage: payload.errorMessage }
|
|
1949
|
+
: {}),
|
|
1950
|
+
})
|
|
1951
|
+
|
|
1952
|
+
const chunk: Chunk = {
|
|
1953
|
+
id: `chunk-structured-${messageId}-${Date.now()}-${Math.random()}`,
|
|
1954
|
+
type: 'structured_output',
|
|
1955
|
+
messageId,
|
|
1956
|
+
timestamp,
|
|
1957
|
+
chunkCount: 1,
|
|
1958
|
+
structuredStatus: payload.status,
|
|
1959
|
+
raw: payload.raw ?? '',
|
|
1960
|
+
...(payload.partial !== undefined
|
|
1961
|
+
? { partial: payload.partial }
|
|
1962
|
+
: {}),
|
|
1963
|
+
...(payload.data !== undefined ? { data: payload.data } : {}),
|
|
1964
|
+
...(payload.reasoning !== undefined
|
|
1965
|
+
? { reasoning: payload.reasoning }
|
|
1966
|
+
: {}),
|
|
1967
|
+
...(payload.errorMessage !== undefined
|
|
1968
|
+
? { errorMessage: payload.errorMessage }
|
|
1969
|
+
: {}),
|
|
1970
|
+
...(payload.delta !== undefined ? { delta: payload.delta } : {}),
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
const conv = state.conversations[conversationId]
|
|
1974
|
+
if (conv?.type === 'client' && messageIndex !== undefined) {
|
|
1975
|
+
queueMessageChunk(conversationId, messageIndex, chunk)
|
|
1976
|
+
} else if (conv?.type === 'client') {
|
|
1977
|
+
addChunkToMessage(conversationId, chunk)
|
|
1978
|
+
} else {
|
|
1979
|
+
addChunk(conversationId, chunk)
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
attachMessageToLatestIteration(conversationId, messageId, requestId)
|
|
1983
|
+
updateConversation(conversationId, {
|
|
1984
|
+
status: payload.status === 'error' ? 'error' : 'active',
|
|
1985
|
+
hasChat: true,
|
|
1986
|
+
})
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
cleanupFns.push(
|
|
1991
|
+
aiEventClient.on('structured-output:started', (e) => {
|
|
1992
|
+
recordStructuredOutput(e.payload)
|
|
1993
|
+
}),
|
|
1994
|
+
aiEventClient.on('structured-output:updated', (e) => {
|
|
1995
|
+
recordStructuredOutput(e.payload)
|
|
1996
|
+
}),
|
|
1997
|
+
aiEventClient.on('structured-output:completed', (e) => {
|
|
1998
|
+
recordStructuredOutput(e.payload)
|
|
1999
|
+
}),
|
|
2000
|
+
aiEventClient.on('structured-output:errored', (e) => {
|
|
2001
|
+
recordStructuredOutput(e.payload)
|
|
2002
|
+
}),
|
|
2003
|
+
)
|
|
1257
2004
|
|
|
1258
2005
|
cleanupFns.push(
|
|
1259
2006
|
aiEventClient.on('text:chunk:content', (e) => {
|
|
1260
2007
|
const streamId = e.payload.streamId
|
|
1261
|
-
const
|
|
2008
|
+
const clientId = e.payload.clientId
|
|
2009
|
+
const conversationId = clientId || streamToConversation.get(streamId)
|
|
1262
2010
|
if (!conversationId) return
|
|
2011
|
+
if (clientId) {
|
|
2012
|
+
streamToConversation.set(streamId, clientId)
|
|
2013
|
+
}
|
|
1263
2014
|
|
|
1264
2015
|
const chunk: Chunk = {
|
|
1265
2016
|
id: `chunk-${Date.now()}-${Math.random()}`,
|
|
@@ -1299,8 +2050,12 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
1299
2050
|
cleanupFns.push(
|
|
1300
2051
|
aiEventClient.on('text:chunk:tool-call', (e) => {
|
|
1301
2052
|
const streamId = e.payload.streamId
|
|
1302
|
-
const
|
|
2053
|
+
const clientId = e.payload.clientId
|
|
2054
|
+
const conversationId = clientId || streamToConversation.get(streamId)
|
|
1303
2055
|
if (!conversationId) return
|
|
2056
|
+
if (clientId) {
|
|
2057
|
+
streamToConversation.set(streamId, clientId)
|
|
2058
|
+
}
|
|
1304
2059
|
|
|
1305
2060
|
const chunk: Chunk = {
|
|
1306
2061
|
id: `chunk-${Date.now()}-${Math.random()}`,
|
|
@@ -1366,8 +2121,12 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
1366
2121
|
cleanupFns.push(
|
|
1367
2122
|
aiEventClient.on('text:chunk:tool-result', (e) => {
|
|
1368
2123
|
const streamId = e.payload.streamId
|
|
1369
|
-
const
|
|
2124
|
+
const clientId = e.payload.clientId
|
|
2125
|
+
const conversationId = clientId || streamToConversation.get(streamId)
|
|
1370
2126
|
if (!conversationId) return
|
|
2127
|
+
if (clientId) {
|
|
2128
|
+
streamToConversation.set(streamId, clientId)
|
|
2129
|
+
}
|
|
1371
2130
|
|
|
1372
2131
|
const chunk: Chunk = {
|
|
1373
2132
|
id: `chunk-${Date.now()}-${Math.random()}`,
|
|
@@ -1415,8 +2174,12 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
1415
2174
|
cleanupFns.push(
|
|
1416
2175
|
aiEventClient.on('text:chunk:thinking', (e) => {
|
|
1417
2176
|
const streamId = e.payload.streamId
|
|
1418
|
-
const
|
|
2177
|
+
const clientId = e.payload.clientId
|
|
2178
|
+
const conversationId = clientId || streamToConversation.get(streamId)
|
|
1419
2179
|
if (!conversationId) return
|
|
2180
|
+
if (clientId) {
|
|
2181
|
+
streamToConversation.set(streamId, clientId)
|
|
2182
|
+
}
|
|
1420
2183
|
|
|
1421
2184
|
const chunk: Chunk = {
|
|
1422
2185
|
id: `chunk-${Date.now()}-${Math.random()}`,
|
|
@@ -1457,8 +2220,12 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
1457
2220
|
cleanupFns.push(
|
|
1458
2221
|
aiEventClient.on('text:chunk:done', (e) => {
|
|
1459
2222
|
const streamId = e.payload.streamId
|
|
1460
|
-
const
|
|
2223
|
+
const clientId = e.payload.clientId
|
|
2224
|
+
const conversationId = clientId || streamToConversation.get(streamId)
|
|
1461
2225
|
if (!conversationId) return
|
|
2226
|
+
if (clientId) {
|
|
2227
|
+
streamToConversation.set(streamId, clientId)
|
|
2228
|
+
}
|
|
1462
2229
|
|
|
1463
2230
|
const chunk: Chunk = {
|
|
1464
2231
|
id: `chunk-${Date.now()}-${Math.random()}`,
|
|
@@ -1540,8 +2307,12 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
1540
2307
|
cleanupFns.push(
|
|
1541
2308
|
aiEventClient.on('text:chunk:error', (e) => {
|
|
1542
2309
|
const streamId = e.payload.streamId
|
|
1543
|
-
const
|
|
2310
|
+
const clientId = e.payload.clientId
|
|
2311
|
+
const conversationId = clientId || streamToConversation.get(streamId)
|
|
1544
2312
|
if (!conversationId) return
|
|
2313
|
+
if (clientId) {
|
|
2314
|
+
streamToConversation.set(streamId, clientId)
|
|
2315
|
+
}
|
|
1545
2316
|
|
|
1546
2317
|
const chunk: Chunk = {
|
|
1547
2318
|
id: `chunk-${Date.now()}-${Math.random()}`,
|
|
@@ -1613,62 +2384,164 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
1613
2384
|
approvalId,
|
|
1614
2385
|
timestamp,
|
|
1615
2386
|
clientId,
|
|
2387
|
+
threadId,
|
|
1616
2388
|
} = e.payload
|
|
1617
2389
|
|
|
1618
|
-
const
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
2390
|
+
const source = normalizeMessageSource(
|
|
2391
|
+
e.payload.source,
|
|
2392
|
+
clientId ? 'client' : 'server',
|
|
2393
|
+
)
|
|
2394
|
+
if (clientId && streamId) {
|
|
2395
|
+
streamToConversation.set(streamId, clientId)
|
|
2396
|
+
}
|
|
1624
2397
|
|
|
1625
|
-
const
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
2398
|
+
const conversationIds = getToolEventConversationIds({
|
|
2399
|
+
clientId,
|
|
2400
|
+
threadId,
|
|
2401
|
+
streamId,
|
|
1629
2402
|
toolCallId,
|
|
1630
|
-
toolName,
|
|
1631
2403
|
approvalId,
|
|
1632
|
-
|
|
1633
|
-
timestamp,
|
|
1634
|
-
chunkCount: 1,
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
if (conv.type === 'client') {
|
|
1638
|
-
addChunkToMessage(conversationId, chunk)
|
|
1639
|
-
} else {
|
|
1640
|
-
addChunk(conversationId, chunk)
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
for (let i = conv.messages.length - 1; i >= 0; i--) {
|
|
1644
|
-
const message = conv.messages[i]
|
|
1645
|
-
if (!message) continue
|
|
2404
|
+
})
|
|
1646
2405
|
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
2406
|
+
for (const conversationId of conversationIds) {
|
|
2407
|
+
if (!state.conversations[conversationId]) {
|
|
2408
|
+
getOrCreateConversation(
|
|
2409
|
+
conversationId,
|
|
2410
|
+
conversationId === clientId && source === 'client'
|
|
2411
|
+
? 'client'
|
|
2412
|
+
: 'server',
|
|
2413
|
+
conversationId === clientId && source === 'client'
|
|
2414
|
+
? `Client Chat (${conversationId.substring(0, 8)})`
|
|
2415
|
+
: `Server Chat (${conversationId.substring(0, 8)})`,
|
|
1650
2416
|
)
|
|
1651
|
-
|
|
1652
|
-
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
let resolvedMessageId = messageId
|
|
2420
|
+
const location = findToolCallLocation(conversationId, { toolCallId })
|
|
2421
|
+
if (location) {
|
|
2422
|
+
updateToolCall(
|
|
2423
|
+
conversationId,
|
|
2424
|
+
location.messageIndex,
|
|
2425
|
+
location.toolCallIndex,
|
|
2426
|
+
{
|
|
1653
2427
|
approvalRequired: true,
|
|
1654
2428
|
approvalId,
|
|
1655
2429
|
state: 'approval-requested',
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
|
|
2430
|
+
},
|
|
2431
|
+
)
|
|
2432
|
+
resolvedMessageId =
|
|
2433
|
+
state.conversations[conversationId]?.messages[
|
|
2434
|
+
location.messageIndex
|
|
2435
|
+
]?.id ?? messageId
|
|
2436
|
+
} else {
|
|
2437
|
+
resolvedMessageId = messageId || `approval-message-${toolCallId}`
|
|
2438
|
+
addMessage(
|
|
2439
|
+
conversationId,
|
|
2440
|
+
createClientToolCallMessage({
|
|
2441
|
+
messageId: resolvedMessageId,
|
|
2442
|
+
toolCallId,
|
|
2443
|
+
toolName,
|
|
2444
|
+
arguments: stringifyToolArguments(input),
|
|
2445
|
+
state: 'approval-requested',
|
|
2446
|
+
timestamp,
|
|
2447
|
+
source:
|
|
2448
|
+
conversationId === clientId && source === 'client'
|
|
2449
|
+
? 'client'
|
|
2450
|
+
: 'server',
|
|
2451
|
+
approvalRequired: true,
|
|
2452
|
+
approvalId,
|
|
2453
|
+
}),
|
|
2454
|
+
)
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
const chunk: Chunk = {
|
|
2458
|
+
id: `chunk-${Date.now()}-${Math.random()}`,
|
|
2459
|
+
type: 'approval',
|
|
2460
|
+
...(resolvedMessageId ? { messageId: resolvedMessageId } : {}),
|
|
2461
|
+
toolCallId,
|
|
2462
|
+
toolName,
|
|
2463
|
+
approvalId,
|
|
2464
|
+
input,
|
|
2465
|
+
timestamp,
|
|
2466
|
+
chunkCount: 1,
|
|
1659
2467
|
}
|
|
2468
|
+
|
|
2469
|
+
if (state.conversations[conversationId]?.type === 'client') {
|
|
2470
|
+
addChunkToMessage(conversationId, chunk)
|
|
2471
|
+
} else {
|
|
2472
|
+
addChunk(conversationId, chunk)
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
if (conversationIds.length === 0) {
|
|
2477
|
+
const fallbackConversationId = clientId || threadId
|
|
2478
|
+
if (!fallbackConversationId) return
|
|
2479
|
+
|
|
2480
|
+
getOrCreateConversation(
|
|
2481
|
+
fallbackConversationId,
|
|
2482
|
+
clientId ? 'client' : 'server',
|
|
2483
|
+
clientId
|
|
2484
|
+
? `Client Chat (${fallbackConversationId.substring(0, 8)})`
|
|
2485
|
+
: `Server Chat (${fallbackConversationId.substring(0, 8)})`,
|
|
2486
|
+
)
|
|
2487
|
+
const resolvedMessageId =
|
|
2488
|
+
messageId || `approval-message-${toolCallId}`
|
|
2489
|
+
addMessage(
|
|
2490
|
+
fallbackConversationId,
|
|
2491
|
+
createClientToolCallMessage({
|
|
2492
|
+
messageId: resolvedMessageId,
|
|
2493
|
+
toolCallId,
|
|
2494
|
+
toolName,
|
|
2495
|
+
arguments: stringifyToolArguments(input),
|
|
2496
|
+
state: 'approval-requested',
|
|
2497
|
+
timestamp,
|
|
2498
|
+
source: clientId ? 'client' : 'server',
|
|
2499
|
+
approvalRequired: true,
|
|
2500
|
+
approvalId,
|
|
2501
|
+
}),
|
|
2502
|
+
)
|
|
2503
|
+
addChunkToMessage(fallbackConversationId, {
|
|
2504
|
+
id: `chunk-${Date.now()}-${Math.random()}`,
|
|
2505
|
+
type: 'approval',
|
|
2506
|
+
messageId: resolvedMessageId,
|
|
2507
|
+
toolCallId,
|
|
2508
|
+
toolName,
|
|
2509
|
+
approvalId,
|
|
2510
|
+
input,
|
|
2511
|
+
timestamp,
|
|
2512
|
+
chunkCount: 1,
|
|
2513
|
+
})
|
|
1660
2514
|
}
|
|
1661
2515
|
}),
|
|
1662
2516
|
)
|
|
1663
2517
|
|
|
1664
|
-
// ============= Chat Events (for usage tracking) =============
|
|
1665
|
-
|
|
1666
2518
|
cleanupFns.push(
|
|
1667
2519
|
aiEventClient.on('text:request:started', (e) => {
|
|
1668
2520
|
const streamId = e.payload.streamId
|
|
1669
2521
|
const model = e.payload.model
|
|
1670
2522
|
const provider = e.payload.provider
|
|
1671
2523
|
const clientId = e.payload.clientId
|
|
2524
|
+
const source = normalizeMessageSource(e.payload.source, 'server')
|
|
2525
|
+
const threadConversationId =
|
|
2526
|
+
source === 'server' ? (e.payload.threadId ?? clientId) : undefined
|
|
2527
|
+
|
|
2528
|
+
if (threadConversationId) {
|
|
2529
|
+
getOrCreateConversation(
|
|
2530
|
+
threadConversationId,
|
|
2531
|
+
'server',
|
|
2532
|
+
`Server Chat (${threadConversationId.substring(0, 8)})`,
|
|
2533
|
+
)
|
|
2534
|
+
streamToConversation.set(streamId, threadConversationId)
|
|
2535
|
+
requestToConversation.set(e.payload.requestId, threadConversationId)
|
|
2536
|
+
updateConversation(threadConversationId, {
|
|
2537
|
+
status: 'active',
|
|
2538
|
+
...e.payload,
|
|
2539
|
+
systemPrompts: e.payload.systemPrompts,
|
|
2540
|
+
hasChat: true,
|
|
2541
|
+
})
|
|
2542
|
+
attachRunToConversation(threadConversationId, e.payload.runId)
|
|
2543
|
+
return
|
|
2544
|
+
}
|
|
1672
2545
|
|
|
1673
2546
|
if (clientId && state.conversations[clientId]) {
|
|
1674
2547
|
streamToConversation.set(streamId, clientId)
|
|
@@ -1679,6 +2552,7 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
1679
2552
|
systemPrompts: e.payload.systemPrompts,
|
|
1680
2553
|
hasChat: true,
|
|
1681
2554
|
})
|
|
2555
|
+
attachRunToConversation(clientId, e.payload.runId)
|
|
1682
2556
|
return
|
|
1683
2557
|
}
|
|
1684
2558
|
|
|
@@ -1694,6 +2568,7 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
1694
2568
|
systemPrompts: e.payload.systemPrompts,
|
|
1695
2569
|
hasChat: true,
|
|
1696
2570
|
})
|
|
2571
|
+
attachRunToConversation(activeClient.id, e.payload.runId)
|
|
1697
2572
|
} else {
|
|
1698
2573
|
const existingServerConv = Object.values(state.conversations).find(
|
|
1699
2574
|
(c) => c.type === 'server' && c.model === model,
|
|
@@ -1711,6 +2586,7 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
1711
2586
|
systemPrompts: e.payload.systemPrompts,
|
|
1712
2587
|
hasChat: true,
|
|
1713
2588
|
})
|
|
2589
|
+
attachRunToConversation(existingServerConv.id, e.payload.runId)
|
|
1714
2590
|
} else {
|
|
1715
2591
|
const serverId = `server-${model}`
|
|
1716
2592
|
getOrCreateConversation(serverId, 'server', `${model} Server`)
|
|
@@ -1721,6 +2597,7 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
1721
2597
|
systemPrompts: e.payload.systemPrompts,
|
|
1722
2598
|
hasChat: true,
|
|
1723
2599
|
})
|
|
2600
|
+
attachRunToConversation(serverId, e.payload.runId)
|
|
1724
2601
|
}
|
|
1725
2602
|
}
|
|
1726
2603
|
}),
|
|
@@ -1790,8 +2667,6 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
1790
2667
|
}),
|
|
1791
2668
|
)
|
|
1792
2669
|
|
|
1793
|
-
// ============= Iteration Events =============
|
|
1794
|
-
|
|
1795
2670
|
cleanupFns.push(
|
|
1796
2671
|
aiEventClient.on('text:iteration:started', (e) => {
|
|
1797
2672
|
const { requestId, streamId, clientId, iteration, messageId } =
|
|
@@ -1901,8 +2776,6 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
1901
2776
|
}),
|
|
1902
2777
|
)
|
|
1903
2778
|
|
|
1904
|
-
// ============= Middleware Events =============
|
|
1905
|
-
|
|
1906
2779
|
/** Find the latest iteration for a given requestId, or the very latest iteration as fallback */
|
|
1907
2780
|
function findLatestIterationIndex(
|
|
1908
2781
|
conv: Conversation,
|
|
@@ -2026,8 +2899,6 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
2026
2899
|
}),
|
|
2027
2900
|
)
|
|
2028
2901
|
|
|
2029
|
-
// ============= Summarize Events =============
|
|
2030
|
-
|
|
2031
2902
|
cleanupFns.push(
|
|
2032
2903
|
aiEventClient.on('summarize:request:started', (e) => {
|
|
2033
2904
|
const { requestId, model, inputLength, timestamp, clientId } = e.payload
|
|
@@ -2105,8 +2976,6 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
2105
2976
|
}),
|
|
2106
2977
|
)
|
|
2107
2978
|
|
|
2108
|
-
// ============= Image Events =============
|
|
2109
|
-
|
|
2110
2979
|
cleanupFns.push(
|
|
2111
2980
|
aiEventClient.on('image:request:started', (e) => {
|
|
2112
2981
|
const { requestId, clientId, timestamp } = e.payload
|
|
@@ -2176,7 +3045,97 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
2176
3045
|
}),
|
|
2177
3046
|
)
|
|
2178
3047
|
|
|
2179
|
-
|
|
3048
|
+
cleanupFns.push(
|
|
3049
|
+
aiEventClient.on('audio:request:started', (e) => {
|
|
3050
|
+
const { requestId, clientId, timestamp } = e.payload
|
|
3051
|
+
|
|
3052
|
+
let conversationId = clientId
|
|
3053
|
+
if (!conversationId || !state.conversations[conversationId]) {
|
|
3054
|
+
conversationId = `audio-${requestId}`
|
|
3055
|
+
getOrCreateConversation(
|
|
3056
|
+
conversationId,
|
|
3057
|
+
'server',
|
|
3058
|
+
`Audio (${requestId.substring(0, 8)})`,
|
|
3059
|
+
)
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
addActivityEvent(conversationId, 'audioEvents', {
|
|
3063
|
+
id: requestId,
|
|
3064
|
+
name: 'audio:request:started',
|
|
3065
|
+
timestamp,
|
|
3066
|
+
payload: e.payload,
|
|
3067
|
+
})
|
|
3068
|
+
}),
|
|
3069
|
+
)
|
|
3070
|
+
|
|
3071
|
+
cleanupFns.push(
|
|
3072
|
+
aiEventClient.on('audio:request:completed', (e) => {
|
|
3073
|
+
const { requestId, clientId, timestamp } = e.payload
|
|
3074
|
+
|
|
3075
|
+
let conversationId = clientId
|
|
3076
|
+
if (!conversationId || !state.conversations[conversationId]) {
|
|
3077
|
+
conversationId = `audio-${requestId}`
|
|
3078
|
+
getOrCreateConversation(
|
|
3079
|
+
conversationId,
|
|
3080
|
+
'server',
|
|
3081
|
+
`Audio (${requestId.substring(0, 8)})`,
|
|
3082
|
+
)
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
addActivityEvent(conversationId, 'audioEvents', {
|
|
3086
|
+
id: requestId,
|
|
3087
|
+
name: 'audio:request:completed',
|
|
3088
|
+
timestamp,
|
|
3089
|
+
payload: e.payload,
|
|
3090
|
+
})
|
|
3091
|
+
}),
|
|
3092
|
+
)
|
|
3093
|
+
|
|
3094
|
+
cleanupFns.push(
|
|
3095
|
+
aiEventClient.on('audio:request:error', (e) => {
|
|
3096
|
+
const { requestId, clientId, timestamp } = e.payload
|
|
3097
|
+
|
|
3098
|
+
let conversationId = clientId
|
|
3099
|
+
if (!conversationId || !state.conversations[conversationId]) {
|
|
3100
|
+
conversationId = `audio-${requestId}`
|
|
3101
|
+
getOrCreateConversation(
|
|
3102
|
+
conversationId,
|
|
3103
|
+
'server',
|
|
3104
|
+
`Audio (${requestId.substring(0, 8)})`,
|
|
3105
|
+
)
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
addActivityEvent(conversationId, 'audioEvents', {
|
|
3109
|
+
id: requestId,
|
|
3110
|
+
name: 'audio:request:error',
|
|
3111
|
+
timestamp,
|
|
3112
|
+
payload: e.payload,
|
|
3113
|
+
})
|
|
3114
|
+
}),
|
|
3115
|
+
)
|
|
3116
|
+
|
|
3117
|
+
cleanupFns.push(
|
|
3118
|
+
aiEventClient.on('audio:usage', (e) => {
|
|
3119
|
+
const { requestId, clientId, timestamp } = e.payload
|
|
3120
|
+
|
|
3121
|
+
let conversationId = clientId
|
|
3122
|
+
if (!conversationId || !state.conversations[conversationId]) {
|
|
3123
|
+
conversationId = `audio-${requestId}`
|
|
3124
|
+
getOrCreateConversation(
|
|
3125
|
+
conversationId,
|
|
3126
|
+
'server',
|
|
3127
|
+
`Audio (${requestId.substring(0, 8)})`,
|
|
3128
|
+
)
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
addActivityEvent(conversationId, 'audioEvents', {
|
|
3132
|
+
id: requestId,
|
|
3133
|
+
name: 'audio:usage',
|
|
3134
|
+
timestamp,
|
|
3135
|
+
payload: e.payload,
|
|
3136
|
+
})
|
|
3137
|
+
}),
|
|
3138
|
+
)
|
|
2180
3139
|
|
|
2181
3140
|
cleanupFns.push(
|
|
2182
3141
|
aiEventClient.on('speech:request:started', (e) => {
|
|
@@ -2224,6 +3183,29 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
2224
3183
|
}),
|
|
2225
3184
|
)
|
|
2226
3185
|
|
|
3186
|
+
cleanupFns.push(
|
|
3187
|
+
aiEventClient.on('speech:request:error', (e) => {
|
|
3188
|
+
const { requestId, clientId, timestamp } = e.payload
|
|
3189
|
+
|
|
3190
|
+
let conversationId = clientId
|
|
3191
|
+
if (!conversationId || !state.conversations[conversationId]) {
|
|
3192
|
+
conversationId = `speech-${requestId}`
|
|
3193
|
+
getOrCreateConversation(
|
|
3194
|
+
conversationId,
|
|
3195
|
+
'server',
|
|
3196
|
+
`Speech (${requestId.substring(0, 8)})`,
|
|
3197
|
+
)
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
addActivityEvent(conversationId, 'speechEvents', {
|
|
3201
|
+
id: requestId,
|
|
3202
|
+
name: 'speech:request:error',
|
|
3203
|
+
timestamp,
|
|
3204
|
+
payload: e.payload,
|
|
3205
|
+
})
|
|
3206
|
+
}),
|
|
3207
|
+
)
|
|
3208
|
+
|
|
2227
3209
|
cleanupFns.push(
|
|
2228
3210
|
aiEventClient.on('speech:usage', (e) => {
|
|
2229
3211
|
const { requestId, clientId, timestamp } = e.payload
|
|
@@ -2247,8 +3229,6 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
2247
3229
|
}),
|
|
2248
3230
|
)
|
|
2249
3231
|
|
|
2250
|
-
// ============= Transcription Events =============
|
|
2251
|
-
|
|
2252
3232
|
cleanupFns.push(
|
|
2253
3233
|
aiEventClient.on('transcription:request:started', (e) => {
|
|
2254
3234
|
const { requestId, clientId, timestamp } = e.payload
|
|
@@ -2295,6 +3275,29 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
2295
3275
|
}),
|
|
2296
3276
|
)
|
|
2297
3277
|
|
|
3278
|
+
cleanupFns.push(
|
|
3279
|
+
aiEventClient.on('transcription:request:error', (e) => {
|
|
3280
|
+
const { requestId, clientId, timestamp } = e.payload
|
|
3281
|
+
|
|
3282
|
+
let conversationId = clientId
|
|
3283
|
+
if (!conversationId || !state.conversations[conversationId]) {
|
|
3284
|
+
conversationId = `transcription-${requestId}`
|
|
3285
|
+
getOrCreateConversation(
|
|
3286
|
+
conversationId,
|
|
3287
|
+
'server',
|
|
3288
|
+
`Transcription (${requestId.substring(0, 8)})`,
|
|
3289
|
+
)
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
addActivityEvent(conversationId, 'transcriptionEvents', {
|
|
3293
|
+
id: requestId,
|
|
3294
|
+
name: 'transcription:request:error',
|
|
3295
|
+
timestamp,
|
|
3296
|
+
payload: e.payload,
|
|
3297
|
+
})
|
|
3298
|
+
}),
|
|
3299
|
+
)
|
|
3300
|
+
|
|
2298
3301
|
cleanupFns.push(
|
|
2299
3302
|
aiEventClient.on('transcription:usage', (e) => {
|
|
2300
3303
|
const { requestId, clientId, timestamp } = e.payload
|
|
@@ -2318,8 +3321,6 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
2318
3321
|
}),
|
|
2319
3322
|
)
|
|
2320
3323
|
|
|
2321
|
-
// ============= Video Events =============
|
|
2322
|
-
|
|
2323
3324
|
cleanupFns.push(
|
|
2324
3325
|
aiEventClient.on('video:request:started', (e) => {
|
|
2325
3326
|
const { requestId, clientId, timestamp } = e.payload
|
|
@@ -2403,6 +3404,11 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
2403
3404
|
state,
|
|
2404
3405
|
clearAllConversations,
|
|
2405
3406
|
selectConversation,
|
|
3407
|
+
clearHooks,
|
|
3408
|
+
selectHook,
|
|
3409
|
+
saveToolFixture,
|
|
3410
|
+
deleteToolFixture,
|
|
3411
|
+
applyToolFixture,
|
|
2406
3412
|
}
|
|
2407
3413
|
|
|
2408
3414
|
return (
|