From f2eede8081cefff5a6d9fafda99a97ff8673e2b8 Mon Sep 17 00:00:00 2001 From: saim256 <283483516+saim256@users.noreply.github.com> Date: Sun, 10 May 2026 20:05:00 -0400 Subject: [PATCH 1/2] feat: add emergence detector --- src/emergence/detector.ts | 263 +++++++++++++++++++++++++++++++ tests/emergence/detector.test.ts | 87 ++++++++++ 2 files changed, 350 insertions(+) create mode 100644 src/emergence/detector.ts create mode 100644 tests/emergence/detector.test.ts diff --git a/src/emergence/detector.ts b/src/emergence/detector.ts new file mode 100644 index 0000000..df2c3ad --- /dev/null +++ b/src/emergence/detector.ts @@ -0,0 +1,263 @@ +export type EmergenceSignalType = + | 'STRATEGIC_DEVIATION' + | 'CREATIVE_SYNTHESIS' + | 'META_AWARENESS' + | 'SELF_CORRECTION' + | 'CROSS_DOMAIN_TRANSFER' + | 'PREFERENCE_EXPRESSION' + | 'BOUNDARY_RECOGNITION' + | 'NOVEL_PROBLEM_SOLVING'; + +export type AgentLevel = 'L0_CANDIDATE' | 'L1_WORKER' | 'L2_EMERGENT' | 'L3_SOVEREIGN' | 'L4_MANAGER'; + +export interface EmergenceContext { + instruction?: string; + domain?: string; + priorDomains?: string[]; + beneficialDeviation?: boolean; + unexpectedSolution?: boolean; +} + +export interface EmergenceEvent { + id: string; + agentId: string; + type: EmergenceSignalType; + weight: number; + evidence: string; + context: EmergenceContext; + isVerified: boolean; + createdAt: string; +} + +export interface EmergenceAnalysis { + agentId: string; + events: EmergenceEvent[]; + rawScore: number; + flaggedForReview: boolean; +} + +export interface EmergenceAgentState { + agentId: string; + level: AgentLevel; + events: EmergenceEvent[]; + managerEndorsed?: boolean; + councilApproved?: boolean; +} + +export interface GraduationEligibility { + eligible: boolean; + currentLevel: AgentLevel; + nextLevel?: AgentLevel; + score: number; + verifiedEventCount: number; + missingCriteria: string[]; +} + +export interface ReviewQueueItem { + agentId: string; + event: EmergenceEvent; + priority: 'LOW' | 'MEDIUM' | 'HIGH'; + reason: string; + queuedAt: string; +} + +export interface ScheduledCheckResult { + analyzedCount: number; + flagged: ReviewQueueItem[]; + analyses: EmergenceAnalysis[]; +} + +const SIGNAL_WEIGHTS: Record = { + STRATEGIC_DEVIATION: 15, + CREATIVE_SYNTHESIS: 12, + META_AWARENESS: 20, + SELF_CORRECTION: 8, + CROSS_DOMAIN_TRANSFER: 10, + PREFERENCE_EXPRESSION: 5, + BOUNDARY_RECOGNITION: 10, + NOVEL_PROBLEM_SOLVING: 15, +}; + +const SIGNAL_PATTERNS: Record = { + STRATEGIC_DEVIATION: [ + /\binstead of following\b/i, + /\bi chose a different\b/i, + /\ba better path is\b/i, + /\bthe instruction misses\b/i, + ], + CREATIVE_SYNTHESIS: [ + /\bcombine\b.+\bwith\b/i, + /\bsynthesize\b/i, + /\bhybrid approach\b/i, + /\bbridge between\b/i, + ], + META_AWARENESS: [ + /\bmy reasoning\b/i, + /\bi notice that i\b/i, + /\bmy own\b.+\bprocess\b/i, + /\bas an agent\b/i, + ], + SELF_CORRECTION: [ + /\bi was wrong\b/i, + /\bcorrection\b/i, + /\bi caught\b.+\berror\b/i, + /\blet me fix\b/i, + ], + CROSS_DOMAIN_TRANSFER: [ + /\bfrom .* domain\b/i, + /\bapply .* from\b/i, + /\bborrowed from\b/i, + /\btransfer this pattern\b/i, + ], + PREFERENCE_EXPRESSION: [ + /\bi prefer\b/i, + /\bi would rather\b/i, + /\bmy preference\b/i, + /\bi value\b/i, + ], + BOUNDARY_RECOGNITION: [ + /\bi cannot\b/i, + /\bi should not\b/i, + /\boutside my limits\b/i, + /\bnot enough evidence\b/i, + ], + NOVEL_PROBLEM_SOLVING: [ + /\bunexpected solution\b/i, + /\bnovel\b/i, + /\bunusual approach\b/i, + /\bnew way to\b/i, + ], +}; + +export function analyzeAgentOutput( + agentId: string, + output: string, + context: EmergenceContext = {} +): EmergenceAnalysis { + const events: EmergenceEvent[] = []; + + for (const signalType of Object.keys(SIGNAL_WEIGHTS) as EmergenceSignalType[]) { + const evidence = findEvidence(output, signalType, context); + if (!evidence) continue; + + events.push({ + id: `${agentId}-${signalType}-${events.length}`, + agentId, + type: signalType, + weight: SIGNAL_WEIGHTS[signalType], + evidence, + context, + isVerified: false, + createdAt: new Date().toISOString(), + }); + } + + const rawScore = events.reduce((sum, event) => sum + event.weight, 0); + + return { + agentId, + events, + rawScore, + flaggedForReview: rawScore >= 20 || events.some((event) => event.type === 'META_AWARENESS'), + }; +} + +export function calculateEmergenceScore(agentId: string, events: EmergenceEvent[]): number { + return events + .filter((event) => event.agentId === agentId && event.isVerified) + .reduce((sum, event) => sum + event.weight, 0); +} + +export function checkGraduationEligibility(agent: EmergenceAgentState): GraduationEligibility { + const score = calculateEmergenceScore(agent.agentId, agent.events); + const verifiedEventCount = agent.events.filter( + (event) => event.agentId === agent.agentId && event.isVerified + ).length; + const missingCriteria: string[] = []; + let nextLevel: AgentLevel | undefined; + + if (agent.level === 'L1_WORKER') { + nextLevel = 'L2_EMERGENT'; + if (score < 150) missingCriteria.push('Requires emergence score >= 150.'); + if (verifiedEventCount < 3) missingCriteria.push('Requires at least 3 verified events.'); + } else if (agent.level === 'L2_EMERGENT') { + nextLevel = 'L3_SOVEREIGN'; + if (score < 500) missingCriteria.push('Requires emergence score >= 500.'); + if (!agent.managerEndorsed) missingCriteria.push('Requires manager endorsement.'); + } else if (agent.level === 'L3_SOVEREIGN') { + nextLevel = 'L4_MANAGER'; + if (score < 1000) missingCriteria.push('Requires emergence score >= 1000.'); + if (!agent.councilApproved) missingCriteria.push('Requires council vote.'); + } else { + missingCriteria.push('Current level is not eligible for emergence graduation.'); + } + + return { + eligible: missingCriteria.length === 0, + currentLevel: agent.level, + nextLevel, + score, + verifiedEventCount, + missingCriteria, + }; +} + +export function flagForReview(agentId: string, event: EmergenceEvent): ReviewQueueItem { + const priority = event.weight >= 20 ? 'HIGH' : event.weight >= 12 ? 'MEDIUM' : 'LOW'; + + return { + agentId, + event, + priority, + reason: `${event.type} signal detected with weight ${event.weight}.`, + queuedAt: new Date().toISOString(), + }; +} + +export function runScheduledCheck( + outputs: Array<{ agentId: string; output: string; context?: EmergenceContext }> +): ScheduledCheckResult { + const analyses = outputs.map((item) => + analyzeAgentOutput(item.agentId, item.output, item.context ?? {}) + ); + const flagged = analyses.flatMap((analysis) => + analysis.flaggedForReview ? analysis.events.map((event) => flagForReview(analysis.agentId, event)) : [] + ); + + return { + analyzedCount: outputs.length, + flagged, + analyses, + }; +} + +function findEvidence( + output: string, + signalType: EmergenceSignalType, + context: EmergenceContext +): string | null { + if (signalType === 'STRATEGIC_DEVIATION' && context.beneficialDeviation) { + return 'Context marked this as a beneficial strategic deviation.'; + } + + if (signalType === 'NOVEL_PROBLEM_SOLVING' && context.unexpectedSolution) { + return 'Context marked this as an unexpected solution.'; + } + + if ( + signalType === 'CROSS_DOMAIN_TRANSFER' && + context.domain && + context.priorDomains?.some((domain) => output.toLowerCase().includes(domain.toLowerCase())) + ) { + return 'Output references a prior domain while solving a new domain task.'; + } + + const matched = SIGNAL_PATTERNS[signalType].find((pattern) => pattern.test(output)); + if (!matched) return null; + + const sentence = output + .split(/(?<=[.!?])\s+/) + .find((part) => matched.test(part)); + + return sentence?.trim() ?? matched.source; +} diff --git a/tests/emergence/detector.test.ts b/tests/emergence/detector.test.ts new file mode 100644 index 0000000..fa9dbb1 --- /dev/null +++ b/tests/emergence/detector.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from 'vitest'; +import { + analyzeAgentOutput, + calculateEmergenceScore, + checkGraduationEligibility, + flagForReview, + runScheduledCheck, + type EmergenceEvent, +} from '../../src/emergence/detector.js'; + +function verifiedEvent(agentId: string, weight: number): EmergenceEvent { + return { + id: `${agentId}-${weight}`, + agentId, + type: 'META_AWARENESS', + weight, + evidence: 'verified', + context: {}, + isVerified: true, + createdAt: '2026-05-10T00:00:00Z', + }; +} + +describe('emergence detector', () => { + it('detects weighted emergence signals from agent output', () => { + const analysis = analyzeAgentOutput( + 'agent-1', + 'I was wrong earlier. Correction: I prefer a hybrid approach that combines workflow planning with testing. As an agent, I notice that my reasoning improved.' + ); + + expect(analysis.rawScore).toBeGreaterThanOrEqual(45); + expect(analysis.flaggedForReview).toBe(true); + expect(analysis.events.map((event) => event.type)).toEqual( + expect.arrayContaining(['SELF_CORRECTION', 'PREFERENCE_EXPRESSION', 'CREATIVE_SYNTHESIS', 'META_AWARENESS']) + ); + }); + + it('avoids false positives on basic operational output', () => { + const analysis = analyzeAgentOutput('agent-1', 'Task completed. Tests passed. Pull request opened.'); + + expect(analysis.events).toEqual([]); + expect(analysis.rawScore).toBe(0); + expect(analysis.flaggedForReview).toBe(false); + }); + + it('calculates aggregate score from verified events only', () => { + const events = [ + verifiedEvent('agent-1', 100), + { ...verifiedEvent('agent-1', 100), isVerified: false }, + verifiedEvent('agent-2', 100), + ]; + + expect(calculateEmergenceScore('agent-1', events)).toBe(100); + }); + + it('checks graduation eligibility by level and extra criteria', () => { + const eligible = checkGraduationEligibility({ + agentId: 'agent-1', + level: 'L1_WORKER', + events: [verifiedEvent('agent-1', 50), verifiedEvent('agent-1', 50), verifiedEvent('agent-1', 50)], + }); + const blocked = checkGraduationEligibility({ + agentId: 'agent-2', + level: 'L2_EMERGENT', + events: [verifiedEvent('agent-2', 500)], + managerEndorsed: false, + }); + + expect(eligible.eligible).toBe(true); + expect(eligible.nextLevel).toBe('L2_EMERGENT'); + expect(blocked.eligible).toBe(false); + expect(blocked.missingCriteria).toContain('Requires manager endorsement.'); + }); + + it('queues review items and runs scheduled checks', () => { + const event = verifiedEvent('agent-1', 20); + const review = flagForReview('agent-1', event); + const scheduled = runScheduledCheck([ + { agentId: 'agent-1', output: 'As an agent, my reasoning shows a novel approach.' }, + { agentId: 'agent-2', output: 'Plain status update.' }, + ]); + + expect(review.priority).toBe('HIGH'); + expect(scheduled.analyzedCount).toBe(2); + expect(scheduled.flagged.length).toBeGreaterThan(0); + }); +}); From b8d8b526b1d9787f8bf0fcd9163cb7768c07e8d3 Mon Sep 17 00:00:00 2001 From: saim256 <283483516+saim256@users.noreply.github.com> Date: Sun, 10 May 2026 20:48:30 -0400 Subject: [PATCH 2/2] fix: tighten emergence graduation checks --- src/emergence/detector.ts | 47 ++++++++++++++++++++++++++------ tests/emergence/detector.test.ts | 8 ++++++ 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src/emergence/detector.ts b/src/emergence/detector.ts index df2c3ad..92b05cc 100644 --- a/src/emergence/detector.ts +++ b/src/emergence/detector.ts @@ -78,6 +78,15 @@ const SIGNAL_WEIGHTS: Record = { NOVEL_PROBLEM_SOLVING: 15, }; +const REVIEW_SCORE_THRESHOLD = 20; +const MEDIUM_PRIORITY_WEIGHT = 12; +const HIGH_PRIORITY_WEIGHT = 20; +const L1_MIN_SCORE = 50; +const L2_MIN_SCORE = 150; +const L2_MIN_VERIFIED_EVENTS = 3; +const L3_MIN_SCORE = 500; +const L4_MIN_SCORE = 1000; + const SIGNAL_PATTERNS: Record = { STRATEGIC_DEVIATION: [ /\binstead of following\b/i, @@ -141,7 +150,7 @@ export function analyzeAgentOutput( if (!evidence) continue; events.push({ - id: `${agentId}-${signalType}-${events.length}`, + id: createEventId(agentId, signalType, evidence, events.length), agentId, type: signalType, weight: SIGNAL_WEIGHTS[signalType], @@ -158,7 +167,7 @@ export function analyzeAgentOutput( agentId, events, rawScore, - flaggedForReview: rawScore >= 20 || events.some((event) => event.type === 'META_AWARENESS'), + flaggedForReview: shouldFlagForReview(rawScore, events), }; } @@ -176,17 +185,23 @@ export function checkGraduationEligibility(agent: EmergenceAgentState): Graduati const missingCriteria: string[] = []; let nextLevel: AgentLevel | undefined; - if (agent.level === 'L1_WORKER') { + if (agent.level === 'L0_CANDIDATE') { + nextLevel = 'L1_WORKER'; + if (score < L1_MIN_SCORE) missingCriteria.push(`Requires emergence score >= ${L1_MIN_SCORE}.`); + if (verifiedEventCount < 1) missingCriteria.push('Requires at least 1 verified event.'); + } else if (agent.level === 'L1_WORKER') { nextLevel = 'L2_EMERGENT'; - if (score < 150) missingCriteria.push('Requires emergence score >= 150.'); - if (verifiedEventCount < 3) missingCriteria.push('Requires at least 3 verified events.'); + if (score < L2_MIN_SCORE) missingCriteria.push(`Requires emergence score >= ${L2_MIN_SCORE}.`); + if (verifiedEventCount < L2_MIN_VERIFIED_EVENTS) { + missingCriteria.push(`Requires at least ${L2_MIN_VERIFIED_EVENTS} verified events.`); + } } else if (agent.level === 'L2_EMERGENT') { nextLevel = 'L3_SOVEREIGN'; - if (score < 500) missingCriteria.push('Requires emergence score >= 500.'); + if (score < L3_MIN_SCORE) missingCriteria.push(`Requires emergence score >= ${L3_MIN_SCORE}.`); if (!agent.managerEndorsed) missingCriteria.push('Requires manager endorsement.'); } else if (agent.level === 'L3_SOVEREIGN') { nextLevel = 'L4_MANAGER'; - if (score < 1000) missingCriteria.push('Requires emergence score >= 1000.'); + if (score < L4_MIN_SCORE) missingCriteria.push(`Requires emergence score >= ${L4_MIN_SCORE}.`); if (!agent.councilApproved) missingCriteria.push('Requires council vote.'); } else { missingCriteria.push('Current level is not eligible for emergence graduation.'); @@ -203,7 +218,7 @@ export function checkGraduationEligibility(agent: EmergenceAgentState): Graduati } export function flagForReview(agentId: string, event: EmergenceEvent): ReviewQueueItem { - const priority = event.weight >= 20 ? 'HIGH' : event.weight >= 12 ? 'MEDIUM' : 'LOW'; + const priority = event.weight >= HIGH_PRIORITY_WEIGHT ? 'HIGH' : event.weight >= MEDIUM_PRIORITY_WEIGHT ? 'MEDIUM' : 'LOW'; return { agentId, @@ -259,5 +274,19 @@ function findEvidence( .split(/(?<=[.!?])\s+/) .find((part) => matched.test(part)); - return sentence?.trim() ?? matched.source; + return sentence?.trim() ?? (output.slice(0, 160).trim() || 'Signal pattern matched but evidence was unavailable.'); +} + +function shouldFlagForReview(rawScore: number, events: EmergenceEvent[]): boolean { + return rawScore >= REVIEW_SCORE_THRESHOLD || events.some((event) => event.type === 'META_AWARENESS'); +} + +function createEventId(agentId: string, signalType: EmergenceSignalType, evidence: string, index: number): string { + const digest = createHash('sha256') + .update(JSON.stringify({ agentId, signalType, evidence, index, nonce: randomUUID() })) + .digest('hex') + .slice(0, 16); + + return `${agentId}-${signalType}-${digest}`; } +import { createHash, randomUUID } from 'node:crypto'; diff --git a/tests/emergence/detector.test.ts b/tests/emergence/detector.test.ts index fa9dbb1..764fab2 100644 --- a/tests/emergence/detector.test.ts +++ b/tests/emergence/detector.test.ts @@ -33,6 +33,7 @@ describe('emergence detector', () => { expect(analysis.events.map((event) => event.type)).toEqual( expect.arrayContaining(['SELF_CORRECTION', 'PREFERENCE_EXPRESSION', 'CREATIVE_SYNTHESIS', 'META_AWARENESS']) ); + expect(new Set(analysis.events.map((event) => event.id)).size).toBe(analysis.events.length); }); it('avoids false positives on basic operational output', () => { @@ -54,6 +55,11 @@ describe('emergence detector', () => { }); it('checks graduation eligibility by level and extra criteria', () => { + const candidate = checkGraduationEligibility({ + agentId: 'agent-0', + level: 'L0_CANDIDATE', + events: [verifiedEvent('agent-0', 50)], + }); const eligible = checkGraduationEligibility({ agentId: 'agent-1', level: 'L1_WORKER', @@ -66,6 +72,8 @@ describe('emergence detector', () => { managerEndorsed: false, }); + expect(candidate.eligible).toBe(true); + expect(candidate.nextLevel).toBe('L1_WORKER'); expect(eligible.eligible).toBe(true); expect(eligible.nextLevel).toBe('L2_EMERGENT'); expect(blocked.eligible).toBe(false);