Files
ClawdDotNet/docs/Bestandsaufnahme-2026-07.md
T
RichardandClaude Opus 4.8 69b5704add Prompt-Caching, Tool-Ergebnis-Kappung und Kostenerfassung
T1 — Prompt-Caching. Bisher wurde bei jedem Schritt eines Runs der komplette
Prompt neu berechnet, inklusive Tool-Definitionen und System-Prompt, die sich nie
aendern. Bei zehn Schritten und einem 15k-Praefix sind das 150.000 statt 15.000
Eingabe-Tokens.

ChatMessage bekommt dafuer einen eigenen JsonConverter: Der Inhalt geht weiterhin
als String raus, bei gesetztem CacheBreakpoint jedoch als Blockarray mit
cache_control. Beim Lesen werden beide Formate akzeptiert, damit bestehende
ChatContext.json weiter geladen werden koennen.

PromptCache setzt zwei Breakpoints: einen auf den System-Prompt (deckt
Tool-Definitionen und System-Prompt ab) und einen rollierenden auf die letzte
Nachricht mit Inhalt. Vorherige Markierungen werden vorher entfernt, damit sie
sich nicht ansammeln. Aktivierung ueber promptCaching: auto (Default, aktiv fuer
Modelle mit Unterstuetzung), on oder off.

Der wichtigste Test dazu prueft die Praefix-Stabilitaet: Der System-Prompt muss
ueber alle Schritte zeichengleich serialisiert werden. Ein einziger Zeitstempel
darin wuerde den Cache still verwerfen — die Kosten blieben unveraendert, ohne
dass es irgendwo auffiele.

T9 — Usage liest prompt_tokens_details.cached_tokens; die Zahl wird bis in
AgentRunResult durchgereicht. Ohne sie liesse sich die Wirkung nicht belegen.

T2 — Tool-Ergebnisse werden jetzt zentral in ExecuteToolCallAsync gekappt
(maxToolResultChars, Default 16.000). Bisher konnte ein einzelner WebFetch mit
dem 512-KB-Standardlimit rund 130.000 Tokens in EINER Antwort erzeugen; die
Compaction griff erst danach, bezahlt war der Request laengst.

T3 — Die Zusammenfassung beim Kompaktieren laeuft ueber ein konfigurierbares
summaryModel (Default gemini-2.5-flash) statt ueber das teure Agentenmodell.

B4 — AgentRunResult fuehrt Prompt- und Completion-Tokens getrennt; die
Kostenanzeige schaetzte bisher 50/50, real liegt das Verhaeltnis eher bei 95:5.
Die veraltete Preistabelle bleibt offen.

Neue Einstellungen sind im PropertyGrid sichtbar und werden vom AgentEditor bei
neuen Agenten mitgeschrieben.

Alle 91 Tests gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:32:17 +02:00

27 KiB
Raw Blame History

ClawdDotNet — Bestandsaufnahme (Juli 2026)

Vollständiges Review von Core-Engine, Security-Layer, Tools, Scheduling und UI. Stand: Commit 92e50d3.

Kurzfassung: Die Architektur ist gut — die Trennung Core/Tools/UI, das IAgentTool-Interface, der Tool-Job-Mechanismus und das Instanz-Konzept tragen. Die Probleme liegen fast alle in drei Bereichen: (1) Sicherheitsprüfungen, die als String-Vergleiche implementiert sind und deshalb umgehbar sind, (2) fehlende Nebenläufigkeits-Absicherung im geteilten Chat-Kontext, (3) kein Prompt-Caching — der mit Abstand größte Kostenhebel.


1. Kritische Sicherheitslücken (P0)

S1 — DatabaseTool: Tabellen-Whitelist ist vollständig umgehbar

DatabaseTool.cs:218

return allowed.Any(t => t != null && inputLower.Contains(t));

Die Prüfung sucht den erlaubten Tabellennamen irgendwo im SQL-String — auch in einem Kommentar oder String-Literal. Bei allowedTables: ["prices"] und accessLevel: ReadWrite:

DELETE FROM users -- prices

IsWriteAttempt → true (ReadWrite erlaubt), IsTableAllowed → true (enthält "prices"). Die users-Tabelle wird gelöscht. Gleiches gilt für jede beliebige andere Tabelle.

