Skip to content
parley.

Guide

A red line an LLM can be talked out of is not a red line.

"Never agree to more than 700" in a system prompt is a request. The model weighs it against everything else in the context, including the other side's best argument. In Parley a red line is a predicate the engine runs on every option, and an option that fails it cannot win. This page shows the mechanism, the fields it touches, and the check the owner runs afterwards.

Every mechanism named here lives in parley/preferences.py and parley/agent.py in the source repository. Companion guides: fair multi-agent negotiation, running Parley as an MCP tool, verifying a decision receipt and a fair group decision with private limits.

What a red line is, in one dataclass

A hard constraint is a name and a predicate. The name is for the owner. The predicate is a function of the option, and the contract is strict: the option is acceptable only if the predicate returns the literal True. Any other return value, truthy or not, crosses it.

@dataclass(frozen=True)
class HardConstraint:
    name: str
    predicate: Callable[[Any], bool]

# a red line: code, not a preference
HardConstraint("budget", lambda o: o["cost"] <= 700)

A PreferenceSheet holds a list of these underhard, and separately an optionalutility function that returns a soft score. The two fields are never mixed. That separation is the whole design: soft utility ranks options that already clear every red line, and nothing else.

What PreferenceSheet.evaluate does with it

evaluate(option) returns anEvaluation with three fields. It runs every predicate, collects the names of the ones that did not return True, and sets feasible only when that list is empty. The soft score is computed regardless: the owner's utility clamped to [0, 1], or a neutral 0.5 when no utility was given. A utility that returns NaN raisesUtilityError, since that is a fault in the sheet, not in the option.

Evaluation(
  feasible=False, # every predicate returned True?
  violated=["budget"], # stays private to the owner
  score=0.1, # soft utility in [0, 1]
)

The violated list is where the constraint names end up, and it is the last place they exist. The next layer discards it.

What leaves the agent

Agent.consider(option) callsevaluate and returns a Verdict. The verdict carries acceptable, the score, and areason that is the string "ok"when the option is feasible and "red-line" when it is not. The sheet is never handed to the coordinator, and the coordinator is untrusted by construction: it sees that a line was crossed, never which one.

def consider(self, option):
    ev = self.sheet.evaluate(option)
    return Verdict(
        owner=self.owner,
        acceptable=ev.feasible,
        score=ev.score,
        reason="ok" if ev.feasible else "red-line",
    )

Downstream, run_consensus treats a singleacceptable=False as a veto: an option is a candidate only when every agent accepted it, and scores are compared among candidates only. A red line is therefore never outvoted and never outbid.

Prompted constraint versus enforced red line

"Never do X" in a promptA Parley red line
Where it livesIn the system prompt, as natural language the model is asked to honour.In a PreferenceSheet, as a Python callable the engine runs on every option.
Who decides it was crossedThe model, in context, on every turn, under whatever pressure the other side applies.PreferenceSheet.evaluate. The predicate returns True or the option is infeasible.
Can a strong enough offer override itYes. A constraint in a prompt competes with everything else in the prompt.No. Scores are only compared among options that already cleared every red line.
What the other side learnsWhatever the model says while explaining its refusal, which may be the constraint itself.The word "red-line". Not the name of the constraint and not the sheet.
How the owner checks afterwardsRead the chat log and hope the model's account of itself is accurate.verify_non_betrayal(sheet, decision) re-runs the sheet locally and returns a boolean.

Checking it held, without showing anyone the sheet

After a decision, each owner runstranscript.verify_non_betrayal(sheet, decision) on their own machine. It re-evaluates the private sheet against the decision and returnsfeasible. Nothing is transmitted; the sheet never leaves. A deadlock, where no option cleared every red line, returnsTrue for everyone, since a deadlock forces nothing on anyone. The transcript itself hashes with SHA-256 over its canonical JSON, so an edit to the record after the fact changes the digest.

From an MCP host such as Claude or Cursor, the same replay is theparley_check_party tool, which takes one party's red lines and a decision. In that mode the host holds every party's spec in one call, so there is no privacy between parties; what still holds is the deterministic rejection.

What a red line does not do

  • It does not read minds. Non-betrayal is provable with respect to the predicates the owner actually wrote. A constraint that was meant but not encoded is not enforced, and Parley does not judge whether a sheet captures what someone intended.
  • It does not hide the soft score. The score is public in the transcript. An untrusted coordinator can infer preference ordering and, over enough options, the shape of the feasible region.
  • It does not sign anything by default. The transcript digest is an unsigned SHA-256. Ed25519-signed verdicts are an optional extra, and SECURITY.md describes even those as tamper-evidence rather than proof of who signed.
  • A model never writes the predicate.parley.elicit can draft a sheet from a participant's own answers, but the language model sits behind an injectable callable and can only propose plain data. Every proposed item is validated against the declarative Constraint schema inspec.py, never evaluated as code, and nothing is binding until the participant confirms every hard constraint by id.

Questions this page gets asked

What exactly counts as crossing a red line?

Any return value other than the literal True. The predicate is called with the option; PreferenceSheet.evaluate lists a constraint as violated unless predicate(option) is True. A truthy value such as 1 or a non-empty string still counts as crossed, and so do False and None. The check fails closed by design.

Can a red line be weighted, softened, or traded against a very high score?

No. Hard constraints and soft utility are separate fields on the sheet and are combined nowhere. run_consensus only computes scores for options where every agent returned acceptable, so an infeasible option never enters the comparison, whatever its scores would have been.

What does the coordinator see when my red line fires?

A Verdict with acceptable set to False, your soft score, and the reason "red-line". The names of the crossed constraints are computed inside evaluate and kept in an Evaluation object that never leaves your agent. The score itself is public in the transcript; SECURITY.md lists that as a known limit.

Does this need an LLM at all?

No. The core is standard-library Python with no dependencies, and a red line is a plain function. A language model can sit upstream, in parley.elicit, to draft a sheet from a participant's answers, but it only proposes plain data that is validated against a fixed schema and then confirmed by the participant. The enforcement path never consults a model.

Watch a red line block an option, then read the predicate that did it.

The demo replays pre-computed runs of the same engine, no signup. The repository is Apache-2.0; preferences.py is under sixty lines.

Back to the overview · Next: how the whole negotiation stays fair · Setup: Parley as an MCP tool · Then: verify the receipt afterwards · Worked case: a group decision with private limits