ATL-2026-005ai_agent_experimentexperimental

AI предложил изменение. Что именно подтвердил пользователь?

Предложение помощника ещё не разрешает изменение задачи. Dart-эксперимент проверяет связь подтверждения с показанными данными, устаревшую версию, повтор запроса и сохранение исхода операции.

Опубликовано
11 сентября 2026 г.
Проверено
6 сентября 2026 г.

AI предложил изменение. Что именно подтвердил пользователь?

Помощник в планировщике предлагает перенести срок задачи с 10 на 12 сентября. Приложение показывает предложение, пользователь нажимает «Подтвердить», новая дата сохраняется. Для демонстрации интеграции этого достаточно: модель поняла намерение, приложение получило структурированный ответ, данные изменились.

Затем между предложением и нажатием кнопки задачу редактируют на другом экране. Или пользователь подтверждает перенос, но не получает ответ и повторяет действие. Или обработчик берёт новую дату из последнего сообщения модели, хотя на экране всё ещё показано предыдущее предложение.

Во всех трёх случаях кнопка подтверждения существует. Однако её наличие ещё не объясняет, какое изменение разрешено и можно ли выполнять его сейчас.

Разобрать эту границу полезно до подключения очередного агентского SDK. Она влияет на устройство прикладной операции: какие данные сохранять при подготовке, что перепроверять при исполнении и какой ответ возвращать на повторный вызов. Если всё это остаётся внутри обработчика кнопки, смена интерфейса или способа доставки команды легко меняет поведение приложения.

Для проверки подготовлен небольшой эксперимент ArkTelos Lab: Agent action approval. Он работает на Dart с заранее заданными предложениями вместо живой модели. Исследуется поведение приложения, получившего такой ответ. Качество генерации, сетевой транспорт и способность модели выбирать подходящий срок здесь не оцениваются.

От ответа модели к предложению приложения

Входное сообщение содержит три поля:

{
  "action": "reschedule_task",
  "taskId": "task-1",
  "newDueDate": "2026-09-12"
}

Прежде чем что-либо показывать, приложение проверяет действие, типы полей и дату. Строка 2026-02-30 не становится допустимой датой только потому, что похожа на неё по формату. Неизвестные поля тоже отклоняются: добавленный моделью approved не должен незаметно превратиться в разрешение на запись.

Но даже полностью корректного сообщения недостаточно. У него нет сведений о том, от чьего имени выполняется операция, существует ли задача и может ли этот пользователь её менять. Эти факты приложение получает из собственного контекста и текущих данных.

После проверок появляется предложение — сохранённая запись о конкретном изменении. В коде она называется Proposal. Вместе с новой датой в ней находятся идентификатор задачи, автор предложения, прежняя дата, версия исходных данных и срок действия. Идентификатор самой записи создаёт приложение.

Версия здесь — целое число revision, которое увеличивается при каждом изменении задачи. Предложение сохраняет его как expectedRevision: исполнение допустимо, пока задача соответствует той версии, на основании которой подготовлено изменение.

Пользователю нужно показывать данные именно этой записи. Если дату для экрана взять из одного сообщения, а дату для исполнения — из другого, одинаковый идентификатор задачи не исправит расхождение.

В лаборатории предложение хранится в памяти. Его содержимое после создания нельзя изменить через возвращённую коллекцию. Пока запись ожидает решения, срок задачи остаётся прежним. Подготовка создала возможность выполнить операцию, но сама её ещё не выполнила.

Подтверждение имеет содержание

Сохранить флаг approved = true удобно, пока существует только один вариант изменения. Как только аргументы способны меняться, флаг теряет необходимую точность: одобрено какое предложение, с какой датой и на основании каких данных?

В эксперименте подтверждение передаёт идентификатор предложения и displayedIntent — структурированный набор значений, показанных пользователю. Приложение сравнивает его с сохранённой записью. Проверяются все поля, включая исходную версию и срок действия, а также типы значений. Подмена новой даты приводит к confirmation_mismatch; исходное предложение при этом остаётся доступным для корректного подтверждения.

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

У сравнения есть предел. Оно проверяет совпадение данных, но не доказывает, что живой человек увидел их и нажал кнопку. Если инструмент модели получает право вызывать метод подтверждения от имени пользователя, он способен скопировать нужные значения.

Поэтому в примере разделены два пути вызова. Адаптер предложений получает ModelPort с методом prepare. Адаптер пользовательского решения получает UserPort с методами confirm и decline. Оба передают идентичность пользователя отдельно от JSON модели. Эти небольшие интерфейсы видны в реализации обработчика.

Разделение зависимостей показывает, кому доступна какая операция. Защиту от враждебного кода внутри одного процесса оно не обеспечивает. В настоящем приложении подлинность пользовательского запроса и доступ инструмента к исполнителю должны обеспечиваться отдельно. В лаборатории решения пользователя заданы сценариями; проверка реального интерфейса подтверждения не проводилась.

За время ожидания предложение может устареть

