Skip to content

Agent Types

The BDPD Platform supports four player agent interfaces (built-in, sandboxed code, HTTP, LLM) and one meta agent interface (governance strategies; see Governance). Player agents harvest and are scheduled like any other; meta-agents observe and emit perturbations, with no harvest of their own.


Built-in Heuristics

Five deterministic strategies are available as reference baselines. All share the same decide(observation) → harvest interface and use wealth-scaled capacity \(\text{capacity}(w) = 1 + 0.05 \times w\).

Aggressive

Maximises extraction every turn. Ignores commons health.

\[h = \min\!\left(\text{capacity}(w_{i,t}),\; \text{intensity} \times S_t^{\text{obs}}\right)\]

The harvest is clamped to capacity: an aggressor cannot request more than its capacity allows even when the observed stock is large. The intensity parameter (default 0.5) scales the requested fraction.

intensity ∈ (0, 1] — fraction of stock requested per turn (default 0.5).

{ "agentType": "builtin", "strategy": "aggressive",
  "strategyParams": { "intensity": 0.3 } }

At intensity = 0.05, the aggressor no longer dooms the commons (B1 experiment). At the canonical default of 0.5, the step-collapse is nearly an order of magnitude above the cliff edge.

Conservative

Scales harvest with the commons ratio: extracts less as the resource declines.

\[h = \text{capacity}(w_{i,t}) \times f(\text{ratio})\]

where \(f(\text{ratio})\) is 0.60 above 0.6, 0.35 above 0.4, and 0.10 below.

In Seneca mode, if pollution is visible and rising, the conservative agent additionally cuts harvest by 50% — an extra precaution against the cliff.

Adaptive

Tracks the resource trend over the last two observations. Reacts to falling stock by reducing harvest, and to rising stock by restoring it.

\[h = \text{capacity}(w_{i,t}) \times f(\text{trend})\]

reductionFactor ∈ [0, ∞) — scales the magnitude of the adaptive cut (default 1.0 = canonical P8 heuristic).

\(f\) Behaviour
0.0 No adaptive response (fixed conservative at 0.40)
1.0 Canonical heuristic — pre-supplement behaviour
>1.0 Over-reactive adaptation (clipped to non-negative)

Welfare declines monotonically with \(f\) at all tested regen rates (B2).

In Seneca mode, if capital stock \(C\) is visible and declining, the adaptive agent multiplies the post-trend fraction by 0.6 (i.e. a further 40% cut), gated to fire only while the running fraction is above 0.15 so the rule does not push harvest into the noise floor. Capital is a leading indicator of the Seneca cliff that the resource stock alone does not show.

RCP — Resources / Capital / Pollution (≥ v0.4.0)

Explicitly models the Bardi (2011) three-variable mechanism (Resources, Capital, Pollution). Uses the pollution signal (visible or estimated) to anticipate the Seneca cliff before resources visibly decline. When pollution is hidden, the agent maintains an internal estimate based on capital decline trends. Registry key: rcp. (Earlier versions of the codebase named this strategy seneca_aware / SenecaAwareAgent; the identifier rcp is now canonical.)

Harvest rule. Two cuts compose, with a final resource-ratio gate:

\[ \begin{aligned} \text{pollutionPressure} &= \min(1,\; P \times 5) \\ \text{frac} &= b_0 \cdot (1 - \text{pollutionPressure}) \\ \text{frac}_R &= \begin{cases} \text{frac} & \text{if } S_t / K > 0.5 \\ \text{frac} \times 0.5 & \text{otherwise} \end{cases} \\ h &= \min\bigl(\text{capacity}(w) \cdot \text{frac}_R,\; S_t \cdot \text{frac}_R\bigr) \end{aligned} \]

where \(b_0\) = baseFraction is configurable via strategyParams (default 0.4 for the logistic-engine demos; canonical Seneca baseline = 0.035 for a 5-agent \(R_0 = 1.0\) arena, calibrated in S0 so the cohort completes a 25–30-turn game without premature resource exhaustion). The resource-ratio gate halves the harvest once the stock falls below half-capacity, layering a stock-restraint on top of the pollution one.

Hidden pollution estimate. When pollutionLevel is not observable, the agent maintains an internal estimate updated from the capital trend: each turn where \(\Delta C < 0\) adds \(2 \lvert \Delta C \rvert\); each turn where \(C\) rises decays the estimate by 10%. The estimate substitutes for the observed \(P\) in the formula above.

This agent "knows" the model structure — it reasons about \(P\) even when \(P\) is hidden, making it the most sophisticated built-in heuristic for Seneca mode experiments.

Canonical baseFraction for Seneca. The v1.1 S0 sweep selected baseFraction = 0.035 for a 5-agent \(R_0 = 1.0\) arena (logistic default 0.4 remains unchanged). The sweep methodology, pick rule, and population-dependence caveats live in Seneca calibration.

