From 8e114b7fa77e073bf553ebc7bad8a1c876953b56 Mon Sep 17 00:00:00 2001 From: marcusquinn <6428977+marcusquinn@users.noreply.github.com> Date: Thu, 8 Jan 2026 01:07:22 +0000 Subject: [PATCH] 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 --- packages/opencode/src/agent/agent.ts | 2 + packages/opencode/src/config/config.ts | 7 ++ packages/opencode/src/tool/task.ts | 9 +-- .../opencode/test/tool/task-subagents.test.ts | 70 +++++++++++++++++++ packages/web/src/content/docs/agents.mdx | 31 ++++++++ 5 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 packages/opencode/test/tool/task-subagents.test.ts diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index c53ca04e238..a154fa6fb84 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -42,6 +42,7 @@ export namespace Agent { prompt: z.string().optional(), options: z.record(z.string(), z.any()), steps: z.number().int().positive().optional(), + subagents: z.array(z.string()).optional(), }) .meta({ ref: "Agent", @@ -227,6 +228,7 @@ export namespace Agent { item.hidden = value.hidden ?? item.hidden item.name = value.name ?? item.name item.steps = value.steps ?? item.steps + item.subagents = value.subagents ?? item.subagents item.options = mergeDeep(item.options, value.options ?? {}) item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {})) } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 28aea4d6777..2a287015107 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -696,6 +696,12 @@ export namespace Config { .describe("Maximum number of agentic iterations before forcing text-only response"), maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."), permission: Permission.optional(), + subagents: z + .array(z.string()) + .optional() + .describe( + "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.", + ), }) .catchall(z.any()) .transform((agent, ctx) => { @@ -716,6 +722,7 @@ export namespace Config { "permission", "disable", "tools", + "subagents", ]) // Extract unknown properties into options diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 8c8cf827aba..612fae1c4da 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -10,6 +10,7 @@ import { iife } from "@/util/iife" import { defer } from "@/util/defer" import { Config } from "../config/config" import { PermissionNext } from "@/permission/next" +import { Wildcard } from "@/util/wildcard" const parameters = z.object({ description: z.string().describe("A short (3-5 words) description of the task"), @@ -27,11 +28,11 @@ const parameters = z.object({ export const TaskTool = Tool.define("task", async (ctx) => { const agents = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary")) - // Filter agents by permissions if agent provided + // Filter agents by permissions and subagents list if agent provided const caller = ctx?.agent - const accessibleAgents = caller - ? agents.filter((a) => PermissionNext.evaluate("task", a.name, caller.permission).action !== "deny") - : agents + const accessibleAgents = agents + .filter((a) => !caller || PermissionNext.evaluate("task", a.name, caller.permission).action !== "deny") + .filter((a) => !caller?.subagents?.length || caller.subagents.some((pattern) => Wildcard.match(a.name, pattern))) const description = DESCRIPTION.replace( "{agents}", diff --git a/packages/opencode/test/tool/task-subagents.test.ts b/packages/opencode/test/tool/task-subagents.test.ts new file mode 100644 index 00000000000..432a0bc40f1 --- /dev/null +++ b/packages/opencode/test/tool/task-subagents.test.ts @@ -0,0 +1,70 @@ +import { describe, test, expect } from "bun:test" +import { Wildcard } from "../../src/util/wildcard" + +describe("Task tool subagents filtering", () => { + // These tests verify the filtering logic used in task.ts + // The actual filtering is: agents.filter(a => !caller?.subagents?.length || caller.subagents.some(pattern => Wildcard.match(a.name, pattern))) + + const mockAgents = [ + { name: "general", description: "General purpose agent" }, + { name: "explore", description: "Codebase exploration" }, + { name: "code-reviewer", description: "Code review agent" }, + { name: "code-formatter", description: "Code formatting agent" }, + { name: "test-runner", description: "Test execution agent" }, + { name: "docs-generator", description: "Documentation generator" }, + ] + + const filterAgents = (agents: typeof mockAgents, subagents?: string[]) => + agents.filter((a) => !subagents?.length || subagents.some((pattern) => Wildcard.match(a.name, pattern))) + + test("returns all agents when subagents is undefined", () => { + const result = filterAgents(mockAgents, undefined) + expect(result).toHaveLength(6) + }) + + test("returns all agents when subagents is empty array", () => { + const result = filterAgents(mockAgents, []) + expect(result).toHaveLength(6) + }) + + test("filters to exact matches", () => { + const result = filterAgents(mockAgents, ["general", "explore"]) + expect(result).toHaveLength(2) + expect(result.map((a) => a.name)).toEqual(["general", "explore"]) + }) + + test("filters using wildcard patterns", () => { + const result = filterAgents(mockAgents, ["code-*"]) + expect(result).toHaveLength(2) + expect(result.map((a) => a.name)).toEqual(["code-reviewer", "code-formatter"]) + }) + + test("filters using mixed exact and wildcard patterns", () => { + const result = filterAgents(mockAgents, ["general", "code-*"]) + expect(result).toHaveLength(3) + expect(result.map((a) => a.name)).toEqual(["general", "code-reviewer", "code-formatter"]) + }) + + test("filters using global wildcard allows all", () => { + const result = filterAgents(mockAgents, ["*"]) + expect(result).toHaveLength(6) + }) + + test("filters using suffix wildcard", () => { + const result = filterAgents(mockAgents, ["*-runner", "*-generator"]) + expect(result).toHaveLength(2) + expect(result.map((a) => a.name)).toEqual(["test-runner", "docs-generator"]) + }) + + test("returns empty when no patterns match", () => { + const result = filterAgents(mockAgents, ["nonexistent", "also-nonexistent"]) + expect(result).toHaveLength(0) + }) + + test("handles single character wildcard", () => { + // ? matches single character + const result = filterAgents(mockAgents, ["code-?eviewer"]) + expect(result).toHaveLength(1) + expect(result[0].name).toBe("code-reviewer") + }) +}) diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index 53de8af5f0c..d30a3382c1f 100644 --- a/packages/web/src/content/docs/agents.mdx +++ b/packages/web/src/content/docs/agents.mdx @@ -617,6 +617,37 @@ Use a valid hex color (e.g., `#FF5733`) or theme color: `primary`, `secondary`, --- +### Subagents + +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. + +```json title="opencode.json" +{ + "agent": { + "build": { + "mode": "primary", + "subagents": ["explore", "general", "code-*"] + } + } +} +``` + +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. + +:::tip +The `subagents` option supports glob patterns. Use `code-*` to match all subagents starting with `code-`, or `*-reviewer` to match all subagents ending with `-reviewer`. +::: + +:::note +If `subagents` is not set or is an empty array, all subagents are available (the default behavior). +::: + +The `subagents` option works alongside `permission.task`: +- `subagents` controls which subagents appear in the tool description (affects token usage) +- `permission.task` controls runtime access (ask/allow/deny) + +--- + ### Top P Control response diversity with the `top_p` option. Alternative to temperature for controlling randomness.