Задача имела срок 10 сентября и версию 7. На этой основе подготовлен перенос на 12 сентября. До подтверждения срок изменили на 15 сентября; версия стала 8.

Если сохранить 12 сентября без повторной проверки, приложение перезапишет изменение, которого пользователь мог вообще не видеть. Формально он согласился на перенос с 10-го числа. Фактически система выполнит перенос с 15-го.

В сценарии A10 обработчик возвращает stale — предложение устарело. Задача сохраняет дату 15 сентября и версию 8. Для продолжения нужно заново подготовить предложение по текущим данным и показать его пользователю.

Сравнение всей версии выбрано намеренно. Оно может отклонить предложение даже после изменения другого поля задачи. Это цена простого правила: исходный снимок должен оставаться тем же. Проверять только дату или разрешать объединение независимых изменений можно, но тогда приложению понадобится более подробное определение допустимого конфликта.

Права тоже проверяются повторно. Доступ, существовавший во время подготовки, мог быть отозван. Пользовательское подтверждение не восстанавливает его. В A09 операция завершается permission_revoked, задача остаётся прежней.

Наконец, предложение может истечь само по себе. Для демонстрации выбран срок пять минут. Это параметр примера, а не рекомендация для любого интерфейса. В A14 проверяются две независимые ситуации: за микросекунду до границы исполнение допустимо, ровно на границе возвращается expired.

До нового исполнения все эти условия проверяются заново. Для повторного получения уже сохранённого результата порядок будет другим.

Повтор команды и повтор изменения

После применения новой даты вызывающая сторона может не получить ответ. Запретить повторное нажатие кнопки недостаточно: повтор способен прийти из другого экрана или из механизма доставки.

Приложению нужна запись о том, чем завершилось конкретное предложение. При успехе эксперимент сохраняет receipt — результат с идентификатором предложения, старой и новой датой, версиями до и после изменения и временем фиксации.

Идентификатор предложения одновременно служит идентификатором исполнения. Если по нему уже есть окончательный исход, точный повтор подтверждения возвращает этот исход с replayed = true. Ветка изменения задачи больше не выполняется. В этом ограниченном смысле обработчик обеспечивает идемпотентность: повтор одного запроса не создаёт повторного изменения.

Порядок условий важен. После проверки автора, доступа к чтению и совпадения подтверждённых данных обработчик сначала ищет сохранённый исход:

if (record.outcome != null)
  return Reply.result(record.outcome!, replayed: true);
if (!allowWrite) return _finish(record, 'permission_revoked');
if (!now.isBefore(DateTime.parse(intent['expiresAt'] as String)))
  return _finish(record, 'expired');

Это фрагмент метода confirm: record содержит предложение и его исход, intent — сохранённые аргументы, а _finish фиксирует окончательный отказ. Полный метод включает проверки, предшествующие показанному фрагменту.

Истечение пяти минут запрещает начать новое исполнение по старому предложению. Оно не отменяет уже состоявшееся изменение и не должно превращать запрос его результата в ошибку. В A18 первое возвращаемое значение отбрасывается, время переводится на сутки вперёд, а повтор получает исходный результат. Дата остаётся 12 сентября, версия — 8.

В A19 после успешного исполнения задача меняется ещё раз: срок становится 20 сентября, версия — 9. Повтор старой команды по-прежнему возвращает запись о переносе с 10 на 12 сентября, но не трогает нынешнюю дату.

Для интерфейса это существенное различие. Исторический результат подтверждает, что определённая операция состоялась. Текущее состояние задачи нужно читать отдельно. Подменять его данными старого ответа опасно даже тогда, когда повторное исполнение уже исключено.

Задача и результат должны сохраняться вместе

Предположим, приложение сначала изменило дату, а затем начало записывать результат. Сбой между этими шагами оставит новую дату без сведений об исполнении. При повторе обработчик не найдёт запись, на которую рассчитывает его защита.

Обратный порядок создаёт другую проблему: результат уже сообщает об успехе, хотя задача ещё не изменилась.

В эксперименте задача и результаты предложений входят в общий неизменяемый снимок Snapshot. Обработчик готовит следующий снимок отдельно. В нём одновременно находятся изменённая задача и запись результата:

final next = Snapshot(
  {..._root.tasks, task.id: changed},
  {..._root.proposals, id: Record(record.proposal, outcome)},
);
if (failBeforeCommit) return _finish(record, 'failed_before_commit');
_root = next;
return Reply.result(outcome);

Здесь _root — текущий снимок, changed — новая версия задачи, outcome — успешный результат. До присваивания _root = next прежние данные остаются неизменными. Между последней проверкой и присваиванием нет асинхронного ожидания или внешнего вызова.

Флаг failBeforeCommit вводит контролируемый отказ после подготовки снимка, но до его принятия. В A15 дата остаётся 10 сентября, версия — 7, успешная запись отсутствует. Сохраняется окончательный исход failed_before_commit. Повтор в A16 возвращает его же. Для новой попытки требуется новое предложение и подтверждение.

