Files
deepseek-harness/docs/subsystems/plan.md
T
Yichen Jiang 4f36177740 feat(agent): publish step decisions as agent.step and add agent/step-boundary
Each proposed step now dispatches the serial agent/step-boundary event
after claiming input, then agent/model, and publishes the chosen model
selection as the optional agent.step until the step ends or its proposal
is rejected, fails, or is cancelled. agent/pre-step listeners, tool
conditions, prompt sections, and the provider and model variables read
agent.step, so the pre-step payload, AssembleContext, and
ToolActivationContext carry no model selection field.

Plan mode commits pending selections at the step boundary, before model
choice, and still narrates the switch in the first admitted step. The
system prompt assembles once after step/start, including runtime context,
and system-prompt/prepare is removed. The runtime-context snapshot is
admitted after the step's accepted messages, so a changed runtime context
alone no longer opens a step.
2026-10-13 17:04:55 +08:00

7.8 KiB

Plan Mode

English | 中文

Plan mode is logged per-agent collaboration state owned by dsh-plan-mode (ctx.planMode, PlanModeController): while active, a deployment-owned guidance section is included in each model request. Plan mode is soft guidance. Sandbox mode and approval policy enforce restrictions independently; neither reads or writes plan state, so deployments configure them separately. The package is optional, and the agent loop does not depend on it. It contributes the plan:policy prompt section and registers the exit_plan_mode tool and /plan command. The design reference 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. The optionally registered plan unit folds committed mode, command settlement, and the mode recorded at the latest request header. ctx.planMode reads that state through stateOf(); the first dependent access fails if the registry, plan key, or turnBoundary key is absent. Clients receive only { active, pending }; resume, fork, and compaction recover both from the log. The complete event declaration is in the persistence log event catalog.

Pending selections and the step-boundary append

Because every session event is turn-enclosed, set(agent, active) appends a selection immediately only between turns. During an open turn the selection remains pending until the next step boundary appends it, in whichever turn that occurs. A selection never forces continuation, so one made after a turn's final step boundary is appended in a later turn. set is a no-op when the target equals the logged-or-already-pending state, and get(agent) returns { active: boolean; pending?: boolean }: the logged state used by the current step plus the selected state waiting to be appended.

The only append point while an agent is running is an agent/step-boundary listener. It runs once for every proposed step, including turn 1 step 1, after the step claims its input and before agent/model, so model choice, agent/pre-step, tool conditions, and plan guidance read the committed state; same-step request-recovery retries do not run it. An append failure fails the proposal and keeps the selection pending for a later boundary. An appended user selection also records one plugin-sourced user/message notice in the first admitted step, 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 selection made after a turn's final step boundary remains process-local and is lost if the process exits before another 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 being ignored. 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 is admitted only while the committed plan state is active, so entering or leaving plan mode changes the request tool declarations and the plan guidance together; Native and PTC execution use the same admitted set, and a call outside plan mode returns UNKNOWN_TOOL. In plan mode it requires a complete markdown plan starting with a # heading and presents it for review through the user-questions seam. Approval returns { approved: true } and records a silent (non-narrated) pending exit that is appended at the next step boundary. Plan guidance therefore remains active for the rest of the assistant's current tool batch, and the tool result itself reports the transition. Keep-planning is a failed call carrying the user's feedback, so the model revises and presents again. A dismissed review returns { approved: false } and concludes the turn, so plan mode stays active until the user's next message. 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 pending entry before it is appended and becomes visible to a request.

The service

ctx.planMode owns the logged plan state, applies selected state at the step boundary and narrates it in the admitted step, and owns the plan:policy section, the /plan command, and the conditional exit tool; get/set signatures are in the generated service catalog.

Cordis API

Generated from source by scripts/gen-cordis-catalog.ts (verified fresh by pnpm run verify-cordis-catalog in doc-sync; regenerate with pnpm run gen-cordis-catalog) — the language sides differ only in locale-specific paired document paths. Signature blocks use a ts cordis-catalog fence and keep the original source JSDoc; dispatch modes are defined in the primer, and the framework-inherited ctx API lives in cordis-api/inherited.md.

ctx.planMode — PlanModeController

ctx.planMode: owns logged plan state, applies and narrates selected state at step start, the plan:policy section, the /plan command, and the conditional exit tool. Client carriers expose the projection's cropped { active, pending } view.

/**
 * Read the logged plan state and any selected state awaiting the next
 * step boundary.
 *
 * @param agent The agent to read.
 * @returns Current logged state plus a pending selection, when present.
 */
get(agent: Agent): { active: boolean; pending?: boolean }

/**
 * Select whether plan mode should be active. Between turns the method
 * appends the change immediately because no step boundary will run until
 * another prompt starts a turn. The open-turn fold is the idle signal:
 * agent status stays `running` through post-turn checkpointing, when no
 * further step boundary runs. During an open turn the selection remains
 * pending until the next step boundary. Repeated selection of the current
 * or already-pending state is a no-op.
 *
 * @param agent The agent to switch.
 * @param active Whether plan mode should be active.
 * @returns what happened: `committed` (logged now), `queued` (awaiting the
 * next step boundary), `cancelled` (an opposite pending selection
 * was cleared; the logged state already matches), or `noop` (already in that
 * state).
 */
set(agent: Agent, active: boolean): 'committed' | 'queued' | 'cancelled' | 'noop'

Types: Agent

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