feat(tools): wire team-mode into skill and subagent resolvers
This commit is contained in:
@@ -4,7 +4,13 @@ import { discoverSkills } from "../../features/opencode-skill-loader"
|
|||||||
|
|
||||||
export async function resolveSkillContent(
|
export async function resolveSkillContent(
|
||||||
skills: string[],
|
skills: string[],
|
||||||
options: { gitMasterConfig?: GitMasterConfig; browserProvider?: BrowserAutomationProvider, disabledSkills?: Set<string>, directory?: string }
|
options: {
|
||||||
|
gitMasterConfig?: GitMasterConfig
|
||||||
|
browserProvider?: BrowserAutomationProvider
|
||||||
|
disabledSkills?: Set<string>
|
||||||
|
teamModeEnabled?: boolean
|
||||||
|
directory?: string
|
||||||
|
}
|
||||||
): Promise<{ content: string | undefined; contents: string[]; error: string | null }> {
|
): Promise<{ content: string | undefined; contents: string[]; error: string | null }> {
|
||||||
if (skills.length === 0) {
|
if (skills.length === 0) {
|
||||||
return { content: undefined, contents: [], error: null }
|
return { content: undefined, contents: [], error: null }
|
||||||
|
|||||||
@@ -26,11 +26,17 @@ import type { FallbackEntry } 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"
|
||||||
|
|
||||||
|
export interface ResolveSubagentExecutionOptions {
|
||||||
|
allowSisyphusJuniorDirect?: boolean
|
||||||
|
allowPrimaryAgentDelegation?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export async function resolveSubagentExecution(
|
export async function resolveSubagentExecution(
|
||||||
args: DelegateTaskArgs,
|
args: DelegateTaskArgs,
|
||||||
executorCtx: ExecutorContext,
|
executorCtx: ExecutorContext,
|
||||||
parentAgent: string | undefined,
|
parentAgent: string | undefined,
|
||||||
categoryExamples: string
|
categoryExamples: string,
|
||||||
|
options: ResolveSubagentExecutionOptions = {},
|
||||||
): Promise<{ agentToUse: string; categoryModel: DelegatedModelConfig | undefined; fallbackChain?: FallbackEntry[]; error?: string }> {
|
): Promise<{ agentToUse: string; categoryModel: DelegatedModelConfig | undefined; fallbackChain?: FallbackEntry[]; error?: string }> {
|
||||||
const { client, agentOverrides, userCategories } = executorCtx
|
const { client, agentOverrides, userCategories } = executorCtx
|
||||||
|
|
||||||
@@ -40,11 +46,17 @@ export async function resolveSubagentExecution(
|
|||||||
|
|
||||||
const agentName = sanitizeSubagentType(args.subagent_type)
|
const agentName = sanitizeSubagentType(args.subagent_type)
|
||||||
|
|
||||||
if (agentName.toLowerCase() === SISYPHUS_JUNIOR_AGENT.toLowerCase()) {
|
if (
|
||||||
|
!options.allowSisyphusJuniorDirect &&
|
||||||
|
agentName.toLowerCase() === SISYPHUS_JUNIOR_AGENT.toLowerCase()
|
||||||
|
) {
|
||||||
|
const exampleHint = categoryExamples.trim() !== ""
|
||||||
|
? `Use category parameter instead (e.g., ${categoryExamples}).`
|
||||||
|
: `Use the category parameter instead (pick one of: quick, deep, ultrabrain, visual-engineering, artistry, writing).`
|
||||||
return {
|
return {
|
||||||
agentToUse: "",
|
agentToUse: "",
|
||||||
categoryModel: undefined,
|
categoryModel: undefined,
|
||||||
error: `Cannot use subagent_type="${SISYPHUS_JUNIOR_AGENT}" directly. Use category parameter instead (e.g., ${categoryExamples}).
|
error: `Cannot use subagent_type="${SISYPHUS_JUNIOR_AGENT}" directly. ${exampleHint}
|
||||||
|
|
||||||
Sisyphus-Junior is spawned automatically when you specify a category. Pick the appropriate category for your task domain.`,
|
Sisyphus-Junior is spawned automatically when you specify a category. Pick the appropriate category for your task domain.`,
|
||||||
}
|
}
|
||||||
@@ -73,7 +85,7 @@ Create the work plan directly - that's your job as the planning agent.`,
|
|||||||
const mergedAgents = mergeWithClaudeCodeAgents(agents, executorCtx.directory)
|
const mergedAgents = mergeWithClaudeCodeAgents(agents, executorCtx.directory)
|
||||||
const matchedPrimaryAgent = findPrimaryAgentMatch(mergedAgents, agentToUse)
|
const matchedPrimaryAgent = findPrimaryAgentMatch(mergedAgents, agentToUse)
|
||||||
|
|
||||||
if (matchedPrimaryAgent) {
|
if (matchedPrimaryAgent && !options.allowPrimaryAgentDelegation) {
|
||||||
return {
|
return {
|
||||||
agentToUse: "",
|
agentToUse: "",
|
||||||
categoryModel: undefined,
|
categoryModel: undefined,
|
||||||
@@ -81,7 +93,11 @@ Create the work plan directly - that's your job as the planning agent.`,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchedAgent = findCallableAgentMatch(mergedAgents, agentToUse)
|
const usePrimary = options.allowPrimaryAgentDelegation && matchedPrimaryAgent !== undefined
|
||||||
|
const matchedAgent = usePrimary
|
||||||
|
? matchedPrimaryAgent
|
||||||
|
: findCallableAgentMatch(mergedAgents, agentToUse)
|
||||||
|
|
||||||
if (!matchedAgent) {
|
if (!matchedAgent) {
|
||||||
return {
|
return {
|
||||||
agentToUse: "",
|
agentToUse: "",
|
||||||
@@ -90,7 +106,9 @@ Create the work plan directly - that's your job as the planning agent.`,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
agentToUse = stripAgentListSortPrefix(matchedAgent.name)
|
agentToUse = usePrimary
|
||||||
|
? matchedAgent.name
|
||||||
|
: stripAgentListSortPrefix(matchedAgent.name)
|
||||||
|
|
||||||
const agentConfigKey = getAgentConfigKey(agentToUse)
|
const agentConfigKey = getAgentConfigKey(agentToUse)
|
||||||
const agentOverride = agentOverrides?.[agentConfigKey as keyof typeof agentOverrides]
|
const agentOverride = agentOverrides?.[agentConfigKey as keyof typeof agentOverrides]
|
||||||
|
|||||||
@@ -168,6 +168,69 @@ describe("resolveSubagentExecution", () => {
|
|||||||
expect(result.error).toBe('Cannot delegate to primary agent "Prometheus - Plan Builder" via task. Select that agent directly instead.')
|
expect(result.error).toBe('Cannot delegate to primary agent "Prometheus - Plan Builder" via task. Select that agent directly instead.')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("allows delegating to a primary agent when allowPrimaryAgentDelegation is enabled (team-mode path)", async () => {
|
||||||
|
//#given
|
||||||
|
readProviderModelsCacheMock.mockReturnValue({
|
||||||
|
models: { anthropic: ["claude-opus-4-7"] },
|
||||||
|
connected: ["anthropic"],
|
||||||
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||||
|
})
|
||||||
|
const args = createBaseArgs({ subagent_type: "sisyphus" })
|
||||||
|
const executorCtx = createExecutorContext(async () => ([
|
||||||
|
{ name: "\u200BSisyphus - Ultraworker", mode: "primary", model: "anthropic/claude-opus-4-7" },
|
||||||
|
{ name: "oracle", mode: "subagent" },
|
||||||
|
]))
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep", {
|
||||||
|
allowPrimaryAgentDelegation: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.error).toBeUndefined()
|
||||||
|
expect(result.agentToUse).toBe("\u200BSisyphus - Ultraworker")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("allows delegating to Sisyphus-Junior when allowSisyphusJuniorDirect is enabled (team-mode path)", async () => {
|
||||||
|
//#given
|
||||||
|
readProviderModelsCacheMock.mockReturnValue({
|
||||||
|
models: { anthropic: ["claude-sonnet-4-6"] },
|
||||||
|
connected: ["anthropic"],
|
||||||
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||||
|
})
|
||||||
|
const args = createBaseArgs({ subagent_type: "sisyphus-junior" })
|
||||||
|
const executorCtx = createExecutorContext(async () => ([
|
||||||
|
{ name: "Sisyphus-Junior", mode: "subagent", model: "anthropic/claude-sonnet-4-6" },
|
||||||
|
{ name: "oracle", mode: "subagent" },
|
||||||
|
]))
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep", {
|
||||||
|
allowSisyphusJuniorDirect: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.error).toBeUndefined()
|
||||||
|
expect(result.agentToUse).toBe("Sisyphus-Junior")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("renders a usable fallback hint when categoryExamples is empty for the default Sisyphus-Junior block", async () => {
|
||||||
|
//#given
|
||||||
|
const args = createBaseArgs({ subagent_type: "sisyphus-junior" })
|
||||||
|
const executorCtx = createExecutorContext(async () => ([
|
||||||
|
{ name: "Sisyphus-Junior", mode: "subagent" },
|
||||||
|
]))
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "")
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.agentToUse).toBe("")
|
||||||
|
expect(result.error).toBeDefined()
|
||||||
|
expect(result.error).not.toContain("(e.g., )")
|
||||||
|
expect(result.error).toContain("pick one of: quick, deep, ultrabrain")
|
||||||
|
})
|
||||||
|
|
||||||
test("requires explicit all or subagent mode for task-callable agents", async () => {
|
test("requires explicit all or subagent mode for task-callable agents", async () => {
|
||||||
//#given
|
//#given
|
||||||
const args = createBaseArgs({ subagent_type: "custom-worker" })
|
const args = createBaseArgs({ subagent_type: "custom-worker" })
|
||||||
|
|||||||
Reference in New Issue
Block a user