feat(FN-1678): merge fusion/fn-1678
This commit is contained in:
@@ -5655,6 +5655,269 @@ describe("Per-task model overrides", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Lane hierarchy model resolution tests ─────────────────────────────────────
|
||||
|
||||
describe("Executor lane hierarchy model resolution", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("resolves task override when both provider and modelId are set", async () => {
|
||||
const store = createMockStore();
|
||||
const capturedOptions: any[] = [];
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
capturedOptions.push(opts);
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
state: {},
|
||||
},
|
||||
} as any;
|
||||
});
|
||||
|
||||
store.getSettings.mockResolvedValue({
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 15000,
|
||||
defaultProvider: "openai",
|
||||
defaultModelId: "gpt-4o",
|
||||
executionGlobalProvider: "google",
|
||||
executionGlobalModelId: "gemini-2.5",
|
||||
executionProvider: undefined,
|
||||
executionModelId: undefined,
|
||||
});
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
|
||||
store.getTask.mockResolvedValue({
|
||||
id: "FN-001",
|
||||
title: "Test",
|
||||
description: "Test task",
|
||||
column: "in-progress",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
prompt: "# test",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
});
|
||||
|
||||
await executor.execute({
|
||||
id: "FN-001",
|
||||
title: "Test",
|
||||
description: "Test task",
|
||||
column: "in-progress",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
});
|
||||
|
||||
// Task override takes precedence
|
||||
expect(capturedOptions[0].defaultProvider).toBe("anthropic");
|
||||
expect(capturedOptions[0].defaultModelId).toBe("claude-sonnet-4-5");
|
||||
});
|
||||
|
||||
it("resolves project execution override when task override is not set", async () => {
|
||||
const store = createMockStore();
|
||||
const capturedOptions: any[] = [];
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
capturedOptions.push(opts);
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
state: {},
|
||||
},
|
||||
} as any;
|
||||
});
|
||||
|
||||
store.getSettings.mockResolvedValue({
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 15000,
|
||||
defaultProvider: "openai",
|
||||
defaultModelId: "gpt-4o",
|
||||
executionGlobalProvider: "google",
|
||||
executionGlobalModelId: "gemini-2.5",
|
||||
executionProvider: "anthropic",
|
||||
executionModelId: "claude-opus-4",
|
||||
});
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
|
||||
store.getTask.mockResolvedValue({
|
||||
id: "FN-001",
|
||||
title: "Test",
|
||||
description: "Test task",
|
||||
column: "in-progress",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
prompt: "# test",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
// No task-level model override
|
||||
});
|
||||
|
||||
await executor.execute({
|
||||
id: "FN-001",
|
||||
title: "Test",
|
||||
description: "Test task",
|
||||
column: "in-progress",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
// No task-level model override
|
||||
});
|
||||
|
||||
// Project execution override takes precedence over global lane
|
||||
expect(capturedOptions[0].defaultProvider).toBe("anthropic");
|
||||
expect(capturedOptions[0].defaultModelId).toBe("claude-opus-4");
|
||||
});
|
||||
|
||||
it("resolves global execution lane when project override is not set", async () => {
|
||||
const store = createMockStore();
|
||||
const capturedOptions: any[] = [];
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
capturedOptions.push(opts);
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
state: {},
|
||||
},
|
||||
} as any;
|
||||
});
|
||||
|
||||
store.getSettings.mockResolvedValue({
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 15000,
|
||||
defaultProvider: "openai",
|
||||
defaultModelId: "gpt-4o",
|
||||
executionGlobalProvider: "google",
|
||||
executionGlobalModelId: "gemini-2.5",
|
||||
executionProvider: undefined,
|
||||
executionModelId: undefined,
|
||||
});
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
|
||||
store.getTask.mockResolvedValue({
|
||||
id: "FN-001",
|
||||
title: "Test",
|
||||
description: "Test task",
|
||||
column: "in-progress",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
prompt: "# test",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
// No task-level model override
|
||||
});
|
||||
|
||||
await executor.execute({
|
||||
id: "FN-001",
|
||||
title: "Test",
|
||||
description: "Test task",
|
||||
column: "in-progress",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
// No task-level model override
|
||||
});
|
||||
|
||||
// Global execution lane takes precedence over default
|
||||
expect(capturedOptions[0].defaultProvider).toBe("google");
|
||||
expect(capturedOptions[0].defaultModelId).toBe("gemini-2.5");
|
||||
});
|
||||
|
||||
it("falls back to default when no lane overrides are set", async () => {
|
||||
const store = createMockStore();
|
||||
const capturedOptions: any[] = [];
|
||||
|
||||
mockedCreateHaiAgent.mockImplementation(async (opts: any) => {
|
||||
capturedOptions.push(opts);
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
state: {},
|
||||
},
|
||||
} as any;
|
||||
});
|
||||
|
||||
store.getSettings.mockResolvedValue({
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 15000,
|
||||
defaultProvider: "openai",
|
||||
defaultModelId: "gpt-4o",
|
||||
executionGlobalProvider: undefined,
|
||||
executionGlobalModelId: undefined,
|
||||
executionProvider: undefined,
|
||||
executionModelId: undefined,
|
||||
});
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
|
||||
store.getTask.mockResolvedValue({
|
||||
id: "FN-001",
|
||||
title: "Test",
|
||||
description: "Test task",
|
||||
column: "in-progress",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
prompt: "# test",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
// No task-level model override
|
||||
});
|
||||
|
||||
await executor.execute({
|
||||
id: "FN-001",
|
||||
title: "Test",
|
||||
description: "Test task",
|
||||
column: "in-progress",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
// No task-level model override
|
||||
});
|
||||
|
||||
// Default takes precedence when no lane overrides are set
|
||||
expect(capturedOptions[0].defaultProvider).toBe("openai");
|
||||
expect(capturedOptions[0].defaultModelId).toBe("gpt-4o");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Per-task thinkingLevel override tests ───────────────────────────
|
||||
|
||||
describe("Per-task thinkingLevel override", () => {
|
||||
|
||||
@@ -497,12 +497,21 @@ export class TaskExecutor {
|
||||
activeEntry.lastModelId = task.modelId;
|
||||
|
||||
const settings = await this.store.getSettings();
|
||||
// Resolve model using canonical lane hierarchy for hot-swap
|
||||
const newProvider = task.modelProvider && task.modelId
|
||||
? task.modelProvider
|
||||
: settings?.defaultProvider;
|
||||
: (settings?.executionProvider && settings?.executionModelId
|
||||
? settings.executionProvider
|
||||
: (settings?.executionGlobalProvider && settings?.executionGlobalModelId
|
||||
? settings.executionGlobalProvider
|
||||
: settings?.defaultProvider));
|
||||
const newModelId = task.modelProvider && task.modelId
|
||||
? task.modelId
|
||||
: settings?.defaultModelId;
|
||||
: (settings?.executionProvider && settings?.executionModelId
|
||||
? settings.executionModelId
|
||||
: (settings?.executionGlobalProvider && settings?.executionGlobalModelId
|
||||
? settings.executionGlobalModelId
|
||||
: settings?.defaultModelId));
|
||||
|
||||
if (newProvider && newModelId) {
|
||||
try {
|
||||
@@ -1414,14 +1423,25 @@ export class TaskExecutor {
|
||||
});
|
||||
|
||||
const agentWork = async () => {
|
||||
// Resolve model settings: use per-task overrides if both provider and modelId are set,
|
||||
// otherwise fall back to global settings
|
||||
// Resolve model settings using canonical lane hierarchy:
|
||||
// 1. Task override pair (modelProvider + modelId)
|
||||
// 2. Project execution override pair (executionProvider + executionModelId)
|
||||
// 3. Global execution lane pair (executionGlobalProvider + executionGlobalModelId)
|
||||
// 4. Default pair (defaultProvider + defaultModelId)
|
||||
const executorProvider = detail.modelProvider && detail.modelId
|
||||
? detail.modelProvider
|
||||
: settings.defaultProvider;
|
||||
: (settings.executionProvider && settings.executionModelId
|
||||
? settings.executionProvider
|
||||
: (settings.executionGlobalProvider && settings.executionGlobalModelId
|
||||
? settings.executionGlobalProvider
|
||||
: settings.defaultProvider));
|
||||
const executorModelId = detail.modelProvider && detail.modelId
|
||||
? detail.modelId
|
||||
: settings.defaultModelId;
|
||||
: (settings.executionProvider && settings.executionModelId
|
||||
? settings.executionModelId
|
||||
: (settings.executionGlobalProvider && settings.executionGlobalModelId
|
||||
? settings.executionGlobalModelId
|
||||
: settings.defaultModelId));
|
||||
const executorFallbackProvider = settings.fallbackProvider;
|
||||
const executorFallbackModelId = settings.fallbackModelId;
|
||||
const executorThinkingLevel = detail.thinkingLevel ?? settings.defaultThinkingLevel;
|
||||
@@ -2337,16 +2357,24 @@ export class TaskExecutor {
|
||||
reviewType, promptContent, baseline,
|
||||
{
|
||||
onText: (delta) => options.onAgentText?.(taskId, delta),
|
||||
// Execution defaults as final fallback
|
||||
defaultProvider: settings.defaultProvider,
|
||||
defaultModelId: settings.defaultModelId,
|
||||
fallbackProvider: settings.fallbackProvider,
|
||||
fallbackModelId: settings.fallbackModelId,
|
||||
defaultThinkingLevel: detail.thinkingLevel ?? settings.defaultThinkingLevel,
|
||||
// Per-task validator overrides take precedence over global validator settings
|
||||
validatorModelProvider: detail.validatorModelProvider ?? settings.validatorProvider,
|
||||
validatorModelId: detail.validatorModelId ?? settings.validatorModelId,
|
||||
validatorFallbackModelProvider: settings.validatorFallbackProvider,
|
||||
validatorFallbackModelId: settings.validatorFallbackModelId,
|
||||
// Task-level validator override (from task)
|
||||
taskValidatorProvider: detail.validatorModelProvider,
|
||||
taskValidatorModelId: detail.validatorModelId,
|
||||
// Project-level validator override
|
||||
projectValidatorProvider: settings.validatorProvider,
|
||||
projectValidatorModelId: settings.validatorModelId,
|
||||
// Project-level validator fallback
|
||||
projectValidatorFallbackProvider: settings.validatorFallbackProvider,
|
||||
projectValidatorFallbackModelId: settings.validatorFallbackModelId,
|
||||
// Global validator lane
|
||||
globalValidatorProvider: settings.validatorGlobalProvider,
|
||||
globalValidatorModelId: settings.validatorGlobalModelId,
|
||||
store,
|
||||
taskId,
|
||||
task: detail,
|
||||
|
||||
@@ -259,7 +259,7 @@ describe("reviewStep — validator model overrides", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("uses validatorModelProvider and validatorModelId when both are set", async () => {
|
||||
it("uses taskValidatorProvider and taskValidatorModelId when both are set", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
|
||||
);
|
||||
@@ -270,8 +270,8 @@ describe("reviewStep — validator model overrides", () => {
|
||||
{
|
||||
defaultProvider: "openai",
|
||||
defaultModelId: "gpt-4o",
|
||||
validatorModelProvider: "anthropic",
|
||||
validatorModelId: "claude-sonnet-4-5",
|
||||
taskValidatorProvider: "anthropic",
|
||||
taskValidatorModelId: "claude-sonnet-4-5",
|
||||
},
|
||||
);
|
||||
|
||||
@@ -281,7 +281,7 @@ describe("reviewStep — validator model overrides", () => {
|
||||
expect(opts.defaultModelId).toBe("claude-sonnet-4-5");
|
||||
});
|
||||
|
||||
it("falls back to defaultProvider/defaultModelId when validatorModelProvider is missing", async () => {
|
||||
it("falls back to defaultProvider/defaultModelId when taskValidatorProvider is missing", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
|
||||
);
|
||||
@@ -292,8 +292,8 @@ describe("reviewStep — validator model overrides", () => {
|
||||
{
|
||||
defaultProvider: "openai",
|
||||
defaultModelId: "gpt-4o",
|
||||
// validatorModelProvider is missing
|
||||
validatorModelId: "claude-sonnet-4-5",
|
||||
// taskValidatorProvider is missing
|
||||
taskValidatorModelId: "claude-sonnet-4-5",
|
||||
},
|
||||
);
|
||||
|
||||
@@ -303,7 +303,7 @@ describe("reviewStep — validator model overrides", () => {
|
||||
expect(opts.defaultModelId).toBe("gpt-4o");
|
||||
});
|
||||
|
||||
it("falls back to defaultProvider/defaultModelId when validatorModelId is missing", async () => {
|
||||
it("falls back to defaultProvider/defaultModelId when taskValidatorModelId is missing", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
|
||||
);
|
||||
@@ -314,8 +314,8 @@ describe("reviewStep — validator model overrides", () => {
|
||||
{
|
||||
defaultProvider: "openai",
|
||||
defaultModelId: "gpt-4o",
|
||||
validatorModelProvider: "anthropic",
|
||||
// validatorModelId is missing
|
||||
taskValidatorProvider: "anthropic",
|
||||
// taskValidatorModelId is missing
|
||||
},
|
||||
);
|
||||
|
||||
@@ -344,6 +344,74 @@ describe("reviewStep — validator model overrides", () => {
|
||||
expect(opts.defaultProvider).toBe("openai");
|
||||
expect(opts.defaultModelId).toBe("gpt-4o");
|
||||
});
|
||||
|
||||
it("resolves project validator override when task override is not set", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
|
||||
);
|
||||
|
||||
await reviewStep(
|
||||
"/tmp/worktree", "FN-100", 1, "Test Step", "plan", "# prompt",
|
||||
undefined,
|
||||
{
|
||||
defaultProvider: "openai",
|
||||
defaultModelId: "gpt-4o",
|
||||
projectValidatorProvider: "anthropic",
|
||||
projectValidatorModelId: "claude-opus-4",
|
||||
// taskValidatorProvider is not set
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(opts.defaultProvider).toBe("anthropic");
|
||||
expect(opts.defaultModelId).toBe("claude-opus-4");
|
||||
});
|
||||
|
||||
it("resolves global validator lane when project override is not set", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
|
||||
);
|
||||
|
||||
await reviewStep(
|
||||
"/tmp/worktree", "FN-100", 1, "Test Step", "plan", "# prompt",
|
||||
undefined,
|
||||
{
|
||||
defaultProvider: "openai",
|
||||
defaultModelId: "gpt-4o",
|
||||
globalValidatorProvider: "google",
|
||||
globalValidatorModelId: "gemini-2.5",
|
||||
// projectValidatorProvider is not set
|
||||
// taskValidatorProvider is not set
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(opts.defaultProvider).toBe("google");
|
||||
expect(opts.defaultModelId).toBe("gemini-2.5");
|
||||
});
|
||||
|
||||
it("falls back to execution default when no validator lanes are set", async () => {
|
||||
mockedCreateHaiAgent.mockResolvedValue(
|
||||
createMockSession("### Verdict: APPROVE\n### Summary\nLooks good."),
|
||||
);
|
||||
|
||||
await reviewStep(
|
||||
"/tmp/worktree", "FN-100", 1, "Test Step", "plan", "# prompt",
|
||||
undefined,
|
||||
{
|
||||
defaultProvider: "openai",
|
||||
defaultModelId: "gpt-4o",
|
||||
// No validator lanes set
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
|
||||
const opts = mockedCreateHaiAgent.mock.calls[0][0];
|
||||
expect(opts.defaultProvider).toBe("openai");
|
||||
expect(opts.defaultModelId).toBe("gpt-4o");
|
||||
});
|
||||
});
|
||||
|
||||
describe("REVIEWER_SYSTEM_PROMPT", () => {
|
||||
|
||||
@@ -191,18 +191,26 @@ export interface ReviewOptions {
|
||||
defaultProvider?: string;
|
||||
/** Default model ID within the provider (e.g. "claude-sonnet-4-5"). When set with `defaultProvider`, overrides the reviewer's model selection. */
|
||||
defaultModelId?: string;
|
||||
/** Validator model provider override. When both `validatorModelProvider` and `validatorModelId` are set, they take precedence over `defaultProvider`/`defaultModelId`. */
|
||||
validatorModelProvider?: string;
|
||||
/** Validator model ID override. When both `validatorModelProvider` and `validatorModelId` are set, they take precedence over `defaultProvider`/`defaultModelId`. */
|
||||
validatorModelId?: string;
|
||||
/** Task-level validator model provider override. When both provider and modelId are set, takes precedence over project/global lanes. */
|
||||
taskValidatorProvider?: string;
|
||||
/** Task-level validator model ID override. When both provider and modelId are set, takes precedence over project/global lanes. */
|
||||
taskValidatorModelId?: string;
|
||||
/** Project-level validator model provider override. Takes precedence over global validator lane. */
|
||||
projectValidatorProvider?: string;
|
||||
/** Project-level validator model ID override. Takes precedence over global validator lane. */
|
||||
projectValidatorModelId?: string;
|
||||
/** Global validator lane provider. Takes precedence over execution defaults. */
|
||||
globalValidatorProvider?: string;
|
||||
/** Global validator lane model ID. Takes precedence over execution defaults. */
|
||||
globalValidatorModelId?: string;
|
||||
/** Fallback model provider used when the primary reviewer model hits a retryable provider-side error. */
|
||||
fallbackProvider?: string;
|
||||
/** Fallback model ID used with `fallbackProvider`. */
|
||||
fallbackModelId?: string;
|
||||
/** Validator fallback model provider override. When both validator fallback fields are set, they take precedence over fallbackProvider/fallbackModelId. */
|
||||
validatorFallbackModelProvider?: string;
|
||||
/** Validator fallback model ID override. When both validator fallback fields are set, they take precedence over fallbackProvider/fallbackModelId. */
|
||||
validatorFallbackModelId?: string;
|
||||
/** Project-level validator fallback provider override. Takes precedence over global fallback. */
|
||||
projectValidatorFallbackProvider?: string;
|
||||
/** Project-level validator fallback model ID override. Takes precedence over global fallback. */
|
||||
projectValidatorFallbackModelId?: string;
|
||||
/** Default thinking effort level for the reviewer agent session. */
|
||||
defaultThinkingLevel?: string;
|
||||
/** Task store for persisting agent log entries. When provided with `taskId`, enables full conversation logging. */
|
||||
@@ -251,19 +259,34 @@ export async function reviewStep(
|
||||
})
|
||||
: null;
|
||||
|
||||
// Resolve validator model settings: use per-task overrides if both provider and modelId are set,
|
||||
// otherwise fall back to defaultProvider/defaultModelId
|
||||
const validatorProvider = options.validatorModelProvider && options.validatorModelId
|
||||
? options.validatorModelProvider
|
||||
: options.defaultProvider;
|
||||
const validatorModelId = options.validatorModelProvider && options.validatorModelId
|
||||
? options.validatorModelId
|
||||
: options.defaultModelId;
|
||||
const validatorFallbackProvider = options.validatorFallbackModelProvider && options.validatorFallbackModelId
|
||||
? options.validatorFallbackModelProvider
|
||||
// Resolve validator model settings using canonical lane hierarchy:
|
||||
// 1. Task-level validator override pair (taskValidatorProvider + taskValidatorModelId)
|
||||
// 2. Project-level validator override pair (projectValidatorProvider + projectValidatorModelId)
|
||||
// 3. Global validator lane pair (globalValidatorProvider + globalValidatorModelId)
|
||||
// 4. Execution default pair (defaultProvider + defaultModelId)
|
||||
const validatorProvider = options.taskValidatorProvider && options.taskValidatorModelId
|
||||
? options.taskValidatorProvider
|
||||
: (options.projectValidatorProvider && options.projectValidatorModelId
|
||||
? options.projectValidatorProvider
|
||||
: (options.globalValidatorProvider && options.globalValidatorModelId
|
||||
? options.globalValidatorProvider
|
||||
: options.defaultProvider));
|
||||
const validatorModelId = options.taskValidatorProvider && options.taskValidatorModelId
|
||||
? options.taskValidatorModelId
|
||||
: (options.projectValidatorProvider && options.projectValidatorModelId
|
||||
? options.projectValidatorModelId
|
||||
: (options.globalValidatorProvider && options.globalValidatorModelId
|
||||
? options.globalValidatorModelId
|
||||
: options.defaultModelId));
|
||||
|
||||
// Resolve validator fallback using lane hierarchy:
|
||||
// 1. Project-level validator fallback (projectValidatorFallbackProvider + projectValidatorFallbackModelId)
|
||||
// 2. Execution fallback (fallbackProvider + fallbackModelId)
|
||||
const validatorFallbackProvider = options.projectValidatorFallbackProvider && options.projectValidatorFallbackModelId
|
||||
? options.projectValidatorFallbackProvider
|
||||
: options.fallbackProvider;
|
||||
const validatorFallbackModelId = options.validatorFallbackModelProvider && options.validatorFallbackModelId
|
||||
? options.validatorFallbackModelId
|
||||
const validatorFallbackModelId = options.projectValidatorFallbackProvider && options.projectValidatorFallbackModelId
|
||||
? options.projectValidatorFallbackModelId
|
||||
: options.fallbackModelId;
|
||||
|
||||
// Resolve per-agent custom instructions for the reviewer role
|
||||
|
||||
@@ -788,12 +788,34 @@ export class StepSessionExecutor {
|
||||
: [];
|
||||
|
||||
// Create fresh agent session for this attempt
|
||||
// Resolve executor model using canonical lane hierarchy:
|
||||
// 1. Task override pair (taskDetail.modelProvider + taskDetail.modelId)
|
||||
// 2. Project execution override pair (settings.executionProvider + settings.executionModelId)
|
||||
// 3. Global execution lane pair (settings.executionGlobalProvider + settings.executionGlobalModelId)
|
||||
// 4. Default pair (settings.defaultProvider + settings.defaultModelId)
|
||||
const executorProvider = taskDetail.modelProvider && taskDetail.modelId
|
||||
? taskDetail.modelProvider
|
||||
: (settings.executionProvider && settings.executionModelId
|
||||
? settings.executionProvider
|
||||
: (settings.executionGlobalProvider && settings.executionGlobalModelId
|
||||
? settings.executionGlobalProvider
|
||||
: settings.defaultProvider));
|
||||
const executorModelId = taskDetail.modelProvider && taskDetail.modelId
|
||||
? taskDetail.modelId
|
||||
: (settings.executionProvider && settings.executionModelId
|
||||
? settings.executionModelId
|
||||
: (settings.executionGlobalProvider && settings.executionGlobalModelId
|
||||
? settings.executionGlobalModelId
|
||||
: settings.defaultModelId));
|
||||
|
||||
const createResult = await createKbAgent({
|
||||
cwd: worktreePath,
|
||||
systemPrompt: `You are an AI agent executing step ${stepIndex} of task ${taskDetail.id}. Follow instructions precisely.`,
|
||||
defaultProvider: taskDetail.modelProvider,
|
||||
defaultModelId: taskDetail.modelId,
|
||||
defaultThinkingLevel: taskDetail.thinkingLevel,
|
||||
defaultProvider: executorProvider,
|
||||
defaultModelId: executorModelId,
|
||||
fallbackProvider: settings.fallbackProvider,
|
||||
fallbackModelId: settings.fallbackModelId,
|
||||
defaultThinkingLevel: taskDetail.thinkingLevel ?? settings.defaultThinkingLevel,
|
||||
customTools: [...pluginTools, ...documentTools],
|
||||
onText: (delta) => {
|
||||
agentLogger.onText(delta);
|
||||
|
||||
@@ -732,8 +732,8 @@ describe("TriageProcessor", () => {
|
||||
{
|
||||
defaultProvider: "anthropic",
|
||||
defaultModelId: "claude-opus-4-6",
|
||||
validatorProvider: "anthropic",
|
||||
validatorModelId: "claude-opus-4-6",
|
||||
projectValidatorProvider: "anthropic",
|
||||
projectValidatorModelId: "claude-opus-4-6",
|
||||
},
|
||||
);
|
||||
|
||||
@@ -751,8 +751,8 @@ describe("TriageProcessor", () => {
|
||||
expect.objectContaining({
|
||||
defaultProvider: "openai-codex",
|
||||
defaultModelId: "gpt-5.4",
|
||||
validatorModelProvider: "zai",
|
||||
validatorModelId: "glm-5.1",
|
||||
projectValidatorProvider: "zai",
|
||||
projectValidatorModelId: "glm-5.1",
|
||||
userComments: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -697,17 +697,25 @@ export class TriageProcessor {
|
||||
onThinking: agentLogger.onThinking,
|
||||
onToolStart: agentLogger.onToolStart,
|
||||
onToolEnd: agentLogger.onToolEnd,
|
||||
// Per-task planning model override takes precedence, then project settings, then global defaults
|
||||
// Resolve planning model using canonical lane hierarchy:
|
||||
// 1. Task planning override pair (planningModelProvider + planningModelId)
|
||||
// 2. Project planning override pair (planningProvider + planningModelId)
|
||||
// 3. Global planning lane pair (planningGlobalProvider + planningGlobalModelId)
|
||||
// 4. Default pair (defaultProvider + defaultModelId)
|
||||
defaultProvider: task.planningModelProvider && task.planningModelId
|
||||
? task.planningModelProvider
|
||||
: (settings.planningProvider && settings.planningModelId
|
||||
? settings.planningProvider
|
||||
: settings.defaultProvider),
|
||||
? settings.planningProvider
|
||||
: (settings.planningGlobalProvider && settings.planningGlobalModelId
|
||||
? settings.planningGlobalProvider
|
||||
: settings.defaultProvider)),
|
||||
defaultModelId: task.planningModelProvider && task.planningModelId
|
||||
? task.planningModelId
|
||||
: (settings.planningProvider && settings.planningModelId
|
||||
? settings.planningModelId
|
||||
: settings.defaultModelId),
|
||||
? settings.planningModelId
|
||||
: (settings.planningGlobalProvider && settings.planningGlobalModelId
|
||||
? settings.planningGlobalModelId
|
||||
: settings.defaultModelId)),
|
||||
fallbackProvider: settings.planningFallbackProvider && settings.planningFallbackModelId
|
||||
? settings.planningFallbackProvider
|
||||
: settings.fallbackProvider,
|
||||
@@ -1191,10 +1199,18 @@ export class TriageProcessor {
|
||||
undefined,
|
||||
{
|
||||
onText: (delta) => options.onAgentText?.(taskId, delta),
|
||||
// Execution defaults as final fallback
|
||||
defaultProvider: currentSettings.defaultProvider,
|
||||
defaultModelId: currentSettings.defaultModelId,
|
||||
validatorModelProvider: currentSettings.validatorProvider,
|
||||
validatorModelId: currentSettings.validatorModelId,
|
||||
// Project-level validator override
|
||||
projectValidatorProvider: currentSettings.validatorProvider,
|
||||
projectValidatorModelId: currentSettings.validatorModelId,
|
||||
// Project-level validator fallback
|
||||
projectValidatorFallbackProvider: currentSettings.validatorFallbackProvider,
|
||||
projectValidatorFallbackModelId: currentSettings.validatorFallbackModelId,
|
||||
// Global validator lane
|
||||
globalValidatorProvider: currentSettings.validatorGlobalProvider,
|
||||
globalValidatorModelId: currentSettings.validatorGlobalModelId,
|
||||
defaultThinkingLevel: currentSettings.defaultThinkingLevel,
|
||||
store,
|
||||
taskId,
|
||||
|
||||
Reference in New Issue
Block a user