Skip to main content

A light game-theory layer for multi-agent systems: agents publish action events, the library reduces them into outcomes.

Project description

gametheory

A light game-theory layer for multi-agent systems. Agents publish action events; the library reduces them into outcome events using a payoff function you supply. It sits on top of LangGraph / CrewAI / AutoGen / anything else without importing them, and it never owns the event loop — you drive it.

  • Zero dependencies. The core is pure stdlib; framework adapters are optional extras.
  • You drive, it reduces. No background threads, no started loops. Synchronous.
  • Pure scoring, impure driving. Payoff and resolution take data and return data; only the transport does I/O.
  • Everything is scoped to a Session. No module globals — run many concurrent games in one process.
  • A session is a fold over its event log. Given the log, replay() deterministically reproduces the outcomes.

Install

pip install agent-gametheory

The distribution is named agent-gametheory on PyPI, but the import is simply import gametheory. For development:

git clone https://github.com/ravikadam/gametheory.git
cd gametheory
pip install -e ".[dev]"     # library + pytest
pytest                      # run the test suite

Quickstart

from gametheory import Game, Session, InMemoryTransport

pd = Game(players=["alice", "bob"], actions=["cooperate", "defect"])

@pd.payoff
def score(actions):
    table = {("cooperate", "cooperate"): (3, 3), ("cooperate", "defect"): (0, 5),
             ("defect", "cooperate"): (5, 0), ("defect", "defect"): (1, 1)}
    a, b = table[(actions["alice"], actions["bob"])]
    return {"alice": a, "bob": b}

s = Session(pd, transport=InMemoryTransport())
s.submit("alice", "cooperate", scope="sealed")   # sealed: simultaneous moves
s.submit("bob", "defect", scope="sealed")
outcome = s.resolve()
print(outcome.payoffs)          # {'alice': 0, 'bob': 5}

A five-minute game theory primer

You don't need a game theory background to use this library — just these ideas, each of which maps to one knob in the API.

A game is players, actions, and payoffs. Some agents (players) each pick an action, and a payoff function turns the combination of actions into a score for everyone. That's the whole formal object, and it's exactly what Game(players=..., actions=...) plus @game.payoff declares. The interesting part is that each player's score depends on what others chose — that interdependence is what makes it a game rather than N separate optimizations.

Simultaneous vs. sequential is an information question. In the prisoner's dilemma or a sealed-bid auction, players move "at the same time" — which really means nobody sees the others' choices before committing. In negotiation or chess, moves are sequential: you see the last move before making yours. In this library that's not two different engines, it's one knob: submit actions with scope="sealed" and other players can't observe them until the round resolves (simultaneous); submit with scope="public" and the next player to act sees them (sequential).

A dominant strategy is a choice that's best no matter what others do. In the prisoner's dilemma, defecting is dominant — and both players defecting leaves both worse off than mutual cooperation. That gap between individually-rational and collectively-good is the classic reason multi-agent systems need coordination mechanisms, not just smarter agents.

Repeated games change behavior. Play the same game over multiple rounds (rounds=5) and history starts to matter: strategies like tit-for-tat can sustain cooperation because today's defection can be punished tomorrow. The Observation handed to every strategy carries the full outcome history for exactly this reason.

Mechanism design is game theory run backwards. Instead of asking "what will players do in this game?", you ask "what game should I put my agents in so that selfish play produces the outcome I want?" The auction example is the canonical case: a second-price (Vickrey) auction awards the job to the best bid but sets the price by the runner-up's bid. Since your bid only decides whether you win — never the price you're paid — bidding your true cost is a dominant strategy. You get honest agents without asking anyone to be honest. Payoffs express such transfers with no special API: negative numbers for payers, positive for payees.

Concepts

Game is a pure spec: players, an action space (an enum list or a (player, action) -> bool validator), a resolution rule, and a payoff function. resolves_when is "all_acted", "any_acted", "timeout" (pass deadline= to resolve), or your own predicate over the standing actions. rounds > 1 enables repeated play.

Session drives one play-through:

s.submit(player, action, scope="public")  # validate + emit an ACTION event
s.resolve()          # -> Outcome when resolves_when holds (else raises NotResolved)
s.step()             # ask each pending player's Strategy, then try to resolve
s.run()              # loop step() for game.rounds rounds -> list[Outcome]
s.history            # accumulated outcomes
s.utilities          # running payoff sum per player

Scopes are the information-visibility knob on every action event:

scope other players see it revealed later?
public immediately
sealed not until the round resolves yes, in the outcome
private never no — referee/payoff only

Strategies are plain callables registered per player; step() hands each one an Observation: {"round", "history", "visible_actions", "self"}, filtered by the scope rules above.

