Ask a coaching agent one question (SSE)
const url = 'http://localhost:8000/api/agents/ask/';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"agent":"rehab","subject":{"kind":"client","id":"3fa85f64-5717-4562-b3fc-2c963f66afa6"},"question":"Where is the asymmetry coming from?"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url http://localhost:8000/api/agents/ask/ \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "agent": "rehab", "subject": { "kind": "client", "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }, "question": "Where is the asymmetry coming from?" }'Server-sent events stream answering ONE question about one subject, in the caller’s stored conversation about it.
agent is a per-TURN choice. Continuing a thread_id with a different agent is allowed and expected — the next answer comes from the new persona in the SAME conversation, the way a chat client switches model — and the turn records which persona produced it. subject must still match the thread’s: a conversation IS about one athlete or squad, and a mismatch is 404.
The request carries a single question, not a history: the server persists the thread (GET /api/agents/thread/) and replays it into the prompt. That bounds the prompt and means a caller cannot forge assistant turns to steer the answer. Both turns are recorded — the question before the stream starts, so it survives a mid-stream disconnect.
subject.kind is team (the caller’s active team; subject.id is ignored) or client (an athlete on that team; subject.id required). The subject’s history is assembled server-side from the same aggregates the coach sees on the dashboard and client page — the request body never carries analysis data.
Frame sequence: event: meta ({mode, lang, agent, subject, thread} — agent is THIS turn’s persona) → event: tool / event: tool_done ({name, detail?, report?, share?, failed?} — which lookup ran and what it offered; tool additionally carries chart_kind when the lookup is a chart, so a client can reserve its space before the data lands), event: visual (ONE server-built chart payload — {kind, title, subtitle, unit, decimals?, labels, series, band?, reference?, max?}, where kind is one of trend|bars|radar and every value comes from a team-scoped query, never from the model; see DESIGN_AGENTS §D9), and event: token ({text} chunks, concatenate for the answer) → event: citations ({sources: [...]} — curated coaching-corpus section titles, empty when grounding is off) → event: done ({ok, thread}). A provider failure mid-answer yields event: warn and the answer ends with a short interruption note; with no model configured the stream returns a deterministic, data-grounded summary instead.
An invalid body yields a single event: error frame with HTTP 400. Rate-limited per user (this route is backed by a paid model); exceeding it returns 429. 403 means the team’s plan does not include agents; 404 means the surface is disabled for this deployment, or the subject is not on the caller’s team.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Query Parameters
Section titled “Query Parameters”Reply language (en / ru / es). Defaults to en.
Request Bodyrequired
Section titled “Request Bodyrequired”Body of POST /api/agents/ask/ — ONE question plus what it is about.
The conversation history is deliberately NOT in the body: the server stores
the thread and replays it (agents.threads). That bounds the prompt, and it
means a caller can’t forge “assistant” turns to steer the next answer.
object
What the conversation is scoped to: the caller’s whole team, or one
athlete on it. id is the Client id and is required only for
kind="client" — a team subject is always the caller’s active team, never
an id from the body (that would be a tenancy hole).
object
team- teamclient- client
Examples
Ask the rehab agent about one athlete
{ "agent": "rehab", "subject": { "kind": "client", "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }, "question": "Where is the asymmetry coming from?"}Ask the performance agent about the squad
{ "agent": "performance", "subject": { "kind": "team" }, "question": "What should the squad focus on next?"}Body of POST /api/agents/ask/ — ONE question plus what it is about.
The conversation history is deliberately NOT in the body: the server stores
the thread and replays it (agents.threads). That bounds the prompt, and it
means a caller can’t forge “assistant” turns to steer the next answer.
object
What the conversation is scoped to: the caller’s whole team, or one
athlete on it. id is the Client id and is required only for
kind="client" — a team subject is always the caller’s active team, never
an id from the body (that would be a tenancy hole).
object
team- teamclient- client
Body of POST /api/agents/ask/ — ONE question plus what it is about.
The conversation history is deliberately NOT in the body: the server stores
the thread and replays it (agents.threads). That bounds the prompt, and it
means a caller can’t forge “assistant” turns to steer the next answer.
object
What the conversation is scoped to: the caller’s whole team, or one
athlete on it. id is the Client id and is required only for
kind="client" — a team subject is always the caller’s active team, never
an id from the body (that would be a tenancy hole).
object
team- teamclient- client
Responses
Section titled “Responses”text/event-stream of agent-answer frames (see description).
object
Human-readable message, or a stable machine code for the cases a client branches on. The standard envelope for 400 (validation — a field-keyed object may appear instead), 401 (missing / invalid credentials), 403 (authenticated but not permitted), and 404 (absent — cross-team records are collapsed to 404 so the API never leaks the existence of another team’s data).
Examplegenerated
{ "detail": "example"}object
Human-readable message, or a stable machine code for the cases a client branches on. The standard envelope for 400 (validation — a field-keyed object may appear instead), 401 (missing / invalid credentials), 403 (authenticated but not permitted), and 404 (absent — cross-team records are collapsed to 404 so the API never leaks the existence of another team’s data).
Examplegenerated
{ "detail": "example"}The caller’s tier does not include the coaching agents.
object
One of five canonical strings. The front-end paywall switches copy + CTA on this value (exact string equality — do not localise). quota_exhausted / seat_limit_reached / client_limit_reached / feature_not_in_plan lead to an upgrade CTA; subscription_suspended leads to a Billing Portal CTA so the buyer can update their payment method.
quota_exhausted- Session quota for the period reached 0subscription_suspended- Stripe dunning in flight (past_due / unpaid)seat_limit_reached- Adding another team member would exceed plan.max_seatsclient_limit_reached- Adding another Client would exceed plan.max_client_profilesfeature_not_in_plan- The tier does not include this whole feature surface
ISO-8601 UTC timestamp the team’s current period ends. Null when the team has no live subscription (legacy un-migrated row). On Free + paid this is the renewal moment; quota refreshes to exactly plan.session_quota (NOT additive — leftover sessions don’t roll over).
The active plan’s role string (free, starter, professional, organization, strategic). Same value as current_role — kept as two separate fields for forward-compatibility with a future named-plan split where current_plan could carry an SKU and current_role the tier label.
The active plan’s role string. See current_plan.
Which feature surface is locked. Present ONLY on feature_not_in_plan, so one modal can name what’s gated without a detail string per feature (#773). The only value today is agents (aikynetix/agents/services.py::AGENTS_FEATURE).
Examples
Free team opens an agent conversation
{ "detail": "feature_not_in_plan", "reset_at": null, "current_plan": "free", "current_role": "free", "feature": "agents"}object
Human-readable message, or a stable machine code for the cases a client branches on. The standard envelope for 400 (validation — a field-keyed object may appear instead), 401 (missing / invalid credentials), 403 (authenticated but not permitted), and 404 (absent — cross-team records are collapsed to 404 so the API never leaks the existence of another team’s data).
Examplegenerated
{ "detail": "example"}object
Human-readable message, or a stable machine code for the cases a client branches on. The standard envelope for 400 (validation — a field-keyed object may appear instead), 401 (missing / invalid credentials), 403 (authenticated but not permitted), and 404 (absent — cross-team records are collapsed to 404 so the API never leaks the existence of another team’s data).
Examplegenerated
{ "detail": "example"}object
429 — rate limit exceeded. Honour the Retry-After response header (seconds to wait) before retrying; it is sent on every 429.
Examplegenerated
{ "detail": "example"}