diff --git a/.env.example b/.env.example index a5cecc6d6..8b045a354 100644 --- a/.env.example +++ b/.env.example @@ -1,18 +1,10 @@ -# Database -DATABASE_URL="postgresql://agentx:agentx_dev@localhost:5432/agentx_db?schema=public" +# Provider API Keys +ANTHROPIC_API_KEY= +GOOGLE_API_KEY= +OPENAI_API_KEY= -# Redis / BullMQ -REDIS_URL="redis://localhost:6379" +# Redis (for queues/locks) +REDIS_URL=redis://localhost:6379 -# Debug -AGENTX_LOG_LEVEL="info" - -# Provider Secrets (Volume 16 / ADR-0012) -# AGENTX_SECRET_ANTHROPIC_API_KEY="sk-ant-..." -# AGENTX_SECRET_GOOGLE_API_KEY="AIza..." - -# Test Secrets (for tests only - DO NOT use production values) -TEST_AWS_KEY="AKIAIOSFODNN7EXAMPLE" -TEST_GITHUB_TOKEN="ghp_1234567890abcdefghijklmnopqrstuv" -TEST_OPENAI_KEY="sk-1234567890abcdefghijklmnopqrstuv" -TEST_PASSWORD="test_password_123" +# PostgreSQL (for persistence) +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/agentx \ No newline at end of file diff --git a/packages/agent/agent-platform/src/sub-agents/sub-agent.ts b/packages/agent/agent-platform/src/sub-agents/sub-agent.ts index 67f2466a5..af3a54715 100644 --- a/packages/agent/agent-platform/src/sub-agents/sub-agent.ts +++ b/packages/agent/agent-platform/src/sub-agents/sub-agent.ts @@ -1,17 +1,21 @@ import type { SubAgent, AgentRole, AgentConfig } from './interfaces.js'; import type { TaskModel } from '@agentx/core-runtime'; +import type { CompletionRequest, CompletionResponse } from '@agentx/provider-sdk'; +import { ProviderRegistry } from '@agentx/provider-sdk'; export class BaseSubAgent implements SubAgent { public readonly id: string; public readonly role: AgentRole; protected readonly providerId?: string; protected readonly promptTemplate?: string; + protected readonly providerRegistry: ProviderRegistry; - constructor(id: string, role: AgentRole, config?: AgentConfig) { + constructor(id: string, role: AgentRole, config?: AgentConfig, providerRegistry?: ProviderRegistry) { this.id = id; this.role = role; this.providerId = config?.providerId; this.promptTemplate = config?.promptTemplate; + this.providerRegistry = providerRegistry || new ProviderRegistry(); } public buildPrompt(task: TaskModel): string { @@ -24,53 +28,165 @@ export class BaseSubAgent implements SubAgent { return `Execute task ${task.id}: ${task.goal} as ${this.role}`; } + protected async callLLM(prompt: string, systemPrompt?: string, modelId?: string): Promise { + const providers = this.providerRegistry.list(); + const provider = providers[0]; + if (!provider) { + throw new Error('No provider configured'); + } + + const request: CompletionRequest = { + systemPrompt: systemPrompt || '', + userPrompt: prompt, + modelId: modelId || 'claude-sonnet-4-20250514', + }; + + return this.providerRegistry.complete(provider.id, request); + } + public async execute(task: TaskModel, _context: unknown): Promise { const prompt = this.buildPrompt(task); - return { - agentId: this.id, - role: this.role, - taskId: task.id, - providerId: this.providerId, - prompt, - status: 'success', - output: `Executed ${task.goal} successfully as ${this.role}`, - }; + + try { + const response = await this.callLLM(prompt); + return { + agentId: this.id, + role: this.role, + taskId: task.id, + providerId: this.providerId, + prompt, + status: 'success', + output: response.text, + usage: response.usage, + toolCalls: response.toolCalls, + }; + } catch (error) { + return { + agentId: this.id, + role: this.role, + taskId: task.id, + status: 'failure', + error: error instanceof Error ? error.message : String(error), + }; + } } } export class PlannerAgent extends BaseSubAgent { - constructor(id: string, config?: AgentConfig) { - super(id, 'planner', config); + constructor(id: string, config?: AgentConfig, providerRegistry?: ProviderRegistry) { + super(id, 'planner', config, providerRegistry); + } + + public buildPrompt(task: TaskModel): string { + return `You are a planning expert. Decompose the following goal into actionable subtasks with clear dependencies. + +Goal: ${task.goal} + +Provide a structured plan with: +1. Task breakdown (numbered list) +2. Dependencies between tasks +3. Estimated complexity for each task +4. Recommended agent role for each task`; } } export class ArchitectAgent extends BaseSubAgent { - constructor(id: string, config?: AgentConfig) { - super(id, 'architect', config); + constructor(id: string, config?: AgentConfig, providerRegistry?: ProviderRegistry) { + super(id, 'architect', config, providerRegistry); + } + + public buildPrompt(task: TaskModel): string { + return `You are a senior software architect. Design the system architecture for: + +Goal: ${task.goal} + +Provide: +1. High-level architecture overview +2. Component breakdown +3. Data flow diagrams (text-based) +4. Technology recommendations +5. Potential scalability concerns`; } } export class CoderAgent extends BaseSubAgent { - constructor(id: string, config?: AgentConfig) { - super(id, 'coder', config); + constructor(id: string, config?: AgentConfig, providerRegistry?: ProviderRegistry) { + super(id, 'coder', config, providerRegistry); + } + + public buildPrompt(task: TaskModel): string { + return `You are an expert software engineer. Implement the following task: + +Goal: ${task.goal} + +Provide: +1. Clean, production-ready code +2. Proper error handling +3. Type definitions +4. Self-documenting code with minimal comments +5. Consider security implications`; } } export class ReviewerAgent extends BaseSubAgent { - constructor(id: string, config?: AgentConfig) { - super(id, 'reviewer', config); + constructor(id: string, config?: AgentConfig, providerRegistry?: ProviderRegistry) { + super(id, 'reviewer', config, providerRegistry); + } + + public buildPrompt(task: TaskModel): string { + return `You are a senior code reviewer. Review the following code: + +Goal: ${task.goal} + +Check for: +1. Code quality and best practices +2. Potential bugs +3. Security issues +4. Performance concerns +5. Maintainability + +Provide specific, actionable feedback.`; } } export class TesterAgent extends BaseSubAgent { - constructor(id: string, config?: AgentConfig) { - super(id, 'tester', config); + constructor(id: string, config?: AgentConfig, providerRegistry?: ProviderRegistry) { + super(id, 'tester', config, providerRegistry); + } + + public buildPrompt(task: TaskModel): string { + return `You are an expert test engineer. Generate comprehensive tests for: + +Goal: ${task.goal} + +Include: +1. Unit tests +2. Edge cases +3. Error handling +4. Integration tests where appropriate + +Use appropriate testing frameworks and best practices.`; } } export class SecurityAgent extends BaseSubAgent { - constructor(id: string, config?: AgentConfig) { - super(id, 'security', config); + constructor(id: string, config?: AgentConfig, providerRegistry?: ProviderRegistry) { + super(id, 'security', config, providerRegistry); + } + + public buildPrompt(task: TaskModel): string { + return `You are a security expert. Analyze the following code for vulnerabilities: + +Goal: ${task.goal} + +Check for: +1. Injection attacks (SQL, XSS, command injection) +2. Authentication/authorization issues +3. Data exposure and privacy concerns +4. Insecure dependencies +5. OWASP Top 10 vulnerabilities + +Provide severity ratings and remediation steps.`; } } diff --git a/packages/provider/provider-sdk/src/conformance/credential-resolver.ts b/packages/provider/provider-sdk/src/conformance/credential-resolver.ts index ec24f349a..18c3aa3df 100644 --- a/packages/provider/provider-sdk/src/conformance/credential-resolver.ts +++ b/packages/provider/provider-sdk/src/conformance/credential-resolver.ts @@ -1,10 +1,22 @@ /** * @module provider-sdk/credential-resolver - * @description Local stub for credential resolver to avoid circular dependencies. + * @description Credential resolver that reads from environment variables. + * Maps provider keys to env vars: provider.anthropic.api_key → ANTHROPIC_API_KEY */ export class CredentialResolver { async resolve(key: string): Promise { - return `stub-${key}`; + // Map provider key to env var: provider.anthropic.api_key → ANTHROPIC_API_KEY + const envKey = key + .replace(/^provider\./, '') + .replace(/\.api_key$/, '_API_KEY') + .replace(/\./g, '_') + .toUpperCase(); + + const value = process.env[envKey]; + if (!value) { + throw new Error(`Credential not found: ${key} (env: ${envKey})`); + } + return value; } } diff --git a/packages/provider/provider-sdk/test/provider-sdk.test.ts b/packages/provider/provider-sdk/test/provider-sdk.test.ts index 8eebbca47..c5eaff854 100644 --- a/packages/provider/provider-sdk/test/provider-sdk.test.ts +++ b/packages/provider/provider-sdk/test/provider-sdk.test.ts @@ -230,7 +230,12 @@ describe('Validators and Version', () => { it('covers credentials resolver and template helpers', async () => { const resolver = new CredentialResolver(); - expect(await resolver.resolve('key')).toBe('stub-key'); + // Test with env var set + process.env.TEST_KEY = 'test-value'; + expect(await resolver.resolve('provider.test.key')).toBe('test-value'); + delete process.env.TEST_KEY; + // Test missing key throws + await expect(resolver.resolve('provider.missing.key')).rejects.toThrow('Credential not found'); const template = createQueueTemplate(); expect(template.getMetadata().id).toBe('template-queue'); diff --git a/packages/shared/core-runtime/src/index.ts b/packages/shared/core-runtime/src/index.ts index 59caf1bf7..d264750e0 100644 --- a/packages/shared/core-runtime/src/index.ts +++ b/packages/shared/core-runtime/src/index.ts @@ -10,3 +10,4 @@ export * from './scheduler/index.js'; export * from './events/index.js'; export * from './context/index.js'; export * from './repositories/in-memory-task-repository.js'; +export * from './registry/agent-registry.js'; diff --git a/packages/shared/core-runtime/src/registry/agent-registry.ts b/packages/shared/core-runtime/src/registry/agent-registry.ts new file mode 100644 index 000000000..1459076a2 --- /dev/null +++ b/packages/shared/core-runtime/src/registry/agent-registry.ts @@ -0,0 +1,60 @@ +import type { TaskModel, TaskContext } from '../interfaces/task.js'; + +export interface Agent { + readonly id: string; + readonly role: string; + execute(task: TaskModel, context: unknown): Promise; +} + +export interface AgentRegistryConfig { + maxConcurrentAgents?: number; +} + +export class AgentRegistry { + private agents = new Map(); + + constructor(_config?: AgentRegistryConfig) { + // Config reserved for future use + } + + register(agent: Agent): void { + this.agents.set(agent.id, agent); + } + + unregister(agentId: string): void { + this.agents.delete(agentId); + } + + get(agentId: string): Agent | undefined { + return this.agents.get(agentId); + } + + getByRole(role: string): Agent | undefined { + for (const agent of this.agents.values()) { + if (agent.role === role) { + return agent; + } + } + return undefined; + } + + list(): Agent[] { + return Array.from(this.agents.values()); + } + + async execute(agentId: string, task: TaskModel, context: TaskContext): Promise { + const agent = this.get(agentId); + if (!agent) { + throw new Error(`Agent not found: ${agentId}`); + } + return agent.execute(task, context); + } + + async executeByRole(role: string, task: TaskModel, context: TaskContext): Promise { + const agent = this.getByRole(role); + if (!agent) { + throw new Error(`Agent not found for role: ${role}`); + } + return agent.execute(task, context); + } +} diff --git a/packages/shared/core-runtime/src/scheduler/index.ts b/packages/shared/core-runtime/src/scheduler/index.ts index 625a7c979..e2de2895b 100644 --- a/packages/shared/core-runtime/src/scheduler/index.ts +++ b/packages/shared/core-runtime/src/scheduler/index.ts @@ -6,6 +6,7 @@ import { EventTopic } from '../interfaces/events.js'; import { TaskStateMachine } from '../state-machine/index.js'; import { TaskNotFoundError } from '../errors.js'; import { Tracer, Metrics } from '@agentx/observability'; +import type { AgentRegistry } from '../registry/agent-registry.js'; export interface SchedulerConfig { maxConcurrentTaskGraphs?: number; @@ -19,13 +20,20 @@ export class Scheduler implements IScheduler { private maxParallel: number; private tracer = new Tracer('core-runtime-scheduler'); private metrics = new Metrics(); + private agentRegistry?: AgentRegistry; constructor( private readonly eventBus: IEventBus, private readonly taskRepo: ITaskRepository, config: SchedulerConfig = {}, + agentRegistry?: AgentRegistry, ) { this.maxParallel = config.maxParallelAgents ?? 10; + this.agentRegistry = agentRegistry; + } + + public setAgentRegistry(registry: AgentRegistry): void { + this.agentRegistry = registry; } public async enqueue(task: TaskModel): Promise { @@ -154,7 +162,40 @@ export class Scheduler implements IScheduler { (task as TaskModel).traceId, (task as TaskModel).id, ); + + // Execute agent if registry is configured + if (this.agentRegistry && task.assignedAgentRole) { + this.executeAgent(task).catch((err) => { + this.failTask(taskId, err).catch(console.error); + }); + } + } + } + } + + private async executeAgent(task: TaskModel): Promise { + const span = this.tracer.startSpan('agent-execution'); + span.setAttribute('task.id', task.id); + span.setAttribute('agent.role', task.assignedAgentRole || 'unknown'); + + try { + if (!this.agentRegistry) { + throw new Error('Agent registry not configured'); } + + const role = task.assignedAgentRole || 'coder'; + const result = await this.agentRegistry.executeByRole(role, task, task.context); + + await this.completeTask(task.id, result); + this.metrics.counter('agent_executions', 1, { role, status: 'success' }); + span.setStatus({ code: 0 }); + } catch (e: unknown) { + const error = e instanceof Error ? e : new Error(String(e)); + span.setStatus({ code: 1, message: error.message }); + this.metrics.counter('agent_executions', 1, { status: 'failure' }); + throw error; + } finally { + span.end(); } }