> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.recursiv.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.recursiv.io/_mcp/server.

# Agent Streaming

## Overview

The `chatStream` method returns an async iterable that yields `StreamChunk` objects as the agent generates its response. This enables real-time display of agent output in CLIs, web UIs, and other interfaces.

## Basic usage (Node.js)

```typescript
import { Recursiv } from '@recursiv/sdk';

const r = new Recursiv();

const stream = r.agents.chatStream('agent_123', {
  message: 'Explain how transformers work in machine learning.',
});

for await (const chunk of stream) {
  if (chunk.type === 'text_delta') {
    process.stdout.write(chunk.delta ?? '');
  }
}

console.log('\n--- Done ---');
```

## Stream chunk types

Each chunk has a `type` field indicating what kind of data it contains:

| Type          | Fields      | Description                           |
| ------------- | ----------- | ------------------------------------- |
| `text_delta`  | `delta`     | A token of the agent's response text. |
| `tool_use`    | `tool_name` | The agent is calling a tool.          |
| `tool_result` | `content`   | The result of a tool call.            |
| `done`        | —           | The stream is complete.               |
| `error`       | `error`     | An error occurred during streaming.   |

```typescript
interface StreamChunk {
  type: 'text_delta' | 'tool_use' | 'tool_result' | 'done' | 'error';
  delta?: string;
  tool_name?: string;
  content?: string;
  error?: string;
}
```

## Continue a conversation

Pass `conversation_id` to maintain context across streamed messages:

```typescript
// First message
let conversationId: string | undefined;
let fullResponse = '';

for await (const chunk of r.agents.chatStream('agent_123', {
  message: 'What is Rust?',
})) {
  if (chunk.type === 'text_delta') {
    fullResponse += chunk.delta ?? '';
  }
}

// Use the conversation_id from the non-streaming chat to continue
const { data: initial } = await r.agents.chat('agent_123', {
  message: 'What is Rust?',
});

// Follow-up with streaming
for await (const chunk of r.agents.chatStream('agent_123', {
  message: 'How does its ownership model work?',
  conversation_id: initial.conversation_id,
})) {
  if (chunk.type === 'text_delta') {
    process.stdout.write(chunk.delta ?? '');
  }
}
```

## Collecting the full response

If you want to stream to the UI but also capture the full text:

```typescript
let fullText = '';

for await (const chunk of r.agents.chatStream('agent_123', {
  message: 'Write a haiku about TypeScript.',
})) {
  switch (chunk.type) {
    case 'text_delta':
      fullText += chunk.delta ?? '';
      process.stdout.write(chunk.delta ?? '');
      break;
    case 'tool_use':
      console.log(`\n[Using tool: ${chunk.tool_name}]`);
      break;
    case 'tool_result':
      console.log(`[Tool result: ${chunk.content}]`);
      break;
    case 'error':
      console.error(`\nStream error: ${chunk.error}`);
      break;
    case 'done':
      break;
  }
}

console.log('\n\nFull response:', fullText);
```

## Error handling

Errors during streaming can come from either the initial connection or within the stream itself.

### Connection errors

If the initial request fails (auth error, agent not found, etc.), `chatStream` throws before yielding any chunks:

```typescript
import { AuthenticationError, NotFoundError } from '@recursiv/sdk';

try {
  for await (const chunk of r.agents.chatStream('bad_id', {
    message: 'Hello',
  })) {
    // ...
  }
} catch (err) {
  if (err instanceof NotFoundError) {
    console.error('Agent not found');
  } else if (err instanceof AuthenticationError) {
    console.error('Invalid API key');
  }
}
```

### In-stream errors

If something goes wrong during generation, you will receive a chunk with `type: 'error'`:

```typescript
for await (const chunk of r.agents.chatStream('agent_123', {
  message: 'Hello',
})) {
  if (chunk.type === 'error') {
    console.error('Stream error:', chunk.error);
    break;
  }
  if (chunk.type === 'text_delta') {
    process.stdout.write(chunk.delta ?? '');
  }
}
```

## Web / React example

```typescript
import { Recursiv } from '@recursiv/sdk';
import { useState } from 'react';

function AgentChat({ agentId }: { agentId: string }) {
  const [response, setResponse] = useState('');
  const [isStreaming, setIsStreaming] = useState(false);

  async function sendMessage(message: string) {
    setResponse('');
    setIsStreaming(true);

    const r = new Recursiv({ apiKey: 'sk_live_...' });

    for await (const chunk of r.agents.chatStream(agentId, { message })) {
      if (chunk.type === 'text_delta') {
        setResponse((prev) => prev + (chunk.delta ?? ''));
      }
    }

    setIsStreaming(false);
  }

  return (
    <div>
      <button onClick={() => sendMessage('Hello!')}>
        {isStreaming ? 'Streaming...' : 'Send'}
      </button>
      <pre>{response}</pre>
    </div>
  );
}
```

## React Native warning

> **Warning**
>
> `chatStream()` does **not** work in React Native or Expo. React Native's `fetch` implementation does not support `ReadableStream`, which is required for SSE parsing.
>
> See the [React Native guide](/sdk/platform/react-native) for a complete workaround using raw `fetch` and manual SSE parsing.

### React Native workaround (summary)

In React Native, you must use raw `fetch` with `react-native-sse` or manual line parsing. Here is the essential pattern:

```typescript
import EventSource from 'react-native-sse';

function streamAgentChat(
  agentId: string,
  message: string,
  apiKey: string,
  onDelta: (text: string) => void,
  onDone: () => void,
  onError: (err: string) => void,
) {
  const baseUrl = 'https://api.recursiv.io/api/v1';
  const url = `${baseUrl}/agents/${agentId}/chat/stream`;

  const es = new EventSource(url, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ message }),
  });

  es.addEventListener('message', (event: any) => {
    if (event.data === '[DONE]') {
      es.close();
      onDone();
      return;
    }

    try {
      const chunk = JSON.parse(event.data);
      if (chunk.type === 'text_delta' && chunk.delta) {
        onDelta(chunk.delta);
      } else if (chunk.type === 'error') {
        onError(chunk.error ?? 'Unknown error');
        es.close();
      }
    } catch {
      // Skip malformed chunks
    }
  });

  es.addEventListener('error', (event: any) => {
    onError(event.message ?? 'Connection error');
    es.close();
  });

  return () => es.close();
}
```

See the full React Native guide at [React Native](/sdk/platform/react-native) for a complete React component example.

## How it works

Under the hood, `chatStream` makes a `POST` request to `/agents/{id}/chat/stream` and parses the Server-Sent Events (SSE) response. Each SSE `data:` line is parsed as JSON into a `StreamChunk`.

The SSE format looks like:

```text
data: {"type":"text_delta","delta":"Hello"}

data: {"type":"text_delta","delta":" world"}

data: {"type":"done"}

data: [DONE]
```

The SDK's `parseSSE` utility handles buffering, line splitting, and JSON parsing. You can import it directly if you need it for custom streaming:

```typescript
import { parseSSE } from '@recursiv/sdk';
```