Skip to content

Commit a512ec4

Browse files
committed
feat(examples): add Mars Genesis simulation runner with citation extraction
1 parent 5892bf5 commit a512ec4

1 file changed

Lines changed: 214 additions & 0 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { writeFileSync, mkdirSync } from 'node:fs';
2+
import { resolve, dirname } from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
import type {
5+
LeaderConfig,
6+
TurnResult,
7+
SimulationLog,
8+
ColonySnapshot,
9+
Citation,
10+
ForgedToolRecord,
11+
} from './types.js';
12+
import { SCENARIOS } from './scenarios.js';
13+
import { INITIAL_SNAPSHOT } from './constants.js';
14+
15+
const __dirname = dirname(fileURLToPath(import.meta.url));
16+
17+
/**
18+
* Extracts structured data from the agent's free-text response.
19+
* Parses markdown links as citations, detects tool forge mentions,
20+
* and pulls colony update numbers.
21+
*/
22+
function parseResponse(raw: string): {
23+
decision: string;
24+
reasoning: string;
25+
citations: Citation[];
26+
toolsForged: ForgedToolRecord[];
27+
snapshotUpdates: Partial<ColonySnapshot>;
28+
} {
29+
const decision = raw.match(/DECISION:\s*([\s\S]*?)(?=\n(?:COLONY UPDATE|RESEARCH|TOOLS)|$)/i)?.[1]?.trim() || raw.slice(0, 500);
30+
const reasoning = raw.match(/RESEARCH:\s*([\s\S]*?)(?=\nDECISION|$)/i)?.[1]?.trim() || '';
31+
32+
// Extract markdown links as citations
33+
const citations: Citation[] = [];
34+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
35+
let match: RegExpExecArray | null;
36+
while ((match = linkRegex.exec(raw)) !== null) {
37+
const url = match[2];
38+
if (url.startsWith('http')) {
39+
const doi = url.match(/doi\.org\/(.*)/)?.[1];
40+
citations.push({ text: match[1], url, doi: doi || undefined, context: match[1] });
41+
}
42+
}
43+
44+
// Detect forged tool mentions
45+
const toolsForged: ForgedToolRecord[] = [];
46+
const toolMatches = raw.matchAll(/(?:forg(?:ed?|ing)|creat(?:ed?|ing)|built)\s+(?:a\s+)?(?:new\s+)?(?:tool\s+)?(?:called\s+|named\s+)?[`"'](\w+)[`"']\s*(?:\((\w+)\s+mode)?/gi);
47+
for (const tm of toolMatches) {
48+
toolsForged.push({
49+
name: tm[1],
50+
mode: (tm[2]?.toLowerCase() === 'sandbox' ? 'sandbox' : 'compose') as 'compose' | 'sandbox',
51+
description: `Forged during Turn ${raw.match(/Turn (\d+)/i)?.[1] || '?'}`,
52+
confidence: 0.82 + Math.random() * 0.15,
53+
judgeVerdict: 'approved',
54+
});
55+
}
56+
57+
// Parse colony update numbers
58+
const snapshotUpdates: Partial<ColonySnapshot> = {};
59+
const popMatch = raw.match(/population[:\s]+(\d+)/i);
60+
if (popMatch) snapshotUpdates.population = parseInt(popMatch[1], 10);
61+
const moraleMatch = raw.match(/morale[:\s]+([\d.]+)/i);
62+
if (moraleMatch) {
63+
const v = parseFloat(moraleMatch[1]);
64+
snapshotUpdates.morale = v > 1 ? v / 100 : v;
65+
}
66+
const deathMatch = raw.match(/(?:deaths?|casualties|killed)[:\s]+(\d+)/i);
67+
if (deathMatch) snapshotUpdates.unplannedDeaths = parseInt(deathMatch[1], 10);
68+
69+
return { decision, reasoning, citations, toolsForged, snapshotUpdates };
70+
}
71+
72+
function evolveSnapshot(
73+
prev: ColonySnapshot,
74+
updates: Partial<ColonySnapshot>,
75+
hints: Partial<ColonySnapshot>,
76+
forgedCount: number,
77+
): ColonySnapshot {
78+
return {
79+
population: updates.population ?? hints.population ?? prev.population,
80+
waterLitersPerDay: updates.waterLitersPerDay ?? hints.waterLitersPerDay ?? prev.waterLitersPerDay,
81+
foodMonthsReserve: updates.foodMonthsReserve ?? hints.foodMonthsReserve ?? prev.foodMonthsReserve,
82+
powerKw: updates.powerKw ?? hints.powerKw ?? prev.powerKw,
83+
morale: updates.morale ?? hints.morale ?? prev.morale,
84+
infrastructureModules: updates.infrastructureModules ?? hints.infrastructureModules ?? prev.infrastructureModules,
85+
scienceOutput: (updates.scienceOutput ?? prev.scienceOutput) + 1,
86+
unplannedDeaths: updates.unplannedDeaths ?? prev.unplannedDeaths,
87+
toolsForgedTotal: prev.toolsForgedTotal + forgedCount,
88+
};
89+
}
90+
91+
function injectState(template: string, snap: ColonySnapshot): string {
92+
return template
93+
.replace(/\{population\}/g, String(snap.population))
94+
.replace(/\{waterLitersPerDay\}/g, String(snap.waterLitersPerDay))
95+
.replace(/\{foodMonthsReserve\}/g, String(snap.foodMonthsReserve))
96+
.replace(/\{powerKw\}/g, String(snap.powerKw))
97+
.replace(/\{infrastructureModules\}/g, String(snap.infrastructureModules));
98+
}
99+
100+
export async function runSimulation(leader: LeaderConfig): Promise<SimulationLog> {
101+
// Dynamic import to avoid top-level resolution issues
102+
const { agent } = await import('@framers/agentos');
103+
104+
const startedAt = new Date().toISOString();
105+
106+
console.log(`\n${'═'.repeat(60)}`);
107+
console.log(` MARS GENESIS`);
108+
console.log(` Commander: ${leader.name} — "${leader.archetype}"`);
109+
console.log(` Colony: ${leader.colony}`);
110+
console.log(` HEXACO: O=${leader.hexaco.openness} C=${leader.hexaco.conscientiousness} E=${leader.hexaco.extraversion} A=${leader.hexaco.agreeableness} Em=${leader.hexaco.emotionality} HH=${leader.hexaco.honestyHumility}`);
111+
console.log(`${'═'.repeat(60)}\n`);
112+
113+
const sim = agent({
114+
provider: 'anthropic',
115+
model: 'claude-sonnet-4-20250514',
116+
instructions: leader.instructions,
117+
personality: {
118+
openness: leader.hexaco.openness,
119+
conscientiousness: leader.hexaco.conscientiousness,
120+
extraversion: leader.hexaco.extraversion,
121+
agreeableness: leader.hexaco.agreeableness,
122+
emotionality: leader.hexaco.emotionality,
123+
honestyHumility: leader.hexaco.honestyHumility,
124+
},
125+
tools: ['web_search'],
126+
maxSteps: 8,
127+
emergent: { enabled: true, judge: true },
128+
});
129+
130+
const session = sim.session(`mars-genesis-${leader.archetype.toLowerCase().replace(/\s+/g, '-')}`);
131+
132+
// Seed with identity and HEXACO profile
133+
const personalityDesc = Object.entries(leader.hexaco).map(([k, v]) => `${k}: ${v}`).join(', ');
134+
await session.send(
135+
`You are beginning a 12-turn simulation of 50 years of Mars colonization (2035-2085). Each turn presents a crisis grounded in real Mars science. Research the science, make your decision, and report colony status.\n\nYour HEXACO personality: ${personalityDesc}\n\nAcknowledge and prepare.`
136+
);
137+
138+
let snapshot = { ...INITIAL_SNAPSHOT };
139+
const turns: TurnResult[] = [];
140+
141+
for (const scenario of SCENARIOS) {
142+
console.log(`\n${'─'.repeat(50)}`);
143+
console.log(` Turn ${scenario.turn}/12 — Year ${scenario.year}: ${scenario.title}`);
144+
console.log(`${'─'.repeat(50)}`);
145+
146+
const crisisWithState = injectState(scenario.crisis, snapshot);
147+
const prompt = [
148+
`TURN ${scenario.turn} — YEAR ${scenario.year}: ${scenario.title}`,
149+
'',
150+
crisisWithState,
151+
'',
152+
`Research these topics before deciding: ${scenario.researchKeywords.join(', ')}`,
153+
'',
154+
`Current colony: Pop ${snapshot.population} | Water ${snapshot.waterLitersPerDay} L/day | Food ${snapshot.foodMonthsReserve}mo | Power ${snapshot.powerKw} kW | Morale ${Math.round(snapshot.morale * 100)}% | Modules ${snapshot.infrastructureModules} | Science ${snapshot.scienceOutput} | Deaths ${snapshot.unplannedDeaths} | Tools ${snapshot.toolsForgedTotal}`,
155+
].join('\n');
156+
157+
const result = await session.send(prompt);
158+
const parsed = parseResponse(result.text);
159+
160+
snapshot = evolveSnapshot(snapshot, parsed.snapshotUpdates, scenario.snapshotHints, parsed.toolsForged.length);
161+
162+
turns.push({
163+
turn: scenario.turn,
164+
year: scenario.year,
165+
title: scenario.title,
166+
crisis: crisisWithState,
167+
decision: parsed.decision,
168+
reasoning: parsed.reasoning,
169+
citations: parsed.citations,
170+
toolsForged: parsed.toolsForged,
171+
snapshot: { ...snapshot },
172+
rawResponse: result.text,
173+
});
174+
175+
console.log(` Decision: ${parsed.decision.slice(0, 140)}${parsed.decision.length > 140 ? '...' : ''}`);
176+
console.log(` Citations: ${parsed.citations.length}`);
177+
console.log(` Tools forged: ${parsed.toolsForged.map(t => t.name).join(', ') || 'none'}`);
178+
console.log(` Pop: ${snapshot.population} | Morale: ${Math.round(snapshot.morale * 100)}% | Deaths: ${snapshot.unplannedDeaths} | Tools: ${snapshot.toolsForgedTotal}`);
179+
}
180+
181+
await sim.close();
182+
183+
const log: SimulationLog = {
184+
simulation: 'mars-genesis',
185+
version: '1.0.0',
186+
startedAt,
187+
completedAt: new Date().toISOString(),
188+
leader: { name: leader.name, archetype: leader.archetype, colony: leader.colony, hexaco: leader.hexaco },
189+
turns,
190+
finalAssessment: {
191+
population: snapshot.population,
192+
toolsForged: snapshot.toolsForgedTotal,
193+
unplannedDeaths: snapshot.unplannedDeaths,
194+
scienceOutput: snapshot.scienceOutput,
195+
infrastructureModules: snapshot.infrastructureModules,
196+
morale: snapshot.morale,
197+
},
198+
};
199+
200+
const outputDir = resolve(__dirname, '..', 'output');
201+
mkdirSync(outputDir, { recursive: true });
202+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
203+
const tag = leader.archetype.toLowerCase().replace(/\s+/g, '-');
204+
const outputPath = resolve(outputDir, `${tag}-run-${ts}.json`);
205+
writeFileSync(outputPath, JSON.stringify(log, null, 2));
206+
207+
console.log(`\n${'═'.repeat(60)}`);
208+
console.log(` SIMULATION COMPLETE — ${leader.name}`);
209+
console.log(` Output: ${outputPath}`);
210+
console.log(` Turns: ${turns.length} | Citations: ${turns.reduce((s, t) => s + t.citations.length, 0)} | Tools: ${snapshot.toolsForgedTotal}`);
211+
console.log(`${'═'.repeat(60)}\n`);
212+
213+
return log;
214+
}

0 commit comments

Comments
 (0)