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.
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).
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.
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.
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:
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
baseFractionfor Seneca. The v1.1 S0 sweep selectedbaseFraction = 0.035for a 5-agent \(R_0 = 1.0\) arena (logistic default0.4remains 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.
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.
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:
-
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. -
Whitelist-based context. The sandbox exposes only:
Math,JSON,Number,String,Boolean,Array,Object,parseFloat,parseInt,isNaN,isFinite,Infinity,NaN, and a silencedconsole. No filesystem, network, or Node.js APIs are reachable. -
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
regenRateis 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>}.
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.
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) | — |