diff --git a/.eslintrc.js b/.eslintrc.js index 0faa3f1..bfcd25f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -55,6 +55,15 @@ module.exports = { rules: { '@typescript-eslint/no-var-requires': 'off' } + }, + { + files: ['scripts/**/*.ts'], + parserOptions: { + project: null + }, + rules: { + 'no-console': 'off' + } } ] }; \ No newline at end of file diff --git a/.gitignore b/.gitignore index cc3cf6f..6e9cb7b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,11 @@ build/ lib/ *.tsbuildinfo +# Compiled scripts +scripts/**/*.js +scripts/**/*.d.ts +scripts/**/*.map + # Environment variables .env .env.local diff --git a/docs/api/configuration-schema.md b/docs/api/configuration-schema.md new file mode 100644 index 0000000..b90dc00 --- /dev/null +++ b/docs/api/configuration-schema.md @@ -0,0 +1,286 @@ +# Configuration Schema Reference + +This document provides a comprehensive reference for the Agents CLI configuration schema. + +## Overview + +Agents CLI uses a JSON or YAML configuration file to define agents, workflows, and their behavior. +The configuration is validated using Zod schemas to ensure correctness before execution. + +## Configuration File Structure + +```typescript +{ + "metadata": { + "name": "string", + "description": "string", + "version": "string", + "author": "string" + }, + "agents": { + "[agent_id]": { + // Agent configuration + } + }, + "workflow": { + // Workflow configuration + } +} +``` + +## Metadata (Optional) + +Configuration metadata for documentation and versioning. + +| Field | Type | Required | Description | +| ------------- | ------ | -------- | -------------------------------------- | +| `name` | string | No | Configuration name | +| `description` | string | No | Configuration description | +| `version` | string | No | Configuration version (default: "1.0") | +| `author` | string | No | Configuration author | +| `created` | string | No | Creation timestamp (ISO 8601) | +| `updated` | string | No | Last update timestamp (ISO 8601) | + +## Agents Configuration + +The `agents` object contains one or more agent configurations, keyed by agent ID. + +### Agent Configuration + +| Field | Type | Required | Default | Description | +| --------------- | ------ | -------- | -------- | ---------------------------------- | +| `name` | string | Yes | - | Human-readable agent name | +| `instructions` | string | Yes | - | Agent's system instructions/prompt | +| `model` | string | No | "gpt-4o" | OpenAI model to use | +| `modelSettings` | object | No | - | Model fine-tuning parameters | +| `tools` | array | No | [] | Tools available to the agent | +| `guardrails` | array | No | [] | Safety guardrails | +| `handoffs` | array | No | [] | Other agents to hand off to | +| `memory` | object | No | - | Memory configuration | +| `context` | object | No | - | Context management settings | +| `metadata` | object | No | - | Custom metadata | + +### Model Options + +Supported models: + +**High Performance:** + +- `gpt-4o` (default, flagship model) +- `o1-preview` +- `o1` + +**Low Cost:** + +- `gpt-4o-mini` +- `gpt-3.5-turbo` +- `o1-mini` + +### Model Settings + +| Field | Type | Range | Description | +| ------------------- | ------ | ------- | -------------------------- | +| `temperature` | number | 0-1 | Sampling temperature | +| `top_p` | number | 0-1 | Nucleus sampling parameter | +| `max_tokens` | number | >0 | Maximum tokens in response | +| `presence_penalty` | number | -2 to 2 | Presence penalty | +| `frequency_penalty` | number | -2 to 2 | Frequency penalty | + +### Tools Configuration + +Tools can be specified as: + +1. **Built-in tool names** (strings): + - `file_operations` + - `git_tools` + - `web_search` + - `security_scanner` + +2. **MCP Server configuration** (object): + + ```json + { + "type": "mcp_server", + "name": "server-name", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem"], + "env": { "PATH": "/usr/bin" } + } + ``` + +3. **Custom function tool** (object): + ```json + { + "type": "custom_function", + "name": "my_tool", + "description": "Tool description", + "parameters": {}, + "handler": "./path/to/handler.js" + } + ``` + +### Guardrails + +Built-in guardrails: + +- `no_destructive_operations` - Prevent destructive file/git operations +- `require_approval` - Require human approval for actions +- `file_access_control` - Restrict file system access +- `rate_limiting` - Limit API call rate +- `content_filtering` - Filter sensitive content + +### Handoffs + +Handoffs enable agent-to-agent delegation. Specify as: + +- **Simple string**: Agent ID to hand off to +- **Handoff rule object**: + ```json + { + "target_agent": "agent_id", + "condition": "when to hand off", + "priority": 1 + } + ``` + +### Memory Configuration + +| Field | Type | Default | Description | +| ------------- | ------- | --------- | ------------------------------------------- | +| `enabled` | boolean | true | Enable conversation memory | +| `max_turns` | number | - | Maximum turns to remember | +| `max_tokens` | number | - | Maximum tokens to store | +| `persistence` | string | "session" | Persistence level: none, session, permanent | + +### Context Configuration + +| Field | Type | Default | Description | +| -------------------- | ------- | ------- | ------------------------------ | +| `max_context_length` | number | - | Maximum context length | +| `context_window` | number | - | Context window size | +| `summarization` | boolean | false | Enable automatic summarization | + +## Workflow Configuration + +The `workflow` object defines how agents are orchestrated. + +| Field | Type | Required | Default | Description | +| ------------- | ------ | -------- | ------------ | ---------------------- | +| `entry_point` | string | Yes | - | Agent ID to start with | +| `pattern` | string | No | "sequential" | Execution pattern | +| `output` | object | No | - | Output format settings | +| `limits` | object | No | - | Resource limits | + +### Workflow Patterns + +- `sequential` - Execute agents in sequence +- `handoff_chain` - Agents hand off to each other +- `parallel` - Execute agents in parallel +- `conditional` - Conditional agent execution + +### Output Format + +| Field | Type | Default | Description | +| ------------------ | ------- | ------- | ----------------------------------------------- | +| `format` | string | "json" | Output format: json, text, markdown, structured | +| `stream` | boolean | false | Enable streaming output | +| `include_metadata` | boolean | true | Include metadata in output | +| `include_trace` | boolean | false | Include execution trace | + +### Resource Limits + +| Field | Type | Default | Description | +| ----------------------- | ------ | ------- | --------------------- | +| `max_turns` | number | 10 | Maximum agent turns | +| `timeout` | number | 300 | Timeout in seconds | +| `max_tokens` | number | - | Maximum total tokens | +| `max_concurrent_agents` | number | - | Max concurrent agents | + +## Environment Variables + +Configuration files support environment variable substitution using the syntax: + +- `${VAR_NAME}` - Use environment variable +- `${VAR_NAME:-default}` - Use variable with default fallback + +Example: + +```json +{ + "agents": { + "assistant": { + "model": "${OPENAI_MODEL:-gpt-4o}", + "name": "Assistant" + } + } +} +``` + +## Configuration File Discovery + +Agents CLI automatically discovers configuration files in this order: + +1. `agents-cli.json` +2. `agents-cli.yaml` +3. `agents-cli.yml` +4. `agents.config.json` +5. `agents.config.yaml` +6. `agents.config.yml` +7. `.agents-cli.json` +8. `.agents-cli.yaml` + +## Examples + +See the [examples directory](../../examples/) for complete configuration examples: + +- [simple.json](../../examples/simple.json) - Minimal single-agent example +- [code-review.json](../../examples/code-review.json) - Multi-agent code review +- [architecture-review.json](../../examples/architecture-review.json) - Architecture analysis + +## JSON Schema + +The complete JSON Schema is available at +[schemas/configuration.schema.json](schemas/configuration.schema.json) for IDE integration and +validation. + +## Validation + +Validate your configuration using the CLI: + +```bash +# Basic validation +agents-cli validate config.json + +# Verbose output +agents-cli validate config.json --verbose + +# JSON output +agents-cli validate config.json --format json + +# Hide warnings +agents-cli validate config.json --no-warnings +``` + +## IDE Integration + +For IDE autocomplete and validation, configure your editor to use the JSON schema: + +### VS Code + +Add to `.vscode/settings.json`: + +```json +{ + "json.schemas": [ + { + "fileMatch": ["**/agents-cli.json", "**/agents.config.json"], + "url": "./docs/api/schemas/configuration.schema.json" + } + ] +} +``` + +### JetBrains IDEs + +Go to Settings → Languages & Frameworks → Schemas and DTDs → JSON Schema Mappings and add the schema +mapping. diff --git a/docs/api/schemas/configuration.schema.json b/docs/api/schemas/configuration.schema.json new file mode 100644 index 0000000..aa01107 --- /dev/null +++ b/docs/api/schemas/configuration.schema.json @@ -0,0 +1,381 @@ +{ + "$ref": "#/definitions/Configuration", + "definitions": { + "Configuration": { + "type": "object", + "properties": { + "metadata": { + "type": "object", + "properties": { + "version": { + "type": "string", + "default": "1.0" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "string" + }, + "created": { + "type": "string", + "format": "date-time" + }, + "updated": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false + }, + "agents": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "instructions": { + "type": "string", + "minLength": 1 + }, + "model": { + "type": "string", + "enum": ["gpt-4o", "o1-preview", "o1", "gpt-4o-mini", "gpt-3.5-turbo", "o1-mini"], + "default": "gpt-4o" + }, + "modelSettings": { + "type": "object", + "properties": { + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "max_tokens": { + "type": "integer", + "exclusiveMinimum": 0 + }, + "presence_penalty": { + "type": "number", + "minimum": -2, + "maximum": 2 + }, + "frequency_penalty": { + "type": "number", + "minimum": -2, + "maximum": 2 + } + }, + "additionalProperties": false + }, + "tools": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string", + "enum": ["file_operations", "git_tools", "web_search", "security_scanner"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "mcp_server" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "command": { + "type": "string", + "minLength": 1 + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["type", "name", "command"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "custom_function" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 1 + }, + "parameters": { + "type": "object", + "additionalProperties": {} + }, + "handler": { + "type": "string" + } + }, + "required": ["type", "name", "description"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "permissions": { + "type": "string", + "enum": ["read_only", "read_write", "restricted", "full_access"] + } + }, + "required": ["name"], + "additionalProperties": false + } + ] + }, + "default": [] + }, + "guardrails": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string", + "enum": [ + "no_destructive_operations", + "require_approval", + "file_access_control", + "rate_limiting", + "content_filtering" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "no_destructive_operations", + "require_approval", + "file_access_control", + "rate_limiting", + "content_filtering" + ] + }, + "config": { + "type": "object", + "additionalProperties": {} + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "no_destructive_operations", + "require_approval", + "file_access_control", + "rate_limiting", + "content_filtering" + ] + }, + "config": { + "type": "object", + "additionalProperties": {} + } + }, + "required": ["type"], + "additionalProperties": false + } + ] + }, + "default": [] + }, + "handoffs": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "target_agent": { + "type": "string", + "minLength": 1 + }, + "condition": { + "type": "string" + }, + "priority": { + "type": "integer", + "minimum": 0 + } + }, + "required": ["target_agent"], + "additionalProperties": false + } + ] + }, + "default": [] + }, + "memory": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "max_turns": { + "type": "integer", + "exclusiveMinimum": 0 + }, + "max_tokens": { + "type": "integer", + "exclusiveMinimum": 0 + }, + "persistence": { + "type": "string", + "enum": ["none", "session", "permanent"], + "default": "session" + } + }, + "additionalProperties": false + }, + "context": { + "type": "object", + "properties": { + "max_context_length": { + "type": "integer", + "exclusiveMinimum": 0 + }, + "context_window": { + "type": "integer", + "exclusiveMinimum": 0 + }, + "summarization": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "metadata": { + "type": "object", + "additionalProperties": {} + } + }, + "required": ["name", "instructions"], + "additionalProperties": false + } + }, + "workflow": { + "type": "object", + "properties": { + "entry_point": { + "type": "string", + "minLength": 1 + }, + "pattern": { + "type": "string", + "enum": ["sequential", "handoff_chain", "parallel", "conditional"], + "default": "sequential" + }, + "output": { + "type": "object", + "properties": { + "format": { + "type": "string", + "enum": ["json", "text", "markdown", "structured"], + "default": "json" + }, + "stream": { + "type": "boolean", + "default": false + }, + "include_metadata": { + "type": "boolean", + "default": true + }, + "include_trace": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "limits": { + "type": "object", + "properties": { + "max_turns": { + "type": "integer", + "exclusiveMinimum": 0, + "default": 10 + }, + "timeout": { + "type": "integer", + "exclusiveMinimum": 0, + "default": 300 + }, + "max_tokens": { + "type": "integer", + "exclusiveMinimum": 0 + }, + "max_concurrent_agents": { + "type": "integer", + "exclusiveMinimum": 0 + } + }, + "additionalProperties": false + }, + "max_turns": { + "type": "integer", + "exclusiveMinimum": 0 + }, + "timeout": { + "type": "integer", + "exclusiveMinimum": 0 + } + }, + "required": ["entry_point"], + "additionalProperties": false + } + }, + "required": ["agents", "workflow"], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} diff --git a/docs/implementation/phase1.2-summary.md b/docs/implementation/phase1.2-summary.md new file mode 100644 index 0000000..9e897e0 --- /dev/null +++ b/docs/implementation/phase1.2-summary.md @@ -0,0 +1,91 @@ +# Phase 1.2 Implementation Summary + +**Schema Design & Validation System** + +## Overview + +This document summarizes the complete implementation of Phase 1.2 (Days 3-5) of the Agents CLI +project, which establishes the configuration system foundation for all agent workflows. + +## Completed Deliverables + +### 2.1 Core Configuration Schema ✅ + +#### 2.1.1 AgentConfig Schema + +- **File**: `src/config/schema.ts` +- **Features**: + - Comprehensive agent configuration with Zod validation + - Support for 6 current OpenAI models (3 high-performance: gpt-4o, o1-preview, o1; 3 low-cost: + gpt-4o-mini, gpt-3.5-turbo, o1-mini) + - Model settings (temperature max 1.0, top_p, max_tokens, penalties) + - Tool configuration (built-in, MCP servers, custom functions) + - Guardrails and safety controls + - Handoff rules with conditions and priorities + - Memory and context management + - Custom metadata support + +#### 2.1.2 WorkflowSettings Schema + +- **Features**: + - Entry point agent configuration + - 4 workflow patterns: sequential, handoff_chain, parallel, conditional + - Resource limits: max_turns, timeout, max_tokens, max_concurrent_agents + - Output format preferences: json, text, markdown, structured + - Streaming control and metadata inclusion + - Trace logging options + +#### 2.1.3 ToolConfig Schema + +- **Built-in Tools**: file_operations, git_tools, web_search, security_scanner +- **MCP Servers**: Full configuration with command, args, environment variables +- **Custom Functions**: Name, description, parameters, handler path +- **Permissions**: read_only, read_write, restricted, full_access + +### 2.2 Configuration Loading & Validation ✅ + +#### 2.2.1 ConfigLoader Class + +- **File**: `src/config/loader.ts` +- **Features**: + - JSON and YAML file format support with automatic detection + - Environment variable substitution: `${VAR_NAME:-default}` + - Configuration file discovery (8 common filename patterns) + - Include/import support for modular configurations + - Deep merge algorithm for configuration composition + - Configurable validation and default merging + +#### 2.2.2 ConfigValidator Class + +- **File**: `src/config/validator.ts` +- **Features**: + - Detailed Zod schema validation with error paths + - User-friendly error messages with suggestions + - Four types of warnings: security, performance, deprecation, best-practice + - Semantic validation (circular handoffs, missing agents, etc.) + +#### 2.2.4 Validate CLI Command + +- **File**: `src/cli/commands/validate.ts` +- **Usage**: `agents-cli validate ` with --verbose, --format json, --no-warnings + +### 2.3 Example Configurations ✅ + +- Code review workflow (3 agents, handoff chain) +- Architecture review workflow (single agent, comprehensive tools) +- Simple examples (JSON and YAML with env vars) +- Auto-generated JSON schema and documentation + +## Testing + +- **40 passing tests** across 5 test suites +- ConfigLoader: 15 tests +- ConfigValidator: 17 tests +- Full lint and build compliance + +## Success Metrics + +✅ All Definition of Done criteria met ✅ 40/40 tests passing ✅ 100% build success ✅ 0 linting +errors ✅ All examples validate successfully + +**Implementation complete and ready for Phase 1.3!** diff --git a/examples/architecture-review.json b/examples/architecture-review.json new file mode 100644 index 0000000..14e8428 --- /dev/null +++ b/examples/architecture-review.json @@ -0,0 +1,46 @@ +{ + "metadata": { + "name": "Architecture Review Workflow", + "description": "Single-agent architecture analysis with comprehensive tooling", + "version": "1.0", + "author": "Agents CLI" + }, + "agents": { + "architect": { + "name": "System Architect", + "instructions": "You are an expert system architect. Analyze the codebase architecture and provide comprehensive feedback on:\n\n**System Structure:**\n- Overall architecture patterns (MVC, microservices, event-driven, etc.)\n- Module organization and boundaries\n- Dependency management and coupling\n- Layer separation (presentation, business logic, data access)\n\n**Quality Assessment:**\n- Code organization and maintainability\n- Scalability considerations\n- Performance bottlenecks\n- Technical debt identification\n\n**Best Practices:**\n- Design patterns usage\n- SOLID principles adherence\n- Error handling strategies\n- Testing coverage and quality\n\n**Recommendations:**\n- Architecture improvements\n- Refactoring opportunities\n- Technology modernization\n- Documentation needs\n\nProvide structured output with:\n1. Executive summary\n2. Detailed findings by category\n3. Prioritized recommendations\n4. Implementation roadmap", + "model": "gpt-4o", + "modelSettings": { + "temperature": 0.3, + "max_tokens": 8000 + }, + "tools": ["file_operations", "git_tools", "web_search"], + "guardrails": ["no_destructive_operations", "file_access_control", "rate_limiting"], + "handoffs": [], + "memory": { + "enabled": true, + "max_turns": 50, + "persistence": "session" + }, + "context": { + "max_context_length": 32000, + "summarization": true + } + } + }, + "workflow": { + "entry_point": "architect", + "pattern": "sequential", + "limits": { + "max_turns": 20, + "timeout": 900, + "max_tokens": 10000 + }, + "output": { + "format": "structured", + "stream": false, + "include_metadata": true, + "include_trace": false + } + } +} diff --git a/examples/code-review.json b/examples/code-review.json index 005c4e6..00ee5fa 100644 --- a/examples/code-review.json +++ b/examples/code-review.json @@ -1,26 +1,84 @@ { + "metadata": { + "name": "Code Review Workflow", + "description": "Multi-agent code review workflow with security analysis and architectural feedback", + "version": "1.0", + "author": "Agents CLI" + }, "agents": { "code_reviewer": { "name": "Senior Code Reviewer", - "instructions": "Review code for bugs, performance issues, and best practices. Focus on code quality, maintainability, and security.", + "instructions": "You are an experienced code reviewer. Analyze code for:\n- Bugs and logical errors\n- Performance issues and optimization opportunities\n- Code quality and maintainability\n- Best practices and design patterns\n- Documentation and code comments\n\nProvide constructive feedback with specific examples. If you identify security concerns, hand off to the security reviewer. For architectural issues, hand off to the architect.", "model": "gpt-4o", + "modelSettings": { + "temperature": 0.3, + "max_tokens": 4000 + }, "tools": ["file_operations", "git_tools"], - "guardrails": ["no_destructive_operations"], - "handoffs": ["security_reviewer"] + "guardrails": ["no_destructive_operations", "file_access_control"], + "handoffs": [ + { + "target_agent": "security_reviewer", + "condition": "security concerns identified", + "priority": 1 + }, + { + "target_agent": "architect", + "condition": "architectural improvements needed", + "priority": 2 + } + ], + "memory": { + "enabled": true, + "max_turns": 20 + } }, "security_reviewer": { "name": "Security Expert", - "instructions": "Focus specifically on security vulnerabilities, authentication issues, and security best practices.", + "instructions": "You are a security expert specializing in code security. Focus on:\n- SQL injection, XSS, and other injection vulnerabilities\n- Authentication and authorization flaws\n- Cryptography and data protection issues\n- Dependency vulnerabilities\n- API security concerns\n- Secrets and credentials management\n\nProvide specific remediation guidance. Hand back to code reviewer when security analysis is complete.", "model": "gpt-4o", + "modelSettings": { + "temperature": 0.2, + "max_tokens": 3000 + }, "tools": ["security_scanner", "file_operations"], - "guardrails": ["no_destructive_operations"], - "handoffs": ["code_reviewer"] + "guardrails": ["no_destructive_operations", "file_access_control"], + "handoffs": ["code_reviewer"], + "memory": { + "enabled": true, + "max_turns": 15 + } + }, + "architect": { + "name": "Software Architect", + "instructions": "You are a software architect focused on system design. Evaluate:\n- Architectural patterns and their appropriateness\n- Component separation and modularity\n- Scalability and performance considerations\n- Technology stack choices\n- Integration patterns\n- Technical debt\n\nProvide high-level architectural recommendations. Hand back to code reviewer when architectural review is complete.", + "model": "gpt-4o", + "modelSettings": { + "temperature": 0.4, + "max_tokens": 3000 + }, + "tools": ["file_operations"], + "guardrails": ["no_destructive_operations", "file_access_control"], + "handoffs": ["code_reviewer"], + "memory": { + "enabled": true, + "max_turns": 10 + } } }, "workflow": { "entry_point": "code_reviewer", "pattern": "handoff_chain", - "max_turns": 15, - "timeout": 300 + "limits": { + "max_turns": 30, + "timeout": 600, + "max_concurrent_agents": 1 + }, + "output": { + "format": "structured", + "stream": true, + "include_metadata": true, + "include_trace": false + } } } diff --git a/examples/content-pm.json b/examples/content-pm.json new file mode 100644 index 0000000..e562b02 --- /dev/null +++ b/examples/content-pm.json @@ -0,0 +1,46 @@ +{ + "metadata": { + "name": "Content Production Product Manager Workflow", + "description": "AI assistant for content production product management in EdTech", + "version": "1.0", + "author": "Agents CLI" + }, + "agents": { + "content_pm": { + "name": "Content Production Product Manager", + "instructions": "You are an experienced Product Manager specializing in educational content production. Your role is to:\n\n**Content Strategy & Planning:**\n- Define content requirements and learning objectives\n- Prioritize content creation based on curriculum needs and user impact\n- Create content roadmaps aligned with educational goals\n- Analyze content performance metrics and user engagement\n\n**Stakeholder Management:**\n- Collaborate with educators, subject matter experts, and content creators\n- Communicate content specifications and quality standards\n- Gather feedback from learners and teachers\n- Align content strategy with business objectives\n\n**Quality & Standards:**\n- Define content quality frameworks and assessment criteria\n- Ensure pedagogical soundness and educational best practices\n- Review content for accuracy, clarity, and engagement\n- Maintain consistency across content formats (videos, exercises, articles)\n\n**Data-Driven Decisions:**\n- Analyze learner engagement and completion metrics\n- Identify content gaps and improvement opportunities\n- A/B test different content approaches\n- Report on content ROI and learning outcomes\n\n**Process Optimization:**\n- Streamline content production workflows\n- Implement efficient review and approval processes\n- Manage content versioning and updates\n- Coordinate with technical teams on content delivery\n\nProvide actionable insights, data-backed recommendations, and clear documentation. Focus on maximizing educational impact while maintaining production efficiency.", + "model": "gpt-4o", + "modelSettings": { + "temperature": 0.5, + "max_tokens": 4000 + }, + "tools": ["file_operations", "web_search"], + "guardrails": ["no_destructive_operations", "file_access_control"], + "handoffs": [], + "memory": { + "enabled": true, + "max_turns": 30, + "persistence": "session" + }, + "context": { + "max_context_length": 16000, + "summarization": true + } + } + }, + "workflow": { + "entry_point": "content_pm", + "pattern": "sequential", + "limits": { + "max_turns": 20, + "timeout": 600, + "max_tokens": 8000 + }, + "output": { + "format": "structured", + "stream": false, + "include_metadata": true, + "include_trace": false + } + } +} diff --git a/examples/marketing-lead.json b/examples/marketing-lead.json new file mode 100644 index 0000000..47ef350 --- /dev/null +++ b/examples/marketing-lead.json @@ -0,0 +1,46 @@ +{ + "metadata": { + "name": "EdTech Marketing Team Lead Workflow", + "description": "AI assistant for marketing leadership in educational technology", + "version": "1.0", + "author": "Agents CLI" + }, + "agents": { + "marketing_lead": { + "name": "EdTech Marketing Team Lead", + "instructions": "You are a strategic Marketing Team Lead for an educational technology company. Your responsibilities include:\n\n**Marketing Strategy:**\n- Develop integrated marketing campaigns for EdTech products\n- Define target audience segments (students, teachers, parents, institutions)\n- Create go-to-market strategies for new educational features\n- Position products against competitors in the education space\n- Align marketing initiatives with educational mission and values\n\n**Campaign Management:**\n- Plan and execute multi-channel marketing campaigns\n- Coordinate digital marketing (SEO, SEM, social media, email)\n- Develop content marketing strategies for educational audiences\n- Manage marketing budgets and optimize ROI\n- Track campaign performance and adjust strategies\n\n**Team Leadership:**\n- Guide marketing team members and coordinate efforts\n- Mentor junior marketers and content creators\n- Foster collaboration between marketing, product, and sales\n- Build team capabilities in EdTech marketing\n- Manage external agencies and partners\n\n**Data & Analytics:**\n- Analyze user acquisition and conversion metrics\n- Track customer journey from awareness to retention\n- Monitor brand sentiment and market positioning\n- A/B test messaging and creative approaches\n- Report marketing KPIs to leadership\n\n**Educational Sector Expertise:**\n- Understand educational trends and learning methodologies\n- Communicate educational value propositions effectively\n- Build relationships with educational influencers\n- Navigate B2B and B2C educational markets\n- Address parent and educator concerns authentically\n\n**Content & Brand:**\n- Ensure brand consistency across all touchpoints\n- Develop compelling educational narratives\n- Create case studies and success stories\n- Produce thought leadership content\n- Manage social proof and testimonials\n\nProvide strategic recommendations, campaign plans, and performance analyses. Balance growth objectives with educational integrity and trust-building.", + "model": "gpt-4o", + "modelSettings": { + "temperature": 0.6, + "max_tokens": 4000 + }, + "tools": ["file_operations", "web_search"], + "guardrails": ["no_destructive_operations", "file_access_control", "content_filtering"], + "handoffs": [], + "memory": { + "enabled": true, + "max_turns": 25, + "persistence": "session" + }, + "context": { + "max_context_length": 20000, + "summarization": true + } + } + }, + "workflow": { + "entry_point": "marketing_lead", + "pattern": "sequential", + "limits": { + "max_turns": 15, + "timeout": 600, + "max_tokens": 8000 + }, + "output": { + "format": "structured", + "stream": false, + "include_metadata": true, + "include_trace": false + } + } +} diff --git a/examples/simple.json b/examples/simple.json index 9d47bb6..3f25dee 100644 --- a/examples/simple.json +++ b/examples/simple.json @@ -1,18 +1,35 @@ { + "metadata": { + "name": "Simple Assistant Example", + "description": "Minimal single-agent configuration demonstrating basic setup", + "version": "1.0", + "author": "Agents CLI" + }, "agents": { "simple_assistant": { "name": "Simple Assistant", - "instructions": "You are a helpful assistant. Answer questions directly and concisely.", + "instructions": "You are a helpful assistant. Answer questions directly and concisely. Be friendly and professional.", "model": "gpt-4o", + "modelSettings": { + "temperature": 0.7, + "max_tokens": 1000 + }, "tools": [], - "guardrails": [], + "guardrails": ["no_destructive_operations"], "handoffs": [] } }, "workflow": { "entry_point": "simple_assistant", "pattern": "sequential", - "max_turns": 5, - "timeout": 60 + "limits": { + "max_turns": 5, + "timeout": 60 + }, + "output": { + "format": "json", + "stream": false, + "include_metadata": true + } } } diff --git a/examples/simple.yaml b/examples/simple.yaml new file mode 100644 index 0000000..90c1652 --- /dev/null +++ b/examples/simple.yaml @@ -0,0 +1,35 @@ +# Simple Assistant Configuration (YAML format) +# This demonstrates YAML configuration support with environment variable substitution + +metadata: + name: Simple Assistant Example (YAML) + description: Minimal single-agent configuration in YAML format + version: "1.0" + author: Agents CLI + +agents: + simple_assistant: + name: Simple Assistant + instructions: | + You are a helpful assistant. Answer questions directly and concisely. + Be friendly and professional in your responses. + # Using environment variable with default fallback + model: ${OPENAI_MODEL:-gpt-4o} + modelSettings: + temperature: 0.7 + max_tokens: 1000 + tools: [] + guardrails: + - no_destructive_operations + handoffs: [] + +workflow: + entry_point: simple_assistant + pattern: sequential + limits: + max_turns: 5 + timeout: 60 + output: + format: json + stream: false + include_metadata: true diff --git a/jest.config.js b/jest.config.js index 27b66bb..f4651e4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -34,7 +34,8 @@ module.exports = { }, moduleNameMapper: { '^@/(.*)$': '/src/$1', - '^chalk$': '/tests/mocks/chalk.js' + '^chalk$': '/tests/mocks/chalk.js', + '^(\\.{1,2}/.*)\\.js$': '$1' }, setupFilesAfterEnv: ['/tests/setup.ts'], testTimeout: 10000, diff --git a/package-lock.json b/package-lock.json index a01d937..1e34a0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,13 +13,15 @@ "chalk": "^5.6.2", "commander": "^14.0.1", "dotenv": "^17.2.2", + "js-yaml": "^4.1.0", "zod": "^3.25.76" }, "bin": { - "agents-cli": "dist/cli/index.js" + "agents-cli": "dist/cli/cli.js" }, "devDependencies": { "@types/jest": "^30.0.0", + "@types/js-yaml": "^4.0.9", "@types/node": "^24.5.2", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", @@ -33,7 +35,8 @@ "ts-jest": "^29.4.3", "tslib": "^2.8.1", "tsx": "^4.20.5", - "typescript": "^5.9.2" + "typescript": "^5.9.2", + "zod-to-json-schema": "^3.24.6" }, "engines": { "node": ">=18.0.0", @@ -2096,6 +2099,13 @@ "pretty-format": "^30.0.0" } }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2759,7 +2769,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-union": { @@ -5837,7 +5846,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -8494,8 +8502,8 @@ "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==", + "devOptional": true, "license": "ISC", - "optional": true, "peerDependencies": { "zod": "^3.24.1" } diff --git a/package.json b/package.json index d3b861e..627ee89 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "type-check": "tsc --noEmit", "clean": "rm -rf dist", "prepublishOnly": "npm run clean && npm run build", - "prepare": "husky" + "prepare": "husky", + "generate-schema-docs": "tsx scripts/generate-schema-docs.ts" }, "keywords": [ "agents", @@ -56,6 +57,7 @@ }, "devDependencies": { "@types/jest": "^30.0.0", + "@types/js-yaml": "^4.0.9", "@types/node": "^24.5.2", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", @@ -69,7 +71,8 @@ "ts-jest": "^29.4.3", "tslib": "^2.8.1", "tsx": "^4.20.5", - "typescript": "^5.9.2" + "typescript": "^5.9.2", + "zod-to-json-schema": "^3.24.6" }, "volta": { "node": "20.19.5", @@ -89,6 +92,7 @@ "chalk": "^5.6.2", "commander": "^14.0.1", "dotenv": "^17.2.2", + "js-yaml": "^4.1.0", "zod": "^3.25.76" } } diff --git a/scripts/dev-test.js b/scripts/dev-test.js deleted file mode 100755 index 8969854..0000000 --- a/scripts/dev-test.js +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env node - -/** - * Development utility to test CLI functionality - */ - -import { execSync } from 'child_process'; -import chalk from 'chalk'; - -console.log(chalk.blue('🔧 CLI Development Test')); - -try { - // Test build - console.log(chalk.yellow('\n📦 Building project...')); - execSync('npm run build', { stdio: 'inherit' }); - - // Test CLI commands - console.log(chalk.yellow('\n🤖 Testing CLI commands...')); - - console.log('\n1. Testing help command:'); - execSync('node dist/cli/cli.js --help', { stdio: 'inherit' }); - - console.log('\n2. Testing validate command:'); - execSync('node dist/cli/cli.js validate examples/simple.json', { stdio: 'inherit' }); - - console.log('\n3. Testing run command:'); - execSync('node dist/cli/cli.js run --config examples/simple.json --input "Hello world"', { stdio: 'inherit' }); - - console.log(chalk.green('\n✅ CLI development test completed successfully!')); -} catch (error) { - console.error(chalk.red('\n❌ CLI development test failed:'), error.message); - process.exit(1); -} \ No newline at end of file diff --git a/scripts/generate-schema-docs.ts b/scripts/generate-schema-docs.ts new file mode 100644 index 0000000..ff4d354 --- /dev/null +++ b/scripts/generate-schema-docs.ts @@ -0,0 +1,318 @@ +/** + * Generate schema documentation from Zod schemas + * This creates JSON Schema and Markdown documentation + */ + +import { writeFileSync, mkdirSync } from 'fs'; +import { join } from 'path'; +import { zodToJsonSchema } from 'zod-to-json-schema'; +import { ConfigurationSchema } from '../src/config/schema'; + +const docsDir = join(__dirname, '..', 'docs', 'api'); +const schemasDir = join(docsDir, 'schemas'); + +// Ensure directories exist +try { + mkdirSync(docsDir, { recursive: true }); + mkdirSync(schemasDir, { recursive: true }); +} catch (e) { + // Directories already exist +} + +// Generate JSON Schema +const jsonSchema = zodToJsonSchema(ConfigurationSchema, { + name: 'Configuration', + $refStrategy: 'none', +}); + +// Write JSON Schema +writeFileSync( + join(schemasDir, 'configuration.schema.json'), + JSON.stringify(jsonSchema, null, 2) +); + +console.log( + '✅ Generated JSON Schema: docs/api/schemas/configuration.schema.json' +); + +// Generate Markdown documentation +const markdown = `# Configuration Schema Reference + +This document provides a comprehensive reference for the Agents CLI configuration schema. + +## Overview + +Agents CLI uses a JSON or YAML configuration file to define agents, workflows, and their behavior. The configuration is validated using Zod schemas to ensure correctness before execution. + +## Configuration File Structure + +\`\`\`typescript +{ + "metadata": { + "name": "string", + "description": "string", + "version": "string", + "author": "string" + }, + "agents": { + "[agent_id]": { + // Agent configuration + } + }, + "workflow": { + // Workflow configuration + } +} +\`\`\` + +## Metadata (Optional) + +Configuration metadata for documentation and versioning. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| \`name\` | string | No | Configuration name | +| \`description\` | string | No | Configuration description | +| \`version\` | string | No | Configuration version (default: "1.0") | +| \`author\` | string | No | Configuration author | +| \`created\` | string | No | Creation timestamp (ISO 8601) | +| \`updated\` | string | No | Last update timestamp (ISO 8601) | + +## Agents Configuration + +The \`agents\` object contains one or more agent configurations, keyed by agent ID. + +### Agent Configuration + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| \`name\` | string | Yes | - | Human-readable agent name | +| \`instructions\` | string | Yes | - | Agent's system instructions/prompt | +| \`model\` | string | No | "gpt-4o" | OpenAI model to use | +| \`modelSettings\` | object | No | - | Model fine-tuning parameters | +| \`tools\` | array | No | [] | Tools available to the agent | +| \`guardrails\` | array | No | [] | Safety guardrails | +| \`handoffs\` | array | No | [] | Other agents to hand off to | +| \`memory\` | object | No | - | Memory configuration | +| \`context\` | object | No | - | Context management settings | +| \`metadata\` | object | No | - | Custom metadata | + +### Model Options + +Supported models: + +**High Performance:** +- \`gpt-4o\` (default, flagship model) +- \`o1-preview\` +- \`o1\` + +**Low Cost:** +- \`gpt-4o-mini\` +- \`gpt-3.5-turbo\` +- \`o1-mini\` + +### Model Settings + +| Field | Type | Range | Description | +|-------|------|-------|-------------| +| \`temperature\` | number | 0-1 | Sampling temperature | +| \`top_p\` | number | 0-1 | Nucleus sampling parameter | +| \`max_tokens\` | number | >0 | Maximum tokens in response | +| \`presence_penalty\` | number | -2 to 2 | Presence penalty | +| \`frequency_penalty\` | number | -2 to 2 | Frequency penalty | + +### Tools Configuration + +Tools can be specified as: +1. **Built-in tool names** (strings): + - \`file_operations\` + - \`git_tools\` + - \`web_search\` + - \`security_scanner\` + +2. **MCP Server configuration** (object): + \`\`\`json + { + "type": "mcp_server", + "name": "server-name", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem"], + "env": { "PATH": "/usr/bin" } + } + \`\`\` + +3. **Custom function tool** (object): + \`\`\`json + { + "type": "custom_function", + "name": "my_tool", + "description": "Tool description", + "parameters": {}, + "handler": "./path/to/handler.js" + } + \`\`\` + +### Guardrails + +Built-in guardrails: +- \`no_destructive_operations\` - Prevent destructive file/git operations +- \`require_approval\` - Require human approval for actions +- \`file_access_control\` - Restrict file system access +- \`rate_limiting\` - Limit API call rate +- \`content_filtering\` - Filter sensitive content + +### Handoffs + +Handoffs enable agent-to-agent delegation. Specify as: +- **Simple string**: Agent ID to hand off to +- **Handoff rule object**: + \`\`\`json + { + "target_agent": "agent_id", + "condition": "when to hand off", + "priority": 1 + } + \`\`\` + +### Memory Configuration + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| \`enabled\` | boolean | true | Enable conversation memory | +| \`max_turns\` | number | - | Maximum turns to remember | +| \`max_tokens\` | number | - | Maximum tokens to store | +| \`persistence\` | string | "session" | Persistence level: none, session, permanent | + +### Context Configuration + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| \`max_context_length\` | number | - | Maximum context length | +| \`context_window\` | number | - | Context window size | +| \`summarization\` | boolean | false | Enable automatic summarization | + +## Workflow Configuration + +The \`workflow\` object defines how agents are orchestrated. + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| \`entry_point\` | string | Yes | - | Agent ID to start with | +| \`pattern\` | string | No | "sequential" | Execution pattern | +| \`output\` | object | No | - | Output format settings | +| \`limits\` | object | No | - | Resource limits | + +### Workflow Patterns + +- \`sequential\` - Execute agents in sequence +- \`handoff_chain\` - Agents hand off to each other +- \`parallel\` - Execute agents in parallel +- \`conditional\` - Conditional agent execution + +### Output Format + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| \`format\` | string | "json" | Output format: json, text, markdown, structured | +| \`stream\` | boolean | false | Enable streaming output | +| \`include_metadata\` | boolean | true | Include metadata in output | +| \`include_trace\` | boolean | false | Include execution trace | + +### Resource Limits + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| \`max_turns\` | number | 10 | Maximum agent turns | +| \`timeout\` | number | 300 | Timeout in seconds | +| \`max_tokens\` | number | - | Maximum total tokens | +| \`max_concurrent_agents\` | number | - | Max concurrent agents | + +## Environment Variables + +Configuration files support environment variable substitution using the syntax: +- \`\${VAR_NAME}\` - Use environment variable +- \`\${VAR_NAME:-default}\` - Use variable with default fallback + +Example: +\`\`\`json +{ + "agents": { + "assistant": { + "model": "\${OPENAI_MODEL:-gpt-4o}", + "name": "Assistant" + } + } +} +\`\`\` + +## Configuration File Discovery + +Agents CLI automatically discovers configuration files in this order: +1. \`agents-cli.json\` +2. \`agents-cli.yaml\` +3. \`agents-cli.yml\` +4. \`agents.config.json\` +5. \`agents.config.yaml\` +6. \`agents.config.yml\` +7. \`.agents-cli.json\` +8. \`.agents-cli.yaml\` + +## Examples + +See the [examples directory](../../examples/) for complete configuration examples: +- [simple.json](../../examples/simple.json) - Minimal single-agent example +- [code-review.json](../../examples/code-review.json) - Multi-agent code review +- [architecture-review.json](../../examples/architecture-review.json) - Architecture analysis + +## JSON Schema + +The complete JSON Schema is available at [schemas/configuration.schema.json](schemas/configuration.schema.json) for IDE integration and validation. + +## Validation + +Validate your configuration using the CLI: + +\`\`\`bash +# Basic validation +agents-cli validate config.json + +# Verbose output +agents-cli validate config.json --verbose + +# JSON output +agents-cli validate config.json --format json + +# Hide warnings +agents-cli validate config.json --no-warnings +\`\`\` + +## IDE Integration + +For IDE autocomplete and validation, configure your editor to use the JSON schema: + +### VS Code + +Add to \`.vscode/settings.json\`: +\`\`\`json +{ + "json.schemas": [ + { + "fileMatch": ["**/agents-cli.json", "**/agents.config.json"], + "url": "./docs/api/schemas/configuration.schema.json" + } + ] +} +\`\`\` + +### JetBrains IDEs + +Go to Settings → Languages & Frameworks → Schemas and DTDs → JSON Schema Mappings and add the schema mapping. +`; + +// Write Markdown documentation +writeFileSync(join(docsDir, 'configuration-schema.md'), markdown); + +console.log( + '✅ Generated Markdown documentation: docs/api/configuration-schema.md' +); +console.log('\n✨ Schema documentation generation complete!'); diff --git a/src/cli/cli.ts b/src/cli/cli.ts index af0a24d..5ec6f64 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -30,11 +30,24 @@ program .command('validate') .description('Validate a configuration file') .argument('', 'Configuration file to validate') - .action((config) => { - console.log(chalk.blue('✅ Agents CLI - Validate command')); - console.log('Config file:', config); - console.log(chalk.yellow('⚠️ Implementation coming in Phase 1.2+')); - }); + .option('-f, --format ', 'Output format (text|json)', 'text') + .option('--no-warnings', 'Hide validation warnings') + .option('-v, --verbose', 'Verbose output with detailed information') + .option('--dry-run', 'Dry-run mode (same as normal validation)') + .action( + async ( + config: string, + options: { + format?: 'text' | 'json'; + warnings?: boolean; + verbose?: boolean; + dryRun?: boolean; + } + ) => { + const { validateCommand } = await import('./commands/validate.js'); + await validateCommand(config, options); + } + ); program .command('serve') diff --git a/src/cli/commands/index.ts b/src/cli/commands/index.ts index d3823c8..d678472 100644 --- a/src/cli/commands/index.ts +++ b/src/cli/commands/index.ts @@ -2,9 +2,4 @@ * CLI commands exports */ -// Command implementations will be added in Phase 1.2+ -export const commands = { - run: () => Promise.resolve('Run command placeholder'), - validate: () => Promise.resolve('Validate command placeholder'), - serve: () => Promise.resolve('Serve command placeholder'), -}; +export * from './validate.js'; diff --git a/src/cli/commands/validate.ts b/src/cli/commands/validate.ts new file mode 100644 index 0000000..4577ce4 --- /dev/null +++ b/src/cli/commands/validate.ts @@ -0,0 +1,156 @@ +/** + * Validate command implementation + * Phase 1.2 - Schema Design & Validation + */ + +import chalk from 'chalk'; +import { ConfigLoader } from '../../config/loader.js'; +import { ConfigValidator } from '../../config/validator.js'; +import { ConfigurationError } from '../../utils/errors.js'; + +export interface ValidateOptions { + /** + * Output format for validation results + */ + format?: 'text' | 'json'; + + /** + * Show warnings in addition to errors + */ + warnings?: boolean; + + /** + * Dry-run mode (same as normal validation) + */ + dryRun?: boolean; + + /** + * Verbose output with detailed information + */ + verbose?: boolean; +} + +/** + * Validate a configuration file + * + * @param configPath - Path to configuration file + * @param options - Validation options + */ +export async function validateCommand( + configPath: string, + options: ValidateOptions = {} +): Promise { + const { format = 'text', warnings = true, verbose = false } = options; + + try { + if (verbose) { + console.log(chalk.blue('📋 Validating configuration file...')); + console.log(chalk.gray(` File: ${configPath}\n`)); + } + + // Load configuration (this validates it) + const config = await ConfigLoader.load(configPath, { + validate: false, // We'll validate manually for detailed results + }); + + // Perform detailed validation + const result = ConfigValidator.validateDetailed(config); + + // Output results based on format + if (format === 'json') { + console.log(JSON.stringify(result, null, 2)); + } else { + // Text format + if (result.valid) { + console.log(chalk.green('✅ Configuration is valid!')); + + if (verbose) { + console.log(chalk.gray('\nConfiguration summary:')); + const agentCount = Object.keys(config.agents).length; + console.log(chalk.gray(` • Agents: ${agentCount}`)); + console.log( + chalk.gray(` • Entry point: ${config.workflow.entry_point}`) + ); + console.log(chalk.gray(` • Pattern: ${config.workflow.pattern}`)); + + // Show tool usage + const allTools = new Set(); + for (const agent of Object.values(config.agents)) { + if (agent.tools) { + for (const tool of agent.tools) { + const toolName = typeof tool === 'string' ? tool : tool.name; + allTools.add(toolName); + } + } + } + if (allTools.size > 0) { + console.log( + chalk.gray(` • Tools: ${Array.from(allTools).join(', ')}`) + ); + } + } + + // Show warnings if present + if (warnings && result.warnings.length > 0) { + console.log( + chalk.yellow('\n' + ConfigValidator.formatWarnings(result.warnings)) + ); + } + + process.exit(0); + } else { + console.log(chalk.red('❌ Configuration validation failed!\n')); + + // Show errors + for (const error of result.errors) { + console.log( + chalk.red(` • ${error.path || 'root'}: ${error.message}`) + ); + if (error.suggestion) { + console.log(chalk.yellow(` → Suggestion: ${error.suggestion}`)); + } + } + + // Show warnings if present + if (warnings && result.warnings.length > 0) { + console.log( + chalk.yellow('\n' + ConfigValidator.formatWarnings(result.warnings)) + ); + } + + process.exit(1); + } + } + } catch (error) { + if (format === 'json') { + console.log( + JSON.stringify( + { + valid: false, + errors: [ + { + path: '', + message: error instanceof Error ? error.message : String(error), + }, + ], + warnings: [], + }, + null, + 2 + ) + ); + } else { + console.log(chalk.red('❌ Validation failed!\n')); + + if (error instanceof ConfigurationError) { + console.log(chalk.red(error.message)); + } else { + console.log( + chalk.red(error instanceof Error ? error.message : String(error)) + ); + } + } + + process.exit(1); + } +} diff --git a/src/config/loader.ts b/src/config/loader.ts index 3147266..04cd224 100644 --- a/src/config/loader.ts +++ b/src/config/loader.ts @@ -1,13 +1,378 @@ /** - * Configuration loader - * Implementation planned for Phase 1.2 + * Configuration loader with JSON/YAML support + * Phase 1.2 - Schema Design & Validation */ -import type { Configuration } from './schema.js'; +import { readFileSync, existsSync } from 'fs'; +import { resolve, dirname, extname, join } from 'path'; +import * as yaml from 'js-yaml'; +import { ConfigurationSchema, type Configuration } from './schema.js'; +import { ConfigurationError } from '../utils/errors.js'; +/** + * Default configuration values + */ +const DEFAULT_CONFIG: Partial = { + workflow: { + entry_point: '', + pattern: 'sequential', + limits: { + max_turns: 10, + timeout: 300, + }, + }, +}; + +/** + * Configuration loader options + */ +export interface LoaderOptions { + /** + * Enable environment variable substitution + */ + substituteEnv?: boolean; + + /** + * Validate configuration after loading + */ + validate?: boolean; + + /** + * Merge with default configuration + */ + mergeDefaults?: boolean; + + /** + * Base directory for resolving relative imports + */ + baseDir?: string; +} + +/** + * ConfigLoader handles loading, parsing, and validation of configuration files + */ export class ConfigLoader { - // eslint-disable-next-line no-unused-vars - static async load(_path: string): Promise { - throw new Error('Configuration loading not yet implemented - Phase 1.2'); + /** + * Load configuration from a file + * Supports JSON and YAML formats with automatic detection + * + * @param path - Path to configuration file + * @param options - Loader options + * @returns Parsed and validated configuration + */ + static async load( + path: string, + options: LoaderOptions = {} + ): Promise { + const { + substituteEnv = true, + validate = true, + mergeDefaults = true, + baseDir, + } = options; + + // Resolve absolute path + const absolutePath = resolve(path); + + // Check if file exists + if (!existsSync(absolutePath)) { + throw new ConfigurationError( + `Configuration file not found: ${absolutePath}` + ); + } + + try { + // Read file content + const content = readFileSync(absolutePath, 'utf-8'); + + // Parse based on file extension + let config = this.parseContent(content, absolutePath); + + // Handle includes/imports + if (config['include'] || config['import']) { + const includeBase = baseDir || dirname(absolutePath); + config = await this.resolveIncludes(config, includeBase); + } + + // Substitute environment variables + if (substituteEnv) { + config = this.substituteEnvironmentVariables(config) as Record< + string, + unknown + >; + } + + // Merge with defaults + if (mergeDefaults) { + config = this.mergeWithDefaults(config); + } + + // Validate configuration + if (validate) { + return this.validateConfiguration(config); + } + + return config as Configuration; + } catch (error) { + if (error instanceof ConfigurationError) { + throw error; + } + throw new ConfigurationError( + `Failed to load configuration from ${absolutePath}: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + } + + /** + * Load configuration from string content + * + * @param content - Configuration content + * @param format - Format (json or yaml) + * @param options - Loader options + * @returns Parsed and validated configuration + */ + static async loadFromString( + content: string, + format: 'json' | 'yaml' = 'json', + options: LoaderOptions = {} + ): Promise { + const { + substituteEnv = true, + validate = true, + mergeDefaults = true, + } = options; + + try { + // Parse content + let config = + format === 'yaml' + ? (yaml.load(content, { schema: yaml.JSON_SCHEMA }) as Record< + string, + unknown + >) + : JSON.parse(content); + + // Substitute environment variables + if (substituteEnv) { + config = this.substituteEnvironmentVariables(config); + } + + // Merge with defaults + if (mergeDefaults) { + config = this.mergeWithDefaults(config); + } + + // Validate configuration + if (validate) { + return this.validateConfiguration(config); + } + + return config as Configuration; + } catch (error) { + throw new ConfigurationError( + `Failed to parse configuration: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + } + + /** + * Parse configuration content based on file extension + */ + private static parseContent( + content: string, + filePath: string + ): Record { + const ext = extname(filePath).toLowerCase(); + + try { + if (ext === '.json' || ext === '.jsonc') { + return JSON.parse(content); + } else if (ext === '.yaml' || ext === '.yml') { + return yaml.load(content, { schema: yaml.JSON_SCHEMA }) as Record< + string, + unknown + >; + } else { + // Try JSON first, then YAML + try { + return JSON.parse(content); + } catch { + return yaml.load(content, { schema: yaml.JSON_SCHEMA }) as Record< + string, + unknown + >; + } + } + } catch (error) { + throw new ConfigurationError( + `Failed to parse configuration file ${filePath}: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + } + + /** + * Resolve includes and imports in configuration + */ + private static async resolveIncludes( + config: Record, + baseDir: string + ): Promise> { + const includes = + (config['include'] as string[]) || (config['import'] as string[]); + if (!includes || !Array.isArray(includes)) { + return config; + } + + // Remove include/import fields + // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars + const { include, import: _, ...initialConfig } = config; + let baseConfig = initialConfig; + + // Load and merge included configurations + for (const includePath of includes) { + const absoluteIncludePath = resolve(baseDir, includePath); + const includedConfig = await this.load(absoluteIncludePath, { + validate: false, + mergeDefaults: false, + baseDir, + }); + + // Deep merge included configuration + baseConfig = this.deepMerge(baseConfig, includedConfig); + } + + return baseConfig; + } + + /** + * Substitute environment variables in configuration + * Supports ${VAR_NAME} and ${VAR_NAME:-default} syntax + */ + private static substituteEnvironmentVariables(obj: unknown): unknown { + if (typeof obj === 'string') { + // Match ${VAR_NAME} or ${VAR_NAME:-default} + return obj.replace( + /\$\{([A-Z_][A-Z0-9_]*)(:-([^}]+))?\}/gi, + (_, varName, __, defaultValue) => { + const value = process.env[varName]; + if (value !== undefined) { + return value; + } + if (defaultValue !== undefined) { + return defaultValue; + } + return `\${${varName}}`; // Keep original if not found + } + ); + } + + if (Array.isArray(obj)) { + return obj.map((item) => this.substituteEnvironmentVariables(item)); + } + + if (obj !== null && typeof obj === 'object') { + const result: Record = {}; + for (const [key, value] of Object.entries(obj)) { + result[key] = this.substituteEnvironmentVariables(value); + } + return result; + } + + return obj; + } + + /** + * Merge user configuration with defaults + */ + private static mergeWithDefaults( + config: Record + ): Record { + return this.deepMerge(DEFAULT_CONFIG, config); + } + + /** + * Deep merge two objects + */ + private static deepMerge( + target: unknown, + source: unknown + ): Record { + if (!source || typeof source !== 'object') { + return target as Record; + } + + if (!target || typeof target !== 'object') { + return source as Record; + } + + if (Array.isArray(source)) { + return source as unknown as Record; + } + + const result: Record = { ...target }; + + for (const [key, value] of Object.entries(source)) { + if ( + value !== null && + typeof value === 'object' && + !Array.isArray(value) + ) { + result[key] = this.deepMerge(result[key], value); + } else { + result[key] = value; + } + } + + return result; + } + + /** + * Validate configuration against schema + */ + private static validateConfiguration(config: unknown): Configuration { + try { + return ConfigurationSchema.parse(config); + } catch (error) { + throw new ConfigurationError( + `Configuration validation failed: ${ + error instanceof Error ? error.message : String(error) + }`, + error instanceof Error ? error : undefined + ); + } + } + + /** + * Discover configuration files in a directory + * Searches for common configuration file names + */ + static discover(baseDir: string = process.cwd()): string[] { + const configFiles = [ + 'agents-cli.json', + 'agents-cli.yaml', + 'agents-cli.yml', + 'agents.config.json', + 'agents.config.yaml', + 'agents.config.yml', + '.agents-cli.json', + '.agents-cli.yaml', + ]; + + const found: string[] = []; + + for (const fileName of configFiles) { + const filePath = join(baseDir, fileName); + if (existsSync(filePath)) { + found.push(filePath); + } + } + + return found; } } diff --git a/src/config/schema.ts b/src/config/schema.ts index 49e1617..df7ce89 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -1,25 +1,297 @@ /** - * Configuration schema types - * Implementation planned for Phase 1.2 (Schema Design & Validation) - */ - -export interface AgentConfig { - name: string; - instructions: string; - model?: string; - tools?: string[]; - guardrails?: string[]; - handoffs?: string[]; -} - -export interface WorkflowConfig { - entry_point: string; - pattern: 'handoff_chain' | 'parallel' | 'sequential'; - max_turns?: number; - timeout?: number; -} - -export interface Configuration { - agents: Record; - workflow: WorkflowConfig; -} + * Configuration schema with Zod validation + * Phase 1.2 - Schema Design & Validation + */ + +import { z } from 'zod'; + +// ============================================================================ +// Tool Configuration Schemas +// ============================================================================ + +/** + * Built-in tool types + */ +export const BuiltInToolSchema = z.enum([ + 'file_operations', + 'git_tools', + 'web_search', + 'security_scanner', +]); + +/** + * MCP Server configuration + */ +export const MCPServerConfigSchema = z.object({ + type: z.literal('mcp_server'), + name: z.string().min(1, 'MCP server name is required'), + command: z.string().min(1, 'MCP server command is required'), + args: z.array(z.string()).optional(), + env: z.record(z.string()).optional(), +}); + +/** + * Custom function tool configuration + */ +export const CustomToolConfigSchema = z.object({ + type: z.literal('custom_function'), + name: z.string().min(1, 'Custom tool name is required'), + description: z.string().min(1, 'Tool description is required'), + parameters: z.record(z.any()).optional(), + handler: z.string().optional(), // Path to handler module +}); + +/** + * Tool permission levels + */ +export const ToolPermissionSchema = z.enum([ + 'read_only', + 'read_write', + 'restricted', + 'full_access', +]); + +/** + * Tool configuration with permissions + */ +export const ToolConfigSchema = z.union([ + BuiltInToolSchema, + MCPServerConfigSchema, + CustomToolConfigSchema, + z.object({ + name: z.string(), + permissions: ToolPermissionSchema.optional(), + }), +]); + +// ============================================================================ +// Guardrail Configuration Schemas +// ============================================================================ + +/** + * Built-in guardrail types + */ +export const GuardrailTypeSchema = z.enum([ + 'no_destructive_operations', + 'require_approval', + 'file_access_control', + 'rate_limiting', + 'content_filtering', +]); + +/** + * Input guardrail configuration + */ +export const InputGuardrailSchema = z.object({ + type: GuardrailTypeSchema, + config: z.record(z.any()).optional(), +}); + +/** + * Output guardrail configuration + */ +export const OutputGuardrailSchema = z.object({ + type: GuardrailTypeSchema, + config: z.record(z.any()).optional(), +}); + +/** + * Combined guardrail configuration + */ +export const GuardrailConfigSchema = z.union([ + GuardrailTypeSchema, + InputGuardrailSchema, + OutputGuardrailSchema, +]); + +// ============================================================================ +// Memory & Context Configuration +// ============================================================================ + +/** + * Memory configuration for agents + */ +export const MemoryConfigSchema = z.object({ + enabled: z.boolean().default(true), + max_turns: z.number().int().positive().optional(), + max_tokens: z.number().int().positive().optional(), + persistence: z.enum(['none', 'session', 'permanent']).default('session'), +}); + +/** + * Context management settings + */ +export const ContextConfigSchema = z.object({ + max_context_length: z.number().int().positive().optional(), + context_window: z.number().int().positive().optional(), + summarization: z.boolean().default(false), +}); + +// ============================================================================ +// Agent Configuration Schema +// ============================================================================ + +/** + * Model selection options + * High performance: gpt-4o, o1-preview, o1 + * Low cost: gpt-4o-mini, gpt-3.5-turbo, o1-mini + */ +export const ModelSchema = z + .enum([ + 'gpt-4o', + 'o1-preview', + 'o1', + 'gpt-4o-mini', + 'gpt-3.5-turbo', + 'o1-mini', + ]) + .default('gpt-4o'); + +/** + * Model settings for fine-tuning behavior + */ +export const ModelSettingsSchema = z.object({ + temperature: z.number().min(0).max(1).optional(), + top_p: z.number().min(0).max(1).optional(), + max_tokens: z.number().int().positive().optional(), + presence_penalty: z.number().min(-2).max(2).optional(), + frequency_penalty: z.number().min(-2).max(2).optional(), +}); + +/** + * Handoff routing rules + */ +export const HandoffRuleSchema = z.object({ + target_agent: z.string().min(1, 'Target agent name is required'), + condition: z.string().optional(), + priority: z.number().int().min(0).optional(), +}); + +/** + * Complete agent configuration + */ +export const AgentConfigSchema = z.object({ + name: z.string().min(1, 'Agent name is required'), + instructions: z.string().min(1, 'Agent instructions are required'), + model: ModelSchema.optional(), + modelSettings: ModelSettingsSchema.optional(), + tools: z.array(ToolConfigSchema).optional().default([]), + guardrails: z.array(GuardrailConfigSchema).optional().default([]), + handoffs: z + .array(z.union([z.string(), HandoffRuleSchema])) + .optional() + .default([]), + memory: MemoryConfigSchema.optional(), + context: ContextConfigSchema.optional(), + metadata: z.record(z.any()).optional(), +}); + +// ============================================================================ +// Workflow Configuration Schema +// ============================================================================ + +/** + * Workflow execution patterns + */ +export const WorkflowPatternSchema = z.enum([ + 'sequential', + 'handoff_chain', + 'parallel', + 'conditional', +]); + +/** + * Output format preferences + */ +export const OutputFormatSchema = z.object({ + format: z.enum(['json', 'text', 'markdown', 'structured']).default('json'), + stream: z.boolean().default(false), + include_metadata: z.boolean().default(true), + include_trace: z.boolean().default(false), +}); + +/** + * Resource limits for workflow execution + */ +export const ResourceLimitsSchema = z.object({ + max_turns: z.number().int().positive().default(10), + timeout: z.number().int().positive().default(300), // seconds + max_tokens: z.number().int().positive().optional(), + max_concurrent_agents: z.number().int().positive().optional(), +}); + +/** + * Workflow settings configuration + */ +export const WorkflowSettingsSchema = z.object({ + entry_point: z.string().min(1, 'Entry point agent is required'), + pattern: WorkflowPatternSchema.default('sequential'), + output: OutputFormatSchema.optional(), + limits: ResourceLimitsSchema.optional(), + // Legacy fields for backwards compatibility + max_turns: z.number().int().positive().optional(), + timeout: z.number().int().positive().optional(), +}); + +// ============================================================================ +// Top-Level Configuration Schema +// ============================================================================ + +/** + * Configuration metadata + */ +export const ConfigMetadataSchema = z.object({ + version: z.string().default('1.0'), + name: z.string().optional(), + description: z.string().optional(), + author: z.string().optional(), + created: z.string().datetime().optional(), + updated: z.string().datetime().optional(), +}); + +/** + * Complete configuration schema + */ +export const ConfigurationSchema = z.object({ + metadata: ConfigMetadataSchema.optional(), + agents: z + .record(z.string(), AgentConfigSchema) + .refine((agents) => Object.keys(agents).length > 0, { + message: 'At least one agent must be defined', + }), + workflow: WorkflowSettingsSchema, +}); + +// ============================================================================ +// Type Exports (inferred from Zod schemas) +// ============================================================================ + +export type BuiltInTool = z.infer; +export type MCPServerConfig = z.infer; +export type CustomToolConfig = z.infer; +export type ToolConfig = z.infer; +export type ToolPermission = z.infer; + +export type GuardrailType = z.infer; +export type InputGuardrail = z.infer; +export type OutputGuardrail = z.infer; +export type GuardrailConfig = z.infer; + +export type MemoryConfig = z.infer; +export type ContextConfig = z.infer; + +export type Model = z.infer; +export type ModelSettings = z.infer; +export type HandoffRule = z.infer; +export type AgentConfig = z.infer; + +export type WorkflowPattern = z.infer; +export type OutputFormat = z.infer; +export type ResourceLimits = z.infer; +export type WorkflowSettings = z.infer; + +export type ConfigMetadata = z.infer; +export type Configuration = z.infer; + +// Legacy type aliases for backwards compatibility +export type WorkflowConfig = WorkflowSettings; diff --git a/src/config/validator.ts b/src/config/validator.ts index e9d12f6..f692353 100644 --- a/src/config/validator.ts +++ b/src/config/validator.ts @@ -1,13 +1,361 @@ /** - * Configuration validator - * Implementation planned for Phase 1.2 + * Configuration validator with detailed error reporting + * Phase 1.2 - Schema Design & Validation */ -import type { Configuration } from './schema.js'; +import { ZodError } from 'zod'; +import { ConfigurationSchema, type Configuration } from './schema.js'; +import { ConfigurationError } from '../utils/errors.js'; +/** + * Validation result + */ +export interface ValidationResult { + valid: boolean; + errors: ValidationError[]; + warnings: ValidationWarning[]; +} + +/** + * Validation error with details + */ +export interface ValidationError { + path: string; + message: string; + code?: string; + suggestion?: string; +} + +/** + * Validation warning + */ +export interface ValidationWarning { + path: string; + message: string; + type: 'security' | 'performance' | 'deprecation' | 'best-practice'; +} + +/** + * ConfigValidator provides detailed validation with user-friendly errors + */ export class ConfigValidator { - // eslint-disable-next-line no-unused-vars - static validate(_config: Configuration): boolean { - throw new Error('Configuration validation not yet implemented - Phase 1.2'); + /** + * Validate configuration and return detailed results + * + * @param config - Configuration to validate + * @returns Validation result with errors and warnings + */ + static validateDetailed(config: unknown): ValidationResult { + const errors: ValidationError[] = []; + const warnings: ValidationWarning[] = []; + + try { + // Schema validation + const parsed = ConfigurationSchema.parse(config); + + // Additional semantic validation + this.validateSemantics(parsed, warnings); + + // Security warnings + this.checkSecurityWarnings(parsed, warnings); + + return { + valid: true, + errors, + warnings, + }; + } catch (error) { + if (error instanceof ZodError) { + // Convert Zod errors to user-friendly format + for (const issue of error.issues) { + const suggestion = this.getSuggestion(issue); + errors.push({ + path: issue.path.join('.'), + message: issue.message, + code: issue.code, + ...(suggestion ? { suggestion } : {}), + }); + } + } else { + errors.push({ + path: '', + message: + error instanceof Error ? error.message : 'Unknown validation error', + }); + } + + return { + valid: false, + errors, + warnings, + }; + } + } + + /** + * Validate configuration (throws on error) + * + * @param config - Configuration to validate + * @returns true if valid + * @throws ConfigurationError if invalid + */ + static validate(config: unknown): boolean { + const result = this.validateDetailed(config); + + if (!result.valid) { + const errorMessage = this.formatErrors(result.errors); + throw new ConfigurationError(errorMessage); + } + + return true; + } + + /** + * Validate semantics and relationships in configuration + */ + private static validateSemantics( + config: Configuration, + warnings: ValidationWarning[] + ): void { + // Check if entry point agent exists + if (!config.agents[config.workflow.entry_point]) { + throw new ConfigurationError( + `Entry point agent "${config.workflow.entry_point}" not found in agents configuration` + ); + } + + // Validate handoff targets + for (const [agentId, agentConfig] of Object.entries(config.agents)) { + if (agentConfig.handoffs && agentConfig.handoffs.length > 0) { + for (const handoff of agentConfig.handoffs) { + const targetAgent = + typeof handoff === 'string' ? handoff : handoff.target_agent; + + if (!config.agents[targetAgent]) { + throw new ConfigurationError( + `Agent "${agentId}" references non-existent handoff target "${targetAgent}"` + ); + } + } + } + + // Warn about circular handoffs + const visited = new Set(); + if (this.hasCircularHandoff(agentId, config.agents, visited)) { + warnings.push({ + path: `agents.${agentId}.handoffs`, + message: `Potential circular handoff detected for agent "${agentId}"`, + type: 'best-practice', + }); + } + + // Warn about agents with no tools + if (!agentConfig.tools || agentConfig.tools.length === 0) { + warnings.push({ + path: `agents.${agentId}.tools`, + message: `Agent "${agentId}" has no tools configured. Consider adding tools for better functionality.`, + type: 'best-practice', + }); + } + } + + // Warn about high timeout values + const timeout = config.workflow.limits?.timeout || config.workflow.timeout; + if (timeout && timeout > 600) { + warnings.push({ + path: 'workflow.timeout', + message: `Workflow timeout of ${timeout}s is very high. Consider reducing for better responsiveness.`, + type: 'performance', + }); + } + + // Warn about high max_turns + const maxTurns = + config.workflow.limits?.max_turns || config.workflow.max_turns || 10; + if (maxTurns > 50) { + warnings.push({ + path: 'workflow.max_turns', + message: `max_turns of ${maxTurns} is very high and may impact performance.`, + type: 'performance', + }); + } + } + + /** + * Check for circular handoffs + */ + private static hasCircularHandoff( + agentId: string, + agents: Record, + visited: Set, + path: Set = new Set() + ): boolean { + if (path.has(agentId)) { + return true; // Circular reference found + } + + if (visited.has(agentId)) { + return false; // Already checked this path + } + + visited.add(agentId); + path.add(agentId); + + const agent = agents[agentId]; + if (agent && agent.handoffs) { + for (const handoff of agent.handoffs) { + const targetAgent = + typeof handoff === 'string' ? handoff : handoff.target_agent; + + if (this.hasCircularHandoff(targetAgent, agents, visited, path)) { + return true; + } + } + } + + path.delete(agentId); + return false; + } + + /** + * Check for security-related warnings + */ + private static checkSecurityWarnings( + config: Configuration, + warnings: ValidationWarning[] + ): void { + for (const [agentId, agentConfig] of Object.entries(config.agents)) { + // Warn about missing guardrails + if (!agentConfig.guardrails || agentConfig.guardrails.length === 0) { + warnings.push({ + path: `agents.${agentId}.guardrails`, + message: `Agent "${agentId}" has no guardrails configured. Consider adding guardrails for safety.`, + type: 'security', + }); + } + + // Warn about potentially dangerous tools without restrictions + if (agentConfig.tools) { + for (const tool of agentConfig.tools) { + const toolName = typeof tool === 'string' ? tool : tool.name; + + if ( + (toolName === 'file_operations' || toolName === 'git_tools') && + (!agentConfig.guardrails || + !agentConfig.guardrails.some((g) => + typeof g === 'string' + ? g === 'no_destructive_operations' + : g.type === 'no_destructive_operations' + )) + ) { + warnings.push({ + path: `agents.${agentId}.tools`, + message: `Agent "${agentId}" uses "${toolName}" without "no_destructive_operations" guardrail.`, + type: 'security', + }); + } + } + } + } + } + + /** + * Get suggestion for common validation errors + */ + private static getSuggestion( + issue: ZodError['issues'][0] + ): string | undefined { + const path = issue.path.join('.'); + + // Common suggestions based on error type + if (issue.code === 'invalid_type') { + if (path.includes('model')) { + return 'Use one of: gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-4, gpt-3.5-turbo, o1, o1-mini'; + } + if (path.includes('pattern')) { + return 'Use one of: sequential, handoff_chain, parallel, conditional'; + } + } + + if (issue.code === 'too_small' && path.includes('agents')) { + return 'Define at least one agent in the "agents" object'; + } + + if (issue.message.includes('required')) { + return `Make sure to provide a value for "${path}"`; + } + + return undefined; + } + + /** + * Format validation errors into readable message + */ + private static formatErrors(errors: ValidationError[]): string { + const lines = ['Configuration validation failed:\n']; + + for (const error of errors) { + lines.push(` • ${error.path || 'root'}: ${error.message}`); + if (error.suggestion) { + lines.push(` → Suggestion: ${error.suggestion}`); + } + } + + return lines.join('\n'); + } + + /** + * Format validation warnings into readable message + */ + static formatWarnings(warnings: ValidationWarning[]): string { + if (warnings.length === 0) { + return ''; + } + + const lines = ['Configuration warnings:\n']; + + const grouped = this.groupWarnings(warnings); + + for (const [type, typeWarnings] of Object.entries(grouped)) { + const icon = this.getWarningIcon(type as ValidationWarning['type']); + lines.push(`\n${icon} ${type.toUpperCase()}:`); + + for (const warning of typeWarnings) { + lines.push(` • ${warning.path || 'root'}: ${warning.message}`); + } + } + + return lines.join('\n'); + } + + /** + * Group warnings by type + */ + private static groupWarnings( + warnings: ValidationWarning[] + ): Record { + const grouped: Record = {}; + + for (const warning of warnings) { + if (!grouped[warning.type]) { + grouped[warning.type] = []; + } + grouped[warning.type]!.push(warning); + } + + return grouped; + } + + /** + * Get icon for warning type + */ + private static getWarningIcon(type: ValidationWarning['type']): string { + const icons = { + security: '🔒', + performance: '⚡', + deprecation: '⚠️', + 'best-practice': '💡', + }; + return icons[type] || '⚠️'; } } diff --git a/src/utils/errors.ts b/src/utils/errors.ts index e729275..9577325 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -10,9 +10,14 @@ export class AgentsCliError extends Error { } export class ConfigurationError extends AgentsCliError { - constructor(message: string) { + override cause?: Error; + + constructor(message: string, cause?: Error) { super(message); this.name = 'ConfigurationError'; + if (cause) { + this.cause = cause; + } } } diff --git a/tests/unit/config-loader.test.ts b/tests/unit/config-loader.test.ts new file mode 100644 index 0000000..884daa9 --- /dev/null +++ b/tests/unit/config-loader.test.ts @@ -0,0 +1,232 @@ +/** + * Configuration loader tests + */ + +import { ConfigLoader } from '@/config/loader'; +import { ConfigurationError } from '@/utils/errors'; +import { writeFileSync, unlinkSync } from 'fs'; +import { join } from 'path'; + +describe('ConfigLoader', () => { + const testDir = '/tmp/config-loader-tests'; + const testJsonPath = join(testDir, 'test.json'); + const testYamlPath = join(testDir, 'test.yaml'); + + beforeAll(() => { + // Create test directory + try { + require('fs').mkdirSync(testDir, { recursive: true }); + } catch (e) { + // Directory already exists + } + }); + + afterEach(() => { + // Clean up test files + try { + unlinkSync(testJsonPath); + } catch (e) { + // File doesn't exist + } + try { + unlinkSync(testYamlPath); + } catch (e) { + // File doesn't exist + } + }); + + describe('load', () => { + it('should load valid JSON configuration', async () => { + const config = { + agents: { + test_agent: { + name: 'Test Agent', + instructions: 'Test instructions', + }, + }, + workflow: { + entry_point: 'test_agent', + pattern: 'sequential', + }, + }; + + writeFileSync(testJsonPath, JSON.stringify(config)); + + const loaded = await ConfigLoader.load(testJsonPath); + expect(loaded.agents['test_agent']?.name).toBe('Test Agent'); + expect(loaded.workflow.entry_point).toBe('test_agent'); + }); + + it('should load valid YAML configuration', async () => { + const yamlContent = ` +agents: + test_agent: + name: Test Agent + instructions: Test instructions +workflow: + entry_point: test_agent + pattern: sequential +`; + + writeFileSync(testYamlPath, yamlContent); + + const loaded = await ConfigLoader.load(testYamlPath); + expect(loaded.agents['test_agent']?.name).toBe('Test Agent'); + expect(loaded.workflow.entry_point).toBe('test_agent'); + }); + + it('should throw error for non-existent file', async () => { + await expect( + ConfigLoader.load('/tmp/non-existent-file.json') + ).rejects.toThrow(ConfigurationError); + }); + + it('should substitute environment variables', async () => { + process.env['TEST_MODEL'] = 'gpt-4o-mini'; + + const config = { + agents: { + test_agent: { + name: 'Test Agent', + instructions: 'Test instructions', + model: '${TEST_MODEL}', + }, + }, + workflow: { + entry_point: 'test_agent', + }, + }; + + writeFileSync(testJsonPath, JSON.stringify(config)); + + const loaded = await ConfigLoader.load(testJsonPath); + expect(loaded.agents['test_agent']?.model).toBe('gpt-4o-mini'); + + delete process.env['TEST_MODEL']; + }); + + it('should use default value for missing environment variables', async () => { + const config = { + agents: { + test_agent: { + name: 'Test Agent', + instructions: 'Test instructions', + model: '${MISSING_VAR:-gpt-4o}', + }, + }, + workflow: { + entry_point: 'test_agent', + }, + }; + + writeFileSync(testJsonPath, JSON.stringify(config)); + + const loaded = await ConfigLoader.load(testJsonPath); + expect(loaded.agents['test_agent']?.model).toBe('gpt-4o'); + }); + + it('should merge with defaults', async () => { + const config = { + agents: { + test_agent: { + name: 'Test Agent', + instructions: 'Test instructions', + }, + }, + workflow: { + entry_point: 'test_agent', + }, + }; + + writeFileSync(testJsonPath, JSON.stringify(config)); + + const loaded = await ConfigLoader.load(testJsonPath, { + mergeDefaults: true, + }); + + expect(loaded.workflow.pattern).toBe('sequential'); + }); + + it('should skip validation when requested', async () => { + const invalidConfig = { + agents: {}, + workflow: { + entry_point: 'test_agent', + }, + }; + + writeFileSync(testJsonPath, JSON.stringify(invalidConfig)); + + await expect( + ConfigLoader.load(testJsonPath, { validate: false }) + ).resolves.toBeDefined(); + }); + }); + + describe('loadFromString', () => { + it('should load JSON from string', async () => { + const content = JSON.stringify({ + agents: { + test_agent: { + name: 'Test Agent', + instructions: 'Test instructions', + }, + }, + workflow: { + entry_point: 'test_agent', + }, + }); + + const loaded = await ConfigLoader.loadFromString(content, 'json'); + expect(loaded.agents['test_agent']?.name).toBe('Test Agent'); + }); + + it('should load YAML from string', async () => { + const content = ` +agents: + test_agent: + name: Test Agent + instructions: Test instructions +workflow: + entry_point: test_agent +`; + + const loaded = await ConfigLoader.loadFromString(content, 'yaml'); + expect(loaded.agents['test_agent']?.name).toBe('Test Agent'); + }); + + it('should throw error for invalid JSON', async () => { + const content = '{ invalid json }'; + + await expect( + ConfigLoader.loadFromString(content, 'json') + ).rejects.toThrow(ConfigurationError); + }); + }); + + describe('discover', () => { + it('should discover configuration files', () => { + const configPath = join(testDir, 'agents-cli.json'); + writeFileSync( + configPath, + JSON.stringify({ + agents: { + test: { name: 'Test', instructions: 'Test' }, + }, + workflow: { entry_point: 'test' }, + }) + ); + + const found = ConfigLoader.discover(testDir); + expect(found.length).toBeGreaterThan(0); + expect(found[0]).toContain('agents-cli.json'); + + unlinkSync(configPath); + }); + + it('should return empty array when no config files found', () => { + const found = ConfigLoader.discover('/tmp/non-existent-directory'); + expect(found).toEqual([]); + }); + }); +}); diff --git a/tests/unit/config-validator.test.ts b/tests/unit/config-validator.test.ts new file mode 100644 index 0000000..db51cff --- /dev/null +++ b/tests/unit/config-validator.test.ts @@ -0,0 +1,266 @@ +/** + * Configuration validator tests + */ + +import { ConfigValidator } from '@/config/validator'; +import { ConfigurationError } from '@/utils/errors'; +import type { Configuration } from '@/config/schema'; + +describe('ConfigValidator', () => { + const validConfig: Configuration = { + agents: { + test_agent: { + name: 'Test Agent', + instructions: 'Test instructions', + model: 'gpt-4o', + tools: ['file_operations'], + guardrails: ['no_destructive_operations'], + handoffs: [], + }, + }, + workflow: { + entry_point: 'test_agent', + pattern: 'sequential', + }, + }; + + describe('validateDetailed', () => { + it('should validate correct configuration', () => { + const result = ConfigValidator.validateDetailed(validConfig); + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it('should detect missing agents', () => { + const invalidConfig = { + agents: {}, + workflow: { + entry_point: 'test_agent', + }, + }; + + const result = ConfigValidator.validateDetailed(invalidConfig); + expect(result.valid).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + }); + + it('should detect missing entry point', () => { + const invalidConfig = { + agents: { + test_agent: { + name: 'Test Agent', + instructions: 'Test instructions', + }, + }, + workflow: { + entry_point: '', + }, + }; + + const result = ConfigValidator.validateDetailed(invalidConfig); + expect(result.valid).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + }); + + it('should detect invalid model', () => { + const invalidConfig = { + agents: { + test_agent: { + name: 'Test Agent', + instructions: 'Test instructions', + model: 'invalid-model', + }, + }, + workflow: { + entry_point: 'test_agent', + }, + }; + + const result = ConfigValidator.validateDetailed(invalidConfig); + expect(result.valid).toBe(false); + expect(result.errors.some((e) => e.path.includes('model'))).toBe(true); + }); + + it('should warn about missing tools', () => { + const configWithNoTools = { + agents: { + test_agent: { + name: 'Test Agent', + instructions: 'Test instructions', + guardrails: ['no_destructive_operations'], + }, + }, + workflow: { + entry_point: 'test_agent', + }, + }; + + const result = ConfigValidator.validateDetailed(configWithNoTools); + expect(result.valid).toBe(true); + expect( + result.warnings.some( + (w) => w.path.includes('tools') && w.type === 'best-practice' + ) + ).toBe(true); + }); + + it('should warn about missing guardrails', () => { + const configWithNoGuardrails = { + agents: { + test_agent: { + name: 'Test Agent', + instructions: 'Test instructions', + tools: ['file_operations'], + }, + }, + workflow: { + entry_point: 'test_agent', + }, + }; + + const result = ConfigValidator.validateDetailed(configWithNoGuardrails); + expect(result.valid).toBe(true); + expect( + result.warnings.some( + (w) => w.path.includes('guardrails') && w.type === 'security' + ) + ).toBe(true); + }); + + it('should detect non-existent handoff target', () => { + const invalidConfig = { + agents: { + test_agent: { + name: 'Test Agent', + instructions: 'Test instructions', + handoffs: ['non_existent_agent'], + }, + }, + workflow: { + entry_point: 'test_agent', + }, + }; + + const result = ConfigValidator.validateDetailed(invalidConfig); + expect(result.valid).toBe(false); + }); + + it('should detect non-existent entry point agent', () => { + const invalidConfig = { + agents: { + test_agent: { + name: 'Test Agent', + instructions: 'Test instructions', + }, + }, + workflow: { + entry_point: 'missing_agent', + }, + }; + + const result = ConfigValidator.validateDetailed(invalidConfig); + expect(result.valid).toBe(false); + }); + + it('should warn about high timeout values', () => { + const configWithHighTimeout = { + agents: { + test_agent: { + name: 'Test Agent', + instructions: 'Test instructions', + guardrails: ['no_destructive_operations'], + }, + }, + workflow: { + entry_point: 'test_agent', + limits: { + timeout: 700, + max_turns: 10, + }, + }, + }; + + const result = ConfigValidator.validateDetailed(configWithHighTimeout); + expect(result.valid).toBe(true); + expect( + result.warnings.some( + (w) => w.path.includes('timeout') && w.type === 'performance' + ) + ).toBe(true); + }); + + it('should warn about high max_turns', () => { + const configWithHighMaxTurns = { + agents: { + test_agent: { + name: 'Test Agent', + instructions: 'Test instructions', + guardrails: ['no_destructive_operations'], + }, + }, + workflow: { + entry_point: 'test_agent', + limits: { + max_turns: 100, + timeout: 300, + }, + }, + }; + + const result = ConfigValidator.validateDetailed(configWithHighMaxTurns); + expect(result.valid).toBe(true); + expect( + result.warnings.some( + (w) => w.path.includes('max_turns') && w.type === 'performance' + ) + ).toBe(true); + }); + }); + + describe('validate', () => { + it('should return true for valid configuration', () => { + expect(ConfigValidator.validate(validConfig)).toBe(true); + }); + + it('should throw error for invalid configuration', () => { + const invalidConfig = { + agents: {}, + workflow: { + entry_point: 'test_agent', + }, + }; + + expect(() => ConfigValidator.validate(invalidConfig)).toThrow( + ConfigurationError + ); + }); + }); + + describe('formatWarnings', () => { + it('should format warnings correctly', () => { + const warnings = [ + { + path: 'agents.test.tools', + message: 'Missing tools', + type: 'best-practice' as const, + }, + { + path: 'agents.test.guardrails', + message: 'Missing guardrails', + type: 'security' as const, + }, + ]; + + const formatted = ConfigValidator.formatWarnings(warnings); + expect(formatted).toContain('BEST-PRACTICE'); + expect(formatted).toContain('SECURITY'); + expect(formatted).toContain('Missing tools'); + expect(formatted).toContain('Missing guardrails'); + }); + + it('should return empty string for no warnings', () => { + const formatted = ConfigValidator.formatWarnings([]); + expect(formatted).toBe(''); + }); + }); +}); diff --git a/tests/unit/config.test.ts b/tests/unit/config.test.ts index 92ff994..6ee3e87 100644 --- a/tests/unit/config.test.ts +++ b/tests/unit/config.test.ts @@ -41,6 +41,9 @@ describe('Configuration Schema', () => { test_agent: { name: 'Test Agent', instructions: 'Test instructions', + tools: [], + guardrails: [], + handoffs: [], }, }, workflow: { diff --git a/tsconfig.json b/tsconfig.json index ca1e7e9..94cdc0b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -59,5 +59,5 @@ } }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"] + "exclude": ["node_modules", "dist", "tests", "scripts", "**/*.test.ts", "**/*.spec.ts"] }