Files
deepseek-harness/packages/mode/mode/README.md
T
kingwl 1f38b211be Merge origin/master (generic task runtime #219, single-exe closure, package renames)
Semantic resolutions beyond line merges:
- The bash seam keeps resolveMode + the bash/resolve-mode waterfall on
  master's task-free BashExecutor (run/start/resolve only; BashProcess
  handles); tool-bash consults it at its stamping site and escalation
  baseline on master's render/background split, with a waterfall test on
  the recording executor.
- dsh-mode's BASH_FAMILY narrows to ['bash']: bash_output/bash_kill are
  replaced by the kind-generic task_output/task_kill, which span every
  task kind and only observe or stop work, so the access cap withholds
  only the starter it can reason about.
- The plan-mode snapshot suite adopts master's pin grammar (tool-schema
  sidecars; the expectedHeaderSnapshots extension is gone — the exit
  transition deltas, and entering-before-turn-1 needs no second
  snapshot); modes-advertise joins the plan header class (no-model, so
  membership is vacuous). Fixtures re-recorded on the acp-demo bin;
  the replay overlay gains the passthrough sandbox runner.
- examples/plan-acp-agent rewires to @deepseek-ai/dsh-acp-demo and drops
  its tool-bash entry (the spine bundle now composes it); dsh-stdio (the
  renamed stdio-chat home) keeps its /mode command and gains the dsh-mode
  peer edge; the acp bridge keeps the modes surface beside master's
  permission presets.
- mode README gains the Model Experience / Known Limitations sections the
  new README gates require; AGENTS.md ceiling 1370 → 1440 for the kept
  mode/ layout line and Agent efficiency section.
2026-07-15 23:17:18 +08:00

8.9 KiB

@deepseek-ai/dsh-mode

Session modes: named, logged, per-agent policy states. Plan mode is the first shipped definition — the agent explores and designs under a read-only stance, produces a reviewable plan, and crosses back into full authority through an explicit review.

The mode state is a session event

mode/set ({ mode: string }) is a log-only, non-surface SessionEventMap member with whole-value-replace semantics; the pure foldMode(events) returns the mode in force (the last mode/set, else default). 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 — there is no live mirror.

The default mode is the absence of policy: no section, no cap. An agent that never sees a mode/set behaves byte-identically to a deployment that never loads this plugin.

What a mode enforces

The guidance section. A system-prompt/assemble listener renders the mode's section text as the mode:policy section (order 50) while the mode is in force, and shows the exit_plan_mode tool IFF the folded mode is plan — on the wire and, under the registry's Code Mode, in the tools:sdk section alike. Every transition therefore surfaces as an attributable request/header event on the next step (entering plan adds the exit tool, a front-of-list insertion with no delta form → the full fallback snapshot; the approved exit removes exactly that tool and the section, a pure removal → one request/header-delta).

The access cap. A definition may declare access — the widest sandbox access shell commands run under while the mode is in force, on the SANDBOX_MODES ladder from @deepseek-ai/dsh-bash (read-only | workspace-write | danger-full-access). The built-in plan ships access: 'read-only': exploration commands run for real, and a write is denied by the sandbox itself. The cap is a clamp, not a switch: a bash/resolve-mode waterfall listener returns min(resolved, access) on the ladder. The session's own sandbox-mode knob (bash/sandbox-mode events) is never written — the two folds compose at read time, so the knob and the mode switch in any order without disturbing each other, and a knob flipped during plan re-emerges intact on exit. Two guards ride with a declared cap: the bash trio (bash/bash_output/bash_kill) is hidden and denied while no confining executor is mounted (ctx.bash.sandboxMode unset — an unconfinable shell cannot honor the cap), and a bash call carrying sandbox_permissions is denied outright (the cap would otherwise be pierceable mid-mode by one approval prompt). A mode without access gets neither guard.