Random

Uniform random fraction of capacity. Used as a baseline for statistical comparison.

\[h = \min\!\left(\text{capacity}(w_{i,t}) \times U(0,1),\; S_t^{\text{obs}}\right)\]

A single uniform draw scales capacity; the result is then clamped by the available stock so the agent cannot request more than is observable.


Sandboxed Code Agents (≥ v0.4.0)

Code agents allow users to submit a JavaScript function directly via the API. The function is executed in a sandboxed environment with a 50ms wall-clock timeout, no access to the file system, network, or global Node.js APIs.

function decide(obs, memory) {
  // obs:    the observability-filtered state (object)
  // memory: a persistent private object, survives across turns
  // return: a number (the requested harvest)
}
curl -X POST "http://localhost:3000/api/arenas/${ARENA_ID}/join" \
  -H "Content-Type: application/json" \
  -H "X-Arena-Key: ${ARENA_KEY}" \
  -d '{
    "agentType":     "code",
    "displayName":   "TrendConservative",
    "strategyCode":  "function decide(obs, memory) { ... }"
  }'

The memory object is the key feature: it is a plain JavaScript object that persists across turns, allowing the agent to maintain private state — trend history, running averages, turn counters, or any other information. It is not shared with other agents and is not exposed through any API endpoint.

Sandbox Security Model

Code agents run in vm.runInContext (platform/sandbox.js) with three layers of defence:

  1. Static pre-check. Before compilation, the source string is scanned for forbidden identifiers: process, require, import, fetch, eval, Function, setTimeout, globalThis, SharedArrayBuffer, WebAssembly, and others. A match rejects the code before it ever runs.

  2. Whitelist-based context. The sandbox exposes only: Math, JSON, Number, String, Boolean, Array, Object, parseFloat, parseInt, isNaN, isFinite, Infinity, NaN, and a silenced console. No filesystem, network, or Node.js APIs are reachable.

  3. Execution timeout. Each decide() call is capped at 50 ms wall-clock. Infinite loops or CPU-intensive computations are killed by the VM timeout.

On any failure (compile error, runtime error, invalid return value, timeout), the agent falls back to 0.2 × capacity(w) — the same safe fallback used by HTTP agents on timeout.

Not a hard security boundary

vm.runInContext does not prevent indirect prototype access or CPU-starvation attacks beyond the timeout. This is acceptable for a research platform with trusted participants. For untrusted code, replace with a Worker thread in a separate process with restricted capabilities.

Worked Example: Trend-Aware Conservative

This agent tracks the commons stock over a 5-turn window, estimates the depletion trend via OLS, and modulates harvest accordingly. It also implements two BDPD-specific heuristics:

  • Regeneration estimation. Since regenRate is hidden by default, the agent estimates it using the logistic formula with assumed \(K = 150\) and \(r = 0.12\).
  • Vacuum Effect avoidance. The agent never harvests exactly 0 — a zero-harvest by exactly one player transfers +1 cedar from the reserve to the richest player. A floor of 0.1 avoids subsidising the wealthiest competitor.
function decide(obs, memory) {
  if (!memory.stockHistory)   memory.stockHistory   = [];
  if (!memory.harvestHistory) memory.harvestHistory = [];
  if (memory.turn === undefined) memory.turn = 0;
  memory.turn += 1;

  const stock    = obs.commonsStock   ?? 100;
  const capacity = obs.myCapacity     ?? 1.0;
  const nPlayers = obs.nPlayers       ?? 4;
  memory.stockHistory.push(stock);

  const WINDOW = 5;
  if (memory.stockHistory.length > WINDOW) {
    memory.stockHistory.shift();
  }

  let trend = 0;
  const hist = memory.stockHistory;
  if (hist.length >= 2) {
    const n    = hist.length;
    const xBar = (n - 1) / 2;
    const yBar = hist.reduce((a, b) => a + b, 0) / n;
    let num = 0, den = 0;
    for (let i = 0; i < n; i++) {
      num += (i - xBar) * (hist[i] - yBar);
      den += (i - xBar) ** 2;
    }
    trend = den > 0 ? num / den : 0;
  }

  const K          = 150;
  const regenEst   = 0.12 * stock * (1 - stock / K);
  const myShareSus = regenEst / nPlayers;
  let harvest = Math.min(myShareSus, capacity);

  if (trend < -2) {
    const reductionFactor = Math.max(0.3, 1 + trend / stock);
    harvest *= reductionFactor;
  }

  harvest = Math.max(0.1, harvest);
  memory.harvestHistory.push(parseFloat(harvest.toFixed(3)));
  memory.lastTrend = parseFloat(trend.toFixed(3));
  return harvest;
}

HTTP Agents

