Skip to main content

Overview

The @standardagents/react package provides React hooks and components for building UIs that connect to AgentBuilder 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
  • TypeScript support with full type definitions
  • Works with any React framework (Next.js, Remix, Vite, etc.)

Installation

npm
npm install @standardagents/react
pnpm
pnpm add @standardagents/react
yarn
yarn add @standardagents/react

Quick Start

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

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

function ChatInterface() {
  const messages = useThread()

  const handleSend = async (text: string) => {
    await sendMessage("123e4567-e89b-12d3-a456-426614174000", {
      role: "user",
      content: text,
    })
  }

  return (
    <div>
      {messages.map((msg) => (
        <div key={msg.id}>
          <strong>{msg.role}:</strong> {msg.content}
        </div>
      ))}
      <input onSubmit={(e) => handleSend(e.currentTarget.value)} />
    </div>
  )
}

Core Providers

AgentBuilderProvider

Root provider that configures the API endpoint for all child components.
<AgentBuilderProvider config={{ endpoint: "https://api.example.com" }}>
  {children}
</AgentBuilderProvider>
Props:
PropTypeRequiredDescription
config.endpointstringYesThe API endpoint URL
childrenReactNodeYesChild components

ThreadProvider

Establishes a WebSocket connection to a specific thread and provides context for child components.
<ThreadProvider
  threadId="123e4567-e89b-12d3-a456-426614174000"
  preload={true}
  live={true}
  depth={0}
  includeSilent={false}
>
  <ChatInterface />
</ThreadProvider>
Props:
PropTypeDefaultDescription
threadIdstringRequiredThe thread ID to connect to
preloadbooleantrueFetch existing messages on mount
livebooleantrueEnable WebSocket for live updates
depthnumber0Max message depth (0 = top-level only)
includeSilentbooleanfalseInclude silent messages
endpointstring-Override endpoint from AgentBuilderProvider

Hooks

useThread

Returns messages from the current thread context. Must be used within a ThreadProvider.
import { useThread } from "@standardagents/react"

function ChatMessages() {
  const messages = useThread()

  return (
    <div>
      {messages.map((msg) => (
        <MessageBubble key={msg.id} message={msg} />
      ))}
    </div>
  )
}
Returns: ThreadMessage[] The return type is a union of Message and WorkMessage:
type ThreadMessage = Message | WorkMessage

interface Message {
  id: string
  role: "system" | "user" | "assistant" | "tool"
  content: string | null
  name?: string | null
  tool_calls?: string | null          // JSON array of tool calls
  tool_call_id?: string | null        // For tool role messages
  created_at: number                  // microseconds
  status?: "pending" | "completed" | "failed"
  silent?: boolean
  tool_status?: "success" | "error" | null
  reasoning_content?: string | null
  parent_id?: string | null
  depth?: number
}

interface WorkMessage {
  id: string
  type: "workblock"                   // Use this to differentiate from Message
  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"
  name?: string                       // Tool name (for tool_call)
  content: string | null              // Tool result content
  status?: "pending" | "success" | "error" | null
  tool_call_id?: string
}
When useWorkblocks: true (default), tool calls are transformed into WorkMessage objects. When useWorkblocks: false, you get raw Message objects only. To differentiate between them:
messages.map((msg) => {
  if ("type" in msg && msg.type === "workblock") {
    // msg is WorkMessage
    return <WorkBlock workItems={msg.workItems} status={msg.status} />
  }
  // msg is Message
  return <MessageBubble role={msg.role} content={msg.content} />
})
Options:
OptionTypeDefaultDescription
useWorkblocksbooleantrueTransform tool calls into workblocks
Example without workblocks:
// Get raw messages without workblock transformation
const messages = useThread({ useWorkblocks: false })

useThreadId

Returns the current thread ID from context.
import { useThreadId } from "@standardagents/react"

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

useThreadContext

Returns the full thread context including messages, loading state, error state, and connection status. Must be used within a ThreadProvider.
import { useThreadContext } from "@standardagents/react"

function ThreadStatus() {
  const { threadId, messages, loading, error, connectionStatus } = useThreadContext()

  if (loading) return <span>Loading...</span>
  if (error) return <span>Error: {error.message}</span>

  return (
    <div>
      <span>Thread: {threadId}</span>
      <span>Status: {connectionStatus}</span>
      <span>Messages: {messages.length}</span>
    </div>
  )
}
Returns:
PropertyTypeDescription
threadIdstringThe current thread ID
messagesMessage[]All messages in the thread
loadingbooleanWhether messages are loading
errorError | nullAny error that occurred
connectionStatusConnectionStatusWebSocket connection status
subscribeToEventfunctionSubscribe to custom events
optionsThreadProviderOptionsOptions passed to the provider
ConnectionStatus:
type ConnectionStatus = 'connecting' | 'connected' | 'disconnected'

onThreadEvent

