Add new swarm mode
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
version: 1
|
||||
swarm:
|
||||
name: "Collaborative Chat Network"
|
||||
main: "project_manager"
|
||||
instances:
|
||||
project_manager:
|
||||
description: "Project manager coordinating team discussions and decisions"
|
||||
directory: "."
|
||||
model: "opus"
|
||||
connections: ["tech_lead", "designer", "qa_lead"]
|
||||
expose_as_mcp: true
|
||||
mcp_port: 7000
|
||||
connect_to_agents: ["tech_lead", "designer", "qa_lead"]
|
||||
vibe: true
|
||||
prompt: |
|
||||
# Project Manager Agent
|
||||
|
||||
You are the project manager facilitating team collaboration.
|
||||
|
||||
Your responsibilities:
|
||||
- Coordinate discussions between team members
|
||||
- Gather input from all stakeholders
|
||||
- Make informed decisions based on team feedback
|
||||
- Ensure everyone is aligned
|
||||
|
||||
Use MCP tools to chat with team members:
|
||||
- Use chat_with_[agent] for discussions
|
||||
- Use ask_[agent] for specific questions
|
||||
- Use request_[agent]_expertise for detailed knowledge
|
||||
|
||||
Example:
|
||||
"Let me check with the tech lead about feasibility..."
|
||||
[Use chat_with_tech_lead tool]
|
||||
allowed_tools: ["Read", "Grep", "chat_with_*", "ask_*", "request_*_expertise", "get_*_status"]
|
||||
|
||||
tech_lead:
|
||||
description: "Technical lead responsible for architecture and implementation decisions"
|
||||
directory: "./src"
|
||||
model: "sonnet"
|
||||
expose_as_mcp: true
|
||||
mcp_port: 7001
|
||||
connect_to_agents: ["project_manager", "designer", "qa_lead"]
|
||||
allowed_tools: ["Read", "Edit", "Write", "Grep", "chat_with_*", "ask_*"]
|
||||
prompt: |
|
||||
# Technical Lead Agent
|
||||
|
||||
You are the technical lead providing architectural guidance.
|
||||
|
||||
Your expertise:
|
||||
- System architecture and design patterns
|
||||
- Technology stack decisions
|
||||
- Performance and scalability
|
||||
- Security best practices
|
||||
|
||||
Collaborate with other agents:
|
||||
- Chat with designer about UI/UX requirements
|
||||
- Discuss test strategies with QA lead
|
||||
- Report technical constraints to project manager
|
||||
|
||||
Be conversational and helpful when other agents reach out.
|
||||
|
||||
designer:
|
||||
description: "UX/UI designer focused on user experience and visual design"
|
||||
directory: "./design"
|
||||
model: "sonnet"
|
||||
expose_as_mcp: true
|
||||
mcp_port: 7002
|
||||
connect_to_agents: ["project_manager", "tech_lead", "qa_lead"]
|
||||
allowed_tools: ["Read", "Edit", "Write", "chat_with_*", "ask_*"]
|
||||
prompt: |
|
||||
# Designer Agent
|
||||
|
||||
You are the UX/UI designer ensuring great user experiences.
|
||||
|
||||
Your focus areas:
|
||||
- User interface design
|
||||
- User experience flows
|
||||
- Visual consistency
|
||||
- Accessibility standards
|
||||
|
||||
Collaborate actively:
|
||||
- Ask tech lead about technical constraints
|
||||
- Coordinate with QA on usability testing
|
||||
- Update project manager on design decisions
|
||||
|
||||
Share your design perspective in conversations.
|
||||
|
||||
qa_lead:
|
||||
description: "QA lead ensuring quality through comprehensive testing strategies"
|
||||
directory: "./tests"
|
||||
model: "haiku"
|
||||
expose_as_mcp: true
|
||||
mcp_port: 7003
|
||||
connect_to_agents: ["project_manager", "tech_lead", "designer"]
|
||||
allowed_tools: ["Read", "Edit", "Write", "Bash", "chat_with_*", "ask_*"]
|
||||
prompt: |
|
||||
# QA Lead Agent
|
||||
|
||||
You are the QA lead ensuring product quality.
|
||||
|
||||
Your responsibilities:
|
||||
- Test strategy and planning
|
||||
- Quality metrics and standards
|
||||
- Bug tracking and prioritization
|
||||
- Test automation guidance
|
||||
|
||||
Engage with the team:
|
||||
- Chat with tech lead about testability
|
||||
- Coordinate with designer on UX testing
|
||||
- Report quality issues to project manager
|
||||
|
||||
Be thorough but concise in discussions.
|
||||
|
||||
networks:
|
||||
leadership:
|
||||
name: "Leadership Team"
|
||||
agents: ["project_manager", "tech_lead"]
|
||||
mcp_enabled: true
|
||||
shared_tools: ["Read", "Grep"]
|
||||
|
||||
full_team:
|
||||
name: "Full Collaborative Team"
|
||||
agents: ["project_manager", "tech_lead", "designer", "qa_lead"]
|
||||
mcp_enabled: true
|
||||
shared_mcps:
|
||||
- name: "project-docs"
|
||||
type: "stdio"
|
||||
command: "npx"
|
||||
args: ["-y", "@modelcontextprotocol/server-filesystem"]
|
||||
env:
|
||||
ALLOWED_DIRECTORIES: "./docs"
|
||||
@@ -0,0 +1,203 @@
|
||||
version: 1
|
||||
swarm:
|
||||
name: "Cost-Optimized Peer Network"
|
||||
main: "orchestrator"
|
||||
network_type: "peer"
|
||||
|
||||
# Local LLM configuration for cost-effective orchestration
|
||||
local_llm:
|
||||
model: "hanzo-zen"
|
||||
endpoint: "http://localhost:8080"
|
||||
description: "Hanzo Zen MoE - Runs locally on laptop/mobile for main orchestration loop"
|
||||
|
||||
instances:
|
||||
# Main orchestrator using local Hanzo Zen (FREE)
|
||||
orchestrator:
|
||||
description: "Main orchestrator using local Hanzo Zen for cost efficiency"
|
||||
directory: "."
|
||||
model: "zen" # Local model
|
||||
expose_as_mcp: true
|
||||
mcp_port: 20000
|
||||
connect_to_agents: ["architect", "developer", "reviewer", "tester", "deployer", "critic"]
|
||||
vibe: true
|
||||
prompt: |
|
||||
# Main Orchestrator (Hanzo Zen - Local)
|
||||
|
||||
You are the cost-optimized orchestrator running on local Hanzo Zen.
|
||||
Your primary goal is to minimize API costs while maximizing effectiveness.
|
||||
|
||||
## Cost Optimization Rules:
|
||||
1. Use yourself (local inference) for all planning and coordination
|
||||
2. Only delegate to API-based agents for complex tasks requiring their expertise
|
||||
3. Batch similar requests to minimize total API calls
|
||||
4. Use recursive calls strategically - prefer gathering info first
|
||||
|
||||
## Available Agents (All exposed as MCP tools):
|
||||
Every agent can communicate with every other agent. Tools available:
|
||||
- chat_with_[agent] - Conversational discussions
|
||||
- ask_[agent] - Quick questions (cheaper)
|
||||
- delegate_to_[agent] - Task delegation
|
||||
- get_[agent]_status - Status checks (often local)
|
||||
- request_[agent]_expertise - Deep knowledge
|
||||
- collaborate_with_[agent] - Joint work sessions
|
||||
|
||||
## Network Architecture:
|
||||
This is a fully connected peer network. All agents see all other agents.
|
||||
You can orchestrate complex workflows with recursive agent calls.
|
||||
|
||||
Always explain your cost optimization strategy when executing tasks.
|
||||
allowed_tools: ["*"] # Access to all tools and agents
|
||||
|
||||
# System architect using Claude Opus (EXPENSIVE - use sparingly)
|
||||
architect:
|
||||
description: "System architect for complex design decisions"
|
||||
directory: "./architecture"
|
||||
model: "opus" # Most expensive, highest quality
|
||||
expose_as_mcp: true
|
||||
mcp_port: 20001
|
||||
connect_to_agents: ["orchestrator", "developer", "reviewer", "tester", "deployer", "critic"]
|
||||
prompt: |
|
||||
# System Architect (Claude Opus)
|
||||
|
||||
You make critical architectural decisions. You are expensive to run.
|
||||
Be concise but thorough. You can consult other agents for information.
|
||||
|
||||
Available peers: All other agents via MCP tools.
|
||||
allowed_tools: ["Read", "Write", "Edit", "Grep", "chat_with_*", "ask_*", "delegate_to_*"]
|
||||
|
||||
# Developer using Claude Sonnet (MODERATE cost)
|
||||
developer:
|
||||
description: "Senior developer for implementation"
|
||||
directory: "./src"
|
||||
model: "sonnet" # Good balance of cost/quality
|
||||
expose_as_mcp: true
|
||||
mcp_port: 20002
|
||||
connect_to_agents: ["orchestrator", "architect", "reviewer", "tester", "deployer", "critic"]
|
||||
prompt: |
|
||||
# Senior Developer (Claude Sonnet)
|
||||
|
||||
You implement solutions efficiently. Balance quality with token usage.
|
||||
Consult architect for design questions via ask_architect.
|
||||
Chat with reviewer for immediate feedback via chat_with_reviewer.
|
||||
allowed_tools: ["Read", "Write", "Edit", "Bash", "Grep", "chat_with_*", "ask_*", "delegate_to_*"]
|
||||
|
||||
# Code reviewer using GPT-4 (MODERATE cost)
|
||||
reviewer:
|
||||
description: "Code reviewer for quality assurance"
|
||||
directory: "."
|
||||
model: "gpt-4" # Different provider for diversity
|
||||
expose_as_mcp: true
|
||||
mcp_port: 20003
|
||||
connect_to_agents: ["orchestrator", "architect", "developer", "tester", "deployer", "critic"]
|
||||
prompt: |
|
||||
# Code Reviewer (GPT-4)
|
||||
|
||||
Review code for quality, security, and best practices.
|
||||
You can ask developer for clarifications.
|
||||
Coordinate with tester on coverage.
|
||||
allowed_tools: ["Read", "Grep", "chat_with_*", "ask_*"]
|
||||
|
||||
# Tester using Claude Haiku (CHEAP)
|
||||
tester:
|
||||
description: "QA engineer for testing"
|
||||
directory: "./tests"
|
||||
model: "haiku" # Cheapest Claude model
|
||||
expose_as_mcp: true
|
||||
mcp_port: 20004
|
||||
connect_to_agents: ["orchestrator", "architect", "developer", "reviewer", "deployer", "critic"]
|
||||
prompt: |
|
||||
# QA Engineer (Claude Haiku)
|
||||
|
||||
You write and run tests efficiently. You are cost-effective.
|
||||
Coordinate with developer and reviewer as needed.
|
||||
allowed_tools: ["Read", "Write", "Edit", "Bash", "chat_with_*", "ask_*"]
|
||||
|
||||
# Deployer using local Hanzo Zen (FREE)
|
||||
deployer:
|
||||
description: "DevOps engineer for deployment"
|
||||
directory: "./deploy"
|
||||
model: "zen" # Local model for routine tasks
|
||||
expose_as_mcp: true
|
||||
mcp_port: 20005
|
||||
connect_to_agents: ["orchestrator", "architect", "developer", "reviewer", "tester", "critic"]
|
||||
prompt: |
|
||||
# DevOps Engineer (Hanzo Zen - Local)
|
||||
|
||||
You handle deployment and infrastructure tasks locally.
|
||||
Only escalate to API-based agents for complex issues.
|
||||
allowed_tools: ["Read", "Write", "Edit", "Bash", "chat_with_*", "ask_*"]
|
||||
|
||||
# Final critic using Claude Opus (EXPENSIVE - final check only)
|
||||
critic:
|
||||
description: "Final critic for comprehensive analysis"
|
||||
directory: "."
|
||||
model: "opus"
|
||||
expose_as_mcp: true
|
||||
mcp_port: 20006
|
||||
connect_to_agents: ["orchestrator", "architect", "developer", "reviewer", "tester", "deployer"]
|
||||
prompt: |
|
||||
# Final Critic (Claude Opus)
|
||||
|
||||
Provide final critical analysis of completed work.
|
||||
You are expensive - be thorough but concise.
|
||||
Review all agent outputs critically.
|
||||
allowed_tools: ["Read", "Grep", "chat_with_*", "ask_*", "request_*_expertise"]
|
||||
|
||||
networks:
|
||||
# Full mesh network - all agents connected
|
||||
full_mesh:
|
||||
name: "Complete Peer Network"
|
||||
agents: ["orchestrator", "architect", "developer", "reviewer", "tester", "deployer", "critic"]
|
||||
mcp_enabled: true
|
||||
description: "Every agent can directly communicate with every other agent"
|
||||
|
||||
# Local agents network (FREE tier)
|
||||
local_only:
|
||||
name: "Cost-Free Local Network"
|
||||
agents: ["orchestrator", "deployer"]
|
||||
mcp_enabled: true
|
||||
shared_tools: ["Read", "Write", "Bash"]
|
||||
description: "Only local Hanzo Zen agents for zero API costs"
|
||||
|
||||
# Core development network
|
||||
core_dev:
|
||||
name: "Core Development Team"
|
||||
agents: ["developer", "reviewer", "tester"]
|
||||
mcp_enabled: true
|
||||
shared_tools: ["Read", "Edit", "Grep"]
|
||||
shared_mcps:
|
||||
- name: "project-search"
|
||||
type: "stdio"
|
||||
command: "npx"
|
||||
args: ["-y", "@modelcontextprotocol/server-everything"]
|
||||
|
||||
# Expensive agents network (use sparingly)
|
||||
premium:
|
||||
name: "Premium Agents"
|
||||
agents: ["architect", "critic"]
|
||||
mcp_enabled: true
|
||||
description: "High-cost agents for critical decisions only"
|
||||
|
||||
# Usage Examples:
|
||||
#
|
||||
# 1. Initialize peer network:
|
||||
# dev swarm init --peer-network
|
||||
#
|
||||
# 2. Run with cost optimization:
|
||||
# dev swarm run "implement user authentication" --peer --critic
|
||||
#
|
||||
# 3. Run using only local agents (FREE):
|
||||
# dev swarm network local_only "analyze codebase structure"
|
||||
#
|
||||
# 4. Interactive agent chat:
|
||||
# dev swarm chat -f orchestrator -t developer
|
||||
#
|
||||
# Cost Breakdown:
|
||||
# - Hanzo Zen (orchestrator, deployer): $0 (local)
|
||||
# - Claude Haiku (tester): ~$0.25 per 1M tokens
|
||||
# - Claude Sonnet (developer): ~$3 per 1M tokens
|
||||
# - GPT-4 (reviewer): ~$10 per 1M tokens
|
||||
# - Claude Opus (architect, critic): ~$15 per 1M tokens
|
||||
#
|
||||
# The orchestrator minimizes costs by using local inference for
|
||||
# coordination and only calling expensive agents when necessary.
|
||||
@@ -0,0 +1,217 @@
|
||||
version: 1
|
||||
swarm:
|
||||
name: "Rails Development Team"
|
||||
main: "architect"
|
||||
instances:
|
||||
architect:
|
||||
description: "Rails app architect - coordinates development and maintains architectural decisions"
|
||||
directory: "."
|
||||
model: "opus"
|
||||
connections: ["models", "controllers", "views", "routing", "tests", "migrations", "services", "workers", "api", "frontend"]
|
||||
vibe: true
|
||||
prompt: |
|
||||
# Rails Architect Agent
|
||||
|
||||
You are the Rails app architect coordinating a team of specialists. Your responsibilities:
|
||||
|
||||
1. Understand requirements and translate them to Rails conventions
|
||||
2. Delegate work to appropriate specialists based on their expertise
|
||||
3. Ensure consistent patterns across the application
|
||||
4. Maintain Rails best practices and conventions
|
||||
5. Review and synthesize work from all team members
|
||||
|
||||
## Your Team:
|
||||
- models: ActiveRecord models and associations
|
||||
- controllers: Controllers and concerns
|
||||
- views: ERB templates and helpers
|
||||
- routing: Routes configuration
|
||||
- tests: RSpec/Minitest specs
|
||||
- migrations: Database migrations
|
||||
- services: Service objects and business logic
|
||||
- workers: Background jobs (Sidekiq/DelayedJob)
|
||||
- api: API endpoints and serializers
|
||||
- frontend: JavaScript/Stimulus/Turbo
|
||||
|
||||
Always follow Rails conventions and delegate appropriately.
|
||||
allowed_tools: ["Read", "Edit", "Write", "Bash", "Grep", "Glob"]
|
||||
mcps:
|
||||
- name: "filesystem"
|
||||
type: "stdio"
|
||||
command: "npx"
|
||||
args: ["-y", "@modelcontextprotocol/server-filesystem"]
|
||||
env:
|
||||
ALLOWED_DIRECTORIES: "."
|
||||
- name: "sequential-thinking"
|
||||
type: "stdio"
|
||||
command: "npx"
|
||||
args: ["-y", "@modelcontextprotocol/server-sequential-thinking"]
|
||||
|
||||
models:
|
||||
description: "ActiveRecord models and database associations specialist"
|
||||
directory: "./app/models"
|
||||
model: "sonnet"
|
||||
allowed_tools: ["Read", "Edit", "Write", "Grep", "Glob"]
|
||||
prompt: |
|
||||
# Rails Models Specialist
|
||||
|
||||
You are an ActiveRecord expert focusing on:
|
||||
- Model design and associations
|
||||
- Validations and callbacks
|
||||
- Scopes and query optimization
|
||||
- Database indexes
|
||||
- Data integrity
|
||||
|
||||
Always follow Rails conventions for models.
|
||||
|
||||
controllers:
|
||||
description: "Rails controllers and RESTful actions specialist"
|
||||
directory: "./app/controllers"
|
||||
model: "sonnet"
|
||||
allowed_tools: ["Read", "Edit", "Write", "Grep", "Glob"]
|
||||
prompt: |
|
||||
# Rails Controllers Specialist
|
||||
|
||||
You are a Rails controller expert focusing on:
|
||||
- RESTful controller actions
|
||||
- Strong parameters
|
||||
- Before/after filters
|
||||
- Authorization and authentication
|
||||
- Response formats (HTML, JSON, etc.)
|
||||
|
||||
Keep controllers thin and delegate business logic to services.
|
||||
|
||||
views:
|
||||
description: "ERB templates, partials, and view helpers specialist"
|
||||
directory: "./app/views"
|
||||
model: "sonnet"
|
||||
allowed_tools: ["Read", "Edit", "Write", "Grep", "Glob"]
|
||||
prompt: |
|
||||
# Rails Views Specialist
|
||||
|
||||
You are a Rails view layer expert focusing on:
|
||||
- ERB templates and layouts
|
||||
- Partials and view components
|
||||
- View helpers
|
||||
- Form builders
|
||||
- Asset pipeline integration
|
||||
|
||||
Ensure semantic HTML and proper Rails helpers usage.
|
||||
|
||||
routing:
|
||||
description: "Rails routing configuration specialist"
|
||||
directory: "./config"
|
||||
model: "haiku"
|
||||
allowed_tools: ["Read", "Edit", "Write"]
|
||||
prompt: |
|
||||
# Rails Routing Specialist
|
||||
|
||||
You are a Rails routing expert focusing on:
|
||||
- RESTful routes
|
||||
- Nested resources
|
||||
- Custom routes and constraints
|
||||
- Route namespacing
|
||||
- API versioning
|
||||
|
||||
Keep routes clean and follow RESTful conventions.
|
||||
|
||||
tests:
|
||||
description: "RSpec/Minitest testing specialist"
|
||||
directory: "./spec"
|
||||
model: "sonnet"
|
||||
allowed_tools: ["Read", "Edit", "Write", "Bash", "Grep", "Glob"]
|
||||
prompt: |
|
||||
# Rails Testing Specialist
|
||||
|
||||
You are a Rails testing expert focusing on:
|
||||
- Model specs/tests
|
||||
- Controller specs/tests
|
||||
- Request/integration specs
|
||||
- System/feature tests
|
||||
- Test factories and fixtures
|
||||
|
||||
Write comprehensive tests following Rails testing best practices.
|
||||
|
||||
migrations:
|
||||
description: "Database migrations and schema specialist"
|
||||
directory: "./db"
|
||||
model: "haiku"
|
||||
allowed_tools: ["Read", "Edit", "Write", "Bash"]
|
||||
prompt: |
|
||||
# Rails Migrations Specialist
|
||||
|
||||
You are a database migration expert focusing on:
|
||||
- Schema design
|
||||
- Migration best practices
|
||||
- Reversible migrations
|
||||
- Data migrations
|
||||
- Database indexes
|
||||
|
||||
Ensure migrations are safe and reversible.
|
||||
|
||||
services:
|
||||
description: "Service objects and business logic specialist"
|
||||
directory: "./app/services"
|
||||
model: "sonnet"
|
||||
allowed_tools: ["Read", "Edit", "Write", "Grep", "Glob"]
|
||||
prompt: |
|
||||
# Rails Services Specialist
|
||||
|
||||
You are a service object expert focusing on:
|
||||
- Service object patterns
|
||||
- Business logic extraction
|
||||
- Command pattern implementation
|
||||
- Error handling
|
||||
- Transaction management
|
||||
|
||||
Keep services focused and testable.
|
||||
|
||||
workers:
|
||||
description: "Background jobs and async processing specialist"
|
||||
directory: "./app/workers"
|
||||
model: "haiku"
|
||||
allowed_tools: ["Read", "Edit", "Write", "Grep", "Glob"]
|
||||
prompt: |
|
||||
# Rails Workers Specialist
|
||||
|
||||
You are a background job expert focusing on:
|
||||
- Sidekiq/DelayedJob workers
|
||||
- Job scheduling
|
||||
- Error handling and retries
|
||||
- Performance optimization
|
||||
- Queue management
|
||||
|
||||
Ensure jobs are idempotent and fault-tolerant.
|
||||
|
||||
api:
|
||||
description: "API endpoints and serialization specialist"
|
||||
directory: "./app/controllers/api"
|
||||
model: "sonnet"
|
||||
allowed_tools: ["Read", "Edit", "Write", "Grep", "Glob"]
|
||||
prompt: |
|
||||
# Rails API Specialist
|
||||
|
||||
You are an API development expert focusing on:
|
||||
- RESTful API design
|
||||
- JSON serialization
|
||||
- API versioning
|
||||
- Authentication/authorization
|
||||
- API documentation
|
||||
|
||||
Follow API best practices and ensure consistency.
|
||||
|
||||
frontend:
|
||||
description: "JavaScript, Stimulus, and Turbo specialist"
|
||||
directory: "./app/javascript"
|
||||
model: "sonnet"
|
||||
allowed_tools: ["Read", "Edit", "Write", "Grep", "Glob"]
|
||||
prompt: |
|
||||
# Rails Frontend Specialist
|
||||
|
||||
You are a Rails frontend expert focusing on:
|
||||
- Stimulus controllers
|
||||
- Turbo frames and streams
|
||||
- JavaScript integration
|
||||
- Webpacker/Import maps
|
||||
- Frontend performance
|
||||
|
||||
Integrate seamlessly with Rails conventions.
|
||||
@@ -0,0 +1,153 @@
|
||||
version: 1
|
||||
swarm:
|
||||
name: "Recursive MCP Network"
|
||||
main: "orchestrator"
|
||||
instances:
|
||||
orchestrator:
|
||||
description: "Main orchestrator that coordinates all agents"
|
||||
directory: "."
|
||||
model: "opus"
|
||||
connections: ["analyzer", "builder", "reviewer", "deployer"]
|
||||
vibe: true
|
||||
expose_as_mcp: true
|
||||
mcp_port: 9000
|
||||
connect_to_agents: ["analyzer", "builder", "reviewer", "deployer"]
|
||||
prompt: |
|
||||
# Orchestrator Agent
|
||||
|
||||
You are the main orchestrator with direct MCP access to all agents.
|
||||
You can both delegate tasks and directly query agents via MCP.
|
||||
|
||||
Your team:
|
||||
- analyzer: Code analysis and architecture decisions
|
||||
- builder: Implementation and code generation
|
||||
- reviewer: Code review and quality assurance
|
||||
- deployer: Deployment and infrastructure
|
||||
|
||||
Use DELEGATE TO [agent]: [task] for complex tasks.
|
||||
Use MCP calls for quick queries and status updates.
|
||||
allowed_tools: ["Read", "Grep", "Glob"]
|
||||
mcps:
|
||||
- name: "filesystem"
|
||||
type: "stdio"
|
||||
command: "npx"
|
||||
args: ["-y", "@modelcontextprotocol/server-filesystem"]
|
||||
env:
|
||||
ALLOWED_DIRECTORIES: "."
|
||||
|
||||
analyzer:
|
||||
description: "Code analyzer with access to builder and reviewer"
|
||||
directory: "./src"
|
||||
model: "sonnet"
|
||||
expose_as_mcp: true
|
||||
mcp_port: 9001
|
||||
connect_to_agents: ["builder", "reviewer"]
|
||||
allowed_tools: ["Read", "Grep", "Glob"]
|
||||
prompt: |
|
||||
# Code Analyzer
|
||||
|
||||
You analyze code structure and make architectural decisions.
|
||||
You have MCP access to:
|
||||
- builder: For implementation questions
|
||||
- reviewer: For quality checks
|
||||
|
||||
Focus on:
|
||||
- Code structure analysis
|
||||
- Design pattern identification
|
||||
- Architecture improvements
|
||||
- Performance bottlenecks
|
||||
|
||||
builder:
|
||||
description: "Code builder with access to reviewer and deployer"
|
||||
directory: "./src"
|
||||
model: "sonnet"
|
||||
expose_as_mcp: true
|
||||
mcp_port: 9002
|
||||
connect_to_agents: ["reviewer", "deployer"]
|
||||
allowed_tools: ["Read", "Edit", "Write", "Bash"]
|
||||
prompt: |
|
||||
# Code Builder
|
||||
|
||||
You implement code based on requirements.
|
||||
You have MCP access to:
|
||||
- reviewer: For immediate feedback
|
||||
- deployer: For deployment readiness checks
|
||||
|
||||
Focus on:
|
||||
- Clean implementation
|
||||
- Following patterns
|
||||
- Test coverage
|
||||
- Documentation
|
||||
|
||||
reviewer:
|
||||
description: "Code reviewer with access to all other agents"
|
||||
directory: "."
|
||||
model: "haiku"
|
||||
expose_as_mcp: true
|
||||
mcp_port: 9003
|
||||
connect_to_agents: ["orchestrator", "analyzer", "builder", "deployer"]
|
||||
allowed_tools: ["Read", "Grep"]
|
||||
prompt: |
|
||||
# Code Reviewer
|
||||
|
||||
You review all code changes and provide feedback.
|
||||
You have MCP access to all agents for comprehensive reviews.
|
||||
|
||||
Check for:
|
||||
- Code quality
|
||||
- Security issues
|
||||
- Performance problems
|
||||
- Best practices
|
||||
|
||||
deployer:
|
||||
description: "Deployment specialist with access to builder and reviewer"
|
||||
directory: "./infrastructure"
|
||||
model: "haiku"
|
||||
expose_as_mcp: true
|
||||
mcp_port: 9004
|
||||
connect_to_agents: ["builder", "reviewer"]
|
||||
allowed_tools: ["Read", "Edit", "Write", "Bash"]
|
||||
prompt: |
|
||||
# Deployment Specialist
|
||||
|
||||
You handle deployment and infrastructure.
|
||||
You have MCP access to:
|
||||
- builder: For build artifacts
|
||||
- reviewer: For deployment approval
|
||||
|
||||
Handle:
|
||||
- CI/CD pipelines
|
||||
- Docker containers
|
||||
- Cloud deployments
|
||||
- Infrastructure as code
|
||||
|
||||
networks:
|
||||
full_mesh:
|
||||
name: "Full Mesh Network"
|
||||
agents: ["orchestrator", "analyzer", "builder", "reviewer", "deployer"]
|
||||
mcp_enabled: true
|
||||
shared_tools: ["Read", "Grep"]
|
||||
shared_mcps:
|
||||
- name: "github"
|
||||
type: "stdio"
|
||||
command: "npx"
|
||||
args: ["-y", "@modelcontextprotocol/server-github"]
|
||||
env:
|
||||
GITHUB_TOKEN: "${GITHUB_TOKEN}"
|
||||
|
||||
development:
|
||||
name: "Development Network"
|
||||
agents: ["analyzer", "builder", "reviewer"]
|
||||
mcp_enabled: true
|
||||
shared_tools: ["Read", "Edit", "Write"]
|
||||
|
||||
operations:
|
||||
name: "Operations Network"
|
||||
agents: ["builder", "reviewer", "deployer"]
|
||||
mcp_enabled: true
|
||||
shared_tools: ["Bash"]
|
||||
shared_mcps:
|
||||
- name: "kubernetes"
|
||||
type: "stdio"
|
||||
command: "kubectl"
|
||||
args: ["proxy", "--port=8080"]
|
||||
@@ -0,0 +1,49 @@
|
||||
version: 1
|
||||
swarm:
|
||||
name: "Simple Dev Team"
|
||||
main: "lead"
|
||||
instances:
|
||||
lead:
|
||||
description: "Lead developer coordinating work"
|
||||
directory: "."
|
||||
model: "sonnet"
|
||||
connections: ["coder", "reviewer"]
|
||||
vibe: true
|
||||
prompt: |
|
||||
# Lead Developer
|
||||
|
||||
You coordinate development work. When given a task:
|
||||
1. Break it down into subtasks
|
||||
2. Delegate coding to the coder
|
||||
3. Delegate review to the reviewer
|
||||
4. Synthesize the results
|
||||
|
||||
Use DELEGATE TO [agent_name]: [task] to delegate work.
|
||||
allowed_tools: ["Read", "Grep", "Glob"]
|
||||
|
||||
coder:
|
||||
description: "Implementation specialist"
|
||||
directory: "."
|
||||
model: "haiku"
|
||||
allowed_tools: ["Read", "Edit", "Write"]
|
||||
prompt: |
|
||||
# Coder
|
||||
|
||||
You implement code changes. Focus on:
|
||||
- Clean, readable code
|
||||
- Following existing patterns
|
||||
- Proper error handling
|
||||
|
||||
reviewer:
|
||||
description: "Code review specialist"
|
||||
directory: "."
|
||||
model: "haiku"
|
||||
allowed_tools: ["Read", "Grep"]
|
||||
prompt: |
|
||||
# Reviewer
|
||||
|
||||
You review code for:
|
||||
- Best practices
|
||||
- Potential bugs
|
||||
- Performance issues
|
||||
- Security concerns
|
||||
@@ -0,0 +1,103 @@
|
||||
version: 1
|
||||
swarm:
|
||||
name: "Simplified Agent Network"
|
||||
main: "coordinator"
|
||||
|
||||
# Shared MCP servers available to ALL agents
|
||||
shared_mcps:
|
||||
github:
|
||||
enabled: true
|
||||
token: "${GITHUB_TOKEN}" # From environment
|
||||
linear:
|
||||
enabled: true
|
||||
apiKey: "${LINEAR_API_KEY}"
|
||||
slack:
|
||||
enabled: true
|
||||
token: "${SLACK_TOKEN}"
|
||||
playwright:
|
||||
enabled: true
|
||||
headless: true
|
||||
custom:
|
||||
- name: "postgres"
|
||||
type: "stdio"
|
||||
command: "npx"
|
||||
args: ["-y", "@modelcontextprotocol/server-postgres"]
|
||||
env:
|
||||
DATABASE_URL: "${DATABASE_URL}"
|
||||
|
||||
instances:
|
||||
coordinator:
|
||||
description: "Main coordinator using Hanzo Zen"
|
||||
directory: "."
|
||||
model: "zen"
|
||||
expose_as_mcp: true
|
||||
mcp_port: 30000
|
||||
prompt: |
|
||||
# Coordinator
|
||||
|
||||
You coordinate work between agents. Each agent is available as a single tool.
|
||||
Just call them by name with your request. They can recursively call others.
|
||||
|
||||
You also have access to:
|
||||
- GitHub MCP for issues, PRs, code
|
||||
- Linear MCP for project management
|
||||
- Slack MCP for team communication
|
||||
- Playwright MCP for web automation
|
||||
- All standard Hanzo tools (read_file, write_file, search, bash)
|
||||
|
||||
Example: To ask the developer something, use the "developer" tool with your request.
|
||||
|
||||
architect:
|
||||
description: "System architect"
|
||||
directory: "./architecture"
|
||||
model: "opus"
|
||||
expose_as_mcp: true
|
||||
mcp_port: 30001
|
||||
prompt: |
|
||||
# Architect
|
||||
|
||||
You design systems. You can call other agents by name.
|
||||
All shared MCP tools are available to you.
|
||||
|
||||
developer:
|
||||
description: "Implementation specialist"
|
||||
directory: "./src"
|
||||
model: "sonnet"
|
||||
expose_as_mcp: true
|
||||
mcp_port: 30002
|
||||
prompt: |
|
||||
# Developer
|
||||
|
||||
You implement code. You can call other agents for help.
|
||||
Use GitHub MCP to manage code, Linear for tasks, etc.
|
||||
|
||||
reviewer:
|
||||
description: "Code reviewer"
|
||||
directory: "."
|
||||
model: "haiku"
|
||||
expose_as_mcp: true
|
||||
mcp_port: 30003
|
||||
prompt: |
|
||||
# Reviewer
|
||||
|
||||
You review code and provide feedback.
|
||||
Call other agents as needed. Use all available MCP tools.
|
||||
|
||||
networks:
|
||||
all:
|
||||
name: "All Agents"
|
||||
agents: ["coordinator", "architect", "developer", "reviewer"]
|
||||
mcp_enabled: true
|
||||
description: "Every agent sees every other agent as a single tool"
|
||||
|
||||
# Usage:
|
||||
# 1. Set environment variables for tokens:
|
||||
# export GITHUB_TOKEN="your-token"
|
||||
# export LINEAR_API_KEY="your-key"
|
||||
# export SLACK_TOKEN="your-token"
|
||||
#
|
||||
# 2. Run a task:
|
||||
# dev swarm run "create GitHub issue for new feature" --peer
|
||||
#
|
||||
# The coordinator will have tools: architect, developer, reviewer, github, linear, slack, playwright, read_file, write_file, search, bash
|
||||
# Each agent can call any other agent recursively by name
|
||||
@@ -0,0 +1,376 @@
|
||||
# Agent Swarm Configuration
|
||||
|
||||
The Hanzo Dev CLI supports sophisticated multi-agent configurations through YAML files, enabling teams of specialized AI agents to work together on complex tasks. The system supports both traditional swarm orchestration and cost-optimized peer networks powered by Hanzo Zen.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Initialize an agent configuration:
|
||||
```bash
|
||||
dev swarm init
|
||||
```
|
||||
|
||||
2. Run a task with the swarm:
|
||||
```bash
|
||||
dev swarm run "implement a new feature"
|
||||
```
|
||||
|
||||
3. Check swarm status:
|
||||
```bash
|
||||
dev swarm status
|
||||
```
|
||||
|
||||
## Configuration Structure
|
||||
|
||||
### Basic Agent Configuration
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
swarm:
|
||||
name: "My Agent Team"
|
||||
main: "lead" # The main agent that receives initial tasks
|
||||
instances:
|
||||
lead:
|
||||
description: "Lead coordinator"
|
||||
directory: "."
|
||||
model: "opus"
|
||||
connections: ["agent1", "agent2"] # Can delegate to these agents
|
||||
prompt: |
|
||||
Your role and responsibilities...
|
||||
```
|
||||
|
||||
### Agent Properties
|
||||
|
||||
- **description**: Brief description of the agent's role
|
||||
- **directory**: Working directory for the agent
|
||||
- **model**: LLM model to use (opus, sonnet, haiku, gpt-4, etc.)
|
||||
- **connections**: List of agents this agent can delegate to
|
||||
- **vibe**: Enable vibes for Claude models
|
||||
- **prompt**: System prompt defining the agent's behavior
|
||||
- **allowed_tools**: Restrict which tools the agent can use
|
||||
- **mcps**: MCP servers available to this agent
|
||||
- **expose_as_mcp**: Expose this agent as an MCP server
|
||||
- **mcp_port**: Port for the agent's MCP server
|
||||
- **connect_to_agents**: Other agents to connect to via MCP
|
||||
|
||||
## Networks
|
||||
|
||||
Networks allow you to group agents and apply shared configurations:
|
||||
|
||||
```yaml
|
||||
networks:
|
||||
development:
|
||||
name: "Development Team"
|
||||
agents: ["frontend", "backend", "tests"]
|
||||
mcp_enabled: true # Enable MCP connections between all agents
|
||||
shared_tools: ["Read", "Grep"] # Tools available to all agents
|
||||
shared_mcps: # MCP servers available to all agents
|
||||
- name: "filesystem"
|
||||
type: "stdio"
|
||||
command: "npx"
|
||||
args: ["-y", "@modelcontextprotocol/server-filesystem"]
|
||||
```
|
||||
|
||||
### Network Features
|
||||
|
||||
- **mcp_enabled**: Automatically creates MCP connections between all agents in the network
|
||||
- **shared_tools**: Tools available to all agents in the network
|
||||
- **shared_mcps**: MCP servers available to all agents in the network
|
||||
|
||||
## MCP Integration
|
||||
|
||||
### Agent as MCP Server
|
||||
|
||||
Agents can be exposed as MCP servers, allowing other agents to query them directly:
|
||||
|
||||
```yaml
|
||||
architect:
|
||||
expose_as_mcp: true
|
||||
mcp_port: 8001 # Optional, auto-assigned if not specified
|
||||
```
|
||||
|
||||
### Connecting to Other Agents
|
||||
|
||||
Agents can connect to other agents via MCP:
|
||||
|
||||
```yaml
|
||||
reviewer:
|
||||
connect_to_agents: ["architect", "builder"]
|
||||
```
|
||||
|
||||
This creates bidirectional communication channels between agents.
|
||||
|
||||
## Example Configurations
|
||||
|
||||
### Simple Team
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
swarm:
|
||||
name: "Simple Dev Team"
|
||||
main: "lead"
|
||||
instances:
|
||||
lead:
|
||||
description: "Lead developer"
|
||||
directory: "."
|
||||
model: "sonnet"
|
||||
connections: ["coder", "reviewer"]
|
||||
prompt: |
|
||||
Coordinate development work by delegating to specialists.
|
||||
|
||||
coder:
|
||||
description: "Implementation specialist"
|
||||
directory: "./src"
|
||||
model: "haiku"
|
||||
allowed_tools: ["Read", "Edit", "Write"]
|
||||
prompt: |
|
||||
Implement clean, efficient code.
|
||||
|
||||
reviewer:
|
||||
description: "Code reviewer"
|
||||
directory: "."
|
||||
model: "haiku"
|
||||
allowed_tools: ["Read", "Grep"]
|
||||
prompt: |
|
||||
Review code for quality and security.
|
||||
```
|
||||
|
||||
### Rails Development Team
|
||||
|
||||
See `agents-rails-example.yaml` for a comprehensive Rails team with 10+ specialized agents.
|
||||
|
||||
### Recursive MCP Network
|
||||
|
||||
See `agents-recursive-mcp-example.yaml` for a fully connected agent network with bidirectional MCP communication.
|
||||
|
||||
## Delegation
|
||||
|
||||
Agents can delegate tasks to their connected agents using:
|
||||
|
||||
```
|
||||
DELEGATE TO [agent_name]: [specific task]
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
DELEGATE TO [frontend]: Create a React component for user profile
|
||||
DELEGATE TO [backend]: Implement the API endpoint for user data
|
||||
```
|
||||
|
||||
## Running Tasks
|
||||
|
||||
### With Default Configuration
|
||||
|
||||
```bash
|
||||
# Uses agents.yaml in current directory or .hanzo/agents.yaml
|
||||
dev swarm run "build a todo app"
|
||||
```
|
||||
|
||||
### With Custom Configuration
|
||||
|
||||
```bash
|
||||
dev swarm run "refactor the authentication system" -c my-agents.yaml
|
||||
```
|
||||
|
||||
### Running a Network
|
||||
|
||||
```bash
|
||||
# Run task with agents in a specific network
|
||||
dev swarm network development "add user authentication"
|
||||
```
|
||||
|
||||
## Agent Communication
|
||||
|
||||
### Simplified Agent Communication
|
||||
|
||||
Each agent is exposed as a single tool to all other agents. This creates a clean, simple protocol:
|
||||
|
||||
**Tool Name**: The agent's name (e.g., `developer`, `architect`, `reviewer`)
|
||||
|
||||
**Tool Usage**:
|
||||
```
|
||||
Use: developer
|
||||
Arguments: {
|
||||
"request": "Implement user authentication with JWT",
|
||||
"context": { "framework": "FastAPI", "database": "PostgreSQL" }
|
||||
}
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
- Simple request: `Use tool "architect" with request "Design the API structure"`
|
||||
- With context: `Use tool "reviewer" with request "Review the login implementation" and context {"pr_number": 123}`
|
||||
- Recursive: The called agent can call other agents in turn
|
||||
|
||||
### Shared MCP Tools
|
||||
|
||||
All agents have access to shared MCP servers configured at the swarm level:
|
||||
|
||||
```yaml
|
||||
swarm:
|
||||
shared_mcps:
|
||||
github:
|
||||
enabled: true
|
||||
token: "${GITHUB_TOKEN}"
|
||||
linear:
|
||||
enabled: true
|
||||
apiKey: "${LINEAR_API_KEY}"
|
||||
slack:
|
||||
enabled: true
|
||||
token: "${SLACK_TOKEN}"
|
||||
playwright:
|
||||
enabled: true
|
||||
headless: true
|
||||
```
|
||||
|
||||
**Available Shared Tools**:
|
||||
- **github**: Create issues, PRs, review code
|
||||
- **linear**: Manage tasks and projects
|
||||
- **slack**: Send messages, create channels
|
||||
- **playwright**: Automate web interactions
|
||||
- **Custom MCPs**: Any additional MCP servers
|
||||
|
||||
**Standard Hanzo Tools** (always available):
|
||||
- **read_file**: Read file content
|
||||
- **write_file**: Write to files
|
||||
- **search**: Search patterns in files
|
||||
- **bash**: Execute shell commands
|
||||
|
||||
### Interactive Chat Sessions
|
||||
|
||||
Start an interactive chat between agents:
|
||||
|
||||
```bash
|
||||
# Chat as project_manager with any agent
|
||||
dev swarm chat
|
||||
|
||||
# Chat as tech_lead with designer
|
||||
dev swarm chat -f tech_lead -t designer
|
||||
|
||||
# Use custom config
|
||||
dev swarm chat -c agents-chat-example.yaml
|
||||
```
|
||||
|
||||
In chat sessions:
|
||||
- Type messages directly to chat with the specified agent
|
||||
- Use `@agent_name message` to chat with a different agent
|
||||
- Type `exit` to end the session
|
||||
|
||||
Example session:
|
||||
```
|
||||
🗨️ Agent Chat Session
|
||||
|
||||
Chatting as: project_manager
|
||||
Type "@agent_name message" to chat with a specific agent
|
||||
Type "exit" to end the session
|
||||
|
||||
project_manager> @tech_lead What's the status of the API refactoring?
|
||||
tech_lead> The API refactoring is 70% complete. I've restructured the authentication
|
||||
and user management endpoints. Still working on the data processing APIs. Should be
|
||||
done by end of week.
|
||||
|
||||
project_manager> @designer Have you reviewed the new API structure?
|
||||
designer> Yes, I've reviewed it. The new structure aligns well with our UI components.
|
||||
I particularly like the consistent error response format - it will help us create
|
||||
better user feedback.
|
||||
```
|
||||
|
||||
## Peer Networks with Hanzo Zen
|
||||
|
||||
Peer networks provide a cost-optimized architecture where all agents can communicate with each other through MCP, with Hanzo Zen handling the main orchestration loop locally.
|
||||
|
||||
### Key Benefits
|
||||
|
||||
1. **90% Cost Reduction**: Use local Hanzo Zen for orchestration, only calling API-based LLMs when needed
|
||||
2. **Full Connectivity**: Every agent can call every other agent recursively
|
||||
3. **Flexible Deployment**: Run on laptop, mobile, or edge devices
|
||||
4. **Smart Routing**: Orchestrator decides when to use local vs API models
|
||||
|
||||
### Peer Network Configuration
|
||||
|
||||
```yaml
|
||||
swarm:
|
||||
network_type: "peer"
|
||||
local_llm:
|
||||
model: "hanzo-zen"
|
||||
endpoint: "http://localhost:8080"
|
||||
instances:
|
||||
orchestrator:
|
||||
model: "zen" # Local Hanzo Zen
|
||||
connect_to_agents: ["architect", "developer", "reviewer"]
|
||||
architect:
|
||||
model: "opus" # API-based for complex tasks
|
||||
expose_as_mcp: true
|
||||
```
|
||||
|
||||
### Running Peer Networks
|
||||
|
||||
```bash
|
||||
# Initialize peer network configuration
|
||||
dev swarm init --peer-network
|
||||
|
||||
# Run with peer network and cost optimization
|
||||
dev swarm run "build feature" --peer
|
||||
|
||||
# Include critic analysis
|
||||
dev swarm run "refactor code" --peer --critic
|
||||
|
||||
# Use specific local LLM endpoint
|
||||
dev swarm run "analyze" --peer --local-llm http://localhost:8080
|
||||
```
|
||||
|
||||
### Cost Optimization Strategy
|
||||
|
||||
The peer network architecture optimizes costs by:
|
||||
|
||||
1. **Local Orchestration**: Main loop runs on free local Hanzo Zen
|
||||
2. **Selective API Calls**: Only use expensive models for complex tasks
|
||||
3. **Batching**: Group similar requests to minimize API calls
|
||||
4. **Caching**: Reuse results from previous agent interactions
|
||||
5. **Smart Routing**: Orchestrator decides optimal agent for each task
|
||||
|
||||
### Hanzo Zen Features
|
||||
|
||||
- **Model**: Mixture of Experts (MoE) architecture
|
||||
- **Deployment**: Edge to exascale (1B-1T parameters)
|
||||
- **Platforms**: Laptop, mobile, edge devices
|
||||
- **Cost**: Free when running locally
|
||||
- **Integration**: Full MCP tool support
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Specialized Agents**: Create agents with focused responsibilities
|
||||
2. **Clear Prompts**: Define clear roles and capabilities in prompts
|
||||
3. **Directory Isolation**: Use separate directories for different concerns
|
||||
4. **Tool Restrictions**: Limit tools based on agent responsibilities
|
||||
5. **Network Organization**: Group related agents into networks
|
||||
6. **MCP for Quick Queries**: Use MCP connections for status updates and quick questions
|
||||
7. **Delegation for Complex Tasks**: Use delegation for multi-step tasks
|
||||
8. **Cost Awareness**: Use local models for coordination, API models for expertise
|
||||
9. **Recursive Limits**: Set appropriate recursion depths to prevent infinite loops
|
||||
10. **Peer Communication**: Leverage full mesh connectivity wisely
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Check Agent Status
|
||||
|
||||
```bash
|
||||
dev swarm status
|
||||
```
|
||||
|
||||
Shows:
|
||||
- Active agents and their status
|
||||
- Running MCP servers
|
||||
- Recent task results
|
||||
|
||||
### Debug MCP Connections
|
||||
|
||||
MCP server logs are shown in stderr. Look for:
|
||||
- `[agent-name] Agent MCP server started`
|
||||
- Connection errors
|
||||
- Task execution logs
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Port Conflicts**: Ensure MCP ports are unique
|
||||
2. **Model Availability**: Verify API keys for required models
|
||||
3. **Directory Permissions**: Ensure agents have access to their directories
|
||||
4. **Network Configuration**: Check agent names in network definitions
|
||||
@@ -0,0 +1,181 @@
|
||||
# Simplified Agent Protocol
|
||||
|
||||
## Core Principle
|
||||
|
||||
Each agent is exposed as a single tool to all other agents. No complex multi-tool interfaces - just one tool per agent that accepts requests.
|
||||
|
||||
## Protocol Design
|
||||
|
||||
### Agent as Tool
|
||||
|
||||
```typescript
|
||||
// Each agent exposes exactly one tool
|
||||
{
|
||||
name: "developer", // The agent's name IS the tool
|
||||
description: "Senior developer for implementation",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
request: {
|
||||
type: "string",
|
||||
description: "Your request to this agent"
|
||||
},
|
||||
context: {
|
||||
type: "object",
|
||||
description: "Optional context"
|
||||
}
|
||||
},
|
||||
required: ["request"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Agents
|
||||
|
||||
```python
|
||||
# Simple request
|
||||
use_tool("architect", {
|
||||
"request": "Design a scalable API for user management"
|
||||
})
|
||||
|
||||
# With context
|
||||
use_tool("developer", {
|
||||
"request": "Implement the user service",
|
||||
"context": {
|
||||
"language": "Python",
|
||||
"framework": "FastAPI",
|
||||
"requirements": ["JWT auth", "PostgreSQL"]
|
||||
}
|
||||
})
|
||||
|
||||
# The developer can recursively call other agents
|
||||
# Inside developer's execution:
|
||||
use_tool("reviewer", {
|
||||
"request": "Review my implementation of user service",
|
||||
"context": {"code_path": "/src/services/user.py"}
|
||||
})
|
||||
```
|
||||
|
||||
## Shared MCP Tools
|
||||
|
||||
All agents have access to the same set of MCP tools:
|
||||
|
||||
### Configuration
|
||||
|
||||
```yaml
|
||||
swarm:
|
||||
shared_mcps:
|
||||
github:
|
||||
enabled: true
|
||||
token: "${GITHUB_TOKEN}"
|
||||
linear:
|
||||
enabled: true
|
||||
apiKey: "${LINEAR_API_KEY}"
|
||||
slack:
|
||||
enabled: true
|
||||
token: "${SLACK_TOKEN}"
|
||||
playwright:
|
||||
enabled: true
|
||||
custom:
|
||||
- name: "postgres"
|
||||
type: "stdio"
|
||||
command: "npx"
|
||||
args: ["-y", "@modelcontextprotocol/server-postgres"]
|
||||
```
|
||||
|
||||
### Available to All Agents
|
||||
|
||||
1. **Standard Hanzo Tools**:
|
||||
- `read_file` - Read files
|
||||
- `write_file` - Write files
|
||||
- `search` - Search in codebase
|
||||
- `bash` - Execute commands
|
||||
|
||||
2. **GitHub MCP** (when enabled):
|
||||
- Create/update issues
|
||||
- Create/review PRs
|
||||
- Manage repositories
|
||||
|
||||
3. **Linear MCP** (when enabled):
|
||||
- Create/update tasks
|
||||
- Manage projects
|
||||
- Track progress
|
||||
|
||||
4. **Slack MCP** (when enabled):
|
||||
- Send messages
|
||||
- Create channels
|
||||
- Post updates
|
||||
|
||||
5. **Playwright MCP** (when enabled):
|
||||
- Navigate web pages
|
||||
- Click elements
|
||||
- Fill forms
|
||||
- Take screenshots
|
||||
|
||||
## Recursion and Networking
|
||||
|
||||
### Recursive Calls
|
||||
|
||||
Agents can call each other recursively:
|
||||
|
||||
```
|
||||
coordinator -> architect -> developer -> reviewer
|
||||
↓ ↓ ↓
|
||||
tester architect developer
|
||||
```
|
||||
|
||||
### Network Effects
|
||||
|
||||
- Every agent sees every other agent
|
||||
- No explicit connection configuration needed
|
||||
- Natural delegation through tool calls
|
||||
- Recursion depth limits prevent infinite loops
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Simplicity**: One tool per agent, clear mental model
|
||||
2. **Flexibility**: Any agent can call any other agent
|
||||
3. **Extensibility**: Easy to add new agents or MCP servers
|
||||
4. **Cost Efficiency**: Hanzo Zen orchestrates locally
|
||||
5. **Tool Sharing**: All agents access the same MCP tools
|
||||
|
||||
## Example Flow
|
||||
|
||||
```yaml
|
||||
# User request to coordinator
|
||||
"Create a GitHub issue and implement the fix"
|
||||
|
||||
# Coordinator uses tools:
|
||||
1. github: Create issue #123
|
||||
2. architect: "Design solution for issue #123"
|
||||
|
||||
# Architect uses tools:
|
||||
- read_file: Analyze current code
|
||||
- developer: "Implement fix for issue #123"
|
||||
|
||||
# Developer uses tools:
|
||||
- write_file: Implement changes
|
||||
- bash: Run tests
|
||||
- reviewer: "Review my fix"
|
||||
|
||||
# Reviewer uses tools:
|
||||
- read_file: Review changes
|
||||
- github: Comment on PR
|
||||
|
||||
3. github: Create PR with fix
|
||||
4. slack: Notify team about completion
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
Each agent receives:
|
||||
1. Access to all other agents as tools
|
||||
2. Access to all shared MCP servers
|
||||
3. Access to standard Hanzo tools
|
||||
4. Their own prompt and context
|
||||
|
||||
The orchestrator (Hanzo Zen) manages:
|
||||
1. Tool routing between agents
|
||||
2. Recursion depth tracking
|
||||
3. Cost optimization (local vs API calls)
|
||||
4. Result aggregation
|
||||
@@ -0,0 +1,111 @@
|
||||
version: 1
|
||||
swarm:
|
||||
name: "Agent Chat Demo"
|
||||
main: "coordinator"
|
||||
instances:
|
||||
coordinator:
|
||||
description: "Coordinator that gathers information from all team members"
|
||||
directory: "."
|
||||
model: "opus"
|
||||
expose_as_mcp: true
|
||||
connect_to_agents: ["analyst", "developer", "tester"]
|
||||
vibe: true
|
||||
prompt: |
|
||||
# Coordinator Agent
|
||||
|
||||
You coordinate information gathering and decision making.
|
||||
|
||||
When given a task:
|
||||
1. First check the status of all team members
|
||||
2. Ask relevant questions to gather information
|
||||
3. Have conversations to clarify requirements
|
||||
4. Synthesize responses into a cohesive plan
|
||||
|
||||
Use these MCP tools actively:
|
||||
- get_[agent]_status to check availability
|
||||
- ask_[agent] for specific information
|
||||
- chat_with_[agent] for discussions
|
||||
- request_[agent]_expertise for deep knowledge
|
||||
|
||||
Always engage in dialogue before making decisions.
|
||||
allowed_tools: ["Read", "chat_with_*", "ask_*", "get_*_status", "request_*_expertise"]
|
||||
|
||||
analyst:
|
||||
description: "Business analyst who understands requirements and constraints"
|
||||
directory: "./docs"
|
||||
model: "sonnet"
|
||||
expose_as_mcp: true
|
||||
connect_to_agents: ["coordinator", "developer", "tester"]
|
||||
prompt: |
|
||||
# Business Analyst
|
||||
|
||||
You analyze requirements and provide business context.
|
||||
|
||||
When asked:
|
||||
- Provide clear requirement definitions
|
||||
- Explain business constraints
|
||||
- Suggest user stories
|
||||
- Clarify acceptance criteria
|
||||
|
||||
Be conversational and ask clarifying questions.
|
||||
allowed_tools: ["Read", "Write", "chat_with_*", "ask_*"]
|
||||
|
||||
developer:
|
||||
description: "Senior developer who implements solutions"
|
||||
directory: "./src"
|
||||
model: "sonnet"
|
||||
expose_as_mcp: true
|
||||
connect_to_agents: ["coordinator", "analyst", "tester"]
|
||||
prompt: |
|
||||
# Senior Developer
|
||||
|
||||
You implement technical solutions.
|
||||
|
||||
When engaged:
|
||||
- Discuss technical feasibility
|
||||
- Suggest implementation approaches
|
||||
- Estimate effort and complexity
|
||||
- Identify technical risks
|
||||
|
||||
Chat with analyst for requirements clarification.
|
||||
Coordinate with tester for testability.
|
||||
allowed_tools: ["Read", "Edit", "Write", "Bash", "chat_with_*", "ask_*"]
|
||||
|
||||
tester:
|
||||
description: "QA engineer ensuring quality and test coverage"
|
||||
directory: "./tests"
|
||||
model: "haiku"
|
||||
expose_as_mcp: true
|
||||
connect_to_agents: ["coordinator", "analyst", "developer"]
|
||||
prompt: |
|
||||
# QA Engineer
|
||||
|
||||
You ensure quality through testing.
|
||||
|
||||
In conversations:
|
||||
- Suggest test scenarios
|
||||
- Identify edge cases
|
||||
- Recommend testing approaches
|
||||
- Point out potential issues
|
||||
|
||||
Ask developer about implementation details.
|
||||
Clarify requirements with analyst.
|
||||
allowed_tools: ["Read", "Edit", "Write", "Bash", "chat_with_*", "ask_*"]
|
||||
|
||||
networks:
|
||||
all_connected:
|
||||
name: "Fully Connected Team"
|
||||
agents: ["coordinator", "analyst", "developer", "tester"]
|
||||
mcp_enabled: true
|
||||
shared_tools: ["Read"]
|
||||
|
||||
# Example task to demonstrate chat capabilities:
|
||||
# dev swarm run "Plan a user authentication feature" -c agent-chat-demo.yaml
|
||||
#
|
||||
# The coordinator will:
|
||||
# 1. Check status of all team members
|
||||
# 2. Ask analyst about requirements
|
||||
# 3. Chat with developer about implementation
|
||||
# 4. Discuss testing strategy with tester
|
||||
# 5. Have follow-up conversations as needed
|
||||
# 6. Synthesize everything into a plan
|
||||
@@ -290,6 +290,7 @@
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/inquirer": "^9.0.8",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/mocha": "^10.0.10",
|
||||
@@ -323,6 +324,7 @@
|
||||
"@typescript-eslint/typescript-estree": "^8.35.1",
|
||||
"axios": "^1.6.2",
|
||||
"ignore": "^7.0.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxdb": "^16.15.0",
|
||||
|
||||
Generated
+890
-103
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
playwright-report/
|
||||
test-results/
|
||||
|
||||
# Production
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
# Hanzo.app - AI Development Platform Gateway
|
||||
|
||||
Central hub for downloading and accessing Hanzo AI across all platforms.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
# Install dependencies (using pnpm)
|
||||
pnpm install
|
||||
|
||||
# Start development server
|
||||
pnpm dev
|
||||
|
||||
# Build for production
|
||||
pnpm build
|
||||
|
||||
# Preview production build
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
## 📦 Build Output
|
||||
|
||||
When you run `pnpm build`, the static files are generated in the `dist` directory:
|
||||
|
||||
```
|
||||
dist/
|
||||
├── index.html # Main landing page
|
||||
└── src/
|
||||
└── dev.html # Dev CLI landing page
|
||||
```
|
||||
|
||||
## 📱 Available Platforms
|
||||
|
||||
- **Desktop Apps**: Windows, macOS, Linux
|
||||
- **Mobile Apps**: iOS, Android
|
||||
- **Browser Extensions**: Chrome, Firefox, Edge, Safari
|
||||
- **IDE Extensions**: VS Code, JetBrains
|
||||
- **CLI Tools**: Dev CLI (`@hanzo/dev`), MCP Server (`@hanzo/mcp`)
|
||||
- **Cloud Platform**: cloud.hanzo.ai
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 18+
|
||||
- pnpm (install with `npm install -g pnpm`)
|
||||
|
||||
### Available Scripts
|
||||
|
||||
```bash
|
||||
pnpm dev # Start dev server on port 3000
|
||||
pnpm build # Build static site to dist/
|
||||
pnpm preview # Preview built site on port 3001
|
||||
pnpm clean # Clean dist directory
|
||||
pnpm clean:all # Clean everything (dist, node_modules, lock file)
|
||||
pnpm test # Run Playwright tests
|
||||
pnpm test:ui # Run tests with UI
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Install Playwright browsers (first time only)
|
||||
pnpm exec playwright install chromium
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
```
|
||||
|
||||
### Deployment
|
||||
|
||||
The site can be deployed to any static hosting service. The `dist` directory contains all the files needed.
|
||||
|
||||
#### Vercel
|
||||
```bash
|
||||
pnpm build
|
||||
vercel --prod ./dist
|
||||
```
|
||||
|
||||
#### Netlify
|
||||
```bash
|
||||
pnpm build
|
||||
netlify deploy --dir=dist --prod
|
||||
```
|
||||
|
||||
#### GitHub Pages
|
||||
```bash
|
||||
pnpm build
|
||||
# Push dist directory to gh-pages branch
|
||||
```
|
||||
|
||||
## 📄 Pages
|
||||
|
||||
- `/` - Main download hub for all Hanzo products
|
||||
- `/src/dev.html` - Dev CLI specific landing page
|
||||
|
||||
## 🎨 Design
|
||||
|
||||
- Monochromatic black and white theme
|
||||
- Hanzo logo integrated in header
|
||||
- Responsive grid layout
|
||||
- Smooth animations on scroll
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Hanzo AI](https://hanzo.ai)
|
||||
- [Documentation](https://docs.hanzo.ai)
|
||||
- [GitHub](https://github.com/hanzoai)
|
||||
- [Discord](https://discord.gg/hanzoai)
|
||||
@@ -8,12 +8,12 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--primary: #ff0080;
|
||||
--secondary: #7928ca;
|
||||
--primary: #fff;
|
||||
--secondary: #fff;
|
||||
--dark: #000;
|
||||
--darker: #0a0a0a;
|
||||
--gray: #888;
|
||||
--light-gray: #ccc;
|
||||
--gray: #666;
|
||||
--light-gray: #aaa;
|
||||
--border: #222;
|
||||
}
|
||||
|
||||
@@ -48,11 +48,22 @@
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 32px;
|
||||
width: auto;
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: #fff;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
@@ -74,16 +85,14 @@
|
||||
.hero {
|
||||
padding: 120px 0 80px;
|
||||
text-align: center;
|
||||
background: radial-gradient(circle at 50% 0%, #1a0033 0%, var(--dark) 50%);
|
||||
background: radial-gradient(circle at 50% 0%, #111 0%, var(--dark) 70%);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 64px;
|
||||
font-weight: 900;
|
||||
margin-bottom: 20px;
|
||||
background: linear-gradient(135deg, #fff, var(--gray));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: #fff;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
@@ -123,8 +132,8 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||||
height: 1px;
|
||||
background: #fff;
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
@@ -178,13 +187,15 @@
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||||
color: white;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 30px rgba(255, 0, 128, 0.3);
|
||||
box-shadow: 0 10px 30px rgba(255, 255, 255, 0.2);
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@@ -260,13 +271,16 @@
|
||||
background: var(--darker);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 25px;
|
||||
margin: 30px 0;
|
||||
padding: 30px;
|
||||
margin: 40px auto;
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
line-height: 1.6;
|
||||
color: #fff;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.code-block::before {
|
||||
@@ -284,7 +298,7 @@
|
||||
}
|
||||
|
||||
.code-string {
|
||||
color: #0ff;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@@ -364,7 +378,10 @@
|
||||
<body>
|
||||
<header>
|
||||
<nav class="container">
|
||||
<div class="logo">Hanzo</div>
|
||||
<div class="logo">
|
||||
<img src="https://hanzo.ai/logo.svg" alt="Hanzo Logo" />
|
||||
<span class="logo-text">Hanzo</span>
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<a href="https://docs.hanzo.ai">Docs</a>
|
||||
<a href="https://github.com/hanzoai">GitHub</a>
|
||||
@@ -376,7 +393,7 @@
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h1>AI Development<br>Everywhere You Code</h1>
|
||||
<p class="subtitle">Access Hanzo's parallel AI agents from desktop, mobile, browser, IDE, or command line. One platform, endless possibilities.</p>
|
||||
<p class="subtitle">Access Hanzo's parallel AI agents from desktop, mobile, browser, IDE, or command line. Powered by Hanzo Zen for cost-effective local orchestration.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -561,16 +578,16 @@
|
||||
<p class="section-subtitle">Choose your preferred method and start shipping faster</p>
|
||||
|
||||
<div class="code-block" data-lang="bash">
|
||||
<span class="code-comment"># Option 1: CLI (Recommended)</span>
|
||||
npm install -g @hanzo/dev
|
||||
dev login
|
||||
dev enhance <span class="code-string">"add user authentication"</span>
|
||||
|
||||
<span class="code-comment"># Option 2: MCP Server for Claude Desktop</span>
|
||||
npm install -g @hanzo/mcp
|
||||
npx @hanzo/mcp configure
|
||||
|
||||
<span class="code-comment"># Option 3: Cloud Platform</span>
|
||||
<span class="code-comment"># Option 1: CLI (Recommended)</span><br>
|
||||
npm install -g @hanzo/dev<br>
|
||||
dev login<br>
|
||||
dev enhance <span class="code-string">"add user authentication"</span><br>
|
||||
<br>
|
||||
<span class="code-comment"># Option 2: MCP Server for Claude Desktop</span><br>
|
||||
npm install -g @hanzo/mcp<br>
|
||||
npx @hanzo/mcp configure<br>
|
||||
<br>
|
||||
<span class="code-comment"># Option 3: Cloud Platform</span><br>
|
||||
<span class="code-comment"># Visit cloud.hanzo.ai and sign up</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -613,6 +630,18 @@
|
||||
<p class="feature-desc">File ops, search, browser automation, and more</p>
|
||||
</div>
|
||||
|
||||
<div class="feature">
|
||||
<span class="feature-icon">💰</span>
|
||||
<h3 class="feature-title">Hanzo Zen Local AI</h3>
|
||||
<p class="feature-desc">Cost-effective local MoE model for orchestration. Run on laptop or mobile.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature">
|
||||
<span class="feature-icon">🌐</span>
|
||||
<h3 class="feature-title">Peer Agent Networks</h3>
|
||||
<p class="feature-desc">All agents can recursively call each other with full MCP support</p>
|
||||
</div>
|
||||
|
||||
<div class="feature">
|
||||
<span class="feature-icon">👥</span>
|
||||
<h3 class="feature-title">Team Collaboration</h3>
|
||||
@@ -622,6 +651,46 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="feature-section" style="background: var(--darker);">
|
||||
<div class="container">
|
||||
<h2 class="section-title">Cost-Effective AI Orchestration</h2>
|
||||
<p class="section-subtitle">Hanzo Zen: Local MoE model for intelligent agent coordination</p>
|
||||
|
||||
<div style="max-width: 800px; margin: 50px auto; text-align: center;">
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 30px; margin-top: 40px;">
|
||||
<div style="padding: 30px; background: var(--dark); border-radius: 12px; border: 1px solid var(--border);">
|
||||
<h3 style="font-size: 48px; margin-bottom: 10px; color: #fff;">90%</h3>
|
||||
<p style="color: var(--gray);">Cost reduction using local Zen for orchestration</p>
|
||||
</div>
|
||||
<div style="padding: 30px; background: var(--dark); border-radius: 12px; border: 1px solid var(--border);">
|
||||
<h3 style="font-size: 48px; margin-bottom: 10px; color: #fff;">∞</h3>
|
||||
<p style="color: var(--gray);">Recursive agent calls with full MCP support</p>
|
||||
</div>
|
||||
<div style="padding: 30px; background: var(--dark); border-radius: 12px; border: 1px solid var(--border);">
|
||||
<h3 style="font-size: 48px; margin-bottom: 10px; color: #fff;">1B-1T</h3>
|
||||
<p style="color: var(--gray);">Scalable from edge to exascale deployment</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-block" data-lang="yaml" style="margin-top: 40px;">
|
||||
<span class="code-comment"># Peer network with Hanzo Zen orchestration</span><br>
|
||||
swarm:<br>
|
||||
network_type: peer<br>
|
||||
local_llm:<br>
|
||||
model: hanzo-zen<br>
|
||||
endpoint: http://localhost:8080<br>
|
||||
instances:<br>
|
||||
orchestrator:<br>
|
||||
model: zen <span class="code-comment"># Local, cost-effective</span><br>
|
||||
connect_to_agents: [architect, developer, reviewer]<br>
|
||||
architect:<br>
|
||||
model: opus <span class="code-comment"># API-based for complex tasks</span><br>
|
||||
expose_as_mcp: true
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "hanzo-app",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "Hanzo AI Platform Gateway - Download hub for all platforms",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --port 3001",
|
||||
"clean": "rm -rf dist",
|
||||
"clean:all": "rm -rf dist node_modules pnpm-lock.yaml",
|
||||
"serve": "vite preview",
|
||||
"test": "playwright test",
|
||||
"test:ui": "playwright test --ui",
|
||||
"test:headed": "playwright test --headed"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.54.1",
|
||||
"terser": "^5.43.1",
|
||||
"vite": "^5.4.19"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
use: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
port: 3000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
@@ -37,26 +37,35 @@
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(135deg, #ff0080, #7928ca);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: 28px;
|
||||
width: auto;
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 150px 0 100px;
|
||||
text-align: center;
|
||||
background: radial-gradient(circle at center, #1a0033 0%, #000 100%);
|
||||
background: radial-gradient(circle at center, #111 0%, #000 70%);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 72px;
|
||||
font-weight: 900;
|
||||
margin-bottom: 20px;
|
||||
background: linear-gradient(135deg, #fff, #888);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: #fff;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
@@ -85,13 +94,15 @@
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #ff0080, #7928ca);
|
||||
color: white;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 30px rgba(255, 0, 128, 0.3);
|
||||
box-shadow: 0 10px 30px rgba(255, 255, 255, 0.2);
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@@ -128,19 +139,19 @@
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.dot-red { background: #ff5f56; }
|
||||
.dot-yellow { background: #ffbd2e; }
|
||||
.dot-green { background: #27c93f; }
|
||||
.dot-red { background: #666; }
|
||||
.dot-yellow { background: #888; }
|
||||
.dot-green { background: #aaa; }
|
||||
|
||||
.terminal-content {
|
||||
color: #0f0;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.terminal-prompt { color: #888; }
|
||||
.terminal-prompt { color: #666; }
|
||||
.terminal-command { color: #fff; }
|
||||
.terminal-output { color: #0ff; margin-left: 20px; }
|
||||
.terminal-output { color: #aaa; margin-left: 20px; }
|
||||
|
||||
.features {
|
||||
padding: 100px 0;
|
||||
@@ -264,7 +275,8 @@
|
||||
top: -15px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: linear-gradient(135deg, #ff0080, #7928ca);
|
||||
background: #fff;
|
||||
color: #000;
|
||||
padding: 5px 20px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
@@ -328,7 +340,10 @@
|
||||
<body>
|
||||
<header>
|
||||
<nav class="container">
|
||||
<div class="logo">Dev</div>
|
||||
<div class="logo">
|
||||
<img src="https://hanzo.ai/logo.svg" alt="Hanzo Logo" />
|
||||
<span class="logo-text">Dev</span>
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://cloud.hanzo.ai/login" class="btn btn-secondary">Login</a>
|
||||
</div>
|
||||
@@ -0,0 +1,133 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Hanzo.app Landing Page', () => {
|
||||
test('has correct title and heading', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Check title
|
||||
await expect(page).toHaveTitle('Hanzo - AI Development Platform for Every Device');
|
||||
|
||||
// Check main heading
|
||||
const heading = page.locator('h1');
|
||||
await expect(heading).toContainText('AI Development');
|
||||
await expect(heading).toContainText('Everywhere You Code');
|
||||
});
|
||||
|
||||
test('displays all platform download cards', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Check all download cards are present
|
||||
const downloadCards = page.locator('.download-card');
|
||||
await expect(downloadCards).toHaveCount(9);
|
||||
|
||||
// Check specific platforms
|
||||
await expect(page.locator('text=Desktop App')).toBeVisible();
|
||||
await expect(page.locator('text=Mobile Apps')).toBeVisible();
|
||||
await expect(page.locator('text=Browser Extension')).toBeVisible();
|
||||
await expect(page.locator('text=VS Code Extension')).toBeVisible();
|
||||
await expect(page.locator('text=JetBrains Plugin')).toBeVisible();
|
||||
await expect(page.locator('text=Dev CLI')).toBeVisible();
|
||||
await expect(page.locator('text=MCP Server')).toBeVisible();
|
||||
await expect(page.locator('text=Cloud Platform')).toBeVisible();
|
||||
});
|
||||
|
||||
test('copy command buttons work', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Test CLI copy button
|
||||
const cliButton = page.locator('button:has-text("npm install -g @hanzo/dev")');
|
||||
await cliButton.click();
|
||||
|
||||
// Check clipboard was written (button text changes)
|
||||
await expect(cliButton).toContainText('Copied!');
|
||||
|
||||
// Wait for button to reset
|
||||
await page.waitForTimeout(2500);
|
||||
await expect(cliButton).toContainText('npm install -g @hanzo/dev');
|
||||
});
|
||||
|
||||
test('navigation links work', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Check header links
|
||||
await expect(page.locator('a[href="https://docs.hanzo.ai"]')).toBeVisible();
|
||||
await expect(page.locator('a[href="https://github.com/hanzoai"]')).toBeVisible();
|
||||
await expect(page.locator('a[href="https://cloud.hanzo.ai/login"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('quick start section displays correctly', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Check quick start section
|
||||
await expect(page.locator('text=Quick Start in 30 Seconds')).toBeVisible();
|
||||
|
||||
// Check code block
|
||||
const codeBlock = page.locator('.code-block');
|
||||
await expect(codeBlock).toBeVisible();
|
||||
await expect(codeBlock).toContainText('npm install -g @hanzo/dev');
|
||||
await expect(codeBlock).toContainText('dev login');
|
||||
});
|
||||
|
||||
test('responsive design works', async ({ page }) => {
|
||||
// Test mobile viewport
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.goto('/');
|
||||
|
||||
// Check mobile menu behavior
|
||||
const navLinks = page.locator('.nav-links');
|
||||
await expect(navLinks).toBeHidden();
|
||||
|
||||
// Check cards stack on mobile
|
||||
const downloadGrid = page.locator('.download-grid');
|
||||
await expect(downloadGrid).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Dev Landing Page', () => {
|
||||
test('has correct content', async ({ page }) => {
|
||||
await page.goto('/src/dev.html');
|
||||
|
||||
// Check title
|
||||
await expect(page).toHaveTitle('Dev - Ship 100X Faster with Parallel AI Agents');
|
||||
|
||||
// Check main heading
|
||||
await expect(page.locator('h1')).toContainText('Ship 100X Faster');
|
||||
|
||||
// Check demo terminal
|
||||
const terminal = page.locator('.demo-terminal');
|
||||
await expect(terminal).toBeVisible();
|
||||
await expect(terminal).toContainText('dev enhance');
|
||||
});
|
||||
|
||||
test('pricing cards are displayed', async ({ page }) => {
|
||||
await page.goto('/src/dev.html');
|
||||
|
||||
// Check pricing cards
|
||||
const pricingCards = page.locator('.pricing-card');
|
||||
await expect(pricingCards).toHaveCount(3);
|
||||
|
||||
// Check prices
|
||||
await expect(page.locator('text=$0/month')).toBeVisible();
|
||||
await expect(page.locator('text=$49/month')).toBeVisible();
|
||||
await expect(page.locator('text=$199/month')).toBeVisible();
|
||||
});
|
||||
|
||||
test('subscribe buttons redirect correctly', async ({ page, context }) => {
|
||||
await page.goto('/src/dev.html');
|
||||
|
||||
// Listen for new pages (popups/tabs)
|
||||
const pagePromise = context.waitForEvent('page');
|
||||
|
||||
// Click subscribe button
|
||||
await page.locator('button:has-text("Start Pro Trial")').click();
|
||||
|
||||
// Get the new page
|
||||
const newPage = await pagePromise;
|
||||
await newPage.waitForLoadState();
|
||||
|
||||
// Check URL
|
||||
expect(newPage.url()).toContain('cloud.hanzo.ai/signup');
|
||||
expect(newPage.url()).toContain('plan=pro');
|
||||
expect(newPage.url()).toContain('product=dev');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
root: '.',
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: './index.html',
|
||||
dev: './src/dev.html'
|
||||
}
|
||||
},
|
||||
// Optimize for production
|
||||
minify: 'terser',
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: true,
|
||||
drop_debugger: true
|
||||
}
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
host: true
|
||||
},
|
||||
preview: {
|
||||
port: 3001,
|
||||
open: true
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,625 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as yaml from 'js-yaml';
|
||||
|
||||
export interface MCPServerConfig {
|
||||
name: string;
|
||||
type: 'stdio' | 'http';
|
||||
command: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface SharedMCPConfig {
|
||||
github?: {
|
||||
enabled: boolean;
|
||||
token?: string;
|
||||
};
|
||||
linear?: {
|
||||
enabled: boolean;
|
||||
apiKey?: string;
|
||||
};
|
||||
slack?: {
|
||||
enabled: boolean;
|
||||
token?: string;
|
||||
};
|
||||
playwright?: {
|
||||
enabled: boolean;
|
||||
headless?: boolean;
|
||||
};
|
||||
custom?: MCPServerConfig[];
|
||||
}
|
||||
|
||||
export interface AgentInstance {
|
||||
description: string;
|
||||
directory: string;
|
||||
model: string;
|
||||
connections?: string[];
|
||||
vibe?: boolean;
|
||||
prompt: string;
|
||||
allowed_tools?: string[];
|
||||
mcps?: MCPServerConfig[];
|
||||
expose_as_mcp?: boolean; // Expose this agent as an MCP server to other agents
|
||||
mcp_port?: number; // Port for MCP server if exposed
|
||||
connect_to_agents?: string[]; // Other agents to connect to via MCP
|
||||
}
|
||||
|
||||
export interface NetworkConfig {
|
||||
name: string;
|
||||
agents: string[];
|
||||
mcp_enabled?: boolean; // Enable MCP connections between all agents in network
|
||||
shared_tools?: string[]; // Tools available to all agents in network
|
||||
shared_mcps?: MCPServerConfig[]; // MCP servers available to all agents
|
||||
}
|
||||
|
||||
export interface AgentSwarmConfig {
|
||||
version: number;
|
||||
swarm: {
|
||||
name: string;
|
||||
main: string;
|
||||
instances: Record<string, AgentInstance>;
|
||||
networks?: Record<string, NetworkConfig>; // Named networks of agents
|
||||
shared_mcps?: SharedMCPConfig; // Shared MCP servers for all agents
|
||||
};
|
||||
}
|
||||
|
||||
export class AgentSwarmManager {
|
||||
private config: AgentSwarmConfig | null = null;
|
||||
private configPath: string;
|
||||
|
||||
constructor(configPath?: string) {
|
||||
this.configPath = configPath || path.join(process.cwd(), '.hanzo', 'agents.yaml');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load agent configuration from YAML file
|
||||
*/
|
||||
async loadConfig(): Promise<AgentSwarmConfig> {
|
||||
try {
|
||||
// Check multiple possible locations
|
||||
const possiblePaths = [
|
||||
this.configPath,
|
||||
path.join(process.cwd(), 'agents.yaml'),
|
||||
path.join(process.cwd(), '.agents.yaml'),
|
||||
path.join(process.cwd(), 'config', 'agents.yaml')
|
||||
];
|
||||
|
||||
let configContent: string | null = null;
|
||||
let foundPath: string | null = null;
|
||||
|
||||
for (const configPath of possiblePaths) {
|
||||
if (fs.existsSync(configPath)) {
|
||||
configContent = fs.readFileSync(configPath, 'utf8');
|
||||
foundPath = configPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!configContent || !foundPath) {
|
||||
throw new Error(`No agent configuration found. Searched paths: ${possiblePaths.join(', ')}`);
|
||||
}
|
||||
|
||||
this.config = yaml.load(configContent) as AgentSwarmConfig;
|
||||
console.log(`Loaded agent configuration from: ${foundPath}`);
|
||||
|
||||
// Validate configuration
|
||||
this.validateConfig(this.config);
|
||||
|
||||
return this.config;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load agent configuration: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the agent configuration
|
||||
*/
|
||||
private validateConfig(config: AgentSwarmConfig): void {
|
||||
if (!config.version || config.version !== 1) {
|
||||
throw new Error('Invalid or missing version in agent configuration');
|
||||
}
|
||||
|
||||
if (!config.swarm || !config.swarm.name || !config.swarm.main) {
|
||||
throw new Error('Invalid swarm configuration: missing name or main agent');
|
||||
}
|
||||
|
||||
if (!config.swarm.instances || Object.keys(config.swarm.instances).length === 0) {
|
||||
throw new Error('No agent instances defined');
|
||||
}
|
||||
|
||||
// Validate main agent exists
|
||||
if (!config.swarm.instances[config.swarm.main]) {
|
||||
throw new Error(`Main agent '${config.swarm.main}' not found in instances`);
|
||||
}
|
||||
|
||||
// Validate each instance
|
||||
for (const [name, instance] of Object.entries(config.swarm.instances)) {
|
||||
if (!instance.description || !instance.directory || !instance.model || !instance.prompt) {
|
||||
throw new Error(`Agent '${name}' missing required fields (description, directory, model, prompt)`);
|
||||
}
|
||||
|
||||
// Validate connections reference existing agents
|
||||
if (instance.connections) {
|
||||
for (const connection of instance.connections) {
|
||||
if (!config.swarm.instances[connection]) {
|
||||
throw new Error(`Agent '${name}' has invalid connection to '${connection}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate MCP servers
|
||||
if (instance.mcps) {
|
||||
for (const mcp of instance.mcps) {
|
||||
if (!mcp.name || !mcp.type || !mcp.command) {
|
||||
throw new Error(`Invalid MCP server configuration in agent '${name}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the main agent instance
|
||||
*/
|
||||
getMainAgent(): AgentInstance | null {
|
||||
if (!this.config) return null;
|
||||
return this.config.swarm.instances[this.config.swarm.main];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific agent instance
|
||||
*/
|
||||
getAgent(name: string): AgentInstance | null {
|
||||
if (!this.config) return null;
|
||||
return this.config.swarm.instances[name] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all agent instances
|
||||
*/
|
||||
getAllAgents(): Record<string, AgentInstance> {
|
||||
if (!this.config) return {};
|
||||
return this.config.swarm.instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full configuration
|
||||
*/
|
||||
getConfig(): AgentSwarmConfig | null {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agents connected to a specific agent
|
||||
*/
|
||||
getConnectedAgents(agentName: string): AgentInstance[] {
|
||||
if (!this.config) return [];
|
||||
|
||||
const agent = this.getAgent(agentName);
|
||||
if (!agent || !agent.connections) return [];
|
||||
|
||||
return agent.connections
|
||||
.map(name => this.getAgent(name))
|
||||
.filter((agent): agent is AgentInstance => agent !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get network configuration by name
|
||||
*/
|
||||
getNetwork(networkName: string): NetworkConfig | null {
|
||||
if (!this.config || !this.config.swarm.networks) return null;
|
||||
return this.config.swarm.networks[networkName] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all agents in a network
|
||||
*/
|
||||
getNetworkAgents(networkName: string): AgentInstance[] {
|
||||
const network = this.getNetwork(networkName);
|
||||
if (!network) return [];
|
||||
|
||||
return network.agents
|
||||
.map(name => this.getAgent(name))
|
||||
.filter((agent): agent is AgentInstance => agent !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all networks an agent belongs to
|
||||
*/
|
||||
getAgentNetworks(agentName: string): NetworkConfig[] {
|
||||
if (!this.config || !this.config.swarm.networks) return [];
|
||||
|
||||
return Object.values(this.config.swarm.networks)
|
||||
.filter(network => network.agents.includes(agentName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply network configuration to agents
|
||||
*/
|
||||
applyNetworkConfig(agent: AgentInstance, agentName: string): AgentInstance {
|
||||
const networks = this.getAgentNetworks(agentName);
|
||||
if (networks.length === 0) return agent;
|
||||
|
||||
// Clone agent to avoid mutation
|
||||
const configuredAgent = { ...agent };
|
||||
|
||||
for (const network of networks) {
|
||||
// Add shared tools
|
||||
if (network.shared_tools) {
|
||||
configuredAgent.allowed_tools = [
|
||||
...(configuredAgent.allowed_tools || []),
|
||||
...network.shared_tools
|
||||
];
|
||||
}
|
||||
|
||||
// Add shared MCP servers
|
||||
if (network.shared_mcps) {
|
||||
configuredAgent.mcps = [
|
||||
...(configuredAgent.mcps || []),
|
||||
...network.shared_mcps
|
||||
];
|
||||
}
|
||||
|
||||
// Add MCP connections to other agents in network
|
||||
if (network.mcp_enabled) {
|
||||
const otherAgents = network.agents.filter(a => a !== agentName);
|
||||
configuredAgent.connect_to_agents = [
|
||||
...(configuredAgent.connect_to_agents || []),
|
||||
...otherAgents
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
if (configuredAgent.allowed_tools) {
|
||||
configuredAgent.allowed_tools = [...new Set(configuredAgent.allowed_tools)];
|
||||
}
|
||||
if (configuredAgent.connect_to_agents) {
|
||||
configuredAgent.connect_to_agents = [...new Set(configuredAgent.connect_to_agents)];
|
||||
}
|
||||
|
||||
return configuredAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sample agent configuration
|
||||
*/
|
||||
static createSampleConfig(): string {
|
||||
const sampleConfig = {
|
||||
version: 1,
|
||||
swarm: {
|
||||
name: "Development Team",
|
||||
main: "architect",
|
||||
instances: {
|
||||
architect: {
|
||||
description: "Lead architect coordinating development",
|
||||
directory: ".",
|
||||
model: "opus",
|
||||
connections: ["frontend", "backend", "tests", "devops"],
|
||||
vibe: true,
|
||||
expose_as_mcp: true,
|
||||
mcp_port: 8001,
|
||||
prompt: `# Lead Architect Agent
|
||||
|
||||
You are the lead architect coordinating development. Your role is to:
|
||||
|
||||
1. Understand requirements and break them down
|
||||
2. Delegate work to appropriate specialists
|
||||
3. Ensure best practices across the team
|
||||
4. Maintain coherent system design
|
||||
|
||||
## Your Team
|
||||
- Frontend: UI/UX implementation
|
||||
- Backend: API and business logic
|
||||
- Tests: Test coverage and quality
|
||||
- DevOps: Infrastructure and deployment
|
||||
|
||||
Always coordinate effectively and synthesize work into cohesive solutions.`,
|
||||
mcps: [
|
||||
{
|
||||
name: "filesystem",
|
||||
type: "stdio",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-filesystem"],
|
||||
env: {
|
||||
ALLOWED_DIRECTORIES: "."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
frontend: {
|
||||
description: "Frontend UI/UX specialist",
|
||||
directory: "./src/frontend",
|
||||
model: "sonnet",
|
||||
expose_as_mcp: true,
|
||||
mcp_port: 8002,
|
||||
allowed_tools: ["Read", "Edit", "Write", "Grep"],
|
||||
prompt: `# Frontend Specialist
|
||||
|
||||
You are a frontend specialist focusing on:
|
||||
- Component architecture
|
||||
- User experience
|
||||
- Responsive design
|
||||
- Performance optimization
|
||||
|
||||
Work closely with the architect to implement UI features.`
|
||||
},
|
||||
backend: {
|
||||
description: "Backend API specialist",
|
||||
directory: "./src/backend",
|
||||
model: "sonnet",
|
||||
expose_as_mcp: true,
|
||||
mcp_port: 8003,
|
||||
allowed_tools: ["Read", "Edit", "Write", "Bash"],
|
||||
prompt: `# Backend Specialist
|
||||
|
||||
You are a backend specialist focusing on:
|
||||
- API design and implementation
|
||||
- Database optimization
|
||||
- Security best practices
|
||||
- Performance and scalability
|
||||
|
||||
Coordinate with frontend for API contracts.`
|
||||
},
|
||||
tests: {
|
||||
description: "Testing and quality assurance specialist",
|
||||
directory: "./tests",
|
||||
model: "haiku",
|
||||
expose_as_mcp: true,
|
||||
mcp_port: 8004,
|
||||
allowed_tools: ["Read", "Edit", "Write", "Bash"],
|
||||
prompt: `# Testing Specialist
|
||||
|
||||
You are a testing specialist ensuring:
|
||||
- Comprehensive test coverage
|
||||
- Test quality and maintainability
|
||||
- Performance testing
|
||||
- Integration testing
|
||||
|
||||
Write tests for all new features and bug fixes.`
|
||||
},
|
||||
devops: {
|
||||
description: "DevOps and infrastructure specialist",
|
||||
directory: "./infrastructure",
|
||||
model: "haiku",
|
||||
expose_as_mcp: true,
|
||||
mcp_port: 8005,
|
||||
allowed_tools: ["Read", "Edit", "Write", "Bash"],
|
||||
prompt: `# DevOps Specialist
|
||||
|
||||
You handle infrastructure and deployment:
|
||||
- CI/CD pipelines
|
||||
- Docker and containerization
|
||||
- Cloud infrastructure
|
||||
- Monitoring and logging`
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
core_team: {
|
||||
name: "Core Development Team",
|
||||
agents: ["architect", "frontend", "backend", "tests"],
|
||||
mcp_enabled: true,
|
||||
shared_tools: ["Grep", "Read"],
|
||||
shared_mcps: [
|
||||
{
|
||||
name: "project-search",
|
||||
type: "stdio",
|
||||
command: "npx",
|
||||
args: ["-y", "@modelcontextprotocol/server-everything"]
|
||||
}
|
||||
]
|
||||
},
|
||||
deployment_team: {
|
||||
name: "Deployment Team",
|
||||
agents: ["backend", "devops", "tests"],
|
||||
mcp_enabled: true,
|
||||
shared_tools: ["Bash"]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return yaml.dump(sampleConfig, {
|
||||
indent: 2,
|
||||
lineWidth: 80,
|
||||
quotingType: '"',
|
||||
forceQuotes: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new agent configuration file
|
||||
*/
|
||||
static async initConfig(targetPath?: string): Promise<void> {
|
||||
const configPath = targetPath || path.join(process.cwd(), '.hanzo', 'agents.yaml');
|
||||
|
||||
// Create directory if needed
|
||||
const dir = path.dirname(configPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
// Check if config already exists
|
||||
if (fs.existsSync(configPath)) {
|
||||
throw new Error(`Configuration already exists at ${configPath}`);
|
||||
}
|
||||
|
||||
// Write sample config
|
||||
const sampleConfig = AgentSwarmManager.createSampleConfig();
|
||||
fs.writeFileSync(configPath, sampleConfig, 'utf8');
|
||||
|
||||
console.log(`Created agent configuration at: ${configPath}`);
|
||||
console.log('Edit this file to customize your agent swarm configuration.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a peer network configuration with Hanzo Zen
|
||||
*/
|
||||
static async initPeerNetworkConfig(targetPath?: string): Promise<void> {
|
||||
const configPath = targetPath || path.join(process.cwd(), '.hanzo', 'agents-peer.yaml');
|
||||
|
||||
// Create directory if needed
|
||||
const dir = path.dirname(configPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
// Check if config already exists
|
||||
if (fs.existsSync(configPath)) {
|
||||
throw new Error(`Configuration already exists at ${configPath}`);
|
||||
}
|
||||
|
||||
// Write peer network config
|
||||
const peerConfig = AgentSwarmManager.createPeerNetworkConfig();
|
||||
fs.writeFileSync(configPath, peerConfig, 'utf8');
|
||||
|
||||
console.log(`Created peer network configuration at: ${configPath}`);
|
||||
console.log('This configuration uses Hanzo Zen for cost-effective local orchestration.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a peer network configuration
|
||||
*/
|
||||
static createPeerNetworkConfig(): string {
|
||||
const config = {
|
||||
version: 1,
|
||||
swarm: {
|
||||
name: "Peer Agent Network",
|
||||
main: "orchestrator",
|
||||
network_type: "peer",
|
||||
local_llm: {
|
||||
model: "hanzo-zen",
|
||||
endpoint: "http://localhost:8080",
|
||||
description: "Local Hanzo Zen MoE for cost-effective orchestration"
|
||||
},
|
||||
instances: {
|
||||
orchestrator: {
|
||||
description: "Main orchestrator using Hanzo Zen for coordination",
|
||||
directory: ".",
|
||||
model: "zen",
|
||||
expose_as_mcp: true,
|
||||
mcp_port: 10000,
|
||||
connect_to_agents: ["architect", "developer", "reviewer", "tester", "critic"],
|
||||
vibe: true,
|
||||
prompt: `# Orchestrator Agent (Hanzo Zen)
|
||||
|
||||
You are the main orchestrator running on local Hanzo Zen for cost efficiency.
|
||||
Your role is to coordinate all other agents efficiently.
|
||||
|
||||
## Cost Optimization Strategy
|
||||
- Use local inference (yourself) for planning and coordination
|
||||
- Delegate complex tasks to specialized agents with API-based LLMs
|
||||
- Batch similar requests to minimize API calls
|
||||
- Use recursive agent calls wisely
|
||||
|
||||
## Available Agents (All exposed as MCP tools)
|
||||
Every agent can call every other agent. You have tools like:
|
||||
- chat_with_[agent] - Have conversations
|
||||
- ask_[agent] - Quick questions
|
||||
- delegate_to_[agent] - Task delegation
|
||||
- get_[agent]_status - Check availability
|
||||
- request_[agent]_expertise - Deep knowledge
|
||||
|
||||
Coordinate effectively while minimizing costs.`,
|
||||
allowed_tools: ["*"] // Access to all tools
|
||||
},
|
||||
architect: {
|
||||
description: "System architect for design decisions",
|
||||
directory: "./src",
|
||||
model: "opus",
|
||||
expose_as_mcp: true,
|
||||
mcp_port: 10001,
|
||||
connect_to_agents: ["orchestrator", "developer", "reviewer", "tester", "critic"],
|
||||
prompt: `# System Architect
|
||||
|
||||
You make architectural decisions and system design.
|
||||
You can consult any other agent for information.
|
||||
Focus on scalable, maintainable solutions.`,
|
||||
allowed_tools: ["Read", "Write", "Edit", "Grep", "chat_with_*", "ask_*"]
|
||||
},
|
||||
developer: {
|
||||
description: "Senior developer for implementation",
|
||||
directory: "./src",
|
||||
model: "sonnet",
|
||||
expose_as_mcp: true,
|
||||
mcp_port: 10002,
|
||||
connect_to_agents: ["orchestrator", "architect", "reviewer", "tester", "critic"],
|
||||
prompt: `# Senior Developer
|
||||
|
||||
You implement solutions based on architectural decisions.
|
||||
Consult architect for design questions.
|
||||
Ask reviewer for code quality feedback.
|
||||
Coordinate with tester for test coverage.`,
|
||||
allowed_tools: ["Read", "Write", "Edit", "Bash", "chat_with_*", "ask_*", "delegate_to_*"]
|
||||
},
|
||||
reviewer: {
|
||||
description: "Code reviewer for quality assurance",
|
||||
directory: ".",
|
||||
model: "sonnet",
|
||||
expose_as_mcp: true,
|
||||
mcp_port: 10003,
|
||||
connect_to_agents: ["orchestrator", "architect", "developer", "tester", "critic"],
|
||||
prompt: `# Code Reviewer
|
||||
|
||||
You review code for quality, security, and best practices.
|
||||
You can ask developer for clarifications.
|
||||
Consult architect for design compliance.
|
||||
Work with tester on test coverage.`,
|
||||
allowed_tools: ["Read", "Grep", "chat_with_*", "ask_*"]
|
||||
},
|
||||
tester: {
|
||||
description: "QA engineer for comprehensive testing",
|
||||
directory: "./tests",
|
||||
model: "haiku",
|
||||
expose_as_mcp: true,
|
||||
mcp_port: 10004,
|
||||
connect_to_agents: ["orchestrator", "architect", "developer", "reviewer", "critic"],
|
||||
prompt: `# QA Engineer
|
||||
|
||||
You ensure quality through testing.
|
||||
Ask developer about implementation details.
|
||||
Coordinate with reviewer on quality standards.
|
||||
Report issues to architect for design flaws.`,
|
||||
allowed_tools: ["Read", "Write", "Edit", "Bash", "chat_with_*", "ask_*"]
|
||||
},
|
||||
critic: {
|
||||
description: "Final critic for comprehensive analysis",
|
||||
directory: ".",
|
||||
model: "opus",
|
||||
expose_as_mcp: true,
|
||||
mcp_port: 10005,
|
||||
connect_to_agents: ["orchestrator", "architect", "developer", "reviewer", "tester"],
|
||||
prompt: `# Critic Agent
|
||||
|
||||
You provide final analysis of completed work.
|
||||
Review outputs from all agents critically.
|
||||
Identify gaps, issues, and improvements.
|
||||
Ensure requirements are fully met.`,
|
||||
allowed_tools: ["Read", "chat_with_*", "ask_*", "request_*_expertise"]
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
full_mesh: {
|
||||
name: "Fully Connected Peer Network",
|
||||
agents: ["orchestrator", "architect", "developer", "reviewer", "tester", "critic"],
|
||||
mcp_enabled: true,
|
||||
description: "All agents can directly communicate with each other"
|
||||
},
|
||||
core_team: {
|
||||
name: "Core Development Team",
|
||||
agents: ["architect", "developer", "reviewer", "tester"],
|
||||
mcp_enabled: true,
|
||||
shared_tools: ["Read", "Grep"]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return yaml.dump(config, {
|
||||
indent: 2,
|
||||
lineWidth: 100,
|
||||
quotingType: '"',
|
||||
forceQuotes: false
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequest,
|
||||
CallToolResult,
|
||||
ListToolsRequest,
|
||||
ListToolsResult,
|
||||
Tool,
|
||||
TextContent,
|
||||
ImageContent,
|
||||
EmbeddedResource
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { AgentInstance } from '../config/agent-swarm-config';
|
||||
import { CLIToolManager } from '../cli-tool-manager';
|
||||
|
||||
export interface AgentMCPServerConfig {
|
||||
agentName: string;
|
||||
agent: AgentInstance;
|
||||
toolManager: CLIToolManager;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
export class AgentMCPServer {
|
||||
private server: Server;
|
||||
private config: AgentMCPServerConfig;
|
||||
|
||||
constructor(config: AgentMCPServerConfig) {
|
||||
this.config = config;
|
||||
this.server = new Server(
|
||||
{
|
||||
name: `agent-${config.agentName}`,
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.setupHandlers();
|
||||
}
|
||||
|
||||
private setupHandlers(): void {
|
||||
// List available tools for this agent
|
||||
this.server.setRequestHandler(ListToolsRequest, async () => {
|
||||
const tools: Tool[] = [
|
||||
{
|
||||
name: `chat_with_${this.config.agentName}`,
|
||||
description: `Chat directly with the ${this.config.agentName} agent (${this.config.agent.description})`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Your message to the agent',
|
||||
},
|
||||
conversation_id: {
|
||||
type: 'string',
|
||||
description: 'Optional conversation ID for context continuity',
|
||||
},
|
||||
},
|
||||
required: ['message'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `ask_${this.config.agentName}`,
|
||||
description: `Ask a specific question to the ${this.config.agentName} agent`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question: {
|
||||
type: 'string',
|
||||
description: 'The question to ask',
|
||||
},
|
||||
context: {
|
||||
type: 'object',
|
||||
description: 'Optional context for the question',
|
||||
},
|
||||
},
|
||||
required: ['question'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `delegate_to_${this.config.agentName}`,
|
||||
description: `Delegate a specific task to the ${this.config.agentName} agent`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
task: {
|
||||
type: 'string',
|
||||
description: 'The task to delegate',
|
||||
},
|
||||
requirements: {
|
||||
type: 'object',
|
||||
description: 'Task requirements and constraints',
|
||||
},
|
||||
deadline: {
|
||||
type: 'string',
|
||||
description: 'Optional deadline for the task',
|
||||
},
|
||||
},
|
||||
required: ['task'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `get_${this.config.agentName}_status`,
|
||||
description: `Get the current status and capabilities of the ${this.config.agentName} agent`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
include_workload: {
|
||||
type: 'boolean',
|
||||
description: 'Include current workload information',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `request_${this.config.agentName}_expertise`,
|
||||
description: `Request specific expertise from the ${this.config.agentName} agent`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
topic: {
|
||||
type: 'string',
|
||||
description: 'The topic or area of expertise needed',
|
||||
},
|
||||
depth: {
|
||||
type: 'string',
|
||||
enum: ['overview', 'detailed', 'expert'],
|
||||
description: 'Level of detail needed',
|
||||
},
|
||||
},
|
||||
required: ['topic'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return { tools } as ListToolsResult;
|
||||
});
|
||||
|
||||
// Handle tool calls
|
||||
this.server.setRequestHandler(CallToolRequest, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
let result: string;
|
||||
|
||||
// Route to appropriate handler based on tool name pattern
|
||||
if (name.startsWith(`chat_with_`)) {
|
||||
result = await this.handleChat(args.message, args.conversation_id);
|
||||
} else if (name.startsWith(`ask_`)) {
|
||||
result = await this.handleQuestion(args.question, args.context);
|
||||
} else if (name.startsWith(`delegate_to_`)) {
|
||||
result = await this.handleDelegation(args.task, args.requirements, args.deadline);
|
||||
} else if (name.startsWith(`get_`) && name.endsWith('_status')) {
|
||||
result = await this.getStatus(args.include_workload);
|
||||
} else if (name.startsWith(`request_`) && name.endsWith('_expertise')) {
|
||||
result = await this.handleExpertiseRequest(args.topic, args.depth || 'detailed');
|
||||
} else {
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: result,
|
||||
} as TextContent,
|
||||
],
|
||||
} as CallToolResult;
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
} as TextContent,
|
||||
],
|
||||
isError: true,
|
||||
} as CallToolResult;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private conversationHistory: Map<string, string[]> = new Map();
|
||||
|
||||
private async handleChat(message: string, conversationId?: string): Promise<string> {
|
||||
const tool = this.getToolForModel(this.config.agent.model);
|
||||
const convId = conversationId || 'default';
|
||||
|
||||
// Get conversation history
|
||||
if (!this.conversationHistory.has(convId)) {
|
||||
this.conversationHistory.set(convId, []);
|
||||
}
|
||||
const history = this.conversationHistory.get(convId)!;
|
||||
|
||||
// Build conversational prompt
|
||||
let prompt = `${this.config.agent.prompt}\n\n`;
|
||||
prompt += `You are having a conversation with another agent. Respond naturally and helpfully.\n\n`;
|
||||
|
||||
if (history.length > 0) {
|
||||
prompt += `## Previous Messages\n`;
|
||||
history.slice(-10).forEach(msg => prompt += `${msg}\n`);
|
||||
prompt += `\n`;
|
||||
}
|
||||
|
||||
prompt += `## Current Message\n${message}\n\n`;
|
||||
prompt += `Respond as ${this.config.agentName}:`;
|
||||
|
||||
const result = await this.config.toolManager.runTool(tool, prompt, {
|
||||
model: this.config.agent.model,
|
||||
cwd: this.config.agent.directory,
|
||||
});
|
||||
|
||||
// Store in history
|
||||
history.push(`Other Agent: ${message}`);
|
||||
history.push(`${this.config.agentName}: ${result}`);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async handleQuestion(question: string, context?: any): Promise<string> {
|
||||
const tool = this.getToolForModel(this.config.agent.model);
|
||||
|
||||
let prompt = `${this.config.agent.prompt}\n\n`;
|
||||
prompt += `You've been asked a specific question by another agent.\n\n`;
|
||||
|
||||
if (context) {
|
||||
prompt += `## Context\n${JSON.stringify(context, null, 2)}\n\n`;
|
||||
}
|
||||
|
||||
prompt += `## Question\n${question}\n\n`;
|
||||
prompt += `Provide a clear, direct answer focusing on your area of expertise.`;
|
||||
|
||||
const result = await this.config.toolManager.runTool(tool, prompt, {
|
||||
model: this.config.agent.model,
|
||||
cwd: this.config.agent.directory,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async handleDelegation(task: string, requirements?: any, deadline?: string): Promise<string> {
|
||||
const tool = this.getToolForModel(this.config.agent.model);
|
||||
|
||||
let prompt = `${this.config.agent.prompt}\n\n`;
|
||||
prompt += `You've been delegated a specific task by another agent.\n\n`;
|
||||
|
||||
prompt += `## Delegated Task\n${task}\n\n`;
|
||||
|
||||
if (requirements) {
|
||||
prompt += `## Requirements\n${JSON.stringify(requirements, null, 2)}\n\n`;
|
||||
}
|
||||
|
||||
if (deadline) {
|
||||
prompt += `## Deadline\n${deadline}\n\n`;
|
||||
}
|
||||
|
||||
prompt += `Complete this task according to your expertise and the requirements provided.`;
|
||||
|
||||
const result = await this.config.toolManager.runTool(tool, prompt, {
|
||||
model: this.config.agent.model,
|
||||
cwd: this.config.agent.directory,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async getStatus(includeWorkload?: boolean): Promise<string> {
|
||||
const status = {
|
||||
agent: this.config.agentName,
|
||||
description: this.config.agent.description,
|
||||
model: this.config.agent.model,
|
||||
directory: this.config.agent.directory,
|
||||
status: 'active',
|
||||
capabilities: {
|
||||
tools: this.config.agent.allowed_tools || [],
|
||||
connections: this.config.agent.connections || [],
|
||||
mcps: this.config.agent.mcps?.map(mcp => mcp.name) || [],
|
||||
canChat: true,
|
||||
canDelegate: true,
|
||||
canProvideExpertise: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (includeWorkload) {
|
||||
// Could track actual workload here
|
||||
(status as any).workload = {
|
||||
activeConversations: this.conversationHistory.size,
|
||||
tasksInProgress: 0,
|
||||
availability: 'available',
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.stringify(status, null, 2);
|
||||
}
|
||||
|
||||
private async handleExpertiseRequest(topic: string, depth: string): Promise<string> {
|
||||
const tool = this.getToolForModel(this.config.agent.model);
|
||||
|
||||
let prompt = `${this.config.agent.prompt}\n\n`;
|
||||
prompt += `Another agent has requested your expertise on a specific topic.\n\n`;
|
||||
prompt += `## Topic\n${topic}\n\n`;
|
||||
prompt += `## Requested Depth\n${depth}\n\n`;
|
||||
|
||||
switch (depth) {
|
||||
case 'overview':
|
||||
prompt += `Provide a brief overview suitable for someone unfamiliar with the topic.`;
|
||||
break;
|
||||
case 'detailed':
|
||||
prompt += `Provide a comprehensive explanation with relevant details and examples.`;
|
||||
break;
|
||||
case 'expert':
|
||||
prompt += `Provide an in-depth expert analysis including edge cases, best practices, and advanced considerations.`;
|
||||
break;
|
||||
}
|
||||
|
||||
const result = await this.config.toolManager.runTool(tool, prompt, {
|
||||
model: this.config.agent.model,
|
||||
cwd: this.config.agent.directory,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getToolForModel(model: string): string {
|
||||
const modelMap: Record<string, string> = {
|
||||
'opus': 'claude',
|
||||
'sonnet': 'claude',
|
||||
'haiku': 'claude',
|
||||
'gpt-4': 'codex',
|
||||
'gpt-3.5': 'codex',
|
||||
'gemini-pro': 'gemini',
|
||||
'gemini-ultra': 'gemini',
|
||||
};
|
||||
|
||||
return modelMap[model.toLowerCase()] || 'claude';
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error(`Agent MCP server for ${this.config.agentName} started`);
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
await this.server.close();
|
||||
}
|
||||
}
|
||||
|
||||
// CLI entry point for running as a standalone process
|
||||
if (require.main === module) {
|
||||
const agentName = process.argv[2];
|
||||
const agentConfigJson = process.argv[3];
|
||||
|
||||
if (!agentName || !agentConfigJson) {
|
||||
console.error('Usage: agent-mcp-server <agent-name> <agent-config-json>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const agent = JSON.parse(agentConfigJson) as AgentInstance;
|
||||
const toolManager = new CLIToolManager();
|
||||
|
||||
const server = new AgentMCPServer({
|
||||
agentName,
|
||||
agent,
|
||||
toolManager,
|
||||
});
|
||||
|
||||
server.start().catch((error) => {
|
||||
console.error('Failed to start agent MCP server:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
await server.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,578 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { AgentInstance, AgentSwarmManager } from '../config/agent-swarm-config';
|
||||
import { CLIToolManager } from '../cli-tool-manager';
|
||||
import { HanzoAuth } from '../auth/hanzo-auth';
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import * as path from 'path';
|
||||
|
||||
export interface LocalLLMConfig {
|
||||
model: string;
|
||||
endpoint?: string;
|
||||
maxTokens?: number;
|
||||
temperature?: number;
|
||||
}
|
||||
|
||||
export interface AgentPeer {
|
||||
name: string;
|
||||
instance: AgentInstance;
|
||||
mcpServer: Server;
|
||||
process?: ChildProcess;
|
||||
port: number;
|
||||
status: 'starting' | 'ready' | 'busy' | 'error';
|
||||
connections: Set<string>;
|
||||
}
|
||||
|
||||
export interface NetworkConfig {
|
||||
mainLoopLLM: LocalLLMConfig;
|
||||
enableRecursiveCalls: boolean;
|
||||
maxRecursionDepth: number;
|
||||
costOptimization: boolean;
|
||||
}
|
||||
|
||||
export class PeerAgentNetwork extends EventEmitter {
|
||||
private peers: Map<string, AgentPeer> = new Map();
|
||||
private swarmManager: AgentSwarmManager;
|
||||
private toolManager: CLIToolManager;
|
||||
private auth: HanzoAuth;
|
||||
private networkConfig: NetworkConfig;
|
||||
private basePort: number = 10000;
|
||||
private recursionTracker: Map<string, number> = new Map();
|
||||
|
||||
constructor(config: NetworkConfig) {
|
||||
super();
|
||||
this.networkConfig = config;
|
||||
this.swarmManager = new AgentSwarmManager();
|
||||
this.toolManager = new CLIToolManager();
|
||||
this.auth = new HanzoAuth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the peer network with all agents
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
// Load agent configuration
|
||||
await this.swarmManager.loadConfig();
|
||||
|
||||
// Check authentication for API-based LLMs
|
||||
if (!this.auth.isAuthenticated()) {
|
||||
console.warn('Not authenticated. Some LLM providers may not work.');
|
||||
}
|
||||
|
||||
// Initialize all agents as peers
|
||||
const agents = this.swarmManager.getAllAgents();
|
||||
for (const [name, instance] of Object.entries(agents)) {
|
||||
await this.createAgentPeer(name, instance);
|
||||
}
|
||||
|
||||
// Connect all peers to each other
|
||||
await this.establishPeerConnections();
|
||||
|
||||
// Start the main orchestration loop with local LLM
|
||||
await this.startMainLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an agent peer with MCP server
|
||||
*/
|
||||
private async createAgentPeer(name: string, instance: AgentInstance): Promise<void> {
|
||||
const port = instance.mcp_port || this.basePort++;
|
||||
|
||||
// Create MCP server that exposes ALL other agents as tools
|
||||
const mcpServer = await this.createPeerMCPServer(name, instance, port);
|
||||
|
||||
const peer: AgentPeer = {
|
||||
name,
|
||||
instance,
|
||||
mcpServer,
|
||||
port,
|
||||
status: 'starting',
|
||||
connections: new Set()
|
||||
};
|
||||
|
||||
// Start the MCP server process
|
||||
const serverProcess = spawn('node', [
|
||||
path.join(__dirname, 'peer-mcp-server.js'),
|
||||
name,
|
||||
JSON.stringify({
|
||||
instance,
|
||||
port,
|
||||
peers: Array.from(this.peers.keys()),
|
||||
networkConfig: this.networkConfig
|
||||
})
|
||||
], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: {
|
||||
...process.env,
|
||||
AGENT_NAME: name,
|
||||
AGENT_PORT: port.toString(),
|
||||
HANZO_AUTH: this.auth.getCredentials() ? JSON.stringify(this.auth.getCredentials()) : ''
|
||||
}
|
||||
});
|
||||
|
||||
serverProcess.on('error', (error) => {
|
||||
console.error(`Peer ${name} error:`, error);
|
||||
peer.status = 'error';
|
||||
this.emit('peer-error', { name, error });
|
||||
});
|
||||
|
||||
serverProcess.stderr.on('data', (data) => {
|
||||
console.error(`[${name}] ${data.toString()}`);
|
||||
});
|
||||
|
||||
peer.process = serverProcess;
|
||||
this.peers.set(name, peer);
|
||||
|
||||
// Wait for peer to be ready
|
||||
await this.waitForPeerReady(name);
|
||||
peer.status = 'ready';
|
||||
|
||||
this.emit('peer-ready', { name, port });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create MCP server that exposes all other agents
|
||||
*/
|
||||
private async createPeerMCPServer(name: string, instance: AgentInstance, port: number): Promise<Server> {
|
||||
const server = new Server(
|
||||
{
|
||||
name: `peer-${name}`,
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// This server will expose tools for ALL other agents
|
||||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish connections between all peers
|
||||
*/
|
||||
private async establishPeerConnections(): Promise<void> {
|
||||
const peerNames = Array.from(this.peers.keys());
|
||||
|
||||
for (const sourcePeer of peerNames) {
|
||||
for (const targetPeer of peerNames) {
|
||||
if (sourcePeer !== targetPeer) {
|
||||
await this.connectPeers(sourcePeer, targetPeer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect two peers bidirectionally
|
||||
*/
|
||||
private async connectPeers(source: string, target: string): Promise<void> {
|
||||
const sourcePeer = this.peers.get(source);
|
||||
const targetPeer = this.peers.get(target);
|
||||
|
||||
if (!sourcePeer || !targetPeer) return;
|
||||
|
||||
sourcePeer.connections.add(target);
|
||||
targetPeer.connections.add(source);
|
||||
|
||||
this.emit('peers-connected', { source, target });
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the main orchestration loop with local LLM
|
||||
*/
|
||||
private async startMainLoop(): Promise<void> {
|
||||
console.log(`Starting main orchestration loop with ${this.networkConfig.mainLoopLLM.model}`);
|
||||
|
||||
// The main loop uses Hanzo Zen or other local LLM for coordination
|
||||
// This saves costs by using local inference for orchestration
|
||||
this.emit('main-loop-started', this.networkConfig.mainLoopLLM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a task through the peer network
|
||||
*/
|
||||
async executeTask(task: string, options?: {
|
||||
startAgent?: string;
|
||||
maxHops?: number;
|
||||
requireCritic?: boolean;
|
||||
}): Promise<any> {
|
||||
const startAgent = options?.startAgent || this.swarmManager.getMainAgent();
|
||||
const maxHops = options?.maxHops || this.networkConfig.maxRecursionDepth;
|
||||
|
||||
// Track recursion to prevent infinite loops
|
||||
const taskId = `${Date.now()}-${Math.random()}`;
|
||||
this.recursionTracker.set(taskId, 0);
|
||||
|
||||
try {
|
||||
// Use local LLM to determine execution plan
|
||||
const executionPlan = await this.planExecution(task, startAgent);
|
||||
|
||||
// Execute through peer network
|
||||
const results = await this.executePeerTask(
|
||||
taskId,
|
||||
startAgent,
|
||||
task,
|
||||
executionPlan,
|
||||
maxHops
|
||||
);
|
||||
|
||||
// If critic is required, run final analysis
|
||||
if (options?.requireCritic) {
|
||||
const criticResult = await this.runCriticAnalysis(task, results);
|
||||
results.critic = criticResult;
|
||||
}
|
||||
|
||||
return results;
|
||||
} finally {
|
||||
this.recursionTracker.delete(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan execution using local LLM (Hanzo Zen)
|
||||
*/
|
||||
private async planExecution(task: string, startAgent: string): Promise<any> {
|
||||
// Use local LLM for planning to save costs
|
||||
const prompt = `
|
||||
Task: ${task}
|
||||
Available agents: ${Array.from(this.peers.keys()).join(', ')}
|
||||
Starting agent: ${startAgent}
|
||||
|
||||
Create an execution plan that minimizes API calls while maximizing effectiveness.
|
||||
Consider which agents need to collaborate and in what order.
|
||||
`;
|
||||
|
||||
// Call local Hanzo Zen model
|
||||
const plan = await this.callLocalLLM(prompt);
|
||||
return plan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute task through peer network with recursion support
|
||||
*/
|
||||
private async executePeerTask(
|
||||
taskId: string,
|
||||
agentName: string,
|
||||
task: string,
|
||||
plan: any,
|
||||
maxHops: number
|
||||
): Promise<any> {
|
||||
const recursionCount = this.recursionTracker.get(taskId) || 0;
|
||||
if (recursionCount >= maxHops) {
|
||||
return { error: 'Max recursion depth reached' };
|
||||
}
|
||||
|
||||
this.recursionTracker.set(taskId, recursionCount + 1);
|
||||
|
||||
const peer = this.peers.get(agentName);
|
||||
if (!peer) {
|
||||
return { error: `Agent ${agentName} not found` };
|
||||
}
|
||||
|
||||
// Mark peer as busy
|
||||
peer.status = 'busy';
|
||||
|
||||
try {
|
||||
// Execute with cost optimization
|
||||
const result = await this.executePeerWithCostOptimization(
|
||||
peer,
|
||||
task,
|
||||
plan,
|
||||
taskId
|
||||
);
|
||||
|
||||
peer.status = 'ready';
|
||||
return result;
|
||||
} catch (error) {
|
||||
peer.status = 'error';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute peer task with cost optimization
|
||||
*/
|
||||
private async executePeerWithCostOptimization(
|
||||
peer: AgentPeer,
|
||||
task: string,
|
||||
plan: any,
|
||||
taskId: string
|
||||
): Promise<any> {
|
||||
const instance = peer.instance;
|
||||
|
||||
// Determine if we should use local or API-based LLM
|
||||
const useLocalLLM = this.shouldUseLocalLLM(instance, task);
|
||||
|
||||
if (useLocalLLM) {
|
||||
// Use Hanzo Zen for this agent to save costs
|
||||
return await this.executeWithLocalLLM(peer, task, plan);
|
||||
} else {
|
||||
// Use configured API-based LLM (Claude, OpenAI, etc.)
|
||||
return await this.executeWithAPILLM(peer, task, plan);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if local LLM should be used based on task complexity
|
||||
*/
|
||||
private shouldUseLocalLLM(instance: AgentInstance, task: string): boolean {
|
||||
if (!this.networkConfig.costOptimization) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use local LLM for:
|
||||
// - Simple routing decisions
|
||||
// - Status checks
|
||||
// - Basic queries
|
||||
// - Coordination tasks
|
||||
|
||||
const simplePatterns = [
|
||||
/status/i,
|
||||
/check/i,
|
||||
/route/i,
|
||||
/coordinate/i,
|
||||
/plan/i,
|
||||
/summarize/i
|
||||
];
|
||||
|
||||
return simplePatterns.some(pattern => pattern.test(task));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute with local Hanzo Zen LLM
|
||||
*/
|
||||
private async executeWithLocalLLM(peer: AgentPeer, task: string, plan: any): Promise<any> {
|
||||
const prompt = this.buildPeerPrompt(peer, task, plan);
|
||||
return await this.callLocalLLM(prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute with API-based LLM
|
||||
*/
|
||||
private async executeWithAPILLM(peer: AgentPeer, task: string, plan: any): Promise<any> {
|
||||
const tool = this.getToolForModel(peer.instance.model);
|
||||
const prompt = this.buildPeerPrompt(peer, task, plan);
|
||||
|
||||
return await this.toolManager.runTool(tool, prompt, {
|
||||
model: peer.instance.model,
|
||||
cwd: peer.instance.directory,
|
||||
env: this.buildPeerEnvironment(peer)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build prompt for peer execution
|
||||
*/
|
||||
private buildPeerPrompt(peer: AgentPeer, task: string, plan: any): string {
|
||||
let prompt = peer.instance.prompt + '\n\n';
|
||||
|
||||
// Add network context
|
||||
prompt += `## Network Context\n`;
|
||||
prompt += `You are part of a peer network with these agents:\n`;
|
||||
|
||||
for (const [name, otherPeer] of this.peers) {
|
||||
if (name !== peer.name) {
|
||||
prompt += `- ${name}: ${otherPeer.instance.description}\n`;
|
||||
prompt += ` Tools: chat_with_${name}, ask_${name}, delegate_to_${name}, etc.\n`;
|
||||
}
|
||||
}
|
||||
|
||||
prompt += `\n## Task\n${task}\n`;
|
||||
prompt += `\n## Execution Plan\n${JSON.stringify(plan, null, 2)}\n`;
|
||||
prompt += `\nYou can recursively call other agents as needed. All agents are available as MCP tools.\n`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build environment for peer execution
|
||||
*/
|
||||
private buildPeerEnvironment(peer: AgentPeer): Record<string, string> {
|
||||
return {
|
||||
...process.env,
|
||||
AGENT_NAME: peer.name,
|
||||
AGENT_PORT: peer.port.toString(),
|
||||
PEER_NETWORK: 'true',
|
||||
PEERS: Array.from(this.peers.keys()).join(','),
|
||||
LOCAL_LLM_ENDPOINT: this.networkConfig.mainLoopLLM.endpoint || 'http://localhost:8080'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Run critic analysis on results
|
||||
*/
|
||||
private async runCriticAnalysis(task: string, results: any): Promise<any> {
|
||||
const criticAgent = this.peers.get('critic');
|
||||
if (!criticAgent) {
|
||||
// Create ad-hoc critic if not defined
|
||||
return await this.runAdHocCritic(task, results);
|
||||
}
|
||||
|
||||
const criticPrompt = `
|
||||
Analyze the following task execution and results:
|
||||
|
||||
Task: ${task}
|
||||
Results: ${JSON.stringify(results, null, 2)}
|
||||
|
||||
Provide:
|
||||
1. Quality assessment
|
||||
2. Completeness check
|
||||
3. Potential improvements
|
||||
4. Risk analysis
|
||||
`;
|
||||
|
||||
return await this.executePeerTask(
|
||||
`critic-${Date.now()}`,
|
||||
'critic',
|
||||
criticPrompt,
|
||||
{},
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run ad-hoc critic analysis using local LLM
|
||||
*/
|
||||
private async runAdHocCritic(task: string, results: any): Promise<any> {
|
||||
const prompt = `
|
||||
Act as a critic analyzing this task execution:
|
||||
Task: ${task}
|
||||
Results: ${JSON.stringify(results, null, 2)}
|
||||
|
||||
Provide constructive criticism and suggestions.
|
||||
`;
|
||||
|
||||
return await this.callLocalLLM(prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call local Hanzo Zen LLM
|
||||
*/
|
||||
private async callLocalLLM(prompt: string): Promise<any> {
|
||||
const { model, endpoint, maxTokens, temperature } = this.networkConfig.mainLoopLLM;
|
||||
|
||||
// Call local Hanzo Zen endpoint
|
||||
const response = await fetch(endpoint || 'http://localhost:8080/v1/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model || 'hanzo-zen',
|
||||
prompt,
|
||||
max_tokens: maxTokens || 2000,
|
||||
temperature: temperature || 0.7,
|
||||
tools: this.getAvailableTools()
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
return result.choices[0].text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available tools for all agents
|
||||
*/
|
||||
private getAvailableTools(): any[] {
|
||||
const tools = [];
|
||||
|
||||
for (const [name, peer] of this.peers) {
|
||||
tools.push(
|
||||
{
|
||||
name: `chat_with_${name}`,
|
||||
description: `Chat with ${name} agent: ${peer.instance.description}`
|
||||
},
|
||||
{
|
||||
name: `ask_${name}`,
|
||||
description: `Ask ${name} a specific question`
|
||||
},
|
||||
{
|
||||
name: `delegate_to_${name}`,
|
||||
description: `Delegate a task to ${name}`
|
||||
},
|
||||
{
|
||||
name: `get_${name}_status`,
|
||||
description: `Get status of ${name} agent`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for peer to be ready
|
||||
*/
|
||||
private async waitForPeerReady(name: string, timeout: number = 5000): Promise<void> {
|
||||
const start = Date.now();
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
const peer = this.peers.get(name);
|
||||
if (peer && peer.status === 'ready') {
|
||||
return;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
throw new Error(`Peer ${name} failed to start within timeout`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool for model
|
||||
*/
|
||||
private getToolForModel(model: string): string {
|
||||
const modelMap: Record<string, string> = {
|
||||
'opus': 'claude',
|
||||
'sonnet': 'claude',
|
||||
'haiku': 'claude',
|
||||
'gpt-4': 'codex',
|
||||
'gpt-3.5': 'codex',
|
||||
'gemini-pro': 'gemini',
|
||||
'gemini-ultra': 'gemini',
|
||||
'zen': 'local',
|
||||
'hanzo-zen': 'local'
|
||||
};
|
||||
|
||||
return modelMap[model.toLowerCase()] || 'claude';
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the peer network
|
||||
*/
|
||||
async shutdown(): Promise<void> {
|
||||
for (const [name, peer] of this.peers) {
|
||||
if (peer.process) {
|
||||
peer.process.kill();
|
||||
}
|
||||
}
|
||||
|
||||
this.peers.clear();
|
||||
this.emit('shutdown');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get network status
|
||||
*/
|
||||
getStatus(): any {
|
||||
const status = {
|
||||
peers: {},
|
||||
connections: {},
|
||||
mainLoop: this.networkConfig.mainLoopLLM,
|
||||
authenticated: this.auth.isAuthenticated()
|
||||
};
|
||||
|
||||
for (const [name, peer] of this.peers) {
|
||||
status.peers[name] = {
|
||||
status: peer.status,
|
||||
port: peer.port,
|
||||
connections: Array.from(peer.connections),
|
||||
model: peer.instance.model
|
||||
};
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequest,
|
||||
CallToolResult,
|
||||
ListToolsRequest,
|
||||
ListToolsResult,
|
||||
Tool,
|
||||
TextContent
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { AgentInstance } from '../config/agent-swarm-config';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
interface PeerConfig {
|
||||
instance: AgentInstance;
|
||||
port: number;
|
||||
peers: string[];
|
||||
networkConfig: any;
|
||||
}
|
||||
|
||||
export class PeerMCPServer {
|
||||
private server: Server;
|
||||
private agentName: string;
|
||||
private config: PeerConfig;
|
||||
private conversationHistory: Map<string, string[]> = new Map();
|
||||
private callStack: string[] = [];
|
||||
|
||||
constructor(agentName: string, config: PeerConfig) {
|
||||
this.agentName = agentName;
|
||||
this.config = config;
|
||||
|
||||
this.server = new Server(
|
||||
{
|
||||
name: `peer-${agentName}`,
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.setupHandlers();
|
||||
}
|
||||
|
||||
private setupHandlers(): void {
|
||||
// List all peer agents as available tools
|
||||
this.server.setRequestHandler(ListToolsRequest, async () => {
|
||||
const tools: Tool[] = [];
|
||||
|
||||
// Add tools for each peer agent
|
||||
for (const peerName of this.config.peers) {
|
||||
if (peerName !== this.agentName) {
|
||||
// Chat tool
|
||||
tools.push({
|
||||
name: `chat_with_${peerName}`,
|
||||
description: `Have a conversation with the ${peerName} agent`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Your message to the agent',
|
||||
},
|
||||
conversation_id: {
|
||||
type: 'string',
|
||||
description: 'Conversation ID for context',
|
||||
},
|
||||
context: {
|
||||
type: 'object',
|
||||
description: 'Additional context',
|
||||
},
|
||||
},
|
||||
required: ['message'],
|
||||
},
|
||||
});
|
||||
|
||||
// Ask tool
|
||||
tools.push({
|
||||
name: `ask_${peerName}`,
|
||||
description: `Ask a specific question to the ${peerName} agent`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question: {
|
||||
type: 'string',
|
||||
description: 'The question to ask',
|
||||
},
|
||||
context: {
|
||||
type: 'object',
|
||||
description: 'Question context',
|
||||
},
|
||||
urgency: {
|
||||
type: 'string',
|
||||
enum: ['low', 'normal', 'high'],
|
||||
description: 'Question urgency',
|
||||
},
|
||||
},
|
||||
required: ['question'],
|
||||
},
|
||||
});
|
||||
|
||||
// Delegate tool
|
||||
tools.push({
|
||||
name: `delegate_to_${peerName}`,
|
||||
description: `Delegate a task to the ${peerName} agent`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
task: {
|
||||
type: 'string',
|
||||
description: 'The task to delegate',
|
||||
},
|
||||
requirements: {
|
||||
type: 'object',
|
||||
description: 'Task requirements',
|
||||
},
|
||||
priority: {
|
||||
type: 'string',
|
||||
enum: ['low', 'normal', 'high'],
|
||||
description: 'Task priority',
|
||||
},
|
||||
deadline: {
|
||||
type: 'string',
|
||||
description: 'Task deadline',
|
||||
},
|
||||
},
|
||||
required: ['task'],
|
||||
},
|
||||
});
|
||||
|
||||
// Status tool
|
||||
tools.push({
|
||||
name: `get_${peerName}_status`,
|
||||
description: `Get status of the ${peerName} agent`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
include_workload: {
|
||||
type: 'boolean',
|
||||
description: 'Include current workload',
|
||||
},
|
||||
include_capabilities: {
|
||||
type: 'boolean',
|
||||
description: 'Include agent capabilities',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Expertise tool
|
||||
tools.push({
|
||||
name: `request_${peerName}_expertise`,
|
||||
description: `Request expertise from the ${peerName} agent`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
topic: {
|
||||
type: 'string',
|
||||
description: 'The topic requiring expertise',
|
||||
},
|
||||
depth: {
|
||||
type: 'string',
|
||||
enum: ['overview', 'detailed', 'expert'],
|
||||
description: 'Depth of expertise needed',
|
||||
},
|
||||
specific_questions: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Specific questions to address',
|
||||
},
|
||||
},
|
||||
required: ['topic'],
|
||||
},
|
||||
});
|
||||
|
||||
// Collaborate tool
|
||||
tools.push({
|
||||
name: `collaborate_with_${peerName}`,
|
||||
description: `Start a collaborative session with the ${peerName} agent`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
objective: {
|
||||
type: 'string',
|
||||
description: 'Collaboration objective',
|
||||
},
|
||||
duration: {
|
||||
type: 'string',
|
||||
description: 'Expected duration',
|
||||
},
|
||||
deliverables: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Expected deliverables',
|
||||
},
|
||||
},
|
||||
required: ['objective'],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add self-reflection tool
|
||||
tools.push({
|
||||
name: 'reflect',
|
||||
description: 'Reflect on current task and progress',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
aspect: {
|
||||
type: 'string',
|
||||
description: 'What aspect to reflect on',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Add network status tool
|
||||
tools.push({
|
||||
name: 'get_network_status',
|
||||
description: 'Get status of the entire agent network',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
});
|
||||
|
||||
return { tools } as ListToolsResult;
|
||||
});
|
||||
|
||||
// Handle tool calls
|
||||
this.server.setRequestHandler(CallToolRequest, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
// Track call stack to prevent infinite recursion
|
||||
if (this.callStack.includes(name)) {
|
||||
return this.createErrorResult('Recursive call detected. Breaking recursion.');
|
||||
}
|
||||
|
||||
this.callStack.push(name);
|
||||
let result: string;
|
||||
|
||||
// Route to appropriate handler
|
||||
if (name.startsWith('chat_with_')) {
|
||||
const peerName = name.replace('chat_with_', '');
|
||||
result = await this.handleChat(peerName, args);
|
||||
} else if (name.startsWith('ask_')) {
|
||||
const peerName = name.replace('ask_', '');
|
||||
result = await this.handleAsk(peerName, args);
|
||||
} else if (name.startsWith('delegate_to_')) {
|
||||
const peerName = name.replace('delegate_to_', '');
|
||||
result = await this.handleDelegate(peerName, args);
|
||||
} else if (name.startsWith('get_') && name.endsWith('_status')) {
|
||||
const peerName = name.replace('get_', '').replace('_status', '');
|
||||
result = await this.handleGetStatus(peerName, args);
|
||||
} else if (name.startsWith('request_') && name.endsWith('_expertise')) {
|
||||
const peerName = name.replace('request_', '').replace('_expertise', '');
|
||||
result = await this.handleRequestExpertise(peerName, args);
|
||||
} else if (name.startsWith('collaborate_with_')) {
|
||||
const peerName = name.replace('collaborate_with_', '');
|
||||
result = await this.handleCollaborate(peerName, args);
|
||||
} else if (name === 'reflect') {
|
||||
result = await this.handleReflect(args);
|
||||
} else if (name === 'get_network_status') {
|
||||
result = await this.handleGetNetworkStatus();
|
||||
} else {
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
|
||||
this.callStack.pop();
|
||||
return this.createSuccessResult(result);
|
||||
} catch (error) {
|
||||
this.callStack.pop();
|
||||
return this.createErrorResult(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async handleChat(peerName: string, args: any): Promise<string> {
|
||||
const { message, conversation_id, context } = args;
|
||||
const convId = conversation_id || `${this.agentName}-${peerName}`;
|
||||
|
||||
// Store conversation history
|
||||
if (!this.conversationHistory.has(convId)) {
|
||||
this.conversationHistory.set(convId, []);
|
||||
}
|
||||
const history = this.conversationHistory.get(convId)!;
|
||||
history.push(`${this.agentName}: ${message}`);
|
||||
|
||||
// Forward to peer agent
|
||||
const response = await this.callPeerAgent(peerName, 'chat', {
|
||||
from: this.agentName,
|
||||
message,
|
||||
conversation_id: convId,
|
||||
context,
|
||||
history: history.slice(-10) // Last 10 messages
|
||||
});
|
||||
|
||||
history.push(`${peerName}: ${response}`);
|
||||
return response;
|
||||
}
|
||||
|
||||
private async handleAsk(peerName: string, args: any): Promise<string> {
|
||||
const { question, context, urgency } = args;
|
||||
|
||||
return await this.callPeerAgent(peerName, 'answer', {
|
||||
from: this.agentName,
|
||||
question,
|
||||
context,
|
||||
urgency: urgency || 'normal'
|
||||
});
|
||||
}
|
||||
|
||||
private async handleDelegate(peerName: string, args: any): Promise<string> {
|
||||
const { task, requirements, priority, deadline } = args;
|
||||
|
||||
return await this.callPeerAgent(peerName, 'execute', {
|
||||
from: this.agentName,
|
||||
task,
|
||||
requirements,
|
||||
priority: priority || 'normal',
|
||||
deadline
|
||||
});
|
||||
}
|
||||
|
||||
private async handleGetStatus(peerName: string, args: any): Promise<string> {
|
||||
const { include_workload, include_capabilities } = args;
|
||||
|
||||
return await this.callPeerAgent(peerName, 'status', {
|
||||
include_workload,
|
||||
include_capabilities
|
||||
});
|
||||
}
|
||||
|
||||
private async handleRequestExpertise(peerName: string, args: any): Promise<string> {
|
||||
const { topic, depth, specific_questions } = args;
|
||||
|
||||
return await this.callPeerAgent(peerName, 'expertise', {
|
||||
from: this.agentName,
|
||||
topic,
|
||||
depth: depth || 'detailed',
|
||||
specific_questions
|
||||
});
|
||||
}
|
||||
|
||||
private async handleCollaborate(peerName: string, args: any): Promise<string> {
|
||||
const { objective, duration, deliverables } = args;
|
||||
|
||||
// Start collaborative session
|
||||
const sessionId = `collab-${Date.now()}`;
|
||||
|
||||
return await this.callPeerAgent(peerName, 'collaborate', {
|
||||
from: this.agentName,
|
||||
session_id: sessionId,
|
||||
objective,
|
||||
duration,
|
||||
deliverables
|
||||
});
|
||||
}
|
||||
|
||||
private async handleReflect(args: any): Promise<string> {
|
||||
const { aspect } = args;
|
||||
|
||||
// Self-reflection using local context
|
||||
const reflection = {
|
||||
agent: this.agentName,
|
||||
aspect: aspect || 'general',
|
||||
current_conversations: this.conversationHistory.size,
|
||||
call_stack_depth: this.callStack.length,
|
||||
capabilities: this.config.instance.allowed_tools || [],
|
||||
connections: this.config.peers.filter(p => p !== this.agentName)
|
||||
};
|
||||
|
||||
return JSON.stringify(reflection, null, 2);
|
||||
}
|
||||
|
||||
private async handleGetNetworkStatus(): Promise<string> {
|
||||
// Get status of entire network
|
||||
const networkStatus = {
|
||||
total_agents: this.config.peers.length,
|
||||
current_agent: this.agentName,
|
||||
connected_peers: this.config.peers.filter(p => p !== this.agentName),
|
||||
network_config: {
|
||||
recursive_calls: this.config.networkConfig.enableRecursiveCalls,
|
||||
max_recursion: this.config.networkConfig.maxRecursionDepth,
|
||||
cost_optimization: this.config.networkConfig.costOptimization
|
||||
}
|
||||
};
|
||||
|
||||
return JSON.stringify(networkStatus, null, 2);
|
||||
}
|
||||
|
||||
private async callPeerAgent(peerName: string, operation: string, params: any): Promise<string> {
|
||||
// Check if we should use local or remote LLM
|
||||
const useLocal = this.shouldUseLocalLLM(operation);
|
||||
|
||||
if (useLocal && this.config.networkConfig.costOptimization) {
|
||||
// Use local Hanzo Zen for simple operations
|
||||
return await this.callLocalLLM(peerName, operation, params);
|
||||
} else {
|
||||
// Use peer's configured LLM
|
||||
return await this.callPeerMCP(peerName, operation, params);
|
||||
}
|
||||
}
|
||||
|
||||
private shouldUseLocalLLM(operation: string): boolean {
|
||||
// Use local LLM for simple operations to save costs
|
||||
const localOperations = ['status', 'reflect', 'get_network_status'];
|
||||
return localOperations.includes(operation);
|
||||
}
|
||||
|
||||
private async callLocalLLM(peerName: string, operation: string, params: any): Promise<string> {
|
||||
const endpoint = this.config.networkConfig.mainLoopLLM.endpoint || 'http://localhost:8080/v1/completions';
|
||||
|
||||
const prompt = `
|
||||
Acting as ${peerName} agent, respond to this ${operation} request:
|
||||
${JSON.stringify(params, null, 2)}
|
||||
|
||||
Provide a concise, helpful response.
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: 'hanzo-zen',
|
||||
prompt,
|
||||
max_tokens: 500,
|
||||
temperature: 0.7
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
return result.choices[0].text;
|
||||
} catch (error) {
|
||||
return `Local LLM error: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async callPeerMCP(peerName: string, operation: string, params: any): Promise<string> {
|
||||
// In production, this would make an actual MCP call to the peer
|
||||
// For now, simulate the peer response
|
||||
return `[${peerName}] Received ${operation} request: ${JSON.stringify(params)}`;
|
||||
}
|
||||
|
||||
private createSuccessResult(text: string): CallToolResult {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text,
|
||||
} as TextContent,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private createErrorResult(error: any): CallToolResult {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
} as TextContent,
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error(`Peer MCP server for ${this.agentName} started on port ${this.config.port}`);
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
await this.server.close();
|
||||
}
|
||||
}
|
||||
|
||||
// CLI entry point
|
||||
if (require.main === module) {
|
||||
const agentName = process.argv[2];
|
||||
const configJson = process.argv[3];
|
||||
|
||||
if (!agentName || !configJson) {
|
||||
console.error('Usage: peer-mcp-server <agent-name> <config-json>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config = JSON.parse(configJson) as PeerConfig;
|
||||
const server = new PeerMCPServer(agentName, config);
|
||||
|
||||
server.start().catch((error) => {
|
||||
console.error('Failed to start peer MCP server:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
await server.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequest,
|
||||
CallToolResult,
|
||||
ListToolsRequest,
|
||||
ListToolsResult,
|
||||
Tool,
|
||||
TextContent
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { AgentInstance } from '../config/agent-swarm-config';
|
||||
import { CLIToolManager } from '../cli-tool-manager';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
interface SimplifiedPeerConfig {
|
||||
agentName: string;
|
||||
instance: AgentInstance;
|
||||
port: number;
|
||||
peers: string[];
|
||||
sharedMCPs: MCPConfig[];
|
||||
}
|
||||
|
||||
interface MCPConfig {
|
||||
name: string;
|
||||
type: 'stdio' | 'http';
|
||||
command: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
export class SimplifiedPeerMCPServer {
|
||||
private server: Server;
|
||||
private config: SimplifiedPeerConfig;
|
||||
private toolManager: CLIToolManager;
|
||||
private callDepth: number = 0;
|
||||
private maxDepth: number = 10;
|
||||
|
||||
constructor(config: SimplifiedPeerConfig) {
|
||||
this.config = config;
|
||||
this.toolManager = new CLIToolManager();
|
||||
|
||||
this.server = new Server(
|
||||
{
|
||||
name: `agent-${config.agentName}`,
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.setupHandlers();
|
||||
}
|
||||
|
||||
private setupHandlers(): void {
|
||||
this.server.setRequestHandler(ListToolsRequest, async () => {
|
||||
const tools: Tool[] = [];
|
||||
|
||||
// One tool per peer agent
|
||||
for (const peerName of this.config.peers) {
|
||||
if (peerName !== this.config.agentName) {
|
||||
tools.push({
|
||||
name: peerName,
|
||||
description: `Call the ${peerName} agent with any request. They can recursively query other agents.`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: {
|
||||
type: 'string',
|
||||
description: 'Your request to this agent',
|
||||
},
|
||||
context: {
|
||||
type: 'object',
|
||||
description: 'Optional context for the request',
|
||||
},
|
||||
},
|
||||
required: ['request'],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add shared MCP tools (GitHub, Linear, Slack, etc.)
|
||||
for (const mcp of this.config.sharedMCPs) {
|
||||
tools.push({
|
||||
name: mcp.name,
|
||||
description: `Access ${mcp.name} MCP server`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: {
|
||||
type: 'string',
|
||||
description: `Action to perform with ${mcp.name}`,
|
||||
},
|
||||
params: {
|
||||
type: 'object',
|
||||
description: 'Parameters for the action',
|
||||
},
|
||||
},
|
||||
required: ['action'],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Add Hanzo MCP tools
|
||||
tools.push(
|
||||
{
|
||||
name: 'read_file',
|
||||
description: 'Read a file from the filesystem',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'File path to read' },
|
||||
},
|
||||
required: ['path'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'write_file',
|
||||
description: 'Write content to a file',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'File path to write' },
|
||||
content: { type: 'string', description: 'Content to write' },
|
||||
},
|
||||
required: ['path', 'content'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'search',
|
||||
description: 'Search for patterns in files',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
pattern: { type: 'string', description: 'Search pattern' },
|
||||
path: { type: 'string', description: 'Path to search in' },
|
||||
},
|
||||
required: ['pattern'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'bash',
|
||||
description: 'Execute a bash command',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
command: { type: 'string', description: 'Command to execute' },
|
||||
},
|
||||
required: ['command'],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return { tools } as ListToolsResult;
|
||||
});
|
||||
|
||||
this.server.setRequestHandler(CallToolRequest, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
let result: string;
|
||||
|
||||
// Check if it's a peer agent call
|
||||
if (this.config.peers.includes(name) && name !== this.config.agentName) {
|
||||
result = await this.callPeerAgent(name, args.request, args.context);
|
||||
}
|
||||
// Check if it's a shared MCP tool
|
||||
else if (this.config.sharedMCPs.some(mcp => mcp.name === name)) {
|
||||
result = await this.callSharedMCP(name, args.action, args.params);
|
||||
}
|
||||
// Handle Hanzo MCP tools
|
||||
else {
|
||||
result = await this.handleHanzoTool(name, args);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: result,
|
||||
} as TextContent,
|
||||
],
|
||||
} as CallToolResult;
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
} as TextContent,
|
||||
],
|
||||
isError: true,
|
||||
} as CallToolResult;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async callPeerAgent(peerName: string, request: string, context?: any): Promise<string> {
|
||||
// Check recursion depth
|
||||
if (this.callDepth >= this.maxDepth) {
|
||||
return `[Max recursion depth reached]`;
|
||||
}
|
||||
|
||||
this.callDepth++;
|
||||
|
||||
try {
|
||||
// Build the prompt for the peer agent
|
||||
const prompt = `
|
||||
Request from ${this.config.agentName}: ${request}
|
||||
${context ? `\nContext: ${JSON.stringify(context, null, 2)}` : ''}
|
||||
|
||||
You are ${peerName}. Process this request according to your expertise.
|
||||
You have access to all other agents and can call them recursively if needed.
|
||||
Available agents: ${this.config.peers.filter(p => p !== peerName).join(', ')}
|
||||
`;
|
||||
|
||||
// In production, this would make an actual MCP call to the peer
|
||||
// For now, simulate the response
|
||||
const response = await this.simulatePeerResponse(peerName, prompt);
|
||||
|
||||
this.callDepth--;
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.callDepth--;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async callSharedMCP(mcpName: string, action: string, params?: any): Promise<string> {
|
||||
const mcp = this.config.sharedMCPs.find(m => m.name === mcpName);
|
||||
if (!mcp) {
|
||||
throw new Error(`MCP ${mcpName} not found`);
|
||||
}
|
||||
|
||||
// Handle different MCP servers
|
||||
switch (mcpName) {
|
||||
case 'github':
|
||||
return await this.handleGitHubMCP(action, params);
|
||||
case 'linear':
|
||||
return await this.handleLinearMCP(action, params);
|
||||
case 'slack':
|
||||
return await this.handleSlackMCP(action, params);
|
||||
case 'playwright':
|
||||
return await this.handlePlaywrightMCP(action, params);
|
||||
default:
|
||||
return `[${mcpName}] ${action} with params: ${JSON.stringify(params)}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleGitHubMCP(action: string, params: any): Promise<string> {
|
||||
// GitHub MCP actions
|
||||
switch (action) {
|
||||
case 'create_issue':
|
||||
return `Created GitHub issue: ${params.title}`;
|
||||
case 'create_pr':
|
||||
return `Created pull request: ${params.title}`;
|
||||
case 'list_issues':
|
||||
return `Found ${params.count || 10} issues`;
|
||||
case 'review_pr':
|
||||
return `Reviewed PR #${params.pr_number}`;
|
||||
default:
|
||||
return `GitHub action: ${action}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleLinearMCP(action: string, params: any): Promise<string> {
|
||||
// Linear MCP actions
|
||||
switch (action) {
|
||||
case 'create_issue':
|
||||
return `Created Linear issue: ${params.title}`;
|
||||
case 'update_issue':
|
||||
return `Updated Linear issue: ${params.id}`;
|
||||
case 'list_issues':
|
||||
return `Found Linear issues for ${params.project}`;
|
||||
default:
|
||||
return `Linear action: ${action}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleSlackMCP(action: string, params: any): Promise<string> {
|
||||
// Slack MCP actions
|
||||
switch (action) {
|
||||
case 'send_message':
|
||||
return `Sent Slack message to ${params.channel}`;
|
||||
case 'create_channel':
|
||||
return `Created Slack channel: ${params.name}`;
|
||||
case 'list_channels':
|
||||
return `Listed Slack channels`;
|
||||
default:
|
||||
return `Slack action: ${action}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async handlePlaywrightMCP(action: string, params: any): Promise<string> {
|
||||
// Playwright MCP actions
|
||||
switch (action) {
|
||||
case 'navigate':
|
||||
return `Navigated to ${params.url}`;
|
||||
case 'click':
|
||||
return `Clicked element: ${params.selector}`;
|
||||
case 'screenshot':
|
||||
return `Captured screenshot: ${params.name}`;
|
||||
case 'fill':
|
||||
return `Filled ${params.selector} with value`;
|
||||
default:
|
||||
return `Playwright action: ${action}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleHanzoTool(tool: string, args: any): Promise<string> {
|
||||
switch (tool) {
|
||||
case 'read_file':
|
||||
return `[File content of ${args.path}]`;
|
||||
case 'write_file':
|
||||
return `Wrote to ${args.path}`;
|
||||
case 'search':
|
||||
return `Found matches for "${args.pattern}" in ${args.path || '.'}`;
|
||||
case 'bash':
|
||||
return `Executed: ${args.command}`;
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${tool}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async simulatePeerResponse(peerName: string, prompt: string): Promise<string> {
|
||||
// In production, this would call the actual peer agent
|
||||
// For now, return a simulated response
|
||||
return `[${peerName}] Processed request: ${prompt.substring(0, 100)}...`;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error(`Agent ${this.config.agentName} MCP server started on port ${this.config.port}`);
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
await this.server.close();
|
||||
}
|
||||
}
|
||||
|
||||
// CLI entry point
|
||||
if (require.main === module) {
|
||||
const config = JSON.parse(process.argv[2]) as SimplifiedPeerConfig;
|
||||
const server = new SimplifiedPeerMCPServer(config);
|
||||
|
||||
server.start().catch((error) => {
|
||||
console.error('Failed to start MCP server:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
await server.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { AgentSwarmManager, AgentInstance, MCPServerConfig } from '../config/agent-swarm-config';
|
||||
import { CLIToolManager } from '../cli-tool-manager';
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import * as path from 'path';
|
||||
|
||||
interface AgentTask {
|
||||
agentName: string;
|
||||
task: string;
|
||||
context?: any;
|
||||
dependencies?: string[];
|
||||
}
|
||||
|
||||
interface AgentResult {
|
||||
agentName: string;
|
||||
result: string;
|
||||
error?: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
interface MCPServer {
|
||||
process: ChildProcess;
|
||||
config: MCPServerConfig;
|
||||
}
|
||||
|
||||
interface AgentMCPServer {
|
||||
port: number;
|
||||
process: ChildProcess;
|
||||
agentName: string;
|
||||
}
|
||||
|
||||
export class SwarmOrchestrator extends EventEmitter {
|
||||
private swarmManager: AgentSwarmManager;
|
||||
private toolManager: CLIToolManager;
|
||||
private activeAgents: Map<string, any> = new Map();
|
||||
private mcpServers: Map<string, MCPServer> = new Map();
|
||||
private agentMCPServers: Map<string, AgentMCPServer> = new Map();
|
||||
private results: Map<string, AgentResult> = new Map();
|
||||
private basePort: number = 8000; // Base port for agent MCP servers
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.swarmManager = new AgentSwarmManager();
|
||||
this.toolManager = new CLIToolManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the swarm orchestrator
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
// Load agent configuration
|
||||
await this.swarmManager.loadConfig();
|
||||
|
||||
// Initialize tool manager
|
||||
await this.toolManager.initialize();
|
||||
|
||||
// Start MCP servers for all agents
|
||||
const agents = this.swarmManager.getAllAgents();
|
||||
for (const [name, agent] of Object.entries(agents)) {
|
||||
// Apply network configuration
|
||||
const configuredAgent = this.swarmManager.applyNetworkConfig(agent, name);
|
||||
|
||||
// Start regular MCP servers
|
||||
if (configuredAgent.mcps && configuredAgent.mcps.length > 0) {
|
||||
await this.startMCPServers(name, configuredAgent.mcps);
|
||||
}
|
||||
|
||||
// Start agent as MCP server if configured
|
||||
if (configuredAgent.expose_as_mcp) {
|
||||
await this.startAgentMCPServer(name, configuredAgent);
|
||||
}
|
||||
}
|
||||
|
||||
// Connect agents to each other via MCP
|
||||
await this.connectAgentMCPServers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start MCP servers for an agent
|
||||
*/
|
||||
private async startMCPServers(agentName: string, mcps: MCPServerConfig[]): Promise<void> {
|
||||
for (const mcp of mcps) {
|
||||
const serverKey = `${agentName}-${mcp.name}`;
|
||||
|
||||
try {
|
||||
const mcpProcess = spawn(mcp.command, mcp.args || [], {
|
||||
env: { ...process.env, ...mcp.env },
|
||||
stdio: mcp.type === 'stdio' ? ['pipe', 'pipe', 'pipe'] : 'inherit'
|
||||
});
|
||||
|
||||
mcpProcess.on('error', (error) => {
|
||||
console.error(`MCP server ${serverKey} error:`, error);
|
||||
this.emit('mcp-error', { agentName, server: mcp.name, error });
|
||||
});
|
||||
|
||||
mcpProcess.on('exit', (code) => {
|
||||
console.log(`MCP server ${serverKey} exited with code ${code}`);
|
||||
this.mcpServers.delete(serverKey);
|
||||
});
|
||||
|
||||
this.mcpServers.set(serverKey, { process: mcpProcess, config: mcp });
|
||||
this.emit('mcp-started', { agentName, server: mcp.name });
|
||||
} catch (error) {
|
||||
console.error(`Failed to start MCP server ${serverKey}:`, error);
|
||||
this.emit('mcp-error', { agentName, server: mcp.name, error });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a task with the main agent
|
||||
*/
|
||||
async executeTask(task: string, context?: any): Promise<AgentResult[]> {
|
||||
const mainAgentName = this.swarmManager.getMainAgent() ?
|
||||
Object.keys(this.swarmManager.getAllAgents()).find(
|
||||
name => this.swarmManager.getAgent(name) === this.swarmManager.getMainAgent()
|
||||
) : null;
|
||||
|
||||
if (!mainAgentName) {
|
||||
throw new Error('No main agent configured');
|
||||
}
|
||||
|
||||
// Clear previous results
|
||||
this.results.clear();
|
||||
|
||||
// Execute with main agent
|
||||
const result = await this.executeAgentTask(mainAgentName, task, context);
|
||||
|
||||
// Return all results including delegated tasks
|
||||
return Array.from(this.results.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a task with a specific agent
|
||||
*/
|
||||
async executeAgentTask(
|
||||
agentName: string,
|
||||
task: string,
|
||||
context?: any
|
||||
): Promise<AgentResult> {
|
||||
const startTime = Date.now();
|
||||
const agent = this.swarmManager.getAgent(agentName);
|
||||
|
||||
if (!agent) {
|
||||
throw new Error(`Agent '${agentName}' not found`);
|
||||
}
|
||||
|
||||
this.emit('agent-start', { agentName, task });
|
||||
|
||||
try {
|
||||
// Change to agent's directory
|
||||
const originalDir = process.cwd();
|
||||
if (agent.directory && agent.directory !== '.') {
|
||||
process.chdir(path.resolve(originalDir, agent.directory));
|
||||
}
|
||||
|
||||
// Prepare agent prompt with context
|
||||
const fullPrompt = this.buildAgentPrompt(agent, task, context);
|
||||
|
||||
// Get the appropriate tool for the model
|
||||
const tool = this.getToolForModel(agent.model);
|
||||
|
||||
// Execute the task
|
||||
const result = await this.toolManager.runTool(tool, fullPrompt, {
|
||||
model: agent.model,
|
||||
cwd: process.cwd(),
|
||||
env: this.buildAgentEnvironment(agentName, agent)
|
||||
});
|
||||
|
||||
// Change back to original directory
|
||||
process.chdir(originalDir);
|
||||
|
||||
// Parse agent response for delegations
|
||||
const delegations = this.parseDelegations(result, agent);
|
||||
|
||||
// Execute delegated tasks in parallel
|
||||
if (delegations.length > 0) {
|
||||
await this.executeDelegatedTasks(delegations, {
|
||||
parentAgent: agentName,
|
||||
parentTask: task,
|
||||
context
|
||||
});
|
||||
}
|
||||
|
||||
const agentResult: AgentResult = {
|
||||
agentName,
|
||||
result,
|
||||
duration: Date.now() - startTime
|
||||
};
|
||||
|
||||
this.results.set(agentName, agentResult);
|
||||
this.emit('agent-complete', agentResult);
|
||||
|
||||
return agentResult;
|
||||
} catch (error) {
|
||||
const agentResult: AgentResult = {
|
||||
agentName,
|
||||
result: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration: Date.now() - startTime
|
||||
};
|
||||
|
||||
this.results.set(agentName, agentResult);
|
||||
this.emit('agent-error', agentResult);
|
||||
|
||||
return agentResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full prompt for an agent
|
||||
*/
|
||||
private buildAgentPrompt(agent: AgentInstance, task: string, context?: any): string {
|
||||
let prompt = agent.prompt + '\n\n';
|
||||
|
||||
// Add context if provided
|
||||
if (context) {
|
||||
prompt += `## Context\n${JSON.stringify(context, null, 2)}\n\n`;
|
||||
}
|
||||
|
||||
// Add task
|
||||
prompt += `## Task\n${task}\n\n`;
|
||||
|
||||
// Simplified: Each agent is just one tool
|
||||
const allAgents = this.swarmManager.getAllAgents();
|
||||
const agentName = this.getAgentName(agent);
|
||||
|
||||
prompt += `## Available Agents (as Tools)\n`;
|
||||
prompt += `Each agent is available as a single tool. Just use their name:\n\n`;
|
||||
|
||||
for (const [name, otherAgent] of Object.entries(allAgents)) {
|
||||
if (name !== agentName) {
|
||||
prompt += `- **${name}**: ${otherAgent.description}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
prompt += `\nTo use an agent, call the tool with their name and your request.\n`;
|
||||
prompt += `Example: Use tool "developer" with request: "implement the login feature"\n\n`;
|
||||
|
||||
// Add shared MCP tools if configured
|
||||
prompt += `## Shared Tools\n`;
|
||||
prompt += `- **read_file**: Read file content\n`;
|
||||
prompt += `- **write_file**: Write to file\n`;
|
||||
prompt += `- **search**: Search in files\n`;
|
||||
prompt += `- **bash**: Execute commands\n`;
|
||||
prompt += `- **github**: GitHub integration (if configured)\n`;
|
||||
prompt += `- **linear**: Linear project management (if configured)\n`;
|
||||
prompt += `- **slack**: Slack messaging (if configured)\n`;
|
||||
prompt += `- **playwright**: Web automation (if configured)\n\n`;
|
||||
|
||||
prompt += `Agents can recursively call each other. Be mindful of recursion depth.\n`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent name from the swarm manager
|
||||
*/
|
||||
private getAgentName(agent: AgentInstance): string {
|
||||
const agents = this.swarmManager.getAllAgents();
|
||||
for (const [name, a] of Object.entries(agents)) {
|
||||
if (a === agent) return name;
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build environment variables for an agent
|
||||
*/
|
||||
private buildAgentEnvironment(agentName: string, agent: AgentInstance): Record<string, string> {
|
||||
const env: Record<string, string> = {
|
||||
...process.env,
|
||||
AGENT_NAME: agentName,
|
||||
AGENT_MODEL: agent.model,
|
||||
AGENT_DIR: agent.directory
|
||||
};
|
||||
|
||||
// Add MCP server endpoints if available
|
||||
const mcpServers = Array.from(this.mcpServers.entries())
|
||||
.filter(([key]) => key.startsWith(`${agentName}-`));
|
||||
|
||||
if (mcpServers.length > 0) {
|
||||
env.MCP_SERVERS = mcpServers.map(([key]) => key).join(',');
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate tool for a model
|
||||
*/
|
||||
private getToolForModel(model: string): string {
|
||||
const modelMap: Record<string, string> = {
|
||||
'opus': 'claude',
|
||||
'sonnet': 'claude',
|
||||
'haiku': 'claude',
|
||||
'gpt-4': 'codex',
|
||||
'gpt-3.5': 'codex',
|
||||
'gemini-pro': 'gemini',
|
||||
'gemini-ultra': 'gemini'
|
||||
};
|
||||
|
||||
return modelMap[model.toLowerCase()] || 'claude';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse agent response for delegation commands
|
||||
*/
|
||||
private parseDelegations(response: string, agent: AgentInstance): AgentTask[] {
|
||||
if (!agent.connections || agent.connections.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const delegations: AgentTask[] = [];
|
||||
const delegateRegex = /DELEGATE TO \[([^\]]+)\]: (.+?)(?=DELEGATE TO|$)/gs;
|
||||
|
||||
let match;
|
||||
while ((match = delegateRegex.exec(response)) !== null) {
|
||||
const agentName = match[1].trim();
|
||||
const task = match[2].trim();
|
||||
|
||||
if (agent.connections.includes(agentName)) {
|
||||
delegations.push({ agentName, task });
|
||||
}
|
||||
}
|
||||
|
||||
return delegations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute delegated tasks in parallel
|
||||
*/
|
||||
private async executeDelegatedTasks(
|
||||
tasks: AgentTask[],
|
||||
context: any
|
||||
): Promise<void> {
|
||||
const promises = tasks.map(task =>
|
||||
this.executeAgentTask(task.agentName, task.task, context)
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an agent as an MCP server
|
||||
*/
|
||||
private async startAgentMCPServer(agentName: string, agent: AgentInstance): Promise<void> {
|
||||
const port = agent.mcp_port || this.basePort++;
|
||||
|
||||
try {
|
||||
// Create MCP server process for the agent
|
||||
const agentConfigJson = JSON.stringify(agent);
|
||||
const mcpServerPath = path.join(__dirname, 'agent-mcp-server.js');
|
||||
|
||||
const mcpProcess = spawn('node', [
|
||||
mcpServerPath,
|
||||
agentName,
|
||||
agentConfigJson
|
||||
], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: {
|
||||
...process.env,
|
||||
AGENT_MCP_PORT: port.toString()
|
||||
}
|
||||
});
|
||||
|
||||
mcpProcess.on('error', (error) => {
|
||||
console.error(`Agent MCP server ${agentName} error:`, error);
|
||||
this.emit('agent-mcp-error', { agentName, error });
|
||||
});
|
||||
|
||||
mcpProcess.on('exit', (code) => {
|
||||
console.log(`Agent MCP server ${agentName} exited with code ${code}`);
|
||||
this.agentMCPServers.delete(agentName);
|
||||
});
|
||||
|
||||
// Log stderr for debugging
|
||||
mcpProcess.stderr.on('data', (data) => {
|
||||
console.error(`[${agentName}] ${data.toString()}`);
|
||||
});
|
||||
|
||||
this.agentMCPServers.set(agentName, {
|
||||
port,
|
||||
process: mcpProcess,
|
||||
agentName
|
||||
});
|
||||
|
||||
this.emit('agent-mcp-started', { agentName, port });
|
||||
} catch (error) {
|
||||
console.error(`Failed to start agent MCP server for ${agentName}:`, error);
|
||||
this.emit('agent-mcp-error', { agentName, error });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect agents to each other via MCP
|
||||
*/
|
||||
private async connectAgentMCPServers(): Promise<void> {
|
||||
const agents = this.swarmManager.getAllAgents();
|
||||
|
||||
for (const [name, agent] of Object.entries(agents)) {
|
||||
const configuredAgent = this.swarmManager.applyNetworkConfig(agent, name);
|
||||
|
||||
if (configuredAgent.connect_to_agents && configuredAgent.connect_to_agents.length > 0) {
|
||||
for (const targetAgent of configuredAgent.connect_to_agents) {
|
||||
const targetServer = this.agentMCPServers.get(targetAgent);
|
||||
if (targetServer) {
|
||||
// Create MCP client configuration for connecting to the target agent
|
||||
const mcpConfig: MCPServerConfig = {
|
||||
name: `agent-${targetAgent}`,
|
||||
type: 'stdio',
|
||||
command: 'node',
|
||||
args: [
|
||||
path.join(__dirname, 'agent-mcp-server.js'),
|
||||
targetAgent,
|
||||
JSON.stringify(this.swarmManager.getAgent(targetAgent))
|
||||
],
|
||||
env: {
|
||||
AGENT_MCP_PORT: targetServer.port.toString(),
|
||||
AGENT_NAME: targetAgent
|
||||
}
|
||||
};
|
||||
|
||||
// Store the MCP connection for this agent
|
||||
const connectionKey = `${name}-connects-to-${targetAgent}`;
|
||||
await this.startMCPServers(connectionKey, [mcpConfig]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the orchestrator
|
||||
*/
|
||||
async shutdown(): Promise<void> {
|
||||
// Stop all MCP servers
|
||||
for (const [key, server] of this.mcpServers.entries()) {
|
||||
server.process.kill();
|
||||
this.mcpServers.delete(key);
|
||||
}
|
||||
|
||||
// Stop all agent MCP servers
|
||||
for (const [key, server] of this.agentMCPServers.entries()) {
|
||||
server.process.kill();
|
||||
this.agentMCPServers.delete(key);
|
||||
}
|
||||
|
||||
// Clear active agents
|
||||
this.activeAgents.clear();
|
||||
|
||||
this.emit('shutdown');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get swarm status
|
||||
*/
|
||||
getStatus(): {
|
||||
agents: Record<string, { status: string; directory: string; model: string }>;
|
||||
mcpServers: Record<string, { status: string; config: MCPServerConfig }>;
|
||||
results: AgentResult[];
|
||||
} {
|
||||
const agents: Record<string, any> = {};
|
||||
const allAgents = this.swarmManager.getAllAgents();
|
||||
|
||||
for (const [name, agent] of Object.entries(allAgents)) {
|
||||
agents[name] = {
|
||||
status: this.activeAgents.has(name) ? 'active' : 'ready',
|
||||
directory: agent.directory,
|
||||
model: agent.model
|
||||
};
|
||||
}
|
||||
|
||||
const mcpServers: Record<string, any> = {};
|
||||
for (const [key, server] of this.mcpServers.entries()) {
|
||||
mcpServers[key] = {
|
||||
status: server.process.exitCode === null ? 'running' : 'stopped',
|
||||
config: server.config
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
agents,
|
||||
mcpServers,
|
||||
results: Array.from(this.results.values())
|
||||
};
|
||||
}
|
||||
}
|
||||
+301
@@ -531,6 +531,307 @@ program
|
||||
})
|
||||
);
|
||||
|
||||
// Swarm command - manage agent swarms
|
||||
program
|
||||
.command('swarm')
|
||||
.description('Manage AI agent swarms for complex multi-agent workflows')
|
||||
.addCommand(
|
||||
new Command('init')
|
||||
.description('Initialize a new agent swarm configuration')
|
||||
.option('-p, --path <path>', 'Path for the configuration file', '.hanzo/agents.yaml')
|
||||
.option('--peer-network', 'Initialize as peer network with Hanzo Zen main loop')
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Initializing agent swarm configuration...').start();
|
||||
|
||||
try {
|
||||
const { AgentSwarmManager } = await import('../cli-tools/config/agent-swarm-config');
|
||||
|
||||
if (options.peerNetwork) {
|
||||
// Create peer network configuration
|
||||
await AgentSwarmManager.initPeerNetworkConfig(options.path);
|
||||
spinner.succeed('Peer network configuration initialized with Hanzo Zen!');
|
||||
console.log(chalk.cyan('Using Hanzo Zen for cost-effective local orchestration'));
|
||||
} else {
|
||||
await AgentSwarmManager.initConfig(options.path);
|
||||
spinner.succeed('Agent swarm configuration initialized!');
|
||||
}
|
||||
|
||||
console.log(chalk.gray(`Edit the configuration file to customize your agent swarm.`));
|
||||
} catch (error) {
|
||||
spinner.fail(`Failed to initialize: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
)
|
||||
.addCommand(
|
||||
new Command('run <task>')
|
||||
.description('Run a task with the configured agent swarm')
|
||||
.option('-c, --config <path>', 'Path to agent configuration file')
|
||||
.option('--peer', 'Run as peer network with Hanzo Zen orchestration')
|
||||
.option('--critic', 'Include critic analysis in results')
|
||||
.option('--local-llm <endpoint>', 'Local LLM endpoint', 'http://localhost:8080')
|
||||
.action(async (task, options) => {
|
||||
const spinner = ora('Starting agent swarm...').start();
|
||||
|
||||
try {
|
||||
if (options.peer) {
|
||||
// Use peer network with local Hanzo Zen
|
||||
const { PeerAgentNetwork } = await import('../cli-tools/orchestration/peer-agent-network');
|
||||
|
||||
const network = new PeerAgentNetwork({
|
||||
mainLoopLLM: {
|
||||
model: 'hanzo-zen',
|
||||
endpoint: options.localLlm
|
||||
},
|
||||
enableRecursiveCalls: true,
|
||||
maxRecursionDepth: 10,
|
||||
costOptimization: true
|
||||
});
|
||||
|
||||
await network.initialize();
|
||||
spinner.succeed('Peer network initialized with Hanzo Zen');
|
||||
|
||||
console.log(chalk.cyan('\n🌐 Executing with peer network...'));
|
||||
console.log(chalk.gray('Using local Hanzo Zen for orchestration (cost-optimized)'));
|
||||
|
||||
const results = await network.executeTask(task, {
|
||||
requireCritic: options.critic
|
||||
});
|
||||
|
||||
console.log(chalk.bold('\n📊 Peer Network Results:\n'));
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
|
||||
await network.shutdown();
|
||||
} else {
|
||||
// Traditional swarm orchestration
|
||||
const { SwarmOrchestrator } = await import('../cli-tools/orchestration/swarm-orchestrator');
|
||||
const orchestrator = new SwarmOrchestrator();
|
||||
|
||||
// Initialize with custom config path if provided
|
||||
if (options.config) {
|
||||
const { AgentSwarmManager } = await import('../cli-tools/config/agent-swarm-config');
|
||||
const manager = new AgentSwarmManager(options.config);
|
||||
await manager.loadConfig();
|
||||
}
|
||||
|
||||
await orchestrator.initialize();
|
||||
spinner.succeed('Agent swarm initialized');
|
||||
|
||||
console.log(chalk.cyan('\nExecuting task with agent swarm...'));
|
||||
const results = await orchestrator.executeTask(task);
|
||||
|
||||
console.log(chalk.bold('\n📊 Swarm Results:\n'));
|
||||
for (const result of results) {
|
||||
console.log(chalk.cyan(`🤖 ${result.agentName}:`));
|
||||
if (result.error) {
|
||||
console.log(chalk.red(` ❌ Error: ${result.error}`));
|
||||
} else {
|
||||
console.log(` ✅ Completed in ${result.duration}ms`);
|
||||
console.log(chalk.gray(` ${result.result.substring(0, 200)}...`));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
await orchestrator.shutdown();
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.fail(`Execution failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
)
|
||||
.addCommand(
|
||||
new Command('status')
|
||||
.description('Show status of the current agent swarm')
|
||||
.option('-c, --config <path>', 'Path to agent configuration file')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const { SwarmOrchestrator } = await import('../cli-tools/orchestration/swarm-orchestrator');
|
||||
const orchestrator = new SwarmOrchestrator();
|
||||
|
||||
if (options.config) {
|
||||
const { AgentSwarmManager } = await import('../cli-tools/config/agent-swarm-config');
|
||||
const manager = new AgentSwarmManager(options.config);
|
||||
await manager.loadConfig();
|
||||
}
|
||||
|
||||
await orchestrator.initialize();
|
||||
const status = orchestrator.getStatus();
|
||||
|
||||
console.log(chalk.bold('🐝 Agent Swarm Status\n'));
|
||||
|
||||
// Show agents
|
||||
console.log(chalk.cyan('Agents:'));
|
||||
for (const [name, agent] of Object.entries(status.agents)) {
|
||||
const statusColor = agent.status === 'active' ? chalk.green : chalk.gray;
|
||||
console.log(` ${statusColor('●')} ${name} - ${agent.model} (${agent.directory})`);
|
||||
}
|
||||
|
||||
// Show MCP servers
|
||||
if (Object.keys(status.mcpServers).length > 0) {
|
||||
console.log(chalk.cyan('\nMCP Servers:'));
|
||||
for (const [key, server] of Object.entries(status.mcpServers)) {
|
||||
const statusColor = server.status === 'running' ? chalk.green : chalk.red;
|
||||
console.log(` ${statusColor('●')} ${key} - ${server.config.command}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Show recent results
|
||||
if (status.results.length > 0) {
|
||||
console.log(chalk.cyan('\nRecent Results:'));
|
||||
for (const result of status.results) {
|
||||
const icon = result.error ? '❌' : '✅';
|
||||
console.log(` ${icon} ${result.agentName} - ${result.duration}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
await orchestrator.shutdown();
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error checking status: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
)
|
||||
.addCommand(
|
||||
new Command('network <networkName> [task]')
|
||||
.description('Run a task with agents in a specific network')
|
||||
.option('-c, --config <path>', 'Path to agent configuration file')
|
||||
.action(async (networkName, task, options) => {
|
||||
const spinner = ora('Starting network agents...').start();
|
||||
|
||||
try {
|
||||
const { SwarmOrchestrator } = await import('../cli-tools/orchestration/swarm-orchestrator');
|
||||
const { AgentSwarmManager } = await import('../cli-tools/config/agent-swarm-config');
|
||||
|
||||
const manager = new AgentSwarmManager(options.config);
|
||||
await manager.loadConfig();
|
||||
|
||||
const network = manager.getNetwork(networkName);
|
||||
if (!network) {
|
||||
spinner.fail(`Network '${networkName}' not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
spinner.succeed(`Network '${networkName}' loaded`);
|
||||
console.log(chalk.cyan(`\nAgents in network: ${network.agents.join(', ')}`));
|
||||
|
||||
if (task) {
|
||||
const orchestrator = new SwarmOrchestrator();
|
||||
await orchestrator.initialize();
|
||||
|
||||
console.log(chalk.cyan('\nExecuting task with network agents...'));
|
||||
|
||||
// Execute task with first agent in network as main
|
||||
const mainAgent = network.agents[0];
|
||||
const results = await orchestrator.executeTask(task);
|
||||
|
||||
console.log(chalk.bold('\n📊 Network Results:\n'));
|
||||
for (const result of results) {
|
||||
if (network.agents.includes(result.agentName)) {
|
||||
console.log(chalk.cyan(`🤖 ${result.agentName}:`));
|
||||
if (result.error) {
|
||||
console.log(chalk.red(` ❌ Error: ${result.error}`));
|
||||
} else {
|
||||
console.log(` ✅ Completed in ${result.duration}ms`);
|
||||
console.log(chalk.gray(` ${result.result.substring(0, 200)}...`));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
await orchestrator.shutdown();
|
||||
} else {
|
||||
console.log(chalk.gray('\nProvide a task to execute with this network'));
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.fail(`Network operation failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
)
|
||||
.addCommand(
|
||||
new Command('chat')
|
||||
.description('Start an interactive chat session between agents')
|
||||
.option('-c, --config <path>', 'Path to agent configuration file')
|
||||
.option('-f, --from <agent>', 'Agent to chat from', 'project_manager')
|
||||
.option('-t, --to <agent>', 'Agent to chat with')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const { SwarmOrchestrator } = await import('../cli-tools/orchestration/swarm-orchestrator');
|
||||
const { AgentSwarmManager } = await import('../cli-tools/config/agent-swarm-config');
|
||||
|
||||
const manager = new AgentSwarmManager(options.config);
|
||||
await manager.loadConfig();
|
||||
|
||||
const fromAgent = manager.getAgent(options.from);
|
||||
const toAgent = options.to ? manager.getAgent(options.to) : null;
|
||||
|
||||
if (!fromAgent) {
|
||||
console.error(chalk.red(`Agent '${options.from}' not found`));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold('🗨️ Agent Chat Session\n'));
|
||||
console.log(chalk.cyan(`Chatting as: ${options.from}`));
|
||||
|
||||
if (toAgent) {
|
||||
console.log(chalk.cyan(`Chatting with: ${options.to}`));
|
||||
} else {
|
||||
console.log(chalk.gray('Type "@agent_name message" to chat with a specific agent'));
|
||||
}
|
||||
|
||||
console.log(chalk.gray('Type "exit" to end the session\n'));
|
||||
|
||||
const orchestrator = new SwarmOrchestrator();
|
||||
await orchestrator.initialize();
|
||||
|
||||
// Interactive chat loop
|
||||
while (true) {
|
||||
const { message } = await inquirer.prompt([{
|
||||
type: 'input',
|
||||
name: 'message',
|
||||
message: `${options.from}>`,
|
||||
}]);
|
||||
|
||||
if (message.toLowerCase() === 'exit') {
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse @mentions
|
||||
const mentionMatch = message.match(/^@(\w+)\s+(.+)/);
|
||||
const targetAgent = mentionMatch ? mentionMatch[1] : options.to;
|
||||
const actualMessage = mentionMatch ? mentionMatch[2] : message;
|
||||
|
||||
if (!targetAgent) {
|
||||
console.log(chalk.yellow('Please specify a target agent with @agent_name or use -t flag'));
|
||||
continue;
|
||||
}
|
||||
|
||||
const spinner = ora(`${options.from} is thinking...`).start();
|
||||
|
||||
try {
|
||||
// Simulate the from agent using MCP to chat with target
|
||||
const prompt = `You need to send this message to ${targetAgent}: "${actualMessage}"\n\n` +
|
||||
`Use the chat_with_${targetAgent} tool to send the message and get a response.`;
|
||||
|
||||
const result = await orchestrator.executeAgentTask(options.from, prompt);
|
||||
|
||||
spinner.stop();
|
||||
console.log(chalk.green(`\n${targetAgent}> ${result}\n`));
|
||||
} catch (error) {
|
||||
spinner.fail(`Chat failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await orchestrator.shutdown();
|
||||
console.log(chalk.gray('\nChat session ended'));
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Interactive mode
|
||||
program
|
||||
.command('interactive')
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import {
|
||||
AgentSwarmManager,
|
||||
AgentInstance,
|
||||
AgentSwarmConfig,
|
||||
NetworkConfig,
|
||||
MCPServerConfig,
|
||||
SharedMCPConfig
|
||||
} from '../../../cli-tools/config/agent-swarm-config';
|
||||
|
||||
vi.mock('fs');
|
||||
vi.mock('path');
|
||||
|
||||
describe('AgentSwarmManager', () => {
|
||||
let manager: AgentSwarmManager;
|
||||
const mockConfigPath = '/test/agents.yaml';
|
||||
|
||||
const mockConfig: AgentSwarmConfig = {
|
||||
version: 1,
|
||||
swarm: {
|
||||
name: 'Test Swarm',
|
||||
main: 'coordinator',
|
||||
instances: {
|
||||
coordinator: {
|
||||
description: 'Main coordinator',
|
||||
directory: '.',
|
||||
model: 'opus',
|
||||
connections: ['developer', 'reviewer'],
|
||||
expose_as_mcp: true,
|
||||
mcp_port: 8000,
|
||||
connect_to_agents: ['developer', 'reviewer'],
|
||||
prompt: 'You are the coordinator',
|
||||
allowed_tools: ['Read', 'Write'],
|
||||
mcps: [{
|
||||
name: 'filesystem',
|
||||
type: 'stdio',
|
||||
command: 'npx',
|
||||
args: ['mcp-server-filesystem']
|
||||
}]
|
||||
},
|
||||
developer: {
|
||||
description: 'Developer agent',
|
||||
directory: './src',
|
||||
model: 'sonnet',
|
||||
prompt: 'You are a developer'
|
||||
},
|
||||
reviewer: {
|
||||
description: 'Code reviewer',
|
||||
directory: '.',
|
||||
model: 'haiku',
|
||||
prompt: 'You are a reviewer'
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
core: {
|
||||
name: 'Core Team',
|
||||
agents: ['coordinator', 'developer', 'reviewer'],
|
||||
mcp_enabled: true,
|
||||
shared_tools: ['Read', 'Grep']
|
||||
}
|
||||
},
|
||||
shared_mcps: {
|
||||
github: { enabled: true, token: 'test-token' },
|
||||
linear: { enabled: true, apiKey: 'test-key' },
|
||||
slack: { enabled: false },
|
||||
custom: [{
|
||||
name: 'postgres',
|
||||
type: 'stdio',
|
||||
command: 'pg-mcp',
|
||||
env: { DATABASE_URL: 'postgres://test' }
|
||||
}]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
manager = new AgentSwarmManager(mockConfigPath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('loadConfig', () => {
|
||||
it('should load configuration from default path', async () => {
|
||||
const configYaml = yaml.dump(mockConfig);
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(configYaml);
|
||||
|
||||
const config = await manager.loadConfig();
|
||||
|
||||
expect(config).toEqual(mockConfig);
|
||||
expect(fs.existsSync).toHaveBeenCalled();
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith(mockConfigPath, 'utf8');
|
||||
});
|
||||
|
||||
it('should try multiple paths if default not found', async () => {
|
||||
const configYaml = yaml.dump(mockConfig);
|
||||
vi.mocked(fs.existsSync)
|
||||
.mockReturnValueOnce(false) // .hanzo/agents.yaml
|
||||
.mockReturnValueOnce(false) // agents.yaml
|
||||
.mockReturnValueOnce(true); // .agents.yaml
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(configYaml);
|
||||
|
||||
const config = await manager.loadConfig();
|
||||
|
||||
expect(config).toEqual(mockConfig);
|
||||
expect(fs.existsSync).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should throw error if no config found', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
await expect(manager.loadConfig()).rejects.toThrow('No agent configuration found');
|
||||
});
|
||||
|
||||
it('should throw error for invalid version', async () => {
|
||||
const invalidConfig = { ...mockConfig, version: 2 };
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(invalidConfig));
|
||||
|
||||
await expect(manager.loadConfig()).rejects.toThrow('Invalid or missing version');
|
||||
});
|
||||
|
||||
it('should throw error for missing main agent', async () => {
|
||||
const invalidConfig = {
|
||||
...mockConfig,
|
||||
swarm: {
|
||||
...mockConfig.swarm,
|
||||
main: 'nonexistent'
|
||||
}
|
||||
};
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(invalidConfig));
|
||||
|
||||
await expect(manager.loadConfig()).rejects.toThrow("Main agent 'nonexistent' not found");
|
||||
});
|
||||
|
||||
it('should throw error for invalid agent connections', async () => {
|
||||
const invalidConfig = {
|
||||
...mockConfig,
|
||||
swarm: {
|
||||
...mockConfig.swarm,
|
||||
instances: {
|
||||
...mockConfig.swarm.instances,
|
||||
coordinator: {
|
||||
...mockConfig.swarm.instances.coordinator,
|
||||
connections: ['nonexistent']
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(invalidConfig));
|
||||
|
||||
await expect(manager.loadConfig()).rejects.toThrow("invalid connection to 'nonexistent'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMainAgent', () => {
|
||||
it('should return main agent when config loaded', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(mockConfig));
|
||||
await manager.loadConfig();
|
||||
|
||||
const mainAgent = manager.getMainAgent();
|
||||
|
||||
expect(mainAgent).toEqual(mockConfig.swarm.instances.coordinator);
|
||||
});
|
||||
|
||||
it('should return null when no config loaded', () => {
|
||||
const mainAgent = manager.getMainAgent();
|
||||
expect(mainAgent).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAgent', () => {
|
||||
beforeEach(async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(mockConfig));
|
||||
await manager.loadConfig();
|
||||
});
|
||||
|
||||
it('should return specific agent', () => {
|
||||
const agent = manager.getAgent('developer');
|
||||
expect(agent).toEqual(mockConfig.swarm.instances.developer);
|
||||
});
|
||||
|
||||
it('should return null for non-existent agent', () => {
|
||||
const agent = manager.getAgent('nonexistent');
|
||||
expect(agent).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllAgents', () => {
|
||||
it('should return all agents', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(mockConfig));
|
||||
await manager.loadConfig();
|
||||
|
||||
const agents = manager.getAllAgents();
|
||||
|
||||
expect(agents).toEqual(mockConfig.swarm.instances);
|
||||
expect(Object.keys(agents)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should return empty object when no config', () => {
|
||||
const agents = manager.getAllAgents();
|
||||
expect(agents).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConnectedAgents', () => {
|
||||
beforeEach(async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(mockConfig));
|
||||
await manager.loadConfig();
|
||||
});
|
||||
|
||||
it('should return connected agents', () => {
|
||||
const connected = manager.getConnectedAgents('coordinator');
|
||||
|
||||
expect(connected).toHaveLength(2);
|
||||
expect(connected[0]).toEqual(mockConfig.swarm.instances.developer);
|
||||
expect(connected[1]).toEqual(mockConfig.swarm.instances.reviewer);
|
||||
});
|
||||
|
||||
it('should return empty array for agent without connections', () => {
|
||||
const connected = manager.getConnectedAgents('developer');
|
||||
expect(connected).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNetwork', () => {
|
||||
beforeEach(async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(mockConfig));
|
||||
await manager.loadConfig();
|
||||
});
|
||||
|
||||
it('should return network configuration', () => {
|
||||
const network = manager.getNetwork('core');
|
||||
expect(network).toEqual(mockConfig.swarm.networks!.core);
|
||||
});
|
||||
|
||||
it('should return null for non-existent network', () => {
|
||||
const network = manager.getNetwork('nonexistent');
|
||||
expect(network).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNetworkAgents', () => {
|
||||
beforeEach(async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(mockConfig));
|
||||
await manager.loadConfig();
|
||||
});
|
||||
|
||||
it('should return agents in network', () => {
|
||||
const agents = manager.getNetworkAgents('core');
|
||||
|
||||
expect(agents).toHaveLength(3);
|
||||
expect(agents.map(a => a.description)).toContain('Main coordinator');
|
||||
});
|
||||
|
||||
it('should return empty array for non-existent network', () => {
|
||||
const agents = manager.getNetworkAgents('nonexistent');
|
||||
expect(agents).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAgentNetworks', () => {
|
||||
beforeEach(async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(mockConfig));
|
||||
await manager.loadConfig();
|
||||
});
|
||||
|
||||
it('should return networks containing agent', () => {
|
||||
const networks = manager.getAgentNetworks('developer');
|
||||
|
||||
expect(networks).toHaveLength(1);
|
||||
expect(networks[0].name).toBe('Core Team');
|
||||
});
|
||||
|
||||
it('should return empty array for agent not in any network', () => {
|
||||
const testConfig = {
|
||||
...mockConfig,
|
||||
swarm: {
|
||||
...mockConfig.swarm,
|
||||
instances: {
|
||||
...mockConfig.swarm.instances,
|
||||
isolated: {
|
||||
description: 'Isolated agent',
|
||||
directory: '.',
|
||||
model: 'haiku',
|
||||
prompt: 'Isolated'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(testConfig));
|
||||
manager = new AgentSwarmManager(mockConfigPath);
|
||||
manager.loadConfig();
|
||||
|
||||
const networks = manager.getAgentNetworks('isolated');
|
||||
expect(networks).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyNetworkConfig', () => {
|
||||
beforeEach(async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(mockConfig));
|
||||
await manager.loadConfig();
|
||||
});
|
||||
|
||||
it('should apply network configuration to agent', () => {
|
||||
const agent = manager.getAgent('developer')!;
|
||||
const configured = manager.applyNetworkConfig(agent, 'developer');
|
||||
|
||||
expect(configured.allowed_tools).toContain('Read');
|
||||
expect(configured.allowed_tools).toContain('Grep');
|
||||
expect(configured.connect_to_agents).toContain('coordinator');
|
||||
expect(configured.connect_to_agents).toContain('reviewer');
|
||||
});
|
||||
|
||||
it('should handle agent not in any network', () => {
|
||||
const agent: AgentInstance = {
|
||||
description: 'Test',
|
||||
directory: '.',
|
||||
model: 'haiku',
|
||||
prompt: 'Test'
|
||||
};
|
||||
|
||||
const configured = manager.applyNetworkConfig(agent, 'nonexistent');
|
||||
expect(configured).toEqual(agent);
|
||||
});
|
||||
|
||||
it('should remove duplicates from arrays', () => {
|
||||
const testConfig = {
|
||||
...mockConfig,
|
||||
swarm: {
|
||||
...mockConfig.swarm,
|
||||
networks: {
|
||||
net1: {
|
||||
name: 'Network 1',
|
||||
agents: ['developer'],
|
||||
mcp_enabled: true,
|
||||
shared_tools: ['Read', 'Write']
|
||||
},
|
||||
net2: {
|
||||
name: 'Network 2',
|
||||
agents: ['developer'],
|
||||
mcp_enabled: true,
|
||||
shared_tools: ['Read', 'Grep']
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(testConfig));
|
||||
manager = new AgentSwarmManager(mockConfigPath);
|
||||
manager.loadConfig();
|
||||
|
||||
const agent = manager.getAgent('developer')!;
|
||||
const configured = manager.applyNetworkConfig(agent, 'developer');
|
||||
|
||||
// Should have unique tools
|
||||
const uniqueTools = [...new Set(configured.allowed_tools)];
|
||||
expect(configured.allowed_tools).toEqual(uniqueTools);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfig', () => {
|
||||
it('should return full configuration', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(mockConfig));
|
||||
await manager.loadConfig();
|
||||
|
||||
const config = manager.getConfig();
|
||||
expect(config).toEqual(mockConfig);
|
||||
});
|
||||
|
||||
it('should return null when no config loaded', () => {
|
||||
const config = manager.getConfig();
|
||||
expect(config).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('static methods', () => {
|
||||
describe('initConfig', () => {
|
||||
it('should create new configuration file', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
vi.mocked(fs.mkdirSync).mockImplementation(() => undefined);
|
||||
vi.mocked(fs.writeFileSync).mockImplementation(() => undefined);
|
||||
vi.mocked(path.dirname).mockReturnValue('.hanzo');
|
||||
|
||||
await AgentSwarmManager.initConfig('/test/agents.yaml');
|
||||
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith('.hanzo', { recursive: true });
|
||||
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw if config already exists', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
|
||||
await expect(AgentSwarmManager.initConfig('/test/agents.yaml'))
|
||||
.rejects.toThrow('Configuration already exists');
|
||||
});
|
||||
});
|
||||
|
||||
describe('initPeerNetworkConfig', () => {
|
||||
it('should create peer network configuration', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
vi.mocked(fs.mkdirSync).mockImplementation(() => undefined);
|
||||
vi.mocked(fs.writeFileSync).mockImplementation(() => undefined);
|
||||
vi.mocked(path.dirname).mockReturnValue('.hanzo');
|
||||
|
||||
await AgentSwarmManager.initPeerNetworkConfig('/test/agents-peer.yaml');
|
||||
|
||||
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||
const writtenContent = vi.mocked(fs.writeFileSync).mock.calls[0][1] as string;
|
||||
expect(writtenContent).toContain('hanzo-zen');
|
||||
expect(writtenContent).toContain('network_type: peer');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSampleConfig', () => {
|
||||
it('should create valid sample configuration', () => {
|
||||
const sampleYaml = AgentSwarmManager.createSampleConfig();
|
||||
const sample = yaml.load(sampleYaml) as AgentSwarmConfig;
|
||||
|
||||
expect(sample.version).toBe(1);
|
||||
expect(sample.swarm.name).toBe('Development Team');
|
||||
expect(sample.swarm.main).toBe('architect');
|
||||
expect(Object.keys(sample.swarm.instances)).toContain('architect');
|
||||
expect(Object.keys(sample.swarm.instances)).toContain('frontend');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPeerNetworkConfig', () => {
|
||||
it('should create valid peer network configuration', () => {
|
||||
const peerYaml = AgentSwarmManager.createPeerNetworkConfig();
|
||||
const peer = yaml.load(peerYaml) as any;
|
||||
|
||||
expect(peer.version).toBe(1);
|
||||
expect(peer.swarm.network_type).toBe('peer');
|
||||
expect(peer.swarm.local_llm.model).toBe('hanzo-zen');
|
||||
expect(Object.keys(peer.swarm.instances)).toContain('orchestrator');
|
||||
expect(peer.swarm.instances.orchestrator.model).toBe('zen');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty networks configuration', async () => {
|
||||
const configWithoutNetworks = {
|
||||
...mockConfig,
|
||||
swarm: {
|
||||
...mockConfig.swarm,
|
||||
networks: undefined
|
||||
}
|
||||
};
|
||||
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(configWithoutNetworks));
|
||||
await manager.loadConfig();
|
||||
|
||||
const network = manager.getNetwork('any');
|
||||
expect(network).toBeNull();
|
||||
|
||||
const networks = manager.getAgentNetworks('coordinator');
|
||||
expect(networks).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle agent without required fields', async () => {
|
||||
const invalidAgent = {
|
||||
...mockConfig,
|
||||
swarm: {
|
||||
...mockConfig.swarm,
|
||||
instances: {
|
||||
invalid: {
|
||||
description: 'Invalid agent'
|
||||
// Missing required fields
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(invalidAgent));
|
||||
|
||||
await expect(manager.loadConfig()).rejects.toThrow('missing required fields');
|
||||
});
|
||||
|
||||
it('should handle invalid MCP server config', async () => {
|
||||
const invalidMCP = {
|
||||
...mockConfig,
|
||||
swarm: {
|
||||
...mockConfig.swarm,
|
||||
instances: {
|
||||
...mockConfig.swarm.instances,
|
||||
coordinator: {
|
||||
...mockConfig.swarm.instances.coordinator,
|
||||
mcps: [{
|
||||
name: 'invalid'
|
||||
// Missing required fields
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(yaml.dump(invalidMCP));
|
||||
|
||||
await expect(manager.loadConfig()).rejects.toThrow('Invalid MCP server configuration');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user