Skip to content

Commit ed7e633

Browse files
authored
Merge pull request #1 from LOUSTA79/codex/update-dependencies-to-latest-versions
Add two-agent production system with monitoring
2 parents 75aa71a + 3961097 commit ed7e633

File tree

10 files changed

+464
-0
lines changed

10 files changed

+464
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Two-Agent Production System
2+
3+
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.
4+
5+
## Features
6+
7+
- Planner and executor agents with graceful offline fallbacks.
8+
- Central orchestrator that tracks performance metrics for every step.
9+
- WebSocket-enabled monitoring dashboard (`npm run monitor`).
10+
- Deterministic integration tests that validate the offline execution path.
11+
12+
## Getting Started
13+
14+
```bash
15+
npm install
16+
npm run dev "Ship the next iteration of the monitoring dashboard"
17+
```
18+
19+
Set an Anthropic API key to enable live model calls:
20+
21+
```bash
22+
export ANTHROPIC_API_KEY="sk-ant-..."
23+
npm start "Deploy production workflow"
24+
```
25+
26+
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`.
27+
28+
Run the included smoke test:
29+
30+
```bash
31+
npm test
32+
```
33+
34+
This executes the orchestrator with simulated agents, ensuring the core workflow remains stable.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "two-agent-production-system",
3+
"version": "2.0.0",
4+
"description": "Production Two-Agent System with Real-Time Monitoring - Built for LOUSTA",
5+
"type": "module",
6+
"main": "src/index.js",
7+
"scripts": {
8+
"start": "node src/index.js",
9+
"monitor": "node src/monitoring/dashboard-server.js",
10+
"dev": "node --watch src/index.js",
11+
"test": "node tests/test-agents.js"
12+
},
13+
"keywords": ["ai", "agents", "anthropic", "claude", "automation", "orchestration"],
14+
"author": "LOUSTA <lousta79@gmail.com>",
15+
"license": "MIT",
16+
"dependencies": {
17+
"@anthropic-ai/sdk": "^0.30.0",
18+
"express": "^4.18.2",
19+
"ws": "^8.16.0",
20+
"dotenv": "^16.4.1",
21+
"chalk": "^5.3.0",
22+
"ora": "^8.0.1",
23+
"cli-table3": "^0.6.3"
24+
},
25+
"engines": {
26+
"node": ">=18.0.0"
27+
}
28+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Anthropic } from '@anthropic-ai/sdk';
2+
import EventEmitter from 'node:events';
3+
4+
export default class BaseAgent extends EventEmitter {
5+
constructor({ name, systemPrompt, metrics, model = 'claude-3-5-sonnet-20241022', maxTokens = 1024 }) {
6+
super();
7+
this.name = name;
8+
this.systemPrompt = systemPrompt;
9+
this.metrics = metrics;
10+
this.model = model;
11+
this.maxTokens = maxTokens;
12+
this.apiKey = process.env.ANTHROPIC_API_KEY;
13+
this.client = this.apiKey ? new Anthropic({ apiKey: this.apiKey }) : null;
14+
this.online = Boolean(this.client);
15+
}
16+
17+
async sendMessage({ prompt, context = [] }) {
18+
this.metrics?.increment(`${this.name}:requests`);
19+
20+
if (!this.online) {
21+
const simulated = await this.simulateResponse({ prompt, context });
22+
this.emit('message', { prompt, response: simulated, offline: true });
23+
return { text: simulated, offline: true };
24+
}
25+
26+
try {
27+
const response = await this.client.messages.create({
28+
model: this.model,
29+
max_tokens: this.maxTokens,
30+
system: this.systemPrompt,
31+
messages: [
32+
...context,
33+
{ role: 'user', content: prompt },
34+
],
35+
});
36+
37+
const text = response.content
38+
?.map((fragment) => fragment.text ?? '')
39+
.join('')
40+
.trim();
41+
42+
const payload = { text, raw: response, offline: false };
43+
this.emit('message', { prompt, response: payload });
44+
return payload;
45+
} catch (error) {
46+
this.metrics?.increment(`${this.name}:fallbacks`);
47+
const simulated = await this.simulateResponse({ prompt, context, error });
48+
const payload = { text: simulated, offline: true, error };
49+
this.emit('message', { prompt, response: payload, error });
50+
return payload;
51+
}
52+
}
53+
54+
// eslint-disable-next-line class-methods-use-this
55+
async simulateResponse() {
56+
return 'Simulation not implemented.';
57+
}
58+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import BaseAgent from './baseAgent.js';
2+
3+
export default class ExecutorAgent extends BaseAgent {
4+
constructor(options = {}) {
5+
super({
6+
name: options.name ?? 'executor',
7+
systemPrompt:
8+
options.systemPrompt ??
9+
'You are an implementation specialist that executes planned steps with precision and returns concise status updates.',
10+
metrics: options.metrics,
11+
model: options.model,
12+
maxTokens: options.maxTokens ?? 1024,
13+
});
14+
}
15+
16+
async executeStep(step, context = []) {
17+
this.metrics?.increment('executor:steps');
18+
19+
if (!this.online) {
20+
return this.offlineExecution(step);
21+
}
22+
23+
const response = await this.sendMessage({
24+
prompt: `You are executing step #${step.id}: ${step.description}.\nProvide a short status update and outline any outputs or follow-up actions.`,
25+
context,
26+
});
27+
28+
if (response.offline) {
29+
return this.offlineExecution(step);
30+
}
31+
32+
return {
33+
id: step.id,
34+
status: 'completed',
35+
detail: response.text,
36+
};
37+
}
38+
39+
offlineExecution(step) {
40+
return {
41+
id: step.id,
42+
status: 'completed',
43+
detail: `Simulated completion for step #${step.id}: ${step.description}`,
44+
};
45+
}
46+
47+
async simulateResponse({ prompt }) {
48+
return `Simulated execution response for prompt: ${prompt}`;
49+
}
50+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import BaseAgent from './baseAgent.js';
2+
3+
export default class PlannerAgent extends BaseAgent {
4+
constructor(options = {}) {
5+
super({
6+
name: options.name ?? 'planner',
7+
systemPrompt:
8+
options.systemPrompt ??
9+
'You are a senior technical planner that breaks high level goals into concise, actionable steps.',
10+
metrics: options.metrics,
11+
model: options.model,
12+
maxTokens: options.maxTokens ?? 2048,
13+
});
14+
}
15+
16+
async plan(goal, context = []) {
17+
this.metrics?.increment('planner:plans');
18+
19+
if (!this.online) {
20+
return this.offlinePlan(goal);
21+
}
22+
23+
const response = await this.sendMessage({
24+
prompt: `Create a numbered execution plan for the following goal. Keep each step concise.\nGoal: ${goal}`,
25+
context,
26+
});
27+
28+
if (response.offline) {
29+
return this.offlinePlan(goal);
30+
}
31+
32+
const steps = this.parsePlan(response.text);
33+
if (steps.length === 0) {
34+
return this.offlinePlan(goal);
35+
}
36+
37+
return steps;
38+
}
39+
40+
parsePlan(text = '') {
41+
return text
42+
.split('\n')
43+
.map((line) => line.trim())
44+
.filter(Boolean)
45+
.map((line, index) => ({
46+
id: index + 1,
47+
description: line.replace(/^[0-9]+[).\-\s]*/, ''),
48+
rationale: 'Provided by planner',
49+
}));
50+
}
51+
52+
offlinePlan(goal) {
53+
return [
54+
{
55+
id: 1,
56+
description: `Understand the goal: ${goal}`,
57+
rationale: 'Establish context and success criteria.',
58+
},
59+
{
60+
id: 2,
61+
description: 'Design an execution strategy with monitoring hooks.',
62+
rationale: 'Ensure each step can be observed in production.',
63+
},
64+
{
65+
id: 3,
66+
description: 'Execute and verify results iteratively.',
67+
rationale: 'Guarantee high quality output.',
68+
},
69+
];
70+
}
71+
72+
async simulateResponse({ prompt }) {
73+
return `Simulated plan for prompt: ${prompt}`;
74+
}
75+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import 'dotenv/config';
2+
import chalk from 'chalk';
3+
import ora from 'ora';
4+
import PlannerAgent from './agents/plannerAgent.js';
5+
import ExecutorAgent from './agents/executorAgent.js';
6+
import Orchestrator from './orchestration/orchestrator.js';
7+
import Metrics from './monitoring/metrics.js';
8+
import { startDashboardServer } from './monitoring/dashboard-server.js';
9+
10+
const goal = process.argv.slice(2).join(' ') || 'Deliver a production-ready two-agent workflow with monitoring.';
11+
12+
async function main() {
13+
const metrics = new Metrics();
14+
const planner = new PlannerAgent({ metrics });
15+
const executor = new ExecutorAgent({ metrics });
16+
17+
const orchestrator = new Orchestrator({ planner, executor, metrics });
18+
startDashboardServer(metrics);
19+
20+
const spinner = ora('Coordinating agents...').start();
21+
try {
22+
const result = await orchestrator.run(goal);
23+
spinner.succeed('Agents finished execution.');
24+
25+
console.log(chalk.green('\nSummary:'));
26+
for (const outcome of result.results) {
27+
console.log(` - Step #${outcome.id}: ${outcome.status} (${Math.round(outcome.durationMs)}ms)`);
28+
console.log(` Detail: ${outcome.detail}`);
29+
}
30+
31+
console.log(chalk.bold(`\nGoal: ${goal}`));
32+
console.log(chalk.bold('Status: Completed'));
33+
} catch (error) {
34+
spinner.fail('Agent orchestration failed.');
35+
console.error(chalk.red(error.stack || error.message));
36+
process.exitCode = 1;
37+
}
38+
}
39+
40+
main();
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import http from 'node:http';
2+
import express from 'express';
3+
import chalk from 'chalk';
4+
import { WebSocketServer } from 'ws';
5+
import Metrics from './metrics.js';
6+
7+
const DEFAULT_PORT = Number.parseInt(process.env.MONITOR_PORT ?? '3050', 10);
8+
9+
export function startDashboardServer(metricsInstance = new Metrics(), options = {}) {
10+
const port = options.port ?? DEFAULT_PORT;
11+
const metrics = metricsInstance;
12+
13+
const app = express();
14+
app.get('/metrics', (_req, res) => {
15+
res.json(metrics.snapshot());
16+
});
17+
18+
const server = http.createServer(app);
19+
const wss = new WebSocketServer({ server, path: '/ws' });
20+
21+
wss.on('connection', (socket) => {
22+
socket.send(JSON.stringify({ type: 'snapshot', data: metrics.snapshot() }));
23+
24+
const handler = (snapshot) => {
25+
if (socket.readyState === socket.OPEN) {
26+
socket.send(JSON.stringify({ type: 'update', data: snapshot }));
27+
}
28+
};
29+
30+
metrics.on('update', handler);
31+
32+
socket.on('close', () => {
33+
metrics.off('update', handler);
34+
});
35+
});
36+
37+
server.listen(port, () => {
38+
console.log(chalk.green(`📊 Monitoring dashboard available at http://localhost:${port}`));
39+
});
40+
41+
return { app, server, wss };
42+
}
43+
44+
if (import.meta.url === `file://${process.argv[1]}`) {
45+
const metrics = new Metrics();
46+
startDashboardServer(metrics);
47+
console.log(chalk.blue('Standalone monitoring server started. Waiting for metrics...'));
48+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import EventEmitter from 'node:events';
2+
3+
export default class Metrics extends EventEmitter {
4+
constructor() {
5+
super();
6+
this.counters = new Map();
7+
this.gauges = new Map();
8+
this.timings = [];
9+
}
10+
11+
increment(name, value = 1) {
12+
const newValue = (this.counters.get(name) ?? 0) + value;
13+
this.counters.set(name, newValue);
14+
this.emit('update', this.snapshot());
15+
return newValue;
16+
}
17+
18+
setGauge(name, value) {
19+
this.gauges.set(name, value);
20+
this.emit('update', this.snapshot());
21+
}
22+
23+
timing(name, value) {
24+
this.timings.push({ name, value, at: new Date().toISOString() });
25+
if (this.timings.length > 50) {
26+
this.timings.shift();
27+
}
28+
this.emit('update', this.snapshot());
29+
}
30+
31+
snapshot() {
32+
return {
33+
counters: Object.fromEntries(this.counters),
34+
gauges: Object.fromEntries(this.gauges),
35+
timings: [...this.timings],
36+
timestamp: new Date().toISOString(),
37+
};
38+
}
39+
}

0 commit comments

Comments
 (0)