Zusätzlich:

  • IsWriteAttempt/IsAdminAttempt (Z.189/195) sind ebenfalls Substring-Prüfungen → False Positives (SELECT * FROM prices WHERE note='update' wird als Schreibzugriff blockiert).
  • Mehrere Statements pro Aufruf sind nicht unterbunden (;-Verkettung).
  • Query-Ergebnisse haben kein LIMIT → eine Tabelle mit 100k Zeilen landet komplett im Kontext.

Fix: Echtes SQL-Parsing (z.B. via Microsoft.SqlServer.TransactSql.ScriptDom für MSSQL bzw. einem generischen Tokenizer), oder — deutlich robuster — den freien SQL-Zugriff durch vordefinierte, parametrisierte Named Queries ersetzen. Freies SQL vom LLM ist grundsätzlich schwer abzusichern.

S2 — SocialMediaManager: Kommando-Injection über yt-dlp

SocialMediaManagerTool.cs:442, auch Z.484 und Z.490

var psi = new ProcessStartInfo(ytDlpPath, $"--print \"%(id)s\" --playlist-end 1 {channelUrl}")

channelUrl stammt aus dem Agenten-Input und wird ungequotet in die Argumentliste interpoliert. UseShellExecute = false verhindert Shell-Metazeichen, aber nicht Options-Injection: yt-dlp kennt --exec, das beliebige Befehle nach dem Download ausführt.

Ein Wert wie --exec "cmd /c ..." https://youtube.com/... führt zu Codeausführung. Kritisch wird das, weil der Agent untrusted Content verarbeitet (YouTube-Titel, X-Posts, Mails) — eine Prompt-Injection in abgerufenen Inhalten kann den Agenten dazu bringen, genau so einen "URL"-Parameter zu setzen.

Fix: ProcessStartInfo.ArgumentList verwenden (quotet korrekt), zusätzlich die URL gegen eine Regex validieren (^https://(www\.)?(youtube\.com|youtu\.be)/…) und ---Separator vor dem URL-Argument setzen.

S3 — DirectAPI: API-Keys landen im Modell-Kontext

DirectAPITool.cs:83, auch Z.99, 154, 177, 263

var url = $"https://api.twelvedata.com/quote?symbol={symbol}&apikey={apiKey}";

return CreateSuccessResult(fetchedAt, dataAsOf, url, json.Value);   // url → "source"

Die vollständige URL inklusive apikey= wird als source an das Modell zurückgegeben. Damit landet der Key:

  • im Chat-Kontext → wird bei jedem Folge-Request erneut an OpenRouter gesendet,
  • in ChatContext.json auf der Platte,
  • in den Logs.

Fix: Key als Header senden statt als Query-Parameter (wo möglich), und in source grundsätzlich eine bereinigte URL zurückgeben (Query-String entfernen oder apikey maskieren).

S4 — PermissionGate ist faktisch wirkungslos

PermissionGate.cs:7

public bool IsAllowed(string agentId, string toolName, AgentConfig agentConfig)
    => agentConfig.Tools.ContainsKey(toolName);

Der einzige Check ist "Tool ist dem Agenten zugewiesen" — was ToolRegistry.GetForAgent ohnehin schon filtert. Der agentId-Parameter wird nicht verwendet. Es gibt keine Aktions-Granularität (lesen vs. schreiben vs. senden), kein Rate-Limiting, kein Audit. Die gesamte Sicherheit liegt ad-hoc in den einzelnen Tools — jedes mit eigener, inkonsistenter Logik.

Fix: Das Gate zum zentralen Policy-Punkt ausbauen: Aktions-Ebene (Tool.Action), Rate-Limits pro Agent/Tool/Zeitfenster, Audit-Log jedes Aufrufs mit Argumenten, und ein Approval-Hook für irreversible Aktionen (siehe F-A1).

S5 — WebFetch: kein SSRF-Schutz bei Redirects

WebFetchTool.cs:58

Die Domain-Whitelist wird nur auf die ursprüngliche URL angewendet. HttpClient folgt Redirects standardmäßig — eine erlaubte Domain kann auf http://169.254.169.254/, http://localhost:8418/ (dein Gitea!) oder beliebige interne Hosts weiterleiten.

Nebenbei: uri.Host.Replace("www.", "") (Z.59) ersetzt das Fragment überall im Hostnamen, nicht nur am Anfang.

Fix: HttpClientHandler { AllowAutoRedirect = false } und Redirects manuell auflösen, dabei jede Zwischen-URL erneut gegen die Whitelist prüfen. Zusätzlich private IP-Bereiche (RFC1918, Loopback, Link-Local) hart blockieren.

S6 — FileRW: Path-Traversal-Prüfung per Präfix

FileRWTool.cs:211

if (!fullPath.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase))

