Aller au contenu principal
Version: 1.0.0

Historique conversationnel des agents

Un agent peut accomplir sa mission en plusieurs étapes : lire une information, appeler une capacité, interpréter le résultat et poursuivre. Le SDK conserve les échanges entre ces étapes, en RAM par défaut. Cela s'applique aux interfaces structured et native, sans modifier le programme Touba.

Historique, observations et mémoire métier​

ÉtatRôleResponsable
Observations runtimeArguments et résultats des invocations réellement exécutéesRuntime
Historique conversationnelMessages fournisseur, identifiants d'appels et métadonnées de continuitéSDK et stockage hôte
Ressource memoryDonnées métier partagées selon les scopes déclarésProgramme Touba et binding hôte

L'historique n'accorde aucune permission. Chaque nouvel appel repasse par les contrôles du runtime. Une capacité peut être utilisée plusieurs fois, même avec des arguments identiques, tant que ses autorisations et budgets le permettent.

Démarrer avec la RAM​

Après installation de @touba/sdk, ce module provider utilise le stockage RAM par défaut. La clé OpenAI reste dans l'environnement :

import { createOpenAiDecisionProvider } from "@touba/sdk";

export default createOpenAiDecisionProvider({
model: "gpt-4.1-mini",
decisionInterface: "native",
history: {
maxMessages: 128,
maxBytes: 262144,
onEvent(event) {
console.info(event.kind, event.messageCount, event.bytes);
},
},
});

Omettre history active les mêmes limites. Chaque invocation possède une identité distincte, y compris deux appels au même agent avec les mêmes données. Les agents délégués et les branches parallèles ne partagent pas leur historique. Le provider et le modèle font également partie de l'identité de la conversation.

Le stockage RAM conserve au maximum 128 conversations, 32 Mio au total et une heure d'inactivité. maxSessions, maxStoreBytes et ttlMs configurent ces bornes dans history. Une conversation terminée reste conservée jusqu'à son expiration ou sa suppression. Les entrées expirées sont purgées au prochain accès ; ce n'est pas un mécanisme d'effacement sécurisé du processus.

Borner la taille​

maxBytes borne les octets JSON UTF-8 des messages transmis (262144 par défaut), et maxMessages leur nombre (128). Le stockage est indépendant : maxRetainedBytes borne chaque conversation retenue à 8 Mio par défaut, en plus des limites globales du store. Réduire le contexte ne supprime pas les échanges retenus. Un stockage plein refuse atomiquement au lieu d'effacer les anciens messages. Le TTL et delete restent applicables ; ce stockage n'est pas un journal d'audit permanent.

Ces bornes d'octets et de messages ne comptent pas les tokens et excluent le catalogue et le système. Le garde de contexte ci-dessous compte le corps HTTP complet avec le fournisseur.

Le SDK retire les échanges complets les plus anciens si nécessaire. Il conserve le dernier échange complet et les observations courantes fournies par le runtime. Un appel et son résultat ne sont jamais séparés. Les événements onEvent indiquent les retraits ; ils ne contiennent pas les messages ni les signatures. Aucun résumé de remplacement n'est inventé par défaut. Si le contexte indispensable dépasse une borne, le SDK refuse avec DecisionHistoryLimitExceeded.

La taille n'est pas une garantie de réussite métier. L'application doit toujours vérifier ses résultats et prévoir une conduite à tenir lorsque le budget s'épuise.

Une réponse fournisseur reçue mais dépourvue de décision typée valide produit AgentDecisionProviderSchemaViolation, pas une panne du service. La trace conserve le diagnostic SDK dans provider_error_code lorsqu'il est disponible. AgentDecisionProviderUnavailable désigne un échec du transport ou du provider. Les refus locaux DecisionHistoryBusy, DecisionHistoryLimitExceeded, DecisionHistoryMismatch, DecisionHistorySummaryInvalid ou ProviderContextWindowExceeded conservent leur propre code. Ils ne sont pas attribués au format de réponse du modèle et ne déclenchent pas de retry fournisseur.

Compter les tokens avant la génération​

