API ВЕГА
English

luftpause

Среда выполнения под вашей обвязкой агента. Каждый вызов инструмента — это await; движок забывает о запуске, а luftpause сохраняет его состояние.

Вызовы инструментов разбивают работу агента на ходы. Для движка инференса каждый ход — новый запрос от неизвестного отправителя: когда инструмент возвращает результат, продолжение встаёт в очередь после всех пакетных запросов, поступивших за время ожидания, а KV cache может быть уже недоступен.

luftpause располагается между обвязкой и движками и сохраняет состояние запуска:

  • Продолжения обрабатываются первыми. Запуск, приостановленный в ожидании вызова инструмента, возобновляется раньше пакетных задач; пакетные ходы оставляют место для интерактивных запусков, которые вернутся в очередь.

  • Запуск остаётся там, где находится его префикс. Пока запуск генерирует ответ или ждёт инструмент, его ходы направляются только на реплику с соответствующим KV cache; новые ходы движок принимает в собственную очередь.

  • Один отправитель не может лишить другого ресурсов. Ожидающие ходы обрабатываются справедливо для всех отправителей.

  • Любая обвязка — два способа подключения. Укажите luftpause в base_url (совместимый с OpenAI интерфейс, собственный цикл агента) или подключитесь через MCP и получите субагентов spawn / await, которые переживают перезапуск.

Измерения на ноутбуке с GPU на 8 ГБ при полной загрузке движков (48 пакетных запусков и 4 интерактивных запуска одновременно):

Пропускная способностьЗадержкаЦена
Агенты с собственным циклом через luftpause по сравнению с прямым подключением к маршрутизатору SGLangВ 2,0 раза выше для интерактивных запусков (12,7 против 6,4 запуска/мин)Медианная задержка интерактивных запусков — 19 с вместо 36 сПакетная пропускная способность ниже на 9 %
Субагенты через MCP с luftpause по сравнению с прямым подключением среды выполнения к маршрутизаторуВ 2,7 раза выше для интерактивных запусков (8,0 против 3,0 запуска/мин)Медианная задержка интерактивных запусков — 27 с вместо 58 сБез изменений для пакетных задач
Параллельный запуск 16 субагентов по сравнению с последовательным запускомВ 5,3 раза быстрее (все 16 завершаются за 23 с вместо 120 с)Медианная задержка для одного субагента — 15–22 с, как и при подключении через маршрутизаторНикаких: один только маршрутизатор справляется за 21,5 с
Claude Code в роли клиента, шесть запросов к WikipediaВ 2,9 раза быстрее (12,5 с вместо 36 с при последовательной обработке без дополнительного промпта)Все шесть субагентов на модели 70B выполняются за один цикл, 9,3 с0 неудачных вызовов инструментов, 0 повторных запусков

Попробуйте

docker compose up -d                # имитационные движки, GPU не требуется
claude mcp add --transport http --scope local luftpause http://localhost:7000/mcp

Команда docker compose up -d загружает опубликованные образы; добавьте --build, чтобы собрать их из исходного кода (первая сборка занимает несколько минут). По умолчанию загружается latest; зафиксировать версию можно так: LUFTPAUSE_VERSION=v0.1.3 docker compose up -d. Среда выполнения также опубликована в официальном реестре MCP под именем io.github.vmedvedovskiy/luftpause.

Теперь Claude Code поддерживает инструменты spawn, await, run, status, cancel, approve, tools и decide. Имитационные движки отвечают текстом-заполнителем. Единственные инструменты субагентов — lookup и deep_search из fake-mcp: они ждут примерно секунду и тоже возвращают текст-заполнитель. Этого достаточно, чтобы увидеть параллельный запуск. Чтобы получать настоящие ответы от уже запущенного на вашей машине Ollama и использовать Wikipedia в качестве инструмента субагентов вместо имитационных инструментов:

docker compose -f compose.yaml -f compose.ollama.yaml up -d scheduler dataplane runtime

API Wikipedia ограничивает частоту запросов от анонимных клиентов, поэтому вызов инструмента может занимать десятки секунд, пока wikipedia-mcp выполняет повторные попытки.

Llama 3.2 1B иногда передаёт схему инструмента вместо аргументов. Среда выполнения отклоняет такой вызов, и субагент может ответить без использования инструмента. В этом случае повторите запрос или загрузите более крупную модель.

Модель необходимо загрузить в Ollama (ollama pull llama3.2:1b или задайте OLLAMA_MODEL).

Кроме того, Ollama должна принимать подключения не только через loopback (OLLAMA_HOST=0.0.0.0), если Docker Desktop уже не может подключиться к ней напрямую. В Windows и macOS Docker Desktop подключается к Ollama с привязкой к loopback по умолчанию. По умолчанию Ollama обрабатывает запросы по одному; увеличьте значение OLLAMA_NUM_PARALLEL, чтобы выполнять несколько запросов одновременно. Поэтому субагенты выстраиваются в очередь внутри Ollama: luftpause не получает от неё метрик очереди и не ограничивает её загрузку. Аналогичным образом работают SGLang на вашем GPU (compose.testbed.yaml) и любая конечная точка, совместимая с OpenAI (пример с размещённым pod vLLM приведён в compose.runpod.yaml).

