From 39610975be97f57139ac472e02e13866d859098d Mon Sep 17 00:00:00 2001 From: LOUSTA Date: Wed, 26 Nov 2025 07:10:08 +1100 Subject: [PATCH] Add two-agent production system with monitoring --- Tool/two-agent-production-system/README.md | 34 +++++++++ Tool/two-agent-production-system/package.json | 28 +++++++ .../src/agents/baseAgent.js | 58 ++++++++++++++ .../src/agents/executorAgent.js | 50 +++++++++++++ .../src/agents/plannerAgent.js | 75 +++++++++++++++++++ Tool/two-agent-production-system/src/index.js | 40 ++++++++++ .../src/monitoring/dashboard-server.js | 48 ++++++++++++ .../src/monitoring/metrics.js | 39 ++++++++++ .../src/orchestration/orchestrator.js | 66 ++++++++++++++++ .../tests/test-agents.js | 26 +++++++ 10 files changed, 464 insertions(+) create mode 100644 Tool/two-agent-production-system/README.md create mode 100644 Tool/two-agent-production-system/package.json create mode 100644 Tool/two-agent-production-system/src/agents/baseAgent.js create mode 100644 Tool/two-agent-production-system/src/agents/executorAgent.js create mode 100644 Tool/two-agent-production-system/src/agents/plannerAgent.js create mode 100644 Tool/two-agent-production-system/src/index.js create mode 100644 Tool/two-agent-production-system/src/monitoring/dashboard-server.js create mode 100644 Tool/two-agent-production-system/src/monitoring/metrics.js create mode 100644 Tool/two-agent-production-system/src/orchestration/orchestrator.js create mode 100644 Tool/two-agent-production-system/tests/test-agents.js diff --git a/Tool/two-agent-production-system/README.md b/Tool/two-agent-production-system/README.md new file mode 100644 index 00000000..ac1c6439 --- /dev/null +++ b/Tool/two-agent-production-system/README.md @@ -0,0 +1,34 @@ +# Two-Agent Production System + +A production-ready orchestration harness that coordinates planning and execution agents with real-time monitoring. Built for LOUSTA using Claude via the official Anthropic SDK. + +## Features + +- Planner and executor agents with graceful offline fallbacks. +- Central orchestrator that tracks performance metrics for every step. +- WebSocket-enabled monitoring dashboard (`npm run monitor`). +- Deterministic integration tests that validate the offline execution path. + +## Getting Started + +```bash +npm install +npm run dev "Ship the next iteration of the monitoring dashboard" +``` + +Set an Anthropic API key to enable live model calls: + +```bash +export ANTHROPIC_API_KEY="sk-ant-..." +npm start "Deploy production workflow" +``` + +The monitoring dashboard is served on [http://localhost:3050](http://localhost:3050) by default and exposes a `/metrics` endpoint plus a WebSocket feed at `/ws`. + +Run the included smoke test: + +```bash +npm test +``` + +This executes the orchestrator with simulated agents, ensuring the core workflow remains stable. diff --git a/Tool/two-agent-production-system/package.json b/Tool/two-agent-production-system/package.json new file mode 100644 index 00000000..88542a35 --- /dev/null +++ b/Tool/two-agent-production-system/package.json @@ -0,0 +1,28 @@ +{ + "name": "two-agent-production-system", + "version": "2.0.0", + "description": "Production Two-Agent System with Real-Time Monitoring - Built for LOUSTA", + "type": "module", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "monitor": "node src/monitoring/dashboard-server.js", + "dev": "node --watch src/index.js", + "test": "node tests/test-agents.js" + }, + "keywords": ["ai", "agents", "anthropic", "claude", "automation", "orchestration"], + "author": "LOUSTA ", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "^0.30.0", + "express": "^4.18.2", + "ws": "^8.16.0", + "dotenv": "^16.4.1", + "chalk": "^5.3.0", + "ora": "^8.0.1", + "cli-table3": "^0.6.3" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/Tool/two-agent-production-system/src/agents/baseAgent.js b/Tool/two-agent-production-system/src/agents/baseAgent.js new file mode 100644 index 00000000..95535545 --- /dev/null +++ b/Tool/two-agent-production-system/src/agents/baseAgent.js @@ -0,0 +1,58 @@ +import { Anthropic } from '@anthropic-ai/sdk'; +import EventEmitter from 'node:events'; + +export default class BaseAgent extends EventEmitter { + constructor({ name, systemPrompt, metrics, model = 'claude-3-5-sonnet-20241022', maxTokens = 1024 }) { + super(); + this.name = name; + this.systemPrompt = systemPrompt; + this.metrics = metrics; + this.model = model; + this.maxTokens = maxTokens; + this.apiKey = process.env.ANTHROPIC_API_KEY; + this.client = this.apiKey ? new Anthropic({ apiKey: this.apiKey }) : null; + this.online = Boolean(this.client); + } + + async sendMessage({ prompt, context = [] }) { + this.metrics?.increment(`${this.name}:requests`); + + if (!this.online) { + const simulated = await this.simulateResponse({ prompt, context }); + this.emit('message', { prompt, response: simulated, offline: true }); + return { text: simulated, offline: true }; + } + + try { + const response = await this.client.messages.create({ + model: this.model, + max_tokens: this.maxTokens, + system: this.systemPrompt, + messages: [ + ...context, + { role: 'user', content: prompt }, + ], + }); + + const text = response.content + ?.map((fragment) => fragment.text ?? '') + .join('') + .trim(); + + const payload = { text, raw: response, offline: false }; + this.emit('message', { prompt, response: payload }); + return payload; + } catch (error) { + this.metrics?.increment(`${this.name}:fallbacks`); + const simulated = await this.simulateResponse({ prompt, context, error }); + const payload = { text: simulated, offline: true, error }; + this.emit('message', { prompt, response: payload, error }); + return payload; + } + } + + // eslint-disable-next-line class-methods-use-this + async simulateResponse() { + return 'Simulation not implemented.'; + } +} diff --git a/Tool/two-agent-production-system/src/agents/executorAgent.js b/Tool/two-agent-production-system/src/agents/executorAgent.js new file mode 100644 index 00000000..86c293a6 --- /dev/null +++ b/Tool/two-agent-production-system/src/agents/executorAgent.js @@ -0,0 +1,50 @@ +import BaseAgent from './baseAgent.js'; + +export default class ExecutorAgent extends BaseAgent { + constructor(options = {}) { + super({ + name: options.name ?? 'executor', + systemPrompt: + options.systemPrompt ?? + 'You are an implementation specialist that executes planned steps with precision and returns concise status updates.', + metrics: options.metrics, + model: options.model, + maxTokens: options.maxTokens ?? 1024, + }); + } + + async executeStep(step, context = []) { + this.metrics?.increment('executor:steps'); + + if (!this.online) { + return this.offlineExecution(step); + } + + const response = await this.sendMessage({ + prompt: `You are executing step #${step.id}: ${step.description}.\nProvide a short status update and outline any outputs or follow-up actions.`, + context, + }); + + if (response.offline) { + return this.offlineExecution(step); + } + + return { + id: step.id, + status: 'completed', + detail: response.text, + }; + } + + offlineExecution(step) { + return { + id: step.id, + status: 'completed', + detail: `Simulated completion for step #${step.id}: ${step.description}`, + }; + } + + async simulateResponse({ prompt }) { + return `Simulated execution response for prompt: ${prompt}`; + } +} diff --git a/Tool/two-agent-production-system/src/agents/plannerAgent.js b/Tool/two-agent-production-system/src/agents/plannerAgent.js new file mode 100644 index 00000000..77e95625 --- /dev/null +++ b/Tool/two-agent-production-system/src/agents/plannerAgent.js @@ -0,0 +1,75 @@ +import BaseAgent from './baseAgent.js'; + +export default class PlannerAgent extends BaseAgent { + constructor(options = {}) { + super({ + name: options.name ?? 'planner', + systemPrompt: + options.systemPrompt ?? + 'You are a senior technical planner that breaks high level goals into concise, actionable steps.', + metrics: options.metrics, + model: options.model, + maxTokens: options.maxTokens ?? 2048, + }); + } + + async plan(goal, context = []) { + this.metrics?.increment('planner:plans'); + + if (!this.online) { + return this.offlinePlan(goal); + } + + const response = await this.sendMessage({ + prompt: `Create a numbered execution plan for the following goal. Keep each step concise.\nGoal: ${goal}`, + context, + }); + + if (response.offline) { + return this.offlinePlan(goal); + } + + const steps = this.parsePlan(response.text); + if (steps.length === 0) { + return this.offlinePlan(goal); + } + + return steps; + } + + parsePlan(text = '') { + return text + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((line, index) => ({ + id: index + 1, + description: line.replace(/^[0-9]+[).\-\s]*/, ''), + rationale: 'Provided by planner', + })); + } + + offlinePlan(goal) { + return [ + { + id: 1, + description: `Understand the goal: ${goal}`, + rationale: 'Establish context and success criteria.', + }, + { + id: 2, + description: 'Design an execution strategy with monitoring hooks.', + rationale: 'Ensure each step can be observed in production.', + }, + { + id: 3, + description: 'Execute and verify results iteratively.', + rationale: 'Guarantee high quality output.', + }, + ]; + } + + async simulateResponse({ prompt }) { + return `Simulated plan for prompt: ${prompt}`; + } +} diff --git a/Tool/two-agent-production-system/src/index.js b/Tool/two-agent-production-system/src/index.js new file mode 100644 index 00000000..29a53b08 --- /dev/null +++ b/Tool/two-agent-production-system/src/index.js @@ -0,0 +1,40 @@ +import 'dotenv/config'; +import chalk from 'chalk'; +import ora from 'ora'; +import PlannerAgent from './agents/plannerAgent.js'; +import ExecutorAgent from './agents/executorAgent.js'; +import Orchestrator from './orchestration/orchestrator.js'; +import Metrics from './monitoring/metrics.js'; +import { startDashboardServer } from './monitoring/dashboard-server.js'; + +const goal = process.argv.slice(2).join(' ') || 'Deliver a production-ready two-agent workflow with monitoring.'; + +async function main() { + const metrics = new Metrics(); + const planner = new PlannerAgent({ metrics }); + const executor = new ExecutorAgent({ metrics }); + + const orchestrator = new Orchestrator({ planner, executor, metrics }); + startDashboardServer(metrics); + + const spinner = ora('Coordinating agents...').start(); + try { + const result = await orchestrator.run(goal); + spinner.succeed('Agents finished execution.'); + + console.log(chalk.green('\nSummary:')); + for (const outcome of result.results) { + console.log(` - Step #${outcome.id}: ${outcome.status} (${Math.round(outcome.durationMs)}ms)`); + console.log(` Detail: ${outcome.detail}`); + } + + console.log(chalk.bold(`\nGoal: ${goal}`)); + console.log(chalk.bold('Status: Completed')); + } catch (error) { + spinner.fail('Agent orchestration failed.'); + console.error(chalk.red(error.stack || error.message)); + process.exitCode = 1; + } +} + +main(); diff --git a/Tool/two-agent-production-system/src/monitoring/dashboard-server.js b/Tool/two-agent-production-system/src/monitoring/dashboard-server.js new file mode 100644 index 00000000..b6f2ce94 --- /dev/null +++ b/Tool/two-agent-production-system/src/monitoring/dashboard-server.js @@ -0,0 +1,48 @@ +import http from 'node:http'; +import express from 'express'; +import chalk from 'chalk'; +import { WebSocketServer } from 'ws'; +import Metrics from './metrics.js'; + +const DEFAULT_PORT = Number.parseInt(process.env.MONITOR_PORT ?? '3050', 10); + +export function startDashboardServer(metricsInstance = new Metrics(), options = {}) { + const port = options.port ?? DEFAULT_PORT; + const metrics = metricsInstance; + + const app = express(); + app.get('/metrics', (_req, res) => { + res.json(metrics.snapshot()); + }); + + const server = http.createServer(app); + const wss = new WebSocketServer({ server, path: '/ws' }); + + wss.on('connection', (socket) => { + socket.send(JSON.stringify({ type: 'snapshot', data: metrics.snapshot() })); + + const handler = (snapshot) => { + if (socket.readyState === socket.OPEN) { + socket.send(JSON.stringify({ type: 'update', data: snapshot })); + } + }; + + metrics.on('update', handler); + + socket.on('close', () => { + metrics.off('update', handler); + }); + }); + + server.listen(port, () => { + console.log(chalk.green(`📊 Monitoring dashboard available at http://localhost:${port}`)); + }); + + return { app, server, wss }; +} + +if (import.meta.url === `file://${process.argv[1]}`) { + const metrics = new Metrics(); + startDashboardServer(metrics); + console.log(chalk.blue('Standalone monitoring server started. Waiting for metrics...')); +} diff --git a/Tool/two-agent-production-system/src/monitoring/metrics.js b/Tool/two-agent-production-system/src/monitoring/metrics.js new file mode 100644 index 00000000..1941b2a1 --- /dev/null +++ b/Tool/two-agent-production-system/src/monitoring/metrics.js @@ -0,0 +1,39 @@ +import EventEmitter from 'node:events'; + +export default class Metrics extends EventEmitter { + constructor() { + super(); + this.counters = new Map(); + this.gauges = new Map(); + this.timings = []; + } + + increment(name, value = 1) { + const newValue = (this.counters.get(name) ?? 0) + value; + this.counters.set(name, newValue); + this.emit('update', this.snapshot()); + return newValue; + } + + setGauge(name, value) { + this.gauges.set(name, value); + this.emit('update', this.snapshot()); + } + + timing(name, value) { + this.timings.push({ name, value, at: new Date().toISOString() }); + if (this.timings.length > 50) { + this.timings.shift(); + } + this.emit('update', this.snapshot()); + } + + snapshot() { + return { + counters: Object.fromEntries(this.counters), + gauges: Object.fromEntries(this.gauges), + timings: [...this.timings], + timestamp: new Date().toISOString(), + }; + } +} diff --git a/Tool/two-agent-production-system/src/orchestration/orchestrator.js b/Tool/two-agent-production-system/src/orchestration/orchestrator.js new file mode 100644 index 00000000..0eee40f4 --- /dev/null +++ b/Tool/two-agent-production-system/src/orchestration/orchestrator.js @@ -0,0 +1,66 @@ +import EventEmitter from 'node:events'; +import { performance } from 'node:perf_hooks'; +import Table from 'cli-table3'; +import chalk from 'chalk'; + +export default class Orchestrator extends EventEmitter { + constructor({ planner, executor, metrics }) { + super(); + this.planner = planner; + this.executor = executor; + this.metrics = metrics; + } + + async run(goal) { + this.metrics?.increment('orchestrator:runs'); + this.emit('start', { goal }); + + const plan = await this.planner.plan(goal); + this.metrics?.setGauge('plan:steps', plan.length); + + this.emit('plan', { plan }); + this.printPlan(plan); + + const stepResults = []; + for (const step of plan) { + const start = performance.now(); + this.metrics?.increment('executor:pending'); + this.emit('step:start', step); + + const result = await this.executor.executeStep(step, stepResults); + + const durationMs = performance.now() - start; + this.metrics?.timing('step:duration_ms', durationMs); + this.metrics?.increment('executor:completed'); + this.metrics?.setGauge('executor:last_duration_ms', Math.round(durationMs)); + + this.emit('step:complete', { step, result, durationMs }); + stepResults.push({ ...result, durationMs }); + } + + const summary = { + goal, + plan, + results: stepResults, + }; + + this.metrics?.increment('orchestrator:completed'); + this.emit('complete', summary); + return summary; + } + + printPlan(plan) { + if (!plan.length) { + console.log(chalk.red('Planner returned an empty plan.')); + return; + } + + const table = new Table({ head: ['#', 'Step', 'Rationale'] }); + for (const step of plan) { + table.push([step.id, step.description, step.rationale ?? '']); + } + + console.log(chalk.cyan('\nExecution Plan:')); + console.log(table.toString()); + } +} diff --git a/Tool/two-agent-production-system/tests/test-agents.js b/Tool/two-agent-production-system/tests/test-agents.js new file mode 100644 index 00000000..274236b7 --- /dev/null +++ b/Tool/two-agent-production-system/tests/test-agents.js @@ -0,0 +1,26 @@ +import assert from 'node:assert/strict'; +import PlannerAgent from '../src/agents/plannerAgent.js'; +import ExecutorAgent from '../src/agents/executorAgent.js'; +import Orchestrator from '../src/orchestration/orchestrator.js'; +import Metrics from '../src/monitoring/metrics.js'; + +async function run() { + const metrics = new Metrics(); + const planner = new PlannerAgent({ metrics }); + const executor = new ExecutorAgent({ metrics }); + const orchestrator = new Orchestrator({ planner, executor, metrics }); + + const summary = await orchestrator.run('Validate the two-agent production system.'); + + assert.ok(Array.isArray(summary.plan), 'Plan should be an array'); + assert.ok(summary.plan.length >= 3, 'Plan should contain at least three steps'); + assert.ok(summary.results.every((step) => step.status === 'completed')); + + console.log('✅ Two-agent orchestration completed successfully.'); + console.log(`Generated ${summary.plan.length} steps.`); +} + +run().catch((error) => { + console.error(error); + process.exitCode = 1; +});