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:
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
35
src/reminders/listenContext.ts
Normal file
35
src/reminders/listenContext.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
68
src/reminders/planModeReminder.ts
Normal file
68
src/reminders/planModeReminder.ts
Normal 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}
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user