Plan, Goal, and Todo
Three similar-looking primitives with different lifecycles
Conclusion: Plan, Goal, and Todo Are Not Three Names for One “Planning State”
DeepSeek Harness places three easily confused collaboration primitives in different lifecycles. Plan mode changes the system guidance for the next model request and records the effective boolean in the Session. Goal retains one revisable, pausable, resumable long-running completion objective and uses a separate process-local activation to decide whether work may continue automatically. Todo is a lightweight list that the model replaces wholesale while working: durable events preserve its history, but the UI-facing “standing plan” is cleared when the next Turn starts.
Entering Plan therefore does not create a Goal, creating a Goal does not synthesize Todos, and several in_progress Todo items do not make the Todo tool itself execute concurrently. All three can coexist, but none is a hidden database or scheduler for the others.
Plan answers “under which collaboration policy should the next request reason?” Goal answers “what must still be completed across autonomous rounds?” Todo answers “which concrete steps does the model currently claim are pending, active, or complete?” PLANSTATE-PLAN-STATEPLANSTATE-GOAL-TYPESPLANSTATE-TODO-TOOL
1. Three State Machines: Authority, Lifetime, and Driver
| Primitive | Authoritative fact | Non-durable state | Who advances it | When it ends or disappears |
|---|---|---|---|---|
| Plan | Last plan/mode { active } | Pending intent awaiting a Step boundary | Human /plan; model via approved exit_plan_mode | Explicit exit; never ends merely because a Turn ended |
| Goal | Whole goal/change snapshot or clear tombstone, plus admitted goal-round messages | armed / disarmed activation and driver reservation | GoalService mutations; optional round driver | complete, blocked, paused, or clear; an active goal may also be merely disarmed |
| Todo | The complete list in each todo/write snapshot | No separate scheduling state | The calling agent's todo_write | Events remain forever; the UI projection becomes null at the next turn/start |
All three gain replayability from SessionEvent, but “durable event” does not mean “equally long-lived presentation.” Plan and Goal fold current state across Turns. Todo events are durable too, while the product projection deliberately interprets the list as standing only until the next Turn.
2. Plan Has Two States: Logged Mode and Pending Selection
plan/mode is a log-only, non-Surface, last-write-wins boolean event; the default without one is false. A committed value survives Session resume, Fork, and full-log replay. The controller also owns a Session-keyed WeakMap containing the target mode that has not reached its commit boundary and whether the switch should be narrated to the model. PLANSTATE-PLAN-STATE
Idle selection
Append plan/mode immediately when no Turn is open because no in-Turn pre-Step remains to wait for.
Running selection
Store only a pending intent; system-prompt assembly already reads that target value.
Accepted boundary
The pre-Step listener first awaits downstream policy and appends only for a non-rejected, non-aborted proposed Step.
Switch narration
Add one plugin user notice only when the latest request header told the model the opposite mode.
A boundary append failure warns, allows the current Step to continue, and retains the pending intent for a later boundary. Same-Step Provider recovery reuses frozen assembly and does not consume the selection. Real-loop tests require the mode to take effect only in a later Step while keeping tool schemas identical. PLANSTATE-PLAN-BOUNDARYPLANSTATE-PLAN-LOOP-TEST
3. Plan Changes Guidance, Not Permission or the Tool Catalog
While active, plan:policy renders the deployment-provided section at system-prompt order 50; while inactive, it contributes empty text. The shipped policy asks the model to explore first, restrict itself to non-mutating investigation, submit a decision-complete plan, and explicitly avoid using Todo as a substitute for the reviewed plan. PLANSTATE-PLAN-ASSEMBLYPLANSTATE-SHIPPED-PLAN-POLICY
exit_plan_mode remains registered in both modes, and Plan does not filter other tools. An integration test even lets a write tool execute successfully while Plan is active. Actual restrictions depend on model compliance plus independent sandbox, approval, and tool policy. Plan mode neither reads nor writes those permission states. PLANSTATE-PLAN-LOOP-TEST
This keeps the tool schema and Code Mode SDK prefix stable, but an application cannot interpret a lit Plan chip as proof that mutation is rejected at execution. A deployment needing hard read-only behavior must configure that separately.
4. Three Plan Control Surfaces: Command Entry, Tool Review, and Direct Service
| Entry | Behavior | Model-history effect | Authority or failure boundary |
|---|---|---|---|
/plan | Select active | The command line stays outside Surface | Runs through the generic command plane; logs run/done |
/plan <message> | Select active, then submit an ordinary user message through agent.steer() | Only the suffix message enters model history | Selection happens first; a later steering failure does not roll it back |
/plan off | Leave, or cancel an uncommitted entry | No direct model input; a switch notice may be added when needed | Only exact lowercase off is the control word |
exit_plan_mode(plan) | Submit complete Markdown for human review | Call and result remain ordinary tool history | Committed mode must be active and review must explicitly approve |
ctx.planMode.set() | A trusted plugin directly selects a target | Influences the model only through later policy or notice | In-process service, not a public user protocol |
The exit tool requires trimmed input beginning with a level-one # heading. Consent requires exactly one plan-review answer, exactly one selected Approve, and no custom text. Keep planning, free-form feedback, duplicate answers, and missing answers all become failed tool results so the model revises. Approval records only a silent pending exit: the current assistant tool batch remains under Plan, and the next accepted pre-Step commits the switch. PLANSTATE-PLAN-CONTROLS
The user-question service requires a supplied Agent to be the exact live registry instance and a current runtime root. A child owned by another live Agent cannot open review. Durable Fork lineage does not permanently remove authority: a Session later resumed as an independent root may review normally. PLANSTATE-PLAN-REVIEW-AUTH
5. Plan’s Web View, Projection, and Crash Recovery Are Not Equivalent
The plan projection folds both command/run(name="plan") and plan/mode. The command derives a wanted target; the mode event commits active and clears wanted. It can therefore recover { active, pending } from a cold log and distribute it through history baselines and session/projection frames to multiple browsers. PLANSTATE-PLAN-PROJECTION
The Web renders a Plan × chip only while the effective target is Plan; clicking it executes /plan off through the command Remote. The same projection selects the composer placeholder. A dedicated review panel presents Approve, Keep planning, and discuss/dismiss; providers that do not understand the intent can still render generic options. PLANSTATE-PLAN-UIPLANSTATE-PLAN-REVIEW-UI
There is no independent plan.get/plan.set business API family. The Web uses generic command Remote plus projection, the model uses the stable exit tool, and host plugins use the service.
6. Goal Is a Complete State Machine with Compare-and-Set Revisions
A Goal identity remains stable across revisions, while GoalRef { id, revision } identifies one exact version. Every snapshot carries objective, phase, and maxGoalRounds; the blocked phase additionally—and only—carries { code, message }. The four phases are active, paused, blocked, and complete. PLANSTATE-GOAL-TYPES
create → active(r1)
active → edit → active(r+1)
active → pause → paused(r+1)
active → block → blocked(r+1)
active|paused|blocked → complete(r+1)
active|paused|blocked --resume, capacity remains→ active(r+1)
any current → clear tombstone(r+1)
complete → create a fresh id at r1
Create, edit, pause, resume, complete, block, and clear advance revision. An admitted positive goal-round user/message increments only roundsStarted, not revision. The next goal mutation copies the current round count into its new whole snapshot. The strict fold rejects revision gaps, illegal phase transitions, reused goal ids, regressing timestamps, and non-sequential rounds. PLANSTATE-GOAL-FOLD
7. Durable Phase and Process-Local Activation Are Separate Axes
| Combination | Meaning | Automatic continuation |
|---|---|---|
active + armed | The objective may continue and this process holds automatic authority | The optional driver may schedule after idle |
active + disarmed | The durable objective is unfinished but automatic authority has been removed | No; explicit resume is required |
paused/blocked/complete + disarmed | The durable lifecycle is stopped | No |
Activation is never logged. A fresh cache, every agent/session-start, driver hot-load, and uncertain failure disarm it; resume writes a new revision and arms it. Resume, Fork, and process restart can therefore restore objective, phase, revision, and round count without silently restoring autonomous execution authority. PLANSTATE-GOAL-SERVICEPLANSTATE-GOAL-COMMIT
GoalService also requires the exact live object in AgentRegistry, not merely the same id. After creation, edit, pause, resume, complete, block, and clear all compare the current GoalRef. When browser tabs, model tools, and human commands race, the first commit advances revision and later stale refs fail explicitly instead of overwriting it.
8. Goal Continuation Driver: Same Session, One Reservation, Per-Round Durability
goal/change arms or revises goal
→ driver coalesces wakeups per exact Agent
→ wait until Agent idle and no competing queued human work
→ flush pending durable prefix
→ reserve {goalId, revision, roundsStarted + 1}
→ Agent.followup(<goal_round>...)
→ pre-step validates full message + live revision before and after downstream hooks
→ admitted user/message increments roundsStarted
→ whole Agent returns idle
→ flush the settled round before reserving the next one
Each Agent's driver state permits one queued, claimed, or admitted attempt. The prompt writes the JSON-quoted objective, round/maxGoalRounds, and instructions to verify progress against current workspace, tool results, and durable state as an ordinary user message. It neither creates a new Agent nor copies the Session. PLANSTATE-GOAL-DRIVER
Ordinary human work arriving before a reservation or joining its batch marks competition and makes the automatic round yield. If objective or revision changes after claim, pre-Step treats the old round as stale, preserves other claimed messages, and reschedules only after the new state settles without consuming the round number. PLANSTATE-GOAL-DRIVER
The round cap counts admitted rounds, not tokens, cost, or time. At capacity the driver writes durable blocked with code round-limit. Test source requires exactly rounds 1 through N and no extra dispatch. PLANSTATE-GOAL-DRIVER-TEST
9. Goal Tool Authority Is Stricter Than GoalService
| Operation | Model-tool authority | Additional rule |
|---|---|---|
get_goal | Exact live, running, current initiator, open Turn | No human message required |
create_goal | A host-attested user message in the runtime root's current Turn | The model judges whether the task is sufficiently long-running |
edit / pause / resume | The same direct-human root Turn | Must read and copy the exact id/revision first |
complete / blocked | Direct human or the exact admitted round of the current Goal | Autonomous blocked also needs the configured round minimum |
Tool authority is derived from actual committed user/message.source inside the open Turn, not from model arguments. A goal round must match the current id, revision, and roundsStarted; an autonomous round may not edit, pause, or resume. PLANSTATE-GOAL-AUTHORITYPLANSTATE-GOAL-TOOLS
After an autonomous round successfully marks complete or blocked, current code does not call concludeTurn(). It uses deferContext() to add a <goal_complete> or <goal_blocked> closing instruction, asking the model for one user-facing message with no more tool calls. A direct-human mutation adds no such context. Tests explicitly require concludesTurn to be undefined. PLANSTATE-GOAL-TOOLSPLANSTATE-GOAL-WRAPUP-TEST
10. Goal Commands, Remote/API, and Web UI Expose Different Operation Sets
| Surface | Operations | How it reads state | Not exposed |
|---|---|---|---|
/goal command | show, create, edit, pause, resume, clear | Calls GoalService directly and renders text | complete, block, per-command round cap |
| Goal Remote/API | create, edit, pause, resume, complete, clear | Current value travels through the goal projection; mutations only acknowledge ref/cleared | get and block |
| Web GoalBar | edit, active→pause, paused→resume, clear | useProjection("goal") | create, complete, blocked-resume button, activation |
| Model tools | get, create, five update actions | get_goal returns live activation | clear |
/goal executes directly in the command plane without opening a model Turn. The Web also projects durable command/run into a right-aligned command-input bubble. GoalBar reads the latest projected CAS ref for every click and combines a synchronous ref with React pending for single-flight behavior; after clear succeeds it locally suppresses that id until the null projection catches up. PLANSTATE-GOAL-COMMANDPLANSTATE-GOAL-COMMAND-UIPLANSTATE-GOAL-COMMAND-VIEWPLANSTATE-GOAL-UIPLANSTATE-GOAL-UI-REMOTE
The Host mutation façade reuses a live Agent or cold-resumes an ordinary Session and rejects identities still owned by subagent routing. It does not reuse the model tool's “direct human Turn” rule; it is a trusted control-plane mutation of the domain. GoalError currently maps to wire internal, with the stable domain code under details.goalCode, so clients must inspect details to distinguish stale revision and other business failures. PLANSTATE-GOAL-APIPLANSTATE-GOAL-API-ROUTINGPLANSTATE-GOAL-API-ERRORS
11. Goal Failure, Cancellation, and Concurrency Recovery
| Condition | State consequence | Automatic retry? |
|---|---|---|
| Durability checkpoint failure for a Goal change or prior round | Retain the phase in the current Session log; disarm activation | No; human resume required |
Agent.followup queue failure | If revision still matches, block as queue-failed | No |
| Downstream pre-Step rejects a valid reservation | Block as prompt-rejected; no admitted round consumed | No |
| Provider/Agent error or max tokens | Goal may remain active but activation becomes disarmed | The driver performs no abnormal auto-retry |
| Cancellation belongs to a queued/claimed/admitted goal attempt | Idle checkpoint tries durable pause; falls back to disarm | No |
| Cancellation belongs only to ordinary human work | Does not invent a Goal phase; may only disarm | No |
| Plugin teardown | Close admission, disarm, cancel active attempt, await quiescence | No later round may start |
The driver serializes work with one attempt and a coalesced run promise per Agent; Goal mutations serialize conflicting control planes through revision CAS. These solve different concurrency problems. If an external LLM-retry plugin recovers successfully within the same Step, the original reservation can settle; the driver itself never converts an unclassified failure into a new round. PLANSTATE-GOAL-DRIVERPLANSTATE-GOAL-DURABILITY-TEST
12. Todo Is Whole-Value Replacement, Not a Task Database
todo_write({ todos }) must send the complete list on every call. Success synchronously appends one todo/write, and current value is the last snapshot. Each item contains only normalized non-empty content and one of pending, in_progress, or completed. There is no id, priority, dependency, nesting, patch, delete-one operation, or read-back operation. An empty array is a valid explicit empty list. PLANSTATE-TODO-TOOL
The tool schema rejects unknown item keys and invalid enum values; execute then trims content, rejects blanks and duplicates, and enforces the deployment's active-count policy. Failure occurs before append and writes no todo/write. A non-Agent caller is rejected because it has no owning Session. PLANSTATE-TODO-TOOLPLANSTATE-TODO-TEST
allowParallelInProgress is a required deployment choice. true changes both model instructions and validation to allow several active items; false rejects more than one. The shipped base profile selects true. The durable invariant deliberately does not reapply the current active-count policy, so a log written under a permissive deployment still replays after policy is tightened. PLANSTATE-SHIPPED-TODO-POLICYPLANSTATE-TODO-INVARIANT
13. Todo Events Persist; the Standing Projection Lasts Only Until the Next Turn
Turn N: todo_write(snapshot A)
todo_write(snapshot B) → projection = B
turn/end → projection still B
Turn N+1: turn/start → projection = null
... old todo/write events remain in the Session log
The todos projection is null before the first write, replaces its whole value on every write, and clears on every turn/start. It does not clear at turn/end, so a completed checklist remains visible after Agent idle until another human or automatic goal round actually opens a Turn. Cold reads, resume, and Fork replay the same full-log fold. PLANSTATE-TODO-PROJECTION
The Web TodoPanel reads only that projection, showing a collapsible card with status counts and all items. A separate todo_write tool row derives “done/total + first active + extra-active count” from durable call arguments. The first is the product's currently standing list; the second is historical tool-call presentation. Their lifetimes differ. PLANSTATE-TODO-UIPLANSTATE-TODO-ROW
todo/write itself is log-only, not a second model message. The model sees its complete tool-call arguments and a compact result. A later whole-list replacement updates only the Todo projection; it does not remove earlier tool history, whose calls continue to occupy Surface until compaction or a similar mechanism shadows them. No context plugin automatically reinjects the latest Todo snapshot into later requests.
14. “Todo Allows Parallel Work” Means the Todo Tool Itself Is Exclusive
ToolRuntime classifies a call as parallel only when its definition supplies isConcurrencySafe(args) and returns exact true. Omission, exceptions, hidden tools, and invalid definitions all fail closed to exclusive. Plan exit, all three Goal tools, and todo_write do not opt in, so each is an ordering barrier. PLANSTATE-TOOL-EXCLUSIVE
Two consecutive todo_write calls in one model batch therefore execute in order, and the second deterministically replaces the first. Multiple in_progress items mean the Agent may genuinely advance work through subagents, background commands, or workflows at the same time. They are not permission for concurrent snapshot writes and do not create a shared swarm task registry. PLANSTATE-TOOL-SCHEDULER
The normal production writer is one Agent's exclusive tool pipeline, where whole-value last-wins already defines order. Goal concurrently serves commands, Remote calls, model tools, and a scheduler, so revision CAS is necessary. Their concurrency models should not be mechanically unified.
15. Event, Surface, Projection, and Replay Matrix
| Fact | Session log | Direct model Surface | Current browser view | Resume/Fork |
|---|---|---|---|---|
plan/mode | Yes | No; acts through the system section and optional notice | plan {active,pending} | Committed active recovers; controller pending does not |
goal/change | Yes | No; tool result or round prompt makes state visible | Whole goal snapshot | Durable state recovers; activation always disarms |
Goal-round user/message | Yes | Yes | Current projection ignores it | Strict fold recovers round count |
todo/write | Yes | No; call/result are visible | todos standing list | Refolds latest write plus later turn/start |
command/run/done | Yes | No | Plan pending, Goal command bubble, generic command row | Pure-log reconstruction |
Compaction rewrites model Surface; it does not delete these log-only domain facts. Plan active and Goal lifecycle do not depend on a summary remembering them. Todo projection also remains reconstructable from the complete log, although its next-Turn clearing rule still applies. Goal-round prompts, tool calls, and results are Surface content, so a later model may know their semantics only through a checkpoint after compaction.
16. Three Confirmed Documentation Drifts
| Documentation claim | Current production behavior | Finding |
|---|---|---|
tool-goal README says autonomous complete/blocked calls concludeTurn() | Source uses deferContext() for a final closing message; tests require no conclusion marker | README is stale |
ui-goal README says Remote mutations write agent/inbox/spliced and queue Goal context | GoalService directly appends log-only goal/change and injects no model context | README still describes an older architecture |
| command-goal README says there is no continuous status widget | The Web now ships a projection-driven GoalBar with edit/pause/resume/clear | The limitation is obsolete |
These findings compare production source and tests at the pinned commit; they are not inferences from prose alone. The core Plan and Todo READMEs broadly match their current control flow within this chapter's scope. Goal evolved across more packages and shows visible synchronization lag. PLANSTATE-DOC-GOAL-TOOLPLANSTATE-DOC-GOAL-UIPLANSTATE-DOC-GOAL-COMMAND
17. Confirmed Constraints, Gaps, and Deliberate Tradeoffs
| Finding | Classification | Practical effect |
|---|---|---|
| Plan provides guidance only | Explicit design | Independent sandbox/approval must enforce hard restrictions |
| Plan's durable pending view and process intent can split after a crash | Recovery constraint | UI may show the target while prompts still use old committed state; reapply required |
| Only Web has specialized plan review and Plan chip | Presentation boundary | Other providers use generic questions and commands |
| Goal projection does not apply admitted goal-round messages | Confirmed projection gap | roundsStarted may remain at the latest mutation until another goal/change; service reads remain accurate |
| Goal projection omits activation | Explicit design | GoalBar cannot distinguish active-armed from active-disarmed and may offer pause rather than resume after recovery |
| GoalBar offers no resume button for a blocked Goal | UI feature gap | Use /goal resume or another Remote/tool path |
| Goal has no independent evaluator or resource budget | Explicit scope | Completion and same-blocker semantics remain model judgment; the cap counts rounds only |
| Todo projection deliberately clears across Turns | Product semantics | It is not a long-lived task board; historical events remain |
| Todo has no read, patch, CAS, or shared scope | Explicit scope | The model resends the whole list; it belongs to one calling Agent Session |
| Todo invariant does not reject extra item keys | Invariant coverage gap | The tool schema rejects them, but the companion misses extra fields forged by a trusted direct Session writer |
GoalService's strict cache fold reads goal-sourced user/message and immediately advances roundsStarted. The lightweight goal projection replaces only on goal/change, and tests pin the same-reference fast path for non-goal-change events. This is browser-projection freshness, not loss of the domain round. PLANSTATE-GOAL-FOLDPLANSTATE-GOAL-PROJECTIONPLANSTATE-GOAL-PROJECTION-TEST
The Todo tool's JSON schema sets additionalProperties: false. Its companion reads content/status and checks normalization, uniqueness, and enum membership, but never compares own keys. Normal model input is protected; the gap concerns trusted producers that bypass the tool and append directly to Session. PLANSTATE-TODO-TOOLPLANSTATE-TODO-INVARIANT
18. Design Assessment and Verification Status
| Design choice | Benefit | Cost |
|---|---|---|
| Plan uses a stable tool catalog and variable system section | More stable KV-cache shape and a clear review protocol | Hard restrictions must live on another permission axis |
| Goal separates durable phase from activation | Resume and Fork cannot accidentally restart autonomy | UI needs another live channel to display actual continuation eligibility |
| Goal combines revision CAS with a one-attempt driver | Cross-surface conflicts fail closed; rounds are serial and auditable | Its lifecycle and failure matrix is much more complex than Plan or Todo |
| Todo uses whole-value snapshots | Simple replay, no item identity, direct model semantics | Long lists repeatedly consume tokens and cannot support collaborative incremental edits |
| All three write log-only domain events | Compaction cannot erase control state | Readers must keep complete log, model Surface, and UI projection distinct |
This chapter traced PlanModeController, command/user-question/Plan UI, GoalService/strict fold/invariant, Goal tools/authority/wrap-up, the same-session round driver, Goal command/Remote/API/GoalBar, Todo tool/invariant/projection/UI, and their unit, integration, and real-loop test sources. Upstream dependencies are not installed, so this report does not claim local test execution. Every documented drift and gap was cross-checked against reachable production control flow at the pinned commit.
My Learning Notes
Autosaved only in this browser. Nothing is uploaded or committed. Export Markdown whenever you want to keep a copy.