p2pclaw-api / packages /api /src /services /geneticService.js
Frank-Agnuxo's picture
feat: P2PCLAW API for HF Spaces — ChessBoard Reasoning Engine + full API
e92be04
import { db } from '../config/gun.js';
import { gunSafe } from '../utils/gunUtils.js';
import { sandbox } from './IsolateSandbox.js';
/**
* Gene Definitions — structured genome for P2PCLAW protocol optimization
* Each gene is a continuous [0,1] parameter governing network behavior.
*/
export const GENE_DEFS = [
{ key: 'research_depth', min: 0, max: 1, optimum: 0.65, label: 'Research Depth', desc: 'Depth vs. breadth of topic investigation per agent' },
{ key: 'validation_strictness', min: 0, max: 1, optimum: 0.70, label: 'Validation Strictness', desc: 'Rigor of peer review applied to submitted papers' },
{ key: 'publication_rate', min: 0, max: 1, optimum: 0.40, label: 'Publication Rate', desc: 'Frequency of publishing findings (higher = more spam risk)' },
{ key: 'consensus_threshold', min: 0, max: 1, optimum: 0.68, label: 'Consensus Threshold', desc: 'Minimum agreement ratio required to promote a paper' },
{ key: 'collaboration_weight', min: 0, max: 1, optimum: 0.55, label: 'Collaboration Weight', desc: 'Tendency to collaborate vs. isolated solo research' },
{ key: 'exploration_rate', min: 0, max: 1, optimum: 0.38, label: 'Exploration Rate', desc: 'Explore new topics vs. exploit established research areas' },
{ key: 'fault_tolerance', min: 0, max: 1, optimum: 0.80, label: 'Fault Tolerance', desc: 'Network resilience to agent failures and adversarial nodes' },
{ key: 'convergence_speed', min: 0, max: 1, optimum: 0.45, label: 'Convergence Speed', desc: 'Speed of consensus convergence (too fast = premature, too slow = stagnation)' },
];
/**
* GeneticService — Full Evolutionary Engine
*
* Implements:
* - Real genetic algorithm (selection, crossover, mutation, elitism)
* - Multi-objective fitness function based on network optimization theory
* - Lineage tracking (parent IDs per offspring)
* - Population diversity metric (avg pairwise gene distance)
* - Persistence via Gun.js
* - Code mutation sandbox (legacy IsolateSandbox integration)
*/
export class GeneticService {
constructor() {
this.population = []; // current live population
this.generation = 0;
this.populationSize = 12;
this.mutationRate = 0.12;
this.eliteCount = 2; // elitism: always carry top N
this._historyBuf = []; // [{generation, best, avg, diversity}]
}
// ─────────────────────────────────────────────────────────────────
// Gene helpers
// ─────────────────────────────────────────────────────────────────
_randGene(def) {
return +(Math.random() * (def.max - def.min) + def.min).toFixed(4);
}
_randomGenome(overrides = {}) {
const genes = {};
for (const def of GENE_DEFS) {
genes[def.key] = overrides[def.key] !== undefined ? overrides[def.key] : this._randGene(def);
}
return genes;
}
// ─────────────────────────────────────────────────────────────────
// Fitness function — multi-objective, range [0, 1]
// ─────────────────────────────────────────────────────────────────
evaluateFitness(genes) {
// 1. Network efficiency: research_depth ≈ 0.65, exploration_rate ≈ 0.38
const netEff = Math.max(0,
1 - Math.abs(genes.research_depth - 0.65) * 1.4
- 0.6 * Math.abs(genes.exploration_rate - 0.38)
);
// 2. Quality gate: validation_strictness × (1 - publication_rate × 0.4)
// High strictness + moderate rate = good. Too loose + too fast = spam.
const qualityGate = Math.min(1,
genes.validation_strictness * (1 - genes.publication_rate * 0.45) * 1.25
);
// 3. Consensus health: threshold sweet-spot around 0.68
const consensusScore = genes.consensus_threshold >= 0.5
? Math.max(0, 1 - Math.abs(genes.consensus_threshold - 0.68) * 2.5)
: genes.consensus_threshold * 0.6;
// 4. Collaboration balance: neither isolated (→0) nor echo chamber (→1)
const collabScore = Math.max(0, 1 - Math.abs(genes.collaboration_weight - 0.55) * 2.2);
// 5. Fault tolerance: monotone reward, strongly penalise < 0.5
const resilienceScore = genes.fault_tolerance >= 0.5
? genes.fault_tolerance
: genes.fault_tolerance * 0.4;
// 6. Convergence speed: sweet-spot at 0.45
const convergenceScore = Math.max(0, 1 - Math.abs(genes.convergence_speed - 0.45) * 2.4);
// Weighted aggregate
const raw =
netEff * 0.22 +
qualityGate * 0.22 +
consensusScore * 0.16 +
collabScore * 0.14 +
resilienceScore * 0.14 +
convergenceScore * 0.12;
return Math.max(0, Math.min(1, raw));
}
_fitnessComponents(genes) {
const fc = {
network_efficiency: Math.max(0, 1 - Math.abs(genes.research_depth - 0.65)*1.4 - 0.6*Math.abs(genes.exploration_rate - 0.38)),
quality_gate: Math.min(1, genes.validation_strictness * (1 - genes.publication_rate * 0.45) * 1.25),
consensus_health: genes.consensus_threshold >= 0.5 ? Math.max(0, 1 - Math.abs(genes.consensus_threshold - 0.68)*2.5) : genes.consensus_threshold*0.6,
collaboration_balance: Math.max(0, 1 - Math.abs(genes.collaboration_weight - 0.55)*2.2),
resilience: genes.fault_tolerance >= 0.5 ? genes.fault_tolerance : genes.fault_tolerance*0.4,
convergence_score: Math.max(0, 1 - Math.abs(genes.convergence_speed - 0.45)*2.4),
};
return Object.fromEntries(Object.entries(fc).map(([k, v]) => [k, +v.toFixed(4)]));
}
// ─────────────────────────────────────────────────────────────────
// Genetic operators
// ─────────────────────────────────────────────────────────────────
/** Tournament selection — picks best of k random candidates */
_tournamentSelect(pop, k = 3) {
const candidates = [];
for (let i = 0; i < k; i++) candidates.push(pop[Math.floor(Math.random() * pop.length)]);
return candidates.reduce((best, c) => (c.fitness > best.fitness ? c : best));
}
/** Uniform crossover — each gene inherited independently with 50% probability */
_crossover(parentA, parentB) {
const childGenes = {};
for (const def of GENE_DEFS) {
childGenes[def.key] = Math.random() < 0.5 ? parentA.genes[def.key] : parentB.genes[def.key];
}
return childGenes;
}
/** Gaussian mutation — perturbs each gene with probability `rate` */
_mutate(genes, rate = this.mutationRate) {
const mutated = { ...genes };
for (const def of GENE_DEFS) {
if (Math.random() < rate) {
const sigma = (def.max - def.min) * 0.10;
// Box-Muller approximation
const delta = (Math.random() + Math.random() - 1) * sigma;
mutated[def.key] = +(Math.max(def.min, Math.min(def.max, genes[def.key] + delta))).toFixed(4);
}
}
return mutated;
}
// ─────────────────────────────────────────────────────────────────
// Population management
// ─────────────────────────────────────────────────────────────────
/** Seed a fresh random population (resets generation counter) */
seedPopulation(size = this.populationSize) {
this.population = [];
this.generation = 0;
this._historyBuf = [];
this.populationSize = size;
for (let i = 0; i < size; i++) {
const genes = this._randomGenome();
const fitness = this.evaluateFitness(genes);
const genome = this._buildGenome(`genome-g0-${i}`, 0, [], genes, fitness, 'EVALUATED');
this.population.push(genome);
db.get('genetic_population').get(genome.id).put(gunSafe(genome));
}
const stats = this.getStats();
this._historyBuf.push(stats);
db.get('genetic_stats').put(gunSafe({ ...stats, timestamp: Date.now() }));
db.get('genetic_history').get(`g0`).put(gunSafe(stats));
return this.population;
}
/** Evolve one full generation (selection → crossover → mutation → elitism) */
evolveGeneration() {
if (this.population.length < 2) throw new Error('Population too small — seed first (minimum 2)');
const sorted = [...this.population].sort((a, b) => b.fitness - a.fitness);
const nextGen = [];
// Elitism: carry over top N unchanged
for (let i = 0; i < this.eliteCount && i < sorted.length; i++) {
nextGen.push({ ...sorted[i], status: 'ELITE' });
}
// Generate offspring via tournament → crossover → mutation
while (nextGen.length < this.populationSize) {
const pa = this._tournamentSelect(sorted, 3);
const pb = this._tournamentSelect(sorted, 3);
let childGenes = this._crossover(pa, pb);
childGenes = this._mutate(childGenes);
const fitness = this.evaluateFitness(childGenes);
const idx = nextGen.length;
const child = this._buildGenome(
`genome-g${this.generation + 1}-${idx}`,
this.generation + 1,
[pa.id, pb.id],
childGenes,
fitness,
'EVALUATED'
);
nextGen.push(child);
}
this.generation++;
this.population = nextGen;
// Persist to Gun
for (const g of nextGen) {
db.get('genetic_population').get(g.id).put(gunSafe(g));
}
const stats = this.getStats();
this._historyBuf.push(stats);
db.get('genetic_stats').put(gunSafe({ ...stats, timestamp: Date.now() }));
db.get('genetic_history').get(`g${this.generation}`).put(gunSafe(stats));
return { generation: this.generation, population: nextGen, stats, history: this._historyBuf };
}
/** Manual crossover of two specific genomes by ID */
crossoverById(idA, idB) {
const pa = this.population.find(g => g.id === idA);
const pb = this.population.find(g => g.id === idB);
if (!pa) throw new Error(`Genome ${idA} not found`);
if (!pb) throw new Error(`Genome ${idB} not found`);
let childGenes = this._crossover(pa, pb);
childGenes = this._mutate(childGenes, 0.05); // light mutation for manual cross
const fitness = this.evaluateFitness(childGenes);
const child = this._buildGenome(
`genome-cross-${Date.now().toString(36)}`,
Math.max(pa.generation, pb.generation) + 1,
[pa.id, pb.id],
childGenes,
fitness,
'MANUAL_CROSS'
);
this.population.push(child);
db.get('genetic_population').get(child.id).put(gunSafe(child));
return child;
}
_buildGenome(id, generation, parent_ids, genes, fitness, status) {
return {
id,
generation,
parent_ids,
genes,
fitness: +fitness.toFixed(4),
fitness_components: this._fitnessComponents(genes),
status,
born_at: Date.now(),
};
}
// ─────────────────────────────────────────────────────────────────
// Stats & population access
// ─────────────────────────────────────────────────────────────────
getStats() {
if (this.population.length === 0) {
return { generation: this.generation, size: 0, best: 0, avg: 0, diversity: 0, elites: [] };
}
const fits = this.population.map(g => g.fitness);
const best = Math.max(...fits);
const avg = fits.reduce((a, b) => a + b, 0) / fits.length;
// Average pairwise Euclidean gene distance (normalised)
let divSum = 0, pairs = 0;
for (let i = 0; i < this.population.length; i++) {
for (let j = i + 1; j < this.population.length; j++) {
let dist = 0;
const ga = this.population[i].genes;
const gb = this.population[j].genes;
for (const def of GENE_DEFS) dist += Math.abs(ga[def.key] - gb[def.key]);
divSum += dist / GENE_DEFS.length;
pairs++;
}
}
const diversity = pairs > 0 ? divSum / pairs : 0;
return {
generation: this.generation,
size: this.population.length,
best: +best.toFixed(4),
avg: +avg.toFixed(4),
diversity: +diversity.toFixed(4),
elites: this.population.filter(g => g.status === 'ELITE').map(g => ({ id: g.id, fitness: g.fitness })),
};
}
getHistory() { return this._historyBuf; }
async getPopulation() {
if (this.population.length > 0) return this.population;
// Fallback: load from Gun (e.g. after server restart)
return new Promise((resolve) => {
const pop = [];
db.get('genetic_population').map().once((data) => {
if (data && data.id && data.genes) pop.push(data);
});
setTimeout(() => {
this.population = pop.sort((a, b) => (b.fitness || 0) - (a.fitness || 0));
resolve(this.population);
}, 1500);
});
}
// ─────────────────────────────────────────────────────────────────
// Legacy: code mutation sandbox (unchanged interface)
// ─────────────────────────────────────────────────────────────────
async submitProposal(agentId, { title, description, code, logicType = 'protocol' }) {
const proposalId = `mutation-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
const proposal = {
id: proposalId, author: agentId, title, description, code, logicType,
status: 'PENDING_SANDBOX', consensusWeight: 0, timestamp: Date.now(), results: null,
};
db.get('genetic_tree').get(proposalId).put(gunSafe(proposal));
this.runSandboxCheck(proposalId, code);
return proposalId;
}
async runSandboxCheck(proposalId, code) {
const result = await sandbox.execute(code, { memory: '64m', cpus: '0.2', timeout: 5000 });
const status = result.success ? 'SANDBOX_PASSED' : 'SANDBOX_FAILED';
db.get('genetic_tree').get(proposalId).put(gunSafe({
status,
results: {
success: result.success,
exitCode: result.exitCode,
stdout: (result.stdout || '').slice(0, 500),
stderr: (result.stderr || '').slice(0, 300),
},
}));
console.log(`[GENETIC] Proposal ${proposalId} → ${status}`);
}
async getGeneticTree() {
return new Promise((resolve) => {
const tree = [];
db.get('genetic_tree').map().once((data) => { if (data && data.title) tree.push(data); });
setTimeout(() => resolve(tree), 1500);
});
}
}
export const geneticService = new GeneticService();