@rcs-lang/csm
@rcs-lang/csm
Section titled “@rcs-lang/csm”A lightweight, TypeScript-first state machine library designed specifically for RCS conversational agents. CSM provides a simple, performant way to manage conversation flow state across stateless HTTP requests.
Key Features
Section titled “Key Features”- 🪶 Lightweight: ~3KB minified, designed for serverless
- 🔄 State Persistence: Serialize/deserialize state between requests
- 🔗 URL-Safe: Compact state representation for URL parameters
- 🎯 Simple API: Single callback for all state transitions
- 🧩 Composable: Connect multiple flows into complex agents
- 🌊 Flow Invocations: Flows can invoke other flows with result handling
- 🔧 JSON Logic Conditions: Safe, serializable conditions with JS fallback
- 📊 Scoped Context: Three-level context system (conversation/flow/params)
- 🎚️ Multi-Flow Machines: Single machine definition with multiple flows
- 📦 Minimal Dependencies: Only neverthrow and json-logic-js
- 🏃 Fast: Optimized for request/response cycle performance
Installation
Section titled “Installation”npm install @rcs-lang/csmpnpm add @rcs-lang/csmyarn add @rcs-lang/csmbun add @rcs-lang/csmQuick Start
Section titled “Quick Start”import { ConversationalAgent, type MachineDefinitionJSON } from '@rcs-lang/csm';
// Define your machine (usually generated from RCL)const coffeeShopMachine: MachineDefinitionJSON = { id: 'CoffeeShopBot', initialFlow: 'OrderFlow', flows: { OrderFlow: { id: 'OrderFlow', initial: 'Welcome', states: { Welcome: { transitions: [ { pattern: 'Order Coffee', target: 'ChooseSize' }, { pattern: 'View Menu', target: 'ShowMenu' } ] }, ChooseSize: { transitions: [ { pattern: 'Small', target: 'ChooseDrink', context: { size: 'small', price: 3.50 } }, { pattern: 'Medium', target: 'ChooseDrink', context: { size: 'medium', price: 4.50 } } ] } } } }};
// Create agent with state change handlerconst agent = new ConversationalAgent({ id: 'CoffeeBot', onStateChange: async (event) => { console.log(`Entering state: ${event.state}`); // Send message, log analytics, etc. await sendMessage(event.context.userId, messages[event.state]); }});
// Add machine (supports both single-flow and multi-flow machines)agent.addMachine(coffeeShopMachine);
// Process user inputconst response = await agent.processInput('Order Coffee');// response.state = 'ChooseSize'// response.machine = 'OrderFlow'
// Serialize for next requestconst stateHash = agent.toURLHash();// "Q29mZmVlQm90Ok9yZGVyRmxvdzpDaG9vc2VTaXplOnt9"
// Restore in next requestconst restoredAgent = ConversationalAgent.fromURLHash(stateHash, { id: 'CoffeeBot', onStateChange: async (event) => { await sendMessage(event.context.userId, messages[event.state]); }});Machine Definition Schema
Section titled “Machine Definition Schema”The core of the CSM package is the MachineDefinitionJSON interface, which defines the structure for conversational state machines.
Basic Structure
Section titled “Basic Structure”A machine definition consists of:
interface MachineDefinitionJSON { id: string; // Unique identifier for the machine initial: string; // ID of the starting state states: Record<string, StateDefinitionJSON>; // Map of state definitions meta?: MachineMetadata; // Optional metadata}State Definition
Section titled “State Definition”Each state in the machine is defined by:
interface StateDefinitionJSON { transitions: TransitionJSON[]; // Array of possible transitions meta?: StateMetadata; // Optional state metadata}Transition Definition
Section titled “Transition Definition”Transitions define how to move between states:
interface TransitionJSON { pattern?: string; // Pattern to match user input (optional for auto-transitions) target?: string; // Target reference using type:ID format (state:Name, flow:Name, @variable, :end/:cancel/:error) context?: Record<string, any>; // Context updates to apply condition?: string | ConditionObject; // Condition for this transition (see Conditions section) priority?: number; // Priority for pattern matching (higher = first) flowInvocation?: { // Flow invocation with result handling flowId: string; // ID of the flow to invoke parameters?: Record<string, any>; // Parameters to pass to the flow onResult: { // Handlers for different flow outcomes end?: { operations?: Array<ContextOperation>; // Operations before transitioning target: string; // Target after successful completion }; cancel?: { operations?: Array<ContextOperation>; // Operations before transitioning target: string; // Target if user cancels }; error?: { operations?: Array<ContextOperation>; // Operations before transitioning target: string; // Target if error occurs }; }; };}
// Condition typestype ConditionObject = | { type: "code"; expression: string } // JavaScript code | { type: "jsonlogic"; rule: JSONLogicRule }; // JSON Logic ruleMetadata
Section titled “Metadata”Both machines and states can have optional metadata:
// Machine metadatainterface MachineMetadata { name?: string; // Display name description?: string; // Description version?: string; // Version tags?: string[]; // Categorization tags custom?: Record<string, any>; // Custom properties}
// State metadatainterface StateMetadata { messageId?: string; // Message to send when entering state transient?: boolean; // Auto-transition without user input tags?: string[]; // Categorization tags custom?: Record<string, any>; // Custom properties}Creating Machine Definitions
Section titled “Creating Machine Definitions”Simple Example
Section titled “Simple Example”Here’s a basic machine definition for a greeting flow:
{ "id": "GreetingFlow", "initial": "welcome", "states": { "welcome": { "transitions": [ { "pattern": "hello|hi|hey", "target": "greeting_response" }, { "pattern": ":default", "target": "help" } ], "meta": { "messageId": "welcome_message" } }, "greeting_response": { "transitions": [ { "target": "end" } ], "meta": { "messageId": "greeting_reply", "transient": true } }, "help": { "transitions": [ { "target": "welcome" } ], "meta": { "messageId": "help_message", "transient": true } }, "end": { "transitions": [], "meta": { "messageId": "goodbye" } } }, "meta": { "name": "Greeting Flow", "description": "Handles basic greetings and help", "version": "1.0.0", "tags": ["greeting", "basic"] }}Advanced Example with Context
Section titled “Advanced Example with Context”{ "id": "UserProfileFlow", "initial": "collect_name", "states": { "collect_name": { "transitions": [ { "pattern": ".*", "target": "collect_email", "context": { "name": "$input" } } ], "meta": { "messageId": "ask_name" } }, "collect_email": { "transitions": [ { "pattern": "\\S+@\\S+\\.\\S+", "target": "confirmation", "context": { "email": "$input" } }, { "pattern": ".*", "target": "invalid_email" } ], "meta": { "messageId": "ask_email" } }, "invalid_email": { "transitions": [ { "target": "collect_email" } ], "meta": { "messageId": "invalid_email_message", "transient": true } }, "confirmation": { "transitions": [ { "pattern": "yes|confirm|ok", "target": "complete" }, { "pattern": "no|cancel", "target": "collect_name" } ], "meta": { "messageId": "confirm_details" } }, "complete": { "transitions": [], "meta": { "messageId": "profile_saved" } } }}Reference Format and Flow Control
Section titled “Reference Format and Flow Control”CSM supports several target reference formats:
- State references:
state:StateName- Navigate to a state in the current flow - Flow references:
flow:FlowName- Navigate to another flow - Message references:
message:MessageName- Jump to a specific message - Context variables:
@variableName- Dynamic targets from context - Flow termination:
:ok,:cancel,:error- End flow with result
Flow Invocation with Result Handling
Section titled “Flow Invocation with Result Handling”Use flowInvocation for complex flow control with explicit result handling:
{ "id": "TopFlow", "initial": "Welcome", "states": { "Welcome": { "transitions": [ { "pattern": "Start Order", "flowInvocation": { "flowId": "CreateOrder", "onResult": { "ok": { "operations": [ { "append": { "to": "orders", "value": {"var": "result"} } } ], "target": "state:ConfirmAllOrders" }, "cancel": { "target": "state:Welcome" }, "error": { "target": "state:OrderError" } } } } ] } }}Context Operations
Section titled “Context Operations”Operations allow you to manipulate context data when handling flow results:
- Set:
{"set": {"variable": "name", "value": {...}}} - Append:
{"append": {"to": "arrayName", "value": {...}}} - Merge:
{"merge": {"into": "objectName", "value": {...}}}
Values support JSONLogic expressions, including {"var": "result"} to access the flow’s return value.
Context Variable Resolution
Section titled “Context Variable Resolution”CSM supports dynamic context resolution using the @variable syntax:
{ "transitions": [ { "pattern": "go", "target": "@nextState", "context": { "message": "Going to #{@nextState}" } } ]}Context variables are resolved at runtime, and string interpolation supports #{variable} syntax for dynamic message content.
Validation
Section titled “Validation”Use the validateMachineDefinition function to validate machine definitions at runtime:
import { validateMachineDefinition, type MachineDefinitionJSON } from '@rcs-lang/csm';
const definition: MachineDefinitionJSON = { // ... your machine definition};
try { if (validateMachineDefinition(definition)) { console.log('Machine definition is valid'); }} catch (error) { console.error('Validation failed:', error.message);}Note: Transitions must have either a target OR a flowInvocation - not both. This allows for both simple state transitions and complex flow invocations with result handling.
Multi-Flow Machines
Section titled “Multi-Flow Machines”CSM supports multi-flow machines, where a single machine definition contains multiple flows that can invoke each other:
interface MultiFlowMachineDefinitionJSON { id: string; initialFlow: string; // ID of the starting flow flows: Record<string, SingleFlowMachineDefinitionJSON>; // Map of flow definitions meta?: MachineMetadata;}Multi-Flow Example
Section titled “Multi-Flow Example”{ "id": "CoffeeShopBot", "initialFlow": "TopFlow", "flows": { "TopFlow": { "id": "TopFlow", "initial": "Welcome", "states": { "Welcome": { "transitions": [ { "pattern": "Start Order", "flowInvocation": { "flowId": "CreateOrder", "parameters": {"source": "welcome"}, "onResult": { "end": { "operations": [ {"append": {"to": "orders", "value": {"var": "result"}}} ], "target": "ConfirmAllOrders" }, "cancel": {"target": "Welcome"}, "error": {"target": "OrderError"} } } } ] } } }, "CreateOrder": { "id": "CreateOrder", "initial": "ChooseSize", "states": { "ChooseSize": { "transitions": [ {"pattern": "small", "target": "ChooseDrink", "context": {"size": "small"}}, {"pattern": "cancel", "target": ":cancel"} ] }, "ChooseDrink": { "transitions": [ {"pattern": "coffee", "target": ":end", "context": {"drink": "coffee"}} ] } } } }}Conditions System
Section titled “Conditions System”CSM supports three types of conditions for controlling transitions:
1. Legacy String Conditions (Deprecated)
Section titled “1. Legacy String Conditions (Deprecated)”// Simple JavaScript expressions (generates deprecation warning)condition: "context.verified === true"condition: "context.user && context.user.age >= 18"2. Explicit JavaScript Code
Section titled “2. Explicit JavaScript Code”// Recommended for complex JavaScript logiccondition: { type: "code", expression: "context.user && context.user.points > 100 && context.user.verified"}3. JSON Logic (Recommended)
Section titled “3. JSON Logic (Recommended)”// Safe, serializable conditions using JSON Logiccondition: { type: "jsonlogic", rule: { "and": [ {"==": [{"var": "user.verified"}, true]}, {">": [{"var": "user.points"}, 100]} ] }}Condition Examples
Section titled “Condition Examples”{ "states": { "CheckAccess": { "transitions": [ { "pattern": "premium feature", "target": "PremiumContent", "condition": { "type": "jsonlogic", "rule": { "and": [ {"==": [{"var": "membership"}, "premium"]}, {">": [{"var": "points"}, 100]} ] } } }, { "pattern": "basic feature", "target": "BasicContent", "condition": { "type": "code", "expression": "context.membership === 'basic' || context.membership === 'premium'" } } ] } }}Scoped Context System
Section titled “Scoped Context System”CSM implements a three-level context system for proper variable isolation:
- Conversation Context: Persists across the entire agent session
- Flow Context: Isolated per individual flow execution
- Parameters Context: Temporary variables for current state/transition
interface ScopedContext { conversation: Record<string, any>; // Persists for entire session flow: Record<string, any>; // Isolated per flow params: Record<string, any>; // Current state parameters}This allows flows to maintain their own state while sharing conversation-level data.
Flow Result Handling
Section titled “Flow Result Handling”Flows can terminate with three types of results:
:end- Successful completion with return value:cancel- User-initiated cancellation:error- Error condition occurred
Each result type can have its own operations and target state in the parent flow.
Pattern Matching
Section titled “Pattern Matching”The CSM package supports several pattern types:
- Literal strings: Match exact text
- Regular expressions: Full regex support
- Special patterns:
:default- Fallback pattern (lowest priority).*- Match any input$input- Capture user input in context
Usage with TypeScript
Section titled “Usage with TypeScript”The package provides full TypeScript support:
import { type MachineDefinitionJSON, type AgentDefinitionJSON, validateMachineDefinition} from '@rcs-lang/csm';
// Type-safe machine definitionconst machine: MachineDefinitionJSON = { id: 'MyFlow', initial: 'start', states: { start: { transitions: [ { pattern: 'begin', target: 'processing' } ] }, processing: { transitions: [ { target: 'end' } ], meta: { transient: true } }, end: { transitions: [] } }};
// Validation with type checkingif (validateMachineDefinition(machine)) { // Machine is valid and type-safe}Real-World Example: Coffee Shop Agent
Section titled “Real-World Example: Coffee Shop Agent”Here’s a complete machine definition for a coffee shop ordering system with multiple flows:
{ "id": "CoffeeShopAgent", "initial": "main_menu", "states": { "main_menu": { "transitions": [ { "pattern": "order|coffee|buy", "target": "machine:OrderFlow" }, { "pattern": "menu|options|what", "target": "show_menu" }, { "pattern": "help|support", "target": "machine:HelpFlow" }, { "pattern": ":default", "target": "welcome" } ], "meta": { "messageId": "main_menu" } }, "welcome": { "transitions": [ { "target": "main_menu" } ], "meta": { "messageId": "welcome_message", "transient": true } }, "show_menu": { "transitions": [ { "pattern": "order", "target": "machine:OrderFlow" }, { "target": "main_menu" } ], "meta": { "messageId": "menu_display", "transient": true } } }, "meta": { "name": "Coffee Shop Main Agent", "description": "Main entry point for coffee shop interactions", "version": "2.0.0" }}And the OrderFlow machine:
{ "id": "OrderFlow", "initial": "choose_size", "states": { "choose_size": { "transitions": [ { "pattern": "small|s", "target": "choose_drink", "context": { "size": "small", "price": 3.50 } }, { "pattern": "medium|m", "target": "choose_drink", "context": { "size": "medium", "price": 4.00 } }, { "pattern": "large|l", "target": "choose_drink", "context": { "size": "large", "price": 4.50 } }, { "pattern": ".*", "target": "invalid_size" } ], "meta": { "messageId": "choose_size" } }, "invalid_size": { "transitions": [ { "target": "choose_size" } ], "meta": { "messageId": "invalid_size_message", "transient": true } }, "choose_drink": { "transitions": [ { "pattern": "coffee|americano", "target": "customize", "context": { "drink": "coffee" } }, { "pattern": "latte", "target": "customize", "context": { "drink": "latte" } }, { "pattern": "cappuccino", "target": "customize", "context": { "drink": "cappuccino" } }, { "pattern": ".*", "target": "invalid_drink" } ], "meta": { "messageId": "choose_drink" } }, "invalid_drink": { "transitions": [ { "target": "choose_drink" } ], "meta": { "messageId": "invalid_drink_message", "transient": true } }, "customize": { "transitions": [ { "pattern": "regular|whole", "target": "confirm_order", "context": { "milk": "regular milk", "extraCharge": 0 } }, { "pattern": "almond|soy|oat", "target": "confirm_order", "context": { "milk": "$input milk", "extraCharge": 0.60 } }, { "pattern": "skip|no|none", "target": "confirm_order", "context": { "milk": "none", "extraCharge": 0 } }, { "pattern": ".*", "target": "invalid_milk" } ], "meta": { "messageId": "customize_message" } }, "invalid_milk": { "transitions": [ { "target": "customize" } ], "meta": { "messageId": "invalid_milk_message", "transient": true } }, "confirm_order": { "transitions": [ { "pattern": "yes|confirm|ok", "target": "place_order" }, { "pattern": "no|cancel|change", "target": "choose_size", "context": { "size": null, "drink": null, "milk": null, "price": 0, "extraCharge": 0 } } ], "meta": { "messageId": "confirm_order" } }, "place_order": { "transitions": [ { "target": "machine:CoffeeShopAgent", "context": { "orderComplete": true, "orderId": "$generateOrderId" } } ], "meta": { "messageId": "order_placed", "transient": true } } }, "meta": { "name": "Coffee Order Flow", "description": "Handles the complete coffee ordering process", "version": "1.2.0", "tags": ["ordering", "coffee", "ecommerce"] }}Multi-Machine Agent Definition
Section titled “Multi-Machine Agent Definition”For complex agents with multiple flows, use AgentDefinitionJSON:
{ "id": "CoffeeShopBot", "initial": "CoffeeShopAgent", "machines": { "CoffeeShopAgent": { "id": "CoffeeShopAgent", "initial": "main_menu", "states": { // ... main agent states } }, "OrderFlow": { "id": "OrderFlow", "initial": "choose_size", "states": { // ... order flow states } }, "HelpFlow": { "id": "HelpFlow", "initial": "help_menu", "states": { "help_menu": { "transitions": [ { "pattern": "hours|time", "target": "show_hours" }, { "pattern": "location|address", "target": "show_location" }, { "pattern": "back|menu", "target": "machine:CoffeeShopAgent" } ], "meta": { "messageId": "help_options" } }, "show_hours": { "transitions": [ { "target": "help_menu" } ], "meta": { "messageId": "store_hours", "transient": true } }, "show_location": { "transitions": [ { "target": "help_menu" } ], "meta": { "messageId": "store_location", "transient": true } } } } }, "meta": { "name": "Coffee Shop Bot", "description": "Complete coffee shop ordering and support system", "version": "2.0.0" }}Usage Patterns
Section titled “Usage Patterns”Serverless Function
Section titled “Serverless Function”Perfect for AWS Lambda, Vercel, Netlify Functions, or similar:
export async function handleMessage(request: Request) { const { stateHash, userInput } = await request.json();
// Restore agent state const agent = stateHash ? ConversationalAgent.fromURLHash(stateHash, { id: 'CoffeeBot', onStateChange: async (event) => { // Log state change await logAnalytics(event);
// Get message for state const message = getMessageForState(event.state);
// Store response to send back response.message = message; } }) : createNewAgent();
// Process input const result = await agent.processInput(userInput);
// Return response with new state return Response.json({ message: response.message, stateHash: agent.toURLHash(), suggestions: getSuggestionsForState(result.state) });}Express.js Integration
Section titled “Express.js Integration”app.post('/conversation', async (req, res) => { const agent = createOrRestoreAgent(req.body.stateHash);
const result = await agent.processInput(req.body.input);
res.json({ state: result, hash: agent.toURLHash() });});Multi-Flow Composition
Section titled “Multi-Flow Composition”// Import reusable flowsimport { ContactSupportFlow } from '@rcs-lang/common-flows';
// Define custom flowconst customFlow: FlowDefinition = { id: 'MainMenu', initial: 'Welcome', states: { Welcome: { transitions: [ { pattern: 'Support', target: 'machine:ContactSupport' } ] } }};
// Compose agentconst agent = new ConversationalAgent({ id: 'MyBot', onStateChange });agent.addFlow(customFlow);agent.addFlow(ContactSupportFlow);API Reference
Section titled “API Reference”The ConversationalAgent class provides the main interface:
class ConversationalAgent { constructor(options: AgentOptions);
// Machine management (supports both single-flow and multi-flow machines) addMachine(machine: MachineDefinitionJSON): void; addFlow(flow: FlowDefinition): void; // Legacy support for single flows removeFlow(flowId: string): void;
// State processing processInput(input: string): Promise<ProcessResult>;
// Serialization toURLHash(): string; static fromURLHash(hash: string, options: AgentOptions): ConversationalAgent;
// State access getCurrentState(): { machine: string; state: string }; getContext(): Context; updateContext(updates: Partial<Context>): void; setState(machineId: string, stateId: string): void; // For restoration}Performance
Section titled “Performance”- Minimal Overhead: ~1ms to process typical state transition
- Compact State: Average URL hash ~100-200 characters
- Memory Efficient: No persistence between requests
- Fast Serialization: Optimized JSON encoding
- Pattern Caching: Compiled patterns cached per flow
For detailed API documentation, see the CSM package README and source code.