🤖 AI-агенты для бизнеса

7. Скрипты (JavaScript и Rhai)

🔵 Инструменты и хуки можно писать как скрипты, исполняемые встроенным движком и подгружаемые из YAML — без пересборки. Два движка, выбор по расширению файла:

Оба движка дают одинаковый контракт и одинаковые функции-помощники (см. §7.3). Путь script: на .js запускается на QuickJS; любое другое расширение — на Rhai.

Готовые примеры в 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: "короткая причина" };
}

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,         // опционально
});

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 Лимиты и гарантии


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:


7.6 Отладка

hook_tool config/scripts/hooks/audit.yaml user "Привет, мир"
hook_tool config/scripts/hooks/moderation.yaml user "Привет, мир" --live

Скриптовые инструменты проверяются тестами в стиле site_tool или в живом чате с агентом, к которому прикреплён инструмент.