Skip to content

Commit f7ebc37

Browse files
authored
feat(core): add dynamic swarm worker tool (#3433)
* feat(core): add dynamic swarm worker tool Add a swarm tool for ad-hoc parallel worker execution with bounded concurrency, wait-all and first-success modes, per-worker failure isolation, and aggregated results. Register the tool in core, prevent nested worker recursion, and document the new workflow. * fix(core): harden swarm worker execution Prevent swarm calls from bypassing the outer scheduler concurrency budget. Disallow interactive question prompts in swarm workers by default, and avoid incomplete Markdown table escaping by using an HTML entity for pipe characters. Add focused tests for the scheduler behavior, worker tool restrictions, and result formatting.
1 parent cd8d9dc commit f7ebc37

11 files changed

Lines changed: 1091 additions & 2 deletions

File tree

docs/developers/tools/_meta.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export default {
55
shell: 'Shell',
66
'todo-write': 'Todo Write',
77
task: 'Task',
8+
swarm: 'Swarm',
89
'exit-plan-mode': 'Exit Plan Mode',
910
'web-fetch': 'Web Fetch',
1011
'web-search': 'Web Search',

docs/developers/tools/introduction.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Qwen Code's built-in tools can be broadly categorized as follows:
5151
- **[Memory Tool](./memory.md) (`save_memory`):** For saving and recalling information across sessions.
5252
- **[Todo Write Tool](./todo-write.md) (`todo_write`):** For creating and managing structured task lists during coding sessions.
5353
- **[Task Tool](./task.md) (`task`):** For delegating complex tasks to specialized subagents.
54+
- **[Swarm Tool](./swarm.md) (`swarm`):** For running many independent lightweight workers in parallel and aggregating their results.
5455
- **[Exit Plan Mode Tool](./exit-plan-mode.md) (`exit_plan_mode`):** For exiting plan mode and proceeding with implementation.
5556

5657
Additionally, these tools incorporate:

docs/developers/tools/swarm.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Swarm Tool (`swarm`)
2+
3+
Use `swarm` to run many independent, simple tasks through ephemeral worker
4+
agents and return a structured aggregate result to the parent agent.
5+
6+
Swarm is intended for map-reduce style work:
7+
8+
- analyzing many files independently
9+
- processing chunks of a large data file
10+
- running independent searches where the first successful result is enough
11+
- collecting per-item summaries, counts, or validation results
12+
13+
For a few complex role-based tasks, use the [`task`](./task.md) tool instead.
14+
For model comparison on the same task, use Agent Arena.
15+
16+
## Arguments
17+
18+
- `description` (string, required): Short description of the overall swarm job.
19+
- `tasks` (array, required): Independent tasks. Each task becomes one worker.
20+
- `id` (string, optional): Stable identifier returned in results.
21+
- `description` (string, required): Short per-worker description.
22+
- `prompt` (string, required): Complete instructions for the worker.
23+
- `mode` (`wait_all` or `first_success`, optional): Defaults to `wait_all`.
24+
- `max_concurrency` (number, optional): Maximum workers to run at once.
25+
- `max_turns` (number, optional): Maximum model/tool turns per worker.
26+
Defaults to `8`.
27+
- `timeout_seconds` (number, optional): Per-worker wall-clock timeout.
28+
- `worker_system_prompt` (string, optional): Shared worker system prompt.
29+
- `allowed_tools` (string array, optional): Tool allowlist for workers.
30+
- `disallowed_tools` (string array, optional): Tools removed from workers.
31+
32+
If `max_concurrency` is omitted, Qwen Code uses
33+
`QWEN_CODE_MAX_SWARM_CONCURRENCY`, then `QWEN_CODE_MAX_TOOL_CONCURRENCY`, then
34+
`10`.
35+
36+
## Result
37+
38+
The tool returns JSON to the parent agent with:
39+
40+
- `summary.total`
41+
- `summary.completed`
42+
- `summary.failed`
43+
- `summary.cancelled`
44+
- `summary.notStarted`
45+
- `results[]` with one entry per task, including `taskId`, `status`, `output`
46+
or `error`, duration, and execution stats when available
47+
48+
Individual worker failures do not abort the whole swarm. The parent agent is
49+
responsible for reading the aggregate result and presenting the final answer.
50+
51+
## Examples
52+
53+
Analyze files in parallel:
54+
55+
```text
56+
swarm(
57+
description="Extract function names",
58+
tasks=[
59+
{
60+
id="src/a.ts",
61+
description="Analyze src/a.ts",
62+
prompt="Read /repo/src/a.ts and return the exported function names."
63+
},
64+
{
65+
id="src/b.ts",
66+
description="Analyze src/b.ts",
67+
prompt="Read /repo/src/b.ts and return the exported function names."
68+
}
69+
],
70+
max_concurrency=10
71+
)
72+
```
73+
74+
Use first successful result:
75+
76+
```text
77+
swarm(
78+
description="Find API route definition",
79+
mode="first_success",
80+
tasks=[
81+
{
82+
description="Search routes directory",
83+
prompt="Search /repo/src/routes for the user creation route."
84+
},
85+
{
86+
description="Search controllers directory",
87+
prompt="Search /repo/src/controllers for the user creation route."
88+
}
89+
]
90+
)
91+
```
92+
93+
## Notes
94+
95+
Workers are lightweight and ephemeral: they are spawned, execute one task,
96+
return a result, and are cleaned up. Workers cannot spawn further subagents or
97+
cron jobs.
98+
99+
Swarm workers run concurrently, so interactive permission prompts are avoided.
100+
Permission hooks can still approve actions, and permissive approval modes still
101+
apply where configured. Prefer read-only or disjoint file scopes for swarm
102+
tasks.

docs/users/features/arena.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,9 @@ Agent Arena is experimental. Current limitations:
199199

200200
## Comparison with other multi-agent modes
201201

202-
Agent Arena is one of several planned multi-agent modes in Qwen Code. **Agent Team** and **Agent Swarm** are not yet implemented — the table below describes their intended design for reference.
202+
Agent Arena is one of several multi-agent modes in Qwen Code. **Agent Team** is not yet implemented. **Agent Swarm** is available as a lightweight tool for batch-style parallel worker execution.
203203

204-
| | **Agent Arena** | **Agent Team** (planned) | **Agent Swarm** (planned) |
204+
| | **Agent Arena** | **Agent Team** (planned) | **Agent Swarm** |
205205
| :---------------- | :----------------------------------------------------- | :------------------------------------------------- | :------------------------------------------------------- |
206206
| **Goal** | Competitive: Find the best solution to the _same_ task | Collaborative: Tackle _different_ aspects together | Batch parallel: Dynamically spawn workers for bulk tasks |
207207
| **Agents** | Pre-configured models compete independently | Teammates collaborate with assigned roles | Workers spawned on-the-fly, destroyed on completion |

packages/core/src/agents/runtime/agent-core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import { type ContextState, templateString } from './agent-headless.js';
7272
*/
7373
export const EXCLUDED_TOOLS_FOR_SUBAGENTS: ReadonlySet<string> = new Set([
7474
ToolNames.AGENT,
75+
ToolNames.SWARM,
7576
ToolNames.CRON_CREATE,
7677
ToolNames.CRON_LIST,
7778
ToolNames.CRON_DELETE,

packages/core/src/config/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2463,6 +2463,10 @@ export class Config {
24632463
const { AgentTool } = await import('../tools/agent/agent.js');
24642464
return new AgentTool(this);
24652465
});
2466+
await registerLazy(ToolNames.SWARM, async () => {
2467+
const { SwarmTool } = await import('../tools/swarm.js');
2468+
return new SwarmTool(this);
2469+
});
24662470
await registerLazy(ToolNames.SKILL, async () => {
24672471
const { SkillTool } = await import('../tools/skill.js');
24682472
return new SkillTool(this);

packages/core/src/core/coreToolScheduler.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import type { HookExecutionResponse } from '../confirmation-bus/types.js';
4343
import { type NotificationType } from '../hooks/types.js';
4444
import type { MessageBus } from '../confirmation-bus/message-bus.js';
4545
import { IdeClient } from '../ide/ide-client.js';
46+
import { ToolNames } from '../tools/tool-names.js';
4647

4748
vi.mock('fs/promises', () => ({
4849
writeFile: vi.fn(),
@@ -3167,6 +3168,61 @@ describe('Fire hook functions integration', () => {
31673168
expect(startIndices.every((i) => i < firstEnd)).toBe(true);
31683169
});
31693170

3171+
it('should execute multiple swarm tools sequentially', async () => {
3172+
const executionLog: string[] = [];
3173+
3174+
const swarmTool = new MockTool({
3175+
name: ToolNames.SWARM,
3176+
execute: async (params) => {
3177+
const id = (params as { id: string }).id;
3178+
executionLog.push(`swarm:start:${id}`);
3179+
await new Promise((r) => setTimeout(r, 50));
3180+
executionLog.push(`swarm:end:${id}`);
3181+
return {
3182+
llmContent: `Swarm ${id} done`,
3183+
returnDisplay: `Swarm ${id} done`,
3184+
};
3185+
},
3186+
});
3187+
3188+
const tools = new Map([[ToolNames.SWARM, swarmTool]]);
3189+
const onAllToolCallsComplete = vi.fn();
3190+
const onToolCallsUpdate = vi.fn();
3191+
const scheduler = createScheduler(
3192+
tools,
3193+
onAllToolCallsComplete,
3194+
onToolCallsUpdate,
3195+
);
3196+
3197+
await scheduler.schedule(
3198+
[
3199+
{
3200+
callId: '1',
3201+
name: ToolNames.SWARM,
3202+
args: { id: 'A' },
3203+
isClientInitiated: false,
3204+
prompt_id: 'p1',
3205+
},
3206+
{
3207+
callId: '2',
3208+
name: ToolNames.SWARM,
3209+
args: { id: 'B' },
3210+
isClientInitiated: false,
3211+
prompt_id: 'p1',
3212+
},
3213+
],
3214+
new AbortController().signal,
3215+
);
3216+
3217+
expect(onAllToolCallsComplete).toHaveBeenCalled();
3218+
expect(executionLog).toEqual([
3219+
'swarm:start:A',
3220+
'swarm:end:A',
3221+
'swarm:start:B',
3222+
'swarm:end:B',
3223+
]);
3224+
});
3225+
31703226
it('should run concurrency-safe tools in parallel and unsafe tools sequentially', async () => {
31713227
const executionLog: string[] = [];
31723228

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export type {
108108
} from './tools/shell.js';
109109
export type { SkillTool, SkillParams } from './tools/skill.js';
110110
export type { AgentTool, AgentParams } from './tools/agent/agent.js';
111+
export type { SwarmTool, SwarmParams, SwarmTask } from './tools/swarm.js';
111112
export type {
112113
TodoWriteTool,
113114
TodoItem,

0 commit comments

Comments
 (0)