36 KiB
Agent Note: Plan mode — a logged per-agent session mode
Status: implemented
Superseded vocabulary (2026-07-22): Collapse named session modes into plan mode replaces this note's generic
dsh-mode,mode/set, definition map, andctx.modesdesign with the current plan-specificdsh-plan-mode,plan/mode,{ section }, andctx.planModecontract. The review, boundary, reconstructability, and sandbox-orthogonality decisions below remain in force; generic API examples are retained as the historical design this simplification removed.
Problem
Before this change, the harness had no durable way to put one agent into a distinct working stance. Plan mode needs the agent to explore and design under planning guidance, produce a reviewable artifact, cross an explicit approval boundary, and restore that state across resume and fork without making the model-visible request diverge from the session log.
The extension seams already supplied the surrounding pieces: system-prompt/assemble shapes guidance per step and the shipped request is logged in request/header* events (reconstructability); ctx.userInteraction carries the approval question and corrective feedback (ask-user precedent); SessionEventMap carries durable per-agent facts (the todo/write precedent). The missing piece was the named session state that joins those seams while leaving execution enforcement on the independent sandbox and approval axes.
Decision
The deliverable is plan mode. It ships as the first session mode — a named, logged, per-agent COLLABORATION state: a mode definition is deployment-configured guidance the model sees, while the mode IN FORCE for an agent is session state folded from its log. Modes are one axis and the enforcement knobs — the sandbox mode, the approval policy — are others: they never read or write each other, matching how Codex keeps its Plan/Default collaboration presets separate from its sandbox and approval settings. One new product package, @deepseek-ai/dsh-mode at packages/mode/mode/, owns the event vocabulary, a thin ctx.modes service, and every listener; the loop does not change. plan is the only required definition — the mode-shaped vocabulary exists so a second mode never renames durable event types, not because more modes ship now.
The state is one SessionEventMap member: mode/set, a log-only, non-surface event carrying { mode: string } with whole-value-replace semantics, plus a pure foldMode(events) that returns the mode in force — the last mode/set, or the default mode when none exists. Because the log is the fact channel, resume, fork, and compaction restore the mode with no extra machinery, and UIs read flips off session/event. The default mode is the absence of mode guidance — no section, filtering, or gate. Loading dsh-mode still contributes one stable exit_plan_mode schema in every mode; that fixed cost avoids tool-catalog churn at mode boundaries.
A mode's whole surface is soft: a mode:policy prompt section renders the active definition's guidance, while exit_plan_mode remains in the registered tool catalog across every mode and rejects at execution unless the folded mode is plan. A transition therefore changes only the system-prompt portion of the attributable request/header on the next step, keeping reconstructability green without changing native schemas or Code Mode's SDK. A mode deliberately enforces NOTHING: no execution gate, no tool filtering, no reach into the sandbox or approval knobs — a user who wants a hard read-only floor while planning switches the sandbox-mode option beside the mode picker, in either order, and neither axis disturbs the other. There is likewise NO per-mode tool allow/deny list — which tools a mode admits is an effects question, parked until tool definitions declare their effects (Deferred); a mode's restraint is its section's guidance plus the exit review.
The model leaves plan mode through the exit_plan_mode tool: its single argument is the plan text, which makes the plan reconstructable from the log, and the tool conducts the review itself through the user-interaction seam — a question whose supporting detail carries the exact plan, with options and a free-text channel, not a bare permission — so an approval flips the logged mode back to the default, and a rejection becomes the corrective error carrying the user's feedback verbatim, which keeps the model planning with direction. A user flips the mode from any surface through ctx.modes.set(); the flip is applied at the next turn boundary (session events are turn-enclosed) and narrated to the model once, only when the model-visible state actually changed.
High-level API
A plan-mode session end to end
The user switches the session to plan mode through the ACP mode picker or /plan [message] in a terminal front door, and from the next step every request ships the configured plan guidance section. When the optional message is present, that same command submits it into the affected step. The exit_plan_mode schema was already present in default and remains byte-identical.
The model explores and designs; the section's guidance is what defers changes into the plan. The sandbox and approval knobs keep whatever the user set them to — a deployment (or user) that wants kernel-enforced read-only during planning pairs plan mode with the independent sandbox-mode option.
When ready, the model calls exit_plan_mode with the plan markdown as its argument; the review question carries that exact markdown as supporting detail — approve, or keep planning, with free-text feedback welcome. A native call also renders the plan card; a Code Mode nested dispatch has no native card, so the review detail is the common presentation surface.
On approve, the tool flips the logged mode back to the default: the next step drops the plan section while retaining the same tool catalog (the changed header is in the log), and execution tracking from there is already todo_write's job. On keep-planning, the model receives a corrective error carrying the user's feedback text, revises, and re-presents.
Deployment configuration
Mode definitions are validated plugin Config — per repo convention, changeable from cordis.yml with no code edit. The deployment must provide the complete plan section; the package embeds no model instructions. Additional modes use the same config map:
- id: mode
name: '@deepseek-ai/dsh-mode'
config:
modes:
plan:
section: |
You are in plan mode: explore and design, then present the
plan for approval through exit_plan_mode.
A definition is exactly { section } — there is deliberately no per-mode tool list and no enforcement field (FAQ). Definition names use the lowercase slash-command subset /^[a-z][a-z0-9_-]*$/u; default is reserved (the absence of policy) and rejected as a key. An invalid name or unknown definition key — a tools list or an access cap included — fails validation at load; an unknown mode name fails loudly at set() time.
In the terminal
Terminal front doors get one entry command per configured definition through the plugin-owned command registry (@deepseek-ai/dsh-commands): dsh-mode registers /plan [message] for the required definition and, for example, /review [message] when review is configured. Each command records its named switch; a non-empty optional message is trimmed and passed to agent.steer(), which places it in a running agent's next step or delegates to send() for a new idle turn. The command name and result stay out of model history, while that explicit message is logged as an ordinary user message under the selected mode. The synthetic default entry contributes no command. The exit review prompts right in the terminal with no new machinery: it is an ordinary user-interaction question, so it rides the composed user-interaction provider's prompt queue that ask_user_question already uses.
Over ACP
The mode PICKER is this package's surface: session/new/session/load advertise availableModes/currentModeId from ctx.modes (consumed opportunistically via ctx.get, the tool-bash pattern), session/set_mode calls set() and notifies current_mode_update optimistically (the pending mode IS the user's selection; the logged mode/set follows at the boundary), and a session/event listener re-notifies on each logged flip that differs from the last sent. The exit tool reuses the user-interaction ACP provider's elicitation flow; its ACP mapping carries the review detail because Code Mode nested dispatches have no native plan card, while native calls may additionally stream the plan card. Individual environment knobs — sandbox mode, approval policy, the model — are NOT modes and belong to session/set_config_option (FAQ).
For agent creators
ctx.modes is the whole programmatic surface: list() returns the configured definitions plus the synthetic default entry (for pickers), get(agent) returns the folded mode plus any pending intent, and set(agent, mode) validates the name against list()'s vocabulary and records the boundary-applied intent — default is always a valid target, so exiting a mode is the same call as entering one. There is no creation-time mode option — a caller selects through set() before the first turn, which flushes identically. There is no live agent/* mirror to subscribe: UIs read mode/set off session/event, per event-domain semantics.
Detailed design
Vocabulary
'mode/set': { mode: string } // SessionEventMap merge in dsh-mode: log-only, non-surface,
// whole-value replace — the last one in the log wins
DEFAULT_MODE = 'default' // the fold of a log with no mode/set; reserved, not definable
The payload carries no reason/provenance field: a tool-driven flip sits next to its tool/call in the log and a user flip sits at its turn boundary, so the cause is log-adjacent — the same "narrative fields are derivable" call the reconstructability Agent Note made for request-header facts (the in-flight env/state event carries a source precisely because its drift variant has NO log-adjacent cause — a contrast, not a conflict). Mode names are config-declared vocabulary, not opaque cross-boundary ids, so they stay bare strings (no Branded<B>).
Config and the resolve step
interface ModeDefinition { section: string } // prompt text — a mode's whole vocabulary
interface ModeConfig { modes: Record<string, ModeDefinition> } // plan is required and owns its complete prompt
resolveConfig(config): ResolvedModes // explicit resolve (the dsh-bash template), fail-loud:
// missing plan, 'default', blank sections, and unknown keys rejected
The one-field shape is deliberate minimalism, not the final vocabulary: a per-tool policy dimension returns as effects metadata on tool definitions (Deferred), read here rather than re-declared per mode — the config shape must not need a migration when it arrives.
The fold, the service, and the flush
foldMode(events) is pure (exported for reconstructors and tests) and folds the append-only session log directly; mode/set is not a surface node, so compaction cannot shadow it. set(agent, mode) validates the name against list()'s vocabulary — the configured definitions plus the reserved default, which is rejected as a config KEY but always accepted as a set() TARGET — drops a no-op (target equals pending, else current), and otherwise records { mode, narrate } in a WeakMap pending-intent slot. It cannot append immediately because every session event is turn-enclosed and an idle agent has no open turn.
Contained listeners on the loop's interception seams (defensive patterns: a policy plugin must not block a prompt or a turn) flush the pending intent as a mode/set append — agent/prompt-submit fires inside the just-opened turn before its first assembly, and agent/turn-continuation fires after an ordinary step closes before its successor. Automatic request recovery bypasses continuation, so a prepended agent/request-error wrapper delegates through the composed policy and asynchronous backoff, then flushes only a retry decision before the waterfall returns to the loop; an effect-scoped lifetime guard suppresses a captured wrapper that resumes after plugin disposal. All three paths sit outside tool execution and log publication (post-commit session/event observers are observe-only), so every step runs under the mode its assembly folded. When the flushed mode differs from the fold at the last request/header, the flush appends one coalesced context/message notice in the same frame ("The user switched this session to plan mode."); the user-visible narration cases are enumerated in the FAQ.
The soft layer: a computed section and a stable exit schema
The registered prompt section reads the calling agent's mode from AssembleContext.agent and resolves to the active definition's guidance or ''. The loop renders per step and logs a complete request/header whenever the rendered header changes, so entering or leaving a mode is attributable. The section is static per mode and the plan itself stays in the conversation as messages and tool arguments; re-injecting separate plan state on every request (Prior art's compaction-survival hack) is unnecessary prompt churn.
The guidance contribution is { name: 'mode:policy', order: 50, text: context => … }: after persona (0), before tool guidance (100–199), and empty for default or agent-less assembly. exit_plan_mode is registered once through ctx.tools and never filtered, so native schemas and Code Mode's generated SDK remain byte-identical across mode switches; a deployment without dsh-mode lacks that one binding. There is NO tools/pre-execute listener: a mode gates nothing, while the exit tool's own folded-mode check rejects out-of-plan calls. The exit review is a question with options and feedback, not a permission, so it lives inside the tool's execution over the user-interaction seam.
exit_plan_mode
defineTool has one required plan: string argument. Native execution records it in the ordinary tool/call; Code Mode records the outer run_code source before execution and appends the normalized nested arguments in tool/code-dispatch after the dispatch settles. execute rejects an agent-less call (the todo_write precedent), rejects any folded mode other than plan, rejects an empty or heading-less plan before asking the reviewer, then conducts one single-select ctx.userInteraction.ask() review whose detail is the exact plan — approve or keep planning — with free-text feedback open. Only exactly one Approve selection consents; every other shape fails closed. Approval records a SILENT boundary-applied intent to switch to default and returns a short confirmation. The deployment guidance tells the model to make this the only and final tool call in its response; if a model violates that rule, the runtime still holds plan guidance for the rest of the batch, and the next step logs a changed header with the guidance removed and tool schemas unchanged. Every non-approval outcome returns a corrective isError and leaves the mode in plan.
Its render intent, decided up front: presentCall is a generic card titled by the plan's first heading with the plan markdown as content, plus a generic result card. Native front doors show that card before the question; Code Mode nested dispatches do not produce native call-card events, so the user-interaction detail independently carries the same plan on every provider. The seam is consumed opportunistically (ctx.get('userInteraction')), so dsh-mode composes without it and degrades to the manual exit pinned in the FAQ.
Dependencies and surfaces
dsh-mode is one product package, not a capability-seam trio (Alternatives considered): it peers on cordis, dsh-session, dsh-agent, dsh-tools, and dsh-system-prompt, injects ['tools', 'systemPrompt'], and reads ctx.userInteraction opportunistically at execute time (a type-only peer edge on dsh-user-interaction); its only UI-facing edges are optional type-only peers (dsh-commands for the per-definition entry commands). Beyond the ctx.modes call surface everything participates through listeners, so dropping the package gracefully removes modes rather than breaking a consumer. Terminal front doors need no mode-specific code: dsh-mode itself registers each definition's command on the command registry when one is composed (an optional type-only peer edge on dsh-commands), and the exit review rides the composed user-interaction provider's prompt queue. The ACP wire mapping is pinned in High-level API; package-wise the bridge takes a type-only peer edge on dsh-mode and reads the service opportunistically, so a bridge without the plugin behaves exactly as today.
The recorded scenario and the harness op
input.json gains one step op, { "op": "setMode", "modeId": "plan" }, driven through the real session/set_mode RPC, and a scripted elicitationAnswers queue. The plan-mode scenario enters plan before turn 1, runs a real cat under the independently configured sandbox, presents a plan through exit_plan_mode, receives scripted approval, then edits on the next step. The first request/header contains the full stable toolset plus the configured mode section; the post-approval changed header retains byte-identical tool schemas and removes only that section. plan-mode-reject pins corrective free-text feedback and the unchanged plan state. Both recordings replay host commands under Seatbelt or bwrap; backend-specific sandbox denial stays at the bash-tool unit tier.
The mechanical tail
No new cordis event is declared (mode/set rides session/event; the listeners attach to existing waterfalls), so the events catalog is untouched. Regenerated in the same change: the persistence log catalog (mode/set), the services catalog (ctx.modes, JSDoc-complete), the config catalog (ModeConfig), the tool catalog (exit_plan_mode), the producer/consumer map and doc graphs, and the module graph. Repo plumbing: a root tsconfig paths entry, the new group's README plus a packages map row (a new top-level group is the deliberate act that table names), an architecture.md capability-services row for ctx.modes (budget-checked), and the cookbook row upgrade.
Deferred
Each behind its own decision: subagent mode inheritance via a forwarded creation-time mode option (removed as unconsumed; it returns with its first consumer), preset modes beyond plan (read-only, accept-edits), the idle-record primitive if pending-intent loss proves real, and — the big one — effects self-declaration on tool definitions: a per-tool read-only/mutating classification (the MCP ToolAnnotations vocabulary — readOnlyHint/destructiveHint — is the natural template, with its untrusted-hint caveat implying trust tiers). That item is what a general per-mode tool policy waits on: this Agent Note first shipped an interim per-mode name allowlist and removed it before release — a hand-maintained list mislabels the effects question, must track every tool a deployment composes, and rots silently as tools arrive — so mode-scoped tool availability (and per-tool ask policies) returns as a CONSUMER of declared effects, which is its restart trigger.
The canonical examples/acp-agent composition mounts the mode and question-tool plugins on the full ACP coding server; plan mode is an additive session feature, not a second server profile. Its snapshot suite pins the plan-shaped initial header, a real read, scripted approval, stable tool schemas across the pure-removal header delta, a subsequent edit, rejection feedback, and the keyless mode wire. A self-skipping real-API smoke boots that same leaf, verifies the file before approving the review, and verifies the approved implementation afterward.
FAQ
Behavioral clarifications of the chosen design; rejected designs live in Alternatives considered, accepted costs in Consequences.
When does a user's mode flip take effect? At the next pre-assembly boundary: agent/prompt-submit covers the first step, agent/turn-continuation covers a normal successor, and the post-composed agent/request-error retry decision covers automatic recovery. A mode selected while a request or retry backoff is in flight therefore shapes the following model request. This is the "applies to subsequent requests" semantics every product in Prior art ships.
When is a mode change narrated to the model? Only when the model-visible state actually changed: the flush compares the flushed mode against the fold at the last request/header and narrates once, coalesced. A net-zero flip sequence (plan then back, all before the boundary) narrates nothing; a tool-driven exit narrates through its own tool result instead; a mode set before the first turn narrates nothing — the section is the state statement. The principle is the in-flight env-state proposal's boundary narration: a silently flipped prompt surface leaves the transcript arguing from a state the header no longer has.
What happens on resume when the config no longer defines the folded mode? A folded mode name the current config no longer defines behaves as the default mode without a notice, so the session neither gains a substitute restriction nor becomes unusable. set()'s loud validation covers only the write path; a resumed log answers to the config it finds.
What if a deployment composes no user-interaction provider? Plan mode stays safe but manual: ctx.userInteraction.ask() throws NO_PROVIDER (and an absent seam never resolves at all), the tool returns the corrective isError, and the exit degrades to the user toggling modes — never to an unreviewed exit. The mode section tells the model to present its plan through exit_plan_mode — and to ask the user in prose if that fails — so it keeps presenting instead of stalling.
Why is there no per-mode tool allowlist? Because "which tools are safe in a planning mode" is a property of each TOOL (its effects), not of the mode — a per-mode name list re-declares that fact in the wrong home, must enumerate every tool the deployment composes (MCP servers included), and rots silently as tools arrive. Until tool definitions declare their effects (Deferred, where the removed interim allowlist is archived with its restart trigger), a mode restrains by its section and the exit review; the exposure is an accepted cost (Consequences).
Do subagents inherit the parent's mode? A fork child inherits for free — the parent's mode/set is inside the seeded prefix. A spawn child starts in the default mode; a creation-time mode option and automatic forwarding by subagent providers are deferred together (Deferred).
How does plan mode relate to the sandbox's read-only mode? They are separate axes that never touch: the mode is the collaboration stance (a mode/set fold), the sandbox mode is an enforcement knob (a bash/sandbox-mode fold, the sandbox Agent Note) — plan mode neither reads nor caps it, exactly as Codex keeps its Plan/Default presets separate from its sandbox and approval settings. A user who wants kernel-enforced read-only while planning sets both: flip the mode picker AND the sandbox-mode option, in either order; each switch changes only its own fold, so there is no interference and no restore step to crash out of. The log attributes each axis to its own event — the stance to mode/set, the confinement to bash/sandbox-mode.
Why aren't sandbox mode, approval policy, or the model themselves modes? They are individual environment knobs and belong to ACP's session/set_config_option; the division this proposal pins is picker-to-modes / knobs-to-config-options, recorded in the feature matrix now that both this stack's picker and the sandbox stack's config options are landed. A mode definition may later bundle env facts (applied through ctx.envState where mounted) so a Codex-style preset stays a single mode; fusing approval policy into the mode CONCEPT itself is rejected in Alternatives considered.
Prior art
A survey of shipped plan modes (Claude Code, Cursor, Copilot, OpenCode, Gemini CLI, Cline, Windsurf, Codex) shows the same five parts everywhere — the low-authority tool policy, plan artifact, approval moment, execution-state switch, and durable state that Problem builds on.
The mode surface is a LIST everywhere it is advertised, never a boolean: Claude Code's picker offers plan beside acceptEdits (plus an auto-mode entry into plan), and Codex exposes Plan beside Default as collaboration-mode presets while keeping approval and sandbox settings separate. This is the surface the ACP feature matrix records as the gap, and what sizes the vocabulary as named modes rather than a flag.
The deployment-owned example prompt borrows the instrumental behavior, not product-specific mechanics. From Codex: remain in plan mode despite imperative implementation language, explore before asking, distinguish repository facts from user-owned choices, and make the plan decision-complete across APIs, data flow, failures, tests, and assumptions. From Claude Code: prohibit mutations and commits, prefer existing patterns, use questions only for requirements or approach choices, and finish through the exit tool rather than a prose approval request. It deliberately omits Codex protocol tags and Claude's plan-file or phased-subagent machinery because those belong to their runtimes, not this plugin contract.
The ecosystems that leave modes to convention show the failure shapes to avoid. Pi-style mode extensions fight over a last-wins global active-tool list, enforce "read-only" by prompt text alone (a hallucinated call to a still-registered tool executes), and re-inject plan state into every request to survive compaction. The contested global list and the re-injection hack close structurally here — per-agent folded state, and a log-only non-surface event compaction cannot shadow. The prompt-only shape, by contrast, is deliberately KEPT — it is what Codex ships for Plan, and it is why the mode axis composes freely with the enforcement axes: a deployment that wants a hard floor pairs the mode with the independent sandbox knob instead of the mode carrying its own enforcement (FAQ).
Alternatives considered
Permission modes as the concept (the Claude Code shape). One permissionMode fusing approval policy and tool policy. Here those are two axes with two owners: the approval seam owns "who answers this question", modes own "what surface does the model get". ACP models them as related but distinct (a mode may select an approval policy later — a mode definition gains a field, not a merger).
A capability-seam trio. Interface/implementation/consumer fits a swappable backend; a mode's variable parts are config values, not implementations. Splitting would manufacture an empty implementation package — the same "don't split preemptively" call the approval seam and todo/ made.
Loop-owned mode state. Rejected on the standing rule (plugins, not loop changes): every hook the feature needs — assemble, pre-execute, turn boundaries, session events — is already a documented seam, so a loop edit would buy nothing but coupling.
A per-mode tool allowlist with a deny-by-default gate (the first shipped shape). Removed before release. A hand-maintained name list re-declares a per-TOOL fact (its effects) per MODE: it must enumerate every tool the deployment composes — MCP servers and future registrations included — and it rots silently as tools arrive (a new read-only tool is blocked until someone edits every mode; the author burden lands on whoever knows the mode, not whoever knows the tool). It also over-promises: the list looks like a security boundary while the real boundary for anything non-shell does not exist. The general dimension is parked on effects self-declaration (Deferred); the consequence — plan mode is guidance-only, the very Pi hole the gate once closed — is accepted deliberately, priced in Consequences.
An access sandbox cap on the mode (the second shipped shape). Also removed before release. ModeDefinition.access clamped the bash seam's per-call sandbox resolution to a mode-declared ceiling (a bash/resolve-mode waterfall + ladder-min listener, with guards withholding bash under an unconfinable executor and denying escalation mid-mode). The state stayed orthogonal — the clamp never wrote the sandbox knob — but the AXES did not: entering plan changed what the sandbox enforced, fusing the collaboration stance with an enforcement level and contradicting the Codex-shaped separation the review converged on (Plan/Default presets never touch sandbox or approval settings). One user-visible symptom of the fusion: flipping the sandbox option to workspace-write while planning silently did nothing. The cap, the waterfall, and the mode→bash dependency edge were removed together; a deployment gets kernel-enforced read-only planning by pairing the mode with the independent sandbox-mode option, and a mode-triggered PRESET (a mode definition bundling suggested knob values, applied as ordinary knob switches) can return later without re-fusing the axes.
Runtime-only mode (UI- or bridge-local, unlogged). Resume and fork would silently drop the mode, and the header deltas a mode causes would have no attributable cause in the log. Logged state is what makes the mode auditable and restorable for free.
Mode flips as context/message via agent.inject(). Reuses an existing turn-enclosure path, but puts policy state into the model transcript — the model does not need to be told twice (the section already tells it), and a log-only fact should not occupy surface.
A plan-file store (.plans/ directory). A second durable home for what the log already carries replayably; a deployment wanting files can add a tool that writes them. One home per fact.
A boolean planMode instead of named modes. Too narrow for the surface the repo already tracks: ACP advertises a mode LIST and the shipped pickers fill it with more than plan (Prior art); generalizing later would rename durable event vocabulary. The string-shaped mechanism costs nothing extra now; only plan ships as a definition.
A tool-policy-stack service (the Pi-critique remedy). A dedicated composition service for tool policies is premature: this implementation performs no mode-scoped tool filtering, and future effect policies can compose through the existing guarded execution seams. Formalize only when declared tool effects create a concrete composition requirement.
Exit approval through the approval seam (a { kind: 'ask' } gate decision). The original sketch, natural while the approval seam was the only asking machinery in flight — but it seats a review in a permission chair: the seam's outcome vocabulary is deliberately closed and one-shot (allowed-once/rejected), so a rejection carries no feedback and an approval can never grow options (approve-and-accept-edits). The exit moment is a question, not a permission — the user-interaction seam gives it options plus the free-text channel, and the rejection feedback reaches the model verbatim. The approval seam remains the right seat for genuine permission gates (the sandbox escalation), and the registry's ask vocabulary stays available to deployments that want one there.
Exit by prose or steering instead of a tool. No artifact and no approval moment — the tool's argument IS the reviewable plan, and its review question is what gives the human a structured yes/no attached to the exact transition.
Consequences
What holds now, pinned by the unit, protocol, snapshot, and real-API tiers:
- The mode in force is a pure function of the session log: resume and fork restore it with no extra machinery, and a
mode/setis followed by a matching completerequest/headeron the next changed step. - A user-driven flip narrates exactly once at the next boundary and a net-zero flip sequence narrates nothing; a tool-driven exit narrates only through its tool result.
- In default mode the plugin contributes no mode section but does contribute the stable
exit_plan_modeschema; a deployment withoutdsh-modelacks that binding. - Native tool schemas and Code Mode's SDK stay byte-identical across default, plan, and custom-mode transitions; only the configured guidance section changes.
- Plan mode changes nothing on the enforcement axes: the toolset, the sandbox mode, escalation, and the approval policy behave identically in plan and default — pairing the mode with the independent sandbox/approval knobs is how a deployment hardens planning.
- Mode definitions are changeable from
cordis.ymlwith no code edit; the complete plan instructions are required there, while missing plan config, malformed definitions, and unknown keys fail at load and unknown mode names fail atset(). exit_plan_modeis always advertised, rejects outside plan, drops only plan guidance after approval, and carries keep-planning feedback in a correctiveisError; ACP mode updates and each surface's user-interaction provider carry the human side.- The docs tail shipped with the landing: READMEs, regenerated catalogs (persistence log, config, cordis services, tools), the packages map and architecture rows, and the cookbook row.
The accepted costs: a pending user flip set while idle is lost if the process dies before the next turn (the UI re-applies; the idle-record primitive is the escape hatch if this bites in practice). A mode transition changes the system prompt at order 50, so the cache path from that point onward changes, but the tool schemas and Code Mode SDK no longer churn. A mode restrains by guidance alone: a model that ignores the section CAN mutate during plan — the review moment, the session log, and independent sandbox, approval, and filesystem policies are the containment surface. Hardening planning means setting those knobs, not widening the mode; the removed enforcement shapes and their effects-declaration restart trigger remain in Alternatives considered and Deferred. The ACP mode surface carries the picker while sandbox, approval, and model selectors remain config options under the division pinned in the FAQ and feature matrix. If ACP removes session modes in favor of config options, the picker mapping can migrate without changing the logged mode state or model surface.