fix(tool): flatten resolveChildModel adapter, remove unsafe cast, add compatibility shim and comments

This commit is contained in:
pi
2026-04-12 11:32:28 +01:00
parent 7691915e38
commit f0cd7ef27a
4 changed files with 179 additions and 24 deletions

View File

@@ -43,6 +43,9 @@ function loadPresetDir(dir: string, source: "global" | "project"): SubagentPrese
const filePath = join(dir, entry.name);
let content: string;
try {
// Fail-open: if a preset file is unreadable for any reason, ignore it
// rather than failing the entire discovery process. This keeps preset
// discovery robust in the presence of partially-broken user files.
content = readFileSync(filePath, "utf8");
} catch (_err) {
continue;
@@ -85,8 +88,11 @@ export function discoverSubagentPresets(cwd: string, options: { homeDir?: string
}
if (projectPresetsDir) {
// Project presets override global presets by name. This is intentional
// to let local projects stage or refine presets without modifying the
// user's global agent presets. There is no confirmation gate for a
// project override; the nearest project takes precedence.
for (const preset of loadPresetDir(projectPresetsDir, "project")) {
// project overrides global by name
map.set(preset.name, preset);
}
}

View File

@@ -60,12 +60,15 @@ export function createSubagentTool(deps: {
listAvailableModelReferences?: typeof listAvailableModelReferences;
normalizeAvailableModelReference?: typeof normalizeAvailableModelReference;
parameters?: typeof SubagentParamsSchema;
// Compatibility: accept injected resolveChildModel functions with either the
// new API ({ callModel?, presetModel? }) or the older test/hooks API
// ({ taskModel?, topLevelModel? }). We adapt at callsite below.
// Compatibility: injected resolveChildModel may be the new API
// ({ callModel?, presetModel? }) or the older test/hooks API
// ({ taskModel?, topLevelModel? }). The local adapter below exposes a
// flattened, simple boundary that takes only { callModel?, presetModel? }
// and adapts to either injected shape internally by providing both key
// names when calling the injected resolver.
resolveChildModel?:
| typeof resolveChildModel
| ((input: { taskModel?: string; topLevelModel?: string }) => ModelSelection);
| ((input: { callModel?: string; presetModel?: string; taskModel?: string; topLevelModel?: string }) => ModelSelection)
| typeof resolveChildModel;
runSingleTask?: (input: {
cwd: string;
meta: Record<string, unknown>;
@@ -136,21 +139,22 @@ export function createSubagentTool(deps: {
step.model = normalizedStepModel;
}
const callResolveChildModel = (input: {
callModel?: string;
presetModel?: string;
taskModel?: string;
topLevelModel?: string;
}) => {
// If an injected resolveChildModel exists, call it with the older-shape
// keys (taskModel/topLevelModel) for compatibility. Otherwise, use the
// internal resolveChildModel which expects { callModel, presetModel }.
// Adapter: accept only the flattened shape { callModel?, presetModel? }
// to keep the tool logic simple. If a resolver was injected with the
// older test/hooks shape, we call it with both key names so it can read
// the old keys. This is a minimal, internal compatibility shim.
const callResolveChildModel = (input: { callModel?: string; presetModel?: string }) => {
if (deps.resolveChildModel) {
const injected = deps.resolveChildModel as unknown as (arg: { taskModel?: string; topLevelModel?: string }) => unknown;
return injected({ taskModel: input.callModel ?? input.taskModel, topLevelModel: input.presetModel ?? input.topLevelModel }) as ModelSelection;
// Provide both naming variants so old and new resolvers both work.
return deps.resolveChildModel({
callModel: input.callModel,
presetModel: input.presetModel,
taskModel: input.callModel,
topLevelModel: input.presetModel,
});
}
return resolveChildModel({ callModel: input.callModel ?? input.taskModel, presetModel: input.presetModel ?? input.topLevelModel });
return resolveChildModel({ callModel: input.callModel, presetModel: input.presetModel });
};
const runTask = async (input: {
@@ -161,12 +165,7 @@ export function createSubagentTool(deps: {
step?: number;
mode: "single" | "parallel" | "chain";
}) => {
const model = callResolveChildModel({
callModel: input.taskModel,
presetModel: params.model,
taskModel: input.taskModel,
topLevelModel: params.model,
});
const model = callResolveChildModel({ callModel: input.taskModel, presetModel: params.model });
const progressFormatter = createProgressFormatter();