@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
IAIClient
interface
- 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-state
Quick Start
import { createClientStateManager } from '@redhat-cloud-services/ai-client-state';
import { IFDClient } from '@redhat-cloud-services/arh-client';
const client = new IFDClient({
baseUrl: 'https://your-api.com',
fetchFunction: (input, init) => fetch(input, init)
});
const stateManager = createClientStateManager(client);
await stateManager.init();
await stateManager.sendMessage('Hello AI!');
Critical: 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
const client = new IFDClient({
baseUrl: 'https://your-api.com',
fetchFunction: fetch
});
const customFetch = function(input, init) {
return fetch(input, init);
};
✅ Correct - Arrow Functions (Recommended)
const client = new IFDClient({
baseUrl: 'https://your-api.com',
fetchFunction: (input, init) => fetch(input, init)
});
✅ Correct - Bound Functions
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';
async function getToken(): Promise<string> {
return 'your-jwt-token-here';
}
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
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'
}
});
if (response.status === 401) {
const newToken = await getToken();
return fetch(input, {
...init,
headers: {
...init?.headers,
'Authorization': `Bearer ${newToken}`
}
});
}
return response;
} catch (error) {
console.error('Authentication error:', error);
throw error;
}
}
});
return createClientStateManager(client);
};
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();
await stateManager.sendMessage('Hello');
const isTemp = stateManager.isTemporaryConversation();
Key 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:
const conversation = await stateManager.createNewConversation();
await stateManager.setActiveConversationId(conversation.id);
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>
> = {
init(): Promise<void>;
isInitialized(): boolean;
isInitializing(): boolean;
setActiveConversationId(conversationId: string): Promise<void>;
getActiveConversationId(): string | null;
getActiveConversationMessages(): Message<T>[];
getConversations(): Conversation<T>[];
createNewConversation(force?: boolean): Promise<IConversation>;
isTemporaryConversation(): boolean;
sendMessage(query: UserQuery, options?: MessageOptions): Promise<any>;
getMessageInProgress(): boolean;
getState(): ClientState<T>;
getInitLimitation(): ClientInitLimitation | undefined;
subscribe(event: Events, callback: () => void): () => void;
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);
await stateManager.init();
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';
interface Message<T = Record<string, unknown>> {
id: string;
answer: string;
role: 'user' | 'bot';
additionalAttributes?: T;
date: Date;
}
interface Conversation<T = Record<string, unknown>> {
id: string;
title: string;
messages: Message<T>[];
locked: boolean;
createdAt: Date;
}
await stateManager.sendMessage('What is OpenShift?');
const messages = stateManager.getActiveConversationMessages();
console.log('User message:', messages[0]);
console.log('Bot response:', messages[1]);
Conversation Management
await stateManager.setActiveConversationId('conv-123');
const conversations = stateManager.getConversations();
console.log('All conversations:', conversations);
const newConversation = await stateManager.createNewConversation();
console.log('Created conversation:', newConversation.id);
const messages = stateManager.getActiveConversationMessages();
const state = stateManager.getState();
console.log('All conversations:', state.conversations);
console.log('Active conversation:', state.activeConversationId);
Sending Messages
Important: The sendMessage
method takes a string (UserQuery
), not a Message
object. The state manager automatically creates Message
objects internally for both user input and bot responses.
Basic Message Sending
const response = await stateManager.sendMessage('Explain Kubernetes pods');
console.log('Bot response:', response);
Streaming Messages
await stateManager.sendMessage('Tell me about container orchestration', { stream: true });
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',
};
await stateManager.sendMessage('Your message here', options);
Event System
Available Events
import { Events } from '@redhat-cloud-services/ai-client-state';
Subscribing to Events
const unsubscribeMessages = stateManager.subscribe(Events.MESSAGE, () => {
const messages = stateManager.getActiveConversationMessages();
console.log('Messages updated:', messages.length);
});
const unsubscribeConversation = stateManager.subscribe(Events.ACTIVE_CONVERSATION, () => {
const state = stateManager.getState();
console.log('Active conversation changed:', state.activeConversationId);
});
const unsubscribeProgress = stateManager.subscribe(Events.IN_PROGRESS, () => {
const isInProgress = stateManager.getMessageInProgress();
console.log('Message in progress:', isInProgress);
});
const unsubscribeConversations = stateManager.subscribe(Events.CONVERSATIONS, () => {
const conversations = stateManager.getConversations();
console.log('Conversations updated:', conversations.length);
});
const unsubscribeInitializing = stateManager.subscribe(Events.INITIALIZING_MESSAGES, () => {
const isInitializing = stateManager.isInitializing();
console.log('Messages initializing:', isInitializing);
});
unsubscribeMessages();
unsubscribeConversation();
unsubscribeProgress();
unsubscribeConversations();
unsubscribeInitializing();
Progress Tracking
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>>> {
return {
messageId: crypto.randomUUID(),
answer: 'Custom response',
conversationId,
additionalAttributes: {}
};
}
}
const customClient = new CustomClient();
const stateManager = createClientStateManager(customClient);
Advanced Usage
Multiple Conversation Handling
await stateManager.init();
const conversation1 = await stateManager.createNewConversation();
const conversation2 = await stateManager.createNewConversation();
await stateManager.setActiveConversationId(conversation1.id);
await stateManager.sendMessage('First message');
await stateManager.setActiveConversationId(conversation2.id);
await stateManager.sendMessage('Second message');
const allConversations = stateManager.getConversations();
console.log('Total conversations:', allConversations.length);
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);
const messages = stateManager.getActiveConversationMessages();
}
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
});
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:
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