Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ yarn add @stackone/ai
bun add @stackone/ai
```

### Optional: AI SDK Integration

If you plan to use the AI SDK integration (Vercel AI SDK), install it separately:

```bash
# Using npm
npm install ai

# Using yarn
yarn add ai

# Using bun
bun add ai
```

## Integrations

The OpenAPIToolSet and StackOneToolSet make it super easy to use these APIs as tools in your AI applications.
Expand Down Expand Up @@ -70,7 +85,7 @@ import { StackOneToolSet } from "@stackone/ai";

const toolset = new StackOneToolSet();

const aiSdkTools = toolset.getTools("hris_*").toAISDK();
const aiSdkTools = await toolset.getTools("hris_*").toAISDK();
await generateText({
model: openai("gpt-5"),
tools: aiSdkTools,
Expand Down Expand Up @@ -274,7 +289,7 @@ const metaTools = await tools.metaTools();
const openAITools = metaTools.toOpenAI();

// Use with AI SDK
const aiSdkTools = metaTools.toAISDK();
const aiSdkTools = await metaTools.toAISDK();
```

#### Example: Dynamic Tool Discovery with AI SDK
Expand Down
4 changes: 4 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/ai-sdk-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const aiSdkIntegration = async (): Promise<void> => {
const tools = toolset.getStackOneTools('hris_get_*', accountId);

// Convert to AI SDK tools
const aiSdkTools = tools.toAISDK();
const aiSdkTools = await tools.toAISDK();

// Use max steps to automatically call the tool if it's needed
const { text } = await generateText({
Expand Down
2 changes: 1 addition & 1 deletion examples/human-in-the-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const humanInTheLoopExample = async (): Promise<void> => {
}

// Get the AI SDK version of the tool without the execute function
const tool = createEmployeeTool.toAISDK({
const tool = await createEmployeeTool.toAISDK({
executable: false,
});

Expand Down
2 changes: 1 addition & 1 deletion examples/meta-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const metaToolsWithAISDK = async (): Promise<void> => {

// Get meta tools for dynamic discovery and execution
const metaTools = await allTools.metaTools();
const aiSdkMetaTools = metaTools.toAISDK();
const aiSdkMetaTools = await metaTools.toAISDK();

// Use meta tools to dynamically find and execute relevant tools
const { text, toolCalls } = await generateText({
Expand Down
2 changes: 1 addition & 1 deletion examples/planning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const planningModule = async (): Promise<void> => {
await generateText({
model: openai('gpt-5'),
prompt: 'You are a workplace agent, onboard the latest hires to our systems',
tools: onboardWorkflow.toAISDK(),
tools: await onboardWorkflow.toAISDK(),
maxSteps: 3,
});
};
Expand Down
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@
"ai": "4.x|5.x",
"openai": "5.x|6.x"
},
"peerDependenciesMeta": {
"ai": {
"optional": true
},
"openai": {
"optional": true
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/StackOneHQ/stackone-ai-node.git"
Expand Down
8 changes: 4 additions & 4 deletions src/tests/json-schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,9 @@ describe('Schema Validation', () => {
});

describe('AI SDK Integration', () => {
it('should convert to AI SDK tool format', () => {
it('should convert to AI SDK tool format', async () => {
const tool = createArrayTestTool();
const aiSdkTool = tool.toAISDK();
const aiSdkTool = await tool.toAISDK();

expect(aiSdkTool).toBeDefined();
// The AI SDK tool is an object with the tool name as the key
Expand Down Expand Up @@ -278,7 +278,7 @@ describe('Schema Validation', () => {
expect(arrayWithItems.items.type).toBe('string');
});

it('should handle the problematic nested array case', () => {
it('should handle the problematic nested array case', async () => {
const tool = createNestedArrayTestTool();
const openAIFormat = tool.toOpenAI();
const parameters = openAIFormat.function.parameters;
Expand All @@ -305,7 +305,7 @@ describe('Schema Validation', () => {
expect(aiSchema).toBeDefined();

// Generate the SDK tool and verify its structure
const aiSdkTool = tool.toAISDK();
const aiSdkTool = await tool.toAISDK();
expect(aiSdkTool).toBeDefined();

const toolObj = aiSdkTool[tool.name];
Expand Down
6 changes: 3 additions & 3 deletions src/tests/meta-tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,8 +414,8 @@ describe('Meta Search Tools', () => {
});

describe('AI SDK format', () => {
it('should convert meta tools to AI SDK format', () => {
const aiSdkTools = metaTools.toAISDK();
it('should convert meta tools to AI SDK format', async () => {
const aiSdkTools = await metaTools.toAISDK();

expect(aiSdkTools).toHaveProperty('meta_search_tools');
expect(aiSdkTools).toHaveProperty('meta_execute_tool');
Expand All @@ -425,7 +425,7 @@ describe('Meta Search Tools', () => {
});

it('should execute through AI SDK format', async () => {
const aiSdkTools = metaTools.toAISDK();
const aiSdkTools = await metaTools.toAISDK();

const result = await aiSdkTools.meta_search_tools.execute?.(
{ query: 'ATS candidates', limit: 2 },
Expand Down
24 changes: 12 additions & 12 deletions src/tests/tool.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,10 @@ describe('StackOneTool', () => {
).toBe('string');
});

it('should convert to AI SDK tool format', () => {
it('should convert to AI SDK tool format', async () => {
const tool = createMockTool();

const aiSdkTool = tool.toAISDK();
const aiSdkTool = await tool.toAISDK();

// Test the basic structure
expect(aiSdkTool).toBeDefined();
Expand All @@ -114,10 +114,10 @@ describe('StackOneTool', () => {
expect(schema.properties.id.type).toBe('string');
});

it('should include execution metadata by default in AI SDK conversion', () => {
it('should include execution metadata by default in AI SDK conversion', async () => {
const tool = createMockTool();

const aiSdkTool = tool.toAISDK();
const aiSdkTool = await tool.toAISDK();
const execution = aiSdkTool.test_tool.execution;

expect(execution).toBeDefined();
Expand All @@ -126,15 +126,15 @@ describe('StackOneTool', () => {
expect(execution?.headers).toEqual({});
});

it('should allow disabling execution metadata exposure for AI SDK conversion', () => {
it('should allow disabling execution metadata exposure for AI SDK conversion', async () => {
const tool = createMockTool().setExposeExecutionMetadata(false);

const aiSdkTool = tool.toAISDK();
const aiSdkTool = await tool.toAISDK();

expect(aiSdkTool.test_tool.execution).toBeUndefined();
});

it('should convert complex parameter types to zod schema', () => {
it('should convert complex parameter types to zod schema', async () => {
const complexTool = new BaseTool(
'complex_tool',
'Complex tool',
Expand Down Expand Up @@ -165,7 +165,7 @@ describe('StackOneTool', () => {
}
);

const aiSdkTool = complexTool.toAISDK();
const aiSdkTool = await complexTool.toAISDK();

// Check that the tool is defined
expect(aiSdkTool).toBeDefined();
Expand All @@ -190,7 +190,7 @@ describe('StackOneTool', () => {

it('should execute AI SDK tool with parameters', async () => {
const tool = createMockTool();
const aiSdkTool = tool.toAISDK();
const aiSdkTool = await tool.toAISDK();

if (!aiSdkTool.test_tool.execute) {
throw new Error('test_tool.execute is undefined');
Expand All @@ -212,7 +212,7 @@ describe('StackOneTool', () => {
throw mockError;
});

const aiSdkTool = tool.toAISDK();
const aiSdkTool = await tool.toAISDK();

if (!aiSdkTool.test_tool.execute) {
throw new Error('test_tool.execute is undefined');
Expand Down Expand Up @@ -300,7 +300,7 @@ describe('Tools', () => {
expect(openAITools[1].function.name).toBe('tool2');
});

it('should convert all tools to AI SDK tools', () => {
it('should convert all tools to AI SDK tools', async () => {
const tool1 = createMockTool();
const tool2 = new StackOneTool(
'another_tool',
Expand Down Expand Up @@ -329,7 +329,7 @@ describe('Tools', () => {

const tools = new Tools([tool1, tool2]);

const aiSdkTools = tools.toAISDK();
const aiSdkTools = await tools.toAISDK();

expect(Object.keys(aiSdkTools).length).toBe(2);
expect(aiSdkTools.test_tool).toBeDefined();
Expand Down
18 changes: 14 additions & 4 deletions src/tool.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as orama from '@orama/orama';
import { jsonSchema } from 'ai';
import type { ChatCompletionTool } from 'openai/resources/chat/completions';
import { RequestBuilder } from './modules/requestBuilder';
import type {
Expand Down Expand Up @@ -181,7 +180,7 @@ export class BaseTool {
/**
* Convert the tool to AI SDK format
*/
toAISDK(
async toAISDK(
options: { executable?: boolean; execution?: ToolExecution | false } = {
executable: true,
}
Expand All @@ -193,6 +192,17 @@ export class BaseTool {
additionalProperties: false,
};

/** AI SDK is optional dependency, import only when needed */
let jsonSchema: typeof import('ai').jsonSchema;
try {
const ai = await import('ai');
jsonSchema = ai.jsonSchema;
} catch {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 7, 2025

Choose a reason for hiding this comment

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

This catch block converts every import failure into "AI SDK is not installed", so real runtime errors inside the AI SDK (when it is installed) will be misreported and harder to debug. Please only wrap the missing-module case and rethrow other errors.

Prompt for AI agents
Address the following comment on src/tool.ts at line 144:

<comment>This catch block converts every import failure into &quot;AI SDK is not installed&quot;, so real runtime errors inside the AI SDK (when it is installed) will be misreported and harder to debug. Please only wrap the missing-module case and rethrow other errors.</comment>

<file context>
@@ -114,29 +114,38 @@ export class BaseTool {
+          }),
+        },
+      };
+    } catch {
+      throw new StackOneError(
+        &#39;AI SDK is not installed. Please install it with: npm install [email protected] or bun add [email protected]&#39;
</file context>

✅ Addressed in ef5efc1

throw new StackOneError(
'AI SDK is not installed. Please install it with: npm install [email protected]|5.x or bun add [email protected]|5.x'
);
}

const schemaObject = jsonSchema(schema);
const toolDefinition: Record<string, unknown> = {
inputSchema: schemaObject, // v5
Expand Down Expand Up @@ -346,14 +356,14 @@ export class Tools implements Iterable<BaseTool> {
/**
* Convert all tools to AI SDK format
*/
toAISDK(
async toAISDK(
options: { executable?: boolean; execution?: ToolExecution | false } = {
executable: true,
}
) {
const result: Record<string, unknown> = {};
for (const tool of this.tools) {
Object.assign(result, tool.toAISDK(options));
Object.assign(result, await tool.toAISDK(options));
}
return result;
}
Expand Down
4 changes: 2 additions & 2 deletions src/toolsets/tests/stackone.mcp-fetch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,14 @@ describe('ToolSet.fetchTools (MCP + RPC integration)', () => {
const tool = tools.toArray()[0];
expect(tool.name).toBe('dummy_action');

const aiTools = tool.toAISDK({ executable: false });
const aiTools = await tool.toAISDK({ executable: false });
const aiToolDefinition = aiTools.dummy_action;
expect(aiToolDefinition).toBeDefined();
expect(aiToolDefinition.description).toBe('Dummy tool');
expect(aiToolDefinition.inputSchema.jsonSchema.properties.foo.type).toBe('string');
expect(aiToolDefinition.execution).toBeUndefined();

const executableTool = tool.toAISDK().dummy_action;
const executableTool = (await tool.toAISDK()).dummy_action;
const result = await executableTool.execute({ foo: 'bar' });

expect(stackOneClient.actions.rpcAction).toHaveBeenCalledWith({
Expand Down
Loading