Skip to content

Streaming ​

Real-time response streaming with SSE support.

Overview ​

Chat UI supports Server-Sent Events (SSE) for real-time streaming of AI responses. This provides a better user experience as users see the response as it's generated.

Enable Streaming ​

Streaming is enabled by default:

tsx
<Chat
  baseUrl="/api/agents"
  agentId="assistant"
  streaming={true}  // Default
/>

To disable streaming:

tsx
<Chat
  baseUrl="/api/agents"
  agentId="assistant"
  streaming={false}
/>

Stream Events ​

The chat handles these SSE events:

Text Streaming ​

tsx
<Chat
  baseUrl="/api/agents"
  agentId="assistant"
  onStreamText={(chunk, fullText) => {
    console.log("Chunk:", chunk);
    console.log("Full text so far:", fullText);
  }}
/>

Tool Calls ​

tsx
<Chat
  baseUrl="/api/agents"
  agentId="assistant"
  onToolCall={(toolName, args) => {
    console.log("Tool called:", toolName);
    console.log("Arguments:", args);
  }}
  onToolResult={(toolName, result) => {
    console.log("Tool result:", toolName, result);
  }}
/>

Stream UI Behavior ​

While streaming:

  1. Typing indicator - Shows while waiting for response
  2. Incremental text - Text appears as it's received
  3. Tool calls - Displayed inline as they occur
  4. Stop button - Stops the turn (on agent-server 0.3.0+ the run itself, see Running turns)

A stream is not the run ​

On @cognipeer/agent-server 0.3.0 or later a streamed request is only a subscriber of the run that executes the turn. If the connection drops, the page does not report a failure: the turn goes on without it, so the page looks at the conversation again and follows what is still running (GET /conversations/:id/events/stream) or shows what finished. Every stream.* frame carries seq, the position in the conversation's event log, and stream.done carries stopReason (end_turn, cancelled, limit, budget_reached, ...), runId and - for a conversation with a usage limit - budget and spentUsd. A stopped answer gets a notice, see Running turns.

Stop Streaming ​

Users can stop the stream using the stop button, or programmatically. With a transport that can cancel (AgentServerClient) stop() stops the run on the server, closes the stream and keeps the text received so far as an answer marked stopped; otherwise it only closes the connection:

tsx
import { useChat } from "@cognipeer/chat-ui";

function CustomChat() {
  const { isLoading, stop, sendMessage } = useChat({
    baseUrl: "/api/agents",
    agentId: "assistant",
  });

  return (
    <div>
      {isLoading && (
        <button onClick={stop}>
          Stop Generation
        </button>
      )}
    </div>
  );
}

Streaming State ​

Track streaming state with hooks:

tsx
import { useChat } from "@cognipeer/chat-ui";

function CustomChat() {
  const {
    isLoading,      // True while streaming
    streamingText,  // Current accumulated text
    activeToolCalls,// Map of active tool calls
  } = useChat({
    baseUrl: "/api/agents",
    agentId: "assistant",
  });

  return (
    <div>
      {isLoading && <div>Generating...</div>}
      {streamingText && <div>Current: {streamingText}</div>}
    </div>
  );
}

Error Handling ​

Handle stream errors:

tsx
<Chat
  baseUrl="/api/agents"
  agentId="assistant"
  onError={(error) => {
    console.error("Stream error:", error);
    // Show error notification
  }}
/>

Retry Failed Requests ​

tsx
import { useChat } from "@cognipeer/chat-ui";

function CustomChat() {
  const { error, retry } = useChat({
    baseUrl: "/api/agents",
    agentId: "assistant",
  });

  return (
    <div>
      {error && (
        <div>
          <p>Error: {error.message}</p>
          <button onClick={retry}>Retry</button>
        </div>
      )}
    </div>
  );
}

Stream events ​

The streaming methods read Server-Sent Events whose data is a JSON StreamEvent. Every frame may carry seq, its position in the conversation's event log (agent-server 0.3.0+).

EventuseChat showsStreamCallbacks
stream.startStarts the live turnonStart
stream.textAppends to the streaming textonText(chunk, fullText)
stream.thinkingTreated as textonText
stream.tool_callA tool row appearsonToolCall
stream.tool_resultThe row finishesonToolResult
stream.progressThe progress lineonProgress
stream.action_requiredApproval and question cardsonActionRequired
stream.action_resolvedThe card settles (also when answered from another app)onActionResolved
stream.errorAn error banner; its code selects a localized messageonError (the error carries code)
stream.doneThe final message, with citations, files, title, usage, status, pendingActions, stopReason, runId, budget, spentUsd, warningsonDone

Custom Stream Handler ​

For custom implementations, use the API client directly. Events arrive through callbacks:

tsx
import { AgentServerClient } from "@cognipeer/chat-ui";

const client = new AgentServerClient({ baseUrl: "/api/agents" });

async function streamMessage(conversationId: string, message: string, signal?: AbortSignal) {
  await client.sendMessageStream(
    conversationId,
    { message },
    {
      onStart: () => console.log("Stream started"),
      onText: (chunk, fullText) => console.log("Text so far:", fullText),
      onToolCall: (event) => console.log("Tool:", event.type === "stream.tool_call" && event.toolName),
      onError: (error) => console.error(error.message, (error as { code?: string }).code),
      onDone: (event) => console.log("Done:", event.type === "stream.done" && event.stopReason),
    },
    signal
  );
}

With streaming={false} (or sendMessage instead of sendMessageStream) the server answers with one JSON response: { message, response, status?, pendingActions?, stopReason?, budget?, spentUsd?, warnings? }.

Performance and proxies ​

  • The built-in ChatMessageList always draws the live turn below the stored messages, so a continuation (for example Continue after a usage limit) never replaces the answer it carries on.
  • A proxy or CDN that buffers text/event-stream makes the text arrive in one piece at the end: turn response buffering off for the route (for nginx, proxy_buffering off).
  • A long history is sent as its newest page (100 messages by default on agent-server); loadEarlierMessages fetches older ones on request. See Running turns.
  • For very long lists in a custom layout, render ChatMessage inside your own virtualized list.

Next Steps ​

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub