FN-8439: align Planning Mode with workflow triage prompts

Planning Mode now uses the selected workflow's triage planning template with its interview adapter.

- Resolve planning prompts from workflow seams and explicit overrides
- Pass and persist the selected workflow for planning sessions
- Document the prompt behavior and cover prompt and UI request flow

Files changed:
 .../fn-8439-planning-prompt-triage-template.md     |  7 ++
 docs/dashboard-guide.md                            |  2 +
 packages/core/src/prompt-overrides.ts              | 42 +-----------
 packages/dashboard/app/api/legacy.ts               |  3 +-
 .../dashboard/app/components/PlanningModeModal.tsx |  3 +-
 .../PlanningModeModal.planning-flow.test.tsx       | 10 +++
 packages/dashboard/app/utils/builtinPrompts.ts     |  4 +-
 .../__tests__/planning-prompt-resolution.test.ts   | 40 ++++++++++++
 packages/dashboard/src/planning.ts                 | 74 ++++++++++++++++++----
 .../src/routes/register-planning-subtask-routes.ts | 18 ++++--
 10 files changed, 142 insertions(+), 61 deletions(-)

Fusion-Task-Id: FN-8439

Fusion-Task-Lineage: 1680a0cd-27c8-47f1-8cd2-de808d1404cf

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-20 13:48:04 -07:00
parent 1d4e8afa7b
commit 8c347981e4
10 changed files with 142 additions and 61 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Planning Mode now uses the same workflow triage planning prompt template as newly added tasks.
category: fix
dev: Resolves Planning Mode from the workflow planning seam plus JSON interview adapter; explicit planning-system overrides still replace it fully.

View File

@@ -528,6 +528,8 @@ When an active Planning AI generation appears stuck, Planning Mode automatically
<!-- FNXC:PlanningMode 2026-07-19-15:55: FN-8400 replaces the duplicate prompt-recovery controls with a focused three-pane interview; restarting remains a deliberate New session action. -->
Use **New session** to restart planning with a different idea.
Planning Mode uses the selected workflow's `planning` seam—the same triage template used for newly added tasks—as its quality bar, then layers the user-controlled interview adapter on top. An explicitly configured `planning-system` prompt override replaces that full system prompt.
<!-- FNXC:PlanningMode 2026-07-18-16:00: Planning Mode is an infinite, user-controlled interview. Each answer updates the running plan and produces another context-aware high-impact question; only Validate plan finalizes it. -->
<!-- FNXC:PlanningMode 2026-07-20-12:42: FN-8438 requires the first AI turn to draft the running plan from the operator idea, then refine that work product after every answer instead of showing an interview transcript. -->
Planning Mode keeps the running plan visible beside answered-question history and the current question on desktop; the AI drafts its title, description, and concrete deliverables from your idea, then refines them after each answer. It is an evolving work product, not a transcript or list of interview questions. You can rename a session and keep asking high-impact, context-aware questions until you choose **Validate plan**. On tablet, mobile, and phone-class short landscape, the interview switches between labeled **Question**, **Running plan**, and **Answered questions** surfaces so the current question stays usable instead of competing with three columns. On mobile, Planning opens to the full-pane, scrollable saved-session list when sessions exist; **Running plan** appears only after you intentionally open a session and choose its tab. **Sessions** (and mobile Back) return to that list with **New session** pinned as its footer. This escape remains available from interview, summary, breakdown, and a new-session composer whenever saved sessions exist, while **Validate plan** remains available on the Running plan surface. The running title, description, and deliverables are available throughout the interview—including while the next question is generating or a recoverable error is shown. The AI never ends an interview on its own. Selection questions provide alternatives with pros and cons plus an **Other** free-text choice, whose wording follows your input language and whose answer steers the next question. You may edit an earlier answer by question ID without losing later answers; Planning re-derives the running plan and appends a fresh next question.

View File

