# 5. Инструменты — состояние, базы и визуализация

🔵 Хранение состояния (scoped store, SQL-база DuckDB) и генерация визуальных
блоков (графики/таблицы). Сводную таблицу всех движков см. в
[«Справочник инструментов»](05-tools-reference.md).

## 5.11 state_tool

Хранилище состояния со скоупом. `kind: state_tool` открывает агенту scoped state
store ядра. Каждый YAML привязывает **одну операцию** (`op`) к **одному скоупу +
ключу** — агенту даётся узкая, целенаправленная способность (например «только
читать user/name»).

Конкретный id скоупа (session/user/template) резолвится в рантайме из контекста
вызывающего инструмента — LLM никогда не видит чужой id.

| Поле | Обязат. | Описание |
|---|---|---|
| `kind` | да | `state_tool` |
| `name` | да | имя для LLM |
| `description` | да | описание для LLM |
| `op` | да | одна операция (см. таблицу ниже) |
| `scope` | да | `user` \| `session` \| `agent` |
| `key` | да | ключ хранения |
| `key_param` | нет | входной аргумент, перекрывающий `key` |
| `default` | нет | значение `get` при промахе |
| `ttl_secs` | нет | TTL для записей |
| `limit` / `offset` | нет | пагинация чтения коллекций |
| `filter` | нет | JSON-фильтр по коллекциям |
| `parameters` | нет | переопределить авто-схему |

### Скоупы

| Скоуп | Хранится под | Резолвится из |
|---|---|---|
| `user` | `user:<user_id>` | `X-User-Id` сессии |
| `session` | `session:<session_id>` | текущая сессия |
| `agent` | `agent:<template>` | имя шаблона агента |

### Виды значений

| Вид | Запись | Чтение |
|---|---|---|
| скаляр | `set` | `get` |
| список | `push` | `list`, `range` |
| множество | `sadd`, `srem` | `smembers`, `scard` |

`delete` удаляет любой ключ; `keys` перечисляет ключи скоупа.

### `op` — справочник

| op | Описание | Авто-параметры | Возвращает |
|---|---|---|---|
| `get` | прочитать скаляр (или `default`/`null`) | — | значение |
| `set` | upsert скаляра | `value` | `{"ok":true}` |
| `delete` | удалить ключ | — | `{"deleted":true\|false}` |
| `keys` | список ключей скоупа | `limit`, `offset` | `[{key,kind,size}]` |
| `push` | добавить в список | `value` | `{"seq":N}` |
| `list` | прочитать список | `limit`, `offset` | `["a", …]` |
| `range` | срез списка `[from..=to]` | `from`, `to` | `["a", …]` |
| `sadd` | добавить в множество | `value` | `{"added":true\|false}` |
| `smembers` | члены множества | `limit`, `offset` | `["x", …]` |
| `srem` | удалить из множества | `value` | `{"removed":true\|false}` |
| `scard` | размер множества | — | `{"count":N}` |

Чтение коллекций ограничено (по умолчанию 1000 элементов).

### `filter`

```yaml
filter:
  path: meta.priority
  op: gte               # eq | ne | contains | prefix | gt | gte | lt | lte | exists
  value: 3
```

Строковые операции регистронезависимы; `gt`/`gte`/`lt`/`lte` сравнивают численно.

---

## 5.17 duckdb_tool

Своя DuckDB-база для агента. Универсальный SQL-доступ к DuckDB: агент получает
собственную базу (или доступ к общей), а забота о соединениях и схеме остаётся в
движке. Подходит, когда key/value `state_tool` мало — нужны таблицы, JOIN,
агрегации, индексы.

```yaml
kind: duckdb_tool
name: memory
description: SQL-база фактов пользователя.
path: data/dbs/{user}.db     # файл, ":memory:" или шаблон пути
operation: query             # необязательно: query | execute | schema
init:                        # необязательно: выполняются ОДИН раз (идемпотентно)
  - |
    CREATE TABLE IF NOT EXISTS facts (
      id BIGINT PRIMARY KEY,
      topic TEXT NOT NULL,
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
readonly: false              # запретить execute
limit: 100                   # страница по умолчанию
max_rows: 1000               # жёсткий кап строк
```

