Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
feat(core): capture the plan as Todos in the plan-mode reminder when …
…Session Workflow is active
  • Loading branch information
Сергей Панченко
Сергей Панченко committed Oct 5, 2026
commit cb53b83edb280f38b63ad404627b6136e06e7236
5 changes: 4 additions & 1 deletion packages/cli/src/acp-integration/session/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13304,7 +13304,10 @@ export class Session implements SessionContext {

if (this.config.getApprovalMode() === ApprovalMode.PLAN) {
reminders.push({
text: getPlanModeSystemReminder(this.config.getSdkMode?.()),
text: getPlanModeSystemReminder(
this.config.getSdkMode?.(),
this.config.isSessionWorkflowTodoContextActive?.() === true,
),
});
}

Expand Down
7 changes: 6 additions & 1 deletion packages/cli/src/nonInteractive/nonInteractiveHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,12 @@ export function buildInitialSystemReminders(config: Config): Part[] {
const reminders: Part[] = [];

if (config.getApprovalMode() === ApprovalMode.PLAN) {
reminders.push({ text: getPlanModeSystemReminder(config.getSdkMode?.()) });
reminders.push({
text: getPlanModeSystemReminder(
config.getSdkMode?.(),
config.isSessionWorkflowTodoContextActive?.() === true,
),
});
}

const arenaManager = config.getArenaManager?.();
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/core/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8906,7 +8906,12 @@ Other open files:
const send = () => run([{ text: 'Plan this change' }], promptId);
await (inSubagent ? runWithAgentContext('agent-1', send) : send());

expect(getPlanModeSystemReminder).toHaveBeenCalledWith(forSubagent);
// The mock Config has no session-workflow gate, so the reminder's
// sessionWorkflow dimension is off.
expect(getPlanModeSystemReminder).toHaveBeenCalledWith(
forSubagent,
false,
);
},
);

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/core/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4534,6 +4534,7 @@ export class LlmClient {
getPlanModeSystemReminder(
shouldUsePlanOnlyReminderInSubagentContext() ||
this.config.getSdkMode(),
this.config.isSessionWorkflowTodoContextActive?.() === true,
),
);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/core/plan-mode-entry-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export const PLAN_MODE_ENTRY_SIBLING_SKIP_MESSAGE =
const PLAN_MODE_LIFECYCLE_REMINDERS = [
getPlanModeSystemReminder(false),
getPlanModeSystemReminder(true),
// Session Workflow on: the only extra variant enter_plan_mode can emit,
// since plan-only mode keeps its reminder unchanged.
getPlanModeSystemReminder(false, true),
];

export function getPlanModeLifecyclePrefix(
Expand Down
34 changes: 34 additions & 0 deletions packages/core/src/core/prompts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1721,6 +1721,40 @@ describe('getPlanModeSystemReminder', () => {
it('should be deterministic', () => {
expect(getPlanModeSystemReminder()).toBe(getPlanModeSystemReminder());
});

it('instructs todo_write before exit_plan_mode for Session Workflow', () => {
const result = getPlanModeSystemReminder(false, true);

expectText(result, [
'Before converging, capture the plan as a structured Todo list',
'call the todo_write tool with one Todo per plan step',
'blockedBy naming the steps that must complete first',
'by calling the exit_plan_mode tool',
]);
// The Todo list is captured first, only then is the plan presented.
expect(result.indexOf('todo_write')).toBeLessThan(
result.indexOf('by calling the exit_plan_mode tool'),
);
});

it('keeps the Todo reminder off when the gate is off or plan-only', () => {
// Gate off (default) and plan-only mode (SDK/subagents, which never run
// the interactive exit_plan_mode approval) must both stay text-only.
const base = getPlanModeSystemReminder();
expectText(base, ['exit_plan_mode'], ['todo_write']);
expectText(
getPlanModeSystemReminder(true, true),
['Present your plan directly'],
['todo_write'],
);
// The Session Workflow variant is exactly the base reminder with one
// Todo-capture sentence inserted before the presentation instruction.
const inserted =
'Before converging, capture the plan as a structured Todo list: call the todo_write tool with one Todo per plan step (each with a stable id, status "pending", and blockedBy naming the steps that must complete first), so the plan can be reviewed as a task graph. Present your plan';
expect(getPlanModeSystemReminder(false, true)).toBe(
base.replace('Present your plan', inserted),
);
});
});

describe('getManualPlanExitSystemReminder', () => {
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/core/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1412,7 +1412,10 @@ function getToolCallExamples(model?: string): string {
* - Wait for user confirmation before making any changes
* - Override any other instructions that would modify system state
*/
export function getPlanModeSystemReminder(planOnly = false): string {
export function getPlanModeSystemReminder(
planOnly = false,
sessionWorkflow = false,
): string {
return `<system-reminder>
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run tools classified as state-modifying (including changing configs or making commits), or otherwise make changes to the system. A shell command whose safety cannot be determined may run only after the user explicitly approves that exact invocation once, and only when it is necessary for the investigation. This supersedes any other instructions you have received (for example, to make edits).

Expand Down Expand Up @@ -1459,7 +1462,7 @@ An exact one-off approval for an unknown shell command approves only that invoca

### When to Converge

Your plan is ready when you have addressed all ambiguities and it covers: what to change, which files to modify, what existing code to reuse (with file paths), and how to verify the changes. Present your plan ${planOnly ? 'directly' : `by calling the ${ToolNames.EXIT_PLAN_MODE} tool, which will prompt the user to confirm the plan`}. Do NOT make any file changes or run any tools that modify the system state in any way until the user has confirmed the plan.
Your plan is ready when you have addressed all ambiguities and it covers: what to change, which files to modify, what existing code to reuse (with file paths), and how to verify the changes.${sessionWorkflow && !planOnly ? ` Before converging, capture the plan as a structured Todo list: call the ${ToolNames.TODO_WRITE} tool with one Todo per plan step (each with a stable id, status "pending", and blockedBy naming the steps that must complete first), so the plan can be reviewed as a task graph.` : ''} Present your plan ${planOnly ? 'directly' : `by calling the ${ToolNames.EXIT_PLAN_MODE} tool, which will prompt the user to confirm the plan`}. Do NOT make any file changes or run any tools that modify the system state in any way until the user has confirmed the plan.
</system-reminder>`;
}

Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/tools/enterPlanMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,10 @@ class EnterPlanModeToolInvocation extends BaseToolInvocation<
}

return {
llmContent: getPlanModeSystemReminder(this.config.getSdkMode()),
llmContent: getPlanModeSystemReminder(
this.config.getSdkMode(),
this.config.isSessionWorkflowTodoContextActive?.() === true,
),
returnDisplay: 'Entered plan mode.',
};
}
Expand Down
Loading