/** * resolve.ts — The reasoning level a child runs at. * * The model is chosen first; the level follows it, because what a model can do * is a property of the model. * * The order of precedence is the request, then the definition, then the * automatic choice. The automatic choice **steps one rung down from the * effort the parent was running at**: the highest level the child's model * supports that is strictly below the parent's, skipping levels the model does * not have. With no parent effort to step down from, it prefers **medium**, * then **low**, then the lowest supported non-off level. It never turns * reasoning off on a model that has it. * * Two rules make the fallback honest: * * - A model that does not reason is not a failure. The level is discarded and * reported as *not applicable*; the launch goes ahead. * - When the parent's model is kept and parent effort is known, take the next * supported non-off rung below it, or the model's floor if none is lower. * Effort already at the floor stands; a parent below the supported floor is * brought to that floor and the limitation is reported. * * The supported levels come from Pi's own report about the model, and an * unsupported request is clamped the way Pi clamps it — never by a numerical * midpoint, and never silently down to "off" on a reasoning model. */ import { type Model } from "@earendil-works/pi-ai"; import { type SubagentThinkingLevel } from "#src/config/thinking-level"; import { EFFORT, levelBelow, stepDown } from "#src/thinking/levels"; import { supportedLevels as adapterSupportedLevels, clampLevel as adapterClampLevel, } from "#src/agents/sdk-adapter"; /** The levels, in ascending order of effort. */ const EFFORT_ORDER: ReadonlyArray = EFFORT; /** Where the level came from. */ export type ThinkingSource = | "request" | "definition" | "automatic" | "parent" | "none"; /** The level the child runs at, and what was decided. */ export type ThinkingDecision = Readonly<{ /** * The level the child runs at. * * Undefined when the model does not reason: the setting is not applicable * rather than wrong. */ level?: SubagentThinkingLevel; /** Whether the model reasons at all. */ applicable: boolean; /** Where the level came from. */ source: ThinkingSource; /** Whether a requested level had to be clamped to what the model supports. */ clamped: boolean; /** A sentence for the launch note. */ reason: string; }>; /** What the resolution needs to know. */ export type ThinkingRequest = Readonly<{ /** The level the request names, when it names one. */ requested?: SubagentThinkingLevel; /** The level the definition names, when it names one. */ defined?: SubagentThinkingLevel; /** The model the child runs on. */ model?: Model; /** The effort the parent was running at, when the parent has one. */ parentLevel?: SubagentThinkingLevel; /** * Whether the parent's model was kept because no lower target was * available. * * Absent means the model changed — the default is a child that works on its * own model, and the retained-parent rule does not apply. */ parentModelKept?: boolean; }>; /** * Resolve the reasoning level for a launch. * * Never fails: a level the model cannot express is clamped, and a model that * cannot reason at all simply has no level. */ export function resolveThinking(request: ThinkingRequest): ThinkingDecision { const { model } = request; if (!model) { // No model was resolved, so nothing is known about what it can do. The // level the caller named passes through untouched, and the session's own // resolution has the last word. const level = request.requested ?? request.defined; if (level === undefined) { return { applicable: false, source: "none", clamped: false, reason: "no model was resolved, so there is no reasoning level to set", }; } return { level, applicable: true, source: request.requested !== undefined ? "request" : "definition", clamped: false, reason: request.requested !== undefined ? `the request asked for ${level}` : `the agent definition asked for ${level}`, }; } const supported = adapterSupportedLevels( model, ) as ReadonlyArray; const nonOff = supported.filter((level) => level !== "off"); if (nonOff.length === 0) { return { applicable: false, source: "none", clamped: false, reason: `${model.provider}/${model.id} does not do extended reasoning, so the thinking level does not apply`, }; } if (request.requested !== undefined) { return explicit(request.requested, "request", supported, model); } if (request.defined !== undefined) { return explicit(request.defined, "definition", supported, model); } if (request.parentModelKept) { return retainedParent(request.parentLevel, supported); } // The child has its own model, so it steps down from the parent's effort // rather than taking a fixed default. "One rung" is expressed in the child's // own array: gapped ladders skip the levels the model does not have. if (request.parentLevel !== undefined) { return belowParent(request.parentLevel, supported, model); } const level = automaticLevel(nonOff); if (!level) { return { applicable: true, source: "automatic", clamped: false, reason: `${model.provider}/${model.id} supports no reasoning level below off`, }; } return { level, applicable: true, source: "automatic", clamped: false, reason: `the default reasoning level for ${model.provider}/${model.id} is ${level}`, }; } /** * A level the request or the definition named. * * A level the model does not support is clamped the way Pi clamps it, and the * clamp is reported: the caller asked for one thing and got another. */ function explicit( requested: SubagentThinkingLevel, source: "request" | "definition", supported: ReadonlyArray, model: Model, ): ThinkingDecision { // A model that reasons does not reason less because the caller asked it to: // an explicit "off" is refused on such a model, and the lowest level it does // support takes its place. A model that does not reason never reaches here — // the level does not apply to it at all. if (requested === "off") { const lowest = supported.find((level) => level !== "off"); if (lowest) { return { level: lowest, applicable: true, source, clamped: true, reason: `${model.provider}/${model.id} does extended reasoning, so "off" was not applied; the lowest level it supports is ${lowest}`, }; } } if (supported.includes(requested)) { return { level: requested, applicable: true, source, clamped: false, reason: source === "request" ? `the request asked for ${requested}` : `the agent definition asked for ${requested}`, }; } const clamped = adapterClampLevel(requested, model) as SubagentThinkingLevel; return { level: clamped, applicable: true, source, clamped: true, reason: source === "request" ? `${requested} is not a level ${model.provider}/${model.id} supports, so the request was clamped to ${clamped}` : `${requested} is not a level ${model.provider}/${model.id} supports, so the definition's level was clamped to ${clamped}`, }; } /** * The automatic level: medium, then low, then the lowest supported non-off * level. */ function automaticLevel( nonOff: ReadonlyArray, ): SubagentThinkingLevel | undefined { if (nonOff.includes("medium")) return "medium"; if (nonOff.includes("low")) return "low"; // The levels arrive in ascending order, so the first non-off is the lowest. return nonOff[0]; } /** * The level for a child that runs on its own model: one rung below the parent. * * The rung is the highest level *this model* supports below the parent's, so a * model that reports `["low", "high", "max"]` steps from `max` to `high` and * from `high` to `low`, never to a level it cannot run and never to `off`. * * Where there is no rung below, the model's own floor is used. That floor can * be the parent's own effort (nothing lower exists, so nothing changes) or, on * a model that cannot go as low as the parent, above it — an unavoidable * property of the model, said plainly in the launch note rather than hidden. */ function belowParent( parentLevel: SubagentThinkingLevel, supported: ReadonlyArray, model: Model, ): ThinkingDecision { const below = levelBelow(supported, parentLevel); if (below !== undefined) { return { level: below, applicable: true, source: "parent", clamped: false, reason: `the parent was running at ${parentLevel}, so the child runs one rung down at ${below}`, }; } const floor = stepDown(supported, parentLevel); if (floor !== undefined && floor === parentLevel) { return { level: floor, applicable: true, source: "parent", clamped: false, reason: `${model.provider}/${model.id} supports no level below ${parentLevel}, so the parent's effort stands`, }; } return { ...(floor ? { level: floor } : {}), applicable: true, source: "parent", clamped: false, reason: floor ? `the lowest level ${model.provider}/${model.id} supports is ${floor}, above the parent's ${parentLevel}; the model has no lower rung` : `${model.provider}/${model.id} supports no reasoning level below off`, }; } /** * The level for a child that keeps the parent's model. * * The next supported non-off rung below the parent's effort is used. Gaps are * skipped; where no lower rung exists the model's non-off floor is used. */ function retainedParent( parentLevel: SubagentThinkingLevel | undefined, supported: ReadonlyArray, ): ThinkingDecision { const nonOff = supported.filter((level) => level !== "off"); if (parentLevel === undefined) { const level = automaticLevel(nonOff); return { ...(level ? { level } : {}), applicable: true, source: "automatic", clamped: false, reason: level ? `the parent's model was kept, and its default reasoning level is ${level}` : "the parent's model was kept, and it supports no reasoning level below off", }; } const parentEffort = EFFORT_ORDER.indexOf(parentLevel); const nextLower = nonOff .filter((level) => EFFORT_ORDER.indexOf(level) < parentEffort) .at(-1); if (nextLower) { return { level: nextLower, applicable: true, source: "parent", clamped: false, reason: `the parent's model was kept, and ${nextLower} is one supported rung below the effort the parent was running at (${parentLevel})`, }; } const floor = nonOff[0]; return { ...(floor ? { level: floor } : {}), applicable: true, source: "parent", clamped: false, reason: floor === parentLevel ? `the parent's model was kept, and it supports no level below the effort the parent was running at (${parentLevel}), so that effort stands` : `the parent's model was kept; its lowest supported non-off level is ${floor}, so the parent's ${parentLevel} effort cannot be lowered on this model`, }; }