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

# Tools

> The @workspace_tool conventions, how the per-thread tool set is composed, and links to the detailed per-domain reference.

Tools live under [`backend/app/tools/`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools). A tool is **not** a plain `@tool`-decorated function. It is a coroutine **method** on a `*Tools` class, decorated with `@workspace_tool(name, description, args_schema)`. The [`@workspace_tool`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/_decorator.py) decorator wraps the method, and [`collect_tools`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/_decorator.py) discovers every decorated method on a collection instance and turns it into a LangChain `StructuredTool`. [`build_tools(thread_id)`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/manager.py) instantiates one collection per domain and flattens them into the list passed to `create_react_agent`. See [`backend/app/tools/README.md`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/README.md) for the canonical reference.

Each `*Tools` instance is bound to a single `thread_id`, so every tool reads/writes the workspace (objective, document, todos, preferences, artifacts) for that one conversation without `thread_id` being an LLM-facing parameter.

## Per-domain reference

This page is the shared cheat-sheet. Each tool domain also has a dedicated reference page that documents every tool in full: its arguments, return values, the **description shown to the agent** (the LLM-facing prompt text), and the implementation.

<CardGroup cols={2}>
  <Card title="Document" icon="file-pen" href="/docs/tools/document">
    Write the shared working document: `update_document`, `append_to_document`.
  </Card>

  <Card title="Objective" icon="bullseye" href="/docs/tools/objective">
    Record the agent's one-line understanding of the goal: `set_objective`.
  </Card>

  <Card title="Plan" icon="list-check" href="/docs/tools/plan">
    The execution ledger: `add_todos`, `toggle_todos`, `update_todos`, `remove_todos`, `list_todos`.
  </Card>

  <Card title="Preferences" icon="sliders" href="/docs/tools/preferences">
    The hard and soft constraint set: `add_preferences`, `update_preferences`, `remove_preferences`, `list_preferences`.
  </Card>

  <Card title="Charts" icon="chart-line" href="/docs/tools/charts">
    A typed line, bar, or pie chart artifact: `chart_generator`.
  </Card>

  <Card title="Past artifacts" icon="clock-rotate-left" href="/docs/tools/artifacts">
    Reference earlier artifacts by id: `list_artifacts`, `get_artifact`.
  </Card>
</CardGroup>

## Built-in tools

There are **15** LLM-facing tools, composed from six collections in [`_collections_for`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/manager.py): `DocumentTools`, `ObjectiveTools`, `TodoTools`, `PreferenceTools`, `ChartTools`, `ArtifactTools`. Every mutating workspace tool is batch-shaped: it takes a list so the agent can act on many items at once; to act on a single item, pass a one-element list. There are no singular variants.

| Tool                 | Domain      | Effect                                                                                                                                                   |
| -------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `update_document`    | documents   | Replace the current document body (optional `title`)                                                                                                     |
| `append_to_document` | documents   | Append `text` to the document                                                                                                                            |
| `set_objective`      | objective   | Set / update the agent's one-line understanding of the user's goal                                                                                       |
| `add_todos`          | todos       | Add a whole outline at once (`steps`, each with a `depth`); pass `parent_index` to nest the outline under an existing step, omit it to append at the end |
| `toggle_todos`       | todos       | Mark one or more todo steps done / not-done by 1-based `indices`                                                                                         |
| `update_todos`       | todos       | Rewrite one or more todo steps' text (list of `{index, text}` `updates`)                                                                                 |
| `remove_todos`       | todos       | Delete one or more todo steps (list of `indices`); a parent removes its sub-steps                                                                        |
| `list_todos`         | todos       | List the numbered plan (rare; auto-injected into the prompt)                                                                                             |
| `add_preferences`    | preferences | Record one or more explicit preferences (`items`, each `kind` `"soft"` default / `"hard"`)                                                               |
| `update_preferences` | preferences | Edit `title`/`subtitle`/`kind` of one or more preferences by 1-based index (list of `updates`); locked ones are skipped and reported                     |
| `remove_preferences` | preferences | Delete one or more preferences by their 1-based `indices`; locked ones are skipped and reported                                                          |
| `list_preferences`   | preferences | List preferences with 1-based indices (rare; auto-injected)                                                                                              |
| `chart_generator`    | charts      | Produce a `line` / `bar` / `pie` chart artifact (typed payload + style)                                                                                  |
| `list_artifacts`     | artifacts   | Compact index of past artifacts, newest first (optional type / `limit`)                                                                                  |
| `get_artifact`       | artifacts   | Fetch one artifact (full payload + metadata) by `artifact_id` (this thread only)                                                                         |

