> ## 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.

# Architecture

> System topology, data stores, the per-thread tool-composition model, the HAI-Co² mapping, and the decisions behind them.

# Architecture

How the HAI-Co² system is put together: the runtime topology, the data stores,
the per-thread tool-composition model, how the codebase maps onto the HAI-Co²
formalism, and the load-bearing decisions behind all of it, each linked to the
[ADR](/docs/adr/index) that records *why*.

This file is the **structure and decisions** map. For how the agent actually
*runs a single turn* (the per-turn context, the ReAct loop, the model-node
retry wrapper, the SSE stream, snapshots, and typed artifacts), see
[agent-core-logic.md](/docs/agent-core-logic).

## Runtime topology

<Tabs>
  <Tab title="ASCII">
    ```ascii theme={null}
    ┌─────────────────────────────────────────────────────────────┐
    │                       Browser (Next.js)                     │
    │  ┌───────────┐  ┌────────────┐  ┌───────────┐  ┌──────────┐ │
    │  │ Prefs     │  │ Document   │  │ Chat      │  │ Planning │ │
    │  │ (top-left)│  │ /artifact  │  │ + trajec- │  │ (obj +   │ │
    │  │           │  │ (centre)   │  │  tory map │  │  plan)   │ │
    │  └───────────┘  └────────────┘  └───────────┘  └──────────┘ │
    └───────────────────────────────┬─────────────────────────────┘
                                    │  HTTPS  (JWT)
                                    ▼
                      ┌─────────────────────────────┐
                      │   FastAPI backend           │
                      │  /api/auth  /api/query/...   │
                      │  /api/workspace/...          │
                      │  /api/conversations/...      │
                      └──────────────┬──────────────┘
                                     │
                    ┌────────────────┼────────────────┐
                    ▼                ▼                ▼
            ┌──────────────┐  ┌──────────────┐  ┌───────────┐
            │ LangGraph    │  │ build_tools  │  │ Postgres  │
            │ React Agent  │  │ (per-thread  │  │ + checkpt │
            │ (stateful;   │  │ *Tools sets) │  │  tables   │
            │  retry-wrap- │  └──────────────┘  └───────────┘
            │  ped model)  │
            └──────┬───────┘
                   │
                   ▼
            ┌─────────────────┐
            │  LLM providers  │  (OpenAI · Mistral · Google · Anthropic …)
            └─────────────────┘
    ```
  </Tab>

  <Tab title="Diagram">
    ```mermaid theme={null}
    flowchart TD
        subgraph Browser["Browser (Next.js)"]
            direction LR
            P["Preferences<br/>(top-left)"]
            D["Document / artifact<br/>(centre)"]
            C["Chat + trajectory map"]
            PL["Planning<br/>(objective + plan)"]
        end
        Browser -->|"HTTPS · JWT"| API["FastAPI backend<br/>/api/auth · /api/query<br/>/api/workspace · /api/conversations"]
        API --> Agent["LangGraph ReAct agent<br/>(stateful; retry-wrapped model)"]
        API --> Tools["build_tools<br/>(per-thread *Tools sets)"]
        API --> DB[("Postgres<br/>+ checkpointer tables")]
        Agent --> LLM["LLM providers<br/>(OpenAI · Mistral · Google · Anthropic)"]
    ```
  </Tab>
</Tabs>

