What goes wrong without a referee
Transport is solved: with MCP and A2A, agents can find each other and exchange messages. What those standards leave open is the adversarial case. When your agent and my agent negotiate a delivery window, a contract clause or a shared budget, three failure modes follow.
- 01
An agent can be argued out of its owner's constraint.
If the constraint lives in a prompt, the other side's agent only has to be more persuasive than the instruction. The owner finds out afterwards, if at all.
- 02
Whoever runs the room sees everyone's cards.
A coordinator that collects full preference sheets to compute an outcome can also sell them, or steer toward the option that suits it.
- 03
Nobody can check the result without seeing the private inputs.
An audit that requires every party to disclose its position is one no party with a real position will agree to.
Three properties, each enforced by a function you can read
Parley closes each failure mode with a property of the program, not a property of the model. The pieces are small: four short files in a zero-dependency package, each readable in one sitting.
1. Deterministic red lines
A hard constraint is a HardConstraint(name, predicate).PreferenceSheet.evaluate marks an option feasible only when every predicate returns True. A crossed red line rejects the option outright; no score, however high, can buy it back.
preferences.pyoption: rooftop, cost 900
Ana → acceptable, score 1.0
Bob → red-line
Cara → acceptable, score 0.5
rooftop is out, regardless of Ana's 1.0
2. Max-min consensus over masked verdicts
The only thing that leaves an agent is a Verdict: acceptable or not, a soft score, and a reason that is exactly"ok" or "red-line". The coordinator never sees the sheet or which constraint fired.run_consensus keeps the options every agent accepted, then picks the one with the highest minimum score, ties broken by the highest total. No candidate means an honest deadlock.
agent.py · consensus.pyleaves each agent:
acceptable · score 0.4 · reason "ok"
rule over feasible options:
max(min score), then max(total score)
no feasible option: status "deadlock"
3. A transcript each owner verifies alone
Transcript.hash() is a SHA-256 over the canonical JSON of every masked verdict and the result, so any later edit changes the digest. Two replays run over it. verify_non_betrayalre-evaluates your own private sheet against the decision, on your machine, revealing nothing. verify_outcome recomputes the max-min winner from the recorded verdicts, so a coordinator that announces some other option is caught by anyone holding the record.
transcript.py · consensus.pytranscript.hash()
→ a9cb75c0a15b…cc2fc72
transcript.verify_non_betrayal(my_sheet, decision)
→ True
verify_outcome(transcript, expected_owners=roster)
→ True
The whole thing in eleven lines
Three owners pick a venue. Ana wants the rooftop. Bob will not spend more than 700, written as a predicate rather than a preference. Cara likes the garden.
from parley.agent import Agent
from parley.consensus import run_consensus
from parley.preferences import HardConstraint, PreferenceSheet
options = [{"venue": "rooftop", "cost": 900}, {"venue": "garden", "cost": 600}, {"venue": "diner", "cost": 300}]
ana = PreferenceSheet("Ana", utility=lambda o: 1.0 if o["venue"] == "rooftop" else 0.4)
bob = PreferenceSheet("Bob", hard=[HardConstraint("budget", lambda o: o["cost"] <= 700)],
utility=lambda o: 1 - o["cost"] / 1000)
cara = PreferenceSheet("Cara", utility=lambda o: 0.9 if o["venue"] == "garden" else 0.5)
result = run_consensus([Agent(s.owner, s) for s in (ana, bob, cara)], options)
print(result.status, result.decision)
for sheet in (ana, bob, cara):
print(sheet.owner, "not betrayed:", result.transcript.verify_non_betrayal(sheet, result.decision))
agreed {'venue': 'garden', 'cost': 600}
Ana not betrayed: True
Bob not betrayed: True
Cara not betrayed: True
The rooftop is Ana's favourite and loses anyway: it crosses Bob's budget line, so it is rejected before any score is weighed. The coordinator only ever sawred-line, never the number 700 or the sheet it came from. Between the remaining two, the floors tie: Ana scores both the garden and the diner 0.4. The tie-break is total welfare, and the garden's 0.4 + 0.4 + 0.9 = 1.7 beats the diner's 0.4 + 0.7 + 0.5 = 1.6, so max-min picks the garden.
How this differs from the tools you may already use
Single-owner orchestration coordinates workers that share a goal; it has no concept of a party whose loss is another party's gain. On-chain agent commerce handles a buyer and a seller and anchors trust in a chain and a token. Parley is multilateral, needs no chain, and anchors trust in predicates you can read and a replay you run yourself.
| Parley | Single-owner orchestration (AutoGen, CrewAI) | On-chain agent commerce (Fetch.ai, Olas) |
|---|
| Whose interests | Several rival owners | One principal, many workers | Two counterparties |
|---|
| Parties per decision | N, multilateral | N workers, one goal | Typically bilateral |
|---|
| Trust anchor | Code predicates and a local replay | The orchestrator | A chain and a token |
|---|
| Crypto required | No | No | Yes |
|---|
| Deployment | Self-hosted, no external calls | Self-hosted or SaaS | Public network |
|---|
What this does not give you yet
- The hash is unsigned by default. A SHA-256 digest proves the record was not edited after the digest was taken; it proves nothing about who produced the record. Ed25519-signed verdicts are an optional extra (
pip install "parley-consensus[crypto]"), and even then a signature binds a key to an exact option-and-verdict pair without proving the key belongs to the owner. SECURITY.md calls this tamper-evidence, not authenticity. - Scores are public. The soft score rides in the transcript, so an untrusted coordinator can infer preference ordering and each party's feasible region. The reason and the sheet stay hidden.
- Privacy needs one process per owner. In the in-process and MCP modes, whoever calls
run_consensusholds every sheet. The masked-verdict guarantee applies when each agent runs as its own bot over HTTP. - No collusion resistance. The max-min rule is not yet hardened against parties that coordinate their reported scores. That is the research track, and SECURITY.md says not to deploy v0 against genuinely adversarial principals in production.
Questions this page gets asked
Is this a voting system?
No. A majority cannot outvote a red line: an option is a candidate only if every agent returned acceptable. Among those candidates, run_consensus picks the one with the highest minimum score, breaking ties on the highest total. The least-happy participant is lifted, not outnumbered.
What happens when no option clears everyone's red lines?
run_consensus returns status "deadlock" with decision None, and the transcript records it that way. The engine never picks a fallback. A deadlock forces nothing on anyone, so verify_non_betrayal returns True for every owner.
What does the coordinator learn about my preferences?
Per option: acceptable or not, a soft score in [0, 1], and a reason that is exactly "ok" or "red-line". It never receives the sheet or the name of a crossed constraint. The score is public in the transcript, so an untrusted coordinator can infer your preference ordering and feasible region; SECURITY.md lists this as a known limit.
Can my existing agent framework take part?
run_consensus accepts anything with an owner attribute and a consider(option) method that returns a Verdict. The in-process Agent and the HTTP RemoteAgent both satisfy that protocol. Sheets are written as Python predicates and utility functions, or as declarative constraints from parley.spec. parley.elicit lets a language model draft a sheet from a participant's own answers, but the model sits behind a callable you inject, proposes plain data only, and nothing binds until the participant confirms each hard constraint. No LLM SDK ships in the core.