Отличия

Маршрутизаторы распределяют запросы по пулу GPU, а MCP-серверы поручают локальной модели подзадачи. Ни те ни другие не понимают, что такое запуск. luftpause понимает: через оба способа подключения он видит каждый ход и каждый вызов инструмента каждого запуска и распределяет их по общему пулу, учитывая три обстоятельства, которые движку неизвестны: приостановленный запуск возобновится, задачи бывают разных классов, а запросы поступают от разных отправителей.

Преимущества достигаются за счёт управления допуском, а не маршрутизации. При одной лишь маршрутизации результат такой же, как у маршрутизатора SGLang с учётом кэша (см. ниже); при незагруженных движках показатели всех вариантов одинаковы. Разница проявляется при полной загрузке движков — именно тогда она и важна.

Два способа подключения

  • Прокси, совместимый с OpenAI — агент с собственным циклом указывает luftpause в base_url. Для каждого запуска он передаёт x-luftpause-run-id и, при необходимости, x-luftpause-class и x-luftpause-caller. После этого каждый вызов инструмента в цикле планируется как await.

  • Среда выполнения MCP — агент, например Claude Code, подключается через MCP и получает инструменты spawn / await. luftpause выполняет цикл каждого субагента через тот же прокси: ходы LLM направляются в пул, вызовы инструментов — на вышестоящие MCP-серверы, в качестве отправителя используется сессия MCP, а журнал позволяет запускам пережить завершение процесса.

В examples/ приведены три агента с собственным циклом, которые задают все три заголовка: пакет openai, OpenAI Agents SDK и LangGraph.

Подключение из

Обвязки, которые не умеют передавать заголовок для каждого запуска, тоже работают через прокси: запуск определяется по tool_call_id, которые она передаёт обратно. Для справедливого распределения достаточно фиксированного значения x-luftpause-caller.

ИнструментПодключение через MCPПодключение через прокси
Claude Codeclaude mcp add --transport http luftpause http://localhost:7000/mcpНе применимо: используется Anthropic Messages API, а не Chat Completions
Codex CLI~/.codex/config.toml: [mcp_servers.luftpause] url = "http://localhost:7000/mcp"Не применимо: wire_api принимает только responses, а прокси предоставляет Chat Completions
Gemini CLIgemini mcp add --transport http luftpause http://localhost:7000/mcp или .gemini/settings.json: {"mcpServers": {"luftpause": {"httpUrl": "http://localhost:7000/mcp"}}}Не применимо: используется Gemini API (не проверено)
Goosegoose session --with-streamable-http-extension "http://localhost:7000/mcp" или goose configure → Add Extension → Remote Extension (Streamable HTTP)GOOSE_PROVIDER=openai OPENAI_HOST=http://localhost:8080 OPENAI_API_KEY=x OPENAI_CUSTOM_HEADERS="x-luftpause-caller=goose"
Cursor~/.cursor/mcp.json: {"mcpServers": {"luftpause": {"url": "http://localhost:7000/mcp"}}}Не применимо: настройка «Переопределить базовый URL OpenAI» задаётся для серверов Cursor, которые не могут подключиться к localhost (не проверено)
Zedsettings.json: "context_servers": {"luftpause": {"url": "http://localhost:7000/mcp"}}settings.json: "language_models": {"openai_compatible": {"luftpause": {"api_url": "http://localhost:8080/v1", "available_models": [{"name": "", "max_tokens": 128000}], "custom_headers": {"x-luftpause-caller": "zed"}}}}
OpenHandsSettings → MCP Servers → добавьте http://localhost:7000/mcpSettings → LLM → Advanced: Custom Model openai/, Base URL http://localhost:8080/v1, любой API key; настройка заголовков отсутствует
Cline / Roo CodeCline cline_mcp_settings.json: {"mcpServers": {"luftpause": {"type": "streamableHttp", "url": "http://localhost:7000/mcp"}}}; Roo .roo/mcp.json: то же самое, но с "type": "streamable-http"Провайдер «OpenAI Compatible», Base URL http://localhost:8080/v1, любой API key; добавьте x-luftpause-caller в Custom Headers (не проверено)

Архитектура

