> ## Documentation Index
> Fetch the complete documentation index at: https://haico.gr/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# ADR-0004: Hierarchical plan & objective

> Model the plan as a depth-ordered tree and add a per-turn editable Objective.

# 0004. Model the plan as a depth-ordered tree and add a per-turn editable Objective

## Context and problem statement

The Planning panel (formerly "To Dos") needs two things it could not express
before: an **Objective** (the agent's evolving, one-line understanding of the
user's goal) and a **hierarchical plan** (1, 1.1, 1.1.1, 2 …) instead of a flat
bullet list. Both must round-trip through the existing snapshot/branch machinery
so that "Continue from here" on an earlier node shows the objective and plan
exactly as they were then (the `snapshot(k+1)`-or-live invariant from
[ADR-0003](/docs/adr/0003-conversation-branching-via-new-thread-and-state-seeding)).
The tension is in the data model: branch rows get fresh primary keys, and
snapshots serialise the workspace to plain JSON, so any representation of the
plan's tree shape and of the objective has to survive both copies cleanly.

## Decision drivers

* **Branch/restore fidelity.** Going back to a node must reproduce its objective
  and plan; everything must ride the existing `resolve_restore_state` /
  `restore_workspace` path with no new copy mechanism.
* **Snapshot portability.** `workspace_snapshots.todos_json` is a flat JSON
  array; the tree must serialise into it and re-materialise without depending on
  row ids (which change on a branch).
* **Backward compatibility.** Existing rows and pre-feature snapshots must remain
  valid (additive columns, sensible defaults).
* **Tool ergonomics.** The agent must address and nest steps unambiguously, and
  toggling a step must stay stable across a turn.
* **Minimal blast radius** on the SSE turn loop, the dynamic prompt, and the
  graph builder.

## Considered options

1. **Plan model: depth column + global pre-order `position`.** Keep the flat,
   ordered `todos` rows; add one `depth` integer. The ordered list *is* the
   depth-first serialisation of the tree.
2. **Plan model: `parent_id` self-FK.** Make `todos` an adjacency list with a
   nullable self-referential parent.
3. **Objective storage: `objectives` table (one row/thread) + snapshot column.**
   Mirror the `documents` pattern.
4. **Objective storage: LangGraph checkpointer state.** Carry the objective in
   the agent's message-channel state.

## Decision outcome

**Chosen: Option 1 (depth column) for the plan, and Option 3 (dedicated
`objectives` table + `workspace_snapshots.objective_text`) for the objective.**

The flat, `depth`-annotated, pre-ordered list is literally the DFS serialisation
of the tree, so it drops into `todos_json` with one extra scalar key and needs
**no parent-id remapping** when a branch re-materialises the rows. The objective
mirrors the document: a per-thread row the agent writes via `set_objective` and
the user edits via `PUT /objective`, snapshotted per turn so it restores for free
on a branch. Dotted numbering (1, 1.1, …) is derived at render time from `depth`
(`compute_dotted_numbers`), so arbitrary nesting depth costs nothing in storage.
The agent nests with `add_todos(parent_index=…)` and still addresses every step by
the stable 1-based flat index that `toggle_todos` already used; dotted numbers are
display-only (the mutating todo tools are batch-only, each taking a list of steps).

### Positive consequences

* Snapshot round-trip is trivial: `{text, done, position, depth}` in, the same
  out; pre-feature snapshots default `depth` to 0 (flat) and `objective_text` to
  `None`, exactly like the pre-lock `locked` default.
* No new branch-copy code: objective + depth flow through the existing
  `resolve_restore_state` / `restore_workspace` / `delete_all_for_thread`.
* Arbitrary nesting depth with no schema change; the depth cap (if ever wanted)
  is a render/validation constant, not a migration.

### Negative consequences

* `add_todos` must compute a pre-order insertion point and shift later positions
  when nesting under a non-terminal parent (one extra `UPDATE`).
* Drag-reorder stays **flat / position-only** (depth preserved); moving a subtree
  to a new parent from the UI is deferred. An orphaned deeper item (e.g. a
  sub-step dragged above its parent) is clamped at render so numbering stays
  legible.
* Progress is counted **leaf-only** (a step with no nested children) so a checked
  parent doesn't inflate the bar; the prompt's notion of "complete" matches this.

## Pros and cons of the options

### Option 1: depth column (chosen)

* **+** Flat ordered+depth list = DFS serialisation; ids irrelevant on restore.
* **+** One additive column; pre-feature rows default to a flat list.
* **−** Non-terminal nesting needs a position shift; cross-level drag deferred.

### Option 2: `parent_id` self-FK

* **+** Canonical tree; subtree moves are well-defined.
* **−** Snapshot JSON has no stable ids, so a *positional* parent reference must
  be serialised anyway, reinventing depth/index inside the JSON.
* **−** Restore must two-pass insert and remap `parent_id` to new row ids; forces
  an `ON DELETE` cascade-vs-orphan decision.

### Option 3: `objectives` table + snapshot column (chosen)

* **+** Mirrors `documents`; user-editable; restores on a branch for free.
* **+** Lives outside the message channel, so it never bloats checkpointer state.

### Option 4: objective in checkpointer state

* **−** Per-node restore of a checkpointer field is awkward; couples the
  objective to message history and duplicates it every turn.

## Links

* Related ADRs: [0003-conversation-branching-via-new-thread-and-state-seeding](/docs/adr/0003-conversation-branching-via-new-thread-and-state-seeding)
* Related issues / PRs: #35 (Objective + rename), #37 (plan-and-execute autonomy)
* Code: `backend/app/db/models.py` (`Objective`, `Todo.depth`,
  `WorkspaceSnapshot.objective_text`), `backend/app/tools/objective.py`,
  `backend/app/tools/todos.py`, `backend/app/tools/_planning_format.py`,
  `backend/app/services/branching.py`, `backend/alembic/versions/0007_planning_objective.py`,
  `frontend/src/components/app/todo-panel.tsx`
* Docs: [agent-core-logic.md](/docs/agent-core-logic), [conversation-branching.md](/docs/conversation-branching), [tools.md](/docs/tools)
