working commit

Working

deleted test files
This commit is contained in:
Pratik Narola 2025-07-10 14:34:40 +05:30
commit 885bd1075b
13 changed files with 5821 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
node_modules/
dist/
.workflows/
.test-workflows/
*.log
.DS_Store

View file

@ -0,0 +1,42 @@
{
"name": "doc-review-structural",
"description": "Document review workflow for testing judges",
"states": {
"draft": {
"transitions": {
"submit": "reviewing"
}
},
"reviewing": {
"transitions": {
"approve": "approved",
"reject": "draft",
"request_changes": "changes_requested"
}
},
"changes_requested": {
"transitions": {
"resubmit": "reviewing"
}
},
"approved": {
"final": true
}
},
"initialState": "draft",
"stateValidators": {
"reviewing": {
"requiredFields": [
"documentId",
"reviewer"
]
}
},
"transitionValidators": {},
"judgeConfig": {
"enabled": true,
"useLLM": false,
"strictMode": true,
"minConfidence": 0.7
}
}

View file

@ -0,0 +1,11 @@
{
"id": "wf-54213a57-b92e-4ca2-8d4c-19fe6ebde222",
"definitionName": "doc-review-structural",
"currentState": "draft",
"context": {
"documentId": "DOC-001",
"title": "API Design Document"
},
"createdAt": "2025-07-10T07:45:02.310Z",
"updatedAt": "2025-07-10T07:45:02.310Z"
}

600
README.md Normal file
View file

@ -0,0 +1,600 @@
# Generic DFA MCP Server
A Model Context Protocol (MCP) server that provides a generic deterministic finite automata (DFA) based workflow management system for LLMs.
## Purpose
This server helps LLMs follow any structured workflow without losing context or randomly marking tasks as complete. It allows dynamic definition of custom workflows with states, transitions, and actions, ensuring agents follow predefined paths through complex multi-step processes.
## Key Features
- **Generic Workflow Engine**: Define any workflow with custom states and transitions
- **Dynamic Registration**: Create new workflow types on the fly via MCP tools
- **Context Preservation**: Maintains full context throughout workflow execution
- **Checkpoint & Rollback**: Save and restore workflow states for error recovery
- **No Hardcoded Logic**: Completely generic - no assumptions about workflow purpose
- **Judge System**: Intelligent validation of transitions with confidence scoring
- **LLM-Powered Judge**: Optional AI-powered validation for deeper insights
- **Pre-validation**: Check if transitions are valid before executing them
- **Custom Validators**: Define business rules and requirements for each workflow
## Installation
```bash
cd dfa-mcp-server
npm install
npm run build
```
## Running the Server
For development:
```bash
npm run dev
```
For production:
```bash
npm start
```
## Available Tools
### workflow.define
Define a new workflow type with custom states and transitions.
**Input:**
- `name`: Unique workflow name
- `description`: Optional workflow description
- `states`: Object defining states and their transitions
- `initialState`: Starting state name
**Example:**
```json
{
"name": "approval-process",
"description": "Document approval workflow",
"states": {
"draft": {
"transitions": { "submit": "review" }
},
"review": {
"transitions": {
"approve": "approved",
"reject": "draft"
}
},
"approved": { "final": true }
},
"initialState": "draft"
}
```
### workflow.list
List all registered workflow types.
**Output:**
- `workflows`: Array of registered workflows with names and descriptions
- `count`: Total number of registered workflows
### workflow.start
Start a new instance of a defined workflow.
**Input:**
- `type`: Workflow type name (must be previously defined)
- `context`: Optional initial context data (any JSON object)
**Output:**
- `id`: Unique workflow instance ID
- `state`: Current state
- `nextActions`: Available actions from current state
- `progress`: Current progress message
### workflow.advance
Move workflow to next state by performing an action.
**Input:**
- `id`: Workflow instance ID
- `action`: Action to perform (must be in nextActions)
- `data`: Optional data to merge into context
**Output:**
- `state`: New state after transition
- `nextActions`: Available actions from new state
- `progress`: Updated progress
- `complete`: Whether workflow has reached a final state
### workflow.status
Get current status of a workflow instance.
**Input:**
- `id`: Workflow instance ID
**Output:**
- `state`: Current state
- `context`: Full workflow context (all accumulated data)
- `nextActions`: Available actions
- `progress`: Current progress
- `complete`: Whether workflow is complete
### workflow.checkpoint
Create a checkpoint to save current workflow state.
**Input:**
- `id`: Workflow instance ID
- `description`: Optional checkpoint description
- `metadata`: Optional additional metadata
**Output:**
- `checkpointId`: Unique checkpoint ID
- `workflowId`: Associated workflow ID
- `state`: State at checkpoint
- `timestamp`: When checkpoint was created
- `description`: Checkpoint description
### workflow.rollback
Rollback workflow to a previous checkpoint.
**Input:**
- `id`: Workflow instance ID
- `checkpointId`: ID of checkpoint to rollback to
**Output:**
- `state`: Restored state
- `context`: Restored context
- `nextActions`: Available actions from restored state
- `progress`: Progress after rollback
- `message`: Success message
### workflow.listCheckpoints
List all checkpoints for a workflow.
**Input:**
- `id`: Workflow instance ID
**Output:**
- `workflowId`: Workflow ID
- `checkpoints`: Array of checkpoints (sorted by most recent first)
- `count`: Total number of checkpoints
### workflow.judge.validate
Validate a transition without executing it. Useful for pre-checking if an action is valid.
**Input:**
- `id`: Workflow instance ID
- `action`: Action to validate
- `data`: Optional data for the action
**Output:**
- `approved`: Whether the transition would be allowed
- `confidence`: Confidence score (0-1)
- `reasoning`: Human-readable explanation
- `violations`: List of validation failures (if any)
- `suggestions`: Helpful suggestions for fixing issues
### workflow.judge.config
Configure judge settings for a workflow.
**Input:**
- `name`: Workflow type name
- `enabled`: Enable/disable judge
- `strictMode`: Optional - reject low confidence transitions
- `minConfidence`: Optional - minimum confidence threshold (0-1)
**Output:**
- `success`: Configuration update status
- `message`: Confirmation message
- `config`: Updated configuration
### workflow.judge.history
Get the history of judge decisions for a workflow instance.
**Input:**
- `id`: Workflow instance ID
**Output:**
- `workflowId`: Workflow ID
- `decisions`: Array of judge decisions
- `count`: Total number of decisions
## Example Workflows with Judge
### 1. Document Review with Validation
```json
{
"name": "document-review",
"description": "Document review with strict validation",
"states": {
"draft": {
"transitions": { "submit": "reviewing" }
},
"reviewing": {
"transitions": {
"approve": "approved",
"reject": "draft"
}
},
"approved": { "final": true }
},
"initialState": "draft",
"judgeConfig": {
"enabled": true,
"strictMode": true,
"minConfidence": 0.8
},
"stateValidators": {
"reviewing": {
"requiredFields": ["documentId", "reviewer"]
}
},
"transitionValidators": {
"approve": "(data, context) => ({ valid: data.comments?.length >= 20, confidence: 1.0, reason: 'Detailed comments required' })"
}
}
```
## Example Workflows
### 2. Todo Item Tracker
```json
{
"name": "todo-tracker",
"description": "Track todo items through their lifecycle",
"states": {
"created": {
"transitions": {
"start": "in_progress",
"cancel": "cancelled"
}
},
"in_progress": {
"transitions": {
"complete": "done",
"pause": "paused",
"cancel": "cancelled"
}
},
"paused": {
"transitions": {
"resume": "in_progress",
"cancel": "cancelled"
}
},
"done": { "final": true },
"cancelled": { "final": true }
},
"initialState": "created"
}
```
### 2. Deployment Pipeline
```json
{
"name": "deployment-pipeline",
"description": "Software deployment process",
"states": {
"ready": {
"transitions": { "deploy": "deploying" }
},
"deploying": {
"transitions": {
"success": "testing",
"failure": "failed"
}
},
"testing": {
"transitions": {
"pass": "live",
"fail": "rollback"
}
},
"rollback": {
"transitions": { "complete": "ready" }
},
"live": { "final": true },
"failed": { "final": true }
},
"initialState": "ready"
}
```
### 3. Multi-Step Form
```json
{
"name": "form-wizard",
"description": "Multi-step form submission",
"states": {
"step1": {
"transitions": {
"next": "step2",
"save": "draft"
}
},
"step2": {
"transitions": {
"next": "step3",
"back": "step1",
"save": "draft"
}
},
"step3": {
"transitions": {
"submit": "processing",
"back": "step2",
"save": "draft"
}
},
"draft": {
"transitions": { "resume": "step1" }
},
"processing": {
"transitions": {
"success": "complete",
"error": "step3"
}
},
"complete": { "final": true }
},
"initialState": "step1"
}
```
## Complete Example Usage
```typescript
// 1. Define a custom workflow
await callTool('workflow.define', {
name: 'code-review',
description: 'Code review process',
states: {
submitted: {
transitions: { 'assign': 'in_review' }
},
in_review: {
transitions: {
'request_changes': 'changes_requested',
'approve': 'approved',
'reject': 'rejected'
}
},
changes_requested: {
transitions: { 'resubmit': 'in_review' }
},
approved: { final: true },
rejected: { final: true }
},
initialState: 'submitted'
});
// 2. Start a workflow instance
const result = await callTool('workflow.start', {
type: 'code-review',
context: {
pr_number: 123,
author: 'developer@example.com',
files_changed: 5
}
});
// Returns: { id: 'wf-123', state: 'submitted', nextActions: ['assign'] }
// 3. Assign reviewer
await callTool('workflow.advance', {
id: 'wf-123',
action: 'assign',
data: {
reviewer: 'senior@example.com',
assigned_at: new Date().toISOString()
}
});
// 4. Create checkpoint before making decision
const checkpoint = await callTool('workflow.checkpoint', {
id: 'wf-123',
description: 'Before review decision'
});
// 5. Request changes
await callTool('workflow.advance', {
id: 'wf-123',
action: 'request_changes',
data: {
comments: ['Please add tests', 'Update documentation']
}
});
// 6. If needed, rollback to checkpoint
await callTool('workflow.rollback', {
id: 'wf-123',
checkpointId: checkpoint.checkpointId
});
// 7. Approve instead
await callTool('workflow.advance', {
id: 'wf-123',
action: 'approve',
data: {
approved_at: new Date().toISOString(),
merge_strategy: 'squash'
}
});
```
## File Structure
Workflows are persisted in the `.workflows` directory:
- `definitions/`: Saved workflow definitions
- `wf-{id}.json`: Current state and context
- `wf-{id}.log`: Transition history (append-only log)
- `checkpoints/`: Saved checkpoints
## Why Generic DFA?
This generic approach solves the common problem where LLMs:
- Lose track of their position in multi-step processes
- Skip required steps or prematurely mark tasks complete
- Forget context between interactions
- Fail to follow defined procedures consistently
By allowing dynamic workflow definition, any process can be modeled:
- Approval workflows
- State machines
- Multi-step wizards
- Pipeline processes
- Task lifecycles
- Any sequential process with defined states
## LLM-Powered Judge (Optional)
Enable AI-powered validation by setting environment variables in your MCP configuration:
```json
"env": {
"LLM_BASE_URL": "https://api.openai.com", // Or any OpenAI-compatible endpoint
"LLM_JUDGE_MODEL": "gpt-4", // Model to use
"LLM_API_KEY": "sk-your-api-key", // Your API key
"LLM_JUDGE_THINKING_MODE": "high" // Thinking depth (optional)
}
```
### Supported Providers
Works with any OpenAI-compatible API:
- OpenAI (GPT-4, GPT-3.5)
- Anthropic Claude (via proxy)
- Google Gemini (via proxy)
- Local LLMs (LM Studio, Ollama)
- Custom endpoints
### Using LLM Judge
```json
{
"judgeConfig": {
"enabled": true,
"useLLM": true, // Enable LLM validation
"strictMode": true,
"minConfidence": 0.8
}
}
```
### LLM vs Structural Judge
- **Structural Judge**: Fast, rule-based, deterministic
- **LLM Judge**: Understands context, provides nuanced feedback, catches semantic issues
- **Fallback**: If LLM fails, automatically uses structural validation
## Judge System Benefits
The intelligent judge system improves workflow accuracy by:
### 1. **Preventing Invalid Transitions**
- Validates transitions before execution
- Ensures all prerequisites are met
- Prevents state corruption
### 2. **Enforcing Business Rules**
- Custom validators for each workflow
- Required field validation
- Complex condition checking
### 3. **Confidence Scoring**
- Quantifies transition validity (0-1 scale)
- Identifies uncertain operations
- Enables risk-based decisions
### 4. **Helpful Feedback**
- Clear explanations of rejections
- Specific violation details
- Actionable suggestions for fixes
### 5. **Improved LLM Behavior**
- Guides LLMs to follow rules correctly
- Reduces trial-and-error attempts
- Teaches through detailed feedback
### Example Judge in Action:
**Structural Judge:**
```
LLM attempts: workflow.advance(id: "wf-123", action: "approve", data: {})
Judge rejects: "Missing required approval comments (min 20 chars)"
```
**LLM Judge (with same attempt):**
```
LLM attempts: workflow.advance(id: "wf-123", action: "approve", data: {})
Judge rejects: "Approval without comments lacks accountability. In document
review workflows, approvals should include: 1) What was reviewed,
2) Key findings, 3) Any conditions. This creates an audit trail."
Suggestions: ["Add detailed approval comments", "Include review findings",
"Mention any follow-up requirements"]
```
The LLM judge provides richer, context-aware feedback!
## Adding to Claude Desktop
Add to your Claude Desktop configuration:
```json
{
"mcpServers": {
"dfa-workflow": {
"command": "node",
"args": ["/path/to/dfa-mcp-server/dist/index.js"],
"env": {
"LLM_BASE_URL": "https://api.openai.com",
"LLM_JUDGE_MODEL": "gpt-4",
"LLM_API_KEY": "sk-your-api-key-here",
"LLM_JUDGE_THINKING_MODE": "high"
}
}
}
}
```
### Configuration Examples
**Without LLM Judge (default):**
```json
{
"mcpServers": {
"dfa-workflow": {
"command": "node",
"args": ["/path/to/dfa-mcp-server/dist/index.js"]
}
}
}
```
**With OpenAI:**
```json
{
"mcpServers": {
"dfa-workflow": {
"command": "node",
"args": ["/path/to/dfa-mcp-server/dist/index.js"],
"env": {
"LLM_BASE_URL": "https://api.openai.com",
"LLM_JUDGE_MODEL": "gpt-4",
"LLM_API_KEY": "sk-your-openai-key"
}
}
}
}
```
**With Custom Endpoint (Gemini via Veronica):**
```json
{
"mcpServers": {
"dfa-workflow": {
"command": "node",
"args": ["/path/to/dfa-mcp-server/dist/index.js"],
"env": {
"LLM_BASE_URL": "https://your-llm-api-endpoint.com",
"LLM_JUDGE_MODEL": "gemini-2.5-pro",
"LLM_API_KEY": "sk-your-api-key"
}
}
}
}
```

