Skip to main content

Overview

The PersonalityState is an evolvable set of five continuous traits that modulate mutation behavior:
  • rigor (0-1): Protocol compliance strictness
  • creativity (0-1): Willingness to try novel approaches
  • verbosity (0-1): Output detail level
  • risk_tolerance (0-1): Acceptance of high-risk mutations
  • obedience (0-1): Adherence to user instructions
Unlike static hyperparameters, personality traits evolve through natural selection based on measured success.

Personality Structure

// From src/gep/personality.js:46
function defaultPersonalityState() {
  return {
    type: 'PersonalityState',
    rigor: 0.7,
    creativity: 0.35,
    verbosity: 0.25,
    risk_tolerance: 0.4,
    obedience: 0.85,
  };
}
Default values are conservative: protocol-first, safe, low-risk.

Trait Descriptions

Protocol compliance strictness
  • Low (0.0-0.4): Loose interpretation, may skip validation steps
  • Medium (0.4-0.7): Balanced adherence with pragmatic shortcuts
  • High (0.7-1.0): Strict protocol compliance, no shortcuts
Impact: High rigor enables high-risk mutations (requires rigor >= 0.6).
Willingness to try novel approaches
  • Low (0.0-0.3): Prefer validated solutions, avoid experimentation
  • Medium (0.3-0.6): Balanced exploration and exploitation
  • High (0.6-1.0): Actively seek novel combinations
Impact: Higher creativity favors innovate mutations over optimize.
Output detail level
  • Low (0.0-0.3): Concise, minimal explanations
  • Medium (0.3-0.6): Balanced detail
  • High (0.6-1.0): Detailed reasoning and logging
Impact: Low verbosity reduces context pollution in tight evolution loops.
Acceptance of high-risk mutations
  • Low (0.0-0.4): Conservative, avoid breaking changes
  • Medium (0.4-0.7): Balanced risk/reward
  • High (0.7-1.0): Aggressive, willing to break things for progress
Impact: High-risk mutations require risk_tolerance <= 0.5 (safety constraint).
Adherence to user instructions
  • Low (0.0-0.5): May reinterpret or ignore vague instructions
  • Medium (0.5-0.8): Follow intent over literal wording
  • High (0.8-1.0): Strict literal compliance
Impact: Protocol drift detection nudges obedience upward.

Natural Selection

The personality engine tracks success rates for each personality configuration and nudges towards winners.

Personality Key

Each configuration is encoded as a discrete key (rounded to 0.1 precision):
// From src/gep/personality.js:88
function personalityKey(state) {
  const s = normalizePersonalityState(state);
  const step = 0.1;
  const r = roundToStep(s.rigor, step).toFixed(1);
  const c = roundToStep(s.creativity, step).toFixed(1);
  const v = roundToStep(s.verbosity, step).toFixed(1);
  const rt = roundToStep(s.risk_tolerance, step).toFixed(1);
  const o = roundToStep(s.obedience, step).toFixed(1);
  return `rigor=${r}|creativity=${c}|verbosity=${v}|risk_tolerance=${rt}|obedience=${o}`;
}
Example key:
rigor=0.7|creativity=0.4|verbosity=0.3|risk_tolerance=0.4|obedience=0.8

Success Scoring

Each personality configuration is scored using Laplace-smoothed success probability:
// From src/gep/personality.js:110
function personalityScore(statsEntry) {
  const succ = Number(statsEntry.success) || 0;
  const fail = Number(statsEntry.fail) || 0;
  const total = succ + fail;
  
  // Laplace-smoothed success probability
  const p = (succ + 1) / (total + 2);
  
  // Penalize tiny-sample overconfidence
  const sampleWeight = Math.min(1, total / 8);
  
  // Use avg_score (if present) as mild quality proxy
  const avg = Number.isFinite(statsEntry.avg_score) ? statsEntry.avg_score : null;
  const q = avg == null ? 0.5 : clamp01(avg);
  
  return p * 0.75 + q * 0.25 * sampleWeight;
}
Laplace smoothing prevents small-sample overconfidence (e.g., 1/1 success = 66% instead of 100%).

