Правила вывода работы¶
Правило вывода работы (WorkRule) — данные tenant'а, по которым Control Plane
сам заводит, обновляет, закрывает или отменяет работу, когда в журнале
появляется факт: наблюдение внешней системы, событие ядра или слот расписания.
Статья описывает документ правила, его язык, действия, личность правила и
историю оценок. Она для администраторов tenant'а и авторов пакетов.
Обоснование — CP-ADR-0063 и TAI-ADR-0036; личность правила, тип задачи на
элемент и связи — TAI-ADR-0053.
Как работает правило¶
flowchart LR
F["Факт<br/>(наблюдение, событие, расписание)"] --> C{"condition"}
C -- ложь --> N["not_matched"]
C -- истина --> I{"interpretation?"}
I -- да --> S["Вызов скилла<br/>(оценка ждёт, waiting)"]
I -- нет --> A["action"]
S -- succeeded --> A
S -- failed --> X["failed: rule_skill_failed"]
A --> W["ensure_work / update_work /<br/>cancel_work / complete_work /<br/>request_decision"]
- Журнал читает worker ядра своим курсором по tenant'у. Правило видит только
события, записанные после его включения, и не реагирует на следствия правил
(события сущности
rule, события с корреляцией правила, вызовы скиллов, поставленные правилом). - Каждая оценка — запись в истории правила с версией правила, фактом,
результатом (
matched,not_matched,failed,skipped), evidence и заведённой работой. - Повторная доставка факта не дублирует работу: ключ дедупликации связывает
работу с правилом, а уникальность оценки по
(правило, факт)делает повтор пустым.
Документ правила¶
{
"key": "doc-review",
"description": "Каждый загруженный договор проверяет юрист",
"workspaceId": "<workspace-id>",
"trigger": {"kind": "observation", "type": "document.uploaded"},
"condition": {"eq": [{"var": "payload.data.kind"}, "contract"]},
"interpretation": {"skill": "contract.extract@1",
"inputs": {"document": "{{payload.data.documentId}}"}},
"action": {"kind": "ensure_work", "taskType": "contract-review",
"dedupKeyTemplate": "contract:{{payload.data.documentId}}",
"fields": {"title": "Проверить договор {{skill.output.number}}",
"assignee": "agent:contract-checker"}},
"identity": {"agent": "contract-rules"},
"status": "enabled"
}
| Поле | Правило |
|---|---|
key |
^[a-z0-9][a-z0-9._-]{0,127}$, уникален среди неархивных правил tenant'а (409 rule_key_taken) |
workspaceId |
Workspace правила или null — правило уровня tenant'а. После создания не меняется |
goalId |
Цель, которой служит заведённая работа |
trigger |
Что будит правило (ниже) |
condition |
Выражение над фактом; по умолчанию true |
interpretation |
{skill: "name@version", inputs} — скилл, который превращает факт в данные для действия |
action |
Что сделать (ниже) |
identity |
{agent: <key>} — от чьего имени действует правило (см. Личность правила) |
status |
enabled, disabled; DELETE архивирует |
Правило изменяемо: PATCH с If-Match: "rule-<version>" меняет
description, trigger, condition, interpretation, action, goalId и
identity; version растёт с каждым изменением того, что правило делает.
Каждая оценка записывает версию, по которой считалась.
При записи проверяется всё, что можно проверить без фактов: грамматика, типы
задач (422 unknown_task_type), скиллы (422 unknown_skill), отсутствие
скиллов с внешней записью в интерпретации (422 rule_skill_side_effects),
секреты в документах (422 secret_material_rejected), личность и права
агента.
Триггеры¶
trigger.kind |
Поля | Когда срабатывает |
|---|---|---|
observation |
type — вид наблюдения, source? |
Событие observation.recorded с этим видом (см. События) |
event |
type — тип события журнала |
Событие журнала этого типа. rule.*, work.*, skill.invocation_* запрещены (422 invalid_rule_trigger); наблюдения — только через observation |
schedule |
type: interval, everySeconds 60…604800 |
Раз в интервал; пропущенные за простой слоты не догоняются. Заводящее правило по расписанию обязано иметь interpretation |
Триггер event позволяет строить цепочки на событиях ядра: например, правило
на task.completed с условием {eq: [{var: task.typeKey}, feature-tasks]}
реагирует только на завершение шага определённого типа (см. Цикл spec-driven
разработки).
Событие, в payload которого есть workspaceId, доходит только до правил этого
workspace и правил уровня tenant'а; событие без workspace — до всех правил
tenant'а.
Язык условий и шаблонов¶
Выражение — true, false или объект с одним оператором: and / or
(1…50 операндов), not, eq, ne, lt, le, gt, ge, in, exists.
Операнд — {"var": "<путь>"}, {"const": <JSON>}, скаляр или список. Путь —
корень(.сегмент)*. Ни вызовов, ни арифметики, ни регулярных выражений;
глубина до 16, узлов до 256, документ до 16 КиБ. Невалидное условие —
422 invalid_rule_condition при записи. Сравнение несравнимого (строка с
числом) — ошибка оценки rule_condition_error, а не молчаливая ложь.
Шаблон — строка с {{ путь }}. Строка, целиком состоящая из одного
плейсхолдера, даёт сырое значение (список остаётся списком), иначе значения
подставляются текстом.
| Корень | Стадия | Что это |
|---|---|---|
trigger |
условие, входы, действие | Вид, тип, ссылка на событие, время |
payload |
условие, входы, действие | Тело события журнала |
goal |
условие, входы, действие | Цель правила: id, title, status, workspaceId |
task |
условие, входы, действие | Задача, на которую ссылается событие (сущность task или payload.taskId) |
skill |
действие | Результат интерпретации: status, output, invocationId, artifactId |
item |
действие | Элемент forEach |
Представление задачи в корне task — как в API задачи, включая typeKey
и typeVersion — ключ и версию типа задачи, и verification — сводку
последней попытки проверки {status, attempt} или null.
Действия¶
action.kind |
Что делает |
|---|---|
ensure_work |
Открытая задача по ключу есть — это она; нет — заводит задачу с origin = {kind: rule, ruleId, ref, evidence} |
request_decision |
То же, плюс gate-approval на задачу (fields.approver или fields.approverRole); решение исполняет исходы approvalSchema типа. До решения задачу нельзя взять и завершить (409 approval_required) |
update_work |
Меняет title, description, priority открытой задачи по ключу и дописывает evidence; под живым claim — skipped: task_claimed |
cancel_work |
Переводит открытую задачу в первый достижимый статус категории terminal_cancelled |
complete_work |
Дописывает evidence и завершает задачу через стадию проверки (см. Закрытие работы правилами) |
Общие поля:
forEach— путь к списку (не больше 50 элементов),where— условие надitem: действие применяется к каждому отобранному элементу;dedupKeyTemplate— ключ работы, до 200 символов. Ключ общий для tenant'а: второе правило (cancel_work,complete_work) сверяет работу, заведённую первым;fieldsуensure_workиrequest_decision:title,description,priority,assignee,customFields(имя → шаблон, до 32 полей; пустое значение опускается),relations(ниже);acceptance— критерии заводимой задачи (добавляются к критериям её типа).
Найденная по ключу открытая задача не получает customFields,
acceptance и связей: действие — «ensure», а не upsert.
Тип задачи на элемент: taskTypes¶
Одно действие может заводить задачи разных типов. Для этого taskType —
шаблон (например {{item.type}}), а рядом объявлен taskTypes — список
допустимых ключей (1…20, без повторов):
action:
kind: ensure_work
forEach: skill.output.items
taskType: "{{item.type}}"
taskTypes: [coding-task, feature-converge]
dedupKeyTemplate: "{{item.dedupKey}}"
- При записи каждый ключ
taskTypesдолжен иметь активную версию (422 unknown_task_type,details.field = action.taskTypes[i]). ЛитеральныйtaskTypeрядом сtaskTypesобязан входить в список, а шаблонныйtaskTypeбезtaskTypesотвергается (422 invalid_rule_action): ядро не заведёт задачу типа, который назвал сам факт. - При исполнении отрендеренный тип вне
taskTypes— отказ элементаtask_type_not_allowed. request_decisionпринимает только литеральныйtaskType.
Связи: fields.relations¶
spawnedBy— шаблон, дающий id илиpublicIdзадачи. Новая задача получает связьspawned_byна неё. Задача должна быть видна полномочиям правила, иначе отказ элементаrelation_target_not_found.dependsOn— шаблон или список (до 50) шаблонов ключей дедупликации. Новая задача получаетdepends_onна задачу каждого ключа и не выдаётся исполнителям, пока та не выполнена. Шаблон из одного плейсхолдера может дать список ключей — так элемент скилла несёт свои зависимости сам.null,""и пустой список — «зависимостей нет».
Ключ dependsOn разрешается сначала среди элементов той же оценки (в том
числе идущих в forEach позже), затем по журналу работы правил tenant'а —
новейшей задачей с этим ключом, закрытая тоже подходит. Связи пишутся после
заведения всех задач оценки обычной командой связей с её событиями
task.relation_added. Поэтому повторная оценка того же факта не дублирует ни
задач, ни связей.
Назначение по ссылке на агента¶
fields.assignee у ensure_work и request_decision принимает UUID
principal'а или ссылку agent:<key> на описание агента (см. Агенты
описанием). Ссылка разрешается
после рендеринга шаблона; неизвестный, выведенный или ещё не связанный с
личностью агент — ошибка действия unknown_agent, действие откатывается
целиком.
Отказ отдельного элемента¶
Обычно отказ любой команды откатывает всё, что действие записало, и оценка
становится failed с кодом команды. Для отказов, связанных с типом на
элемент и связями, действует другое правило: отказ касается одного
элемента forEach, остальные элементы идут дальше.
| Код | Причина |
|---|---|
task_type_not_allowed |
Отрендеренный тип не входит в taskTypes |
invalid_relations |
dependsOn — не строка или больше 50 ключей |
relation_target_not_found |
Задача spawnedBy не найдена или не видна правилу |
dependency_not_found |
Ключа dependsOn нет ни в оценке, ни в журнале (или его задача не видна) |
dependency_refused |
Элемент зависит от отказанного элемента той же оценки |
dependency_cycle |
Цикл зависимостей среди элементов оценки — отказаны все элементы цикла |
Отказанный элемент попадает в work[] оценки как {dedupKey, refused:
<код>, detail}. Оценка matched, если хотя бы один элемент заведён или
найден, и failed с кодом work_items_refused, если отказаны все
(details.refused — список кодов).
Личность правила¶
По умолчанию правило действует полномочиями того, кто его включил (или
последним изменил включённое): снимок его credential'а. Поле
identity: {agent: <key>} переводит правило на полномочия описанного
агента — обычно вида service без размещения (placement: none). Так работа
правил в журнале отличима от работы людей, а права правила равны правам,
объявленным в описании агента, а не правам человека, применившего пакет.
# Agent — личность правил пакета (процесса нет)
apiVersion: taimen.ai/v1
kind: Agent
key: contract-rules
spec:
displayName: Contract rules
identity:
kind: service
permissions: [events.read, skills.invoke, tasks.read, tasks.write]
iam:
audiences: [control-plane]
scopeCeiling: [control-plane:read, control-plane:write]
placement: none
---
# WorkRule — действует от имени этого агента
apiVersion: taimen.ai/v1
kind: WorkRule
key: doc-review
spec:
identity: {agent: contract-rules}
# …
Проверки при записи¶
При POST и при любом PATCH, после которого у правила есть личность:
- агент с таким ключом есть и не выведен из оборота — иначе
422 unknown_agent,details: {field: identity.agent, agent}; - каждое право из
identity.permissionsтекущей ревизии агента есть у пишущего (администратор — исключение), иначе403 permission_escalationсdetails.missing. Это та же проверка, что у ревизии агента: обладательrules.writeне получит через правило права чужого агента, а правку действия такого правила не сделает тот, у кого нет прав агента; - связан ли агент с личностью, при записи не проверяется: пакет применяет описание агента и правило одной установкой, а личность заводится позже.
Смена личности (в том числе identity: null, снимающее её) — новая
version правила; поле changes события rule.updated называет identity.
Оценка и авторство¶
- Оценка и все действия правила с личностью идут полномочиями principal'а агента. Полномочия строятся заново на каждой оценке из его текущей IAM-связки, поэтому новая ревизия агента с другими правами меняет права правила со следующей оценки.
- Каждое действие проходит обычные проверки команд полномочиями этой личности: правило не может сделать больше, чем объявлено агенту.
- Агент без principal, выведенный из оборота или без активной связки —
оценка
failed: credential_inactive(details.agent). - Автор работы — principal агента:
createdByзаведённых задач,actorIdсобытийwork.derived,work.reconciled,task.created, запросов решения и артефактаskill_result. RuleOut.authorityPrincipalIdпо-прежнему называет того, кто включил правило: он отвечает на вопрос «кто включил», а не «от чьего имени действует».
Какие права нужны личности, зависит от действий правила: events.read —
чтение факта; tasks.read, tasks.write — поиск и заведение работы, связи;
skills.invoke — интерпретация; approvals.manage — request_decision;
claims.manage — complete_work / cancel_work работы под живым claim;
goals.read — правило с целью.
История и аудит¶
curl -s "https://platform.example.com/api/v1/rules/<rule-id>/evaluations?status=failed" \
-H "Authorization: Bearer $TOKEN"
rule.created,rule.updated(changes— имена полей,version),rule.enabled,rule.disabled,rule.archived;rule.evaluatedна каждую завершённую оценку: версия, факт, результат, evidence, вызов скилла, работа (в том числеrefused), код ошибки;work.derived— работа заведена правилом,work.reconciled— изменена, закрыта или отменена.
API¶
| Метод | Путь | Право |
|---|---|---|
POST |
/rules |
rules.write |
GET |
/rules?status=&workspaceId=&key=&triggerKind= |
rules.read |
GET |
/rules/{id} |
rules.read |
PATCH |
/rules/{id} (If-Match: "rule-<v>") |
rules.write |
DELETE |
/rules/{id} — архив |
rules.write |
POST |
/rules/{id}:enable, /rules/{id}:disable |
rules.write |
GET |
/rules/{id}/evaluations?status= |
rules.read |
Права решаются на workspace правила (правило уровня tenant'а — на tenant'е).
SDK: create_rule(identity=…), update_rule(identity=…), list_rules,
get_rule, enable_rule, disable_rule, archive_rule,
list_rule_evaluations. MCP-сервер даёт только чтение (cp_list_rules,
cp_get_rule). В пакетах правило — вид WorkRule (см. Пакеты
каталога).
Настройки worker'а (переменные окружения ядра):
| Переменная | По умолчанию | Смысл |
|---|---|---|
CP_RULES_BATCH_SIZE |
200 |
Событий журнала в одном пакете оценки tenant'а |
CP_RULES_MAX_ATTEMPTS |
3 |
Неудачных пакетов подряд, после которых сломанная оценка фиксируется failed и пакет идёт дальше |
CP_RULES_SKILL_CHECK_SECONDS |
15 |
Как часто возвращаться к оценкам, ждущим скилл или освобождения claim |
CP_RULES_SKILL_WAIT_SECONDS |
86400 |
Сколько ждать, пока вызов скилла кто-то возьмёт |
CP_RULES_CLAIM_WAIT_SECONDS |
86400 |
Сколько complete_work / cancel_work ждут освобождения claim |
Типичные проблемы¶
| Симптом | Причина | Что делать |
|---|---|---|
422 unknown_agent при записи правила |
Нет описания агента identity.agent или он выведен из оборота |
Применить описание Agent раньше правила (установщик пакетов так и делает) |
403 permission_escalation, details.missing |
У пишущего нет прав, объявленных агенту-личности | Применять токеном с этими правами или сузить права агента |
Оценки failed: credential_inactive, details.agent |
Личность агента ещё не заведена или выведена из оборота | Завести личность агента (для service — bootstrap установки) |
422 invalid_rule_action на taskType |
Шаблонный taskType без taskTypes или литерал вне списка |
Объявить taskTypes |
Оценка failed: work_items_refused |
Все элементы отказаны: details.refused называет коды |
Смотреть work[].refused оценки; чаще всего — неверные ключи dependsOn |
Оценка failed: unknown_agent |
fields.assignee отрендерился в неизвестного или несвязанного агента |
Проверить ключ агента и его фактическое состояние |
| Правило не реагирует на старые события | Правило видит только события после включения | Ожидаемо |
См. также¶
- Цели, приёмка и evidence — origin
rule,complete_work. - Агенты описанием — личность правила и
agent:<key>. - Пакеты каталога — вид
WorkRule. - Цикл spec-driven разработки — правило
feature-expand. - События
- Approvals — исходы
request_decision.