# @standardagents/react

React hooks and components for Standard Agents Builder - connect to AI agent threads with real-time updates

## Overview

The `@standardagents/react` package provides React hooks and components for building UIs that connect to Standard Agents Builder threads. It handles real-time message streaming, custom event listening, and workblock transformation for tool visualization.

- Key Features

  - Real-time WebSocket updates for live message streaming
  - Context-based architecture with `ThreadProvider`
  - Workblock transformation for UI-friendly tool displays
  - Custom event system for backend-to-frontend communication
  - File uploads and attachments for sending images to the LLM
  - TypeScript support with full type definitions
  - Works with any React framework (Next.js, Remix, Vite, etc.)

## Installation

```bash npm
npm install @standardagents/react
```

```bash pnpm
pnpm add @standardagents/react
```

```bash yarn
yarn add @standardagents/react
```

## Quick Start

```tsx
import {
  AgentBuilderProvider,
  ThreadProvider,
  useThread,
} from "@standardagents/react"

function App() {
  return (
    <AgentBuilderProvider config={{ endpoint: "https://your-api.com/api" }}>
      <ThreadProvider threadId="123e4567-e89b-12d3-a456-426614174000">
        <ChatInterface />
      </ThreadProvider>
    </AgentBuilderProvider>
  )
}

function ChatInterface() {
  const { messages, sendMessage, status } = useThread()

  const handleSend = async (text: string) => {
    await sendMessage({ role: "user", content: text })
  }

  return (
    <div>
      <p>Status: {status}</p>
      {messages.map((msg) => (
        <div key={msg.id}>
          <strong>{msg.role}:</strong> {msg.content}
        </div>
      ))}
      <input onKeyDown={(e) => e.key === "Enter" && handleSend(e.currentTarget.value)} />
    </div>
  )
}
```

## Core Providers

### AgentBuilderProvider

Root provider that configures the API endpoint for all child components.

```tsx
<AgentBuilderProvider config={{ endpoint: "https://api.example.com/api" }}>
  {children}
</AgentBuilderProvider>
```

**Props:**

| Prop              | Type        | Required | Description          |
| ----------------- | ----------- | -------- | -------------------- |
| `config.endpoint` | `string`    | Yes      | The API endpoint URL |
| `children`        | `ReactNode` | Yes      | Child components     |

### ThreadProvider

Establishes a WebSocket connection to a specific thread and provides context for child components.

```tsx
<ThreadProvider
  threadId="123e4567-e89b-12d3-a456-426614174000"
  preload={true}
  live={true}
  useWorkblocks={false}
  depth={0}
  includeSilent={false}
>
  <ChatInterface />
</ThreadProvider>
```

**Props:**

| Prop                | Type      | Default  | Description                                 |
| ------------------- | --------- | -------- | ------------------------------------------- |
| `threadId`          | `string`  | Required | The thread ID to connect to                 |
| `preload`           | `boolean` | `true`   | Fetch existing messages on mount            |
| `live`              | `boolean` | `true`   | Enable WebSocket for live updates           |
| `useWorkblocks`     | `boolean` | `false`  | Transform tool calls into workblocks        |
| `useSubagentBlocks` | `boolean` | `false`  | Transform messages into subagent blocks     |
| `depth`             | `number`  | `0`      | Max message depth (0 = top-level only)      |
| `includeSilent`     | `boolean` | `false`  | Include silent messages                     |
| `endpoint`          | `string`  | -        | Override endpoint from AgentBuilderProvider |

## Hooks

### useThread

Hook to access the full thread context. Must be used within a `ThreadProvider`.

```tsx
import { useThread } from "@standardagents/react"

function ChatMessages() {
  const {
    // Messages & State
    messages,
    workblocks,
    loading,
    error,
    status,
    runStatus,

    // Actions
    sendMessage,
    stopExecution,
    deleteMessage,

    // Custom Events
    onEvent,

    // File Management (uploads to filesystem)
    files,
    addFiles,
    removeFile,
    getFileUrl,
    getPreviewUrl,

    // Attachment Management (sent to LLM)
    attachments,
    addAttachment,
    removeAttachment,
    clearAttachments,
  } = useThread()

  return (
    <div>
      <p>Status: {status}</p>
      {loading && <p>Loading...</p>}
      {messages.map((msg) => (
        <MessageBubble key={msg.id} message={msg} />
      ))}
    </div>
  )
}
```

