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 install @standardagents/react
pnpm add @standardagents/react
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:
Prop Type Required Description config.endpointstringYes The API endpoint URL childrenReactNodeYes Child 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:
Prop Type Default Description threadIdstringRequired The 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:
Option Type Default Description 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:
Property Type Description 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 >
)
}
When to use onThreadEvent vs useThreadEvent
Use Case Hook Trigger side effects (fetch data, navigate) onThreadEventDisplay event data in JSX useThreadEventNeed to react to every event onThreadEventOnly care about latest value useThreadEvent
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:
Parameter Type Description 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:
Parameter Type Description 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
View Complete Chat Interface 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