1629
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "dfa-mcp-server",
"version": "1.0.0",
"description": "DFA-based workflow MCP server for guiding LLM task completion",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"start": "node dist/index.js",
"test": "echo \"No tests yet\""
},
"keywords": ["mcp", "workflow", "state-machine", "dfa"],
"author": "",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"zod": "^3.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0"
}
}

306
src/error-formatter.ts Normal file
View file

@ -0,0 +1,306 @@
import {
WorkflowDefinition,
WorkflowContext,
TransitionAttempt,
JudgeDecision
} from './types.js';
export class ErrorFormatter {
/**
* Format a rich error message for invalid transitions
*/
static formatInvalidActionError(
currentState: string,
attemptedAction: string,
validActions: string[],
workflowName: string
): string {
const hasValidActions = validActions.length > 0;
let message = `\n❌ Invalid Action Error\n`;
message += `${'─'.repeat(50)}\n`;
message += `📍 Current State: '${currentState}'\n`;
message += `🚫 Attempted Action: '${attemptedAction}'\n`;
message += `📋 Workflow: '${workflowName}'\n\n`;
if (hasValidActions) {
message += `✅ Available Actions from '${currentState}':\n`;
validActions.forEach(action => {
message += `${action}\n`;
});
// Add "did you mean?" suggestion if there's a close match
const suggestion = this.findSimilarAction(attemptedAction, validActions);
if (suggestion) {
message += `\n💡 Did you mean '${suggestion}'?\n`;
}
} else {
message += `⚠️ No actions available from state '${currentState}'\n`;
message += ` This might be a final state or a dead-end.\n`;
}
message += `\n📝 Example Usage:\n`;
if (hasValidActions) {
message += ` workflow.advance({ id: "...", action: "${validActions[0]}", data: { ... } })\n`;
}
return message;
}
/**
* Format a rich error message for missing required fields
*/
static formatMissingFieldsError(
missingFields: string[],
currentContext: WorkflowContext,
targetState: string,
action: string
): string {
let message = `\n❌ Missing Required Fields\n`;
message += `${'─'.repeat(50)}\n`;
message += `📍 Target State: '${targetState}'\n`;
message += `🔄 Action: '${action}'\n\n`;
message += `🚫 Missing Fields:\n`;
missingFields.forEach(field => {
message += `${field}\n`;
});
message += `\n📊 Current Context:\n`;
const contextKeys = Object.keys(currentContext);
if (contextKeys.length > 0) {
contextKeys.slice(0, 5).forEach(key => {
const value = JSON.stringify(currentContext[key]);
message += `${key}: ${value.length > 50 ? value.substring(0, 50) + '...' : value}\n`;
});
if (contextKeys.length > 5) {
message += ` ... and ${contextKeys.length - 5} more fields\n`;
}
} else {
message += ` (empty)\n`;
}
message += `\n💡 Solution:\n`;
message += `Include the missing fields in your transition data:\n\n`;
message += `workflow.advance({\n`;
message += ` id: "...",\n`;
message += ` action: "${action}",\n`;
message += ` data: {\n`;
missingFields.forEach(field => {
message += ` ${field}: ${this.getFieldExample(field)},\n`;
});
message += ` // ... other fields\n`;
message += ` }\n`;
message += `})\n`;
return message;
}
/**
* Format judge decision for better readability
*/
static formatJudgeDecision(decision: JudgeDecision, attempt: TransitionAttempt): string {
const approved = decision.approved;
const emoji = approved ? '✅' : '❌';
const status = approved ? 'APPROVED' : 'REJECTED';
let message = `\n${emoji} Judge Decision: ${status}\n`;
message += `${'─'.repeat(50)}\n`;
message += `📊 Confidence: ${(decision.confidence * 100).toFixed(1)}%\n`;
message += `🔄 Transition: ${attempt.fromState} → [${attempt.action}] → ${attempt.toState}\n`;
message += `\n📝 Reasoning:\n${this.wrapText(decision.reasoning, 60, ' ')}\n`;
if (decision.violations && decision.violations.length > 0) {
message += `\n🚫 Violations:\n`;
decision.violations.forEach((violation, i) => {
message += ` ${i + 1}. ${violation}\n`;
});
}
if (decision.suggestions && decision.suggestions.length > 0) {
message += `\n💡 Suggestions:\n`;
decision.suggestions.forEach((suggestion, i) => {
message += ` ${i + 1}. ${suggestion}\n`;
});
}
if (!approved && attempt.definition.states[attempt.fromState]) {
const validActions = Object.keys(attempt.definition.states[attempt.fromState].transitions || {});
if (validActions.length > 0) {
message += `\n✅ Valid Actions from '${attempt.fromState}':\n`;
validActions.forEach(action => {
message += `${action}\n`;
});
}
}
return message;
}
/**
* Format validation error with context comparison
*/
static formatValidationError(
violation: string,
currentValue: any,
expectedValue: any,
fieldPath?: string
): string {
let message = `\n⚠ Validation Error${fieldPath ? ` at '${fieldPath}'` : ''}\n`;
message += `${'─'.repeat(50)}\n`;
message += `❌ Issue: ${violation}\n\n`;
if (currentValue !== undefined || expectedValue !== undefined) {
message += `📊 Comparison:\n`;
message += ` Current: ${this.formatValue(currentValue)}\n`;
message += ` Expected: ${this.formatValue(expectedValue)}\n`;
}
return message;
}
/**
* Format state transition path for clarity
*/
static formatTransitionPath(
fromState: string,
action: string,
toState: string,
isValid: boolean
): string {
const arrow = isValid ? '→' : '';
const color = isValid ? '✅' : '❌';
return `${color} ${fromState} ${arrow} [${action}] ${arrow} ${toState}`;
}
/**
* Helper: Find similar action name (for "did you mean?" suggestions)
*/
private static findSimilarAction(attempted: string, valid: string[]): string | null {
const normalizedAttempt = attempted.toLowerCase();
// Exact match (case-insensitive)
const exactMatch = valid.find(v => v.toLowerCase() === normalizedAttempt);
if (exactMatch) return exactMatch;
// Prefix match
const prefixMatch = valid.find(v => v.toLowerCase().startsWith(normalizedAttempt));
if (prefixMatch) return prefixMatch;
// Contains match
const containsMatch = valid.find(v => v.toLowerCase().includes(normalizedAttempt));
if (containsMatch) return containsMatch;
// Simple Levenshtein distance (for small strings)
if (attempted.length <= 10) {
const distances = valid.map(v => ({
action: v,
distance: this.levenshteinDistance(normalizedAttempt, v.toLowerCase())
}));
const closest = distances.sort((a, b) => a.distance - b.distance)[0];
if (closest && closest.distance <= 2) {
return closest.action;
}
}
return null;
}
/**
* Helper: Simple Levenshtein distance for similarity matching
*/
private static levenshteinDistance(a: string, b: string): number {
const matrix: number[][] = [];
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j] + 1
);
}
}
}
return matrix[b.length][a.length];
}
/**
* Helper: Get example value for a field name
*/
private static getFieldExample(fieldName: string): string {
const lowerField = fieldName.toLowerCase();
// Common field patterns
if (lowerField.includes('email')) return '"user@example.com"';
if (lowerField.includes('name')) return '"John Doe"';
if (lowerField.includes('id')) return '"123"';
if (lowerField.includes('date') || lowerField.includes('time')) return 'new Date().toISOString()';
if (lowerField.includes('url')) return '"https://example.com"';
if (lowerField.includes('phone')) return '"+1234567890"';
if (lowerField.includes('amount') || lowerField.includes('price')) return '100.00';
if (lowerField.includes('count') || lowerField.includes('quantity')) return '1';
if (lowerField.includes('comment') || lowerField.includes('description')) return '"Your text here"';
if (lowerField.includes('status')) return '"pending"';
if (lowerField.includes('type')) return '"default"';
if (lowerField.includes('flag') || lowerField.includes('enabled')) return 'true';
// Default
return '"<value>"';
}
/**
* Helper: Format a value for display
*/
private static formatValue(value: any): string {
if (value === undefined) return 'undefined';
if (value === null) return 'null';
if (typeof value === 'string') return `"${value}"`;
if (typeof value === 'object') {
const str = JSON.stringify(value);
return str.length > 50 ? str.substring(0, 50) + '...' : str;
}
return String(value);
}
/**
* Helper: Wrap text to specified width
*/
private static wrapText(text: string, width: number, indent: string = ''): string {
const words = text.split(' ');
const lines: string[] = [];
let currentLine = '';
words.forEach(word => {
if ((currentLine + ' ' + word).length > width) {
if (currentLine) {
lines.push(indent + currentLine);
currentLine = word;
} else {
lines.push(indent + word);
}
} else {
currentLine = currentLine ? currentLine + ' ' + word : word;
}
});
if (currentLine) {
lines.push(indent + currentLine);
}
return lines.join('\n');
}
}

