# Ask endpoint and SSE streaming

Call `POST /v1/ask` with an account API key. `Accept: text/event-stream` selects ask-v2. For JSON, set `version: ask-v2` to receive an `events` array and `continuation`. Plain JSON without conversation fields retains ask-v1 response semantics. The [operation reference](../../operations/ask.md) contains the full schema.

The pl-prod planner is `codex-app-server`, with an explicitly configured model and a dedicated service-owned CLI-managed ChatGPT login. The model plans validated tools; public API evidence supplies facts. Missing configuration/login refuses without charging. Anthropic is an optional API-key-only provider; subscription OAuth credentials are refused.

## Minimal streaming client

Save as `ask-stream.py`, set `LANDFEED_API_KEY`, and run `python3 ask-stream.py`. Only the Python standard library is needed. It never retries automatically. Pre-admission failures use JSON and an HTTP status rather than an SSE stream.

<!-- 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))
```


A frame has `event: name` and JSON `data:`, followed by a blank line. An accepted uninterrupted stream ends exactly once with `done`, even after `error`. A disconnect can lose that frame and does not cancel completed work. Do not buffer or publicly cache the authenticated stream.

## Events and settlement

| event | Meaning |
| --- | --- |
| `answer.citations` | Numbered citations with source/date. |
| `answer.delta` | Answer text after settlement. |
| `artifact` | Whole signed API result. |
| `budget.exceeded` | Unexecuted plan exceeds budget. |
| `client_tool.call` | Client UI action request. |
| `confirm.required` | Pause for user approval. |
| `done` | End and continuation; cost after settlement. |
| `error` | Failure; inspect settlement state. |
| `step.finished` | Finished step and settled cost. |
| `step.started` | Request and estimate before execution. |


See the [SSE event reference](../../sse-events.md) for every field and constraint. `step.started` exposes a public request and estimated cost before execution. `step.finished` exposes settled cost after the turn-level decision. `artifact` contains an exact public API body, not model-authored facts. New artifacts and factual answer text wait for settlement and are withheld on settlement failure.

Budget defaults to 5 credits, maximum 20. Reserve the whole budget before provider work. Public tools retain their public prices; a completed answer adds 1 credit. A budget-stopped ask-v2 turn is entirely free, including earlier reads; new artifacts and facts are withheld. Client-only and confirmation-only turns add no answer surcharge. `done.credits_total` is authoritative only after successful settlement; a zero following `error` is not proof of a refund.

## Stateless continuation

Keep `messages`, entire signed `artifact` objects and the latest `continuation` on the client. Resend them as `messages`, `results`, `continuation` with the next question. Signatures bind exact data and account: never edit artifacts or move them between accounts. Continuations expire after one hour. Resumes still need a nonempty `question`, for example „Kontynuuj.”. Artifact ids are references, not server-side storage lookups. Only the latest 20 messages enter the planner verbatim; older intent is summarized in the current request.

`confirm.required` pauses before watch creation/deletion, webhook registration, export or any public step above 3 credits. Obtain user approval, then resend the signed continuation with `confirmation {step_id, approved:true}` for that exact action. Public exports remain unavailable. Confirmation signatures are not one-use storage: replaying webhook registration rotates the secret and resets its cursor. Never replay it automatically.

For `client_tool.call`, execute the declared UI action and send `client_results` containing `call_id` and `result` with the continuation. Client results are untrusted UI state, never parcel evidence. See [limits](rate-limits.md) for context/body bounds. The server does not persist questions, answers or artifact payloads as conversations; clients own their own memory and retention.

The whole-request Ask budget defaults to 120 seconds; operators may set positive finite `LANDFEED_ASK_REQUEST_SECONDS`. Provider setup/turn waits are clamped to the remaining budget. While open, SSE sends `: keepalive` comments every 10 seconds, including silent model waits. Ignore comments; they are not events or billed steps. Request expiry sends `error` and then `done`; no unfinished public step is charged. Completed public work retains its existing settlement.
