> For the complete documentation index, see [llms.txt](https://docs.graphlit.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.graphlit.dev/api-guides/use-cases/conversations/conversation-stream-agent-real-time-ui.md).

# Stream Agent Real-Time UI

## User Intent

"How do I build a real-time streaming chat UI with AI responses?"

## Operation

**SDK Method**: `streamAgent()`\
**Use Case**: Real-time streaming chat interface with tool calling

***

## Complete Code Example (TypeScript)

```typescript
import { Graphlit } from 'graphlit-client';
import { AgentStreamEvent } from 'graphlit-client/dist/generated/graphql-types';

const graphlit = new Graphlit();

// UI State
let conversationId: string | undefined;
let currentMessage = '';
let isTyping = false;

await graphlit.streamAgent(
  'What are the key findings in the research papers?',
  async (event: AgentStreamEvent) => {
    switch (event.type) {
      case 'conversation_started':
        // UI: Store conversation ID, show typing indicator
        conversationId = event.conversationId;
        isTyping = true;
        updateUI({ showTyping: true });
        break;
        
      case 'message_update':
        // UI: Append text chunk to message bubble (real-time)
        currentMessage += event.message.message;
        updateMessageBubble(currentMessage);
        
        if (!event.isStreaming) {
          // UI: Message complete, hide typing, show final metadata
          isTyping = false;
          finalizeMessage({
            text: currentMessage,
            tokens: event.message.tokens,
            model: event.message.model
          });
          currentMessage = ''; // Reset for next message
        }
        break;
        
      case 'tool_update':
        // UI: Show tool execution card with status
        updateToolCard({
          name: event.toolCall.name,
          status: event.status, // 'preparing' | 'executing' | 'completed' | 'failed'
          arguments: event.toolCall.arguments,
          result: event.result,
          error: event.error
        });
        break;
        
      case 'reasoning_update':
        // UI: Show expandable "Thinking..." section (Claude extended thinking)
        updateReasoningBlock({
          content: event.reasoning,
          isVisible: true
        });
        break;
        
      case 'conversation_completed':
        // UI: Hide typing indicator, show token count badge
        isTyping = false;
        updateUI({
          showTyping: false,
          metadata: {
            tokens: event.message.tokens,
            model: event.message.model,
            throughput: event.message.throughput
          }
        });
        break;
        
      case 'error':
        // UI: Show error toast, enable retry button
        showError({
          message: event.error.message,
          code: event.error.code,
          recoverable: event.error.recoverable
        });
        if (event.error.recoverable) {
          showRetryButton();
        }
        break;
    }
  },
  conversationId,  // Continue existing conversation
  undefined,       // Use default specification
  [],              // Tools (optional)
  {}               // Tool handlers (optional)
);
```

***

## Event Types → UI Patterns

### 1. `conversation_started`

**When**: Conversation begins\
**UI Actions**:

* Store `conversationId` for subsequent turns
* Show typing indicator
* Scroll to bottom of chat

```typescript
case 'conversation_started':
  setConversationId(event.conversationId);
  setIsTyping(true);
  scrollToBottom();
  break;
```

### 2. `message_update`

**When**: Message chunks arrive (streaming) and completion\
**UI Actions**:

* Append each chunk to message bubble
* When `isStreaming: false`, finalize message

```typescript
case 'message_update':
  if (event.isStreaming) {
    // Stream chunk by chunk
    appendTextToMessageBubble(event.message.message);
  } else {
    // Final message with metadata
    finalizeMessageBubble({
      fullText: event.message.message,
      tokens: event.message.tokens,
      model: event.message.model,
      throughput: event.message.throughput
    });
  }
  break;
```

### 3. `tool_update`

**When**: AI calls a tool/function\
**UI Actions**:

* Show tool execution card
* Update status: preparing → executing → completed/failed
* Display result or error

```typescript
case 'tool_update':
  const toolCard = findOrCreateToolCard(event.toolCall.id);
  
  switch (event.status) {
    case 'preparing':
      toolCard.setStatus('Preparing...');
      toolCard.setIcon('spinner');
      break;
    case 'executing':
      toolCard.setStatus(`Executing ${event.toolCall.name}`);
      toolCard.showArguments(event.toolCall.arguments);
      break;
    case 'completed':
      toolCard.setStatus('Completed');
      toolCard.setIcon('check');
      toolCard.showResult(event.result);
      break;
    case 'failed':
      toolCard.setStatus('Failed');
      toolCard.setIcon('error');
      toolCard.showError(event.error);
      break;
  }
  break;
```

### 4. `reasoning_update`

**When**: Model is thinking (Claude extended thinking)\
**UI Actions**:

* Show expandable "Thinking..." section
* Display reasoning content

```typescript
case 'reasoning_update':
  showReasoningPanel({
    content: event.reasoning,
    isCollapsible: true,
    defaultExpanded: false
  });
  break;
```

### 5. `conversation_completed`

**When**: Full response complete\
**UI Actions**:

* Hide typing indicator
* Enable input field
* Show token count badge
* Update conversation metadata

```typescript
case 'conversation_completed':
  setIsTyping(false);
  enableInputField();
  showMetadataBadge({
    tokens: event.message.tokens,
    model: event.message.model,
    duration: calculateDuration()
  });
  break;
```

### 6. `error`

**When**: Error occurs\
**UI Actions**:

* Show error toast/banner
* If `recoverable: true`, show retry button
* Log error for debugging

```typescript
case 'error':
  showErrorToast({
    title: 'Error',
    message: event.error.message,
    severity: event.error.recoverable ? 'warning' : 'error'
  });
  
  if (event.error.recoverable) {
    showRetryButton(() => {
      // Retry the same prompt
      retryLastMessage();
    });
  }
  break;
```

***

## Multi-Turn Conversation

```typescript
// Store conversation ID across turns
let conversationId: string | undefined;

// First message
await graphlit.streamAgent(
  'What is quantum computing?',
  async (event) => {
    if (event.type === 'conversation_started') {
      conversationId = event.conversationId; // Store for next turn
    }
    // ... handle other events
  }
);

// Second message (with context from first)
await graphlit.streamAgent(
  'Can you give an example?',
  async (event) => {
    // ... handle events
  },
  conversationId  // Same conversation = has context
);
```

***

## Tool Calling UI

```typescript
const tools = [
  {
    name: 'searchDocuments',
    description: 'Search through documents',
    schema: JSON.stringify({
      type: 'object',
      properties: {
        query: { type: 'string' }
      }
    })
  }
];

const toolHandlers = {
  searchDocuments: async (args: { query: string }) => {
    const results = await graphlit.queryContents({
      search: args.query
    });
    return { results };
  }
};

await graphlit.streamAgent(
  'Find information about AI safety',
  async (event) => {
    if (event.type === 'tool_update') {
      // UI: Show tool card
      // "🔍 Searching documents for 'AI safety'..."
      // Then show results when completed
    }
  },
  conversationId,
  undefined,
  tools,
  toolHandlers
);
```

***

## Cancellation

```typescript
const abortController = new AbortController();

// Start streaming
const streamPromise = graphlit.streamAgent(
  'Write a long essay...',
  async (event) => {
    // ... handle events
  },
  undefined,
  undefined,
  [],
  {},
  {
    abortSignal: abortController.signal  // Pass abort signal
  }
);

// User clicks "Stop" button
stopButton.onClick(() => {
  abortController.abort();  // Cancel streaming
  showMessage('Generation stopped');
});
```

***

## Production Pattern (React Example)

```typescript
import { useState } from 'react';

function ChatInterface() {
  const [conversationId, setConversationId] = useState<string>();
  const [messages, setMessages] = useState<Message[]>([]);
  const [currentChunk, setCurrentChunk] = useState('');
  const [isStreaming, setIsStreaming] = useState(false);
  const [tools, setTools] = useState<ToolStatus[]>([]);

  const graphlit = new Graphlit();

  async function sendMessage(prompt: string) {
    setIsStreaming(true);
    setCurrentChunk('');

    await graphlit.streamAgent(
      prompt,
      async (event: AgentStreamEvent) => {
        switch (event.type) {
          case 'conversation_started':
            setConversationId(event.conversationId);
            break;

          case 'message_update':
            if (event.isStreaming) {
              setCurrentChunk(prev => prev + event.message.message);
            } else {
              // Finalize message
              setMessages(prev => [...prev, {
                role: 'assistant',
                content: event.message.message,
                tokens: event.message.tokens,
                model: event.message.model
              }]);
              setCurrentChunk('');
            }
            break;

          case 'tool_update':
            setTools(prev => updateToolStatus(prev, event));
            break;

          case 'conversation_completed':
            setIsStreaming(false);
            break;

          case 'error':
            setIsStreaming(false);
            showError(event.error.message);
            break;
        }
      },
      conversationId
    );
  }

  return (
    <div className="chat-interface">
      <MessageList messages={messages} />
      {currentChunk && <StreamingMessage text={currentChunk} />}
      {isStreaming && <TypingIndicator />}
      {tools.length > 0 && <ToolStatusCards tools={tools} />}
      <InputField onSend={sendMessage} disabled={isStreaming} />
    </div>
  );
}
```

***

## Key Differences: streamAgent vs promptConversation

| Feature           | streamAgent             | promptConversation      |
| ----------------- | ----------------------- | ----------------------- |
| **Streaming**     | ✅ Real-time chunks      | ❌ Wait for complete     |
| **Tool calling**  | ✅ Supported             | ❌ Not supported         |
| **Citations**     | ❌ Not available         | ✅ Returns citations     |
| **UI complexity** | Higher (event handling) | Lower (single response) |
| **Use case**      | Chat UI, streaming      | Simple Q\&A, citations  |

**When to use streamAgent**:

* Building chat UI with real-time streaming
* Need tool/function calling
* Want to show AI "thinking" process

**When to use promptConversation**:

* Simple Q\&A without streaming
* Need citations/sources
* Don't need tool calling

***

## Common Issues

**Issue**: Events arrive out of order\
**Solution**: This shouldn't happen. Ensure you're not modifying shared state incorrectly.

**Issue**: Message chunks duplicated\
**Solution**: Only append text when `isStreaming: true`. Final message comes with `isStreaming: false`.

**Issue**: Conversation ID not available\
**Solution**: Wait for `conversation_started` event before using `conversationId`.

**Issue**: Tools not executing\
**Solution**: Verify `tools` array and `toolHandlers` object are passed correctly.

**Issue**: Can't cancel streaming\
**Solution**: Pass `abortSignal` in options parameter.
