feat(listen): plan mode system reminder (#1331)

Co-authored-by: cpacker <packercharles@gmail.com>
Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
Christina Tong
2026-03-10 16:05:51 -07:00
committed by GitHub
parent 05a3aec87c
commit a69fbfe877
6 changed files with 156 additions and 64 deletions

View File

@@ -95,6 +95,7 @@ import {
ralphMode, ralphMode,
} from "../ralph/mode"; } from "../ralph/mode";
import { buildSharedReminderParts } from "../reminders/engine"; import { buildSharedReminderParts } from "../reminders/engine";
import { getPlanModeReminder } from "../reminders/planModeReminder";
import { import {
createSharedReminderState, createSharedReminderState,
enqueueCommandIoReminder, 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 // Check if plan file exists
function planFileExists(fallbackPlanFilePath?: string | null): boolean { function planFileExists(fallbackPlanFilePath?: string | null): boolean {
const planFilePath = permissionMode.getPlanFilePath() ?? fallbackPlanFilePath; const planFilePath = permissionMode.getPlanFilePath() ?? fallbackPlanFilePath;

View File

@@ -2,6 +2,7 @@ export type SharedReminderMode =
| "interactive" | "interactive"
| "headless-one-shot" | "headless-one-shot"
| "headless-bidirectional" | "headless-bidirectional"
| "listen"
| "subagent"; | "subagent";
export type SharedReminderId = export type SharedReminderId =
@@ -42,12 +43,22 @@ export const SHARED_REMINDER_CATALOG: ReadonlyArray<SharedReminderDefinition> =
{ {
id: "permission-mode", id: "permission-mode",
description: "Permission mode reminder", description: "Permission mode reminder",
modes: ["interactive", "headless-one-shot", "headless-bidirectional"], modes: [
"interactive",
"headless-one-shot",
"headless-bidirectional",
"listen",
],
}, },
{ {
id: "plan-mode", id: "plan-mode",
description: "Plan mode behavioral reminder", description: "Plan mode behavioral reminder",
modes: ["interactive", "headless-one-shot", "headless-bidirectional"], modes: [
"interactive",
"headless-one-shot",
"headless-bidirectional",
"listen",
],
}, },
{ {
id: "reflection-step-count", id: "reflection-step-count",

View File

@@ -119,7 +119,7 @@ async function buildPermissionModeReminder(
const previousMode = context.state.lastNotifiedPermissionMode; const previousMode = context.state.lastNotifiedPermissionMode;
const shouldEmit = (() => { const shouldEmit = (() => {
if (context.mode === "interactive") { if (context.mode === "interactive" || context.mode === "listen") {
if (previousMode === null) { if (previousMode === null) {
// First turn: only remind if in a non-default mode (e.g. bypassPermissions). // First turn: only remind if in a non-default mode (e.g. bypassPermissions).
return currentMode !== "default"; return currentMode !== "default";

View File

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

View File

@@ -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}
`;
}

View File

@@ -41,6 +41,16 @@ import { computeDiffPreviews } from "../helpers/diffPreview";
import { permissionMode } from "../permissions/mode"; import { permissionMode } from "../permissions/mode";
import { type QueueItem, QueueRuntime } from "../queue/queueRuntime"; import { type QueueItem, QueueRuntime } from "../queue/queueRuntime";
import { mergeQueuedTurnInput } from "../queue/turnQueueRuntime"; 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 { settingsManager } from "../settings-manager";
import { isInteractiveApprovalTool } from "../tools/interactivePolicy"; import { isInteractiveApprovalTool } from "../tools/interactivePolicy";
import { loadTools } from "../tools/manager"; import { loadTools } from "../tools/manager";
@@ -344,6 +354,7 @@ type ListenerRuntime = {
* Threaded into the next send for persistence normalization. * Threaded into the next send for persistence normalization.
*/ */
pendingInterruptedToolCallIds: string[] | null; pendingInterruptedToolCallIds: string[] | null;
reminderState: SharedReminderState;
bootWorkingDirectory: string; bootWorkingDirectory: string;
workingDirectoryByConversation: Map<string, string>; workingDirectoryByConversation: Map<string, string>;
}; };
@@ -532,6 +543,7 @@ function createRuntime(): ListenerRuntime {
continuationEpoch: 0, continuationEpoch: 0,
activeExecutingToolCallIds: [], activeExecutingToolCallIds: [],
pendingInterruptedToolCallIds: null, pendingInterruptedToolCallIds: null,
reminderState: createSharedReminderState(),
bootWorkingDirectory, bootWorkingDirectory,
workingDirectoryByConversation: new Map<string, string>(), workingDirectoryByConversation: new Map<string, string>(),
coalescedSkipQueueItemIds: new Set<string>(), coalescedSkipQueueItemIds: new Set<string>(),
@@ -2856,6 +2868,32 @@ async function handleIncomingMessage(
messagesToSend.push(...msg.messages); 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; let currentInput = messagesToSend;
const sendOptions: Parameters<typeof sendMessageStream>[2] = { const sendOptions: Parameters<typeof sendMessageStream>[2] = {
agentId, agentId,