Eine Schleife, die plant, Werkzeuge ausführt und die Ergebnisse liest, und die sich fehlende Werkzeuge selbst schreibt. Sie läuft in einfachen OTP-Prozessen, ohne Agent-Framework.
Diese Seite geht die beweglichen Teile durch, spielt einen Lauf Schritt für Schritt nach und listet die Grenzen so, wie sie heute im Code stehen. Jede Zahl darauf stammt aus dem Quellcode.
Jede Seite fügt der vorigen eine Idee hinzu. Alle teilen sich dieselben Werkzeuge und Prozesse.
Dasselbe Ziel geht an zwei Agenten, die sich nur in ihrem Gehirn unterscheiden. Der eine nutzt Stichwortregeln (ClassicPlan), der andere ein Modell (AiPlan, Mock oder Claude). Die Ergebnisse stehen nebeneinander.
Du beschreibst ein Werkzeug, und ein Modell schreibt das Elixir-Modul. Du siehst zu, wie es den Sanitizer passiert, kompiliert, unter einem versionierten Namen geladen wird und einen Probeaufruf beantwortet. Die Registry listet alle bisher generierten Werkzeuge.
Jede Unterhaltung ist ein GenServer. Ein Worker fährt die Schleife, und Fortschritt, Freigaben, die Spur und gemerkte Fakten erreichen die LiveView über PubSub, sobald sie passieren.
Wähle eine Aufgabe und geh sie Schritt für Schritt durch. Das Schleifen-Diagramm hebt den Teil hervor, der gerade arbeitet. Das ist eine Nachstellung: Werkzeugnamen und Texte sind Beispiele, aber jeder Schritt ist einer, den der Code tatsächlich macht.
Der einzige Teil, der etwas entscheidet, ist der Planer, und er ist austauschbar. Alles drumherum ist einfaches OTP und Phoenix: die Schleife, die Werkzeuge, die Prozesse und die Prüfungen.
Das Modell merkt sich zwischen zwei Aufrufen nichts. Vor jeder Runde setzt TaskAgent den Kontext des Planers neu zusammen: sechs Teile, jeder aus einer anderen Quelle, jeder unterschiedlich lange gültig. Alles, was der Agent „weiß“, muss darin stehen.
Die Regeln: vorhandene Werkzeuge nutzen, neue in snake_case erfinden, nach Ergebnissen neu planen, dauerhafte Fakten merken.
Der Katalog plus jedes generierte Werkzeug aus der Registry, mit Name, Pflichtparametern und Beschreibung. Ein in Runde 1 geschriebenes Werkzeug wird in Runde 2 angeboten.
Die Fakten aus der JSON-Datei, höchstens 50 zu je 300 Zeichen. Jede Runde neu gelesen, ein gerade gemerkter Fakt zählt also sofort.
Die letzten 4 Runden, jeweils verkürzt auf die Aufgabe und ihre Antwort (oder ihre Schritt-Ergebnisse). Damit wird „und jetzt in Kelvin“ auflösbar.
Die Nachricht, die du gerade geschickt hast, unverändert.
Jeder Schritt dieser Aufgabe mit Parametern und Ergebnis oder Fehler, jeweils nach 2000 Zeichen abgeschnitten. So beobachtet die Schleife.
system: You plan the tools of a software agent. … (etwa 40 Zeilen Regeln) user: Available tools: - ai_plan(goal) – AI assisted task planning - celsius_to_fahrenheit(celsius) – Converts Celsius to Fahrenheit - celsius_to_kelvin(celsius) – Converts Celsius to Kelvin - classic_plan(goal) – Rule based task planning without AI - forget(id) – Forgets the remembered fact with the given id - remember(fact) – Remembers a short, self-contained fact … Things you remember: [1] The user lives in Hamburg Conversation so far: - User: Convert 30 degrees Celsius to Fahrenheit Result: 30 °C is 86 °F. Task: and now in Kelvin Results so far: - celsius_to_kelvin({"celsius":30}) -> %{kelvin: 303.15}
celsius_to_kelvin wurde in Runde 1 generiert und steht schon in der Werkzeugliste, sein Ergebnis unter „Results so far“.So sieht der Kontext des Planers mit dem Claude-Backend aus. Der Mock-Planer bekommt dieselbe Map, schaut aber nur auf die Aufgabe und die Ergebnisse.
Nennt der Planer ein Werkzeug, das noch niemand geschrieben hat, übernimmt Dynamic.ensure_action/2. Was Sanitizer, Compiler, Schema-Prüfung oder Probeaufruf ablehnen, geht mit Begründung zurück an das Modell, insgesamt höchstens drei Versuche. Lehnst du ab, ist Schluss.
Die Grenzen sind markiert. Absicht heißt eine bewusste Entscheidung für eine Demo, Abkürzung etwas, das ein echtes System bräuchte, Risiko ein Sicherheitsvorbehalt und Modell Verhalten, das in Eval-Läufen mit Claude zu sehen war.
Der Planer sieht die Ergebnisse jedes Schritts und plant die nächste Runde. So kann er Werte von einem Schritt in den nächsten tragen, einen gescheiterten Schritt wiederholen oder antworten.
Ein unbekannter Werkzeugname löst Codegenerierung aus. Das Modul wird abgelegt, kompiliert, unter einem versionierten Namen geladen und in späteren Runden wiederverwendet.
Ein Gang durch den AST mit einer Verbotsliste (Shell, Eval, Spawn, Atome) und einer Liste erlaubter Module. Dateien schreiben geht nur unterhalb des Sandbox-Verzeichnisses, ins Netz nur per GET über eine einzige Funktion.
Neuer Code wartet vor seinem ersten Lauf auf einen Klick: freigeben oder ablehnen, mit dem vollständigen Quelltext. Standardmäßig an, nach 10 Minuten läuft die Wartezeit ab.
Jeder Schritt läuft genau einmal, in einem eigenen Prozess mit Zeit- und Heap-Grenze. Wird eine überschritten, entsteht ein lesbarer Fehler, auf den der Planer reagieren kann.
Aufrufe an die Claude-API werden bei 408, 429, 5xx und 529 und bei abgebrochenen Verbindungen wiederholt, unter Beachtung von retry-after. Werkzeuge nie, damit keine Seite doppelt abgerufen wird.
„Remember that …“ merkt sich einen Fakt, „forget fact 3“ entfernt ihn. Fakten stehen in jedem Planer-Prompt und erscheinen sofort in jedem offenen Chat.
Jeder Planer-Aufruf, jede Codegenerierung, jeder Werkzeug-Schritt und jede Wartezeit auf eine Freigabe steht mit Dauer und Tokens da, darunter die Summen. Dieselben Events speisen Kennzahlen im LiveDashboard.
Ein GenServer pro Unterhaltung, gefunden über eine Registry. Er übersteht ein Neuladen der Seite und endet nach 15 Minuten ohne Zuschauer. Stürzt er ab, startet die LiveView einen neuen, andere Unterhaltungen bleiben unberührt.
Der Planer kann der Mock oder Claude sein, der Generator der Mock, Claude oder ein lokales Ollama-Modell. Du wählst pro Lauf in der Oberfläche, und die Mocks laufen ohne API-Key.
mix agent.eval lässt feste Fälle mehrmals laufen und meldet eine Erfolgsquote mit Runden, Tokens und Zeit. Die Fälle prüfen den Weg des Agenten, nicht nur die Antwort.
Werkzeuge sind ein Behaviour mit vier Callbacks, validiert mit NimbleOptions. Alles andere ist GenServer, Registry, DynamicSupervisor, Task.Supervisor und PubSub.
Der Sanitizer sieht nur den AST. Code, der ihn passiert, läuft mit allen Rechten des BEAM-Knotens. Was fehlt, ist Isolation auf Betriebssystemebene.
RisikoDie URL kommt vom Modell. Ein GET kann interne Dienste und Metadaten-Endpunkte treffen oder Daten in einen Query-String schreiben. Wo das zählt, gehört die Einschränkung ins Netz.
RisikoKatalog-Werkzeuge und wiederverwendete generierte Werkzeuge laufen ohne Nachfrage. Rechte pro Werkzeug gibt es nicht, und der Eval-Harness gibt automatisch frei.
RisikoDer Planer sieht die letzten 4 Runden, jeweils zusammengefasst, und Schritt-Ergebnisse nach 2000 Zeichen abgeschnitten. Nichts verdichtet älteren Kontext.
AbkürzungNachrichten gibt es nur im Agent-Prozess. Nach 15 Minuten ohne Zuschauer oder einem Neustart sind sie weg. Dauerhaft gespeichert werden nur Fakten und generierte Werkzeuge.
AbkürzungFakten sind eine globale Liste mit höchstens 50 Einträgen zu je 300 Zeichen. Für einen Nutzer reicht das; bei mehreren bräuchte jeder sein eigenes.
AbkürzungRegistry, ETS-Werkzeugregistry und geladene Module gelten nur auf einem Knoten. Ein Cluster bräuchte verteilte Suche und Code-Laden auf jedem Knoten.
AbkürzungDie Spur zählt Tokens, zeigt aber keine Preise, weil Preise im Code veralten. Ein Budget, das einen Lauf stoppt, gibt es nicht.
AbkürzungDer Planer liefert Schritte als erzwungenes JSON, statt die Tool-Aufrufe der API zu nutzen. Nur so kann der Stichwort-Mock dieselbe Schleife antreiben.
AbsichtSchritte laufen nacheinander, damit jeder sein eigenes Ergebnis behält. Parallele Werkzeugaufrufe gibt es nicht, und jede Unterhaltung bearbeitet eine Aufgabe zur Zeit; eine Nachricht in der Zwischenzeit wird ignoriert.
AbsichtNach 5 Runden mit Werkzeugaufrufen gibt die Schleife mit :max_rounds auf und liefert, was sie hat.
Generierte Werkzeuge rechnen, verarbeiten Text oder lesen eine Seite. Uhr, Zufallszahlen, Prozesse und Schreiben außerhalb des Sandbox-Verzeichnisses bekommen sie nicht.
AbsichtDer Mock-Planer sucht Stichwörter und ignoriert Verlauf und Fakten. Der Mock-Generator gibt nur seine Parameter zurück. Sie zeigen die Mechanik, keine Intelligenz.
AbsichtFortschritt kommt pro geplanter Runde und pro fertigem Schritt. Der Text des Modells selbst wird nicht gestreamt.
AbsichtMit Claude kamen Temperaturumrechnungen in 0 Runden zurück: die richtige Antwort ganz ohne Werkzeug. Das ist vernünftig, überspringt aber die Werkzeuge, die der Fall testen sollte.
ModellSollte es die Wörter eines Seitentitels zählen, generierte Claude ein kombiniertes Werkzeug, statt ein Seiten- und ein Text-Werkzeug zu verketten.
ModellIn 3 von 30 Läufen gerieten Teile des Schritts in den Werkzeugnamen. Die Pipeline lehnte sie ab, und der Planer fing sich wieder. Das Ausgabe-Schema beschränkt Namen jetzt per Pattern; ein Claude-Lauf hat das noch nicht geprüft.
ModellEinmal generierte Claude prioritize_todo_list, obwohl classic_plan im Katalog stand. Das kostete etwa 23 Sekunden.
Das sind die Voreinstellungen im Code. Die meisten lassen sich über config :agent_demo, ... ändern.
| Was | Wert | Wo |
|---|---|---|
| Runden mit Werkzeugaufrufen pro Aufgabe | 5 | TaskAgent @max_rounds |
| Frühere Runden, die der Planer sieht | 4 | ChatAgent @history_turns |
| Schritt-Ergebnis für den Planer | 2000 Zeichen | ToolPlanner.Anthropic |
| Zeit pro Werkzeug-Schritt / Probeaufruf | 60 s / 20 s | Dynamic.Limits, Dynamic |
| Heap pro Werkzeug-Schritt | 64 MB | Dynamic.Limits |
| HTTP-GET Timeout / Größe | 10 s / 2 MB | Dynamic.Http |
| Generierungsversuche (1 + Reparaturen) | 3 | Dynamic generation_attempts |
| Werkzeugname | ^[a-z][a-z0-9_]{2,49}$ | Dynamic.name_pattern/0 |
| Warten auf Freigabe | 10 min | ChatAgent.Worker |
| Agent endet ohne Zuschauer nach | 15 min | ChatAgent @idle_timeout |
| Fakten / Länge pro Fakt | 50 / 300 Zeichen | Memory |
| Wiederholungen eines Claude-API-Aufrufs | 3 | AI.Retry |
| Wiederholungen eines Werkzeug-Schritts | 0 | TaskAgent |
Das ist mix agent.eval --planner anthropic --generator anthropic --runs 3 mit der ersten Fassung der Fälle: 27 von 30 Läufen bestanden. Die Spalte mit den Runden zeigte, dass „bestanden“ nicht immer hieß, dass der Agent den vorgesehenen Weg genommen hatte.
celsius, celsius_reuse und use_memory wurden ohne Werkzeug beantwortet. Also wurde nichts generiert und nichts wiederverwendet.min_rounds und nothing_generated. Einen Claude-Lauf mit den neuen Fällen gibt es noch nicht.