@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.
@@ -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
- constructor(tools, messages) {
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 allLazyNames = Array.from(this.lazyToolMap.keys());
137
- 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.`;
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;"}
@@ -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,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -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. Only meaningful when used with chat(). */
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.35.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.6",
80
- "@tanstack/ai-utils": "0.3.0"
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-4o'),
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 type { AnyTool, Tool } from '../../../types'
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 with all lazy tool names
228
- const allLazyNames = Array.from(this.lazyToolMap.keys())
229
- 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.`
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. Only meaningful when used with chat(). */
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