diff --git a/openapi.yml b/openapi.yml
index 909544c..03b5000 100644
--- a/openapi.yml
+++ b/openapi.yml
@@ -12,6 +12,16 @@ info:
- **Evaluate Task**: automatic feedback and grading for student submissions.
- **Chat**: conversational interactions around tasks, submissions, or general
learning questions.
+
+ **Progress streaming.** Any operation MAY support an opt-in Server-Sent
+ Events progress stream, selected per request with the
+ `Accept: text/event-stream` header. The stream is a sequence of
+ `SseProgressStep` frames, optional keep-alive comments, and exactly one
+ terminal frame carrying the operation's normal `200` body (on success) or an
+ `ErrorResponse` (on failure); the HTTP status stays `200` throughout.
+ Services advertise support via `supportsStreaming` in their `/health`
+ capabilities. This is distinct from `configuration.llm.stream`, which
+ governs token-level streaming from the LLM provider.
tags:
- name: evaluate
diff --git a/paths/chat/chat-health.yml b/paths/chat/chat-health.yml
index 4d3ec90..8401390 100644
--- a/paths/chat/chat-health.yml
+++ b/paths/chat/chat-health.yml
@@ -37,6 +37,12 @@ get:
supportsChat: true
supportsUserPreferences: true
supportsStreaming: true
+ supportedProgressStages:
+ - "preparing"
+ - "starting"
+ - "thinking"
+ - "completed"
+ - "failed"
"supportsDataPolicy": "NOT_SUPPORTED"
supportedLanguages:
- "en"
diff --git a/paths/chat/chat.yml b/paths/chat/chat.yml
index ff3dd0c..a475199 100644
--- a/paths/chat/chat.yml
+++ b/paths/chat/chat.yml
@@ -139,7 +139,35 @@ post:
temperature: 0.5
responses:
"200":
- description: Successful chat response.
+ description: >
+ Successful chat response.
+
+
+ ### Streaming variant (opt-in)
+
+
+ If the request sends `Accept: text/event-stream`, the response is a
+ Server-Sent Events progress stream instead of a single JSON body. The
+ HTTP status stays `200` for the entire stream, including failures. It
+ consists of:
+
+
+ - zero or more progress frames — the SSE `event:` field is the stage
+ name and `data:` is an `SseProgressStep` JSON object;
+
+ - optional keep-alive comment lines (each starting with `:`);
+
+ - exactly one terminal frame — `event: completed` with `data:` an
+ `SseChatTerminalFrame` (the `200` body as `output` / `metadata`, plus
+ a `steps` replay), or `event: failed` with `data:` an
+ `SseChatTerminalFrame` whose `error` is an `ErrorResponse`.
+
+
+ Failures are always delivered as a terminal `failed` frame, never as an
+ HTTP error status. `X-Request-Id` and `X-Api-Version` are sent once, as
+ response headers, when the stream opens. This is independent of
+ `configuration.llm.stream`, which governs token-level streaming from the
+ LLM provider.
headers:
X-Request-Id:
description: Request id for tracing this request across services.
@@ -213,6 +241,40 @@ post:
model: "gpt-5.2"
temperature: 0.5
outputTokens: 143
+ text/event-stream:
+ schema:
+ $ref: "./schemas/SseChatTerminalFrame.yml"
+ examples:
+ completedFrame:
+ summary: >
+ Terminal `event: completed` frame `data` (preceded on the wire by
+ `event: preparing` / `event: thinking` progress frames whose
+ `data` is an SseProgressStep, and optional `:` keep-alive lines).
+ value:
+ output:
+ role: ASSISTANT
+ content: "Polymorphism lets one interface represent many underlying forms."
+ metadata:
+ responseTimeMs: 1800
+ steps:
+ - stage: "preparing"
+ message: "Preparing worker"
+ timestamp: "2025-12-10T11:15:00Z"
+ - stage: "thinking"
+ message: "Composing response"
+ timestamp: "2025-12-10T11:15:02Z"
+ failedFrame:
+ summary: "Terminal `event: failed` frame `data` (HTTP status is still 200)."
+ value:
+ output: null
+ error:
+ title: "Chat failed"
+ message: "The LLM provider connection failed."
+ code: "LLM_PROVIDER_ERROR"
+ steps:
+ - stage: "preparing"
+ message: "Preparing worker"
+ timestamp: "2025-12-10T11:15:00Z"
"400":
$ref: "./responses/400-BadRequest.yml"
"403":
diff --git a/paths/chat/schemas/ChatCapabilities.yml b/paths/chat/schemas/ChatCapabilities.yml
index 08cf660..d8ffbb7 100644
--- a/paths/chat/schemas/ChatCapabilities.yml
+++ b/paths/chat/schemas/ChatCapabilities.yml
@@ -4,6 +4,8 @@ required:
- supportsChat
- supportsDataPolicy
additionalProperties: true
+allOf:
+ - $ref: "../../shared/schemas/StreamingCapabilities.yml"
properties:
supportsChat:
type: boolean
@@ -11,9 +13,6 @@ properties:
supportsUserPreferences:
type: boolean
description: Indicates whether the service supports adapting to user preferences.
- supportsStreaming:
- type: boolean
- description: Indicates whether the service supports streaming responses.
supportsDataPolicy:
$ref: "../../shared/enums/DataPolicySupport.yml"
supportedLanguages:
diff --git a/paths/chat/schemas/SseChatTerminalFrame.yml b/paths/chat/schemas/SseChatTerminalFrame.yml
new file mode 100644
index 0000000..a852b6a
--- /dev/null
+++ b/paths/chat/schemas/SseChatTerminalFrame.yml
@@ -0,0 +1,36 @@
+type: object
+description: >
+ Data payload of the single terminal frame of a `POST /chat` SSE progress
+ stream.
+
+ On `event: completed`, `output` / `metadata` hold exactly the endpoint's
+ normal `200 application/json` body (`ChatResponse`) and `error` is absent.
+
+ On `event: failed`, `output` is null and `error` holds an `ErrorResponse` with
+ the same `title` / `code` / `details` shape clients already handle for `400` /
+ `500` responses. The HTTP status stays `200` regardless.
+
+ `steps` is always present: the ordered replay of every progress step emitted
+ during the stream.
+allOf:
+ - $ref: "../../shared/schemas/SseTerminalSteps.yml"
+ - type: object
+ properties:
+ output:
+ description: The generated assistant response; null on a `failed` frame.
+ anyOf:
+ - $ref: "./Message.yml"
+ - type: "null"
+ metadata:
+ type:
+ - object
+ - "null"
+ description: Optional metadata about response generation.
+ additionalProperties: true
+ error:
+ type:
+ - object
+ - "null"
+ description: Present only on a `failed` frame.
+ allOf:
+ - $ref: "../../shared/schemas/ErrorResponse.yml"
diff --git a/paths/evaluate/evaluate-health.yml b/paths/evaluate/evaluate-health.yml
index 36cacdc..e224d37 100644
--- a/paths/evaluate/evaluate-health.yml
+++ b/paths/evaluate/evaluate-health.yml
@@ -40,6 +40,13 @@ get:
supportsFormativeFeedback: true
supportsSummativeFeedback: true
supportsDataPolicy: "PARTIAL"
+ supportsStreaming: true
+ supportedProgressStages:
+ - "preparing"
+ - "starting"
+ - "evaluating"
+ - "completed"
+ - "failed"
supportedArtefactProfiles:
- type: "TEXT"
supportedFormats: ["plain", "markdown"]
diff --git a/paths/evaluate/evaluate.yml b/paths/evaluate/evaluate.yml
index 77818a0..57d2ff6 100644
--- a/paths/evaluate/evaluate.yml
+++ b/paths/evaluate/evaluate.yml
@@ -339,7 +339,35 @@ post:
version: 1
responses:
"200":
- description: Successfully generated feedback.
+ description: >
+ Successfully generated feedback.
+
+
+ ### Streaming variant (opt-in)
+
+
+ If the request sends `Accept: text/event-stream`, the response is a
+ Server-Sent Events progress stream instead of a single JSON body. The
+ HTTP status stays `200` for the entire stream, including failures. It
+ consists of:
+
+
+ - zero or more progress frames — the SSE `event:` field is the stage
+ name and `data:` is an `SseProgressStep` JSON object;
+
+ - optional keep-alive comment lines (each starting with `:`);
+
+ - exactly one terminal frame — `event: completed` with `data:` an
+ `SseEvaluateTerminalFrame` (the `200` body under `feedback`, plus a
+ `steps` replay), or `event: failed` with `data:` an
+ `SseEvaluateTerminalFrame` whose `error` is an `ErrorResponse`.
+
+
+ Failures are always delivered as a terminal `failed` frame, never as an
+ HTTP error status. `X-Request-Id` and `X-Api-Version` are sent once, as
+ response headers, when the stream opens. This is independent of
+ `configuration.llm.stream`, which governs token-level streaming from the
+ LLM provider.
headers:
X-Request-Id:
description: Request id for tracing this request across services.
@@ -352,9 +380,7 @@ post:
content:
application/json:
schema:
- type: array
- items:
- $ref: "./schemas/Feedback.yml"
+ $ref: "./schemas/EvaluateResponse.yml"
examples:
exampleResponse:
summary: Example feedback response
@@ -379,6 +405,40 @@ post:
- feedbackId: "fb-2"
title: "Overall structure"
message: "The overall structure of your answer is clear and easy to follow."
+ text/event-stream:
+ schema:
+ $ref: "./schemas/SseEvaluateTerminalFrame.yml"
+ examples:
+ completedFrame:
+ summary: >
+ Terminal `event: completed` frame `data` (preceded on the wire by
+ `event: preparing` / `event: evaluating` progress frames whose
+ `data` is an SseProgressStep, and optional `:` keep-alive lines).
+ value:
+ feedback:
+ - feedbackId: "fb-1"
+ title: "Clarify your definition"
+ message: "Distinguish subtype from parametric polymorphism."
+ awardedPoints: 2.5
+ steps:
+ - stage: "preparing"
+ message: "Preparing worker"
+ timestamp: "2025-12-16T09:30:01Z"
+ - stage: "evaluating"
+ message: "Ran 3/10 checks"
+ timestamp: "2025-12-16T09:30:04Z"
+ failedFrame:
+ summary: "Terminal `event: failed` frame `data` (HTTP status is still 200)."
+ value:
+ feedback: null
+ error:
+ title: "Evaluation failed"
+ message: "The worker exited before returning feedback."
+ code: "LLM_PROVIDER_ERROR"
+ steps:
+ - stage: "preparing"
+ message: "Preparing worker"
+ timestamp: "2025-12-16T09:30:01Z"
"202":
$ref: "./responses/202-Accepted.yml"
"400":
diff --git a/paths/evaluate/schemas/EvaluateCapabilities.yml b/paths/evaluate/schemas/EvaluateCapabilities.yml
index 92796e9..cb7d3b5 100644
--- a/paths/evaluate/schemas/EvaluateCapabilities.yml
+++ b/paths/evaluate/schemas/EvaluateCapabilities.yml
@@ -7,6 +7,8 @@ required:
- supportsSummativeFeedback
- supportsDataPolicy
additionalProperties: true
+allOf:
+ - $ref: "../../shared/schemas/StreamingCapabilities.yml"
properties:
supportsEvaluate:
type: boolean
diff --git a/paths/evaluate/schemas/EvaluateResponse.yml b/paths/evaluate/schemas/EvaluateResponse.yml
new file mode 100644
index 0000000..0303b1a
--- /dev/null
+++ b/paths/evaluate/schemas/EvaluateResponse.yml
@@ -0,0 +1,4 @@
+type: array
+description: Ordered list of feedback items generated for the submission.
+items:
+ $ref: "./Feedback.yml"
diff --git a/paths/evaluate/schemas/SseEvaluateTerminalFrame.yml b/paths/evaluate/schemas/SseEvaluateTerminalFrame.yml
new file mode 100644
index 0000000..8319adc
--- /dev/null
+++ b/paths/evaluate/schemas/SseEvaluateTerminalFrame.yml
@@ -0,0 +1,32 @@
+type: object
+description: >
+ Data payload of the single terminal frame of a `POST /evaluate` SSE progress
+ stream.
+
+ On `event: completed`, `feedback` holds exactly the endpoint's normal
+ `200 application/json` body and `error` is absent.
+
+ On `event: failed`, `feedback` is null and `error` holds an `ErrorResponse`
+ with the same `title` / `code` / `details` shape clients already handle for
+ `400` / `500` responses. The HTTP status stays `200` regardless.
+
+ `steps` is always present: the ordered replay of every progress step emitted
+ during the stream.
+allOf:
+ - $ref: "../../shared/schemas/SseTerminalSteps.yml"
+ - type: object
+ properties:
+ feedback:
+ type:
+ - array
+ - "null"
+ description: The generated feedback items; null on a `failed` frame.
+ items:
+ $ref: "./Feedback.yml"
+ error:
+ type:
+ - object
+ - "null"
+ description: Present only on a `failed` frame.
+ allOf:
+ - $ref: "../../shared/schemas/ErrorResponse.yml"
diff --git a/paths/shared/schemas/LLMConfiguration.yml b/paths/shared/schemas/LLMConfiguration.yml
index 7829e1e..0d07dea 100644
--- a/paths/shared/schemas/LLMConfiguration.yml
+++ b/paths/shared/schemas/LLMConfiguration.yml
@@ -23,7 +23,12 @@ properties:
type:
- boolean
- "null"
- description: Optional flag indicating whether streaming responses are requested.
+ description: >
+ Optional flag requesting token-level streaming from the underlying LLM
+ provider. This is distinct from API-layer progress streaming, which is
+ opt-in per request via the `Accept: text/event-stream` header and is
+ advertised by `supportsStreaming` in a service's `/health` capabilities.
+ A service MAY support either, both, or neither.
credentials:
type:
- object
diff --git a/paths/shared/schemas/SseProgressStep.yml b/paths/shared/schemas/SseProgressStep.yml
new file mode 100644
index 0000000..6acce1b
--- /dev/null
+++ b/paths/shared/schemas/SseProgressStep.yml
@@ -0,0 +1,30 @@
+type: object
+description: >
+ A single progress step emitted while an operation runs. Carried as the SSE
+ `data:` payload of an intermediate progress frame (the SSE `event:` field
+ carries the stage name), and replayed in the terminal frame's `steps` array.
+additionalProperties: true
+required:
+ - stage
+ - timestamp
+properties:
+ stage:
+ type: string
+ description: >
+ Lifecycle stage this step reports. Informative, not a fixed enum:
+ implementations MAY introduce new stages without a spec change, and clients
+ MUST tolerate values they do not recognise. Each service advertises the
+ stage values it emits via `supportedProgressStages` in its `/health`
+ capabilities. Common values: "preparing", "starting", "evaluating"
+ (evaluate), "thinking" (chat), and the terminal "completed" / "failed".
+ message:
+ type:
+ - string
+ - "null"
+ description: >
+ Short, human-readable description of the step, suitable for display to a
+ learner or teacher.
+ timestamp:
+ type: string
+ format: date-time
+ description: Time this step was generated (RFC 3339 / ISO 8601).
diff --git a/paths/shared/schemas/SseTerminalSteps.yml b/paths/shared/schemas/SseTerminalSteps.yml
new file mode 100644
index 0000000..9a9240d
--- /dev/null
+++ b/paths/shared/schemas/SseTerminalSteps.yml
@@ -0,0 +1,13 @@
+type: object
+description: >
+ Shared fragment for the terminal SSE frame: the ordered replay of every
+ progress step emitted during the stream, so a client that connected late or
+ dropped intermediate frames still receives the full trace.
+required:
+ - steps
+properties:
+ steps:
+ type: array
+ description: Ordered list of every SseProgressStep emitted during the stream.
+ items:
+ $ref: "./SseProgressStep.yml"
diff --git a/paths/shared/schemas/StreamingCapabilities.yml b/paths/shared/schemas/StreamingCapabilities.yml
new file mode 100644
index 0000000..54422fd
--- /dev/null
+++ b/paths/shared/schemas/StreamingCapabilities.yml
@@ -0,0 +1,26 @@
+type: object
+description: >
+ Shared capability fragment describing an operation's support for opt-in
+ Server-Sent Events (SSE) progress streaming. Composed via `allOf` into each
+ service's capabilities schema so every streaming-capable endpoint advertises
+ streaming the same way.
+additionalProperties: true
+properties:
+ supportsStreaming:
+ type: boolean
+ description: >
+ Indicates whether this operation supports opt-in SSE progress streaming,
+ selected per request with the `Accept: text/event-stream` header. This is
+ distinct from `configuration.llm.stream`, which governs token-level
+ streaming from the underlying LLM provider. A service MAY support either,
+ both, or neither.
+ supportedProgressStages:
+ type:
+ - array
+ - "null"
+ description: >
+ Optional list of `SseProgressStep.stage` values this service may emit
+ (e.g. ["preparing", "starting", "evaluating", "completed", "failed"]).
+ Informative only; clients MUST tolerate stages that are not listed.
+ items:
+ type: string