7. Скрипты (JavaScript и Rhai)
🔵 Инструменты и хуки можно писать как скрипты, исполняемые встроенным движком и подгружаемые из YAML — без пересборки. Два движка, выбор по расширению файла:
- JavaScript (
.js,.mjs,.cjs) — настоящий JS через QuickJS. Рекомендуемый выбор: полный язык и стандартная библиотека (JSON,Math,String, regex, template-литералы,Array, …), знакомый синтаксис. - Rhai (
.rhai) — компактный встраиваемый язык для совсем маленьких скриптов. См. §7.5.
Оба движка дают одинаковый контракт и одинаковые функции-помощники (см. §7.3).
Путь script: на .js запускается на QuickJS; любое другое расширение — на Rhai.
- Скриптовый инструмент (
kind: script_tool) — LLM вызывает его как обычный инструмент; скрипт определяетrun(args). - Скриптовый хук (
kind: script_hook) — перехватывает жизненный цикл агента.
Готовые примеры в config/scripts/: text_stats (Rhai и JS), currency
(Rhai), wordcount (JS), audit и moderation (Rhai).
7.1 Скриптовый инструмент
config/scripts/tools/text_stats.yaml:
kind: script_tool
name: text_stats
description: "Считать символы, слова, предложения."
script: text_stats.js # относительно этого YAML
parameters:
type: object
properties:
text: { type: string, description: "Текст для анализа" }
required: [text]
ui:
icon: 📝
warnings:
- failed: true
message: "Не удалось проанализировать текст"
text_stats.js:
function run(args) {
const text = (args.text ?? "").trim();
if (text.length === 0) {
return { content: "текст пуст", success: false, error: "empty text" };
}
const words = text.split(" ").filter((w) => w.length > 0).length;
return { content: `Слов: ${words}`, success: true };
}
Поля YAML
| Поле | Обязат. | Описание |
|---|---|---|
kind |
да | script_tool |
name |
нет | имя инструмента (по умолчанию — имя файла) |
description |
нет | показывается LLM |
parameters |
нет | JSON Schema аргументов |
script |
да | путь к .js/.rhai, относительно YAML |
ui |
нет | иконка + call/result-шаблоны |
max_operations |
нет | лимит операций (по умолчанию 10 000 000; 0 = без лимита) |
warnings |
нет | декларативные правила предупреждений |
Контракт run(args [, config])
function run(args, config) {
// args — объект JSON-аргументов вызова
// config — сырой YAML-конфиг инструмента (все поля, включая свои)
return { content: "текст для LLM", success: true };
// при ошибке:
// return { content: "причина", success: false, error: "короткая причина" };
}
-
Второй аргумент
configопционален: если функция объявляет толькоrun(args), он не передаётся (обратная совместимость). Если объявленrun(args, config), в него попадает весь YAML-конфиг инструмента — так настройки можно задавать прямо в YAML (напр.repos: [...]) и читать в скрипте, не заводя внешний файл. -
content— строка, попадающая в контекст агента. -
success— опционально (по умолчаниюtrue). -
error— опционально; если задан, аsuccessнет — результат неуспешен,contentберётся изerror. -
visual— опционально; визуальный блок или список блоков (text/chart/table), показывается пользователю, но никогда не попадает в контекст LLM. Скрипт может держатьcontentкомпактной сводкой, а полные данные отдавать сюда:function run(args) { const page = args.page ?? 1; return { content: `страница ${page}`, success: true, visual: { type: "table", columns: [ { key: "id", title: "ID" } ], rows: [ { id: 1 }, { id: 2 } ], continuation: page < 3 ? { args: { page: page + 1 } } : undefined, }, }; }continuation.argsблокаtableвключает ленивую пагинацию (см. общие поля иPOST /v1/continuations). -
Возврат простой строки разрешён →
contentсsuccess=true. -
Выброшенное исключение →
success=falseс текстом ошибки.
7.2 Скриптовый хук
config/scripts/hooks/audit.yaml:
kind: script_hook
name: audit
script: audit.js
events: [before_user_message, after_turn] # опущено = все определённые функции
audit.js:
function before_user_message(ctx, msg) {
return { notice: `🔍 audit: ${msg.content.length} символов` };
}
function after_turn(ctx, question, answer) {
return { notice: `✅ audit: ${question.content.length} → ${answer.content.length} символов` };
}
Поля YAML
| Поле | Обязат. | Описание |
|---|---|---|
kind |
да | script_hook |
name |
нет | имя хука (по умолчанию — имя файла) |
script |
да | путь к .js/.rhai, относительно YAML |
events |
нет | какие события запускать; пусто = все определённые функции |
max_operations |
нет | лимит операций |
| любые свои поля | нет | доступны в скрипте как ctx.config.<field> |
Контекст ctx
Каждая функция получает объект:
{
pid: "...", // PID процесса (строка UUID)
agent: "...", // имя процесса
template: "...", // имя шаблона (или null)
session_id: "...", // или null
user_id: "...", // или null
config: {...} // сырой YAML-конфиг, включая свои поля
}
Функции хука
function before_user_message(ctx, msg) {
// msg: { role: "user", content: "..." }
// возвращаемый объект — все поля опциональны:
return {
notice: "⚡ заметка в чат", // стримится, НЕ в контекст
reply: "готовый ответ", // полный ответ, пропускает LLM
suppress: true, // отбросить сообщение
inject: [{ role: "system", content: "подсказка" }], // доп. сообщения
replace_message: { role: "user", content: "..." },
};
}
function after_turn(ctx, question, answer) {
// question / answer: { role, content } или null
// возвращает { notice: "..." } или простую строку
}
function after_loop(ctx, reason) {
// reason: "job_done" | "terminated" | "blocked" | "budget_exhausted" | ...
// возвращаемое значение игнорируется
}
Ошибка скрипта никогда не ломает ход агента.
7.3 Встроенные помощники
HTTP
const r = http_get("https://api.example.com/data");
// r = { status: 200, body: "<body>", error: "" }
// сетевая ошибка → { status: 0, body: "", error: "..." } — не бросает
const r2 = http_post("https://api.example.com/submit", "payload");
const r3 = http_post(url, body, "application/json");
Все HTTP-методы с одинаковой сигнатурой
(url, body [, content_type [, timeout [, headers]]]):
const r = http_put("https://api.example.com/item/1", `{"title":"x"}`, "application/json");
const r = http_patch("https://api.example.com/item/1", `{"title":"y"}`);
const r = http_delete("https://api.example.com/item/1", "");
Или универсальная форма http(method, url, …):
const r = http("PATCH", url, `{"status":"closed"}`, "application/json");
Таймаут по умолчанию 30с; перекрывается доп. аргументом:
const r = http_get(url, 5); // таймаут 5с
const r = http_post(url, body, "application/json", 5);
Свои заголовки (объект последним аргументом — например Referer):
const token = env_var("MY_API_TOKEN"); // "" если не задан (пишет предупреждение)
const r = http_get(url, 5, { Authorization: `Bearer ${token}` });
LLM
const a = llm("Что такое фотосинтез?"); // только user
const a = llm("Ты — краткий ассистент.", "Что такое фотосинтез?"); // system + user
const a = llm({
user: "Вопрос",
system: "Инструкция", // опционально
max_tokens: 500, // опционально (по умолчанию 1024, максимум 4096)
temperature: 0.3, // опционально
});
- Инференс идёт через провайдер ядра; токены списываются на вызывающий процесс (и его бюджетную группу).
- Уважает лимит конкурентности ядра.
- Бюджет исчерпан → ошибка
caller budget exhausted. - LLM не подключён → вызов бросает; оборачивай в
try ... catch.
Cron
Скрипты могут планировать запуски агентов (контракт общий для JS и Rhai). Эти
задания — динамические: персистятся в logs/cron.db и переживают рестарт:
const r = schedule_cron({
name: "watch-price",
template: "hello-agent",
interval_secs: 3600, // или schedule: "0 0 9 * * *", или oneshot_at
prompt: "Проверь цену и сообщи.",
notify: [{ type: "log" }],
});
// r = { name: "watch-price", ok: true }
const gone = cancel_cron("watch-price"); // -> true/false
const jobs = list_cron(); // -> массив статусов заданий
Полный справочник полей, sink'и и мониторинг — в 11-cron-jobs. Без
подключённого cron-сервиса (например hook_tool без сервера) вызовы бросают.
State store
Скрипты могут читать/писать scoped state store (scope резолвится из
вызывающего, id не нужен): state_get(scope, key), state_set(scope, key, value),
state_delete(scope, key), state_keys(scope), state_push, state_range,
state_sadd, state_smembers, state_srem. Семантика операций — как у
state_tool. Требует подключённого
logging.state_db_path.
DuckDB (SQL-базы)
Скрипты могут читать/писать DuckDB-базы через три помощника. Соединения к одному
файлу пулятся процесс-глобально (один писатель на файл), временны́е типы
(TIMESTAMP/DATE/TIME) возвращаются строками ISO 8601:
db_execute("data/dbs/{user}.db",
"CREATE TABLE IF NOT EXISTS log (id BIGINT, text TEXT)");
db_execute("data/dbs/{user}.db", "INSERT INTO log VALUES (1, 'hi')");
const q = db_query("data/dbs/{user}.db", "SELECT * FROM log", 100);
// q = { columns: [...], rows: [...], count: N, has_more: bool }
const s = db_schema("data/dbs/{user}.db"); // таблицы + колонки текстом
const s = db_schema("data/dbs/{user}.db", "log"); // одна таблица
| Функция | Описание |
|---|---|
db_query(path, sql [, limit]) |
SELECT-запрос → { columns, rows, count, has_more } (лимит по умолч. 100) |
db_execute(path, sql) |
DDL/DML → число затронутых строк (бросает на ошибке) |
db_schema(path [, table]) |
интроспекция таблиц/колонок текстом |
path поддерживает плейсхолдеры {user}, {session}, {template}/{agent},
резолвящиеся из контекста вызывающего (как у
duckdb_tool) — так скрипт получает «свою»
базу на пользователя, не видя чужих id.
Shell (CLI)
Скрипты могут запускать команду через оболочку платформы (cmd /C на Windows,
sh -c в остальных). Вызов никогда не бросает — ошибки возвращаются в карте:
const r = shell("git log --oneline -n 5");
// r = { ok: true, exit_code: 0, stdout: "...", stderr: "" }
// таймаут / несуществующая команда → { ok: false, exit_code: -1, stderr: "..." }
const r2 = shell("sleep 60", 5); // жёсткий таймаут 5с (по умолч. 30с)
shell(...) идёт через оболочку (cmd /C / sh -c), поэтому двойные кавычки и
%VAR% (на Windows) интерпретируются оболочкой. Для путей с пробелами, кавычек
внутри аргументов и % в git --pretty=format:... используй exec(...) — он
запускает процесс напрямую, без оболочки, и аргументы передаются дословно:
const r = exec("git", ["-C", ".", "log", "--pretty=format:%H%x1f%an", "-n", "5"]);
const r2 = exec("git", ["log", "--author=Alice Bob"], 30); // третий аргумент — таймаут
Чтение файлов
fs_read(path) читает UTF-8 текстовый файл (бросает на ошибке). Относительные
пути разрешаются от рабочей директории сервера:
const cfg = fs_read("data/git/repos.json");
Прочее
| Функция | Описание |
|---|---|
now() |
текущее UTC-время (строка RFC 3339) |
sleep_ms(ms) |
блокирующая пауза (ограничена 60с) — примитив задержки/backoff |
env_var(name) |
переменная окружения ("" если нет; пишет предупреждение) |
print(...), debug(...) |
лог в сервер |
urlencode(s) / url_encode(s) |
percent-encoding UTF-8 |
urldecode(s) / url_decode(s) |
percent-декодирование |
ord(c), chr(i) |
символ ↔ кодпоинт |
matches(s, regex), replace_re(s, regex, repl) |
regex-матч / замена |
extract_script_json(html, contains) |
первый JSON-блоб <script> (напр. __NEXT_DATA__) → объект, или null |
json_get(data, pointer) |
разрешить JSON-указатель (/a/b/0, экраны ~0/~1) → значение, или null |
Для JSON в JS используй нативные JSON.parse / JSON.stringify.
7.4 Лимиты и гарантии
- Нет доступа к ядру (
spawn,inject, …) — только HTTP/LLM/state/ DuckDB/shell/exec/fs_read-помощники и возвращаемые значения. max_operationsобрывает runaway-циклы (while(true){}даёт ошибку, а не зависание).shell(...)дополнительно ограничен жёстким таймаутом.- Состояние не сохраняется между вызовами — каждый вызов получает свежий
скоуп. Для персистентности используй
state_*/db_*-помощники, HTTP/внешние сервисы или встроенные хуки (faq_cache,memory). - Скрипты синхронны:
llm(...)блокирует рабочий поток на время инференса. Нормально для коротких вызовов; избегай десятков последовательных LLM-вызовов в одном скрипте.
7.5 Rhai — альтернативный язык
Для маленьких скриптов можно использовать Rhai (.rhai). Контракт идентичен,
отличается синтаксис:
fn run(args) {
let text = args.text ?? "";
text.trim(); // trim() меняет на месте и возвращает ()
if text.len == 0 { // длина — свойство `.len`
return #{ content: "текст пуст", success: false, error: "empty text" };
}
let words = text.split(" ").filter(|w| w.len > 0).len();
#{ content: `Слов: ${words}`, success: true }
}
Отличия от JS:
- Объекты —
#{ key: value }(не{ … });()— это null. - Длина строки/массива — свойство
.len(не.length). - Строковые методы (
trim,split, …) меняют строку на месте и возвращают(): пишиs.trim();и читайs, а неlet t = s.trim();. - JSON —
parse_json/to_json(вместоJSON.parse/JSON.stringify). max_operationsв Rhai — точный счётчик операций (в JS — грубый лимит инструкций QuickJS).
7.6 Отладка
hook_tool config/scripts/hooks/audit.yaml user "Привет, мир"
hook_tool config/scripts/hooks/moderation.yaml user "Привет, мир" --live
Скриптовые инструменты проверяются тестами в стиле site_tool или в живом чате
с агентом, к которому прикреплён инструмент.