Skip to content

@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.

Key Features
  • 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
npm install @standardagents/client

pnpm

pnpm
pnpm add @standardagents/client

yarn

yarn
yarn add @standardagents/client

AgentBuilderClient

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_ping protocol 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() returns ThreadFile objects with status uploading before the upload starts
  • State updates: executeUpload() and executeUploads() report each state change to a callback
  • Thumbnail generation: Image uploads include a thumbnail unless generateThumbnail is false

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

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.