hyperframes-genie / BAML

Minimale BAML-Fähigkeit. BAML ist eine statisch typisierte, ausdrucksorientierte Sprache mit erstklassigen LLM-Funktionen – ähnlich wie TypeScript, snake_case-Methoden usw. Nützlich zum Erstellen von KI-Workflows, Agenten und Evaluierungen.

Was ist BAML?

Diese Fähigkeit bietet eine umfassende Einführung und Referenz für BAML, eine statisch typisierte, ausdrucksorientierte Programmiersprache mit einer deklarativen DSL für LLM-Aufrufe. Entwickler können BAML über die Befehlszeile (CLI) mit `baml describe` erkunden, um die vollständige Quelle aller Standardbibliotheksfunktionen zu erhalten. BAML verhält sich weitgehend wie JavaScript/TypeScript, ist aber strenger und sounder. Es unterstützt Funktionen wie strukturierte Ausgaben für LLMs (Rückgabetyp = Schema), Prompt-Interpolation mit Backtick-Strings und `${...}`, polymorphe Clients (openai, anthropic usw.), Interfaces mit dynamischem Dispatch, Pattern Matching, grüne Threads für Concurrency (`spawn`/`await`) und Ressourcensicherheit mit `defer`/`cleanup`. Die Sprache kann aus Python und TypeScript/JavaScript aufgerufen werden und bietet eine vollständige Toolchain mit einem Compiler (`baml check`), Code-Generierung und Testsuiten.

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

Installed? Explore more Produktivität & Zusammenarbeit skills: steipete/gemini, steipete/gh-issues, steipete/skill-creator · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

baml

BAML ist eine statisch typisierte, ausdrucksorientierte Sprache – TypeScript mit snake_case-Methoden, name: type,-Feldern, Aufzählungen, Interfaces, Generics, Closures, optionalem Chaining, Backtick-Strings mit ${...}-Interpolation, .to_string() auf jedem Wert, einer echten Standardbibliothek. Und einer deklarativen DSL für LLM-Aufrufe (function … { client: prompt: }, test), die sich darin auflöst, sodass die strukturierte Ausgabe eines Modells einfach ein typisierter Rückgabewert ist.

Die CLI ist die Dokumentation. Entdecke via baml describe:

brew install baml                # CLI-Binärdatei: `baml`

baml init                        # neues Projekt (baml.toml + baml_src/)
baml help <command>              # CLI-Optionen und Beispiele
baml describe baml.json          # Referenz für jedes Modul/jeden Typ/jede Methode/jede Signatur/jedes Schlüsselwort
baml describe Array --budget 120 #   (Array, String, Map, assert, match, patterns, spawn, python, ...)
                                 #   endet mit „N more lines“? erneut ausführen mit `--budget <N>`
baml check                       # Projekt auf Kompilierungsfehler prüfen
baml run -e 'expr'               # Ausdruck auswerten; schnelles Feedback + Syntaxprüfung
baml test --list && baml test    # jeden test/testset-Block ausführen
baml fmt baml_src/main.baml      # Formatierung des Projekts kanonisieren

baml describe <name> gibt den vollständigen Quelltext der Stdlib-Funktionen aus – der schnellste Weg, das Verhalten zu überprüfen, und der einzige Pfad für eingebaute Funktionen wie assert (keine Datei auf der Festplatte). Reine Funktionen benötigen keinen Test/Client – überprüfe sie mit baml run -e 'add(2, 3)'.

Beschreibe keine APIs, die bereits unten demonstriert werden, es sei denn, du stößt auf Fehler. Du kannst auf der Grundlage der Beispiele beginnen und sie bei weiteren Fehlern oder für tatsächliche Stdlib-Details verwenden.

Es verhält sich meistens wie JavaScript/TypeScript, mit sehr ähnlicher Syntax – aber BAML ist sounder/strikter.

Best Practices und Informationen

  • LLM-Funktion = typisierter Rückgabewert. Der RÜCKGABETYP ist das Schema, das das Modell produzieren muss (class, enum, Literal-Union, string[], T?). Strukturierte Ausgabe ist einfach ein typisierter Wert – übergib ihn an gewöhnlichen Code.
  • Prompts sind Backtick-Strings mit ${...}-Interpolation. Schreibe prompt: … ${arg} … und injiziere immer ${ctx.output_format()} für eine strukturierte Rückgabe. Escape mit \`` / ${`; verschachteln mit zusätzlichen Backticks.
  • Clients sind Werte, keine Konfigurationsblöcke. client Fast = openai.ResponsesClient.new(model = "…", api_key = env.OPENAI_API_KEY); – der alte Block client<llm> Name { provider: …, options: {…} } ist entfernt. Alles, was ai.Client implementiert, funktioniert (openai.ResponsesClient, openai.ChatClient, anthropic.Client, …; Konstruktorparameter unterscheiden sich je nach Anbieter). api_key und base_url akzeptieren ai.Credential: einen Literal-String, null für den Standard des Anbieters oder eine spät gebundene env.NAME-Referenz, die zur Anfragezeit aufgelöst wird. Erhöhe die Zuverlässigkeit durch Wrapper: ai.clients.Retry.new(inner = c, max_attempts = 3) und ai.clients.RoundRobin.new(members = […]) haben .new, aber ai.clients.Fallback { members: […] } hat keines – konstruiere es als Klassenliteral. Verwende dann client: Fast in der Funktion oder die Kurzform client: "openai/gpt-4o-mini". baml describe openai / baml describe ai.Credential / baml describe ai.clients.
  • Forme das Schema mit Feldattributen. @description("…") fügt einen ///-Hinweis hinzu, den das Modell in ${ctx.output_format()} sieht; @alias("name") benennt den ausgegebenen JSON-Schlüssel um. Verkettung: tags: string[] @alias("labels") @description("…").
  • Teste den reinen Code, nicht das Modell. Unit-Tests für Orchestrierung/Post-Processing auf Literaldaten mit assert.*. Das Aufrufen einer LLM-Funktion in einem test mit einem Live-Client führt zu einer echten Anfrage. Um Prompts, Requests und Parsing offline zu testen, verwende F@spec(args) oder einen skriptgesteuerten Client (siehe KI-Funktionen ausführen).
  • Baue Strings mit Interpolation, nicht mit Zwang. score=${n} konvertiert jeden Wert in einen String (implizites .to_string()); rufe .to_string() für den String allein auf. + benötigt beide Seiten bereits als Strings ("n=" + 5 wird nicht kompilieren).
  • catch für einige, catch_all für alle. expr catch (e) { baml.errors.ParseError => fallback } behandelt einen bestimmten Fehler; expr catch_all (e) { _ => fallback } ist erschöpfend – für einen Workflow-Top / Einstiegspunkt. Fehler propagieren implizit; Aufrufer müssen sie nicht neu deklarieren. Auslösen mit throw baml.errors.InvalidArgument { message: "…" } (Fehlertypen sind die eingebauten baml.errors.*-Klassen – InvalidArgument/ParseError/Io/Timeout/…; baml describe baml.errors); annotiere eine fehlerhafte Signatur mit -> T throws ErrType. Bevorzuge eine typisierte Ergebnis-Union (type R = Ok | Err) gegenüber dem Werfen für die gewöhnliche Steuerung.
  • Interfaces = gemeinsames Verhalten + dynamischer Dispatch. interface I { function m(self) -> T throws never }; jede Methode deklariert ihre throws-Klausel. Eine Methode mit einem Rumpf ist eine Standardimplementierung, die ein Implementierer überschreiben kann. Eine Klasse optiert via implements I { … } ein; ein Wert, der als I (oder I[]) typisiert ist, dispatches zur Laufzeit an den Implementierer. Interfaces können auch assoziierte Typen und generische Beschränkungen deklarieren. baml describe interfaces.
  • requires, keine Vererbung. BAML hat keine Vererbung: Eine Klasse kann keine andere Klasse erweitern. interface Pet requires Animal, Named { … } macht diese Interfaces zu Voraussetzungen von Pet. Die Standard-Rümpfe von Pet können ihre Methoden aufrufen, und ein Wert, der als Pet typisiert ist, macht sie verfügbar. Jeder Pet-Implementierer schreibt auch seine eigenen implements Animal- und implements Named-Blöcke (sonst E0125). Ein Diamant von requires kompiliert, und das gemeinsame Interface wird einmal implementiert; ein Zyklus ist ein Fehler. Wenn zwei Interfaces für eine Klasse denselben Methodennamen deklarieren, ist ein unqualifizierter Aufruf ein Fehler (E0121); rufe stattdessen obj.as<I>.m() auf.
  • Pattern Matching. match (v) { … } über Werte/Typen; Zweige sind pattern => expr – Literale, let x: T (binden + eingrenzen), Klassendestrukturierung T { f: let y }, Oder-Muster A | B, Wächter … if cond, _; muss erschöpfend sein. Auch v is T → bool (schränkt ein) und if let x: T = v { … } else { … }. baml describe patterns.
  • Eingrenzung – nur lokale Variablen. v is T, v != null / v == null und ein Wahrheitstest grenzen v nur ein, wenn v eine lokale Variable ist (ein let oder ein Parameter), die kein Closure erfasst: im bewachten if-/while-Zweig, nach einem frühen Ausstieg (if (!(v is T)) { return … }) und auf der rechten Seite von && (linke Seite wahr) und || (linke Seite falsch) – v is T && v.f == 1, s == null || s.length() == 0. NICHT eingegrenzt: eine erfasste lokale Variable, ein Feldpfad (h.v is T && h.v.f → E0007) und ein Indexpfad (xs[0] is T). Tasks können zwischen Anweisungen wechseln, sodass eine andere Task oder ein Alias ein Feld zwischen dem Test und dem Lesen ändern kann. Schreibe für einen Feldzugriff zuerst let v = h.v; oder if let v: T = h.v { … } oder match (h.v) { let v: T => … } (jede liest das Feld einmal). Eine Zuweisung an die lokale Variable ersetzt die Tatsache durch den zugewiesenen Typ. baml describe is.
  • Nebenläufigkeit = grüne Threads. spawn { … } gibt ein Future zurück; await sammelt es ein. Kombiniere viele mit baml.future.all / all_settled / race / any. all_settled gibt für jede Eingabe Success<T>, Failure<E> oder Panicked zurück; eine Eingabestornierung ist ein Panicked-Ergebnis. Konfiguriere einen Spawn mit einer with-Klausel: spawn with limit, token { … } – jeder aufgeführte Wert ist ein baml.spawn.Modifier, der den Plan, den der Spawn erstellt, von links nach rechts transformiert. baml.spawn.Limit.new(n) begrenzt die Nebenläufigkeit (überzählige Spawns werden in die Warteschlange gestellt), ein baml.spawn.CancelToken bricht kooperativ ab, baml.spawn.Root.new() trennt die Aufgabe von der Stornierung ihres Erzeugers. baml describe spawn / baml describe baml.future.
  • Ressourcensicherheit – defer, cleanup, catch (e, ctx). defer { … } führt einen Block beim Verlassen des Gültigkeitsbereichs aus, LIFO, auf jedem Pfad (return / throw / fall-through) – wie Go. Eine Klassenmethode namens function cleanup(self) -> void ist ein Finalizer: Sie wird höchstens einmal pro Instanz ausgeführt, egal ob du sie aufrufst, per defer planst oder der GC sie freigibt. catch (e, ctx) bindet einen ErrorContext zusammen mit dem Fehler – ein Fehler, der während der Behandlung eines anderen ausgelöst wird, kettet sich daran an, sodass ctx.root_cause() / ctx.cause zum ursprünglichen Fehler zurückgehen und ctx.to_string() die gesamte Kette darstellt (Python __context__-Stil). while let PATTERN = expr { … } schleift, bis das Muster fehlschlägt (z. B. Abarbeiten eines T?-zurückgebenden .pop()).
  • Rufe BAML aus Python/TS auf. Deklariere einen [generator.<name>] in baml.toml, führe baml generate aus, importiere dann das typisierte baml_sdk. Installation + Nutzung: baml describe python / baml describe typescript / baml describe baml_sdk.
  • Sicherer Zugriff über Indizierung. Subscript führt bei einem fehlenden Index/Schlüssel zu einem Fehler; verwende .at(i)/.get(k) (→ T?), greife mit ?. durch, verwende Standard mit ?? (Klammerung: (m.get(k) ?? 0) + 1).
  • Stdlib-Methoden sind snake_case und werden auf einem Wert aufgerufen. Einige geben ein neues Ergebnis zurück, andere mutieren direkt, wenige tun beides (sort_by_key sortiert den Empfänger und gibt ihn zurück) – um die Dokumentation zu lesen, baml describe <word/type/identifier/keyword/etc>.
  • Klassenfelder name: type,; Konstruktion Type { field: val }. Methoden nehmen ein einfaches self; Fabriken sind freie Funktionen. Felder sind veränderlich (wie TS): obj.field = v und obj.field += n funktionieren, und eine self-Methode kann direkt mutieren – eine Methode mit Seiteneffekt gibt void zurück. Klassen sind Referenztypen: find/at(i)/Subscript geben einen Live-Alias zurück, keine Kopie, sodass die Mutation des Ergebnisses dieses Element innerhalb des Arrays mutiert (xs.find(p)?.n += 1 aktualisiert xs), und eine an eine Funktion übergebene Klasse kann vom Aufgerufenen mutiert werden. Struct-Update-Spread wird unterstützt: User { ...u, tier: Tier.Free }. Leere Klassen sind legal (class Marker {}) – praktisch als Union-Varianten. Enums sind einfache Varianten – keine Methoden, keine assoziierten Daten (E.A.foo() wird nicht kompilieren); setze Verhalten in freie Funktionen, die match verwenden. enum E { A, B }, zugreifen mit E.A.
  • Blöcke sind Ausdrücke – der letzte Ausdruck ist der Wert (kein ;); return x; für frühen Ausstieg. Eine Funktion nur mit Seiteneffekten gibt void zurück – eine andere Schreibweise für null, den Unit-Typ – und kann ihren letzten Ausdruck weglassen oder return;. Ein Wert, der als letzter Ausdruck eines solchen Rumpfes übrig bleibt, ist ein Fehler, wird nicht verworfen: beende ihn mit ; (xs.push(x); – push gibt die neue Länge zurück). for (let x in xs) iteriert WERTE; while (cond) { … } schleift. Closures (x) -> { ... } leiten Parameter/Rückgabe aus dem Kontext ab (annotiere (x: T) -> R nur bei Mehrdeutigkeit; der -> ist erforderlich). .map/.filter geben Arrays direkt zurück (kein .collect()). Leere Map benötigt einen Typ: let m: map<string, int> = {};.
  • Kein ternärer Operator – if/else ist der Ausdruck. Es gibt kein cond ? a : b; if (cond) { a } else { b } ist ein Ausdruck, der einen Wert zurückgibt, also weise ihn direkt zu: let label = if (x > 3) { "big" } else { "small" };. Jeder Zweig ist ein Block, dessen letzter Ausdruck sein Wert ist (kein return). Verkette mit else if und kombiniere mit if let PATTERN = expr { … } else { … } zum Binden und Eingrenzen.
  • Bedingungen verwenden Wahrheitswerte. if, while, Match-Wächter, &&, || und ! akzeptieren jeden Wert. Falsy-Werte sind false, null, numerische Null, leere Strings, leere Arrays/Maps und leere Bytes; alles andere ist truthy. && und || geben immer noch bool zurück, keinen Operandenwert; sie kursschließen. Eine truthy optionale lokale Variable grenzt im genommenen Zweig und auf der rechten Seite von && ein (s && s.length() > 0).
  • Arrays haben einen JS-ähnlichen Methodensatz – map/filter/filter_map/reduce/find/some/every/flat_map/slice/concat/join/includes/length(), plus direkte push/pop/shift/unshift/sort_by/sort_by_key. Die meisten nehmen Closures, die throws können. baml describe Array gibt weitere Informationen.
  • Lokale let-Bindungen sind neu zuweisbar (x = x + 1) – kein mut-Schlüsselwort (es ist TS let, nicht Rust); es gibt auch kein const.
  • Argumente: Standardwerte mit =, Schlüsselwortaufrufe mit = (nie :). Deklariere einen Standardwert in der Signatur: function f(a: int, b: int = 10); rufe f(1) oder f(1, b = 2) auf. Ein Parameter mit Standardwert muss per Name übergeben werden – f(1, 2) ist ein Fehler („defaulted parameter 'b' must be passed by name“). Jeder Parameter (auch erforderliche) kann per Name übergeben werden (f(a = 1, b = 2)), und du kannst einen mittleren Standardwert überspringen, um einen späteren zu setzen (f(1, c = 9)). Die Schlüsselwortsyntax ist name = value; name: value wird nicht geparst (: ist für Typen/Felder). T? macht ein Argument NICHT optional – im Gegensatz zu TS b?: T ist ein b: T?-Parameter immer noch erforderlich (du musst null übergeben, sonst „expected N argument(s), got …“); füge = null hinzu, um es auslassbar zu machen. Eingebaute Funktionen folgen diesem Muster: baml.http.fetch(url, timeout = baml.time.Duration.from_seconds(10)).
  • Wo es sich von TS unterscheidet (die stillen Fallen): Arithmetik ist typgesteuert, nicht im TS-Stil. int / int ist abschneidende Ganzzahldivision (285 / 100 == 2, NICHT 2.85) und % ist der Rest (285 % 100 == 85); dies kompiliert einwandfrei und ergibt einfach eine stillschweigend falsche Zahl, daher ist es die wertvollste Falle. Mische einen Float hinein, um eine Gleitkommadivision zu erhalten (285 / 100.0 == 2.85, 285.0 / 100 == 2.85); jede gemischte int/float-Operation fördert zu float (5 + 2.0 == 7.0). Es gibt kein .to_float() – konvertiere einen int mit n * 1.0 (oder dividiere durch einen Float). Ein int-Ergebnis wird nicht automatisch in float umgewandelt (let x: float = 285 / 100 ist ein Kompilierungsfehler). + ist nur numerisch: String-Konkatenation erfordert, dass beide Seiten bereits string sind ("n=" + 5 wird nicht kompilieren – verwende stattdessen ${...}-Interpolation). Vergleiche (==, <, …, strukturelles ==) und &&/||/! sind wie in TS.
  • Tests: einzelnes test "name" { ... } (kein Wrapper); testset GRUPPIERT nur. Assertions (nur 6): assert.equal/approx_equal/is_true/is_type/not_null/contains. assert.equal vergleicht strukturell (tief, über Klassen/Arrays/Maps) – und das tut auch einfaches ==, das die Bool-Form ist. assert.equal ist exakt bei Floats; verwende assert.approx_equal(actual, expected, eps) für berechnete Werte. Letztes Assert: kein abschließendes ;. Kanonische IDs sind root::TestName für einen Test der obersten Ebene und root::Testset::TestName innerhalb eines Testsets. Führe einen mit baml test root::Testset::TestName aus.
  • Tabellen und Daten: baml describe data / baml describe tables.
  • bamlx verwaltet extrahierte Pakete. Stöpsle ein Paket ein: bamlx add <gh/{owner}/{repo}> (oder bamlx add git <url>). bamlx status listet installierte Pakete auf; bamlx update aktualisiert; bamlx remove <name> entfernt; bamlx tree zeigt die Abhängigkeitsstruktur. Deklariere ein Paket mit [dependency.<name>] in baml.toml (Quelle ist eines von registry, git, path). baml describe bamlx.

KI-Funktionen ausführen

Führe eine beliebige function aus, die einen { client: prompt: }-Block enthält:

# eval gibt den strukturierten Rückgabetyp aus (JSON-ähnlich)
baml run -e 'my_llm_func(args)'
# my_llm_func(args)  -- ruft das LLM auf und gibt die Antwort aus

⏳ Läuft langsam? Kein Problem. Konfiguriere einen Schnellspek-Client für die Offline-Iteration:

client NonLLM = ai.TestClient.new(wire = 🌟 json-string, delay = 0.05)   # <-- rufe `ai.TestClient.new` auf
client NonLLM = ai.TestClient.new(file = "specs/some_spec.json", delay = 0.05)  # `file` lädt aus einem Pfad

function MyFunc(x: int) -> MyResponse {
  client: NonLLM   # <-- verwende den Testclient
  prompt: `````   # <-- (Backtick-string) wird nicht ausgeführt, aber prompt und ausgabe werden protokolliert

}


**`ai.TestClient`** ist eine Klasse – `baml describe ai.TestClient`. Der Klassentyp hat Methoden `.new(wire = …)` und `.new(file = …)`. Er zeichnet den Prompt/die Anfrage/die Ausgabe auf und gibt `wire` oder `file` als Ausgabe zurück. *Hinweis zur Eingabe von Backtick-Strings in der Konsole*: `baml run` akzeptiert Prompts als String-Parameter (`client: "test"`) – für einen `string`-Prompt, um `baml run`-Eingaben zu umgehen. Prompt-Vorlagen können in Stdlib-Dokumenten übergeben werden.

## LLM-Ausgabe-Parsing

Ein LLM-Aufruf gibt einen Wert des Rückgabetyps zurück (kein roher Text). Die strukturierte Ausgabe wird aus dem Prompt unter Verwendung von `${ctx.output_format()}` geparst.

**Über Parsing-Fehler hinweghelfen:** Füge der Funktion einen `catch`-Block hinzu oder verwende `retry` auf einem Client: `client Fast = ai.clients.Retry.new(inner = BaseClient, max_attempts = 3)`. Verwende für `enum`-Felder einen `@alias`, um Alternativnamen zu unterstützen.

## Kontext und Tools

Für Tool-/Funktionsaufrufe initialisiere den Client mit dem Namen des Tools, das der LLM aufrufen kann. Definiere ein Tool als normale BAML-Funktion:

```baml
function get_weather(city: string) -> string {
  # kein `client`/`prompt` – es ist eine reine BAML-Funktion
  return if (

Verwandte Skills