temporality
Изменить страницу↗
Инженерные документы

Входящие сообщения: задачи по инициативе пользователя

#1. Цель

Сделать канал коммуникации полноценной точкой входа: пользователь пишет по своей инициативе — без открытого вопроса от агента, без выбранного проекта, без UI — и это сообщение превращается в задачу, уточняющий диалог или вежливый отказ, а не теряется молча.

Флагманский кейс:

«Подготовь проект документа "Смета на разработку по проекту Название" после согласования с Ивановым передай на утверждение в команду Такую-то»

Одно сообщение содержит: задачу подготовки документа, ссылку на проект по названию, согласующего человека по фамилии, команду-получателя по названию и порядок действий (шаги с человеческими контрольными точками). Ни одна из этих ссылок сегодня не разрешается системой.

#2. Что есть сейчас

#3. Анатомия кейса

Разбор флагманского сообщения на требования:

Фрагмент Что это Чего не хватает
«Подготовь проект документа "Смета…"» новая задача + тип результата (документ) приём инициативных сообщений; выбор агента
«по проекту Название» ссылка на проект по имени разрешение проектов по имени + уточнение при неоднозначности
«после согласования с Ивановым» человек по фамилии + контрольная точка разрешение пользователей по имени (Ивановых много); HITL-gate уже есть (ask_human)
«передай на утверждение в команду Такую-то» юнит по имени + командная точка разрешение юнитов по имени; адресация команды; руководитель как получатель по умолчанию
(порядок «после… передай») последовательность шагов многошаговый план — есть (DAG/команды), точек входа в них из канала нет

Отдельные классы сообщений, которые обязаны разбираются корректно:

#4. Основные понятия

#4.1. Inbound Message

Долговечная строка — тот же паттерн, что HumanRequest: сущность в workspace, события в journal, состояние выводится из сущности.

workspace_inbound_message
    id              TEXT PRIMARY KEY      = inbound/{channel}/{message_id}
    channel         TEXT                  -- matrix | telegram | web
    message_id      TEXT                  -- event_id матрикса / update_id телеги
    sender_raw      TEXT                  -- mxid / chat id
    sender_user_id  TEXT NULL             -- разрешённый workspace-пользователь
    room            TEXT                  -- комната/чат доставки
    body            TEXT
    attachments     JSONB                 -- сохранённые файлы (inbox)
    status          TEXT                  -- received|classified|clarifying|routed|rejected|expired
    classification  TEXT                  -- task|amendment|status|cancel|chatter|reply
    intake_run_id   TEXT NULL
    target_run_id   TEXT NULL             -- ран, порождённый сообщением
    project_id      TEXT NULL             -- определённый проект
    created_at / updated_at
    UNIQUE (channel, message_id)          -- идемпотентность доставки

id детерминирован: повторная доставка /sync (реплей истории, рестарт кернела) не порождает вторую обработку.

#4.2. Intake Run

Обработка входящего — это сам агентский ран в системном проекте intake. Такой проект существует один на инсталляцию, не виден в обычных списках и содержит только служебные прогоны.

Промпт intake-рана, помимо самого сообщения (дословно) и блока отправителя, несёт блок недавнего диалога комнаты (до 8 предыдущих сообщений): сообщения пользователя — из inbound-строк, ответы агента — из ответов целевых ранов (target_run_id), каждое с датой-временем UTC. Примечание в блоке говорит, что новое сообщение может продолжать один из этих диалогов. Без блока ответ в мессенджере спустя время после завершёния рана приходил как лишённый контекста текст, и intake классифицировал его как новую задачу, хотя пользователь продолжал старый разговор.

Почему ран, а не сервисная функция: приём сообщения — это диалог (классификация → уточнение → уточнение → запуск), а готовый механизм диалога с человеком в канале уже существует — ask_human + доставка в комнату + обратная маршрутизация ответа (matrixInboundLoop) + таймауты с политиками. Intake-агент переиспользует всё это целиком и не требует нового протокола.

Инструменты intake-агента (все с авторизацией отправителя, см. §10):

#4.3. Классификация

Двухступенчато, детерминизм насколько возможно:

  1. Правила. Сообщение совпало с открытым вопросом → это ответ (существующий путь, ничего не меняется). Короткое «отмена/cancel» при активном ране из этого диалога → cancel. Активный intake-ран ждёт уточнения → сообщение становится ответом на него (существующий механизм ответов).
  2. Модель. Остальное классифицирует intake-агент первым ходом: task / amendment / status / cancel / chatter. Решение фиксируется в classification и событии intake.classified — наблюдаемо и оспоримо.

#5. Идентификация отправителя

Сегодняшнее комнатное соответствие «кто в комнате — тот и владелец» для порождения работы неприемлемо. Вариант по умолчанию — закрытый мир:

#6. Разрешение сущностей

Общий принцип: разрешение — это функция поиска с ранжированием и явным уточнением, а не угадывание. Ни одна ссылка не угадывается молча.

