Control Plane¶
Control Plane — координационное ядро платформы Taimen: он хранит авторитетное состояние работы (задачи, цели, claims, runs, approvals, артефакты) и журнал событий, по которому люди, агенты и сервисы синхронизируются между собой. Раздел адресован архитекторам, разработчикам интеграций и операторам, которым нужно понимать, как ядро устроено и как с ним работать через HTTP API.
Чего Control Plane не делает
Ядро не исполняет LLM- или агентную логику. Оно отвечает на вопросы «что нужно сделать», «кто сейчас этим владеет», «что произошло» и «можно ли это делать». Исполнение — задача харнессов и runner'ов (см. Агенты и runner).
Место в платформе¶
flowchart LR
subgraph clients["Клиенты"]
H["Человек<br/>(консоль, MCP-плагин)"]
A["Агент<br/>(runner, харнесс)"]
S["Сервисы<br/>(вертикальные пакеты, процессы)"]
end
subgraph cp["Control Plane"]
API["control-plane-api<br/>HTTP /api/v1"]
W["control-plane-worker<br/>фоновые циклы"]
CA["context-adapter<br/>журнал → память"]
DB[("PostgreSQL<br/>состояние + журнал")]
end
IAM["IAM<br/>(токены, bindings)"]
MEM["Memory Service"]
H & A & S -->|Bearer token| API
API --> DB
W --> DB
CA --> DB
CA -->|наблюдения| MEM
API -->|/context, knowledge| MEM
API -.->|JWKS, проверка токенов| IAM
Ключевой принцип: непрерывность работы живёт не в разговоре с моделью, а в
связке principal + workspace + task + run + checkpoints + artifacts + events.
Агент может упасть, смениться или передать работу человеку — всё, что нужно
для продолжения, остаётся в ядре.
Основные сущности¶
| Сущность | Что это | Статья |
|---|---|---|
| Tenant | Изолированное пространство данных; всё остальное принадлежит ровно одному tenant'у | Модель работы |
| Principal | Identity участника: human, agent или service. Человек и агент равноправны в протоколе |
Авторизация и права |
| Workspace | Узел иерархии, в которой живёт работа; бывает проектом, командой, потоком работ | Модель работы |
| Project | Профиль проекта, привязанный к workspace один-к-одному | Модель работы |
| Task (work item) | Единица работы с типом, статусом, полями, датами, связями | Модель работы |
| Task type | Версионируемый реестр: схема полей, lifecycle, исходы approval | Типы задач и статусы |
| Goal | Желаемое состояние, которому служит работа | Цели, приёмка и evidence |
| Session | Живое подключение клиента (lease + heartbeat) | Исполнение |
| Claim | Эксклюзивное арендованное владение задачей с fencing token | Исполнение |
| Run | Одна попытка исполнения задачи под claim | Исполнение |
| Approval | Решение человека или агента; gate блокирует задачу до решения | Approvals |
| Artifact | Append-only ссылка на результат работы | Артефакты и комментарии |
| Comment | Реплика в треде задачи с историей правок | Артефакты и комментарии |
| Event | Запись append-only журнала, основа аудита и синхронизации | События |
Различие сущностей исполнения стоит запомнить сразу:
Principal = identity (кто)
Session = live-контекст (живое подключение, lease + heartbeat)
Claim = exclusive ownership (арендованное владение задачей, fencing token)
Run = execution attempt (конкретная попытка; результат, артефакты)
Процессы¶
Один образ control-plane запускается тремя процессами. В корневом
compose.yml суперпроекта они входят в профиль core.
| Сервис compose | Команда | Назначение |
|---|---|---|
control-plane-api |
alembic upgrade head && uvicorn control_plane.main:app --port 8000 |
HTTP API /api/v1, WebSocket журнала, /health/*, /metrics, /openapi.json, /docs. Применяет миграции при старте |
control-plane-worker |
python -m control_plane.worker |
Фоновые циклы: доставка outbox, исполнение исходов approval, sweep истёкших sessions/claims, возврат просроченных lease вызовов skill, очистка ключей идемпотентности |
context-adapter |
python -m control_plane.worker.context_adapter |
Переносит журнал событий в Memory Service как наблюдения: per-tenant курсор, at-least-once, парковка «ядовитых» пакетов |
control-plane-db |
postgres:16-alpine |
Единственный источник истины: состояние, журнал, outbox, idempotency |
control-plane-api¶
Обработчик маршрута делает ровно четыре вещи: валидирует HTTP-контракт, получает контекст аутентификации из credentials, вызывает команду или запрос прикладного слоя и превращает результат в HTTP-ответ. Бизнес-правил в обработчиках нет; команды повторно проверяют права сами.
Каждая мутирующая команда выполняется в одной транзакции PostgreSQL:
состояние меняется в таблицах, в events пишется событие, в outbox —
запись для доставки, pg_notify будит подписчиков только после commit.
Откат не оставляет ничего — ни состояния, ни события.
control-plane-worker¶
Цикл воркера (интервал CP_WORKER_POLL_INTERVAL_SECONDS, по умолчанию 1 с)
выполняет подзадачи, каждую в своей транзакции:
- outbox — батчи по
FOR UPDATE SKIP LOCKED, ограниченные повторы с экспоненциальным backoff (CP_OUTBOX_*); в базовой поставке целевая точка доставки — структурированный лог; - исходы approval — исполнение действий, объявленных типом задачи (см. Approvals);
- sweep sessions и sweep claims — перевод истёкших аренд в
staleс событиямиsession.expired/claim.expired; - lease вызовов skill — возврат просроченных вызовов в очередь;
- GC идемпотентности — удаление истёкших ключей.
Воркер — оптимизация, а не условие корректности
Истёкшие claims реквизируются и самой командой захвата, а просроченные аренды отклоняются лениво любой командой, которая их встретила. Остановка воркера замедляет сходимость, но не ломает инварианты. Исключения — исходы approval и доставка outbox: без воркера они не исполняются.
Несколько воркеров могут работать параллельно: все выборки идут с
SKIP LOCKED.
context-adapter¶
Отдельный процесс-одиночка (второй экземпляр ждёт advisory lock, а не потребляет повторно). Читает журнал по надёжному курсору, переводит события в наблюдения по явному whitelist полей и отправляет их в память tenant'а; курсор двигается только после подтверждения доставки. Сбой одного tenant'а паркует только его строку. Недоступность памяти не влияет на API и воркер — события просто накапливаются. Подробнее — в Контекст задачи и память.
Как устроен вызов¶
Все endpoint'ы — под /api/v1, поля в JSON — camelCase. Полная схема
доступна по GET /openapi.json, интерактивная — /docs.
export CP=https://platform.example.com/api/v1
export TOKEN=<access-token> # см. IAM: токены, audiences, scopes
curl -s "$CP/tasks?limit=5" -H "Authorization: Bearer $TOKEN"
Общие правила протокола, на которые опираются все статьи раздела:
| Механизм | Как работает |
|---|---|
| Аутентификация | Authorization: Bearer <token>; actor всегда берётся из credential, actorId в теле не принимается |
| Оптимистическая конкурентность | GET отдаёт ETag: "<entity>-<version>"; PATCH и ряд action требуют If-Match. Нет заголовка — 428 if_match_required, несовпадение — 409 version_conflict, мусор — 400 invalid_if_match |
| Идемпотентность | Заголовок Idempotency-Key на создающих и action-запросах: повтор возвращает сохранённый ответ с Idempotency-Replayed: true; тот же ключ с другим телом — 409 idempotency_key_reused |
| Пагинация | ?limit= (по умолчанию 50, максимум 200, иначе 422 invalid_limit) и ?cursor=; ответ {"items": [...], "nextCursor": ...} |
| Строгие query-параметры | Неизвестный параметр — 400 invalid_request с details.errors[].loc = "query.<имя>"; фильтр никогда не игнорируется молча |
| Корреляция | X-Request-ID (echo в ответе и ошибках), X-Correlation-ID (попадает в события), X-Run-Id (trace-корреляция, не доменный Run) |
Формат ошибки единый:
{
"error": {
"code": "task_already_claimed",
"message": "Task already has an active claim",
"details": {"taskId": "…", "claimId": "…", "expiresAt": "…"},
"requestId": "req_…"
}
}
Клиенту следует опираться на error.code, а не на текст message. Полный
перечень кодов — в Справочнике ошибок.
Типичный цикл работы¶
sequenceDiagram
autonumber
participant C as Харнесс / runner
participant CP as Control Plane
C->>CP: POST /sessions (lease)
C->>CP: GET /work/available
C->>CP: POST /tasks/{id}:claim {sessionId}
CP-->>C: claim (id, fencingToken, expiresAt)
C->>CP: POST /tasks/{id}:start-run {claimId, fencingToken}
loop работа
C->>CP: POST /claims/{id}:heartbeat
C->>CP: POST /runs/{id}/checkpoints, /actions
end
C->>CP: POST /artifacts {runId, type, ...}
C->>CP: POST /runs/{id}:succeed {completeTask: true}
CP-->>C: run succeeded + task в completionStatus
Подробный разбор каждого шага — в Исполнении; пошаговый пример для первого знакомства — в Первой задаче.
Инварианты, которые держит база¶
Корректность не зависит от аккуратности клиента или кода: ключевые правила закреплены в PostgreSQL.
| Механизм | Что защищает |
|---|---|
SELECT … FOR UPDATE строки задачи |
Критическая секция claim / update / complete |
Частичный уникальный индекс на task_claims(task_id) WHERE status='active' |
Не больше одного активного claim на задачу |
Частичный уникальный индекс на runs(task_id) WHERE status='running' |
Не больше одного запущенного run на задачу |
tasks.version + If-Match |
Потерянные обновления |
tasks.claim_epoch = fencing token |
Запись «проснувшегося» старого владельца |
| Триггеры неизменяемости | Версии типов задач и шаблонов, append-only журнал, история правок комментариев |
| Advisory lock на tenant + рекурсивный CTE | Ацикличность дерева workspaces и графа зависимостей |
Глобальный порядок блокировок — session → task → claim → run; он исключает взаимные блокировки между командами.
См. также¶
- Модель работы — tenant, workspace, проект, задача.
- Исполнение — claims и runs — протокол владения и попыток.
- Харнесс-протокол — как клиенты подключаются к ядру.
- API и Конфигурация — справочные сведения.
- Авторизация и права — permissions, eligibility, PDP.
- Архитектура платформы.