## Method Reference

### sendMessage

Send a message to the thread with the pending attachments. The pending queue clears when sending starts. A failed send restores the queue for the same session.

```tsx
const { sendMessage, attachments, addAttachment } = useThread()

// Simple text message
await sendMessage({ role: 'user', content: 'Hello!' })

// With attachments (call addAttachment first)
// Attachments are auto-included and cleared after send
addAttachment(imageFile)
await sendMessage({ role: 'user', content: 'What is in this image?' })
```

Sending returns a queued acknowledgement. Its `messageId` identifies the accepted message. Read the message from thread state after its `message_data` frame arrives.

**Signature:**

```typescript
sendMessage(payload: Omit<SendMessagePayload, 'attachments'>): Promise<SendMessageResponse>

interface SendMessageResponse {
  status: 'queued'
  messageId: string
}

interface SendMessagePayload {
  role: 'user' | 'assistant' | 'system'
  content: string
  silent?: boolean
}
```

**Optimistic UI:** When you call `sendMessage()`, an optimistic message with attachment previews is shown immediately. The real message replaces it when confirmed by the server via WebSocket.

---

### addAttachment

Queue file(s) to be sent with the next message. Files are NOT uploaded immediately - they're held locally with preview URLs and sent inline (base64) when `sendMessage()` is called.

**Use this for images/files that should be sent directly to the LLM.**

```tsx
const { addAttachment, attachments } = useThread()

// Single file
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
  if (e.target.files?.[0]) {
    addAttachment(e.target.files[0])
  }
}

// Multiple files
addAttachment(Array.from(fileInput.files))

// FileList directly
addAttachment(e.target.files)

// Show previews for pending attachments
{attachments.map(a => (
  <div key={a.id}>
    {a.isImage && a.previewUrl && (
      <img src={a.previewUrl} alt={a.name} />
    )}
    <span>{a.name}</span>
    <button onClick={() => removeAttachment(a.id)}>Remove</button>
  </div>
))}
```

**Signature:**

```typescript
addAttachment(files: File | File[] | FileList): void
```

**PendingAttachment Type:**

```typescript
interface PendingAttachment {
  id: string              // Temporary ID
  file: File              // Original file object
  name: string
  mimeType: string
  size: number
  isImage: boolean
  previewUrl: string | null  // Object URL for image preview
  width?: number
  height?: number
}
```

---

### removeAttachment

Remove a pending attachment before sending.

```tsx
const { removeAttachment, attachments } = useThread()

<button onClick={() => removeAttachment(attachment.id)}>Remove</button>
```

**Signature:**

```typescript
removeAttachment(id: string): void
```

---

### clearAttachments

Remove all pending attachments.

```tsx
const { clearAttachments, attachments } = useThread()

{attachments.length > 0 && (
  <button onClick={clearAttachments}>Clear All</button>
)}
```

**Signature:**

```typescript
clearAttachments(): void
```

---

### addFiles

Upload files to the thread's filesystem. Files are uploaded immediately and stored persistently.

**Use this for documents/files that should be stored on the thread but NOT sent directly to the LLM.**

```tsx
const { addFiles, files, getPreviewUrl } = useThread()

// Upload files to filesystem
addFiles(fileInput.files)

// Display uploaded files
{files.map(f => (
  <div key={f.id}>
    {f.isImage && <img src={getPreviewUrl(f)} alt={f.name} />}
    <span>{f.name}</span>
    <span>{f.status}</span> {/* 'uploading' | 'ready' | 'committed' | 'error' */}
  </div>
))}
```

**Signature:**

```typescript
addFiles(files: File[] | FileList): void
```

**ThreadFile Type:**

```typescript
interface ThreadFile {
  id: string
  name: string
  mimeType: string
  size: number
  isImage: boolean
  localPreviewUrl: string | null
  status: 'uploading' | 'ready' | 'committed' | 'error'
  error?: string
  path?: string
  width?: number
  height?: number
  messageId?: string
}
```

---

### removeFile

Remove a pending file upload (cannot remove already committed files). A file that is still uploading has its HTTP request aborted, and it never reports `ready` or `error` afterwards.