%%{init: {"themeVariables": {"fontFamily": "-apple-system, Segoe UI, Helvetica, Arial, sans-serif"}, "flowchart": {"wrappingWidth": 260}}}%%
flowchart LR
    mcp["Claude Code<br/>or any MCP client"]
    own["Agents with<br/>their own loop"]

    subgraph lp["luftpause"]
        runtime["<b>runtime</b> · .NET<br/>spawn · await · journal<br/>sub-agent loops"]
        dp["<b>dataplane</b> · Rust<br/>OpenAI-compatible proxy"]
        sched["<b>scheduler</b> · .NET<br/>decide(state, event)"]
    end

    tools[("MCP tool<br/>servers")]

    subgraph pool["Engine pool"]
        direction TB
        engines["SGLang · vLLM"]
        router["SGLang router"]
        hosted["Hosted OpenAI-<br/>compatible APIs"]
    end

    mcp -- MCP --> runtime
    own -- "HTTP / SSE" --> dp
    runtime -- "tool calls" --> tools
    runtime -- "LLM turns" --> dp
    dp -- "turns" --> pool
    sched <-- "Admit · Report · run ended" --> dp
    sched -. "health · load" .-> pool
  • runtime (runtime/) — MCP-сервер. Каждый запущенный процесс выполняется как .NET-задача (Task), в которой работает цикл агента; вызовы инструментов направляются на настроенные MCP-серверы. Все процессы записываются в журнал runs.jsonl и возобновляются после перезапуска. По завершении процесса dataplane получает уведомление (DELETE /v1/runs/{id}), чтобы планировщик сразу удалил его из памяти.

  • dataplane (dataplane/crates/dataplane) — прокси на базе tokio/axum. Он определяет процесс по заголовку или идентификаторам вызовов инструментов, считывает заголовки класса и вызывающей стороны, запрашивает у планировщика, куда направить ход, передаёт потоковый ответ движка без изменений и сообщает о первом токене, использовании ресурсов и причине завершения.

  • scheduler (scheduler/) — ядро принятия решений состоит из чистых функций, работающих с неизменяемым состоянием (Luftpause.Scheduling); gRPC-хост выполняет соответствующие действия.

  • Комплект для тестирования — fake-engine (OpenAI и /classify с имитацией задержки), fake-mcp (инструменты lookup и медленный deep_search), tools/loadgen (агенты, параллельный запуск субагентов через MCP и тесты пропускной способности с замкнутым циклом).

Что делает

