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 heremarker. - 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
05ef98eed4dc663e75369a01b6dad86ad18811f3947d290217b9403f119da9b2
|
|
| MD5 |
90396d88a05d3b939db74bdf7c65ccd8
|
|
| BLAKE2b-256 |
4e27db338cb134fadd774b7f0b4ebae7496c452f994aea35662f8684076f958d
|
File details
Details for the file agent_gametheory-0.1.0-py3-none-any.whl.
File metadata
- Download URL: agent_gametheory-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2fafc539ce5abb0af1d6ed9c94bb391bfaab70e36179332d4355b6bad6cb809e
|
|
| MD5 |
9b8d7bde0efce4c433661860f155252a
|
|
| BLAKE2b-256 |
6b2764c85cd085bdd32b0d9b015504f3af52429ba790f09caf61d6d4fabc8d68
|