Skip to main content
Für einen selbst geschriebenen Agenten oder ein Framework, für das Failproof AI keinen Adapter hat. Es ist nichts zu instrumentieren: Du sendest die Events selbst. Das ist dieselbe API, die die vier Framework-Adapter im Hintergrund nutzen. Sie sind lediglich Übersetzungsschichten darüber.

Installation

Keine Extras, keine Abhängigkeiten.

Instrumentierung

Von oben nach unten gelesen, sagt es genau das, was es bedeutet: Und was jeder Scope tatsächlich sendet: Alles darin kann session_id und agent_id weglassen. Die Scopes binden die Identität auf Kontextvariablen, und jeder Event-Aufruf liest sie zurück – du musst IDs nie durch deine Funktionen durchreichen. Alle drei funktionieren sowohl mit async with als auch mit with. Verschachtelte Agenten bilden den Baum. parent_id und Tiefe werden aus dem Stack berechnet:

Wie ein Scope schließt

agent() behandelt Exceptions für dich: Der Fehler wird vor agent_end gesendet, weil das Dashboard den Span bei agent_end schließt und alles danach keinem Span mehr zugeordnet wird. Eine Stornierung ist kein Fehler, daher verschmutzen abgebrochene Durchläufe die Fehlerübersicht nicht. Die Exception wird immer erneut ausgelöst: Ein Scope verschluckt sie nie.

Die Event-Methoden

Fünfzehn Methoden in sechs Familien. Die meisten kommen paarweise – du sendest den Öffner, dann den Schließer, und das SDK misst den Span dazwischen.
Bevorzuge die Scopes – agent() und tool_call() – wo immer sie passen. Sie garantieren das schließende Event, auch wenn der Body eine Exception wirft. Greife direkt auf diese Methoden zurück, wenn dein Kontrollfluss nicht verschachtelt ist, z. B. bei einem Modellaufruf innerhalb eines Hilfsfunktions.
Die beiden Human-Familien zeigen in entgegengesetzte Richtungen.Kein Framework signalisiert das zweite Paar – das musst du immer selbst senden.
Übergib request_id, wenn Modellaufrufe parallel laufen. Ohne sie werden Anfragen und Antworten in Eingangsreihenfolge pro Agent gepaart – und parallele Aufrufe werden falsch gepaart, sodass jede Antwort der falschen Anfrage zugeordnet wird.

Beispiel

Eine Tool-Calling-Schleife gegen die OpenAI API, ohne Agent-Framework:
Das erzeugt dieselben sechs Event-Typen, die ein Adapter liefern würde. Die vollständige ausführbare Version inklusive Tool-Definitionen liegt im SDK-Repository unter docs/manual/examples/.

Threads und Async

Kontextvariablen werden automatisch in asyncio-Tasks übertragen. In neue Threads werden sie nicht übertragen, da ein Thread mit einem leeren Kontext startet.
Ohne propagate() wirft das Worker-Event einen TypeError, der den Fix benennt, anstatt auf keiner Session zu landen. Das ist beabsichtigt: Ein Event ohne Session wird beim Ingest übersprungen und mit 200 beantwortet – das ist genau der stille Fehler, den die Identitätsschicht verhindern soll.

Ein Framework ohne Adapter instrumentieren

Jedes Agent-Framework gibt dir dieselben drei Nahtpunkte. Mappe sie und du hast eine vollständige Trace – die vier mitgelieferten Adapter tun nichts anderes.
1

Den Durchlauf einrahmen

2

Jedes Tool einrahmen

In was auch immer das Framework als Tool-Wrapper oder Middleware bezeichnet.
3

Jeden Modellaufruf paaren

Hast du eine Node-, Step- oder Middleware-Grenze, die es wert ist, sichtbar zu sein? Wickle sie in ein Hook-Paar – hook_triggered / hook_completed – und nicht in ein verschachteltes agent(). agent_id ist eine Facette mit niedriger Kardinalität, und ein Eintrag pro Node überfüllt sie. Hook-Spans werden genauso dargestellt und geben dir Latenz pro Node.
Manuell und automatisch komponieren. Ein Adapter, der innerhalb eines manuell erstellten Scopes läuft, schließt sich dieser Session an und wird dem Agenten als übergeordnet zugeordnet – du bekommst einen Baum statt zwei. Das ist nützlich, wenn du ein Framework selbst instrumentierst, das neben einem unterstützten läuft.
Zwei Gründe, und die drei Nahtpunkte oben sind die Antwort auf beide:
  • autogen-core wird seit September 2025 nicht mehr gepflegt.
  • AG2 bietet keinen prozessweiten Registrierungspunkt, der dem Hook-System der anderen Frameworks entspricht – die Instrumentierung erfordert daher das Einwickeln jedes Agenten an jeder Konstruktionsstelle.
