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
| Cycle | Rigor | Creativity | Risk Tolerance | Outcome | Notes |
|---|
| 1 | 0.7 | 0.35 | 0.4 | Failed | Repair too conservative |
| 2 | 0.8 | 0.35 | 0.3 | Success | Tightened rigor after error |
| 3 | 0.8 | 0.45 | 0.35 | Success | Nudged creativity for opportunity |
| 4 | 0.8 | 0.45 | 0.35 | Success | Best-known config, no mutation |
| 5 | 0.8 | 0.45 | 0.35 | Success | Stable 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