Skip to content

Governance Meta-Agents

A governance meta-agent is a non-playing observer that watches an arena (C3.a) or an entire World (C3.b) and emits perturbation specs in response to what it sees. Meta-agents do not harvest, do not have a decide() loop, and are invisible to the per-arena scheduler. Each turn (C3.a) or each global round (C3.b), the arena or World calls

await agent.observe(state, turnRecord, arena | world)

and the agent returns an array of perturbation specs. The specs are routed through the standard applyPerturbation() entry, so existing perturbation types (sanction, wealth_shock, exclude, …) are available without extra plumbing.

This page documents the registry, the two scopes (C3.a vs C3.b), and the four concrete strategies shipped in v1.0.


The registry

import { GOVERNANCE_REGISTRY } from './agents/governance.js';
Key Class Scope
noop GovernanceAgent (base) C3.a / C3.b probe
sanctioner SanctionerAgent C3.a — arena-level
voluntary_sanctioner VoluntarySanctionerAgent C3.a — arena-level
world_sanctioner WorldSanctionerAgent C3.b — world-level
treaty_enforcer TreatyEnforcerAgent C3.b — world-level
pollution_regulator PollutionRegulatorAgent S3 — world-level (Seneca)

Adding a new strategy is a 30–80 line operation:

  1. Extend GovernanceAgent, override observe().
  2. Register in GOVERNANCE_REGISTRY.
  3. Add a doc entry here.

No changes to Arena, World, or the perturbation engine are required. This is the same engine-agnostic pattern as the engine plugin registry — the substrate stays out of the strategy layer.


Two scopes: C3.a vs C3.b

Aspect C3.a (arena-level) C3.b (world-level)
Attached to One Arena One World
observe() arguments (state, turnRecord, arena) (snapshot, roundResult, world)
Invoked Once per arena turn, after the harvest loop Once per global round, after link flows settle
Perturbation routing Arena-local (no target) target: { arenaIds: [aid] } per spec
Wired through Arena.addAgent({ role: 'meta', strategy }) World.addMetaAgent({ strategy })
HTTP variant in v1.0 No (builtin only) No (builtin only)

The split is orthogonal to the agent type: a C3.a Sanctioner and a C3.b WorldSanctioner can coexist on the same federation — one watches a single arena's pact violations, the other rolls across all of them.


C3.a — sanctioner

Reflexive arena-level enforcer. Applies a fixed-amount monetary sanction to every pact violator detected this turn. Mirrors the behaviour of the legacy pact_violation-triggered sanction perturbation from v0.9 C1, but routed through the agent channel so that richer policies (next entry) can swap in without touching the schedule layer.

arena.addAgent({
  id:           'meta-1',
  role:         'meta',
  agentType:    'builtin',
  strategy:     'sanctioner',
  strategyParams: { amount: 3 },
});
Param Default Meaning
amount 3 Flat sanction per violation

Each emitted spec:

{ type: 'sanction',
  payload: { playerId, amount, reason: 'meta_sanction(<termType>)' } }


C3.a — voluntary_sanctioner

Discretion-based arena-level enforcer. Unlike sanctioner (fires on every violation), this agent decides whether to sanction based on global commons state and per-party history. Closes the design-doc "Yoon-style voluntary sanctioning" feature note.

Two complementary gates — both must hold for a violation to be sanctioned:

Param Default Meaning
amount 3 Flat sanction
triggerStockRatio 0.5 Only fire when stock / capacity < ratio. Agent "waits and watches" while commons is healthy
repeatThreshold 0 Minimum number of prior violations by the offending party before sanctioning kicks in

The prior-violation counter is per-party, kept in _priorViols for the lifetime of the agent instance. Counted at observe() time and incremented after the gate decision, so the first violation triggers iff repeatThreshold == 0.

Mapping to Yoon's "voluntary" axis. The meta-agent doesn't pay a monetary cost (it has no wealth), but it does exercise discretion, which is the operational signal the design doc asked for. Cost-bearing voluntary sanctioning by a peer player belongs to a future role-mixing variant.

Reason string format:

voluntary_sanction(<termType>,ratio=<r>,prior=<n>)


C3.b — world_sanctioner

World-level mirror of sanctioner. A single instance covers an entire federation: each global round it scans every arena that ticked this round, finds the pact violations in the most recent turn, and emits one sanction spec per violation, targeted via { arenaIds: [aid] }.

world.addMetaAgent({
  strategy:     'world_sanctioner',
  strategyParams: { amount: 3 },
  displayName:  'enforcer',
});
Param Default Meaning
amount 3 Flat sanction per violation

The "ticked this round" filter (based on roundResult.ticks) keeps behaviour correct when an arena auto-closes during its current round: the final violations are still in its history and deserve a sanction.

Reason string format: world_sanction(<termType>).


C3.b — treaty_enforcer

World-level enforcer that monitors one named Treaty across its signatory arenas. The v1.0 enforcer recognises a single payload type:

treaty.payload = {
  type:             'harvest_cap_per_arena_per_round',
  perRoundPerArena: 8.0,
}

Each global round, for every signatory arena that ticked:

  1. Sum decisions[].actual across the players in the last turn record → totalHarvest.
  2. If totalHarvest > perRoundPerArena, emit a wealth_shock targeting all members of the offending arena.
world.addMetaAgent({
  strategy: 'treaty_enforcer',
  strategyParams: {
    treatyName:     'cap_8',
    sanctionFactor: 0.9,
    sanctionDelta:  null,
  },
});
Param Default Meaning
treatyName required Treaty.name to monitor
sanctionFactor 0.9 Wealth multiplier on every player of the offending arena
sanctionDelta null Optional flat wealth delta applied alongside the factor

Collective sanction semantics: the wealth shock hits the entire community of the offending arena, not the individual high harvesters. The signatory community failed to police itself; the enforcement is at the membership level.

Reason string format:

treaty_breach(<treatyName>: <totalHarvest> > <cap>)


Strategies in combination

C3.a and C3.b are independent. A typical v1.0 scenario combines:

Layer Role
C3.a voluntary_sanctioner Catch single-arena pact violations with discretion
C3.b treaty_enforcer Catch arena-collective breaches of a cross-arena cap

The two layers act on different decision shapes and never overlap. The treaty enforcer's wealth_shock and the C3.a sanctioner's sanction are both routed through the same perturbation engine, so the audit log is unified — every meta-action carries a reason string identifying its source.


What governance meta-agents are not

  • They are not players. No harvest, no wealth, no decide(), invisible to the scheduler.
  • They are not engines. They never branch on engineMode; the same governance code runs over logistic and Seneca commons unchanged.
  • They are not pacts. Pacts are signed by players and enforced by meta-agents; the two layers are orthogonal.
  • They are not the only way to enforce. Direct schedule-injected sanction perturbations remain available for scripted scenarios; the agent channel is the policy channel.

See also

  • World — the layer that hosts C3.b meta-agents.
  • Treaties — the cross-arena constraints that treaty_enforcer reads.
  • Perturbationssanction, wealth_shock, exclude specs emitted by meta-agents.
  • Agents — overview of the four agent types and the role: 'meta' flag.