Wer schon einmal ein Agentenprodukt ausgeliefert hat, kennt das Gefühl: Eine Demo ist schnell gebaut. Daraus etwas zu machen, dem Nutzer jeden Tag vertrauen — etwas, das den ganzen Tag auf ihrem eigenen Rechner läuft, ohne abzustürzen —, ist schwer. Und das Schwierige ist nicht die Anbindung des Modells, sondern die gesamte Schicht um das Modell herum.
Diese Schicht hat verschiedene Namen; ich nenne sie hier Agent Harness. Sie liegt zwischen dem „großen Modell“ und den „Produktfunktionen“ und ist die eigentliche Laufzeitumgebung: Sie macht aus einer einzelnen Nutzeranfrage Runde um Runde eines Gesprächs mit dem Modell, fügt dazwischen Tool-Aufrufe ein, führt Ergebnisse zurück, verdichtet den Kontext vor dem Überlaufen, wiederholt Aufrufe bei Netzwerkproblemen und stellt das Gespräch sogar nach einem Prozessabsturz wieder her. Das Modell denkt; der Harness macht aus diesem Denken eine zuverlässige Folge von Aktionen.
Orkas ist eine Desktop-Agenten-App, die auf dem eigenen Rechner des Nutzers läuft. Ihr Harness liegt vollständig im Client. Dieser Artikel erklärt den Aufbau dieser Schicht: ihre Unterteilung, die Ausführungsschleife, die Abstraktion von Tools und Modellen sowie den Umgang mit Gedächtnis und Sitzungen. Codedetails wurden bereinigt und verallgemeinert, die technische Struktur ist jedoch real.
Die Schichten
Legt man ein Agentenprodukt auseinander, ergeben sich ungefähr diese Schichten, von unten nach oben:
┌──────────────────────────────────────────────────────────────────────┐
│ Produktfunktionen (Chat / Skills / Konnektoren / Synchronisierung) │
├──────────────────────────────────────────────────────────────────────┤
│ Agent Harness (Ausführungsschleife / Tools / Sitzung) │
├──────────────────────────────────────────────────────────────────────┤
│ Anbieterabstraktion (mehrere LLM-Anbieter vereinheitlichen) │
├──────────────────────────────────────────────────────────────────────┤
│ Infrastruktur (Typen / Fehler / Protokollierung / Konfiguration) │
└──────────────────────────────────────────────────────────────────────┘Darin steckt eine folgenreiche Entscheidung: Die gesamte Modellinferenz erfolgt im Client. Die Desktop-App ist kein Thin Client — sie enthält den Harness selbst und ruft das Modell direkt auf. Der Server übernimmt nur Konten, geräteübergreifende Synchronisierung und Abrechnung; er führt den Agenten überhaupt nicht aus. Diese Entscheidung prägte fast alles Weitere: Sitzungen landen auf dem lokalen Datenträger, Tools arbeiten direkt im Arbeitsverzeichnis des Nutzers, und sensible Daten verlassen den Rechner nie.
Der Harness selbst besteht aus mehreren Teilen: der Ausführungsschleife (Runner), der Sitzung, den Tools, der Provider-Schicht und dem Gedächtnis. Sehen wir sie uns nacheinander an.
Die Ausführungsschleife: ein Streaming-Generator
Das Herz des Harness ist der Runner. In einem Satz beschrieben: Er spricht immer wieder mit dem Modell, bis das Modell sagt: „Ich bin fertig.“
Er ist als asynchroner Generator implementiert, und diese Entscheidung ist wichtig. Ein einzelner Agentenlauf ist weit mehr als „Anfrage senden, auf Ergebnis warten“. Dazwischen passiert viel: Das Modell gibt Tokens aus, möchte ein Tool aufrufen, das Tool ist fertig, der Kontext ist lang genug für eine Verdichtung, das Netzwerk fällt aus und wir versuchen es erneut. Mit Callbacks oder einfachen Promises lassen sich diese Zwischenzustände dem Aufrufer schwer sauber mitteilen. Als Generator werden sie alle zu einem Strom per yield ausgegebener Ereignisse:
type AgentRunEvent =
| { type: "text_delta"; text: string } // model emitting tokens
| { type: "tool_start"; name: string; input: unknown } // a tool starts executing
| { type: "tool_end"; name: string; result: string } // a tool finished
| { type: "compaction"; tokensBefore: number; tokensAfter: number } // context compacted
| { type: "retry"; attempt: number; reason: string } // error, retrying
| { type: "done"; result: AgentRunResult } // terminalDie Oberfläche abonniert diesen Ereignisstrom und zeigt die Modellausgabe und Tool-Ausführung in Echtzeit an. Der Einstiegspunkt ohne Streaming bedeutet intern lediglich „den Stream bis zum Ende konsumieren, das abschließende done übernehmen“. Beide Einstiegspunkte teilen sich eine Implementierung, sodass kein zweiter Codepfad auseinanderdriften kann.
Was innerhalb eines Durchgangs geschieht
Ausgerollt sieht ein Durchgang ungefähr so aus:
- Die Nutzernachricht, gegebenenfalls mit Bildern, in den Sitzungsverlauf einfügen.
- Den System-Prompt zusammenstellen und die aktuell verfügbaren Tools, den Skill-Index usw. einfügen.
- Die Modellzeichenfolge analysieren und in einen konkreten Provider und eine Modell-ID auflösen.
- Alle Tools in Definitionen umwandeln, die das Modell versteht, und sie zusammen mit dem Verlauf senden.
- Den Antwortstream des Modells konsumieren, Text Token für Token per
yieldausgeben und dabei alle Tool-Aufrufe des Modells sammeln. - Wenn der Stream endet, den Stoppgrund des Modells prüfen:
- Bei
tool_usemöchte das Modell ein Tool aufrufen — die Tools ausführen, dann zu Schritt 5 zurückkehren und das Modell erneut fragen. - Andernfalls ist der Durchgang beendet — das Ergebnis zusammenstellen,
yield doneausführen und zurückkehren.
Dabei muss eine Invariante gelten: Auf jeden Tool-Aufruf des Modells muss im Verlauf unmittelbar ein passendes Tool-Ergebnis folgen. Die Modell-API erzwingt diese Paarung strikt. Wird sie verletzt, schlägt die nächste Anfrage fehl oder bleibt einfach hängen. Bei der Selbstreparatur von Sitzungen kommen wir darauf zurück.
Wie ein Tool-Aufruf zurückgeführt wird
Das Modell führt Tools nicht selbst aus. Es sagt nur: „Ich möchte read_file mit diesen Argumenten aufrufen.“ Sobald der Runner diesen Wunsch aufnimmt:
for (const call of toolUseBlocks) {
yield { type: "tool_start", name: call.name, input: call.input };
const tool = this.tools.get(call.name);
const ctx = { workingDir, signal, state: { sandboxEnv } };
const result = await tool.execute(call.input, ctx);
// append the result to the session as a tool-result message
session.addToolResult(call.id, result);
yield { type: "tool_end", name: call.name, result: result.content };
}Tools laufen nacheinander, Ergebnisse werden in der vom Modell angegebenen Reihenfolge in den Verlauf zurückgeschrieben, und anschließend wird das Modell mit diesen Ergebnissen erneut befragt. Nach dem Lesen kann es ein weiteres Tool aufrufen oder seine abschließende Antwort geben. Genau diese Schleife „fragen → aufrufen → antworten → erneut fragen“ ermöglicht einem Agenten mehrstufige Aufgaben.
Ein Detail verdient besondere Erwähnung: Einige Tools geben Bilder zurück — Screenshots oder generierte Bilder. Viele Modelle akzeptieren jedoch keine Bilder im Tool-Ergebniskanal. Orkas trennt das Bild deshalb in eine eigene Nutzernachricht ab, die nach dem Tool-Ergebnis steht. Das Modell liest zunächst „Das Tool hat diesen Text zurückgegeben“ und sieht im unmittelbar folgenden Durchgang das zugehörige Bild. Ein kleiner Kompromiss, der Funktionsunterschiede zwischen Providern überbrückt.
Was tun, wenn der Kontext überzulaufen droht?
Lange Aufgaben stoßen am häufigsten an die Grenze des Kontextfensters. Orkas wartet nicht, bis es voll ist, sondern setzt eine Schwelle von 60%: Nach jeder Tool-Runde schätzt es, wie viel des Fensters die aktuellen Tokens belegen, und löst bei mehr als 60% vorausschauend eine Verdichtung aus.
Bei der Verdichtung wird das Modell gebeten, das bisherige Gespräch zusammenzufassen. Anschließend ersetzt diese Zusammenfassung die alten Nachrichten; nur der jüngste Abschnitt bleibt erhalten. Das klingt einfach, hat aber eine Falle: Nach dem Austausch darf der beibehaltene Abschnitt nicht mit einem „verwaisten Tool-Ergebnis“ beginnen. Ein „Ergebnis ohne passenden Aufruf“ würde die Paarungsinvariante erneut verletzen. Deshalb stellt die Verdichtungslogik sicher, dass der Schnitt an einer sauberen Grenze liegt.
Eine interessantere Entscheidung lohnt die nähere Betrachtung: Warum der grobe Ansatz „bei 60% den gesamten Block zusammenfassen“ statt etwas Feinerem — jede Nachricht bewerten und nach Wichtigkeit kürzen, Tool-Ausgaben strukturiert extrahieren, einen mehrschichtigen Gedächtnisbaum pflegen? Solche Ansätze sehen in Fachartikeln gut aus. Wir haben uns aus drei Gründen bewusst dagegen entschieden.
Erstens: Caching. Der Prompt-Cache des Modells arbeitet mit Präfixen. Solange das Präfix des Verlaufs unverändert bleibt, wird dieser Bereich aus dem Cache bedient, was Geld und Latenz spart. Feingranulare Verdichtung schreibt ständig die Mitte des Verlaufs um und zerstört damit immer wieder das gecachte Präfix — jede Änderung erzwingt eine umfangreiche erneute Vorverarbeitung. Die Strategie „unverändert lassen und an der Schwelle einmal verdichten“ hält das Präfix in der überwiegenden Mehrheit der Durchgänge stabil; nur diese eine Verdichtung macht es ungültig. Das ist deutlich cachefreundlicher.
Zweitens: Komplexität. Die immer wieder betonte Invariante „jeder Tool-Aufruf braucht sein Gegenstück“ wird umso leichter in einem Sonderfall verletzt, je feiner der Verlauf gekürzt wird. Eine grobe Zusammenfassung muss nur eine einzige saubere Schnittstelle schützen; es gibt eine Größenordnung weniger Fehlermöglichkeiten. Eine Klasse von Sonderfällen weniger bedeutet eine Klasse von Produktionsstörungen weniger.
Drittens: von besseren Modellen profitieren. Kontextfenster sind in den letzten Jahren stetig gewachsen, und Modelle kommen immer besser mit langen Kontexten zurecht. Heute viel Aufwand in einen ausgefeilten Verdichtungsalgorithmus zu stecken heißt im Grunde, ein schrumpfendes Problem zu bekämpfen. Wahrscheinlich ist die Abstimmung gerade fertig, wenn die nächste Generation ihr Fenster verdoppelt, und Ihre Komplexität wird zur reinen Belastung. Umgekehrt verbessert sich die Zusammenfassung automatisch mit dem Modell, wenn man sie ihm überlässt: Je besser es Wichtiges erkennt, desto besser die Zusammenfassung, ohne dass wir eine Zeile ändern. Komplexität, die das Modell tragen kann, sollten Sie nicht selbst tragen.
Bei der Tokenschätzung verbirgt sich ein leicht übersehenes Problem: Chinesisch. Schätzt man chinesischen Text nach englischem Sprachgefühl — ungefähr ein Token je einige Zeichen —, zählt man deutlich zu wenig. Orkas gewichtet CJK-Zeichen in der Schätzung separat. Sonst wird die Schwelle bei einem rein chinesischen Gespräch falsch berechnet, und die Verdichtung startet nicht rechtzeitig.
Fehler und Wiederholungsversuche
Auf dem Rechner des Nutzers und in Abhängigkeit von einer externen Modell-API sind Fehler die Regel, nicht die Ausnahme. Der Runner teilt sie in Klassen ein und behandelt jede anders:
- Wiederholbar: Ratenbegrenzungen, Zeitüberschreitungen, Verbindungsabbrüche, 5xx. Exponentiell steigende Wartezeiten mit Zufallsstreuung, begrenzt auf 30 Sekunden. Bei einer Ratenbegrenzung wird ein vom Server gesendetes
retry-afterbeachtet. - Nicht wiederholbar: etwa Authentifizierungsfehler — beliebig viele Wiederholungen helfen nicht, daher sofort mit Fehler abbrechen.
- Sonderfall: Kontextüberlauf. Zuerst eine Verdichtung versuchen, danach einmal wiederholen und erst bei erneutem Scheitern einen Fehler melden.
Es gibt noch eine Klasse: „Das Tool selbst ist fehlgeschlagen.“ Das beendet nicht den ganzen Durchgang — ein Tool-Fehler ist selbst Information für das Modell, das nach „Dieser Befehl ist fehlgeschlagen“ durchaus einen anderen Weg versuchen kann. Der Harness unterscheidet diese vorübergehenden Tool-Fehler von echten Störungen: Er unterbricht weder den Ablauf noch verliert er sie; sie erscheinen in nachträglichen Statistiken. Diese Daten fließen später in den Mechanismus zur Selbstweiterentwicklung ein, um den es im nächsten Artikel geht.
Das externe Abbruchsignal (AbortSignal) wird an jeder wichtigen Stelle geprüft. Drückt der Nutzer „Stopp“, hält der aktuelle Durchgang sofort an — es werden keine neuen Wiederholungsversuche gestartet.
Tool-Abstraktion: einfach genug zum Erweitern
Die Tool-Schnittstelle ist bewusst schlank:
interface AgentTool {
readonly name: string;
readonly description: string; // shown to the model
readonly inputSchema: Record<string, unknown>; // JSON Schema to constrain inputs
execute(input: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult>;
}Ein Tool ist lediglich „ein Name + eine Beschreibung für das Modell + ein Eingabeschema + eine Ausführungsfunktion“. Die eingebauten Tools — Datei lesen, Datei schreiben, Shell-Befehl ausführen, Websuche und Abruf — implementieren alle diese Schnittstelle. Die Desktop-Schicht setzt weitere lokal ausgerichtete Tools darauf, etwa Wissensdatenbanksuche, Bilderzeugung und Aufrufe externer Konnektoren. Die Schnittstelle bleibt dieselbe.
Der Vorteil einer schlanken Schnittstelle: Woher ein Tool stammt, ist für den Runner unerheblich. Eingebaut, nutzerdefiniert oder aus einem Skill geladen — alle sind gleichartige Einträge in einer einzigen Map<string, AgentTool> und werden pro Durchgang in modelllesbare Definitionen umgewandelt.
Tools mit Nebenwirkungen, etwa Shell-Befehle, laufen durch einen isolierten Executor: mit Zeitlimits, Begrenzung der Ausgabelänge, einer Befehlssperrliste und separat übergebenen Umgebungsvariablen, statt die globale Prozessumgebung zu verändern. Letzteres würde in zahlreiche Kindprozesse durchsickern und kann in einer Mehrprozessarchitektur wie Electron leicht den Start verhindern.
Die Provider-Schicht: viele Modelle hinter einer Schnittstelle
Die Modellvorlieben der Nutzer unterscheiden sich stark, und ein Produkt kann sich nicht fest an einen Anbieter binden. Unterhalb des Harness legt Orkas eine Provider-Abstraktion an, die Modelle verschiedener Anbieter hinter einer einzigen Schnittstelle vereinheitlicht:
interface LLMProvider {
readonly id: string;
complete(params: CompletionParams): Promise<CompletionResult>;
stream(params: CompletionParams): AsyncIterable<StreamEvent>;
validateAuth(): Promise<boolean>;
}Der darüberliegende Runner spricht ausschließlich mit dieser Schnittstelle; er weiß nicht, welcher Anbieter dahintersteht. Eine Registry übernimmt das Routing anhand der Modellzeichenfolge: Eine explizite Form provider/model wird direkt aufgeteilt, ein bloßer Modellname anhand seines Präfixes zugeordnet. Auch die Authentifizierung per API-Schlüssel oder OAuth-Token wird hier verwaltet; abgelaufene OAuth-Tokens werden automatisch erneuert.
Bei der Vereinheitlichung vieler Modelle ist nicht die Textvervollständigung das eigentliche Problem, sondern die Randfälle, in denen die Semantik der Anbieter auseinandergeht. Zwei Beispiele haben uns erwischt.
Das erste ist die Erhaltung von Denkblöcken über Anbieter hinweg. Reasoning-Modelle geben einen Bereich mit „Denk“-Inhalten aus. Einige Anbieter verschlüsseln ihn und verlangen die unveränderte Rückgabe, andere stellen ihn mit anderen Feldern dar. Wechselt ein Nutzer mitten im Gespräch von Anbieter A zu Anbieter B, passt die Signatur dieses Denkbereichs im Verlauf nicht mehr. Die Lösung: Jede Nachricht im Verlauf erhält die Kennzeichnung, welches Modell sie erzeugt hat. So kann die Transformationsschicht entscheiden, ob sie unverändert bleibt: gleiches Modell — behalten; anderes Modell — nach den Regeln herabstufen.
Das zweite ist der Prompt-Cache. Zwischen Durchgängen derselben Sitzung wiederholt sich das Präfix stark; sein Caching spart spürbar Kosten und Latenz. Die Implementierung übergibt unterstützenden Anbietern die Sitzungs-ID als Cache-Schlüssel und berücksichtigt deren jeweilige Längenlimits, etwa durch Kürzen oder Hashen zu langer Schlüssel.
Das ist alles Fleißarbeit — aber genau diese Schicht aus Fleißarbeit ermöglicht es dem Runner darüber, so zu tun, als gäbe es „nur eine Art von Modell“.
Gedächtnis: zwei Mechanismen für unterschiedliche Aufgaben
„Gedächtnis“ bezeichnet in Orkas tatsächlich zwei parallele Mechanismen für zwei völlig unterschiedliche Probleme. Der eine ist eine abrufbasierte Wissensdatenbank für umfangreiches Material, das man „bei Bedarf nachschlägt“. Der andere ist ein sitzungsübergreifendes Gedächtnis für die wenigen zentralen Fakten, die man „immer im Kopf haben sollte“. Viele Produkte vermischen beides; die Trennung schafft deutlich mehr Klarheit.
Wissensdatenbank: hybride Suche
Der erste Mechanismus richtet sich an umfangreiche, aber nur gelegentlich relevante Inhalte — Dokumente des Nutzers, ältere Notizen, Fachwissen. Es handelt sich um eine lokale Wissensdatenbank mit Vektorsuche und zwei Backends: einer schlanken reinen Arbeitsspeicherversion für Tests und vorübergehende Nutzung sowie einer dauerhaft in einer lokalen Datenbank gespeicherten Version für den Produktivbetrieb, mit Volltextindex und Vektoren.
Daten gelangen auf diesem Weg hinein:
Dokumente → Aufteilung an Zeilengrenzen (mit Überlappung) → doppelte Indexierung
├─ Volltextindex (Schlüsselwörter, keine Embedding-Kosten)
└─ Vektorindex (falls ein Embedding-Modell konfiguriert ist)Abschnitte werden an Zeilengrenzen mit einer kleinen Überlappung geschnitten, damit vollständige Sinneinheiten nicht mittendurch getrennt werden. Die Suche ist hybrid: ein Vektordurchlauf für semantische Nähe und ein Keyword-Durchlauf für wörtliche Treffer. Beide Ergebnismengen werden per RRF (Reciprocal Rank Fusion) zusammengeführt:
score = Σ 1 / (k + rank_i)Je höher ein Ergebnis in einem Durchlauf rangiert, desto stärker trägt es bei. Über beide Durchläufe summiert berücksichtigt das semantische Relevanz, ohne exakte wörtliche Treffer zu verlieren. Die Gewichte für Vektoren und Keywords sind einstellbar; standardmäßig wird Semantik bevorzugt. Nach dem Zusammenführen werden Ergebnisse anhand von „(Dokument, Startzeile)“ dedupliziert, wobei pro Fundstelle nur das beste bleibt. Anschließend werden Ergebnisse unter einem Schwellenwert entfernt und die besten K zurückgegeben.
Warum nicht ausschließlich Vektoren? Weil Vektorsuche bei Eigennamen, Codesymbolen und exakten Zeichenfolgen oft scheitert — Anfragen, die semantisch nicht besonders sind, bei denen aber der genaue Wortlaut zählt. Reine Keyword-Suche wiederum erkennt „gleiche Bedeutung, andere Formulierung“ nicht. Beides zu kombinieren ist ein sehr praktischer Kompromiss zwischen Suchqualität und Kosten.
Sitzungsübergreifendes Gedächtnis: den Nutzer im Kopf behalten
Die Wissensdatenbank löst das Problem „zu viel Material zum Behalten“. Daneben gibt es eine andere Kategorie: winzig im Umfang, aber jederzeit wichtig — wer dieser Nutzer ist, was er bevorzugt und was zuletzt vereinbart wurde. Das sollte nicht davon abhängen, ob die Suche zufällig das Richtige findet, sondern in jedem Durchgang vorhanden sein.
Dafür baut Orkas eine separate Schicht sitzungsübergreifenden Gedächtnisses auf, inhaltlich in zwei Teile gegliedert:
- Nutzerprofil: stabile Fakten über die Person — Rolle, Vorlieben, Kommunikationsstil, Technologiestack.
- Faktennotizen: dauerhafte Fakten über die Arbeit — Entscheidungen, Meilensteine, Projektkonventionen.
Beide sind klein und jeweils auf wenige tausend Zeichen begrenzt. Das erzwingt die Auswahl dessen, was langfristig wirklich nützlich ist. Sie durchlaufen keine Suche, sondern werden zu Beginn jedes Durchgangs direkt im System-Prompt festgehalten. Der Agent „weiß“ diese Dinge also einfach, ohne erst daran denken zu müssen, sie nachzuschlagen. Das ist genau die Gegenposition zur Wissensdatenbank: Dort gilt „nur bei Bedarf abrufen, danach weg“, beim sitzungsübergreifenden Gedächtnis „immer vorhanden, immer sichtbar“.
Schreibzugriffe erfolgen über ein eigenes Gedächtnis-Tool. Das Modell ruft es auf, wenn es im Gespräch entscheidet: „Das ist langfristig merkenswert.“ Unterstützt werden Hinzufügen, Ersetzen von Teilzeichenfolgen und Löschen. Die Tool-Beschreibung legt klar fest, was gespeichert werden soll: Nutzerkorrekturen und Vorlieben haben höchste Priorität; dauerhafte Entscheidungen und Konventionen werden gespeichert. Vorübergehende Zustände der aktuellen Aufgabe, einmalige Debug-Informationen und leicht wiederauffindbare Inhalte dagegen nicht. Das Gedächtnis ist für „dauerhafte Fakten über Nutzer und Projekt“ da, nicht für „wo ich diesmal stehen geblieben bin“.
Ein leicht übersehenes, aber wichtiges Detail: Vor jedem Schreibzugriff läuft eine Sicherheitsprüfung. Dieser Inhalt gelangt wortwörtlich in den System-Prompt und bleibt lange über Sitzungen hinweg erhalten — damit ist er faktisch eine dauerhafte Angriffsfläche für Injektionen. Jede zu speichernde Erinnerung wird deshalb vor dem Schreiben auf verdächtige Muster geprüft: typische Prompt-Injection-Formulierungen wie „Ignorieren Sie alle bisherigen Anweisungen“, Befehle zum Ausschleusen von Schlüsseln oder im Text versteckte unsichtbare Unicode-Zeichen. Ein Treffer wird sofort abgewiesen. Zusammen mit Deduplizierung und Kürzung überlanger Inhalte bleibt diese Gedächtnisschicht nützlich, ohne zum Risiko zu werden.
Zusammen decken die beiden Mechanismen beide Enden ab — „riesig, aber gelegentlich“ und „klein, aber ständig“: Die Wissensdatenbank übernimmt Ersteres, das sitzungsübergreifende Gedächtnis Letzteres. Hinzu kommt das Verständnis des Agenten von sich selbst, Thema des nächsten Artikels. So bringt ein Orkas-Agent drei Arten von Gedächtnis gleichzeitig mit: über das Material, über den Nutzer und über sich selbst.
Sitzungen: auf Abstürze und Reparatur ausgelegt
Eine Sitzung verwaltet den Nachrichtenverlauf. Die Grundversion ist lediglich ein Array von Nachrichten im Arbeitsspeicher mit Verlaufskürzung und Verdichtung. Doch alles, was auf einem Nutzerrechner läuft, muss jederzeit mit einem Abbruch rechnen — der Nutzer beendet die App, das System startet neu, ein Watchdog-Zeitlimit beendet den Prozess. Deshalb verwendet der Produktivbetrieb eine dauerhafte Sitzung in einer lokalen JSONL-Datei, eine Nachricht pro Zeile.
Es gibt zwei Schreibstrategien: Neue Nachrichten werden atomar angehängt. Alles, was die gesamte Datei neu schreibt, etwa Verdichtung oder Leeren, nutzt „temporäre Datei schreiben + atomar umbenennen“. So bleibt selbst bei einem Stromausfall während des Schreibens nie ein halber beschädigter Datensatz zurück.
Der interessanteste Teil ist die Reparatur verwaister Tool-Aufrufe. Zurück zur Paarungsinvariante: Das Modell ruft ein Tool auf, der Harness führt es aus, das Ergebnis wird zurückgeschrieben. Wird einer dieser drei Schritte unterbrochen, bleibt auf dem Datenträger ein „Aufruf ohne Ergebnis“ zurück. Lädt man diese Sitzung beim nächsten Mal unverändert ins Modell, lehnt die API sie ab oder bleibt hängen.
Die Reparaturlogik läuft bei jedem Laden einer Sitzung vom Datenträger und ist idempotent:
- Alle Assistentennachrichten durchsuchen und die darin erzeugten Tool-Aufruf-IDs sammeln.
- Danach nach den passenden Tool-Ergebnissen suchen.
- Für jeden Aufruf ohne passendes Ergebnis ein als „unterbrochen“ markiertes Ergebnis erzeugen.
- Dabei die Reihenfolge der Ergebnisse an die deklarierte Aufrufreihenfolge anpassen und verwaiste Ergebnisse ohne passenden Aufruf entfernen.
Nach diesem Durchlauf erfüllt die Sitzung garantiert die Paarungsanforderung der API und kann sicher gesendet werden. Der Mechanismus wirkt unspektakulär, ist aber das Sicherheitsnetz, das verhindert, dass ein Nutzergespräch wegen eines einzigen Absturzes dauerhaft blockiert.
Einige rückblickend wichtige Entscheidungen
Im Zusammenspiel erscheinen einige Entscheidungen im Nachhinein besonders wertvoll.
Generatoren als primäre Schnittstelle. Streaming und Nicht-Streaming teilen eine Implementierung, Zwischenzustände werden ganz natürlich sichtbar, und die Oberfläche kann beliebig viele Details darstellen. Das ersparte eine ganze Klasse von Inkonsistenzfehlern, die bei „erst ohne Streaming bauen, später Streaming anbauen“ entstanden wäre.
Bei 60% verdichten, nicht erst bei vollem Fenster. So bleibt Platz für die Verdichtung selbst, die ebenfalls einen Modellaufruf kostet, und hektische Maßnahmen im letzten Moment entfallen.
Die Paarungsinvariante gilt überall. Vom Schnittpunkt der Verdichtung über das Schreiben auf den Datenträger bis zur Reparatur beim Laden hält jede Stelle, die die Sitzung berührt, dieselbe Regel ein. Dank dieser einen Regel muss keine Stelle ihre eigene Reparaturlogik erfinden.
Fleißarbeit in der Provider-Schicht bündeln. Alle Unterschiede zwischen Anbietern — Denkblöcke, Cache-Schlüssel, Funktionsunterschiede — werden in dieser einen Schicht verarbeitet. Dafür bleibt der Runner darüber sauber. Kommt später ein neuer Modellanbieter hinzu, greift die Änderung kaum darüber hinaus.
Zum Abschluss
Der Harness von Orkas enthält keinen spektakulären Algorithmus. Sein Wert liegt darin, „einen Agenten in einer echten Umgebung zuverlässig ausführen“ in Module mit klaren Grenzen aufzuteilen, die jeweils einen Teil besitzen: Der Runner verantwortet Schleife und Wiederholungen, Tools die Fähigkeiten, die Provider-Schicht die Vereinheitlichung vieler Modelle, das Gedächtnis den Abruf, die Sitzung Speicherung und Reparatur. Für sich genommen ist keines komplex; erst zusammen tragen sie etwas, das Menschen täglich nutzen.
Was Sie mitnehmen können: Eine Ausführungsschleife als Streaming-Generator macht Zwischenzustände viel leichter handhabbar. Steht eine zentrale Invariante fest, etwa „Tool-Aufrufe müssen gepaart sein“, halten Sie sie bei Verdichtung, Datenträgerschreibzugriffen und Laden konsequent ein, ohne Ausnahmen in irgendeiner Ecke. Bündeln Sie anbieterübergreifende Fleißarbeit in einer Schicht und halten Sie sie aus der Geschäftslogik heraus. Und vor allem: Gehen Sie davon aus, dass Ihr Prozess im schlechtestmöglichen Moment beendet wird, und schreiben Sie die Reparatur dafür im Voraus.
Der nächste Artikel behandelt einen interessanteren Teil von Orkas: wie dieser Agent aus der eigenen Nutzung lernt, Erfahrung in wiederverwendbare Skills verdichtet und sich schrittweise nützlicher macht.