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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-12 20:09:57 -07:00
parent c110b72df1
commit 9f8db7d1b5
9 changed files with 255 additions and 11 deletions

View File

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

View File

@@ -142,9 +142,10 @@ On mobile viewports, the Right Dock never renders. The compact Header actions an
<!-- FNXC:AutomationTools 2026-06-26-00:00: Automation AI-prompt steps now default to the full coding tool set and expose per-step restrictions so operators can intentionally narrow tool access without breaking legacy schedules. -->
<!-- FNXC:AutomationLiveOutput 2026-06-26-00:00: Manual automation runs stream step, text, and tool activity into the Automations card while preserving the final run-result history after completion. -->
<!-- FNXC:Automations 2026-07-12-19:14: Schedule and routine AI-capable model selectors persist an optional Thinking Level on each step. Default/inherit stays empty, concrete off..xhigh values are stored with the JSON step configuration; runtime application is tracked separately. -->
<!-- FNXC:Automations 2026-07-12-19:14: Schedule and routine AI-capable model selectors persist an optional Thinking Level on each step. Default/inherit stays empty, concrete off..xhigh values are stored with the JSON step configuration. -->
<!-- FNXC:Automations 2026-07-12-20:30: FN-7903 makes the persisted step Thinking Level runtime-active for scheduled, routine, and manual AI Prompt runs, and maps Create Task step choices onto the spawned task. -->
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.

View File

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

View File

@@ -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<typeof vi.fn>).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",

View File

@@ -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 },

View File

@@ -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);

View File

@@ -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]);

View File

@@ -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<string>;
@@ -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<never>((_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<AiPromptExecutor> {
const disposeLog = createLogger("cron-runner");
return async (prompt: string, modelProvider?: string, modelId?: string, allowedTools?: string[], liveCallbacks?: AiPromptLiveCallbacks): Promise<string> => {
return async (prompt: string, modelProvider?: string, modelId?: string, allowedTools?: string[], thinkingLevel?: string, liveCallbacks?: AiPromptLiveCallbacks): Promise<string> => {
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;

View File

@@ -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<never>((_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 },