baml par xyzcharlize/hyperframes-genie

Compétence BAML minimale. BAML est un langage typé statiquement, orienté expression, avec des fonctions LLM de première classe — syntaxe proche de TypeScript, méthodes en snake_case, etc. Utile pour construire des workflows IA, agents et évaluations.

Qu'est-ce que hyperframes-genie ?

Cette compétence définit BAML, un langage de programmation complet et un DSL déclaratif conçus pour les workflows d'IA, les agents et les évaluations. BAML se distingue par la combinaison d'un langage de type TypeScript (avec typage statique, snake_case, classes, interfaces, génériques) et d'un DSL intégré pour les appels LLM : les fonctions marquées déclarent explicitement leur client et leur prompt, et la sortie structurée du modèle n'est qu'une valeur typée retournée par la fonction. La compétence couvre la syntaxe (expressions backtick avec interpolation `${...}`, `match` exhaustif, blocs comme expressions, pas d'opérateur ternaire), le système de types (classes comme types référence, champs mutables, typage optionnel `T?`, unions, énumérations), les tests et le débogage via la CLI (`baml describe`, `baml run`, `baml test`), la sécurité des ressources (`defer`, `cleanup`), la concurrence légère (`spawn`/`await`), les modèles de conception (erreurs typées avec `catch` spécifique, interfaces avec dispatch dynamique, `requires` comme alternative à l'héritage, wrapping de clients pour la fiabilité), et l'import du SDK généré (`baml generate`) depuis Python ou TypeScript.

Compatible avec✓Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/xyzcharlize/hyperframes-genie/tree/HEAD/.agents/skills/baml-core

Installed? Explore more Productivité et collaboration skills: steipete/gemini, steipete/gh-issues, steipete/skill-creator · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

baml

BAML est un langage typé statiquement, orienté expression — du TypeScript avec des méthodes en snake_case, des champs name: type,, des énumérations, des interfaces, des génériques, des fermetures, du chaînage optionnel, des chaînes backtick avec interpolation ${...}, .to_string() sur n'importe quelle valeur, une vraie bibliothèque standard. Et un DSL déclaratif pour les appels LLM (function … { client: prompt: }, test) qui se désucre en lui-même, de sorte que la sortie structurée d'un modèle n'est qu'une valeur typée retournée.

La CLI est la documentation. Découvrez via baml describe :

brew install baml                # Binaire CLI : `baml`

baml init                        # Nouveau projet (baml.toml + baml_src/)
baml help <commande>             # Options CLI et exemples
baml describe baml.json          # Référence pour tout module/type/méthode/signature/mot-clé
baml describe Array --budget 120 #   (Array, String, Map, assert, match, patterns, spawn, python, ...)
                                 #   se termine par "N more lines" ? relancez avec `--budget <N>`
baml check                       # Vérification à la compilation du projet
baml run -e 'expr'               # Évalue une expression ; feedback rapide + vérification syntaxique
baml test --list && baml test    # Exécute chaque bloc test/testset
baml fmt baml_src/main.baml      # Canonise le formatage du projet

baml describe <nom> affiche le corps source complet des fonctions de la bibliothèque standard — le moyen le plus rapide de vérifier le comportement, et le seul chemin pour les fonctions intégrées comme assert (pas de fichier sur disque). Les fonctions pures n'ont besoin ni de test ni de client — vérifiez-les avec baml run -e 'add(2, 3)'.

Ne décrivez pas les API déjà démontrées ci-dessous, sauf si vous rencontrez des erreurs. Vous pouvez commencer avec les exemples et les utiliser si vous rencontrez des erreurs supplémentaires ou si vous souhaitez des détails concrets sur la bibliothèque standard.

Il se comporte principalement comme JavaScript/TypeScript, avec une syntaxe très similaire — mais BAML est plus sûr/strict.

Bonnes pratiques et informations

  • Fonction LLM = retour typé. Le TYPE DE RETOUR est le schéma que le modèle doit produire (class, enum, union littérale, string[], T?). La sortie structurée n'est qu'une valeur typée — transmettez-la à du code ordinaire.
  • Les prompts sont des chaînes backtick avec interpolation ${...}. Écrivez prompt: … ${arg} …, et injectez toujours ${ctx.output_format()} pour un retour structuré. Échappez avec \`` / ${` ; imbriquez avec des backticks supplémentaires.
  • Les clients sont des valeurs, pas des blocs de configuration. client Fast = openai.ResponsesClient.new(model = "…", api_key = env.OPENAI_API_KEY); — l'ancien bloc client<llm> Nom { provider: …, options: {…} } est supprimé. Tout ce qui implémente ai.Client fonctionne (openai.ResponsesClient, openai.ChatClient, anthropic.Client, … ; les paramètres du constructeur diffèrent selon le fournisseur). api_key et base_url acceptent ai.Credential : une chaîne littérale, null pour la valeur par défaut du fournisseur, ou une référence env.NOM résolue au moment de la requête. Composez la fiabilité en encapsulant : ai.clients.Retry.new(inner = c, max_attempts = 3) et ai.clients.RoundRobin.new(members = […]) ont .new, mais ai.clients.Fallback { members: […] } n'en a pas — construisez-le comme un littéral de classe. Utilisez ensuite client: Fast dans la fonction, ou le raccourci client: "openai/gpt-4o-mini". baml describe openai / baml describe ai.Credential / baml describe ai.clients.
  • Façonnez le schéma avec des attributs de champ. @description("…") ajoute une indication /// que le modèle voit dans ${ctx.output_format()} ; @alias("name") renomme la clé JSON émise. Enchaînez : tags: string[] @alias("labels") @description("…").
  • Testez le code pur, pas le modèle. Testez unitairement l'orchestration/le post-traitement sur des données littérales avec assert.*. Appeler une fonction LLM dans un test avec un client réel fait une vraie requête. Pour tester les prompts, les requêtes et l'analyse hors ligne, utilisez F@spec(args) ou un client scripté (voir Exécution de fonctions IA).
  • Construisez des chaînes avec interpolation, pas coercition. score=${n} transforme n'importe quelle valeur en chaîne (.to_string() implicite) ; appelez .to_string() pour la chaîne seule. + nécessite que les deux côtés soient déjà des chaînes ("n=" + 5 ne compile pas).
  • catch pour certains, catch_all pour tous. expr catch (e) { baml.errors.ParseError => fallback } gère une erreur spécifique ; expr catch_all (e) { _ => fallback } est exhaustif — pour un sommet de workflow / point d'entrée. Les erreurs se propagent implicitement ; les appelants n'ont pas besoin de les redéclarer. Lancez avec throw baml.errors.InvalidArgument { message: "…" } (les types d'erreur sont les classes baml.errors.* intégrées — InvalidArgument/ParseError/Io/Timeout/… ; baml describe baml.errors) ; annotez une signature faillible avec -> T throws ErrType. Préférez une union de résultat typée (type R = Ok | Err) au throw pour le flux de contrôle ordinaire.
  • Interfaces = comportement partagé + dispatch dynamique. interface I { function m(self) -> T throws never } ; chaque méthode déclare sa clause throws. Une méthode avec un corps est une implémentation par défaut, qu'un implémenteur peut surcharger. Une classe adhère via implements I { … } ; une valeur typée I (ou I[]) dispatche vers l'implémenteur à l'exécution. Les interfaces peuvent aussi déclarer des types associés et des bornes génériques. baml describe interfaces.
  • requires, pas d'héritage. BAML n'a pas d'héritage : une classe ne peut pas étendre une classe. interface Pet requires Animal, Named { … } fait de ces interfaces des prérequis de Pet. Les corps par défaut de Pet peuvent appeler leurs méthodes, et une valeur typée Pet les expose. Chaque implémenteur de Pet écrit aussi ses propres blocs implements Animal et implements Named (sinon E0125). Un diamant de requires compile, et l'interface partagée est implémentée une fois ; un cycle est une erreur. Lorsque deux interfaces sur une même classe déclarent le même nom de méthode, un appel non qualifié est une erreur (E0121) ; appelez obj.as<I>.m() à la place.
  • Filtrage par motif (pattern matching). match (v) { … } sur des valeurs/types ; les branches sont motif => expr — littéraux, let x: T (lier + affiner), déstructuration de classe T { f: let y }, motifs ou A | B, gardes … if cond, _ ; doit être exhaustif. Aussi v is T → bool (affine) et if let x: T = v { … } else { … }. baml describe patterns.
  • Affinement — uniquement les variables locales. v is T, v != null / v == null et un test de vérité affinent v uniquement lorsque v est une variable locale (un let ou un paramètre) qui n'est capturée par aucune fermeture : dans la branche if / while gardée, après une sortie anticipée (if (!(v is T)) { return … }), et à droite de && (côté gauche vrai) et || (côté gauche faux) — v is T && v.f == 1, s == null || s.length() == 0. PAS affiné : une variable locale capturée, un chemin de champ (h.v is T && h.v.f → E0007) et un chemin d'index (xs[0] is T). Les tâches peuvent s'intercaler entre les instructions, donc une autre tâche ou un alias peut changer un champ entre le test et la lecture. Pour un champ, écrivez d'abord let v = h.v;, ou if let v: T = h.v { … }, ou match (h.v) { let v: T => … } (chacun lit le champ une fois). Une affectation à la variable locale remplace le fait par le type assigné. baml describe is.
  • Concurrence = threads verts. spawn { … } retourne un Future ; await le collecte. Combinez-en plusieurs avec baml.future.all / all_settled / race / any. all_settled retourne Success<T>, Failure<E>, ou Panicked pour chaque entrée ; l'annulation d'une entrée est un résultat Panicked. Configurez un spawn avec une clause with : spawn with limit, token { … } — chaque valeur listée est un baml.spawn.Modifier qui transforme le plan que le spawn construit, de gauche à droite. baml.spawn.Limit.new(n) limite la concurrence (les spawns en excès sont mis en file d'attente), un baml.spawn.CancelToken annule coopérativement, baml.spawn.Root.new() détache la tâche de l'annulation de son spawner. baml describe spawn / baml describe baml.future.
  • Sécurité des ressources — defer, cleanup, catch (e, ctx). defer { … } exécute un bloc à la sortie de la portée, LIFO, sur tous les chemins (return / throw / fall-through) — comme Go. Une méthode de classe nommée function cleanup(self) -> void est un finaliseur : elle s'exécute au plus une fois par instance, que vous l'appeliez, la deferiez, ou que le GC la récupère. catch (e, ctx) lie un ErrorContext en plus de l'erreur — une erreur levée lors de la gestion d'une autre s'enchaîne dessus, donc ctx.root_cause() / ctx.cause remontent jusqu'à l'échec original et ctx.to_string() affiche toute la chaîne (style Python __context__). while let MOTIF = expr { … } boucle jusqu'à ce que le motif échoue (par exemple, vider un .pop() retournant T?).
  • Appelez BAML depuis Python/TS. Déclarez [generator.<nom>] dans baml.toml, exécutez baml generate, puis importez le baml_sdk typé. Installation + utilisation : baml describe python / baml describe typescript / baml describe baml_sdk.
  • Accès sécurisé plutôt qu'indexation. L'indice panique sur un index/clé manquant ; utilisez .at(i)/.get(k) (→ T?), traversez avec ?., valeur par défaut avec ?? (parenthésé : (m.get(k) ?? 0) + 1).
  • Les méthodes de la stdlib sont en snake_case, appelées sur une valeur. Certaines retournent une nouvelle, d'autres mutent sur place, quelques-unes font les deux (sort_by_key trie le récepteur et le retourne) — pour lire la documentation, baml describe <mot/type/identifiant/mot-clé/etc>.
  • Champs de classe name: type, ; construction Type { field: val }. Les méthodes prennent un self nu ; les factories sont des fonctions libres. Les champs sont mutables (comme TS) : obj.field = v et obj.field += n fonctionnent, et une méthode self peut muter sur place — une méthode avec effet de bord retourne void. Les classes sont des types référence : find/at(i)/indice retournent un alias vivant, pas une copie, donc muter le résultat mute cet élément dans le tableau (xs.find(p)?.n += 1 met à jour xs), et une classe passée à une fonction peut être mutée par l'appelé. La propagation de mise à jour de structure est supportée : User { ...u, tier: Tier.Free }. Les classes vides sont légales (class Marker {}) — pratiques comme variantes d'union. Les énumérations sont des variantes simples — pas de méthodes, pas de données associées (E.A.foo() ne compile pas) ; mettez le comportement dans des fonctions libres qui font match. enum E { A, B }, accès E.A.
  • Les blocs sont des expressions — la dernière expression est la valeur (pas de ;) ; return x; pour une sortie anticipée. Une fonction à effet de bord uniquement retourne void — une autre orthographe de null, le type unité — et peut omettre son expression finale ou return;. Une valeur laissée comme expression finale d'un tel corps est une erreur, pas ignorée : terminez-la par ; (xs.push(x); — push retourne la nouvelle longueur). for (let x in xs) itère sur les VALEURS ; while (cond) { … } boucle. Les fermetures (x) -> { ... } infèrent le paramètre/retour du contexte (annotez (x: T) -> R uniquement en cas d'ambiguïté ; le -> est requis). .map/.filter retournent directement des tableaux (pas de .collect()). Une carte vide nécessite un type : let m: map<string, int> = {};.
  • Pas d'opérateur ternaire — if/else est l'expression. Il n'y a pas de cond ? a : b ; if (cond) { a } else { b } est une expression qui retourne une valeur, donc assignez-la directement : let label = if (x > 3) { "big" } else { "small" };. Chaque branche est un bloc dont la dernière expression est sa valeur (pas de return). Enchaînez avec else if, et associez avec if let MOTIF = expr { … } else { … } pour lier et affiner.
  • Les conditions utilisent la vérité. if, while, les gardes de match, &&, ||, et ! acceptent n'importe quelle valeur. Les valeurs fausses sont false, null, zéro numérique, les chaînes vides, les tableaux/cartes vides, et les bytes vides ; tout le reste est vrai. && et || retournent bool, pas une valeur d'opérande ; ils court-circuitent. Une variable locale optionnelle vraie est affinée dans la branche prise et à droite de && (s && s.length() > 0).
  • Les tableaux ont un ensemble de méthodes similaire à JS — map/filter/filter_map/reduce/find/some/every/flat_map/slice/concat/join/includes/length(), plus les mutations sur place push/pop/shift/unshift/sort_by/sort_by_key. La plupart prennent des fermetures qui peuvent throws. baml describe Array donne plus d'informations.
  • Les liaisons locales let sont réassignables (x = x + 1) — pas de mot-clé mut (c'est TS let, pas Rust) ; il n'y a pas non plus de const.
  • Args : valeurs par défaut avec =, appels par mots-clés avec = (jamais :). Déclarez une valeur par défaut dans la signature : function f(a: int, b: int = 10) ; appelez f(1) ou f(1, b = 2). Un paramètre avec valeur par défaut doit être passé par nom — f(1, 2) est une erreur (le paramètre par défaut 'b' doit être passé par nom). Tout paramètre (même requis) peut être passé par nom (f(a = 1, b = 2)), et vous pouvez sauter une valeur par défaut du milieu pour en définir une plus tard (f(1, c = 9)). La syntaxe des mots-clés est name = value ; name: value ne sera pas analysé (: est pour les types/champs). T? NE rend PAS un argument optionnel — contrairement à TS b?: T, un paramètre b: T? est toujours requis (vous devez passer null, sinon N argument(s) attendu(s), reçu(s) …) ; ajoutez = null pour le rendre omissible. Les fonctions intégrées suivent cela : baml.http.fetch(url, timeout = baml.time.Duration.from_seconds(10)).
  • Où il diverge de TS (les pièges silencieux) : l'arithmétique est guidée par le type, pas à la TS. int / int est une division entière tronquante (285 / 100 == 2, PAS 2.85) et % est le reste (285 % 100 == 85) ; cela compile très bien et donne un nombre discrètement faux, donc c'est le piège le plus important. Ajoutez un float pour obtenir une division flottante (285 / 100.0 == 2.85, 285.0 / 100 == 2.85) ; toute opération mixte int/float promeut en float (5 + 2.0 == 7.0). Il n'y a pas de .to_float() — convertissez un int avec n * 1.0 (ou divisez par un float). Un résultat int ne se coerce pas automatiquement en float lors de l'assignation (let x: float = 285 / 100 est une erreur de compilation). + est uniquement numérique : la concaténation de chaînes nécessite que les deux côtés soient déjà string ("n=" + 5 ne compile pas — utilisez l'interpolation ${...}). Les comparaisons (==, <, …, == structurelle) et &&/||/! sont comme en TS.
  • Tests : test "nom" { ... } seul (pas de wrapper) ; testset sert seulement à GROUPER. Assertions (seulement 6) : assert.equal/approx_equal/is_true/is_type/not_null/contains. assert.equal compare structurellement (profondément, à travers classes/tableaux/maps) — et == simple aussi, qui est la forme booléenne. assert.equal est exact sur les floats ; utilisez assert.approx_equal(réel, attendu, eps) pour les valeurs calculées. Dernière assertion : pas de ; final. Les ID canoniques sont root::NomTest pour un test de premier niveau et root::Testset::NomTest à l'intérieur d'un testset. Exécutez-en un avec baml test <id>. Testez du code pur directement : baml run -e 'assert.equal(add(2, 3), 5)'.
  • Exécution de fonctions IA : f(args) — appelle la fonction LLM normale. f@spec(args) — réexécute uniquement le post-traitement avec le résultat en cache d'une précédente exécution de f ; cela contourne le LLM. f@spec(args, spec_id = "ma_spec") — utilise un bloc test nommé comme source du résultat en cache. baml describe ai donne run, spec, full_spec, stream, chat.
  • Environnement et variables globales. env.NOM_VAR résout une variable d'environnement au moment de l'appel ; ctx est le contexte d'exécution implicite (.output_format(), .session_id, .callbacks). $E.Nom est une constante de compilation. baml describe env / baml describe ctx / baml describe dollar.
  • Généralisation/méta-programmation : E.Nom pour les variantes d'énumération. Les métadonnées sur les énumérations se font via @description sur le type ou le champ. baml describe $ / baml describe E.
  • use explicite : use stdlib.io.{ read_file, write_file } ; use importe dans la portée locale. baml describe use.
  • Les alias créent des noms synonymes : alias MiCadena = string;. baml describe alias.
  • Types littéraux : let x: "rouge" | "bleu" = "rouge";. baml describe types.
  • Valeurs par défaut des champs : class C { val: int = 10, }. baml describe classes.
  • Tâches = async implicite + parallélisme memory-safe : Les fonctions marquées task s'exécutent sur un pool de threads ; les tâches partagent l'espace d'adressage (pas de mémoire partagée concurrente). Fonctions marquées task (concurrentes, pas de mémoire partagée). Tâches de type thread (vrai parallélisme, sans async/await sur le fil Python) ; les fonctions task sont async par défaut. baml describe tasks.
  • Valeurs volatiles — atomic : class Shared { @atomic counter: int, } — les accès counter sont atomiques ; pas besoin de verrouillage manuel. Les lectures/écritures atomic sont cohérentes entre les threads. baml describe atomic.
  • type = alias d'union : type MonUnion = A | B; — où A, B sont des classes. type est aussi utilisé pour les types littéraux. baml describe type.
  • Mots-clés de contrôle : break, continue dans les boucles. break prend une valeur optionnelle (break val). baml describe break.
  • Commentaires de documentation /// : attachés aux déclarations (classes, fonctions, interfaces). baml describe ///.
  • Le module baml.json : baml define + baml describe génèrent/consultent la documentation de la bibliothèque standard.

Skills associés