Skip to main content
The NimbleBrain SDK supports real-time streaming of Nira’s responses via Server-Sent Events (SSE). This enables you to display responses character-by-character as they’re generated, creating a smooth typewriter effect for your users.

Why Streaming?

Better UX

Users see responses appear in real-time instead of waiting for the full response

Tool Visibility

Show when Nira is using tools (checking weather, searching, etc.)

Lower Perceived Latency

First token appears quickly, even for long responses

Progressive Rendering

Display partial results while Nira continues working

Basic Streaming

The nira.messages.stream() method returns an async generator that yields events:
for await (const event of nb.nira.messages.stream(conversationId, 'Hello!')) {
  if (event.type === 'content') {
    process.stdout.write(event.data.text as string);
  }
}

Event Types

The stream yields different event types as the response progresses:
Event TypeDescriptionData Fields
message.startNira began generating a response-
contentA chunk of text contenttext
tool.startNira started using a toolname, display
tool.completeTool finished executingname, result
message.completeFull response is readymessageId
errorAn error occurrederror
doneStream is complete-

Complete Event Handling

Here’s how to handle all event types:
for await (const event of nb.nira.messages.stream(conversationId, userMessage)) {
  switch (event.type) {
    case 'message.start':
      // Response is beginning
      console.log('[Nira is thinking...]');
      break;

    case 'content':
      // Display the text chunk
      const text = event.data.text as string;
      process.stdout.write(text);
      break;

    case 'tool.start':
      // Nira is using a tool
      const toolName = event.data.display as string;
      console.log(`\n[Using: ${toolName}]`);
      break;

    case 'tool.complete':
      // Tool finished
      console.log('[Tool complete]');
      break;

    case 'message.complete':
      // Full response ready
      const messageId = event.data.messageId as string;
      console.log(`\n[Message ID: ${messageId}]`);
      break;

    case 'error':
      // Handle error
      const error = event.data.error as string;
      console.error('\n[Error]:', error);
      break;

    case 'done':
      // Stream finished
      console.log('\n--- Stream complete ---');
      break;
  }
}

React Example

Here’s how to build a streaming chat component in React:
import { useState, useCallback } from 'react';
import { NimbleBrain } from '@nimblebrain/sdk';

const nb = new NimbleBrain({ apiKey: 'nb_live_...' });

interface Message {
  role: 'user' | 'assistant';
  content: string;
}

function Chat({ conversationId }: { conversationId: string }) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState('');
  const [isStreaming, setIsStreaming] = useState(false);
  const [currentTool, setCurrentTool] = useState<string | null>(null);

  const sendMessage = useCallback(async () => {
    if (!input.trim() || isStreaming) return;

    const userMessage = input;
    setInput('');

    // Add user message
    setMessages((prev) => [...prev, { role: 'user', content: userMessage }]);

    // Start streaming
    setIsStreaming(true);
    setMessages((prev) => [...prev, { role: 'assistant', content: '' }]);

    try {
      for await (const event of nb.nira.messages.stream(conversationId, userMessage)) {
        switch (event.type) {
          case 'content':
            // Append text to the last message
            setMessages((prev) => {
              const updated = [...prev];
              const last = updated[updated.length - 1];
              last.content += event.data.text as string;
              return updated;
            });
            break;

          case 'tool.start':
            setCurrentTool(event.data.display as string);
            break;

          case 'tool.complete':
            setCurrentTool(null);
            break;

          case 'error':
            console.error('Stream error:', event.data.error);
            break;
        }
      }
    } catch (error) {
      console.error('Error:', error);
    } finally {
      setIsStreaming(false);
      setCurrentTool(null);
    }
  }, [input, isStreaming, conversationId]);

  return (
    <div className="chat-container">
      <div className="messages">
        {messages.map((msg, i) => (
          <div key={i} className={`message ${msg.role}`}>
            {msg.content}
          </div>
        ))}

        {currentTool && (
          <div className="tool-indicator">
            Using: {currentTool}...
          </div>
        )}
      </div>

      <div className="input-area">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
          placeholder="Type a message..."
          disabled={isStreaming}
        />
        <button onClick={sendMessage} disabled={isStreaming}>
          {isStreaming ? 'Sending...' : 'Send'}
        </button>
      </div>
    </div>
  );
}

Node.js CLI Example

A complete command-line chat application:
import { NimbleBrain } from '@nimblebrain/sdk';
import * as readline from 'readline';

async function main() {
  const nb = new NimbleBrain({ apiKey: process.env.NIMBLEBRAIN_API_KEY! });

  // Create a conversation with Nira
  const conversation = await nb.nira.conversations.create('CLI Chat');

  console.log('Chatting with Nira');
  console.log('Type "quit" to exit\n');

  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  const prompt = () => {
    rl.question('You: ', async (input) => {
      if (input.toLowerCase() === 'quit') {
        rl.close();
        return;
      }

      process.stdout.write('Nira: ');

      for await (const event of nb.nira.messages.stream(conversation.id, input)) {
        switch (event.type) {
          case 'content':
            process.stdout.write(event.data.text as string);
            break;
          case 'tool.start':
            process.stdout.write(`\n  [${event.data.display}] `);
            break;
          case 'tool.complete':
            process.stdout.write('done\n');
            break;
        }
      }

      console.log('\n');
      prompt();
    });
  };

  prompt();
}

main();

Error Handling in Streams

Always wrap streaming in try-catch:
try {
  for await (const event of nb.nira.messages.stream(conversationId, message)) {
    if (event.type === 'error') {
      // Handle error event from the stream
      throw new Error(event.data.error as string);
    }

    if (event.type === 'content') {
      process.stdout.write(event.data.text as string);
    }
  }
} catch (error) {
  if (error instanceof Error) {
    console.error('Stream error:', error.message);
  }
}

Cancelling Streams

To cancel a stream early, you can break out of the loop:
let cancelled = false;

// Somewhere else in your code
cancelButton.onclick = () => { cancelled = true; };

for await (const event of nb.nira.messages.stream(conversationId, message)) {
  if (cancelled) {
    console.log('Stream cancelled by user');
    break;
  }

  if (event.type === 'content') {
    process.stdout.write(event.data.text as string);
  }
}

Best Practices

Show Loading States

Display an indicator when waiting for message.start

Handle Tool Events

Show users when Nira is using tools for transparency

Buffer Content

For smooth rendering, consider buffering small chunks

Handle Disconnects

Implement reconnection logic for long-running streams

StreamEvent Type Reference

interface StreamEvent {
  type: 'message.start' | 'content' | 'tool.start' | 'tool.complete' | 'message.complete' | 'error' | 'done';
  data: Record<string, unknown>;
}

// Common data fields by event type:
// content:          { text: string }
// tool.start:       { name: string, display: string }
// tool.complete:    { name: string, result?: unknown }
// message.complete: { messageId: string }
// error:            { error: string }