2. Руководство разработчика › 2.3 Скрипты › AI-инструменты

AI-инструмент — это модульный скрипт, который владелец магазина регистрирует для AI-помощника. При подключении агент узнаёт, сколько их у магазина, перечень берёт вызовом tool_list и запускает инструменты по имени — так магазин получает собственные «кнопки»: завести товар так, как заведено у вас, закрыть заказ по вашим правилам.

Перечень — данные, а не код. Новый инструмент появляется у агента без обновления программы: строки в реестре плюс модуль в units. Реестр владелец ведёт в программе: Проектирование → AI-компоненты, вкладка «AI-инструменты»; как заполнять окно — «AI-инструменты» в руководстве пользователя.

Реестр

Реестр — единственный источник подписи: агент узнаёт из него и что инструмент делает (описания), и как его звать (команды с полями). Модуль себя не описывает — в нём остались только тела команд.

Права выдаются людям: агент входит в магазин под логином того сотрудника, что сидит за клавиатурой — отдельного пользователя ИИ не существует, — и грант считается по нему: строка его собственная или строка его группы. Отсюда и то, что один и тот же инструмент у разных сотрудников умеет разное. Право — это команда: сколько команд, столько прав. Держатель права «Изменение настроек AI» (PUT_AGENT_OPTION) владеет всеми командами автоматически. Перечень открыт всем: агент видит все инструменты и все команды — выданные строкой may:, остальные строкой also:; пустой may: — он подсказывает сотруднику, у кого просить доступ.

Инструмент — это сущность, команда — её функция

Один инструмент покрывает одну сущность целиком — «Пользователи», «Задачи», — и это один модуль. Команда называет одно её дело, и её имя в реестре — это имя функции модуля: CmdList, CmdDone. Приставка Cmd отличает команду от функций-помощников, которые агенту не видны.

Право выдаётся на команду целиком, со всей её параметрической широтой, — поэтому команды делят по праву, а не по значениям параметра. В «Планировщике» CmdNoteAdd только комментирует, и его выдают всем, кто видит задачу, а двигают задачу CmdState, CmdPass, CmdDone и CmdClose — каждую можно выдать отдельно.

Контракт вызова

Модуль объявляет пространство имён своим именем заглавными, и движок зовёт функцию команды напрямую — ни точки входа, ни разводящего switch в модуле нет. Этим же именем агент называет инструмент: в перечне и в tool_run модуль melbis_agent_currency.php — это MELBIS_AGENT_CURRENCY, а рядом перечень показывает путь по дереву реестра, Business -> Currencies:

namespace MELBIS_AGENT_USER;

/**
 * Function CmdList
 * The users of the store
 **/
function CmdList($mUserId, $mParam)
{
    // ...
}

Функция возвращает массив, и он целиком уезжает агенту — как он устроен, описано ниже, в «Ответе команды».

Рутина — дело движка

До модуля движок проверяет всё, что записано в реестре, и модулю достаются только правила предметной области:

Тип Что принимает Что получает модуль
str любое одиночное значение строка
int целое число или его запись словом "12" int
float число float
bool true/false, yes/no, 1/0, on/off bool
date день как YYYY-MM-DD строка в форме базы
time час как HH:MM:SS, секунды можно опустить строка в форме базы
datetime день, и час после него; час можно опустить строка в форме базы
json список или объект; скаляр — отказ массив
jsonl поток строк: список объектов (см. ниже) массив строк
files вложения вызова, не поле (см. ниже) строки таблицы файлов в $mParam['files']

Дни и часы приводятся к той форме, в которой их держит база, и «31 февраля» отбивается, а не сползает на третье марта. Проверять их в модуле не нужно и не следует: база строгая, и слово, которого она не прочтёт, губит не поле, а весь ответ.

Поток строк. Команда, которая грузит пачку однородных строк — товары импорта, значения характеристик, цены прайса, — объявляет параметр типа jsonl. Помощник кладёт туда строки массивом — прямо в вызов или, когда их много, в файл полей вызова params_source, который пишет его скрипт. Модуль получает массив в обоих случаях и файлов не читает вовсе.

Разница не в удобстве, а в возможности: партия в пятьсот строк, набранная помощником в параметрах вызова, — это сотни тысяч знаков в его собственном сообщении, а файл, который написал его скрипт, не проходит через него совсем.

