@tanstack/ai 0.8.0 → 0.9.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 +6 -6
- package/dist/esm/activities/chat/index.js +74 -4
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +64 -40
- package/dist/esm/activities/chat/stream/processor.js +466 -218
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/stream/types.d.ts +17 -0
- package/dist/esm/activities/chat/tools/lazy-tool-manager.d.ts +57 -0
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js +159 -0
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -0
- package/dist/esm/activities/chat/tools/tool-calls.js +3 -0
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-definition.d.ts +2 -0
- package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
- package/dist/esm/types.d.ts +24 -4
- package/package.json +2 -2
- package/src/activities/chat/index.ts +88 -5
- package/src/activities/chat/stream/processor.ts +630 -259
- package/src/activities/chat/stream/types.ts +18 -0
- package/src/activities/chat/tools/lazy-tool-manager.ts +254 -0
- package/src/activities/chat/tools/tool-calls.ts +3 -0
- package/src/activities/chat/tools/tool-definition.ts +2 -0
- package/src/types.ts +26 -2
|
@@ -45,6 +45,24 @@ export interface ChunkStrategy {
|
|
|
45
45
|
reset?: () => void
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Per-message streaming state.
|
|
50
|
+
* Tracks the accumulation of text, tool calls, and thinking content
|
|
51
|
+
* for a single message in the stream.
|
|
52
|
+
*/
|
|
53
|
+
export interface MessageStreamState {
|
|
54
|
+
id: string
|
|
55
|
+
role: 'user' | 'assistant' | 'system'
|
|
56
|
+
totalTextContent: string
|
|
57
|
+
currentSegmentText: string
|
|
58
|
+
lastEmittedText: string
|
|
59
|
+
thinkingContent: string
|
|
60
|
+
toolCalls: Map<string, InternalToolCallState>
|
|
61
|
+
toolCallOrder: Array<string>
|
|
62
|
+
hasToolCallsSinceTextStart: boolean
|
|
63
|
+
isComplete: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
/**
|
|
49
67
|
* Result from processing a stream
|
|
50
68
|
*/
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { convertSchemaToJsonSchema } from './schema-converter'
|
|
2
|
+
import type { Tool } from '../../../types'
|
|
3
|
+
|
|
4
|
+
const DISCOVERY_TOOL_NAME = '__lazy__tool__discovery__'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Manages lazy tool discovery for the chat agent loop.
|
|
8
|
+
*
|
|
9
|
+
* Lazy tools are not sent to the LLM initially. Instead, a synthetic
|
|
10
|
+
* "discovery tool" is provided that lets the LLM discover lazy tools
|
|
11
|
+
* by name, receiving their full descriptions and schemas on demand.
|
|
12
|
+
*/
|
|
13
|
+
export class LazyToolManager {
|
|
14
|
+
private readonly eagerTools: ReadonlyArray<Tool>
|
|
15
|
+
private readonly lazyToolMap: Map<string, Tool>
|
|
16
|
+
private readonly discoveredTools: Set<string>
|
|
17
|
+
private hasNewDiscoveries: boolean
|
|
18
|
+
private readonly discoveryTool: Tool | null
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
tools: ReadonlyArray<Tool>,
|
|
22
|
+
messages: ReadonlyArray<{
|
|
23
|
+
role: string
|
|
24
|
+
content?: any
|
|
25
|
+
toolCalls?: Array<{
|
|
26
|
+
id: string
|
|
27
|
+
type: string
|
|
28
|
+
function: { name: string; arguments: string }
|
|
29
|
+
}>
|
|
30
|
+
toolCallId?: string
|
|
31
|
+
}>,
|
|
32
|
+
) {
|
|
33
|
+
const eager: Array<Tool> = []
|
|
34
|
+
this.lazyToolMap = new Map()
|
|
35
|
+
this.discoveredTools = new Set()
|
|
36
|
+
this.hasNewDiscoveries = false
|
|
37
|
+
|
|
38
|
+
// Separate tools into eager and lazy
|
|
39
|
+
for (const tool of tools) {
|
|
40
|
+
if (tool.lazy) {
|
|
41
|
+
this.lazyToolMap.set(tool.name, tool)
|
|
42
|
+
} else {
|
|
43
|
+
eager.push(tool)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
this.eagerTools = eager
|
|
47
|
+
|
|
48
|
+
// If no lazy tools, no discovery tool needed
|
|
49
|
+
if (this.lazyToolMap.size === 0) {
|
|
50
|
+
this.discoveryTool = null
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Scan message history to pre-populate discoveredTools
|
|
55
|
+
this.scanMessageHistory(messages)
|
|
56
|
+
|
|
57
|
+
// Create the synthetic discovery tool
|
|
58
|
+
this.discoveryTool = this.createDiscoveryTool()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns the set of tools that should be sent to the LLM:
|
|
63
|
+
* eager tools + discovered lazy tools + discovery tool (if undiscovered tools remain).
|
|
64
|
+
* Resets the hasNewDiscoveries flag.
|
|
65
|
+
*/
|
|
66
|
+
getActiveTools(): Array<Tool> {
|
|
67
|
+
this.hasNewDiscoveries = false
|
|
68
|
+
|
|
69
|
+
const active: Array<Tool> = [...this.eagerTools]
|
|
70
|
+
|
|
71
|
+
// Add discovered lazy tools
|
|
72
|
+
for (const name of this.discoveredTools) {
|
|
73
|
+
const tool = this.lazyToolMap.get(name)
|
|
74
|
+
if (tool) {
|
|
75
|
+
active.push(tool)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Add discovery tool if there are still undiscovered lazy tools
|
|
80
|
+
if (
|
|
81
|
+
this.discoveryTool &&
|
|
82
|
+
this.discoveredTools.size < this.lazyToolMap.size
|
|
83
|
+
) {
|
|
84
|
+
active.push(this.discoveryTool)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return active
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Returns whether new tools have been discovered since the last getActiveTools() call.
|
|
92
|
+
*/
|
|
93
|
+
hasNewlyDiscoveredTools(): boolean {
|
|
94
|
+
return this.hasNewDiscoveries
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Returns true if the given name is a lazy tool that has not yet been discovered.
|
|
99
|
+
*/
|
|
100
|
+
isUndiscoveredLazyTool(name: string): boolean {
|
|
101
|
+
return this.lazyToolMap.has(name) && !this.discoveredTools.has(name)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Returns a helpful error message for when an undiscovered lazy tool is called.
|
|
106
|
+
*/
|
|
107
|
+
getUndiscoveredToolError(name: string): string {
|
|
108
|
+
return `Error: Tool '${name}' must be discovered first. Call ${DISCOVERY_TOOL_NAME} with toolNames: ['${name}'] to discover it.`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Scans message history to find previously discovered lazy tools.
|
|
113
|
+
* Looks for assistant messages with discovery tool calls and their
|
|
114
|
+
* corresponding tool result messages.
|
|
115
|
+
*/
|
|
116
|
+
private scanMessageHistory(
|
|
117
|
+
messages: ReadonlyArray<{
|
|
118
|
+
role: string
|
|
119
|
+
content?: any
|
|
120
|
+
toolCalls?: Array<{
|
|
121
|
+
id: string
|
|
122
|
+
type: string
|
|
123
|
+
function: { name: string; arguments: string }
|
|
124
|
+
}>
|
|
125
|
+
toolCallId?: string
|
|
126
|
+
}>,
|
|
127
|
+
): void {
|
|
128
|
+
// Collect tool call IDs for discovery tool invocations
|
|
129
|
+
const discoveryCallIds = new Set<string>()
|
|
130
|
+
|
|
131
|
+
for (const msg of messages) {
|
|
132
|
+
if (msg.role === 'assistant' && msg.toolCalls) {
|
|
133
|
+
for (const tc of msg.toolCalls) {
|
|
134
|
+
if (tc.function.name === DISCOVERY_TOOL_NAME) {
|
|
135
|
+
discoveryCallIds.add(tc.id)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (discoveryCallIds.size === 0) return
|
|
142
|
+
|
|
143
|
+
// Find corresponding tool result messages
|
|
144
|
+
for (const msg of messages) {
|
|
145
|
+
if (
|
|
146
|
+
msg.role === 'tool' &&
|
|
147
|
+
msg.toolCallId &&
|
|
148
|
+
discoveryCallIds.has(msg.toolCallId)
|
|
149
|
+
) {
|
|
150
|
+
try {
|
|
151
|
+
const content =
|
|
152
|
+
typeof msg.content === 'string'
|
|
153
|
+
? msg.content
|
|
154
|
+
: JSON.stringify(msg.content)
|
|
155
|
+
const parsed = JSON.parse(content)
|
|
156
|
+
if (parsed && Array.isArray(parsed.tools)) {
|
|
157
|
+
for (const tool of parsed.tools) {
|
|
158
|
+
if (
|
|
159
|
+
tool &&
|
|
160
|
+
typeof tool.name === 'string' &&
|
|
161
|
+
this.lazyToolMap.has(tool.name)
|
|
162
|
+
) {
|
|
163
|
+
this.discoveredTools.add(tool.name)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
// Malformed JSON — skip gracefully
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Creates the synthetic discovery tool that the LLM can call
|
|
176
|
+
* to discover lazy tools' descriptions and schemas.
|
|
177
|
+
*/
|
|
178
|
+
private createDiscoveryTool(): Tool {
|
|
179
|
+
const undiscoveredNames = (): Array<string> => {
|
|
180
|
+
const names: Array<string> = []
|
|
181
|
+
for (const [name] of this.lazyToolMap) {
|
|
182
|
+
if (!this.discoveredTools.has(name)) {
|
|
183
|
+
names.push(name)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return names
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const lazyToolMap = this.lazyToolMap
|
|
190
|
+
|
|
191
|
+
// Build the static description with all lazy tool names
|
|
192
|
+
const allLazyNames = Array.from(this.lazyToolMap.keys())
|
|
193
|
+
const description = `You have access to additional tools that can be discovered. Available tools: [${allLazyNames.join(', ')}]. Call this tool with a list of tool names to discover their full descriptions and argument schemas before using them.`
|
|
194
|
+
|
|
195
|
+
// Use the arrow function to capture `this` context
|
|
196
|
+
const manager = this
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
name: DISCOVERY_TOOL_NAME,
|
|
200
|
+
description,
|
|
201
|
+
inputSchema: {
|
|
202
|
+
type: 'object',
|
|
203
|
+
properties: {
|
|
204
|
+
toolNames: {
|
|
205
|
+
type: 'array',
|
|
206
|
+
items: { type: 'string' },
|
|
207
|
+
description:
|
|
208
|
+
'List of tool names to discover. Each name must match one of the available tools.',
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
required: ['toolNames'],
|
|
212
|
+
},
|
|
213
|
+
execute: (args: { toolNames: Array<string> }) => {
|
|
214
|
+
const tools: Array<{
|
|
215
|
+
name: string
|
|
216
|
+
description: string
|
|
217
|
+
inputSchema?: any
|
|
218
|
+
}> = []
|
|
219
|
+
const errors: Array<string> = []
|
|
220
|
+
|
|
221
|
+
for (const name of args.toolNames) {
|
|
222
|
+
const tool = lazyToolMap.get(name)
|
|
223
|
+
if (tool) {
|
|
224
|
+
manager.discoveredTools.add(name)
|
|
225
|
+
manager.hasNewDiscoveries = true
|
|
226
|
+
const jsonSchema = tool.inputSchema
|
|
227
|
+
? convertSchemaToJsonSchema(tool.inputSchema)
|
|
228
|
+
: undefined
|
|
229
|
+
tools.push({
|
|
230
|
+
name: tool.name,
|
|
231
|
+
description: tool.description,
|
|
232
|
+
...(jsonSchema ? { inputSchema: jsonSchema } : {}),
|
|
233
|
+
})
|
|
234
|
+
} else {
|
|
235
|
+
errors.push(
|
|
236
|
+
`Unknown tool: '${name}'. Available tools: [${undiscoveredNames().join(', ')}]`,
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const result: {
|
|
242
|
+
tools: typeof tools
|
|
243
|
+
errors?: Array<string>
|
|
244
|
+
} = { tools }
|
|
245
|
+
|
|
246
|
+
if (errors.length > 0) {
|
|
247
|
+
result.errors = errors
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return result
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -32,6 +32,7 @@ export interface ClientTool<
|
|
|
32
32
|
inputSchema?: TInput
|
|
33
33
|
outputSchema?: TOutput
|
|
34
34
|
needsApproval?: boolean
|
|
35
|
+
lazy?: boolean
|
|
35
36
|
metadata?: Record<string, unknown>
|
|
36
37
|
execute?: (
|
|
37
38
|
args: InferSchemaType<TInput>,
|
|
@@ -96,6 +97,7 @@ export interface ToolDefinitionConfig<
|
|
|
96
97
|
inputSchema?: TInput
|
|
97
98
|
outputSchema?: TOutput
|
|
98
99
|
needsApproval?: boolean
|
|
100
|
+
lazy?: boolean
|
|
99
101
|
metadata?: Record<string, unknown>
|
|
100
102
|
}
|
|
101
103
|
|
package/src/types.ts
CHANGED
|
@@ -91,6 +91,8 @@ export interface ToolCall {
|
|
|
91
91
|
name: string
|
|
92
92
|
arguments: string // JSON string
|
|
93
93
|
}
|
|
94
|
+
/** Provider-specific metadata to carry through the tool call lifecycle */
|
|
95
|
+
providerMetadata?: Record<string, unknown>
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
// ============================================================================
|
|
@@ -493,6 +495,9 @@ export interface Tool<
|
|
|
493
495
|
/** If true, tool execution requires user approval before running. Works with both server and client tools. */
|
|
494
496
|
needsApproval?: boolean
|
|
495
497
|
|
|
498
|
+
/** If true, this tool is lazy and will only be sent to the LLM after being discovered via the lazy tool discovery mechanism. Only meaningful when used with chat(). */
|
|
499
|
+
lazy?: boolean
|
|
500
|
+
|
|
496
501
|
/** Additional metadata for adapters or custom extensions */
|
|
497
502
|
metadata?: Record<string, any>
|
|
498
503
|
}
|
|
@@ -730,6 +735,7 @@ export type AGUIEventType =
|
|
|
730
735
|
| 'TOOL_CALL_END'
|
|
731
736
|
| 'STEP_STARTED'
|
|
732
737
|
| 'STEP_FINISHED'
|
|
738
|
+
| 'MESSAGES_SNAPSHOT'
|
|
733
739
|
| 'STATE_SNAPSHOT'
|
|
734
740
|
| 'STATE_DELTA'
|
|
735
741
|
| 'CUSTOM'
|
|
@@ -806,8 +812,8 @@ export interface TextMessageStartEvent extends BaseAGUIEvent {
|
|
|
806
812
|
type: 'TEXT_MESSAGE_START'
|
|
807
813
|
/** Unique identifier for this message */
|
|
808
814
|
messageId: string
|
|
809
|
-
/** Role
|
|
810
|
-
role: 'assistant'
|
|
815
|
+
/** Role of the message sender */
|
|
816
|
+
role: 'user' | 'assistant' | 'system' | 'tool'
|
|
811
817
|
}
|
|
812
818
|
|
|
813
819
|
/**
|
|
@@ -841,8 +847,12 @@ export interface ToolCallStartEvent extends BaseAGUIEvent {
|
|
|
841
847
|
toolCallId: string
|
|
842
848
|
/** Name of the tool being called */
|
|
843
849
|
toolName: string
|
|
850
|
+
/** ID of the parent message that initiated this tool call */
|
|
851
|
+
parentMessageId?: string
|
|
844
852
|
/** Index for parallel tool calls */
|
|
845
853
|
index?: number
|
|
854
|
+
/** Provider-specific metadata to carry into the ToolCall */
|
|
855
|
+
providerMetadata?: Record<string, unknown>
|
|
846
856
|
}
|
|
847
857
|
|
|
848
858
|
/**
|
|
@@ -897,6 +907,19 @@ export interface StepFinishedEvent extends BaseAGUIEvent {
|
|
|
897
907
|
content?: string
|
|
898
908
|
}
|
|
899
909
|
|
|
910
|
+
/**
|
|
911
|
+
* Emitted to provide a snapshot of all messages in a conversation.
|
|
912
|
+
*
|
|
913
|
+
* Unlike StateSnapshot (which carries arbitrary application state),
|
|
914
|
+
* MessagesSnapshot specifically delivers the conversation transcript.
|
|
915
|
+
* This is a first-class AG-UI event type.
|
|
916
|
+
*/
|
|
917
|
+
export interface MessagesSnapshotEvent extends BaseAGUIEvent {
|
|
918
|
+
type: 'MESSAGES_SNAPSHOT'
|
|
919
|
+
/** Complete array of messages in the conversation */
|
|
920
|
+
messages: Array<UIMessage>
|
|
921
|
+
}
|
|
922
|
+
|
|
900
923
|
/**
|
|
901
924
|
* Emitted to provide a full state snapshot.
|
|
902
925
|
*/
|
|
@@ -941,6 +964,7 @@ export type AGUIEvent =
|
|
|
941
964
|
| ToolCallEndEvent
|
|
942
965
|
| StepStartedEvent
|
|
943
966
|
| StepFinishedEvent
|
|
967
|
+
| MessagesSnapshotEvent
|
|
944
968
|
| StateSnapshotEvent
|
|
945
969
|
| StateDeltaEvent
|
|
946
970
|
| CustomEvent
|