Skip to content

Commit 8e114b7

Browse files
committed
feat(agent): add subagents config for per-agent task tool filtering
Add a new 'subagents' configuration option that allows primary agents to specify which subagents appear in their Task tool description. This reduces token overhead when many subagents are configured but only a subset is relevant to a particular agent. - Add 'subagents' field to Agent config schema (supports glob patterns) - Filter task tool description based on caller's subagents list - Add tests for the new filtering behavior - Update documentation with usage examples Closes #7269
1 parent 4551282 commit 8e114b7

5 files changed

Lines changed: 115 additions & 4 deletions

File tree

packages/opencode/src/agent/agent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export namespace Agent {
4242
prompt: z.string().optional(),
4343
options: z.record(z.string(), z.any()),
4444
steps: z.number().int().positive().optional(),
45+
subagents: z.array(z.string()).optional(),
4546
})
4647
.meta({
4748
ref: "Agent",
@@ -227,6 +228,7 @@ export namespace Agent {
227228
item.hidden = value.hidden ?? item.hidden
228229
item.name = value.name ?? item.name
229230
item.steps = value.steps ?? item.steps
231+
item.subagents = value.subagents ?? item.subagents
230232
item.options = mergeDeep(item.options, value.options ?? {})
231233
item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {}))
232234
}

