33 * This eliminates duplication between stdio.ts and processParamsGetTools.
44 */
55
6- import type { ValidateFunction } from 'ajv' ;
76import type { ApifyClient } from 'apify' ;
87
98import log from '@apify/log' ;
109
1110import { defaults , HelperTools } from '../const.js' ;
12- import { callActor , getCallActorDescription } from '../tools/actor .js' ;
11+ import { buildCategories , CATEGORY_NAMES , toolCategoriesEnabledByDefault } from '../tools/categories .js' ;
1312import { getActorOutput } from '../tools/common/get-actor-output.js' ;
1413import { 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