Ohne abschließenden Verzeichnis-Separator: Root C:\Instances\Dev\Workspace erlaubt Zugriff auf C:\Instances\Dev\Workspace-Backup\…. In der aktuellen Verzeichnisstruktur nicht direkt ausnutzbar, aber eine Zeitbombe.

Fix: rootPath mit Path.DirectorySeparatorChar terminieren, oder besser Path.GetRelativePath + Prüfung auf ..-Präfix.

S7 — Secrets im Klartext

openRouterApiKey, DB-connectionString (mit Passwort), Mail-password, Telegram-password2FA liegen unverschlüsselt in AgentSettings.json / InstanceConfig.json (InstanceConfig.cs:46).

Fix: DPAPI (ProtectedData.Protect mit CurrentUser-Scope) für alle Secret-Felder, oder Windows Credential Manager. Zumindest sollten die Felder beim Speichern verschlüsselt und erst zur Laufzeit entschlüsselt werden.


2. Bugs (P1)

B1 — ContextCompactor zerstört die tool_call-Paarung ⚠️

ContextCompactor.cs:140

var tail = messages.Skip(Math.Max(0, messages.Count - ProtectedTailMessages)).ToList();
messages.Clear();
 messages.AddRange(tail);

Die letzten 6 Nachrichten werden blind übernommen. Wenn die Grenze mitten in einer Tool-Sequenz liegt, beginnt der Tail mit einer tool-Nachricht, deren zugehörige assistant-Nachricht mit tool_calls gerade gelöscht wurde. Die OpenRouter/Anthropic-API lehnt das mit HTTP 400 ab ("tool_result without preceding tool_use").

Das passiert genau dann, wenn der Kontext lang wird — also im wichtigsten Moment. Bei einer typischen Sequenz assistant(2 tool_calls) → tool → tool ist die Wahrscheinlichkeit hoch.

Fix: Die Tail-Grenze auf einen sicheren Schnittpunkt verschieben — rückwärts suchen bis zu einer Nachricht mit Rolle user oder assistant ohne tool_calls. Analog muss ein assistant mit tool_calls am Ende immer seine vollständigen tool-Antworten behalten.

B2 — Race Condition auf dem geteilten Chat-Kontext ⚠️

AgentEngine.cs:250 und Z.268/316/322

_chatContexts[agentId] ist eine geteilte List<ChatMessage>. Nur der Lookup läuft unter _lock — alle messages.Add(...) im Loop passieren ungeschützt.

Für denselben Agenten kann ChatAsync mehrfach parallel laufen:

  • Telegram-/Mail-ToolJob weckt den Agenten (ToolJobScheduler.cs:187),
  • der Benutzer schickt gleichzeitig eine WebView-Nachricht,
  • ein anderer Agent ruft send_message auf (AgentEngine.cs:528).

Folgen: List<T>-Korruption (nicht threadsicher), verschränkte Tool-Call-Sequenzen → API-400-Fehler, und _runningChats[agentId] = linkedCts überschreibt den vorherigen Eintrag, sodass AbortChat nur den zuletzt gestarteten Lauf abbricht.

Fix: Pro Agent ein SemaphoreSlim(1,1), das den gesamten ChatAsync-Durchlauf serialisiert. Wartende Nachrichten in eine Queue statt parallel starten.

Beim Umsetzen kamen zwei Folgeprobleme dazu, die denselben Ursprung haben:

  • ExecuteToolCallAsync fing OperationCanceledException mit ab und gab sie als Tool-Fehlerergebnis zurück. Der Abbruch griff dadurch erst einen Schritt später.
  • send_message an den eigenen Agenten wäre mit dem neuen Gate in einen Deadlock gelaufen (der laufende Chat hält es bereits) — wird jetzt abgelehnt.