```tsx
const { removeFile } = useThread()

<button onClick={() => removeFile(file.id)}>Cancel Upload</button>
```

---

### File URL Helpers

```tsx
const { getFileUrl, getThumbnailUrl, getPreviewUrl } = useThread()

// Full file URL (for download/viewing)
const url = getFileUrl(file)

// Thumbnail URL (for images)
const thumb = getThumbnailUrl(file)

// Preview URL (smart - uses local preview for pending, thumbnail for committed)
const preview = getPreviewUrl(file)
```

---

### stopExecution

Stop the current agent execution. Calling it when nothing is running is safe: the request succeeds and the thread is left unchanged.

```tsx
const { stopExecution } = useThread()

<button onClick={stopExecution}>Stop</button>
```

---

### deleteMessage

Delete a message from the thread. The message is optimistically removed from the UI immediately. If the server request fails, the message is restored.

```tsx
const { deleteMessage, messages } = useThread()

async function handleDelete(messageId: string) {
  try {
    await deleteMessage(messageId)
  } catch (err) {
    console.error('Failed to delete:', err)
  }
}

// Usage
{messages.map(msg => (
  <div key={msg.id}>
    <span>{msg.content}</span>
    <button onClick={() => handleDelete(msg.id)}>Delete</button>
  </div>
))}
```

**Signature:**

```typescript
deleteMessage(messageId: string): Promise<void>
```

**Note:** Server-side attachment files are automatically cleaned up when a message is deleted.

---

### onEvent / subscribeToEvent

Subscribe to custom events emitted by the agent.

```tsx
const { onEvent } = useThread()

useEffect(() => {
  const unsubscribe = onEvent<{ status: string }>('progress', (data) => {
    console.log('Progress:', data.status)
  })
  return unsubscribe
}, [])
```

## addFiles vs addAttachment

| Feature              | `addFiles()`                   | `addAttachment()`                                         |
| -------------------- | ------------------------------ | --------------------------------------------------------- |
| **Purpose**          | Filesystem storage             | Send to LLM                                               |
| **Upload timing**    | Immediate                      | On `sendMessage()`                                        |
| **Storage**          | Thread filesystem (persistent) | Thread filesystem (persistent), referenced by the message |
| **Image processing** | None (raw storage)             | Server-side resize/optimize                               |
| **Use case**         | Documents, persistent files    | Chat images to LLM                                        |

Message uploads carry base64 bytes in the request.

## Real-time File Synchronization

The `files` array from `useThread()` automatically stays synchronized with the server. When files are added, modified, or deleted on the thread (by the agent, tools, or other clients), the `files` array updates in real-time via WebSocket.

### File Events

The SDK listens for three file-related events:

| Event          | Description                                   |
| -------------- | --------------------------------------------- |
| `file_created` | A new file was added to the thread filesystem |
| `file_updated` | An existing file was modified                 |
| `file_deleted` | A file was removed from the thread filesystem |

### Custom File Event Handling

You can also subscribe to these events directly for custom handling:

```tsx
const { onEvent } = useThread()

useEffect(() => {
  const unsubscribe = onEvent<{ path: string; file: { name: string; mimeType: string; size: number } }>('file_created', (data) => {
    console.log('File created:', data.path)
    // Custom notification, analytics, etc.
  })
  return unsubscribe
}, [onEvent])
```

### How It Works

1. **Initial load**: When `ThreadProvider` mounts, it fetches the current file list from the server
2. **Live updates**: WebSocket events update the `files` array as changes occur
3. **Optimistic UI**: Local uploads via `addFiles()` appear immediately as "uploading" then transition to "committed"
4. **Deduplication**: Server files take priority over local pending files with the same path

## useThread Return Value

**Full ThreadContextValue:**

