--- source_sha: "b373de64c03d" --- # Governance { #governance } Budgets, Freigaben, Alerts und der Audit-Trail. Die vier Dinge, die eine Agent-Plattform zu etwas machen, hinter das Sie eine Kreditkarte legen können. Sie alle gelten auf jeder Oberfläche gleich, weil jede Oberfläche durch einen einzigen Runner läuft. Die jüngste dieser Oberflächen ist ein **Trigger**: ein Agent, der sich selbst nach einem Zeitplan oder auf ein eingehendes Ereignis hin ausführt — ein GitHub-Issue, eine eingehende E-Mail (siehe [Konzepte](concepts.md#trigger)). Er nutzt dieselbe Kostenerfassung, Bestätigungswarteschlange und Prüfungsspur. Die [Richtlinie für unbeaufsichtigte Aktionen](#routine-action-consent) verschärft, welche Aufrufe eine Bestätigung brauchen; ausdrückliche dauerhafte Zustimmung benennt einzelne Aktionen und eine Version. Unbeaufsichtigte Arbeit bestimmt auch den Umgang mit einer **Ablehnung**: - Ein ausgelöster Run, den das Budget stoppt, endet mit `budget_exceeded` und **wartet auf das nächste Auslösen**, statt wiederholt zu werden. - Ein ausgelöster Run, den sein Ersteller nicht mehr machen darf — er hat die Organisation verlassen oder seinen Grant auf den Agent verloren — **deaktiviert den Trigger** und schreibt einen Audit-Eintrag, der sagt, warum. - Ein ausgelöster Run, der am Modell selbst scheitert — ein Ausfall beim Provider, ein widerrufener Schlüssel — wird als `failed` festgehalten und dem nächsten Auslösen überlassen, statt den Heartbeat dazu zu bringen, dieselbe Wand erneut anzulaufen. Das `last_run_id` des Triggers zeigt weiterhin darauf, damit seine Historie ehrlich bleibt. Eine Ablehnung, die ein Heartbeat jede Minute wiederholt, wäre eine Rechnung oder ein Alert, die niemals aufhören. !!! warning "Nicht geschluckt wird ein Fehlschlag, der nie zu einem Datensatz wurde" Wenn der Run einen Endzustand erreicht hat, der Schreibvorgang, der ihn festhält, aber nicht committet wurde — ein Transkript oder ein Schreiben des Conversation-State, das eine Exception auslöste, nachdem die Antwort vorlag —, **lässt das Auslösen den Flow scheitern**, statt Erfolg zu melden. Der halb geschriebene Run wird zurückgerollt, statt als vollständig committet zu werden, ohne dass etwas dahintersteht. Ein Event-Trigger fügt an seinem Rand eine weitere Ablehnung hinzu: ein Webhook, dessen Signatur sich nicht gegen das eigene Secret des Triggers verifizieren lässt, ist ein 403, der den Runner überhaupt nie erreicht. ## Zustellung von Routinen { #routine-delivery } Eine im privaten Chat über das Plattform-MCP erstellte Routine antwortet in diesem Chat, sofern Sie kein anderes Ziel wählen. Ihre Karte zeigt den aktuellen Zustand, die nächste Aktivierung sowie die vorhandenen Aktionen zum Bearbeiten, Pausieren und Entfernen. Das Ausführungsprotokoll bleibt unter Routines verfügbar. Vor dem Speichern einer fertigen Antwort prüft die Plattform erneut die aktive Mitgliedschaft der erstellenden Person und ihr aktuelles Schreibrecht im gewählten Chat. Ein gelöschtes oder unzugängliches Ziel lässt die Antwort im Ausführungsprotokoll und zeigt **Letzte Antwort im Chat nicht verfügbar** an der Routine. Ein Datenbankfehler während der Nachrichtenzustellung wird isoliert, sodass der abgeschlossene Lauf weder rückgängig gemacht noch der Agent erneut ausgeführt wird. Abschlussmeldungen verlinken den Chat erst nach erfolgreicher Zustellung. `last_delivery_status` unterscheidet Zustellung, nur Protokoll, keine Antwort und einen unvollständigen Lauf; historische Einträge bleiben bis zum nächsten Lauf unbekannt. Die Migration `0265_trigger_delivery_status.py` ergänzt diesen Status, ohne frühere Erfolge zu erraten. Setzen Sie `ends_at` mit UTC-Offset für zeitlich begrenzte Aufgaben und `monthly_budget_usd` für ein engeres Monatslimit. Das MCP-Aktualisierungstool kann beide Einstellungen entfernen. Ein gewöhnlicher API-Schlüssel kann keine Routine zur Nutzung der persönlichen Konten seines Ausstellers ermächtigen. Hintergrundläufe, Identitätsübernahmen und fortgesetzte Gruppen- oder öffentliche Runden erteilen ebenfalls keine solche Zustimmung. Die minütlichen Durchläufe erledigen abgelaufene Endtermine unabhängig von der nächsten Aktivierung, auch bei pausierten Routinen und Beobachtungen. Sie deaktivieren aktive Arbeit, schließen offene Blockaden der Routine und stornieren deren ausstehende Benachrichtigungen ohne Modellaufruf. Nach einem Neustart holt der Worker dies nach; die Bereinigung kann auf einen Worker oder einen weiteren Stapel warten, während Prüfungen vor Werkzeugen und Antwortzustellung abgelaufene Arbeit sofort ablehnen. Die Migration `0269_routine_end_cleanup.py` ergänzt eine Markierung des erledigten Endtermins und partielle Indizes für ausstehende Enden. Sie erstellt keine Ersatzroutinen, Freigaben oder Funde. Eine Änderung des Endes aktiviert die Bereinigung erneut; ein wiederholter Durchlauf wiederholt keine erledigten Effekte. Ein Downgrade entfernt nur diese Hilfsdaten, nicht das konfigurierte Ende oder die Identität der Routine. Eine beobachtete MCP-Verbindung speichert auch ihre Autorisierungsgeneration. Eine abgeschlossene erneute OAuth-Zustimmung oder ein ausdrücklich ersetztes Token beziehungsweise eine geänderte Adresse macht die bisherige Quellenzustimmung ungültig, selbst wenn die Verbindungs-ID gleich bleibt. Normale Token-Erneuerung, Bezeichnungen und Änderungen des Standardkontos ersetzen diese Identität nicht. Eine erneut geprüfte Quelle gilt für spätere Läufe; ein alter angehaltener Lauf kann die neue Zustimmung nicht übernehmen. Prüfe nach der Migration `0270_mcp_authorization_generation.py` die Quellen bestehender Beobachtungen ausdrücklich erneut (`review_sources=true` oder **Angehängte Quellen erneut freigeben** im Editor). Alte Richtlinien bleiben lesbar, erraten aber nicht das Konto hinter einer historischen Autorisierung. Für ein Konto außerhalb der zugelassenen Generation werden keine Anbieter-Zugangsdaten gelesen. Leere alte Worker und aktualisiere API und Worker gemeinsam. Pausiere Beobachtungen vor dem Downgrade; setze sie erst mit generationsprüfendem Code und geprüften Quellen fort. Ersetzte Zugangsdaten leeren die Werkzeugliste des alten Kontos bis zur nächsten erfolgreichen Prüfung. Routinenläufe behalten die für diese Ausführung zugelassenen MCP-Konten und Werkzeugaliase. Fortgesetzte Aufrufe verwenden dieselben Aliase: Umbenennen oder ein anderes Standardkonto leiten eine bestehende Freigabe nicht um. Delegierte bleiben innerhalb der zugelassenen Konten. Vor Aufrufen werden Kontoinhaber, Adresse, Autorisierungsgeneration und Werkzeuglisten erneut geprüft; neue oder geänderte Konten werden vor dem Lesen der Zugangsdaten ausgefiltert. Eine angehaltene Routine ohne diesen Zulassungsstand kann MCP-Aufrufe verwalteter Konten nicht fortsetzen. Starte nach Prüfung des Zugriffs einen neuen Lauf. Die Vorgaben interaktiver Agenten und ausdrückliche Bestätigungsrichtlinien bleiben unverändert; kontospezifische Freigabeausnahmen für Routinen sind von dieser Quellenprüfung getrennt. Die Versionsauswahl folgt entweder der standardmäßig veröffentlichten Version oder legt eine unveränderliche Version fest. Eine Veröffentlichung verschiebt diese Festlegung nicht. Entfernen Sie sie, um wieder der Standardversion zu folgen. Eine benannte Umgebung und eine festgelegte Version schließen sich aus; aktuelle Zugriffsrechte, Budgets und Freigaben gelten weiterhin. Im Routineformular können Sie das Ende festlegen oder entfernen. Das Datum verwendet die am Feld angezeigte Gerätezeitzone, unabhängig von der Cron-Zeitzone, und wird mit UTC-Offset an die API gesendet. Änderungen anderer Felder lassen das bestehende Ende unverändert. Eine Routine prüft vor jedem Werkzeug ihren Ersteller, aktive Mitgliedschaft, Rolle und Ausführungsrechte erneut, auch bei Delegationen und nach einer Freigabe. Pausieren, Löschen, Ablauf, Widerruf persönlicher Zustimmung oder Änderungen dieser Berechtigung verhindern weitere Werkzeugaufrufe. Bereits an externe Dienste gesendete Effekte werden dadurch nicht zurückgerufen. Migration `0260_routine_run_origin` bewahrt den Ursprung beim Löschen; ältere geparkte Zeitpläne mit verlorenem Ursprung werden abgelehnt, statt die Rechte der freigebenden Person zu übernehmen. ## Budgets { #budgets } !!! abstract "Drei Ebenen, und sie sind keine Varianten einer Zahl" Das Cap eines Agents, gemessen an der Gesamtsumme der Organisation, wird von den Runs seiner Nachbarn aufgebraucht; das der Organisation, gemessen an einem einzelnen Agent, ist überhaupt keine Obergrenze. Siehe [warum sie sich nicht zusammenlegen lassen](#why-they-cannot-be-collapsed). | Ebene | Gesetzt in | Misst | Angehoben von | | --- | --- | --- | --- | | **Agent, monatlich** | dem Spec des Agents | den eigenen Runs dieses Agents | wer den Agent bearbeiten darf | | **Abteilung, monatlich** | den Einstellungen der Abteilung | den Runs, die ihre Mitglieder gestartet haben, auf jedem Agent | wer `members:manage` und `budgets:manage` hält | | **Organisation, monatlich** | den Einstellungen der Organisation | jedem Run _und_ jeder Ingestion in der Organisation | wer `budgets:manage` hält | Eine **neue Organisation startet mit bereits gesetzter Obergrenze für die Organisation** — dem `DEFAULT_ORG_MONTHLY_BUDGET_USD` des Deployments ([Konfiguration](configuration.md), ab Werk 100 $) —, damit ein frischer Tenant nicht nur einen entlaufenen Agent von einer überraschenden Rechnung entfernt ist. Ab diesem Punkt ist es ein gewöhnliches Cap: auf der Zeile der Organisation bearbeitbar und genau wie ein von Hand gesetztes durchgesetzt. Ein Deployment, das lieber ohne Cap startet, lässt diese Einstellung leer; so oder so wird das Cap einer bestehenden Organisation dafür nie geändert. ### Warum sie sich nicht zusammenlegen lassen { #why-they-cannot-be-collapsed } Das waren sie einmal, mit `min()`, und das Ergebnis war falsch. Das Cap eines Agents, gemessen an der Gesamtsumme der Organisation, wird von den Runs seiner Nachbarn aufgebraucht, und genau das macht es zu keinem Cap. Das Cap einer Organisation, gemessen an den Ausgaben eines einzelnen Agents, würde nie binden. Also misst jedes Cap seine eigene Größe, und die Abfrage reist mit dem Limit. Ein Agent mit 5 $ unter einer Obergrenze von 50 $ bindet jetzt, wenn _er_ 5 $ ausgegeben hat, und die Ablehnung nennt das Cap, das tatsächlich gebunden hat, statt es daraus abzuleiten, welche der beiden Zahlen kleiner war. Ein Agent kann die Obergrenze der Organisation weiterhin nicht lockern: Der Eintrag der Organisation steht mit seiner eigenen Zahl da, was auch immer der Spec verlangt, und die Ausgaben eines Agents sind Teil derer der Organisation — ein Agent mit 100 $ unter einer Organisation mit 10 $ wird also bei 10 $ gestoppt. Beide Caps sind dort lesbar, wo ihre Ausgaben stehen: das der Organisation auf ihrer eigenen Zeile (`GET /orgs/{org_id}`) und das jedes Agents als `budget_monthly_usd` in der Agent-Liste — die Zahl der _veröffentlichten_ Version, denn das ist die, die der Runner durchsetzt, und nicht das, was der Draft gerade verspricht. Die Headroom-Karte des Dashboards verbindet diese mit `GET /spend`, sodass ein Cap im Anmarsch zu sehen ist, bevor `budget_exceeded` in der Run-Historie auftaucht. ### Das Cap einer Abteilung { #a-departments-cap } Eine [Abteilung](departments.md#a-departments-budget) kann ein eigenes monatliches Cap tragen. Es liegt zwischen den anderen beiden: Ein Run wird gegen das Cap des Agents geprüft, dann gegen jede Abteilung mit Cap, in der die Person ist, die ihn gestartet hat, dann gegen das der Organisation. Jede Abteilung misst den Monat ihrer eigenen Mitglieder, sodass eine Person in Finanzen und Vertrieb von derjenigen gestoppt wird, die zuerst ausgeschöpft ist, und die Ablehnung nennt sie - _Sales department monthly budget exhausted_. Die Leitung der Abteilung und die Administratoren werden einmal benachrichtigt, wenn sie 80 % ihres Monats überschreitet, und erneut, wenn ihr Cap einen Run stoppt. Ein Run, den niemand gestartet hat - ein Zeitplan, ein Besucher in einem Kanal - gehört zu keiner Abteilung und wird nicht gezählt. ### Durchgesetzt wird vor der Anfrage { #enforcement-is-before-the-request } !!! danger "Vorher, nicht nachher" Hinterher zu prüfen heißt, dass die Anfrage, die das Budget gesprengt hat, bereits bezahlt war — und eine Schleife kann jedes Mal um einen teuren Aufruf überschießen. !!! important "Ein fehlgeschlagener Run hält trotzdem fest, was er ausgegeben hat" Ein Budget, das Fehlschläge ignoriert, ist kein Budget. Die Abrechnung geschieht auf jeder Oberfläche in einem `finally`-Block, und der Commit ist explizit, statt dem Session-Kontext überlassen zu werden — der bei jeder Exception zurückrollt und bei einer Cancellation überhaupt nie erreicht wird. !!! important "Eine Baseline ist das, was _andere_ Runs ausgegeben haben" Beide Abfragen lassen die eigene Zeile des fragenden Runs aus. Ein fortgesetzter Run behält seine Zeile, und bis dahin hat `finish_run` committet, was er ausgegeben hat — während das Ledger mit derselben Zahl neu befüllt wird, was auch nötig ist, sonst würde der Abschluss der Fortsetzung die Kosten mit dem überschreiben, was allein die Fortsetzung gekostet hat. An beiden Stellen gezählt, kam ein bei 10 $ gedeckelter Agent, der 6 $ ausgegeben hatte und dann parkte, bei seiner ersten Modellanfrage auf `6 + 6 = 12` und wurde mit 4 $ Luft abgelehnt — und teilte seinem Besitzer mit, er habe ein Cap erreicht, bei dem er zu 60 % stand. Das Cap der Organisation zählte identisch doppelt. Der Guard ist ein harter Stopp für einen Run, der die Ausgaben _sieht_, und die eigenen Kosten eines Runs landen erst auf seiner Zeile, wenn er fertig ist. Die Baseline, die ein Run liest, ist also die Summe der Runs, die bereits fertig sind, und **gleichzeitige Runs sind füreinander unsichtbar**. Fünfzig Runs, die gemeinsam gegen eine Organisation starten, der ein Aufruf bis zu ihrem Cap fehlt, lesen alle dieselbe Baseline unterhalb des Caps, laufen alle weiter und überschießen um bis zu ihre gemeinsamen Kosten. Das ist eine Eigenschaft eines Aggregats ohne eine einzelne Zeile zum Sperren — anders als das Überschießen pro Run und pro Schleife oben, das die Prüfung vor der Anfrage sehr wohl begrenzt. Deshalb ist das Cap eine Obergrenze auf **committete** Ausgaben und kein Gate, das gleichzeitige Runs serialisiert. Ein Deployment, das ein striktes Cap braucht, lässt seine Agents durch eine einzige Queue laufen statt parallel. ### Evals zahlen aus denselben Budgets { #evals-spend-from-the-same-budgets } Ein [Eval-Run](evals.md) besteht aus gewöhnlichen Runs. Jeder Fall wird daher vor jeder Modellanfrage gegen die Limits des Agents und der Organisation geprüft, und was er ausgibt, zählt gegen beide - ein eigenes Eval-Budget gibt es nicht. Ein großes Dataset kann deshalb das Monatsbudget aufbrauchen, das der Produktionsverkehr braucht. Der Start eines Runs zeigt, was jedes Limit nach der geschätzten Ausgabe behält, und wird abgelehnt, bevor etwas läuft, wenn die Schätzung höher ist als das, was einem der beiden bleibt; siehe [Das verbleibende Budget](evals.md#the-budget-it-leaves). ### Ein Run kostet mehr als seine Modellanfragen { #a-run-costs-more-than-its-model-requests } Eine Wissenssuche bettet die Frage ein, bevor sie danach suchen kann, und dieses Embedding wird dem Run berechnet, der es angefordert hat. Der Embedding-Dienst ist prozessweit — er bedient jeden Run und jeden Ingestion-Job zugleich — und bucht deshalb gegen den Run, der _gerade gemessen wird_, statt ein Budget als Argument entgegenzunehmen. Was das Messen zu etwas macht, das eine Oberfläche vergessen kann, und das Vergessen ist lautlos: keine Exception, keine Warnung, nur ein Run, der weniger meldet, als er ausgegeben hat, und ein Monat der Organisation, der es nie sieht. Also gehört der Zähler zum vorbereiteten Run und nicht zur Oberfläche. Einen zu öffnen ist kein Schritt, von dem eine neue Oberfläche wissen muss, denn es gibt keinen Weg, einen vorbereiteten Agent ohne ihn auszuführen. Die Capability [Lange Gespräche](reference/capabilities.md#context-management) ist das andere. Ihre zusammenfassende Strategie schreibt die Zusammenfassung über einen Agent, den sie selbst baut, sodass diese Anfrage an keinem Budget-Guard vorbeikommt; die Capability bucht ihre Kosten gegen denselben Zähler. _Außerhalb_ des Guards zu liegen hat eine Konsequenz, die man kennen sollte: Die Ausgabe wird festgehalten statt abgelehnt, sodass eine Kompaktierung, die ein Cap überschreitet, den Run bei der Anfrage danach stoppt. ### Kosten, die sich nicht messen ließen, sagen das { #a-cost-that-could-not-be-measured-says-so } Die von Stav berechneten Kosten haben Vorrang vor dem Katalog: `usage.cost` mit `cost_currency` oder die Header `x-stav-cost` und `x-stav-cost-currency`. Das gilt für Chat Completions und Responses, auch beim Streaming. USD wird direkt verwendet; NOK erfordert `STAV_NOK_PER_USD` ([Konfiguration](configuration.md)), vom Betreiber gepflegt. Der Wert gibt NOK pro Dollar an: `10` würde NOK durch 10 teilen und ist keine Empfehlung für den aktuellen Kurs. Es wird kein Kurs automatisch abgerufen. Usage bewahrt Betrag, Währung, Kurs und USD-Kosten; das Ledger markiert die Quelle als vom Anbieter gemeldet. Ungültige Beträge, nicht unterstützte Währungen oder fehlende Umrechnung lassen den Katalog-Fallback bestehen. Ohne verwendbare vom Anbieter gemeldete Kosten gilt: `genai-prices` kennt nicht jedes Modell. Trifft ein Run auf eines, für das es keinen Eintrag hat, wird diese Anfrage mit null gebucht und der Run als `cost_is_partial` markiert — die Summe ist um genau das zu niedrig, was diese Anfragen gekostet haben, und die ehrliche Lesart davon ist eine **Untergrenze**. Dieses Flag reist inzwischen den ganzen Weg nach unten. Es steht auf der Run-Zeile, auf der Message-Zeile, die ein Turn schreibt, und auf der Summe, die eine Unterhaltung meldet; jede Oberfläche, die Geld zeichnet, setzt ein `≥` davor statt einer Zahl, die sich als exakt liest. Null auf einer Message, die geschrieben wurde, bevor es die Spalte gab, bedeutet _nicht erfasst_, was nicht dieselbe Aussage ist wie „exakt" — ein Client markiert nur, was er weiß. **Jede Oberfläche hält es jetzt fest, und jeder Turn hält seinen eigenen Anteil fest.** Eine Message, die von einem Channel, der API oder dem Widget geschrieben wurde, trug bis vor Kurzem gar keine Kosten, sodass sich ein Slack-Thread nicht aufsummieren ließ. Die geschriebene Zahl ist die _Differenz_ zu dem, was die früheren Turns dieses Runs bereits beanspruchen, nicht die Zahl der Run-Zeile: Eine Run-Zeile ist kumulativ, und ein Run, der parkte und fortgesetzt wurde, schreibt zwei Assistenten-Turns — beide mit der Zeile zu stempeln würde die geparkte Hälfte doppelt zählen. Die Messages eines Runs summieren sich daher auf genau das, was der Run als seine Ausgabe angibt. **Ein Turn, bei dem der Run mittendrin gestoppt wurde, sagt das.** Ein abgebrochener Run hinterlässt, was der Agent geschrieben hatte, als der Socket schloss oder `stop` gedrückt wurde, und das liest sich genau wie eine fertige Antwort — ein Leser nimmt eine abgeschnittene also für alles, was der Agent zu sagen hatte, und das ausgegebene Geld sieht aus, als hätte es das gekauft. Das Transkript trägt den Status des Runs pro Turn, und der Chat markiert ihn. ### Wie voll das Context-Window ist { #how-full-the-context-window-is } Die dritte Obergrenze, und die, die niemand kommen sieht. Ein Budget lehnt mit einer Nachricht ab, mit der jemand etwas anfangen kann. Ein Workspace lehnt einen Schreibvorgang ab. Ein **Context-Window** wird vom Provider abgelehnt, mitten in der Antwort, und der Run scheitert einfach. Jeder Agent trägt deshalb eine Anzeige — nicht nur einer mit gebundener Capability [Lange Gespräche](reference/capabilities.md#context-management), denn die Warnung zählt am meisten für den Agent, der _nicht_ kompaktieren wird. Sie meldet, wie viele Token die letzte Anfrage eines Turns getragen hat, _nach_ jeder Kompaktierung: Der Wert fällt, wenn die Kompaktierung wirkt, weil er misst, was hinausging, und nicht, was die Unterhaltung hält. Die Zahl ist das `input_tokens` des Providers selbst, keine Schätzung der Historie. Eine Zeichenzählung kann die Tool-Definitionen nicht sehen, und die werden bei jeder Anfrage berechnet — Tausende Token bei einem Agent mit Wissen, einem Sandbox und Delegation, also ein Drittel der echten Zahl, das genau in dem Moment fehlt, in dem die Zahl zählt. **Die Zählung wird auf dem Turn gespeichert; der Anteil nicht.** Wie viel Historie es gibt, überlebt einen Modellwechsel. Welchem Bruchteil eines Windows das entspricht, nicht — und der Chat lässt jemanden zwischen den Turns das Modell wechseln. Eine Historie von 500.000 Token ist die Hälfte eines Modells mit 1M Context und **390 %** eines Modells mit 128K, und das zweite ist eine Anfrage, die der Provider rundheraus ablehnt. Ein mit dem Messwert eingefrorener Anteil würde weiterhin „50 %" lesen. Also wird der Nenner dort aufgelöst, wo die Auswahl bekannt ist, aus dem eigenen erfassten Window des Modellprofils und der Preis-Registry dahinter. Wo keines von beiden etwas sagen kann, wird **überhaupt kein Anteil gezeichnet** — ein Prozentwert gegen ein angenommenes Window ist eine Schätzung, die als Messung auftritt. Dieser Wechsel ist auch das, wofür die Kompaktierung da ist. Ihr Auslöser ist ein **pro Anfrage aufgelöster Bruchteil** gegen das Modell, an das die Anfrage geht, sodass eine Historie, die im alten Window bequem Platz hatte, unter dem neuen gleich beim nächsten Turn kompaktiert wird — bevor die Anfrage hinausgeht, nicht nachdem der Provider sie abgelehnt hat. Ein Agent ohne gebundene Kompaktierung hat stattdessen die Anzeige, und sonst nichts. **Der Auslöser misst, was der Provider gemessen hat.** Er verankert sich an der jüngsten Antwort, die Provider-Usage trägt — die `input_tokens` jener Anfrage zählten die Instruktionen, jedes Tool-Schema und jede vorherige Message — und schätzt nur, was danach kam. Deshalb trägt eine wiederholte Unterhaltung, was jede Antwort gekostet hat: Ohne Anker zählt der Auslöser Zeichen, und ein echter Agent las hier 9 Token, wo der Provider 3.859 berechnet hatte. Die Anzeige sagte 77 %; der Auslöser sah nichts zu tun. **Eine Zusammenfassung wird behalten.** Die Kompaktierung schreibt die Messages eines Runs um; der Faden zwischen den Turns wird aus dem Transkript neu aufgebaut, sodass eine Zusammenfassung früher an der Turn-Grenze weggeworfen wurde und der nächste Turn eine weitere über eine um einen Turn längere Historie kaufte — zwei aufeinanderfolgende Turns einer echten Unterhaltung bezahlten hier je eine Zusammenfassung derselben fünf Messages, und die zweite kündigte an, neun zusammenzufassen. Also wird die kompaktierte Historie in die Unterhaltung geschrieben, samt ihrer Reichweite, und der nächste Turn startet von ihr und spielt nur nach, was seither gesagt wurde. Wer den Faden wieder öffnet, findet dasselbe wie das Modell. Behalten wird nur eine Zusammenfassung. Die ältesten Messages fallen zu lassen und Tool-Ergebnisse zu leeren kostet nichts, um es zu wiederholen, und es aufzuschreiben würde einen Verlust dauerhaft machen, der derzeit bei jedem Turn neu gegen das Window abgewogen wird. Eine Einstellung kann nicht funktionieren und sagt das, statt zu laufen. Wenn allein die Instruktionen und Tool-Schemata über dem Auslöser liegen, kann keine Zusammenfassung darunter kommen — sie stehen nicht in der Historie, die zusammenzufassen wäre. Jede Anfrage würde dann eine Zusammenfassung kaufen, die nichts ändert. Die Kompaktierung wird übersprungen, der Chat sagt warum, und die Behebung ist Sache des Autors: ein größeres Window oder ein höherer Bruchteil. Diese Ablehnung ruht auf einer Zahl, die eine _Antwort_ erzeugt, sodass ein Turn seine eigene nicht messen kann, bevor er entscheiden muss — und ein Chat-Turn ist meist eine Anfrage. Die Unterhaltung trägt den letzten Messwert, und ein Run startet von ihm. Der erste Turn eines Fadens hat daher nichts, woran er sich halten kann, und kompaktiert wie konfiguriert; ab dem zweiten steht die Ablehnung zur Verfügung. Abgelehnt werden nur die Strategien, die etwas kaufen: Die ältesten Messages fallen zu lassen und Tool-Ergebnisse zu leeren ruft kein Modell auf, also laufen sie unabhängig vom Window. ### Delegation gibt das Budget des Parents aus { #delegation-spends-the-parents-budget } Ein Run kann die ganze Unterhaltung eines anderen Agents enthalten — siehe [Delegate vs. Inline-Spezialist](concepts.md#delegate-vs-inline-specialist). Ein Run hat **ein Spend-Ledger**, und jeder Delegate schreibt hinein. Das ist es, was das Cap des Parents die Ausgaben einer Delegation vor seiner nächsten Modellanfrage sehen lässt, genau in dem Moment, in dem Delegation vervielfacht, was ein Turn kosten kann. Jeder Eintrag ist mit der Delegation gestempelt, die ihn gebucht hat, und so beantwortet ein einziges Ledger trotzdem „was hat _dieser_ Delegate gekostet" — siehe unten. Daraus folgt, dass **die Caps, die innerhalb einer Delegation binden, die des Parents sind**. Das eigene `budget.monthly_usd` eines Delegates wird mitten im Run des Parents nicht durchgesetzt: Zwei Guards, die ein Ledger messen, würden jede Anfrage doppelt zählen, und die Obergrenze, auf die es ankommt, ist die des Runs, den jemand gestartet hat. Das eigene Cap des Delegates gilt weiterhin für Runs _des Delegates selbst_. Jeder Delegate bepreist allerdings seine eigenen Anfragen, denn ein Guard bepreist, was er festhält: Ein Delegate auf Anthropic, gemessen durch einen für OpenAI gebauten Guard, würde gegen den falschen Katalog bepreist — lautlos, und meist als unbepreist. Drei weitere Obergrenzen gibt es, weil ein Budget ein schlechtes Mittel ist, um ein Fan-out zu stoppen — es merkt es erst, wenn das Geld weg ist. `max_depth` begrenzt die Verschachtelung, `max_fanout` begrenzt, wie viele Delegationen zugleich laufen, und das eigene `max_steps` jedes Delegates begrenzt seine Schleife. Siehe die [`subagents`-Capability](reference/capabilities.md#delegation). Zwei dieser drei gehören dem Delegate selbst, und das ist die Linie, die das Budget nicht überschreitet: `max_steps` wird vom Spec des Delegates gelesen, und sein `max_depth` deckelt, wie tief _es_ gehen darf, wie viel Raum sein Aufrufer auch übrig hatte. Ein Cap auf Ausgaben ist ein Cap auf den Run, den jemand gestartet hat; ein Cap auf Verschachtelung ist eine Entscheidung, die der Autor des Delegates getroffen hat und seine Prüfer lesen, also kann ein Aufrufer sie nicht aufweiten. ### Wie ein delegierter Run festgehalten wird { #what-a-delegated-run-is-recorded-as } Eine Delegation an einen **veröffentlichten** Agent bekommt eine eigene `agent_runs`-Zeile, die `parent_run_id` und die Task-Id der Delegation trägt. Ein **Inline-Spezialist** bekommt keine: Er hat keinen Agent, dem eine zuzuordnen wäre, also sind seine Kosten die _des Runs_, und der Tool-Aufruf im Transkript ist der Datensatz. Welchem Run allerdings, ist die Frage, die [#228](https://github.com/vstorm-co/agenticos/issues/228) beantwortet hat. - Ein Spezialist direkt unter dem eigenen Agent des Runs wird auf die **oberste Zeile** gebucht, die ohnehin das ganze Ledger ist. - Ein Spezialist unter einem **veröffentlichten Delegate** wird auf die Zeile _dieses Delegates_ gebucht, nicht auf die oberste — der Monat des Delegates enthält also, was sein Spezialist ausgegeben hat, und das ist der einzige Ort, an dem es ehrlich landen könnte. Jeder Ledger-Eintrag trägt daher zwei Zuordnungen: die Delegation, die ihn gemacht hat, für das Panel, und die nächstgelegene Agent-Zeile, auf die er gebucht wird, für den Monat. Die beiden sind für jede Anfrage gleich, die ein veröffentlichter Delegate auf eigene Rechnung stellt, und weichen nur unter einem Inline-Spezialisten voneinander ab — dessen Panel seinen eigenen Anteil behält, während seine Ausgaben die Zeile seines Vorfahren erreichen. !!! important "Die Zeile des Parents ist die Autorität; die Zeile eines Kindes ist ihr Anteil daran" Ein Delegate gibt in das gemeinsame Ledger aus, und **jeder Eintrag in diesem Ledger trägt die Delegation, die ihn gemacht hat**. Die Kosten einer Delegation sind die Summe ihrer eigenen Einträge — der Anfragen, die ihr eigener Agent gestellt hat, einmal bepreist, durch dieselbe Abfrage, die auch die Summe des Runs benutzt. Sie sind in beiden Modi und in jeder Tiefe exakt, und sie hängen nicht davon ab, wann die Delegation zufällig abgerechnet wurde. Genau davon hingen sie einmal ab, und es waren zwei Defekte. Die Zahl war der *Zuwachs* der gemeinsamen Summe über die Delegation hinweg, sodass eine Delegation im Hintergrund — abgerechnet, wenn sie das nächste Mal abgefragt wird, was nach der Antwort des Parents sein kann — alles aufsog, was der Parent zwischenzeitlich ausgegeben hatte: Ein Delegate, der 0,01 $ ausgegeben hatte, wurde mit 0,51 $ festgehalten, wenn der Parent danach 0,50 $ ausgab. Und ein Delegate, der weiter delegiert, hatte die Ausgaben seiner eigenen Delegates in seinem Fenster, die deren Zeilen erneut festhalten, sodass seine Monatssumme seine Enkel mitzählte. Das mit einem Ledger *pro Agent* aufzuteilen bleibt das Design, das zu vermeiden ist — genau das hindert das Cap des Parents daran, überhaupt zu binden. Ein Ledger, zugeordnet, behält beide Eigenschaften: Das Cap des Parents sieht jede Anfrage vor der nächsten, und jede delegierte Zeile sagt, was dieser eine Agent ausgegeben hat, seine eigenen Inline-Spezialisten eingeschlossen und seine veröffentlichten Delegates ausgeschlossen. Die Zeile des Parents bleibt die Autorität für den Run. Ihr `cost_usd` ist das ganze Ledger, Delegates eingeschlossen, und das ist es, was der Organisation berechnet wird; die Child-Zeilen teilen dasselbe Geld nach Agent auf und addieren nie etwas hinzu. `cost_is_partial` gilt ebenfalls je Zeile: Ein Parent auf einem Modell, das `genai-prices` nicht kennt, macht die Summe des Parents zu einer Untergrenze und sagt nichts über einen Delegate, der auf einem bepreisten Modell lief. Das `started_at` und `ended_at` einer delegierten Zeile sind die **eigene** Spanne der Delegation, abgelesen am Task-Handle, das die Bibliothek stempelt, wenn der Delegate startet und wenn er endet — nicht der Moment, in dem die Zeile abgerechnet wurde. Von der Abrechnung gelesen, erschien eine Delegation im Hintergrund als Run ohne Dauer zur falschen Zeit, einsortiert nach Arbeit, die vor ihr fertig war; zwei, die sich tatsächlich überlappten, wurden im selben Augenblick festgehalten, ohne dass etwas das sagte. Ein terminales Handle mit einem Ende, aber ohne Start — ein Delegate, der abgebrochen ist oder scheiterte, bevor er zu laufen begann — hält eine Spanne der Länge null an diesem Ende fest, nie ein Null; und wo die Bibliothek ablehnt, bevor überhaupt ein Handle existiert — eine unbekannte `chat_trace_id` —, wird keine delegierte Zeile geschrieben. Eine Delegation, die auf einer Freigabe geparkt hat, spannt sich über jeden Turn, in dem sie lief: Ihr frühester Start wird über das Parken hinweg mitgeführt, so wie ihre Kosten (siehe unten), sodass die Zeile beginnt, wenn der Delegate zuerst begann, und endet, wenn er endgültig endete — nicht beim Fortsetzen, das sie abgerechnet hat. Die beiden werden nicht so summiert wie die Kosten; die ehrliche Antwort ist der Start des ersten Segments und das Ende des letzten. **Eine Delegation, die auf einer Freigabe geparkt hat, ist mehr als ein Anteil.** Ihre Turns liefen in verschiedenen Prozessen gegen verschiedene Ledger, und das Ledger eines fortgesetzten Turns ist ein frisches Objekt, das nichts von vor dem Parken hält — was die Child-Zeile festhält, ist also jedes Segment zusammen: Der geparkte Zustand behält, was die Delegation beim Stoppen gekostet hatte, und der Turn, der sie beendet, addiert seinen eigenen Anteil. Eine Zeile wird geschrieben, einmal, von dem Turn, in dem die Delegation endet — ein Delegate, der zweimal parkte, hinterlässt drei Segmente und eine Zeile. `cost_is_partial` wird auf dieselbe Weise mitgeführt, und zwar aus einem Grund, den das Geld nicht teilt: Es gilt jetzt je Zeile statt je Run, sodass ein Delegate, der _vor_ der Freigabe eine unbepreiste Anfrage stellte und auf einem bepreisten Modell fortsetzte, sonst auf seiner Zeile exakte Kosten behaupten würde. Das Flag ist wahr, wenn es für irgendein Segment wahr war. Das ist es wert, gesagt zu werden, weil der Fehlschlag, den es ersetzt, unsichtbar war. Die Zeile hielt früher nur das fest, was der Delegate _nach_ dem letzten Fortsetzen ausgegeben hatte, und das ist in der gewöhnlichen Form — erst die Arbeit tun, dann um Erlaubnis bitten, auf das Ergebnis hin zu handeln — die kleine Hälfte. Nichts ging in der Summe daneben, denn das Geld lag die ganze Zeit in der Zeile des Parents; falsch war jede Zahl, die „was hat dieser Delegate gekostet" beantwortet. Die Child-Zeile ist das, was zwei verschiedene Fragen beantwortbar macht, und sie wollen entgegengesetzte Arithmetik: | Die Frage | Child-Zeilen | | --- | --- | | **Was schuldet die Organisation?** | **ausgeschlossen** - die Zeile des Parents enthält diese Token bereits, beides zu zählen stellt der Organisation also eine Anfrage doppelt in Rechnung | | **Was hat _dieser Agent_ diesen Monat gekostet?** | **eingeschlossen** - die Zeilen eines Delegates sind der einzige Ort, an dem seine eigenen Ausgaben festgehalten sind, und jede hält die eigenen Anfragen dieses Agents und die seiner Inline-Spezialisten ([#228](https://github.com/vstorm-co/agenticos/issues/228)), aber nicht die seiner veröffentlichten Delegates, die eigene Zeilen haben | Die zweite ist das, was „der Researcher hat diesen Monat 40 $ gekostet" beantwortbar macht, und sie ist es, worauf ein Nutzungsbericht je Agent oder ein Budget-Alert auf diesen Agent auslöst. Die Monatszahl der Organisation trägt zusätzlich die Ausgaben für Ingestion, die Zahl je Agent nicht: Eine gemeinsame Wissensdatenbank zu indexieren sind niemandes Agent-Ausgaben. Eine fehlgeschlagene Child-Zeile wird ebenfalls an die Regel des Parents gehalten, was `error` sagen darf. Was unter einem Delegate auslöst, ist ein Modell-Client, dessen Nachricht die URL der fehlschlagenden Anfrage tragen kann — samt Schlüssel, auf einem eigenen Endpunkt —, also speichern die Zeile und der Abschlussframe der Delegation denselben kontrollierten Satz wie die Zeile des Parents, und der eigene Text des Providers geht ins Server-Log. Ein Delegate, den sein Usage-Limit oder eine Budget-Obergrenze gestoppt hat, behält die Nachricht des Limits unversehrt: Das ist eine Obergrenze, die ihre Arbeit tut, kein Fehlschlag, der zu diagnostizieren wäre. Die Regel reicht auch bis ins Transkript des _Parents_. Wenn ein Agent im Hintergrund delegiert, fragt er `check_task` und `wait_tasks` nach dem Ausgang ab, und was diese antworten, wird zu einer Tool-Aufruf-Zeile in der Unterhaltung — unversehrt gespeichert, denn eine Tool-Rückgabe ist die eigene Antwort des Tools und nicht etwas, das diese Plattform verfasst hat. Also nennen sie die Klasse der Exception, statt die Nachricht des Providers zu wiederholen, und das ist `subagents-pydantic-ai` 0.2.20 und der Grund, warum die Untergrenze dort liegt. **Die gefensterte Zahl des Dashboards trägt es ebenfalls.** `GET /stats/usage` antwortet mit einem `cost`-Block für den Zeitraum, den der Filter gewählt hat, und dieser Block sind Runs _plus_ Ingestion — dieselbe Arithmetik, mit der das monatliche Cap gemessen wird — mit `model_usd` und `ingestion_usd` daneben, damit ein Leser sehen kann, wohin das Geld ging, ohne zu subtrahieren. Bis 0.0.152 meldete er allein die Modellhälfte, was zwei verschiedene Definitionen von Kosten auf eine Karte brachte: Die Schlagzahl bewegte sich mit dem Zeitraumfilter und zählte Runs, während die Zeile „seit Monatsbeginn" darunter die ganze Rechnung zählte, und nichts sagte, dass sie verschiedene Fragen beantworteten. Auf einem Deployment, das Dokumente indexiert, widersprachen sie sich schlicht. Bei `scope=own` ist die Ingestion-Hälfte null statt ein Anteil: Ein Dokument wird von einem Worker indexiert und `ingestion_spend` hält keinen Nutzer fest, einer Person das Fenster für eine Sammlung zu berechnen, die jemand anderes synchronisiert hat, hieße also, ihre Ausgaben zu erfinden. **Jede Abfrage muss sagen, welche der beiden sie beantwortet**, und die erste Spalte ist die Voreinstellung. Die Zahl seit Monatsbeginn und die Aufschlüsselung je Agent dahinter schließen Child-Zeilen aus, sodass sie sich auf die Summe darüber addieren — und die Nutzungs-E-Mail der Organisation meldet dieselbe Summe statt eine Summe aus einer von ihnen. Nur eine Frage, die _über einen Agent_ gestellt wird, schließt sie ein. Drei dieser fünf Abfragen gingen ohne diese Unterscheidung live und meldeten je 1,40 $ für 1,00 $ Arbeit; kommt eine neue hinzu, ist die Voreinstellung die sichere. #### Die zwei Anbieterfragen brauchen eine dritte Antwort { #the-two-vendor-questions-need-a-third-answer } **Nach Provider** und **nach Schlüssel** können keine der beiden Spalten benutzen, und das falsch zu machen ist auf dem Bildschirm unsichtbar. Child-Zeilen auszuschließen summiert korrekt und ordnet das Geld des Delegates dann dem Anbieter des _Parents_ zu, weil das der Provider auf der summierten Zeile ist: Ein Orchestrator auf OpenAI, der 0,40 $ Arbeit an einen Agent auf Anthropic delegierte, meldete `openai $1.00`und überhaupt keine Anthropic-Zeile. Sie einzuschließen meldete`openai $1.00` + `anthropic $0.40` — mehr als die Rechnung. Diese beiden summieren also die **eigenen** Ausgaben jedes Runs: seine Kosten abzüglich der Kosten seiner direkten Delegationen. `openai $0.60` + `anthropic $0.40`, was zugleich die richtige Zuordnung und die richtige Summe ist. Es verschachtelt sich — einem Delegate, der weiter delegiert hat, werden seine Enkel dadurch herausgerechnet, einmal — und über jede Zeile summiert kommt es immer noch auf die Rechnung, weil die Kosten jedes Kindes von seiner eigenen Zeile addiert und von der seines Parents entfernt werden. Ein Schlüssel funktioniert genauso, und er zählt mehr: Ein Schlüssel ist das, was jemand rotiert, wenn eine Rechnung falsch aussieht. ### Das Budget eines Sprachanrufs { #a-voice-calls-budget } Ein [Sprachanruf](voice.md) ist ein Run, und seine Ausgaben zählen gegen die Budgets des Agents und der Organisation wie jede andere. Drei Dinge sind anders, weil sich ein Anruf nicht vor jeder Anfrage prüfen lässt, wie es bei einem Text-Run geschieht: - **Bevor er beginnt**, wird die längstmögliche Dauer des Anrufs mal der Worst-Case-Minutenpreis des Modells gegen das verbleibende Budget geprüft. Ein Anruf, der sich nicht bezahlen ließe, wird abgelehnt, statt begonnen und abgeschnitten zu werden. - **Während er läuft**, wird jede Antwort beim Abschluss auf den Run gebucht, und die Ausgaben des Monats werden alle 15 Sekunden neu gelesen, sodass andere Runs, die inzwischen ausgeben, gesehen werden. Ein Budget, das mitten im Anruf ausgeht, bekommt einen gesprochenen Satz und beendet den Anruf, festgehalten als `budget_exceeded`. - **Seine Länge ist die des Agents.** Ein Anruf dauert höchstens so lange wie der längste Anruf des Agents, standardmäßig 10 Minuten und höchstens 50; ein eigenes Kontingent an Anrufminuten gibt es nicht, die Budgets sind also die einzige Grenze dessen, was Anrufe ausgeben. Einen Anruf, dessen Prozess gestorben ist, schließt der Voice-Sweep zu den Kosten, die sein letzter Heartbeat festgehalten hat — anders als einen Text-Run, dessen Ausgaben mit seinem Prozess sterben. ### Arbeit aus eigener Initiative { #proactive-work } Nächtliche Vorschlags- und Briefing-Runs werden unter dem veröffentlichten **AgenticOS**-Starter-Agent der Organisation (Slug `agenticos`) aufgezeichnet, damit jeder eine Zeile, Kosten und eine Spur hat; der Starter läuft jedoch nicht und wählt das Modell nicht: Jeder solche Run trägt die [Systemrolle](#system-agents), die die Arbeit erledigt hat. Das eigene Proaktivitätsmodell und die Limits wählen Sie unter **Organisationen → Mitglieder → Proaktivität**. Aktivität zeigt das Symbol und den Filter **Proaktiv**, die Modellanfragen und ihre Kosten sowie den aufgezeichneten Austausch einschließlich Tool-Ergebnissen und der strukturierten Endantwort. Fehlgeschlagene Prüfungen behalten den Austausch, der vor dem Fehler vorlag. Diese Aufzeichnungen erfordern `runs:view`, wie andere Aktivitätseinträge, und erzeugen keine Unterhaltungen im Chat. Ältere Runs können nie aufgezeichnete Austausche nicht wiederherstellen; zu große Aufzeichnungen sagen ausdrücklich, dass sie gekürzt wurden. Das Modellfeld verwendet dieselbe Auswahl für Anbieter, Modell und Vault-Schlüssel wie der Agent Builder. Mit `connections:manage` kann ein Mitglied dort ein Modellprofil erstellen; ohne diese Berechtigung wählt es ein vorhandenes Profil. Ein neues Profil wird sofort gespeichert. **Speichern** übernimmt die Auswahl für die nächtlichen Vorschläge. Das Briefing listet vorhandene Funde ohne Modellaufruf. Beobachtungen durch ausgewählte Agenten teilen das Limit pro Person. Die Plattform kann ein wenig aus eigener Initiative tun, und tut nichts davon, bis eine Organisation es einschaltet: die Karte **Proaktivität** auf ihrer Mitgliederseite (`budgets:manage`). Eingeschaltet liest ein Hintergrundjob die letzten Anfragen jeder Person an den Tagen und zu der Stunde, die die Organisation gewählt hat - in der Zeitzone jeder Person oder eine Stunde vor der Arbeitszeit, die ihr Profil angibt - und schlägt eine Routine für eine Anfrage vor, die sie immer wiederholt, ein Profil oder die ersten Nachrichten, die ihr leerer Chat anbietet ([Konsole](console.md#chat)). Jeder ist ein Modellaufruf, auf dem Modell, das die Organisation beim Einschalten gewählt hat (es gibt keinen Standard: `proactive_model_profile_id` ist Pflicht), in der Run-Historie als Run der Oberfläche **proactive** festgehalten und wie jeder andere auf das Budget der Organisation angerechnet. Eine Person kann sich unter **Einstellungen → Assistent** ausnehmen. Der nächtliche Blick wird 20 Stunden nach seinem eigenen letzten Versuch fällig. Beobachtungen und Speicherwartung verschieben diesen Zeitpunkt nicht. Beobachtungen zählen weiterhin zum persönlichen Proaktivitätsbudget. Ein erfolgreicher Blick speichert die Person und einen Hash jedes gelesenen Prompts, ohne den Text zu kopieren. Das Abschließen einer bereits gelesenen Genehmigungs- oder Fragepause löst daher keinen weiteren bezahlten Modellaufruf aus. Ein zurückgestellter Prompt bleibt ungelesen, bis ein späterer Blick ihn berücksichtigt; geänderter Text ist wieder neu. Fehlgeschlagene Modellaufrufe markieren keine Prompts als gelesen. Nach dem Upgrade verschieben alte Hintergrund-Runs ohne Rollenzuordnung den nächsten Blick nicht. Bestehende Nachrichten haben keinen Lesenachweis, sodass geeignete aktuelle Prompts einmal nachgeholt werden können. Die üblichen Schalter und Budgets gelten weiterhin. Der nächtliche Job liest nur einen Prompt, der den Eingabe-Guardrail des Agents passiert hat, an den er ging, denn das Proaktivitätsmodell hat keine eigenen [Guardrails](reference/capabilities.md#guardrails): - Ein Prompt, den ein Guardrail abgelehnt hat, bleibt draußen. - Ein Prompt, den kein Run angenommen hat, bleibt draußen: einer an einen archivierten Agent, einer, der abgelehnt wurde, bevor sein Run begann, und ein Chat-Prompt, der an einen Workflow geht. - Ein Prompt, dessen Run noch läuft, wartet. Der Job liest ihn in einer späteren Nacht, wenn der Run beendet ist. - Stoppt ein Run vor seiner ersten Model-Anfrage, zum Beispiel am Budget, lässt der Job den Eingabe-Guardrail dieses Runs über den Prompt laufen und lässt aus, was er blockiert. Gibt es die Agent-Version nicht mehr, bleibt der Prompt draußen. - Hat die Aufbewahrung den Run gelöscht, liest der Job den Prompt nur, wenn eine Antwort eines Models auf ihn folgt. Ein Prompt von vor Version 1.36.0 wird genauso gelesen, also bleibt die Ablehnung eines Eingabe-Guardrails draußen, denn sie hat keine Antwort hinterlassen. Ein gesprochener Turn zeichnet kein Model auf, also bleibt er draußen, sobald sein Run weg ist. Routinen-Prompts werden mit der festgelegten Agent-Version, der Version der benannten Umgebung oder der aktuellen Agent-Version geprüft, wie beim Start der Routine. Ein fehlendes Ziel oder eine unlesbare Spec wird ausgelassen, ebenso eine Routine, deren letzter Lauf abgelehnt wurde. Eine bereits gestartete Routine, deren letzter Run durch die Aufbewahrung gelöscht wurde, wird ebenfalls ausgelassen: Ein unbekanntes Ergebnis kann eine Ablehnung nicht aufheben. Ein Prompt, den ein Guardrail geschwärzt hat, passiert, und der Job liest ihn so, wie er gespeichert ist, also ungeschwärzt ([#2655](https://github.com/vstorm-co/agenticos/issues/2655)). Darüber hinaus begrenzt das **Limit für Arbeit aus eigener Initiative** der Organisation die Runs jeder Person in einem Kalendermonat - standardmäßig 1 $, auf derselben Karte eingestellt (`proactive_limit_usd`). Eine Person, bei der nichts Neues passiert ist, kostet nichts: Der Job ruft das Modell nur, wenn es etwas zu lesen gibt. **Beobachtungen** laufen durch den ausgewählten veröffentlichten Agenten, mit demselben Runner wie Chat und Routinen sowie seinen eigenen Anweisungen und seinem Modell. Sie werden getrennt eingeschaltet (`proactive_watches_enabled`) und teilen das Proaktivitätsbudget je Person, nicht das Briefing-Modell. Erstelle sie unter **Routinen**, neben den Zeitplänen eines Agenten mit **Beobachten und informieren**, neben einem persönlichen Konto oder im privaten Chat. Wähle Agent, Konto, Intervall und optionales Ende. Die Beobachtung folgt Veröffentlichungen, einer benannten Umgebung oder einer unveränderlichen Version. Die Organisation bestimmt das Mindestintervall (`proactive_watch_min_minutes`). Die Veröffentlichung muss diesen persönlichen Katalogdienst bereits einbinden. Eine Beobachtung aktiviert keine fehlende Bindung und ersetzt keinen Agenten. Eigene Server ohne persönliche Katalogbindung werden hier nicht unterstützt. Die Erstellung genehmigt das konkrete Konto und seinen Endpunkt, die gemeinsame Teilmenge der Tool-Listen von Konto und Agent sowie die derzeit angehängten Wissensbasen und Kontextdateien. Spätere Veröffentlichungen dürfen Zugriff entfernen, aber keine Konten, Tools oder Quellen hinzufügen. **Aktuelle Quellen erneut genehmigen** erneuert diese Zustimmung ausdrücklich. Ein neues primäres Konto ersetzt das genehmigte Konto nicht. Jede Prüfung ist ein gewöhnlicher **proactive**-Run des tatsächlichen Agenten und seiner Version. Quelltools brauchen `readOnlyHint`, und ein Lesetool, vor dem das MCP-Binding des Agenten fragt, bleibt bei der Prüfung außen vor. Verfügbar sind das genehmigte Konto, Wissen/Kontext, Guardrails, Planung und Systemerinnerungen; Web, Sandbox, ausführbare Skills, Speicherschreibzugriffe und Delegation sind nicht verfügbar. Erforderliche interaktive Genehmigungen verhindern die Einrichtung und werden nicht umgangen. Mitgliedschaft, Ausführungsrecht, Quellzustimmung, Pause und Ablauf werden vor Tools und Ergebnisspeicherung nochmals geprüft. Neue Funde behalten ihre stabile Beobachtungs-/Element-ID im persönlichen Posteingang und erscheinen im Quellchat; außerhalb des Chats erstellte Beobachtungen verwenden den primären Chat. Nur ausdrücklich gewünschte dringende Funde senden zusätzlich eine sofortige Benachrichtigung. Im Standardmodus **Nur bei relevanten Änderungen** gibt es ohne Änderung keine Chatnachricht oder Abschlussbenachrichtigung. Bei fehlendem Chatzugriff bleibt der Fund im Posteingang und `delivery_unavailable` wird aufgezeichnet. Der letzte Versuch ist von der letzten erfolgreichen Prüfung getrennt. Eine Modellantwort ohne erfolgreichen Quellzugriff oder mit ungelöstem Lesefehler ist `not_completed`. Nicht verfügbare Quellen und entzogener Zugriff sind explizit; `budget_paused` bedeutet verbrauchtes Budget. Diese Fehler verschieben den Erfolgszeitpunkt nicht. Unerwartete Fehler lassen den Job weiterhin fehlschlagen. `0261_watch_attempt_status.py` beginnt die Versuchshistorie ohne erfundene ältere Erfolge. `0262_watch_agent_sources.py` erhält IDs, Funde, Intervall und Aktivierungszustand, erfindet aber keine Agentenzuordnung. Bestehende Beobachtungen zeigen `needs_agent` und rufen kein Modell auf, bis ihr Eigentümer einen veröffentlichten Agenten und seine Quellen bestätigt. Das Löschen einer Beobachtung entfernt Ursprung und Quellenzustimmung eines laufenden oder wartenden Runs nicht. Erstellung und Änderung erfordern die eigene angemeldete Sitzung oder den vertrauenswürdigen privaten menschlichen Chat. Gewöhnliche API-Schlüssel, Publisher-Ersatzkontext, Hintergrundroutinen und Identitätsübernahme können diesen Dauerzugriff nicht erteilen. Die Plattform-MCP-Tools `list_watches`, `create_watch`, `update_watch` und `delete_watch` verwenden denselben Dienst und dieselben IDs. Vollständige private Chat-Aufträge erstellen oder ändern sofort; Veröffentlichung, Einladungen, Löschen und sofortiges Ausführen behalten ihre Genehmigungsregeln. Eine Beobachtung kann **Nach jeder Prüfung, auch ohne Änderungen** wählen (`report_mode: every_check`). Jede erfolgreiche Prüfung liefert dann den vereinbarten Bericht in den Quellchat, auch ohne neue Funde, und verwendet das gemeinsame Benachrichtigungssystem. Fehlgeschlagene oder nicht verfügbare Lesezugriffe werden nie als unveränderter Stand gemeldet. Die Aktivierungszeit bleibt im Job; der Berichtsmarker wird mit den Ergebnissen gespeichert. Eine Wiederholung derselben abgeschlossenen Aktivierung sendet keinen zweiten Bericht und bezahlt keine zweite Prüfung. Ein weiterer Klassifikationsaufruf entfällt. Zeitpläne und Beobachtungen erben deine Benachrichtigungseinstellungen. **Benachrichtigungen für diese Routine** erlaubt beliebige Kombinationen aus E-Mail, Geräte-Push, Slack, Telegram, Mattermost, Microsoft Teams und SMS oder nur die App. `notification_channels: null` erbt; eine leere Liste verhindert externe Zustellung. Persönlich ausgeschaltete Kanäle bleiben aus. Wartende Sendungen prüfen die aktuelle Richtlinie; Pause oder Löschen stoppt sie. Verpflichtende Sicherheits-/Konfigurationshinweise bleiben verpflichtend. Bereits vom Anbieter angenommene Nachrichten lassen sich nicht zurückrufen. Wähle unter **Einstellungen → Benachrichtigungen** ausdrücklich einen verknüpften privaten Chat je Plattform für die aktuelle Organisation. Eine Kontoverknüpfung allein aktiviert nichts. Gruppenchats, fremde Konten und Bots anderer Organisationen sind ausgeschlossen. E-Mail und Push behalten ihre Einrichtung. Die Chatauswahl bei den Ereigniseinstellungen bestimmt, welche Plattform du gerade bearbeitest. Ein Ziel bindet die konkrete Kontoverknüpfung und Bot-Autorisierung. Entknüpfen, erneutes Verknüpfen oder ein anderer Bot-Token/Server erfordern eine neue Auswahl. Wartende Sendungen wechseln nie automatisch das Ziel. Vor dem Senden werden Mitgliedschaft, Kontostatus, Bot-Richtlinie, SMS-Verknüpfungsfrist, Einstellungen, Ressourcenzugriff und gemeinsamer Fallstatus erneut geprüft. Ohne Entscheidungsrecht enthält ein Genehmigungshinweis nur die Tatsache des Wartens, keinen Entscheidungslink. Chatplattformen bieten keinen gemeinsamen idempotenten Sendevertrag. Ein unbestätigter Versand erhält einen persistenten Versuch: Timeout, Anbieterfehler oder Worker-Ausfall bleiben als sichtbarer Fehler erhalten. Ein Slack-HTTP-429, der die Anfrage ausdrücklich ablehnt, erlaubt nach `Retry-After` einen erneuten Versuch, höchstens fünf Claims insgesamt. Jeder Versand löscht zunächst die Retry-Freigabe; ein Absturz dieses Versuchs wiederholt ihn nicht. Das Zurückstellen entwertet das alte Übergabe-Token. Andere Chatfehler sind endgültig. E-Mail und Push behalten ihre Wiederholungsregeln. Slack-Hinweise nutzen begrenzte Klartextblöcke und **Open in AgenticOS** zum selben autorisierten Fall in der Konsole. Namen und Inhalte erzeugen keine Erwähnungen oder automatischen Vorschauen. Ohne Entscheidungsrecht bleibt nur der Wartehinweis ohne Entscheidungslink. Zustellung bezeichnet Plattformannahme; Bestätigung bleibt eine ausdrückliche Entscheidung im gemeinsamen Fall. Migration `0273_notification_safe_retries.py` gewährt bestehenden Zeilen keine Retry-Freigabe. Ein Downgrade stellt einen Chatversuch wieder her. Migration `0271_notification_routes.py` ergänzt Ziele und Autorisierungsidentitäten. Leere die Worker und aktualisiere API/Worker gemeinsam, bevor du neue Kanäle wählst. Bestehende Konten bleiben aus. Ein Downgrade entfernt Chatziele, deren Versandhistorie und Einstellungen und begrenzt Routineauswahlen auf E-Mail/Push; externe Sendungen bleiben bestehen. `0263_routine_notification_policy.py` führte die ursprünglichen Routineeinstellungen ein. ### Budgetpausen { #routine-budget-pauses } Erreicht eine Routine ihr eigenes Monatslimit, erhält ihre besitzende Person eine Budgetmeldung je Zeitraum und konfiguriertem Limit. Eine Beobachtung nennt das gemeinsame Budget für proaktive Arbeit dieser Person und verweist zur Prüfung an die Administration. Die Prüfung stoppt vor dem Öffnen eines Laufs oder einem Modellaufruf; die Meldung nennt die Monatsausgaben. Die bestehenden Budgetpräferenzen und die Kanalgrenzen der Routine gelten weiterhin. Die Meldung öffnet einen bearbeitbaren Fall für diese Aufgabe und diesen Monat. Weitere Prüfungen verwenden denselben Fall. Bestätigen oder Stummschalten storniert ausstehende Zustellungen. Hat die nächste Aktivierung wieder Budget oder wird die Aufgabe entfernt, werden ihre offenen Budgetfälle erledigt und ausstehende Zustellungen storniert. Ein geändertes Limit erlaubt eine neue Meldung bei dessen Erreichen; ein stummgeschalteter Fall bleibt im Monat stumm. ### Aktionsfreigaben { #routine-action-consent } Eine unbeaufsichtigte Routine fragt vor Fähigkeitsaktionen, lokalen Dateiänderungen und Delegation. Externe MCP-Aufrufe fragen, sofern ihr Server sie nicht als nur lesend kennzeichnet; eine strengere Bestätigungspolitik der Verbindung gilt weiter. Die Vorgaben interaktiver Agenten bleiben gleich. Der Codemodus ist für Routinen ausgeschaltet, damit Sammelskripte keine Einzelkontrollen umgehen. Schreibzugriffe auf AgenticOS Virtual Tables laufen innerhalb ihrer aktuellen Tabellen- und Operationsfreigaben ohne Ausnahme für die Routine. Eine ausdrücklich konfigurierte Freigaberichtlinie des Agenten gilt weiterhin für diese internen Schreibzugriffe. Die erstellende Person öffnet **Aktionsfreigaben** ihrer Routine und wählt Fähigkeitsaktionen sowie einzelne MCP-Aktionen eines benannten Kontos. Das erfordert `approvals:decide` und den Organisationsschalter `chat_may_waive_approvals`. API-Schlüssel, Agenten, übernommene Identitäten und Administratoren für andere Personen dürfen nicht zustimmen. Ausdrücklich vom Agenten oder einer MCP-Bindung geforderte Bestätigungen bleiben bestehen. Eine MCP-Ausnahme benennt das konkrete Konto, den Adresshash, die Autorisierungsgeneration und den ursprünglichen Aktionsnamen. E-Mail-Versand erlaubt weder Löschen noch Schreibzugriffe auf ein anderes Konto. Persönliche Konten stehen nur Routinen zur Verfügung, die als ihre erstellende Person handeln. Die Auswahl verwendet die gespeicherte Werkzeugliste; prüfe die Verbindung oder bitte deren Administration, wenn sie fehlt. Alle Freigaben lassen sich auch ohne funktionierende Werkzeugliste widerrufen. Beim Speichern müssen Version und Konfiguration noch der menschlichen Prüfung entsprechen. Eine Freigabe nennt die unveränderliche veröffentlichte Version, stabile Fähigkeits- und Werkzeugkennungen sowie die Routinenkonfiguration. Geänderte Arbeit oder Ziele machen sie ungültig. Jeder Lauf behält seine zugelassenen Freigabe-IDs; eine spätere Zustimmung gibt keinen geparkten Lauf frei, und Widerruf mit erneuter Zustimmung erzeugt eine neue Identität. Mitgliedschaft, Rechte, Organisationspolitik und Zustimmung werden vor jedem Werkzeug erneut geprüft. Jeder freigegebene Aufruf zeichnet eine genehmigte Zeile mit zustimmender Person und Argumenten auf. Delegierte brauchen eigene Entscheidungen und erben keine Ausnahme des Elternlaufs. Eine dauerhafte Aktionsfreigabe speichert auch die aktuellen Revisionen der gebundenen Skills, Skill-Ressourcen, Kontextdateien und Projektanweisungen. Jede Bearbeitung macht die Ausnahme ungültig, selbst wenn die veröffentlichte Agentenversion gleich bleibt oder der alte Text später wiederhergestellt wird. Prüfen Sie die Aktionen erneut, um eine neue Ausnahme zu erteilen. Bestehende Ausnahmen ohne Revisionsfingerabdruck gelten nur für Agenten ohne solche veränderlichen Anweisungsquellen weiter. `0267_routine_action_consents.py` erzeugt keine Ausnahmen für bestehende Routinen. Eine bisher unbeaufsichtigte Aktion kann nun auf Bestätigung warten. Alte Worker abwickeln lassen und API sowie Worker gemeinsam aktualisieren, bevor Aufgaben weiterlaufen; ein alter Worker erzwingt diese Kontrolle nicht. Ein Downgrade entfernt gespeicherte Ausnahmen; Bestätigungsprotokolle und bereits ausgeführte Wirkungen bleiben bestehen. ### Antworten fortgesetzter Routinen { #resumed-routine-answers } Eine nach Bestätigung oder beantworteter Frage abgeschlossene Routine stellt ihre Konsolenantwort in derselben Transaktion wie den Endzustand zu. Der gespeicherte Auftrag enthält fertigen Text und Kennungen; ein Neustart benötigt keinen neuen Modellaufruf. Zustellstatus, Chatnachricht und Abschlussmeldung werden gemeinsam gespeichert. Gleichzeitige Versuche und Wiederholungen hängen keine zweite Antwort an. Ein autorisiertes Konversationsereignis aktualisiert den offenen Quellchat nach dem Commit, ohne ihn zurückzusetzen; ältere Abonnenten behalten ihren Ereigniswortschatz. Der Worker prüft aktuellen Routinenzustand, Frist, erstellende Person und Ziel. Chat-Zustellung prüft aktive Mitgliedschaft, Ausführungsrecht am Agenten und aktuelles Schreibrecht im privaten Chat erneut. Archivierte Chats sind für neue Nachrichten geschlossen. Bei unerreichbarem Ziel bleibt das Ergebnis in Aktivität und die Zustellung wird als nicht verfügbar vermerkt. Datenbankfehler lassen den Auftrag für bis zu drei Versuche offen; Administration → Hintergrundaufgaben zeigt eine ausgeschöpfte Zustellung. Die Abschlussmeldung verlinkt den Chat nach erfolgreicher Zustellung. Die Fortsetzung in externen Kanälen bleibt eigene Abnahmearbeit. `0268_routine_answer_delivery.py` spielt keine historischen Ergebnisse nach. API und Worker gemeinsam aktualisieren. Vor einem Downgrade ausstehende Aufträge `deliver-routine-answer` abwickeln oder abbrechen, da er gespeicherte Übergaben entfernt; bereits zugestellte Nachrichten bleiben erhalten. ### Störungsfälle { #routine-failure-incidents } Ein fehlgeschlagener Routinenlauf, eine unzugängliche Beobachtungsquelle oder ein fehlender Zielchat öffnet einen Fall für die besitzende Person. Weitere Fehler aktualisieren dessen letzte Ursache und Lauf ohne erneuten Alarm. Bestätigen lässt ihn bearbeitbar und still; Stummschalten gilt für diese Ursache auch nach einer Erholung. Ein neuer Fehler nach bestätigter Erholung öffnet denselben Fall erneut und sendet eine neue Meldung. Der gemeinsame Runner meldet erste, fortgesetzte und nach einem Prozessabsturz beendete Läufe. Unter terminalen Ergebnissen bestimmt nur der zuletzt gestartete Hauptlauf den Zustand einer Routine; delegierte Unterläufe und verspätete Abschlüsse älterer Läufe überschreiben ihn nicht. Eine Beobachtung erholt sich erst nach einem bestätigten Quellenabruf; ein abgeschlossener Modelllauf allein genügt nicht. Verspätete Prüfungen erhalten den neuesten Versuch und den letzten erfolgreichen Abruf unabhängig voneinander. Erkannter Ablauf oder Entfernung erledigt offene Blockaden und storniert Zustellungen. Pausierte, abgelaufene oder gelöschte Routinen schreiben keine fertige Antwort in ihren Zielchat. Beobachtungs- und Erholungszeiten schützen Entscheidungen vor Callback-Retries und älteren Ergebnissen. Scheitert das Speichern einer Benachrichtigung, bleibt der neue Vorfall erneut meldbar. Agenten- und Lauflimits teilen den Vorfall der besitzenden Person ohne eine zweite Fehlermail. Organisations- und Abteilungsbudgets bleiben unabhängige Finanzmeldungen, auch wenn die besitzende Person administriert. Ihr Routinenfall bleibt in der App, wenn die Finanzmeldung sie bereits informiert. Die Budgetpräferenzen gelten weiterhin. `0266_routine_activation_clock.py` ergänzt die Zeiten ohne erfundene historische Versuche. Vorhandene Fall-IDs für Beobachtungsfehler bleiben erhalten. API und Worker werden gemeinsam aktualisiert: Der Zustellungsworker muss die neuen Budgetmetadaten verstehen. Ein Downgrade entfernt die Zeiten, erhält aber Fälle, Benachrichtigungen und Entscheidungen. ### Systemagenten { #system-agents } Manche Arbeit gehört der Plattform selbst und braucht dennoch ein Modell. Eine **Systemrolle** gibt ihr einen Eigentümer, den ein Betreiber sehen kann: eine Konfiguration je Organisation und Rolle, nie eine Agent-Zeile, nie eine je Job und nie dem Starter-Agent oder einem Agent zugeschrieben, dessen Anweisungen nicht liefen (#2575). **Organisationen → KI und Dienste → Hintergrunddienste** (`budgets:manage`) listet jede Rolle mit ihrem Modell, den Ausgaben dieses Monats und ihren letzten Ausführungen. | Rolle | Was sie tut | Konfiguriert | | --- | --- | --- | | `daily_briefing` | Der nächtliche Blick auf die Woche jeder Person und das daraus gebaute Briefing | Im Tab Proaktivität: dasselbe Modell und Limit, hier nur lesbar | | `memory_maintenance` | Liest eine abgeschlossene zugelassene Runde einmal und hält die dauerhaften Fakten, Vorlieben und Entscheidungen darin als [Claims](reference/capabilities.md#memory-claims) fest | Hier: ein Schalter, ein Modell und ein eigenes Monatsbudget | Die Gedächtnispflege benötigt den AgenticOS-Starter nicht. Ihr Run behält den Ursprungsagenten als Speicherreferenz und wird als `memory_maintenance` abgerechnet; ausgeführt werden nur die Extraktionsanweisungen der Plattform auf dem für die Rolle gewählten Modell. Entfernen oder Zurückziehen des Starters deaktiviert die Extraktion anderer veröffentlichter Agenten nicht. Eine Rolle wird ausgeschaltet, nicht gelöscht. Die **Gedächtnispflege** benötigt ein ausdrücklich gewähltes Modell und beachtet ihr Monatsbudget sowie die Obergrenze der Organisation. Sie liest abgeschlossene Gesprächsrunden nur, wenn der Runner eine reale Person mit Schreibrecht zugelassen hat: private Chats, verknüpfte Kanalnachrichten und eigene Zeitplan- oder Workflow-Aufgaben. Embeds, API-Schlüssel, Identitätsübernahmen, Ereignisse von Außenstehenden und Delegationen sind ausgeschlossen. Der Auftrag bewahrt den ursprünglichen Empfängerkreis. Private Runden schreiben in das Profil der Person oder ihre Beziehung zum Agenten; Gruppenrunden ausschließlich in den Quellraum, auch bei allgemeinen Fakten. Nach der Modellantwort werden Mitgliedschaft und Löschungen unter der Schreibsperre erneut geprüft. Ältere Aufträge ohne Empfängerdaten bleiben auf private Web-Chats beschränkt. Die Pflege teilt und korrigiert keine Claims. Deterministische Konsolidierung benötigt weder Modell noch Rolle. ### Was die Run-Historie zeigt { #what-run-history-shows } **`GET /runs` listet nur Runs der obersten Ebene, und sein `total` zählt diese.** Dieselbe Voreinstellung wie die Monatssumme der Organisation, und aus demselben Grund: Ineinander verschränkt lassen sich die beiden Arten von Zeile nicht in einer Kostenspalte lesen. Ein Fan-out aus drei Delegationen ist ein Run, der auf der Seite 1,00 $ kostet und auf der Rechnung 1,00 $; zusammen gelistet waren es vier Zeilen mit 1,00 $ + 0,40 $ + 0,40 $ + 0,40 $ neben einer Zahl seit Monatsbeginn von 1,00 $, und beide Hälften hatten über verschiedene Fragen recht. Die Liste nimmt dieselbe zweiseitige Arithmetik wie die Summen oben, aus demselben Grund — sodass eine Oberfläche, die auf einen Agent eingeengt ist, zeigt, was _dieser Agent_ getan hat, die Arbeit seiner Delegates eingeschlossen: | Frage | Antwort | | --- | --- | | `GET /runs` | Runs, die jemand gestartet hat. `parent_run_id IS NULL` | | `GET /runs?agent_id=&include_delegations=true` | Die eigene Historie eines Agents. Das fragen das Panel Recent runs im Builder und `?agent=` in **Aktivität** (Aktivität), denn die Zeilen eines Delegates sind der einzige Datensatz darüber, was er selbst getan hat | | `GET /runs?parent_run_id=` | Was dieser Run delegiert hat — die Abfrage, für die `agent_runs_parent_run_id_idx` existiert. Hat Vorrang vor `include_delegations` | | `GET /runs/` | Ein Run, delegiert oder nicht. Wo ein Link aus einem Transkript landet | | `GET /runs//transcript` | Die Turns dieses Runs, der Reihe nach - was eine Run-Detailansicht als Schritte rendert. Autorisiert, nicht besessen (unten) | **Einen Run zu lesen ist autorisiert, nicht besessen.** Ein Kollege, der `runs:view` hält, liest einen Run, den jemand anderes gestartet hat. Die Autorität über einen Run liegt bei der Organisation, denn ein Run ist das, was der Organisation berechnet wird und wofür sie zur Verantwortung gezogen wird — und nicht Privateigentum dessen, der auf Los gedrückt hat. Die Entscheidung liegt also im Service statt in einem Route-Gate: Er löst den Run zuerst gegen die Organisation des Aufrufers auf und prüft dann `runs:view`. Drei Konsequenzen: - Ein Run in **einem anderen Tenant liest sich als abwesend** — derselbe 404, mit dem eine Id antwortet, die es nie gab, bis in den Body hinein —, sodass sich über die Antwort nicht herausfinden lässt, dass ein Run existiert. - Ein Run, der **ohne Unterhaltung** lief (ein API-Aufruf, der keine `conversation_id` übergab), hat kein Transkript zu lesen und sagt das mit einer `conversation_id` von null statt mit einer leeren Liste, die sich als „er hat nichts getan" läse. - Nichts davon weitet `GET /conversations/{id}/messages`, was **auf den Besitzer beschränkt** bleibt. Dass das Transkript eines Runs für einen Kollegen lesbar ist, darf den privaten Faden, in dem es steckt, nicht ebenfalls lesbar machen. **Jeder Turn, den das Transkript ausliefert, trägt die Bewertungen, die Menschen dazu hinterlassen haben** — den eigenen Daumen des lesenden Aufrufers, die Likes und Dislikes der Organisation und den Kommentar der jüngsten negativen Bewertung. Eine gewöhnliche Message-Zeile hält nichts davon, also werden sie in einem Zug aus `message_ratings` gelesen und an die Turns geheftet. Ein Turn, den niemand bewertet hat, trägt sie leer und liest sich genau wie eine gewöhnliche Message. Das ist es, was die Run-Detailansicht die negativ bewerteten Antworten und die dazu hinterlassenen Worte zeigen lässt — die Unterhaltungen hinter der Qualitätszahl des Dashboards (#209) —, gelesen dort, wo der Run gelesen wird, statt nur im Bewertungsexport für App-Admins. Gezeigt wird der Kommentar einer **negativen** Bewertung, nie einer positiven, und der jüngste, wenn ein Turn mehr als einen Einwand auf sich zog. ### Was die Aggregate des Dashboards zeigen { #what-the-dashboards-aggregates-show } `GET /stats/usage` nimmt dieselben zwei Seiten und dieselbe Voreinstellung. Die zusammengesetzte Antwort ist die Frage der Organisation, also zählt jeder Block darin nur Zeilen der obersten Ebene: die Kosten des Zeitraums und ihre Aufteilung nach Provider (die doppelte Rechnung oben), aber auch die Run-Summe, die Tagesreihe, die Aufteilung nach Ausgängen, die Oberflächen, die Latenz-Perzentile, die Zahl der aktiven Personen und die Tabelle je Person. Über die Kosten hinaus _kopiert_ eine delegierte Zeile `user_id` und `surface` ihres Parents, sie zu zählen würde also zusätzlich eine zweite Person und eine zweite Ankunft auf einem Channel erfinden, den jemand einmal benutzt hat. Zwei Aggregate nehmen die andere Seite, und beide werden über einen Agent gefragt: | Frage | Child-Zeilen | | --- | --- | | `by_agent` — die Adoption-Karte | **eingeschlossen.** Ausgeschlossen hat ein Agent, der vierhundertmal am Tag als jemandes Delegate läuft, keine Zeile, und die Karte bezeichnet jeden veröffentlichten Agent ohne Zeile als vergessen und bietet an, ihn zu archivieren. Ihre Balken können daher die Run-Summe daneben übersteigen; nichts summiert sie | | `?group_by=version` — die Karte zum Versionsvergleich | **eingeschlossen.** Ein Spezialist, der nur je als Delegate ausgeführt wird, hätte sonst nichts, was sich über seine Versionen vergleichen ließe | Die Invariante, die so oder so überlebt: Die Segmente des Ausgangs-Donuts summieren sich weiterhin auf `total_runs`, und sein Segment `awaiting_approval` zählt weiterhin dieselben geparkten Runs wie die Freigabe-Karte, denn diese drei kommen von derselben Seite des Schalters. Die eine Abfrage ganz ohne Delegationsfilter ist die Zahl der Runs des Aufrufers, die auf einer Entscheidung geparkt sind. Ein geparktes Kind ist ein steckengebliebener Parent, und diese Karte beantwortet „warum wird mein Agent nicht fertig"; heute ändert das nichts, denn eine Delegation wird bereits fertig in die Datenbank geschrieben und parkt daher nie. Die letzten beiden sind `?run=` auf der Seite **Aktivität** (Aktivität): ein Run, die Delegationen darunter jeweils mit der Task-Id abgezeichnet, die ihre `subagent_*`-Frames trugen, und ein Link hinauf zu dem Run, dem eine Delegation berechnet wurde. Ein Delegationspanel in einem Chat verlinkt mit der `run_id` dorthin, die sein terminaler Frame trägt — deshalb trägt der Frame eine. Delegierte Zeilen in der Tabelle der obersten Ebene zu verschachteln wird hier bewusst _nicht_ getan; ein Tabellen-Primitiv für das ganze Produkt ist [separat vorgeschlagen](https://github.com/vstorm-co/agenticos/issues/139), und die Verschachtelung gehört dorthin und nicht in eine maßgeschneiderte Run-Tabelle. ### Was der Kostenbildschirm zeigt { #what-the-cost-screen-shows } `GET /spend` nimmt sein Fenster auf zwei Wegen, weil die Seite nach beiden Arten fragt: `days` für die Voreinstellungen _letzte N Tage_ und `from`/`to` für _diesen Monat_, _letzten Monat_ und einen Kalenderbereich. `from` gewinnt, wenn beide ankommen — ein expliziter Bereich ist eine speziellere Anfrage als eine Voreinstellung, die niemand geändert hat —, und `period_days` kommt in diesem Fall als null zurück, statt eine Zahl zu wiederholen, der der Bereich widerspricht. **Jedes Panel auf dem Bildschirm liest dasselbe Fenster.** Die Zeilen je Agent, By provider und By key nehmen alle das aufgelöste `since`/`until` statt einer eigenen Tageszahl, sodass zwei Zahlen nebeneinander nicht verschiedene Runs beschreiben können. Das ist derselbe Defekt, den #198 ein Panel weiter oben benennt. **Seit Monatsbeginn ignoriert das Fenster vollständig**, und ebenso jedes daran gemessene Cap je Agent. Eine monatliche Obergrenze, verglichen mit rollenden sieben Tagen, liest sich an dem Tag, an dem das Cap tatsächlich erreicht wurde, als zu 20 % ausgeschöpft. Jede Zeile je Agent trägt **zwei Kostenzahlen unter zwei verschiedenen Namen**, und das ist die Regel dieser Seite durchgehend: | | | | --- | --- | | `cost_usd` | Sein Anteil am Fenster, **nur Runs der obersten Ebene**, sodass sich die Spalte auf die Summe darüber addiert | | `month_to_date_usd` | Sein **eigener** Kalendermonat, delegierte Zeilen **eingeschlossen** — die Ausgaben, auf die sein `monthly_cap_usd` ein Cap ist. Er addiert sich nicht auf den Monat der Organisation und wird auch nicht so gezeichnet | `partial_run_count` sagt, wie viel davon überhaupt eine Tatsache ist: wie viele **Runs der obersten Ebene** im Fenster sich nicht vollständig bepreisen ließen, sodass die Kosten um genau so viele eine Untergrenze sind. _„3 von 40 Runs ließen sich nicht bepreisen"_ ist etwas, womit ein Leser etwas anfangen kann; eine Zahl mit einem Pluszeichen daran nicht. Ein Run zählt, wenn ein Modell in seinem Baum keinen Preis hatte, **die seiner Delegates eingeschlossen** — der Baum teilt sich ein Spend-Ledger, ein unbepreister Delegate macht also auch die Zeile des Parents zu einer Untergrenze. So herrscht eine Zahl über alle drei Aufschlüsselungen: By provider und By key summieren die eigenen Ausgaben jeder Zeile, delegierte Zeilen eingeschlossen, und eine Untergrenze in einer von beiden markiert eine Zahl, die die verursachende Zeile nie angesehen hat. Sie **misst** By agent, das dieselben Zeilen der obersten Ebene zählt, und **markiert** die anderen beiden nur — sie zählt Bäume, sodass ein Parent mit drei unbepreisten Delegates als `1` erscheint, während drei Zahlen darunter eine Untergrenze sind. Ein Baum, der **über den Anfang des Fensters hinausragt** — die Zeile des Delegates darin, die seines Parents davor —, wird über den Delegate gezählt: Die Parent-Zeile, die sonst die Markierung trüge, liegt außerhalb jedes Aggregats dieser Seite, während die eigenen Ausgaben des Delegates in beiden Aufteilungen liegen. Sie landet bei dem Agent, als der der Delegate lief, einmal je hinausragendem Baum, wie viele Delegationen auch die Kante überquerten, und nur dann, wenn die eigenen Anfragen des Delegates unbepreist blieben — eine bepreiste Delegation unter einem unbepreisten Parent außerhalb des Fensters wirft keinen Vorbehalt auf, denn das Geld im Fenster selbst ist exakt ([#620](https://github.com/vstorm-co/agenticos/issues/620)). Eine Zeile ist **eine je Agent**, mit `agent_name` darauf. Früher war sie eine je Agent _und Modell_ und trug nur `model_label` — der Tab listete also Modellnamen, wo ein Leser einen Agent erwartet, und teilte einen Agent auf zwei Zeilen auf, weil er auf zwei Modellen geantwortet hatte. Die Form je Modell überlebt dort, wo sie die gestellte Frage ist: Die Nutzungs-E-Mail gruppiert weiterhin so. **Wer es ausgegeben hat, ist eine vierte Aufschlüsselung**, unterhalb von By provider, By key und By agent — diejenige, die mit Personen antwortet statt mit Anbietern oder Agents. Sie liest dieselben `group_by=user`-Zeilen wie die Adoption-Tabelle des Dashboards — nur Runs der obersten Ebene, die geschäftigsten zuerst —, sodass die Kosten eines Delegates einmal landen, innerhalb des Runs, der ihn gestartet hat. Und sie deckt das Fenster ab, das der Rest des Tabs zeigt, statt einer eigenen rollenden Voreinstellung. **Wann es ausgegeben wurde** ist ein Balken pro UTC-Tag über dasselbe Fenster, aus `by_day` — die eigenen Kosten jedes Runs, sodass die Tage dieselbe Rechnung ergeben, die die Aufschlüsselungen teilen. Ein Tag ohne Runs wird als null gezeichnet statt ausgelassen. Jede Aufschlüsselung zeigt den Anteil jeder Zeile an ihrer Summe, und ein Agent mit Monatslimit zeigt den bisherigen Monat gegen dieses Limit. Die Personen der Organisation zu benennen ist dieselbe Entscheidung, die auch die Dashboard-Karte trifft, also nimmt sie dasselbe Gate: `runs:view`, gehalten von Builder und Operator ebenso wie von den beiden Verwaltern, und sie sagt das in ihrem eigenen Text. Ein Aufrufer ohne `runs:view` sieht sie nicht. Die Karte ist abwesend und ihre Frage wird nie gestellt, statt dass eine Anfrage abgelehnt zurückkommt. ### Die Freigabe-Queue einengen { #narrowing-the-approvals-queue } `GET /approvals` liefert zwei Sichten auf dieselben Zeilen. Voreingestellt nur die ausstehenden, das ist die Queue, auf der jemand handelt; `?status=approved&status=rejected` ist der Datensatz dessen, was entschieden wurde, und er trägt den Namen und die Notiz der entscheidenden Person, denn eine nackte UUID ist keine Spur der Verantwortlichkeit. Auf einer entschiedenen Zeile gibt es bewusst keine Bedienelemente. | Parameter | | | --- | --- | | `status` | Wiederholbar. Abwesend heißt ausstehend — die Queue | | `triggered_by_user_id` | Wessen Runs den Aufruf geparkt haben. Abgelesen an `agent_runs`: Eine Freigabe gehört zu einem Run und ein Run zu einer Person. Sie lässt Runs aus, in denen der Veröffentlicher für ein nicht verknüpftes Chat-Konto eingesprungen ist; solche Zeilen tragen stattdessen `asked_in_chat` (Plattform, Name und Bot), und ihre `triggered_by_*`-Felder sind leer | | `created_from`, `created_to` | Wann der Aufruf geparkt wurde, an beiden Enden einschließlich | | `oldest_first` | Voreingestellt wahr, und die Voreinstellung ist tragend — siehe oben: Nichts lässt einen Aufruf altern, also würde „neueste zuerst" die Zeile begraben, die am dringendsten gesehen werden muss | Jede Zeile nennt drei Dinge, die in anderen Tabellen liegen — den Agent, die Person, deren Run den Aufruf geparkt hat, und die Person, die entschieden hat. Der Agent und der Run sind Inner Joins, weil beide Fremdschlüssel kaskadieren, sodass eine Freigabe keinen von beiden überleben kann; die zwei Personen sind Outer Joins, denn eine Entscheidung muss die Löschung des Kontos ihrer entscheidenden Person überleben, und der Besucher eines Widgets ist von vornherein anonym. ### Die Run-Historie einengen { #narrowing-run-history } | Parameter | | | --- | --- | | `status` | Wiederholbar. `?status=failed&status=budget_exceeded` ist die Zeig-mir-die-Probleme-Abfrage, und die beiden sind genau deshalb getrennte Status, damit nach dem einen zu fragen nicht heißt, nach dem anderen zu fragen | | `surface` | Woher der Run kam | | `user_id` | Als wer der Run lief, was nicht immer der ist, der gefragt hat — die Runs eines Widgets tragen die Identität des Widget-Besitzers, weil der Besucher anonym ist | | `model_label` | Das Modell **so, wie der Run es festgehalten hat**, exakt verglichen. Nicht über den Modellkatalog aufgelöst: Die Spalte ist das, was geantwortet hat, und ein Profil, aus dem es kam, kann seither umbenannt oder gelöscht worden sein. Die Modellkarte des Dashboards zählt dieselben Zeichenketten, sodass „die Runs hinter diesem Balken" auf beiden Bildschirmen eine Menge ist | | `started_from`, `started_to` | An beiden Enden einschließlich, denn ein Bereichswähler übergibt ganze Tage | | `environment_id` | Runs auf der Version, die dieses Environment festpinnt. **Nie ein delegierter Run:** Die Version eines Delegates kommt von einem Pin, also wird die Spalte auf einem solchen bewusst nie geschrieben, und auf `production` einzuengen lässt jede Delegation fallen. Eine Oberfläche, die Delegationen einschließt, muss das sagen | | `exposure_id` | Runs, die über eine Bindung zugelassen wurden. Null für das Dashboard und die API | | `agent_version_id` | Runs, die einen eingefrorenen Spec ausgeführt haben — das „zeig mir die Zeilen hinter dieser Zahl" der Versionsleiste | | `rated` | `down` oder `up` — Runs, bei denen jemand eine vom Run erzeugte Message bewertet hat | | `order_by`, `descending` | `started_at` (die Voreinstellung, neueste zuerst), `duration`, `cost` oder `tokens` | **Jeder Filter engt die Zählung ebenso ein wie die Seite**, sodass `total` immer die Zeilen darunter beschreibt. Die Liste und die Zählung sind zwei Abfragen, und eine Bedingung, die nur eine von beiden erreicht, liest sich als Paging-Fehler statt als fehlende Klausel. `started_from` ist auch das, was diese Zählung mit dem Geld daneben in Einklang bringen lässt. Ungefenstert liest sie _alle Zeit_, während eine Ausgabenzahl einen Kalendermonat liest, sodass eine drei Jahre alte Organisation „8.412 Runs" neben „31,20 $" zeigte und die naheliegende Lesart des Paars um drei Jahre falsch war. Eine Zahl und eine Ausgabenzahl auf einem Bildschirm teilen sich ein Fenster, oder sie sagen, welches Fenster jede meint. Ein Wert außerhalb seines Typs wird mit einem 422 abgelehnt, statt gegen nichts abgeglichen zu werden: `status` und `surface` sind Zeichenkettenspalten, sodass `?status=complete` sonst mit einer leeren Seite antwortete — und eine leere Seite liest sich als _diese Woche ist nichts schiefgegangen_. `order_by` nimmt eine von vier Ordnungen statt eines Spaltennamens, aus demselben Grund plus einem weiteren: Ein aus einem Query-String zusammengesetztes `ORDER BY` ist eine Angriffsfläche für Injection. **Jeder Einzelne davon reist in der URL**, und das ist es, was eine Dashboard-Karte an ihre eigenen Zeilen übergeben lässt: `/runs?surface=mattermost&period=30d` öffnet **Aktivität** (Aktivität) mit der bereits gesetzten Facette und einer Zählung, die zu der Karte passt, die verlinkt hat. Bis #768 waren sie lokaler State, sodass die p95-Zahl die einzige Zahl auf dem Dashboard war, die die Runs dahinter erreichen konnte, und drei Karten überhaupt keinen Link trugen — es gab nichts Ehrliches, worauf man sie hätte zeigen lassen können. **Einschließlich dessen, welcher Tab offen ist.** `?tab=approvals` und `?tab=spend` öffnen die Queue und den Kostenbildschirm; die Run-Historie ist die Voreinstellung und wird nie geschrieben. Das ist die Adresse, die ein Link auf eine _Entscheidung_ braucht, und der Grund, warum es den Parameter gibt: Das „See all" der Freigabe-Karte und der Alert, der sagt, dass ein Run geparkt ist, mussten beide auf die Run-Historie zeigen, wo sich nichts entscheiden lässt (#934). Ein von einem Link genannter Tab wird **gegen das aufgelöst, was der Leser öffnen darf**. `approvals` ist auf `approvals:decide` gegated, sodass ein Link, der ihn trägt und jemanden ohne die Berechtigung erreicht, die Run-Historie öffnet statt einer Leiste, deren ausgewählter Tab keinen Inhalt hat. Den Tab zu wechseln schließt ein offenes Run-Detail und nimmt `?run=` mit. Ein Panel, das den Tab überlebt, der es geöffnet hat, steht neben einer Queue, mit der es nichts zu tun hat — und unterhalb von `lg` ersetzt es die Liste, sodass die Leiste lebendig blieb, während der Inhalt jedes Tabs verborgen war. **Die Dauer wird in SQL berechnet, über die ganze eingeengte Menge.** Das ist es, was von _„p95 ist 14,8 s"_ auf dem Dashboard zu **diesen Runs** führt. Eine Seite mit fünfundzwanzig Zeilen zu sortieren sortiert die falsche Menge, denn der langsamste Run eines Monats steht nicht in den Zeilen, die eine Seite mit den neuesten zuerst zufällig zurückgegeben hat. `cost` und `tokens` sind dieselbe Anordnung für Geld und Kontextgewicht. Ein Run ohne `ended_at` sortiert unter allen dreien **in beiden Richtungen als Letztes**. Er hat keine Dauer, er ist auch nicht der schnellste Run, und seine Kosten- und Token-Zahlen werden erst geschrieben, wenn er fertig ist — so sortiert, wie gespeichert, läse sich ein noch laufender Run als der billigste und leichteste in der Organisation. Wie lange ein _noch laufender_ Run schon läuft, ist eine andere Frage, und keine dieser Ordnungen beantwortet sie. **Aktivität** (Aktivität) zeigt diese Dauer auf drei Wegen, und alle drei führen zu derselben Abfrage: - Die Spaltenüberschrift **Took** ist ein Sortier-Bedienelement — wie die Überschrift Started daneben und wie jede sortierbare Überschrift im Produkt —, sodass ein Klick die Historie nach `duration` neu ordnet und nicht die fünfundzwanzig Zeilen auf dem Bildschirm. - Die **p95-Zahl des Dashboards verlinkt hierher**, nach Dauer sortiert über dasselbe Fenster: `?sort=duration` mit dem `started_from` / `started_to` des Zeitraums. So sind die Zahl und die Runs dahinter einen Klick voneinander entfernt — die Regel, der der Rest dieser beiden Seiten schon folgt, und die eine Dimension, in der sie es nicht taten (#210). **`rated=down` ist hier die Queue mit dem höchsten Signal** — die Antworten, von denen echte Menschen sagten, sie seien falsch, in ihren eigenen Worten. Eine Bewertung hängt an einer Message, also läuft dieser Join über `messages.run_id`: Zwei Runs in einer Unterhaltung behalten ihre eigenen Bewertungen, und deshalb gibt es diese Spalte statt eines Zeitfensters über den Faden. Es ist ein `EXISTS`, sodass ein Run, den drei Personen schlecht fanden, eine Zeile ist und nicht drei; und ein Run, den eine Person mochte, während eine andere ihn schlecht fand, passt auf **beides**, `up` und `down`, weil beides von ihm wahr ist. Das auf ein Urteil je Run zu reduzieren hieße, einen Konsens zu erfinden, den die Zeilen nicht festhalten. Dieselbe Tatsache reist auch ohne den Filter auf der Zeile mit. `AgentRunRead.down_rated` ist `true`, wenn irgendwer eine vom Run erzeugte Antwort unter null bewertet hat, für eine Seite in einer Abfrage berechnet, und es ist das, worauf die Run-Historie ein 👎 zeichnet. Es ist wie jeder Lesevorgang hier auf die Organisation des Aufrufers begrenzt — der negativ bewertete Run eines Nachbarn wird nie für einen anderen Tenant markiert. Der **Kommentar**, mit dem dieser Daumen hinterlassen wurde, wird im Run-Detail gelesen (`?run=`), nicht auf der Zeile. Es ist von Nutzern geschriebener Text über eine Unterhaltung, und ihn hinter das Detail zu legen ist die bewusste Linie zwischen einer Markierung, die jeder mit `runs:view` sieht, und den Worten, die sie erklären. Das ist der Join, für den `rated=down` gebaut wurde: Das Dashboard sagt, die Qualität sei um vier Punkte gefallen, und hier werden die Unterhaltungen gelesen, die das getan haben. Den Trend liest das Dashboard über `GET /api/v1/ratings/summary` (eine Schlagzahl-Aufteilung plus eine Reihe je Tag): `scope=org` unter `runs:view`, `scope=own` für die eigenen Unterhaltungen eines Mitglieds, dieselbe Scope-Regel und dasselbe Fenster-Vokabular wie `GET /stats/usage` (siehe [Berechtigungen](permissions.md)). Nur Zählungen — die Kommentare bleiben hinter dem Run-Detail oben. Die drei Zahlen in **Aktivität** (Aktivität) über den Tabs bleiben die der Organisation, einschließlich der Run-Zählung, auch wenn die Tabelle darunter auf einen Agent eingeengt ist. Eine Zählung je Agent neben dem Monat der Organisation wären zwei Fragen unter einem Etikett — und die Zählung je Agent ist diejenige, die Delegationen einschließt. **Eine verwaiste Delegation wird ohne ihr Handle gemeldet.** `parent_run_id` ist `ON DELETE SET NULL`, sodass das Löschen des Parents eine Zeile hinterlässt, die korrekt beginnt, zur Rechnung zu zählen — aber ein Fremdschlüssel kann nur seine eigene Spalte auf null setzen, und die gespeicherte `subagent_task_id` benennt dann ein Transkript, das mit dem Parent gegangen ist. `AgentRunRead` hält sie zurück, wann immer `parent_run_id` null ist, sodass keine Oberfläche ein Delegations-Handle anbietet, das nichts erreicht. ### Nach CSV exportieren { #exporting-to-csv } Alles, was die drei Tabs zeigen, lässt sich als CSV vom Bildschirm nehmen: die Zeilen, die jemand gegen eine Rechnung abgleicht, einem Finanzteam übergibt oder an ein Audit hängt. Eine Seite, die die Frage auf dem Bildschirm beantworten kann und außerhalb davon nicht, schickt Menschen in die Datenbank. | Frage | Antwort | | --- | --- | | `GET /runs/export` | Die Run-Historie, dieselben Filter wie `GET /runs` und dieselbe Voreinstellung nur oberste Ebene. `runs:view` | | `GET /approvals/export` | Der Freigabe-Datensatz, dieselben Filter wie `GET /approvals`. `approvals:decide` | | `GET /spend/export` | Die Ausgaben-Aufschlüsselung je Agent, dasselbe Fenster wie `GET /spend`. `runs:view` | | `GET /audit/export` | Die Audit-Spur über ein Fenster, CSV oder JSONL (`?fmt=`). `audit:read` | Der Audit-Export ist der einzige, der auch **JSONL** anbietet (`?fmt=jsonl`), ein JSON-Objekt pro Zeile, weil eine Audit-Spur ebenso oft von einer Log-Pipeline eingelesen wie in einer Tabelle geöffnet wird; beide beschreiben dieselben Einträge, mit `details` als JSON-String in der CSV-Zelle und als verschachteltes Objekt in den Zeilen. Er liefert genau die Felder, die das Lesemodell von `GET /audit` zeigt — die gespeicherte `ip_address` steht nicht auf diesem Tab und daher auch nicht im Export — und hält wie jeder Export hier seinen eigenen Abruf in der Spur fest (`audit.export`, mit Fenster, Format und Zeilenzahl). Der Ausgaben-Export trägt nur die Zahlen des Fensters — `cost_usd`, `run_count` und `partial_run_count`. `month_to_date_usd` und `monthly_cap_usd` des Spend-Tabs bleiben weg: Sie lesen den Kalendermonat, während `cost_usd` das Fenster des Exports liest, und zwei Dollarspalten auf zwei Zeitbasen in einer heruntergeladenen Datei werden von einem Leser, der den Unterschied nicht sieht, quer aufsummiert. Eine heruntergeladene Datei trägt eine Zeitbasis, nämlich das Fenster, nach dem gefragt wurde. Ein Export ist ein Massen-Lesevorgang und keine Schaltfläche, und er beantwortet sechs Fragen, die die Listen-Routen nicht beantworten müssen: - **Mandantentrennung.** Jeder Export trägt das Gate des Tabs, aus dem er kommt, und jeder Lesevorgang ist auf die Organisation des Aufrufers begrenzt — die Zeilen eines Nachbarn erreichen ihn nie, einschließlich einer Zeile, die dem Aufrufer in einer Organisation gehört, die nicht die ist, aus der er fragt. Die zwei auf `runs:view` legen in der Abfrage zusätzlich einen **Boden von `Scope.OWN`** ein: Ein Aufrufer, dessen `runs:view` weniger als die ganze Organisation erreicht, exportiert nur seine eigenen Zeilen, `WHERE user_id = `, und eine `user_id`, die er übergibt, wird mit seiner eigenen überschrieben, statt sie zu weiten. Keine eingebaute Rolle hält `runs:view` bisher unterhalb von `all`; der Boden liegt bereit für den Zeitpunkt, an dem die Scope-Entscheidung für Member und Viewer fällt. - **Größe.** Ein Export hat von Natur aus keine Obergrenze, also bekommt er eine per Design. Der **Datumsbereich ist verpflichtend** — eine Anfrage ohne beide Enden wird abgelehnt — und die Treffermenge ist **auf 10.000 Zeilen gedeckelt**, darüber wird die Anfrage mit einer Nachricht abgelehnt, die die Anzahl nennt und dem Aufrufer sagt, er solle den Bereich einengen. Niemals eine stille Kürzung: Eine beschnittene CSV ist schlimmer als eine abgelehnte, denn eine Tabellenkalkulation summiert, was ankommt. Das Cap ist es, was den Body in einem Durchgang bauen und den Audit-Eintrag committen lässt, bevor die Antwort hinausgeht, statt ihn über eine gehaltene Verbindung zu streamen. - **Teilweise Kosten.** `cost_is_partial` ist im Run-Export eine eigene Spalte und `partial_run_count` im Ausgaben-Export, sodass eine Untergrenze eine Tabellensumme überlebt. Ein Run, dessen einziges Modell unbepreist war, exportiert seine echten `cost_usd` von `0` neben `cost_is_partial=true` — nie eine nackte `0`, die ein Leser für kostenlos hält. - **Delegierte Runs.** Der Run-Export nimmt voreingestellt nur Zeilen der obersten Ebene, genau wie die Liste, sodass die Summe von `cost_usd` die Rechnung ergibt und nicht das Doppelte. Die Haltung steht in der Datei und nicht nur hier: Jede Zeile trägt eine Spalte `parent_run_id`, leer für einen Run, den jemand gestartet hat, und gesetzt für eine Delegation, sodass ein Leser, der sich für `include_delegations` entscheidet, sehen kann, welche Zeilen bei einer Gesamtsumme doppelt zählen würden. - **Personenbezogene Daten.** Jeder Export liefert genau die Identität, die sein Tab ohnehin zeigt. Die Run-Tabelle zeigt eine `user_id` und keinen Namen, also liefert der Run-Export allein die Id — eine CSV, wer was ausgeführt hat, mit aufgelösten Namen, ist die Tabelle je Person, die Entscheidung 3 des Designs für **Aktivität** (Aktivität) abgelehnt hat, angekommen als Download. Die Freigabe-Queue löst die auslösenden und entscheidenden E-Mail-Adressen auf dem Bildschirm bereits auf, also behält der Freigabe-Export sie. Er enthält auch den Anzeigenamen oder das Handle eines Chat-Fremden, wie die Queue sie zeigt, in drei Spalten: `asked_in_chat_platform`, `asked_in_chat_account` und `asked_in_chat_bot`. In einer solchen Zeile sind beide E-Mail-Spalten leer, weil der Veröffentlicher nicht gefragt hat. - **Audit.** Jeder Export schreibt einen `audit_log`-Eintrag — ein privilegierter Massen-Lesevorgang, jetzt billig festzuhalten und später unmöglich zu rekonstruieren. Er nennt das Fenster, die angewendeten Filter und die Zeilenzahl, nie den Anfrage-Body. ### Ein gepinnter Delegate bewegt sich nicht von allein { #a-pinned-delegate-does-not-move-on-its-own } Ein Delegate ist auf eine Version gepinnt, sodass ein Fix, den sein Autor ausliefert, für seine Aufrufer nichts ändert, bis jemand den Parent gegen den neuen Pin neu veröffentlicht. Das ist dieselbe Garantie, die das Veröffentlichen hier überall gibt, und sie schneidet in beide Richtungen: Ein in einem Delegate behobener Fehler ist ein Fehler, der in jedem Parent, der sich nicht bewegt hat, weiterlebt. Der Builder ist der Ort, an dem das sichtbar gemacht wird — er vergleicht jeden Pin mit dem, was der Delegate jetzt veröffentlicht, und bietet an, ihn zu verschieben —, denn Veralterung, die nichts sichtbar macht, ist ein festgefrorener Fehler. Ein Pin, dessen Version es nicht mehr gibt, **lässt den Run scheitern** und nennt den Delegate; nie ein leises Zurückfallen auf die aktuelle Version. **Einen Delegate zu archivieren hört auf, ihn antworten zu lassen, auch als jemandes Delegate.** Ein Pin auf einen bereits archivierten Agent wird beim Veröffentlichen abgelehnt, und ein Agent, der nach dem Pinnen archiviert wurde, lässt den Run seines Aufrufers namentlich scheitern — sonst würde einen Agent außer Dienst zu nehmen ihn ausgerechnet an der einen Stelle, an der niemand hinsieht, unbegrenzt weiterlaufen lassen, und der Autor, der ihn ausgemustert hat, erführe es nie. ### Step-Limits { #step-limits } Die andere Art von Ausreißer ist eine Tool-Schleife: billig je Aufruf, und sie wird nie fertig. Ein Budget rechnet dafür nur ab. `max_steps` deckelt, wie viele Modellanfragen ein Run stellen darf, und ist das, was sie tatsächlich stoppt. Der Schleifenwächter stoppt die meisten Schleifen früher und sagt, warum. Jeder Agent hat ihn, standardmäßig eingeschaltet. Ruft ein Run ein Tool viermal in Folge mit denselben Argumenten auf oder durchläuft er einen Zyklus aus zwei oder drei Aufrufen dreimal, wird dem Modell gesagt, dass es in einer Schleife steckt und anders vorgehen soll. Laufen die Aufrufe nach dieser Warnung wieder in eine Schleife, endet der Run als `loop_stopped`, mit einer Begründung, die die Tools nennt - nie ihre Argumente. Aufrufe mit anderen Argumenten sind nie eine Schleife, und nur die Aufrufe des Runs selbst zählen. Der Builder setzt beide Schwellen oder schaltet den Wächter aus, neben `max_steps` (`loop_guard` im Spec). ### Run-Reviewer { #run-reviewers } Ein langer Run driftet: Anweisungen verblassen, Behauptungen ohne Quelle häufen sich, der Agent entfernt sich von der Anfrage. Ein Agent kann bis zu fünf Reviewer haben (`reviewers` im Spec, _Run-Reviewer_ im Builder neben den Limits), jeder ein zweites, meist günstigeres Modell mit einem Anliegen. Alle `every` Modell-Requests liest ein Reviewer die letzten `window` Tokens des Runs und lässt ihn entweder laufen oder schickt eine kurze Korrektur, die das Modell des Runs beim nächsten Request mit dem Namen des Reviewers erhält. Die Run-Ansicht listet jede Korrektur und wer sie geschickt hat. Die Aufrufe eines Reviewers werden vor dem Absenden gegen das Budget des Agents und der Organisation geprüft und auf dem Run gebucht, bepreist als das Modell des Reviewers. Ein wegen des Budgets abgelehnter Aufruf wird übersprungen, und ein Reviewer, der scheitert, wird geloggt; keines von beiden lässt den Run scheitern. Ein Agent ohne Reviewer macht keine zusätzlichen Modellaufrufe. ### Code-Modus { #code-mode } Ein Agent, der Datensätze abarbeitet - zehn Zeilen aktualisieren, dann zusammenfassen -, zahlt für jeden abhängigen Aufruf einen Modell-Turn, und jeder Turn schickt den ganzen Kontext erneut. Mit eingeschaltetem _Code-Modus_ (`code_mode` im Spec, neben den Limits im Builder) werden die Tools des Agents in ein einziges `run_code`-Tool gefaltet: Das Modell schreibt ein kurzes Python-Skript, das in der Monty-Sandbox läuft und die Tools in Schleifen und parallel aufruft, bis zu `max_tool_calls` pro Skript. Höchstens vier Aufrufe eines Skripts laufen gleichzeitig, der Rest wartet, sodass ein Skript, das Hunderte Sandbox-Aufrufe auffächert, sie nicht alle zugleich öffnet. Jeder Aufruf aus einem Skript durchläuft dieselben Guardrails, dasselbe Budget und dieselben Berechtigungsprüfungen wie ein direkter Aufruf. Ein Tool, das eine Freigabe braucht, wird nie hineingefaltet: Es bleibt ein normaler Aufruf, und der Run hält wie immer für die Person an. Ebenso Rückfragen, Delegationen und Tools, deren Aufruf das ist, was die Person sieht (Entwürfe, Diagramme, Zeichnungen). Die Run-Ansicht listet jedes Skript und jeden Aufruf, den es gemacht hat. ### Berichte { #reporting } Ein Run, der sich nicht bepreisen ließ — ein Modell, das `genai-prices` nicht kennt —, wird mit einer Warnung bei null festgehalten, und die Summe wird als **Untergrenze** gekennzeichnet, statt geraten zu werden. Die UI zeigt das als ein `+` neben der Zahl. **Cache-Lese- und Schreibvorgänge werden getrennt geführt.** Die Eingabetokens eines Runs enthalten die, die der Anbieter aus seinem Prompt-Cache bedient hat, und die, die er hineingeschrieben hat, und alle drei werden getrennt bepreist - bei Claude Sonnet 5.5 kostet ein Cache-Lesevorgang 5 % des Eingabepreises und ein Schreibvorgang 125 %. Jeder Run und jede Antwort speichert `cache_read_tokens` und `cache_write_tokens` neben der Summe, die Kosten werden aus dieser Aufteilung berechnet, und die Run-Ansicht und die Kosten unter jeder Antwort zeigen sie - damit lässt sich ein Run mit der Konsole des Anbieters abgleichen. **Die Kosten eines Runs enthalten seine Delegationen.** Der Run, den jemand gestartet hat, trägt das ganze Ledger, Delegates eingeschlossen; die eigene Zeile einer Delegation teilt dasselbe Geld nach Agent auf, und die Run-Ansicht sagt, dass ihre Delegationen schon in der Zahl darüber stecken. Beide zu addieren zählt die Delegationen doppelt. **Ein Prompt-Cache, der nicht mehr trifft, wird markiert.** Caching lohnt sich nur, solange der Anfang jedes Requests gleich bleibt; eine umgestellte Tool-Liste oder ein Zeitstempel in den Anweisungen lässt den Anbieter den vollen Preis für Tokens berechnen, die er im Cache hatte. Jeder Agent liest bei jeder Antwort die Cache-Zahlen des Anbieters, und ein Request, dessen gecachter Präfix eingebrochen ist, wird im Request-Wasserfall des Runs markiert - _Cache gebrochen_, solange der Anbieter ihn noch hätte halten sollen, _Cache abgelaufen_, wenn die Aufbewahrung vorbei war - mit der Zahl der gecachten Tokens, die er nicht zurückgelesen hat, und mit Agent und Modell geloggt. Der erste Request eines Turns wird mit dem verglichen, was der vorige Turn gecacht hat, wenn derselbe API-Worker beide bedient. Die Zahlen werden an jedem Run gespeichert, daher summiert **Runs → Spend**, wo der Cache nicht gehalten hat: eine Karte _Prompt-Cache_, eine Zeile pro Agent und Modell, mit Busts, Abläufen und den gecachten Tokens, die nicht zurückgelesen wurden, im Zeitraum - ein Maß für den Fehlgriff, nicht für erneut Berechnetes, da auch eine Komprimierung den gecachten Prompt verkleinern kann. Solange jeder Cache gehalten hat, erscheint die Karte nicht. ## Freigaben { #approvals } Ein Tool, das der Autor des Agents hinter ein Gate gestellt hat, parkt den Run und wartet auf eine Person. Nichts steht hinter einem Gate, bis jemand darum bittet: Ein Gate, das niemand gewählt hat, parkt Runs in Kanälen und Routinen, in denen niemand da ist, um zu antworten. Die Auflösung geht vom Spezifischsten aus: 1. die eigene Übersteuerung des Tools, falls es eine hat 2. der `approval`-Modus der Capability (`required` | `never` | `default`) 3. keine Freigabe, wenn beide `default` sagen `side_effecting` entscheidet hier nichts mehr. Der Builder zeigt es neben der Wahl, sodass ein Autor die Tools, die auf die Welt einwirken, zuerst sieht. Der Builder benennt das Ergebnis in Worten, statt die Regel zu beschreiben, denn eine Regel, die der Leser im Kopf ausführen muss, ist eine Einstellung, an die sich niemand herantraut. ```mermaid sequenceDiagram autonumber participant M as Model participant G as ApprovalGate participant Q as Approvals queue participant P as A person M->>G: call a gated tool G->>Q: park it, with the arguments G-->>M: run ends `awaiting_approval` P->>Q: reads the arguments, decides alt approved Q->>G: resume with the arguments that were read G->>M: execute those, not what it proposes now else rejected Q->>G: resume, replaying the denial G->>M: a refusal it can relay, not a crash else expired Q--xM: the run ends `cancelled`. No further model request end ``` Fünf Eigenschaften, die man kennen sollte: - **Ein geparkter Run ist fortsetzbar.** Seine Message-Historie ist gespeichert, sodass die Entscheidung auf die Unterhaltung angewendet wird, zu der sie gehört, statt neu zu beginnen. - **Ein geparkter Run überlebt ein Neuladen und sagt das.** Das Transkript speichert den Aufruf, auf dem der Run stehen blieb, als `awaiting_approval` statt als `running`, sodass das erneute Öffnen der Unterhaltung den wartenden Schritt weiterhin zeigt — und `GET /runs/{id}/parked` antwortet mit den ausstehenden Aufrufen (die zu entscheidende Freigabe, das Tool, seine Argumente), und so baut der Chat das Freigabe-Panel wieder auf, das der Live-Frame `tool_approval_required` demjenigen gab, der gerade zusah. Er antwortet für einen Run, der nicht geparkt ist, leer, und er ist wie die Queue auf `approvals:decide` gegated, weil seine Zeilen zur Entscheidung angeboten werden ([#601](https://github.com/vstorm-co/agenticos/issues/601)). Der Schritt liest sich nicht als ewig wartend: Ein Fortsetzen erledigt ihn mit dem, was der Aufruf zurückgab, ein Ablauf mit dem Hinweis auf das Zeitlimit. - **Eine Fortsetzung sagt, was sie getan hat.** `POST /runs/{id}/resume` antwortet mit den Tool-Aufrufen, die die Fortsetzung gemacht hat, der Reihe nach, jeder mit dem, was zurückkam — und das Transkript hält sie fest, ob sie zu einer Antwort gelangte oder nicht. Beide Hälften fehlten früher, und eine Freigabe konnte eine unbegrenzte Menge Arbeit verbergen: Der Agent lief innerhalb der Resume-Anfrage statt auf dem Socket, den die Unterhaltung streamt, also kündigte nichts seine Aufrufe an, und der Transkript-Schreibvorgang wurde für ein Segment ohne Antwort übersprungen. Ein Run, der eine Datei las und dann darum bat, einen zweiten Befehl auszuführen, zeigte zwischen den beiden Freigaben nichts und hielt auch nichts fest. - **Und was der freigegebene Aufruf selbst zurückgab, landet auf dem Schritt, der freigegeben wurde.** Es kommt getrennt von den eigenen Aufrufen der Fortsetzung an (`settled`, nicht `steps`), weil es von der Ausführung gemacht wurde, die geparkt hat: Das Fortsetzen erzeugt seine Rückgabe ohne den Aufruf, zu dem sie gehört, also schließt es eine bereits geschriebene Zeile, statt eine neue zu öffnen. Es als Schritt festzuhalten würde denselben Befehl zweimal in den Turn setzen; es überhaupt nicht festzuhalten — was geschah, bis es geschah — machte den einen Aufruf, den jemand bewusst geprüft hat, zu dem einen Aufruf ohne Ausgabe irgendwo. - **Er bleibt fortsetzbar, wenn das Fortsetzen scheitert.** Ein Run wird auf der Version fortgesetzt, auf der er geparkt hat, und der Spec dieser Version kann seither aufgehört haben zu bauen — ein von einer Bindung benanntes Secret gelöscht, ein Modellprofil entfernt, eine Capability in einem Deploy weggefallen, eine MCP-Verbindung nicht mehr geteilt. Der Spec wird zusammengesetzt, bevor der Run die Freigabe-Queue verlässt, sodass eine Ablehnung dort den _Versuch_ ablehnt: Die Entscheidung steht, und das Fortsetzen funktioniert wieder, sobald der Spec es tut. Ein im Chat gestellter Run, den niemand fortgesetzt hat, wird vom Sicherheitsnetz etwa eine Stunde nach seiner letzten Entscheidung beendet. - **Wer einen im Chat gestellten Run fortsetzt.** Die Entscheidung einer Person in der Konsole, im Web-Chat oder über die API setzt den Run in seinem Chat fort, als sie, und die Antwort wird dort gepostet; die Entscheidung eines Schlüssels wird festgehalten und setzt nichts fort. Eine Fortsetzung kann verloren gehen oder abgelehnt werden, dann wartet der Run in `awaiting_approval` ohne etwas Offenes. Das stündliche Sicherheitsnetz beendet einen solchen Kanal-Run `STRANDED_AFTER` (eine Stunde) nach seiner letzten Entscheidung und sagt es dem Chat einmal - nicht, wenn die Entscheidung mehr als 7 Tage alt ist, und nicht, wenn der Chat es schon erfahren hat. Es betrachtet nur Kanal-Runs. Siehe [Außerhalb des Chats entschieden](channels.md#decided-outside-the-chat). - **Eine entschiedene Freigabe kann nicht zweimal entschieden werden.** Die zweite Entscheidung wird abgelehnt — einschließlich einer Entscheidung, die eine Sekunde nach dem Ablauf-Sweep ankommt, der den Aufruf genommen hat. - **Ein geparkter Aufruf wird durch Zeitablauf verweigert, sobald er `APPROVAL_EXPIRY_HOURS` überschreitet**, und der Run dahinter wird beendet statt für immer geparkt zu bleiben. Der Status ist `expired` mit einem `decided_by_user_id` von null, und das ist es, was in der Verantwortungsspur einen Ablauf von einer Zurückweisung unterscheidet. Ein im Chat gestellter Run sagt außerdem dem Chat einmal, dass die Anfrage abgelaufen ist. Die Aktivität-Seite zeigt weiterhin das **Alter** der ältesten Wartezeit, denn eine Queue innerhalb ihres Ablauffensters ist die, auf der jemand noch handeln kann. - **`required` funktioniert auf jeder Capability**, nicht nur auf nebenwirkungsbehafteten. „Das liest nur, aber in meiner Organisation gibt das trotzdem jemand frei" ist eine echte Entscheidung und lässt sich ausdrücken. - **Außer auf einem Tool, das der Modell-Provider ausführt, wo es beim Veröffentlichen abgelehnt wird.** Das Gate umschließt die _Tool-Ausführung_, und das ist der einzige Ort, an dem sich ein Aufruf halten lässt, sodass ein natives Fetch oder eine native Suche — auf der Seite des Providers ausgeführt — es nie erreicht, und eines davon zu gaten würde die Queue leer lassen, während der Agent ohne Freigabe handelt. Welche Konfigurationen welche Tools übergeben, erklärt die Capability selbst (`provider_executed` in ihrem `register(...)`), sodass die Ablehnung jede Capability abdeckt, die eine vom Provider ausgeführte Methode bekommt, statt nur die, die ein Validator zufällig kannte ([#857](https://github.com/vstorm-co/agenticos/issues/857)). Wählen Sie eine Methode, die dieses Deployment selbst ausführt, oder lassen Sie die Freigabepflicht fallen; beides sind legitime Agents, und welches gewollt ist, ist keine Entscheidung, die stellvertretend für den Autor getroffen wird. - **Und eine Version, die veröffentlicht wurde, bevor es diese Ablehnung gab, läuft nicht.** Nichts validiert eine eingefrorene Version erneut — ein Run lädt seinen gespeicherten Spec und setzt ihn zusammen —, also läuft dieselbe Prüfung erneut, wenn der Agent gebaut wird, und lehnt ab, statt die Methode still zu tauschen, damit das Gate funktioniert. Der Preis ist real und gewollt: Ein Agent, der so gelaufen ist, hört auf, mit einer Nachricht, die sagt, was zu ändern ist. Was aufhört, ist ein Agent, dessen Betreiber um eine Freigabe bat, um die nie jemand gebeten wurde. - **Ein geparkter Aufruf kann nennen, wofür seine IDs stehen.** Ein Tool kann seinen eigenen Aufruf beschreiben: die Schreibvorgänge in Tabellen nennen den Namen der Tabelle. Ein Datensatz wird nie genannt, denn das Label eines Datensatzes ist der Text einer seiner Zellen, und die Freigabe bewahrt es auf. Die Namen werden als die Person des Runs aufgelöst, wenn der Aufruf parkt, sodass nichts genannt wird, was diese Person nicht sehen darf, und an der Freigabe als `context` gespeichert. Die Karte im Chat und die Seite Approvals beginnen mit einem daraus gebauten Satz, etwa „Share the view Closed tickets of Support with everyone who may read the table“, über den Argumenten, die weiterhin das sind, was ausgeführt wird. Der Satz wird über die stabile ID des Tools gefunden, sodass ein Tool, das ein Binding umbenannt hat, ihn behält. Eine fehlgeschlagene oder langsame Beschreibung parkt den Aufruf ohne sie, und ein Aufruf, den ein fortgesetzter Run bereits entschieden hat, wird nicht erneut beschrieben. - **Ein Modellschritt kann mehrere Aufrufe parken.** Ein Modell, das mit zwei nebenwirkungsbehafteten Aufrufen zugleich antwortet — „mail den Kunden und den Kundenbetreuer an" — parkt beide, jeder mit einer eigenen Freigabe-Zeile, die für sich entschieden wird. Die Zeilen werden geschrieben, wenn der Run parkt, statt beim Gaten jedes Aufrufs, weil die Aufrufe nebenläufig laufen und die Datenbank-Session des Runs nicht nebenläufigkeitssicher ist ([#169](https://github.com/vstorm-co/agenticos/issues/169)). ### Wie sehr eine Unterhaltung gefragt werden will { #how-much-one-conversation-wants-to-be-asked } Die Freigabekarte bietet auch **Dieses Tool in diesem Chat erlauben** und **Dieses Tool für diesen Agenten immer erlauben**. Die Auswahl gilt nur für das benannte Tool und Konto, die zustimmende Person und die veröffentlichte Agentenversion. Sie erfordert dieselbe Berechtigung und Organisationseinstellung wie die Freigabe aller Aktionen. Sie autorisiert weder Delegierte noch unbeaufsichtigte Ausführungen. Eine neue Agentenversion erfordert eine neue Auswahl. Die Auswahl wird auf dem Server gespeichert. **Chat controls → Preferences → Gespeicherte Tool-Freigaben → Erneut fragen** widerruft eine Zustimmung, ohne die ursprüngliche Entscheidung zu löschen. Vor jeder automatischen Freigabe werden Mitgliedschaft, Freigabeberechtigung und Organisationsrichtlinie erneut geprüft. Der Modus zum Nachfragen bei jeder Aktion hat Vorrang. Jede automatische Freigabe protokolliert weiterhin die zustimmende Person und die tatsächlichen Aufrufargumente. Die Regel oben gehört dem Agent, wird beim Veröffentlichen und je Tool entschieden, und das ist der richtige Ort dafür: Sie ist eine Aussage darüber, was der Agent _ist_. Was sie nicht ausdrücken kann, ist die Stimmung einer Sitzung — wer zwanzig Turns lang mit einem Agent arbeitet, der drei Tools gatet, beantwortet in jedem Turn dieselben drei Fragen, und der einzige Ausweg war, den Agent neu zu veröffentlichen und ihn für alle dauerhaft zu ändern, um einen Nachmittag zu retten ([#925](https://github.com/vstorm-co/agenticos/issues/925)). Eine Chat-Sitzung trägt also einen **Freigabe-Modus**, auf dem Send-Frame neben der Modell-Übersteuerung und in den Run hineingelesen: | Modus | Was er tut | | --- | --- | | **Follow the agent** (Voreinstellung) | Der Spec entscheidet. Genau das Verhalten, das es vor dem Bedienelement gab, und das, was ein Client bekommt, der nichts sendet | | **Approve everything** | Stehende Zustimmung für diese Unterhaltung: Jeder gegatete Aufruf wird gewährt, ohne zu parken — und jeder schreibt trotzdem seine Zeile | | **Ask about everything** | Gate jedes Tool, das der Agent erreichen kann, einschließlich derer, die der Spec ungegatet ließ, und derer, die keiner Capability gehören | Vier Dinge machen es zu einer Sitzungseinstellung statt zu einem Loch im Modell: - **Es wird abgelehnt, nie herabgestuft.** Einem Aufrufer, der nicht verzichten darf, wird das gesagt; der Turn läuft nicht still dem Spec folgend weiter, denn wer glaubt, die Fragen abgeschaltet zu haben, und dann einen geparkten Run vorfindet, dem wurde das Gegenteil dessen gesagt, was geschehen ist. Die Prüfung liegt in `AgentRunnerService.prepare`, dem einen Trichter, den ein frischer und ein fortgesetzter Run teilen, statt an einem Socket, den ein Aufrufer vergessen könnte. - **Ein Verzicht braucht `approvals:decide` und die Erlaubnis der Organisation.** Eine stehende Zustimmung _ist_ die Entscheidung, zu deren Festhalten die Freigabe-Queue existiert, und `member` und `builder` führen Agents aus, ohne sie zu halten — ohne die Berechtigungsprüfung gewährte sich also der alltägliche Chat-Nutzer mit einem Klick die Autorität, die die API ihm einen Endpunkt weiter verweigert. Der eigene Schalter der Organisation (`chat_may_waive_approvals`, für neue Organisationen voreingestellt ein, geändert von jemandem, der `approvals:decide` hält) ist die Obergrenze: Ohne ihn ist das bewusste Gate eines Builders auf `send_email` in jeder Unterhaltung einen Klick von nichts entfernt und das Modell je Tool nur beratend. - **Kein Channel heißt weiterhin nein.** Nur eine Web-Chat-Sitzung darf verzichten. Ein Zeitplan, ein Webhook, ein Embed und ein Channel werden alle abgelehnt, denn `ApprovalGate` lehnt einen Run, bei dem niemand zu fragen ist, bereits ab, und eine stehende Zustimmung darf nicht der Weg daran vorbei werden. - **Jeder verzichtete Aufruf wird trotzdem festgehalten.** Die Zeile wird als `approved` geschrieben, benennt das Konto, das zugestimmt hat, mit `decided_via = "standing"` — und der Freigabe-Datensatz sagt das in Worten neben dem Namen. Die Zeile wegzulassen würde einen verzichteten Run von einem Agent ununterscheidbar machen, der nie gegatet war, und das wäre diese ganze Spur, die still aufhört, eine zu sein. Niemand hat diese Argumente gelesen, bevor sie liefen; die Zeile ist der Ort, an dem sie jemand hinterher liest. **Ask about everything verschärft die Regeln der Sitzung.** Für die Auswahl dieses strengeren Modus ist keine besondere Berechtigung nötig. Der Modus erfasst MCP-Aufrufe, die der Runner ausführt, obwohl diese Tools nicht von Capabilities deklariert werden. Erlaubnislisten an Verbindung und Agent-Bindung bestimmen, welche MCP-Tools verfügbar sind; was sie für eine Person zurückhält, ist die eigene `approval`-Regel der Bindung - `writes` oder `all` -, die der Standardmodus **Follow the agent** wie die einer Capability anwendet (siehe [welche Aufrufe auf eine Person warten](mcp.md#which-calls-wait-for-a-person)). ### Eine Freigabe innerhalb eines Sprachanrufs { #an-approval-inside-a-voice-call } Ein [Sprachanruf](voice.md#approvals-in-a-call) kann nicht pausieren und auf die Queue warten, also werden seine Freigaben im Anruf entschieden, von der Person im Anruf: - Die Anfrage wird sofort in die Queue geschrieben, und der Anruf zeigt das Tool, die exakten Argumente, mit denen es laufen wird, und was vom Anrufer verstanden wurde. Freigeben führt diese Argumente aus, einmal. - Entschieden wird sie **nur über den Anruf**, und nur von einem Anrufer, der `approvals:decide` hält. `POST /approvals/{id}` weist eine Anfrage ab, die zu einem laufenden Anruf gehört, und der Sweep für liegengebliebene Freigaben unten überspringt Voice-Runs. - **60 Sekunden** ohne Entscheidung lassen sie ablaufen, ohne entscheidende Person - dasselbe null, das der Sweep schreibt. Ein Anruf, der aus irgendeinem Grund endet, lässt alles ablaufen, worauf er noch gewartet hat. - Ein Mitglied ohne `approvals:decide` bekommt das klar gesagt, und das Tool wird abgelehnt. Ein Besucher auf einem Widget oder einer gehosteten Seite sieht nie ein Tool, das eine Freigabe braucht: Es wird aus seinem Anruf weggelassen. `approval_mode` folgt dem Spec des Agents, wie im Text-Chat, und jede Entscheidung erreicht den Audit-Trail wie jede andere. ### Eine Entscheidung, die niemand trifft { #a-decision-nobody-makes } Eine Freigabe wartet auf eine Person, und manche warten ewig: Die prüfende Person ist gegangen, das Tool wurde an einem Freitag angefragt, niemand wusste, dass die Entscheidung ihm zufiel. Nichts in einem Anfragepfad kann eine beenden — die ganze Prämisse ist, dass keine Anfrage kommt —, also verweigert ein stündlicher Sweep durch Zeitablauf alles, was jenseits von `APPROVAL_EXPIRY_HOURS` noch aussteht (drei Tage voreingestellt, was ein Wochenende überspannt). !!! warning "Es geht um den Run, nicht um die Zeile" Eine ausstehende Freigabe hält ihren Run unbegrenzt in `awaiting_approval`: Arbeit, die weder fertig ist noch fertig werden wird. Ein solcher Run steht in der Historie und im Alter der ältesten Wartezeit auf dem Dashboard, also folgt der Sweep jedem abgelaufenen Aufruf hinab bis zum Run dahinter und beendet ihn, `cancelled` — niemand kam zurück, und was er vor dem Parken ausgegeben hat, bleibt stehen. Drei Dinge, die er bewusst nicht tut: - **Er setzt den Run nicht fort.** Ein _zurückgewiesener_ Aufruf wird durch Fortsetzen erledigt: Die Verweigerung wird nachgespielt und der Agent arbeitet bis zu einer Antwort weiter. Das ist eine Modellanfrage gegen die eigenen Schlüssel der Organisation, und eine solche nach Zeitplan zu stellen, für einen Run, auf den niemand wartet, sind keine Kosten, die ungefragt entstehen sollen. - **Er beendet keinen Run, dessen Aufruf noch in seinem Fenster liegt.** Ein Run parkt auf allen seinen offenen Aufrufen zugleich, also wird er erst beendet, wenn keiner von ihnen mehr aussteht. - **Er benennt keine entscheidende Person.** `decided_by_user_id` bleibt null und ebenso der Akteur des Audit-Eintrags, denn das ist die Tatsache, die festgehalten wird. Null bedeutet dort die Plattform nach Zeitplan, und nichts anderes kann eine erzeugen. Das ist der einzige Lesevorgang in der Codebasis, der jede Organisation überschreitet, aus dem Grund, dass ein Zeitplan keinen Tenant hat, auf den er begrenzt sein könnte. Jeder Schreibvorgang, den er macht, liegt weiterhin in der Organisation der jeweiligen Zeile. ### Ein Run, dessen Prozess gestorben ist { #a-run-whose-process-died } Der andere Zustand, den nichts im Prozess je auflösen wird. Die Zeile eines Runs wird als `running` committet, bevor sein Modell aufgerufen wird ([#12][12-issue]), sodass ein Worker, der mitten im Run getötet wird — OOM, ein Deploy, der nicht drainiert —, eine dauerhafte Zeile hinterlässt, die nichts mehr zu Ende bringt: für immer in **Aktivität** (Aktivität), und sie blockiert jeden Zeitplan, für dessen Trigger sie der verknüpfte Run war. Während sein Modell arbeitet, stempelt der Prozess, der einen Run ausführt, ihn alle `RUN_HEARTBEAT_SECONDS` als lebendig. Ein minütlicher Sweep beendet alles, was `STALE_RUN_REAPED_AFTER_SECONDS` lang ohne Lebenszeichen noch `running` ist — zwei Minuten voreingestellt, null schaltet es ab — als **`failed`** und sagt es den offenen Konsolen, sodass ein Chat nicht mehr "Working" über einem Zug zeigt, den niemand beantwortet, und anbietet, erneut zu fragen. Den Run eines Sprachanrufs überlässt er dem Reaper der Anrufe, der den Anruf auflegt. Niemand hat diesen Run gestoppt, die Infrastruktur hat es getan, und ein Betreiber, der die Run-Historie nach Problemen filtert, ist genau der, der das sehen sollte. Der Fehler auf der Zeile ist der eigene Satz des Sweeps. Der Prozess, der mehr wusste, ist gestorben. Das Alter eines Runs ist hier sein **letzter Übergang**, nicht sein erster Start. Ein Fortsetzen behält das ursprüngliche `started_at` — der Run spannt sich über beide Segmente —, sodass ein Run, der Tage nach dem Parken freigegeben wurde, ab dem Moment altert, in dem sein Nachspielen begann, statt ab einem Start, der ihn mitten im Nachspielen abernten ließe. Die Obergrenze muss so oder so nicht exakt sein, denn ein lebender Run, den der Sweep trotzdem umschaltet, schaltet sich selbst zurück: Sein eigener terminaler Schreibvorgang landet später und gewinnt. Was ein abgeernteter Run nicht wiederherstellen kann, sind seine **Ausgaben**. Das Ledger ist mit dem Prozess gestorben, also behält die Zeile die Nullen, mit denen sie geöffnet wurde, statt eine Zahl zu bekommen, die jemand gegen eine Rechnung abgleichen würde. Und niemand bekommt Post. Die Fehlschlag-Benachrichtigung reist auf `finish` mit, das den Agent und seinen Spec zur Hand hat; ein Sweep hat weder noch. [12-issue]: https://github.com/vstorm-co/agenticos/issues/12 ### Eine Freigabe innerhalb einer Delegation { #an-approval-inside-a-delegation } Die Tools eines Delegates werden vom eigenen Spec des Delegates gegatet, und er erreicht dieselbe Queue, an der der Aufrufer des Parents ohnehin schon wartet — ein Spezialist, der eine Person braucht, braucht die Person, die dasteht. Der Eintrag nennt das Tool **des Delegates** und die Argumente, die es vorgeschlagen hat, denn das eigene Gate des Delegates hat ihn geschrieben. Und er nennt, **welcher Delegate ihn aufruft**. Ohne diesen letzten Teil sagt die Queue `send_email`, ohne zu sagen, ob der Agent, mit dem jemand spricht, oder ein Spezialist namens `researcher` sie sendet. Das ist eine Queue, die Menschen blind freigeben — und in einer Delegation ist das, was freigegeben wird, oft folgenschwerer als der Agent, mit dem die prüfende Person zu tun zu haben glaubt. Diesen Delegate zu löschen tilgt nicht den Datensatz dessen, wozu er autorisiert war: Die Zeile behält den Namen des Delegates und lässt nur die Verknüpfung zu seinem nun verschwundenen Agent fallen. Das gilt auch, wenn die Löschung landet, während der Run noch geparkt ist, bevor die Freigabe-Zeile geschrieben wurde — der aufgeschobene Schreibvorgang ([#169](https://github.com/vstorm-co/agenticos/issues/169)) löst die noch vorhandenen Delegates auf und schreibt für einen verschwundenen eine null-Id, genau das, was ein Löschen nach dem Entstehen der Zeile getan hätte. Was der Run des Parents tut, ist parken, statt etwas in die Hand zu bekommen, das wie eine fertige Delegation aussieht. Das ist es wert, gesagt zu werden, weil es früher anders war: Jeder hier gebaute Agent deklariert einen Output-Typ, der einen Run mit seinen geparkten Aufrufen als _Output_ enden lässt, statt auszulösen, und die Delegationsbibliothek serialisierte dieses Objekt und reichte dem Modell des Parents `{"calls": [], "approvals": [...]}` als Bericht des Spezialisten, Task als abgeschlossen markiert. Es war der Standardpfad und kein Sonderfall, und es ist in der gepinnten Version behoben. Sie freizugeben **setzt den Delegate fort**, statt erneut zu delegieren. Der geparkte Zustand ist ein Baum — eine Ebene je Agent, jede mit ihrer eigenen Unterhaltung und ihren eigenen geparkten Aufrufen —, sodass die Freigabe den ausgesetzten Delegate dort fortsetzt, wo er stehen blieb, mit dem Urteil an dem Aufruf, den die prüfende Person tatsächlich gesehen hat. Der `task`-Aufruf des Parents wird nachgespielt, und die Delegation findet die Stelle, die sie verlassen hat. Ein Spezialist innerhalb eines Delegates verhält sich genauso, eine Ebene tiefer. Das zählt, weil die Alternative kein langsameres Fortsetzen ist, sondern eine andere Antwort. Die Delegation neu auszuführen würde die Unterhaltung des Delegates bei null beginnen und sein Modell beim zweiten Mal ein anderes Tool aufrufen lassen, sodass das, was eine prüfende Person freigegeben hat, nicht das wäre, was ausgeführt wird. Was der Delegate bereits ausgegeben hatte, reist mit seiner Stelle mit, sodass die Zeile, die geschrieben wird, wenn die Delegation endlich endet, alles davon abdeckt — siehe [wie ein delegierter Run festgehalten wird](#what-a-delegated-run-is-recorded-as). Beide Hälften des Baums werden je Delegation statt je Run gehalten, und das ist es, was einen Spezialisten drei Ebenen tiefer parken und trotzdem seinem eigenen Agent zurechnen lässt. Die Ausgaben werden selbst dann behalten, wenn die _Stelle_ des Delegates es nicht konnte — die Message-Historie der Bibliothek ist Telemetrie nach bestem Bemühen, und eine von vorn ausgeführte Delegation hat trotzdem ausgegeben, was sie ausgegeben hat. !!! warning "MCP-Freigaben werden an der Bindung eingestellt" Die Freigabeeinstellung einer Capability deckt MCP-Tools nicht ab; die `approval`-Regel der Bindung tut es, auf jeder Oberfläche, auf der der Agent läuft. `writes` hält die Tools zurück, die der Server nicht als schreibgeschützt markiert - das ist die Angabe des Servers selbst, wählen Sie für einen Server außerhalb Ihrer Kontrolle also `all`. Im Webchat erfasst **Ask about everything** (`ask_all`) auch den Rest; die strengere Einstellung einer Chatsitzung gilt nicht automatisch für einen Zeitplan oder einen anderen Zugang. Beim Modellanbieter ausgeführte Tools durchlaufen das lokale Freigabe-Gate nicht. ### Ein Workflow-Schritt, der eine Person fragt { #workflow-step-approvals } Ein Workflow fragt mit einem [`human.approval`](reference/workflow-nodes.md#human-approval)-Schritt statt mit einem abgesicherten Tool: Der Schritt schreibt seine eigene Anfrage - einen Titel, die ihm übergebenen Details, wer entscheiden darf und bis wann -, und sein Run wartet darauf. Der Tab **Approvals** listet sie über den Tool-Aufrufen, und eine Entscheidung weckt den Run, der nach der Antwort weitergeht. Eine Entscheidung ist endgültig und wird mit dem Gefragten im Audit-Trail festgehalten und bleibt unter **Decided** neben dem Protokoll der Tool-Aufrufe aufgelistet. Einen Schritt, der seine Freigebenden nennt, können nur sie entscheiden. Eine Anfrage, die niemand rechtzeitig entscheidet, läuft ab, und ein abgebrochener Run bricht seine Anfragen ab. Ein Schritt mit `editable` zeigt der freigebenden Person auch einen Vorschlag, etwa eine vorbereitete Antwort, den sie vor der Freigabe ändern darf. Der Audit-Eintrag hält fest, ob der Schritt editierbar war, ob die Person ihn geändert hat (`edited`) und wie lang der Vorschlag und der freigegebene Text sind (`proposal_length`, `final_length`), nie den Text selbst. Der Text liegt im Klartext bei der Anfrage, bis die Historie des Runs gelöscht wird. Die Anfragen werden auch gezählt. Die Stats-Karte eines Workflows und seine Statistik-API melden je Schritt und je Tag, wie viele wie vorgeschlagen freigegeben, bearbeitet, abgelehnt wurden oder abgelaufen sind, wie lange die Entscheidung dauerte und wie stark die Änderungen den Text verändert haben, jede als Verhältnis von `0` bis `1` (`edit_ratio`, auch im Audit-Eintrag); siehe [Running a workflow](workflows.md#running-a-workflow). Die organisationsweite Übersicht, `GET /api/v1/workflow-approvals/stats`, braucht `workflows:view` und umfasst nur Workflows, die das Mitglied ansehen darf. Sie enthält weder Vorschlag noch freigegebenen Text noch eine Zahl zu einzelnen Freigebenden: Sie zählt Anfragen, nicht Personen. ### Artefakt-Einträge { #artifact-entries } Eine Seite, die Tabellendaten liest, wird mit der Einwilligung ihres Besitzers geteilt, und jeder Schritt davon hinterlässt einen Eintrag mit der Person, die ihn getan hat. Keiner trägt einen Datensatz, einen Filterwert oder eine Zeile: Sie halten Versionen, Fingerabdrücke, Anfrage-IDs und die Worte, die eine Person bestätigt hat. | Aktion | Aufgezeichnet, wenn | | --- | --- | | `artifact.published` | Eine Version veröffentlicht wurde, mit ihrer Nummer, dem Fingerabdruck ihrer Seitenabfragen und `actions`, den IDs der Workflows, die sie deklariert | | `artifact.data_consented` | Der Besitzer die Daten einer Version bestätigt hat oder ihr automatisch zugestimmt wurde (`automatic`). Nennt die Version und den Fingerabdruck, `for_owner`, wenn ein Owner oder Admin für ihn bestätigt hat, und `via_grant`, wenn ein `edit_data`-Grant sie erteilt hat | | `artifact.data_withdrawn` | Ein Editor die Seite daran gehindert hat, ihre Daten zu teilen; nennt die Version, die der Zielgruppe ausgeliefert wurde, ob die automatische Einwilligung an war, und wie viele `edit_data`-Grants es auf `edit` gesenkt hat | | `artifact.auto_consent_changed` | Der Besitzer „Share new versions automatically“ ein- oder ausgeschaltet hat, mit dem Text, den er bestätigt hat | | `artifact.edit_data_granted` | Der Besitzer oder ein Owner oder Admin einer Person oder Gruppe „Can edit, including data“ gegeben hat, mit dem Text, den er bestätigt hat | | `artifact.owner_changed` | Eine Seite den Besitzer gewechselt hat: der alte und der neue Besitzer, wer es getan hat, und alle `edit_data`-Grants, die es entfernt hat | | `artifact.share_requested` | Eine Anfrage gestellt wurde - eine `audience`-, `access`- oder `version`-Anfrage - von einer Person oder vom Run ihres Agents | | `artifact.share_request_approved` und `artifact.share_request_declined` | Eine Anfrage entschieden wurde, und von wem | | `organization.public_live_data_changed` | Ein Owner oder Admin **Public pages with live data** für die Organisation ein- oder ausgeschaltet hat, mit dem alten und dem neuen Wert | | `artifact.public_data_consented` | Der Besitzer oder ein Owner oder Admin für ihn bestätigt hat, dass jeder mit dem öffentlichen Link der Seite und die Websites, die sie einbetten, ihre Daten lesen dürfen. Nennt die Version, den Fingerabdruck, die Tabellen und Spalten, die einbettenden Websites und ob ein Passwort gesetzt ist, sowie `for_owner`, wenn jemand für ihn bestätigt hat | | `artifact.public_data_withdrawn` | Ein Bearbeiter diese Einwilligung zurückgezogen hat, angemeldet als er selbst (nicht imitierend), mit dem Fingerabdruck, den sie hielt | | `artifact.public_data_voided` | Die Einwilligung gelöscht wurde, weil sich etwas darunter geändert hat, mit einem `reason`: `link_off`, `password_removed`, `origin_added`, `owner_changed`, `consent_withdrawn` oder `archived` | | `artifact.action_started` | Ein Mitglied hat einen Workflow aus der Aktion einer Seite bestätigt und gestartet. Nennt die Seite, die Version, den Workflow und den Run, nie die Eingabewerte, die am Run stehen | **Public pages with live data** ist eine Einstellung der Organisation, standardmäßig aus, in den Einstellungen der Organisation neben dem Verzicht auf Freigaben. Sie entscheidet, ob die Daten einer Seite Personen ohne Konto über einen öffentlichen Link oder ein Embed erreichen dürfen. Anders als der Verzicht braucht sie `org:settings` - Owner oder Admin - und muss angemeldet als man selbst geändert werden: Eine imitierende Session wird abgelehnt. Die Seite braucht weiterhin die [Einwilligung ihres Besitzers für öffentliche Daten](artifacts.md#live-data-on-a-public-link). Das Ausschalten ist ein Kill-Switch: Öffentliche Seiten verlieren ihre Daten beim nächsten Laden und behalten ihre Einwilligung, die wieder gilt, sobald der Schalter wieder an ist. Die Sichtbarkeit oder die Grants einer Seite zu erweitern, wird wie bei jeder anderen Ressource als Einträge `resource.share`, `resource.unshare` und `resource.visibility_changed` aufgezeichnet. Das Tool `share_view` gibt es nicht mehr, und damit auch nicht die Freigabe, die es abgesichert hat. ## Alerts { #alerts } Jeder Alert hier handelt von einem Run, auf den niemand schaut. Ein Chat-Run, der an seinem Budget stoppt, sagt das auf dem Bildschirm; derselbe Run, von einer Slack-Erwähnung, einem Zeitplan oder einem API-Aufruf gestartet, stoppt lautlos, und das Erste, was irgendwer davon hört, ist jemand, der fragt, warum der Agent still geworden ist. ### In-App, neben E-Mail { #in-app-alongside-email } Jeder Alert in diesem Abschnitt schreibt zwei Dinge: die unten beschriebene E-Mail, und eine Zeile an **der Glocke** in der [Konsole](console.md#the-bell) - dem eigenen Postfach der Konsole, keine Kopie der Mail. Die Zeile _ist_ die In-App-Zustellung; nichts weiter muss gelingen, damit sie erscheint. E-Mail ist ein zweiter, unabhängig wiederholter Kanal desselben Schreibvorgangs, weshalb der eine fehlschlagen kann - eine unzustellbare Adresse, ein ausgefallenes SMTP-Relay - ohne dass der andere je davon erfährt. Beide Kanäle werden unabhängig voneinander geschaltet, je Ereignis, unter **Settings → Notifications** - eine Person kann die In-App-Zeile für Freigaben behalten und ihre E-Mail abschalten, oder umgekehrt. Anfragen zum Teilen einer Seite und Besitzerwechsel nennen die Seite und die Person, nie ihre Daten. Dieselbe Seite trägt auch jedes andere Ereignis, das das Postfach zustellt: einen Run, der Ein `eval`-Run ist einer von vielen gemeinsam gemeldeten Runs einer Evaluation; er sendet keine einzelne Abschluss- oder Fehlermeldung. unbeaufsichtigt fertig wird oder scheitert - das Ende einer Routine nach ihr benannt und mit dem Anfang ihrer Antwort, für die Person, die sie eingerichtet hat -, die Bitte, eine Capability einzuschalten, für den Besitzer des Agents, Vorschläge, die die [Arbeit aus eigener Initiative](#proactive-work) für eine Person gemacht hat, die Ingestion eines Dokuments, die **scheitert**, die eigene Gesamtzahl eines Konnektor-Laufs, und die eigene Ankündigung eines App-Admins - `POST /admin/announcements`, noch keine Console-Seite - adressiert nach Organisation und, optional, nach Rolle, und beschränkt auf einen oder beide Kanäle. Ein Dokument, das sauber indexiert wurde, schreibt nichts, und das ist Absicht. Früher schrieb es je eine Zeile, was im Normalfall - ein Ordner Dateien, auf einmal hinzugefügt - eine Benachrichtigung pro Datei bedeutete, die besagte, dass nichts schiefgegangen war, und die begrub, was gelesen werden musste. Wo eine gewöhnliche Ingestion jetzt gemeldet wird, ist der eigene Status des Dokuments in seiner Collection und, bei einem Konnektor-Lauf, die eine Zeile, die `sync_completed` schreibt, wenn der gesamte Versuch fertig ist. Ein Scheitern erreicht weiterhin den, der die Datei hochgeladen hat, oder die Administratoren der Organisation, wenn es niemand war. Die eine Ausnahme sind die weiter unten beschriebenen wöchentlichen und monatlichen Usage-Reports, die auf dem Agent konfiguriert werden: Beide teilen sich eine einzige, veraltete E-Mail-Einstellung, sodass das Abschalten der E-Mail des einen Reports auch die des anderen abschaltet - die In-App-Zeile jedes Reports schaltet sich weiterhin einzeln. Anders als alles oben lässt sich ein Sicherheitsereignis oder eine Konfigurationsänderung auf keinem der beiden Kanäle abschalten. Es erreicht die eigenen Owner und Admins dieser Organisation - nicht das breitere `admins`-Publikum von oben - und nie die App-Admins des Deployments, außer die Aktion selbst hat keine Organisation, der sie zuzuordnen wäre; in diesem Fall bekommt sie stattdessen jeder App-Admin. Nichts davon weitet aus, was diese Seite dokumentiert: Es ist dasselbe Postfach, in dem die oben auf dem Agent konfigurierten Alerts landen, und die Opt-out-Regel unten gilt weiterhin für alles, was sich abschalten lässt. Aber nicht unbegrenzt: Jedes ist auf zwanzig Schreibvorgänge pro Minute je Akteur und Ereignistyp begrenzt, sodass ein Konto mit schnellen Änderungen nicht jeden Admin überfluten kann - der Audit-Eintrag dahinter wird trotzdem aufgezeichnet, auf dem Trail selbst ([Audit](#audit)), unabhängig davon, ob die Benachrichtigung das Limit überstanden hat. Jenseits dieser Grenze wird der Posteingang nicht einfach still. An die Stelle des Rests tritt eine Meldung pro Akteur und Minute, die sagt, dass die Minute voller war, als der Posteingang auflisten kann, und dass jedes dieser Ereignisse auf dem Trail steht. Das zählt, weil sich diese beiden nicht abschalten lassen: ein Akteur könnte das Kontingent sonst auf zwanzig harmlose Änderungen verbrauchen und danach das eine tun, worauf es zu achten gilt, ohne dass irgendetwas es sagt. Die Meldung trägt keine Zählung - zählen hieße, sie bei jedem weiteren Ereignis neu zu schreiben, also genau die Flut, die die Grenze verhindern soll. Eine Zeile fällt neunzig Tage nach dem Schreiben aus dem Postfach, wenn sie _gelesen_ ist, und ein Jahr danach unabhängig davon, ob sie je geöffnet wurde - beides gezählt ab dem Schreiben, nie ab dem Lesen, sodass eine am Tag vor ihrer oberen Grenze geöffnete Zeile mit jeder anderen so alten zusammen verschwindet. Ein Sweep im Hintergrund, den niemand auslöst. Der E-Mail-Sweep versendet keine Zeile, die diese Grenze bereits überschritten hat, sodass ein nach langem Ausfall wiederanlaufender Worker keine Benachrichtigung auf ihrem Weg in die Löschung verschicken kann. Das [Leeren](console.md#the-bell) ist das andere und nicht dasselbe: Ein Mensch nimmt eine Zeile sofort aus der eigenen Liste, und die Zeile selbst wird behalten, bis dieser Sweep sie erreicht. Was das übersteht, hängt davon ab, worum es in der Benachrichtigung ging. Ein Sicherheitsereignis, eine Konfigurationsänderung und die eigene Ankündigung eines Admins beginnen als Audit-Eintrag, und dieser Eintrag überlebt die Zeile, die das Postfach gezeigt hat ([Audit](#audit)). Der Ausgang eines Runs, ein Ingestion-Ergebnis und ein Nutzungsbericht schreiben keinen eigenen Audit-Eintrag: Der Run, das Dokument und die Ausgaben, die sie beschreiben, sind der Datensatz, und die Benachrichtigung ist nur, wie jemand davon erfahren hat - die berechneten Zahlen eines periodischen Berichts, die nirgends außer in der Benachrichtigung leben, sind das Einzige, was eine abgelaufene Zeile mitnimmt. ### Ein Ergebnis, einmal erledigt { #one-outcome } Eine wartende Freigabe, ein fehlgeschlagener Lauf und ein Fund einer Beobachtung sind Dinge, auf die jemand reagieren muss, also ist jedes ein **Ergebnis**: ein Eintrag pro Person und Ursache, egal wie viele Stellen davon berichten (#2575). Die Karte über dem Chat, aus dem es stammt, die Zeile an der Glocke, der Push und die E-Mail sind Zustellungen dieses einen Ergebnisses, nicht vier Einträge. Ein Ergebnis wird einmal bestätigt, gelöst oder stummgeschaltet, von der Chat-Karte oder der Glocke aus, und jede Stelle folgt. Bestätigen markiert es als gelesen und storniert Push und E-Mail, die noch auf den Versand warten; Lösen und Stummschalten räumen es zusätzlich weg. Eine Zustellung, die ein Worker schon hält, wird kurz vor dem Senden erneut geprüft, sodass ein dazwischen erledigtes nicht hinausgeht. `POST /outcomes/{id}/settle` nimmt die Aktion entgegen, und `GET /outcomes` listet die eigenen Ergebnisse des Aufrufers, optional für einen Chat. Die Entscheidung über die letzte offene Freigabe eines Laufs erledigt sein Ergebnis. Eine stummgeschaltete Ursache bleibt stumm. Bei anderen Benachrichtigungen kann eine gelöste oder bestätigte Ursache bei ihrer Rückkehr wieder geöffnet werden; ein wiederholter Fund behält seinen bisherigen Zustand. Jeder Fund einer Beobachtung, auch ohne dringende Glockenmeldung, teilt einen Fall mit seinem Inbox-Eintrag und seiner Karte im ursprünglichen Chat. **Erledigt** löst diesen Fall; Ablehnen schaltet ihn stumm. Beide Aktionen stornieren ausstehende externe Zustellungen. Bestätigen stoppt Benachrichtigungen und weitere Meldungen im Briefing, lässt den Fund aber mit **Zur Kenntnis genommen** in der Inbox bearbeitbar. Das Löschen einer Beobachtung erledigt ihre offenen Fälle, bevor ihre Funde gelöscht werden. Entscheidungen gelten nur für die besitzende Person innerhalb derselben Organisation. Nur ausdrückliches **Wiederherstellen** eines abgelehnten Fundes öffnet denselben Fall erneut. Alte Benachrichtigungen und Zustellungen werden nicht wiederholt. Die Migration `0264_finding_outcome_state.py` verknüpft vorhandene Funde mit ihren Fällen, erstellt Fälle für nicht dringende Funde, bewahrt IDs und frühere Inbox-Entscheidungen und storniert überholte Zustellungen. Ein Downgrade entfernt die Verknüpfung, kann aber weder Entscheidungen rückgängig machen noch vom Anbieter angenommene Nachrichten zurückrufen; bewahren Sie ein Backup vor dem Upgrade auf. ### Auf dem Agent konfiguriert { #configured-on-the-agent } Wer von einem Agent hört, ist Teil des Specs des Agents, unter **Limits → Alerts**. Ein deploymentweites Publikum machte den lauten Agent und den, den niemand verpassen darf, zu derselben Einstellung, sodass der einzige Weg, den ersten leiser zu stellen, war, für den zweiten taub zu werden. | Alert | Löst aus, wenn | Voreingestelltes Publikum | | --- | --- | --- | | **Budget** | dieser Agent sein eigenes monatliches Cap erreicht hat | die Admins und der Besitzer des Agents | | **Freigaben** | ein Tool-Aufruf geparkt hat | wer den Run gestartet hat, plus die Admins | | **Nutzung** | wöchentlich und monatlich, was dieser Agent ausgegeben hat | aus | Das Log eines Workflows hat eigene Alerts, die auf der Logs-Seite des Workflows gesetzt werden statt in einer Spec; siehe [Alerts](workflows.md#log-alerts). Sie lösen aus, wenn die passenden Zeilen einer Regel ihren Schwellenwert in ihrem Fenster erreichen, und gehen an den Eigentümer des Workflows, sofern die Regel nichts anderes sagt. Es gelten dieselben Glocken- und E-Mail-Einstellungen wie im Rest dieses Abschnitts, unter **Workflow log alerts**. Ein Publikum ist eine Liste von Rollen, nicht von Adressen: | Publikum | Löst sich auf zu | | --- | --- | | `admins` | die Owner und Admins der Organisation, **plus die App-Admins des Deployments** | | `owner` | der Besitzer des Agents | | `initiator` | wer den Run gestartet hat; niemand, bei einem Run, den ein Zeitplan begonnen hat | | `chosen` | genau die daneben benannten Mitglieder | Rollen statt Adressen, weil ein Spec in das Repository eines Kunden exportiert wird und die Menschen darin überlebt: `admins` bedeutet nach einer Umstrukturierung weiterhin die richtigen Personen, und es bedeutet sie in derjenigen Organisation, in die der Spec importiert wird. Ein benanntes Mitglied, das gegangen ist, steuert nichts bei, statt auszulösen — eine Freigabe-Queue darf nicht verstummen, weil eine Id sich nicht mehr auflöst. ### Ein Alert verlinkt dorthin, wo die Entscheidung ist { #an-alert-links-to-where-the-decision-is } **Freigabe-Post öffnet die Queue** — `/runs?tab=approvals`, den Approvals-Tab von **Aktivität** (Aktivität), die einzige Oberfläche, die Approve und Reject trägt. Früher öffnete sie `/agents/{id}`, den Builder: ein Satz Prosa darüber, dass Tool-Aufrufe eine Queue erreichen, und keine Queue. Also landete die eine E-Mail, deren ganzer Zweck _jemand muss jetzt entscheiden_ ist, eine Suche von der Entscheidung entfernt, während der geparkte Run auf `ApprovalService.expire_stale` zualterte (#935). Für den Tab gab es keine URL, bis #934 sie in `?tab=` legte. Sie nennt den Run bewusst **nicht** mit `&run=`, obwohl der Alert einen hält: Die Entscheidungs-Bedienelemente sitzen auf der Zeile in der Queue, und unterhalb von `lg` ersetzt ein fokussierter Run die Liste — was sie vor dem Leser verbergen würde, der am ehesten am Telefon sitzt. Budget-Post öffnet den Agent, und das ist das richtige Ziel dafür: Das Cap, über das sie berichtet, wird dort bearbeitet. ### Der Freigabe-Alert sind zwei E-Mails { #the-approval-alert-is-two-emails } `approvals:decide` gehört zu `owner`, `admin` und `operator`. Das voreingestellte Publikum für einen geparkten Aufruf schließt ein, wer den Run gestartet hat, und ein Builder, der seinen eigenen Agent aus dem Chat startet, ist der gewöhnliche Initiator — der Alert erreichte also regelmäßig jemanden, den die Plattform ablehnen würde. Sie bekamen **„waiting on your approval"** mit einer Schaltfläche **Review the request**, folgten ihr, und **Aktivität** (Aktivität) zeichnete überhaupt keinen Approvals-Tab: Die Ablehnung kam als abwesender Tab an statt als Satz. Das Publikum wird also nach der Berechtigung aufgeteilt, statt darauf beschnitten: | Empfänger hält | Bekommt | | --- | --- | | `approvals:decide` | die Anfrage, mit dem Link zur Queue | | irgendetwas Geringeres | die _Tatsache_: Der Run ist angehalten, nicht gescheitert, ihn freizugeben ist Sache eines Owners, Admins oder Operators, und von ihnen wird nichts verlangt | Zu beschneiden würde stattdessen die eine Person, die definitiv auf den Run wartet — die Person, die ihn gestartet hat —, nichts darüber hören lassen, warum er gestoppt hat. Die zweite E-Mail trägt **keinen Link**, und das ist gewollt und nicht unfertig. Dass `agents:view` eine Rollenberechtigung ist, macht keinen einzelnen Agent erreichbar: Der Zugriff auf einen Agent wird je Ressource aufgelöst, sodass ein `chosen`-Empfänger ohne Grant auf einen privaten Agent eine zweite Handlungsaufforderung bekäme, die die Plattform ablehnt — derselbe Defekt an einer neuen Stelle. Von diesem Leser wird nichts verlangt, also wird ihm nichts angeboten. Ebenso wenig behauptet sie, irgendwer sonst sei informiert worden: Ein Publikum aus einer Person ohne Entscheidungsrecht heißt, dass niemand, der entscheiden kann, überhaupt angemailt wurde, und ein Satz, der etwas anderes verspricht, ließe sie auf jemanden warten, der nie davon gehört hat. Welche Rollen entscheiden, wird am Berechtigungskatalog abgelesen und nicht daneben aufgelistet: Eine Rolle, die `approvals:decide` gewinnt oder verliert, darf das Routing nicht zurücklassen, und das ist derselbe Defekt eine Ebene höher. ### Jeder Link sagt, um welche Organisation es geht { #every-link-says-which-organization-it-is-about } Die Konsole handelt in derjenigen Organisation, die der Leser zuletzt benutzt hat: `apiClient` stempelt `X-Organization-Id` aus einer je Browser gespeicherten Auswahl. Jede Alert-URL trug früher keine, sodass jemand in zwei Organisationen, der zuletzt in Globex gearbeitet hatte, den Freigabe-Alert für einen Run in Acme öffnete und **Globex'** Queue las — sehr wahrscheinlich leer, und zu lesen als _es wartet nichts_ über einen Run, der geparkt ist und auf `ApprovalService.expire_stale` zualtert. Die Agent-Links waren leiser falsch: `/agents/{id}` unter der falschen Organisation ist eine Ablehnung für einen Agent, den der Leser wirklich sehen kann, einen Wechsel entfernt. Also trägt jeder Link `org=`, an einer Stelle gebaut — `NotificationService._link`, das den Trenner aus dem Pfad wählt, weil der Freigabe-Link bereits `?tab=approvals` trägt — statt an jeder der vier Aufrufstellen, und die Konsole übernimmt sie genau so, wie sie die Id in `/orgs/{id}` übernimmt: Eine Seite, die eine Organisation nennt, _ist_ diese Organisation. Die Übernahme ist ein Layout-Effekt in `ActiveOrgGuard`, vor dem Zurücksetzen des Tenant-Caches und vor den eigenen Abfragen der Seite, sodass die erste Anfrage der Seite bereits den richtigen Tenant trägt. Der Pfad steht über dem Parameter — ein Link sagt, um welche Organisation ein Alert ging, und kann niemanden von der Seite wegbewegen, auf der er gerade steht. Einem Leser, der diese Organisation inzwischen verlassen hat, wird das gesagt, statt ihn still zu verschieben: Die Ablehnung nennt den Link als Grund, denn im Stillen umgeschaltet zu werden ist der Weg, auf dem die Seite einer anderen Organisation zur Antwort auf den Alert wird. Sie kann die Organisation nicht nennen — der Leser ist kein Mitglied, also steht er nicht in ihrer Liste. ### Zwei Regeln, die nicht verhandelbar sind { #two-rules-that-are-not-negotiable } **Ein Opt-out je Person zieht immer nur ab.** Die eigenen Schalter jedes Empfängers unter **Settings → Notifications** werden zuletzt angewendet. Ein Agent kann entscheiden, dass die Admins von ihm hören sollen; ein Admin kann trotzdem entscheiden, dass er keine Budget-Post will. Nichts, was der Autor eines Agents schreibt, verpflichtet jemanden in ein Postfach. **Das Cap der Organisation ignoriert den Spec vollständig.** Dieses Limit stoppt jeden Agent in der Organisation, und der Autor eines Agents kann es nicht anheben, also geht sein Alert an die Administratoren, was auch immer irgendein Agent verlangt. Ein Agent kann kein Limit stummschalten, das er nicht kontrolliert. ### Stille bedeutet etwas { #silence-is-meaningful } Eine Organisation, die nichts ausgeführt hat, bekommt keinen Bericht. Ein wöchentliches „0 Runs, 0,00 $" ist der Bericht, den Menschen in einen Ordner filtern, und dann landet der, auf den es ankam, ebenfalls dort. Die Zahl in diesem Bericht sind die Ausgaben der Organisation über das Fenster — dieselbe Arithmetik, mit der das Cap durchgesetzt wird, Ingestion eingeschlossen und delegierte Runs einmal gezählt. Ein Bericht, dessen Summe dem Limit widerspricht, das die Plattform durchsetzt, ist schlimmer als kein Bericht, denn beide Zahlen sehen maßgeblich aus. Das Senden blockiert nie und löst nie in den Aufrufer hinein aus: Ein Run, der bereits geendet hat, darf nicht noch einmal scheitern, weil SMTP ausgefallen war. ## Bezahlte Evaluierung { #ai-assistance } Bewertung, Testfallgenerierung und Verbesserung sind ausdrücklich gestartete, bezahlte Aktionen. Modell und Anweisungen gehören zum Dataset: **Bewertung** konfiguriert den Bewerter, **Einstellungen** Generierung und Verbesserung. Vor bezahlter Arbeit zeigt die Konsole eine Kostenschätzung. Es gibt keinen organisationsweiten KI-Schalter und keine organisationsweite Modellvorgabe für Bewerter. Der Preis wählt kein vermeintlich stärkeres Modell. Ein automatisch erstellter Testsatz beginnt mit dem Modell des Agenten; ein unabhängiger Bewerter wird ausdrücklich am Dataset gewählt. Berechtigungen, Modellzugangsdaten, Organisationsbudgets und Run-Limits gelten weiterhin. Jeder Aufruf wird gemessen und in Aktivität erfasst. Wartende Aufgaben behalten ihre Startkonfiguration; Änderungen gelten für spätere Arbeit. Historische Helferzeilen bleiben für Zuordnung und Budgetprüfung erhalten. ## Übergangene Promotionen { #promotion-overrides } Eine Umgebung, die von [Eval-Suites bewacht](environments.md#guarding-a-promotion-with-evals) wird, übernimmt eine Version erst, wenn sie auf ihr bestehen. An ihnen vorbei zu promoten ist ein **Übergehen**: Es braucht `promotions:override` - Owner und Admins haben es, und eine eigene Rolle kann es bekommen - von einer Person, die als sie selbst angemeldet ist, nicht von einem API-Schlüssel oder einem Administrator, der sie imitiert, und eine Begründung von mindestens zehn Zeichen. Es wird im [Audit-Trail](#audit) als `agent.environment_promotion_overridden` verzeichnet, mit der Begründung und dem Ergebnis jedes Wächters, an der Version in der Historie gezeigt und an den Eigentümer jedes nicht bestandenen Wächters geschickt. Zu ändern, welche Suites eine Umgebung bewachen, die Festlegung eines Wächters zu verschieben oder abzuschwächen, was ein Wächter prüft, braucht dieselbe Berechtigung; jede dieser Änderungen, und eine strengere Schwelle, wird als `agent.environment_guards_changed` verzeichnet. Eine Änderung an der Bewertung eines Datasets wird als `dataset.scoring_changed` verzeichnet, mit den Namen der geänderten Teile statt ihrem Inhalt, das Anlegen seines Judges als `dataset.judge_created`. ## Audit { #audit } Aktionen, die den Zugriff ändern oder Geld ausgeben, werden mit einem Akteur festgehalten, und ein Kontext ohne Subjekt **löst aus**, statt die Abwesenheit weiterreisen zu lassen. Ein Eintrag, der niemanden nennt, bedeutet also genau zwei Dinge, und die `action` sagt welches: den Ablauf-Sweep für Freigaben und einen Betreiberbefehl an der Shell des Deployments. Zugangsdaten an eine Sammlung zu binden ist eine dieser Aktionen. `sync_source`-Einträge halten das Erstellen, Klonen, Umhängen und Löschen einer Quelle fest, denn die Zeile entscheidet, wer am Ende lesen kann, was sie aufnimmt ([Dateiverarbeitung](file-processing.md#who-ends-up-able-to-read-what-a-source-ingested)). Ein privilegierter **Massen-Lesevorgang** wird ebenfalls festgehalten. Jeder CSV-Export schreibt einen Eintrag `runs.export`, `approvals.export` oder `spend.export`, der das Fenster und die Zeilenzahl nennt — wer die ganze Tabelle vom Bildschirm genommen hat, ist eine Frage, die jetzt billig zu beantworten und später unmöglich zu rekonstruieren ist. Der Schreibvorgang teilt sich die Transaktion der handelnden Anfrage, also scheitert er geschlossen: Ein Eintrag, der sich nicht festhalten lässt, rollt die Aktion zurück, die er beschreibt, statt eine privilegierte Mutation unauditiert landen zu lassen. `audit:read` gatet das Lesen. Die Umgehung eines App-Admins ist genau das, wozu die Spur existiert, um sie zur Verantwortung zu ziehen. Die Spur hält auch sich selbst ehrlich. Jeder Eintrag reiht sich in eine Hash-Kette je Organisation ein — er trägt einen Hash über seinen eigenen Inhalt, in den der Hash des vorherigen Eintrags eingeflochten ist — sodass das Ändern, Umordnen oder Einfügen eines Eintrags, oder das Löschen eines aus der Mitte, jeden nachfolgenden Hash auseinanderlaufen lässt. `agenticos cmd audit-verify` läuft jede Kette ab, berechnet die Hashes neu und nennt den ersten Eintrag, der nicht mehr passt; ohne Argument prüft es jede Kette, auch die deploymentweite, die mandantenlose Aktionen hält — eine Änderung der Deployment-Einstellungen, eine Impersonierung, App-Admin-Benutzerverwaltung — und endet mit einem Exit-Code ungleich null, wenn eine Kette fehlschlägt. Das ist **Erkennung, keine Verhinderung** — ein Operator mit Zugriff auf die Datenbank kann eine Zeile weiterhin umschreiben und jeden nachfolgenden Hash neu berechnen — ein sauberer Lauf belegt also, dass niemand manipuliert hat, der nicht auch die Kette neu geschmiedet hat, kein Beweis, dass die Zeilen unveränderlich sind. Zwei Löschungen kann die Kette von sich aus nicht erkennen, weil die verbleibenden Zeilen intern konsistent bleiben: das Abschneiden der neuesten Einträge einer Kette und das vollständige Löschen der Kette einer Organisation. Diese fängt stattdessen ein **Checkpoint** ab — eine organisationsweite Höchstmarke, die `record_audit` neben jedem Eintrag vorrückt, unter einem Datenbank-Trigger, der ihr Zurückgehen oder Löschen verweigert. `audit-verify` meldet eine Kette, deren Kopf hinter ihrem Checkpoint liegt, oder einen Checkpoint, dessen Kette weg ist. Was dieser Trigger abdeckt, gehört genau gesagt, weil man leicht mehr hineinliest. Er schließt den gewöhnlichen Schreibpfad — einen App-Administrator, der durch das Produkt handelt, und einen Fehler in diesem Code — also das Bedrohungsmodell, für das diese Spur geschrieben ist. Gegen jemanden mit den Zugangsdaten der Datenbank selbst ist er keine Kontrolle. Die Anwendung und ihre Migrationen verbinden sich mit derselben Rolle, und dieser Rolle gehört die Checkpoint-Tabelle: sie kann den Trigger entfernen, und ein `TRUNCATE` leert die Tabelle, ohne einen zeilenbezogenen Delete-Trigger überhaupt auszulösen. Ein Superuser kann beides. Das zu schließen erfordert die Höchstmarke dort, wo die Rollen dieser Datenbank nicht hinreichen — in einem Append-only- oder Object-Lock-Speicher außerhalb —, was eine geplante Folgearbeit bleibt. Zwei auditierte Schreibvorgänge für eine Organisation können die Kette nicht aufspalten: jeder hängt unter einer organisationsbezogenen Sperre an, sodass sie sich zu einer einzigen Linie serialisieren, statt beide denselben Kopf zu verlängern. Eine **impersonierte** Aktion nennt beide. Wenn ein App-Admin als ein anderes Konto handelt, trägt das Access-Token den Administrator als `act`-Claim; jeder Eintrag, den diese Anfrage festhält, behält `actor_user_id` als das Konto, als das gehandelt wird, und fügt `impersonator_user_id` hinzu — den Administrator dahinter. So löst sich „wer hat die Unterhaltung dieses Kunden gelesen" auf eine Person auf, selbst wenn die Aktion als die des Kunden festgehalten wurde. Bei einer gewöhnlichen Anfrage ist es null, wo niemand als jemand anderes handelt, und nichts wird nachgetragen: Ob eine vergangene Aktion impersoniert war, lässt sich im Nachhinein nicht wissen, und eine Antwort zu erfinden wäre eher eine falsche Anschuldigung als eine fehlende. Die Impersonierung selbst wird an beiden Enden festgehalten. `admin.user.impersonate` nennt das Konto, die Session-Zeile, die die Impersonierung ist, und wann sie abläuft; `admin.user.impersonation_ended` nennt dieselbe Session, wenn der Administrator sie beendet, mit dem Administrator als Akteur. Ein Ablauf schreibt nichts, denn niemand hat gehandelt — und ebenso wenig die Person, die sich überall abmeldet, was eine Impersonierung über `DELETE /sessions` so beendet, wie es jede andere Session beendet. Ob es der Person _gesagt_ wird, ist die Deployment-Einstellung `notify_impersonated_users` ([Das Deployment](deployment.md#acting-as-another-account)); ist sie aus, was die Voreinstellung ist, ist diese Spur der einzige Datensatz. Eine Impersonierung kann **keine externe Identität** an das Konto binden, als das sie handelt. Einen Chat-Konto-Link zu bestätigen und den OAuth einer Integration abzuschließen heften beide eine Identität an denjenigen, der die Anfrage ist, und unter einer Impersonierung ist das die Zielperson — das eigene Telegram-Konto oder der OAuth-Grant des Administrators würde sich also an das Konto einer anderen Person heften und die Stunde überleben, auf die die Impersonierung begrenzt ist. Beides wird während einer Impersonierung mit einem 403 abgelehnt, denn ein Administrator, der die Verbindung eines Mitglieds repariert, ist kein Ablauf, den diese Plattform hat. Das Mitglied verknüpft seine eigenen Konten, als es selbst. Die Linie verläuft bei allen an das Mitglied gehefteten Zugangsdaten, nicht nur bei einer Identität: Über das Chat-Konto und den OAuth-Grant oben hinaus wird auch ein Bearer-Token abgelehnt, das auf die Verbindung des Mitglieds getippt wird. Ein solches Token ist nicht die eigene Identität des Administrators, aber unter dem Vault-Scope des Mitglieds versiegelt spricht es für jeden Agent des Mitglieds als das Konto, zu dem es gehört, überlebt die Stunde, auf die die Impersonierung begrenzt ist, und steht gegen das Mitglied festgehalten. Was ein Administrator weiterhin im Namen des Mitglieds tut, ist Konfiguration, die kein Secret speichert — ein Name, eine URL, eine Tool-Allowlist — und das Löschen eines gespeicherten Tokens, was nichts aufbewahrt. Die geteilte Verbindung **einer Organisation** ist dasselbe, obwohl sie von Admins eingetragen werden soll: Ein Bearer-Token, das unter einer Impersonierung auf eine solche getippt wird, wird ebenfalls abgelehnt, denn unter dem Vault-Scope der Organisation versiegelt spricht es für jeden Agent, den die Organisation bindet, als das eigene Konto des Administrators, über die Stunde hinaus, in der die Impersonierung endet. Eine Org-Verbindung zu erstellen hielt den Administrator dahinter bereits fest; eine zu aktualisieren hielt gar nichts fest, sodass ein auf eine bestehende Verbindung rotiertes Token jetzt dieselbe Spur hinterlässt (#1521). ## Aufbewahrung { #retention } Bis #1420 wurde nichts nach Zeitplan gelöscht. Gespräche, ihre Dateien, Run-Zeilen und Manifeste, Workspaces, das Gedächtnis eines Agenten, hochgeladene Dokumente und Audit-Einträge lebten, bis jemand die Organisation löschte. Das ist in die eine Richtung ein Datenschutzproblem und — beim Audit — in die andere ein Compliance-Problem: HIPAA §164.316(b)(2) will einen Audit-Eintrag sechs Jahre aufbewahren, die DSGVO will alles andere minimieren. Die Frist gilt deshalb **pro Klasse**, und beide Pflichten bekommen eine Einstellung. Zu setzen unter **Organisationen → ein Workspace → Mitglieder → Aufbewahrung**, abgesichert über `org:settings`. Ein Sweep läuft einmal täglich und löscht **hart**: eine Richtlinie, die die Zeilen behielte, wäre keine. Daten der [Virtual Tables](virtual-tables.md#limits-and-retention) gehören zum selben Sweep, mit Fristen für das ganze Deployment, die keine Organisation setzt: Idempotenz-Receipts nach 24 Stunden, zugestellte Outbox-Zeilen nach 3 Tagen und die History eines Datensatzes nach 365 Tagen, wodurch auch die History eines gelöschten Datensatzes entfernt wird. Der Eintrag, den er schreibt, nennt `table_receipts`, `table_outbox` oder `table_history` und eine Zahl. Die drei eigenen Zahlen des Deployments - `retention_defaults`, `retention_max_days` und `audit_retention_floor_days` - sind Felder der Deployment-Einstellungen, von einer App-Administratorin über `PATCH /admin/deployment-settings` geschrieben wie jede andere Einstellung dort. Ein Konsolenformular dafür gibt es noch nicht; die Seite der Organisation ist der Ort für die Fristen je Tenant. ### Die Klassen { #the-classes } | Klasse | Was mitgeht | Gemessen ab | | --- | --- | --- | | Gespräche | Nachrichten, Tool-Aufrufe und Dateien; eine gesperrte Charge lässt Gespräche mit nicht abgeschlossenen Runs aus, und ein dauerhafter Auftrag zur Dateilöschung wird zusammen mit den Zeilen bestätigt | Der letzten Aktivität des Threads, damit einer, zu dem jemand zurückkehrt, nicht alt ist | | Runs | Die Run-Zeile, ihr Manifest und ihre Tool-Freigaben | Dem Start des Runs | | Workspaces | Die Aufzeichnung der Plattform über die Dateien eines Agenten. Beim `state`-Backend _ist_ die Zeile der Speicher; die Dateien eines Sandbox-Backends räumt dessen eigene TTL ab. Die eines Projekts bleiben, solange das Projekt besteht | Der letzten Nutzung | | Apps | Eine [veröffentlichte Seite](artifacts.md), alle Versionen, Grants und der öffentliche Link; die Dateilöschung wird in derselben Transaktion eingereiht | Der letzten Veröffentlichung, damit ein Bericht, den ein Zeitplan neu veröffentlicht, nicht alt ist | | Gedächtnis | Die Gedächtnisdateien eines Agenten | Dem letzten Schreiben, denn eine Notiz wird einmal geschrieben und monatelang gelesen | | Hochgeladene Dokumente | Die Zeile, ihre Vektoren und die hochgeladene Datei | Dem Zeitpunkt des Hochladens | | Audit | Einträge auf der Spur dieser Organisation | Dem Zeitpunkt des Eintrags | **Ein Dokument, das noch eingelesen wird, ebenfalls nicht.** Ein Worker hält es, und ihm Zeile und hochgeladenes Original unter einer Ingestion wegzunehmen, die danach Vektoren schreibt, hinterlässt durchsuchbaren Inhalt, den kein späterer Sweep benennen kann. Ausgemustert werden nur fertige und fehlgeschlagene Zeilen. **Ein von einem Konnektor synchronisiertes Dokument wird nicht weggeräumt.** Seine Lebensdauer gehört der Quelle, die es dort abgelegt hat: es hier zu löschen entfernte eine Zeile, die der nächste `new_only`-Sync aus derselben unveränderten Datei wieder anlegt — Embedding-Kosten für nichts. Weggeräumt wird, was jemand hochgeladen hat und dessen Lebensdauer nichts anderes besitzt. ### Welche Zahl gewinnt { #which-number-wins } Drei Schichten, aufgelöst in `app/core/retention.py` und sonst nirgends: 1. **Die Vorgabe des Deployments**, für eine Organisation, die nichts gesagt hat. Fehlt sie, heißt das für immer — eine Plattform, die beim Upgrade begänne, die Historie einer bestehenden Installation zu löschen, wäre eine, der beim nächsten Upgrade niemand mehr traute. 2. **Die eigene Frist der Organisation**, kürzer oder länger. 3. **Die Obergrenze des Deployments**: nichts dieser Klasse lebt hier länger als N Tage, und eine Organisation kann sie nicht anheben. Beim Audit läuft es andersherum. Das Deployment setzt eine **Untergrenze** — wie kurz ein Eintrag höchstens leben darf, sechs Jahre, bis eine Betreiberin das ändert — und eine Organisation darf sie verlängern, nie verkürzen. **Audit wird noch nicht weggeräumt**: die Frist wird aufgelöst und gemeldet und eine Organisation an die Untergrenze gehalten, aber kein Eintrag gelöscht - die Hash-Kette und ihr Append-only-Checkpoint stehen darauf, dass Einträge nirgendwo hingehen, und ein bloßes Löschen lässt `audit-verify` die Ausmusterung als Manipulation melden. Eine Kette auszumustern und die Unversehrtheit der übrigen weiterhin beweisen zu können, ist ein Mechanismus, den es hier noch nicht gibt - also gilt die Untergrenze, und der Sweep lässt das Audit in Ruhe. Eine Obergrenze gilt für das Audit weiterhin, wo die beiden einander nicht widersprechen. Wo doch, gewinnt die Untergrenze, und der Widerspruch wird gemeldet. Eine Spur, die eine Administratorin kürzen kann, ist keine Spur, also wird eine Frist unterhalb der Untergrenze **abgelehnt** statt still auf sie angehoben: Einträge länger zu behalten als die Zahl auf dem Bildschirm sagt, ist ein Fehler eigener Art. Eine Untergrenze über einer Obergrenze ist ein Widerspruch, und er wird gemeldet statt aufgelöst. Die Einstellungsseite nennt die Klasse; eine Administratorin entscheidet, welche gilt. Eine der beiden zu wählen ließe ein Deployment zurück, das sich anders verhält als seine eigene Einstellungsseite. ### Was eine Löschung überlebt { #what-survives-a-purge } **Die Rechnung.** Die Ausgaben eines Monats sind eine Summe über `agent_runs`; diese hart zu löschen würde den Monatswert einer Organisation mit dem vorbeiziehenden Fenster auf null fallen lassen, und eine auf dieser Zahl gemessene Obergrenze griffe für den Rest des Monats nicht mehr. Der Sweep liest, was die ablaufenden Runs gekostet haben, bevor er sie löscht, und hält eine Summe je Organisation und Monat in `purged_run_spend` — eine Zahl und einen Zähler, ohne Agent, ohne Modell, ohne jemandes Namen. `app/services/spend.py` addiert sie zur laufenden Summe, und das ist die einzige Stelle, an der beide sich treffen. **Die Spur des Sweeps selbst.** Ein Eintrag je Organisation und Sweep, der die Klasse und den Zähler nennt und sonst nichts: ein Audit-Eintrag, der zitierte, was er gelöscht hat, hielte den Inhalt über die Aufbewahrung hinaus, die ihn entfernt hat. ### Wenn es fehlschlägt { #when-it-fails } Pro Klasse, nicht pro Sweep. Ein ausgefallener Vector Store darf nicht verhindern, dass Gespräche gelöscht werden; also wird jede Klasse versucht, ihr Fehler protokolliert und im Audit-Eintrag benannt, und der Sweep geht weiter. Der nächste Durchlauf versucht es erneut, denn eine Charge, die nichts entfernt hat, kommt einfach wieder vorbei. Gelöscht wird in Chargen von 500, bis zu vierzig Durchläufe je Klasse und Sweep. Eine Organisation, die nach zwei Jahren auf neunzig Tage stellt, arbeitet ihren Rückstand über mehrere Tage ab statt in einem Sweep, der eine Stunde lang eine Sperre hält, mit allen anderen periodischen Flows dahinter in der Schlange. Jede Klasse läuft in einem Savepoint: Ein SQL-Fehler nimmt ihre Änderungen und Löschaufträge zurück, ohne spätere Klassen abzubrechen. Dauerhafte Jobs entfernen Dateien erst nach dem Commit. Speicherfehler und Wiederholungen sind unter Background jobs sichtbar. Ein Rollback der Löschung lässt die Dateien intakt. ### Was das nicht erreicht { #what-this-does-not-reach } - **Backups.** Eine Löschung entfernt Zeilen und Dateien aus dem laufenden Deployment. Was Ihr Backup-Zeitplan hält, lassen Sie selbst ablaufen, und ein Restore bringt zurück, was der Snapshot enthielt. - **Was Sie anderswohin geschickt haben.** Logs an einen externen Collector, Traces in einem gehosteten Observability-Projekt und alles, was ein Modellanbieter behält, richten sich nach den Einstellungen jener Dienste, nicht nach dieser. - **Steuerung je Gedächtniseintrag**, das ist [#1594](https://github.com/vstorm-co/agenticos/issues/1594), und das Löschen einer Person quer durch eine Organisation, das ist [#1421](https://github.com/vstorm-co/agenticos/issues/1421). Diese Seite handelt vom Alter; jene handeln von einer Person. ## Was all das nicht abdeckt { #what-none-of-this-covers } Es ist wert, gesagt zu werden, weil eine Governance-Seite anderes nahelegt: - **Keine Ratenbegrenzung je Agent.** Die vorhandenen Limits sitzen auf den öffentlichen Oberflächen — das Embed-Widget misst Nachrichten je Besucher, ein Channel-Bot misst jeden Absender ([Channels](channels.md)) — nicht auf dem Deployment: Es gibt kein Anfragebudget je Agent, und die eigenen Routen der Konsole werden nicht gemessen. - **Keine Inhaltsfilterung.** Was ein Agent sagt, ist das, was das Modell gesagt hat. - **Keine Egress-Kontrolle bei MCP.** Ein gebundener Server wird vom Worker über das Netzwerk erreicht; einzuschränken, wohin das gehen darf, ist Deployment-Konfiguration und keine Einstellung hier. ## Zusammenfassung { #recap } - **Zwei Caps, und sie lassen sich nicht zusammenlegen.** Ein Budget begrenzt Geld; ein Step-Limit begrenzt eine Schleife, die je Aufruf billig ist und nie fertig wird. - Das Budget wird **vor** jeder Modellanfrage geprüft, und ein fehlgeschlagener Run hält trotzdem fest, was er ausgegeben hat. - Das Cap ist eine Obergrenze auf **committete** Ausgaben. Gleichzeitige Runs können einander nicht sehen, ein striktes Cap bedeutet also eine Queue. - Eine **Freigabe wird einmal entschieden**, und eine zweite Entscheidung über eine entschiedene Freigabe wird abgelehnt. - Einen Run zu lesen ist **autorisiert, nicht besessen** — `runs:view` liest den Run eines Kollegen, und der eines anderen Tenants liest sich als abwesend. - **Stille bedeutet etwas.** Ein Alert, der nicht ankam, heißt, dass die Sache nicht passiert ist, und das stimmt nur, weil hier nichts nach bestem Bemühen läuft. - **Eine Promotion an ihren Evals vorbei ist ein Übergehen**, mit Begründung, eigener Berechtigung und einem Eintrag an der Version. ## Referenz { #reference } - [Konzepte](concepts.md) - Spec, Version, Exposure, Run. - [Berechtigungen](permissions.md) - wer das alles setzen darf. - [Konfiguration](configuration.md) - die Einstellungen auf Deployment-Ebene. ## Automatische Pause bei wiederholten Fehlern { #routine-health } Routinen und Beobachtungen pausieren standardmäßig nach **drei aufeinanderfolgenden erfolglosen Prüfungen**. Im Formular lässt sich ein Limit von 1 bis 20 festlegen oder dieses Limit deaktivieren. Anbieter- und Ausführungsfehler, ungültige Beobachtungsergebnisse und unerreichbare Quellen zählen mit. Warten auf Genehmigung, einen Offline-Computer und Zustellfehler zählen nicht; eine leere, aber verifizierte erfolgreiche Prüfung setzt die Serie zurück. Andere Berechtigungs- und Budgetstopps gelten auch bei deaktiviertem Fehlerlimit. Jede Aktivierung erhält genau eine dauerhafte Zustandsbeobachtung. Wiederholungen erhöhen den Zähler nicht; verspätete Abschlüsse werden in Aktivierungsreihenfolge ausgewertet. Die automatische Pause stoppt künftige Aktivierungen; eine laufende Modellanfrage kann noch enden. Der bestehende Vorfall des Eigentümers vermerkt die Pause, seine Benachrichtigung verwendet gemeinsame Einstellungen und gewählte Ziele. Ein unbestätigter Vorfall erhält eine Pausenmeldung, die den früheren Inbox-Eintrag ersetzt. Fortsetzen oder eine Richtlinienänderung macht wartende Pausenmeldungen ungültig. Die Bestätigung des Vorfalls setzt weder den Ausführungszustand zurück noch die Routine fort. Nach der Fehlerbehebung startet ausdrückliches Fortsetzen ein neues Fehlerfenster. Eine Änderung des Limits startet ebenfalls ein neues Fenster. Die Liste zeigt automatische Pause und Fehlerzahl. Höchstens 50 Beobachtungen pro Routine bleiben gespeichert, ohne Quelltexte oder Anbieterfehler. Sie gehören zum größenbegrenzten persönlichen Datenexport und werden mit Eigentümer oder Routine kaskadierend gelöscht. Migration `0275_routine_health.py` aktiviert den Standard auch für bestehende Routinen: API und Worker gemeinsam aktualisieren. Ein Downgrade entfernt Metadaten und Richtlinie, lässt die Routine jedoch pausiert. ## Geprüfte Routinequellen { #routine-sources } Beim Erstellen hält eine Routine ihre veröffentlichte Quellenkonfiguration, konkreten MCP-Konten, Zustimmungsgenerationen und bekannten Werkzeugnamen fest. Die Prüfung umfasst fixierte Delegaten, Wissenssammlungen, Skills, Kontext und Fähigkeitenkonfiguration; entfernte A2A-Registrierungen behalten außerdem ihre Endpunkt-, Karten- und Zugangsdatenbindung. Reine Änderungen an Anweisungen oder Modelleinstellungen können einer Veröffentlichung folgen. Neue Quellen, geänderte Konten und erneuerte Kontozustimmungen erfordern eine Prüfung im Quellenbereich des Zustimmungsdialogs durch den Eigentümer. Diese Prüfung hebt keine Aktionsfreigaben auf und gewährt keinen Ressourcenzugriff. Aktuelle Berechtigungen gelten weiterhin. Eingereihte Aktivierungen, auf Computer wartende Arbeit und geparkte Läufe behalten ihre ursprüngliche Prüfungsidentität. Eine spätere Prüfung erweitert alte Arbeit nicht. Der Postfach-Poll prüft das Eingangskonto vor dem Lesen; Ereignisse behalten die davor gültige Zustimmung. MCP-Aufrufe verwenden geprüfte Aliase und schließen ungeprüfte Werkzeuge aus. Nicht verfügbare geprüfte Quellen führen zu einer ausdrücklichen Ablehnung. Eine vor der Ausführung erforderliche Prüfung erscheint im bestehenden Aufmerksamkeitsfall des Eigentümers und zählt nicht als Modellfehler. Migration `0276_routine_sources.py` erteilt bestehenden Routinen keine Zustimmung. Alte Worker abarbeiten lassen, API und Worker gemeinsam aktualisieren und alte Routinen vor dem Ausführen ausdrücklich prüfen. Nur der Eigentümer in der Konsole kann die Zustimmung ersetzen; API-Schlüssel, Identitätsübernahmen und unbeaufsichtigte Agenten dürfen dies nicht. Routinen mit Budget verwenden einen gemeinsamen Nebenläufigkeitsschlüssel der Warteschlange, damit gleichzeitige Ereignisse nicht mehrere Modellaufrufe gegen denselben abgeschlossenen Ausgabenstand zulassen. Speicherextraktion läuft ebenfalls nacheinander pro Wartungsrolle einer Organisation. Jeder Auftrag prüft das aktuelle Budget erneut; ein bereits zugelassener Aufruf darf noch über die Grenze hinaus enden. Bereitstellungsweite Meldungen über pausierte Jobs behalten den normalen Gelesen-/Verworfen-Status einer Benachrichtigung; sie werden nicht als Organisationsfälle dargestellt. ### Routinen, Arbeitsgespräche und Inbox { #routines-working-conversations-and-inbox } **Routines** listet zeitgesteuerte Aufgaben, ereignisgesteuerte Aufgaben und Vorlagen zur lesenden Überwachung gemeinsam. Ein Trigger bestimmt, _wann_ eine Routine läuft; die Überwachungsvorlage begrenzt, _was_ sie tun darf. Für eine Verantwortung genügt eine Routine, statt Watch und Zeitplan für dasselbe Postfach. Wählen Sie **Nach jeder abgeschlossenen Ausführung** (`result_mode: report`, der bisherige Standard) für Berichte oder **Nur wenn etwas Aufmerksamkeit braucht** (`result_mode: inbox`) für stille Prüfungen. Der Agent liefert relevante Quellelemente mit stabilen Element- und Revisionskennungen, Kontext und dem vollständigen angeforderten Entwurf. Ein leeres Ergebnis erzeugt weder Gespräch noch Abschlussmeldung. Jedes Element erhält ein privates Arbeitsgespräch und einen Inbox-Vorgang. Wiederholte Revisionen werden dauerhaft dedupliziert, auch nach Erledigung. Eine neue Revision aktualisiert dasselbe Gespräch und öffnet den Vorgang erneut; stummgeschaltete Elemente bleiben stumm. Öffnen Sie das Gespräch aus der Inbox, um den Entwurf zu ändern oder den nächsten Schritt zu beauftragen. Lesen und Bearbeiten versenden nichts und erledigen den Vorgang nicht. Werkzeugberechtigungen und Freigaben gelten weiterhin. Über die Plattform-MCP-Werkzeuge `list_inbox_cases` und `settle_inbox_case` findet der Agent den Vorgang und erledigt ihn nach erfolgreicher beauftragter Arbeit oder einer ausdrücklichen Erledigungsbestätigung. Inbox und Chat teilen den Zustand; beim Abschluss entfallen ausstehende Benachrichtigungen. Lesende Überwachung kann Entwurfstext im Gespräch vorbereiten, aber nicht im Quelldienst speichern oder senden. Nächtliche Vorschläge bleiben eine optionale, unabhängige Analyse jüngster Anfragen und Erinnerungen, kein weiterer Postfachscan. Überwachung nutzt das Modell ihres gewählten Agenten und benötigt kein Vorschlagsmodell. Die Organisation bestimmt weiterhin Verfügbarkeit, Häufigkeit und das gemeinsame Hintergrundbudget pro Person. Gewöhnliche Routinen behalten Agenten-, Organisations- und optionale eigene Budgetgrenzen. Tage und Uhrzeit bestimmen auch die Zusammenfassung nicht dringender Funde. Die morgendliche Zusammenfassung listet bereits gespeicherte, nicht dringende Funde ohne weiteren Modellaufruf oder Quellenscan. Sie ist eine Benachrichtigung, keine zweite Routine. Die Inbox-MCP-Werkzeuge stehen angemeldeten Sitzungen und vertrauenswürdigen Zugangsdaten eines privaten Chats zur Verfügung; Letztere sind auf Vorgänge dieses Chats beschränkt. Gewöhnliche API-Schlüssel, Hintergrundläufe und Ersatzidentitäten des Herausgebers dürfen private Inbox-Vorgänge weder lesen noch erledigen.