@tanstack/ai 0.35.0 → 0.37.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.d.ts +7 -1
- package/dist/esm/activities/chat/index.js +2 -1
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/tools/lazy-tool-manager.d.ts +3 -2
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js +9 -3
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
- package/dist/esm/activities/chat/tools/lazy-tools.d.ts +15 -0
- package/dist/esm/activities/chat/tools/lazy-tools.js +16 -0
- package/dist/esm/activities/chat/tools/lazy-tools.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types.d.ts +22 -1
- package/package.json +3 -3
- package/skills/ai-core/tool-calling/SKILL.md +26 -1
- package/src/activities/chat/index.ts +8 -0
- package/src/activities/chat/tools/lazy-tool-manager.ts +12 -4
- package/src/activities/chat/tools/lazy-tools.ts +33 -0
- package/src/index.ts +5 -0
- package/src/types.ts +23 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnyTool, Tool } from '../../../types.js';
|
|
1
|
+
import { AnyTool, LazyToolsConfig, Tool } from '../../../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Name of the synthetic tool the LLM calls to discover lazy tools.
|
|
4
4
|
*
|
|
@@ -20,6 +20,7 @@ export declare class LazyToolManager {
|
|
|
20
20
|
private readonly discoveredTools;
|
|
21
21
|
private hasNewDiscoveries;
|
|
22
22
|
private readonly discoveryTool;
|
|
23
|
+
private readonly lazyToolsConfig;
|
|
23
24
|
constructor(tools: ReadonlyArray<Tool>, messages: ReadonlyArray<{
|
|
24
25
|
role: string;
|
|
25
26
|
content?: any;
|
|
@@ -32,7 +33,7 @@ export declare class LazyToolManager {
|
|
|
32
33
|
};
|
|
33
34
|
}>;
|
|
34
35
|
toolCallId?: string;
|
|
35
|
-
}
|
|
36
|
+
}>, lazyToolsConfig?: LazyToolsConfig);
|
|
36
37
|
/**
|
|
37
38
|
* Returns the set of tools that should be sent to the LLM:
|
|
38
39
|
* eager tools + discovered lazy tools + discovery tool (if undiscovered tools remain).
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { convertSchemaToJsonSchema } from "./schema-converter.js";
|
|
2
|
+
import { renderLazyCatalogEntry } from "./lazy-tools.js";
|
|
2
3
|
const DISCOVERY_TOOL_NAME = "__lazy__tool__discovery__";
|
|
3
4
|
class LazyToolManager {
|
|
4
5
|
eagerTools;
|
|
@@ -6,7 +7,9 @@ class LazyToolManager {
|
|
|
6
7
|
discoveredTools;
|
|
7
8
|
hasNewDiscoveries;
|
|
8
9
|
discoveryTool;
|
|
9
|
-
|
|
10
|
+
lazyToolsConfig;
|
|
11
|
+
constructor(tools, messages, lazyToolsConfig = {}) {
|
|
12
|
+
this.lazyToolsConfig = lazyToolsConfig;
|
|
10
13
|
const eager = [];
|
|
11
14
|
this.lazyToolMap = /* @__PURE__ */ new Map();
|
|
12
15
|
this.discoveredTools = /* @__PURE__ */ new Set();
|
|
@@ -133,8 +136,11 @@ class LazyToolManager {
|
|
|
133
136
|
return names;
|
|
134
137
|
};
|
|
135
138
|
const lazyToolMap = this.lazyToolMap;
|
|
136
|
-
const
|
|
137
|
-
const
|
|
139
|
+
const include = this.lazyToolsConfig.includeDescription ?? "none";
|
|
140
|
+
const allLazyEntries = Array.from(this.lazyToolMap.values()).map(
|
|
141
|
+
(t) => renderLazyCatalogEntry(t.name, t.description, include)
|
|
142
|
+
);
|
|
143
|
+
const description = `You have access to additional tools that can be discovered. Available tools: [${allLazyEntries.join(", ")}]. Call this tool with a list of tool names to discover their full descriptions and argument schemas before using them.`;
|
|
138
144
|
const manager = this;
|
|
139
145
|
return {
|
|
140
146
|
name: DISCOVERY_TOOL_NAME,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lazy-tool-manager.js","sources":["../../../../../src/activities/chat/tools/lazy-tool-manager.ts"],"sourcesContent":["import { convertSchemaToJsonSchema } from './schema-converter'\nimport type { AnyTool, Tool } from '../../../types'\n\n/**\n * Name of the synthetic tool the LLM calls to discover lazy tools.\n *\n * Exported so callers building custom message-compaction / history-trimming\n * logic can reference the discovery tool by constant instead of hard-coding\n * the string (which is an internal contract that could change).\n */\nexport const DISCOVERY_TOOL_NAME = '__lazy__tool__discovery__'\n\n/**\n * Manages lazy tool discovery for the chat agent loop.\n *\n * Lazy tools are not sent to the LLM initially. Instead, a synthetic\n * \"discovery tool\" is provided that lets the LLM discover lazy tools\n * by name, receiving their full descriptions and schemas on demand.\n */\nexport class LazyToolManager {\n private readonly eagerTools: ReadonlyArray<Tool>\n private readonly lazyToolMap: Map<string, Tool>\n private readonly discoveredTools: Set<string>\n private hasNewDiscoveries: boolean\n private readonly discoveryTool: Tool | null\n\n constructor(\n tools: ReadonlyArray<Tool>,\n messages: ReadonlyArray<{\n role: string\n content?: any\n toolCalls?: Array<{\n id: string\n type: string\n function: { name: string; arguments: string }\n }>\n toolCallId?: string\n }>,\n ) {\n const eager: Array<Tool> = []\n this.lazyToolMap = new Map()\n this.discoveredTools = new Set()\n this.hasNewDiscoveries = false\n\n // Separate tools into eager and lazy\n for (const tool of tools) {\n if (tool.lazy) {\n this.lazyToolMap.set(tool.name, tool)\n } else {\n eager.push(tool)\n }\n }\n this.eagerTools = eager\n\n // If no lazy tools, no discovery tool needed\n if (this.lazyToolMap.size === 0) {\n this.discoveryTool = null\n return\n }\n\n // Scan message history to pre-populate discoveredTools\n this.scanMessageHistory(messages)\n\n // Create the synthetic discovery tool\n this.discoveryTool = this.createDiscoveryTool()\n }\n\n /**\n * Returns the set of tools that should be sent to the LLM:\n * eager tools + discovered lazy tools + discovery tool (if undiscovered tools remain).\n * Resets the hasNewDiscoveries flag.\n */\n getActiveTools(): Array<Tool> {\n this.hasNewDiscoveries = false\n\n const active: Array<Tool> = [...this.eagerTools]\n\n // Add discovered lazy tools\n for (const name of this.discoveredTools) {\n const tool = this.lazyToolMap.get(name)\n if (tool) {\n active.push(tool)\n }\n }\n\n // Add discovery tool if there are still undiscovered lazy tools\n if (\n this.discoveryTool &&\n this.discoveredTools.size < this.lazyToolMap.size\n ) {\n active.push(this.discoveryTool)\n }\n\n return active\n }\n\n /**\n * Returns the tools that should be available for *execution* this turn.\n *\n * This is the advertised set (`getActiveTools()`, passed in as `activeTools`)\n * plus the discovery tool when a pending call references it but it is no\n * longer advertised. Once every lazy tool has been discovered the discovery\n * tool is dropped from the advertised set, but a model may still re-request\n * discovery (long context / hallucination); keeping it executable lets that\n * call return the schemas again instead of failing with \"Unknown tool\".\n *\n * The advertised set is intentionally left unchanged — only execution lookup\n * is widened. Operates on the already-built `activeTools`: it must NOT call\n * `getActiveTools()`, which would reset `hasNewDiscoveries` before the\n * post-execution refresh check in the agent loop.\n */\n getExecutableTools(\n activeTools: ReadonlyArray<AnyTool>,\n pendingToolCallNames: ReadonlyArray<string>,\n ): ReadonlyArray<AnyTool> {\n if (\n this.discoveryTool &&\n pendingToolCallNames.includes(DISCOVERY_TOOL_NAME) &&\n !activeTools.some((t) => t.name === DISCOVERY_TOOL_NAME)\n ) {\n return [...activeTools, this.discoveryTool]\n }\n return activeTools\n }\n\n /**\n * Returns whether new tools have been discovered since the last getActiveTools() call.\n */\n hasNewlyDiscoveredTools(): boolean {\n return this.hasNewDiscoveries\n }\n\n /**\n * Returns true if the given name is a lazy tool that has not yet been discovered.\n */\n isUndiscoveredLazyTool(name: string): boolean {\n return this.lazyToolMap.has(name) && !this.discoveredTools.has(name)\n }\n\n /**\n * Returns a helpful error message for when an undiscovered lazy tool is called.\n */\n getUndiscoveredToolError(name: string): string {\n return `Error: Tool '${name}' must be discovered first. Call ${DISCOVERY_TOOL_NAME} with toolNames: ['${name}'] to discover it.`\n }\n\n /**\n * Scans message history to find previously discovered lazy tools.\n * Looks for assistant messages with discovery tool calls and their\n * corresponding tool result messages.\n */\n private scanMessageHistory(\n messages: ReadonlyArray<{\n role: string\n content?: any\n toolCalls?: Array<{\n id: string\n type: string\n function: { name: string; arguments: string }\n }>\n toolCallId?: string\n }>,\n ): void {\n // Collect tool call IDs for discovery tool invocations\n const discoveryCallIds = new Set<string>()\n\n for (const msg of messages) {\n if (msg.role === 'assistant' && msg.toolCalls) {\n for (const tc of msg.toolCalls) {\n if (tc.function.name === DISCOVERY_TOOL_NAME) {\n discoveryCallIds.add(tc.id)\n }\n }\n }\n }\n\n if (discoveryCallIds.size === 0) return\n\n // Find corresponding tool result messages\n for (const msg of messages) {\n if (\n msg.role === 'tool' &&\n msg.toolCallId &&\n discoveryCallIds.has(msg.toolCallId)\n ) {\n try {\n const content =\n typeof msg.content === 'string'\n ? msg.content\n : JSON.stringify(msg.content)\n const parsed = JSON.parse(content)\n if (parsed && Array.isArray(parsed.tools)) {\n for (const tool of parsed.tools) {\n if (\n tool &&\n typeof tool.name === 'string' &&\n this.lazyToolMap.has(tool.name)\n ) {\n this.discoveredTools.add(tool.name)\n }\n }\n }\n } catch {\n // Malformed JSON — skip gracefully\n }\n }\n }\n }\n\n /**\n * Creates the synthetic discovery tool that the LLM can call\n * to discover lazy tools' descriptions and schemas.\n */\n private createDiscoveryTool(): Tool {\n const undiscoveredNames = (): Array<string> => {\n const names: Array<string> = []\n for (const [name] of this.lazyToolMap) {\n if (!this.discoveredTools.has(name)) {\n names.push(name)\n }\n }\n return names\n }\n\n const lazyToolMap = this.lazyToolMap\n\n // Build the static description with all lazy tool names\n const allLazyNames = Array.from(this.lazyToolMap.keys())\n 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.`\n\n // Use the arrow function to capture `this` context\n const manager = this\n\n return {\n name: DISCOVERY_TOOL_NAME,\n description,\n inputSchema: {\n type: 'object',\n properties: {\n toolNames: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'List of tool names to discover. Each name must match one of the available tools.',\n },\n },\n required: ['toolNames'],\n },\n execute: (args: { toolNames: Array<string> }) => {\n const tools: Array<{\n name: string\n description: string\n inputSchema?: any\n }> = []\n const errors: Array<string> = []\n\n for (const name of args.toolNames) {\n const tool = lazyToolMap.get(name)\n if (tool) {\n // Only flag a refresh for genuinely new discoveries. Re-requesting\n // an already-discovered tool still returns its schema below (the\n // model asked for it), but must not trigger a redundant tool-list\n // refresh + continue in the agent loop.\n if (!manager.discoveredTools.has(name)) {\n manager.discoveredTools.add(name)\n manager.hasNewDiscoveries = true\n }\n const jsonSchema = tool.inputSchema\n ? convertSchemaToJsonSchema(tool.inputSchema)\n : undefined\n tools.push({\n name: tool.name,\n description: tool.description,\n ...(jsonSchema ? { inputSchema: jsonSchema } : {}),\n })\n } else {\n errors.push(\n `Unknown tool: '${name}'. Available tools: [${undiscoveredNames().join(', ')}]`,\n )\n }\n }\n\n const result: {\n tools: typeof tools\n errors?: Array<string>\n } = { tools }\n\n if (errors.length > 0) {\n result.errors = errors\n }\n\n return result\n },\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lazy-tool-manager.js","sources":["../../../../../src/activities/chat/tools/lazy-tool-manager.ts"],"sourcesContent":["import { convertSchemaToJsonSchema } from './schema-converter'\nimport { renderLazyCatalogEntry } from './lazy-tools'\nimport type { AnyTool, LazyToolsConfig, Tool } from '../../../types'\n\n/**\n * Name of the synthetic tool the LLM calls to discover lazy tools.\n *\n * Exported so callers building custom message-compaction / history-trimming\n * logic can reference the discovery tool by constant instead of hard-coding\n * the string (which is an internal contract that could change).\n */\nexport const DISCOVERY_TOOL_NAME = '__lazy__tool__discovery__'\n\n/**\n * Manages lazy tool discovery for the chat agent loop.\n *\n * Lazy tools are not sent to the LLM initially. Instead, a synthetic\n * \"discovery tool\" is provided that lets the LLM discover lazy tools\n * by name, receiving their full descriptions and schemas on demand.\n */\nexport class LazyToolManager {\n private readonly eagerTools: ReadonlyArray<Tool>\n private readonly lazyToolMap: Map<string, Tool>\n private readonly discoveredTools: Set<string>\n private hasNewDiscoveries: boolean\n private readonly discoveryTool: Tool | null\n private readonly lazyToolsConfig: LazyToolsConfig\n\n constructor(\n tools: ReadonlyArray<Tool>,\n messages: ReadonlyArray<{\n role: string\n content?: any\n toolCalls?: Array<{\n id: string\n type: string\n function: { name: string; arguments: string }\n }>\n toolCallId?: string\n }>,\n lazyToolsConfig: LazyToolsConfig = {},\n ) {\n this.lazyToolsConfig = lazyToolsConfig\n const eager: Array<Tool> = []\n this.lazyToolMap = new Map()\n this.discoveredTools = new Set()\n this.hasNewDiscoveries = false\n\n // Separate tools into eager and lazy\n for (const tool of tools) {\n if (tool.lazy) {\n this.lazyToolMap.set(tool.name, tool)\n } else {\n eager.push(tool)\n }\n }\n this.eagerTools = eager\n\n // If no lazy tools, no discovery tool needed\n if (this.lazyToolMap.size === 0) {\n this.discoveryTool = null\n return\n }\n\n // Scan message history to pre-populate discoveredTools\n this.scanMessageHistory(messages)\n\n // Create the synthetic discovery tool\n this.discoveryTool = this.createDiscoveryTool()\n }\n\n /**\n * Returns the set of tools that should be sent to the LLM:\n * eager tools + discovered lazy tools + discovery tool (if undiscovered tools remain).\n * Resets the hasNewDiscoveries flag.\n */\n getActiveTools(): Array<Tool> {\n this.hasNewDiscoveries = false\n\n const active: Array<Tool> = [...this.eagerTools]\n\n // Add discovered lazy tools\n for (const name of this.discoveredTools) {\n const tool = this.lazyToolMap.get(name)\n if (tool) {\n active.push(tool)\n }\n }\n\n // Add discovery tool if there are still undiscovered lazy tools\n if (\n this.discoveryTool &&\n this.discoveredTools.size < this.lazyToolMap.size\n ) {\n active.push(this.discoveryTool)\n }\n\n return active\n }\n\n /**\n * Returns the tools that should be available for *execution* this turn.\n *\n * This is the advertised set (`getActiveTools()`, passed in as `activeTools`)\n * plus the discovery tool when a pending call references it but it is no\n * longer advertised. Once every lazy tool has been discovered the discovery\n * tool is dropped from the advertised set, but a model may still re-request\n * discovery (long context / hallucination); keeping it executable lets that\n * call return the schemas again instead of failing with \"Unknown tool\".\n *\n * The advertised set is intentionally left unchanged — only execution lookup\n * is widened. Operates on the already-built `activeTools`: it must NOT call\n * `getActiveTools()`, which would reset `hasNewDiscoveries` before the\n * post-execution refresh check in the agent loop.\n */\n getExecutableTools(\n activeTools: ReadonlyArray<AnyTool>,\n pendingToolCallNames: ReadonlyArray<string>,\n ): ReadonlyArray<AnyTool> {\n if (\n this.discoveryTool &&\n pendingToolCallNames.includes(DISCOVERY_TOOL_NAME) &&\n !activeTools.some((t) => t.name === DISCOVERY_TOOL_NAME)\n ) {\n return [...activeTools, this.discoveryTool]\n }\n return activeTools\n }\n\n /**\n * Returns whether new tools have been discovered since the last getActiveTools() call.\n */\n hasNewlyDiscoveredTools(): boolean {\n return this.hasNewDiscoveries\n }\n\n /**\n * Returns true if the given name is a lazy tool that has not yet been discovered.\n */\n isUndiscoveredLazyTool(name: string): boolean {\n return this.lazyToolMap.has(name) && !this.discoveredTools.has(name)\n }\n\n /**\n * Returns a helpful error message for when an undiscovered lazy tool is called.\n */\n getUndiscoveredToolError(name: string): string {\n return `Error: Tool '${name}' must be discovered first. Call ${DISCOVERY_TOOL_NAME} with toolNames: ['${name}'] to discover it.`\n }\n\n /**\n * Scans message history to find previously discovered lazy tools.\n * Looks for assistant messages with discovery tool calls and their\n * corresponding tool result messages.\n */\n private scanMessageHistory(\n messages: ReadonlyArray<{\n role: string\n content?: any\n toolCalls?: Array<{\n id: string\n type: string\n function: { name: string; arguments: string }\n }>\n toolCallId?: string\n }>,\n ): void {\n // Collect tool call IDs for discovery tool invocations\n const discoveryCallIds = new Set<string>()\n\n for (const msg of messages) {\n if (msg.role === 'assistant' && msg.toolCalls) {\n for (const tc of msg.toolCalls) {\n if (tc.function.name === DISCOVERY_TOOL_NAME) {\n discoveryCallIds.add(tc.id)\n }\n }\n }\n }\n\n if (discoveryCallIds.size === 0) return\n\n // Find corresponding tool result messages\n for (const msg of messages) {\n if (\n msg.role === 'tool' &&\n msg.toolCallId &&\n discoveryCallIds.has(msg.toolCallId)\n ) {\n try {\n const content =\n typeof msg.content === 'string'\n ? msg.content\n : JSON.stringify(msg.content)\n const parsed = JSON.parse(content)\n if (parsed && Array.isArray(parsed.tools)) {\n for (const tool of parsed.tools) {\n if (\n tool &&\n typeof tool.name === 'string' &&\n this.lazyToolMap.has(tool.name)\n ) {\n this.discoveredTools.add(tool.name)\n }\n }\n }\n } catch {\n // Malformed JSON — skip gracefully\n }\n }\n }\n }\n\n /**\n * Creates the synthetic discovery tool that the LLM can call\n * to discover lazy tools' descriptions and schemas.\n */\n private createDiscoveryTool(): Tool {\n const undiscoveredNames = (): Array<string> => {\n const names: Array<string> = []\n for (const [name] of this.lazyToolMap) {\n if (!this.discoveredTools.has(name)) {\n names.push(name)\n }\n }\n return names\n }\n\n const lazyToolMap = this.lazyToolMap\n\n // Build the static description, rendering each entry per includeDescription.\n // With the default 'none' this is byte-identical to the legacy output.\n const include = this.lazyToolsConfig.includeDescription ?? 'none'\n const allLazyEntries = Array.from(this.lazyToolMap.values()).map((t) =>\n renderLazyCatalogEntry(t.name, t.description, include),\n )\n const description = `You have access to additional tools that can be discovered. Available tools: [${allLazyEntries.join(', ')}]. Call this tool with a list of tool names to discover their full descriptions and argument schemas before using them.`\n\n // Use the arrow function to capture `this` context\n const manager = this\n\n return {\n name: DISCOVERY_TOOL_NAME,\n description,\n inputSchema: {\n type: 'object',\n properties: {\n toolNames: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'List of tool names to discover. Each name must match one of the available tools.',\n },\n },\n required: ['toolNames'],\n },\n execute: (args: { toolNames: Array<string> }) => {\n const tools: Array<{\n name: string\n description: string\n inputSchema?: any\n }> = []\n const errors: Array<string> = []\n\n for (const name of args.toolNames) {\n const tool = lazyToolMap.get(name)\n if (tool) {\n // Only flag a refresh for genuinely new discoveries. Re-requesting\n // an already-discovered tool still returns its schema below (the\n // model asked for it), but must not trigger a redundant tool-list\n // refresh + continue in the agent loop.\n if (!manager.discoveredTools.has(name)) {\n manager.discoveredTools.add(name)\n manager.hasNewDiscoveries = true\n }\n const jsonSchema = tool.inputSchema\n ? convertSchemaToJsonSchema(tool.inputSchema)\n : undefined\n tools.push({\n name: tool.name,\n description: tool.description,\n ...(jsonSchema ? { inputSchema: jsonSchema } : {}),\n })\n } else {\n errors.push(\n `Unknown tool: '${name}'. Available tools: [${undiscoveredNames().join(', ')}]`,\n )\n }\n }\n\n const result: {\n tools: typeof tools\n errors?: Array<string>\n } = { tools }\n\n if (errors.length > 0) {\n result.errors = errors\n }\n\n return result\n },\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { LazyToolsConfig } from '../../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Extract the first sentence of a description (up to the first ., !, or ?
|
|
4
|
+
* followed by whitespace or end-of-string). Falls back to the whole trimmed
|
|
5
|
+
* string when there is no sentence terminator.
|
|
6
|
+
*/
|
|
7
|
+
export declare function firstSentence(text: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Render one entry in a lazy-tool catalog according to `includeDescription`.
|
|
10
|
+
* - 'none' (default) → bare name (preserves legacy chat behavior)
|
|
11
|
+
* - 'first-sentence' → `name — <first sentence>`
|
|
12
|
+
* - 'full' → `name — <full description>`
|
|
13
|
+
* Falls back to the bare name when there is no description.
|
|
14
|
+
*/
|
|
15
|
+
export declare function renderLazyCatalogEntry(name: string, description: string, includeDescription?: LazyToolsConfig['includeDescription']): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
function firstSentence(text) {
|
|
2
|
+
const trimmed = text.trim();
|
|
3
|
+
if (!trimmed) return "";
|
|
4
|
+
const match = trimmed.match(/^.*?[.!?](?=\s|$)/);
|
|
5
|
+
return (match ? match[0] : trimmed).trim();
|
|
6
|
+
}
|
|
7
|
+
function renderLazyCatalogEntry(name, description, includeDescription = "none") {
|
|
8
|
+
if (includeDescription === "none" || !description.trim()) return name;
|
|
9
|
+
const desc = includeDescription === "first-sentence" ? firstSentence(description) : description.trim();
|
|
10
|
+
return desc ? `${name} — ${desc}` : name;
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
firstSentence,
|
|
14
|
+
renderLazyCatalogEntry
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=lazy-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lazy-tools.js","sources":["../../../../../src/activities/chat/tools/lazy-tools.ts"],"sourcesContent":["import type { LazyToolsConfig } from '../../../types'\n\n/**\n * Extract the first sentence of a description (up to the first ., !, or ?\n * followed by whitespace or end-of-string). Falls back to the whole trimmed\n * string when there is no sentence terminator.\n */\nexport function firstSentence(text: string): string {\n const trimmed = text.trim()\n if (!trimmed) return ''\n const match = trimmed.match(/^.*?[.!?](?=\\s|$)/)\n return (match ? match[0] : trimmed).trim()\n}\n\n/**\n * Render one entry in a lazy-tool catalog according to `includeDescription`.\n * - 'none' (default) → bare name (preserves legacy chat behavior)\n * - 'first-sentence' → `name — <first sentence>`\n * - 'full' → `name — <full description>`\n * Falls back to the bare name when there is no description.\n */\nexport function renderLazyCatalogEntry(\n name: string,\n description: string,\n includeDescription: LazyToolsConfig['includeDescription'] = 'none',\n): string {\n if (includeDescription === 'none' || !description.trim()) return name\n const desc =\n includeDescription === 'first-sentence'\n ? firstSentence(description)\n : description.trim()\n return desc ? `${name} — ${desc}` : name\n}\n"],"names":[],"mappings":"AAOO,SAAS,cAAc,MAAsB;AAClD,QAAM,UAAU,KAAK,KAAA;AACrB,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,QAAQ,QAAQ,MAAM,mBAAmB;AAC/C,UAAQ,QAAQ,MAAM,CAAC,IAAI,SAAS,KAAA;AACtC;AASO,SAAS,uBACd,MACA,aACA,qBAA4D,QACpD;AACR,MAAI,uBAAuB,UAAU,CAAC,YAAY,KAAA,EAAQ,QAAO;AACjE,QAAM,OACJ,uBAAuB,mBACnB,cAAc,WAAW,IACzB,YAAY,KAAA;AAClB,SAAO,OAAO,GAAG,IAAI,MAAM,IAAI,KAAK;AACtC;"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type { GenerationMiddleware, GenerationMiddlewareContext, GenerationActiv
|
|
|
23
23
|
export { createCapability, defineChatMiddleware, createChatMiddleware, } from './activities/chat/middleware/index.js';
|
|
24
24
|
export type { Capability, CapabilityHandle, CapabilityContext, CapabilityGetter, CapabilityProvider, } from './activities/chat/middleware/index.js';
|
|
25
25
|
export * from './types.js';
|
|
26
|
+
export { firstSentence, renderLazyCatalogEntry, } from './activities/chat/tools/lazy-tools.js';
|
|
26
27
|
export { buildBaseUsage, type BaseUsageInput } from './utilities/usage.js';
|
|
27
28
|
export { resolveMediaPrompt } from './utilities/media-prompt.js';
|
|
28
29
|
export type { ResolvedMediaPrompt } from './utilities/media-prompt.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { DISCOVERY_TOOL_NAME } from "./activities/chat/tools/lazy-tool-manager.j
|
|
|
14
14
|
import { brandProviderTool } from "./tools/provider-tool.js";
|
|
15
15
|
import { combineStrategies, maxIterations, untilFinishReason } from "./activities/chat/agent-loop-strategies.js";
|
|
16
16
|
import { createFrozenRegistry, createToolRegistry } from "./tool-registry.js";
|
|
17
|
+
import { firstSentence, renderLazyCatalogEntry } from "./activities/chat/tools/lazy-tools.js";
|
|
17
18
|
import { buildBaseUsage } from "./utilities/usage.js";
|
|
18
19
|
import { resolveMediaPrompt } from "./utilities/media-prompt.js";
|
|
19
20
|
import { normalizeSystemPrompts } from "./system-prompts.js";
|
|
@@ -71,6 +72,7 @@ export {
|
|
|
71
72
|
defineChatMiddleware,
|
|
72
73
|
detectImageMimeType,
|
|
73
74
|
extendAdapter,
|
|
75
|
+
firstSentence,
|
|
74
76
|
generateAudio,
|
|
75
77
|
generateImage,
|
|
76
78
|
generateMessageId,
|
|
@@ -91,6 +93,7 @@ export {
|
|
|
91
93
|
parsePartialJSON,
|
|
92
94
|
parseWithStandardSchema,
|
|
93
95
|
realtimeToken,
|
|
96
|
+
renderLazyCatalogEntry,
|
|
94
97
|
resolveMediaPrompt,
|
|
95
98
|
streamToText,
|
|
96
99
|
summarize,
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -486,11 +486,26 @@ export interface Tool<TInput extends SchemaInput = SchemaInput, TOutput extends
|
|
|
486
486
|
execute?: ToolExecuteFunction<TInput, TOutput, TContext> | undefined;
|
|
487
487
|
/** If true, tool execution requires user approval before running. Works with both server and client tools. */
|
|
488
488
|
needsApproval?: boolean;
|
|
489
|
-
/** If true, this tool is lazy and will only be sent to the LLM after being discovered via the lazy tool discovery mechanism.
|
|
489
|
+
/** If true, this tool is lazy and will only be sent to the LLM after being discovered via the lazy tool discovery mechanism. Works with both chat() (the synthetic discovery tool) and Code Mode (kept out of the system prompt and revealed via discover_tools). */
|
|
490
490
|
lazy?: boolean;
|
|
491
491
|
/** Additional metadata for adapters or custom extensions */
|
|
492
492
|
metadata?: Record<string, any> | undefined;
|
|
493
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* Configuration for the lazy-tool discovery catalog, shared by chat() and
|
|
496
|
+
* Code Mode. Optional in both — lazy behavior is triggered purely by tools
|
|
497
|
+
* marked `lazy: true`; this only tunes how much of each lazy tool's
|
|
498
|
+
* description appears in the pre-discovery catalog. The post-discovery payload
|
|
499
|
+
* always returns the full description + schema.
|
|
500
|
+
*/
|
|
501
|
+
export interface LazyToolsConfig {
|
|
502
|
+
/**
|
|
503
|
+
* How much of each lazy tool's description appears in the pre-discovery
|
|
504
|
+
* catalog (the names list shown before the model discovers the tool).
|
|
505
|
+
* @default 'none'
|
|
506
|
+
*/
|
|
507
|
+
includeDescription?: 'full' | 'first-sentence' | 'none';
|
|
508
|
+
}
|
|
494
509
|
export type AnyTool = Omit<Tool<any, any, any, any>, 'execute'> & {
|
|
495
510
|
execute?: ((args: any, context?: any) => any) | undefined;
|
|
496
511
|
};
|
|
@@ -635,6 +650,12 @@ export interface TextOptions<TProviderOptionsSuperset extends Record<string, any
|
|
|
635
650
|
*/
|
|
636
651
|
systemPrompts?: Array<SystemPrompt>;
|
|
637
652
|
agentLoopStrategy?: AgentLoopStrategy;
|
|
653
|
+
/**
|
|
654
|
+
* Optional configuration for lazy-tool discovery (tools marked `lazy: true`).
|
|
655
|
+
* Tunes how much of each lazy tool's description appears in the discovery
|
|
656
|
+
* catalog. Optional — defaults to `{ includeDescription: 'none' }`.
|
|
657
|
+
*/
|
|
658
|
+
lazyToolsConfig?: LazyToolsConfig;
|
|
638
659
|
/**
|
|
639
660
|
* Observability metadata attached to this call. Surfaced to middleware,
|
|
640
661
|
* devtools, and the event client; values may be arbitrarily structured
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -76,8 +76,8 @@
|
|
|
76
76
|
"@ag-ui/core": "^0.0.52",
|
|
77
77
|
"@standard-schema/spec": "^1.1.0",
|
|
78
78
|
"partial-json": "^0.1.7",
|
|
79
|
-
"@tanstack/ai-event-client": "0.6.
|
|
80
|
-
"@tanstack/ai-utils": "0.3.
|
|
79
|
+
"@tanstack/ai-event-client": "0.6.8",
|
|
80
|
+
"@tanstack/ai-utils": "0.3.1"
|
|
81
81
|
},
|
|
82
82
|
"peerDependencies": {
|
|
83
83
|
"@opentelemetry/api": ">=1.9.0"
|
|
@@ -360,7 +360,7 @@ const compareProducts = compareProductsDef.server(async ({ productIds }) => {
|
|
|
360
360
|
export async function POST(request: Request) {
|
|
361
361
|
const { messages } = await request.json()
|
|
362
362
|
const stream = chat({
|
|
363
|
-
adapter: openaiText('gpt-
|
|
363
|
+
adapter: openaiText('gpt-5.5'),
|
|
364
364
|
messages,
|
|
365
365
|
tools: [getProducts, compareProducts],
|
|
366
366
|
agentLoopStrategy: maxIterations(20),
|
|
@@ -375,6 +375,31 @@ gets the full schema, then calls `compareProducts` directly.
|
|
|
375
375
|
Once discovered, a tool stays available for the conversation.
|
|
376
376
|
When all lazy tools are discovered, the discovery tool is removed automatically.
|
|
377
377
|
|
|
378
|
+
### Tuning the lazy catalog with `lazyToolsConfig`
|
|
379
|
+
|
|
380
|
+
By default the discovery-tool catalog lists only bare names (`'none'`). Pass
|
|
381
|
+
`lazyToolsConfig` to `chat()` to include more context:
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
const stream = chat({
|
|
385
|
+
adapter: openaiText('gpt-5.5'),
|
|
386
|
+
messages,
|
|
387
|
+
tools: [getProducts, compareProducts],
|
|
388
|
+
agentLoopStrategy: maxIterations(20),
|
|
389
|
+
lazyToolsConfig: { includeDescription: 'first-sentence' },
|
|
390
|
+
})
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
`includeDescription` values:
|
|
394
|
+
|
|
395
|
+
| Value | Catalog entry | When to use |
|
|
396
|
+
| ------------------ | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
397
|
+
| `'none'` (default) | `compareProducts` | Smallest prompt; model discovers by name |
|
|
398
|
+
| `'first-sentence'` | `compareProducts — Compare two or more products side by side.` | Helps the model decide whether to discover without extra tokens |
|
|
399
|
+
| `'full'` | `compareProducts — Compare two or more products side by side. Accepts productIds array.` | Use when descriptions are short or the model needs full context to route correctly |
|
|
400
|
+
|
|
401
|
+
The post-discovery payload always returns the full description and schema regardless of this setting.
|
|
402
|
+
|
|
378
403
|
## MCP Tools
|
|
379
404
|
|
|
380
405
|
`@tanstack/ai-mcp` lets a server-side `chat()` call discover and invoke tools
|
|
@@ -47,6 +47,7 @@ import type {
|
|
|
47
47
|
CustomEvent,
|
|
48
48
|
InferSchemaType,
|
|
49
49
|
JSONSchema,
|
|
50
|
+
LazyToolsConfig,
|
|
50
51
|
ModelMessage,
|
|
51
52
|
RunFinishedEvent,
|
|
52
53
|
SchemaInput,
|
|
@@ -235,6 +236,12 @@ export interface TextActivityOptions<
|
|
|
235
236
|
abortController?: TextOptions['abortController']
|
|
236
237
|
/** Strategy for controlling the agent loop */
|
|
237
238
|
agentLoopStrategy?: TextOptions['agentLoopStrategy']
|
|
239
|
+
/**
|
|
240
|
+
* Optional configuration for lazy-tool discovery (tools marked `lazy: true`).
|
|
241
|
+
* Tunes how much of each lazy tool's description appears in the discovery
|
|
242
|
+
* catalog. Optional — defaults to `{ includeDescription: 'none' }`.
|
|
243
|
+
*/
|
|
244
|
+
lazyToolsConfig?: LazyToolsConfig
|
|
238
245
|
/** Unique conversation identifier for tracking */
|
|
239
246
|
conversationId?: TextOptions['conversationId']
|
|
240
247
|
/** Thread/conversation ID for AG-UI protocol. Auto-generated if not provided. */
|
|
@@ -606,6 +613,7 @@ class TextEngine<
|
|
|
606
613
|
this.lazyToolManager = new LazyToolManager(
|
|
607
614
|
config.params.tools || [],
|
|
608
615
|
this.messages,
|
|
616
|
+
config.params.lazyToolsConfig,
|
|
609
617
|
)
|
|
610
618
|
this.tools = this.lazyToolManager.getActiveTools()
|
|
611
619
|
this.toolCallManager = new ToolCallManager<
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { convertSchemaToJsonSchema } from './schema-converter'
|
|
2
|
-
import
|
|
2
|
+
import { renderLazyCatalogEntry } from './lazy-tools'
|
|
3
|
+
import type { AnyTool, LazyToolsConfig, Tool } from '../../../types'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Name of the synthetic tool the LLM calls to discover lazy tools.
|
|
@@ -23,6 +24,7 @@ export class LazyToolManager {
|
|
|
23
24
|
private readonly discoveredTools: Set<string>
|
|
24
25
|
private hasNewDiscoveries: boolean
|
|
25
26
|
private readonly discoveryTool: Tool | null
|
|
27
|
+
private readonly lazyToolsConfig: LazyToolsConfig
|
|
26
28
|
|
|
27
29
|
constructor(
|
|
28
30
|
tools: ReadonlyArray<Tool>,
|
|
@@ -36,7 +38,9 @@ export class LazyToolManager {
|
|
|
36
38
|
}>
|
|
37
39
|
toolCallId?: string
|
|
38
40
|
}>,
|
|
41
|
+
lazyToolsConfig: LazyToolsConfig = {},
|
|
39
42
|
) {
|
|
43
|
+
this.lazyToolsConfig = lazyToolsConfig
|
|
40
44
|
const eager: Array<Tool> = []
|
|
41
45
|
this.lazyToolMap = new Map()
|
|
42
46
|
this.discoveredTools = new Set()
|
|
@@ -224,9 +228,13 @@ export class LazyToolManager {
|
|
|
224
228
|
|
|
225
229
|
const lazyToolMap = this.lazyToolMap
|
|
226
230
|
|
|
227
|
-
// Build the static description
|
|
228
|
-
|
|
229
|
-
const
|
|
231
|
+
// Build the static description, rendering each entry per includeDescription.
|
|
232
|
+
// With the default 'none' this is byte-identical to the legacy output.
|
|
233
|
+
const include = this.lazyToolsConfig.includeDescription ?? 'none'
|
|
234
|
+
const allLazyEntries = Array.from(this.lazyToolMap.values()).map((t) =>
|
|
235
|
+
renderLazyCatalogEntry(t.name, t.description, include),
|
|
236
|
+
)
|
|
237
|
+
const description = `You have access to additional tools that can be discovered. Available tools: [${allLazyEntries.join(', ')}]. Call this tool with a list of tool names to discover their full descriptions and argument schemas before using them.`
|
|
230
238
|
|
|
231
239
|
// Use the arrow function to capture `this` context
|
|
232
240
|
const manager = this
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { LazyToolsConfig } from '../../../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract the first sentence of a description (up to the first ., !, or ?
|
|
5
|
+
* followed by whitespace or end-of-string). Falls back to the whole trimmed
|
|
6
|
+
* string when there is no sentence terminator.
|
|
7
|
+
*/
|
|
8
|
+
export function firstSentence(text: string): string {
|
|
9
|
+
const trimmed = text.trim()
|
|
10
|
+
if (!trimmed) return ''
|
|
11
|
+
const match = trimmed.match(/^.*?[.!?](?=\s|$)/)
|
|
12
|
+
return (match ? match[0] : trimmed).trim()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Render one entry in a lazy-tool catalog according to `includeDescription`.
|
|
17
|
+
* - 'none' (default) → bare name (preserves legacy chat behavior)
|
|
18
|
+
* - 'first-sentence' → `name — <first sentence>`
|
|
19
|
+
* - 'full' → `name — <full description>`
|
|
20
|
+
* Falls back to the bare name when there is no description.
|
|
21
|
+
*/
|
|
22
|
+
export function renderLazyCatalogEntry(
|
|
23
|
+
name: string,
|
|
24
|
+
description: string,
|
|
25
|
+
includeDescription: LazyToolsConfig['includeDescription'] = 'none',
|
|
26
|
+
): string {
|
|
27
|
+
if (includeDescription === 'none' || !description.trim()) return name
|
|
28
|
+
const desc =
|
|
29
|
+
includeDescription === 'first-sentence'
|
|
30
|
+
? firstSentence(description)
|
|
31
|
+
: description.trim()
|
|
32
|
+
return desc ? `${name} — ${desc}` : name
|
|
33
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -154,6 +154,11 @@ export type {
|
|
|
154
154
|
// All types
|
|
155
155
|
export * from './types'
|
|
156
156
|
|
|
157
|
+
export {
|
|
158
|
+
firstSentence,
|
|
159
|
+
renderLazyCatalogEntry,
|
|
160
|
+
} from './activities/chat/tools/lazy-tools'
|
|
161
|
+
|
|
157
162
|
// Usage utilities
|
|
158
163
|
export { buildBaseUsage, type BaseUsageInput } from './utilities/usage'
|
|
159
164
|
|
package/src/types.ts
CHANGED
|
@@ -654,13 +654,29 @@ export interface Tool<
|
|
|
654
654
|
/** If true, tool execution requires user approval before running. Works with both server and client tools. */
|
|
655
655
|
needsApproval?: boolean
|
|
656
656
|
|
|
657
|
-
/** If true, this tool is lazy and will only be sent to the LLM after being discovered via the lazy tool discovery mechanism.
|
|
657
|
+
/** If true, this tool is lazy and will only be sent to the LLM after being discovered via the lazy tool discovery mechanism. Works with both chat() (the synthetic discovery tool) and Code Mode (kept out of the system prompt and revealed via discover_tools). */
|
|
658
658
|
lazy?: boolean
|
|
659
659
|
|
|
660
660
|
/** Additional metadata for adapters or custom extensions */
|
|
661
661
|
metadata?: Record<string, any> | undefined
|
|
662
662
|
}
|
|
663
663
|
|
|
664
|
+
/**
|
|
665
|
+
* Configuration for the lazy-tool discovery catalog, shared by chat() and
|
|
666
|
+
* Code Mode. Optional in both — lazy behavior is triggered purely by tools
|
|
667
|
+
* marked `lazy: true`; this only tunes how much of each lazy tool's
|
|
668
|
+
* description appears in the pre-discovery catalog. The post-discovery payload
|
|
669
|
+
* always returns the full description + schema.
|
|
670
|
+
*/
|
|
671
|
+
export interface LazyToolsConfig {
|
|
672
|
+
/**
|
|
673
|
+
* How much of each lazy tool's description appears in the pre-discovery
|
|
674
|
+
* catalog (the names list shown before the model discovers the tool).
|
|
675
|
+
* @default 'none'
|
|
676
|
+
*/
|
|
677
|
+
includeDescription?: 'full' | 'first-sentence' | 'none'
|
|
678
|
+
}
|
|
679
|
+
|
|
664
680
|
export type AnyTool = Omit<Tool<any, any, any, any>, 'execute'> & {
|
|
665
681
|
execute?: ((args: any, context?: any) => any) | undefined
|
|
666
682
|
}
|
|
@@ -819,6 +835,12 @@ export interface TextOptions<
|
|
|
819
835
|
*/
|
|
820
836
|
systemPrompts?: Array<SystemPrompt>
|
|
821
837
|
agentLoopStrategy?: AgentLoopStrategy
|
|
838
|
+
/**
|
|
839
|
+
* Optional configuration for lazy-tool discovery (tools marked `lazy: true`).
|
|
840
|
+
* Tunes how much of each lazy tool's description appears in the discovery
|
|
841
|
+
* catalog. Optional — defaults to `{ includeDescription: 'none' }`.
|
|
842
|
+
*/
|
|
843
|
+
lazyToolsConfig?: LazyToolsConfig
|
|
822
844
|
/**
|
|
823
845
|
* Observability metadata attached to this call. Surfaced to middleware,
|
|
824
846
|
* devtools, and the event client; values may be arbitrarily structured
|