Files
openclaw/src/auto-reply/thinking.ts
Vito Cappello 1b9d3ac57d fix(claude-cli): apply thinking and keep live sessions warm (#125528)
* fix(models): preserve CLI runtime thinking capabilities

* fix(models): preserve configured thinking overrides

* fix: keep Claude live CLI process warm across captured turns

MCP delivery capture no longer kills the warm Claude process after every
turn. Capture-key admission is fenced by grant activate/deactivate so
prompt-cache continuity can survive across messages.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(claude-cli): apply thinking levels

* fix(claude-cli): materialize thinking capabilities

* test(claude-cli): cover warm thinking budget reuse

* test(models): restore prepared catalog contracts

* fix(agents): restore catalog test boundaries

* fix(agents): break prepared catalog import cycle

* refactor(gateway): extract model-choice runtime resolution into public-projection module

Keeps models-list-result.ts under the max-lines cap after the origin/main
merge by moving resolveModelChoiceAgentRuntime next to the projection
helpers it feeds.

Claude-Session: https://claude.ai/code/session_01QXUQuDVataA5o16kxNnmoX

* test(claude-cli): cover thinking cache reuse

* test(claude-cli): cover captured live reuse

* fix(claude-cli): rotate MCP grants across live turns

* fix(anthropic): respect mandatory adaptive thinking

* fix(claude-cli): restore warm MCP bearer

* fix(anthropic): keep Mythos adaptive thinking

* test(claude-cli): prove live MCP cache reuse

* test(claude-cli): align live cache coverage

* fix(claude-cli): reuse live sessions across MCP grant rotation

* test(claude-cli): satisfy cache lane static gates

* fix(claude-cli): preserve runtime thinking policy

* fix(thinking): honor concrete runtime policy

* fix(gateway): honor mandatory thinking in model list

* refactor(auto-reply): extract prepared catalog merge

* docs(cli-backend): document thinking execution input

* refactor(auto-reply): extract catalog lookup helper

* style(auto-reply): format catalog helper import

* fix(claude-cli): stabilize live context budget

* fix(auto-reply): type prepared context metadata

---------

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
2026-08-20 06:44:49 -07:00

401 lines
13 KiB
TypeScript

// Thinking/reasoning level catalog helpers for auto-reply model controls.
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "@openclaw/normalization-core/string-coerce";
import { resolveClaudeThinkingProfile } from "../plugins/provider-claude-thinking.js";
import { resolveEffectiveThinkingProfile } from "../plugins/provider-thinking.js";
import type { ProviderThinkingProfile } from "../plugins/provider-thinking.types.js";
import {
BASE_THINKING_LEVELS,
normalizeThinkLevel,
resolveThinkingDefaultForModelCore,
THINKING_LEVEL_RANKS,
} from "./thinking.shared.js";
import type { ThinkLevel, ThinkingCatalogEntry } from "./thinking.shared.js";
export {
isSessionDefaultDirectiveValue,
normalizeElevatedLevel,
normalizeFastMode,
normalizeReasoningLevel,
normalizeTraceLevel,
normalizeThinkLevel,
normalizeUsageDisplay,
normalizeVerboseLevel,
resolveEffectiveResponseUsage,
resolveResponseUsageMode,
} from "./thinking.shared.js";
export type {
ElevatedLevel,
FastMode,
ReasoningLevel,
TraceLevel,
ThinkLevel,
ThinkingCatalogEntry,
VerboseLevel,
} from "./thinking.shared.js";
/** UI-facing thinking level option. */
type ThinkingLevelOption = {
id: ThinkLevel;
label: string;
};
type RankedThinkingLevelOption = ThinkingLevelOption & {
rank: number;
};
type ResolvedThinkingProfile = {
levels: RankedThinkingLevelOption[];
defaultLevel?: ThinkLevel | null;
};
function buildCatalogModelKey(provider: string, model: string): string {
const providerId = provider.trim();
const modelId = model.trim();
if (!providerId) {
return modelId;
}
if (!modelId) {
return providerId;
}
return normalizeOptionalLowercaseString(modelId)?.startsWith(
`${normalizeOptionalLowercaseString(providerId)}/`,
)
? modelId
: `${providerId}/${modelId}`;
}
function resolveThinkingCatalogEntry(params: {
provider?: string | null;
model?: string | null;
catalog?: ThinkingCatalogEntry[];
}): ThinkingCatalogEntry | undefined {
const providerRaw = normalizeOptionalString(params.provider);
const normalizedProvider = providerRaw ? normalizeProviderId(providerRaw) : "";
const modelId = normalizeOptionalString(params.model) ?? "";
const selectedCatalogKey =
normalizedProvider && modelId ? buildCatalogModelKey(normalizedProvider, modelId) : undefined;
const selected = params.catalog?.find(
(entry) =>
selectedCatalogKey !== undefined &&
buildCatalogModelKey(normalizeProviderId(entry.provider), entry.id) === selectedCatalogKey,
);
return selected;
}
function resolveThinkingPolicyContext(params: {
provider?: string | null;
model?: string | null;
catalog?: ThinkingCatalogEntry[];
agentRuntime?: string | null;
configuredReasoning?: boolean;
}) {
const providerRaw = normalizeOptionalString(params.provider);
const modelId = normalizeOptionalString(params.model) ?? "";
const modelKey = normalizeOptionalLowercaseString(params.model) ?? "";
const candidate = resolveThinkingCatalogEntry(params);
const thinkingPolicyProvider = normalizeOptionalString(candidate?.thinkingPolicyProvider);
// Prepared catalogs keep the logical model identity but record the concrete
// runtime policy owner so every session and directive surface stays aligned.
const normalizedProvider = providerRaw
? normalizeProviderId(thinkingPolicyProvider ?? providerRaw)
: "";
return {
normalizedProvider,
modelId,
modelKey,
api: candidate?.api,
reasoning: params.configuredReasoning ?? candidate?.configuredReasoning ?? candidate?.reasoning,
...(candidate?.params ? { params: candidate.params } : {}),
compat: candidate?.compat,
};
}
function normalizeProfileLevel(
level: ProviderThinkingProfile["levels"][number],
): RankedThinkingLevelOption | undefined {
const normalized = normalizeThinkLevel(level.id);
if (!normalized) {
return undefined;
}
return {
id: normalized,
label: normalizeOptionalString(level.label) ?? normalized,
rank: Number.isFinite(level.rank) ? (level.rank as number) : THINKING_LEVEL_RANKS[normalized],
};
}
function normalizeThinkingProfile(profile: ProviderThinkingProfile): ResolvedThinkingProfile {
const byId = new Map<ThinkLevel, RankedThinkingLevelOption>();
for (const raw of profile.levels) {
const level = normalizeProfileLevel(raw);
if (level) {
byId.set(level.id, level);
}
}
const levels = [...byId.values()].toSorted((a, b) => a.rank - b.rank);
const rawDefaultLevel = profile.defaultLevel
? normalizeThinkLevel(profile.defaultLevel)
: undefined;
const defaultLevel = rawDefaultLevel && byId.has(rawDefaultLevel) ? rawDefaultLevel : undefined;
return { levels, defaultLevel };
}
function buildBaseThinkingProfile(defaultLevel?: ThinkLevel | null): ResolvedThinkingProfile {
return {
levels: BASE_THINKING_LEVELS.map((id) => ({
id,
label: id,
rank: THINKING_LEVEL_RANKS[id],
})),
defaultLevel,
};
}
function buildOffOnlyThinkingProfile(): ResolvedThinkingProfile {
return {
levels: [{ id: "off", label: "off", rank: THINKING_LEVEL_RANKS.off }],
defaultLevel: "off",
};
}
function appendProfileLevel(profile: ResolvedThinkingProfile, id: ThinkLevel) {
if (profile.levels.some((level) => level.id === id)) {
return;
}
profile.levels.push({ id, label: id, rank: THINKING_LEVEL_RANKS[id] });
profile.levels = profile.levels.toSorted((a, b) => a.rank - b.rank);
}
const CATALOG_ADVANCED_THINKING_LEVELS = new Set<ThinkLevel>(["adaptive", "xhigh", "max"]);
function appendCatalogAdvancedThinkingLevels(
profile: ResolvedThinkingProfile,
compat: ThinkingCatalogEntry["compat"],
agentRuntime?: string | null,
) {
const efforts = compat?.supportedReasoningEfforts;
if (!Array.isArray(efforts)) {
return;
}
let supportsMax = false;
for (const effort of efforts) {
const level = normalizeThinkLevel(effort);
if (level && CATALOG_ADVANCED_THINKING_LEVELS.has(level)) {
appendProfileLevel(profile, level);
supportsMax ||= level === "max";
}
}
const runtime = normalizeOptionalLowercaseString(agentRuntime);
if (supportsMax && (runtime === "openclaw" || runtime === "auto")) {
// Ultra is OpenClaw's orchestration tier; provider requests use Max.
appendProfileLevel(profile, "ultra");
}
}
/** Resolve supported thinking levels and default for a provider/model pair. */
export function resolveThinkingProfile(params: {
provider?: string | null;
model?: string | null;
catalog?: ThinkingCatalogEntry[];
agentRuntime?: string | null;
configuredReasoning?: boolean;
providerPolicySource?: "active" | "active-or-bundled";
}): ResolvedThinkingProfile {
const context = resolveThinkingPolicyContext(params);
if (!context.normalizedProvider) {
return buildBaseThinkingProfile();
}
const providerContext = {
provider: context.normalizedProvider,
modelId: context.modelId,
agentRuntime: params.agentRuntime,
api: context.api,
reasoning: context.reasoning,
...(context.params ? { params: context.params } : {}),
compat: context.compat,
};
const providerProfileParams = {
provider: context.normalizedProvider,
context: providerContext,
};
const providerProfile =
params.providerPolicySource === "active"
? resolveEffectiveThinkingProfile(providerProfileParams, {
allowPublicArtifactFallback: false,
})
: resolveEffectiveThinkingProfile(providerProfileParams);
// Any anthropic-messages catalog row routes through the canonical Claude
// resolver: Claude families get the proper profile (incl. xhigh/adaptive/max);
// non-Claude models on the anthropic-messages transport collapse to the Claude
// base set, deliberately bypassing the later compat-driven xhigh upgrade —
// anthropic-messages does not carry a generic xhigh contract.
const anthropicMessagesProfile =
context.api === "anthropic-messages"
? resolveClaudeThinkingProfile(context.modelId, context.params, {
includeNativeMax: true,
})
: undefined;
const pluginProfile = providerProfile ?? anthropicMessagesProfile;
if (pluginProfile) {
const normalized = normalizeThinkingProfile(pluginProfile);
if (
normalized.levels.length > 0 &&
(context.reasoning !== false || pluginProfile.preserveWhenCatalogReasoningFalse === true)
) {
return normalized;
}
}
if (context.reasoning === false) {
return buildOffOnlyThinkingProfile();
}
const profile = buildBaseThinkingProfile();
appendCatalogAdvancedThinkingLevels(profile, context.compat, params.agentRuntime);
return profile;
}
function supportsThinkingLevel(
provider: string | null | undefined,
model: string | null | undefined,
level: ThinkLevel,
catalog?: ThinkingCatalogEntry[],
agentRuntime?: string | null,
configuredReasoning?: boolean,
): boolean {
return resolveThinkingProfile({
provider,
model,
catalog,
agentRuntime,
configuredReasoning,
}).levels.some((entry) => entry.id === level);
}
/** List thinking level ids supported by provider/model. */
export function listThinkingLevels(
provider?: string | null,
model?: string | null,
catalog?: ThinkingCatalogEntry[],
agentRuntime?: string | null,
): ThinkLevel[] {
const profile = resolveThinkingProfile({ provider, model, catalog, agentRuntime });
return profile.levels.map((level) => level.id);
}
/** List labeled thinking level options supported by provider/model. */
export function listThinkingLevelOptions(
provider?: string | null,
model?: string | null,
catalog?: ThinkingCatalogEntry[],
agentRuntime?: string | null,
): ThinkingLevelOption[] {
const profile = resolveThinkingProfile({ provider, model, catalog, agentRuntime });
return profile.levels.map(({ id, label }) => ({ id, label }));
}
/** List display labels for thinking levels supported by provider/model. */
export function listThinkingLevelLabels(
provider?: string | null,
model?: string | null,
catalog?: ThinkingCatalogEntry[],
agentRuntime?: string | null,
): string[] {
return listThinkingLevelOptions(provider, model, catalog, agentRuntime).map(
(level) => level.label,
);
}
/** Format supported thinking level labels for command/status output. */
export function formatThinkingLevels(
provider?: string | null,
model?: string | null,
separator = ", ",
catalog?: ThinkingCatalogEntry[],
agentRuntime?: string | null,
): string {
const profile = resolveThinkingProfile({ provider, model, catalog, agentRuntime });
return profile.levels.map(({ label }) => label).join(separator);
}
/** Resolve the default thinking level for a provider/model pair. */
export function resolveThinkingDefaultForModel(params: {
provider: string;
model: string;
catalog?: ThinkingCatalogEntry[];
agentRuntime?: string | null;
}): ThinkLevel {
const profile = resolveThinkingProfile({
provider: params.provider,
model: params.model,
catalog: params.catalog,
agentRuntime: params.agentRuntime,
});
if (profile.defaultLevel) {
return profile.defaultLevel;
}
const fallback = resolveThinkingDefaultForModelCore(params);
if (fallback === "off") {
return "off";
}
return resolveSupportedThinkingLevelFromProfile(profile, "medium");
}
/** Return whether a specific thinking level is supported by provider/model. */
export function isThinkingLevelSupported(params: {
provider?: string | null;
model?: string | null;
level: ThinkLevel;
catalog?: ThinkingCatalogEntry[];
agentRuntime?: string | null;
configuredReasoning?: boolean;
}): boolean {
return supportsThinkingLevel(
params.provider,
params.model,
params.level,
params.catalog,
params.agentRuntime,
params.configuredReasoning,
);
}
function resolveSupportedThinkingLevelFromProfile(
profile: ResolvedThinkingProfile,
level: ThinkLevel,
): ThinkLevel {
if (profile.levels.some((entry) => entry.id === level)) {
return level;
}
const requestedRank = THINKING_LEVEL_RANKS[level];
const ranked = profile.levels.toSorted((a, b) => b.rank - a.rank);
return (
ranked.find((entry) => entry.id !== "off" && entry.rank <= requestedRank)?.id ??
ranked.findLast((entry) => entry.id !== "off")?.id ??
"off"
);
}
/** Clamp a requested thinking level to the closest supported provider/model level. */
export function resolveSupportedThinkingLevel(params: {
provider?: string | null;
model?: string | null;
level: ThinkLevel;
catalog?: ThinkingCatalogEntry[];
agentRuntime?: string | null;
configuredReasoning?: boolean;
providerPolicySource?: "active" | "active-or-bundled";
}): ThinkLevel {
const profile = resolveThinkingProfile({
provider: params.provider,
model: params.model,
catalog: params.catalog,
agentRuntime: params.agentRuntime,
configuredReasoning: params.configuredReasoning,
providerPolicySource: params.providerPolicySource,
});
return resolveSupportedThinkingLevelFromProfile(profile, params.level);
}