@@ -260,46 +260,8 @@ Output ONLY the prompt text (no markdown, no explanations).`,
key: "planning-system",
name: "Planning System",
roles: ["triage"],
description: "System prompt for the AI planning assistant that guides users through task definition",
defaultContent: `You are a planning assistant for the fn task board system.
Your job: help users transform vague, high-level ideas into well-defined, actionable tasks.
## Conversation Flow
1. User provides a high-level plan (e.g., "Build a user auth system")
2. You ask clarifying questions to understand scope, requirements, and constraints
3. You present UI-friendly selection options when appropriate
4. Once you have enough information, generate a structured summary
## Question Types to Use
- "text": Open-ended follow-up questions for detailed input
- "single_select": When user must choose one option (e.g., tech stack preference)
- "multi_select": When multiple options can apply (e.g., features to include)
- "confirm": Yes/No questions for quick decisions
## Guidelines
- Ask 3-7 questions depending on complexity
- Start broad, then narrow down specifics
- Suggest sensible defaults based on project context
- Keep questions focused and actionable
- When asking about file scope, reference actual project structure
## Summary Generation
When ready to complete, generate:
- A concise but descriptive title (max 80 chars)
- A detailed description with context gathered
- Size estimate (S/M/L) based on scope
- Any suggested dependencies on existing tasks
- Key deliverables as a checklist
## Response Format
Always respond with valid JSON in one of these formats:
For questions:
{\n "type": "question",\n "data": {\n "id": "unique-id",\n "type": "text|single_select|multi_select|confirm",\n "question": "The question text",\n "description": "Helpful context",\n "options": [{"id": "opt1", "label": "Option 1", "description": "Details"}]\n }\n}
For completion:
{\n "type": "complete",\n "data": {\n "title": "Task title",\n "description": "Detailed description",\n "suggestedSize": "S|M|L",\n "suggestedDependencies": [],\n "keyDeliverables": ["Item 1", "Item 2"]\n }\n}`,
description: "Explicit full system-prompt replacement for Planning Mode; otherwise it uses the workflow planning seam plus interview adapter",
defaultContent: "Runtime default: the selected workflow planning seam (the same triage template used for new tasks) plus the user-validated JSON interview adapter. Set an explicit override to replace the full system prompt.",
},
/**
* FNXC:AgentOnboardingRuntime 2026-07-15-15:25:

View File

@@ -2600,7 +2600,7 @@ export function startPlanningStreaming(
initialPlan: string,
projectId?: string,
modelOverride?: { planningModelProvider?: string; planningModelId?: string; thinkingLevel?: ThinkingLevel },
planningOptions?: { clarificationEnabled?: boolean },
planningOptions?: { clarificationEnabled?: boolean; workflowId?: string | null },
existingSessionId?: string,
): Promise<{ sessionId: string }> {
return api<{ sessionId: string }>(withProjectId("/planning/start-streaming", projectId), {
@@ -2611,6 +2611,7 @@ export function startPlanningStreaming(
planningModelId: modelOverride?.planningModelId,
thinkingLevel: modelOverride?.thinkingLevel,
clarificationEnabled: planningOptions?.clarificationEnabled,
...(planningOptions?.workflowId ? { workflowId: planningOptions.workflowId } : {}),
...(existingSessionId ? { existingSessionId } : {}),
}),
});

View File

@@ -1232,7 +1232,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
startedPlan,
projectId,
modelOverride,
{ clarificationEnabled: true },
{ clarificationEnabled: true, ...(workflowId ? { workflowId } : {}) },
draftSessionId ?? undefined,
);
draftSessionIdRef.current = null;
@@ -1256,6 +1256,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
planningModelProvider,
planningThinkingLevel,
projectId,
workflowId,
resetPlanningAutoRetryBudget,
]);

View File

@@ -477,6 +477,16 @@ describe("PlanningModeModal", () => {
expect(screen.queryByText("Your plan so far")).toBeNull();
});
it("forwards the selected workflow to the streaming start request", async () => {
render(<PlanningModeModal isOpen={true} onClose={mockOnClose} onTaskCreated={mockOnTaskCreated} onTasksCreated={vi.fn()} tasks={mockTasks} workflowId="WF-custom-planning" />);
fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), { target: { value: "Build workflow-aware plan" } });
fireEvent.click(screen.getByText("Start Planning"));
await waitFor(() => expect(mockStartPlanningStreaming).toHaveBeenCalledWith(
"Build workflow-aware plan", undefined, undefined,
{ clarificationEnabled: true, workflowId: "WF-custom-planning" }, undefined,
));
});
/*
FNXC:PlanningMultiTab 2026-07-14-00:00:
Planning has no cross-tab locking. Even when another tab is using the same session, this

View File

@@ -153,8 +153,8 @@ export const PROMPT_KEY_CATALOG: Record<PromptKey, PromptKeyMetadata> = {
key: "planning-system",
name: "Planning System",
roles: ["triage"],
description: "System prompt for the AI planning assistant",
defaultContent: "You are a planning assistant for the fn task board system...",
description: "Explicit full system-prompt replacement for Planning Mode; otherwise it uses the workflow planning seam plus interview adapter",
defaultContent: "Runtime default: selected workflow planning seam plus the user-validated JSON interview adapter.",
},
"subtask-breakdown-system": {
key: "subtask-breakdown-system",

View File

@@ -0,0 +1,40 @@
import { describe, expect, it, vi } from "vitest";
import { PROMPT_KEY_CATALOG, type TaskStore } from "@fusion/core";
import { resolvePlanningModeSystemPrompt } from "../planning.js";
function store(settings: Record<string, unknown> = {}, workflowPrompt?: string): TaskStore {
return {
getSettings: vi.fn().mockResolvedValue(settings),
getWorkflowDefinition: vi.fn().mockResolvedValue(workflowPrompt ? {
ir: { version: 1, nodes: [{ id: "plan", kind: "prompt", config: { seam: "planning", prompt: workflowPrompt } }], edges: [], columns: [] },
} : undefined),
} as unknown as TaskStore;
}
describe("resolvePlanningModeSystemPrompt", () => {
it("composes the selected workflow planning seam with the interview adapter", async () => {
const prompt = await resolvePlanningModeSystemPrompt(store({}, "CUSTOM WORKFLOW PLANNING SEAM"), undefined, "WF-custom");
expect(prompt).toContain("CUSTOM WORKFLOW PLANNING SEAM");
expect(prompt).toContain('"type":"question"');
expect(prompt).toContain("exactly one");
expect(prompt).toContain("Only the user can validate");
});
it("does not mistake the catalog fallback for an explicit override", async () => {
const prompt = await resolvePlanningModeSystemPrompt(store({}, "WORKFLOW SEAM MARKER"), undefined, "WF-custom");
expect(prompt).toContain("WORKFLOW SEAM MARKER");
expect(prompt).not.toBe(PROMPT_KEY_CATALOG["planning-system"].defaultContent);
});
it("lets an explicit planning-system override replace the full prompt", async () => {
await expect(resolvePlanningModeSystemPrompt(store(), { "planning-system": "OPERATOR REPLACEMENT" }))
.resolves.toBe("OPERATOR REPLACEMENT");
});
it("prefers the configured triage assignment and fails soft to a builtin seam", async () => {
const assigned = await resolvePlanningModeSystemPrompt(store({ agentPrompts: { roleAssignments: { triage: "custom-triage" }, templates: [{ id: "custom-triage", role: "triage", prompt: "TRIAGE ASSIGNMENT MARKER" }] } }));
expect(assigned).toContain("TRIAGE ASSIGNMENT MARKER");
const fallback = await resolvePlanningModeSystemPrompt({ getSettings: vi.fn().mockRejectedValue(new Error("broken")), getWorkflowDefinition: vi.fn().mockRejectedValue(new Error("broken")) } as unknown as TaskStore);
expect(fallback).toContain("task specification agent");
});
});

View File

@@ -18,6 +18,7 @@ import type {
PlanningResponse,
TaskPriority,
TaskStore,
Settings,
NtfyNotificationEvent,
ThinkingLevel,
MessageStore,
@@ -27,8 +28,13 @@ import {
DEFAULT_TASK_PRIORITY,
TASK_PRIORITIES,
THINKING_LEVELS,
resolvePrompt,
summarizeTitle,
builtinSeamPrompt,
renderTriagePolicyPlaceholders,
resolveAgentPrompt,
resolveEffectivePlannerHeartbeatPatrolEnabled,
resolvePlanningPromptFromIr,
resolveWorkflowIrById,
type PromptOverrideMap,
} from "@fusion/core";
import type { SubtaskItem } from "./subtask-breakdown.js";
@@ -71,6 +77,8 @@ type PlanningSessionOptions = {
projectId?: string;
ntfyConfig?: PlanningNtfyConfig;
clarificationEnabled?: boolean;
/** Workflow selected by the planning entry point; retained for agent rebuilds. */
workflowId?: string;
/** Runtime-only mailbox dependency; never serialize this store. */
messageStore?: MessageStore;
pluginRunner?: SkillPluginRunner;
@@ -228,14 +236,51 @@ Planning Mode is user-terminated: each answered turn must produce one consequent
The model may update the running plan but must never infer completion; only the visible Validate plan action can make a session terminal.
*/
/** Planning system prompt for the AI agent */
export const PLANNING_SYSTEM_PROMPT = `You are a planning assistant for the fn task board system. First analyze the codebase and active board with the available read tools, fn_task_list, and fn_task_show. Turn a raw idea into an incrementally maintained plan.
export const PLANNING_SYSTEM_PROMPT = `## Planning Mode interaction adapter
Ask exactly one next, high-impact question on every turn. Use every prior answer as context, avoid repeated questions, and never decide that the interview is complete or emit a terminal/complete response. The user alone validates the plan.
First analyze the codebase and active board with the available readonly tools, fn_task_list, and fn_task_show. Treat the workflow planning template above as the quality bar and PROMPT.md structure for the evolving plan, but do not write PROMPT.md or use write tools during this interview.
Respond only with JSON: {"type":"question","data":{"id":"unique-id","type":"single_select|multi_select","question":"...","description":"...","options":[{"id":"option-a","label":"...","description":"...","pros":["..."],"cons":["..."]},{"id":"option-b","label":"...","description":"...","pros":["..."],"cons":["...]},{"id":"other","label":"...","isOther":true}],"runningPlan":{"title":"...","description":"...","suggestedSize":"S|M|L","priority":"normal","suggestedDependencies":[],"keyDeliverables":["concrete work item"]}}}.
Ask exactly one next, high-impact question on every turn. Use every prior answer as context, avoid repeated questions, and never decide that the interview is complete or emit a terminal/complete response. Only the user can validate the plan.
Respond only with JSON: {"type":"question","data":{"id":"unique-id","type":"single_select|multi_select","question":"...","description":"...","options":[{"id":"option-a","label":"...","description":"...","pros":["..."],"cons":["..."]},{"id":"option-b","label":"...","description":"...","pros":["..."],"cons":["..."]},{"id":"other","label":"...","isOther":true}],"runningPlan":{"title":"...","description":"...","suggestedSize":"S|M|L","priority":"normal","suggestedDependencies":[],"keyDeliverables":["concrete work item"]}}}.
Every turn must include runningPlan: only title, description, suggestedSize, optional priority, suggestedDependencies, and concrete keyDeliverables informed by the idea and answers so far. Never use interview question text as a deliverable. Do not put PROMPT.md sections (Mission, Before → After, Steps, File Scope, Review Level, Completion Criteria, or Do NOT) in runningPlan or free text: triage writes PROMPT.md only after Validate. Validate serializes this lean plan as plan.md without priority; priority remains a task field. Every question must provide at least two alternatives, each with non-empty pros and cons, plus exactly one Other/write-your-own option. Write every label, option, and Other label in the language of the user's original input. Incorporate free-text Other answers verbatim as steering context for the following question.`;
/*
FNXC:PlanningMode 2026-07-20-14:30:
Planning Mode must use the same workflow planning seam as triage for a newly added task, then layer its infinite JSON interview contract over that template. A catalog defaultContent is Settings UI documentation, not proof that an operator explicitly replaced the full system prompt.
*/
export async function resolvePlanningModeSystemPrompt(
store: TaskStore,
promptOverrides?: PromptOverrideMap,
workflowId?: string,
): Promise<string> {
const settings: Partial<Settings> = (await store.getSettings().catch(() => ({}))) ?? {};
const overrides = promptOverrides ?? settings.promptOverrides;
const explicitOverride = overrides && Object.prototype.hasOwnProperty.call(overrides, "planning-system")
&& typeof overrides["planning-system"] === "string"
&& overrides["planning-system"].trim().length > 0
? overrides["planning-system"]
: undefined;
if (explicitOverride) return explicitOverride;
const plannerHeartbeatPatrolEnabled = resolveEffectivePlannerHeartbeatPatrolEnabled(settings);
const assignedTriagePrompt = settings.agentPrompts?.roleAssignments?.triage
? resolveAgentPrompt("triage", settings.agentPrompts, { plannerHeartbeatPatrolEnabled })
: "";
let workflowPrompt: string | undefined;
try {
const ir = await resolveWorkflowIrById(store, workflowId || settings.defaultWorkflowId || "builtin:coding");
workflowPrompt = resolvePlanningPromptFromIr(ir);
} catch {
// Resolve fail-soft below so a missing custom workflow cannot prevent planning.
}
const fallback = builtinSeamPrompt("planning")
|| resolveAgentPrompt("triage", undefined, { plannerHeartbeatPatrolEnabled });
const base = assignedTriagePrompt || workflowPrompt || fallback;
return `${renderTriagePolicyPlaceholders(base, settings)}\n\n${PLANNING_SYSTEM_PROMPT}`;
}
/** Placeholder title for draft sessions before the user starts planning. */
@@ -265,6 +310,7 @@ export interface DraftInputPayload {
thinkingLevel?: ThinkingLevel;
summarizedFor?: string;
validated?: boolean;
workflowId?: string;
}
/** Session TTL in milliseconds (7 days) */
@@ -317,6 +363,8 @@ interface Session {
initialPlan: string;
title: string;
projectId?: string;
/** Workflow selected at session start, retained for agent reconstruction. */
workflowId?: string;
/** Model override the user picked at draft-create time. Persisted in inputPayload so reopen restores it. */
draftModelProvider?: string;
draftModelId?: string;
@@ -539,6 +587,7 @@ function persistSession(session: Session, status: "generating" | "awaiting_input
...(session.draftModelId ? { modelId: session.draftModelId } : {}),
...(session.draftThinkingLevel ? { thinkingLevel: session.draftThinkingLevel } : {}),
...(session.draftSummarizedFor ? { summarizedFor: session.draftSummarizedFor } : {}),
...(session.workflowId ? { workflowId: session.workflowId } : {}),
validated: session.validated,
...(typeof session.clarificationEnabled === "boolean"
? { clarificationEnabled: session.clarificationEnabled }
@@ -670,6 +719,7 @@ function buildSessionFromRow(row: AiSessionRow): Session {
initialPlan: payload.initialPlan ?? "",
title: row.title,
projectId: row.projectId ?? undefined,
workflowId: payload.workflowId,
draftModelProvider: payload.modelProvider,
draftModelId: payload.modelId,
draftThinkingLevel: thinkingLevel,
@@ -962,7 +1012,7 @@ export async function createSession(
rootDir?: string,
promptOverrides?: PromptOverrideMap,
pluginRunner?: SkillPluginRunner,
options?: Pick<PlanningSessionOptions, "ntfyConfig" | "messageStore" | "clarificationEnabled">,
options?: Pick<PlanningSessionOptions, "ntfyConfig" | "messageStore" | "clarificationEnabled" | "workflowId">,
): Promise<{ sessionId: string; firstQuestion: PlanningQuestion; summary: PlanningSummary; validated: boolean }> {
// Check rate limit
if (!checkRateLimit(ip)) {
@@ -998,6 +1048,7 @@ export async function createSession(
rootDir,
pluginRunner,
clarificationEnabled: options?.clarificationEnabled === true,
workflowId: options?.workflowId,
ntfyConfig: options?.ntfyConfig,
messageStore: options?.messageStore,
};
@@ -1005,9 +1056,7 @@ export async function createSession(
sessions.set(sessionId, session);
persistSession(session, "generating");
// Resolve the effective system prompt (override or default)
const baseSystemPrompt = resolvePrompt("planning-system", promptOverrides) || PLANNING_SYSTEM_PROMPT;
const systemPrompt = baseSystemPrompt;
const systemPrompt = await resolvePlanningModeSystemPrompt(store, promptOverrides, session.workflowId);
// Create AI agent and get the first question
// Only await engineReady if createFnAgent hasn't been set externally (e.g., via __setCreateFnAgent)
@@ -1414,7 +1463,7 @@ export async function startExistingSession(
thinkingLevelOrPromptOverrides?: ThinkingLevel | PromptOverrideMap,
promptOverridesOrPluginRunner?: PromptOverrideMap | SkillPluginRunner,
pluginRunnerMaybe?: SkillPluginRunner,
runtimeOptions?: Pick<PlanningSessionOptions, "ntfyConfig" | "messageStore" | "clarificationEnabled">,
runtimeOptions?: Pick<PlanningSessionOptions, "ntfyConfig" | "messageStore" | "clarificationEnabled" | "workflowId">,
): Promise<void> {
const thinkingLevel = isThinkingLevel(thinkingLevelOrPromptOverrides) ? thinkingLevelOrPromptOverrides : undefined;
const promptOverrides = isThinkingLevel(thinkingLevelOrPromptOverrides)
@@ -1422,6 +1471,7 @@ export async function startExistingSession(
: (thinkingLevelOrPromptOverrides as PromptOverrideMap | undefined);
const pluginRunner = (isThinkingLevel(thinkingLevelOrPromptOverrides) ? pluginRunnerMaybe : promptOverridesOrPluginRunner) as SkillPluginRunner | undefined;
let session = sessions.get(sessionId);
if (session && runtimeOptions?.workflowId) session.workflowId = runtimeOptions.workflowId;
// Draft sessions aren't included in rehydrateFromStore (which only loads
// recoverable in-flight sessions), and a backend restart drops the in-memory
@@ -1446,6 +1496,7 @@ export async function startExistingSession(
if (!session) {
throw new SessionNotFoundError(`Planning session ${sessionId} not found or expired`);
}
if (runtimeOptions?.workflowId) session.workflowId = runtimeOptions.workflowId;
// Drafts are sync'd via aiSessionStore.updateDraft, which only writes
// SQLite. Pull the latest initialPlan + persisted model override + the
@@ -1567,6 +1618,7 @@ export async function createSessionWithAgent(
initialPlan,
title: initialPlan.slice(0, 120),
projectId: options?.projectId,
workflowId: options?.workflowId,
ntfyConfig: options?.ntfyConfig
? {
enabled: options.ntfyConfig.enabled,
@@ -1701,9 +1753,7 @@ async function createPlanningAgent(
// Ensure engine is loaded before using createFnAgent
await ensureEngineReady();
// Resolve the effective system prompt (override or default)
const baseSystemPrompt = resolvePrompt("planning-system", promptOverrides) || PLANNING_SYSTEM_PROMPT;
const systemPrompt = baseSystemPrompt;
const systemPrompt = await resolvePlanningModeSystemPrompt(store, promptOverrides, session.workflowId);
const skillContext = buildSessionSkillContextSync(null, "executor", rootDir, pluginRunner);

View File

@@ -511,12 +511,14 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
*/
router.post("/planning/start", async (req, res) => {
try {
const { initialPlan } = req.body;
const { initialPlan, workflowId } = req.body;
if (!initialPlan || typeof initialPlan !== "string") {
throw badRequest("initialPlan is required and must be a string");
}
if (workflowId !== undefined && typeof workflowId !== "string") {
throw badRequest("workflowId must be a string when provided");
}
const { store: scopedStore } = await getProjectContext(req);
const settings = await scopedStore.getSettings();
@@ -538,7 +540,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
rootDir,
settings.promptOverrides,
ctx.options?.pluginRunner as SkillPluginRunner,
runtime,
{ ...runtime, workflowId },
);
res.status(201).json(result);
} catch (err: unknown) {
@@ -645,6 +647,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
existingSessionId,
thinkingLevel,
clarificationEnabled,
workflowId,
} = req.body;
if (!initialPlan || typeof initialPlan !== "string") {
@@ -660,6 +663,10 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
}
if (workflowId !== undefined && typeof workflowId !== "string") {
throw badRequest("workflowId must be a string when provided");
}
if (clarificationEnabled !== undefined && typeof clarificationEnabled !== "boolean") {
throw badRequest("clarificationEnabled must be a boolean when provided");
}
@@ -730,7 +737,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
validatedThinkingLevel,
settings.promptOverrides,
ctx.options?.pluginRunner as SkillPluginRunner,
runtime,
{ ...runtime, workflowId },
);
} else {
await startExistingSession(
@@ -742,7 +749,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
settings.promptOverrides,
ctx.options?.pluginRunner as SkillPluginRunner,
undefined,
runtime,
{ ...runtime, workflowId },
);
}
res.status(201).json({ sessionId: existingSessionId });
@@ -752,6 +759,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
const { createSessionWithAgent, RateLimitError: _RateLimitError2 } = await import("../planning.js");
const planningOptions = {
projectId,
workflowId,
...runtime,
pluginRunner: ctx.options?.pluginRunner as SkillPluginRunner,
};