ВозможностьКак работает
spawn / await через MCPspawn(task, model?, serviceClass?, system?, maxToolCalls?, tools?, maxTokens?, parallelToolCalls?, parent?, budgetTokens?, budgetToolCalls?, budgetSeconds?, approveTools?) сразу возвращает дескриптор; await(handles, all|any, timeout) собирает результаты с полями toolCalls / failedToolCalls. Также доступны tools, status, cancel и decide(input, labels?, model?): без labels используются метки модели однопроходной классификации; с 2–20 метками выполняется один ход генеративной модели с max_tokens: 1, а результат оценивается по логарифмам вероятностей первого токена. В ответе возвращаются label, probabilities и unscored
Сохранение запусковКаждый запуск записывается в журнал (runs.jsonl в Runtime:StateDir): создание, сообщения каждого раунда вызова инструментов и завершение. После перезапуска среда выполнения воспроизводит журнал, возвращает результаты завершённых запусков при вызове await и возобновляет незавершённые с последнего полного раунда вызова инструментов
Дерево запусковspawn(parent) создаёт дочерний запуск; status показывает parent и children; отмена родительского запуска через cancel отменяет все его выполняющиеся дочерние запуски. Эндпоинт среды выполнения GET /status возвращает дерево запусков: для каждого запуска указаны его собственные показатели и суммарные показатели поддерева (ходы, вызовы инструментов, неудачные вызовы инструментов, токены запроса и ответа, время выполнения ходов и инструментов)
Бюджетыspawn(budgetTokens?, budgetToolCalls?, budgetSeconds?) ограничивает весь бюджет поддерева запуска — самого запуска и всех созданных в нём дочерних запусков, включая будущие. Ограничения задаются по сумме токенов запроса и ответа, числу вызовов инструментов и времени в секундах. Они проверяются на каждом ходу и раунде вызова инструментов во всём поддереве запуска с заданным бюджетом. Для ограничения по времени используется таймер, поэтому лимит сработает, даже если поддерево простаивает. При превышении лимита поддерево отменяется с сообщением budget exceeded: /, а status и await показывают budget с полями limits, used и exceeded
Подтверждение вызовов инструментовВызов инструмента субагентом ожидает подтверждения от пользователя клиента, если сервер пометил инструмент как destructiveHint: true (и не как readOnlyHint: true) или если он указан в spawn(approveTools). Статус запуска меняется на AwaitingApproval, а в pendingApproval {tool, arguments} указываются ожидающий подтверждения инструмент и его аргументы. Если клиент поддерживает elicitation и его вызов await или run охватывает этот запуск, ему отправляется форма с одним полем (allow): в рамках сессии — как запрос elicitation, а в запросе без сессии по протоколу 2026-07-28 — как результат, требующий ввода. Остальные клиенты могут ответить вызовом approve(handle, allow). При отказе субагент получает сообщение Error: the user declined this tool call. Решения сохраняются в журнале: если ответ получен непосредственно перед перезапуском, повторный запрос не отправляется; если ответа не было — запрос отправляется снова. Клиенты: Claude Code 2.1.218 в терминале показывает запрос во время await, и ответ передаётся запуску (проверено); вкладка Code в настольном приложении Claude отклоняет запрос без отображения — при любом режиме разрешений, поэтому запуск получает отказ в вызове инструмента; клиент Python mcp 2.3 был опрошен обоими способами на тестовом стеке; любой клиент может использовать approve
MCP Tasksrun(task, ...) принимает аргументы spawn и ожидает завершения запуска в рамках того же вызова. Клиент, поддерживающий расширение Tasks (io.modelcontextprotocol/tasks, протокол 2026-07-28), вместо этого получает задачу: он опрашивает tasks/get; если запуск ожидает подтверждения, задача получает статус input_required с запросом ввода elicitation, на который клиент отвечает через tasks/update. Задачи записываются в tasks.jsonl рядом с runs.jsonl; задача, остававшаяся открытой при остановке среды выполнения, после перезапуска завершается с ошибкой, а её запуск возобновляется обычным образом. Для остальных клиентов run остаётся одним блокирующим вызовом. Клиенты: клиент SDK C# 2.2 (проверено в процессе); Claude Code 2.1.218 не поддерживает Tasks, поэтому для него run остаётся блокирующим вызовом
СтатусЭндпоинт планировщика GET /status на :5001 (HTTP/1; gRPC работает на :5000) возвращает сведения о репликах и выполняемых ходах относительно их ёмкости, удерживаемых ходах по классам и вызывающим сторонам с указанием самого долгого ожидания, а также о запусках по этапам и классам. Эндпоинт GET / на этом же адресе открывает страницу, которая опрашивает его каждые 2 секунды; параметр ?runtime=http://host:7000 дополнительно отображает дерево запусков этой среды выполнения и затраты по поддеревьям
Справедливое распределение между вызывающими сторонамиКаждый ход содержит x-luftpause-caller (сессию MCP для субагентов и произвольную строку для агентов с собственным циклом). Удерживаемые ходы сначала передаются классу interactive, затем — вызывающей стороне с наименьшим числом выполняемых ходов. Поэтому всплеск запросов от одной стороны не блокирует остальные
Инструменты вне ходовЗапуск, ожидающий ответа инструмента, не занимает слот хода. Вызовы инструментов выполняются в среде выполнения и обращаются к серверам MCP по HTTP или stdio. Если задано spawn(parallelToolCalls: true), модель может запросить несколько инструментов в одном ответе, и они будут запущены параллельно. По умолчанию эта возможность выключена: Llama 3.x выполняет один вызов на ответ
Допуск с учётом нагрузки движкаДвижок принимает ходы, пока сообщает об отсутствии очереди ожидания (метрики SGLang и vLLM /metrics). Когда очередь появляется, luftpause ограничивает число ходов числом запросов, которые выполняет движок, а остальные удерживает. Движки, не сообщающие о нагрузке, не ограничиваются
Строгая привязка запусковЗапуск, который генерирует ответ или приостановлен в ожидании инструмента, допускается только к той реплике, которая обслуживала его последний ход и кэширует его префикс, даже если на другой реплике есть свободные ресурсы. Запуск может перейти на другую реплику, если его текущая реплика неисправна или он простаивает. Результаты измерений приведены ниже: именно эта привязка обеспечивает прирост интерактивной производительности. Оценка объёма памяти KV поверх неё снижала пропускную способность, поэтому от неё отказались
Классы обслуживанияХоды пакетной обработки оставляют на движке ресурсы для каждого интерактивного запуска, приостановленного на этом движке: такой запуск возобновится. Ходы, удерживаемые luftpause, отправляются сначала классу interactive, затем распределяются по справедливости между вызывающими сторонами и после этого — в порядке поступления. Они ожидают, пока не будут размещены или пока клиент не отключится
Маршрутизаторы как бэкендыРеплика может быть маршрутизатором (например, маршрутизатором SGLang), который выбирает движок. Её нагрузка определяется по движкам за ней (LoadFrom)
Маршрутизация по моделиЭндпоинты указывают обслуживаемые модели; ход направляется только на реплику, обслуживающую соответствующую модель. Для неизвестной модели сразу возвращается ошибка 404
Размещённые эндпоинтыЛюбой API, совместимый с OpenAI (vLLM на RunPod, OpenAI, OpenRouter и другие), можно использовать как реплику: ключ API берётся из переменной окружения, путь проверки состояния задаётся для каждого эндпоинта отдельно, поддерживается https
Перезапуск планировщикаПлоскость передачи данных хранит сведения о последней реплике, этапе, классе и вызывающей стороне для каждого запуска (простаивающие запуски удаляются через 30 минут), а также сведения обо всех выполняемых ходах. При запуске и после каждого переподключения она передаёт их планировщику (Resync) до возобновления потока событий. Поэтому привязка, резерв ресурсов для приостановленных интерактивных запусков и подсчёт ёмкости сохраняются при перезапуске планировщика. Ходы, удержанные во время сбоя, после восстановления планировщика снова допускаются к выполнению с теми же идентификаторами
Проверка состоянияПуть /health (или другой путь, заданный для конкретного эндпоинта) проверяется каждые 2 секунды; неисправные реплики не получают ходы
Надёжность вызовов инструментовАргументы приводятся к скалярным типам, заданным в схеме, проверяются и при ошибке отклоняются с указанием ожидаемой структуры вместо молчаливого принятия некорректного вызова. Вызовы, переданные в виде текста, восстанавливаются
Идентификация запускаИспользуется x-luftpause-run-id или значения tool_call_id, которые агент возвращает в ответе
Однопроходные движкиPOST /v1/decide (формат vLLM /classify) выполняется на отдельных репликах вне состояния запусков. Для вызова decide с заданными вызывающей стороной labels вторая модель не нужна: запрос выполняется как один интерактивный ход на генеративной реплике
ТрассировкаЕсли задана переменная OTEL_EXPORTER_OTLP_ENDPOINT, каждый сервис передаёт трассировочные интервалы OpenTelemetry по OTLP (соглашения GenAI): среда выполнения создаёт интервал invoke_agent для каждого запуска (дочерний интервал родительского запуска или traceparent MCP-запроса, вызвавшего spawn), интервал chat для каждого хода модели с числом токенов и причиной завершения, а также интервал execute_tool для каждого вызова инструмента; плоскость передачи данных создаёт интервал для каждого проксируемого хода; планировщик создаёт интервал admit с атрибутами luftpause.replica и luftpause.held_ms. traceparent по стандарту W3C передаётся плоскости передачи данных, планировщику, движкам и HTTP-серверам инструментов, а события ходов содержат идентификатор трассировки. Если переменная не задана, данные не передаются
АутентификацияЕсли задана переменная LUFTPAUSE_TOKEN, для всех маршрутов /v1 плоскости передачи данных, а также для /mcp и GET /status среды выполнения требуется заголовок Authorization: Bearer (сравнение выполняется за постоянное время); в противном случае возвращается ошибка 401 в формате JSON. Среда выполнения отправляет тот же токен Bearer плоскости передачи данных; заголовок Authorization клиента не передаётся дальше, а движки получают настроенный для них ключ. Если переменная не задана, оба сервиса выводят предупреждение в журнал и принимают запросы от всех клиентов

