Files
oh-my-pi/packages/coding-agent/src/thinking.ts
T
can1357 7f866a48a8 feat(coding-agent): added per-turn AUTO_THINKING in coding-agent session
- Added AUTO_THINKING as a configured thinking level in settings, schema, SDK, and session plumbing.
- Implemented per-turn auto reasoning classification with online/local prompts, effort clamping, and skip guards.
- Updated model selectors, ACP options, footer/status UI, and events to render auto and auto->resolved states.
- Added AUTO_THINKING parse/clamp tests and fixed local-module cycle and hashline preview regressions.
2026-05-31 03:32:21 +02:00

160 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { type ResolvedThinkingLevel, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { clampThinkingLevelForModel, Effort, getSupportedEfforts, type Model, THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
/**
* Metadata used to render thinking selector values in the coding-agent UI.
*/
export interface ThinkingLevelMetadata {
value: ThinkingLevel;
label: string;
description: string;
}
const THINKING_LEVEL_METADATA: Record<ThinkingLevel, ThinkingLevelMetadata> = {
[ThinkingLevel.Inherit]: {
value: ThinkingLevel.Inherit,
label: "inherit",
description: "Inherit session default",
},
[ThinkingLevel.Off]: { value: ThinkingLevel.Off, label: "off", description: "No reasoning" },
[ThinkingLevel.Minimal]: {
value: ThinkingLevel.Minimal,
label: "min",
description: "Very brief reasoning (~1k tokens)",
},
[ThinkingLevel.Low]: { value: ThinkingLevel.Low, label: "low", description: "Light reasoning (~2k tokens)" },
[ThinkingLevel.Medium]: {
value: ThinkingLevel.Medium,
label: "medium",
description: "Moderate reasoning (~8k tokens)",
},
[ThinkingLevel.High]: { value: ThinkingLevel.High, label: "high", description: "Deep reasoning (~16k tokens)" },
[ThinkingLevel.XHigh]: {
value: ThinkingLevel.XHigh,
label: "xhigh",
description: "Maximum reasoning (~32k tokens)",
},
};
const THINKING_LEVELS = new Set<string>([ThinkingLevel.Inherit, ThinkingLevel.Off, ...THINKING_EFFORTS]);
const EFFORT_LEVELS = new Set<string>(THINKING_EFFORTS);
/**
* Parses a provider-facing effort value.
*/
export function parseEffort(value: string | null | undefined): Effort | undefined {
return value !== undefined && value !== null && EFFORT_LEVELS.has(value) ? (value as Effort) : undefined;
}
/**
* Parses an agent-local thinking selector.
*/
export function parseThinkingLevel(value: string | null | undefined): ThinkingLevel | undefined {
return value !== undefined && value !== null && THINKING_LEVELS.has(value) ? (value as ThinkingLevel) : undefined;
}
/**
* Returns display metadata for a thinking selector.
*/
export function getThinkingLevelMetadata(level: ThinkingLevel): ThinkingLevelMetadata {
return THINKING_LEVEL_METADATA[level];
}
/**
* Converts an agent-local selector into the effort sent to providers.
*/
export function toReasoningEffort(level: ThinkingLevel | undefined): Effort | undefined {
if (level === undefined || level === ThinkingLevel.Off || level === ThinkingLevel.Inherit) {
return undefined;
}
return level;
}
/**
* Resolves a selector against the current model while preserving explicit "off".
*/
export function resolveThinkingLevelForModel(
model: Model | undefined,
level: ThinkingLevel | undefined,
): ResolvedThinkingLevel | undefined {
if (level === undefined || level === ThinkingLevel.Inherit) {
return undefined;
}
if (level === ThinkingLevel.Off) {
return ThinkingLevel.Off;
}
return clampThinkingLevelForModel(model, level);
}
/**
* Sentinel selector for the coding-agent "auto" thinking mode. Kept entirely
* inside the coding-agent layer: it is never an {@link Effort} or
* {@link ThinkingLevel}, so provider mapping/clamping keeps seeing concrete
* efforts. The session resolves `auto` to a concrete effort each turn.
*/
export const AUTO_THINKING = "auto" as const;
/** A thinking selector as configured by the user — a concrete level or `auto`. */
export type ConfiguredThinkingLevel = ThinkingLevel | typeof AUTO_THINKING;
/** Metadata used to render the `auto` selector value alongside concrete levels. */
export interface ConfiguredThinkingLevelMetadata {
value: ConfiguredThinkingLevel;
label: string;
description: string;
}
const AUTO_THINKING_METADATA: ConfiguredThinkingLevelMetadata = {
value: AUTO_THINKING,
label: "auto",
description: "Auto-detect per prompt (low–xhigh)",
};
/**
* Parses a configured thinking selector, accepting `auto` in addition to every
* value {@link parseThinkingLevel} accepts. {@link parseThinkingLevel} itself
* stays strict so model-suffix parsing (`model:high`) keeps rejecting `auto`.
*/
export function parseConfiguredThinkingLevel(value: string | null | undefined): ConfiguredThinkingLevel | undefined {
if (value === AUTO_THINKING) return AUTO_THINKING;
return parseThinkingLevel(value);
}
/** Returns display metadata for a configured selector, including `auto`. */
export function getConfiguredThinkingLevelMetadata(level: ConfiguredThinkingLevel): ConfiguredThinkingLevelMetadata {
return level === AUTO_THINKING ? AUTO_THINKING_METADATA : getThinkingLevelMetadata(level);
}
/**
* Resolves an auto-classified effort against the active model's supported
* range. Unlike {@link clampThinkingLevelForModel}, `auto` never resolves below
* {@link Effort.Low}: the eligible pool is the model's supported efforts at or
* above Low (falling back to the full supported set only when the model maxes
* out below Low). Within that pool the request snaps to the highest level not
* exceeding it, or the pool minimum when the request is below the pool.
*/
export function clampAutoThinkingEffort(model: Model | undefined, effort: Effort): Effort {
const supported = model ? getSupportedEfforts(model) : THINKING_EFFORTS;
if (supported.length === 0) return effort;
const lowIndex = THINKING_EFFORTS.indexOf(Effort.Low);
const eligible = supported.filter(level => THINKING_EFFORTS.indexOf(level) >= lowIndex);
const pool = eligible.length > 0 ? eligible : supported;
const requestedIndex = THINKING_EFFORTS.indexOf(effort);
let chosen = pool[0];
for (const candidate of pool) {
if (THINKING_EFFORTS.indexOf(candidate) > requestedIndex) break;
chosen = candidate;
}
return chosen;
}
/**
* The provisional concrete level shown while `auto` is configured but before a
* turn has been classified. Prefers the model's `defaultLevel`, otherwise High,
* clamped into the auto range. Returns `undefined` for non-reasoning models.
*/
export function resolveProvisionalAutoLevel(model: Model | undefined): Effort | undefined {
if (!model?.reasoning) return undefined;
return clampAutoThinkingEffort(model, model.thinking?.defaultLevel ?? Effort.High);
}