From a69fbfe877012d0c7c3d700620a3c081e47426e9 Mon Sep 17 00:00:00 2001 From: Christina Tong Date: Tue, 10 Mar 2026 16:05:51 -0700 Subject: [PATCH] feat(listen): plan mode system reminder (#1331) Co-authored-by: cpacker Co-authored-by: Letta Code --- src/cli/App.tsx | 62 +--------------------------- src/reminders/catalog.ts | 15 ++++++- src/reminders/engine.ts | 2 +- src/reminders/listenContext.ts | 35 ++++++++++++++++ src/reminders/planModeReminder.ts | 68 +++++++++++++++++++++++++++++++ src/websocket/listen-client.ts | 38 +++++++++++++++++ 6 files changed, 156 insertions(+), 64 deletions(-) create mode 100644 src/reminders/listenContext.ts create mode 100644 src/reminders/planModeReminder.ts diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 60b48df..725c8d7 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -95,6 +95,7 @@ import { ralphMode, } from "../ralph/mode"; import { buildSharedReminderParts } from "../reminders/engine"; +import { getPlanModeReminder } from "../reminders/planModeReminder"; import { createSharedReminderState, enqueueCommandIoReminder, @@ -684,67 +685,6 @@ function saveLastAgentBeforeExit() { } } -// Get plan mode system reminder if in plan mode -function getPlanModeReminder(): string { - if (permissionMode.getMode() !== "plan") { - return ""; - } - - const planFilePath = permissionMode.getPlanFilePath(); - const applyPatchRelativePath = planFilePath - ? relative(process.cwd(), planFilePath).replace(/\\/g, "/") - : null; - - // Generate dynamic reminder with plan file path - return `${SYSTEM_REMINDER_OPEN} - Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received. - -## Plan File Info: -${planFilePath ? `No plan file exists yet. You should create your plan at ${planFilePath} using a write tool (e.g. Write, ApplyPatch, etc. depending on your toolset).\n${applyPatchRelativePath ? `If using apply_patch, use this exact relative patch path: ${applyPatchRelativePath}.` : ""}` : "No plan file path assigned."} - -You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions. - -**Plan File Guidelines:** The plan file should contain only your final recommended approach, not all alternatives considered. Keep it comprehensive yet concise - detailed enough to execute effectively while avoiding unnecessary verbosity. - -## Enhanced Planning Workflow - -### Phase 1: Initial Understanding -Goal: Gain a comprehensive understanding of the user's request by reading through code and asking them questions. - -1. Understand the user's request thoroughly -2. Explore the codebase to understand existing patterns and relevant code -3. Use AskUserQuestion tool to clarify ambiguities in the user request up front. - -### Phase 2: Planning -Goal: Come up with an approach to solve the problem identified in phase 1. - -- Provide any background context that may help with the task without prescribing the exact design itself -- Create a detailed plan - -### Phase 3: Synthesis -Goal: Synthesize the perspectives from Phase 2, and ensure that it aligns with the user's intentions by asking them questions. - -1. Collect all findings from exploration -2. Keep track of critical files that should be read before implementing the plan -3. Use AskUserQuestion to ask the user questions about trade offs. - -### Phase 4: Final Plan -Once you have all the information you need, ensure that the plan file has been updated with your synthesized recommendation including: - -- Recommended approach with rationale -- Key insights from different perspectives -- Critical files that need modification - -### Phase 5: Call ExitPlanMode -At the very end of your turn, once you have asked the user questions and are happy with your final plan file - you should always call ExitPlanMode to indicate to the user that you are done planning. - -This is critical - your turn should only end with either asking the user a question or calling ExitPlanMode. Do not stop unless it's for these 2 reasons. - -NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins. -${SYSTEM_REMINDER_CLOSE} -`; -} - // Check if plan file exists function planFileExists(fallbackPlanFilePath?: string | null): boolean { const planFilePath = permissionMode.getPlanFilePath() ?? fallbackPlanFilePath; diff --git a/src/reminders/catalog.ts b/src/reminders/catalog.ts index 46f5675..7905240 100644 --- a/src/reminders/catalog.ts +++ b/src/reminders/catalog.ts @@ -2,6 +2,7 @@ export type SharedReminderMode = | "interactive" | "headless-one-shot" | "headless-bidirectional" + | "listen" | "subagent"; export type SharedReminderId = @@ -42,12 +43,22 @@ export const SHARED_REMINDER_CATALOG: ReadonlyArray = { id: "permission-mode", description: "Permission mode reminder", - modes: ["interactive", "headless-one-shot", "headless-bidirectional"], + modes: [ + "interactive", + "headless-one-shot", + "headless-bidirectional", + "listen", + ], }, { id: "plan-mode", description: "Plan mode behavioral reminder", - modes: ["interactive", "headless-one-shot", "headless-bidirectional"], + modes: [ + "interactive", + "headless-one-shot", + "headless-bidirectional", + "listen", + ], }, { id: "reflection-step-count", diff --git a/src/reminders/engine.ts b/src/reminders/engine.ts index b4d82c3..8dd3dfa 100644 --- a/src/reminders/engine.ts +++ b/src/reminders/engine.ts @@ -119,7 +119,7 @@ async function buildPermissionModeReminder( const previousMode = context.state.lastNotifiedPermissionMode; const shouldEmit = (() => { - if (context.mode === "interactive") { + if (context.mode === "interactive" || context.mode === "listen") { if (previousMode === null) { // First turn: only remind if in a non-default mode (e.g. bypassPermissions). return currentMode !== "default"; diff --git a/src/reminders/listenContext.ts b/src/reminders/listenContext.ts new file mode 100644 index 0000000..1b43ec2 --- /dev/null +++ b/src/reminders/listenContext.ts @@ -0,0 +1,35 @@ +import type { ReflectionSettings } from "../cli/helpers/memoryReminder"; +import type { SharedReminderContext } from "./engine"; +import type { SharedReminderState } from "./state"; + +// hardcoded for now as we only need plan mode reminder for listener mode +const LISTEN_REFLECTION_SETTINGS: ReflectionSettings = { + trigger: "off", + behavior: "reminder", + stepCount: 25, +}; + +interface BuildListenReminderContextParams { + agentId: string; + state: SharedReminderState; + resolvePlanModeReminder: () => string | Promise; +} + +export function buildListenReminderContext( + params: BuildListenReminderContextParams, +): SharedReminderContext { + return { + mode: "listen", + agent: { + id: params.agentId, + name: null, + description: null, + lastRunAt: null, + }, + state: params.state, + sessionContextReminderEnabled: false, + reflectionSettings: LISTEN_REFLECTION_SETTINGS, + skillSources: [], + resolvePlanModeReminder: params.resolvePlanModeReminder, + }; +} diff --git a/src/reminders/planModeReminder.ts b/src/reminders/planModeReminder.ts new file mode 100644 index 0000000..55c8a9f --- /dev/null +++ b/src/reminders/planModeReminder.ts @@ -0,0 +1,68 @@ +import { relative } from "node:path"; +import { SYSTEM_REMINDER_CLOSE, SYSTEM_REMINDER_OPEN } from "../constants"; +import { permissionMode } from "../permissions/mode"; + +/** + * Build the plan mode system reminder if plan mode is active. + * Returns empty string if not in plan mode. + * + * Shared between App.tsx (interactive CLI) and listen-client.ts (listener mode). + */ +export function getPlanModeReminder(): string { + if (permissionMode.getMode() !== "plan") { + return ""; + } + + const planFilePath = permissionMode.getPlanFilePath(); + const applyPatchRelativePath = planFilePath + ? relative(process.cwd(), planFilePath).replace(/\\/g, "/") + : null; + + return `${SYSTEM_REMINDER_OPEN} + Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received. + +## Plan File Info: +${planFilePath ? `No plan file exists yet. You should create your plan at ${planFilePath} using a write tool (e.g. Write, ApplyPatch, etc. depending on your toolset).\n${applyPatchRelativePath ? `If using apply_patch, use this exact relative patch path: ${applyPatchRelativePath}.` : ""}` : "No plan file path assigned."} + +You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions. + +**Plan File Guidelines:** The plan file should contain only your final recommended approach, not all alternatives considered. Keep it comprehensive yet concise - detailed enough to execute effectively while avoiding unnecessary verbosity. + +## Enhanced Planning Workflow + +### Phase 1: Initial Understanding +Goal: Gain a comprehensive understanding of the user's request by reading through code and asking them questions. + +1. Understand the user's request thoroughly +2. Explore the codebase to understand existing patterns and relevant code +3. Use AskUserQuestion tool to clarify ambiguities in the user request up front. + +### Phase 2: Planning +Goal: Come up with an approach to solve the problem identified in phase 1. + +- Provide any background context that may help with the task without prescribing the exact design itself +- Create a detailed plan + +### Phase 3: Synthesis +Goal: Synthesize the perspectives from Phase 2, and ensure that it aligns with the user's intentions by asking them questions. + +1. Collect all findings from exploration +2. Keep track of critical files that should be read before implementing the plan +3. Use AskUserQuestion to ask the user questions about trade offs. + +### Phase 4: Final Plan +Once you have all the information you need, ensure that the plan file has been updated with your synthesized recommendation including: + +- Recommended approach with rationale +- Key insights from different perspectives +- Critical files that need modification + +### Phase 5: Call ExitPlanMode +At the very end of your turn, once you have asked the user questions and are happy with your final plan file - you should always call ExitPlanMode to indicate to the user that you are done planning. + +This is critical - your turn should only end with either asking the user a question or calling ExitPlanMode. Do not stop unless it's for these 2 reasons. + +NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins. +${SYSTEM_REMINDER_CLOSE} +`; +} diff --git a/src/websocket/listen-client.ts b/src/websocket/listen-client.ts index 1c69b8a..ead5084 100644 --- a/src/websocket/listen-client.ts +++ b/src/websocket/listen-client.ts @@ -41,6 +41,16 @@ import { computeDiffPreviews } from "../helpers/diffPreview"; import { permissionMode } from "../permissions/mode"; import { type QueueItem, QueueRuntime } from "../queue/queueRuntime"; import { mergeQueuedTurnInput } from "../queue/turnQueueRuntime"; +import { + buildSharedReminderParts, + prependReminderPartsToContent, +} from "../reminders/engine"; +import { buildListenReminderContext } from "../reminders/listenContext"; +import { getPlanModeReminder } from "../reminders/planModeReminder"; +import { + createSharedReminderState, + type SharedReminderState, +} from "../reminders/state"; import { settingsManager } from "../settings-manager"; import { isInteractiveApprovalTool } from "../tools/interactivePolicy"; import { loadTools } from "../tools/manager"; @@ -344,6 +354,7 @@ type ListenerRuntime = { * Threaded into the next send for persistence normalization. */ pendingInterruptedToolCallIds: string[] | null; + reminderState: SharedReminderState; bootWorkingDirectory: string; workingDirectoryByConversation: Map; }; @@ -532,6 +543,7 @@ function createRuntime(): ListenerRuntime { continuationEpoch: 0, activeExecutingToolCallIds: [], pendingInterruptedToolCallIds: null, + reminderState: createSharedReminderState(), bootWorkingDirectory, workingDirectoryByConversation: new Map(), coalescedSkipQueueItemIds: new Set(), @@ -2856,6 +2868,32 @@ async function handleIncomingMessage( messagesToSend.push(...msg.messages); + const firstMessage = msg.messages[0]; + const isApprovalMessage = + firstMessage && + "type" in firstMessage && + firstMessage.type === "approval" && + "approvals" in firstMessage; + + if (!isApprovalMessage) { + const { parts: reminderParts } = await buildSharedReminderParts( + buildListenReminderContext({ + agentId: agentId || "", + state: runtime.reminderState, + resolvePlanModeReminder: getPlanModeReminder, + }), + ); + + if (reminderParts.length > 0) { + for (const m of messagesToSend) { + if ("role" in m && m.role === "user" && "content" in m) { + m.content = prependReminderPartsToContent(m.content, reminderParts); + break; + } + } + } + } + let currentInput = messagesToSend; const sendOptions: Parameters[2] = { agentId,