@agent-e/core 1.6.5 → 1.6.7

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,16 @@ 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 [persona, fraction] of Object.entries(personaDistribution)) {
254
+ populationByRole[persona] = Math.round(fraction * totalAgents);
255
+ }
256
+ for (const [role, count] of Object.entries(populationByRole)) {
257
+ roleShares[role] = count / Math.max(1, totalAgents);
258
+ }
259
+ }
248
260
  const churnByRole = {};
249
261
  for (const e of roleChangeEvents) {
250
262
  const role = e.role ?? "unknown";
@@ -734,25 +746,45 @@ var P4_MaterialsFlowFasterThanCooldown = {
734
746
  category: "supply_chain",
735
747
  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
748
  check(metrics, _thresholds) {
737
- const { supplyByResource, populationByRole, velocity, totalAgents } = metrics;
749
+ const { supplyByResource, populationByRole, velocity, velocityByCurrency, totalAgents } = metrics;
738
750
  const totalSupply = Object.values(supplyByResource).reduce((s, v) => s + v, 0);
739
751
  const avgSupplyPerAgent = totalAgents > 0 ? totalSupply / totalAgents : 0;
740
752
  const roleEntries = Object.entries(populationByRole);
741
753
  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
- };
754
+ if (totalRoles >= 2 && avgSupplyPerAgent < 0.5) {
755
+ for (const [currency, currVelocity] of Object.entries(velocityByCurrency)) {
756
+ if (currVelocity < 5) {
757
+ return {
758
+ violated: true,
759
+ severity: 5,
760
+ evidence: { currency, currVelocity, avgSupplyPerAgent, totalRoles },
761
+ suggestedAction: {
762
+ parameterType: "yield",
763
+ scope: { currency },
764
+ direction: "increase",
765
+ magnitude: 0.15,
766
+ reasoning: `${currency} velocity is ${currVelocity.toFixed(1)} \u2014 materials not flowing. Increase yield to compensate.`
767
+ },
768
+ confidence: 0.7,
769
+ estimatedLag: 8
770
+ };
771
+ }
772
+ }
773
+ if (Object.keys(velocityByCurrency).length === 0 && velocity < 5) {
774
+ return {
775
+ violated: true,
776
+ severity: 5,
777
+ evidence: { avgSupplyPerAgent, velocity, totalRoles },
778
+ suggestedAction: {
779
+ parameterType: "yield",
780
+ direction: "increase",
781
+ magnitude: 0.15,
782
+ reasoning: "Low supply per agent with stagnant velocity. Increase yield to compensate."
783
+ },
784
+ confidence: 0.65,
785
+ estimatedLag: 8
786
+ };
787
+ }
756
788
  }
757
789
  if (avgSupplyPerAgent > 2) {
758
790
  return {
@@ -911,28 +943,42 @@ var P8_RegulatorCannotFightDesign = {
911
943
  id: "P8",
912
944
  name: "Regulator Cannot Fight the Design",
913
945
  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) {
946
+ 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.",
947
+ check(metrics, thresholds) {
916
948
  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
- }
949
+ const dominant = Object.entries(roleShares).sort((a, b) => b[1] - a[1])[0];
950
+ if (!dominant) return { violated: false };
951
+ const [dominantRole, dominantShare] = dominant;
952
+ if (dominantShare <= 0.3) return { violated: false };
953
+ const isStructural = thresholds.dominantRoles?.includes(dominantRole) ?? false;
954
+ if (isStructural) {
955
+ return {
956
+ violated: true,
957
+ severity: 3,
958
+ evidence: { role: dominantRole, share: dominantShare, classification: "structural" },
959
+ suggestedAction: {
960
+ parameterType: "rate",
961
+ direction: "set",
962
+ magnitude: 0,
963
+ reasoning: `${dominantRole} dominance (${(dominantShare * 100).toFixed(0)}%) is by design. No intervention.`
964
+ },
965
+ confidence: 0.85
966
+ };
934
967
  }
935
- return { violated: false };
968
+ return {
969
+ violated: true,
970
+ severity: 7,
971
+ evidence: { role: dominantRole, share: dominantShare, classification: "pathological", avgSatisfaction },
972
+ suggestedAction: {
973
+ parameterType: "reward",
974
+ scope: { tags: [dominantRole] },
975
+ direction: "decrease",
976
+ magnitude: 0.1,
977
+ reasoning: `${dominantRole} at ${(dominantShare * 100).toFixed(0)}% is not a designed majority \u2014 apply crowding pressure.`
978
+ },
979
+ confidence: 0.7,
980
+ estimatedLag: 12
981
+ };
936
982
  }
937
983
  };
938
984
  var INCENTIVE_PRINCIPLES = [
@@ -1198,7 +1244,7 @@ var P15_PoolsNeedCapAndDecay = {
1198
1244
  id: "P15",
1199
1245
  name: "Pools Need Cap + Decay",
1200
1246
  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.",
1247
+ 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
1248
  check(metrics, thresholds) {
1203
1249
  const { poolCapPercent } = thresholds;
1204
1250
  for (const [pool, currencyAmounts] of Object.entries(metrics.poolSizesByCurrency)) {
@@ -1231,7 +1277,7 @@ var P16_WithdrawalPenaltyScales = {
1231
1277
  id: "P16",
1232
1278
  name: "Withdrawal Penalty Scales with Lock Duration",
1233
1279
  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.",
1280
+ description: "Depleted pool with estimated high staking signals early-withdrawal abuse. Symptom detector \u2014 penalty formula is developer responsibility.",
1235
1281
  check(metrics, _thresholds) {
1236
1282
  for (const [poolName, currencyAmounts] of Object.entries(metrics.poolSizesByCurrency)) {
1237
1283
  for (const curr of metrics.currencies) {
@@ -1347,7 +1393,7 @@ var P17_GracePeriodBeforeIntervention = {
1347
1393
  id: "P17",
1348
1394
  name: "Grace Period Before Intervention",
1349
1395
  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.",
1396
+ 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
1397
  check(metrics, _thresholds) {
1352
1398
  if (metrics.tick < 30 && metrics.avgSatisfaction < 40) {
1353
1399
  return {
@@ -1668,7 +1714,7 @@ var P27_AdjustmentsNeedCooldowns = {
1668
1714
  id: "P27",
1669
1715
  name: "Adjustments Need Cooldowns",
1670
1716
  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.",
1717
+ description: "High churn + low satisfaction may indicate oscillation from rapid adjustments. Cooldown enforcement is structural (Planner). This is a symptom detector.",
1672
1718
  check(metrics, _thresholds) {
1673
1719
  const { churnRate, avgSatisfaction } = metrics;
1674
1720
  if (churnRate > 0.08 && avgSatisfaction < 50) {
@@ -1725,7 +1771,7 @@ var P38_CommunicationPreventsRevolt = {
1725
1771
  id: "P38",
1726
1772
  name: "Communication Prevents Revolt",
1727
1773
  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.",
1774
+ description: "High churn may indicate unexplained changes. Logging enforcement is structural (DecisionLog). Flags high churn as signal to review recent decisions.",
1729
1775
  check(metrics, _thresholds) {
1730
1776
  const { churnRate } = metrics;
1731
1777
  if (churnRate > 0.1) {
@@ -1750,7 +1796,7 @@ var REGULATOR_PRINCIPLES = [
1750
1796
  P25_CorrectLeversForCorrectProblems,
1751
1797
  P26_ContinuousPressureBeatsThresholdCuts,
1752
1798
  P27_AdjustmentsNeedCooldowns,
1753
- P28_StructuralDominanceIsNotPathological,
1799
+ // P28 merged into P8 (v1.6.7)
1754
1800
  P38_CommunicationPreventsRevolt
1755
1801
  ];
1756
1802
 
@@ -2059,7 +2105,7 @@ var P43_SimulationMinimum = {
2059
2105
  id: "P43",
2060
2106
  name: "Simulation Minimum (100 Iterations)",
2061
2107
  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.",
2108
+ description: "Wild inflation swings (>30%) may indicate insufficient simulation data. Minimum iteration enforcement is structural (Simulator config). Symptom detector.",
2063
2109
  check(metrics, thresholds) {
2064
2110
  const { inflationRate } = metrics;
2065
2111
  if (Math.abs(inflationRate) > 0.3) {
@@ -2226,7 +2272,7 @@ var P49_IdleAssetTax = {
2226
2272
  id: "P49",
2227
2273
  name: "Idle Asset Tax",
2228
2274
  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.',
2275
+ 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
2276
  check(metrics, _thresholds) {
2231
2277
  const { giniCoefficient, top10PctShare, velocity } = metrics;
2232
2278
  if (giniCoefficient > 0.55 && top10PctShare > 0.6 && velocity < 5) {
@@ -2259,7 +2305,7 @@ var P33_FairNotEqual = {
2259
2305
  id: "P33",
2260
2306
  name: "Fair \u2260 Equal",
2261
2307
  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.",
2308
+ 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
2309
  check(metrics, thresholds) {
2264
2310
  for (const curr of metrics.currencies) {
2265
2311
  const giniCoefficient = metrics.giniCoefficientByCurrency[curr] ?? 0;
@@ -2369,7 +2415,7 @@ var P45_TimeBudget = {
2369
2415
  id: "P45",
2370
2416
  name: "Time Budget",
2371
2417
  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.",
2418
+ description: "timeToValue > 30 with satisfaction < 55 suggests excessive time demand. Proxy metric \u2014 does not measure individual available time.",
2373
2419
  check(metrics, thresholds) {
2374
2420
  const { timeToValue, avgSatisfaction } = metrics;
2375
2421
  const timePressure = timeToValue > 30;
@@ -2607,7 +2653,7 @@ var P52_EndowmentEffect = {
2607
2653
  id: "P52",
2608
2654
  name: "Endowment Effect",
2609
2655
  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).",
2656
+ description: "High completion (>90%) + low satisfaction (<60) suggests activities not creating perceived value. May indicate missing endowment effect.",
2611
2657
  check(metrics, _thresholds) {
2612
2658
  const { avgSatisfaction, churnRate } = metrics;
2613
2659
  const { eventCompletionRate } = metrics;
@@ -2680,7 +2726,7 @@ var P54_OperationalCadence = {
2680
2726
  id: "P54",
2681
2727
  name: "Operational Cadence",
2682
2728
  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.",
2729
+ description: "Low velocity (<2) + low satisfaction after tick 100 = supply stagnation. Advisory signal for developer to audit content freshness.",
2684
2730
  check(metrics, _thresholds) {
2685
2731
  const { velocity, avgSatisfaction } = metrics;
2686
2732
  if (velocity < 2 && avgSatisfaction < 55 && metrics.tick > 100) {
@@ -3530,11 +3576,14 @@ var DEFAULT_PERSONA_CONFIG = {
3530
3576
  dormantWindow: 20,
3531
3577
  atRiskDropThreshold: 0.5,
3532
3578
  powerUserMinSystems: 3,
3533
- historyWindow: 50
3579
+ historyWindow: 50,
3580
+ reclassifyInterval: 10
3534
3581
  };
3535
3582
  var PersonaTracker = class {
3536
3583
  constructor(config) {
3537
3584
  this.agents = /* @__PURE__ */ new Map();
3585
+ this.cachedDistribution = {};
3586
+ this.lastClassifiedTick = -Infinity;
3538
3587
  this.config = { ...DEFAULT_PERSONA_CONFIG, ...config };
3539
3588
  }
3540
3589
  /**
@@ -3542,6 +3591,7 @@ var PersonaTracker = class {
3542
3591
  * Call this once per tick BEFORE getDistribution().
3543
3592
  */
3544
3593
  update(state, events) {
3594
+ if (!state.agentBalances) return;
3545
3595
  const tick = state.tick;
3546
3596
  const txByAgent = /* @__PURE__ */ new Map();
3547
3597
  if (events) {
@@ -3597,8 +3647,18 @@ var PersonaTracker = class {
3597
3647
  /**
3598
3648
  * Classify all tracked agents and return the population distribution.
3599
3649
  * Returns { Whale: 0.05, ActiveTrader: 0.18, Passive: 0.42, ... }
3650
+ * Caches results and only reclassifies at `reclassifyInterval` boundaries.
3600
3651
  */
3601
- getDistribution() {
3652
+ getDistribution(currentTick) {
3653
+ const tick = currentTick ?? 0;
3654
+ if (tick - this.lastClassifiedTick < this.config.reclassifyInterval && Object.keys(this.cachedDistribution).length > 0) {
3655
+ return this.cachedDistribution;
3656
+ }
3657
+ this.lastClassifiedTick = tick;
3658
+ this.cachedDistribution = this._classify();
3659
+ return this.cachedDistribution;
3660
+ }
3661
+ _classify() {
3602
3662
  const agentIds = [...this.agents.keys()];
3603
3663
  const total = agentIds.length;
3604
3664
  if (total === 0) return {};
@@ -3904,16 +3964,17 @@ var AgentE = class {
3904
3964
  this.currentTick = currentState.tick;
3905
3965
  const events = this.eventBuffer;
3906
3966
  this.eventBuffer = [];
3967
+ this.personaTracker.update(currentState, events);
3968
+ const personaDist = this.personaTracker.getDistribution(currentState.tick);
3907
3969
  let metrics;
3908
3970
  try {
3909
- metrics = this.observer.compute(currentState, events);
3971
+ metrics = this.observer.compute(currentState, events, personaDist);
3910
3972
  } catch (err) {
3911
3973
  console.error(`[AgentE] Observer.compute() failed at tick ${currentState.tick}:`, err);
3912
3974
  return;
3913
3975
  }
3914
3976
  this.store.record(metrics);
3915
- this.personaTracker.update(currentState, events);
3916
- metrics.personaDistribution = this.personaTracker.getDistribution();
3977
+ metrics.personaDistribution = personaDist;
3917
3978
  const { rolledBack, settled } = await this.executor.checkRollbacks(metrics, this.adapter);
3918
3979
  for (const plan2 of rolledBack) {
3919
3980
  this.planner.recordRolledBack(plan2);