What plan mode is
Plan mode is a logged, per-agent collaboration state that changes the language guidance the model receives and gates a specific exit tool — it is soft guidance, not an enforcement mechanism. The single package is @deepseek-ai/dsh-plan-mode under packages/plan/, with the UI half in packages/client/ui-plan. Sandbox mode and approval policy enforce restrictions independently and do not read or write plan state.
Plan mode gives the model a deployment-authored system-prompt section telling it to explore and design first, and registers exit_plan_mode so a completed plan is presented to the human for review before real work proceeds. It does not restrict tools, and it does not require approval for each step by itself.
Activation: a per-session persisted setting
Plan mode is stored as plan/mode — a log-only, whole-value-replace SessionEventMap member holding { active: boolean }. Because it is logged, foldPlanMode(events) recovers plan state directly from the session log, so resume, fork, and compaction preserve it automatically. UIs observe committed flips through session/event.
The service surface (ctx.planMode):
| Member | Semantics |
|---|---|
set(agent, active) | Appends the standalone plan/mode event immediately when idle (no in-turn pre-step runs before the next prompt); when running, holds a pending selection for the next accepted in-turn pre-step. Returns committed/queued, a cancelled reversal, or noop. |
get(agent) | Returns { active, pending? }, separating the logged state (used to assemble the current step) from a user's mid-turn selection. |
A changed user selection contributes one plugin-sourced user/message notice when the last logged request header described the other state. Forked agents inherit logged plan state; newly spawned agents begin inactive.
Behavior differences vs. normal mode
- While active, the plugin renders the configured
sectionas the system-prompt section namedplan:policyat prompt order 50. Inactive mode contributes no text, so there is no token cost and no KV-invalidation until a transition. exit_plan_modeis always registered (both states), keeping tool schemas stable across the transition. Its execute path accepts only active plan mode, and it leaves plan mode only after an exact user approval viactx.userQuestions.- Entering or leaving changes the system prompt from order 50 onward, which shifts the KV-cache reusable boundary.
This is a notable contrast with tool- and guard-based enforcement: plan mode never forbids a tool. A deployment that needs enforced restrictions must configure the sandbox and approval controls independently.
The review / exit flow
exit_plan_mode is the reviewed exit. When the model presents its plan, the tool issues a question with the plan-review presentation intent:
const REVIEW_ID = 'plan-review'
intent: { kind: 'plan-review', approve: APPROVE_LABEL }A capable UI (the Web client) presents the plan as a decision instead of a generic question, naming the approve label; a UI that does not know the tag renders the generic option list. Either way the tool reads the same answer. An approved review returns the canonical { approved: true }; a rejected review is a failed call carrying review feedback; a dismissed review (the user closes the request to speak) is reported to the model as such, telling it to stay in plan mode and wait for the message.
EXIT_PLAN_MODE = 'exit_plan_mode' is exported from packages/plan/plan-mode/src/index.ts; its schema is pinned in docs/tool-catalog.md under @deepseek-ai/dsh-plan-mode.
The /plan command and exit conditions
When ctx.commands is composed, the package registers /plan and reserves the exact argument off for direct exit:
| Input | Behavior |
|---|---|
/plan | Select plan mode (active) |
/plan <message> | Select plan mode first, then submit <message> through agent.steer() so it becomes the next step's ordinary logged user message under plan guidance |
/plan off | Select inactive without sending model input; also cancels a pending entry |
Exit conditions are therefore two: /plan off (direct human exit), or an approved exit_plan_mode review after the model presents a plan. There is no timer-based or tool-count-based exit.
Exit-tool schema and the review exchange
exit_plan_mode remains available in both states, which keeps tool schemas stable across the transition; a caller pays the schema cost according to ToolRuntime mode either way. Execution behavior differs by state:
| State | exit_plan_mode behavior |
|---|---|
| Active plan mode | Opens the plan-review user question; approved → returns { approved: true } and leaves plan mode; rejected → failed call with review feedback; dismissed → failed call naming the user's takeover |
| Inactive | Fails the call (the tool has nothing to exit) |
The review question declares the plan-review presentation intent, naming Approve as the approving label. Because an intent changes presentation only, a UI that does not recognise the tag renders a generic option list yet answers with the same labels, so the tool reads the same { approved: boolean } value either way. The plan arguments, the review result, and a narrated active exit all extend conversation history normally and do not change the tool catalog.
The session projection unit
When the composition mounts ctx.sessionProjections (@deepseek-ai/dsh-session-projection), this package registers the plan projection unit. It folds two event kinds: a command/run record named plan with recorded args sets the wanted target (off → inactive, anything else → active), and plan/mode commits the logged state and clears it. view derives { active, pending }, where pending is a pure replay quantity recoverable from the log alone. This is served to host consumers via ./types and client aggregates via ./client. Compositions without the registry are unaffected.
Durable storage semantics
Plan state is a whole-value-replace event, not a delta: each plan/mode write logs the entire { active } value, so foldPlanMode(events) needs no accumulation to recover the current state — the last logged value (or false) wins. This has concrete consequences:
- Resume and fork reconstruct plan mode purely from the log; there is no in-memory flag to reinvent.
- Compaction shadows older
plan/modeevents along with everything else; because only the last value matters, shadowing intermediate flips changes nothing. - A
set()while the agent is idle appends immediately, because no in-turn pre-step runs before the next prompt. Aset()while running queues the selection for the next accepted in-turn pre-step; initial and continuation pre-steps both apply pending selections. A same-step request-recovery retry reuses its frozen assembly and leaves the selection pending for the next pre-step.
The distinction between active (logged, durable) and pending (a mid-turn choice not yet committed) is why get(agent) returns { active, pending? }.
Model experience: targeted summary
- While active, the
plan:policysection appears at prompt order 50; inactive contributes nothing. Entering or leaving changes the system prompt from order 50 onward and shifts the KV-cache reusable boundary — this is the one cache effect plan mode has. - The optional
/plan <message>suffix becomes one trimmed user text block throughagent.steer()and costs the same history tokens as submitting that text separately; bare/planand/plan offadd none. - An approved
exit_plan_modereview leaves plan mode after theplan/reviewexchange; a dismissed review tells the model to stay in plan mode and wait for the user's message.
Source layout and public surface
packages/plan/plan-mode/src/ is compact — four files:
| File | Concern |
|---|---|
index.ts | The plugin apply(): registers plan:policy section, the /plan command, exit_plan_mode, the plan/mode event, and ctx.planMode; exports EXIT_PLAN_MODE and the SectionConfig type |
types.ts | The plan/mode event payload and merge-extensible SessionEventMap declaration |
client.ts | The client-safe plan projection value and SessionProjectionMap key used by the Web transcript tail |
invariant.ts | Validate the plan/mode event and the projection fold for the replay invariant companion |
The plugin alone is the entire @deepseek-ai/dsh-plan-mode package — there is no separate Service Definition / Provider split like the compaction family. Plan mode is a product plugin that composes through three seams: the system-prompt section registry (for plan:policy), ctx.commands (for /plan), and ctx.userQuestions (for the exit_plan_mode review). When a deployment omits ctx.commands, the /plan command is simply not registered; the service and exit_plan_mode still function.
Configuration
- id: plan-mode
name: '@deepseek-ai/dsh-plan-mode'
config:
section: |
You are in plan mode. Explore and design before presenting the complete
plan through exit_plan_mode.section is required and non-empty; unknown keys fail at load. The package deliberately accepts no arbitrary named modes, tool filters, sandbox settings, or approval policy — a scoped, explicit design recorded in the plan-specific collaboration-state Agent Note.
UI side (brief)
packages/client/ui-plan is the Web renderer. It consumes the plugin-owned /plan command and provides the specialized plan-review renderer that presents exit_plan_mode's detail as a plan under review with an approve action. Only the Web UI has this specialized renderer; another interaction provider presents the same question through its generic option flow. The UI drives ctx.planMode.set() and observes plan/mode flips through session/event.
Caveat worth documenting: a selection made after a turn's final accepted pre-step is lost if the process exits before another accepted in-turn pre-step, so the UI must reapply it.
How plan mode differs from sandbox and approval
A common confusion is expecting plan mode to restrict behavior. It does not — it changes language guidance and gates one exit tool. The three mechanisms compose independently:
| Mechanic | Package | What it enforces |
|---|---|---|
| Plan mode | dsh-plan-mode | Soft guidance (system-prompt section) + reviewed exit_plan_mode; no tool restriction |
| Sandbox mode | dsh-sandbox / native | Filesystem and OS confinement for tool execution |
| Approval policy | dsh-user-approval | One-shot ask/never gates before tool execution |
A deployment that wants plan-first collaboration plus enforced safety configures all three; plan mode neither reads nor writes the sandbox or approval knob state. The separation is deliberate so each dimension can evolve independently.
Known limitations and design boundaries
The upstream README is explicit about what plan mode is not:
- Guidance, not enforcement. Deployments that need enforced restrictions must configure sandbox and approval controls independently.
- No creation-time plan option. Forked agents inherit logged plan state, while newly spawned agents begin inactive.
- No tool restrictions, timers, or approval policy are part of this package — a scoped, deliberate design.
- Live-child review boundary. A live child owned by another agent cannot open the
exit_plan_modereview; the failed call tells the child to include the unresolved decision in its final result. Durable fork lineage alone does not prevent a session resumed as a runtime root from opening the review. - Single specialized renderer. Only the Web UI has a
plan-reviewrenderer; another interaction provider presents the same request through its generic option flow.
Further reading
- Interactions: Commands, Questions, Approvals — the question seam and command plane that
/planandexit_plan_moderely on. - System-Prompt Assembly — how the
plan:policysection is registered at order 50. - Permissions & Approval — why plan mode is guidance only and enforcement is separate.
packages/plan/plan-mode/README.md— the full behavioral contract (durable state, projection, model experience)..agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md— the design decision record.