MODE ÉDITION — CLIQUEZ UN TEXTE · CTRL+S POUR ENREGISTRER
Assistant personnel · architecture technique

Jarvis

Une voix, une flotte d'agents, une mémoire qui dure.

2entrées — voix, Telegram
16sous-agents n8n
1base Postgres + pgvector
13catégories de mémoire
Jarvis01 / 12
01Vue d'ensemble

Trois étages, et une seule phrase qui les traverse

Jarvis n'est pas un service mais un assemblage de trois briques indépendantes : un canal qui capte, un cerveau qui route, une base qui se souvient. Chacune peut tomber sans emporter les deux autres.

Étage 1 — les entrées

Capter

Deux portes, une seule sortie : le même orchestrateur, avec un canal déclaré.

LiveKit Cloudvoix — agent JARVIS, eu-central
Telegramtexte et vocal transcrit
Serveur MCP n8nun outil unique : parler_a_jarvis
PWA + application de bureauconvoquent l'agent, affichent le HUD

L'invariant : tout ce qui arrive par la voix est une transcription. Une date qui contredit le jour même est presque toujours un mot mal entendu.

Étage 2 — l'automatisation

Router

Un orchestrateur ne fait rien lui-même : il choisit qui fait, puis vérifie.

jarvis-orchestratorn8n — 31 nœuds, 29 liaisons
16 sous-agentsun workflow chacun, appelés en toolWorkflow
Contrôle de sincériténœud Code, après le modèle
Branche Notionles grosses restitutions, sans passer par un modèle

L'invariant : aucun geste irréversible sans validation de Boris. Le garde-fou vit dans l'orchestrateur, jamais dans les sous-agents.

Étage 3 — la mémoire

Retenir

Écriture à chaud pendant la conversation, distillation à froid une fois le silence installé.

PostgreSQL 17 + pgvectorconteneur privé, aucun port publié
4 tables de mémoirecategories, sessions, tours, faits
7 tables de flottedepenses, tarifs, quotas, reglages, voyages…
Rapport de sessiontoutes les 15 min — Sonnet 5

L'invariant : rien n'est jamais supprimé. Un fait périmé est daté et sort de la circulation, il ne disparaît pas.

Vue d'ensemble02 / 12
02Module LiveKit

La pile vocale : trois modèles, un seul aller-retour

Le code vit dans jarvis-voice/ et se déploie au CLI par lk agent deploy. Les trois modèles passent par LiveKit Inference : aucune clé fournisseur séparée à gérer.

Entrée La voix de Borismicro navigateur ou application de bureau WebRTC
STT Transcriptionlanguage: multi — jamais « en » deepgram/nova-3-multi
Agent Persona et outilun seul outil : parler_a_jarvis openai/gpt-4.1-mini
MCP Serveur MCP n8nbearerAuth, path jarvis-livekit sFSVByv2KNilurt3
TTS Synthèse vocalegratuite — c'est elle qui tient le budget fishaudio/s2.1-pro-free
Sortie Réponse parlée1,1 à 5,6 s de bout en bout, mesuré eu-central
Module LiveKit03 / 12
03Module LiveKit — les réglages

Trois réglages décident si la voix marche ou non

Aucun des trois n'est visible depuis une interface. Chacun a produit une panne complète, et deux d'entre elles étaient parfaitement silencieuses.

0 fenêtre de session client — le défaut est 5 s
0 par minute de conversation, tout compris
0 minutes de session consommées ce mois

client_session_timeout_seconds

Le défaut de 5 secondes coupait toute demande mobilisant deux sous-agents : 7 à 9 s pour agenda + mails. La météo passait, les mails non — d'où un système qui semblait marcher par intermittence.

Porté à 180 s dans le code. L'erreur n'apparaissait que dans lk agent logs.

language: multi

Boris parle anglais, mais son monde est français. En anglais strict, nova-3 a transcrit « Dijon » en « in June » : l'orchestrateur a daté sa réponse du 15 juin.

Même piège pour les noms de contacts, les rues et les adresses.

agent_name = "JARVIS"

Un agent nommé ne rejoint aucune salle de son plein gré : il faut un dispatch explicite. Un client qui publie son micro sans convoquer parle dans le vide, sans la moindre erreur nulle part.

Diagnostic en une commande : lk agent status.

Plafond 1 — minutes de session

1 000 par mois. C'est celui qu'affiche le tableau de bord, et le seul que Boris peut relever.

Plafond 2 — crédit d'inférence

2,50 $, soit environ 5 h 40 de conversation. C'est lui qui tombe en premier.

Plafond 3 — minutes WebRTC

