Type-safe, phase-based conversational state machine with lifecycle hooks and Redis persistence.
- Phase-based state machine — define conversation flows as numbered or named phases
- Type-safe generics —
ConversationState<TPhase, TData>parameterized to your domain - Lifecycle hooks —
onEnter,onMessage,onAction,onExitper phase - Automatic transitions — chain phases together with auto-transition support
- Redis persistence — conversation state survives server restarts
- Validation framework — required fields, custom validators, merge helpers
- Injectable logger — bring your own pino/winston or use the console default
- Handler registry — dynamic handler registration and lookup
graph TD
A[User Message/Action] --> B[ConversationOrchestrator]
B --> C[StateManager.getState]
C --> D{Active?}
D -->|No| E[Throw Error]
D -->|Yes| F[HandlerRegistry.getHandler]
F --> G[Handler.onMessage / onAction]
G --> H{nextPhase?}
H -->|No| I[Return Response]
H -->|Yes| J[Validate Transition]
J --> K[Handler.onExit]
K --> L[StateManager.transitionPhase]
L --> M[NewHandler.onEnter]
M --> N{Chained Transition?}
N -->|Yes| J
N -->|No| I
subgraph Redis
O[(State)] --- P[(Transition Log)]
end
C -.-> O
L -.-> P
npm installimport type { PhaseConfig } from "conversational-flow-engine";
enum OrderPhase {
WELCOME = 1,
SELECT_ITEM = 2,
CHECKOUT = 3,
CONFIRMATION = 4,
}
interface OrderData {
item?: string;
quantity?: number;
email?: string;
}
const transitions = new Map<OrderPhase, OrderPhase[]>([
[OrderPhase.WELCOME, [OrderPhase.SELECT_ITEM]],
[OrderPhase.SELECT_ITEM, [OrderPhase.CHECKOUT]],
[OrderPhase.CHECKOUT, [OrderPhase.CONFIRMATION, OrderPhase.SELECT_ITEM]],
[OrderPhase.CONFIRMATION, []],
]);import { BasePhaseHandler } from "conversational-flow-engine";
class WelcomeHandler extends BasePhaseHandler<OrderPhase, OrderData> {
constructor() {
super({
config: {
phase: OrderPhase.WELCOME,
name: "Welcome",
description: "Greet the user",
required: true,
canSkip: false,
canGoBack: false,
autoTransition: false,
},
validTransitions: [OrderPhase.SELECT_ITEM],
totalPhases: 4,
});
}
async onEnter(state) {
return this.createResponse("Welcome! What would you like to order?", {
nextPhase: OrderPhase.SELECT_ITEM,
});
}
async onMessage(state, message) {
return this.createResponse("Let me help you find something.", {
nextPhase: OrderPhase.SELECT_ITEM,
});
}
async onAction(state, action, data) {
return this.createResponse("Let's get started!");
}
}import Redis from "ioredis";
import {
ConversationOrchestrator,
PhaseHandlerRegistry,
} from "conversational-flow-engine";
const redis = new Redis();
const registry = new PhaseHandlerRegistry<OrderPhase, OrderData>();
registry.register(new WelcomeHandler());
// ... register other handlers
const orchestrator = new ConversationOrchestrator<OrderPhase, OrderData>({
redis,
handlerRegistry: registry,
initialPhase: OrderPhase.WELCOME,
validTransitions: transitions,
});
// Start a conversation
const result = await orchestrator.startConversation("session-123");
console.log(result.response.message);
// Handle user messages
const msgResult = await orchestrator.handleMessage({
sessionId: "session-123",
message: "I want a large coffee",
});
// Handle button actions
const actionResult = await orchestrator.handleAction({
sessionId: "session-123",
action: "confirm_order",
});src/
├── index.ts # Barrel exports
├── types.ts # Generic types (ConversationState, PhaseHandler, etc.)
├── logger.ts # Injectable logger interface + console default
├── orchestrator.ts # Main orchestrator (message routing, transitions)
├── state-manager.ts # Redis-only state persistence
├── base-handler.ts # Abstract base class with validation helpers
└── handler-registry.ts # Dynamic handler registration
examples/
└── pizza-ordering/ # Complete 5-phase example
├── index.ts
├── phases.ts
└── handlers/
├── welcome.handler.ts
├── size.handler.ts
└── confirmation.handler.ts
The core state object, parameterized by your phase enum and data type:
interface ConversationState<TPhase, TData> {
conversationId: string;
sessionId: string;
currentPhase: TPhase;
phaseData: Record<string, any>; // Temporary, reset on transition
collectedData: TData; // Persistent across phases
status: ConversationStatus;
// ... timestamps, metadata
}Each phase handler implements four lifecycle methods:
| Method | When Called | Purpose |
|---|---|---|
onEnter |
Transitioning into this phase | Show initial UI/message |
onMessage |
User sends text in this phase | Process conversational input |
onAction |
User clicks a button/action | Handle discrete interactions |
onExit |
Transitioning out of this phase | Cleanup, logging |
Transitions are validated against a Map<TPhase, TPhase[]>:
// Only these transitions are allowed
const transitions = new Map([
[Phase.A, [Phase.B, Phase.C]], // A can go to B or C
[Phase.B, [Phase.C]], // B can only go to C
[Phase.C, []], // C is terminal
]);All state is stored in Redis with configurable TTL:
- State key:
{prefix}:state:{sessionId} - Transitions key:
{prefix}:transitions:{conversationId} - Default TTL: 2 hours
MIT