CodeGym /Cours /ChatGPT Apps /Mémoire et état d’un agent : session vs persistent, ...

Mémoire et état d’un agent : session vs persistent, checkpoints

ChatGPT Apps
Niveau 12 , Leçon 2
Disponible

1. Pourquoi un agent a‑t‑il besoin d’une mémoire séparée

Cette partie s’appuie sur les leçons précédentes du module 12 sur les agents : nous y avons déjà abordé l’architecture de base, le run‑cycle et les outils ; ici, le focus est sur la mémoire et l’état.

Par analogie avec une application web classique, la LLM est ici un CPU très intelligent, capable d’exécuter des « programmes textuels » complexes. Et l’état de l’agent est une combinaison de RAM et de SSD : des données de session de courte durée et un stockage à long terme.

Dans un chat ChatGPT classique, sans votre code, la « mémoire », c’est simplement une liste de messages system/user/assistant/tool, que le modèle voit dans la requête en cours. Pour un agent, cela ne suffit pas, car :

  • il doit se souvenir de la progression de processus complexes : quelle étape du workflow a déjà été franchie, quelles options de cadeaux ont déjà été filtrées, ce que l’utilisateur a confirmé ;
  • il doit connaître des faits durables sur l’utilisateur : préférences, adresse de livraison, historique des commandes passées ;
  • il doit savoir survivre aux pannes : si le serveur tombe au milieu d’une sélection de cadeau, l’utilisateur ne doit pas tout ressaisir.

Si vous essayez de tout conserver uniquement dans le contexte du prompt, vous atteindrez rapidement la limite de la fenêtre de contexte et paierez des tokens pour les mêmes faits. En parallèle, vous prenez des risques en matière de sécurité : trop de données inutiles sont régulièrement envoyées au modèle. C’est pourquoi, dans les systèmes à base d’agents, il existe toujours un état explicite — des objets qui vivent en dehors de l’historique des messages et que vous contrôlez vous‑même.

2. Couches d’état d’un agent : contexte, session, persistent

Commençons par un découpage en couches. Un agent possède généralement au moins trois niveaux de « mémoire » :

  1. Historique des messages (dialogue context).
  2. État de session (session state).
  3. État à long terme (persistent state).

Il est important de ne pas confondre ces notions.

Historique des messages : « mémoire sale »

L’historique des messages, c’est ce que la LLM voit à chaque étape : instructions system, requêtes de l’utilisateur, réponses de l’agent, résultats des outils.

L’avantage, c’est que vous n’avez pas à le gérer à la main — le SDK d’agents et la plateforme s’en chargent via l’entité Session/Conversation.

L’inconvénient, c’est que c’est une « mémoire sale » : il y a beaucoup de mots superflus, de répétitions, de données fortuites de l’utilisateur. Ces données coûtent cher en tokens et sont mal structurées. Vous ne voulez pas que la liste de 200 cadeaux déjà filtrés soit relue à chaque fois par le modèle sous forme de texte brut (plain text).

Session state : mémoire de travail à court terme

L’état Session est un objet structuré qui vit dans le cadre d’une seule session/conversation de l’agent. Une bonne analogie pour un développeur front‑end est useState ou un Redux store qui vit tant que l’onglet est ouvert.

On y trouve des éléments comme :

  • l’étape courante du processus (par exemple, "collecting_profile" ou "filtering_candidates") ;
  • un cache temporaire des résultats des outils ;
  • les paramètres de session : locale, canal choisi, cases à cocher du type « l’utilisateur a accepté les conditions ».

Cet état peut être stocké à proximité de l’agent — dans Redis, dans un KV store en mémoire ou via le SessionService intégré d’un SDK particulier. L’essentiel : ne pas essayer d’enfourner tout cela dans le system prompt.

Persistent state : données à long terme

L’état Persistent vit longtemps : entre les sessions, les parcours d’achat (checkout), les appareils. C’est le profil de l’utilisateur, ses commandes, les listes de souhaits enregistrées, les réglages.

Idée clé : l’agent ne « se souvient » pas des données persistantes par magie, il les « lit » via des outils — par exemple, get_user_profile, get_past_orders. Pas de variables globales cachées à l’intérieur de l’agent ; toujours un appel explicite.

Tableau comparatif