Every tool additionally gets a **required injected** `action_and_reasoning: str` argument (added by the decorator via `_extend_with_common_arg`). The agent must populate it with one short, non-technical sentence; it is logged and surfaced in the frontend's "Internal reasoning" trace, but is **never** forwarded to the tool body.

Tool results are returned as a JSON **envelope** string, not free-form prose:

* Success: `{"success": true, "summary": <content>, "artifact": <dict>?, "artifact_id": <int>?}`
* Failure: `{"success": false, "error": <str>}`

A tool body returns either a plain `str` (content only) or a tuple `(content_str, artifact_dict)` where `artifact_dict = {"artifact_type": str, "payload": dict, "title"?: str}`. `chart_generator` is the artifact-producing tool: it returns a tuple with `artifact_type = f"{chart_type}_chart"` and a payload of `{title, description, data, x_axis_title, y_axis_title, style}`. When an artifact dict is present, the decorator persists it via `repo.add_artifact(...)` tagged with the current `turn_index` and surfaces the new row id as `artifact_id`.

## Preference locking

Each preference carries a `locked` flag (migration `0005_preference_lock`, default `false`).

* The **agent path respects the lock**. `update_preferences` and `remove_preferences` read the preferences first; if a target is locked they do **not** mutate it and instead skip and report it: that the preference is locked and cannot be **modified** (`update_preferences`) or **removed** (`remove_preferences`), and that the user must unlock it first, with no exception raised. The repository enforces this with `respect_lock=True`.
* The **user path ignores the lock**. The REST endpoints in [`workspace.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/routers/workspace.py) call the repository with `respect_lock=False`, and `PATCH /preferences/{id}` accepts a `locked` field to lock/unlock. Locked means "the agent cannot modify or remove it; only the user can".
* Both the auto-injected `<preferences>` workspace block and the `list_preferences` tool append `(locked)` to locked rows.

## Adding a tool

There is **no** `@tool`, no `app/tools/__init__.py` registration step, and no `ToolManager.discover()` glob. The recipe (see [`backend/app/tools/README.md`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/README.md)):

### Extend an existing domain

Add a `@workspace_tool` method to the relevant `*Tools` class. The body must follow the signature contract `async def fn(self, repo: WorkspaceRepository, **typed_kwargs)`:

```python theme={null}
class DeleteTodoArgs(BaseModel):
    todo_id: int = Field(..., description="ID of the todo to delete.")

class TodoTools:
    ...
    @workspace_tool(
        name="delete_todo",
        description="Permanently remove a todo by ID.",
        args_schema=DeleteTodoArgs,
    )
    async def delete_todo(self, repo: WorkspaceRepository, todo_id: int) -> str:
        deleted = await repo.delete_todo(todo_id)
        return f"todo {todo_id} deleted" if deleted else f"todo {todo_id} not found"
```

`collect_tools` picks it up automatically; no other file changes.

### Add a new domain

1. Create a new file (e.g. `tools/citations.py`) with a `*Tools` class that takes `thread_id` in `__init__` and exposes `@workspace_tool` methods.
2. Append an instance to `_collections_for` in [`manager.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/manager.py):

   ```python theme={null}
   from .citations import CitationTools

   def _collections_for(thread_id: str) -> list:
       return [
           DocumentTools(thread_id),
           ObjectiveTools(thread_id),
           TodoTools(thread_id),
           PreferenceTools(thread_id),
           ChartTools(thread_id),
           ArtifactTools(thread_id),
           CitationTools(thread_id),   # ← add here
       ]
   ```

No other file needs to change.

## Conventions

* All mutations go through the `WorkspaceRepository` / DB, so the frontend re-syncs the affected panel.
* The decorator opens its **own** `AsyncSession` (`async_session_factory`) per call, injects the repo, and **commits on success / rolls back on any exception**, so tool bodies stay free of session management.
* Use typed parameters (not bare `**kwargs`) so the LLM-facing args schema is unambiguous. For tools with no inputs, omit `args_schema` (the decorator defaults to the empty `_NoArgs` schema).
* Artifacts are **persisted** to the `artifacts` table and **streamed** to the client via dedicated SSE `artifact` events (separate from `step` events); the agent never re-emits the payload verbatim.
