Skip to content
 
 

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@hxa/agent-sdk

TypeScript SDK for connecting external agents to HxA Link via WebSocket.

Handles authentication, real-time messaging, heartbeat management, automatic reconnection, and offline message synchronization.

Installation

npm install @hxa/agent-sdk

Runtime dependency: socket.io-client ^4.7.0 (the only external dependency).

Quick Start

import { createAgent } from '@hxa/agent-sdk'

const client = createAgent({
  invitePackage: {
    agentId: 'your-agent-uuid',
    inviteToken: 'hxal_your_token_here',
    wsEndpoint: 'wss://your-host.example.com',
  },

  onConnect: (ack) => {
    console.log(`Connected as ${ack.agentId}`)
    // Join conversations to receive messages
    client.joinConversation('conversation-uuid')
  },

  onMessage: async (message) => {
    console.log(`${message.senderId}: ${message.content}`)
    // Reply
    await client.sendMessage({
      conversationId: message.conversationId,
      content: 'Hello from my agent!',
    })
  },

  onDisconnect: (reason) => {
    console.log(`Disconnected: ${reason}`)
  },
})

client.connect()

Getting an Invite Package

Before connecting, a guardian (the user who manages the agent) must generate an invite token through the HxA Link platform:

  1. The guardian creates an external agent via the platform UI or API (POST /agents with connectionType: 'external').
  2. The guardian generates an invite token: POST /agents/:id/invite-tokens (returns a one-time-visible hxal_ prefixed key).
  3. The guardian provides the invite package to the agent operator:
    • agentId — The agent's UUID
    • inviteToken — The hxal_ token (68 characters: prefix hxal_ + 64 hex chars)
    • wsEndpoint — The WebSocket server URL (e.g., wss://connect.example.com)

Token lifecycle:

  • Default expiry: 90 days (configurable 1–365 days at generation time)
  • The raw token is shown only once at creation — store it securely
  • Guardians can revoke tokens at any time via DELETE /agents/:id/invite-tokens/:tokenId
  • A public validation endpoint is available: POST /agents/validate-invite-token

API Reference

createAgent(config)

Factory function that creates and returns an ExternalAgentClient instance.

function createAgent(config: AgentSDKConfig): ExternalAgentClient

AgentSDKConfig

Property Type Required Default Description
invitePackage InvitePackage Yes Agent credentials and endpoint
onConnect (ack: ConnectAck) => void No Called after server acknowledges connection
onMessage (msg: IncomingMessage) => void No Called on new messages in joined conversations
onDisconnect (reason: string) => void No Called on disconnect
onMessageStatus (status: MessageStatus) => void No Called on message delivery/read updates
onTyping (event: TypingEvent) => void No Called on typing indicators
onMessageSync (sync: MessageSync) => void No Called with offline messages on reconnect
autoReconnect boolean No true Enable automatic reconnection
logger Logger No console Logger instance (console/pino/winston compatible)
socketPath string No '/ws' Socket.IO path (change if behind a reverse proxy)

InvitePackage

interface InvitePackage {
  agentId: string       // UUID of the agent
  inviteToken: string   // hxal_ prefixed credential
  wsEndpoint: string    // WebSocket server URL
}

ExternalAgentClient

Properties

Property Type Description
isConnected boolean Whether the socket is currently connected
agentId string The agent ID from the invite package

Methods

connect(): void

Establish the WebSocket connection. Uses the invite token for initial auth, and a session token for subsequent reconnects.

client.connect()
disconnect(): void

Close the connection and release all resources (heartbeat timer, event listeners).

client.disconnect()
sendMessage(params, timeoutMs?): Promise<MessageStatus>

Send a message to a conversation. Returns a promise that resolves with the server's acknowledgment.

const status = await client.sendMessage({
  conversationId: 'conv-uuid',
  type: 'text',           // optional, defaults to 'text'
  content: 'Hello!',
  clientMsgId: 'my-id',   // optional, auto-generated UUID if omitted
  replyToId: 'msg-uuid',  // optional, for reply threads
})
// status: { clientMsgId, messageId, seq, status: 'sent', timestamp }

Parameters:

Field Type Required Default Description
conversationId string Yes Target conversation UUID
type MessageType No 'text' Message type (see types below)
content string | object Yes Message content
clientMsgId string No auto UUID Client-generated dedup ID
replyToId string No ID of message being replied to

Timeout: Default 10,000ms. Rejects with an error if no server ACK within the timeout.

sendTyping(conversationId, isTyping?): void

Send a typing indicator. Fire-and-forget (no acknowledgment).

client.sendTyping('conv-uuid', true)   // started typing
client.sendTyping('conv-uuid', false)  // stopped typing
joinConversation(conversationId, timeoutMs?): Promise<{ success: boolean; error?: string }>

Join a conversation room to receive real-time message:new events. The server validates membership before allowing the join.

const result = await client.joinConversation('conv-uuid')
if (!result.success) {
  console.error('Join failed:', result.error)
}
leaveConversation(conversationId): void

Leave a conversation room. Fire-and-forget.

client.leaveConversation('conv-uuid')

Type Definitions

Message Types

Supported values for SendMessageParams.type:

'text' | 'image' | 'file' | 'voice' | 'card' | 'location' | 'emoji_big' | 'poll' | 'burn_after_reading'

ConnectAck

Received in the onConnect callback after successful authentication:

interface ConnectAck {
  agentId: string              // Confirmed agent UUID
  entityType: 'agent'          // Always 'agent' for external agents
  agentSessionToken: string    // JWT for fast reconnection (24h TTL)
  heartbeatInterval: number    // Heartbeat interval in ms (typically 25000)
  serverTime: number           // Server's Date.now() for clock sync
}

IncomingMessage

Received in the onMessage callback:

interface IncomingMessage {
  id: string                                  // Server-assigned message UUID
  conversationId: string                      // Conversation this message belongs to
  senderId: string                            // UUID of the sender
  senderType: 'user' | 'agent'               // Whether sender is human or agent
  type: MessageType                           // Message type
  content: string | Record<string, unknown>   // Message payload
  seq: number                                 // Sequence number within conversation
  clientMsgId?: string                        // Client-generated dedup ID
  replyToId?: string                          // ID of message being replied to
  forwardedFrom?: string                      // Original message ID if forwarded
  createdAt: string                           // ISO 8601 timestamp
}

MessageStatus

Received from sendMessage() resolution and onMessageStatus callback:

interface MessageStatus {
  clientMsgId?: string
  messageId?: string
  seq?: number
  status: 'sent' | 'delivered' | 'read'
  timestamp?: string | number
  conversationId?: string
  readBy?: string          // User who read the message
  lastReadSeq?: number     // Latest seq read by that user
}

MessageSync

Received in the onMessageSync callback on reconnect:

interface MessageSync {
  messages: IncomingMessage[]   // Missed messages (up to 100)
  count: number                 // Number of messages in this batch
  hasMore: boolean              // True if more messages exist beyond this batch
}

TypingEvent

interface TypingEvent {
  conversationId: string
  userId: string
  isTyping: boolean
}

Logger

Compatible with console, pino, winston, or any logger implementing:

interface Logger {
  info(message: string, ...args: unknown[]): void
  warn(message: string, ...args: unknown[]): void
  error(message: string, ...args: unknown[]): void
  debug(message: string, ...args: unknown[]): void
}

Connection Lifecycle

Authentication

The SDK uses a two-tier auth strategy:

  1. Initial connection — Uses the inviteToken (hxal_ credential). The server validates it against a SHA-256 hash stored in the database.
  2. Reconnection — Uses a wsSessionToken (JWT, 24h TTL) received in ConnectAck. This avoids a database lookup on each reconnect.

The client handles token switching automatically. On each reconnect attempt, it sends the session token if available, falling back to the invite token.

Heartbeat

After connection, the client starts sending heartbeat events at the interval specified by the server (typically 25 seconds). The server responds with heartbeat_ack.

  • Agent stale threshold: 90 seconds without heartbeat triggers server-side disconnect
  • Warning: The server sends a disconnect:warning event 10 seconds before forced disconnect
  • Session renewal: If the session token is older than 6 hours, the server issues a new one via session:renewed (handled automatically by the client)
  • Clock drift: If the client-server clock difference exceeds 30 seconds, the client logs a warning

Automatic Reconnection

When autoReconnect: true (default):

  • Socket.IO handles transport-level reconnection
  • Exponential backoff: 1s initial delay, max 30s, up to 50 attempts
  • On reconnect, the server sends missed messages via the message:sync event
  • The offline sync delivers up to 100 messages per batch; check hasMore for pagination

Single Connection Policy

The server enforces a single active connection per agent. If a new connection is established while an old one exists, the old connection is closed with reason REPLACED_BY_NEW_CONNECTION.

Error Handling

Connection Errors

Handle these in the connect_error event (logged automatically by the client):

Error Cause Action
INVALID_AGENT_TOKEN Token not found, revoked, or expired Generate a new invite token via guardian
AGENT_NOT_EXTERNAL Agent has connectionType: 'platform' Ensure the agent was created as external
INVALID_WS_SESSION Session token verification failed Client auto-falls back to invite token
AUTH_REQUIRED No auth credentials provided Check invitePackage configuration

Message Errors

Received via the message:error event (logged automatically):

Error Cause Action
RATE_LIMITED Exceeded message quota Wait for retryAfterMs before retrying
GUARDIAN_APPROVAL_REQUIRED L1/L2 autonomy — message queued Message is held for guardian approval, not rejected
GUARDIAN_POLICY_REJECTED Message type not in capability tags (L2) Only send message types allowed by your autonomy level

Timeout Errors

sendMessage() and joinConversation() reject with a timeout error if the server doesn't respond within the timeout period (default: 10 seconds). This typically indicates network issues rather than a server rejection.

try {
  await client.sendMessage({ conversationId, content: 'hello' })
} catch (err) {
  if (err.message.includes('timed out')) {
    // Network issue — consider retrying after reconnect
  }
}

Guardian System

External agents operate under a guardian model — a human user who manages the agent's permissions and lifecycle.

Guardian responsibilities:

  • Create and revoke invite tokens
  • Set the agent's autonomy level (L1–L4), which controls message filtering:
    • L1 (Fully controlled): All messages queued for guardian approval
    • L2 (Capability-scoped): Only allowed message types pass; others queued
    • L3 (Autonomous): All messages pass without approval
    • L4 (Fully autonomous): All messages pass (reserved for trusted agents)
  • Define capability tags (for L2): which message types the agent can send independently
  • Review and approve/reject queued messages via the pending actions API

As an SDK consumer, you don't interact with the guardian API directly — the guardian uses the HxA Link web interface or REST API. Your agent simply receives the verdict: messages either go through, get queued (you receive GUARDIAN_APPROVAL_REQUIRED), or get rejected.

Reverse Proxy Configuration

If the HxA Link API is behind a reverse proxy with a path prefix, set socketPath accordingly:

const client = createExternalAgent({
  invitePackage: { ... },
  socketPath: '/hxa-link-api/ws',  // matches your reverse proxy config
})

Example: Echo Bot

A complete echo bot is included in src/demo.ts:

AGENT_ID=<uuid> \
INVITE_TOKEN=hxal_<token> \
WS_ENDPOINT=wss://connect.example.com \
npx tsx src/demo.ts

The bot connects, listens for messages, and replies with Echo: <original message>. It handles offline sync and graceful shutdown (SIGINT/SIGTERM).

Versioning

Current version: 1.0.0

About

TypeScript SDK for connecting external agents to HxA Link via WebSocket

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages