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

# Preference tools

> Agent tools that record, read, edit, and remove the decoded hard and soft constraint set for a thread.

The `PreferenceTools` collection manages the decoded hard and soft constraint set for a conversation (the top-left panel in the UI). One instance is created per thread, bound to its `thread_id`, so every tool reads and writes the preferences for that one conversation without `thread_id` being an LLM-facing parameter.

Each preference carries a `locked` flag. The agent path RESPECTS this flag: `update_preferences` and `remove_preferences` read the preferences first, and any locked target is skipped and reported (not mutated), with the repository enforcing this via `respect_lock=True`. The user path (REST endpoints) ignores the lock and can always edit, and it is also where preferences are locked and unlocked.

The mutating tools are batch-shaped: `add_preferences` and `update_preferences` take lists, and `remove_preferences` takes a list of indices, so the agent can act on many preferences in one call (pass a one-element list to act on a single preference). `list_preferences` is read-only.

Shared conventions (the `@workspace_tool` contract, the JSON result envelope, and the injected `action_and_reasoning` argument) are documented in [Tools](/docs/tools). Every tool below also receives the injected `action_and_reasoning` argument described there; it is not listed as an argument row.

Source: [`backend/app/tools/preferences.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/preferences.py)

## `add_preferences`

Record one or more decoded preferences (hard or soft constraints) extracted from the user's message.

**Description shown to the agent:**

> Record one or more decoded user preferences (hard or soft constraints). Use this for every add — pass a one-element list to record a single preference.

**Arguments**

| Argument | Type                                                 | Required | Description shown to the agent                                                                         |
| -------- | ---------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------ |
| `items`  | `array` of `{title, subtitle?, kind:"hard"\|"soft"}` | Yes      | The explicitly-stated preferences to record, in order. Pass a single preference as a one-element list. |

Each item in `items` is a preference object:

| Field      | Type             | Required                  | Description shown to the agent                    |
| ---------- | ---------------- | ------------------------- | ------------------------------------------------- |
| `title`    | `string`         | Yes                       | Short label, e.g. 'Academic tone'.                |
| `subtitle` | `string`         | No                        | Free-text qualifier, e.g. 'Formal and objective'. |
| `kind`     | `"hard"\|"soft"` | No (defaults to `"soft"`) | 'hard' for must-haves, 'soft' for preferences.    |

**Returns:** a confirmation string with the number of preferences recorded, e.g. `recorded 2 preferences`.

**Implementation:** records the batch via `repo.add_preferences_bulk`. New preferences are unlocked.

Source: [`backend/app/tools/preferences.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/preferences.py)

## `list_preferences`

Read the current preference / constraint list for this thread.

**Description shown to the agent:**

> List the current preferences / constraints.

**Arguments**

None (the agent still passes the injected `action_and_reasoning`).

**Returns:** newline-separated lines of the form `#N [kind] title — subtitle`, each appended with `(locked)` when the preference is locked, or a placeholder string when no preferences have been recorded yet.

**Implementation:** has no `args_schema` (the decorator defaults to the empty schema). Reads via `repo.list_preferences`. The `(locked)` suffix is also added to locked rows in the auto-injected `<preferences>` workspace block.

Source: [`backend/app/tools/preferences.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/preferences.py)

## `update_preferences`

Edit the `title`, `subtitle`, or `kind` of one or more existing preferences, addressed by their 1-based indices.

**Description shown to the agent:**

> Edit the title, subtitle, or kind of one or more existing preferences by their 1-based indices. Pass a list of `{index, title?, subtitle?, kind?}`. Locked preferences are skipped and reported so the agent can tell the user.

**Arguments**

| Argument  | Type                                           | Required | Description shown to the agent                                                                                                                          |
| --------- | ---------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `updates` | `array` of `{index, title?, subtitle?, kind?}` | Yes      | The preferences to edit, each as `{index, title?, subtitle?, kind?}`; omit a field to keep its current value. Pass a single edit as a one-element list. |

Each item in `updates` is an edit object:

| Field      | Type                        | Required | Description shown to the agent                                                                |
| ---------- | --------------------------- | -------- | --------------------------------------------------------------------------------------------- |
| `index`    | `integer` (1-based, `>= 1`) | Yes      | 1-based position of the preference within this conversation (as shown by `list_preferences`). |
| `title`    | `string`                    | No       | New short label; omit to keep the current value.                                              |
| `subtitle` | `string`                    | No       | New qualifier; omit to keep the current value.                                                |
| `kind`     | `"hard"\|"soft"`            | No       | 'hard' or 'soft'; omit to keep the current value.                                             |

**Returns:** a summary naming the updated preferences, plus any that were locked (skipped and reported, "ask the user to unlock first") or out of range, or `no matching preferences found` when nothing applied.

**Implementation:** resolves indices against the list as it stands at call time. Locked targets are skipped before mutating; the repository call uses `respect_lock=True`, so locked items are never modified by the agent path.

Source: [`backend/app/tools/preferences.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/preferences.py)

## `remove_preferences`

Delete one or more preferences, addressed by their 1-based indices.

**Description shown to the agent:**

> Remove one or more preferences by their 1-based indices (as shown by `list_preferences`; pass a list). Locked preferences are skipped and reported. Call `list_preferences` first to confirm the indices, and remove only when the user explicitly asks you to drop a preference.

**Arguments**

| Argument  | Type                           | Required | Description shown to the agent                                                                        |
| --------- | ------------------------------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `indices` | `array` of `integer` (1-based) | Yes      | 1-based positions of the preferences to remove (as shown by `list_preferences`). Pass one or several. |

**Returns:** a summary naming the removed preferences, plus any that were locked (skipped and reported, "ask the user to unlock first") or out of range, or `no matching preferences found` when nothing applied.

**Implementation:** resolves all targets up front so indices reference the current list. Deletion uses `respect_lock=True`, so locked items are skipped and reported rather than removed by the agent path.

Source: [`backend/app/tools/preferences.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/tools/preferences.py)