Avec OpenAI Responses, ajoutez context: { contextWindowTokens: 32768 } et maxOutputTokens: 1000 à la factory. Ici, 32768 est un plafond applicatif volontaire : choisissez une valeur au plus égale à la fenêtre vérifiée du modèle. Le SDK ne devine pas la fenêtre à partir d'un nom de modèle. Sans context, seules les bornes d'historique en octets/messages s'appliquent.

Le garde compte le corps sérialisé, donc aussi les instructions, outils, schémas et métadonnées. Il réserve par défaut ceil(tokens_entree × 1.2) + 512 + maxOutputTokens. inputTokenMultiplier et safetyMarginTokens règlent la marge. Le comptage fournisseur peut être estimatif ; la marge n'est pas une preuve mathématique sur un tokenizer externe. La limite reste un plafond hôte explicite.

Si cela dépasse la fenêtre, le SDK retire du contexte un ancien échange complet et recompte, avant toute génération. Il garde le dernier échange et toutes les observations courantes. Si ce minimum ne tient pas : ProviderContextWindowExceeded. Un comptage indisponible donne ProviderContextCountUnavailable, sans génération. Une erreur HTTP de génération n'est jamais interprétée comme un signal de compaction. Aucun résultat modèle déjà obtenu n'est rejoué pour améliorer sa réponse.

Pour Anthropic Messages ou Gemini generateContent, branchez createProviderContextFetch sur le fetch du connecteur AI SDK avant de fournir son modèle à createAiSdkDecisionProvider. Le garde accepte provider: "anthropic" ou "google". Il prend en charge les endpoints directs officiels ; les proxies, Bedrock, Vertex et autres transports nécessitent un adaptateur qualifié. Les états serveur implicites, la troncature automatique OpenAI et le cache externe Gemini sont refusés par ce garde. Fournissez les messages explicitement.

Les événements provider_context_count exposent seulement fournisseur, compte, réserve, fenêtre et verdict fits. Le comptage ajoute des requêtes et de la latence, même s'il ne génère aucune réponse. Il ne remplace ni les budgets d'appels du runtime ni un plafond monétaire hôte. Voir le tutoriel de contexte et compaction.

Choisir une compaction avec résumé​

Par défaut, aucun appel LLM de résumé n'est effectué. La fenêtre conserve un suffixe d'échanges complets. Un hôte peut fournir history.summarize, fonction asynchrone recevant { previousSummary, turns, maxBytes }. Elle renvoie un texte non vide borné par maxSummaryBytes (4096 octets par défaut). Elle reçoit seulement les échanges sortant du contexte depuis le résumé précédent. En opaque_refs, ces échanges restent projetés, sans réintroduction des arguments bruts.

Le résumé est ajouté comme donnée utilisateur non fiable, jamais comme instruction système ou permission. Il porte le nombre d'échanges couverts et leur empreinte ; decision_history_summary rend la réduction observable sans exposer le texte. Le contexte résultant est recompté, résumé compris. Les originaux restent retenus. Une réponse invalide du résumeur refuse avec DecisionHistorySummaryInvalid.

Si ce callback utilise un modèle, l'hôte doit borner et comptabiliser ces appels, protéger leurs données et gérer leur timeout. Ils ne sont pas gratuitement absorbés dans le compteur d'appels agentiques du runtime. Le SDK n'affirme pas qu'un résumé est fidèle : une tâche exigeant les détails exacts doit utiliser les observations conservées ou une ressource métier gouvernée. Ne placez pas d'autorisations dans le résumé.

Persister avec SQLite​

Pour conserver les messages après redémarrage, créez un répertoire privé, ignoré par Git, puis utilisez le stockage SQLite livré avec le SDK et Node.js 24 :

import { mkdirSync } from "node:fs";
import { createOpenAiDecisionProvider } from "@touba/sdk";
import { createSqliteDecisionHistoryStore } from "@touba/sdk/history/sqlite";

mkdirSync(".private-state", { recursive: true, mode: 0o700 });
const store = createSqliteDecisionHistoryStore({
filename: ".private-state/conversations.sqlite",
});

