--- source_sha: "49d132bd9406" --- # Koncepcje { #concepts } Pięć rzeczowników. Wszystko w produkcie jest z nich zbudowane, a większość nieporozumień wokół niego bierze się z brania jednego za drugi. ```mermaid graph LR S[Spec] -->|publish freezes| V[Version] V -->|an exposure admits a caller| E[Exposure] V -->|a trigger fires on a schedule or an event| T[Trigger] E -->|one execution| R[Run] T -->|one execution| R R -->|records| C[cost, tokens, version] ``` Jeśli nie przeczytasz nic więcej na tej stronie, przeczytaj pierwsze dwa. ## Spec { #spec } **Agent jako dane.** Instrukcje, profil modelu, powiązania capability, odwołania do kolekcji i skilli, budżet oraz to, kogo zawiadomić, gdy coś się stanie. Jest zdefiniowany w [`app/agents/spec.py`](reference/spec.md) i walidowany przez Pydantic. Agent ma dokładnie jednego speca _roboczego_ (draft), którego Builder edytuje i zapisuje na bieżąco. Spec trzyma się dwóch reguł i to właśnie one czynią go użytecznym. !!! abstract "Odwołania, nigdy wartości" Spec nazywa profil modelu, kolekcję, id narzędzia. Nigdy nie zaszywa w sobie ciągu z nazwą modelu, connection stringa ani sekretu. To właśnie dlatego bezpiecznie jest wrzucić go do własnego repozytorium i dlatego organizacja może zrotować klucz, nie dotykając ani jednego agenta. !!! abstract "Rozwój przez dodawanie" Nowe pola dostają wartości domyślne, więc agent opublikowany dziś nadal wczytuje się po aktualizacji. Usunięcie albo zmiana nazwy pola to migracja, a nie edycja. !!! info "Odpowiedź może być danymi" Domyślnie agent odpowiada tekstem. Ustaw **Answer format** na dane strukturalne albo `output_schema` w specu, a odpowie obiektem z nazwanymi polami: model dostaje prośbę o ten kształt, a odpowiedź, która go łamie, wraca do poprawki, zanim run się skończy. API zwraca obiekt jako `structured`, czat i kanały pokazują go jako blok JSON, a krok `agent.run` przekazuje go dalej. !!! info "Szablon to spec, który ktoś już napisał" [Szablony agentów](first-agent.md#3-build-the-agent) dostarczane z platformą to spece zawierające wszystko, co folder w obrazie może wiedzieć: instrukcje, capability i skille do zainstalowania. Nie nazywają żadnego modelu ani żadnej kolekcji, bo to są UUID-y, których nikt spoza twojego wdrożenia nie ma - i dlatego zainstalowany szablon jest draftem. !!! note "Categories i tagi są obok speca, a nie w nim" Wiersz agenta niesie też edytowalne **categories** i **tagi** — lokalne dla organizacji etykiety do wyszukiwania i filtrowania agentów. Podobnie jak avatar, nie zmieniają ani tego, co agent uruchamia, ani znaczenia eksportowanego YAML-a, więc otagowanie na nowo nie wymaga publikacji ani nowej wersji; to metadane wiersza, a nie pole speca. Pozostają poza przenośnym artefaktem, i o to właśnie chodzi w trzymaniu ich poza specem: eksport speca nie emituje żadnych categories ani tagów, import zostawia istniejące categories i tagi celu bez zmian (plik żadnych nie niesie, więc ich nie czyści ani nie nadpisuje), a jedynie klon startuje bez tagów. ## Wersja { #version } **Zamrożony spec.** Publikacja kopiuje draft do wersji. Środowiska decydują, która wersja obsługuje daną powierzchnię, a runy zapisują, która została wykonana. Dlatego _co ten agent zrobił w zeszły wtorek_ pozostaje pytaniem, na które da się odpowiedzieć po kilkunastu edycjach. Dlatego też rollback publikuje **nową** wersję skopiowaną ze starej, zamiast kasować historię — oś czasu pokazuje, że rollback się wydarzył, zamiast udawać, że zła wersja nigdy nie istniała. Zapisy szkicu, publikacje, rollbacki i zmiany środowisk wykonują się kolejno dla jednego agenta. Zapis czekający na publikację pozostaje później szkicem i nie zostaje przez nią nadpisany. Równoczesne publikacje otrzymują odrębne numery wersji, a promocja domyślnego środowiska utrzymuje zgodność wersji obsługującej agenta. Nowe runy mogą startować podczas walidacji publikacji. ### Środowiska { #environments } **Środowiska** to nazwane wskaźniki na wersje, a każde z nich mówi, czy publikacja może nim ruszyć. !!! important "Publikacja bije wersję. Postawienie jej gdzieś to osobna decyzja" Publikacja przestawiała kiedyś domyślne środowisko, jakiekolwiek by nie było, więc poprawienie promptu zmieniało w tym samym kliknięciu to, czym odpowiadał działający bot, i nic na ekranie o tym nie mówiło. Środowisko zatem albo: - **czeka, aż coś zostanie na nie wypromowane** — tak powinno działać `production`, kiedy obok jest już `dev` albo `staging`; albo - **podąża za każdą publikacją** — czego zwykle oczekuje się od `dev`, w którym ktoś iteruje, i tak działa `production`, dopóki jest jedynym środowiskiem. Dwie konsekwencje warte powiedzenia: 1. **Pierwsza** publikacja tworzy `production` na wersji, którą właśnie wybiła, bo agent bez środowiska nie ma się w ogóle gdzie uruchomić, i to środowisko podąża za publikacjami, dopóki ktoś go nie przypnie. Środowisko dodane później czeka na promocję, chyba że ustawi się inaczej. 2. **Rollback ląduje tak samo** jak publikacja — _jest_ publikacją starszego speca. Postawienie starej wersji z powrotem przed ludźmi to jedno kliknięcie w jej wierszu historii (promote), a nie efekt uboczny przywrócenia draftu. Bot kanału powiązany ze środowiskiem serwuje jego wersję. `Agent.current_version_id` to wskaźnik środowiska domyślnego i to przez niego rozwiązuje się powierzchnia, która nie nazywa żadnego środowiska — więc przesuwa się, gdy przesuwa się to środowisko. !!! tip "Wersje nie muszą wchodzić na żywo wszystkie naraz" [Środowisko](environments.md) to nazwa przypięta do wersji, więc `staging` może serwować wersję 7, podczas gdy `production` zostaje na 6. Publikacja bije wersję; postawienie jej gdzieś to osobna decyzja. ## Ekspozycja { #exposure } **Gdzie agent jest osiągalny i dla kogo.** Czat webowy, klucz API HTTP, publiczny link, bot Slacka lub Telegrama, osadzony widget. !!! success "Każda powierzchnia idzie przez jeden runner" Budżety, zatwierdzenia, ślad audytowy i sprawdzenia uprawnień są identyczne niezależnie od tego, czy run przyszedł z okna czatu, czy ze wzmianki na Slacku, bo istnieje dokładnie jedna ścieżka kodu, która wykonuje agenta. Kanały mają dwie reguły warte osobnego powiedzenia: - **Bot odpowiada jako jeden agent.** To pojedyncza tożsamość na czacie, więc powiązanie drugiego agenta z jednym botem jest odrzucane, a `@slug` to alias stojącego za nim agenta, a nie sposób na wybieranie spośród kilku. - **Run wykonuje się jako nadawca**, nigdy jako bot. Niepowiązana tożsamość czatowa jest odrzucana, zamiast uruchamiać run bez roli, bo run, za który nikogo nie da się rozliczyć, jest gorszy niż run, który się nie odbył. ## Trigger { #trigger } **Kiedy agent uruchamia się, gdy nikt nie siedzi przy klawiaturze.** Podobnie jak ekspozycja, trigger jest stanem operacyjnym obok agenta, a nie częścią speca. Dodajesz go, wstrzymujesz i usuwasz bez wybijania wersji i nie jest eksportowany do twojego YAML-a — niesie rzeczy, których spec nieść nie może, takie jak podmiot i moment ostatniego odpalenia. ### Uruchamia się jako człowiek { #it-runs-as-a-person } Run z triggera wykonuje się **jako członek, który utworzył trigger**, rozwiązywany na nowo przy każdym odpaleniu, nigdy jako wymyślony użytkownik serwisowy. To ta sama reguła co przy wzmiance na kanale i z tego samego powodu. Kiedy ten członek nie może już uruchamiać agenta — odszedł z organizacji albo odebrano mu grant na niego — trigger **wyłącza się sam i zapisuje dlaczego**, zamiast w nieskończoność ponawiać odmowę. ### To nie jest rozmowa tej osoby { #it-is-not-that-persons-conversation } Twórca pożycza odpaleniu swoje **uprawnienia**: rolę i granty. Nie pożycza mu swojej **tożsamości**. Nikt nie słucha runa bez nadzoru, więc nie odpowiada on żadnej zidentyfikowanej osobie, tak samo jak gość na osadzonym widgecie. Te magazyny prowadzone dla konkretnej osoby są dla niego zamknięte: - [Pamięć](reference/capabilities.md#memory-files): każde narzędzie odpowiada, że rozmowa nie ma pamięci. - [Poprzednie rozmowy](reference/capabilities.md#conversation-search): oba narzędzia odmawiają. - [Osobiste powiązanie MCP](mcp.md#whose-account-a-binding-speaks-through): własnego Notion albo skrzynki twórcy nie ma, a agent dostaje informację dlaczego. - [Aplikacje](reference/capabilities.md#artifacts): `publish_artifact` nadal publikuje, a strona jest prywatna dla twórcy, ale `read_artifact` odmawia, więc odpalenie publikuje całą stronę od nowa zamiast ją edytować. Tak jest celowo. Magazyn prowadzony dla osoby jest odczytywany tej osobie, a odpalenie, którego nikt nie rozpoczął, nie może do niego pisać w jej imieniu. [Sandbox](sandbox.md) z `session_scope: user` trzyma się tej samej reguły: odpalenie nie ma tam własnego workspace'u. ### Chyba że twórca pozwoli mu działać jako on { #unless-its-creator-lets-it-run-as-them } **Uruchamiaj jako ja**, ustawienie triggera, to jedyny wyjątek, i włączyć je może tylko twórca, dla siebie. Odpalenie sięga wtedy do jego pamięci i jego własnych połączeń MCP tak jak jego własny czat — poranne _powiedz mi, co ważnego w mojej skrzynce_, którego domyślne ustawienie nie potrafi — na tych warunkach: - **Harmonogram czyta i zapisuje pamięć; zdarzenie tylko ją czyta.** Prompt zdarzenia niesie maila albo treść webhooka, której nikt nie sprawdził, a notatka zapisana na jej podstawie byłaby czytana w każdym kolejnym czacie jako własna notatka twórcy. - **Poprzednie rozmowy pozostają zamknięte** w obu przypadkach. - **Edycja kogoś innego to wyłącza.** Zmiana wiadomości, środowiska, filtra albo kanału Teams triggera, którego nie utworzyłeś, wyłącza ustawienie, więc nikt nie przepisze tego, co działa jako ty, ani nie wyśle odpowiedzi gdzie indziej. Administrator działający jako ty przez impersonację ani skrypt z twoim kluczem API nie mogą go włączyć. - **Tak samo nowa wersja opublikowana nie przez ciebie osobiście.** Publikacja, promocja albo cofnięcie przez kogokolwiek innego to wyłącza - także zrobione twoim kluczem API albo przez administratora w impersonacji. - **Zapis to pokazuje.** Run wskazuje trigger, który go odpalił, a każdy wpis audytu zapisany przez odpalenie niesie `via_trigger` z członkiem, jako który działało. - **Może pracować na własnym komputerze twórcy.** Z włączonym Run as me twórca może wskazać triggerowi folder na swoim komputerze, w polu **Pracuj na (Work on)**; każde odpalenie pracuje wtedy tam przez [aplikację desktopową](desktop.md#work-on-this-computer). Wyłączenie Run as me to czyści, a odpalenie, które nie działa jako twórca, nie dostaje komputera. Ta reguła decyduje o tym, jak projektujesz agenta uruchamianego przez trigger: | Run bez nadzoru potrzebuje | Użyj | | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | Stałych faktów albo preferencji | Własnego promptu triggera, [pliku kontekstowego](context.md) podpiętego do agenta albo pamięci twórcy z **Uruchamiaj jako ja** | | Stanu zachowanego od jednego odpalenia do następnego | [Sandboksa](sandbox.md) z `session_scope: conversation`: każde odpalenie jednego triggera dopisuje się do tej samej rozmowy z logiem runów | | Skrzynki pocztowej albo innej zewnętrznej usługi | Połączenia powiązanego kontem organizacji albo własnego połączenia twórcy z **Uruchamiaj jako ja** | [Obserwowanie stron pod kątem zmian](howto/competitor-monitoring.md) i [osobisty asystent](howto/personal-assistant.md) trafiają na tę regułę i pokazują obejście. ### Cała reszta to zwykły run { #everything-else-is-an-ordinary-run } Bo idzie przez ten sam runner: budżet jest egzekwowany tak samo, zatwierdzenie parkuje go tak samo, ślad audytowy nazywa go tak samo. Jest stemplowany powierzchnią `schedule`, więc pytanie _jak ten agent jest używany_ potrafi odróżnić run bez nadzoru od runa człowieka. Każde odpalenie to osobny run w **Aktywności** (Activity), a jego odpowiedzi zbierają się w jednej rozmowie będącej dziennikiem runów, którą trigger otwiera raz — zawczasu, w momencie utworzenia triggera, więc jest klikalną pozycją, zanim jeszcze kiedykolwiek się odpali. Trigger może też publikować każdą odpowiedź na kanale albo w czacie grupowym Teams, w którym jest bot jego organizacji ([Microsoft Teams](channels.md#microsoft-teams)). ### Dwa sposoby odpalania { #two-ways-to-fire } === "Harmonogram" Odpala się z zegara, w jednej z dwóch postaci: - **Interwał** — „co N sekund”, najdrobniej co minutę, bo heartbeat zgarnia należne raz na minutę. - Wyrażenie **cron** obliczane w UTC — `0 9 * * *` dla 09:00 każdego dnia albo dowolny pięciopolowy crontab. Sześciopolowa postać z kolumną sekund jest odrzucana: sekundy to kadencja, której heartbeat chodzący raz na minutę nie jest w stanie dotrzymać. Serwis liczy następne odpalenie dla obu tak samo, a run, który przeżyje swój własny interwał, kończy się przed kolejnym odpaleniem, zamiast nawarstwiać się sam na sobie. === "Zdarzenie" Odpala się na przyjściu czegoś: issue z GitHuba, przychodzącego maila albo uniwersalnego źródła API — wszystkiego, co potrafi wysłać POST-em podpisany JSON, więc krok kodu w Zapierze czy Make, albo mały skrypt, pokrywa cokolwiek innego, na czym chcesz odpalać. Przychodzi jako podpisany webhook, który platforma weryfikuje względem sekretu przypisanego do triggera i [zapieczętowanego w vault](secrets.md), dopasowuje do opcjonalnego filtra danego źródła, a potem agent uruchamia się z payloadem doklejonym do swojego promptu. Zdarzenie **nie ma następnego odpalenia** — nic nie jest należne, dopóki nie wyląduje dostarczenie — więc heartbeat nigdy go nie widzi. Dodanie źródła to wartość w jednym enumie i gałąź w jednym module. Nie zmienia niczego w wierszu. ### Uruchom teraz { #run-now } Każdy trigger można też **uruchomić teraz**: jedno dodatkowe odpalenie na żądanie, które nie rusza jego kadencji. Jest ono _przyjmowane_, a nie oczekiwane. Żądanie odpowiada, gdy tylko odpalenie zostanie zakolejkowane dla workera jako job — te same trwałe drzwi, przez które przechodzi odpalenie z harmonogramu albo z dostarczenia — a run pojawia się w rozmowie-dzienniku runów triggera na bieżąco. Dzięki temu agent, który pracuje minutami, nie trzyma otwartego żądania przeglądarki, aż proxy da za wygraną, a przyjęte odpalenie przeżywa proces API, który je przyjął. Każdy harmonogram i każde zdarzenie w organizacji są wypisane razem, w poprzek jej agentów, przefiltrowane do tych, które wolno ci uruchamiać — to samo `agents:run` na poziomie zasobu, które bramkuje ich tworzenie. !!! tip "W produkcie nazywają się Routines" Obie rodziny razem to **Routines**: jedna parasolowa nazwa, której używają nawigacja, boczny pasek czatu, onboarding i polskie tłumaczenie (*Rutyny*), więc człowiek spotyka jedno słowo, gdziekolwiek ta funkcja wychodzi na wierzch. Lista obejmująca całą organizację to `/routines`, a dashboard ma **kafelek Routines** — dodawalną kartę pokazującą kadencję każdej rutyny, następne odpalenie oraz wynik, koszt i ocenę ostatniego runa. Nienadzorowana połowa organizacji jest widoczna tym samym rzutem oka co ta nadzorowana. [Triggery i harmonogramy](triggers.md) to cała historia. ## Run { #run } **Jedno wykonanie.** Ma podmiot, wersję, powierzchnię, status, liczniki tokenów i koszt. **Run, który się nie powiedzie, i tak zapisuje, ile wydał.** To, jak się _skończył_, jest osobnym statusem, a nie `failed`, bo operator filtrujący pod kątem problemów nie powinien przedzierać się przez poprawnie działającą platformę: | Status | Run | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `failed` | zepsuł się | | `budget_exceeded` | dobił do limitu — limit wydatków robiący swoje | | `guardrail_blocked` | został odrzucony przez [guardrail](reference/capabilities.md#guardrails) | | `loop_stopped` | powtarzał te same wywołania narzędzi po ostrzeżeniu i został [zatrzymany](governance.md#step-limits) | | `cancelled` | został zatrzymany: przyciskiem stop w kompozytorze, przez zerwany socket, przez delegację anulowaną z góry. Na każdej powierzchni, nie tylko na strumieniowej | | `awaiting_approval` | zaparkował na zatwierdzeniu i jest **wznawialny** — jego historia wiadomości jest zapisana, więc decyzja dotyczy rozmowy, do której należy | Run, który parkuje _wewnątrz delegacji_, zapisuje po jednym poziomie na agenta, każdy z własną rozmową, więc zatwierdzenie kontynuuje delegata, który się zatrzymał, zamiast zaczynać jego pracę od nowa. !!! note "Run może zawierać inny run" Delegacja dostaje własny wiersz w `agent_runs` niosący `parent_run_id`, więc pytanie *ile kosztował w tym miesiącu ten researcher* ma odpowiedź — przy czym oba dzielą jedną księgę wydatków. Statusu `delegated` celowo nie ma. `parent_run_id` odpowiada na „jak ten run się zaczął”; status odpowiada na „jak się skończył”. Dwa pytania. ### Run i jego transkrypt { #a-run-and-its-transcript } Run mówi, ile kosztował. `messages.run_id` mówi, co _zrobił_. Każda tura, którą run wyprodukował, niesie id runa, więc „kroki tego runa” to jedno zapytanie, a nie zgadywanie — i właśnie to czyta zejście z historii runów przez `GET /runs/{id}/transcript`. Ten odczyt jest **autoryzowany, a nie własnościowy**. Kolega z `runs:view` czyta run uruchomiony przez kogoś innego, bo run należy do organizacji, a nie do tego, kto go zaczął. Run innego tenanta czyta się jako nieobecny — to samo 404, którym odpowiada nieznane id — a run, który wykonał się bez rozmowy, mówi o tym pustym `conversation_id`, a nie pustą listą. To osobna trasa, a nie filtr na endpointcie rozmowy, więc sam endpoint rozmowy pozostaje ograniczony do właściciela. Zobacz [Governance](governance.md#what-run-history-shows). `?scope=conversation` poszerza ten sam odczyt na cały wątek, w którym siedzi run, na potrzeby widoku szczegółów pokazującego run w kontekście i przewijającego do niego. To wygoda, a nie sięgnięcie dalej: każda tura, którą run zapisuje, niesie swój `run_id` — łącznie z pytaniem użytkownika — więc posiadacz `runs:view` mógł już złożyć ten wątek, iterując po transkryptach jego runów. Odczyt szczegółów niesie też `prev_run_id` / `next_run_id`, czyli runy po obu stronach _w tej samej rozmowie_, więc przechodzenie po wątku to dwie strzałki, a nie wycieczki z powrotem do listy. !!! warning "To powiązanie jest kolumną, a nie oknem czasowym — i tak ma być" Dwa runy zaczęte w jednej rozmowie przeplatają się. Wycięcie wiadomości oknem między `started_at` a `ended_at` zwraca tury pierwszego runa *oraz* drugiego, a run bez `ended_at` — anulowany albo wciąż trwający — nie zwraca zupełnie nic. Oba są błędne w sposób, którego czytelnik nie widzi. Prompt jest zapisywany _zanim_ powstanie wiersz runa, bo złożenie agenta, które kończy się odmową — skasowany sekret, profil modelu usunięty przy wdrożeniu — nie może zgubić tego, co ktoś napisał. Zostaje powiązany, gdy tylko jest run, z którym da się go powiązać. Skasowanie runa zeruje kolumnę, zamiast kasować tury: te słowa i tak padły, a rozmowa jest miejscem, w którym ktoś je czyta. Powiązanie tury to nie to samo co jej zapisanie, a to, co każda powierzchnia faktycznie zapisuje, jest osobnym pytaniem, na które ta kolumna nie odpowiada, skoro wiąże wiersze, które już istnieją. Powierzchnie niestrumieniowe są zapisywane przez runnera, a nie przez siebie same, i to właśnie ujednoliciło je; wyjątki są w [Powierzchniach](channels.md#what-each-surface-records). Historia runów filtruje dokładnie po tym. `GET /runs` przyjmuje listę statusów rozdzieloną przecinkami (`?status=failed,budget_exceeded`), bo pytanie operatora dotyczy _zbioru_ wyników, a nie jednego statusu naraz. Nieznany status jest odrzucany, zamiast po cichu nie pasować do niczego — pusta strona musi znaczyć „nie ma takich runów”. ### Run i to, co przekazał modelowi { #a-run-and-what-it-handed-the-model } Transkrypt mówi, o co zapytano i co wróciło. Nie mówi, co model **dostał** — który prompt, które narzędzia, opisane jak, przy jakich ustawieniach — i nic z tego nie da się wywnioskować po fakcie. To, co powiedziano modelowi, to instrukcje speca plus instrukcje platformy, plus to, co dokleiło powiązanie z kanałem, plus powiązane skille, plus ten [system reminder](reference/capabilities.md), który odpalił się przy tym żądaniu. To, co model mógł wywołać, to rejestr capability plus serwery MCP organizacji, minus to, co ukrył [tool search](mcp.md). Odtwarzanie tego ze składowanego speca byłoby drugą implementacją buildera, a druga implementacja to coś, co nie zgadza się z pierwszą. Dlatego to jest **zapisywane, a nie odtwarzane.** Model, na którym działa agent, jest opakowany, a każde żądanie zostaje spisane w locie: - instrukcje i części systemowe; - każda definicja narzędzia dokładnie w takiej postaci, w jakiej dostał ją provider; - wysłane ustawienia; - jeden wpis na żądanie, z jego czasem trwania, tokenami i tym, co poprosiło wywołać dalej; - pełna lista wiadomości ostatniego żądania. To, co jest zapisane, jest więc tym, co zostało wysłane. `GET /runs/{id}/manifest` odczytuje to z powrotem, autoryzowane tak samo jak transkrypt — istnienie rozstrzygane najpierw względem twojej organizacji, potem `runs:view`. !!! danger "Dwie rzeczy, których celowo nie robi" Nigdy nie zapisuje przekazywania wprost do providera (`extra_headers`, `extra_body`), bo to tam jedzie poświadczenie providera, a [vault](secrets.md) jest jedynym miejscem, w którym trzyma się sekret. A run, który nigdy nie dotarł do modelu — zatrzymany przez budżet, zablokowany przez guardrail na wejściu — nie zapisuje nic i odpowiada 404, bo pusty dokument twierdziłby, że agent nie dostał żadnego promptu i żadnych narzędzi. Zapis zbyt duży, by go zachować, jest **przycinany, a nie odrzucany**, i mówi o tym. Etapami, z których każdy jest mierzony: najpierw idą wiadomości, potem schematy argumentów narzędzi, potem opisy narzędzi, a na końcu sam prompt. Te dwa ostatnie są cięte do rozpoznawalnej długości, a nie wyrzucane, bo własne instrukcje agenta i opis zdalnego narzędzia MCP są nieograniczone i to one czynią zapis przerośniętym, gdy wiadomości i schematów już nie ma. Tym, co przetrwa cokolwiek się stanie, są ustawienia i wodospad żądań. Żądanie, które **się nie powiodło**, jest wpisem w tym wodospadzie jak każdy inny, strumieniowane czy nie. Niesie klasę wyjątku i nigdy jego komunikat, bo SDK providera wstawia w ten ciąg URL, który zawiódł — a więc i klucz w jego query stringu. --- ## Jeszcze trzy, bo łatwo je pomylić { #three-more-because-they-are-easy-to-confuse } ### Capability a narzędzie { #capability-vs-tool } **Capability** to jednostka, którą ktoś przyznaje: `knowledge`, `web_research`, `code_execution`. Może wnieść kilka **narzędzi** i niesie konfigurację oraz politykę zatwierdzeń. Zatwierdzenie rozstrzyga się od najbardziej szczegółowego: 1. własne nadpisanie narzędzia, potem 2. tryb capability, potem 3. brak zatwierdzania, gdy żadne z nich nic nie mówi. Builder podaje _wynik_ słowami, zamiast opisywać regułę, bo reguła, którą czytelnik musi przeliczyć w głowie, to ustawienie, którego nikt nie śmie dotknąć. Zobacz [katalog capability](reference/capabilities.md), żeby poznać to, co jest w zestawie, i [Dodaj capability](howto/add-capability.md), żeby dodać nową. !!! note "Narzędzia MCP są wyjątkiem od wszystkiego powyższego" Narzędzia, które przychodzą z [serwera MCP](mcp.md), są odkrywane w czasie uruchomienia, więc nic ich nie zadeklarowało i nic ich nie bramkuje. ### Kolekcja a skill { #collection-vs-skill } **Kolekcja** to dokumenty pocięte na fragmenty i zembedowane, i się ją _przeszukuje_. Model wybiera, czego szukać; nigdy nie może poszerzyć tego, gdzie szuka. **Skill** to folder Markdownu — `SKILL.md` i jakiekolwiek pliki idące z nim w parze — i się go _czyta_. Jego opis jest jedyną częścią, którą model widzi przed decyzją, czy go otworzyć, i dlatego opis skilla powinien mówić, **kiedy ma zastosowanie**, a nie co jest w środku. Praktyczna różnica: wyszukiwanie kosztuje jedno wywołanie embedujące na każde szukanie i zwraca fragmenty. Skill nie kosztuje nic, dopóki nie zostanie otwarty, a wtedy zwraca cały dokument. Zobacz [Skille](skills.md), żeby poznać format i pełne porównanie. ### Delegat a specjalista inline { #delegate-vs-inline-specialist } Agent może oddać część zadania innemu agentowi. Są dwa sposoby, żeby powiedzieć, kim jest ten drugi agent, wyglądają podobnie we własnym słowniku Buildera, a prawie wszystko, co w delegacji się liczy, wynika z tego, który wybrałeś. **Delegat** to inny opublikowany agent w organizacji, wskazany przez `agent_id` _oraz_ `agent_version_id` — przypięty. Adresuje się go slugiem, tym samym uchwytem, który rozwiązuje wzmianka na kanale. **Specjalista inline** jest zdefiniowany wewnątrz speca rodzica: nazwa, opis, który model rodzica czyta przed delegowaniem, instrukcje oraz — bo streszczacz nieumiejący czytać kolekcji jest bezużyteczny — własny model, własne capability, własne kolekcje i skille oraz własny limit kroków. To czyni specjalistę agentem pod każdym względem prócz jednego. Cztery rzeczy czynią tu coś agentem: | | Opublikowany delegat | Specjalista inline | | -------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | **Wersjonowany** | tak — przypięty, a przypięcie rusza się tylko wtedy, gdy ktoś nim ruszy | **nie** | | **Sprawdzany pod kątem uprawnień przy publikacji** | tak — `agents:run` na tym wierszu | tak — te same sprawdzenia zakresów, sekretów, kolekcji i skilli, które dostają własne powiązania rodzica | | **Własne capability** | tak — te z jego opublikowanego speca | tak — własne plus to, czym dzieli się rodzic | | **Mierzony i limitowany** | tak | tak — limitami runa, co [obowiązuje wewnątrz każdej delegacji](governance.md#delegation-spends-the-parents-budget) | **Brakującą rzeczą jest wersja** i wynika z niej wszystko, czego specjalista nie może: nic innego nie może się do niego odwołać, edycja rodzica go zmienia, nie dostaje własnego wiersza runa i nie może delegować dalej. Opublikowany delegat da się przejrzeć i wyeksportować. Specjalista to akapit w czyimś specu. Używaj więc specjalisty do pracy, która nie powinna wymagać publikowania agenta — „streść to w trzech punktach” — a delegata do zdolności, którą organizacja posiada i wykorzystuje wielokrotnie. Trzeci rodzaj stoi obok nich: **zdalny agent**, agent innego dostawcy osiągalny przez A2A i zarejestrowany w organizacji po jego agent card. Wywołuje się go tak samo i rozlicza z tego samego budżetu, ale nie jest tu wersjonowany — jego właściciel zmienia go po swojej stronie — więc odwołanie wskazuje rejestrację, a nie wersję. Zobacz [Zdalni agenci przez A2A](reference/capabilities.md#remote-agents). #### Specjaliści dynamiczni i droga wyjścia { #dynamic-specialists-and-the-way-out } Pod tym inline siedzi trzeci rodzaj: **specjalista dynamiczny**, wymyślony przez model w czasie uruchomienia pod [`allow_dynamic`](reference/capabilities.md#delegation). To specjalista, który nie jest nawet zapisany w specu rodzica i nie jest nigdzie utrwalany — zachowanie go oznaczałoby opublikowanie agenta, a publikacja jest czynnością człowieka. Ta reguła jest projektem, a nie ograniczeniem, i tym, co ją nim czyni, jest wyjście: człowiek może **wypromować** specjalistę do agenta w wersji roboczej. Promocja działa na specjaliście inline w Builderze, a na dynamicznym z panelu delegacji na czacie, dopóki run, który go stworzył, jest jeszcze na ekranie — to jedyne okno, w którym definicja specjalisty dynamicznego jest czytelna, bo jedzie ona w otwierającej ramce delegacji i nic nie przechowuje jej po zakończeniu tury. Tworzy zwykły draft z instrukcji, modelu, capability, kolekcji i skilli specjalisty, należący do tego, kto go wypromował, i podlegający zwykłemu sprawdzeniu `agents:edit`. I na tym się zatrzymuje: nie publikuje, nie przypina nowego agenta jako delegata jego rodzica i nie usuwa specjalisty, z którego powstał. Każda z tych rzeczy to następna decyzja, z normalną walidacją przed nią. Bez tego wyjścia jedynym sposobem na zachowanie dobrego specjalisty było skopiowanie jego instrukcji z logu czatu — co daje agenta, którego pochodzenia nikt nie widzi, czyli dokładnie ten skutek w postaci nieśledzonego agenta, któremu reguła o nieutrwalaniu ma zapobiegać. !!! important "Jedno pojęcie „agenta”, używane rekurencyjnie" Ryzykiem, przed którym ten kształt ma chronić, jest *drugie*, równoległe pojęcie agenta — takie, po którym walidacja publikacji nie chodzi i którego model uprawnień nie widzi. Specjalista byłby dla niego oczywistym miejscem: nie wygląda jak agent i właśnie dlatego jest kuszącym miejscem, żeby sięgnąć po kolekcję, której nikt nie udostępnił, albo capability, której nikt nie przyznał. Chroni przed tym odmowa napisania drugiego formatu. Specjalista to otypowany *podzbiór* speca, używający tych samych powiązań capability, sprawdzany tym samym rekurencyjnym przebiegiem publikacji i składany przez ten sam builder. Jeden typ speca, jeden walidator, jeden builder, jeden komponent Buildera — każdy używany rekurencyjnie. Jeśli któremukolwiek z nich wyrośnie druga kopia na potrzeby specjalistów, ta kopia jest błędem. **Przypięcie zawodzi głośno, zamiast dryfować.** Delegat, którego przypięta wersja już nie istnieje, kończy run błędem i nazywa tego delegata. Celowo nie ma cofania się do jego bieżącej wersji: powodem przypinania jest to, że nic nie zmienia się bez decyzji, a cicha aktualizacja jest gorsza od odmowy, bo nikt się o niej nie dowiaduje. Koszt tego jest płacony w Builderze, który porównuje każde przypięcie z tym, co delegat publikuje teraz, i proponuje jego przesunięcie. Zobacz [capability `subagents`](reference/capabilities.md#delegation), żeby poznać pułapy i narzędzia, oraz [Uprawnienia](permissions.md#delegation-is-not-a-privilege-boundary), żeby dowiedzieć się, kto komu może delegować. --- ## Pomocnicy ewaluacji { #system-agents } Ocenianie, generowanie przypadków i proponowanie ulepszeń korzystają z pomocników zdefiniowanych w kodzie. Konfiguruje się ich na datasecie; nie występują w katalogu jako agenci do tworzenia i edycji. Architect i asystenci AgenticOS pozostają zwykłymi agentami produktu. Zakładka **Ocenianie** datasetu wybiera model oceniający, instrukcje, kryteria i próg. **Ustawienia** wybierają model i instrukcje generowania przypadków oraz ulepszania agenta, używając tego samego selektora modeli i edytora Markdown co Agent Builder. Początkowe instrukcje pochodzą z kodu. Zmiany wymagają `datasets:edit`, a dodanie modelu także `connections:manage`. Zadania generowania, żądania asystenta i sesje ulepszania zachowują model i instrukcje z chwili startu. Zmiana ustawień dotyczy kolejnych zadań. Historyczne identyfikatory pomocników i opublikowane wersje pozostają w bazie dla historii runów, budżetów i starych przypięć; nie zajmują limitu agentów użytkownika. ### Izolacja { #system-agent-spec } Pomocnicy działają przez dotychczasowy runner na powierzchni `eval`. Przed płatnym wywołaniem sprawdzane są dostęp osoby, poświadczenia modelu i budżety; koszty i błędy widać w Aktywności. Izolacja blokuje zapisy do produkcyjnych połączeń i ogranicza odczyty wiedzy do dostępu zlecającej osoby. Pomocnika nie można wystawić przez chat, kanał, harmonogram ani delegację. ### Istniejące zadania { #retiring-a-system-agent } Migracja zachowuje modele i instrukcje istniejących oceniających oraz zadań w kolejce. Historyczne runy i przypięcia zestawów zachowują swoje identyfikatory. Nie ma organizacyjnego przełącznika wsparcia AI ani organizacyjnego modelu oceniania; konfiguruje się dataset i korzysta z dotychczasowych uprawnień oraz budżetów. ## Profile modeli { #model-profiles } **Profil modelu** to nazwany model wsparty przechowywanym kluczem: `openai default`, `OpenRouter prod`. Agenci wskazują na profile, nigdy na ciągi z nazwami modeli. W tej pośredniości rzecz cała. Zrotowanie klucza albo przeniesienie wszystkich agentów z jednego modelu na inny to edycja jednego profilu, a nie czterdziestu speców. Profil, za którym nie stoi żadne poświadczenie, jest oznaczony `no key` wszędzie, gdzie się pojawia, bo to jedyny fakt rozstrzygający, czy agent może się w ogóle uruchomić. Pickery go nie oferują, a publikacja go odrzuca, dopóki znów nie ma klucza. Profilu, którego używają agenci, nie można usunąć; zobacz [Zmiana klucza lub endpointu profilu](models.md#changing-a-profiles-key-or-endpoint). Ceny pochodzą z [`genai-prices`](https://github.com/pydantic/genai-prices), utrzymywanego przez Pydantic, a nie z tabelki w tym repozytorium — tabelka prowadzona ręcznie nie potrafi wyrazić cen progowych i po cichu się starzeje. Model, którego pakiet nie zna, jest zapisywany z kosztem zero i ostrzeżeniem, a suma runa jest oznaczana jako dolna granica, zamiast być zgadywana. Zobacz [Modele i providery](models.md), żeby poznać dwadzieścia siedem providerów, poświadczenie, którego chce każdy z nich, i zachowanie mechanizmów zapasowych. ## Organizacje { #organizations } Każdy zasób jest przypisany do organizacji. Izolacja jest egzekwowana przez **schemat** — kolumny `NOT NULL`, ograniczenia check, ograniczenia unikalności o zasięgu jednego tenanta — a nie tylko przez warstwę serwisów, więc pominięta klauzula `WHERE` jest naruszeniem ograniczenia zamiast wyciekiem danych. Vault idzie dalej: szyfrogram sekretu jest związany z organizacją, która go zapisała, więc wiersza skopiowanego między tenantami nie da się odszyfrować. ## Podsumowanie { #recap } - **Spec** to agent jako dane, trzymający odwołania i nigdy wartości. - **Publikacja** zamraża go w **wersję**, a _środowisko_ to osobna decyzja o tym, którą wersję spotykają ludzie. - **Ekspozycja** to miejsce, w którym jest osiągalny; **trigger** to moment, w którym uruchamia się, gdy nikt nie patrzy. Oba są stanem operacyjnym obok speca, a nie w nim. - **Run** to jedno wykonanie i zapisuje, ile kosztował, nawet gdy się nie powiódł. - Każda powierzchnia, każdy trigger i każda delegacja idą przez **jeden runner** i dlatego nadzoru nie da się obejść z poziomu wywołującego. ## Dalej { #next }
- :material-account-key:{ .lg .middle } **[Uprawnienia](permissions.md)** Role, zakresy i granty. - :material-shield-check:{ .lg .middle } **[Governance](governance.md)** Budżety, zatwierdzenia, alerty, audyt. - :material-toolbox:{ .lg .middle } **[Capability](reference/capabilities.md)** Co można dać agentowi. - :material-connection:{ .lg .middle } **[MCP](mcp.md)** Narzędzia, których nikt tutaj nie musi pisać.
Ponadto: [Modele](models.md) o providerach i koszcie, [Sekrety](secrets.md) o tym, dlaczego szyfrogram nie może zmienić tenanta, i [Architektura](architecture.md) o tym, jak rozłożony jest kod.