5 000 par mois. Elles courent application ouverte, micro fermé : c'est la connexion, pas la parole.

Aucune API de facturation

Le comptable déduit les minutes et affiche l'écart avec le relevé. Le tableau de bord retarde jusqu'à deux heures.

Module LiveKit04 / 12
04Automatisation n8n

L'orchestrateur route, il n'exécute pas

Un seul workflow reçoit tout : message, sessionId, canal. Il n'a aucun accès direct à Gmail, au Drive ou au web — il ne sait qu'appeler des sous-agents et lire ce qu'ils répondent.

  • Un sous-workflow, pas un chat. Le chat trigger d'origine répondait 200 sans jamais créer d'exécution. Il a été supprimé, pas réparé : les vraies portes sont Telegram et le serveur MCP.
  • Les sous-agents n'ont pas de mémoire, volontairement. Ils reçoivent tout leur contexte dans input. Une mémoire sans clé de session ferait fuiter le contexte entre deux appels sans rapport.
  • Le nom du champ $fromAI est un contrat. Treize outils annonçaient input dans leur description et mappaient instruction : aucun sous-agent n'était appelable. La flotte n'avait jamais tourné pour cette seule raison.
  • Le garde-fou est un nœud, pas une consigne. Le Contrôle de sincérité relit les étapes réellement exécutées et ampute toute promesse que rien n'honore. Une règle de prompt ne se vérifie pas.
  • Un canal déclaré change la sortie. En livekit, la réponse sera lue à voix haute : le markdown est retiré mécaniquement, trois phrases maximum, aucune URL.
Automatisation n8n05 / 12
05Automatisation n8n — la flotte

Seize sous-agents, un workflow chacun

Chaque sous-agent est un workflow n8n autonome, déclenché par executeWorkflowTrigger et branché sur l'orchestrateur en toolWorkflow. En typeVersion 2.2 le nom du nœud est le nom de l'outil.

agent_email
Lire, chercher, rédiger, envoyer.
Gmail
agent_calendrier
Créer, modifier, ajouter un participant.
Google Calendar
agent_taches
Listes et échéances.
Google Tasks
agent_contacts
Le carnet d'adresses : une adresse ne se devine jamais.
Airtable
agent_drive
Fichiers, plus un listing par date d'ajout.
Google Drive + API v3
agent_documents
Créer et rédiger un document.
Google Docs
agent_recherche_web
Recherche, encyclopédie, actualité, vidéo.
SerpApi · Wikipedia · HN · YouTube
agent_scraper
Lire une page en profondeur.
Firecrawl
agent_maps
Géocodage et itinéraires.
Mapbox MCP
agent_weather
Météo — Dijon par défaut.
API météo
agent_creation_visuelle
Images et vidéos, sur devis interne.
fal.ai
agent_initiatives
Veille du matin, ménage, propositions.
Gmail · Calendar · Tasks
agent_life_planner
Voyages : transport, hébergement, activités.
SerpApi · SNCF
agent_comptable
Ce que Jarvis dépense, poste par poste.
Postgres · API n8n
agent_spotify
Écrit en réel, en attente de sa credential OAuth.
Spotify — inactif
agent_reseaux_sociaux
Quatre credentials à créer.
LinkedIn · Meta · YouTube — inactif
La flotte06 / 12
06Le chemin d'une phrase

Ce qui se passe entre la question et la réponse

Cinq étapes, dont deux que Boris ne voit jamais : le chargement de la mémoire avant le modèle, et la vérification après lui.

  1. 1 EntréeMCP LiveKit ou Telegram. Le canal est déclaré, il changera la forme de la réponse.
  2. 2 Charger la mémoireUn appel Postgres, un aller-retour : faits retenus + trois derniers résumés.
  3. 3 RouterLe modèle appelle zéro, un ou plusieurs sous-agents, et lit ce qu'ils renvoient.
  4. 4 VérifierUn nœud Code compare la réponse aux outils réellement exécutés.
  5. 5 ArchiverLes deux tours partent en base. Rien n'est résumé maintenant.

Trois mensonges, trois filets

Une action annoncée sans qu'aucun outil ait tourné ; une source en erreur passée sous silence ; un envoi Telegram promis sur le canal voix, où rien ne peut le tenir.

Le cas qui ampute

Préfixer un avertissement devant une promesse intacte obligerait Boris à arbitrer entre deux phrases contradictoires. Le nœud retire la phrase, puis en ajoute une honnête.

Une alerte est prononcée

Les trois textes étaient d'abord écrits comme des consignes à un modèle. Aucun modèle ne tourne après ce nœud : l'agent vocal lisait la consigne à voix haute.

