Added: Ability to hide subagents from primary agents system prompt. (#4773)

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
This commit is contained in:
Sewer.
2026-01-07 04:29:17 +00:00
committed by GitHub
parent eaa0826e7f
commit fd7b7eacd3
8 changed files with 580 additions and 6 deletions

View File

@@ -188,6 +188,7 @@ export namespace Agent {
item.topP = value.top_p ?? item.topP
item.mode = value.mode ?? item.mode
item.color = value.color ?? item.color
item.hidden = value.hidden ?? item.hidden
item.name = value.name ?? item.name
item.steps = value.steps ?? item.steps
item.options = mergeDeep(item.options, value.options ?? {})

View File

@@ -465,6 +465,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)"),
options: z.record(z.string(), z.any()).optional(),
color: z
.string()
@@ -490,6 +494,7 @@ export namespace Config {
"temperature",
"top_p",
"mode",
"hidden",
"color",
"steps",
"maxSteps",

View File

@@ -232,6 +232,7 @@ export namespace PermissionNext {
const result = new Set<string>()
for (const tool of tools) {
const permission = EDIT_TOOLS.includes(tool) ? "edit" : tool
const rule = ruleset.findLast((r) => Wildcard.match(permission, r.permission))
if (!rule) continue
if (rule.pattern === "*" && rule.action === "deny") result.add(tool)

View File

@@ -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 { Tool } from "@/tool/tool"
import { PermissionNext } from "@/permission/next"
import { SessionStatus } from "./status"
@@ -382,7 +382,8 @@ export namespace SessionPrompt {
messageID: assistantMessage.id,
sessionID: sessionID,
abort,
extra: { bypassAgentCheck: true },
callID: part.callID,
extra: { userInvokedAgents: [task.agent] },
async metadata(input) {
await Session.updatePart({
...part,
@@ -543,12 +544,20 @@ export namespace SessionPrompt {
model,
abort,
})
// Track agents explicitly invoked by user via @ autocomplete
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,
session,
model,
tools: lastUser.tools,
processor,
userInvokedAgents,
})
if (step === 1) {
@@ -637,6 +646,7 @@ export namespace SessionPrompt {
session: Session.Info
tools?: Record<string, boolean>
processor: SessionProcessor.Info
userInvokedAgents: string[]
}) {
using _ = log.time("resolveTools")
const tools: Record<string, AITool> = {}
@@ -646,7 +656,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: { title?: string; metadata?: any }) => {
const match = input.processor.partFromToolCall(options.toolCallId)
@@ -789,6 +799,29 @@ 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)
// If no subagents are permitted, remove the task tool entirely
if (filtered.length === 0) {
delete tools.task
} else {
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
}
@@ -1098,6 +1131,9 @@ export namespace SessionPrompt {
}
if (part.type === "agent") {
// Check if this agent would be denied by task permission
const perm = PermissionNext.evaluate("task", part.name, agent.permission)
const hint = perm.action === "deny" ? " . Invoked by user; guaranteed to exist." : ""
return [
{
id: Identifier.ascending("part"),
@@ -1111,9 +1147,12 @@ export namespace SessionPrompt {
sessionID: input.sessionID,
type: "text",
synthetic: true,
// An extra space is added here. Otherwise the 'Use' gets appended
// to user's last word; making a combined word
text:
"Use the above message and context to generate a prompt and call the task tool with subagent: " +
part.name,
" Use the above message and context to generate a prompt and call the task tool with subagent: " +
part.name +
hint,
},
]
}

View File

@@ -10,6 +10,13 @@ import { SessionPrompt } from "../session/prompt"
import { iife } from "@/util/iife"
import { defer } from "@/util/defer"
import { Config } from "../config/config"
import { PermissionNext } from "@/permission/next"
export { DESCRIPTION as TASK_DESCRIPTION }
export function filterSubagents(agents: Agent.Info[], ruleset: PermissionNext.Ruleset) {
return agents.filter((a) => PermissionNext.evaluate("task", a.name, ruleset).action !== "deny")
}
export const TaskTool = Tool.define("task", async () => {
const agents = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary"))
@@ -30,8 +37,10 @@ export const TaskTool = Tool.define("task", async () => {
}),
async execute(params, ctx) {
const config = await Config.get()
const userInvokedAgents = (ctx.extra?.userInvokedAgents ?? []) as string[]
// Skip permission check when invoked from a command subtask (user already approved by invoking the command)
if (!ctx.extra?.bypassAgentCheck) {
if (!ctx.extra?.bypassAgentCheck && !userInvokedAgents.includes(params.subagent_type)) {
await ctx.ask({
permission: "task",
patterns: [params.subagent_type],