From 9f8db7d1b5b700152bc35c8fc8efb16076ba2206 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 12 Jul 2026 20:09:57 -0700 Subject: [PATCH] FN-7903: wire thinkingLevel into AI session creation for automation steps Threads the persisted per-step Thinking Level (from FN-7900) into runtime AI session creation and task spawning, so scheduled, routine, and manual automation runs actually apply the chosen reasoning effort instead of only storing it. - CronRunner passes step.thinkingLevel through AiPromptExecutor to createFnAgent's defaultThinkingLevel for scheduled AI-prompt steps - RoutineRunner forwards step.thinkingLevel to the shared AiPromptExecutor seam for routine AI-prompt steps - Cron/routine create-task steps map step.thinkingLevel onto TaskCreateInput.thinkingLevel so spawned tasks inherit the configured reasoning effort - Dashboard's inline/manual AI-prompt and create-task automation routes apply the same defaultThinkingLevel / TaskCreateInput.thinkingLevel behavior - Updated docs (dashboard-guide.md, settings-reference.md) to describe the now-active runtime behavior - Added a changeset for @runfusion/fusion (minor) and expanded cron-runner/routine-runner/routes-automation test coverage Files changed: .changeset/fn-7903-automation-thinking-level.md | 7 ++ docs/dashboard-guide.md | 5 +- docs/settings-reference.md | 2 +- .../src/__tests__/routes-automation.test.ts | 67 +++++++++++++++++++ packages/dashboard/src/routes.ts | 10 +++ packages/engine/src/__tests__/cron-runner.test.ts | 75 +++++++++++++++++++++- .../engine/src/__tests__/routine-runner.test.ts | 68 +++++++++++++++++++- packages/engine/src/cron-runner.ts | 21 +++++- packages/engine/src/routine-runner.ts | 11 +++- 9 files changed, 255 insertions(+), 11 deletions(-) Fusion-Task-Id: FN-7903 Fusion-Task-Lineage: c7eb4660-975d-4c5d-820e-0f1a3ac8b6a6 Co-authored-by: Fusion (runfusion.ai) --- .../fn-7903-automation-thinking-level.md | 7 ++ docs/dashboard-guide.md | 5 +- docs/settings-reference.md | 2 +- .../src/__tests__/routes-automation.test.ts | 67 +++++++++++++++++ packages/dashboard/src/routes.ts | 10 +++ .../engine/src/__tests__/cron-runner.test.ts | 75 ++++++++++++++++++- .../src/__tests__/routine-runner.test.ts | 68 ++++++++++++++++- packages/engine/src/cron-runner.ts | 21 +++++- packages/engine/src/routine-runner.ts | 11 ++- 9 files changed, 255 insertions(+), 11 deletions(-) create mode 100644 .changeset/fn-7903-automation-thinking-level.md diff --git a/.changeset/fn-7903-automation-thinking-level.md b/.changeset/fn-7903-automation-thinking-level.md new file mode 100644 index 0000000000..4cc4ddd85e --- /dev/null +++ b/.changeset/fn-7903-automation-thinking-level.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Schedule and routine AI steps now apply the chosen thinking level at run time. +category: feature +dev: Threads AutomationStep.thinkingLevel into createFnAgent (defaultThinkingLevel) across cron-runner, routine-runner, and the dashboard inline ai-prompt path, and maps it onto create-task steps' task thinkingLevel (FN-7903, follow-up to FN-7900). diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 85bb0bbb78..44992af956 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -142,9 +142,10 @@ On mobile viewports, the Right Dock never renders. The compact Header actions an - + + -Open **Automations** from the left sidebar (or the mobile More surfaces) to create cron, webhook, API, or manual routines. AI Prompt steps now run with all selectable coding tools by default: **Read**, **Bash**, **Edit**, **Write**, **Grep**, **Find**, and **Ls**. In the routine editor, use **Allowed tools** on a simple AI Prompt action or any multi-step AI Prompt step to clear or re-select tools. Leaving every tool selected stores the legacy default, so existing schedules continue to run with full tool access; clearing every box is an explicit no-tools configuration. AI Prompt and Create Task action model selectors also include **Thinking Level**: leave it on **Default** to inherit the project setting, or choose a concrete reasoning effort to save it with that step. +Open **Automations** from the left sidebar (or the mobile More surfaces) to create cron, webhook, API, or manual routines. AI Prompt steps now run with all selectable coding tools by default: **Read**, **Bash**, **Edit**, **Write**, **Grep**, **Find**, and **Ls**. In the routine editor, use **Allowed tools** on a simple AI Prompt action or any multi-step AI Prompt step to clear or re-select tools. Leaving every tool selected stores the legacy default, so existing schedules continue to run with full tool access; clearing every box is an explicit no-tools configuration. AI Prompt and Create Task action model selectors also include **Thinking Level**: leave it on **Default** to inherit the project setting, or choose a concrete reasoning effort. Scheduled, routine, and manual AI Prompt runs apply that saved reasoning effort at session creation; Create Task steps copy it onto the task they create. When you choose **Run now**, the routine card opens a **Live output** panel while the manual run is active. The panel appends step status, AI text deltas, and tool start/finish activity as the run executes, then the card falls back to the persisted final run output and run history once the server records the result. The same `RoutineCard` surface is used by the floating modal and embedded Automations view, so live output appears in both presentations and collapses into a single-column card layout on mobile. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index 702d9c535f..77ed312bd0 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -1018,7 +1018,7 @@ The three GPT-5.6 codenamed OpenAI Codex variants (`gpt-5.6-luna`, `gpt-5.6-sol` 6. Assigned durable agent runtime model (`runtimeConfig.model` or `runtimeConfig.modelProvider` + `runtimeConfig.modelId`) when both provider and model ID are set and no task/lane/default pair is configured 7. Automatic provider/model resolution -Workflow prompt steps and scheduled/manual AI-prompt automation steps use the same executor lane before falling back to project/global defaults; explicit step-level `modelProvider` + `modelId` values still take precedence for that individual step. If a non-mock, non-test-mode session still reaches runtime creation without a complete provider/model pair, Fusion logs a warning and records `noModelResolved` plus `runtimeBuiltInFallbackModel` on `session:runtime-resolved` so the runtime's built-in fallback model is observable. +Workflow prompt steps and scheduled/manual AI-prompt automation steps use the same executor lane before falling back to project/global defaults; explicit step-level `modelProvider` + `modelId` values still take precedence for that individual step. Automation AI Prompt steps also apply an explicit step `thinkingLevel` at session creation, while Create Task automation steps copy that reasoning-effort value onto the spawned task; leaving it empty preserves the lane/default thinking-level inheritance. If a non-mock, non-test-mode session still reaches runtime creation without a complete provider/model pair, Fusion logs a warning and records `noModelResolved` plus `runtimeBuiltInFallbackModel` on `session:runtime-resolved` so the runtime's built-in fallback model is observable. ### Heartbeat model (durable agents) diff --git a/packages/dashboard/src/__tests__/routes-automation.test.ts b/packages/dashboard/src/__tests__/routes-automation.test.ts index 8b37efda3b..968eb5af5f 100644 --- a/packages/dashboard/src/__tests__/routes-automation.test.ts +++ b/packages/dashboard/src/__tests__/routes-automation.test.ts @@ -978,6 +978,41 @@ describe("Automation routes", () => { })); }); + it("forwards manual ai-prompt thinkingLevel into session creation and leaves omitted level unset", async () => { + vi.mocked(createFnAgent).mockClear(); + const mockStore = createMockAutomationStore(); + mockStore.getSchedule.mockResolvedValue({ + ...FAKE_SCHEDULE, + command: "", + steps: [ + { + id: "step-ai-high", + type: "ai-prompt", + name: "High thinking AI", + prompt: "Summarize deeply", + thinkingLevel: " high ", + }, + { + id: "step-ai-default", + type: "ai-prompt", + name: "Default thinking AI", + prompt: "Summarize normally", + }, + ], + }); + + const { app } = buildApp(mockStore); + const res = await REQUEST(app, "POST", "/api/automations/sched-001/run"); + + expect(res.status).toBe(200); + expect(vi.mocked(createFnAgent)).toHaveBeenNthCalledWith(1, expect.objectContaining({ + defaultThinkingLevel: "high", + })); + expect(vi.mocked(createFnAgent)).toHaveBeenNthCalledWith(2, expect.objectContaining({ + defaultThinkingLevel: undefined, + })); + }); + it("streams buffered live events for a completed manual AI prompt run", async () => { vi.mocked(createFnAgent).mockClear(); const mockStore = createMockAutomationStore(); @@ -1083,6 +1118,7 @@ describe("Automation routes", () => { expect(vi.mocked(createFnAgent)).toHaveBeenCalledWith(expect.objectContaining({ tools: "coding", toolsAllowlist: undefined, + defaultThinkingLevel: undefined, })); }); @@ -1099,6 +1135,7 @@ describe("Automation routes", () => { taskTitle: "Weekly report", taskDescription: "Create weekly maintenance report", taskColumn: "todo", + thinkingLevel: " high ", }, ], }); @@ -1118,6 +1155,7 @@ describe("Automation routes", () => { title: "Weekly report", description: "Create weekly maintenance report", column: "todo", + thinkingLevel: "high", }), ); expect(res.body.result.stepResults[0]).toEqual( @@ -1129,6 +1167,35 @@ describe("Automation routes", () => { ); }); + it("leaves manual create-task thinkingLevel unset when the step omits it", async () => { + const mockStore = createMockAutomationStore(); + mockStore.getSchedule.mockResolvedValue({ + ...FAKE_SCHEDULE, + command: "", + steps: [ + { + id: "step-task-default", + type: "create-task", + name: "Create default follow-up", + taskDescription: "Create default maintenance report", + }, + ], + }); + const { app, store } = buildApp(mockStore); + (store.createTask as ReturnType).mockResolvedValue({ + id: "FN-9005", + title: "", + description: "Create default maintenance report", + }); + + const res = await REQUEST(app, "POST", "/api/automations/sched-001/run"); + + expect(res.status).toBe(200); + expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({ + thinkingLevel: undefined, + })); + }); + it("create-task automation step remains successful without explicit tracking issue creation", async () => { const createIssueSpy = vi.spyOn(GitHubClient.prototype, "createIssue").mockResolvedValue({ owner: "task", diff --git a/packages/dashboard/src/routes.ts b/packages/dashboard/src/routes.ts index 496ce0c21b..d3867f1230 100644 --- a/packages/dashboard/src/routes.ts +++ b/packages/dashboard/src/routes.ts @@ -5387,8 +5387,12 @@ async function executeAiPromptStep( /* * FNXC:McpConfig 2026-06-26-00:00: * Manual AI-prompt workflow runs are operator-triggered coding-agent sessions, so they must receive the task-store resolved MCP set just like task executor lanes. Do not log resolved MCP payloads because env/header values may contain materialized secrets. + * + * FNXC:Automations 2026-07-12-20:30: + * Manual/inline automation AI runs bypass CronRunner's executor seam, so they must pass the persisted step thinking level directly as createFnAgent.defaultThinkingLevel. Undefined or blank values preserve inherited defaults. */ const mcpServers = await resolveManualAiPromptMcpServers(taskStore); + const defaultThinkingLevel = step.thinkingLevel?.trim() || undefined; const { session } = await createFnAgent({ cwd: process.cwd(), @@ -5397,6 +5401,7 @@ async function executeAiPromptStep( toolsAllowlist: step.allowedTools, defaultProvider: modelProvider, defaultModelId: modelId, + defaultThinkingLevel, mcpServers, onText: (delta: string) => { responseText += delta; @@ -5464,12 +5469,17 @@ async function executeCreateTaskStep( } try { + /* + FNXC:Automations 2026-07-12-20:30: + Manual/inline create-task automation runs map the persisted step thinking level onto the created task so manual execution matches scheduled and routine behavior. + */ const task = await taskStore.createTask({ title: step.taskTitle?.trim() || undefined, description: step.taskDescription.trim(), column: (step.taskColumn as import("@fusion/core").Column) || "triage", modelProvider: step.modelProvider?.trim() || undefined, modelId: step.modelId?.trim() || undefined, + thinkingLevel: (step.thinkingLevel?.trim() || undefined) as import("@fusion/core").TaskCreateInput["thinkingLevel"], source: { sourceType: "workflow_step", sourceMetadata: { stepId: step.id }, diff --git a/packages/engine/src/__tests__/cron-runner.test.ts b/packages/engine/src/__tests__/cron-runner.test.ts index 601cbe2f57..e6ee48362b 100644 --- a/packages/engine/src/__tests__/cron-runner.test.ts +++ b/packages/engine/src/__tests__/cron-runner.test.ts @@ -223,6 +223,21 @@ describe("CronRunner", () => { expect(capturedOptions.toolsAllowlist).toBeUndefined(); }); + it("forwards thinkingLevel as createFnAgent defaultThinkingLevel and omits blank values", async () => { + const capturedOptions: any[] = []; + piModuleMocks.createFnAgent.mockImplementation(async (options: any) => { + capturedOptions.push(options); + return { session: { dispose: vi.fn() } }; + }); + + const executor = await createAiPromptExecutor("/test/project"); + await executor("Think deeply", undefined, undefined, undefined, "high"); + await executor("Inherit defaults", undefined, undefined, undefined, " "); + + expect(capturedOptions[0].defaultThinkingLevel).toBe("high"); + expect(capturedOptions[1].defaultThinkingLevel).toBeUndefined(); + }); + it("forwards store-resolved MCP servers to scheduled AI prompt sessions", async () => { let capturedOptions: any; piModuleMocks.createFnAgent.mockImplementation(async (options: any) => { @@ -1034,6 +1049,7 @@ describe("CronRunner", () => { "anthropic", "claude-sonnet-4-5", undefined, + undefined, ); }); @@ -1224,7 +1240,37 @@ describe("CronRunner", () => { await runner.executeSchedule(schedule); - expect(mockExecutor).toHaveBeenCalledWith("Use selected tools", "anthropic", "claude-sonnet-4-5", ["Read", "Grep"]); + expect(mockExecutor).toHaveBeenCalledWith("Use selected tools", "anthropic", "claude-sonnet-4-5", ["Read", "Grep"], undefined); + }); + + it("passes explicit step thinkingLevel to executor and leaves omitted level unset", async () => { + const store = createMockStore(); + const mockExecutor = createAiMockExecutor("response"); + const schedule = createMockSchedule({ + command: "", + steps: [ + makeStep({ + type: "ai-prompt", + name: "High thinking", + prompt: "Think deeply", + thinkingLevel: "high", + command: undefined, + }), + makeStep({ + type: "ai-prompt", + name: "Default thinking", + prompt: "Use default", + command: undefined, + }), + ], + }); + const automationStore = createMockAutomationStore([schedule]); + runner = new CronRunner(store, automationStore, { aiPromptExecutor: mockExecutor }); + + await runner.executeSchedule(schedule); + + expect(mockExecutor).toHaveBeenNthCalledWith(1, "Think deeply", "anthropic", "claude-sonnet-4-5", undefined, "high"); + expect(mockExecutor).toHaveBeenNthCalledWith(2, "Use default", "anthropic", "claude-sonnet-4-5", undefined, undefined); }); it("passes step model provider and model ID to executor", async () => { @@ -1248,7 +1294,7 @@ describe("CronRunner", () => { await runner.executeSchedule(schedule); - expect(mockExecutor).toHaveBeenCalledWith("Do something", "openai", "gpt-4o", undefined); + expect(mockExecutor).toHaveBeenCalledWith("Do something", "openai", "gpt-4o", undefined, undefined); }); it("falls back to settings defaults when step has no model", async () => { @@ -1273,7 +1319,7 @@ describe("CronRunner", () => { await runner.executeSchedule(schedule); - expect(mockExecutor).toHaveBeenCalledWith("Use defaults", "anthropic", "claude-sonnet-4-5", undefined); + expect(mockExecutor).toHaveBeenCalledWith("Use defaults", "anthropic", "claude-sonnet-4-5", undefined, undefined); }); it("returns configuration error when no executor is provided", async () => { @@ -1514,6 +1560,7 @@ describe("CronRunner", () => { column: "todo", modelProvider: "anthropic", modelId: "claude-sonnet-4-5", + thinkingLevel: undefined, source: { sourceType: "cron", sourceMetadata: { scheduleId: "test-schedule-id", stepId: expect.any(String) }, @@ -1521,6 +1568,28 @@ describe("CronRunner", () => { }); }); + it("maps explicit create-task thinkingLevel and leaves omitted level unset", async () => { + const mockTask = { id: "FN-7788", title: "", description: "Some task" }; + const createTaskMock = vi.fn().mockResolvedValue(mockTask); + const store = createMockStore({} as any); + (store as any).createTask = createTaskMock; + + const schedule = createMockSchedule({ + command: "", + steps: [ + makeCreateTaskStep({ taskDescription: "High effort task", thinkingLevel: "high" }), + makeCreateTaskStep({ taskDescription: "Inherited effort task" }), + ], + }); + const automationStore = createMockAutomationStore([schedule]); + runner = new CronRunner(store, automationStore); + + await runner.executeSchedule(schedule); + + expect(createTaskMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ thinkingLevel: "high" })); + expect(createTaskMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ thinkingLevel: undefined })); + }); + it("defaults column to triage when taskColumn is not set", async () => { const mockTask = { id: "FN-9999", title: "", description: "Some task" }; const createTaskMock = vi.fn().mockResolvedValue(mockTask); diff --git a/packages/engine/src/__tests__/routine-runner.test.ts b/packages/engine/src/__tests__/routine-runner.test.ts index 49531320b8..2034c19cc2 100644 --- a/packages/engine/src/__tests__/routine-runner.test.ts +++ b/packages/engine/src/__tests__/routine-runner.test.ts @@ -346,11 +346,77 @@ describe("RoutineRunner", () => { const result = await runner.executeRoutine("routine-ai", "api", undefined, liveCallbacks); expect(result.success).toBe(true); - expect(aiPromptExecutor).toHaveBeenCalledWith("Analyze this", undefined, undefined, ["Read", "Grep"], liveCallbacks); + expect(aiPromptExecutor).toHaveBeenCalledWith("Analyze this", undefined, undefined, ["Read", "Grep"], undefined, liveCallbacks); expect(liveCallbacks.onStep).toHaveBeenCalledWith(expect.objectContaining({ stepId: "step-ai", status: "started" })); expect(liveCallbacks.onStep).toHaveBeenCalledWith(expect.objectContaining({ stepId: "step-ai", status: "completed", success: true })); }); + it("forwards explicit and omitted ai-prompt thinking levels to the AI executor", async () => { + const routine = createMockRoutine({ + id: "routine-ai-thinking", + agentId: undefined, + steps: [ + { + id: "step-ai-high", + type: "ai-prompt", + name: "Analyze deeply", + prompt: "Analyze deeply", + thinkingLevel: "high", + }, + { + id: "step-ai-default", + type: "ai-prompt", + name: "Analyze normally", + prompt: "Analyze normally", + }, + ], + }); + const routineStore = createMockRoutineStore([routine]); + const aiPromptExecutor = vi.fn().mockResolvedValue("ai output"); + const runner = createRoutineRunner({ routineStore, aiPromptExecutor }); + + const result = await runner.executeRoutine("routine-ai-thinking", "api"); + + expect(result.success).toBe(true); + expect(aiPromptExecutor).toHaveBeenNthCalledWith(1, "Analyze deeply", undefined, undefined, undefined, "high", undefined); + expect(aiPromptExecutor).toHaveBeenNthCalledWith(2, "Analyze normally", undefined, undefined, undefined, undefined, undefined); + }); + + it("maps explicit and omitted create-task thinking levels onto spawned task input", async () => { + const routine = createMockRoutine({ + id: "routine-task-thinking", + agentId: undefined, + steps: [ + { + id: "step-task-high", + type: "create-task", + name: "Create high effort task", + taskDescription: "Investigate deeply", + thinkingLevel: "high", + }, + { + id: "step-task-default", + type: "create-task", + name: "Create default task", + taskDescription: "Investigate normally", + }, + ], + }); + const routineStore = createMockRoutineStore([routine]); + const createTask = vi + .fn() + .mockResolvedValueOnce({ id: "FN-7001", title: "", description: "Investigate deeply" }) + .mockResolvedValueOnce({ id: "FN-7002", title: "", description: "Investigate normally" }); + const taskStore = { ...createMockTaskStore(), createTask } as unknown as TaskStore; + const runner = createRoutineRunner({ routineStore, taskStore }); + + const result = await runner.executeRoutine("routine-task-thinking", "api"); + + expect(result.success).toBe(true); + expect(createTask).toHaveBeenNthCalledWith(1, expect.objectContaining({ thinkingLevel: "high" })); + expect(createTask).toHaveBeenNthCalledWith(2, expect.objectContaining({ thinkingLevel: undefined })); + }); + it("cleans up inFlightExecutions map even on error", async () => { const routine = createMockRoutine({ id: "routine-error-cleanup", enabled: false }); const routineStore = createMockRoutineStore([routine]); diff --git a/packages/engine/src/cron-runner.ts b/packages/engine/src/cron-runner.ts index 747ac73dbb..28408a56fd 100644 --- a/packages/engine/src/cron-runner.ts +++ b/packages/engine/src/cron-runner.ts @@ -208,11 +208,16 @@ export type AiPromptLiveCallbacks = { onToolEnd?: (name: string, isError: boolean, result?: unknown) => void; }; +/* +FNXC:Automations 2026-07-12-20:30: +FN-7903 applies FN-7900's persisted per-step reasoning effort at runtime. Scheduled and routine AI steps pass `thinkingLevel` before live callbacks so undefined keeps inheriting the resolved default while explicit values become createFnAgent's defaultThinkingLevel. +*/ export type AiPromptExecutor = ( prompt: string, modelProvider?: string, modelId?: string, allowedTools?: string[], + thinkingLevel?: string, liveCallbacks?: AiPromptLiveCallbacks, ) => Promise; @@ -885,6 +890,7 @@ export class CronRunner { const defaultModel = resolveExecutionSettingsModel(settings); const modelProvider = step.modelProvider?.trim() || defaultModel.provider; const modelId = step.modelId?.trim() || defaultModel.modelId; + const thinkingLevel = step.thinkingLevel?.trim() || undefined; const model = modelProvider && modelId ? `${modelProvider}/${modelId}` @@ -894,7 +900,7 @@ export class CronRunner { try { // Race between executor and timeout - const resultPromise = this.aiPromptExecutor(step.prompt, modelProvider, modelId, step.allowedTools); + const resultPromise = this.aiPromptExecutor(step.prompt, modelProvider, modelId, step.allowedTools, thinkingLevel); const timeoutPromise = new Promise((_resolve, reject) => { setTimeout(() => reject(new Error(`AI prompt step timed out after ${timeoutMs / 1000}s`)), timeoutMs); }); @@ -959,13 +965,17 @@ export class CronRunner { }; } - // Build TaskCreateInput from step fields + /* + FNXC:Automations 2026-07-12-20:30: + Create-task automation steps spawn normal Fusion tasks, so the persisted per-step thinking level must map onto TaskCreateInput.thinkingLevel. Empty values stay unset to preserve task/settings inheritance. + */ const taskInput: TaskCreateInput = { title: step.taskTitle?.trim() || undefined, description: step.taskDescription.trim(), column: (step.taskColumn as Column) || "triage", modelProvider: step.modelProvider?.trim() || undefined, modelId: step.modelId?.trim() || undefined, + thinkingLevel: (step.thinkingLevel?.trim() || undefined) as TaskCreateInput["thinkingLevel"], source: { sourceType: "cron", sourceMetadata: { scheduleId, stepId: step.id }, @@ -1029,7 +1039,7 @@ const AI_AUTOMATION_SYSTEM_PROMPT = [ export async function createAiPromptExecutor(cwd: string, store?: TaskStore): Promise { const disposeLog = createLogger("cron-runner"); - return async (prompt: string, modelProvider?: string, modelId?: string, allowedTools?: string[], liveCallbacks?: AiPromptLiveCallbacks): Promise => { + return async (prompt: string, modelProvider?: string, modelId?: string, allowedTools?: string[], thinkingLevel?: string, liveCallbacks?: AiPromptLiveCallbacks): Promise => { let responseText = ""; const skillContext = buildSessionSkillContextSync(null, "executor", cwd, undefined); @@ -1039,8 +1049,12 @@ export async function createAiPromptExecutor(cwd: string, store?: TaskStore): Pr FNXC:McpConfig 2026-06-26-00:00: Scheduled AI automations are coding-agent work surfaces. ProjectEngine passes the TaskStore so configured MCP servers are forwarded; lightweight in-process runtime callers may omit the store and keep the pre-existing empty-MCP behavior. + + FNXC:Automations 2026-07-12-20:30: + Scheduled/routine automation sessions forward the persisted per-step thinking level as createFnAgent's defaultThinkingLevel. Undefined or whitespace-only values intentionally omit the option so settings-level inheritance remains unchanged. */ const mcpServers = store ? (await resolveMcpServersForStore(store)).servers : undefined; + const defaultThinkingLevel = thinkingLevel?.trim() || undefined; const { session } = await createFnAgent({ cwd, systemPrompt: AI_AUTOMATION_SYSTEM_PROMPT, @@ -1051,6 +1065,7 @@ export async function createAiPromptExecutor(cwd: string, store?: TaskStore): Pr ...(skillContext.additionalSkillPaths.length > 0 ? { additionalSkillPaths: skillContext.additionalSkillPaths } : {}), defaultProvider: modelProvider, defaultModelId: modelId, + defaultThinkingLevel, mcpServers, onText: (delta: string) => { responseText += delta; diff --git a/packages/engine/src/routine-runner.ts b/packages/engine/src/routine-runner.ts index 8cfa60cd36..f7d6f788a3 100644 --- a/packages/engine/src/routine-runner.ts +++ b/packages/engine/src/routine-runner.ts @@ -458,8 +458,12 @@ export class RoutineRunner { return { stepId: step.id, stepName: step.name, stepIndex, success: false, output: "", error: "AI execution is not configured", startedAt, completedAt: new Date().toISOString() }; } try { + /* + FNXC:Automations 2026-07-12-20:30: + Routine AI-prompt steps share the CronRunner AiPromptExecutor seam. Pass the persisted per-step thinking level before live callbacks so explicit reasoning effort applies and omitted/blank values inherit defaults. + */ const output = await Promise.race([ - this.options.aiPromptExecutor(step.prompt, step.modelProvider, step.modelId, step.allowedTools, liveCallbacks), + this.options.aiPromptExecutor(step.prompt, step.modelProvider, step.modelId, step.allowedTools, step.thinkingLevel?.trim() || undefined, liveCallbacks), new Promise((_resolve, reject) => setTimeout(() => reject(new Error(`AI prompt step timed out after ${timeoutMs / 1000}s`)), timeoutMs)), ]); return { stepId: step.id, stepName: step.name, stepIndex, success: true, output: truncateOutput(output, ""), startedAt, completedAt: new Date().toISOString() }; @@ -476,12 +480,17 @@ export class RoutineRunner { return { stepId: step.id, stepName: step.name, stepIndex, success: false, output: "", error: "Create-task step has no task description specified", startedAt, completedAt: new Date().toISOString() }; } + /* + FNXC:Automations 2026-07-12-20:30: + Routine create-task steps map their persisted thinking level onto the spawned task. Blank values remain undefined to keep the task's normal thinking-level inheritance. + */ const taskInput: TaskCreateInput = { title: step.taskTitle?.trim() || undefined, description: step.taskDescription.trim(), column: (step.taskColumn as Column) || "triage", modelProvider: step.modelProvider?.trim() || undefined, modelId: step.modelId?.trim() || undefined, + thinkingLevel: (step.thinkingLevel?.trim() || undefined) as TaskCreateInput["thinkingLevel"], source: { sourceType: "automation", sourceMetadata: { routineId: routine.id, stepId: step.id },