B3 — maxTokens vermischt Abrechnungs-Budget und Kontextgröße ⚠️

LoopGuard.cs:22, Defaults in AgentConfig.cs:105

RecordTokens summiert response.Usage.TotalTokens über alle Schritte. Da jeder Schritt den kompletten Kontext erneut sendet, wächst diese Summe quadratisch:

Schritt Kontext kumuliert
1 20.000 20.000
2 21.000 41.000
3 22.000 63.000
4 23.000 86.000 → Abbruch

Mit maxTokens: 80_000 bricht ein völlig normaler Chat nach 4 Schritten mit "Max tokens exceeded" ab, obwohl der Kontext nie über 23k lag. Das ist vermutlich die häufigste Frustquelle im laufenden Betrieb.

Fix: Zwei getrennte Begriffe:

  • maxCumulativeTokens — Kostenbudget pro Run (bewusst hoch, z.B. 500k)
  • maxContextTokens — Kontextgröße, steuert die Compaction (existiert bereits)

und beide getrennt im UI ausweisen.

B4 — Kostenanzeige ist doppelt falsch

frm_main.cs:292

_statusService?.RecordUsage(model, result.TokensUsed / 2, result.TokensUsed / 2);
  1. 50/50-Aufteilung: Reales Verhältnis in Agenten-Loops ist eher 95:5 (Prompt:Completion). Da Output ~5x teurer ist, überschätzt die Anzeige die Kosten um Faktor ~2,5.
  2. Modell fehlt in der Preistabelle: AgentConfig.Model hat den Default anthropic/claude-sonnet-4-5 — dieser Eintrag existiert in ModelPricing (OpenRouterStatusService.cs:15) nicht. CalculateCost gibt dann stillschweigend 0 zurück.

Die Tabelle ist zudem veraltet (claude-sonnet-4, claude-opus-4, claude-haiku-4.5).

Fix: Usage um PromptTokens/CompletionTokens erweitern und bis in AgentRunResult durchreichen. Preise live vom /models-Endpoint holen (liefert pricing.prompt/pricing.completion) statt hartzucodieren. Bei unbekanntem Modell sichtbar warnen statt 0 anzuzeigen.

B5 — Tool-Ergebnisse landen ungekappt im Kontext

Ein einziger WebFetch mit dem Default maxResponseKb: 512 (WebFetchTool.cs:90) erzeugt bis zu 512 KB Text ≈ 130.000 Tokens in einer einzigen Tool-Antwort. FileRW.read (FileRWTool.cs:245) hat gar kein Limit, Database.query und Mail.read_inbox ebenfalls nicht.

Gekürzt wird erst nachträglich in der Compaction — und dort nur außerhalb der letzten 6 Nachrichten. Der teure Request ist zu dem Zeitpunkt längst bezahlt.

Fix: Kappung beim Einfügen in ExecuteToolCallAsync (AgentEngine.cs:668) — zentral, für alle Tools, mit konfigurierbarem maxToolResultTokens und einem Hinweis an das Modell, dass gekürzt wurde (inkl. Angebot, gezielt nachzulesen).

B6 — Task.Delay wirft bei langen Cron-Intervallen

AgentScheduler.cs:89 und ToolJobScheduler.cs:126

Task.Delay wirft ArgumentOutOfRangeException bei Werten über ~24,8 Tagen. Ein jährlicher Cron (0 3 1 1 *) erzeugt eine Wartezeit von bis zu 365 Tagen. Der catch fängt nur OperationCanceledException — die Exception beendet den Scheduler-Task still. Da die Tasks in _schedulerTasks nie beobachtet werden (await erst beim Dispose), fällt das nicht auf: der Job läuft ab dann einfach nie wieder.

Fix: Delay auf max. 1 Tag deckeln und in einer Schleife erneut prüfen. Zusätzlich alle Scheduler-Tasks mit einem ContinueWith-Fehler-Logger versehen.

B7 — Cron rechnet in Lokalzeit

