Skip to content

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):

MemberSemantics
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 section as the system-prompt section named plan:policy at prompt order 50. Inactive mode contributes no text, so there is no token cost and no KV-invalidation until a transition.
  • exit_plan_mode is 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 via ctx.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:

ts
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:

InputBehavior
/planSelect 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 offSelect 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:

Stateexit_plan_mode behavior
Active plan modeOpens 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
InactiveFails 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/mode events 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. A set() 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:policy section 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 through agent.steer() and costs the same history tokens as submitting that text separately; bare /plan and /plan off add none.
  • An approved exit_plan_mode review leaves plan mode after the plan/review exchange; 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:

FileConcern
index.tsThe 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.tsThe plan/mode event payload and merge-extensible SessionEventMap declaration
client.tsThe client-safe plan projection value and SessionProjectionMap key used by the Web transcript tail
invariant.tsValidate 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

yaml
- 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:

MechanicPackageWhat it enforces
Plan modedsh-plan-modeSoft guidance (system-prompt section) + reviewed exit_plan_mode; no tool restriction
Sandbox modedsh-sandbox / nativeFilesystem and OS confinement for tool execution
Approval policydsh-user-approvalOne-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_mode review; 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-review renderer; 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 /plan and exit_plan_mode rely on.
  • System-Prompt Assembly — how the plan:policy section 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.