@tanstack/ai 0.40.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/activities/chat/agent-loop-strategies.d.ts +40 -3
- package/dist/esm/activities/chat/agent-loop-strategies.js +4 -0
- package/dist/esm/activities/chat/agent-loop-strategies.js.map +1 -1
- package/dist/esm/activities/chat/index.d.ts +5 -0
- package/dist/esm/activities/chat/index.js +102 -33
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +7 -0
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -0
- package/dist/esm/activities/chat/stream/message-updaters.js +3 -1
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.js +18 -1
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-definition.d.ts +14 -11
- package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
- package/dist/esm/activities/generateAudio/index.d.ts +1 -1
- package/dist/esm/activities/generateAudio/index.js.map +1 -1
- package/dist/esm/activities/generateImage/index.d.ts +1 -1
- package/dist/esm/activities/generateImage/index.js +1 -1
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/activities/generateSpeech/index.d.ts +1 -1
- package/dist/esm/activities/generateSpeech/index.js.map +1 -1
- package/dist/esm/activities/generateTranscription/index.d.ts +1 -1
- package/dist/esm/activities/generateTranscription/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.d.ts +12 -12
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +4 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/realtime/event-emitter.d.ts +5 -0
- package/dist/esm/realtime/event-emitter.js +27 -0
- package/dist/esm/realtime/event-emitter.js.map +1 -0
- package/dist/esm/realtime/index.d.ts +1 -0
- package/dist/esm/realtime/index.js.map +1 -1
- package/dist/esm/realtime/types.d.ts +9 -1
- package/dist/esm/types.d.ts +43 -2
- package/package.json +1 -1
- package/skills/ai-core/chat-experience/SKILL.md +58 -0
- package/skills/ai-core/media-generation/SKILL.md +42 -2
- package/skills/ai-core/middleware/SKILL.md +18 -3
- package/skills/ai-core/tool-calling/SKILL.md +15 -1
- package/src/activities/chat/agent-loop-strategies.ts +43 -3
- package/src/activities/chat/index.ts +144 -36
- package/src/activities/chat/messages.ts +9 -0
- package/src/activities/chat/stream/message-updaters.ts +7 -1
- package/src/activities/chat/stream/processor.ts +30 -2
- package/src/activities/chat/tools/tool-definition.ts +33 -11
- package/src/activities/generateAudio/index.ts +2 -2
- package/src/activities/generateImage/index.ts +2 -2
- package/src/activities/generateSpeech/index.ts +2 -2
- package/src/activities/generateTranscription/index.ts +2 -2
- package/src/activities/generateVideo/index.ts +29 -15
- package/src/index.ts +3 -1
- package/src/realtime/event-emitter.ts +46 -0
- package/src/realtime/index.ts +2 -0
- package/src/realtime/types.ts +9 -0
- package/src/types.ts +43 -2
|
@@ -240,6 +240,11 @@ export interface TextActivityOptions<
|
|
|
240
240
|
abortController?: TextOptions['abortController']
|
|
241
241
|
/** Strategy for controlling the agent loop */
|
|
242
242
|
agentLoopStrategy?: TextOptions['agentLoopStrategy']
|
|
243
|
+
/**
|
|
244
|
+
* Cap how many tool calls from a single model turn are executed.
|
|
245
|
+
* Excess calls receive error results. See {@link TextOptions.maxToolCallsPerTurn}.
|
|
246
|
+
*/
|
|
247
|
+
maxToolCallsPerTurn?: TextOptions['maxToolCallsPerTurn']
|
|
243
248
|
/**
|
|
244
249
|
* Optional configuration for lazy-tool discovery (tools marked `lazy: true`).
|
|
245
250
|
* Tunes how much of each lazy tool's description appears in the discovery
|
|
@@ -473,6 +478,23 @@ interface TextEngineConfig<
|
|
|
473
478
|
type ToolPhaseResult = 'continue' | 'stop' | 'wait'
|
|
474
479
|
type CyclePhase = 'processText' | 'executeToolCalls'
|
|
475
480
|
|
|
481
|
+
/**
|
|
482
|
+
* Validate and normalize `maxToolCallsPerTurn`.
|
|
483
|
+
* Unset → unlimited. `0` → execute none. Negatives / non-finite → throw
|
|
484
|
+
* (Array#slice treats negatives as "from end", which is not a useful cap).
|
|
485
|
+
*/
|
|
486
|
+
function resolveMaxToolCallsPerTurn(
|
|
487
|
+
cap: number | undefined,
|
|
488
|
+
): number | undefined {
|
|
489
|
+
if (cap == null) return undefined
|
|
490
|
+
if (!Number.isFinite(cap) || cap < 0) {
|
|
491
|
+
throw new Error(
|
|
492
|
+
`maxToolCallsPerTurn must be a non-negative finite number, got ${cap}`,
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
return Math.floor(cap)
|
|
496
|
+
}
|
|
497
|
+
|
|
476
498
|
/**
|
|
477
499
|
* Combine two optional AbortSignals into one that aborts when either does.
|
|
478
500
|
* Returns the other signal directly when one is absent or already aborted.
|
|
@@ -519,6 +541,12 @@ class TextEngine<
|
|
|
519
541
|
|
|
520
542
|
private messages: Array<ModelMessage>
|
|
521
543
|
private iterationCount = 0
|
|
544
|
+
/** Cumulative tool calls counted in this run (emitted + pending resume). */
|
|
545
|
+
private toolCallCount = 0
|
|
546
|
+
/** Tool calls in the most recent budgeted batch (0 when none). */
|
|
547
|
+
private lastTurnToolCallCount = 0
|
|
548
|
+
/** Tool call IDs already counted toward `toolCallCount` (avoids double-count on resume). */
|
|
549
|
+
private readonly countedToolCallIds = new Set<string>()
|
|
522
550
|
private lastFinishReason: string | null = null
|
|
523
551
|
private streamStartTime = 0
|
|
524
552
|
private totalChunkCount = 0
|
|
@@ -534,6 +562,7 @@ class TextEngine<
|
|
|
534
562
|
private earlyTermination = false
|
|
535
563
|
private toolPhase: ToolPhaseResult = 'continue'
|
|
536
564
|
private cyclePhase: CyclePhase = 'processText'
|
|
565
|
+
private readonly maxToolCallsPerTurn: number | undefined
|
|
537
566
|
// Client state extracted from initial messages (before conversion to ModelMessage)
|
|
538
567
|
private readonly initialApprovals: Map<string, boolean>
|
|
539
568
|
private readonly initialClientToolResults: Map<string, any>
|
|
@@ -599,6 +628,9 @@ class TextEngine<
|
|
|
599
628
|
this.systemPrompts = config.params.systemPrompts || []
|
|
600
629
|
this.loopStrategy =
|
|
601
630
|
config.params.agentLoopStrategy || maxIterationsStrategy(5)
|
|
631
|
+
this.maxToolCallsPerTurn = resolveMaxToolCallsPerTurn(
|
|
632
|
+
config.params.maxToolCallsPerTurn,
|
|
633
|
+
)
|
|
602
634
|
this.initialMessageCount = config.params.messages.length
|
|
603
635
|
|
|
604
636
|
// Extract client state (approvals, client tool results) from original messages BEFORE conversion
|
|
@@ -1302,9 +1334,14 @@ class TextEngine<
|
|
|
1302
1334
|
|
|
1303
1335
|
const finishEvent = this.createSyntheticFinishedEvent()
|
|
1304
1336
|
|
|
1337
|
+
// Same fan-out budget as live model turns (seeded history / resume).
|
|
1338
|
+
// Count is deduped so wait→resume after a live turn does not double-count.
|
|
1339
|
+
const { toExecute: budgetedToolCalls, skippedResults } =
|
|
1340
|
+
this.applyToolCallBudget(pendingToolCalls)
|
|
1341
|
+
|
|
1305
1342
|
// Handle undiscovered lazy tool calls with self-correcting error messages
|
|
1306
1343
|
const undiscoveredLazyResults: Array<ToolResult> = []
|
|
1307
|
-
const executablePendingCalls =
|
|
1344
|
+
const executablePendingCalls = budgetedToolCalls.filter((tc) => {
|
|
1308
1345
|
if (this.lazyToolManager.isUndiscoveredLazyTool(tc.function.name)) {
|
|
1309
1346
|
undiscoveredLazyResults.push({
|
|
1310
1347
|
toolCallId: tc.id,
|
|
@@ -1321,16 +1358,27 @@ class TextEngine<
|
|
|
1321
1358
|
return true
|
|
1322
1359
|
})
|
|
1323
1360
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1361
|
+
// Non-executed outcomes (undiscovered lazy + per-turn fan-out skips).
|
|
1362
|
+
// Emitted after executed results so the stream prefers real results first.
|
|
1363
|
+
const deferredErrorResults = [...undiscoveredLazyResults, ...skippedResults]
|
|
1364
|
+
|
|
1365
|
+
// Build args lookup so buildToolResultChunks can emit TOOL_CALL_START +
|
|
1366
|
+
// TOOL_CALL_ARGS before TOOL_CALL_END during continuation re-executions.
|
|
1367
|
+
const argsMap = new Map<string, string>()
|
|
1368
|
+
for (const tc of pendingToolCalls) {
|
|
1369
|
+
argsMap.set(tc.id, tc.function.arguments)
|
|
1331
1370
|
}
|
|
1332
1371
|
|
|
1333
1372
|
if (executablePendingCalls.length === 0) {
|
|
1373
|
+
if (deferredErrorResults.length > 0) {
|
|
1374
|
+
for (const chunk of this.buildToolResultChunks(
|
|
1375
|
+
deferredErrorResults,
|
|
1376
|
+
finishEvent,
|
|
1377
|
+
argsMap,
|
|
1378
|
+
)) {
|
|
1379
|
+
yield* this.pipeThroughMiddleware(chunk)
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1334
1382
|
return 'continue'
|
|
1335
1383
|
}
|
|
1336
1384
|
|
|
@@ -1384,28 +1432,23 @@ class TextEngine<
|
|
|
1384
1432
|
return 'stop'
|
|
1385
1433
|
}
|
|
1386
1434
|
|
|
1435
|
+
const allResults = [...executionResult.results, ...deferredErrorResults]
|
|
1436
|
+
|
|
1387
1437
|
// Notify middleware of tool phase completion (devtools emits aggregate events here)
|
|
1388
1438
|
await this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
|
|
1389
1439
|
toolCalls: pendingToolCalls,
|
|
1390
|
-
results:
|
|
1440
|
+
results: allResults,
|
|
1391
1441
|
needsApproval: executionResult.needsApproval,
|
|
1392
1442
|
needsClientExecution: executionResult.needsClientExecution,
|
|
1393
1443
|
})
|
|
1394
1444
|
|
|
1395
|
-
// Build args lookup so buildToolResultChunks can emit TOOL_CALL_START +
|
|
1396
|
-
// TOOL_CALL_ARGS before TOOL_CALL_END during continuation re-executions.
|
|
1397
|
-
const argsMap = new Map<string, string>()
|
|
1398
|
-
for (const tc of pendingToolCalls) {
|
|
1399
|
-
argsMap.set(tc.id, tc.function.arguments)
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
1445
|
if (
|
|
1403
1446
|
executionResult.needsApproval.length > 0 ||
|
|
1404
1447
|
executionResult.needsClientExecution.length > 0
|
|
1405
1448
|
) {
|
|
1406
|
-
if (
|
|
1449
|
+
if (allResults.length > 0) {
|
|
1407
1450
|
for (const chunk of this.buildToolResultChunks(
|
|
1408
|
-
|
|
1451
|
+
allResults,
|
|
1409
1452
|
finishEvent,
|
|
1410
1453
|
argsMap,
|
|
1411
1454
|
)) {
|
|
@@ -1432,7 +1475,7 @@ class TextEngine<
|
|
|
1432
1475
|
}
|
|
1433
1476
|
|
|
1434
1477
|
const toolResultChunks = this.buildToolResultChunks(
|
|
1435
|
-
|
|
1478
|
+
allResults,
|
|
1436
1479
|
finishEvent,
|
|
1437
1480
|
argsMap,
|
|
1438
1481
|
)
|
|
@@ -1446,6 +1489,8 @@ class TextEngine<
|
|
|
1446
1489
|
|
|
1447
1490
|
private async *processToolCalls(): AsyncGenerator<StreamChunk, void, void> {
|
|
1448
1491
|
if (!this.shouldExecuteToolPhase()) {
|
|
1492
|
+
// Text-only turn — clear per-turn count so strategies see 0 tools.
|
|
1493
|
+
this.lastTurnToolCallCount = 0
|
|
1449
1494
|
this.setToolPhase('stop')
|
|
1450
1495
|
return
|
|
1451
1496
|
}
|
|
@@ -1454,15 +1499,20 @@ class TextEngine<
|
|
|
1454
1499
|
const finishEvent = this.finishedEvent
|
|
1455
1500
|
|
|
1456
1501
|
if (!finishEvent || toolCalls.length === 0) {
|
|
1502
|
+
this.lastTurnToolCallCount = 0
|
|
1457
1503
|
this.setToolPhase('stop')
|
|
1458
1504
|
return
|
|
1459
1505
|
}
|
|
1460
1506
|
|
|
1507
|
+
// Count every model-emitted tool call (including ones we may skip).
|
|
1508
|
+
const { toExecute: budgetedToolCalls, skippedResults } =
|
|
1509
|
+
this.applyToolCallBudget(toolCalls)
|
|
1510
|
+
|
|
1461
1511
|
this.addAssistantToolCallMessage(toolCalls)
|
|
1462
1512
|
|
|
1463
1513
|
// Handle undiscovered lazy tool calls with self-correcting error messages
|
|
1464
1514
|
const undiscoveredLazyResults: Array<ToolResult> = []
|
|
1465
|
-
const executableToolCalls =
|
|
1515
|
+
const executableToolCalls = budgetedToolCalls.filter((tc) => {
|
|
1466
1516
|
if (this.lazyToolManager.isUndiscoveredLazyTool(tc.function.name)) {
|
|
1467
1517
|
undiscoveredLazyResults.push({
|
|
1468
1518
|
toolCallId: tc.id,
|
|
@@ -1479,17 +1529,21 @@ class TextEngine<
|
|
|
1479
1529
|
return true
|
|
1480
1530
|
})
|
|
1481
1531
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
this.finishedEvent,
|
|
1486
|
-
)) {
|
|
1487
|
-
yield* this.pipeThroughMiddleware(chunk)
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1532
|
+
// Non-executed outcomes (undiscovered lazy + per-turn fan-out skips).
|
|
1533
|
+
// Emitted after executed results so the stream prefers real results first.
|
|
1534
|
+
const deferredErrorResults = [...undiscoveredLazyResults, ...skippedResults]
|
|
1490
1535
|
|
|
1491
1536
|
if (executableToolCalls.length === 0) {
|
|
1492
|
-
// All tool calls were undiscovered lazy tools
|
|
1537
|
+
// All tool calls were undiscovered lazy tools and/or skipped by the
|
|
1538
|
+
// per-turn fan-out cap — errors emitted, continue loop (strategy may stop).
|
|
1539
|
+
if (deferredErrorResults.length > 0) {
|
|
1540
|
+
for (const chunk of this.buildToolResultChunks(
|
|
1541
|
+
deferredErrorResults,
|
|
1542
|
+
finishEvent,
|
|
1543
|
+
)) {
|
|
1544
|
+
yield* this.pipeThroughMiddleware(chunk)
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1493
1547
|
this.toolCallManager.clear()
|
|
1494
1548
|
this.setToolPhase('continue')
|
|
1495
1549
|
return
|
|
@@ -1548,10 +1602,13 @@ class TextEngine<
|
|
|
1548
1602
|
return
|
|
1549
1603
|
}
|
|
1550
1604
|
|
|
1605
|
+
// Executed results first, then deferred errors (fan-out skips / undiscovered)
|
|
1606
|
+
const allResults = [...executionResult.results, ...deferredErrorResults]
|
|
1607
|
+
|
|
1551
1608
|
// Notify middleware of tool phase completion (devtools emits aggregate events here)
|
|
1552
1609
|
await this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
|
|
1553
1610
|
toolCalls,
|
|
1554
|
-
results:
|
|
1611
|
+
results: allResults,
|
|
1555
1612
|
needsApproval: executionResult.needsApproval,
|
|
1556
1613
|
needsClientExecution: executionResult.needsClientExecution,
|
|
1557
1614
|
})
|
|
@@ -1560,9 +1617,9 @@ class TextEngine<
|
|
|
1560
1617
|
executionResult.needsApproval.length > 0 ||
|
|
1561
1618
|
executionResult.needsClientExecution.length > 0
|
|
1562
1619
|
) {
|
|
1563
|
-
if (
|
|
1620
|
+
if (allResults.length > 0) {
|
|
1564
1621
|
for (const chunk of this.buildToolResultChunks(
|
|
1565
|
-
|
|
1622
|
+
allResults,
|
|
1566
1623
|
finishEvent,
|
|
1567
1624
|
)) {
|
|
1568
1625
|
yield* this.pipeThroughMiddleware(chunk)
|
|
@@ -1587,10 +1644,7 @@ class TextEngine<
|
|
|
1587
1644
|
return
|
|
1588
1645
|
}
|
|
1589
1646
|
|
|
1590
|
-
const toolResultChunks = this.buildToolResultChunks(
|
|
1591
|
-
executionResult.results,
|
|
1592
|
-
finishEvent,
|
|
1593
|
-
)
|
|
1647
|
+
const toolResultChunks = this.buildToolResultChunks(allResults, finishEvent)
|
|
1594
1648
|
|
|
1595
1649
|
for (const chunk of toolResultChunks) {
|
|
1596
1650
|
yield* this.pipeThroughMiddleware(chunk)
|
|
@@ -1943,10 +1997,64 @@ class TextEngine<
|
|
|
1943
1997
|
iterationCount: this.iterationCount,
|
|
1944
1998
|
messages: this.messages,
|
|
1945
1999
|
finishReason: this.lastFinishReason,
|
|
2000
|
+
toolCallCount: this.toolCallCount,
|
|
2001
|
+
lastTurnToolCallCount: this.lastTurnToolCallCount,
|
|
1946
2002
|
}) && this.toolPhase === 'continue'
|
|
1947
2003
|
)
|
|
1948
2004
|
}
|
|
1949
2005
|
|
|
2006
|
+
/**
|
|
2007
|
+
* Record tool calls (deduped by id) and return the subset that should be
|
|
2008
|
+
* executed after applying `maxToolCallsPerTurn`. Excess calls get synthetic
|
|
2009
|
+
* error results so every tool_call still has a matching result.
|
|
2010
|
+
*
|
|
2011
|
+
* Used for both live model turns and pending/resume batches. IDs already
|
|
2012
|
+
* counted in this run (e.g. wait→resume after a live turn) are not
|
|
2013
|
+
* re-added to `toolCallCount`.
|
|
2014
|
+
*/
|
|
2015
|
+
private applyToolCallBudget(toolCalls: Array<ToolCall>): {
|
|
2016
|
+
toExecute: Array<ToolCall>
|
|
2017
|
+
skippedResults: Array<ToolResult>
|
|
2018
|
+
} {
|
|
2019
|
+
this.lastTurnToolCallCount = toolCalls.length
|
|
2020
|
+
let newlyCounted = 0
|
|
2021
|
+
for (const tc of toolCalls) {
|
|
2022
|
+
if (!this.countedToolCallIds.has(tc.id)) {
|
|
2023
|
+
this.countedToolCallIds.add(tc.id)
|
|
2024
|
+
newlyCounted++
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
this.toolCallCount += newlyCounted
|
|
2028
|
+
|
|
2029
|
+
const cap = this.maxToolCallsPerTurn
|
|
2030
|
+
if (cap == null || toolCalls.length <= cap) {
|
|
2031
|
+
return { toExecute: toolCalls, skippedResults: [] }
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
this.logger.agentLoop(
|
|
2035
|
+
`maxToolCallsPerTurn=${cap} skipped=${toolCalls.length - cap}`,
|
|
2036
|
+
{
|
|
2037
|
+
maxToolCallsPerTurn: cap,
|
|
2038
|
+
emitted: toolCalls.length,
|
|
2039
|
+
skipped: toolCalls.length - cap,
|
|
2040
|
+
},
|
|
2041
|
+
)
|
|
2042
|
+
|
|
2043
|
+
const toExecute = toolCalls.slice(0, cap)
|
|
2044
|
+
const skippedResults: Array<ToolResult> = toolCalls
|
|
2045
|
+
.slice(cap)
|
|
2046
|
+
.map((tc) => ({
|
|
2047
|
+
toolCallId: tc.id,
|
|
2048
|
+
toolName: tc.function.name,
|
|
2049
|
+
result: {
|
|
2050
|
+
error: `Skipped: exceeded maxToolCallsPerTurn (${cap})`,
|
|
2051
|
+
},
|
|
2052
|
+
state: 'output-error' as const,
|
|
2053
|
+
}))
|
|
2054
|
+
|
|
2055
|
+
return { toExecute, skippedResults }
|
|
2056
|
+
}
|
|
2057
|
+
|
|
1950
2058
|
private isAborted(): boolean {
|
|
1951
2059
|
return !!this.effectiveSignal?.aborted
|
|
1952
2060
|
}
|
|
@@ -444,12 +444,21 @@ export function modelMessageToUIMessage(
|
|
|
444
444
|
// Handle tool calls
|
|
445
445
|
if (modelMessage.toolCalls && modelMessage.toolCalls.length > 0) {
|
|
446
446
|
for (const toolCall of modelMessage.toolCalls) {
|
|
447
|
+
// Model-message arguments are complete, so surface the parsed input.
|
|
448
|
+
// A malformed arguments string just leaves `input` undefined.
|
|
449
|
+
let input: unknown
|
|
450
|
+
try {
|
|
451
|
+
input = JSON.parse(toolCall.function.arguments)
|
|
452
|
+
} catch {
|
|
453
|
+
input = undefined
|
|
454
|
+
}
|
|
447
455
|
parts.push({
|
|
448
456
|
type: 'tool-call',
|
|
449
457
|
id: toolCall.id,
|
|
450
458
|
name: toolCall.function.name,
|
|
451
459
|
arguments: toolCall.function.arguments,
|
|
452
460
|
state: 'input-complete', // Model messages have complete arguments
|
|
461
|
+
...(input !== undefined && { input }),
|
|
453
462
|
...(toolCall.metadata !== undefined && { metadata: toolCall.metadata }),
|
|
454
463
|
})
|
|
455
464
|
}
|
|
@@ -58,6 +58,8 @@ export function updateToolCallPart(
|
|
|
58
58
|
name: string
|
|
59
59
|
arguments: string
|
|
60
60
|
state: ToolCallState
|
|
61
|
+
/** Parsed input — set when the arguments are complete. */
|
|
62
|
+
input?: unknown
|
|
61
63
|
metadata?: Record<string, unknown>
|
|
62
64
|
},
|
|
63
65
|
): Array<UIMessage> {
|
|
@@ -76,6 +78,9 @@ export function updateToolCallPart(
|
|
|
76
78
|
// Gemini's thoughtSignature on TOOL_CALL_START) we must not lose it on
|
|
77
79
|
// subsequent updates that don't re-supply it.
|
|
78
80
|
const metadata = toolCall.metadata ?? existing?.metadata
|
|
81
|
+
// Same for the parsed input: it's supplied once at completion, so
|
|
82
|
+
// subsequent arg-less updates (approval, etc.) must not drop it.
|
|
83
|
+
const input = toolCall.input ?? existing?.input
|
|
79
84
|
|
|
80
85
|
const toolCallPart: ToolCallPart = {
|
|
81
86
|
type: 'tool-call',
|
|
@@ -83,9 +88,10 @@ export function updateToolCallPart(
|
|
|
83
88
|
name: toolCall.name,
|
|
84
89
|
arguments: toolCall.arguments,
|
|
85
90
|
state: toolCall.state,
|
|
86
|
-
// Carry forward approval and
|
|
91
|
+
// Carry forward approval, output and parsed input from the existing part
|
|
87
92
|
...(existing?.approval && { approval: { ...existing.approval } }),
|
|
88
93
|
...(existing?.output !== undefined && { output: existing.output }),
|
|
94
|
+
...(input !== undefined && { input }),
|
|
89
95
|
...(metadata !== undefined && { metadata }),
|
|
90
96
|
}
|
|
91
97
|
|
|
@@ -1298,15 +1298,35 @@ export class StreamProcessor {
|
|
|
1298
1298
|
// received, back-fill the arguments string so the UIMessage ToolCallPart
|
|
1299
1299
|
// carries the correct value (defensive against adapters that skip ARGS).
|
|
1300
1300
|
if (chunk.input !== undefined && !existingToolCall.arguments) {
|
|
1301
|
-
|
|
1301
|
+
try {
|
|
1302
|
+
existingToolCall.arguments = JSON.stringify(chunk.input)
|
|
1303
|
+
} catch {
|
|
1304
|
+
// circular refs, BigInt, etc. — leave arguments empty rather than
|
|
1305
|
+
// aborting stream processing
|
|
1306
|
+
}
|
|
1302
1307
|
}
|
|
1303
1308
|
|
|
1304
1309
|
const index = msgState.toolCallOrder.indexOf(chunk.toolCallId)
|
|
1305
1310
|
this.completeToolCall(messageId, index, existingToolCall)
|
|
1306
1311
|
// If TOOL_CALL_END provides parsed input, use it as the canonical parsed
|
|
1307
1312
|
// arguments (overrides the accumulated string parse from completeToolCall)
|
|
1313
|
+
// and refresh the rendered part's `input` so it reflects the canonical
|
|
1314
|
+
// value rather than the possibly-divergent accumulated-args parse that
|
|
1315
|
+
// completeToolCall wrote (e.g. an adapter that coerces values differently
|
|
1316
|
+
// between the streamed args and the final structured input).
|
|
1308
1317
|
if (chunk.input !== undefined) {
|
|
1309
1318
|
existingToolCall.parsedArguments = chunk.input
|
|
1319
|
+
this.messages = updateToolCallPart(this.messages, messageId, {
|
|
1320
|
+
id: existingToolCall.id,
|
|
1321
|
+
name: existingToolCall.name,
|
|
1322
|
+
arguments: existingToolCall.arguments,
|
|
1323
|
+
state: 'input-complete',
|
|
1324
|
+
input: chunk.input,
|
|
1325
|
+
...(existingToolCall.metadata !== undefined && {
|
|
1326
|
+
metadata: existingToolCall.metadata,
|
|
1327
|
+
}),
|
|
1328
|
+
})
|
|
1329
|
+
this.emitMessagesChange()
|
|
1310
1330
|
}
|
|
1311
1331
|
}
|
|
1312
1332
|
|
|
@@ -1890,12 +1910,20 @@ export class StreamProcessor {
|
|
|
1890
1910
|
return
|
|
1891
1911
|
}
|
|
1892
1912
|
|
|
1893
|
-
// Update UIMessage
|
|
1913
|
+
// Update UIMessage. The arguments are complete now, so surface the parsed
|
|
1914
|
+
// input on the part. For adapters that skip TOOL_CALL_ARGS the arguments
|
|
1915
|
+
// string was back-filled from TOOL_CALL_END.input, so this parse matches
|
|
1916
|
+
// the canonical input. If a TOOL_CALL_END.input diverges from the
|
|
1917
|
+
// accumulated args, handleToolCallEndEvent re-updates the part with the
|
|
1918
|
+
// canonical value after this call.
|
|
1894
1919
|
this.messages = updateToolCallPart(this.messages, messageId, {
|
|
1895
1920
|
id: toolCall.id,
|
|
1896
1921
|
name: toolCall.name,
|
|
1897
1922
|
arguments: toolCall.arguments,
|
|
1898
1923
|
state: 'input-complete',
|
|
1924
|
+
...(toolCall.parsedArguments !== undefined && {
|
|
1925
|
+
input: toolCall.parsedArguments,
|
|
1926
|
+
}),
|
|
1899
1927
|
...(toolCall.metadata !== undefined && { metadata: toolCall.metadata }),
|
|
1900
1928
|
})
|
|
1901
1929
|
this.emitMessagesChange()
|
|
@@ -26,6 +26,10 @@ export interface ClientTool<
|
|
|
26
26
|
TOutput extends SchemaInput = SchemaInput,
|
|
27
27
|
TName extends string = string,
|
|
28
28
|
TContext = unknown,
|
|
29
|
+
// Captured as a literal (`true` / `false`) so downstream types — notably
|
|
30
|
+
// the tool-call part's `approval` field — can be gated on it. Defaults to
|
|
31
|
+
// `false` when the tool config omits `needsApproval`.
|
|
32
|
+
TNeedsApproval extends boolean = false,
|
|
29
33
|
> {
|
|
30
34
|
__toolSide: 'client'
|
|
31
35
|
name: TName
|
|
@@ -37,7 +41,7 @@ export interface ClientTool<
|
|
|
37
41
|
// because `undefined` doesn't extend the schema constraint.
|
|
38
42
|
inputSchema?: TInput
|
|
39
43
|
outputSchema?: TOutput
|
|
40
|
-
needsApproval?:
|
|
44
|
+
needsApproval?: TNeedsApproval
|
|
41
45
|
lazy?: boolean
|
|
42
46
|
metadata?: Record<string, unknown>
|
|
43
47
|
execute?: ToolExecuteFunction<TInput, TOutput, TContext>
|
|
@@ -51,18 +55,22 @@ export interface ToolDefinitionInstance<
|
|
|
51
55
|
TOutput extends SchemaInput = SchemaInput,
|
|
52
56
|
TName extends string = string,
|
|
53
57
|
TContext = unknown,
|
|
58
|
+
TNeedsApproval extends boolean = false,
|
|
54
59
|
> extends Tool<TInput, TOutput, TName, TContext> {
|
|
55
60
|
__toolSide: 'definition'
|
|
61
|
+
// Narrow the base `needsApproval?: boolean` to the captured literal so it
|
|
62
|
+
// survives into `ToolCallPartForTool`'s approval gate.
|
|
63
|
+
needsApproval?: TNeedsApproval
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
/**
|
|
59
67
|
* Union type for any kind of client-side tool (client tool or definition)
|
|
60
68
|
*/
|
|
61
69
|
export type AnyClientTool =
|
|
62
|
-
| (Omit<ClientTool<any, any, string, any>, 'execute'> & {
|
|
70
|
+
| (Omit<ClientTool<any, any, string, any, boolean>, 'execute'> & {
|
|
63
71
|
execute?: ((args: any, context?: any) => any) | undefined
|
|
64
72
|
})
|
|
65
|
-
| (Omit<ToolDefinitionInstance<any, any, string, any>, 'execute'> & {
|
|
73
|
+
| (Omit<ToolDefinitionInstance<any, any, string, any, boolean>, 'execute'> & {
|
|
66
74
|
execute?: ((args: any, context?: any) => any) | undefined
|
|
67
75
|
})
|
|
68
76
|
|
|
@@ -100,12 +108,13 @@ export interface ToolDefinitionConfig<
|
|
|
100
108
|
TInput extends SchemaInput = SchemaInput,
|
|
101
109
|
TOutput extends SchemaInput = SchemaInput,
|
|
102
110
|
TName extends string = string,
|
|
111
|
+
TNeedsApproval extends boolean = false,
|
|
103
112
|
> {
|
|
104
113
|
name: TName
|
|
105
114
|
description: string
|
|
106
115
|
inputSchema?: TInput
|
|
107
116
|
outputSchema?: TOutput
|
|
108
|
-
needsApproval?:
|
|
117
|
+
needsApproval?: TNeedsApproval
|
|
109
118
|
lazy?: boolean
|
|
110
119
|
metadata?: Record<string, unknown>
|
|
111
120
|
}
|
|
@@ -117,7 +126,14 @@ export interface ToolDefinition<
|
|
|
117
126
|
TInput extends SchemaInput = SchemaInput,
|
|
118
127
|
TOutput extends SchemaInput = SchemaInput,
|
|
119
128
|
TName extends string = string,
|
|
120
|
-
|
|
129
|
+
TNeedsApproval extends boolean = false,
|
|
130
|
+
> extends ToolDefinitionInstance<
|
|
131
|
+
TInput,
|
|
132
|
+
TOutput,
|
|
133
|
+
TName,
|
|
134
|
+
unknown,
|
|
135
|
+
TNeedsApproval
|
|
136
|
+
> {
|
|
121
137
|
/**
|
|
122
138
|
* Create a server-side tool with execute function
|
|
123
139
|
*/
|
|
@@ -126,11 +142,13 @@ export interface ToolDefinition<
|
|
|
126
142
|
) => ServerTool<TInput, TOutput, TName, TContext>
|
|
127
143
|
|
|
128
144
|
/**
|
|
129
|
-
* Create a client-side tool with optional execute function
|
|
145
|
+
* Create a client-side tool with optional execute function.
|
|
146
|
+
* Carries the definition's `needsApproval` literal through to the client
|
|
147
|
+
* tool so the tool-call part's `approval` field stays gated on it.
|
|
130
148
|
*/
|
|
131
149
|
client: <TContext = unknown>(
|
|
132
150
|
execute?: ToolExecuteFunction<TInput, TOutput, TContext>,
|
|
133
|
-
) => ClientTool<TInput, TOutput, TName, TContext>
|
|
151
|
+
) => ClientTool<TInput, TOutput, TName, TContext, TNeedsApproval>
|
|
134
152
|
}
|
|
135
153
|
|
|
136
154
|
/**
|
|
@@ -192,10 +210,14 @@ export function toolDefinition<
|
|
|
192
210
|
TInput extends SchemaInput = SchemaInput,
|
|
193
211
|
TOutput extends SchemaInput = SchemaInput,
|
|
194
212
|
TName extends string = string,
|
|
213
|
+
// `const` forces the literal (`true` / `false`) to be captured from the
|
|
214
|
+
// config's optional `needsApproval` — without it TS widens to `boolean`,
|
|
215
|
+
// which collapses the approval gate in `ToolCallPartForTool`.
|
|
216
|
+
const TNeedsApproval extends boolean = false,
|
|
195
217
|
>(
|
|
196
|
-
config: ToolDefinitionConfig<TInput, TOutput, TName>,
|
|
197
|
-
): ToolDefinition<TInput, TOutput, TName> {
|
|
198
|
-
const definition: ToolDefinition<TInput, TOutput, TName> = {
|
|
218
|
+
config: ToolDefinitionConfig<TInput, TOutput, TName, TNeedsApproval>,
|
|
219
|
+
): ToolDefinition<TInput, TOutput, TName, TNeedsApproval> {
|
|
220
|
+
const definition: ToolDefinition<TInput, TOutput, TName, TNeedsApproval> = {
|
|
199
221
|
__toolSide: 'definition',
|
|
200
222
|
...config,
|
|
201
223
|
server<TContext = unknown>(
|
|
@@ -210,7 +232,7 @@ export function toolDefinition<
|
|
|
210
232
|
|
|
211
233
|
client<TContext = unknown>(
|
|
212
234
|
execute?: ToolExecuteFunction<TInput, TOutput, TContext>,
|
|
213
|
-
): ClientTool<TInput, TOutput, TName, TContext> {
|
|
235
|
+
): ClientTool<TInput, TOutput, TName, TContext, TNeedsApproval> {
|
|
214
236
|
return {
|
|
215
237
|
__toolSide: 'client',
|
|
216
238
|
...config,
|
|
@@ -14,10 +14,10 @@ import {
|
|
|
14
14
|
runGenerationFinish,
|
|
15
15
|
runGenerationStart,
|
|
16
16
|
runGenerationUsage,
|
|
17
|
-
} from '../middleware'
|
|
17
|
+
} from '../middleware/run'
|
|
18
18
|
import type { InternalLogger } from '../../logger/internal-logger'
|
|
19
19
|
import type { DebugOption } from '../../logger/types'
|
|
20
|
-
import type { GenerationMiddleware } from '../middleware'
|
|
20
|
+
import type { GenerationMiddleware } from '../middleware/types'
|
|
21
21
|
import type { AudioAdapter } from './adapter'
|
|
22
22
|
import type { AudioGenerationResult, StreamChunk } from '../../types'
|
|
23
23
|
|
|
@@ -14,11 +14,11 @@ import {
|
|
|
14
14
|
runGenerationFinish,
|
|
15
15
|
runGenerationStart,
|
|
16
16
|
runGenerationUsage,
|
|
17
|
-
} from '../middleware'
|
|
17
|
+
} from '../middleware/run'
|
|
18
18
|
import { resolveMediaPrompt } from '../../utilities/media-prompt'
|
|
19
19
|
import type { InternalLogger } from '../../logger/internal-logger'
|
|
20
20
|
import type { DebugOption } from '../../logger/types'
|
|
21
|
-
import type { GenerationMiddleware } from '../middleware'
|
|
21
|
+
import type { GenerationMiddleware } from '../middleware/types'
|
|
22
22
|
import type { ImageAdapter } from './adapter'
|
|
23
23
|
import type {
|
|
24
24
|
ImageGenerationResult,
|
|
@@ -14,10 +14,10 @@ import {
|
|
|
14
14
|
runGenerationFinish,
|
|
15
15
|
runGenerationStart,
|
|
16
16
|
runGenerationUsage,
|
|
17
|
-
} from '../middleware'
|
|
17
|
+
} from '../middleware/run'
|
|
18
18
|
import type { InternalLogger } from '../../logger/internal-logger'
|
|
19
19
|
import type { DebugOption } from '../../logger/types'
|
|
20
|
-
import type { GenerationMiddleware } from '../middleware'
|
|
20
|
+
import type { GenerationMiddleware } from '../middleware/types'
|
|
21
21
|
import type { TTSAdapter } from './adapter'
|
|
22
22
|
import type { StreamChunk, TTSResult } from '../../types'
|
|
23
23
|
|
|
@@ -14,10 +14,10 @@ import {
|
|
|
14
14
|
runGenerationFinish,
|
|
15
15
|
runGenerationStart,
|
|
16
16
|
runGenerationUsage,
|
|
17
|
-
} from '../middleware'
|
|
17
|
+
} from '../middleware/run'
|
|
18
18
|
import type { InternalLogger } from '../../logger/internal-logger'
|
|
19
19
|
import type { DebugOption } from '../../logger/types'
|
|
20
|
-
import type { GenerationMiddleware } from '../middleware'
|
|
20
|
+
import type { GenerationMiddleware } from '../middleware/types'
|
|
21
21
|
import type { TranscriptionAdapter } from './adapter'
|
|
22
22
|
import type {
|
|
23
23
|
StreamChunk,
|