Skip to content

Commit 8d42e6a

Browse files
committed
Subagents are defined primarily as native tools! They are rewritten to be spawn_agents calls
1 parent 12d7d5f commit 8d42e6a

File tree

8 files changed

+273
-85
lines changed

8 files changed

+273
-85
lines changed

common/src/tools/params/tool/spawn-agents.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,14 @@ const inputSchema = z
3131
`Spawn multiple agents and send a prompt and/or parameters to each of them. These agents will run in parallel. Note that that means they will run independently. If you need to run agents sequentially, use spawn_agents with one agent at a time instead.`,
3232
)
3333
const description = `
34-
Use this tool to spawn agents to help you complete the user request. Each agent has specific requirements for prompt and params based on their inputSchema.
34+
Use this tool to spawn agents to help you complete the user request. Each agent has specific requirements for prompt and params based on their tools schema.
3535
3636
The prompt field is a simple string, while params is a JSON object that gets validated against the agent's schema.
3737
38+
Each agent available is already defined as another tool, or, dynamically defined later in the conversation.
39+
40+
You can call agents either as direct tool calls (e.g., \`example-agent\`) or use \`spawn_agents\`. Both formats work, but **prefer using spawn_agents** because it allows you to spawn multiple agents in parallel for better performance. When using direct tool calls, the schema is flat (prompt is a field alongside other params), whereas spawn_agents uses nested \`prompt\` and \`params\` fields.
41+
3842
Example:
3943
${$getNativeToolCallExampleString({
4044
toolName,

packages/agent-runtime/src/run-agent-step.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { getAgentStreamFromTemplate } from './prompt-agent-stream'
1313
import { runProgrammaticStep } from './run-programmatic-step'
1414
import { additionalSystemPrompts } from './system-prompt/prompts'
1515
import { getAgentTemplate } from './templates/agent-registry'
16+
import { buildAgentToolSet } from './templates/prompts'
1617
import { getAgentPrompt } from './templates/strings'
1718
import { getToolSet } from './tools/prompts'
1819
import { processStream } from './tools/stream-parser'
@@ -639,6 +640,15 @@ export async function loopAgentSteps(
639640
},
640641
})) ?? ''
641642

643+
// Build agent tools (agents as direct tool calls) for non-inherited tools
644+
const agentTools = useParentTools
645+
? {}
646+
: await buildAgentToolSet({
647+
...params,
648+
spawnableAgents: agentTemplate.spawnableAgents,
649+
agentTemplates: localAgentTemplates,
650+
})
651+
642652
const tools = useParentTools
643653
? parentTools
644654
: await getToolSet({
@@ -652,6 +662,7 @@ export async function loopAgentSteps(
652662
}
653663
return cachedAdditionalToolDefinitions
654664
},
665+
agentTools,
655666
})
656667

657668
const hasUserMessage = Boolean(
Lines changed: 132 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,141 @@
11
import { getAgentTemplate } from './agent-registry'
22
import { buildArray } from '@codebuff/common/util/array'
33
import { schemaToJsonStr } from '@codebuff/common/util/zod-schema'
4+
import { z } from 'zod/v4'
45

56
import type { AgentTemplate } from '@codebuff/common/types/agent-template'
67
import type { Logger } from '@codebuff/common/types/contracts/logger'
78
import type { ParamsExcluding } from '@codebuff/common/types/function-params'
89
import type { AgentTemplateType } from '@codebuff/common/types/session-state'
9-
import { getToolCallString } from '@codebuff/common/tools/utils'
10+
import type { ToolSet } from 'ai'
1011

11-
export async function buildSpawnableAgentsDescription(
12+
/**
13+
* Gets the short agent name from a fully qualified agent ID.
14+
* E.g., 'codebuff/file-picker@1.0.0' -> 'file-picker'
15+
*/
16+
export function getAgentShortName(agentType: AgentTemplateType): string {
17+
const withoutVersion = agentType.split('@')[0]
18+
const parts = withoutVersion.split('/')
19+
return parts[parts.length - 1]
20+
}
21+
22+
/**
23+
* Builds a flat input schema for an agent tool by combining prompt and params.
24+
* E.g., { prompt?: string, ...paramsFields }
25+
*/
26+
export function buildAgentFlatInputSchema(agentTemplate: AgentTemplate): z.ZodType {
27+
const { inputSchema } = agentTemplate
28+
29+
// Start with an empty object schema
30+
let schemaFields: Record<string, z.ZodType> = {}
31+
32+
// Add prompt field if defined
33+
if (inputSchema?.prompt) {
34+
schemaFields.prompt = inputSchema.prompt.optional()
35+
}
36+
37+
// Merge params fields directly into the schema (flat structure)
38+
if (inputSchema?.params) {
39+
// Get the shape of the params schema if it's an object
40+
const paramsJsonSchema = z.toJSONSchema(inputSchema.params, { io: 'input' })
41+
if (paramsJsonSchema.properties) {
42+
for (const [key, propSchema] of Object.entries(paramsJsonSchema.properties)) {
43+
// Skip if we already have a prompt field
44+
if (key === 'prompt') continue
45+
46+
// Create a zod schema from the JSON schema property
47+
const isRequired = (paramsJsonSchema.required as string[] | undefined)?.includes(key)
48+
// Use z.any() with description since we can't perfectly reconstruct the original zod type
49+
const fieldSchema = z.any().describe(
50+
(propSchema as any).description || `Parameter: ${key}`
51+
)
52+
schemaFields[key] = isRequired ? fieldSchema : fieldSchema.optional()
53+
}
54+
}
55+
}
56+
57+
return z.object(schemaFields).describe(
58+
agentTemplate.spawnerPrompt || `Spawn the ${agentTemplate.displayName} agent`
59+
)
60+
}
61+
62+
/**
63+
* Builds AI SDK tool definitions for spawnable agents.
64+
* These tools allow the model to call agents directly as tool calls.
65+
*/
66+
export async function buildAgentToolSet(
67+
params: {
68+
spawnableAgents: AgentTemplateType[]
69+
agentTemplates: Record<string, AgentTemplate>
70+
logger: Logger
71+
} & ParamsExcluding<
72+
typeof getAgentTemplate,
73+
'agentId' | 'localAgentTemplates'
74+
>,
75+
): Promise<ToolSet> {
76+
const { spawnableAgents, agentTemplates } = params
77+
78+
const toolSet: ToolSet = {}
79+
80+
for (const agentType of spawnableAgents) {
81+
const agentTemplate = await getAgentTemplate({
82+
...params,
83+
agentId: agentType,
84+
localAgentTemplates: agentTemplates,
85+
})
86+
87+
if (!agentTemplate) continue
88+
89+
const shortName = getAgentShortName(agentType)
90+
const inputSchema = buildAgentFlatInputSchema(agentTemplate)
91+
92+
// Use the same structure as other tools in toolParams
93+
toolSet[shortName] = {
94+
description: agentTemplate.spawnerPrompt || `Spawn the ${agentTemplate.displayName} agent`,
95+
inputSchema,
96+
}
97+
}
98+
99+
return toolSet
100+
}
101+
102+
/**
103+
* Builds the description of a single agent for the system prompt.
104+
*/
105+
function buildSingleAgentDescription(
106+
agentType: AgentTemplateType,
107+
agentTemplate: AgentTemplate | null,
108+
): string {
109+
if (!agentTemplate) {
110+
// Fallback for unknown agents
111+
return `- ${agentType}: Dynamic agent (description not available)
112+
prompt: {"description": "A coding task to complete", "type": "string"}
113+
params: None`
114+
}
115+
116+
const { inputSchema } = agentTemplate
117+
const inputSchemaStr = inputSchema
118+
? [
119+
`prompt: ${schemaToJsonStr(inputSchema.prompt)}`,
120+
`params: ${schemaToJsonStr(inputSchema.params)}`,
121+
].join('\n')
122+
: ['prompt: None', 'params: None'].join('\n')
123+
124+
return buildArray(
125+
`- ${agentType}: ${agentTemplate.spawnerPrompt}`,
126+
agentTemplate.includeMessageHistory &&
127+
'This agent can see the current message history.',
128+
agentTemplate.inheritParentSystemPrompt &&
129+
"This agent inherits the parent's system prompt for prompt caching.",
130+
inputSchemaStr,
131+
).join('\n')
132+
}
133+
134+
/**
135+
* Builds the full spawnable agents specification for subagent instructions.
136+
* This is used when inheritSystemPrompt is true to tell subagents which agents they can spawn.
137+
*/
138+
export async function buildFullSpawnableAgentsSpec(
12139
params: {
13140
spawnableAgents: AgentTemplateType[]
14141
agentTemplates: Record<string, AgentTemplate>
@@ -18,7 +145,7 @@ export async function buildSpawnableAgentsDescription(
18145
'agentId' | 'localAgentTemplates'
19146
>,
20147
): Promise<string> {
21-
const { spawnableAgents, agentTemplates, logger } = params
148+
const { spawnableAgents, agentTemplates } = params
22149
if (spawnableAgents.length === 0) {
23150
return ''
24151
}
@@ -37,43 +164,11 @@ export async function buildSpawnableAgentsDescription(
37164
)
38165

39166
const agentsDescription = subAgentTypesAndTemplates
40-
.map(([agentType, agentTemplate]) => {
41-
if (!agentTemplate) {
42-
// Fallback for unknown agents
43-
return `- ${agentType}: Dynamic agent (description not available)
44-
prompt: {"description": "A coding task to complete", "type": "string"}
45-
params: None`
46-
}
47-
const { inputSchema } = agentTemplate
48-
const inputSchemaStr = inputSchema
49-
? [
50-
`prompt: ${schemaToJsonStr(inputSchema.prompt)}`,
51-
`params: ${schemaToJsonStr(inputSchema.params)}`,
52-
].join('\n')
53-
: ['prompt: None', 'params: None'].join('\n')
54-
55-
return buildArray(
56-
`- ${agentType}: ${agentTemplate.spawnerPrompt}`,
57-
agentTemplate.includeMessageHistory &&
58-
'This agent can see the current message history.',
59-
agentTemplate.inheritParentSystemPrompt &&
60-
"This agent inherits the parent's system prompt for prompt caching.",
61-
inputSchemaStr,
62-
).join('\n')
63-
})
167+
.map(([agentType, agentTemplate]) => buildSingleAgentDescription(agentType, agentTemplate))
64168
.filter(Boolean)
65169
.join('\n\n')
66170

67-
return `\n\n## Spawnable Agents
68-
69-
Use the spawn_agents tool to spawn agents to help you complete the user request.
70-
71-
Notes:
72-
- You can not call the agents as tool names directly: you must use the spawn_agents tool with the correct parameters to spawn them!
73-
- There are two types of input arguments for agents: prompt and params. The prompt is a string, and the params is a json object. Some agents require only one or the other, some require both, and some require none.
74-
- Below are the *only* available agents by their agent_type. Other agents may be referenced earlier in the conversation, but they are not available to you.
75-
76-
Spawn only the below agents:
171+
return `You are a subagent that can only spawn the following agents using the spawn_agents tool:
77172
78173
${agentsDescription}`
79174
}