| Поле | Обязат. | Описание |
|---|---|---|
| `kind` | да | `duckdb_tool` |
| `name` | да | имя для LLM |
| `description` | нет | описание для LLM |
| `path` | да | файл базы или `:memory:`. Поддерживает `{user}`, `{session}`, `{template}`/`{agent}` |
| `operation` | нет | привязать к одной операции; без него операция выбирается аргументом `operation` (по умолч. `query`) |
| `init` | нет | строка или список SQL; выполняется один раз при первом открытии базы |
| `readonly` | нет | `true` запрещает `execute` |
| `limit` / `max_rows` | нет | страница по умолчанию / кап строк |
| `parameters` | нет | переопределить авто-схему |

### Операции

| operation | Назначение | Параметры |
|---|---|---|
| `query` | выполнить SQL и вернуть `columns` + `rows` | `sql`, `limit` |
| `execute` | DDL/DML (`CREATE`, `INSERT`, `UPDATE`, `DELETE`, …) | `sql` |
| `schema` | таблицы/вью и их колонки | `table` (необязательно) |

`query` материализует до `limit` строк и выставляет `has_more`, когда их больше —
агент повторяет запрос со своим `LIMIT`/`OFFSET`. Результат `query` дополнительно
кладётся в `data` (для композиции через `visualize_tool` `source`) и в `visual`
(таблица для UI); `content` — JSON для LLM. Временны́е типы (`TIMESTAMP`, `DATE`,
`TIME`) возвращаются строками ISO 8601.

### Свои базы через user / session / agent

Плейсхолдеры в `path` резолвятся из контекста вызова:

```yaml
path: data/dbs/{user}.db       # одна база на пользователя
path: data/dbs/{template}.db   # одна база на шаблон агента
path: data/dbs/{session}.db    # одна база на сессию
```

Нерезолвимый плейсхолдер (например `{user}` без user id) — явная ошибка, а не
молчаливое падение в общий файл. Подстановка санитизируется (нельзя выйти из
каталога).

### Общая база для нескольких инструментов и инстансов

DuckDB допускает одного писателя на файл, поэтому движок держит
**процесс-глобальный пул соединений** по каноническому пути: несколько
определений `duckdb_tool` (и параллельные сессии агентов), указывающих на один
файл, делят одно мьютекс-защищённое соединение. `init` при этом выполняется
ровно один раз. Пример — `memory` + `memory_schema` на общей `{user}.db` в
`config/agents/db-demo/`.

---

## 5.22 sql_tool

