feat(FN-1063): add per-task thinkingLevel override in executor
- Extract per-task thinkingLevel from task detail, falling back to global defaultThinkingLevel - Apply override in initial execution, retry session, and spawned agent paths - Add 3 tests: per-task override, global fallback, explicit 'off' overrides global
This commit is contained in:
@@ -5120,6 +5120,163 @@ describe("Per-task model overrides", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Per-task thinkingLevel override tests ───────────────────────────
|
||||
|
||||
describe("Per-task thinkingLevel override", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("uses per-task thinkingLevel when set on the task", async () => {
|
||||
const store = createMockStore();
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
},
|
||||
} as any);
|
||||
|
||||
// Override getTask to return task with thinkingLevel override
|
||||
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(),
|
||||
thinkingLevel: "high",
|
||||
});
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
|
||||
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(),
|
||||
thinkingLevel: "high",
|
||||
});
|
||||
|
||||
// Should use per-task thinkingLevel override
|
||||
const callArgs = mockedCreateHaiAgent.mock.calls[0];
|
||||
expect(callArgs).toBeDefined();
|
||||
expect(callArgs[0].defaultThinkingLevel).toBe("high");
|
||||
});
|
||||
|
||||
it("falls back to global defaultThinkingLevel when task has no thinkingLevel", async () => {
|
||||
const store = createMockStore();
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
},
|
||||
} as any);
|
||||
|
||||
store.getSettings.mockResolvedValue({
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 15000,
|
||||
groupOverlappingFiles: false,
|
||||
autoMerge: false,
|
||||
worktreeInitCommand: undefined,
|
||||
defaultThinkingLevel: "medium",
|
||||
});
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
|
||||
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 thinkingLevel set
|
||||
});
|
||||
|
||||
// Should fall back to global defaultThinkingLevel
|
||||
const callArgs = mockedCreateHaiAgent.mock.calls[0];
|
||||
expect(callArgs).toBeDefined();
|
||||
expect(callArgs[0].defaultThinkingLevel).toBe("medium");
|
||||
});
|
||||
|
||||
it("uses explicit 'off' thinkingLevel from task over global setting", async () => {
|
||||
const store = createMockStore();
|
||||
|
||||
mockedCreateHaiAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
},
|
||||
} as any);
|
||||
|
||||
store.getSettings.mockResolvedValue({
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 15000,
|
||||
groupOverlappingFiles: false,
|
||||
autoMerge: false,
|
||||
worktreeInitCommand: undefined,
|
||||
defaultThinkingLevel: "high",
|
||||
});
|
||||
|
||||
// Override getTask to return task with thinkingLevel: "off"
|
||||
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(),
|
||||
thinkingLevel: "off",
|
||||
});
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
|
||||
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(),
|
||||
thinkingLevel: "off",
|
||||
});
|
||||
|
||||
// Should use task's explicit "off" instead of global "high"
|
||||
const callArgs = mockedCreateHaiAgent.mock.calls[0];
|
||||
expect(callArgs).toBeDefined();
|
||||
expect(callArgs[0].defaultThinkingLevel).toBe("off");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Invalid transition error handling tests ─────────────────────────
|
||||
|
||||
describe("Invalid transition error handling", () => {
|
||||
|
||||
@@ -978,6 +978,9 @@ export class TaskExecutor {
|
||||
: settings.defaultModelId;
|
||||
const executorFallbackProvider = settings.fallbackProvider;
|
||||
const executorFallbackModelId = settings.fallbackModelId;
|
||||
const executorThinkingLevel = detail.thinkingLevel
|
||||
? detail.thinkingLevel
|
||||
: settings.defaultThinkingLevel;
|
||||
|
||||
// Determine whether we're resuming a previous session (pause/resume)
|
||||
// or starting fresh. Use file-based sessions so conversation state
|
||||
@@ -1002,7 +1005,7 @@ export class TaskExecutor {
|
||||
defaultModelId: executorModelId,
|
||||
fallbackProvider: executorFallbackProvider,
|
||||
fallbackModelId: executorFallbackModelId,
|
||||
defaultThinkingLevel: settings.defaultThinkingLevel,
|
||||
defaultThinkingLevel: executorThinkingLevel,
|
||||
sessionManager,
|
||||
});
|
||||
|
||||
@@ -1189,7 +1192,7 @@ export class TaskExecutor {
|
||||
defaultModelId: executorModelId,
|
||||
fallbackProvider: executorFallbackProvider,
|
||||
fallbackModelId: executorFallbackModelId,
|
||||
defaultThinkingLevel: settings.defaultThinkingLevel,
|
||||
defaultThinkingLevel: executorThinkingLevel,
|
||||
sessionManager: SessionManager.create(worktreePath),
|
||||
});
|
||||
// Update session file for the retry session (so pause/resume works)
|
||||
@@ -1708,7 +1711,7 @@ export class TaskExecutor {
|
||||
defaultModelId: settings.defaultModelId,
|
||||
fallbackProvider: settings.fallbackProvider,
|
||||
fallbackModelId: settings.fallbackModelId,
|
||||
defaultThinkingLevel: settings.defaultThinkingLevel,
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user