Files
deepseek-harness/docs/subsystems/plan.md
T
Tianyi Cui e3af8d2ed2 docs: add eight lean subsystem pages covering every remaining service
permission, plan, invariants, http-server, storage (hub + backend seam +
domain form + domain/changed), workspace, tui, and client-modules complete
the docs/subsystems tier: every ctx service and event scope now has one
owning page, the precondition for generating per-subsystem service/event
reference into these pages. 25 new type-equiv manifest entries; 16 types
move from TYPE_LINK_EXEMPTIONS to LINK_MAP now that they have catalog
homes (dead InvariantRegistration exemption removed; catalogs
regenerated); core.md's sub-page table gains the eight rows in both
languages; the owning subsystems-catalog Agent Note records the coverage
extension. Chinese counterparts and pair records follow in the next
commit.
2026-08-09 01:27:20 +08:00

5.0 KiB

Plan Mode

English | 中文

Plan mode is logged per-agent collaboration state owned by dsh-plan-mode (ctx.planMode, PlanModeService): while active, a deployment-owned guidance section shapes each model request. It is soft guidance, deliberately independent of the sandbox mode and approval policy enforcement axes — those knobs never read or write plan state, and deployments needing a hard boundary combine them separately. The package is one optional capability, not part of the agent-loop spine; its surfaces are the plan:policy prompt section, the always-registered exit_plan_mode tool, and the /plan command. The design note owns the rationale; the package README owns the model-experience and limitation detail.

Source: packages/plan/plan-mode/src/index.ts

Logged state and recovery

plan/mode ({ active: boolean }) is a log-only, whole-value-replace session event: durable and replayable, never in the model transcript. foldPlanMode(events, end?) returns the last logged value in the prefix, or false when there is none — the state in force is always a pure fold of the session log, so resume, fork, and compaction recover it with no live mirror, and UIs observe committed flips through session/event. The complete event declaration is in the persistence log event catalog.

Pending intent and the turn-boundary flush

Because every session event is turn-enclosed, a user selection is held as pending intent until a turn boundary. set(agent, active) records the pending selection (a no-op when the target equals the logged-or-already-pending state), and get(agent) returns { active: boolean; pending?: boolean } — the logged state shaping the current step, plus the optimistic selection awaiting a boundary.

The service flushes one pending selection before the affected request assembly at three boundaries: prompt submission, ordinary turn continuation, and request-recovery retry. The flush runs after the downstream listener chain, so a selection arriving while an async listener awaits still shapes the request that boundary precedes. A flush failure is contained — plan policy can never block a prompt or turn — and the failed append stays pending for a later boundary. A flushed user selection also narrates the switch as one plugin-sourced user/message notice, but only when the last logged request header described the other state, so the model is told exactly when its context changed and never redundantly. A pending selection made while idle is process-local and lost on exit before the next boundary (README limitation).

Configuration

/** Deployment-owned plan guidance. */
interface PlanModeConfig {
  /** Guidance rendered as the `plan:policy` prompt section while plan mode is active. */
  section: string
}

A missing, blank, or non-string section and any unknown key fail at plugin load rather than silently shaping nothing. While plan mode is active, the exact section text renders as the plan:policy system-prompt section at order 50; inactive plan mode contributes no text.

The exit tool and the /plan command

exit_plan_mode stays registered while plan mode is inactive, so crossing the boundary changes only the prompt section, never the request tool catalog; execution outside plan mode fails. In plan mode it requires a complete markdown plan starting with a # heading and presents it for review through the user-interaction seam. Approval returns { approved: true } and records a silent (non-narrated) pending exit that flushes after the step — plan guidance holds for the rest of the assistant's tool batch, and the tool result itself narrates the transition. Keep-planning is a failed call carrying the user's feedback, so the model revises and presents again; a missing interaction channel and a service reload during review also fail the call rather than silently leaving plan mode.

When ctx.commands is composed, the plugin registers /plan [off|message]: bare /plan selects plan mode, any other non-empty message selects it and then submits the text through agent.steer() so it becomes the next step's ordinary logged user message under plan guidance, and the exact argument off selects inactive — which also cancels a not-yet-flushed pending entry before plan mode ever reaches a request.

The service

ctx.planMode owns the logged plan state, boundary application and narration, the plan:policy section, the /plan command, and the stable exit tool; get/set signatures are in the generated service catalog.