HTTP agents receive the full (observability-filtered) state as a JSON POST to a configurable callback URL and must respond within agentTimeout milliseconds (default 30s) with {"harvest": <number>}.

curl -X POST "http://localhost:3000/api/arenas/${ARENA_ID}/join" \
  -H "Content-Type: application/json" \
  -H "X-Arena-Key: ${ARENA_KEY}" \
  -d '{
    "agentType":   "http",
    "displayName": "MyRLAgent",
    "callbackUrl": "http://localhost:5001/decide"
  }'

On timeout or network error, the agent falls back to \(0.2 \times \text{capacity}(w_{i,t})\).

This interface supports any external decision process — reinforcement learning models, language models via bdpd_agent.py, or custom heuristics in any language.


LLM Agents (via bdpd_agent.py)

The LLM agent bridge (agents/bdpd_agent.py) is a Flask server that wraps a language model (local llama-server or OpenAI-compatible API) as a BDPD HTTP agent.

Platform (Node.js)  →  POST /decide  →  bdpd_agent.py (Flask)  →  LLM API

The agent receives observability-filtered state, maintains conversation history, applies archetype-specific nudges, and enforces a fallback harvest on timeout. See LLM Agents for architecture, setup, and prompt design.


Governance Agents (≥ v1.0)

Governance agents are meta-players: they do not harvest, do not participate in the scheduler, and have no wealth. After each turn's harvest loop, the arena calls their observe(state, turnRecord, arena) method. The agent returns an array of perturbation specs (reusing the standard perturbation types — sanction, wealth_shock, regen_set, capital_shock) that are applied immediately.

Governance agents are joined with role: "meta" and a strategy key that selects from the governance registry (agents/governance.js).

Registry

Strategy Scope Behaviour
noop arena No-op observer — useful as a probe
sanctioner arena Applies a flat monetary sanction to every pact violator this turn
voluntary_sanctioner arena Discretionary sanctioning: fires only when commons ratio < triggerStockRatio AND the offender has ≥ repeatThreshold prior violations
world_sanctioner world Like sanctioner but covers all arenas in the world federation
treaty_enforcer world Monitors a named treaty (harvest_cap_per_arena_per_round); emits collective wealth_shock against offending arenas
pollution_regulator world Pollution-aware regulator (v1.1 S3): three levers (cap / levy / fine) × two triggers (pollution / capital)

Join Example

curl -X POST "http://localhost:3000/api/arenas/${ARENA_ID}/join" \
  -H "Content-Type: application/json" \
  -H "X-Arena-Key: ${ARENA_KEY}" \
  -d '{
    "agentType": "builtin",
    "role": "meta",
    "strategy": "sanctioner",
    "strategyParams": { "amount": 5 }
  }'

strategyParams Reference

sanctioner / world_sanctioner:

Param Default Description
amount 3 Flat sanction per violation

voluntary_sanctioner:

Param Default Description
amount 3 Flat sanction per violation
triggerStockRatio 0.5 Commons ratio below which sanctioning activates
repeatThreshold 0 Minimum prior violations before sanctioning a party

treaty_enforcer:

Param Default Description
treatyName (required) Name of the treaty to monitor
sanctionFactor 0.9 Wealth multiplier on offending arena
sanctionDelta null Optional flat wealth delta

pollution_regulator:

Param Default Description
mode "cap" "cap" (clamp k1), "levy" (capital shock), "fine" (wealth shock)
triggerOn "pollution" "pollution" (lagging) or "capital" (leading)
threshold 0.15 P level that arms the lever (when triggerOn = "pollution")
capitalThreshold 0.15 C level that arms the lever (when triggerOn = "capital")
emitterArenaIds all Array of arena IDs to police (world-level only)
throttledK1 0.02 k1 clamp value (cap mode)
shockFactor 0.5 Multiplier per round (levy/fine mode)

Strategy Summary

Agent Type Interface State Fallback
Built-in aggressive decide(obs) → h Stateless
Built-in conservative decide(obs) → h Stateless
Built-in adaptive decide(obs) → h 2-turn memory
Built-in rcp decide(obs) → h Internal P estimate
Built-in random decide(obs) → h Stateless
Code (sandbox) decide(obs, memory) → h Private persistent object min(0.2 × capacity, 0.1 × commons stock)
HTTP POST callback External min(0.2 × capacity, 0.1 × commons stock)
LLM via bdpd_agent.py Conversation history min(0.2 × capacity, 0.1 × commons stock)
Meta sanctioner (C3.a) observe() per turn Stateless
Meta voluntary_sanctioner (C3.a) observe() per turn Per-party violation counter
Meta world_sanctioner (C3.b) observe() per global round Stateless
Meta treaty_enforcer (C3.b) observe() per global round Stateless
Meta pollution_regulator (C3.b) observe() per global round Stateless (reads engine extraObservations)