diff --git a/docs/agents/CLI-COVERAGE.md b/docs/agents/CLI-COVERAGE.md new file mode 100644 index 000000000..7bbbd8832 --- /dev/null +++ b/docs/agents/CLI-COVERAGE.md @@ -0,0 +1,183 @@ +# Multi-Agent Networks CLI Coverage Report + +## Summary + +The Multi-Agent Networks feature has full CLI coverage. All agent and network +commands are implemented in `src/cli/commands/agent.ts` via the +`AgentCommandFactory` class. + +## SDK Coverage + +The SDK provides full programmatic access to Multi-Agent Networks: + +```typescript +import { NeuroLink } from "neurolink"; + +const neurolink = new NeuroLink(); + +// Create and execute an agent +const agent = neurolink.createAgent(definition); +const result = await agent.execute("Your input here"); + +// Create and execute a network +const network = neurolink.createNetwork(config); +const result = await neurolink.executeNetwork(network, { message: input }); +``` + +## CLI Coverage + +### Agent Commands + +#### `neurolink agent create` + +Create a new agent definition from inline flags or a JSON file. + +```bash +# Inline flags +neurolink agent create \ + --id researcher \ + --name "Research Agent" \ + --description "Searches and analyzes information" \ + --instructions "You are a research assistant..." \ + --provider anthropic \ + --model claude-3-5-sonnet-20241022 + +# From a JSON file +neurolink agent create --file agent-config.json +``` + +**Status:** Implemented + +--- + +#### `neurolink agent list` + +List all agents registered in the current session. + +```bash +neurolink agent list + +neurolink agent list --format json + +neurolink agent list --format table --detailed +``` + +**Status:** Implemented + +--- + +#### `neurolink agent execute` / `neurolink agent run` + +Execute a registered agent. `run` is an alias for `execute`. + +```bash +neurolink agent execute researcher "Find information about AI trends" + +neurolink agent run writer "Write a blog post" --stream + +neurolink agent execute researcher "Analyze this" \ + --context '{"language": "typescript"}' \ + --maxSteps 15 +``` + +**Status:** Implemented + +--- + +### Network Commands + +#### `neurolink network create` + +Create an agent network from a JSON configuration file. + +```bash +neurolink network create \ + --name "Content Team" \ + --file network-config.json + +# Override router settings +neurolink network create \ + --name "Content Team" \ + --file network-config.json \ + --routerProvider anthropic \ + --routerModel claude-3-5-sonnet-20241022 +``` + +**Status:** Implemented + +--- + +#### `neurolink network list` + +List all networks registered in the current session. + +```bash +neurolink network list + +neurolink network list --format json + +neurolink network list --format table --detailed +``` + +**Status:** Implemented + +--- + +#### `neurolink network execute` / `neurolink network run` + +Execute a registered network. `run` is an alias for `execute`. + +```bash +neurolink network execute content-team "Write an article about AI" + +neurolink network run research-team "Analyze market trends" --stream + +neurolink network execute content-team "Research topic" \ + --maxSteps 20 \ + --timeout 120000 +``` + +**Status:** Implemented + +--- + +## Shared Flags + +All `agent` and `network` subcommands support: + +| Flag | Type | Default | Description | +| ---------------- | ------------------- | ------- | ----------------------------- | +| `--format` | `text\|json\|table` | `text` | Output format | +| `--output` | `string` | — | Save output to file | +| `--quiet` / `-q` | `boolean` | `false` | Suppress non-essential output | +| `--debug` | `boolean` | `false` | Enable debug output | + +`agent execute` / `agent run` and `network execute` / `network run` also accept: + +| Flag | Type | Default | Description | +| ------------ | --------- | -------- | -------------------------------------- | +| `--stream` | `boolean` | `false` | Stream output in real-time | +| `--context` | `string` | — | Additional context as JSON | +| `--maxSteps` | `number` | `10` | Maximum execution steps | +| `--timeout` | `number` | `120000` | Timeout in milliseconds (network only) | + +## Commands Not Implemented + +The following commands are out of scope for the current implementation: + +- `neurolink agent show ` — show individual agent details +- `neurolink agent delete ` — remove a registered agent +- `neurolink network show ` — show individual network details +- `neurolink network delete ` — remove a registered network +- `neurolink network status ` — live network health/load status +- `neurolink message send` / `neurolink message broadcast` — direct messaging + +Session state (registered agents and networks) is in-memory and does not +persist across CLI invocations. + +## Source Reference + +- Implementation: `src/cli/commands/agent.ts` +- Command factory class: `AgentCommandFactory` +- Agent subcommands: `create`, `list`, `execute`, `run` +- Network subcommands: `create`, `list`, `execute`, `run` diff --git a/docs/agents/CONFIGURATION.md b/docs/agents/CONFIGURATION.md new file mode 100644 index 000000000..5036c897d --- /dev/null +++ b/docs/agents/CONFIGURATION.md @@ -0,0 +1,469 @@ +# Multi-Agent Networks Configuration Guide + +## Overview + +This document describes all configuration options for the Multi-Agent Networks +feature in NeuroLink. + +## Agent Configuration + +### AgentDefinition + +The core configuration for creating an agent: + +```typescript +type AgentDefinition = { + /** Unique identifier for the agent */ + id: string; + + /** Human-readable name */ + name: string; + + /** Description of capabilities (used by the router to select this agent) */ + description: string; + + /** System instructions for the agent */ + instructions: string; + + /** AI provider to use (optional, falls back to NeuroLink default) */ + provider?: AIProviderName | string; + + /** Model to use (optional, falls back to provider default) */ + model?: string; + + /** Tool names available to this agent (filters the global tool set) */ + tools?: string[]; + + /** Input schema for validation (Zod schema) */ + inputSchema?: z.ZodSchema; + + /** Output schema for parsing (Zod schema) */ + outputSchema?: z.ZodSchema; + + /** Maximum steps per execution */ + maxSteps?: number; + + /** Temperature for generation */ + temperature?: number; + + /** Whether agent can delegate to others */ + canDelegate?: boolean; + + /** Custom metadata */ + metadata?: Record; +}; +``` + +### Example Agent Configurations + +#### Basic Agent + +```typescript +const basicAgent: AgentDefinition = { + id: "assistant", + name: "General Assistant", + description: "A helpful general-purpose assistant", + instructions: "You are a helpful assistant. Answer questions concisely.", +}; +``` + +#### Specialized Agent with Tools + +```typescript +const codeAgent: AgentDefinition = { + id: "code-analyzer", + name: "Code Analysis Agent", + description: "Analyzes code for bugs, security issues, and improvements", + instructions: `You are an expert code analyst. Examine code carefully and: + 1. Identify potential bugs + 2. Flag security vulnerabilities + 3. Suggest improvements + 4. Follow best practices`, + provider: "anthropic", + model: "claude-3-5-sonnet-20241022", + tools: ["readFile", "searchCode", "lintCode"], + maxSteps: 15, + temperature: 0.3, + canDelegate: false, + metadata: { + expertise: ["typescript", "javascript", "python"], + analysisTypes: ["bugs", "security", "performance"], + }, +}; +``` + +#### Agent with Schema Validation + +```typescript +import { z } from "zod"; + +const structuredAgent: AgentDefinition = { + id: "data-extractor", + name: "Data Extraction Agent", + description: "Extracts structured data from unstructured text", + instructions: + "Extract the requested information and return it in the specified format.", + inputSchema: z.object({ + text: z.string().describe("Text to extract data from"), + fields: z.array(z.string()).describe("Fields to extract"), + }), + outputSchema: z.object({ + extracted: z.record(z.string(), z.unknown()), + confidence: z.number().min(0).max(1), + }), +}; +``` + +## Network Configuration + +### AgentNetworkConfig + +Configuration for creating a multi-agent network: + +```typescript +type AgentNetworkConfig = { + /** Network identifier */ + id?: string; + + /** Network name */ + name: string; + + /** Network description */ + description?: string; + + /** Agents in the network */ + agents: AgentDefinition[]; + + /** Optional workflows */ + workflows?: NetworkWorkflowDefinition[]; + + /** Network-level tool names */ + tools?: string[]; + + /** Router configuration */ + router?: RouterConfig; + + /** Default execution options */ + defaults?: NetworkDefaults; +}; +``` + +### RouterConfig + +The router is a system prompt plus provider settings that the AI SDK uses to +select which agent tool to invoke. There is no separate `RouterAgent` class — +routing is performed by the AI SDK's built-in generate loop. + +```typescript +type RouterConfig = { + /** Provider for the routing step */ + provider?: AIProviderName | string; + + /** Model to use for the routing step */ + model?: string; + + /** Custom routing instructions (appended to the default router system prompt) */ + instructions?: string; + + /** Maximum routing attempts before the loop stops */ + maxAttempts?: number; + + /** Confidence threshold for accepting a routing decision (0-1) */ + confidenceThreshold?: number; +}; +``` + +Example: + +```typescript +const router: RouterConfig = { + provider: "anthropic", + model: "claude-3-5-sonnet-20241022", + instructions: "Always prefer the specialized agent over the general one.", + maxAttempts: 3, + confidenceThreshold: 0.7, +}; +``` + +### NetworkDefaults + +Default settings for network execution: + +```typescript +type NetworkDefaults = { + /** Maximum steps per execution */ + maxSteps?: number; + + /** Timeout in milliseconds */ + timeout?: number; + + /** Default temperature */ + temperature?: number; +}; +``` + +## Topology Configurations + +### Hub-Spoke Topology + +Central hub agent coordinates with spoke agents: + +```typescript +type HubSpokeConfig = { + /** ID of the hub agent */ + hubAgentId: string; + + /** IDs of spoke agents */ + spokeAgentIds: string[]; + + /** Load balancing strategy */ + loadBalancing: "round-robin" | "least-loaded" | "random"; + + /** Max concurrent tasks per spoke */ + maxConcurrentTasksPerSpoke: number; + + /** Enable failover to other spokes */ + failoverEnabled?: boolean; + + /** Enable priority-based routing */ + priorityRouting?: boolean; + + /** Health check interval in ms */ + healthCheckInterval?: number; +}; +``` + +### Mesh Topology + +All agents can communicate directly: + +```typescript +type MeshConfig = { + /** IDs of agents in the mesh */ + agentIds: string[]; + + /** Auto-discover agent capabilities */ + autoDiscovery: boolean; + + /** Maximum hops for message routing */ + maxHops: number; + + /** Enable peer-to-peer delegation */ + enableP2PDelegation: boolean; + + /** Access control matrix (optional) */ + accessControl?: Record; + + /** Enable audit logging */ + auditLogging?: boolean; +}; +``` + +### Hierarchical Topology + +Tree-structured agent organization: + +```typescript +type HierarchicalConfig = { + /** Root agent ID */ + rootAgentId: string; + + /** Hierarchy levels */ + levels: Array<{ + level: number; + agents: string[]; + canDelegate?: boolean; + }>; + + /** Allow cross-level communication */ + allowCrossLevel: boolean; + + /** Enable automatic escalation */ + autoEscalation: boolean; + + /** Escalation confidence threshold */ + escalationThreshold?: number; + + /** Maximum escalation depth */ + maxEscalationDepth?: number; +}; +``` + +## MessageBus Configuration + +### MessageBusConfig + +Configuration for inter-agent messaging: + +```typescript +type MessageBusConfig = { + /** Maximum queue size */ + maxQueueSize: number; + + /** Message TTL in milliseconds */ + messageTTL: number; + + /** Enable message persistence */ + persistence: boolean; + + /** Persistence adapter */ + persistenceAdapter?: "memory" | "redis" | "file"; + + /** Enable dead letter queue */ + deadLetterQueue: boolean; + + /** Delivery guarantee */ + deliveryGuarantee: "at-most-once" | "at-least-once" | "exactly-once"; +}; +``` + +### Priority Levels + +```typescript +const PRIORITY_LEVELS = { + CRITICAL: 0, // Processed immediately + HIGH: 1, // Processed before normal + NORMAL: 2, // Standard processing + LOW: 3, // When capacity available + BACKGROUND: 4, // During idle time +}; +``` + +## Execution Options + +### AgentExecutionOptions + +Options for executing an agent: + +```typescript +type AgentExecutionOptions = { + /** Execution context */ + context?: Record; + + /** Max steps for this execution */ + maxSteps?: number; + + /** Timeout in milliseconds */ + timeout?: number; + + /** Trace ID for correlation */ + traceId?: string; + + /** Thread ID for conversation */ + threadId?: string; + + /** Resource ID for scoping */ + resourceId?: string; +}; +``` + +### NetworkExecutionOptions + +Options for executing a network: + +```typescript +type NetworkExecutionOptions = AgentExecutionOptions & { + /** Target agent (for direct routing) */ + targetAgent?: string; + + /** Skip routing, use target directly */ + skipRouting?: boolean; + + /** Streaming callback */ + onStream?: (event: StreamEvent) => void; + + /** Progress callback */ + onProgress?: (progress: ExecutionProgress) => void; +}; +``` + +## Environment Variables + +Configure behavior via environment variables: + +```bash +# Default provider for agents without explicit provider +NEUROLINK_DEFAULT_PROVIDER=vertex + +# Default model +NEUROLINK_DEFAULT_MODEL=gemini-2.0-flash + +# Maximum concurrent agent executions +NEUROLINK_MAX_CONCURRENT_AGENTS=10 + +# Default execution timeout (ms) +NEUROLINK_AGENT_TIMEOUT=30000 + +# Enable agent execution tracing +NEUROLINK_AGENT_TRACING=true + +# MessageBus persistence +NEUROLINK_MESSAGEBUS_PERSISTENCE=memory + +# Routing confidence threshold +NEUROLINK_ROUTING_THRESHOLD=0.7 +``` + +## Configuration Best Practices + +### Agent Descriptions + +Write clear, detailed descriptions — they are critical for router selection: + +```typescript +// Bad: too vague +description: "Handles code"; + +// Good: specific capabilities +description: "Analyzes source code for bugs, security vulnerabilities, " + + "and performance issues. Supports TypeScript, JavaScript, " + + "and Python. Can suggest fixes and refactoring improvements."; +``` + +### Tool Selection + +Only include tools the agent actually needs: + +```typescript +// Too many tools — reduces focus and adds noise +tools: ["readFile", "writeFile", "execute", "search", "analyze", "deploy"]; + +// Focused tool set — matches the agent's role +tools: ["readFile", "searchCode", "analyzeAST"]; +``` + +### Temperature Settings + +Match temperature to task type: + +```typescript +// Analytical tasks — low temperature +temperature: 0.2; + +// Creative tasks — higher temperature +temperature: 0.7; + +// Default balanced +temperature: 0.5; +``` + +### Timeout Configuration + +Set appropriate timeouts based on task complexity: + +```typescript +// Simple tasks +timeout: 10000; + +// Complex multi-step tasks +timeout: 60000; + +// Research/comprehensive tasks +timeout: 120000; +``` + +## Related Documentation + +- [TESTING.md](./TESTING.md) - Testing guide +- [VERIFICATION.md](./VERIFICATION.md) - Verification checklist +- [API Reference](../api/agents.md) - Full API documentation diff --git a/docs/agents/TESTING.md b/docs/agents/TESTING.md new file mode 100644 index 000000000..da7617dc5 --- /dev/null +++ b/docs/agents/TESTING.md @@ -0,0 +1,271 @@ +# Multi-Agent Networks Testing Guide + +## Overview + +This document provides comprehensive guidance for testing the Multi-Agent +Networks feature in NeuroLink. + +## Prerequisites + +### Environment Setup + +1. **Node.js**: Ensure Node.js 18+ is installed +2. **pnpm**: Install pnpm package manager +3. **Dependencies**: Install project dependencies + +```bash +# Install dependencies +pnpm install + +# Build the project +pnpm run build +``` + +### Required Environment Variables + +For integration tests with real providers, set the following: + +```bash +# Provider API Keys (at least one required for integration tests) +export OPENAI_API_KEY="your-openai-key" +export ANTHROPIC_API_KEY="your-anthropic-key" +export GOOGLE_AI_STUDIO_API_KEY="your-google-ai-key" + +# Test configuration +export TEST_PROVIDER="vertex" # or openai, anthropic, etc. +export TEST_MODEL="gemini-2.0-flash" # optional model override +export VERBOSE="true" # enable debug logging +``` + +## Test Structure + +The agent feature uses a single continuous test suite rather than individual +vitest unit test files. + +### Continuous Test Suite + +Located at `test/continuous-test-suite-agents.ts`: + +- Self-contained TypeScript script run directly with `tsx` +- Covers all components: Agent, AgentNetwork, MessageBus, topologies +- Uses fixture files from `test/fixtures/agents/` +- Reports pass/fail per test case with timing + +### Test Fixtures + +Located in `test/fixtures/agents/`: + +``` +test/fixtures/agents/ +├── agent-definitions.json # Agent configurations +├── network-topologies.json # Network topology configs +├── routing-rules.json # Routing decision test cases +└── messages.json # MessageBus test payloads +``` + +## Running Tests + +### Run the Agent Test Suite + +```bash +# Run the continuous integration test suite +npx tsx test/continuous-test-suite-agents.ts + +# With verbose output +VERBOSE=true npx tsx test/continuous-test-suite-agents.ts + +# With a specific provider +TEST_PROVIDER=openai npx tsx test/continuous-test-suite-agents.ts +``` + +### Run All NeuroLink Tests (includes agent suite) + +```bash +pnpm test + +# With coverage +pnpm run test:coverage +``` + +### Run in CI + +```yaml +# Example GitHub Actions config +- name: Run Agent Tests + run: npx tsx test/continuous-test-suite-agents.ts + env: + TEST_PROVIDER: vertex + GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }} +``` + +## Test Categories + +### 1. Agent Class Tests + +Tests for the core `Agent` class in `src/lib/agent/agent.ts`: + +- Agent creation with various configurations +- `execute()` with string and object input +- `stream()` output +- Input/output validation with Zod schemas +- Error handling and status tracking +- Tool filtering (the `toolFilter` mechanism) + +### 2. Network Topology Tests + +Tests for network configurations: + +- Hub-Spoke topology creation and execution +- Mesh topology peer-to-peer communication +- Hierarchical topology parent-child delegation +- `AgentCoordinator` strategies: `executeWithDependencies`, `roundRobin`, + `leastBusy` + +### 3. Routing Tests + +Routing is performed by the AI SDK's generate loop (agents-as-tools pattern). +The router is a system prompt, not a separate class. Tests verify: + +- Correct agent tool is selected for given input +- Routing completes with `finishReason: stop` +- `RouterConfig` fields (`provider`, `model`, `instructions`, `maxAttempts`, + `confidenceThreshold`) take effect + +### 4. MessageBus Tests + +Tests for inter-agent communication: + +- Publish/subscribe patterns +- Request-response patterns +- Broadcast messages +- Priority queue ordering +- Message delivery guarantees + +## Writing New Tests + +### Integration Test Pattern + +All agent tests follow the continuous test suite pattern — not vitest `describe` +blocks. Add new tests by pushing results to the suite's result array: + +```typescript +results.push( + await runTest("Test name here", async () => { + // Setup + const fixture = loadFixture("agent-definitions.json"); + + // Execute + const result = await someOperation(); + + // Assert + assertEqual(result.status, "success", "Should succeed"); + assertDefined(result.data, "Should have data"); + }), +); +``` + +### Importing Agent in Tests + +Use the correct import path — the module is singular and lowercase: + +```typescript +import { Agent } from "../../src/lib/agent/agent.js"; +import { NeuroLink } from "../../dist/index.js"; +``` + +Do not use `../../src/lib/agents/Agent.js` (plural directory, capitalized file) +— that path does not exist. + +### Mock SDK Creation + +```typescript +function createMockSdk(options?: { + generateResponse?: { content: string }; + streamChunks?: Array<{ content?: string }>; + shouldFail?: boolean; + errorMessage?: string; +}) { + return { + generate: async () => { + if (options?.shouldFail) { + throw new Error(options.errorMessage); + } + return { content: options?.generateResponse?.content ?? "Mock response" }; + }, + stream: async function* () { + for (const chunk of options?.streamChunks ?? []) { + yield chunk; + } + }, + }; +} +``` + +## Debugging Tests + +### Enable Verbose Logging + +```bash +VERBOSE=true npx tsx test/continuous-test-suite-agents.ts +``` + +### Isolate a Single Test + +Because the suite is a plain script, wrap the test in a standalone file or add +a name filter variable and short-circuit other tests: + +```typescript +// Quick one-off in a scratch file +import { Agent } from "../src/lib/agent/agent.js"; + +const agent = new Agent( + { + id: "test", + name: "Test", + description: "Test agent", + instructions: "You are a test agent.", + }, + mockSdk as unknown as NeuroLink, +); + +console.log(await agent.execute("hello")); +``` + +## Test Coverage Goals + +| Component | Target Coverage | +| ------------ | --------------- | +| Agent | 90% | +| AgentNetwork | 85% | +| MessageBus | 90% | +| Topologies | 80% | + +## Known Limitations + +1. **Real Provider Tests**: Require API keys and may incur costs +2. **Streaming Tests**: May be sensitive to timing + +## Troubleshooting + +### Import Errors + +Ensure the project is built before running the suite: + +```bash +pnpm run build +npx tsx test/continuous-test-suite-agents.ts +``` + +### Tests Timing Out + +Set a longer timeout via the environment, or check provider rate limits: + +```bash +TEST_PROVIDER=openai OPENAI_API_KEY=sk-... npx tsx test/continuous-test-suite-agents.ts +``` + +## Related Documentation + +- [CONFIGURATION.md](./CONFIGURATION.md) - Configuration options +- [VERIFICATION.md](./VERIFICATION.md) - Manual verification checklist +- [CLI-COVERAGE.md](./CLI-COVERAGE.md) - CLI coverage report diff --git a/docs/agents/VERIFICATION.md b/docs/agents/VERIFICATION.md new file mode 100644 index 000000000..2fb54a8b8 --- /dev/null +++ b/docs/agents/VERIFICATION.md @@ -0,0 +1,173 @@ +# Multi-Agent Networks Verification Checklist + +## Overview + +This document provides a manual verification checklist for the Multi-Agent +Networks feature. + +## Pre-Verification Setup + +### 1. Environment Preparation + +- [ ] Node.js 18+ installed +- [ ] pnpm installed +- [ ] Project dependencies installed (`pnpm install`) +- [ ] Project built (`pnpm run build`) +- [ ] At least one provider API key configured + +### 2. Required API Keys + +Configure at least one of: + +- [ ] `OPENAI_API_KEY` +- [ ] `ANTHROPIC_API_KEY` +- [ ] `GOOGLE_AI_STUDIO_API_KEY` +- [ ] `GOOGLE_CLOUD_PROJECT` (for Vertex AI) + +## Integration Test Verification + +Run: `npx tsx test/continuous-test-suite-agents.ts` + +### Agent Class Integration + +- [ ] Fixtures load correctly +- [ ] All agent definitions valid +- [ ] Multiple providers configured +- [ ] Tool configurations correct +- [ ] Mock SDK works + +### Network Topology Integration + +- [ ] Hub-spoke config valid +- [ ] Mesh config valid +- [ ] Hierarchical config valid +- [ ] Router configs valid +- [ ] Network defaults valid + +### Routing Rules Integration + +- [ ] All rules defined +- [ ] Pattern matching works +- [ ] Confidence thresholds set +- [ ] Fallback behavior defined +- [ ] Priority ordering correct + +### MessageBus Integration + +- [ ] All message types defined +- [ ] Test messages valid +- [ ] Subscription patterns work +- [ ] Priority levels correct +- [ ] Test scenarios execute + +## Functional Verification + +### Basic Agent Operations + +- [ ] Create agent programmatically +- [ ] Execute agent with text input +- [ ] Execute agent with structured input +- [ ] Stream agent output +- [ ] Handle agent errors + +### Network Operations + +- [ ] Create network with multiple agents +- [ ] Execute network task +- [ ] Observe routing decisions +- [ ] Track execution traces +- [ ] Handle network failures + +### Messaging Operations + +- [ ] Publish message +- [ ] Subscribe and receive +- [ ] Request-response works +- [ ] Broadcast reaches all +- [ ] Priority respected + +## Performance Verification + +### Response Time + +- [ ] Single agent < 5s +- [ ] Network routing < 1s +- [ ] Message delivery < 100ms + +### Concurrency + +- [ ] 10 concurrent agents +- [ ] 100 messages/second +- [ ] No memory leaks + +## Error Handling Verification + +### Agent Errors + +- [ ] Invalid input handled +- [ ] Provider errors caught +- [ ] Timeout errors handled +- [ ] Schema validation errors + +### Network Errors + +- [ ] Routing failures handled +- [ ] Agent unavailable handled +- [ ] Network timeout handled + +### Message Errors + +- [ ] Subscriber errors isolated +- [ ] Timeout errors reported +- [ ] Invalid message rejected + +## Documentation Verification + +- [ ] README complete +- [ ] API documented +- [ ] Examples provided +- [ ] Error messages clear + +## CLI Coverage Verification + +All agent and network CLI commands are implemented in +`src/cli/commands/agent.ts`. + +### Agent Commands + +- [ ] `neurolink agent create` - available +- [ ] `neurolink agent list` - available +- [ ] `neurolink agent execute` - available +- [ ] `neurolink agent run` (alias for execute) - available + +### Network Commands + +- [ ] `neurolink network create` - available +- [ ] `neurolink network list` - available +- [ ] `neurolink network execute` - available +- [ ] `neurolink network run` (alias for execute) - available + +See [CLI-COVERAGE.md](./CLI-COVERAGE.md) for full flag reference and usage +examples. + +## Final Verification Summary + +| Category | Method | Status | +| -------------------- | ---------------------------------------------- | ------ | +| Agent Class | `npx tsx test/continuous-test-suite-agents.ts` | ? | +| AgentNetwork | `npx tsx test/continuous-test-suite-agents.ts` | ? | +| MessageBus | `npx tsx test/continuous-test-suite-agents.ts` | ? | +| HubSpokeTopology | `npx tsx test/continuous-test-suite-agents.ts` | ? | +| MeshTopology | `npx tsx test/continuous-test-suite-agents.ts` | ? | +| HierarchicalTopology | `npx tsx test/continuous-test-suite-agents.ts` | ? | +| CLI Commands | Manual smoke test (`neurolink agent --help`) | ? | +| Integration | `npx tsx test/continuous-test-suite-agents.ts` | ? | + +## Sign-Off + +- [ ] Integration tests passing +- [ ] CLI commands smoke-tested +- [ ] Performance acceptable +- [ ] Documentation complete + +**Verified by:** \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ **Date:** \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ diff --git a/src/cli/commands/agent.ts b/src/cli/commands/agent.ts new file mode 100644 index 000000000..9404aace7 --- /dev/null +++ b/src/cli/commands/agent.ts @@ -0,0 +1,941 @@ +/** + * Agent CLI Commands for NeuroLink + * Implements comprehensive multi-agent orchestration commands + * + * Commands: + * - agent create: Create a new agent definition + * - agent list: List registered agents + * - agent execute: Execute a single agent + * - network create: Create an agent network + * - network execute: Execute a network + */ + +import type { CommandModule, Argv } from "yargs"; +import type { + CliAgentCommandArgs, + CliNetworkCommandArgs, + AgentDefinition, + AgentNetworkConfig, + NetworkStartChunk, + RoutingDecisionChunk, + PrimitiveStartChunk, + AgentTextChunk, + AgentToolCallChunk, + AgentToolResultChunk, + PrimitiveEndChunk, + NetworkCompleteChunk, + NetworkErrorChunk, +} from "../../lib/types/index.js"; +import { NeuroLink } from "../../lib/neurolink.js"; +import { logger } from "../../lib/utils/logger.js"; +import chalk from "chalk"; +import ora from "ora"; +import fs from "fs"; +import path from "path"; + +// In-memory storage for agents and networks (session-based) +const registeredAgents: Map = new Map(); +const registeredNetworks: Map = new Map(); + +/** + * Agent CLI command factory + */ +export class AgentCommandFactory { + /** + * Create the main agent command with subcommands + */ + static createAgentCommands(): CommandModule { + return { + command: "agent ", + describe: "Manage AI agents for multi-agent orchestration", + builder: (yargs) => { + return yargs + .command( + "create", + "Create a new agent definition", + (yargs) => this.buildCreateOptions(yargs), + (argv) => this.executeCreate(argv as CliAgentCommandArgs), + ) + .command( + "list", + "List registered agents", + (yargs) => this.buildListOptions(yargs), + (argv) => this.executeList(argv as CliAgentCommandArgs), + ) + .command( + "execute ", + "Execute a single agent with given input", + (yargs) => this.buildExecuteOptions(yargs), + (argv) => this.executeAgent(argv as CliAgentCommandArgs), + ) + .command( + "run ", + "Execute a single agent (alias for execute)", + (yargs) => this.buildExecuteOptions(yargs), + (argv) => this.executeAgent(argv as CliAgentCommandArgs), + ) + .option("format", { + choices: ["text", "json", "table"], + default: "text", + description: "Output format", + }) + .option("output", { + type: "string", + description: "Save output to file", + }) + .option("quiet", { + type: "boolean", + alias: "q", + default: false, + description: "Suppress non-essential output", + }) + .option("debug", { + type: "boolean", + default: false, + description: "Enable debug output", + }) + .demandCommand(1, "Please specify an agent subcommand") + .help(); + }, + handler: () => { + // No-op handler as subcommands handle everything + }, + }; + } + + /** + * Create the network command with subcommands + */ + static createNetworkCommands(): CommandModule { + return { + command: "network ", + describe: "Manage agent networks for multi-agent orchestration", + builder: (yargs) => { + return yargs + .command( + "create", + "Create a new agent network", + (yargs) => this.buildNetworkCreateOptions(yargs), + (argv) => this.executeNetworkCreate(argv as CliNetworkCommandArgs), + ) + .command( + "list", + "List registered networks", + (yargs) => this.buildNetworkListOptions(yargs), + (argv) => this.executeNetworkList(argv as CliNetworkCommandArgs), + ) + .command( + "execute ", + "Execute an agent network with given input", + (yargs) => this.buildNetworkExecuteOptions(yargs), + (argv) => this.executeNetwork(argv as CliNetworkCommandArgs), + ) + .command( + "run ", + "Execute an agent network (alias for execute)", + (yargs) => this.buildNetworkExecuteOptions(yargs), + (argv) => this.executeNetwork(argv as CliNetworkCommandArgs), + ) + .option("format", { + choices: ["text", "json", "table"], + default: "text", + description: "Output format", + }) + .option("output", { + type: "string", + description: "Save output to file", + }) + .option("quiet", { + type: "boolean", + alias: "q", + default: false, + description: "Suppress non-essential output", + }) + .option("debug", { + type: "boolean", + default: false, + description: "Enable debug output", + }) + .demandCommand(1, "Please specify a network subcommand") + .help(); + }, + handler: () => { + // No-op handler as subcommands handle everything + }, + }; + } + + // ============================================================================ + // AGENT COMMAND BUILDERS + // ============================================================================ + + private static buildCreateOptions(yargs: Argv): Argv { + return yargs + .option("id", { + type: "string", + description: "Unique agent identifier", + demandOption: true, + }) + .option("name", { + type: "string", + description: "Human-readable agent name", + demandOption: true, + }) + .option("description", { + type: "string", + description: "Description of agent capabilities (for routing)", + demandOption: true, + }) + .option("instructions", { + type: "string", + description: "System instructions for the agent", + demandOption: true, + }) + .option("provider", { + type: "string", + description: "AI provider (e.g., openai, anthropic, vertex)", + }) + .option("model", { + type: "string", + description: "Model to use (e.g., gpt-4o, claude-3-sonnet)", + }) + .option("tools", { + type: "array", + description: "Tools available to the agent", + }) + .option("maxSteps", { + type: "number", + default: 10, + description: "Maximum execution steps", + }) + .option("temperature", { + type: "number", + default: 0.7, + description: "Generation temperature (0-1)", + }) + .option("file", { + type: "string", + description: "Load agent definition from JSON file", + }) + .example( + '$0 agent create --id researcher --name "Research Agent" --description "Searches and analyzes information" --instructions "You are a research assistant..."', + "Create a research agent", + ) + .example( + "$0 agent create --file agent-config.json", + "Create agent from config file", + ); + } + + private static buildListOptions(yargs: Argv): Argv { + return yargs + .option("detailed", { + type: "boolean", + default: false, + description: "Show detailed agent information", + }) + .example("$0 agent list", "List all registered agents") + .example("$0 agent list --format json", "List agents in JSON format"); + } + + private static buildExecuteOptions(yargs: Argv): Argv { + return yargs + .positional("id", { + type: "string", + description: "Agent ID to execute", + demandOption: true, + }) + .positional("input", { + type: "string", + description: "Input prompt for the agent", + demandOption: true, + }) + .option("context", { + type: "string", + description: "Additional context as JSON", + }) + .option("maxSteps", { + type: "number", + description: "Override maximum steps", + }) + .option("stream", { + type: "boolean", + default: false, + description: "Stream output in real-time", + }) + .example( + '$0 agent execute researcher "Find information about AI trends"', + "Execute the researcher agent", + ) + .example( + '$0 agent run writer "Write a blog post" --stream', + "Execute with streaming", + ); + } + + // ============================================================================ + // NETWORK COMMAND BUILDERS + // ============================================================================ + + private static buildNetworkCreateOptions(yargs: Argv): Argv { + return yargs + .option("name", { + type: "string", + description: "Network name", + demandOption: true, + }) + .option("description", { + type: "string", + description: "Network description", + }) + .option("file", { + type: "string", + description: "Load network configuration from JSON file", + demandOption: true, + }) + .option("routerProvider", { + type: "string", + description: "Provider for the routing agent", + }) + .option("routerModel", { + type: "string", + description: "Model for the routing agent", + }) + .example( + '$0 network create --name "Content Team" --file network-config.json', + "Create a content team network", + ); + } + + private static buildNetworkListOptions(yargs: Argv): Argv { + return yargs + .option("detailed", { + type: "boolean", + default: false, + description: "Show detailed network information", + }) + .example("$0 network list", "List all registered networks") + .example("$0 network list --format json", "List networks in JSON format"); + } + + private static buildNetworkExecuteOptions(yargs: Argv): Argv { + return yargs + .positional("id", { + type: "string", + description: "Network ID to execute", + demandOption: true, + }) + .positional("input", { + type: "string", + description: "Input message for the network", + demandOption: true, + }) + .option("context", { + type: "string", + description: "Additional context as JSON", + }) + .option("maxSteps", { + type: "number", + default: 10, + description: "Maximum execution steps", + }) + .option("timeout", { + type: "number", + default: 120000, + description: "Execution timeout in milliseconds", + }) + .option("stream", { + type: "boolean", + default: false, + description: "Stream output in real-time", + }) + .example( + '$0 network execute content-team "Write an article about AI"', + "Execute the content team network", + ) + .example( + '$0 network run research-team "Analyze market trends" --stream', + "Execute with streaming", + ); + } + + // ============================================================================ + // AGENT COMMAND HANDLERS + // ============================================================================ + + private static async executeCreate(argv: CliAgentCommandArgs): Promise { + const spinner = ora("Creating agent...").start(); + + try { + let definition: AgentDefinition; + + if (argv.file) { + // Load from file + const filePath = path.resolve(argv.file); + if (!fs.existsSync(filePath)) { + throw new Error(`Agent definition file not found: ${filePath}`); + } + const content = fs.readFileSync(filePath, "utf-8"); + definition = JSON.parse(content) as AgentDefinition; + } else { + // Build from command line arguments + definition = { + id: argv.id!, + name: argv.name!, + description: argv.description!, + instructions: argv.instructions!, + provider: argv.provider, + model: argv.model, + tools: argv.tools as string[], + maxSteps: argv.maxSteps, + temperature: argv.temperature, + }; + } + + // Validate required fields + if ( + !definition.id || + !definition.name || + !definition.description || + !definition.instructions + ) { + throw new Error( + "Agent definition requires: id, name, description, and instructions", + ); + } + + // Register the agent + registeredAgents.set(definition.id, definition); + + spinner.succeed( + chalk.green(`Agent "${definition.name}" created successfully`), + ); + + if (!argv.quiet) { + logger.always(chalk.cyan("\nAgent Details:")); + logger.always(` ID: ${definition.id}`); + logger.always(` Name: ${definition.name}`); + logger.always(` Description: ${definition.description}`); + if (definition.provider) { + logger.always(` Provider: ${definition.provider}`); + } + if (definition.model) { + logger.always(` Model: ${definition.model}`); + } + if (definition.tools?.length) { + logger.always(` Tools: ${definition.tools.join(", ")}`); + } + logger.always(` Max Steps: ${definition.maxSteps || 10}`); + logger.always(` Temperature: ${definition.temperature || 0.7}`); + } + + if (argv.format === "json") { + logger.always(JSON.stringify(definition, null, 2)); + } + } catch (error) { + spinner.fail(chalk.red("Failed to create agent")); + logger.error((error as Error).message); + process.exitCode = 1; + } + } + + private static async executeList(argv: CliAgentCommandArgs): Promise { + try { + const agents = Array.from(registeredAgents.values()); + + if (agents.length === 0) { + logger.always( + chalk.yellow( + "No agents registered. Use 'neurolink agent create' to create one.", + ), + ); + return; + } + + if (argv.format === "json") { + logger.always(JSON.stringify(agents, null, 2)); + return; + } + + if (argv.format === "table") { + const tableData = agents.map((a) => ({ + ID: a.id, + Name: a.name, + Provider: a.provider || "default", + Model: a.model || "default", + Tools: a.tools?.length || 0, + })); + logger.info(JSON.stringify(tableData, null, 2)); + return; + } + + // Text format + logger.always(chalk.cyan(`\nRegistered Agents (${agents.length}):\n`)); + for (const agent of agents) { + logger.always(chalk.bold(` ${agent.name} (${agent.id})`)); + logger.always(chalk.gray(` ${agent.description}`)); + if (agent.provider || agent.model) { + logger.always( + chalk.gray( + ` Provider: ${agent.provider || "default"}, Model: ${agent.model || "default"}`, + ), + ); + } + if (agent.tools?.length) { + logger.always(chalk.gray(` Tools: ${agent.tools.join(", ")}`)); + } + logger.always(""); + } + } catch (error) { + logger.error(`Failed to list agents: ${(error as Error).message}`); + process.exitCode = 1; + } + } + + private static async executeAgent(argv: CliAgentCommandArgs): Promise { + const spinner = ora("Executing agent...").start(); + + try { + const agentId = argv.id as string; + const input = argv.input as string; + + // Get agent definition + const definition = registeredAgents.get(agentId); + if (!definition) { + throw new Error( + `Agent not found: ${agentId}. Use 'neurolink agent list' to see available agents.`, + ); + } + + // Parse context if provided + let context: Record | undefined; + if (argv.context) { + try { + context = JSON.parse(argv.context); + } catch { + throw new Error("Invalid JSON in --context parameter"); + } + } + + // Create NeuroLink instance and agent + const neurolink = new NeuroLink(); + const agent = await neurolink.createAgent(definition); + + if (argv.stream) { + spinner.stop(); + logger.always( + chalk.cyan(`\n[${definition.name}] Streaming response:\n`), + ); + + // Stream execution + for await (const chunk of agent.stream(input, { + context, + maxSteps: argv.maxSteps, + })) { + if (chunk.type === "agent-text" && chunk.content) { + process.stdout.write(chunk.content); + } else if (chunk.type === "agent-tool-call" && chunk.toolName) { + logger.always(chalk.yellow(`\n[Tool: ${chunk.toolName}]`)); + } else if (chunk.type === "agent-tool-result") { + logger.always( + chalk.green( + `[Tool Result: ${chunk.success ? "Success" : "Failed"}]`, + ), + ); + } else if (chunk.type === "agent-complete") { + logger.always(chalk.gray(`\n\nCompleted in ${chunk.duration}ms`)); + } else if (chunk.type === "agent-error") { + logger.always(chalk.red(`\nError: ${chunk.error}`)); + process.exitCode = 1; + } + } + logger.always(""); + } else { + // Non-streaming execution + const result = await agent.execute(input, { + context, + maxSteps: argv.maxSteps, + }); + + spinner.succeed(chalk.green("Agent execution completed")); + + if (argv.format === "json") { + logger.always(JSON.stringify(result, null, 2)); + } else { + logger.always(chalk.cyan(`\n[${definition.name}] Response:\n`)); + logger.always(result.content); + + if (!argv.quiet && result.toolsUsed?.length) { + logger.always( + chalk.gray(`\nTools used: ${result.toolsUsed.join(", ")}`), + ); + } + if (!argv.quiet) { + logger.always(chalk.gray(`\nCompleted in ${result.duration}ms`)); + } + } + + // Save to file if output specified + if (argv.output) { + const output = + argv.format === "json" + ? JSON.stringify(result, null, 2) + : result.content; + fs.writeFileSync(argv.output, output); + logger.always(chalk.green(`\nOutput saved to: ${argv.output}`)); + } + } + } catch (error) { + spinner.fail(chalk.red("Agent execution failed")); + logger.error((error as Error).message); + process.exitCode = 1; + } + } + + // ============================================================================ + // NETWORK COMMAND HANDLERS + // ============================================================================ + + private static async executeNetworkCreate( + argv: CliNetworkCommandArgs, + ): Promise { + const spinner = ora("Creating agent network...").start(); + + try { + if (!argv.file) { + throw new Error( + "Network configuration file is required. Use --file to specify.", + ); + } + + // Load network configuration from file + const filePath = path.resolve(argv.file); + if (!fs.existsSync(filePath)) { + throw new Error(`Network configuration file not found: ${filePath}`); + } + const content = fs.readFileSync(filePath, "utf-8"); + const config = JSON.parse(content) as AgentNetworkConfig; + + // Override with command line options + if (argv.name) { + config.name = argv.name; + } + if (argv.description) { + config.description = argv.description; + } + if (argv.routerProvider || argv.routerModel) { + config.router = config.router || {}; + if (argv.routerProvider) { + config.router.provider = argv.routerProvider; + } + if (argv.routerModel) { + config.router.model = argv.routerModel; + } + } + + // Validate required fields + if (!config.name || !config.agents || config.agents.length === 0) { + throw new Error( + "Network configuration requires: name and at least one agent", + ); + } + + // Generate ID if not provided + const networkId = + config.id || config.name.toLowerCase().replace(/\s+/g, "-"); + config.id = networkId; + + // Register the network + registeredNetworks.set(networkId, config); + + // Also register individual agents + for (const agentDef of config.agents) { + registeredAgents.set(agentDef.id, agentDef); + } + + spinner.succeed( + chalk.green(`Network "${config.name}" created successfully`), + ); + + if (!argv.quiet) { + logger.always(chalk.cyan("\nNetwork Details:")); + logger.always(` ID: ${networkId}`); + logger.always(` Name: ${config.name}`); + if (config.description) { + logger.always(` Description: ${config.description}`); + } + logger.always(` Agents: ${config.agents.length}`); + for (const agent of config.agents) { + logger.always(` - ${agent.name} (${agent.id})`); + } + if (config.router?.provider) { + logger.always(` Router Provider: ${config.router.provider}`); + } + if (config.router?.model) { + logger.always(` Router Model: ${config.router.model}`); + } + } + + if (argv.format === "json") { + logger.always(JSON.stringify(config, null, 2)); + } + } catch (error) { + spinner.fail(chalk.red("Failed to create network")); + logger.error((error as Error).message); + process.exitCode = 1; + } + } + + private static async executeNetworkList( + argv: CliNetworkCommandArgs, + ): Promise { + try { + const networks = Array.from(registeredNetworks.values()); + + if (networks.length === 0) { + logger.always( + chalk.yellow( + "No networks registered. Use 'neurolink network create' to create one.", + ), + ); + return; + } + + if (argv.format === "json") { + logger.always(JSON.stringify(networks, null, 2)); + return; + } + + if (argv.format === "table") { + const tableData = networks.map((n) => ({ + ID: n.id || n.name.toLowerCase().replace(/\s+/g, "-"), + Name: n.name, + Agents: n.agents.length, + Workflows: n.workflows?.length || 0, + Tools: n.tools?.length || 0, + })); + logger.info(JSON.stringify(tableData, null, 2)); + return; + } + + // Text format + logger.always( + chalk.cyan(`\nRegistered Networks (${networks.length}):\n`), + ); + for (const network of networks) { + const networkId = + network.id || network.name.toLowerCase().replace(/\s+/g, "-"); + logger.always(chalk.bold(` ${network.name} (${networkId})`)); + if (network.description) { + logger.always(chalk.gray(` ${network.description}`)); + } + logger.always( + chalk.gray( + ` Agents: ${network.agents.map((a) => a.name).join(", ")}`, + ), + ); + if (network.workflows?.length) { + logger.always( + chalk.gray(` Workflows: ${network.workflows.length}`), + ); + } + if (network.tools?.length) { + logger.always(chalk.gray(` Tools: ${network.tools.join(", ")}`)); + } + logger.always(""); + } + } catch (error) { + logger.error(`Failed to list networks: ${(error as Error).message}`); + process.exitCode = 1; + } + } + + private static async executeNetwork( + argv: CliNetworkCommandArgs, + ): Promise { + const spinner = ora("Executing agent network...").start(); + + try { + const networkId = argv.id as string; + const input = argv.input as string; + + // Get network configuration + const config = registeredNetworks.get(networkId); + if (!config) { + throw new Error( + `Network not found: ${networkId}. Use 'neurolink network list' to see available networks.`, + ); + } + + // Parse context if provided + let context: Record | undefined; + if (argv.context) { + try { + context = JSON.parse(argv.context); + } catch { + throw new Error("Invalid JSON in --context parameter"); + } + } + + // Create NeuroLink instance and network + const neurolink = new NeuroLink(); + const network = await neurolink.createNetwork(config); + + if (argv.stream) { + spinner.stop(); + logger.always(chalk.cyan(`\n[${config.name}] Streaming execution:\n`)); + + // Stream execution + for await (const chunk of neurolink.streamNetwork( + network, + { + message: input, + context, + }, + { + maxSteps: argv.maxSteps, + timeout: argv.timeout, + }, + )) { + switch (chunk.type) { + case "network-start": { + const startChunk = chunk as NetworkStartChunk; + logger.always( + chalk.blue(`Network started: ${startChunk.networkId}`), + ); + break; + } + case "routing-decision": { + const routingChunk = chunk as RoutingDecisionChunk; + logger.always( + chalk.yellow( + `\nRouting to: ${routingChunk.decision.selectedPrimitive.name}`, + ), + ); + logger.always( + chalk.gray( + ` Confidence: ${(routingChunk.decision.confidence * 100).toFixed(0)}%`, + ), + ); + logger.always( + chalk.gray(` Reasoning: ${routingChunk.decision.reasoning}`), + ); + break; + } + case "primitive-start": { + const primitiveStartChunk = chunk as PrimitiveStartChunk; + logger.always( + chalk.cyan( + `\n[${primitiveStartChunk.primitive.name}] Starting...`, + ), + ); + break; + } + case "agent-text": { + const textChunk = chunk as AgentTextChunk; + process.stdout.write(textChunk.content); + break; + } + case "agent-tool-call": { + const toolCallChunk = chunk as AgentToolCallChunk; + logger.always( + chalk.yellow(`\n[Tool: ${toolCallChunk.toolName}]`), + ); + break; + } + case "agent-tool-result": { + const toolResultChunk = chunk as AgentToolResultChunk; + logger.always( + chalk.green( + `[Tool Result: ${toolResultChunk.success ? "Success" : "Failed"}]`, + ), + ); + break; + } + case "primitive-end": { + const primitiveEndChunk = chunk as PrimitiveEndChunk; + logger.always( + chalk.cyan(`\n[${primitiveEndChunk.primitive.name}] Completed`), + ); + break; + } + case "network-complete": { + const completeChunk = chunk as NetworkCompleteChunk; + logger.always( + chalk.green( + `\n\nNetwork completed in ${completeChunk.result.duration}ms`, + ), + ); + logger.always( + chalk.gray( + `Steps taken: ${completeChunk.result.trace.steps.length}`, + ), + ); + break; + } + case "network-error": { + const errorChunk = chunk as NetworkErrorChunk; + logger.always(chalk.red(`\nNetwork error: ${errorChunk.error}`)); + process.exitCode = 1; + break; + } + } + } + logger.always(""); + } else { + // Non-streaming execution + const result = await neurolink.executeNetwork( + network, + { + message: input, + context, + }, + { + maxSteps: argv.maxSteps, + timeout: argv.timeout, + }, + ); + + spinner.succeed(chalk.green("Network execution completed")); + + if (argv.format === "json") { + logger.always(JSON.stringify(result, null, 2)); + } else { + logger.always(chalk.cyan(`\n[${config.name}] Result:\n`)); + logger.always(result.content); + + if (!argv.quiet) { + logger.always(chalk.gray(`\nStatus: ${result.status}`)); + logger.always(chalk.gray(`Duration: ${result.duration}ms`)); + logger.always(chalk.gray(`Steps: ${result.trace.steps.length}`)); + + if (result.trace.routingDecisions.length > 0) { + logger.always(chalk.gray("\nRouting History:")); + for (const decision of result.trace.routingDecisions) { + logger.always( + chalk.gray( + ` ${decision.stepIndex + 1}. ${decision.selectedPrimitive.name} (${(decision.confidence * 100).toFixed(0)}%)`, + ), + ); + } + } + } + } + + // Save to file if output specified + if (argv.output) { + const output = + argv.format === "json" + ? JSON.stringify(result, null, 2) + : result.content; + fs.writeFileSync(argv.output, output); + logger.always(chalk.green(`\nOutput saved to: ${argv.output}`)); + } + } + } catch (error) { + spinner.fail(chalk.red("Network execution failed")); + logger.error((error as Error).message); + process.exitCode = 1; + } + } +} diff --git a/src/cli/factories/commandFactory.ts b/src/cli/factories/commandFactory.ts index 29de10788..cab26906a 100644 --- a/src/cli/factories/commandFactory.ts +++ b/src/cli/factories/commandFactory.ts @@ -49,6 +49,7 @@ import { } from "../utils/videoFileUtils.js"; import { OllamaCommandFactory } from "./ollamaCommandFactory.js"; import { SageMakerCommandFactory } from "./sagemakerCommandFactory.js"; +import { AgentCommandFactory } from "../commands/agent.js"; /** * CLI Command Factory for generate commands @@ -1585,6 +1586,20 @@ export class CLICommandFactory { return MCPCommandFactory.createDiscoverCommand(); } + /** + * Create agent commands for multi-agent orchestration + */ + static createAgentCommands(): CommandModule { + return AgentCommandFactory.createAgentCommands(); + } + + /** + * Create network commands for agent network orchestration + */ + static createNetworkCommands(): CommandModule { + return AgentCommandFactory.createNetworkCommands(); + } + /** * Create memory commands */ diff --git a/src/cli/parser.ts b/src/cli/parser.ts index 6f512ce3f..b29b83932 100644 --- a/src/cli/parser.ts +++ b/src/cli/parser.ts @@ -217,6 +217,12 @@ export function initializeCliParser() { // Loop Command - Using CLICommandFactory .command(CLICommandFactory.createLoopCommand()) + // Agent Commands - Using CLICommandFactory (Multi-Agent Orchestration) + .command(CLICommandFactory.createAgentCommands()) + + // Network Commands - Using CLICommandFactory (Agent Network Orchestration) + .command(CLICommandFactory.createNetworkCommands()) + // Setup Commands - Using SetupCommandFactory .command(SetupCommandFactory.createSetupCommands()) diff --git a/src/lib/agent/agent.ts b/src/lib/agent/agent.ts new file mode 100644 index 000000000..d6de66c78 --- /dev/null +++ b/src/lib/agent/agent.ts @@ -0,0 +1,506 @@ +/** + * Agent - Core agent implementation for NeuroLink + * + * An Agent wraps a NeuroLink instance with specialized behavior, instructions, + * and tool restrictions. Agents can be composed into networks for multi-agent + * orchestration using the agents-as-tools pattern. + */ + +import { EventEmitter } from "events"; +import type { z } from "zod"; +import type { NeuroLink } from "../neurolink.js"; +import type { + AgentDefinition, + AgentExecutionOptions, + AgentInput, + AgentInstance, + AgentResult, + AgentStatus, + AgentStreamChunk, + GenerateOptions, + StreamOptions, +} from "../types/index.js"; +import { ErrorFactory } from "../utils/errorHandling.js"; +import { logger } from "../utils/logger.js"; + +/** + * Agent - Wraps a NeuroLink instance with specialized behavior + * + * Features: + * - Custom instructions and persona + * - Tool restrictions per agent (via toolFilter on generate/stream) + * - Input/output schema validation + * - Streaming support + * - Execution metrics tracking + * + * @example + * ```typescript + * const agent = new Agent({ + * id: 'researcher', + * name: 'Research Agent', + * description: 'Searches and analyzes information', + * instructions: 'You are a research assistant...', + * tools: ['websearchGrounding', 'readFile'], + * }, neurolink); + * + * const result = await agent.execute('Find information about quantum computing'); + * ``` + */ +export class Agent implements AgentInstance { + readonly id: string; + readonly name: string; + readonly description: string; + readonly instructions: string; + readonly provider?: string; + readonly model?: string; + readonly tools?: string[]; + readonly inputSchema?: z.ZodSchema; + readonly outputSchema?: z.ZodSchema; + readonly maxSteps: number; + readonly temperature: number; + readonly canDelegate: boolean; + readonly metadata?: Record; + + private neurolink: NeuroLink; + private emitter: EventEmitter; + private executionCount: number = 0; + private lastExecutionTime?: number; + private totalExecutionTime: number = 0; + + constructor(definition: AgentDefinition, neurolink: NeuroLink) { + // Validate required fields + if (!definition.id || typeof definition.id !== "string") { + throw ErrorFactory.invalidConfiguration( + "AgentDefinition.id", + "Agent definition must have a valid id", + ); + } + if (!definition.name || typeof definition.name !== "string") { + throw ErrorFactory.invalidConfiguration( + "AgentDefinition.name", + "Agent definition must have a valid name", + ); + } + if (!definition.description || typeof definition.description !== "string") { + throw ErrorFactory.invalidConfiguration( + "AgentDefinition.description", + "Agent definition must have a valid description", + ); + } + if ( + !definition.instructions || + typeof definition.instructions !== "string" + ) { + throw ErrorFactory.invalidConfiguration( + "AgentDefinition.instructions", + "Agent definition must have valid instructions", + ); + } + + this.id = definition.id; + this.name = definition.name; + this.description = definition.description; + this.instructions = definition.instructions; + this.provider = definition.provider; + this.model = definition.model; + this.tools = definition.tools; + this.inputSchema = definition.inputSchema; + this.outputSchema = definition.outputSchema; + this.maxSteps = definition.maxSteps ?? 10; + this.temperature = definition.temperature ?? 0.7; + this.canDelegate = definition.canDelegate ?? false; + this.metadata = definition.metadata; + + this.neurolink = neurolink; + this.emitter = new EventEmitter(); + + logger.debug(`[Agent:${this.id}] Created agent: ${this.name}`, { + tools: this.tools?.length || 0, + maxSteps: this.maxSteps, + canDelegate: this.canDelegate, + }); + } + + /** + * Execute the agent with given input + * + * @param input - Text input or structured data + * @param options - Execution options + * @returns Agent result with content and metadata + */ + async execute( + input: AgentInput, + options?: AgentExecutionOptions, + ): Promise { + const startTime = Date.now(); + this.executionCount++; + + const traceId = options?.traceId ?? `agent-${this.id}-${Date.now()}`; + + logger.debug(`[Agent:${this.id}] Starting execution`, { + traceId, + input: typeof input === "string" ? input.slice(0, 100) : "structured", + executionCount: this.executionCount, + }); + + this.emitter.emit("agent:start", { + agentId: this.id, + traceId, + timestamp: startTime, + }); + + try { + // Validate input if schema provided + if (this.inputSchema && typeof input !== "string") { + const validation = this.inputSchema.safeParse(input); + if (!validation.success) { + throw new Error( + `Input validation failed: ${validation.error.message}`, + ); + } + } + + // Build the prompt with agent context + const prompt = this.buildPrompt(input, options?.context); + + // Build generation options + const generateOptions = this.buildGenerateOptions( + prompt, + options, + traceId, + ); + + // Execute via NeuroLink + const result = await this.neurolink.generate(generateOptions); + + const duration = Date.now() - startTime; + this.lastExecutionTime = duration; + this.totalExecutionTime += duration; + + // Parse output if schema provided + let parsedOutput: unknown; + if (this.outputSchema && result.content) { + try { + const parsed = JSON.parse(result.content); + const validation = this.outputSchema.safeParse(parsed); + if (validation.success) { + parsedOutput = validation.data; + } else { + logger.warn(`[Agent:${this.id}] Output schema validation failed`, { + error: validation.error.message, + }); + } + } catch { + logger.warn(`[Agent:${this.id}] Failed to parse output as JSON`); + } + } + + logger.debug(`[Agent:${this.id}] Execution completed`, { + traceId, + duration, + contentLength: result.content?.length || 0, + toolsUsed: result.toolsUsed?.length || 0, + }); + + // Pass through toolExecutions, adding duration (not provided by generate()) + const toolExecutions = result.toolExecutions?.map((te) => ({ + name: te.name, + input: te.input, + output: te.output, + duration: 0, + })); + + const agentResult: AgentResult = { + content: result.content || "", + object: parsedOutput, + usage: result.usage, + toolsUsed: result.toolsUsed, + toolExecutions, + duration, + status: "success", + agentId: this.id, + }; + + this.emitter.emit("agent:complete", { + agentId: this.id, + traceId, + duration, + result: agentResult, + }); + + return agentResult; + } catch (error) { + const duration = Date.now() - startTime; + this.lastExecutionTime = duration; + + logger.error(`[Agent:${this.id}] Execution failed`, { + traceId, + error: error instanceof Error ? error.message : String(error), + duration, + }); + + this.emitter.emit("agent:error", { + agentId: this.id, + traceId, + error, + duration, + }); + + return { + content: "", + error: error instanceof Error ? error.message : String(error), + duration, + status: "error", + agentId: this.id, + }; + } + } + + /** + * Stream execution results + * + * @param input - Text input or structured data + * @param options - Execution options + * @yields Agent stream chunks + */ + async *stream( + input: AgentInput, + options?: AgentExecutionOptions, + ): AsyncIterable { + const startTime = Date.now(); + const traceId = options?.traceId ?? `agent-${this.id}-${Date.now()}`; + + this.emitter.emit("agent:start", { + agentId: this.id, + traceId, + timestamp: startTime, + }); + + yield { + type: "agent-start", + agentId: this.id, + timestamp: startTime, + traceId, + }; + + try { + // Validate input if schema provided + if (this.inputSchema && typeof input !== "string") { + const validation = this.inputSchema.safeParse(input); + if (!validation.success) { + throw new Error( + `Input validation failed: ${validation.error.message}`, + ); + } + } + + const prompt = this.buildPrompt(input, options?.context); + const streamOptions = this.buildStreamOptions(prompt, options, traceId); + + // Execute via NeuroLink + const streamResult = await this.neurolink.stream(streamOptions); + + let fullContent = ""; + + for await (const chunk of streamResult.stream) { + // Handle different chunk types from the stream + if ("content" in chunk && typeof chunk.content === "string") { + fullContent += chunk.content; + yield { + type: "agent-text", + agentId: this.id, + content: chunk.content, + isPartial: true, + timestamp: Date.now(), + traceId, + }; + } + + // Handle tool calls if present + if ("toolCall" in chunk && chunk.toolCall) { + const toolCall = chunk.toolCall as { + toolName?: string; + args?: unknown; + toolCallId?: string; + }; + yield { + type: "agent-tool-call", + agentId: this.id, + toolName: toolCall.toolName || "unknown", + args: toolCall.args, + toolCallId: toolCall.toolCallId || `tool-${Date.now()}`, + timestamp: Date.now(), + traceId, + }; + } + + // Handle tool results if present + if ("toolResult" in chunk && chunk.toolResult) { + const toolResult = chunk.toolResult as { + toolName?: string; + toolCallId?: string; + result?: unknown; + success?: boolean; + }; + yield { + type: "agent-tool-result", + agentId: this.id, + toolName: toolResult.toolName || "unknown", + toolCallId: toolResult.toolCallId || `tool-${Date.now()}`, + result: toolResult.result, + success: toolResult.success ?? true, + timestamp: Date.now(), + traceId, + }; + } + } + + const duration = Date.now() - startTime; + this.lastExecutionTime = duration; + this.executionCount++; + this.totalExecutionTime += duration; + + this.emitter.emit("agent:complete", { + agentId: this.id, + traceId, + duration, + content: fullContent, + }); + + yield { + type: "agent-complete", + agentId: this.id, + content: fullContent, + usage: streamResult.usage, + duration, + timestamp: Date.now(), + traceId, + }; + } catch (error) { + this.emitter.emit("agent:error", { + agentId: this.id, + traceId, + error, + }); + + yield { + type: "agent-error", + agentId: this.id, + error: error instanceof Error ? error.message : String(error), + timestamp: Date.now(), + traceId, + }; + } + } + + /** + * Get agent status + */ + getStatus(): AgentStatus { + return { + id: this.id, + name: this.name, + executionCount: this.executionCount, + lastExecutionTime: this.lastExecutionTime, + available: true, + }; + } + + /** + * Get average execution time + */ + getAverageExecutionTime(): number { + if (this.executionCount === 0) { + return 0; + } + return this.totalExecutionTime / this.executionCount; + } + + /** + * Subscribe to agent events + */ + on(event: string, handler: (...args: unknown[]) => void): void { + this.emitter.on(event, handler); + } + + /** + * Unsubscribe from agent events + */ + off(event: string, handler: (...args: unknown[]) => void): void { + this.emitter.off(event, handler); + } + + /** + * Build prompt from input and context + */ + private buildPrompt( + input: AgentInput, + context?: Record, + ): string { + let prompt = typeof input === "string" ? input : JSON.stringify(input); + + if (context && Object.keys(context).length > 0) { + prompt = `Context: ${JSON.stringify(context)}\n\nTask: ${prompt}`; + } + + return prompt; + } + + /** + * Build generation options for NeuroLink.generate() + * + * Uses toolFilter to delegate tool restriction to BaseProvider.applyToolFiltering() + * rather than pre-filtering tools manually. + */ + private buildGenerateOptions( + prompt: string, + options?: AgentExecutionOptions, + traceId?: string, + ): GenerateOptions { + return { + input: { text: prompt }, + provider: this.provider, + model: this.model, + temperature: this.temperature, + systemPrompt: this.instructions, + // toolFilter delegates to BaseProvider.applyToolFiltering() natively + ...(this.tools && this.tools.length > 0 && { toolFilter: this.tools }), + maxSteps: options?.maxSteps ?? this.maxSteps, + requestId: traceId, + context: { + agentId: this.id, + agentName: this.name, + ...options?.context, + }, + }; + } + + /** + * Build stream options for NeuroLink.stream() + * + * Uses toolFilter to delegate tool restriction to BaseProvider.applyToolFiltering() + * rather than pre-filtering tools manually. + */ + private buildStreamOptions( + prompt: string, + options?: AgentExecutionOptions, + traceId?: string, + ): StreamOptions { + return { + input: { text: prompt }, + provider: this.provider, + model: this.model, + temperature: this.temperature, + systemPrompt: this.instructions, + // toolFilter delegates to BaseProvider.applyToolFiltering() natively + ...(this.tools && this.tools.length > 0 && { toolFilter: this.tools }), + maxSteps: options?.maxSteps ?? this.maxSteps, + context: { + agentId: this.id, + agentName: this.name, + traceId, + ...options?.context, + }, + }; + } +} diff --git a/src/lib/agent/agentNetwork.ts b/src/lib/agent/agentNetwork.ts new file mode 100644 index 000000000..e45003861 --- /dev/null +++ b/src/lib/agent/agentNetwork.ts @@ -0,0 +1,590 @@ +/** + * AgentNetwork - Multi-Agent Orchestration for NeuroLink + * + * Uses the ai SDK's built-in tool loop: each agent is wrapped as an ai SDK + * tool, and the network's router is a single neurolink.generate() call with + * maxSteps. The SDK iterates automatically (tool call → execute → feed result + * → next call) until the model stops or maxSteps is reached. + */ + +import { randomUUID } from "crypto"; +import { EventEmitter } from "events"; +import { tool } from "ai"; +import { z } from "zod"; +import type { Tool } from "ai"; +import type { NeuroLink } from "../neurolink.js"; +import type { + AgentNetworkConfig, + AgentPrimitive, + AgentTextChunk, + AgentToolCallChunk, + AgentToolResultChunk, + CoreMessage, + NetworkExecutionInput, + NetworkExecutionOptions, + NetworkExecutionResult, + NetworkExecutionStep, + NetworkExecutionTrace, + NetworkStreamChunk, + NetworkTokenUsage, + Primitive, + ToolPrimitive, + WorkflowPrimitive, +} from "../types/index.js"; +import { logger } from "../utils/logger.js"; +import { ErrorFactory } from "../utils/errorHandling.js"; +import { Agent } from "./agent.js"; + +/** + * AgentNetwork - Multi-agent orchestration using the ai SDK tool loop + * + * Each agent in the network is registered as an ai SDK `tool()`. A single + * `neurolink.generate()` call with `maxSteps` acts as the router: the model + * picks which agent tool(s) to call, the SDK executes them and feeds results + * back, and the loop continues until the model emits `finishReason: "stop"` or + * maxSteps is exhausted. + * + * @example + * ```typescript + * const network = neurolink.createNetwork({ + * name: 'Content Team', + * agents: [researchAgent, writerAgent, reviewerAgent], + * router: { model: 'gpt-4o' } + * }); + * + * const result = await network.execute({ + * message: 'Write an article about AI trends' + * }); + * ``` + */ +export class AgentNetwork { + readonly id: string; + readonly name: string; + readonly description?: string; + + private neurolink: NeuroLink; + private agents: Map = new Map(); + private workflows: Map = new Map(); + private primitives: Map = new Map(); + private emitter: EventEmitter; + private config: AgentNetworkConfig; + + // Lazy tool initialization + private toolsInitialized = false; + private toolsInitPromise: Promise | null = null; + + constructor(config: AgentNetworkConfig, neurolink: NeuroLink) { + if (!config.name || typeof config.name !== "string") { + throw ErrorFactory.invalidConfiguration( + "name", + "AgentNetwork config must have a valid name", + ); + } + if (!Array.isArray(config.agents) || config.agents.length === 0) { + throw ErrorFactory.invalidConfiguration( + "agents", + "AgentNetwork config must have at least one agent", + ); + } + + this.id = config.id ?? randomUUID(); + this.name = config.name; + this.description = config.description; + this.neurolink = neurolink; + this.config = config; + this.emitter = new EventEmitter(); + + this.initializeAgents(config); + this.initializeWorkflows(config); + + logger.info(`[AgentNetwork:${this.id}] Created network: ${this.name}`, { + agentCount: this.agents.size, + workflowCount: this.workflows.size, + toolCount: config.tools?.length || 0, + }); + } + + // ============================================================================ + // INITIALIZATION + // ============================================================================ + + private initializeAgents(config: AgentNetworkConfig): void { + for (const agentDef of config.agents) { + const agent = new Agent(agentDef, this.neurolink); + this.agents.set(agentDef.id, agent); + + const primitive: AgentPrimitive = { + id: agentDef.id, + type: "agent", + name: agentDef.name, + description: agentDef.description, + inputSchema: agentDef.inputSchema, + outputSchema: agentDef.outputSchema, + agent, + }; + + this.primitives.set(agentDef.id, primitive); + logger.debug( + `[AgentNetwork:${this.id}] Registered agent: ${agentDef.name}`, + ); + } + } + + private initializeWorkflows(config: AgentNetworkConfig): void { + if (!config.workflows) { + return; + } + + for (const workflowDef of config.workflows) { + const primitive: WorkflowPrimitive = { + id: workflowDef.id, + type: "workflow", + name: workflowDef.name, + description: workflowDef.description, + inputSchema: workflowDef.inputSchema, + outputSchema: workflowDef.outputSchema, + workflow: workflowDef.workflow, + }; + + this.workflows.set(workflowDef.id, primitive); + this.primitives.set(workflowDef.id, primitive); + logger.debug( + `[AgentNetwork:${this.id}] Registered workflow: ${workflowDef.name}`, + ); + } + } + + private async initializeTools(): Promise { + if (!this.config.tools || this.config.tools.length === 0) { + return; + } + + try { + const availableTools = await this.neurolink.getAllAvailableTools(); + + for (const toolName of this.config.tools) { + if (this.primitives.has(`tool-${toolName}`)) { + continue; + } + + const toolInfo = availableTools.find((t) => t.name === toolName); + if (!toolInfo) { + logger.warn(`[AgentNetwork:${this.id}] Tool not found: ${toolName}`); + continue; + } + + const primitive: ToolPrimitive = { + id: `tool-${toolName}`, + type: "tool", + name: toolName, + description: toolInfo.description || `Tool: ${toolName}`, + tool: { + name: toolName, + description: toolInfo.description, + inputSchema: toolInfo.inputSchema, + }, + execute: async (args) => { + return this.neurolink.executeTool(toolName, args); + }, + }; + + this.primitives.set(primitive.id, primitive); + logger.debug(`[AgentNetwork:${this.id}] Registered tool: ${toolName}`); + } + } catch (error) { + logger.warn(`[AgentNetwork:${this.id}] Failed to initialize tools`, { + error: error instanceof Error ? error.message : String(error), + }); + } + } + + private async ensureToolsInitialized(): Promise { + if (this.toolsInitialized) { + return; + } + if (this.toolsInitPromise) { + return this.toolsInitPromise; + } + + this.toolsInitPromise = this.initializeTools(); + await this.toolsInitPromise; + this.toolsInitialized = true; + } + + // ============================================================================ + // AGENT-AS-TOOL CONSTRUCTION + // ============================================================================ + + /** + * Build a Record of ai SDK tools — one per agent in the network. + * The router model calls these tools to delegate subtasks. + */ + private buildAgentTools(): Record { + const tools: Record = {}; + + for (const [id, agentInstance] of this.agents) { + // Capture in closure so the async execute below closes over the right values + const capturedId = id; + const capturedAgent = agentInstance; + + const schema = z.object({ + task: z.string().describe("The task to delegate to this agent"), + }); + + tools[`agent_${capturedId}`] = tool({ + description: `Agent: ${capturedAgent.name} - ${capturedAgent.description}`, + inputSchema: schema, + execute: async (params: z.infer) => { + logger.debug( + `[AgentNetwork:${this.id}] Delegating to agent: ${capturedAgent.name}`, + { task: params.task.slice(0, 100) }, + ); + const result = await capturedAgent.execute(params.task); + return { + agentId: capturedId, + content: result.content, + status: result.status, + error: result.error, + }; + }, + }); + } + + return tools; + } + + /** + * Build the router system prompt that describes all available agents and + * instructs the model to delegate tasks via agent tools. + */ + private buildRouterSystemPrompt(): string { + const agentDescriptions = Array.from(this.agents.values()) + .map((a) => `- agent_${a.id}: ${a.name} — ${a.description}`) + .join("\n"); + + const baseInstructions = + this.config.router?.instructions ?? + "You are a task orchestrator. Analyze the user's request and delegate to the most appropriate agent(s). You may call multiple agents sequentially if the task requires multiple steps. Once all necessary agents have responded, synthesize their outputs into a final answer."; + + return `${baseInstructions} + +Available agents: +${agentDescriptions} + +Use the appropriate agent tool(s) to handle the task. Return a clear, complete final answer once all needed agents have completed their work.`; + } + + /** + * Build NetworkExecutionTrace steps from generate() toolExecutions. + */ + private buildTrace( + toolExecutions: + | Array<{ name: string; input: Record; output: unknown }> + | undefined, + traceId: string, + startTime: number, + ): NetworkExecutionTrace { + const steps: NetworkExecutionStep[] = (toolExecutions ?? []).map( + (exec, index) => { + // Tool name format is "agent_" — extract agent id + const agentId = exec.name.startsWith("agent_") + ? exec.name.slice("agent_".length) + : exec.name; + + const primitive = this.primitives.get(agentId); + + return { + index, + primitive: { + type: primitive?.type ?? "agent", + id: agentId, + name: primitive?.name ?? agentId, + }, + input: exec.input, + output: exec.output, + duration: 0, // individual step timing not available from generate() + timestamp: startTime, + }; + }, + ); + + return { + traceId, + steps, + routingDecisions: [], // routing is implicit in the model's tool calls + startTime, + endTime: Date.now(), + }; + } + + // ============================================================================ + // PUBLIC EXECUTION API + // ============================================================================ + + /** + * Execute the network with intelligent routing via the ai SDK tool loop. + * + * A single `neurolink.generate()` call is issued. The model decides which + * agent tool(s) to call; the SDK executes them and loops until `stop` or + * `maxSteps` is reached. + */ + async execute( + input: NetworkExecutionInput, + options?: NetworkExecutionOptions, + ): Promise { + const startTime = Date.now(); + const traceId = options?.tracing?.traceId ?? randomUUID(); + const maxSteps = options?.maxSteps ?? this.config.defaults?.maxSteps ?? 10; + + await this.ensureToolsInitialized(); + + const message = this.extractMessageContent(input.message); + + logger.info(`[AgentNetwork:${this.id}] Starting execute`, { + traceId, + maxSteps, + agentCount: this.agents.size, + }); + + this.emit("network:start", { traceId, input: message, startTime }); + + try { + const agentTools = this.buildAgentTools(); + const systemPrompt = this.buildRouterSystemPrompt(); + + const result = await this.neurolink.generate({ + input: { text: message }, + systemPrompt, + provider: this.config.router?.provider as string | undefined, + model: this.config.router?.model, + maxSteps, + tools: agentTools, + } as Parameters[0]); + + const toolExecutions = (result.toolExecutions ?? []) as Array<{ + name: string; + input: Record; + output: unknown; + }>; + + const trace = this.buildTrace(toolExecutions, traceId, startTime); + + // Aggregate token usage across all agent tool calls + const usage = result.usage; + const totalUsage: NetworkTokenUsage = { + promptTokens: usage?.input ?? 0, + completionTokens: usage?.output ?? 0, + totalTokens: usage?.total ?? 0, + byAgent: {}, + }; + + const executionResult: NetworkExecutionResult = { + content: result.content || "", + trace, + usage: totalUsage, + status: "completed", + duration: Date.now() - startTime, + }; + + this.emit("network:complete", { traceId, result: executionResult }); + return executionResult; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + + logger.error(`[AgentNetwork:${this.id}] Execution failed`, { + traceId, + error: errorMessage, + }); + + const failedResult: NetworkExecutionResult = { + content: "", + trace: { + traceId, + steps: [], + routingDecisions: [], + startTime, + endTime: Date.now(), + }, + usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }, + status: "error", + duration: Date.now() - startTime, + error: errorMessage, + }; + + this.emit("network:error", { traceId, error: errorMessage }); + return failedResult; + } + } + + /** + * Stream network execution using the ai SDK tool loop. + * + * Calls `neurolink.stream()` with agent tools. Text chunks, tool calls, and + * tool results are forwarded as typed NetworkStreamChunk events. + */ + async *stream( + input: NetworkExecutionInput, + options?: NetworkExecutionOptions, + ): AsyncIterable { + const startTime = Date.now(); + const traceId = options?.tracing?.traceId ?? randomUUID(); + const maxSteps = options?.maxSteps ?? this.config.defaults?.maxSteps ?? 10; + + await this.ensureToolsInitialized(); + + const message = this.extractMessageContent(input.message); + + yield { + type: "network-start", + networkId: this.id, + input: message, + timestamp: startTime, + traceId, + }; + + const totalUsage: NetworkTokenUsage = { + promptTokens: 0, + completionTokens: 0, + totalTokens: 0, + }; + let finalContent = ""; + + try { + const agentTools = this.buildAgentTools(); + const systemPrompt = this.buildRouterSystemPrompt(); + + const streamResult = await this.neurolink.stream({ + input: { text: message }, + systemPrompt, + provider: this.config.router?.provider as string | undefined, + model: this.config.router?.model, + maxSteps, + tools: agentTools, + } as Parameters[0]); + + for await (const chunk of streamResult.stream) { + if ("content" in chunk && typeof chunk.content === "string") { + finalContent += chunk.content; + yield { + type: "agent-text", + agentId: this.id, + content: chunk.content, + isPartial: true, + timestamp: Date.now(), + traceId, + } as AgentTextChunk; + } else if ("toolCall" in chunk) { + const tc = chunk as { + toolCall?: { + toolName?: string; + args?: unknown; + toolCallId?: string; + }; + }; + yield { + type: "agent-tool-call", + agentId: this.id, + toolName: tc.toolCall?.toolName ?? "unknown", + args: tc.toolCall?.args, + toolCallId: tc.toolCall?.toolCallId ?? `tool-${Date.now()}`, + timestamp: Date.now(), + traceId, + } as AgentToolCallChunk; + } else if ("toolResult" in chunk) { + const tr = chunk as { + toolResult?: { + toolName?: string; + toolCallId?: string; + result?: unknown; + }; + }; + yield { + type: "agent-tool-result", + agentId: this.id, + toolName: tr.toolResult?.toolName ?? "unknown", + toolCallId: tr.toolResult?.toolCallId ?? `tool-${Date.now()}`, + result: tr.toolResult?.result, + success: true, + timestamp: Date.now(), + traceId, + } as AgentToolResultChunk; + } + } + + // Collect final usage if available + if (streamResult.usage) { + totalUsage.promptTokens = streamResult.usage.input ?? 0; + totalUsage.completionTokens = streamResult.usage.output ?? 0; + totalUsage.totalTokens = streamResult.usage.total ?? 0; + } + + const trace: NetworkExecutionTrace = { + traceId, + steps: [], + routingDecisions: [], + startTime, + endTime: Date.now(), + }; + + yield { + type: "network-complete", + result: { + content: finalContent, + trace, + usage: totalUsage, + status: "completed", + duration: Date.now() - startTime, + }, + timestamp: Date.now(), + traceId, + }; + } catch (error) { + yield { + type: "network-error", + error: error instanceof Error ? error.message : String(error), + timestamp: Date.now(), + traceId, + }; + } + } + + // ============================================================================ + // PUBLIC ACCESSORS + // ============================================================================ + + getAgent(id: string): Agent | undefined { + return this.agents.get(id); + } + + getAllAgents(): Agent[] { + return Array.from(this.agents.values()); + } + + getAllPrimitives(): Primitive[] { + return Array.from(this.primitives.values()); + } + + on(event: string, handler: (...args: unknown[]) => void): void { + this.emitter.on(event, handler); + } + + off(event: string, handler: (...args: unknown[]) => void): void { + this.emitter.off(event, handler); + } + + // ============================================================================ + // PRIVATE HELPERS + // ============================================================================ + + private extractMessageContent(message: string | CoreMessage[]): string { + if (typeof message === "string") { + return message; + } + return message.map((m) => m.content).join("\n"); + } + + private emit(event: string, ...args: unknown[]): void { + this.emitter.emit(event, ...args); + } +} diff --git a/src/lib/agent/communication/index.ts b/src/lib/agent/communication/index.ts new file mode 100644 index 000000000..97953e78f --- /dev/null +++ b/src/lib/agent/communication/index.ts @@ -0,0 +1,10 @@ +/** + * Agent Communication Module + * + * Provides communication infrastructure for multi-agent networks. + * + * Types for this module live in src/lib/types/agentNetwork.ts and are + * re-exported via the central barrel at src/lib/types/index.ts. + */ + +export { MessageBus } from "./message-bus.js"; diff --git a/src/lib/agent/communication/message-bus.ts b/src/lib/agent/communication/message-bus.ts new file mode 100644 index 000000000..bdf0ad06a --- /dev/null +++ b/src/lib/agent/communication/message-bus.ts @@ -0,0 +1,537 @@ +/** + * Message Bus - Inter-agent communication infrastructure + * + * Provides a publish-subscribe message bus for agent-to-agent communication + * with support for: + * - Topic-based messaging + * - Request-response patterns + * - Broadcast messaging + * - Message persistence and replay + */ + +import { EventEmitter } from "events"; +import { randomUUID } from "crypto"; +import { logger } from "../../utils/logger.js"; +import type { + AgentMessage, + MessageHandler, + SubscriptionOptions, + MessageBusConfig, + MessageBusSubscription, +} from "../../types/index.js"; + +/** + * Message Bus - Central hub for agent communication + */ +export class MessageBus { + private subscriptions: Map = new Map(); + private messageHistory: AgentMessage[] = []; + private pendingRequests: Map< + string, + { + resolve: (msg: AgentMessage) => void; + reject: (err: Error) => void; + timeout: NodeJS.Timeout; + } + > = new Map(); + private deadLetterQueue: AgentMessage[] = []; + private config: Required; + private emitter: EventEmitter; + + constructor(config?: MessageBusConfig) { + this.config = { + maxHistorySize: 1000, + defaultTtl: 60000, + enablePersistence: false, + enableDeadLetterQueue: true, + requestTimeout: 30000, + ...config, + }; + this.emitter = new EventEmitter(); + + // Increase max listeners for large networks + this.emitter.setMaxListeners(100); + + logger.debug("[MessageBus] Created with config", { + maxHistorySize: this.config.maxHistorySize, + enableDeadLetterQueue: this.config.enableDeadLetterQueue, + }); + } + + /** + * Subscribe to a topic + */ + subscribe( + topic: string, + subscriberId: string, + handler: MessageHandler, + options?: SubscriptionOptions, + ): string { + const subscriptionId = randomUUID(); + const subscription: MessageBusSubscription = { + id: subscriptionId, + topic, + handler, + options: options ?? {}, + messageCount: 0, + subscriberId, + }; + + const topicSubs = this.subscriptions.get(topic) ?? []; + topicSubs.push(subscription); + this.subscriptions.set(topic, topicSubs); + + logger.debug(`[MessageBus] Subscription created`, { + subscriptionId, + topic, + subscriberId, + }); + + this.emitter.emit("subscription:created", { + subscriptionId, + topic, + subscriberId, + }); + return subscriptionId; + } + + /** + * Unsubscribe from a topic + */ + unsubscribe(subscriptionId: string): boolean { + for (const [topic, subs] of this.subscriptions) { + const index = subs.findIndex((s) => s.id === subscriptionId); + if (index !== -1) { + subs.splice(index, 1); + if (subs.length === 0) { + this.subscriptions.delete(topic); + } + logger.debug(`[MessageBus] Subscription removed`, { + subscriptionId, + topic, + }); + return true; + } + } + return false; + } + + /** + * Unsubscribe all subscriptions for an agent + */ + unsubscribeAll(subscriberId: string): number { + let count = 0; + for (const [topic, subs] of this.subscriptions) { + const filtered = subs.filter((s) => s.subscriberId !== subscriberId); + count += subs.length - filtered.length; + if (filtered.length === 0) { + this.subscriptions.delete(topic); + } else { + this.subscriptions.set(topic, filtered); + } + } + return count; + } + + /** + * Publish a message to a topic + */ + async publish( + topic: string, + senderId: string, + payload: unknown, + options?: Partial< + Omit + >, + ): Promise { + const message: AgentMessage = { + id: randomUUID(), + type: options?.type ?? "event", + topic, + senderId, + payload, + priority: options?.priority ?? "normal", + timestamp: Date.now(), + ttl: options?.ttl ?? this.config.defaultTtl, + recipientId: options?.recipientId, + correlationId: options?.correlationId, + replyTo: options?.replyTo, + metadata: options?.metadata, + }; + + await this.deliverMessage(message); + } + + /** + * Send a direct message to a specific agent + */ + async sendDirect( + senderId: string, + recipientId: string, + payload: unknown, + options?: Partial< + Omit< + AgentMessage, + "id" | "senderId" | "recipientId" | "payload" | "timestamp" + > + >, + ): Promise { + const topic = `direct:${recipientId}`; + const message: AgentMessage = { + id: randomUUID(), + type: "direct", + topic, + senderId, + recipientId, + payload, + priority: options?.priority ?? "normal", + timestamp: Date.now(), + ttl: options?.ttl ?? this.config.defaultTtl, + metadata: options?.metadata, + }; + + await this.deliverMessage(message); + } + + /** + * Send a request and wait for response + */ + async request( + topic: string, + senderId: string, + payload: unknown, + timeout?: number, + ): Promise { + const correlationId = randomUUID(); + const replyTo = `reply:${correlationId}`; + + // Create a promise that will resolve when we get the response + const responsePromise = new Promise((resolve, reject) => { + const timeoutMs = timeout ?? this.config.requestTimeout; + const timeoutHandle = setTimeout(() => { + this.pendingRequests.delete(correlationId); + this.unsubscribeByTopic(replyTo, senderId); + reject(new Error(`Request timed out after ${timeoutMs}ms`)); + }, timeoutMs); + + this.pendingRequests.set(correlationId, { + resolve, + reject, + timeout: timeoutHandle, + }); + }); + + // Subscribe to reply topic + this.subscribe(replyTo, senderId, (msg) => { + const pending = this.pendingRequests.get(correlationId); + if (pending) { + clearTimeout(pending.timeout); + this.pendingRequests.delete(correlationId); + this.unsubscribeByTopic(replyTo, senderId); + pending.resolve(msg); + } + }); + + // Send the request + await this.publish(topic, senderId, payload, { + type: "request", + correlationId, + replyTo, + }); + + return responsePromise; + } + + /** + * Reply to a request + */ + async reply( + originalMessage: AgentMessage, + senderId: string, + payload: unknown, + ): Promise { + if (!originalMessage.replyTo) { + throw new Error("Cannot reply to message without replyTo field"); + } + + await this.publish(originalMessage.replyTo, senderId, payload, { + type: "response", + correlationId: originalMessage.correlationId, + }); + } + + /** + * Broadcast a message to all subscribers + */ + async broadcast( + senderId: string, + payload: unknown, + excludeTopics?: string[], + ): Promise { + const message: AgentMessage = { + id: randomUUID(), + type: "broadcast", + topic: "broadcast", + senderId, + payload, + priority: "normal", + timestamp: Date.now(), + ttl: this.config.defaultTtl, + }; + + // Deliver to all topics except excluded ones + for (const topic of this.subscriptions.keys()) { + if (excludeTopics?.includes(topic)) { + continue; + } + if (topic.startsWith("reply:") || topic.startsWith("direct:")) { + continue; + } + + await this.deliverMessage({ ...message, topic }); + } + } + + /** + * Deliver a message to subscribers + */ + private async deliverMessage(message: AgentMessage): Promise { + // Add to history + this.messageHistory.push(message); + if (this.messageHistory.length > this.config.maxHistorySize) { + this.messageHistory.shift(); + } + + // Check if message has expired + if (message.ttl && Date.now() - message.timestamp > message.ttl) { + logger.debug(`[MessageBus] Message expired`, { messageId: message.id }); + return; + } + + const subs = this.subscriptions.get(message.topic) ?? []; + const deliveryPromises: Promise[] = []; + + for (const sub of subs) { + // Check if subscription should receive this message + if (!this.shouldDeliver(message, sub)) { + continue; + } + + // Check max messages limit + if ( + sub.options.maxMessages !== undefined && + sub.options.maxMessages !== -1 && + sub.messageCount >= sub.options.maxMessages + ) { + continue; + } + + sub.messageCount++; + + deliveryPromises.push( + Promise.resolve(sub.handler(message)).catch((error) => { + logger.error(`[MessageBus] Message delivery failed`, { + messageId: message.id, + subscriptionId: sub.id, + error: error instanceof Error ? error.message : String(error), + }); + + // Add to dead letter queue if enabled + if (this.config.enableDeadLetterQueue) { + this.deadLetterQueue.push(message); + } + }), + ); + } + + await Promise.all(deliveryPromises); + this.emitter.emit("message:delivered", { + messageId: message.id, + topic: message.topic, + }); + } + + /** + * Check if message should be delivered to subscription + */ + private shouldDeliver( + message: AgentMessage, + sub: MessageBusSubscription, + ): boolean { + const opts = sub.options; + + // Filter by sender + if ( + opts.filterBySender && + !opts.filterBySender.includes(message.senderId) + ) { + return false; + } + + // Filter by type + if (opts.filterByType && !opts.filterByType.includes(message.type)) { + return false; + } + + // Filter by priority + if ( + opts.filterByPriority && + !opts.filterByPriority.includes(message.priority) + ) { + return false; + } + + // Custom filter + if (opts.customFilter && !opts.customFilter(message)) { + return false; + } + + // Direct messages: check recipient + if (message.type === "direct" && message.recipientId !== sub.subscriberId) { + return false; + } + + return true; + } + + /** + * Unsubscribe by topic for a specific subscriber + */ + private unsubscribeByTopic(topic: string, subscriberId: string): void { + const subs = this.subscriptions.get(topic); + if (subs) { + const filtered = subs.filter((s) => s.subscriberId !== subscriberId); + if (filtered.length === 0) { + this.subscriptions.delete(topic); + } else { + this.subscriptions.set(topic, filtered); + } + } + } + + /** + * Get message history for a topic + */ + getHistory(topic?: string, limit?: number): AgentMessage[] { + let messages = topic + ? this.messageHistory.filter((m) => m.topic === topic) + : this.messageHistory; + + if (limit) { + messages = messages.slice(-limit); + } + + return messages; + } + + /** + * Get dead letter queue messages + */ + getDeadLetterQueue(): AgentMessage[] { + return [...this.deadLetterQueue]; + } + + /** + * Clear dead letter queue + */ + clearDeadLetterQueue(): void { + this.deadLetterQueue = []; + } + + /** + * Replay messages from history + */ + async replayHistory( + topic: string, + subscriberId: string, + since?: number, + ): Promise { + const messages = this.messageHistory.filter( + (m) => m.topic === topic && (!since || m.timestamp >= since), + ); + + const subs = + this.subscriptions + .get(topic) + ?.filter((s) => s.subscriberId === subscriberId) ?? []; + + for (const message of messages) { + for (const sub of subs) { + if (this.shouldDeliver(message, sub)) { + await Promise.resolve(sub.handler(message)).catch(() => { + // Ignore replay errors + }); + } + } + } + } + + /** + * Get all topics + */ + getTopics(): string[] { + return Array.from(this.subscriptions.keys()); + } + + /** + * Get subscriber count for a topic + */ + getSubscriberCount(topic: string): number { + return this.subscriptions.get(topic)?.length ?? 0; + } + + /** + * Get statistics + */ + getStats(): { + topicCount: number; + totalSubscriptions: number; + historySize: number; + deadLetterQueueSize: number; + pendingRequests: number; + } { + let totalSubscriptions = 0; + for (const subs of this.subscriptions.values()) { + totalSubscriptions += subs.length; + } + + return { + topicCount: this.subscriptions.size, + totalSubscriptions, + historySize: this.messageHistory.length, + deadLetterQueueSize: this.deadLetterQueue.length, + pendingRequests: this.pendingRequests.size, + }; + } + + /** + * Subscribe to bus events + */ + on(event: string, handler: (...args: unknown[]) => void): void { + this.emitter.on(event, handler); + } + + /** + * Unsubscribe from bus events + */ + off(event: string, handler: (...args: unknown[]) => void): void { + this.emitter.off(event, handler); + } + + /** + * Shutdown the message bus + */ + shutdown(): void { + // Clear pending requests + for (const [, pending] of this.pendingRequests) { + clearTimeout(pending.timeout); + pending.reject(new Error("Message bus shutdown")); + } + this.pendingRequests.clear(); + + // Clear subscriptions + this.subscriptions.clear(); + + logger.debug("[MessageBus] Shutdown complete"); + } +} diff --git a/src/lib/agent/coordination/coordinator.ts b/src/lib/agent/coordination/coordinator.ts new file mode 100644 index 000000000..f564fe0cb --- /dev/null +++ b/src/lib/agent/coordination/coordinator.ts @@ -0,0 +1,728 @@ +/** + * Agent Coordinator - Manages coordination between agents in a network + * + * The Coordinator is responsible for: + * - Managing agent lifecycle and state + * - Coordinating task execution across multiple agents + * - Handling agent dependencies and execution order + * - Managing shared context and state between agents + */ + +import { EventEmitter } from "events"; +import { randomUUID } from "crypto"; +import { withTimeout } from "../../utils/async/withTimeout.js"; +import type { Agent } from "../agent.js"; +import { logger } from "../../utils/logger.js"; +import type { + AgentResult, + AgentStatus, + AgentInstance, + NetworkExecutionStep, + CoordinatorConfig, + CoordinationContext, + CoordinationResult, + TaskAssignment, +} from "../../types/index.js"; + +/** + * Agent Coordinator - Orchestrates multi-agent execution + */ +export class AgentCoordinator { + private agents: Map = new Map(); + private config: CoordinatorConfig; + private emitter: EventEmitter; + private activeExecutions: Map> = new Map(); + private executionHistory: NetworkExecutionStep[] = []; + private roundRobinCursor: number = 0; + + constructor(config?: Partial) { + this.config = { + strategy: "sequential", + maxConcurrency: 3, + agentTimeout: 60000, + continueOnFailure: false, + ...config, + }; + this.emitter = new EventEmitter(); + + logger.debug("[AgentCoordinator] Created with config", { + strategy: this.config.strategy, + maxConcurrency: this.config.maxConcurrency, + }); + } + + /** + * Register an agent with the coordinator + */ + registerAgent(agent: Agent): void { + this.agents.set(agent.id, agent); + logger.debug(`[AgentCoordinator] Registered agent: ${agent.name}`); + } + + /** + * Unregister an agent + */ + unregisterAgent(agentId: string): void { + this.agents.delete(agentId); + } + + /** + * Get all registered agents + */ + getAgents(): Agent[] { + return Array.from(this.agents.values()); + } + + /** + * Get agent status + */ + getAgentStatus(agentId: string): AgentStatus | undefined { + const agent = this.agents.get(agentId); + return agent?.getStatus(); + } + + /** + * Execute a coordinated task across agents + */ + async coordinate( + task: string, + options?: Partial, + ): Promise { + const startTime = Date.now(); + const executionId = randomUUID(); + const config = { ...this.config, ...options }; + + const context: CoordinationContext = { + currentStep: 0, + previousResults: new Map(), + sharedState: new Map(), + metadata: { + startTime, + strategy: config.strategy, + executionId, + }, + }; + + this.emitter.emit("coordination:start", { executionId, task }); + + try { + let result: CoordinationResult; + + switch (config.strategy) { + case "sequential": + result = await this.executeSequential(task, context, config); + break; + case "parallel": + result = await this.executeParallel(task, context, config); + break; + case "pipeline": + result = await this.executePipeline(task, context, config); + break; + case "roundRobin": + result = await this.executeRoundRobin(task, context, config); + break; + case "leastBusy": + result = await this.executeLeastBusy(task, context, config); + break; + case "custom": + if (!config.customCoordinator) { + throw new Error( + "Custom coordinator function required for custom strategy", + ); + } + result = await config.customCoordinator( + this.getAgents(), + task, + context, + ); + break; + default: + throw new Error(`Unknown coordination strategy: ${config.strategy}`); + } + + this.emitter.emit("coordination:complete", { executionId, result }); + return result; + } catch (error) { + const errorResult: CoordinationResult = { + success: false, + agentResults: context.previousResults, + steps: this.executionHistory, + errors: [ + { + agentId: "coordinator", + error: error instanceof Error ? error.message : String(error), + }, + ], + duration: Date.now() - startTime, + metadata: { + executionId, + strategy: config.strategy, + agentsExecuted: context.previousResults.size, + agentsFailed: 1, + }, + }; + + this.emitter.emit("coordination:error", { executionId, error }); + return errorResult; + } + } + + /** + * Execute agents sequentially + */ + private async executeSequential( + task: string, + context: CoordinationContext, + config: CoordinatorConfig, + ): Promise { + const agents = this.getAgents(); + if (agents.length === 0) { + return { + success: false, + agentResults: context.previousResults, + steps: [], + finalOutput: "", + errors: [{ agentId: "coordinator", error: "No agents registered" }], + duration: Date.now() - context.metadata.startTime, + metadata: { + executionId: context.metadata.executionId, + strategy: config.strategy ?? "sequential", + agentsExecuted: 0, + agentsFailed: 0, + }, + }; + } + const errors: Array<{ agentId: string; error: string }> = []; + const steps: NetworkExecutionStep[] = []; + let currentInput = task; + let finalOutput = ""; + + for (let i = 0; i < agents.length; i++) { + const agent = agents[i]; + context.currentStep = i; + + const stepStart = Date.now(); + try { + const result = await this.executeAgentWithTimeout( + agent, + currentInput, + context, + ); + + context.previousResults.set(agent.id, result); + steps.push({ + index: i, + primitive: { type: "agent", id: agent.id, name: agent.name }, + input: currentInput, + output: result.content, + duration: Date.now() - stepStart, + usage: result.usage, + timestamp: stepStart, + }); + + if (result.status === "success") { + currentInput = result.content; + finalOutput = result.content; + } else if (result.error) { + errors.push({ agentId: agent.id, error: result.error }); + if (!config.continueOnFailure) { + break; + } + } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + errors.push({ agentId: agent.id, error: errorMsg }); + steps.push({ + index: i, + primitive: { type: "agent", id: agent.id, name: agent.name }, + input: currentInput, + error: errorMsg, + duration: Date.now() - stepStart, + timestamp: stepStart, + }); + if (!config.continueOnFailure) { + break; + } + } + } + + return { + success: errors.length === 0, + agentResults: context.previousResults, + steps, + finalOutput, + errors, + duration: Date.now() - context.metadata.startTime, + metadata: { + executionId: context.metadata.executionId, + strategy: "sequential", + agentsExecuted: context.previousResults.size, + agentsFailed: errors.length, + }, + }; + } + + /** + * Execute agents in parallel + */ + private async executeParallel( + task: string, + context: CoordinationContext, + config: CoordinatorConfig, + ): Promise { + const agents = this.getAgents(); + if (agents.length === 0) { + return { + success: false, + agentResults: context.previousResults, + steps: [], + finalOutput: "", + errors: [{ agentId: "coordinator", error: "No agents registered" }], + duration: Date.now() - context.metadata.startTime, + metadata: { + executionId: context.metadata.executionId, + strategy: config.strategy ?? "parallel", + agentsExecuted: 0, + agentsFailed: 0, + }, + }; + } + const errors: Array<{ agentId: string; error: string }> = []; + const steps: NetworkExecutionStep[] = []; + const maxConcurrency = Math.max(1, config.maxConcurrency ?? 3); + + // Split agents into batches based on concurrency + const batches: Agent[][] = []; + for (let i = 0; i < agents.length; i += maxConcurrency) { + batches.push(agents.slice(i, i + maxConcurrency)); + } + + let stepIndex = 0; + const allResults: string[] = []; + + for (const batch of batches) { + const batchPromises = batch.map(async (agent) => { + const stepStart = Date.now(); + const currentIndex = stepIndex++; + + try { + const result = await this.executeAgentWithTimeout( + agent, + task, + context, + ); + + context.previousResults.set(agent.id, result); + steps.push({ + index: currentIndex, + primitive: { type: "agent", id: agent.id, name: agent.name }, + input: task, + output: result.content, + duration: Date.now() - stepStart, + usage: result.usage, + timestamp: stepStart, + }); + + if (result.status === "success") { + allResults.push(result.content); + } else if (result.error) { + errors.push({ agentId: agent.id, error: result.error }); + } + + return result; + } catch (error) { + const errorMsg = + error instanceof Error ? error.message : String(error); + errors.push({ agentId: agent.id, error: errorMsg }); + steps.push({ + index: currentIndex, + primitive: { type: "agent", id: agent.id, name: agent.name }, + input: task, + error: errorMsg, + duration: Date.now() - stepStart, + timestamp: stepStart, + }); + return null; + } + }); + + await Promise.all(batchPromises); + } + + // Combine results + const finalOutput = allResults.join("\n\n---\n\n"); + + return { + success: errors.length === 0, + agentResults: context.previousResults, + steps, + finalOutput, + errors, + duration: Date.now() - context.metadata.startTime, + metadata: { + executionId: context.metadata.executionId, + strategy: "parallel", + agentsExecuted: context.previousResults.size, + agentsFailed: errors.length, + }, + }; + } + + /** + * Execute agents in a pipeline (output feeds into next) + */ + private async executePipeline( + task: string, + context: CoordinationContext, + config: CoordinatorConfig, + ): Promise { + // Pipeline is essentially sequential with explicit input chaining + return this.executeSequential(task, context, config); + } + + /** + * Execute using round-robin distribution + */ + private async executeRoundRobin( + task: string, + context: CoordinationContext, + _config: CoordinatorConfig, + ): Promise { + const agents = this.getAgents(); + if (agents.length === 0) { + return { + success: false, + agentResults: new Map(), + steps: [], + errors: [{ agentId: "coordinator", error: "No agents registered" }], + duration: 0, + metadata: { + executionId: context.metadata.executionId, + strategy: "roundRobin", + agentsExecuted: 0, + agentsFailed: 0, + }, + }; + } + + // For round robin, select the next agent in rotation using persistent cursor + const selectedIndex = this.roundRobinCursor % agents.length; + this.roundRobinCursor++; + const agent = agents[selectedIndex]; + + const stepStart = Date.now(); + const result = await this.executeAgentWithTimeout(agent, task, context); + + context.previousResults.set(agent.id, result); + + const step: NetworkExecutionStep = { + index: 0, + primitive: { type: "agent", id: agent.id, name: agent.name }, + input: task, + output: result.content, + duration: Date.now() - stepStart, + usage: result.usage, + timestamp: stepStart, + }; + + return { + success: result.status === "success", + agentResults: context.previousResults, + steps: [step], + finalOutput: result.content, + errors: result.error ? [{ agentId: agent.id, error: result.error }] : [], + duration: Date.now() - context.metadata.startTime, + metadata: { + executionId: context.metadata.executionId, + strategy: "roundRobin", + agentsExecuted: 1, + agentsFailed: result.status === "error" ? 1 : 0, + }, + }; + } + + /** + * Execute using least busy agent + */ + private async executeLeastBusy( + task: string, + context: CoordinationContext, + _config: CoordinatorConfig, + ): Promise { + const agents = this.getAgents(); + if (agents.length === 0) { + return { + success: false, + agentResults: new Map(), + steps: [], + errors: [{ agentId: "coordinator", error: "No agents registered" }], + duration: 0, + metadata: { + executionId: context.metadata.executionId, + strategy: "leastBusy", + agentsExecuted: 0, + agentsFailed: 0, + }, + }; + } + + // Find the least busy agent (fewest active executions or lowest execution count) + let leastBusyAgent = agents[0]; + let lowestCount = leastBusyAgent.getStatus().executionCount; + + for (const agent of agents) { + const status = agent.getStatus(); + if (status.executionCount < lowestCount) { + lowestCount = status.executionCount; + leastBusyAgent = agent; + } + } + + const stepStart = Date.now(); + const result = await this.executeAgentWithTimeout( + leastBusyAgent, + task, + context, + ); + + context.previousResults.set(leastBusyAgent.id, result); + + const step: NetworkExecutionStep = { + index: 0, + primitive: { + type: "agent", + id: leastBusyAgent.id, + name: leastBusyAgent.name, + }, + input: task, + output: result.content, + duration: Date.now() - stepStart, + usage: result.usage, + timestamp: stepStart, + }; + + return { + success: result.status === "success", + agentResults: context.previousResults, + steps: [step], + finalOutput: result.content, + errors: result.error + ? [{ agentId: leastBusyAgent.id, error: result.error }] + : [], + duration: Date.now() - context.metadata.startTime, + metadata: { + executionId: context.metadata.executionId, + strategy: "leastBusy", + agentsExecuted: 1, + agentsFailed: result.status === "error" ? 1 : 0, + }, + }; + } + + /** + * Execute an agent with timeout + */ + private async executeAgentWithTimeout( + agent: AgentInstance, + input: string, + context: CoordinationContext, + ): Promise { + const timeout = this.config.agentTimeout ?? 60000; + + const executionPromise = agent.execute(input, { + context: Object.fromEntries(context.sharedState), + traceId: context.metadata.executionId, + }); + + // Track active execution + this.activeExecutions.set(agent.id, executionPromise); + + try { + return await withTimeout( + executionPromise, + timeout, + `Agent execution timeout after ${timeout}ms`, + ); + } finally { + this.activeExecutions.delete(agent.id); + } + } + + /** + * Execute multiple task assignments with dependencies + */ + async executeWithDependencies( + assignments: TaskAssignment[], + ): Promise { + const startTime = Date.now(); + const executionId = randomUUID(); + const context: CoordinationContext = { + currentStep: 0, + previousResults: new Map(), + sharedState: new Map(), + metadata: { + startTime, + strategy: "custom", + executionId, + }, + }; + + const errors: Array<{ agentId: string; error: string }> = []; + const steps: NetworkExecutionStep[] = []; + const completed = new Set(); + const failed = new Set(); + const pending = new Map(assignments.map((a) => [a.agent.id, a])); + let stepCounter = 0; + + while (pending.size > 0) { + // Find assignments whose dependencies are satisfied + const ready: TaskAssignment[] = []; + for (const [_agentId, assignment] of pending) { + const deps = assignment.dependencies ?? []; + if (deps.every((d) => completed.has(d))) { + ready.push(assignment); + } + } + + if (ready.length === 0 && pending.size > 0) { + // Circular dependency or unmet dependency + errors.push({ + agentId: "coordinator", + error: "Unresolvable dependencies detected", + }); + break; + } + + // Sort by priority and execute + ready.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)); + + // Execute in parallel batches + const maxConcurrency = Math.max(1, this.config.maxConcurrency ?? 3); + for (let i = 0; i < ready.length; i += maxConcurrency) { + const batch = ready.slice(i, i + maxConcurrency); + const batchPromises = batch.map(async (assignment) => { + const stepStart = Date.now(); + const stepIndex = stepCounter++; + + // Fail fast if any dependency already failed + const deps = assignment.dependencies ?? []; + const hasFailedDep = deps.some((d) => failed.has(d)); + if (hasFailedDep) { + const failedDepId = deps.find((d) => failed.has(d)); + errors.push({ + agentId: assignment.agent.id, + error: `Dependency '${failedDepId}' failed`, + }); + failed.add(assignment.agent.id); + pending.delete(assignment.agent.id); + return null; + } + + try { + const result = await this.executeAgentWithTimeout( + assignment.agent, + assignment.input, + context, + ); + + context.previousResults.set(assignment.agent.id, result); + pending.delete(assignment.agent.id); + + steps.push({ + index: stepIndex, + primitive: { + type: "agent", + id: assignment.agent.id, + name: assignment.agent.name, + }, + input: assignment.input, + output: result.content, + duration: Date.now() - stepStart, + usage: result.usage, + timestamp: stepStart, + }); + + if (result.error) { + errors.push({ + agentId: assignment.agent.id, + error: result.error, + }); + failed.add(assignment.agent.id); + } else { + completed.add(assignment.agent.id); + } + + return result; + } catch (error) { + const errorMsg = + error instanceof Error ? error.message : String(error); + errors.push({ agentId: assignment.agent.id, error: errorMsg }); + failed.add(assignment.agent.id); + pending.delete(assignment.agent.id); + + steps.push({ + index: stepIndex, + primitive: { + type: "agent", + id: assignment.agent.id, + name: assignment.agent.name, + }, + input: assignment.input, + error: errorMsg, + duration: Date.now() - stepStart, + timestamp: stepStart, + }); + + return null; + } + }); + + await Promise.all(batchPromises); + } + } + + // Combine final outputs + const finalOutputs: string[] = []; + for (const result of context.previousResults.values()) { + if (result.content) { + finalOutputs.push(result.content); + } + } + + return { + success: errors.length === 0, + agentResults: context.previousResults, + steps, + finalOutput: finalOutputs.join("\n\n"), + errors, + duration: Date.now() - startTime, + metadata: { + executionId, + strategy: "custom", + agentsExecuted: completed.size, + agentsFailed: errors.length, + }, + }; + } + + /** + * Update coordinator configuration + */ + updateConfig(config: Partial): void { + this.config = { ...this.config, ...config }; + } + + /** + * Subscribe to coordinator events + */ + on(event: string, handler: (...args: unknown[]) => void): void { + this.emitter.on(event, handler); + } + + /** + * Unsubscribe from coordinator events + */ + off(event: string, handler: (...args: unknown[]) => void): void { + this.emitter.off(event, handler); + } +} diff --git a/src/lib/agent/coordination/index.ts b/src/lib/agent/coordination/index.ts new file mode 100644 index 000000000..b882a85b3 --- /dev/null +++ b/src/lib/agent/coordination/index.ts @@ -0,0 +1,11 @@ +/** + * Agent Coordination Module + * + * Provides coordination and task distribution capabilities for multi-agent networks. + * + * Types for this module live in src/lib/types/agentNetwork.ts and are + * re-exported via the central barrel at src/lib/types/index.ts. + */ + +export { AgentCoordinator } from "./coordinator.js"; +export { TaskDistributor } from "./task-distributor.js"; diff --git a/src/lib/agent/coordination/task-distributor.ts b/src/lib/agent/coordination/task-distributor.ts new file mode 100644 index 000000000..8d744affd --- /dev/null +++ b/src/lib/agent/coordination/task-distributor.ts @@ -0,0 +1,712 @@ +/** + * Task Distributor - Distributes and balances tasks across agents + * + * The Task Distributor handles: + * - Task decomposition into subtasks + * - Load balancing across available agents + * - Priority-based task queuing + * - Task affinity and skill matching + */ + +import { EventEmitter } from "events"; +import type { Agent } from "../agent.js"; +import { logger } from "../../utils/logger.js"; +import { withTimeout } from "../../utils/async/withTimeout.js"; +import type { + TaskAnalysis, + TaskPriority, + DistributableTask, + DistributionResult, + AgentCapability, + TaskDistributorConfig, + TaskQueueItem, +} from "../../types/index.js"; + +/** + * Priority values for sorting + */ +const PRIORITY_VALUES: Record = { + critical: 5, + high: 4, + normal: 3, + low: 2, + background: 1, +}; + +/** + * Task Distributor - Manages task distribution across agents + */ +export class TaskDistributor { + private agents: Map = new Map(); + private capabilities: Map = new Map(); + private taskQueue: TaskQueueItem[] = []; + private activeResults: Map = new Map(); + private config: TaskDistributorConfig; + private emitter: EventEmitter; + private isProcessing = false; + + constructor(config: TaskDistributorConfig) { + this.config = { + maxQueueSize: 1000, + maxRetries: 3, + retryDelay: 1000, + taskTimeout: 60000, + enableDecomposition: false, + ...config, + }; + this.emitter = new EventEmitter(); + + logger.debug("[TaskDistributor] Created with config", { + strategy: config.strategy, + maxQueueSize: this.config.maxQueueSize, + }); + } + + /** + * Register an agent with capabilities + */ + registerAgent(agent: Agent, capability?: Partial): void { + this.agents.set(agent.id, agent); + + // Extract skills from agent tools if available + const skills = capability?.skills ?? agent.tools ?? []; + + this.capabilities.set(agent.id, { + agentId: agent.id, + skills, + currentLoad: 0, + avgResponseTime: 0, + successRate: 1, + affinityTags: capability?.affinityTags, + }); + + logger.debug(`[TaskDistributor] Registered agent: ${agent.name}`, { + skills: skills.length, + }); + } + + /** + * Unregister an agent + */ + unregisterAgent(agentId: string): void { + this.agents.delete(agentId); + this.capabilities.delete(agentId); + } + + /** + * Update agent capability + */ + updateCapability(agentId: string, update: Partial): void { + const current = this.capabilities.get(agentId); + if (current) { + this.capabilities.set(agentId, { ...current, ...update }); + } + } + + /** + * Submit a task for distribution + */ + async submitTask(task: DistributableTask): Promise { + // Check queue capacity + if ( + this.config.maxQueueSize && + this.taskQueue.length >= this.config.maxQueueSize + ) { + throw new Error("Task queue is full"); + } + + // Initialize result + const result: DistributionResult = { + taskId: task.id, + agentId: "", + distributedAt: Date.now(), + status: "pending", + }; + this.activeResults.set(task.id, result); + + // Create a settlement promise BEFORE queuing so we never miss the event. + // Resolves when the task reaches "completed" or "failed" via the emitter. + const settled = new Promise((resolve) => { + const onComplete = (evt: { taskId: string }) => { + if (evt.taskId === task.id) { + this.emitter.off("task:completed", onComplete); + this.emitter.off("task:failed", onFail); + resolve(this.activeResults.get(task.id) ?? result); + } + }; + const onFail = (evt: { taskId: string }) => { + if (evt.taskId === task.id) { + this.emitter.off("task:completed", onComplete); + this.emitter.off("task:failed", onFail); + resolve(this.activeResults.get(task.id) ?? result); + } + }; + this.emitter.on("task:completed", onComplete); + this.emitter.on("task:failed", onFail); + }); + + // Add to queue + this.taskQueue.push({ + task, + addedAt: Date.now(), + attempts: 0, + }); + + this.emitter.emit("task:submitted", { taskId: task.id }); + + // Trigger processing (no-ops if already running; the finally block will + // re-trigger itself until the queue is drained). + await this.processQueue(); + + // Wait until the task actually finishes (completed or failed). + return settled; + } + + /** + * Submit multiple tasks + */ + async submitTasks(tasks: DistributableTask[]): Promise { + return Promise.all(tasks.map((task) => this.submitTask(task))); + } + + /** + * Decompose a complex task into subtasks + */ + async decomposeTask( + task: DistributableTask, + analysis: TaskAnalysis, + ): Promise { + if (!this.config.enableDecomposition) { + return [task]; + } + + const subtasks: DistributableTask[] = []; + + // Create subtasks based on requirements + for (let i = 0; i < analysis.requirements.length; i++) { + const req = analysis.requirements[i]; + if (req.mandatory) { + subtasks.push({ + id: `${task.id}-subtask-${i}`, + input: `${task.input}\n\nFocus on: ${req.description}`, + priority: task.priority, + requiredSkills: req.type === "tool" ? [req.description] : undefined, + parentTaskId: task.id, + metadata: { + ...task.metadata, + subtaskIndex: i, + requirementType: req.type, + }, + }); + } + } + + // If no subtasks created, return original + if (subtasks.length === 0) { + return [task]; + } + + return subtasks; + } + + /** + * Process the task queue + */ + private async processQueue(): Promise { + if (this.isProcessing || this.taskQueue.length === 0) { + return; + } + + this.isProcessing = true; + + try { + // Sort queue by priority and deadline + this.taskQueue.sort((a, b) => { + // Priority first + const priorityDiff = + PRIORITY_VALUES[b.task.priority] - PRIORITY_VALUES[a.task.priority]; + if (priorityDiff !== 0) { + return priorityDiff; + } + + // Then by deadline + if (a.task.deadline && b.task.deadline) { + return a.task.deadline - b.task.deadline; + } + + // Then by queue time + return a.addedAt - b.addedAt; + }); + + // Process tasks + // Tracks how many items have been rotated to the back of the queue + // without any task being executed. When this equals the queue length + // every waiting item has been seen at least once with no forward + // progress — a deadlock caused by circular or unresolvable dependencies. + let rotatedWithoutProgress = 0; + + while (this.taskQueue.length > 0) { + const item = this.taskQueue[0]; + + // Check dependencies + if (item.task.dependencies && item.task.dependencies.length > 0) { + // Detect failed or missing dependencies first to avoid infinite spinning + const failedDep = item.task.dependencies.find((depId) => { + const depResult = this.activeResults.get(depId); + return depResult?.status === "failed" || !depResult; + }); + if (failedDep) { + this.taskQueue.shift(); + rotatedWithoutProgress = 0; // structural change — reset counter + const result = this.activeResults.get(item.task.id); + if (result) { + result.status = "failed"; + result.error = `Dependency '${failedDep}' failed or not found`; + } + this.emitter.emit("task:failed", { + taskId: item.task.id, + error: `Dependency '${failedDep}' failed or not found`, + }); + continue; + } + + const allDepsComplete = item.task.dependencies.every((depId) => { + const depResult = this.activeResults.get(depId); + return depResult?.status === "completed"; + }); + + if (!allDepsComplete) { + // Move to end of queue and continue + this.taskQueue.shift(); + this.taskQueue.push(item); + rotatedWithoutProgress++; + + // Full rotation with no progress — circular/unresolvable dependency + if (rotatedWithoutProgress >= this.taskQueue.length) { + this.failAllQueuedTasks( + "Circular or unresolvable dependency detected", + ); + break; + } + continue; + } + } + + // Select agent based on strategy + const agent = await this.selectAgent(item.task); + if (!agent) { + // No suitable agent found + if (item.attempts < (this.config.maxRetries ?? 3)) { + item.attempts++; + await this.delay(this.config.retryDelay ?? 1000); + continue; + } + + // Max retries reached + this.taskQueue.shift(); + const result = this.activeResults.get(item.task.id); + if (result) { + result.status = "failed"; + result.error = "No suitable agent found"; + } + this.emitter.emit("task:failed", { + taskId: item.task.id, + error: "No suitable agent found", + }); + continue; + } + + // Remove from queue and execute + this.taskQueue.shift(); + rotatedWithoutProgress = 0; // reset: we made forward progress + await this.executeTask(item.task, agent); + } + } finally { + this.isProcessing = false; + // Re-process any tasks that were queued while this pass was running. + // Use a zero-delay tick to avoid unbounded call-stack growth. + if (this.taskQueue.length > 0) { + await new Promise((resolve) => setTimeout(resolve, 0)); + await this.processQueue(); + } + } + } + + /** + * Select an agent based on strategy + */ + private async selectAgent( + task: DistributableTask, + ): Promise { + const availableAgents = Array.from(this.agents.values()); + if (availableAgents.length === 0) { + return undefined; + } + + switch (this.config.strategy) { + case "skillBased": + return this.selectBySkill(task, availableAgents); + case "loadBalanced": + return this.selectByLoad(availableAgents); + case "priority": + return this.selectByPriority(task, availableAgents); + case "affinity": + return this.selectByAffinity(task, availableAgents); + case "broadcast": + // For broadcast, return first agent (broadcast handled separately) + return availableAgents[0]; + default: + return availableAgents[0]; + } + } + + /** + * Select agent by skill match + */ + private selectBySkill( + task: DistributableTask, + agents: Agent[], + ): Agent | undefined { + if (!task.requiredSkills || task.requiredSkills.length === 0) { + return agents[0]; + } + + let bestAgent: Agent | undefined; + let bestScore = 0; + + for (const agent of agents) { + const capability = this.capabilities.get(agent.id); + if (!capability) { + continue; + } + + // Calculate skill match score + let score = 0; + if (this.config.skillMatcher) { + score = this.config.skillMatcher(task, agent); + } else { + // Default skill matching + for (const requiredSkill of task.requiredSkills) { + const hasSkill = capability.skills.some( + (s) => + s.toLowerCase().includes(requiredSkill.toLowerCase()) || + requiredSkill.toLowerCase().includes(s.toLowerCase()), + ); + if (hasSkill) { + score++; + } + } + } + + // Factor in success rate + score *= capability.successRate; + + // Factor in load (prefer less busy) + score *= 1 - capability.currentLoad; + + if (score > bestScore) { + bestScore = score; + bestAgent = agent; + } + } + + return bestAgent ?? agents[0]; + } + + /** + * Select agent by load + */ + private selectByLoad(agents: Agent[]): Agent | undefined { + let leastLoadedAgent: Agent | undefined; + let lowestLoad = Infinity; + + for (const agent of agents) { + const capability = this.capabilities.get(agent.id); + if (capability && capability.currentLoad < lowestLoad) { + lowestLoad = capability.currentLoad; + leastLoadedAgent = agent; + } + } + + return leastLoadedAgent ?? agents[0]; + } + + /** + * Select agent by priority matching + */ + private selectByPriority( + task: DistributableTask, + agents: Agent[], + ): Agent | undefined { + // For critical/high priority, prefer agents with best success rate + if (task.priority === "critical" || task.priority === "high") { + let bestAgent: Agent | undefined; + let bestRate = 0; + + for (const agent of agents) { + const capability = this.capabilities.get(agent.id); + if (capability && capability.successRate > bestRate) { + bestRate = capability.successRate; + bestAgent = agent; + } + } + + return bestAgent ?? agents[0]; + } + + // For low/background priority, prefer agents with lowest load + return this.selectByLoad(agents); + } + + /** + * Select agent by affinity + */ + private selectByAffinity( + task: DistributableTask, + agents: Agent[], + ): Agent | undefined { + // Check preferred agent first + if (task.preferredAgent) { + const preferred = this.agents.get(task.preferredAgent); + if (preferred) { + return preferred; + } + } + + // Check affinity tags + if (task.metadata?.affinityTags) { + const taskTags = task.metadata.affinityTags as string[]; + + for (const agent of agents) { + const capability = this.capabilities.get(agent.id); + if (capability?.affinityTags) { + const hasMatch = taskTags.some((tag) => + capability.affinityTags!.includes(tag), + ); + if (hasMatch) { + return agent; + } + } + } + } + + // Fall back to load-based selection + return this.selectByLoad(agents); + } + + /** + * Execute a task on an agent + */ + private async executeTask( + task: DistributableTask, + agent: Agent, + ): Promise { + const result = this.activeResults.get(task.id); + if (!result) { + return; + } + + result.agentId = agent.id; + result.status = "running"; + + // Update agent load + const capability = this.capabilities.get(agent.id); + if (capability) { + capability.currentLoad = Math.min(1, capability.currentLoad + 0.2); + } + + this.emitter.emit("task:started", { taskId: task.id, agentId: agent.id }); + + const startTime = Date.now(); + + try { + // Execute with timeout + const agentResult = await withTimeout( + agent.execute(task.input, { + context: task.metadata, + timeout: task.deadline + ? task.deadline - Date.now() + : this.config.taskTimeout, + }), + this.config.taskTimeout ?? 60000, + "Task execution timeout", + ); + + result.result = agentResult; + result.completedAt = Date.now(); + result.status = agentResult.status === "success" ? "completed" : "failed"; + result.error = agentResult.error; + + // Update capability stats + if (capability) { + const duration = Date.now() - startTime; + capability.avgResponseTime = + (capability.avgResponseTime + duration) / 2; + if (agentResult.status === "success") { + capability.successRate = capability.successRate * 0.9 + 1 * 0.1; // Weighted average + } else { + capability.successRate = capability.successRate * 0.9; // Decay on failure + } + } + + this.emitter.emit("task:completed", { + taskId: task.id, + agentId: agent.id, + status: result.status, + }); + } catch (error) { + result.status = "failed"; + result.error = error instanceof Error ? error.message : String(error); + result.completedAt = Date.now(); + + // Update capability stats + if (capability) { + capability.successRate = capability.successRate * 0.9; + } + + this.emitter.emit("task:failed", { + taskId: task.id, + agentId: agent.id, + error: result.error, + }); + } finally { + // Reduce agent load + if (capability) { + capability.currentLoad = Math.max(0, capability.currentLoad - 0.2); + } + } + } + + /** + * Broadcast a task to all agents + */ + async broadcastTask( + task: DistributableTask, + ): Promise> { + const results = new Map(); + const agents = Array.from(this.agents.values()); + + const promises = agents.map(async (agent) => { + const taskCopy: DistributableTask = { + ...task, + id: `${task.id}-${agent.id}`, + }; + + const result: DistributionResult = { + taskId: taskCopy.id, + agentId: agent.id, + distributedAt: Date.now(), + status: "running", + }; + + try { + const agentResult = await agent.execute(taskCopy.input); + result.result = agentResult; + result.status = + agentResult.status === "success" ? "completed" : "failed"; + result.completedAt = Date.now(); + } catch (error) { + result.status = "failed"; + result.error = error instanceof Error ? error.message : String(error); + result.completedAt = Date.now(); + } + + results.set(agent.id, result); + }); + + await Promise.all(promises); + return results; + } + + /** + * Get task result + */ + getTaskResult(taskId: string): DistributionResult | undefined { + return this.activeResults.get(taskId); + } + + /** + * Get queue status + */ + getQueueStatus(): { + pending: number; + active: number; + completed: number; + failed: number; + } { + let completed = 0; + let failed = 0; + let active = 0; + + for (const result of this.activeResults.values()) { + switch (result.status) { + case "completed": + completed++; + break; + case "failed": + failed++; + break; + case "running": + active++; + break; + } + } + + return { + pending: this.taskQueue.length, + active, + completed, + failed, + }; + } + + /** + * Clear completed/failed tasks + */ + clearCompleted(): void { + for (const [taskId, result] of this.activeResults) { + if (result.status === "completed" || result.status === "failed") { + this.activeResults.delete(taskId); + } + } + } + + /** + * Fail all queued tasks (used on deadlock detection) + */ + private failAllQueuedTasks(reason: string): void { + while (this.taskQueue.length > 0) { + const stuck = this.taskQueue.shift()!; + const stuckResult = this.activeResults.get(stuck.task.id); + if (stuckResult) { + stuckResult.status = "failed"; + stuckResult.error = reason; + } + this.emitter.emit("task:failed", { + taskId: stuck.task.id, + error: reason, + }); + } + } + + /** + * Helper delay function + */ + private delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + /** + * Subscribe to distributor events + */ + on(event: string, handler: (...args: unknown[]) => void): void { + this.emitter.on(event, handler); + } + + /** + * Unsubscribe from distributor events + */ + off(event: string, handler: (...args: unknown[]) => void): void { + this.emitter.off(event, handler); + } +} diff --git a/src/lib/agent/index.ts b/src/lib/agent/index.ts new file mode 100644 index 000000000..a4c8591b0 --- /dev/null +++ b/src/lib/agent/index.ts @@ -0,0 +1,64 @@ +/** + * Agent Module - Multi-Agent Networks for NeuroLink + * + * This module provides multi-agent orchestration capabilities: + * - Agent: Individual agent with specialized instructions and tools + * - AgentNetwork: Multi-agent orchestration using agents-as-tools pattern + * + * Types for this module live in src/lib/types/agentNetwork.ts and are + * re-exported via the central barrel at src/lib/types/index.ts. + * + * @example Basic Usage + * ```typescript + * import { Agent, AgentNetwork } from '@juspay/neurolink'; + * + * const neurolink = new NeuroLink(); + * + * // Create a network with multiple agents + * const network = neurolink.createNetwork({ + * name: 'Content Team', + * agents: [ + * { id: 'researcher', name: 'Researcher', description: '...', instructions: '...' }, + * { id: 'writer', name: 'Writer', description: '...', instructions: '...' } + * ] + * }); + * + * // Execute the network + * const result = await network.execute({ + * message: 'Write an article about AI trends' + * }); + * ``` + */ + +// Core agent classes +export { Agent } from "./agent.js"; +export { AgentNetwork } from "./agentNetwork.js"; + +// Direct tools (existing) +export { + directAgentTools, + getAvailableToolNames, + getToolsForCategory, +} from "./directTools.js"; + +// Routing prompts and utilities +export { + buildConfidencePrompt, + buildMultiStepPlanningPrompt, + buildRoutingPrompt, + parseRoutingResponse, + ROUTING_PROMPTS, +} from "./prompts/routingPrompts.js"; + +// Coordination module +export { AgentCoordinator, TaskDistributor } from "./coordination/index.js"; + +// Communication module +export { MessageBus } from "./communication/index.js"; + +// Orchestration module +export { + NetworkOrchestrator, + NetworkTopology, + TopologyBuilder, +} from "./orchestration/index.js"; diff --git a/src/lib/agent/orchestration/index.ts b/src/lib/agent/orchestration/index.ts new file mode 100644 index 000000000..f1d1c9558 --- /dev/null +++ b/src/lib/agent/orchestration/index.ts @@ -0,0 +1,11 @@ +/** + * Network Orchestration Module + * + * Provides high-level orchestration capabilities for agent networks. + * + * Types for this module live in src/lib/types/agentNetwork.ts and are + * re-exported via the central barrel at src/lib/types/index.ts. + */ + +export { NetworkOrchestrator } from "./orchestrator.js"; +export { NetworkTopology, TopologyBuilder } from "./topology.js"; diff --git a/src/lib/agent/orchestration/orchestrator.ts b/src/lib/agent/orchestration/orchestrator.ts new file mode 100644 index 000000000..ebd39bb9f --- /dev/null +++ b/src/lib/agent/orchestration/orchestrator.ts @@ -0,0 +1,641 @@ +/** + * Network Orchestrator - High-level orchestration of agent networks + * + * The Orchestrator manages: + * - Network lifecycle (creation, execution, shutdown) + * - Multi-network coordination + * - Network-level policies and constraints + * - Resource management and scaling + */ + +import { EventEmitter } from "events"; +import { randomUUID } from "crypto"; +import type { NeuroLink } from "../../neurolink.js"; +import type { AgentNetwork } from "../agentNetwork.js"; +import { AgentCoordinator } from "../coordination/coordinator.js"; +import { MessageBus } from "../communication/message-bus.js"; +import { logger } from "../../utils/logger.js"; +import type { + AgentNetworkConfig, + HierarchicalNetworkConfig, + NetworkExecutionInput, + NetworkExecutionOptions, + NetworkExecutionResult, + NetworkStreamChunk, + HierarchicalExecutionTrace, + CoordinationStrategy, + OrchestrationMode, + NetworkState, + NetworkInfo, + OrchestratorConfig, + ExecutionRequest, +} from "../../types/index.js"; + +/** + * Network Orchestrator - Central controller for agent networks + */ +export class NetworkOrchestrator { + private neurolink: NeuroLink; + private networks: Map = new Map(); + private networkInfo: Map = new Map(); + private coordinators: Map = new Map(); + private messageBus: MessageBus; + private config: Required; + private emitter: EventEmitter; + private executionQueue: ExecutionRequest[] = []; + private activeExecutions: Map> = + new Map(); + + constructor(neurolink: NeuroLink, config?: OrchestratorConfig) { + this.neurolink = neurolink; + this.config = { + defaultMode: "autonomous", + maxConcurrentExecutions: 5, + defaultTimeout: 120000, + enableHierarchy: true, + maxHierarchyDepth: 3, + enableSharedMessageBus: true, + resourceLimits: { + maxNetworks: 10, + maxAgentsPerNetwork: 20, + maxTotalAgents: 100, + }, + ...config, + }; + this.emitter = new EventEmitter(); + this.messageBus = new MessageBus(); + + logger.info("[NetworkOrchestrator] Initialized", { + maxConcurrentExecutions: this.config.maxConcurrentExecutions, + enableHierarchy: this.config.enableHierarchy, + }); + } + + /** + * Create a new agent network + */ + async createNetwork( + config: AgentNetworkConfig, + mode?: OrchestrationMode, + ): Promise { + // Check resource limits + if ( + this.config.resourceLimits?.maxNetworks && + this.networks.size >= this.config.resourceLimits.maxNetworks + ) { + throw new Error("Maximum number of networks reached"); + } + + if ( + this.config.resourceLimits?.maxAgentsPerNetwork && + config.agents.length > this.config.resourceLimits.maxAgentsPerNetwork + ) { + throw new Error( + `Maximum agents per network (${this.config.resourceLimits.maxAgentsPerNetwork}) exceeded`, + ); + } + + if (this.config.resourceLimits?.maxTotalAgents) { + const currentTotalAgents = Array.from(this.networkInfo.values()).reduce( + (sum, info) => sum + info.agentCount, + 0, + ); + if ( + currentTotalAgents + config.agents.length > + this.config.resourceLimits.maxTotalAgents + ) { + throw new Error( + `Maximum total agents (${this.config.resourceLimits.maxTotalAgents}) exceeded`, + ); + } + } + + // Create the network via NeuroLink + const network = await this.neurolink.createNetwork(config); + + // Track network info + const info: NetworkInfo = { + id: network.id, + name: network.name, + state: "ready", + agentCount: config.agents.length, + mode: mode ?? this.config.defaultMode, + createdAt: Date.now(), + executionCount: 0, + childNetworkIds: [], + }; + this.networkInfo.set(network.id, info); + this.networks.set(network.id, network); + + // Create coordinator for the network + const coordinator = new AgentCoordinator({ + strategy: "sequential", + maxConcurrency: 3, + }); + for (const agent of network.getAllAgents()) { + coordinator.registerAgent(agent); + } + this.coordinators.set(network.id, coordinator); + + this.emitter.emit("network:created", { + networkId: network.id, + name: network.name, + }); + + logger.info(`[NetworkOrchestrator] Network created: ${network.name}`, { + networkId: network.id, + agentCount: config.agents.length, + }); + + return network; + } + + /** + * Create a hierarchical network + */ + async createHierarchicalNetwork( + config: HierarchicalNetworkConfig, + parentNetworkId?: string, + ): Promise { + if (!this.config.enableHierarchy) { + throw new Error("Hierarchical networks are disabled"); + } + + // Check hierarchy constraints + if (parentNetworkId) { + const parentInfo = this.networkInfo.get(parentNetworkId); + if (!parentInfo) { + throw new Error(`Parent network not found: ${parentNetworkId}`); + } + + // Check depth + let depth = 0; + let currentParent = parentInfo; + while (currentParent.parentNetworkId) { + depth++; + currentParent = this.networkInfo.get(currentParent.parentNetworkId)!; + if (depth >= this.config.maxHierarchyDepth) { + throw new Error( + `Maximum hierarchy depth (${this.config.maxHierarchyDepth}) exceeded`, + ); + } + } + } + + const network = await this.createNetwork( + config, + config.supervisionMode ?? "hierarchical", + ); + + // Set up hierarchy relationships + if (parentNetworkId) { + const info = this.networkInfo.get(network.id)!; + info.parentNetworkId = parentNetworkId; + + const parentInfo = this.networkInfo.get(parentNetworkId)!; + parentInfo.childNetworkIds.push(network.id); + } + + return network; + } + + /** + * Get a network by ID + */ + getNetwork(networkId: string): AgentNetwork | undefined { + return this.networks.get(networkId); + } + + /** + * Get network info + */ + getNetworkInfo(networkId: string): NetworkInfo | undefined { + return this.networkInfo.get(networkId); + } + + /** + * Get all networks + */ + getAllNetworks(): NetworkInfo[] { + return Array.from(this.networkInfo.values()); + } + + /** + * Execute a network + */ + async executeNetwork( + networkId: string, + input: NetworkExecutionInput, + options?: NetworkExecutionOptions, + ): Promise { + const network = this.networks.get(networkId); + if (!network) { + throw new Error(`Network not found: ${networkId}`); + } + + const info = this.networkInfo.get(networkId)!; + if (info.state === "paused") { + throw new Error(`Network is paused: ${networkId}`); + } + if (info.state === "shutdown") { + throw new Error(`Network is shut down: ${networkId}`); + } + if (info.state === "executing") { + // Queue the execution + return this.queueExecution({ networkId, input, options }); + } + + // Check concurrent execution limit + if (this.activeExecutions.size >= this.config.maxConcurrentExecutions) { + return this.queueExecution({ networkId, input, options }); + } + + return this.executeNetworkInternal(network, info, input, options); + } + + /** + * Internal network execution + */ + private async executeNetworkInternal( + network: AgentNetwork, + info: NetworkInfo, + input: NetworkExecutionInput, + options?: NetworkExecutionOptions, + ): Promise { + info.state = "executing"; + info.executionCount++; + + const executionPromise = (async () => { + try { + this.emitter.emit("network:execution:start", { + networkId: network.id, + input, + }); + + const result = await network.execute(input, { + ...options, + timeout: options?.timeout ?? this.config.defaultTimeout, + }); + + info.lastExecutionAt = Date.now(); + info.state = "ready"; + + this.emitter.emit("network:execution:complete", { + networkId: network.id, + result, + }); + + return result; + } catch (error) { + info.state = "error"; + this.emitter.emit("network:execution:error", { + networkId: network.id, + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } finally { + this.activeExecutions.delete(network.id); + this.processExecutionQueue(); + } + })(); + + this.activeExecutions.set(network.id, executionPromise); + return executionPromise; + } + + /** + * Queue an execution request + */ + private async queueExecution( + request: ExecutionRequest, + ): Promise { + return new Promise((resolve, reject) => { + const _queuedRequest = { + ...request, + resolve, + reject, + }; + this.executionQueue.push(_queuedRequest); + this.executionQueue.sort((a, b) => { + const priorityOrder = { high: 0, normal: 1, low: 2 }; + return ( + (priorityOrder[a.priority ?? "normal"] ?? 1) - + (priorityOrder[b.priority ?? "normal"] ?? 1) + ); + }); + + this.emitter.emit("execution:queued", { networkId: request.networkId }); + }); + } + + /** + * Process queued executions + */ + private async processExecutionQueue(): Promise { + while ( + this.executionQueue.length > 0 && + this.activeExecutions.size < this.config.maxConcurrentExecutions + ) { + const request = this.executionQueue.shift()! as ExecutionRequest & { + resolve?: (result: NetworkExecutionResult) => void; + reject?: (err: unknown) => void; + }; + const network = this.networks.get(request.networkId); + const info = this.networkInfo.get(request.networkId); + + if (network && info && info.state !== "executing") { + // Fire-and-forget: executeNetworkInternal is NOT awaited so the while + // loop drains the entire queue synchronously before any launched + // execution sets info.state = "executing" (that happens at the first + // await inside executeNetworkInternal). This is intentional — it lets + // one queue-drain pass start multiple networks in parallel up to + // maxConcurrentExecutions. + this.executeNetworkInternal( + network, + info, + request.input, + request.options, + ) + .then((result) => request.resolve?.(result)) + .catch((err) => request.reject?.(err)); + } else if (request.reject) { + request.reject( + new Error(`Network unavailable for execution: ${request.networkId}`), + ); + } + } + } + + /** + * Stream network execution + */ + async *streamNetwork( + networkId: string, + input: NetworkExecutionInput, + options?: NetworkExecutionOptions, + ): AsyncIterable { + const network = this.networks.get(networkId); + if (!network) { + throw new Error(`Network not found: ${networkId}`); + } + + const info = this.networkInfo.get(networkId)!; + if (info.state === "paused") { + throw new Error(`Network is paused: ${networkId}`); + } + if (info.state === "shutdown") { + throw new Error(`Network is shut down: ${networkId}`); + } + if (info.state === "executing") { + throw new Error( + `Network is already executing: ${networkId}. Streaming does not support queuing.`, + ); + } + if (this.activeExecutions.size >= this.config.maxConcurrentExecutions) { + throw new Error( + `Maximum concurrent executions (${this.config.maxConcurrentExecutions}) reached. Streaming does not support queuing.`, + ); + } + + info.state = "executing"; + info.executionCount++; + + const streamPromise = + Promise.resolve() as unknown as Promise; // placeholder to track in activeExecutions + this.activeExecutions.set(networkId, streamPromise); + + try { + yield* network.stream(input, options); + info.lastExecutionAt = Date.now(); + } finally { + info.state = "ready"; + this.activeExecutions.delete(networkId); + this.processExecutionQueue(); + } + } + + /** + * Execute hierarchical network with delegation + */ + async executeHierarchical( + networkId: string, + input: NetworkExecutionInput, + options?: NetworkExecutionOptions, + ): Promise { + const network = this.networks.get(networkId); + const info = this.networkInfo.get(networkId); + if (!network || !info) { + throw new Error(`Network not found: ${networkId}`); + } + + const traceId = randomUUID(); + const trace: HierarchicalExecutionTrace = { + traceId, + steps: [], + routingDecisions: [], + startTime: Date.now(), + hierarchyLevel: 0, + childTraces: [], + }; + + // Calculate hierarchy level + let currentInfo = info; + while (currentInfo.parentNetworkId) { + trace.hierarchyLevel++; + trace.parentTraceId = currentInfo.parentNetworkId; + currentInfo = this.networkInfo.get(currentInfo.parentNetworkId)!; + } + + // Execute main network + const result = await this.executeNetwork(networkId, input, options); + trace.steps = result.trace.steps; + trace.routingDecisions = result.trace.routingDecisions; + trace.endTime = Date.now(); + + // Execute child networks if needed based on delegation rules + for (const childId of info.childNetworkIds) { + const childNetwork = this.networks.get(childId); + if (childNetwork) { + const childTrace = await this.executeHierarchical( + childId, + { message: result.content, context: input.context }, + options, + ); + trace.childTraces?.push(childTrace); + } + } + + return trace; + } + + /** + * Pause a network + */ + pauseNetwork(networkId: string): void { + const info = this.networkInfo.get(networkId); + if (info && info.state === "ready") { + info.state = "paused"; + this.emitter.emit("network:paused", { networkId }); + } + } + + /** + * Resume a network + */ + resumeNetwork(networkId: string): void { + const info = this.networkInfo.get(networkId); + if (info && info.state === "paused") { + info.state = "ready"; + this.emitter.emit("network:resumed", { networkId }); + } + } + + /** + * Shutdown a network + */ + async shutdownNetwork(networkId: string): Promise { + const info = this.networkInfo.get(networkId); + if (!info) { + return; + } + + // Shutdown child networks first + for (const childId of info.childNetworkIds) { + await this.shutdownNetwork(childId); + } + + // Remove from parent's child list + if (info.parentNetworkId) { + const parentInfo = this.networkInfo.get(info.parentNetworkId); + if (parentInfo) { + parentInfo.childNetworkIds = parentInfo.childNetworkIds.filter( + (id) => id !== networkId, + ); + } + } + + info.state = "shutdown"; + this.networks.delete(networkId); + this.networkInfo.delete(networkId); + this.coordinators.delete(networkId); + + this.emitter.emit("network:shutdown", { networkId }); + + logger.info(`[NetworkOrchestrator] Network shutdown: ${networkId}`); + } + + /** + * Coordinate multiple networks + */ + async coordinateNetworks( + networkIds: string[], + task: string, + strategy: CoordinationStrategy = "parallel", + ): Promise> { + const results = new Map(); + + switch (strategy) { + case "sequential": + case "pipeline": { + // Each network receives the previous network's output as its input + let currentInput = task; + for (const networkId of networkIds) { + const result = await this.executeNetwork(networkId, { + message: currentInput, + }); + results.set(networkId, result); + currentInput = result.content; + } + break; + } + + case "parallel": { + await Promise.all( + networkIds.map(async (networkId) => { + const result = await this.executeNetwork(networkId, { + message: task, + }); + results.set(networkId, result); + }), + ); + break; + } + + default: + throw new Error(`Unsupported coordination strategy: ${strategy}`); + } + + return results; + } + + /** + * Get orchestrator statistics + */ + getStats(): { + totalNetworks: number; + activeExecutions: number; + queuedExecutions: number; + totalExecutions: number; + networksByState: Record; + } { + const networksByState: Record = { + idle: 0, + initializing: 0, + ready: 0, + executing: 0, + paused: 0, + error: 0, + shutdown: 0, + }; + + let totalExecutions = 0; + for (const info of this.networkInfo.values()) { + networksByState[info.state]++; + totalExecutions += info.executionCount; + } + + return { + totalNetworks: this.networks.size, + activeExecutions: this.activeExecutions.size, + queuedExecutions: this.executionQueue.length, + totalExecutions, + networksByState, + }; + } + + /** + * Get the shared message bus + */ + getMessageBus(): MessageBus { + return this.messageBus; + } + + /** + * Subscribe to orchestrator events + */ + on(event: string, handler: (...args: unknown[]) => void): void { + this.emitter.on(event, handler); + } + + /** + * Unsubscribe from orchestrator events + */ + off(event: string, handler: (...args: unknown[]) => void): void { + this.emitter.off(event, handler); + } + + /** + * Shutdown the orchestrator + */ + async shutdown(): Promise { + // Shutdown all networks + for (const networkId of this.networks.keys()) { + await this.shutdownNetwork(networkId); + } + + // Shutdown message bus + this.messageBus.shutdown(); + + logger.info("[NetworkOrchestrator] Shutdown complete"); + } +} diff --git a/src/lib/agent/orchestration/topology.ts b/src/lib/agent/orchestration/topology.ts new file mode 100644 index 000000000..29577064e --- /dev/null +++ b/src/lib/agent/orchestration/topology.ts @@ -0,0 +1,656 @@ +/** + * Network Topology - Defines and manages network structure + * + * Provides different topology patterns for agent networks: + * - Star: Central coordinator with peripheral agents + * - Mesh: All agents can communicate with each other + * - Hierarchical: Tree structure with supervisors and workers + * - Ring: Agents form a circular communication pattern + * - Custom: User-defined topology + */ + +import { randomUUID } from "crypto"; +import type { Agent } from "../agent.js"; +import { logger } from "../../utils/logger.js"; +import type { + TopologyType, + TopologyNode, + TopologyEdge, + TopologyConfig, + TopologyStats, +} from "../../types/index.js"; + +/** + * Network Topology - Manages agent network structure + */ +export class NetworkTopology { + private nodes: Map = new Map(); + private edges: Map = new Map(); + private config: TopologyConfig; + private topologyId: string; + + constructor(config: TopologyConfig) { + this.config = config; + this.topologyId = randomUUID(); + + logger.debug(`[NetworkTopology] Created with type: ${config.type}`); + } + + /** + * Build topology from agents + */ + buildFromAgents(agents: Agent[]): void { + // Clear existing topology + this.nodes.clear(); + this.edges.clear(); + + // Create nodes for all agents + for (const agent of agents) { + this.addNode(agent); + } + + // Build edges based on topology type + switch (this.config.type) { + case "star": + this.buildStarTopology(agents); + break; + case "mesh": + this.buildMeshTopology(agents); + break; + case "hierarchical": + this.buildHierarchicalTopology(agents); + break; + case "ring": + this.buildRingTopology(agents); + break; + case "custom": + this.buildCustomTopology(agents); + break; + } + + logger.info(`[NetworkTopology] Built ${this.config.type} topology`, { + nodes: this.nodes.size, + edges: this.edges.size, + }); + } + + /** + * Add a node to the topology + */ + addNode(agent: Agent, role?: TopologyNode["role"]): TopologyNode { + const node: TopologyNode = { + id: `node-${agent.id}`, + agentId: agent.id, + agentName: agent.name, + role: role ?? "worker", + connections: [], + childIds: [], + }; + + this.nodes.set(node.id, node); + return node; + } + + /** + * Remove a node from the topology + */ + removeNode(nodeId: string): boolean { + const node = this.nodes.get(nodeId); + if (!node) { + return false; + } + + // Remove all edges connected to this node + for (const edgeId of [...this.edges.keys()]) { + const edge = this.edges.get(edgeId)!; + if (edge.sourceId === nodeId || edge.targetId === nodeId) { + this.edges.delete(edgeId); + } + } + + // Remove from other nodes' connections + for (const otherNode of this.nodes.values()) { + otherNode.connections = otherNode.connections.filter( + (id) => id !== nodeId, + ); + otherNode.childIds = otherNode.childIds.filter((id) => id !== nodeId); + if (otherNode.parentId === nodeId) { + otherNode.parentId = undefined; + } + } + + this.nodes.delete(nodeId); + return true; + } + + /** + * Add an edge between nodes + */ + addEdge( + sourceId: string, + targetId: string, + type: TopologyEdge["type"] = "bidirectional", + weight: number = 1, + ): TopologyEdge | undefined { + const sourceNode = this.nodes.get(sourceId); + const targetNode = this.nodes.get(targetId); + + if (!sourceNode || !targetNode) { + logger.warn(`[NetworkTopology] Cannot add edge: node not found`); + return undefined; + } + + const edge: TopologyEdge = { + id: `edge-${sourceId}-${targetId}`, + sourceId, + targetId, + type, + weight, + }; + + this.edges.set(edge.id, edge); + + // Update node connections + if (!sourceNode.connections.includes(targetId)) { + sourceNode.connections.push(targetId); + } + + if ( + type === "bidirectional" && + !targetNode.connections.includes(sourceId) + ) { + targetNode.connections.push(sourceId); + } + + return edge; + } + + /** + * Remove an edge + */ + removeEdge(edgeId: string): boolean { + const edge = this.edges.get(edgeId); + if (!edge) { + return false; + } + + // Update node connections + const sourceNode = this.nodes.get(edge.sourceId); + const targetNode = this.nodes.get(edge.targetId); + + if (sourceNode) { + sourceNode.connections = sourceNode.connections.filter( + (id) => id !== edge.targetId, + ); + } + + if (targetNode && edge.type === "bidirectional") { + targetNode.connections = targetNode.connections.filter( + (id) => id !== edge.sourceId, + ); + } + + this.edges.delete(edgeId); + return true; + } + + /** + * Build star topology (hub and spoke) + */ + private buildStarTopology(agents: Agent[]): void { + if (agents.length === 0) { + return; + } + + // Find or create coordinator + let coordinatorNode: TopologyNode | undefined; + if (this.config.coordinatorId) { + coordinatorNode = Array.from(this.nodes.values()).find( + (n) => n.agentId === this.config.coordinatorId, + ); + } + + if (!coordinatorNode) { + // Use first agent as coordinator + coordinatorNode = Array.from(this.nodes.values())[0]; + } + + coordinatorNode.role = "coordinator"; + + // Connect all other nodes to coordinator + for (const node of this.nodes.values()) { + if (node.id !== coordinatorNode.id) { + node.role = "worker"; + this.addEdge(coordinatorNode.id, node.id); + } + } + } + + /** + * Build mesh topology (fully connected) + */ + private buildMeshTopology(_agents: Agent[]): void { + const nodeArray = Array.from(this.nodes.values()); + + // Connect every node to every other node + for (let i = 0; i < nodeArray.length; i++) { + nodeArray[i].role = "peer"; + for (let j = i + 1; j < nodeArray.length; j++) { + this.addEdge(nodeArray[i].id, nodeArray[j].id); + } + } + } + + /** + * Build hierarchical topology (tree) + */ + private buildHierarchicalTopology(agents: Agent[]): void { + if (agents.length === 0) { + return; + } + + const nodeArray = Array.from(this.nodes.values()); + const maxChildren = this.config.maxChildren ?? 3; + + if (maxChildren < 1) { + throw new Error( + `[NetworkTopology] maxChildren must be >= 1, got ${maxChildren}. ` + + `A value of 0 or less would orphan every non-root node because no ` + + `children can ever be assigned to a parent.`, + ); + } + + // Find or set root + let rootNode: TopologyNode | undefined; + if (this.config.rootId) { + rootNode = nodeArray.find((n) => n.agentId === this.config.rootId); + } + if (!rootNode) { + rootNode = nodeArray[0]; + } + + rootNode.role = "supervisor"; + const assigned = new Set([rootNode.id]); + const queue: TopologyNode[] = [rootNode]; + + let nodeIndex = 1; // Start from second node + + while (queue.length > 0 && nodeIndex < nodeArray.length) { + const parent = queue.shift()!; + let childCount = 0; + + while (childCount < maxChildren && nodeIndex < nodeArray.length) { + const child = nodeArray[nodeIndex]; + if (!assigned.has(child.id)) { + // Set parent-child relationship + child.parentId = parent.id; + parent.childIds.push(child.id); + child.role = + nodeIndex < nodeArray.length - maxChildren + ? "supervisor" + : "worker"; + + // Add edge + this.addEdge(parent.id, child.id, "unidirectional"); + + assigned.add(child.id); + queue.push(child); + childCount++; + } + nodeIndex++; + } + } + } + + /** + * Build ring topology + */ + private buildRingTopology(_agents: Agent[]): void { + const nodeArray = Array.from(this.nodes.values()); + + if (nodeArray.length === 0) { + return; + } + + // Connect each node to the next, and last to first + for (let i = 0; i < nodeArray.length; i++) { + nodeArray[i].role = "peer"; + const nextIndex = (i + 1) % nodeArray.length; + this.addEdge(nodeArray[i].id, nodeArray[nextIndex].id, "unidirectional"); + } + } + + /** + * Build custom topology from configuration + */ + private buildCustomTopology(_agents: Agent[]): void { + if (!this.config.customEdges) { + return; + } + + // Create node lookup by agent ID + const nodeByAgentId = new Map(); + for (const node of this.nodes.values()) { + nodeByAgentId.set(node.agentId, node); + node.role = "peer"; + } + + // Add custom edges + for (const edge of this.config.customEdges) { + const sourceNode = nodeByAgentId.get(edge.source); + const targetNode = nodeByAgentId.get(edge.target); + + if (sourceNode && targetNode) { + this.addEdge( + sourceNode.id, + targetNode.id, + edge.bidirectional !== false ? "bidirectional" : "unidirectional", + ); + } + } + } + + /** + * Get node by ID + */ + getNode(nodeId: string): TopologyNode | undefined { + return this.nodes.get(nodeId); + } + + /** + * Get node by agent ID + */ + getNodeByAgentId(agentId: string): TopologyNode | undefined { + for (const node of this.nodes.values()) { + if (node.agentId === agentId) { + return node; + } + } + return undefined; + } + + /** + * Get all nodes + */ + getAllNodes(): TopologyNode[] { + return Array.from(this.nodes.values()); + } + + /** + * Get all edges + */ + getAllEdges(): TopologyEdge[] { + return Array.from(this.edges.values()); + } + + /** + * Get connected nodes + */ + getConnectedNodes(nodeId: string): TopologyNode[] { + const node = this.nodes.get(nodeId); + if (!node) { + return []; + } + + return node.connections + .map((id) => this.nodes.get(id)) + .filter((n): n is TopologyNode => n !== undefined); + } + + /** + * Find shortest path between two nodes (BFS) + */ + findShortestPath(sourceId: string, targetId: string): string[] | undefined { + if (sourceId === targetId) { + return [sourceId]; + } + + const visited = new Set(); + const queue: Array<{ nodeId: string; path: string[] }> = [ + { nodeId: sourceId, path: [sourceId] }, + ]; + + while (queue.length > 0) { + const { nodeId, path } = queue.shift()!; + if (visited.has(nodeId)) { + continue; + } + visited.add(nodeId); + + const node = this.nodes.get(nodeId); + if (!node) { + continue; + } + + for (const connectedId of node.connections) { + if (connectedId === targetId) { + return [...path, targetId]; + } + + if (!visited.has(connectedId)) { + queue.push({ nodeId: connectedId, path: [...path, connectedId] }); + } + } + } + + return undefined; // No path found + } + + /** + * Check if two nodes are connected (directly or indirectly) + */ + areConnected(sourceId: string, targetId: string): boolean { + return this.findShortestPath(sourceId, targetId) !== undefined; + } + + /** + * Get nodes by role + */ + getNodesByRole(role: TopologyNode["role"]): TopologyNode[] { + return Array.from(this.nodes.values()).filter((n) => n.role === role); + } + + /** + * Get coordinator/root node + */ + getCoordinator(): TopologyNode | undefined { + return ( + Array.from(this.nodes.values()).find((n) => n.role === "coordinator") || + Array.from(this.nodes.values()).find( + (n) => n.role === "supervisor" && !n.parentId, + ) + ); + } + + /** + * Calculate topology statistics + */ + getStats(): TopologyStats { + const nodes = Array.from(this.nodes.values()); + const nodeCount = nodes.length; + const edgeCount = this.edges.size; + + if (nodeCount === 0) { + return { + nodeCount: 0, + edgeCount: 0, + avgConnections: 0, + maxConnections: 0, + minConnections: 0, + diameter: 0, + density: 0, + }; + } + + const connectionCounts = nodes.map((n) => n.connections.length); + const maxConnections = Math.max(...connectionCounts); + const minConnections = Math.min(...connectionCounts); + const avgConnections = + connectionCounts.reduce((a, b) => a + b, 0) / nodeCount; + + // Calculate diameter (max shortest path) + let diameter = 0; + for (const source of nodes) { + for (const target of nodes) { + if (source.id !== target.id) { + const path = this.findShortestPath(source.id, target.id); + if (path && path.length - 1 > diameter) { + diameter = path.length - 1; + } + } + } + } + + // Calculate density + const maxPossibleEdges = (nodeCount * (nodeCount - 1)) / 2; + const density = maxPossibleEdges > 0 ? edgeCount / maxPossibleEdges : 0; + + return { + nodeCount, + edgeCount, + avgConnections, + maxConnections, + minConnections, + diameter, + density, + }; + } + + /** + * Export topology as JSON + */ + toJSON(): { + id: string; + type: TopologyType; + nodes: TopologyNode[]; + edges: TopologyEdge[]; + } { + return { + id: this.topologyId, + type: this.config.type, + nodes: Array.from(this.nodes.values()), + edges: Array.from(this.edges.values()), + }; + } + + /** + * Import topology from JSON + */ + fromJSON(data: { + id?: string; + type: TopologyType; + nodes: TopologyNode[]; + edges: TopologyEdge[]; + }): void { + // Restore the original topology id so round-trips preserve identity + if (data.id !== undefined) { + this.topologyId = data.id; + } + + this.config.type = data.type; + this.nodes.clear(); + this.edges.clear(); + + for (const node of data.nodes) { + this.nodes.set(node.id, node); + } + + for (const edge of data.edges) { + this.edges.set(edge.id, edge); + } + } + + /** + * Get topology type + */ + getType(): TopologyType { + return this.config.type; + } + + /** + * Get topology ID + */ + getId(): string { + return this.topologyId; + } +} + +/** + * Topology builder for fluent API + */ +export class TopologyBuilder { + private agents: Agent[] = []; + private config: TopologyConfig; + + constructor(type: TopologyType) { + this.config = { type }; + } + + /** + * Add an agent + */ + addAgent(agent: Agent): TopologyBuilder { + this.agents.push(agent); + return this; + } + + /** + * Add multiple agents + */ + addAgents(agents: Agent[]): TopologyBuilder { + this.agents.push(...agents); + return this; + } + + /** + * Set coordinator (for star topology) + */ + setCoordinator(agentId: string): TopologyBuilder { + this.config.coordinatorId = agentId; + return this; + } + + /** + * Set root (for hierarchical topology) + */ + setRoot(agentId: string): TopologyBuilder { + this.config.rootId = agentId; + return this; + } + + /** + * Set max children (for hierarchical topology) + */ + setMaxChildren(max: number): TopologyBuilder { + this.config.maxChildren = max; + return this; + } + + /** + * Add custom edge + */ + addCustomEdge( + sourceAgentId: string, + targetAgentId: string, + bidirectional: boolean = true, + ): TopologyBuilder { + if (!this.config.customEdges) { + this.config.customEdges = []; + } + this.config.customEdges.push({ + source: sourceAgentId, + target: targetAgentId, + bidirectional, + }); + return this; + } + + /** + * Build the topology + */ + build(): NetworkTopology { + const topology = new NetworkTopology(this.config); + topology.buildFromAgents(this.agents); + return topology; + } +} diff --git a/src/lib/agent/prompts/routingPrompts.ts b/src/lib/agent/prompts/routingPrompts.ts new file mode 100644 index 000000000..9d44cab11 --- /dev/null +++ b/src/lib/agent/prompts/routingPrompts.ts @@ -0,0 +1,257 @@ +/** + * Routing Prompts for Agent Network + * + * These prompts guide the routing agent in selecting the appropriate + * primitive (agent, workflow, or tool) for a given task. + */ + +import type { Primitive, AgentRoutingDecision } from "../../types/index.js"; + +/** + * Prompt templates for routing decisions + */ +export const ROUTING_PROMPTS = { + /** + * System instructions for the routing agent + */ + SYSTEM_INSTRUCTIONS: `You are a task routing agent responsible for analyzing tasks and selecting the best primitive to handle them. + +Your role is to: +1. Analyze the incoming task to understand what needs to be done +2. Review the available primitives (agents, workflows, tools) +3. Select the most appropriate primitive based on capabilities +4. Format the input appropriately for the selected primitive +5. Provide your reasoning and confidence level + +Always respond in valid JSON format.`, + + /** + * Template for task analysis and routing + */ + TASK_ROUTING: `Analyze the following task and select the most appropriate primitive to handle it. + +Available Primitives: +{{PRIMITIVES}} + +Task to route: +{{TASK}} + +Consider: +1. The primitive's description and capabilities +2. The complexity of the task +3. Required tools or skills +4. Expected output format + +Respond in JSON format: +{ + "selectedPrimitive": { + "type": "agent" | "workflow" | "tool", + "id": "", + "name": "" + }, + "confidence": <0.0-1.0>, + "reasoning": "", + "formattedInput": "", + "alternatives": [ + { + "type": "agent" | "workflow" | "tool", + "id": "", + "confidence": <0.0-1.0> + } + ] +}`, + + /** + * Template for confidence evaluation + */ + CONFIDENCE_EVALUATION: `Evaluate the confidence that the selected primitive can successfully complete the task. + +Selected Primitive: +- Type: {{PRIMITIVE_TYPE}} +- Name: {{PRIMITIVE_NAME}} +- Description: {{PRIMITIVE_DESCRIPTION}} + +Task: {{TASK}} + +Rate the confidence from 0.0 to 1.0 based on: +- How well the primitive's capabilities match the task +- The specificity of the primitive's description +- The complexity of the task vs primitive's scope +- Potential edge cases or limitations`, + + /** + * Template for multi-step planning + */ + MULTI_STEP_PLANNING: `Plan the execution of this complex task that may require multiple primitives. + +Task: {{TASK}} + +Available Primitives: +{{PRIMITIVES}} + +Create an execution plan with the following structure: +{ + "isMultiStep": true/false, + "steps": [ + { + "stepNumber": 1, + "primitiveId": "", + "description": "", + "dependsOn": [] + } + ], + "expectedOutcome": "" +}`, +}; + +/** + * Build a routing prompt with primitives and task + * + * @param task - The task to route + * @param primitives - Available primitives + * @param options - Additional options + * @returns The formatted prompt + */ +export function buildRoutingPrompt( + task: string, + primitives: Primitive[], + options?: { + includeAlternatives?: boolean; + maxPrimitivesToShow?: number; + }, +): string { + const { includeAlternatives = true, maxPrimitivesToShow = 20 } = + options ?? {}; + + // Format primitives list + const primitivesDescription = primitives + .slice(0, maxPrimitivesToShow) + .map((p) => { + const tools = + p.type === "agent" && "agent" in p + ? (p.agent as { tools?: string[] }).tools + : undefined; + const toolsInfo = + tools && tools.length > 0 ? `\n Tools: ${tools.join(", ")}` : ""; + return `- [${p.type.toUpperCase()}] ${p.id}: ${p.name}\n Description: ${p.description}${toolsInfo}`; + }) + .join("\n"); + + let prompt = ROUTING_PROMPTS.TASK_ROUTING.replace( + "{{PRIMITIVES}}", + primitivesDescription, + ).replace("{{TASK}}", task); + + if (!includeAlternatives) { + // Remove alternatives section from expected output + prompt = prompt.replace( + `"alternatives": [ + { + "type": "agent" | "workflow" | "tool", + "id": "", + "confidence": <0.0-1.0> + } + ]`, + "", + ); + } + + return prompt; +} + +/** + * Parse routing response from LLM + * + * @param response - The raw LLM response + * @returns Parsed routing decision or null if parsing fails + */ +export function parseRoutingResponse( + response: string, +): Partial | null { + try { + // Try to extract JSON from the response + const jsonMatch = response.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + return null; + } + + const parsed = JSON.parse(jsonMatch[0]); + + // Validate required fields + if ( + !parsed.selectedPrimitive || + !parsed.selectedPrimitive.id || + !parsed.selectedPrimitive.type + ) { + return null; + } + + return { + selectedPrimitive: { + type: parsed.selectedPrimitive.type, + id: parsed.selectedPrimitive.id, + name: parsed.selectedPrimitive.name || parsed.selectedPrimitive.id, + }, + confidence: + typeof parsed.confidence === "number" ? parsed.confidence : 0.8, + reasoning: parsed.reasoning || "No reasoning provided", + formattedInput: parsed.formattedInput, + alternatives: Array.isArray(parsed.alternatives) + ? parsed.alternatives.map( + (alt: { type: string; id: string; confidence?: number }) => ({ + type: alt.type, + id: alt.id, + confidence: + typeof alt.confidence === "number" ? alt.confidence : 0.5, + }), + ) + : undefined, + }; + } catch { + return null; + } +} + +/** + * Build confidence evaluation prompt + * + * @param primitive - The selected primitive + * @param task - The task being evaluated + * @returns The formatted prompt + */ +export function buildConfidencePrompt( + primitive: Primitive, + task: string, +): string { + return ROUTING_PROMPTS.CONFIDENCE_EVALUATION.replace( + "{{PRIMITIVE_TYPE}}", + primitive.type, + ) + .replace("{{PRIMITIVE_NAME}}", primitive.name) + .replace("{{PRIMITIVE_DESCRIPTION}}", primitive.description) + .replace("{{TASK}}", task); +} + +/** + * Build multi-step planning prompt + * + * @param task - The task to plan + * @param primitives - Available primitives + * @returns The formatted prompt + */ +export function buildMultiStepPlanningPrompt( + task: string, + primitives: Primitive[], +): string { + const primitivesDescription = primitives + .map( + (p) => + `- [${p.type.toUpperCase()}] ${p.id}: ${p.name} - ${p.description}`, + ) + .join("\n"); + + return ROUTING_PROMPTS.MULTI_STEP_PLANNING.replace("{{TASK}}", task).replace( + "{{PRIMITIVES}}", + primitivesDescription, + ); +} diff --git a/src/lib/index.ts b/src/lib/index.ts index 9eb3bbf94..eb18fa04b 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1150,3 +1150,22 @@ export { // Server Bridge createAuthValidatorFromProvider, } from "./auth/index.js"; + +// ============================================================================ +// Multi-Agent Orchestration exports +// ============================================================================ +export { Agent } from "./agent/agent.js"; +export { AgentNetwork } from "./agent/agentNetwork.js"; + +// Advanced agent orchestration exports +export { + // Coordination + AgentCoordinator, + TaskDistributor, + // Communication + MessageBus, + // Orchestration + NetworkOrchestrator, + NetworkTopology, + TopologyBuilder, +} from "./agent/index.js"; diff --git a/src/lib/neurolink.ts b/src/lib/neurolink.ts index 62438d39b..edffb9104 100644 --- a/src/lib/neurolink.ts +++ b/src/lib/neurolink.ts @@ -23,6 +23,15 @@ import { EventEmitter } from "events"; import pLimit from "p-limit"; import type { AIProviderName } from "./constants/enums.js"; import { ErrorCategory, ErrorSeverity } from "./constants/enums.js"; +// Multi-agent orchestration type imports +import type { + AgentDefinition, + AgentNetworkConfig, + NetworkExecutionInput, + NetworkExecutionOptions, + NetworkExecutionResult, + NetworkStreamChunk, +} from "./types/index.js"; import { CIRCUIT_BREAKER, CIRCUIT_BREAKER_RESET_MS, @@ -13337,6 +13346,219 @@ Current user's request: ${currentInput}`; return getPreset(presetName); } + // ============================================================================ + // MULTI-AGENT ORCHESTRATION METHODS + // ============================================================================ + + /** + * Create an Agent instance for multi-agent orchestration. + * + * Agents are specialized AI entities with defined instructions, tools, and behavior. + * They can be composed into networks for complex task orchestration. + * + * @param definition - Agent definition specifying behavior and capabilities + * @returns A new Agent instance + * + * @example + * ```typescript + * const researcher = neurolink.createAgent({ + * id: 'researcher', + * name: 'Research Agent', + * description: 'Searches and analyzes information from various sources', + * instructions: 'You are a research assistant. Search thoroughly and cite sources.', + * tools: ['websearchGrounding', 'readFile'], + * model: 'gpt-4o' + * }); + * + * const result = await researcher.execute('Find recent AI breakthroughs'); + * ``` + * + * @see {@link AgentDefinition} for definition options + * @see {@link Agent} for agent methods + * @since 8.38.0 + */ + async createAgent( + definition: AgentDefinition, + ): Promise { + const { Agent } = await import("./agent/agent.js"); + logger.debug("[NeuroLink] Creating agent", { + id: definition.id, + name: definition.name, + tools: definition.tools?.length || 0, + }); + return new Agent(definition, this); + } + + /** + * Create an AgentNetwork for multi-agent orchestration. + * + * Networks coordinate multiple agents, workflows, and tools with intelligent + * LLM-powered routing. The router agent analyzes tasks and delegates to + * the most appropriate primitive. + * + * @param config - Network configuration with agents, workflows, and routing settings + * @returns A new AgentNetwork instance + * + * @example + * ```typescript + * const network = neurolink.createNetwork({ + * name: 'Content Team', + * description: 'Collaborative content creation pipeline', + * agents: [ + * { + * id: 'researcher', + * name: 'Researcher', + * description: 'Finds and verifies information', + * instructions: 'Research topics thoroughly...', + * }, + * { + * id: 'writer', + * name: 'Writer', + * description: 'Creates engaging content', + * instructions: 'Write clear, engaging content...', + * }, + * { + * id: 'editor', + * name: 'Editor', + * description: 'Reviews and improves content', + * instructions: 'Review for clarity and accuracy...', + * } + * ], + * router: { + * model: 'gpt-4o', + * confidenceThreshold: 0.7 + * } + * }); + * + * const result = await network.execute({ + * message: 'Write an article about quantum computing' + * }); + * ``` + * + * @see {@link AgentNetworkConfig} for configuration options + * @see {@link AgentNetwork} for network methods + * @since 8.38.0 + */ + async createNetwork( + config: AgentNetworkConfig, + ): Promise { + const { AgentNetwork } = await import("./agent/agentNetwork.js"); + logger.debug("[NeuroLink] Creating agent network", { + name: config.name, + agentCount: config.agents.length, + workflowCount: config.workflows?.length || 0, + toolCount: config.tools?.length || 0, + }); + return new AgentNetwork(config, this); + } + + /** + * Execute an agent network with the given input. + * + * @param network - The agent network to execute + * @param input - Execution input (message and context) + * @param options - Optional execution options + * @returns Network execution result with content, trace, and usage + * + * @see {@link NetworkExecutionInput} for input options + * @see {@link NetworkExecutionResult} for result structure + * @since 8.38.0 + */ + async executeNetwork( + network: import("./agent/agentNetwork.js").AgentNetwork, + input: NetworkExecutionInput, + options?: NetworkExecutionOptions, + ): Promise { + logger.debug("[NeuroLink] Executing agent network", { + networkId: network.id, + networkName: network.name, + hasContext: !!input.context, + }); + return network.execute(input, options); + } + + /** + * Stream agent network execution with real-time events. + * + * @param network - The agent network to stream + * @param input - Execution input (message and context) + * @param options - Optional execution options + * @returns Async iterable of network stream chunks + * + * @see {@link NetworkStreamChunk} for chunk types + * @since 8.38.0 + */ + async *streamNetwork( + network: import("./agent/agentNetwork.js").AgentNetwork, + input: NetworkExecutionInput, + options?: NetworkExecutionOptions, + ): AsyncIterable { + logger.debug("[NeuroLink] Streaming agent network", { + networkId: network.id, + networkName: network.name, + hasContext: !!input.context, + }); + yield* network.stream(input, options); + } + + // ============================================================================ + // ADVANCED ORCHESTRATION METHODS + // ============================================================================ + + /** + * Create a NetworkOrchestrator for managing multiple agent networks. + * + * @param config - Orchestrator configuration options + * @returns A new NetworkOrchestrator instance + * @since 8.38.0 + */ + async createOrchestrator( + config?: import("./types/index.js").OrchestratorConfig, + ): Promise { + const { NetworkOrchestrator } = + await import("./agent/orchestration/index.js"); + logger.debug("[NeuroLink] Creating network orchestrator", { + maxConcurrentExecutions: config?.maxConcurrentExecutions, + defaultMode: config?.defaultMode, + }); + return new NetworkOrchestrator(this, config); + } + + /** + * Create an AgentCoordinator for managing agent coordination strategies. + * + * @param config - Coordinator configuration options + * @returns A new AgentCoordinator instance + * @since 8.38.0 + */ + async createCoordinator( + config?: import("./types/index.js").CoordinatorConfig, + ): Promise { + const { AgentCoordinator } = await import("./agent/coordination/index.js"); + logger.debug("[NeuroLink] Creating agent coordinator", { + strategy: config?.strategy, + maxConcurrency: config?.maxConcurrency, + }); + return new AgentCoordinator(config); + } + + /** + * Create a MessageBus for inter-agent communication. + * + * @param config - Message bus configuration options + * @returns A new MessageBus instance + * @since 8.38.0 + */ + async createMessageBus( + config?: import("./types/index.js").MessageBusConfig, + ): Promise { + const { MessageBus } = await import("./agent/communication/index.js"); + logger.debug("[NeuroLink] Creating message bus", { + maxHistorySize: config?.maxHistorySize, + }); + return new MessageBus(config); + } + /** * Dispose of all resources and cleanup connections * Call this method when done using the NeuroLink instance to prevent resource leaks diff --git a/src/lib/types/agentNetwork.ts b/src/lib/types/agentNetwork.ts new file mode 100644 index 000000000..7d6cdec1a --- /dev/null +++ b/src/lib/types/agentNetwork.ts @@ -0,0 +1,1657 @@ +/** + * Agent Network Types for Multi-Agent Orchestration + * + * These types define the core abstractions for NeuroLink's multi-agent system, + * enabling intelligent routing, agent collaboration, and hierarchical networks. + */ + +import type { z } from "zod"; +import type { AIProviderName } from "../constants/enums.js"; +import type { TokenUsage } from "./analytics.js"; + +// ============================================================================ +// AGENT DEFINITION TYPES +// ============================================================================ + +/** + * Agent definition for creating agents in the network + */ +export type AgentDefinition = { + /** Unique identifier for the agent */ + id: string; + + /** Human-readable name */ + name: string; + + /** Description of the agent's capabilities (critical for routing) */ + description: string; + + /** System instructions for the agent */ + instructions: string; + + /** Provider to use for this agent */ + provider?: AIProviderName | string; + + /** Model to use for this agent */ + model?: string; + + /** Tools available to this agent (tool names) */ + tools?: string[]; + + /** Input schema for structured agent input */ + inputSchema?: z.ZodSchema; + + /** Output schema for structured agent output */ + outputSchema?: z.ZodSchema; + + /** Maximum number of steps this agent can take (default: 10) */ + maxSteps?: number; + + /** Temperature for generation (default: 0.7) */ + temperature?: number; + + /** Whether this agent can delegate to other agents (default: false) */ + canDelegate?: boolean; + + /** Custom metadata for routing decisions */ + metadata?: Record; + + /** Per-agent credentials override */ + credentials?: Record; +}; + +/** + * Agent input - can be a string or structured data + */ +export type AgentInput = string | Record; + +/** + * Result of agent execution + */ +export type AgentResult = { + /** Generated content */ + content: string; + + /** Structured output if schema was provided */ + object?: unknown; + + /** Token usage for this execution */ + usage?: TokenUsage; + + /** Tools used during execution */ + toolsUsed?: string[]; + + /** Detailed tool execution info */ + toolExecutions?: Array<{ + name: string; + input: Record; + output: unknown; + duration: number; + }>; + + /** Execution duration in milliseconds */ + duration: number; + + /** Execution status */ + status: "success" | "error"; + + /** Error message if status is error */ + error?: string; + + /** Agent ID that produced this result */ + agentId: string; +}; + +/** + * Options for agent execution + */ +export type AgentExecutionOptions = { + /** Additional context for the agent */ + context?: Record; + + /** Override max steps for this execution */ + maxSteps?: number; + + /** Trace ID for observability */ + traceId?: string; + + /** Parent span ID for nested tracing */ + parentSpanId?: string; + + /** Timeout in milliseconds */ + timeout?: number; + + /** Per-execution credentials override */ + credentials?: Record; +}; + +/** + * Agent status information + */ +export type AgentStatus = { + /** Agent ID */ + id: string; + + /** Agent name */ + name: string; + + /** Number of executions */ + executionCount: number; + + /** Last execution time in ms */ + lastExecutionTime?: number; + + /** Whether agent is available */ + available: boolean; +}; + +// ============================================================================ +// NETWORK PRIMITIVE TYPES +// ============================================================================ + +/** + * Types of primitives that can be orchestrated in the network + */ +export type NetworkPrimitiveType = "agent" | "workflow" | "tool"; + +/** + * Base primitive type for all orchestrable components + */ +export type NetworkPrimitive = { + /** Unique identifier */ + id: string; + + /** Type of primitive */ + type: NetworkPrimitiveType; + + /** Human-readable name */ + name: string; + + /** Description for routing decisions */ + description: string; + + /** Input schema for validation */ + inputSchema?: z.ZodSchema; + + /** Output schema for validation */ + outputSchema?: z.ZodSchema; +}; + +/** + * Agent as a network primitive + */ +export type AgentPrimitive = NetworkPrimitive & { + type: "agent"; + /** The agent instance */ + agent: AgentInstance; +}; + +/** + * Workflow definition for network integration + */ +export type NetworkWorkflow = { + /** Execute the workflow with given input */ + execute(input: unknown): Promise<{ output: unknown }>; + + /** Optional streaming support */ + stream?(input: unknown): AsyncIterable; +}; + +/** + * Workflow definition config + */ +export type NetworkWorkflowDefinition = { + id: string; + name: string; + description: string; + inputSchema?: z.ZodSchema; + outputSchema?: z.ZodSchema; + workflow: NetworkWorkflow; +}; + +/** + * Workflow as a network primitive + */ +export type WorkflowPrimitive = NetworkPrimitive & { + type: "workflow"; + /** The workflow instance */ + workflow: NetworkWorkflow; +}; + +/** + * Tool info for network integration + */ +export type NetworkToolInfo = { + name: string; + description?: string; + inputSchema?: unknown; +}; + +/** + * Tool as a network primitive + */ +export type ToolPrimitive = NetworkPrimitive & { + type: "tool"; + /** Tool information */ + tool: NetworkToolInfo; + /** Execute the tool */ + execute: (args: unknown, context?: AgentExecutionContext) => Promise; +}; + +/** + * Union type for all primitives + */ +export type Primitive = AgentPrimitive | WorkflowPrimitive | ToolPrimitive; + +// ============================================================================ +// NETWORK CONFIGURATION TYPES +// ============================================================================ + +/** + * Configuration for creating an agent network + */ +export type AgentNetworkConfig = { + /** Unique identifier for the network (auto-generated if not provided) */ + id?: string; + + /** Human-readable name */ + name: string; + + /** Description of the network's purpose */ + description?: string; + + /** Agents in the network */ + agents: AgentDefinition[]; + + /** Workflows available in the network */ + workflows?: NetworkWorkflowDefinition[]; + + /** Additional tools available to all agents (tool names) */ + tools?: string[]; + + /** Routing agent configuration */ + router?: RouterConfig; + + /** Default execution options */ + defaults?: NetworkDefaults; + + /** Memory configuration for the network */ + memory?: NetworkMemoryConfig; +}; + +/** + * Router configuration + */ +export type RouterConfig = { + /** Provider for the routing agent */ + provider?: AIProviderName | string; + + /** Model for the routing agent */ + model?: string; + + /** Custom routing instructions */ + instructions?: string; + + /** Maximum routing attempts before fallback */ + maxAttempts?: number; + + /** Confidence threshold for routing (0-1) */ + confidenceThreshold?: number; +}; + +/** + * Memory configuration for the network + */ +export type NetworkMemoryConfig = { + /** Enable shared memory across agents */ + shared?: boolean; + + /** Memory provider */ + provider?: "in-memory" | "redis"; + + /** Memory TTL in seconds */ + ttl?: number; + + /** Maximum messages to retain */ + maxMessages?: number; +}; + +/** + * Default execution options for the network + */ +export type NetworkDefaults = { + /** Maximum steps per execution */ + maxSteps?: number; + + /** Timeout in milliseconds */ + timeout?: number; + + /** Default temperature */ + temperature?: number; +}; + +// ============================================================================ +// EXECUTION TYPES +// ============================================================================ + +/** + * Execution context passed to primitives + */ +export type AgentExecutionContext = { + /** Session ID for memory */ + sessionId?: string; + + /** Trace ID for observability */ + traceId?: string; + + /** Parent span ID */ + parentSpanId?: string; + + /** Additional context data */ + [key: string]: unknown; +}; + +/** + * Input for network execution + */ +export type NetworkExecutionInput = { + /** The task or message to process */ + message: string | CoreMessage[]; + + /** Thread ID for conversation context */ + threadId?: string; + + /** User/resource identifier */ + resourceId?: string; + + /** Additional context */ + context?: Record; +}; + +/** + * Core message format (simplified) + */ +export type CoreMessage = { + role: "user" | "assistant" | "system"; + content: string; +}; + +/** + * Options for network execution + */ +export type NetworkExecutionOptions = { + /** Maximum execution steps across the network */ + maxSteps?: number; + + /** Timeout in milliseconds */ + timeout?: number; + + /** Enable streaming */ + stream?: boolean; + + /** Additional context */ + context?: Record; + + /** Tracing configuration */ + tracing?: { + enabled?: boolean; + traceId?: string; + parentSpanId?: string; + }; + + /** Model settings override */ + modelSettings?: { + temperature?: number; + maxTokens?: number; + topP?: number; + }; + + /** Output schema for structured output */ + outputSchema?: z.ZodSchema; +}; + +/** + * Result of network execution + */ +export type NetworkExecutionResult = { + /** Final output content */ + content: string; + + /** Structured output if schema was provided */ + object?: unknown; + + /** Execution trace */ + trace: NetworkExecutionTrace; + + /** Token usage across all agents */ + usage: NetworkTokenUsage; + + /** Execution status */ + status: NetworkExecutionStatus; + + /** Time taken in milliseconds */ + duration: number; + + /** Error message if status is error */ + error?: string; +}; + +/** + * Execution trace for debugging and monitoring + */ +export type NetworkExecutionTrace = { + /** Unique trace ID */ + traceId: string; + + /** Steps taken during execution */ + steps: NetworkExecutionStep[]; + + /** Routing decisions made */ + routingDecisions: AgentRoutingDecision[]; + + /** Start timestamp */ + startTime: number; + + /** End timestamp */ + endTime?: number; +}; + +/** + * Single execution step in the trace + */ +export type NetworkExecutionStep = { + /** Step index */ + index: number; + + /** Primitive that was executed */ + primitive: { + type: NetworkPrimitiveType; + id: string; + name: string; + }; + + /** Input to the primitive */ + input: unknown; + + /** Output from the primitive */ + output?: unknown; + + /** Error if step failed */ + error?: string; + + /** Duration in milliseconds */ + duration: number; + + /** Token usage for this step */ + usage?: TokenUsage; + + /** Timestamp */ + timestamp: number; +}; + +/** + * Routing decision record + */ +export type AgentRoutingDecision = { + /** Step at which decision was made */ + stepIndex: number; + + /** Task description analyzed */ + taskDescription: string; + + /** Selected primitive */ + selectedPrimitive: { + type: NetworkPrimitiveType; + id: string; + name: string; + }; + + /** Confidence score (0-1) */ + confidence: number; + + /** Reasoning for the decision */ + reasoning: string; + + /** Alternative primitives considered */ + alternatives?: Array<{ + type: NetworkPrimitiveType; + id: string; + confidence: number; + }>; + + /** Formatted input for the selected primitive */ + formattedInput?: string; +}; + +/** + * Token usage aggregated across the network + */ +export type NetworkTokenUsage = { + /** Total prompt tokens */ + promptTokens: number; + + /** Total completion tokens */ + completionTokens: number; + + /** Total tokens */ + totalTokens: number; + + /** Breakdown by agent */ + byAgent?: Record< + string, + { + promptTokens: number; + completionTokens: number; + totalTokens: number; + } + >; +}; + +/** + * Execution status enum + */ +export type NetworkExecutionStatus = + | "pending" + | "running" + | "completed" + | "error" + | "suspended"; + +// ============================================================================ +// RESULT TYPES +// ============================================================================ + +/** + * Result from executing a primitive + */ +export type PrimitiveExecutionResult = { + /** Output from the primitive */ + output: unknown; + + /** Error message if execution failed */ + error?: string; + + /** Token usage */ + usage?: TokenUsage; + + /** Execution duration in ms */ + duration?: number; +}; + +// ============================================================================ +// AGENT INTERFACE +// ============================================================================ + +/** + * Interface for agent instances + */ +export type AgentInstance = { + /** Agent ID */ + readonly id: string; + + /** Agent name */ + readonly name: string; + + /** Agent description */ + readonly description: string; + + /** Agent instructions */ + readonly instructions: string; + + /** Execute the agent */ + execute( + input: AgentInput, + options?: AgentExecutionOptions, + ): Promise; + + /** Stream execution results */ + stream( + input: AgentInput, + options?: AgentExecutionOptions, + ): AsyncIterable; + + /** Get agent status */ + getStatus(): AgentStatus; +}; + +// ============================================================================ +// STREAMING TYPES +// ============================================================================ + +/** + * Agent-specific streaming chunk types + */ +export type AgentStreamChunkType = + | "agent-start" + | "agent-thinking" + | "agent-text" + | "agent-tool-call" + | "agent-tool-result" + | "agent-complete" + | "agent-error"; + +/** + * Agent stream chunk + */ +export type AgentStreamChunk = { + /** Chunk type */ + type: AgentStreamChunkType; + + /** Agent ID */ + agentId: string; + + /** Timestamp */ + timestamp: number; + + /** Trace ID */ + traceId: string; + + /** Content (for text chunks) */ + content?: string; + + /** Whether content is partial (for text chunks) */ + isPartial?: boolean; + + /** Token usage (for complete chunks) */ + usage?: TokenUsage; + + /** Duration in ms (for complete chunks) */ + duration?: number; + + /** Error message (for error chunks) */ + error?: string; + + /** Tool name (for tool chunks) */ + toolName?: string; + + /** Tool call ID (for tool chunks) */ + toolCallId?: string; + + /** Tool arguments (for tool call chunks) */ + args?: unknown; + + /** Tool result (for tool result chunks) */ + result?: unknown; + + /** Whether tool succeeded (for tool result chunks) */ + success?: boolean; +}; + +/** + * Network streaming chunk types + */ +export type NetworkStreamChunkType = + | "network-start" + | "routing-start" + | "routing-decision" + | "routing-end" + | "primitive-start" + | "primitive-progress" + | "primitive-end" + | "agent-thinking" + | "agent-text" + | "agent-tool-call" + | "agent-tool-result" + | "workflow-step" + | "network-progress" + | "network-complete" + | "network-error"; + +/** + * Base streaming chunk with common fields + */ +export type NetworkStreamChunkBase = { + /** Chunk type */ + type: NetworkStreamChunkType; + + /** Timestamp */ + timestamp: number; + + /** Trace ID */ + traceId: string; + + /** Current step index */ + stepIndex?: number; +}; + +/** + * Network start event + */ +export type NetworkStartChunk = NetworkStreamChunkBase & { + type: "network-start"; + networkId: string; + input: string; +}; + +/** + * Routing decision event + */ +export type RoutingDecisionChunk = NetworkStreamChunkBase & { + type: "routing-decision"; + decision: AgentRoutingDecision; +}; + +/** + * Primitive start event + */ +export type PrimitiveStartChunk = NetworkStreamChunkBase & { + type: "primitive-start"; + primitive: { + type: NetworkPrimitiveType; + id: string; + name: string; + }; + input: unknown; +}; + +/** + * Primitive end event + */ +export type PrimitiveEndChunk = NetworkStreamChunkBase & { + type: "primitive-end"; + primitive: { + type: NetworkPrimitiveType; + id: string; + name: string; + }; + output: unknown; +}; + +/** + * Agent text generation event + */ +export type AgentTextChunk = NetworkStreamChunkBase & { + type: "agent-text"; + agentId: string; + content: string; + isPartial: boolean; +}; + +/** + * Agent tool call event + */ +export type AgentToolCallChunk = NetworkStreamChunkBase & { + type: "agent-tool-call"; + agentId: string; + toolName: string; + args: unknown; + toolCallId: string; +}; + +/** + * Agent tool result event + */ +export type AgentToolResultChunk = NetworkStreamChunkBase & { + type: "agent-tool-result"; + agentId: string; + toolName: string; + toolCallId: string; + result: unknown; + success: boolean; +}; + +/** + * Network complete event + */ +export type NetworkCompleteChunk = NetworkStreamChunkBase & { + type: "network-complete"; + result: NetworkExecutionResult; +}; + +/** + * Network error event + */ +export type NetworkErrorChunk = NetworkStreamChunkBase & { + type: "network-error"; + error: string; +}; + +/** + * Union type for all streaming chunks + */ +export type NetworkStreamChunk = + | NetworkStartChunk + | RoutingDecisionChunk + | PrimitiveStartChunk + | PrimitiveEndChunk + | AgentTextChunk + | AgentToolCallChunk + | AgentToolResultChunk + | NetworkCompleteChunk + | NetworkErrorChunk; + +// ============================================================================ +// TASK ANALYSIS TYPES +// ============================================================================ + +/** + * Result of task analysis + */ +export type TaskAnalysis = { + /** Identified intent of the task */ + intent: string; + + /** Entities extracted from the task */ + entities: Entity[]; + + /** Requirements for completing the task */ + requirements: Requirement[]; + + /** Task complexity assessment */ + complexity: "simple" | "moderate" | "complex"; + + /** Suggested primitives for handling */ + suggestedPrimitives: string[]; +}; + +/** + * Entity extracted from task + */ +export type Entity = { + /** Entity type */ + type: string; + + /** Entity value */ + value: string; + + /** Confidence score */ + confidence: number; +}; + +/** + * Requirement for task completion + */ +export type Requirement = { + /** Requirement type */ + type: "tool" | "capability" | "data"; + + /** Requirement description */ + description: string; + + /** Whether it's mandatory */ + mandatory: boolean; +}; + +// ============================================================================ +// ROUTING CONTEXT TYPES +// ============================================================================ + +/** + * Context for routing decisions + */ +export type RoutingContext = { + /** Previous routing decisions */ + previousDecisions?: AgentRoutingDecision[]; + + /** Conversation history */ + conversationHistory?: CoreMessage[]; + + /** User preferences */ + userPreferences?: Record; + + /** Session context */ + sessionContext?: Record; +}; + +// ============================================================================ +// HIERARCHICAL NETWORK TYPES +// ============================================================================ + +/** + * Configuration for hierarchical networks + */ +export type HierarchicalNetworkConfig = AgentNetworkConfig & { + /** Maximum nesting depth */ + maxDepth?: number; + + /** Delegation rules for child networks */ + delegationRules?: DelegationRule[]; + + /** Supervision mode */ + supervisionMode?: "autonomous" | "supervised" | "collaborative"; +}; + +/** + * Delegation rule for hierarchical networks + */ +export type DelegationRule = { + /** Rule condition */ + condition: DelegationCondition; + + /** Target network or agent */ + targetNetwork: string; + + /** Priority (higher = checked first) */ + priority: number; +}; + +/** + * Delegation condition types + */ +export type DelegationCondition = + | { type: "keyword"; keywords: string[] } + | { type: "complexity"; threshold: "simple" | "moderate" | "complex" } + | { type: "toolRequired"; tools: string[] } + | { type: "custom"; evaluator: (task: string) => boolean }; + +/** + * Hierarchical execution trace + */ +export type HierarchicalExecutionTrace = NetworkExecutionTrace & { + /** Parent trace ID if this is a child network */ + parentTraceId?: string; + + /** Child traces */ + childTraces?: HierarchicalExecutionTrace[]; + + /** Hierarchy level (0 = root) */ + hierarchyLevel: number; +}; + +// ============================================================================ +// SUPERVISOR TYPES +// ============================================================================ + +/** + * Supervisor agent definition + */ +export type SupervisorAgentDefinition = AgentDefinition & { + /** Supervision policy */ + supervisionPolicy: SupervisionPolicy; +}; + +/** + * Supervision policy configuration + */ +export type SupervisionPolicy = { + /** Confidence below which to review */ + reviewThreshold: number; + + /** Severity above which to escalate */ + escalationThreshold: number; + + /** Maximum retries before escalation */ + maxRetries: number; + + /** Tool names requiring approval */ + requireApprovalFor: string[]; +}; + +/** + * Supervision options + */ +export type SupervisionOptions = { + /** Whether to enforce approval */ + enforceApproval?: boolean; + + /** Timeout for approval */ + approvalTimeout?: number; + + /** Fallback behavior on timeout */ + timeoutBehavior?: "reject" | "approve" | "escalate"; +}; + +/** + * Result of supervised execution + */ +export type SupervisedResult = AgentResult & { + /** Whether approval was required */ + requiredApproval: boolean; + + /** Approval decision */ + approvalDecision?: ReviewDecision; + + /** Escalation info if escalated */ + escalation?: EscalationResult; +}; + +/** + * Review decision by supervisor + */ +export type ReviewDecision = { + /** Whether approved */ + approved: boolean; + + /** Reason for decision */ + reason: string; + + /** Modifications made */ + modifications?: Record; + + /** Timestamp */ + timestamp: number; +}; + +/** + * Result of escalation + */ +export type EscalationResult = { + /** Whether escalation was handled */ + handled: boolean; + + /** Handler that processed escalation */ + handler?: string; + + /** Resolution */ + resolution?: string; + + /** Timestamp */ + timestamp: number; +}; + +// ============================================================================ +// EXTRACTED FROM AGENT FEATURE MODULES (per CLAUDE.md rules 2, 11, 12) +// Types defined here are imported by src/lib/agent/* feature files. +// ============================================================================ + +// ============================================================================ +// From src/lib/agent/communication/message-bus.ts +// ============================================================================ + +/** + * Message types for agent communication + */ +export type MessageType = + | "request" // Request expecting response + | "response" // Response to request + | "broadcast" // One-to-many message + | "direct" // Point-to-point message + | "event" // Event notification + | "command"; // Command/instruction + +/** + * Message priority levels + */ +export type MessagePriority = "high" | "normal" | "low"; + +/** + * Message structure for agent communication + */ +export type AgentMessage = { + /** Unique message ID */ + id: string; + + /** Message type */ + type: MessageType; + + /** Topic/channel for the message */ + topic: string; + + /** Sender agent ID */ + senderId: string; + + /** Recipient agent ID (for direct messages) */ + recipientId?: string; + + /** Message payload */ + payload: unknown; + + /** Correlation ID (for request-response) */ + correlationId?: string; + + /** Reply-to topic (for request-response) */ + replyTo?: string; + + /** Message priority */ + priority: MessagePriority; + + /** Timestamp */ + timestamp: number; + + /** Time-to-live in ms (after which message expires) */ + ttl?: number; + + /** Message metadata */ + metadata?: Record; +}; + +/** + * Message handler function type + */ +export type MessageHandler = (message: AgentMessage) => void | Promise; + +/** + * Subscription options + */ +export type SubscriptionOptions = { + /** Filter messages by sender */ + filterBySender?: string[]; + + /** Filter messages by type */ + filterByType?: MessageType[]; + + /** Filter messages by priority */ + filterByPriority?: MessagePriority[]; + + /** Custom filter function */ + customFilter?: (message: AgentMessage) => boolean; + + /** Maximum messages to receive (-1 for unlimited) */ + maxMessages?: number; +}; + +/** + * Message bus configuration + */ +export type MessageBusConfig = { + /** Maximum messages to retain in history */ + maxHistorySize?: number; + + /** Default message TTL in ms */ + defaultTtl?: number; + + /** Enable message persistence */ + enablePersistence?: boolean; + + /** Dead letter queue for failed messages */ + enableDeadLetterQueue?: boolean; + + /** Request timeout for request-response pattern */ + requestTimeout?: number; +}; + +// ============================================================================ +// From src/lib/agent/communication/protocols.ts +// ============================================================================ + +/** + * Protocol state + */ +export type ProtocolState = + | "initiated" + | "pending" + | "active" + | "completed" + | "failed" + | "timeout"; + +/** + * Aggregation request payload + */ +export type AggregationRequest = { + /** Protocol session ID */ + sessionId: string; + + /** Protocol state */ + state: ProtocolState; + + /** Aggregation data */ + data: { + results: Array<{ agentId: string; result: unknown }>; + aggregationType: "merge" | "summarize" | "vote" | "custom"; + customAggregator?: string; + }; +}; + +// ============================================================================ +// From src/lib/agent/coordination/coordinator.ts +// ============================================================================ + +/** + * Coordination strategy for multi-agent execution + */ +export type CoordinationStrategy = + | "sequential" // Execute agents one after another + | "parallel" // Execute independent agents in parallel + | "pipeline" // Output of one agent feeds into next + | "roundRobin" // Distribute tasks in round-robin fashion + | "leastBusy" // Route to least busy agent + | "custom"; // Custom strategy via callback + +/** + * Configuration for the coordinator + */ +export type CoordinatorConfig = { + /** Coordination strategy to use */ + strategy: CoordinationStrategy; + + /** Maximum concurrent agent executions (for parallel strategy) */ + maxConcurrency?: number; + + /** Timeout for individual agent execution in ms */ + agentTimeout?: number; + + /** Whether to continue on agent failure */ + continueOnFailure?: boolean; + + /** Custom coordination logic (for custom strategy) */ + customCoordinator?: ( + agents: AgentInstance[], + task: string, + context: CoordinationContext, + ) => Promise; + + /** Retry configuration */ + retry?: { + maxRetries: number; + retryDelay: number; + backoffMultiplier?: number; + }; +}; + +/** + * Context passed during coordination + */ +export type CoordinationContext = AgentExecutionContext & { + /** Current execution step */ + currentStep: number; + + /** Total expected steps */ + totalSteps?: number; + + /** Results from previous agents */ + previousResults: Map; + + /** Shared state across agents */ + sharedState: Map; + + /** Coordination metadata */ + metadata: { + startTime: number; + strategy: CoordinationStrategy; + executionId: string; + }; +}; + +/** + * Result of a coordinated execution + */ +export type CoordinationResult = { + /** Whether coordination was successful */ + success: boolean; + + /** Results from all agents */ + agentResults: Map; + + /** Execution steps taken */ + steps: NetworkExecutionStep[]; + + /** Final combined output */ + finalOutput?: string; + + /** Any errors encountered */ + errors: Array<{ agentId: string; error: string }>; + + /** Total duration in ms */ + duration: number; + + /** Execution metadata */ + metadata: { + executionId: string; + strategy: CoordinationStrategy; + agentsExecuted: number; + agentsFailed: number; + }; +}; + +/** + * Task assignment for an agent + */ +export type TaskAssignment = { + /** Agent to execute */ + agent: AgentInstance; + + /** Task input */ + input: string; + + /** Dependencies (agent IDs that must complete first) */ + dependencies?: string[]; + + /** Priority (higher = executed first) */ + priority?: number; + + /** Timeout override */ + timeout?: number; +}; + +// ============================================================================ +// From src/lib/agent/coordination/task-distributor.ts +// ============================================================================ + +/** + * Distribution strategy for tasks + */ +export type DistributionStrategy = + | "skillBased" // Match task to agent skills + | "loadBalanced" // Distribute evenly across agents + | "priority" // Process highest priority first + | "affinity" // Route based on agent affinity + | "broadcast"; // Send to all agents + +/** + * Task priority levels + */ +export type TaskPriority = + | "critical" + | "high" + | "normal" + | "low" + | "background"; + +/** + * Task definition for distribution + */ +export type DistributableTask = { + /** Unique task ID */ + id: string; + + /** Task description/input */ + input: string; + + /** Task priority */ + priority: TaskPriority; + + /** Required skills/capabilities */ + requiredSkills?: string[]; + + /** Preferred agent (for affinity) */ + preferredAgent?: string; + + /** Task metadata */ + metadata?: Record; + + /** Deadline timestamp */ + deadline?: number; + + /** Parent task ID (for subtasks) */ + parentTaskId?: string; + + /** Dependencies (task IDs) */ + dependencies?: string[]; +}; + +/** + * Result of task distribution + */ +export type DistributionResult = { + /** Task ID */ + taskId: string; + + /** Assigned agent ID */ + agentId: string; + + /** Execution result */ + result?: AgentResult; + + /** Distribution timestamp */ + distributedAt: number; + + /** Completion timestamp */ + completedAt?: number; + + /** Status */ + status: "pending" | "running" | "completed" | "failed"; + + /** Error if failed */ + error?: string; +}; + +/** + * Agent capability description + */ +export type AgentCapability = { + /** Agent ID */ + agentId: string; + + /** Skills/capabilities */ + skills: string[]; + + /** Current load (0-1) */ + currentLoad: number; + + /** Average response time in ms */ + avgResponseTime: number; + + /** Success rate (0-1) */ + successRate: number; + + /** Affinity tags */ + affinityTags?: string[]; +}; + +/** + * Task Distributor configuration + */ +export type TaskDistributorConfig = { + /** Distribution strategy */ + strategy: DistributionStrategy; + + /** Maximum queue size */ + maxQueueSize?: number; + + /** Maximum retries per task */ + maxRetries?: number; + + /** Retry delay in ms */ + retryDelay?: number; + + /** Task timeout in ms */ + taskTimeout?: number; + + /** Enable task decomposition */ + enableDecomposition?: boolean; + + /** Custom skill matcher */ + skillMatcher?: (task: DistributableTask, agent: AgentInstance) => number; +}; + +// ============================================================================ +// From src/lib/agent/orchestration/orchestrator.ts +// ============================================================================ + +/** + * Orchestration mode + */ +export type OrchestrationMode = + | "autonomous" // Network operates independently + | "supervised" // Human oversight required + | "collaborative" // Multiple networks work together + | "hierarchical"; // Parent-child network structure + +/** + * Network state + */ +export type NetworkState = + | "idle" + | "initializing" + | "ready" + | "executing" + | "paused" + | "error" + | "shutdown"; + +/** + * Network info + */ +export type NetworkInfo = { + id: string; + name: string; + state: NetworkState; + agentCount: number; + mode: OrchestrationMode; + createdAt: number; + lastExecutionAt?: number; + executionCount: number; + parentNetworkId?: string; + childNetworkIds: string[]; +}; + +/** + * Orchestrator configuration + */ +export type OrchestratorConfig = { + /** Default orchestration mode */ + defaultMode?: OrchestrationMode; + + /** Maximum concurrent network executions */ + maxConcurrentExecutions?: number; + + /** Default execution timeout */ + defaultTimeout?: number; + + /** Enable hierarchical networks */ + enableHierarchy?: boolean; + + /** Maximum hierarchy depth */ + maxHierarchyDepth?: number; + + /** Enable shared message bus */ + enableSharedMessageBus?: boolean; + + /** Resource limits */ + resourceLimits?: { + maxNetworks?: number; + maxAgentsPerNetwork?: number; + maxTotalAgents?: number; + }; +}; + +/** + * Execution request + */ +export type ExecutionRequest = { + networkId: string; + input: NetworkExecutionInput; + options?: NetworkExecutionOptions; + priority?: "high" | "normal" | "low"; +}; + +// ============================================================================ +// From src/lib/agent/orchestration/topology.ts +// ============================================================================ + +/** + * Topology type + */ +export type TopologyType = "star" | "mesh" | "hierarchical" | "ring" | "custom"; + +/** + * Node in the topology + */ +export type TopologyNode = { + /** Unique node ID */ + id: string; + + /** Agent ID (maps to agent) */ + agentId: string; + + /** Agent name */ + agentName: string; + + /** Node role in topology */ + role: "coordinator" | "supervisor" | "worker" | "peer"; + + /** Connected node IDs */ + connections: string[]; + + /** Parent node ID (for hierarchical) */ + parentId?: string; + + /** Child node IDs (for hierarchical) */ + childIds: string[]; + + /** Node metadata */ + metadata?: Record; +}; + +/** + * Edge in the topology + */ +export type TopologyEdge = { + /** Unique edge ID */ + id: string; + + /** Source node ID */ + sourceId: string; + + /** Target node ID */ + targetId: string; + + /** Edge type */ + type: "bidirectional" | "unidirectional"; + + /** Communication weight (for routing optimization) */ + weight: number; + + /** Edge metadata */ + metadata?: Record; +}; + +/** + * Topology configuration + */ +export type TopologyConfig = { + /** Topology type */ + type: TopologyType; + + /** Coordinator agent ID (for star topology) */ + coordinatorId?: string; + + /** Root agent ID (for hierarchical topology) */ + rootId?: string; + + /** Maximum children per node (for hierarchical) */ + maxChildren?: number; + + /** Custom edges (for custom topology) */ + customEdges?: Array<{ + source: string; + target: string; + bidirectional?: boolean; + }>; +}; + +/** + * Topology statistics + */ +export type TopologyStats = { + nodeCount: number; + edgeCount: number; + avgConnections: number; + maxConnections: number; + minConnections: number; + diameter: number; // Maximum shortest path + density: number; // Edge count / max possible edges +}; + +// ============================================================================ +// From src/lib/agent/prompts/routingPrompts.ts +// ============================================================================ + +/** + * Options for routing prompt generation + */ +export type RoutingPromptOptions = { + /** Include alternative primitives in response */ + includeAlternatives?: boolean; + + /** Maximum primitives to include in prompt */ + maxPrimitivesToShow?: number; + + /** Additional context for routing */ + additionalContext?: string; + + /** Conversation history for context */ + conversationHistory?: Array<{ role: string; content: string }>; +}; + +/** + * MessageBus subscription record + */ +export type MessageBusSubscription = { + id: string; + topic: string; + handler: MessageHandler; + options: SubscriptionOptions; + messageCount: number; + subscriberId: string; +}; + +/** + * Task distributor queue item + */ +export type TaskQueueItem = { + task: DistributableTask; + addedAt: number; + attempts: number; +}; diff --git a/src/lib/types/cli.ts b/src/lib/types/cli.ts index 3e4fc733c..8c4faa507 100644 --- a/src/lib/types/cli.ts +++ b/src/lib/types/cli.ts @@ -1792,3 +1792,71 @@ export type CliServeFlatRoute = { description?: string; group: string; }; + +/** + * Agent command arguments for multi-agent orchestration + */ +export type CliAgentCommandArgs = BaseCommandArgs & { + /** Agent ID */ + id?: string; + /** Agent name */ + name?: string; + /** Agent description */ + description?: string; + /** Agent instructions/system prompt */ + instructions?: string; + /** AI provider to use */ + provider?: string; + /** Model name */ + model?: string; + /** Tools available to the agent */ + tools?: string[]; + /** Maximum execution steps */ + maxSteps?: number; + /** Temperature setting */ + temperature?: number; + /** Input prompt for execution */ + input?: string; + /** Context data as JSON */ + context?: string; + /** Agent definition file path */ + file?: string; + /** Output file path */ + output?: string; + /** Enable streaming output */ + stream?: boolean; + /** Show detailed information */ + detailed?: boolean; +}; + +/** + * Network command arguments for agent network orchestration + */ +export type CliNetworkCommandArgs = BaseCommandArgs & { + /** Network ID */ + id?: string; + /** Network name */ + name?: string; + /** Network description */ + description?: string; + /** Network configuration file path */ + file?: string; + /** Input message for execution */ + input?: string; + /** Maximum execution steps */ + maxSteps?: number; + /** Execution timeout in ms */ + timeout?: number; + /** Context data as JSON */ + context?: string; + /** Output file path */ + output?: string; + /** Enable streaming output */ + stream?: boolean; + /** Router provider */ + routerProvider?: string; + /** Router model */ + routerModel?: string; + /** Show detailed information */ + detailed?: boolean; +}; diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index 1e419be02..56c59728d 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -69,3 +69,6 @@ export * from "./streamDedup.js"; // Curator P3-6: NoOutputGeneratedError sentinel chunk shape export * from "./noOutputSentinel.js"; + +// Multi-Agent orchestration types +export * from "./agentNetwork.js"; diff --git a/test/continuous-test-agents-live.ts b/test/continuous-test-agents-live.ts new file mode 100644 index 000000000..42bd7e732 --- /dev/null +++ b/test/continuous-test-agents-live.ts @@ -0,0 +1,348 @@ +#!/usr/bin/env tsx +/** + * Continuous test suite — live SDK + CLI + Agent system. + * Exercises generate/stream and Agent/AgentNetwork against a real provider. + * + * Usage: + * TEST_PROVIDER=vertex npx tsx test/continuous-test-agents-live.ts + */ + +import { execFileSync } from "child_process"; +import { NeuroLink } from "../src/lib/neurolink.js"; +import { withTimeout } from "../src/lib/utils/async/withTimeout.js"; + +type TestResult = { + name: string; + result: boolean | null; // true = PASS, false = FAIL, null = SKIP + error: string | null; + duration: number; +}; + +const TEST_CONFIG = { + provider: process.env.TEST_PROVIDER || "vertex", + model: process.env.TEST_MODEL || undefined, + maxTokens: 100, + timeout: 60000, +}; + +const results: TestResult[] = []; + +function runCli(args: string[]): string { + return execFileSync("node", ["dist/cli/index.js", ...args], { + encoding: "utf-8", + timeout: TEST_CONFIG.timeout, + stdio: ["ignore", "pipe", "pipe"], + }); +} + +async function run( + name: string, + fn: () => Promise, +): Promise { + const start = Date.now(); + let result: boolean | null = false; + let error: string | null = null; + try { + result = await withTimeout( + fn(), + TEST_CONFIG.timeout, + `${name} timed out after ${TEST_CONFIG.timeout}ms`, + ); + } catch (e) { + error = e instanceof Error ? e.message : String(e); + } + const duration = Date.now() - start; + results.push({ name, result, error, duration }); + const icon = result === true ? "✅" : result === null ? "⏭️ " : "❌"; + const status = result === true ? "PASS" : result === null ? "SKIP" : "FAIL"; + console.log(`${icon} ${status} [${duration}ms] ${name}`); + if (error) { + console.log(` ↳ ${error.split("\n")[0].slice(0, 200)}`); + } +} + +// ============================================================================ +// SDK tests +// ============================================================================ + +let sdk: NeuroLink; + +async function testSdkGenerate(): Promise { + const result = await sdk.generate({ + input: { text: "Say exactly: PONG" }, + provider: TEST_CONFIG.provider as never, + ...(TEST_CONFIG.model && { model: TEST_CONFIG.model }), + maxTokens: TEST_CONFIG.maxTokens, + }); + if (!result?.content || typeof result.content !== "string") { + throw new Error(`Invalid generate result: ${JSON.stringify(result)}`); + } + return result.content.length > 0; +} + +async function testSdkStream(): Promise { + const result = await sdk.stream({ + input: { text: "Count from 1 to 3" }, + provider: TEST_CONFIG.provider as never, + ...(TEST_CONFIG.model && { model: TEST_CONFIG.model }), + maxTokens: TEST_CONFIG.maxTokens, + }); + let content = ""; + let chunkCount = 0; + for await (const chunk of result.stream) { + chunkCount++; + if ("content" in chunk && typeof chunk.content === "string") { + content += chunk.content; + } + } + if (chunkCount === 0) { + throw new Error("Stream yielded zero chunks"); + } + if (content.length === 0) { + throw new Error("Stream yielded chunks but no content"); + } + return true; +} + +// ============================================================================ +// CLI tests +// ============================================================================ + +async function testCliGen(): Promise { + try { + const out = runCli([ + "gen", + "Say PONG", + "--provider", + TEST_CONFIG.provider, + "--max-tokens", + String(TEST_CONFIG.maxTokens), + ...(TEST_CONFIG.model ? ["--model", TEST_CONFIG.model] : []), + ]); + if (!out || out.trim().length === 0) { + throw new Error("CLI produced empty output"); + } + return true; + } catch (e) { + const err = e as { stderr?: Buffer; message?: string }; + const stderr = err.stderr?.toString() ?? ""; + throw new Error( + `CLI gen failed: ${err.message} stderr=${stderr.slice(0, 200)}`, + { cause: e }, + ); + } +} + +async function testCliStream(): Promise { + try { + const out = runCli([ + "stream", + "Count to 3", + "--provider", + TEST_CONFIG.provider, + "--max-tokens", + String(TEST_CONFIG.maxTokens), + ...(TEST_CONFIG.model ? ["--model", TEST_CONFIG.model] : []), + ]); + if (!out || out.trim().length === 0) { + throw new Error("CLI stream produced empty output"); + } + return true; + } catch (e) { + const err = e as { stderr?: Buffer; message?: string }; + const stderr = err.stderr?.toString() ?? ""; + throw new Error( + `CLI stream failed: ${err.message} stderr=${stderr.slice(0, 200)}`, + { cause: e }, + ); + } +} + +// ============================================================================ +// Agent tests (wrap SDK generate/stream) +// ============================================================================ + +async function testAgentExecute(): Promise { + const agent = await sdk.createAgent({ + id: "echo-agent", + name: "Echo Agent", + description: "Repeats back what it is told.", + instructions: "Answer concisely in under 20 words.", + provider: TEST_CONFIG.provider, + ...(TEST_CONFIG.model && { model: TEST_CONFIG.model }), + }); + const result = await agent.execute("Say PONG"); + if (!result?.content || typeof result.content !== "string") { + throw new Error(`Invalid agent result: ${JSON.stringify(result)}`); + } + return result.content.length > 0; +} + +async function testAgentStream(): Promise { + const agent = await sdk.createAgent({ + id: "stream-agent", + name: "Stream Agent", + description: "Streams responses.", + instructions: "Answer concisely in under 20 words.", + provider: TEST_CONFIG.provider, + ...(TEST_CONFIG.model && { model: TEST_CONFIG.model }), + }); + const chunks: Array<{ type: string }> = []; + let textContent = ""; + for await (const chunk of agent.stream("Count from 1 to 3")) { + chunks.push({ type: chunk.type }); + if (chunk.type === "agent-text" && "content" in chunk) { + textContent += (chunk as { content: string }).content; + } + } + const hasStart = chunks.some((c) => c.type === "agent-start"); + const hasComplete = chunks.some((c) => c.type === "agent-complete"); + if (!hasStart || !hasComplete) { + throw new Error( + `Missing required chunks. Got: ${chunks.map((c) => c.type).join(", ")}`, + ); + } + if (textContent.length === 0) { + throw new Error("No agent-text chunks with content"); + } + return true; +} + +// ============================================================================ +// AgentNetwork tests (routes + executes multiple agents) +// ============================================================================ + +async function testNetworkExecute(): Promise { + const network = await sdk.createNetwork({ + name: "Simple Network", + description: "Test network with one agent", + agents: [ + { + id: "responder", + name: "Responder", + description: "Responds to simple questions.", + instructions: "Answer concisely in under 20 words.", + provider: TEST_CONFIG.provider, + ...(TEST_CONFIG.model && { model: TEST_CONFIG.model }), + }, + ], + router: { + provider: TEST_CONFIG.provider, + ...(TEST_CONFIG.model && { model: TEST_CONFIG.model }), + }, + maxSteps: 2, + }); + const result = await network.execute({ message: "Say PONG" }); + if (!result?.content || typeof result.content !== "string") { + throw new Error( + `Invalid network result: ${JSON.stringify(result).slice(0, 200)}`, + ); + } + if (!result.trace || !Array.isArray(result.trace.steps)) { + throw new Error(`Missing trace.steps: ${JSON.stringify(result.trace)}`); + } + return result.trace.steps.length > 0; +} + +async function testNetworkStream(): Promise { + const network = await sdk.createNetwork({ + name: "Stream Network", + description: "Test streaming network", + agents: [ + { + id: "responder", + name: "Responder", + description: "Responds to simple questions.", + instructions: "Answer concisely in under 20 words.", + provider: TEST_CONFIG.provider, + ...(TEST_CONFIG.model && { model: TEST_CONFIG.model }), + }, + ], + router: { + provider: TEST_CONFIG.provider, + ...(TEST_CONFIG.model && { model: TEST_CONFIG.model }), + }, + maxSteps: 2, + }); + const chunkTypes: string[] = []; + for await (const chunk of network.stream({ message: "Say PONG" })) { + chunkTypes.push(chunk.type); + } + const hasStart = chunkTypes.includes("network-start"); + const hasComplete = + chunkTypes.includes("network-complete") || + chunkTypes.includes("network-end"); + if (!hasStart) { + throw new Error( + `Missing network-start chunk. Got: ${chunkTypes.slice(0, 10).join(", ")}...`, + ); + } + if (!hasComplete) { + throw new Error( + `Missing network-complete/end chunk. Got: ${chunkTypes.slice(-5).join(", ")}`, + ); + } + return true; +} + +// ============================================================================ +// Runner +// ============================================================================ + +async function main(): Promise { + console.log(`\n🧪 Continuous Test Suite — Agents Live\n`); + console.log(`Provider: ${TEST_CONFIG.provider}`); + console.log(`Model: ${TEST_CONFIG.model ?? "(provider default)"}\n`); + + sdk = new NeuroLink(); + + console.log("─── SDK ──────────────────────────────────────────"); + await run("SDK generate", testSdkGenerate); + await run("SDK stream", testSdkStream); + + console.log("\n─── CLI ──────────────────────────────────────────"); + await run("CLI gen", testCliGen); + await run("CLI stream", testCliStream); + + console.log("\n─── Agent ────────────────────────────────────────"); + await run("Agent.execute() → generate", testAgentExecute); + await run("Agent.stream() → stream", testAgentStream); + + console.log("\n─── Network ──────────────────────────────────────"); + await run("AgentNetwork.execute() → routes + generate", testNetworkExecute); + await run("AgentNetwork.stream() → routes + stream", testNetworkStream); + + // Summary + console.log("\n════════════════════════════════════════════════════"); + const pass = results.filter((r) => r.result === true).length; + const fail = results.filter((r) => r.result === false).length; + const skip = results.filter((r) => r.result === null).length; + console.log( + `Results: ${pass} PASS · ${fail} FAIL · ${skip} SKIP (of ${results.length})`, + ); + if (fail > 0) { + console.log("\nFailures:"); + results + .filter((r) => r.result === false) + .forEach((r) => { + console.log(` ❌ ${r.name}`); + if (r.error) { + console.log(` ${r.error.split("\n")[0].slice(0, 300)}`); + } + }); + } + console.log(""); + + try { + await sdk.dispose(); + } catch { + // ignore + } + + process.exit(fail > 0 ? 1 : 0); +} + +main().catch((err) => { + console.error("💥 Unhandled error:", err); + process.exit(1); +}); diff --git a/test/continuous-test-suite-agents.ts b/test/continuous-test-suite-agents.ts new file mode 100644 index 000000000..9a3f51953 --- /dev/null +++ b/test/continuous-test-suite-agents.ts @@ -0,0 +1,1622 @@ +#!/usr/bin/env tsx + +/** + * Continuous Integration Test Suite for Multi-Agent Networks + * + * This test suite verifies the Multi-Agent Networks feature including: + * 1. Agent class - fixture validation, construction, execution via mock SDK + * 2. AgentNetwork - fixture topology validation + * 3. Routing rules - system-prompt-based routing (agents-as-tools pattern) + * 4. MessageBus - pub/sub, request-response, broadcast, priority queues + * 5. Integration - real Agent/AgentNetwork construction from source modules + * + * Architecture note: routing is implemented as a system prompt + agents-as-tools + * (ai SDK tool loop). RouterAgent, SupervisorAgent, AgentEvaluator, + * ProtocolManager, and DelegationProtocolHandler no longer exist. + * + * Run with: npx tsx test/continuous-test-suite-agents.ts + */ + +import * as fs from "fs"; +import * as path from "path"; +import { fileURLToPath } from "url"; +import { execFileSync } from "child_process"; + +// ES Module directory handling +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// ============================================================================ +// Type Definitions +// ============================================================================ + +type ColorName = + | "reset" + | "bright" + | "red" + | "green" + | "yellow" + | "blue" + | "magenta" + | "cyan"; + +type TestStatus = "PASS" | "FAIL" | "TESTING" | "SKIP"; + +type TestResult = { + name: string; + status: TestStatus; + duration: number; + details?: string; + error?: string; +}; + +type TestSuiteResult = { + name: string; + tests: TestResult[]; + passed: number; + failed: number; + skipped: number; + duration: number; +}; + +type AgentDefinitionFixture = { + id: string; + name: string; + description: string; + instructions: string; + provider?: string; + model?: string; + tools?: string[]; + maxSteps?: number; + temperature?: number; + canDelegate?: boolean; + metadata?: Record; +}; + +type NetworkTopologyFixture = { + id: string; + name: string; + description: string; + topology: string; + config: Record; + agents: string[]; +}; + +type RoutingRuleFixture = { + id: string; + name: string; + patterns: string[]; + keywords: string[]; + targetAgent: string; + priority: number; + confidence: number; +}; + +type MessageFixture = { + id: string; + type: string; + fromAgent?: string; + toAgent?: string; + payload?: unknown; + priority?: number; +}; + +// ============================================================================ +// Test Configuration +// ============================================================================ + +const TEST_CONFIG = { + provider: process.env.TEST_PROVIDER || "vertex", + model: process.env.TEST_MODEL || undefined, + timeout: 30000, + verbose: process.env.VERBOSE === "true", +}; + +// Color codes for output +const colors: Record = { + reset: "\x1b[0m", + bright: "\x1b[1m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + cyan: "\x1b[36m", +}; + +// ============================================================================ +// Logging Utilities +// ============================================================================ + +function log(message: string, color: ColorName = "reset"): void { + console.log(`${colors[color]}${message}${colors.reset}`); +} + +function logSection(title: string): void { + log(`\n${"=".repeat(70)}`, "cyan"); + log(` ${title}`, "cyan"); + log(`${"=".repeat(70)}`, "cyan"); +} + +function logSubSection(title: string): void { + log(`\n${"-".repeat(50)}`, "blue"); + log(` ${title}`, "blue"); + log(`${"-".repeat(50)}`, "blue"); +} + +function logTest(testName: string, status: TestStatus, details = ""): void { + const icons: Record = { + PASS: "✅", + FAIL: "❌", + TESTING: "⚠️", + SKIP: "⏭️", + }; + const statusColors: Record = { + PASS: "green", + FAIL: "red", + TESTING: "yellow", + SKIP: "yellow", + }; + log(`${icons[status]} ${testName}`, statusColors[status]); + if (details) { + log(` ${details}`, "reset"); + } +} + +function logDebug(message: string): void { + if (TEST_CONFIG.verbose) { + log(`[DEBUG] ${message}`, "magenta"); + } +} + +// ============================================================================ +// Fixture Loading +// ============================================================================ + +function loadFixture(filename: string): T { + const fixturePath = path.join(__dirname, "fixtures", "agents", filename); + const content = fs.readFileSync(fixturePath, "utf-8"); + return JSON.parse(content) as T; +} + +// ============================================================================ +// Mock SDK for Testing +// ============================================================================ + +function createMockSdk(options?: { + generateResponse?: { content: string; usage?: unknown; toolsUsed?: string[] }; + streamChunks?: Array<{ content?: string }>; + shouldFail?: boolean; + errorMessage?: string; +}) { + return { + generate: async () => { + if (options?.shouldFail) { + throw new Error(options.errorMessage ?? "Mock generation error"); + } + return { + content: options?.generateResponse?.content ?? "Mock response", + usage: options?.generateResponse?.usage ?? { + promptTokens: 10, + completionTokens: 20, + totalTokens: 30, + }, + toolsUsed: options?.generateResponse?.toolsUsed ?? [], + }; + }, + stream: async function* () { + const chunks = options?.streamChunks ?? [ + { content: "Hello " }, + { content: "world!" }, + ]; + for (const chunk of chunks) { + yield chunk; + } + }, + dispose: async () => { + // Cleanup mock resources + }, + }; +} + +// ============================================================================ +// Test Helpers +// ============================================================================ + +async function runTest( + name: string, + testFn: () => Promise, +): Promise { + const startTime = Date.now(); + try { + await testFn(); + const duration = Date.now() - startTime; + logTest(name, "PASS", `(${duration}ms)`); + return { name, status: "PASS", duration }; + } catch (error) { + const duration = Date.now() - startTime; + const errorMessage = error instanceof Error ? error.message : String(error); + logTest(name, "FAIL", errorMessage); + return { name, status: "FAIL", duration, error: errorMessage }; + } +} + +function skipTest(name: string, reason: string): TestResult { + logTest(name, "SKIP", reason); + return { name, status: "SKIP", duration: 0, details: reason }; +} + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`Assertion failed: ${message}`); + } +} + +function assertEqual(actual: T, expected: T, message?: string): void { + if (actual !== expected) { + throw new Error( + `${message || "Assertion failed"}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`, + ); + } +} + +function assertDefined( + value: T | undefined | null, + message?: string, +): asserts value is T { + if (value === undefined || value === null) { + throw new Error(`${message || "Value is undefined or null"}`); + } +} + +function assertContains( + str: string, + substring: string, + message?: string, +): void { + if (!str.includes(substring)) { + throw new Error( + `${message || "String does not contain expected substring"}: "${substring}" not found in "${str}"`, + ); + } +} + +// ============================================================================ +// Test Suites +// ============================================================================ + +/** + * Test Suite: Agent Class + */ +async function testAgentClass(): Promise { + logSection("AGENT CLASS TESTS"); + const results: TestResult[] = []; + const startTime = Date.now(); + + // Load fixtures + const fixtureData = loadFixture<{ + agents: Record; + }>("agent-definitions.json"); + const agents = fixtureData.agents; + + // Test: Agent creation with basic definition + results.push( + await runTest("Agent creation with basic definition", async () => { + const basicAgent = agents.basicAgent; + assertDefined(basicAgent, "Basic agent fixture should exist"); + assertEqual(basicAgent.id, "test-basic-agent", "Agent ID should match"); + assertEqual( + basicAgent.name, + "Basic Test Agent", + "Agent name should match", + ); + assert( + basicAgent.description.length > 0, + "Agent should have description", + ); + assert( + basicAgent.instructions.length > 0, + "Agent should have instructions", + ); + }), + ); + + // Test: Agent creation with full configuration + results.push( + await runTest("Agent creation with full configuration", async () => { + const fullAgent = agents.fullConfigAgent; + assertDefined(fullAgent, "Full config agent fixture should exist"); + assertEqual(fullAgent.provider, "openai", "Provider should be set"); + assertEqual(fullAgent.model, "gpt-4o-mini", "Model should be set"); + assert(Array.isArray(fullAgent.tools), "Tools should be an array"); + assertEqual(fullAgent.maxSteps, 10, "Max steps should be set"); + assertEqual(fullAgent.temperature, 0.5, "Temperature should be set"); + assertEqual(fullAgent.canDelegate, true, "canDelegate should be set"); + assertDefined(fullAgent.metadata, "Metadata should be defined"); + }), + ); + + // Test: Agent with different providers + results.push( + await runTest("Agents with different providers", async () => { + const providers = new Set(); + for (const agent of Object.values(agents)) { + if (agent.provider) { + providers.add(agent.provider); + } + } + // Should have multiple different providers configured + assert( + providers.size >= 3, + `Should have at least 3 different providers, got ${providers.size}`, + ); + assert(providers.has("openai"), "Should include OpenAI provider"); + assert(providers.has("anthropic"), "Should include Anthropic provider"); + }), + ); + + // Test: Agent tool configurations + results.push( + await runTest("Agent tool configurations", async () => { + const codeAnalyzer = agents.codeAnalysisAgent; + assertDefined(codeAnalyzer.tools, "Code analyzer should have tools"); + assert( + codeAnalyzer.tools.length >= 2, + "Code analyzer should have multiple tools", + ); + assertContains( + codeAnalyzer.tools.join(","), + "readFile", + "Should include readFile tool", + ); + }), + ); + + // Test: Minimal agent configuration + results.push( + await runTest("Minimal agent configuration", async () => { + const minimal = agents.minimalAgent; + assertDefined(minimal, "Minimal agent should exist"); + assertEqual(minimal.id, "minimal", "Minimal agent ID should match"); + // Minimal agent should only have required fields + assert( + minimal.provider === undefined, + "Minimal agent should not have provider", + ); + assert( + minimal.model === undefined, + "Minimal agent should not have model", + ); + }), + ); + + // Test: Error-prone agent for error handling tests + results.push( + await runTest("Error-prone agent configuration", async () => { + const errorProne = agents.errorProneAgent; + assertDefined(errorProne, "Error-prone agent should exist"); + assertEqual( + errorProne.provider, + "invalid-provider", + "Should have invalid provider", + ); + assertEqual( + errorProne.model, + "non-existent-model", + "Should have non-existent model", + ); + assertDefined( + errorProne.metadata?.expectedErrors, + "Should have expected errors metadata", + ); + }), + ); + + // Test: Mock SDK generate execution + results.push( + await runTest("Mock SDK generate execution", async () => { + const sdk = createMockSdk({ + generateResponse: { content: "Test response", toolsUsed: ["testTool"] }, + }); + const result = await sdk.generate(); + assertEqual(result.content, "Test response", "Content should match"); + assert(result.toolsUsed.includes("testTool"), "Should include tool used"); + }), + ); + + // Test: Mock SDK stream execution + results.push( + await runTest("Mock SDK stream execution", async () => { + const sdk = createMockSdk({ + streamChunks: [{ content: "chunk1" }, { content: "chunk2" }], + }); + const chunks: string[] = []; + for await (const chunk of sdk.stream()) { + if (chunk.content) { + chunks.push(chunk.content); + } + } + assertEqual(chunks.length, 2, "Should have 2 chunks"); + assertEqual(chunks[0], "chunk1", "First chunk should match"); + }), + ); + + // Test: Mock SDK error handling + results.push( + await runTest("Mock SDK error handling", async () => { + const sdk = createMockSdk({ + shouldFail: true, + errorMessage: "Test error", + }); + try { + await sdk.generate(); + throw new Error("Should have thrown"); + } catch (error) { + assertContains( + String(error), + "Test error", + "Error message should contain expected text", + ); + } + }), + ); + + const duration = Date.now() - startTime; + return { + name: "Agent Class Tests", + tests: results, + passed: results.filter((r) => r.status === "PASS").length, + failed: results.filter((r) => r.status === "FAIL").length, + skipped: results.filter((r) => r.status === "SKIP").length, + duration, + }; +} + +/** + * Test Suite: Network Topologies + */ +async function testNetworkTopologies(): Promise { + logSection("NETWORK TOPOLOGY TESTS"); + const results: TestResult[] = []; + const startTime = Date.now(); + + // Load fixtures + const fixtureData = loadFixture<{ + networks: Record; + routerConfigs: Record; + networkDefaults: Record; + }>("network-topologies.json"); + const networks = fixtureData.networks; + + // Test: Hub-Spoke topology configuration + results.push( + await runTest("Hub-Spoke topology configuration", async () => { + const hubSpoke = networks.simpleHubSpoke; + assertDefined(hubSpoke, "Hub-spoke network should exist"); + assertEqual( + hubSpoke.topology, + "hub-spoke", + "Topology type should be hub-spoke", + ); + assertDefined(hubSpoke.config.hubAgentId, "Should have hub agent ID"); + assert( + Array.isArray(hubSpoke.config.spokeAgentIds), + "Should have spoke agent IDs array", + ); + }), + ); + + // Test: Advanced Hub-Spoke with failover + results.push( + await runTest("Advanced Hub-Spoke with failover", async () => { + const advanced = networks.advancedHubSpoke; + assertDefined(advanced, "Advanced hub-spoke should exist"); + assertEqual( + advanced.config.failoverEnabled, + true, + "Failover should be enabled", + ); + assertEqual( + advanced.config.priorityRouting, + true, + "Priority routing should be enabled", + ); + assertDefined( + advanced.config.healthCheckInterval, + "Should have health check interval", + ); + }), + ); + + // Test: Mesh topology configuration + results.push( + await runTest("Mesh topology configuration", async () => { + const mesh = networks.simpleMesh; + assertDefined(mesh, "Mesh network should exist"); + assertEqual(mesh.topology, "mesh", "Topology type should be mesh"); + assert( + Array.isArray(mesh.config.agentIds), + "Should have agent IDs array", + ); + assertDefined(mesh.config.maxHops, "Should have max hops configuration"); + }), + ); + + // Test: Secure Mesh with access control + results.push( + await runTest("Secure Mesh with access control", async () => { + const secureMesh = networks.secureMesh; + assertDefined(secureMesh, "Secure mesh should exist"); + assertEqual( + secureMesh.config.autoDiscovery, + false, + "Auto discovery should be disabled", + ); + assertDefined( + secureMesh.config.accessControl, + "Should have access control configuration", + ); + assertEqual( + secureMesh.config.auditLogging, + true, + "Audit logging should be enabled", + ); + }), + ); + + // Test: Hierarchical topology configuration + results.push( + await runTest("Hierarchical topology configuration", async () => { + const hierarchical = networks.simpleHierarchical; + assertDefined(hierarchical, "Hierarchical network should exist"); + assertEqual( + hierarchical.topology, + "hierarchical", + "Topology type should be hierarchical", + ); + assertDefined( + hierarchical.config.rootAgentId, + "Should have root agent ID", + ); + assert( + Array.isArray(hierarchical.config.levels), + "Should have levels array", + ); + }), + ); + + // Test: Complex hierarchical with cross-level communication + results.push( + await runTest("Complex hierarchical with cross-level", async () => { + const complex = networks.complexHierarchical; + assertDefined(complex, "Complex hierarchical should exist"); + assertEqual( + complex.config.allowCrossLevel, + true, + "Cross-level should be allowed", + ); + assertDefined( + complex.config.escalationThreshold, + "Should have escalation threshold", + ); + assertDefined( + complex.config.maxEscalationDepth, + "Should have max escalation depth", + ); + }), + ); + + // Test: Minimal network configuration + results.push( + await runTest("Minimal network configuration", async () => { + const minimal = networks.minimalNetwork; + assertDefined(minimal, "Minimal network should exist"); + assertEqual( + minimal.agents.length, + 1, + "Minimal network should have 1 agent", + ); + const spokeIds = minimal.config.spokeAgentIds as string[]; + assertEqual(spokeIds.length, 0, "Minimal network should have no spokes"); + }), + ); + + // Test: Error test network configuration + results.push( + await runTest("Error test network configuration", async () => { + const errorNetwork = networks.errorTestNetwork; + assertDefined(errorNetwork, "Error test network should exist"); + assert( + errorNetwork.agents.includes("error-prone"), + "Should include error-prone agent", + ); + }), + ); + + // Test: Router configurations + results.push( + await runTest("Router configurations", async () => { + const routerConfigs = fixtureData.routerConfigs; + assertDefined(routerConfigs.defaultRouter, "Default router should exist"); + assertDefined(routerConfigs.strictRouter, "Strict router should exist"); + assertDefined(routerConfigs.hybridRouter, "Hybrid router should exist"); + }), + ); + + // Test: Network defaults + results.push( + await runTest("Network defaults", async () => { + const defaults = fixtureData.networkDefaults; + assertDefined(defaults.standard, "Standard defaults should exist"); + assertDefined( + defaults.highThroughput, + "High throughput defaults should exist", + ); + assertDefined(defaults.reliable, "Reliable defaults should exist"); + }), + ); + + const duration = Date.now() - startTime; + return { + name: "Network Topology Tests", + tests: results, + passed: results.filter((r) => r.status === "PASS").length, + failed: results.filter((r) => r.status === "FAIL").length, + skipped: results.filter((r) => r.status === "SKIP").length, + duration, + }; +} + +/** + * Test Suite: Routing Rules + */ +async function testRoutingRules(): Promise { + logSection("ROUTING RULES TESTS"); + const results: TestResult[] = []; + const startTime = Date.now(); + + // Load fixtures + const fixtureData = loadFixture<{ + routingRules: Record; + routingDecisions: Record; + confidenceThresholds: Record; + testCases: Record; + }>("routing-rules.json"); + const rules = fixtureData.routingRules; + + // Test: Code analysis routing rule + results.push( + await runTest("Code analysis routing rule", async () => { + const codeRule = rules.codeAnalysis; + assertDefined(codeRule, "Code analysis rule should exist"); + assertEqual( + codeRule.targetAgent, + "code-analyzer", + "Target should be code-analyzer", + ); + assert(codeRule.patterns.length > 0, "Should have patterns"); + assert(codeRule.keywords.length > 0, "Should have keywords"); + assert(codeRule.confidence >= 0.8, "Should have high confidence"); + }), + ); + + // Test: Data processing routing rule + results.push( + await runTest("Data processing routing rule", async () => { + const dataRule = rules.dataProcessing; + assertDefined(dataRule, "Data processing rule should exist"); + assertEqual( + dataRule.targetAgent, + "data-processor", + "Target should be data-processor", + ); + assertContains( + dataRule.keywords.join(","), + "csv", + "Should include csv keyword", + ); + assertContains( + dataRule.keywords.join(","), + "json", + "Should include json keyword", + ); + }), + ); + + // Test: Research routing rule + results.push( + await runTest("Research routing rule", async () => { + const researchRule = rules.research; + assertDefined(researchRule, "Research rule should exist"); + assertEqual( + researchRule.targetAgent, + "researcher", + "Target should be researcher", + ); + assertContains( + researchRule.patterns.join(","), + "research", + "Should include research pattern", + ); + }), + ); + + // Test: Validation routing rule + results.push( + await runTest("Validation routing rule", async () => { + const validationRule = rules.validation; + assertDefined(validationRule, "Validation rule should exist"); + assertEqual( + validationRule.targetAgent, + "validator", + "Target should be validator", + ); + assertContains( + validationRule.keywords.join(","), + "validate", + "Should include validate keyword", + ); + }), + ); + + // Test: Priority ordering + results.push( + await runTest("Priority ordering of rules", async () => { + const priorities = Object.values(rules).map((r) => r.priority); + const sortedPriorities = [...priorities].sort((a, b) => a - b); + // Check that priorities are unique and ordered + assertEqual( + new Set(priorities).size, + priorities.length, + "Priorities should be unique", + ); + assertEqual( + rules.codeAnalysis.priority, + 1, + "Code analysis should have highest priority", + ); + }), + ); + + // Test: Confidence thresholds + results.push( + await runTest("Confidence thresholds", async () => { + const thresholds = fixtureData.confidenceThresholds; + assertDefined(thresholds.high, "High threshold should exist"); + assertDefined(thresholds.medium, "Medium threshold should exist"); + assertDefined(thresholds.low, "Low threshold should exist"); + assertDefined(thresholds["very-low"], "Very-low threshold should exist"); + }), + ); + + // Test: Simple routing decision + results.push( + await runTest("Simple routing decision", async () => { + const decisions = fixtureData.routingDecisions as Record< + string, + { + expectedDecision: { selectedAgent: string; confidence: number }; + } + >; + const simple = decisions.simpleMatch; + assertDefined(simple, "Simple match should exist"); + assertEqual( + simple.expectedDecision.selectedAgent, + "code-analyzer", + "Should route to code-analyzer", + ); + assert( + simple.expectedDecision.confidence >= 0.9, + "Should have high confidence", + ); + }), + ); + + // Test: Ambiguous routing decision + results.push( + await runTest("Ambiguous routing decision", async () => { + const decisions = fixtureData.routingDecisions as Record< + string, + { + expectedDecision: { + selectedAgent: string; + confidence: number; + alternativeAgents?: string[]; + }; + } + >; + const ambiguous = decisions.ambiguousMatch; + assertDefined(ambiguous, "Ambiguous match should exist"); + assert( + ambiguous.expectedDecision.confidence < 0.9, + "Should have lower confidence for ambiguous input", + ); + assert( + (ambiguous.expectedDecision.alternativeAgents?.length ?? 0) > 0, + "Should have alternative agents", + ); + }), + ); + + // Test: Fallback routing decision + results.push( + await runTest("Fallback routing decision", async () => { + const decisions = fixtureData.routingDecisions as Record< + string, + { + expectedDecision: { selectedAgent: string; isFallback?: boolean }; + } + >; + const noMatch = decisions.noMatch; + assertDefined(noMatch, "No match should exist"); + assertEqual( + noMatch.expectedDecision.isFallback, + true, + "Should be marked as fallback", + ); + assertEqual( + noMatch.expectedDecision.selectedAgent, + "coordinator", + "Should fallback to coordinator", + ); + }), + ); + + // Test: Pattern matching test cases + results.push( + await runTest("Pattern matching test cases", async () => { + const testCases = fixtureData.testCases; + assertDefined( + testCases.exactPatternMatch, + "Exact pattern tests should exist", + ); + assertDefined(testCases.keywordMatch, "Keyword match tests should exist"); + assertDefined(testCases.negativeTests, "Negative tests should exist"); + assertDefined(testCases.edgeCases, "Edge cases should exist"); + }), + ); + + const duration = Date.now() - startTime; + return { + name: "Routing Rules Tests", + tests: results, + passed: results.filter((r) => r.status === "PASS").length, + failed: results.filter((r) => r.status === "FAIL").length, + skipped: results.filter((r) => r.status === "SKIP").length, + duration, + }; +} + +/** + * Test Suite: MessageBus + */ +async function testMessageBus(): Promise { + logSection("MESSAGE BUS TESTS"); + const results: TestResult[] = []; + const startTime = Date.now(); + + // Load fixtures + const fixtureData = loadFixture<{ + messageTypes: Record; + testMessages: { + taskMessages: MessageFixture[]; + resultMessages: MessageFixture[]; + statusMessages: MessageFixture[]; + broadcastMessages: MessageFixture[]; + requestResponsePairs: Array<{ + request: MessageFixture; + response: MessageFixture; + }>; + eventMessages: MessageFixture[]; + }; + subscriptionPatterns: Record; + priorityLevels: Record; + testScenarios: Record; + }>("messages.json"); + + // Test: Message types definition + results.push( + await runTest("Message types definition", async () => { + const types = fixtureData.messageTypes; + assertDefined(types.task, "Task message type should exist"); + assertDefined(types.result, "Result message type should exist"); + assertDefined(types.status, "Status message type should exist"); + assertDefined(types.broadcast, "Broadcast message type should exist"); + assertDefined(types.request, "Request message type should exist"); + assertDefined(types.response, "Response message type should exist"); + assertDefined(types.event, "Event message type should exist"); + }), + ); + + // Test: Task messages + results.push( + await runTest("Task messages", async () => { + const tasks = fixtureData.testMessages.taskMessages; + assert(tasks.length >= 3, "Should have at least 3 task messages"); + for (const task of tasks) { + assertEqual(task.type, "task", "Type should be task"); + assertDefined(task.fromAgent, "Should have fromAgent"); + assertDefined(task.toAgent, "Should have toAgent"); + assertDefined(task.payload, "Should have payload"); + } + }), + ); + + // Test: Result messages + results.push( + await runTest("Result messages", async () => { + const results_ = fixtureData.testMessages.resultMessages; + assert(results_.length >= 3, "Should have at least 3 result messages"); + for (const result of results_) { + assertEqual(result.type, "result", "Type should be result"); + assertDefined(result.fromAgent, "Should have fromAgent"); + } + }), + ); + + // Test: Status messages + results.push( + await runTest("Status messages", async () => { + const statuses = fixtureData.testMessages.statusMessages; + assert(statuses.length >= 3, "Should have at least 3 status messages"); + // Check for various states + const states = new Set( + statuses.map((s) => (s as unknown as { state: string }).state), + ); + assert(states.has("idle"), "Should include idle state"); + assert(states.has("executing"), "Should include executing state"); + }), + ); + + // Test: Broadcast messages + results.push( + await runTest("Broadcast messages", async () => { + const broadcasts = fixtureData.testMessages.broadcastMessages; + assert( + broadcasts.length >= 2, + "Should have at least 2 broadcast messages", + ); + for (const broadcast of broadcasts) { + assertEqual(broadcast.type, "broadcast", "Type should be broadcast"); + assertDefined( + (broadcast as unknown as { topic: string }).topic, + "Should have topic", + ); + } + }), + ); + + // Test: Request-response pairs + results.push( + await runTest("Request-response pairs", async () => { + const pairs = fixtureData.testMessages.requestResponsePairs; + assert( + pairs.length >= 2, + "Should have at least 2 request-response pairs", + ); + for (const pair of pairs) { + assertDefined(pair.request, "Should have request"); + assertDefined(pair.response, "Should have response"); + assertEqual( + (pair.request as unknown as { requestId: string }).requestId, + (pair.response as unknown as { requestId: string }).requestId, + "Request IDs should match", + ); + } + }), + ); + + // Test: Event messages + results.push( + await runTest("Event messages", async () => { + const events = fixtureData.testMessages.eventMessages; + assert(events.length >= 2, "Should have at least 2 event messages"); + for (const event of events) { + assertEqual(event.type, "event", "Type should be event"); + assertDefined( + (event as unknown as { eventType: string }).eventType, + "Should have eventType", + ); + assertDefined( + (event as unknown as { timestamp: string }).timestamp, + "Should have timestamp", + ); + } + }), + ); + + // Test: Subscription patterns + results.push( + await runTest("Subscription patterns", async () => { + const patterns = fixtureData.subscriptionPatterns; + assertDefined(patterns.allMessages, "All messages pattern should exist"); + assertDefined(patterns.taskOnly, "Task only pattern should exist"); + assertDefined( + patterns.specificAgent, + "Specific agent pattern should exist", + ); + }), + ); + + // Test: Priority levels + results.push( + await runTest("Priority levels", async () => { + const levels = fixtureData.priorityLevels; + assertDefined(levels.critical, "Critical priority should exist"); + assertDefined(levels.high, "High priority should exist"); + assertDefined(levels.normal, "Normal priority should exist"); + assertDefined(levels.low, "Low priority should exist"); + assertDefined(levels.background, "Background priority should exist"); + }), + ); + + // Test: Test scenarios + results.push( + await runTest("Test scenarios", async () => { + const scenarios = fixtureData.testScenarios; + assertDefined( + scenarios.basicPubSub, + "Basic pub/sub scenario should exist", + ); + assertDefined( + scenarios.requestResponse, + "Request-response scenario should exist", + ); + assertDefined(scenarios.broadcast, "Broadcast scenario should exist"); + assertDefined( + scenarios.priorityQueue, + "Priority queue scenario should exist", + ); + assertDefined( + scenarios.errorHandling, + "Error handling scenario should exist", + ); + assertDefined(scenarios.timeout, "Timeout scenario should exist"); + }), + ); + + const duration = Date.now() - startTime; + return { + name: "Message Bus Tests", + tests: results, + passed: results.filter((r) => r.status === "PASS").length, + failed: results.filter((r) => r.status === "FAIL").length, + skipped: results.filter((r) => r.status === "SKIP").length, + duration, + }; +} + +/** + * Test Suite: Integration Tests (imports actual Agent/AgentNetwork/MessageBus) + * + * Tests real construction and field validation using the actual classes. + * Agent/AgentNetwork accept a NeuroLink instance, but construction (not + * execution) can be validated without a live provider. + */ +async function testIntegration(): Promise { + logSection("INTEGRATION TESTS"); + const results: TestResult[] = []; + const startTime = Date.now(); + + // Dynamically import real implementations + type AgentClass = { + new ( + def: { + id: string; + name: string; + description: string; + instructions: string; + provider?: string; + model?: string; + tools?: string[]; + maxSteps?: number; + temperature?: number; + canDelegate?: boolean; + }, + neurolink: unknown, + ): { + id: string; + name: string; + description: string; + canDelegate: boolean; + maxSteps: number; + temperature: number; + }; + }; + + type AgentNetworkClass = { + new ( + config: { + name: string; + description?: string; + agents: Array<{ + id: string; + name: string; + description: string; + instructions: string; + }>; + }, + neurolink: unknown, + ): { id: string; name: string; getAllAgents(): unknown[] }; + }; + + type MessageBusClass = { + new (): { + publish(topic: string, senderId: string, payload: unknown): Promise; + subscribe( + topic: string, + subscriberId: string, + handler: (msg: unknown) => void, + ): string; + }; + }; + + let AgentCtor: AgentClass | undefined; + let AgentNetworkCtor: AgentNetworkClass | undefined; + let MessageBusCtor: MessageBusClass | undefined; + + try { + const mod = await import("../src/lib/agent/agent.js"); + AgentCtor = mod.Agent as unknown as AgentClass; + logDebug("Imported Agent class"); + } catch (e) { + if ((e as NodeJS.ErrnoException).code === "ERR_MODULE_NOT_FOUND") { + results.push( + skipTest( + "Import Agent class", + "Agent module not available - run `pnpm build` first", + ), + ); + } else { + throw e; + } + } + + try { + const mod = await import("../src/lib/agent/agentNetwork.js"); + AgentNetworkCtor = mod.AgentNetwork as unknown as AgentNetworkClass; + logDebug("Imported AgentNetwork class"); + } catch (e) { + if ((e as NodeJS.ErrnoException).code === "ERR_MODULE_NOT_FOUND") { + results.push( + skipTest( + "Import AgentNetwork class", + "AgentNetwork module not available - run `pnpm build` first", + ), + ); + } else { + throw e; + } + } + + try { + const mod = await import("../src/lib/agent/communication/message-bus.js"); + MessageBusCtor = mod.MessageBus as unknown as MessageBusClass; + logDebug("Imported MessageBus class"); + } catch (e) { + if ((e as NodeJS.ErrnoException).code === "ERR_MODULE_NOT_FOUND") { + results.push( + skipTest( + "Import MessageBus class", + "MessageBus module not available - run `pnpm build` first", + ), + ); + } else { + throw e; + } + } + + // Agent construction: validate field assignment and defaults + if (AgentCtor) { + results.push( + await runTest( + "Agent construction sets id, name, description", + async () => { + // A minimal stub that satisfies the NeuroLink shape Agent reads during construction + // (Agent constructor only stores the reference; it calls neurolink.generate() on execute()) + const stubNeurolink = {} as unknown; + const agent = new AgentCtor!( + { + id: "integration-test-agent", + name: "Integration Test Agent", + description: "Agent for integration testing", + instructions: "You are a test agent.", + }, + stubNeurolink, + ); + assertEqual( + agent.id, + "integration-test-agent", + "Agent ID should match", + ); + assertEqual( + agent.name, + "Integration Test Agent", + "Agent name should match", + ); + assertEqual( + agent.description, + "Agent for integration testing", + "Agent description should match", + ); + }, + ), + ); + + results.push( + await runTest("Agent construction applies defaults", async () => { + const stubNeurolink = {} as unknown; + const agent = new AgentCtor!( + { + id: "defaults-agent", + name: "Defaults Agent", + description: "Tests defaults", + instructions: "Instructions here.", + }, + stubNeurolink, + ); + assertEqual(agent.maxSteps, 10, "Default maxSteps should be 10"); + assertEqual( + agent.temperature, + 0.7, + "Default temperature should be 0.7", + ); + assertEqual( + agent.canDelegate, + false, + "Default canDelegate should be false", + ); + }), + ); + + results.push( + await runTest("Agent construction rejects missing id", async () => { + const stubNeurolink = {} as unknown; + try { + new AgentCtor!( + { + id: "", + name: "Bad Agent", + description: "Should fail", + instructions: "Instructions.", + }, + stubNeurolink, + ); + throw new Error("Should have thrown on empty id"); + } catch (err) { + assertContains(String(err), "id", "Error should mention id field"); + } + }), + ); + + results.push( + await runTest("Agent construction accepts full config", async () => { + const stubNeurolink = {} as unknown; + const agent = new AgentCtor!( + { + id: "full-config-agent", + name: "Full Config Agent", + description: "Fully configured agent", + instructions: "Full instructions.", + provider: "anthropic", + model: "claude-3-5-sonnet-latest", + tools: ["readFile", "writeFile"], + maxSteps: 5, + temperature: 0.3, + canDelegate: true, + }, + stubNeurolink, + ); + assertEqual(agent.maxSteps, 5, "maxSteps should be 5"); + assertEqual(agent.temperature, 0.3, "temperature should be 0.3"); + assertEqual(agent.canDelegate, true, "canDelegate should be true"); + }), + ); + } + + // AgentNetwork construction: validate name, agents registration + if (AgentNetworkCtor) { + results.push( + await runTest("AgentNetwork construction registers agents", async () => { + const stubNeurolink = {} as unknown; + const network = new AgentNetworkCtor!( + { + name: "Test Network", + description: "Integration test network", + agents: [ + { + id: "agent-a", + name: "Agent A", + description: "First agent", + instructions: "Instructions A.", + }, + { + id: "agent-b", + name: "Agent B", + description: "Second agent", + instructions: "Instructions B.", + }, + ], + }, + stubNeurolink, + ); + assertEqual(network.name, "Test Network", "Network name should match"); + const agents = network.getAllAgents(); + assertEqual( + (agents as unknown[]).length, + 2, + "Network should have 2 agents registered", + ); + }), + ); + + results.push( + await runTest( + "AgentNetwork construction rejects empty agents", + async () => { + const stubNeurolink = {} as unknown; + try { + new AgentNetworkCtor!( + { + name: "Empty Network", + agents: [], + }, + stubNeurolink, + ); + throw new Error("Should have thrown on empty agents array"); + } catch (err) { + assertContains(String(err), "agent", "Error should mention agents"); + } + }, + ), + ); + + results.push( + await runTest( + "AgentNetwork construction rejects missing name", + async () => { + const stubNeurolink = {} as unknown; + try { + new AgentNetworkCtor!( + { + name: "", + agents: [ + { id: "a", name: "A", description: "D", instructions: "I." }, + ], + }, + stubNeurolink, + ); + throw new Error("Should have thrown on empty name"); + } catch (err) { + assertContains( + String(err), + "name", + "Error should mention name field", + ); + } + }, + ), + ); + } + + // MessageBus: pub/sub without I/O + if (MessageBusCtor) { + results.push( + await runTest("MessageBus publish and subscribe", async () => { + const bus = new MessageBusCtor!(); + const received: unknown[] = []; + bus.subscribe("test-topic", "test-subscriber", (msg) => { + received.push(msg); + }); + await bus.publish("test-topic", "test-publisher", { data: "hello" }); + // Allow microtask queue to flush + await new Promise((resolve) => setTimeout(resolve, 0)); + assert( + received.length >= 1, + "MessageBus should deliver published message to subscriber", + ); + assert( + (received[0] as { payload: { data: string } }).payload.data === + "hello", + "MessageBus should deliver correct payload", + ); + }), + ); + } + + const duration = Date.now() - startTime; + return { + name: "Integration Tests", + tests: results, + passed: results.filter((r) => r.status === "PASS").length, + failed: results.filter((r) => r.status === "FAIL").length, + skipped: results.filter((r) => r.status === "SKIP").length, + duration, + }; +} + +/** + * Test Suite: CLI Coverage Report + * + * Verifies that the Multi-Agent Networks CLI subcommands (`agent`, `network`) + * are registered and respond to --help. Does not execute create/execute commands + * since those require arguments and a running process-local agent registry. + */ +async function testCLICoverage(): Promise { + logSection("CLI COVERAGE REPORT"); + const results: TestResult[] = []; + const startTime = Date.now(); + + log( + "\n CLI COVERAGE: Verifying Multi-Agent Networks CLI subcommands.", + "cyan", + ); + log("", "reset"); + + // Check that the CLI entry point exists (real check, not a skip) + results.push( + await runTest("CLI entry point exists (dist/cli/index.js)", async () => { + const cliPath = path.join(__dirname, "../dist/cli/index.js"); + const cliExists = fs.existsSync(cliPath); + // Not a hard failure — dev may not have built yet; mark as SKIP if missing + if (!cliExists) { + throw new Error( + "dist/cli/index.js not found — run `pnpm build` before this check", + ); + } + }), + ); + + const cliPath = path.join(__dirname, "../dist/cli/index.js"); + + results.push( + await runTest("CLI: neurolink agent --help responds", async () => { + const output = execFileSync("node", [cliPath, "agent", "--help"], { + encoding: "utf-8", + timeout: 10000, + }); + assert( + output.includes("agent"), + "CLI agent --help should mention agent subcommand", + ); + }), + ); + + results.push( + await runTest("CLI: neurolink network --help responds", async () => { + const output = execFileSync("node", [cliPath, "network", "--help"], { + encoding: "utf-8", + timeout: 10000, + }); + assert( + output.includes("network"), + "CLI network --help should mention network subcommand", + ); + }), + ); + + results.push( + await runTest("CLI: neurolink agent create --help responds", async () => { + const output = execFileSync( + "node", + [cliPath, "agent", "create", "--help"], + { encoding: "utf-8", timeout: 10000 }, + ); + assert( + output.includes("create"), + "CLI agent create --help should mention create", + ); + }), + ); + + results.push( + await runTest("CLI: neurolink network create --help responds", async () => { + const output = execFileSync( + "node", + [cliPath, "network", "create", "--help"], + { encoding: "utf-8", timeout: 10000 }, + ); + assert( + output.includes("create"), + "CLI network create --help should mention create", + ); + }), + ); + + const duration = Date.now() - startTime; + return { + name: "CLI Coverage Report", + tests: results, + passed: results.filter((r) => r.status === "PASS").length, + failed: results.filter((r) => r.status === "FAIL").length, + skipped: results.filter((r) => r.status === "SKIP").length, + duration, + }; +} + +// ============================================================================ +// Main Execution +// ============================================================================ + +async function runAllTests(): Promise { + const startTime = Date.now(); + const suiteResults: TestSuiteResult[] = []; + + log("\n", "reset"); + log( + "╔══════════════════════════════════════════════════════════════════════╗", + "cyan", + ); + log( + "║ MULTI-AGENT NETWORKS - CONTINUOUS INTEGRATION TEST SUITE ║", + "cyan", + ); + log( + "╚══════════════════════════════════════════════════════════════════════╝", + "cyan", + ); + log("", "reset"); + log(`Provider: ${TEST_CONFIG.provider}`, "reset"); + log(`Model: ${TEST_CONFIG.model || "default"}`, "reset"); + log(`Verbose: ${TEST_CONFIG.verbose}`, "reset"); + log("", "reset"); + + try { + // Run all test suites + suiteResults.push(await testAgentClass()); + suiteResults.push(await testNetworkTopologies()); + suiteResults.push(await testRoutingRules()); + suiteResults.push(await testMessageBus()); + suiteResults.push(await testIntegration()); + suiteResults.push(await testCLICoverage()); + } catch (error) { + log(`\n❌ Fatal error running tests: ${error}`, "red"); + process.exit(1); + } + + // Summary + const totalDuration = Date.now() - startTime; + const totalPassed = suiteResults.reduce((sum, s) => sum + s.passed, 0); + const totalFailed = suiteResults.reduce((sum, s) => sum + s.failed, 0); + const totalSkipped = suiteResults.reduce((sum, s) => sum + s.skipped, 0); + const totalTests = totalPassed + totalFailed + totalSkipped; + + logSection("TEST SUMMARY"); + + log("\nSuite Results:", "bright"); + for (const suite of suiteResults) { + const status = + suite.failed === 0 + ? "✅" + : suite.failed > 0 && suite.passed > 0 + ? "⚠️" + : "❌"; + log( + `${status} ${suite.name}: ${suite.passed}/${suite.tests.length} passed (${suite.duration}ms)`, + suite.failed === 0 ? "green" : "yellow", + ); + } + + log("\n" + "=".repeat(70), "cyan"); + log(`TOTAL: ${totalTests} tests`, "bright"); + log(` ✅ Passed: ${totalPassed}`, "green"); + log(` ❌ Failed: ${totalFailed}`, totalFailed > 0 ? "red" : "green"); + log(` ⏭️ Skipped: ${totalSkipped}`, "yellow"); + log(` ⏱️ Duration: ${totalDuration}ms`, "reset"); + log("=".repeat(70), "cyan"); + + // Exit with appropriate code + if (totalFailed > 0) { + log("\n❌ Some tests failed!", "red"); + process.exit(1); + } else { + log("\n✅ All tests passed!", "green"); + process.exit(0); + } +} + +// Run if executed directly +runAllTests().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); diff --git a/test/fixtures/agents/agent-definitions.json b/test/fixtures/agents/agent-definitions.json new file mode 100644 index 000000000..671ac4be5 --- /dev/null +++ b/test/fixtures/agents/agent-definitions.json @@ -0,0 +1,242 @@ +{ + "description": "Test fixtures for Multi-Agent Network agents", + "version": "1.0.0", + "agents": { + "basicAgent": { + "id": "test-basic-agent", + "name": "Basic Test Agent", + "description": "A simple agent for basic testing scenarios", + "instructions": "You are a helpful assistant that provides concise answers." + }, + "fullConfigAgent": { + "id": "test-full-agent", + "name": "Full Configuration Agent", + "description": "A fully configured agent with all options specified", + "instructions": "You are a comprehensive test assistant with full configuration.", + "provider": "openai", + "model": "gpt-4o-mini", + "tools": ["readFile", "writeFile", "searchCode"], + "maxSteps": 10, + "temperature": 0.5, + "canDelegate": true, + "metadata": { + "category": "testing", + "priority": "high", + "capabilities": ["file-operations", "code-search"] + } + }, + "codeAnalysisAgent": { + "id": "code-analyzer", + "name": "Code Analysis Agent", + "description": "Analyzes code for patterns, bugs, and improvements", + "instructions": "You are an expert code analyst. Examine code carefully and provide detailed analysis including potential bugs, code smells, and improvement suggestions.", + "provider": "anthropic", + "model": "claude-3-5-sonnet-20241022", + "tools": ["readFile", "searchCode", "analyzeAST"], + "maxSteps": 15, + "temperature": 0.3, + "canDelegate": false, + "metadata": { + "expertise": ["typescript", "javascript", "python"], + "analysisTypes": ["bugs", "security", "performance"] + } + }, + "dataProcessingAgent": { + "id": "data-processor", + "name": "Data Processing Agent", + "description": "Processes and transforms data in various formats", + "instructions": "You are a data processing specialist. Transform, validate, and analyze data according to specifications.", + "provider": "vertex", + "model": "gemini-2.0-flash", + "tools": ["readFile", "writeFile", "parseCSV", "transformJSON"], + "maxSteps": 20, + "temperature": 0.2, + "canDelegate": true, + "metadata": { + "formats": ["json", "csv", "xml", "yaml"], + "maxDataSize": "10MB" + } + }, + "researchAgent": { + "id": "researcher", + "name": "Research Agent", + "description": "Conducts research and gathers information from various sources", + "instructions": "You are a thorough researcher. Gather information, synthesize findings, and present comprehensive summaries.", + "provider": "google-ai-studio", + "model": "gemini-2.5-flash", + "tools": ["webSearch", "readFile", "summarize"], + "maxSteps": 25, + "temperature": 0.6, + "canDelegate": true, + "metadata": { + "domains": ["technology", "science", "business"], + "outputFormats": ["summary", "report", "bullets"] + } + }, + "coordinatorAgent": { + "id": "coordinator", + "name": "Coordinator Agent", + "description": "Coordinates tasks between multiple agents and manages workflows", + "instructions": "You are a task coordinator. Break down complex tasks, delegate to appropriate agents, and synthesize results.", + "provider": "openai", + "model": "gpt-4o", + "tools": ["delegateTask", "aggregateResults", "trackProgress"], + "maxSteps": 30, + "temperature": 0.4, + "canDelegate": true, + "metadata": { + "role": "coordinator", + "maxDelegations": 5, + "priorityLevels": ["critical", "high", "medium", "low"] + } + }, + "validationAgent": { + "id": "validator", + "name": "Validation Agent", + "description": "Validates inputs, outputs, and data integrity", + "instructions": "You are a validation specialist. Verify data integrity, check constraints, and ensure compliance with schemas.", + "provider": "anthropic", + "model": "claude-3-5-haiku-20241022", + "tools": ["validateSchema", "checkConstraints", "verifyFormat"], + "maxSteps": 5, + "temperature": 0.1, + "canDelegate": false, + "metadata": { + "strictMode": true, + "validationRules": ["schema", "format", "range", "uniqueness"] + } + }, + "minimalAgent": { + "id": "minimal", + "name": "Minimal Agent", + "description": "Minimal configuration for edge case testing", + "instructions": "Respond briefly." + }, + "streamingAgent": { + "id": "streamer", + "name": "Streaming Agent", + "description": "Agent optimized for streaming responses", + "instructions": "You provide detailed, progressive responses suitable for streaming output.", + "provider": "openai", + "model": "gpt-4o-mini", + "maxSteps": 5, + "temperature": 0.7, + "canDelegate": false, + "metadata": { + "streamOptimized": true, + "chunkSize": "sentence" + } + }, + "errorProneAgent": { + "id": "error-prone", + "name": "Error Prone Agent", + "description": "Agent configured to test error handling scenarios", + "instructions": "You may encounter errors during execution.", + "provider": "invalid-provider", + "model": "non-existent-model", + "maxSteps": 1, + "temperature": 0.0, + "metadata": { + "testType": "error-handling", + "expectedErrors": ["provider-not-found", "model-unavailable"] + } + } + }, + "inputSchemas": { + "analysisInput": { + "type": "object", + "properties": { + "code": { "type": "string", "description": "Code to analyze" }, + "language": { + "type": "string", + "enum": ["typescript", "javascript", "python"] + }, + "analysisType": { + "type": "string", + "enum": ["bugs", "security", "performance", "all"] + } + }, + "required": ["code", "language"] + }, + "dataProcessingInput": { + "type": "object", + "properties": { + "data": { + "type": ["string", "object", "array"], + "description": "Data to process" + }, + "format": { "type": "string", "enum": ["json", "csv", "xml"] }, + "operations": { + "type": "array", + "items": { "type": "string" }, + "description": "List of operations to perform" + } + }, + "required": ["data", "format"] + }, + "researchInput": { + "type": "object", + "properties": { + "topic": { "type": "string", "description": "Research topic" }, + "depth": { "type": "string", "enum": ["shallow", "medium", "deep"] }, + "sources": { + "type": "array", + "items": { "type": "string" }, + "description": "Preferred sources" + } + }, + "required": ["topic"] + } + }, + "outputSchemas": { + "analysisOutput": { + "type": "object", + "properties": { + "issues": { + "type": "array", + "items": { + "type": "object", + "properties": { + "severity": { + "type": "string", + "enum": ["critical", "high", "medium", "low"] + }, + "type": { "type": "string" }, + "message": { "type": "string" }, + "line": { "type": "number" } + } + } + }, + "suggestions": { "type": "array", "items": { "type": "string" } }, + "score": { "type": "number", "minimum": 0, "maximum": 100 } + } + }, + "researchOutput": { + "type": "object", + "properties": { + "summary": { "type": "string" }, + "keyFindings": { "type": "array", "items": { "type": "string" } }, + "sources": { "type": "array", "items": { "type": "string" } }, + "confidence": { "type": "number", "minimum": 0, "maximum": 1 } + } + } + }, + "testInputs": { + "simpleText": "Hello, can you help me?", + "codeSnippet": "function add(a, b) { return a + b; }", + "complexTask": "Analyze this codebase and identify potential security vulnerabilities, then propose fixes.", + "dataTransform": { + "data": [ + { "id": 1, "name": "Alice" }, + { "id": 2, "name": "Bob" } + ], + "format": "json", + "operations": ["filter", "sort", "transform"] + }, + "researchQuery": { + "topic": "AI agent orchestration patterns", + "depth": "deep", + "sources": ["academic", "industry"] + } + } +} diff --git a/test/fixtures/agents/messages.json b/test/fixtures/agents/messages.json new file mode 100644 index 000000000..9e35ceba7 --- /dev/null +++ b/test/fixtures/agents/messages.json @@ -0,0 +1,514 @@ +{ + "description": "Test fixtures for MessageBus inter-agent communication", + "version": "1.0.0", + "messageTypes": { + "task": { + "description": "Task assignment message", + "schema": { + "type": "task", + "taskId": "string", + "fromAgent": "string", + "toAgent": "string", + "payload": "object", + "priority": "number", + "deadline": "string (ISO date)" + } + }, + "result": { + "description": "Task result message", + "schema": { + "type": "result", + "taskId": "string", + "fromAgent": "string", + "toAgent": "string", + "result": "any", + "status": "string", + "duration": "number" + } + }, + "status": { + "description": "Agent status message", + "schema": { + "type": "status", + "fromAgent": "string", + "state": "string", + "load": "number", + "availableCapacity": "number" + } + }, + "broadcast": { + "description": "Broadcast message to all agents", + "schema": { + "type": "broadcast", + "fromAgent": "string", + "topic": "string", + "payload": "any" + } + }, + "request": { + "description": "Request-response message", + "schema": { + "type": "request", + "requestId": "string", + "fromAgent": "string", + "toAgent": "string", + "method": "string", + "params": "object", + "timeout": "number" + } + }, + "response": { + "description": "Response to a request", + "schema": { + "type": "response", + "requestId": "string", + "fromAgent": "string", + "toAgent": "string", + "result": "any", + "error": "string (optional)" + } + }, + "event": { + "description": "Event notification", + "schema": { + "type": "event", + "eventId": "string", + "fromAgent": "string", + "eventType": "string", + "payload": "any", + "timestamp": "string (ISO date)" + } + } + }, + "testMessages": { + "taskMessages": [ + { + "id": "task-001", + "type": "task", + "taskId": "analyze-code-001", + "fromAgent": "coordinator", + "toAgent": "code-analyzer", + "payload": { + "code": "function add(a, b) { return a + b; }", + "language": "javascript", + "analysisType": "all" + }, + "priority": 1, + "deadline": "2025-01-31T14:00:00Z" + }, + { + "id": "task-002", + "type": "task", + "taskId": "process-data-001", + "fromAgent": "coordinator", + "toAgent": "data-processor", + "payload": { + "data": [ + { "id": 1, "value": 100 }, + { "id": 2, "value": 200 } + ], + "format": "json", + "operations": ["filter", "transform"] + }, + "priority": 2, + "deadline": "2025-01-31T15:00:00Z" + }, + { + "id": "task-003", + "type": "task", + "taskId": "research-001", + "fromAgent": "coordinator", + "toAgent": "researcher", + "payload": { + "topic": "Multi-agent AI systems", + "depth": "deep" + }, + "priority": 3, + "deadline": "2025-01-31T16:00:00Z" + } + ], + "resultMessages": [ + { + "id": "result-001", + "type": "result", + "taskId": "analyze-code-001", + "fromAgent": "code-analyzer", + "toAgent": "coordinator", + "result": { + "issues": [], + "suggestions": ["Consider adding type annotations"], + "score": 85 + }, + "status": "success", + "duration": 1250 + }, + { + "id": "result-002", + "type": "result", + "taskId": "process-data-001", + "fromAgent": "data-processor", + "toAgent": "coordinator", + "result": { + "processedData": [{ "id": 1, "value": 100, "status": "active" }], + "recordsProcessed": 2, + "recordsFiltered": 1 + }, + "status": "success", + "duration": 850 + }, + { + "id": "result-003", + "type": "result", + "taskId": "research-001", + "fromAgent": "researcher", + "toAgent": "coordinator", + "result": { + "summary": "Multi-agent AI systems involve...", + "keyFindings": ["Finding 1", "Finding 2"], + "sources": ["source1.com", "source2.org"] + }, + "status": "success", + "duration": 3500 + }, + { + "id": "result-004", + "type": "result", + "taskId": "failed-task-001", + "fromAgent": "error-prone", + "toAgent": "coordinator", + "result": null, + "status": "error", + "error": "Provider unavailable", + "duration": 150 + } + ], + "statusMessages": [ + { + "id": "status-001", + "type": "status", + "fromAgent": "code-analyzer", + "state": "idle", + "load": 0.2, + "availableCapacity": 0.8 + }, + { + "id": "status-002", + "type": "status", + "fromAgent": "data-processor", + "state": "executing", + "load": 0.6, + "availableCapacity": 0.4 + }, + { + "id": "status-003", + "type": "status", + "fromAgent": "researcher", + "state": "idle", + "load": 0.1, + "availableCapacity": 0.9 + }, + { + "id": "status-004", + "type": "status", + "fromAgent": "error-prone", + "state": "error", + "load": 0, + "availableCapacity": 0, + "error": "Agent in error state" + } + ], + "broadcastMessages": [ + { + "id": "broadcast-001", + "type": "broadcast", + "fromAgent": "coordinator", + "topic": "status-check", + "payload": { + "requestId": "check-001", + "respondBy": "2025-01-31T14:30:00Z" + } + }, + { + "id": "broadcast-002", + "type": "broadcast", + "fromAgent": "coordinator", + "topic": "config-update", + "payload": { + "setting": "maxSteps", + "newValue": 15 + } + }, + { + "id": "broadcast-003", + "type": "broadcast", + "fromAgent": "coordinator", + "topic": "shutdown-notice", + "payload": { + "reason": "maintenance", + "shutdownTime": "2025-01-31T18:00:00Z" + } + } + ], + "requestResponsePairs": [ + { + "request": { + "id": "request-001", + "type": "request", + "requestId": "req-001", + "fromAgent": "coordinator", + "toAgent": "code-analyzer", + "method": "getCapabilities", + "params": {}, + "timeout": 5000 + }, + "response": { + "id": "response-001", + "type": "response", + "requestId": "req-001", + "fromAgent": "code-analyzer", + "toAgent": "coordinator", + "result": { + "capabilities": ["analyze", "lint", "security-scan"], + "supportedLanguages": ["typescript", "javascript", "python"] + } + } + }, + { + "request": { + "id": "request-002", + "type": "request", + "requestId": "req-002", + "fromAgent": "data-processor", + "toAgent": "validator", + "method": "validateSchema", + "params": { + "data": { "id": 1, "name": "test" }, + "schema": "userSchema" + }, + "timeout": 3000 + }, + "response": { + "id": "response-002", + "type": "response", + "requestId": "req-002", + "fromAgent": "validator", + "toAgent": "data-processor", + "result": { + "valid": true, + "errors": [] + } + } + }, + { + "request": { + "id": "request-003", + "type": "request", + "requestId": "req-003", + "fromAgent": "researcher", + "toAgent": "error-prone", + "method": "process", + "params": {}, + "timeout": 2000 + }, + "response": { + "id": "response-003", + "type": "response", + "requestId": "req-003", + "fromAgent": "error-prone", + "toAgent": "researcher", + "result": null, + "error": "Agent unavailable" + } + } + ], + "eventMessages": [ + { + "id": "event-001", + "type": "event", + "eventId": "evt-001", + "fromAgent": "code-analyzer", + "eventType": "task-started", + "payload": { + "taskId": "analyze-code-001" + }, + "timestamp": "2025-01-31T14:00:00Z" + }, + { + "id": "event-002", + "type": "event", + "eventId": "evt-002", + "fromAgent": "code-analyzer", + "eventType": "task-completed", + "payload": { + "taskId": "analyze-code-001", + "duration": 1250 + }, + "timestamp": "2025-01-31T14:00:01.250Z" + }, + { + "id": "event-003", + "type": "event", + "eventId": "evt-003", + "fromAgent": "error-prone", + "eventType": "error", + "payload": { + "errorType": "ProviderError", + "message": "Provider unavailable", + "recoverable": false + }, + "timestamp": "2025-01-31T14:01:00Z" + } + ] + }, + "subscriptionPatterns": { + "allMessages": { + "pattern": "*", + "description": "Subscribe to all messages" + }, + "taskOnly": { + "pattern": "task:*", + "description": "Subscribe to all task messages" + }, + "specificAgent": { + "pattern": "from:code-analyzer", + "description": "Subscribe to messages from specific agent" + }, + "topicBased": { + "pattern": "topic:status-check", + "description": "Subscribe to specific topic broadcasts" + }, + "eventType": { + "pattern": "event:task-completed", + "description": "Subscribe to specific event types" + } + }, + "priorityLevels": { + "critical": { + "level": 0, + "description": "Highest priority - processed immediately", + "timeout": 5000 + }, + "high": { + "level": 1, + "description": "High priority - processed before normal", + "timeout": 10000 + }, + "normal": { + "level": 2, + "description": "Normal priority - standard processing", + "timeout": 30000 + }, + "low": { + "level": 3, + "description": "Low priority - processed when capacity available", + "timeout": 60000 + }, + "background": { + "level": 4, + "description": "Background priority - processed during idle time", + "timeout": 300000 + } + }, + "testScenarios": { + "basicPubSub": { + "description": "Basic publish-subscribe pattern", + "steps": [ + { "action": "subscribe", "agent": "coordinator", "topic": "results" }, + { + "action": "publish", + "agent": "code-analyzer", + "topic": "results", + "message": "result-001" + }, + { "action": "verify", "agent": "coordinator", "received": "result-001" } + ] + }, + "requestResponse": { + "description": "Request-response pattern with timeout", + "steps": [ + { + "action": "request", + "from": "coordinator", + "to": "code-analyzer", + "method": "getCapabilities" + }, + { + "action": "respond", + "from": "code-analyzer", + "to": "coordinator", + "result": "capabilities" + }, + { + "action": "verify", + "agent": "coordinator", + "received": "capabilities" + } + ] + }, + "broadcast": { + "description": "Broadcast to all agents", + "steps": [ + { + "action": "subscribe", + "agents": ["code-analyzer", "data-processor", "researcher"], + "topic": "broadcast" + }, + { + "action": "broadcast", + "agent": "coordinator", + "message": "broadcast-001" + }, + { + "action": "verify", + "agents": ["code-analyzer", "data-processor", "researcher"], + "received": "broadcast-001" + } + ] + }, + "priorityQueue": { + "description": "Messages processed by priority", + "steps": [ + { "action": "send", "message": "task-002", "priority": 2 }, + { "action": "send", "message": "task-001", "priority": 1 }, + { "action": "send", "message": "task-003", "priority": 3 }, + { + "action": "verify-order", + "expected": ["task-001", "task-002", "task-003"] + } + ] + }, + "errorHandling": { + "description": "Handle message delivery errors", + "steps": [ + { "action": "send", "to": "non-existent-agent", "message": "task-001" }, + { "action": "verify", "error": "agent-not-found" } + ] + }, + "timeout": { + "description": "Handle request timeout", + "steps": [ + { + "action": "request", + "from": "coordinator", + "to": "slow-agent", + "timeout": 100 + }, + { "action": "verify", "error": "timeout" } + ] + } + }, + "deliveryGuarantees": { + "atMostOnce": { + "description": "Message delivered zero or one time", + "useCase": "Non-critical notifications" + }, + "atLeastOnce": { + "description": "Message delivered one or more times", + "useCase": "Important tasks that can handle duplicates" + }, + "exactlyOnce": { + "description": "Message delivered exactly one time", + "useCase": "Critical operations like financial transactions" + } + } +} diff --git a/test/fixtures/agents/network-topologies.json b/test/fixtures/agents/network-topologies.json new file mode 100644 index 000000000..ea36c93b8 --- /dev/null +++ b/test/fixtures/agents/network-topologies.json @@ -0,0 +1,273 @@ +{ + "description": "Test fixtures for Multi-Agent Network topologies", + "version": "1.0.0", + "networks": { + "simpleHubSpoke": { + "id": "simple-hub-spoke", + "name": "Simple Hub-Spoke Network", + "description": "Basic hub-spoke topology with 1 hub and 3 spokes", + "topology": "hub-spoke", + "config": { + "hubAgentId": "coordinator", + "spokeAgentIds": ["code-analyzer", "data-processor", "researcher"], + "loadBalancing": "round-robin", + "maxConcurrentTasksPerSpoke": 2 + }, + "agents": ["coordinator", "code-analyzer", "data-processor", "researcher"] + }, + "advancedHubSpoke": { + "id": "advanced-hub-spoke", + "name": "Advanced Hub-Spoke Network", + "description": "Hub-spoke with priority routing and failover", + "topology": "hub-spoke", + "config": { + "hubAgentId": "coordinator", + "spokeAgentIds": [ + "code-analyzer", + "data-processor", + "researcher", + "validator" + ], + "loadBalancing": "least-loaded", + "maxConcurrentTasksPerSpoke": 5, + "failoverEnabled": true, + "priorityRouting": true, + "healthCheckInterval": 5000 + }, + "agents": [ + "coordinator", + "code-analyzer", + "data-processor", + "researcher", + "validator" + ] + }, + "simpleMesh": { + "id": "simple-mesh", + "name": "Simple Mesh Network", + "description": "Basic mesh topology where all agents can communicate", + "topology": "mesh", + "config": { + "agentIds": ["code-analyzer", "data-processor", "researcher"], + "autoDiscovery": true, + "maxHops": 2, + "enableP2PDelegation": true + }, + "agents": ["code-analyzer", "data-processor", "researcher"] + }, + "secureMesh": { + "id": "secure-mesh", + "name": "Secure Mesh Network", + "description": "Mesh with access controls and audit logging", + "topology": "mesh", + "config": { + "agentIds": [ + "code-analyzer", + "data-processor", + "researcher", + "validator" + ], + "autoDiscovery": false, + "maxHops": 3, + "enableP2PDelegation": true, + "accessControl": { + "code-analyzer": ["data-processor", "validator"], + "data-processor": ["code-analyzer", "researcher"], + "researcher": ["data-processor"], + "validator": ["code-analyzer", "data-processor", "researcher"] + }, + "auditLogging": true + }, + "agents": ["code-analyzer", "data-processor", "researcher", "validator"] + }, + "simpleHierarchical": { + "id": "simple-hierarchical", + "name": "Simple Hierarchical Network", + "description": "Two-level hierarchy with coordinator at top", + "topology": "hierarchical", + "config": { + "rootAgentId": "coordinator", + "levels": [ + { + "level": 1, + "agents": ["code-analyzer", "researcher"] + }, + { + "level": 2, + "agents": ["data-processor", "validator"] + } + ], + "allowCrossLevel": false, + "autoEscalation": true + }, + "agents": [ + "coordinator", + "code-analyzer", + "researcher", + "data-processor", + "validator" + ] + }, + "complexHierarchical": { + "id": "complex-hierarchical", + "name": "Complex Hierarchical Network", + "description": "Multi-level hierarchy with cross-level communication", + "topology": "hierarchical", + "config": { + "rootAgentId": "coordinator", + "levels": [ + { + "level": 1, + "agents": ["code-analyzer", "researcher"], + "canDelegate": true + }, + { + "level": 2, + "agents": ["data-processor"], + "canDelegate": true + }, + { + "level": 3, + "agents": ["validator"], + "canDelegate": false + } + ], + "allowCrossLevel": true, + "autoEscalation": true, + "escalationThreshold": 0.7, + "maxEscalationDepth": 2 + }, + "agents": [ + "coordinator", + "code-analyzer", + "researcher", + "data-processor", + "validator" + ] + }, + "minimalNetwork": { + "id": "minimal-network", + "name": "Minimal Network", + "description": "Single agent network for edge case testing", + "topology": "hub-spoke", + "config": { + "hubAgentId": "minimal", + "spokeAgentIds": [] + }, + "agents": ["minimal"] + }, + "errorTestNetwork": { + "id": "error-test-network", + "name": "Error Test Network", + "description": "Network configured to test error scenarios", + "topology": "mesh", + "config": { + "agentIds": ["error-prone", "validator"], + "autoDiscovery": false, + "maxHops": 1 + }, + "agents": ["error-prone", "validator"] + } + }, + "routerConfigs": { + "defaultRouter": { + "type": "semantic", + "confidenceThreshold": 0.7, + "fallbackAgent": "coordinator", + "maxRetries": 3 + }, + "strictRouter": { + "type": "rule-based", + "confidenceThreshold": 0.9, + "fallbackAgent": null, + "maxRetries": 1, + "rules": [ + { + "pattern": "code|analyze|bug|security", + "targetAgent": "code-analyzer", + "priority": 1 + }, + { + "pattern": "data|transform|csv|json", + "targetAgent": "data-processor", + "priority": 2 + }, + { + "pattern": "research|find|search|information", + "targetAgent": "researcher", + "priority": 3 + } + ] + }, + "hybridRouter": { + "type": "hybrid", + "confidenceThreshold": 0.75, + "fallbackAgent": "coordinator", + "maxRetries": 2, + "useSemanticFallback": true + } + }, + "networkDefaults": { + "standard": { + "maxConcurrentTasks": 10, + "taskTimeout": 30000, + "retryPolicy": { + "maxRetries": 3, + "backoffMultiplier": 2, + "initialDelay": 1000 + } + }, + "highThroughput": { + "maxConcurrentTasks": 50, + "taskTimeout": 15000, + "retryPolicy": { + "maxRetries": 1, + "backoffMultiplier": 1, + "initialDelay": 500 + } + }, + "reliable": { + "maxConcurrentTasks": 5, + "taskTimeout": 60000, + "retryPolicy": { + "maxRetries": 5, + "backoffMultiplier": 3, + "initialDelay": 2000 + } + } + }, + "testScenarios": { + "singleAgentTask": { + "description": "Task handled by a single agent", + "input": "Analyze this simple function", + "expectedRoute": "code-analyzer", + "expectedHops": 1 + }, + "multiAgentCollaboration": { + "description": "Task requiring multiple agents", + "input": "Research best practices, then analyze our codebase against them", + "expectedAgents": ["researcher", "code-analyzer"], + "expectedHops": 2 + }, + "delegationChain": { + "description": "Task with delegation chain", + "input": "Coordinate a full code review including data validation", + "expectedChain": ["coordinator", "code-analyzer", "validator"], + "expectedHops": 3 + }, + "failoverScenario": { + "description": "Task with failover when primary agent fails", + "input": "Process this data (primary unavailable)", + "network": "errorTestNetwork", + "primaryAgent": "error-prone", + "fallbackAgent": "validator", + "note": "Both agents are co-located in errorTestNetwork (agents: error-prone, validator)" + }, + "broadcastTask": { + "description": "Task broadcast to all agents", + "input": "Status check", + "broadcast": true, + "expectedResponses": "all" + } + } +} diff --git a/test/fixtures/agents/routing-rules.json b/test/fixtures/agents/routing-rules.json new file mode 100644 index 000000000..f18068b80 --- /dev/null +++ b/test/fixtures/agents/routing-rules.json @@ -0,0 +1,311 @@ +{ + "description": "Test fixtures for Multi-Agent Network routing — routing is implemented via LLM system prompt (agents-as-tools pattern), not a RouterAgent class", + "version": "2.0.0", + "routingRules": { + "codeAnalysis": { + "id": "route-code-analysis", + "name": "Code Analysis Routing", + "patterns": [ + "analyze code", + "find bugs", + "security scan", + "code review", + "lint", + "static analysis" + ], + "keywords": [ + "code", + "function", + "class", + "bug", + "vulnerability", + "security" + ], + "targetAgent": "code-analyzer", + "priority": 1, + "confidence": 0.9 + }, + "dataProcessing": { + "id": "route-data-processing", + "name": "Data Processing Routing", + "patterns": [ + "transform data", + "parse csv", + "convert json", + "process file", + "data pipeline" + ], + "keywords": [ + "data", + "csv", + "json", + "xml", + "transform", + "parse", + "convert" + ], + "targetAgent": "data-processor", + "priority": 2, + "confidence": 0.85 + }, + "research": { + "id": "route-research", + "name": "Research Routing", + "patterns": [ + "research topic", + "find information", + "search for", + "investigate", + "gather data" + ], + "keywords": [ + "research", + "find", + "search", + "investigate", + "information", + "learn" + ], + "targetAgent": "researcher", + "priority": 3, + "confidence": 0.8 + }, + "validation": { + "id": "route-validation", + "name": "Validation Routing", + "patterns": [ + "validate input", + "check schema", + "verify format", + "ensure compliance" + ], + "keywords": [ + "validate", + "verify", + "check", + "schema", + "format", + "compliance" + ], + "targetAgent": "validator", + "priority": 4, + "confidence": 0.85 + }, + "coordination": { + "id": "route-coordination", + "name": "Coordination Routing", + "patterns": [ + "coordinate tasks", + "manage workflow", + "orchestrate", + "delegate work" + ], + "keywords": [ + "coordinate", + "manage", + "orchestrate", + "delegate", + "workflow", + "plan" + ], + "targetAgent": "coordinator", + "priority": 5, + "confidence": 0.75 + } + }, + "routingDecisions": { + "simpleMatch": { + "input": "Analyze this TypeScript function for bugs", + "expectedDecision": { + "selectedAgent": "code-analyzer", + "confidence": 0.95, + "reasoning": "Input contains 'analyze' and 'bugs' keywords matching code analysis patterns", + "alternativeAgents": [] + } + }, + "ambiguousMatch": { + "input": "Check the data format in this code", + "expectedDecision": { + "selectedAgent": "validator", + "confidence": 0.7, + "reasoning": "Input matches both validation and code analysis patterns", + "alternativeAgents": ["code-analyzer", "data-processor"] + } + }, + "multiAgentTask": { + "input": "Research best practices then analyze our codebase", + "expectedDecision": { + "selectedAgent": "coordinator", + "confidence": 0.85, + "reasoning": "Complex task requiring multiple agents should be coordinated", + "decomposedTasks": [ + { "agent": "researcher", "task": "Research best practices" }, + { + "agent": "code-analyzer", + "task": "Analyze codebase against best practices" + } + ] + } + }, + "noMatch": { + "input": "Tell me a joke about programming", + "expectedDecision": { + "selectedAgent": "coordinator", + "confidence": 0.5, + "reasoning": "No specific routing pattern matched, falling back to coordinator", + "isFallback": true + } + }, + "highConfidence": { + "input": "Scan this code for SQL injection vulnerabilities", + "expectedDecision": { + "selectedAgent": "code-analyzer", + "confidence": 0.98, + "reasoning": "Strong match for security-related code analysis", + "alternativeAgents": [] + } + }, + "lowConfidence": { + "input": "Help me with this", + "expectedDecision": { + "selectedAgent": "coordinator", + "confidence": 0.3, + "reasoning": "Vague input with no clear routing signals", + "isFallback": true, + "requiresClarification": true + } + } + }, + "confidenceThresholds": { + "high": { + "threshold": 0.9, + "action": "route-directly", + "description": "High confidence - route directly to target agent" + }, + "medium": { + "threshold": 0.7, + "action": "route-with-monitoring", + "description": "Medium confidence - route but monitor for potential re-routing" + }, + "low": { + "threshold": 0.5, + "action": "request-clarification", + "description": "Low confidence - may need user clarification" + }, + "very-low": { + "threshold": 0.3, + "action": "fallback-to-coordinator", + "description": "Very low confidence - use fallback coordinator" + } + }, + "routingStrategies": { + "system-prompt": { + "description": "Routing is implemented as a system prompt — each agent is exposed as an ai SDK tool and the router model picks which agent tool(s) to call", + "implementation": "AgentNetwork.buildRouterSystemPrompt() + AgentNetwork.buildAgentTools()", + "pros": [ + "Handles novel inputs", + "Context-aware", + "Flexible", + "No separate RouterAgent class needed" + ], + "cons": ["Higher latency on first hop", "Requires LLM call"] + }, + "custom-instructions": { + "description": "Supply RouterConfig.instructions to override the default routing prompt", + "implementation": "AgentNetworkConfig.router.instructions", + "pros": ["Domain-specific guidance", "Predictable for known patterns"], + "cons": ["Requires prompt maintenance"] + } + }, + "testCases": { + "exactPatternMatch": [ + { + "input": "analyze code", + "expectedAgent": "code-analyzer", + "expectedConfidence": 0.9 + }, + { + "input": "transform data", + "expectedAgent": "data-processor", + "expectedConfidence": 0.85 + }, + { + "input": "research topic", + "expectedAgent": "researcher", + "expectedConfidence": 0.8 + }, + { + "input": "validate input", + "expectedAgent": "validator", + "expectedConfidence": 0.85 + } + ], + "keywordMatch": [ + { "input": "Find bugs in my function", "expectedAgent": "code-analyzer" }, + { + "input": "Convert this CSV to JSON", + "expectedAgent": "data-processor" + }, + { "input": "I need information about AI", "expectedAgent": "researcher" }, + { "input": "Check if this schema is valid", "expectedAgent": "validator" } + ], + "negativeTests": [ + { "input": "", "shouldFail": true, "reason": "Empty input" }, + { "input": " ", "shouldFail": true, "reason": "Whitespace only" }, + { "input": null, "shouldFail": true, "reason": "Null input" } + ], + "edgeCases": [ + { + "input": "analyze data in code", + "description": "Multiple keyword matches", + "acceptableAgents": ["code-analyzer", "data-processor"] + }, + { + "input": "ANALYZE CODE", + "description": "Case insensitivity test", + "expectedAgent": "code-analyzer" + }, + { + "input": "Can you analyze this code?", + "description": "Question format", + "expectedAgent": "code-analyzer" + }, + { + "input": "I want to analyze code and then validate the results", + "description": "Multi-step task", + "expectedAgent": "coordinator" + } + ], + "priorityTests": [ + { + "input": "analyze and validate code", + "description": "Both code-analyzer and validator match", + "expectedAgent": "code-analyzer", + "reason": "code-analyzer has higher priority (1) than validator (4)" + }, + { + "input": "process data and research findings", + "description": "Both data-processor and researcher match", + "expectedAgent": "data-processor", + "reason": "data-processor has higher priority (2) than researcher (3)" + } + ] + }, + "fallbackBehavior": { + "whenNoMatch": { + "action": "delegate-to-coordinator", + "coordinator": "coordinator", + "reason": "Coordinator can decompose unclear tasks" + }, + "whenLowConfidence": { + "action": "request-clarification", + "clarificationPrompt": "I'm not sure which agent would best handle your request. Could you provide more details about what you'd like to accomplish?", + "maxClarificationAttempts": 2 + }, + "whenAgentUnavailable": { + "action": "try-alternative", + "maxAlternatives": 3, + "escalateIfAllFail": true + } + } +}