Gateway Stream Events

Live progress events the gateway forwards over WebSocket after a client negotiates streaming

The gateway relays ten structured progress events to WebSocket clients once they negotiate the streaming capability, so UIs can paint reasoning, tool progress, model fallbacks, retries, live todos, tool results, and the final answer as they arrive.

python
from praisonaiagents import Agent

agent = Agent(name="stream-agent", instructions="Answer while streaming reasoning and tool progress.")
agent.start("Look up the weather in Paris and explain your reasoning.")

The agent emits token, reasoning, and tool events; the gateway forwards each to the WS client, which distinguishes a provisional accepted ack from the final answer.

Diagram

token_stream

reasoning_stream

tool_call_stream

tool_progress_stream

model_fallback_stream

retry_stream

todo_stream

tool_result_stream

stream_error

stream_end

👤 User

🧠 Agent

🗼 Gateway

🔌 WS Client

Quick Start

  1. Open a streaming client
    python
    import asyncio
    from praisonai.gateway import GatewayClient
    
    async def main():
        client = GatewayClient(
            url="ws://localhost:8765",
            agent_id="assistant",
            capabilities=["streaming"],   # advertise streaming in hello
        )
        await client.connect()
    
    asyncio.run(main())
  2. Print each event type
    python
    import asyncio
    from praisonai.gateway import GatewayClient
    
    async def main():
        client = GatewayClient(
            url="ws://localhost:8765",
            agent_id="assistant",
            capabilities=["streaming"],
        )
        await client.connect()
    
        async for event in client.events():
            if event.type == "reasoning_stream":
                print("[reasoning]", event.data)
            elif event.type == "tool_progress_stream":
                print("[tool]", event.data)
            elif event.type == "model_fallback_stream":
                md = event.data["metadata"]
                print(f"[fallback] {md['from_model']} → {md['to_model']} ({md['reason_category']})")
            elif event.type == "retry_stream":
                md = event.data["metadata"]
                print(f"[retry] attempt {md['attempt']}/{md['max_attempts']} in {md['delay']}s")
            elif event.type == "todo_stream":
                print("[todos]", event.data["metadata"]["todos"])
            elif event.type == "tool_result_stream":
                print("[result]", event.data["tool_call"])
            elif event.type == "stream_error":
                print("[error]", event.data)
            elif event.type == "stream_end":
                print("[done]")
            else:
                print(event.type, event.data)
    
    asyncio.run(main())

Event Vocabulary

Each streaming event maps to an EventType in praisonaiagents.gateway.protocols and is advertised in hello_ok.features["events"] when the client negotiates streaming.

TypeScript SDK users: the equivalent typed enum is GatewayEventType in praisonai-ts/src/gateway, kept in lockstep with Python.

EventWire typePayloadWhen it fires
TOKEN_STREAMtoken_streamtext deltaLLM token deltas
TOOL_CALL_STREAMtool_call_streamtool call frameTool call streamed to client
REASONING_STREAMreasoning_streamDELTA_TEXT with is_reasoning=TrueModel emits reasoning/thinking
TOOL_PROGRESS_STREAMtool_progress_streamtool progress frameTool reports in-flight progress
MODEL_FALLBACK_STREAMmodel_fallback_streammetadata: { from_model, to_model, reason_category, fallback_index }Primary model failed; agent switched to a backup
RETRY_STREAMretry_streammetadata: { attempt, max_attempts, delay, reason }Backing off before retrying a rate-limited / transient failure
TODO_STREAMtodo_streammetadata: { todos: [...] } — full ordered listAgent updates its plan / todo list
TOOL_RESULT_STREAMtool_result_streamtool_call: {...}, metadata: {...}A tool execution completed with a result
STREAM_ERRORstream_errorerror frameStreaming pipeline hits an error
STREAM_ENDstream_endterminal markerStream completes

What each new event means to a live UI

  • model_fallback_stream — the primary model went down and the agent switched to a backup mid-turn. Render a “switched to backup model” banner instead of leaving the token stream frozen. metadata.from_model is the model that failed, metadata.to_model is now serving the turn, metadata.reason_category classifies the failure (rate_limit, unavailable, error), and metadata.fallback_index is the 0-based position in the fallback chain.
  • retry_stream — the agent is backing off before retrying a rate-limited or transient failure. Render a countdown (“retrying in 3s… attempt 2/5”) so the pause reads as progress, not a freeze. metadata.attempt / metadata.max_attempts drive the counter, metadata.delay is seconds until the next attempt, and metadata.reason is a short human string.
  • todo_stream — the agent mutated its plan. metadata.todos is the full ordered list (each item carries its status and text), so re-render the whole checklist from it.
  • tool_result_stream — a tool finished with a result. tool_call carries the name, args, id, and result — render a tool result card; metadata holds any extra context.