Couche Où ça vit Cycle de vie Exemples de données
Messages Session / SDK / OpenAI Un run / un dialogue messages system/user/tool
Session state KV / SessionService / Redis Tant que la session est active étape du workflow, caches temporaires
Persistent BD (Postgres/NoSQL/ACP backend) Entre les sessions et les dialogues profil, commandes, listes enregistrées

3. Session state : qu’est‑ce que c’est et comment le stocker

Imaginez que l’agent GiftGenius mène un processus en plusieurs étapes :

  1. Collecte le profil du destinataire du cadeau.
  2. Génère une liste de candidats.
  3. Les filtre selon le budget, la livraison, la région.
  4. Prépare la sélection finale.

Pendant le processus, il interagit constamment avec l’utilisateur et appelle des outils. Tout ce qui concerne « la progression d’une session donnée de sélection de cadeau » a logiquement sa place dans le session state.

Exemple de structure d’état de session pour GiftGenius

Décrivons le type d’état de session en TypeScript :

// État dans le cadre d'une "sélection de cadeau"
export type GiftSessionState = {
  step:
    | "collecting_profile"
    | "generating_candidates"
    | "filtering"
    | "finalizing";

  // brouillon du profil du destinataire
  profileDraft?: {
    recipientType?: string;
    ageRange?: string;
    interests?: string[];
    dislikes?: string[];
  };

  // id des produits candidats, reçus du backend
  candidateIds?: string[];

  // cadeau choisi par l'utilisateur
  selectedGiftId?: string;

  // indicateurs techniques
  locale?: string;
};

Ici, nous évitons délibérément d’y mettre des objets produits complets — seulement leurs ID. Les données complètes doivent vivre dans la BD ; lorsque c’est nécessaire, l’agent appelle l’outil get_gift_details(gift_id).

Session dans un Agents SDK (conceptuellement)

Dans de nombreux SDK d’agents, il existe une abstraction de session qui prend en charge le stockage de l’historique des messages et vous permet de stocker en plus un état structuré. En pseudo‑code, cela pourrait ressembler à ceci :

import { createRunner, OpenAIConversationsSession } from "@openai/agents";
// le type GiftSessionState d'exemple ci-dessus

const session = new OpenAIConversationsSession<GiftSessionState>({
  sessionId: "chatgpt-thread-id-or-random",
});

const runner = createRunner({ agent });

const result = await runner.run({
  session,
  input: "Je veux un cadeau pour un collègue jusqu’à 50 $",
});

Le SDK, sous le capot :

  • récupère l’historique des messages pour cette session ;
  • ajoute le nouveau message utilisateur ;
  • transmet le tout au modèle et aux outils ;
  • enregistre l’état mis à jour (y compris session.state) de nouveau.

De votre côté, vous manipulez session.state comme un objet normal.

Mise à jour du session state depuis les outils

Schéma typique : un outil qui calcule quelque chose met à jour simultanément l’état de session. Par exemple, un outil qui collecte le profil du destinataire à partir des réponses de l’utilisateur :

export async function updateProfileDraft(
  session: GiftSessionState,
  answers: { questionId: string; value: string }
): Promise<GiftSessionState> {
  const next: GiftSessionState = { ...session };

  if (!next.profileDraft) {
    next.profileDraft = {};
  }

  if (answers.questionId === "interests") {
    next.profileDraft.interests = answers.value.split(",").map((s) => s.trim());
  }

  // ...autres champs

  next.step = "generating_candidates";
  return next;
}

Ici, nous passons à l’outil non pas toute la Session du SDK, mais seulement son state (type GiftSessionState). Dans du code réel, il est pertinent de nommer un tel argument, par exemple, currentState pour ne pas le confondre avec l’objet Session.

L’agent appelle cet outil, reçoit un nouvel objet d’état et l’enregistre de nouveau dans session.state.

4. Persistent state : mémoire à long terme de l’agent

Rappelons que GiftGenius ne fonctionne pas que dans un seul chat. L’utilisateur peut revenir une semaine plus tard, depuis un autre appareil, et dire : « Choisis un cadeau pour le même ami que la dernière fois, mais le budget a augmenté ».

Cette information ne doit pas vivre dans le session state, mais dans un stockage persistant : base de données, backend commerce/ACP (la couche commerce, qui fera l’objet d’un module séparé), etc.

Exemple de modèle persistant