Список вместо одного значения. К любому типу, кроме json, jsonl и files, дописывается хвост /json — int/json, str/json, datetime/json. Такое поле принимает и одно значение, и список, а модулю всегда достаётся массив, так что развилки «один или несколько» в коде не появляется:

$list = implode(',', $mParam['store_id']);   // int/json: элементы уже взвешены

Каждый элемент взвешивается тем же типом, что и одиночное значение, — поэтому int/json и есть то, что делает безопасной подстановку списка в IN ( … ). Пустой список и список внутри списка — отказ.

Множественность — свойство строки реестра, а не поля вообще: у одной и той же price в читающей команде может стоять float/json (диапазон, поиск), а в пишущей — float, потому что записать в колонку список нельзя.

Файлы. Команда, принимающая вложения, объявляет один параметр типа files. В подписи он показывается не полем, а пометкой команды — файлы прикладываются к вызову, а не передаются в параметрах. Движок раскладывает их до модуля, и команда читает готовые строки из $mParam['files']; обязательный files без вложений — отказ, вложения в команду без files — отказ. Команда, принявшая файлы, называет их в ответе под ключом files — так движок знает, что у файлов есть хозяин; безхозные он убирает сам.

Внутри команды

Ответ команды

Функция команды возвращает массив. Движок отдаёт его агенту целиком, MCP-сервер раскладывает строки таблиц по файлам, и агент получает ответ одной формы — кто бы ни ответил: команда или движок, отказавший до неё.

Ключи

Ключей пять, и в ответе они есть всегда:

Ключ Что это
result true — сделано, false — отказ; третьего не бывает
message что произошло и что делать дальше: коротко и по-английски — его читает модель и пересказывает человеку своими словами
detail значения, которые вернула команда: id новой строки, ids нескольких, found — сколько нашлось, состояние объекта после действия
tables строки: именованные списки {"<имя>": [{…}, {…}]}
files вложения, которые команда оставила себе

result и message пишет команда. detail, tables и files, которых она не написала, движок дописывает пустыми: {}, {} и []. Всё, что команда возвращает сверх строк и вложений, кладут в detail.

Машинных слов-кодов в ответе нет: агенту хватает result и текста. Значения в detail называют словами предметной области — id, version_id, found, — а не data1.

Строки — только в tables. Всё, что может оказаться длиннее нескольких строк, команда кладёт в tables. MCP-сервер пишет каждую таблицу в файл на компьютере агента и оставляет в ответе число строк, путь к файлу и первые три строки. Список, положенный в detail, агент получит целиком, прямо в переписку: тысяча товаров — это сотни тысяч знаков в его памяти. Связанное отдают отдельными таблицами, строки ссылаются друг на друга по id, а соединяет их агент своим скриптом. Так отвечает на чтение библиотека melbis_inc_agent_table:

return [
    'result'  => true,
    'message' => 'The tables asked for',
    'tables'  => ['currency' => $rows]
    ];

Имя таблицы становится частью имени файла, поэтому это одно слово: currency, goods, errors.

Вложения. Команда, принимающая файлы, называет принятые под ключом files (см. «Рутина — дело движка»). Если она их не назвала — отказала или забыла, — движок убирает все файлы вызова и дописывает к message: The 2 file(s) sent with this call are gone: the command [CmdAdd] did not take them.

Отказ

Отказ — это result: false и message, который называет следующий шаг. Кто отказал, агенту не важно: движок до модуля отказывает в той же форме.

Кто Когда Пример message
движок незнакомая команда Unknown command [CmdNope]. The tool has: CmdList, CmdAdd, CmdUpdate, CmdRemove
движок команда не выдана The command [CmdRemove] of the tool [Business -> Currencies] is not granted to you… — и где её выдают
движок поле, которого команда не объявила The command [CmdAdd] has no field [nme] - … — и какие поля она принимает
движок нет обязательного поля The command [CmdAdd] needs [name] - … — и описание поля
движок тип не сошёлся The [id] takes a whole number, and [abc] is not one
команда правило предметной области Nothing was named to change
команда таблица занята The [currency] tables are busy
команда удаление без apply Deleting 1 row(s) of <таблица>, and with them 3 <таблица>. Say apply

Последняя строка — первый шаг сноса со второго вызова: агент показывает человеку, что уйдёт следом, и после согласия повторяет вызов с apply: true.

Падение

Отказ — это ответ. Падение — когда ответа нет: инструмента с таким адресом нет, модуль не найден, в нём нет функции команды, она прервалась исключением или ответила без result. Тогда движок отвечает текстом ошибки, и агент получает его отказом без JSON:

