@standardagents/client
Framework-agnostic client library for Standard Agents Builder - HTTP client, WebSocket management, and file uploads
Overview
The @standardagents/client package provides the core client library for Standard Agents. It's a framework-agnostic foundation that @standardagents/react and @standardagents/vue build upon.
- HTTP client for all API operations
- WebSocket connection management with auto-reconnection
- File upload handling with thumbnail generation
- Utility functions for workblocks and attachments
- Zero framework dependencies
Installation
npm
npm install @standardagents/clientpnpm
pnpm add @standardagents/clientyarn
yarn add @standardagents/clientAgentBuilderClient
HTTP client for all Standard Agents API operations.
import { AgentBuilderClient } from '@standardagents/client'
const client = new AgentBuilderClient('https://your-api.com/api')
// Create a thread. The result is { threadId, agent_id }.
const { threadId } = await client.createThread({
agent_id: 'support-agent',
})
// Send a message. The result is a queued acknowledgement.
const queued = await client.sendMessage(threadId, {
role: 'user',
content: 'Hello!',
})
// Get message history
const messages = await client.getMessages(threadId, { limit: 50 })createThread() resolves to CreateThreadResponse: { threadId, agent_id }. Use threadId for later thread requests. Call getThread(threadId) to read the thread's metadata.
sendMessage() resolves to SendMessageResponse: { status: 'queued', messageId }. Read the message from thread state after its message_data frame arrives.
Methods
| Method | Description |
|---|---|
listAgents(options?) |
List the agents you can create a thread for. { primary: true } returns only primary agents |
createThread(payload) |
Create a new thread. Resolves to { threadId, agent_id } |
getThread(threadId) |
Fetch thread metadata |
getMessages(threadId, options?) |
Fetch message history |
sendMessage(threadId, payload) |
Send a message. Resolves to { status: 'queued', messageId } |
getMessagesPage(threadId, options?) |
Fetch one page of message history with its pagination data |
stopExecution(threadId) |
Cancel in-flight execution |
deleteMessage(threadId, messageId) |
Delete a message. Resolves to { success } |
uploadFile(threadId, file, options?) |
Upload a file to the thread filesystem. Resolves to an AttachmentRef |
listFiles(threadId) |
List all files in the thread filesystem |
getFileUrl(threadId, path) |
Build the URL of a file in the thread filesystem |
getThumbnailUrl(threadId, path) |
Build the thumbnail URL of an image file |
getEndpoint() |
Return the endpoint without a trailing slash |
Constructor
new AgentBuilderClient(endpoint: string, requestFetch?: typeof fetch)endpoint is the API endpoint URL. requestFetch is an optional fetch-compatible function. See Shared HTTP transport and Authentication.
ThreadSessionStore
ThreadSessionStore owns one thread session in @standardagents/client.
It has no framework dependency.
Each Vue or React provider owns a separate store.
Authorized subscriptions remain separate.
import { AgentBuilderClient, ThreadSessionStore } from '@standardagents/client'
const session = new ThreadSessionStore(new AgentBuilderClient('/api'), 'thread-123', {
preload: true,
live: true,
})
const unsubscribe = session.subscribe(() => console.log(session.getSnapshot()))
session.start()
await session.sendMessage({ role: 'user', content: 'Hello' })
// A selection change invalidates outstanding reads and commands.
session.setThreadId('thread-456')
// Call during teardown.
unsubscribe()
session.dispose()getSnapshot() returns a stable snapshot until the next state change.
It contains messages, committed files, run status, connection status, loading flags, pagination flags, and an error.
subscribeEvents() receives custom thread event envelopes.
Event data defaults to unknown.
The store owns message upserts, chunks, optimistic sends, deletion rollback, and file events.
It merges the newest message page after each reconnect.
Recovery reads preserve existing messages and operation errors.
A successful read clears the error that an earlier history read reported.
Session epochs reject stale reads and command completions.
File snapshots retain lifecycle events received during the read.
An empty selection stays idle.
A stopped session reports disconnected and no loading, including after a thread or filter change.
A session that has neither started nor stopped reports the state its first start() will enter.
stop() cancels the connection lifecycle.
start() resumes a stopped store.
dispose() removes its subscriptions.
loadOlderMessages() loads an older page through a complete workblock boundary.
loadMessagesByIds() loads explicit references.
refresh() requests an explicit newest-page reconciliation.
No method schedules a refresh timer.
Vue refs and React's useSyncExternalStore adapt this state for rendering.
Framework adapters own upload queues, pending attachments, and display grouping.
Wire protocol
@standardagents/client/protocol defines the shared socket frames.
Durable Objects, client adapters, and the admin UI import this module.
It covers messages, custom thread events, logs, registry activity, files, KV changes, and effects.
Chunk and run-status types reuse @standardagents/spec definitions.
Portable message and attachment fields extend the spec declarations.
Serialized attachment columns and optimistic preview URLs remain Builder extensions.
Builder-specific frames remain runtime extensions.
Existing frame names remain available from the client package root.
Registry activity frames carry only changed fields.
An omitted execution-state field retains its previous value.
isLogStreamEvent() checks log-channel envelopes at the JSON boundary.
Log patches retain an unknown payload by default.
LogWebSocketCallbacks.onQueuedTools receives log_queued_tools frames.
ThreadConnectionManager
Manages WebSocket connections with automatic reconnection and heartbeat.
import { AgentBuilderClient, ThreadConnectionManager } from '@standardagents/client'
const client = new AgentBuilderClient('https://your-api.com/api/api')
const manager = new ThreadConnectionManager(client, 'thread-123', {
onMessage: event => console.log('Message:', event.data),
onChunk: event => console.log('Chunk:', event.chunk),
onStatusChange: status => console.log('Status:', status),
onReconnected: ({ initial }) => console.log('Connected:', { initial }),
onError: event => console.error('Server error:', event.error),
})
manager.connect()
// Call on teardown.
manager.disconnect()Connection features
- Recovery uses exponential backoff with jitter.
- Quiet sockets send
stream_pingprotocol keepalives. onReconnected({ initial })runs after each accepted open.- Repeated pre-open failures consult the application's auth gate.
- Disconnect invalidates pending sockets and auth decisions.
Options
interface ThreadConnectionOptions {
depth?: number
includeSilent?: boolean
heartbeatInterval?: number
maxReconnectDelay?: number
reconnectAuthGate?: () => boolean | Promise<boolean>
preOpenFailureThreshold?: number
canReconnect?: () => boolean
onAuthorized?: () => void
}FileUploadManager
Handles file uploads with thumbnail generation and progress tracking.
import { AgentBuilderClient, FileUploadManager } from '@standardagents/client'
import type { ThreadFile } from '@standardagents/client'
const client = new AgentBuilderClient('https://your-api.com/api')
const uploader = new FileUploadManager()
async function upload(threadId: string, selected: FileList) {
// Create pending files for display before the upload starts
const items = uploader.queueFiles(selected)
const files = new Map<string, ThreadFile>(items.map(({ pending }) => [pending.id, pending]))
// Upload each file. The callback receives each state change.
const results = await uploader.executeUploads(threadId, items, client, (pendingId, updates) => {
const current = files.get(pendingId)
if (current) files.set(pendingId, { ...current, ...updates })
})
// Each result is an AttachmentRef or an Error
return results
}Features
- Pending files:
queueFiles()returnsThreadFileobjects with statusuploadingbefore the upload starts - State updates:
executeUpload()andexecuteUploads()report each state change to a callback - Thumbnail generation: Image uploads include a thumbnail unless
generateThumbnailisfalse
Utility Functions
transformToWorkblocks
Groups tool calls with their results for UI rendering.
import { transformToWorkblocks } from '@standardagents/client'
const messages = await client.getMessages(threadId)
const workblocks = transformToWorkblocks(messages)
// Workblocks group consecutive tool calls:
// [assistant with tool_calls, tool result, tool result, assistant]
// becomes:
// [workblock { workItems: [...] }, assistant]parseAttachments
Extract attachment references from a message.
import { parseAttachments } from '@standardagents/client'
const attachments = parseAttachments(message)
// Returns: AttachmentRef[]Image Processing
import {
generateImageThumbnail,
canGenerateThumbnails,
} from '@standardagents/client'
// Check if thumbnails are supported (requires Canvas API)
if (canGenerateThumbnails()) {
// thumbnail is base64 data. width and height are the original image size.
const { thumbnail, width, height } = await generateImageThumbnail(file)
}File Helpers
import {
generatePendingFileId,
readFileAsDataUrl,
isImageMimeType,
messagesToFiles,
} from '@standardagents/client'
// Generate unique ID for pending uploads
const id = generatePendingFileId()
// Read file as data URL
const dataUrl = await readFileAsDataUrl(file)
// Check if MIME type is an image
isImageMimeType('image/png') // true
// Extract files from messages
const files = messagesToFiles(messages)Type Exports
The package exports all types needed for building clients:
import type {
// Core message types
Message,
WorkMessage,
WorkItem,
ThreadMessage,
Thread,
// Attachment types
AttachmentRef,
ThreadFile,
PendingAttachment,
AttachmentPayload,
// Configuration
AgentBuilderConfig,
// Request/Response types
GetMessagesOptions,
SendMessagePayload,
CreateThreadPayload,
// Run status
ThreadStatus,
ThreadRunState,
// WebSocket event types
MessageDataEvent,
MessageChunkEvent,
ErrorEvent,
ThreadEvent,
MessageStreamEvent,
LogDataEvent,
CustomEvent,
StoppedByUserEvent,
LogStreamEvent,
// Callback types
MessageWebSocketCallbacks,
LogWebSocketCallbacks,
ThreadConnectionCallbacks,
// Connection status
ConnectionStatus,
} from '@standardagents/client'Key Types
Message:
interface Message {
id: string
role: 'system' | 'user' | 'assistant' | 'tool'
content: string | null
name?: string | null
tool_calls?: string | null
tool_call_id?: string | null
created_at: number
status?: 'pending' | 'completed' | 'failed'
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.
PendingAttachment:
interface PendingAttachment {
id: string
file: File
name: string
mimeType: string
size: number
isImage: boolean
previewUrl: string | null
width?: number
height?: number
}AttachmentRef:
interface AttachmentRef {
id: string
type: 'file'
path: string
name: string
mimeType: string
size: number
width?: number
height?: number
description?: string
}ConnectionStatus:
type ConnectionStatus = 'connecting' | 'connected' | 'reconnecting' | 'disconnected'Authentication
The client reads auth tokens from localStorage:
// Token is read from this key
const token = localStorage.getItem('agentbuilder_auth_token')
// Included in requests as:
// Authorization: Bearer {token}The client constructor takes no token. To send a different credential, pass a fetch-compatible function as the second constructor argument. See Shared HTTP transport.
Building Custom Integrations
Use @standardagents/client directly when:
- Building for a framework without an official SDK
- Creating custom tooling or CLI applications
- Building server-side integrations
- Needing fine-grained control over connections
import {
AgentBuilderClient,
ThreadConnectionManager,
transformToWorkblocks,
} from '@standardagents/client'
import type { Message, ThreadMessage } from '@standardagents/client'
class CustomChatClient {
private client: AgentBuilderClient
private connection: ThreadConnectionManager | null = null
private threadId: string | null = null
private messages: Message[] = []
onUpdate?: (workblocks: ThreadMessage[]) => void
constructor(endpoint: string) {
this.client = new AgentBuilderClient(endpoint)
}
async connect(threadId: string) {
this.threadId = threadId
// Load existing messages
this.messages = await this.client.getMessages(threadId)
this.publish()
// Set up live connection
this.connection = new ThreadConnectionManager(this.client, threadId, {
// A message_data frame carries the complete message record
onMessage: (event) => {
const index = this.messages.findIndex((m) => m.id === event.data.id)
if (index === -1) this.messages.push(event.data)
else this.messages[index] = event.data
this.publish()
},
})
this.connection.connect()
}
async send(content: string) {
if (!this.threadId) throw new Error('Call connect() first')
await this.client.sendMessage(this.threadId, {
role: 'user',
content,
})
}
disconnect() {
this.connection?.disconnect()
this.connection = null
}
private publish() {
this.onUpdate?.(transformToWorkblocks(this.messages))
}
}Next Steps
Full-featured React hooks and components
Vue 3 composables and components
WebSocket protocol documentation
REST API reference
Reconnect recovery
ThreadConnectionManager calls onReconnected({ initial }) after each successful open.
The first open has initial: true.
An explicit disconnect() ends the session, so the first open after a later connect() is initial again.
An auth halt keeps the session, so the open after a later connect() has initial: false.
SDK providers read the newest message page on subsequent opens.
When preload is enabled, a snapshot after the first socket open closes the initial subscription gap.
reconcileMessages(current, snapshot, baseline, hasMore?) preserves mutations received during the read.
hasMore: false marks a complete history snapshot.
Shared HTTP transport
Pass a fetch-compatible function as the second constructor argument. HTTP requests and WebSocket ticket requests will use this transport.
const client = new AgentBuilderClient('/api', requestFetch)Shared live connections
LiveConnection owns one authorized subscription's recovery.
Its socket factory can use mintWebSocketUrl() with the application's fetch transport.
The lifecycle owns jittered backoff, quiet stream_ping keepalives, stale-socket guards, and auth gating.
AuthReconnectSupervisor shares one auth probe across application channels.
Registry, message, and privileged log subscriptions remain separate.
AgentBuilderClient.connectLogWebSocket() connects to /threads/:id/logs.
That subscription requires log-read authorization.
A LiveConnection accepts a socket factory or a LiveConnectionUrlSource with a url(signal) function. URL sources resolve ticket exchange before socket construction. A retired URL result cannot create a socket. Existing raw-socket factories remain supported.
Socket factories and URL sources receive an AbortSignal. SDK and admin adapters forward it to ticket exchange. Disconnect aborts that signal. The raw message and log connection methods accept signal in their options.