Skip to content

SSE Event protocol ​

Use POST /jobs/{name}/events to consume live progress, logs, artifacts, or Agent messages from a single Job invocation. Keep reading after connecting, stop when result arrives, and determine the outcome from its success field.

Determining completion of an event stream

Connection and framing ​

The request envelope and authentication match ordinary HTTP. A successful response has Content-Type text/event-stream. Each event's event line equals kind; data is a single-line JSON value, followed by a blank line:

text
event: progress
data: {"kind":"progress","name":"submitted","started_at":null,"finished_at":null,"percentage":100.0}

event: result
data: {"kind":"result","answer":{"task_id":"base#demo#example","run_id":"f5caee3a7b3c40849d0fb3bdc0f0cd23","task":"demo"},"success":true,"metadata":{}}

The example above acknowledges acceptance by the submit Job; it does not indicate that demo has completed successfully. Use stream_task to follow a background Task; its final result.answer is the terminal status.

bash
curl -N -X POST http://127.0.0.1:1024/jobs/stream_task/events \
  -H "Authorization: Bearer $AXONX_SERVICE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"arguments":{"task_id":"base#demo#example"}}'

Native EventSource only supports GET and cannot directly construct this protocol's POST request with Authorization. Browsers should use fetch and stream reading; Studio already implements the corresponding decoding.

Event types ​

progress ​

FieldType and defaultMeaning
kind"progress"Discriminator field
nameNonempty stringStep/progress name
started_at / finished_atdatetime or nullTimestamps; default null
percentagenumber or null0–100; default null

progress may update the same name multiple times. A single step's percentage is not the percentage of the entire research chain.

log ​

FieldType and defaultMeaning
contentstring, empty stringDecoded text
start_offset / next_offsetinteger, 0Byte offsets for the start of this chunk and the next read
file_sizeinteger, 0Known log file size
has_more_before / has_more_afterboolean, falseWhether more content exists before/after the window
resetboolean, falseLog reset indicator
channelstring, "task"Log channel

TaskLogChunk from the read API and log events share their main fields; log events additionally include kind/channel. Pagination uses byte offsets, rather than JavaScript character counts. When reset appears, reset local window state, then process the chunk using its offsets.

artifact ​

FieldType and defaultMeaning
pathstring, requiredArtifact path
sha256string, ""Checksum
sizeinteger, 0Size in bytes
media_typestring/null, nullMedia type
extraobject, {}Additional description

The protocol supports artifact events, but not every built-in Job emits them. Check the task's terminal status and metadata to determine whether an artifact is available.

agent_message ​

FieldTypeMeaning
session_idNonempty stringCurrent session
type_namestringBackend message type
messageobjectBackend message data
sequenceinteger ≥0Sequence number of the current event
presentationArray of AgentBlockPatch; default []Presentation block patches

Each patch contains operation, block_id, block_type, delta (default ""), and payload (default {}). block_type is thinking/text/tool/system/error; operation is:

operationClient behavior
startCreate a block for block_id and record its type and payload
appendAppend delta to the same block's text
replaceReplace the block with authoritative content; update presentation according to payload
finishFinish the block and retain its final content

The backend may send text deltas first and then a complete Assistant message, so replace is essential; appending all text again would duplicate it. presentation is AxonX's display projection, while message retains backend data. sequence is not a general cursor for resuming across requests, and block_id is not a message UUID.

result ​

result is JobResponse plus kind="result", containing answer, success, and metadata. The service closes the event source after sending the first result. On an exception, result may carry success=false and error text; if no result is produced, the service makes a best effort to send a failure result saying “Stream produced no terminal result”.

Python consumption ​

python
import asyncio
import os
from axonx.components.client import HttpClient
from axonx.components.job import ResultEvent

async def main():
    async with HttpClient(
        target="127.0.0.1:1024",
        token=os.environ["AXONX_SERVICE_TOKEN"],
        timeout=600.0,
    ) as client:
        async for event in client.stream_job(
            "stream_task", {"task_id": "base#demo#example"}
        ):
            if isinstance(event, ResultEvent):
                print(event.success, event.answer)
            else:
                print(event.model_dump(mode="json"))

asyncio.run(main())

HttpClient checks Content-Type, validates data against known event models, and raises RemoteServiceError if the stream disconnects without result.

Browser consumption ​

The following example demonstrates framing and terminal-result handling; production applications should also validate fields using Studio's event models and Agent reducer.

typescript
async function follow(token: string, taskId: string) {
  const response = await fetch("/jobs/stream_task/events", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ arguments: { task_id: taskId } }),
  });
  if (!response.ok || !response.body) throw new Error("Connection failed");
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let pending = "";
  let resultSeen = false;
  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      pending += decoder.decode(value, { stream: true });
      let boundary: number;
      while ((boundary = pending.indexOf("\n\n")) >= 0) {
        const frame = pending.slice(0, boundary);
        pending = pending.slice(boundary + 2);
        const data = frame
          .split("\n")
          .find((line) => line.startsWith("data: "));
        if (!data) continue;
        const event = JSON.parse(data.slice(6));
        if (event.kind === "result") {
          resultSeen = true;
          return event;
        }
        console.log(event);
      }
    }
    if (!resultSeen) throw new Error("No terminal result received");
  } finally {
    await reader.cancel();
    reader.releaseLock();
  }
}

Disconnection, cancellation, and retries ​

Closing the stream cancels execution or consumption of this Job invocation; it does not guarantee cancellation of an independent Task already started by submit. Use cancel to explicitly cancel a Task, and cancel_agent_turn to stop an Agent turn. Reissuing a request after disconnection may replay logs or rerun a Job; do not automatically retry Jobs that write data, install packages, or send messages.

The protocol provides no Last-Event-ID, global event ID, exactly-once guarantee, or general replay guarantee. next_offset from read_task_log supports continuing log-window reads; this does not mean every event can be resumed.

Implementation references: components/job/events.py, components/service/http/jobs.py, components/client/http.py, axonx_studio/src/shared/api/.

Agent-native quant research.