Skip to content

Commit 18e14e8

Browse files
Sync public snapshot from freebuff-private
Source: CodebuffAI/freebuff-private@24db2944f87409339e7220722747ddd956383e0e
1 parent ba7e164 commit 18e14e8

4 files changed

Lines changed: 92 additions & 2 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, expect, test } from 'bun:test'
2+
3+
import {
4+
MAX_SPAWN_AGENTS_PER_CALL,
5+
spawnAgentsParams,
6+
} from '../params/tool/spawn-agents'
7+
8+
describe('the spawn_agents batch limit', () => {
9+
const batch = (n: number) => ({
10+
agents: Array.from({ length: n }, () => ({
11+
agent_type: 'file-picker',
12+
prompt: 'find files',
13+
})),
14+
})
15+
16+
test(`${MAX_SPAWN_AGENTS_PER_CALL} agents pass the schema`, () => {
17+
expect(
18+
spawnAgentsParams.inputSchema.safeParse(batch(MAX_SPAWN_AGENTS_PER_CALL))
19+
.success,
20+
).toBe(true)
21+
})
22+
23+
test(`${MAX_SPAWN_AGENTS_PER_CALL + 1} agents are refused by the schema`, () => {
24+
const result = spawnAgentsParams.inputSchema.safeParse(
25+
batch(MAX_SPAWN_AGENTS_PER_CALL + 1),
26+
)
27+
expect(result.success).toBe(false)
28+
})
29+
30+
test('the 21-agent swarm that ran on 2026-10-10 no longer validates', () => {
31+
expect(spawnAgentsParams.inputSchema.safeParse(batch(21)).success).toBe(
32+
false,
33+
)
34+
})
35+
})

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export const spawnAgentsOutputSchema = z
1717
.and(jsonObjectSchema)
1818
.array()
1919

20+
/** Most agents one spawn_agents call may spawn; the runtime handler refuses a
21+
* bigger batch with the same number, so bypassing this schema changes nothing. */
22+
export const MAX_SPAWN_AGENTS_PER_CALL = 6
23+
2024
const toolName = 'spawn_agents'
2125
const endsAgentStep = true
2226
const inputSchema = z
@@ -101,11 +105,19 @@ const inputSchema = z
101105
.optional()
102106
.describe('Parameters object for the agent'),
103107
})
104-
.array(),
108+
.array()
109+
// One swarm at a time, and never a big one: a free-mode root once
110+
// fanned out to 21 subagents in a single call (2026-10-10). The
111+
// runtime handler enforces the same ceiling, so a caller that
112+
// bypasses the schema still cannot.
113+
.max(
114+
MAX_SPAWN_AGENTS_PER_CALL,
115+
`Spawn at most ${MAX_SPAWN_AGENTS_PER_CALL} agents in one call. For more, spawn again after these return.`,
116+
),
105117
),
106118
})
107119
.describe(
108-
`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.`,
120+
`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. At most 6 agents per call.`,
109121
)
110122
const description = `
111123
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.

‎packages/agent-runtime/src/__tests__/spawn-agents-permissions.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,34 @@ describe('Spawn Agents Permissions', () => {
280280
},
281281
})
282282

283+
it('should refuse a batch over MAX_SPAWN_AGENTS_PER_CALL without starting any agent', async () => {
284+
const parentAgent = createMockAgent('parent', ['thinker'])
285+
const childAgent = createMockAgent('thinker')
286+
const sessionState = getInitialSessionState(mockFileContext)
287+
// The 21-agent swarm of 2026-10-10, replayed.
288+
const toolCall: CodebuffToolCall<'spawn_agents'> = {
289+
toolName: 'spawn_agents' as const,
290+
toolCallId: 'test-tool-call-id',
291+
input: {
292+
agents: Array.from({ length: 21 }, () => ({
293+
agent_type: 'thinker',
294+
prompt: 'swarm',
295+
})),
296+
},
297+
}
298+
299+
const { output } = await handleSpawnAgents({
300+
...handleSpawnAgentsBaseParams,
301+
agentState: sessionState.mainAgentState,
302+
agentTemplate: parentAgent,
303+
localAgentTemplates: { thinker: childAgent },
304+
toolCall,
305+
})
306+
307+
expect(JSON.stringify(output)).toContain('at most 6')
308+
expect(mockLoopAgentSteps).not.toHaveBeenCalled()
309+
})
310+
283311
it('should allow spawning when agent is in spawnableAgents list', async () => {
284312
const parentAgent = createMockAgent('parent', ['thinker', 'reviewer'])
285313
const childAgent = createMockAgent('thinker')

‎packages/agent-runtime/src/tools/handlers/tool/spawn-agents.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { MAX_SPAWN_AGENTS_PER_CALL } from '@codebuff/common/tools/params/tool/spawn-agents'
12
import { jsonToolResult } from '@codebuff/common/util/messages'
23

34
import { outputForParent } from '../../../util/agent-output'
@@ -85,6 +86,20 @@ export const handleSpawnAgents = (async (
8586
const { agents } = toolCall.input
8687
const { logger } = params
8788

89+
// Enforce the schema's MAX_SPAWN_AGENTS_PER_CALL even when validation was
90+
// bypassed (custom tool-call parsing, a cached schema from an older client).
91+
// The batch answers in place: no subagent starts and no token is spent.
92+
if (agents.length > MAX_SPAWN_AGENTS_PER_CALL) {
93+
return {
94+
output: jsonToolResult([
95+
{
96+
agentType: agents[0]?.agent_type ?? 'unknown',
97+
errorMessage: `Refusing to spawn ${agents.length} agents at once: at most ${MAX_SPAWN_AGENTS_PER_CALL} per spawn_agents call. Split the fan-out into smaller batches.`,
98+
},
99+
]),
100+
}
101+
}
102+
88103
await previousToolCallFinished
89104

90105
const results = await Promise.allSettled(

0 commit comments

Comments
 (0)