Семантика ошибок

Что происходит с запросом, проходящим через dataplane (/v1/chat/completions, /v1/decide),

если что-то идёт не так. Тела ошибок имеют формат {"error": {"message": "..."}}; во всех ответах, кроме случая недоступности планировщика, присутствует заголовок x-luftpause-run-id. При сбое ход сразу освобождает свой слот, а запуск переходит в состояние ожидания, а не остаётся приостановленным на вызове инструмента. Поэтому повторная попытка допускается наравне с любым следующим ходом этого запуска. Повторный запрос с тем же x-luftpause-run-id сохраняет привязку запуска и его положение в очереди справедливого распределения ресурсов.

СлучайЧто видит клиентЧто делает luftpauseЧто следует делать агенту с собственным циклом
Клиент отключается, пока его ход ожидает выполненияничего (клиент отключился)отменяет вызов планировщика; ход покидает очередь ожидания (TurnAbandoned), запрос к движку не отправляется. Если в этот момент ход уже направлялся к движку, он помечается как неудачный, а его слот освобождаетсяесли клиент прервал ожидание из-за тайм-аута на своей стороне, отправить ход повторно с тем же ID запуска: он ещё не выполнялся
Клиент отключается во время потоковой передачиничего (клиент отключился)закрывает запрос к движку, останавливая генерацию, и помечает ход как неудачныйотправить ход повторно с тем же ID запуска; частичный ответ нигде не сохраняется
Движок недоступен502, engine : ; сразу при отклонении соединения или по истечении системного тайм-аута подключения, если узел не отвечаетсообщает о начале и неудачном завершении хода; реплика остаётся в ротации, пока проверка состояния не обнаружит сбой (период — 2 с)немного подождать и повторить запрос с тем же ID запуска; до тех пор, пока проверка не пометит реплику как недоступную, повторный запрос может попасть на неё же
Сбой движка во время потоковой передачи200, уже отправленные фрагменты, затем закрытие соединения без последнего фрагмента или [DONE]сообщает о неудачном завершении ходасчитать ответ потерянным, отбросить его и повторить весь ход с тем же ID запуска
Движок сразу возвращает ошибку (429, 5xx)статус и тело ответа движка без изменений, включая его заголовок Retry-Afterсообщает о начале и неудачном завершении ходапри 429 подождать время, указанное в Retry-After (если заголовка нет — около 1 с), и повторить запрос с тем же ID запуска; при 5xx повторить несколько раз с увеличивающейся задержкой, затем прекратить попытки
Неизвестная модель404, no Generative replica serves model ''отклоняет запрос до постановки в очередь; запрос к движку не отправляется, ход не регистрируетсяпрекратить попытки и исправить имя модели или конфигурацию конечной точки; повторный запрос вернёт тот же ответ
Планировщик недоступен или перезапускаетсяничего: ход остаётся в ожидании, как если бы он был поставлен в очередь, пока планировщик не ответит или клиент не отключитсякаждую секунду повторяет попытку допуска с тем же ID хода; после восстановления соединения синхронизирует запуски и выполняющиеся ходы, прежде чем снова допускать запросы; до этого запросы к движку не отправляютсяничего не делать; если тайм-аут на стороне клиента короче периода сбоя, ситуация аналогична любому ожидающему ходу: повторить запрос с тем же ID запуска

Константы

В luftpause используется немного фиксированных значений. Ограничений на время ожидания в очереди и на число ходов нет: ожидающий ход ждёт так же, как в собственной очереди движка, а запуск завершается, когда исчерпан бюджет вызовов инструментов. Остальные параметры:

