paracosm 0.9.1048 → 0.9.1051

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,7 @@
1
1
  import { type NormalizedSimulationConfig } from './sim-config.js';
2
+ import { type ValidatedObjectOptions } from '../llm/generateValidatedObject.js';
3
+ import type { ZodType } from 'zod';
4
+ import type { ActorConfig } from '../runtime/orchestrator/index.js';
2
5
  import type { ScenarioPackage } from '../engine/types.js';
3
6
  /**
4
7
  * SSE broadcast contract. The optional `actorId` lets per-actor
@@ -79,4 +82,48 @@ onArtifact?: (artifact: import('../engine/schema/index.js').RunArtifact, leader:
79
82
  export declare function runBatchSimulations(simConfig: NormalizedSimulationConfig, broadcast: BroadcastFn, signal?: AbortSignal, scenario?: ScenarioPackage,
80
83
  /** Optional callback fired after each leader's artifact completes. */
81
84
  onArtifact?: (artifact: import('../engine/schema/index.js').RunArtifact, leader: import('../runtime/orchestrator/index.js').ActorConfig) => void | Promise<void>): Promise<void>;
85
+ /** One finished run handed to a verdict call. */
86
+ export interface JudgedRun {
87
+ leader: ActorConfig;
88
+ /** Launch-order index (cohort runs). */
89
+ index: number;
90
+ /** The completed RunArtifact. */
91
+ result: any;
92
+ }
93
+ /** What a verdict call needs besides the runs it judges. */
94
+ export interface VerdictCallOptions {
95
+ /** Provider and keys; the run's normalized config satisfies it. */
96
+ credentials: Pick<NormalizedSimulationConfig, 'provider' | 'apiKey' | 'anthropicKey'>;
97
+ verdictModel: string;
98
+ turns: number;
99
+ /** The scenario the runs played; its labels and scores frame the verdict. */
100
+ scenario: ScenarioPackage;
101
+ broadcast: BroadcastFn;
102
+ /**
103
+ * Replaces the provider call (tests). Production callers omit it and
104
+ * generateValidatedObject uses AgentOS.
105
+ */
106
+ generateObjectImpl?: ValidatedObjectOptions<ZodType>['_generateObjectImpl'];
107
+ }
108
+ /**
109
+ * Pair verdict: builds the judge request for two finished runs from the
110
+ * scenario's own scores and words, calls the verdict model and
111
+ * broadcasts `verdict`, or `verdict_skipped` with the reason when the
112
+ * request or the model call fails. Never throws, so the run still
113
+ * reaches its `complete` event.
114
+ */
115
+ export declare function judgePair(opts: VerdictCallOptions & {
116
+ a: JudgedRun;
117
+ b: JudgedRun;
118
+ }): Promise<void>;
119
+ /**
120
+ * Cohort verdict (the clean runs of a batch): builds the ranking
121
+ * request from the scenario's own scores and words, calls the verdict
122
+ * model and broadcasts `cohort_verdict`, or `verdict_skipped` with the
123
+ * reason when the request or the model call fails. Never throws, so the
124
+ * run still reaches its `complete` event.
125
+ */
126
+ export declare function judgeCohort(opts: VerdictCallOptions & {
127
+ runs: JudgedRun[];
128
+ }): Promise<void>;
82
129
  //# sourceMappingURL=pair-runner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pair-runner.d.ts","sourceRoot":"","sources":["../../src/cli/pair-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAKzF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AA8D1D;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;AAEnF;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,EAC/C,KAAK,EAAE,MAAM,GACZ,MAAM,CAQR;AAyBD,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,0BAA0B,EACrC,SAAS,EAAE,WAAW;AACtB;;;;;;;GAOG;AACH,MAAM,CAAC,EAAE,WAAW;AACpB;;;;;;;GAOG;AACH,QAAQ,GAAE,eAA8B;AACxC;;;;;GAKG;AACH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,2BAA2B,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,kCAAkC,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAC/J,OAAO,CAAC,IAAI,CAAC,CAsMf;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,0BAA0B,EACrC,SAAS,EAAE,WAAW,EACtB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,GAAE,eAA8B;AACxC,mEAAmE;AACnE,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,2BAA2B,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,kCAAkC,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAC/J,OAAO,CAAC,IAAI,CAAC,CAsGf;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,0BAA0B,EACrC,SAAS,EAAE,WAAW,EACtB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,GAAE,eAA8B;AACxC,sEAAsE;AACtE,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,2BAA2B,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,kCAAkC,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAC/J,OAAO,CAAC,IAAI,CAAC,CA0Qf"}