| Property           | Type                                        | Description                                                                                                                                     |
| ------------------ | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `threadId`         | `string`                                    | The current thread ID                                                                                                                           |
| `messages`         | `Message[]`                                 | Loaded messages from the thread                                                                                                                 |
| `workblocks`       | `ThreadMessage[]`                           | Messages transformed to workblocks (if `useWorkblocks` is true)                                                                                 |
| `subagentBlocks`   | `SubagentThreadMessage[]`                   | Messages transformed to subagent blocks (if `useSubagentBlocks` is true)                                                                        |
| `groupedMessages`  | `GroupedThreadMessage[]`                    | The active grouped view: subagent blocks, then workblocks, then raw messages                                                                    |
| `loading`          | `boolean`                                   | Whether messages are loading                                                                                                                    |
| `isLoading`        | `boolean`                                   | Alias for `loading`                                                                                                                             |
| `error`            | `Error \| null`                             | Any error that occurred                                                                                                                         |
| `status`           | `ConnectionStatus`                          | WebSocket connection status                                                                                                                     |
| `connectionStatus` | `ConnectionStatus`                          | Alias for `status`                                                                                                                              |
| `runStatus`        | `ThreadStatus \| null`                      | Run status that the server sends: a snapshot on each connect, then each change. `null` until the first status arrives, and when `live` is false |
| `options`          | `ThreadProviderOptions`                     | Resolved options from ThreadProvider props                                                                                                      |
| `sendMessage`      | `(payload) => Promise<SendMessageResponse>` | Send a message (auto-includes attachments)                                                                                                      |
| `stopExecution`    | `() => Promise<void>`                       | Stop current execution                                                                                                                          |
| `deleteMessage`    | `(messageId: string) => Promise<void>`      | Delete a message (optimistic UI)                                                                                                                |
| `onEvent`          | `<T>(type, listener) => () => void`         | Subscribe to custom events                                                                                                                      |
| `subscribeToEvent` | `<T>(type, listener) => () => void`         | Alias for `onEvent`                                                                                                                             |
| `files`            | `ThreadFile[]`                              | All files (pending uploads + committed)                                                                                                         |
| `addFiles`         | `(files) => void`                           | Upload files to filesystem                                                                                                                      |
| `removeFile`       | `(id) => void`                              | Remove a pending file                                                                                                                           |
| `getFileUrl`       | `(file) => string`                          | Get file URL                                                                                                                                    |
| `getThumbnailUrl`  | `(file) => string`                          | Get thumbnail URL                                                                                                                               |
| `getPreviewUrl`    | `(file) => string \| null`                  | Get preview URL                                                                                                                                 |
| `attachments`      | `PendingAttachment[]`                       | Pending attachments for next message                                                                                                            |
| `addAttachment`    | `(files) => void`                           | Add attachment(s) for next message                                                                                                              |
| `removeAttachment` | `(id) => void`                              | Remove a pending attachment                                                                                                                     |
| `clearAttachments` | `() => void`                                | Clear all pending attachments                                                                                                                   |

**ConnectionStatus:**

```typescript
type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting'
```

**ThreadStatus:**

```typescript
interface ThreadStatus {
  state: 'idle' | 'running' | 'failed'
  since: number            // microseconds since epoch
  reason?: string | null   // stop reason of the last run; null while running
}
```

`status` reports the WebSocket connection. `runStatus` reports the agent run. Read `runStatus?.state === 'running'` to find out whether a run is active.

### useThreadId

Returns the current thread ID from context.

```tsx
import { useThreadId } from "@standardagents/react"

function ThreadInfo() {
  const threadId = useThreadId()
  return <span>Thread: {threadId}</span>
}
```

### Custom Events

Listen for custom events emitted by the backend using `onEvent` from `useThread()`:

```tsx
import { useState, useEffect, useCallback } from "react"
import { useThread } from "@standardagents/react"

function GamePreview() {
  const { onEvent } = useThread()
  const [gameStatus, setGameStatus] = useState<{ success: boolean } | null>(null)

  useEffect(() => {
    // Subscribe to custom events - returns unsubscribe function
    const unsubscribe = onEvent<{ success: boolean }>('game_built', (data) => {
      setGameStatus(data)
    })
    return unsubscribe
  }, [onEvent])

  return gameStatus?.success ? <GameIframe /> : <LoadingSpinner />
}
```

**Backend Integration:**

Tools and hooks emit custom thread events through their `ThreadState` parameter:

```typescript
// Inside a tool or hook's execute callback:
state.emit("game_built", { success: true })
```

## Workblocks

Workblocks are a UI-friendly transformation of tool calls. When `useWorkblocks: true`, consecutive assistant messages with tool calls are grouped into workblocks.

### Raw Messages

```
assistant (with tool_calls) → tool result → tool result → assistant (final)
```