SQL-доступ к внешней **PostgreSQL** или **MySQL** — сетевой собрат
[`duckdb_tool`](#517-duckdb_tool) с тем же интерфейсом `query`/`execute`/`schema`,
но для подключения к существующей учётной / CRM / ERP базе.

```yaml
kind: sql_tool
name: crm_db
description: Чтение из базы CRM.
dialect: postgres            # postgres | mysql
url_env: CRM_DATABASE_URL    # URL подключения из env (postgres://… / mysql://…)
operation: query             # необязательно: query | execute | schema
readonly: false              # запретить execute
limit: 100                   # страница по умолчанию
max_rows: 1000               # жёсткий кап строк
init:                        # необязательно: выполняется один раз при создании пула
  - SET NAMES utf8mb4;
```

| Поле | Обязат. | Описание |
|---|---|---|
| `kind` | да | `sql_tool` |
| `name` | да | имя для LLM |
| `description` | нет | описание для LLM |
| `dialect` | да | `postgres` \| `mysql` |
| `url_env` | да¹ | имя env с URL подключения (секреты — не в YAML) |
| `url` | нет | буквальный URL (только для dev; предпочти `url_env`) |
| `operation` | нет | привязать к одной операции; без него операция выбирается аргументом `operation` (по умолч. `query`) |
| `readonly` | нет | `true` запрещает `execute` |
| `limit` / `max_rows` | нет | страница по умолчанию / кап строк |
| `parameters` | нет | переопределить авто-схему |

¹ либо `url_env`, либо `url` — одно из двух обязательно.

### Операции

| operation | Назначение | Параметры |
|---|---|---|
| `query` | выполнить SQL и вернуть `columns` + `rows` | `sql`, `limit` |
| `execute` | DDL/DML (`CREATE`, `INSERT`, `UPDATE`, `DELETE`, …) | `sql` |
| `schema` | таблицы/вью и их колонки | `table` (необязательно) |

`query` материализует до `limit` строк и выставляет `has_more`; результат кладётся
в `data` (композиция через `visualize_tool` `source`) и `visual` (таблица).
Соединения делятся через **процесс-глобальный пул** по URL: несколько определений
`sql_tool` на одну базу используют один пул, `init` выполняется один раз.
Временны́е типы, `uuid` и пр. возвращаются строками.

---

## 5.12 chart_tool и table_tool

Статичные визуальные данные. Движки для предвычисленных/статичных данных:
компактный `content` (сводка для LLM) + `visual` (полный блок для UI).

```yaml
kind: table_tool
name: price_list
description: Показать прайс.
parameters: { type: object, properties: {} }
data_file: data/prices.json
title: "Цены"
summary: "{{ count }} позиций"
columns:
  - { key: model, title: "Модель" }
  - { key: price, title: "Цена", align: right }
options: { sortable: true, searchable: true }
```

```yaml
kind: chart_tool
name: sales_chart
description: Продажи по кварталам.
parameters: { type: object, properties: {} }
data_file: data/sales.json       # { type, title, labels, series }
summary: "{{ count }} точек"
```

Общие поля: `kind`, `name`, `description`, `parameters`, `data_file` (путь
относительно YAML) или inline `data`, `summary` (MiniJinja, контекст `count`),
`ui`. `table_tool` дополнительно: `title`, `columns`, `options` (`sortable` /
`searchable` / `paged`). `chart_tool` берёт `data` как spec (`type` = `bar` |
`line` | `area` | `pie` | `scatter`).

Оба движка принимают динамический аргумент **`data`** в вызове — он перекрывает
статичный `data`/`data_file`.

---

## 5.13 visualize_tool

Графики и таблицы из произвольных данных. Универсальный собрат
`chart_tool`/`table_tool`: превращает **данные, переданные аргументами**, в
график или таблицу, с пайплайном трансформаций. LLM отдаёт строки (или spec),
движок детерминированно агрегирует.

```yaml
kind: visualize_tool
name: make_chart
description: Построить график/таблицу из переданных строк.
```

| Аргумент | Описание |
|---|---|
| `data` | массив объектов-строк, **или** полный spec таблицы/графика |
| `source` | сослаться на результат прошлого вызова инструмента: `{ tool, index, field, path, pages }` |
| `group_by` | поле группировки |
| `aggregate` | `{ count, sum, avg, min, max }` — результаты с префиксом (`sum_cost`, …) |
| `filter` | `{ field: { op: value } }` |
| `sort` | `{ by, dir: asc\|desc }` |
| `limit` | максимум строк |
| `as` | `table` \| `bar` \| `line` \| `area` \| `pie` \| `scatter` |
| `title`, `x`, `series`, `columns`, `options` | оформление |

### Композиция инструментов (`source`)

Вместо копирования данных можно сослаться на результат прошлого вызова в той же
сессии:

```jsonc
{ "source": { "tool": "trace_sessions" }, "group_by": "agent",
  "aggregate": { "sum": ["errors"] }, "as": "bar" }
```

[`trace_tool`](05-tools-observe.md#59-trace_tool)'s `sessions` возвращает
структурированные строки в `data` — готовый источник. Явный `data` выигрывает у
`source`.