Le chemin d'une phrase07 / 12
07Mémoire PostgreSQL — le cycle

Écrire à chaud, distiller à froid

Rien n'est résumé pendant la conversation : le chemin chaud n'écrit que du texte brut. La distillation attend le silence, parce qu'une conversation ne dit ce qu'elle valait qu'une fois finie.

Chemin chaud — pendant que Boris parle

Deux appels, aucun modèle

Le coût du chemin chaud doit rester négligeable, sinon se souvenir devient plus cher que répondre.

1
chargement_initial() Une fonction SQL, un aller-retour. Elle rend les faits retenus et les trois derniers résumés de session, déjà mis en forme.
2
noter_tour() × 2 La question et la réponse partent en base. La fonction rattache le tour à la session ouverte sur cette clé, ou en ouvre une.
3
La clé de session livekit:boris pour la voix, telegram:<chatId> pour l'écrit. Deux fils séparés, une seule base.
Chemin froid — toutes les 15 minutes

Un seul modèle, une seule session

Une session traitée par passage : aucun appariement d'items à gérer, chaque exécution reste lisible.

4
cloturer_sessions() Une session est close après 30 minutes sans tour. En vocal la salle se ferme ; une conversation Telegram, elle, ne finit jamais explicitement : c'est le silence qui fait la frontière.
5
Sonnet 5 — sortie JSON stricte Il reçoit la transcription et les faits déjà connus, et rend un résumé, des sujets, et des faits assortis d'une opération. Environ 0,015 $ par conversation.
6
appliquer_fait() + embedding Chaque fait est vectorisé par text-embedding-3-small puis appliqué. Le résumé est écrit avant les faits : si un fait échoue, la session reste marquée traitée et l'exécution passe au rouge, sans boucle infinie.
Mémoire — le cycle08 / 12
08Mémoire PostgreSQL — le schéma

Quatre tables, et une seule qui compte vraiment

Les trois premières servent à produire la quatrième. tours est la source de vérité du texte échangé ; faits est ce que Jarvis emporte dans la conversation suivante.

categoriesla taxonomie
  • code text · PK
  • libelle text
  • poids real · défaut 1.0
  • description text
Une table plutôt qu'un ENUM : une catégorie s'ajoute avec un INSERT, et poids pondère déjà le score de recherche.
sessionsune conversation
  • id bigserial · PK
  • cle_session text
  • canal voix | telegram
  • debut timestamptz
  • dernier_tour timestamptz
  • fin timestamptz · null = ouverte
  • nb_tours integer
  • resume text
  • sujets text[]
  • traitee_at null = à distiller
  • cout_usd numeric(10,6)
toursle texte brut
  • id bigserial · PK
  • session_id → sessions.id
  • externe_id text · UNIQUE
  • role boris | jarvis
  • texte text
  • at timestamptz
externe_id porte l'idempotence : rejouer une exécution n8n ne duplique pas le tour.
index & contraintesce qui tient l'ensemble
  • faits_sujet_actif_idx UNIQUE (categorie, lower(sujet)) WHERE retire_at IS NULL
  • faits_embedding_idx USING hnsw (embedding vector_cosine_ops)
  • sessions_a_traiter_idx WHERE fin IS NOT NULL AND traitee_at IS NULL
L'index unique est partiel : un sujet retiré libère sa place, un sujet actif ne peut pas exister deux fois.
faitsle cœur de la mémoire
  • id bigserial · PK
  • categorie → categories.code
  • sujet text · sert au dédoublonnage
  • contenu text · une phrase autoportante
  • confiance declare | observe | deduit
  • statut observation → confirme → suggere
  • permanent boolean
  • occurrences integer
  • embedding vector(1536)
  • session_id → sessions.id
  • cree_at timestamptz
  • maj_at timestamptz
  • retire_at timestamptz
  • motif_retrait text
  • nb_rappels integer
  • dernier_rappel timestamptz
Rien n'est jamais supprimé. retire_at sort un fait de la circulation et garde son motif.
Mémoire — le schéma09 / 12
09Mémoire PostgreSQL — la structuration

Un fait, treize catégories, quatre opérations

Le modèle de distillation ne peut pas écrire librement en base. Il choisit une catégorie, une confiance, et une opération parmi quatre : tout le reste est du SQL.

Les treize catégories — table categories
identite_boris Qui est Boris identite_jarvis Qui Jarvis doit être preferences Goûts et façons de faire decisions Arbitrages tranchés, avec leur motif croyances Convictions de fond contraintes Temps, budget, matériel habitudes Rythmes et routines projets Chantiers actifs et prochain pas relations Personnes et leur lien aux projets vocabulaire Jargon propre à Boris erreurs_retours Ce qui n'a pas fonctionné energie Moments productifs, charge meta_memoire Ce qui marche dans le fait de se souvenir
La distinction qui porte tout le raisonnement

declare veut dire Boris l'a dit. observe veut dire Jarvis l'a constaté sur son propre fonctionnement — presque toujours un journal de bugs, souvent périmé (« YouTube inaccessible » suivi de « YouTube a marché »). Ces faits-là n'ont rien à faire dans le contexte d'une conversation.

Le poids de la confiance dans la recherche sémantique
declare× 1,00
observe× 0,90
deduit× 0,75

Le score final multiplie la similarité cosinus par le poids de la catégorie et par ce coefficient. Un fait déduit est présenté avec la mention « hypothèse ».

Les quatre opérations — fonction appliquer_fait()

ajouter

Insertion, avec repli sur mise à jour si le sujet existe déjà dans la catégorie.

confirmer

Incrémente occurrences. Un fait revu passe de deduit à observe.

remplacer

L'ancien sort de circulation, daté ; le nouveau est inséré en dessous.

retirer

Pose retire_at et un motif. Aucun DELETE n'existe dans le code.

Une ligne réelle de la table faits
id 3 categorie preferences sujet couleur préférée contenu « La couleur préférée de Boris est le bleu. » confiance declare permanent false occurrences 1 embedding vector(1536) retire_at NULL
Mémoire — la structuration10 / 12
10Mémoire PostgreSQL — la lecture

Écrire un fait ne suffit pas : encore faut-il le relire

Une mémoire ne vaut que par son critère de chargement. Celui de Jarvis a été faux pendant cinq jours, et la panne était invisible : la base était pleine, indexée, et jamais lue.

chargement_initial(cle_session) — à chaque tour

Un seul appel SQL, deux blocs : les faits retenus, puis les trois derniers résumés de session, horodatés et étiquetés par canal.

Critère : permanent OU confiance = 'declare', trié par poids de catégorie puis par fraîcheur, plafonné à 30 faits pour protéger le prompt.

recherche_semantique(embedding, limite, seuil)

Similarité cosinus sur l'index HNSW, seuil à 0.35, huit résultats par défaut, pondérés par catégorie et par confiance.

Écrite, indexée, pas encore appelée. Tant qu'aucun outil ne l'invoque, la profondeur de l'historique reste inexploitée : c'est le premier chantier de la mémoire.

Ce que la mémoire a déjà produit

Elle a signalé d'elle-même deux échecs répétés de l'agent calendrier. L'enquête a trouvé quatre défauts réels, dont une heure de fin explicite silencieusement écrasée. Aucun autre mécanisme ne les aurait vus.

Le fait invisible — corrigé le 21 août 2026
22/24

faits enregistrés, indexés, jamais chargés

+40

tokens par appel, le prix du correctif

La leçon

Boris signale que Jarvis a oublié sa couleur préférée. Le fait existait bien : catégorie preferences, confiance declare, enregistré cinq jours plus tôt. Mais permanent valait false, et le chargement ne lisait que les faits permanents.

Un fait invisible équivaut à un fait absent. Et un fait permanent faux est pire qu'un fait manquant : il est chargé dans chaque conversation et se donne l'autorité d'une certitude.

Mémoire — la lecture11 / 12
11Ce que l'ensemble coûte

6,29 $ par mois, et la répartition renverse l'intuition

Chiffres relevés par l'agent comptable, qui lit l'API n8n hors bande et n'appelle aucun modèle sur son chemin normal. Un monitoring ne doit jamais coûter plus cher que ce qu'il surveille.

L'orchestrateur coûte six fois l'agent vocal — et il n'apparaissait dans aucun tableau de bord fournisseur. Son prompt système repart en entier à chaque itération d'outil.

Ce que l'architecture a appris

Un chiffre déduit s'affiche à côté du relevé du fournisseur, jamais à sa place. La première mesure LiveKit était fausse d'un facteur deux : elle prenait l'amplitude de la session mémoire, qui contenait un trou de 23,8 minutes. Une session mémoire n'est pas une session facturée.

Une règle de prompt ne se vérifie pas. Les garde-fous qui tiennent sont des nœuds Code : ils relisent ce qui a réellement tourné. Ceux qui étaient écrits dans le prompt ont tous été pris en défaut.

Ne jamais valider à l'oreille. La première réponse vocale « correcte » citait une ville jamais demandée et un mois faux. Seule la lecture de l'exécution n8n montre ce qui a été transcrit et ce qui a été produit.

Jarvis — architecture12 / 12