Skip to Content
DesignDéfinitions d'agents déclaratifs — Portage depuis Claude Code 2.1.168

Définitions d’agents déclaratifs — Portage depuis Claude Code 2.1.168

Document de conception interne pour le portage du schéma d’agent déclaratif (markdown + frontmatter YAML) de Claude Code vers qwen-code, traitant l’issue #4821  et coordonnant avec le portage du workflow dans l’issue #4721  / PR #4732 .

État d’implémentation (tranches verticales)

La PR #4842  a livré les champs avec un chemin d’exécution de bout en bout à l’époque. La PR #4870  a ensuite remplacé le parseur YAML pour prendre en charge les scalaires de bloc. Cette PR de suivi s’appuie sur les deux : elle remplace le stringifier YAML (la PR #4870 l’avait laissé implémenté à la main — voir docs/design/yaml-parser-replacement.md), expose mcpServers + hooks sur SubagentConfig, et les connecte au runtime pour que les serveurs MCP et les hooks par agent se déclenchent réellement lorsqu’un sous-agent s’exécute.

ChampStatutNotes
permissionModelivré (#4842)fait le pont avec le approvalMode existant de qwen au moment du parsing
maxTurnslivré (#4842)connecté au chemin d’exécution runConfig.max_turns existant
color allowlistlivré (#4842)restreint le champ existant à l’ensemble _Y de CC + gestion du marqueur legacy auto
mcpServerslivré (suivi)round-trip YAML imbriqué fiable via eemeli/yaml stringify ; le runtime override fusionne les serveurs de session + agent via le wrapper Config du sous-agent + reconstruction forcée du registre d’outils
hookslivré (suivi)entrées éphémères de HookRegistry enregistrées au spawn du sous-agent, supprimées via onStop ; la v1 se déclenche globalement (pas de filtre par portée d’agent)
effortdifféréaucun paramètre effort au niveau du modèle n’existe encore dans les providers qwen
memorydifférél’auto-memory de qwen n’a pas encore de distinction de portée user/project/local
isolationdifféréla PR workflow #4732 possède le runtime ; la valeur par défaut par agent arrivera quand celle-ci sera livrée
initialPromptdifférénécessite le flag CLI --agent (pas d’infrastructure main-session-agent dans qwen)
skillsdifférénécessite la consommation de config.skills par SkillManager

Le dossier complet de rétro-ingénierie ci-dessous est conservé comme référence de conception pour les champs différés — les constantes de schéma, la sémantique DL7/Ig5, les messages d’erreur et la matrice de coordination avec le workflow sont toujours essentiels pour ce travail.


Phase 0 — Périmètre

ÉlémentValeur
Dernière version upstream vérifiéeClaude Code 2.1.168 (l’issue #4821 référence ≥ 2.1.167, nous sommes une version au-dessus)
Binaire natif/private/tmp/cc-2.1.168/package/claude (220 Mo)
Extraction des strings/private/tmp/cc-2.1.168/claude.strings (~342 k lignes)
Worktree.claude/worktrees/gifted-hamilton-684741
Branchelazzy/gifted-hamilton-684741 basée sur main @ 45efb1d3a
Hors périmètreCode du workflow de la PR #4732 (worktree séparé lazzy/lucid-pare-974192) — coordination via l’interface uniquement
Règle d’auteurL’auteur est LaZzyMan ; aucun trailer Co-Authored-By ou d’outillage IA dans les commits, PRs, issues ou commentaires (selon ~/.claude/CLAUDE.md)

Phase 1 — Résultats de la rétro-ingénierie

Toutes les affirmations ici ont été vérifiées de manière indépendante via grep sur claude.strings et ont survécu à des tests de réfutation adverses. Niveaux de confiance : C = Confirmé (preuve binaire directe), I = Inféré (synthétisé à partir de plusieurs faits confirmés), O = Ouvert (encore incertain).

Schéma — les 15 champs, réfutés et reconfirmés

Le shadow schema du frontmatter de l’agent est Ig5, utilisé à l’intérieur de ug5.agent pour la télémétrie tengu_frontmatter_shadow_unknown_key / _mismatch. Le chargeur de production est DL7 (parseAgentFromMarkdown), qui effectue une validation champ par champ implémentée à la main avec des messages d’erreur personnalisés. Un schéma JSON-form JL7 séparé (utilisé par fL7 / parseAgentFromJson) est plus strict, mais constitue un chemin de code différent (utilisé par --agents <json> et settings.agents).

#ChampType (Ig5 / DL7)RequisDéfautEnum / ContrainteConf
1namestring, non-emptyouinone — DL7: if(!T||typeof T!=="string")return nullC strings:308120, 309074
2descriptionstring, non-emptyouiJL7: .min(1, "Description cannot be empty")C strings:308120, 309074, 309076
3modelstringnonundefinedinherit (insensible à la casse) normalisé en littéral "inherit" ; sinon passé tel quel après rognageC strings:308120, 309075, 309076
4toolsstring|array (MDH union)nonundefinedtoken unique *undefined (signifie “hériter de tout”) ; dupliqué via AXH/FbKC strings:308120 (MDH/AXH), 309075
5disallowedToolsstring|array (MDH)nonundefined”Ignoré si tools est défini” (selon le texte describe) ; appliqué par les appelantsC strings:308120, 309075
6effortstring|integernonundefinedenum GN=["low","medium","high","xhigh","max"] OU int ; alias P37={med:"medium"}C strings:308120, 309075, GN/P37 inline
7permissionModestringnonundefinedenum $E = Gmq = [...kc]kc=["acceptEdits","auto","bypassPermissions","default","dontAsk","plan"] (6 valeurs)C strings:307649 (kc), 308120, 309075
8mcpServersz.unknown() (Ig5); array(jL7) (JL7)nonundefinedchaque élément : string OU record(string, MCPServerSpec) ; safeParse par élément dans DL7C strings:308120, 309075, 309076
9hooksz.unknown() (Ig5); _u() (JL7)nonundefinedvalidé paresseusement au runtime via TKO_u().safeParse (forme des hooks de settings.json)C strings:308120, 309073 (TKO), 309076
10maxTurnsunion(number, string, null)nonundefinedentier positif (parsé par W46 — accepte les numériques ou les chaînes numériques)C strings:308120, 309075 (W46), 309076
11skillsstring|array (MDH)non[] (émis)normalisé via ml(q.skills) = FbK(H) ?? [] ; pas de wildcard * (contrairement à tools)C strings:308120, 309075
12initialPromptstringnonundefineduniquement des espaces → undefined ; auto-soumis uniquement lorsque l’agent est la session principale (via --agent / settings), ignoré en tant que sous-agentC strings:308120, 309075
13memorystringnonundefinedenum ["user","project","local"]C strings:308120, 309075, 309076
14backgroundstring|bool (eiH=EL8)nonundefinedaccepte true / false / "true" / "false" ; seules les valeurs truthy sont normalisées à true, sinon undefinedC strings:308120, 309075
15isolationstringnonundefinedenum uniquement ["worktree"] (PAS ["none","worktree"] — il s’agit d’un schéma différent à strings:313284 pour les paramètres de session en arrière-plan)C strings:308120, 309075, 309076

Observation subtile qui a survécu à la réfutation : bien que skills soit “optionnel”, la clause d’émission de DL7 est ...I !== void 0 && {skills: I} et ml(undefined) retourne [] (non-undefined), donc l’enregistrement final émis contiendra skills: [] même si le frontmatter omet le champ. Cela affecte les vérifications d’égalité en aval — à signaler pour le portage de qwen-code.

Champs supplémentaires possibles au-delà des 15

#ChampTypeDéfautEnum / ContrainteConf
16colorstringundefinedenum _Y = ["red","blue","green","yellow","purple","orange","pink","cyan"] ; décrit comme "@internal — display color in the agents UI" ; les valeurs en dehors de _Y sont silencieusement ignorées au moment du parsing (DL7 émet ...z && typeof z === "string" && _Y.includes(z) && {color: z})C strings:308120, 309075, _Y inline
Il s’agit du seul nouveau champ frontmatter d’agent en dehors de la liste de #4821. Champs recherchés mais NON trouvés sur Ig5 / JL7 : version, tags, labels, category, icon, alias / aliases, experimental, deprecated, owner, author, homepage, displayName, shortDescription (tous ces champs n’apparaissent que dans le schéma de skill bg5 ou des identifiants non liés).

Loader — carte des fichiers et des fonctions

SujetFonctionEmplacementConf
Assembleur de registre de haut niveauQL (nom d’export getAgentDefinitionsWithOverrides)strings:309076C
Parcours du système de fichiers (partagé avec les skills/commands/output-styles)Gm (mémorisé via h6)strings:312887C
Découverte par fichier .mdd_q (= loadMarkdownFiles, ripgrep avec --files --hidden --follow --no-ignore --glob *.md, 3 s AbortSignal.timeout, fallback wY3 lorsque __("true"))strings:312887C
Analyseur par fichier (markdown)DL7 (= parseAgentFromMarkdown)strings:309074C
Analyseur par fichier (JSON)fL7 (= parseAgentFromJson), utilise le schéma JL7strings:309073C
Chargeur d’agents de pluginb0_ → par répertoire oR7 → par fichier sR7strings:308780, 308779C
Intégrés (Built-ins)naH() — émet [JqH=general-purpose, KL7=statusline-setup, …] plus YI=fork implicitestrings:309073, 308663C
Résolveur d’overridesDS() (= getActiveAgentsFromList) — voir Resolution Orderstrings:309073C
Invalidation du cacheu0_() (= clearAgentDefinitionsCache) — vide QL.cache + Gm.cachestrings:309073C
Observateur FS (chokidar)s_T()Q4_=s_T() à l’init du module (WB6)strings:316417C

Gm("agents", _) lit trois baseDirs (policySettings, userSettings, projectSettings), chacun étant tagué sur l’enregistrement, puis déduplique par inode (supprime les doublons de même inode provenant de liens symboliques / liens physiques, log Skipping duplicate file '<path>' from <source> (same inode already loaded from <firstSource>)). Télémétrie : tengu_dir_search avec managedFilesFound, userFilesFound, projectFilesFound, projectDirsSearched, subdir.

Ordre de résolution — priorité définitive

La fonction DS() filtre son entrée par source, puis itère un tableau d’ordre fixe dans une Map indexée par agentType. Comme Map.set écrase les valeurs, le DERNIER bucket touché gagne :

[built-in, plugin, userSettings, projectSettings, flagSettings, policySettings] ^ highest precedence
SourceOriginePriorité d’overrideConf
built-innaH() (codé en dur dans le binaire)1 (la plus basse)C strings:309073
pluginb0_agentsPath/agentsPaths par plugin2C strings:308780
userSettings~/.claude/agents/ (CLAUDE_CONFIG_DIR ou ~/.claude)3C strings:312887, 307489
projectSettings<cwd>/.claude/agents/ PLUS parcours iV_() jusqu’au homedir / git root4C strings:312887, iV_ inline
flagSettingsflag CLI --agents <json> (schéma qKO = h.record(h.string(), JL7()))5C strings:330190, 309076
policySettingsrépertoire géré par le système : macOS /Library/Application Support/ClaudeCode/.claude/agents, Linux /etc/claude-code/.claude/agents, Windows C:\Program Files\ClaudeCode\.claude\agents6 (la plus haute)C strings:307649 (H2), 312887

Les collisions sont résolues silencieusement — seul l’événement de télémétrie tengu_plugin_name_collision se déclenche (winner_source: T.at(-1)) ; aucun avertissement “X overrides built-in” n’est affiché à l’utilisateur. (strings:308742 hMH.)

Comportement subtil : iV_() parcourt de l’intérieur vers l’extérieur depuis cwd, mais comme Map.set applique la règle du dernier gagnant, l’arborescence externe .claude/agents/ l’emporte sur l’arborescence interne au sein de projectSettings. C’est surprenant — à ajouter aux questions en suspens.

Analyseur de frontmatter

QuestionRéponseConf
Bibliothèque utilisée ?Aucune — splitter fait maison lz appelant Bun.YAML.parse (via le wrapper l5H). Pas de gray-matter, js-yaml, ni front-matter dans le binaire.C strings:307902 (l5H), 307905 (lz), 110303 (erreurs Bun.YAML)
Regexn5H = /^---\s*\n([\s\S]*?)---\s*\n?/C strings:307905
Gestion des erreursÉchec du parsing YAML → nouvelle tentative avec normalisation des tabulations en 2 espaces ; si cela échoue encore, log Failed to parse YAML frontmatter in <file>: <err> en warn et retourne {frontmatter: {}, content: body} (ne lance JAMAIS d’exception)C strings:307905, 151839
Extraction du corpsDécoupe de chaîne simple H.slice(K[0].length) après le --- de fermeture ; normalisé ensuite par v$H (probablement suppression du saut de ligne initial)C strings:307905
Partagé entre agents / skills / commands / output-styles ?Oui — le même lz est réutilisé par Iq_ (chargeur de skills), f13 (chargeur de commands obsolète) et le chargeur d’agents via Gmd_qC strings:312690
Validateur de schémaZod v4 (bundled). Marqueurs spécifiques à la v4 looseObject, treeifyError, prettifyError, toJSONSchema présentsC strings:141270-141395, 141586
Mode de validationShadowahH("agent", frontmatter) exécute ug5.agent().strict().safeParse() pour la télémétrie uniquement ; DL7 ignore le résultat et procède à sa propre validation champ par champ. L’objet frontmatter permissif est la source de vérité au runtime.C strings:308120 (ahH/ug5), 309074 (DL7 appelle mais ignore)
Événements de télémétrietengu_frontmatter_shadow_unknown_key, tengu_frontmatter_shadow_mismatch (dédupliqués via le Set A37 en mémoire)C strings:154634, 154636

Câblage — Outil Agent + flag CLI

CoucheFonctionConf
Schéma de l’outil Task/Agent ($_3)Déclare subagent_type: string.optional() ; si omis, retombe sur general-purpose (ou fork si AI() renvoie true)C strings:~309220
Recherche de sous-agentactiveAgents.find(a => a.agentType === requestedType) sur toolUseContext.options.agentDefinitions.activeAgentsC strings:~309220
Fallback approximatif (Fuzzy)MWK(s) = s.normalize("NFKC").toLowerCase().replace(/[\p{White_Space}\p{Pd}_]+/gu, "") ; correspondance ambiguë → AgentTypeError ; nouvelle correspondance exacte → tengu_subagent_type_normalizedC strings:~309220
Porte de permissionlV_(toolPermissionContext, "Task", agentType) — refus → Agent type '<x>' has been denied by permission rule 'Task(<x>)' from <source>.C strings:~309220
Source du system-promptLe corps Markdown devient getSystemPrompt: () => body + ('\n\n' + UVH(agentType, memoryScope) when memory enabled) — closure capturée au moment du parsingC strings:309074-6 (DL7)
Rendu du thread principalPp({mainThreadAgentDefinition, …}) — si l’agent a appendSystemPrompt: true (le built-in fourre-tout claude), le corps est ajouté à celui par défaut ; sinon il REMPLACE celui par défautC strings:311015
CLI --agent <name>Déclaré via Commander ; gestionnaire d’action if(I) process.env.CLAUDE_CODE_AGENT = I; — injecte dans la variable d’env, lu ailleurs dans appState.agent. Également enregistré dans le fichier pid.C strings:330190, 142138
CLI --agents <json>Flag séparé ; enregistrement JSON {name: {description, prompt, …}} validé par qKO = h.record(h.string(), JL7()) ; rejoint le même registre activeAgents avec source: flagSettingsC strings:330190, 309076

Cycle de vie — chargement à froid + rechargement à chaud

AspectComportementConf
Chargement à froidParesseux (Lazy) — QL est mémorisé via h6 (wrapper de cache) ; le premier accès lit le système de fichiers + les plugins, les accès suivants retournent le cacheC strings:309076
Mécanisme de rechargement à chaudwatcher chokidar s_T() enregistré à l’init du module (WB6) ; surveille .claude/agents (utilisateur + projet) ainsi que les répertoires skills + commandsC strings:316417
Flags du watcherpersistent:true, ignoreInitial:true, depth:2, awaitWriteFinish:{stabilityThreshold,pollInterval}, ignored:(p,s) => s?.isFile() ? !p.endsWith(".md") : false, usePolling:kZ4 (true pour macOS), événements add/change/unlinkC strings:316417
Debounce300 ms (l_T = 300) ; le handler appelle RIH(), Vv(), u0_(), … — u0_() invalide le cache des agentsC strings:316417, 309073
Polling adaptatifactif = intervalle n_T = 2000 ms ; inactif (pas d’interaction pendant r_T = 60000 ms) → i_T = 30000 ms ; recrée l’instance chokidar lors du basculementC strings:316417
Commande slash /agentsUI local-jsx pour gérer les agents (Library/create/edit/delete/run) — **N’**est PAS une commande de rescanC strings:314593
Commande slash /reload-pluginsRéexécute QL(W8()), recompte les agents ; couvre les agents provenant de plugins (que chokidar ne surveille PAS)C strings:314595, 190948
Autres chemins d’invalidationclearSessionCaches (utilisé par /clear) appelle également u0_()C strings:313246

Questions ouvertes (Phase 1)

#QuestionConfChemin de résolution
Q1L’omission de color dans #4821 est-elle intentionnelle (c’est @internal) ou un oubli ?OConsidérer comme intentionnel — porter le champ mais le marquer comme internal/UI-only
Q2Le comportement permissif de DL7 (background accepte des strings, maxTurns accepte des strings) est-il une fonctionnalité documentée pour l’utilisateur ou un hack de rétrocompatibilité ?OLe reproduire pour assurer la parité, mais avertir dans la documentation du port
Q3Pourquoi l’enum isolation ["worktree"] s’applique-t-elle uniquement aux agents alors que le schéma de configuration de background-session accepte ["none","worktree"] ?OProbablement “no isolation” = champ omis ; documenter explicitement
Q4Le flag --agents <json> (flagSettings) se situe-t-il intentionnellement à la précédence 5 (au-dessus de project, en dessous de policy) ?Oqwen-code peut ignorer ce flag dans la v1 et reporter la décision
Q5Push innermost-first par iV_ + Map.set last-wins → outer-tree wins pour les collisions de projectSettings. Piège (footgun) ou intentionnel ?Oqwen-code devrait adopter la sémantique innermost-wins pour éviter ce piège

Phase 2 — Plan d’implémentation pour qwen-code

État actuel — vue d’ensemble en un paragraphe

qwen-code dispose déjà d’une infrastructure de subagents substantielle : SubagentManager (packages/core/src/subagents/subagent-manager.ts) implémente les opérations CRUD sur les fichiers markdown+YAML frontmatter dans .qwen/agents/ (projet) et ~/.qwen/agents/ (utilisateur), soutenu par un parseur YAML personnalisé (packages/core/src/utils/yaml-parser.ts — pas de dépendance gray-matter / yaml, confirmé par package.json). SubagentConfig (packages/core/src/subagents/types.ts:41-122) possède déjà name, description, tools, disallowedTools, approvalMode, systemPrompt, model, runConfig, color, background. SubagentLevel prend déjà en charge cinq scopes (session, project, user, extension, builtin) avec la précédence session > project > user > extension > builtin (subagent-manager.ts:189-220). L’outil Agent (packages/core/src/tools/agent/agent.ts) déclare subagent_type et rafraîchit dynamiquement son enum de schéma via subagentManager.changeListener. Un pont convertClaudeAgentConfig() existe déjà dans packages/core/src/extension/claude-converter.ts:162-220 avec un mapping de noms d’outils et un mapping permissionMode → approvalMode. Le manque est : (a) le schéma omet 8 champs de #4821 (effort, permissionMode en tant que champ de premier ordre, mcpServers, hooks, maxTurns au niveau supérieur, skills, initialPrompt, memory, isolation) ; (b) pas de flag CLI --agent <name> ; (c) pas de rechargement à chaud de type chokidar (l’invalidation de type extension existe, mais pas pour les agents du système de fichiers) ; (d) maxTurns est actuellement imbriqué sous runConfig.max_turns — il doit être promu au niveau supérieur conformément à #2409.

Décisions architecturales

D1. Réutiliser le yaml-parser existant pour le frontmatter

Décision : Réutiliser packages/core/src/utils/yaml-parser.ts (déjà utilisé par SubagentManager.parseSubagentContent et le chargeur de skills). Justification : Le lz de Claude Code est le même parseur partagé utilisé pour les skills + commands + agents ; qwen-code reproduit déjà ce modèle. Ajouter gray-matter ou js-yaml représente une complexité inutile. Le parseur existant gère la division --- … --- et reste silencieux en cas d’entrée malformée (ce qui correspond à l’approche warn-and-return-empty de lz).

D2. Ordre de résolution / précédence

Décision : Utiliser `session > project (.qwen/agents/) > user (~/.qwen/agents/)

extension > builtin— c'est-à-dire **conserver l'ordre existant de qwen-code SubagentLevel, et ne PAS reproduire les bucketsflagSettings/policySettingsde Claude Code dans la v1**. **Justification :** LespolicySettings de Claude Code (répertoire géré) relèvent d'un déploiement entreprise que qwen-code n'a pas. Les agents injectés par flag (—agents ) sont une fonctionnalité pour utilisateurs avancés qui peut arriver en P4. La précédence à cinq niveaux existante de qwen-code couvre déjà les cas qui importent pour #4821 : project override user override built-in. Le niveau extension` s’insère proprement entre user et builtin.

D3. Validation — conserver le SubagentValidator existant

Décision : Étendre SubagentValidator (packages/core/src/subagents/) pour valider les huit nouveaux champs. Ne PAS introduire zod à moins que le pipeline de skillManager ne l’utilise déjà ; si le validateur existant est fait maison (hand-rolled), gardez-le fait maison. Justification : Le Ig5 de Claude Code est uniquement pour l’ombre (shadow-only) — la validation à l’exécution est le DL7 fait maison. Reproduire ce modèle garde les messages d’erreur lisibles (par ex. Agent file <path> has invalid permissionMode '<x>'. Valid options: …) sans ajouter une autre dépendance. Si skillManager utilise déjà zod, suivez ce choix par cohérence — à déterminer (TBD) en lisant le code des skills lors de la préparation de la P1.

D4. Rechargement à chaud — reporter ; s’appuyer sur le chargement à froid + rechargement explicite

Décision : La v1 ne livre PAS de watcher chokidar. Les hooks d’invalidation de cache existent déjà (subagentManager a changeListener et un rafraîchissement explicite piloté par CRUD). Le rechargement au niveau projet se produit au démarrage de la session ; les modifications en session via l’UI /agents invalident. Une commande slash /reload-agents (ou en chevauchant /reload-plugins) peut arriver en P4 si la demande des utilisateurs existe. Justification : Le rechargement à chaud via un watcher FS est coûteux (chokidar ajoute une boucle de polling avec une planification adaptative — l’implémentation de Claude Code à elle seule comprend ~150 lignes de bookkeeping). Le chargement à froid au démarrage est amplement suffisant pour la v1 et correspond à la façon dont SubagentManager est câblé aujourd’hui. Cela ouvre la porte pour la P4.

D5. Câbler le flag CLI --agent <name> — v1 dans le périmètre

Décision : Ajouter --agent <name> à packages/cli/src/config/config.ts CliArgs. Comportement : recherche dans le registre résolu, définit l’agent comme l’agent du thread principal, lève une erreur claire si le nom ne se résout pas. Reproduit la sémantique de Claude Code (remplace le system prompt par défaut sauf si l’agent a appendSystemPrompt: true). N’utilise PAS d’indirection via la variable d’env CLAUDE_CODE_AGENT — l’objet Config de qwen-code peut le porter directement. Justification : C’est le point d’entrée utilisateur pour #4821 — sans cela, les agents déclaratifs ne sont accessibles que via le paramètre subagent_type de l’outil Agent, ce qui est trop indirect pour un cas d’usage “définir mon agent par défaut”. --agents <json> (au pluriel) peut être reporté à la P4.

D6. Coordination de Workflow.agentType — contrat d’interface

Décision : Exposer une interface de résolution stable que le createProductionDispatch de la PR #4732 pourra appeler lorsqu’elle arrivera. Plus précisément :

ContratPropriétaire
Le name du frontmatter EST la chaîne agentType du workflow (égalité de clé, sensible à la casse)cette PR
Le plancher hardcodé disallowedTools du workflow ([SEND_MESSAGE, EXIT_PLAN_MODE], reproduit depuis l’upstream Tg8 ; vérifié dans la PR #4732 en tant que ToolNames.SEND_MESSAGE, ToolNames.EXIT_PLAN_MODE) fait l’UNION avec les disallowedTools au niveau agent — le plancher est toujours appliqué, même lorsque la définition de l’agent définit toolsla PR workflow consomme
Le opts.isolation par appel override le isolation: 'worktree' par défaut au niveau agentla PR workflow consomme
model, effort, permissionMode, maxTurns de la définition de l’agent override les valeurs par défaut du workflow lorsqu’ils sont définisla PR workflow consomme
Le corps de l’Agent devient le systemPrompt du subagent ; le WORKFLOW_SUBAGENT_SYSTEM_PROMPT du workflow est le fallback lorsque agentType ne se résout pasla PR workflow consomme
Lorsque agentType n’est pas défini ou échoue à se résoudre, le workflow retombe sur le subagent de workflow intégré (graceful, pas de throw)la PR workflow consomme
Résolution de la contradiction #4721 / #4821 (priorité entre tools et disallowedTools) : ce port configure le registre des agents de sorte que disallowedTools est toujours transporté séparément de tools. La règle « ignoré si tools est défini » du tableau de #4821 est appliquée par les appelants de l’Agent-tool (c’est-à-dire lors de la construction du ToolConfig du sous-agent), et non au moment du parsing. Cela permet au workflow de toujours faire l’union de son plancher avec disallowedTools, indépendamment du fait que l’agent définisse tools ou non. Le registre des agents est un simple transporteur de données ; les règles de priorité résident au niveau du point de dispatch. Cela résout le conflit apparent entre la règle « ignoré » de #4821 et la règle « union » de #4721.

Canonicalisation des noms d’outils : Utiliser ToolNames.SEND_MESSAGE et ToolNames.EXIT_PLAN_MODE (vérifiés par rapport au diff de la PR #4732), exportés en tant que constantes nommées depuis packages/core/src/agents/runtime/workflow-orchestrator.ts une fois intégrés. Le port de declarative-agents lui-même n’a PAS besoin d’importer ceux-ci — ils constituent le plancher du workflow, appliqués au niveau du point de dispatch du workflow.

Structure des modules

CheminNouveau / ModifiéObjectif
packages/core/src/subagents/types.tsModifiéAjouter 8 nouveaux champs à SubagentConfig : effort, permissionMode (correspond déjà via approvalMode — garder les deux ? voir D7 ci-dessous), mcpServers, hooks, maxTurns (promu au niveau supérieur, déprécier runConfig.max_turns), skills, initialPrompt, memory, isolation
packages/core/src/subagents/subagent-manager.tsModifiéÉtendre parseSubagentContent / serializeSubagent pour assurer l’aller-retour des nouveaux champs ; étendre les appels de SubagentValidator
packages/core/src/subagents/subagent-validator.ts (chemin supposé)ModifiéAjouter la validation par champ correspondant aux messages d’erreur de DL7 : Agent file <path> has invalid permissionMode '<x>'. Valid options: … etc.
packages/core/src/subagents/agent-frontmatter-schema.tsNouveauSource unique de vérité pour les constantes d’énumération : EFFORT_VALUES, PERMISSION_MODE_VALUES, MEMORY_VALUES, ISOLATION_VALUES, COLOR_VALUES. Reproduit Claude Code 2.1.168 à l’identique.
packages/core/src/subagents/builtin-agents.tsModifiéLes nouveaux champs ont par défaut la valeur undefined ; aucun changement de comportement
packages/core/src/tools/agent/agent.tsModifiéLire les nouveaux champs depuis le SubagentConfig résolu lors de la construction des options du sous-agent (model, maxTurns, permissionMode, effort) ; faire passer la sémantique de surcharge par appel de isolation pour #4721
packages/cli/src/config/config.tsModifiéAjouter le flag --agent <name> ; résoudre par rapport à SubagentManager au démarrage ; erreur si le nom ne se résout pas
packages/cli/src/config/config.test.tsModifiéTests pour la résolution du flag --agent + chemin d’erreur
packages/core/src/extension/claude-converter.tsModifiéAjouter le mapping pour les nouveaux champs lors de l’importation des fichiers .md de Claude (mcpServers, hooks, maxTurns au niveau supérieur, memory, isolation, etc.)
packages/core/src/subagents/agent-frontmatter-schema.test.tsNouveauTests de snapshot pour les listes d’énumérations ; tests de parsing/sérialisation aller-retour
packages/core/src/subagents/subagent-manager.test.tsModifiéTests pour la validation des nouveaux champs, la priorité, les messages d’erreur
packages/core/src/tools/agent/agent.test.tsModifiéTests pour l’intégration des nouveaux champs dans le runtime du sous-agent
docs/cli/agents.md (si existant) ou docs/declarative-agents.mdNouveauRéférence destinée aux utilisateurs : schéma à 16 champs + exemples

D7. permissionMode vs approvalMode — faire le pont, pas remplacer

Décision : Accepter à la fois permissionMode (compatible Claude) et l’actuel approvalMode (compatible qwen) dans le frontmatter. Au parsing, si permissionMode est défini, le mapper vers approvalMode en utilisant le tableau existant dans claude-converter.ts:195-208 (default → default, plan → plan, acceptEdits → auto-edit, dontAsk → default, bypassPermissions → yolo). Si les deux sont présents, approvalMode l’emporte (plus spécifique à qwen-code) et émettre un événement de télémétrie de type tengu_frontmatter_shadow_* indiquant que les deux ont été définis. Justification : Préserve la rétrocompatibilité avec les .qwen/agents/*.md existants qui utilisent approvalMode, tout en acceptant le permissionMode de Claude Code à l’identique afin que les utilisateurs puissent intégrer des fichiers d’agents Claude Code sans modification.

Tableau de correspondance du schéma

Champ Claude Code 2.1.168Champ qwen-codeAdaptationNotes
namenameaucuneidentique, requis
descriptiondescriptionaucuneidentique, requis
modelmodelaccepter inherit, fast, haiku, sonnet, opus, ou authType:model-idqwen-code supporte déjà le vocabulaire plus large ; inherit est nouveau
toolstoolsaccepter string|array ; * → undefined (hériter de tout)déjà supporté en tant qu’array ; ajouter la gestion de string + *
disallowedToolsdisallowedToolsaccepter string|array ; toujours transporté séparément de toolsrègle de priorité (#4821 « ignoré si tools est défini ») appliquée par les appelants, pas le parser
efforteffort (nouveau)enum low/medium/high/xhigh/max + entier ; alias med → mediuml’effet runtime est spécifique à qwen (mapper vers le paramètre d’effort de réflexion existant si présent, sinon stocker et ignorer)
permissionModepermissionMode (nouveau) + fait le pont vers approvalModeenum acceptEdits/auto/bypassPermissions/default/dontAsk/plan ; tableau de correspondance selon D7accepter le format Claude à l’identique
mcpServersmcpServers (nouveau)array de (string | {name: spec}) ; valider par élément, supprimer les mauvaises entrées avec un warningintégration dans le runtime MCP dans P4
hookshooks (nouveau)objet correspondant à la forme des hooks de settings.jsonintégration dans le runtime des hooks dans P4
maxTurnsmaxTurns (nouveau niveau supérieur)entier positif ; accepter les chaînes numériques pour la paritépromu depuis runConfig.max_turns ; garder la forme imbriquée comme alias déprécié
skillsskills (nouveau)array de noms de skills ; chaîne séparée par des virgules également acceptéeruntime : précharger via skillManager au démarrage de l’agent
initialPromptinitialPrompt (nouveau)string ; uniquement des espaces → undefined ; ne se déclenche que lorsque l’agent est la session principaleintégré via le chemin du flag --agent
memorymemory (nouveau)enum user/project/local ; charge depuis .qwen/agent-memory/<name>/ etc.runtime dans P4
backgroundbackgroundaccepter bool ou string "true"/"false" ; uniquement truthy → truedéjà supporté ; assouplir les règles de parsing
isolationisolation (nouveau)enum uniquement ["worktree"]runtime géré par la PR workflow (#4732 P3+) ; le registre ne fait que transporter le champ
color (non documenté #16)colorenum _Y = ["red","blue","green","yellow","purple","orange","pink","cyan"] ; valeurs en dehors supprimées silencieusementdéjà dans SubagentConfig de qwen ; durcir la validation pour correspondre à la liste d’autorisation de Claude Code

Plan de tests TDD

BlocFichier de testCe qui est vérifié
Constantes enum du schémaagent-frontmatter-schema.test.ts (nouveau)EFFORT_VALUES, PERMISSION_MODE_VALUES, MEMORY_VALUES, ISOLATION_VALUES, COLOR_VALUES correspondent byte pour byte à Claude Code 2.1.168 (snapshot)
Parser — cas nominalsubagent-manager.test.tsAnalyse round-trip de .qwen/agents/test.md avec les 16 champs → l’enregistrement émis a la forme attendue
Parser — champs requissubagent-manager.test.tsL’absence de name retourne null + log warn ; l’absence de description retourne null + log warn
Parser — validation des enumssubagent-manager.test.tsDes valeurs invalides pour permissionMode / memory / isolation / effort / color émettent chacune un warn spécifique (correspondant au libellé DL7) et le champ est ignoré
Parser — types de champs tolérantssubagent-manager.test.tsbackground: "true"true ; maxTurns: "5"5 ; effort: "med""medium" ; tools: "Read,Edit"["Read","Edit"] ; tools: "*" → undefined
Parser — liste blanche des couleurssubagent-manager.test.tscolor: "magenta" est ignoré silencieusement (pas d’erreur), color: "blue" est conservé
Particularité du champ skillssubagent-manager.test.tsL’omission de skills résulte en skills: [] (correspond au comportement d’émission DL7 de Claude Code)
Précédence de résolutionsubagent-manager.test.tsMême name dans projet + utilisateur → le projet l’emporte ; dans utilisateur + intégré → l’utilisateur l’emporte ; dans extension + intégré → l’extension l’emporte
Déduplication des inodessubagent-manager.test.tsDeux chemins vers le même inode (symlink) → un seul enregistrement, log émis
Pont permissionModesubagent-manager.test.tspermissionMode: bypassPermissionsapprovalMode: yolo résolu ; les deux définis → approvalMode l’emporte + télémétrie
Flag CLI --agentpackages/cli/src/config/config.test.tsLe flag définit l’agent du thread principal ; un nom non résolu lève une erreur avec Agent type '<x>' not found. Available agents: …
Fallback flou de l’outil Agentagent.test.tssubagent_type: "Test_Engineer" résout vers un test-engineer enregistré via la normalisation NFKC-lowercase
Erreur not-found de l’outil Agentagent.test.tssubagent_type non résolu → le message d’erreur correspond à Agent type '<x>' not found. Available agents: <list>
Contrat de workflowagent-frontmatter-schema.test.tsL’interface exportée getAgentByName(name) retourne le SubagentConfig complet incluant isolation, disallowedTools, model, effort, permissionMode, maxTurns (consommable par le workflow PR #4732)

Plan de PR par phases

PhaseTitrePérimètreBloqué par
P1feat(core) : champs de schéma d'agent déclaratif (effort, permissionMode, maxTurns au niveau supérieur, memory, isolation, liste blanche des couleurs)Ajout des champs à SubagentConfig ; extension du parser + validateur + sérialiseur ; dépréciation de runConfig.max_turns ; ajout du module de constantes enum ; testsAucun
P2feat(core) : branchement des nouveaux champs d'agent dans le runtime de l'outil AgentAcheminement de model, effort, maxTurns, du pont permissionMode/approvalMode vers le site d’appel AgentTool.execute()AgentHeadless.create() ; testsP1
P3feat(cli) : flag --agent pour la sélection de l'agent du thread principalAjout de --agent <name> à CliArgs ; résolution au démarrage ; chemin d’erreur ; testsP1
P4(optionnel, scope-creep) feat(core) : mcpServers + hooks + skills + initialPrompt + runtime memoryBranchement des quatre champs “métadonnées uniquement en v1” vers des effets runtime réelsP1, plus les sous-systèmes skill/MCP/hook

Chaque PR cible un delta ≤ 800 LOC (tests exclus) ; P1 est le plus important avec ~600 LOC de validateur + tests.


Phase 3 — Matrice de coordination avec le portage du workflow (#4721 / PR #4732)

Fonctionnalité declarative-agentsInteraction avec le workflowPropriétaireBloqué par
Champ name comme clé de registreChaîne de recherche opts.agentType du workflow (#4721  explicite)cette PR définit le contrat du registre ; la PR workflow le consommeaucun — la forme du registre peut se stabiliser en premier
Champ disallowedTools sur l’agentLe workflow fait l’UNION avec le plancher codé en dur [SEND_MESSAGE, EXIT_PLAN_MODE] (selon #4721  §2 — vérifié par rapport au diff de la PR #4732 : ToolNames.SEND_MESSAGE, ToolNames.EXIT_PLAN_MODE)cette PR porte le champ ; la PR workflow fait l’union au dispatchla PR workflow #4732 P3 est mergée
Champ tools sur l’agentLe workflow transmet verbatim au ToolConfig.tools du sous-agentcette PR porte le champ ; la PR workflow le branchePR workflow #4732 P3
Champ model sur l’agentLe opts.model du workflow écrase par appel ; le model de l’agent est la valeur par défautcette PR porte le champ ; la PR workflow résout la précédencePR workflow #4732 P3
Champ effort sur l’agentL’écrasement au site d’appel du workflow l’emporte ; fallback sur la valeur par défaut de l’agentcette PR porte le champ ; la PR workflow résoutPR workflow #4732 P3
Champ permissionMode sur l’agentMappe vers l’approvalMode du sous-agent au dispatch ; l’écrasement au site d’appel du workflow l’emportecette PR porte le champ via le pont D7 ; la PR workflow le branchePR workflow #4732 P3
Champ maxTurns sur l’agentRemplace le WORKFLOW_SUBAGENT_MAX_TURNS = 50 codé en dur du workflow lorsque l’agent le définitcette PR porte le champ ; la PR workflow résout la précédencePR workflow #4732 P3
Champ isolation: 'worktree' sur l’agentValeur par défaut ; l’écrasement par appel opts.isolation prime (#4721  §3)cette PR porte le champ ; la PR workflow possède le runtimePR workflow #4732 P3+ (lève actuellement une erreur dans P1)
Champ initialPrompt sur l’agentLe workflow ne l’utilise pas (ne se déclenche que lorsque l’agent est la session principale via --agent)cette PR + CLIaucun (indépendant)
memory, mcpServers, hooks, skillsLe workflow n’a pas de traitement spécial au-delà de la transmission au runtime du sous-agentcette PR porte les champs ; branchement runtime dans P4 / futurPRs futures
Mises à jour de EXCLUDED_TOOLS_FOR_SUBAGENTSLa PR workflow #4732 ajoute WORKFLOW à l’ensemble (selon la découverte issue/contexte PR — bien qu’une réfutation adversaire note que ce n’est PAS encore dans agent-core.ts sur main, seulement dans worktree)la PR workflow en est propriétaire ; cette PR n’est pas modifiéeaucun
Forme canonique du nom d’outil pour le plancher du workflow (ToolNames.SEND_MESSAGE)Cette PR n’importe pas les constantes du plancher ; elle ne porte que les chaînes disallowedTools telles qu’écrites. La PR workflow est propriétaire de la canonisation.PR workflowPR workflow #4732
Ordre de livraisonCette PR (P1+P2+P3) est livrée indépendamment du workflow. La PR workflow #4732 P3 est conditionnée à l’importabilité d’un résolveur de type getAgentByName() de cette PR.parallèle jusqu’à P3 du workflowP3 du workflow lit depuis les exports de cette PR
Pas de blocage circulaire : cette PR et la PR de workflow peuvent être fusionnées en parallèle lors de leurs phases P1/P2. Elles se synchronisent au niveau de workflow-P3, qui a besoin du résolveur de registre de cette PR. Si cette PR est fusionnée en premier, workflow-P3 lira à partir de celle-ci. Si la PR de workflow est fusionnée en premier, elle sera livrée avec la recherche subagent_type existante (retournant les valeurs par défaut du workflow en cas d’absence) et basculera vers le résolveur plus riche une fois cette PR fusionnée.

Phase 4 — Risques et questions ouvertes

Risques

#RisqueMesure d’atténuation
R1Dérive du schéma entre les versions mineures de Claude Code (2.1.168 → 2.1.x)Épingler le module de constantes d’énumération à “vérifié avec 2.1.168” avec un commentaire de documentation ; relancer le strings-grep sur les nouvelles versions dans le cadre de la skill feature-reverse
R2runConfig.max_turnsmaxTurns de premier niveau est un changement de schéma incompatible pour les fichiers .qwen/agents/*.md existantsConserver la forme imbriquée comme alias obsolète avec une dépréciation d’un cycle ; émettre un warn lors de l’analyse, documenter dans le CHANGELOG
R3Perte de données lors de la conversion aller-retour permissionModeapprovalMode (Claude a 6 modes, qwen en a environ 4)Mapper explicitement les deux directions selon D7 ; émettre de la télémétrie en cas de définition double ; ne PAS réécrire silencieusement lors de la sauvegarde
R4Les nouveaux champs (hooks, mcpServers, skills, memory) sont présents dans le registre mais n’ont pas d’implémentation runtime dans v1 → les utilisateurs peuvent les définir sans obtenir d’effet, silencieusementDocumenter clairement le périmètre de la v1 ; émettre un log d’information unique par agent lorsqu’un champ “présent mais sans runtime” n’est pas vide
R5La vérification adversarial-verify a signalé que EXCLUDED_TOOLS_FOR_SUBAGENTS n’inclut PAS WORKFLOW sur main — cela peut signifier que le portage du workflow n’est pas encore fusionné ou que la garde recursive-fanout est manquanteConfirmer avec l’auteur de la PR de workflow (LaZzyMan = moi-même) que la garde est intégrée avec la PR #4732, et non dans ce portage
R6Le comportement projectSettings où l’arbre externe l’emporte sur l’arbre interne (Q5) est un piège s’il est reproduitqwen-code choisit explicitement l’arbre le plus interne l’emporte ; testé via le fixture R5
R7Le champ color est documenté comme @internal dans le texte descriptif du binaire — nous portons peut-être quelque chose qu’Anthropic ne prend explicitement pas en chargeLe porter mais le marquer @internal dans la documentation de qwen-code également ; le traiter comme UI uniquement ; ne pas l’exposer dans la documentation de référence destinée aux utilisateurs

Questions ouvertes — résolutions proposées

#QuestionRésolution
Q1L’omission de color dans #4821 est-elle intentionnelle ?Considérer comme intentionnel. Porter le champ ; ne PAS le mentionner dans la documentation destinée aux utilisateurs, sauf comme “disponible, interne”.
Q2Comportement tolérant de DL7 : documenter ou bricoler ?Le reproduire. Accepter background: "true", maxTurns: "5", effort: "med" pour assurer la parité, même si ce n’est pas documenté. Ajouter des tests.
Q3Pourquoi l’énumération d’isolation diffère-t-elle entre le schéma d’agent et le schéma de session en arrière-plan ?Documenter la divergence dans un commentaire de code ; “no isolation” = champ omis, pas une valeur d’énumération.
Q4--agents <json> (pluriel, flagSettings) doit-il être intégré en v1 ?Reporter à P4. Surface CLI pour les utilisateurs avancés ; la v1 livre uniquement --agent <name> (singulier), ce qui est ce dont #4821 se soucie.
Q5Priorité entre arbre interne et arbre externe pour les .qwen/agents/ imbriqués ?L’arbre le plus interne l’emporte. Outrepasser le comportement accidentel de Claude Code où l’externe l’emporte. Fixture de test dans P1.
Q6Priorité entre tools et disallowedTools : #4821 dit “ignoré si tools est défini” ; #4721 dit “union avec le plancher du workflow”Le registre est une donnée passive. Le parser préserve les deux champs indépendamment. Les règles de priorité se trouvent au point de dispatch (Agent tool / workflow). Résout la contradiction.
Q7Forme canonique du nom d’outil pour le plancher disallowedTools du workflow — vérifié par rapport à la PR #4732 comme étant ToolNames.SEND_MESSAGE, ToolNames.EXIT_PLAN_MODENe concerne pas cette PR — relève de la PR de workflow. Documenter uniquement dans la matrice de coordination.
Q8La résolution de clôture de #2409 affecte-t-elle quelque chose ?Hériter des directives de #2409 “promouvoir model + maxTurns au premier niveau”. Déjà intégré dans ce plan.
Q9Les agents de niveau extension dans la priorité SubagentLevel existante de qwen-code doivent-ils rester au-dessus de builtin (actuel) ou en dessous (Claude Code n’a pas d’équivalent) ?Garder extension > builtin. Les extensions sont installées par l’utilisateur ; les intégrations sont les valeurs par défaut du fournisseur. L’installé par l’utilisateur l’emporte.
Q10Les issues #4821, #4721, #4732 sont-elles entièrement spécifiées pour le contrat proposé par ce document ?Poster un commentaire de coordination sur #4821 liant ce document, résumant les décisions champ par champ, et demandant aux mainteneurs d’accuser réception : (a) parité du schéma avec les 16 champs de Claude Code 2.1.168, (b) pont D7 permissionMode/approvalMode, (c) ordre de priorité D2, (d) résolution de la contradiction tools/disallowedTools par le registre en tant que donnée passive.

Actions de coordination

#Action
A1Poster un résumé champ par champ + 5 décisions sur #4821 pour accusé de réception des mainteneurscommentaire sur #4821
A2Créer un lien croisé de ce document depuis #4721 en notant la matrice de la Phase 3commentaire sur #4721
A3Une fois la P1 de ce portage fusionnée, notifier #4732 pour passer au résolveur plus richecommentaire sur la PR #4732 (quand ce sera prêt)
A4Relancer strings-grep sur la prochaine version mineure de Claude Code pour la détection de dérive du schémacron job de la skill feature-reverse (manuel jusqu’à nouvel ordre)
Last updated on