Skip to content

Planning

Planning is the contract that turns “do this change” into an explicit implementation strategy before heavy mutation starts.

In agent systems there are usually three distinct concepts that get conflated:

  1. Execution checklist tracking (task-progress state)
  2. Planning collaboration mode (an interaction phase where implementation is intentionally deferred)
  3. Plan artifact (a durable spec that another engineer/agent can execute)

Keeping these separate is critical. If they collapse into one concept, you get brittle behavior: progress checklists treated as design specs, or design specs accidentally triggering execution.

Planning is hard because agent runtimes need to enforce semantics across model behavior, tool handlers, and UI protocols:

  • The model must be able to propose strategy without prematurely mutating files.
  • The runtime must encode which actions are allowed in planning context vs execution context.
  • The client must render plan state clearly enough that humans can approve, reject, or refine.
  • The system must avoid deadlocks where planning never exits, or exits without decision-complete detail.

This is not only prompt engineering. It is a runtime-state problem.


  • Todo Write for structured checklist mutation during execution.
  • Todo Read for state re-synchronization and progress visibility.
  • Agent Loop for where planning behavior sits in the full turn lifecycle.

Aider at commit b9050e1d5faf8096eae7a46a9ecc05a86231384b does planning as a workflow pattern, not as a first-class planning tool.

Aider does not expose a structured planning tool like update_plan, plan_enter, or plan_exit.

In aider/commands.py, planning-adjacent commands are chat-mode switches:

  • cmd_ask()
  • cmd_code()
  • cmd_architect()
  • cmd_chat_mode()

There is no cmd_plan or model-callable planning function.

/ask routes through _generic_chat_command(args, "ask") and uses AskCoder (aider/coders/ask_coder.py), which is explicitly non-editing.

Operationally this is how Aider users plan:

  1. switch to ask mode
  2. discuss approach in plain conversation
  3. switch back to code/architect mode to execute

The planning state is conversational context, not a typed plan object.

Architect Mode as Planning + Translation Pipeline

Section titled “Architect Mode as Planning + Translation Pipeline”

ArchitectCoder (aider/coders/architect_coder.py) is Aider’s strongest planning-like mechanism:

  1. Architect model generates a detailed change strategy in natural language.
  2. Aider asks for confirmation (confirm_ask("Edit the files?") unless auto-accept).
  3. Editor model converts that plan text into concrete edits.

This is a planning pipeline, but still not a tool protocol with plan lifecycle events.

Planning in Legacy Function-Calling Formats

Section titled “Planning in Legacy Function-Calling Formats”

Older Aider function-coder variants include an explanation field described as a step-by-step plan:

  • aider/coders/single_wholefile_func_coder.py
  • aider/coders/wholefile_func_coder.py
  • aider/coders/editblock_func_coder.py

That field is embedded in an edit payload and is not persisted as an independent planning object with status transitions.

Aider has good planning behavioral ergonomics but limited planning protocol semantics. Planning is a convention layered over chat modes, not a runtime primitive with explicit lifecycle states.


Codex at commit 4ab44e2c5cc54ed47e47a6729dfd8aa5a3dc2476 has an explicit split between planning checklist updates and true planning mode.

update_plan Is a Checklist Tool, Not Plan Mode

Section titled “update_plan Is a Checklist Tool, Not Plan Mode”

core/src/tools/handlers/plan.rs defines update_plan and directly documents intent: the function output is not important; the input payload is for clients to render checklist state.

protocol/src/plan_tool.rs defines a typed schema:

  • StepStatus: pending | in_progress | completed
  • PlanItemArg { step, status }
  • UpdatePlanArgs { explanation?, plan[] }

Important semantic guard in handle_update_plan(...):

  • if current mode is ModeKind::Plan, tool returns an error
  • message: update_plan is a TODO/checklist tool and is not allowed in Plan mode

This prevents mode confusion at runtime.

core/src/tools/spec.rs always registers PLAN_TOOL and binds handler update_plan.

Execution path:

  1. model issues update_plan function call
  2. handler parses UpdatePlanArgs
  3. handler emits EventMsg::PlanUpdate(args)
  4. tool returns textual acknowledgement (Plan updated)

Notably, descriptive prompt guidance says “at most one in_progress”, but handler does not enforce cardinality; it only parses and forwards.

EventMsg::PlanUpdate(UpdatePlanArgs) is part of the core protocol (protocol/src/protocol.rs).

Render paths:

  • TUI: tui/src/chatwidget.rs -> on_plan_update() -> history_cell::new_plan_update()
  • History cell (tui/src/history_cell.rs) renders checklist with status styling
  • Human exec output (exec/src/event_processor_with_human_output.rs) prints status markers (, , )

exec/src/event_processor_with_jsonl_output.rs converts PlanUpdate events into a todo_list thread item lifecycle:

  • first update in a turn -> item.started
  • subsequent updates -> item.updated
  • turn completion -> item.completed

StepStatus::Completed maps to TodoItem.completed=true; all other statuses map to false.

This means Codex exposes planning checklist state to SDK consumers as a stable typed item (sdk/typescript/src/items.ts).

Plan Mode Is a Separate Collaboration Contract

Section titled “Plan Mode Is a Separate Collaboration Contract”

Plan mode preset comes from core/src/models_manager/collaboration_mode_presets.rs, loading core/templates/collaboration_mode/plan.md.

Key plan-mode mechanics:

  • request_user_input allowed only in ModeKind::Plan (protocol/src/config_types.rs)
  • plan output is expected inside <proposed_plan>...</proposed_plan> block
  • streaming parser (core/src/proposed_plan_parser.rs) and codex.rs split normal assistant text from plan deltas
  • plan deltas emit EventMsg::PlanDelta; finalized plan emits completed plan item

