@tanstack/ai 0.45.1 → 0.46.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/index.js +41 -4
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/tools/unique-tool-names.d.ts +20 -0
- package/dist/esm/activities/chat/tools/unique-tool-names.js +57 -0
- package/dist/esm/activities/chat/tools/unique-tool-names.js.map +1 -0
- package/dist/esm/adapter-internals.d.ts +1 -0
- package/dist/esm/adapter-internals.js +2 -1
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +4 -2
- package/dist/esm/middlewares/usage-attributes.d.ts +2 -2
- package/dist/esm/middlewares/usage-attributes.js +9 -2
- package/dist/esm/middlewares/usage-attributes.js.map +1 -1
- package/dist/esm/stream-to-response.d.ts +26 -0
- package/dist/esm/stream-to-response.js +1 -1
- package/dist/esm/stream-to-response.js.map +1 -1
- package/dist/esm/stream-to-websocket.d.ts +123 -0
- package/dist/esm/stream-to-websocket.js +249 -0
- package/dist/esm/stream-to-websocket.js.map +1 -0
- package/dist/esm/types.d.ts +8 -7
- package/package.json +2 -2
- package/skills/ai-core/media-generation/SKILL.md +9 -8
- package/src/activities/chat/index.ts +69 -8
- package/src/activities/chat/tools/unique-tool-names.ts +73 -0
- package/src/adapter-internals.ts +4 -0
- package/src/index.ts +17 -0
- package/src/middlewares/usage-attributes.ts +12 -2
- package/src/stream-to-response.ts +2 -2
- package/src/stream-to-websocket.ts +418 -0
- package/src/types.ts +10 -5
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { normalizeToolResult } from '../../utilities/tool-result'
|
|
26
26
|
import { isProviderExecutedToolCall } from '../../utilities/provider-executed'
|
|
27
27
|
import { LazyToolManager } from './tools/lazy-tool-manager'
|
|
28
|
+
import { assertUniqueToolNames } from './tools/unique-tool-names'
|
|
28
29
|
import {
|
|
29
30
|
MiddlewareAbortError,
|
|
30
31
|
ToolCallManager,
|
|
@@ -42,7 +43,11 @@ import {
|
|
|
42
43
|
} from './tools/approval-schema'
|
|
43
44
|
import { maxIterations as maxIterationsStrategy } from './agent-loop-strategies'
|
|
44
45
|
import { isCancelRequestedReason } from './cancel'
|
|
45
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
convertMessagesToModelMessages,
|
|
48
|
+
generateMessageId,
|
|
49
|
+
modelMessageToUIMessage,
|
|
50
|
+
} from './messages'
|
|
46
51
|
import { MiddlewareRunner } from './middleware/compose'
|
|
47
52
|
import { getRunDetached } from './middleware/run-store'
|
|
48
53
|
import { publishRunDetachedSignal } from '../../delivery-detach'
|
|
@@ -738,6 +743,7 @@ class TextEngine<
|
|
|
738
743
|
[]
|
|
739
744
|
private currentThinkingContent = ''
|
|
740
745
|
private currentThinkingSignature = ''
|
|
746
|
+
private hasSeenReasoningEvents = false
|
|
741
747
|
private eventOptions?: Record<string, unknown> | undefined
|
|
742
748
|
private eventToolNames?: Array<string>
|
|
743
749
|
private finishedEvent: RunFinishedEvent | null = null
|
|
@@ -841,6 +847,7 @@ class TextEngine<
|
|
|
841
847
|
this.messages = convertMessagesToModelMessages(config.params.messages)
|
|
842
848
|
|
|
843
849
|
// Initialize lazy tool manager after messages are converted (needs message history for scanning)
|
|
850
|
+
assertUniqueToolNames(config.params.tools || [])
|
|
844
851
|
this.lazyToolManager = new LazyToolManager(
|
|
845
852
|
config.params.tools || [],
|
|
846
853
|
this.messages,
|
|
@@ -1152,6 +1159,7 @@ class TextEngine<
|
|
|
1152
1159
|
duration: Date.now() - this.streamStartTime,
|
|
1153
1160
|
})
|
|
1154
1161
|
} else {
|
|
1162
|
+
this.addTerminalReasoningMessage()
|
|
1155
1163
|
this.terminalHookCalled = true
|
|
1156
1164
|
await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
|
|
1157
1165
|
finishReason: this.lastFinishReason,
|
|
@@ -1282,6 +1290,7 @@ class TextEngine<
|
|
|
1282
1290
|
this.accumulatedThinking = []
|
|
1283
1291
|
this.currentThinkingContent = ''
|
|
1284
1292
|
this.currentThinkingSignature = ''
|
|
1293
|
+
this.hasSeenReasoningEvents = false
|
|
1285
1294
|
this.finishedEvent = null
|
|
1286
1295
|
this.streamedToolErrorResults.clear()
|
|
1287
1296
|
|
|
@@ -1533,16 +1542,19 @@ class TextEngine<
|
|
|
1533
1542
|
this.handleStepFinishedEvent(chunk)
|
|
1534
1543
|
break
|
|
1535
1544
|
|
|
1545
|
+
case 'REASONING_MESSAGE_CONTENT':
|
|
1546
|
+
this.handleReasoningMessageContentEvent(chunk)
|
|
1547
|
+
break
|
|
1548
|
+
|
|
1536
1549
|
case 'TOOL_CALL_RESULT':
|
|
1537
1550
|
// Tool result is already added to messages in buildToolResultChunks
|
|
1538
1551
|
break
|
|
1539
1552
|
|
|
1540
1553
|
case 'REASONING_START':
|
|
1541
1554
|
case 'REASONING_MESSAGE_START':
|
|
1542
|
-
case 'REASONING_MESSAGE_CONTENT':
|
|
1543
1555
|
case 'REASONING_MESSAGE_END':
|
|
1544
1556
|
case 'REASONING_END':
|
|
1545
|
-
//
|
|
1557
|
+
// No special handling needed
|
|
1546
1558
|
break
|
|
1547
1559
|
|
|
1548
1560
|
default:
|
|
@@ -1651,14 +1663,29 @@ class TextEngine<
|
|
|
1651
1663
|
private handleStepFinishedEvent(
|
|
1652
1664
|
chunk: Extract<StreamChunk, { type: 'STEP_FINISHED' }>,
|
|
1653
1665
|
): void {
|
|
1654
|
-
if (
|
|
1655
|
-
|
|
1666
|
+
if (!this.hasSeenReasoningEvents) {
|
|
1667
|
+
if (chunk.delta) {
|
|
1668
|
+
this.currentThinkingContent += chunk.delta
|
|
1669
|
+
} else if (chunk.content) {
|
|
1670
|
+
if (chunk.content.startsWith(this.currentThinkingContent)) {
|
|
1671
|
+
this.currentThinkingContent = chunk.content
|
|
1672
|
+
} else if (!this.currentThinkingContent.startsWith(chunk.content)) {
|
|
1673
|
+
this.currentThinkingContent += chunk.content
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1656
1676
|
}
|
|
1657
1677
|
if (chunk.signature) {
|
|
1658
1678
|
this.currentThinkingSignature = chunk.signature
|
|
1659
1679
|
}
|
|
1660
1680
|
}
|
|
1661
1681
|
|
|
1682
|
+
private handleReasoningMessageContentEvent(
|
|
1683
|
+
chunk: Extract<StreamChunk, { type: 'REASONING_MESSAGE_CONTENT' }>,
|
|
1684
|
+
): void {
|
|
1685
|
+
this.hasSeenReasoningEvents = true
|
|
1686
|
+
this.currentThinkingContent += chunk.delta
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1662
1689
|
/**
|
|
1663
1690
|
* Tools available for execution this turn. The discovery tool is dropped
|
|
1664
1691
|
* from the advertised set (`this.tools`) once every lazy tool is discovered,
|
|
@@ -2065,6 +2092,30 @@ class TextEngine<
|
|
|
2065
2092
|
this.middlewareCtx.messages = this.messages
|
|
2066
2093
|
}
|
|
2067
2094
|
|
|
2095
|
+
private addTerminalReasoningMessage(): void {
|
|
2096
|
+
this.finalizeCurrentThinkingStep()
|
|
2097
|
+
if (this.accumulatedThinking.length === 0) return
|
|
2098
|
+
|
|
2099
|
+
const messages = this.middlewareCtx.messages
|
|
2100
|
+
const alreadyPresent = messages.some(
|
|
2101
|
+
(message) =>
|
|
2102
|
+
message.role === 'assistant' && message.id === this.currentMessageId,
|
|
2103
|
+
)
|
|
2104
|
+
if (alreadyPresent) return
|
|
2105
|
+
|
|
2106
|
+
this.messages = [
|
|
2107
|
+
...messages,
|
|
2108
|
+
{
|
|
2109
|
+
role: 'assistant',
|
|
2110
|
+
content: this.accumulatedContent || null,
|
|
2111
|
+
id: this.currentMessageId ?? undefined,
|
|
2112
|
+
createdAt: this.currentMessageCreatedAt ?? undefined,
|
|
2113
|
+
thinking: this.accumulatedThinking,
|
|
2114
|
+
},
|
|
2115
|
+
]
|
|
2116
|
+
this.middlewareCtx.messages = this.messages
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2068
2119
|
/**
|
|
2069
2120
|
* Extract client state (approvals and client tool results) from original messages.
|
|
2070
2121
|
* This is called in the constructor BEFORE converting to ModelMessage format,
|
|
@@ -2254,12 +2305,18 @@ class TextEngine<
|
|
|
2254
2305
|
: message.content === null
|
|
2255
2306
|
? undefined
|
|
2256
2307
|
: JSON.stringify(message.content)
|
|
2308
|
+
const id =
|
|
2309
|
+
message.id ||
|
|
2310
|
+
`snapshot_${this.runIdOverride ?? this.requestId}_${index}`
|
|
2311
|
+
const parts =
|
|
2312
|
+
message.role === 'assistant' && message.thinking?.length
|
|
2313
|
+
? modelMessageToUIMessage(message, id).parts
|
|
2314
|
+
: undefined
|
|
2257
2315
|
return {
|
|
2258
|
-
id
|
|
2259
|
-
message.id ||
|
|
2260
|
-
`snapshot_${this.runIdOverride ?? this.requestId}_${index}`,
|
|
2316
|
+
id,
|
|
2261
2317
|
role: message.role,
|
|
2262
2318
|
...(content !== undefined ? { content } : {}),
|
|
2319
|
+
...(parts ? { parts } : {}),
|
|
2263
2320
|
...('toolCalls' in message && message.toolCalls
|
|
2264
2321
|
? { toolCalls: message.toolCalls }
|
|
2265
2322
|
: {}),
|
|
@@ -3573,6 +3630,7 @@ class TextEngine<
|
|
|
3573
3630
|
this.applyResumeToolState(config.resumeToolState)
|
|
3574
3631
|
this.messages = config.messages
|
|
3575
3632
|
this.systemPrompts = config.systemPrompts
|
|
3633
|
+
assertUniqueToolNames(config.tools)
|
|
3576
3634
|
this.tools = config.tools
|
|
3577
3635
|
this.params = {
|
|
3578
3636
|
...this.params,
|
|
@@ -3765,6 +3823,9 @@ export function chat<
|
|
|
3765
3823
|
>,
|
|
3766
3824
|
): TextActivityResult<TSchema, TStream, TTools> {
|
|
3767
3825
|
validateCapabilities(options.middleware ?? [], options.adapter)
|
|
3826
|
+
if (options.tools) {
|
|
3827
|
+
assertUniqueToolNames(options.tools)
|
|
3828
|
+
}
|
|
3768
3829
|
|
|
3769
3830
|
const { outputSchema, stream } = options
|
|
3770
3831
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Tool } from '../../../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thrown when `chat({ tools })` (or a provider converter) receives two tools
|
|
5
|
+
* with the same public `name`.
|
|
6
|
+
*
|
|
7
|
+
* The common case is a provider-native factory (`webSearchTool()`) next to an
|
|
8
|
+
* ordinary function that reused the reserved name (`web_search`). Providers
|
|
9
|
+
* reject that pair, so we fail before the request is built.
|
|
10
|
+
*/
|
|
11
|
+
export class DuplicateToolNameError extends Error {
|
|
12
|
+
readonly toolName: string
|
|
13
|
+
|
|
14
|
+
constructor(toolName: string, message: string) {
|
|
15
|
+
super(message)
|
|
16
|
+
this.name = 'DuplicateToolNameError'
|
|
17
|
+
this.toolName = toolName
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isProviderNativeTool(tool: Tool): boolean {
|
|
22
|
+
const kind = tool.metadata?.['__kind']
|
|
23
|
+
return typeof kind === 'string' && kind.length > 0
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function nativeAndCustomMessage(toolName: string) {
|
|
27
|
+
return [
|
|
28
|
+
`Cannot pass two tools named "${toolName}" in the same chat() call.`,
|
|
29
|
+
`One is the provider-native tool from a factory (for example webSearchTool()).`,
|
|
30
|
+
`The other is your own function with the same public name.`,
|
|
31
|
+
`Tool names in one tools array must be unique.`,
|
|
32
|
+
`Keep the factory for hosted search, or keep your function and give it a different name.`,
|
|
33
|
+
].join(' ')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function duplicateNameMessage(toolName: string) {
|
|
37
|
+
return [
|
|
38
|
+
`Cannot pass two tools named "${toolName}" in the same chat() call.`,
|
|
39
|
+
`Tool names in one tools array must be unique.`,
|
|
40
|
+
].join(' ')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Throws {@link DuplicateToolNameError} when two tools share a public name.
|
|
45
|
+
*
|
|
46
|
+
* The native-vs-custom message fires when one of the colliding tools carries
|
|
47
|
+
* adapter `metadata.__kind` (set by a provider factory) and another does not.
|
|
48
|
+
*/
|
|
49
|
+
export function assertUniqueToolNames(tools: ReadonlyArray<Tool>): void {
|
|
50
|
+
const byName = new Map<string, Array<Tool>>()
|
|
51
|
+
for (const tool of tools) {
|
|
52
|
+
const group = byName.get(tool.name)
|
|
53
|
+
if (group) {
|
|
54
|
+
group.push(tool)
|
|
55
|
+
} else {
|
|
56
|
+
byName.set(tool.name, [tool])
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const [name, group] of byName) {
|
|
61
|
+
if (group.length < 2) {
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
const hasNative = group.some(isProviderNativeTool)
|
|
65
|
+
const hasCustom = group.some((tool) => !isProviderNativeTool(tool))
|
|
66
|
+
throw new DuplicateToolNameError(
|
|
67
|
+
name,
|
|
68
|
+
hasNative && hasCustom
|
|
69
|
+
? nativeAndCustomMessage(name)
|
|
70
|
+
: duplicateNameMessage(name),
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/adapter-internals.ts
CHANGED
|
@@ -25,6 +25,10 @@ export {
|
|
|
25
25
|
PendingTurnCapability,
|
|
26
26
|
providePendingTurn,
|
|
27
27
|
} from './activities/chat/middleware/pending-turn'
|
|
28
|
+
export {
|
|
29
|
+
assertUniqueToolNames,
|
|
30
|
+
DuplicateToolNameError,
|
|
31
|
+
} from './activities/chat/tools/unique-tool-names'
|
|
28
32
|
export {
|
|
29
33
|
appendOutputSchemaInstruction,
|
|
30
34
|
parseJsonFromAssistantText,
|
package/src/index.ts
CHANGED
|
@@ -103,6 +103,7 @@ export type {
|
|
|
103
103
|
|
|
104
104
|
// MCP error classes (value exports — usable with instanceof)
|
|
105
105
|
export { MCPDuplicateToolNameError } from './activities/chat/mcp/manager'
|
|
106
|
+
export { DuplicateToolNameError } from './activities/chat/tools/unique-tool-names'
|
|
106
107
|
|
|
107
108
|
// Schema conversion (Standard JSON Schema compliant)
|
|
108
109
|
export {
|
|
@@ -138,6 +139,22 @@ export type {
|
|
|
138
139
|
UpsertableStreamDurability,
|
|
139
140
|
} from './stream-durability'
|
|
140
141
|
|
|
142
|
+
// WebSocket transport utilities
|
|
143
|
+
export {
|
|
144
|
+
toWebSocketStream,
|
|
145
|
+
toWebSocketResponse,
|
|
146
|
+
resumeWebSocketStream,
|
|
147
|
+
resumeWebSocketResponse,
|
|
148
|
+
encodeWsFrame,
|
|
149
|
+
decodeWsFrame,
|
|
150
|
+
} from './stream-to-websocket'
|
|
151
|
+
export type {
|
|
152
|
+
WebSocketLike,
|
|
153
|
+
WsRunContext,
|
|
154
|
+
WebSocketStreamInit,
|
|
155
|
+
InboundFrame,
|
|
156
|
+
} from './stream-to-websocket'
|
|
157
|
+
|
|
141
158
|
// Tool call management
|
|
142
159
|
export { ToolCallManager } from './activities/chat/tools/tool-calls'
|
|
143
160
|
|
|
@@ -12,8 +12,8 @@ import type { TokenUsage } from '../types'
|
|
|
12
12
|
* `gen_ai.usage.cost` and `gen_ai.usage.total_tokens` are de-facto extensions
|
|
13
13
|
* consumed by backends like PostHog (which otherwise re-derive cost from their
|
|
14
14
|
* own price tables, losing cache discounts and gateway markup). Fields with no
|
|
15
|
-
* semconv or de-facto convention (`
|
|
16
|
-
* `unitsBilled`) are TanStack-namespaced.
|
|
15
|
+
* semconv or de-facto convention (`billed`, `costDetails`, and the deprecated
|
|
16
|
+
* `durationSeconds`/`unitsBilled`) are TanStack-namespaced.
|
|
17
17
|
*
|
|
18
18
|
* Shared by `otelMiddleware` across every activity (chat and the media
|
|
19
19
|
* activities) so usage lands identically whichever activity produced the span.
|
|
@@ -30,6 +30,16 @@ export function usageAttributes(
|
|
|
30
30
|
'gen_ai.usage.input_tokens': usage.promptTokens,
|
|
31
31
|
'gen_ai.usage.output_tokens': usage.completionTokens,
|
|
32
32
|
}
|
|
33
|
+
// The self-describing billed quantity: the unit rides along as a string
|
|
34
|
+
// attribute so backends can label/aggregate non-token usage without
|
|
35
|
+
// out-of-band knowledge of the provider.
|
|
36
|
+
if (usage.billed !== undefined) {
|
|
37
|
+
const quantity = firstNumber(usage.billed.quantity)
|
|
38
|
+
if (quantity !== undefined) {
|
|
39
|
+
attrs['tanstack.ai.usage.billed_quantity'] = quantity
|
|
40
|
+
attrs['tanstack.ai.usage.billed_unit'] = usage.billed.unit
|
|
41
|
+
}
|
|
42
|
+
}
|
|
33
43
|
const optional: Array<[key: string, value: unknown]> = [
|
|
34
44
|
['gen_ai.usage.total_tokens', usage.totalTokens],
|
|
35
45
|
['gen_ai.usage.cost', usage.cost],
|
|
@@ -78,7 +78,7 @@ function combineFailures(
|
|
|
78
78
|
)
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
function runErrorChunk(
|
|
81
|
+
export function runErrorChunk(
|
|
82
82
|
error: unknown,
|
|
83
83
|
): Extract<StreamChunk, { type: 'RUN_ERROR' }> {
|
|
84
84
|
const payload = toRunErrorPayload(error)
|
|
@@ -366,7 +366,7 @@ export const RUN_ACCEPTED_EVENT = 'run.accepted'
|
|
|
366
366
|
* The returned `getId` maps each forwarded chunk to the exact opaque offset
|
|
367
367
|
* returned by the durability adapter for the SSE `id:` line.
|
|
368
368
|
*/
|
|
369
|
-
function durableStreamSource<TOffset extends string>(
|
|
369
|
+
export function durableStreamSource<TOffset extends string>(
|
|
370
370
|
stream: AsyncIterable<StreamChunk>,
|
|
371
371
|
durability: StreamDurability<TOffset>,
|
|
372
372
|
options: {
|