Décrivons le modèle de profil du destinataire dans la BD (simplifié, en type TypeScript) :

// Ce qui est stocké dans la BD
export type RecipientProfile = {
  id: string;
  userId: string;
  label: string; // "collègue du marketing"
  recipientType: string;
  ageRange?: string;
  interests: string[];
  dislikes: string[];
  lastUsedAt: string; // date ISO
};

Et un dépôt (prenons pour l’instant une simple Map — en vrai vous feriez une couche ORM/SQL) :

const profiles = new Map<string, RecipientProfile>();

export const RecipientRepo = {
  async findByUser(userId: string): Promise<RecipientProfile[]> {
    return [...profiles.values()].filter((p) => p.userId === userId);
  },

  async save(profile: RecipientProfile): Promise<void> {
    profiles.set(profile.id, profile);
  },
};

L’agent accède au persistent via des outils

Il est important que l’agent n’accède pas directement à la BD, mais passe par des tools. Ainsi, il reste une entité « propre » : d’un côté — la LLM et la logique d’orchestration, de l’autre — l’implémentation des intégrations.

Par exemple, l’outil get_recipient_profiles :

export async function getRecipientProfilesTool(input: {
  userId: string;
}): Promise<{ profiles: RecipientProfile[] }> {
  const profiles = await RecipientRepo.findByUser(input.userId);

  return {
    profiles,
  };
}

Dans la description de l’outil, l’agent lit : « utilise cet outil pour obtenir les profils enregistrés des destinataires pour l’utilisateur courant ». Il décide lui‑même du moment opportun pour l’appeler.

Bilan : le session state concerne la progression d’une conversation particulière et des caches temporaires que l’on peut perdre sans dommage. Les données persistantes sont ce qui doit survivre aux sessions et aux appareils : profils, commandes, listes de souhaits. L’agent les lit toujours via des outils, et non en s’en « souvenant magiquement ».

5. Comment session et persistent coopèrent dans le run‑cycle

Assemblons maintenant le tout dans un schéma global. À chaque étape du run‑cycle d’un agent, on suit une courte séquence :

  1. Récupérer le session state par sessionId.
  2. Au besoin, charger les données persistantes pertinentes depuis la BD via des outils.
  3. Former le contexte pour le modèle (messages + état structuré).
  4. Le modèle décide : répondre en texte ou appeler des outils.
  5. Les outils mettent à jour soit le session state, soit les données persistantes (via la BD).
  6. Enregistrer le nouvel état de session et, si nécessaire, créer un checkpoint (voir plus loin).
  7. Renvoyer la réponse à l’utilisateur.

Schéma en mermaid :

