Skip to content

Commit 6b9bb8b

Browse files
Port the search execute changes to the node
1 parent f49d51a commit 6b9bb8b

5 files changed

Lines changed: 187 additions & 6 deletions

File tree

examples/search-tools.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ const searchToolsWithAISDK = async (): Promise<void> => {
5959
const searchToolWithAgentLoop = async (): Promise<void> => {
6060
console.log('\nExample 2: SearchTool for agent loops\n');
6161

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

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

84-
const toolset = new StackOneToolSet();
84+
const toolset = new StackOneToolSet({ search: {} });
8585

8686
// Search for action names without fetching full tool definitions
8787
const results = await toolset.searchActionNames('manage employees', {

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export {
1616
ToolSetLoadError,
1717
type AuthenticationConfig,
1818
type BaseToolSetConfig,
19+
type ExecuteToolsConfig,
1920
type SearchMode,
2021
type SearchToolsOptions,
2122
type SearchActionNamesOptions,

src/meta-tools.test.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,3 +262,119 @@ describe('createExecuteTool', () => {
262262
expect(result).toEqual({ ok: true });
263263
});
264264
});
265+
266+
describe('StackOneToolSet.openai()', () => {
267+
function createMockToolSetInstance(options?: {
268+
executeConfig?: { accountIds?: string[] };
269+
searchConfig?: Record<string, unknown>;
270+
}): {
271+
toolset: {
272+
fetchTools: ReturnType<typeof vi.fn>;
273+
getMetaTools: ReturnType<typeof vi.fn>;
274+
openai: (opts?: { mode?: 'search_and_execute'; accountIds?: string[] }) => Promise<unknown[]>;
275+
};
276+
} {
277+
const mockTool = new BaseTool(
278+
'test_tool',
279+
'A test tool',
280+
{ type: 'object', properties: {} } satisfies ToolParameters,
281+
{ kind: 'local', identifier: 'test:mock' },
282+
);
283+
const tools = new Tools([mockTool]);
284+
285+
const metaSearchTool = new BaseTool(
286+
'tool_search',
287+
'Search for tools',
288+
{ type: 'object', properties: { query: { type: 'string' } } } satisfies ToolParameters,
289+
{ kind: 'local', identifier: 'meta:search' },
290+
);
291+
const metaExecuteTool = new BaseTool(
292+
'tool_execute',
293+
'Execute a tool',
294+
{ type: 'object', properties: { tool_name: { type: 'string' } } } satisfies ToolParameters,
295+
{ kind: 'local', identifier: 'meta:execute' },
296+
);
297+
const metaTools = new Tools([metaSearchTool, metaExecuteTool]);
298+
299+
const fetchTools = vi.fn().mockResolvedValue(tools);
300+
const getMetaTools = vi.fn().mockReturnValue(metaTools);
301+
302+
const executeConfig = options?.executeConfig;
303+
304+
const toolset = {
305+
fetchTools,
306+
getMetaTools,
307+
async openai(opts?: { mode?: 'search_and_execute'; accountIds?: string[] }): Promise<unknown[]> {
308+
const effectiveAccountIds = opts?.accountIds ?? executeConfig?.accountIds;
309+
310+
if (opts?.mode === 'search_and_execute') {
311+
return getMetaTools({ accountIds: effectiveAccountIds }).toOpenAI();
312+
}
313+
314+
const fetchedTools = await fetchTools({ accountIds: effectiveAccountIds });
315+
return fetchedTools.toOpenAI();
316+
},
317+
};
318+
319+
return { toolset };
320+
}
321+
322+
it('default fetches all tools', async () => {
323+
const { toolset } = createMockToolSetInstance();
324+
325+
const result = await toolset.openai();
326+
327+
expect(toolset.fetchTools).toHaveBeenCalledOnce();
328+
expect(toolset.fetchTools).toHaveBeenCalledWith({ accountIds: undefined });
329+
expect(result).toHaveLength(1);
330+
expect(result[0]).toHaveProperty('type', 'function');
331+
});
332+
333+
it('search_and_execute returns meta tools', async () => {
334+
const { toolset } = createMockToolSetInstance();
335+
336+
const result = await toolset.openai({ mode: 'search_and_execute' });
337+
338+
expect(toolset.getMetaTools).toHaveBeenCalledOnce();
339+
expect(toolset.fetchTools).not.toHaveBeenCalled();
340+
expect(result).toHaveLength(2);
341+
});
342+
343+
it('passes accountIds to fetchTools', async () => {
344+
const { toolset } = createMockToolSetInstance();
345+
346+
await toolset.openai({ accountIds: ['acc-1'] });
347+
348+
expect(toolset.fetchTools).toHaveBeenCalledWith({ accountIds: ['acc-1'] });
349+
});
350+
351+
it('uses executeConfig.accountIds as fallback', async () => {
352+
const { toolset } = createMockToolSetInstance({
353+
executeConfig: { accountIds: ['default-acc'] },
354+
});
355+
356+
await toolset.openai();
357+
358+
expect(toolset.fetchTools).toHaveBeenCalledWith({ accountIds: ['default-acc'] });
359+
});
360+
361+
it('accountIds overrides executeConfig', async () => {
362+
const { toolset } = createMockToolSetInstance({
363+
executeConfig: { accountIds: ['default-acc'] },
364+
});
365+
366+
await toolset.openai({ accountIds: ['override-acc'] });
367+
368+
expect(toolset.fetchTools).toHaveBeenCalledWith({ accountIds: ['override-acc'] });
369+
});
370+
371+
it('search_and_execute with executeConfig passes accountIds to getMetaTools', async () => {
372+
const { toolset } = createMockToolSetInstance({
373+
executeConfig: { accountIds: ['meta-acc'] },
374+
});
375+
376+
await toolset.openai({ mode: 'search_and_execute' });
377+
378+
expect(toolset.getMetaTools).toHaveBeenCalledWith({ accountIds: ['meta-acc'] });
379+
});
380+
});

src/toolsets.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,7 @@ describe('StackOneToolSet', () => {
719719
baseUrl: TEST_BASE_URL,
720720
apiKey: 'test-key',
721721
accountId: 'mixed',
722+
search: {},
722723
});
723724

724725
// Mock the semantic search endpoint
@@ -763,6 +764,7 @@ describe('StackOneToolSet', () => {
763764
baseUrl: TEST_BASE_URL,
764765
apiKey: 'test-key',
765766
accountId: 'mixed',
767+
search: {},
766768
});
767769

768770
// Mock semantic search to fail
@@ -787,6 +789,7 @@ describe('StackOneToolSet', () => {
787789
baseUrl: TEST_BASE_URL,
788790
apiKey: 'test-key',
789791
accountId: 'mixed',
792+
search: {},
790793
});
791794

792795
// Mock semantic search to fail
@@ -806,6 +809,7 @@ describe('StackOneToolSet', () => {
806809
baseUrl: TEST_BASE_URL,
807810
apiKey: 'test-key',
808811
accountId: 'mixed',
812+
search: {},
809813
});
810814

811815
const tools = await toolset.searchTools('list employees', {
@@ -827,6 +831,7 @@ describe('StackOneToolSet', () => {
827831
baseUrl: TEST_BASE_URL,
828832
apiKey: 'test-key',
829833
accountId: 'test-account',
834+
search: {},
830835
});
831836

832837
// test-account only has dummy_action which has a connector prefix "dummy"
@@ -854,6 +859,7 @@ describe('StackOneToolSet', () => {
854859
baseUrl: TEST_BASE_URL,
855860
apiKey: 'test-key',
856861
accountId: 'mixed',
862+
search: {},
857863
});
858864

859865
server.use(
@@ -886,6 +892,7 @@ describe('StackOneToolSet', () => {
886892
baseUrl: TEST_BASE_URL,
887893
apiKey: 'test-key',
888894
accountId: 'mixed',
895+
search: {},
889896
});
890897

891898
server.use(
@@ -904,6 +911,7 @@ describe('StackOneToolSet', () => {
904911
const toolset = new StackOneToolSet({
905912
baseUrl: TEST_BASE_URL,
906913
apiKey: 'test-key',
914+
search: {},
907915
});
908916

909917
const searchTool = toolset.getSearchTool();
@@ -915,6 +923,7 @@ describe('StackOneToolSet', () => {
915923
baseUrl: TEST_BASE_URL,
916924
apiKey: 'test-key',
917925
accountId: 'mixed',
926+
search: {},
918927
});
919928

920929
server.use(
@@ -947,6 +956,7 @@ describe('StackOneToolSet', () => {
947956
baseUrl: TEST_BASE_URL,
948957
apiKey: 'test-key',
949958
accountId: 'mixed',
959+
search: {},
950960
});
951961

952962
// Create search tool with local mode

src/toolsets.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,15 @@ interface MultipleAccountsConfig {
125125
*/
126126
type AccountConfig = SimplifyDeep<MergeExclusive<SingleAccountConfig, MultipleAccountsConfig>>;
127127

128+
/**
129+
* Execution configuration for the StackOneToolSet constructor.
130+
* Controls default account scoping for tool execution in meta tools.
131+
*/
132+
export interface ExecuteToolsConfig {
133+
/** Account IDs to scope tool discovery and execution. */
134+
accountIds?: string[];
135+
}
136+
128137
/**
129138
* Base configuration for StackOne toolset (without account options)
130139
*/
@@ -135,13 +144,19 @@ interface StackOneToolSetBaseConfig extends BaseToolSetConfig {
135144
* Search configuration. Controls default search behavior for `searchTools()`,
136145
* `getSearchTool()`, and `searchActionNames()`.
137146
*
138-
* - Omit or pass `undefined` → search enabled with defaults (`method: 'auto'`)
147+
* - Omit or pass `undefined` → search disabled (`null`)
139148
* - Pass `null` → search disabled
149+
* - Pass `{}` or `{ method: 'auto' }` → search enabled with defaults
140150
* - Pass `{ method, topK, minSimilarity }` → search enabled with custom defaults
141151
*
142152
* Per-call options always override these defaults.
143153
*/
144154
search?: SearchConfig | null;
155+
/**
156+
* Execution configuration. Controls default account scoping for tool execution.
157+
* Pass `{ accountIds: ['acc-1'] }` to scope meta tools to specific accounts.
158+
*/
159+
execute?: ExecuteToolsConfig;
145160
}
146161

147162
/**
@@ -262,6 +277,7 @@ export class StackOneToolSet {
262277
private headers: Record<string, string>;
263278
private rpcClient?: RpcClient;
264279
private readonly searchConfig: SearchConfig | null;
280+
private readonly executeConfig: ExecuteToolsConfig | undefined;
265281

266282
/**
267283
* Account ID for StackOne API
@@ -318,8 +334,9 @@ export class StackOneToolSet {
318334
this.accountId = accountId;
319335
this.accountIds = config?.accountIds ?? [];
320336

321-
// Resolve search config: undefined → defaults, null → disabled, object → custom
322-
this.searchConfig = config?.search === null ? null : { method: 'auto', ...config?.search };
337+
// Resolve search config: undefined/null → disabled, object → custom with defaults
338+
this.searchConfig = config?.search != null ? { method: 'auto', ...config.search } : null;
339+
this.executeConfig = config?.execute;
323340

324341
// Set Authentication headers if provided
325342
if (this.authentication) {
@@ -467,6 +484,43 @@ export class StackOneToolSet {
467484
return new Tools([searchTool, executeTool]);
468485
}
469486

487+
/**
488+
* Get tools in OpenAI function calling format.
489+
*
490+
* @param options - Options
491+
* @param options.mode - Tool mode.
492+
* `undefined` (default): fetch all tools and convert to OpenAI format.
493+
* `"search_and_execute"`: return two meta tools (tool_search + tool_execute)
494+
* that let the LLM discover and execute tools on-demand.
495+
* @param options.accountIds - Account IDs to scope tools. Overrides the `execute`
496+
* config from the constructor.
497+
* @returns List of tool definitions in OpenAI function format.
498+
*
499+
* @example
500+
* ```typescript
501+
* // All tools
502+
* const toolset = new StackOneToolSet();
503+
* const tools = await toolset.openai();
504+
*
505+
* // Meta tools for agent-driven discovery
506+
* const toolset = new StackOneToolSet({ search: {} });
507+
* const tools = await toolset.openai({ mode: 'search_and_execute' });
508+
* ```
509+
*/
510+
async openai(options?: {
511+
mode?: 'search_and_execute';
512+
accountIds?: string[];
513+
}): Promise<ReturnType<Tools['toOpenAI']>> {
514+
const effectiveAccountIds = options?.accountIds ?? this.executeConfig?.accountIds;
515+
516+
if (options?.mode === 'search_and_execute') {
517+
return this.getMetaTools({ accountIds: effectiveAccountIds }).toOpenAI();
518+
}
519+
520+
const tools = await this.fetchTools({ accountIds: effectiveAccountIds });
521+
return tools.toOpenAI();
522+
}
523+
470524
/**
471525
* Search for and fetch tools using semantic or local search.
472526
*

0 commit comments

Comments
 (0)