КонстантаЗначениеНазначениеГде задана
Длина ответа256 токенов на ход; задаётся для каждого запуска через spawn(maxTokens), значение 0 передаёт ограничение на усмотрение движкаSGLang резервирует память для самого длинного ответа, который может выдать запрос. Без ограничения движок обрабатывал в батче в семь раз меньше запросов (6 против 42 запусков в минуту на тестовом стенде), а слишком длинные ответы нарушали формат Llama «один вызов на ответ»runtime/…/LuftpauseTools.cs
Бюджет вызовов инструментов8 на запуск; задаётся для каждого запуска через spawn(maxToolCalls)После исчерпания бюджета модель больше не получает инструменты и должна сформировать ответ. Это также ограничивает число ходов запуска значением «бюджет + 1»runtime/…/LuftpauseTools.cs
Тайм-аут await60 с по умолчанию, максимум 600 сВремя ожидания одного вызова await до возврата текущего состояния запусков. Значение 60 с не превышает собственный лимит клиентов MCP на один запрос (у Claude Code по HTTP он составляет 60 с)runtime/…/LuftpauseTools.cs
Тайм-аут хода10 минЕсли завершение хода так и не было зарегистрировано (например, dataplane аварийно завершил работу в середине хода), ход удаляется, а его слот освобождаетсяscheduler/…/Expiry.cs
Тайм-аут бездействия запуска30 минЕсли в течение этого времени у запуска не было ходов, он удаляется; следующий ход запускается без привязки. Runtime сообщает о завершении своих субагентов. Агенты с собственным циклом сами не сообщают о завершении, поэтому для их состояния требуется сигнал окончанияscheduler/…/Expiry.cs
Период проверки2 сСостояние и загрузка движка (/metrics) проверяются с этой периодичностью; допуск запросов учитывает загрузку движка с такой задержкой. Это также тайм-аут проверкиscheduler/…/Probing.cs
Интервал системного таймера1 сЗапускает два описанных выше механизма удаления по тайм-аутуscheduler/…/Clock.cs
Тайм-аут от runtime до dataplane10 минМаксимальное время выполнения одного хода, включая ожидание в очереди, до сбоя запуска; совпадает со значением по умолчанию для клиентов OpenAI и Anthropicruntime/…/Program.cs
Задержка повтора при 429Значение из ответа Retry-After, иначе 1 сRuntime повторяет ход, отклонённый движком с кодом 429 (ограничение частоты запросов в hosted API), пока запуск не будет отменёнruntime/…/LlmClient.cs
Тайм-аут подключения к движку2 сПо истечении этого времени dataplane прекращает попытки подключиться к движкуdataplane/…/main.rs
Задержка переподключения для отправки событий1 сПосле этой паузы dataplane заново подключается к потоку событий планировщикаdataplane/…/report.rs
Память ID вызовов инструментов30 мин; очистка каждые 60 сВремя, в течение которого dataplane может распознать запуск по значениям tool_call_id, которые агент возвращает в ответах (для агентов, не передающих заголовок запуска)dataplane/…/run_identity.rs
Журнал запусковRuntime:StateDir, по умолчанию state, в контейнере — /var/lib/luftpauseРасположение файла runs.jsonl; это единственное состояние, которое runtime сохраняет между перезапускамиruntime/…/Program.cs
Заголовок вызывающей стороныx-luftpause-caller, ограничен 128 байтамиИдентифицирует вызывающую сторону для справедливого распределения ресурсов; если заголовок отсутствует, все запросы считаются запросами одного анонимного клиентаdataplane/…/proxy.rs

Запуск

docker compose up -d --build                     # scheduler, dataplane, runtime, fake engines, fake MCP tools
dotnet run --project tools/loadgen -- --spawn 10 # fan 10 sub-agents out over MCP and await them
dotnet run --project tools/loadgen -- --agents 8 --duration-s 60 --class batch --caller me # 8 own-loop agents through the proxy, closed loop
tools/check.sh                                   # both .NET test projects, loadgen, cargo test (when cargo is installed), image builds

Выпуск версии: tools/release.sh vX.Y.Z собирает, помечает тегами и отправляет три образа в ghcr.io/luftpause (переменная LUFTPAUSE_REGISTRY позволяет изменить реестр), а также создаёт тег git. При DRY_RUN=1 команда только выводит команды, не выполняя их.

Для запуска с токеном: LUFTPAUSE_TOKEN= docker compose up -d, затем claude mcp add --transport http luftpause http://localhost:7000/mcp --header "Authorization: Bearer ".

Ollama на хосте в качестве единственной реплики (модель задаётся через OLLAMA_MODEL; по умолчанию — llama3.2:1b):

docker compose -f compose.yaml -f compose.ollama.yaml up -d scheduler dataplane runtime

Две реплики SGLang с Llama-3.2-1B FP8 на одной GPU (конфигурация тестового стенда, использованного ниже):

docker compose -f compose.yaml -f compose.testbed.yaml up -d --build scheduler dataplane runtime fake-mcp sglang-a sglang-b

