# 12. Воркфлоу

🔵 Воркфлоу — декларативные многошаговые пайплайны поверх агентов и
инструментов. Каждый воркфлоу — 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` |
| `condition` | ветвление на id шага по шаблонизированному булевому выражению |
| `for_each` | итерация по коллекции с вложенным списком шагов на каждый элемент |

Шаблоны — MiniJinja (`{{ var }}`, `{% if %}`), рендерятся от скоупа переменных;
`{{ x | tojson }}` встраивает структурированные значения, `env_var('NAME')`
читает переменную окружения.

## 12.2 Пример

```yaml
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` |
| `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` может
переформатировать.

## 12.4 Контроль доступа

Воркфлоу может объявить шаблоны, которые трогает, — тогда **скоупед-оператор**
запустит его только если эти шаблоны в его скоупе:

```yaml
name: reports
templates: [rss-agent, summarizer]   # шаблоны, которые воркфлоу может трогать
steps: [ ... ]
```

- `Scope::All` (полный админ) запускает любой воркфлоу.
- Скоупед-оператор (`templates: [...]` в `auth.admin_tokens`) запускает воркфлоу
  только если все его `templates` в его скоупе, и видит только такие воркфлоу.
- Воркфлоу без `templates` — **только для админа**.

`run` / `get_run` возвращают `403` для скоупед-оператора вне его скоупа; `list`
их не показывает.

## 12.5 Триггеры

Воркфлоу может стартовать без явного API-вызова:

```yaml
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>` | админ | статус запуска + история шагов |
| `GET /workflows` | админ | HTML-страница (запуск + просмотр) |

Запуски пишутся в `logs/workflows.db` (`workflow_runs`); статус `running` →
`success` / `error`, с per-step записями (id, status, output, error, retries,
timings). Состояние между рестартами не персистится — только история запусков.

## 12.7 Запуск из агентов и инструментов

Воркфлоу можно запустить из другого агента через `workflow_tool`:

```yaml
kind: workflow_tool
name: run_triage
description: Запустить воркфлоу триажа.
workflow: triage            # дефолтный воркфлоу (перекрывается аргументом `workflow`)
```

Аргументы: `workflow` (имя, опционально при конфиге) и `input` (объект).
Инструмент возвращает run id; запуск продолжается асинхронно.

## 12.8 Локальное тестирование

```bash
workflow_tool config/workflows/smoke.yaml '{"items":["Cargo.toml","README.md"]}'
# или input из файла (удобно на Windows):
workflow_tool config/workflows/agent_demo.yaml --args-file args.json
```

`tool`-шаги работают со встроенными + YAML-инструментами из `config/agents/`;
`agent`-шаги используют провайдер из `config/server.yaml` (или
`DEEPSEEK_API_KEY`). Запускай из корня репозитория, чтобы `config/` резолвился.

## 12.9 Примечания

- **Поток управления.** Шаги идут по порядку; `condition` прыгает на id
  `then`/`else`, выполнение продолжается оттуда. Шаг с `next` прыгает на него
  вместо следующего по списку — используй `next`, чтобы закрыть `if`-ветку и
  пропустить `else`. Прыжки защищены от бесконечных циклов.
- `for_each` по пустому массиву — no-op (шаг успешен с `[]`).
- Упавший шаг валит весь запуск; `retry` применяется к шагу до этого.
- **Стейтфул-сессии.** Дай двум и более `agent`-шагам один ключ `session` — они
  поделят один разговор: первый создаёт сессию (лениво), следующие её
  переиспользуют. Сессия закрывается, когда заканчивается охватывающий скоуп.
  Каждая итерация `for_each` получает свою сессию на ключ.
- `output_schema` шага `agent` переиспользует тот же контракт структурированного
  завершения, что и агенты (см. §4): схема заменяет параметры `job_done`, а
  проверенные аргументы становятся результатом.