Das manuelle Mappen der Nahtpunkte zeichnet dieselben Events mit derselben Genauigkeit auf wie ein mitgelieferter Adapter.

Tiefer eintauchen

Wie die Aufzeichnung tatsächlich funktioniert. Nichts davon ist nötig, um loszulegen.
Jede Aufzeichnung hat dieselbe Form: Ein Span öffnet sich, Arbeit wird darin verschachtelt, und jedes öffnende Event bekommt ein schließendes.Das Paar ist die Einheit. Jedes schließende Event trägt eine Dauer, die das SDK vom öffnenden Event an misst.Unten ist ein realer Durchlauf pro Framework – aufgenommen aus den Beispielen, die mit dem SDK mitgeliefert werden, Modellname normalisiert. Beachte, wie viel von einem einzigen Aufruf zurückkommt.
14 events
Nodes werden zu Hook-Paaren, sodass du Latenz pro Node erhältst, ohne die Agentenliste zu überfüllen.
Es gibt kein Session-End-Event. Eine Session ist nichts, das du schließt – sie ist eine Gruppe von Events, die eine session_id teilen.Der Status wird aus der Form der Trace abgeleitet:Eine Session endet also, wenn jedes Paar geschlossen ist. Die Adapter senden agent_end für dich, und beim Teardown schließen sie alles noch Offene und markieren es als unvollständig – ein abgestürzter Durchlauf wird als done mit einer sichtbaren Lücke abgeschlossen, statt hängen zu bleiben.
Deshalb kann eine Session zwei Aufrufe umspannen. Ein LangGraph interrupt() pausiert den Durchlauf, der Root-Span bleibt absichtlich offen, und der fortsetzende Aufruf schließt ihn. Beide Aufrufe gehören zu einer Session.
session_id und agent_id sind bei jeder Event-Methode optional. Werden sie weggelassen, werden sie aus dem umschließenden Scope aufgelöst:
Sie explizit zu übergeben funktioniert weiterhin und hat Vorrang. Wenn nichts gebunden und nichts übergeben wurde, wirft der Aufruf einen TypeError, der den Fix benennt, anstatt ein Event ohne Session zu senden, das beim Ingest übersprungen und mit 200 beantwortet werden würde.Scopes binden die Identität auf Kontextvariablen. Diese werden automatisch in asyncio-Tasks übertragen, aber nicht in neue Threads – wickle einen Worker in failproofai_sdk.propagate() ein.

Wer welche ID vergibt

Wie Adapter session_id auflösen

Der erste Treffer gewinnt:
  1. Eine explizite session_id-Option
  2. Metadaten pro Aufruf
  3. Der umschließende session()-Scope
  4. Framework-Metadaten
  5. Die eigene Run-ID des Frameworks
Sie wird nie erfunden, solange eines davon existiert – eine synthetisierte ID würde einen Durchlauf auf mehrere Sessions aufteilen.

agent_id niedrig halten

Sie ist die primäre Facette auf jeder Dashboard-Ansicht und eine LowCardinality(String)-Spalte. Ein Wert pro Durchlauf degradiert die Spalte und füllt das Filter-Dropdown mit einem Eintrag pro Durchlauf.Adapter schützen diese Spalte für dich:Die echte ID wird auf fw_agent_id / fw_run_id behalten, wo sie abfragbar bleibt, ohne eine Facette zu sein.
Diese Schutzmaßnahme betrifft nur Labels, die das Framework gewählt hat. Eine agent_id, die du selbst übergibst – an event.* oder an failproofai_sdk.agent(...) – wird genau so aufgezeichnet. Ein explizites Argument stillschweigend umzuschreiben wäre schlimmer als die Kardinalität, die es verhindert – benenne deine eigenen Spans entsprechend.
Welches Framework was aufzeichnet, gemessen aus den obigen Durchläufen:Ein Strich bedeutet, das Framework kennt dieses Konzept nicht. human_pause und human_interrupt beschreiben eine Person, die auf den Agenten einwirkt – das signalisiert kein Framework. Diese musst du selbst senden.
Ein Event kommt nie allein. Eines öffnet einen Span, eines schließt ihn, und das schließende Event trägt eine Dauer, die das SDK vom öffnenden Event an misst.
Ein öffnendes Event ohne schließendes ist ein Span, der nie endet. Die Session wird als noch laufend angezeigt, für immer, und ihre aktive Dauer wächst weiter. Das ist der Fehlerfall, auf den du achten musst, wenn du manuell instrumentierst.

Korrelationsregeln

  • Verwende dieselbe tool_call_id, hook_id, pause_id oder input_id für das passende Abschlussevent.
  • Das SDK berechnet duration_ms für tool_result, hook_completed, agent_resume und human_input. Es an diese Methoden zu übergeben wirft einen ValueError.
  • duration_ms wird bei model_response akzeptiert, weil nur der Aufrufer die echte Provider-Latenz kennt. Es muss ein Integer sein – ein Float wirft am Aufrufpunkt einen ValueError, da der Server die Spalte als vorzeichenlosen 32-Bit-Integer liest und sonst NULL speichern würde.
  • Korrelationsschlüssel sind nach Art und Session begrenzt, sodass ein Tool-Aufruf und ein Hook sicher dieselbe ID teilen dürfen, und zwei parallele Sessions dieselben IDs wiederverwenden können, ohne zu kollidieren. Sie sind nicht nach Agent begrenzt: Ein Paar, das unter einem Agenten geöffnet und unter einem anderen geschlossen wird, korreliert trotzdem – das ist der Normalfall in Multi-Agent-Frameworks.
  • request_id paart model_request mit model_response. Ohne sie werden Model-Events in Reihenfolge pro Agent gepaart, sodass parallele Aufrufe falsch gepaart werden.
  • Ein über Prozesse hinweg aufgeteiltes Paar korreliert noch auf der Serverseite, aber das SDK kann seine In-Process-Dauer nicht berechnen.
  • Die Pending-Map hält maximal 10.000 Starts und entfernt den ältesten Eintrag, wenn sie voll ist.
Die Installation von failproofai-sdk installiert alles, alle vier Adapter inklusive. Die Extras ziehen das Framework nach, nicht den Adapter.
import failproofai_sdk ist vertraglich abhängigkeitsfrei, erzwungen durch einen Test, der das gebaute Wheel mit --no-deps installiert, und einen weiteren, der beweist, dass kein Framework sys.modules erreicht.
Es gibt kein failproofai_sdk.crewai-Attribut. Adapter werden absichtlich nicht am Top-Level-Paket exponiert: Darauf zuzugreifen würde das Framework als Nebeneffekt des Attributzugriffs importieren und das Null-Abhängigkeits-Versprechen brechen. Verwende instrument().
Die Auto-Erkennung liest sys.modules, nicht die Liste installierter Pakete – ein installiertes, aber nie importiertes Framework wird nicht instrumentiert und nie in deinem Namen importiert. Um zu sehen, was verdrahtet ist:
instrument("crewai") auf einer Maschine ohne CrewAI wirft keine Exception. Es loggt eine Warnung und gibt () zurück, sodass ein fehlendes Framework nie einen Prozess zum Absturz bringt, der auch andere instrumentiert.Die Warnung enthält den zugrunde liegenden ImportError, und diese Meldung nennt den genauen Installationsbefehl – die Lösung steht also in deinen Logs, nicht versteckt.
Setze FAILPROOFAI_SDK_STRICT=1, damit stattdessen eine Exception ausgelöst wird. Dieses Flag wird einmal gelesen und gecacht, also exportiere es vor dem Start deines Prozesses, statt es mittendrin zu setzen.
instrument() muss nach deinem Framework-Import kommen. Die Auto-Erkennung liest sys.modules, also findet ein bloßer Aufruf vor dem Import nichts, installiert nichts und gibt () zurück.
Machst du das falsch, läuft der Prozess mit importiertem SDK, scheinbar installiertem Adapter und keinem einzigen gesendeten Event. Es loggt eine Warnung, die genau das sagt – also prüfe zuerst deine Logs, wenn ein Durchlauf nichts aufzeichnet.
Der Spool macht das Ganze sicher: Dein Agent blockiert nie auf das Netzwerk, und ein Cloud-Ausfall bedeutet ein wachsendes Verzeichnis statt verlorener Events.Jeder Flush schreibt eine Batch-Datei: zuerst .tmp, dann fsync, dann ein atomares Umbenennen:
Der Daemon liest nur .jsonl, kann also nie eine halb geschriebene Datei lesen. Der Dateiname trägt Timestamp, Prozess-ID und Sequenznummer, sodass zwei Prozesse, die in derselben Millisekunde flushen, nicht kollidieren können. Die Queue ist auf 10.000 Events begrenzt; darüber hinaus werden die ältesten gelöscht und geloggt.
collector.redact gilt nicht für deine SDK-Events. Es sieht sie nie.
Der Daemon versendet deine Batches. Er öffnet oder überschreibt sie nicht.Bereinigung läuft dort, wo der Daemon seine eigenen Events schreibt – nicht wo Batches versendet werden. Ein Prompt oder ein Tool-Argument mit einem API-Key enthält ihn also noch beim Ankommen.Das ist beabsichtigt. Das sind deine eigenen Instrumentierungsaufrufe, und sie im Transit umzuschreiben würde bedeuten, dass die Events, die du empfängst, nicht die Events sind, die du gesendet hast.
Du kontrollierst Payloads an der Quelle, an zwei Stellen:
  • Schalte Content-Capture am Adapter aus. Der Optionsname unterscheidet sich, und ein Adapter hat keinen – das ist kein universeller Schalter:
    • LangChain / LangGraph, Pydantic AI — capture_content=False
    • LlamaIndex — capture_messages=False
    • CrewAI — kein Content-Schalter; session_id ist die einzige Option, die es liest – Prompts und Completions werden also immer aufgezeichnet.
    instrument() ignoriert Optionen, die ein Adapter nicht liest, sodass das Übergeben des falschen Namens nichts auslöst und nichts ändert.
  • Gib das Geheimnis von vornherein nicht an input= weiter.
collector.redact ist kein Ersatz für beides.
Ein leeres Spool-Verzeichnis ist der gesunde Zustand. Verwende es nicht zur Lieferungsüberprüfung.
Der Daemon löscht jeden Batch innerhalb von Millisekunden nach dem Versenden, sodass ein ls mit dem Collector um die Wette läuft und nur einen Bruchteil der gesendeten Events zeigt – nicht zu unterscheiden von einem SDK, das nichts aufgezeichnet hat.Um zu bestätigen, dass Events tatsächlich angekommen sind, prüfe das Dashboard. Um den Spool beim Füllen zu beobachten, stoppe den Daemon zuerst.
Jeder Callback läuft innerhalb eines Wrappers, dessen einzige Aufgabe es ist, weiterzuwerfen – dein Aufruf sitzt also in genau einem try, und alles, was das SDK tut, geschieht außerhalb davon.Der Standard ist in Produktion richtig und beim Debuggen falsch, weil er nur beweisen kann „es ist nicht abgestürzt”. Setze FAILPROOFAI_SDK_STRICT=1, um einen verschluckten Fehler laut zu machen.

Häufige Probleme

Ein öffnendes Event hat kein schließendes: ein model_request ohne model_response oder ein tool_use ohne tool_result. Verwende die Scopes, die das Paar auch dann garantieren, wenn der Body eine Exception wirft. Wenn du die Event-Methoden direkt aufrufst, verwende try und finally.
Es wird vom passenden öffnenden Event an gemessen und wird daher bei tool_result, hook_completed, agent_resume und human_input abgelehnt. Bei model_response wird es akzeptiert, weil nur du die echte Provider-Latenz kennst, und es muss ein Integer sein.
Der Thread hat den Kontext nie geerbt. Wickle das Callable in failproofai_sdk.propagate() ein. Siehe Threads und Async.
Zusätzliche Felder werden zuletzt zusammengeführt, sodass eines mit dem Namen eines echten Feldes wie model oder outcome dieses überschreiben und eine gespeicherte Spalte verändern würde. Verwende einen Namespace; die Adapter nutzen das Präfix fw_.
agent_id ist eine Facette mit niedriger Kardinalität, und du hast eine Run-ID hineingespeichert. Verwende eine Rolle oder einen Node-Namen und leg die echte ID in ein Payload-Feld.

Weiter

So funktioniert es

Paare, IDs, Session-Lebenszyklus und Zustellung.

Eine Trace lesen

Folge der Kausalität durch die soeben aufgezeichnete Session.

Framework-Adapter

LangGraph, CrewAI, LlamaIndex und Pydantic AI.