packages/opencode/src/config/config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,12 @@ export namespace Config {
696696
.describe("Maximum number of agentic iterations before forcing text-only response"),
697697
maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."),
698698
permission: Permission.optional(),
699+
subagents: z
700+
.array(z.string())
701+
.optional()
702+
.describe(
703+
"List of subagent names this agent can spawn via the Task tool. When set, only these subagents will appear in the task tool description. Supports glob patterns (e.g., 'explore', 'code-*'). If not set, all subagents are available.",
704+
),
699705
})
700706
.catchall(z.any())
701707
.transform((agent, ctx) => {
@@ -716,6 +722,7 @@ export namespace Config {
716722
"permission",
717723
"disable",
718724
"tools",
725+
"subagents",
719726
])
720727

721728
// Extract unknown properties into options

packages/opencode/src/tool/task.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { iife } from "@/util/iife"
1010
import { defer } from "@/util/defer"
1111
import { Config } from "../config/config"
1212
import { PermissionNext } from "@/permission/next"
13+
import { Wildcard } from "@/util/wildcard"
1314

1415
const parameters = z.object({
1516
description: z.string().describe("A short (3-5 words) description of the task"),
@@ -27,11 +28,11 @@ const parameters = z.object({
2728
export const TaskTool = Tool.define("task", async (ctx) => {
2829
const agents = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary"))
2930

30-
// Filter agents by permissions if agent provided
31+
// Filter agents by permissions and subagents list if agent provided
3132
const caller = ctx?.agent
32-
const accessibleAgents = caller
33-
? agents.filter((a) => PermissionNext.evaluate("task", a.name, caller.permission).action !== "deny")
34-
: agents
33+
const accessibleAgents = agents
34+
.filter((a) => !caller || PermissionNext.evaluate("task", a.name, caller.permission).action !== "deny")
35+
.filter((a) => !caller?.subagents?.length || caller.subagents.some((pattern) => Wildcard.match(a.name, pattern)))
3536

3637
const description = DESCRIPTION.replace(
3738
"{agents}",
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { describe, test, expect } from "bun:test"
2+
import { Wildcard } from "../../src/util/wildcard"
3+
4+
describe("Task tool subagents filtering", () => {
5+
// These tests verify the filtering logic used in task.ts
6+
// The actual filtering is: agents.filter(a => !caller?.subagents?.length || caller.subagents.some(pattern => Wildcard.match(a.name, pattern)))
7+
8+
const mockAgents = [
9+
{ name: "general", description: "General purpose agent" },
10+
{ name: "explore", description: "Codebase exploration" },
11+
{ name: "code-reviewer", description: "Code review agent" },
12+
{ name: "code-formatter", description: "Code formatting agent" },
13+
{ name: "test-runner", description: "Test execution agent" },
14+
{ name: "docs-generator", description: "Documentation generator" },
15+
]
16+
17+
const filterAgents = (agents: typeof mockAgents, subagents?: string[]) =>
18+
agents.filter((a) => !subagents?.length || subagents.some((pattern) => Wildcard.match(a.name, pattern)))
19+
20+
test("returns all agents when subagents is undefined", () => {
21+
const result = filterAgents(mockAgents, undefined)
22+
expect(result).toHaveLength(6)
23+
})
24+
25+
test("returns all agents when subagents is empty array", () => {
26+
const result = filterAgents(mockAgents, [])
27+
expect(result).toHaveLength(6)
28+
})
29+
30+
test("filters to exact matches", () => {
31+
const result = filterAgents(mockAgents, ["general", "explore"])
32+
expect(result).toHaveLength(2)
33+
expect(result.map((a) => a.name)).toEqual(["general", "explore"])
34+
})
35+
36+
test("filters using wildcard patterns", () => {
37+
const result = filterAgents(mockAgents, ["code-*"])
38+
expect(result).toHaveLength(2)
39+
expect(result.map((a) => a.name)).toEqual(["code-reviewer", "code-formatter"])
40+
})
41+
42+
test("filters using mixed exact and wildcard patterns", () => {
43+
const result = filterAgents(mockAgents, ["general", "code-*"])
44+
expect(result).toHaveLength(3)
45+
expect(result.map((a) => a.name)).toEqual(["general", "code-reviewer", "code-formatter"])
46+
})
47+
48+
test("filters using global wildcard allows all", () => {
49+
const result = filterAgents(mockAgents, ["*"])
50+
expect(result).toHaveLength(6)
51+
})
52+
53+
test("filters using suffix wildcard", () => {
54+
const result = filterAgents(mockAgents, ["*-runner", "*-generator"])
55+
expect(result).toHaveLength(2)
56+
expect(result.map((a) => a.name)).toEqual(["test-runner", "docs-generator"])
57+
})
58+
59+
test("returns empty when no patterns match", () => {
60+
const result = filterAgents(mockAgents, ["nonexistent", "also-nonexistent"])
61+
expect(result).toHaveLength(0)
62+
})
63+
64+
test("handles single character wildcard", () => {
65+
// ? matches single character
66+
const result = filterAgents(mockAgents, ["code-?eviewer"])
67+
expect(result).toHaveLength(1)
68+
expect(result[0].name).toBe("code-reviewer")
69+
})
70+
})

packages/web/src/content/docs/agents.mdx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,37 @@ Use a valid hex color (e.g., `#FF5733`) or theme color: `primary`, `secondary`,
617617

618618
---
619619

620+
### Subagents
621+
622+
Control which subagents appear in the Task tool description with the `subagents` option. This is useful for reducing token overhead when you have many subagents configured but only want specific ones available to a particular agent.
623+
624+
```json title="opencode.json"
625+
{
626+
"agent": {
627+
"build": {
628+
"mode": "primary",
629+
"subagents": ["explore", "general", "code-*"]
630+
}
631+
}
632+
}
633+
```
634+
635+
When `subagents` is set, only the listed subagents will appear in the Task tool description. This reduces the token cost of the system prompt, which can be significant when you have many subagents configured.
636+
637+
:::tip
638+
The `subagents` option supports glob patterns. Use `code-*` to match all subagents starting with `code-`, or `*-reviewer` to match all subagents ending with `-reviewer`.
639+
:::
640+
641+
:::note
642+
If `subagents` is not set or is an empty array, all subagents are available (the default behavior).
643+
:::
644+
645+
The `subagents` option works alongside `permission.task`:
646+
- `subagents` controls which subagents appear in the tool description (affects token usage)
647+
- `permission.task` controls runtime access (ask/allow/deny)
648+
649+
---
650+
620651
### Top P
621652

622653
Control response diversity with the `top_p` option. Alternative to temperature for controlling randomness.

0 commit comments

Comments
 (0)