Replay rebuilds outcomes from an event log with no transport or strategy calls:

from gametheory import replay
assert replay(s.transport.events) == s.history

Exceptions (InvalidAction, NotResolved, GameError) live in gametheory.types.

Using it in a multi-agent system

The integration contract is deliberately tiny: your agents compute actions however they like; the game only sees session.submit(player, action, scope). The library never calls your framework, never blocks, and never spins a loop, so it composes with any orchestrator that can call a function.

Pattern 1 — LLM agents as strategies

A Strategy is just Observation -> Action. Replace a rule with a model call and the game doesn't know the difference:

@game.strategy("buyer", scope="public")
def buyer(obs):
    # obs = {"round", "history", "visible_actions", "self"} — everything the
    # agent is *allowed* to know, with sealed/private info already filtered.
    prompt = f"Round {obs['round']}. Standing offers: {obs['visible_actions']}. ..."
    return parse_action(llm.invoke(prompt))   # e.g. an Anthropic/OpenAI call

session.run()   # the library asks each strategy in turn, then scores

Because the Observation is built from scope rules, you can hand it straight into a prompt without leaking sealed bids or private signals — the information structure of the game is enforced before the model ever sees it.

Pattern 2 — external agents drive, the session referees

When agents live in LangGraph, CrewAI, or separate services, don't register strategies at all. Let your orchestrator run the agents and treat the session as a referee that validates, hides, resolves, and scores:

# inside each agent's node/task, whenever it decides:
session.submit("worker-3", bid, scope="sealed")

# in your orchestrator, when the round should close:
outcome = session.resolve()          # or session.step() on a schedule
broadcast(outcome.payoffs)           # feed results back to the agents

The langgraph extra ships a thin adapter (gametheory.adapters.langgraph) that wraps players as graph nodes and resolution as a terminal node — but it's sugar; the two calls above are the whole integration.

Pattern 3 — the event log as the system of record

Every submit/resolve is a frozen Event published to the Transport. Subscribe to stream them into your own bus, UI, or store — and audit any game after the fact:

transport.subscribe(lambda ev: log.info("%s r%d %s: %s", ev.kind, ev.round, ev.player, ev.payload))
assert replay(transport.events) == session.history   # deterministic audit

This is where post-hoc analysis belongs (win rates, efficiency, whether your mechanism actually induced honesty) — over the log, outside the library.

Which pattern when?

  • Prototyping strategies / simulations → Pattern 1 (step()/run()).
  • Real agents with their own runtimes → Pattern 2 (submit()/resolve()).
  • Either way, keep Pattern 3 on for observability and replayable audits.

Examples

python examples/prisoners_dilemma.py   # simultaneous play via sealed scope
python examples/negotiation.py         # alternating offers over a 100 surplus
python examples/compute_auction.py     # sealed-bid reverse Vickrey with transfers
  • prisoners_dilemma.py — repeated play, tit-for-tat vs. always-defect, sealed simultaneous moves.
  • negotiation.py — buyer and seller split a surplus of 100 over up to 6 alternating-offer rounds; public offers, custom resolution predicate, breakdown cost when talks fail. Each strategy is a plain function with a # swap for an LLM strategy here marker.
  • compute_auction.py — N workers bid to run a batch job; the scheduler runs a sealed-bid second-price auction. Shows transfers (winner paid, scheduler pays) and demonstrates that overbidding loses the job while lowballing wins it at a loss.

Out of scope

Equilibrium solvers, learning algorithms, persistence, async, CLIs. Those are analyses you run over the event log afterwards — the log is designed to make that easy.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agent_gametheory-0.1.0.tar.gz (15.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agent_gametheory-0.1.0-py3-none-any.whl (13.4 kB view details)

Uploaded Python 3

File details

Details for the file agent_gametheory-0.1.0.tar.gz.

File metadata

  • Download URL: agent_gametheory-0.1.0.tar.gz
  • Upload date:
  • Size: 15.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for agent_gametheory-0.1.0.tar.gz
Algorithm Hash digest
SHA256 05ef98eed4dc663e75369a01b6dad86ad18811f3947d290217b9403f119da9b2
MD5 90396d88a05d3b939db74bdf7c65ccd8
BLAKE2b-256 4e27db338cb134fadd774b7f0b4ebae7496c452f994aea35662f8684076f958d

See more details on using hashes here.

File details

Details for the file agent_gametheory-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for agent_gametheory-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2fafc539ce5abb0af1d6ed9c94bb391bfaab70e36179332d4355b6bad6cb809e
MD5 9b8d7bde0efce4c433661860f155252a
BLAKE2b-256 6b2764c85cd085bdd32b0d9b015504f3af52429ba790f09caf61d6d4fabc8d68

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page