1- import { endsAgentStepParam } from '@codebuff/common/tools/constants'
1+ import { endsAgentStepParam , toolNames } from '@codebuff/common/tools/constants'
22import { toolParams } from '@codebuff/common/tools/list'
33import { generateCompactId } from '@codebuff/common/util/string'
44import { cloneDeep } from 'lodash'
@@ -7,7 +7,11 @@ import { getMCPToolData } from '../mcp'
77import { MCP_TOOL_SEPARATOR } from '../mcp-constants'
88import { getAgentShortName } from '../templates/prompts'
99import { codebuffToolHandlers } from './handlers/list'
10- import { transformSpawnAgentsInput } from './handlers/tool/spawn-agent-utils'
10+ import {
11+ getMatchingSpawn ,
12+ transformSpawnAgentsInput ,
13+ } from './handlers/tool/spawn-agent-utils'
14+ import { getAgentTemplate } from '../templates/agent-registry'
1115import { ensureZodSchema } from './prompts'
1216
1317
@@ -127,7 +131,7 @@ export type ExecuteToolCallParams<T extends string = ToolName> = {
127131} & AgentRuntimeDeps &
128132 AgentRuntimeScopedDeps
129133
130- export function executeToolCall < T extends ToolName > (
134+ export async function executeToolCall < T extends ToolName > (
131135 params : ExecuteToolCallParams < T > ,
132136) : Promise < void > {
133137 const {
@@ -194,12 +198,102 @@ export function executeToolCall<T extends ToolName>(
194198 ? transformSpawnAgentsInput ( input , agentTemplate . spawnableAgents )
195199 : input
196200
201+ // TODO: Allow tools to provide a validation function, and move this logic into the spawn_agents validation function.
202+ // Pre-validate spawn_agents to filter out non-existent agents before streaming
203+ let effectiveInput = transformedInput
204+ if ( toolName === 'spawn_agents' ) {
205+ const agents = ( transformedInput as Record < string , unknown > ) . agents
206+ if ( Array . isArray ( agents ) ) {
207+ const BASE_AGENTS = [
208+ 'base' ,
209+ 'base-free' ,
210+ 'base-max' ,
211+ 'base-experimental' ,
212+ ]
213+ const isBaseAgent = BASE_AGENTS . includes ( agentTemplate . id )
214+
215+ const validationResults = await Promise . allSettled (
216+ agents . map ( async ( agent ) => {
217+ if ( ! agent || typeof agent !== 'object' ) {
218+ return { valid : false as const , error : 'Invalid agent entry' }
219+ }
220+ const agentTypeStr = ( agent as Record < string , unknown > ) . agent_type
221+ if ( typeof agentTypeStr !== 'string' || ! agentTypeStr ) {
222+ return { valid : false as const , error : 'Agent entry missing agent_type' }
223+ }
224+
225+ if ( ! isBaseAgent ) {
226+ const matchingSpawn = getMatchingSpawn (
227+ agentTemplate . spawnableAgents ,
228+ agentTypeStr ,
229+ )
230+ if ( ! matchingSpawn ) {
231+ if ( toolNames . includes ( agentTypeStr as ToolName ) ) {
232+ return { valid : false as const , error : `"${ agentTypeStr } " is a tool, not an agent. Call it directly as a tool instead of wrapping it in spawn_agents.` }
233+ }
234+ return { valid : false as const , error : `Agent "${ agentTypeStr } " is not available to spawn` }
235+ }
236+ }
237+
238+ try {
239+ const template = await getAgentTemplate ( {
240+ agentId : agentTypeStr ,
241+ localAgentTemplates : params . localAgentTemplates ,
242+ fetchAgentFromDatabase : params . fetchAgentFromDatabase ,
243+ databaseAgentCache : params . databaseAgentCache ,
244+ logger,
245+ apiKey : params . apiKey ,
246+ } )
247+ if ( ! template ) {
248+ if ( toolNames . includes ( agentTypeStr as ToolName ) ) {
249+ return { valid : false as const , error : `"${ agentTypeStr } " is a tool, not an agent. Call it directly as a tool instead of wrapping it in spawn_agents.` }
250+ }
251+ return { valid : false as const , error : `Agent "${ agentTypeStr } " does not exist` }
252+ }
253+ } catch {
254+ return { valid : false as const , error : `Agent "${ agentTypeStr } " could not be loaded` }
255+ }
256+
257+ return { valid : true as const , agent }
258+ } ) ,
259+ )
260+
261+ const validAgents : unknown [ ] = [ ]
262+ const errors : string [ ] = [ ]
263+
264+ for ( const result of validationResults ) {
265+ if ( result . status === 'rejected' ) {
266+ errors . push ( 'Agent validation failed unexpectedly' )
267+ } else if ( result . value . valid ) {
268+ validAgents . push ( result . value . agent )
269+ } else {
270+ errors . push ( result . value . error )
271+ }
272+ }
273+
274+ if ( errors . length > 0 ) {
275+ if ( validAgents . length === 0 ) {
276+ const errorMsg = `Failed to spawn agents: ${ errors . join ( '; ' ) } `
277+ onResponseChunk ( { type : 'error' , message : errorMsg } )
278+ logger . debug (
279+ { toolName, errors } ,
280+ 'All agents in spawn_agents are invalid, not streaming tool call' ,
281+ )
282+ return previousToolCallFinished
283+ }
284+ const errorMsg = `Some agents could not be spawned: ${ errors . join ( '; ' ) } . Proceeding with valid agents only.`
285+ onResponseChunk ( { type : 'error' , message : errorMsg } )
286+ effectiveInput = { ...transformedInput , agents : validAgents }
287+ }
288+ }
289+ }
290+
197291 // Only emit tool_call event after permission check passes
198292 onResponseChunk ( {
199293 type : 'tool_call' ,
200294 toolCallId,
201295 toolName,
202- input : transformedInput ,
296+ input : effectiveInput ,
203297 agentId : agentState . agentId ,
204298 parentAgentId : agentState . parentId ,
205299 includeToolCall : ! excludeToolFromMessageHistory ,
@@ -212,10 +306,10 @@ export function executeToolCall<T extends ToolName>(
212306 toolName
213307 ] as unknown as CodebuffToolHandlerFunction < T >
214308
215- // Use transformed input for spawn_agents so the handler receives the correct agent types
309+ // Use effective input for spawn_agents so the handler receives the correct agent types
216310 const finalToolCall =
217311 toolName === 'spawn_agents'
218- ? { ...toolCall , input : transformedInput }
312+ ? { ...toolCall , input : effectiveInput }
219313 : toolCall
220314
221315 const toolResultPromise = handler ( {
0 commit comments