export default createOpenAiDecisionProvider({
model: "gpt-4.1-mini",
decisionInterface: "native",
history: { store, namespace: "support-production" },
});

Un répertoire déjà existant conserve ses permissions : vérifiez-les avant usage. Avec filename, le SDK crée ou remet la base et ses auxiliaires SQLite à 0600 sur POSIX, avant ouverture. Il refuse les liens symboliques, fichiers non réguliers et répertoires parents inscriptibles par d'autres utilisateurs (DecisionHistoryStorageUnsafe). Cela ne chiffre pas le contenu. Sur Windows, configurez également les ACL du répertoire. Le stockage peut aussi recevoir database, une connexion DatabaseSync ouverte, à la place de filename. Sa fermeture et la protection de ses fichiers restent alors à la charge de l'application. SQLite conserve par défaut 128 conversations, 32 Mio et 24 heures d'inactivité ; ses options maxSessions, maxBytes et ttlMs règlent ces limites de stockage.

Le namespace est obligatoire avec un stockage partagé : attribuez-le depuis la configuration de confiance de l'application, jamais depuis une demande du modèle. Séparez les applications et les clients qui doivent rester isolés. Le fichier contient des données de conversation : protégez ses accès, sauvegardes et durées de conservation selon votre politique. Il n'est pas chiffré par le SDK.

Une reprise d'approbation conserve l'identité d'invocation. Réutilisez le même stockage, namespace, modèle et interface. Si seule la RAM était utilisée et que le processus a redémarré, les observations du checkpoint restent disponibles, mais les anciens messages fournisseur ne peuvent pas être recréés. L'événement resumedWithoutMessages signale cette situation.

Deux décisions simultanées pour la même conversation sont refusées avec DecisionHistoryBusy. Après un arrêt brutal, SQLite conserve sa réservation. Arrêtez et vérifiez l'ancien worker avant store.recover(sessionKey, { workerStopped: true }). Cette opération libère le stockage, pas un effet métier incertain. La réconciliation des effets et le checkpoint restent du ressort du runtime et de l'application. store.delete(sessionKey) supprime une conversation inactive ; store.close() ferme la connexion créée par le stockage.

store.read(sessionKey) renvoie un instantané privé détaché, ou null si absent ou expiré, sans renouveler le TTL. La lecture d'une conversation active refuse. Le tutoriel SQLite montre une inspection après fermeture du processus et explique la différence entre nouveau run et reprise.

Brancher une autre base​

history.store accepte un adaptateur hôte avec transact(key, operation). Il doit fournir à operation un instantané privé de l'enregistrement, ou null, puis sauvegarder atomiquement le record retourné et renvoyer result. L'opération est asynchrone et peut appeler le fournisseur. L'adaptateur doit empêcher deux traitements concurrents de la même clé, préserver l'ancien état en cas d'exception et gérer capacité, expiration et reprise après crash. Un simple couple get/set sans exclusion ne satisfait pas ce contrat.

Continuité et frontière de contenu​

En mode inline, le SDK conserve les messages retournés par le connecteur, y compris les identifiants et métadonnées opaques. Le résultat d'un appel natif est ajouté seulement quand l'observation correspondante revient du runtime. Une proposition en attente n'est jamais présentée comme une opération exécutée. Les appels structurés utilisent aussi une génération permettant de conserver les messages. Un générateur generateObject injecté qui ne retourne qu'un objet typé permet de conserver cet objet, pas des métadonnées qu'il n'expose pas.

En mode opaque_refs, la projection du runtime prévaut : les anciens arguments bruts du modèle ne sont pas réintroduits par l'historique. Seules les entrées projetées et des métadonnées de décision sont conservées pour le prochain appel.

history: false choisit explicitement un provider sans conservation des messages. Les observations réelles restent présentées comme données, sans identifiant d’appel ni signature fournisseur inventés. Un hôte personnalisé qui active l'historique doit transmettre run_id, invocation_id et agent_ref. Le runtime Touba les fournit automatiquement.