### Transformed Workblocks

```
workblock {
  workItems: [tool_call, tool_result, tool_call, tool_result]
  status: 'completed' | 'pending' | 'failed'
} → assistant (final)
```

### WorkMessage Type

```typescript
interface WorkMessage {
  id: string
  type: "workblock"
  content: string | null
  reasoning_content?: string | null
  workItems: WorkItem[]
  status: "pending" | "completed" | "failed"
  created_at: number
  depth?: number
}

interface WorkItem {
  id: string
  type: "tool_call" | "tool_result" | "content"
  name?: string
  content: string | null
  status?: "pending" | "success" | "error" | null
  tool_call_id?: string
}
```

### Rendering Workblocks

```tsx
function MessageList() {
  const { workblocks } = useThread()

  return (
    <>
      {workblocks.map((msg) => {
        if ("type" in msg && msg.type === "workblock") {
          return <WorkBlockDisplay key={msg.id} workblock={msg} />
        }
        return <MessageBubble key={msg.id} message={msg} />
      })}
    </>
  )
}

function WorkBlockDisplay({ workblock }: { workblock: WorkMessage }) {
  return (
    <div className={`workblock ${workblock.status}`}>
      {workblock.workItems.map((item) => (
        <div key={item.id}>
          {item.type === "tool_call" ? `Calling ${item.name}...` : item.content}
        </div>
      ))}
    </div>
  )
}
```

## TypeScript Support

The package is written in TypeScript and includes full type definitions.

```tsx
import type {
  // Core types
  Message,
  WorkMessage,
  WorkItem,
  ThreadMessage,
  Thread,

  // Configuration
  AgentBuilderConfig,
  GetMessagesOptions,
  SendMessagePayload,
  ThreadProviderOptions,

  // Attachment types
  PendingAttachment,
  AttachmentPayload,
  AttachmentRef,
  ThreadFile,

  // Run status
  ThreadStatus,
  ThreadRunState,

  // WebSocket events
  MessageDataEvent,
  MessageChunkEvent,
  ErrorEvent,
  ThreadEvent,
  MessageStreamEvent,
  LogDataEvent,
  CustomEvent,
  StoppedByUserEvent,
  LogStreamEvent,

  // WebSocket callbacks
  MessageWebSocketCallbacks,
  LogWebSocketCallbacks,
} from "@standardagents/react"

// Connection status type
import type { ConnectionStatus } from "@standardagents/react"
```

### Message Type

```typescript
interface Message {
  id: string
  role: "system" | "user" | "assistant" | "tool"
  content: string | null
  name?: string | null
  tool_calls?: string | null          // JSON array
  tool_call_id?: string | null
  log_id?: string | null
  created_at: number                  // microseconds
  request_sent_at?: number | null
  response_completed_at?: number | null
  status?: "pending" | "completed" | "failed"
  silent?: boolean
  tool_status?: "success" | "error" | null
  reasoning_content?: string | null
  reasoning_details?: string | null   // JSON array
  parent_id?: string | null
  depth?: number
  attachments?: string | AttachmentRef[] | null
}
```

HTTP message reads and `message_data` frames carry attachment arrays. The string form in the type is tolerance for older runtimes, whose frames could carry the stored JSON string. Use `parseAttachments(message)` when rendering SDK state.

## Complete Example

The example creates a thread with `AgentBuilderClient.createThread()` and then mounts `ThreadProvider` for that thread. Set `AGENT_ID` to the name of an agent in your project. It reads `runStatus` to disable the input while a run is active.

### View Complete Chat Interface Example with Image Attachments

