fix(delegate-task): restrict task-callable agent modes

This commit is contained in:
YeonGyu-Kim
2026-04-04 19:33:11 +09:00
parent ad025ee0f8
commit f8398dbec3
2 changed files with 42 additions and 14 deletions
@@ -85,6 +85,41 @@ describe("resolveSubagentExecution", () => {
})
})
test("hides primary agents from task delegation lookups", async () => {
//#given
const args = createBaseArgs({ subagent_type: "sisyphus" })
const executorCtx = createExecutorContext(async () => ([
{ name: "sisyphus", mode: "primary" },
{ name: "oracle", mode: "subagent" },
{ name: "metis", mode: "all" },
]))
//#when
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
//#then
expect(result.agentToUse).toBe("")
expect(result.categoryModel).toBeUndefined()
expect(result.error).toBe('Unknown agent: "sisyphus". Available agents: metis, oracle')
})
test("requires explicit all or subagent mode for task-callable agents", async () => {
//#given
const args = createBaseArgs({ subagent_type: "custom-worker" })
const executorCtx = createExecutorContext(async () => ([
{ name: "custom-worker" },
{ name: "oracle", mode: "subagent" },
]))
//#when
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
//#then
expect(result.agentToUse).toBe("")
expect(result.categoryModel).toBeUndefined()
expect(result.error).toBe('Unknown agent: "custom-worker". Available agents: oracle')
})
test("normalizes matched agent model string before returning categoryModel", async () => {
//#given
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
+7 -14
View File
@@ -16,6 +16,8 @@ import { resolveModelForDelegateTask } from "./model-selection"
import { fuzzyMatchModel } from "../../shared/model-availability"
import type { CategoryConfig } from "../../config/schema"
type AgentMode = "subagent" | "primary" | "all" | undefined
function applyCategoryParams(
base: DelegatedModelConfig,
config: CategoryConfig | undefined,
@@ -85,7 +87,7 @@ Create the work plan directly - that's your job as the planning agent.`,
preferResponseOnMissingData: true,
})
const callableAgents = agents.filter((a) => a.mode !== "primary")
const callableAgents = agents.filter((agent) => isTaskCallableAgentMode(agent.mode))
const resolvedDisplayName = getAgentDisplayName(agentToUse)
const matchedAgent = callableAgents.find(
@@ -93,19 +95,6 @@ Create the work plan directly - that's your job as the planning agent.`,
|| agent.name.toLowerCase() === resolvedDisplayName.toLowerCase()
)
if (!matchedAgent) {
const isPrimaryAgent = agents
.filter((a) => a.mode === "primary")
.find((agent) => agent.name.toLowerCase() === agentToUse.toLowerCase()
|| agent.name.toLowerCase() === resolvedDisplayName.toLowerCase())
if (isPrimaryAgent) {
return {
agentToUse: "",
categoryModel: undefined,
error: `Cannot call primary agent "${isPrimaryAgent.name}" via task. Primary agents are top-level orchestrators.`,
}
}
const availableAgents = callableAgents
.map((a) => a.name)
.sort()
@@ -239,3 +228,7 @@ Create the work plan directly - that's your job as the planning agent.`,
return { agentToUse, categoryModel, fallbackChain }
}
function isTaskCallableAgentMode(mode: AgentMode): boolean {
return mode === "all" || mode === "subagent"
}