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 <noreply@anthropic.com>
This commit is contained in:
Arthur Ria
2026-08-25 15:08:13 +02:00
parent 5ec2990347
commit 6f54d765c4
4 changed files with 108 additions and 15 deletions
+16
View File
@@ -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
+17 -1
View File
@@ -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)
-7
View File
@@ -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
+75 -7
View File
@@ -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)
}]
};
}