Skip to content
Merged
167 changes: 167 additions & 0 deletions examples/agent-tool-search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* This example demonstrates the search and execute tools pattern (tool_search + tool_execute)
* for LLM-driven tool discovery and execution.
*
* Instead of loading all tools upfront, the LLM autonomously searches for
* relevant tools and executes them — keeping token usage minimal.
*
* @example
* ```bash
* # Run with required environment variables:
* STACKONE_API_KEY=your-key OPENAI_API_KEY=your-key STACKONE_ACCOUNT_ID=your-account npx tsx examples/agent-tool-search.ts
* ```
*/

import process from 'node:process';
import { openai } from '@ai-sdk/openai';
import { StackOneToolSet } from '@stackone/ai';
import { generateText, stepCountIs } from 'ai';
import OpenAI from 'openai';

const apiKey = process.env.STACKONE_API_KEY;
if (!apiKey) {
console.error('STACKONE_API_KEY environment variable is required');
process.exit(1);
}

if (!process.env.OPENAI_API_KEY) {
console.error('OPENAI_API_KEY environment variable is required');
process.exit(1);
}

const accountId = process.env.STACKONE_ACCOUNT_ID;

/**
* Example 1: Search and execute with Vercel AI SDK
*
* The LLM receives only tool_search and tool_execute — two small tool definitions
* regardless of how many tools exist. It searches for what it needs and executes.
*/
const toolsWithAISDK = async (): Promise<void> => {
console.log('Example 1: Search and execute with Vercel AI SDK\n');

const toolset = new StackOneToolSet({
search: { method: 'semantic', topK: 3 },
...(accountId ? { accountId } : {}),
});

// Get search and execute tools — returns a Tools collection with tool_search + tool_execute
const accountIds = accountId ? [accountId] : [];
const tools = toolset.getTools({ accountIds });

console.log(
`Search and execute: ${tools
.toArray()
.map((t) => t.name)
.join(', ')}`,
);
console.log();

// Pass to the LLM — it will search for calendly tools, then execute
const { text, steps } = await generateText({
model: openai('gpt-5.4'),
tools: await tools.toAISDK(),
prompt: 'List my upcoming Calendly events for the next week.',
stopWhen: stepCountIs(10),
});

console.log('AI Response:', text);
console.log('\nSteps taken:');
for (const step of steps) {
for (const call of step.toolCalls ?? []) {
const args = (call as unknown as Record<string, unknown>).args;
const argsStr = args ? JSON.stringify(args).slice(0, 100) : '{}';
console.log(` - ${call.toolName}(${argsStr})`);
}
}
};

/**
* Example 2: Search and execute with OpenAI Chat Completions
*
* Same pattern, different framework. The search and execute tools convert to any format.
*/
const toolsWithOpenAI = async (): Promise<void> => {
console.log('\nExample 2: Search and execute with OpenAI Chat Completions\n');

const toolset = new StackOneToolSet({
search: { method: 'semantic', topK: 3 },
...(accountId ? { accountId } : {}),
});

const accountIds = accountId ? [accountId] : [];
const tools = toolset.getTools({ accountIds });
const openaiTools = tools.toOpenAI();

const client = new OpenAI();
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
{
role: 'system',
content:
'You are a helpful scheduling assistant. Use tool_search to find relevant tools, then tool_execute to run them. Always read the parameter schemas from tool_search results carefully. If a tool needs a user URI, first search for and call a "get current user" tool to obtain it. If a tool execution fails, try different parameters or a different tool.',
},
{
role: 'user',
content: 'Check my upcoming Calendly events and list them.',
},
];

// Agent loop — let the LLM drive search and execution
const maxIterations = 10;
for (let i = 0; i < maxIterations; i++) {
const response = await client.chat.completions.create({
model: 'gpt-5.4',
messages,
tools: openaiTools,
tool_choice: 'auto',
});
Comment on lines +109 to +117
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.

const choice = response.choices[0];

if (!choice.message.tool_calls?.length) {
console.log('Final response:', choice.message.content);
break;
}

// Add assistant message with tool calls
messages.push(choice.message);

// Execute each tool call
for (const toolCall of choice.message.tool_calls) {
if (toolCall.type !== 'function') {
continue;
}
Comment on lines +131 to +133
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


console.log(`LLM called: ${toolCall.function.name}(${toolCall.function.arguments})`);

const tool = tools.getTool(toolCall.function.name);
if (!tool) {
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify({ error: `Unknown tool: ${toolCall.function.name}` }),
});
continue;
}

const result = await tool.execute(toolCall.function.arguments);
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result),
});
}
}
};

// Main execution
const main = async (): Promise<void> => {
try {
await toolsWithAISDK();
await toolsWithOpenAI();
} catch (error) {
console.error('Error running examples:', error);
}
};

await main();
12 changes: 5 additions & 7 deletions examples/search-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ const searchToolsWithAISDK = async (): Promise<void> => {
const searchToolWithAgentLoop = async (): Promise<void> => {
console.log('\nExample 2: SearchTool for agent loops\n');

// Default constructor — search enabled with method: 'auto'
const toolset = new StackOneToolSet();
// Enable search with default method: 'auto'
const toolset = new StackOneToolSet({ search: {} });

// Per-call options override constructor defaults when needed
const searchTool = toolset.getSearchTool({ search: 'auto' });
Expand All @@ -81,7 +81,7 @@ const searchToolWithAgentLoop = async (): Promise<void> => {
const searchActionNames = async (): Promise<void> => {
console.log('\nExample 3: Lightweight action name search\n');

const toolset = new StackOneToolSet();
const toolset = new StackOneToolSet({ search: {} });

// Search for action names without fetching full tool definitions
const results = await toolset.searchActionNames('manage employees', {
Expand All @@ -90,14 +90,12 @@ const searchActionNames = async (): Promise<void> => {

console.log('Search results:');
for (const result of results) {
console.log(
` - ${result.actionName} (${result.connectorKey}): score=${result.similarityScore.toFixed(2)}`,
);
console.log(` - ${result.id}: score=${result.similarityScore.toFixed(2)}`);
}

// Then fetch specific tools based on the results
if (results.length > 0) {
const topActions = results.filter((r) => r.similarityScore > 0.7).map((r) => r.actionName);
const topActions = results.filter((r) => r.similarityScore > 0.7).map((r) => r.id);
console.log(`\nFetching tools for top actions: ${topActions.join(', ')}`);

const tools = await toolset.fetchTools({ actions: topActions });
Expand Down
Loading
Loading