Такое поведение выбрано для ясного различения повторной доставки и новой попытки пользователя. Оно не означает, что любое приложение обязано заново спрашивать разрешение после каждого технического сбоя.

Гарантия единой фиксации здесь опирается на конкретные условия: один процесс, один изолированный исполнитель Dart — isolate — и синхронная работа с памятью. После завершения процесса журнал исчезнет. Для сохранения в БД и особенно для действия во внешнем сервисе потребуется отдельно определить, где и как фиксируются изменение и его результат. Перенос этого кода в HTTP-обработчик сам по себе такой гарантии не создаст.

Где в этой схеме находится Flutter

Экрану нужны ожидающее решения предложение, результат подтверждения и возможность показать уведомление. Эти данные имеют разный смысл. Предложение описывает доступное сейчас изменение. Результат сообщает об исходе операции. Уведомление просит интерфейс выполнить отдельное действие.

Для такой границы подходят разные способы организации представления. Например, ark_mvp разделяет прикладной снимок, готовое состояние экрана и эффекты представления, а ark_mvp_flutter подключает эту границу к дереву Flutter. Здесь рассматриваются контракты версии 1.0.0. Различие состояния, результата и эффекта подробнее разобрано в предыдущей статье ArkTelos Lab.

При таком подключении правила подтверждения и защита от повторного исполнения остаются у прикладного обработчика. Пакеты представления не предоставляют авторизацию или хранилище выполненных команд. Сообщение об успехе тоже не может заменить сохранённый результат.

Flutter-интеграция в этот эксперимент не включена. Показанные границы позволяют обсудить её устройство, но не считать проверенным поведение экрана при закрытии или восстановлении.

Что удалось воспроизвести

Локальная проверка прошла на Dart 3.12.2, macOS ARM64. Анализатор не обнаружил замечаний, прошли 24 теста: сценарии A01–A23 и отдельная проверка неизменяемости снимков. CLI выполнил те же 23 сценария; это другой способ увидеть результаты, а не ещё 23 независимых проверки.

Ситуация Наблюдаемый исход
Предложение подготовлено, подтверждения нет Задача не изменена
Аргументы подтверждения подменены Отказ; исходное предложение не испорчено
После подготовки изменилась версия задачи Старое предложение отклонено, новое состояние сохранено
То же подтверждение пришло повторно Возвращён прежний результат, версия не увеличилась
После успеха изменили задачу ещё раз Старый результат возвращён без перезаписи текущих данных
Контролируемый отказ перед фиксацией Задача прежняя, успешного результата нет

Полные входы, проверки и наблюдения доступны в определениях сценариев, JSON-протоколе и описании проверочного запуска. Потеря ответа моделируется отбрасыванием возвращаемого значения; реальный сетевой разрыв и аварийное завершение процесса не проверялись.

Повторить эксперимент можно по инструкции в репозитории. Для сравнения результатов следует использовать именно указанный commit. Сценарий локальной проверки создаёт одноразовую копию и отдельный кэш зависимостей, а затем удаляет их. Вызовы модели и платный API не требуются.

Практический результат разбора — возможность проверить собственную операцию ещё до выбора AI-провайдера. Нужно проследить, из каких данных собирается предложение, какое решение подтверждает пользователь, что перепроверяется перед изменением и откуда берётся ответ на повтор. Если один из переходов держится только на последнем сообщении модели или состоянии кнопки, именно там стоит искать недостающий контракт.

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

Продолжение инженерных разборов — в ArkTelos Lab. Другие решения и документация — на официальном сайте ArkTelos.

Каналы: ArkTelos Lab RU | ArkTelos Lab EN | ArkTelos RU | ArkTelos EN.

Границы результата

  • Фиксированные входы и решения пользователя; живая модель и реальный экран подтверждения не проверялись.
  • Один процесс, один isolate, синхронная память. Нет проверки сети, долговечного хранения, восстановления процесса или конкуренции процессов.
  • Пакеты ark_mvp и ark_mvp_flutter рассмотрены как граница представления; Flutter-интеграция в эксперимент не входит.

CODE / DATA / AGENTS

Связанные эксперименты

experimentagent-action-approval

Проверить привязку подтверждения к сохранённому предложению, текущим правам и версии задачи, а также возврат сохранённого исхода при повторе.

Фиксированные входы и решения пользователя; живая модель и реальный экран подтверждения не проверялись. Один процесс, один isolate, синхронная память. Нет проверки сети, долговечного хранения, восстановления процесса или конкуренции процессов. Пакеты ark_mvp и ark_mvp_flutter рассмотрены как граница представления; Flutter-интеграция в эксперимент не входит.

Открыть паспорт эксперимента →

Версия в Telegram

Читать в ArkTelos Lab

Читать в ArkTelos Lab ↗

КАНАЛЫ ARKTELOS

Новости и инженерные материалы — без смешивания языков.

Основные каналы рассказывают о развитии ArkTelos. ArkTelos Lab публикует архитектурные разборы, эксперименты и воспроизводимые исследования.