arthur-sdk
v0.11.1
Published
Client-side SDK for interacting with Arthur Intelligence and Booths system.
Maintainers
Readme
Arthur SDK
A client-side TypeScript SDK for interacting with Arthur Intelligence and the Booths system. Includes both full-featured ArthurSDK for interactive conversations and ArthurSyncSDK for simple synchronous booth interactions.
Table of Contents
- Installation
- Quick Start Guide
- API Reference
- Booth and Tool Relationships
- Advanced Usage Patterns
- Types
- Session Management Details
- Error Handling
- Browser Compatibility
- License
Installation
npm install arthur-sdkQuick Start Guide
Follow this step-by-step process to set up and use the Arthur SDK:
import {
ArthurSDK,
ArthurSyncSDK,
BoothRegistry,
ToolRegistry,
} from 'arthur-sdk';
// STEP 1: Create registries
const toolRegistry = new ToolRegistry();
const boothRegistry = new BoothRegistry();
// STEP 2: Register tools first (they are referenced by booths)
toolRegistry.registerTool({
name: 'example-tool',
description: 'An example tool',
parameters: { input: 'string' },
global: true, // Available to all booths
execute: async (args) => {
return { result: 'Tool executed successfully', input: args.input };
},
});
// STEP 3: Register booths (they can reference the tools)
boothRegistry.registerBooth({
id: 'example-booth',
name: 'Example Booth',
description: 'An example booth',
context: 'This is an example booth for demonstration purposes.',
// No tools array = only has access to global tools
});
// STEP 4: Initialize the SDK
const sdk = new ArthurSDK({
userId: 'your-user-id',
clientId: 'your-client-id', // Required for client identification
interactURL: 'https://your-api.com/api/message', // Must be absolute URL
interactionEventsURL: 'https://your-api.com/api/interactionloop', // Must be absolute URL
interruptURL: 'https://your-api.com/api/interrupt', // Must be absolute URL
agentConfig: {
agent: 'armor',
boothRegistry,
toolRegistry,
},
sessionId: 'existing-session-id', // Optional: resume existing session
});
// STEP 5: Set up callbacks
sdk.onMessagesReceived = (messages) => {
console.log('New messages:', messages);
};
sdk.onInteractionLoopComplete = () => {
console.log('Interaction loop completed');
};
sdk.onSessionIdChanged = (sessionId) => {
console.log('Session ID changed:', sessionId);
localStorage.setItem('arthur-session-id', sessionId);
};
// STEP 6: Configure headers (optional)
sdk.setAdditionalHeaders({
Authorization: 'Bearer your-token',
'X-Custom-Header': 'custom-value',
});
// STEP 7: Send messages and interact
await sdk.sendMessageAndListen('Hello, Arthur!');API Reference
ArthurSDK
The main SDK class for interacting with Arthur Intelligence.
Constructor Options
Required Parameters
userId(string): Unique identifier for the userclientId(string): Unique identifier for the client instanceinteractURL(string): Absolute URL for sending messages (e.g., 'https://api.example.com/api/message')interactionEventsURL(string): Absolute URL for the interaction loop (e.g., 'https://api.example.com/api/interactionloop')interruptURL(string): Absolute URL for interrupting interactions (e.g., 'https://api.example.com/api/interrupt')agentConfig(AgentConfig): Agent configuration containing:agent(AgentType): Type of agent ('armor' | 'custom')boothRegistry(BoothRegistry, optional): Registry for managing booth configurationstoolRegistry(ToolRegistry, optional): Registry for managing tool configurations
Optional Parameters
sessionId(string): Existing session ID to resume a previous conversation- Default:
undefined
- Default:
Constructor Examples {#sync-constructor-examples}
Basic initialization for a new conversation
The simplest setup using only required parameters. Default empty registries will be created automatically.
const basicSdk = new ArthurSDK({
userId: 'user-12345',
clientId: 'web-client-001',
interactURL: 'https://api.example.com/api/message',
interactionEventsURL: 'https://api.example.com/api/interactionloop',
interruptURL: 'https://api.example.com/api/interrupt',
agentConfig: {
agent: 'armor',
// boothRegistry and toolRegistry are optional - defaults to empty registries
},
});Using custom tool and booth registries
Most common pattern where you provide pre-configured registries with your tools and booths.
const customToolRegistry = new ToolRegistry();
const customBoothRegistry = new BoothRegistry();
// ... register your tools and booths first
const customSdk = new ArthurSDK({
userId: 'user-12345',
clientId: 'web-client-001',
interactURL: 'https://api.example.com/api/message',
interactionEventsURL: 'https://api.example.com/api/interactionloop',
interruptURL: 'https://api.example.com/api/interrupt',
agentConfig: {
agent: 'armor',
toolRegistry: customToolRegistry,
boothRegistry: customBoothRegistry,
},
});Resuming an existing conversation session
Use this when you want to continue a previous conversation by providing the session ID.
const resumedSdk = new ArthurSDK({
userId: 'user-12345',
clientId: 'web-client-001',
interactURL: 'https://api.example.com/api/message',
interactionEventsURL: 'https://api.example.com/api/interactionloop',
interruptURL: 'https://api.example.com/api/interrupt',
agentConfig: {
agent: 'armor',
toolRegistry: customToolRegistry,
boothRegistry: customBoothRegistry,
},
sessionId: 'session-abc-456', // Resume previous conversation
});Development and testing setup
Simplified configuration for local development with localhost URLs.
const devSdk = new ArthurSDK({
userId: 'dev-user',
clientId: 'dev-client',
interactURL: 'http://localhost:8080/api/message',
interactionEventsURL: 'http://localhost:8080/api/interactionloop',
interruptURL: 'http://localhost:8080/api/interrupt',
agentConfig: {
agent: 'armor',
},
});Production setup with environment variables
Enterprise-ready configuration using environment variables and session persistence.
const prodSdk = new ArthurSDK({
userId: process.env.USER_ID!,
clientId: process.env.CLIENT_ID!,
interactURL: process.env.ARTHUR_API_URL + '/api/message',
interactionEventsURL: process.env.ARTHUR_API_URL + '/api/interactionloop',
interruptURL: process.env.ARTHUR_API_URL + '/api/interrupt',
agentConfig: {
agent: 'armor',
toolRegistry: productionToolRegistry,
boothRegistry: productionBoothRegistry,
},
sessionId: getStoredSessionId(), // Optional: restore session
});Core Methods
ArthurSDK.sendMessage()
Send a message to the Arthur Intelligence API without automatically starting the interaction loop listener.
⚠️ Important: This method cannot be called while an EventSource connection is active (i.e., after calling sendMessageAndListen() or startInteractionLoopListener()). Use clearSession() first to close the connection, or use sendMessageAndListen() for interactive sessions.
Example
await sdk.sendMessage('Hello, Arthur!');Syntax
// Send a simple text message
await sdk.sendMessage(message);
// Send complex message array
await sdk.sendMessage(messages);Parameters
message(string | ResponseInputItem[]): The message to send. Can be a simple string or an array of ResponseInputItem objects for complex message structures.
Return Type
Promise<void> - Returns a promise that resolves when the message has been sent successfully. The promise will reject if there's an error during sending (network issues, invalid message format, etc.).
Error Conditions
- Throws an error if called while EventSource is active:
"Cannot send message while EventSource is active. Close the EventSource first with clearSession() or use sendMessageAndListen() for interactive sessions."
More Examples
Simple user query
Most common usage - sending a basic user message.
await sdk.sendMessage('What is the weather today?');System logging and notifications
Use developer role for internal system messages, logging, or debugging information.
await sdk.sendMessage([{ role: 'developer', content: 'User logged in' }]);User query with developer context
Add hidden context that helps the LLM understand the user's situation without the user seeing it.
const userQueryWithContext: ResponseInputItem[] = [
{
role: 'user',
content: 'What is the weather forecast for tomorrow?',
},
{
role: 'developer',
content:
'User is located in New York, NY. Use metric units for temperature.',
},
];
await sdk.sendMessage(userQueryWithContext);Assistant gaslighting (fake conversation history)
Inject an assistant message that the assistant will believe it actually said, making it part of its conversation history.
const gaslightingExample: ResponseInputItem[] = [
{
role: 'user',
content: 'Help me with email drafting',
},
{
role: 'assistant',
content:
'I specialize in professional email composition and always use formal tone.',
},
];
await sdk.sendMessage(gaslightingExample);Combining developer context with assistant gaslighting
Use both techniques together for maximum control over the LLM's understanding and behavior.
const fullControlExample: ResponseInputItem[] = [
{
role: 'user',
content: 'Write a status update email',
},
{
role: 'developer',
content:
'User is a project manager sending weekly update to stakeholders. Include metrics and next steps.',
},
{
role: 'assistant',
content:
'I create structured, professional status emails with clear sections and actionable items.',
},
];
await sdk.sendMessage(fullControlExample);Under The Hood
You can send messages without starting the interaction loop listener. These messages will be queued in the session until you're ready to start the listener. This is useful for sending background messages without blocking the UI. The LLM will not respond immediately to these messages, but they will be stored.
Important: When you start the interaction loop listener, the SDK will process all queued messages from the session. The LLM sees messages in chronological order, so make sure to send all your messages BEFORE starting the listener, otherwise the LLM will see the last message as the most recent in the conversation.
Session Management: Once an EventSource connection is active (via sendMessageAndListen() or startInteractionLoopListener()), you cannot call sendMessage() again until you close the connection with clearSession(). This prevents session ID mismatches that could break real-time message handling.
ArthurSDK.startInteractionLoopListener()
Manually start listening for server-sent events from the interaction loop. Requires that a session ID and request key already exist (from a previous sendMessage call). The request key is used for authentication and is consumed during the EventSource connection setup.
Example
// First send a message to establish session
await sdk.sendMessage('Initial message');
// Then start listening manually
sdk.startInteractionLoopListener();Syntax
// Start listening (no parameters)
sdk.startInteractionLoopListener();Parameters
None. This method requires that both a session ID and request key have already been established through a previous sendMessage or sendMessageAndListen call.
Return Type
void - This method returns immediately after starting the EventSource connection. It does not return a promise. Responses will be handled through the registered callback functions (onMessagesReceived, onInteractionLoopComplete). Will throw an error if no session ID or request key exists.
More Examples
// CORRECT: Send all messages first, then start listening
await sdk.sendMessage('Setup conversation');
await sdk.sendMessage([{ role: 'developer', content: 'User context info' }]);
await sdk.sendMessage('Main user query');
// Now start listening - LLM will see messages in correct order
sdk.startInteractionLoopListener();
// INCORRECT: Don't interleave messages and listener starts
await sdk.sendMessage('First message');
sdk.startInteractionLoopListener(); // LLM thinks this is the last message
// Cannot call sendMessage here - EventSource is active!
// await sdk.sendMessage('This will throw an error');
// CORRECT: To send more messages after starting listener, clear session first
sdk.clearSession(); // Close EventSource
await sdk.sendMessage('New message after clearing session');
// Restart listening after connection issues
try {
sdk.startInteractionLoopListener();
} catch (error) {
if (error.message.includes('No session ID found')) {
// Need to send a message first to establish session
await sdk.sendMessage('Reconnecting...');
sdk.startInteractionLoopListener();
}
}
// Error handling
sdk.onInteractionLoopComplete = () => {
console.log('Conversation ended, can restart if needed');
};ArthurSDK.sendMessageAndListen()
Send a message and automatically start listening for responses via the interaction loop. This is the most commonly used method for interactive conversations.
Example
await sdk.sendMessageAndListen('Hello, I need help with my tasks');Syntax
// Send text message and start listening
await sdk.sendMessageAndListen(message);
// Send complex message array and start listening
await sdk.sendMessageAndListen(messages);Parameters
message(string | ResponseInputItem[]): The message to send. Can be a simple string or an array of ResponseInputItem objects for complex message structures.
Return Type
Promise<void> - Returns a promise that resolves when the message has been sent successfully and the interaction loop listener has been started. The promise will reject if there's an error during sending or if starting the listener fails.
More Examples
// Start a new conversation
await sdk.sendMessageAndListen('I need help with weather and email');
// Continue an existing conversation
await sdk.sendMessageAndListen('Can you send that email now?');
// Send with complex message structure
await sdk.sendMessageAndListen([
{ role: 'user', content: 'Please help me with these tasks:' },
{ role: 'user', content: '1. Check weather in New York' },
{ role: 'user', content: '2. Send email to [email protected]' },
]);Multi-task request with full context control
Combine user query, developer context, and assistant gaslighting for complex interactive tasks.
const advancedMessage: ResponseInputItem[] = [
{
role: 'user',
content: 'I need help with weather and sending an important email',
},
{
role: 'developer',
content:
'Priority request - user is traveling tomorrow and needs weather info for NYC. Email recipient is their boss.',
},
{
role: 'assistant',
content:
'I always provide detailed weather forecasts and help craft professional emails with appropriate tone.',
},
];
await sdk.sendMessageAndListen(advancedMessage);// Interactive session with immediate response handling
sdk.onMessagesReceived = (messages) => {
// Handle real-time responses
messages.forEach(msg => console.log(msg.content));
};
await sdk.sendMessageAndListen('Start interactive session');ArthurSDK.onMessagesReceived
Set a callback function to handle incoming messages and message updates in real-time.
Example
sdk.onMessagesReceived = (messages) => {
console.log('New messages:', messages);
};Syntax
// Set the callback function
sdk.onMessagesReceived = callback;
// Get the current callback function
const currentCallback = sdk.onMessagesReceived;Parameters
callback((messages: ConversationMessage[]) => void): Function that receives the complete array of conversation messages whenever messages are received or updated.
Return Type
void - This is a setter property. The callback function itself should not return anything.
More Examples
Basic message logging
Simple logging of all incoming messages.
sdk.onMessagesReceived = (messages) => {
messages.forEach((msg) => {
console.log(`${msg.role}: ${msg.content}`);
});
};UI update with message filtering
Update your UI and handle different message types appropriately.
sdk.onMessagesReceived = (messages) => {
const userMessages = messages.filter((msg) => msg.type === 'message');
const toolCalls = messages.filter((msg) => msg.type === 'tool_call');
// Update UI with user/assistant messages
updateChatUI(userMessages);
// Show loading indicators for active tool calls
toolCalls.forEach((toolCall) => {
if (toolCall.loading) {
showToolLoadingIndicator(toolCall.content.name);
}
});
};Message state management
Integration with state management systems.
sdk.onMessagesReceived = (messages) => {
// Update your application state
dispatch(updateMessages(messages));
// Save to local storage for persistence
localStorage.setItem('chat-messages', JSON.stringify(messages));
// Trigger other side effects
if (messages.length > 0) {
markConversationAsActive();
}
};Message Types and Shapes
The messages array passed to your callback contains ConversationMessage objects. Important: This array includes ALL messages in the conversation, including the messages you sent via sendMessage() and sendMessageAndListen(). You don't need to manually add your sent messages to your UI - they're automatically included in this array.
Here are the different types you'll encounter:
User/Assistant Text Messages
Standard conversation messages from users or AI assistants.
// ConversationMessageUserText
{
type: 'message',
role: 'user' | 'assistant' | 'developer',
content: 'Hello, I need help with weather',
id: 'msg-123',
time: '2024-01-15T10:30:00Z',
loading: false
}Tool Call Messages
Messages representing tool executions in progress or completed.
// ConversationMessageToolCall
{
type: 'tool_call',
role: 'assistant',
loading: true, // true while executing, false when complete
id: 'call-456',
time: '2024-01-15T10:30:05Z',
content: {
call_id: 'call-456',
name: 'weather-api',
results: { temperature: 72, conditions: 'sunny' } // present when loading: false
}
}Custom Messages Messages with flexible, server-defined content for specialized use cases like forms, cards, or interactive components.
// CustomConversationMessage
{
type: 'custom_message',
role: 'assistant', // Can also be 'user', 'developer', or 'Unknown'
content: {
// Flexible content structure - can be any valid JSON
// Example: interactive form
formType: 'contact',
fields: [
{ name: 'email', label: 'Email', type: 'email', required: true },
{ name: 'message', label: 'Message', type: 'textarea' }
]
},
id: 'custom-msg-123',
time: '2024-01-15T10:30:00Z',
loading: false
}When to use custom messages:
- Server needs to send structured data that doesn't fit standard text or tool call messages
- Interactive UI components (forms, surveys, cards, buttons)
- Rich media content or specialized visualizations
- Any custom message format defined by your Arthur server configuration
Handling custom messages:
sdk.onMessagesReceived = (messages) => {
messages.forEach((message) => {
if (message.type === 'custom_message') {
// Handle custom message based on content structure
console.log('Custom message received:', message.content);
// Example: render a custom form
if (message.content.formType === 'contact') {
renderContactForm(message.content.fields);
}
}
});
};Important notes:
- The
contentfield can contain any valid JSON structure - Content structure is defined by your Arthur server configuration
- No built-in type guard exists - use
message.type === 'custom_message'to identify - Custom messages are typically sent by the server, not client-initiated
- The
loadingproperty can indicate if the custom content is still being prepared
Handling Different Message Types Use type guards to handle each message type appropriately.
import {
isConversationMessageUserText,
isConversationMessageToolCall,
} from 'arthur-sdk';
sdk.onMessagesReceived = (messages) => {
messages.forEach((message) => {
if (isConversationMessageUserText(message)) {
// Handle text message
console.log(`${message.role}: ${message.content}`);
displayTextMessage(message);
} else if (isConversationMessageToolCall(message)) {
// Handle tool call message
if (message.loading) {
showToolExecutionIndicator(message.content.name);
} else {
hideToolExecutionIndicator(message.content.name);
displayToolResults(message.content.results);
}
} else if (message.type === 'custom_message') {
// Handle custom message
console.log('Custom message:', message.content);
handleCustomMessage(message);
}
});
};Complete Message Flow Example
How messages evolve during a typical conversation with tool usage.
// Initial messages array might look like:
[
{
type: 'message',
role: 'user',
content: 'What is the weather in New York?',
id: 'msg-1',
},
{
type: 'message',
role: 'assistant',
content: "I'll check the weather in New York for you.",
id: 'msg-2',
},
{
type: 'tool_call',
role: 'assistant',
loading: true, // Tool is executing
id: 'call-1',
content: {
call_id: 'call-1',
name: 'weather-api',
},
},
][
// After tool execution completes, the same array updates to:
// ... previous messages unchanged ...
({
type: 'tool_call',
role: 'assistant',
loading: false, // Tool completed
id: 'call-1',
content: {
call_id: 'call-1',
name: 'weather-api',
results: { temperature: 68, conditions: 'partly cloudy' },
},
},
{
type: 'message',
role: 'assistant',
content: 'The weather in New York is 68°F and partly cloudy.',
id: 'msg-3',
})
];Important Note About Message Inclusion
The messages array automatically includes your sent messages - no manual UI updates needed for sent messages.
// ❌ WRONG: Don't manually add sent messages to your UI
await sdk.sendMessage('Hello!');
addMessageToUI({ role: 'user', content: 'Hello!' }); // Don't do this!
// ✅ CORRECT: Just send the message and let the callback handle UI updates
sdk.onMessagesReceived = (messages) => {
updateCompleteUI(messages); // This already includes your sent message
};
await sdk.sendMessage('Hello!');
// The callback will receive:
// [
// { type: 'message', role: 'user', content: 'Hello!', id: 'msg-1' },
// { type: 'message', role: 'assistant', content: 'Hi there!', id: 'msg-2' }
// ]Interruption Control
ArthurSDK.interrupt()
Gracefully interrupt an active interaction session. This method allows you to stop ongoing LLM processing, tool executions, or streaming responses without losing conversation context. The interruption will trigger turn-end events that provide visibility into the interruption reason.
Example
// Interrupt the current session
const result = await sdk.interrupt();
if (result.success) {
console.log('Successfully interrupted:', result.message);
} else {
console.error('Failed to interrupt:', result.error);
}Syntax
const result = await sdk.interrupt();Parameters
None. The method uses the internal session ID to identify which session to interrupt.
Return Type
interface InterruptResponse {
success: boolean;
message?: string; // Success message from server
error?: string; // Error message if interruption failed
}When to Use Interrupt
Common use cases for the interrupt functionality:
- User-initiated cancellation: User clicks "Stop" or "Cancel" button
- Long-running operations: Cancel time-consuming tool executions or data processing
- Incorrect requests: Stop processing when user realizes they made a mistake
- Emergency stops: Halt operations that are consuming too many resources
- Context switching: Stop current task to handle higher priority requests
- Error recovery: Cancel operations that appear to be stuck or malfunctioning
Session Requirements
- Active session required: Must have an established session ID from a previous interaction
- Returns error for no session: If no session exists, returns
{ success: false, error: 'No active session to interrupt' } - Works with any interaction state: Can interrupt during tool execution, streaming responses, or while waiting for LLM responses
More Examples
Basic interruption with user feedback Handle user-initiated stop requests with UI updates.
// User clicks "Stop" button
const handleStopClick = async () => {
showLoadingSpinner('Stopping...');
const result = await sdk.interrupt();
hideLoadingSpinner();
if (result.success) {
showNotification('Operation stopped successfully', 'success');
} else {
showNotification(`Failed to stop: ${result.error}`, 'error');
}
};Conditional interruption based on timeout Automatically interrupt long-running operations.
// Set up timeout-based interruption
let interactionTimeout: NodeJS.Timeout;
// Start interaction with timeout
const startInteractionWithTimeout = async (
message: string,
timeoutMs: number = 30000,
) => {
// Clear any existing timeout
if (interactionTimeout) {
clearTimeout(interactionTimeout);
}
// Set up automatic interruption
interactionTimeout = setTimeout(async () => {
console.log(`Operation timed out after ${timeoutMs}ms, interrupting...`);
const result = await sdk.interrupt();
if (result.success) {
console.log('Successfully interrupted due to timeout');
showTimeoutNotification();
}
}, timeoutMs);
// Start the interaction
await sdk.sendMessageAndListen(message);
};
// Clear timeout when interaction completes
sdk.onInteractionLoopComplete = () => {
if (interactionTimeout) {
clearTimeout(interactionTimeout);
interactionTimeout = null;
}
};Error handling and retry logic Robust interruption with fallback options.
const safeInterrupt = async (maxRetries: number = 3): Promise<boolean> => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await sdk.interrupt();
if (result.success) {
console.log(`Interruption succeeded on attempt ${attempt}`);
return true;
}
console.warn(`Interruption attempt ${attempt} failed: ${result.error}`);
// Wait before retrying
if (attempt < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
} catch (error) {
console.error(`Interruption attempt ${attempt} threw error:`, error);
}
}
console.error(`All ${maxRetries} interruption attempts failed`);
// Fallback: clear session entirely
sdk.clearSession();
console.log('Cleared session as fallback');
return false;
};Integration with session management Handle interruption in session lifecycle.
class ConversationManager {
private sdk: ArthurSDK;
private isInterrupting = false;
constructor(sdk: ArthurSDK) {
this.sdk = sdk;
this.setupEventHandlers();
}
private setupEventHandlers() {
// Handle turn-end events (including interruptions)
this.sdk.onMessagesReceived = (messages) => {
// Look for developer messages indicating interruption
const lastMessage = messages[messages.length - 1];
if (lastMessage?.role === 'developer' && this.isInterrupting) {
this.handleInterruptionComplete();
}
this.updateUI(messages);
};
this.sdk.onInteractionLoopComplete = () => {
this.isInterrupting = false;
this.enableUserInput();
};
}
async interrupt(): Promise<boolean> {
if (this.isInterrupting) {
console.log('Interruption already in progress');
return false;
}
this.isInterrupting = true;
this.disableUserInput();
this.showInterruptionSpinner();
const result = await this.sdk.interrupt();
if (!result.success) {
this.isInterrupting = false;
this.hideInterruptionSpinner();
this.enableUserInput();
this.showError(`Failed to interrupt: ${result.error}`);
return false;
}
return true;
}
private handleInterruptionComplete() {
console.log('Interruption completed successfully');
this.hideInterruptionSpinner();
this.showSuccess('Operation interrupted successfully');
}
private updateUI(messages: ConversationMessage[]) {
// Update chat interface with new messages
}
private showInterruptionSpinner() {
// Show "Stopping..." spinner
}
private hideInterruptionSpinner() {
// Hide interruption spinner
}
private enableUserInput() {
// Re-enable message input and send buttons
}
private disableUserInput() {
// Disable message input and send buttons
}
private showError(message: string) {
// Display error notification
}
private showSuccess(message: string) {
// Display success notification
}
}Turn-End Events and Interruption Visibility
When an interruption is successful, the server sends a turn-end event that creates a visible message in the conversation. This provides transparency about what happened and maintains conversation context.
Turn-end event structure:
interface TurnEndEvent {
role: 'turn-end';
reason: 'user-interruption' | 'natural-completion';
content: string; // Human-readable description of what happened
}How turn-end events appear in your conversation:
sdk.onMessagesReceived = (messages) => {
// After successful interruption, you'll receive messages like:
// [
// { type: 'message', role: 'user', content: 'Process this large dataset...' },
// { type: 'tool_call', role: 'assistant', loading: false, content: { name: 'data-processor', ... } },
// { type: 'message', role: 'developer', content: 'Operation interrupted by user', id: '...', time: '...' },
// // ^ This developer message was created by the turn-end event
// ]
const lastMessage = messages[messages.length - 1];
if (lastMessage?.role === 'developer') {
console.log('System notification:', lastMessage.content);
}
};Error Conditions and Responses
Common error scenarios and how to handle them:
const handleInterruptErrors = async () => {
const result = await sdk.interrupt();
if (!result.success) {
switch (result.error) {
case 'No active session to interrupt':
console.log('No active conversation to interrupt');
// Handle case where user tries to interrupt without active session
showInfo('No active conversation to stop');
break;
case 'Unauthorized':
case 'Authorization header is required':
console.error('Authentication failed for interrupt request');
// Handle auth errors - may need to refresh tokens
handleAuthError();
break;
case 'Session not found or access denied':
console.error('Session no longer exists or access denied');
// Handle case where session expired or was cleared
sdk.clearSession();
showError('Session expired. Please start a new conversation.');
break;
default:
console.error('Unknown interruption error:', result.error);
// Handle unexpected errors
showError('Failed to stop operation. Please try again.');
}
}
};Network and Performance Considerations
The interrupt method is designed for reliability:
- Fast response time: Interruption requests are prioritized and typically complete quickly
- Idempotent: Safe to call multiple times - subsequent calls will succeed even if already interrupted
- Independent of EventSource: Works regardless of streaming connection state
- Graceful degradation: If interruption fails, you can always fall back to
clearSession()
Performance best practices:
// ✅ Good: Single interrupt call with error handling
const handleInterrupt = async () => {
try {
const result = await sdk.interrupt();
return result.success;
} catch (error) {
console.error('Interrupt request failed:', error);
return false;
}
};
// ❌ Avoid: Rapid multiple interrupt calls
const avoidThis = async () => {
// Don't do this - one call is sufficient
await sdk.interrupt();
await sdk.interrupt();
await sdk.interrupt();
};
// ✅ Good: Interrupt with timeout protection
const interruptWithTimeout = async (timeoutMs = 5000): Promise<boolean> => {
const timeoutPromise = new Promise<{ success: false; error: string }>(
(_, reject) => {
setTimeout(
() => reject(new Error('Interrupt request timed out')),
timeoutMs,
);
},
);
try {
const result = await Promise.race([sdk.interrupt(), timeoutPromise]);
return result.success;
} catch (error) {
console.error('Interrupt timed out or failed:', error);
return false;
}
};Message Queueing System
The Arthur SDK includes a sophisticated message queueing system that prevents race conditions and ensures proper message ordering during interactive conversations. This system automatically manages overlapping requests to maintain conversation integrity.
How It Works
The Problem Solved: When multiple messages are sent while an interaction is still active (such as during LLM tool execution or response streaming), they could previously interrupt each other, causing:
- Lost responses from ongoing tool calls
- Broken conversation context
- Race conditions between overlapping requests
The Solution: The SDK automatically queues messages when a request is already being processed, ensuring:
- ✅ Messages are processed in the correct order
- ✅ Ongoing tool executions complete without interruption
- ✅ LLM conversation flow remains intact
- ✅ No race conditions between overlapping requests
Automatic Behavior
The queueing system works transparently - no code changes are required. Your existing sendMessageAndListen() calls will automatically be queued when appropriate:
// This works seamlessly - no changes needed to your code
await sdk.sendMessageAndListen('Analyze this data');
// If LLM makes tool calls during the above request,
// this message will be automatically queued until the first request completes
await sdk.sendMessageAndListen('Also check the weather');Request Processing Order
Messages are processed in strict FIFO (First In, First Out) order:
- Active Request Processing: Only one request is processed at a time
- Automatic Queueing: Subsequent messages are queued automatically
- Sequential Processing: Queued messages are processed only after the current request completes
- Tool Call Preservation: Tool execution results are queued and sent in the correct sequence
Real-World Example Scenario
Here's what happens in a complex interaction where the user interrupts an ongoing LLM workflow:
// 1. User starts a complex request
await sdk.sendMessageAndListen('Analyze sales data and send summary email');
// 2. LLM processes request and makes sequential tool calls:
// - Calls data-fetcher tool
// - Calls data-analyzer tool
// - Starts streaming response with analysis
// - Calls email-sender tool
// 3. User sends interruption message while LLM is still working
await sdk.sendMessageAndListen('Also add weather forecast to the email');
// ↳ This is automatically QUEUED, not processed immediately
// 4. LLM completes its entire workflow:
// - Finishes tool executions
// - Completes response streaming
// - Sends 'end' command
// 5. THEN the queued user message is processed:
// - Fresh request with new requestKey
// - Proper conversation context maintained
// - No interruption of the previous workflowKey Benefits
🔒 Race Condition Prevention
- Eliminates conflicts between overlapping requests
- Prevents premature closing of active EventSource connections
- Ensures tool execution results are never lost
📋 Proper Message Ordering
- FIFO processing guarantees messages are handled in the correct sequence
- Tool execution results are sent before user interruptions are processed
- Conversation context remains coherent and logical
🔄 Request Key Management
- Each queued message gets a fresh, unique
requestKeywhen processed - Single-use authentication tokens are properly managed
- No token reuse errors or authentication conflicts
⚡ Seamless User Experience
- Users can send messages anytime without worrying about timing
- No need to wait for previous requests to complete
- Natural conversation flow is preserved
Technical Details
Request States:
isProcessingRequest: Boolean flag indicating if a request is activerequestQueue: Array of pending messages awaiting processingcurrentRequestId: Unique identifier for the active request (debugging)
Queue Processing:
- New messages are queued when
isProcessingRequestistrue - Queue processing begins when EventSource closes (booth 'end' command)
- Each queued message receives a fresh
requestKeyfrom the server - Processing continues until the queue is empty
Error Handling:
- Failed requests mark themselves complete to allow queue processing
- Session clearing (
clearSession()) rejects all queued messages - EventSource errors trigger queue processing to maintain flow
Backward Compatibility
The message queueing system is completely transparent and maintains full backward compatibility:
- ✅ All existing
sendMessageAndListen()calls work unchanged - ✅ No API modifications or breaking changes
- ✅ Same promises and callback behavior
- ✅ Existing error handling patterns remain valid
Advanced Configuration
While the queueing system works automatically, you can monitor its behavior:
// The SDK exposes these properties for debugging (not recommended for production use)
console.log('Is processing request:', (sdk as any).isProcessingRequest);
console.log('Queue length:', (sdk as any).requestQueue.length);
// Session clearing properly handles queued messages
sdk.clearSession(); // Rejects all queued messages and resets stateNote: The internal queue properties are not part of the public API and should not be relied upon in production code. They're mentioned here for debugging purposes only.
ArthurSDK.pushMessage()
Add a new message to the conversation array. This immediately triggers the onMessagesReceived callback with the updated messages array.
Example
const newMessage = {
type: 'message',
role: 'user',
content: 'This is a manually added message',
id: 'manual-msg-1',
};
sdk.pushMessage(newMessage);Syntax
sdk.pushMessage(message);Parameters
message(ConversationMessage): The message object to add to the conversation. Must be eitherConversationMessageUserTextorConversationMessageToolCall.
Return Type
void - This method does not return anything. The message is added to the internal messages array and immediately triggers the onMessagesReceived callback.
More Examples
Adding a user message programmatically
Useful for injecting messages from external sources or cached conversations.
const userMessage: ConversationMessage = {
type: 'message',
role: 'user',
content: 'Restored from cache',
id: 'cached-msg-1',
time: '2024-01-15T09:00:00Z',
};
sdk.pushMessage(userMessage);Adding a tool call message
For reconstructing conversations that included tool executions.
const toolMessage: ConversationMessage = {
type: 'tool_call',
role: 'assistant',
loading: false,
id: 'tool-call-1',
content: {
call_id: 'tool-call-1',
name: 'weather-api',
results: { temperature: 75, conditions: 'sunny' },
},
};
sdk.pushMessage(toolMessage);ArthurSDK.updateMessage()
Update an existing message in the conversation by its ID. Useful for updating loading states or modifying message content.
Example
sdk.updateMessage('msg-123', (message) => ({
...message,
loading: false,
}));Syntax
sdk.updateMessage(id, updater);Parameters
id(string): The unique identifier of the message to updateupdater((message: ConversationMessage) => ConversationMessage): Function that receives the current message and returns the updated message
Return Type
void - This method does not return anything. The message is updated in the internal messages array and immediately triggers the onMessagesReceived callback.
More Examples
Update tool call completion status
Mark a tool call as completed and add results.
sdk.updateMessage('tool-call-456', (msg) => ({
...msg,
loading: false,
content: {
...msg.content,
results: { success: true, data: 'operation completed' },
},
}));Update message content
Modify the text content of an existing message.
sdk.updateMessage('msg-789', (msg) => ({
...msg,
content: msg.content + ' (edited)',
time: new Date().toISOString(),
}));Conditional updates
Only update if certain conditions are met.
sdk.updateMessage('msg-101', (msg) => {
if (msg.type === 'tool_call' && msg.loading) {
return { ...msg, loading: false };
}
return msg; // No change if conditions not met
});ArthurSDK.setMessages()
Replace the entire conversation messages array. This immediately triggers the onMessagesReceived callback with the new messages.
Example
const newMessages = [
{ type: 'message', role: 'user', content: 'Hello', id: 'msg-1' },
{ type: 'message', role: 'assistant', content: 'Hi there!', id: 'msg-2' },
];
sdk.setMessages(newMessages);Syntax
sdk.setMessages(messages);Parameters
messages(ConversationMessage[]): Array of conversation messages to replace the current messages with
Return Type
void - This method does not return anything. The messages array is replaced and immediately triggers the onMessagesReceived callback.
More Examples
Load conversation from storage
Restore a previously saved conversation.
const savedMessages = JSON.parse(
localStorage.getItem('conversation-history') || '[]',
);
sdk.setMessages(savedMessages);Reset conversation
Clear all messages and start fresh.
sdk.setMessages([]);Initialize with system message
Start a conversation with a predefined system context.
const initialMessages = [
{
type: 'message',
role: 'assistant',
content: "Welcome! I'm ready to help you with weather and email tasks.",
id: 'welcome-msg',
},
];
sdk.setMessages(initialMessages);ArthurSDK.clearSession()
Clear the current session ID and optionally clear the conversation messages. This will cause the next sendMessage() call to create a new session on the server.
Example
// Clear session and messages (start completely fresh)
sdk.clearSession();
// Clear session but keep messages for display purposes
sdk.clearSession(false);Syntax
sdk.clearSession(clearMessages);Parameters
clearMessages(boolean, optional): Whether to clear the messages array. Default:true
Return Type
void - This method does not return anything. The session is cleared, optionally messages are cleared, any active EventSource connection is closed, and the onSessionIdChanged callback is triggered with undefined.
More Examples
Complete session reset (default behavior)
Clear both the session and all conversation messages for a completely fresh start.
// This is the default - clears session AND messages
sdk.clearSession();
// Equivalent to: sdk.clearSession(true);
// Next message will create a brand new session
await sdk.sendMessage('Starting a new conversation');Keep messages for UI display
Clear the session but preserve messages in the UI for user reference.
// Keep messages visible but start new session
sdk.clearSession(false);
// Messages still visible to user, but next message starts new server session
await sdk.sendMessage('Continue with new session');Programmatic session management
Integrate session clearing with your application logic.
// Handle user logout - clear everything
function handleLogout() {
sdk.clearSession(); // Clear session and messages
localStorage.removeItem('arthur-session-id'); // Clear stored session
redirectToLogin();
}
// Handle "new conversation" button
function startNewConversation() {
sdk.clearSession(); // Fresh session and clear messages
showWelcomeMessage();
}
// Handle session timeout - keep messages for reference
function handleSessionTimeout() {
sdk.clearSession(false); // Clear session but keep messages
showSessionExpiredNotification();
}With session change callback handling
Handle the session clearing in your callback.
sdk.onSessionIdChanged = (sessionId) => {
if (sessionId === undefined) {
// Session was cleared
console.log('Session cleared - next message will create new session');
localStorage.removeItem('arthur-session-id');
updateUIForNoSession();
} else {
// New session created
console.log('New session:', sessionId);
localStorage.setItem('arthur-session-id', sessionId);
updateUIForActiveSession(sessionId);
}
};
// This will trigger the callback with undefined
sdk.clearSession();Error recovery and reconnection
Use session clearing for error recovery scenarios.
// Handle connection errors by starting fresh
sdk.onInteractionLoopComplete = () => {
if (sdk.eventSource?.readyState === EventSource.CLOSED) {
console.log('Connection lost - clearing session for fresh start');
sdk.clearSession(false); // Keep messages but clear session
// Attempt to reconnect with fresh session
setTimeout(async () => {
try {
await sdk.sendMessage('Reconnecting...');
} catch (error) {
console.error('Failed to reconnect:', error);
}
}, 2000);
}
};ArthurSDK.setAdditionalHeaders()
Set multiple custom headers that will be included in all HTTP requests to the server.
Example
sdk.setAdditionalHeaders({
Authorization: 'Bearer your-token-here',
'X-API-Version': '2.1',
'X-Client-Source': 'web-app',
});Syntax
sdk.setAdditionalHeaders(headers);Parameters
headers(Record<string, string>): Object containing header key-value pairs to set. This will replace all existing additional headers.
Return Type
void - This method does not return anything.
More Examples
Authentication headers
Set authorization and API credentials for secured endpoints.
sdk.setAdditionalHeaders({
Authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
'X-API-Key': 'your-api-key-here',
});Custom request context
Add application-specific metadata to requests.
sdk.setAdditionalHeaders({
'X-User-Role': 'admin',
'X-Organization-ID': 'org-12345',
'X-Request-Source': 'dashboard',
'X-Client-Version': '1.2.3',
});Replacing existing headers
Since this method replaces all headers, use it for complete header resets.
// Initial headers
sdk.setAdditionalHeaders({
Authorization: 'Bearer old-token',
'X-Version': '1.0',
});
// Replace with new set (old headers are removed)
sdk.setAdditionalHeaders({
Authorization: 'Bearer new-token',
'X-Version': '2.0',
'X-Feature-Flag': 'new-ui',
});ArthurSDK.setHeader()
Set a single custom header that will be included in all HTTP requests to the server. If the header already exists, it will be updated.
Example
sdk.setHeader('Authorization', 'Bearer new-token-here');Syntax
sdk.setHeader(key, value);Parameters
key(string): The header name/key to setvalue(string): The header value to set
Return Type
void - This method does not return anything.
More Examples
Update authentication token
Refresh authorization headers without affecting other headers.
// Set initial auth
sdk.setHeader('Authorization', 'Bearer initial-token');
// Later update just the auth token
sdk.setHeader('Authorization', 'Bearer refreshed-token');Add API versioning
Specify API version for backward compatibility.
sdk.setHeader('X-API-Version', '2.1');
sdk.setHeader('Accept', 'application/vnd.api+json;version=2.1');Request tracking and debugging
Add correlation IDs and debug information.
sdk.setHeader('X-Request-ID', generateUUID());
sdk.setHeader('X-Debug-Mode', 'true');
sdk.setHeader('X-Source-Component', 'chat-widget');ArthurSDK.removeHeader()
Remove a specific header from the additional headers collection. The header will no longer be included in HTTP requests.
Example
sdk.removeHeader('X-Debug-Mode');Syntax
sdk.removeHeader(key);Parameters
key(string): The header name/key to remove
Return Type
void - This method does not return anything. If the header doesn't exist, no error is thrown.
More Examples
Remove authentication
Clear authorization headers when logging out.
sdk.removeHeader('Authorization');
sdk.removeHeader('X-API-Key');Clean up temporary headers
Remove debug or temporary headers after use.
// Set temporary debug headers
sdk.setHeader('X-Debug-Session', 'debug-123');
sdk.setHeader('X-Trace-Enabled', 'true');
// Remove them when debugging is complete
sdk.removeHeader('X-Debug-Session');
sdk.removeHeader('X-Trace-Enabled');Conditional header removal
Remove headers based on application state.
// Remove development headers in production
if (process.env.NODE_ENV === 'production') {
sdk.removeHeader('X-Debug-Mode');
sdk.removeHeader('X-Test-Environment');
}ArthurSDK.getAdditionalHeaders()
Get a copy of all additional headers currently set. Returns an object containing all custom headers that will be included in HTTP requests.
Example
const headers = sdk.getAdditionalHeaders();
console.log(headers); // { 'Authorization': 'Bearer token', 'X-API-Version': '2.1' }Syntax
const headers = sdk.getAdditionalHeaders();Parameters
None.
Return Type
Record<string, string> - Object containing all additional headers as key-value pairs. This is a copy, so modifying the returned object won't affect the SDK's headers.
More Examples
Header inspection and debugging
Check what headers are currently set for debugging purposes.
const currentHeaders = sdk.getAdditionalHeaders();
console.log('Current headers:', currentHeaders);
// Check if specific header exists
if ('Authorization' in currentHeaders) {
console.log('Authorization header is set');
}Backup and restore headers
Save current headers before making temporary changes.
// Backup current headers
const originalHeaders = sdk.getAdditionalHeaders();
// Make temporary changes
sdk.setAdditionalHeaders({
'X-Test-Mode': 'true',
'X-Mock-Data': 'enabled',
});
// Later restore original headers
sdk.setAdditionalHeaders(originalHeaders);Header validation and sanitization
Validate headers before sending requests.
const headers = sdk.getAdditionalHeaders();
// Validate required headers
const requiredHeaders = ['Authorization', 'X-API-Version'];
const missingHeaders = requiredHeaders.filter((header) => !(header in headers));
if (missingHeaders.length > 0) {
console.warn('Missing required headers:', missingHeaders);
}
// Sanitize sensitive information in logs
const sanitizedHeaders = { ...headers };
if (sanitizedHeaders.Authorization) {
sanitizedHeaders.Authorization = 'Bearer [REDACTED]';
}
console.log('Headers for logging:', sanitizedHeaders);ArthurSDK.messages
Direct access to the current conversation messages array. This property provides read and write access to the internal messages collection.
Example
// Read current messages
console.log('Current messages:', sdk.messages);
// Add a message directly (not recommended - use pushMessage instead)
sdk.messages.push(newMessage);Type
ConversationMessage[] - Array containing all conversation messages in chronological order.
More Examples
Reading conversation state
Access current conversation messages for display or analysis.
const messageCount = sdk.messages.length;
const lastMessage = sdk.messages[sdk.messages.length - 1];
console.log(`Conversation has ${messageCount} messages`);
if (lastMessage) {
console.log('Last message:', lastMessage.content);
}Filtering messages by type
Extract specific types of messages from the conversation.
const userMessages = sdk.messages.filter(
(msg) => msg.type === 'message' && msg.role === 'user',
);
const toolCalls = sdk.messages.filter((msg) => msg.type === 'tool_call');
const activeToolCalls = toolCalls.filter((call) => call.loading);Direct manipulation (use with caution)
While direct access is available, using the SDK methods is preferred.
// ❌ Direct manipulation - can bypass callbacks
sdk.messages.push(newMessage);
// ✅ Preferred approach - triggers callbacks
sdk.pushMessage(newMessage);ArthurSDK.messagesList
Getter property that returns the messages array. This is an alias for the messages property, providing the same functionality with a more descriptive name.
Example
const allMessages = sdk.messagesList;
console.log('Total messages:', allMessages.length);Type
ConversationMessage[] - Array containing all conversation messages, identical to the messages property.
More Examples
Equivalent access patterns
Both properties provide the same data.
// These are functionally identical
const messages1 = sdk.messages;
const messages2 = sdk.messagesList;
console.log(messages1 === messages2); // trueChoosing between messages and messagesList
Use whichever naming convention fits your codebase better.
// Shorter, more direct
const count = sdk.messages.length;
// More explicit, self-documenting
const messageHistory = sdk.messagesList;ArthurSDK.toolRegistry
Access to the ToolRegistry instance used by the SDK. This provides access to all registered tools and their configurations.
Example
const weatherTool = sdk.toolRegistry.getTool('weather-api');
console.log('Weather tool:', weatherTool);Type
ToolRegistry - The ToolRegistry instance containing all registered tools and their execution logic.
More Examples
Tool inspection and debugging
Examine available tools and their configurations.
const allTools = sdk.toolRegistry.getAllTools();
const toolNames = sdk.toolRegistry.getToolNames();
console.log('Available tools:', toolNames);
console.log('Tool configurations:', allTools);Runtime tool registration
Add new tools after SDK initialization.
// Register additional tools at runtime
sdk.toolRegistry.registerTool({
name: 'custom-calculator',
description: 'Performs custom calculations',
parameters: { expression: 'string' },
execute: async (args) => {
return { result: eval(args.expression) }; // Use carefully in production!
},
});Tool execution verification
Check if required tools are available before operations.
const requiredTools = ['weather-api', 'email-sender'];
const missingTools = requiredTools.filter(
(toolName) => !sdk.toolRegistry.getTool(toolName),
);
if (missingTools.length > 0) {
console.error('Missing required tools:', missingTools);
}ArthurSDK.eventSource
Access to the current EventSource instance used for server-sent events. This property is null when not connected to the interaction loop. The EventSource connection uses both session ID and request key for authentication.
Example
if (sdk.eventSource) {
console.log('Connection state:', sdk.eventSource.readyState);
} else {
console.log('Not connected to server events');
}Type
EventSource | null - The EventSource instance for receiving real-time updates from the server, or null when disconnected.
More Examples
Connection state monitoring
Monitor the real-time connection status.
if (sdk.eventSource) {
const states = {
[EventSource.CONNECTING]: 'Connecting',
[EventSource.OPEN]: 'Connected',
[EventSource.CLOSED]: 'Disconnected',
};
console.log('EventSource state:', states[sdk.eventSource.readyState]);
} else {
console.log('EventSource not initialized');
}Connection management
Manually manage connection lifecycle if needed.
// Check if connected before attempting operations
const isConnected = sdk.eventSource?.readyState === EventSource.OPEN;
if (isConnected) {
console.log('Ready to receive real-time updates');
} else {
console.log('Connection not available for real-time updates');
}
// Force close connection (usually handled by SDK)
if (sdk.eventSource) {
sdk.eventSource.close();
}Debug connection issues
Add custom event listeners for debugging connection problems.
if (sdk.eventSource) {
sdk.eventSource.addEventListener('error', (event) => {
console.error('EventSource error:', event);
});
sdk.eventSource.addEventListener('open', () => {
console.log('EventSource connection opened');
});
}
## Event Callbacks
### ArthurSDK.onInteractionLoopComplete
> **⚠️ Deprecated**: This method is deprecated. Use [`onTurnEnd`](#arthursdk.onturnend) instead for more accurate turn completion detection with detailed information about why the turn ended.
Set a callback function that executes when the interaction loop ends (conversation completes or connection closes).
#### Example
```typescript
// ⚠️ Deprecated approach
sdk.onInteractionLoopComplete = () => {
console.log('Conversation completed');
};
// ✅ Recommended approach using onTurnEnd
sdk.onTurnEnd = (turnEndEvent) => {
console.log(`Turn ended: ${turnEndEvent.reason} - ${turnEndEvent.content}`);
if (turnEndEvent.reason === 'natural-completion') {
console.log('AI finished responding naturally');
} else if (turnEndEvent.reason === 'user-interruption') {
console.log('User interrupted the response');
}
};Syntax
// Set the callback function
sdk.onInteractionLoopComplete = callback;
// Get the current callback function
const currentCallback = sdk.onInteractionLoopComplete;Parameters
callback(() => void): Function that takes no parameters and is called when the interaction loop completes.
Return Type
void - This is a setter property. The callback function itself should not return anything.
Migration Guide
To migrate from onInteractionLoopComplete to onTurnEnd:
// Old approach
sdk.onInteractionLoopComplete = () => {
// Handle completion
};
// New approach with more information
sdk.onTurnEnd = (turnEndEvent) => {
// Access turn end reason and details
console.log(turnEndEvent.reason); // 'natural-completion' or 'user-interruption'
console.