Architecture Caméléon CVM — Opérateurs & Moteur

Version : v0.21.6-dev Auteurs : O. Cugnon de Sévricourt, C. Cugnon de Sévricourt


1. Pourquoi Caméléon ?

Le problème : orchestration IA = graphe complexe

Les pipelines IA modernes sont des graphes dirigés avec branchements, boucles, routage conditionnel et appels LLM coûteux. Trois exigences émergent que les outils actuels ne couvrent pas formellement :

Les 5 principes fondateurs

  1. Formalisme Petri — vérifiabilité mathématique (vivacité, bornitude, accessibilité). Le moteur est un interpréteur de compositions basé sur un modèle formel de réseau de Petri étendu, formalisé dans un article peer-reviewed (arXiv:1110.4802).
  2. Bidirectionalité — forward et backward natifs. Pas d'exécution one-shot : le rollback est garanti à chaque step via l'historique déterministe.
  3. Séparation données / flux — opérateurs structurels (flux pur, copie de références) vs behavioral (traitement de données, interaction, injection). Le moteur ne connaît que canFire(), run(), rollback().
  4. Auto-description — un opérateur porte sa règle de tir, son exécution et son rollback. Pas de dispatch centralisé dans le moteur — Template Method sur l'opérateur.
  5. Bijection prompt → pipeline — un LLM peut générer un fichier .cm valide en connaissant juste le catalogue d'opérateurs. Le .cm est pur topologie.
CVM Moteur formel Formalisme Petri Vivacité · Bornitude · Accessibilité Bidirectionalité Forward · Backward · Rollback Séparation flux / données Structurels vs Transformants Auto-description canFire · run · rollback Bijection prompt → pipeline LLM génère un .cm valide Chaque principe est garanti par le modèle formel, pas par convention
Fig. 1 — Les 5 principes fondateurs de Caméléon

Positionnement vs outils existants

CritèreLangChain / LangGraphCrewAICaméléon CVM
Modèle d'exécutionEvent-driven / DAGAgent-basedPetri net formel
Vérification formelleNonNonOui (vivacité, bornitude)
RéversibilitéNonNonOui (backward natif)
Séparation flux / donnéesPartielleNonOui (2 familles)
ReproductibilitéManuelleNonOui (historique déterministe)
ExtensibilitéPlugin customAgent customObjet dans lib (canFire/run/rollback)
Standard patternsAucunAucunWCP van der Aalst
EVENT-DRIVEN LangChain · LangGraph · CrewAI · Exécution DAG ou agent-based · Pas de preuve formelle · Pas de rollback natif · Debug = logs · Reproductibilité non garantie · Pas de standard (ad hoc) PETRI FORMEL Caméléon CVM ✓ Vivacité (le pipeline progresse) ✓ Bornitude (pas d'explosion de tokens) ✓ Accessibilité (états atteignables connus) ✓ Rollback natif (backward + historique) ✓ Replay déterministe ✓ Standard WCP (43 patterns) Formalisé en Coloured Petri Nets (Jensen 1992)
Fig. 2 — Garanties formelles Caméléon vs orchestrateurs event-driven

Filiation : Petri 1962 (places + transitions) → Jensen 1992 (tokens typés = CPN) → van der Aalst 1999 (Workflow Patterns en CPN) → Caméléon (connexion = place, opérateur = transition, données typées = coloured tokens)


2. Architecture Caméléon — les 4 couches

Le découplage fondamental

L'architecture repose sur 4 couches indépendantes, chacune testable et utilisable séparément. Aucune couche ne dépend d'une couche au-dessus d'elle :

RENDERER cameleon.html — Visual | Code | Split Pas de logique métier state, fire CVM ENGINE cvm-engine.js — forward() | backward() | run() Pas de connaissance opérateur lit topologie canFire / run / rollback COMPOSITION .cm.js — instances + connexions + positions Topologie pure référence types OPERATORS LIB operators-lib.js — canFire() | run() | rollback() Auto-descriptif + testable
Fig. 3 — Les 4 couches de l'architecture Caméléon

Format .cm (composition)

Le fichier .cm ne porte que la topologie — instances, connexions et layout visuel :


# pipeline.cm — topologie pure
operators:
  - id: pdf_loader
    label: PDF Loader
    instance: 1
    x: 90
    y: 150

connections:
  - from: pdf_loader.out_pdf
    to: binary_to_string.in_binary

Évolution : dans la version cible avec operators-lib.js, le .cm ne portera plus les inputs/outputs (ils viennent de la lib). Il ne restera que type, instance, x, y et les connexions.

Flux de données : du .cm à l'exécution

.cm topologie CVM Engine résout types → lib OPS + CONNS instances résolues Exécution canFire → run → update operators-lib.js références history[cursor] Renderer state updates
Fig. 4 — Flux de données : chargement d'un .cm et exécution par le moteur

Extensibilité : ajouter un opérateur

Ajouter un opérateur = ajouter un seul objet dans operators-lib.js. Rien d'autre à modifier. Le moteur le découvre via canFire(), le renderer l'affiche via les déclarations de ports de la lib.

Renderer × pas modifié CVM Engine × pas modifié .cm × pas modifié + Nouvel opérateur operators-lib.js — seul fichier modifié auto-découverte
Fig. 5 — Extensibilité : ajouter un opérateur ne touche que la lib

Extensibilité renderer

Ajouter un viewer ou un éditeur = se brancher sur l'état CVM. Le renderer lit l'état du moteur, s'abonne aux changements, et rend. Les 3 vues existantes (Visual, Code, Split) sont des exemples. De futures vues (timeline, debug, profiling) pourront être ajoutées indépendamment.

⚠️ Note : le POC v0.12 intègre encore les 4 couches dans un fichier unique (cameleon-cvm-poc-0.12.html). L'extraction en fichiers séparés est prévue à partir de la v0.13.


3. Architecture CVM — le moteur

La CVM (Caméléon Virtual Machine) est le moteur d'exécution. Elle implémente un interpréteur de compositions basé sur un modèle formel de réseau de Petri : connexions = places, opérateurs = transitions, données = coloured tokens.

Règle de tir par défaut

La règle standard d'exécution d'un opérateur dans Caméléon est ANY + garde EMPTY :

Conséquence : au premier passage, l'opérateur attend que chaque entrée ait reçu au moins une donnée. Ensuite il devient réactif — il tire dès qu'un input est rafraîchi, même si les autres sont OLD.

Historique : ce comportement réactif était déjà celui de l'implémentation originale de Caméléon en C++/QT. L'opérateur Sync a été créé précisément pour imposer une barrière ALL quand nécessaire.

NE TIRE PAS in_a NEW Op in_b EMPTY × garde EMPTY in_a OLD Op in_b OLD × aucun NEW TIRE ✓ in_a NEW Op in_b OLD in_a NEW Op in_b NEW Policy ANY : au moins 1 NEW, pas de EMPTY
Fig. 6 — Règle de tir ANY + garde EMPTY (défaut)

Les trois firing policies

Chaque opérateur porte une propriété firingPolicy qui détermine sa condition de tir. Si omise, le défaut est ANY.

PolicyCondition de tirCas d'usage
ANYAu moins 1 entrée NEW, aucune EMPTYDéfaut — Processor, Fork, Sink, Accumulator, Join/Merge
ALLToutes les entrées sont NEWSync (barrière de synchronisation explicite)
CONDSous-ensemble spécifique NEW (délégué à l'opérateur)Switch, Gate

⚠️ Source a une règle spéciale : il tire si toutes ses sorties sont EMPTY (producteur initial de données).

ANY (DÉFAUT) ≥ 1 entrée NEW 0 entrée EMPTY Processor • Fork • Sink Accumulator • Merge Réactif après 1er passage ALL Toutes entrées NEW simultanément Sync Barrière de synchronisation COND Sous-ensemble spécifique délégué à op.condition() Switch • Gate Routage conditionnel
Fig. 7 — Les trois firing policies

Deux familles d'opérateurs

Les opérateurs Caméléon se répartissent en deux familles :

Opérateurs Structurels Contrôle de flux Copie de références Sync ALL N → N Fork ANY 1 → N Merge ANY N → 1 Switch COND 1 → N Gate COND 1 → 1 Transformants Traitement de données run() produit des données Source SPÉCIAL 0 → N Sink ANY N → 0 Processor ANY N → M Accum. ANY N → M ANY (défaut) ALL COND
Fig. 8 — Taxonomie complète des opérateurs

Opérateurs structurels

Les structurels contrôlent le flux sans transformer les données. Leur run() copie des références, pas de calcul.

OpérateurPortsPolicyRôle
SyncN → NALLBarrière — ne tire que si toutes les entrées sont NEW simultanément. Copie les références en sortie.
Fork1 → NANYDuplique une donnée vers N sorties
Join / MergeN → 1ANYFusionne N branches — pièce maîtresse des boucles
Switch1 → NCONDRoutage conditionnel — active une seule sortie
Gate1 → 1CONDLaisse passer ou bloque selon un signal de contrôle

Opérateurs behavioral

Les behavioral effectuent un traitement de données, une interaction humaine ou une injection live. Leur run() produit de nouvelles données en sortie ou déclenche une interaction.

OpérateurPortsPolicyRôle
Source0 → NSpécialProducteur initial — tire si toutes ses sorties sont EMPTY
SinkN → 0ANYConsomme / termine un flux
ProcessorN → MANYTransformation générique
AccumulatorN → MANYTraitement par lot (batch)

canFire() — méthode sur l'opérateur

La règle de tir est une méthode portée par l'opérateur (Template Method), pas un dispatch centralisé dans le moteur. Le moteur appelle op.canFire(), point.

OpérateurcanFire()Logique
Défaut (ANY)!EMPTY && ≥1 NEWHérité par Processor, Fork, Merge, Sink, Accumulator
Sync (ALL)all NEWBarrière de synchronisation
Switch (COND)in_data NEWDélégué à l'opérateur
Sourceall outputs EMPTYProducteur initial — règle inversée

// Défaut (ANY + garde EMPTY) — hérité par Processor, Fork, Merge, Sink, Accumulator
const Operator = {
  canFire(inputStates, outputStates) {
    if (inputStates.some(s => s === "EMPTY")) return false;
    return inputStates.some(s => s === "NEW");
  }
};

// Overrides structurels
Sync.canFire   = (in) => in.every(s => s === "NEW");         // ALL
Switch.canFire = (in) => in.in_data === "NEW";               // COND custom
Source.canFire = (in, out) => out.every(s => s === "EMPTY"); // sorties EMPTY
engine.fireable() ops.filter(op => op.canFire(...)) ANY (défaut) !EMPTY && ≥1 NEW ALL (Sync) all NEW COND (Switch/Gate) délégué à op Template Method — le moteur ne connaît pas les règles, il appelle canFire()
Fig. 9 — canFire() : dispatch Template Method sur l'opérateur

POC v0.12 : le moteur actuel utilise un canFire() centralisé dans fireable(). La migration vers Template Method est prévue pour l'extraction cvm.js (v0.14+).

Boucles

Les connexions peuvent revenir à NEW après OLD (cycle dans le graphe). Le Merge (ANY) est la pièce maîtresse : il réinjecte un token dans la boucle.

PATTERN BOUCLE : MERGE + GUARD entrée Merge ANY Processor run() Guard COND sortie condition vraie feedback — reconnexion arrivée condition fausse → retour à Merge flux forward feedback loop exit conditionnel
Fig. 10 — Pattern boucle : Merge + Guard avec feedback

Modes d'exécution

ModeComportementHistorique
Séquentiel (actuel)Un seul opérateur tire par stephistory[i] = un tir
Concurrent (futur)Tous les READY tirent simultanémenthistory[i] = groupe de tirs
Configuration : `execution.mode: sequentialconcurrent dans le .cm`.
SÉQUENTIEL Op A Op B Op C step 1 step 2 step 3 CONCURRENT (futur) Op A Op B Op C step 1 (groupe) history = [A, B, C, ...] history = [{A,B,C}, ...] Le mode concurrent est équivalent au tir simultané en Petri net L'historique déterministe est préservé dans les deux modes
Fig. 11 — Modes d'exécution : séquentiel vs concurrent

Signature type dans operators-lib

Chaque opérateur est un objet auto-descriptif dans operators-lib.js. Convention : si firingPolicy est omis, le défaut ANY s'applique.


// Behavioral (Processor) — firingPolicy omis = ANY
{
  id: "processor_example",
  label: "Mon Processor",
  family: "behavioral",
  inputs:  [{ id: "in_a", type: "DataString" }],
  outputs: [{ id: "out_a", type: "DataString" }],
  run(inputs) {
    return { out_a: transform(inputs.in_a) };
  },
  rollback(outputs) { /* inverse */ }
}

// Structurel (Sync) — firingPolicy explicitement ALL
{
  id: "sync",
  label: "Sync",
  family: "structural",
  firingPolicy: "ALL",
  inputs:  [{ id: "in_a", type: "Any" }, { id: "in_b", type: "Any" }],
  outputs: [{ id: "out_a", type: "Any" }, { id: "out_b", type: "Any" }],
  run(inputs) {
    return { out_a: inputs.in_a, out_b: inputs.in_b };  // copie de ref
  },
  rollback(outputs) { /* inverse */ }
}

// Source — règle spéciale (tire si sorties EMPTY)
{
  id: "pdf_loader",
  label: "PDF Loader",
  family: "behavioral",
  inputs:  [],
  outputs: [{ id: "out_pdf", type: "Binary" }],
  canFire(inputStates, outputStates) {
    return outputStates.every(s => s === "EMPTY");
  },
  run(inputs) {
    return { out_pdf: /* binary data */ };
  },
  rollback(outputs) { /* inverse */ }
}

Convention : family vaut "structural" ou "behavioral". Le moteur utilise cette information pour le futur mode concurrent (structurels = copie de ref instantanée, behavioral = exécution asynchrone possible).

Pseudo-code moteur CVM

Le moteur est agnostique — il ne connaît aucun opérateur spécifique. Trois méthodes suffisent :


// 1. Liste des opérateurs READY
function fireable() {
  return OPS.filter(op =>
    op.canFire(inputStates(op), outputStates(op))
  );
}

// 2. Exécution forward
function execFire(op) {
  const inputs = collectInputs(op);
  const result = op.run(inputs);

  // Mettre à jour les états des connexions
  incomingConns(op).forEach(c => c.state = "OLD");
  outgoingConns(op).forEach(c => {
    c.state = "NEW";
    c.data = result[c.plugFrom];
  });

  pushHistory(op);
}

// 3. Exécution backward (rollback)
function execBackward() {
  const prev = popHistory();
  prev.op.rollback(prev.outputs);
  restoreState(prev.snapshot);
}

// Boucle principale (mode séquentiel)
function forward() {
  const ready = fireable();
  if (ready.length === 0) return detectEndState();
  execFire(ready[0]); // séquentiel = premier READY
}

// Détection état terminal
function detectEndState() {
  const hasNew   = CONNS.some(c => c.state === "NEW");
  const hasEmpty = CONNS.some(c => c.state === "EMPTY");
  if (!hasNew && !hasEmpty) return "COMPLETED";
  return "DEADLOCK";
}
BOUCLE PRINCIPALE DU MOTEUR CVM Start fireable() ready? oui execFire(ready[0]) loop non detectEndState() COMPLETED DEADLOCK tout OLD NEW ou EMPTY restants Le moteur ne contient aucune logique spécifique aux opérateurs
Fig. 12 — Boucle principale du moteur CVM

3 méthodes suffisent : fireable() + execFire() + execBackward(). Le moteur est agnostique — toute la logique spécifique est dans les opérateurs de la lib.

Workflow Patterns (WCP)

Référence : workflowpatterns.com — 43 patterns control-flow formalisés en Coloured Petri Nets par van der Aalst et al.

Correspondance directe (basiques WCP 1–5)

WCPPatternCaméléon
WCP-1SequenceImplicite (connexion)
WCP-2Parallel SplitFork (1→N)
WCP-3SynchronizationSync (ALL)
WCP-4Exclusive ChoiceSwitch (COND)
WCP-5Simple MergeMerge (ANY)

Composables avec nos primitives

WCPPatternRéalisation Caméléon
WCP-6Multi-ChoiceSwitch non-exclusif (multi-sortie)
WCP-8Multi-MergeMerge ANY (déjà couvert)
WCP-10Arbitrary CyclesMerge + Guard
WCP-11Implicit TerminationDétection Completed actuelle
WCP-21Structured LoopMerge + Guard (while/repeat)

Extensions futures prioritaires (pour IA)

WCPPatternExtension prévue
WCP-9DiscriminatorPolicy FIRST (premier LLM qui répond gagne)
WCP-12–15Multiple InstancesMapReduce (fork dynamique + sync)
WCP-19/20/25CancelSignal CANCEL propagé sur sous-graphe (timeout LLM)
WCP 1–5 : LES 5 PATTERNS FONDAMENTAUX WCP-1 Sequence A B WCP-2 Fork A B C WCP-3 Sync A B Sync WCP-4 Switch Sw B C WCP-5 Merge A B Merge Flux actif ALL COND Inactif
Fig. 13 — WCP 1–5 : les 5 patterns fondamentaux réalisés par les primitives Caméléon
STRATÉGIE DE COUVERTURE WCP Directs WCP 1–5 Sequence, Fork, Sync, Switch, Merge Composables WCP 6,8,10,11,21 Merge+Guard, Switch multi Extensions futures WCP-9 Discriminator policy FIRST WCP-12–15 Multi-Instance MapReduce WCP-19/20/25 Cancel signal CANCEL Prioritaires pour les use cases IA (timeout LLM, race, batch)
Fig. 14 — Stratégie de couverture des 43 Workflow Patterns

Stratégie : couvrir les 43 WCP en 3 cercles — directs (5 patterns = 5 primitives), composables (combinaison de primitives), extensions (nouvelles policies ou mécanismes). Référence : van der Aalst et al., Workflow Patterns: The Definitive Guide, MIT Press 2016.


4. Architecture Données

Types de données

Caméléon utilise un système de types simples pour les données transitant sur les connexions. Chaque type a une couleur associée pour le rendu visuel.

TypeCouleurDescription
Binary#a0b0c8Données binaires brutes (PDF, images, audio)
DataString#4f8affChaîne de caractères (texte, prompts, réponses LLM)
DataJSON#a78bfaStructure JSON (metadata, config, réponse structurée)
DataBool#f4845fBooléen (signal de contrôle, résultat de validation)
DataChunk#00e5a0Fragment de texte extrait (chunk RAG, embedding)

// Registre TYPES du POC
const TYPES = {
  Binary:     { color: "#a0b0c8", label: "Binary" },
  DataString: { color: "#4f8aff", label: "String" },
  DataJSON:   { color: "#a78bfa", label: "JSON" },
  DataBool:   { color: "#f4845f", label: "Bool" },
  DataChunk:  { color: "#00e5a0", label: "Chunk" },
};
Bin Binary PDF, images, audio #a0b0c8 Str DataString Texte, prompts #4f8aff { } DataJSON Metadata, config #a78bfa T/F DataBool Signal, validation #f4845f Ch DataChunk Fragment RAG #00e5a0
Fig. 15 — Les 5 types de données Caméléon

Typage des connexions

Le type d'une connexion est déterminé par le plug source (output). Le moteur valide la compatibilité entre le type du plug source et le type du plug cible (input).

RègleSourceCibleRésultat
IdentiqueDataStringDataString✓ Valide
Any acceptéDataStringAny✓ Valide (Any accepte tout)
Any sourceAnyDataJSON✓ Valide (vérifié au runtime)
IncompatibleBinaryDataBool✗ Rejeté
Coërcion (futur)DataJSONDataString≈ Via convertisseur implicite
VALIDE Str DataString Str ANY TARGET Str Any Any REJETÉ Bin Binary ≠ Bool Bool COËRCION (futur) { } JSON → Str Str La couleur de la connexion SVG est celle du type source (output plug)
Fig. 16 — Règles de typage des connexions

Sérialisation des données

Les données transitent sur les connexions sous forme de tokens typés. Le moteur ne regarde jamais le contenu — seuls les opérateurs interprètent les données.


// Transit données sur une connexion
connexion.state = "NEW";
connexion.data  = {
  type:  "DataString",
  value: "Résultat du LLM...",
  meta:  { timestamp: Date.now(), source: "gpt4" }
};

Extensibilité : ajouter un type


// Ajouter un type = une ligne dans le registre
TYPES.DataImage = { color: "#e879f9", label: "Image" };

Référence : les Data Patterns (Russell et al., 2005) complètent les Workflow Patterns en formalisant 40 patterns de visibilité, interaction et transfert de données. L'architecture Caméléon est conçue pour couvrir ces patterns à terme, en complément des WCP control-flow déjà couverts.


5. Questions ouvertes

QUESTIONS OUVERTES PAR DOMAINE Types Coërcion vs strict ? Restreindre Any ? Types composites ? List<T>, Map<K,V> ? Exécution Data Patterns ? Persistance state ? Propagation erreurs ? Retry intégré ? Topologie Topo dynamique ? Spawn sous-graphe ? Sandboxing run() ? Impact formel ? Versioning Migration .cm ? Version pinning ? Changelog ops ? Compatibilité ? Ces questions sont ouvertes — chacune fera l'objet d'un ADR dédié quand une décision sera prise
Fig. 17 — Questions ouvertes regroupées par domaine

Système de types

Coërcion vs strict Faut-il un mode strict (rejet immédiat si types différents) et un mode coërcion (conversion implicite via registre de convertisseurs) ? Ou un seul mode avec opt-in explicite ?

Type Any Any est actuellement accepté partout. Faut-il restreindre son usage aux structurels (Sync, Fork, Merge) et l'interdire dans les behavioral pour forcer le typage explicite ?

Types composites Faut-il des types paramétrés comme List<DataString> ou Map<String, DataJSON> ? Quand ? L'Accumulator en aurait besoin pour le batch.

Exécution

Data Patterns (Russell et al.) Les 40 Data Patterns couvrent la visibilité, l'interaction et le transfert de données. Comment les intégrer au modèle Caméléon sans compliquer l'API opérateur ?

Persistance Le history[] est en mémoire. Faut-il une persistance disque pour les pipelines longs (RAG sur corpus massif) ? Quel format de sérialisation pour le state snapshot ?

Propagation des erreurs Un run() qui échoue doit-il propager un token d'erreur typé (DataError) ou arrêter le pipeline ? Faut-il un mécanisme de retry intégré au moteur ou dans un opérateur dédié ?

Topologie et sécurité

Topologie dynamique Un opérateur peut-il créer des connexions à la volée (spawn de sous-graphe) ? C'est nécessaire pour WCP-12–15 (Multiple Instances / MapReduce). Comment cela affecte-t-il la vérification formelle ?

Sandboxing run() Les opérateurs run() exécutent du code arbitraire. Faut-il un sandboxing (Web Worker, VM isolée, Deno permissions) pour protéger le moteur ? Quel impact sur la performance ?

Versioning

Versions opérateurs dans .cm existants Si un opérateur change de signature (ajout d'un port, changement de type), comment gérer les .cm existants ? Migration automatique, version pinning, ou rejet avec message explicite ?

Ces questions sont ouvertes — chacune fera l'objet d'un ADR dédié quand une décision sera prise.