Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
765812c
feat: add configurable subagent visibility per agent
Sewer56 Nov 26, 2025
9de0068
docs: add subagents configuration documentation
Sewer56 Nov 26, 2025
b582b53
fix: TUI autocomplete now respects agent subagents visibility config
Sewer56 Nov 26, 2025
515e859
Merge remote-tracking branch 'origin/dev' into add-hide-subagents
Sewer56 Nov 26, 2025
3c7b3e5
Merge tag 'v1.0.115' into add-hide-subagents
Sewer56 Nov 26, 2025
eab8fc8
chore: format code
actions-user Nov 26, 2025
0d2a06c
Merge branch 'dev' into add-hide-subagents
Sewer56 Nov 29, 2025
1bbb7d0
Update Nix flake.lock and hashes
actions-user Nov 29, 2025
29cdde3
Merge branch 'dev' into add-hide-subagents
Sewer56 Nov 30, 2025
a7924bd
Update Nix flake.lock and hashes
actions-user Nov 30, 2025
1e5c116
Merge branch 'dev' into add-hide-subagents
Sewer56 Dec 3, 2025
d51e467
Merge branch 'dev' into add-hide-subagents
Sewer56 Dec 4, 2025
aaea27e
Merge branch 'dev' into add-hide-subagents
Sewer56 Dec 6, 2025
f3b40d7
Merge remote-tracking branch 'origin/dev' into add-hide-subagents
Sewer56 Dec 12, 2025
a52acd1
feat(opencode): add visible field and task permission to agents
Sewer56 Dec 12, 2025
58f8dce
fix: don't hide visible==false from system prompt.
Sewer56 Dec 12, 2025
cbf549f
Merge remote-tracking branch 'origin/dev' into add-hide-subagents
Sewer56 Dec 12, 2025
f353f08
improve: allow user to invoke agents even if they are denied
Sewer56 Dec 12, 2025
c42bd93
improve: added quick note signifying that the subagent call is user i…
Sewer56 Dec 12, 2025
3d290b8
refactor: rename visible to hidden for subagent config
Sewer56 Dec 16, 2025
50fd411
Merge remote-tracking branch 'origin/dev' into add-hide-subagents
Sewer56 Dec 16, 2025
7bf9f16
Merge remote-tracking branch 'origin/dev' into add-hide-subagents
Sewer56 Dec 18, 2025
3120e07
Merge remote-tracking branch 'origin/dev' into add-hide-subagents
Sewer56 Dec 20, 2025
b761290
Merge remote-tracking branch 'origin/dev' into add-hide-subagents
Sewer56 Dec 21, 2025
16aec20
Merge remote-tracking branch 'origin/dev' into add-hide-subagents
Sewer56 Dec 21, 2025
74175bd
Merge remote-tracking branch 'origin/dev' into add-hide-subagents
Sewer56 Dec 22, 2025
3cba3a9
Merge remote-tracking branch 'origin/dev' into add-hide-subagents
Sewer56 Dec 22, 2025
bc82564
Merge remote-tracking branch 'origin/dev' into add-hide-subagents
Sewer56 Dec 23, 2025
e004cdc
Merge remote-tracking branch 'origin/dev' into add-hide-subagents
Sewer56 Dec 24, 2025
fe55ee1
Merge tag 'v1.0.195' into add-hide-subagents
Sewer56 Dec 24, 2025
cc79bd7
Merge tag 'v1.0.197' into add-hide-subagents
Sewer56 Dec 24, 2025
270ff9c
Merge remote-tracking branch 'origin/dev' into add-hide-subagents
Sewer56 Dec 26, 2025
fbcb276
Merge remote-tracking branch 'origin/dev' into add-hide-subagents
Sewer56 Dec 26, 2025
cac9b3a
Merge remote-tracking branch 'origin/dev' into add-hide-subagents
Sewer56 Dec 27, 2025
5023b0c
Merge tag 'v1.0.204' into add-hide-subagents
Sewer56 Dec 28, 2025
fb5cc53
Merge remote-tracking branch 'origin/dev' into add-hide-subagents
Sewer56 Dec 31, 2025
7ae8567
Merge tag 'v1.0.220' into add-hide-subagents
Sewer56 Dec 31, 2025
42d66de
Merge tag 'v1.0.221' into add-hide-subagents
Sewer56 Jan 1, 2026
59c67a1
Merge tag 'v1.0.222' into add-hide-subagents
Sewer56 Jan 1, 2026
45b4078
Merge tag 'v1.0.223' into add-hide-subagents
Sewer56 Jan 2, 2026
51c0d37
Merge tag 'before-permission-rework' into add-hide-subagents
Sewer56 Jan 2, 2026
3eb358f
feat: allow hiding subagents and controlling task invocation permissions
Sewer56 Jan 2, 2026
54614c0
Merge 3eb358f: recreate hide subagents feature on permission rework
Sewer56 Jan 2, 2026
f9da342
Merge tag 'v1.0.224' into add-hide-subagents
Sewer56 Jan 2, 2026
2798b2f
fix: treat slash command subagents as user-invoked to bypass permissi…
Sewer56 Jan 2, 2026
da0e1dd
fix: treat slash command subagents as user-invoked to bypass permissi…
Sewer56 Jan 2, 2026
6176b51
Merge tag 'v1.1.1' into add-hide-subagents
Sewer56 Jan 5, 2026
c831237
Merge remote-tracking branch 'origin/production' into add-hide-subagents
Sewer56 Jan 5, 2026
932b457
fix: include task tool when subagent-specific patterns are allowed
Sewer56 Jan 5, 2026
1063b13
docs: clarify that last matching rule wins for task permissions
Sewer56 Jan 5, 2026
e354ac9
Merge branch 'dev' into add-hide-subagents
rekram1-node Jan 5, 2026
dff1c6d
rm todo
rekram1-node Jan 6, 2026
966171c
Merge branch 'dev' into add-hide-subagents
rekram1-node Jan 7, 2026
7a46f44
clean
rekram1-node Jan 7, 2026
2c670a6
test: fix
rekram1-node Jan 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ export namespace Agent {
topP: z.number().optional(),
temperature: z.number().optional(),
color: z.string().optional(),
hidden: z.boolean().optional(),
permission: z.object({
edit: Config.Permission,
bash: z.record(z.string(), Config.Permission),
webfetch: Config.Permission.optional(),
doom_loop: Config.Permission.optional(),
external_directory: Config.Permission.optional(),
task: z.record(z.string(), Config.Permission).optional(),
}),
model: z
.object({
Expand Down Expand Up @@ -215,6 +217,7 @@ export namespace Agent {
permission,
color,
maxSteps,
hidden,
...extra
} = value
item.options = {
Expand All @@ -237,6 +240,7 @@ export namespace Agent {
if (top_p != undefined) item.topP = top_p
if (mode) item.mode = mode
if (color) item.color = color
if (hidden != undefined) item.hidden = hidden
// just here for consistency & to prevent it from being added as an option
if (name) item.name = name
if (maxSteps != undefined) item.maxSteps = maxSteps
Expand Down Expand Up @@ -323,12 +327,25 @@ function mergeAgentPermissions(basePermission: any, overridePermission: any): Ag
}
}

let mergedTask
if (merged.task) {
if (typeof merged.task === "object") {
mergedTask = mergeDeep(
{
"*": "allow",
},
merged.task,
)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor style suggestion: The style guide prefers avoiding let statements. This could be refactored using a conditional expression or IIFE pattern, similar to the existing mergedBash handling above. However, since this follows the exact same pattern as the existing code, keeping it consistent is also valid. Just a thought for potential cleanup - feel free to ignore if you prefer keeping it consistent with the existing mergedBash pattern.


const result: Agent.Info["permission"] = {
edit: merged.edit ?? "allow",
webfetch: merged.webfetch ?? "allow",
bash: mergedBash ?? { "*": "allow" },
doom_loop: merged.doom_loop,
external_directory: merged.external_directory,
task: mergedTask,
}

return result
Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,10 @@ export namespace Config {
disable: z.boolean().optional(),
description: z.string().optional().describe("Description of when to use the agent"),
mode: z.enum(["subagent", "primary", "all"]).optional(),
hidden: z
.boolean()
.optional()
.describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"),
color: z
.string()
.regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format")
Expand All @@ -410,6 +414,7 @@ export namespace Config {
webfetch: Permission.optional(),
doom_loop: Permission.optional(),
external_directory: Permission.optional(),
task: z.record(z.string(), Permission).optional(),
})
.optional(),
})
Expand Down
38 changes: 34 additions & 4 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { SessionSummary } from "./summary"
import { NamedError } from "@opencode-ai/util/error"
import { fn } from "@/util/fn"
import { SessionProcessor } from "./processor"
import { TaskTool } from "@/tool/task"
import { TaskTool, filterSubagents, TASK_DESCRIPTION } from "@/tool/task"
import { SessionStatus } from "./status"
import { LLM } from "./llm"
import { iife } from "@/util/iife"
Expand Down Expand Up @@ -465,12 +465,19 @@ export namespace SessionPrompt {
model,
abort,
})

const userInvokedAgents = msgs
.filter((m) => m.info.role === "user")
.flatMap((m) => m.parts.filter((p) => p.type === "agent") as MessageV2.AgentPart[])
.map((p) => p.name)

const tools = await resolveTools({
agent,
sessionID,
model,
tools: lastUser.tools,
processor,
userInvokedAgents,
})

if (step === 1) {
Expand Down Expand Up @@ -532,6 +539,7 @@ export namespace SessionPrompt {
sessionID: string
tools?: Record<string, boolean>
processor: SessionProcessor.Info
userInvokedAgents: string[]
}) {
using _ = log.time("resolveTools")
const tools: Record<string, AITool> = {}
Expand Down Expand Up @@ -564,7 +572,7 @@ export namespace SessionPrompt {
abort: options.abortSignal!,
messageID: input.processor.message.id,
callID: options.toolCallId,
extra: { model: input.model },
extra: { model: input.model, userInvokedAgents: input.userInvokedAgents },
agent: input.agent.name,
metadata: async (val) => {
const match = input.processor.partFromToolCall(options.toolCallId)
Expand Down Expand Up @@ -668,6 +676,23 @@ export namespace SessionPrompt {
}
tools[key] = item
}

// Regenerate task tool description with filtered subagents
if (tools.task) {
const all = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary"))
const filtered = filterSubagents(all, input.agent.permission.task ?? {})
const description = TASK_DESCRIPTION.replace(
"{agents}",
filtered
.map((a) => `- ${a.name}: ${a.description ?? "This subagent should only be called manually by the user."}`)
.join("\n"),
)
tools.task = {
...tools.task,
description,
}
}

return tools
}

Expand Down Expand Up @@ -901,6 +926,8 @@ export namespace SessionPrompt {
}

if (part.type === "agent") {
const perm = Wildcard.all(part.name, agent.permission.task ?? {})
const hint = perm === "deny" ? " . Invoked by user; guaranteed to exist." : ""
return [
{
id: Identifier.ascending("part"),
Expand All @@ -915,8 +942,11 @@ export namespace SessionPrompt {
type: "text",
synthetic: true,
text:
"Use the above message and context to generate a prompt and call the task tool with subagent: " +
part.name,
// An extra space is added here. Otherwise the 'Use' gets appended
// to user's last word; making a combined word
" Use the above message and context to generate a prompt and call the task tool with subagent: " +
part.name +
hint,
},
]
}
Expand Down
33 changes: 33 additions & 0 deletions packages/opencode/src/tool/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import { Agent } from "../agent/agent"
import { SessionPrompt } from "../session/prompt"
import { iife } from "@/util/iife"
import { defer } from "@/util/defer"
import { Wildcard } from "@/util/wildcard"
import { Permission } from "../permission"

export { DESCRIPTION as TASK_DESCRIPTION }

export function filterSubagents(agents: Agent.Info[], permissions: Record<string, Config.Permission>) {
return agents.filter((a) => Wildcard.all(a.name, permissions) !== "deny")
}
import { Config } from "../config/config"

export const TaskTool = Tool.define("task", async () => {
Expand All @@ -30,6 +38,31 @@ export const TaskTool = Tool.define("task", async () => {
async execute(params, ctx) {
const agent = await Agent.get(params.subagent_type)
if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`)
const calling = await Agent.get(ctx.agent)
if (calling) {
const userInvokedAgents = (ctx.extra?.userInvokedAgents ?? []) as string[]
// Skip permission check if user explicitly invoked this agent via @ autocomplete
if (!userInvokedAgents.includes(params.subagent_type)) {
const perm = Wildcard.all(params.subagent_type, calling.permission.task ?? {})
if (perm === "deny") {
throw new Error(`Agent '${params.subagent_type}' is not available to ${ctx.agent}`)
}
if (perm === "ask") {
await Permission.ask({
type: "task",
title: `Invoke subagent: ${params.subagent_type}`,
pattern: params.subagent_type,
callID: ctx.callID,
sessionID: ctx.sessionID,
messageID: ctx.messageID,
metadata: {
subagent: params.subagent_type,
description: params.description,
},
})
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style suggestion: The nested if statements here create deep nesting which the style guide discourages. Consider refactoring with early returns to flatten the control flow:

Suggested change
}
const calling = await Agent.get(ctx.agent)
if (!calling) {
// continue with existing flow
}
const userInvokedAgents = (ctx.extra?.userInvokedAgents ?? []) as string[]
// Skip permission check if user explicitly invoked this agent via @ autocomplete
if (userInvokedAgents.includes(params.subagent_type)) {
// User explicitly invoked, skip permission check
}
const perm = Wildcard.all(params.subagent_type, calling.permission.task ?? {})
if (perm === "deny") {
throw new Error(`Agent '${params.subagent_type}' is not available to ${ctx.agent}`)
}
if (perm === "ask") {
await Permission.ask({
type: "task",
title: `Invoke subagent: ${params.subagent_type}`,
pattern: params.subagent_type,
callID: ctx.callID,
sessionID: ctx.sessionID,
messageID: ctx.messageID,
metadata: {
subagent: params.subagent_type,
description: params.description,
},
})
}

Alternatively, the entire permission check block could be extracted into a separate function to reduce complexity. This is just a suggestion - the current code works correctly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw don't apply these diffs, typically the agent sucks at diffs but has decent feedback, ill tweak prompt for diffs

}
}
const session = await iife(async () => {
if (params.session_id) {
const found = await Session.get(params.session_id).catch(() => {})
Expand Down
Loading
Loading