```tsx
import { useState } from "react"
import type { ChangeEvent } from "react"
import {
  AgentBuilderClient,
  AgentBuilderProvider,
  ThreadProvider,
  useThread,
  useThreadId,
  parseAttachments,
} from "@standardagents/react"
import type { ThreadMessage, Message, WorkMessage } from "@standardagents/react"

const ENDPOINT = "/api"

// Use the name of an agent in your project
const AGENT_ID = "support_agent"

const client = new AgentBuilderClient(ENDPOINT)

async function createThread(): Promise<string> {
  const { threadId } = await client.createThread({ agent_id: AGENT_ID })
  return threadId
}

function App() {
  const [threadId, setThreadId] = useState<string | null>(null)

  return (
    <AgentBuilderProvider config={{ endpoint: ENDPOINT }}>
      {threadId ? (
        <ThreadProvider threadId={threadId} useWorkblocks>
          <ChatInterface />
        </ThreadProvider>
      ) : (
        <button onClick={() => createThread().then(setThreadId)}>
          Start Chat
        </button>
      )}
    </AgentBuilderProvider>
  )
}

function ChatInterface() {
  const {
    workblocks,
    sendMessage,
    stopExecution,
    status,
    runStatus,
    loading,
    // Attachments for sending to LLM
    attachments,
    addAttachment,
    removeAttachment,
  } = useThread()

  const [input, setInput] = useState("")

  const isRunning = runStatus?.state === "running"

  const handleSend = async () => {
    if (!input.trim() || isRunning) return
    await sendMessage({ role: "user", content: input })
    setInput("")
  }

  const handleFileSelect = (e: ChangeEvent<HTMLInputElement>) => {
    if (e.target.files?.length) {
      addAttachment(e.target.files)
    }
  }

  return (
    <div>
      <p>Status: {status} {loading && "(loading...)"}</p>

      <MessageList messages={workblocks} />

      {/* Pending attachments preview */}
      {attachments.length > 0 && (
        <div className="attachment-preview">
          {attachments.map((a) => (
            <div key={a.id} className="attachment-item">
              {a.isImage && a.previewUrl && (
                <img src={a.previewUrl} alt={a.name} width={100} />
              )}
              <span>{a.name}</span>
              <button onClick={() => removeAttachment(a.id)}>×</button>
            </div>
          ))}
        </div>
      )}

      <div className="input-row">
        <input
          type="file"
          accept="image/*"
          multiple
          onChange={handleFileSelect}
        />
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && handleSend()}
          disabled={isRunning}
          placeholder="Type a message..."
        />
        {isRunning ? (
          <button onClick={stopExecution}>Stop</button>
        ) : (
          <button onClick={handleSend}>Send</button>
        )}
      </div>
    </div>
  )
}

function MessageList({ messages }: { messages: ThreadMessage[] }) {
  return (
    <div>
      {messages.map((msg) => {
        if ("type" in msg && msg.type === "workblock") {
          return <WorkBlock key={msg.id} block={msg} />
        }
        const message = msg as Message
        if (message.role === "tool") return null
        return (
          <div key={message.id} className={message.role}>
            {message.content}
            {message.attachments && <MessageAttachments message={message} />}
          </div>
        )
      })}
    </div>
  )
}

function MessageAttachments({ message }: { message: Message }) {
  const threadId = useThreadId()
  const parsed = parseAttachments(message)
  return (
    <div className="message-attachments">
      {parsed.map((a) => (
        <img
          key={a.id}
          src={a.localPreviewUrl || client.getFileUrl(threadId, a.path)}
          alt={a.name}
        />
      ))}
    </div>
  )
}

function WorkBlock({ block }: { block: WorkMessage }) {
  return (
    <div className={`workblock ${block.status}`}>
      {block.workItems.map((item) => (
        <div key={item.id}>
          {item.type === "tool_call" ? (
            <span>Tool: {item.name}</span>
          ) : (
            <span>{item.content}</span>
          )}
        </div>
      ))}
    </div>
  )
}
```

## Next Steps

- [Core Concepts](/docs/core-concepts/agents)

  Learn about agents, prompts, and tools

* [Architecture](/docs/introduction/architecture)

  How the backend framework works

## Reconnect recovery

A thread provider reads the newest message page after a socket reconnect.
`preload` controls the initial history reads.
When `preload` is enabled, a snapshot after the first socket open closes the initial subscription gap.
Recovery also reads history when `preload` is false.
Recovery runs in the background.
Snapshot results preserve in-flight events and optimistic mutations.
Thread switches invalidate pending history responses.
Deletions reconcile within the newest page window.
Older cached rows outside that window remain until history is read again.

Thread providers adapt a framework-free `ThreadSessionStore` from `@standardagents/client`. The store owns message history, live reducers, run status, committed files, and optimistic commands. Reconnect recovery runs once in that shared store. Upload queues and display grouping stay in the framework adapter. Socket frame types are available from `@standardagents/client/protocol`.