The frontend is a single workspace shell ([`frontend/src/app/app/page.tsx`](https://github.com/petrosrapto/HAICO/blob/main/frontend/src/app/app/page.tsx))
with four panels: Preferences (top-left), the Planning panel (Objective + plan,
lower-left), the centre document/artifact panel, and the chat with the
co-construction **trajectory map** above it. The side panels talk to the backend
over plain REST; the chat opens an SSE stream to the agent. The wiring of every
panel and event is detailed in [agent-core-logic.md](/docs/agent-core-logic).

## The per-thread tool set

`build_tools(thread_id)` (in [`backend/app/tools/manager.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/manager.py))
is **not** a registry. It composes the per-domain `*Tools` collections
(`DocumentTools`, `ObjectiveTools`, `TodoTools`, `PreferenceTools`, `ChartTools`,
`ArtifactTools`), all bound to the same `thread_id`, and flattens their
`@workspace_tool`-decorated methods into one `List[StructuredTool]` for the
React agent (15 tools today). Adding a tool is a method on an existing `*Tools`
class; adding a domain is a new class appended in `_collections_for`. That is
the only wiring change. The decorator (`@workspace_tool`) handles the rest:
schema extension, a fresh DB session per call, artifact persistence, and the
JSON envelope. See [agent-core-logic.md §5](/docs/agent-core-logic) and
[tools.md](/docs/tools).

## Persistent state

Postgres holds both the HAI-Co² shared workspace and the agent's memory:

| Table                   | Holds                                                                                                                                              |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `objectives`            | the agent's one-line read of the goal (`Uᵗ`), one row per thread                                                                                   |
| `documents`             | the centre-panel document (`X̂`), `unique(thread_id)`                                                                                              |
| `todos`                 | the plan (`X_j`), a depth-ordered, pre-order tree (one `depth` column)                                                                             |
| `preferences`           | the decoded hard/soft constraint set (`Uᵗ`), each with a `locked` flag                                                                             |
| `artifacts`             | typed artifacts (charts, …), tagged with the producing `turn_index`                                                                                |
| `workspace_snapshots`   | one immutable row per user turn, captured **before** the agent runs (objective, document, prefs, todos, user message), the replay/branch substrate |
| `conversation_users`    | thread → user link (ownership, listing, soft-delete) carrying a monotonic `turn_index`                                                             |
| `conversation_branches` | parent links for forked threads (`parent_thread_id`, `branch_point_turn`)                                                                          |
| `feedback`              | per-turn user feedback on the co-construction (`scope`+`aspect`-discriminated), anchored on `(thread_id, turn_index)`                              |

Alongside these, the **LangGraph checkpointer tables** store the full per-thread
message history (the HAI-Co² informational state `I`). They are LangGraph's own
schema, created idempotently (`CREATE TABLE IF NOT EXISTS`) by
`checkpointer.setup()` in [`init_checkpointer.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/db/init_checkpointer.py)
at container startup. The agent is otherwise **stateless per request**:
`build_agent()` is rebuilt every turn, but its memory persists because both the
checkpointer and the workspace tables are keyed by `thread_id`. The full schema
(after migration 0007) is documented in [database-schema.md](/docs/database-schema).

## Key flows

### 1. User asks a question (`/api/query/stream_steps/sse`)

1. Request authenticated via JWT.
2. `thread_id` validated (must belong to the user), or created on first message.
3. A workspace **snapshot is captured and `turn_index` bumped BEFORE the agent runs** (`_capture_snapshot` writes one immutable `workspace_snapshots` row recording the objective, document, prefs, todos, and user message the agent will see this turn).
4. `build_agent()` is called with the request's model config. The system prompt is **rebuilt dynamically each turn** by an async callable that injects the current `<workspace>` (objective, preferences, todos, document, latest-artifact pointer); this `SystemMessage` is recomputed, never persisted to the checkpointer. The tool-bound model is wrapped so blank / malformed / truncated generations are retried inside the model node (see [agent-core-logic.md §4.1](/docs/agent-core-logic)).
5. Agent streamed via `agent.astream(..., stream_mode="updates")`; each newly produced message becomes an SSE `step` event (frames are deduplicated on a content signature), with `: keep-alive` comments emitted during long single generations.
6. Tool calls are visible in the stream → the frontend can preview *what the agent is about to do*.
7. When a tool's envelope carries a typed artifact (e.g. a chart), a dedicated SSE `artifact` event is emitted so the centre panel updates mid-stream.
8. A final `complete` event closes the stream (or an `error` event on failure).

### 2. Agent edits the shared workspace

Tools registered in `app/tools/` can mutate `objectives`, `documents`, `todos`,
and `preferences`. After the stream completes, the frontend re-fetches workspace
state (`refreshWorkspace`) so the side panels reflect the agent's mutations;
planning panels also refresh **mid-stream** as tool results land. Typed
artifacts arrive **live** via SSE `artifact` events during the stream.

Preferences carry a `locked` flag. The **agent path respects it**:
`update_preferences` / `remove_preferences` skip any locked preference (reporting
it as skipped rather than erroring). The **user path ignores it**: the REST
endpoints edit/delete with `respect_lock=False`, and `PATCH /preferences/{id}`
accepts a `locked` field to lock/unlock. The injected `<preferences>` block marks
locked rows with `(locked)`.

### 3. Branching & restore (`/api/conversations/{thread_id}/branch`)

Every completed user turn is already an immutable "commit", the
`workspace_snapshots` row keyed by `(thread_id, turn_index)` captured before the
agent ran. A conversation forks at any turn *k* into a **new `thread_id`**: the
workspace as of turn *k* is copied, and the message-history prefix is seeded into
the new checkpointer thread with one `aupdate_state` call. The choice of "new
thread + seeded state" over per-table `branch_id`s is recorded in
[ADR-0003](/docs/adr/0003-conversation-branching-via-new-thread-and-state-seeding);
the full mechanics live in [conversation-branching.md](/docs/conversation-branching).

The Studio surfaces this as the **co-construction trajectory map** above the chat
(`GET /api/conversations/{thread_id}/graph` assembles the conversation family:
nodes = turns, edges = turn/branch links). **Continue from here** previews any
step read-only and forks a new path only once the user sends a message there;
per-message **retry** and **edit** actions fork the same way ("fork, never
destroy"), so earlier states are never overwritten.

## Mapping to HAI-Co²

| HAI-Co² object             | Implementation                                                                                            |
| -------------------------- | --------------------------------------------------------------------------------------------------------- |
| `X̂` (working artifact)    | Document + typed artifacts (charts etc.) rows                                                             |
| `X_j` (construction space) | Document + todo plan, a depth-ordered, numbered tree (DB rows)                                            |
| `Uᵗ` (utility / objective) | `objectives` table (the goal) + preferences table (decoded hard/soft constraints)                         |
| `I` (informational state)  | LangGraph checkpointer (full message history) + preference rows + `workspace_snapshots` (per-turn replay) |
| `π` (policy)               | Compiled React agent (with the model-node retry wrapper)                                                  |
| Refinement maps `f_j⁻¹`    | LLM calls inside tools (high-T sampling possible)                                                         |
| Active edit feedback       | User UI edits → API → tool-invoked agent context                                                          |

## Key decisions (the ADRs)

The structural choices above are recorded as [Architecture Decision Records](/docs/adr/index);
the ADR is the source of truth for *why*. This doc describes *what is*; when the
two disagree, the newer ADR wins and this file should be updated.

| Decision                                                                                    | ADR                                                                           |
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| We record decisions as MADR-style ADRs under `docs/adr/`                                    | [ADR-0000](/docs/adr/0000-record-architecture-decisions)                           |
| Two-branch (`develop` + `main`) tag-driven release model                                    | [ADR-0001](/docs/adr/0001-two-branch-model)                                        |
| Dual development pathways: traditional + AI-assisted, gated by issue label                  | [ADR-0002](/docs/adr/0002-dual-development-pathways)                               |
| Branch conversations as new threads seeded via checkpointer state, not per-table branch IDs | [ADR-0003](/docs/adr/0003-conversation-branching-via-new-thread-and-state-seeding) |
| Model the plan as a depth-ordered tree; add a per-turn editable Objective                   | [ADR-0004](/docs/adr/0004-hierarchical-plan-and-objective)                         |

A few decisions are not (yet) their own ADR but shape the codebase; they are
documented in the agent deep-dive instead:

* **Dynamic, non-persisted system prompt**: the workspace is re-injected every
  turn rather than written into checkpointer state, trading recomputation for
  bounded history. See [agent-core-logic.md §3](/docs/agent-core-logic).
* **Snapshots, not persisted prompts, for replay**: one immutable
  `workspace_snapshots` row per turn makes any past context deterministically
  reconstructable. See [agent-core-logic.md §6](/docs/agent-core-logic).
* **Typed-artifact channel**: the agent emits typed JSON payloads
  (`{artifact_type, payload}`), never SVG or React code; the frontend owns
  rendering. See [agent-core-logic.md §10.4](/docs/agent-core-logic).
* **Model-node reliability wrapper**: blank / malformed / truncated provider
  turns are retried (or warned on) inside the model node, transparently to the
  SSE stream. See [agent-core-logic.md §4.1](/docs/agent-core-logic).