Response Frame States

Every WebSocket response frame carries a status: a provisional accepted ack arrives first, then the real answer as final with a structured outcome.

Diagram

📨 accepted
provisional ack

✅ final
outcome: ok/error/rejected

statusoutcome present?MeaningClient action
acceptednoProvisional ack (“queued”)Show spinner / typing indicator
finalyes (ok \error \rejected); outcome.error is added when outcome.status == "error"Real answer with completion stateRender text + reflect outcome

Typed outcome.error on failed turns

A failed turn adds a machine-readable error object to outcome.

json
{
  "type": "response",
  "status": "final",
  "content": "Error: ...",
  "outcome": {
    "status": "error",
    "error": {
      "code": "rate_limit",
      "retryable": true,
      "retry_after_seconds": 30,
      "message": "Rate limited by the model provider. Please retry shortly."
    }
  }
}
FieldMeaning
codeError class, e.g. rate_limit, context_overflow, billing
retryabletrue when resending can succeed
retry_after_secondsProvider’s wait, or null
messageFixed safe text, never raw str(e)

See Gateway Turn Error Codes for the full code table.


User Interaction Flow

A streaming UI paints each event as it arrives — reasoning bubble, then a tool progress chip, then the final answer.

Diagram
AgentGatewayUIUserAgentGatewayUIUsersend messagemessageresponse (status=accepted)show spinnerreasoning deltareasoning_streampaint reasoning bubbletool progresstool_progress_streamshow tool chipstream_endresponse (status=final, outcome)render answer, clear chip

When the primary model fails mid-turn, model_fallback_stream and retry_stream fill the visible-progress gap so the token stream never looks frozen.

Diagram
AgentGatewayUIUserAgentGatewayUIUsertoken deltastoken_streampaint tokensprimary model downmodel_fallback_stream (gpt-4o → claude)“switched to backup model” bannerrate limited, backing offretry_stream (attempt 2/5, delay 3s)“retrying in 3s…” countdowntoken deltas resumetoken_streamtokens resume

Common Patterns

Reasoning Inline with Answer

python
import asyncio
from praisonai.gateway import GatewayClient

async def main():
    client = GatewayClient(url="ws://localhost:8765", agent_id="assistant", capabilities=["streaming"])
    await client.connect()

    reasoning, answer = [], []
    async for event in client.events():
        if event.type == "reasoning_stream":
            reasoning.append(event.data.get("text", ""))
        elif event.type == "token_stream":
            answer.append(event.data.get("text", ""))
        elif event.type == "stream_end":
            print("Reasoning:", "".join(reasoning))
            print("Answer:", "".join(answer))
            break

asyncio.run(main())

Tool Progress Chip

python
import asyncio
from praisonai.gateway import GatewayClient

async def main():
    client = GatewayClient(url="ws://localhost:8765", agent_id="assistant", capabilities=["streaming"])
    await client.connect()

    async for event in client.events():
        if event.type == "tool_progress_stream":
            show_chip(event.data)          # display in-flight tool progress
        elif event.type == "stream_end":
            clear_chip()                   # remove the chip when the stream ends
            break

asyncio.run(main())

Backup-Model Banner

python
import asyncio
from praisonai.gateway import GatewayClient

async def main():
    client = GatewayClient(url="ws://localhost:8765", agent_id="assistant", capabilities=["streaming"])
    await client.connect()

    async for event in client.events():
        if event.type == "model_fallback_stream":
            md = event.data["metadata"]
            show_banner(f"Switched to {md['to_model']} ({md['reason_category']})")
        elif event.type == "stream_end":
            break

asyncio.run(main())

Live Todo / Plan Progress

python
import asyncio
from praisonai.gateway import GatewayClient

async def main():
    client = GatewayClient(url="ws://localhost:8765", agent_id="assistant", capabilities=["streaming"])
    await client.connect()

    async for event in client.events():
        if event.type == "todo_stream":
            render_checklist(event.data["metadata"]["todos"])  # full ordered list
        elif event.type == "stream_end":
            break

asyncio.run(main())

Best Practices

Gate rendering on client.supports_event(…)

Wrap new-event rendering in client.supports_event("reasoning_stream") and client.supports_event("tool_progress_stream") so legacy gateways — which never advertise these events — degrade silently instead of breaking.

Treat stream_error as a soft signal

A stream_error frame does not end the run. The gateway may still deliver a final response with outcome.status="error". Surface the error but keep listening until stream_end / final.

Never assume type: response means done

Both the provisional ack and the real answer share type: "response". Branch on status: show a spinner on accepted, render only on final, and read outcome.status for the completion state.


Related