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

# Extending HAI-Co²

> Build on HAI-Co²: add an agent tool, a whole tool domain, a typed artifact type, or an LLM provider. Each is a small, registry-driven change.

HAI-Co² is built around four clean extension seams. Each follows the same philosophy: **declare the
thing where the registry looks for it, and the framework wires the rest.** Every one is a one-to-three
file change.

| You want to add                   | Edits                  | Where                                                                 |
| --------------------------------- | ---------------------- | --------------------------------------------------------------------- |
| A **tool** in an existing domain  | 1 (zero wiring)        | a `@workspace_tool` method on a `*Tools` class                        |
| A **tool domain**                 | 2                      | a new `*Tools` class + register it in `manager.py`                    |
| A **typed artifact** (chart-like) | 1 backend + 2 frontend | a tool that returns an artifact + a React renderer + a registry entry |
| An **LLM provider**               | 3                      | a provider module + a `Settings` key + a dispatch arm                 |

This page is the consolidated how-to. For the underlying mechanics see [Agent core logic §5](/docs/agent-core-logic)
(tool system), [§7](/docs/agent-core-logic) (SSE), and [§10](/docs/agent-core-logic) (artifacts); the
[Tools](/docs/tools) cheat-sheet; and [`backend/app/tools/README.md`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/README.md).

## 1. Add a tool to an existing domain

A tool is an `async` method on an existing `*Tools` class decorated with `@workspace_tool`. The
decorator ([`backend/app/tools/_decorator.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/_decorator.py)) tags the method; `collect_tools` walks the class,
finds every tagged method, and wraps each into a LangChain `StructuredTool`. **Adding a method to an
already-registered class needs no wiring.**

The method contract is `async def fn(self, repo, **typed_kwargs) -> str` (or `-> (str, dict)`):
`self.thread_id` is bound to the conversation, and `repo` (a `WorkspaceRepository` on a fresh DB
session) is injected by the wrapper, which also pops `action_and_reasoning`, commits on success or
rolls back on error, and returns the JSON envelope. You only write the body.

```python theme={null}
# backend/app/tools/todos.py
from pydantic import BaseModel, Field
from ._decorator import workspace_tool
from ..db.repositories.workspace_repository import WorkspaceRepository

class DeleteTodoArgs(BaseModel):
    todo_id: int = Field(..., description="ID of the todo step to delete.")

class TodoTools:
    def __init__(self, thread_id: str) -> None:
        self.thread_id = thread_id

    @workspace_tool(
        name="delete_todo",
        description="Permanently remove one todo step by its ID.",
        args_schema=DeleteTodoArgs,
    )
    async def delete_todo(self, repo: WorkspaceRepository, todo_id: int) -> str:
        await repo.delete_todo(self.thread_id, todo_id)
        return f"Removed todo {todo_id}."
```

Conventions that matter:

* **Args schema** is a Pydantic model; each `Field` description is the LLM's only guidance, so make it
  instructive. Do **not** add `action_and_reasoning` yourself, the decorator appends it (and the
  frontend shows only that sentence in the *Internal Reasoning* trace). For a zero-input tool, omit
  `args_schema` entirely (see `list_preferences` in [`preferences.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/preferences.py)).
* **Return** a plain `str` (the LLM-facing summary) or a tuple `(content, artifact)` where `artifact`
  is `{"artifact_type": str, "payload": dict, "title"?: str}` (see §3).
* **Batch shape** is the house style for mutations: take a list of items so one call edits many (see
  `add_preferences` / `update_todos`).
* Mention the new tool in the `HAICO_SYSTEM_PROMPT` "Tool guide" in [`builder.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/services/agent/builder.py) so the agent knows when to use it.

## 2. Add a new tool domain

A domain is a `*Tools` class in its own file under [`backend/app/tools/`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools). Two edits:

<Steps>
  <Step title="Create the class">
    `backend/app/tools/citations.py` with `__init__(self, thread_id)` and one `@workspace_tool`
    method per operation (mirror `charts.py` / `preferences.py`).
  </Step>

  <Step title="Register one instance">
    In [`backend/app/tools/manager.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/manager.py), import it and append it to `_collections_for(thread_id)`:

    ```python theme={null}
    from .citations import CitationTools
    # ...
    return [
        DocumentTools(thread_id), ObjectiveTools(thread_id), TodoTools(thread_id),
        PreferenceTools(thread_id), ChartTools(thread_id), ArtifactTools(thread_id),
        CitationTools(thread_id),   # ← new domain
    ]
    ```
  </Step>
</Steps>

`build_tools(thread_id)` flattens `collect_tools` over every collection. That is the only wiring
change.

## 3. Add a typed artifact type (end to end)

A typed artifact is large, renderable data the agent produces but does not read back verbatim (charts
are the built-in example). The backend persists and streams it; the frontend dispatches on
`artifact_type` to a React renderer.

