Skip to content

Commit d8326eb

Browse files
committed
Don't stream tool call for spawned agents with an incorrect agent id
1 parent 4f15c45 commit d8326eb

File tree

1 file changed

+100
-6
lines changed

1 file changed

+100
-6
lines changed

packages/agent-runtime/src/tools/tool-executor.ts

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { endsAgentStepParam } from '@codebuff/common/tools/constants'
1+
import { endsAgentStepParam, toolNames } from '@codebuff/common/tools/constants'
22
import { toolParams } from '@codebuff/common/tools/list'
33
import { generateCompactId } from '@codebuff/common/util/string'
44
import { cloneDeep } from 'lodash'
@@ -7,7 +7,11 @@ import { getMCPToolData } from '../mcp'
77
import { MCP_TOOL_SEPARATOR } from '../mcp-constants'
88
import { getAgentShortName } from '../templates/prompts'
99
import { 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'
1115
import { 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

Comments
 (0)