Команда, которая сама ловит ошибку и отвечает result: false со словами, лучше падения: из текста исключения агент не поймёт, что делать дальше.

Что видит агент

MCP-сервер отдаёт агенту ответ команды с теми же ключами. Строки каждой таблицы он пишет в файл и ставит на их место, где они лежат, а рядом добавляет time:

{"result": true, "message": "The tables asked for",
 "detail": {},
 "tables": {"currency": {"rows": 3,
                         "file": "D:\\Melbis\\shop.example.com\\mcp\\claude-code\\prices-sept\\tables\\MELBIS_AGENT_CURRENCY.CmdList.currency.jsonl",
                         "head": ["{\"id\":\"1\",\"code\":\"USD\"}", "…", "…"]}},
 "files": [],
 "time": {"server": 11, "module": 1, "trip": 46}}

Файл таблицы перезаписывается следующим вызовом той же команды. result: false приходит агенту с признаком ошибки и тем же JSON. Всё, что принимает и отдаёт вызов, — «tool_run», много вызовов сразу — «tool_pool».

Примеры

Добавление. Модуль возвращает:

return [
    'result'  => true,
    'message' => 'The row of currency is added',
    'detail'  => ['id' => 15]
    ];

Агент получает:

{"result": true, "message": "The row of currency is added",
 "detail": {"id": 15}, "tables": {}, "files": [],
 "time": {"server": 14, "module": 3, "trip": 52}}

Изменение, в котором ничего не названо — отказ самой команды:

{"result": false, "message": "Nothing was named to change",
 "detail": {}, "tables": {}, "files": [],
 "time": {"server": 9, "module": 1, "trip": 44}}

Удаление. Первый вызов, без apply:

{"result": false, "message": "Deleting 1 row(s) of <таблица>, and with them 3 <таблица>. Say apply",
 "detail": {}, "tables": {}, "files": []}

Второй вызов, с apply: true, после согласия человека:

{"result": true, "message": "1 row(s) of <таблица> gone…",
 "detail": {}, "tables": {}, "files": []}

Отказ движка — модуль не запускался, поэтому в time нет module:

{"result": false, "message": "Unknown command [CmdNope]. The tool has: CmdList, CmdAdd, CmdUpdate, CmdRemove",
 "detail": {}, "tables": {}, "files": [],
 "time": {"server": 6, "trip": 47}}

Непринятые вложения — команда отказала, и два отправленных файла убраны:

{"result": false,
 "message": "… The 2 file(s) sent with this call are gone: the command [CmdAdd] did not take them.",
 "detail": {}, "tables": {}, "files": []}

Описания делят работу

Агент собирает вызов из трёх слоёв текста, и у каждого своя глубина:

В перечне tool_list едет только начало описания инструмента — первое предложение — и слова его команд. Описания команд и параметры агент берёт по одному инструменту, когда собрался им работать.

Библиотеки

Общая часть инструментов — модули-библиотеки melbis_inc_agent_* (system, table, query, store, file). Юнит подключает библиотеку манифестом (includes: melbis_inc_agent_system.php=1) и объявляет алиас:

use MELBIS_INC_AGENT_SYSTEM as SYS;
// ...
$mine = SYS\RightOne('topic', $mUserId, 'descr', $topic_id);

Публикуемые алиасы библиотека называет в своём манифесте, в поле param_info — по ним движок сверяет use при сохранении.

Резервная копия и обновление

Проектирование → AI-компоненты → Экспорт (право TOOL_EXPORT; у агента — tool_export) собирает весь реестр и модули за ним в один архив: index.json с деревом и md5-суммой каждого файла, tools/<unit>.json на инструмент — команды с полями, — units/ с модулями, манифестами и библиотеками, которые нашлись по манифестам. Гранты в архив не входят: они про людей магазина, а не про инструменты.

Двери импорта нет, и это решение: после установки владелец дорабатывает инструменты под себя, и обновление — всегда сравнение, а не накат. Агент распаковывает контейнер, по суммам индекса находит разошедшееся и переносит точечно: реестр — прямыми запросами, модуль — сохранением файла.

Где лежит актуальный набор дистрибутива и как по нему обновлять инструменты магазина, агенту сказано в документации MCP-сервера — достаточно попросить.

Эталоны

Живые образцы в демонстрационном магазине платформы: