Skip to content

@cognitive-swarm/evolution

npm

Self-evolving swarm: agents detect expertise gaps, propose new specialists, and dissolve underperformers.

Install

bash
npm install @cognitive-swarm/evolution

SwarmEvolver

typescript
import { SwarmEvolver } from '@cognitive-swarm/evolution'

const evolver = new SwarmEvolver(llmProvider, {
  minVotesForSpawn: 2,
  approvalThreshold: 0.6,
  minValueForKeep: 0.3,
  evaluationWindow: 3,
})

The evolver manages the full lifecycle:

  1. Agents detect gaps in collective expertise → reportGap()
  2. Other agents confirm the gap → confirmGap()
  3. When enough confirmations, spawn a new specialist → proposeSpawn()
  4. Track spawned agent contribution → evaluate()
  5. Dissolve agents that don't provide value → suggestPrune()

Gap Detection

typescript
// Agent detects a gap
evolver.reportGap({
  id: 'g1',
  detectedBy: 'agent-1',
  domain: 'reverse-engineering',
  reason: 'Found obfuscated code that needs binary analysis',
  urgency: 0.8,
  timestamp: Date.now(),
})

// Another agent confirms the gap
evolver.confirmGap('g1', 'agent-2')

// Check confirmation count
evolver.getConfirmationCount('g1')  // 2

// Dismiss a gap
evolver.dismissGap('g1', 'agent-3')
typescript
interface GapSignal {
  readonly id: string
  readonly detectedBy: string
  readonly domain: string
  readonly reason: string
  readonly suggestedRole?: string
  readonly urgency: number        // 0..1
  readonly timestamp: number
}

Spawning Agents

When enough agents confirm a gap, propose a new specialist:

typescript
const proposal = await evolver.proposeSpawn('g1', ['analyst', 'critic'])
// proposal is null if confirmations < minVotesForSpawn

if (proposal) {
  console.log(proposal.role)             // 'RE Specialist' (LLM-generated)
  console.log(proposal.roleDescription)  // generated description
  console.log(proposal.personality)      // generated personality vector
  console.log(proposal.temporary)        // true if urgency < 0.5
}

The role description and personality are generated by the LLM based on the gap context.

typescript
interface SpawnProposal {
  readonly id: string
  readonly gapId: string
  readonly role: string
  readonly roleDescription: string
  readonly personality: PersonalityVector
  readonly listens: readonly SignalType[]
  readonly canEmit: readonly SignalType[]
  readonly temporary: boolean
  readonly proposedBy: readonly string[]
  readonly votes: readonly VoteRecord[]
  readonly status: 'pending' | 'approved' | 'rejected'
}

Evaluating Spawned Agents

Track whether spawned agents provide value:

typescript
const result = evolver.evaluate(
  'spawned-agent-1',   // agentId
  12,                   // signalsSent
  3,                    // proposalsMade
  5,                    // roundsActive
)

console.log(result.valueScore)       // 0..1
console.log(result.recommendation)   // 'keep' | 'dissolve'
console.log(result.reason)

Value score: 0.4 * signalScore + 0.6 * proposalScore. Agents evaluated before evaluationWindow rounds always get "keep".

typescript
interface EvaluationResult {
  readonly agentId: string
  readonly valueScore: number
  readonly roundsActive: number
  readonly recommendation: 'keep' | 'dissolve'
  readonly reason: string
}

Pruning

Suggest agents to remove based on value scores and redundancy:

typescript
const redundancyScores = new Map([
  ['spawned-agent-1', 0.85],  // highly similar to another agent
  ['spawned-agent-2', 0.3],
])

const report = evolver.suggestPrune(redundancyScores)

for (const candidate of report.candidates) {
  console.log(`${candidate.agentId}: ${candidate.reason} (redundancy: ${candidate.redundancyScore})`)
}
typescript
interface PruneReport {
  readonly candidates: readonly PruneCandidate[]
  readonly pruneCount: number
}

interface PruneCandidate {
  readonly agentId: string
  readonly reason: string
  readonly redundancyScore: number
}

EvolverConfig

typescript
interface EvolverConfig {
  readonly minVotesForSpawn?: number      // default: 2
  readonly approvalThreshold?: number    // default: 0.6
  readonly minValueForKeep?: number      // default: 0.3
  readonly evaluationWindow?: number     // rounds before evaluation, default: 3
}

Usage via Orchestrator

The orchestrator uses evolution internally when configured:

typescript
const swarm = new SwarmOrchestrator({
  agents,
  evolution: {
    enabled: true,
    maxEvolvedAgents: 3,
    evaluationWindow: 5,
    minValueForKeep: 0.5,
    cooldownRounds: 3,
    nmiPruneThreshold: 0.8,
  },
})

const result = await swarm.solve('complex multi-domain task')
console.log(result.evolutionReport?.spawned)

Stream Events

typescript
for await (const event of swarm.solveWithStream('task')) {
  if (event.type === 'evolution:spawned') {
    // event.agentId, event.domain, event.reason
  }
  if (event.type === 'evolution:dissolved') {
    // event.agentId, event.reason
  }
}

Released under the Apache 2.0 License.