So Codex has two planning channels by design:

  1. checklist tracking (update_plan / PlanUpdate)
  2. plan authoring mode (<proposed_plan> / PlanDelta)

app-server/src/bespoke_event_handling.rs explicitly comments that update_plan is a todo/checklist tool and not plan-mode updates, then maps EventMsg::PlanUpdate to TurnPlanUpdatedNotification for API v2 clients.

This reinforces the split across transport boundaries.

Codex has direct tests validating planning semantics:

  • core/tests/suite/tool_harness.rs
    • successful update_plan event emission
    • malformed payload rejection
  • exec/tests/event_processor_with_json_output.rs
    • todo_list start/update/complete lifecycle from plan updates

OpenCode at commit 7ed449974864361bad2c1f1405769fd2c2fcdf42 models planning as an experimental mode transition between two primary agents (build and plan) plus a plan file contract.

packages/opencode/src/tool/plan.ts defines two tools:

  • plan_enter
  • plan_exit

Both tools:

  1. ask the user a structured yes/no question via Question.ask(...)
  2. if accepted, synthesize a new user message with target agent (plan or build)
  3. inject synthetic text telling the next turn what to do

This is not merely prompting; it changes session agent routing.

packages/opencode/src/agent/agent.ts defines native plan agent permissions:

  • question: allow
  • plan_exit: allow
  • broad edit deny, with exception for plan markdown path patterns
  • external directory allowlist for plan storage path

This is a hard permission wall: plan mode is constrained to plan-artifact editing and read-only exploration.

Session.plan(...) in packages/opencode/src/session/index.ts computes durable plan path:

  • repo present: <worktree>/.opencode/plans/<timestamp>-<slug>.md
  • no VCS: <global data>/plans/<timestamp>-<slug>.md

This yields a concrete artifact that survives turn boundaries.

packages/opencode/src/session/prompt.ts injects synthetic reminders for plan mode.

With OPENCODE_EXPERIMENTAL_PLAN_MODE enabled, injected instructions enforce a multi-phase workflow:

  1. explore and clarify
  2. design with subagents
  3. review
  4. write final plan file
  5. call plan_exit

The injected reminder explicitly forbids non-readonly execution outside the plan file.

packages/opencode/src/tool/registry.ts only exposes plan_enter/plan_exit when:

  • OPENCODE_EXPERIMENTAL_PLAN_MODE is true
  • client is CLI

So planning mode is feature-flagged, not universal.

OpenCode planning is composed from multiple primitives:

  • question for decision collection
  • todowrite for execution checklist visibility
  • plan_enter/plan_exit for mode transitions
  • plan markdown file for durable design artifact

This compositional design is flexible but requires careful permission and UX coordination.


“Plan” often names both a checklist and a collaboration mode. Codex avoids this with explicit runtime guards and comments; systems without this split drift into inconsistent behavior.

Prompt Rules Without Runtime Guards Are Fragile

Section titled “Prompt Rules Without Runtime Guards Are Fragile”

Instruction text like “only one in_progress” helps model behavior but is not a safety boundary. If invariants matter, enforce them in handler validation.

Plan-Only Modes Need File-System Guardrails

Section titled “Plan-Only Modes Need File-System Guardrails”

If a planning agent can still mutate arbitrary files, “planning mode” becomes advisory. OpenCode’s explicit allowlist for plan files is the right direction.

Separate Streams Need Clear Rendering Semantics

Section titled “Separate Streams Need Clear Rendering Semantics”

Codex’s <proposed_plan> streaming parser exists because interleaving plan text with normal assistant text causes UI ambiguity and tool-loop confusion.

Plan loops can become endless if no concrete “done planning” transition exists. OpenCode’s plan_exit handshake and Codex’s <proposed_plan> completion convention both solve this in different ways.


Keep the Split: Checklist vs Plan Mode vs Plan Artifact

Section titled “Keep the Split: Checklist vs Plan Mode vs Plan Artifact”

Adopt three explicit primitives:

  1. update_plan (execution checklist)
  2. collaboration Plan mode (planning-only behavior profile)
  3. durable plan artifact (.openoxide/plans/*.md)

Do not merge these abstractions.

Plan mode should enforce:

  • read-only tool subset by default
  • explicit allowlist for plan artifact path
  • structured user-decision tool availability (request_user_input)

Checklist tool should be disallowed in Plan mode unless explicitly intended.

update_plan:

  • typed enum statuses
  • optional explanation
  • handler-level validation for exactly one in_progress when incomplete
  • emit typed PlanUpdate events

Plan finalization:

  • dedicated PlanDelta stream item for progressive rendering
  • finalized plan event with markdown payload

Store plan files under:

  • <repo>/.openoxide/plans/ for VCS-backed sessions
  • global app data fallback for no-repo sessions

Include session id + timestamp in filename for traceability.

Expose two visually distinct lanes:

  • checklist updates (task tracking)
  • plan document stream (design artifact)

Support TUI + API consumers with consistent event vocabulary.

  • serde/schemars for tool schemas
  • tokio channels for mode/event flow
  • ratatui for plan/todo cell rendering
  • pulldown-cmark for proposed plan markdown visualization
  • sqlx or rusqlite for optional persisted checklist history
  1. Treat planning as runtime state, not only prompt text.
  2. Enforce mode invariants in code, not conventions.
  3. Keep checklist and design-spec streams separate end-to-end.
  4. Make plan exit an explicit protocol step.