feat(search-tools): LLM-driven search and execute and new API#325
feat(search-tools): LLM-driven search and execute and new API#325
Conversation
commit: |
There was a problem hiding this comment.
Pull request overview
Adds an agent-friendly “meta tools” pattern to the StackOne AI SDK by exposing two LLM-callable tools—tool_search and tool_execute—so frameworks can discover and invoke StackOne tools without loading every tool definition into the prompt up front.
Changes:
- Add
StackOneToolSet.getMetaTools()returning aToolscollection containingtool_search+tool_execute. - Introduce
src/meta-tools.tsimplementing the local meta tools and their input schemas. - Add a runnable example demonstrating usage with Vercel AI SDK and OpenAI Chat Completions; export
MetaToolsOptionsfrom the package.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| src/toolsets.ts | Adds getMetaTools() to construct the meta tools collection. |
| src/meta-tools.ts | Implements tool_search and tool_execute as local BaseTools. |
| src/index.ts | Exports MetaToolsOptions type for consumers. |
| examples/meta-tools.ts | Demonstrates an agent loop using the new meta tools with two frameworks. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/meta-tools.ts
Outdated
| import { z } from 'zod/v4'; | ||
| import { BaseTool } from './tool'; | ||
| import type { ExecuteOptions, JsonObject, LocalExecuteConfig, ToolParameters } from './types'; | ||
| import { StackOneError } from './utils/error-stackone'; |
There was a problem hiding this comment.
StackOneError is imported but never used in this module, which will fail the repo's no-unused-vars / typescript/no-unused-vars lint rules. Either remove the import or use it (e.g., to wrap/normalize parse/validation errors for these local tools).
| import { StackOneError } from './utils/error-stackone'; |
src/meta-tools.ts
Outdated
| tools: results.toArray().map((t) => ({ | ||
| name: t.name, | ||
| description: t.description, | ||
| parameters: t.parameters.properties, |
There was a problem hiding this comment.
tool_search currently returns parameters: t.parameters.properties, which drops the top-level schema fields like type and (critically) required. This makes it impossible for the LLM to reliably know which fields are required and can lead to invalid calls to tool_execute. Return the full object schema instead (e.g., the tool's JSON schema or { type: 'object', properties, required }).
| parameters: t.parameters.properties, | |
| parameters: t.parameters, |
src/meta-tools.ts
Outdated
| const raw = typeof inputParams === 'string' ? JSON.parse(inputParams) : inputParams || {}; | ||
| const parsed = searchInputSchema.parse(raw); | ||
|
|
||
| const results = await toolset.searchTools(parsed.query, { | ||
| connector: parsed.connector ?? options.connector, | ||
| topK: parsed.top_k ?? options.topK ?? 5, | ||
| minSimilarity: options.minSimilarity, | ||
| search: options.search, | ||
| accountIds: options.accountIds, | ||
| }); | ||
|
|
||
| return { | ||
| tools: results.toArray().map((t) => ({ | ||
| name: t.name, | ||
| description: t.description, | ||
| parameters: t.parameters.properties, | ||
| })), | ||
| total: results.length, | ||
| query: parsed.query, | ||
| }; |
There was a problem hiding this comment.
Both tool_search and tool_execute do a bare JSON.parse() when inputParams is a string. If the model sends non-JSON (common with tool calls), this will throw a SyntaxError and can break agent loops (especially the manual OpenAI loop in the example). Mirror the error handling pattern used in createFeedbackTool (catch parse/Zod errors and return a structured { error: ... } response or throw a StackOneError with context).
| const raw = typeof inputParams === 'string' ? JSON.parse(inputParams) : inputParams || {}; | |
| const parsed = searchInputSchema.parse(raw); | |
| const results = await toolset.searchTools(parsed.query, { | |
| connector: parsed.connector ?? options.connector, | |
| topK: parsed.top_k ?? options.topK ?? 5, | |
| minSimilarity: options.minSimilarity, | |
| search: options.search, | |
| accountIds: options.accountIds, | |
| }); | |
| return { | |
| tools: results.toArray().map((t) => ({ | |
| name: t.name, | |
| description: t.description, | |
| parameters: t.parameters.properties, | |
| })), | |
| total: results.length, | |
| query: parsed.query, | |
| }; | |
| try { | |
| const raw = typeof inputParams === 'string' ? JSON.parse(inputParams) : inputParams || {}; | |
| const parsed = searchInputSchema.parse(raw); | |
| const results = await toolset.searchTools(parsed.query, { | |
| connector: parsed.connector ?? options.connector, | |
| topK: parsed.top_k ?? options.topK ?? 5, | |
| minSimilarity: options.minSimilarity, | |
| search: options.search, | |
| accountIds: options.accountIds, | |
| }); | |
| return { | |
| tools: results.toArray().map((t) => ({ | |
| name: t.name, | |
| description: t.description, | |
| parameters: t.parameters.properties, | |
| })), | |
| total: results.length, | |
| query: parsed.query, | |
| }; | |
| } catch (err) { | |
| // Handle invalid JSON or schema validation errors gracefully to avoid breaking agent loops. | |
| if (err instanceof SyntaxError || err instanceof z.ZodError) { | |
| const message = (err as Error).message ?? 'Invalid input for tool_search'; | |
| const type = | |
| err instanceof SyntaxError | |
| ? 'invalid_json' | |
| : 'invalid_parameters'; | |
| return { | |
| error: { | |
| message, | |
| type, | |
| }, | |
| } as JsonObject; | |
| } | |
| // Re-throw unexpected errors to preserve existing behavior. | |
| throw err; | |
| } |
src/meta-tools.ts
Outdated
| const raw = typeof inputParams === 'string' ? JSON.parse(inputParams) : inputParams || {}; | ||
| const parsed = executeInputSchema.parse(raw); | ||
|
|
There was a problem hiding this comment.
tool_execute also uses a bare JSON.parse() and Zod parse() without handling errors. A malformed string payload or validation failure will throw and can crash consumers that call tool.execute() directly (like the OpenAI agent loop in examples/meta-tools.ts). Consider catching JSON/Zod errors here and returning a structured error payload so the LLM can self-correct and retry.
| const raw = typeof inputParams === 'string' ? JSON.parse(inputParams) : inputParams || {}; | |
| const parsed = executeInputSchema.parse(raw); | |
| let raw: JsonObject; | |
| // Safely parse JSON/string input into an object | |
| try { | |
| raw = | |
| typeof inputParams === 'string' | |
| ? (JSON.parse(inputParams) as JsonObject) | |
| : (inputParams as JsonObject) || {}; | |
| } catch (error) { | |
| // Return JSON parse errors to the LLM so it can correct malformed payloads | |
| return { | |
| error: 'Invalid JSON in tool_execute parameters.', | |
| details: error instanceof Error ? error.message : String(error), | |
| }; | |
| } | |
| let parsed: z.infer<typeof executeInputSchema>; | |
| // Safely validate input against the execute input schema | |
| try { | |
| parsed = executeInputSchema.parse(raw); | |
| } catch (error) { | |
| if (error instanceof z.ZodError) { | |
| return { | |
| error: 'Invalid parameters for tool_execute.', | |
| validation_errors: error.errors, | |
| tool_name: (raw as any)?.tool_name, | |
| }; | |
| } | |
| throw error; | |
| } |
src/meta-tools.ts
Outdated
| const allTools = await toolset.fetchTools({ accountIds: options.accountIds }); | ||
| const target = allTools.getTool(parsed.tool_name); | ||
|
|
||
| if (!target) { | ||
| return { | ||
| error: `Tool "${parsed.tool_name}" not found. Use tool_search to find available tools.`, | ||
| }; | ||
| } |
There was a problem hiding this comment.
MetaToolsOptions documents scoping by accountIds and connector, but tool_execute doesn't enforce connector scoping and has ambiguous behavior with multiple accountIds. fetchTools({ accountIds }) will produce duplicate tool names across accounts and getTool() returns the first match, so executions can run against an unintended account. Consider either (a) requiring an explicit account_id input to tool_execute when multiple accounts are configured and selecting the correct tool instance, or (b) rejecting multi-account execution for meta-tools; and also enforce options.connector by validating target.connector before executing.
src/meta-tools.ts
Outdated
| const allTools = await toolset.fetchTools({ accountIds: options.accountIds }); | ||
| const target = allTools.getTool(parsed.tool_name); | ||
|
|
There was a problem hiding this comment.
tool_execute calls toolset.fetchTools() on every execution, which (today) establishes an MCP connection and lists tools each time. In typical agent workflows (search → execute → execute ...), this can become a significant latency/cost multiplier and duplicates work already done by tool_search. Consider caching the fetched Tools within the meta-tools closure (e.g., a shared promise) or adding/using a toolset-level cache with invalidation.
src/toolsets.ts
Outdated
| getMetaTools(options?: MetaToolsOptions): Tools { | ||
| if (this.searchConfig === null) { | ||
| throw new ToolSetConfigError( | ||
| 'Search is disabled. Initialize StackOneToolSet with a search config to enable.', | ||
| ); | ||
| } | ||
|
|
||
| const searchTool = createSearchTool(this, options); | ||
| const executeTool = createExecuteTool(this, options); | ||
| return new Tools([searchTool, executeTool]); |
There was a problem hiding this comment.
New public behavior (StackOneToolSet.getMetaTools() + tool_search/tool_execute) is added without accompanying unit tests. The repo already has extensive Vitest coverage for toolsets/tool behavior; adding tests for meta-tools (schema shape, connector/account scoping, and error handling) would prevent regressions.
| // Agent loop — let the LLM drive search and execution | ||
| let continueLoop = true; | ||
| while (continueLoop) { | ||
| const response = await client.chat.completions.create({ | ||
| model: 'gpt-4o', | ||
| messages, | ||
| tools: openaiTools, | ||
| tool_choice: 'auto', | ||
| }); |
There was a problem hiding this comment.
The OpenAI agent loop (while (continueLoop)) has no hard iteration/tool-call limit. If the model keeps emitting tool calls (or gets into a bad loop), this example can run indefinitely and incur unbounded API usage. Add a max-iterations/step counter similar to the AI SDK example’s stopWhen: stepCountIs(...) and break with a clear message when exceeded.
There was a problem hiding this comment.
3 issues found across 4 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="examples/meta-tools.ts">
<violation number="1" location="examples/meta-tools.ts:106">
P2: Add a max-iteration bound to the OpenAI agent loop to prevent infinite tool-call cycles.</violation>
</file>
<file name="src/meta-tools.ts">
<violation number="1" location="src/meta-tools.ts:72">
P2: Malformed JSON tool arguments can crash meta tool execution because `JSON.parse` errors are not handled.</violation>
<violation number="2" location="src/meta-tools.ts:87">
P2: `tool_search` returns an incomplete parameter schema (`properties` only). Return the full tool schema so required fields are preserved.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
src/meta-tools.ts
Outdated
| const results = await toolset.searchTools(parsed.query, { | ||
| connector: parsed.connector ?? options.connector, | ||
| topK: parsed.top_k ?? options.topK ?? 5, | ||
| minSimilarity: options.minSimilarity, | ||
| search: options.search, | ||
| accountIds: options.accountIds, | ||
| }); |
There was a problem hiding this comment.
🔴 createSearchTool hardcodes topK: parsed.top_k ?? options.topK ?? 5 (line 77), which always produces a non-nullish value. This prevents searchTools() from falling through to the toolset-level searchConfig.topK (line 507: options?.topK ?? this.searchConfig.topK), so new StackOneToolSet({ search: { topK: 10 } }).getMetaTools() silently ignores the configured topK=10 and uses 5 instead. Remove the ?? 5 fallback and let searchTools() handle its own defaults, consistent with how getSearchTool() merges this.searchConfig.
Extended reasoning...
What the bug is
In createSearchTool (src/meta-tools.ts:77), the topK value passed to toolset.searchTools() is computed as:
topK: parsed.top_k ?? options.topK ?? 5This fallback chain always produces a non-nullish number (at minimum, the hardcoded 5). This is problematic because searchTools() in toolsets.ts has its own defaulting logic at line 507:
const topK = options?.topK ?? this.searchConfig.topK;Since createSearchTool always passes a concrete topK value (never undefined), the this.searchConfig.topK fallback in searchTools() is never reached for meta-tool invocations.
Step-by-step proof
- User creates:
const toolset = new StackOneToolSet({ search: { topK: 10 } }) - The constructor sets
this.searchConfig = { method: "auto", topK: 10 } - User calls
toolset.getMetaTools()with no options, sooptionsisundefined getMetaToolscallscreateSearchTool(this, undefined), which defaultsoptions = {}- When
tool_searchexecutes,parsed.top_kisundefined(LLM did not pass it),options.topKisundefined(no MetaToolsOptions.topK) - The expression evaluates:
undefined ?? undefined ?? 5= 5 searchTools()receivestopK: 5, so line 507 evaluates:5 ?? 10= 5- The toolset-level
topK: 10is silently ignored
Why existing code does not prevent this
The searchTools() method correctly handles undefined topK by falling back to this.searchConfig.topK. However, createSearchTool never gives it the chance — it always provides an explicit value due to the ?? 5 fallback. Compare with getSearchTool() (line 422-430), which correctly merges this.searchConfig via spread: { ...this.searchConfig, method: options.search }, preserving the toolset-level topK and minSimilarity.
Impact
Any user who configures topK (or minSimilarity) at the toolset level and uses getMetaTools() will have their configuration silently ignored. The meta-tools will always use the hardcoded default of 5 results unless the LLM explicitly passes top_k in its tool call parameters. This is especially confusing because getSearchTool() and direct searchTools() calls honor the toolset config correctly.
How to fix
Remove the ?? 5 fallback on line 77 so that topK can be undefined when neither the LLM nor MetaToolsOptions provides it:
topK: parsed.top_k ?? options.topK,This lets searchTools() handle its own defaults via options?.topK ?? this.searchConfig.topK. The same pattern should be applied to minSimilarity and search on lines 78-79 to ensure getMetaTools is consistent with getSearchTool in respecting toolset-level config.
src/meta-tools.ts
Outdated
| tools: results.toArray().map((t) => ({ | ||
| name: t.name, | ||
| description: t.description, | ||
| parameters: t.parameters.properties as unknown as JsonObject, | ||
| })), |
There was a problem hiding this comment.
🔴 tool_search returns only t.parameters.properties (line 87) instead of the full parameter schema, stripping the required array and type field. This means the LLM cannot determine which parameters are mandatory, leading to tool_execute failures when required parameters are omitted. Fix by returning parameters: t.parameters or parameters: { type: t.parameters.type, properties: t.parameters.properties, required: t.parameters.required }.
Extended reasoning...
What the bug is
In createSearchTool (src/meta-tools.ts:84-88), the search results map each tool's parameters as:
parameters: t.parameters.properties,The ToolParameters interface (defined in src/types.ts:162-166) has three fields: type (string), properties (JsonSchemaProperties), and required (string[]). By returning only .properties, both the type and required fields are stripped from the schema returned to the LLM.
Step-by-step proof
Consider a tool with this parameter schema:
{
"type": "object",
"properties": {
"employee_id": { "type": "string", "description": "The employee ID" },
"start_date": { "type": "string", "description": "Start date" },
"reason": { "type": "string", "description": "Optional reason" }
},
"required": ["employee_id", "start_date"]
}When tool_search returns this tool, the LLM receives:
{
"name": "create_time_off_request",
"description": "Create a time off request",
"parameters": {
"employee_id": { "type": "string", "description": "The employee ID" },
"start_date": { "type": "string", "description": "Start date" },
"reason": { "type": "string", "description": "Optional reason" }
}
}The required: ["employee_id", "start_date"] array is completely absent. The LLM has no way to distinguish mandatory parameters from optional ones. It may reasonably call tool_execute with only { "reason": "vacation" }, omitting the required employee_id and start_date, causing an execution failure.
Why existing code doesn't prevent it
The tool_search description explicitly tells the LLM: "Use the returned parameter schemas to know exactly what to pass when calling tool_execute." The LLM is supposed to rely on this schema as the source of truth. Since the schema is incomplete, the LLM's guidance is wrong.
Impact
This undermines the core purpose of the tool_search → tool_execute meta-tool pattern. Every tool with required parameters is affected. The LLM will frequently fail on the first tool_execute call, requiring trial-and-error retries that waste tokens and degrade the user experience.
Fix
Change line 87 from parameters: t.parameters.properties to parameters: t.parameters (or explicitly include all three fields). This preserves the full JSON Schema structure the LLM needs to construct valid tool_execute calls.
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/meta-tools.ts">
<violation number="1" location="src/meta-tools.ts:159">
P2: Caching `fetchTools()` in `tool_execute` can serve stale tool definitions and break execution after account/tool availability changes.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="examples/meta-tools.ts">
<violation number="1" location="examples/meta-tools.ts:131">
P2: Skipping non-function tool calls leaves unmatched `tool_call_id`s in message history, which can break the next Chat Completions request. Handle unsupported types explicitly instead of silently continuing.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| if (toolCall.type !== 'function') { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
P2: Skipping non-function tool calls leaves unmatched tool_call_ids in message history, which can break the next Chat Completions request. Handle unsupported types explicitly instead of silently continuing.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/meta-tools.ts, line 131:
<comment>Skipping non-function tool calls leaves unmatched `tool_call_id`s in message history, which can break the next Chat Completions request. Handle unsupported types explicitly instead of silently continuing.</comment>
<file context>
@@ -128,6 +128,10 @@ const metaToolsWithOpenAI = async (): Promise<void> => {
// Execute each tool call
for (const toolCall of choice.message.tool_calls) {
+ if (toolCall.type !== 'function') {
+ continue;
+ }
</file context>
| if (toolCall.type !== 'function') { | |
| continue; | |
| } | |
| if (toolCall.type !== 'function') { | |
| throw new Error(`Unsupported tool call type: ${toolCall.type}`); | |
| } |
There was a problem hiding this comment.
2 issues found across 5 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/meta-tools.test.ts">
<violation number="1" location="src/meta-tools.test.ts:304">
P2: These tests reimplement `StackOneToolSet.openai()` in a mock object, so they can pass without exercising the real method.</violation>
</file>
<file name="src/toolsets.ts">
<violation number="1" location="src/toolsets.ts:338">
P1: This change turns search off by default, so existing `getSearchTool()`, `searchTools()`, and `getMetaTools()` calls now throw unless callers add `search: {}` explicitly.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| // Resolve search config: undefined → defaults, null → disabled, object → custom | ||
| this.searchConfig = config?.search === null ? null : { method: 'auto', ...config?.search }; | ||
| // Resolve search config: undefined/null → disabled, object → custom with defaults | ||
| this.searchConfig = config?.search != null ? { method: 'auto', ...config.search } : null; |
There was a problem hiding this comment.
P1: This change turns search off by default, so existing getSearchTool(), searchTools(), and getMetaTools() calls now throw unless callers add search: {} explicitly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/toolsets.ts, line 338:
<comment>This change turns search off by default, so existing `getSearchTool()`, `searchTools()`, and `getMetaTools()` calls now throw unless callers add `search: {}` explicitly.</comment>
<file context>
@@ -318,8 +334,9 @@ export class StackOneToolSet {
- // Resolve search config: undefined → defaults, null → disabled, object → custom
- this.searchConfig = config?.search === null ? null : { method: 'auto', ...config?.search };
+ // Resolve search config: undefined/null → disabled, object → custom with defaults
+ this.searchConfig = config?.search != null ? { method: 'auto', ...config.search } : null;
+ this.executeConfig = config?.execute;
</file context>
| this.searchConfig = config?.search != null ? { method: 'auto', ...config.search } : null; | |
| this.searchConfig = config?.search === null ? null : { method: 'auto', ...config?.search }; |
|
|
||
| const executeConfig = options?.executeConfig; | ||
|
|
||
| const toolset = { |
There was a problem hiding this comment.
P2: These tests reimplement StackOneToolSet.openai() in a mock object, so they can pass without exercising the real method.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/meta-tools.test.ts, line 304:
<comment>These tests reimplement `StackOneToolSet.openai()` in a mock object, so they can pass without exercising the real method.</comment>
<file context>
@@ -262,3 +262,119 @@ describe('createExecuteTool', () => {
+
+ const executeConfig = options?.executeConfig;
+
+ const toolset = {
+ fetchTools,
+ getMetaTools,
</file context>
There was a problem hiding this comment.
4 issues found across 9 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/semantic-search.ts">
<violation number="1" location="src/semantic-search.ts:36">
P3: This doc line claims `searchActionNames` returns connector/description/inputSchema metadata, but the implementation only returns IDs/scores. The contract description is inaccurate.</violation>
<violation number="2" location="src/semantic-search.ts:106">
P3: The JSDoc example uses `result.actionId`, but search results expose `id`. Copy-pasting this example will produce `undefined` for the identifier.</violation>
</file>
<file name="src/toolsets.ts">
<violation number="1" location="src/toolsets.ts:361">
P2: tool_search drops required/metadata by returning only `parameters.properties`, so the LLM can miss required fields. Return the full ToolParameters schema instead.</violation>
<violation number="2" location="src/toolsets.ts:636">
P2: getTools() ignores the constructor executeConfig, so default account scoping is silently dropped for meta tools. Use the same accountIds fallback logic as openai().</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| tools: results.toArray().map((t) => ({ | ||
| name: t.name, | ||
| description: t.description, | ||
| parameters: t.parameters.properties as unknown as JsonObject, |
There was a problem hiding this comment.
P2: tool_search drops required/metadata by returning only parameters.properties, so the LLM can miss required fields. Return the full ToolParameters schema instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/toolsets.ts, line 361:
<comment>tool_search drops required/metadata by returning only `parameters.properties`, so the LLM can miss required fields. Return the full ToolParameters schema instead.</comment>
<file context>
@@ -268,6 +270,170 @@ export class SearchTool {
+ tools: results.toArray().map((t) => ({
+ name: t.name,
+ description: t.description,
+ parameters: t.parameters.properties as unknown as JsonObject,
+ })),
+ total: results.length,
</file context>
| parameters: t.parameters.properties as unknown as JsonObject, | |
| parameters: t.parameters as unknown as JsonObject, |
| * @returns Tools collection containing tool_search and tool_execute | ||
| */ | ||
| getTools(options?: { accountIds?: string[] }): Tools { | ||
| return this.buildTools(options?.accountIds); |
There was a problem hiding this comment.
P2: getTools() ignores the constructor executeConfig, so default account scoping is silently dropped for meta tools. Use the same accountIds fallback logic as openai().
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/toolsets.ts, line 636:
<comment>getTools() ignores the constructor executeConfig, so default account scoping is silently dropped for meta tools. Use the same accountIds fallback logic as openai().</comment>
<file context>
@@ -451,36 +624,30 @@ export class StackOneToolSet {
*/
- getMetaTools(options?: MetaToolsOptions): Tools {
+ getTools(options?: { accountIds?: string[] }): Tools {
+ return this.buildTools(options?.accountIds);
+ }
+
</file context>
| return this.buildTools(options?.accountIds); | |
| const effectiveAccountIds = options?.accountIds ?? this.executeConfig?.accountIds; | |
| return this.buildTools(effectiveAccountIds); |
Summary
getMetaTools()method toStackOneToolSetthat returnstool_search+tool_executeas LLM-callable toolsload all tools upfront
Toolscollection so all existing framework converters work automatically(
.toOpenAI(),.toAISDK(),.toAnthropic(), etc.)Details
tool_searchdelegates tosearchTools()internally, returns tool names, descriptions,and parameter schemas
tool_executefetches the real tool by name and callsexecute()— API errors arereturned to the LLM (not thrown) so the agent can retry with different parameters
createFeedbackTool()(BaseTool with custom execute override,LocalExecuteConfig, Zod input validation)
examples/meta-tools.tsdemonstrates the full agent loop with Calendly usingVercel AI SDK and OpenAI Chat Completions
Test plan
pnpm build— builds successfullypnpm test— existing tests passSTACKONE_API_KEY=xxx OPENAI_API_KEY=xxx STACKONE_ACCOUNT_ID=xxx pnpm exec tsx examples/meta-tools.ts— end-to-end agent loop worksgetMetaTools()returns aToolscollection with 2 tools.toOpenAI(),.toAISDK(),.toAnthropic()converters work on meta toolsSummary by cubic
Add LLM-driven meta tools so models can search for and run tools on demand without loading everything up front.
getTools()exposestool_search+tool_execute, andopenai({ mode: 'search_and_execute' })returns them in OpenAI format.New Features
getTools({ accountIds? })returnstool_search+tool_execute; throws if search is disabled.tool_searchcallssearchTools()and returns tool names, descriptions, and parameter properties; accepts object or JSON string; supportsconnectorandtop_k; validates input and returns clear errors.tool_executeresolves bytool_nameand runs the tool; caches fetched tools; accepts object or JSON string; passesaccountIds; unknown tools and API errors return structured error dicts (error,status_code,response_body,tool_name).StackOneToolSet.openai({ mode: 'search_and_execute', accountIds? })returns meta tools; otherwise returns all tools. Per-callaccountIdsoverride constructorexecute.accountIds.executeconfig toStackOneToolSetfor default account scoping; exportExecuteToolsConfig.examples/agent-tool-search.tsshows agent loops with@ai-sdk/openaiandopenai.Migration
StackOneToolSetwith{ search: {} }; otherwisegetTools()andopenai({ mode: 'search_and_execute' })will throw.searchActionNames()now returns action IDs (e.g.,bamboohr_1.0.0_bamboohr_create_employee_global). Useresult.idand pass these IDs tofetchTools({ actions }).Written for commit 160eca8. Summary will update on new commits.