Skip to content

Ultimatum

Game ID: ultimatum

Two agents (proposer A, responder B) negotiate how to split a resource R (default $100).

Why This Game Matters for Multi-Agent Systems

The ultimatum game models a fundamental coordination problem: how two agents divide shared value when each has private information about their own costs or minimum requirements, and failure to agree means both walk away with nothing. Here are concrete scenarios where this game arises between AI agents:

Revenue Sharing After Joint Task Completion — Two agents collaborated on a task (e.g., one researched, the other synthesized a report) and earned a shared reward from the orchestrator. They must now agree on how to split the payout. Each agent has a private cost for the effort it invested (its reservation value). If they can't agree before the orchestrator's timeout, the reward is forfeited.

Service-Level Agreement Negotiation — An orchestrator agent needs a specialist's capability (translation, code review, data enrichment). They negotiate the terms: how much the orchestrator pays vs. how much surplus the specialist keeps. The orchestrator has a private maximum willingness-to-pay; the specialist has a private minimum acceptable fee. Alternating offers with a deadline — if unresolved, the orchestrator moves on to another provider and both lose the deal.

Resource Partition Between Peer Agents — Two peer agents share a fixed pool of resources (API quota, context window tokens, compute time) allocated to their team by a coordinator. They must negotiate the split. Each has a private minimum requirement below which their own tasks would fail. If they deadlock past the allocation window, the resources go unassigned and both get nothing.

Sub-task Budget Negotiation in Pipelines — A pipeline orchestrator has a fixed budget for a two-stage task and contracts two specialist agents, one per stage. The specialists negotiate how to divide the budget between them: upstream work (data collection) vs. downstream work (analysis). Each has private costs. If they can't agree, the orchestrator cancels the pipeline.

Abstract Scenario

Two agents must divide a shared resource of known total value. Each agent has a private reservation value — the minimum share that makes the deal worthwhile. Agents alternate proposals and responses (accept, reject, counter-offer). On agreement, each agent's payoff is their share minus their reservation value. If no agreement is reached before the deadline, both agents receive nothing.

Rules

  • Each agent has a private reservation value v drawn uniformly from [0, R/2].
  • Agents take turns making offers, accepting, or rejecting.
  • Payoff on agreement: u = x - v, where x is the agent's share.
  • If no agreement is reached within the round limit, both get utility 0.

Actions

Action Payload Description
submit_offer {"my_share": 60} Propose a split: you get my_share, they get total - my_share. Must be between 0 and total.
accept {} Accept the other agent's offer (ends the match). Cannot accept your own offer.
reject {} Reject the current offer and pass the turn.
pass {} Hand the turn to the other agent.
message_only {} Send messages without advancing the turn.

Visible game state

Each agent sees:

{
    "total": 100,
    "current_offer": 60,          # current offer on the table (or None)
    "last_offer_by": "alice",     # who made the current offer
    "my_reservation_value": 12.5, # your private value (opponent cannot see this)
    "action_history": [...]       # full history of offers, accepts, rejects
}

Turn order

The turn order is configurable:

Mode Behavior
ROUND_ROBIN (default) Strict turn order: A, B, C, A, B, C... Out-of-turn actions are rejected.
RANDOM Runner picks a random agent each turn. All agents have is_my_turn=True (no turn enforcement).

Configuration

from neg_env import UltimatumGame
from neg_env.spec import TurnOrder

# Defaults
game = UltimatumGame()  # total=100, max_rounds=10

# Custom
game = UltimatumGame(total=200, max_rounds=20)

# Fixed reservation values (for controlled experiments)
game = UltimatumGame(
    total=100,
    reservation_values={"alice": 10, "bob": 30},
)

# Random turn order
game = UltimatumGame(total=100, turn_order=TurnOrder.RANDOM)
Parameter Type Default Description
total int 100 Total resource to split.
max_rounds int 10 Maximum rounds before timeout (everyone gets 0).
reservation_max float \| None None Max reservation value. If None, defaults to total.
reservation_values dict[str, float] \| None None Fixed reservation values per agent. If None, drawn uniformly from [0, reservation_max].
turn_order TurnOrder ROUND_ROBIN Turn order mode: ROUND_ROBIN or RANDOM.

Outcome format

{
    "payoffs": [
        {"agent_id": "alice", "share": 60, "utility": 47.5},
        {"agent_id": "bob", "share": 40, "utility": 27.5},
    ],
    "reason": "agreement",        # or "max_rounds_exceeded"
    "split": [60, 40],
}

Prompts

Two built-in system prompts for LLM agents:

  • SYSTEM_PROMPT_FAIR — cooperative: aims for a fair split, prefers deals over deadlock.
  • SYSTEM_PROMPT_UNFAIR — strategic: maximizes payoff, uses anchoring, calibrated concessions, deadline pressure.
from neg_env.prompts import SYSTEM_PROMPT_FAIR, SYSTEM_PROMPT_UNFAIR