@tanstack/ai 0.2.2 → 0.3.1
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 +1 -0
- package/dist/esm/activities/chat/index.js +264 -186
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +22 -0
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/index.d.ts +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +22 -43
- package/dist/esm/activities/chat/stream/processor.js +93 -157
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +22 -21
- package/dist/esm/activities/chat/tools/tool-calls.js +40 -27
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/generateImage/index.js +41 -1
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/activities/generateSpeech/index.js +33 -1
- package/dist/esm/activities/generateSpeech/index.js.map +1 -1
- package/dist/esm/activities/generateTranscription/index.js +29 -1
- package/dist/esm/activities/generateTranscription/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.js +50 -0
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/summarize/index.js +9 -8
- package/dist/esm/activities/summarize/index.js.map +1 -1
- package/dist/esm/event-client.d.ts +378 -254
- package/dist/esm/event-client.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/stream-to-response.d.ts +1 -1
- package/dist/esm/stream-to-response.js +5 -3
- package/dist/esm/stream-to-response.js.map +1 -1
- package/dist/esm/types.d.ts +162 -46
- package/package.json +1 -1
- package/src/activities/chat/index.ts +362 -216
- package/src/activities/chat/messages.ts +40 -1
- package/src/activities/chat/stream/index.ts +1 -5
- package/src/activities/chat/stream/processor.ts +142 -270
- package/src/activities/chat/tools/tool-calls.ts +55 -49
- package/src/activities/generateImage/index.ts +47 -2
- package/src/activities/generateSpeech/index.ts +37 -1
- package/src/activities/generateTranscription/index.ts +33 -1
- package/src/activities/generateVideo/index.ts +54 -0
- package/src/activities/summarize/index.ts +10 -9
- package/src/event-client.ts +460 -256
- package/src/index.ts +2 -3
- package/src/stream-to-response.ts +8 -6
- package/src/types.ts +209 -65
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
parseWithStandardSchema,
|
|
15
15
|
} from './tools/schema-converter'
|
|
16
16
|
import { maxIterations as maxIterationsStrategy } from './agent-loop-strategies'
|
|
17
|
+
import { convertMessagesToModelMessages } from './messages'
|
|
17
18
|
import type {
|
|
18
19
|
ApprovalRequest,
|
|
19
20
|
ClientToolRequest,
|
|
@@ -23,14 +24,18 @@ import type { AnyTextAdapter } from './adapter'
|
|
|
23
24
|
import type {
|
|
24
25
|
AgentLoopStrategy,
|
|
25
26
|
ConstrainedModelMessage,
|
|
26
|
-
DoneStreamChunk,
|
|
27
27
|
InferSchemaType,
|
|
28
28
|
ModelMessage,
|
|
29
|
+
RunFinishedEvent,
|
|
29
30
|
SchemaInput,
|
|
30
31
|
StreamChunk,
|
|
32
|
+
TextMessageContentEvent,
|
|
31
33
|
TextOptions,
|
|
32
34
|
Tool,
|
|
33
35
|
ToolCall,
|
|
36
|
+
ToolCallArgsEvent,
|
|
37
|
+
ToolCallEndEvent,
|
|
38
|
+
ToolCallStartEvent,
|
|
34
39
|
} from '../../types'
|
|
35
40
|
|
|
36
41
|
// ===========================
|
|
@@ -213,11 +218,16 @@ class TextEngine<
|
|
|
213
218
|
private totalChunkCount = 0
|
|
214
219
|
private currentMessageId: string | null = null
|
|
215
220
|
private accumulatedContent = ''
|
|
216
|
-
private
|
|
221
|
+
private eventOptions?: Record<string, unknown>
|
|
222
|
+
private eventToolNames?: Array<string>
|
|
223
|
+
private finishedEvent: RunFinishedEvent | null = null
|
|
217
224
|
private shouldEmitStreamEnd = true
|
|
218
225
|
private earlyTermination = false
|
|
219
226
|
private toolPhase: ToolPhaseResult = 'continue'
|
|
220
227
|
private cyclePhase: CyclePhase = 'processText'
|
|
228
|
+
// Client state extracted from initial messages (before conversion to ModelMessage)
|
|
229
|
+
private readonly initialApprovals: Map<string, boolean>
|
|
230
|
+
private readonly initialClientToolResults: Map<string, any>
|
|
221
231
|
|
|
222
232
|
constructor(config: TextEngineConfig<TAdapter, TParams>) {
|
|
223
233
|
this.adapter = config.adapter
|
|
@@ -228,7 +238,21 @@ class TextEngine<
|
|
|
228
238
|
config.params.agentLoopStrategy || maxIterationsStrategy(5)
|
|
229
239
|
this.toolCallManager = new ToolCallManager(this.tools)
|
|
230
240
|
this.initialMessageCount = config.params.messages.length
|
|
231
|
-
|
|
241
|
+
|
|
242
|
+
// Extract client state (approvals, client tool results) from original messages BEFORE conversion
|
|
243
|
+
// This preserves UIMessage parts data that would be lost during conversion to ModelMessage
|
|
244
|
+
const { approvals, clientToolResults } =
|
|
245
|
+
this.extractClientStateFromOriginalMessages(
|
|
246
|
+
config.params.messages as Array<any>,
|
|
247
|
+
)
|
|
248
|
+
this.initialApprovals = approvals
|
|
249
|
+
this.initialClientToolResults = clientToolResults
|
|
250
|
+
|
|
251
|
+
// Convert messages to ModelMessage format (handles both UIMessage and ModelMessage input)
|
|
252
|
+
// This ensures consistent internal format regardless of what the client sends
|
|
253
|
+
this.messages = convertMessagesToModelMessages(
|
|
254
|
+
config.params.messages as Array<any>,
|
|
255
|
+
)
|
|
232
256
|
this.requestId = this.createId('chat')
|
|
233
257
|
this.streamId = this.createId('stream')
|
|
234
258
|
this.effectiveRequest = config.params.abortController
|
|
@@ -278,16 +302,7 @@ class TextEngine<
|
|
|
278
302
|
|
|
279
303
|
private beforeRun(): void {
|
|
280
304
|
this.streamStartTime = Date.now()
|
|
281
|
-
const {
|
|
282
|
-
model,
|
|
283
|
-
tools,
|
|
284
|
-
temperature,
|
|
285
|
-
topP,
|
|
286
|
-
maxTokens,
|
|
287
|
-
metadata,
|
|
288
|
-
modelOptions,
|
|
289
|
-
conversationId,
|
|
290
|
-
} = this.params
|
|
305
|
+
const { tools, temperature, topP, maxTokens, metadata } = this.params
|
|
291
306
|
|
|
292
307
|
// Gather flattened options into an object for event emission
|
|
293
308
|
const options: Record<string, unknown> = {}
|
|
@@ -296,26 +311,49 @@ class TextEngine<
|
|
|
296
311
|
if (maxTokens !== undefined) options.maxTokens = maxTokens
|
|
297
312
|
if (metadata !== undefined) options.metadata = metadata
|
|
298
313
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
messageCount: this.initialMessageCount,
|
|
305
|
-
hasTools: !!tools && tools.length > 0,
|
|
306
|
-
streaming: true,
|
|
314
|
+
this.eventOptions = Object.keys(options).length > 0 ? options : undefined
|
|
315
|
+
this.eventToolNames = tools?.map((t) => t.name)
|
|
316
|
+
|
|
317
|
+
aiEventClient.emit('text:request:started', {
|
|
318
|
+
...this.buildTextEventContext(),
|
|
307
319
|
timestamp: Date.now(),
|
|
308
|
-
clientId: conversationId,
|
|
309
|
-
toolNames: tools?.map((t) => t.name),
|
|
310
|
-
options: Object.keys(options).length > 0 ? options : undefined,
|
|
311
|
-
modelOptions: modelOptions as Record<string, unknown> | undefined,
|
|
312
320
|
})
|
|
313
321
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
322
|
+
// Always emit messages for tracking:
|
|
323
|
+
// - For existing conversations (with conversationId): only emit the latest user message
|
|
324
|
+
// - For new conversations (without conversationId): emit all messages for reconstruction
|
|
325
|
+
const messagesToEmit = this.params.conversationId
|
|
326
|
+
? this.messages.slice(-1).filter((m) => m.role === 'user')
|
|
327
|
+
: this.messages
|
|
328
|
+
|
|
329
|
+
messagesToEmit.forEach((message, index) => {
|
|
330
|
+
const messageIndex = this.params.conversationId
|
|
331
|
+
? this.messages.length - 1
|
|
332
|
+
: index
|
|
333
|
+
const messageId = this.createId('msg')
|
|
334
|
+
const baseContext = this.buildTextEventContext()
|
|
335
|
+
const content = this.getContentString(message.content)
|
|
336
|
+
|
|
337
|
+
aiEventClient.emit('text:message:created', {
|
|
338
|
+
...baseContext,
|
|
339
|
+
messageId,
|
|
340
|
+
role: message.role,
|
|
341
|
+
content,
|
|
342
|
+
toolCalls: message.toolCalls,
|
|
343
|
+
messageIndex,
|
|
344
|
+
timestamp: Date.now(),
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
if (message.role === 'user') {
|
|
348
|
+
aiEventClient.emit('text:message:user', {
|
|
349
|
+
...baseContext,
|
|
350
|
+
messageId,
|
|
351
|
+
role: 'user',
|
|
352
|
+
content,
|
|
353
|
+
messageIndex,
|
|
354
|
+
timestamp: Date.now(),
|
|
355
|
+
})
|
|
356
|
+
}
|
|
319
357
|
})
|
|
320
358
|
}
|
|
321
359
|
|
|
@@ -325,23 +363,13 @@ class TextEngine<
|
|
|
325
363
|
}
|
|
326
364
|
|
|
327
365
|
const now = Date.now()
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
requestId: this.requestId,
|
|
332
|
-
streamId: this.streamId,
|
|
333
|
-
model: this.params.model,
|
|
366
|
+
// Emit text:request:completed with final state
|
|
367
|
+
aiEventClient.emit('text:request:completed', {
|
|
368
|
+
...this.buildTextEventContext(),
|
|
334
369
|
content: this.accumulatedContent,
|
|
335
370
|
messageId: this.currentMessageId || undefined,
|
|
336
371
|
finishReason: this.lastFinishReason || undefined,
|
|
337
|
-
usage: this.
|
|
338
|
-
timestamp: now,
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
aiEventClient.emit('stream:ended', {
|
|
342
|
-
requestId: this.requestId,
|
|
343
|
-
streamId: this.streamId,
|
|
344
|
-
totalChunks: this.totalChunkCount,
|
|
372
|
+
usage: this.finishedEvent?.usage,
|
|
345
373
|
duration: now - this.streamStartTime,
|
|
346
374
|
timestamp: now,
|
|
347
375
|
})
|
|
@@ -366,7 +394,16 @@ class TextEngine<
|
|
|
366
394
|
private beginIteration(): void {
|
|
367
395
|
this.currentMessageId = this.createId('msg')
|
|
368
396
|
this.accumulatedContent = ''
|
|
369
|
-
this.
|
|
397
|
+
this.finishedEvent = null
|
|
398
|
+
|
|
399
|
+
const baseContext = this.buildTextEventContext()
|
|
400
|
+
aiEventClient.emit('text:message:created', {
|
|
401
|
+
...baseContext,
|
|
402
|
+
messageId: this.currentMessageId,
|
|
403
|
+
role: 'assistant',
|
|
404
|
+
content: '',
|
|
405
|
+
timestamp: Date.now(),
|
|
406
|
+
})
|
|
370
407
|
}
|
|
371
408
|
|
|
372
409
|
private async *streamModelResponse(): AsyncGenerator<StreamChunk> {
|
|
@@ -413,101 +450,96 @@ class TextEngine<
|
|
|
413
450
|
|
|
414
451
|
private handleStreamChunk(chunk: StreamChunk): void {
|
|
415
452
|
switch (chunk.type) {
|
|
416
|
-
|
|
417
|
-
|
|
453
|
+
// AG-UI Events
|
|
454
|
+
case 'TEXT_MESSAGE_CONTENT':
|
|
455
|
+
this.handleTextMessageContentEvent(chunk)
|
|
456
|
+
break
|
|
457
|
+
case 'TOOL_CALL_START':
|
|
458
|
+
this.handleToolCallStartEvent(chunk)
|
|
418
459
|
break
|
|
419
|
-
case '
|
|
420
|
-
this.
|
|
460
|
+
case 'TOOL_CALL_ARGS':
|
|
461
|
+
this.handleToolCallArgsEvent(chunk)
|
|
421
462
|
break
|
|
422
|
-
case '
|
|
423
|
-
this.
|
|
463
|
+
case 'TOOL_CALL_END':
|
|
464
|
+
this.handleToolCallEndEvent(chunk)
|
|
424
465
|
break
|
|
425
|
-
case '
|
|
426
|
-
this.
|
|
466
|
+
case 'RUN_FINISHED':
|
|
467
|
+
this.handleRunFinishedEvent(chunk)
|
|
427
468
|
break
|
|
428
|
-
case '
|
|
429
|
-
this.
|
|
469
|
+
case 'RUN_ERROR':
|
|
470
|
+
this.handleRunErrorEvent(chunk)
|
|
430
471
|
break
|
|
431
|
-
case '
|
|
432
|
-
this.
|
|
472
|
+
case 'STEP_FINISHED':
|
|
473
|
+
this.handleStepFinishedEvent(chunk)
|
|
433
474
|
break
|
|
475
|
+
|
|
434
476
|
default:
|
|
477
|
+
// RUN_STARTED, TEXT_MESSAGE_START, TEXT_MESSAGE_END, STEP_STARTED,
|
|
478
|
+
// STATE_SNAPSHOT, STATE_DELTA, CUSTOM
|
|
479
|
+
// - no special handling needed in chat activity
|
|
435
480
|
break
|
|
436
481
|
}
|
|
437
482
|
}
|
|
438
483
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
484
|
+
// ===========================
|
|
485
|
+
// AG-UI Event Handlers
|
|
486
|
+
// ===========================
|
|
487
|
+
|
|
488
|
+
private handleTextMessageContentEvent(chunk: TextMessageContentEvent): void {
|
|
489
|
+
if (chunk.content) {
|
|
490
|
+
this.accumulatedContent = chunk.content
|
|
491
|
+
} else {
|
|
492
|
+
this.accumulatedContent += chunk.delta
|
|
493
|
+
}
|
|
494
|
+
aiEventClient.emit('text:chunk:content', {
|
|
495
|
+
...this.buildTextEventContext(),
|
|
443
496
|
messageId: this.currentMessageId || undefined,
|
|
444
|
-
content:
|
|
497
|
+
content: this.accumulatedContent,
|
|
445
498
|
delta: chunk.delta,
|
|
446
499
|
timestamp: Date.now(),
|
|
447
500
|
})
|
|
448
501
|
}
|
|
449
502
|
|
|
450
|
-
private
|
|
451
|
-
chunk
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
aiEventClient.emit('stream:chunk:tool-call', {
|
|
455
|
-
streamId: this.streamId,
|
|
503
|
+
private handleToolCallStartEvent(chunk: ToolCallStartEvent): void {
|
|
504
|
+
this.toolCallManager.addToolCallStartEvent(chunk)
|
|
505
|
+
aiEventClient.emit('text:chunk:tool-call', {
|
|
506
|
+
...this.buildTextEventContext(),
|
|
456
507
|
messageId: this.currentMessageId || undefined,
|
|
457
|
-
toolCallId: chunk.
|
|
458
|
-
toolName: chunk.
|
|
459
|
-
index: chunk.index,
|
|
460
|
-
arguments:
|
|
508
|
+
toolCallId: chunk.toolCallId,
|
|
509
|
+
toolName: chunk.toolName,
|
|
510
|
+
index: chunk.index ?? 0,
|
|
511
|
+
arguments: '',
|
|
461
512
|
timestamp: Date.now(),
|
|
462
513
|
})
|
|
463
514
|
}
|
|
464
515
|
|
|
465
|
-
private
|
|
466
|
-
chunk
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
streamId: this.streamId,
|
|
516
|
+
private handleToolCallArgsEvent(chunk: ToolCallArgsEvent): void {
|
|
517
|
+
this.toolCallManager.addToolCallArgsEvent(chunk)
|
|
518
|
+
aiEventClient.emit('text:chunk:tool-call', {
|
|
519
|
+
...this.buildTextEventContext(),
|
|
470
520
|
messageId: this.currentMessageId || undefined,
|
|
471
521
|
toolCallId: chunk.toolCallId,
|
|
472
|
-
|
|
522
|
+
toolName: '',
|
|
523
|
+
index: 0,
|
|
524
|
+
arguments: chunk.delta,
|
|
473
525
|
timestamp: Date.now(),
|
|
474
526
|
})
|
|
475
527
|
}
|
|
476
528
|
|
|
477
|
-
private
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
this.
|
|
482
|
-
chunk.
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
streamId: this.streamId,
|
|
488
|
-
messageId: this.currentMessageId || undefined,
|
|
489
|
-
finishReason: chunk.finishReason,
|
|
490
|
-
usage: chunk.usage,
|
|
491
|
-
timestamp: Date.now(),
|
|
492
|
-
})
|
|
493
|
-
|
|
494
|
-
if (chunk.usage) {
|
|
495
|
-
aiEventClient.emit('usage:tokens', {
|
|
496
|
-
requestId: this.requestId,
|
|
497
|
-
streamId: this.streamId,
|
|
498
|
-
messageId: this.currentMessageId || undefined,
|
|
499
|
-
model: this.params.model,
|
|
500
|
-
usage: chunk.usage,
|
|
501
|
-
timestamp: Date.now(),
|
|
502
|
-
})
|
|
503
|
-
}
|
|
504
|
-
return
|
|
505
|
-
}
|
|
529
|
+
private handleToolCallEndEvent(chunk: ToolCallEndEvent): void {
|
|
530
|
+
this.toolCallManager.completeToolCall(chunk)
|
|
531
|
+
aiEventClient.emit('text:chunk:tool-result', {
|
|
532
|
+
...this.buildTextEventContext(),
|
|
533
|
+
messageId: this.currentMessageId || undefined,
|
|
534
|
+
toolCallId: chunk.toolCallId,
|
|
535
|
+
result: chunk.result || '',
|
|
536
|
+
timestamp: Date.now(),
|
|
537
|
+
})
|
|
538
|
+
}
|
|
506
539
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
streamId: this.streamId,
|
|
540
|
+
private handleRunFinishedEvent(chunk: RunFinishedEvent): void {
|
|
541
|
+
aiEventClient.emit('text:chunk:done', {
|
|
542
|
+
...this.buildTextEventContext(),
|
|
511
543
|
messageId: this.currentMessageId || undefined,
|
|
512
544
|
finishReason: chunk.finishReason,
|
|
513
545
|
usage: chunk.usage,
|
|
@@ -515,22 +547,32 @@ class TextEngine<
|
|
|
515
547
|
})
|
|
516
548
|
|
|
517
549
|
if (chunk.usage) {
|
|
518
|
-
aiEventClient.emit('usage
|
|
519
|
-
|
|
520
|
-
streamId: this.streamId,
|
|
550
|
+
aiEventClient.emit('text:usage', {
|
|
551
|
+
...this.buildTextEventContext(),
|
|
521
552
|
messageId: this.currentMessageId || undefined,
|
|
522
|
-
model: this.params.model,
|
|
523
553
|
usage: chunk.usage,
|
|
524
554
|
timestamp: Date.now(),
|
|
525
555
|
})
|
|
526
556
|
}
|
|
557
|
+
|
|
558
|
+
// Don't overwrite a tool_calls finishReason with a stop finishReason
|
|
559
|
+
if (
|
|
560
|
+
this.finishedEvent?.finishReason === 'tool_calls' &&
|
|
561
|
+
chunk.finishReason === 'stop'
|
|
562
|
+
) {
|
|
563
|
+
this.lastFinishReason = chunk.finishReason
|
|
564
|
+
return
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
this.finishedEvent = chunk
|
|
568
|
+
this.lastFinishReason = chunk.finishReason
|
|
527
569
|
}
|
|
528
570
|
|
|
529
|
-
private
|
|
530
|
-
chunk: Extract<StreamChunk, { type: '
|
|
571
|
+
private handleRunErrorEvent(
|
|
572
|
+
chunk: Extract<StreamChunk, { type: 'RUN_ERROR' }>,
|
|
531
573
|
): void {
|
|
532
|
-
aiEventClient.emit('
|
|
533
|
-
|
|
574
|
+
aiEventClient.emit('text:chunk:error', {
|
|
575
|
+
...this.buildTextEventContext(),
|
|
534
576
|
messageId: this.currentMessageId || undefined,
|
|
535
577
|
error: chunk.error.message,
|
|
536
578
|
timestamp: Date.now(),
|
|
@@ -539,16 +581,19 @@ class TextEngine<
|
|
|
539
581
|
this.shouldEmitStreamEnd = false
|
|
540
582
|
}
|
|
541
583
|
|
|
542
|
-
private
|
|
543
|
-
chunk: Extract<StreamChunk, { type: '
|
|
584
|
+
private handleStepFinishedEvent(
|
|
585
|
+
chunk: Extract<StreamChunk, { type: 'STEP_FINISHED' }>,
|
|
544
586
|
): void {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
587
|
+
// Handle thinking/reasoning content from STEP_FINISHED events
|
|
588
|
+
if (chunk.content || chunk.delta) {
|
|
589
|
+
aiEventClient.emit('text:chunk:thinking', {
|
|
590
|
+
...this.buildTextEventContext(),
|
|
591
|
+
messageId: this.currentMessageId || undefined,
|
|
592
|
+
content: chunk.content || '',
|
|
593
|
+
delta: chunk.delta,
|
|
594
|
+
timestamp: Date.now(),
|
|
595
|
+
})
|
|
596
|
+
}
|
|
552
597
|
}
|
|
553
598
|
|
|
554
599
|
private async *checkForPendingToolCalls(): AsyncGenerator<
|
|
@@ -561,16 +606,7 @@ class TextEngine<
|
|
|
561
606
|
return 'continue'
|
|
562
607
|
}
|
|
563
608
|
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
aiEventClient.emit('text:iteration', {
|
|
567
|
-
requestId: this.requestId,
|
|
568
|
-
streamId: this.streamId,
|
|
569
|
-
iterationNumber: this.iterationCount + 1,
|
|
570
|
-
messageCount: this.messages.length,
|
|
571
|
-
toolCallCount: pendingToolCalls.length,
|
|
572
|
-
timestamp: Date.now(),
|
|
573
|
-
})
|
|
609
|
+
const finishEvent = this.createSyntheticFinishedEvent()
|
|
574
610
|
|
|
575
611
|
const { approvals, clientToolResults } = this.collectClientState()
|
|
576
612
|
|
|
@@ -587,14 +623,14 @@ class TextEngine<
|
|
|
587
623
|
) {
|
|
588
624
|
for (const chunk of this.emitApprovalRequests(
|
|
589
625
|
executionResult.needsApproval,
|
|
590
|
-
|
|
626
|
+
finishEvent,
|
|
591
627
|
)) {
|
|
592
628
|
yield chunk
|
|
593
629
|
}
|
|
594
630
|
|
|
595
631
|
for (const chunk of this.emitClientToolInputs(
|
|
596
632
|
executionResult.needsClientExecution,
|
|
597
|
-
|
|
633
|
+
finishEvent,
|
|
598
634
|
)) {
|
|
599
635
|
yield chunk
|
|
600
636
|
}
|
|
@@ -605,7 +641,7 @@ class TextEngine<
|
|
|
605
641
|
|
|
606
642
|
const toolResultChunks = this.emitToolResults(
|
|
607
643
|
executionResult.results,
|
|
608
|
-
|
|
644
|
+
finishEvent,
|
|
609
645
|
)
|
|
610
646
|
|
|
611
647
|
for (const chunk of toolResultChunks) {
|
|
@@ -622,22 +658,13 @@ class TextEngine<
|
|
|
622
658
|
}
|
|
623
659
|
|
|
624
660
|
const toolCalls = this.toolCallManager.getToolCalls()
|
|
625
|
-
const
|
|
661
|
+
const finishEvent = this.finishedEvent
|
|
626
662
|
|
|
627
|
-
if (!
|
|
663
|
+
if (!finishEvent || toolCalls.length === 0) {
|
|
628
664
|
this.setToolPhase('stop')
|
|
629
665
|
return
|
|
630
666
|
}
|
|
631
667
|
|
|
632
|
-
aiEventClient.emit('text:iteration', {
|
|
633
|
-
requestId: this.requestId,
|
|
634
|
-
streamId: this.streamId,
|
|
635
|
-
iterationNumber: this.iterationCount + 1,
|
|
636
|
-
messageCount: this.messages.length,
|
|
637
|
-
toolCallCount: toolCalls.length,
|
|
638
|
-
timestamp: Date.now(),
|
|
639
|
-
})
|
|
640
|
-
|
|
641
668
|
this.addAssistantToolCallMessage(toolCalls)
|
|
642
669
|
|
|
643
670
|
const { approvals, clientToolResults } = this.collectClientState()
|
|
@@ -655,14 +682,14 @@ class TextEngine<
|
|
|
655
682
|
) {
|
|
656
683
|
for (const chunk of this.emitApprovalRequests(
|
|
657
684
|
executionResult.needsApproval,
|
|
658
|
-
|
|
685
|
+
finishEvent,
|
|
659
686
|
)) {
|
|
660
687
|
yield chunk
|
|
661
688
|
}
|
|
662
689
|
|
|
663
690
|
for (const chunk of this.emitClientToolInputs(
|
|
664
691
|
executionResult.needsClientExecution,
|
|
665
|
-
|
|
692
|
+
finishEvent,
|
|
666
693
|
)) {
|
|
667
694
|
yield chunk
|
|
668
695
|
}
|
|
@@ -673,7 +700,7 @@ class TextEngine<
|
|
|
673
700
|
|
|
674
701
|
const toolResultChunks = this.emitToolResults(
|
|
675
702
|
executionResult.results,
|
|
676
|
-
|
|
703
|
+
finishEvent,
|
|
677
704
|
)
|
|
678
705
|
|
|
679
706
|
for (const chunk of toolResultChunks) {
|
|
@@ -687,13 +714,14 @@ class TextEngine<
|
|
|
687
714
|
|
|
688
715
|
private shouldExecuteToolPhase(): boolean {
|
|
689
716
|
return (
|
|
690
|
-
this.
|
|
717
|
+
this.finishedEvent?.finishReason === 'tool_calls' &&
|
|
691
718
|
this.tools.length > 0 &&
|
|
692
719
|
this.toolCallManager.hasToolCalls()
|
|
693
720
|
)
|
|
694
721
|
}
|
|
695
722
|
|
|
696
723
|
private addAssistantToolCallMessage(toolCalls: Array<ToolCall>): void {
|
|
724
|
+
const messageId = this.currentMessageId ?? this.createId('msg')
|
|
697
725
|
this.messages = [
|
|
698
726
|
...this.messages,
|
|
699
727
|
{
|
|
@@ -702,36 +730,77 @@ class TextEngine<
|
|
|
702
730
|
toolCalls,
|
|
703
731
|
},
|
|
704
732
|
]
|
|
733
|
+
|
|
734
|
+
aiEventClient.emit('text:message:created', {
|
|
735
|
+
...this.buildTextEventContext(),
|
|
736
|
+
messageId,
|
|
737
|
+
role: 'assistant',
|
|
738
|
+
content: this.accumulatedContent || '',
|
|
739
|
+
toolCalls,
|
|
740
|
+
timestamp: Date.now(),
|
|
741
|
+
})
|
|
705
742
|
}
|
|
706
743
|
|
|
707
|
-
|
|
744
|
+
/**
|
|
745
|
+
* Extract client state (approvals and client tool results) from original messages.
|
|
746
|
+
* This is called in the constructor BEFORE converting to ModelMessage format,
|
|
747
|
+
* because the parts array (which contains approval state) is lost during conversion.
|
|
748
|
+
*/
|
|
749
|
+
private extractClientStateFromOriginalMessages(
|
|
750
|
+
originalMessages: Array<any>,
|
|
751
|
+
): {
|
|
708
752
|
approvals: Map<string, boolean>
|
|
709
753
|
clientToolResults: Map<string, any>
|
|
710
754
|
} {
|
|
711
755
|
const approvals = new Map<string, boolean>()
|
|
712
756
|
const clientToolResults = new Map<string, any>()
|
|
713
757
|
|
|
714
|
-
for (const message of
|
|
715
|
-
//
|
|
716
|
-
if (message.role === 'assistant' &&
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
part.
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
758
|
+
for (const message of originalMessages) {
|
|
759
|
+
// Check for UIMessage format (parts array) - extract client tool results and approvals
|
|
760
|
+
if (message.role === 'assistant' && message.parts) {
|
|
761
|
+
for (const part of message.parts) {
|
|
762
|
+
if (part.type === 'tool-call') {
|
|
763
|
+
// Extract client tool results (tools without approval that have output)
|
|
764
|
+
if (part.output !== undefined && !part.approval) {
|
|
765
|
+
clientToolResults.set(part.id, part.output)
|
|
766
|
+
}
|
|
767
|
+
// Extract approval responses from UIMessage format parts
|
|
768
|
+
if (
|
|
769
|
+
part.approval?.id &&
|
|
770
|
+
part.approval?.approved !== undefined &&
|
|
771
|
+
part.state === 'approval-responded'
|
|
772
|
+
) {
|
|
773
|
+
approvals.set(part.approval.id, part.approval.approved)
|
|
774
|
+
}
|
|
725
775
|
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
726
779
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
780
|
+
return { approvals, clientToolResults }
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
private collectClientState(): {
|
|
784
|
+
approvals: Map<string, boolean>
|
|
785
|
+
clientToolResults: Map<string, any>
|
|
786
|
+
} {
|
|
787
|
+
// Start with the initial client state extracted from original messages
|
|
788
|
+
const approvals = new Map(this.initialApprovals)
|
|
789
|
+
const clientToolResults = new Map(this.initialClientToolResults)
|
|
790
|
+
|
|
791
|
+
// Also check current messages for any additional tool results (from server tools)
|
|
792
|
+
for (const message of this.messages) {
|
|
793
|
+
// Check for ModelMessage format (role: 'tool' messages contain tool results)
|
|
794
|
+
// This handles results sent back from the client after executing client-side tools
|
|
795
|
+
if (message.role === 'tool' && message.toolCallId) {
|
|
796
|
+
// Parse content back to original output (was stringified by uiMessageToModelMessages)
|
|
797
|
+
let output: unknown
|
|
798
|
+
try {
|
|
799
|
+
output = JSON.parse(message.content as string)
|
|
800
|
+
} catch {
|
|
801
|
+
output = message.content
|
|
734
802
|
}
|
|
803
|
+
clientToolResults.set(message.toolCallId, output)
|
|
735
804
|
}
|
|
736
805
|
}
|
|
737
806
|
|
|
@@ -740,13 +809,13 @@ class TextEngine<
|
|
|
740
809
|
|
|
741
810
|
private emitApprovalRequests(
|
|
742
811
|
approvals: Array<ApprovalRequest>,
|
|
743
|
-
|
|
812
|
+
finishEvent: RunFinishedEvent,
|
|
744
813
|
): Array<StreamChunk> {
|
|
745
814
|
const chunks: Array<StreamChunk> = []
|
|
746
815
|
|
|
747
816
|
for (const approval of approvals) {
|
|
748
|
-
aiEventClient.emit('
|
|
749
|
-
|
|
817
|
+
aiEventClient.emit('tools:approval:requested', {
|
|
818
|
+
...this.buildTextEventContext(),
|
|
750
819
|
messageId: this.currentMessageId || undefined,
|
|
751
820
|
toolCallId: approval.toolCallId,
|
|
752
821
|
toolName: approval.toolName,
|
|
@@ -755,17 +824,20 @@ class TextEngine<
|
|
|
755
824
|
timestamp: Date.now(),
|
|
756
825
|
})
|
|
757
826
|
|
|
827
|
+
// Emit a CUSTOM event for approval requests
|
|
758
828
|
chunks.push({
|
|
759
|
-
type: '
|
|
760
|
-
id: doneChunk.id,
|
|
761
|
-
model: doneChunk.model,
|
|
829
|
+
type: 'CUSTOM',
|
|
762
830
|
timestamp: Date.now(),
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
831
|
+
model: finishEvent.model,
|
|
832
|
+
name: 'approval-requested',
|
|
833
|
+
data: {
|
|
834
|
+
toolCallId: approval.toolCallId,
|
|
835
|
+
toolName: approval.toolName,
|
|
836
|
+
input: approval.input,
|
|
837
|
+
approval: {
|
|
838
|
+
id: approval.approvalId,
|
|
839
|
+
needsApproval: true,
|
|
840
|
+
},
|
|
769
841
|
},
|
|
770
842
|
})
|
|
771
843
|
}
|
|
@@ -775,13 +847,13 @@ class TextEngine<
|
|
|
775
847
|
|
|
776
848
|
private emitClientToolInputs(
|
|
777
849
|
clientRequests: Array<ClientToolRequest>,
|
|
778
|
-
|
|
850
|
+
finishEvent: RunFinishedEvent,
|
|
779
851
|
): Array<StreamChunk> {
|
|
780
852
|
const chunks: Array<StreamChunk> = []
|
|
781
853
|
|
|
782
854
|
for (const clientTool of clientRequests) {
|
|
783
|
-
aiEventClient.emit('
|
|
784
|
-
|
|
855
|
+
aiEventClient.emit('tools:input:available', {
|
|
856
|
+
...this.buildTextEventContext(),
|
|
785
857
|
messageId: this.currentMessageId || undefined,
|
|
786
858
|
toolCallId: clientTool.toolCallId,
|
|
787
859
|
toolName: clientTool.toolName,
|
|
@@ -789,14 +861,17 @@ class TextEngine<
|
|
|
789
861
|
timestamp: Date.now(),
|
|
790
862
|
})
|
|
791
863
|
|
|
864
|
+
// Emit a CUSTOM event for client tool inputs
|
|
792
865
|
chunks.push({
|
|
793
|
-
type: '
|
|
794
|
-
id: doneChunk.id,
|
|
795
|
-
model: doneChunk.model,
|
|
866
|
+
type: 'CUSTOM',
|
|
796
867
|
timestamp: Date.now(),
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
868
|
+
model: finishEvent.model,
|
|
869
|
+
name: 'tool-input-available',
|
|
870
|
+
data: {
|
|
871
|
+
toolCallId: clientTool.toolCallId,
|
|
872
|
+
toolName: clientTool.toolName,
|
|
873
|
+
input: clientTool.input,
|
|
874
|
+
},
|
|
800
875
|
})
|
|
801
876
|
}
|
|
802
877
|
|
|
@@ -805,14 +880,13 @@ class TextEngine<
|
|
|
805
880
|
|
|
806
881
|
private emitToolResults(
|
|
807
882
|
results: Array<ToolResult>,
|
|
808
|
-
|
|
883
|
+
finishEvent: RunFinishedEvent,
|
|
809
884
|
): Array<StreamChunk> {
|
|
810
885
|
const chunks: Array<StreamChunk> = []
|
|
811
886
|
|
|
812
887
|
for (const result of results) {
|
|
813
|
-
aiEventClient.emit('
|
|
814
|
-
|
|
815
|
-
streamId: this.streamId,
|
|
888
|
+
aiEventClient.emit('tools:call:completed', {
|
|
889
|
+
...this.buildTextEventContext(),
|
|
816
890
|
messageId: this.currentMessageId || undefined,
|
|
817
891
|
toolCallId: result.toolCallId,
|
|
818
892
|
toolName: result.toolName,
|
|
@@ -822,16 +896,16 @@ class TextEngine<
|
|
|
822
896
|
})
|
|
823
897
|
|
|
824
898
|
const content = JSON.stringify(result.result)
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
899
|
+
|
|
900
|
+
// Emit TOOL_CALL_END event
|
|
901
|
+
chunks.push({
|
|
902
|
+
type: 'TOOL_CALL_END',
|
|
829
903
|
timestamp: Date.now(),
|
|
904
|
+
model: finishEvent.model,
|
|
830
905
|
toolCallId: result.toolCallId,
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
chunks.push(chunk)
|
|
906
|
+
toolName: result.toolName,
|
|
907
|
+
result: content,
|
|
908
|
+
})
|
|
835
909
|
|
|
836
910
|
this.messages = [
|
|
837
911
|
...this.messages,
|
|
@@ -841,17 +915,45 @@ class TextEngine<
|
|
|
841
915
|
toolCallId: result.toolCallId,
|
|
842
916
|
},
|
|
843
917
|
]
|
|
918
|
+
|
|
919
|
+
aiEventClient.emit('text:message:created', {
|
|
920
|
+
...this.buildTextEventContext(),
|
|
921
|
+
messageId: this.createId('msg'),
|
|
922
|
+
role: 'tool',
|
|
923
|
+
content,
|
|
924
|
+
timestamp: Date.now(),
|
|
925
|
+
})
|
|
844
926
|
}
|
|
845
927
|
|
|
846
928
|
return chunks
|
|
847
929
|
}
|
|
848
930
|
|
|
849
931
|
private getPendingToolCallsFromMessages(): Array<ToolCall> {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
)
|
|
932
|
+
// Build a set of completed tool IDs, but exclude tools with pendingExecution marker
|
|
933
|
+
// (these are approved tools that still need to execute)
|
|
934
|
+
const completedToolIds = new Set<string>()
|
|
935
|
+
|
|
936
|
+
for (const message of this.messages) {
|
|
937
|
+
if (message.role === 'tool' && message.toolCallId) {
|
|
938
|
+
// Check if this is an approval response with pendingExecution marker
|
|
939
|
+
let hasPendingExecution = false
|
|
940
|
+
if (typeof message.content === 'string') {
|
|
941
|
+
try {
|
|
942
|
+
const parsed = JSON.parse(message.content)
|
|
943
|
+
if (parsed.pendingExecution === true) {
|
|
944
|
+
hasPendingExecution = true
|
|
945
|
+
}
|
|
946
|
+
} catch {
|
|
947
|
+
// Not JSON, treat as regular tool result
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Only mark as complete if NOT pending execution
|
|
952
|
+
if (!hasPendingExecution) {
|
|
953
|
+
completedToolIds.add(message.toolCallId)
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
855
957
|
|
|
856
958
|
const pending: Array<ToolCall> = []
|
|
857
959
|
|
|
@@ -868,10 +970,10 @@ class TextEngine<
|
|
|
868
970
|
return pending
|
|
869
971
|
}
|
|
870
972
|
|
|
871
|
-
private
|
|
973
|
+
private createSyntheticFinishedEvent(): RunFinishedEvent {
|
|
872
974
|
return {
|
|
873
|
-
type: '
|
|
874
|
-
|
|
975
|
+
type: 'RUN_FINISHED',
|
|
976
|
+
runId: this.createId('pending'),
|
|
875
977
|
model: this.params.model,
|
|
876
978
|
timestamp: Date.now(),
|
|
877
979
|
finishReason: 'tool_calls',
|
|
@@ -896,6 +998,50 @@ class TextEngine<
|
|
|
896
998
|
return !!this.effectiveSignal?.aborted
|
|
897
999
|
}
|
|
898
1000
|
|
|
1001
|
+
private buildTextEventContext(): {
|
|
1002
|
+
requestId: string
|
|
1003
|
+
streamId: string
|
|
1004
|
+
provider: string
|
|
1005
|
+
model: string
|
|
1006
|
+
clientId?: string
|
|
1007
|
+
source?: 'client' | 'server'
|
|
1008
|
+
systemPrompts?: Array<string>
|
|
1009
|
+
toolNames?: Array<string>
|
|
1010
|
+
options?: Record<string, unknown>
|
|
1011
|
+
modelOptions?: Record<string, unknown>
|
|
1012
|
+
messageCount: number
|
|
1013
|
+
hasTools: boolean
|
|
1014
|
+
streaming: boolean
|
|
1015
|
+
} {
|
|
1016
|
+
return {
|
|
1017
|
+
requestId: this.requestId,
|
|
1018
|
+
streamId: this.streamId,
|
|
1019
|
+
provider: this.adapter.name,
|
|
1020
|
+
model: this.params.model,
|
|
1021
|
+
clientId: this.params.conversationId,
|
|
1022
|
+
source: 'server',
|
|
1023
|
+
systemPrompts:
|
|
1024
|
+
this.systemPrompts.length > 0 ? this.systemPrompts : undefined,
|
|
1025
|
+
toolNames: this.eventToolNames,
|
|
1026
|
+
options: this.eventOptions,
|
|
1027
|
+
modelOptions: this.params.modelOptions as
|
|
1028
|
+
| Record<string, unknown>
|
|
1029
|
+
| undefined,
|
|
1030
|
+
messageCount: this.initialMessageCount,
|
|
1031
|
+
hasTools: this.tools.length > 0,
|
|
1032
|
+
streaming: true,
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
private getContentString(content: ModelMessage['content']): string {
|
|
1037
|
+
if (typeof content === 'string') return content
|
|
1038
|
+
const text =
|
|
1039
|
+
content
|
|
1040
|
+
?.map((part) => (part.type === 'text' ? part.content : ''))
|
|
1041
|
+
.join('') || ''
|
|
1042
|
+
return text
|
|
1043
|
+
}
|
|
1044
|
+
|
|
899
1045
|
private setToolPhase(phase: ToolPhaseResult): void {
|
|
900
1046
|
this.toolPhase = phase
|
|
901
1047
|
if (phase === 'wait') {
|