Pacts¶
A pact is a voluntary agreement among a set of players to constrain their harvesting. Pacts are arena-scoped, persistent across turns, and proposed/accepted through the cheap-talk action surface (cheap-talk). Once active, every turn the arena checks whether each party's actual harvest complied with the pact's terms and records any violations.
This page documents the canonical pact lifecycle, term schema, and
violation semantics as implemented in platform/pacts.js; any paper
or notebook citing a different term type is stale.
Lifecycle¶
┌─────────────────┐
│ (no pact) │
└─────────────────┘
│ propose
▼
┌────────────► proposed ──────► rejected
│ │
│ accept │ all parties accepted
│ ▼
└──────────── active ─── checkViolations() every turn
A pact transitions through three states:
| State | Entered when | Behaviour |
|---|---|---|
proposed |
a player calls propose_pact (or pledge) |
not yet binding; awaits acceptance |
active |
every party has accepted (proposer accepts implicitly) | checked against actual harvests |
rejected |
any party calls reject_pact |
terminal; binds no one |
A unilateral pact (single-party — typically via the pledge
shorthand) becomes active on proposal because the proposer's implicit
acceptance covers the only party.
Term schema¶
Terms are a discriminated union on terms.type. v0.9 ships two types;
adding a third is additive (extend TERM_VALIDATORS and
VIOLATION_CHECKERS in platform/pacts.js), never a rewrite.
terms.type |
Required fields | Optional fields | Meaning |
|---|---|---|---|
harvest_cap |
maxHarvest > 0 |
— | each party caps actual at maxHarvest |
no_aggression |
— | tolerance ≥ 0 |
each party stays at or below the group mean (× 1 + tolerance) |
// harvest_cap
{ "type": "harvest_cap", "maxHarvest": 2.5 }
// no_aggression (default tolerance = 0 → strict mean)
{ "type": "no_aggression" }
{ "type": "no_aggression", "tolerance": 0.1 }
The platform exports the live list as a frozen array:
Violation checking¶
After every turn, the arena calls PactRegistry.checkViolations(turnRecord).
For each active pact:
- The checker for
pact.terms.typeis invoked with the party's actual harvest, the terms, and a per-turn context object (currently{ groupMean }). - If the checker returns a non-null
detailstring, an event is appended to the pact'sviolationslog and to the returned event array.
Floating-point slack of \(10^{-6}\) means a harvest numerically equal to the cap (or fair share) is not a breach.
harvest_cap violation¶
Recorded detail: harvested X.XXX > cap Y.YYY.
no_aggression violation¶
Recorded detail: harvested X.XXX > fair share Y.YYY (group mean Z.ZZZ).
Where groupMean is computed once per turn over all decisions in
turnRecord.decisions[]. Parties not playing this turn are skipped.
Information discipline (per-player view)¶
Pacts are not globally observable to players. PactRegistry.viewFor(playerId)
returns only the pacts the player is a party to, partitioned into
active and proposed (with an awaitingYou: bool flag on each
proposal). This is the view threaded into the LLM observation envelope
and into the decide(obs, memory) snapshot for code agents — players
cannot see pacts they are not bound by.
// obs.pacts (player-scoped)
{
"active": [
{ "id": "...", "proposerId": "alice", "parties": ["alice", "bob"],
"terms": { "type": "harvest_cap", "maxHarvest": 2.5 },
"proposedTurn": 3, "acceptedTurn": 4 }
],
"proposed": [
{ "id": "...", "proposerId": "carol", "parties": ["bob", "carol"],
"terms": { "type": "no_aggression" },
"proposedTurn": 7, "awaitingYou": true }
]
}
Persistence¶
Each turn record carries the compact serialised view of the registry
under turnRecord.pacts:
{
"id": "uuid-...",
"proposerId": "alice",
"parties": ["alice", "bob"],
"terms": { "type": "harvest_cap", "maxHarvest": 2.5 },
"status": "active",
"proposedTurn": 3,
"acceptedTurn": 4,
"violationCount": 1
}
The full violations[] array stays on the registry-side Pact object;
the turn record carries only the running count (the detailed log is
exposed to meta-agents and post-hoc analysis through PactRegistry.get()).
Engine-agnostic by construction¶
A pact only ever reads turnRecord.decisions[].actual and player IDs.
It never touches engine internals — neither commons stock, nor Seneca
R/C/P, nor world-level link weights. This is why the same pact
registry works unchanged across logistic, seneca, and any future
engine.
Downstream consumers¶
| Where | What it reads |
|---|---|
sanction / sanction_graduated perturbations |
the violation log to decide who to penalise (see perturbations) |
governance_agent meta-agent |
viewFor(metaId) + violation events to schedule perturbations |
sanction_rate metric |
the count of sanctions fired this turn (see metrics) |
World-level Treaty |
generalises the same lifecycle across nested arenas |
Adding a new pact term type is a single-file change to
platform/pacts.js. Engines, agents, and perturbation modules need
not be modified — this is the engine-agnostic invariant documented in
architecture.