@agent-e/core 1.6.6 → 1.6.8

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/index.mjs CHANGED
@@ -60,7 +60,9 @@ var DEFAULT_THRESHOLDS = {
60
60
  relativePriceConvergenceTarget: 0.85,
61
61
  priceDiscoveryWindowTicks: 20,
62
62
  giftTradeFilterRatio: 0.15,
63
- disposalTradeWeightDiscount: 0.5
63
+ disposalTradeWeightDiscount: 0.5,
64
+ // Structural dominance (P8)
65
+ dominantRoles: []
64
66
  };
65
67
  var PERSONA_HEALTHY_RANGES = {
66
68
  Active: { min: 0.2, max: 0.4 },
@@ -92,7 +94,7 @@ var Observer = class {
92
94
  registerCustomMetric(name, fn) {
93
95
  this.customMetricFns[name] = fn;
94
96
  }
95
- compute(state, recentEvents) {
97
+ compute(state, recentEvents, personaDistribution) {
96
98
  if (!state.currencies || state.currencies.length === 0) {
97
99
  console.warn("[AgentE] Warning: state.currencies is empty. Metrics will be zeroed.");
98
100
  }
@@ -245,6 +247,18 @@ var Observer = class {
245
247
  for (const [role, count] of Object.entries(populationByRole)) {
246
248
  roleShares[role] = count / Math.max(1, totalAgents);
247
249
  }
250
+ const uniqueRoles = new Set(Object.values(state.agentRoles));
251
+ const rolesEmpty = uniqueRoles.size <= 1;
252
+ if (rolesEmpty && personaDistribution && Object.keys(personaDistribution).length > 0) {
253
+ for (const key of Object.keys(populationByRole)) delete populationByRole[key];
254
+ for (const key of Object.keys(roleShares)) delete roleShares[key];
255
+ for (const [persona, fraction] of Object.entries(personaDistribution)) {
256
+ populationByRole[persona] = Math.round(fraction * totalAgents);
257
+ }
258
+ for (const [role, count] of Object.entries(populationByRole)) {
259
+ roleShares[role] = count / Math.max(1, totalAgents);
260
+ }
261
+ }
248
262
  const churnByRole = {};
249
263
  for (const e of roleChangeEvents) {
250
264
  const role = e.role ?? "unknown";
@@ -734,25 +748,50 @@ var P4_MaterialsFlowFasterThanCooldown = {
734
748
  category: "supply_chain",
735
749
  description: "Input delivery rate must exceed or match production cooldown rate. If producers produce every 5 ticks but only receive raw materials every 10 ticks, they starve regardless of supply levels.",
736
750
  check(metrics, _thresholds) {
737
- const { supplyByResource, populationByRole, velocity, totalAgents } = metrics;
751
+ const { supplyByResource, populationByRole, velocity, velocityByCurrency, totalAgents } = metrics;
738
752
  const totalSupply = Object.values(supplyByResource).reduce((s, v) => s + v, 0);
739
753
  const avgSupplyPerAgent = totalAgents > 0 ? totalSupply / totalAgents : 0;
740
754
  const roleEntries = Object.entries(populationByRole);
741
755
  const totalRoles = roleEntries.length;
742
- if (totalRoles >= 2 && velocity < 5 && avgSupplyPerAgent < 0.5) {
743
- return {
744
- violated: true,
745
- severity: 5,
746
- evidence: { avgSupplyPerAgent, velocity, totalRoles },
747
- suggestedAction: {
748
- parameterType: "yield",
749
- direction: "increase",
750
- magnitude: 0.15,
751
- reasoning: "Low supply per agent with stagnant velocity. Increase yield to compensate."
752
- },
753
- confidence: 0.65,
754
- estimatedLag: 8
755
- };
756
+ if (totalRoles >= 2 && avgSupplyPerAgent < 0.5) {
757
+ const stagnantCurrencies = [];
758
+ for (const [currency, currVelocity] of Object.entries(velocityByCurrency)) {
759
+ if (currVelocity < 5) {
760
+ stagnantCurrencies.push({ currency, currVelocity });
761
+ }
762
+ }
763
+ if (stagnantCurrencies.length > 0) {
764
+ const worst = stagnantCurrencies.reduce((a, b) => a.currVelocity < b.currVelocity ? a : b);
765
+ return {
766
+ violated: true,
767
+ severity: 5,
768
+ evidence: { stagnantCurrencies, worst: worst.currency, avgSupplyPerAgent, totalRoles },
769
+ suggestedAction: {
770
+ parameterType: "yield",
771
+ scope: { currency: worst.currency },
772
+ direction: "increase",
773
+ magnitude: 0.15,
774
+ reasoning: `${stagnantCurrencies.length} currencies stagnant (worst: ${worst.currency} at ${worst.currVelocity.toFixed(1)}). Increase yield to compensate.`
775
+ },
776
+ confidence: 0.7,
777
+ estimatedLag: 8
778
+ };
779
+ }
780
+ if (Object.keys(velocityByCurrency).length === 0 && velocity < 5) {
781
+ return {
782
+ violated: true,
783
+ severity: 5,
784
+ evidence: { avgSupplyPerAgent, velocity, totalRoles },
785
+ suggestedAction: {
786
+ parameterType: "yield",
787
+ direction: "increase",
788
+ magnitude: 0.15,
789
+ reasoning: "Low supply per agent with stagnant velocity. Increase yield to compensate."
790
+ },
791
+ confidence: 0.65,
792
+ estimatedLag: 8
793
+ };
794
+ }
756
795
  }
757
796
  if (avgSupplyPerAgent > 2) {
758
797
  return {
@@ -911,28 +950,31 @@ var P8_RegulatorCannotFightDesign = {
911
950
  id: "P8",
912
951
  name: "Regulator Cannot Fight the Design",
913
952
  category: "incentive",
914
- description: "If the economy is designed to have a majority role (e.g. dominant role exceeds 55%), the regulator must know this and exempt that role from population suppression. AgentE at tick 1 seeing dominant role exceeds 55% and slashing pool rewards is overreach.",
915
- check(metrics, _thresholds) {
953
+ description: "If a role dominates above 30%, classify as structural (in dominantRoles config) or pathological. Structural dominance gets no intervention. Pathological dominance gets crowding pressure.",
954
+ check(metrics, thresholds) {
916
955
  const { roleShares, avgSatisfaction } = metrics;
917
- if (avgSatisfaction < 45) {
918
- const dominantRole = Object.entries(roleShares).sort((a, b) => b[1] - a[1])[0];
919
- if (dominantRole && dominantRole[1] > 0.3) {
920
- return {
921
- violated: true,
922
- severity: 4,
923
- evidence: { dominantRole: dominantRole[0], share: dominantRole[1], avgSatisfaction },
924
- suggestedAction: {
925
- parameterType: "reward",
926
- direction: "increase",
927
- magnitude: 0.1,
928
- reasoning: `Low satisfaction with ${dominantRole[0]} dominant. Regulator may be suppressing a structurally necessary role. Ease pressure on dominant role rewards.`
929
- },
930
- confidence: 0.55,
931
- estimatedLag: 8
932
- };
933
- }
956
+ const dominant = Object.entries(roleShares).sort((a, b) => b[1] - a[1])[0];
957
+ if (!dominant) return { violated: false };
958
+ const [dominantRole, dominantShare] = dominant;
959
+ if (dominantShare <= 0.3) return { violated: false };
960
+ const isStructural = thresholds.dominantRoles?.includes(dominantRole) ?? false;
961
+ if (isStructural) {
962
+ return { violated: false };
934
963
  }
935
- return { violated: false };
964
+ return {
965
+ violated: true,
966
+ severity: 7,
967
+ evidence: { role: dominantRole, share: dominantShare, classification: "pathological", avgSatisfaction },
968
+ suggestedAction: {
969
+ parameterType: "reward",
970
+ scope: { tags: [dominantRole] },
971
+ direction: "decrease",
972
+ magnitude: 0.1,
973
+ reasoning: `${dominantRole} at ${(dominantShare * 100).toFixed(0)}% is not a designed majority \u2014 apply crowding pressure.`
974
+ },
975
+ confidence: 0.7,
976
+ estimatedLag: 12
977
+ };
936
978
  }
937
979
  };
938
980
  var INCENTIVE_PRINCIPLES = [
@@ -1198,7 +1240,7 @@ var P15_PoolsNeedCapAndDecay = {
1198
1240
  id: "P15",
1199
1241
  name: "Pools Need Cap + Decay",
1200
1242
  category: "currency",
1201
- description: "Any pool (bank, reward pool) without a cap accumulates infinitely. A pool at 42% of total supply means 42% of the economy is frozen. Cap at 5%, decay at 2%/tick.",
1243
+ description: "Any pool (bank, reward pool) without a cap accumulates infinitely. A pool at 42% of total supply means 42% of the economy is frozen. Cap configurable (default 10%). Violation fires at 2\xD7 cap share of total supply.",
1202
1244
  check(metrics, thresholds) {
1203
1245
  const { poolCapPercent } = thresholds;
1204
1246
  for (const [pool, currencyAmounts] of Object.entries(metrics.poolSizesByCurrency)) {
@@ -1231,7 +1273,7 @@ var P16_WithdrawalPenaltyScales = {
1231
1273
  id: "P16",
1232
1274
  name: "Withdrawal Penalty Scales with Lock Duration",
1233
1275
  category: "currency",
1234
- description: "A 50-tick lock period with a penalty calculated as /100 means agents can exit after 1 tick and keep 99% of accrued yield. Penalty must scale linearly: (1 - ticksStaked/lockDuration) \xD7 yield.",
1276
+ description: "Depleted pool with estimated high staking signals early-withdrawal abuse. Symptom detector \u2014 penalty formula is developer responsibility.",
1235
1277
  check(metrics, _thresholds) {
1236
1278
  for (const [poolName, currencyAmounts] of Object.entries(metrics.poolSizesByCurrency)) {
1237
1279
  for (const curr of metrics.currencies) {
@@ -1347,7 +1389,7 @@ var P17_GracePeriodBeforeIntervention = {
1347
1389
  id: "P17",
1348
1390
  name: "Grace Period Before Intervention",
1349
1391
  category: "bootstrap",
1350
- description: "Any intervention before tick 50 is premature. The economy needs time to bootstrap with designed distributions. AgentE intervening at tick 1 against dominant role exceeds 55% (designed) killed the economy instantly.",
1392
+ description: "Any intervention before tick 30 is premature. The economy needs time to bootstrap with designed distributions. Early intervention against designed dominance can kill the economy instantly.",
1351
1393
  check(metrics, _thresholds) {
1352
1394
  if (metrics.tick < 30 && metrics.avgSatisfaction < 40) {
1353
1395
  return {
@@ -1668,7 +1710,7 @@ var P27_AdjustmentsNeedCooldowns = {
1668
1710
  id: "P27",
1669
1711
  name: "Adjustments Need Cooldowns",
1670
1712
  category: "regulator",
1671
- description: "Adjusting the same parameter twice in a window causes oscillation. Minimum 15 ticks between same-parameter adjustments. This is enforced in the Planner but checked here as a diagnostic.",
1713
+ description: "High churn + low satisfaction may indicate oscillation from rapid adjustments. Cooldown enforcement is structural (Planner). This is a symptom detector.",
1672
1714
  check(metrics, _thresholds) {
1673
1715
  const { churnRate, avgSatisfaction } = metrics;
1674
1716
  if (churnRate > 0.08 && avgSatisfaction < 50) {
@@ -1690,42 +1732,11 @@ var P27_AdjustmentsNeedCooldowns = {
1690
1732
  return { violated: false };
1691
1733
  }
1692
1734
  };
1693
- var P28_StructuralDominanceIsNotPathological = {
1694
- id: "P28",
1695
- name: "Structural Dominance \u2260 Pathological Monopoly",
1696
- category: "regulator",
1697
- description: 'A designed dominant role (majority exceeds 55%) should not trigger population suppression. AgentE must distinguish between "this role is dominant BY DESIGN" (configured via dominantRoles) and "this role took over unexpectedly".',
1698
- check(metrics, _thresholds) {
1699
- const { roleShares, avgSatisfaction } = metrics;
1700
- const dominant = Object.entries(roleShares).sort((a, b) => b[1] - a[1])[0];
1701
- if (!dominant) return { violated: false };
1702
- const [dominantRole, dominantShare] = dominant;
1703
- if (dominantShare > 0.4 && avgSatisfaction > 70) {
1704
- return { violated: false };
1705
- }
1706
- if (dominantShare > 0.4 && avgSatisfaction < 50) {
1707
- return {
1708
- violated: true,
1709
- severity: 5,
1710
- evidence: { dominantRole, dominantShare, avgSatisfaction },
1711
- suggestedAction: {
1712
- parameterType: "cost",
1713
- direction: "decrease",
1714
- magnitude: 0.1,
1715
- reasoning: `${dominantRole} dominant (${(dominantShare * 100).toFixed(0)}%) with low satisfaction. Pathological dominance \u2014 agents trapped, not thriving. Ease costs to allow role switching.`
1716
- },
1717
- confidence: 0.65,
1718
- estimatedLag: 15
1719
- };
1720
- }
1721
- return { violated: false };
1722
- }
1723
- };
1724
1735
  var P38_CommunicationPreventsRevolt = {
1725
1736
  id: "P38",
1726
1737
  name: "Communication Prevents Revolt",
1727
1738
  category: "regulator",
1728
- description: "Every adjustment must be logged with reasoning. An adjustment made without explanation to participants causes revolt. AgentE logs every decision \u2014 this principle checks that logging is active.",
1739
+ description: "High churn may indicate unexplained changes. Logging enforcement is structural (DecisionLog). Flags high churn as signal to review recent decisions.",
1729
1740
  check(metrics, _thresholds) {
1730
1741
  const { churnRate } = metrics;
1731
1742
  if (churnRate > 0.1) {
@@ -1750,7 +1761,7 @@ var REGULATOR_PRINCIPLES = [
1750
1761
  P25_CorrectLeversForCorrectProblems,
1751
1762
  P26_ContinuousPressureBeatsThresholdCuts,
1752
1763
  P27_AdjustmentsNeedCooldowns,
1753
- P28_StructuralDominanceIsNotPathological,
1764
+ // P28 merged into P8 (v1.6.7)
1754
1765
  P38_CommunicationPreventsRevolt
1755
1766
  ];
1756
1767
 
@@ -2059,7 +2070,7 @@ var P43_SimulationMinimum = {
2059
2070
  id: "P43",
2060
2071
  name: "Simulation Minimum (100 Iterations)",
2061
2072
  category: "statistical",
2062
- description: "Fewer than 100 Monte Carlo iterations produces unreliable predictions. The variance of a 10-iteration simulation is so high that you might as well be guessing. This principle enforces the minimum in the Simulator.",
2073
+ description: "Wild inflation swings (>30%) may indicate insufficient simulation data. Minimum iteration enforcement is structural (Simulator config). Symptom detector.",
2063
2074
  check(metrics, thresholds) {
2064
2075
  const { inflationRate } = metrics;
2065
2076
  if (Math.abs(inflationRate) > 0.3) {
@@ -2226,7 +2237,7 @@ var P49_IdleAssetTax = {
2226
2237
  id: "P49",
2227
2238
  name: "Idle Asset Tax",
2228
2239
  category: "resource",
2229
- description: 'Appreciating assets without holding cost \u2192 wealth concentration. If hoarding an asset makes you richer just by holding it, everyone hoards. Decay rates, storage costs, or expiry are "idle asset taxes" that force circulation.',
2240
+ description: "Concentrated idle wealth (Gini >0.55, top 10% >60%, velocity <5). Raises transaction fees as proxy holding tax. Decay/storage/expiry requires developer implementation.",
2230
2241
  check(metrics, _thresholds) {
2231
2242
  const { giniCoefficient, top10PctShare, velocity } = metrics;
2232
2243
  if (giniCoefficient > 0.55 && top10PctShare > 0.6 && velocity < 5) {
@@ -2259,7 +2270,7 @@ var P33_FairNotEqual = {
2259
2270
  id: "P33",
2260
2271
  name: "Fair \u2260 Equal",
2261
2272
  category: "participant_experience",
2262
- description: "Gini = 0 is boring \u2014 everyone has the same and there is nothing to strive for. Healthy inequality from skill/effort is fine. Inequality from money (pay-to-win) is toxic. Target Gini 0.3-0.45: meaningful spread, not oligarchy.",
2273
+ description: "Gini = 0 is boring \u2014 everyone has the same and there is nothing to strive for. Healthy inequality from skill/effort is fine. Inequality from money (pay-to-win) is toxic. Below 0.10 Gini = too flat; above configurable thresholds = oligarchy.",
2263
2274
  check(metrics, thresholds) {
2264
2275
  for (const curr of metrics.currencies) {
2265
2276
  const giniCoefficient = metrics.giniCoefficientByCurrency[curr] ?? 0;
@@ -2369,7 +2380,7 @@ var P45_TimeBudget = {
2369
2380
  id: "P45",
2370
2381
  name: "Time Budget",
2371
2382
  category: "participant_experience",
2372
- description: "required_time \u2264 available_time \xD7 0.8. If the economy requires more engagement than participants can realistically give, it is a disguised paywall. The 0.8 buffer accounts for real life.",
2383
+ description: "timeToValue > 30 with satisfaction < 55 suggests excessive time demand. Proxy metric \u2014 does not measure individual available time.",
2373
2384
  check(metrics, thresholds) {
2374
2385
  const { timeToValue, avgSatisfaction } = metrics;
2375
2386
  const timePressure = timeToValue > 30;
@@ -2607,7 +2618,7 @@ var P52_EndowmentEffect = {
2607
2618
  id: "P52",
2608
2619
  name: "Endowment Effect",
2609
2620
  category: "operations",
2610
- description: "Participants who never owned premium assets do not value them. Free trial activities that let participants experience premium assets drive conversions because ownership creates perceived value (endowment effect).",
2621
+ description: "High completion (>90%) + low satisfaction (<60) suggests activities not creating perceived value. May indicate missing endowment effect.",
2611
2622
  check(metrics, _thresholds) {
2612
2623
  const { avgSatisfaction, churnRate } = metrics;
2613
2624
  const { eventCompletionRate } = metrics;
@@ -2680,7 +2691,7 @@ var P54_OperationalCadence = {
2680
2691
  id: "P54",
2681
2692
  name: "Operational Cadence",
2682
2693
  category: "operations",
2683
- description: ">50% of activities that are re-wrapped existing supply \u2192 stagnation. The cadence must include genuinely new supply at regular intervals. This is an advisory principle \u2014 AgentE can flag but cannot fix supply.",
2694
+ description: "Low velocity (<2) + low satisfaction after tick 100 = supply stagnation. Advisory signal for developer to audit content freshness.",
2684
2695
  check(metrics, _thresholds) {
2685
2696
  const { velocity, avgSatisfaction } = metrics;
2686
2697
  if (velocity < 2 && avgSatisfaction < 55 && metrics.tick > 100) {
@@ -3530,11 +3541,14 @@ var DEFAULT_PERSONA_CONFIG = {
3530
3541
  dormantWindow: 20,
3531
3542
  atRiskDropThreshold: 0.5,
3532
3543
  powerUserMinSystems: 3,
3533
- historyWindow: 50
3544
+ historyWindow: 50,
3545
+ reclassifyInterval: 10
3534
3546
  };
3535
3547
  var PersonaTracker = class {
3536
3548
  constructor(config) {
3537
3549
  this.agents = /* @__PURE__ */ new Map();
3550
+ this.cachedDistribution = {};
3551
+ this.lastClassifiedTick = -Infinity;
3538
3552
  this.config = { ...DEFAULT_PERSONA_CONFIG, ...config };
3539
3553
  }
3540
3554
  /**
@@ -3542,6 +3556,7 @@ var PersonaTracker = class {
3542
3556
  * Call this once per tick BEFORE getDistribution().
3543
3557
  */
3544
3558
  update(state, events) {
3559
+ if (!state.agentBalances) return;
3545
3560
  const tick = state.tick;
3546
3561
  const txByAgent = /* @__PURE__ */ new Map();
3547
3562
  if (events) {
@@ -3597,8 +3612,18 @@ var PersonaTracker = class {
3597
3612
  /**
3598
3613
  * Classify all tracked agents and return the population distribution.
3599
3614
  * Returns { Whale: 0.05, ActiveTrader: 0.18, Passive: 0.42, ... }
3615
+ * Caches results and only reclassifies at `reclassifyInterval` boundaries.
3600
3616
  */
3601
- getDistribution() {
3617
+ getDistribution(currentTick) {
3618
+ const tick = currentTick ?? 0;
3619
+ if (tick - this.lastClassifiedTick < this.config.reclassifyInterval && Object.keys(this.cachedDistribution).length > 0) {
3620
+ return this.cachedDistribution;
3621
+ }
3622
+ this.lastClassifiedTick = tick;
3623
+ this.cachedDistribution = this._classify();
3624
+ return this.cachedDistribution;
3625
+ }
3626
+ _classify() {
3602
3627
  const agentIds = [...this.agents.keys()];
3603
3628
  const total = agentIds.length;
3604
3629
  if (total === 0) return {};
@@ -3904,16 +3929,17 @@ var AgentE = class {
3904
3929
  this.currentTick = currentState.tick;
3905
3930
  const events = this.eventBuffer;
3906
3931
  this.eventBuffer = [];
3932
+ this.personaTracker.update(currentState, events);
3933
+ const personaDist = this.personaTracker.getDistribution(currentState.tick);
3907
3934
  let metrics;
3908
3935
  try {
3909
- metrics = this.observer.compute(currentState, events);
3936
+ metrics = this.observer.compute(currentState, events, personaDist);
3910
3937
  } catch (err) {
3911
3938
  console.error(`[AgentE] Observer.compute() failed at tick ${currentState.tick}:`, err);
3912
3939
  return;
3913
3940
  }
3914
3941
  this.store.record(metrics);
3915
- this.personaTracker.update(currentState, events);
3916
- metrics.personaDistribution = this.personaTracker.getDistribution();
3942
+ metrics.personaDistribution = personaDist;
3917
3943
  const { rolledBack, settled } = await this.executor.checkRollbacks(metrics, this.adapter);
3918
3944
  for (const plan2 of rolledBack) {
3919
3945
  this.planner.recordRolledBack(plan2);
@@ -4462,7 +4488,7 @@ export {
4462
4488
  P25_CorrectLeversForCorrectProblems,
4463
4489
  P26_ContinuousPressureBeatsThresholdCuts,
4464
4490
  P27_AdjustmentsNeedCooldowns,
4465
- P28_StructuralDominanceIsNotPathological,
4491
+ P8_RegulatorCannotFightDesign as P28_StructuralDominanceIsNotPathological,
4466
4492
  P29_BottleneckDetection,
4467
4493
  P2_ClosedLoopsNeedDirectHandoff,
4468
4494
  P30_DynamicBottleneckRotation,