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:
@@ -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.
|
||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user