Skip to content

Commit db34d71

Browse files
committed
refactor: switch tools-loader to buildCategories(uiMode), remove deep-clone hack
Refactor loadToolsFromInput() to use buildCategories(uiMode) instead of the static toolCategories map. This eliminates three legacy workarounds: 1. Deep-clone hack (JSON.parse/stringify with function stripping/reattachment) — no longer needed since buildCategories() returns the correct variant directly and tool definitions are frozen (Object.freeze from PR #4) 2. openaiOnly runtime filter — no longer needed since buildCategories() returns ui: [] in default mode (openai-only tools are never included) 3. call-actor description mutation — no longer needed since each mode variant has its own description baked in Also: - Add two-level internal tool name classification: mode-specific map for selection, all-names set for preventing misclassification as Actor IDs - Remove openaiOnly from internal tool definitions (no longer used for filtering) - Deprecate openaiOnly in ToolBase type (kept for cross-repo compat) - Remove dead getCallActorDescription() function - Deprecate callActor adapter in actor.ts
1 parent fc95924 commit db34d71

File tree

5 files changed

+64
-82
lines changed

5 files changed

+64
-82
lines changed

src/tools/actor.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,14 @@
99
* This adapter file maintains backward compatibility for existing imports.
1010
* PR #4 will wire variants directly into the category registry, making this adapter unnecessary.
1111
*/
12-
import type { HelperTool, InternalToolArgs, ToolEntry, UiMode } from '../types.js';
12+
import type { HelperTool, InternalToolArgs, ToolEntry } from '../types.js';
1313
import { defaultCallActor } from './default/call-actor.js';
1414
import { openaiCallActor } from './openai/call-actor.js';
1515

1616
// Re-exports to maintain backward compatibility and support other modules
1717
export { callActorGetDataset, type CallActorGetDatasetResult } from './core/actor-execution.js';
1818
export { getActorsAsTools } from './core/actor-tools-factory.js';
1919

20-
/**
21-
* Returns the call-actor description for the given UI mode.
22-
* Maintained for backward compatibility with tools-loader.ts which mutates the description at load time.
23-
* PR #4 will remove this in favor of direct variant registration.
24-
*/
25-
export function getCallActorDescription(uiMode?: UiMode): string {
26-
const variant = uiMode === 'openai' ? openaiCallActor : defaultCallActor;
27-
return variant.description ?? '';
28-
}
29-
3020
const defaultVariant = defaultCallActor as HelperTool;
3121

3222
/**
@@ -35,7 +25,8 @@ const defaultVariant = defaultCallActor as HelperTool;
3525
* The tool definition (name, inputSchema, outputSchema, etc.) uses the default variant's metadata.
3626
* The `call` handler inspects `apifyMcpServer.options.uiMode` to delegate to the right implementation.
3727
*
38-
* Note: The description may be overridden by tools-loader.ts at load time for openai mode.
28+
* @deprecated This adapter is no longer needed — buildCategories(uiMode) returns the correct
29+
* variant directly. Will be removed once all consumers are migrated.
3930
*/
4031
export const callActor: ToolEntry = Object.freeze({
4132
...defaultVariant,

src/tools/openai/fetch-actor-details-internal.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ const fetchActorDetailsInternalArgsSchema = z.object({
2626
export const fetchActorDetailsInternalTool: ToolEntry = Object.freeze({
2727
type: 'internal',
2828
name: HelperTools.ACTOR_GET_DETAILS_INTERNAL,
29-
openaiOnly: true,
3029
description: `Fetch Actor details with flexible output options (UI mode internal tool).
3130
3231
This tool is available because the LLM is operating in UI mode. Use it for internal lookups

src/tools/openai/search-actors-internal.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ const searchActorsInternalArgsSchema = z.object({
3030
export const searchActorsInternalTool: ToolEntry = Object.freeze({
3131
type: 'internal',
3232
name: HelperTools.STORE_SEARCH_INTERNAL,
33-
openaiOnly: true,
3433
description: `Search Actors internally (UI mode internal tool).
3534
3635
This tool is available because the LLM is operating in UI mode. Use it for internal lookups

src/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ export type ToolBase = z.infer<typeof ToolSchema> & {
9191
ajvValidate: ValidateFunction;
9292
/** Whether this tool requires Skyfire pay ID validation (uses Apify API) */
9393
requiresSkyfirePayId?: boolean;
94-
/** Whether this tool is only available in OpenAI UI mode */
94+
/**
95+
* Whether this tool is only available in OpenAI UI mode.
96+
* @deprecated No longer used for filtering. Mode-specific tools are resolved at build time
97+
* via buildCategories(uiMode). Will be removed in a future release.
98+
*/
9599
openaiOnly?: boolean;
96100
};
97101

src/utils/tools-loader.ts

Lines changed: 56 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,38 @@
33
* This eliminates duplication between stdio.ts and processParamsGetTools.
44
*/
55

6-
import type { ValidateFunction } from 'ajv';
76
import type { ApifyClient } from 'apify';
87

98
import log from '@apify/log';
109

1110
import { defaults, HelperTools } from '../const.js';
12-
import { callActor, getCallActorDescription } from '../tools/actor.js';
11+
import { buildCategories, CATEGORY_NAMES, toolCategoriesEnabledByDefault } from '../tools/categories.js';
1312
import { getActorOutput } from '../tools/common/get-actor-output.js';
1413
import { addTool } from '../tools/common/helpers.js';
15-
import { getActorRun } from '../tools/common/run.js';
16-
import { getActorsAsTools, toolCategories, toolCategoriesEnabledByDefault } from '../tools/index.js';
17-
import type { ActorStore, Input, InternalToolArgs, ToolCategory, ToolEntry, UiMode } from '../types.js';
18-
import { getExpectedToolsByCategories } from './tool-categories-helpers.js';
19-
20-
// Lazily-computed cache of internal tools by name to avoid circular init issues.
21-
let INTERNAL_TOOL_BY_NAME_CACHE: Map<string, ToolEntry> | null = null;
22-
function getInternalToolByNameMap(): Map<string, ToolEntry> {
23-
if (!INTERNAL_TOOL_BY_NAME_CACHE) {
24-
const allInternal = getExpectedToolsByCategories(Object.keys(toolCategories) as ToolCategory[]);
25-
INTERNAL_TOOL_BY_NAME_CACHE = new Map<string, ToolEntry>(
26-
allInternal.map((entry) => [entry.name, entry]),
27-
);
14+
import { getActorsAsTools } from '../tools/index.js';
15+
import type { ActorStore, Input, ToolCategory, ToolEntry, UiMode } from '../types.js';
16+
17+
/**
18+
* Set of all known internal tool names across ALL modes.
19+
* Used for classifying selectors: if a selector matches a known internal tool name,
20+
* it's not treated as an Actor ID — even if it's absent from the current mode's categories.
21+
*/
22+
let ALL_INTERNAL_TOOL_NAMES_CACHE: Set<string> | null = null;
23+
function getAllInternalToolNames(): Set<string> {
24+
if (!ALL_INTERNAL_TOOL_NAMES_CACHE) {
25+
const allNames = new Set<string>();
26+
// Collect tool names from both modes to ensure complete classification
27+
for (const mode of [undefined, 'openai' as UiMode]) {
28+
const categories = buildCategories(mode);
29+
for (const name of CATEGORY_NAMES) {
30+
for (const tool of categories[name]) {
31+
allNames.add(tool.name);
32+
}
33+
}
34+
}
35+
ALL_INTERNAL_TOOL_NAMES_CACHE = allNames;
2836
}
29-
return INTERNAL_TOOL_BY_NAME_CACHE;
37+
return ALL_INTERNAL_TOOL_NAMES_CACHE;
3038
}
3139

3240
/**
@@ -44,6 +52,9 @@ export async function loadToolsFromInput(
4452
uiMode?: UiMode,
4553
actorStore?: ActorStore,
4654
): Promise<ToolEntry[]> {
55+
// Build mode-resolved categories — tools are already the correct variant for this mode
56+
const categories = buildCategories(uiMode);
57+
4758
// Helpers for readability
4859
const normalizeSelectors = (value: Input['tools']): (string | ToolCategory)[] | undefined => {
4960
if (value === undefined) return undefined;
@@ -59,6 +70,14 @@ export async function loadToolsFromInput(
5970
const addActorEnabled = input.enableAddingActors === true;
6071
const actorsExplicitlyEmpty = (Array.isArray(input.actors) && input.actors.length === 0) || input.actors === '';
6172

73+
// Build mode-specific tool-by-name map for individual tool selection
74+
const modeToolByName = new Map<string, ToolEntry>();
75+
for (const name of CATEGORY_NAMES) {
76+
for (const tool of categories[name]) {
77+
modeToolByName.set(tool.name, tool);
78+
}
79+
}
80+
6281
// Partition selectors into internal picks (by category or by name) and Actor names
6382
const internalSelections: ToolEntry[] = [];
6483
const actorSelectorsFromTools: string[] = [];
@@ -67,21 +86,27 @@ export async function loadToolsFromInput(
6786
if (selector === 'preview') {
6887
// 'preview' category is deprecated. It contained `call-actor` which is now default
6988
log.warning('Tool category "preview" is deprecated');
70-
internalSelections.push(callActor);
89+
const callActorTool = modeToolByName.get(HelperTools.ACTOR_CALL);
90+
if (callActorTool) internalSelections.push(callActorTool);
7191
continue;
7292
}
7393

74-
const categoryTools = toolCategories[selector as ToolCategory];
94+
const categoryTools = categories[selector as ToolCategory];
7595

7696
if (categoryTools) {
7797
internalSelections.push(...categoryTools);
7898
continue;
7999
}
80-
const internalByName = getInternalToolByNameMap().get(String(selector));
100+
const internalByName = modeToolByName.get(String(selector));
81101
if (internalByName) {
82102
internalSelections.push(internalByName);
83103
continue;
84104
}
105+
// If this is a known internal tool name (from another mode), skip it silently
106+
// rather than treating it as an Actor ID
107+
if (getAllInternalToolNames().has(String(selector))) {
108+
continue;
109+
}
85110
// Treat unknown selectors as Actor IDs/full names.
86111
// Potential heuristic (future): if (String(selector).includes('/')) => definitely an Actor.
87112
actorSelectorsFromTools.push(String(selector));
@@ -123,12 +148,15 @@ export async function loadToolsFromInput(
123148
// No selectors: either expose only add-actor (when enabled), or default categories
124149
result.push(addTool);
125150
} else if (!actorsExplicitlyEmpty) {
126-
result.push(...getExpectedToolsByCategories(toolCategoriesEnabledByDefault));
151+
// Use mode-resolved default categories
152+
for (const cat of toolCategoriesEnabledByDefault) {
153+
result.push(...categories[cat]);
154+
}
127155
}
128156

129-
// In openai mode, add UI-specific tools
157+
// In openai mode, unconditionally add UI-specific tools (regardless of selectors)
130158
if (uiMode === 'openai') {
131-
result.push(...(toolCategories.ui || []));
159+
result.push(...categories.ui);
132160
}
133161

134162
// Actor tools (if any)
@@ -141,6 +169,8 @@ export async function loadToolsFromInput(
141169
* Auto-inject get-actor-run and get-actor-output when call-actor or actor tools are present.
142170
* Insert them right after call-actor to follow the logical workflow order:
143171
* search → details → call → run status → output → docs → actor tools
172+
*
173+
* Uses mode-resolved variants from buildCategories() for get-actor-run.
144174
*/
145175
const hasCallActor = result.some((entry) => entry.name === HelperTools.ACTOR_CALL);
146176
const hasActorTools = result.some((entry) => entry.type === 'actor');
@@ -150,7 +180,9 @@ export async function loadToolsFromInput(
150180

151181
const toolsToInject: ToolEntry[] = [];
152182
if (!hasGetActorRun && (hasCallActor || uiMode === 'openai')) {
153-
toolsToInject.push(getActorRun);
183+
// Use mode-resolved get-actor-run variant
184+
const modeGetActorRun = modeToolByName.get(HelperTools.ACTOR_RUNS_GET);
185+
if (modeGetActorRun) toolsToInject.push(modeGetActorRun);
154186
}
155187
if (!hasGetActorOutput && (hasCallActor || hasActorTools || hasAddActorTool)) {
156188
toolsToInject.push(getActorOutput);
@@ -178,48 +210,5 @@ export async function loadToolsFromInput(
178210

179211
// De-duplicate by tool name for safety
180212
const seen = new Set<string>();
181-
const deduped = result.filter((entry) => !seen.has(entry.name) && seen.add(entry.name));
182-
183-
// Filter out openai-only tools when not in openai mode
184-
const filtered = uiMode === 'openai'
185-
? deduped
186-
: deduped.filter((entry) => !entry.openaiOnly);
187-
188-
// TODO: rework this solition as it was quickly hacked together for hotfix
189-
// Deep clone except ajvValidate and call functions
190-
191-
// holds the original functions of the tools
192-
const toolFunctions = new Map<string, { ajvValidate?: ValidateFunction<unknown>; call?:(args: InternalToolArgs) => Promise<object> }>();
193-
for (const entry of filtered) {
194-
if (entry.type === 'internal') {
195-
toolFunctions.set(entry.name, { ajvValidate: entry.ajvValidate, call: entry.call });
196-
} else {
197-
toolFunctions.set(entry.name, { ajvValidate: entry.ajvValidate });
198-
}
199-
}
200-
201-
const cloned = JSON.parse(JSON.stringify(filtered, (key, value) => {
202-
if (key === 'ajvValidate' || key === 'call') return undefined;
203-
return value;
204-
})) as ToolEntry[];
205-
206-
// restore the original functions
207-
for (const entry of cloned) {
208-
const funcs = toolFunctions.get(entry.name);
209-
if (funcs) {
210-
if (funcs.ajvValidate) {
211-
entry.ajvValidate = funcs.ajvValidate;
212-
}
213-
if (entry.type === 'internal' && funcs.call) {
214-
entry.call = funcs.call;
215-
}
216-
}
217-
}
218-
219-
for (const entry of cloned) {
220-
if (entry.name === HelperTools.ACTOR_CALL) {
221-
entry.description = getCallActorDescription(uiMode);
222-
}
223-
}
224-
return cloned;
213+
return result.filter((entry) => !seen.has(entry.name) && seen.add(entry.name));
225214
}

0 commit comments

Comments
 (0)