FN-6219: prefer fresh agent model settings for automatic runs

Automatic agent runs now resolve lane-specific task and settings models before falling back to durable runtime defaults.

- Prefer fresh execution, planning, heartbeat, merger, and validator model resolution over stale assigned-agent runtime config when complete settings are available.
- Centralize validator session model resolution and align mission validation with the shared helper.
- Add regression coverage for automatic run model precedence and test-mode handling.
- Add a patch changeset for the published CLI package.

Files changed:
 .changeset/fuzzy-agents-run.md                     |   5 +
 .../core/src/__tests__/model-resolution.test.ts    |  40 +++++
 .../agent-session-helpers-test-mode.test.ts        |  35 ++--
 .../src/__tests__/agent-session-helpers.test.ts    | 199 ++++++++++++++++++---
 .../src/__tests__/heartbeat-executor.test.ts       |  10 +-
 .../src/__tests__/mission-execution-loop.test.ts   |   6 +-
 packages/engine/src/__tests__/triage.test.ts       |   4 +-
 packages/engine/src/agent-session-helpers.ts       | 111 +++++++-----
 packages/engine/src/mission-execution-loop.ts      |  33 +---
 packages/engine/src/triage.ts                      |   6 +-
 10 files changed, 326 insertions(+), 123 deletions(-)

Fusion-Task-Id: FN-6219
Fusion-Task-Lineage: c3ef6d91-234f-4ed1-97c7-9be8408cd86c
This commit is contained in:
gsxdsm
2026-06-11 00:14:43 -07:00
parent 28e74c74db
commit 4ea9d66b4f
10 changed files with 336 additions and 133 deletions

View File

@@ -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.

View File

