@redhat-cloud-services/ai-client-state
v0.15.0
Published
State management for AI client conversations with event-driven architecture and cross-package compatibility.
Keywords
Readme
@redhat-cloud-services/ai-client-state
State management for AI client conversations with event-driven architecture and cross-package compatibility.
Features
- Lazy Initialization - Conversations are created automatically on first sendMessage (no auto-creation during init)
- Temporary Conversation Pattern - Seamless conversation creation using temporary conversation IDs
- Automatic Promotion - First sendMessage automatically promotes temporary to real conversation
- Conversation Management - Handle multiple AI conversations with persistent state
- Conversation Locking - Prevent message sending to locked conversations with automatic error handling
- Event-Driven Architecture - Subscribe to state changes with typed events
- Message Flow Control - Track message progress and handle streaming responses
- Client Agnostic - Works with any AI client implementing
IAIClientinterface - TypeScript Support - Comprehensive type definitions for all state operations
- Zero UI Dependencies - Pure state management without framework coupling
- Retry Logic - Promotion failures include retry mechanism with user-friendly error messages
Installation
npm install @redhat-cloud-services/ai-client-stateQuick Start
import { createClientStateManager } from '@redhat-cloud-services/ai-client-state';
import { IFDClient } from '@redhat-cloud-services/arh-client';
// Create any AI client (ARH, Lightspeed, etc.)
const client = new IFDClient({
baseUrl: 'https://your-api.com',
fetchFunction: (input, init) => fetch(input, init) // Arrow function to preserve context
});
// Create state manager
const stateManager = createClientStateManager(client);
// Initialize (no longer auto-creates conversations)
await stateManager.init();
// LAZY INITIALIZATION: First sendMessage auto-creates conversation
await stateManager.sendMessage('Hello AI!'); // Auto-promotes temporary conversationCritical: fetchFunction Configuration
IMPORTANT: When configuring the fetchFunction in AI clients, always use arrow functions or properly bound functions to preserve the this context and avoid reference issues.
CRITICAL: Do NOT set 'Content-Type' headers in your fetchFunction - the AI client will set these internally based on the endpoint requirements. Setting custom Content-Type can interfere with the client's internal logic and cause request failures.
❌ Incorrect - Function Reference Issues
// DON'T DO THIS - 'this' context can be lost
const client = new IFDClient({
baseUrl: 'https://your-api.com',
fetchFunction: fetch // Direct reference can cause context issues
});
// DON'T DO THIS - 'this' binding issues
const customFetch = function(input, init) {
// 'this' may not refer to expected object
return fetch(input, init);
};✅ Correct - Arrow Functions (Recommended)
// ALWAYS USE ARROW FUNCTIONS for fetchFunction
const client = new IFDClient({
baseUrl: 'https://your-api.com',
fetchFunction: (input, init) => fetch(input, init)
});✅ Correct - Bound Functions
// Alternative: Bind to window to preserve context
const client = new IFDClient({
baseUrl: 'https://your-api.com',
fetchFunction: fetch.bind(window)
});Authentication with Arrow Functions
import { createClientStateManager } from '@redhat-cloud-services/ai-client-state';
import { IFDClient } from '@redhat-cloud-services/arh-client';
// Imaginary token retrieval function
async function getToken(): Promise<string> {
// Your token retrieval logic here
return 'your-jwt-token-here';
}
// CORRECT: Use arrow function for authenticated fetch
const client = new IFDClient({
baseUrl: 'https://your-api.com',
fetchFunction: async (input, init) => {
const token = await getToken();
return fetch(input, {
...init,
headers: {
...init?.headers,
'Authorization': `Bearer ${token}`
}
});
}
});
const stateManager = createClientStateManager(client);
await stateManager.init();Complex Authentication Example
// For more complex auth scenarios with error handling
const createAuthenticatedClient = async () => {
const client = new IFDClient({
baseUrl: 'https://your-api.com',
fetchFunction: async (input, init) => {
try {
const token = await getToken();
const response = await fetch(input, {
...init,
headers: {
...init?.headers,
'Authorization': `Bearer ${token}`,
'User-Agent': 'AI-Client/1.0'
// DO NOT set 'Content-Type' - the client handles this internally
}
});
// Handle token refresh if needed
if (response.status === 401) {
const newToken = await getToken(); // Refresh token
return fetch(input, {
...init,
headers: {
...init?.headers,
'Authorization': `Bearer ${newToken}`
// DO NOT set 'Content-Type' - the client handles this internally
}
});
}
return response;
} catch (error) {
console.error('Authentication error:', error);
throw error;
}
}
});
return createClientStateManager(client);
};
// Usage
const stateManager = await createAuthenticatedClient();
await stateManager.init();Conversation Locking
The state manager supports conversation locking to prevent users from sending messages to conversations that are no longer active or have been archived. This feature provides a better user experience by preventing confusion and ensuring messages are only sent to appropriate conversations.
How Conversation Locking Works
- Locked conversations prevent new messages from being sent
- Automatic error handling shows user-friendly messages when attempting to send to locked conversations
- Client integration allows AI clients to determine lock status based on their data
- Event system properly handles locked conversation scenarios
Lazy Initialization (Default Behavior)
The state manager now uses lazy initialization by default. Conversations are created automatically on first sendMessage() call, providing a seamless user experience.
How Lazy Initialization Works
const stateManager = createClientStateManager(client);
await stateManager.init(); // No conversations created
// First sendMessage automatically creates conversation
await stateManager.sendMessage('Hello'); // Creates temporary conversation, then promotes to real conversation
// Check if current conversation is temporary (should be false after promotion)
const isTemp = stateManager.isTemporaryConversation(); // false after successful promotionKey Implementation Details
- Temporary Conversation ID: Uses
'__temp_conversation__'constant for temporary state - Automatic Promotion: First sendMessage promotes temporary to real conversation via
client.createNewConversation() - Retry Logic: MAX_RETRY_ATTEMPTS = 2 for promotion failures with user-friendly error messages
- Backward Compatibility: All existing APIs preserved, only initialization behavior changed
Manual Conversation Creation
You can still create conversations manually if needed:
// Create conversation explicitly
const conversation = await stateManager.createNewConversation();
await stateManager.setActiveConversationId(conversation.id);
// Now send messages to the specific conversation
await stateManager.sendMessage('Hello explicit conversation');API Reference
StateManager Interface
export type StateManager<
T extends Record<string, unknown> = Record<string, unknown>,
C extends IAIClient<T> = IAIClient<T>
> = {
// Initialization
init(): Promise<void>;
isInitialized(): boolean;
isInitializing(): boolean;
// Conversation Management
setActiveConversationId(conversationId: string): Promise<void>;
getActiveConversationId(): string | null;
getActiveConversationMessages(): Message<T>[];
getConversations(): Conversation<T>[];
createNewConversation(force?: boolean): Promise<IConversation>;
isTemporaryConversation(): boolean;
// Message Management
sendMessage(query: UserQuery, options?: MessageOptions): Promise<any>;
getMessageInProgress(): boolean;
// State Access
getState(): ClientState<T>;
getInitLimitation(): ClientInitLimitation | undefined;
// Event System
subscribe(event: Events, callback: () => void): () => void;
// Client Access
getClient(): C;
}
export type UserQuery = string;
export interface MessageOptions {
stream?: boolean;
[key: string]: unknown;
}
export enum Events {
MESSAGE = 'message',
ACTIVE_CONVERSATION = 'active-conversation',
IN_PROGRESS = 'in-progress',
CONVERSATIONS = 'conversations',
INITIALIZING_MESSAGES = 'initializing-messages',
INIT_LIMITATION = 'init-limitation',
}Core Concepts
State Manager
The state manager wraps any IAIClient and provides conversation state management:
import { createClientStateManager, Events } from '@redhat-cloud-services/ai-client-state';
const stateManager = createClientStateManager(client);
// Initialize the state manager
await stateManager.init();
// Check initialization status
if (stateManager.isInitialized()) {
console.log('State manager ready');
}
if (stateManager.isInitializing()) {
console.log('State manager initializing...');
}Data Structures
import { Message, Conversation } from '@redhat-cloud-services/ai-client-state';
// Message structure (automatically created by state manager)
interface Message<T = Record<string, unknown>> {
id: string;
answer: string;
role: 'user' | 'bot';
additionalAttributes?: T;
date: Date;
}
// Conversation structure
interface Conversation<T = Record<string, unknown>> {
id: string;
title: string;
messages: Message<T>[];
locked: boolean; // Prevents new messages when true
createdAt: Date;
}
// Example: Messages are automatically created when you send strings
await stateManager.sendMessage('What is OpenShift?');
// This creates a user message internally and triggers the bot response
// Access messages from the conversation
const messages = stateManager.getActiveConversationMessages();
console.log('User message:', messages[0]); // { id: '...', answer: 'What is OpenShift?', role: 'user' }
console.log('Bot response:', messages[1]); // { id: '...', answer: 'OpenShift is...', role: 'bot' }Conversation Management
// Set active conversation (async)
await stateManager.setActiveConversationId('conv-123');
// Get all conversations
const conversations = stateManager.getConversations();
console.log('All conversations:', conversations);
// Create new conversation
const newConversation = await stateManager.createNewConversation();
console.log('Created conversation:', newConversation.id);
// Get messages from active conversation
const messages = stateManager.getActiveConversationMessages();
// Access raw state for advanced use cases
const state = stateManager.getState();
console.log('All conversations:', state.conversations);
console.log('Active conversation:', state.activeConversationId);Sending Messages
Important: The
sendMessagemethod takes a string (UserQuery), not aMessageobject. The state manager automatically createsMessageobjects internally for both user input and bot responses.
Basic Message Sending
// Send non-streaming message (pass string directly)
const response = await stateManager.sendMessage('Explain Kubernetes pods');
console.log('Bot response:', response);Streaming Messages
// Send streaming message (uses client's default streaming handler)
await stateManager.sendMessage('Tell me about container orchestration', { stream: true });
// Messages are automatically updated as chunks arrive
const messages = stateManager.getActiveConversationMessages();
const botResponse = messages.find(m => m.role === 'bot');
console.log('Streaming response so far:', botResponse?.answer);Custom Message Options
import { MessageOptions } from '@redhat-cloud-services/ai-client-state';
const options: MessageOptions = {
stream: true,
customHeader: 'value',
// Any additional options are passed to the underlying client
};
await stateManager.sendMessage('Your message here', options);Event System
Available Events
import { Events } from '@redhat-cloud-services/ai-client-state';
// Available Events:
// Events.MESSAGE - When messages are added/updated
// Events.ACTIVE_CONVERSATION - When active conversation changes
// Events.IN_PROGRESS - When message sending status changes
// Events.CONVERSATIONS - When conversation list changes
// Events.INITIALIZING_MESSAGES - When conversation history is being loaded
// Events.INIT_LIMITATION - When client initialization has limitationsSubscribing to Events
// Subscribe to message updates
const unsubscribeMessages = stateManager.subscribe(Events.MESSAGE, () => {
const messages = stateManager.getActiveConversationMessages();
console.log('Messages updated:', messages.length);
});
// Subscribe to conversation changes
const unsubscribeConversation = stateManager.subscribe(Events.ACTIVE_CONVERSATION, () => {
const state = stateManager.getState();
console.log('Active conversation changed:', state.activeConversationId);
});
// Subscribe to progress updates
const unsubscribeProgress = stateManager.subscribe(Events.IN_PROGRESS, () => {
const isInProgress = stateManager.getMessageInProgress();
console.log('Message in progress:', isInProgress);
});
// Subscribe to conversation list changes
const unsubscribeConversations = stateManager.subscribe(Events.CONVERSATIONS, () => {
const conversations = stateManager.getConversations();
console.log('Conversations updated:', conversations.length);
});
// Subscribe to message initialization events
const unsubscribeInitializing = stateManager.subscribe(Events.INITIALIZING_MESSAGES, () => {
const isInitializing = stateManager.isInitializing();
console.log('Messages initializing:', isInitializing);
});
// Cleanup subscriptions
unsubscribeMessages();
unsubscribeConversation();
unsubscribeProgress();
unsubscribeConversations();
unsubscribeInitializing();Progress Tracking
// Check if a message is currently being sent
const isInProgress = stateManager.getMessageInProgress();
if (isInProgress) {
console.log('Please wait, message being processed...');
} else {
console.log('Ready to send next message');
}Client Integration Examples
ARH Client Integration
import { IFDClient } from '@redhat-cloud-services/arh-client';
import { createClientStateManager } from '@redhat-cloud-services/ai-client-state';
const arhClient = new IFDClient({
baseUrl: 'https://arh-api.redhat.com',
fetchFunction: authenticatedFetch
});
const stateManager = createClientStateManager(arhClient);
await stateManager.init();Lightspeed Client Integration
import { LightspeedClient } from '@redhat-cloud-services/lightspeed-client';
import { createClientStateManager } from '@redhat-cloud-services/ai-client-state';
const lightspeedClient = new LightspeedClient({
baseUrl: 'https://lightspeed-api.openshift.com',
fetchFunction: (input, init) => fetch(input, init)
});
const stateManager = createClientStateManager(lightspeedClient);
await stateManager.init();Custom Client Integration
import {
IAIClient,
IConversation,
IMessageResponse,
ClientInitLimitation,
IInitErrorResponse
} from '@redhat-cloud-services/ai-client-common';
import { createClientStateManager } from '@redhat-cloud-services/ai-client-state';
class CustomClient implements IAIClient {
async init(): Promise<{
conversations: IConversation[];
limitation?: ClientInitLimitation;
error?: IInitErrorResponse;
}> {
return {
conversations: []
};
}
async createNewConversation(): Promise<IConversation> {
return {
id: crypto.randomUUID(),
title: 'New Conversation',
locked: false,
createdAt: new Date()
};
}
async sendMessage(conversationId: string, message: string): Promise<IMessageResponse<Record<string, unknown>>> {
// Your custom implementation
return {
messageId: crypto.randomUUID(),
answer: 'Custom response',
conversationId,
additionalAttributes: {}
};
}
// ... implement other IAIClient methods
}
const customClient = new CustomClient();
const stateManager = createClientStateManager(customClient);Advanced Usage
Multiple Conversation Handling
// Create multiple conversations
await stateManager.init(); // No longer auto-creates conversations
// Create conversations explicitly
const conversation1 = await stateManager.createNewConversation();
const conversation2 = await stateManager.createNewConversation();
// Switch between conversations
await stateManager.setActiveConversationId(conversation1.id);
await stateManager.sendMessage('First message');
await stateManager.setActiveConversationId(conversation2.id);
await stateManager.sendMessage('Second message');
// Get all conversations
const allConversations = stateManager.getConversations();
console.log('Total conversations:', allConversations.length);
// Access specific conversation from state
const state = stateManager.getState();
const conv1 = state.conversations[conversation1.id];
const conv2 = state.conversations[conversation2.id];Error Handling
try {
await stateManager.sendMessage('Your message here');
} catch (error) {
console.error('Failed to send message:', error);
// State manager automatically cleans up failed messages
const messages = stateManager.getActiveConversationMessages();
// Failed bot message will be removed from conversation
}State Inspection
const state = stateManager.getState();
console.log('Initialization status:', {
isInitialized: state.isInitialized,
isInitializing: state.isInitializing
});
console.log('Conversation state:', {
activeConversationId: state.activeConversationId,
conversationCount: Object.keys(state.conversations).length,
messageInProgress: state.messageInProgress
});
// Access specific conversation details
Object.entries(state.conversations).forEach(([id, conversation]) => {
console.log(`Conversation ${id}:`, {
messageCount: conversation.messages.length,
lastMessage: conversation.messages[conversation.messages.length - 1]
});
});Compatible Packages
Works seamlessly with:
- @redhat-cloud-services/arh-client - Intelligent Front Door (IFD) API client
- @redhat-cloud-services/lightspeed-client - OpenShift Lightspeed API client
- @redhat-cloud-services/ai-react-state - React hooks and context provider
- Any custom client implementing @redhat-cloud-services/ai-client-common interfaces
Building
Run nx build ai-client-state to build the library.
Running unit tests
Run nx test ai-client-state to execute the unit tests via Jest.
Development
This package follows the workspace standards:
- Event-driven architecture with proper cleanup
- Comprehensive error handling and recovery
- TypeScript strict mode with full type coverage
- Zero UI framework dependencies for maximum compatibility
