paracosm 0.9.1048 → 0.9.1050
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.
- package/dist/cli/pair-runner.d.ts +47 -0
- package/dist/cli/pair-runner.d.ts.map +1 -1
- package/dist/cli/pair-runner.js +265 -187
- package/dist/cli/pair-runner.js.map +1 -1
- package/dist/cli/verdict-scores.d.ts +73 -0
- package/dist/cli/verdict-scores.d.ts.map +1 -0
- package/dist/cli/verdict-scores.js +199 -0
- package/dist/cli/verdict-scores.js.map +1 -0
- package/dist/dashboard/dist/assets/index-D6F9vsRg.js +195 -0
- package/dist/dashboard/dist/index.html +1 -1
- package/dist/engine/compiler/compile-from-seed.d.ts +4 -0
- package/dist/engine/compiler/compile-from-seed.d.ts.map +1 -1
- package/dist/engine/compiler/compile-from-seed.js +2 -0
- package/dist/engine/compiler/compile-from-seed.js.map +1 -1
- package/dist/engine/types.d.ts +17 -1
- package/dist/engine/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/scenarios/t2d-glp1-protocol.json +12 -6
- package/dist/dashboard/dist/assets/index-C5NcH86Q.js +0 -195
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"pair-runner.d.ts","sourceRoot":"","sources":["../../src/cli/pair-runner.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/pair-runner.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
`
|
|
51
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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
|