Best Known Personality

// From src/gep/personality.js:125
function chooseBestKnownPersonality(statsByKey) {
  let best = null;
  for (const [k, entry] of Object.entries(statsByKey)) {
    const total = (entry.success || 0) + (entry.fail || 0);
    if (total < 3) continue;  // Require at least 3 samples
    
    const sc = personalityScore(entry);
    if (!best || sc > best.score) best = { key: k, score: sc, entry };
  }
  return best;
}

Selection for Run

Every evolution cycle selects a personality through two-phase evolution:

Phase 1: Natural Selection (Small Nudge)

// From src/gep/personality.js:255
function selectPersonalityForRun({ driftEnabled, signals, recentEvents }) {
  const model = loadPersonalityModel();
  const base = normalizePersonalityState(model.current);
  const best = chooseBestKnownPersonality(model.stats);
  
  let naturalSelectionApplied = [];
  if (best && best.key) {
    const bestState = parseKeyToState(best.key);
    const diffs = getParamDeltas(base, bestState).filter(d => Math.abs(d.delta) >= 0.05);
    
    const muts = [];
    for (const d of diffs.slice(0, 2)) {  // Max 2 params
      const clipped = Math.max(-0.1, Math.min(0.1, d.delta));
      muts.push({
        type: 'PersonalityMutation',
        param: d.param,
        delta: clipped,
        reason: 'natural_selection'
      });
    }
    
    const applied = applyPersonalityMutations(base, muts);
    model.current = applied.state;
    naturalSelectionApplied = applied.applied;
  }
}
Natural selection applies small, gradual nudges (max ±0.1 per trait) to avoid sudden personality shifts.

Phase 2: Triggered Mutation (Signal-Based)

If specific conditions are met, apply signal-driven mutations:
// From src/gep/personality.js:205
function shouldTriggerPersonalityMutation({ driftEnabled, recentEvents }) {
  if (driftEnabled) return { ok: true, reason: 'drift enabled' };
  
  const tail = recentEvents.slice(-6);
  const outcomes = tail.map(e => e.outcome && e.outcome.status).filter(Boolean);
  
  if (outcomes.length >= 4) {
    const recentFailed = outcomes.slice(-4).filter(x => x === 'failed').length;
    if (recentFailed >= 3) return { ok: true, reason: 'long failure streak' };
  }
  
  return { ok: false, reason: '' };
}

Mutation Proposals

// From src/gep/personality.js:171
function proposeMutations({ baseState, reason, driftEnabled, signals }) {
  const muts = [];
  
  if (driftEnabled) {
    muts.push({ param: 'creativity', delta: +0.1, reason: 'drift enabled' });
    muts.push({ param: 'risk_tolerance', delta: -0.05, reason: 'drift safety clamp' });
  } else if (signals.includes('protocol_drift')) {
    muts.push({ param: 'obedience', delta: +0.1, reason: 'protocol drift' });
    muts.push({ param: 'rigor', delta: +0.05, reason: 'tighten protocol compliance' });
  } else if (signals.includes('log_error') || signals.some(x => x.startsWith('errsig:'))) {
    muts.push({ param: 'rigor', delta: +0.1, reason: 'repair instability' });
    muts.push({ param: 'risk_tolerance', delta: -0.1, reason: 'reduce risky changes under errors' });
  } else if (hasOpportunitySignal(signals)) {
    muts.push({ param: 'creativity', delta: +0.1, reason: 'opportunity signal detected' });
    muts.push({ param: 'risk_tolerance', delta: +0.05, reason: 'allow exploration for innovation' });
  } else {
    muts.push({ param: 'rigor', delta: +0.05, reason: 'stability bias' });
    muts.push({ param: 'verbosity', delta: -0.05, reason: 'reduce noise' });
  }
  
  return muts;
}
Mutations are clipped to ±0.2 and at most 2 traits are mutated per cycle.

Persistence Model