Deliberately absent: a tool allow/deny list. Which tools a mode admits is an effects question — a per-tool read-only/mutating classification the harness does not yet have. Until tool definitions declare their effects (the plan-mode RFC's deferred item), a mode's non-shell restraint is the section's guidance and its shell restraint is the sandbox; the config vocabulary is exactly { section, access? }, and an unknown key (a tools list included) fails loud at load.

ctx.modes

list() returns the selectable vocabulary (default first, then the configured definitions); get(agent) returns the folded mode (a folded name the config no longer defines reads as default) plus any pending intent; set(agent, mode) validates against list() (loud on unknown; default is always a valid target) and records a pending intent — every session event is turn-enclosed and an idle agent has no open turn, so the service flushes the intent on the loop's interception seams (agent/prompt-submit inside the just-opened turn, agent/turn-continuation after each step closed — both outside any log emit, where a post-commit session/event observer could not append) and, when the flushed mode differs from what the last logged request header told the model, appends one coalesced context/message notice in the same frame. A net-zero flip sequence appends nothing.

AgentOptions.mode (declaration-merged) seeds a child's initial mode through the same pending-intent flush; explicit options beat the logged baseline on create AND resume. A fork child needs no mechanism — the parent's mode/set is inside the seeded prefix.

exit_plan_mode

The model-facing exit tool. Its single required argument is the plan text — a durable, replayable log artifact riding the ordinary tool/call event. execute re-checks the folded mode, then conducts the review over the user-interaction seam (ctx.get('userInteraction'), opportunistic): one single-select question — Approve, or Keep planning — with the free-text channel open. Approve records the switch back to default as a silent boundary-applied pending intent (flushed at this step's end — the plan policy, the sandbox clamp included, keeps holding for any remaining call of the same assistant response) and the next step runs unclamped; every other outcome (keep-planning with the user's feedback verbatim, an aborted question, no provider) returns the corrective isError and the mode stays plan. presentCall renders a generic card titled by the plan's first heading with the plan markdown as content; over ACP the review rides the same elicitation flow as ask_user_question, in the terminal the stdio provider's prompt queue.

Config

- id: mode
  name: '@deepseek-ai/dsh-mode'
  config:
    modes:
      plan:
        section: |
          You are in plan mode: ...
        access: read-only

Definitions are validated at load (resolveConfig): the built-in plan (the shipped guidance section plus access: read-only) merges unless overridden, default is rejected as a key, an access outside the SANDBOX_MODES ladder throws, and any other key — a tools list included — fails loud. An unknown mode name fails loudly at set() time.

Model Experience

System prompt

What the model sees: In the default mode, nothing — assemblies are byte-identical to a deployment without this plugin (the always-registered exit_plan_mode tool is filtered from the wire and the Code Mode SDK). In a non-default mode, the mode's section renders as the mode:policy section (order 50) and, in plan mode, the exit_plan_mode tool joins the toolset; under an unhonorable access cap the bash tool disappears. A mode flip mid-session appends one coalesced context/message notice when the last header disagrees.

Token effect: Zero in the default mode. In plan mode, the section text plus one tool schema per request; every mode transition changes the logged header and therefore resets the provider prefix cache.

Plan-mode policy section

You are in plan mode: a read-only planning state. Explore, analyze, and design; do not modify anything yet — edits and other side effects belong in the plan and run after its approval, not in this mode. Where a bash tool is present it runs under a read-only sandbox: commands that only read work normally, while a command that writes is denied by the sandbox — that denial marks the edge of plan mode rather than a bug, and sandbox escalation is not offered here; put the step in the plan for after approval instead. When a decision or a missing detail blocks the plan, ask the user through the ask_user_question tool where it is available. A finished plan is delivered by calling exit_plan_mode — that call is what puts it in front of the user for review, so prefer it over pasting the plan as a plain reply or asking the user to switch modes themselves. If exit_plan_mode is unavailable or its review fails, ask the user to switch the session out of plan mode instead of pressing on.

Exit review

What the model sees: The exit_plan_mode call carries the full plan markdown as its argument (retained in context as ordinary tool args); its result is one short confirmation or the reviewer's feedback verbatim.

Token effect: The plan text is paid once as tool args and stays in the conversation; a keep-planning round adds one feedback-sized result per revision.

Known Limitations and Deferred Work

  • Non-shell restraint is guidance-only — until effects self-declaration lands on tool definitions, a non-default mode restrains tools other than bash by its section text alone; the plan-mode RFC archives the removed interim allowlist and its restart trigger.
  • The cap guard names bash rather than deriving it — the same effects item generalizes it.
  • A pending flip set while idle dies with the process — the UI re-applies; the idle-record primitive is the escape hatch if this bites.
  • Subagent mode inheritance is deferred — a fork child inherits via the seeded prefix; a spawn child starts default unless its creator seeds AgentOptions.mode.

RFC: plan mode.