npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@redhat-cloud-services/ai-client-state

v0.15.0

Published

State management for AI client conversations with event-driven architecture and cross-package compatibility.

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 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';

// 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 conversation

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

// 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 promotion

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:

// 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 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

// 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 limitations

Subscribing 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:

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