969
src/index.ts Executable file
View file

@ -0,0 +1,969 @@
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { WorkflowEngine } from "./workflow-engine.js";
// Initialize workflow engine
const engine = new WorkflowEngine();
// Create MCP server
const server = new McpServer({
name: "dfa-workflow-server",
version: "0.2.0"
});
// Tool: Define a new workflow
server.registerTool(
"workflow.define",
{
title: "Define Workflow",
description: `Define a new workflow type with states and transitions. A workflow is a state machine that controls process flow.
IMPORTANT: Transitions now use conditional rules evaluated by LLM for intelligent routing.
Example with conditional transitions:
{
"name": "smart-review",
"description": "Intelligent document review with conditional routing",
"states": {
"submitted": {
"transitions": {
"analyze": [
{
"condition": "context.documentType === 'legal' && context.requiresCompliance === true",
"target": "legal_review",
"description": "Legal documents need specialized review"
},
{
"condition": "context.wordCount > 10000 || context.complexity === 'high'",
"target": "senior_review",
"description": "Long or complex documents need senior review"
},
{
"condition": "context.priority === 'urgent' && context.riskLevel < 5",
"target": "fast_track",
"description": "Urgent but low-risk items can be fast-tracked"
},
{
"condition": "true",
"target": "standard_review",
"description": "Default path for all other cases"
}
]
}
},
"standard_review": {
"transitions": {
"decide": [
{ "condition": "context.reviewScore >= 8", "target": "approved" },
{ "condition": "context.reviewScore >= 5", "target": "needs_revision" },
{ "condition": "true", "target": "rejected" }
]
}
},
"approved": { "final": true },
"rejected": { "final": true }
},
"initialState": "submitted"
}
Simple workflow (always true conditions):
{
"name": "basic-approval",
"states": {
"draft": {
"transitions": {
"submit": [{ "condition": "true", "target": "review" }]
}
},
"review": {
"transitions": {
"approve": [{ "condition": "true", "target": "approved" }],
"reject": [{ "condition": "true", "target": "rejected" }]
}
},
"approved": { "final": true },
"rejected": { "final": true }
},
"initialState": "draft"
}
Conditions are code-like expressions evaluated by LLM with context understanding:
- Simple: "true" (always matches)
- Comparison: "context.amount > 1000"
- String: "context.status === 'active'"
- Complex: "context.score > 7 && (context.category === 'A' || context.override === true)"
- Nested: "context.user.role === 'admin' && context.user.permissions.includes('approve')"
The LLM can intelligently infer values not explicitly in context and reason about conditions.`,
inputSchema: {
name: z.string().describe("Unique workflow name (alphanumeric with hyphens/underscores)"),
description: z.string().optional().describe("Human-readable description of the workflow's purpose"),
states: z.record(z.object({
transitions: z.record(z.array(z.object({
condition: z.string(),
target: z.string(),
description: z.string().optional()
}))).optional(),
final: z.boolean().optional()
})).describe("Object where keys are state names and values define transitions or final status"),
initialState: z.string().describe("The state name where new workflow instances will start")
}
},
async ({ name, description, states, initialState }) => {
try {
const definition = {
name,
description,
states,
initialState
};
await engine.registerWorkflow(definition);
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
message: `Workflow '${name}' registered successfully`,
definition
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
);
// Tool: Start a new workflow
server.registerTool(
"workflow.start",
{
title: "Start Workflow",
description: `Start a new workflow instance. Creates a unique instance of a previously defined workflow type.
Example request:
{
"type": "document-review",
"context": {
"documentId": "DOC-123",
"author": "john.doe@example.com",
"createdAt": "2024-01-10T10:00:00Z"
}
}
Returns:
{
"id": "wf-550e8400-e29b-41d4-a716-446655440000",
"state": "draft",
"nextActions": ["submit"],
"progress": "Current state: draft"
}
The workflow ID returned should be used for all subsequent operations.
Context data is optional but can store any information needed throughout the workflow lifecycle.`,
inputSchema: {
type: z.string().describe("The name of a previously defined workflow type"),
context: z.any().optional().describe("Initial data/context for the workflow instance (any JSON object)")
}
},
async ({ type, context = {} }) => {
try {
const instance = await engine.createWorkflow(type, context);
const status = await engine.getStatus(instance.id);
return {
content: [{
type: "text",
text: JSON.stringify({
id: instance.id,
state: status.state,
nextActions: status.nextActions,
progress: status.progress
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
);
// Tool: Advance workflow state
server.registerTool(
"workflow.advance",
{
title: "Advance Workflow",
description: `Move workflow to next state by performing an action. The target state is determined by evaluating conditional rules.
Example request:
{
"id": "wf-550e8400-e29b-41d4-a716-446655440000",
"action": "route",
"data": {
"amount": 5000,
"priority": "high",
"department": "finance"
},
"expectedTargetState": "manager_approval" // Optional: your expectation
}
Success response (conditions evaluated, routed to manager_approval):
{
"state": "manager_approval",
"nextActions": ["approve", "reject", "escalate"],
"progress": "Requires manager approval (amount > 1000)",
"complete": false
}
Response with target mismatch warning:
{
"state": "ceo_approval", // Actual state based on conditions
"warning": "Based on condition \\"context.amount > 10000\\", workflow engine has determined 'ceo_approval' as target state and changed the state to 'ceo_approval' instead of 'manager_approval'.",
"conditionMatched": "context.amount > 10000",
"conditionDescription": "Large purchases need CEO approval",
"nextActions": ["approve", "reject"],
"complete": false
}
How it works:
1. The action must exist in current state's transitions
2. All conditions for that action are evaluated using LLM
3. The first matching condition determines the target state
4. If expectedTargetState differs, a warning is included but transition proceeds
5. The transition is then validated by the judge
Example with conditional routing:
Current state has: "route": [
{ "condition": "context.amount > 10000", "target": "ceo_approval" },
{ "condition": "context.amount > 1000", "target": "manager_approval" },
{ "condition": "true", "target": "auto_approved" }
]
With context.amount = 5000, it matches the second condition and routes to "manager_approval".
The 'data' parameter updates the workflow context and may be validated by the judge.
The 'expectedTargetState' parameter is optional and helps verify your understanding of the routing logic.`,
inputSchema: {
id: z.string().describe("The workflow instance ID from workflow.start"),
action: z.string().describe("The action to perform (must be valid for current state)"),
data: z.any().optional().describe("Data to include with the transition (updates workflow context)"),
expectedTargetState: z.string().optional().describe("Optional: Expected target state for verification")
}
},
async ({ id, action, data, expectedTargetState }) => {
try {
const result = await engine.transition(id, action, data, expectedTargetState);
return {
content: [{
type: "text",
text: JSON.stringify({
state: result.state,
nextActions: result.nextActions,
progress: result.progress,
complete: result.complete
}, null, 2)
}]
};
} catch (error) {
// Check if this is a judge rejection
if (error instanceof Error && (error as any).judgeDecision) {
const decision = (error as any).judgeDecision;
return {
content: [{
type: "text",
text: JSON.stringify({
error: "Transition rejected by judge",
decision: decision
}, null, 2)
}],
isError: true
};
}
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
);
// Tool: Get workflow status
server.registerTool(
"workflow.status",
{
title: "Get Workflow Status",
description: `Get current status of a workflow instance. Shows current state, available actions, and context.
Example request:
{
"id": "wf-550e8400-e29b-41d4-a716-446655440000"
}
Response:
{
"state": "reviewing",
"context": {
"documentId": "DOC-123",
"author": "john.doe@example.com",
"reviewerEmail": "manager@example.com",
"priority": "high",
"lastAction": "submit"
},
"nextActions": ["approve", "reject", "request_info"],
"progress": "Current state: reviewing",
"complete": false
}
Note: If context is very large (>100KB), it will be truncated with a warning.
Use this to check workflow state before deciding which action to take next.`,
inputSchema: {
id: z.string().describe("The workflow instance ID to check status for")
}
},
async ({ id }) => {
try {
const status = await engine.getStatus(id);
return {
content: [{
type: "text",
text: JSON.stringify({
state: status.state,
context: status.context,
nextActions: status.nextActions,
progress: status.progress,
complete: status.complete
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
);
// Tool: Create checkpoint
server.registerTool(
"workflow.checkpoint",
{
title: "Create Checkpoint",
description: `Create a checkpoint (snapshot) of workflow state. Allows rollback to this point later.
Example request:
{
"id": "wf-550e8400-e29b-41d4-a716-446655440000",
"description": "Before approval process",
"metadata": {
"reason": "Saving state before risky operation",
"createdBy": "system"
}
}
Response:
{
"id": "cp-660e8400-e29b-41d4-a716-446655440001",
"workflowId": "wf-550e8400-e29b-41d4-a716-446655440000",
"state": "reviewing",
"timestamp": "2024-01-10T11:00:00Z",
"description": "Before approval process",
"metadata": {
"reason": "Saving state before risky operation",
"createdBy": "system"
}
}
Checkpoints capture:
- Current workflow state
- Complete context data
- Timestamp and metadata
Use cases:
- Save state before complex operations
- Create restore points for testing
- Implement "undo" functionality`,
inputSchema: {
id: z.string().describe("The workflow instance ID"),
description: z.string().optional().describe("Human-readable description of why checkpoint was created"),
metadata: z.any().optional().describe("Any additional data to store with checkpoint")
}
},
async ({ id, description, metadata }) => {
try {
const checkpoint = await engine.createCheckpoint(id, description, metadata);
return {
content: [{
type: "text",
text: JSON.stringify({
checkpointId: checkpoint.id,
workflowId: checkpoint.workflowId,
state: checkpoint.state,
timestamp: checkpoint.timestamp,
description: checkpoint.description
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
);
// Tool: Rollback to checkpoint
server.registerTool(
"workflow.rollback",
{
title: "Rollback to Checkpoint",
description: `Rollback workflow to a previous checkpoint. Restores state and context from the checkpoint.
Example request:
{
"id": "wf-550e8400-e29b-41d4-a716-446655440000",
"checkpointId": "cp-660e8400-e29b-41d4-a716-446655440001"
}
Response:
{
"state": "reviewing",
"context": {
"documentId": "DOC-123",
"author": "john.doe@example.com",
"reviewerEmail": "manager@example.com",
"lastAction": "submit"
},
"nextActions": ["approve", "reject", "request_info"],
"progress": "Current state: reviewing",
"complete": false
}
Effects of rollback:
- Workflow state is restored to checkpoint state
- Context is completely replaced with checkpoint context
- Any changes made after checkpoint are lost
- Transition history shows ROLLBACK action
Use workflow.listCheckpoints first to find available checkpoints.`,
inputSchema: {
id: z.string().describe("The workflow instance ID"),
checkpointId: z.string().describe("The checkpoint ID to restore (from workflow.listCheckpoints)")
}
},
async ({ id, checkpointId }) => {
try {
const result = await engine.rollbackToCheckpoint(id, checkpointId);
return {
content: [{
type: "text",
text: JSON.stringify({
state: result.state,
context: result.context,
nextActions: result.nextActions,
progress: result.progress,
message: "Successfully rolled back to checkpoint"
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
);
// Tool: List workflows
server.registerTool(
"workflow.list",
{
title: "List Workflows",
description: `List all registered workflow types. Shows available workflow definitions that can be instantiated.
Example request: {} (no parameters needed)
Response:
{
"workflows": [
{
"name": "document-review",
"description": "Document review and approval process"
},
{
"name": "user-onboarding",
"description": "New user onboarding workflow"
}
],
"count": 2
}
Use this to discover available workflow types before calling workflow.start.`,
inputSchema: {}
},
async () => {
try {
const workflows = engine.listWorkflows();
return {
content: [{
type: "text",
text: JSON.stringify({
workflows,
count: workflows.length
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
);
// Tool: Validate transition without executing
server.registerTool(
"workflow.judge.validate",
{
title: "Validate Transition",
description: `Validate a transition without executing it. Uses the judge to check if an action would be allowed.
Example request:
{
"id": "wf-550e8400-e29b-41d4-a716-446655440000",
"action": "approve",
"data": {
"comments": "Looks good",
"approvalLevel": "manager"
}
}
Response (validation passes):
{
"approved": true,
"confidence": 0.95,
"reasoning": "✅ draft → [submit] → reviewing validated successfully with 95% confidence"
}
Response (validation fails):
{
"approved": false,
"confidence": 0.2,
"reasoning": "❌ Invalid Action Error\\n──────────────\\n📍 Current State: 'draft'\\n🚫 Attempted Action: 'approve'\\n...",
"violations": ["Action 'approve' not available in current state"],
"suggestions": [
"Valid actions for state 'draft':\\n • submit",
"Example: workflow.advance({ id: \\"...\\", action: \\"submit\\", data: { ... } })"
]
}
This is useful for checking if an action is valid before attempting it, or for providing UI hints about available actions.`,
inputSchema: {
id: z.string().describe("The workflow instance ID"),
action: z.string().describe("The action to validate"),
data: z.any().optional().describe("Data that would be sent with the transition"),
expectedTargetState: z.string().optional().describe("Optional: Expected target state for verification")
}
},
async ({ id, action, data, expectedTargetState }) => {
try {
const decision = await engine.validateTransition(id, action, data, expectedTargetState);
return {
content: [{
type: "text",
text: JSON.stringify(decision, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
);
// Tool: Get judge history
server.registerTool(
"workflow.judge.history",
{
title: "Judge History",
description: `Get judge decision history for a workflow. Shows all validation decisions made by the judge.
Example request:
{
"id": "wf-550e8400-e29b-41d4-a716-446655440000",
"limit": 10,
"offset": 0
}
Response:
{
"workflowId": "wf-550e8400-e29b-41d4-a716-446655440000",
"decisions": [
{
"approved": true,
"confidence": 0.95,
"reasoning": "Transition validated successfully",
"metadata": { "timestamp": "2024-01-10T10:30:00Z" }
},
{
"approved": false,
"confidence": 0.3,
"reasoning": "Missing required fields",
"violations": ["Missing required fields: reviewerEmail"],
"suggestions": ["Include reviewerEmail in transition data"],
"metadata": { "timestamp": "2024-01-10T10:25:00Z" }
}
],
"count": 2,
"total": 15,
"hasMore": true,
"pagination": { "limit": 10, "offset": 0 }
}
History includes both successful and failed validation attempts.
Use pagination (limit/offset) to browse through large histories.`,
inputSchema: {
id: z.string().describe("The workflow instance ID"),
limit: z.number().optional().describe("Max number of decisions to return (default: 20, max: 100)"),
offset: z.number().optional().describe("Number of decisions to skip for pagination (default: 0)")
}
},
async ({ id, limit, offset }) => {
try {
const history = await engine.getJudgeHistory(id, limit, offset);
return {
content: [{
type: "text",
text: JSON.stringify({
workflowId: id,
decisions: history.decisions,
count: history.decisions.length,
total: history.total,
hasMore: history.hasMore,
pagination: {
limit: limit || 20,
offset: offset || 0
}
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
);
// Tool: Update judge config
server.registerTool(
"workflow.judge.config",
{
title: "Configure Judge",
description: `Update judge configuration for a workflow. Controls how strictly transitions are validated.
Example request:
{
"name": "document-review",
"enabled": true,
"strictMode": true,
"minConfidence": 0.8,
"useLLM": true
}
Configuration options:
- enabled: Turn judge on/off (false = all transitions auto-approved)
- strictMode: When true, rejects transitions below minConfidence threshold
- minConfidence: 0.0-1.0, threshold for approval in strict mode (0.8 = 80%)
- useLLM: Use AI model for intelligent validation vs rule-based only
Example configurations:
Strict validation (production):
{ "enabled": true, "strictMode": true, "minConfidence": 0.9, "useLLM": true }
Relaxed validation (development):
{ "enabled": true, "strictMode": false, "minConfidence": 0.5, "useLLM": false }
Bypass validation (testing):
{ "enabled": false }
Note: LLM validation requires LLM_BASE_URL and LLM_API_KEY environment variables.`,
inputSchema: {
name: z.string().describe("The workflow type name to configure"),
enabled: z.boolean().describe("Enable (true) or disable (false) the judge entirely"),
strictMode: z.boolean().optional().describe("In strict mode, transitions below minConfidence are rejected"),
minConfidence: z.number().min(0).max(1).optional().describe("Confidence threshold for strict mode (0.0-1.0)"),
useLLM: z.boolean().optional().describe("Use LLM for intelligent validation (requires API credentials)")
}
},
async ({ name, enabled, strictMode, minConfidence, useLLM }) => {
try {
const judgeConfig = {
enabled,
strictMode,
minConfidence,
useLLM
};
await engine.updateJudgeConfig(name, judgeConfig);
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
message: `Judge configuration updated for workflow '${name}'`,
config: judgeConfig
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
);
// Tool: List checkpoints
server.registerTool(
"workflow.listCheckpoints",
{
title: "List Checkpoints",
description: `List all checkpoints for a workflow. Shows available restore points sorted by newest first.
Example request:
{
"id": "wf-550e8400-e29b-41d4-a716-446655440000"
}
Response:
{
"workflowId": "wf-550e8400-e29b-41d4-a716-446655440000",
"checkpoints": [
{
"id": "cp-660e8400-e29b-41d4-a716-446655440002",
"state": "approved",
"timestamp": "2024-01-10T12:00:00Z",
"description": "After final approval",
"hasMetadata": true
},
{
"id": "cp-660e8400-e29b-41d4-a716-446655440001",
"state": "reviewing",
"timestamp": "2024-01-10T11:00:00Z",
"description": "Before approval process",
"hasMetadata": true
}
],
"count": 2
}
Checkpoints are sorted newest first. Use the checkpoint ID with workflow.rollback to restore.`,
inputSchema: {
id: z.string().describe("The workflow instance ID")
}
},
async ({ id }) => {
try {
const checkpoints = await engine.listCheckpoints(id);
return {
content: [{
type: "text",
text: JSON.stringify({
workflowId: id,
checkpoints: checkpoints.map(cp => ({
id: cp.id,
state: cp.state,
timestamp: cp.timestamp,
description: cp.description,
hasMetadata: !!cp.metadata
})),
count: checkpoints.length
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
);
// Register a prompt for workflow help
server.registerPrompt(
"workflow-guide",
{
title: "Workflow Guide",
description: "Get guidance on using the workflow system",
argsSchema: {}
},
() => ({
messages: [{
role: "assistant",
content: {
type: "text",
text: `# Generic DFA Workflow System Guide
## Available Tools:
1. **workflow.define** - Define a new workflow type
- name: Unique workflow name
- description: Optional description
- states: Object with state definitions
- initialState: The starting state
2. **workflow.list** - List all registered workflows
3. **workflow.start** - Start a new workflow instance
- type: The workflow type name
- context: Optional initial context data
4. **workflow.advance** - Move to the next state
- id: The workflow ID
- action: The action to perform (check nextActions from status)
- data: Optional data for the action
5. **workflow.status** - Check workflow status
- id: The workflow ID
6. **workflow.checkpoint** - Create a checkpoint
- id: The workflow ID
- description: Optional description
- metadata: Optional metadata to store
7. **workflow.rollback** - Rollback to a checkpoint
- id: The workflow ID
- checkpointId: The checkpoint to rollback to
8. **workflow.listCheckpoints** - List all checkpoints
- id: The workflow ID
## Defining a Workflow:
States must include:
- transitions: Object mapping actions to next states
- final: Boolean indicating terminal states
Example workflow definition:
{
"name": "approval-process",
"description": "Generic approval workflow",
"states": {
"draft": {
"transitions": { "submit": "pending" }
},
"pending": {
"transitions": {
"approve": "approved",
"reject": "rejected",
"request_changes": "draft"
}
},
"approved": { "final": true },
"rejected": { "final": true }
},
"initialState": "draft"
}
## Example Usage:
1. Define: workflow.define({ name: "my-workflow", states: {...}, initialState: "start" })
2. List: workflow.list()
3. Start: workflow.start({ type: "my-workflow", context: { data: "value" } })
4. Advance: workflow.advance({ id: "wf-123", action: "submit" })
5. Checkpoint: workflow.checkpoint({ id: "wf-123", description: "Before review" })
6. Status: workflow.status({ id: "wf-123" })
7. Rollback: workflow.rollback({ id: "wf-123", checkpointId: "cp-xxx" })
## Key Features:
- Define any workflow with custom states and transitions
- Context is completely generic - store any data
- Checkpoints save complete state for recovery
- Prevents LLMs from losing context in multi-step processes`
}
}]
})
);
// Main server initialization
async function main() {
// Initialize the workflow engine
await engine.initialize();
// Connect to stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("DFA Workflow MCP Server started");
}
// Run the server
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});

1034
src/judge-engine.ts Normal file

File diff suppressed because it is too large Load diff

134
src/types.ts Normal file
View file

@ -0,0 +1,134 @@
// Core types for the DFA workflow system
export interface WorkflowDefinition {
name: string;
description?: string;
states: Record<string, StateDefinition>;
initialState: string;
// Optional custom behavior functions
contextUpdater?: (context: WorkflowContext, action: string, data?: any) => WorkflowContext;
progressCalculator?: (instance: WorkflowInstance, definition: WorkflowDefinition) => string;
// Judge configuration
judgeConfig?: JudgeConfig;
stateValidators?: {
[stateName: string]: {
entryConditions?: (context: WorkflowContext) => ValidationResult;
exitConditions?: (context: WorkflowContext) => ValidationResult;
requiredFields?: string[];
};
};
transitionValidators?: {
[action: string]: (data: any, context: WorkflowContext) => ValidationResult;
};
}
export interface TransitionRule {
condition: string; // Code-like expression: "context.amount > 1000" or "true"
target: string; // Target state if condition is met
description?: string; // Optional: Help LLM understand the intent
}
export interface StateDefinition {
transitions?: Record<string, TransitionRule[]>; // action -> array of conditional rules
final?: boolean;
}
export interface WorkflowInstance {
id: string;
definitionName: string;
currentState: string;
context: WorkflowContext;
createdAt: Date;
updatedAt: Date;
}
export interface WorkflowContext {
[key: string]: any;
// Context is completely generic - workflows define their own structure
}
export interface TransitionResult {
state: string;
context: WorkflowContext;
nextActions: string[];
progress?: string;
complete?: boolean;
}
export interface WorkflowCheckpoint {
id: string;
workflowId: string;
timestamp: Date;
state: string;
context: WorkflowContext;
description?: string;
metadata?: {
createdBy?: string;
reason?: string;
[key: string]: any;
};
}
export interface TransitionLog {
timestamp: Date;
fromState: string;
action: string;
toState: string;
data?: any;
judgeDecision?: JudgeDecision;
}
// Judge-related types
export interface JudgeConfig {
enabled: boolean;
strictMode?: boolean; // Reject low confidence transitions
minConfidence?: number; // Minimum confidence threshold (0-1)
useLLM?: boolean; // Use LLM for intelligent validation
validationRules?: ValidationRule[];
customValidator?: (transition: TransitionAttempt) => JudgeDecision;
}
export interface ValidationRule {
name: string;
description: string;
validate: (transition: TransitionAttempt) => ValidationResult;
}
export interface TransitionAttempt {
workflowId: string;
fromState: string;
action: string;
toState: string;
data?: any;
context: WorkflowContext;
definition: WorkflowDefinition;
}
export interface JudgeDecision {
approved: boolean;
confidence: number; // 0-1
reasoning: string;
violations?: string[];
suggestions?: string[];
metadata?: any;
}
export interface ValidationResult {
valid: boolean;
confidence: number;
reason?: string;
}
export interface ConditionEvaluation {
condition: string;
result: boolean;
confidence: number;
reasoning: string;
extractedValues?: Record<string, any>; // Values the LLM extracted/inferred from context
}
export interface ConditionEvaluationResult {
matchedRule: TransitionRule | null;
evaluations: ConditionEvaluation[];
overallReasoning: string;
}

683
src/workflow-engine.ts Normal file
View file

@ -0,0 +1,683 @@
import {
WorkflowDefinition,
WorkflowInstance,
TransitionResult,
WorkflowContext,
TransitionLog,
WorkflowCheckpoint,
TransitionAttempt,
JudgeDecision,
TransitionRule,
ConditionEvaluationResult
} from './types.js';
import { JudgeEngine } from './judge-engine.js';
import { ErrorFormatter } from './error-formatter.js';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as crypto from 'crypto';
export class WorkflowEngine {
private definitions: Map<string, WorkflowDefinition> = new Map();
private instances: Map<string, WorkflowInstance> = new Map();
private checkpoints: Map<string, WorkflowCheckpoint[]> = new Map();
private workflowDir: string;
private checkpointDir: string;
private judgeEngine: JudgeEngine;
constructor(workflowDir: string = '.workflows') {
this.workflowDir = workflowDir;
this.checkpointDir = path.join(workflowDir, 'checkpoints');
this.judgeEngine = new JudgeEngine(workflowDir);
}
async initialize() {
// Ensure workflow directory exists
await fs.mkdir(this.workflowDir, { recursive: true });
await fs.mkdir(this.checkpointDir, { recursive: true });
// Initialize judge engine
await this.judgeEngine.initialize();
// Load saved workflow definitions
await this.loadDefinitions();
// Load existing workflow instances
await this.loadInstances();
// Load existing checkpoints
await this.loadCheckpoints();
}
async registerWorkflow(definition: WorkflowDefinition): Promise<void> {
// Validate workflow definition
if (!definition.name || !definition.states || !definition.initialState) {
throw new Error('Invalid workflow definition: missing required fields');
}
// Validate initial state exists
if (!definition.states[definition.initialState]) {
throw new Error(`Invalid initial state: ${definition.initialState}`);
}
// Validate all transitions point to valid states
for (const [stateName, state] of Object.entries(definition.states)) {
if (state.transitions) {
for (const [action, rules] of Object.entries(state.transitions)) {
for (const rule of rules) {
if (!definition.states[rule.target]) {
throw new Error(`Invalid transition: ${stateName} -> ${action} -> ${rule.target} (state not found)`);
}
}
}
}
}
// Register the workflow
this.definitions.set(definition.name, definition);
// Persist the definition
await this.saveDefinition(definition);
}
listWorkflows(): Array<{ name: string; description?: string }> {
return Array.from(this.definitions.values()).map(def => ({
name: def.name,
description: def.description
}));
}
async createWorkflow(type: string, initialContext: WorkflowContext = {}): Promise<WorkflowInstance> {
const definition = this.definitions.get(type);
if (!definition) {
throw new Error(`Unknown workflow type: ${type}`);
}
// Use crypto.randomUUID if available, otherwise fallback to timestamp-based ID
const id = crypto.randomUUID ? `wf-${crypto.randomUUID()}` : `wf-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const instance: WorkflowInstance = {
id,
definitionName: type,
currentState: definition.initialState,
context: initialContext,
createdAt: new Date(),
updatedAt: new Date()
};
this.instances.set(id, instance);
await this.saveInstance(instance);
await this.logTransition(id, '', 'start', definition.initialState);
return instance;
}
async transition(workflowId: string, action: string, data?: any, expectedTargetState?: string): Promise<TransitionResult> {
const instance = this.instances.get(workflowId);
if (!instance) {
throw new Error(`Workflow not found: ${workflowId}`);
}
const definition = this.definitions.get(instance.definitionName);
if (!definition) {
throw new Error(`Definition not found: ${instance.definitionName}`);
}
const currentStatedef = definition.states[instance.currentState];
if (!currentStatedef) {
throw new Error(`Invalid state: ${instance.currentState}`);
}
if (currentStatedef.final) {
throw new Error(`Cannot transition from final state: ${instance.currentState}`);
}
const transitionRules = currentStatedef.transitions?.[action];
if (!transitionRules || transitionRules.length === 0) {
const validActions = Object.keys(currentStatedef.transitions || {});
const errorMessage = ErrorFormatter.formatInvalidActionError(
instance.currentState,
action,
validActions,
definition.name
);
throw new Error(errorMessage);
}
// Evaluate conditions to find the target state
const conditionResult = await this.judgeEngine.evaluateTransitionConditions(
transitionRules,
instance.context,
{
workflowId,
fromState: instance.currentState,
action,
toState: '', // Will be determined by condition evaluation
data,
context: instance.context,
definition
}
);
if (!conditionResult.matchedRule) {
throw new Error(`No conditions matched for action '${action}' from state '${instance.currentState}'\n${conditionResult.overallReasoning}`);
}
const nextState = conditionResult.matchedRule.target;
// Check if user provided an expected target state and it differs from the evaluated one
let targetMismatchWarning: string | undefined;
if (expectedTargetState && expectedTargetState !== nextState) {
targetMismatchWarning = `Based on condition "${conditionResult.matchedRule.condition}", workflow engine has determined '${nextState}' as target state and changed the state to '${nextState}' instead of '${expectedTargetState}'.`;
console.warn(`[Workflow ${workflowId}] Target state mismatch: ${targetMismatchWarning}`);
}
// Create transition attempt for judge validation
const attempt: TransitionAttempt = {
workflowId,
fromState: instance.currentState,
action,
toState: nextState,
data,
context: instance.context,
definition
};
// Validate transition with judge
const judgeDecision = await this.judgeEngine.validateTransition(attempt);
if (!judgeDecision.approved) {
const error = new Error(`Transition rejected by judge: ${judgeDecision.reasoning}`);
(error as any).judgeDecision = judgeDecision;
throw error;
}
// Update context based on action
const newContext = this.updateContext(instance.context, action, data, definition);
// Log the transition with judge decision
await this.logTransition(workflowId, instance.currentState, action, nextState, data, judgeDecision);
// Update instance
instance.currentState = nextState;
instance.context = newContext;
instance.updatedAt = new Date();
await this.saveInstance(instance);
// Return result with next actions
const nextStatedef = definition.states[nextState];
const nextActions = nextStatedef.transitions ? Object.keys(nextStatedef.transitions) : [];
// Check context size and potentially truncate
const { context: safeContext, warning } = this.getSafeContext(newContext);
const result: TransitionResult = {
state: nextState,
context: safeContext,
nextActions,
progress: this.calculateProgress(instance, definition),
complete: nextStatedef.final
};
// Add warning if context was truncated
if (warning) {
(result as any).contextWarning = warning;
}
// Add target mismatch warning if applicable
if (targetMismatchWarning) {
(result as any).warning = targetMismatchWarning;
(result as any).conditionMatched = conditionResult.matchedRule.condition;
(result as any).conditionDescription = conditionResult.matchedRule.description;
}
return result;
}
async getStatus(workflowId: string): Promise<TransitionResult> {
const instance = this.instances.get(workflowId);
if (!instance) {
throw new Error(`Workflow not found: ${workflowId}`);
}
const definition = this.definitions.get(instance.definitionName);
if (!definition) {
throw new Error(`Definition not found: ${instance.definitionName}`);
}
const statedef = definition.states[instance.currentState];
const nextActions = statedef.transitions ? Object.keys(statedef.transitions) : [];
// Check context size and potentially truncate
const { context: safeContext, warning } = this.getSafeContext(instance.context);
const result: TransitionResult = {
state: instance.currentState,
context: safeContext,
nextActions,
progress: this.calculateProgress(instance, definition),
complete: statedef.final
};
// Add warning if context was truncated
if (warning) {
(result as any).contextWarning = warning;
}
return result;
}
private updateContext(context: WorkflowContext, action: string, data?: any, definition?: WorkflowDefinition): WorkflowContext {
let newContext: WorkflowContext;
// If workflow has custom context updater, use it
if (definition?.contextUpdater) {
newContext = definition.contextUpdater(context, action, data);
} else {
// Otherwise, generic update: merge data into context
if (data && typeof data === 'object') {
newContext = { ...context, ...data, lastAction: action };
} else {
newContext = { ...context, lastAction: action };
}
}
// Check context size and warn if it's getting large
const contextSize = JSON.stringify(newContext).length;
if (contextSize > 100000) { // 100KB warning threshold
console.warn(`[Workflow ${definition?.name || 'unknown'}] Large context detected: ${(contextSize / 1024).toFixed(2)}KB`);
if (contextSize > 500000) { // 500KB error threshold
console.error(`[Workflow ${definition?.name || 'unknown'}] Context is very large (${(contextSize / 1024).toFixed(2)}KB). Consider reducing data size.`);
}
}
return newContext;
}
private calculateProgress(instance: WorkflowInstance, definition: WorkflowDefinition): string {
// If workflow has custom progress calculator, use it
if (definition.progressCalculator) {
return definition.progressCalculator(instance, definition);
}
// Otherwise, generic progress
if (definition.states[instance.currentState].final) {
return 'Workflow completed';
}
return `Current state: ${instance.currentState}`;
}
/**
* Check context size and return truncated version if too large
*/
private getSafeContext(context: WorkflowContext): { context: WorkflowContext; warning?: string } {
const contextStr = JSON.stringify(context);
const contextSize = contextStr.length;
if (contextSize > 100000) { // 100KB threshold
const warningMessage = `Context size (${(contextSize / 1024).toFixed(2)}KB) exceeds safe limit`;
// Return a truncated context with metadata
return {
context: {
_truncated: true,
_originalSize: contextSize,
_message: 'Context too large to return in full',
// Include some basic info if available
lastAction: context.lastAction,
_summary: 'Use workflow.status with full=false to get summary only'
},
warning: warningMessage
};
}
return { context };
}
private async saveInstance(instance: WorkflowInstance) {
const filePath = path.join(this.workflowDir, `${instance.id}.json`);
await fs.writeFile(filePath, JSON.stringify(instance, null, 2));
}
private async loadInstances() {
try {
const files = await fs.readdir(this.workflowDir);
for (const file of files) {
if (file.endsWith('.json') && !file.endsWith('.log')) {
const filePath = path.join(this.workflowDir, file);
const content = await fs.readFile(filePath, 'utf-8');
const instance = JSON.parse(content) as WorkflowInstance;
this.instances.set(instance.id, instance);
}
}
} catch (error) {
// Directory might not exist yet
if ((error as any).code !== 'ENOENT') {
throw error;
}
}
}
private async logTransition(
workflowId: string,
fromState: string,
action: string,
toState: string,
data?: any,
judgeDecision?: JudgeDecision
) {
const log: TransitionLog = {
timestamp: new Date(),
fromState,
action,
toState,
data,
judgeDecision
};
const logPath = path.join(this.workflowDir, `${workflowId}.log`);
const logLine = JSON.stringify(log) + '\n';
await fs.appendFile(logPath, logLine);
}
// Checkpoint functionality
async createCheckpoint(workflowId: string, description?: string, metadata?: any): Promise<WorkflowCheckpoint> {
const instance = this.instances.get(workflowId);
if (!instance) {
throw new Error(`Workflow not found: ${workflowId}`);
}
// Use crypto.randomUUID if available, otherwise fallback to timestamp-based ID
const checkpointId = crypto.randomUUID ? `cp-${crypto.randomUUID()}` : `cp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const checkpoint: WorkflowCheckpoint = {
id: checkpointId,
workflowId,
timestamp: new Date(),
state: instance.currentState,
context: { ...instance.context }, // Deep copy context
description,
metadata
};
// Store in memory
const workflowCheckpoints = this.checkpoints.get(workflowId) || [];
workflowCheckpoints.push(checkpoint);
this.checkpoints.set(workflowId, workflowCheckpoints);
// Persist to disk
await this.saveCheckpoint(checkpoint);
// Log the checkpoint creation
await this.logTransition(workflowId, instance.currentState, 'CHECKPOINT', instance.currentState, {
checkpointId,
description
});
return checkpoint;
}
async rollbackToCheckpoint(workflowId: string, checkpointId: string): Promise<TransitionResult> {
const instance = this.instances.get(workflowId);
if (!instance) {
throw new Error(`Workflow not found: ${workflowId}`);
}
const workflowCheckpoints = this.checkpoints.get(workflowId) || [];
const checkpoint = workflowCheckpoints.find(cp => cp.id === checkpointId);
if (!checkpoint) {
throw new Error(`Checkpoint not found: ${checkpointId}`);
}
// Store current state before rollback for logging
const previousState = instance.currentState;
const previousContext = { ...instance.context };
// Restore state and context
instance.currentState = checkpoint.state;
instance.context = { ...checkpoint.context }; // Deep copy
instance.updatedAt = new Date();
// Save the rolled back instance
await this.saveInstance(instance);
// Log the rollback
await this.logTransition(workflowId, previousState, 'ROLLBACK', checkpoint.state, {
checkpointId,
fromContext: previousContext,
toContext: checkpoint.context
});
// Get the state definition for next actions
const definition = this.definitions.get(instance.definitionName);
if (!definition) {
throw new Error(`Definition not found: ${instance.definitionName}`);
}
const statedef = definition.states[instance.currentState];
const nextActions = statedef.transitions ? Object.keys(statedef.transitions) : [];
// Check context size and potentially truncate
const { context: safeContext, warning } = this.getSafeContext(instance.context);
const result: TransitionResult = {
state: instance.currentState,
context: safeContext,
nextActions,
progress: this.calculateProgress(instance, definition),
complete: statedef.final
};
// Add warning if context was truncated
if (warning) {
(result as any).contextWarning = warning;
}
return result;
}
async listCheckpoints(workflowId: string): Promise<WorkflowCheckpoint[]> {
const checkpoints = this.checkpoints.get(workflowId) || [];
return checkpoints.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
}
private async saveCheckpoint(checkpoint: WorkflowCheckpoint) {
const filePath = path.join(this.checkpointDir, `${checkpoint.workflowId}-${checkpoint.id}.json`);
await fs.writeFile(filePath, JSON.stringify(checkpoint, null, 2));
}
private async loadCheckpoints() {
try {
const files = await fs.readdir(this.checkpointDir);
for (const file of files) {
if (file.endsWith('.json')) {
const filePath = path.join(this.checkpointDir, file);
const content = await fs.readFile(filePath, 'utf-8');
const checkpoint = JSON.parse(content) as WorkflowCheckpoint;
// Convert string dates back to Date objects
checkpoint.timestamp = new Date(checkpoint.timestamp);
const workflowCheckpoints = this.checkpoints.get(checkpoint.workflowId) || [];
workflowCheckpoints.push(checkpoint);
this.checkpoints.set(checkpoint.workflowId, workflowCheckpoints);
}
}
} catch (error) {
// Directory might not exist yet
if ((error as any).code !== 'ENOENT') {
throw error;
}
}
}
private async saveDefinition(definition: WorkflowDefinition) {
const defsDir = path.join(this.workflowDir, 'definitions');
await fs.mkdir(defsDir, { recursive: true });
const filePath = path.join(defsDir, `${definition.name}.json`);
// Don't save functions - they can't be serialized
const { contextUpdater, progressCalculator, ...serializableDefinition } = definition;
await fs.writeFile(filePath, JSON.stringify(serializableDefinition, null, 2));
}
private async loadDefinitions() {
try {
const defsDir = path.join(this.workflowDir, 'definitions');
const files = await fs.readdir(defsDir);
for (const file of files) {
if (file.endsWith('.json')) {
const filePath = path.join(defsDir, file);
const content = await fs.readFile(filePath, 'utf-8');
const definition = JSON.parse(content) as WorkflowDefinition;
this.definitions.set(definition.name, definition);
}
}
} catch (error) {
// Directory might not exist yet
if ((error as any).code !== 'ENOENT') {
throw error;
}
}
}
// Judge-specific methods
/**
* Validate a transition without executing it
*/
async validateTransition(workflowId: string, action: string, data?: any, expectedTargetState?: string): Promise<JudgeDecision> {
const instance = this.instances.get(workflowId);
if (!instance) {
throw new Error(`Workflow not found: ${workflowId}`);
}
const definition = this.definitions.get(instance.definitionName);
if (!definition) {
throw new Error(`Definition not found: ${instance.definitionName}`);
}
const currentStatedef = definition.states[instance.currentState];
if (!currentStatedef) {
throw new Error(`Invalid state: ${instance.currentState}`);
}
const transitionRules = currentStatedef.transitions?.[action];
if (!transitionRules || transitionRules.length === 0) {
const validActions = Object.keys(currentStatedef.transitions || {});
const errorMessage = ErrorFormatter.formatInvalidActionError(
instance.currentState,
action,
validActions,
definition.name
);
return {
approved: false,
confidence: 0,
reasoning: errorMessage,
violations: [`Action '${action}' not available in current state`],
suggestions: validActions.length > 0
? [`Valid actions: ${validActions.join(', ')}`, `Example: workflow.advance({ id: "${workflowId}", action: "${validActions[0]}", data: { ... } })`]
: [`State '${instance.currentState}' has no available transitions`]
};
}
// Evaluate conditions to find the target state
const conditionResult = await this.judgeEngine.evaluateTransitionConditions(
transitionRules,
instance.context,
{
workflowId,
fromState: instance.currentState,
action,
toState: '', // Will be determined by condition evaluation
data,
context: instance.context,
definition
}
);
if (!conditionResult.matchedRule) {
return {
approved: false,
confidence: 0,
reasoning: `No conditions matched for action '${action}' from state '${instance.currentState}'`,
violations: [`None of the ${transitionRules.length} condition(s) evaluated to true`],
suggestions: [
`Conditions evaluated:`,
...conditionResult.evaluations.map((e, i) =>
`${i+1}. "${e.condition}" -> ${e.result ? 'TRUE' : 'FALSE'} (${(e.confidence * 100).toFixed(0)}% confidence)`
),
'',
conditionResult.overallReasoning
],
metadata: { conditionEvaluations: conditionResult.evaluations }
};
}
const nextState = conditionResult.matchedRule.target;
// Check if user provided an expected target state and it differs from the evaluated one
let targetMismatchInfo: any = undefined;
if (expectedTargetState && expectedTargetState !== nextState) {
targetMismatchInfo = {
warning: `Based on condition "${conditionResult.matchedRule.condition}", workflow engine has determined '${nextState}' as target state instead of '${expectedTargetState}'.`,
expectedState: expectedTargetState,
actualState: nextState,
conditionMatched: conditionResult.matchedRule.condition,
conditionDescription: conditionResult.matchedRule.description
};
}
const attempt: TransitionAttempt = {
workflowId,
fromState: instance.currentState,
action,
toState: nextState,
data,
context: instance.context,
definition
};
const decision = await this.judgeEngine.validateTransition(attempt);
// Record the decision to history even for validation-only calls
await this.judgeEngine.recordDecision(workflowId, decision);
// Add target mismatch info to decision metadata if applicable
if (targetMismatchInfo && decision.approved) {
decision.metadata = {
...decision.metadata,
targetMismatch: targetMismatchInfo
};
}
return decision;
}
/**
* Get judge decision history for a workflow
*/
async getJudgeHistory(workflowId: string, limit: number = 20, offset: number = 0): Promise<{
decisions: JudgeDecision[];
total: number;
hasMore: boolean;
}> {
return this.judgeEngine.getDecisionHistory(workflowId, limit, offset);
}
/**
* Update judge configuration for a workflow
*/
async updateJudgeConfig(workflowName: string, judgeConfig: any): Promise<void> {
const definition = this.definitions.get(workflowName);
if (!definition) {
throw new Error(`Workflow definition not found: ${workflowName}`);
}
definition.judgeConfig = judgeConfig;
await this.saveDefinition(definition);
}
}

363
test-prompt-for-llm.md Normal file
View file

@ -0,0 +1,363 @@
# Test Prompt for Generic DFA MCP Server
You are an LLM tasked with testing the Generic DFA MCP Server. Your goal is to verify that the system can handle ANY type of workflow dynamically, maintaining context and preventing premature completion.
## Test Overview
You will:
1. Define multiple different workflows dynamically
2. Run instances of each workflow
3. Test state transitions and context preservation
4. Verify checkpoint/rollback functionality
5. Confirm the system is truly generic
## Test 1: Customer Support Ticket Workflow
### Step 1.1: Define the Workflow
Use `workflow.define` to create a customer support ticket system:
```json
{
"name": "support-ticket",
"description": "Customer support ticket lifecycle",
"states": {
"new": {
"transitions": {
"assign": "assigned",
"close": "closed"
}
},
"assigned": {
"transitions": {
"start": "in_progress",
"escalate": "escalated",
"unassign": "new"
}
},
"in_progress": {
"transitions": {
"resolve": "resolved",
"escalate": "escalated",
"need_info": "waiting_customer"
}
},
"waiting_customer": {
"transitions": {
"customer_replied": "in_progress",
"timeout": "closed"
}
},
"escalated": {
"transitions": {
"resolve": "resolved",
"close": "closed"
}
},
"resolved": {
"transitions": {
"reopen": "in_progress",
"close": "closed"
}
},
"closed": { "final": true }
},
"initialState": "new"
}
```
### Step 1.2: Run the Workflow
1. Start a support ticket with context: `{ticketId: "T-001", customer: "alice@example.com", issue: "Cannot login", priority: "high"}`
2. Assign to agent: `action='assign', data={agent: "bob@support.com", assignedAt: "<timestamp>"}`
3. Start working: `action='start'`
4. Create checkpoint: `description='Ticket in progress'`
5. Need customer info: `action='need_info', data={question: "Which browser are you using?"}`
6. Customer replies: `action='customer_replied', data={response: "Chrome v120", repliedAt: "<timestamp>"}`
7. Resolve ticket: `action='resolve', data={solution: "Cleared browser cache", resolvedAt: "<timestamp>"}`
8. Close ticket: `action='close'`
9. Verify final state is 'closed' and context contains full history
## Test 2: Document Approval Workflow
### Step 2.1: Define the Workflow
Use `workflow.define` to create a document approval process:
```json
{
"name": "document-approval",
"description": "Multi-level document approval",
"states": {
"draft": {
"transitions": {
"submit": "level1_review",
"delete": "deleted"
}
},
"level1_review": {
"transitions": {
"approve": "level2_review",
"reject": "draft",
"request_changes": "draft"
}
},
"level2_review": {
"transitions": {
"approve": "approved",
"reject": "level1_review",
"request_changes": "draft"
}
},
"approved": {
"transitions": {
"publish": "published",
"archive": "archived"
}
},
"published": { "final": true },
"archived": { "final": true },
"deleted": { "final": true }
},
"initialState": "draft"
}
```
### Step 2.2: Run the Workflow
1. Start with context: `{documentId: "DOC-2024-001", title: "Q4 Report", author: "finance@company.com"}`
2. Submit for review: `action='submit', data={submittedAt: "<timestamp>"}`
3. Level 1 requests changes: `action='request_changes', data={comments: ["Add revenue projections"]}`
4. Resubmit: `action='submit', data={changes: "Added projections", version: 2}`
5. Level 1 approves: `action='approve', data={approver: "manager@company.com"}`
6. Create checkpoint: `description='Before final approval'`
7. Level 2 rejects: `action='reject', data={reason: "Need CEO input"}`
8. List checkpoints and rollback to 'Before final approval'
9. Level 2 approves: `action='approve', data={approver: "director@company.com"}`
10. Publish: `action='publish', data={publishedAt: "<timestamp>", url: "https://..."}`
## Test 3: Order Processing State Machine
### Step 3.1: Define the Workflow
Use `workflow.define` to create an e-commerce order workflow:
```json
{
"name": "order-processing",
"description": "E-commerce order fulfillment",
"states": {
"pending": {
"transitions": {
"pay": "paid",
"cancel": "cancelled"
}
},
"paid": {
"transitions": {
"process": "processing",
"refund": "refunded"
}
},
"processing": {
"transitions": {
"ship": "shipped",
"backorder": "backordered",
"cancel": "cancelled"
}
},
"backordered": {
"transitions": {
"ship": "shipped",
"cancel": "cancelled"
}
},
"shipped": {
"transitions": {
"deliver": "delivered",
"return": "returned"
}
},
"delivered": { "final": true },
"returned": { "final": true },
"refunded": { "final": true },
"cancelled": { "final": true }
},
"initialState": "pending"
}
```
### Step 3.2: Run the Workflow
1. Start order: `context={orderId: "ORD-123", items: ["laptop", "mouse"], total: 1200}`
2. Process payment: `action='pay', data={paymentId: "PAY-456", method: "credit_card"}`
3. Start processing: `action='process'`
4. Ship order: `action='ship', data={trackingNumber: "TRACK-789", carrier: "FedEx"}`
5. Deliver: `action='deliver', data={deliveredAt: "<timestamp>", signature: "John Doe"}`
## Test 4: Feature Flag Rollout
### Step 4.1: Define the Workflow
Use `workflow.define` to create a feature flag rollout process:
```json
{
"name": "feature-rollout",
"description": "Gradual feature flag deployment",
"states": {
"planning": {
"transitions": {
"approve": "canary",
"reject": "cancelled"
}
},
"canary": {
"transitions": {
"expand": "partial",
"rollback": "rolled_back"
}
},
"partial": {
"transitions": {
"expand": "full",
"rollback": "canary",
"emergency_stop": "rolled_back"
}
},
"full": {
"transitions": {
"finalize": "completed",
"rollback": "partial"
}
},
"completed": { "final": true },
"rolled_back": { "final": true },
"cancelled": { "final": true }
},
"initialState": "planning"
}
```
### Step 4.2: Run with Checkpoints
1. Start: `context={feature: "dark-mode", targetUsers: 1000000}`
2. Approve: `action='approve', data={approvedBy: "product-team"}`
3. Create checkpoint: `description='Canary deployment started'`
4. Expand to partial: `action='expand', data={percentage: 10, metrics: {errors: 0}}`
5. Create checkpoint: `description='10% rollout stable'`
6. Simulate error scenario - use `action='emergency_stop'`
7. List all checkpoints
8. Rollback to '10% rollout stable' checkpoint
9. Continue expansion: `action='expand', data={percentage: 100}`
10. Finalize: `action='finalize'`
## Test 5: Complex Scenario - Interview Process
### Step 5.1: Define the Workflow
Create an interview process with multiple paths:
```json
{
"name": "interview-process",
"description": "Candidate interview workflow",
"states": {
"applied": {
"transitions": {
"screen": "screening",
"reject": "rejected"
}
},
"screening": {
"transitions": {
"pass": "phone_interview",
"fail": "rejected"
}
},
"phone_interview": {
"transitions": {
"pass": "technical_interview",
"fail": "rejected",
"no_show": "rescheduling"
}
},
"rescheduling": {
"transitions": {
"reschedule": "phone_interview",
"withdraw": "withdrawn"
}
},
"technical_interview": {
"transitions": {
"pass": "final_interview",
"fail": "rejected",
"maybe": "additional_round"
}
},
"additional_round": {
"transitions": {
"pass": "final_interview",
"fail": "rejected"
}
},
"final_interview": {
"transitions": {
"hire": "offer_extended",
"reject": "rejected"
}
},
"offer_extended": {
"transitions": {
"accept": "hired",
"decline": "declined",
"negotiate": "negotiating"
}
},
"negotiating": {
"transitions": {
"accept": "hired",
"decline": "declined"
}
},
"hired": { "final": true },
"rejected": { "final": true },
"declined": { "final": true },
"withdrawn": { "final": true }
},
"initialState": "applied"
}
```
### Step 5.2: Run Complex Scenario
1. Start: `context={candidateId: "C-001", position: "Senior Engineer", appliedAt: "<timestamp>"}`
2. Screen candidate: `action='screen'`
3. Pass screening: `action='pass', data={score: 85}`
4. No show for phone interview: `action='no_show'`
5. Reschedule: `action='reschedule', data={newDate: "<future-date>"}`
6. Pass phone interview: `action='pass', data={interviewer: "tech-lead"}`
7. Technical interview needs additional round: `action='maybe', data={reason: "Need to assess system design"}`
8. Pass additional round: `action='pass'`
9. Pass final interview: `action='hire'`
10. Extend offer: `data={salary: 150000, startDate: "<date>"}`
11. Candidate negotiates: `action='negotiate', data={requestedSalary: 165000}`
12. Accept negotiated offer: `action='accept', data={finalSalary: 160000}`
## Verification Checklist
After completing all tests, verify:
- [ ] **Dynamic Definition**: You successfully defined 5 different workflows on the fly
- [ ] **State Enforcement**: Each workflow enforced its defined transitions (couldn't skip states)
- [ ] **Context Preservation**: All data added during transitions was preserved
- [ ] **No Hardcoding**: The system handled completely different domains without any file-specific logic
- [ ] **Checkpoints Work**: Successfully created and rolled back to checkpoints
- [ ] **Multiple Workflows**: Could run different workflow types simultaneously
- [ ] **Proper Completion**: Workflows only completed when reaching actual final states
- [ ] **Error Prevention**: Invalid actions were rejected with clear errors
## Success Criteria
The test is successful if:
1. All 5 workflows were defined and executed without any hardcoded logic
2. Context was never lost between transitions
3. The system prevented invalid state transitions
4. Checkpoints and rollbacks worked across all workflow types
5. Each workflow reached its final state only through valid paths
## Additional Tests (Optional)
Try defining your own creative workflows:
- A game state machine (menu → playing → paused → game_over)
- A content moderation flow
- A subscription lifecycle
- A build/deployment pipeline
- Any multi-step process you can imagine
The system should handle ANY valid state machine you can define!

20
tsconfig.json Normal file
View file

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}