diff --git a/.changeset/fuzzy-agents-run.md b/.changeset/fuzzy-agents-run.md new file mode 100644 index 0000000000..7510bf1028 --- /dev/null +++ b/.changeset/fuzzy-agents-run.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Fix automatic agent runs to resolve executor, planning, heartbeat, merger, and validator models from fresh task/settings configuration before falling back to durable agent runtime defaults. diff --git a/packages/core/src/__tests__/model-resolution.test.ts b/packages/core/src/__tests__/model-resolution.test.ts index e26bbb7a60..d79ba0352c 100644 --- a/packages/core/src/__tests__/model-resolution.test.ts +++ b/packages/core/src/__tests__/model-resolution.test.ts @@ -119,6 +119,46 @@ describe("model-resolution", () => { ).toEqual({ provider: "openai", modelId: "gpt-4.1" }); }); + it("ignores partial pairs at every precedence tier", () => { + expect( + resolveProjectDefaultModel({ + defaultProviderOverride: "openai", + defaultProvider: "anthropic", + defaultModelId: "claude-sonnet-4-5", + }), + ).toEqual({ provider: "anthropic", modelId: "claude-sonnet-4-5" }); + + expect( + resolveTaskExecutionModel( + { modelProvider: "task-provider" }, + { + executionProvider: "openai", + executionModelId: "gpt-4.1", + }, + ), + ).toEqual({ provider: "openai", modelId: "gpt-4.1" }); + + expect( + resolveTaskPlanningModel( + { planningModelId: "task-planning-model" }, + { + planningGlobalProvider: "anthropic", + planningGlobalModelId: "claude-sonnet-4-5", + }, + ), + ).toEqual({ provider: "anthropic", modelId: "claude-sonnet-4-5" }); + + expect( + resolveTaskValidatorModel( + { validatorModelProvider: "validator-task-provider" }, + { + defaultProviderOverride: "google", + defaultModelIdOverride: "gemini-2.5-pro", + }, + ), + ).toEqual({ provider: "google", modelId: "gemini-2.5-pro" }); + }); + it("forces every lane to mock when testMode is true", () => { const settings = { testMode: true, diff --git a/packages/engine/src/__tests__/agent-session-helpers-test-mode.test.ts b/packages/engine/src/__tests__/agent-session-helpers-test-mode.test.ts index d1e74ec413..6bc4e7b570 100644 --- a/packages/engine/src/__tests__/agent-session-helpers-test-mode.test.ts +++ b/packages/engine/src/__tests__/agent-session-helpers-test-mode.test.ts @@ -4,6 +4,7 @@ import { resolveHeartbeatSessionModels, resolveMergerSessionModel, resolvePlanningSessionModel, + resolveValidatorSessionModel, } from "../agent-session-helpers.js"; const assignedAgentRuntimeConfig = { @@ -33,6 +34,10 @@ describe("agent-session-helpers test mode overrides", () => { provider: "mock", modelId: "scripted", }); + expect(resolveValidatorSessionModel("openai", "gpt-4.1", settings, assignedAgentRuntimeConfig)).toEqual({ + provider: "mock", + modelId: "scripted", + }); expect(resolveMergerSessionModel(settings, assignedAgentRuntimeConfig)).toEqual({ provider: "mock", modelId: "scripted", @@ -65,6 +70,10 @@ describe("agent-session-helpers test mode overrides", () => { provider: "mock", modelId: "scripted", }); + expect(resolveValidatorSessionModel("openai", "gpt-4.1", settings, assignedAgentRuntimeConfig)).toEqual({ + provider: "mock", + modelId: "scripted", + }); expect(resolveMergerSessionModel(settings, assignedAgentRuntimeConfig)).toEqual({ provider: "mock", modelId: "scripted", @@ -77,7 +86,7 @@ describe("agent-session-helpers test mode overrides", () => { }); }); - it("keeps existing behavior when test mode is inactive", () => { + it("prefers freshly resolved settings over runtimeConfig when test mode is inactive", () => { const settings = { executionProvider: "openai", executionModelId: "gpt-4.1", @@ -90,22 +99,26 @@ describe("agent-session-helpers test mode overrides", () => { }; expect(resolveExecutorSessionModel("task-provider", "task-model", settings, assignedAgentRuntimeConfig)).toEqual({ - provider: "anthropic", - modelId: "claude-sonnet-4-5", + provider: "task-provider", + modelId: "task-model", }); expect(resolvePlanningSessionModel("task-provider", "task-model", settings, assignedAgentRuntimeConfig)).toEqual({ - provider: "anthropic", - modelId: "claude-sonnet-4-5", + provider: "task-provider", + modelId: "task-model", + }); + expect(resolveValidatorSessionModel("task-provider", "task-model", settings, assignedAgentRuntimeConfig)).toEqual({ + provider: "task-provider", + modelId: "task-model", }); expect(resolveMergerSessionModel(settings, assignedAgentRuntimeConfig)).toEqual({ - provider: "anthropic", - modelId: "claude-sonnet-4-5", + provider: "openai", + modelId: "gpt-4.1", }); expect(resolveHeartbeatSessionModels(settings, assignedAgentRuntimeConfig)).toEqual({ - defaultProvider: "anthropic", - defaultModelId: "claude-sonnet-4-5", - fallbackProvider: "openai", - fallbackModelId: "gpt-4.1", + defaultProvider: "openai", + defaultModelId: "gpt-4.1", + fallbackProvider: undefined, + fallbackModelId: undefined, }); }); }); diff --git a/packages/engine/src/__tests__/agent-session-helpers.test.ts b/packages/engine/src/__tests__/agent-session-helpers.test.ts index c249713205..0ef1a3b8b8 100644 --- a/packages/engine/src/__tests__/agent-session-helpers.test.ts +++ b/packages/engine/src/__tests__/agent-session-helpers.test.ts @@ -1,8 +1,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { extractRuntimeHint, + extractRuntimeModel, + resolveExecutorSessionModel, resolveHeartbeatSessionModels, resolveMergerSessionModel, + resolvePlanningSessionModel, + resolveValidatorSessionModel, } from "../agent-session-helpers.js"; const { resolveRuntimeMock } = vi.hoisted(() => ({ @@ -39,49 +43,199 @@ describe("extractRuntimeHint", () => { }); }); -describe("resolveHeartbeatSessionModels", () => { - it("uses agent runtime model as primary and execution settings as fallback", () => { +describe("extractRuntimeModel", () => { + it("parses combined and separate runtime model pairs", () => { + expect(extractRuntimeModel({ model: " anthropic/claude-sonnet-4-5 " })).toEqual({ + provider: "anthropic", + modelId: "claude-sonnet-4-5", + }); + expect(extractRuntimeModel({ modelProvider: " openai ", modelId: " gpt-4.1 " })).toEqual({ + provider: "openai", + modelId: "gpt-4.1", + }); + }); + + it("does not turn malformed combined strings into complete model pairs", () => { + expect(extractRuntimeModel({ model: "gpt 5.3" })).toEqual({ + provider: undefined, + modelId: undefined, + }); + }); +}); + +describe("resolve session model parity", () => { + const settings = { + executionProvider: "openai", + executionModelId: "gpt-4.1", + planningProvider: "anthropic", + planningModelId: "claude-sonnet-4-5", + defaultProviderOverride: "google", + defaultModelIdOverride: "gemini-2.5-pro", + defaultProvider: "zai", + defaultModelId: "glm-5.1", + }; + + it("uses the same fresh settings model for executor and heartbeat when runtimeConfig is absent", () => { + const executor = resolveExecutorSessionModel(undefined, undefined, settings); + const heartbeat = resolveHeartbeatSessionModels(settings); + + expect(executor).toEqual({ provider: "openai", modelId: "gpt-4.1" }); + expect(heartbeat).toEqual({ + defaultProvider: executor.provider, + defaultModelId: executor.modelId, + fallbackProvider: undefined, + fallbackModelId: undefined, + }); + }); + + it("ignores partial runtimeConfig pairs without mixing runtime and settings fields", () => { expect(resolveHeartbeatSessionModels( { executionProvider: "openai", executionModelId: "gpt-4.1", }, - { model: "anthropic/claude-sonnet-4-5" }, + { modelProvider: "stale-provider" }, )).toEqual({ + defaultProvider: "openai", + defaultModelId: "gpt-4.1", + fallbackProvider: undefined, + fallbackModelId: undefined, + }); + + expect(resolveHeartbeatSessionModels( + { + executionProvider: "openai", + executionModelId: "gpt-4.1", + }, + { modelId: "gpt 5.3" }, + )).toEqual({ + defaultProvider: "openai", + defaultModelId: "gpt-4.1", + fallbackProvider: undefined, + fallbackModelId: undefined, + }); + }); + + it("does not let a stale complete runtime model mask newer task or settings models", () => { + const staleRuntimeConfig = { model: "openai-codex/gpt-5.3-codex" }; + + expect(resolveExecutorSessionModel("task-provider", "task-model", settings, staleRuntimeConfig)).toEqual({ + provider: "task-provider", + modelId: "task-model", + }); + expect(resolveExecutorSessionModel(undefined, undefined, settings, staleRuntimeConfig)).toEqual({ + provider: "openai", + modelId: "gpt-4.1", + }); + expect(resolvePlanningSessionModel("planning-task-provider", "planning-task-model", settings, staleRuntimeConfig)).toEqual({ + provider: "planning-task-provider", + modelId: "planning-task-model", + }); + expect(resolvePlanningSessionModel(undefined, undefined, settings, staleRuntimeConfig)).toEqual({ + provider: "anthropic", + modelId: "claude-sonnet-4-5", + }); + expect(resolveHeartbeatSessionModels(settings, staleRuntimeConfig)).toEqual({ + defaultProvider: "openai", + defaultModelId: "gpt-4.1", + fallbackProvider: undefined, + fallbackModelId: undefined, + }); + expect(resolveValidatorSessionModel("validator-task-provider", "validator-task-model", settings, staleRuntimeConfig)).toEqual({ + provider: "validator-task-provider", + modelId: "validator-task-model", + }); + expect(resolveMergerSessionModel(settings, staleRuntimeConfig)).toEqual({ + provider: "google", + modelId: "gemini-2.5-pro", + }); + }); + + it("does not leak malformed gpt 5.3-style runtimeConfig into any automatic lane", () => { + const malformedRuntimeConfig = { modelId: "gpt 5.3" }; + + expect(resolveExecutorSessionModel(undefined, undefined, settings, malformedRuntimeConfig)).toEqual({ + provider: "openai", + modelId: "gpt-4.1", + }); + expect(resolvePlanningSessionModel(undefined, undefined, settings, malformedRuntimeConfig)).toEqual({ + provider: "anthropic", + modelId: "claude-sonnet-4-5", + }); + expect(resolveHeartbeatSessionModels(settings, malformedRuntimeConfig)).toEqual({ + defaultProvider: "openai", + defaultModelId: "gpt-4.1", + fallbackProvider: undefined, + fallbackModelId: undefined, + }); + expect(resolveValidatorSessionModel(undefined, undefined, settings, malformedRuntimeConfig)).toEqual({ + provider: "google", + modelId: "gemini-2.5-pro", + }); + expect(resolveMergerSessionModel(settings, malformedRuntimeConfig)).toEqual({ + provider: "google", + modelId: "gemini-2.5-pro", + }); + }); + + it("falls back through project override and global defaults before runtimeConfig", () => { + const staleRuntimeConfig = { model: "stale-provider/stale-model" }; + + expect(resolveMergerSessionModel({ + defaultProviderOverride: "google", + defaultModelIdOverride: "gemini-2.5-pro", defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", - fallbackProvider: "openai", - fallbackModelId: "gpt-4.1", - }); + }, staleRuntimeConfig)).toEqual({ provider: "google", modelId: "gemini-2.5-pro" }); + expect(resolveMergerSessionModel({ + defaultProvider: "anthropic", + defaultModelId: "claude-sonnet-4-5", + }, staleRuntimeConfig)).toEqual({ provider: "anthropic", modelId: "claude-sonnet-4-5" }); }); - it("uses execution settings model when runtime override is missing", () => { - expect(resolveHeartbeatSessionModels( - { - executionProvider: "openai", - executionModelId: "gpt-4.1", - }, - {}, - )).toEqual({ - defaultProvider: "openai", - defaultModelId: "gpt-4.1", + it("uses a complete runtime model only when no lane/task/default model is configured", () => { + const runtimeConfig = { modelProvider: "anthropic", modelId: "claude-opus-4" }; + + expect(resolveExecutorSessionModel(undefined, undefined, undefined, runtimeConfig)).toEqual({ + provider: "anthropic", + modelId: "claude-opus-4", + }); + expect(resolvePlanningSessionModel(undefined, undefined, undefined, runtimeConfig)).toEqual({ + provider: "anthropic", + modelId: "claude-opus-4", + }); + expect(resolveHeartbeatSessionModels(undefined, runtimeConfig)).toEqual({ + defaultProvider: "anthropic", + defaultModelId: "claude-opus-4", fallbackProvider: undefined, fallbackModelId: undefined, }); + expect(resolveValidatorSessionModel(undefined, undefined, undefined, runtimeConfig)).toEqual({ + provider: "anthropic", + modelId: "claude-opus-4", + }); + expect(resolveMergerSessionModel(undefined, runtimeConfig)).toEqual({ + provider: "anthropic", + modelId: "claude-opus-4", + }); }); - it("does not duplicate fallback when runtime and execution model are the same", () => { - expect(resolveHeartbeatSessionModels( - { - executionProvider: "openai", - executionModelId: "gpt-4.1", - }, - { modelProvider: "openai", modelId: "gpt-4.1" }, - )).toEqual({ - defaultProvider: "openai", - defaultModelId: "gpt-4.1", - fallbackProvider: undefined, - fallbackModelId: undefined, + it("covers backend-only surfaces; desktop and mobile breakpoints are not applicable", () => { + expect(resolveExecutorSessionModel(undefined, undefined, settings, { model: "stale/old" })).toEqual({ + provider: "openai", + modelId: "gpt-4.1", + }); + expect(resolvePlanningSessionModel(undefined, undefined, settings, { model: "stale/old" })).toEqual({ + provider: "anthropic", + modelId: "claude-sonnet-4-5", + }); + expect(resolveValidatorSessionModel(undefined, undefined, settings, { model: "stale/old" })).toEqual({ + provider: "google", + modelId: "gemini-2.5-pro", + }); + expect(resolveMergerSessionModel(settings, { model: "stale/old" })).toEqual({ + provider: "google", + modelId: "gemini-2.5-pro", }); }); }); @@ -220,15 +374,10 @@ describe("createResolvedAgentSession", () => { }); describe("resolveMergerSessionModel", () => { - it("uses assigned agent runtime model when both provider and modelId are present", () => { + it("uses assigned agent runtime model only when no default model pair is configured", () => { expect( resolveMergerSessionModel( - { - defaultProviderOverride: "openai", - defaultModelIdOverride: "gpt-4.1", - defaultProvider: "anthropic", - defaultModelId: "claude-3-5-sonnet", - }, + {}, { model: " anthropic/claude-3-5-sonnet-20241022 " }, ), ).toEqual({ diff --git a/packages/engine/src/__tests__/heartbeat-executor.test.ts b/packages/engine/src/__tests__/heartbeat-executor.test.ts index 81aff37206..938c16c790 100644 --- a/packages/engine/src/__tests__/heartbeat-executor.test.ts +++ b/packages/engine/src/__tests__/heartbeat-executor.test.ts @@ -3001,7 +3001,7 @@ describe("executeHeartbeat", () => { expect(taskLogTool.name).toBe("fn_task_log"); }); - it("passes runtime model as primary and execution settings model as fallback", async () => { + it("passes execution settings model ahead of stale runtime model", async () => { const store = createStoreWithAgentForExec({ runtimeConfig: { model: "anthropic/claude-sonnet-4-5" }, }); @@ -3022,10 +3022,10 @@ describe("executeHeartbeat", () => { expect(mockedCreateFnAgent).toHaveBeenCalledOnce(); const callArgs = mockedCreateFnAgent.mock.calls[0]![0]; - expect(callArgs.defaultProvider).toBe("anthropic"); - expect(callArgs.defaultModelId).toBe("claude-sonnet-4-5"); - expect(callArgs.fallbackProvider).toBe("openai"); - expect(callArgs.fallbackModelId).toBe("gpt-4.1"); + expect(callArgs.defaultProvider).toBe("openai"); + expect(callArgs.defaultModelId).toBe("gpt-4.1"); + expect(callArgs.fallbackProvider).toBeUndefined(); + expect(callArgs.fallbackModelId).toBeUndefined(); }); it("passes undefined model when runtimeConfig has no model", async () => { diff --git a/packages/engine/src/__tests__/mission-execution-loop.test.ts b/packages/engine/src/__tests__/mission-execution-loop.test.ts index d1bb8d7dc2..7f1f169a95 100644 --- a/packages/engine/src/__tests__/mission-execution-loop.test.ts +++ b/packages/engine/src/__tests__/mission-execution-loop.test.ts @@ -1145,7 +1145,7 @@ describe("MissionExecutionLoop", () => { })); }); - it("uses assigned agent runtime model ahead of task/settings for mission validation", async () => { + it("uses task/settings validator model ahead of assigned agent runtime model for mission validation", async () => { const feature = createMockFeature({ loopState: "implementing", taskId: "FN-MODEL-AGENT", status: "in-progress" }); missionStore._setFeature(feature); missionStore.getFeatureByTaskId = vi.fn().mockReturnValue(feature); @@ -1185,8 +1185,8 @@ describe("MissionExecutionLoop", () => { expect(createResolvedAgentSession).toHaveBeenCalledWith(expect.objectContaining({ sessionPurpose: "validation", - defaultProvider: "agent-provider", - defaultModelId: "agent-model", + defaultProvider: "task-validator", + defaultModelId: "task-validator-model", })); }); diff --git a/packages/engine/src/__tests__/triage.test.ts b/packages/engine/src/__tests__/triage.test.ts index 517352ca03..22a43433ad 100644 --- a/packages/engine/src/__tests__/triage.test.ts +++ b/packages/engine/src/__tests__/triage.test.ts @@ -3084,7 +3084,7 @@ describe("taskCreate tool model inheritance", () => { expect(capturedArgs.systemPrompt).toContain("agent ID: agent-007"); }); - it("prefers assigned-agent runtime model and falls back when incomplete", async () => { + it("prefers planning settings model ahead of assigned-agent runtime model", async () => { const completeRuntimeTask = createTriageTask({ id: "FN-AGENT-MODEL-1", assignedAgentId: "agent-model-complete" }); const incompleteRuntimeTask = createTriageTask({ id: "FN-AGENT-MODEL-2", assignedAgentId: "agent-model-incomplete" }); @@ -3152,7 +3152,7 @@ describe("taskCreate tool model inheritance", () => { const completeCall = capturedArgs.find((entry) => entry.taskId === "FN-AGENT-MODEL-1"); const fallbackCall = capturedArgs.find((entry) => entry.taskId === "FN-AGENT-MODEL-2"); - expect(completeCall).toMatchObject({ defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5" }); + expect(completeCall).toMatchObject({ defaultProvider: "openai", defaultModelId: "gpt-4o" }); expect(fallbackCall).toMatchObject({ defaultProvider: "openai", defaultModelId: "gpt-4o" }); }); diff --git a/packages/engine/src/agent-session-helpers.ts b/packages/engine/src/agent-session-helpers.ts index 98939f5162..31c1144d17 100644 --- a/packages/engine/src/agent-session-helpers.ts +++ b/packages/engine/src/agent-session-helpers.ts @@ -14,9 +14,12 @@ import type { AgentSession } from "@earendil-works/pi-coding-agent"; import { isTestModeActive, resolveExecutionSettingsModel, + resolveProjectDefaultModel, resolveTaskExecutionModel, resolveTaskPlanningModel, + resolveTaskValidatorModel, TEST_MODE_RESOLVED, + type ResolvedModelSelection, type Settings, } from "@fusion/core"; import { resolveRuntime, buildRuntimeResolutionContext, isMockProviderId, type SessionPurpose } from "./runtime-resolution.js"; @@ -131,6 +134,32 @@ export function extractRuntimeModel( }; } +function hasCompleteRuntimeModel( + model: ResolvedModelSelection, +): model is { provider: string; modelId: string } { + return Boolean(model.provider && model.modelId); +} + +function pickSettingsThenRuntimeModel( + settingsModel: ResolvedModelSelection, + assignedAgentRuntimeConfig?: Record, +): { provider: string | undefined; modelId: string | undefined } { + if (settingsModel.provider && settingsModel.modelId) { + return { + provider: settingsModel.provider, + modelId: settingsModel.modelId, + }; + } + + const assignedRuntimeModel = extractRuntimeModel(assignedAgentRuntimeConfig); + return hasCompleteRuntimeModel(assignedRuntimeModel) + ? assignedRuntimeModel + : { + provider: settingsModel.provider, + modelId: settingsModel.modelId, + }; +} + export function resolveExecutorSessionModel( taskModelProvider: string | undefined, taskModelId: string | undefined, @@ -144,11 +173,6 @@ export function resolveExecutorSessionModel( }; } - const assignedRuntimeModel = extractRuntimeModel(assignedAgentRuntimeConfig); - if (assignedRuntimeModel.provider && assignedRuntimeModel.modelId) { - return assignedRuntimeModel; - } - const resolvedTaskModel = resolveTaskExecutionModel( { modelProvider: taskModelProvider, @@ -157,10 +181,7 @@ export function resolveExecutorSessionModel( settings, ); - return { - provider: resolvedTaskModel.provider, - modelId: resolvedTaskModel.modelId, - }; + return pickSettingsThenRuntimeModel(resolvedTaskModel, assignedAgentRuntimeConfig); } export function resolvePlanningSessionModel( @@ -176,11 +197,6 @@ export function resolvePlanningSessionModel( }; } - const assignedRuntimeModel = extractRuntimeModel(assignedAgentRuntimeConfig); - if (assignedRuntimeModel.provider && assignedRuntimeModel.modelId) { - return assignedRuntimeModel; - } - const resolvedTaskPlanningModel = resolveTaskPlanningModel( { planningModelProvider: taskPlanningModelProvider, @@ -189,10 +205,31 @@ export function resolvePlanningSessionModel( settings, ); - return { - provider: resolvedTaskPlanningModel.provider, - modelId: resolvedTaskPlanningModel.modelId, - }; + return pickSettingsThenRuntimeModel(resolvedTaskPlanningModel, assignedAgentRuntimeConfig); +} + +export function resolveValidatorSessionModel( + taskValidatorModelProvider: string | undefined, + taskValidatorModelId: string | undefined, + settings: Partial | undefined, + assignedAgentRuntimeConfig?: Record, +): { provider: string | undefined; modelId: string | undefined } { + if (isTestModeActive(settings)) { + return { + provider: TEST_MODE_RESOLVED.provider, + modelId: TEST_MODE_RESOLVED.modelId, + }; + } + + const resolvedTaskValidatorModel = resolveTaskValidatorModel( + { + validatorModelProvider: taskValidatorModelProvider, + validatorModelId: taskValidatorModelId, + }, + settings, + ); + + return pickSettingsThenRuntimeModel(resolvedTaskValidatorModel, assignedAgentRuntimeConfig); } export function resolveHeartbeatSessionModels( @@ -213,21 +250,14 @@ export function resolveHeartbeatSessionModels( }; } - const assignedRuntimeModel = extractRuntimeModel(assignedAgentRuntimeConfig); const executionSettingsModel = resolveExecutionSettingsModel(settings); - - const defaultProvider = assignedRuntimeModel.provider ?? executionSettingsModel.provider; - const defaultModelId = assignedRuntimeModel.modelId ?? executionSettingsModel.modelId; - - const executionPairAvailable = Boolean(executionSettingsModel.provider && executionSettingsModel.modelId); - const defaultMatchesExecution = - defaultProvider === executionSettingsModel.provider && defaultModelId === executionSettingsModel.modelId; + const resolvedModel = pickSettingsThenRuntimeModel(executionSettingsModel, assignedAgentRuntimeConfig); return { - defaultProvider, - defaultModelId, - fallbackProvider: executionPairAvailable && !defaultMatchesExecution ? executionSettingsModel.provider : undefined, - fallbackModelId: executionPairAvailable && !defaultMatchesExecution ? executionSettingsModel.modelId : undefined, + defaultProvider: resolvedModel.provider, + defaultModelId: resolvedModel.modelId, + fallbackProvider: undefined, + fallbackModelId: undefined, }; } @@ -242,22 +272,11 @@ export function resolveMergerSessionModel( }; } - const assignedRuntimeModel = extractRuntimeModel(assignedAgentRuntimeConfig); - if (assignedRuntimeModel.provider && assignedRuntimeModel.modelId) { - return assignedRuntimeModel; - } - - if (settings?.defaultProviderOverride && settings.defaultModelIdOverride) { - return { - provider: settings.defaultProviderOverride, - modelId: settings.defaultModelIdOverride, - }; - } - - return { - provider: settings?.defaultProvider, - modelId: settings?.defaultModelId, - }; + // Merger intentionally uses the default lane rather than execution/validator + // lanes. Validator-specific callers resolve `resolveValidatorSettingsModel` + // before falling back here; generic merger work uses project/global defaults. + const defaultModel = resolveProjectDefaultModel(settings); + return pickSettingsThenRuntimeModel(defaultModel, assignedAgentRuntimeConfig); } /** diff --git a/packages/engine/src/mission-execution-loop.ts b/packages/engine/src/mission-execution-loop.ts index cfd77f3e7e..d70347bc73 100644 --- a/packages/engine/src/mission-execution-loop.ts +++ b/packages/engine/src/mission-execution-loop.ts @@ -22,17 +22,12 @@ import type { Settings, Milestone, } from "@fusion/core"; -import { - TEST_MODE_RESOLVED, - isTestModeActive, - resolveTaskValidatorModel, -} from "@fusion/core"; import { createFnAgent, promptWithFallback, type AgentResult } from "./pi.js"; import { mergeEffectiveSettings } from "./effective-settings.js"; import { createResolvedAgentSession, extractRuntimeHint, - extractRuntimeModel, + resolveValidatorSessionModel, } from "./agent-session-helpers.js"; import { createLogger } from "./logger.js"; import { createFallbackModelObserver } from "./fallback-model-observer.js"; @@ -573,30 +568,12 @@ export class MissionExecutionLoop extends EventEmitter { settings: Partial | undefined, assignedAgentRuntimeConfig?: Record, ): { provider: string | undefined; modelId: string | undefined } { - if (isTestModeActive(settings)) { - return { - provider: TEST_MODE_RESOLVED.provider, - modelId: TEST_MODE_RESOLVED.modelId, - }; - } - - const assignedRuntimeModel = extractRuntimeModel(assignedAgentRuntimeConfig); - if (assignedRuntimeModel.provider && assignedRuntimeModel.modelId) { - return assignedRuntimeModel; - } - - const resolvedTaskModel = resolveTaskValidatorModel( - { - validatorModelProvider: task?.validatorModelProvider, - validatorModelId: task?.validatorModelId, - }, + return resolveValidatorSessionModel( + task?.validatorModelProvider, + task?.validatorModelId, settings, + assignedAgentRuntimeConfig, ); - - return { - provider: resolvedTaskModel.provider, - modelId: resolvedTaskModel.modelId, - }; } /** diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts index dfc0f5d63f..8647e48c16 100644 --- a/packages/engine/src/triage.ts +++ b/packages/engine/src/triage.ts @@ -1335,9 +1335,9 @@ export class TriageProcessor { }); // Resolve planning model using executor-style precedence: - // 1. Assigned durable agent runtime model pair when complete - // 2. Task planning override pair - // 3. Planning/project/global fallbacks + // 1. Task planning override pair + // 2. Planning/project/global fallbacks + // 3. Assigned durable agent runtime model pair when no fresh model pair exists const planningModel = resolvePlanningSessionModel( task.planningModelProvider, task.planningModelId,