@@ -119,6 +119,46 @@ describe("model-resolution", () => {
).toEqual({ provider: "openai", modelId: "gpt-4.1" }); ).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", () => { it("forces every lane to mock when testMode is true", () => {
const settings = { const settings = {
testMode: true, testMode: true,

View File

@@ -4,6 +4,7 @@ import {
resolveHeartbeatSessionModels, resolveHeartbeatSessionModels,
resolveMergerSessionModel, resolveMergerSessionModel,
resolvePlanningSessionModel, resolvePlanningSessionModel,
resolveValidatorSessionModel,
} from "../agent-session-helpers.js"; } from "../agent-session-helpers.js";
const assignedAgentRuntimeConfig = { const assignedAgentRuntimeConfig = {
@@ -33,6 +34,10 @@ describe("agent-session-helpers test mode overrides", () => {
provider: "mock", provider: "mock",
modelId: "scripted", modelId: "scripted",
}); });
expect(resolveValidatorSessionModel("openai", "gpt-4.1", settings, assignedAgentRuntimeConfig)).toEqual({
provider: "mock",
modelId: "scripted",
});
expect(resolveMergerSessionModel(settings, assignedAgentRuntimeConfig)).toEqual({ expect(resolveMergerSessionModel(settings, assignedAgentRuntimeConfig)).toEqual({
provider: "mock", provider: "mock",
modelId: "scripted", modelId: "scripted",
@@ -65,6 +70,10 @@ describe("agent-session-helpers test mode overrides", () => {
provider: "mock", provider: "mock",
modelId: "scripted", modelId: "scripted",
}); });
expect(resolveValidatorSessionModel("openai", "gpt-4.1", settings, assignedAgentRuntimeConfig)).toEqual({
provider: "mock",
modelId: "scripted",
});
expect(resolveMergerSessionModel(settings, assignedAgentRuntimeConfig)).toEqual({ expect(resolveMergerSessionModel(settings, assignedAgentRuntimeConfig)).toEqual({
provider: "mock", provider: "mock",
modelId: "scripted", 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 = { const settings = {
executionProvider: "openai", executionProvider: "openai",
executionModelId: "gpt-4.1", executionModelId: "gpt-4.1",
@@ -90,22 +99,26 @@ describe("agent-session-helpers test mode overrides", () => {
}; };
expect(resolveExecutorSessionModel("task-provider", "task-model", settings, assignedAgentRuntimeConfig)).toEqual({ expect(resolveExecutorSessionModel("task-provider", "task-model", settings, assignedAgentRuntimeConfig)).toEqual({
provider: "anthropic", provider: "task-provider",
modelId: "claude-sonnet-4-5", modelId: "task-model",
}); });
expect(resolvePlanningSessionModel("task-provider", "task-model", settings, assignedAgentRuntimeConfig)).toEqual({ expect(resolvePlanningSessionModel("task-provider", "task-model", settings, assignedAgentRuntimeConfig)).toEqual({
provider: "anthropic", provider: "task-provider",
modelId: "claude-sonnet-4-5", modelId: "task-model",
});
expect(resolveValidatorSessionModel("task-provider", "task-model", settings, assignedAgentRuntimeConfig)).toEqual({
provider: "task-provider",
modelId: "task-model",
}); });
expect(resolveMergerSessionModel(settings, assignedAgentRuntimeConfig)).toEqual({ expect(resolveMergerSessionModel(settings, assignedAgentRuntimeConfig)).toEqual({
provider: "anthropic", provider: "openai",
modelId: "claude-sonnet-4-5", modelId: "gpt-4.1",
}); });
expect(resolveHeartbeatSessionModels(settings, assignedAgentRuntimeConfig)).toEqual({ expect(resolveHeartbeatSessionModels(settings, assignedAgentRuntimeConfig)).toEqual({
defaultProvider: "anthropic", defaultProvider: "openai",
defaultModelId: "claude-sonnet-4-5", defaultModelId: "gpt-4.1",
fallbackProvider: "openai", fallbackProvider: undefined,
fallbackModelId: "gpt-4.1", fallbackModelId: undefined,
}); });
}); });
}); });

View File

@@ -1,8 +1,12 @@
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { import {
extractRuntimeHint, extractRuntimeHint,
extractRuntimeModel,
resolveExecutorSessionModel,
resolveHeartbeatSessionModels, resolveHeartbeatSessionModels,
resolveMergerSessionModel, resolveMergerSessionModel,
resolvePlanningSessionModel,
resolveValidatorSessionModel,
} from "../agent-session-helpers.js"; } from "../agent-session-helpers.js";
const { resolveRuntimeMock } = vi.hoisted(() => ({ const { resolveRuntimeMock } = vi.hoisted(() => ({
@@ -39,49 +43,199 @@ describe("extractRuntimeHint", () => {
}); });
}); });
describe("resolveHeartbeatSessionModels", () => { describe("extractRuntimeModel", () => {
it("uses agent runtime model as primary and execution settings as fallback", () => { 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( expect(resolveHeartbeatSessionModels(
{ {
executionProvider: "openai", executionProvider: "openai",
executionModelId: "gpt-4.1", executionModelId: "gpt-4.1",
}, },
{ model: "anthropic/claude-sonnet-4-5" }, { modelProvider: "stale-provider" },
)).toEqual({ )).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", defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-4-5", defaultModelId: "claude-sonnet-4-5",
fallbackProvider: "openai", }, staleRuntimeConfig)).toEqual({ provider: "google", modelId: "gemini-2.5-pro" });
fallbackModelId: "gpt-4.1", 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", () => { it("uses a complete runtime model only when no lane/task/default model is configured", () => {
expect(resolveHeartbeatSessionModels( const runtimeConfig = { modelProvider: "anthropic", modelId: "claude-opus-4" };
{
executionProvider: "openai", expect(resolveExecutorSessionModel(undefined, undefined, undefined, runtimeConfig)).toEqual({
executionModelId: "gpt-4.1", provider: "anthropic",
}, modelId: "claude-opus-4",
{}, });
)).toEqual({ expect(resolvePlanningSessionModel(undefined, undefined, undefined, runtimeConfig)).toEqual({
defaultProvider: "openai", provider: "anthropic",
defaultModelId: "gpt-4.1", modelId: "claude-opus-4",
});
expect(resolveHeartbeatSessionModels(undefined, runtimeConfig)).toEqual({
defaultProvider: "anthropic",
defaultModelId: "claude-opus-4",
fallbackProvider: undefined, fallbackProvider: undefined,
fallbackModelId: 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", () => { it("covers backend-only surfaces; desktop and mobile breakpoints are not applicable", () => {
expect(resolveHeartbeatSessionModels( expect(resolveExecutorSessionModel(undefined, undefined, settings, { model: "stale/old" })).toEqual({
{ provider: "openai",
executionProvider: "openai", modelId: "gpt-4.1",
executionModelId: "gpt-4.1", });
}, expect(resolvePlanningSessionModel(undefined, undefined, settings, { model: "stale/old" })).toEqual({
{ modelProvider: "openai", modelId: "gpt-4.1" }, provider: "anthropic",
)).toEqual({ modelId: "claude-sonnet-4-5",
defaultProvider: "openai", });
defaultModelId: "gpt-4.1", expect(resolveValidatorSessionModel(undefined, undefined, settings, { model: "stale/old" })).toEqual({
fallbackProvider: undefined, provider: "google",
fallbackModelId: undefined, 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", () => { 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( expect(
resolveMergerSessionModel( resolveMergerSessionModel(
{ {},
defaultProviderOverride: "openai",
defaultModelIdOverride: "gpt-4.1",
defaultProvider: "anthropic",
defaultModelId: "claude-3-5-sonnet",
},
{ model: " anthropic/claude-3-5-sonnet-20241022 " }, { model: " anthropic/claude-3-5-sonnet-20241022 " },
), ),
).toEqual({ ).toEqual({

View File

@@ -3001,7 +3001,7 @@ describe("executeHeartbeat", () => {
expect(taskLogTool.name).toBe("fn_task_log"); 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({ const store = createStoreWithAgentForExec({
runtimeConfig: { model: "anthropic/claude-sonnet-4-5" }, runtimeConfig: { model: "anthropic/claude-sonnet-4-5" },
}); });
@@ -3022,10 +3022,10 @@ describe("executeHeartbeat", () => {
expect(mockedCreateFnAgent).toHaveBeenCalledOnce(); expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]; const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
expect(callArgs.defaultProvider).toBe("anthropic"); expect(callArgs.defaultProvider).toBe("openai");
expect(callArgs.defaultModelId).toBe("claude-sonnet-4-5"); expect(callArgs.defaultModelId).toBe("gpt-4.1");
expect(callArgs.fallbackProvider).toBe("openai"); expect(callArgs.fallbackProvider).toBeUndefined();
expect(callArgs.fallbackModelId).toBe("gpt-4.1"); expect(callArgs.fallbackModelId).toBeUndefined();
}); });
it("passes undefined model when runtimeConfig has no model", async () => { it("passes undefined model when runtimeConfig has no model", async () => {

View File

@@ -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" }); const feature = createMockFeature({ loopState: "implementing", taskId: "FN-MODEL-AGENT", status: "in-progress" });
missionStore._setFeature(feature); missionStore._setFeature(feature);
missionStore.getFeatureByTaskId = vi.fn().mockReturnValue(feature); missionStore.getFeatureByTaskId = vi.fn().mockReturnValue(feature);
@@ -1185,8 +1185,8 @@ describe("MissionExecutionLoop", () => {
expect(createResolvedAgentSession).toHaveBeenCalledWith(expect.objectContaining({ expect(createResolvedAgentSession).toHaveBeenCalledWith(expect.objectContaining({
sessionPurpose: "validation", sessionPurpose: "validation",
defaultProvider: "agent-provider", defaultProvider: "task-validator",
defaultModelId: "agent-model", defaultModelId: "task-validator-model",
})); }));
}); });

View File

@@ -3084,7 +3084,7 @@ describe("taskCreate tool model inheritance", () => {
expect(capturedArgs.systemPrompt).toContain("agent ID: agent-007"); 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 completeRuntimeTask = createTriageTask({ id: "FN-AGENT-MODEL-1", assignedAgentId: "agent-model-complete" });
const incompleteRuntimeTask = createTriageTask({ id: "FN-AGENT-MODEL-2", assignedAgentId: "agent-model-incomplete" }); 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 completeCall = capturedArgs.find((entry) => entry.taskId === "FN-AGENT-MODEL-1");
const fallbackCall = capturedArgs.find((entry) => entry.taskId === "FN-AGENT-MODEL-2"); 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" }); expect(fallbackCall).toMatchObject({ defaultProvider: "openai", defaultModelId: "gpt-4o" });
}); });

View File

@@ -14,9 +14,12 @@ import type { AgentSession } from "@earendil-works/pi-coding-agent";
import { import {
isTestModeActive, isTestModeActive,
resolveExecutionSettingsModel, resolveExecutionSettingsModel,
resolveProjectDefaultModel,
resolveTaskExecutionModel, resolveTaskExecutionModel,
resolveTaskPlanningModel, resolveTaskPlanningModel,
resolveTaskValidatorModel,
TEST_MODE_RESOLVED, TEST_MODE_RESOLVED,
type ResolvedModelSelection,
type Settings, type Settings,
} from "@fusion/core"; } from "@fusion/core";
import { resolveRuntime, buildRuntimeResolutionContext, isMockProviderId, type SessionPurpose } from "./runtime-resolution.js"; 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<string, unknown>,
): { 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( export function resolveExecutorSessionModel(
taskModelProvider: string | undefined, taskModelProvider: string | undefined,
taskModelId: 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( const resolvedTaskModel = resolveTaskExecutionModel(
{ {
modelProvider: taskModelProvider, modelProvider: taskModelProvider,
@@ -157,10 +181,7 @@ export function resolveExecutorSessionModel(
settings, settings,
); );
return { return pickSettingsThenRuntimeModel(resolvedTaskModel, assignedAgentRuntimeConfig);
provider: resolvedTaskModel.provider,
modelId: resolvedTaskModel.modelId,
};
} }
export function resolvePlanningSessionModel( export function resolvePlanningSessionModel(
@@ -176,11 +197,6 @@ export function resolvePlanningSessionModel(
}; };
} }
const assignedRuntimeModel = extractRuntimeModel(assignedAgentRuntimeConfig);
if (assignedRuntimeModel.provider && assignedRuntimeModel.modelId) {
return assignedRuntimeModel;
}
const resolvedTaskPlanningModel = resolveTaskPlanningModel( const resolvedTaskPlanningModel = resolveTaskPlanningModel(
{ {
planningModelProvider: taskPlanningModelProvider, planningModelProvider: taskPlanningModelProvider,
@@ -189,10 +205,31 @@ export function resolvePlanningSessionModel(
settings, settings,
); );
return { return pickSettingsThenRuntimeModel(resolvedTaskPlanningModel, assignedAgentRuntimeConfig);
provider: resolvedTaskPlanningModel.provider, }
modelId: resolvedTaskPlanningModel.modelId,
}; export function resolveValidatorSessionModel(
taskValidatorModelProvider: string | undefined,
taskValidatorModelId: string | undefined,
settings: Partial<Settings> | undefined,
assignedAgentRuntimeConfig?: Record<string, unknown>,
): { 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( export function resolveHeartbeatSessionModels(
@@ -213,21 +250,14 @@ export function resolveHeartbeatSessionModels(
}; };
} }
const assignedRuntimeModel = extractRuntimeModel(assignedAgentRuntimeConfig);
const executionSettingsModel = resolveExecutionSettingsModel(settings); const executionSettingsModel = resolveExecutionSettingsModel(settings);
const resolvedModel = pickSettingsThenRuntimeModel(executionSettingsModel, assignedAgentRuntimeConfig);
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;
return { return {
defaultProvider, defaultProvider: resolvedModel.provider,
defaultModelId, defaultModelId: resolvedModel.modelId,
fallbackProvider: executionPairAvailable && !defaultMatchesExecution ? executionSettingsModel.provider : undefined, fallbackProvider: undefined,
fallbackModelId: executionPairAvailable && !defaultMatchesExecution ? executionSettingsModel.modelId : undefined, fallbackModelId: undefined,
}; };
} }
@@ -242,22 +272,11 @@ export function resolveMergerSessionModel(
}; };
} }
const assignedRuntimeModel = extractRuntimeModel(assignedAgentRuntimeConfig); // Merger intentionally uses the default lane rather than execution/validator
if (assignedRuntimeModel.provider && assignedRuntimeModel.modelId) { // lanes. Validator-specific callers resolve `resolveValidatorSettingsModel`
return assignedRuntimeModel; // before falling back here; generic merger work uses project/global defaults.
} const defaultModel = resolveProjectDefaultModel(settings);
return pickSettingsThenRuntimeModel(defaultModel, assignedAgentRuntimeConfig);
if (settings?.defaultProviderOverride && settings.defaultModelIdOverride) {
return {
provider: settings.defaultProviderOverride,
modelId: settings.defaultModelIdOverride,
};
}
return {
provider: settings?.defaultProvider,
modelId: settings?.defaultModelId,
};
} }
/** /**

View File

@@ -22,17 +22,12 @@ import type {
Settings, Settings,
Milestone, Milestone,
} from "@fusion/core"; } from "@fusion/core";
import {
TEST_MODE_RESOLVED,
isTestModeActive,
resolveTaskValidatorModel,
} from "@fusion/core";
import { createFnAgent, promptWithFallback, type AgentResult } from "./pi.js"; import { createFnAgent, promptWithFallback, type AgentResult } from "./pi.js";
import { mergeEffectiveSettings } from "./effective-settings.js"; import { mergeEffectiveSettings } from "./effective-settings.js";
import { import {
createResolvedAgentSession, createResolvedAgentSession,
extractRuntimeHint, extractRuntimeHint,
extractRuntimeModel, resolveValidatorSessionModel,
} from "./agent-session-helpers.js"; } from "./agent-session-helpers.js";
import { createLogger } from "./logger.js"; import { createLogger } from "./logger.js";
import { createFallbackModelObserver } from "./fallback-model-observer.js"; import { createFallbackModelObserver } from "./fallback-model-observer.js";
@@ -573,30 +568,12 @@ export class MissionExecutionLoop extends EventEmitter {
settings: Partial<Settings> | undefined, settings: Partial<Settings> | undefined,
assignedAgentRuntimeConfig?: Record<string, unknown>, assignedAgentRuntimeConfig?: Record<string, unknown>,
): { provider: string | undefined; modelId: string | undefined } { ): { provider: string | undefined; modelId: string | undefined } {
if (isTestModeActive(settings)) { return resolveValidatorSessionModel(
return { task?.validatorModelProvider,
provider: TEST_MODE_RESOLVED.provider, task?.validatorModelId,
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,
},
settings, settings,
assignedAgentRuntimeConfig,
); );
return {
provider: resolvedTaskModel.provider,
modelId: resolvedTaskModel.modelId,
};
} }
/** /**

View File

@@ -1335,9 +1335,9 @@ export class TriageProcessor {
}); });
// Resolve planning model using executor-style precedence: // Resolve planning model using executor-style precedence:
// 1. Assigned durable agent runtime model pair when complete // 1. Task planning override pair
// 2. Task planning override pair // 2. Planning/project/global fallbacks
// 3. Planning/project/global fallbacks // 3. Assigned durable agent runtime model pair when no fresh model pair exists
const planningModel = resolvePlanningSessionModel( const planningModel = resolvePlanningSessionModel(
task.planningModelProvider, task.planningModelProvider,
task.planningModelId, task.planningModelId,