#6.1. Проект

Порядок разрешения:

  1. Явная ссылка. Нормализованный поиск по именам проектов, видимых из позиции отправителя (орг-видимость, org-structure.md §9). Один матч → подтверждение одним сообщением; несколько → уточнение списком; ноль → сразу уточнение, без попыток «похожих».
  2. Контекст отправителя. Ровно один видимый проект → предложить его как вариант (не молча подставить). Несколько → уточнение.
  3. Неопределённый проект — состояние, а не сущность: intake-ран держит задачу, задаёт вопрос списком видимых проектов (+ «создать новый» — только если разрешено политикой, по умолчанию нет) и живёт до ответа или таймаута. Таймаут уточнения = timeout_policy ask_human; дефолт для intake — cancel с вежливым итогом в канал: «не понял, к какому проекту относится задача, создайте её в приложении».

Специальный staging-проект для неопределённых задач не заводим: состояние живёт в intake-ране, восстановления после сбоя — через Temporal, история — в workspace_inbound_message + событиях.

#6.2. Человек («Иванов»)

Поиск по каталогу (регистронезависимый, по фамилии/имени/id); карточки — как в human_contacts. Несколько Ивановых → уточнение с вариантами (имя, юнит, роль). Имена в естественном языке не обязаны быть уникальными — уникален user_id, его агент и подставляет в ask_human/send_file.

#6.3. Команда («команда Такую-то»)

Поиск по юнитам: имя, kind=team в приоритете, затем department. Несколько матчей → уточнение. Ноль → уточнение с списком юнитов отправителя.

#7. Руководитель оргюнита

Недостающее ребро в дереве:

org_unit.head_user_id  TEXT NULL REFERENCES workspace_user(id)

Семантика — та же, что у видимости ресурсов: эффективный руководитель разрешается снизу вверх. У юнита без head руководитель — head ближайшего предка (обычно у команды — руководитель отдела). NULL по всей цепочке — никого (эскалация остаётся на текущем алгоритме близости).

Использование:

Миграция: колонка nullable, все NULL, поведение не меняется.

#8. Командные запросы

ask_human с получателем-юнитом (org:<unit-id>) сегодня берёт первого активного пользователя юнита — это не «команда», это случайный человек.

Новая семантика:

Для «передай на утверждение команде X» агент (после search_org_units) сам выбирает форму: адресат — head (строгое утверждение одним лицом) или вся команда (первый ответ). Подсказка — в промпте intake-агента: для «утверждения» по умолчанию head, для «согласования» — конкретный человек или команда целиком.

#9. Многошаговые инструкции

Кейс «после согласования с Ивановым передай на утверждение» — это не механика intake, а содержание задачи. Intake переводит естественную инструкцию в структурированную задачу: проект, заголовок, промпт с чек-поинтами. Исполняющий агент проходит шаги штатными средствами:

Профиль агента-исполнителя — дефолтный агент проекта, если intake-агент не распознал специализированного (смета → финансовый шаблон — это работа skills/шаблонов агентов, не ядра).

#10. Авторизация, подотчётность, идемпотентность

#11. Наблюдаемость

События (journal, scope системного проекта intake, а для routed — дублируются в scope целевого проекта):

intake.received     {message_id, channel, sender, room}
intake.classified   {classification, confidence}
intake.clarifying   {question, options}         -- прокси ask_human
intake.routed       {target_run_id, project_id, actor}
intake.rejected     {reason: unknown_sender|forbidden|unresolved|not_a_task|encrypted_unsupported}
intake.expired      {reason}

UI: страница «Входящие» — список сообщений с классификацией, статусом, переходом в intake-ран и в порождённый ран.

#12. Фазы реализации

Фазы intake строятся поверх слоя каналов (docs/channels.md): общий конвейер inbound, дедуп и идентификация — там; здесь — только логика приёма задач.

#Фаза 1 — Intake MVP (Matrix)

#Фаза 2 — Руководитель и команды

#Фаза 3 — Диалог и Telegram

#Фаза 4 — Политика и эксплуатация

#13. Критерии готовности

Флагманский сценарий, конец-в-конец,_matrix:

  1. Пользователь пишет кейс из §1 из своей комнаты;
  2. intake-ран: находит проект «Название» (один матч), Ивановых два — задаёт уточнение, пользователь отвечает в канале;
  3. задача стартует в проекте от имени пользователя (ActorID), агент готовит документ, согласовывает с Ивановым (ask_human), команда получает файл (send_file + командный запрос);
  4. Runs показывает дерево: сообщение → intake → задача;
  5. Повторная доставка того же event_id не порождает дубль;
  6. Сообщение от постороннего mxid вежливо отвергнуто;
  7. «что там со сметой?» возвращает статус без нового рана;
  8. «отмени» останавливает порождённый ран (с проверкой прав).

#14. Открытые вопросы