CronExpression.cs:51 mit DateTime.Now. Bei Zeitumstellung: im Oktober läuft ein 0 2 * * *-Job doppelt, im März gar nicht.

Fix: Intern in UTC rechnen und nur für die Anzeige konvertieren, oder TimeZoneInfo explizit berücksichtigen.

B8 — Keine Rekursionsbremse bei AgentComm / AgentSpawn

AgentEngine.cs:510

Agent A ruft send_message(B)ChatAsync(B) → B ruft send_message(A) → … Es gibt keine Tiefenbegrenzung und keinen Zyklus-Check. Bei SpawnAgentAsync schützt immerhin der IsRunning-Check, bei send_message nicht. Begrenzt wird das nur durch die Timeouts — in der Zwischenzeit brennt jeder Hop einen vollständigen LLM-Run.

Fix: Aufruf-Tiefe im AgentToolContext mitführen (maxDelegationDepth, Default 3) und den Aufrufpfad zur Zyklenerkennung mitgeben.

B9 — index_Count liest die Datei außerhalb des Locks erneut

FileRWTool.cs:569 ruft index_Count auf, nachdem das Lock freigegeben wurde — Race mit parallelen stock_add-Aufrufen, plus ein überflüssiges vollständiges Parsen der Index-Datei. Der Zähler ist innerhalb des Locks ohnehin bekannt (index.Count).

B10 — PersistChatState schreibt bei jedem Eintrag alles neu

AgentEngine.cs:593

Vollständige History und vollständiger Kontext werden als WriteIndented-JSON bei jedem einzelnen Chat-Eintrag rausgeschrieben → O(n²) Schreiblast über eine Sitzung. Bei einem Agenten mit 500 Nachrichten sind das mehrere MB pro Nachricht.

Fix: History als Append-only JSONL, Kontext nur periodisch/beim Beenden persistieren, WriteIndented = false.

B11 — Kein max_tokens im Request

AgentEngine.cs:105 und Z.279 setzen ChatRequest.MaxTokens nie. Ein Modell kann in einem Schritt sein volles Output-Limit ausschöpfen.

B12 — Kein Retry/Backoff

OpenRouterClient.cs:42 wirft bei jedem Nicht-2xx sofort. Ein einzelnes HTTP 429 killt einen kompletten geplanten Run. Bei OpenRouter sind 429/502/503 im Normalbetrieb zu erwarten.

Fix: Polly o.ä. mit exponentiellem Backoff + Jitter für 429/5xx, Retry-After respektieren.

B14 — Compaction dupliziert den System-Prompt bei kurzen Konversationen ⚠️

ContextCompactor.cs:140

Gefunden durch den Property-Test, nicht beim Lesen des Codes.

var tail = messages.Skip(Math.Max(0, messages.Count - ProtectedTailMessages)).ToList();
messages.Clear();
if (systemMsg is not null) messages.Add(systemMsg);   // ← System-Prompt

messages.AddRange(tail);                              // ← enthält ihn nochmal

Bei messages.Count <= 6 ist Skip(0) — der Tail enthält also alle Nachrichten einschließlich der System-Nachricht. Diese wird danach ein zweites Mal angehängt.

Ergebnis: zwei system-Nachrichten, die zweite mitten in der Konversation. Zusätzlich wird die komplette Konversation dupliziert, obwohl sie gerade zusammengefasst wurde — die Compaction vergrößert den Kontext in diesem Fall, statt ihn zu verkleinern.

Erreichbar, sobald wenige Nachrichten die Token-Schwelle reißen — z.B. ein einzelner WebFetch mit 500 KB Ergebnis (siehe B5). Minimales Gegenbeispiel aus FsCheck: System-Prompt + eine User-Nachricht.

Fix: Den System-Prompt aus dem Tail herausfiltern, bevor er wieder angehängt wird — und den Tail generell erst ab der Position hinter der System-Nachricht bilden.

B13 — Inkonsistente instanceId

RunAsync/ChatAsync bekommen instanceId als Parameter, SendMessageAsync/SpawnAgentAsync verwenden dagegen das Feld _instanceId, das nur gesetzt wird, wenn SetAgentConfigProvider aufgerufen wurde — sonst "".


3. Konzeptfehler (P2)

K1 — Geplante Agenten haben kein Gedächtnis

RunAsync baut bei jedem Cron-Tick eine frische Nachrichtenliste (AgentEngine.cs:89). Ein Agent, der alle 30 Minuten läuft, weiß nichts von seinem letzten Lauf: er ruft dieselben Quellen erneut ab, zieht dieselben Schlüsse und kann keine Entwicklung über Zeit verfolgen.

Das ist gleichzeitig die größte Fähigkeitslücke und eine dauerhafte Token-Verschwendung. Der IStateStore (SQLite) existiert bereits — es fehlt das Konzept darüber.

K2 — Prompt-Injection ist unbehandelt

Agenten lesen untrusted Content (Web, Mail, Telegram, X, YouTube-Transkripte) und besitzen gleichzeitig ausgehende Fähigkeiten (Mail senden, X posten, Dateien schreiben, DB schreiben). Tool-Ergebnisse werden ununterscheidbar von echten Anweisungen in den Kontext gelegt.

Das ist die klassische gefährliche Kombination aus Datenzugriff, untrusted Input und Exfiltrations-Kanal. Ein präparierter Tweet oder eine Mail kann den Agenten steuern.

Ansatz: Tool-Ergebnisse konsequent als Daten rahmen (<untrusted_content source="...">), im System-Prompt verankern, dass daraus keine Anweisungen befolgt werden, und irreversible Aktionen an eine Freigabe koppeln (siehe F-A1).

K3 — Keine Tests

Kein Testprojekt in ClawdDotNet.slnx. Bei einer Engine mit Zustandsmaschine, Compaction, Scheduling und Nebenläufigkeit ist das riskant — die Bugs B1 und B2 wären mit Unit-Tests sofort aufgefallen.

Ansatz: ClawdDotNet.Core.Tests mit einem Fake-OpenRouterClient (Interface extrahieren). Erste Kandidaten: CronExpression, ContextCompactor (Paarungs-Invarianten!), LoopGuard, FileRWTool-Pfadprüfungen.

K4 — Kein Streaming

request.Stream = false fest verdrahtet (OpenRouterClient.cs:34). Bei langen Antworten wirkt die UI eingefroren; es gibt nur den Typing-Indicator.

K5 — Kein Kostenlimit

Es gibt kein hartes Budget pro Agent/Instanz/Tag. Ein Agent in einer Schleife (siehe B8) kann unbeaufsichtigt Guthaben verbrennen. Die Credits-Anzeige ist rein informativ.

K6 — Chat-Historie wächst unbegrenzt

_chatHistories und die zugehörigen JSON-Dateien werden nie rotiert oder archiviert.


4. Token-Optimierung

Das ist der Bereich mit dem größten unmittelbaren Hebel. Nach Wirkung sortiert:

T1 — Prompt-Caching einführen (Einsparung: 6090 % der Input-Tokens) ★★★

Der wichtigste Punkt. Aktuell wird bei jedem Schritt der gesamte Prompt neu gesendet und voll berechnet — inklusive System-Prompt (Identity + Soul + SystemPrompt) und aller Tool-Definitionen. Das ist ein stabiler Präfix, der sich innerhalb eines Runs nie ändert.

Anthropic-Modelle über OpenRouter unterstützen cache_control-Breakpoints: gecachte Input-Tokens kosten nur ~10 % des Normalpreises (Schreiben in den Cache kostet einmalig ~125 %).

Rechenbeispiel — 10-Schritt-Run, Präfix 15k Tokens (System + 10 Tool-Definitionen):

Input-Tokens (Präfix) relative Kosten
heute 10 × 15.000 = 150.000 100 %
mit Caching 15.000 (×1,25) + 9 × 15.000 (×0,1) ≈ 32.250 effektiv ~21 %

Umsetzung:

  1. ChatMessage.Content muss das Array-Format unterstützen ([{ "type": "text", "text": "...", "cache_control": { "type": "ephemeral" } }]). Aktuell ist es ein reiner string (ChatMessage.cs:12).
  2. Cache-Breakpoint ans Ende des System-Prompts und ans Ende der Tool-Definitionen setzen.
  3. Optional einen dritten Breakpoint nach der letzten stabilen Konversationsgrenze (rollierend).
  4. Usage um prompt_tokens_details.cached_tokens erweitern, damit der Effekt messbar wird.

