feat(FN-3105): add planning depth selection to planning flows
- Add planning depth controls and styling in PlanningModeModal with updated modal reentry behavior - Thread selected depth through legacy frontend API calls and planning route handlers - Extend planning prompt builder/runtime wiring to honor depth values for subtask planning - Add route, planning, and modal tests covering depth selector behavior and validation Fusion-Task-Id: FN-3105
This commit is contained in:
@@ -2578,10 +2578,18 @@ export type AgentOnboardingStreamEvent =
|
||||
| { type: "complete"; data: Record<string, never> };
|
||||
|
||||
/** Start a new planning session with an initial plan */
|
||||
export function startPlanning(initialPlan: string, projectId?: string): Promise<PlanningSession> {
|
||||
export function startPlanning(
|
||||
initialPlan: string,
|
||||
projectId?: string,
|
||||
planningOptions?: { planningDepth?: "small" | "medium" | "large"; customQuestionCount?: number },
|
||||
): Promise<PlanningSession> {
|
||||
return api<PlanningSession>(withProjectId("/planning/start", projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ initialPlan }),
|
||||
body: JSON.stringify({
|
||||
initialPlan,
|
||||
planningDepth: planningOptions?.planningDepth,
|
||||
customQuestionCount: planningOptions?.customQuestionCount,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2589,7 +2597,8 @@ export function startPlanning(initialPlan: string, projectId?: string): Promise<
|
||||
export function startPlanningStreaming(
|
||||
initialPlan: string,
|
||||
projectId?: string,
|
||||
modelOverride?: { planningModelProvider?: string; planningModelId?: string }
|
||||
modelOverride?: { planningModelProvider?: string; planningModelId?: string },
|
||||
planningOptions?: { planningDepth?: "small" | "medium" | "large"; customQuestionCount?: number },
|
||||
): Promise<{ sessionId: string }> {
|
||||
return api<{ sessionId: string }>(withProjectId("/planning/start-streaming", projectId), {
|
||||
method: "POST",
|
||||
@@ -2597,6 +2606,8 @@ export function startPlanningStreaming(
|
||||
initialPlan,
|
||||
planningModelProvider: modelOverride?.planningModelProvider,
|
||||
planningModelId: modelOverride?.planningModelId,
|
||||
planningDepth: planningOptions?.planningDepth,
|
||||
customQuestionCount: planningOptions?.customQuestionCount,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -387,6 +387,58 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.planning-depth-selector {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: var(--space-md);
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.planning-depth-chip-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.planning-depth-chip.btn {
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
min-height: calc(var(--space-lg) + var(--space-xl));
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
}
|
||||
|
||||
.planning-depth-chip.btn:hover {
|
||||
border-color: var(--todo);
|
||||
background: var(--card-hover);
|
||||
}
|
||||
|
||||
.planning-depth-chip-active.btn,
|
||||
.planning-depth-chip-active.btn:hover {
|
||||
background: var(--todo);
|
||||
border-color: var(--todo);
|
||||
color: var(--bg);
|
||||
}
|
||||
|
||||
.planning-depth-question-count {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
align-items: flex-start;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.planning-depth-question-input.input {
|
||||
width: calc(var(--space-2xl) * 2 + var(--space-xs));
|
||||
min-height: calc(var(--space-lg) + var(--space-xl));
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
}
|
||||
|
||||
.session-lock-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -1223,6 +1275,20 @@
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.planning-depth-selector {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.planning-depth-chip-group {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.planning-depth-question-count {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.planning-deps-list {
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
@@ -138,6 +138,8 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
||||
} = useAiSessionSync();
|
||||
const [planningModelProvider, setPlanningModelProvider] = useState<string | undefined>(undefined);
|
||||
const [planningModelId, setPlanningModelId] = useState<string | undefined>(undefined);
|
||||
const [planningDepth, setPlanningDepth] = useState<"small" | "medium" | "large">("medium");
|
||||
const [customQuestionCount, setCustomQuestionCount] = useState("");
|
||||
const [loadedModels, setLoadedModels] = useState<ModelInfo[]>([]);
|
||||
const [modelsLoading, setModelsLoading] = useState(false);
|
||||
const [modelsError, setModelsError] = useState<string | null>(null);
|
||||
@@ -253,6 +255,8 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
||||
setIsRetrying(false);
|
||||
setPlanningModelProvider(undefined);
|
||||
setPlanningModelId(undefined);
|
||||
setPlanningDepth("medium");
|
||||
setCustomQuestionCount("");
|
||||
currentSessionIdRef.current = null;
|
||||
setLockSessionId(null);
|
||||
}, []);
|
||||
@@ -469,7 +473,21 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
||||
? { planningModelProvider, planningModelId }
|
||||
: undefined;
|
||||
|
||||
const { sessionId } = await startPlanningStreaming(plan.trim(), projectId, modelOverride);
|
||||
const parsedCustomQuestionCount = customQuestionCount.trim()
|
||||
? Number.parseInt(customQuestionCount, 10)
|
||||
: undefined;
|
||||
|
||||
const { sessionId } = await startPlanningStreaming(
|
||||
plan.trim(),
|
||||
projectId,
|
||||
modelOverride,
|
||||
{
|
||||
planningDepth,
|
||||
customQuestionCount: Number.isInteger(parsedCustomQuestionCount)
|
||||
? parsedCustomQuestionCount
|
||||
: undefined,
|
||||
},
|
||||
);
|
||||
currentSessionIdRef.current = sessionId;
|
||||
setLockSessionId(sessionId);
|
||||
setSelectedSessionId(sessionId);
|
||||
@@ -483,7 +501,15 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
||||
currentSessionIdRef.current = null;
|
||||
setLockSessionId(null);
|
||||
}
|
||||
}, [connectToPlanningStream, initialPlan, planningModelId, planningModelProvider, projectId]);
|
||||
}, [
|
||||
connectToPlanningStream,
|
||||
customQuestionCount,
|
||||
initialPlan,
|
||||
planningDepth,
|
||||
planningModelId,
|
||||
planningModelProvider,
|
||||
projectId,
|
||||
]);
|
||||
|
||||
// Focus textarea when opening
|
||||
useEffect(() => {
|
||||
@@ -1149,6 +1175,8 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
||||
setStreamingOutput("");
|
||||
setPlanningModelProvider(undefined);
|
||||
setPlanningModelId(undefined);
|
||||
setPlanningDepth("medium");
|
||||
setCustomQuestionCount("");
|
||||
currentSessionIdRef.current = null;
|
||||
setLockSessionId(null);
|
||||
setSelectedSessionId(null);
|
||||
@@ -1350,6 +1378,40 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="planning-depth-selector">
|
||||
<div className="planning-depth-chip-group" role="group" aria-label="Planning depth">
|
||||
{([
|
||||
{ value: "small", label: "Small" },
|
||||
{ value: "medium", label: "Medium" },
|
||||
{ value: "large", label: "Large" },
|
||||
] as const).map((depthOption) => (
|
||||
<button
|
||||
key={depthOption.value}
|
||||
type="button"
|
||||
className={`planning-depth-chip btn ${planningDepth === depthOption.value ? "btn-primary planning-depth-chip-active" : ""}`}
|
||||
onClick={() => setPlanningDepth(depthOption.value)}
|
||||
aria-pressed={planningDepth === depthOption.value}
|
||||
>
|
||||
{depthOption.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<label className="planning-depth-question-count" htmlFor="planning-depth-questions">
|
||||
<span>Questions</span>
|
||||
<input
|
||||
id="planning-depth-questions"
|
||||
className="input planning-depth-question-input"
|
||||
type="number"
|
||||
min={1}
|
||||
max={20}
|
||||
value={customQuestionCount}
|
||||
onChange={(e) => setCustomQuestionCount(e.target.value)}
|
||||
placeholder="Auto"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="planning-view-footer">
|
||||
<button
|
||||
className="btn btn-primary planning-start-btn"
|
||||
|
||||
@@ -186,7 +186,10 @@ describe("ModalReentry", () => {
|
||||
|
||||
// Wait for auto-start (which reads the prop)
|
||||
await waitFor(() => {
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("From prop", undefined, undefined);
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("From prop", undefined, undefined, {
|
||||
planningDepth: "medium",
|
||||
customQuestionCount: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
// localStorage should NOT be read since prop was provided
|
||||
|
||||
@@ -381,6 +381,52 @@ describe("PlanningModeModal", () => {
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build auth system", undefined, {
|
||||
planningModelProvider: "anthropic",
|
||||
planningModelId: "claude-sonnet-4-5",
|
||||
}, {
|
||||
planningDepth: "medium",
|
||||
customQuestionCount: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("renders planning depth controls with medium selected by default", () => {
|
||||
render(
|
||||
<PlanningModeModal
|
||||
isOpen={true}
|
||||
onClose={mockOnClose}
|
||||
onTaskCreated={mockOnTaskCreated}
|
||||
onTasksCreated={vi.fn()}
|
||||
tasks={mockTasks}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Small" })).toBeDefined();
|
||||
expect(screen.getByRole("button", { name: "Medium" }).getAttribute("aria-pressed")).toBe("true");
|
||||
expect(screen.getByRole("button", { name: "Large" })).toBeDefined();
|
||||
expect(screen.getByLabelText("Questions")).toBeDefined();
|
||||
});
|
||||
|
||||
it("updates selected depth and sends custom question count", async () => {
|
||||
render(
|
||||
<PlanningModeModal
|
||||
isOpen={true}
|
||||
onClose={mockOnClose}
|
||||
onTaskCreated={mockOnTaskCreated}
|
||||
onTasksCreated={vi.fn()}
|
||||
tasks={mockTasks}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Large" }));
|
||||
fireEvent.change(screen.getByLabelText("Questions"), { target: { value: "7" } });
|
||||
fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), {
|
||||
target: { value: "Build auth system" },
|
||||
});
|
||||
fireEvent.click(screen.getByText("Start Planning"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build auth system", undefined, undefined, {
|
||||
planningDepth: "large",
|
||||
customQuestionCount: 7,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -401,7 +447,10 @@ describe("PlanningModeModal", () => {
|
||||
fireEvent.click(screen.getByText("Start Planning"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build auth system", undefined, undefined);
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build auth system", undefined, undefined, {
|
||||
planningDepth: "medium",
|
||||
customQuestionCount: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -419,7 +468,10 @@ describe("PlanningModeModal", () => {
|
||||
|
||||
// Wait for startPlanningStreaming to be called (allow time for setTimeout in useEffect)
|
||||
await waitFor(() => {
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build a login system from new task dialog", undefined, undefined);
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build a login system from new task dialog", undefined, undefined, {
|
||||
planningDepth: "medium",
|
||||
customQuestionCount: undefined,
|
||||
});
|
||||
}, { timeout: 2000 });
|
||||
|
||||
// Should transition to question view
|
||||
@@ -442,7 +494,10 @@ describe("PlanningModeModal", () => {
|
||||
|
||||
// The auto-start should happen with the initial plan (allow time for setTimeout in useEffect)
|
||||
await waitFor(() => {
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Pre-filled plan from new task", undefined, undefined);
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Pre-filled plan from new task", undefined, undefined, {
|
||||
planningDepth: "medium",
|
||||
customQuestionCount: undefined,
|
||||
});
|
||||
}, { timeout: 2000 });
|
||||
});
|
||||
});
|
||||
@@ -496,7 +551,10 @@ describe("PlanningModeModal", () => {
|
||||
|
||||
// Wait for streaming to be called
|
||||
await waitFor(() => {
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build auth system", undefined, undefined);
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build auth system", undefined, undefined, {
|
||||
planningDepth: "medium",
|
||||
customQuestionCount: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
// Should transition to question view via streaming
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
SessionNotFoundError,
|
||||
InvalidSessionStateError,
|
||||
parseAgentResponse,
|
||||
buildDepthPromptSuffix,
|
||||
generateSubtasksFromPlanning,
|
||||
formatInterviewQA,
|
||||
SESSION_TTL_MS,
|
||||
@@ -1447,6 +1448,28 @@ describe("planning module", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildDepthPromptSuffix", () => {
|
||||
it("returns small depth guidance", () => {
|
||||
expect(buildDepthPromptSuffix("small")).toContain("Ask exactly 1-2 focused questions");
|
||||
});
|
||||
|
||||
it("returns large depth guidance", () => {
|
||||
expect(buildDepthPromptSuffix("large")).toContain("Ask 5-8 thorough questions");
|
||||
});
|
||||
|
||||
it("returns custom count guidance", () => {
|
||||
expect(buildDepthPromptSuffix(undefined, 5)).toBe(
|
||||
"Ask exactly 5 questions. Adjust depth and breadth to fit within that count.",
|
||||
);
|
||||
});
|
||||
|
||||
it("prioritizes custom count over depth guidance", () => {
|
||||
expect(buildDepthPromptSuffix("medium", 7)).toBe(
|
||||
"Ask exactly 7 questions. Adjust depth and breadth to fit within that count.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseAgentResponse", () => {
|
||||
it("parses clean JSON question response", () => {
|
||||
const input = '{"type":"question","data":{"id":"q-1","type":"text","question":"What scope?"}}';
|
||||
|
||||
@@ -10131,6 +10131,38 @@ describe("Planning Mode Routes", () => {
|
||||
});
|
||||
|
||||
describe("POST /planning/start-streaming", () => {
|
||||
it("rejects invalid planning depth", async () => {
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/planning/start-streaming",
|
||||
JSON.stringify({
|
||||
initialPlan: "Build a user auth system",
|
||||
planningDepth: "extra-large",
|
||||
}),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("planningDepth");
|
||||
});
|
||||
|
||||
it("rejects out-of-range custom question count", async () => {
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"POST",
|
||||
"/api/planning/start-streaming",
|
||||
JSON.stringify({
|
||||
initialPlan: "Build a user auth system",
|
||||
customQuestionCount: 21,
|
||||
}),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("customQuestionCount");
|
||||
});
|
||||
|
||||
it("accepts optional model params in request body", async () => {
|
||||
const messages: Array<{ role: string; content: string }> = [];
|
||||
const mockAgent = {
|
||||
|
||||
@@ -221,6 +221,32 @@ const RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000;
|
||||
/** Generation timeout in milliseconds (120 seconds). */
|
||||
export const GENERATION_TIMEOUT_MS = 120_000;
|
||||
|
||||
export type PlanningDepth = "small" | "medium" | "large";
|
||||
|
||||
const PLANNING_DEPTH_PROMPT_SUFFIX: Record<PlanningDepth, string> = {
|
||||
small:
|
||||
"Ask exactly 1-2 focused questions. Prioritize speed and getting to a summary quickly. Skip optional clarification.",
|
||||
medium:
|
||||
"Ask 3-5 well-rounded questions. Balance breadth and depth. This is the default behavior.",
|
||||
large:
|
||||
"Ask 5-8 thorough questions. Deeply explore scope, edge cases, dependencies, and implementation details. Be comprehensive.",
|
||||
};
|
||||
|
||||
export function buildDepthPromptSuffix(
|
||||
depth?: PlanningDepth,
|
||||
customQuestionCount?: number,
|
||||
): string {
|
||||
if (Number.isInteger(customQuestionCount) && (customQuestionCount ?? 0) > 0) {
|
||||
return `Ask exactly ${customQuestionCount} questions. Adjust depth and breadth to fit within that count.`;
|
||||
}
|
||||
|
||||
if (!depth) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return PLANNING_DEPTH_PROMPT_SUFFIX[depth];
|
||||
}
|
||||
|
||||
// ── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/** SSE event types for planning session streaming */
|
||||
@@ -673,6 +699,8 @@ export async function createSession(
|
||||
_store?: TaskStore,
|
||||
rootDir?: string,
|
||||
promptOverrides?: PromptOverrideMap,
|
||||
planningDepth?: PlanningDepth,
|
||||
customQuestionCount?: number,
|
||||
): Promise<{ sessionId: string; firstQuestion: PlanningQuestion }> {
|
||||
// Check rate limit
|
||||
if (!checkRateLimit(ip)) {
|
||||
@@ -704,7 +732,9 @@ export async function createSession(
|
||||
persistSession(session, "generating");
|
||||
|
||||
// Resolve the effective system prompt (override or default)
|
||||
const systemPrompt = resolvePrompt("planning-system", promptOverrides) || PLANNING_SYSTEM_PROMPT;
|
||||
const baseSystemPrompt = resolvePrompt("planning-system", promptOverrides) || PLANNING_SYSTEM_PROMPT;
|
||||
const depthPromptSuffix = buildDepthPromptSuffix(planningDepth, customQuestionCount);
|
||||
const systemPrompt = depthPromptSuffix ? `${baseSystemPrompt}\n\n${depthPromptSuffix}` : baseSystemPrompt;
|
||||
|
||||
// Create AI agent and get the first question
|
||||
// Only await engineReady if createFnAgent hasn't been set externally (e.g., via __setCreateFnAgent)
|
||||
@@ -859,7 +889,12 @@ export async function createSessionWithAgent(
|
||||
modelProvider?: string,
|
||||
modelId?: string,
|
||||
promptOverrides?: PromptOverrideMap,
|
||||
options?: { projectId?: string; ntfyConfig?: PlanningNtfyConfig },
|
||||
options?: {
|
||||
projectId?: string;
|
||||
ntfyConfig?: PlanningNtfyConfig;
|
||||
planningDepth?: PlanningDepth;
|
||||
customQuestionCount?: number;
|
||||
},
|
||||
): Promise<string> {
|
||||
// Check rate limit
|
||||
if (!checkRateLimit(ip)) {
|
||||
@@ -897,7 +932,15 @@ export async function createSessionWithAgent(
|
||||
persistSession(session, "generating");
|
||||
|
||||
// Initialize AI agent in background - it will stream via planningStreamManager
|
||||
initializeAgent(session, rootDir, modelProvider, modelId, promptOverrides).catch((err) => {
|
||||
initializeAgent(
|
||||
session,
|
||||
rootDir,
|
||||
modelProvider,
|
||||
modelId,
|
||||
promptOverrides,
|
||||
options?.planningDepth,
|
||||
options?.customQuestionCount,
|
||||
).catch((err) => {
|
||||
diagnostics.errorFromException("Failed to initialize agent for session", err, { sessionId, operation: "initialize-agent" });
|
||||
persistSession(session, "error", err.message || "Failed to initialize AI agent");
|
||||
planningStreamManager.broadcast(sessionId, {
|
||||
@@ -918,9 +961,19 @@ async function initializeAgent(
|
||||
modelProvider?: string,
|
||||
modelId?: string,
|
||||
promptOverrides?: PromptOverrideMap,
|
||||
planningDepth?: PlanningDepth,
|
||||
customQuestionCount?: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
session.agent = await createPlanningAgent(session, rootDir, modelProvider, modelId, promptOverrides);
|
||||
session.agent = await createPlanningAgent(
|
||||
session,
|
||||
rootDir,
|
||||
modelProvider,
|
||||
modelId,
|
||||
promptOverrides,
|
||||
planningDepth,
|
||||
customQuestionCount,
|
||||
);
|
||||
session.updatedAt = new Date();
|
||||
|
||||
// Send initial message to get first question
|
||||
@@ -944,12 +997,16 @@ async function createPlanningAgent(
|
||||
modelProvider?: string,
|
||||
modelId?: string,
|
||||
promptOverrides?: PromptOverrideMap,
|
||||
planningDepth?: PlanningDepth,
|
||||
customQuestionCount?: number,
|
||||
): Promise<AgentResult> {
|
||||
// Ensure engine is loaded before using createFnAgent
|
||||
await ensureEngineReady();
|
||||
|
||||
// Resolve the effective system prompt (override or default)
|
||||
const systemPrompt = resolvePrompt("planning-system", promptOverrides) || PLANNING_SYSTEM_PROMPT;
|
||||
const baseSystemPrompt = resolvePrompt("planning-system", promptOverrides) || PLANNING_SYSTEM_PROMPT;
|
||||
const depthPromptSuffix = buildDepthPromptSuffix(planningDepth, customQuestionCount);
|
||||
const systemPrompt = depthPromptSuffix ? `${baseSystemPrompt}\n\n${depthPromptSuffix}` : baseSystemPrompt;
|
||||
|
||||
return createFnAgent({
|
||||
cwd: rootDir,
|
||||
|
||||
@@ -393,12 +393,28 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
*/
|
||||
router.post("/planning/start", async (req, res) => {
|
||||
try {
|
||||
const { initialPlan } = req.body;
|
||||
const { initialPlan, planningDepth, customQuestionCount } = req.body;
|
||||
|
||||
if (!initialPlan || typeof initialPlan !== "string") {
|
||||
throw badRequest("initialPlan is required and must be a string");
|
||||
}
|
||||
|
||||
if (
|
||||
planningDepth !== undefined
|
||||
&& planningDepth !== "small"
|
||||
&& planningDepth !== "medium"
|
||||
&& planningDepth !== "large"
|
||||
) {
|
||||
throw badRequest('planningDepth must be one of "small", "medium", or "large" when provided');
|
||||
}
|
||||
|
||||
if (
|
||||
customQuestionCount !== undefined
|
||||
&& (!Number.isInteger(customQuestionCount) || customQuestionCount < 1 || customQuestionCount > 20)
|
||||
) {
|
||||
throw badRequest("customQuestionCount must be an integer between 1 and 20 when provided");
|
||||
}
|
||||
|
||||
const { store: scopedStore } = await getProjectContext(req);
|
||||
const settings = await scopedStore.getSettings();
|
||||
const ip = req.ip || req.socket.remoteAddress || "unknown";
|
||||
@@ -411,6 +427,8 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
scopedStore,
|
||||
rootDir,
|
||||
settings.promptOverrides,
|
||||
planningDepth,
|
||||
customQuestionCount,
|
||||
);
|
||||
res.status(201).json(result);
|
||||
} catch (err: unknown) {
|
||||
@@ -438,7 +456,13 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
*/
|
||||
router.post("/planning/start-streaming", async (req, res) => {
|
||||
try {
|
||||
const { initialPlan, planningModelProvider, planningModelId } = req.body;
|
||||
const {
|
||||
initialPlan,
|
||||
planningModelProvider,
|
||||
planningModelId,
|
||||
planningDepth,
|
||||
customQuestionCount,
|
||||
} = req.body;
|
||||
|
||||
if (!initialPlan || typeof initialPlan !== "string") {
|
||||
throw badRequest("initialPlan is required and must be a string");
|
||||
@@ -452,6 +476,22 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
throw badRequest("planningModelId must be a string when provided");
|
||||
}
|
||||
|
||||
if (
|
||||
planningDepth !== undefined
|
||||
&& planningDepth !== "small"
|
||||
&& planningDepth !== "medium"
|
||||
&& planningDepth !== "large"
|
||||
) {
|
||||
throw badRequest('planningDepth must be one of "small", "medium", or "large" when provided');
|
||||
}
|
||||
|
||||
if (
|
||||
customQuestionCount !== undefined
|
||||
&& (!Number.isInteger(customQuestionCount) || customQuestionCount < 1 || customQuestionCount > 20)
|
||||
) {
|
||||
throw badRequest("customQuestionCount must be an integer between 1 and 20 when provided");
|
||||
}
|
||||
|
||||
const { store: scopedStore, projectId } = await getProjectContext(req);
|
||||
const settings = await scopedStore.getSettings();
|
||||
const ip = req.ip || req.socket.remoteAddress || "unknown";
|
||||
@@ -487,6 +527,8 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
dashboardHost: settings.ntfyDashboardHost,
|
||||
events: settings.ntfyEvents,
|
||||
},
|
||||
planningDepth,
|
||||
customQuestionCount,
|
||||
},
|
||||
);
|
||||
res.status(201).json({ sessionId });
|
||||
|
||||
Reference in New Issue
Block a user