From 6f54d765c43ce8150d97498a6d0c0058e2601801 Mon Sep 17 00:00:00 2001 From: Arthur Ria Date: Tue, 25 Aug 2026 15:08:13 +0200 Subject: [PATCH] L5.1 : fenetre verbatim sur le blob data de get_workflow_details La reponse embarquait la definition EasyBuilder complete, au-dela du seuil de rejet du client MCP (~70 000 caracteres, D24). Deux parametres de fenetre au schema (D23) : max_data_chars (defaut 20 000) et data_offset (defaut 0). Les metadonnees du workflow restent completes dans chaque tranche ; seul `data` est fenetre, et dataTotalChars est porte par toute reponse. La tranche est verbatim -- decoupe de chaine, rien d'autre. Ne jamais resumer ni parser ce blob : la concatenation des tranches doit reconstituer la definition a l'octet pres. Verifie : 20 000 + 20 000 + 20 000 + 11 512 = 71 512, concatenation identique au blob d'origine (premiers et derniers caracteres compris). Mesures avant/apres (protocole, LIMAGRAIN, longueur de content[0].text) : StackerCrane_LocationIsAccessibleByExtractor_PR 79 092 -> 23 117 (blob data : 71 512, desormais annonce par dataTotalChars) CST_SendRejectContainersToPK (CustomApp) 101 816 -> 23 023 (blob data : 92 362) StackerCrane_LoadMovementForOutboundTask_PR 10 587 -> 10 652 (blob de 9 013 : sous le defaut, objet workflow identique a l'octet pres, ni truncated ni hint -- les +65 caracteres sont les trois champs de fenetre, contrat "total toujours porte" de D24) Gardes de valeur dans le code de l'outil, pas dans le wrapper (D23 ne valide que les noms) : data_offset: -5 et max_data_chars: 1.5 echouent avant tout appel reseau avec un message nommant l'attendu. Baseline preservee : 23 outils, 6 resources. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 16 ++++++++ DECISIONS.md | 18 +++++++- ROADMAP.md | 7 ---- src/tools/workflow-tools.js | 82 +++++++++++++++++++++++++++++++++---- 4 files changed, 108 insertions(+), 15 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index dd767ff..032245e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -190,6 +190,22 @@ ne les augmentez pas à l'aveugle. --- +## Sorties bornées (D24) + +Une réponse d'outil de plus de ~70 000 caractères est **rejetée par le client +MCP**. Les outils qui peuvent dépasser ce seuil bornent et **signalent** : +`truncated: true` (jamais `false`), `hint` actionnable, `returned`, et le total +avant la coupe. Réutilisez ce vocabulaire, n'en inventez pas un second. + +**`get_workflow_details` fenêtre le blob `data`** (la définition EasyBuilder : +71 512 caractères sur un StackerCrane, 92 362 sur un gros `CST_*`) : +`max_data_chars` (défaut 20 000) et `data_offset` (défaut 0). Les métadonnées +restent complètes, `dataTotalChars` est porté par toute réponse, et la tranche +est **verbatim** — concaténer les tranches dans l'ordre des offsets reconstitue +la définition à l'octet près. Ne la résumez pas, ne la « parsez » pas. + +--- + ## Écrire une requête WMS ```js diff --git a/DECISIONS.md b/DECISIONS.md index 992893c..4e62164 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -458,7 +458,7 @@ sa réponse et **signale** la coupe. Le signal est commun : | `truncated: true` | présent **uniquement** quand la réponse a été coupée — jamais `truncated: false` | | `hint` | présent ssi `truncated` ; actionnable : dit comment continuer (`offset` suivant) ou réduire (filtres, `context_lines`…) | | `returned` | nombre d'éléments effectivement renvoyés | -| total (`totalParameters`, `totalResults`) | total **avant** la coupe — `truncated` se vérifie donc depuis la réponse elle-même | +| total (`totalParameters`, `totalResults`, `dataTotalChars`) | total **avant** la coupe — `truncated` se vérifie donc depuis la réponse elle-même | Les mécanismes restent **volontairement locaux**, car ils diffèrent : `get_system_parameters` pagine (`limit`/`offset` au schéma — rien n'est perdu, @@ -468,6 +468,22 @@ on continue avec l'offset suivant) ; `search_logs` plafonne le volume en plus `omitted`, le compte écarté. Pas de helper partagé : le factoriser forcerait une abstraction commune à deux mécanismes qui n'en ont pas. +**Périmètre étendu (lot 5, 25/08/2026).** Trois familles d'outils dépassaient +encore le seuil, toutes mesurées sur `LIMAGRAI2512` : + +`get_workflow_details` **fenêtre le blob `data`** (`max_data_chars`, défaut +20 000 ; `data_offset`, défaut 0) — 79 092 caractères pour un StackerCrane +(dont 71 512 de blob), 101 816 pour `CST_SendRejectContainersToPK` (92 362 de +blob), ramenés à ~23 000. La tranche est **verbatim** : découpe de chaîne, rien +d'autre. Ne jamais résumer, reformuler ni « parser » cette définition +EasyBuilder — la concaténation des tranches dans l'ordre des offsets doit la +reconstituer à l'octet près (vérifié : 20 000 + 20 000 + 20 000 + 11 512 = +71 512, concaténation identique au blob d'origine). Les métadonnées du workflow +restent complètes dans chaque tranche ; seul `data` est fenêtré, et +`dataTotalChars` est porté par **toute** réponse — y compris non tronquée, où +la seule différence avec l'ancienne réponse est ces trois champs de fenêtre +(+65 caractères mesurés). + Deux garde-fous de cadrage : - **Ne pas réduire les défauts existants** (`max_results` 50, `context_lines` 2) diff --git a/ROADMAP.md b/ROADMAP.md index f0e5630..30d3840 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -96,13 +96,6 @@ rejet client (~70 000 caractères, D24) : | `get_workflow_details(CST_SendRejectContainersToPK)` | ~101 800 | | `call_query_api("Products", query_type: 1, limit: 1)` — **une seule ligne** Writing | **95 288** | -### L5.1 — Paginer le blob `data` de `get_workflow_details` - -La définition complète d'un workflow dépasse le seuil (~101 800 pour -`CST_SendRejectContainersToPK`, 79 092 pour un `StackerCrane_…` EasyWMS). -Découper le blob `data` en tranches verbatim (paramètres de fenêtre au schéma), -signalées D24 — sans jamais résumer ni reformuler le contenu. - ### L5.2 — Garde de taille sur les outils de requête Une ligne Writing = un agrégat complet sérialisé (95 288 caractères là où la diff --git a/src/tools/workflow-tools.js b/src/tools/workflow-tools.js index 278558a..7ad3e66 100644 --- a/src/tools/workflow-tools.js +++ b/src/tools/workflow-tools.js @@ -5,6 +5,12 @@ const workflowService = require('../services/workflow-service'); +// Taille par défaut d'une tranche du blob `data` de get_workflow_details. +// Ordre de grandeur cible de D24 (~20-25 000 caractères par réponse) : avec +// l'échappement JSON et les métadonnées, 20 000 caractères de blob tiennent +// sous ~23 000 caractères de réponse. +const DEFAULT_MAX_DATA_CHARS = 20000; + /** * List available workflow tools */ @@ -39,7 +45,8 @@ function listTools() { }, { name: 'get_workflow_details', - description: 'Get full details of a specific workflow by ID or name', + description: `Get full details of a specific workflow by ID or name. +The EasyBuilder definition (the \`data\` blob) is large — 71 512 characters for a StackerCrane workflow, 92 362 for CST_SendRejectContainersToPK — so it is returned as a VERBATIM WINDOW (max_data_chars / data_offset). Workflow metadata is always complete; only \`data\` is windowed. dataTotalChars always carries the full blob size, and concatenating the slices in offset order reproduces the definition byte for byte.`, inputSchema: { type: 'object', additionalProperties: false, @@ -52,6 +59,16 @@ function listTools() { type: 'string', description: 'AD application the workflow belongs to (default: the active profile\'s application, usually EasyWMS). Client-specific workflows (CST_*) live in "CustomApp".', }, + max_data_chars: { + type: 'number', + description: `Maximum number of characters of the \`data\` blob returned by this call (default: ${DEFAULT_MAX_DATA_CHARS}). The slice is verbatim — never summarised, reformatted or parsed. Pass 0 for metadata only.`, + default: DEFAULT_MAX_DATA_CHARS, + }, + data_offset: { + type: 'number', + description: 'Character offset in the `data` blob where the returned slice starts (default: 0). When the response carries truncated: true, its hint gives the next offset to pass here.', + default: 0, + }, }, required: ['workflow_id'], }, @@ -143,23 +160,74 @@ async function searchWorkflows(args) { }; } +/** + * Garde de valeur des paramètres de fenêtre. Le wrapper D23 valide les noms de + * paramètres, pas les valeurs — la garde vit donc ici, avant tout appel réseau. + */ +function assertWindowValue(value, fallback, paramName) { + if (value == null) return fallback; + if (!Number.isInteger(value) || value < 0) { + const attendu = paramName === 'max_data_chars' + ? `taille max de la tranche du blob data, défaut ${DEFAULT_MAX_DATA_CHARS}, 0 = métadonnées seules` + : 'offset de départ dans le blob data, défaut 0'; + throw new Error( + `${paramName} invalide : ${JSON.stringify(value)}. Attendu : un entier >= 0 (${attendu}).` + ); + } + return value; +} + /** * Tool: get_workflow_details + * + * La définition EasyBuilder (blob `data`) fait à elle seule 71 512 caractères + * sur un StackerCrane et 92 362 sur CST_SendRejectContainersToPK : la réponse + * complète dépassait le seuil de rejet du client MCP (D24). On renvoie une + * TRANCHE VERBATIM du blob (découpe de chaîne, rien d'autre) : les métadonnées + * restent complètes, et concaténer les tranches dans l'ordre des offsets + * reconstitue la définition à l'octet près. Ne jamais résumer ni « parser » ce + * blob pour n'en renvoyer que des morceaux jugés utiles. */ async function getWorkflowDetails(args) { - const { workflow_id, application } = args; + const { workflow_id, application, max_data_chars, data_offset } = args; - console.error(`[WorkflowTools] Getting workflow details: ${workflow_id} (application: ${application || '(profil)'})`); + const maxDataChars = assertWindowValue(max_data_chars, DEFAULT_MAX_DATA_CHARS, 'max_data_chars'); + const dataOffset = assertWindowValue(data_offset, 0, 'data_offset'); + + console.error(`[WorkflowTools] Getting workflow details: ${workflow_id} (application: ${application || '(profil)'}, max_data_chars=${maxDataChars}, data_offset=${dataOffset})`); const workflow = await workflowService.getWorkflowDetails(workflow_id, application); + const payload = { success: true, workflow }; + + // Seul un blob `data` textuel se fenêtre ; un workflow sans définition (ou + // d'une forme inattendue) sort inchangé. + if (typeof workflow?.data === 'string') { + const total = workflow.data.length; + const slice = workflow.data.slice(dataOffset, dataOffset + maxDataChars); + const nextOffset = dataOffset + slice.length; + + payload.workflow = { ...workflow, data: slice }; + // La taille totale est portée par TOUTE réponse : truncated se vérifie + // depuis la réponse elle-même (D24). + payload.dataTotalChars = total; + payload.dataOffset = dataOffset; + payload.returned = slice.length; + + if (nextOffset < total) { + payload.truncated = true; + payload.hint = + `Blob \`data\` tronqué : ${slice.length} caractère(s) sur ${total} renvoyé(s) depuis l'offset ${dataOffset}. ` + + `Rappelez get_workflow_details avec les mêmes workflow_id/application et data_offset: ${nextOffset} pour la tranche ` + + `suivante (max_data_chars change la taille des tranches). Les tranches sont verbatim : les concaténer dans l'ordre ` + + `des offsets reconstitue la définition EasyBuilder à l'octet près.`; + } + } + return { content: [{ type: 'text', - text: JSON.stringify({ - success: true, - workflow - }, null, 2) + text: JSON.stringify(payload, null, 2) }] }; }