Skip to content

Commit 826936f

Browse files
committed
refactor: add buildCategories(uiMode) function and CATEGORY_NAMES const array
Introduce buildCategories(uiMode?) that returns mode-resolved tool variants directly (openai tools in openai mode, default tools otherwise), eliminating the need for runtime adapter dispatch. The same category names are used in both modes — only the tool implementations differ. - Add CATEGORY_NAMES const array as the single source of truth for category names - Add ToolCategoryMap type derived from CATEGORY_NAMES - Add buildCategories() function with mode-specific tool resolution - Change ToolCategory type to derive from CATEGORY_NAMES instead of keyof typeof toolCategories - Mark static toolCategories as @deprecated (will be removed when tools-loader migrates) - Add compile-time satisfies checks via ToolCategoryMap type annotation - Add unit tests for buildCategories: mode variants, circular-init safety, ordering
1 parent 1db6fe6 commit 826936f

File tree

4 files changed

+179
-7
lines changed

4 files changed

+179
-7
lines changed

src/tools/categories.ts

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* The final tool ordering presented to MCP clients is determined by tools-loader.ts,
99
* which also auto-injects get-actor-run and get-actor-output right after call-actor.
1010
*/
11-
import type { ToolCategory } from '../types.js';
11+
import type { ToolEntry, UiMode } from '../types.js';
1212
import { callActor } from './actor.js';
1313
import { getDataset, getDatasetItems, getDatasetSchema } from './common/dataset.js';
1414
import { getUserDatasetsList } from './common/dataset_collection.js';
@@ -21,12 +21,36 @@ import { getUserKeyValueStoresList } from './common/key_value_store_collection.j
2121
import { abortActorRun, getActorRun, getActorRunLog } from './common/run.js';
2222
import { getUserRunsList } from './common/run_collection.js';
2323
import { searchApifyDocsTool } from './common/search-apify-docs.js';
24+
import { defaultCallActor } from './default/call-actor.js';
25+
import { defaultFetchActorDetails } from './default/fetch-actor-details.js';
26+
import { defaultGetActorRun } from './default/get-actor-run.js';
27+
import { defaultSearchActors } from './default/search-actors.js';
2428
import { fetchActorDetailsTool } from './fetch-actor-details.js';
29+
import { openaiCallActor } from './openai/call-actor.js';
30+
import { openaiFetchActorDetails } from './openai/fetch-actor-details.js';
2531
import { fetchActorDetailsInternalTool } from './openai/fetch-actor-details-internal.js';
32+
import { openaiGetActorRun } from './openai/get-actor-run.js';
33+
import { openaiSearchActors } from './openai/search-actors.js';
2634
import { searchActorsInternalTool } from './openai/search-actors-internal.js';
2735
import { searchActors } from './store_collection.js';
2836