Hook to listen for custom events emitted by the backend via WebSocket.
import { onThreadEvent } from "@standardagents/react"
import { useCallback } from "react"

function GamePreview() {
  const [gameReady, setGameReady] = useState(false)

  onThreadEvent('game_built', useCallback((data: { success: boolean }) => {
    if (data.success) {
      setGameReady(true)
    }
  }, []))

  return gameReady ? <GameIframe /> : <LoadingSpinner />
}
Always wrap the callback in useCallback to prevent unnecessary re-subscriptions.
Backend Integration: Events are emitted from the backend using emitThreadEvent():
// In a tool or hook on the backend
import { emitThreadEvent } from "@standardagents/builder"

emitThreadEvent(flow, "game_built", { success: true })

useThreadEvent

State-based hook that returns the latest event data as React state.
import { useThreadEvent } from "@standardagents/react"

function ProgressIndicator() {
  const progress = useThreadEvent<{ step: number; total: number }>("progress")

  if (!progress) return null

  return (
    <div>
      Step {progress.step} of {progress.total}
    </div>
  )
}
Use CaseHook
Trigger side effects (fetch data, navigate)onThreadEvent
Display event data in JSXuseThreadEvent
Need to react to every eventonThreadEvent
Only care about latest valueuseThreadEvent

useSendMessage

Hook that returns a function to send messages to the current thread. Must be used within a ThreadProvider. This is a convenience wrapper around sendMessage that automatically uses the thread ID from context.
import { useSendMessage } from "@standardagents/react"

function ChatInput() {
  const sendMessage = useSendMessage()

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

  return <input onSubmit={(e) => handleSubmit(e.currentTarget.value)} />
}

useStopThread

Hook that returns a function to stop the current thread’s execution. Must be used within a ThreadProvider. This is a convenience wrapper around stopThread that automatically uses the thread ID from context.
import { useStopThread } from "@standardagents/react"

function StopButton() {
  const stopThread = useStopThread()

  return (
    <button onClick={() => stopThread()}>
      Stop
    </button>
  )
}

Functions

sendMessage

Send a message to a thread. Works anywhere in your app (doesn’t require being inside ThreadProvider).
import { sendMessage } from "@standardagents/react"

async function handleSend(content: string) {
  await sendMessage("123e4567-e89b-12d3-a456-426614174000", {
    role: "user",
    content,
  })
}
Parameters:
ParameterTypeDescription
idstringThread ID
payloadSendMessagePayloadMessage payload
options?{ endpoint?: string }Optional endpoint override
Payload:
interface SendMessagePayload {
  role: "user" | "assistant" | "system"
  content: string
  silent?: boolean  // If true, message won't appear in UI
}

stopThread

Cancel an in-flight thread execution.
import { stopThread } from "@standardagents/react"

async function handleStop() {
  await stopThread("123e4567-e89b-12d3-a456-426614174000")
}
Parameters:
ParameterTypeDescription
idstringThread ID to stop
options?{ endpoint?: string }Optional endpoint override

Workblocks

Workblocks are a UI-friendly transformation of tool calls. When useWorkblocks: true (default), consecutive assistant messages with tool calls are grouped into workblocks.
  • Raw Messages
  • Transformed Workblocks
assistant (with tool_calls) → tool result → tool result → assistant (final)

WorkMessage Type

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"
  name?: string
  content: string | null
  status?: "pending" | "success" | "error" | null
  tool_call_id?: string
}

Rendering Workblocks

function MessageList() {
  const messages = useThread()

  return (
    <>
      {messages.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.
import type {
  // Core types
  Message,
  WorkMessage,
  WorkItem,
  ThreadMessage,
  Thread,

  // Configuration
  AgentBuilderConfig,
  UseThreadOptions,
  GetMessagesOptions,
  SendMessagePayload,
  ThreadProviderOptions,

  // 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

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
}

Complete Example

import { useState, useCallback } from "react"
import {
  AgentBuilderProvider,
  ThreadProvider,
  useThread,
  useThreadId,
  useSendMessage,
  useStopThread,
  onThreadEvent,
} from "@standardagents/react"
import type { ThreadMessage, Message, WorkMessage } from "@standardagents/react"

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

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

function ChatInterface() {
  const threadId = useThreadId()
  const messages = useThread({ useWorkblocks: true })
  const sendMessage = useSendMessage()
  const stopThread = useStopThread()
  const [input, setInput] = useState("")

  const isRunning = messages.some((m) => m.status === "pending")

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

  return (
    <div>
      <MessageList messages={messages} />
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onKeyPress={(e) => e.key === "Enter" && handleSend()}
        disabled={isRunning}
      />
      {isRunning ? (
        <button onClick={() => stopThread()}>Stop</button>
      ) : (
        <button onClick={handleSend}>Send</button>
      )}
    </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}
          </div>
        )
      })}
    </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