Skip to content

Commit c2347b5

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 2e4fe97 commit c2347b5

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
@@ -35,6 +35,7 @@ export namespace Agent {
3535
prompt: z.string().optional(),
3636
options: z.record(z.string(), z.any()),
3737
steps: z.number().int().positive().optional(),
38+
subagents: z.array(z.string()).optional(),
3839
})
3940
.meta({
4041
ref: "Agent",
@@ -198,6 +199,7 @@ export namespace Agent {
198199
item.hidden = value.hidden ?? item.hidden
199200
item.name = value.name ?? item.name
200201
item.steps = value.steps ?? item.steps
202+
item.subagents = value.subagents ?? item.subagents
201203
item.options = mergeDeep(item.options, value.options ?? {})
202204
item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {}))
203205
}

packages/opencode/src/config/config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,12 @@ export namespace Config {
502502
.describe("Maximum number of agentic iterations before forcing text-only response"),
503503
maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."),
504504
permission: Permission.optional(),
505+
subagents: z
506+
.array(z.string())
507+
.optional()
508+
.describe(
509+
"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.",
510+
),
505511
})
506512
.catchall(z.any())
507513
.transform((agent, ctx) => {
@@ -521,6 +527,7 @@ export namespace Config {
521527
"permission",
522528
"disable",
523529
"tools",
530+
"subagents",
524531
])
525532

526533
// 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
@@ -11,6 +11,7 @@ import { iife } from "@/util/iife"
1111
import { defer } from "@/util/defer"
1212
import { Config } from "../config/config"
1313
import { PermissionNext } from "@/permission/next"
14+
import { Wildcard } from "@/util/wildcard"
1415

1516
const parameters = z.object({
1617
description: z.string().describe("A short (3-5 words) description of the task"),
@@ -23,11 +24,11 @@ const parameters = z.object({
2324
export const TaskTool = Tool.define("task", async (ctx) => {
2425
const agents = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary"))
2526

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

3233
const description = DESCRIPTION.replace(
3334
"{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
@@ -566,6 +566,37 @@ Users can always invoke any subagent directly via the `@` autocomplete menu, eve
566566

567567
---
568568

569+
### Subagents
570+
571+
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.
572+
573+
```json title="opencode.json"
574+
{
575+
"agent": {
576+
"build": {
577+
"mode": "primary",
578+
"subagents": ["explore", "general", "code-*"]
579+
}
580+
}
581+
}
582+
```
583+
584+
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.
585+
586+
:::tip
587+
The `subagents` option supports glob patterns. Use `code-*` to match all subagents starting with `code-`, or `*-reviewer` to match all subagents ending with `-reviewer`.
588+
:::
589+
590+
:::note
591+
If `subagents` is not set or is an empty array, all subagents are available (the default behavior).
592+
:::
593+
594+
The `subagents` option works alongside `permission.task`:
595+
- `subagents` controls which subagents appear in the tool description (affects token usage)
596+
- `permission.task` controls runtime access (ask/allow/deny)
597+
598+
---
599+
569600
### Additional
570601

571602
Any other options you specify in your agent configuration will be **passed through directly** to the provider as model options. This allows you to use provider-specific features and parameters.

0 commit comments

Comments
 (0)