Воркфлоу
🔵 Воркфлоу — декларативные многошаговые пайплайны поверх агентов и
инструментов. Каждый воркфлоу — YAML-файл в config/workflows/, описывающий
упорядоченный список шагов, работающих с накапливающимся скоупом переменных
(payload input плюс output_key каждого шага).
Шаги hot-reload'ятся; запуск — POST /v1/workflows/<name>/run.
12.1 Типы шагов
type |
Назначение |
|---|---|
agent |
запустить шаблон (или inline instructions) с шаблонизированным prompt; опционально — структурированный результат через output_schema |
tool |
вызвать инструмент напрямую с шаблонизированными args |
saga |
упорядоченная группа инструментов с компенсациями: при падении шага выполненные откатываются в обратном порядке |
condition |
ветвление на id шага по шаблонизированному булевому выражению |
for_each |
итерация по коллекции с вложенным списком шагов на каждый элемент |
loop |
повторить вложенный блок до выполнения until (или max_iterations) — внутренний цикл агента |
verify |
проверить условия (инструмент и/или агент-судья) → { passed, checks }, без падения шага |
Шаблоны — MiniJinja ({{ var }}, {% if %}), рендерятся от скоупа переменных;
{{ x | tojson }} встраивает структурированные значения, env_var('NAME')
читает переменную окружения.
12.2 Пример
name: triage
description: Просмотреть входящие коммиты и завести тикеты.
steps:
- id: fetch
type: tool
tool: git_commits
args: { since: "{{ input.since | default('yesterday') }}" }
output_key: commits
- id: process
type: for_each
over: commits.items # dot-path в скоуп переменных → массив
item: commit # переменная цикла
concurrency: 3
max_items: 100
steps:
- id: analyze
type: agent
template: review
prompt: "Отревьюй {{ commit.sha }}: {{ commit.message }}"
output_key: review
output_schema:
type: object
properties:
needs_action: { type: boolean }
summary: { type: string }
required: [needs_action, summary]
retry: { max: 2, backoff_secs: 5 }
timeout_secs: 120
- id: branch
type: condition
when: "{{ review.needs_action }}"
then: file_ticket
- id: file_ticket
type: tool
tool: send_report
args: { body: "{{ review.summary }}" }
collect:
reviews: "{{ results | tojson }}"
- id: done
type: tool
tool: send_report
args: { body: "{{ reviews }}" }
12.3 Справочник шагов
agent
| Поле | Описание |
|---|---|
template |
шаблон для запуска (взаимоисключается с instructions) |
instructions |
inline-системный промпт без шаблона |
tools |
доп. имена инструментов (сливаются с шаблонными) |
prompt |
сообщение пользователя (MiniJinja) |
output_key |
куда сохранить результат в скоуп |
output_schema |
JSON Schema — агент завершается через job_done; проверенный payload становится значением output_key |
session |
ключ стейтфул-сессии (шаги с одним ключом в одном запуске делят контекст) |
retry |
{ max, backoff_secs } — ретраи всего шага при ошибке |
timeout_secs |
лимит хода по времени |
budget |
лимит токенов { max_per_turn, max_per_minute, max_total }; для inline instructions заменяет дефолтный лимит (4000 / 50000 / 200000); для template игнорируется |
next |
явный id следующего шага (см. §12.9) |
Без output_schema вывод шага — текст финального ответа агента. С ним шаг
падает, если агент не вернул валидный структурированный результат.
tool
| Поле | Описание |
|---|---|
tool |
имя зарегистрированного инструмента |
args |
аргументы (строковые листья — MiniJinja; лист, рендерящийся в JSON, передаётся структурой) |
output_key |
куда сохранить результат — data инструмента, если есть, иначе content |
idempotency_key |
идемпотентный ключ для сайд-эффектных тулов (см. idempotency: в 05-tools-core) |
next |
явный id следующего шага |
condition
| Поле | Описание |
|---|---|
when |
MiniJinja-выражение → true/false |
then |
id шага при истинности |
else |
id шага при ложности |
for_each
| Поле | Описание |
|---|---|
over |
dot-path в скоуп → массив (напр. commits.items) |
item |
имя переменной цикла (по умолчанию item) |
concurrency |
максимум параллельных итераций (по умолчанию 1) |
max_items |
жёсткий лимит элементов |
steps |
вложенный под-воркфлоу, запускается на каждый элемент |
collect |
карта имя → шаблон, сворачивающая per-item результаты (доступны как results) обратно в родительский скоуп |
next |
явный id следующего шага |
Каждая итерация работает с копией родительского скоупа + item. Её выводы
становятся per-item результатом в results (массив), который collect может
переформатировать.
saga
Упорядоченная группа сайд-эффектных инструментов с компенсациями: каждый
forward-шаг может объявить compensate, и падение шага откатывает уже
выполненные шаги в обратном порядке. Шаги персистятся в durable outbox, поэтому
зависшая сага переживает рестарт.
Значение шага — массив результатов forward-шагов.
| Поле | Описание |
|---|---|
saga_id |
id саги (по умолчанию — id шага); ключ durable-состояния |
steps |
список { tool, args, idempotency_key?, max_attempts?, compensate? } |
steps[].compensate |
{ tool, args } — компенсирующий вызов при откате |
output_key |
куда положить массив результатов |
next |
явный id следующего шага |
steps:
- id: checkout
type: saga
saga_id: order-{{ input.order_id }}
output_key: checkout
steps:
- tool: reserve_stock
args: { sku: "{{ input.sku }}", qty: 1 }
compensate: { tool: release_stock, args: { sku: "{{ input.sku }}", qty: 1 } }
- tool: charge_card
args: { amount: "{{ input.amount }}", key: "{{ input.order_id }}" }
При падении charge_card ядро вызовет release_stock и пометит шаг
compensated; шаг саги завершится ошибкой (saga '…' failed and was rolled back).
loop
Внутренний цикл: повторяет body до тех пор, пока until не отрендерится в
true, но не более max_iterations.
| Поле | Описание |
|---|---|
max_iterations |
жёсткий лимит итераций (обязательно) |
body |
вложенный список шагов, исполняется каждую итерацию |
until |
MiniJinja-выражение → true/false; true останавливает цикл (успех) |
on_max |
что делать при исчерпании max_iterations: fail (по умолчанию) или success |
iteration |
имя переменной-счётчика (0-based), по умолчанию iteration |
output_key |
куда положить { iterations, stopped, max_iterations } |
next |
явный id следующего шага |
Выводы body накапливаются в скоупе цикла и переносятся в родительский скоуп
после завершения (поэтому until после каждой итерации видит свежий
verification).
steps:
- id: fix
type: loop
max_iterations: 8
until: "{{ verification.passed }}"
on_max: fail
body:
- id: act
type: agent
template: bugfix-agent
prompt: "Исправь дефект (попытка {{ iteration }}): {{ input.summary }}"
output_key: attempt
- id: check
type: verify
tool: run_tests
args: { cmd: "cargo test" }
output_key: verification
verify
Проверка одного или нескольких условий. В отличие от остальных шагов, неуспех
проверки — не ошибка шага: возвращается { "passed": false, "checks": [...] },
чтобы на него можно было ветвиться (например, в until цикла).
| Поле | Описание |
|---|---|
checks |
список проверок: { type: tool, tool, args, expect } или { type: agent, template/instructions, prompt, output_schema } |
require |
all (по умолчанию) или any |
output_key |
куда положить результат { passed, checks } |
next |
явный id следующего шага |
Для проверки-инструмента expect описывает, как прочитать результат: без
expect берётся success инструмента; иначе — pointer (JSON Pointer в data,
иначе распарсенный content) плюс equals/truthy. Проверка-агент обязана
вернуть структурированный { "passed": bool }.
- id: v
type: verify
require: all
output_key: verification
checks:
- type: tool
tool: run_tests
args: { cmd: "cargo test" }
expect: { pointer: /passed, equals: true }
- type: agent
template: reviewer
prompt: "Проверь изменения: {{ diff }}"
output_schema:
type: object
properties: { passed: { type: boolean }, reason: { type: string } }
required: [passed]
12.4 Контроль доступа
Воркфлоу может объявить шаблоны, которые трогает, — тогда скоупед-оператор запустит его только если эти шаблоны в его скоупе:
name: reports
templates: [rss-agent, summarizer] # шаблоны, которые воркфлоу может трогать
steps: [ ... ]
Scope::All(полный админ) запускает любой воркфлоу.- Скоупед-оператор (
templates: [...]вauth.admin_tokens) запускает воркфлоу только если все егоtemplatesв его скоупе, и видит только такие воркфлоу. - Воркфлоу без
templates— только для админа.
run / get_run возвращают 403 для скоупед-оператора вне его скоупа; list
их не показывает.
12.5 Триггеры
Воркфлоу может стартовать без явного API-вызова:
name: reports
triggers:
cron:
- schedule: "0 0 9 * * *" # cron: sec min hour dom month dow (UTC)
input: { topic: daily } # фиксированный input (по умолчанию {})
webhook:
token_env: WF_REPORTS_TOKEN # опциональный bearer (рекомендуется)
steps: [ ... ]
| Триггер | Описание |
|---|---|
cron[].schedule |
cron-выражение (6 полей, секундное разрешение) |
cron[].input |
фиксированный input на запуск |
webhook |
включает POST /v1/workflows/<name>/webhook; JSON-тело становится input |
webhook.token_env |
env с bearer-токеном; не задан/пуст → все запросы отклоняются |
12.6 HTTP API
| Endpoint | Auth | Описание |
|---|---|---|
GET /v1/workflows |
админ | список воркфлоу + последние запуски (?runs=N) |
POST /v1/workflows/<name>/run |
админ | запустить; JSON-тело становится input. Возвращает { ok, id } |
POST /v1/workflows/<name>/webhook |
свой токен | запустить с webhook (JSON-тело → input) |
GET /v1/workflows/runs/<id> |
админ | статус запуска + история шагов |
POST /v1/workflows/runs/<id>/cancel |
админ | остановить запуск на следующей границе шага (статус cancelled, журнал снимается) |
GET /workflows |
админ | HTML-страница (запуск + просмотр) |
Запуски пишутся в logs/workflows.db (workflow_runs); статус running →
success / error, с per-step записями (id, status, output, error, retries,
timings). Состояние между рестартами не персистится — только история запусков.
12.7 Запуск из агентов и инструментов
Воркфлоу можно запустить из другого агента через workflow_tool:
kind: workflow_tool
name: run_triage
description: Запустить воркфлоу триажа.
workflow: triage # дефолтный воркфлоу (перекрывается аргументом `workflow`)
Аргументы: workflow (имя, опционально при конфиге) и input (объект).
Инструмент возвращает run id; запуск продолжается асинхронно.
12.8 Локальное тестирование
agent-os module workflow run config/workflows/smoke.yaml '{"items":["Cargo.toml","README.md"]}'
# или input из файла (удобно на Windows):
agent-os module workflow run config/workflows/agent_demo.yaml --args-file args.json
tool-шаги работают со встроенными + YAML-инструментами из config/agents/;
agent-шаги используют провайдер из config/server.yaml (или
DEEPSEEK_API_KEY). Запускай из корня репозитория, чтобы config/ резолвился.
12.9 Примечания
- Поток управления. Шаги идут по порядку;
conditionпрыгает на idthen/else, выполнение продолжается оттуда. Шаг сnextпрыгает на него вместо следующего по списку — используйnext, чтобы закрытьif-ветку и пропуститьelse. Прыжки защищены от бесконечных циклов. for_eachпо пустому массиву — no-op (шаг успешен с[]).- Упавший шаг валит весь запуск;
retryприменяется к шагу до этого. - Стейтфул-сессии. Дай двум и более
agent-шагам один ключsession— они поделят один разговор: первый создаёт сессию (лениво), следующие её переиспользуют. Сессия закрывается, когда заканчивается охватывающий скоуп. Каждая итерацияfor_eachполучает свою сессию на ключ. output_schemaшагаagentпереиспользует тот же контракт структурированного завершения, что и агенты (см. §4): схема заменяет параметрыjob_done, а проверенные аргументы становятся результатом.
12.10 Долговечность (durable)
По умолчанию состояние запуска живёт только в памяти: при рестарте незавершённый
запуск теряется (остаётся running в истории). Флаг durable: true включает
журнал запуска — движок коммитит scope и позицию на каждой границе шага
(для loop — на каждой итерации), а на старте сервера недоделанные запуски
перезапускаются с последней границы (logs/workflows.db, таблица
workflow_run_state).
name: long-harness
durable: true
budget: # опциональный бюджет на весь запуск
max_per_turn: 8000
max_per_minute: 60000
max_total: 500000
steps: [ ... ]
Семантика — at-least-once (как у durable-сессий и outbox): незавершённый шаг
или итерация переигрываются целиком, поэтому сайд-эффекты шагов должны быть
идемпотентны (поле idempotency_key у tool-шага или saga). Вложенные шаги
внутри loop доигрываются с начала текущей итерации, а for_each — только с
незавершённых элементов (выполненные элементы берутся из журнала и не
переигрываются).
budgetзадаёт лимит общей группыwf:<run_id>, которую используют все агент-шаги запуска; её потраченный итог входит в журнал, поэтому после перезапуска лимит не сбрасывается.durable-запуск получает такую группу всегда (без лимита, еслиbudgetне задан) — для учёта стоимости (токенов).- Идемпотентность. У
durable-запускаtool-шаг без явногоidempotency_keyполучает стабильный ключwf:<run_id>:<шаг>:<итерация>: при переигрывании границы повторный сайд-эффект дедуплицируется side-effect-хранилищем (нуженlogging.side_effect_db_path/outbox — иначе дедуп живёт только в памяти). Явныйidempotency_keyимеет приоритет. - Дедлайн запуска.
timeout_secsограничивает время всего запуска: на границе шага запуск останавливается со статусомtimeout. Абсолютный дедлайн восстанавливается из времени старта в журнале, поэтому перезапуск не продлевает лимит. - Запуск, превысивший лимит попыток восстановления (3), помечается
error(«resume limit exceeded») и больше не переигрывается — это карантин «ядовитого» запуска. - Требует включённого
logging.workflow_db_path(по умолчаниюlogs/workflows.db).
12.11 Харнесс (изолированный workspace)
Блок harness: превращает воркфлоу во «внутренний харнесс» — durable-запуск с
циклом (loop/verify) в изолированной рабочей директории. Пример: починка
дефекта из тикета.
name: jira-bugfix
durable: true
harness:
workspace:
kind: git_worktree # или dir
repo: "."
base: origin/main
cleanup: on_success # on_success (по умолчанию) | always | never
triggers:
webhook: { token_env: JIRA_HOOK_TOKEN }
steps:
- id: fix
type: loop
max_iterations: 8
until: "{{ verification.passed }}"
body:
- { id: act, type: agent, template: bugfix-agent,
prompt: "Исправь: {{ input.summary }}", session: fix }
- { id: check, type: verify, tool: run_tests,
args: { cmd: "cargo test" }, output_key: verification }
- Движок на старте готовит workspace и кладёт в скоуп переменную
workspace({ path, kind, branch, repo }) — шаги адресуют её как{{ workspace.path }}. git_worktree: отдельная веткаharness/<run_id>отbase; правки агента изолированы и видны как diff, ветка остаётся после очистки как артефакт.cleanup:on_successудаляет при успехе,always— всегда,never— сохраняет для разбора.- Workspace привязан к durable-журналу: после рестарта запуск переподключает
ту же директорию/ворктри (
prepareидемпотентен). - Workspace-инструменты. Путь рабочей директории передаётся агент-шагам как
template_vars[code_root], поэтому инструменты модуляcode(read_file,write_file,edit_file,list_files,grep,run_command) работают внутри изолированного workspace, а не в cwd сервера. - Артефакты: перед очисткой движок собирает из workspace
git status,git diff --statиgit diff(для git-ворктри) и кладёт в read-model;/harnessпоказывает diff запуска. - Наблюдаемость: страница
/harnessи APIGET /v1/harness/runs,GET /v1/harness/runs/:id(модульharness,logs/harness.db). Деталь запуска показывает таймлайн шагов/итераций (из событий workflow), стоимость (токены), число итераций, workspace и артефакты. - Сводка:
GET /v1/harness/summary?window_secs=— success/failure rate, средние итерации/длительность, суммарные токены и глубина очереди work items; на странице выводится полосой карточек. - Метрики на
GET /metrics:harness_runs_<status>_total,harness_run_duration_ms,harness_run_iterations,harness_run_tokens_total,harness_work_items_<status>(глубина очереди).
12.12 Work items
Work item — нормализованная единица работы харнесса (тикет, платёж, инцидент).
Дедуплицируется по (source, external_id); жизненный цикл:
new → claimed → running → done | failed | escalated.
Источники (Jira/GitHub/почта/вебхук) шлют ingest — повторный ingest того же
(source, external_id) обновляет содержимое, но не сбрасывает статус:
POST /v1/harness/work-items
{ "source": "jira", "external_id": "PROJ-123",
"title": "Fix login", "body": "...", "priority": 2, "metadata": { } }
API (модуль harness, таблица harness_work_items):
| Метод | Назначение |
|---|---|
GET /v1/harness/work-items?status=&source= |
список (фильтры, свежие/приоритетные сверху) |
GET /v1/harness/work-items/:id |
один item |
POST .../:id/claim |
взять в работу (lease; второй claim проигрывает) |
POST .../:id/status |
ручная смена статуса оператором |
POST .../:id/run {workflow, input?} |
claim + запуск harness-воркфлоу; запуск линкуется к item |
По завершении связанного запуска статус item обновляется автоматически:
success → done, cancelled → new (возврат в очередь), иначе failed.
12.13 Реестр харнессов
Набор харнессов задаётся реестром — файлами config/harnesses/<id>.yaml.
Харнесс — это домен-агностичный внутренний цикл (loop/verify), не обязательно
про код; workspace опционален. Реестр связывает человеческое описание с
workflow'ом-исполнителем и источником работы.
id: jira-bugfix
title: Bugfix из тикета
description: |
Забирает дефект, воспроизводит, чинит в изолированном workspace,
прогоняет тесты и отдаёт на ревью.
workflow: jira-bugfix # workflow с блоком harness:
enabled: true
source:
kind: jira # какой источник кормит (* = любой)
auto_run: true # новый work item запускает харнесс сам
filter: { project: PROJ } # опц. равенство по metadata work item
defaults: { priority: 1 } # опц. значения по умолчанию
GET /v1/harness/harnesses— список определений (виден на/harness).- Маршрутизация: при ingest work item подбирается первый включённый
харнесс, чей
source.kind(иfilter) совпал; приauto_run: trueзапуск стартует сам, иначе — черезPOST /v1/harness/work-items/:id/run. - Грузится при старте из
config/harnesses/(каталог рядом сconfig/workflows/). - Operator-тулы для инженера (агент
admin, черезtool_call):harness_list,harness_toggle,harness_summary,harness_runs,harness_work_items,harness_ingest,harness_run_item— вconfig/agents/toolbox/tools/.