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
| État | Rôle | Responsable |
|---|---|---|
| Observations runtime | Arguments et résultats des invocations réellement exécutées | Runtime |
| Historique conversationnel | Messages fournisseur, identifiants d'appels et métadonnées de continuité | SDK et stockage hôte |
Ressource memory | Données métier partagées selon les scopes déclarés | Programme 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.