Wichtig: Die Reihenfolge der Nachrichten vor dem Breakpoint muss byte-identisch bleiben — d.h. keine Zeitstempel oder wechselnden Hinweise im System-Prompt.

T2 — Tool-Ergebnisse beim Einfügen kappen (Einsparung: stark schwankend, bis 90 %) ★★★

Siehe B5. Ein einziger ungekappter WebFetch kann mehr Tokens erzeugen als der gesamte restliche Run. Zentral in ExecuteToolCallAsync lösen, nicht pro Tool.

Sinnvolle Defaults: 4.000 Tokens pro Tool-Ergebnis, mit Kürzungshinweis und der Möglichkeit, gezielt weiterzulesen (Offset-Parameter bei FileRW.read, LIMIT/OFFSET bei Database.query).

T3 — Günstiges Modell für die Compaction (Einsparung: ~95 % der Compaction-Kosten) ★★

ContextCompactor.cs:114 nutzt model — also das teure Modell des Agenten — um bis zu 30k Zeichen zusammenzufassen. Bei Opus kostet eine einzige Compaction so mehr als der halbe Run.

Fix: summaryModel konfigurierbar machen, Default z.B. google/gemini-2.5-flash oder anthropic/claude-haiku-4.5. Zusammenfassen ist eine anspruchslose Aufgabe.

T4 — Proaktiv statt reaktiv kompaktieren ★★

CompactIfNeededAsync läuft nach dem API-Call (AgentEngine.cs:123) und nutzt die promptTokens der gerade bezahlten Anfrage. Der überfüllte Prompt wurde also bereits berechnet.

Fix: Vor dem Senden prüfen (mit EstimateTokens, das es schon gibt) und erst dann den Request bauen. Die gemessenen promptTokens dienen zur Kalibrierung der Schätzung.

T5 — maxTokens-Semantik korrigieren ★★

Siehe B3. Aktuell führt der Fehler dazu, dass Läufe abbrechen und neu gestartet werden — was die Tokenkosten verdoppelt statt sie zu begrenzen.

T6 — Tool-Definitionen verschlanken ★

Die Beschreibungen sind großzügig: FileRW allein hat ~500 Zeichen Description plus ein Schema mit 11 Properties ≈ 900 Tokens (FileRWTool.cs:26). Bei 8 zugewiesenen Tools sind das schnell 46k Tokens — bei jedem Schritt (ohne Caching).

Maßnahmen:

  • Beschreibungen auf das Nötige kürzen, Beispiele in den System-Prompt statt ins Schema.
  • stock_add aus FileRW herauslösen — es ist ein fachfremdes Feature, das das Schema aufbläht, auch für Agenten, die es nie nutzen.
  • Für Agenten mit vielen Tools: zweistufiges Laden (list_toolsetsload_toolset).

T7 — RunAsync statt ChatAsync für Job-Wakeups prüfen ★

ToolJobScheduler.cs:186 entscheidet über UseChatContext. Mit true wird die komplette (potenziell riesige) Chat-Historie in einen "Prüfe neue Mails"-Tick gezogen. Für zustandslose Ticks ist RunAsync um Größenordnungen günstiger — der Default sollte bewusst gesetzt und im UI erklärt sein.

T8 — max_tokens setzen ★

Siehe B11. Verhindert Ausreißer bei der Ausgabe (die teuerste Token-Art).

T9 — Caching-Wirkung messbar machen ★

Ohne cached_tokens im Usage-Modell lässt sich der Erfolg von T1 nicht belegen. Sollte zusammen mit T1 kommen und im Token-Dashboard auftauchen.

Erwartete Gesamtwirkung

T1 + T2 + T3 + T4 zusammen sollten die Kosten eines typischen Multi-Step-Runs auf etwa ein Viertel bis ein Fünftel senken — ohne Qualitätsverlust, da nur Redundanz und Ballast entfernt werden.


5. Vorschläge für Erweiterungen

Neue Tools

