docs/governance.pl.md
Rama main · réplica de lectura del repositorio original.
---
source_sha: "b373de64c03d"
---
# Governance { #governance }
Budżety, zatwierdzenia, alerty i ślad audytowy. Cztery rzeczy, które sprawiają,
że za platformę agentową można ręczyć kartą kredytową.
Wszystkie działają identycznie na każdej powierzchni (surface), ponieważ każda
powierzchnia przechodzi przez jeden runner.
Najnowszą z tych powierzchni jest **trigger**: agent uruchamiający sam siebie
według harmonogramu albo na przychodzące zdarzenie — issue na GitHubie,
przychodzący e-mail (zobacz [Koncepcje](concepts.md#trigger)).
Korzysta z tej samej ewidencji kosztów, kolejki zatwierdzeń i audytu.
[Polityka działań bez nadzoru](#routine-action-consent) zaostrza wybór wywołań
wymagających zatwierdzenia; jawna stała zgoda wskazuje konkretne działania i wersję.
Praca bez nadzoru określa też, co dzieje się z **odmową**:
- Odpalony run, który zatrzyma budżet, kończy się jako `budget_exceeded` i
**czeka na kolejne odpalenie**, zamiast być ponawiany.
- Odpalony run, którego twórca nie może już wykonać — odszedł z organizacji albo
stracił swój grant do agenta — **wyłącza trigger** i zapisuje wpis audytowy
mówiący dlaczego.
- Odpalony run, który przewraca się na samym modelu — awaria providera,
unieważniony klucz — zostaje zapisany jako `failed` i zostawiony do kolejnego
odpalenia, zamiast wywracać heartbeat w ponawianie uderzeń w tę samą ścianę.
`last_run_id` triggera dalej na niego wskazuje, żeby jego historia pozostała
uczciwa.
Odmowa ponawiana przez heartbeat co minutę byłaby rachunkiem albo alertem, który
nigdy się nie kończy.
!!! warning "Czego nie połykamy: awarii, która nigdy nie stała się zapisem"
Jeśli run osiągnął stan terminalny, ale zapis, który go rejestruje, nie
został zacommitowany — zapis transkryptu albo stanu rozmowy, który podniósł
wyjątek już po tym, jak odpowiedź była gotowa — odpalenie **wywraca flow**,
zamiast raportować sukces. Run zapisany do połowy zostaje wycofany, zamiast
zostać zacommitowany jako ukończony, choć nic za nim nie stoi.
Trigger zdarzeniowy dokłada na swojej krawędzi jeszcze jedną odmowę: webhook,
którego podpis nie weryfikuje się względem własnego sekretu triggera, to 403,
które w ogóle nie dociera do runnera.
## Dostarczanie wyników rutyny { #routine-delivery }
Rutyna utworzona w prywatnym czacie przez MCP platformy odpowiada w tym czacie,
chyba że wybierzesz inny cel. Jej karta w rozmowie pokazuje aktualny stan,
następną aktywację oraz istniejące przyciski edycji, wstrzymania i usunięcia.
Dziennik uruchomień pozostaje dostępny w Routines.
Przed zapisaniem gotowej odpowiedzi platforma ponownie sprawdza aktywne członkostwo twórcy i jego bieżące prawo zapisu w wybranej rozmowie. Usunięty lub niedostępny cel pozostawia odpowiedź w logu wykonania, a rutyna pokazuje **Ostatni wynik niedostępny w czacie**. Błąd bazy podczas dostarczania wiadomości jest izolowany, aby nie cofał zakończonego wykonania ani nie uruchamiał ponownie agenta.
Powiadomienie o zakończeniu wskazuje rozmowę dopiero po udanym dostarczeniu.
`last_delivery_status` rozróżnia dostarczenie, sam log, brak odpowiedzi i niezakończony przebieg; stare wpisy pozostają nieznane do kolejnego wykonania. Migracja `0265_trigger_delivery_status.py` dodaje ten stan bez zgadywania wcześniejszych sukcesów.
Ustaw `ends_at` z przesunięciem UTC dla pracy ograniczonej w czasie oraz
`monthly_budget_usd` dla niższego limitu miesięcznego. Narzędzie aktualizacji
MCP pozwala usunąć oba ustawienia. Zwykły klucz API nie może upoważnić rutyny
do korzystania z osobistych kont jego wystawcy. Takiej zgody nie nadają też uruchomienia
w tle, podszywanie się ani wznowione tury grupowe lub publiczne.
Przebiegi wykonywane co minutę rozliczają minione terminy niezależnie od kolejnej aktywacji, także dla wstrzymanych rutyn i obserwacji. Wyłączają aktywną pracę, zamykają jej nieaktualne blokery i anulują oczekujące powiadomienia bez wywołania modelu. Worker nadrabia zaległości po restarcie; sprzątanie może poczekać na workera lub kolejną partię, a kontrole terminu przed narzędziami i dostarczeniem odpowiedzi od razu odmawiają pracy po terminie.
Migracja `0269_routine_end_cleanup.py` dodaje znacznik rozliczonego terminu i częściowe indeksy oczekujących zakończeń. Nie tworzy zastępczych rutyn, zgód ani ustaleń. Przesunięcie końca ponownie uzbraja sprzątanie; ponowny przebieg nie powtarza rozliczonych efektów. Downgrade usuwa wyłącznie te dane pomocnicze, zachowując ustawiony koniec i tożsamość rutyny.
Obserwowane połączenie MCP zapisuje także generację autoryzacji. Zakończona ponowna zgoda OAuth lub jawna wymiana tokenu albo adresu unieważnia starą zgodę obserwacji na źródło, nawet gdy identyfikator połączenia pozostaje ten sam. Zwykłe odświeżenie tokenu, etykiety i zmiana konta domyślnego nie podmieniają tej tożsamości. Ponownie sprawdzone źródło jest dostępne dla późniejszych uruchomień; stary wstrzymany run nie może pożyczyć nowej zgody.
Po migracji `0270_mcp_authorization_generation.py` jawnie przejrzyj źródła istniejących obserwacji (`review_sources=true` lub **Ponownie zatwierdź dołączone źródła** w edytorze obserwacji). Starsze polityki pozostają czytelne, ale nie zgadują konta za historyczną autoryzacją. Poświadczenia dostawcy nie są odczytywane dla konta spoza dopuszczonej generacji. Opróżnij stare workery i zaktualizuj API oraz workery razem. Przed downgrade wstrzymaj obserwacje; wznawiaj je dopiero z kodem sprawdzającym generację i po przeglądzie źródeł. Wymiana poświadczeń czyści listę narzędzi poprzedniego konta do kolejnego udanego sprawdzenia.
Uruchomienia rutyn zachowują konta MCP i aliasy narzędzi dopuszczone dla danego wykonania. Wznowione wywołanie używa tych aliasów: zmiana nazwy konta lub konta domyślnego nie przekierowuje istniejącego zatwierdzenia. Delegaci pozostają w granicach dopuszczonych kont. Przed wywołaniem ponownie sprawdzane są właściciel konta, adres, generacja autoryzacji i lista narzędzi; nowe lub zmienione konta są odfiltrowywane przed odczytem poświadczeń.
Wstrzymana rutyna bez tego zapisu dopuszczenia nie może kontynuować wywołań MCP zarządzanych kont. Po przeglądzie dostępu rozpocznij nowy run. Domyślne zachowanie agenta interaktywnego i jawne polityki zatwierdzania nie zmieniają się; wyjątki zatwierdzania dla konkretnych kont pozostają osobnym mechanizmem względem kontroli źródła.
Wybór wersji pozwala śledzić domyślną opublikowaną wersję albo przypiąć jedną niezmienną wersję. Publikacja nie przesuwa pinu. Usuń go, aby ponownie śledzić wersję domyślną. Nazwane środowisko i pin wersji wzajemnie się wykluczają; nadal obowiązują bieżące uprawnienia, budżety i zatwierdzenia.
Formularz rutyny pozwala ustawić lub usunąć datę zakończenia. Data używa pokazanej przy polu strefy urządzenia, niezależnie od strefy harmonogramu cron, i trafia do API jako timestamp z offsetem. Edycja innego pola zachowuje dotychczasowy koniec bez zmian.
Rutyna przed każdym narzędziem ponownie sprawdza twórcę, aktywne członkostwo, rolę i granty uruchamiania, także w delegacjach i po zatwierdzeniu. Pauza, usunięcie, wygaśnięcie, cofnięcie zgody na osobiste konta lub zmiana tych uprawnień odmawia kolejnych wywołań. Kontrola nie cofa efektu już wysłanego do zewnętrznej usługi. Migracja `0260_routine_run_origin` zachowuje pochodzenie po usunięciu rutyny; starszy zaparkowany harmonogram, który utracił pochodzenie, jest odrzucany zamiast pożyczać uprawnienia zatwierdzającego.
## Budżety { #budgets }
!!! abstract "Trzy poziomy, i nie są to warianty jednej liczby"
Limit agenta mierzony względem sumy całej organizacji wyczerpują runy jego
sąsiadów; limit organizacji mierzony względem jednego agenta nie jest żadnym
pułapem. Zobacz [dlaczego nie da się ich zwinąć w jedno](#why-they-cannot-be-collapsed).
| Poziom | Ustawiany w | Mierzy | Podnosi |
| --- | --- | --- | --- |
| **Miesięczny agenta** | spec agenta | runy tego agenta | ten, kto może edytować agenta |
| **Miesięczny działu** | ustawienia działu | runy uruchomione przez jego członków, na dowolnym agencie | ten, kto ma `members:manage` i `budgets:manage` |
| **Miesięczny organizacji** | ustawienia organizacji | każdy run _oraz_ ingestię w organizacji | ten, kto ma `budgets:manage` |
**Nowa organizacja startuje z już ustawionym pułapem organizacji** —
`DEFAULT_ORG_MONTHLY_BUDGET_USD` wdrożenia ([konfiguracja](configuration.md),
$100 zaraz po instalacji) — więc świeżego tenanta nie dzieli od niespodziewanego
rachunku jeden rozbiegany agent. Od tego momentu jest to zwykły limit:
edytowalny w wierszu organizacji i egzekwowany dokładnie tak samo jak ustawiony
ręcznie. Wdrożenie, które woli startować bez limitu, zostawia to ustawienie
puste; tak czy inaczej limit istniejącej organizacji nigdy nie jest z tego
powodu zmieniany.
### Dlaczego nie da się ich zwinąć w jedno { #why-they-cannot-be-collapsed }
Kiedyś dało się, przez `min()`, i wynik był błędny. Limit agenta mierzony
względem sumy całej organizacji wyczerpują runy jego sąsiadów, i dokładnie to
sprawia, że nie jest on limitem. Limit organizacji mierzony względem wydatków
jednego agenta nigdy by nie zawiązał.
Więc każdy limit mierzy własną wielkość, a wyszukiwanie podróżuje razem z
ograniczeniem. Agent za $5 pod pułapem $50 wiąże teraz wtedy, gdy _on sam_ wydał
$5, a odmowa nazywa ten limit, który faktycznie zawiązał, zamiast wnioskować go
z tego, która z dwóch liczb była mniejsza.
Agent dalej nie może poluzować pułapu organizacji: wpis organizacji jest obecny
ze swoją własną liczbą niezależnie od tego, o co prosi spec, a wydatki agenta są
częścią wydatków organizacji — więc agent za $100 pod organizacją za $10 zostaje
zatrzymany na $10.
Oba limity są czytelne tam, gdzie są ich wydatki: limit organizacji w jej własnym
wierszu (`GET /orgs/{org_id}`), a limit każdego agenta jako `budget_monthly_usd`
na liście agentów — liczba z _opublikowanej_ wersji, bo to ją egzekwuje runner, a
nie to, co akurat obiecuje draft. Karta zapasu na dashboardzie zestawia je z
`GET /spend`, więc zbliżający się limit widać, zanim w historii runów zacznie
pojawiać się `budget_exceeded`.
### Limit działu { #a-departments-cap }
[Dział](departments.md#a-departments-budget) może mieć własny miesięczny limit.
Stoi między pozostałymi dwoma: run jest sprawdzany względem limitu agenta,
potem każdego działu z limitem, w którym jest osoba, która go uruchomiła, a na
końcu względem limitu organizacji. Każdy dział liczy miesiąc własnych członków,
więc osobę z Finansów i Sprzedaży zatrzyma ten z dwóch, który wyczerpie się
pierwszy, a odmowa go nazwie - _Sales department monthly budget exhausted_.
Lider działu i administratorzy dostają jedno powiadomienie, gdy dział przekroczy
80% miesiąca, i kolejne, gdy limit zatrzyma run. Run, którego nikt nie
uruchomił - harmonogram, gość na kanale - nie należy do żadnego działu i nie
jest liczony.
### Egzekwowanie odbywa się przed żądaniem { #enforcement-is-before-the-request }
!!! danger "Przed, a nie po"
Sprawdzanie po fakcie oznacza, że żądanie, które złamało budżet, zostało już
opłacone — a pętla może przekraczać limit o jedno drogie wywołanie za każdym
razem.
!!! important "Nieudany run i tak zapisuje, ile wydał"
Budżet, który ignoruje awarie, nie jest budżetem. Księgowanie dzieje się w
bloku `finally` na każdej powierzchni, a commit jest jawny, a nie zostawiony
kontekstowi sesji — ten wycofuje się przy każdym wyjątku i przy anulowaniu
nie jest osiągany w ogóle.
!!! important "Punkt odniesienia to to, co wydały _inne_ runy"
Oba wyszukiwania pomijają wiersz pytającego runa. Wznowiony run zachowuje
swój wiersz, a do tego czasu `finish_run` zacommitował już to, co wydał —
podczas gdy rejestr jest ponownie zasiewany tą samą kwotą, i musi być, bo
inaczej dokończenie kontynuacji nadpisałoby koszt tym, co kosztowała sama
kontynuacja. Liczony w obu miejscach, agent z limitem $10, który wydał $6 i
zaparkował, wracał z `6 + 6 = 12` przy pierwszym żądaniu do modelu i dostawał
odmowę, mając $4 zapasu, mówiąc swojemu właścicielowi, że osiągnął limit,
którego wykorzystał w 60%. Limit organizacji dubluje się identycznie.
Bramka jest twardym stopem dla runa, który _widzi_ wydatki, a własny koszt runa
ląduje w jego wierszu dopiero wtedy, gdy run się kończy.
Więc punkt odniesienia, który run odczytuje, to suma runów już zakończonych, a
**równoległe runy są dla siebie nawzajem niewidzialne**. Pięćdziesiąt runów
startujących razem w organizacji będącej jedno wywołanie od swojego limitu
odczytuje ten sam punkt odniesienia poniżej limitu, każdy rusza dalej i razem
przekraczają limit nawet o swój łączny koszt.
To właściwość agregatu, który nie ma pojedynczego wiersza do zablokowania — w
przeciwieństwie do przekroczenia na run i na pętlę powyżej, które sprawdzenie
przed żądaniem faktycznie ogranicza. Dlatego limit jest pułapem na
**zacommitowane** wydatki, a nie bramką szeregującą jednoczesne runy.
Wdrożenie, które potrzebuje ścisłego limitu, przepuszcza swoich agentów przez
jedną kolejkę, zamiast równolegle.
### Evale wydają z tych samych budżetów { #evals-spend-from-the-same-budgets }
[Eval run](evals.md) składa się ze zwykłych runów, więc każdy przypadek jest
sprawdzany względem limitów agenta i organizacji przed każdym żądaniem do
modelu, a to, co wyda, liczy się do obu - nie ma osobnego budżetu na evale.
Duży dataset może więc zużyć miesięczny budżet, którego potrzebuje ruch
produkcyjny. Start runa pokazuje, ile każdy limit zachowa po wydaniu szacunku,
i jest odrzucany, zanim cokolwiek się uruchomi, gdy szacunek przekracza to, co
zostało w którymkolwiek z nich; zob.
[Budżet, który zostaje](evals.md#the-budget-it-leaves).
### Run kosztuje więcej niż jego żądania do modelu { #a-run-costs-more-than-its-model-requests }
Wyszukiwanie w wiedzy najpierw osadza pytanie, zanim będzie mogło go użyć do
szukania, a to osadzanie jest księgowane na runa, który o nie poprosił. Usługa
osadzeń jest globalna dla procesu — obsługuje naraz każdy run i każde zadanie
ingestii — więc księguje na tym runie, który jest _aktualnie mierzony_, zamiast
przyjmować budżet jako argument.
Przez co licznik staje się czymś, o czym powierzchnia może zapomnieć, a
zapomnienie jest ciche: żadnego wyjątku i żadnego ostrzeżenia, po prostu run,
który raportuje mniej, niż wydał, i miesiąc organizacji, który tego nigdy nie
zobaczy. Dlatego licznik należy do przygotowanego runa, a nie do powierzchni.
Otwarcie go nie jest krokiem, o którym nowa powierzchnia musi wiedzieć, bo nie
ma sposobu, żeby wykonać przygotowanego agenta bez niego.
Capability [Długie rozmowy](reference/capabilities.md#context-management) to ta
druga taka rzecz. Jej strategia podsumowująca zapisuje podsumowanie przez
agenta, którego buduje sama, więc to żądanie nie przechodzi przez żadną bramkę
budżetu; capability księguje to, co kosztowało, na tym samym liczniku. Bycie
_poza_ bramką ma jedną konsekwencję, którą warto znać: wydatek zostaje zapisany,
a nie odrzucony, więc kompaktowanie przekraczające limit zatrzymuje run na
kolejnym żądaniu.
### Koszt, którego nie dało się zmierzyć, mówi o tym wprost { #a-cost-that-could-not-be-measured-says-so }
Koszt naliczony przez Stav ma pierwszeństwo przed katalogiem: `usage.cost` z
`cost_currency` lub nagłówki `x-stav-cost` i `x-stav-cost-currency`. Dotyczy to
Chat Completions i Responses, także ze streamingiem. USD używane jest wprost;
NOK wymaga `STAV_NOK_PER_USD` ([konfiguracja](configuration.md)), utrzymywanego
przez operatora. To NOK za dolara: wartość `10` dzieliłaby NOK przez 10;
nie jest to rekomendacja bieżącego kursu. Kurs nie jest pobierany automatycznie.
Usage zachowuje kwotę, walutę, kurs i koszt w USD, a ledger oznacza źródło jako
raportowane przez dostawcę. Nieprawidłowa kwota, nieobsługiwana waluta lub brak
przeliczenia pozostawiają wycenę z katalogu.
Bez użytecznego kosztu raportowanego przez dostawcę wycena korzysta z
`genai-prices`. Pakiet nie zna każdego modelu. Gdy run trafia na taki, dla którego nie ma
wpisu, to żądanie jest księgowane na zero, a run zostaje oznaczony jako
`cost_is_partial` — suma jest zaniżona dokładnie o to, ile kosztowały tamte
żądania, a uczciwe jej odczytanie to **dolna granica**.
Ta flaga podróżuje teraz całą drogę w dół. Jest w wierszu runa, w wierszu
wiadomości, który zapisuje tura, i w sumie, którą raportuje rozmowa; każda
powierzchnia rysująca pieniądze rysuje przed nimi `≥` zamiast liczby, którą
czyta się jako dokładną. Null w wiadomości zapisanej, zanim kolumna istniała,
oznacza _nie zapisano_, co nie jest tym samym twierdzeniem co „dokładne” —
klient oznacza tylko to, co wie.
**Każda powierzchnia zapisuje to teraz, a każda tura zapisuje swój własny
udział.** Wiadomość zapisana przez kanał, API albo widget do niedawna nie niosła
żadnego kosztu, więc wątku na Slacku nie dało się zsumować. Zapisywana liczba to
_różnica_ względem tego, co deklarują już wcześniejsze tury tego runa, a nie
liczba z wiersza runa: wiersz runa jest skumulowany, a run, który zaparkował i
został wznowiony, zapisuje dwie tury asystenta — ostemplowanie obu liczbą z
wiersza policzyłoby zaparkowaną połowę dwa razy. Wiadomości jednego runa sumują
się więc dokładnie do tego, co run deklaruje, że wydał.
**Tura, w połowie której run został zatrzymany, mówi o tym wprost.** Anulowany
run zostawia to, co agent zdążył napisać, gdy socket się zamknął albo wciśnięto
`stop`, a czyta się to dokładnie jak skończoną odpowiedź — więc czytelnik bierze
uciętą za wszystko, co agent miał do powiedzenia, a wydane pieniądze wyglądają,
jakby kupiły właśnie to. Transkrypt niesie status runa dla każdej tury, a czat to
zaznacza.
### Jak pełne jest okno kontekstu { #how-full-the-context-window-is }
Trzeci pułap, i ten, którego nikt nie widzi nadchodzącego. Budżet odmawia
komunikatem, z którym da się coś zrobić. Workspace odmawia zapisu. **Okno
kontekstu** zostaje odrzucone przez providera, w środku odpowiedzi, a run po
prostu się wywraca.
Dlatego każdy agent nosi wskaźnik — nie tylko ten, który ma podpiętą
capability [Długie rozmowy](reference/capabilities.md#context-management), bo
ostrzeżenie liczy się najbardziej dla agenta, który _nie_ będzie kompaktował.
Raportuje on, ile tokenów niosło ostatnie żądanie tury, _po_ ewentualnym
kompaktowaniu: odczyt spada, gdy kompaktowanie działa, bo mierzy to, co wyszło, a
nie to, co trzyma rozmowa.
Ta liczba to własne `input_tokens` providera, a nie oszacowanie historii.
Liczenie znaków nie widzi definicji narzędzi, a te są płatne przy każdym żądaniu
— tysiące tokenów u agenta z wiedzą, sandboksem i delegacją, czyli jedna trzecia
prawdziwej liczby brakująca dokładnie w momencie, w którym ta liczba ma
znaczenie.
**Licznik jest zapisywany na turze; udział procentowy nie.**
To, ile jest historii, przeżywa zmianę modelu. To, jaką częścią okna ona jest,
już nie — a czat pozwala przełączyć model między turami.
Historia o długości 500 000 tokenów to połowa modelu z kontekstem 1M i **390%**
modelu ze 128K, a to drugie to żądanie, które provider odrzuca wprost. Udział
zamrożony razem z odczytem dalej pokazywałby „50%”.
Dlatego mianownik jest rozstrzygany tam, gdzie znany jest wybór modelu, z okna
zapisanego w profilu modelu i ze stojącego za nim rejestru cen. Gdy żadne z nich
nie potrafi tego powiedzieć, **udział nie jest rysowany w ogóle** — procent
liczony względem założonego okna to zgadywanie podane jako pomiar.
Ta zmiana modelu jest też tym, do czego służy kompaktowanie. Jego progiem jest
**ułamek rozstrzygany na każde żądanie** względem modelu, do którego to żądanie
idzie, więc historia, która wygodnie mieściła się w starym oknie, zostaje
skompaktowana już przy następnej turze pod nowym — zanim żądanie wyjdzie, a nie
po tym, jak provider je odrzuci. Agent bez podpiętego kompaktowania ma zamiast
tego wskaźnik, i nic więcej.
**Próg mierzy to, co zmierzył provider.** Kotwiczy się na najnowszej odpowiedzi
niosącej zużycie od providera — `input_tokens` tamtego żądania policzyły
instrukcje, każdy schemat narzędzia i każdą wcześniejszą wiadomość — i szacuje
wyłącznie to, co pojawiło się po niej. Dlatego odtwarzana rozmowa niesie koszt
każdej odpowiedzi: bez kotwicy próg liczy znaki, a prawdziwy agent odczytał tu 9
tokenów tam, gdzie provider policzył 3859. Wskaźnik pokazywał 77%; próg nie
widział nic do zrobienia.
**Podsumowanie jest zachowywane.** Kompaktowanie przepisuje wiadomości jednego
runa; wątek między turami jest odbudowywany z transkryptu, więc podsumowanie
było kiedyś wyrzucane na granicy tury, a następna tura kupowała kolejne, nad
historią dłuższą o jedną turę — dwie kolejne tury prawdziwej rozmowy zapłaciły
tu każda za podsumowanie tych samych pięciu wiadomości, a drugie ogłosiło, że
podsumowuje dziewięć. Dlatego skompaktowana historia jest zapisywana do rozmowy
razem z tym, jak daleko sięga, a następna tura startuje od niej i odtwarza tylko
to, co powiedziano od tamtej pory. Ponowne otwarcie wątku zastaje to samo, co
widzi model.
Zachowywane jest tylko podsumowanie. Odrzucenie najstarszych wiadomości i
wyczyszczenie wyników narzędzi nic nie kosztują przy powtórzeniu, a zapisanie ich
utrwaliłoby stratę, która dziś jest rozważana na nowo względem okna przy każdej
turze.
Jedno ustawienie nie może zadziałać i mówi o tym, zamiast działać. Gdy same
instrukcje i schematy narzędzi przekraczają próg, żadne podsumowanie nie zejdzie
poniżej niego — nie ma ich w historii do podsumowania. Każde żądanie kupowałoby
wtedy podsumowanie, które nic nie zmienia. Kompaktowanie jest pomijane, czat mówi
dlaczego, a naprawa należy do autora: większe okno albo wyższy ułamek.
Ta odmowa opiera się na liczbie, którą produkuje _odpowiedź_, więc tura nie może
zmierzyć swojej własnej, zanim musi zdecydować — a tura czatu to zwykle jedno
żądanie. Rozmowa niesie ostatni odczyt, a run od niego startuje. Pierwsza tura
wątku nie ma się więc na czym oprzeć i kompaktuje zgodnie z konfiguracją; od
drugiej odmowa jest dostępna. Odrzucane są tylko te strategie, które coś kupują:
odrzucanie najstarszych wiadomości i czyszczenie wyników narzędzi nie wołają
modelu, więc działają niezależnie od tego, jakie jest okno.
### Delegacja wydaje z budżetu rodzica { #delegation-spends-the-parents-budget }
Run może zawierać całą rozmowę innego agenta — zobacz
[delegat kontra wbudowany specjalista](concepts.md#delegate-vs-inline-specialist).
Jeden run ma **jeden rejestr wydatków** i każdy delegat zapisuje do niego. To
właśnie sprawia, że limit rodzica widzi wydatki delegacji przed swoim kolejnym
żądaniem do modelu, dokładnie w momencie, w którym delegacja zwielokrotnia to,
ile może kosztować tura. Każdy wpis jest ostemplowany delegacją, która go
zaksięgowała, i to dzięki temu jeden rejestr dalej odpowiada na pytanie „ile
kosztował _ten_ delegat” — zobacz niżej.
Wynika z tego, że **limity, które wiążą wewnątrz delegacji, to limity rodzica**.
Własny `budget.monthly_usd` delegata nie jest egzekwowany w trakcie runa rodzica:
dwie bramki mierzące jeden rejestr liczyłyby każde żądanie podwójnie, a pułap,
który ma znaczenie, to ten na runie, który ktoś uruchomił. Własny limit delegata
dalej rządzi runami _samego delegata_.
Każdy delegat wycenia jednak własne żądania, bo bramka wycenia to, co zapisuje:
delegat na Anthropic mierzony przez bramkę zbudowaną dla OpenAI byłby wyceniany
według złego katalogu — po cichu i zwykle jako niewyceniony.
Istnieją trzy dalsze pułapy, bo budżet jest kiepskim sposobem na zatrzymanie
fan-outu — orientuje się dopiero wtedy, gdy pieniądze już poszły. `max_depth`
ogranicza zagnieżdżenie, `max_fanout` ogranicza, ile delegacji działa naraz, a
własne `max_steps` każdego delegata ogranicza jego pętlę. Zobacz
[capability `subagents`](reference/capabilities.md#delegation).
Dwa z tych trzech należą do samego delegata, i to jest granica, której budżet nie
przekracza: `max_steps` jest odczytywane ze speca delegata, a jego `max_depth`
ogranicza, jak głęboko _on_ może zejść, niezależnie od tego, ile miejsca zostało
jego wołającemu. Limit na wydatki jest limitem na run, który ktoś uruchomił;
limit na zagnieżdżenie to decyzja podjęta przez autora delegata i przeczytana
przez jego recenzentów, więc wołający nie może jej poszerzyć.
### Jako co zapisywany jest zdelegowany run { #what-a-delegated-run-is-recorded-as }
Delegacja do **opublikowanego** agenta dostaje własny wiersz w `agent_runs`,
niosący `parent_run_id` i id zadania delegacji. **Wbudowany specjalista** nie
dostaje żadnego: nie ma agenta, któremu można by go przypisać, więc jego koszt
należy do _runa_, a zapisem jest wywołanie narzędzia w transkrypcie.
Tylko którego runa — na to pytanie odpowiedziało
[#228](https://github.com/vstorm-co/agenticos/issues/228).
- Specjalista bezpośrednio pod własnym agentem runa księguje się na **wiersz
najwyższego poziomu**, który i tak jest całym rejestrem.
- Specjalista pod **opublikowanym delegatem** księguje się na wiersz _tego
delegata_, a nie na ten najwyższego poziomu — więc miesiąc delegata zawiera to,
co wydał jego specjalista, i to jedyne miejsce, w którym mogłoby to uczciwie
wylądować.
Każdy wpis w rejestrze niesie więc dwa przypisania: delegację, która go wykonała,
dla panelu, oraz najbliższy wiersz agenta, na który się księguje, dla miesiąca.
Oba są równe przy każdym żądaniu, które opublikowany delegat wykonuje na własny
rachunek, a rozjeżdżają się tylko pod wbudowanym specjalistą — którego panel
zachowuje własny udział, podczas gdy jego wydatki trafiają do wiersza przodka.
!!! important "Wiersz rodzica jest źródłem prawdy; wiersz dziecka to jego udział w nim"
Delegat wydaje do wspólnego rejestru, a **każdy wpis w tym rejestrze niesie
delegację, która go wykonała**. Koszt delegacji to suma jej własnych wpisów —
żądań wystawionych przez jej własnego agenta, wycenionych raz, przez to samo
wyszukiwanie, którego używa suma runa. Jest dokładny w obu trybach i na
każdej głębokości, i nie zależy od tego, kiedy akurat delegacja została
rozliczona.
Kiedyś zależał dokładnie od tego i były to dwie usterki. Liczba była
*przyrostem* wspólnej sumy w trakcie delegacji, więc delegacja w tle —
rozliczana przy kolejnym odpytaniu, które może nastąpić po tym, jak rodzic
już odpowiedział — wchłaniała wszystko, co rodzic wydał w międzyczasie:
delegat, który wydał $0,01, był zapisywany jako $0,51, jeśli rodzic wydał
potem $0,50. A delegat, który deleguje dalej, miał wydatki własnych delegatów
wewnątrz swojego okna, a ich wiersze zapisują je ponownie, więc jego suma
miesięczna liczyła jego wnuki.
Rozdzielenie tego na rejestr *na agenta* to wciąż projekt, którego należy
unikać — to właśnie on sprawia, że limit rodzica w ogóle przestaje wiązać.
Jeden rejestr z przypisaniami zachowuje obie właściwości: limit rodzica widzi
każde żądanie przed kolejnym, a każdy zdelegowany wiersz mówi, ile wydał ten
jeden agent, wliczając jego własnych wbudowanych specjalistów i wyłączając
jego opublikowanych delegatów.
Wiersz rodzica pozostaje źródłem prawdy dla runa. Jego `cost_usd` to cały
rejestr, z delegatami włącznie, i to nim obciążana jest organizacja; wiersze
dzieci dzielą te same pieniądze według agenta i nigdy do nich nie dodają.
`cost_is_partial` jest również per wiersz: rodzic na modelu, którego
`genai-prices` nie zna, czyni sumę rodzica dolną granicą i nie mówi nic o
delegacie, który działał na modelu wycenionym.
`started_at` i `ended_at` zdelegowanego wiersza to **własny** przedział
delegacji, odczytany z uchwytu zadania, który biblioteka stempluje, gdy
delegat startuje i gdy kończy — a nie moment rozliczenia wiersza. Liczona od
rozliczenia, delegacja w tle czytała się jako run o zerowym czasie trwania w
złym momencie, uszeregowany po pracy, która skończyła się przed nią; dwie,
które naprawdę na siebie zachodziły, były zapisywane w tej samej chwili i nic
nie mówiło, że tak było. Terminalny uchwyt z końcem, ale bez startu — delegat
anulowany albo przewrócony, zanim zaczął się wykonywać — zapisuje przedział o
zerowej długości w tym końcu, nigdy null; a tam, gdzie biblioteka odmawia,
zanim uchwyt w ogóle powstanie — nieznane `chat_trace_id` — żaden zdelegowany
wiersz nie jest zapisywany. Delegacja, która zaparkowała na zatwierdzeniu,
rozciąga się na każdą turę, w której działała: jej najwcześniejszy start jest
przenoszony przez parkowanie tak samo jak jej koszt (niżej), więc wiersz
zaczyna się wtedy, gdy delegat zaczął po raz pierwszy, i kończy się wtedy, gdy
skończył naprawdę — a nie przy wznowieniu, które go rozliczyło. Nie są one
sumowane tak jak koszt; uczciwą odpowiedzią jest start pierwszego segmentu i
koniec ostatniego.
**Delegacja, która zaparkowała na zatwierdzeniu, to więcej niż jeden udział.**
Jej tury działały w różnych procesach, na różnych rejestrach, a rejestr
wznowionej tury to świeży obiekt, który nie trzyma niczego sprzed parkowania —
więc wiersz dziecka zapisuje wszystkie segmenty dodane do siebie: stan
zaparkowany zachowuje to, ile delegacja kosztowała, gdy się zatrzymała, a tura,
która ją kończy, dokłada własny udział. Zapisywany jest jeden wiersz, raz, przez
turę, w której delegacja się kończy — delegat, który zaparkował dwa razy,
zostawia trzy segmenty i jeden wiersz.
`cost_is_partial` jest przenoszona tak samo, i z powodu, którego pieniądze nie
dzielą: jest teraz per wiersz, a nie per run, więc delegat, który wykonał
niewycenione żądanie _przed_ zatwierdzeniem i wznowił się na wycenionym modelu,
inaczej deklarowałby w swoim wierszu dokładny koszt. Flaga jest prawdziwa, jeśli
była prawdziwa dla któregokolwiek segmentu.
Warto to powiedzieć, bo awaria, którą to zastępuje, była niewidzialna. Wiersz
trzymał kiedyś tylko to, co delegat wydał _po_ ostatnim wznowieniu, co przy
zwykłym kształcie — wykonaj pracę, potem poproś o pozwolenie na działanie na jej
wyniku — jest tą mniejszą połową. Nic nie przestawało się zgadzać, bo pieniądze
przez cały czas były w wierszu rodzica; błędna była każda liczba odpowiadająca na
pytanie „ile kosztował ten delegat”.
Wiersz dziecka jest tym, co czyni odpowiadalnymi dwa różne pytania, a każde chce
przeciwnej arytmetyki:
| Pytanie | Wiersze dzieci |
| --- | --- |
| **Ile jest winna organizacja?** | **wyłączone** — wiersz rodzica zawiera już te tokeny, więc liczenie obu obciąża organizację dwa razy za jedno żądanie |
| **Ile kosztował _ten agent_ w tym miesiącu?** | **włączone** — wiersze delegata to jedyne miejsce, w którym zapisane są jego własne wydatki, a każdy z nich trzyma własne żądania tego agenta i żądania jego wbudowanych specjalistów ([#228](https://github.com/vstorm-co/agenticos/issues/228)), ale nie jego opublikowanych delegatów, którzy mają własne wiersze |
To drugie sprawia, że na pytanie „ile researcher kosztował w tym miesiącu” da się
odpowiedzieć $40, i to na nim odpalają się raport użycia per agent albo alert
budżetowy na tym agencie. Miesięczna liczba organizacji niesie też wydatki na
ingestię, czego liczba per agent nie robi: indeksowanie współdzielonej bazy
wiedzy nie jest wydatkiem niczyjego agenta.
Przewrócony wiersz dziecka podlega też regule rodzica o tym, co może mówić
`error`. Pod delegatem podnosi się klient modelu, którego komunikat może nieść
URL nieudanego żądania — z kluczem włącznie, na własnym endpoincie — więc wiersz
i zamykająca ramka delegacji przechowują to samo kontrolowane zdanie co wiersz
rodzica, a własny tekst providera trafia do logu serwera. Delegat zatrzymany
przez swój limit użycia albo przez pułap budżetu zachowuje komunikat limitu w
całości: to pułap wykonujący swoją pracę, a nie awaria do zdiagnozowania.
Reguła sięga też transkryptu _rodzica_. Gdy agent deleguje w tle, odpytuje
`check_task` i `wait_tasks` o wynik, a to, co one odpowiadają, staje się wierszem
wywołania narzędzia w rozmowie — przechowywanym w całości, bo zwrot z narzędzia
jest własną odpowiedzią narzędzia, a nie czymś, co ta platforma ułożyła. Dlatego
nazywają one klasę wyjątku, zamiast powtarzać komunikat providera, co zapewnia
`subagents-pydantic-ai` 0.2.20 i co jest powodem, dla którego to właśnie ta
wersja jest wersją minimalną.
**Okienkowa liczba na dashboardzie też to niesie.** `GET /stats/usage` odpowiada
blokiem `cost` dla dowolnego okresu wybranego filtrem, a ten blok to runy _plus_
ingestia — ta sama arytmetyka, którą mierzy się limit miesięczny — z `model_usd`
i `ingestion_usd` obok, żeby czytelnik widział, gdzie poszły pieniądze, bez
odejmowania. Do 0.0.152 raportował samą połowę modelową, co stawiało na jednej
karcie dwie różne definicje kosztu: nagłówek poruszał się z filtrem okresu i
liczył runy, podczas gdy linia od początku miesiąca pod nim liczyła cały
rachunek, i nic nie mówiło, że odpowiadają one na różne pytania. Na wdrożeniu,
które indeksuje dokumenty, po prostu się nie zgadzały.
Przy `scope=own` połowa ingestii jest zerem, a nie udziałem: dokument jest
indeksowany przez workera, a `ingestion_spend` nie zapisuje żadnego użytkownika,
więc obciążenie okna jednej osoby za kolekcję zsynchronizowaną przez kogoś innego
byłoby wymyślaniem jej wydatków.
**Każde zapytanie musi powiedzieć, na które z dwóch pytań odpowiada**, a domyślna
jest pierwsza kolumna. Liczba od początku miesiąca i stojący za nią rozkład per
agent wyłączają wiersze dzieci, więc sumują się do sumy wypisanej nad nimi — a
e-mail organizacji o użyciu raportuje tę samą sumę, a nie sumę jednego z nich.
Włączają je tylko pytania zadane _o jednego agenta_. Trzy z tych pięciu zapytań
trafiły na produkcję bez tego rozróżnienia i każde raportowało $1,40 za $1,00
pracy; jeśli dochodzi nowe, domyślne jest to bezpieczne.
#### Dwa pytania o dostawcę potrzebują trzeciej odpowiedzi { #the-two-vendor-questions-need-a-third-answer }
**By provider** i **By key** nie mogą użyć żadnej z tych kolumn, a pomyłka w tym
miejscu jest na ekranie niewidoczna. Wyłączenie wierszy dzieci sumuje poprawnie,
ale przypisuje pieniądze delegata dostawcy _rodzica_, bo to jest provider w
sumowanym wierszu: orkiestrator na OpenAI delegujący pracę za $0,40 agentowi na
Anthropic raportował `openai $1.00`i ani jednego wiersza Anthropic. Włączenie
ich raportowało`openai $1.00` + `anthropic $0.40` — więcej niż rachunek.
Dlatego te dwa sumują **własne** wydatki każdego runa: jego koszt pomniejszony o
koszty jego bezpośrednich delegacji. `openai $0.60` + `anthropic $0.40`, co jest
i właściwym przypisaniem, i właściwą sumą. Zagnieżdża się to — delegat, który
delegował dalej, ma swoje wnuki odjęte przez siebie, raz — a zsumowane po każdym
wierszu dalej daje rachunek, bo koszt każdego dziecka jest dodawany przez jego
własny wiersz i usuwany przez wiersz jego rodzica. Klucz działa tak samo i znaczy
więcej: to klucz ktoś rotuje, gdy rachunek wygląda źle.
### Budżet rozmowy głosowej { #a-voice-calls-budget }
[Rozmowa głosowa](voice.md) jest runem, a jej wydatki liczą się do budżetów
agenta i organizacji jak każde inne. Różnią się trzy rzeczy, bo rozmowy nie da
się sprawdzić przed każdym żądaniem tak, jak sprawdza się run tekstowy:
- **Zanim się zacznie**, najdłuższy możliwy czas rozmowy pomnożony przez
najgorszą cenę modelu za minutę jest porównywany z pozostałym budżetem.
Rozmowa, na którą nie byłoby stać, jest odrzucana, zamiast zacząć się i zostać
ucięta.
- **W trakcie**, każda odpowiedź jest księgowana na runie, gdy się skończy, a
wydatki miesiąca są odczytywane na nowo co 15 sekund, więc widać runy, które w
tym czasie wydają. Budżet, który wyczerpie się w trakcie rozmowy, dostaje jedno
wypowiedziane zdanie i kończy rozmowę, zapisaną jako `budget_exceeded`.
- **Jej długość należy do agenta.** Rozmowa trwa najwyżej tyle, ile najdłuższa
rozmowa agenta, domyślnie 10 minut i najwyżej 50; nie ma osobnej puli minut
rozmów, więc budżety są jedynym limitem tego, co rozmowy wydają.
Rozmowę, której proces umarł, zamyka przemiatanie rozmów głosowych z kosztem
zapisanym przez jej ostatni heartbeat - inaczej niż run tekstowy, którego
wydatki umierają razem z jego procesem.
### Praca z własnej inicjatywy { #proactive-work }
Nocne sugestie i briefingi są zapisywane pod opublikowanym agentem startowym
**AgenticOS** organizacji (slug `agenticos`), żeby każde miało wiersz, koszt i
ślad, ale agent startowy się nie wykonuje i nie wybiera modelu: każde takie
uruchomienie niesie [rolę systemową](#system-agents), która wykonała pracę.
Osobny model proaktywności i limity wybierasz w **Organizacje → Członkowie →
Proaktywność**.
Aktywność pokazuje ikonę i filtr **Proaktywne**, żądania do
modelu i ich koszty oraz zapisaną wymianę, łącznie z wynikami narzędzi i
końcową strukturalną odpowiedzią. Nieudane sprawdzenia zachowują wymianę
dostępną sprzed błędu. Te zapisy wymagają `runs:view`, jak inne zapisy
Aktywności, i nie tworzą rozmów w Czacie. Starsze uruchomienia nie odzyskają
wymian, które nigdy nie zostały zapisane; zapisy zbyt duże wyraźnie mówią, że
zostały przycięte.
Pole modelu korzysta z tego samego wyboru dostawcy, modelu i klucza z sejfu co
Builder agenta. Uprawnienie `connections:manage` pozwala utworzyć tam profil
modelu; bez niego wybiera się istniejący profil. Nowy profil jest zapisywany
od razu. **Zapisz** stosuje wybór do nocnych sugestii. Briefing wylicza istniejące znaleziska bez wywołania modelu. Obserwacje wybranego
agenta współdzielą limit na osobę.
Platforma może zrobić trochę z własnej inicjatywy, ale nie robi nic takiego,
dopóki organizacja tego nie włączy: karta **Proaktywność** na stronie jej
członków (`budgets:manage`). Po włączeniu zadanie w tle przegląda ostatnie
prośby każdej osoby w dni i o godzinie, które wybrała organizacja - w strefie
czasowej każdej osoby albo godzinę przed godzinami pracy podanymi w jej profilu -
i proponuje rutynę dla prośby, którą powtarza, profil albo pierwsze wiadomości,
które oferuje jej pusty czat ([Konsola](console.md#chat)).
Każde to jedno
wywołanie modelu, na modelu wybranym przez organizację przy włączaniu (nie ma
domyślnego: `proactive_model_profile_id` jest wymagany), zapisane w historii
runów jako run z powierzchni **proactive** i liczone do budżetu organizacji jak
każde inne. Osoba może wyłączyć to dla siebie w **Ustawienia → Asystent**.
Nocny przegląd staje się należny 20 godzin po własnej ostatniej próbie.
Obserwacje i konserwacja pamięci nie przesuwają tego terminu. Obserwacje nadal
wliczają się do limitu wydatków na proaktywność danej osoby.
Udany przegląd zapisuje identyfikator osoby i skrót każdego przeczytanego
promptu, bez kopiowania tekstu. Zakończenie wcześniej przeczytanego oczekiwania
na zatwierdzenie lub odpowiedź nie powoduje więc kolejnego płatnego wywołania
modelu. Odroczony prompt pozostaje nieprzeczytany, aż uwzględni go późniejszy
przegląd; zmieniony tekst znów jest nowy. Nieudane wywołanie modelu nie oznacza
promptów jako przeczytanych.
Po aktualizacji stare runy tła bez przypisanej roli nie opóźniają kolejnego
przeglądu. Istniejące wiadomości nie mają znacznika odczytu, więc kwalifikujące
się ostatnie prompty mogą trafić do jednego przeglądu uzupełniającego, zgodnie
ze zwykłymi przełącznikami i budżetami.
Nocne zadanie czyta tylko prompt, który przeszedł wejściowy
[guardrail](reference/capabilities.md#guardrails) agenta, do którego go wysłano,
bo model proaktywności nie ma własnych guardraili:
- Prompt odrzucony przez guardrail jest pomijany.
- Prompt, którego nie przyjął żaden run, jest pomijany: wysłany do
zarchiwizowanego agenta, odrzucony przed otwarciem runu i prompt z czatu
przekazany do workflow.
- Prompt, którego run jeszcze trwa, czeka. Zadanie czyta go kolejnej nocy, gdy
run już się skończy.
- Gdy run zatrzymał się przed pierwszym żądaniem do modelu, na przykład na
budżecie, zadanie uruchamia na prompcie wejściowy guardrail tego runu i pomija
to, co guardrail blokuje. Gdy wersji agenta już nie ma, prompt jest pomijany.
- Gdy retencja usunęła run, zadanie czyta prompt tylko wtedy, gdy pod nim jest
odpowiedź modelu. Prompt sprzed wersji 1.36.0 jest czytany tak samo, więc
odmowa wejściowego guardraila, która nie zostawiła odpowiedzi, pozostaje poza
odczytem. Tura mówiona nie zapisuje modelu, więc jest pomijana, gdy nie ma już
jej runu.
Prompty rutyn przechodzą sprawdzenie według przypiętej wersji agenta, wersji
nazwanego środowiska lub bieżącej wersji agenta, zgodnie z konfiguracją rutyny.
Brak celu lub nieczytelna specyfikacja oznacza pominięcie rutyny. Pomijana jest
też rutyna, której ostatnie uruchomienie zostało odrzucone. Rutyna uruchomiona
wcześniej, której ostatni run usunęła retencja, również jest pomijana: nieznany
wynik nie może wycofać odmowy.
Prompt zredagowany przez guardrail przechodzi, a zadanie czyta go tak, jak jest
zapisany, bez redakcji
([#2655](https://github.com/vstorm-co/agenticos/issues/2655)).
Ponadto runy z własnej inicjatywy każdej osoby w miesiącu kalendarzowym ogranicza
**limit pracy z własnej inicjatywy** organizacji - domyślnie 1 $, ustawiany na tej
samej karcie (`proactive_limit_usd`). Osoba, u której nic nowego się nie
wydarzyło, nic nie kosztuje: zadanie woła model tylko wtedy, gdy jest co czytać.
**Obserwacje** wykonuje wybrany przez osobę opublikowany agent, tym samym
silnikiem co czat i rutyny, z własnymi instrukcjami i modelem. Włącza się je
osobno (`proactive_watches_enabled`); współdzielą limit wydatków proaktywności
na osobę, ale nie model sugestii. Utwórz obserwację w **Rutynach**, obok
harmonogramów agenta przyciskiem **Obserwuj i informuj**, przy prywatnym koncie
albo w prywatnej rozmowie. Wybierz agenta, własne konto, częstotliwość i opcjonalny
koniec. Obserwacja może śledzić publikacje, wskazywać nazwane środowisko albo
przypiętą niezmienną wersję. Minimalny odstęp nadal ustala organizacja
(`proactive_watch_min_minutes`).
Publikacja musi już zawierać osobiste powiązanie tej usługi katalogowej.
Obserwacja nie włącza brakującego powiązania ani nie podstawia innego agenta.
Własne serwery bez osobistego powiązania katalogowego nie są tu obsługiwane.
Utworzenie zatwierdza konkretne konto i adres serwera, część wspólną list
narzędzi konta i agenta oraz obecnie dołączone bazy wiedzy i pliki kontekstu.
Kolejna publikacja może zawęzić dostęp, ale nie dodaje kont, narzędzi ani źródeł.
**Zatwierdź ponownie obecne źródła** jawnie odświeża zgodę. Zmiana konta głównego
nie podmienia konta zatwierdzonego dla obserwacji.
Każdy przegląd jest zwykłym runem **proactive**, przypisanym rzeczywistemu
agentowi i wersji. Narzędzia źródła muszą mieć `readOnlyHint`, a odczyt, przed
którym pyta powiązanie MCP agenta, zostaje pominięty w przeglądzie. Obserwacja dopuszcza
zatwierdzone konto, wiedzę/kontekst, guardrails, planowanie i przypomnienia
systemowe; nie udostępnia sieci, sandboxa, wykonywalnych skills, zapisu pamięci
ani delegacji. Wymagane interaktywne zgody blokują konfigurację zamiast zostać
pominięte. Członkostwo, uprawnienie uruchomienia, zgoda na źródła, pauza i koniec
są sprawdzane ponownie przed narzędziami i zapisem wyników.
Nowe ustalenia zachowują identyfikator obserwacji i rzeczy w skrzynce osoby
oraz wracają do rozmowy źródłowej; utworzenie poza czatem używa rozmowy głównej.
Tylko żądane pilne ustalenie wysyła też natychmiastowe powiadomienie. Domyślny
tryb **Tylko gdy coś istotnego się zmieni** nie tworzy wiadomości ani
powiadomienia o zakończeniu przy braku zmian. Utrata dostępu do rozmowy
zostawia ustalenie w skrzynce i zapisuje `delivery_unavailable`.
Ostatnia próba jest oddzielna od ostatniego udanego odczytu. Odpowiedź modelu bez
udanego odczytu źródła lub z nierozwiązanym błędem odczytu to `not_completed`.
Niedostępne źródło i odebrany dostęp są jawne, a `budget_paused` oznacza wyczerpany
budżet. Te błędy nie przesuwają czasu udanego odczytu. Nieoczekiwany błąd nadal
kończy zadanie błędem.
`0261_watch_attempt_status.py` rozpoczyna historię prób bez zgadywania starszych
sukcesów. `0262_watch_agent_sources.py` zachowuje identyfikatory obserwacji,
ustalenia, częstotliwość i stan aktywności, ale nie zgaduje przypisania agenta.
Starsze obserwacje pokazują `needs_agent` i nie wołają modelu, dopóki właściciel
nie wybierze opublikowanego agenta i nie zatwierdzi źródeł. Usunięcie obserwacji
nie usuwa pochodzenia ani zgody zapisanych w trwającym lub oczekującym runie.
Utworzenie lub zmiana wymaga własnej zalogowanej sesji albo zaufanej prywatnej
rozmowy z człowiekiem. Zwykły klucz API, kontekst publikującego, rutyna w tle
ani impersonacja nie mogą nadać stałego dostępu. Narzędzia platformowego MCP
`list_watches`, `create_watch`, `update_watch` i `delete_watch` używają tej samej
usługi i identyfikatorów. Kompletne prośby w prywatnym czacie od razu tworzą lub
zmieniają obserwację; publikacja, zaproszenia, usuwanie i uruchomienie teraz
zachowują zwykłe zasady zatwierdzania.
Obserwacja może wybrać **Po każdym sprawdzeniu, także bez zmian**
(`report_mode: every_check`). Każdy udany odczyt dostarcza wtedy umówiony raport
do rozmowy źródłowej, także bez nowych ustaleń, i korzysta ze wspólnego systemu
powiadomień. Błąd lub niedostępność odczytu nigdy nie stają się raportem o braku
zmian. Termin aktywacji pozostaje w zadaniu; znacznik raportu jest zatwierdzany
z wynikami, więc ponowienie ukończonej aktywacji nie powtarza raportu ani płatnego
odczytu. Nie ma drugiego wywołania klasyfikującego.
Harmonogramy i obserwacje dziedziczą Twoje preferencje powiadomień. Pole
**Powiadomienia dla tej rutyny** pozwala wybrać dowolne połączenie e-maila, push,
Slacka, Telegrama, Mattermosta, Microsoft Teams i SMS albo samą aplikację.
`notification_channels: null` dziedziczy; pusta lista wyłącza wysyłki zewnętrzne.
Rutyna nie włącza kanału wyłączonego w Twoich preferencjach. Oczekujące wysyłki
sprawdzają aktualną politykę; pauza lub usunięcie rutyny je zatrzymuje. Obowiązkowe
alerty bezpieczeństwa i konfiguracji pozostają obowiązkowe. Wiadomości przyjętej
przez dostawcę nie można wycofać.
W **Ustawienia → Powiadomienia** jawnie wybierasz jeden połączony prywatny czat
na platformę w bieżącej organizacji. Samo połączenie konta nie włącza powiadomień.
Czaty grupowe, cudze konta i boty innych organizacji są wykluczone. E-mail i push
zachowują obecną konfigurację. Selektor czatu przy preferencjach zdarzeń zmienia
platformę, której przełączniki edytujesz.
Miejsce jest związane z konkretnym połączeniem konta i autoryzacją bota.
Odłączenie, ponowne połączenie, zmiana tokenu/serwera bota lub miejsca wymaga
ponownego wyboru. Stare wysyłki nie są przekierowywane. Przed wysyłką sprawdzane
są członkostwo, aktywność konta, polityka bota, ważność połączenia SMS, preferencje,
dostęp do zasobu i wspólny stan sprawy. Osoba bez prawa decyzji o zatwierdzeniu
otrzymuje tylko informację o oczekiwaniu, bez odnośnika do decyzji.
Platformy czatu nie mają wspólnego kontraktu idempotentnej wysyłki.
Niepotwierdzona wysyłka ma jedną trwałą próbę: timeout, błąd dostawcy lub utrata
workera pozostawia widoczny błąd. HTTP 429 Slacka, jawnie odrzucające żądanie,
pozwala ponowić po `Retry-After`, do pięciu prób łącznie. Każde wysłanie najpierw
usuwa zgodę na retry, więc awaria tej próby nie powoduje powtórzenia. Odroczenie
unieważnia stary token przekazania. Inne błędy czatu są końcowe. E-mail i push
zachowują swoje reguły ponawiania.
Powiadomienia Slack używają ograniczonych bloków zwykłego tekstu i przycisku
**Open in AgenticOS** do tej samej autoryzowanej sprawy w konsoli. Nazwy i treść
nie tworzą wzmianek ani automatycznych podglądów. Osoba bez prawa decyzji otrzymuje
tylko informację o oczekiwaniu, bez odnośnika do decyzji. Dostarczenie oznacza
przyjęcie przez platformę; potwierdzenie pozostaje jawną decyzją we wspólnej sprawie.
Migracja `0273_notification_safe_retries.py` nie nadaje istniejącym wierszom prawa
ponowienia. Cofnięcie przywraca jedną próbę dostarczenia na czat.
Migracja `0271_notification_routes.py` dodaje jawne miejsca i identyfikatory
autoryzacji. Opróżnij workery i zaktualizuj API/workery razem przed wyborem nowych
kanałów. Aktualizacja nie włącza żadnego istniejącego konta. Downgrade usuwa miejsca,
historię dostarczeń i preferencje czatów oraz zawęża wybory rutyn do e-maila/push;
nie cofa zewnętrznych wysyłek. Migracja `0263_routine_notification_policy.py`
wprowadziła pierwotne ustawienia rutyn.
### Wstrzymanie przez budżet { #routine-budget-pauses }
Gdy rutyna wykorzysta własny limit miesięczny, właściciel dostaje jedno
powiadomienie budżetowe na okres i ustawiony limit. Obserwacja wskazuje wspólny
budżet pracy proaktywnej tej osoby i kieruje do administratora, który może go
zmienić. Sprawdzenie zatrzymuje się przed utworzeniem przebiegu i wywołaniem
modelu; komunikat podaje wydatki miesięczne. Nadal obowiązują preferencje
powiadomień budżetowych i kanały wybrane dla rutyny.
Komunikat otwiera jedną sprawę dla tego zadania i miesiąca. Kolejne sprawdzenia
używają tej samej sprawy. Potwierdzenie lub wyciszenie anuluje oczekujące
wysyłki. Gdy następna aktywacja znów ma budżet albo zadanie zostaje usunięte,
otwarte sprawy budżetowe zamykają się i anulują oczekujące wysyłki. Zmiana limitu
pozwala wysłać nowy komunikat po jego wykorzystaniu; sprawa wyciszona pozostaje
wyciszona przez dany miesiąc.
### Zgody na działania { #routine-action-consent }
Rutyna działająca bez nadzoru pyta przed działaniami funkcji, zmianami lokalnych
plików i delegowaniem. Wywołania zewnętrznego MCP pytają, chyba że serwer oznacza je
jako tylko do odczytu; silniejsza polityka zatwierdzania połączenia nadal obowiązuje.
Domyślne zachowanie zwykłego interaktywnego agenta nie zmienia się. Tryb kodu jest
wyłączony w rutynach, aby skrypt zbiorczy nie omijał kontroli pojedynczych działań.
Zapisy do Virtual Tables w AgenticOS działają w granicach aktualnych uprawnień do tabel i operacji, bez wyjątku dla rutyny. Jawna polityka zatwierdzania agenta nadal obowiązuje dla tych wewnętrznych zapisów.
Twórca może otworzyć **Zgody na działania** swojej rutyny i wybrać działania funkcji oraz pojedyncze działania MCP na wskazanym koncie. Wymaga to `approvals:decide` i ustawienia organizacji `chat_may_waive_approvals`. Klucz API, agent, sesja podszywania się ani administrator działający za innego właściciela nie mogą nadać zgody. Nie da się tu wyłączyć zatwierdzeń jawnie wymaganych przez agenta lub powiązanie MCP.
Wyjątek MCP wskazuje konkretne konto, skrót adresu, generację autoryzacji i oryginalną nazwę działania. Wysyłanie poczty nie upoważnia do kasowania ani zapisów na innym koncie. Konta osobiste są dostępne tylko dla rutyny działającej jako jej twórca. Wybór korzysta z zapisanej listy narzędzi połączenia; gdy jest niedostępna, sprawdź połączenie lub poproś jego administratora. Cofnięcie wszystkich zgód nie wymaga działającej listy narzędzi.
Zapis wymaga, aby wersja i konfiguracja nadal odpowiadały przeglądowi osoby.
Zgoda wskazuje niezmienną opublikowaną wersję, stałe identyfikatory funkcji i
narzędzia oraz konfigurację rutyny. Zmiana pracy lub odbiorcy ją unieważnia. Każdy
przebieg zachowuje przyjęte identyfikatory zgód; dodanie zgody później nie zwalnia
wstrzymanego przebiegu, a cofnięcie i ponowne nadanie tworzy nową tożsamość. Przy
każdym narzędziu ponownie sprawdzane są członkostwo, uprawnienia, polityka organizacji
i zgoda. Każde wykonanie na podstawie zgody zapisuje zatwierdzony wpis z osobą i
argumentami. Delegaci wymagają własnych decyzji; nie pożyczają wyjątku rodzica.
Stała zgoda na działania zapisuje także aktualne rewizje przypiętych skills, ich zasobów, plików kontekstu i instrukcji projektu. Edycja któregokolwiek unieważnia wyjątek, nawet gdy opublikowana wersja agenta się nie zmienia lub później przywrócono dawną treść. Ponownie przejrzyj działania, aby nadać nowy wyjątek. Istniejące wyjątki bez odcisku rewizji pozostają ważne tylko dla agentów bez takich zmiennych źródeł instrukcji.
`0267_routine_action_consents.py` nie tworzy wyjątków istniejącym rutynom. Zmienia
się wykonanie bez nadzoru: dotychczasowe działanie może teraz czekać na zatwierdzenie.
Opróżnij stare workery i zaktualizuj API oraz workery razem przed wznowieniem zadań,
ponieważ stary worker nie egzekwuje tej kontroli. Downgrade usuwa zapisane wyjątki;
wpisy audytu zatwierdzeń i wykonane już skutki pozostają.
### Odpowiedzi wznowionych rutyn { #resumed-routine-answers }
Rutyna zakończona po zatwierdzeniu lub odpowiedzi na pytanie kolejkuje odpowiedź
do konsoli w tej samej transakcji co stan końcowy. Zapis zawiera gotowy tekst i
identyfikatory, więc restart nie wymaga kolejnego wywołania modelu. Stan dostarczenia,
wiadomość w czacie i powiadomienie o zakończeniu zapisują się razem; równoległe
próby i ponowienia nie dopisują drugiej odpowiedzi. Zdarzenie dostępne po kontroli
uprawnień odświeża otwarty czat źródłowy po zatwierdzeniu transakcji; starsi
odbiorcy zachowują dotychczasowy zestaw zdarzeń.
Worker sprawdza bieżący stan rutyny, termin, twórcę i odbiorcę. Zapis w czacie
ponownie sprawdza aktywne członkostwo, prawo uruchamiania agenta i bieżące prawo
zapisu w prywatnej rozmowie twórcy. Zarchiwizowane rozmowy są zamknięte na nowe
wiadomości. Niedostępny cel pozostawia wynik w Aktywności i zapisuje niedostarczenie.
Błąd bazy pozostawia zapis oczekujący na maksymalnie trzy próby zadania;
Administracja → Zadania w tle pokazuje wyczerpaną próbę dostarczenia. Powiadomienie
linkuje do czatu po poprawnym dostarczeniu. Kontynuacja w zewnętrznych kanałach
pozostaje osobnym punktem odbioru.
`0268_routine_answer_delivery.py` nie odtwarza historycznych wyników. Zaktualizuj
API i workery razem. Przed downgrade opróżnij lub anuluj oczekujące zadania
`deliver-routine-answer`, ponieważ usuwa ich zapisane przekazania; nie wycofuje
już dostarczonych wiadomości.
### Sprawy dotyczące awarii { #routine-failure-incidents }
Nieudany przebieg rutyny, niedostępne źródło obserwacji lub brak rozmowy docelowej
otwierają jedną sprawę właściciela. Kolejne błędy aktualizują jej ostatnią
przyczynę i przebieg bez następnego alertu. Potwierdzenie pozostawia sprawę do
załatwienia i wycisza alerty; wyciszenie przyczyny obowiązuje również po
odzyskaniu sprawności. Nowa awaria po potwierdzonej poprawie ponownie otwiera tę
samą sprawę i wysyła jedno nowe powiadomienie.
Wspólny runner obsługuje pierwsze i wznowione przebiegi oraz awarie wykryte po
śmierci procesu. Spośród wyników terminalnych stan rutyny zmienia tylko ostatnio
rozpoczęty przebieg główny; delegowane podzadania i spóźnione zakończenia starszych
przebiegów go nie nadpisują. Obserwacja odzyskuje sprawność po
zweryfikowanym odczycie źródła; samo zakończenie modelu nie wystarcza. Spóźnione
sprawdzenia zachowują niezależnie najnowszą próbę i ostatni poprawny odczyt.
Wykrycie wygaśnięcia lub usunięcie zamyka otwarte blokady i anuluje oczekujące wysyłki.
Rutyny wstrzymane, wygasłe i usunięte nie zapisują gotowej odpowiedzi w wybranym
czacie.
Zegary obserwacji i poprawnego wykonania chronią decyzję osoby przed retry
callbacka i starszym wynikiem. Błąd zapisu powiadomienia pozwala ponowić nowy
incydent. Limity agenta i przebiegu łączą alert właściciela z jego sprawą zamiast tworzyć
drugi e-mail o błędzie. Alerty budżetowe organizacji i działu pozostają osobnymi
powiadomieniami finansowymi, również gdy właściciel jest administratorem. Jego
sprawa rutyny pozostaje w aplikacji, gdy powiadomienie finansowe już go informuje.
Nadal obowiązują preferencje budżetowe.
`0266_routine_activation_clock.py` dodaje zegary bez zgadywania dawnych prób.
Identyfikatory istniejących spraw awarii obserwacji pozostają zachowane. Wdrażaj
API i workery razem: worker dostarczający musi rozumieć nowe metadane alertów
budżetowych. Downgrade usuwa zegary, pozostawiając sprawy, powiadomienia i decyzje.
### Agenci systemowi { #system-agents }
Część pracy należy do samej platformy i mimo to potrzebuje modelu. **Rola
systemowa** daje jej właściciela, którego operator widzi: jedna konfiguracja na
organizację i rolę, nigdy wiersz agenta, nigdy jedna na zadanie i nigdy
przypisana agentowi startowemu ani żadnemu agentowi, którego instrukcje się nie
wykonały (#2575). **Organizacje → AI i usługi → Usługi w tle**
(`budgets:manage`) wylicza każdą rolę z modelem, na którym działa, wydatkami w tym
miesiącu i ostatnimi wykonaniami.
| Rola | Co robi | Konfiguracja |
| --- | --- | --- |
| `daily_briefing` | Nocne spojrzenie na tydzień każdej osoby i zbudowany z niego briefing | W zakładce Proaktywność: ten sam model i limit, tutaj tylko do odczytu |
| `memory_maintenance` | Czyta zakończoną dopuszczoną turę raz i zapisuje trwałe fakty, preferencje i decyzje jako [claimy](reference/capabilities.md#memory-claims) | Tutaj: przełącznik, model i własny budżet miesięczny |
Utrzymanie pamięci nie wymaga startowego agenta AgenticOS. Przebieg zachowuje agenta źródłowego jako referencję zapisu i jest rozliczany jako `memory_maintenance`; wykonują się wyłącznie platformowe instrukcje ekstrakcji na modelu wybranym dla roli. Usunięcie lub wycofanie publikacji agenta startowego nie wyłącza ekstrakcji innych opublikowanych agentów.
Rolę się wyłącza, nie usuwa. **Utrzymanie pamięci** wymaga wybranego modelu i
respektuje własny budżet miesięczny oraz limit organizacji. Czyta zakończone tury,
jeżeli runner dopuścił rzeczywistą osobę z prawem zapisu pamięci: prywatny czat,
wiadomości z powiązanego kanału oraz jej własne zadania harmonogramu lub workflow.
Wykluczone są embedy, klucze API, podszywanie się, zdarzenia od osób z zewnątrz i delegacje.
Zadanie zachowuje pierwotnych odbiorców. Prywatna tura zapisuje claimy w profilu osoby
lub relacji z agentem; tura grupowa wyłącznie w pokoju źródłowym, nawet jeśli model
uzna fakt za ogólny. Po odpowiedzi modelu bieżące członkostwo i polecenia usunięcia są
sprawdzane ponownie pod blokadą zapisu. Starsze zadania bez informacji o odbiorcach
pozostają ograniczone do prywatnego czatu webowego. Utrzymanie nie udostępnia ani nie
poprawia claimów. Deterministyczna konsolidacja nie potrzebuje modelu ani roli.
### Co pokazuje historia runów { #what-run-history-shows }
**`GET /runs` wypisuje wyłącznie runy najwyższego poziomu, a jego `total` liczy
właśnie je.** Ta sama wartość domyślna co przy miesięcznej sumie organizacji i z
tego samego powodu: przeplecionych, tych dwóch rodzajów wierszy nie da się czytać
w jednej kolumnie kosztu. Fan-out trzech delegacji to jeden run kosztujący $1,00
na stronie i $1,00 na rachunku; wypisane razem były to cztery wiersze pokazujące
$1,00 + $0,40 + $0,40 + $0,40 obok liczby $1,00 od początku miesiąca, i obie
połowy miały rację, tylko o czym innym.
Lista przyjmuje tę samą dwustronną arytmetykę co sumy powyżej i z tego samego
powodu — więc powierzchnia zawężona do jednego agenta pokazuje, co zrobił _ten
agent_, wraz z pracą delegatów:
| Pytanie | Odpowiedź |
| --- | --- |
| `GET /runs` | Runy, które ktoś uruchomił. `parent_run_id IS NULL` |
| `GET /runs?agent_id=<id>&include_delegations=true` | Własna historia jednego agenta. O to pytają panel Recent runs w Builderze i `?agent=` na stronie **Aktywność** (Activity), bo wiersze delegata to jedyny zapis tego, co on sam zrobił |
| `GET /runs?parent_run_id=<id>` | Co ten run zdelegował — zapytanie, dla którego istnieje `agent_runs_parent_run_id_idx`. Ma pierwszeństwo przed `include_delegations` |
| `GET /runs/<id>` | Jeden run, zdelegowany albo nie. Tu ląduje link z transkryptu |
| `GET /runs/<id>/transcript` | Tury tego runa, po kolei — to, co widok szczegółów runa rysuje jako kroki. Autoryzowane, nie posiadane (niżej) |
**Czytanie runa jest autoryzowane, nie własnościowe.**
Współpracownik mający `runs:view` czyta run uruchomiony przez kogoś innego.
Władza nad runem należy do organizacji, bo run jest tym, czym organizacja jest
obciążana i z czego jest rozliczana — a nie prywatną własnością tego, kto
nacisnął start.
Dlatego decyzja mieszka w serwisie, a nie w bramce na route: najpierw rozstrzyga
run względem organizacji wołającego, a potem sprawdza `runs:view`.
Trzy konsekwencje:
- Run w **innym tenancie czyta się jako nieobecny** — to samo 404, którym
odpowiada id, które nigdy nie istniało, co do treści ciała włącznie — więc
odpowiedzi nie da się użyć do odkrycia, że run istnieje.
- Run, który działał **bez rozmowy** (wywołanie API, które nie przekazało
`conversation_id`), nie ma transkryptu do przeczytania i mówi o tym nullowym
`conversation_id`, a nie pustą listą, którą czytałoby się jako „nic nie
zrobił”.
- Nic z tego nie poszerza `GET /conversations/{id}/messages`, które pozostaje
**zawężone do właściciela**. To, że transkrypt runa jest czytelny dla
współpracownika, nie może uczynić czytelnym również prywatnego wątku, w którym
ten run siedzi.
**Każda tura, którą podaje transkrypt, niesie oceny, jakie ludzie na niej
zostawili** — własny kciuk czytającego wołającego, polubienia i niepolubienia
organizacji oraz komentarz z najnowszej oceny negatywnej.
Zwykły wiersz wiadomości nie trzyma żadnej z tych rzeczy, więc są one czytane z
`message_ratings` jedną paczką i doczepiane do tur. Tura, której nikt nie ocenił,
niesie je puste i czyta się dokładnie tak jak zwykła wiadomość.
To właśnie pozwala widokowi szczegółów runa pokazać odpowiedzi ocenione
negatywnie i słowa zostawione przy nich — rozmowy stojące za liczbą jakości na
dashboardzie (#209) — czytane tam, gdzie czytany jest run, a nie wyłącznie w
eksporcie ocen dla administratora aplikacji.
Pokazywany komentarz pochodzi z oceny **negatywnej**, nigdy z pozytywnej, i jest
najnowszy, gdy tura ściągnęła więcej niż jeden sprzeciw.
### Co pokazują agregaty na dashboardzie { #what-the-dashboards-aggregates-show }
`GET /stats/usage` przyjmuje te same dwie strony i tę samą wartość domyślną.
Złożona odpowiedź jest pytaniem organizacji, więc każdy blok w niej liczy
wyłącznie wiersze najwyższego poziomu: koszt okresu i jego rozbicie na providerów
(podwójny rachunek powyżej), ale też sumę runów, szereg dzienny, rozbicie
wyników, powierzchnie, percentyle opóźnień, liczbę aktywnych osób i tabelę per
osoba. Poza kosztem zdelegowany wiersz _kopiuje_ `user_id` i `surface` swojego
rodzica, więc policzenie go dodatkowo wymyśliłoby drugą osobę i drugie wejście na
kanał, z którego ktoś skorzystał raz.
Dwa agregaty biorą drugą stronę i oba są pytaniami o jednego agenta:
| Pytanie | Wiersze dzieci |
| --- | --- |
| `by_agent` — karta adopcji | **włączone.** Bez nich agent, który działa czterysta razy dziennie jako czyjś delegat, nie ma żadnego wiersza, a karta nazywa każdego opublikowanego agenta bez wiersza zapomnianym i proponuje jego zarchiwizowanie. Jej słupki mogą więc przekraczać sumę runów obok nich; nic ich nie sumuje |
| `?group_by=version` — karta porównania wersji | **włączone.** Specjalista, który wykonuje się wyłącznie jako delegat, inaczej nie miałby czego porównywać między swoimi wersjami |
Niezmiennik, który przeżywa w obu wariantach: segmenty pierścienia wyników dalej
sumują się do `total_runs`, a jego segment `awaiting_approval` dalej liczy te
same zaparkowane runy co karta zatwierdzeń, bo te trzy rzeczy pochodzą z tej
samej strony przełącznika.
Jedynym zapytaniem bez żadnego filtra delegacji jest liczba runów wołającego
zaparkowanych na decyzji. Zaparkowane dziecko to zablokowany rodzic, a ta karta
odpowiada na pytanie „dlaczego mój agent nie kończy”; dziś nic to nie zmienia, bo
delegacja jest zapisywana do bazy już jako zakończona i dlatego nigdy nie
parkuje.
Ostatnie dwa to `?run=<id>` na stronie Aktywność: jeden run, delegacje pod nim,
każda z plakietką id zadania, które niosły jej ramki `subagent_*`, oraz link w
górę do runa, na który delegacja została zaksięgowana. Panel delegacji w czacie
linkuje tam z `run_id`, które niesie jego ramka terminalna — i właśnie dlatego ta
ramka je niesie. Zagnieżdżanie zdelegowanych wierszy wewnątrz tabeli najwyższego
poziomu celowo _nie_ jest tutaj robione; prymityw tabeli współdzielony przez cały
produkt jest [proponowany osobno](https://github.com/vstorm-co/agenticos/issues/139),
i zagnieżdżanie należy do niego, a nie do jednej szytej na miarę tabeli runów.
### Co pokazuje ekran kosztów { #what-the-cost-screen-shows }
`GET /spend` przyjmuje swoje okno na dwa sposoby, bo strona pyta o oba rodzaje:
`days` dla presetów _ostatnich N dni_ oraz `from`/`to` dla _tego miesiąca_,
_poprzedniego miesiąca_ i zakresu z kalendarza. Gdy przychodzą oba, wygrywa
`from` — jawny zakres jest bardziej konkretnym żądaniem niż wartość domyślna,
której nikt nie zmienił — a `period_days` wraca wtedy jako null, zamiast
powtarzać liczbę, której zakres przeczy.
**Każdy panel na tym ekranie czyta to samo okno.** Wiersze per agent, By provider
i By key biorą rozstrzygnięte `since`/`until`, a nie własną liczbę dni, więc dwie
liczby obok siebie nie mogą skończyć jako opis różnych runów. To ta sama usterka,
którą #198 nazywa jeden panel wyżej.
**Wartość od początku miesiąca ignoruje to okno całkowicie**, tak samo jak każdy
limit per agent mierzony względem niej. Miesięczny pułap porównany z kroczącymi
siedmioma dniami czyta się jako wykorzystany w 20% w dniu, w którym limit został
faktycznie osiągnięty.
Każdy wiersz per agent niesie **dwie liczby kosztu pod dwiema różnymi nazwami**,
co jest regułą tej strony na całej jej długości:
| | |
| --- | --- |
| `cost_usd` | Jego udział w oknie, **wyłącznie runy najwyższego poziomu**, więc kolumna sumuje się do sumy nad nią |
| `month_to_date_usd` | Jego **własny** miesiąc kalendarzowy, ze zdelegowanymi wierszami **włącznie** — wydatki, na które `monthly_cap_usd` jest limitem. Nie sumuje się do miesiąca organizacji i nie jest rysowany, jakby się sumował |
`partial_run_count` mówi, ile z tego wszystkiego jest faktem: ilu **runów
najwyższego poziomu** w oknie nie dało się w pełni wycenić, więc koszt jest dolną
granicą dokładnie o tyle. _„3 z 40 runów nie dało się wycenić”_ to coś, z czym
czytelnik może coś zrobić; liczba z doklejonym plusem — nie.
Run liczy się wtedy, gdy którykolwiek model w jego drzewie nie miał ceny, **z
modelami jego delegatów włącznie** — drzewo dzieli jeden rejestr wydatków, więc
niewyceniony delegat czyni dolną granicą także wiersz rodzica. To pozwala jednej
liczbie rządzić wszystkimi trzema rozbiciami: By provider i By key sumują własne
wydatki każdego wiersza, wraz ze zdelegowanymi, a dolna granica w którymkolwiek z
nich jest oznaczana przez licznik, który nigdy nie zajrzał do wiersza, który ją
powoduje. **Mierzy** on By agent, które liczy te same wiersze najwyższego
poziomu, a pozostałe dwa tylko **oznacza** — liczy drzewa, więc jeden rodzic z
trzema niewycenionymi delegatami pokazuje `1`, podczas gdy trzy liczby pod nim są
dolną granicą.
Drzewo, które **stoi okrakiem na początku okna** — wiersz delegata w środku,
wiersz jego rodzica przed nim — jest liczone przez delegata: wiersz rodzica,
który inaczej niósłby to oznaczenie, jest poza każdym agregatem na tej stronie,
podczas gdy własne wydatki delegata są wewnątrz obu rozbić. Ląduje to na agencie,
jako który działał delegat, raz na każde okraczające drzewo, niezależnie od tego,
ile delegacji przekroczyło krawędź, i tylko wtedy, gdy własne żądania delegata
były niewycenione — wyceniona delegacja pod niewycenionym rodzicem spoza okna nie
podnosi żadnego zastrzeżenia, bo pieniądze samego okna są dokładne
([#620](https://github.com/vstorm-co/agenticos/issues/620)).
Wiersz jest **jeden na agenta**, z `agent_name` na nim. Kiedyś był jeden na
agenta _i model_, niosąc tylko `model_label` — więc zakładka wypisywała nazwy
modeli tam, gdzie czytelnik spodziewa się agenta, i dzieliła jednego agenta na
dwa wiersze za to, że odpowiadał na dwóch modelach. Kształt per model przetrwał
tam, gdzie jest zadawanym pytaniem: e-mail o użyciu dalej grupuje w ten sposób.
**Kto to wydał to czwarte rozbicie**, pod By provider, By key i By agent — to,
które odpowiada ludźmi, a nie dostawcami czy agentami.
Czyta te same wiersze `group_by=user` co tabela adopcji na dashboardzie —
wyłącznie runy najwyższego poziomu, od najbardziej zajętych — więc koszt delegata
ląduje raz, wewnątrz runa, który go uruchomił. I obejmuje to okno, które pokazuje
reszta zakładki, a nie własną kroczącą wartość domyślną.
**Kiedy wydano** to słupek na każdy dzień UTC w tym samym oknie, z `by_day` —
własny koszt każdego runa, więc dni sumują się do tego samego rachunku, który dzielą
zestawienia. Dzień bez runów jest rysowany jako zero, a nie pomijany. Każde zestawienie
pokazuje udział wiersza w swojej sumie, a agent z miesięcznym limitem pokazuje miesiąc
na tle tego limitu.
Nazywanie ludzi z organizacji to ta sama decyzja, którą podejmuje karta na
dashboardzie, więc obejmuje ją ta sama bramka: `runs:view`, które mają builder i
operator, a także obie role zarządcze, i mówi o tym własny tekst tej karty.
Wołający bez `runs:view` jej nie widzi. Karty nie ma i jej pytanie nigdy nie
zostaje zadane, zamiast żądania, które wraca odrzucone.
### Zawężanie kolejki zatwierdzeń { #narrowing-the-approvals-queue }
`GET /approvals` podaje dwa widoki tych samych wierszy. Domyślnie tylko
oczekujące, czyli kolejkę, na której ktoś działa; `?status=approved&status=rejected`
to zapis tego, co zostało rozstrzygnięte, i niesie nazwisko decydenta oraz jego
notatkę, bo sam UUID nie jest śladem odpowiedzialności. Na rozstrzygniętym
wierszu celowo nie ma żadnych kontrolek.
| Parametr | |
| --- | --- |
| `status` | Powtarzalny. Brak oznacza oczekujące — kolejkę |
| `triggered_by_user_id` | Czyje runy zaparkowały to wywołanie. Odczytywane z `agent_runs`: zatwierdzenie należy do runa, a run należy do osoby. Pomija runy, w których publikujący zastąpił niepowiązane konto czatu; takie wiersze niosą zamiast tego `asked_in_chat` (platforma, nazwa i bot), a ich pola `triggered_by_*` są puste |
| `created_from`, `created_to` | Kiedy wywołanie zostało zaparkowane, włącznie z obiema granicami |
| `oldest_first` | Domyślnie true, i ta wartość domyślna jest nośna — zobacz wyżej: nic nie wypycha wywołania z kolejki z powodu wieku, więc sortowanie od najnowszych pogrzebałoby wiersz, który najbardziej potrzebuje zobaczenia |
Każdy wiersz nazywa trzy rzeczy, które mieszkają w innych tabelach — agenta,
osobę, której run zaparkował to wywołanie, i osobę, która rozstrzygnęła. Agent i
run to inner joiny, bo oba klucze obce kaskadują, więc zatwierdzenie nie może
przeżyć żadnego z nich; dwie osoby to outer joiny, bo decyzja musi przeżyć
usunięcie konta decydenta, a odwiedzający widget jest anonimowy od samego
początku.
### Zawężanie historii runów { #narrowing-run-history }
| Parametr | |
| --- | --- |
| `status` | Powtarzalny. `?status=failed&status=budget_exceeded` to zapytanie „pokaż mi problemy”, a te dwa są osobnymi statusami właśnie po to, żeby pytanie o jeden nie było pytaniem o drugi |
| `surface` | Skąd przyszedł run |
| `user_id` | Jako **kto** run działał, co nie zawsze jest tym, kto zapytał — runy widgetu niosą tożsamość właściciela widgetu, bo odwiedzający jest anonimowy |
| `model_label` | Model **tak, jak zapisał go run**, dopasowywany dokładnie. Nierozstrzygany przez katalog modeli: kolumna jest tym, co odpowiedziało, a profil, z którego pochodziła, mógł od tego czasu zostać przemianowany albo usunięty. Karta modeli na dashboardzie liczy te same ciągi znaków, więc „runy stojące za tym słupkiem” to jeden zbiór na obu ekranach |
| `started_from`, `started_to` | Włącznie z obiema granicami, bo wybierak zakresu podaje całe dni |
| `environment_id` | Runy na wersji, którą przypina to środowisko. **Nigdy zdelegowany run:** wersja delegata pochodzi z przypięcia, więc kolumna celowo nigdy nie jest na nim zapisywana, a zawężenie do `production` odrzuca każdą delegację. Powierzchnia, która włącza delegacje, musi to powiedzieć |
| `exposure_id` | Runy wpuszczone przez jedno powiązanie. Null dla dashboardu i dla API |
| `agent_version_id` | Runy, które wykonały jeden zamrożony spec — „pokaż mi wiersze stojące za tą liczbą” z paska wersji |
| `rated` | `down` albo `up` — runy, w których ktoś ocenił wiadomość wyprodukowaną przez ten run |
| `order_by`, `descending` | `started_at` (domyślnie, od najnowszych), `duration`, `cost` albo `tokens` |
**Każdy filtr zawęża licznik tak samo jak stronę**, więc `total` zawsze opisuje
wiersze pod sobą. Lista i licznik to dwa zapytania, a filtr docierający tylko do
jednego z nich czyta się jak błąd stronicowania, a nie jak brakująca klauzula.
`started_from` jest też tym, co czyni ten licznik uzgadnialnym z pieniędzmi obok
niego. Bez okna czyta _cały czas_, podczas gdy liczba wydatków czyta jeden
miesiąc kalendarzowy, więc trzyletnia organizacja pokazywała „8412 runów” obok
„$31,20”, a oczywisty odczyt tej pary mylił się o trzy lata. Liczba runów i
liczba wydatków na jednym ekranie dzielą jedno okno albo mówią, które okno
opisuje każda z nich.
Wartość spoza swojego typu zostaje odrzucona z kodem 422, zamiast być
dopasowywana do niczego: `status` i `surface` to kolumny tekstowe, więc
`?status=complete` odpowiedziałoby inaczej pustą stroną — a pusta strona czyta
się jako _w tym tygodniu nic się nie popsuło_. `order_by` przyjmuje jedno z
czterech uporządkowań, a nie nazwę kolumny, z tego samego powodu plus jeszcze
jednego: `ORDER BY` składane z query stringa to powierzchnia do wstrzyknięcia.
**Każdy z nich podróżuje w URL-u**, i to właśnie pozwala karcie na dashboardzie
przekazać czytelnika do swoich własnych wierszy:
`/runs?surface=mattermost&period=30d` otwiera Aktywność z już ustawionym filtrem i
z licznikiem zgodnym z kartą, która podlinkowała. Do #768 były stanem lokalnym,
więc liczba p95 była jedyną liczbą na dashboardzie, która potrafiła dosięgnąć
stojących za nią runów, a trzy karty nie niosły żadnego linku — nie było niczego
uczciwego, na co można by je skierować.
**W tym również tego, która zakładka jest otwarta.** `?tab=approvals` i
`?tab=spend` otwierają kolejkę i ekran kosztów; historia runów jest domyślna i
nigdy nie jest zapisywana.
To jest adres, którego potrzebuje link do _decyzji_, i powód, dla którego ten
parametr istnieje: „See all” na karcie zatwierdzeń i alert mówiący, że run jest
zaparkowany, musiały oba wskazywać na historię runów, gdzie niczego nie da się
rozstrzygnąć (#934).
Zakładka wskazana przez link jest **rozstrzygana względem tego, co czytelnik może
otworzyć**. `approvals` jest bramkowane przez `approvals:decide`, więc link,
który je niesie i trafia do kogoś bez tego uprawnienia, otwiera historię runów, a
nie pasek, którego wybrana zakładka nie ma treści.
Przełączenie zakładek zamyka otwarte szczegóły runa i zabiera ze sobą `?run=`.
Panel, który przeżywa zakładkę, która go otworzyła, siedzi obok kolejki, z którą
nie ma nic wspólnego — a poniżej `lg` zastępuje listę, więc pasek pozostawał
żywy, podczas gdy treść każdej zakładki była ukryta.
**Czas trwania jest liczony w SQL-u, na całym zawężonym zbiorze.**
To właśnie prowadzi od _„p95 to 14,8 s”_ na dashboardzie do **tamtych runów**.
Sortowanie jednej strony dwudziestu pięciu wierszy sortuje zły zbiór, bo
najwolniejszego runa miesiąca nie ma w tych wierszach, które akurat zwróciła
strona od najnowszych. `cost` i `tokens` to ten sam układ dla pieniędzy i dla
wagi kontekstu.
Run bez `ended_at` sortuje się **jako ostatni w obu kierunkach we wszystkich
trzech**. Nie ma czasu trwania, nie jest też najszybszym runem, a jego liczby
kosztu i tokenów są zapisywane dopiero wtedy, gdy się kończy — sortowany tak, jak
jest przechowywany, wciąż trwający run czytałby się jako najtańszy i najlżejszy w
organizacji.
To, jak długo trwa _wciąż działający_ run, jest osobnym pytaniem i żadne z tych
uporządkowań na nie nie odpowiada.
Aktywność pokazuje ten czas trwania na trzy sposoby i wszystkie trzy prowadzą do
tego samego zapytania:
- Nagłówek kolumny **Took** jest kontrolką sortowania — tak jak nagłówek Started
obok niego i jak każdy sortowalny nagłówek w produkcie — więc kliknięcie
porządkuje historię według `duration`, a nie według dwudziestu pięciu wierszy
na ekranie.
- **Liczba p95 na dashboardzie linkuje tutaj**, posortowana według czasu trwania
w tym samym oknie: `?sort=duration` razem z `started_from` / `started_to`
danego okresu.
Więc liczbę i stojące za nią runy dzieli jedno kliknięcie — reguła, którą reszta
tych dwóch stron już stosuje, i jeden wymiar, w którym tego nie robiły (#210).
**`rated=down` to kolejka o najwyższym sygnale** — odpowiedzi, o których
prawdziwi ludzie powiedzieli własnymi słowami, że są błędne. Ocena wisi na
wiadomości, więc ten join biegnie przez `messages.run_id`: dwa runy w jednej
rozmowie zachowują własne oceny, i dlatego ta kolumna istnieje zamiast okna
czasowego nad wątkiem. Jest to `EXISTS`, więc run, którego trzy osoby nie
polubiły, to jeden wiersz, a nie trzy; a run, który jedna osoba polubiła, a druga
nie, pasuje **do obu**: `up` i `down`, bo oba są o nim prawdziwe. Sprowadzenie
tego do jednego werdyktu na run wymyśliłoby konsensus, którego wiersze nie
zapisują.
Ten sam fakt jedzie na wierszu również bez filtra. `AgentRunRead.down_rated` jest
`true`, gdy ktokolwiek ocenił poniżej zera odpowiedź wyprodukowaną przez ten run,
liczone dla całej strony jednym zapytaniem, i to na tym historia runów rysuje 👎.
Jest ograniczone do organizacji wołającego jak każdy odczyt tutaj — run sąsiada,
oceniony negatywnie, nigdy nie jest oznaczany dla innego tenanta.
**Komentarz**, z którym zostawiono ten kciuk, czyta się w szczegółach runa
(`?run=<id>`), a nie w wierszu. To tekst napisany przez użytkownika o jednej
rozmowie, a umieszczenie go za szczegółami to celowa granica między znacznikiem,
który widzi każdy z `runs:view`, a słowami, które go wyjaśniają.
To ten join, dla którego zbudowano `rated=down`: dashboard mówi, że jakość spadła
o cztery punkty, a tutaj czyta się rozmowy, które to zrobiły.
Trend, który czyta dashboard, to `GET /api/v1/ratings/summary` (nagłówkowy
podział plus szereg dzienny): `scope=org` pod `runs:view`, `scope=own` dla
własnych rozmów członka, ta sama reguła zakresu i to samo słownictwo okien co w
`GET /stats/usage` (zobacz [Uprawnienia](permissions.md)). Same liczniki —
komentarze zostają za szczegółami runa, powyżej.
Trzy liczby Aktywności nad zakładkami pozostają liczbami organizacji, łącznie z
licznikiem runów, nawet gdy tabela pod nimi jest zawężona do jednego agenta.
Licznik per agent obok miesiąca organizacji byłby dwoma pytaniami pod jedną
etykietą — a to licznik per agent jest tym, który włącza delegacje.
**Osierocona delegacja jest raportowana bez swojego uchwytu.** `parent_run_id` ma
`ON DELETE SET NULL`, więc usunięcie rodzica zostawia wiersz, który poprawnie
zaczyna liczyć się do rachunku — ale klucz obcy potrafi wyzerować tylko własną
kolumnę, a przechowywany `subagent_task_id` nazywa wtedy transkrypt, który
odszedł razem z rodzicem. `AgentRunRead` zatrzymuje go zawsze, gdy
`parent_run_id` jest nullem, więc żadna powierzchnia nie oferuje uchwytu
delegacji, który nigdzie nie prowadzi.
### Eksport do CSV { #exporting-to-csv }
Wszystko, co pokazują te trzy zakładki, można zabrać z ekranu jako CSV: wiersze,
które ktoś uzgadnia z fakturą, przekazuje działowi finansów albo dołącza do
audytu. Strona, która potrafi odpowiedzieć na pytanie na ekranie, ale nie poza
nim, odsyła ludzi do bazy danych.
| Pytanie | Odpowiedź |
| --- | --- |
| `GET /runs/export` | Historia runów, te same filtry co `GET /runs` i to samo domyślne ograniczenie do najwyższego poziomu. `runs:view` |
| `GET /approvals/export` | Zapis zatwierdzeń, te same filtry co `GET /approvals`. `approvals:decide` |
| `GET /spend/export` | Rozbicie wydatków per agent, to samo okno co `GET /spend`. `runs:view` |
| `GET /audit/export` | Ślad audytowy w oknie czasu, CSV albo JSONL (`?fmt=`). `audit:read` |
Eksport audytu jako jedyny oferuje też **JSONL** (`?fmt=jsonl`), po jednym
obiekcie JSON na linię, bo ślad audytowy równie często trafia do potoku logów, co
otwiera się w arkuszu; oba opisują te same wpisy, z `details` spłaszczonym do
łańcucha JSON w komórce CSV i zachowanym jako zagnieżdżony obiekt w liniach.
Wiezie dokładnie te pola, które wystawia model odczytu `GET /audit` — zapisane
`ip_address` nie jest na tej zakładce, więc nie ma go i w eksporcie — i jak każdy
eksport tutaj zapisuje własny odczyt w śladzie (`audit.export`, z nazwanym oknem,
formatem i liczbą wierszy).
Eksport wydatków niesie tylko liczby z okna — `cost_usd`, `run_count` i
`partial_run_count`. `month_to_date_usd` i `monthly_cap_usd` z zakładki Spend są
z niego pominięte: czytają one miesiąc kalendarzowy, podczas gdy `cost_usd` czyta
okno eksportu, a dwie kolumny dolarowe na dwóch podstawach czasowych w jednym
pobranym pliku zostają zsumowane w poprzek przez czytelnika, który nie widzi
różnicy. Pobrany plik niesie jedną podstawę czasową — to okno, o które
poproszono.
Eksport jest odczytem masowym, a nie przyciskiem, i odpowiada na sześć pytań, na
które route'y listujące odpowiadać nie muszą:
- **Wielodostępność.** Każdy eksport niesie bramkę zakładki, z której pochodzi, a
każdy odczyt jest zawężony do organizacji wołającego — wiersze sąsiada nigdy do
niego nie docierają, łącznie z wierszem, którego wołający jest właścicielem w
organizacji innej niż ta, z której pyta. Te dwa na `runs:view` stosują
dodatkowo **dolną granicę `Scope.OWN` w zapytaniu**: wołający, którego
`runs:view` sięga mniej niż całej organizacji, eksportuje wyłącznie własne
wiersze, `WHERE user_id = <them>`, a `user_id`, które przekaże, zostaje
nadpisane jego własnym, zamiast poszerzać zakres. Żadna wbudowana rola nie ma
jeszcze `runs:view` poniżej `all`; ta granica jest na miejscu na czas, gdy
zapadnie decyzja o zakresie dla member/viewer.
- **Rozmiar.** Eksport z natury nie ma pułapu, więc dostaje go z projektu.
**Zakres dat jest obowiązkowy** — żądanie bez obu końców zostaje odrzucone — a
dopasowanie jest **ograniczone do 10 000 wierszy**, powyżej czego żądanie
zostaje odrzucone komunikatem nazywającym tę liczbę i mówiącym wołającemu, żeby
zawęził zakres. Nigdy ciche obcięcie: przycięty CSV jest gorszy niż odrzucony,
bo arkusz sumuje to, co do niego przyjdzie. To ograniczenie pozwala zbudować
ciało odpowiedzi w jednym przebiegu i zacommitować wpis audytowy, zanim
odpowiedź wyjdzie, zamiast strumieniować ją przez trzymane połączenie.
- **Koszt częściowy.** `cost_is_partial` ma własną kolumnę w eksporcie runów, a
`partial_run_count` własną kolumnę w eksporcie wydatków, więc dolna granica
przeżywa sumowanie w arkuszu. Run, którego jedyny model był niewyceniony,
eksportuje swój prawdziwy `cost_usd` równy `0` obok `cost_is_partial=true` —
nigdy samo `0`, które czytelnik weźmie za darmowe.
- **Zdelegowane runy.** Eksport runów domyślnie obejmuje wyłącznie wiersze
najwyższego poziomu, dokładnie tak jak lista, więc zsumowanie `cost_usd` daje
rachunek, a nie jego dwukrotność. To stanowisko jest w pliku, a nie tylko
tutaj: każdy wiersz niesie kolumnę `parent_run_id`, pustą dla runa, który ktoś
uruchomił, i wypełnioną dla delegacji, więc czytelnik, który świadomie włączy
`include_delegations`, widzi, które wiersze policzyłyby się podwójnie przy
sumowaniu w całości.
- **Dane osobowe.** Każdy eksport wysyła dokładnie tę tożsamość, którą jego
zakładka już pokazuje. Tabela runów pokazuje `user_id` i żadnego nazwiska, więc
eksport runów wysyła samo id — CSV z tym, kto co uruchomił, z rozwiązanymi
nazwiskami to ta tabela per osoba, której odmówiła decyzja 3 projektu Aktywności,
przybywająca jako plik do pobrania. Kolejka zatwierdzeń rozwiązuje już na
ekranie adresy e-mail osoby wyzwalającej i decydującej, więc eksport
zatwierdzeń je zachowuje. Niesie też wyświetlaną nazwę albo uchwyt obcego z czatu, tak jak pokazuje je kolejka, w trzech kolumnach: `asked_in_chat_platform`, `asked_in_chat_account` i `asked_in_chat_bot`. W takim wierszu obie kolumny e-mail są puste, bo publikujący nie pytał.
- **Audyt.** Każdy eksport zapisuje jeden wpis w `audit_log` — uprzywilejowany
odczyt masowy, tani do zapisania teraz i niemożliwy do odtworzenia później.
Nazywa okno, zastosowane filtry i liczbę wierszy, nigdy ciało żądania.
### Przypięty delegat nie rusza się sam { #a-pinned-delegate-does-not-move-on-its-own }
Delegat jest przypięty do wersji, więc wypuszczenie poprawki przez jego autora
nie zmienia niczego dla jego wołających, dopóki ktoś nie opublikuje rodzica
ponownie względem nowego przypięcia. To ta sama gwarancja, którą publikowanie
daje tu wszędzie indziej, i tnie w obie strony: błąd naprawiony w delegacie to
błąd wciąż żywy w każdym rodzicu, który się nie ruszył.
Pokazuje to Builder — porównuje każde przypięcie z tym, co delegat publikuje
teraz, i proponuje jego przesunięcie — bo nieaktualność, której nic nie pokazuje,
to błąd zamrożony w miejscu. Przypięcie, którego wersja już nie istnieje,
**wywraca run** i nazywa delegata; nigdy ciche zejście do bieżącej wersji.
**Zarchiwizowanie delegata zatrzymuje jego odpowiadanie, również w roli czyjegoś
delegata.** Przypięcie do już zarchiwizowanego agenta zostaje odrzucone przy
publikacji, a agent zarchiwizowany po tym, jak został przypięty, wywraca run
swojego wołającego, wymieniając go z nazwy — inaczej wycofanie agenta ze służby
zostawiałoby go działającym bez końca w tym jednym miejscu, w które nikt nie
zagląda, a autor, który go wycofał, nigdy by się o tym nie dowiedział.
### Limity kroków { #step-limits }
Drugi rodzaj rozbiegania to pętla narzędziowa: tania w przeliczeniu na wywołanie
i nigdy się nie kończy. Budżet tylko wystawia za nią rachunek. `max_steps`
ogranicza, ile żądań do modelu może wykonać jeden run, i to on faktycznie ją
zatrzymuje.
Strażnik pętli zatrzymuje większość pętli wcześniej i mówi dlaczego. Ma go każdy
agent, domyślnie włączony. Gdy run wywoła jedno narzędzie z tymi samymi
argumentami cztery razy z rzędu albo trzy razy obejdzie cykl dwóch lub trzech
wywołań, model dostaje informację, że się zapętlił i ma zmienić podejście. Jeśli
wywołania po tym ostrzeżeniu znowu się zapętlą, run kończy się jako
`loop_stopped`, z powodem wymieniającym narzędzia - nigdy ich argumenty.
Wywołania z różnymi argumentami nigdy nie są pętlą i liczą się tylko wywołania
tego runa. Builder ustawia oba progi albo wyłącza strażnika obok `max_steps`
(`loop_guard` w specu).
### Recenzenci runu { #run-reviewers }
Długi run dryfuje: instrukcje bledną, twierdzenia bez źródła się mnożą, agent
odchodzi od prośby. Agent może mieć do pięciu recenzentów (`reviewers` w specu,
_Recenzenci runu_ w Builderze obok limitów), każdy to drugi, zwykle tańszy model z
jednym zadaniem. Co `every` żądań do modelu recenzent czyta ostatnie `window`
tokenów runu i albo zostawia go w spokoju, albo wysyła jedną krótką poprawkę,
którą model runu dostaje przy następnym żądaniu, z nazwą recenzenta. Widok runu
wymienia każdą poprawkę i to, kto ją wysłał.
Wywołania recenzenta są sprawdzane z budżetem agenta i organizacji, zanim
zostaną wykonane, i księgowane na runie po cenie modelu recenzenta. Wywołanie
odrzucone z powodu budżetu jest pomijane, a recenzent, który zawiedzie, jest
logowany; żadne z nich nie przerywa runu. Agent bez recenzenta nie wykonuje
dodatkowych wywołań modelu.
### Tryb kodu { #code-mode }
Agent, który przechodzi przez rekordy - zaktualizuj dziesięć wierszy, potem je
podsumuj - płaci turą modelu za każde zależne wywołanie, a każda tura wysyła
ponownie cały kontekst. Z włączonym _Trybem kodu_ (`code_mode` w specu, obok
limitów w Builderze) narzędzia agenta są zwijane w jedno narzędzie `run_code`:
model pisze krótki skrypt Pythona, który działa w sandboksie Monty i wywołuje
narzędzia w pętlach i równolegle, do `max_tool_calls` na skrypt.
Naraz działają najwyżej cztery wywołania skryptu, a reszta czeka, więc skrypt
rozsyłający setki wywołań w sandboksie nie otworzy ich wszystkich jednocześnie.
Każde wywołanie ze skryptu przechodzi przez te same guardraile, budżet i
sprawdzenia uprawnień co wywołanie bezpośrednie. Narzędzie wymagające
zatwierdzenia nigdy nie trafia do skryptu: pozostaje zwykłym wywołaniem, a run
zatrzymuje się dla człowieka jak zawsze. Tak samo pytania, delegowania i
narzędzia, których wywołanie jest tym, co widzi osoba (wersje robocze, wykresy,
rysunki). Widok runu wymienia każdy skrypt i każde
wywołanie, które wykonał.
### Raportowanie { #reporting }
Run, którego nie dało się wycenić — model, którego `genai-prices` nie zna — jest
zapisywany na zero z ostrzeżeniem, a suma jest oznaczana jako **dolna granica**,
zamiast być zgadywana. UI pokazuje to jako `+` obok liczby.
**Odczyty i zapisy cache są liczone osobno.** Tokeny wejściowe runa obejmują te,
które dostawca podał z cache promptu, i te, które do niego zapisał, a wszystkie
trzy są wyceniane osobno - na Claude Sonnet 5.5 odczyt z cache kosztuje 5% stawki
wejściowej, a zapis 125%. Każdy run i każda odpowiedź zapisują
`cache_read_tokens` i `cache_write_tokens` obok sumy, koszt jest liczony z tego
podziału, a widok runa i koszt pod każdą odpowiedzią go pokazują - i to pozwala
uzgodnić run z konsolą dostawcy.
**Koszt runa obejmuje jego delegacje.** Run, który ktoś uruchomił, niesie cały
ledger, łącznie z delegatami; własny wiersz delegacji dzieli te same pieniądze
według agenta, a widok runa mówi, że jego delegacje są już w liczbie nad nimi.
Dodanie obu liczy delegacje dwa razy.
**Cache promptu, który przestaje trafiać, jest oznaczany.** Cache'owanie opłaca
się tylko, dopóki początek każdego żądania jest identyczny; zmieniona kolejność
narzędzi albo znacznik czasu w instrukcjach sprawia, że dostawca liczy pełną
stawkę za tokeny, które miał w cache. Każdy agent odczytuje z każdej odpowiedzi
liczby cache podane przez dostawcę, a żądanie, którego prefiks z cache się
załamał, jest oznaczane w wykresie żądań runu - _cache zerwany_, gdy dostawca
powinien go jeszcze trzymać, _cache wygasł_, gdy minęła retencja - z liczbą
tokenów z cache, których nie odczytało, i logowane z agentem i modelem.
Pierwsze żądanie tury jest porównywane z tym, co zapisała poprzednia tura, gdy
obie obsługuje ten sam worker API.
Liczby są zapisywane na każdym runie, więc **Runs → Spend** sumuje, gdzie cache
nie trzymał: karta _Cache promptu_, jeden wiersz na agenta i model, z busts,
wygaśnięciami i tokenami z cache, których nie odczytano z powrotem, w wybranym
oknie - to miara chybienia, a nie tego, co naliczono ponownie, bo prompt trzymany
w cache może też skrócić kompakcja.
Karty nie ma, dopóki każdy cache trzymał.
## Zatwierdzenia { #approvals }
Narzędzie, które autor agenta objął bramką, parkuje run i czeka na człowieka.
Nic nie jest za bramką, dopóki ktoś o to nie poprosi: bramka, której nikt nie
wybrał, parkuje runy w kanałach i rutynach, gdzie nikogo nie ma, żeby odpowiedzieć.
Rozstrzyganie idzie od najbardziej szczegółowego:
1. własne nadpisanie narzędzia, jeśli je ma
2. tryb `approval` capability (`required` | `never` | `default`)
3. brak zatwierdzania, gdy oba mówią `default`
`side_effecting` niczego już tu nie rozstrzyga. Builder pokazuje go obok wyboru,
więc narzędzia działające na świat autor widzi jako pierwsze.
Builder podaje wynik słowami, zamiast opisywać regułę, bo reguła, którą czytelnik
musi wykonać w głowie, to ustawienie, którego nikt nie odważy się dotknąć.
```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
```
Pięć właściwości, które warto znać:
- **Zaparkowany run da się wznowić.** Jego historia wiadomości jest
przechowywana, więc decyzja jest stosowana do rozmowy, do której należy, a nie
zaczyna wszystkiego od nowa.
- **Zaparkowany run przeżywa przeładowanie i dalej to mówi.** Transkrypt
przechowuje wywołanie, na którym run się zatrzymał, jako `awaiting_approval`, a
nie `running`, więc ponowne otwarcie rozmowy dalej pokazuje czekający krok — a
`GET /runs/{id}/parked` odpowiada oczekującymi wywołaniami (zatwierdzenie do
rozstrzygnięcia, narzędzie, jego argumenty), i tak właśnie czat odbudowuje
panel zatwierdzenia, który ramka `tool_approval_required` na żywo dała temu,
kto akurat patrzył. Dla runa, który nie jest zaparkowany, odpowiada pusto i
jest bramkowany przez `approvals:decide` tak samo jak kolejka, bo jego wiersze
są oferowane do rozstrzygnięcia
([#601](https://github.com/vstorm-co/agenticos/issues/601)). Krok nie czyta się
jako czekający w nieskończoność: wznowienie rozlicza go tym, co zwróciło
wywołanie, a wygaśnięcie — powiadomieniem o upływie czasu.
- **Kontynuacja mówi, co zrobiła.** `POST /runs/{id}/resume` odpowiada
wywołaniami narzędzi, które wykonała kontynuacja, po kolei, każde z tym, co
wróciło — a transkrypt zapisuje je niezależnie od tego, czy doszła do
odpowiedzi. Kiedyś brakowało obu połówek i jedno zatwierdzenie mogło ukryć
nieograniczoną ilość pracy: agent działał wewnątrz żądania wznowienia, a nie na
sockecie, którym strumieniuje rozmowa, więc nic nie ogłaszało jego wywołań, a
zapis transkryptu był pomijany dla segmentu bez odpowiedzi. Run, który
przeczytał plik, a potem poprosił o uruchomienie drugiej komendy, nie pokazywał
niczego między dwoma zatwierdzeniami i niczego też nie zapisywał.
- **A to, co zwróciło samo zatwierdzone wywołanie, ląduje na kroku, który został
zatwierdzony.** Przychodzi osobno od własnych wywołań kontynuacji (`settled`, a
nie `steps`), bo wykonało je to wykonanie, które zaparkowało: wznowienie
produkuje jego zwrot bez wywołania, do którego on należy, więc zamyka wiersz
już zapisany, zamiast otwierać nowy. Zapisanie go jako kroku wstawiłoby tę samą
komendę do tury dwa razy; niezapisywanie go w ogóle — co działo się do czasu,
aż zaczęło być zapisywane — czyniło z jedynego wywołania, które ktoś świadomie
przejrzał, jedyne wywołanie bez wyniku gdziekolwiek.
- **Dalej da się go wznowić, jeśli kontynuacja się nie powiedzie.** Run jest
kontynuowany na wersji, na której zaparkował, a spec tej wersji mógł od tego
czasu przestać się budować — usunięty sekret, który nazywa powiązanie, usunięty
profil modelu, capability wyrzucona przy wdrożeniu, cofnięte współdzielenie
połączenia MCP. Spec jest składany, zanim run opuści kolejkę zatwierdzeń, więc
odmowa w tym miejscu odrzuca _próbę_: decyzja pozostaje w mocy, a wznawianie
znów działa, gdy zadziała spec. Run zadany na czacie, którego nikt nie kontynuował,
kończy siatka bezpieczeństwa około godziny po ostatniej decyzji.
- **Kto kontynuuje run zadany na czacie.** Decyzja osoby w konsoli, w czacie webowym
albo przez API kontynuuje run na jego czacie, jako ona, i odpowiedź trafia tam;
decyzja klucza jest zapisywana i niczego nie kontynuuje. Kontynuacja może zginąć
albo zostać odrzucona, wtedy run czeka w `awaiting_approval` bez niczego
oczekującego. Cogodzinna siatka bezpieczeństwa kończy taki run kanałowy
`STRANDED_AFTER` (godzinę) po ostatniej decyzji i raz mówi o tym czatowi - nie,
gdy decyzja jest starsza niż 7 dni, ani gdy czat już o tym usłyszał. Obejmuje
tylko runy kanałowe. Zob.
[Rozstrzygnięte poza czatem](channels.md#decided-outside-the-chat).
- **Rozstrzygniętego zatwierdzenia nie da się rozstrzygnąć drugi raz.** Druga
decyzja zostaje odrzucona — łącznie z decyzją przychodzącą sekundę po tym, jak
wywołanie zabrało przemiatanie wygasających.
- **Zaparkowane wywołanie zostaje odrzucone przez upływ czasu, gdy przekroczy
`APPROVAL_EXPIRY_HOURS`**, a stojący za nim run zostaje rozliczony, zamiast
zostać zaparkowanym na zawsze. Status to `expired` z nullowym
`decided_by_user_id`, i to właśnie odróżnia wygaśnięcie od odrzucenia w śladzie
odpowiedzialności. Run zadany na czacie raz mówi też czatowi, że prośba wygasła. Strona Aktywność dalej pokazuje **wiek** najstarszego
oczekiwania, bo kolejka mieszcząca się w swoim oknie wygasania to ta, na której
ktoś jeszcze może zadziałać.
- **`required` działa na dowolnej capability**, nie tylko na tych ze skutkami
ubocznymi. „To tylko czyta, ale w mojej organizacji i tak ktoś to zatwierdza”
to prawdziwa decyzja i da się ją wyrazić.
- **Z wyjątkiem narzędzia, które uruchamia provider modelu — tam zostaje to
odrzucone przy publikacji.** Bramka opakowuje _wykonanie narzędzia_, czyli
jedyne miejsce, w którym da się zatrzymać wywołanie, więc natywne pobranie
strony albo natywne wyszukiwanie — wykonywane po stronie providera — nigdy do
niej nie dociera, a zabramkowanie takiego narzędzia zostawiłoby pustą kolejkę,
podczas gdy agent działałby bez zatwierdzenia. Które konfiguracje przekazują
które narzędzia, deklaruje sama capability (`provider_executed` w swoim
`register(...)`), więc odmowa obejmuje każdą capability, której przybędzie
metoda wykonywana po stronie providera, a nie tylko te, o których akurat
wiedział walidator
([#857](https://github.com/vstorm-co/agenticos/issues/857)). Wybierz metodę,
którą to wdrożenie uruchamia samo, albo zrezygnuj z wymogu zatwierdzenia; oba
są prawomocnymi agentami, a to, którego się chce, nie jest decyzją do podjęcia
za autora.
- **A wersja opublikowana, zanim ta odmowa istniała, nie działa.** Nic nie
waliduje ponownie zamrożonej wersji — run ładuje swój przechowywany spec i go
składa — więc to samo sprawdzenie wykonuje się jeszcze raz przy budowaniu
agenta i odmawia, zamiast po cichu podmienić metodę, żeby bramka zadziałała.
Koszt jest realny i zamierzony: agent, który dotąd tak działał, zatrzymuje się,
z komunikatem mówiącym, co zmienić. Zatrzymuje się agent, którego operator
poprosił o zatwierdzenie, o które nigdy nikogo nie proszono.
- **Zaparkowane wywołanie może nazwać to, co oznaczają jego id.** Narzędzie może
opisać własne wywołanie: zapisy do tabel podają nazwę tabeli. Rekord nigdy nie jest nazywany, bo etykieta rekordu
to tekst jednej z jego komórek, a zatwierdzenie by go przechowało. Nazwy są
ustalane jako osoba runa w chwili parkowania wywołania, więc nie pojawia się nic,
czego ta osoba nie może zobaczyć, i są zapisywane w zatwierdzeniu jako
`context`. Karta w czacie i strona Approvals zaczynają od zbudowanego z nich
zdania, na przykład „Share the view Closed tickets of Support with everyone who
may read the table”, nad argumentami, które pozostają tym, co się wykona.
Zdanie jest odnajdywane po stałym id narzędzia, więc narzędzie przemianowane
przez binding je zachowuje. Nieudany albo zbyt wolny opis parkuje wywołanie bez
niego, a wywołanie już rozstrzygnięte przez wznowiony run nie jest opisywane
ponownie.
- **Jeden krok modelu może zaparkować kilka wywołań.** Model, który odpowiada
dwoma wywołaniami ze skutkami ubocznymi naraz — „wyślij e-mail do klienta i do
opiekuna konta” — parkuje oba, każde jako własny wiersz zatwierdzenia
rozstrzygany osobno. Wiersze są zapisywane wtedy, gdy run parkuje, a nie w
momencie bramkowania każdego wywołania, bo wywołania działają równolegle, a
sesja bazodanowa runa nie jest bezpieczna przy współbieżności
([#169](https://github.com/vstorm-co/agenticos/issues/169)).
### Jak bardzo jedna rozmowa chce być pytana { #how-much-one-conversation-wants-to-be-asked }
Karta akceptacji oferuje również **Zezwalaj na ten tool w tej rozmowie** i **Zawsze zezwalaj na ten tool dla tego agenta**. Zgoda dotyczy tylko wskazanego narzędzia i konta, osoby udzielającej zgody oraz opublikowanej wersji agenta. Wymaga tego samego uprawnienia i przełącznika organizacji co zatwierdzanie wszystkiego. Nie upoważnia delegata ani pracy bez nadzoru. Zmiana wersji agenta wymaga ponownego wyboru.
Wybory są zapisywane na serwerze. **Chat controls → Preferences → Zapamiętane akceptacje tooli → Pytaj ponownie** cofa zgodę bez usuwania historii pierwotnej akceptacji. Przed każdym automatycznym zatwierdzeniem ponownie sprawdzane są członkostwo, uprawnienie do akceptacji i polityka organizacji. Tryb pytania o wszystko ma pierwszeństwo przed zapisanymi zgodami. Każde automatyczne zatwierdzenie nadal zapisuje osobę udzielającą zgody i rzeczywiste argumenty wywołania.
Reguła powyżej należy do agenta, jest rozstrzygana w czasie publikacji i per
narzędzie, i to jest dla niej właściwe miejsce: to wypowiedź o tym, czym agent
_jest_. Czego nie potrafi wyrazić, to nastrój jednej sesji — ktoś przechodzący
dwadzieścia tur z agentem, który bramkuje trzy narzędzia, odpowiada na te same
trzy pytania w każdej turze, a jedynym wyjściem było ponowne opublikowanie agenta
i zmienienie go dla wszystkich, na stałe, żeby naprawić jedno popołudnie
([#925](https://github.com/vstorm-co/agenticos/issues/925)).
Dlatego sesja czatu niesie **tryb zatwierdzeń**, na ramce wysyłki obok nadpisania
modelu, wczytywany do runa:
| Tryb | Co robi |
| --- | --- |
| **Follow the agent** (domyślny) | Rozstrzyga spec. Dokładnie to zachowanie, które istniało, zanim powstała ta kontrolka, i to, co dostaje klient, który nie wysyła niczego |
| **Approve everything** | Stała zgoda dla tej rozmowy: każde zabramkowane wywołanie jest przyznawane bez parkowania — i każde dalej zapisuje swój wiersz |
| **Ask about everything** | Bramkuj każde narzędzie, do którego agent może sięgnąć, łącznie z tymi, których spec nie zabramkował, i z tymi, których nie posiada żadna capability |
Cztery rzeczy czynią z tego ustawienie sesji, a nie dziurę w modelu:
- **Zostaje odrzucone, nigdy obniżone.** Wołający, który nie może zrzec się
pytań, dostaje o tym informację; tura nie rusza po cichu zgodnie ze specem, bo
ktoś, kto wierzy, że wyłączył pytania, a potem znajduje zaparkowany run,
usłyszał coś odwrotnego do tego, co się stało. Sprawdzenie mieszka w
`AgentRunnerService.prepare`, jedynym lejku wspólnym dla świeżego i wznowionego
runa, a nie przy sockecie, o którym wołający mógłby zapomnieć.
- **Zrzeczenie się wymaga `approvals:decide` i zgody organizacji.** Stała zgoda
_jest_ tą decyzją, dla której zapisywania istnieje kolejka zatwierdzeń, a
`member` i `builder` uruchamiają agentów, nie mając tego uprawnienia — więc bez
sprawdzenia uprawnienia codzienny użytkownik czatu przyznaje sobie, jednym
kliknięciem, władzę, której API odmawia mu jeden endpoint dalej. Pułapem jest
własny przełącznik organizacji (`chat_may_waive_approvals`, domyślnie
włączony dla nowych organizacji, zmieniany przez kogoś z `approvals:decide`): bez niego świadomie
postawiona przez Buildera bramka na `send_email` jest jedno kliknięcie od
zniknięcia w każdej rozmowie, a model per narzędzie staje się doradczy.
- **Brak kanału dalej znaczy nie.** Zrzec się pytań może wyłącznie sesja czatu w
przeglądarce. Harmonogram, webhook, embed i kanał zostają odrzucone, bo
`ApprovalGate` odmawia już runowi, który nie ma kogo zapytać, a stała zgoda nie
może stać się obejściem tego.
- **Każde zrzeczone wywołanie i tak jest zapisywane.** Wiersz jest zapisywany
jako `approved`, z nazwą konta, które wyraziło zgodę, i z
`decided_via = "standing"` — a zapis zatwierdzeń mówi o tym słowami obok tej
nazwy. Pominięcie wiersza sprawiłoby, że runa ze zrzeczeniem nie dałoby się
odróżnić od agenta, który nigdy nie był bramkowany, czyli że cały ten ślad po
cichu przestałby nim być. Nikt nie przeczytał tych argumentów, zanim się
wykonały; wiersz jest miejscem, w którym ktoś czyta je potem.
**Ask about everything zaostrza politykę sesji.** Wybranie tego surowszego trybu
nie wymaga specjalnego uprawnienia. Obejmuje on wywołania MCP obsługiwane przez runner, choć
narzędzia te nie są deklarowane przez capabilities. Listy dozwolonych narzędzi
na połączeniu i powiązaniu z agentem określają, co jest dostępne; na człowieka
każe im czekać własna polityka `approval` powiązania - `writes` albo `all` - którą
domyślny tryb **Follow the agent** stosuje tak jak politykę capability (zobacz
[które wywołania czekają na człowieka](mcp.md#which-calls-wait-for-a-person)).
### Zatwierdzenie w trakcie rozmowy głosowej { #an-approval-inside-a-voice-call }
[Rozmowa głosowa](voice.md#approvals-in-a-call) nie może się wstrzymać i czekać
na kolejkę, więc jej zatwierdzenia rozstrzyga się w rozmowie, a decyduje osoba,
która w niej jest:
- Prośba trafia do kolejki od razu, a rozmowa pokazuje narzędzie, dokładne
argumenty, z którymi się wykona, i to, co według transkrypcji powiedział
rozmówca. Zatwierdzenie wykonuje te argumenty, raz.
- Rozstrzyga się ją **wyłącznie przez rozmowę** i tylko wtedy, gdy rozmówca ma
`approvals:decide`. `POST /approvals/{id}` odrzuca prośbę, która należy do
trwającej rozmowy, a opisane niżej przemiatanie nieaktualnych zatwierdzeń
pomija runy głosowe.
- **60 sekund** bez decyzji powoduje jej wygaśnięcie, bez decydenta - ten sam
null, który zapisuje przemiatanie. Rozmowa, która kończy się z dowolnego
powodu, powoduje wygaśnięcie wszystkiego, na co jeszcze czekała.
- Członek bez `approvals:decide` słyszy to wprost, a narzędzie jest odrzucane.
Odwiedzający w widgecie albo na hostowanej stronie nigdy nie widzi narzędzia,
które wymaga zatwierdzenia: jest ono pomijane w jego rozmowie.
`approval_mode` wynika ze speca agenta, tak jak w czacie tekstowym, a każda
decyzja trafia do śladu audytowego jak każda inna.
### Decyzja, której nikt nie podejmuje { #a-decision-nobody-makes }
Zatwierdzenie czeka na człowieka, a niektóre czekają wiecznie: recenzent odszedł,
o narzędzie poproszono w piątek, nikt nie wiedział, że to on ma rozstrzygnąć. Nic
w ścieżce żądania nie może takiego zatwierdzenia zakończyć — całe założenie jest
takie, że żadne żądanie nie nadchodzi — więc godzinne przemiatanie odrzuca przez
upływ czasu wszystko, co dalej jest oczekujące po `APPROVAL_EXPIRY_HOURS`
(domyślnie trzy dni, co obejmuje weekend).
!!! warning "Liczy się run, a nie wiersz"
Oczekujące zatwierdzenie trzyma swój run w `awaiting_approval` bez końca:
praca, która ani nie jest skończona, ani skończona nie będzie.
Taki run siedzi w historii i w wieku najdłużej czekającego na dashboardzie, więc
przemiatanie idzie za każdym wygasłym wywołaniem w dół, do stojącego za nim runa,
i kończy go jako `cancelled` — nikt nie wrócił, a to, co run wydał, zanim
zaparkował, pozostaje w mocy.
Trzy rzeczy, których celowo nie robi:
- **Nie kontynuuje runa.** _Odrzucone_ wywołanie rozlicza się przez wznowienie:
odmowa jest odtwarzana, a agent idzie dalej, do odpowiedzi. To żądanie do
modelu na własnych kluczach organizacji, a wykonywanie go z harmonogramu, dla
runa, na który nikt nie czeka, nie jest kosztem do poniesienia bez pytania.
- **Nie kończy runa, który ma wywołanie wciąż mieszczące się w swoim oknie.** Run
parkuje na wszystkich swoich zaległych wywołaniach naraz, więc kończy się
dopiero wtedy, gdy żadne z nich nie jest już oczekujące.
- **Nie nazywa decydenta.** `decided_by_user_id` pozostaje nullem i tak samo
aktor wpisu audytowego, bo to jest właśnie zapisywany fakt. Null w tym miejscu
oznacza platformę działającą z harmonogramu i nic innego nie potrafi go
wyprodukować.
To jedyny odczyt w całym kodzie, który przecina każdą organizację, z tego powodu,
że harmonogram nie ma tenanta, do którego można by go zawęzić. Każdy zapis, który
wykonuje, dalej jest w organizacji własnej danego wiersza.
### Run, którego proces umarł { #a-run-whose-process-died }
Drugi stan, którego nic wewnątrz procesu nigdy nie rozstrzygnie.
Wiersz runa jest commitowany jako `running`, zanim zostanie zawołany jego model
([#12][12-issue]), więc worker zabity w środku runa — OOM, wdrożenie, które nie
drenuje — zostawia trwały wiersz, którego nie ma już co dokończyć: w Aktywności na
zawsze i blokujący każdy harmonogram, dla którego triggera był podlinkowanym
runem.
Gdy model pracuje, proces wykonujący run oznacza go jako żywy co
`RUN_HEARTBEAT_SECONDS`. Przemiatanie co minutę kończy jako **`failed`** wszystko,
co dalej jest `running` bez znaku życia przez `STALE_RUN_REAPED_AFTER_SECONDS` —
domyślnie dwie minuty, zero to wyłącza — i mówi o tym otwartym konsolom, więc
czat przestaje pokazywać "Working" nad turą, na którą nikt nie odpowiada, i
proponuje zapytać ponownie. Run rozmowy głosowej zostaje dla reapera rozmów,
który ją rozłącza. Nikt nie zatrzymał tego runa, zrobiła to infrastruktura, a operator
filtrujący historię runów pod kątem problemów to dokładnie ten, kto powinien go
zobaczyć.
Błąd w wierszu to własne zdanie przemiatania. Proces, który wiedział więcej,
umarł.
Wiek runa jest tu jego **ostatnim przejściem**, a nie pierwszym startem.
Wznowienie zachowuje oryginalne `started_at` — run rozciąga się na oba segmenty —
więc run zatwierdzony wiele dni po zaparkowaniu starzeje się od momentu, w którym
zaczęło się jego odtwarzanie, a nie od startu, przez który zostałby sprzątnięty w
środku odtwarzania.
Ten pułap i tak nie musi być dokładny w żadną stronę, bo żywy run, którego
przemiatanie mimo wszystko przestawi, przestawia się z powrotem sam: jego własny
zapis terminalny ląduje później i wygrywa.
Czego sprzątnięty run nie może odzyskać, to jego **wydatki**. Rejestr umarł razem
z procesem, więc wiersz zachowuje zera, z którymi został otwarty, zamiast dostać
liczbę, którą ktoś uzgadniałby z rachunkiem.
I nikt nie dostaje maila. Powiadomienie o awarii jedzie na `finish`, które ma w
ręku agenta i jego spec; przemiatanie nie ma ani jednego, ani drugiego.
[12-issue]: https://github.com/vstorm-co/agenticos/issues/12
### Zatwierdzenie wewnątrz delegacji { #an-approval-inside-a-delegation }
Narzędzia delegata są bramkowane przez własny spec delegata, a trafia on do tej
samej kolejki, na której czeka już wołający rodzica — specjalista, który
potrzebuje człowieka, potrzebuje tego człowieka, który tam stoi.
Wpis nazywa narzędzie **delegata** i argumenty, które ten zaproponował, bo
zapisała go własna bramka delegata. I nazywa **który delegat je wywołuje**.
Bez tej ostatniej części kolejka mówi `send_email`, nie mówiąc, czy wysyła to
agent, z którym ktoś rozmawia, czy specjalista o nazwie `researcher`. To kolejka,
którą ludzie zatwierdzają na ślepo — a w delegacji rzecz zatwierdzana bywa
poważniejsza w skutkach niż agent, z którym recenzent sądzi, że ma do czynienia.
Usunięcie tego delegata nie wymazuje zapisu tego, do czego był upoważniony:
wiersz zachowuje nazwę delegata i porzuca tylko link do jego już nieistniejącego
agenta. Dzieje się tak nawet wtedy, gdy usunięcie ląduje, podczas gdy run jest
jeszcze zaparkowany, zanim wiersz zatwierdzenia został zapisany — odroczony zapis
([#169](https://github.com/vstorm-co/agenticos/issues/169)) rozstrzyga delegatów
dalej obecnych i zapisuje nullowe id dla tego, który zniknął, dokładnie tak, jak
zrobiłoby usunięcie go po tym, jak wiersz już istniał.
Run rodzica parkuje, zamiast dostać do ręki coś, co wygląda jak skończona
delegacja. Warto to powiedzieć, bo kiedyś było inaczej: każdy agent budowany tu
deklaruje typ wyjścia, który pozwala runowi skończyć się ze swoimi zaparkowanymi
wywołaniami jako _wyjściem_, zamiast podnieść wyjątek, a biblioteka delegacji
serializowała ten obiekt i podawała modelowi rodzica
`{"calls": [], "approvals": [...]}` jako raport specjalisty, z zadaniem
oznaczonym jako ukończone. To była ścieżka domyślna, a nie przypadek brzegowy, i
jest naprawiona w przypiętej wersji.
Zatwierdzenie **kontynuuje delegata**, zamiast delegować ponownie. Zaparkowany
stan jest drzewem — jeden poziom na agenta, każdy z własną rozmową i własnymi
zaparkowanymi wywołaniami — więc przyznanie zatwierdzenia wznawia zawieszonego
delegata od miejsca, w którym się zatrzymał, z werdyktem dołączonym do wywołania,
które recenzent faktycznie zobaczył. Wywołanie `task` rodzica jest odtwarzane, a
delegacja odnajduje miejsce, które zostawiła. Specjalista wewnątrz delegata
zachowuje się tak samo, o jeden poziom niżej.
To ma znaczenie, bo alternatywą nie jest wolniejsze wznowienie, tylko inna
odpowiedź. Ponowne uruchomienie delegacji zaczęłoby rozmowę delegata od zera i
pozwoliło jego modelowi wywołać za drugim razem inne narzędzie, więc to, co
recenzent zatwierdził, nie byłoby tym, co się wykonało.
To, co delegat już wydał, podróżuje razem z jego miejscem, więc wiersz zapisywany,
gdy delegacja wreszcie się kończy, obejmuje to wszystko — zobacz
[jako co zapisywany jest zdelegowany run](#what-a-delegated-run-is-recorded-as).
Obie połowy drzewa są trzymane per delegacja, a nie per run, i to pozwala
specjaliście trzy poziomy niżej zaparkować i dalej być rozliczonym na własnego
agenta. Wydatki są zachowywane nawet wtedy, gdy _miejsca_ delegata zachować się
nie dało — historia wiadomości z biblioteki jest telemetrią w miarę możliwości, a
delegacja uruchomiona od nowa i tak wydała to, co wydała.
!!! warning "Zatwierdzanie MCP ustawia się na powiązaniu"
Ustawienie zatwierdzeń capability nie obejmuje narzędzi MCP; obejmuje je
polityka `approval` powiązania, na każdej powierzchni, na której działa agent.
`writes` zatrzymuje narzędzia, których serwer nie oznacza jako tylko do odczytu
- to deklaracja samego serwera, więc dla serwera spoza Twojej kontroli wybierz
`all`. W czacie internetowym **Ask about everything** (`ask_all`) obejmuje też
resztę; nie zakładaj, że surowsze ustawienie sesji czatu obowiązuje w
harmonogramie lub innym punkcie wejścia. Narzędzia wykonywane po stronie
dostawcy modelu nie przechodzą przez lokalną bramkę zatwierdzeń.
### Krok workflow, który pyta osobę { #workflow-step-approvals }
Workflow pyta krokiem [`human.approval`](reference/workflow-nodes.md#human-approval),
a nie narzędziem za bramką: krok zapisuje własną prośbę - tytuł, podane mu
szczegóły, kto może zdecydować i do kiedy - a jego run na nią czeka. Zakładka
**Approvals** pokazuje je nad wywołaniami narzędzi, a decyzja budzi run, który idzie
dalej według odpowiedzi. Decyzja jest ostateczna i trafia do dziennika audytu razem
z tym, o co pytano, i zostaje na liście **Decided** razem z zapisem wywołań
narzędzi. Krok, który wskazuje zatwierdzających, mogą rozstrzygnąć tylko
oni. Prośba, której nikt nie rozstrzygnie na czas, wygasa, a anulowany run anuluje
swoje prośby.
Krok `editable` pokazuje też zatwierdzającemu propozycję, na przykład przygotowaną
odpowiedź, którą może zmienić przed zatwierdzeniem. Wpis audytu zapisuje, czy krok
był edytowalny, czy zatwierdzający go zmienił (`edited`) oraz długość propozycji i
zatwierdzonego tekstu (`proposal_length`, `final_length`) - nigdy sam tekst. Tekst
jest przechowywany przy prośbie, jawnym tekstem, dopóki historia runa nie zostanie
usunięta.
Prośby są też liczone. Karta Stats workflow i jego API statystyk podają, per krok i
per dzień, ile zatwierdzono bez zmian, ile zmieniono, odrzucono lub ile wygasło, jak
długo ludzie decydowali i jak bardzo ich poprawki coś zmieniły, każda jako stosunek od
`0` do `1` (`edit_ratio`, także we wpisie audytu); zob.
[Running a workflow](workflows.md#running-a-workflow). Zbiorcze zestawienie
organizacji, `GET /api/v1/workflow-approvals/stats`, wymaga `workflows:view` i obejmuje
tylko workflow, które członek może oglądać. Nie zawiera ani propozycji, ani
zatwierdzonego tekstu, ani liczby dla pojedynczego zatwierdzającego: liczy prośby, nie
ludzi.
### Wpisy artefaktów { #artifact-entries }
Strona czytająca dane tabel jest udostępniana za zgodą swojego właściciela i każdy
krok tego procesu zostawia wpis z osobą, która go wykonała. Żaden z nich nie niesie
rekordu, wartości filtra ani wiersza: zawierają wersje, odciski, identyfikatory
próśb i słowa, które osoba potwierdziła.
| Akcja | Zapisywana, gdy |
| --- | --- |
| `artifact.published` | Opublikowano wersję, z jej numerem, odciskiem jej zapytań strony i `actions`, czyli id workflowów, które deklaruje |
| `artifact.data_consented` | Właściciel potwierdził dane wersji albo wyrażono na nie zgodę automatycznie (`automatic`). Nazywa wersję i odcisk, `for_owner`, gdy potwierdził za niego Owner lub Admin, oraz `via_grant`, gdy dał ją grant `edit_data` |
| `artifact.data_withdrawn` | Edytor zatrzymał udostępnianie danych przez stronę; nazywa wersję, którą serwowano odbiorcom, i czy auto-zgoda była włączona, oraz ile grantów `edit_data` obniżono do `edit` |
| `artifact.auto_consent_changed` | Właściciel włączył albo wyłączył „Share new versions automatically", z tekstem, który potwierdził |
| `artifact.edit_data_granted` | Właściciel, albo Owner lub Admin, nadał osobie albo grupie „Can edit, including data", z tekstem, który potwierdził |
| `artifact.owner_changed` | Strona zmieniła właściciela: dawny i nowy właściciel, kto to zrobił i jakie granty `edit_data` odrzucono |
| `artifact.share_requested` | Złożono prośbę — typu `audience`, `access` albo `version` — przez osobę albo przez run jej agenta |
| `artifact.share_request_approved` i `artifact.share_request_declined` | Prośbę rozstrzygnięto i kto to zrobił |
| `organization.public_live_data_changed` | Owner albo Admin włączył lub wyłączył dla organizacji **Public pages with live data**, ze starą i nową wartością |
| `artifact.public_data_consented` | Właściciel, albo w jego imieniu Owner lub Admin, potwierdził, że każdy z publicznym linkiem strony i witryny, które ją osadzają, mogą czytać jej dane. Nazywa wersję, odcisk, tabele i kolumny, witryny osadzające i to, czy hasło jest ustawione, oraz `for_owner`, gdy ktoś potwierdził w jego imieniu |
| `artifact.public_data_withdrawn` | Edytor cofnął tę zgodę, zalogowany jako on sam (bez impersonacji), z odciskiem, który ona trzymała |
| `artifact.public_data_voided` | Zgoda została wyczyszczona, bo zmieniło się coś pod nią, z `reason`: `link_off`, `password_removed`, `origin_added`, `owner_changed`, `consent_withdrawn` albo `archived` |
| `artifact.action_started` | Członek potwierdził i uruchomił workflow z akcji strony. Wskazuje stronę, wersję, workflow i run, nigdy wartości wejścia, które są na runie |
**Public pages with live data** to ustawienie organizacji, domyślnie wyłączone, w
ustawieniach organizacji obok zrzeczenia się zatwierdzeń. Decyduje, czy dane strony
mogą dotrzeć do osób bez konta przez publiczny link albo embed. W odróżnieniu od
zrzeczenia wymaga `org:settings` — Owner albo Admin — i trzeba je zmienić, będąc
zalogowanym jako ten sam użytkownik: sesja z impersonacją jest odrzucana. Strona nadal
potrzebuje [zgody właściciela na dane publiczne](artifacts.md#live-data-on-a-public-link).
Wyłączenie jest wyłącznikiem awaryjnym: publiczne strony tracą dane przy następnym
ładowaniu i zachowują swoją zgodę, która znów obowiązuje po ponownym włączeniu.
Poszerzenie widoczności albo grantów strony jest zapisywane jako wpisy
`resource.share`, `resource.unshare` i `resource.visibility_changed`, jak dla każdego
innego zasobu. Narzędzie `share_view` zniknęło, a wraz z nim zatwierdzenie, które je bramkowało.
## Alerty { #alerts }
Każdy alert tutaj dotyczy runa, na którego nikt nie patrzy. Run czatu, który
zatrzymuje się na swoim budżecie, mówi o tym na ekranie; ten sam run uruchomiony
wzmianką na Slacku, harmonogramem albo wywołaniem API zatrzymuje się po cichu, a
pierwsze, co ktokolwiek o tym słyszy, to czyjeś pytanie, dlaczego agent zamilkł.
### W aplikacji, obok e-maila { #in-app-alongside-email }
Każdy alert w tej sekcji zapisuje dwie rzeczy: e-mail opisany niżej oraz wiersz
przy **dzwonku** w [Konsoli](console.md#the-bell) — jej własnej skrzynce, a nie
kopii maila. Wiersz _jest_ dostarczeniem w aplikacji; nic dodatkowego nie musi
się powieść, żeby się pojawił. E-mail to drugi, niezależnie ponawiany kanał z
tego samego zapisu, dlatego jeden może zawieść — odbity adres, padły przekaźnik
SMTP — a drugi nigdy się o tym nie dowie.
Oba kanały przełącza się niezależnie, dla każdego zdarzenia osobno, w
**Settings → Notifications** — dana osoba może zostawić wiersz w aplikacji dla
zatwierdzeń, a wyłączyć jego e-mail, albo odwrotnie.
Prośba o udostępnienie strony i zmiana jej właściciela podają nazwę strony i osoby, nigdy jej dane.
Ta sama strona niesie też
każde inne zdarzenie, które dostarcza skrzynka: nienadzorowane zakończenie albo
Run `eval` jest jednym z wielu raportowanych razem przez ewaluację; nie wysyła osobnego powiadomienia o zakończeniu ani błędzie.
błąd runa - koniec rutyny nazwany jej imieniem i zaczynający się początkiem jej
odpowiedzi, dla osoby, która ją ustawiła - prośbę o włączenie capability, dla
właściciela agenta, propozycje, które [praca z własnej
inicjatywy](#proactive-work) przygotowała dla osoby, **błąd** ingestii dokumentu, własną zbiorczą liczbę z przebiegu
konektora oraz własne ogłoszenie administratora aplikacji -
`POST /admin/announcements`, jeszcze bez strony w konsoli - adresowane do
organizacji i, opcjonalnie, do roli, oraz ograniczone do jednego albo obu
kanałów.
Dokument, który zaindeksował się czysto, nie zapisuje nic, i to jest celowe.
Kiedyś zapisywał wiersz na dokument, co w zwykłym przypadku - folder plików
dodanych naraz - oznaczało powiadomienie na plik mówiące, że nic się nie stało,
i grzebało te, które trzeba było przeczytać. Zwykła ingestia jest teraz
raportowana tam, gdzie ma sens: przez własny status dokumentu w kolekcji oraz,
dla przebiegu konektora, przez jedną linię, którą `sync_completed` zapisuje po
zakończeniu całej próby. Błąd nadal trafia do tego, kto wgrał plik, albo do
administratorów organizacji, jeśli nie wgrał go nikt.
Jedyny wyjątek to tygodniowe i miesięczne raporty użycia konfigurowane na
agencie, opisane niżej: oba dzielą jedno, starsze ustawienie e-maila, więc
wyłączenie e-maila jednego raportu wyłącza też e-mail drugiego — wiersz w
aplikacji dla każdego z nich nadal przełącza się osobno.
W przeciwieństwie do wszystkiego powyżej, zdarzenia bezpieczeństwa i zmiany
konfiguracji nie da się wyłączyć na żadnym z kanałów. Trafiają do właścicieli i
administratorów tej konkretnej organizacji — nie do szerszej grupy odbiorców
`admins` powyżej, i nigdy do administratorów aplikacji wdrożenia, chyba że sama
akcja nie ma organizacji, której można by ją przypisać — wtedy trafia zamiast
tego do każdego administratora aplikacji. Nic z tego nie poszerza tego, co
dokumentuje ta strona: to ta sama skrzynka, do której trafiają alerty
konfigurowane na agencie powyżej, a reguła rezygnacji poniżej wciąż dotyczy
wszystkiego, co można wyłączyć.
Nie jest to jednak nieograniczone: każde jest ograniczone do dwudziestu
zapisów na minutę na aktora i typ zdarzenia, więc jedno konto wprowadzające
szybkie zmiany nie zaleje każdego administratora — wpis audytowy stojący za
każdym z nich i tak zostaje zapisany, na samym śladzie ([Audyt](#audit)),
niezależnie od tego, czy powiadomienie przetrwało limit.
Powyżej tego limitu skrzynka nie milknie. Miejsce reszty zajmuje jedno
powiadomienie na aktora na minutę, mówiące, że minuta była bardziej ruchliwa,
niż skrzynka jest w stanie wypisać, i że każde z tych zdarzeń jest na śladzie.
Ma to znaczenie, bo tych dwóch nie da się wyłączyć: aktor mógłby inaczej zużyć
przydział na dwadzieścia niegroźnych edycji, a potem zrobić tę jedną rzecz,
którą warto obserwować, i nic by o tym nie powiedziało. To powiadomienie nie
niesie licznika — liczenie oznaczałoby przepisywanie go przy każdym kolejnym
zdarzeniu, czyli dokładnie ten zalew, przed którym limit ma chronić.
Wiersz znika ze skrzynki dziewięćdziesiąt dni po tym, jak został zapisany,
jeśli jest _przeczytany_, i rok po tym niezależnie od tego, czy w ogóle został
otwarty — licząc zawsze od zapisania, nigdy od przeczytania, więc wiersz
otwarty dzień przed swoją górną granicą znika razem z każdym innym w tym
wieku. To zamiatanie w tle, a nie coś, co uruchamia człowiek. Zamiatanie
mailowe nie wyśle wiersza, który już przekroczył tę granicę, więc worker
wracający po długiej przerwie nie rozśle powiadomień w drodze do ich usunięcia.
[Wyczyszczenie](console.md#the-bell) to co innego, nie to samo: człowiek
natychmiast wyjmuje wiersz z własnej listy, a sam wiersz zostaje zachowany,
dopóki nie sięgnie po niego to zamiatanie.
To, co przetrwa dłużej, zależy od tego, czego powiadomienie dotyczyło. Zdarzenie
bezpieczeństwa, zmiana konfiguracji i własne ogłoszenie administratora zaczynają
się od wpisu audytowego i ten wpis przeżywa wiersz, który pokazała skrzynka
([Audyt](#audit)). Wynik runu, rezultat ingestii i raport użycia nie piszą
własnego wpisu audytowego: zapisem są run, dokument i wydatek, które opisują, a
powiadomienie to tylko sposób, w jaki ktoś się o nich dowiedział — własne
wyliczone liczby raportu okresowego, które nie żyją nigdzie poza powiadomieniem,
są jedyną rzeczą, jaką wygasły wiersz zabiera ze sobą.
### Jeden wynik, zamykany raz { #one-outcome }
Oczekujące zatwierdzenie, nieudany przebieg i znalezisko obserwatora to rzeczy, na które ktoś musi
zareagować, więc każda jest **wynikiem**: jedną pozycją na osobę i przyczynę, niezależnie od liczby
miejsc, które o niej mówią (#2575). Karta nad czatem, z którego pochodzi, wiersz w dzwonku, push i
e-mail to dostarczenia tego jednego wyniku, a nie cztery pozycje.
Wynik potwierdza się, rozwiązuje lub wycisza raz, z karty w czacie albo z dzwonka, i każde miejsce za
tym podąża. Potwierdzenie oznacza go jako przeczytany i anuluje push oraz e-mail, które jeszcze czekają
na wysłanie; rozwiązanie i wyciszenie dodatkowo go czyszczą. Dostarczenie, które worker już trzyma, jest
sprawdzane ponownie tuż przed wysłaniem, więc to zamknięte w międzyczasie nie wychodzi.
`POST /outcomes/{id}/settle` przyjmuje akcję, a `GET /outcomes` zwraca własne wyniki wywołującego,
opcjonalnie dla jednego czatu.
Rozstrzygnięcie ostatniego oczekującego zatwierdzenia przebiegu zamyka jego wynik. Wyciszona przyczyna pozostaje wyciszona. Dla innych powiadomień rozwiązana lub potwierdzona przyczyna może otworzyć się ponownie, gdy wróci; powtórzone znalezisko obserwacji zachowuje swój stan.
Każde znalezisko obserwacji, także niepilne i bez powiadomienia w dzwonku, ma wspólną sprawę z wierszem w skrzynce i kartą w rozmowie źródłowej. **Załatwione** rozwiązuje tę sprawę; odrzucenie znaleziska ją wycisza. Obie akcje anulują oczekujące dostarczenia zewnętrzne. Potwierdzenie zatrzymuje powiadomienia i kolejne ogłoszenia w briefingu, ale pozostawia znalezisko do załatwienia w skrzynce z oznaczeniem **Przyjęte do wiadomości**. Usunięcie obserwacji rozwiązuje pozostałe sprawy przed usunięciem znalezisk. Decyzje dotyczą wyłącznie właściciela w tej samej organizacji.
Tylko jawne **Przywróć** odrzuconego znaleziska ponownie otwiera tę samą sprawę. Nie wysyła ponownie starych powiadomień. Migracja `0264_finding_outcome_state.py` łączy istniejące znaleziska ze sprawami, tworzy sprawy dla niepilnych znalezisk, zachowuje identyfikatory i wcześniejsze decyzje w skrzynce oraz anuluje nieaktualne oczekujące dostarczenia. Downgrade usuwa powiązanie, ale nie cofa decyzji ani wiadomości przyjętych już przez dostawcę; zachowaj kopię sprzed aktualizacji.
### Konfigurowane na agencie { #configured-on-the-agent }
To, kto słyszy o danym agencie, jest częścią speca agenta, pod
**Limits → Alerts**. Odbiorcy ustawiani dla całego wdrożenia czynili z
hałaśliwego agenta i z tego, którego nikomu nie wolno przegapić, jedno
ustawienie, więc jedynym sposobem na uciszenie pierwszego było ogłuchnięcie na
drugiego.
| Alert | Odpala się, gdy | Domyślni odbiorcy |
| --- | --- | --- |
| **Budget** | ten agent osiągnął swój własny limit miesięczny | administratorzy i właściciel agenta |
| **Approvals** | wywołanie narzędzia zaparkowało | ten, kto uruchomił run, plus administratorzy |
| **Usage** | tygodniowo i miesięcznie, ile ten agent wydał | wyłączone |
Log workflow ma własne alerty, ustawiane na jego stronie Logs, a nie w specyfikacji;
zob. [Alerty](workflows.md#log-alerts). Odpalają się, gdy pasujące wiersze reguły
osiągną jej próg w jej oknie, i trafiają do właściciela workflow, o ile reguła nie mówi
inaczej. Podlegają tym samym preferencjom dzwonka i e-maila co reszta tej sekcji, w
**Workflow log alerts**.
Odbiorcy to lista ról, a nie adresów:
| Odbiorcy | Rozwiązują się do |
| --- | --- |
| `admins` | właściciele i administratorzy organizacji, **plus administratorzy aplikacji tego wdrożenia** |
| `owner` | właściciel agenta |
| `initiator` | ten, kto uruchomił run; nikt, gdy run rozpoczął harmonogram |
| `chosen` | dokładnie ci członkowie, którzy są przy nim wymienieni |
Role, a nie adresy, bo spec jest eksportowany do repozytorium klienta i przeżywa
wymienionych w nim ludzi: `admins` dalej oznacza właściwych ludzi po
reorganizacji i oznacza ich w tej organizacji, do której spec zostanie
zaimportowany. Wymieniony z nazwy członek, który odszedł, nie wnosi nic, zamiast
podnosić wyjątek — kolejka zatwierdzeń nie może zamilknąć dlatego, że jedno id
już się nie rozwiązuje.
### Alert linkuje tam, gdzie jest decyzja { #an-alert-links-to-where-the-decision-is }
**Mail o zatwierdzeniach otwiera kolejkę** — `/runs?tab=approvals`, zakładkę
Approvals w Aktywności, która jest jedyną powierzchnią niosącą Approve i Reject.
Kiedyś otwierał `/agents/{id}`, czyli Buildera: jedno zdanie prozy o wywołaniach
narzędzi trafiających do kolejki i żadnej kolejki. Więc ten jeden e-mail, którego
całym celem jest _ktoś musi zdecydować, teraz_, lądował o jedno wyszukiwanie od
decyzji, podczas gdy zaparkowany run starzał się w stronę
`ApprovalService.expire_stale` (#935). Ta zakładka nie miała żadnego URL-a,
dopóki #934 nie umieściło jej w `?tab=`.
Celowo **nie** nazywa runa przez `&run=`, choć alert takowy trzyma: kontrolki
decyzji są w wierszu kolejki, a poniżej `lg` skupiony run zastępuje listę — co
ukryłoby je przed czytelnikiem, który najpewniej jest na telefonie.
Mail budżetowy otwiera agenta i to jest dla niego właściwy cel: limit, o którym
raportuje, edytuje się właśnie tam.
### Alert o zatwierdzeniu to dwa e-maile { #the-approval-alert-is-two-emails }
`approvals:decide` należy do `owner`, `admin` i `operator`. Domyślni odbiorcy
zaparkowanego wywołania obejmują tego, kto uruchomił run, a builder uruchamiający
własnego agenta z czatu jest zwyczajnym inicjatorem — więc alert rutynowo
docierał do kogoś, komu platforma by odmówiła. Dostawał **„waiting on your
approval”** z przyciskiem **Review the request**, klikał go, a Aktywność nie
rysowała żadnej zakładki Approvals: odmowa przybywała jako nieobecna zakładka, a
nie jako zdanie.
Dlatego odbiorcy są dzieleni według uprawnienia, a nie przycinani do niego:
| Odbiorca ma | Dostaje |
| --- | --- |
| `approvals:decide` | prośbę, z linkiem do kolejki |
| cokolwiek mniej | _fakt_: run jest wstrzymany, a nie przewrócony, zatwierdzenie go należy do właściciela, administratora albo operatora, i nic się od niego nie wymaga |
Przycięcie zamiast tego zostawiłoby jedyną osobę, która na pewno czeka na ten run
— osobę, która go uruchomiła — bez żadnej informacji o tym, dlaczego się
zatrzymał.
Drugi e-mail nie niesie **żadnego linku**, i to jest celowe, a nie
niedokończone. To, że `agents:view` jest uprawnieniem roli, nie czyni
pojedynczego agenta osiągalnym: dostęp do agenta jest rozstrzygany per zasób,
więc odbiorca z `chosen` bez grantu do prywatnego agenta dostałby drugie wezwanie
do działania, którego platforma odmawia — ta sama usterka w nowym miejscu. Od
tego czytelnika nic się nie wymaga, więc nic mu się nie oferuje. Nie twierdzi
też, że powiedziano komukolwiek innemu: odbiorcy złożeni z jednego niedecydenta
oznaczają, że do nikogo, kto może zdecydować, nie poszedł żaden mail, a zdanie
obiecujące co innego zostawiłoby go w oczekiwaniu na kogoś, kto nigdy się nie
dowiedział.
To, które role rozstrzygają, jest odczytywane z katalogu uprawnień, a nie
wypisane obok niego: rola zyskująca albo tracąca `approvals:decide` nie może
zostawić routingu w tyle, co jest tą samą usterką jeden poziom wyżej.
### Każdy link mówi, której organizacji dotyczy { #every-link-says-which-organization-it-is-about }
Konsola działa na tej organizacji, której czytelnik użył ostatnio: `apiClient`
stempluje `X-Organization-Id` z wyboru utrwalonego per przeglądarka. Kiedyś żaden
URL alertu go nie niósł, więc ktoś należący do dwóch organizacji, kto ostatnio
pracował w Globexie, otwierał alert o zatwierdzeniu dla runa w Acme i czytał
kolejkę **Globexa** — najpewniej pustą i czytającą się jako _nic nie czeka_ o
runie, który jest zaparkowany i starzeje się w stronę
`ApprovalService.expire_stale`. Linki do agentów myliły się ciszej: `/agents/{id}`
pod niewłaściwą organizacją to odmowa dla agenta, którego czytelnik naprawdę może
zobaczyć, o jedno przełączenie dalej.
Dlatego każdy link niesie `org=<id>`, budowany w jednym miejscu — w
`NotificationService._link`, które wybiera separator na podstawie ścieżki, bo
link do zatwierdzeń niesie już `?tab=approvals` — a nie w każdym z czterech
miejsc wywołania, a konsola przyjmuje go dokładnie tak, jak przyjmuje id w
`/orgs/{id}`: strona, która nazywa organizację, _jest_ tą organizacją. Przyjęcie
to efekt układu w `ActiveOrgGuard`, przed zresetowaniem cache'u tenanta i przed
własnymi zapytaniami strony, więc pierwsze żądanie, które strona wykonuje, niesie
już właściwego tenanta. Ścieżka bije parametr — link mówi, której organizacji
dotyczył alert, i nie może przenieść kogoś ze strony, na której właśnie stoi.
Czytelnik, który od tego czasu opuścił tę organizację, dostaje o tym informację,
zamiast być po cichu przeniesionym: odmowa nazywa link jako powód, bo ciche
przełączenie jest sposobem, w jaki strona innej organizacji staje się odpowiedzią
na alert. Nie może nazwać tej organizacji — czytelnik nie jest jej członkiem,
więc nie ma jej na jego liście.
### Dwie reguły, które nie podlegają negocjacji { #two-rules-that-are-not-negotiable }
**Osobista rezygnacja zawsze tylko odejmuje.** Własne przełączniki każdego
odbiorcy w **Settings → Notifications** są stosowane na końcu. Agent może
zdecydować, że administratorzy powinni o nim słyszeć; administrator dalej może
zdecydować, że nie chce maili budżetowych. Nic, co napisze autor agenta, nie
wciela nikogo do skrzynki odbiorczej.
**Limit organizacji ignoruje spec całkowicie.** To ograniczenie zatrzymuje
każdego agenta w organizacji, a autor agenta nie może go podnieść, więc jego
alert idzie do administratorów niezależnie od tego, o co prosi którykolwiek
agent. Agent nie może uciszyć ograniczenia, którego nie kontroluje.
### Cisza coś znaczy { #silence-is-meaningful }
Organizacja, która nic nie uruchomiła, nie dostaje raportu. Cotygodniowe
„0 runs, $0.00” to raport, który ludzie filtrują do folderu, a potem trafia tam
również ten, który miał znaczenie.
Liczba w tym raporcie to wydatki organizacji w danym oknie — ta sama arytmetyka,
którą egzekwuje się limit, z ingestią włącznie i ze zdelegowanymi runami
policzonymi raz. Raport, którego suma nie zgadza się z ograniczeniem
egzekwowanym przez platformę, jest gorszy niż brak raportu, bo obie liczby
wyglądają wiarygodnie.
Wysyłka nigdy nie blokuje i nigdy nie podnosi wyjątku do wołającego: run, który
już się skończył, nie może przewrócić się ponownie dlatego, że SMTP był
niedostępny.
## Płatna ewaluacja { #ai-assistance }
Ocenianie, generowanie przypadków i ulepszanie są jawnymi płatnymi działaniami. Model i instrukcje należą do datasetu: **Ocenianie** konfiguruje oceniającego, a **Ustawienia** generowanie i ulepszanie. Konsola pokazuje szacunek przed uruchomieniem płatnej pracy.
Nie ma organizacyjnego przełącznika wsparcia AI ani organizacyjnego nadpisania modelu oceniającego. Cena nie wybiera rzekomo silniejszego modelu. Automatycznie utworzony zestaw testowy zaczyna od modelu danego agenta; niezależnego oceniającego wybiera się jawnie na datasecie.
Nadal obowiązują uprawnienia osoby, poświadczenia modelu, budżety organizacji i limity runów. Każde wywołanie jest mierzone i zapisywane w Aktywności. Zadanie w kolejce zachowuje model oraz instrukcje z chwili startu; zmiany ustawień dotyczą kolejnych zadań. Historyczne wiersze pomocników pozostają dla atrybucji i egzekwowania budżetów.
## Pominięcia promocji { #promotion-overrides }
Środowisko [strzeżone przez suity ewaluacji](environments.md#guarding-a-promotion-with-evals)
przyjmuje wersję dopiero, gdy przejdą na niej. Promocja z ich pominięciem to
**pominięcie**: wymaga `promotions:override` - mają je owner i admin, a można je
dać roli własnej - od osoby zalogowanej jako ona sama, nie od klucza API ani
administratora, który się pod nią podszywa, oraz powodu o długości co najmniej
dziesięciu znaków. Trafia do [audit logu](#audit) jako
`agent.environment_promotion_overridden`, z powodem i wynikiem każdego
strażnika, widać je na wersji w historii, a właściciel każdego strażnika, który
nie przeszedł, dostaje wiadomość.
Zmiana listy strażników środowiska, przesunięcie przypięcia strażnika albo
osłabienie tego, co sprawdza strażnik, wymaga tego samego uprawnienia; każda z
tych zmian, a także ostrzejszy próg, jest zapisywana jako
`agent.environment_guards_changed`. Zmiana oceniania datasetu jest zapisywana
jako `dataset.scoring_changed`, z nazwami zmienionych części zamiast ich
treści, a utworzenie sędziego datasetu jako `dataset.judge_created`.
## Audyt { #audit }
Działania, które zmieniają dostęp albo wydają pieniądze, są zapisywane z aktorem,
a kontekst bez podmiotu **podnosi wyjątek**, zamiast pozwolić temu brakowi
podróżować dalej. Więc wpis, który nikogo nie nazywa, oznacza dokładnie dwie
rzeczy, a `action` mówi którą: przemiatanie wygasających zatwierdzeń albo komendę
operatora w powłoce wdrożenia.
Powiązanie poświadczenia z kolekcją jest jednym z tych działań. Wpisy
`sync_source` zapisują tworzenie, klonowanie, przekierowywanie i usuwanie źródła,
bo ten wiersz rozstrzyga, kto ostatecznie może czytać to, co źródło wciąga
([Przetwarzanie plików](file-processing.md#who-ends-up-able-to-read-what-a-source-ingested)).
Uprzywilejowany **odczyt masowy** też jest zapisywany. Każdy eksport do CSV
zapisuje wpis `runs.export`, `approvals.export` albo `spend.export` nazywający
okno i liczbę wierszy — to, kto zabrał całą tabelę z ekranu, jest pytaniem tanim
do odpowiedzenia teraz i niemożliwym do odtworzenia później.
Ten zapis dzieli transakcję działającego żądania, więc zawodzi na zamknięcie:
wpis, którego nie da się zapisać, wycofuje działanie, które opisuje, zamiast
pozwolić uprzywilejowanej mutacji wylądować bez audytu.
Czytanie go bramkuje `audit:read`. Obejście przez administratora aplikacji jest
dokładnie tym, co ten ślad ma rozliczać.
Ślad pilnuje też własnej spójności. Każdy wpis dołącza do łańcucha skrótów
prowadzonego per organizacja — niesie skrót swojej własnej treści z wplecionym
skrótem poprzedniego wpisu — więc edycja, zmiana kolejności lub wstawienie wpisu,
albo usunięcie jednego ze środka, rozjeżdża każdy skrót po nim.
`agenticos cmd audit-verify` przechodzi każdy łańcuch, przelicza skróty i wskazuje
pierwszy wpis, który już się nie zgadza; bez argumentu sprawdza każdy łańcuch, w
tym ten obejmujący całe wdrożenie, który trzyma działania bez organizacji — zmianę
ustawień wdrożenia, impersonację, zarządzanie użytkownikami przez administratora
aplikacji — i kończy się kodem niezerowym, jeśli którykolwiek łańcuch zawiedzie. To
**wykrywanie, nie zapobieganie** — operator z dostępem do bazy wciąż może przepisać
wiersz i przeliczyć każdy skrót po nim — więc czysty przebieg jest dowodem braku
manipulacji przez kogoś, kto nie przekuł łańcucha na nowo, a nie dowodem, że
wiersze są niezmienne.
Dwóch usunięć łańcuch nie wychwyci sam z siebie, bo pozostałe wiersze zostają
wewnętrznie spójne: odcięcia najnowszych wpisów z łańcucha oraz usunięcia całego
łańcucha organizacji. Te wychwytuje **checkpoint** — wskaźnik najwyższego stanu per
organizacja, który `record_audit` posuwa naprzód przy każdym wpisie, pod triggerem
bazy odmawiającym cofnięcia go lub usunięcia. `audit-verify` flaguje łańcuch,
którego głowa jest poniżej checkpointu, albo checkpoint, którego łańcuch zniknął.
Warto precyzyjnie powiedzieć, co ten trigger obejmuje, bo łatwo przeczytać w nim
więcej. Zamyka zwykłą ścieżkę zapisu — administratora aplikacji działającego przez
produkt oraz błąd w tym kodzie — czyli model zagrożeń, pod który ten ślad jest
pisany.
Nie jest kontrolą przeciwko komuś, kto ma poświadczenia samej bazy. Aplikacja i jej
migracje łączą się tą samą rolą, a ta rola jest właścicielem tabeli checkpointów:
może zdjąć trigger, a `TRUNCATE` opróżnia tabelę, w ogóle nie odpalając triggera
wierszowego. Superużytkownik może jedno i drugie. Domknięcie tego wymaga wskaźnika
trzymanego tam, gdzie role tej bazy nie sięgają — w magazynie tylko-do-dopisywania
albo z blokadą obiektów poza nią — co pozostaje planowanym działaniem następczym.
Dwa audytowane zapisy dla jednej organizacji nie mogą rozwidlić łańcucha: każdy
dopisuje pod blokadą per organizacja, więc szeregują się w jedną linię, zamiast
oba przedłużać tę samą głowę.
Działanie wykonane w ramach **impersonacji** nazywa obu. Gdy administrator
aplikacji działa jako inne konto, token dostępu niesie administratora w
roszczeniu `act`; każdy wpis zapisany przez to żądanie trzyma w `actor_user_id`
konto, jako które się działa, i dokłada `impersonator_user_id` — administratora
stojącego za tym. Więc „kto przeczytał rozmowę tego klienta” rozwiązuje się do
osoby nawet wtedy, gdy działanie zostało zapisane jako własne działanie klienta.
Przy zwykłym żądaniu, gdzie nikt nie działa jako ktoś inny, pole jest nullem i
nic nie jest uzupełniane wstecz: tego, czy przeszłe działanie odbyło się w ramach
impersonacji, nie da się wiedzieć po fakcie, a wymyślenie odpowiedzi byłoby
fałszywym oskarżeniem, a nie brakiem.
Sama impersonacja jest zapisywana z obu stron. `admin.user.impersonate` nazywa
konto, wiersz sesji, którym ta impersonacja jest, i moment jej wygaśnięcia;
`admin.user.impersonation_ended` nazywa tę samą sesję, gdy administrator ją
kończy, z administratorem jako aktorem. Wygaśnięcie nie zapisuje nic, bo nikt nie
działał — i tak samo nic nie zapisuje osoba wylogowująca się wszędzie, co kończy
impersonację przez `DELETE /sessions` tak, jak kończy każdą inną sesję. To, czy
ta osoba zostaje _poinformowana_, jest ustawieniem wdrożenia
`notify_impersonated_users` ([Wdrożenie](deployment.md#acting-as-another-account));
gdy jest wyłączone, co jest wartością domyślną, ten ślad jest jedynym zapisem.
Impersonacja nie może **powiązać zewnętrznej tożsamości** z kontem, jako które
działa. Potwierdzenie połączenia konta czatu i dokończenie OAuth integracji
przypinają tożsamość do tego, kim jest żądanie, a pod impersonacją jest to konto
docelowe — więc własne konto Telegram administratora albo jego grant OAuth
przypięłyby się do cudzego konta i przeżyły godzinę, do której impersonacja jest
ograniczona. Oba zostają odrzucone kodem 403 w trakcie impersonacji, bo
administrator naprawiający połączenie członka nie jest przepływem, który ta
platforma ma. Członek łączy własne konta, jako on sam.
Granicą jest dowolne poświadczenie przypięte do członka, a nie tylko tożsamość:
poza kontem czatu i grantem OAuth powyżej odrzucany jest też token bearer wpisany
w połączenie członka. Taki token nie jest własną tożsamością administratora, ale
zapieczętowany w zakresie vault członka mówi jako konto, do którego należy, dla
każdego z agentów tego członka, przeżywa godzinę, do której impersonacja jest
ograniczona, i stoi zapisany na członka. Tym, co administrator dalej robi w
imieniu członka, jest konfiguracja, która nie przechowuje żadnego sekretu —
nazwa, URL, lista dozwolonych narzędzi — oraz wyczyszczenie przechowywanego
tokena, które niczego nie zachowuje.
Współdzielone połączenie **organizacji** działa tak samo, choć z założenia wpisuje
je administrator: token bearer wpisany do niego pod impersonacją też zostaje
odrzucony, bo zapieczętowany w zakresie vault organizacji mówi jako własne konto
administratora dla każdego agenta, którego organizacja powiąże, poza godzinę, w
której impersonacja się kończy. Utworzenie połączenia organizacji już wcześniej
zapisywało administratora za nim; aktualizacja nie zapisywała niczego, więc token
rotowany na istniejące połączenie zostawia teraz ten sam ślad (#1521).
## Retencja { #retention }
Do #1420 nic nie było usuwane według harmonogramu. Rozmowy, ich pliki, wiersze
runów i manifesty, workspace'y, pamięć agentów, wgrane dokumenty i wpisy
audytowe żyły, dopóki ktoś nie usunął organizacji. To problem ochrony danych w
jedną stronę i — dla audytu — problem zgodności w drugą: HIPAA §164.316(b)(2)
chce trzymać zapis audytowy sześć lat, a RODO chce minimalizować całą resztę.
Dlatego okres jest **na klasę** i oba obowiązki dostają swoje ustawienie.
Ustawisz to w **Organizacje → workspace → Członkowie → Retencja**, za bramką
`org:settings`. Sweep chodzi raz dziennie i **usuwa twardo**: polityka, która
zostawiałaby wiersze, nie byłaby polityką.
Dane [Virtual Tables](virtual-tables.md#limits-and-retention) wchodzą do tego samego sweepa,
z okresami całego wdrożenia, których nie ustawia żadna organizacja: receipts idempotencji
po 24 godzinach, wysłane wiersze outbox po 3 dniach i history rekordu po 365 dniach, co
usuwa też history usuniętego rekordu. Wpis, który zapisuje, nazywa `table_receipts`,
`table_outbox` lub `table_history` i liczbę.
Trzy własne liczby wdrożenia — `retention_defaults`, `retention_max_days` i
`audit_retention_floor_days` — to pola ustawień wdrożenia, zapisywane przez app
admina przez `PATCH /admin/deployment-settings` jak każde inne ustawienie tam.
Formularza w konsoli dla nich jeszcze nie ma; strona organizacji jest miejscem,
gdzie ustawia się okresy per tenant.
### Klasy { #the-classes }
| Klasa | Co odchodzi razem z nią | Liczone od |
| --- | --- | --- |
| Rozmowy | Wiadomości, wywołania narzędzi i pliki; zablokowana partia pomija rozmowy z niezakończonymi runami, a trwałe zadanie usunięcia plików zatwierdza się razem z usunięciem wierszy | Ostatniej aktywności wątku, więc ten, do którego ktoś wraca, nie jest stary |
| Runy | Wiersz runu, jego manifest i jego zatwierdzenia narzędzi | Startu runu |
| Workspace'y | Zapis platformy o plikach agenta. Dla backendu `state` wiersz _jest_ magazynem; pliki backendu sandboxowego sprząta własny TTL sandboxa. Pliki projektu zostają, dopóki jest projekt | Ostatniego użycia |
| Aplikacje | [Opublikowana strona](artifacts.md), wszystkie wersje, granty i publiczny link; usunięcie plików trafia do kolejki w tej samej transakcji | Ostatniej publikacji, więc raport, który harmonogram publikuje ponownie, nie jest stary |
| Pamięć | Pliki pamięci agenta | Ostatniego zapisu, bo notatka jest pisana raz, a czytana miesiącami |
| Wgrane dokumenty | Wiersz, jego wektory i wgrany plik | Momentu wgrania |
| Audyt | Wpisy na ścieżce audytowej tej organizacji | Momentu zapisania wpisu |
**Dokument wciąż ingestowany też nie jest zamiatany.** Trzyma go worker, a
zabranie jego wiersza i wgranego oryginału spod ingestii, która potem zapisze
wektory, zostawia przeszukiwalną treść, której żaden późniejszy sweep nie nazwie.
Wycofywane są tylko wiersze zakończone i błędne.
**Dokument zsynchronizowany przez konektor nie jest zamiatany.** Jego czas życia
należy do źródła, które go tam umieściło: usunięcie go tutaj skasowałoby wiersz,
który kolejny sync `new_only` odtworzy z tego samego niezmienionego pliku,
paląc spend na embeddingi na nic. Zamiatane jest to, co ktoś wgrał, a czego
czasu życia nic innego nie posiada.
### Który numer wygrywa { #which-number-wins }
Trzy warstwy, rozstrzygane w `app/core/retention.py` i nigdzie indziej:
1. **Domyślna wartość wdrożenia**, dla organizacji, która nic nie powiedziała.
Brak znaczy na zawsze — platforma, która po aktualizacji zaczęłaby usuwać
historię istniejącej instalacji, byłaby platformą, której nikt nie zaufałby
przy kolejnej aktualizacji.
2. **Własny okres organizacji**, krótszy albo dłuższy.
3. **Sufit wdrożenia**: nic z tej klasy nie żyje tu dłużej niż N dni, i
organizacja nie może go podnieść.
Audyt działa odwrotnie. Wdrożenie ustawia **podłogę** — najkrócej, jak wpis może
żyć, sześć lat, dopóki operator tego nie zmieni — a organizacja może ją wydłużyć
i nigdy skrócić. **Audytu nic jeszcze nie zamiata**: okres się rozstrzyga i jest
raportowany, a organizacja jest trzymana przy podłodze, ale żaden wpis nie jest
usuwany, bo łańcuch haszy i jego append-only checkpoint stoją na tym, że wpisy
nigdzie nie idą, a gołe usunięcie sprawia, że `audit-verify` raportuje wycofanie
jako manipulację. Wycofanie łańcucha z jednoczesnym udowodnieniem, że reszta jest
nienaruszona, to mechanizm, którego tu jeszcze nie ma — więc podłoga obowiązuje, a
sweep zostawia audyt w spokoju.
Sufit dalej obowiązuje audyt tam, gdzie oba nie są sprzeczne. Tam, gdzie są,
wygrywa podłoga, a sprzeczność jest raportowana. Ścieżka, którą administrator może skrócić, nie jest ścieżką,
więc okres poniżej podłogi jest **odrzucany**, a nie po cichu do niej podnoszony:
trzymanie wpisów dłużej, niż mówi liczba na ekranie, to własny rodzaj błędu.
Podłoga powyżej sufitu to sprzeczność i jest raportowana, a nie rozstrzygana.
Strona ustawień nazywa klasę; administrator decyduje, która obowiązuje. Wybranie
jednej zostawiłoby wdrożenie zachowujące się inaczej niż jego własna strona
ustawień.
### Co przeżywa czystkę { #what-survives-a-purge }
**Rachunek.** Spend miesiąca to suma po `agent_runs`, więc twarde usunięcie ich
zrzuciłoby miesięczny wynik organizacji do zera wraz z mijającym oknem, a limit
mierzony na tej liczbie przestałby obowiązywać do końca miesiąca. Sweep czyta,
ile kosztowały wygasające runy, zanim je usunie, i trzyma sumę na organizację na
miesiąc w `purged_run_spend` — liczbę i licznik, bez agenta, bez modelu, bez
czyjegokolwiek nazwiska. `app/services/spend.py` dodaje to do sumy na żywo i to
jedyne miejsce, w którym te dwie rzeczy się spotykają.
**Ślad samego sweepa.** Jeden wpis na organizację na sweep, nazywający klasę i
licznik, i nic więcej: wpis audytowy cytujący to, co usunął, trzymałby treść
dłużej niż retencja, która ją usunęła.
### Kiedy się nie uda { #when-it-fails }
Per klasa, nie per sweep. Niedostępny vector store nie może zatrzymać usuwania
rozmów, więc każda klasa jest próbowana, jej błąd logowany i nazwany we wpisie
audytowym, a sweep idzie dalej. Kolejny przebieg ponawia, bo partia, która nic
nie usunęła, po prostu wraca.
Usuwanie idzie partiami po 500, do czterdziestu przebiegów na klasę na sweep.
Organizacja, która po dwóch latach ustawia dziewięćdziesiąt dni, odrabia zaległość
przez kilka dni, a nie w jednym sweepie trzymającym locka przez godzinę z całą
resztą cyklicznych flowów w kolejce za nim.
Każda klasa ma własny savepoint: błąd SQL wycofuje jej zmiany i zadania
usunięcia plików, nie blokując kolejnych klas. Trwałe zadania usuwają pliki po
zatwierdzeniu usunięcia wierszy; błędy magazynu i ponowienia są widoczne na stronie
Background jobs. Wycofanie usunięcia pozostawia pliki nienaruszone.
### Czego to nie sięga { #what-this-does-not-reach }
- **Backupów.** Czystka usuwa wiersze i pliki z żywego wdrożenia. To, co trzyma
twój harmonogram backupów, wygaszasz sam, a restore przywróci to, co było w
snapshotcie.
- **Tego, co wysłałeś gdzie indziej.** Logi wysłane do zewnętrznego kolektora,
trace'y w hostowanym projekcie observability i cokolwiek trzyma dostawca
modelu rządzą się własnymi ustawieniami tych usług, nie tym.
- **Kontroli per wpis pamięci**, czyli [#1594](https://github.com/vstorm-co/agenticos/issues/1594),
ani usunięcia jednej osoby w całej organizacji, czyli
[#1421](https://github.com/vstorm-co/agenticos/issues/1421). Ta strona jest o
wieku; tamte są o podmiocie.
## Czego nic z tego nie obejmuje { #what-none-of-this-covers }
Warto to powiedzieć, bo strona o governance sugeruje coś innego:
- **Brak ograniczania tempa per agent.** Ograniczenia, które istnieją, siedzą na
powierzchniach publicznych — widget embeda mierzy wiadomości per odwiedzający,
bot kanału mierzy każdego nadawcę ([Kanały](channels.md)) — a nie na wdrożeniu:
nie ma budżetu żądań per agent, a własne route'y konsoli nie są mierzone.
- **Brak filtrowania treści.** To, co mówi agent, jest tym, co powiedział model.
- **Brak kontroli ruchu wychodzącego dla MCP.** Do powiązanego serwera sięga się
po sieci z workera; ograniczanie tego, dokąd może on sięgnąć, jest konfiguracją
wdrożenia, a nie ustawieniem tutaj.
## Podsumowanie { #recap }
- **Dwa limity, i nie da się ich zwinąć w jeden.** Budżet ogranicza pieniądze;
limit kroków ogranicza pętlę, która jest tania w przeliczeniu na wywołanie i
nigdy się nie kończy.
- Budżet jest sprawdzany **przed** każdym żądaniem do modelu, a nieudany run i
tak zapisuje, ile wydał.
- Limit jest pułapem na **zacommitowane** wydatki. Równoległe runy nie widzą się
nawzajem, więc ścisły limit oznacza jedną kolejkę.
- **Zatwierdzenie jest rozstrzygane raz**, a druga decyzja na rozstrzygniętym
zatwierdzeniu zostaje odrzucona.
- Czytanie runa jest **autoryzowane, nie własnościowe** — `runs:view` czyta run
współpracownika, a run innego tenanta czyta się jako nieobecny.
- **Cisza coś znaczy.** Alert, który nie przyszedł, oznacza, że dana rzecz się
nie wydarzyła, co jest prawdą tylko dlatego, że nic tutaj nie działa w trybie
„w miarę możliwości”.
- **Promocja z pominięciem evali to pominięcie** - z powodem, własnym
uprawnieniem i zapisem na wersji.
## Materiały { #reference }
- [Koncepcje](concepts.md) — spec, wersja, ekspozycja, run.
- [Uprawnienia](permissions.md) — kto może cokolwiek z tego ustawić.
- [Konfiguracja](configuration.md) — ustawienia na poziomie wdrożenia.
## Automatyczne wstrzymanie po kolejnych błędach { #routine-health }
Rutyny i obserwacje domyślnie wstrzymują się po **trzech kolejnych nieudanych sprawdzeniach**. W formularzu rutyny ustaw limit od 1 do 20 lub wyłącz ten limit. Liczą się błędy dostawcy i wykonania, nieprawidłowe odczyty obserwacji oraz niedostępne źródła. Oczekiwanie na zgodę, komputer offline i błędy dostarczenia nie zwiększają licznika; puste, ale poprawnie zweryfikowane sprawdzenie zeruje serię. Inne blokady uprawnień i budżetu obowiązują także po wyłączeniu tego limitu.
Każda aktywacja ma jeden trwały zapis stanu. Ponowienia nie zwiększają licznika, a opóźnione zakończenia są oceniane według kolejności aktywacji. Automatyczne wstrzymanie zatrzymuje przyszłe aktywacje; trwające żądanie do modelu może się zakończyć.
Istniejąca sprawa właściciela zapisuje wstrzymanie, a powiadomienie korzysta ze wspólnych preferencji i wybranych tras. Niepotwierdzona sprawa otrzymuje jedno zawiadomienie o wstrzymaniu, zastępujące wcześniejszy wpis w Inboxie. Wznowienie lub zmiana polityki unieważnia oczekujące zawiadomienia o wstrzymaniu. Potwierdzenie sprawy nie zeruje stanu wykonania ani nie wznawia rutyny. Po naprawieniu problemu jawnie wznów rutynę, aby rozpocząć nowe liczenie. Zmiana limitu błędów również rozpoczyna nowe liczenie.
Lista pokazuje automatyczne wstrzymanie i liczbę błędów. Metadane zachowują najwyżej 50 obserwacji na rutynę, bez treści źródeł i błędów dostawców. Trafiają do ograniczonego rozmiarem eksportu danych właściciela i są usuwane kaskadowo z właścicielem lub rutyną. Migracja `0275_routine_health.py` włącza domyślny limit także dla istniejących rutyn: aktualizuj API i workery razem. Wycofanie migracji usuwa metadane i politykę, zachowując wstrzymanie rutyny.
## Zatwierdzone źródła rutyn { #routine-sources }
Podczas tworzenia rutyna utrwala opublikowaną konfigurację źródeł, konkretne konta MCP, generacje zgody na uwierzytelnienie i znane nazwy narzędzi. Przegląd obejmuje przypiętych delegatów, kolekcje wiedzy, umiejętności, kontekst i konfigurację możliwości; zdalne rejestracje A2A zachowują powiązanie endpointu, karty i poświadczenia. Sama zmiana instrukcji lub ustawień modelu może podążać za publikacją. Dodanie źródła, zmiana konta lub ponowne udzielenie zgody na konto wymaga przeglądu panelu źródeł w oknie zgód rutyny przez jej właściciela. Ten przegląd nie znosi zatwierdzeń działań ani nie przyznaje dostępu do zasobów. Nadal obowiązują aktualne uprawnienia.
Zakolejkowane uruchomienia, oczekiwanie na komputer i zaparkowane przebiegi zachowują identyfikator pierwotnego przeglądu. Późniejszy przegląd nie rozszerza uprawnień starej pracy. Polling skrzynki sprawdza konto wejściowe przed odczytem, a zdarzenia zachowują zgodę obowiązującą przed nim. Wywołania MCP używają zatwierdzonych aliasów i wykluczają niezatwierdzone narzędzia; niedostępne źródła powodują jawną odmowę. Konieczność przeglądu przed wykonaniem pojawia się w istniejącej sprawie właściciela i nie jest błędem modelu.
Migracja `0276_routine_sources.py` nie nadaje zgód istniejącym rutynom. Opróżnij starą kolejkę workerów, zaktualizuj API i workery razem, a przed uruchomieniem jawnie przejrzyj stare rutyny. Zgodę może zastąpić tylko właściciel w konsoli; nie mogą tego zrobić klucze API, podszywanie się ani agenci działający bez nadzoru.
Uruchomienia rutyny z budżetem współdzielą klucz współbieżności kolejki, aby równoczesne zdarzenia nie dopuszczały wielu wywołań modelu na podstawie tego samego zakończonego wydatku. Ekstrakcja pamięci również jest wykonywana kolejno dla roli konserwacji organizacji. Każde zadanie ponownie sprawdza budżet; jedno dopuszczone wywołanie nadal może zakończyć się ponad limitem.
Alerty o wstrzymanych zadaniach całego wdrożenia zachowują zwykły stan odczytu i odrzucenia powiadomienia; nie są przedstawiane jako sprawy organizacji.
### Rutyny, rozmowy robocze i Inbox { #routines-working-conversations-and-inbox }
**Routines** pokazuje wspólnie zadania czasowe, zdarzeniowe i gotowe warianty
monitorowania w trybie odczytu. Trigger określa, _kiedy_ uruchomić rutynę;
wariant monitorujący ogranicza, _co_ może zrobić. Jedna odpowiedzialność powinna
mieć jedną rutynę, bez dublowania obserwacji i zadania czasowego dla tej samej poczty.
Wybierz **Po każdym zakończonym wykonaniu** (`result_mode: report`, dotychczasowa
wartość domyślna) dla raportów albo **Tylko gdy coś wymaga uwagi** (`result_mode:
inbox`) dla cichych sprawdzeń. Agent zwraca istotne elementy źródła ze stałymi
identyfikatorami elementu i wersji, kontekstem i pełnym zamówionym draftem.
Pusty wynik nie tworzy rozmowy ani powiadomienia o zakończeniu. Każdy element ma
jedną prywatną rozmowę i jedną sprawę w Inbox. Powtórne odczyty tej samej wersji
są trwale deduplikowane, także po rozwiązaniu sprawy. Nowa wersja aktualizuje tę
samą rozmowę i otwiera sprawę ponownie; wyciszony element pozostaje wyciszony.
Z Inbox otwórz rozmowę, aby poprawić draft lub zlecić następny krok. Czytanie i
edycja draftu nie wysyłają go i nie kończą sprawy. Nadal obowiązują uprawnienia i
zatwierdzanie narzędzi. Narzędzia platformowego MCP `list_inbox_cases` oraz
`settle_inbox_case` pozwalają agentowi znaleźć sprawę i rozwiązać ją po udanym
wykonaniu zleconej pracy albo jawnym potwierdzeniu zakończenia. Inbox i czat
współdzielą stan; zamknięcie anuluje oczekujące powiadomienia. Monitorowanie w
trybie odczytu może przygotować tekst draftu w rozmowie, ale nie zapisze go ani
nie wyśle przez usługę źródłową.
Nocne sugestie pozostają opcjonalną, niezależną analizą ostatnich próśb i pamięci,
a nie kolejnym skanowaniem poczty. Monitorowanie używa modelu wybranego agenta i
nie wymaga skonfigurowania modelu sugestii. Organizacja nadal kontroluje jego
dostępność, częstotliwość i współdzielony budżet pracy w tle na osobę. Zwykłe
rutyny zachowują limity agenta, organizacji i opcjonalny własny limit. Dni i
godzina określają również porę zestawienia niepilnych znalezisk.
Poranne zestawienie wylicza zapisane wcześniej niepilne znaleziska bez kolejnego
wywołania modelu i skanowania źródeł. To dostarczenie powiadomienia, a nie druga
rutyna. Narzędzia MCP do Inbox działają w zalogowanej sesji i przez zaufane
poświadczenia prywatnej rozmowy; te ostatnie obejmują wyłącznie sprawy tej rozmowy.
Zwykłe klucze API, uruchomienia w tle i zastępcza tożsamość publikującego nie mogą
czytać ani zamykać prywatnych spraw.