# Rozmowa i strumień SSE

Wyślij `POST /v1/ask` z kluczem konta. `Accept: text/event-stream` wybiera ask-v2. Dla JSON ustaw `version: ask-v2`, aby dostać zdarzenia w tablicy `events` i pole `continuation`. Zwykły JSON bez pól rozmowy zachowuje format odpowiedzi ask-v1. [Opis operacji](../../operations/ask.md) zawiera pełny schemat.

Na pl-prod planowaniem wywołań zajmuje się `codex-app-server`, z jawnie wybranym modelem i osobnym logowaniem ChatGPT zarządzanym przez CLI dla usługi. Fakty pochodzą z odpowiedzi API. Brak konfiguracji lub logowania oznacza odmowę bez opłaty. Opcjonalny dostawca Anthropic wymaga klucza API; tokeny subskrypcji OAuth są odrzucane.

## Minimalny klient strumienia

Python używa biblioteki standardowej. Zapisz kod jako `ask-stream.py`, ustaw `LANDFEED_API_KEY` i uruchom `python3 ask-stream.py`. Nie ponawia wywołania automatycznie. Błąd przed przyjęciem strumienia przychodzi jako odpowiedź JSON z kodem HTTP.

<!-- example:ask-stream.py -->
```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://mcp.agentdzialek.pl/v1/ask",
    method="POST",
    headers={"X-Api-Key": os.environ["LANDFEED_API_KEY"],
             "Content-Type": "application/json", "Accept": "text/event-stream"},
    data=json.dumps({"question": "Czy działka leży w obszarze zagrożenia powodziowego?",
                     "context": {"parcel_id": "146501_1.0101.12/3"},
                     "max_credits": 5, "version": "ask-v2"}).encode("utf-8"),
)
artifacts, citations, text, continuation = [], [], "", None
name, data_lines, done, failed = None, [], False, False
with urllib.request.urlopen(request, timeout=30) as response:
    if not response.headers.get("Content-Type", "").startswith("text/event-stream"):
        raise RuntimeError("Expected SSE; inspect the JSON admission error")
    for raw_line in response:
        line = raw_line.decode("utf-8").rstrip("\r\n")
        if line.startswith(":"):
            continue
        if line.startswith("event:"):
            name = line[6:].lstrip(" ")
        elif line.startswith("data:"):
            data_lines.append(line[5:].lstrip(" "))
        elif not line and data_lines:
            data = json.loads("\n".join(data_lines))
            if name == "artifact":
                artifacts.append(data)  # Save the whole signed object, including data.
            elif name == "answer.delta":
                text += data["text"]
            elif name == "answer.citations":
                citations = data
            elif name == "error":
                failed = True
                print(json.dumps({"error": data}, ensure_ascii=False))
            elif name in ("confirm.required", "client_tool.call", "budget.exceeded"):
                print(json.dumps({"event": name, "data": data}, ensure_ascii=False))
            elif name == "done":
                done, continuation = True, data["continuation"]
                # With a preceding error, a zero total does not prove successful settlement.
                if not failed:
                    print(json.dumps({"credits_total": data["credits_total"]}))
            name, data_lines = None, []
if not done:
    raise RuntimeError("Stream interrupted; reconcile billing before replay")
print(json.dumps({"answer": text, "citations": citations, "artifacts": artifacts,
                  "continuation": continuation, "failed": failed}, ensure_ascii=False))
```


Ramka ma nazwę w `event:` i JSON w `data:`; pusty wiersz kończy ramkę. Przyjęty i nieprzerwany strumień kończy jedno `done`, również po `error`. Zerwane połączenie może nie dostarczyć końcowej ramki; nie dowodzi to anulowania pracy. Nie buforuj ani nie zapisuj w publicznej pamięci podręcznej uwierzytelnionego strumienia.

## Zdarzenia i rozliczenie

| event | Znaczenie |
| --- | --- |
| `answer.citations` | Cytowania z datą i źródłem. |
| `answer.delta` | Fragment odpowiedzi po rozliczeniu. |
| `artifact` | Cały podpisany wynik API. |
| `budget.exceeded` | Niewykonany plan przekracza budżet. |
| `client_tool.call` | Żądanie czynności interfejsu. |
| `confirm.required` | Pauza przed działaniem wymagającym zgody. |
| `done` | Koniec i kontynuacja; koszt po rozliczeniu. |
| `error` | Błąd; sprawdź stan rozliczenia. |
| `step.finished` | Zakończony krok i rozliczony koszt. |
| `step.started` | Plan i koszt przed wykonaniem. |


Pełne pola i ograniczenia: [referencja SSE](../../sse-events.md). `step.started` podaje plan żądania i szacowany koszt przed wykonaniem. `step.finished` podaje rozliczony koszt po decyzji o całej turze. `artifact` zawiera dokładną odpowiedź publicznego API, a nie tekst napisany przez model. Treść odpowiedzi i nowe artefakty są udostępniane po rozliczeniu. Przy błędzie rozliczenia są wstrzymane.

Domyślny budżet to 5 kredytów, maksymalny 20. Przed pracą serwer rezerwuje cały budżet. Narzędzia korzystają z publicznych cen, a zakończona odpowiedź dodaje 1 kredyt. Tura ask-v2 zatrzymana przez budżet jest bezpłatna w całości, również za wcześniejsze odczyty; nowe artefakty i fakty nie są wtedy udostępniane. Narzędzia interfejsu i samo oczekiwanie na potwierdzenie nie dodają opłaty za odpowiedź. `done.credits_total` jest miarodajne dopiero po udanym rozliczeniu; po `error` nie traktuj zera jako potwierdzonego zwrotu.

## Kontynuacja bez zapisywania rozmowy na serwerze

Klient przechowuje `messages`, całe podpisane obiekty z `artifact` i najnowszą `continuation`. Przy kolejnym pytaniu odeślij je jako `messages`, `results` i `continuation`. Podpis obejmuje dane i konto; nie modyfikuj obiektu, nie przesyłaj go między kontami. Continuation wygasa po godzinie. Każde wznowienie wymaga niepustego `question`, na przykład „Kontynuuj.”. Serwer nie odszukuje wyniku po samym identyfikatorze artefaktu. Do planera trafia ostatnich 20 wiadomości; starszy zamiar jest podsumowywany w bieżącym żądaniu.

`confirm.required` zatrzymuje pracę przed utworzeniem lub usunięciem obserwacji, rejestracją webhooka, eksportem i krokiem za więcej niż 3 kredyty. Dopiero zgoda użytkownika pozwala odesłać `confirmation` z właściwym `step_id` i `approved: true` wraz z podpisaną kontynuacją. Eksport pozostaje niedostępny w obecnym publicznym interfejsie. Podpis nie czyni potwierdzenia jednorazowym: ponowna rejestracja webhooka rotuje sekret i zeruje kursor, dlatego nie odtwarzaj jej automatycznie.

Przy `client_tool.call` wykonaj zadeklarowaną czynność interfejsu i odeślij `client_results` z `call_id`, `result` oraz kontynuacją. Stan interfejsu jest niezaufany i nie stanowi dowodu o działce. Limity kontekstu i rozmiarów opisano w [limitach](rate-limits.md). Pytania, odpowiedzi i zawartość artefaktów nie są zapisywane jako rozmowa przez serwer; klient odpowiada za własną pamięć i retencję.

Limit czasu żądania Ask wynosi domyślnie 120 sekund. Operator może ustawić dodatnią, skończoną wartość `LANDFEED_ASK_REQUEST_SECONDS`. Oczekiwanie na dostawcę mieści się w pozostałym czasie. Otwarty strumień wysyła co 10 sekund komentarz `: keepalive`, również podczas oczekiwania na model. Komentarz nie jest zdarzeniem ani płatnym krokiem. Przekroczenie czasu kończy się `error` i `done`; niewykonany krok nie jest płatny. Zakończona praca zachowuje dotychczasowe rozliczenie.
