Skip to Content
ДизайнДекларативные определения агентов — портирование из Claude Code 2.1.168

Декларативные определения агентов — портирование из Claude Code 2.1.168

Внутренний проектный документ по портированию схемы декларативных агентов (markdown + YAML frontmatter) из Claude Code в qwen-code. Решает задачу #4821  и координируется с портированием workflow в задаче #4721  / PR #4732 .

Статус реализации (вертикальные срезы)

PR #4842  выпустил поля с end-to-end runtime-путём на тот момент. PR #4870  затем заменил YAML-парсер для поддержки блочных скаляров. Этот follow-up PR строится на обоих: он заменяет YAML-сериализатор (в PR #4870 он был написан вручную — см. docs/design/yaml-parser-replacement.md), выводит mcpServers + hooks в SubagentConfig и подключает их к runtime, чтобы per-agent MCP-серверы и хуки действительно срабатывали при запуске подагента.

ПолеСтатусПримечания
permissionModeвыпущено (#4842)связывается с существующим в qwen approvalMode на этапе парсинга
maxTurnsвыпущено (#4842)подключено к существующему runtime-пути runConfig.max_turns
color allowlistвыпущено (#4842)ужесточает существующее поле до набора _Y из CC + обработка legacy-сентинела auto
mcpServersвыпущено (follow-up)вложенный YAML безопасен при round-trip благодаря eemeli/yaml stringify; runtime-переопределение объединяет серверы сессии и агента через Config-обёртку подагента + принудительная пересборка tool-registry
hooksвыпущено (follow-up)эфемерные записи HookRegistry регистрируются при создании подагента, удаляются через onStop; v1 срабатывает глобально (без фильтра по области действия агента)
effortотложенов провайдерах qwen пока нет параметра effort на уровне модели
memoryотложенов auto-memory qwen пока нет разделения на области user/project/local
isolationотложеноruntime принадлежит workflow из PR #4732; дефолтное значение для агента появится вместе с ним
initialPromptотложенотребует CLI-флаг --agent (в qwen нет инфраструктуры main-session-agent)
skillsотложенотребует потребления config.skills через SkillManager

Полная запись обратного инжиниринга ниже сохранена как проектный эталон для отложенных полей — константы схемы, семантика DL7/Ig5, сообщения об ошибках и матрица координации с workflow по-прежнему критичны для этой работы.


Фаза 0 — Границы

ПунктЗначение
Проверенный upstreamClaude Code 2.1.168 (задача #4821 ссылается на ≥ 2.1.167, мы на один релиз выше)
Нативный бинарник/private/tmp/cc-2.1.168/package/claude (220 МБ)
Извлечённые строки/private/tmp/cc-2.1.168/claude.strings (~342 тыс. строк)
Worktree.claude/worktrees/gifted-hamilton-684741
Веткаlazzy/gifted-hamilton-684741 от main @ 45efb1d3a
Вне скоупаКод workflow из PR #4732 (отдельный worktree lazzy/lucid-pare-974192) — координируемся только через интерфейс
Правило авторстваАвтор — LaZzyMan; никаких Co-Authored-By или трейлеров AI-инструментов в коммитах, PR, задачах или комментариях (согласно ~/.claude/CLAUDE.md)

Фаза 1 — Результаты обратного инжиниринга

Все утверждения здесь были независимо проверены через grep по claude.strings и выдержали состязательное опровержение. Уровни уверенности: C = Confirmed (подтверждено, есть прямые доказательства в бинарнике), I = Inferred (выведено из нескольких подтверждённых фактов), O = Open (открыто, всё ещё не определено).

Схема — 15 полей, опровергнутых и подтверждённых

Теневая схема frontmatter агента — это Ig5, используется внутри ug5.agent для телеметрии tengu_frontmatter_shadow_unknown_key / _mismatch. Продакшн-загрузчик — это DL7 (parseAgentFromMarkdown), который выполняет написанную вручную попольную валидацию с кастомными сообщениями об ошибках. Отдельная JSON-схема JL7 (используется fL7 / parseAgentFromJson) более строгая, но это другой путь кода (используется для --agents <json> и settings.agents).

#ПолеТип (Ig5 / DL7)ОбязательноПо умолчаниюEnum / ОграничениеУверенность
1namestring, non-emptyданет — DL7: if(!T||typeof T!=="string")return nullC strings:308120, 309074
2descriptionstring, non-emptyдаJL7: .min(1, "Description cannot be empty")C strings:308120, 309074, 309076
3modelstringнетundefinedinherit (case-insensitive) нормализуется в литерал "inherit"; иначе передаётся как есть с trimC strings:308120, 309075, 309076
4toolsstring|array (MDH union)нетundefinedодиночный токен *undefined (означает “наследовать все”); дублируется через AXH/FbKC strings:308120 (MDH/AXH), 309075
5disallowedToolsstring|array (MDH)нетundefined”Игнорируется, если задан tools” (согласно описанию); применяется вызывающим кодомC strings:308120, 309075
6effortstring|integerнетundefinedenum GN=["low","medium","high","xhigh","max"] ИЛИ int; алиас P37={med:"medium"}C strings:308120, 309075, GN/P37 inline
7permissionModestringнетundefinedenum $E = Gmq = [...kc], где kc=["acceptEdits","auto","bypassPermissions","default","dontAsk","plan"] (6 значений)C strings:307649 (kc), 308120, 309075
8mcpServersz.unknown() (Ig5); array(jL7) (JL7)нетundefinedкаждый элемент: string ИЛИ record(string, MCPServerSpec); попольный safeParse в DL7C strings:308120, 309075, 309076
9hooksz.unknown() (Ig5); _u() (JL7)нетundefinedвалидируется лениво в runtime через TKO_u().safeParse (форма хуков из settings.json)C strings:308120, 309073 (TKO), 309076
10maxTurnsunion(number, string, null)нетundefinedположительное целое число (парсится через W46 — принимает число или числовую строку)C strings:308120, 309075 (W46), 309076
11skillsstring|array (MDH)нет[] (в выводе)нормализуется через ml(q.skills) = FbK(H) ?? []; нет wildcard * (в отличие от tools)C strings:308120, 309075
12initialPromptstringнетundefinedтолько пробелы → undefined; авто-сабмитится только если агент является основной сессией (через --agent / settings), игнорируется как подагентC strings:308120, 309075
13memorystringнетundefinedenum ["user","project","local"]C strings:308120, 309075, 309076
14backgroundstring|bool (eiH=EL8)нетundefinedпринимает true / false / "true" / "false"; только truthy нормализуется в true, иначе undefinedC strings:308120, 309075
15isolationstringнетundefinedenum только ["worktree"] (НЕ ["none","worktree"] — это другая схема в strings:313284 для настроек background-session)C strings:308120, 309075, 309076

Тонкое наблюдение, выдержавшее опровержение: хотя skills и является “опциональным”, emit-выражение в DL7 выглядит как ...I !== void 0 && {skills: I}, а ml(undefined) возвращает [] (не undefined), поэтому итоговая выгружаемая запись будет содержать skills: [], даже если поле отсутствует во frontmatter. Это влияет на проверки на равенство ниже по коду — учтите это при портировании в qwen-code.

Возможные дополнительные поля помимо 15

#ПолеТипПо умолчаниюEnum / ОграничениеУверенность
16colorstringundefinedenum _Y = ["red","blue","green","yellow","purple","orange","pink","cyan"]; описывается как "@internal — display color in the agents UI"; значения вне _Y молча отбрасываются при парсинге (DL7 выводит ...z && typeof z === "string" && _Y.includes(z) && {color: z})C strings:308120, 309075, _Y inline
Это единственное новое поле agent-frontmatter сверх списка из #4821. Поля, которые искали, но НЕ нашли в Ig5 / JL7: version, tags, labels, category, icon, alias / aliases, experimental, deprecated, owner, author, homepage, displayName, shortDescription (все они встретились только в схеме skill bg5 или в нерелевантных идентификаторах).

Loader — карта файлов и функций

ЗадачаФункцияРасположениеConf
Сборщик реестра верхнего уровняQL (экспортируется как getAgentDefinitionsWithOverrides)strings:309076C
Обходчик файловой системы (общий для skills/commands/output-styles)Gm (мемоизируется через h6)strings:312887C
Обнаружение для каждого .mdd_q (= loadMarkdownFiles, ripgrep с --files --hidden --follow --no-ignore --glob *.md, 3 с AbortSignal.timeout, фоллбэк wY3 при __("true"))strings:312887C
Парсер для каждого файла (markdown)DL7 (= parseAgentFromMarkdown)strings:309074C
Парсер для каждого файла (JSON)fL7 (= parseAgentFromJson), использует схему JL7strings:309073C
Загрузчик плагинов агентовb0_ → для каждой директории oR7 → для каждого файла sR7strings:308780, 308779C
ВстроенныеnaH() — возвращает [JqH=general-purpose, KL7=statusline-setup, …] плюс неявный YI=forkstrings:309073, 308663C
Резолвер переопределенийDS() (= getActiveAgentsFromList) — см. порядок разрешенияstrings:309073C
Инвалидация кэшаu0_() (= clearAgentDefinitionsCache) — очищает QL.cache + Gm.cachestrings:309073C
Вотчер ФС (chokidar)s_T()Q4_=s_T() при инициализации модуля (WB6)strings:316417C

Gm("agents", _) читает три базовые директории (policySettings, userSettings, projectSettings), помечая каждую в записи, затем дедуплицирует по inode (отбрасывает дубликаты с одинаковым inode от симлинков / хардлинков, логирует Skipping duplicate file '<path>' from <source> (same inode already loaded from <firstSource>)). Телеметрия: tengu_dir_search с параметрами managedFilesFound, userFilesFound, projectFilesFound, projectDirsSearched, subdir.

Порядок разрешения (Resolution order) — окончательный приоритет

Функция DS() фильтрует входные данные по source, затем итерирует массив фиксированного порядка в Map, где ключом выступает agentType. Поскольку Map.set перезаписывает значения, побеждает ПОСЛЕДНИЙ затронутый бакет:

[built-in, plugin, userSettings, projectSettings, flagSettings, policySettings] ^ highest precedence
ИсточникПроисхождениеПриоритет переопределенияConf
built-innaH() (захардкожено в бинарнике)1 (самый низкий)C strings:309073
pluginb0_ → для каждого плагина agentsPath/agentsPaths2C strings:308780
userSettings~/.claude/agents/ (CLAUDE_CONFIG_DIR или ~/.claude)3C strings:312887, 307489
projectSettings<cwd>/.claude/agents/ ПЛЮС обход iV_() вверх до homedir / git root4C strings:312887, iV_ inline
flagSettingsCLI-флаг --agents <json> (схема qKO = h.record(h.string(), JL7()))5C strings:330190, 309076
policySettingsсистемная директория: macOS /Library/Application Support/ClaudeCode/.claude/agents, Linux /etc/claude-code/.claude/agents, Windows C:\Program Files\ClaudeCode\.claude\agents6 (самый высокий)C strings:307649 (H2), 312887

Коллизии разрешаются молча — срабатывает только телеметрическое событие tengu_plugin_name_collision (winner_source: T.at(-1)); пользователю не показывается предупреждение “X overrides built-in”. (strings:308742 hMH.)

Тонкий момент в поведении: iV_() обходит директории от самых вложенных, поднимаясь вверх от cwd, но поскольку в Map.set побеждает последнее значение, .claude/agents/ из внешнего дерева переопределяет внутреннее дерево в рамках projectSettings. Это неожиданно — вынесено в открытые вопросы.

Парсер frontmatter

ВопросОтветConf
Используется ли библиотека?Нет — самописный сплиттер lz, вызывающий Bun.YAML.parse (через обёртку l5H). В бинарнике нет gray-matter, js-yaml или front-matter.C strings:307902 (l5H), 307905 (lz), 110303 (ошибки Bun.YAML)
Регулярное выражениеn5H = /^---\s*\n([\s\S]*?)---\s*\n?/C strings:307905
Обработка ошибокОшибка парсинга YAML → повторная попытка с нормализацией табов в 2 пробела; если снова ошибка, логировать Failed to parse YAML frontmatter in <file>: <err> на уровне warn и вернуть {frontmatter: {}, content: body} (НИКОГДА не выбрасывает исключение)C strings:307905, 151839
Извлечение телаПростой срез строки H.slice(K[0].length) после закрывающего ---; позже нормализуется через v$H (вероятно, удаление начального перевода строки)C strings:307905
Используется ли совместно для agents / skills / commands / output-styles?Да — тот же lz переиспользуется в Iq_ (загрузчик skills), f13 (загрузчик устаревших commands) и загрузчике агентов через Gmd_qC strings:312690
Валидатор схемыZod v4 (встроен). Присутствуют маркеры только для v4: looseObject, treeifyError, prettifyError, toJSONSchemaC strings:141270-141395, 141586
Режим валидацииТеневойahH("agent", frontmatter) запускает ug5.agent().strict().safeParse() только для телеметрии; DL7 игнорирует результат и продолжает собственную валидацию по полям. Мягкий объект frontmatter является источником истины в рантайме.C strings:308120 (ahH/ug5), 309074 (DL7 вызывает, но игнорирует)
События телеметрииtengu_frontmatter_shadow_unknown_key, tengu_frontmatter_shadow_mismatch (дедупликация через внутрипроцессный Set A37)C strings:154634, 154636

Связывание — Agent tool + CLI-флаг

СлойЧто делаетConf
Схема Task/Agent tool ($_3)Объявляет subagent_type: string.optional(); если пропущено, используется фоллбэк на general-purpose (или fork, если AI() возвращает true)C strings:~309220
Поиск подагентаactiveAgents.find(a => a.agentType === requestedType) по toolUseContext.options.agentDefinitions.activeAgentsC strings:~309220
Нечёткий фоллбэкMWK(s) = s.normalize("NFKC").toLowerCase().replace(/[\p{White_Space}\p{Pd}_]+/gu, ""); неоднозначное совпадение → AgentTypeError; точное повторное совпадение → tengu_subagent_type_normalizedC strings:~309220
Гейт разрешенийlV_(toolPermissionContext, "Task", agentType) — при отказе → Agent type '<x>' has been denied by permission rule 'Task(<x>)' from <source>.C strings:~309220
Источник системного промптаТело Markdown становится getSystemPrompt: () => body + ('\n\n' + UVH(agentType, memoryScope) when memory enabled) — замыкание захватывается во время парсингаC strings:309074-6 (DL7)
Рендер в главном потокеPp({mainThreadAgentDefinition, …}) — если у агента appendSystemPrompt: true (универсальный встроенный claude), тело добавляется к дефолтному; иначе ЗАМЕНЯЕТ дефолтноеC strings:311015
CLI --agent <name>Объявляется через Commander; обработчик действия if(I) process.env.CLAUDE_CODE_AGENT = I; — записывает в переменную окружения, которая в другом месте читается в appState.agent. Также записывается в pid-файл.C strings:330190, 142138
CLI --agents <json>Отдельный флаг; JSON-запись {name: {description, prompt, …}} валидируется через qKO = h.record(h.string(), JL7()); присоединяется к тому же реестру activeAgents с source: flagSettingsC strings:330190, 309076

Жизненный цикл — холодная загрузка + горячая перезагрузка

АспектПоведениеConf
Холодная загрузкаЛенивая — QL мемоизируется через h6 (обертка кэша); первый доступ читает файловую систему + плагины, последующие доступы возвращают кэшированноеC strings:309076
Механизм горячей перезагрузкиchokidar watcher s_T() регистрируется при инициализации модуля (WB6); отслеживает .claude/agents (пользовательские + проектные), а также директории skills и commandsC strings:316417
Флаги watcherpersistent:true, ignoreInitial:true, depth:2, awaitWriteFinish:{stabilityThreshold,pollInterval}, ignored:(p,s) => s?.isFile() ? !p.endsWith(".md") : false, usePolling:kZ4 (macOS true), события add/change/unlinkC strings:316417
Дебаунс300 мс (l_T = 300); обработчик вызывает RIH(), Vv(), u0_(), …u0_() инвалидирует кэш агентовC strings:316417, 309073
Адаптивный опросактивный = интервал n_T = 2000 мс; неактивный (нет взаимодействия в течение r_T = 60000 мс) → i_T = 30000 мс; пересоздает экземпляр chokidar при переключенииC strings:316417
Слэш-команда /agentsUI на local-jsx для управления агентами (Library/create/edit/delete/run) — НЕ команда пересканированияC strings:314593
Слэш-команда /reload-pluginsПовторно запускает QL(W8()), пересчитывает агентов; охватывает агентов из плагинов (которые chokidar НЕ отслеживает)C strings:314595, 190948
Другие пути инвалидацииclearSessionCaches (используется в /clear) также вызывает u0_()C strings:313246

Открытые вопросы (Фаза 1)

#ВопросConfПуть решения
Q1Является ли отсутствие color в #4821 намеренным (он помечен как @internal) или это упущение?OСчитать намеренным — портировать поле, но пометить как internal/UI-only
Q2Является ли мягкое поведение DL7 (background принимает строки, maxTurns принимает строки) задокументированной функцией для пользователей или хаком для обратной совместимости?OЗеркально отразить для паритета, но предупредить в документации по портированию
Q3Почему enum isolation ["worktree"] предназначен только для агентов, в то время как схема настроек фоновых сессий принимает ["none","worktree"]?OВероятно, “no isolation” = пропущенное поле; явно задокументировать
Q4Намеренно ли --agents <json> (flagSettings) имеет приоритет 5 (выше проекта, ниже политики)?Oqwen-code может пропустить этот флаг в v1, отложив решение
Q5Push от внутреннего к внешнему через iV_ + Map.set (побеждает последний) → побеждает внешнее дерево при коллизиях projectSettings. Это скрытая угроза или намеренное поведение?Oqwen-code следует выбрать семантику победы внутреннего (innermost-wins), чтобы избежать скрытой угрозы

Фаза 2 — План реализации для qwen-code

Текущее состояние — обзор в одном абзаце

qwen-code уже поставляется с обширной инфраструктурой подагентов: SubagentManager (packages/core/src/subagents/subagent-manager.ts) реализует CRUD над файлами markdown+YAML frontmatter в .qwen/agents/ (проектные) и ~/.qwen/agents/ (пользовательские), опираясь на кастомный YAML-парсер (packages/core/src/utils/yaml-parser.ts — без зависимостей gray-matter / yaml, что подтверждено в package.json). SubagentConfig (packages/core/src/subagents/types.ts:41-122) уже содержит name, description, tools, disallowedTools, approvalMode, systemPrompt, model, runConfig, color, background. SubagentLevel уже поддерживает пять областей видимости (session, project, user, extension, builtin) с приоритетом session > project > user > extension > builtin (subagent-manager.ts:189-220). Инструмент Agent (packages/core/src/tools/agent/agent.ts) объявляет subagent_type и динамически обновляет enum своей схемы через subagentManager.changeListener. Мост convertClaudeAgentConfig() уже существует в packages/core/src/extension/claude-converter.ts:162-220 с маппингом имен инструментов и маппингом permissionMode → approvalMode. Пробел заключается в следующем: (a) в схеме отсутствуют 8 полей из #4821 (effort, permissionMode как первоклассное поле, mcpServers, hooks, maxTurns как поле верхнего уровня, skills, initialPrompt, memory, isolation); (b) нет CLI-флага --agent <name>; (c) нет горячей перезагрузки в стиле chokidar (инвалидация в стиле расширений есть, но не для файловых систем агентов); (d) maxTurns сейчас вложен в runConfig.max_turns — его нужно поднять на верхний уровень согласно #2409.

Архитектурные решения

D1. Переиспользовать существующий yaml-parser для frontmatter

Решение: Переиспользовать packages/core/src/utils/yaml-parser.ts (уже используется в SubagentManager.parseSubagentContent и загрузчике skills). Обоснование: lz в Claude Code — это тот же общий парсер, используемый для skills + commands + agents; qwen-code уже повторяет этот паттерн. Добавление gray-matter или js-yaml — это лишние изменения. Существующий парсер обрабатывает разделение --- … --- и молчалив при некорректном вводе (соответствует подходу lz warn-and-return-empty).

D2. Порядок разрешения / приоритетов

Решение: Использовать session > project (.qwen/agents/) > user (~/.qwen/agents/) > extension > builtin — т.е. сохранить существующий порядок SubagentLevel в qwen-code, НЕ зеркалировать бакеты flagSettings/policySettings из Claude Code в v1. Обоснование: policySettings в Claude Code (управляемая директория) — это корпоративный сценарий развертывания, которого нет в qwen-code. Инжектируемые через флаги агенты (--agents <json>) — это фича для продвинутых пользователей, которую можно отложить до P4. Существующий пятиуровневый приоритет qwen-code уже покрывает случаи, важные для #4821: проект переопределяет пользователя, пользователь переопределяет встроенное. Уровень extension чисто встраивается между user и builtin.

D3. Валидация — оставить существующий SubagentValidator

Решение: Расширить SubagentValidator (packages/core/src/subagents/) для валидации восьми новых полей. НЕ внедрять zod, если пайплайн skillManager его еще не использует; если существующий валидатор написан вручную, оставить его таким. Обоснование: Ig5 в Claude Code работает только в тени — рантайм-валидация реализована вручную через DL7. Следование этому паттерну сохраняет читаемость сообщений об ошибках (например, Agent file <path> has invalid permissionMode '<x>'. Valid options: …) без привлечения еще одной зависимости. Если skillManager уже использует zod, следуйте этому выбору для согласованности — TBD при чтении кода skills в рамках подготовки к P1.

D4. Горячая перезагрузка — отложить; полагаться на холодную загрузку + явную перезагрузку

Решение: v1 НЕ поставляется с chokidar watcher. Хуки инвалидации кэша уже существуют (у subagentManager есть changeListener и явное обновление, управляемое CRUD). Перезагрузка на уровне проекта происходит при старте сессии; редактирование внутри сессии через UI /agents вызывает инвалидацию. Слэш-команда /reload-agents (или использование /reload-plugins) может быть добавлена в P4, если будет запрос от пользователей. Обоснование: Горячая перезагрузка через FS watcher ресурсоемка (chokidar добавляет цикл опроса с адаптивным планированием — одна только реализация в Claude Code занимает ~150 строк служебного кода). Холодная загрузка при старте вполне достаточна для v1 и соответствует тому, как SubagentManager подключен сегодня. Оставляем дверь открытой для P4.

D5. Подключить CLI-флаг —agent — в скоупе v1

Решение: Добавить --agent <name> в CliArgs файла packages/cli/src/config/config.ts. Поведение: искать в разрешенном реестре, устанавливать агента как агента основного потока, выбрасывать понятную ошибку, если имя не разрешается. Соответствовать семантике Claude Code (заменять системный промпт по умолчанию, если только у агента нет appendSystemPrompt: true). НЕ использовать косвенную передачу через env-переменную CLAUDE_CODE_AGENT — объект Config в qwen-code может хранить его напрямую. Обоснование: Это точка входа для пользователя в #4821 — без нее декларативные агенты доступны только через параметр subagent_type инструмента Agent, что слишком косвенно для сценария “установить мой агент по умолчанию”. --agents <json> (во множественном числе) можно отложить до P4.

D6. Координация Workflow.agentType — контракт интерфейса

Решение: Предоставить стабильный интерфейс резолвера, который сможет вызывать createProductionDispatch из PR #4732, когда он будет смержен. В частности:

КонтрактВладелец
Frontmatter name — это и есть строка workflow agentType (равенство ключей, с учетом регистра)этот PR
Жестко заданный минимум disallowedTools в workflow ([SEND_MESSAGE, EXIT_PLAN_MODE], зеркально от upstream Tg8; подтверждено в PR #4732 как ToolNames.SEND_MESSAGE, ToolNames.EXIT_PLAN_MODE) ОБЪЕДИНЯЕТСЯ (UNION) с disallowedTools на уровне агента — минимум применяется всегда, даже если в определении агента заданы toolsпотребляется PR workflow
opts.isolation для каждого вызова переопределяет значение по умолчанию isolation: 'worktree' на уровне агентапотребляется PR workflow
Значения model, effort, permissionMode, maxTurns из определения агента переопределяют значения по умолчанию workflow, если они заданыпотребляется PR workflow
Тело агента становится systemPrompt подагента; WORKFLOW_SUBAGENT_SYSTEM_PROMPT workflow используется как фоллбэк, если agentType не разрешаетсяпотребляется PR workflow
Если agentType не задан или не разрешается, workflow откатывается к встроенному подагенту workflow (корректно, без выброса ошибки)потребляется PR workflow
Разрешение противоречия #4721 / #4821 (приоритет tools над disallowedTools): при этом портировании реестр агентов записывается так, что disallowedTools всегда передается отдельно от tools. Правило «игнорируется, если задан tools» из таблицы #4821 применяется вызывающими сторонами Agent-tool (т. е. при создании ToolConfig подагента), а не на этапе парсинга. Это позволяет рабочему процессу всегда объединять свой базовый набор с disallowedTools независимо от того, задан ли у агента tools. Реестр агентов выступает пассивным переносчиком данных; правила приоритета находятся в точке диспетчеризации. Это разрешает кажущийся конфликт между правилом «игнорируется» из #4821 и правилом «объединение» из #4721.

Канонизация имен инструментов: Используйте ToolNames.SEND_MESSAGE и ToolNames.EXIT_PLAN_MODE (проверено по диффу PR #4732), экспортируемые как именованные константы из packages/core/src/agents/runtime/workflow-orchestrator.ts после его слияния. Самому порту declarative-agents НЕ нужно импортировать их — это базовый набор рабочего процесса, применяемый в точке диспетчеризации рабочего процесса.

Структура модулей

ПутьНовый / ИзмененНазначение
packages/core/src/subagents/types.tsИзмененДобавить 8 новых полей в SubagentConfig: effort, permissionMode (уже маппится через approvalMode — оставить оба? см. D7 ниже), mcpServers, hooks, maxTurns (вынести на верхний уровень, объявить runConfig.max_turns устаревшим), skills, initialPrompt, memory, isolation
packages/core/src/subagents/subagent-manager.tsИзмененРасширить parseSubagentContent / serializeSubagent для полного цикла (чтения и записи) новых полей; расширить вызовы SubagentValidator
packages/core/src/subagents/subagent-validator.ts (предполагаемый путь)ИзмененДобавить валидацию для каждого поля в соответствии с сообщениями об ошибках из DL7: Agent file <path> has invalid permissionMode '<x>'. Valid options: … и т.д.
packages/core/src/subagents/agent-frontmatter-schema.tsНовыйЕдиный источник истины для констант перечислений: EFFORT_VALUES, PERMISSION_MODE_VALUES, MEMORY_VALUES, ISOLATION_VALUES, COLOR_VALUES. Зеркалирует Claude Code 2.1.168 без изменений.
packages/core/src/subagents/builtin-agents.tsИзмененНовые поля по умолчанию равны undefined; изменения поведения нет
packages/core/src/tools/agent/agent.tsИзмененЧитать новые поля из разрешенного SubagentConfig при создании опций подагента (model, maxTurns, permissionMode, effort); пробросить семантику переопределения isolation для каждого вызова для #4721
packages/cli/src/config/config.tsИзмененДобавить флаг --agent <name>; разрешать его через SubagentManager при запуске; выдавать ошибку, если имя не разрешается
packages/cli/src/config/config.test.tsИзмененТесты для разрешения флага --agent + пути обработки ошибок
packages/core/src/extension/claude-converter.tsИзмененДобавить маппинг для новых полей при импорте .md файлов Claude (mcpServers, hooks, maxTurns на верхнем уровне, memory, isolation и т.д.)
packages/core/src/subagents/agent-frontmatter-schema.test.tsНовыйSnapshot-тесты для списков перечислений; тесты полного цикла парсинга/сериализации
packages/core/src/subagents/subagent-manager.test.tsИзмененТесты для валидации новых полей, приоритетов, сообщений об ошибках
packages/core/src/tools/agent/agent.test.tsИзмененТесты для проброса новых полей в runtime подагента
docs/cli/agents.md (если существует) или docs/declarative-agents.mdНовыйПользовательская справка: схема из 16 полей + примеры

D7. permissionMode против approvalMode — создаем мост, а не заменяем

Решение: Принимать ОБА варианта: permissionMode (совместимый с Claude) и существующий approvalMode (совместимый с qwen) в frontmatter. При парсинге, если задан permissionMode, маппить его в approvalMode с использованием существующей таблицы в claude-converter.ts:195-208 (default → default, plan → plan, acceptEdits → auto-edit, dontAsk → default, bypassPermissions → yolo). Если заданы оба, выигрывает approvalMode (более специфичный для qwen-code), и генерируется телеметрическое событие в стиле tengu_frontmatter_shadow_*, указывающее, что были заданы оба значения. Обоснование: Сохраняется обратная совместимость с существующими .qwen/agents/*.md, использующими approvalMode, при этом permissionMode из Claude Code принимается без изменений, чтобы пользователи могли использовать файлы агентов Claude Code без правок.

Таблица маппинга схемы

Поле Claude Code 2.1.168Поле qwen-codeАдаптацияПримечания
namenameнетидентично, обязательно
descriptiondescriptionнетидентично, обязательно
modelmodelпринимать inherit, fast, haiku, sonnet, opus или authType:model-idqwen-code уже поддерживает более широкий словарь; inherit — новое
toolstoolsпринимать string|array; * → undefined (inherit-all)уже поддерживается как array; добавить обработку string и *
disallowedToolsdisallowedToolsпринимать string|array; всегда передается отдельно от toolsправило приоритета (#4821 «игнорируется, если задан tools») применяется вызывающими сторонами, а не парсером
efforteffort (новое)перечисление low/medium/high/xhigh/max + целое число; алиас med → mediumэффект в runtime специфичен для qwen (маппится на существующий регулятор thinking-effort, если есть, иначе сохраняется и игнорируется)
permissionModepermissionMode (новое) + мостик к approvalModeперечисление acceptEdits/auto/bypassPermissions/default/dontAsk/plan; таблица маппинга согласно D7принимать в формате Claude без изменений
mcpServersmcpServers (новое)массив (string | {name: spec}); валидация по элементам, отбрасывание некорректных записей с предупреждениеминтеграция с MCP runtime в P4
hookshooks (новое)объект, соответствующий форме hooks в settings.jsonинтеграция с hook runtime в P4
maxTurnsmaxTurns (новое поле верхнего уровня)положительное целое число; для совместимости принимается числовая строкавыносится из runConfig.max_turns; вложенная форма сохраняется как устаревший алиас
skillsskills (новое)массив имен скиллов; также принимается строка, разделенная запятымиruntime: предзагрузка через skillManager при запуске агента
initialPromptinitialPrompt (новое)строка; состоящая только из пробелов → undefined; срабатывает только когда агент является основной сессиейпробрасывается через путь флага --agent
memorymemory (новое)перечисление user/project/local; загружается из .qwen/agent-memory/<name>/ и т.д.runtime в P4
backgroundbackgroundпринимать bool или строку "true"/"false"; только truthy → trueуже поддерживается; ослабить правила парсинга
isolationisolation (новое)перечисление только ["worktree"]runtime принадлежит workflow PR (#4732 P3+); реестр просто переносит поле
color (недокументированное #16)colorперечисление _Y = ["red","blue","green","yellow","purple","orange","pink","cyan"]; значения вне списка тихо отбрасываютсяуже есть в qwen SubagentConfig; ужесточить валидацию до allowlist из Claude Code

План тестирования TDD

БлокТестовый файлЧто проверяется
Константы перечислений схемыagent-frontmatter-schema.test.ts (новый)EFFORT_VALUES, PERMISSION_MODE_VALUES, MEMORY_VALUES, ISOLATION_VALUES, COLOR_VALUES совпадают с Claude Code 2.1.168 байт в байт (снэпшот)
Парсер — успешный сценарийsubagent-manager.test.tsПолный цикл парсинга .qwen/agents/test.md со всеми 16 полями → сгенерированная запись имеет ожидаемую структуру
Парсер — обязательные поляsubagent-manager.test.tsОтсутствие name возвращает null + warn-лог; отсутствие description возвращает null + warn-лог
Парсер — валидация перечисленийsubagent-manager.test.tsНекорректные permissionMode / memory / isolation / effort / color вызывают специфичное предупреждение (соответствующее формулировке DL7), поле отбрасывается
Парсер — нестрогие типы полейsubagent-manager.test.tsbackground: "true"true; maxTurns: "5"5; effort: "med""medium"; tools: "Read,Edit"["Read","Edit"]; tools: "*" → undefined
Парсер — белый список цветовsubagent-manager.test.tscolor: "magenta" тихо отбрасывается (без ошибки), color: "blue" сохраняется
Особенность поля skillssubagent-manager.test.tsпропуск skills приводит к skills: [] (соответствует поведению генерации Claude Code DL7)
Приоритет разрешенияsubagent-manager.test.tsОдинаковый name в project + user → побеждает project; в user + builtin → побеждает user; в extension + builtin → побеждает extension
Дедупликация по inodesubagent-manager.test.tsДва пути к одному inode (симлинк) → только одна запись, выводится лог
Бридж permissionModesubagent-manager.test.tspermissionMode: bypassPermissions → разрешается в approvalMode: yolo; заданы оба → побеждает approvalMode + телеметрия
CLI-флаг --agentpackages/cli/src/config/config.test.tsФлаг устанавливает агент главного потока; неразрешенное имя выбрасывает ошибку с текстом Agent type '<x>' not found. Available agents: …
Нечеткий фоллбэк инструмента Agentagent.test.tssubagent_type: "Test_Engineer" разрешается в зарегистрированный test-engineer через нормализацию NFKC-lowercase
Ошибка “не найдено” для инструмента Agentagent.test.tsНеразрешенный subagent_type → сообщение об ошибке соответствует Agent type '<x>' not found. Available agents: <list>
Контракт workflowagent-frontmatter-schema.test.tsЭкспортируемый интерфейс getAgentByName(name) возвращает полный SubagentConfig, включая isolation, disallowedTools, model, effort, permissionMode, maxTurns (может использоваться в workflow PR #4732)

Поэтапный план PR

ЭтапНазваниеОбъем измененийБлокировки
P1feat(core): declarative agent schema fields (effort, permissionMode, maxTurns top-level, memory, isolation, color allowlist)Добавить поля в SubagentConfig; расширить парсер + валидатор + сериализатор; пометить runConfig.max_turns как устаревшее; добавить модуль констант перечислений; тестыНет
P2feat(core): wire new agent fields into Agent tool runtimeПробросить model, effort, maxTurns, бридж permissionMode/approvalMode в место вызова AgentTool.execute()AgentHeadless.create(); тестыP1
P3feat(cli): --agent flag for main-thread agent selectionДобавить --agent <name> в CliArgs; разрешать при запуске; обработка ошибок; тестыP1
P4(опционально, расширение скоупа) feat(core): mcpServers + hooks + skills + initialPrompt + memory runtimeПодключить четыре поля “только метаданные в v1” к реальным эффектам в рантаймеP1, плюс подсистемы skill/MCP/hook

Цель для каждого PR — дельта ≤ 800 LOC (без учета тестов); P1 самый большой, около 600 LOC валидатора + тесты.


Этап 3 — Матрица координации с портированием workflow (#4721 / PR #4732)

Фича декларативных агентовВзаимодействие с workflowВладелецБлокируется
Поле name как ключ реестраСтрока поиска opts.agentType в workflow (#4721  явно)этот PR определяет контракт реестра; workflow PR потребляетнет — форма реестра может стабилизироваться первой
Поле disallowedTools в агентеWorkflow делает UNION с жестко заданным базовым набором [SEND_MESSAGE, EXIT_PLAN_MODE] (согласно #4721  §2 — проверено по диффу PR #4732: ToolNames.SEND_MESSAGE, ToolNames.EXIT_PLAN_MODE)этот PR содержит поле; workflow PR делает UNION при диспатчемердж P3 workflow PR #4732
Поле tools в агентеWorkflow передает как есть в ToolConfig.tools подагентаэтот PR содержит поле; workflow PR пробрасываетP3 workflow PR #4732
Поле model в агентеopts.model из workflow переопределяется при каждом вызове; model агента используется по умолчаниюэтот PR содержит поле; workflow PR разрешает приоритетP3 workflow PR #4732
Поле effort в агентеПереопределение в месте вызова workflow имеет приоритет; fallback на значение по умолчанию агентаэтот PR содержит поле; workflow PR разрешаетP3 workflow PR #4732
Поле permissionMode в агентеМаппится в approvalMode подагента при диспатче; переопределение в месте вызова workflow имеет приоритетэтот PR содержит поле через D7-бридж; workflow PR пробрасываетP3 workflow PR #4732
Поле maxTurns в агентеЗаменяет жестко заданное WORKFLOW_SUBAGENT_MAX_TURNS = 50 в workflow, если задано в агентеэтот PR содержит поле; workflow PR разрешает приоритетP3 workflow PR #4732
Поле isolation: 'worktree' в агентеПо умолчанию; переопределяется opts.isolation при каждом вызове (#4721  §3)этот PR содержит поле; workflow PR владеет рантаймомP3+ workflow PR #4732 (сейчас падает в P1)
Поле initialPrompt в агентеWorkflow не использует его (срабатывает только когда агент является главной сессией через --agent)этот PR + CLIнет (независимо)
memory, mcpServers, hooks, skillsWorkflow не имеет специальной обработки, кроме передачи в рантайм подагентаэтот PR содержит поля; подключение к рантайму в P4 / в будущембудущие PR
Обновления EXCLUDED_TOOLS_FOR_SUBAGENTSWorkflow PR #4732 добавляет WORKFLOW в набор (согласно контексту issue/PR — хотя при критическом разборе отмечено, что этого еще НЕТ в agent-core.ts в main, только в worktree)workflow PR владеет; этот PR не затронутнет
Каноническая форма имени инструмента для базового набора workflow (ToolNames.SEND_MESSAGE)Этот PR не импортирует базовые константы; он только содержит строки disallowedTools в исходном виде. Канонизацией занимается workflow PR.workflow PRworkflow PR #4732
Порядок релизаЭтот PR (P1+P2+P3) релизится независимо от workflow. P3 workflow PR #4732 заблокирован до тех пор, пока резолвер типа getAgentByName() из этого PR не станет импортируемым.параллельно до P3 workflowP3 workflow читает из экспортов этого PR
Без циклических блокировок: этот PR и PR для workflow могут быть смержены параллельно на этапах P1/P2. Они синхронизируются на этапе workflow-P3, которому требуется резолвер реестра из этого PR. Если этот PR будет смержен первым, workflow-P3 будет читать данные из него. Если первым будет смержен PR для workflow, он будет выпущен с существующим поиском по subagent_type (возвращая значения по умолчанию для workflow при промахе) и переключится на более расширенный резолвер после того, как этот PR будет смержен.

Этап 4 — Риски и открытые вопросы

Риски

#РискМеры по снижению рисков
R1Расхождение схем между минорными релизами Claude Code (2.1.168 → 2.1.x)Зафиксировать модуль констант enum с комментарием “проверено на 2.1.168”; повторно запускать strings-grep для новых релизов в рамках скилла feature-reverse
R2runConfig.max_turnsmaxTurns верхнего уровня — это breaking change схемы для существующих файлов .qwen/agents/*.mdСохранить вложенную форму как устаревший алиас с одним циклом депрекейта; выводить предупреждение при парсинге, задокументировать в CHANGELOG
R3Потери при круговом преобразовании permissionModeapprovalMode (в Claude 6 режимов, в qwen около 4)Явно мапить оба направления согласно D7; отправлять телеметрию при двойной установке; НЕ переписывать молча при сохранении
R4Новые поля (hooks, mcpServers, skills, memory) перенесены в реестр, но не поддерживаются рантаймом в v1 → пользователи могут их задать, но они молча не сработаютЧетко описать скоуп v1 в документации; выводить одноразовый info-лог для каждого агента, если поле “перенесено, но пока не поддерживается рантаймом” не пустое
R5Adversarial-verify отметил, что EXCLUDED_TOOLS_FOR_SUBAGENTS НЕ включает WORKFLOW в main — это может означать, что порт workflow еще не смержен или отсутствует защита от recursive-fanoutУточнить у автора PR для workflow (LaZzyMan = я), что защита будет добавлена в PR #4732, а не в этом порте
R6Поведение projectSettings, при котором внешнее дерево побеждает внутреннее (Q5), может привести к ошибкам, если его скопироватьqwen-code явно выбирает принцип побеждает самое внутреннее; протестировано с помощью фикстуры R5
R7Поле color задокументировано как @internal в описании бинарника — возможно, мы портируем то, что Anthropic явно не поддерживаетПортировать его, но также пометить как @internal в документации qwen-code; считать только UI-элементом; не выводить в пользовательскую справочную документацию

Открытые вопросы — предлагаемые решения

#ВопросРешение
Q1Намеренно ли color исключен из #4821?Считать намеренным. Портировать поле; НЕ упоминать в пользовательской документации, кроме как “доступно, internal”.
Q2Мягкое поведение DL7: документировать или сделать хак?Зеркалить. Принимать background: "true", maxTurns: "5", effort: "med" для паритета, даже если это не задокументировано. Добавить тесты.
Q3Почему enum isolation отличается в схеме агента и схеме background-session?Задокументировать расхождение в комментарии к коду; “no isolation” = поле отсутствует, а не является значением enum.
Q4Должен ли --agents <json> (множественное число, flagSettings) попасть в v1?Отложить до P4. CLI-интерфейс для продвинутых пользователей; v1 включает только --agent <name> (единственное число), что и требуется в #4821.
Q5Приоритет внутреннего и внешнего дерева для вложенных .qwen/agents/?Побеждает самое внутреннее. Переопределить случайное поведение Claude Code, где побеждает внешнее. Тестовая фикстура в P1.
Q6Приоритет tools и disallowedTools: #4821 говорит “игнорируется, если задан tools”; #4721 говорит “объединение с минимумом workflow”Реестр — это просто данные. Парсер сохраняет оба поля независимо. Правила приоритета находятся в точке диспатча (Agent tool / workflow). Разрешает противоречие.
Q7Каноническая форма имени инструмента для минимума workflow disallowedTools — проверено по PR #4732 как ToolNames.SEND_MESSAGE, ToolNames.EXIT_PLAN_MODEНе касается этого PR — это зона ответственности PR для workflow. Задокументировать только в матрице координации.
Q8Влияет ли close-resolution из #2409 на что-либо?Наследовать гайдлайн из #2409 “повысить model + maxTurns до верхнего уровня”. Уже учтено в этом плане.
Q9Должны ли агенты уровня extension в существующем приоритете SubagentLevel qwen-code оставаться выше builtin (как сейчас) или ниже (в Claude Code нет аналога)?Оставить extension > builtin. Расширения устанавливаются пользователем; встроенные — по умолчанию от вендора. Установленные пользователем побеждают.
Q10Полностью ли определены issues #4821, #4721, #4732 для контракта, предлагаемого этим документом?Оставить координационный комментарий в #4821 со ссылкой на этот документ, кратким описанием решений по каждому полю и просьбой к мейнтейнерам подтвердить: (a) паритет схем с 16 полями Claude Code 2.1.168, (b) мост D7 permissionMode/approvalMode, (c) порядок приоритетов D2, (d) разрешение противоречия tools/disallowedTools через концепцию “реестр — это просто данные”.

Задачи по координации

#ДействиеГде
A1Опубликовать краткое описание по каждому полю + 5 решений в #4821 для подтверждения мейнтейнерамикомментарий в #4821
A2Добавить перекрестную ссылку на этот документ из #4721 с упоминанием матрицы Этапа 3комментарий в #4721
A3После того как P1 этого порта будет смержен, пингануть #4732 для переключения на более расширенный резолверкомментарий в PR #4732 (когда будет готов)
A4Повторно запустить strings-grep для следующего минорного релиза Claude Code для обнаружения расхождений схемcron-задача скилла feature-reverse (вручную до этого момента)
Last updated on