feat(FN-3892): honor assigned runtime model for merger sessions
- Add resolveMergerSessionModel helper to centralize merger model selection precedence - Route merger session creation paths through the helper for commit, push/rebase, and autostash recovery flows - Add comprehensive engine tests for merger model resolution and fallback behavior - Document merger model hierarchy in settings reference and add a patch changeset for @runfusion/fusion Fusion-Task-Id: FN-3892
This commit is contained in:
5
.changeset/FN-3892-merger-runtime-model.md
Normal file
5
.changeset/FN-3892-merger-runtime-model.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Merger sessions now honor the assigned agent's `runtimeConfig.model` before falling back to project/global defaults, matching executor and planning lanes.
|
||||||
@@ -493,9 +493,10 @@ Fusion uses a dual-scope model settings system with five lanes. Global settings
|
|||||||
|
|
||||||
### Merger model
|
### Merger model
|
||||||
|
|
||||||
1. Project `defaultProviderOverride` + `defaultModelIdOverride`
|
1. Assigned durable agent runtime model (`runtimeConfig.model` or `runtimeConfig.modelProvider` + `runtimeConfig.modelId`) when both provider and model ID are set
|
||||||
2. Global `defaultProvider` + `defaultModelId`
|
2. Project `defaultProviderOverride` + `defaultModelIdOverride`
|
||||||
3. Automatic provider/model resolution
|
3. Global `defaultProvider` + `defaultModelId`
|
||||||
|
4. Automatic provider/model resolution
|
||||||
|
|
||||||
### Title summarization model
|
### Title summarization model
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { extractRuntimeHint } from "../agent-session-helpers.js";
|
import { extractRuntimeHint, resolveMergerSessionModel } from "../agent-session-helpers.js";
|
||||||
|
|
||||||
describe("extractRuntimeHint", () => {
|
describe("extractRuntimeHint", () => {
|
||||||
it("returns undefined for undefined config", () => {
|
it("returns undefined for undefined config", () => {
|
||||||
@@ -22,3 +22,92 @@ describe("extractRuntimeHint", () => {
|
|||||||
expect(extractRuntimeHint({ runtimeHint: 42 })).toBeUndefined();
|
expect(extractRuntimeHint({ runtimeHint: 42 })).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("resolveMergerSessionModel", () => {
|
||||||
|
it("uses assigned agent runtime model when both provider and modelId are present", () => {
|
||||||
|
expect(
|
||||||
|
resolveMergerSessionModel(
|
||||||
|
{
|
||||||
|
defaultProviderOverride: "openai",
|
||||||
|
defaultModelIdOverride: "gpt-4.1",
|
||||||
|
defaultProvider: "anthropic",
|
||||||
|
defaultModelId: "claude-3-5-sonnet",
|
||||||
|
},
|
||||||
|
{ model: " anthropic/claude-3-5-sonnet-20241022 " },
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
provider: "anthropic",
|
||||||
|
modelId: "claude-3-5-sonnet-20241022",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to default override pair when runtime model is not fully specified", () => {
|
||||||
|
expect(
|
||||||
|
resolveMergerSessionModel(
|
||||||
|
{
|
||||||
|
defaultProviderOverride: "openai",
|
||||||
|
defaultModelIdOverride: "gpt-4.1",
|
||||||
|
defaultProvider: "anthropic",
|
||||||
|
defaultModelId: "claude-3-5-sonnet",
|
||||||
|
},
|
||||||
|
{ modelProvider: "anthropic" },
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
provider: "openai",
|
||||||
|
modelId: "gpt-4.1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to global defaults when no override pair is configured", () => {
|
||||||
|
expect(
|
||||||
|
resolveMergerSessionModel(
|
||||||
|
{
|
||||||
|
defaultProvider: "anthropic",
|
||||||
|
defaultModelId: "claude-3-5-sonnet",
|
||||||
|
},
|
||||||
|
{ modelId: "claude-3-opus" },
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
provider: "anthropic",
|
||||||
|
modelId: "claude-3-5-sonnet",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores partial override pairs and falls back to global defaults", () => {
|
||||||
|
expect(
|
||||||
|
resolveMergerSessionModel({
|
||||||
|
defaultProviderOverride: "openai",
|
||||||
|
defaultProvider: "anthropic",
|
||||||
|
defaultModelId: "claude-3-5-sonnet",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
provider: "anthropic",
|
||||||
|
modelId: "claude-3-5-sonnet",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveMergerSessionModel({
|
||||||
|
defaultModelIdOverride: "gpt-4.1",
|
||||||
|
defaultProvider: "anthropic",
|
||||||
|
defaultModelId: "claude-3-5-sonnet",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
provider: "anthropic",
|
||||||
|
modelId: "claude-3-5-sonnet",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("works when assignedAgentRuntimeConfig is undefined", () => {
|
||||||
|
expect(
|
||||||
|
resolveMergerSessionModel({
|
||||||
|
defaultProviderOverride: "openai",
|
||||||
|
defaultModelIdOverride: "gpt-4.1",
|
||||||
|
defaultProvider: "anthropic",
|
||||||
|
defaultModelId: "claude-3-5-sonnet",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
provider: "openai",
|
||||||
|
modelId: "gpt-4.1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -163,6 +163,28 @@ export function resolvePlanningSessionModel(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveMergerSessionModel(
|
||||||
|
settings: Partial<Settings> | undefined,
|
||||||
|
assignedAgentRuntimeConfig?: Record<string, unknown>,
|
||||||
|
): { provider: string | undefined; modelId: string | undefined } {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an agent session using runtime resolution.
|
* Create an agent session using runtime resolution.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ import {
|
|||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
import { describeModel, promptWithFallback } from "./pi.js";
|
import { describeModel, promptWithFallback } from "./pi.js";
|
||||||
import { accumulateSessionTokenUsage } from "./session-token-usage.js";
|
import { accumulateSessionTokenUsage } from "./session-token-usage.js";
|
||||||
import { createResolvedAgentSession, extractRuntimeHint } from "./agent-session-helpers.js";
|
import { createResolvedAgentSession, extractRuntimeHint, resolveMergerSessionModel } from "./agent-session-helpers.js";
|
||||||
import { createFallbackModelObserver } from "./fallback-model-observer.js";
|
import { createFallbackModelObserver } from "./fallback-model-observer.js";
|
||||||
import { buildSessionSkillContext } from "./session-skill-context.js";
|
import { buildSessionSkillContext } from "./session-skill-context.js";
|
||||||
import type { WorktreePool } from "./worktree-pool.js";
|
import type { WorktreePool } from "./worktree-pool.js";
|
||||||
@@ -820,6 +820,7 @@ async function attemptInMergeVerificationFix(
|
|||||||
? await agentStoreWithGetAgent.getAgent(assignedAgentId).catch(() => null)
|
? await agentStoreWithGetAgent.getAgent(assignedAgentId).catch(() => null)
|
||||||
: null;
|
: null;
|
||||||
const mergerRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
const mergerRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
||||||
|
const mergerSessionModel = resolveMergerSessionModel(settings, assignedAgent?.runtimeConfig);
|
||||||
const { session } = await createResolvedAgentSession({
|
const { session } = await createResolvedAgentSession({
|
||||||
sessionPurpose: "merger",
|
sessionPurpose: "merger",
|
||||||
runtimeHint: mergerRuntimeHint,
|
runtimeHint: mergerRuntimeHint,
|
||||||
@@ -846,12 +847,8 @@ Do not refactor, rename broadly, or make opportunistic improvements.
|
|||||||
onThinking: logger.onThinking,
|
onThinking: logger.onThinking,
|
||||||
onToolStart: logger.onToolStart,
|
onToolStart: logger.onToolStart,
|
||||||
onToolEnd: logger.onToolEnd,
|
onToolEnd: logger.onToolEnd,
|
||||||
defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride
|
defaultProvider: mergerSessionModel.provider,
|
||||||
? settings.defaultProviderOverride
|
defaultModelId: mergerSessionModel.modelId,
|
||||||
: settings.defaultProvider,
|
|
||||||
defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride
|
|
||||||
? settings.defaultModelIdOverride
|
|
||||||
: settings.defaultModelId,
|
|
||||||
fallbackProvider: settings.fallbackProvider,
|
fallbackProvider: settings.fallbackProvider,
|
||||||
fallbackModelId: settings.fallbackModelId,
|
fallbackModelId: settings.fallbackModelId,
|
||||||
defaultThinkingLevel: settings.defaultThinkingLevel,
|
defaultThinkingLevel: settings.defaultThinkingLevel,
|
||||||
@@ -1919,6 +1916,7 @@ async function runAiAgentForAutostashConflict(params: {
|
|||||||
? await agentStoreWithGetAgent.getAgent(assignedAgentId).catch(() => null)
|
? await agentStoreWithGetAgent.getAgent(assignedAgentId).catch(() => null)
|
||||||
: null;
|
: null;
|
||||||
const mergerRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
const mergerRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
||||||
|
const mergerSessionModel = resolveMergerSessionModel(settings, assignedAgent?.runtimeConfig);
|
||||||
|
|
||||||
const systemPrompt = `You are an autostash-conflict resolution agent running after a Fusion merge has already committed on the main branch.
|
const systemPrompt = `You are an autostash-conflict resolution agent running after a Fusion merge has already committed on the main branch.
|
||||||
|
|
||||||
@@ -1962,12 +1960,8 @@ ${fileList}
|
|||||||
onThinking: agentLogger.onThinking,
|
onThinking: agentLogger.onThinking,
|
||||||
onToolStart: agentLogger.onToolStart,
|
onToolStart: agentLogger.onToolStart,
|
||||||
onToolEnd: agentLogger.onToolEnd,
|
onToolEnd: agentLogger.onToolEnd,
|
||||||
defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride
|
defaultProvider: mergerSessionModel.provider,
|
||||||
? settings.defaultProviderOverride
|
defaultModelId: mergerSessionModel.modelId,
|
||||||
: settings.defaultProvider,
|
|
||||||
defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride
|
|
||||||
? settings.defaultModelIdOverride
|
|
||||||
: settings.defaultModelId,
|
|
||||||
fallbackProvider: settings.fallbackProvider,
|
fallbackProvider: settings.fallbackProvider,
|
||||||
fallbackModelId: settings.fallbackModelId,
|
fallbackModelId: settings.fallbackModelId,
|
||||||
defaultThinkingLevel: settings.defaultThinkingLevel,
|
defaultThinkingLevel: settings.defaultThinkingLevel,
|
||||||
@@ -2290,6 +2284,7 @@ async function runAiAgentForAutostashHardFail(params: {
|
|||||||
? await agentStoreWithGetAgent.getAgent(assignedAgentId).catch(() => null)
|
? await agentStoreWithGetAgent.getAgent(assignedAgentId).catch(() => null)
|
||||||
: null;
|
: null;
|
||||||
const mergerRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
const mergerRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
||||||
|
const mergerSessionModel = resolveMergerSessionModel(settings, assignedAgent?.runtimeConfig);
|
||||||
|
|
||||||
const systemPrompt = `You are an autostash hard-failure recovery agent for the Fusion merger.
|
const systemPrompt = `You are an autostash hard-failure recovery agent for the Fusion merger.
|
||||||
|
|
||||||
@@ -2343,12 +2338,8 @@ ${fileList}
|
|||||||
onThinking: agentLogger.onThinking,
|
onThinking: agentLogger.onThinking,
|
||||||
onToolStart: agentLogger.onToolStart,
|
onToolStart: agentLogger.onToolStart,
|
||||||
onToolEnd: agentLogger.onToolEnd,
|
onToolEnd: agentLogger.onToolEnd,
|
||||||
defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride
|
defaultProvider: mergerSessionModel.provider,
|
||||||
? settings.defaultProviderOverride
|
defaultModelId: mergerSessionModel.modelId,
|
||||||
: settings.defaultProvider,
|
|
||||||
defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride
|
|
||||||
? settings.defaultModelIdOverride
|
|
||||||
: settings.defaultModelId,
|
|
||||||
fallbackProvider: settings.fallbackProvider,
|
fallbackProvider: settings.fallbackProvider,
|
||||||
fallbackModelId: settings.fallbackModelId,
|
fallbackModelId: settings.fallbackModelId,
|
||||||
defaultThinkingLevel: settings.defaultThinkingLevel,
|
defaultThinkingLevel: settings.defaultThinkingLevel,
|
||||||
@@ -4143,6 +4134,7 @@ async function resolveComplexRebaseConflictsWithAi(
|
|||||||
pluginRunner?: import("./plugin-runner.js").PluginRunner;
|
pluginRunner?: import("./plugin-runner.js").PluginRunner;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
runtimeHint?: string;
|
runtimeHint?: string;
|
||||||
|
assignedAgentRuntimeConfig?: Record<string, unknown>;
|
||||||
onSession?: (session: { dispose: () => void }) => void;
|
onSession?: (session: { dispose: () => void }) => void;
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -4171,6 +4163,7 @@ You are assisting with a paused \`git pull --rebase\`.
|
|||||||
});
|
});
|
||||||
|
|
||||||
throwIfAborted(options?.signal, taskId);
|
throwIfAborted(options?.signal, taskId);
|
||||||
|
const mergerSessionModel = resolveMergerSessionModel(settings, options?.assignedAgentRuntimeConfig);
|
||||||
const { session } = await createResolvedAgentSession({
|
const { session } = await createResolvedAgentSession({
|
||||||
sessionPurpose: "merger",
|
sessionPurpose: "merger",
|
||||||
runtimeHint: options?.runtimeHint,
|
runtimeHint: options?.runtimeHint,
|
||||||
@@ -4182,12 +4175,8 @@ You are assisting with a paused \`git pull --rebase\`.
|
|||||||
onThinking: agentLogger.onThinking,
|
onThinking: agentLogger.onThinking,
|
||||||
onToolStart: agentLogger.onToolStart,
|
onToolStart: agentLogger.onToolStart,
|
||||||
onToolEnd: agentLogger.onToolEnd,
|
onToolEnd: agentLogger.onToolEnd,
|
||||||
defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride
|
defaultProvider: mergerSessionModel.provider,
|
||||||
? settings.defaultProviderOverride
|
defaultModelId: mergerSessionModel.modelId,
|
||||||
: settings.defaultProvider,
|
|
||||||
defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride
|
|
||||||
? settings.defaultModelIdOverride
|
|
||||||
: settings.defaultModelId,
|
|
||||||
fallbackProvider: settings.fallbackProvider,
|
fallbackProvider: settings.fallbackProvider,
|
||||||
fallbackModelId: settings.fallbackModelId,
|
fallbackModelId: settings.fallbackModelId,
|
||||||
defaultThinkingLevel: settings.defaultThinkingLevel,
|
defaultThinkingLevel: settings.defaultThinkingLevel,
|
||||||
@@ -4241,6 +4230,7 @@ async function resolveRebaseConflictSet(
|
|||||||
onAgentText?: (delta: string) => void;
|
onAgentText?: (delta: string) => void;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
runtimeHint?: string;
|
runtimeHint?: string;
|
||||||
|
assignedAgentRuntimeConfig?: Record<string, unknown>;
|
||||||
onSession?: (session: { dispose: () => void }) => void;
|
onSession?: (session: { dispose: () => void }) => void;
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -4289,6 +4279,7 @@ async function pullWithRebaseAndResolveConflicts(
|
|||||||
onAgentText?: (delta: string) => void;
|
onAgentText?: (delta: string) => void;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
runtimeHint?: string;
|
runtimeHint?: string;
|
||||||
|
assignedAgentRuntimeConfig?: Record<string, unknown>;
|
||||||
onSession?: (session: { dispose: () => void }) => void;
|
onSession?: (session: { dispose: () => void }) => void;
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -4385,6 +4376,7 @@ export async function pushToRemoteAfterMerge(
|
|||||||
onAgentText?: (delta: string) => void;
|
onAgentText?: (delta: string) => void;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
runtimeHint?: string;
|
runtimeHint?: string;
|
||||||
|
assignedAgentRuntimeConfig?: Record<string, unknown>;
|
||||||
onSession?: (session: { dispose: () => void }) => void;
|
onSession?: (session: { dispose: () => void }) => void;
|
||||||
},
|
},
|
||||||
): Promise<{ pushed: boolean; error?: string }> {
|
): Promise<{ pushed: boolean; error?: string }> {
|
||||||
@@ -5891,6 +5883,7 @@ export async function aiMergeTask(
|
|||||||
onAgentText: options.onAgentText,
|
onAgentText: options.onAgentText,
|
||||||
signal: options.signal,
|
signal: options.signal,
|
||||||
runtimeHint: pushRuntimeHint,
|
runtimeHint: pushRuntimeHint,
|
||||||
|
assignedAgentRuntimeConfig: pushAssignedAgent?.runtimeConfig,
|
||||||
onSession: options.onSession,
|
onSession: options.onSession,
|
||||||
});
|
});
|
||||||
if (pushResult.pushed) {
|
if (pushResult.pushed) {
|
||||||
@@ -6833,6 +6826,7 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo
|
|||||||
? await agentStoreWithGetAgent.getAgent(assignedAgentId).catch(() => null)
|
? await agentStoreWithGetAgent.getAgent(assignedAgentId).catch(() => null)
|
||||||
: null;
|
: null;
|
||||||
const mergerRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
const mergerRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
||||||
|
const mergerSessionModel = resolveMergerSessionModel(settings, assignedAgent?.runtimeConfig);
|
||||||
|
|
||||||
const { session } = await createResolvedAgentSession({
|
const { session } = await createResolvedAgentSession({
|
||||||
sessionPurpose: "merger",
|
sessionPurpose: "merger",
|
||||||
@@ -6846,12 +6840,8 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo
|
|||||||
onThinking: agentLogger.onThinking,
|
onThinking: agentLogger.onThinking,
|
||||||
onToolStart: agentLogger.onToolStart,
|
onToolStart: agentLogger.onToolStart,
|
||||||
onToolEnd: agentLogger.onToolEnd,
|
onToolEnd: agentLogger.onToolEnd,
|
||||||
defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride
|
defaultProvider: mergerSessionModel.provider,
|
||||||
? settings.defaultProviderOverride
|
defaultModelId: mergerSessionModel.modelId,
|
||||||
: settings.defaultProvider,
|
|
||||||
defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride
|
|
||||||
? settings.defaultModelIdOverride
|
|
||||||
: settings.defaultModelId,
|
|
||||||
fallbackProvider: settings.fallbackProvider,
|
fallbackProvider: settings.fallbackProvider,
|
||||||
fallbackModelId: settings.fallbackModelId,
|
fallbackModelId: settings.fallbackModelId,
|
||||||
defaultThinkingLevel: settings.defaultThinkingLevel,
|
defaultThinkingLevel: settings.defaultThinkingLevel,
|
||||||
|
|||||||
Reference in New Issue
Block a user