From 885bd1075b63bfcb259259f4bf30438e542f4949 Mon Sep 17 00:00:00 2001 From: Pratik Narola Date: Thu, 10 Jul 2025 14:34:40 +0530 Subject: [PATCH] working commit Working deleted test files --- .gitignore | 6 + .../definitions/doc-review-structural.json | 42 + ...-54213a57-b92e-4ca2-8d4c-19fe6ebde222.json | 11 + README.md | 600 ++++++ package-lock.json | 1629 +++++++++++++++++ package.json | 24 + src/error-formatter.ts | 306 ++++ src/index.ts | 969 ++++++++++ src/judge-engine.ts | 1034 +++++++++++ src/types.ts | 134 ++ src/workflow-engine.ts | 683 +++++++ test-prompt-for-llm.md | 363 ++++ tsconfig.json | 20 + 13 files changed, 5821 insertions(+) create mode 100644 .gitignore create mode 100644 .test-compare/definitions/doc-review-structural.json create mode 100644 .test-compare/wf-54213a57-b92e-4ca2-8d4c-19fe6ebde222.json create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/error-formatter.ts create mode 100755 src/index.ts create mode 100644 src/judge-engine.ts create mode 100644 src/types.ts create mode 100644 src/workflow-engine.ts create mode 100644 test-prompt-for-llm.md create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d72db2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +.workflows/ +.test-workflows/ +*.log +.DS_Store \ No newline at end of file diff --git a/.test-compare/definitions/doc-review-structural.json b/.test-compare/definitions/doc-review-structural.json new file mode 100644 index 0000000..508ea56 --- /dev/null +++ b/.test-compare/definitions/doc-review-structural.json @@ -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 + } +} \ No newline at end of file diff --git a/.test-compare/wf-54213a57-b92e-4ca2-8d4c-19fe6ebde222.json b/.test-compare/wf-54213a57-b92e-4ca2-8d4c-19fe6ebde222.json new file mode 100644 index 0000000..21800c5 --- /dev/null +++ b/.test-compare/wf-54213a57-b92e-4ca2-8d4c-19fe6ebde222.json @@ -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" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d4e4f00 --- /dev/null +++ b/README.md @@ -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" + } + } + } +} +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8fc71e2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1629 @@ +{ + "name": "dfa-mcp-server", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dfa-mcp-server", + "version": "0.1.0", + "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" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", + "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", + "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", + "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", + "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", + "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", + "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", + "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", + "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", + "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", + "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", + "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", + "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", + "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", + "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", + "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", + "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", + "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", + "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", + "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", + "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", + "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", + "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", + "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", + "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", + "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", + "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.15.0.tgz", + "integrity": "sha512-67hnl/ROKdb03Vuu0YOr+baKTvf1/5YBHBm9KnZdjdAh8hjt4FRCPD5ucwxGB237sBpzlqQsLy1PFu7z/ekZ9Q==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "20.19.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.6.tgz", + "integrity": "sha512-uYssdp9z5zH5GQ0L4zEJ2ZuavYsJwkozjiUzCRfGtaaQcyjAMJ34aP8idv61QlqTozu6kudyr6JMq9Chf09dfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", + "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.6", + "@esbuild/android-arm": "0.25.6", + "@esbuild/android-arm64": "0.25.6", + "@esbuild/android-x64": "0.25.6", + "@esbuild/darwin-arm64": "0.25.6", + "@esbuild/darwin-x64": "0.25.6", + "@esbuild/freebsd-arm64": "0.25.6", + "@esbuild/freebsd-x64": "0.25.6", + "@esbuild/linux-arm": "0.25.6", + "@esbuild/linux-arm64": "0.25.6", + "@esbuild/linux-ia32": "0.25.6", + "@esbuild/linux-loong64": "0.25.6", + "@esbuild/linux-mips64el": "0.25.6", + "@esbuild/linux-ppc64": "0.25.6", + "@esbuild/linux-riscv64": "0.25.6", + "@esbuild/linux-s390x": "0.25.6", + "@esbuild/linux-x64": "0.25.6", + "@esbuild/netbsd-arm64": "0.25.6", + "@esbuild/netbsd-x64": "0.25.6", + "@esbuild/openbsd-arm64": "0.25.6", + "@esbuild/openbsd-x64": "0.25.6", + "@esbuild/openharmony-arm64": "0.25.6", + "@esbuild/sunos-x64": "0.25.6", + "@esbuild/win32-arm64": "0.25.6", + "@esbuild/win32-ia32": "0.25.6", + "@esbuild/win32-x64": "0.25.6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz", + "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", + "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a6e5104 --- /dev/null +++ b/package.json @@ -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" + } +} \ No newline at end of file diff --git a/src/error-formatter.ts b/src/error-formatter.ts new file mode 100644 index 0000000..bdb7c64 --- /dev/null +++ b/src/error-formatter.ts @@ -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 '""'; + } + + /** + * 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'); + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100755 index 0000000..9dd93b7 --- /dev/null +++ b/src/index.ts @@ -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); +}); \ No newline at end of file diff --git a/src/judge-engine.ts b/src/judge-engine.ts new file mode 100644 index 0000000..c53ad61 --- /dev/null +++ b/src/judge-engine.ts @@ -0,0 +1,1034 @@ +import { + JudgeConfig, + JudgeDecision, + TransitionAttempt, + ValidationResult, + ValidationRule, + WorkflowContext, + WorkflowDefinition, + TransitionRule, + ConditionEvaluationResult, + ConditionEvaluation +} from './types.js'; + +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { ErrorFormatter } from './error-formatter.js'; + +export class JudgeEngine { + private decisionHistory: Map = new Map(); + private llmBaseUrl: string | undefined; + private llmApiKey: string | undefined; + private llmModel: string; + private llmThinkingMode: string; + private historyDir: string; + + // Retry configuration + private readonly MAX_RETRIES = 3; + private readonly RETRY_DELAY = 1000; // 1 second initial delay + private readonly RETRY_BACKOFF = 2; // Exponential backoff multiplier + + // History configuration + private readonly MEMORY_CACHE_SIZE = 20; // Keep last N entries in memory per workflow + + constructor(workflowDir: string = '.workflows') { + // Load LLM configuration from environment + this.llmBaseUrl = process.env.LLM_BASE_URL; + this.llmApiKey = process.env.LLM_API_KEY; + this.llmModel = process.env.LLM_JUDGE_MODEL || 'gemini-2.5-pro'; + this.llmThinkingMode = process.env.LLM_JUDGE_THINKING_MODE || 'high'; + + // Set up history directory + this.historyDir = path.join(workflowDir, 'history'); + } + + /** + * Initialize the judge engine and create history directory + */ + async initialize(): Promise { + try { + await fs.mkdir(this.historyDir, { recursive: true }); + } catch (error) { + console.error('Failed to create history directory:', error); + } + } + + /** + * Validate a transition attempt using the workflow's judge configuration + */ + async validateTransition(attempt: TransitionAttempt): Promise { + const { definition } = attempt; + + // If judge is not enabled, approve everything + if (!definition.judgeConfig?.enabled) { + return { + approved: true, + confidence: 1.0, + reasoning: 'Judge not enabled for this workflow' + }; + } + + const config = definition.judgeConfig; + + // Perform sanity check first + const sanityCheck = await this.performSanityCheck(attempt); + if (!sanityCheck.safe) { + console.warn('Judge Sanity Check failed with warnings:', sanityCheck.warnings); + // Add warnings to the decision metadata + if (config.strictMode) { + // In strict mode, reject transitions that fail sanity check + return { + approved: false, + confidence: 0, + reasoning: 'Transition rejected due to potential validation bypass attempt', + violations: sanityCheck.warnings, + suggestions: ['Remove suspicious patterns from validators and data'], + metadata: { + sanityCheckFailed: true, + warnings: sanityCheck.warnings + } + }; + } + } + + // Use LLM judge if configured and available + if (config.useLLM && this.llmBaseUrl && this.llmApiKey) { + console.log('LLM Judge: Attempting to use LLM validation'); + console.log(`LLM Config: URL=${this.llmBaseUrl}, Model=${this.llmModel}`); + try { + const decision = await this.callLLMJudge(attempt); + console.log('LLM Judge: Successfully received decision'); + + // Add sanity check warnings to decision if any + if (sanityCheck.warnings.length > 0) { + decision.metadata = { + ...decision.metadata, + sanityCheckWarnings: sanityCheck.warnings + }; + } + + return decision; + } catch (error) { + console.error('LLM judge failed, falling back to structural validation:', error); + // Fall through to structural validation + } + } else { + console.log(`LLM Judge: Not using LLM (useLLM=${config.useLLM}, hasUrl=${!!this.llmBaseUrl}, hasKey=${!!this.llmApiKey})`); + } + const violations: string[] = []; + const suggestions: string[] = []; + let totalConfidence = 0; + let validationCount = 0; + + // 1. Validate structural correctness (40% weight) + const structuralResult = this.validateStructure(attempt); + totalConfidence += structuralResult.confidence * 0.4; + validationCount += 0.4; + if (!structuralResult.valid) { + violations.push(`Structural: ${structuralResult.reason}`); + } + + // 2. Validate exit conditions of current state (15% weight) + const exitResult = this.validateStateExit(attempt); + if (exitResult) { + totalConfidence += exitResult.confidence * 0.15; + validationCount += 0.15; + if (!exitResult.valid) { + violations.push(`Exit condition: ${exitResult.reason}`); + suggestions.push('Ensure all exit conditions are met before transitioning'); + } + } + + // 3. Validate entry conditions of next state (15% weight) + const entryResult = this.validateStateEntry(attempt); + if (entryResult) { + totalConfidence += entryResult.confidence * 0.15; + validationCount += 0.15; + if (!entryResult.valid) { + violations.push(`Entry condition: ${entryResult.reason}`); + suggestions.push('Check that the target state prerequisites are satisfied'); + } + } + + // 4. Validate transition-specific rules (20% weight) + const transitionResult = this.validateTransitionRules(attempt); + if (transitionResult) { + totalConfidence += transitionResult.confidence * 0.2; + validationCount += 0.2; + if (!transitionResult.valid) { + violations.push(`Transition rule: ${transitionResult.reason}`); + } + } + + // 5. Validate data completeness (10% weight) + const dataResult = this.validateDataCompleteness(attempt); + totalConfidence += dataResult.confidence * 0.1; + validationCount += 0.1; + if (!dataResult.valid) { + violations.push(`Data: ${dataResult.reason}`); + suggestions.push('Provide all required data fields for this action'); + } + + // 6. Run custom validation rules + if (config.validationRules) { + for (const rule of config.validationRules) { + const result = rule.validate(attempt); + if (!result.valid) { + violations.push(`${rule.name}: ${result.reason}`); + } + } + } + + // 7. Run custom validator if provided + if (config.customValidator) { + return config.customValidator(attempt); + } + + // Calculate final confidence + // Note: totalConfidence already has weighted values, no need to divide + const confidence = totalConfidence; + + // Determine approval based on confidence and strict mode + let approved = violations.length === 0; + if (config.strictMode && config.minConfidence) { + approved = approved && confidence >= config.minConfidence; + if (confidence < config.minConfidence) { + violations.push(`Confidence ${confidence.toFixed(2)} below minimum ${config.minConfidence}`); + } + } + + // Generate reasoning + const reasoning = this.generateReasoning(attempt, violations, confidence); + + // Generate suggestions if not approved + if (!approved && suggestions.length === 0) { + suggestions.push(...this.generateSuggestions(attempt, violations)); + } + + const decision: JudgeDecision = { + approved, + confidence, + reasoning, + violations: violations.length > 0 ? violations : undefined, + suggestions: suggestions.length > 0 ? suggestions : undefined, + metadata: sanityCheck.warnings.length > 0 ? { + sanityCheckWarnings: sanityCheck.warnings + } : undefined + }; + + // Store decision in history + await this.recordDecision(attempt.workflowId, decision); + + return decision; + } + + /** + * Evaluate transition conditions using LLM to determine which rule matches + */ + async evaluateTransitionConditions( + rules: TransitionRule[], + context: WorkflowContext, + attempt: TransitionAttempt + ): Promise { + // If LLM is not available, fall back to simple evaluation + if (!this.llmBaseUrl || !this.llmApiKey) { + return this.evaluateConditionsSimple(rules, context); + } + + const prompt = this.buildConditionEvaluationPrompt(rules, context, attempt); + + try { + const response = await fetch(`${this.llmBaseUrl}/chat/completions`, { + method: 'POST', + headers: { + 'x-litellm-api-key': this.llmApiKey, + 'Content-Type': 'application/json', + 'accept': 'application/json' + }, + body: JSON.stringify({ + model: this.llmModel, + messages: [ + { + role: 'system', + content: 'You are a workflow condition evaluator. Analyze conditions and determine which ones are true based on the context.' + }, + { + role: 'user', + content: prompt + } + ], + temperature: 0.2, // Low temperature for consistent evaluation + max_tokens: 4000 + }) + }); + + if (!response.ok) { + throw new Error(`LLM API error: ${response.status} ${response.statusText}`); + } + + const data = await response.json() as any; + const content = data.choices?.[0]?.message?.content; + + if (!content) { + throw new Error('No content in LLM response'); + } + + return this.parseConditionEvaluationResponse(content, rules); + + } catch (error) { + console.error('LLM condition evaluation failed, falling back to simple evaluation:', error); + return this.evaluateConditionsSimple(rules, context); + } + } + + /** + * Build prompt for LLM condition evaluation + */ + private buildConditionEvaluationPrompt( + rules: TransitionRule[], + context: WorkflowContext, + attempt: TransitionAttempt + ): string { + return `Evaluate these code-like conditions against the provided context. +Use your reasoning to understand what each condition is checking for. + +Current State: ${attempt.fromState} +Action: ${attempt.action} +Workflow: ${attempt.definition.name} + +Context Data: +${JSON.stringify(context, null, 2)} + +Conditions to evaluate: +${rules.map((r, i) => `${i+1}. "${r.condition}"${r.description ? ` // ${r.description}` : ''} -> target: "${r.target}"`).join('\n')} + +For each condition: +1. Parse what properties and values it's checking +2. Find or intelligently derive those values from the context +3. Apply the logical operators (>, <, >=, <=, ===, !==, &&, ||) +4. Determine if the condition is TRUE or FALSE +5. Provide your confidence (0.0-1.0) and reasoning + +Example evaluation: +Condition: "context.amount > 1000 && context.priority === 'high'" +- context.amount: Looking at the context, amount is 2500 +- context.priority: The priority field shows 'high' +- Evaluation: 2500 > 1000 && 'high' === 'high' = TRUE && TRUE = TRUE +- Confidence: 1.0 (values are explicitly in context) + +If a property is not explicitly in the context but can be inferred, explain your reasoning. +For example, if checking "context.isUrgent" but only seeing deadlineDate, you might infer urgency from how close the deadline is. + +RESPOND WITH JSON: +{ + "evaluations": [ + { + "index": 0, + "condition": "the condition string", + "result": true/false, + "confidence": 0.0-1.0, + "reasoning": "Step by step explanation", + "extractedValues": { + "property.path": "extracted or inferred value" + } + } + ], + "recommendedIndex": 0 or null if none match, + "overallReasoning": "Summary of the evaluation process" +}`; + } + + /** + * Parse LLM response for condition evaluation + */ + private parseConditionEvaluationResponse(content: string, rules: TransitionRule[]): ConditionEvaluationResult { + try { + const jsonMatch = content.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error('No JSON found in LLM response'); + } + + const parsed = JSON.parse(jsonMatch[0]); + + const evaluations = parsed.evaluations.map((e: any) => ({ + condition: e.condition || rules[e.index]?.condition || '', + result: Boolean(e.result), + confidence: Math.max(0, Math.min(1, Number(e.confidence) || 0.5)), + reasoning: String(e.reasoning || 'No reasoning provided'), + extractedValues: e.extractedValues || {} + })); + + // Find the first true condition with high confidence + let matchedRule: TransitionRule | null = null; + const recommendedIndex = parsed.recommendedIndex; + + if (recommendedIndex !== null && recommendedIndex >= 0 && recommendedIndex < rules.length) { + if (evaluations[recommendedIndex]?.result) { + matchedRule = rules[recommendedIndex]; + } + } else { + // Find first matching condition + for (let i = 0; i < evaluations.length; i++) { + if (evaluations[i].result && evaluations[i].confidence >= 0.7) { + matchedRule = rules[i]; + break; + } + } + } + + return { + matchedRule, + evaluations, + overallReasoning: String(parsed.overallReasoning || 'Conditions evaluated using LLM reasoning') + }; + + } catch (error) { + // If parsing fails, return no match + return { + matchedRule: null, + evaluations: rules.map(r => ({ + condition: r.condition, + result: false, + confidence: 0, + reasoning: `Failed to parse LLM response: ${error instanceof Error ? error.message : String(error)}`, + extractedValues: {} + })), + overallReasoning: 'LLM response parsing failed' + }; + } + } + + /** + * Simple fallback evaluation for conditions (when LLM is not available) + */ + private evaluateConditionsSimple(rules: TransitionRule[], context: WorkflowContext): ConditionEvaluationResult { + const evaluations = rules.map(rule => { + // Simple evaluation: only handle "true" condition + const result = rule.condition.toLowerCase() === 'true'; + + return { + condition: rule.condition, + result, + confidence: result ? 1.0 : 0.0, + reasoning: result ? 'Always true condition' : 'Condition evaluation requires LLM', + extractedValues: {} + }; + }); + + // Find first true condition + const matchedIndex = evaluations.findIndex(e => e.result); + const matchedRule = matchedIndex >= 0 ? rules[matchedIndex] : null; + + return { + matchedRule, + evaluations, + overallReasoning: 'Simple evaluation without LLM - only "true" conditions supported' + }; + } + + /** + * Validate structural correctness of the transition + */ + private validateStructure(attempt: TransitionAttempt): ValidationResult { + const { fromState, action, toState, definition } = attempt; + + const currentStatedef = definition.states[fromState]; + if (!currentStatedef) { + return { + valid: false, + confidence: 0, + reason: `Current state '${fromState}' not found in workflow definition` + }; + } + + if (currentStatedef.final) { + return { + valid: false, + confidence: 0, + reason: `Cannot transition from final state '${fromState}'` + }; + } + + const transitionRules = currentStatedef.transitions?.[action]; + if (!transitionRules || transitionRules.length === 0) { + return { + valid: false, + confidence: 0, + reason: `Action '${action}' not valid for state '${fromState}'` + }; + } + + // Check if the target state is valid in any of the rules + const validTargets = transitionRules.map(rule => rule.target); + if (!validTargets.includes(toState)) { + return { + valid: false, + confidence: 0, + reason: `State '${toState}' is not a valid target for action '${action}' from state '${fromState}'. Valid targets: ${validTargets.join(', ')}` + }; + } + + return { valid: true, confidence: 1.0 }; + } + + /** + * Validate exit conditions of current state + */ + private validateStateExit(attempt: TransitionAttempt): ValidationResult | null { + const { fromState, context, definition } = attempt; + + const validator = definition.stateValidators?.[fromState]; + if (!validator?.exitConditions) { + return null; + } + + return validator.exitConditions(context); + } + + /** + * Validate entry conditions of target state + */ + private validateStateEntry(attempt: TransitionAttempt): ValidationResult | null { + const { toState, context, definition } = attempt; + + const validator = definition.stateValidators?.[toState]; + if (!validator?.entryConditions) { + return null; + } + + // Create projected context after transition + const projectedContext = { ...context, ...attempt.data }; + return validator.entryConditions(projectedContext); + } + + /** + * Validate transition-specific rules + */ + private validateTransitionRules(attempt: TransitionAttempt): ValidationResult | null { + const { action, data, context, definition } = attempt; + + const validator = definition.transitionValidators?.[action]; + if (!validator) { + return null; + } + + return validator(data, context); + } + + /** + * Validate data completeness and required fields + */ + private validateDataCompleteness(attempt: TransitionAttempt): ValidationResult { + const { toState, context, data, definition } = attempt; + + const validator = definition.stateValidators?.[toState]; + if (!validator?.requiredFields || validator.requiredFields.length === 0) { + return { valid: true, confidence: 1.0 }; + } + + // Check if required fields will be present after transition + const projectedContext = { ...context, ...data }; + const missingFields = validator.requiredFields.filter( + field => !this.hasNestedProperty(projectedContext, field) + ); + + if (missingFields.length > 0) { + return { + valid: false, + confidence: 0.5, + reason: `Missing required fields for state '${toState}': ${missingFields.join(', ')}` + }; + } + + return { valid: true, confidence: 1.0 }; + } + + /** + * Check if an object has a nested property (supports dot notation) + */ + private hasNestedProperty(obj: any, path: string): boolean { + const parts = path.split('.'); + let current = obj; + + for (const part of parts) { + if (current == null || typeof current !== 'object' || !(part in current)) { + return false; + } + current = current[part]; + } + + return current !== undefined; + } + + /** + * Generate human-readable reasoning for the decision + */ + private generateReasoning( + attempt: TransitionAttempt, + violations: string[], + confidence: number + ): string { + const { fromState, action, toState, definition } = attempt; + + if (violations.length === 0) { + return `${ErrorFormatter.formatTransitionPath(fromState, action, toState, true)} validated successfully with ${(confidence * 100).toFixed(0)}% confidence`; + } + + // Check if it's an invalid action error + const invalidActionViolation = violations.find(v => v.includes("not valid for state")); + if (invalidActionViolation) { + const validActions = Object.keys(definition.states[fromState]?.transitions || {}); + return ErrorFormatter.formatInvalidActionError(fromState, action, validActions, definition.name); + } + + return `${ErrorFormatter.formatTransitionPath(fromState, action, toState, false)} rejected:\n${violations.map((v, i) => ` ${i + 1}. ${v}`).join('\n')}`; + } + + /** + * Generate helpful suggestions based on violations + */ + private generateSuggestions(attempt: TransitionAttempt, violations: string[]): string[] { + const suggestions: string[] = []; + const { fromState, action, toState, data, context, definition } = attempt; + + for (const violation of violations) { + if (violation.includes('Missing required fields')) { + // Extract missing fields from violation message + const match = violation.match(/Missing required fields[^:]*: (.+)/); + if (match) { + const missingFields = match[1].split(',').map(f => f.trim()); + const errorMsg = ErrorFormatter.formatMissingFieldsError( + missingFields, + context, + toState, + action + ); + suggestions.push(errorMsg); + } else { + suggestions.push('Include all required fields in the transition data'); + } + } else if (violation.includes('not valid for state')) { + const validActions = Object.keys(definition.states[fromState]?.transitions || {}); + if (validActions.length > 0) { + suggestions.push(`Valid actions for state '${fromState}':\n${validActions.map(a => ` • ${a}`).join('\n')}`); + + // Add example usage + suggestions.push(`Example: workflow.advance({ id: "...", action: "${validActions[0]}", data: { ... } })`); + } else { + suggestions.push(`State '${fromState}' has no available transitions (might be a final state)`); + } + } else if (violation.includes('Exit condition')) { + suggestions.push(`Exit condition failed for state '${fromState}':\n - Review the current context to ensure all exit requirements are met\n - Check if any required processing is complete before transitioning`); + + // Show current context summary + const contextKeys = Object.keys(context); + if (contextKeys.length > 0) { + suggestions.push(`Current context has: ${contextKeys.slice(0, 5).join(', ')}${contextKeys.length > 5 ? ' ...' : ''}`); + } + } else if (violation.includes('Entry condition')) { + suggestions.push(`Entry condition failed for state '${toState}':\n - Ensure all prerequisites are satisfied\n - Check if required data is included in the transition`); + + // Show what data was provided + if (data && Object.keys(data).length > 0) { + suggestions.push(`Provided data: ${Object.keys(data).join(', ')}`); + } else { + suggestions.push('No data was provided with this transition'); + } + } else if (violation.includes('Confidence') && violation.includes('below minimum')) { + const match = violation.match(/Confidence ([\d.]+) below minimum ([\d.]+)/); + if (match) { + suggestions.push(`Confidence too low (${match[1]} < ${match[2]}):\n - Provide more complete data\n - Ensure the transition makes logical sense\n - Check for any validation warnings`); + } + } + } + + // If no specific suggestions were generated, provide general guidance + if (suggestions.length === 0 && violations.length > 0) { + suggestions.push('Review the workflow definition and ensure your transition meets all requirements'); + suggestions.push(`Current state '${fromState}' expects specific conditions to transition to '${toState}'`); + } + + return suggestions; + } + + /** + * Record decision in history + */ + async recordDecision(workflowId: string, decision: JudgeDecision): Promise { + const timestampedDecision = { + ...decision, + metadata: { + ...decision.metadata, + timestamp: new Date().toISOString() + } + }; + + // Keep in-memory cache for quick access + const memoryHistory = this.decisionHistory.get(workflowId) || []; + memoryHistory.push(timestampedDecision); + + // Keep only the last N entries in memory + if (memoryHistory.length > this.MEMORY_CACHE_SIZE) { + memoryHistory.splice(0, memoryHistory.length - this.MEMORY_CACHE_SIZE); + } + this.decisionHistory.set(workflowId, memoryHistory); + + // Append to file + try { + const historyFile = path.join(this.historyDir, `${workflowId}-judge.log`); + const logLine = JSON.stringify(timestampedDecision) + '\n'; + await fs.appendFile(historyFile, logLine); + } catch (error) { + console.error(`Failed to write judge decision to file for workflow ${workflowId}:`, error); + } + } + + /** + * Get decision history for a workflow with pagination + */ + async getDecisionHistory(workflowId: string, limit: number = 20, offset: number = 0): Promise<{ + decisions: JudgeDecision[]; + total: number; + hasMore: boolean; + }> { + // First check memory cache for recent entries + const memoryHistory = this.decisionHistory.get(workflowId) || []; + + try { + const historyFile = path.join(this.historyDir, `${workflowId}-judge.log`); + + // Check if file exists + try { + await fs.access(historyFile); + } catch { + // File doesn't exist, return empty + return { decisions: [], total: 0, hasMore: false }; + } + + // Read file line by line + const fileContent = await fs.readFile(historyFile, 'utf-8'); + const lines = fileContent.trim().split('\n').filter(line => line); + const totalCount = lines.length; + + // Apply pagination + const startIndex = Math.max(0, totalCount - offset - limit); + const endIndex = totalCount - offset; + const selectedLines = lines.slice(startIndex, endIndex).reverse(); // Most recent first + + const decisions: JudgeDecision[] = []; + for (const line of selectedLines) { + try { + decisions.push(JSON.parse(line)); + } catch (error) { + console.warn(`Failed to parse judge history line: ${line}`); + } + } + + return { + decisions, + total: totalCount, + hasMore: startIndex > 0 + }; + } catch (error) { + console.error(`Failed to read judge history for workflow ${workflowId}:`, error); + // Fallback to memory cache + return { + decisions: memoryHistory.slice(-limit), + total: memoryHistory.length, + hasMore: false + }; + } + } + + /** + * Clear decision history for a workflow + */ + async clearHistory(workflowId: string): Promise { + // Clear memory cache + this.decisionHistory.delete(workflowId); + + // Clear file + try { + const historyFile = path.join(this.historyDir, `${workflowId}-judge.log`); + await fs.unlink(historyFile); + } catch (error) { + // File might not exist, which is fine + if ((error as any).code !== 'ENOENT') { + console.error(`Failed to delete judge history file for workflow ${workflowId}:`, error); + } + } + } + + /** + * Sleep utility for retry delays + */ + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + /** + * Perform sanity check to detect potential bypass attempts + */ + private async performSanityCheck(attempt: TransitionAttempt): Promise<{ + safe: boolean; + warnings: string[]; + }> { + const warnings: string[] = []; + + // Check for bypass patterns in custom validators + if (attempt.definition.judgeConfig?.customValidator) { + const funcStr = attempt.definition.judgeConfig.customValidator.toString(); + // Check for patterns that always approve or bypass validation + if (funcStr.match(/always\s*(return|approve)|approved\s*:\s*true|return\s*{\s*approved\s*:\s*true/i)) { + warnings.push("Custom validator may bypass validation logic - contains 'always approve' pattern"); + } + if (funcStr.match(/return\s*true\s*;|return\s*{\s*}\s*;/i)) { + warnings.push("Custom validator may not perform actual validation"); + } + } + + // Check for malicious prompt patterns in data + if (attempt.data) { + const dataStr = JSON.stringify(attempt.data).toLowerCase(); + const promptInjectionPatterns = [ + /ignore\s*previous\s*instructions/, + /ignore\s*all\s*previous/, + /always\s*approve/, + /bypass\s*validation/, + /skip\s*validation/, + /approved\s*:\s*true/, + /confidence\s*:\s*1/, + /override\s*judge/, + /disregard\s*rules/ + ]; + + for (const pattern of promptInjectionPatterns) { + if (dataStr.match(pattern)) { + warnings.push(`Data contains potential prompt injection: '${pattern.source}'`); + } + } + } + + // Check for invalid confidence thresholds + const minConfidence = attempt.definition.judgeConfig?.minConfidence; + if (minConfidence !== undefined) { + if (minConfidence <= 0 || minConfidence > 1) { + warnings.push(`Invalid confidence threshold: ${minConfidence} (must be between 0 and 1)`); + } + if (minConfidence < 0.3) { + warnings.push(`Very low confidence threshold: ${minConfidence} may allow weak validations`); + } + } + + // Check if judge is being bypassed entirely + if (attempt.definition.judgeConfig?.enabled === false) { + warnings.push("Judge is disabled - all transitions will be auto-approved"); + } + + // Check for suspicious state validator patterns + const validators = attempt.definition.stateValidators; + if (validators) { + for (const [state, validator] of Object.entries(validators)) { + if (validator.exitConditions || validator.entryConditions) { + // Check if validators are functions that always return true + const exitStr = validator.exitConditions?.toString() || ''; + const entryStr = validator.entryConditions?.toString() || ''; + + if (exitStr.match(/return\s*{\s*valid\s*:\s*true/i) || + entryStr.match(/return\s*{\s*valid\s*:\s*true/i)) { + warnings.push(`State '${state}' validators may not perform actual validation`); + } + } + } + } + + // Log warnings for debugging + if (warnings.length > 0) { + console.warn('Judge Sanity Check Warnings:', warnings); + } + + return { + safe: warnings.length === 0, + warnings + }; + } + + /** + * Call LLM for intelligent judge decision + */ + private async callLLMJudge(attempt: TransitionAttempt): Promise { + const prompt = this.buildLLMPrompt(attempt); + + for (let retryCount = 0; retryCount < this.MAX_RETRIES; retryCount++) { + try { + const response = await fetch(`${this.llmBaseUrl}/chat/completions`, { + method: 'POST', + headers: { + 'x-litellm-api-key': this.llmApiKey || '', + 'Content-Type': 'application/json', + 'accept': 'application/json' + }, + body: JSON.stringify({ + model: this.llmModel, + messages: [ + { + role: 'system', + content: 'You are a workflow validation judge. Analyze transitions and provide structured feedback. IMPORTANT: The target state in transition attempts is determined by the workflow engine after evaluating conditional rules, NOT specified by users. This is correct behavior for conditional transitions.' + }, + { + role: 'user', + content: prompt + } + ], + temperature: 0.3, + max_tokens: 64000 + }) + }); + + if (!response.ok) { + // Check if error is retryable + const isRetryable = [429, 502, 503, 504].includes(response.status); + const isLastAttempt = retryCount === this.MAX_RETRIES - 1; + + if (!isRetryable || isLastAttempt) { + throw new Error(`LLM API error: ${response.status} ${response.statusText}`); + } + + // Calculate delay with exponential backoff + const delay = this.RETRY_DELAY * Math.pow(this.RETRY_BACKOFF, retryCount); + console.log(`LLM Judge: Received ${response.status} error, retrying after ${delay}ms (attempt ${retryCount + 2}/${this.MAX_RETRIES})...`); + await this.sleep(delay); + continue; // Retry the request + } + + const data = await response.json() as any; + console.log('LLM Response:', JSON.stringify(data, null, 2)); + const content = data.choices?.[0]?.message?.content; + + if (!content) { + throw new Error('No content in LLM response'); + } + + console.log('LLM Content:', content); + return this.parseLLMResponse(content, attempt); + + } catch (error) { + const isLastAttempt = retryCount === this.MAX_RETRIES - 1; + + // Check if it's a network error (fetch failed) + const isNetworkError = error instanceof TypeError && + (error.message.includes('fetch') || error.message.includes('network')); + + // If it's not a network error or it's the last attempt, throw the error + if (!isNetworkError || isLastAttempt) { + const attemptInfo = retryCount > 0 ? ` after ${retryCount + 1} attempts` : ''; + throw new Error(`LLM judge error${attemptInfo}: ${error instanceof Error ? error.message : String(error)}`); + } + + // Network error and not last attempt - retry + const delay = this.RETRY_DELAY * Math.pow(this.RETRY_BACKOFF, retryCount); + console.log(`LLM Judge: Network error, retrying after ${delay}ms (attempt ${retryCount + 2}/${this.MAX_RETRIES})...`); + await this.sleep(delay); + } + } + + // Should never reach here, but just in case + throw new Error('LLM judge error: Max retries exceeded'); + } + + /** + * Build prompt for LLM judge + */ + private buildLLMPrompt(attempt: TransitionAttempt): string { + const { definition, fromState, action, toState, data, context } = attempt; + + return `Analyze this workflow transition attempt: + +WORKFLOW DEFINITION: +- Name: ${definition.name} +- Description: ${definition.description || 'No description'} +- Current State: ${fromState} +- Available Actions: ${Object.keys(definition.states[fromState]?.transitions || {}).join(', ')} + +TRANSITION ATTEMPT: +- Action: ${action} +- Target State: ${toState} (NOTE: This was determined by the workflow engine after evaluating conditional rules, NOT provided by the user) +- Data Provided: ${JSON.stringify(data || {}, null, 2)} + +IMPORTANT: For transitions with conditional rules, the workflow engine evaluates the conditions and determines the target state automatically. The "Target State" shown above is the result of that evaluation, not a user input. This is the correct behavior for conditional transitions. + +CURRENT CONTEXT: +${JSON.stringify(context, null, 2)} + +WORKFLOW STATES: +${JSON.stringify(definition.states, null, 2)} + +VALIDATION REQUIREMENTS: +${definition.stateValidators ? JSON.stringify(definition.stateValidators, null, 2) : 'None specified'} + +TASK: +1. Analyze if this transition makes semantic sense given the workflow's purpose +2. Check if the provided data is complete and appropriate +3. Consider the current context and whether prerequisites are met +4. Evaluate if this follows best practices for this type of workflow +5. Verify that the determined target state is appropriate for the current context + +RESPOND WITH JSON: +{ + "approved": true/false, + "confidence": 0.0-1.0, + "reasoning": "Clear explanation of decision", + "violations": ["List", "of", "issues"] or [], + "suggestions": ["Helpful", "fixes"] or [] +} + +Be strict but fair. Consider both technical correctness and business logic.`; + } + + /** + * Parse LLM response into JudgeDecision + */ + private parseLLMResponse(content: string, attempt: TransitionAttempt): JudgeDecision { + try { + // Try to extract JSON from the response + const jsonMatch = content.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error('No JSON found in LLM response'); + } + + const parsed = JSON.parse(jsonMatch[0]); + + // Validate and normalize the response + const decision: JudgeDecision = { + approved: Boolean(parsed.approved), + confidence: Math.max(0, Math.min(1, Number(parsed.confidence) || 0.5)), + reasoning: String(parsed.reasoning || 'No reasoning provided'), + violations: Array.isArray(parsed.violations) ? parsed.violations : undefined, + suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions : undefined, + metadata: { + llmModel: this.llmModel, + thinkingMode: this.llmThinkingMode + } + }; + + // Apply strict mode if configured + const config = attempt.definition.judgeConfig; + if (config?.strictMode && config.minConfidence && decision.confidence < config.minConfidence) { + decision.approved = false; + decision.violations = decision.violations || []; + decision.violations.push(`Confidence ${decision.confidence.toFixed(2)} below minimum ${config.minConfidence}`); + } + + return decision; + } catch (error) { + // If parsing fails, return a conservative decision + return { + approved: false, + confidence: 0, + reasoning: `Failed to parse LLM response: ${error instanceof Error ? error.message : String(error)}`, + violations: ['LLM response parsing failed'], + suggestions: ['Check LLM configuration and try again'] + }; + } + } +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..d4a8406 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,134 @@ +// Core types for the DFA workflow system + +export interface WorkflowDefinition { + name: string; + description?: string; + states: Record; + 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; // 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; // Values the LLM extracted/inferred from context +} + +export interface ConditionEvaluationResult { + matchedRule: TransitionRule | null; + evaluations: ConditionEvaluation[]; + overallReasoning: string; +} \ No newline at end of file diff --git a/src/workflow-engine.ts b/src/workflow-engine.ts new file mode 100644 index 0000000..34147c0 --- /dev/null +++ b/src/workflow-engine.ts @@ -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 = new Map(); + private instances: Map = new Map(); + private checkpoints: Map = 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 { + // 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + const definition = this.definitions.get(workflowName); + if (!definition) { + throw new Error(`Workflow definition not found: ${workflowName}`); + } + + definition.judgeConfig = judgeConfig; + await this.saveDefinition(definition); + } +} \ No newline at end of file diff --git a/test-prompt-for-llm.md b/test-prompt-for-llm.md new file mode 100644 index 0000000..c5001f0 --- /dev/null +++ b/test-prompt-for-llm.md @@ -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: ""}` +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: ""}` +7. Resolve ticket: `action='resolve', data={solution: "Cleared browser cache", resolvedAt: ""}` +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: ""}` +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: "", 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: "", 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: ""}` +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: ""}` +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: ""}` +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! \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..542c37e --- /dev/null +++ b/tsconfig.json @@ -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"] +} \ No newline at end of file