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:
7
.changeset/fn-8439-planning-prompt-triage-template.md
Normal file
7
.changeset/fn-8439-planning-prompt-triage-template.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 } : {}),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user