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

# Artifacts

> What an artifact is in HAI-Co², the typed-artifact pattern, and the full produce, persist, stream, render lifecycle of the centre panel's working object.

HAI-Co² is **artifact-centric**: the thing the human and the agent build together is an artifact in the centre panel, and most of the system exists to produce, version, and render it. The panel shows one of two kinds of artifact at a time:

* the **live document** (the default `X̂` working object): free-form text the agent writes with `update_document` / `append_to_document` and the user edits directly. It lives in the `documents` table, one row per thread.
* a **typed artifact**: structured, renderable data a tool emits (a chart today, a table or diagram tomorrow). Each one is an immutable row in the `artifacts` table, tagged with the turn that produced it, and rendered by a dedicated frontend component.

When a tool produces a typed artifact mid-turn, the centre panel auto-switches to render it; selecting the document again returns to the live working object. This page explains what typed artifacts are, why they are designed this way, and how one flows from the agent to the screen.

## The typed-artifact pattern

A tool never emits markup, SVG, or React. It emits a **typed JSON payload** plus a short `artifact_type` discriminator, and the frontend owns the rendering. The pattern is described in [`charts.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/charts.py) and buys three things:

* **Safety.** The agent cannot inject HTML, scripts, or styles, so there is no XSS or sandboxing surface. The renderer is trusted code in the repo, not model output.
* **A small agent surface.** The agent learns a compact data schema (for a chart: `chart_type`, `data`, a few labels), not a rendering API. That keeps tool descriptions short and the model reliable.
* **Large data that bypasses the context window.** An artifact payload can be large. It is persisted and streamed to the browser, but it is **not** injected back into the agent's context. The agent refers to past artifacts by id (see [What the agent sees](#what-the-agent-sees)), so a 500-row dataset never costs context tokens.

## Anatomy of an artifact

A tool body signals an artifact by returning a `(content, artifact)` tuple instead of a plain string. The `content` is the short confirmation the agent reads; the `artifact` dict is everything the frontend needs:

```json theme={null}
{
  "artifact_type": "line_chart",
  "title": "Q1 vs Q2",
  "payload": { "title": "Q1 vs Q2", "data": [ { "name": "Q1", "value": 30 } ], "x_axis_title": "Quarter" }
}
```

Once persisted, it becomes a row in the `artifacts` table:

| Column          | Type                   | Role                                                                 |
| --------------- | ---------------------- | -------------------------------------------------------------------- |
| `id`            | BigInteger PK          | Stable id the agent and UI reference.                                |
| `thread_id`     | VARCHAR, indexed       | Scopes the artifact to one conversation.                             |
| `turn_index`    | Integer                | The user-turn that produced it (HAICO's coarse clock).               |
| `artifact_type` | VARCHAR(64)            | The renderer discriminator (`line_chart`, `bar_chart`, …).           |
| `title`         | VARCHAR(255), nullable | Optional label shown in the panel's switcher.                        |
| `payload`       | JSONB                  | The opaque, renderer-specific data; the backend never interprets it. |
| `created_at`    | DateTime               | Server timestamp.                                                    |

A composite index `ix_artifacts_thread_turn` on `(thread_id, turn_index)` keeps per-turn lookups cheap. The `payload` is deliberately schemaless at the database level: a new artifact type needs no migration, only a tool that fills it and a renderer that reads it. The full schema and its place in the data model are in [Database schema](/docs/database-schema).

## Lifecycle: produce, persist, stream, render

<Tabs>
  <Tab title="ASCII">
    ```ascii theme={null}
     A tool returns (content, artifact)
       artifact = { artifact_type, payload, title? }
                          │
                          ▼
     @workspace_tool decorator
       • splits content (for the agent) from artifact
       • repo.add_artifact(thread_id, turn_index, artifact_type, payload, title)
       • wraps the JSON envelope { success, summary, artifact, artifact_id }
                          │
                          ▼
     artifacts table  (Postgres; payload = JSONB; tagged with turn_index)
                          │
                          ▼
     SSE "artifact" event  { artifact_id, artifact_type, title, payload }
       • emitted mid-turn, for ANY artifact_type, deduplicated by id
                          │
                          ▼
     Frontend registry:  artifact_type ─▶ React renderer
       • the centre panel auto-switches to the new artifact
       • an unknown type falls back to a banner + a JSON dump
    ```
  </Tab>

  <Tab title="Mermaid">
    ```mermaid theme={null}
    flowchart TD
        T["A tool returns (content, artifact)<br/>artifact = artifact_type · payload · title?"] --> D["@workspace_tool decorator<br/>split · persist · wrap envelope"]
        D -->|"repo.add_artifact(turn_index, type, payload)"| DB[("artifacts table<br/>Postgres JSONB · tagged with turn_index")]
        D -->|"envelope: success · summary · artifact · artifact_id"| S["SSE artifact event<br/>artifact_id · type · title · payload"]
        DB -.-> S
        S -->|"artifact_type"| R["Frontend registry → React renderer<br/>centre panel auto-switches"]
    ```
  </Tab>
</Tabs>

<Steps>
  <Step title="Produce (a tool)">
    A tool returns `(content, artifact)`. The chart tool, for example, validates and normalises its data, then returns the confirmation string plus `{artifact_type: "line_chart", title, payload}`. See the [Chart tool](/docs/tools/charts).
  </Step>

  <Step title="Persist (the decorator)">
    The [`@workspace_tool`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/_decorator.py) decorator splits the tuple, and if an artifact is present it calls `repo.add_artifact(...)` tagging the row with the current `turn_index` (read from a per-turn contextvar set just before the agent runs). The new row id is surfaced in the success envelope as `artifact_id`, so the agent can reference it later.
  </Step>

  <Step title="Stream (the SSE layer)">
    The query router ([`query.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/routers/query.py)) parses each tool message, and when one carries an artifact it emits a dedicated SSE `artifact` event, separate from the `step` and `complete` events. It fires for **any** `artifact_type` and is deduplicated by `artifact_id`, so the panel updates live, mid-turn, before the agent's final reply.
  </Step>

  <Step title="Render (the frontend registry)">
    The browser dispatches on `artifact_type` through the `REGISTRY` map in [`registry.tsx`](https://github.com/petrosrapto/HAICO/blob/main/frontend/src/components/app/artifacts/registry.tsx) to a React renderer (the chart views use recharts). Renderers validate the payload defensively, and an unregistered type falls back to a banner plus a pretty-printed JSON dump, so a bad or unknown payload never crashes the panel.
  </Step>
</Steps>

The tool result the agent reads back is a JSON envelope, never the payload:

```json theme={null}
{ "success": true, "summary": "Created a line chart titled 'Q1 vs Q2' with 4 data points.", "artifact": { "artifact_type": "line_chart", "title": "Q1 vs Q2", "payload": { } }, "artifact_id": 42 }
```

On the browser side the artifact is typed as:

```ts theme={null}
interface Artifact {
  id: number;
  thread_id: string;
  turn_index: number;
  artifact_type: string;
  title: string | null;
  payload: Record<string, unknown>;
  created_at: string;
}
```

## What the agent sees

To keep payloads out of the context window, the agent is given a **pointer, not the data**. Each turn, the workspace block injects only the latest artifact's id, type, turn, and title:

```
<latest_artifact id="42" type="line_chart" turn_index="3" title="Q1 vs Q2"/>
```

When the agent actually needs the data (to compare against, summarise, or revise it) it calls the read-back tools:

* `list_artifacts` returns a compact, newest-first index of past artifacts for the thread.
* `get_artifact` returns one artifact's full payload plus metadata, scoped to the current conversation.

Both are documented in [Past-artifact tools](/docs/tools/artifacts), and the injection mechanics are in [Agent core logic §10](/docs/agent-core-logic).

## Built-in artifact types

| `artifact_type` | Produced by                                 | Renderer                 | Use                                           |
| --------------- | ------------------------------------------- | ------------------------ | --------------------------------------------- |
| `line_chart`    | `chart_generator`                           | recharts `LineChartView` | Trends and ordered sequences.                 |
| `bar_chart`     | `chart_generator`                           | recharts `BarChartView`  | Category comparisons and counts.              |
| `pie_chart`     | `chart_generator`                           | recharts `PieChartView`  | Parts of a whole (5 to 7 categories).         |
| `document`      | (renderer registered; no tool emits it yet) | `ReadOnlyDocumentView`   | Read-only view of a document-shaped artifact. |

The three chart types share one tool, [`chart_generator`](/docs/tools/charts); the type string is built as `f"{chart_type}_chart"`. The `document` renderer is registered so a document-shaped artifact would display, but no current tool produces one (the live working document lives in the `documents` table, not the `artifacts` table). Because `artifact_type` is a free-form string with no database allow-list, adding a type is purely additive.

## Artifacts across branches and history

The `turn_index` tag is what lets artifacts participate in HAI-Co²'s versioning. When a conversation is [branched](/docs/conversation-branching) at turn `k`, every artifact with `turn_index <= k` is **copied** onto the new thread (re-inserted with fresh ids, in ascending original-id order so latest-artifact lookups stay correct). Workspace **snapshots** are deliberately *not* copied (each one embeds a full document body, so duplicating them would be quadratic in storage); the pre-branch history is reconstructed lazily instead. The trade-off and the copy rules are detailed in [Database schema](/docs/database-schema).

## Add a new artifact type

End to end it is three small edits: a tool that returns the artifact tuple (no extra backend wiring, the decorator and SSE layer handle persistence and streaming for any type), a React renderer, and one line registering it in `REGISTRY`. The full recipe with code is in [Extending HAI-Co² §3](/docs/extending).

## Where to read next

* [Agent core logic §10](/docs/agent-core-logic): artifact types and the injection/SSE mechanics in context.
* [Chart tool](/docs/tools/charts) and [Past-artifact tools](/docs/tools/artifacts): the producing and read-back tools in full.
* [Database schema](/docs/database-schema): the `artifacts` table, snapshots, and what branching copies.
* [Extending HAI-Co² §3](/docs/extending): add your own typed artifact type.
