Skip to content

feat(search-tools): LLM-driven search and execute and new API#325

Merged
glebedel merged 14 commits intomainfrom
meta_tools
Mar 25, 2026
Merged

feat(search-tools): LLM-driven search and execute and new API#325
glebedel merged 14 commits intomainfrom
meta_tools

Conversation

@shashi-stackone
Copy link
Copy Markdown
Contributor

@shashi-stackone shashi-stackone commented Mar 11, 2026

Summary

  • Add getMetaTools() method to StackOneToolSet that returns tool_search +
    tool_execute as LLM-callable tools
  • The LLM autonomously searches for relevant tools and executes them, replacing the need to
    load all tools upfront
  • Returns a Tools collection so all existing framework converters work automatically
    (.toOpenAI(), .toAISDK(), .toAnthropic(), etc.)

Details

  • tool_search delegates to searchTools() internally, returns tool names, descriptions,
    and parameter schemas
  • tool_execute fetches the real tool by name and calls execute() — API errors are
    returned to the LLM (not thrown) so the agent can retry with different parameters
  • Follows the same pattern as createFeedbackTool() (BaseTool with custom execute override,
    LocalExecuteConfig, Zod input validation)
  • New example examples/meta-tools.ts demonstrates the full agent loop with Calendly using
    Vercel AI SDK and OpenAI Chat Completions

Test plan

  • pnpm build — builds successfully
  • pnpm test — existing tests pass
  • STACKONE_API_KEY=xxx OPENAI_API_KEY=xxx STACKONE_ACCOUNT_ID=xxx pnpm exec tsx examples/meta-tools.ts — end-to-end agent loop works
  • Verify getMetaTools() returns a Tools collection with 2 tools
  • Verify .toOpenAI(), .toAISDK(), .toAnthropic() converters work on meta tools

Summary by cubic

Add LLM-driven meta tools so models can search for and run tools on demand without loading everything up front. getTools() exposes tool_search + tool_execute, and openai({ mode: 'search_and_execute' }) returns them in OpenAI format.

  • New Features

    • getTools({ accountIds? }) returns tool_search + tool_execute; throws if search is disabled.
    • tool_search calls searchTools() and returns tool names, descriptions, and parameter properties; accepts object or JSON string; supports connector and top_k; validates input and returns clear errors.
    • tool_execute resolves by tool_name and runs the tool; caches fetched tools; accepts object or JSON string; passes accountIds; 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-call accountIds override constructor execute.accountIds.
    • Added execute config to StackOneToolSet for default account scoping; export ExecuteToolsConfig. examples/agent-tool-search.ts shows agent loops with @ai-sdk/openai and openai.
  • Migration

    • Search is now opt-in. Initialize StackOneToolSet with { search: {} }; otherwise getTools() and openai({ mode: 'search_and_execute' }) will throw.
    • Semantic search API update: searchActionNames() now returns action IDs (e.g., bamboohr_1.0.0_bamboohr_create_employee_global). Use result.id and pass these IDs to fetchTools({ actions }).

Written for commit 160eca8. Summary will update on new commits.

@shashi-stackone shashi-stackone requested a review from a team as a code owner March 11, 2026 00:17
Copilot AI review requested due to automatic review settings March 11, 2026 00:17
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 11, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@stackone/ai@325

commit: 160eca8

@shashi-stackone shashi-stackone changed the title feat(meta-tools): add getMetaTools() for LLM-driven tool discovery and execution feat(meta-tools): add getMetaTools() for LLM-driven tool discovery and execution Mar 11, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 a Tools collection containing tool_search + tool_execute.
  • Introduce src/meta-tools.ts implementing the local meta tools and their input schemas.
  • Add a runnable example demonstrating usage with Vercel AI SDK and OpenAI Chat Completions; export MetaToolsOptions from 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.

import { z } from 'zod/v4';
import { BaseTool } from './tool';
import type { ExecuteOptions, JsonObject, LocalExecuteConfig, ToolParameters } from './types';
import { StackOneError } from './utils/error-stackone';
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
import { StackOneError } from './utils/error-stackone';

Copilot uses AI. Check for mistakes.
tools: results.toArray().map((t) => ({
name: t.name,
description: t.description,
parameters: t.parameters.properties,
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }).

Suggested change
parameters: t.parameters.properties,
parameters: t.parameters,

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +91
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,
};
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
Comment on lines +134 to +136
const raw = typeof inputParams === 'string' ? JSON.parse(inputParams) : inputParams || {};
const parsed = executeInputSchema.parse(raw);

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
Comment on lines +137 to +144
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.`,
};
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +137 to +139
const allTools = await toolset.fetchTools({ accountIds: options.accountIds });
const target = allTools.getTool(parsed.tool_name);

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
src/toolsets.ts Outdated
Comment on lines +458 to +467
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]);
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +104 to +112
// 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',
});
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +75 to +81
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,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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 ?? 5

This 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

  1. User creates: const toolset = new StackOneToolSet({ search: { topK: 10 } })
  2. The constructor sets this.searchConfig = { method: "auto", topK: 10 }
  3. User calls toolset.getMetaTools() with no options, so options is undefined
  4. getMetaTools calls createSearchTool(this, undefined), which defaults options = {}
  5. When tool_search executes, parsed.top_k is undefined (LLM did not pass it), options.topK is undefined (no MetaToolsOptions.topK)
  6. The expression evaluates: undefined ?? undefined ?? 5 = 5
  7. searchTools() receives topK: 5, so line 507 evaluates: 5 ?? 10 = 5
  8. The toolset-level topK: 10 is 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.

Comment on lines +84 to +88
tools: results.toArray().map((t) => ({
name: t.name,
description: t.description,
parameters: t.parameters.properties as unknown as JsonObject,
})),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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_searchtool_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.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +131 to +133
if (toolCall.type !== 'function') {
continue;
}
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
if (toolCall.type !== 'function') {
continue;
}
if (toolCall.type !== 'function') {
throw new Error(`Unsupported tool call type: ${toolCall.type}`);
}
Fix with Cubic

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
this.searchConfig = config?.search != null ? { method: 'auto', ...config.search } : null;
this.searchConfig = config?.search === null ? null : { method: 'auto', ...config?.search };
Fix with Cubic


const executeConfig = options?.executeConfig;

const toolset = {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

@shashi-stackone shashi-stackone changed the title feat(meta-tools): add getMetaTools() for LLM-driven tool discovery and execution feat(search-tools): LLM-driven search and execute and new API Mar 24, 2026
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
parameters: t.parameters.properties as unknown as JsonObject,
parameters: t.parameters as unknown as JsonObject,
Fix with Cubic

* @returns Tools collection containing tool_search and tool_execute
*/
getTools(options?: { accountIds?: string[] }): Tools {
return this.buildTools(options?.accountIds);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
return this.buildTools(options?.accountIds);
const effectiveAccountIds = options?.accountIds ?? this.executeConfig?.accountIds;
return this.buildTools(effectiveAccountIds);
Fix with Cubic

@shashi-stackone shashi-stackone changed the title feat(search-tools): LLM-driven search and execute and new API feat(search-tools): LLM-driven search and execute and new API Mar 24, 2026
Copy link
Copy Markdown
Contributor

@glebedel glebedel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@glebedel glebedel merged commit e06602b into main Mar 25, 2026
18 checks passed
@glebedel glebedel deleted the meta_tools branch March 25, 2026 12:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants