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 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.
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 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 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.