FN-7794: apply fallback model's own thinking level when swapping in at runtime
Adds fallbackThinkingLevel plumbing so, when Fusion swaps from a primary model to a configured fallback model (executor, validator/reviewer, merger, planning, title-summarizer, heartbeat, and workflow-step lanes), the fallback's own configured thinking level is applied instead of silently reusing the primary lane's level. - Add fallbackThinkingLevel option to AgentRuntimeOptions (agent-runtime.ts), AgentOptions (pi.ts), and ReviewOptions (reviewer.ts) - Add per-lane resolvers: resolveExecutorFallbackThinkingLevel, resolvePlanningFallbackThinkingLevel, resolveValidatorFallbackThinkingLevel, resolveTitleSummarizerFallbackThinkingLevel, resolveMergerFallbackThinkingLevel (agent-session-helpers.ts), each following fallback-provider precedence and falling back to the primary lane/default thinking level when unset - Export new resolvers from packages/engine/src/index.ts - Apply the resolved fallback thinking level in createFnAgent's applyThinkingLevelIfSupported once a session has swapped to the fallback model (pi.ts) - Wire fallbackThinkingLevel through executor session creation (workflow-step, task validator, child-agent, and main executor session paths), merger session creation, and heartbeat session creation - Promote the fallback thinking level alongside the fallback model/provider when the no-visible-key Grok CLI fallback is promoted to primary, so the cleared fallback pair doesn't leave the session on the superseded primary's thinking level - Route workflow-step fallback thinking level by which fallback candidate (validatorFallback vs globalFallback) actually matched - Document fallbackThinkingLevel runtime-swap behavior in docs/settings-reference.md - Add minor changeset for @runfusion/fusion - Add regression tests covering fallback thinking-level resolution and application (agent-session-helpers.test.ts, pi.test.ts) and a shared test helper (executor-test-helpers.ts) Files changed: .changeset/fn-7794-fallback-thinking-level.md | 7 ++ docs/settings-reference.md | 3 + .../src/__tests__/agent-session-helpers.test.ts | 38 ++++++ .../engine/src/__tests__/executor-test-helpers.ts | 23 ++++ packages/engine/src/__tests__/pi.test.ts | 136 +++++++++++++++++++++ packages/engine/src/agent-heartbeat.ts | 3 +- packages/engine/src/agent-runtime.ts | 5 + packages/engine/src/agent-session-helpers.ts | 54 ++++++++ packages/engine/src/executor.ts | 31 ++++- packages/engine/src/index.ts | 5 + packages/engine/src/merger.ts | 7 +- packages/engine/src/pi.ts | 16 ++- packages/engine/src/reviewer.ts | 6 + 13 files changed, 327 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-7794 Fusion-Task-Lineage: c94d621a-ccbd-42b2-9fe6-cb619418ad90 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7794-fallback-thinking-level.md
Normal file
7
.changeset/fn-7794-fallback-thinking-level.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Apply fallback models' own thinking levels when runtime swaps to them.
|
||||
category: feature
|
||||
dev: Adds fallbackThinkingLevel session plumbing, resolver precedence, and Grok CLI fallback remap handling.
|
||||
@@ -65,6 +65,9 @@ Defaults from `DEFAULT_GLOBAL_SETTINGS`; key scope from `GLOBAL_SETTINGS_KEYS`.
|
||||
| `fallbackProvider` | `string` | `undefined` | Fallback provider when the selected/default model hits transient provider failures or model-compatibility/auth-tier rejections. Dashboard chat also offers this fallback for explicit user-selected models, but the engine only swaps for retryable provider/model-selection failures. |
|
||||
| `fallbackModelId` | `string` | `undefined` | Fallback model ID (must pair with `fallbackProvider`). |
|
||||
| `fallbackThinkingLevel` | `ThinkingLevel` | `undefined` | Optional global fallback-lane thinking override for the `fallbackProvider`/`fallbackModelId` pair. Inherits `defaultThinkingLevel` when unset. |
|
||||
|
||||
Fallback thinking-level values are applied at runtime when Fusion swaps from the primary model to the configured fallback model; if unset, the active lane/default thinking level continues to apply.
|
||||
|
||||
| `defaultThinkingLevel` | `"off" \| "minimal" \| "low" \| "medium" \| "high" \| "xhigh"` | `undefined` | Default reasoning effort for AI sessions. `xhigh` requests maximum reasoning effort; Claude CLI adapters map it to `high` for non-Opus models and `max` for Opus models. If a provider/runtime rejects simultaneous `thinking` and `reasoning_effort` parameters, Fusion retries without the explicit thinking override instead of failing the run. |
|
||||
| `ntfyEnabled` | `boolean` | `false` | Enable ntfy push notifications. |
|
||||
| `failureNotificationMode` | `"sticky-only" \| "terminal-only" \| "all"` | `"sticky-only"` | Failure notification behavior. `sticky-only` defers failed-task notifications by `failureNotificationDelayMs` and suppresses transient self-recoveries. `terminal-only` suppresses while auto-retry is still active and only dispatches when `paused === true` or `column === "in-review"` with `status === "failed"`. `all` restores legacy immediate failure notifications. |
|
||||
|
||||
@@ -4,14 +4,19 @@ import {
|
||||
extractRuntimeModel,
|
||||
resolveExecutorSessionModel,
|
||||
resolveExecutorThinkingLevel,
|
||||
resolveExecutorFallbackThinkingLevel,
|
||||
resolveHeartbeatSessionModels,
|
||||
resolveImplicitPlanningFallbackModel,
|
||||
resolveMergerSessionModel,
|
||||
resolveMergerFallbackThinkingLevel,
|
||||
resolvePlanningSessionModel,
|
||||
resolvePlanningThinkingLevel,
|
||||
resolvePlanningFallbackThinkingLevel,
|
||||
resolveTitleSummarizerThinkingLevel,
|
||||
resolveTitleSummarizerFallbackThinkingLevel,
|
||||
resolveValidatorSessionModel,
|
||||
resolveValidatorThinkingLevel,
|
||||
resolveValidatorFallbackThinkingLevel,
|
||||
} from "../agent-session-helpers.js";
|
||||
|
||||
const { resolveRuntimeMock } = vi.hoisted(() => ({
|
||||
@@ -59,6 +64,39 @@ describe("resolve model-lane thinking levels", () => {
|
||||
defaultThinkingLevel: "low",
|
||||
})).toBe("medium");
|
||||
});
|
||||
|
||||
it("resolves fallback thinking through fallback key then executor lane then defaults", () => {
|
||||
expect(resolveExecutorFallbackThinkingLevel("task", { fallbackThinkingLevel: "high", executionThinkingLevel: "low" })).toBe("high");
|
||||
expect(resolveExecutorFallbackThinkingLevel(undefined, { executionThinkingLevel: "minimal", defaultThinkingLevel: "low" })).toBe("minimal");
|
||||
expect(resolveExecutorFallbackThinkingLevel(undefined, { defaultThinkingLevelOverride: "medium", defaultThinkingLevel: "low" })).toBe("medium");
|
||||
expect(resolveExecutorFallbackThinkingLevel(undefined, { defaultThinkingLevel: "low" })).toBe("low");
|
||||
});
|
||||
|
||||
it("resolves workflow fallback thinking before global fallback then lane defaults", () => {
|
||||
expect(resolvePlanningFallbackThinkingLevel({ planningFallbackThinkingLevel: "xhigh", fallbackThinkingLevel: "high", planningThinkingLevel: "low" })).toBe("xhigh");
|
||||
expect(resolvePlanningFallbackThinkingLevel({ fallbackThinkingLevel: "high", planningThinkingLevel: "low" })).toBe("high");
|
||||
expect(resolvePlanningFallbackThinkingLevel({ planningThinkingLevel: "low", defaultThinkingLevel: "minimal" })).toBe("low");
|
||||
expect(resolvePlanningFallbackThinkingLevel({ defaultThinkingLevelOverride: "medium", defaultThinkingLevel: "minimal" })).toBe("medium");
|
||||
expect(resolvePlanningFallbackThinkingLevel({ defaultThinkingLevel: "minimal" })).toBe("minimal");
|
||||
|
||||
expect(resolveValidatorFallbackThinkingLevel(undefined, { validatorFallbackThinkingLevel: "xhigh", fallbackThinkingLevel: "high", validatorThinkingLevel: "low" })).toBe("xhigh");
|
||||
expect(resolveValidatorFallbackThinkingLevel(undefined, { fallbackThinkingLevel: "high", validatorThinkingLevel: "low" })).toBe("high");
|
||||
expect(resolveValidatorFallbackThinkingLevel(undefined, { validatorThinkingLevel: "low", defaultThinkingLevel: "minimal" })).toBe("low");
|
||||
expect(resolveValidatorFallbackThinkingLevel(undefined, { defaultThinkingLevelOverride: "medium", defaultThinkingLevel: "minimal" })).toBe("medium");
|
||||
expect(resolveValidatorFallbackThinkingLevel(undefined, { defaultThinkingLevel: "minimal" })).toBe("minimal");
|
||||
});
|
||||
|
||||
it("resolves title summarizer and merger fallback thinking through fallback and default chains", () => {
|
||||
expect(resolveTitleSummarizerFallbackThinkingLevel({ titleSummarizerFallbackThinkingLevel: "xhigh", fallbackThinkingLevel: "high", titleSummarizerThinkingLevel: "low" })).toBe("xhigh");
|
||||
expect(resolveTitleSummarizerFallbackThinkingLevel({ fallbackThinkingLevel: "high", titleSummarizerThinkingLevel: "low" })).toBe("high");
|
||||
expect(resolveTitleSummarizerFallbackThinkingLevel({ titleSummarizerThinkingLevel: "low", defaultThinkingLevel: "minimal" })).toBe("low");
|
||||
expect(resolveTitleSummarizerFallbackThinkingLevel({ defaultThinkingLevelOverride: "medium", defaultThinkingLevel: "minimal" })).toBe("medium");
|
||||
expect(resolveTitleSummarizerFallbackThinkingLevel({ defaultThinkingLevel: "minimal" })).toBe("minimal");
|
||||
|
||||
expect(resolveMergerFallbackThinkingLevel({ fallbackThinkingLevel: "high", defaultThinkingLevel: "low" })).toBe("high");
|
||||
expect(resolveMergerFallbackThinkingLevel({ defaultThinkingLevelOverride: "medium", defaultThinkingLevel: "low" })).toBe("medium");
|
||||
expect(resolveMergerFallbackThinkingLevel({ defaultThinkingLevel: "low" })).toBe("low");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractRuntimeHint", () => {
|
||||
|
||||
@@ -89,6 +89,29 @@ vi.mock("../agent-session-helpers.js", async () => {
|
||||
?? (typeof settings?.executionGlobalThinkingLevel === "string" ? settings.executionGlobalThinkingLevel : undefined)
|
||||
?? (typeof settings?.defaultThinkingLevelOverride === "string" ? settings.defaultThinkingLevelOverride : undefined)
|
||||
?? (typeof settings?.defaultThinkingLevel === "string" ? settings.defaultThinkingLevel : undefined),
|
||||
/*
|
||||
* FNXC:Settings-ThinkingLevel 2026-07-10-14:20:
|
||||
* FN-7794 added fallback-swap thinking resolvers (resolveExecutorFallbackThinkingLevel / resolveValidatorFallbackThinkingLevel) that executor.ts now calls unconditionally on the main session-creation and workflow-step-review hot paths. This shared harness mocks the whole `agent-session-helpers.js` module, so leaving these unmocked throws "No export is defined on the mock" for every test that reaches those paths (51 files depend on this harness). Mirror production's fallback-key -> lane-key -> default-override -> default precedence.
|
||||
*/
|
||||
resolveExecutorFallbackThinkingLevel: (taskThinkingLevel: string | undefined, settings: Record<string, unknown> | undefined) =>
|
||||
(typeof settings?.fallbackThinkingLevel === "string" ? settings.fallbackThinkingLevel : undefined)
|
||||
?? taskThinkingLevel
|
||||
?? (typeof settings?.executionThinkingLevel === "string" ? settings.executionThinkingLevel : undefined)
|
||||
?? (typeof settings?.executionGlobalThinkingLevel === "string" ? settings.executionGlobalThinkingLevel : undefined)
|
||||
?? (typeof settings?.defaultThinkingLevelOverride === "string" ? settings.defaultThinkingLevelOverride : undefined)
|
||||
?? (typeof settings?.defaultThinkingLevel === "string" ? settings.defaultThinkingLevel : undefined),
|
||||
resolveValidatorThinkingLevel: (taskThinkingLevel: string | undefined, settings: Record<string, unknown> | undefined) =>
|
||||
(typeof settings?.validatorThinkingLevel === "string" ? settings.validatorThinkingLevel : undefined)
|
||||
?? taskThinkingLevel
|
||||
?? (typeof settings?.defaultThinkingLevelOverride === "string" ? settings.defaultThinkingLevelOverride : undefined)
|
||||
?? (typeof settings?.defaultThinkingLevel === "string" ? settings.defaultThinkingLevel : undefined),
|
||||
resolveValidatorFallbackThinkingLevel: (taskThinkingLevel: string | undefined, settings: Record<string, unknown> | undefined) =>
|
||||
(typeof settings?.validatorFallbackThinkingLevel === "string" ? settings.validatorFallbackThinkingLevel : undefined)
|
||||
?? (typeof settings?.fallbackThinkingLevel === "string" ? settings.fallbackThinkingLevel : undefined)
|
||||
?? (typeof settings?.validatorThinkingLevel === "string" ? settings.validatorThinkingLevel : undefined)
|
||||
?? taskThinkingLevel
|
||||
?? (typeof settings?.defaultThinkingLevelOverride === "string" ? settings.defaultThinkingLevelOverride : undefined)
|
||||
?? (typeof settings?.defaultThinkingLevel === "string" ? settings.defaultThinkingLevel : undefined),
|
||||
resolveExecutorSessionModel: (
|
||||
taskModelProvider: string | undefined,
|
||||
taskModelId: string | undefined,
|
||||
|
||||
@@ -724,6 +724,142 @@ describe("session failure diagnostics", () => {
|
||||
expect(sessionWithThinking.setThinkingLevel).toHaveBeenCalledWith("xhigh");
|
||||
});
|
||||
|
||||
it("applies fallback thinking level after prompt-time fallback swap", async () => {
|
||||
const createAgentSessionMock = vi.mocked(createAgentSession);
|
||||
|
||||
const primarySetThinkingLevel = vi.fn();
|
||||
const fallbackSetThinkingLevel = vi.fn();
|
||||
const primarySession = {
|
||||
model: { provider: "test", id: "primary-model" },
|
||||
prompt: vi.fn().mockRejectedValue(new Error("429 Too Many Requests")),
|
||||
subscribe: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
setThinkingLevel: primarySetThinkingLevel,
|
||||
sessionFile: undefined,
|
||||
} as unknown as AgentSession;
|
||||
|
||||
const fallbackSession = {
|
||||
model: { provider: "test", id: "fallback-model" },
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
subscribe: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
setThinkingLevel: fallbackSetThinkingLevel,
|
||||
sessionFile: undefined,
|
||||
} as unknown as AgentSession;
|
||||
|
||||
createAgentSessionMock.mockReset();
|
||||
createAgentSessionMock
|
||||
.mockResolvedValueOnce({ session: primarySession } as any)
|
||||
.mockResolvedValueOnce({ session: fallbackSession } as any);
|
||||
|
||||
const { session } = await createFnAgent({
|
||||
cwd: "/test/project",
|
||||
systemPrompt: "Test fallback thinking",
|
||||
defaultProvider: "test",
|
||||
defaultModelId: "primary-model",
|
||||
fallbackProvider: "test",
|
||||
fallbackModelId: "fallback-model",
|
||||
defaultThinkingLevel: "low",
|
||||
fallbackThinkingLevel: "high",
|
||||
});
|
||||
|
||||
await expect((session as any).promptWithFallback("Run task")).resolves.toBeUndefined();
|
||||
|
||||
expect(primarySetThinkingLevel).toHaveBeenCalledWith("low");
|
||||
expect(fallbackSetThinkingLevel).toHaveBeenCalledWith("high");
|
||||
expect(fallbackSetThinkingLevel).not.toHaveBeenCalledWith("low");
|
||||
});
|
||||
|
||||
it("uses default thinking level for fallback swap when fallback thinking is unset", async () => {
|
||||
const createAgentSessionMock = vi.mocked(createAgentSession);
|
||||
|
||||
const primarySetThinkingLevel = vi.fn();
|
||||
const fallbackSetThinkingLevel = vi.fn();
|
||||
const primarySession = {
|
||||
model: { provider: "test", id: "primary-model" },
|
||||
prompt: vi.fn().mockRejectedValue(new Error("429 Too Many Requests")),
|
||||
subscribe: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
setThinkingLevel: primarySetThinkingLevel,
|
||||
sessionFile: undefined,
|
||||
} as unknown as AgentSession;
|
||||
|
||||
const fallbackSession = {
|
||||
model: { provider: "test", id: "fallback-model" },
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
subscribe: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
setThinkingLevel: fallbackSetThinkingLevel,
|
||||
sessionFile: undefined,
|
||||
} as unknown as AgentSession;
|
||||
|
||||
createAgentSessionMock.mockReset();
|
||||
createAgentSessionMock
|
||||
.mockResolvedValueOnce({ session: primarySession } as any)
|
||||
.mockResolvedValueOnce({ session: fallbackSession } as any);
|
||||
|
||||
const { session } = await createFnAgent({
|
||||
cwd: "/test/project",
|
||||
systemPrompt: "Test fallback default thinking",
|
||||
defaultProvider: "test",
|
||||
defaultModelId: "primary-model",
|
||||
fallbackProvider: "test",
|
||||
fallbackModelId: "fallback-model",
|
||||
defaultThinkingLevel: "low",
|
||||
});
|
||||
|
||||
await expect((session as any).promptWithFallback("Run task")).resolves.toBeUndefined();
|
||||
|
||||
expect(fallbackSetThinkingLevel).toHaveBeenCalledWith("low");
|
||||
});
|
||||
|
||||
it("disables thinking when fallback session rejects thinking/reasoning compatibility", async () => {
|
||||
const createAgentSessionMock = vi.mocked(createAgentSession);
|
||||
|
||||
const primarySetThinkingLevel = vi.fn();
|
||||
const fallbackSetThinkingLevel = vi.fn(() => {
|
||||
throw new Error("400 cannot specify both 'thinking' and 'reasoning_effort'");
|
||||
});
|
||||
const primarySession = {
|
||||
model: { provider: "test", id: "primary-model" },
|
||||
prompt: vi.fn().mockRejectedValue(new Error("429 Too Many Requests")),
|
||||
subscribe: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
setThinkingLevel: primarySetThinkingLevel,
|
||||
sessionFile: undefined,
|
||||
} as unknown as AgentSession;
|
||||
|
||||
const fallbackSession = {
|
||||
model: { provider: "test", id: "fallback-model" },
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
subscribe: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
setThinkingLevel: fallbackSetThinkingLevel,
|
||||
sessionFile: undefined,
|
||||
} as unknown as AgentSession;
|
||||
|
||||
createAgentSessionMock.mockReset();
|
||||
createAgentSessionMock
|
||||
.mockResolvedValueOnce({ session: primarySession } as any)
|
||||
.mockResolvedValueOnce({ session: fallbackSession } as any);
|
||||
|
||||
const { session } = await createFnAgent({
|
||||
cwd: "/test/project",
|
||||
systemPrompt: "Test fallback thinking conflict",
|
||||
defaultProvider: "test",
|
||||
defaultModelId: "primary-model",
|
||||
fallbackProvider: "test",
|
||||
fallbackModelId: "fallback-model",
|
||||
defaultThinkingLevel: "low",
|
||||
fallbackThinkingLevel: "high",
|
||||
});
|
||||
|
||||
await expect((session as any).promptWithFallback("Run task")).resolves.toBeUndefined();
|
||||
|
||||
expect(fallbackSetThinkingLevel).toHaveBeenCalledTimes(1);
|
||||
expect(fallbackSetThinkingLevel).toHaveBeenCalledWith("high");
|
||||
});
|
||||
|
||||
it("forwards materialized MCP servers into session creation and prompt options for supported providers", async () => {
|
||||
const createAgentSessionMock = vi.mocked(createAgentSession);
|
||||
const session = {
|
||||
|
||||
@@ -55,7 +55,7 @@ import { acquireTaskWorktree } from "./worktree-acquisition.js";
|
||||
import { createRunAuditor, generateSyntheticRunId, type DatabaseMutationType, type EngineRunContext } from "./run-audit.js";
|
||||
import { promptWithFallback } from "./pi.js";
|
||||
import { buildAgentGatedActionSummary } from "./permanent-agent-gating.js";
|
||||
import { createResolvedAgentSession, extractRuntimeHint, resolveHeartbeatSessionModels } from "./agent-session-helpers.js";
|
||||
import { createResolvedAgentSession, extractRuntimeHint, resolveHeartbeatSessionModels, resolveExecutorFallbackThinkingLevel } from "./agent-session-helpers.js";
|
||||
import { resolveMcpServersForStore } from "./mcp-resolution.js";
|
||||
import type { AgentActionGateContext } from "./agent-action-gate.js";
|
||||
import { buildSessionSkillContextSync } from "./session-skill-context.js";
|
||||
@@ -2814,6 +2814,7 @@ export class HeartbeatMonitor {
|
||||
defaultModelId: heartbeatSessionModels.defaultModelId,
|
||||
fallbackProvider: heartbeatSessionModels.fallbackProvider,
|
||||
fallbackModelId: heartbeatSessionModels.fallbackModelId,
|
||||
fallbackThinkingLevel: resolveExecutorFallbackThinkingLevel(undefined, heartbeatModelSettings),
|
||||
runAuditor: audit,
|
||||
settings: heartbeatModelSettings,
|
||||
mcpServers: heartbeatMcp.servers,
|
||||
|
||||
@@ -98,6 +98,11 @@ export interface AgentRuntimeOptions {
|
||||
fallbackProvider?: string;
|
||||
/** Optional fallback model ID */
|
||||
fallbackModelId?: string;
|
||||
/**
|
||||
* FNXC:Settings-ThinkingLevel 2026-07-10-00:00:
|
||||
* Runtime session options carry the fallback model's own thinking level so a fallback swap can apply it, or fall back to `defaultThinkingLevel` when unset.
|
||||
*/
|
||||
fallbackThinkingLevel?: string;
|
||||
/** Default thinking effort level (e.g. "medium", "high") */
|
||||
defaultThinkingLevel?: string;
|
||||
/** Optional pre-configured SessionManager for persistence */
|
||||
|
||||
@@ -207,6 +207,54 @@ export function resolveMergerThinkingLevel(settings: Partial<Settings> | undefin
|
||||
return firstThinkingLevel(settings?.defaultThinkingLevelOverride, settings?.defaultThinkingLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* FNXC:Settings-ThinkingLevel 2026-07-10-00:00:
|
||||
* Fallback thinking resolvers mirror fallback provider/model precedence: lane-specific fallback thinking wins where a lane can select a lane fallback model, then global fallback thinking, then the primary lane/default thinking chain for compatibility when no fallback-specific value is configured.
|
||||
*/
|
||||
export function resolveExecutorFallbackThinkingLevel(
|
||||
taskThinkingLevel: ThinkingLevel | string | undefined,
|
||||
settings: Partial<Settings> | undefined,
|
||||
): string | undefined {
|
||||
return firstThinkingLevel(
|
||||
settings?.fallbackThinkingLevel,
|
||||
resolveExecutorThinkingLevel(taskThinkingLevel, settings),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolvePlanningFallbackThinkingLevel(
|
||||
settings: Partial<Settings> | undefined,
|
||||
taskThinkingLevel?: ThinkingLevel | string,
|
||||
): string | undefined {
|
||||
return firstThinkingLevel(
|
||||
settings?.planningFallbackThinkingLevel,
|
||||
settings?.fallbackThinkingLevel,
|
||||
resolvePlanningThinkingLevel(settings, taskThinkingLevel),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveValidatorFallbackThinkingLevel(
|
||||
taskThinkingLevel: ThinkingLevel | string | undefined,
|
||||
settings: Partial<Settings> | undefined,
|
||||
): string | undefined {
|
||||
return firstThinkingLevel(
|
||||
settings?.validatorFallbackThinkingLevel,
|
||||
settings?.fallbackThinkingLevel,
|
||||
resolveValidatorThinkingLevel(taskThinkingLevel, settings),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveTitleSummarizerFallbackThinkingLevel(settings: Partial<Settings> | undefined): string | undefined {
|
||||
return firstThinkingLevel(
|
||||
settings?.titleSummarizerFallbackThinkingLevel,
|
||||
settings?.fallbackThinkingLevel,
|
||||
resolveTitleSummarizerThinkingLevel(settings),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveMergerFallbackThinkingLevel(settings: Partial<Settings> | undefined): string | undefined {
|
||||
return firstThinkingLevel(settings?.fallbackThinkingLevel, resolveMergerThinkingLevel(settings));
|
||||
}
|
||||
|
||||
function hasCompleteRuntimeModel(
|
||||
model: ResolvedModelSelection,
|
||||
): model is { provider: string; modelId: string } {
|
||||
@@ -259,8 +307,14 @@ function applyGrokCliNoKeyRuntimeOptions(
|
||||
...runtimeOptions,
|
||||
defaultProvider: runtimeOptions.fallbackProvider,
|
||||
defaultModelId: stripGrokCliModelProviderPrefix(runtimeOptions.fallbackModelId),
|
||||
/*
|
||||
* FNXC:Settings-ThinkingLevel 2026-07-10-00:00:
|
||||
* When the no-visible-key Grok CLI fallback is promoted to the primary runtime, promote its fallback thinking level too; the cleared fallback pair must not leave the Grok CLI session using the superseded primary model's thinking level.
|
||||
*/
|
||||
defaultThinkingLevel: runtimeOptions.fallbackThinkingLevel ?? runtimeOptions.defaultThinkingLevel,
|
||||
fallbackProvider: undefined,
|
||||
fallbackModelId: undefined,
|
||||
fallbackThinkingLevel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,9 @@ import {
|
||||
extractRuntimeHint,
|
||||
resolveExecutorSessionModel,
|
||||
resolveExecutorThinkingLevel,
|
||||
resolveExecutorFallbackThinkingLevel,
|
||||
resolveValidatorThinkingLevel,
|
||||
resolveValidatorFallbackThinkingLevel,
|
||||
} from "./agent-session-helpers.js";
|
||||
import { buildSessionSkillContext } from "./session-skill-context.js";
|
||||
import type { SkillSelectionContext } from "./skill-resolver.js";
|
||||
@@ -6719,6 +6721,12 @@ export class TaskExecutor {
|
||||
: detail.thinkingLevel,
|
||||
settings,
|
||||
),
|
||||
fallbackThinkingLevel: resolveValidatorFallbackThinkingLevel(
|
||||
typeof config.thinkingLevel === "string" && WORKFLOW_THINKING_LEVEL_SET.has(config.thinkingLevel)
|
||||
? (config.thinkingLevel as ThinkingLevel)
|
||||
: detail.thinkingLevel,
|
||||
settings,
|
||||
),
|
||||
taskValidatorProvider: detail.validatorModelProvider,
|
||||
taskValidatorModelId: detail.validatorModelId,
|
||||
projectValidatorProvider: settings.validatorProvider,
|
||||
@@ -10606,7 +10614,9 @@ export class TaskExecutor {
|
||||
);
|
||||
const executorFallbackProvider = settings.fallbackProvider;
|
||||
const executorFallbackModelId = settings.fallbackModelId;
|
||||
const executorThinkingLevel = resolveExecutorThinkingLevel(this.graphSeamThinkingLevel.get(task.id) ?? detail.thinkingLevel, settings);
|
||||
const executorSessionThinkingSource = this.graphSeamThinkingLevel.get(task.id) ?? detail.thinkingLevel;
|
||||
const executorThinkingLevel = resolveExecutorThinkingLevel(executorSessionThinkingSource, settings);
|
||||
const executorFallbackThinkingLevel = resolveExecutorFallbackThinkingLevel(executorSessionThinkingSource, settings);
|
||||
|
||||
// U1 telemetry: now that the session model/provider/node are resolved,
|
||||
// give the agent logger the context it needs to emit usage_events tool
|
||||
@@ -10705,6 +10715,7 @@ export class TaskExecutor {
|
||||
defaultModelId: executorModelId,
|
||||
fallbackProvider: executorFallbackProvider,
|
||||
fallbackModelId: executorFallbackModelId,
|
||||
fallbackThinkingLevel: executorFallbackThinkingLevel,
|
||||
defaultThinkingLevel: executorThinkingLevel,
|
||||
runAuditor: audit,
|
||||
settings,
|
||||
@@ -11132,6 +11143,7 @@ export class TaskExecutor {
|
||||
defaultModelId: executorModelId,
|
||||
fallbackProvider: executorFallbackProvider,
|
||||
fallbackModelId: executorFallbackModelId,
|
||||
fallbackThinkingLevel: executorFallbackThinkingLevel,
|
||||
defaultThinkingLevel: executorThinkingLevel,
|
||||
runAuditor: audit,
|
||||
settings,
|
||||
@@ -13636,6 +13648,7 @@ export class TaskExecutor {
|
||||
defaultModelId: settings.defaultModelId,
|
||||
fallbackProvider: settings.fallbackProvider,
|
||||
fallbackModelId: settings.fallbackModelId,
|
||||
fallbackThinkingLevel: resolveValidatorFallbackThinkingLevel(latestDetailForReview.thinkingLevel, settings),
|
||||
defaultThinkingLevel: resolveValidatorThinkingLevel(latestDetailForReview.thinkingLevel, settings),
|
||||
// Task-level validator override (from task)
|
||||
taskValidatorProvider: latestDetailForReview.validatorModelProvider,
|
||||
@@ -14997,7 +15010,8 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
|
||||
const useOverride = !!(workflowStep.modelProvider && workflowStep.modelId);
|
||||
|
||||
type ModelTuple = { provider?: string; modelId?: string };
|
||||
const fallbackCandidates: Array<ModelTuple & { label: string }> = [
|
||||
type WorkflowStepFallbackLabel = "validatorFallback" | "globalFallback";
|
||||
const fallbackCandidates: Array<ModelTuple & { label: WorkflowStepFallbackLabel }> = [
|
||||
{ provider: settings.validatorFallbackProvider, modelId: settings.validatorFallbackModelId, label: "validatorFallback" },
|
||||
{ provider: settings.fallbackProvider, modelId: settings.fallbackModelId, label: "globalFallback" },
|
||||
];
|
||||
@@ -15144,8 +15158,17 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
|
||||
/*
|
||||
* FNXC:Settings-ThinkingLevel 2026-07-10-00:00:
|
||||
* WorkflowStep sessions resolve reasoning effort as node/step `thinkingLevel` first, then task override, then settings defaults/lane fallbacks.
|
||||
*
|
||||
* FNXC:Settings-ThinkingLevel 2026-07-10-14:20:
|
||||
* The step's own `fallback` attempt already swaps to a distinct model (validator fallback OR global fallback pair) — it must honor THAT model's fallback thinking level, not silently reuse the primary lane's thinking level. Route by which candidate `fallback.label` actually matched instead of only special-casing `validatorFallback`.
|
||||
*/
|
||||
const workflowStepThinkingLevel = resolveExecutorThinkingLevel(workflowStep.thinkingLevel ?? task.thinkingLevel, settings);
|
||||
const workflowStepThinkingSource = workflowStep.thinkingLevel ?? task.thinkingLevel;
|
||||
const workflowStepThinkingLevel = attemptLabel === "fallback"
|
||||
? (fallback?.label === "validatorFallback"
|
||||
? resolveValidatorFallbackThinkingLevel(workflowStepThinkingSource, settings)
|
||||
: resolveExecutorFallbackThinkingLevel(workflowStepThinkingSource, settings))
|
||||
: resolveExecutorThinkingLevel(workflowStepThinkingSource, settings);
|
||||
const workflowStepFallbackThinkingLevel = resolveExecutorFallbackThinkingLevel(workflowStepThinkingSource, settings);
|
||||
const { session } = await createResolvedAgentSession({
|
||||
sessionPurpose: "executor",
|
||||
runtimeHint: workflowRuntimeHint,
|
||||
@@ -15157,6 +15180,7 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
|
||||
defaultModelId: modelId,
|
||||
fallbackProvider: settings.fallbackProvider,
|
||||
fallbackModelId: settings.fallbackModelId,
|
||||
fallbackThinkingLevel: workflowStepFallbackThinkingLevel,
|
||||
defaultThinkingLevel: workflowStepThinkingLevel,
|
||||
runAuditor: createRunAuditor(this.store, this.getRunContextFor(task.id)),
|
||||
settings,
|
||||
@@ -18033,6 +18057,7 @@ Child agent: ${agent.id} (${name})`;
|
||||
defaultModelId: childExecutorModelId,
|
||||
fallbackProvider: settings.fallbackProvider,
|
||||
fallbackModelId: settings.fallbackModelId,
|
||||
fallbackThinkingLevel: resolveExecutorFallbackThinkingLevel(undefined, settings),
|
||||
runAuditor: createRunAuditor(this.store, this.getRunContextFor(taskId)),
|
||||
settings,
|
||||
taskEnv,
|
||||
|
||||
@@ -835,6 +835,11 @@ export {
|
||||
promptWithAutoRetry,
|
||||
describeAgentModel,
|
||||
resolveExecutorThinkingLevel,
|
||||
resolveExecutorFallbackThinkingLevel,
|
||||
resolvePlanningFallbackThinkingLevel,
|
||||
resolveValidatorFallbackThinkingLevel,
|
||||
resolveTitleSummarizerFallbackThinkingLevel,
|
||||
resolveMergerFallbackThinkingLevel,
|
||||
extractRuntimeHint,
|
||||
extractRuntimeModel,
|
||||
type ResolvedSessionOptions,
|
||||
|
||||
@@ -109,7 +109,7 @@ import { evaluateAutoMergeFactProviders } from "./auto-merge-fact-providers.js";
|
||||
import { resolveMergePolicy, type MergeFileScopeMode } from "./merge-trait.js";
|
||||
import { describeModel, promptWithFallback } from "./pi.js";
|
||||
import { accumulateSessionTokenUsage } from "./session-token-usage.js";
|
||||
import { createResolvedAgentSession, extractRuntimeHint, resolveMergerSessionModel, resolveMergerThinkingLevel } from "./agent-session-helpers.js";
|
||||
import { createResolvedAgentSession, extractRuntimeHint, resolveMergerSessionModel, resolveMergerThinkingLevel, resolveMergerFallbackThinkingLevel } from "./agent-session-helpers.js";
|
||||
import { createFallbackModelObserver } from "./fallback-model-observer.js";
|
||||
import { buildSessionSkillContext } from "./session-skill-context.js";
|
||||
import { resolveMcpServersForStore } from "./mcp-resolution.js";
|
||||
@@ -1969,6 +1969,7 @@ Do not refactor, rename broadly, or make opportunistic improvements.
|
||||
defaultModelId: mergerSessionModel.modelId,
|
||||
fallbackProvider: settings.fallbackProvider,
|
||||
fallbackModelId: settings.fallbackModelId,
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings),
|
||||
runAuditor: createRunAuditor(store, {
|
||||
runId: mergeRunContext?.runId ?? generateSyntheticRunId("merge", taskId),
|
||||
@@ -3163,6 +3164,7 @@ ${fileList}
|
||||
defaultModelId: mergerSessionModel.modelId,
|
||||
fallbackProvider: settings.fallbackProvider,
|
||||
fallbackModelId: settings.fallbackModelId,
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings),
|
||||
runAuditor: createRunAuditor(store, {
|
||||
runId: generateSyntheticRunId("merge", taskId),
|
||||
@@ -3580,6 +3582,7 @@ ${fileList}
|
||||
defaultModelId: mergerSessionModel.modelId,
|
||||
fallbackProvider: settings.fallbackProvider,
|
||||
fallbackModelId: settings.fallbackModelId,
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings),
|
||||
runAuditor: createRunAuditor(store, {
|
||||
runId: generateSyntheticRunId("merge", taskId),
|
||||
@@ -7084,6 +7087,7 @@ You are assisting with a paused \`git pull --rebase\`.
|
||||
defaultModelId: mergerSessionModel.modelId,
|
||||
fallbackProvider: settings.fallbackProvider,
|
||||
fallbackModelId: settings.fallbackModelId,
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings),
|
||||
runAuditor: createRunAuditor(store, {
|
||||
runId: generateSyntheticRunId("merge", taskId),
|
||||
@@ -12039,6 +12043,7 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo
|
||||
defaultModelId: mergerSessionModel.modelId,
|
||||
fallbackProvider: settings.fallbackProvider,
|
||||
fallbackModelId: settings.fallbackModelId,
|
||||
fallbackThinkingLevel: resolveMergerFallbackThinkingLevel(settings),
|
||||
defaultThinkingLevel: resolveMergerThinkingLevel(settings),
|
||||
runAuditor: createRunAuditor(store, {
|
||||
runId: generateSyntheticRunId("merge", taskId),
|
||||
|
||||
@@ -1033,6 +1033,11 @@ export interface AgentOptions {
|
||||
fallbackProvider?: string;
|
||||
/** Optional fallback model ID used with `fallbackProvider`. */
|
||||
fallbackModelId?: string;
|
||||
/**
|
||||
* FNXC:Settings-ThinkingLevel 2026-07-10-00:00:
|
||||
* Fallback model swaps must honor the fallback model's configured thinking level while preserving the lane/default thinking level when no fallback-specific value is set.
|
||||
*/
|
||||
fallbackThinkingLevel?: string;
|
||||
/** Default thinking effort level (e.g. "medium", "high"). When provided, sets the session's thinking level after creation. */
|
||||
defaultThinkingLevel?: string;
|
||||
/** Optional pre-configured SessionManager. When provided, the agent session
|
||||
@@ -2525,11 +2530,18 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
|
||||
|
||||
let thinkingCompatibilityDisabled = false;
|
||||
const applyThinkingLevelIfSupported = (targetSession: AgentSession, sourceModel: string): void => {
|
||||
if (!options.defaultThinkingLevel || thinkingCompatibilityDisabled) {
|
||||
/*
|
||||
* FNXC:Settings-ThinkingLevel 2026-07-10-00:00:
|
||||
* Fallback-swap sessions apply the fallback model's configured thinking level, or transparently keep the lane/default level when no fallback-specific value exists. The compatibility-disable guard remains shared so thinking/reasoning conflicts disable explicit thinking for both primary and fallback paths.
|
||||
*/
|
||||
const effectiveThinkingLevel = usingFallback
|
||||
? options.fallbackThinkingLevel ?? options.defaultThinkingLevel
|
||||
: options.defaultThinkingLevel;
|
||||
if (!effectiveThinkingLevel || thinkingCompatibilityDisabled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
(targetSession as PromptableSession).setThinkingLevel(options.defaultThinkingLevel as any);
|
||||
(targetSession as PromptableSession).setThinkingLevel(effectiveThinkingLevel as any);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
if (!isThinkingReasoningConflictError(message)) {
|
||||
|
||||
@@ -78,6 +78,11 @@ export interface ReviewOptions {
|
||||
projectValidatorFallbackModelId?: string;
|
||||
/** Default thinking effort level for the reviewer agent session. */
|
||||
defaultThinkingLevel?: string;
|
||||
/**
|
||||
* FNXC:Settings-ThinkingLevel 2026-07-10-00:00:
|
||||
* Validator/reviewer sessions accept a fallback-specific thinking level so retryable fallback swaps preserve the selected fallback model's reasoning effort.
|
||||
*/
|
||||
fallbackThinkingLevel?: string;
|
||||
/** Task store for persisting agent log entries. When provided with `taskId`, enables full conversation logging. */
|
||||
store?: TaskStore;
|
||||
/** Task ID for agent log persistence. Required alongside `store`. */
|
||||
@@ -415,6 +420,7 @@ export async function reviewStep(
|
||||
defaultModelId: overrides?.forceModelId ?? validatorModelId,
|
||||
fallbackProvider: validatorFallbackProvider,
|
||||
fallbackModelId: validatorFallbackModelId,
|
||||
fallbackThinkingLevel: options.fallbackThinkingLevel,
|
||||
defaultThinkingLevel: options.defaultThinkingLevel,
|
||||
runAuditor,
|
||||
settings: effectiveSettings,
|
||||
|
||||
Reference in New Issue
Block a user