1
+ {"version":3,"file":"pair-runner.d.ts","sourceRoot":"","sources":["../../src/cli/pair-runner.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,7 @@ import { marsScenario } from '../engine/scenarios/index.js';
3
3
  import { apiKeyForProvider } from '../engine/provider/credentials.js';
4
4
  import { generateValidatedObject } from '../llm/generateValidatedObject.js';
5
5
  import { VerdictSchema, CohortVerdictSchema } from '../runtime/validators/verdict.js';
6
+ import { cardRows, differingStateRows, formatScoreTable, formatStateTable, scenarioScoreRows, verdictWords, } from './verdict-scores.js';
6
7
  /**
7
8
  * Largest cohort that gets a full LLM-ranked verdict. Past this size
8
9
  * the prompt + response token budget explodes and the dashboard
@@ -22,9 +23,12 @@ const MAX_COHORT_VERDICT_N = 50;
22
23
  * "#3 Captain Reyes", etc.)
23
24
  * @param leader actor config (carries HEXACO + archetype + unit)
24
25
  * @param result completed RunArtifact for this actor
25
- * @param col `result.finalState.metrics`, hoisted for clarity
26
+ * @param words the scenario's nouns
27
+ *
28
+ * The final scores are not in this block: the verdict prompts carry
29
+ * them side by side for every actor (see `verdict-scores.ts`).
26
30
  */
27
- function formatActorSummary(label, leader, result, col) {
31
+ function formatActorSummary(label, leader, result, words) {
28
32
  const fp = result.fingerprint || {};
29
33
  const toolbox = result.forgedToolbox || [];
30
34
  const topTools = toolbox.slice(0, 5).map((t) => `${t.name}(${t.firstForgedDepartment}, reused ${t.reuseCount}x)`).join('; ');
@@ -39,7 +43,7 @@ function formatActorSummary(label, leader, result, col) {
39
43
  ? Object.entries(causeCounts).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${n} ${k}`).join(', ')
40
44
  : 'no deaths';
41
45
  return [
42
- `${label}: ${leader.name} "${leader.archetype}" (${leader.unit})`,
46
+ `${label}: ${leader.name} "${leader.archetype}"${leader.unit ? ` (${leader.unit})` : ''}`,
43
47
  // Optional-chain every HEXACO axis so actors using the ai-agent
44
48
  // (or any other) trait model — where `leader.hexaco` is undefined
45
49
  // because the profile lives under `traitProfile` instead — don't
@@ -47,9 +51,8 @@ function formatActorSummary(label, leader, result, col) {
47
51
  // to 0.50 (the schema default) so the prompt still carries a
48
52
  // structurally complete HEXACO line for the LLM to reason from.
49
53
  ` HEXACO: O${(leader.hexaco?.openness ?? 0.5).toFixed(2)} C${(leader.hexaco?.conscientiousness ?? 0.5).toFixed(2)} E${(leader.hexaco?.extraversion ?? 0.5).toFixed(2)} A${(leader.hexaco?.agreeableness ?? 0.5).toFixed(2)} Em${(leader.hexaco?.emotionality ?? 0.5).toFixed(2)} HH${(leader.hexaco?.honestyHumility ?? 0.5).toFixed(2)}`,
50
- ` Final: Pop ${col?.population ?? '?'}, Morale ${Math.round((col?.morale ?? 0) * 100)}%, Food ${col?.foodMonthsReserve?.toFixed(1) ?? '?'}mo, Power ${col?.powerKw?.toFixed(0) ?? '?'}kW, Modules ${col?.infrastructureModules?.toFixed(1) ?? '?'}, Science ${col?.scienceOutput ?? '?'}`,
51
- ` Mortality: ${deathEvents.length} total (${causeSummary})`,
52
- ` Innovation: ${toolbox.length} unique tools forged (${fp.innovation || 'n/a'}), citations ${result.totalCitations}`,
54
+ ` Mortality: ${deathEvents.length} ${words.population} died (${causeSummary})`,
55
+ ` Innovation: ${toolbox.length} unique tools forged (${fp.innovation || 'n/a'}), citations ${result.totalCitations ?? 0}`,
53
56
  topTools ? ` Top tools: ${topTools}` : ' No tools forged',
54
57
  ` Fingerprint: ${fp.summary || 'n/a'}`,
55
58
  ` Cost: $${result.cost?.totalCostUSD?.toFixed(4) ?? '?'} over ${result.cost?.llmCalls ?? '?'} LLM calls`,
@@ -214,8 +217,6 @@ onArtifact) {
214
217
  if (settled.length === 2) {
215
218
  try {
216
219
  const [a, b] = settled;
217
- const colA = a.result.finalState?.metrics;
218
- const colB = b.result.finalState?.metrics;
219
220
  const verdictModel = resolveVerdictModel(simConfig.provider || 'openai', simConfig.economics);
220
221
  if (!verdictModel) {
221
222
  broadcast('complete', {
@@ -225,87 +226,7 @@ onArtifact) {
225
226
  });
226
227
  return;
227
228
  }
228
- // Build a richer per-leader summary including innovation telemetry
229
- // (forged toolbox details, fingerprint classification) so the LLM
230
- // verdict actually reasons about emergent tool use, not just final
231
- // colony numbers.
232
- // Pair verdict shares the module-level `formatActorSummary`
233
- // helper with the cohort verdict so the per-leader prompt block
234
- // (HEXACO, cause-of-death rollup, forged toolbox, fingerprint,
235
- // cost) stays canonical across both code paths instead of
236
- // drifting between two near-identical closure copies.
237
- const formatLeader = (label, v, col) => formatActorSummary(label, v.leader, v.result, col);
238
- // Verdict runs once per completed sim and is the single most
239
- // user-facing synthesis call in the pipeline: it reads both
240
- // final states, the per-leader cause-of-death breakdown, the
241
- // forged toolbox, and writes the headline + summary that the
242
- // user sees first when the run finishes. Cheap-tier output on
243
- // this call was noticeably flatter than flagship output, so
244
- // pay the ~$0.02-0.05 per run for the better read.
245
- try {
246
- const { object: verdict, fromFallback } = await generateValidatedObject({
247
- provider: simConfig.provider || 'openai',
248
- model: verdictModel,
249
- apiKey: apiKeyForProvider(simConfig.provider || 'openai', simConfig),
250
- schema: VerdictSchema,
251
- schemaName: 'Verdict',
252
- prompt: `You are judging a colony simulation. Two AI commanders with opposing HEXACO personality profiles led identical colonies through ${turns} turns from the same starting conditions and deterministic seed. Your job is to write a verdict that explains WHY the runs diverged the way they did, not just WHO won.
253
-
254
- ${formatLeader('LEADER A', a, colA)}
255
-
256
- ${formatLeader('LEADER B', b, colB)}
257
-
258
- TRADEOFFS TO WEIGH
259
- Tool forging is a cost / capability tradeoff: every forged tool spent a judge LLM call and ate analyst attention; failed forges hurt morale and produced no reusable capability; successful tools let later decisions reason about concrete numbers. A leader who built few tools and reused them many times has a disciplined, cost-efficient signature. A leader who forged many novel tools has an exploratory signature with broader capability surface. Both are valid strategies and your scoring should reflect that trade, not punish either extreme.
260
-
261
- Mortality is a cause-specific signal, not a number. The "Mortality" line above names HOW each colonist died. A leader who lost 5 to starvation made different resource-allocation decisions than a leader who lost 5 to radiation cancer; a leader with despair deaths presided over a colony in psychological freefall. Reference the specific causes when they shape the story.
262
-
263
- REASONING — populate the "reasoning" field of your JSON response with a numbered list covering:
264
- (1) Population trajectory — how did each colony's population evolve, and which tradeoffs produced that shape?
265
- (2) Morale + psychological state — which leader's colony held together emotionally, and what does that say about HEXACO + decision style?
266
- (3) Resource efficiency — food, power, infrastructure — which side ran leaner, which hit crises?
267
- (4) Innovation signature — tools forged vs reused, breadth vs depth. What does each leader's toolbox say about their cognition?
268
- (5) Mortality story — which causes dominated each side, and what does THAT say about the leader's priorities?
269
- (6) The single most impactful divergence — resource decision, crisis response, tool strategy, or emergent behavior. Name it precisely.
270
- (7) Weighing the tradeoffs, who won and why.
271
-
272
- Then fill out:
273
- winner: "A" or "B" or "tie"
274
- winnerName: the winning leader's name, or "Tie" for a tie
275
- headline: one-line verdict grounded in the key divergence (max 80 chars)
276
- summary: 2-3 sentences naming the personality + tool-use + mortality pattern that drove the divergence
277
- keyDivergence: the single most impactful difference between the two runs
278
- scores: { a: { survival, prosperity, morale, innovation }, b: { survival, prosperity, morale, innovation } } — each 0-10`,
279
- });
280
- if (fromFallback) {
281
- console.log(' Verdict schema fallback; skipping broadcast');
282
- // Pair verdict fell back the same way the cohort version
283
- // does — emit the same explanatory event so the UI surfaces
284
- // a "no verdict" banner instead of going silent.
285
- broadcast('verdict_skipped', {
286
- mode: 'pair',
287
- reason: 'generation_failed',
288
- detail: 'Schema validation fallback exhausted',
289
- });
290
- }
291
- else {
292
- broadcast('verdict', {
293
- ...verdict,
294
- leaderA: { name: a.leader.name, archetype: a.leader.archetype, unit: a.leader.unit },
295
- leaderB: { name: b.leader.name, archetype: b.leader.archetype, unit: b.leader.unit },
296
- finalStats: {
297
- a: { population: colA?.population, morale: colA?.morale, food: colA?.foodMonthsReserve, power: colA?.powerKw, modules: colA?.infrastructureModules, science: colA?.scienceOutput, tools: a.result.totalToolsForged },
298
- b: { population: colB?.population, morale: colB?.morale, food: colB?.foodMonthsReserve, power: colB?.powerKw, modules: colB?.infrastructureModules, science: colB?.scienceOutput, tools: b.result.totalToolsForged },
299
- },
300
- });
301
- console.log(`\n VERDICT: ${verdict.headline}`);
302
- console.log(` Winner: ${verdict.winnerName} (${verdict.winner})`);
303
- console.log(` ${verdict.summary}\n`);
304
- }
305
- }
306
- catch (verdictErr) {
307
- console.log(' Verdict generation failed:', verdictErr);
308
- }
229
+ await judgePair({ credentials: simConfig, verdictModel, turns, scenario, broadcast, a, b });
309
230
  }
310
231
  catch (outerErr) {
311
232
  console.log(' Verdict outer failure:', outerErr);
@@ -579,104 +500,7 @@ onArtifact) {
579
500
  cleanRuns.push({ leader: actorsWithTags[i].leader, index: actorsWithTags[i].index, result });
580
501
  }
581
502
  if (verdictModel && cleanRuns.length >= 2 && cleanRuns.length <= MAX_COHORT_VERDICT_N) {
582
- const summaries = cleanRuns.map(({ leader, index, result }) => {
583
- const col = result.finalState?.metrics;
584
- return formatActorSummary(`#${index + 1} ${leader.name}`, leader, result, col);
585
- }).join('\n\n');
586
- try {
587
- const { object: cohortVerdict, fromFallback } = await generateValidatedObject({
588
- provider: simConfig.provider || 'openai',
589
- model: verdictModel,
590
- apiKey: apiKeyForProvider(simConfig.provider || 'openai', simConfig),
591
- schema: CohortVerdictSchema,
592
- schemaName: 'CohortVerdict',
593
- prompt: `You are judging a cohort simulation. ${cleanRuns.length} AI commanders with different HEXACO personality profiles led identical worlds through ${turns} turns from the same starting conditions and deterministic seed. Your job is to rank them from best to worst and explain WHY each placed where it did.
594
-
595
- ACTOR SUMMARIES (in launch order):
596
-
597
- ${summaries}
598
-
599
- TRADEOFFS TO WEIGH
600
- Tool forging is a cost/capability tradeoff: every forged tool spent a judge LLM call and ate analyst attention; failed forges hurt morale and produced no reusable capability; successful tools let later decisions reason about concrete numbers. A leader who built few tools and reused them many times has a disciplined, cost-efficient signature; a leader who forged many novel tools has an exploratory signature with broader capability surface. Both are valid strategies and your scoring should reflect that trade, not punish either extreme.
601
-
602
- Mortality is a cause-specific signal, not a number. The "Mortality" line names HOW each colonist died. Starvation deaths reveal resource-allocation choices; radiation-cancer deaths reveal shielding priorities; despair deaths reveal a colony in psychological freefall. Reference specific causes when they shape the story.
603
-
604
- REASONING — populate the "reasoning" field with a numbered breakdown covering:
605
- (1) Population trajectory across the cohort — which actors held the line, which collapsed?
606
- (2) Morale + psychological state — which actor's colony held together emotionally?
607
- (3) Resource efficiency — food, power, infrastructure — which ran leanest, which hit crises?
608
- (4) Innovation signature — breadth vs depth in the forged toolbox across actors.
609
- (5) Mortality story — which causes dominated, and what does that say about each actor's priorities?
610
- (6) The single most impactful divergence across the cohort.
611
- (7) The full ranking with brief why-this-rank.
612
-
613
- Then fill out:
614
- winner: the name of the top-ranked actor (exact match from one of the names above)
615
- winnerIndex: the 0-based launch-order index of the winner
616
- headline: one-line verdict (max 80 chars) grounded in the key cohort divergence
617
- summary: 2-3 sentences naming the personality + tool-use + mortality pattern that drove the winner's success relative to the rest of the cohort
618
- keyDivergence: the single most impactful difference between the winner and the next-best actor
619
- rankings: every actor as a separate entry, with rank (1 = winner), actorName, actorIndex, scores { survival, prosperity, morale, innovation } each 0-10, and a 1-2 sentence rationale specific to that actor`,
620
- });
621
- if (fromFallback) {
622
- console.log(' Cohort verdict schema fallback; skipping broadcast');
623
- // Schema-validation fallback exhausted on the cohort verdict
624
- // call. The original code logged this and silently dropped
625
- // the broadcast, leaving the dashboard with no verdict and
626
- // no explanation — exactly the user-reported "I see no
627
- // victor for cohort runs" failure. Cheap-tier models
628
- // (gpt-5.4-nano, claude-haiku-4-5) hit the rankings array
629
- // schema (min 2 entries, each with rank + scores + rationale)
630
- // more often than they hit the simpler pair Verdict schema,
631
- // so cohort runs lost the banner where pair runs kept it.
632
- broadcast('verdict_skipped', {
633
- mode: 'cohort',
634
- reason: 'generation_failed',
635
- detail: 'Schema validation fallback exhausted',
636
- actorCount: cleanRuns.length,
637
- });
638
- }
639
- else {
640
- broadcast('cohort_verdict', {
641
- ...cohortVerdict,
642
- actors: cleanRuns.map(({ leader, index }) => ({
643
- name: leader.name,
644
- archetype: leader.archetype,
645
- unit: leader.unit,
646
- index,
647
- })),
648
- finalStats: cleanRuns.map(({ leader, index, result }) => ({
649
- actorName: leader.name,
650
- actorIndex: index,
651
- population: result.finalState?.metrics?.population,
652
- morale: result.finalState?.metrics?.morale,
653
- food: result.finalState?.metrics?.foodMonthsReserve,
654
- power: result.finalState?.metrics?.powerKw,
655
- modules: result.finalState?.metrics?.infrastructureModules,
656
- science: result.finalState?.metrics?.scienceOutput,
657
- tools: result.totalToolsForged,
658
- })),
659
- });
660
- console.log(`\n COHORT VERDICT: ${cohortVerdict.headline}`);
661
- console.log(` Winner: ${cohortVerdict.winner} (#${cohortVerdict.winnerIndex + 1})`);
662
- console.log(` ${cohortVerdict.summary}\n`);
663
- }
664
- }
665
- catch (verdictErr) {
666
- const detail = verdictErr instanceof Error ? verdictErr.message : String(verdictErr);
667
- console.log(' Cohort verdict generation failed:', verdictErr);
668
- // Surface the failure to the dashboard so the user isn't left
669
- // staring at a finished run with no verdict + no explanation.
670
- // Without this event, a transient LLM error during the cohort
671
- // ranking call (rate limit, schema fallback exhausted) just
672
- // logged server-side and the UI silently dropped the banner.
673
- broadcast('verdict_skipped', {
674
- mode: 'cohort',
675
- reason: 'generation_failed',
676
- detail,
677
- actorCount: cleanRuns.length,
678
- });
679
- }
503
+ await judgeCohort({ credentials: simConfig, verdictModel, turns, scenario, broadcast, runs: cleanRuns });
680
504
  }
681
505
  else if (verdictModel && cleanRuns.length > MAX_COHORT_VERDICT_N) {
682
506
  console.log(` [batch] Cohort verdict skipped: ${cleanRuns.length} actors exceeds MAX_COHORT_VERDICT_N=${MAX_COHORT_VERDICT_N}`);
@@ -705,4 +529,258 @@ Then fill out:
705
529
  ...(allAborted ? { aborted: true } : {}),
706
530
  });
707
531
  }