Чтобы просматривать трейсы в Jaeger, добавьте compose.otel.yaml. Этот файл запускает jaegertracing/all-in-one и направляет OTEL_EXPORTER_OTLP_ENDPOINT трёх сервисов на Jaeger. Если тестовый стенд передаёт traceparent в запросах MCP, в том же трейсе будут видны его субагенты, их ходы и решение планировщика о допуске запросов. Интерфейс доступен по адресу http://localhost:16686.

docker compose -f compose.yaml -f compose.otel.yaml up -d --build

Hosted pod с vLLM в качестве дополнительной реплики (ключ и ID pod задаются через переменные окружения):

docker compose -f compose.yaml -f compose.testbed.yaml -f compose.runpod.yaml up -d scheduler runtime">

RUNPOD_POD_ID=<pod-id> RUNPOD_KEY=<key> docker compose -f compose.yaml -f compose.testbed.yaml -f compose.runpod.yaml up -d scheduler runtime

Замеры

Один ноутбук с RTX 4060 Laptop 8 GB и двумя репликами SGLang на базе Llama-3.2-1B-Instruct FP8,

если не указано иное. Число повторов указано для каждой таблицы; различия в пределах разброса считаются шумом. Данные для таблиц с высокой нагрузкой получены с помощью tools/bench/flood.sh (субагенты через MCP) и tools/bench/proxy-flood.sh (агенты с собственным циклом). Каждый скрипт принимает ревизии git для сравнения и собирает их в отдельные образы с соответствующими тегами.

Параллельный запуск: spawn × 16 + await all и запуск по одному субагенту (3 повтора)

16 субагентов, задачи объёмом около 2,8 тыс. токенов, длительность вызова инструмента lookup фиксирована на уровне 2 с, не более 3 вызовов для каждого субагента.

Время для всех 16По сравнению с последовательным запуском
последовательно (spawn, await, следующий запуск)120.5 с—
параллельный запуск в luftpause22.9 с5.3×
параллельный запуск напрямую через маршрутизатор SGLang21.5 с5.6×

Для каждого субагента параллельный запуск в luftpause и через маршрутизатор занимает одинаковое время (p50 — 15–22 с); разница в 1,4 с укладывается в разброс между запусками.

Интерактивные запуски при высокой пакетной нагрузке (2 повтора, результаты обоих)

48 пакетных запусков создают нагрузку на реплики, параллельно выполняются 4 интерактивных запуска, окно — 3 минуты, контекст — до 8000 символов, не более 3 вызовов инструментов на запуск, замкнутый цикл (завершённый запуск немедленно заменяется новым). В варианте без планировщика оба движка ставили запросы в очередь, поэтому пул был насыщен. Во всех строках SGLang запускается с параметром --enable-priority-scheduling; приоритеты не передаются.

Агенты с собственным циклом, base_url указывает на luftpause (tools/loadgen --agents):

Интерактивных запусков/минВремя интерактивного запуска p50Пакетных запусков/мин
напрямую к маршрутизатору SGLang, без учёта класса запросов6.0 / 6.738.5 с / 33.6 с83.7 / 82.3
luftpause, допуск по загрузке движка, запуски могут перемещаться (0afb28a)8.7 / 9.023.2 с / 25.9 с79.3 / 79.0
luftpause, допуск по загрузке движка со строгой привязкой запусков к репликам12.0 / 13.318.5 с / 19.8 с75.7 / 76.0
luftpause, дополнительно учитывается оценка использования памяти KV (удалено)13.0 / 10.717.8 с / 20.8 с76.3 / 76.0

Субагенты через MCP (tools/loadgen --spawn):

Интерактивных запусков/минВремя интерактивного запуска p50Пакетных запусков/мин
среда выполнения напрямую обращается к маршрутизатору SGLang3.0 / 3.056.6 с / 59.5 с39.2 / 36.5
luftpause, допуск по загрузке движка, запуски могут перемещаться (0afb28a)4.0 / 5.0, 4.3 / 5.644.4 с / 36.7 с35.5 / 36.6
luftpause, допуск по загрузке движка со строгой привязкой запусков к репликам8.0 / 8.0 (а в тестовой ветке — 8.0 / 8.0, 7.3 / 9.3)29.2 с / 25.2 с36.2 / 38.5
luftpause, дополнительно учитывается оценка использования памяти KV (удалено)6.3 / 6.6, 6.0 / 7.329.7 с / 35.4 с35.5 / 35.5

Через прокси пропускная способность интерактивных запусков в 2 раза выше, чем у маршрутизатора без учёта класса запросов, а при работе через MCP — в 2,7 раза выше. При этом число пакетных запусков через прокси уменьшается на 9 %, а через MCP не меняется. Между строками различается только логика допуска. Если разрешить запускам перемещаться между репликами, 39 из 48 пакетных запусков меняли реплику во время выполнения и теряли кэш префикса. Весь прирост даёт закрепление генерирующего или приостановленного запуска за его репликой. Дополнительный учёт оценки использования памяти KV для каждого шага, сверенной с данными о пуле, которые сообщает движок, снижал пропускную способность интерактивных запусков через MCP примерно на одну пятую и никак не влиял на неё через прокси, поэтому эту оценку убрали. При небольшой нагрузке (контексты на 2000 символов, 24 пакетных запуска) движки не ставят запросы в очередь, и все варианты показывают одинаковый результат — около 14 интерактивных запусков в минуту.

