Сводка по дизайну сводок использования инструментов
Обозначения быстрой модели для параллельных пакетов инструментов — мотивация, конкурентный анализ с Claude Code, архитектура и обоснование подхода только-добавление через Static, что привело к текущему рендеру в полном режиме.
Пользовательская документация: Сводки использования инструментов.
1. Краткое описание
После завершения каждого пакета инструментов Qwen Code запускает короткий вызов быстрой модели, который возвращает метку в стиле темы коммита git, описывающую пакет. Метка отображается в виде приглушенной строки ● <label> в полном режиме и заменяет общий заголовок Tool × N в компактном режиме. Генерация выполняется асинхронно и параллельно с потоком API следующего раунда, поэтому задержка около 1 с скрывается за стримингом основной модели.
| Измерение | Claude Code | Qwen Code |
|---|---|---|
| Точка срабатывания | query.ts — после финализации пакета инструментов | useGeminiStream.ts → handleCompletedTools — та же точка жизненного цикла |
| Модель генерации | Haiku через queryHaiku | Настроенная fastModel через GeminiClient.generateContent |
| Поведение суб-агентов | !toolUseContext.agentId — только основная сессия | Неявно — суб-агенты работают через agents/runtime/, а не useGeminiStream |
| Планирование | Асинхронный запуск, ожидание перед началом стриминга следующего раунда | Асинхронный запуск, добавление в историю при разрешении |
| Формат вывода | ToolUseSummaryMessage, передаваемый в поток SDK | HistoryItemToolUseSummary, добавляемый в историю UI + фабрика экспортируется для будущего использования в SDK |
| Включение | Переменная окружения CLAUDE_CODE_EMIT_TOOL_USE_SUMMARIES, по умолчанию выкл | Настройка experimental.emitToolUseSummaries (по умолчанию вкл) + переменная окружения |
| Основной потребитель | Мобильные клиенты / SDK | Компактный режим CLI + полный режим, будущий SDK |
| Промпт | Git-коммит-тема, прошедшее время, самое характерное существительное (дословный перевод) | Идентичный системный промпт |
| Обрезка входных данных | 300 символов на поле инструмента через truncateJson | Идентичная |
| Префикс намерения | Первые 200 символов последнего сообщения ассистента | Идентичный |
| Кэширование промптов | enablePromptCaching: true на вызове Haiku | Пока не реализовано (доступен маршрут через forked-agent; отмечено как будущая оптимизация) |
| Постобработка метки | Сырой текст модели | cleanSummary (удаляет markdown, кавычки, префиксы ошибок; ограничение 100 символов, защита от ReDoS) |
| Сохранение сессии | Только поток; каждая сессия генерирует заново | Только история UI; ChatRecordingService не сохраняет записи tool_use_summary |
2. Анализ реализации Claude Code
2.1 Поток
Claude Code выполняет цикл инструментов в query.ts. После выполнения пакета инструментов и нормализации результатов функция-генератор разветвляет вызов Haiku, сохраняет ожидающий промис в nextPendingToolUseSummary и продолжает выполнение вызова API следующего раунда. Задержка Haiku (~1 с) перекрывается стримингом основной модели (5–30 с), поэтому пользователь не замечает дополнительной задержки. Непосредственно перед отправкой контента следующего раунда генератор ожидает ожидающую сводку и передает сообщение tool_use_summary в поток.
tool_batch_complete → fork queryHaiku (асинхронный запуск)
↓
next_turn_stream_starts
↓
← Promise сводки разрешается во время стриминга →
↓
await pendingToolUseSummary → yield ToolUseSummaryMessage
↓
продолжение со следующим раундом2.2 Ключевые исходные файлы
| Компонент | Файл | Ключевая логика |
|---|---|---|
| Генератор | services/toolUseSummary/toolUseSummaryGenerator.ts:45-97 | generateToolUseSummary({ tools, signal, isNonInteractiveSession, lastAssistantText }) |
| Триггер | query.ts:1411-1482 | Проверка через emitToolUseSummaries + не суб-агент; разветвление Haiku; сохранение promise |
| Ожидание+отправка | query.ts:1055-1060 | Ожидание pendingToolUseSummary на границе следующего раунда, передача сообщения |
| Фабрика сообщений | utils/messages.ts:5105-5116 | createToolUseSummaryMessage(summary, precedingToolUseIds) |
| Флаг функции | query/config.ts:23,36-38 | emitToolUseSummaries: isEnvTruthy(CLAUDE_CODE_EMIT_TOOL_USE_SUMMARIES) |
2.3 Дизайнерские решения
- Всегда генерировать, когда флаг включен, независимо от компактного/детального состояния. Сводка — это артефакт уровня потока; UI решает, отображать ли ее.
- Отправлять как тип сообщения первого класса.
tool_use_summaryнаходится на одном уровне сuser,assistant,tool_resultв потоке SDK с полемprecedingToolUseIdsдля связывания с пакетом. - Суб-агенты исключены.
!toolUseContext.agentId— вывод суб-агентов агрегируется выше; отдельные пакеты суб-агентов создавали бы зашумленные метки, которые никогда не появляются в основном UI. - По умолчанию выключено. Флаг только через окружение гарантирует нулевую стоимость, пока потребитель SDK не решит использовать функцию. Сам терминал CC не отображает сообщение.
- Обрезка входных данных до 300 символов на поле. Покрывает основной риск стоимости — один большой результат инструмента, раздувающий промпт, при этом сохраняя достаточно сигнала для метки.
3. Реализация Qwen Code
3.1 Поток
Qwen Code подключается к той же точке жизненного цикла (useGeminiStream.handleCompletedTools), но отображает результат по обе стороны ui.compactMode, чтобы функция была полезна пользователям CLI без какой-либо инфраструктуры SDK.
tool_batch_complete (handleCompletedTools)
↓
config.getEmitToolUseSummaries()?
↓
fork generateToolUseSummary (асинхронный запуск)
↓
submitQuery() для следующего раунда (стриминг начинается)
↓
← Promise сводки разрешается во время стриминга →
↓
addItem({type:'tool_use_summary', summary, precedingToolUseIds})
↓
HistoryItemDisplay рендерит:
compactMode=false → ● <label> отдельная строка
compactMode=true → скрыт; MainContent смотрит и вставляет в заголовок CompactToolGroupDisplay3.2 Ключевые исходные файлы
| Компонент | Файл | Ключевая логика |
|---|---|---|
| Сервис | packages/core/src/services/toolUseSummary.ts | generateToolUseSummary, truncateJson, cleanSummary, фабрика сообщений |
| Флаг конфигурации | packages/core/src/config/config.ts:getEmitToolUseSummaries | Переменная окружения → настройки → по умолчанию (true) |
| Триггер | packages/cli/src/ui/hooks/useGeminiStream.ts:handleCompletedTools | Запускает вызов быстрой модели, addItem при разрешении |
| Рендер полного режима | packages/cli/src/ui/components/HistoryItemDisplay.tsx | Рендерит строку ● <label> при !compactMode |
| Поиск в компактном | packages/cli/src/ui/components/MainContent.tsx | summaryByCallId map → compactLabel prop для каждой tool_group |
| Заголовок компактного | packages/cli/src/ui/components/messages/CompactToolGroupDisplay.tsx | Заменяет стандартный Tool × N на <Summary> · N tools при наличии метки |
| Обработка слияния | packages/cli/src/ui/utils/mergeCompactToolGroups.ts | Обрабатывает tool_use_summary как скрытый в компактном для смежности |
| Тип UI | packages/cli/src/ui/types.ts:HistoryItemToolUseSummary | { type: 'tool_use_summary', summary, precedingToolUseIds } |
3.3 Ограничение <Static>: только добавление
Центральное архитектурное решение в этом PR: почему метка в полном режиме — это отдельный элемент истории, а не украшение для самой tool_group.
Qwen Code рендерит транскрипт через Ink <Static>. Static работает по принципу “только добавление”: как только элемент зафиксирован в буфере терминала, Ink не будет перерисовывать эту область, пока не будет вызван refreshStatic() для очистки и полного перерендера транскрипта. Это модель производительности, от которой зависит CLI — статические элементы не перерисовываются при каждом нажатии клавиши.
Теперь учтем время вызова быстрой модели:
T0 Пакет инструментов завершен, tool_group добавлена в историю
T0+ε tool_group рендерится через <Static> и фиксируется в буфере
T0+1с Вызов быстрой модели разрешается с меткойВ момент T0+1с мы не можем ретроспективно добавить метку к уже зафиксированной tool_group. Есть два варианта:
- Обновить свойства tool_group + вызвать
refreshStatic(). Работает, но вызывает полную перерисовку транскрипта при каждом пакете — одна из самых дорогих операций UI в приложении. Видимая вспышка. Неприемлемо для декоративной метки. - Отобразить сводку как новый элемент истории, добавленный после tool_group. Static обрабатывает это естественно — новые элементы добавляются чисто, без перерисовки.
Этот PR выбирает вариант 2 в полном режиме. Запись tool_use_summary — это реальный элемент истории, отображаемый HistoryItemDisplay как одна приглушенная строка ● <label>. Без refreshStatic.
Компактный режим отличается из-за mergeCompactToolGroups. Когда последовательные tool_groups объединяются, MainContent уже вызывает refreshStatic() — это существующий путь выполнения, и он перерисовывает объединенную группу с меткой, найденной в истории. Таким образом, компактный режим получает метку как замену заголовка. Чтобы избежать отображения метки дважды (один раз как заголовок компактного режима, второй раз как завершающая строка ● <label>), HistoryItemDisplay скрывает отдельную строку, когда compactMode равен true.
Полный режим Компактный режим (с объединением)
─────────── ──────────────────────────────
[tool_group] [объединенная tool_group — заголовок заменен по lookup]
● <label> (строка ● <label> скрыта)3.4 Семантика флага
Три уровня, разрешаемые в порядке приоритета:
QWEN_CODE_EMIT_TOOL_USE_SUMMARIES=0|1|true|false— переменная окружения, наивысший приоритет.experimental.emitToolUseSummariesвsettings.json— по умолчаниюtrue.- Неявный пропуск — если
config.getFastModel()возвращаетundefined, генерация пропускается независимо от флага. Без ошибки, без видимых изменений для пользователя.
3.5 Очистка вывода
cleanSummary запускается для каждого ответа модели перед добавлением в историю:
- Берется только первая строка (отбрасываются преамбулы рассуждений модели).
- Удаляются префиксы маркеров списка (
-,*,•) — модели иногда возвращают метку как элемент списка. - Удаляются окружающие кавычки/бэктики через ограниченный regex
{1,10}(безопасно для CodeQL; ни одна реальная метка не содержит более нескольких обертывающих кавычек). - Удаляются префиксы типа
Label:,Summary:,Result:,Output:, которые некоторые модели добавляют. - Отклоняются формы сообщений об ошибках (
API error: ...,Error: ...,I cannot ...,I can't ...,Unable to ...) — возвращается пустая строка, элемент истории не добавляется. - Жесткое ограничение длины до 100 символов (мобильный UI обрезает около 30; запас покрывает фразы на CJK).
3.6 Телеметрия
Вызов генерации сводки устанавливает promptId: 'tool_use_summary_generation', поэтому его использование токенов учитывается отдельно в /stats. Это позволяет пользователям видеть точную дополнительную стоимость функции, не смешивая ее с подсказками-предложениями или использованием основной сессии.
4. Отличия от Claude Code (и причины)
| Отличие | Причина |
|---|---|
| Слой настроек в дополнение к флагу окружения | Qwen Code отображает метку в CLI; пользователям нужен постоянный переключатель, а не экспорт переменной для каждой оболочки. |
| По умолчанию вкл вместо выкл | Метка сразу видна пользователю в обоих режимах отображения; пользователи, настраивающие fastModel, уже подписываются на функции быстрой модели. |
Специализированная постобработка cleanSummary | Qwen Code поддерживает более разнообразных провайдеров, чем CC; некоторые модели добавляют Label: или оборачивают в кавычки. Нормализация на границе обеспечивает единообразие UI. |
Хранит HistoryItemToolUseSummary вместо отправки потокового сообщения | Реализация в первую очередь для CLI; маршрут через поток SDK — это будущий PR. Фабрика ToolUseSummaryMessage уже экспортирована для этой работы. |
| Кэширование промптов пока не реализовано | Быстрая модель часто совпадает с основной для пользователей, не настроивших отдельную. Добавление общего кэша требует маршрутизации через forkedAgent.ts; отслеживается как последующая задача. |
| Двойной путь рендера (строчный в полном режиме + заголовок в компактном) | По умолчанию Qwen Code использует ui.compactMode: false; без строчного рендера в полном режиме функция была бы невидима для большинства пользователей. |
5. Известные ограничения
- Нет сохранения сессии.
tool_use_summaryне записывается в JSONL-файл записи чата. При возобновлении сессии метки теряются; группы инструментов отображаются с общим заголовком как запасной вариант. Низкий приоритет: метки естественным образом генерируются заново при продолжении сессии. - Пока нет отправки в поток SDK. Фабрика сообщений экспортирована, но CLI пока не передает
tool_use_summaryв мост SDK. Следующий PR. - Нет кэширования промптов. Каждый пакет приводит к затратам на новые входные токены. Незначительно в абсолютных величинах (~300 токенов), но измеримо, если запускать десятки пакетов за один раунд.
- Сводка для объединенных компактных групп берет метку первого пакета. Если пользователь запускает десять различных пакетов подряд (тесный цикл, не типично), заголовок объединенного компактного режима будет показывать только намерение первого пакета. Компромисс принят: разворачивание меток по пакетам в объединенном представлении визуально более зашумлено, чем использование первой.
- Требуется быстрая модель. Без настроенной
fastModelгенерация пропускается. Использование основной модели в качестве запасного варианта намеренно запрещено, чтобы сохранить контролируемую стоимость.
6. Будущие работы
- Подключить
ToolUseSummaryMessageк мосту SDK, чтобы существующая фабрика использовалась на стороне потребителей. - Направить генерацию через
forkedAgent.tsсenablePromptCaching, чтобы повторяющиеся префиксы имен инструментов попадали в кэши провайдеров. - Опционально: сохранять записи
tool_use_summaryвChatRecordingServiceи восстанавливать при возобновлении сессии. - Опционально: ярлыки меток по имени инструмента (например, всегда
Read <filename>для одного вызоваread_file) как быстрый путь до LLM.