Files
oh-my-opencode/src/features/team-mode/team-runtime/resolve-member.ts
T
YeonGyu-Kim d8f89b6965 fix(team-mode): preserve per-category model for kind:"category" members
The shared resolveCategoryExecution path treats `agents.sisyphus-junior.model`
(plugin config) as a higher-precedence override than the category default. That
ranking is correct for plain `task(category=...)` delegations - it lets users
pin sisyphus-junior to their preferred general model - but in team-mode it
collapses every kind:"category" member onto the same model.

Hyperplan was the visible victim: skeptic/validator/researcher/architect/creative
are routed through sisyphus-junior with five distinct categories
(unspecified-low / unspecified-high / deep / ultrabrain / artistry). With any
sisyphus-junior model configured, all five resolved to that single override
model instead of their category defaults, defeating the multi-model adversarial
debate.

Strip the override at the team-mode boundary in resolveMember rather than
changing resolveCategoryExecution itself, so:
- delegate-task callers keep the existing override semantics (3 regression
  tests at tools.test.ts:2832, 2958, 3020 stay green)
- per-category user overrides (`categories[X].model`) and explicit fallback
  chains continue to apply
- only the `kind:"category"` team-mode path opts out of the global override

Lock the contract with a regression test in resolve-member.test.ts.

Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode).
2026-05-06 14:20:50 +09:00

131 lines
3.9 KiB
TypeScript

import type { FallbackEntry } from "../../../shared/model-requirements"
import type { DelegatedModelConfig } from "../../../shared/model-resolution-types"
import type { ExecutorContext } from "../../../tools/delegate-task/executor-types"
import type { DelegateTaskArgs } from "../../../tools/delegate-task/types"
import type { Member } from "../types"
import {
buildSystemContent,
resolveCategoryExecution,
resolveSubagentExecution,
} from "./resolve-member-dependencies"
export class TeamMemberResolutionError extends Error {
constructor(public readonly memberName: string, public readonly cause: Error) {
super(`Failed to resolve member '${memberName}': ${cause.message}`)
this.name = "TeamMemberResolutionError"
}
}
export interface ResolvedMember {
memberName: string
agentToUse: string
model: DelegatedModelConfig | undefined
fallbackChain: FallbackEntry[] | undefined
systemContent: string
}
function createBaseDelegateTaskArgs(prompt: string): Pick<DelegateTaskArgs, "description" | "load_skills" | "prompt" | "run_in_background"> {
return {
description: "Resolve team member",
load_skills: [],
prompt,
run_in_background: false,
}
}
function normalizeResolutionError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error))
}
function resolveSystemContent(input: {
agentToUse: string
categoryPromptAppend?: string
maxPromptTokens?: number
model: DelegatedModelConfig | undefined
}): string {
return buildSystemContent({
agentName: input.agentToUse,
categoryPromptAppend: input.categoryPromptAppend,
maxPromptTokens: input.maxPromptTokens,
model: input.model,
}) ?? ""
}
// Strip global `agents.sisyphus-junior.model` override at the team-mode boundary —
// `resolveCategoryExecution` ranks it above category defaults (correct for plain
// `task(category=…)`, wrong here) and would collapse every team member to the same model.
function withoutSisyphusJuniorOverride(ctx: ExecutorContext): ExecutorContext {
if (ctx.sisyphusJuniorModel === undefined) return ctx
return { ...ctx, sisyphusJuniorModel: undefined }
}
export async function resolveMember(
member: Member,
ctx: ExecutorContext,
categoryExamples: string,
parentAgent?: string,
): Promise<ResolvedMember> {
try {
if (member.kind === "category") {
const execution = await resolveCategoryExecution(
{
...createBaseDelegateTaskArgs(member.prompt),
category: member.category,
subagent_type: "sisyphus-junior",
},
withoutSisyphusJuniorOverride(ctx),
undefined,
undefined,
)
if (execution.error) {
throw new Error(execution.error)
}
return {
memberName: member.name,
agentToUse: execution.agentToUse,
model: execution.categoryModel,
fallbackChain: execution.fallbackChain,
systemContent: resolveSystemContent({
agentToUse: execution.agentToUse,
categoryPromptAppend: execution.categoryPromptAppend,
maxPromptTokens: execution.maxPromptTokens,
model: execution.categoryModel,
}),
}
}
const execution = await resolveSubagentExecution(
{
...createBaseDelegateTaskArgs(member.prompt ?? ""),
subagent_type: member.subagent_type,
},
ctx,
parentAgent,
categoryExamples,
{
allowSisyphusJuniorDirect: true,
allowPrimaryAgentDelegation: true,
},
)
if (execution.error) {
throw new Error(execution.error)
}
return {
memberName: member.name,
agentToUse: execution.agentToUse,
model: execution.categoryModel,
fallbackChain: execution.fallbackChain,
systemContent: resolveSystemContent({
agentToUse: execution.agentToUse,
model: execution.categoryModel,
}),
}
} catch (error) {
throw new TeamMemberResolutionError(member.name, normalizeResolutionError(error))
}
}