paracosm 0.4.146 → 0.4.148
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/src/runtime/citations-catalog.d.ts +34 -0
- package/dist/src/runtime/citations-catalog.d.ts.map +1 -0
- package/dist/src/runtime/citations-catalog.js +52 -0
- package/dist/src/runtime/citations-catalog.js.map +1 -0
- package/dist/src/runtime/commander-setup.d.ts +70 -0
- package/dist/src/runtime/commander-setup.d.ts.map +1 -0
- package/dist/src/runtime/commander-setup.js +116 -0
- package/dist/src/runtime/commander-setup.js.map +1 -0
- package/dist/src/runtime/cost-tracker.d.ts +87 -0
- package/dist/src/runtime/cost-tracker.d.ts.map +1 -0
- package/dist/src/runtime/cost-tracker.js +119 -0
- package/dist/src/runtime/cost-tracker.js.map +1 -0
- package/dist/src/runtime/orchestrator.d.ts +2 -41
- package/dist/src/runtime/orchestrator.d.ts.map +1 -1
- package/dist/src/runtime/orchestrator.js +41 -292
- package/dist/src/runtime/orchestrator.js.map +1 -1
- package/dist/src/runtime/tool-ledger.d.ts +40 -0
- package/dist/src/runtime/tool-ledger.d.ts.map +1 -1
- package/dist/src/runtime/tool-ledger.js +57 -0
- package/dist/src/runtime/tool-ledger.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,8 +3,10 @@ import { resolve, dirname } from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { webSearchTool, createEmergentEngine, createCallForgedTool, wrapForgeTool, } from './emergent-setup.js';
|
|
5
5
|
import { humanizeToolName, parseDeptReport, parseCmdDecision, emptyReport, emptyDecision, decisionToPolicy, } from './parsers.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { createCostTracker } from './cost-tracker.js';
|
|
7
|
+
import { buildPersonalityCue, buildCommanderBootstrap, runDepartmentPromotions, } from './commander-setup.js';
|
|
8
|
+
import { buildAvailableToolsBlock, buildForgedToolbox } from './tool-ledger.js';
|
|
9
|
+
import { buildCitationCatalog } from './citations-catalog.js';
|
|
8
10
|
import { SeededRng } from '../engine/core/rng.js';
|
|
9
11
|
import { classifyOutcome, classifyOutcomeById } from '../engine/core/progression.js';
|
|
10
12
|
import { SimulationKernel } from '../engine/core/kernel.js';
|
|
@@ -16,7 +18,7 @@ import { EventDirector } from './director.js';
|
|
|
16
18
|
import { generateAgentReactions } from './agent-reactions.js';
|
|
17
19
|
import { recordReactionMemory, consolidateMemory, updateRelationshipsFromReactions } from './agent-memory.js';
|
|
18
20
|
import { DEFAULT_EXECUTION, resolveSimulationModels, } from '../cli/sim-config.js';
|
|
19
|
-
import { applyCustomEventToCrisis,
|
|
21
|
+
import { applyCustomEventToCrisis, buildYearSchedule } from './runtime-helpers.js';
|
|
20
22
|
import { classifyProviderError, shouldAbortRun } from './provider-errors.js';
|
|
21
23
|
import { EffectRegistry } from '../engine/effect-registry.js';
|
|
22
24
|
import { marsScenario } from '../engine/mars/index.js';
|
|
@@ -30,128 +32,20 @@ export async function runSimulation(leader, keyPersonnel, opts = {}) {
|
|
|
30
32
|
const sid = `${sc.labels.shortName}-v2-${leader.archetype.toLowerCase().replace(/\s+/g, '-')}`;
|
|
31
33
|
const modelConfig = resolveSimulationModels(provider, opts.models);
|
|
32
34
|
// Cost tracking: accumulate token usage and estimated cost across all LLM calls
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
* the prompt-caching wiring actually hit — a run with `cacheReadTokens
|
|
41
|
-
* = 0` despite configured cache breakpoints means caching didn't work
|
|
42
|
-
* and the reported cost is not reflecting the savings we expected.
|
|
43
|
-
*
|
|
44
|
-
* Reported via `_cost` on every SSE emit so the dashboard breakdown
|
|
45
|
-
* modal can display cache hit rate and estimated savings.
|
|
46
|
-
*/
|
|
47
|
-
let cacheReadTokens = 0;
|
|
48
|
-
let cacheCreationTokens = 0;
|
|
49
|
-
const newBucket = () => ({
|
|
50
|
-
totalTokens: 0, totalCostUSD: 0, calls: 0,
|
|
51
|
-
cacheReadTokens: 0, cacheCreationTokens: 0,
|
|
52
|
-
});
|
|
53
|
-
const costBySite = {
|
|
54
|
-
director: newBucket(),
|
|
55
|
-
commander: newBucket(),
|
|
56
|
-
departments: newBucket(),
|
|
57
|
-
judge: newBucket(),
|
|
58
|
-
reactions: newBucket(),
|
|
59
|
-
other: newBucket(),
|
|
60
|
-
};
|
|
61
|
-
// Pricing comes from pricing.ts so MODEL_PRICING has one home.
|
|
62
|
-
const defaultPricing = getDefaultPricing(modelConfig);
|
|
63
|
-
const priceForSite = buildPriceForSite(modelConfig);
|
|
64
|
-
/**
|
|
65
|
-
* Record token/cost usage from a single LLM call.
|
|
66
|
-
*
|
|
67
|
-
* @param result The result object from generateText / session.send(),
|
|
68
|
-
* which carries an optional `usage` field populated by AgentOS.
|
|
69
|
-
* @param site Which pipeline stage made the call. Used to build the
|
|
70
|
-
* per-site cost breakdown the dashboard StatsBar can drill into.
|
|
71
|
-
* Defaults to 'other' when a call-site isn't tagged (harmless
|
|
72
|
-
* fallback, but tag every new call site so the breakdown stays
|
|
73
|
-
* meaningful).
|
|
74
|
-
*/
|
|
75
|
-
function trackUsage(result, site = 'other') {
|
|
76
|
-
if (!result?.usage)
|
|
77
|
-
return;
|
|
78
|
-
const tokensThisCall = result.usage.totalTokens ?? 0;
|
|
79
|
-
const callCacheRead = result.usage.cacheReadTokens ?? 0;
|
|
80
|
-
const callCacheCreate = result.usage.cacheCreationTokens ?? 0;
|
|
81
|
-
let costThisCall;
|
|
82
|
-
if (typeof result.usage.costUSD === 'number') {
|
|
83
|
-
// Provider returned a cost that already accounts for cache tiers.
|
|
84
|
-
costThisCall = result.usage.costUSD;
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
// Fallback: compute a cache-aware estimate using commander-tier
|
|
88
|
-
// pricing as an approximation. Cache reads billed at 0.1×, cache
|
|
89
|
-
// creations at 1.25× input rate. Previous estimate ignored cache
|
|
90
|
-
// entirely so a run with heavy caching would UNDER-report the
|
|
91
|
-
// billed amount by ~10-15%.
|
|
92
|
-
const input = result.usage.promptTokens ?? 0;
|
|
93
|
-
const output = result.usage.completionTokens ?? 0;
|
|
94
|
-
costThisCall =
|
|
95
|
-
(input * defaultPricing.input / 1_000_000)
|
|
96
|
-
+ (output * defaultPricing.output / 1_000_000)
|
|
97
|
-
+ (callCacheRead * defaultPricing.input * 0.10 / 1_000_000)
|
|
98
|
-
+ (callCacheCreate * defaultPricing.input * 1.25 / 1_000_000);
|
|
99
|
-
}
|
|
100
|
-
totalTokens += tokensThisCall;
|
|
101
|
-
totalCostUSD += costThisCall;
|
|
102
|
-
llmCalls++;
|
|
103
|
-
cacheReadTokens += callCacheRead;
|
|
104
|
-
cacheCreationTokens += callCacheCreate;
|
|
105
|
-
const bucket = costBySite[site];
|
|
106
|
-
bucket.totalTokens += tokensThisCall;
|
|
107
|
-
bucket.totalCostUSD += costThisCall;
|
|
108
|
-
bucket.calls++;
|
|
109
|
-
bucket.cacheReadTokens += callCacheRead;
|
|
110
|
-
bucket.cacheCreationTokens += callCacheCreate;
|
|
111
|
-
}
|
|
35
|
+
// Cost tracker: per-site buckets + cache-aware fallback estimation.
|
|
36
|
+
// Lives in cost-tracker.ts so the math has one home and the turn loop
|
|
37
|
+
// reads as dispatch. trackUsage(result, site) is called at every LLM
|
|
38
|
+
// call site with a site tag; buildCostPayload() is called before each
|
|
39
|
+
// SSE emit so the dashboard breakdown modal sees live data.
|
|
40
|
+
const costTracker = createCostTracker(modelConfig);
|
|
41
|
+
const trackUsage = costTracker.trackUsage;
|
|
112
42
|
const emit = (type, data) => {
|
|
113
|
-
// Per-site cache savings math: estimate USD saved vs a hypothetical
|
|
114
|
-
// no-caching run. Anthropic bills cache_read at 0.10x input rate
|
|
115
|
-
// (saves 0.90x) and cache_creation at 1.25x (costs +0.25x). So net
|
|
116
|
-
// savings per site = (reads × 0.90 - creates × 0.25) × inputPrice.
|
|
117
|
-
// A fresh creation on turn 1 is a net COST relative to no-cache
|
|
118
|
-
// (the run hasn't amortized the 1.25x overhead yet). By turn 3+
|
|
119
|
-
// with reads dominating, savings are substantial.
|
|
120
|
-
let runCacheSavingsUSD = 0;
|
|
121
|
-
const breakdown = {};
|
|
122
|
-
for (const [k, v] of Object.entries(costBySite)) {
|
|
123
|
-
if (v.calls > 0) {
|
|
124
|
-
const sitePricing = priceForSite(k);
|
|
125
|
-
const siteSavings = ((v.cacheReadTokens * 0.90) - (v.cacheCreationTokens * 0.25))
|
|
126
|
-
* sitePricing.input / 1_000_000;
|
|
127
|
-
runCacheSavingsUSD += siteSavings;
|
|
128
|
-
breakdown[k] = {
|
|
129
|
-
totalTokens: v.totalTokens,
|
|
130
|
-
totalCostUSD: Math.round(v.totalCostUSD * 10000) / 10000,
|
|
131
|
-
calls: v.calls,
|
|
132
|
-
...(v.cacheReadTokens > 0 ? { cacheReadTokens: v.cacheReadTokens } : {}),
|
|
133
|
-
...(v.cacheCreationTokens > 0 ? { cacheCreationTokens: v.cacheCreationTokens } : {}),
|
|
134
|
-
...(Math.abs(siteSavings) >= 0.0001
|
|
135
|
-
? { cacheSavingsUSD: Math.round(siteSavings * 10000) / 10000 }
|
|
136
|
-
: {}),
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
43
|
opts.onEvent?.({
|
|
141
44
|
type,
|
|
142
45
|
leader: leader.name,
|
|
143
46
|
data: {
|
|
144
47
|
...data,
|
|
145
|
-
_cost:
|
|
146
|
-
totalTokens,
|
|
147
|
-
totalCostUSD: Math.round(totalCostUSD * 10000) / 10000,
|
|
148
|
-
llmCalls,
|
|
149
|
-
cacheReadTokens,
|
|
150
|
-
cacheCreationTokens,
|
|
151
|
-
/** Run-wide USD saved by prompt caching vs a no-cache run. */
|
|
152
|
-
cacheSavingsUSD: Math.round(runCacheSavingsUSD * 10000) / 10000,
|
|
153
|
-
breakdown,
|
|
154
|
-
},
|
|
48
|
+
_cost: costTracker.buildCostPayload(),
|
|
155
49
|
},
|
|
156
50
|
});
|
|
157
51
|
};
|
|
@@ -263,100 +157,31 @@ export async function runSimulation(leader, keyPersonnel, opts = {}) {
|
|
|
263
157
|
maxSteps: opts.execution?.commanderMaxSteps ?? DEFAULT_EXECUTION.commanderMaxSteps,
|
|
264
158
|
});
|
|
265
159
|
const cmdSess = commander.session(`${sid}-cmd`);
|
|
266
|
-
// Bootstrap commander
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
const personalityCue = (() => {
|
|
271
|
-
const h = leader.hexaco;
|
|
272
|
-
const cues = [];
|
|
273
|
-
if (h.openness > 0.7)
|
|
274
|
-
cues.push('You favor novel, untested approaches over proven ones');
|
|
275
|
-
if (h.openness < 0.3)
|
|
276
|
-
cues.push('You favor proven protocols over experiments');
|
|
277
|
-
if (h.conscientiousness > 0.7)
|
|
278
|
-
cues.push('You demand evidence and contingency plans before committing');
|
|
279
|
-
if (h.conscientiousness < 0.3)
|
|
280
|
-
cues.push('You move fast and accept ambiguity');
|
|
281
|
-
if (h.emotionality > 0.7)
|
|
282
|
-
cues.push('You weigh human cost heavily — even small mortality risks deter you');
|
|
283
|
-
if (h.emotionality < 0.3)
|
|
284
|
-
cues.push('You will accept casualties for strategic gain');
|
|
285
|
-
if (h.agreeableness < 0.4)
|
|
286
|
-
cues.push('You override department consensus when you see a better path');
|
|
287
|
-
if (h.honestyHumility < 0.4)
|
|
288
|
-
cues.push('You leverage information asymmetries when useful');
|
|
289
|
-
return cues.length ? `Your decision style: ${cues.join('. ')}.` : '';
|
|
290
|
-
})();
|
|
291
|
-
// Bootstrap the commander. This is the FIRST LLM call in the run, so if
|
|
292
|
-
// the user's API key is invalid or credits are exhausted, this is where
|
|
293
|
-
// we find out. Report it through the classifier so the dashboard banner
|
|
294
|
-
// fires before we burn compute launching a sim that has no hope of
|
|
295
|
-
// producing valid output.
|
|
160
|
+
// Bootstrap the commander with a HEXACO-derived personality cue. This
|
|
161
|
+
// is the FIRST LLM call in the run, so if the user's key is invalid
|
|
162
|
+
// or credits are exhausted, the classifier fires here before we burn
|
|
163
|
+
// compute on a run that has no hope of producing valid output.
|
|
296
164
|
try {
|
|
297
|
-
trackUsage(await cmdSess.send(
|
|
298
|
-
`${personalityCue} ` +
|
|
299
|
-
`Your personality MUST visibly shape your choices — do not converge on a centrist option just because ` +
|
|
300
|
-
`it sounds reasonable. If your traits push you toward the risky option, take it; if they push you toward ` +
|
|
301
|
-
`the safe option, take it. The simulation's value is in how different leaders produce different outcomes ` +
|
|
302
|
-
`from the same starting state. ` +
|
|
303
|
-
`When the crisis includes options with IDs, you MUST include selectedOptionId in your JSON response. ` +
|
|
304
|
-
`Return JSON with selectedOptionId, decision, rationale, selectedPolicies, rejectedPolicies, ` +
|
|
305
|
-
`expectedTradeoffs, watchMetricsNextTurn. Acknowledge.`), 'commander');
|
|
165
|
+
trackUsage(await cmdSess.send(buildCommanderBootstrap(buildPersonalityCue(leader.hexaco))), 'commander');
|
|
306
166
|
}
|
|
307
167
|
catch (err) {
|
|
308
168
|
reportProviderError(err, 'commander-bootstrap');
|
|
309
|
-
// If this is a terminal provider error,
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
// gets a proper `complete` SSE event (with the `provider_error` event
|
|
313
|
-
// already sent above).
|
|
314
|
-
}
|
|
315
|
-
// Turn 0: Commander promotes department heads from agent roster
|
|
316
|
-
console.log(' [Turn 0] Commander evaluating roster for promotions...');
|
|
317
|
-
const promotionDepts = sc.departments.map(d => d.id);
|
|
318
|
-
const roleNames = Object.fromEntries(sc.departments.map(d => [d.id, d.role]));
|
|
319
|
-
const candidateSummaries = promotionDepts.map(dept => {
|
|
320
|
-
const candidates = kernel.getCandidates(dept, 5);
|
|
321
|
-
return `## ${dept.toUpperCase()} — Top 5 Candidates:\n${candidates.map(c => {
|
|
322
|
-
const age = startYear - c.core.birthYear;
|
|
323
|
-
const h = c.hexaco;
|
|
324
|
-
return `- ${c.core.name} (${c.core.id}), age ${age}, spec: ${c.career.specialization}, O:${h.openness.toFixed(2)} C:${h.conscientiousness.toFixed(2)} E:${h.extraversion.toFixed(2)} A:${h.agreeableness.toFixed(2)} Em:${h.emotionality.toFixed(2)} HH:${h.honestyHumility.toFixed(2)}`;
|
|
325
|
-
}).join('\n')}`;
|
|
326
|
-
}).join('\n\n');
|
|
327
|
-
const promoResult = await cmdSess.send(buildPromotionPrompt(candidateSummaries));
|
|
328
|
-
// Promotion is a commander-session call, so it lands in the commander bucket.
|
|
329
|
-
trackUsage(promoResult, 'commander');
|
|
330
|
-
const promoMatch = promoResult.text.match(/\{[\s\S]*"promotions"[\s\S]*\}/);
|
|
331
|
-
if (promoMatch) {
|
|
332
|
-
try {
|
|
333
|
-
const pd = JSON.parse(promoMatch[0]);
|
|
334
|
-
for (const p of pd.promotions || []) {
|
|
335
|
-
try {
|
|
336
|
-
kernel.promoteAgent(p.agentId, p.department, p.role, leader.name);
|
|
337
|
-
console.log(` ✦ ${p.agentId} → ${p.role}: ${p.reason?.slice(0, 80)}`);
|
|
338
|
-
emit('promotion', { agentId: p.agentId, department: p.department, role: p.role, reason: p.reason?.slice(0, 120) });
|
|
339
|
-
}
|
|
340
|
-
catch (err) {
|
|
341
|
-
console.log(` ✦ Promotion failed: ${err}`);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
catch (e) {
|
|
346
|
-
console.warn(' [promotion] Failed to parse promotion JSON:', e);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
// Fallback: promote top candidate per dept if commander didn't produce valid JSON
|
|
350
|
-
for (const dept of promotionDepts) {
|
|
351
|
-
const hasLeader = kernel.getState().agents.some(c => c.promotion?.department === dept);
|
|
352
|
-
if (!hasLeader) {
|
|
353
|
-
const top = kernel.getCandidates(dept, 1)[0];
|
|
354
|
-
if (top) {
|
|
355
|
-
kernel.promoteAgent(top.core.id, dept, roleNames[dept] || `Head of ${dept}`, leader.name);
|
|
356
|
-
console.log(` ✦ [fallback] ${top.core.name} → ${roleNames[dept]}`);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
169
|
+
// If this is a terminal provider error, isAborted() is now true; the
|
|
170
|
+
// turn loop skips LLM work but continues so the end-of-run cleanup
|
|
171
|
+
// path runs and the user gets a proper `complete` SSE event.
|
|
359
172
|
}
|
|
173
|
+
// Turn 0: commander promotes department heads from the kernel's
|
|
174
|
+
// candidate roster. Any dept the commander skips gets its top
|
|
175
|
+
// candidate promoted via fallback so turn 1 starts with a full cabinet.
|
|
176
|
+
await runDepartmentPromotions({
|
|
177
|
+
kernel,
|
|
178
|
+
scenario: sc,
|
|
179
|
+
leader,
|
|
180
|
+
startYear,
|
|
181
|
+
sendToCommander: (prompt) => cmdSess.send(prompt),
|
|
182
|
+
trackUsage,
|
|
183
|
+
emit,
|
|
184
|
+
});
|
|
360
185
|
// Captured forge events keyed by department. Each `wrapForgeTool` push
|
|
361
186
|
// here on every successful or failed forge; we drain the dept's bucket
|
|
362
187
|
// around each `dept_done` emit to attribute forges to the right event.
|
|
@@ -1343,89 +1168,13 @@ Respond with valid JSON ONLY (no markdown, no prose outside the JSON):
|
|
|
1343
1168
|
? sc.hooks.fingerprintHook(final, outcomeLog, leader, toolRegs, maxTurns)
|
|
1344
1169
|
: {};
|
|
1345
1170
|
const fingerprint = { ...generic, ...scenarioOverlay };
|
|
1346
|
-
//
|
|
1347
|
-
//
|
|
1348
|
-
//
|
|
1349
|
-
|
|
1350
|
-
//
|
|
1351
|
-
//
|
|
1352
|
-
|
|
1353
|
-
// calls; the ledger.history captures every USE (forge or reuse).
|
|
1354
|
-
const forgedToolbox = (() => {
|
|
1355
|
-
const out = [];
|
|
1356
|
-
for (const [name, entry] of forgedLedger) {
|
|
1357
|
-
// Find the original forge metadata (description, mode) from the
|
|
1358
|
-
// first matching captured forge if available; fall back to history.
|
|
1359
|
-
const firstForge = allForges.find(f => f.name === name);
|
|
1360
|
-
const departments = new Set();
|
|
1361
|
-
let approved = false;
|
|
1362
|
-
let maxConfidence = 0;
|
|
1363
|
-
let sampleOutput = null;
|
|
1364
|
-
let reforgeCount = 0;
|
|
1365
|
-
let rejectedReforges = 0;
|
|
1366
|
-
for (const h of entry.history) {
|
|
1367
|
-
departments.add(h.department);
|
|
1368
|
-
if (!h.rejected)
|
|
1369
|
-
approved = true;
|
|
1370
|
-
if (typeof h.confidence === 'number' && h.confidence > maxConfidence)
|
|
1371
|
-
maxConfidence = h.confidence;
|
|
1372
|
-
if (h.output)
|
|
1373
|
-
sampleOutput = h.output;
|
|
1374
|
-
if (h.isReforge) {
|
|
1375
|
-
reforgeCount++;
|
|
1376
|
-
if (h.rejected)
|
|
1377
|
-
rejectedReforges++;
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
const totalUses = entry.history.length;
|
|
1381
|
-
out.push({
|
|
1382
|
-
name,
|
|
1383
|
-
description: firstForge?.description || name,
|
|
1384
|
-
mode: firstForge?.mode || 'sandbox',
|
|
1385
|
-
firstForgedTurn: entry.firstForgedTurn,
|
|
1386
|
-
firstForgedDepartment: entry.firstForgedDepartment,
|
|
1387
|
-
firstForgedEventTitle: entry.firstForgedEventTitle,
|
|
1388
|
-
departments: [...departments],
|
|
1389
|
-
reuseCount: Math.max(0, totalUses - 1),
|
|
1390
|
-
totalUses,
|
|
1391
|
-
reforgeCount,
|
|
1392
|
-
rejectedReforges,
|
|
1393
|
-
approved: approved || (firstForge?.approved ?? false),
|
|
1394
|
-
// Use the highest judge confidence across this tool's invocations.
|
|
1395
|
-
// If the tool was never approved (only failed forges), confidence
|
|
1396
|
-
// is 0 — never fabricate an 0.85 default that misrepresents
|
|
1397
|
-
// rejected tools as borderline-passable.
|
|
1398
|
-
confidence: maxConfidence || (firstForge?.approved ? firstForge.confidence : 0),
|
|
1399
|
-
inputSchema: entry.inputSchema,
|
|
1400
|
-
outputSchema: entry.outputSchema,
|
|
1401
|
-
sampleOutput,
|
|
1402
|
-
history: entry.history.slice(),
|
|
1403
|
-
});
|
|
1404
|
-
}
|
|
1405
|
-
return out.sort((a, b) => a.firstForgedTurn - b.firstForgedTurn);
|
|
1406
|
-
})();
|
|
1407
|
-
// Flat unique citation list across all department reports.
|
|
1408
|
-
const citationCatalog = (() => {
|
|
1409
|
-
const byKey = new Map();
|
|
1410
|
-
for (const { turn: t, report } of allDepartmentReports) {
|
|
1411
|
-
for (const c of report.citations) {
|
|
1412
|
-
const key = (c.url || '').trim() || `text:${(c.text || '').trim()}`;
|
|
1413
|
-
if (!key)
|
|
1414
|
-
continue;
|
|
1415
|
-
let entry = byKey.get(key);
|
|
1416
|
-
if (!entry) {
|
|
1417
|
-
entry = { text: c.text || c.url || '', url: c.url || '', doi: c.doi, departments: new Set(), turns: new Set() };
|
|
1418
|
-
byKey.set(key, entry);
|
|
1419
|
-
}
|
|
1420
|
-
if (report.department)
|
|
1421
|
-
entry.departments.add(report.department);
|
|
1422
|
-
entry.turns.add(t);
|
|
1423
|
-
if (!entry.doi && c.doi)
|
|
1424
|
-
entry.doi = c.doi;
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
return [...byKey.values()].map(e => ({ ...e, departments: [...e.departments], turns: [...e.turns].sort((a, b) => a - b) }));
|
|
1428
|
-
})();
|
|
1171
|
+
// Canonical Forged Toolbox: deduplicated by tool name with first-forge
|
|
1172
|
+
// metadata, full invocation history, and reuse/rejection rollups.
|
|
1173
|
+
// Matches the data the dashboard's ToolboxSection renders.
|
|
1174
|
+
const forgedToolbox = buildForgedToolbox(forgedLedger, allForges);
|
|
1175
|
+
// Flat unique citation list across all department reports. URL is the
|
|
1176
|
+
// dedup key; each entry carries the departments + turns that cited it.
|
|
1177
|
+
const citationCatalog = buildCitationCatalog(allDepartmentReports);
|
|
1429
1178
|
const output = {
|
|
1430
1179
|
simulation: `${sc.id}-v3`,
|
|
1431
1180
|
leader: { name: leader.name, archetype: leader.archetype, colony: leader.colony, hexaco: leader.hexaco },
|
|
@@ -1451,7 +1200,7 @@ Respond with valid JSON ONLY (no markdown, no prose outside the JSON):
|
|
|
1451
1200
|
/** Per-turn agent reactions with mood + quote + memory snippets. */
|
|
1452
1201
|
agentReactions: allAgentReactions,
|
|
1453
1202
|
/** Cost telemetry for the run. */
|
|
1454
|
-
cost:
|
|
1203
|
+
cost: costTracker.finalCost(),
|
|
1455
1204
|
/**
|
|
1456
1205
|
* When a terminal provider error (quota exhausted, invalid API key)
|
|
1457
1206
|
* was hit during the run, this carries the classified reason. `null`
|