> ## Documentation Index
> Fetch the complete documentation index at: https://dev.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.md) 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.md).

## Runtime topology

**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 …)
        └─────────────────┘
```

**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)"]
```

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.md).

## 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.md) and
[tools.md](/docs/tools.md).

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

## 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.md)).
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.md);
the full mechanics live in [conversation-branching.md](/docs/conversation-branching.md).

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.md);
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.md) |
| Two-branch (`develop` + `main`) tag-driven release model | [ADR-0001](/docs/adr/0001-two-branch-model.md) |
| Dual development pathways: traditional + AI-assisted, gated by issue label | [ADR-0002](/docs/adr/0002-dual-development-pathways.md) |
| 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.md) |
| Model the plan as a depth-ordered tree; add a per-turn editable Objective | [ADR-0004](/docs/adr/0004-hierarchical-plan-and-objective.md) |

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.md).
* **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.md).
* **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.md).
* **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.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.