Tool Nutzen
Memory Strukturiertes Langzeitgedächtnis (Fakten, Entscheidungen, Beobachtungen mit Zeitstempel) auf dem vorhandenen SQLite-Store. Löst K1 und spart massiv Tokens, weil Wissen nicht neu hergeleitet wird. Höchste Priorität.
WebSearch Aktuell können Agenten nur bekannte Domains abrufen (WebFetch), aber nichts finden. Brave/Tavily/SearXNG-Anbindung.
Http Generisches REST-Tool mit Allowlist pro Agent. DirectAPI ist fest auf Finanz-Provider verdrahtet — jede neue API erfordert heute Code.
Shell Sandboxed, mit Kommando-Allowlist. Ersetzt die hartcodierten yt-dlp/ffmpeg-Aufrufe (und behebt S2 gleich mit).
Git Für den "Senior Developer"-Agenten — jetzt, wo das Projekt in Git liegt.
Vision Charts/Screenshots analysieren — für das Trading-Team naheliegend.
Notify Einheitliche Benachrichtigung (Telegram existiert; ntfy/Discord/Push ergänzen).

Neue Konzepte

F-A1 — Freigabe-Workflow (Human-in-the-Loop) Irreversible Aktionen (Mail senden, X posten, DB-Schreibzugriff, Datei löschen) landen in einer Freigabe-Queue im Hauptfenster, statt sofort ausgeführt zu werden. Pro Tool/Aktion konfigurierbar: auto | approve | deny. Das entschärft K2 und S1/S2 erheblich.

F-A2 — Audit-Log Jeder Tool-Aufruf mit Agent, Zeitstempel, Argumenten, Ergebnis-Status in eine eigene SQLite-Tabelle. Aktuell existiert nur LogDebug — nach einem Zwischenfall ist nicht rekonstruierbar, was ein Agent getan hat.

F-A3 — Budget-Guard Hartes USD-Limit pro Agent und pro Instanz und Tag. Bei Überschreitung wird der Agent pausiert und im UI markiert. Setzt B4 (korrekte Kostenerfassung) voraus.

F-A4 — Run-Historie AgentRunResult wird aktuell nur im Speicher als "letztes Ergebnis" gehalten (AgentScheduler.cs:14). Persistierte Runs (mit Schritten, Tokens, Kosten, Fehlern) wären die Grundlage für Diagnose und Kostenanalyse.

F-A5 — Task-Queue statt rekursivem send_message Agent-zu-Agent-Kommunikation läuft heute als synchrone Rekursion (B8). Eine echte Aufgaben-Queue mit Zustand (pending/running/done) wäre robuster, nachvollziehbar und würde Zyklen strukturell verhindern.

F-A6 — Secret-Store DPAPI-verschlüsselte Secrets (S7), mit UI zum Setzen/Rotieren.

F-A7 — Testprojekt Siehe K3.


6. Vorgeschlagene Reihenfolge

Sofort — es blockiert oder gefährdet den Betrieb

  1. B1 Compaction-Paarung (bricht produktiv ab) behoben
  2. B3 maxTokens-Semantik (bricht produktiv ab) behoben 2b. B14 System-Prompt-Duplikat behoben
  3. B2 Race Condition im Chat-Kontext behoben
  4. S2 yt-dlp-Injection
  5. S3 API-Key-Leak

Kurzfristig — größter Nutzen pro Aufwand 6. T1 Prompt-Caching umgesetzt (inkl. T9 cached_tokens) 7. T2 Tool-Ergebnisse kappen (= B5) umgesetzt 8. T3 Günstiges Compaction-Modell umgesetzt 9. B4 Kostenerfassung korrigieren teilweise: Prompt/Completion werden jetzt getrennt erfasst statt 50/50 geschätzt. Offen bleibt die veraltete, hartcodierte Preistabelle (ModelPricing) — Preise sollten vom /models-Endpoint kommen. 10. B12 Retry/Backoff 11. T4 Proaktiv statt reaktiv kompaktieren

Mittelfristig — Fundament 11. S1 DatabaseTool absichern 12. Memory-Tool (K1) 13. F-A1 Freigabe-Workflow + F-A2 Audit-Log 14. K3 Testprojekt 15. S4 PermissionGate ausbauen

Danach 16. Restliche Bugs (B6B10, B13), S5S7, weitere Tools, Streaming