**Backend (no new wiring beyond the tool):** return the artifact tuple from a tool. The decorator
persists it via `repo.add_artifact(...)` tagged with the per-turn index, and the SSE layer
([`query.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/routers/query.py)) emits a dedicated `artifact` event for **any** `artifact_type`, which is what updates
the centre panel mid-stream.

```python theme={null}
# backend/app/tools/tables.py
@workspace_tool(name="data_table", description="Render structured rows as a sortable table.",
                args_schema=DataTableArgs)
async def data_table(self, repo, columns, rows, title) -> tuple[str, dict]:
    payload = {"columns": columns, "rows": rows, "title": title}
    return (f"Generated a {len(rows)}-row table.",
            {"artifact_type": "data_table", "title": title, "payload": payload})
```

**Frontend (the only required new code):**

<Steps>
  <Step title="Write a renderer">
    `frontend/src/components/app/artifacts/data-table-view.tsx`, props `{ artifact }` (the `Artifact`
    type is in [`frontend/src/lib/api.ts`](https://github.com/petrosrapto/HAICO/blob/main/frontend/src/lib/api.ts)). Narrow `artifact.payload` defensively (it is typed
    `unknown`); [`chart-views.tsx`](https://github.com/petrosrapto/HAICO/blob/main/frontend/src/components/app/artifacts/chart-views.tsx) shows the validate-and-fallback pattern so a bad payload
    never crashes the panel.
  </Step>

  <Step title="Register it (one line)">
    Add an entry to `REGISTRY` in [`registry.tsx`](https://github.com/petrosrapto/HAICO/blob/main/frontend/src/components/app/artifacts/registry.tsx): `data_table: DataTableView`. Unknown types fall
    back to an "Unknown artifact type" banner plus a JSON dump, so nothing breaks before you add the
    renderer.
  </Step>
</Steps>

Optional polish: an icon/label case in `artifact-panel.tsx`, shared payload types in `artifacts/types.ts`,
a renderer test in `artifacts/registry.test.tsx`, and a "Tool guide" line in the system prompt.
Artifact rows store `artifact_type` as a free-form string, so no migration or allow-list is needed for
a new type.

## 4. Add an LLM provider or model

The model factory is [`backend/app/services/llm/llm.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/services/llm/llm.py) `get_llm(config)`. It merges the
per-request config over `settings.default_llm`, then dispatches on the lowercased `API` field to a
provider module. Each provider module is a thin wrapper that returns a LangChain chat model (or
`None` if its key is missing, so the factory raises a provider-named error). This is the **least
documented** seam, so here it is in full.

### A new native provider

<Steps>
  <Step title="Provider module">
    `backend/app/services/llm/xyz.py`, mirroring [`anthropic.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/services/llm/anthropic.py) / `cohere.py`:

    ```python theme={null}
    def get_xyz_llm(model_id, api_key, **kwargs):
        if not api_key:
            return None
        return ChatXyz(model=model_id, api_key=api_key, **kwargs)
    ```
  </Step>

  <Step title="Settings key">
    Add the field to `Settings` in [`backend/app/core/config.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/core/config.py):

    ```python theme={null}
    xyz_api_key: str = Field(default="", alias="XYZ_API_KEY")
    ```
  </Step>

  <Step title="Dispatch arm">
    In `llm.py`, import it and add an arm, and update the "Supported:" error string:

    ```python theme={null}
    from .xyz import get_xyz_llm
    # ...
    elif api == "xyz":
        llm = get_xyz_llm(model_id=model_id, api_key=settings.xyz_api_key, **args)
    ```
  </Step>
</Steps>

### A new model on an existing provider

Usually **no code**: set `llm.provider` / `llm.model` in the environment's config YAML
(`deployment/{local,dev,prod}/backend/config.*.yaml`, read by `config.py`), or pass a per-request
model config. If the new model rejects a `temperature` parameter, add its id prefix to
`_REASONING_MODEL_PREFIXES` or `_ANTHROPIC_NO_TEMPERATURE_PREFIXES` in `llm.py` (the factory strips
`temperature` for those).

### An OpenAI-compatible endpoint

For DeepSeek / Together / xAI / vLLM and similar, set `API=openai` plus `endpoint_url` (injected as
`base_url`). To use a dedicated key instead of the OpenAI one, add `"<url>": "<settings_attr>"` to
`ENDPOINT_API_KEY_MAP` in `llm.py` and a matching `*_api_key` field in `config.py`.

<Note>
  Per-request overrides flow from the frontend through `build_agent(runtime_config)` into
  `get_llm(model_config)`, which accepts either a flat `{API, model_id, endpoint_url?, args}` object or
  a nested `{"model": {...}}` wrapper.
</Note>

## Where to read next

* [Agent core logic](/docs/agent-core-logic): §5 the tool system, §7 the SSE stream, §10 artifact types.
* [Tools](/docs/tools): the `@workspace_tool` conventions cheat-sheet.
* [`backend/app/tools/README.md`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/README.md): the most detailed tools how-to, including removing a tool.
* [Architecture](/docs/architecture): how these pieces connect at runtime.