flowchart TD
    A[Recevoir l'input utilisateur] --> B["Charger la Session (state + messages)"]
    B --> C{Faut-il des données persistantes ?}
    C -- Oui --> D[Appeler les tools : get_user_profile, get_recipient_profiles]
    C -- Non --> E[Former le contexte pour la LLM]
    D --> E
    E --> F["Appeler le modèle (LLM)"]
    F --> G{Le modèle veut-il appeler un tool ?}
    G -- Oui --> H[Exécuter l'outil, mettre à jour session/persistent]
    G -- Non --> I[Préparer la réponse finale]
    H --> J[Créer un checkpoint et enregistrer la Session]
    I --> J
    J --> K[Réponse à l’utilisateur]

Un tel cycle rend le comportement de l’agent reproductible : à chaque étape, nous savons explicitement quel était l’état avant l’appel au modèle et ce qui a changé après.

6. Checkpoints : instantanés de l’état de l’agent

Les checkpoints sont des « instantanés d’état » de l’agent enregistrés à une étape importante du processus. Ce n’est pas juste « l’état de session courant », mais un fait consigné dans un stockage externe : à l’étape N, nous avions tel état, tels résultats d’outils, tel input utilisateur.

Pourquoi ils sont utiles :

  • reprise après erreurs et pannes ;
  • fonction « reprendre plus tard » côté utilisateur ;
  • débogage : reproductibilité d’un run problématique ;
  • audit : ce que l’agent a fait avant, par exemple, de créer une commande.

Ce qui entre généralement dans un checkpoint

Un checkpoint typique contient :

  • des identifiants : runId, userId, workflowId, stepId ;
  • l’état de session à ce moment ;
  • les identifiants clés des entités persistantes (par exemple, l’id du brouillon de commande) ;
  • des métadonnées : date de création, version de l’agent.

Évitez d’y mettre tout le texte du dialogue. Plus bas, dans la section sur l’hygiène de la mémoire, nous reviendrons sur ce qu’il faut conserver ou non.

Il vaut mieux stocker un lien vers la Session ou un court résumé des étapes.

7. Concevoir les checkpoints pour GiftGenius

Prenons notre processus de sélection de cadeau et décidons où placer les checkpoints. Par exemple :

  • après la collecte du profil du destinataire ;
  • après la génération et le premier filtrage des candidats ;
  • avant de proposer le choix final à l’utilisateur.

Types pour le checkpoint et l’état du workflow

Décrivons l’état du workflow (très proche de GiftSessionState, mais c’est un « instantané » pour les checkpoints) :

export type GiftWorkflowStep =
  | "profile_collected"
  | "candidates_generated"
  | "filtered"
  | "final_choice_made";

export type GiftCheckpoint = {
  id: string;
  runId: string;
  userId: string;

  step: GiftWorkflowStep;

  // partie de l'état de session
  // dont nous avons besoin pour la restauration
  sessionState: GiftSessionState;

  // quels id de candidats ont été générés
  candidateIds: string[];

  createdAt: string; // ISO
  agentVersion: string;
};

Stockage des checkpoints (simplifié)

Comme précédemment, créons une simple Map au lieu d’une vraie BD :

const checkpoints = new Map<string, GiftCheckpoint>();

export const GiftCheckpointRepo = {
  async save(cp: GiftCheckpoint) {
    checkpoints.set(cp.id, cp);
  },

  async findByRun(runId: string): Promise<GiftCheckpoint[]> {
    return [...checkpoints.values()].filter((c) => c.runId === runId);
  },

  async findLastByUser(userId: string): Promise<GiftCheckpoint | undefined> {
    return [...checkpoints.values()]
      .filter((c) => c.userId === userId)
      .sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0];
  },
};

Création d’un checkpoint depuis le code de l’agent

Imaginons un helper appelé après une étape importante :

import { randomUUID } from "crypto";

export async function createCheckpoint(params: {
  runId: string;
  userId: string;
  step: GiftWorkflowStep;
  sessionState: GiftSessionState;
  candidateIds: string[];
}) {
  const checkpoint: GiftCheckpoint = {
    id: randomUUID(),
    runId: params.runId,
    userId: params.userId,
    step: params.step,
    sessionState: params.sessionState,
    candidateIds: params.candidateIds,
    createdAt: new Date().toISOString(),
    agentVersion: "v1.3.0",
  };

  await GiftCheckpointRepo.save(checkpoint);
}

L’agent peut appeler au bon moment :

await createCheckpoint({
  runId,
  userId,
  step: "filtered",
  sessionState,
  candidateIds,
});

Lors de la restauration, nous :

  1. Trouvons le dernier checkpoint par runId ou userId.
  2. Restaurons session.state depuis checkpoint.sessionState.
  3. Si nécessaire, rechargeons depuis la BD les données à jour via candidateIds.

8. Où stocker techniquement session, persistent et checkpoints

Au niveau de l’infrastructure, vous avez généralement trois classes de stockage différentes :

  • In‑memory — pour le dev/démo, rapide mais éphémère.
  • Redis (ou un autre KV store) — pour l’état de session.
  • Base relationnelle/NoSQL — pour les données persistantes et les checkpoints.

In‑memory store pour le développement local

Pour un mode dev local, un simple in‑memory store suffit largement. Par exemple, un mini‑stockage avec TTL pour les sessions :

type StoredSession<T> = {
  state: T;
  expiresAt: number;
};

const sessions = new Map<string, StoredSession<GiftSessionState>>();

export function saveSession(sessionId: string, state: GiftSessionState) {
  sessions.set(sessionId, {
    state,
    expiresAt: Date.now() + 30 * 60 * 1000, // 30 minutes
  });
}

export function loadSession(sessionId: string): GiftSessionState | undefined {
  const stored = sessions.get(sessionId);
  if (!stored) return undefined;
  if (stored.expiresAt < Date.now()) {
    sessions.delete(sessionId);
    return undefined;
  }
  return stored.state;
}

C’est très bien pour le dev local, mais en prod, avec un scalage horizontal (plusieurs instances), cela ne fonctionnera plus.

Redis pour le session state

En production, il est pratique de stocker l’état de session dans Redis :

  • écritures/lectures rapides ;
  • TTL « out of the box » ;
  • accessible à toutes les instances du service.

Exemple pseudo‑code (simplifié) :

// Wrapper autour du client Redis
export async function saveSessionToRedis(
  sessionId: string,
  state: GiftSessionState
) {
  const json = JSON.stringify(state);
  await redis.set(`session:${sessionId}`, json, "EX", 60 * 30); // 30 minutes
}

export async function loadSessionFromRedis(
  sessionId: string
): Promise<GiftSessionState | undefined> {
  const json = await redis.get(`session:${sessionId}`);
  return json ? (JSON.parse(json) as GiftSessionState) : undefined;
}

Postgres/une autre BD pour le persistent et les checkpoints

Le persistent state et les checkpoints sont des entités « sérieuses », pour lesquelles les transactions, migrations, index et autres joies comptent. On les place dans Postgres, MySQL, Firestore, etc.

Le patron architectural est simple :

  • session dans Redis avec TTL ;
  • persistent et checkpoints dans une BD sans TTL (ou avec une politique de rétention dépendant du métier).

9. Hygiène de la mémoire : tailles, confidentialité, séparation des responsabilités

La mémoire de l’agent, ce n’est pas « on pose un objet quelque part et on y va ». Quelques règles importantes permettent d’économiser de l’argent et de préserver votre sommeil.

Ne mettez pas tout dans les messages

L’historique des messages est une ressource coûteuse :

  • sa longueur influence fortement le coût d’une requête au modèle ;
  • on y trouve généralement beaucoup de « bruit ».

Donc :

  • essayez d’extraire au plus tôt les faits de l’historique vers un état structuré ;
  • utilisez la synthèse (summarization) pour les anciennes parties de l’historique ;
  • si vous stockez l’historique textuel dans les checkpoints, faites‑le séparément de ce qui est envoyé au modèle.

Confidentialité et PII

Surtout pour les scénarios commerce, il est crucial de ne pas stocker de données sensibles là où elles ne doivent pas se retrouver. La documentation d’architecture de la mémoire souligne clairement qu’il ne faut pas conserver de PII dans les messages ou les checkpoints sans nettoyage.

Règles pratiques :

  • n’insérez pas d’e‑mail/téléphone/adresse directement dans le session state, si ce n’est pas nécessaire au fonctionnement de l’agent ;
  • dans les logs et checkpoints, privilégiez les identifiants (userId, recipientProfileId) plutôt que des chaînes brutes ;
  • si vous devez faire transiter des PII sur plusieurs étapes, utilisez des champs protégés séparés dans le stockage persistant, et ne transmettez dans l’état que la clé.

Séparer données métier et log de dialogue

Un bon schéma consiste à considérer le state comme une mémoire « propre » et les messages comme « sales ».

C’est‑à‑dire :

  • les entités métier (profils, commandes, paniers) vivent tout le temps dans la BD ;
  • le state/les checkpoints contiennent le minimum nécessaire pour reprendre le processus ;
  • les logs/l’historique du chat sont stockés séparément (par exemple dans un stockage vectoriel) et servent à l’analytique, mais ne sont pas injectés à chaque requête modèle.

10. Mini‑pratique : que conserveriez‑vous ?

Pour ancrer la différence entre les couches de mémoire, prenons un cas concret. Inutile d’écrire du code — il suffit d’imaginer les structures sur papier ou mentalement.

Imaginez que votre agent GiftGenius a tenu le dialogue suivant avec un utilisateur :

  • Utilisateur : « J’ai besoin d’un cadeau pour un collègue développeur, budget jusqu’à 50 $, il aime les jeux de société et la caféine. »
  • Agent : pose quelques questions de clarification.
  • Utilisateur : « Il déteste les mugs et a déjà trop de carnets. »
  • Agent : génère une liste de 10 idées, l’utilisateur en choisit une, mais dit : « Je reviendrai pour finaliser plus tard. »

Réfléchissez :

  1. Que mettriez‑vous dans le session state (qui peut expirer dans 30 minutes) ?
  2. Qu’est‑ce qui irait dans le stockage persistant pour que l’utilisateur puisse revenir une semaine plus tard ?
  3. À quoi ressemblerait le checkpoint après le choix de l’idée, mais avant la finalisation de la commande ?

Essayez d’esquisser les types TypeScript correspondants et les fonctions saveSessionState, savePersistentState, createGiftIdeaCheckpoint par analogie avec les exemples de ce cours. Si vous le souhaitez, vous pouvez même les écrire rapidement dans un éditeur en vous inspirant des exemples ci‑dessus — ce sera un bon mini‑checkpoint avant le prochain cours.

11. Erreurs courantes liées à la mémoire d’un agent

Erreur n° 1 : vouloir tout stocker uniquement dans l’historique des messages.
Le développeur se réjouit : « Le modèle voit déjà toute la conversation, pourquoi inventer un state en plus ? ». Au bout de quelques dizaines de messages, la fenêtre de contexte est saturée de bruit, les tokens coûtent le prix d’un nouveau MacBook, et le comportement de l’agent devient instable — il ne voit tout simplement plus des faits importants anciens. Il faut résoudre ce problème en séparant explicitement le session state et le stockage persistant, pas en augmentant les limites.

Erreur n° 2 : mélanger session et persistent dans un seul objet.
Il est parfois tentant de créer une « grosse » entité AgentState, d’y mettre tout et de l’enregistrer « tel quel » dans la base. La frontière se brouille alors entre les données temporaires d’une conversation et les données à long terme de l’utilisateur. On commence à voir des situations du type « après le déploiement, toutes les sessions se sont mystérieusement restaurées avec des données d’il y a un an » ou « la session d’un utilisateur a accidentellement récupéré le profil persistant de quelqu’un d’autre ». Séparez consciemment les niveaux.

Erreur n° 3 : stocker trop d’informations dans les checkpoints.
Une erreur fréquente est d’enregistrer dans un checkpoint tout le JSON de la réponse des outils, tout l’historique du dialogue, les données brutes des intégrations, etc. Après quelques semaines, la base des checkpoints enfle de manière indécente, les sauvegardes prennent une heure, et les requêtes BD ralentissent. Un checkpoint doit contenir uniquement les faits réellement nécessaires pour poursuivre le processus, plus un minimum de métadonnées.

Erreur n° 4 : oublier le TTL et le nettoyage du session state.
Si les états de session n’ont pas de durée de vie, n’importe quelle expérimentation fortuite de l’utilisateur en mode Dev reste dans Redis pour toujours. Quelques mois plus tard, vous regardez le monitoring et voyez une montagne de sessions « oubliées » qui consomment de la mémoire. Le niveau session doit être conçu avec un TTL explicite, et le niveau persistent avec une politique de rétention réfléchie.

Erreur n° 5 : stocker des PII dans le state et les checkpoints sans nécessité.
C’est particulièrement dangereux quand on met sans réfléchir un e‑mail, une adresse, un numéro de carte dans le session state, puis que cet objet est sérialisé dans les logs, part vers l’analytique et se retrouve dans les checkpoints. Cela crée de sérieux risques réglementaires et de sécurité. Mieux vaut stocker des identifiants sûrs et, si nécessaire, les résoudre en données réelles via des outils protégés séparés.

Erreur n° 6 : absence de stratégie de restauration depuis les checkpoints.
Certaines équipes enregistrent consciencieusement des checkpoints, mais ne réfléchissent pas à la façon dont l’agent doit réellement s’en restaurer. Du coup, quand « quelque chose se passe mal », les développeurs regardent une table avec de beaux JSON, mais n’ont pas de code qui sache reconstruire le run à partir de ceux‑ci. Le checkpointing sans scénario de restauration n’est qu’un log coûteux, pas un outil de fiabilité.

Erreur n° 7 : lier étroitement l’agent à une implémentation de stockage spécifique.
Si le code de l’agent parle directement à Redis/Postgres, il est plus difficile à déplacer, à tester et à faire évoluer. Lors d’un changement d’architecture (par exemple, apparition de ressources MCP ou d’un service d’état séparé), il faudra remanier la logique de l’agent. Il est bien meilleur que l’agent ne voie que des abstractions Session et un ensemble de tools, et que les outils sachent où se trouvent réellement les données.

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION