Genehmigungsmechanismen in LangGraph.js: Das Anhalten von Agenten mit interrupt() und Befehlen
Erstellen Sie ein minimales Approval-Gate für LangGraph.js, das vor einem Nebeneffekt pausiert, eine menschliche Entscheidung im Terminal einholt und sicher von einem Checkpoint fortsetzt.
Einige Agentenaktionen sind zu bedeutend, um unbeaufsichtigt ausgeführt zu werden: Das Versenden einer E-Mail im Namen eines anderen, das Löschen eines Datensatzes oder die Freigabe einer Zahlung. In solchen Fällen soll der Agent den Vorschlag unterbreiten, anhalten und auf eine Zustimmung oder Ablehnung warten. In dieser Anleitung wird dieses Verhalten in LangGraph.js mit dem kleinstmöglichen Graphen implementiert, damit Sie genau sehen können, wie interrupt(), ein Checkpointer, ein thread_id sowie Command({ resume }) zusammenwirken, um eine Ausführung zu pausieren und später wieder aufzunehmen.
Beabsichtigt wird hier keine Mehr-Agenten-Orchestrierung und auch kein Aufruf eines LLMs. Das gesamte Muster lässt sich in einem Satz beschreiben: Pause, Entscheidung durch einen Menschen, Fortsetzung.
Wann ein Agent nicht das letzte Wort haben sollte
Autonomie ist wertvoll, wenn man bereit ist, dem Agenten zu erlauben, nach eigenem Ermessen zu handeln. Viele Aktionen erfüllen diese Voraussetzung nicht, und ein menschlicher Überprüfungs Schritt lohnt sich trotz der damit verbundenen Komplexität. Typische Beispiele sind:
- Das Versenden einer E-Mail oder eines Chat-Nachrichts für einen Benutzer
- Die Änderung oder Löschung eines Datenbank-Eintrags
- Die Freigabe einer Zahlung
- Das Bereitstellen von Code
- Die Deaktivierung von Cloud-Ressourcen
- Die Weiterleitung eines Support-Tickets
- Die Veröffentlichung von durch KI erzeugtem Inhalt
In jedem Fall ist das Ziel dasselbe. Der Agent übernimmt weiterhin die Entscheidungsfindung und bereitet die Aktion vor, legt jedoch seine Absicht dar und überlässt die endgültige Entscheidung einem Menschen, bevor etwas Unumkehrbares geschieht. Diese Anordnung ist im Praxistest das, was Human-in-the-Loop (HITL) bedeutet.
Wie interrupt() und Command zusammenpassen
Im Kern funktioniert HITL in LangGraph folgendermaßen: Der Graph stoppt seine Ausführung, wartet auf Eingaben von außen und setzt diese anschließend zur Weiterarbeit ein.
Die Unterbrechung erfolgt mithilfe von interrupt(). Wenn ein Knoten diese Funktion aufruft, stoppt LangGraph die aktuelle Ausführung und speichert den Zustand des Graphen über den konfigurierten Checkpointer, sodass dieselbe Ausführung später fortgesetzt werden kann. Ihre Anwendung erhält den Wert, den Sie an interrupt() übergeben haben, zeigt ihn einem Menschen an, sammelt eine Antwort ein und setzt den Graphen anschließend mit einem Command-Objekt fort, das die Antwort enthält.
Die Gesamtabfolge sieht wie folgt aus:
Graph starts
↓
Agent decides to send email
↓
⏸ interrupt()
↓
Human reviews the action
↓
Approve / Reject
↓
Command({ resume: ... })
↓
Graph continues
Behalten Sie diese Struktur im Hinterkopf; jeder Codeabschnitt darunter entspricht einem Pfeil in dieser Struktur.
Das Szenario: Ein E-Mail, der Zustimmung benötigt
In dem Beispiel wird eine E-Mail verwendet. Der Agent entscheidet, dass er diese Nachricht senden möchte:
Meeting at 5 PM with Aman
Bevor diese Nachricht irgendwohin gesendet wird, sollte jemand sie überprüfen und entweder genehmigen oder ablehnen. Die Struktur der Entscheidungswege sieht wie folgt aus:
User
↓
Agent decides to send email
↓
⏸ Human approval
↓
┌───────────────┐
│ Approve │ → Send email
│ Reject │ → Stop
└───────────────┘
Um den Fokus auf die Mechanismen zum Anhalten und Wiederaufnehmen zu halten, wird kein echter E-Mail-Anbieter verwendet. Der Knoten, der die E-Mail „sendet“, gibt sie lediglich in die Konsole aus. Ein späterer Austausch gegen eine echte API ändert nichts am Ablauf der Steuerung.
Schritt für Schritt das Diagramm erstellen
LangGraph installieren und die Einbindungen vorbereiten
Fangen Sie mit einem leeren Node.js-Projekt an und fügen Sie LangGraph hinzu:
npm install @langchain/langgraph
Je nach installierter Version kann LangGraph.js auch @langchain/core als Peer-Abhängigkeit erfordern; falls npm darauf hinweist oder die Importe fehlschlagen, fügen Sie auch dieses Paket hinzu und prüfen Sie die aktuellen Installationsanleitungen.
Die Eingabe durch den Benutzer erfolgt über die Terminal-Schnittstelle mittels von Node integriertem readline/promises-Modul, weshalb kein zusätzliches Paket erforderlich ist. Die Importe beinhalten den Graphen-Baukasten, den Hilfsmechanismus für Zustandsannotationen, die START- und END-Sentinels, interrupt, den im Speicher gespeicherten Checkpointer sowie Command, zusätzlich die Komponenten von readline:
import {
StateGraph,
Annotation,
START,
END,
interrupt,
MemorySaver,
Command,
} from "@langchain/langgraph";
import readline from "node:readline/promises";
import {
stdin as input,
stdout as output,
} from "node:process";
Die Datei verwendet ES-Modul-Syntax (import), daher muss sie entweder mit der Erweiterung .mjs benannt werden oder in package.json muss "type": "module" festgelegt werden. Zudem ist eine Node-Version erforderlich, die await auf Modulebene unterstützt, da der auszuführende Code später auf dieser Ebene auf await wartet.
Definieren Sie den Zustand, den der Graph enthält
Der Zustand in LangGraph ist ein gemeinsames Objekt, das durch den Graphen fließt. Jeder Knoten liest daraus und gibt teilweise aktualisierte Werte zurück. Dieser Graph benötigt lediglich zwei Felder:
message, der E-Mail-Text, den der Agent vorschlägtdecision, die Antwort, die der Mensch gibt
const StateAnnotation = Annotation.Root({
message: Annotation,
decision: Annotation,
});
Annotation.Root() deklariert die Struktur des Zustands. Da kein Reducer angegeben ist, nimmt jedes Feld einfach den neuesten darauf geschriebenen Wert an. Der Agent-Knoten füllt message aus; der Freigabeknoten füllt decision aus, sobald der Mensch geantwortet hat.
Schreiben Sie die Aktion, die Sie schützen möchten
Danach kommt der Knoten, der die risikoreiche Operation darstellt. In der Produktion würde dieser eine E-Mail-API aufrufen. Hier wird lediglich protokolliert:
function sendEmail(state) {
console.log(`\n📧 Email sent: "${state.message}"`);
return {};
}
Was diese Funktion tut, ist fast unwichtig. Entscheidend ist wann sie ausgeführt wird: niemals bevor ein Mensch zugestimmt hat. Der Rest des Graphen dient dazu, diese Reihenfolge durchzusetzen. Beachten Sie, dass sie ein leeres Objekt zurückgibt, was bedeutet, dass der Zustand unverändert bleibt.
Stellvertreter für die Entscheidung des Agenten
In einem echten System wäre dies der Punkt, an dem ein LLM die Anfrage des Benutzers liest und entscheidet, dass eine E-Mail erforderlich ist – vermutlich durch Aufruf von Tools. Das Hinzufügen eines Modells würde nur von den MECHANISMEN des HITL ablenken, daher übernimmt eine einfache Funktion die Rolle des Agenten und gibt die Nachricht zurück, die sie „ausgewählt“ hat:
function agent() {
return {
message: "Meeting at 5 PM with Aman",
};
}
Lesen Sie diesen Knoten als Ankündigung der Absicht des Agenten: dies ist die E-Mail, die er senden möchte. Wenn Sie ihn später durch ein LLM sowie Logik zum Aufruf von Tools ersetzen, bleibt das zugehörige Genehmigungsverfahren im Wesentlichen unverändert.
Pause für einen Menschen mit interrupt()
Dies ist das Herzstück des Musters. Der Genehmigungsnode ruft interrupt() mit einem Payload auf, der beschreibt, was eine Entscheidung erfordert, und gibt das zurück, was als neue decision kommt:
function humanApproval(state) {
const decision = interrupt({
message: state.message,
question: "Do you want to send this email?",
});
return {
decision,
};
}
Sobald die Ausführung auf den Aufruf stößt
interrupt(...)
stoppt das Diagramm. Das übergebene Objekt wird zum Interrupt-Payload, den die aufrufende Anwendung lesen kann. In diesem Fall ist es:
{
message: "Meeting at 5 PM with Aman",
question: "Do you want to send this email?"
}
Die Anwendung zeigt diesen Payload einer Person an, wartet auf eine Antwort und setzt das Diagramm fort. Der entscheidende Punkt ist, dass der Wert, den Sie bei der Fortsetzung angeben, zum Rückgabewert von interrupt() wird. Somit verwandelt sich die Zeile
const decision = interrupt(...);
im Grunde in Folgendes, sobald die Person zustimmt:
const decision = "approve";
Der Wert wird als decision in den Zustand geschrieben. Eine Routing-Funktion wählt anschließend auf dieser Grundlage den nächsten Schritt aus:
function routeAfterApproval(state) {
if (state.decision === "approve") {
return "sendEmail";
}
return END;
}
Eine "approve"-Antwort führt zu sendEmail; alles andere beendet den Ablauf. Jede Nicht-Zustimmung als Stopp zu behandeln, ist ein sinnvoller Standardwert für eine Sicherheitssperre: Falls jemals ein unerwarteter Wert eingeht, versagt das System abgeschlossen, anstatt die Aktion auszuführen.
Einen Checkpointer hinzufügen und den Graph verbinden
Vor der Kompilierung ist noch ein weiteres Element erforderlich: ein Checkpointer. Da der Ablauf stoppen und später fortgesetzt werden soll, muss LangGraph den Ausführungszustand zum Zeitpunkt der Unterbrechung speichern. Ohne Checkpointer gibt es nichts, von dem aus weitergefahren werden könnte. Für eine Demo reicht die Implementierung im Arbeitsspeicher aus:
const checkpointer = new MemorySaver();
Registrieren Sie nun die drei Knoten, verbinden Sie START mit dem Agenten und den Agenten mit dem Freigabeschritt, fügen Sie eine bedingte Kante vom Freigabeschritt hinzu, die durch routeAfterApproval gesteuert wird, und kompilieren Sie mit dem Checkpointer:
const graph = new StateGraph(StateAnnotation)
.addNode("agent", agent)
.addNode("humanApproval", humanApproval)
.addNode("sendEmail", sendEmail)
.addEdge(START, "agent")
.addEdge("agent", "humanApproval")
.addConditionalEdges(
"humanApproval",
routeAfterApproval,
{
sendEmail: "sendEmail",
[END]: END,
}
)
.compile({
checkpointer,
});
Der dritte Argument von addConditionalEdges ordnet jedem Wert zu, den der Router zurückgeben kann, einen Zielknoten zu, was es LangGraph außerdem ermöglicht, das Diagramm korrekt darzustellen. Die resultierende Topologie:
START
↓
agent
↓
humanApproval
↓
┌──────────────┐
│ │
approve reject
│ │
↓ ↓
sendEmail END
│
↓
END
MemorySaver speichert Checkpoints im Prozessgedächtnis, was ideal für Experimente ist, aber nach Beendigung des Prozesses nutzlos wird. Für echte Implementierungen sollte ein persistenter Checkpointer unter Verwendung einer Datenbank verwendet werden, damit eine pausierte Ausführung auch nach Neustarts erhalten bleibt und von einem anderen Prozess oder Server wieder aufgenommen werden kann – was in der Regel der Fall ist, wenn eine Genehmigung Stunden später über eine Web-Oberfläche eingehend wird. Um genauer zu erfahren, wie Checkpoints intern gespeichert werden, lesen Sie wie LangGraphs in-Memory-Speicherer Checkpoints organisiert und schreibt.
Der andere Aspekt der Persistenz ist thread_id. Er identifiziert, um welche geparkte Ausführung es sich handelt. Beim Pausieren und Wiederaufnehmen muss derselbe thread_id verwendet werden; andernfalls hat LangGraph keine Möglichkeit, die gespeicherte Ausführung zu finden.
Ausführung des Flusses aus der Terminalkonsole
Starten der Ausführung und Erkennen von Unterbrechungen
Eine readline-Schnittstelle verwandelt die Terminalkonsole in den menschlichen Prüfer:
const rl = readline.createInterface({
input,
output,
});
Das Konfigurationsobjekt enthält den thread_id unter dem Schlüssel configurable. Alles, was mit dieser Ausführung zu tun hat – sowohl der ursprüngliche Aufruf als auch die Wiederaufnahme – muss dieses gleiche Objekt (oder zumindest dieselbe ID) übergeben:
const config = {
configurable: {
thread_id: "thread-1",
},
};
Starten Sie das Diagramm mit einem Anfangszustand. Der Agent-Node überschreibt den leeren message-Wert:
const stream = await graph.stream(
{
message: "",
},
config
);
Die Ausführung fließt über agent zu humanApproval, wo interrupt() sie stoppt. Anschließend liefert der Stream einen Datensatz, der eine __interrupt__-Schlüssel enthält. Der value-Wert des ersten Eintrags ist der Payload, der an interrupt() übergeben wurde; dieser wird vom Loop für den Prüfer ausgegeben:
for await (const chunk of stream) {
if (chunk.__interrupt__) {
const interruptValue =
chunk.__interrupt__[0].value;
console.log(
"\n⏸ Waiting for human approval...\n"
);
console.log(
"The agent wants to send this email:"
);
console.log(`"${interruptValue.message}"`);
console.log(
`\n${interruptValue.question}`
);
}
}
Die Ausgabe im Terminal sieht wie folgt aus. Die erste Zeile repräsentiert die ursprüngliche Anfrage des Benutzers im Kontext; der oben gezeigte Code gibt sie nicht aus:
User: Send an email to Aman about the 5 PM meeting
⏸ Waiting for human approval...
The agent wants to send this email:
"Meeting at 5 PM with Aman"
Do you want to send this email?
Derzeit ist das Diagramm pausiert und es wurde keine E-Mail gesendet. Der Ablauf befindet sich im Checkpointer und wartet.
Eine Entscheidung einholen und validieren
Fragen Sie nun den Prüfer. Die Schleife fragt immer wieder nach, bis sie eine der beiden akzeptierten Antworten erhält, wobei zuerst Leerzeichen und Groß-/Kleinschreibung normalisiert werden:
let humanAnswer;
while (true) {
humanAnswer = (
await rl.question("\nApprove or reject: ")
)
.trim()
.toLowerCase();
if (
humanAnswer === "approve" ||
humanAnswer === "reject"
) {
break;
}
console.log(
'Please type "approve" or "reject".'
);
}
Im Terminal wird Genehmigen oder ablehnen: angezeigt und es kommt zu einer Blockade. Das Eingeben von approve bricht die Schleife, sodass das Diagramm fortgesetzt werden kann. Die Validierung der Eingabe vor Wiederaufnahme lohnt sich aufgrund der wenigen zusätzlichen Zeilen: Der zurückgegebene Wert ist genau derselbe, den Ihre Routing-Logik sehen wird.
Mit Befehl fortsetzen
Fortsetzen bedeutet, das Graph erneut aufzurufen, aber anstelle neuer Eingabedaten wird ein Command übergeben, dessen resume-Feld die Antwort des Benutzers enthält:
await graph.invoke(
new Command({
resume: humanAnswer,
}),
config
);
Dieselbe config wird wiederverwendet, was bedeutet, dass derselbe thread_id verwendet wird, und so findet LangGraph die unterbrochene Ausführung. Der resume-Wert wird als Rückgabewert von interrupt() übergeben. Wenn „approve“ eingegeben wird, liefert der Aufruf innerhalb von humanApproval
const decision = interrupt(...);
jetzt „approve“ zurück. Der Knoten gibt dies als decision aus, und der Router führt aus:
function routeAfterApproval(state) {
if (state.decision === "approve") {
return "sendEmail";
}
return END;
}
Weil decision gleich „approve“ ist, wechselt der Kontrollfluss zu sendEmail, und die Konsole gibt aus:
📧 Email sent: "Meeting at 5 PM with Aman"
Die E-Mail wurde nur nach ausdrücklicher Genehmigung „versandt“. Wenn Sie fertig sind, rufen Sie rl.close() auf, damit die readline-Schnittstelle stdin freigibt und der Prozess beendet werden kann.
Wie eine Ablehnung aussieht
Führen Sie das Skript erneut aus. Da MemorySaver im Speicher vorhanden ist, startet ein neuer Prozess mit einem leeren Checkpoint-Speicher; wenn Sie es innerhalb desselben Prozesses erneut ausführen, verwenden Sie eine neue thread_id, damit nicht ein bereits abgeschlossener Lauf fortgesetzt wird. Wenn die Eingabemaske erscheint,
Approve or reject:
Antwort:
reject
Der Graph wird mit diesem Wert fortgesetzt. Der folgende Auszug gibt den genauen Wert zur Klarheit an; im Skript ist es einfach humanAnswer:
await graph.invoke(
new Command({
resume: "reject",
}),
config
);
Dieses Mal enthält state.decision den Wert "reject", wodurch der Router END zurückgibt und sendEmail niemals ausgelöst wird:
Agent wants to send email
↓
⏸ Paused
↓
Human: reject
↓
END
Dieser Unterschied ist wichtig. Das Diagramm erzeugt bei Ablehnung nicht nur eine andere Nachricht; der Knoten, der die Nebeneffekte ausführt, wird überhaupt nicht ausgeführt. Genau das macht den Genehmigungsprozess zu einem echten Schutzmechanismus und nicht nur zu einer kosmetischen Maßnahme.
Das Gesamtbild
Wenn man alles zusammenfasst, sieht das vollständige Diagramm so aus:
┌─────────────┐
│ START │
└──────┬──────┘
↓
┌─────────────┐
│ Agent │
└──────┬──────┘
↓
┌───────────────────┐
│ Human Approval │
│ │
│ ⏸ interrupt() │
└─────────┬─────────┘
↓
Human decides
/ \
/ \
approve reject
↓ ↓
┌────────────┐ END
│ sendEmail │
└──────┬─────┘
↓
END
In einem Satz: Der Agent trifft eine Entscheidung, das Diagramm pausiert, ein Mensch prüft die Situation, das Diagramm setzt seine Ausführung fort – erst danach wird die Aktion ausgeführt.
Das Problem bei der erneuten Ausführung: Nebeneffekte nach einer Unterbrechung beibehalten
Ein Verhalten von interrupt() verwirrt viele Menschen. Kurz gesagt setzt LangGraph seine Ausführung nicht von der Zeile nach interrupt() fort. Stattdessen wird der Knoten, der die Unterbrechung enthält, von seiner ersten Zeile aus erneut ausgeführt. Der Unterschied bei der zweiten Ausführung besteht darin, dass der Aufruf von interrupt() den Fortsetzungswert sofort zurückgibt anstelle einer Pause.
Das hat direkte Auswirkungen auf Nebeneffekte. Alles, was vor interrupt() im selben Knoten steht, wird einmal ausgeführt, wenn der Graph pausiert, und erneut, wenn er fortgesetzt wird. Dies ist das Muster, das man vermeiden sollte:
function humanApproval(state) {
saveSomethingToDatabase();
const decision = interrupt("Approve?");
return { decision };
}
Hier würde saveSomethingToDatabase() für eine einzige Genehmigung zweimal ausgeführt werden. Die Lösung ist strukturell: Der Genehmigungs-Knoten sollte frei von Nebeneffekten sein, und jede tatsächliche Aktion sollte in einem späteren Knoten platziert werden, der erst ausgeführt wird, nachdem die Person geantwortet hat. So ist das Beispiel aufgebaut:
humanApproval
↓
interrupt()
↓
human response
↓
sendEmail
Falls Sie tatsächlich vor einer Unterbrechung im selben Knoten Arbeit ausführen müssen, machen Sie diese idempotent (sie kann sicher wiederholt werden, beispielsweise ein Upsert mit einer stabilen ID als Schlüssel) oder verschieben Sie sie in einen eigenen vorangehenden Knoten, dessen abgeschlossenes Ergebnis bereits gespeichert wurde und nicht erneut ausgeführt wird.
Was läuft im Hintergrund, Schritt für Schritt
Sobald der Code abgeschlossen ist, ist der Lebenszyklus kurz:
- Der Graph beginnt mit der Ausführung.
- Der Agentknoten entscheidet, dass er eine E-Mail senden möchte.
- Die Ausführung erreicht
interrupt(). - LangGraph stoppt den Lauf.
- Der aktuelle Zustand wird vom Checkpointer gespeichert.
- Die Anwendung erhält die Unterbrechungspayload.
- Jemand prüft die vorgeschlagene Aktion.
- Diese Person trifft eine Entscheidung.
- Die Anwendung setzt den Graphen mit
Commandunter Verwendung derselbenthread_idfort.
interrupt() gibt die Antwort der Person zurück.Die API-Aufrufe sind der einfachere Teil; das Muster von Pause und Wiederaufnahme ist die Idee, die man sich einprägen sollte:
Graph
│
▼
Agent decision
│
▼
interrupt()
│
│
┌───┴───┐
│ Human │
└───┬───┘
│
approve/reject
│
▼
resume
│
▼
Continue
Sobald dieser Ablauf klar ist, wirkt HITL nicht mehr geheimnisvoll. Es handelt sich dabei einfach um eine gepausierte Ausführung mit einer eingegebenen Antwort am Ende.
Überprüfung der eigenen Implementierung
Bevor man sich auf ein Genehmigungsverfahren verlässt, sollten einige schnelle Tests durchgeführt werden:
- Genehmigen Sie einmal und stellen Sie sicher, dass die Aktion genau einmal ausgeführt wird.
- Lehnen Sie ab und stellen Sie sicher, dass der Aktionspunkt niemals ausgeführt wird – nicht nur, dass die Ausgabe anders ist.
- Geben Sie eine ungültige Antwort ein und stellen Sie sicher, dass die Anfrage wiederholt wird anstatt mit Unsinn fortgesetzt zu werden.
- Fahren Sie mit einer anderen
thread_idfort und beobachten Sie, dass die ursprüngliche Ausführung nicht beeinflusst wird.
Wo dieselbe Schleuse Anwendung findet
E-Mail ist lediglich eine praktische Demonstration. Die identische Struktur eignet sich für jede Aktion, die menschliche Überwachung erfordert: Nachrichtenversand, Aktualisierungen oder Löschungen von Aufzeichnungen, Zahlungsfreigaben, Bereitstellungen, Aufräumen von Cloud-Ressourcen, Veröffentlichung generierter Inhalte oder Eskalation von Supportanfragen. Der Aktionspunkt ändert sich; die Schleuse davor nicht. Wenn Sie Freigabeschritte zusammen mit anderen Orchestrierungsmustern wie Routing und Fan-Out sehen möchten, zeigt diese Übersicht über fünf LangGraph-Muster sie nebeneinander.
Kernpunkte
interrupt()pausiert das Diagramm und übermittelt eine Nutzlast an Ihre Anwendung; der Wert, mit dem Sie fortfahren, wird zu dessen Rückgabewert.Command({ resume: ... })gibt die Antwort des Benutzers wieder in den pausierten Ablauf zurück.- Ein Checkpointer ist für das Pausieren erforderlich; verwenden Sie in der Produktion einen persistenten, damit Genehmigungen auch nach Neustarts eintreffen können.
- Derselbe
thread_idmuss verwendet werden, um einen bestimmten Ablauf zu pausieren und fortzusetzen. - Der Knoten, der
interrupt()enthält, wird beim Fortsetzen von oben neu ausgeführt, daher sollten Nebeneffekte in späteren Knoten platziert oder idempotent gemacht werden. - Leiten Sie alles außer einer ausdrücklichen Genehmigung zur Beendigung um, damit das Gate geschlossen bleibt.
Man benötigt keinen umfangreichen Workflow, um einen Menschen zur Kontrolle eines Agenten zu bringen. Eine gut platzierte Pause vor dem entscheidenden Schritt ermöglicht es dem Agenten, den größten Teil der Arbeit zu erledigen, während eine Person das letzte Wort hat.
Verwandte Artikel
- Approval-Gated Agents in LangGraph: interrupt(), Checkpoints und ein Store — Schritt für Schritt einen LangGraph-Agenten erstellen: ein expliziter ReAct-Graph, menschliche Freigabe mittels interrupt(), sowie cross-thread-Memory mit einem Store – und abschließend ein Inbox-Assistent, der zuerst fragt.