fix(delegate-task): restore hidden plan delegation
This commit is contained in:
@@ -1,10 +1,9 @@
|
|||||||
|
import { loadProjectAgents, loadUserAgents } from "../../features/claude-code-agent-loader"
|
||||||
import {
|
import {
|
||||||
getAgentConfigKey,
|
getAgentConfigKey,
|
||||||
getAgentDisplayName,
|
getAgentDisplayName,
|
||||||
stripAgentListSortPrefix,
|
stripAgentListSortPrefix,
|
||||||
stripInvisibleAgentCharacters,
|
|
||||||
} from "../../shared/agent-display-names"
|
} from "../../shared/agent-display-names"
|
||||||
import { loadUserAgents, loadProjectAgents } from "../../features/claude-code-agent-loader"
|
|
||||||
|
|
||||||
export type AgentMode = "subagent" | "primary" | "all" | undefined
|
export type AgentMode = "subagent" | "primary" | "all" | undefined
|
||||||
|
|
||||||
@@ -16,7 +15,7 @@ export type AgentInfo = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeSubagentType(subagentType: string): string {
|
export function sanitizeSubagentType(subagentType: string): string {
|
||||||
return subagentType.trim().replace(/^[\\\/"']+|[\\\/"']+$/g, "").trim()
|
return subagentType.trim().replace(/^[\\/"']+|[\\/"']+$/g, "").trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeWithClaudeCodeAgents(
|
export function mergeWithClaudeCodeAgents(
|
||||||
@@ -69,10 +68,10 @@ export function isTaskCallableAgentMode(mode: AgentMode): boolean {
|
|||||||
return mode === "all" || mode === "subagent"
|
return mode === "all" || mode === "subagent"
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDemotedPlanAgent(agent: AgentInfo): boolean {
|
export function isDemotedPlanAgent(agent: AgentInfo): boolean {
|
||||||
return agent.hidden === true
|
return agent.hidden === true
|
||||||
&& agent.mode === "subagent"
|
&& agent.mode === "subagent"
|
||||||
&& stripInvisibleAgentCharacters(agent.name).trim().toLowerCase() === "plan"
|
&& stripAgentListSortPrefix(agent.name).trim().toLowerCase() === "plan"
|
||||||
}
|
}
|
||||||
|
|
||||||
function isVisibleToTask(agent: AgentInfo): boolean {
|
function isVisibleToTask(agent: AgentInfo): boolean {
|
||||||
|
|||||||
@@ -1,30 +1,65 @@
|
|||||||
import type { DelegateTaskArgs } from "./types"
|
import type { DelegateTaskArgs } from "./types"
|
||||||
import type { ExecutorContext } from "./executor-types"
|
import type { ExecutorContext } from "./executor-types"
|
||||||
import type { DelegatedModelConfig } from "./types"
|
import type { DelegatedModelConfig } from "./types"
|
||||||
import { isPlanFamily } from "./constants"
|
import { isPlanAgent, isPlanFamily } from "./constants"
|
||||||
import { SISYPHUS_JUNIOR_AGENT } from "./sisyphus-junior-agent"
|
import { SISYPHUS_JUNIOR_AGENT } from "./sisyphus-junior-agent"
|
||||||
import { applyCategoryParams } from "./delegated-model-config"
|
import { applyCategoryParams } from "./delegated-model-config"
|
||||||
|
import { getAvailableModelsForDelegateTask } from "./available-models"
|
||||||
import { resolveEffectiveFallbackEntry } from "./fallback-entry-resolution"
|
import { resolveEffectiveFallbackEntry } from "./fallback-entry-resolution"
|
||||||
import { applyFallbackEntrySettings } from "./fallback-entry-settings"
|
import { applyFallbackEntrySettings } from "./fallback-entry-settings"
|
||||||
|
import type { AgentInfo } from "./subagent-discovery"
|
||||||
import {
|
import {
|
||||||
type AgentInfo,
|
|
||||||
sanitizeSubagentType,
|
|
||||||
mergeWithClaudeCodeAgents,
|
|
||||||
findPrimaryAgentMatch,
|
findPrimaryAgentMatch,
|
||||||
findCallableAgentMatch,
|
findCallableAgentMatch,
|
||||||
|
sanitizeSubagentType,
|
||||||
listCallableAgentNames,
|
listCallableAgentNames,
|
||||||
|
mergeWithClaudeCodeAgents,
|
||||||
|
isDemotedPlanAgent,
|
||||||
} from "./subagent-discovery"
|
} from "./subagent-discovery"
|
||||||
import { normalizeModelFormat } from "../../shared/model-format-normalizer"
|
|
||||||
import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
|
|
||||||
import { normalizeFallbackModels, flattenToFallbackModelStrings } from "../../shared/model-resolver"
|
|
||||||
import { buildFallbackChainFromModels } from "../../shared/fallback-chain-from-models"
|
|
||||||
import { getAgentConfigKey, stripAgentListSortPrefix } from "../../shared/agent-display-names"
|
|
||||||
import { normalizeSDKResponse } from "../../shared"
|
|
||||||
import { log } from "../../shared/logger"
|
|
||||||
import { getAvailableModelsForDelegateTask } from "./available-models"
|
|
||||||
import type { FallbackEntry } from "../../shared/model-requirements"
|
import type { FallbackEntry } from "../../shared/model-requirements"
|
||||||
|
import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
|
||||||
import { resolveModelForDelegateTask } from "./model-selection"
|
import { resolveModelForDelegateTask } from "./model-selection"
|
||||||
import { fuzzyMatchModel } from "../../shared/model-availability"
|
import { fuzzyMatchModel } from "../../shared/model-availability"
|
||||||
|
import { getAgentConfigKey, stripAgentListSortPrefix } from "../../shared/agent-display-names"
|
||||||
|
import { buildFallbackChainFromModels } from "../../shared/fallback-chain-from-models"
|
||||||
|
import { normalizeSDKResponse } from "../../shared"
|
||||||
|
import { normalizeModelFormat } from "../../shared/model-format-normalizer"
|
||||||
|
import { flattenToFallbackModelStrings, normalizeFallbackModels } from "../../shared/model-resolver"
|
||||||
|
import { log } from "../../shared/logger"
|
||||||
|
|
||||||
|
const DEFAULT_PLAN_FALLBACK_AGENT = "plan"
|
||||||
|
const RESERVED_HIDDEN_NATIVE_AGENTS = new Set(["build"])
|
||||||
|
|
||||||
|
function isReservedHiddenNativeAgent(agentName: string): boolean {
|
||||||
|
return RESERVED_HIDDEN_NATIVE_AGENTS.has(getAgentConfigKey(agentName))
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldUseHiddenPlanAgent(
|
||||||
|
requestedAgent: string,
|
||||||
|
serverPrimaryAgent: AgentInfo | undefined,
|
||||||
|
serverMatchedAgent: AgentInfo | undefined,
|
||||||
|
sisyphusAgentConfig: ExecutorContext["sisyphusAgentConfig"],
|
||||||
|
hasDemotedPlan: boolean,
|
||||||
|
): boolean {
|
||||||
|
if (serverPrimaryAgent) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasDemotedPlan) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverMatchedAgent) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPlanAgent(requestedAgent)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return sisyphusAgentConfig?.planner_enabled !== false
|
||||||
|
&& sisyphusAgentConfig?.replace_plan !== false
|
||||||
|
}
|
||||||
|
|
||||||
export interface ResolveSubagentExecutionOptions {
|
export interface ResolveSubagentExecutionOptions {
|
||||||
allowSisyphusJuniorDirect?: boolean
|
allowSisyphusJuniorDirect?: boolean
|
||||||
@@ -74,18 +109,36 @@ Create the work plan directly - that's your job as the planning agent.`,
|
|||||||
|
|
||||||
let agentToUse = agentName
|
let agentToUse = agentName
|
||||||
let categoryModel: DelegatedModelConfig | undefined
|
let categoryModel: DelegatedModelConfig | undefined
|
||||||
let fallbackChain: FallbackEntry[] | undefined = undefined
|
let fallbackChain: FallbackEntry[] | undefined
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const agentsResult = await client.app.agents()
|
const agentsResult = await client.app.agents()
|
||||||
const agents = normalizeSDKResponse(agentsResult, [] as AgentInfo[], {
|
const agents = normalizeSDKResponse(agentsResult, [] as AgentInfo[], {
|
||||||
preferResponseOnMissingData: true,
|
preferResponseOnMissingData: true,
|
||||||
})
|
})
|
||||||
|
const hasDemotedPlan = agents.some(isDemotedPlanAgent)
|
||||||
|
const serverPrimaryAgent = findPrimaryAgentMatch(agents, agentToUse)
|
||||||
|
const serverMatchedAgent = findCallableAgentMatch(agents, agentToUse)
|
||||||
|
|
||||||
const mergedAgents = mergeWithClaudeCodeAgents(agents, executorCtx.directory)
|
const mergedAgents = mergeWithClaudeCodeAgents(agents, executorCtx.directory)
|
||||||
const matchedPrimaryAgent = findPrimaryAgentMatch(mergedAgents, agentToUse)
|
const matchedPrimaryAgent = findPrimaryAgentMatch(mergedAgents, agentToUse)
|
||||||
|
const useHiddenPlanFallback = shouldUseHiddenPlanAgent(
|
||||||
|
agentToUse,
|
||||||
|
serverPrimaryAgent,
|
||||||
|
serverMatchedAgent,
|
||||||
|
executorCtx.sisyphusAgentConfig,
|
||||||
|
hasDemotedPlan,
|
||||||
|
)
|
||||||
|
|
||||||
if (matchedPrimaryAgent && !options.allowPrimaryAgentDelegation) {
|
if (isReservedHiddenNativeAgent(agentToUse) && !serverPrimaryAgent && !serverMatchedAgent) {
|
||||||
|
return {
|
||||||
|
agentToUse: "",
|
||||||
|
categoryModel: undefined,
|
||||||
|
error: `Unknown agent: "${agentToUse}". Available agents: ${listCallableAgentNames(agents)}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedPrimaryAgent && !options.allowPrimaryAgentDelegation && !useHiddenPlanFallback) {
|
||||||
return {
|
return {
|
||||||
agentToUse: "",
|
agentToUse: "",
|
||||||
categoryModel: undefined,
|
categoryModel: undefined,
|
||||||
@@ -94,10 +147,17 @@ Create the work plan directly - that's your job as the planning agent.`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const usePrimary = options.allowPrimaryAgentDelegation && matchedPrimaryAgent !== undefined
|
const usePrimary = options.allowPrimaryAgentDelegation && matchedPrimaryAgent !== undefined
|
||||||
const matchedAgent = usePrimary
|
let matchedAgent = usePrimary
|
||||||
? matchedPrimaryAgent
|
? matchedPrimaryAgent
|
||||||
: findCallableAgentMatch(mergedAgents, agentToUse)
|
: findCallableAgentMatch(mergedAgents, agentToUse)
|
||||||
|
|
||||||
|
if (useHiddenPlanFallback) {
|
||||||
|
matchedAgent = {
|
||||||
|
name: DEFAULT_PLAN_FALLBACK_AGENT,
|
||||||
|
mode: "subagent",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!matchedAgent) {
|
if (!matchedAgent) {
|
||||||
return {
|
return {
|
||||||
agentToUse: "",
|
agentToUse: "",
|
||||||
@@ -153,7 +213,8 @@ Create the work plan directly - that's your job as the planning agent.`,
|
|||||||
categoryModel = applyCategoryParams(resolvedModel, agentCategoryConfig)
|
categoryModel = applyCategoryParams(resolvedModel, agentCategoryConfig)
|
||||||
}
|
}
|
||||||
} else if (resolutionSkipped && (agentOverride?.model ?? agentCategoryModel)) {
|
} else if (resolutionSkipped && (agentOverride?.model ?? agentCategoryModel)) {
|
||||||
const normalized = normalizeModelFormat((agentOverride?.model ?? agentCategoryModel)!)
|
const explicitModel = agentOverride?.model ?? agentCategoryModel
|
||||||
|
const normalized = explicitModel ? normalizeModelFormat(explicitModel) : undefined
|
||||||
if (normalized) {
|
if (normalized) {
|
||||||
const variantToUse = agentOverride?.variant ?? agentCategoryConfig?.variant
|
const variantToUse = agentOverride?.variant ?? agentCategoryConfig?.variant
|
||||||
const resolvedModel = variantToUse ? { ...normalized, variant: variantToUse } : normalized
|
const resolvedModel = variantToUse ? { ...normalized, variant: variantToUse } : normalized
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test"
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"
|
||||||
import type { DelegateTaskArgs } from "../types"
|
import type { DelegateTaskArgs } from "../types"
|
||||||
import type { ExecutorContext } from "../executor-types"
|
import type { ExecutorContext } from "../executor-types"
|
||||||
|
|
||||||
@@ -282,6 +282,270 @@ describe("resolveSubagentExecution", () => {
|
|||||||
expect(result.categoryModel).toBeUndefined()
|
expect(result.categoryModel).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("preserves hidden sort-prefixed plan agent model instead of using fallback", async () => {
|
||||||
|
//#given
|
||||||
|
readProviderModelsCacheMock.mockReturnValue({
|
||||||
|
models: { anthropic: ["claude-opus-4-7"] },
|
||||||
|
connected: ["anthropic"],
|
||||||
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||||
|
})
|
||||||
|
const args = createBaseArgs({ subagent_type: "plan" })
|
||||||
|
const executorCtx = createExecutorContext(async () => ([
|
||||||
|
{ name: "1|plan", mode: "subagent", hidden: true, model: "anthropic/claude-opus-4-7" },
|
||||||
|
{ name: "oracle", mode: "subagent" },
|
||||||
|
]))
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.error).toBeUndefined()
|
||||||
|
expect(result.agentToUse).toBe("plan")
|
||||||
|
expect(result.categoryModel).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-7" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("allows OpenCode-hidden-list plan fallback when planner_enabled and replace_plan are true", async () => {
|
||||||
|
//#given
|
||||||
|
const args = createBaseArgs({ subagent_type: "plan" })
|
||||||
|
const executorCtx = createExecutorContext(async () => ([
|
||||||
|
{ name: "oracle", mode: "subagent" },
|
||||||
|
]), {
|
||||||
|
sisyphusAgentConfig: {
|
||||||
|
planner_enabled: true,
|
||||||
|
replace_plan: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.error).toBeUndefined()
|
||||||
|
expect(result.agentToUse).toBe("plan")
|
||||||
|
expect(result.categoryModel).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ loader: "user", aliasName: "plan" },
|
||||||
|
{ loader: "user", aliasName: '"plan"' },
|
||||||
|
{ loader: "project", aliasName: "plan" },
|
||||||
|
{ loader: "project", aliasName: '"plan"' },
|
||||||
|
])(
|
||||||
|
"uses built-in hidden plan fallback when a $loader $aliasName alias exists",
|
||||||
|
async ({ loader, aliasName }) => {
|
||||||
|
//#given
|
||||||
|
readProviderModelsCacheMock.mockReturnValue({
|
||||||
|
models: { openai: ["gpt-5.3-codex"] },
|
||||||
|
connected: ["openai"],
|
||||||
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||||
|
})
|
||||||
|
|
||||||
|
loadUserAgentsMock.mockImplementation(() => {
|
||||||
|
if (loader === "user") {
|
||||||
|
return {
|
||||||
|
[aliasName]: {
|
||||||
|
description: "Colliding plan alias from user agents",
|
||||||
|
mode: "subagent",
|
||||||
|
model: "openai/gpt-5.3-codex",
|
||||||
|
},
|
||||||
|
} satisfies ClaudeCodeAgentRecord
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
loadProjectAgentsMock.mockImplementation(() => {
|
||||||
|
if (loader === "project") {
|
||||||
|
return {
|
||||||
|
[aliasName]: {
|
||||||
|
description: "Colliding plan alias from project agents",
|
||||||
|
mode: "subagent",
|
||||||
|
model: "openai/gpt-5.3-codex",
|
||||||
|
},
|
||||||
|
} satisfies ClaudeCodeAgentRecord
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const args = createBaseArgs({ subagent_type: "plan" })
|
||||||
|
const executorCtx = createExecutorContext(async () => ([
|
||||||
|
{ name: "oracle", mode: "subagent" },
|
||||||
|
]), {
|
||||||
|
sisyphusAgentConfig: {
|
||||||
|
planner_enabled: true,
|
||||||
|
replace_plan: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.error).toBeUndefined()
|
||||||
|
expect(result.agentToUse).toBe("plan")
|
||||||
|
expect(result.categoryModel).toBeUndefined()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ loader: "user", aliasName: "plan" },
|
||||||
|
{ loader: "user", aliasName: '"plan"' },
|
||||||
|
{ loader: "project", aliasName: "plan" },
|
||||||
|
{ loader: "project", aliasName: '"plan"' },
|
||||||
|
])(
|
||||||
|
"uses built-in hidden plan fallback when a $loader primary $aliasName alias exists",
|
||||||
|
async ({ loader, aliasName }) => {
|
||||||
|
//#given
|
||||||
|
loadUserAgentsMock.mockImplementation(() => {
|
||||||
|
if (loader === "user") {
|
||||||
|
return {
|
||||||
|
[aliasName]: {
|
||||||
|
description: "Colliding primary plan alias from user agents",
|
||||||
|
mode: "primary",
|
||||||
|
model: "openai/gpt-5.3-codex",
|
||||||
|
},
|
||||||
|
} satisfies ClaudeCodeAgentRecord
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
loadProjectAgentsMock.mockImplementation(() => {
|
||||||
|
if (loader === "project") {
|
||||||
|
return {
|
||||||
|
[aliasName]: {
|
||||||
|
description: "Colliding primary plan alias from project agents",
|
||||||
|
mode: "primary",
|
||||||
|
model: "openai/gpt-5.3-codex",
|
||||||
|
},
|
||||||
|
} satisfies ClaudeCodeAgentRecord
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const args = createBaseArgs({ subagent_type: "plan" })
|
||||||
|
const executorCtx = createExecutorContext(async () => ([
|
||||||
|
{ name: "oracle", mode: "subagent" },
|
||||||
|
]), {
|
||||||
|
sisyphusAgentConfig: {
|
||||||
|
planner_enabled: true,
|
||||||
|
replace_plan: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.error).toBeUndefined()
|
||||||
|
expect(result.agentToUse).toBe("plan")
|
||||||
|
expect(result.categoryModel).toBeUndefined()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ loader: "user", aliasName: "build" },
|
||||||
|
{ loader: "user", aliasName: '"build"' },
|
||||||
|
{ loader: "user", aliasName: "1|build" },
|
||||||
|
{ loader: "user", aliasName: "\u200Bbuild" },
|
||||||
|
{ loader: "project", aliasName: "build" },
|
||||||
|
{ loader: "project", aliasName: '"build"' },
|
||||||
|
{ loader: "project", aliasName: "1|build" },
|
||||||
|
{ loader: "project", aliasName: "\u200Bbuild" },
|
||||||
|
])(
|
||||||
|
"rejects omitted hidden build when a $loader $aliasName alias exists",
|
||||||
|
async ({ loader, aliasName }) => {
|
||||||
|
//#given
|
||||||
|
loadUserAgentsMock.mockImplementation(() => {
|
||||||
|
if (loader === "user") {
|
||||||
|
return {
|
||||||
|
[aliasName]: {
|
||||||
|
description: "Colliding hidden build alias from user agents",
|
||||||
|
mode: "subagent",
|
||||||
|
model: "openai/gpt-5.3-codex",
|
||||||
|
},
|
||||||
|
} satisfies ClaudeCodeAgentRecord
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
loadProjectAgentsMock.mockImplementation(() => {
|
||||||
|
if (loader === "project") {
|
||||||
|
return {
|
||||||
|
[aliasName]: {
|
||||||
|
description: "Colliding hidden build alias from project agents",
|
||||||
|
mode: "subagent",
|
||||||
|
model: "openai/gpt-5.3-codex",
|
||||||
|
},
|
||||||
|
} satisfies ClaudeCodeAgentRecord
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const args = createBaseArgs({ subagent_type: "build" })
|
||||||
|
const executorCtx = createExecutorContext(async () => ([
|
||||||
|
{ 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: "build". Available agents: oracle')
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
test("preserves a visible server plan agent instead of using fallback", async () => {
|
||||||
|
//#given
|
||||||
|
readProviderModelsCacheMock.mockReturnValue({
|
||||||
|
models: { openai: ["gpt-5.3-codex"] },
|
||||||
|
connected: ["openai"],
|
||||||
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||||
|
})
|
||||||
|
const args = createBaseArgs({ subagent_type: "plan" })
|
||||||
|
const executorCtx = createExecutorContext(async () => ([
|
||||||
|
{ name: "plan", mode: "subagent", model: "openai/gpt-5.3-codex" },
|
||||||
|
{ name: "oracle", mode: "subagent" },
|
||||||
|
]), {
|
||||||
|
sisyphusAgentConfig: {
|
||||||
|
planner_enabled: true,
|
||||||
|
replace_plan: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.error).toBeUndefined()
|
||||||
|
expect(result.agentToUse).toBe("plan")
|
||||||
|
expect(result.categoryModel).toEqual({ providerID: "openai", modelID: "gpt-5.3-codex" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
[{ planner_enabled: false, replace_plan: true }],
|
||||||
|
[{ planner_enabled: true, replace_plan: false }],
|
||||||
|
])(
|
||||||
|
"does not allow hidden plan fallback when planner config blocks replacement (%j)",
|
||||||
|
async (sisyphusAgentConfig) => {
|
||||||
|
//#given
|
||||||
|
const args = createBaseArgs({ subagent_type: "plan" })
|
||||||
|
const executorCtx = createExecutorContext(async () => ([
|
||||||
|
{ name: "oracle", mode: "subagent" },
|
||||||
|
]), {
|
||||||
|
sisyphusAgentConfig,
|
||||||
|
})
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.agentToUse).toBe("")
|
||||||
|
expect(result.categoryModel).toBeUndefined()
|
||||||
|
expect(result.error).toBe('Unknown agent: "plan". Available agents: oracle')
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
test("hidden agents are excluded from error hints except callable demoted plan", async () => {
|
test("hidden agents are excluded from error hints except callable demoted plan", async () => {
|
||||||
//#given
|
//#given
|
||||||
const args = createBaseArgs({ subagent_type: "nonexistent" })
|
const args = createBaseArgs({ subagent_type: "nonexistent" })
|
||||||
|
|||||||
Reference in New Issue
Block a user