29-
export const toolCategories = {
37+
/**
38+
* Canonical list of all tool category names.
39+
* Used to derive the ToolCategory type and validate category maps at compile time.
40+
*/
41+
export const CATEGORY_NAMES = ['experimental', 'actors', 'ui', 'docs', 'runs', 'storage', 'dev'] as const;
42+
43+
/** Map from category name to an array of tool entries. */
44+
export type ToolCategoryMap = Record<(typeof CATEGORY_NAMES)[number], ToolEntry[]>;
45+
46+
/**
47+
* Static tool categories using adapter tools that dispatch at runtime based on uiMode.
48+
*
49+
* @deprecated Use {@link buildCategories} instead, which returns mode-resolved tool variants
50+
* directly without runtime dispatching. This static map will be removed once the tools-loader
51+
* is refactored to use buildCategories().
52+
*/
53+
export const toolCategories: ToolCategoryMap = {
3054
experimental: [
3155
addTool,
3256
],
@@ -65,7 +89,56 @@ export const toolCategories = {
6589
],
6690
};
6791

68-
export const toolCategoriesEnabledByDefault: ToolCategory[] = [
92+
/**
93+
* Build tool categories for a given UI mode.
94+
*
95+
* Returns the same category names as {@link toolCategories}, but with mode-resolved
96+
* tool variants: openai mode gets openai-specific implementations (async execution,
97+
* widget metadata), default mode gets standard implementations.
98+
*
99+
* This eliminates the need for runtime adapter dispatch — each tool is the correct
100+
* variant for its mode from the start.
101+
*/
102+
export function buildCategories(uiMode?: UiMode): ToolCategoryMap {
103+
const isOpenai = uiMode === 'openai';
104+
return {
105+
experimental: [
106+
addTool,
107+
],
108+
actors: isOpenai
109+
? [openaiSearchActors, openaiFetchActorDetails, openaiCallActor]
110+
: [defaultSearchActors, defaultFetchActorDetails, defaultCallActor],
111+
ui: isOpenai
112+
? [searchActorsInternalTool, fetchActorDetailsInternalTool]
113+
: [],
114+
docs: [
115+
searchApifyDocsTool,
116+
fetchApifyDocsTool,
117+
],
118+
runs: [
119+
isOpenai ? openaiGetActorRun : defaultGetActorRun,
120+
getUserRunsList,
121+
getActorRunLog,
122+
abortActorRun,
123+
],
124+
storage: [
125+
getDataset,
126+
getDatasetItems,
127+
getDatasetSchema,
128+
getActorOutput,
129+
getKeyValueStore,
130+
getKeyValueStoreKeys,
131+
getKeyValueStoreRecord,
132+
getUserDatasetsList,
133+
getUserKeyValueStoresList,
134+
],
135+
dev: [
136+
getHtmlSkeleton,
137+
],
138+
};
139+
}
140+
141+
export const toolCategoriesEnabledByDefault: (typeof CATEGORY_NAMES)[number][] = [
69142
'actors',
70143
'docs',
71144
];

src/tools/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { HelperTools } from '../const.js';
22
import type { ToolCategory } from '../types.js';
33
import { getExpectedToolsByCategories } from '../utils/tool-categories-helpers.js';
44
import { callActorGetDataset, getActorsAsTools } from './actor.js';
5-
import { toolCategories, toolCategoriesEnabledByDefault } from './categories.js';
5+
import { buildCategories, CATEGORY_NAMES, toolCategories, toolCategoriesEnabledByDefault } from './categories.js';
66

77
// Use string constants instead of importing tool objects to avoid circular dependency
88
export const unauthEnabledTools: string[] = [
@@ -13,7 +13,7 @@ export const unauthEnabledTools: string[] = [
1313

1414
// Re-export from categories.ts
1515
// This is actually needed to avoid circular dependency issues
16-
export { toolCategories, toolCategoriesEnabledByDefault };
16+
export { buildCategories, CATEGORY_NAMES, toolCategories, toolCategoriesEnabledByDefault };
1717

1818
// Computed here (not in helper file) to avoid module initialization issues
1919
export const defaultTools = getExpectedToolsByCategories(toolCategoriesEnabledByDefault);

src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type z from 'zod';
1717
import type { ApifyClient } from './apify-client.js';
1818
import type { ACTOR_PRICING_MODEL, TELEMETRY_ENV, TOOL_STATUS } from './const.js';
1919
import type { ActorsMcpServer } from './mcp/server.js';
20-
import type { toolCategories } from './tools/index.js';
20+
import type { CATEGORY_NAMES } from './tools/categories.js';
2121
import type { StructuredPricingInfo } from './utils/pricing-info.js';
2222
import type { ProgressTracker } from './utils/progress.js';
2323

@@ -230,7 +230,7 @@ export type PricingInfo = ActorRunPricingInfo & {
230230
tieredPricing?: TieredPricing;
231231
} | PricePerEventActorPricingInfo;
232232

233-
export type ToolCategory = keyof typeof toolCategories;
233+
export type ToolCategory = (typeof CATEGORY_NAMES)[number];
234234
/**
235235
* Selector for tools input - can be a category key or a specific tool name.
236236
*/
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { HelperTools } from '../../src/const.js';
4+
import { buildCategories, CATEGORY_NAMES, toolCategories } from '../../src/tools/index.js';
5+
import type { ToolCategory, ToolEntry } from '../../src/types.js';
6+
7+
describe('CATEGORY_NAMES', () => {
8+
it('should match the keys of the static toolCategories', () => {
9+
const staticKeys = Object.keys(toolCategories).sort();
10+
const categoryNames = [...CATEGORY_NAMES].sort();
11+
expect(categoryNames).toEqual(staticKeys);
12+
});
13+
});
14+
15+
describe('buildCategories', () => {
16+
it('should return all category keys matching CATEGORY_NAMES', () => {
17+
const defaultResult = buildCategories();
18+
const openaiResult = buildCategories('openai');
19+
20+
for (const name of CATEGORY_NAMES) {
21+
expect(defaultResult).toHaveProperty(name);
22+
expect(openaiResult).toHaveProperty(name);
23+
}
24+
});
25+
26+
it('should return no undefined entries in any category (circular-init safety)', () => {
27+
const defaultResult = buildCategories();
28+
const openaiResult = buildCategories('openai');
29+
30+
for (const name of CATEGORY_NAMES) {
31+
for (const tool of defaultResult[name]) {
32+
expect(tool).toBeDefined();
33+
expect(tool.name).toBeDefined();
34+
}
35+
for (const tool of openaiResult[name]) {
36+
expect(tool).toBeDefined();
37+
expect(tool.name).toBeDefined();
38+
}
39+
}
40+
});
41+
42+
it('should return empty ui category in default mode', () => {
43+
const result = buildCategories();
44+
expect(result.ui).toEqual([]);
45+
});
46+
47+
it('should return non-empty ui category in openai mode', () => {
48+
const result = buildCategories('openai');
49+
expect(result.ui.length).toBeGreaterThan(0);
50+
});
51+
52+
it('should return different tool variants for actors category based on mode', () => {
53+
const defaultResult = buildCategories();
54+
const openaiResult = buildCategories('openai');
55+
56+
// Both modes should have the same tool names in actors category
57+
const defaultNames = defaultResult.actors.map((t: ToolEntry) => t.name);
58+
const openaiNames = openaiResult.actors.map((t: ToolEntry) => t.name);
59+
expect(defaultNames).toEqual(openaiNames);
60+
61+
// But the actual tool objects should be different (different call implementations)
62+
expect(defaultResult.actors[0]).not.toBe(openaiResult.actors[0]);
63+
});
64+
65+
it('should return different get-actor-run variants based on mode', () => {
66+
const defaultResult = buildCategories();
67+
const openaiResult = buildCategories('openai');
68+
69+
const defaultGetRun = defaultResult.runs.find((t: ToolEntry) => t.name === HelperTools.ACTOR_RUNS_GET);
70+
const openaiGetRun = openaiResult.runs.find((t: ToolEntry) => t.name === HelperTools.ACTOR_RUNS_GET);
71+
72+
expect(defaultGetRun).toBeDefined();
73+
expect(openaiGetRun).toBeDefined();
74+
// Different objects (different implementations)
75+
expect(defaultGetRun).not.toBe(openaiGetRun);
76+
});
77+
78+
it('should share identical tools for mode-independent categories', () => {
79+
const defaultResult = buildCategories();
80+
const openaiResult = buildCategories('openai');
81+
82+
const modeIndependentCategories: ToolCategory[] = ['experimental', 'docs', 'storage', 'dev'];
83+
for (const cat of modeIndependentCategories) {
84+
expect(defaultResult[cat]).toEqual(openaiResult[cat]);
85+
}
86+
});
87+
88+
it('should preserve tool ordering within categories', () => {
89+
const result = buildCategories();
90+
const actorNames = result.actors.map((t: ToolEntry) => t.name);
91+
92+
// Verify workflow order: search → details → call
93+
expect(actorNames).toEqual([
94+
HelperTools.STORE_SEARCH,
95+
HelperTools.ACTOR_GET_DETAILS,
96+
HelperTools.ACTOR_CALL,
97+
]);
98+
});
99+
});

0 commit comments

Comments
 (0)