Воркфлоу

🔵 Воркфлоу — декларативные многошаговые пайплайны поверх агентов и инструментов. Каждый воркфлоу — 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: [ ... ]

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 Примечания

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 — только с незавершённых элементов (выполненные элементы берутся из журнала и не переигрываются).

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 }

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 }      # опц. значения по умолчанию