packages/agent-runtime/src/templates/strings.ts

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { schemaToJsonStr } from '@codebuff/common/util/zod-schema'
44
import { z } from 'zod/v4'
55

66
import { getAgentTemplate } from './agent-registry'
7-
import { buildSpawnableAgentsDescription } from './prompts'
7+
import { buildFullSpawnableAgentsSpec } from './prompts'
88
import { PLACEHOLDER, placeholderValues } from './types'
99
import {
1010
getGitChangesPrompt,
@@ -108,8 +108,6 @@ export async function formatPrompt(
108108
[PLACEHOLDER.REMAINING_STEPS]: () => `${agentState.stepsRemaining!}`,
109109
[PLACEHOLDER.PROJECT_ROOT]: () => fileContext.projectRoot,
110110
[PLACEHOLDER.SYSTEM_INFO_PROMPT]: () => getSystemInfoPrompt(fileContext),
111-
[PLACEHOLDER.TOOLS_PROMPT]: async () => '',
112-
[PLACEHOLDER.AGENTS_PROMPT]: () => buildSpawnableAgentsDescription(params),
113111
[PLACEHOLDER.USER_CWD]: () => fileContext.cwd,
114112
[PLACEHOLDER.USER_INPUT_PROMPT]: () => escapeString(lastUserInput ?? ''),
115113
[PLACEHOLDER.INITIAL_AGENT_PROMPT]: () =>
@@ -144,11 +142,6 @@ export async function formatPrompt(
144142
}
145143
type StringField = 'systemPrompt' | 'instructionsPrompt' | 'stepPrompt'
146144

147-
const additionalPlaceholders = {
148-
systemPrompt: [PLACEHOLDER.TOOLS_PROMPT, PLACEHOLDER.AGENTS_PROMPT],
149-
instructionsPrompt: [],
150-
stepPrompt: [],
151-
} satisfies Record<StringField, string[]>
152145
export async function getAgentPrompt<T extends StringField>(
153146
params: {
154147
agentTemplate: AgentTemplate
@@ -164,7 +157,7 @@ export async function getAgentPrompt<T extends StringField>(
164157
'prompt' | 'tools' | 'spawnableAgents'
165158
> &
166159
ParamsExcluding<
167-
typeof buildSpawnableAgentsDescription,
160+
typeof buildFullSpawnableAgentsSpec,
168161
'spawnableAgents' | 'agentTemplates'
169162
>,
170163
): Promise<string | undefined> {
@@ -178,11 +171,6 @@ export async function getAgentPrompt<T extends StringField>(
178171
} = params
179172

180173
let promptValue = agentTemplate[promptType.type]
181-
for (const placeholder of additionalPlaceholders[promptType.type]) {
182-
if (!promptValue.includes(placeholder)) {
183-
promptValue += `\n\n${placeholder}`
184-
}
185-
}
186174

187175
let prompt = await formatPrompt({
188176
...params,
@@ -202,18 +190,25 @@ export async function getAgentPrompt<T extends StringField>(
202190
if (promptType.type === 'instructionsPrompt' && agentState.agentType) {
203191
// Add subagent tools message when using parent's tools for prompt caching
204192
if (useParentTools) {
205-
addendum +=
206-
`\n\nYou are a subagent that only has access to the following tools: ${agentTemplate.toolNames.join(', ')}. Do not attempt to use any other tools.`
193+
addendum += `\n\nYou are a subagent that only has access to the following tools: ${agentTemplate.toolNames.join(', ')}. Do not attempt to use any other tools.`
194+
195+
// For subagents with inheritSystemPrompt, include full spawnable agents spec
196+
// since the parent's system prompt may not have these agents listed
197+
if (agentTemplate.spawnableAgents.length > 0) {
198+
addendum +=
199+
'\n\n' +
200+
(await buildFullSpawnableAgentsSpec({
201+
...params,
202+
spawnableAgents: agentTemplate.spawnableAgents,
203+
agentTemplates,
204+
}))
205+
}
206+
} else if (agentTemplate.spawnableAgents.length > 0) {
207+
// For non-inherited tools, agents are already defined as tools with full schemas,
208+
// so we just list the available agent IDs here
209+
addendum += `\n\nYou can spawn the following agents: ${agentTemplate.spawnableAgents.join(', ')}.`
207210
}
208211

209-
addendum +=
210-
'\n\n' +
211-
(await buildSpawnableAgentsDescription({
212-
...params,
213-
spawnableAgents: agentTemplate.spawnableAgents,
214-
agentTemplates,
215-
}))
216-
217212
// Add output schema information if defined
218213
if (agentTemplate.outputSchema) {
219214
addendum += '\n\n## Output Schema\n\n'

packages/agent-runtime/src/templates/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export type { AgentTemplate, StepGenerator, StepHandler }
1313

1414
const placeholderNames = [
1515
'AGENT_NAME',
16-
'AGENTS_PROMPT',
1716
'CONFIG_SCHEMA',
1817
'FILE_TREE_PROMPT_SMALL',
1918
'FILE_TREE_PROMPT',
@@ -24,7 +23,6 @@ const placeholderNames = [
2423
'PROJECT_ROOT',
2524
'REMAINING_STEPS',
2625
'SYSTEM_INFO_PROMPT',
27-
'TOOLS_PROMPT',
2826
'USER_CWD',
2927
'USER_INPUT_PROMPT',
3028
] as const

packages/agent-runtime/src/tools/prompts.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,9 @@ ${toolDescriptions.join('\n\n')}
272272
export async function getToolSet(params: {
273273
toolNames: string[]
274274
additionalToolDefinitions: () => Promise<CustomToolDefinitions>
275+
agentTools: ToolSet
275276
}): Promise<ToolSet> {
276-
const { toolNames, additionalToolDefinitions } = params
277+
const { toolNames, additionalToolDefinitions, agentTools } = params
277278

278279
const toolSet: ToolSet = {}
279280
for (const toolName of toolNames) {
@@ -289,5 +290,10 @@ export async function getToolSet(params: {
289290
} as Omit<typeof toolDefinition, 'inputSchema'> & { inputSchema: z.ZodType }
290291
}
291292

293+
// Add agent tools (agents as direct tool calls)
294+
for (const [toolName, toolDefinition] of Object.entries(agentTools)) {
295+
toolSet[toolName] = toolDefinition
296+
}
297+
292298
return toolSet
293299
}

0 commit comments

Comments
 (0)