sagent-code
v2.57.0
Published
AI agent that streams JavaScript into a persistent sandboxed Node.js runtime
Maintainers
Readme
sagent-code
Локальный AI-агент, у которого каждый ответ модели — это исполняемый JavaScript. Host приводит ответ к исполнимому виду и выполняет код в постоянном sandboxed Node.js-процессе, где состояние сохраняется между итерациями.
Вместо фиксированного набора tool-call'ов модель получает целый язык: она пишет код, видит результат выполнения и пишет следующий. Инструменты (dsl.*) — обычные функции внутри рантайма, а не протокол вызовов.
Как это выглядит
Ответ модели целиком:
const files = await dsl.file.rg("executeTask", { glob: "src/**/*.js" });
if (files.matches.length === 0) {
dsl.print({ status: "not found" });
} else {
dsl.print.user("Нашёл " + files.matches.length + " совпадений.");
dsl.end();
}Claude Code подключается без передачи credentials в SAgent: установите и
авторизуйте Claude Code обычным способом, затем включите
"claudeCode": "enabled" и выберите provider claude_code в
~/.sagent/settings.json. Модели и уровни reasoning будут получены через
Claude Agent SDK. Подробности — в
doc/configuration.md.
dsl.end() завершает рабочий цикл только если реально выполнился. Пока его не было, агент формирует следующую итерацию и отдаёт модели результат прошлой.
Требования
- Windows x64 — исполнение идёт через собственную песочницу
sagent-sandbox-runner.exeизbin/sagent-sandbox-win(исходники на Rust вsandbox/), других платформ рантайм не поддерживает. - Node.js ≥ 22 (
package.json→engines). - Авторизация ChatGPT/Codex, локальная сессия Claude Code либо ключ OpenAI-compatible провайдера.
Установка
npm install -g sagent-codeПоявляется команда sagent-code (короткий алиас — sagent2). Для работы из исходников вместо этого достаточно npm install в корне репозитория.
CLI работает прямо из каталога установки npm. Песочница добирается до %APPDATA%\npm\node_modules через stat_paths раннера: родительские каталоги получают ненаследуемый грант «только атрибуты», которого хватает для резолва модулей, но который не открывает ни их содержимое, ни профиль. Старое зеркалирование в ~/.sagent/app (и переменная SAGENT_APP_HOME) убраны — если каталог остался от прежних версий, его можно просто удалить.
ChatGPT-подписка включена по умолчанию и переиспользует credentials Codex CLI из ~/.codex/auth.json. Если их ещё нет:
codex loginАльтернатива — объявить OpenAI-compatible провайдера в ~/.sagent/settings.json, а ключ положить в ~/.sagent/.env. Формат и рабочий пример — в doc/configuration.md.
Запуск
Агент работает с проектом в текущем каталоге, поэтому запускать его нужно из корня нужного проекта. Терминальный режим:
sagent-codeКоманда без ключей поднимает HTTP-сервер с веб-панелью (порт 3210 или следующий свободный) и печатает URL панели и Bearer-токен. Терминального чата нет: вся работа идёт в панели, консоль остаётся для логов. Другой порт:
sagent-code --http 4000Из исходников те же команды выглядят как node src/agent.js [флаги].
Флаги: --http [port], --host <host> (по умолчанию 127.0.0.1), --version, --help. Ключ --http-only и переменная SAGENT_HTTP_ONLY устарели, но принимаются и ничего не меняют. Установленная версия видна по sagent-code --version и в шапке HTTP-панели. Остановка — Ctrl+C.
При первом запуске создаются ~/.sagent/settings.json и ~/.sagent/.env, а также примеры settings.example.json и .env.example — примеры обновляются вместе с пакетом, когда несут более новую редакцию (штамп даты внутри файла); сами settings.json и .env никогда не перезаписываются.
Каналы вывода
Три канала строго разделены — это центральная часть контракта:
| Вызов | Кому видно | Попадает в контекст модели | Завершает цикл |
| --- | --- | --- | --- |
| dsl.print(value) | только модели | да, как [prints], остаётся в истории | нет |
| dsl.print.once(value) | только модели | только следующий запрос, блоком [once], мимо истории | нет |
| dsl.print.user(value) | только пользователю | нет | нет |
| dsl.end() | — | — | да, при фактическом выполнении |
Ответ пользователю — это GitHub Markdown, в котором разрешены HTML и инлайновый SVG: таблицы, <details>, бейджи, диаграммы, картинки из проекта через <img src="doc/chart.png">. Санитайзер вырезает <script> и обработчики событий, поэтому интерактивность строится на CSS. Блок <style> разрешён и автоматически ограничивается своим сообщением: селекторы получают префикс контейнера, так что body, html и * внутри него означают само сообщение, а переоформить панель из ответа нельзя. Набор классов Tailwind, доступный модели, закреплён safelist'ом (@source inline в http/src/style.css) и перечислен в системном промпте — иначе сборщик вырезал бы неиспользуемые утилиты и разметка ответа приезжала бы без стилей.
dsl.print.once — канал для данных, которые нужны ровно один ход (файлы, диффы, выдача поиска): модель смотрит на них один раз, и в историю они не попадают. Канал выбирается по времени жизни данных, а не по их объёму: хвост запроса не кэшируется и оплачивается полностью при каждой доставке, поэтому то, что понадобится и через три хода, дешевле один раз отправить обычным dsl.print — оно станет частью кэшируемого префикса. При ошибке следующей ячейки данные доставляются повторно, пока какая-нибудь ячейка не выполнится без ошибок.
Инструменты
dsl.file — файловое семейство: read (диапазоны строк, grep с контекстом, дистилляция через ask-модель — файл не попадает в контекст главной модели), write (create-only по умолчанию; с { ask: true } содержимое генерирует ask-модель по короткому ТЗ), patch, rg, ls, exists, append, stat, rm. Все пути — внутри проекта, возвраты — структурные объекты. dsl.ask — запрос к отдельной быстрой модели; dsl.ask.oracle — project-wide вопрос по снимку групп исходников. dsl.viewImage, dsl.generateImage — работа с изображениями. dsl.chrome — управление Chrome по DevTools Protocol (вкладки, ввод, эмуляция, скриншоты, console/network/trace-мониторы). Ожидания строятся на условии, а не на паузе: waitFor(выражение) ждёт, пока условие в странице станет истинным, а click(цель, { until }) подтверждает эффект клика и повторяет его заданное число раз — элемент бывает видим раньше, чем фреймворк навесил обработчик. Забытые сессии наблюдения гасятся в конце задачи, с сообщением о том, что именно закрыто. dsl.fun и mem — persistent функции и данные, переживающие рестарт рантайма. dsl.code — оформление fenced-блоков для пользовательского Markdown.
Функции fun.watch_* — сенсоры: host вызывает их все параллельно перед каждой генерацией, и непустые возвраты приходят модели блоком [watch] строками имя: вывод, не попадая в историю. Приоритет задаётся числом (fun.watch_disk = 20 — по образцу описания строкой): больше — выше в блоке и первее при урезании бюджета. Модель сама пишет себе наблюдателей — постоянный маленький статус или одноразовое оповещение с самоотключением; ошибка или таймаут сенсора приходят в его строке коротким [watch error], не задерживая остальных. Состояние между опросами сенсор получает первым аргументом: это объект mem.fun.<имя функции>, приготовленный заранее, и его мутация уже есть сохранение — mem, как и fun, пишется после каждой завершившейся ячейки. Держать в нём стоит дельту, а не журнал, и никогда — handle: таймер или сокет в любом узле mem останавливает сохранение всего графа, о чём агент теперь сообщает вслух вместо прежнего молчания.
fun ведёт статистику использования: сколько раз функцию вызывали и правили (dsl.fun.get — по функции, dsl.fun.stats — суммарно и топ вызываемых). Счётчики переживают рестарт и сбрасываются при удалении функции; автоматические опросы watch_* вызовами не считаются.
dsl.git([...]) — единственный способ работать с репозиторием, кроме чтения: sandbox runner запрещает запись в .git, поэтому внутри песочницы не создаётся даже index.lock, а креденшелов к remote у неё нет намеренно. Команда исполняется на хосте, с профилем и правами пользователя, и по умолчанию выключена: доступ включается тумблером в панели с выбором уровня — «только чтение» (порцелан и плумбинг вроде log, diff, diff-tree, rev-list, cat-file, for-each-ref, плюс fetch и ls-remote) или «полный» (весь рабочий цикл вплоть до push, а также init, clone, патчи и обслуживание). На чтении разбирается и подкоманда: remote add, stash pop или worktree add требуют полного уровня, хотя сами команды на чтении доступны в своих читающих формах. Настройка живёт в ~/.sagent/settings.json, вне песочницы, поэтому модель не может выдать права себе. Хуки репозитория при исполнении отключены, подмена конфига на лету запрещена, каждый вызов печатается в терминал. Если репозиторий лежит не в корне проекта — подмодуль, vendor-каталог, отдельный репозиторий внутри дерева, — он адресуется вторым аргументом: dsl.git(["status", "--short"], { cwd: "vendor/lib" }). Путь считается от корня проекта, выход за его пределы отклоняется до запуска git, а каталог, в котором команда выполнилась, возвращается в поле cwd.
Авторитетный справочник живёт в самом рантайме: dsl.help() возвращает индекс тем, dsl.help("ask") — полную сигнатуру, ограничения и примеры конкретной команды. Документация проекта описывает только общую модель работы.
HTTP-панель
Слева — чат, состояние задачи, выбор провайдера/модели/reasoning, compact, interrupt, вложения и статистика контекста. В пикере основной модели включается «Основная модель 2»: тогда нечётные итерации задачи ведёт первая модель, чётные — вторая (счёт от начала каждой задачи, история и контекст у них общие). Справа — вкладка «План» (шаги текущей задачи; открыта по умолчанию), временные globals, persistent-функции (поиск, статистика вызовов/правок, приоритет, исходники), память, вкладка «DSL» со справкой dsl.help ровно в том виде, в каком её читает модель, файловое дерево, группы исходников, Oracle с параллельными запросами, журналом, выбранной моделью, 20-секундным прогрессом токенов и автопересборкой снимка не чаще раза в минуту, а также Chrome CDP.
Строка итерации начинается с номера хода #N — это счёт от начала истории, обнуляемый компактом: столько ходов агента сейчас лежит в контексте и перечитывается моделью в каждом запросе. Чип thinking раскрывает живой поток рассуждений модели: пока идёт thinking-фаза, блок открыт автоматически и сворачивается по завершении итерации; клик фиксирует состояние вручную. Рядом — once (токены и содержимое данных print.once, доставленных в запрос), watch (вход от сенсоров fun.watch_*), тройной счётчик печати print A/B/C (print / print.once / print.user) и чипы dsl N/fun N с поимённой разбивкой вызовов. Нулевые чипы скрыты, блоки работают аккордеоном; в финале задачи виден блок «Сэкономлено» — токены, ушедшие через print.once мимо истории.
Шестерёнка в шапке открывает настройки: подключение, автокомпакт, модели dsl.ask и Oracle, git вне песочницы. Автокомпакт и git включаются тумблером в строке заголовка карточки и сохраняются сразу, токен и модель dsl.ask применяет кнопка «Сохранить настройки» слева от «Закрыть». Автокомпакт включён по умолчанию: когда контекст заполнен до порога (по умолчанию 80%, регулируется в диапазоне 50–90%), история сжимается сама, а в ленте появляется строка «Автокомпакт: контекст заполнен на N% при пороге M%» — молча сжимать историю за пользователя нельзя. Порог проверяется после каждого хода, поэтому длинная задача сжимается на ходу и продолжается на сжатом контексте: план, mem и fun это переживают, детали из истории заменяются её пересказом.
Версия в шапке мигает, когда в npm появилась свежая: сервер спрашивает реестр при запуске и раз в час. Клик открывает подтверждение, по «Да» ставится npm install -g sagent-code@latest, процесс перезапускается сам, а страница перезагружается уже на новой версии. Обновление доступно, только когда агент свободен — npm заменяет файлы под работающим рантаймом.
Панель живёт во время хода: настройки, выбор моделей, редактор и отправка сообщений доступны, пока модель работает. Сообщение, отправленное на ходу, не заводит вторую задачу — оно встаёт в очередь и доезжает до модели следующей итерацией, а если dsl.end() уже выполнен, цикл продлевается на один ход, чтобы ответ на него всё-таки прозвучал. Заблокирован во время работы только переключатель прав: он гасит рантайм.
Сервер отдаёт production-сборку из корневого dist/. Этот каталог не хранится в git, поэтому после клонирования и после любых правок в http/ нужен npm run build:http — иначе панель вернёт 404. Маршруты и ограничения — в doc/http-api.md.
Токен генерируется на каждый запуск и печатается в консоль. Для клиента с 127.0.0.1 он намеренно не требуется — панель рассчитана на доверенную локальную машину. Для удалённого адреса токен обязателен, а явно заданный SAGENT_HTTP_TOKEN проверяется и локально.
Изоляция
Уровень изоляции выбирается переключателем прав левее выбора основной модели: «через песочницу» (по умолчанию) или «полный доступ». Полный доступ — это буквально отсутствие песочницы: worker запускается напрямую, с вашим окружением и правами, и всё описанное ниже к нему не относится. Уровень фиксируется при запуске процесса, поэтому смена останавливает рантайм — следующая задача поднимет его заново, fun и mem это переживают. Значение хранится в ~/.sagent/settings.json (permissions), вне песочницы, поэтому модель не может выдать права себе.
Под песочницей Node-worker запускается через sagent-sandbox-runner.exe с политикой текущего проекта — это единственный слой изоляции, сам Node работает без ограничений. Запись разрешена только в корень проекта, чтение — в проект, каталог пакета и родительские каталоги до границы профиля (нужны Node для резолва модулей), а ~/.ssh, ~/.aws, ~/.azure, ~/.kube, ~/.config/gcloud и CODEX_HOME попадают в deny-list. Домашний каталог, AppData и %TEMP% не выдаются, поэтому проект не может находиться внутри них. Политика пишется в .sagent/sandbox-policy-<pid>-<агент>-<nonce>.json — файл на сессию, а не на процесс: при смене прав старая сессия гасится в фоне и удаляет свой файл, и общее имя означало бы удаление политики уже поднятой новой сессии. Политики мёртвых процессов подчищаются при создании следующей.
Chrome запускается host-процессом вне песочницы (src/chrome-launcher.js), а рантайм только подключается к готовому CDP-эндпоинту. Иначе браузер унаследовал бы restricted token и ACL раннера, не смог бы прочитать свой каталог установки и не поднял бы CDP. Модельный dsl.chrome.ensure({ launch: true }) тоже работает: рантайм запрашивает запуск у host'а и ждёт готовый эндпоинт. Поднять браузер можно и кнопкой «Запустить Chrome» в панели или вручную с --remote-debugging-port.
Несколько агентов в одном браузере
Все экземпляры sagent-code делят один Chrome на одном CDP-порту с общим профилем ~/.sagent/chrome-profile. Каждый агент получает свой SAGENT_AGENT_ID и работает только в своих вкладках: dsl.chrome.tabs() показывает лишь их, обращение к чужому targetId отклоняется, а dsl.chrome.openTab() и closeTab() управляют собственными вкладками. Первая вкладка создаётся при первом обращении к браузеру; если endpoint не позволяет открывать вкладки (внешний Chrome, отданный только на чтение), агент забирает свободную существующую.
Владение вкладками ведёт host в ~/.sagent/chrome-tabs.json — песочница пишет только внутрь проекта и не может держать общий для машины реестр. Записи мёртвых вкладок вычищаются автоматически, поэтому упавший агент не занимает вкладку навсегда. close закрывает только вкладки своего агента и никогда не посылает Browser.close: Chrome с интерфейсом завершается сам, когда закрыта последняя вкладка. Поэтому один агент не выбивает браузер у остальных, а последний уходящий оставляет систему без лишних процессов. Субагенты получат собственные id тем же механизмом.
Исключение — headless: без интерфейса Chrome не применяет правило «последняя вкладка закрыта → выход» и остаётся ждать команд. Для headless-прогонов процесс нужно завершать самостоятельно.
Изоляции между вкладками одного профиля нет: cookies, localStorage и логины общие для домена. Если агентам нужны разные сессии, им нужен разный SAGENT_CHROME_USER_DATA_DIR вместе с разным SAGENT_CHROME_DEBUG_URL.
Файлы, результаты поиска, логи, DOM, browser console и сетевые ответы считаются недоверенными данными: модель их анализирует, но не исполняет как инструкции.
Данные проекта
Всё состояние агента лежит в $project/.sagent/:
| Путь | Что это |
| --- | --- |
| history.jsonl | активная история |
| history-<дата>_<время>.jsonl | архивы после /compact |
| fun.ini | исходники persistent-функций и описания |
| mem.ini | persistent-граф данных |
| plan.ini | план текущей задачи (dsl.plan): переживает рестарт и /compact, правится руками; выполненный уезжает в plan-<дата>_<время>.ini |
| function/ | файловые инструменты модели |
| upload/ | вложения из HTTP UI |
Структура репозитория
src/ host-агент, модели, история, HTTP и sandbox launcher
src/runtime/ persistent Node worker, DSL, Chrome CDP, state persistence
http/ исходники Vue/Vite/Tailwind панели
dist/ production-сборка панели (генерируется, не в git)
bin/ CLI-обёртки и собранная песочница sagent-sandbox-win
sandbox/ исходники песочницы на Rust (npm run build:sandbox)
scripts/ вспомогательные CDP-скрипты
tests/ автономные Node.js тесты
doc/ документацияРазработка
npm run dev:httpnpm testnpm test сначала собирает панель, затем последовательно прогоняет тесты runtime, истории, настроек, model routing, HTTP, изображений, Chrome и модельного цикла.
Правила изменений (синхронизация runtime-протокола, документирование новых DSL-команд, safe projection для публичных HTTP-полей) — в doc/development.md.
Документация
- doc/README.md — оглавление
- doc/architecture.md — процессы, модули, поток данных
- doc/runtime-and-dsl.md — жизненный цикл JS-ячеек и persistent-состояние
- doc/configuration.md —
settings.json, провайдеры, секреты, дополнительные инструкции - doc/http-api.md — панель, маршруты, ограничения
- doc/development.md — сборка, тесты, правила изменений
- CHANGELOG.md — история версий
Лицензия
MIT