532
+ const TOOL_TRADEOFF = 'Tool forging is a cost / capability tradeoff: every forged tool spent a judge LLM call and ate analyst attention; failed forges hurt morale and produced no reusable capability; successful tools let later decisions reason about concrete numbers. A leader who built few tools and reused them many times has a disciplined, cost-efficient signature. A leader who forged many novel tools has an exploratory signature with broader capability surface. Both are valid strategies and your scoring should reflect that trade, not punish either extreme.';
533
+ /** How the card shows a score row: everything but the values. */
534
+ function scoreRowDisplay(r, words) {
535
+ return {
536
+ id: r.id,
537
+ label: r.label,
538
+ format: r.format,
539
+ ...(r.better ? { better: r.better } : {}),
540
+ ...(r.unit ? { unit: r.unit } : {}),
541
+ ...(r.format === 'currency' && words.currency ? { currency: words.currency } : {}),
542
+ };
543
+ }
544
+ /** The opening and the score blocks both verdict prompts share. */
545
+ function verdictContext(scenario, runs, tags) {
546
+ const words = verdictWords(scenario);
547
+ const states = runs.map(r => r.result?.finalState);
548
+ const rows = scenarioScoreRows(scenario, states);
549
+ const stateRows = differingStateRows(scenario, states);
550
+ const intro = `You are judging runs of ${words.name}, a simulation of ${words.aSettlement}.${words.tagline ? ` ${words.tagline}` : ''}`;
551
+ const scores = `${words.name.toUpperCase()} SCORES (each run's final value, ${tags.join(' | ')})\n${formatScoreTable(rows, tags, words)}`;
552
+ const otherState = stateRows.length > 0
553
+ ? `\n\nOTHER STATE THAT DIFFERS (${tags.join(' | ')})\n${formatStateTable(stateRows, tags)}`
554
+ : '';
555
+ const mortality = `Mortality is a cause-specific signal, not a number. The "Mortality" line names how many of each ${words.settlement}'s ${words.population} died and of what. Different causes point at different decisions; reference the specific causes when they shape the story.`;
556
+ return { words, rows, intro, scoreBlock: scores + otherState, mortality };
557
+ }
558
+ /** The judge request for two finished runs, with the words and rows the broadcast reuses. */
559
+ function pairVerdictRequest(turns, scenario, a, b) {
560
+ const { words, rows, intro, scoreBlock, mortality } = verdictContext(scenario, [a, b], ['A', 'B']);
561
+ const prompt = `${intro}
562
+
563
+ Two AI ${words.actors} with opposing HEXACO personality profiles each ran the same ${words.settlement} through ${turns} turns from the same starting conditions and deterministic seed. Your job is to decide which of them ran the ${words.settlement} better on ${words.name}'s own scores, and to explain WHY the runs diverged the way they did, not just WHO won.
564
+
565
+ ${scoreBlock}
566
+
567
+ ${formatActorSummary('LEADER A', a.leader, a.result, words)}
568
+
569
+ ${formatActorSummary('LEADER B', b.leader, b.result, words)}
570
+
571
+ HOW TO DECIDE
572
+ The scores above are how ${words.name} measures ${words.aSettlement}; decide the winner on them. Where a line says which way is better, use it; where it does not, judge the direction from the score's name and the scenario. A leader who leads on most of the scores wins, even when each lead is small. A tie is only for outcomes that are the same on these scores; runs whose scores differ are never a tie on account of style, toolbox or cost.
573
+
574
+ TRADEOFFS TO WEIGH
575
+ ${TOOL_TRADEOFF}
576
+
577
+ ${mortality}
578
+
579
+ REASONING: populate the "reasoning" field of your JSON response with a numbered list covering:
580
+ (1) Score by score: for each score above, who leads and by how much, and which decisions produced the gap?
581
+ (2) The ${words.population}: how did each ${words.settlement}'s ${words.population} and their morale fare, and what does that say about HEXACO + decision style?
582
+ (3) Innovation signature: tools forged vs reused, breadth vs depth. What does each leader's toolbox say about their cognition?
583
+ (4) Mortality story: which causes dominated each side, if any, and what does THAT say about the leader's priorities?
584
+ (5) The single most impactful divergence: ${words.eventSingular} response, resource decision, tool strategy, or emergent behavior. Name it precisely.
585
+ (6) Weighing the scores and the tradeoffs, who won and why.
586
+
587
+ Then fill out:
588
+ winner: "A" or "B" or "tie"
589
+ winnerName: the winning leader's name, or "Tie" for a tie
590
+ headline: one-line verdict grounded in the key divergence (max 80 chars)
591
+ summary: 2-3 sentences naming the personality + tool-use pattern and the scores that decided it
592
+ keyDivergence: the single most impactful difference between the two runs
593
+ scores: { a: { survival, prosperity, morale, innovation }, b: { survival, prosperity, morale, innovation } }: each 0-10, where survival is how well the ${words.settlement} and its ${words.population} came through, prosperity is the standing on ${words.name}'s scores above, morale is the ${words.population}' morale and innovation is the tool signature`;
594
+ return { prompt, words, rows };
595
+ }
596
+ /**
597
+ * Pair verdict: builds the judge request for two finished runs from the
598
+ * scenario's own scores and words, calls the verdict model and
599
+ * broadcasts `verdict`, or `verdict_skipped` with the reason when the
600
+ * request or the model call fails. Never throws, so the run still
601
+ * reaches its `complete` event.
602
+ */
603
+ export async function judgePair(opts) {
604
+ const { credentials, verdictModel, turns, scenario, broadcast, a, b, generateObjectImpl } = opts;
605
+ const colA = a.result.finalState?.metrics;
606
+ const colB = b.result.finalState?.metrics;
607
+ try {
608
+ const { prompt, words, rows } = pairVerdictRequest(turns, scenario, a, b);
609
+ const { object: verdict, fromFallback } = await generateValidatedObject({
610
+ provider: credentials.provider || 'openai',
611
+ model: verdictModel,
612
+ apiKey: apiKeyForProvider(credentials.provider || 'openai', credentials),
613
+ _generateObjectImpl: generateObjectImpl,
614
+ schema: VerdictSchema,
615
+ schemaName: 'Verdict',
616
+ prompt,
617
+ });
618
+ if (fromFallback) {
619
+ console.log(' Verdict schema fallback; skipping broadcast');
620
+ // Pair verdict fell back the same way the cohort version
621
+ // does — emit the same explanatory event so the UI surfaces
622
+ // a "no verdict" banner instead of going silent.
623
+ broadcast('verdict_skipped', {
624
+ mode: 'pair',
625
+ reason: 'generation_failed',
626
+ detail: 'Schema validation fallback exhausted',
627
+ });
628
+ }
629
+ else {
630
+ broadcast('verdict', {
631
+ ...verdict,
632
+ leaderA: { name: a.leader.name, archetype: a.leader.archetype, unit: a.leader.unit },
633
+ leaderB: { name: b.leader.name, archetype: b.leader.archetype, unit: b.leader.unit },
634
+ settlementNoun: words.settlement,
635
+ finalStats: {
636
+ a: { population: colA?.population, morale: colA?.morale, food: colA?.foodMonthsReserve, power: colA?.powerKw, modules: colA?.infrastructureModules, science: colA?.scienceOutput, tools: a.result.totalToolsForged },
637
+ b: { population: colB?.population, morale: colB?.morale, food: colB?.foodMonthsReserve, power: colB?.powerKw, modules: colB?.infrastructureModules, science: colB?.scienceOutput, tools: b.result.totalToolsForged },
638
+ // The scenario's header scores, in header order, for the
639
+ // verdict card and the report scoreboard.
640
+ metrics: cardRows(rows).map(r => ({
641
+ ...scoreRowDisplay(r, words),
642
+ a: r.values[0],
643
+ b: r.values[1],
644
+ })),
645
+ },
646
+ });
647
+ console.log(`\n VERDICT: ${verdict.headline}`);
648
+ console.log(` Winner: ${verdict.winnerName} (${verdict.winner})`);
649
+ console.log(` ${verdict.summary}\n`);
650
+ }
651
+ }
652
+ catch (verdictErr) {
653
+ console.log(' Verdict generation failed:', verdictErr);
654
+ broadcast('verdict_skipped', {
655
+ mode: 'pair',
656
+ reason: 'generation_failed',
657
+ detail: verdictErr instanceof Error ? verdictErr.message : String(verdictErr),
658
+ });
659
+ }
660
+ }
661
+ /** The ranking request for a cohort's finished runs, with the words and rows the broadcast reuses. */
662
+ function cohortVerdictRequest(turns, scenario, cleanRuns) {
663
+ const tags = cleanRuns.map(({ index }) => `#${index + 1}`);
664
+ const { words, rows, intro, scoreBlock, mortality } = verdictContext(scenario, cleanRuns, tags);
665
+ const summaries = cleanRuns.map(({ leader, index, result }) => formatActorSummary(`#${index + 1}`, leader, result, words)).join('\n\n');
666
+ const prompt = `${intro}
667
+
668
+ ${cleanRuns.length} AI ${words.actors} with different HEXACO personality profiles each ran the same ${words.settlement} through ${turns} turns from the same starting conditions and deterministic seed. Your job is to rank them from best to worst on ${words.name}'s own scores and explain WHY each placed where it did.
669
+
670
+ ${scoreBlock}
671
+
672
+ ACTOR SUMMARIES (in launch order):
673
+
674
+ ${summaries}
675
+
676
+ HOW TO RANK
677
+ The scores above are how ${words.name} measures ${words.aSettlement}; rank on them. Where a line says which way is better, use it; where it does not, judge the direction from the score's name and the scenario. An actor who leads another on most of the scores ranks above it, even when each lead is small; style, toolbox and cost break a ranking only between actors whose scores are the same.
678
+
679
+ TRADEOFFS TO WEIGH
680
+ ${TOOL_TRADEOFF}
681
+
682
+ ${mortality}
683
+
684
+ REASONING: populate the "reasoning" field with a numbered breakdown covering:
685
+ (1) Score by score across the cohort: who leads each score, who trails, and by how much?
686
+ (2) The ${words.population}: how did each ${words.settlement}'s ${words.population} and their morale fare?
687
+ (3) Innovation signature: breadth vs depth in the forged toolbox across actors.
688
+ (4) Mortality story: which causes dominated, if any, and what does that say about each actor's priorities?
689
+ (5) The single most impactful divergence across the cohort.
690
+ (6) The full ranking with brief why-this-rank.
691
+
692
+ Then fill out:
693
+ winner: the name of the top-ranked actor (exact match from one of the names above)
694
+ winnerIndex: the 0-based launch-order index of the winner
695
+ headline: one-line verdict (max 80 chars) grounded in the key cohort divergence
696
+ summary: 2-3 sentences naming the personality + tool-use pattern and the scores that put the winner ahead of the rest of the cohort
697
+ keyDivergence: the single most impactful difference between the winner and the next-best actor
698
+ rankings: every actor as a separate entry, with rank (1 = winner), actorName, actorIndex, scores { survival, prosperity, morale, innovation } each 0-10 (survival: how well the ${words.settlement} and its ${words.population} came through; prosperity: the standing on ${words.name}'s scores above), and a 1-2 sentence rationale specific to that actor`;
699
+ return { prompt, words, rows };
700
+ }
701
+ /**
702
+ * Cohort verdict (the clean runs of a batch): builds the ranking
703
+ * request from the scenario's own scores and words, calls the verdict
704
+ * model and broadcasts `cohort_verdict`, or `verdict_skipped` with the
705
+ * reason when the request or the model call fails. Never throws, so the
706
+ * run still reaches its `complete` event.
707
+ */
708
+ export async function judgeCohort(opts) {
709
+ const { credentials, verdictModel, turns, scenario, broadcast, runs: cleanRuns, generateObjectImpl } = opts;
710
+ try {
711
+ const { prompt, words, rows } = cohortVerdictRequest(turns, scenario, cleanRuns);
712
+ const { object: cohortVerdict, fromFallback } = await generateValidatedObject({
713
+ provider: credentials.provider || 'openai',
714
+ model: verdictModel,
715
+ apiKey: apiKeyForProvider(credentials.provider || 'openai', credentials),
716
+ _generateObjectImpl: generateObjectImpl,
717
+ schema: CohortVerdictSchema,
718
+ schemaName: 'CohortVerdict',
719
+ prompt,
720
+ });
721
+ if (fromFallback) {
722
+ console.log(' Cohort verdict schema fallback; skipping broadcast');
723
+ // Schema-validation fallback exhausted on the cohort verdict
724
+ // call. The original code logged this and silently dropped
725
+ // the broadcast, leaving the dashboard with no verdict and
726
+ // no explanation — exactly the user-reported "I see no
727
+ // victor for cohort runs" failure. Cheap-tier models
728
+ // (gpt-5.4-nano, claude-haiku-4-5) hit the rankings array
729
+ // schema (min 2 entries, each with rank + scores + rationale)
730
+ // more often than they hit the simpler pair Verdict schema,
731
+ // so cohort runs lost the banner where pair runs kept it.
732
+ broadcast('verdict_skipped', {
733
+ mode: 'cohort',
734
+ reason: 'generation_failed',
735
+ detail: 'Schema validation fallback exhausted',
736
+ actorCount: cleanRuns.length,
737
+ });
738
+ }
739
+ else {
740
+ broadcast('cohort_verdict', {
741
+ ...cohortVerdict,
742
+ actors: cleanRuns.map(({ leader, index }) => ({
743
+ name: leader.name,
744
+ archetype: leader.archetype,
745
+ unit: leader.unit,
746
+ index,
747
+ })),
748
+ settlementNoun: words.settlement,
749
+ finalStats: cleanRuns.map(({ leader, index, result }, i) => ({
750
+ actorName: leader.name,
751
+ actorIndex: index,
752
+ population: result.finalState?.metrics?.population,
753
+ morale: result.finalState?.metrics?.morale,
754
+ food: result.finalState?.metrics?.foodMonthsReserve,
755
+ power: result.finalState?.metrics?.powerKw,
756
+ modules: result.finalState?.metrics?.infrastructureModules,
757
+ science: result.finalState?.metrics?.scienceOutput,
758
+ tools: result.totalToolsForged,
759
+ metrics: cardRows(rows).map(r => ({
760
+ ...scoreRowDisplay(r, words),
761
+ value: r.values[i],
762
+ })),
763
+ })),
764
+ });
765
+ console.log(`\n COHORT VERDICT: ${cohortVerdict.headline}`);
766
+ console.log(` Winner: ${cohortVerdict.winner} (#${cohortVerdict.winnerIndex + 1})`);
767
+ console.log(` ${cohortVerdict.summary}\n`);
768
+ }
769
+ }
770
+ catch (verdictErr) {
771
+ const detail = verdictErr instanceof Error ? verdictErr.message : String(verdictErr);
772
+ console.log(' Cohort verdict generation failed:', verdictErr);
773
+ // Surface the failure to the dashboard so the user isn't left
774
+ // staring at a finished run with no verdict + no explanation.
775
+ // Without this event, a transient LLM error during the cohort
776
+ // ranking call (rate limit, schema fallback exhausted) just
777
+ // logged server-side and the UI silently dropped the banner.
778
+ broadcast('verdict_skipped', {
779
+ mode: 'cohort',
780
+ reason: 'generation_failed',
781
+ detail,
782
+ actorCount: cleanRuns.length,
783
+ });
784
+ }
785
+ }
708
786
  //# sourceMappingURL=pair-runner.js.map