Использование больших языковых моделей (LLM) для программирования и взаимодействия с терминалом становится все более востребованной областью в индустрии искусственного интеллекта. Компания Engine Labs недавно продемонстрировала значительный прорыв в этом направлении, достигнув второго места на престижном рейтинге Terminal Bench. Их терминальный инструмент превзошел популярное решение Claude Code приблизительно на 25%, что эквивалентно улучшению на 10 процентных пунктов — впечатляющее достижение для небольшой, но опытной технической команды. В данной статье мы подробно рассмотрим эволюцию разработки терминального инструмента Engine Labs, основные вызовы и методы, которые позволили добиться таких успехов, а также причины, почему эта разработка стала важной в контексте современных AI-агентов для программирования. Современные AI-агенты все чаще нуждаются в возможности взаимодействовать с операционной системой через командную строку, в частности с Bash.
Такие решения есть у OpenAI Codex, Google Jules и Devin, однако реализация каждого из них имеет свои особенности. Например, Anthropic предлагает создавать постоянную Bash-сессию, с которой языковая модель взаимодействует без прерывания, тогда как OpenAI запускает каждую команду в отдельном дочернем процессе. Engine Labs взяли этот вопрос как отправную точку для исследований, и с течением времени пришли к выводу, что постоянная сессия обладает рядом преимуществ. Первоначальная реализация Engine Labs была похожа на подход OpenAI: команды, генерируемые языковой моделью, запускались как отдельные дочерние процессы через Node.js модуль child_process.
Такой подход был прост и достаточно эффективен для базовых задач, однако вскоре проявились ограничения. Команды, требующие взаимодействия с пользователем, например, запрос пароля или выбор параметров установки, приводили к зависаниям или тайм-аутам, так как процесс ожидал ввода, которого не было. Кроме того, запуск REPL-сред, таких как Python или Node.js интерактивные интерфейсы, вообще не работал в таком режиме, ведь каждая команда запускалась отдельно и не могла сохранять контекст. Чтобы избежать подобных проблем, команда Engine Labs вводила различные профилактические меры — устанавливали переменные окружения, запрещающие интерактивное взаимодействие, например DEBIAN_FRONTEND=noninteractive, и давала агенту рекомендации избегать команд, требующих ввода пользователя.
Несмотря на эти меры, решение оставалось неудобным и ограниченным, особенно для задач, где необходимо выполнение длительных или непрерывных процессов. Помимо этого, не предлагая возможности запуска постоянных сессий, инструмент сталкивался со сложностями при работе с командами, которые не завершались самостоятельно, например, веб-серверами или процессами, ожидающими сетевого ответа. Работа над тайм-аутами частично решала проблему, но ограничивала возможности агента запускать длительные операции, такие как компиляция больших проектов или сборка Docker-образов. Следующим этапом стала попытка реализовать постоянную Bash-сессию с использованием библиотеки node-pty, которая имитировала терминал и обеспечивала стабильный интерфейс для взаимодействия с оболочкой. Это позволяло сохранению состояния между командами и давало возможность работать с интерактивными процессами.
Однако это сразу породило новую проблему — как определить, что команда завершена и терминал готов принять следующий ввод? Практически это была усеченная версия классической задачи останова (halting problem), известной своей вычислительной сложностью и невозможностью гарантированного решения для всех случаев. Решение Engine Labs при этом не стремилось найти универсальный ответ, а использовало набор эвристик и практических трюков, направленных на покрытие наиболее частых сценариев взаимодействия. Одним из таких приемов было отслеживание «стабильности» вывода — если вывод не изменялся в течение определенного времени, прогнозировалось, что команда завершена или ожидает ввода. В работе с короткими командами, вроде ls, это давало отличный результат. Для долгого выполнения, например запуска Python REPL, агент наблюдал за появлением характерного приглашения >>>, после чего считал сеанс готовым к следующему действию.
Другой важной практикой была имитация подхода, используемого в демонстрациях Anthropic — после каждой выполненной команды автоматически добавлялся echo с уникальным сигнальным значением. Поиск этого значения в выводе помогал идентифицировать момент окончания обработки команды. Однако такой подход не работал с интерактивными средами, запускаемыми в терминале. Engine Labs пытались углубиться в системное программирование, чтобы с помощью утилиты strace отслеживать системные вызовы терминала и определять, ожидает ли процесс ввод. Это могло бы теоретически решить вопрос с зависаниями, но практика показала, что сложность решения и технические ограничения перевесили потенциальные выгоды, и этот путь был оставлен.
Особое внимание в разработке уделялось управлению временем ожидания вывода, ведь слишком короткая задержка могла приводить к ошибочным срабатыванием на команды с долгим молчанием, а слишком длинная — к замедлению работы агента в целом. В итоге, разработчики пришли к компромиссным настройкам, способным балансировать между задержкой и отзывчивостью, учитывая реальные ограничения среды, в которой исполняются команды — далеко не всегда оптимальной мощности. Стоит отметить, что в отличие от других решений, Engine Labs уделили внимание обработке управляющих символов и клавиш со стрелками, что позволило более реалистично имитировать поведение терминала и работы пользователя внутри сессии. Несмотря на то, что компания не раскрывает деталей коммерческих наработок, упомянуто, что в их текущей реализации отсутствуют субагенты или дополнительные терминальные инструменты — все построено вокруг одного Bash-инструмента. Такое решение принесло значительный успех — Engine Labs заняли второе место в Terminal Bench, уступив только компании, основным продуктом которой является терминал.