Claude Code в роли клиента

Claude Code попросили параллельно выполнить шесть поисков по рекам. Он запустил шесть задач и дождался их завершения одним вызовом: весь поиск занял 12,5 с с учётом собственных обменов Claude Code, тогда как последовательное выполнение тех же задач заняло бы 36 с.

В новой сессии ему дали только задачу «исследовать эти шесть рек с помощью локальных субагентов». Claude Code вывел список инструментов субагентов, ограничил запуски инструментами Wikipedia, запустил шесть задач, дождался их завершения одним вызовом и повторно запустил задачи, ответы на которые оказались недостаточно точными.

Инструменты: wikipedia-mcp через stdio.

СубагентыПовторные запуски ClaudeОтклонённые вызовы инструментовРезультат
локальная модель 1B, до приведения аргументов438 из 46ответы в основном основаны на знаниях модели
локальная модель 3B, с приведением аргументов20 из 20длины и устья указаны верно, списки стран ненадёжны
Llama 3.3 70B AWQ на поде RunPod vLLM00 из 6все шесть ответов получены из Wikipedia за один цикл (9,3 с)

Под на 70B находился в одном пуле с локальной репликой 3B: шаги с запросом к llama-3.3-70b отправлялись на под через https с его API key, а все остальные запросы оставались локальными. Поле failedToolCalls в await позволяет вызывающей стороне определить, что ответ был получен не с помощью инструментов.

Маршрутизация (N = 8 одновременно выполняющихся субагентов, 4 повтора)

Запусков/минТокены запроса, полученные из кэша
привязка luftpause30.1 ± 1.744.6 %
маршрутизатор SGLang, cache_aware30.1 ± 0.544.6 %
маршрутизатор SGLang, round_robin28.2 ± 0.525.7 %

Привязка даёт такой же результат, как маршрутизация SGLang с учётом кэша, и превосходит round_robin на 7–15 % при умеренной нагрузке. Когда одновременно выполняется 16–24 запуска, кэши постоянно обновляются, и выбор политики перестаёт влиять на результат.

Однопроходная обработка (классификатор vLLM, RoBERTa-base)

luftpause обслуживает два типа моделей в одном пуле. Генеративный движок (SGLang, vLLM, размещённый API) выполняет шаги агента: он генерирует токены один за другим, хранит KV-кэш для каждого запуска, а шаг может завершиться вызовом инструментов и приостановкой запуска. Однопроходный движок отвечает за один прямой проход: энкодер или классификатор в стиле RoBERTa, работающий через /classify в vLLM, возвращает типизированные метки с вероятностями, без генерации, потока данных и сохранения состояния между вызовами. Агенты используют такой движок для задач, которым не нужен сгенерированный ответ: например, для маршрутизации запроса, оценки результата работы инструмента или выбора следующего шага. Это занимает несколько миллисекунд вместо генеративного шага. Субагенты обращаются к нему через инструмент MCP decide, а агенты с собственным циклом — через POST /v1/decide. Однопроходные шаги направляются на отдельные реплики и не входят в состояние запусков, поэтому не занимают генеративные слоты и не ждут их освобождения. Если набор меток не входит в обучающие данные классификатора, вызов decide(labels: [...]) оценивает метки по логарифмическим вероятностям первых токенов генеративной реплики. Для этого требуется один шаг генерации длиной в один токен.

На тестовом стенде с моделью 1B система относит фразу «сервер трижды вернул ошибку 500, а дежурный телефон звонит» к метке urgent с вероятностью 0.89, а вопрос о населении — к lookup с вероятностью 0.99. Но фразы «2 + 2» и «спасибо, на этом всё» она тоже относит к lookup: механизм работает, но модель 1B склонна выбирать первую метку. Используйте более крупную модель или калибруйте оценки.

НагрузкаЗадержка на уровне плоскости данных
один вызов за разp50 20.6 мс
20 одновременно работающих агентовp50 85 мс, p99 180 мс

Лицензия

Business Source License 1.1, переходящая в Apache License 2.0 через четыре года после публикации каждой версии. На практике это означает, что проект можно бесплатно использовать, изменять, включать в свои продукты и использовать в своей компании, а также распространять — без требований copyleft к собственному коду. До перехода лицензии запрещено только одно: предоставлять сам luftpause третьим лицам в виде размещённого или управляемого сервиса. Таким образом, исходный код доступен, но проект не является открытым ПО по определению OSI, и мы прямо об этом говорим. Причина типична для инфраструктурных проектов: небольшая команда не может конкурировать с облачным провайдером, который оборачивает проект в консоль и продаёт его как сервис. Такой исход приведёт к закрытию проекта, а не к его развитию. Дата перехода отвечает на вопрос «станет ли проект когда-нибудь открытым ПО»: да, для каждой версии это произойдёт в дату, указанную в лицензии. Если до этого срока ограничение мешает вам что-либо сделать, создайте issue — разрешение для отдельного случая получить несложно. Вклады принимаются на тех же условиях (см. CONTRIBUTING.md).