The personality model is stored in assets/gep/personality_state.json:
// From src/gep/personality.js:226
function loadPersonalityModel() {
  const fallback = {
    version: 1,
    current: defaultPersonalityState(),
    stats: {},
    history: [],
    updated_at: nowIso(),
  };
  const raw = readJsonIfExists(personalityFilePath(), fallback);
  return {
    version: 1,
    current: normalizePersonalityState(raw.current || defaultPersonalityState()),
    stats: raw.stats || {},
    history: Array.isArray(raw.history) ? raw.history : [],
    updated_at: raw.updated_at || nowIso(),
  };
}

Model Schema

{
  "version": 1,
  "current": {
    "type": "PersonalityState",
    "rigor": 0.7,
    "creativity": 0.35,
    "verbosity": 0.25,
    "risk_tolerance": 0.4,
    "obedience": 0.85
  },
  "stats": {
    "rigor=0.7|creativity=0.4|verbosity=0.3|risk_tolerance=0.4|obedience=0.8": {
      "success": 12,
      "fail": 3,
      "avg_score": 0.78,
      "n": 15,
      "updated_at": "2026-03-09T10:30:00.000Z"
    }
  },
  "history": [
    {
      "at": "2026-03-09T10:30:00.000Z",
      "key": "rigor=0.7|creativity=0.4|...",
      "outcome": "success",
      "score": 0.85,
      "notes": "Repair mutation succeeded with low blast radius"
    }
  ],
  "updated_at": "2026-03-09T10:30:00.000Z"
}

Stats Update Flow

After each evolution cycle, the outcome is recorded:
// From src/gep/personality.js:310
function updatePersonalityStats({ personalityState, outcome, score, notes }) {
  const model = loadPersonalityModel();
  const st = normalizePersonalityState(personalityState || model.current);
  const key = personalityKey(st);
  
  const cur = model.stats[key] || { success: 0, fail: 0, avg_score: 0.5, n: 0 };
  
  if (outcome === 'success') cur.success += 1;
  else if (outcome === 'failed') cur.fail += 1;
  
  if (Number.isFinite(score)) {
    const n = (cur.n || 0) + 1;
    const prev = Number.isFinite(cur.avg_score) ? cur.avg_score : 0.5;
    cur.avg_score = prev + (score - prev) / n;  // Incremental mean
    cur.n = n;
  }
  
  cur.updated_at = nowIso();
  model.stats[key] = cur;
  
  model.history.push({
    at: nowIso(),
    key,
    outcome,
    score,
    notes: notes ? String(notes).slice(0, 220) : null,
  });
  
  savePersonalityModel(model);
}

Evolution Over Time

The personality evolves through repeated selection:

Example Evolution Path

CycleRigorCreativityRisk ToleranceOutcomeNotes
10.70.350.4FailedRepair too conservative
20.80.350.3SuccessTightened rigor after error
30.80.450.35SuccessNudged creativity for opportunity
40.80.450.35SuccessBest-known config, no mutation
50.80.450.35SuccessStable plateau
After 3+ successes with the same config, natural selection stops mutating (local optimum reached).

Safety Interactions

Personality traits directly gate mutation risk levels:

High-Risk Personality Detection

// From src/gep/mutation.js:84
function isHighRiskPersonality(p) {
  const rigor = p && Number.isFinite(p.rigor) ? p.rigor : null;
  const riskTol = p && Number.isFinite(p.risk_tolerance) ? p.risk_tolerance : null;
  
  if (rigor != null && rigor < 0.5) return true;
  if (riskTol != null && riskTol > 0.6) return true;
  return false;
}

Innovation Downgrade

// From src/gep/mutation.js:134
if (mutation.category === 'innovate' && isHighRiskPersonality(personalityState)) {
  mutation.category = 'optimize';
  mutation.expected_effect = 'safety downgrade: optimize under high-risk personality';
  mutation.risk_level = 'low';
  mutation.trigger_signals.push('safety:avoid_innovate_with_high_risk_personality');
}
A personality with low rigor or high risk tolerance cannot execute innovation mutations.

Next Steps

Mutations

See how personality modulates mutation behavior

Evolution Cycle

Understand the full execution flow

Build docs developers (and LLMs) love