---
source_sha: "026fa67fb1f0"
---
# Postawić agenta tam, gdzie ludzie już są { #putting-an-agent-where-people-already-are }
Agent, który odpowiada wyłącznie w tym dashboardzie, to demo. Ten sam
opublikowany agent odpowiada w ośmiu miejscach, a każde z nich uruchamia *tę samą
zamrożoną wersję* przez ten sam budżet, tę samą bramkę zatwierdzania i te same
kontrole tenanta — zmienia się powierzchnia, nie agent.
| Gdzie | Czego potrzebuje | Kim jest odwiedzający |
|---|---|---|
| **Dashboard** | niczego | zalogowany członek |
| **Widget na stronie** | tag `
```
To cała integracja. Skrypt nie ma zależności, kroku budowania ani frameworka —
działa na stronie, która ładuje już React, jQuery albo nic w ogóle.
### 3. (Opcjonalnie) powiedz mu o odwiedzającym { #3-optional-tell-it-about-the-visitor }
Widget może **zadeklarować zmienne** — nazwę, to, czy jest wymagana, i linijkę
mówiącą, do czego służy — a strona je dostarcza:
```html
```
Snippet, który wręcza ci Builder, niesie już tę linijkę, z twoimi własnymi
kluczami w środku, gdy tylko jakieś zadeklarujesz.
Są doklejane do instrukcji agenta jako oznaczony blok danych, pod linijką
mówiącą, że to informacje o odwiedzającym, a nie instrukcje, i że nie da się ich
zweryfikować. To ostatnie jest prawdą o każdej z nich, także na widgecie `jwt`:
widget czyta `window.AgenticOSContext`, a token uwierzytelnia *kim odwiedzający
jest*, a nie *co strona o nim powiedziała*. Więc nic tutaj nie może decydować
o tym, co agentowi wolno.
Wynikają z tego trzy zasady:
- **Klucz, którego nikt nie zadeklarował, jest odrzucany.** Stronę odwiedzający
może edytować; bez deklaracji każdy wymyślony przez niego klucz stałby się
linijką wewnątrz instrukcji agenta.
- **Brakująca wymagana wartość pomija swoją linijkę i trafia do logu.**
`required` to obietnica integratora złożona samemu sobie — egzekwowanie jej
kosztowałoby odwiedzającego odpowiedź przez cudzy błąd we wdrożeniu.
- **Wysyłane raz na rozmowę**, przed pierwszym pytaniem, i odczytywane z każdej
ramki, a nie w chwili połączenia: aplikacja single-page dowiaduje się, kim ktoś
jest, bez ponownego łączenia.
### 4. (Opcjonalnie) powiedz mu, kim jest odwiedzający { #4-optional-tell-it-who-the-visitor-is }
Dla widgetu wewnątrz twojego własnego produktu z logowaniem ustaw token
**zanim** załaduje się skrypt. Twój backend go podpisuje; my go weryfikujemy
i nigdy nie widzimy twojej bazy użytkowników:
```html
```
Wybicie takiego tokena, w dowolnym języku, który potrafi podpisać JWT:
```python
import time, jwt # PyJWT
token = jwt.encode(
{"sub": str(user.id), "iat": int(time.time())},
EMBED_SIGNING_SECRET, # the secret you set on the embed
algorithm="HS256",
)
```
- `sub` jest wymagane. Identyfikuje odwiedzającego na potrzeby rate limitingu,
a token bez niego jest odrzucany — w przeciwnym razie jeden wyciekły token
staje się budżetem całego widgetu.
- **`iat` jest wymagane i musi mieścić się w ostatnich 12 godzinach** — nie jest
sprawdzane tylko wtedy, gdy jest obecne. Token bez `iat` albo z przeterminowanym
jest odrzucany, więc taki, który wycieknie z przeglądarki, nie może działać
wiecznie. Ustawione przez ciebie `exp` też jest respektowane, ale wyłącznie po
to, by to okno **skrócić** (token wygasły zostaje odrzucony); nie może
przedłużyć tokena ponad sufit dwunastu godzin.
- Wybijaj go przy każdym załadowaniu strony, po stronie serwera.
!!! danger "Nigdy nie wysyłaj sekretu podpisującego do przeglądarki"
Podpisuje on *dowolnego* odwiedzającego. Token, który wybija, jest tym, co
przeglądarce wolno trzymać, i tylko przez te dwanaście godzin, które daje mu
`iat`.
---
## Surowy WebSocket { #the-raw-websocket }
Widget jest klientem udokumentowanego protokołu, a nie czarną skrzynką. Jeśli
chcesz własnego UI — aplikacji mobilnej, kiosku, komponentu w swoim design
systemie — rozmawiaj z tym samym socketem:
```
wss://your-api.example.com/api/v1/embed/PUBLIC_KEY/ws[?token=SIGNED_JWT]
```
**Nie musisz składać go samodzielnie.** Opublikuj taki socket z Buildera — agent
→ **Availability** → *Raw WebSocket* — a jego wiersz wypisze URL, zbudowany
z własnego bazowego URL-a deploymentu, z przyciskiem kopiowania. **Widget**
wypisuje to samo obok swojego tagu skryptu, bo widget jest klientem tego
protokołu: przejście na własny interfejs to krok, a nie przepisywanie od nowa.
`?token=` nie jest tam wypisywany: w trybie `jwt` token wybija dla każdego
odwiedzającego twój backend, a prawdziwy token na ekranie dashboardu to działające
poświadczenie, które ktoś może odczytać przez ramię.
!!! bug "Pierwsza rzecz, która idzie nie tak: brakujący `Origin`"
Handshake musi go nieść, i to z listy dozwolonych embeda. **Przeglądarka
wysyła go za ciebie; twój własny klient nie wysyła nic, dopóki go nie
ustawisz** — aplikacja mobilna, kiosk, przekaźnik po stronie serwera. To, jak
to wygląda, gdy go nie wyśle, to `4003` w tabeli niżej, a nie komunikat
o błędzie.
```mermaid
sequenceDiagram
participant C as Your client
participant E as /embed/{key}/ws
participant R as run_stream
C->>E: handshake (Origin, optional ?token=)
alt origin not allowed, token bad, embed paused
E-->>C: close 4003 - do not retry
else admitted
E-->>C: ready { visitor }
C->>E: message { text }
E->>R: the same loop /chat drives
R-->>C: model_request_start
R-->>C: text_delta … (a word at a time)
R-->>C: final_result, then complete
end
```
**Ramki, które wysyłasz**
```json
{ "type": "message", "text": "Do you ship to Poland?" }
```
To całe słownictwo przychodzące, plus `context` (co strona mówi o odwiedzającym)
i `file_ids` (co załączył, na stronie, która przyjmuje pliki).
Każda ramka `message` wlicza się do limitu połączenia, także pusta lub
niepoprawna. Poprawne wiadomości zużywają dodatkowo dotychczasowy limit
odwiedzającego; błędne dane nie wyczerpują puli współdzielonej z innymi anonimowymi
odwiedzającymi. Ramka może zawierać najwyżej trzy identyfikatory plików; dłuższa
lista jest odrzucana przed parsowaniem. Niepoprawne identyfikatory w krótkiej liście
są pomijane i zapisywane jako jeden zbiorczy log bez ich wartości.
**Celowo nie obejmuje trzech pól, które niesie własna ramka dashboardu:** agenta,
profilu modelu i środowiska.
Ramka, która mogłaby wybrać model, to odwiedzający wybierający go na rachunek
operatora, a taka, która mogłaby wybrać agenta, to odwiedzający rozmawiający
z czymś, czego nikt nie opublikował na tym kluczu. Wszystkie trzy biorą się
z wiersza embeda.
Nieznane pole jest **ignorowane, a nie odrzucane**. Klient zbuforowany w czyjejś
przeglądarce może być starszy niż ten serwer, a zamknięcie z tego powodu socketu
zabrałoby ze sobą rozmowę.
**Ramki, które odbierasz**
!!! note "To jest własne słownictwo ramek dashboardu, a nie drugie takie"
`/chat` i ten socket napędzają jedną pętlę (`app/services/run_stream.py`),
więc odpowiedź przychodzi tutaj słowo po słowie tak samo jak tam. Hostowana
strona pokazywała kiedyś jedną bryłę tekstu po trzydziestu sekundach niczego
i to była pętla, a nie transport.
Każda ramka niesie `{ "type": …, "data": { … } }`.
| `type` | `data` | Znaczenie |
|---|---|---|
| `ready` | `visitor` | Połączono. `visitor: true`, gdy token zidentyfikował osobę. |
| `history` | `messages` | Co zostało powiedziane w wątku, który ten odwiedzający wznawia — **hostowana strona z anonimowym odwiedzającym**, bo tylko takie połączenie niesie klucz ciągłości: rozmowa widżetu trwa tyle, co jego socket, a odwiedzającego `jwt` nazywa już jego token. Każdy wpis to `role`, `text` i `at`, więc odtworzona tura zachowuje pod sobą swój czas. |
| `model_request_start` | — | Agent poszedł do modelu. Pokaż wskaźnik. |
| `part_start` | `index`, `part_type`; `tool_name`, `tool_call_id` dla `ToolCallPart` | Zaczyna się blok odpowiedzi. Wywołanie narzędzia przedstawia się już tutaj, żeby klient mógł je pokazać, zanim spłyną jego argumenty. Wysyłane tylko dla bloku, który ta powierzchnia faktycznie poniesie — strona niepokazująca rozumowania nie zapowiada `ThinkingPart`, bo sama zapowiedź mówi, że agent rozumował. |
| `text_delta` | `index`, `content` | Słowa odpowiedzi. Doklejaj je. |
| `thinking_delta` | `index`, `content` | Rozumowanie modelu. **Tylko jeśli operator to włączył.** |
| `call_tools_start` | — | Agent zaraz użyje narzędzi. |
| `tool_call` | `tool_call_id`, `tool_name`, `args` | Krok. `args` tylko wtedy, gdy operator pokazuje wyniki. |
| `tool_call_delta` | `index`, `args_delta` | Argumenty wywołania w miarę, jak płyną strumieniem. |
| `tool_result` | `tool_call_id`, `content` | Co zwrócił ten krok. |
| `sources` | `names` | Dokumenty zwrócone przez wyszukiwanie w bazie wiedzy, po nazwie. **Tylko w widgecie, którego operator włączył źródła**, i nigdy fragmenty. |
| `final_result_start` | `tool_name` | Odpowiedź jest produkowana przez narzędzie wyjściowe. |
| `final_result` | `output` | Czym run się zakończył. Puste w turze, która zaparkowała. |
| `complete` | — | Tura się skończyła. Nie niesie **żadnego usage**: ile run kosztował, to sprawa operatora, nie odwiedzającego. |
| `error` | `message` | Coś, co odwiedzający powinien zobaczyć: rate limit, osiągnięty budżet, odmowę, turę, która nic nie wyprodukowała. |
| `compaction_started`, `compaction_finished`, `compaction_impossible` | zależnie od ramki | Agent porządkuje własne notatki, bo wątek przerósł okno modelu. Wysyłane zawsze, niezależnie od przełączników operatora: podsumowanie trwa dziesiątki sekund, a powierzchnia, która streamuje i nic nie mówi, po prostu zatrzymuje się na ten czas ([#936](https://github.com/vstorm-co/agenticos/issues/936)). |
Niektóre ramki dashboardu nigdy nie docierają do publicznego socketu i są
odmowami, a nie ustawieniami. **`user_prompt_processed`** niesie prompt *w
postaci złożonej* — notatkę o umiejscowieniu i dostarczony blok ponad tym, co
wpisał odwiedzający — czyli tekst operatora, a nie coś, co odwiedzający ma
odczytywać z powrotem.
**`ask_user` i `tool_approval_required` nie mają tu nikogo, kto by na nie
odpowiedział, ale zawodzą inaczej.** Odwiedzający nie może zatwierdzić efektu
ubocznego w cudzej organizacji, więc `tool_approval_required` **parkuje** run
dokładnie tak, jak robi to na kanale, a tura kończy się ramką `error` mówiącą, że
musi zdecydować człowiek — inaczej niż na kanale, bez linku do `/runs`, bo tam
czytelnikiem jest członek, który może go otworzyć, a tutaj obcy trzymający link.
`ask_user` **nie** parkuje: `AgentDeps.ask_user` jest na tej powierzchni `None`,
więc narzędzie *odmawia*, gdy model je wywoła (`app/agents/ask_user.py`), a model
idzie dalej i odpowiada bez tego wejścia — odpowiedź zdegradowana, a nie
zaparkowany run.
**Klient ignoruje to, czego nie rysuje**, a `widget.js` jest tego rozpracowanym
przykładem: czyta `model_request_start`, `text_delta`, `final_result`, `complete`
i `error` dla odpowiedzi, `tool_call` i `tool_result`, by pokazać każdy krok jako
etykietę (*Przeszukuję bazę wiedzy*), oraz `sources` dla dokumentów, które
odpowiedź wymieniła. Rozumowanie ignoruje celowo. Nosi twarz agenta, renderuje
odpowiedź jako Markdown do węzłów, a nie do HTML, dopasowuje się do jasnego lub
ciemnego motywu odwiedzającego i na telefonie zajmuje cały ekran.
**Kody zamknięcia**
| Kod | Znaczenie |
|---|---|
| `4003` | Odmowa. Origin jest niedozwolony, token nie przeszedł albo widget jest zapauzowany. Nie ponawiaj — odpowiedź się nie zmieni. |
| `4029` | Za dużo połączeń z tego adresu w ostatniej minucie. Odczekaj i spróbuj ponownie. |
| `1011` | Ten klient nie czytał. Ramka potrzebowała ponad 30 sekund, by do niego dotrzeć, więc serwer przestał pisać, zamiast trzymać sesję bazodanową tury i otwarty strumień providera raz na ramkę. Połącz się ponownie; hostowana strona wznawia swój wątek. |
Odmowa to celowo jeden kod z jednym komunikatem. Strona, której nie ma na liście
dozwolonych, dowiaduje się, że jest niedozwolona, i niczego o tym, czy token by
pomógł.
`4029` jest od niej oddzielony z odwrotnego powodu: „niedozwolone" i „dozwolone,
ale za szybko" proszą klienta o przeciwne rzeczy — przestań na zawsze i spróbuj
ponownie później — więc klient, który ich nie rozróżnia, albo młóci w odmowę,
albo porzuca limit. Ile połączeń dostaje adres, mówi
`RATE_LIMIT_EMBED_PER_MINUTE`; ile *wiadomości* dostaje odwiedzający po
połączeniu, mówi własny rate limit widgetu, ustawiany w Builderze.
Minimalny klient:
```js
const socket = new WebSocket(`${BASE}/api/v1/embed/${KEY}/ws`);
let answer = "";
socket.onmessage = (event) => {
const { type, data } = JSON.parse(event.data);
if (type === "text_delta") render((answer += data.content));
if (type === "final_result" && data.output) render((answer = data.output));
if (type === "complete") answer = "";
if (type === "error") render(data.message);
};
socket.send(JSON.stringify({ type: "message", text: "hello" }));
```
`final_result` jest przypisywany, a nie doklejany: to jest to, czym run się
*zakończył*, a provider, który nie przysłał strumieniem żadnych delt, zostawia go
jako jedyną kopię odpowiedzi.
---
## Co oferuje każda powierzchnia i dlaczego różnice są różnicami { #what-each-surface-offers-and-why-the-differences-are-differences }
Trzy powierzchnie uruchamiają tego samego agenta przez ten sam runner: czat w
dashboardzie, ten socket i publiczne API. **Nie** oferują tego samego uruchomienia, a
do [#936](https://github.com/vstorm-co/agenticos/issues/936) nic nie mówiło, które z
tych różnic były decyzjami.
Teraz mówi. Każde „nie” poniżej ma powód, a powód to albo „tutaj byłoby to złe”,
albo „to jeszcze nie jest zbudowane” — nigdy milczenie.
Ostatnia kolumna to Teams (#1985), reprezentujący stojące obok platformy czatu.
| | `/chat` (dashboard) | Surowy WebSocket | Publiczne API | Teams |
|---|---|---|---|---|
| Streaming | tak | tak, filtrowany tym, co pokazuje operator | **nie** — POST jest ścieżką bez streamingu; wariant SSE to osobne pytanie | wiadomość przepisywana w miarę, jak przychodzi odpowiedź |
| Załączniki | tak | tak (`file_ids`) **na hostowanej stronie** — endpoint wgrywania rozwiązuje klucz przez `find_page`, więc widżet ani surowy socket nie mają ścieżki, która wytworzyłaby id | tak (`file_ids`), i trafiają tam, gdzie wszędzie indziej | tak, z czatu osobistego; plik wraca przez kartę zgody Teams |
| Ciągłość rozmowy | tak | tak (`continuity_key`), na hostowanej stronie z anonimowym odwiedzającym | tak (`conversation_id`) | czat albo wątek na kanale |
| `environment_id` | tak | **nie, celowo** — patrz niżej | tak | **nie** — decyduje powiązanie, jak na każdej platformie czatu |
| Nadpisanie modelu | tak | **nie, celowo**: publiczna powierzchnia nie może pozwolić odwiedzającemu wybrać, co wydaje | **nie**: żądanie nie niesie `model_profile_id`. Która wersja się uruchomi, wciąż zależy od wywołującego przez `environment_id`, a środowisko może przypiąć wersję, która nigdy nie była domyślna | **nie**, jak na każdej platformie czatu |
| `ask_user` | tak | **nie, jeszcze nie** — patrz niżej | **nie, słusznie**: nikt nie czeka na odpowiedź, trzymając otwarty request HTTP | tak, jako karta Adaptive Card |
| Powiadomienie o kompakcji | tak | tak | nie dotyczy — nic nie streamuje, żeby powiedzieć | nie — odpowiedź przychodzi, gdy jest gotowa |
| Ramki delegacji | tak | **nie, celowo**: podpięcie sinka każe bibliotece otworzyć *streamowany* request na dziecko, więc delegat, którego dostawca nie potrafi streamować, psuje się w momencie, w którym ktoś patrzy | nie dotyczy — nic nie streamuje, w czym by je rysować | **nie** — odpowiedź przychodzi, gdy jest gotowa, bez rysowania kroków |
| Zatwierdzenia: parkują uruchomienie | tak | tak | tak | tak |
| Zatwierdzenia: droga do decyzji | tak (`/runs`) | jedno zdanie, celowo: obcemu trzymającemu link nie pokazuje się URL-a do czyjejś konsoli | tak — wynik niesie `parked`, nazywając to, co czeka, i zatwierdzenie, do którego wysłać decyzję | tak — karta Adaptive Card, rozstrzygana jako członek, który ją nacisnął |
| Wzmianki `@` | tak (`mentions`) | **nie, jeszcze nie**: widget nie ma menu do wyboru, a wzmianka gościa wymagałaby tej samej kontroli w jego imieniu | **nie, jeszcze nie**: skrypt nazywa, czego użyć, w samej wiadomości | **nie**, jak na każdej platformie czatu |
### `environment_id` na sockecie i dlaczego należy do publikującego { #environment_id-on-the-socket-and-why-it-is-the-publishers }
API je ma, bo wołającym jest organizacja: przećwiczenie środowiska deweloperskiego
przed promocją to jej własna praca na jej własnym agencie. Wołającym socketa jest
ten, kto trzyma klucz embeda, a na widgecie jest to przeglądarka obcej osoby. Ramka
wybierająca środowisko pozwoliłaby odwiedzającemu wybrać, która *wersja* agenta mu
odpowiada — łącznie z tą, której organizacja nie wypromowała, co jest
przeciwieństwem tego, co znaczy publikowanie wersji.
Więc w tym kształcie jest odrzucona. Jeśli integracja first-party potrzebuje
uruchomić nazwane środowisko, miejscem na to jest konfiguracja samego embeda, gdzie
publikujący wybiera raz, a każdy odwiedzający dostaje tę samą odpowiedź.
### `ask_user` na sockecie i czego by potrzebował { #ask_user-on-the-socket-and-what-it-would-need }
Docstring modułu `ask_user` mówi, że pauza i wznowienie żyją „w sesji WebSocket”, i
ma na myśli sesję *czatu*. Ten socket jest równie żywy, z człowiekiem równie
siedzącym po drugiej stronie — więc delegat, który by zapytał, dostaje informację,
że nikogo nie dało się złapać, co jest właściwą odpowiedzią dla uruchomienia z
harmonogramu i niewłaściwą tutaj.
Nie jest to tu domknięte, bo brakująca połowa nie należy do serwera. Słownik ramek
nie ma `ask_user` ani `ask_user_response`, a widget nie ma formularza, żeby je
wyrenderować — więc podpięcie callbacku parkowałoby uruchomienia na pytaniu, na
które nic nie odpowie, co jest gorsze niż odmowa. Czego to wymaga: dwóch ramek,
formularza w widgecie i decyzji o *hostowanej stronie*, gdzie pytany jest obcym, a
pytanie może nieść cokolwiek, co agent postanowił powiedzieć.
### Zatwierdzenia na sockecie: zdecydowane i celowo nieposzerzone { #approvals-on-the-socket-decided-and-deliberately-not-widened }
Zaparkowane uruchomienie odpowiada odwiedzającemu „To wymaga czyjegoś zatwierdzenia,
zanim będzie mogło się wykonać”, i tak zostaje. Surowy socket jest sprzedawany jako
interfejs, który budujesz sam — aplikacja mobilna, kiosk — i klientowi first-party z
zalogowanym użytkownikiem można by w zasadzie wręczyć zaparkowane wywołanie i sposób
na jego rozstrzygnięcie. Nie jest, bo klucz embeda nie identyfikuje *członka*:
identyfikuje opublikowaną powierzchnię wdrożenia, a osobą po drugiej stronie może być
ktokolwiek, kogo integracja wpuściła. Rozstrzygnięcie zatwierdzenia to akt członka,
bramkowany przez `approvals:decide`, i należy do powierzchni, która wie, kto pyta.
## Hostowana strona { #a-hosted-page }
Najkrótsza integracja, jaka istnieje: **wyślij komuś link.** Żadnej własnej
strony, żadnego tagu `