fix(engine): route workflow reviews through validator models (#2533)

## Summary

- classify review-type workflow steps with the existing review-step
classifier
- resolve their primary, fallback, and thinking-level settings from the
validator model lane
- retain per-step model overrides and executor-purpose workflow-step
tooling
- keep ordinary workflow steps on the execution lane
- make missing-fallback diagnostics identify the correct lane

## Why

Code Review, Plan Review, verification, and inline-review gates were
executed through the implementation model lane merely because they run
inside `executeWorkflowStep()`. That defeats configured reviewer-model
separation and can make the same model implement and validate its own
work.

This changes model selection—not the workflow-step session/tooling
contract—so review steps remain executor-purpose sessions while using
validator lane models.

## Verification

- `FUSION_PG_TEST_SKIP=1 corepack pnpm@10.33.0 --filter @fusion/engine
exec vitest run src/__tests__/executor-workflow-step-model.test.ts` — 14
passed
- `corepack pnpm@10.33.0 --filter @fusion/engine typecheck`
- `corepack pnpm@10.33.0 changeset status --since=origin/main`
- `git diff --check origin/main...HEAD`


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Review-type workflow steps now route through the configured validator
model lane (instead of the execution lane).
* Validator primary/fallback and thinking-level settings are applied
correctly for review steps.
  * Step/task overrides still take priority over lane-based resolution.
* Fallback retry sessions now use the appropriate validator/executor
configuration, with lane-specific fallback guidance when fallback
settings are missing.
* **Tests**
* Expanded executor workflow-step model resolution and routing/fallback
precedence assertions for validator-lane behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Phil Larson
2026-07-29 00:05:04 -07:00
committed by GitHub
parent 9a8fc409ff
commit 72391c90b2
4 changed files with 262 additions and 34 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Route review-type workflow steps through validator model lanes instead of the execution model.
category: fix
dev: Review classifiers now select validator primary, fallback, and thinking-level settings while preserving executor runtime semantics and explicit step/task overrides.

View File

@@ -124,8 +124,10 @@ function withSessionDefaults(session: any, options?: { systemPrompt?: unknown })
} }
vi.mock("../agent-session-helpers.js", async () => { vi.mock("../agent-session-helpers.js", async () => {
const actual = await vi.importActual<typeof import("../agent-session-helpers.js")>("../agent-session-helpers.js");
const { createFnAgent } = await import("../pi.js"); const { createFnAgent } = await import("../pi.js");
return { return {
...actual,
createResolvedAgentSession: async (options: any) => { createResolvedAgentSession: async (options: any) => {
const result = await createFnAgent(options); const result = await createFnAgent(options);
return { return {
@@ -157,15 +159,26 @@ vi.mock("../agent-session-helpers.js", async () => {
?? (typeof settings?.defaultThinkingLevelOverride === "string" ? settings.defaultThinkingLevelOverride : undefined) ?? (typeof settings?.defaultThinkingLevelOverride === "string" ? settings.defaultThinkingLevelOverride : undefined)
?? (typeof settings?.defaultThinkingLevel === "string" ? settings.defaultThinkingLevel : undefined), ?? (typeof settings?.defaultThinkingLevel === "string" ? settings.defaultThinkingLevel : undefined),
resolveValidatorThinkingLevel: (taskThinkingLevel: string | undefined, settings: Record<string, unknown> | undefined) => resolveValidatorThinkingLevel: (taskThinkingLevel: string | undefined, settings: Record<string, unknown> | undefined) =>
(typeof settings?.validatorThinkingLevel === "string" ? settings.validatorThinkingLevel : undefined) taskThinkingLevel
?? taskThinkingLevel ?? (typeof settings?.validatorThinkingLevel === "string" ? settings.validatorThinkingLevel : undefined)
?? (typeof settings?.validatorGlobalThinkingLevel === "string" ? settings.validatorGlobalThinkingLevel : undefined)
?? (typeof (settings?.selectedWorkflowModelLanes as Record<string, unknown> | undefined)?.validatorThinkingLevel === "string"
? (settings?.selectedWorkflowModelLanes as Record<string, unknown>).validatorThinkingLevel as string
: undefined)
?? (typeof settings?.defaultThinkingLevelOverride === "string" ? settings.defaultThinkingLevelOverride : undefined) ?? (typeof settings?.defaultThinkingLevelOverride === "string" ? settings.defaultThinkingLevelOverride : undefined)
?? (typeof settings?.defaultThinkingLevel === "string" ? settings.defaultThinkingLevel : undefined), ?? (typeof settings?.defaultThinkingLevel === "string" ? settings.defaultThinkingLevel : undefined),
resolveValidatorFallbackThinkingLevel: (taskThinkingLevel: string | undefined, settings: Record<string, unknown> | undefined) => resolveValidatorFallbackThinkingLevel: (taskThinkingLevel: string | undefined, settings: Record<string, unknown> | undefined) =>
(typeof settings?.validatorFallbackThinkingLevel === "string" ? settings.validatorFallbackThinkingLevel : undefined) (typeof settings?.validatorFallbackThinkingLevel === "string" ? settings.validatorFallbackThinkingLevel : undefined)
?? (typeof settings?.fallbackThinkingLevel === "string" ? settings.fallbackThinkingLevel : undefined) ?? (typeof settings?.fallbackThinkingLevel === "string" ? settings.fallbackThinkingLevel : undefined)
?? (typeof settings?.validatorThinkingLevel === "string" ? settings.validatorThinkingLevel : undefined) ?? (typeof (settings?.selectedWorkflowModelLanes as Record<string, unknown> | undefined)?.validatorFallbackThinkingLevel === "string"
? (settings?.selectedWorkflowModelLanes as Record<string, unknown>).validatorFallbackThinkingLevel as string
: undefined)
?? taskThinkingLevel ?? taskThinkingLevel
?? (typeof settings?.validatorThinkingLevel === "string" ? settings.validatorThinkingLevel : undefined)
?? (typeof settings?.validatorGlobalThinkingLevel === "string" ? settings.validatorGlobalThinkingLevel : undefined)
?? (typeof (settings?.selectedWorkflowModelLanes as Record<string, unknown> | undefined)?.validatorThinkingLevel === "string"
? (settings?.selectedWorkflowModelLanes as Record<string, unknown>).validatorThinkingLevel as string
: undefined)
?? (typeof settings?.defaultThinkingLevelOverride === "string" ? settings.defaultThinkingLevelOverride : undefined) ?? (typeof settings?.defaultThinkingLevelOverride === "string" ? settings.defaultThinkingLevelOverride : undefined)
?? (typeof settings?.defaultThinkingLevel === "string" ? settings.defaultThinkingLevel : undefined), ?? (typeof settings?.defaultThinkingLevel === "string" ? settings.defaultThinkingLevel : undefined),
resolveExecutorSessionModel: ( resolveExecutorSessionModel: (
@@ -197,6 +210,7 @@ vi.mock("../agent-session-helpers.js", async () => {
} }
return { provider: undefined, modelId: undefined }; return { provider: undefined, modelId: undefined };
}, },
}; };
}); });
vi.mock("../worktree-names.js", async () => { vi.mock("../worktree-names.js", async () => {

View File

@@ -9,18 +9,26 @@ import {
} from "./executor-test-helpers.js"; } from "./executor-test-helpers.js";
type CapturedSession = { type CapturedSession = {
sessionPurpose?: string;
defaultProvider?: string; defaultProvider?: string;
defaultModelId?: string; defaultModelId?: string;
fallbackProvider?: string;
fallbackModelId?: string;
defaultThinkingLevel?: string; defaultThinkingLevel?: string;
fallbackThinkingLevel?: string;
}; };
function captureSession(output = '{"verdict":"APPROVE","notes":""}'): { last?: CapturedSession } { function captureSession(output = '{"verdict":"APPROVE","notes":""}'): { last?: CapturedSession } {
const holder: { last?: CapturedSession } = {}; const holder: { last?: CapturedSession } = {};
mockedCreateFnAgent.mockImplementation(async (opts: any) => { mockedCreateFnAgent.mockImplementation(async (opts: any) => {
holder.last = { holder.last = {
sessionPurpose: opts.sessionPurpose,
defaultProvider: opts.defaultProvider, defaultProvider: opts.defaultProvider,
defaultModelId: opts.defaultModelId, defaultModelId: opts.defaultModelId,
fallbackProvider: opts.fallbackProvider,
fallbackModelId: opts.fallbackModelId,
defaultThinkingLevel: opts.defaultThinkingLevel, defaultThinkingLevel: opts.defaultThinkingLevel,
fallbackThinkingLevel: opts.fallbackThinkingLevel,
}; };
const listeners: Array<(event: any) => void> = []; const listeners: Array<(event: any) => void> = [];
@@ -129,13 +137,22 @@ describe("executor workflow-step model resolution", () => {
const captured = await runStepWithSettings({ const captured = await runStepWithSettings({
executionProvider: "openai", executionProvider: "openai",
executionModelId: "gpt-4o", executionModelId: "gpt-4o",
executionFallbackProvider: "openai",
executionFallbackModelId: "gpt-4o-mini",
executionThinkingLevel: "medium",
fallbackThinkingLevel: "low",
defaultProvider: "anthropic", defaultProvider: "anthropic",
defaultModelId: "claude-3-5-sonnet", defaultModelId: "claude-3-5-sonnet",
}); });
expect(captured).toMatchObject({ expect(captured).toMatchObject({
sessionPurpose: "executor",
defaultProvider: "openai", defaultProvider: "openai",
defaultModelId: "gpt-4o", defaultModelId: "gpt-4o",
fallbackProvider: "openai",
fallbackModelId: "gpt-4o-mini",
defaultThinkingLevel: "medium",
fallbackThinkingLevel: "low",
}); });
expect(captured).not.toMatchObject({ expect(captured).not.toMatchObject({
defaultProvider: "anthropic", defaultProvider: "anthropic",
@@ -143,6 +160,176 @@ describe("executor workflow-step model resolution", () => {
}); });
}); });
it("routes review-type workflow steps through the validator model lane", async () => {
const captured = await runStepWithSettings(
{
executionProvider: "openai-codex",
executionModelId: "gpt-5.6-terra",
validatorProvider: "openai-codex",
validatorModelId: "gpt-5.6-sol",
validatorFallbackProvider: "openai-codex",
validatorFallbackModelId: "gpt-5.6-flash",
validatorThinkingLevel: "high",
validatorFallbackThinkingLevel: "low",
},
{
step: {
id: "graph:code-review-step",
name: "Code Review",
optionalGroupId: "code-review",
},
},
);
expect(captured).toMatchObject({
sessionPurpose: "executor",
defaultProvider: "openai-codex",
defaultModelId: "gpt-5.6-sol",
fallbackProvider: "openai-codex",
fallbackModelId: "gpt-5.6-flash",
defaultThinkingLevel: "high",
fallbackThinkingLevel: "low",
});
});
it.each([
["inline-fix metadata", { name: "Implementation Check", reviewCanFixInline: true }],
["Code Review group metadata", { name: "Implementation Check", optionalGroupId: "code-review" }],
["Plan Review identity", { id: "graph:plan-review-step", name: "Plan Review" }],
["verification name", { name: "Artifact Verification" }],
])("classifies %s as validator-routed", async (_label, step) => {
await expect(
runStepWithSettings(
{
executionProvider: "executor-provider",
executionModelId: "executor-model",
validatorProvider: "validator-provider",
validatorModelId: "validator-model",
},
{ step },
),
).resolves.toMatchObject({
sessionPurpose: "executor",
defaultProvider: "validator-provider",
defaultModelId: "validator-model",
});
});
it("keeps near-match ordinary names on executor lanes", async () => {
await expect(
runStepWithSettings(
{
executionProvider: "executor-provider",
executionModelId: "executor-model",
validatorProvider: "validator-provider",
validatorModelId: "validator-model",
},
{ step: { name: "Implementation Overview" } },
),
).resolves.toMatchObject({
sessionPurpose: "executor",
defaultProvider: "executor-provider",
defaultModelId: "executor-model",
});
});
it("uses selected-workflow validator lanes after project and global validator lanes fall through", async () => {
await expect(
runStepWithSettings(
{
selectedWorkflowModelLanes: {
validatorProvider: "workflow-validator-provider",
validatorModelId: "workflow-validator-model",
validatorFallbackProvider: "workflow-fallback-provider",
validatorFallbackModelId: "workflow-fallback-model",
validatorFallbackThinkingLevel: "low",
},
defaultProvider: "default-provider",
defaultModelId: "default-model",
},
{ step: { name: "Code Review", optionalGroupId: "code-review" } },
),
).resolves.toMatchObject({
defaultProvider: "workflow-validator-provider",
defaultModelId: "workflow-validator-model",
fallbackProvider: "workflow-fallback-provider",
fallbackModelId: "workflow-fallback-model",
fallbackThinkingLevel: "low",
});
});
it("keeps review step and task overrides ahead of validator-lane settings", async () => {
await expect(
runStepWithSettings(
{
validatorProvider: "project-validator-provider",
validatorModelId: "project-validator-model",
validatorThinkingLevel: "medium",
},
{
task: {
validatorModelProvider: "task-validator-provider",
validatorModelId: "task-validator-model",
validatorThinkingLevel: "high",
thinkingLevel: "low",
},
step: {
name: "Code Review",
optionalGroupId: "code-review",
},
},
),
).resolves.toMatchObject({
defaultProvider: "task-validator-provider",
defaultModelId: "task-validator-model",
defaultThinkingLevel: "high",
});
await expect(
runStepWithSettings(
{
validatorProvider: "project-validator-provider",
validatorModelId: "project-validator-model",
},
{
step: {
name: "Code Review",
optionalGroupId: "code-review",
modelProvider: "step-provider",
modelId: "step-model",
},
},
),
).resolves.toMatchObject({
defaultProvider: "step-provider",
defaultModelId: "step-model",
});
});
it.each([
["provider only", { modelProvider: "partial-provider" }],
["model only", { modelId: "partial-model" }],
])("does not mix a %s step override with the validator lane", async (_label, partialOverride) => {
await expect(
runStepWithSettings(
{
validatorProvider: "validator-provider",
validatorModelId: "validator-model",
},
{
step: {
name: "Code Review",
optionalGroupId: "code-review",
...partialOverride,
},
},
),
).resolves.toMatchObject({
defaultProvider: "validator-provider",
defaultModelId: "validator-model",
});
});
it("keeps step and task overrides ahead of execution-lane settings", async () => { it("keeps step and task overrides ahead of execution-lane settings", async () => {
await expect( await expect(
runStepWithSettings( runStepWithSettings(

View File

@@ -16,7 +16,7 @@ import type { TaskStore, Task, TaskDetail, TaskTokenUsage, StepStatus, Settings,
import { getUnmetSchedulingDependencies } from "./scheduler.js"; import { getUnmetSchedulingDependencies } from "./scheduler.js";
import type { ImplementationExit, ImplementationExitReporter } from "./executor/implementation-exit.js"; import type { ImplementationExit, ImplementationExitReporter } from "./executor/implementation-exit.js";
import { emitWorkflowLifecycleEvent } from "@fusion/core"; import { emitWorkflowLifecycleEvent } from "@fusion/core";
import { RetryStormError, serializeRetryStormError, evaluateCompletedPromotionFailureProvenance, evaluateSkipBypassTaint, resolveWorkflowIrForTask, evaluateForeachMergeProof, resolveCompleteColumn, resolveMergeOrchestrationColumn, resolveReboundTarget, resolveLifecycleColumns, resolveColumnAgentBinding, resolveEffectiveAgent, instanceNodeId, getWorkflowExtensionRegistry, getBuiltinWorkflow, parseNoOpCompletionMarker, allowsAutoMergeProcessing, resolveEffectiveAutoMerge, isLiveSharedBranchGroupMemberIntegration, resolveMaxAutoMergeRetries, resolveMaxConsecutiveToolFailureRetries, resolveConsecutiveToolFailureRetryBackoffMs, resolveConsecutiveToolFailureThreshold, resolveExecutorEscalationTarget, resolveOptionalStepRevisionBudget, resolveOptionalReviewRevisionBudget, DEFAULT_MAX_POST_REVIEW_FIXES, COMPLETION_SUMMARY_NODE_ID, upsertWorkflowStepResult, AWAITING_APPROVAL_PAUSE_REASON, THINKING_LEVELS, ACTIVE_WORKFLOW_WORK_ITEM_STATES, AgentStore, resolveExecutorFallbackModel } from "@fusion/core"; import { RetryStormError, serializeRetryStormError, evaluateCompletedPromotionFailureProvenance, evaluateSkipBypassTaint, resolveWorkflowIrForTask, evaluateForeachMergeProof, resolveCompleteColumn, resolveMergeOrchestrationColumn, resolveReboundTarget, resolveLifecycleColumns, resolveColumnAgentBinding, resolveEffectiveAgent, instanceNodeId, getWorkflowExtensionRegistry, getBuiltinWorkflow, parseNoOpCompletionMarker, allowsAutoMergeProcessing, resolveEffectiveAutoMerge, isLiveSharedBranchGroupMemberIntegration, resolveMaxAutoMergeRetries, resolveMaxConsecutiveToolFailureRetries, resolveConsecutiveToolFailureRetryBackoffMs, resolveConsecutiveToolFailureThreshold, resolveExecutorEscalationTarget, resolveOptionalStepRevisionBudget, resolveOptionalReviewRevisionBudget, DEFAULT_MAX_POST_REVIEW_FIXES, COMPLETION_SUMMARY_NODE_ID, upsertWorkflowStepResult, AWAITING_APPROVAL_PAUSE_REASON, THINKING_LEVELS, ACTIVE_WORKFLOW_WORK_ITEM_STATES, AgentStore, resolveExecutorFallbackModel, resolveValidatorFallbackModel } from "@fusion/core";
import { finalizeProvenAutoMergeTask } from "./auto-merge-finalization.js"; import { finalizeProvenAutoMergeTask } from "./auto-merge-finalization.js";
import { mergeEffectiveSettings } from "./effective-settings.js"; import { mergeEffectiveSettings } from "./effective-settings.js";
import { generateFeatureVideo, type GenerateFeatureVideoOptions } from "./review-artifacts/feature-video.js"; import { generateFeatureVideo, type GenerateFeatureVideoOptions } from "./review-artifacts/feature-video.js";
@@ -106,6 +106,7 @@ import {
createResolvedAgentSession, createResolvedAgentSession,
extractRuntimeHint, extractRuntimeHint,
resolveExecutorSessionModel, resolveExecutorSessionModel,
resolveValidatorSessionModel,
resolveExecutorThinkingLevel, resolveExecutorThinkingLevel,
resolveExecutorFallbackThinkingLevel, resolveExecutorFallbackThinkingLevel,
resolveValidatorThinkingLevel, resolveValidatorThinkingLevel,
@@ -17535,29 +17536,42 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
}, },
}); });
// Determine primary model and an explicit fallback. The workflow step's // Determine primary model and an explicit fallback. Review-type workflow
// own override takes precedence; otherwise use the canonical executor // steps use the validator lane; ordinary workflow prompts use the executor
// hierarchy: task override → project execution lane → global execution lane // lane. A complete per-step override remains authoritative for either lane.
// → project default override → global default. The fallback is the per-step // FNXC:ModelResolution 2026-06-25-12:00: FN-7039 requires ordinary workflow
// override's missing-counterpart settings, then the executor fallback lane, // steps to inherit project execution-lane model settings before defaults.
// which itself falls through to the shared global fallback pair. // Review gates are independent validation surfaces and must not silently use
// FNXC:ModelResolution 2026-06-25-12:00: FN-7039 requires workflow steps to inherit project execution-lane model settings before default settings so configured Execution models reach step sessions unless the step itself overrides them. // the same implementation model merely because they execute in this method.
const assignedRuntimeConfig = await this.getAssignedAgentRuntimeConfig(task.assignedAgentId); const assignedRuntimeConfig = await this.getAssignedAgentRuntimeConfig(task.assignedAgentId);
const executorModel = resolveExecutorSessionModel( const laneModel = isReviewTypeWorkflowStep
task.modelProvider, ? resolveValidatorSessionModel(
task.modelId, task.validatorModelProvider,
settings, task.validatorModelId,
assignedRuntimeConfig, settings,
); assignedRuntimeConfig,
const primaryProvider = workflowStep.modelProvider || executorModel.provider; )
const primaryModelId = workflowStep.modelId || executorModel.modelId; : resolveExecutorSessionModel(
task.modelProvider,
task.modelId,
settings,
assignedRuntimeConfig,
);
const useOverride = !!(workflowStep.modelProvider && workflowStep.modelId); const useOverride = !!(workflowStep.modelProvider && workflowStep.modelId);
const primaryProvider = useOverride ? workflowStep.modelProvider : laneModel.provider;
const primaryModelId = useOverride ? workflowStep.modelId : laneModel.modelId;
const executorFallback = resolveExecutorFallbackModel(settings); const workflowFallback = isReviewTypeWorkflowStep
const fallback = executorFallback.provider && executorFallback.modelId ? resolveValidatorFallbackModel(settings)
&& (executorFallback.provider !== primaryProvider || executorFallback.modelId !== primaryModelId) : resolveExecutorFallbackModel(settings);
? executorFallback const fallback = workflowFallback.provider && workflowFallback.modelId
&& (workflowFallback.provider !== primaryProvider || workflowFallback.modelId !== primaryModelId)
? workflowFallback
: undefined; : undefined;
const fallbackSettingsHint = isReviewTypeWorkflowStep
? "settings.validatorFallbackProvider/validatorFallbackModelId or fallbackProvider/fallbackModelId"
: "settings.executionFallbackProvider/executionFallbackModelId or fallbackProvider/fallbackModelId";
const fallbackLaneLabel = isReviewTypeWorkflowStep ? "validator" : "executor";
const timeoutMs = Math.max(60_000, settings.workflowStepTimeoutMs ?? 900_000); const timeoutMs = Math.max(60_000, settings.workflowStepTimeoutMs ?? 900_000);
@@ -17566,7 +17580,6 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
modelId: string | undefined, modelId: string | undefined,
attemptLabel: string, attemptLabel: string,
): Promise<WorkflowStepOutcome> => { ): Promise<WorkflowStepOutcome> => {
// Workflow step agents inherit executor instructions
const stepInstructions = await this.resolveInstructionsForRole("executor", settings); const stepInstructions = await this.resolveInstructionsForRole("executor", settings);
const stepSystemPrompt = buildSystemPromptWithInstructions(systemPrompt, stepInstructions); const stepSystemPrompt = buildSystemPromptWithInstructions(systemPrompt, stepInstructions);
@@ -17700,16 +17713,23 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
/* /*
* FNXC:Settings-ThinkingLevel 2026-07-10-00:00: * 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. * WorkflowStep sessions resolve reasoning effort as node/step `thinkingLevel` first, then the task override for their selected model lane, then settings defaults/lane fallbacks.
* *
* FNXC:Settings-ThinkingLevel 2026-07-10-14:20: * 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`. * 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 workflowStepThinkingSource = workflowStep.thinkingLevel ?? task.thinkingLevel; const workflowStepThinkingSource = workflowStep.thinkingLevel
?? (isReviewTypeWorkflowStep ? task.validatorThinkingLevel ?? task.thinkingLevel : task.thinkingLevel);
const workflowStepThinkingLevel = attemptLabel === "fallback" const workflowStepThinkingLevel = attemptLabel === "fallback"
? resolveExecutorFallbackThinkingLevel(workflowStepThinkingSource, settings) ? isReviewTypeWorkflowStep
: resolveExecutorThinkingLevel(workflowStepThinkingSource, settings); ? resolveValidatorFallbackThinkingLevel(workflowStepThinkingSource, settings)
const workflowStepFallbackThinkingLevel = resolveExecutorFallbackThinkingLevel(workflowStepThinkingSource, settings); : resolveExecutorFallbackThinkingLevel(workflowStepThinkingSource, settings)
: isReviewTypeWorkflowStep
? resolveValidatorThinkingLevel(workflowStepThinkingSource, settings)
: resolveExecutorThinkingLevel(workflowStepThinkingSource, settings);
const workflowStepFallbackThinkingLevel = isReviewTypeWorkflowStep
? resolveValidatorFallbackThinkingLevel(workflowStepThinkingSource, settings)
: resolveExecutorFallbackThinkingLevel(workflowStepThinkingSource, settings);
const { session } = await createResolvedAgentSession({ const { session } = await createResolvedAgentSession({
sessionPurpose: "executor", sessionPurpose: "executor",
runtimeHint: workflowRuntimeHint, runtimeHint: workflowRuntimeHint,
@@ -17719,8 +17739,8 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
tools: toolMode, tools: toolMode,
defaultProvider: provider, defaultProvider: provider,
defaultModelId: modelId, defaultModelId: modelId,
fallbackProvider: executorFallback.provider, fallbackProvider: workflowFallback.provider,
fallbackModelId: executorFallback.modelId, fallbackModelId: workflowFallback.modelId,
fallbackThinkingLevel: workflowStepFallbackThinkingLevel, fallbackThinkingLevel: workflowStepFallbackThinkingLevel,
defaultThinkingLevel: workflowStepThinkingLevel, defaultThinkingLevel: workflowStepThinkingLevel,
runAuditor: createRunAuditor(this.store, this.getRunContextFor(task.id)), runAuditor: createRunAuditor(this.store, this.getRunContextFor(task.id)),
@@ -17970,7 +17990,7 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
if (!retryMalformed) return retryOutcome; if (!retryMalformed) return retryOutcome;
await this.store.logEntry( await this.store.logEntry(
task.id, task.id,
`Workflow step '${workflowStep.name}' produced malformed output on both the primary attempt and one self-retry — no fallback model configured (set settings.executionFallbackProvider/Id or fallbackProvider/Id)`, `Workflow step '${workflowStep.name}' produced malformed output on both the primary attempt and one self-retry — no fallback model configured (set ${fallbackSettingsHint})`,
); );
return retryOutcome; return retryOutcome;
} }
@@ -17978,12 +17998,12 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
executorLog.warn(`${task.id}: workflow step '${workflowStep.name}' ${reason} and no fallback model is configured`); executorLog.warn(`${task.id}: workflow step '${workflowStep.name}' ${reason} and no fallback model is configured`);
await this.store.logEntry( await this.store.logEntry(
task.id, task.id,
`Workflow step '${workflowStep.name}' ${reason} — no fallback model configured (set settings.executionFallbackProvider/Id or fallbackProvider/Id)`, `Workflow step '${workflowStep.name}' ${reason} — no fallback model configured (set ${fallbackSettingsHint})`,
); );
return primaryOutcome; return primaryOutcome;
} }
executorLog.log(`${task.id}: retrying workflow step '${workflowStep.name}' with executor fallback ${fallback.provider}/${fallback.modelId} after primary ${primaryOutcome.timedOut ? "timeout" : "malformed output"}`); executorLog.log(`${task.id}: retrying workflow step '${workflowStep.name}' with ${fallbackLaneLabel} fallback ${fallback.provider}/${fallback.modelId} after primary ${primaryOutcome.timedOut ? "timeout" : "malformed output"}`);
return runOnce(fallback.provider, fallback.modelId, "fallback"); return runOnce(fallback.provider, fallback.modelId, "fallback");
} }