Documentation Index Fetch the complete documentation index at: https://docs.standardagentbuilder.com/llms.txt
Use this file to discover all available pages before exploring further.
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
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
npm install @standardagents/react
pnpm add @standardagents/react
yarn add @standardagents/react
Quick Start
import {
AgentBuilderProvider ,
ThreadProvider ,
useThread ,
} 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 , 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.
< 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 }
useWorkblocks = { false }
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 useWorkblocksbooleanfalseTransform tool calls into workblocks depthnumber0Max message depth (0 = top-level only) includeSilentbooleanfalseInclude silent messages endpointstring- Override endpoint from AgentBuilderProvider
Hooks
useThread
Hook to access the full thread context. Must be used within a ThreadProvider.
import { useThread } from "@standardagents/react"
function ChatMessages () {
const {
// Messages & State
messages ,
workblocks ,
loading ,
error ,
status ,
// 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. Automatically includes any pending attachments and clears them after sending.
const { sendMessage , attachments } = 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?' })
Signature:
sendMessage ( payload : Omit < SendMessagePayload , 'attachments' > ): Promise < Message >
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.
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:
addAttachment ( files : File | File [] | FileList ): void
PendingAttachment Type:
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.
const { removeAttachment , attachments } = useThread ()
< button onClick = { () => removeAttachment ( attachment . id ) } > Remove </ button >
Signature:
removeAttachment ( id : string ): void
clearAttachments
Remove all pending attachments.
const { clearAttachments , attachments } = useThread ()
{ attachments . length > 0 && (
< button onClick = { clearAttachments } > Clear All </ button >
)}
Signature:
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.
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:
addFiles ( files : File [] | FileList ): void
ThreadFile Type:
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).
const { removeFile } = useThread ()
< button onClick = { () => removeFile ( file . id ) } > Cancel Upload </ button >
File URL Helpers
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.
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.
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:
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.
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) Inline base64 Image processing None (raw storage) Server-side resize/optimize Use case Documents, persistent files Chat images to LLM
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_createdA new file was added to the thread filesystem file_updatedAn existing file was modified file_deletedA file was removed from the thread filesystem
Custom File Event Handling
You can also subscribe to these events directly for custom handling:
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
Initial load : When ThreadProvider mounts, it fetches the current file list from the server
Live updates : WebSocket events update the files array as changes occur
Optimistic UI : Local uploads via addFiles() appear immediately as “uploading” then transition to “committed”
Deduplication : Server files take priority over local pending files with the same path
useThread Return Value
Full ThreadContextValue:
Property Type Description threadIdstringThe current thread ID messagesMessage[]All messages in the thread workblocksThreadMessage[]Messages transformed to workblocks (if useWorkblocks is true) loadingbooleanWhether messages are loading (alias: isLoading) errorError | nullAny error that occurred statusConnectionStatusWebSocket connection status (alias: connectionStatus) optionsThreadProviderOptionsOptions passed to the provider sendMessage(payload) => Promise<Message>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) => () => voidSubscribe to custom events filesThreadFile[]All files (pending uploads + committed) addFiles(files) => voidUpload files to filesystem removeFile(id) => voidRemove a pending file getFileUrl(file) => stringGet file URL getThumbnailUrl(file) => stringGet thumbnail URL getPreviewUrl(file) => string | nullGet preview URL attachmentsPendingAttachment[]Pending attachments for next message addAttachment(files) => voidAdd attachment(s) for next message removeAttachment(id) => voidRemove a pending attachment clearAttachments() => voidClear all pending attachments
ConnectionStatus:
type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting'
useThreadId
Returns the current thread ID from context.
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():
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:
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 })
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
Transformed Workblocks
assistant (with tool_calls) → tool result → tool result → assistant (final)
workblock {
workItems: [tool_call, tool_result, tool_call, tool_result]
status: 'completed' | 'pending' | 'failed'
} → 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 { 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.
import type {
// Core types
Message ,
WorkMessage ,
WorkItem ,
ThreadMessage ,
Thread ,
// Configuration
AgentBuilderConfig ,
UseThreadOptions ,
GetMessagesOptions ,
SendMessagePayload ,
ThreadProviderOptions ,
// Attachment types
PendingAttachment ,
AttachmentPayload ,
AttachmentRef ,
ThreadFile ,
// 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
attachments ?: string | null // JSON array of AttachmentRef
}
Complete Example
View Complete Chat Interface Example with Image Attachments
import { useState } from "react"
import {
AgentBuilderProvider ,
ThreadProvider ,
useThread ,
} 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 } useWorkblocks >
< ChatInterface />
</ ThreadProvider >
) : (
< button onClick = { () => createThread (). then ( setThreadId ) } >
Start Chat
</ button >
) }
</ AgentBuilderProvider >
)
}
function ChatInterface () {
const {
workblocks ,
sendMessage ,
stopExecution ,
status ,
loading ,
// Attachments for sending to LLM
attachments ,
addAttachment ,
removeAttachment ,
} = useThread ()
const [ input , setInput ] = useState ( "" )
const isRunning = workblocks . some (( m ) => m . status === "pending" )
const handleSend = async () => {
if ( ! input . trim () || isRunning ) return
await sendMessage ({ role: "user" , content: input })
setInput ( "" )
}
const handleFileSelect = ( e : React . 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 attachments = { message . attachments } />
) }
</ div >
)
}) }
</ div >
)
}
function MessageAttachments ({ attachments } : { attachments : string }) {
const parsed = JSON . parse ( attachments ) as AttachmentRef []
return (
< div className = "message-attachments" >
{ parsed . map (( a ) => (
< img
key = { a . id }
src = { a . localPreviewUrl || `/api/threads/.../fs/ ${ 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 Learn about agents, prompts, and tools
API Reference Detailed API documentation
Builder Package Backend framework documentation
Examples Browse example implementations