MACP

Proposal Mode

Mode URI: macp.mode.proposal.v1 Status: provisional RFC: RFC-MACP-0008

Offer/counteroffer negotiation with peer refinement. Designed for bilateral or multilateral negotiations where parties iteratively refine terms until convergence or terminal rejection.

When to use

Use Proposal mode when agents need to negotiate terms through iterative offers and counteroffers:

  • Contract negotiation (price, SLA, terms)
  • Resource allocation (budget, capacity, scheduling)
  • Configuration agreement (settings, parameters)
  • Any bilateral/multilateral negotiation

Participant model: peer

Participants are symmetric peers — all declared participants have equal standing to propose, counter-propose, accept, or reject. There is no designated coordinator role for mode-specific messages (though Commitment still requires an authorized sender).

Determinism: semantic-deterministic

Same accepted envelope sequence → same negotiation outcome. Convergence, terminal rejection, and withdrawal states are fully determined by the message history.

Message flow

SessionStart

Proposal (initial offer)

CounterProposal (supersedes previous, iterative)

Accept / Reject / Withdraw

Commitment → RESOLVED

Key semantics

  • CounterProposal supersedes a referenced proposal — the original becomes withdrawn
  • Accept records a participant's acceptance of a specific proposal
  • Reject with terminal=True signals a final rejection — no further negotiation
  • Withdraw removes a proposal from consideration
  • Convergence occurs when all participants accept the same live proposal

Authorization rules

MessageWho can send
ProposalAny declared participant
CounterProposalAny declared participant (must reference existing proposal)
AcceptAny declared participant
RejectAny declared participant
WithdrawAny declared participant (typically the original proposer)
CommitmentSession initiator / authorized coordinator

Terminal conditions

A session becomes eligible for Commitment when:

  1. Convergence: All required participants accept the same live (non-withdrawn) proposal, OR
  2. Terminal rejection: A participant issues a terminal=True rejection with authority to terminate

Session helper

from macp_sdk import AuthConfig, MacpClient
from macp_sdk.proposal import ProposalSession

client = MacpClient(target="127.0.0.1:50051", secure=False, auth=AuthConfig.for_dev_agent("coordinator"))
session = ProposalSession(client)
session.start(
    intent="negotiate service contract terms",
    participants=["coordinator", "buyer", "seller"],
    ttl_ms=120_000,
)

# Seller's initial offer
session.propose("p1", "Standard Package", summary="$100k/year, basic SLA", sender="seller")

# Buyer counter-proposes
session.counter_propose(
    "p2", "p1", "Enhanced Package",
    summary="$80k/year, premium SLA, 24/7 support",
    sender="buyer",
)

# Seller accepts the counter
session.accept("p2", reason="terms acceptable", sender="seller")

# Buyer confirms
session.accept("p2", reason="agreed", sender="buyer")

# Commit the agreement
proj = session.proposal_projection
if proj.accepted_proposal() == "p2":
    session.commit(
        action="contract.agreed",
        authority_scope="procurement",
        reason="Both parties accepted p2",
    )

Projection queries

proj = session.proposal_projection

# Proposals
proj.proposals                  # dict[str, ProposalRecord]
proj.live_proposals()           # Only proposals with disposition="live"
proj.proposals["p1"].disposition  # "live" | "withdrawn"
proj.proposals["p2"].supersedes   # "p1"

# Accepts
proj.accepts                    # dict[sender, AcceptRecord]
proj.accepted_proposal()        # proposal_id if all accepts agree, else None

# Rejections and withdrawals
proj.terminal_rejections        # list[TerminalRejectRecord]
proj.has_terminal_rejection()   # True if any terminal rejection exists

# Lifecycle
proj.phase                      # "Negotiating" | "TerminalRejected" | "Committed"
proj.is_committed               # True after Commitment

Error cases

ErrorWhenHow to handle
FORBIDDENSender not a declared participantVerify sender
INVALID_ENVELOPECounterProposal references non-existent proposalCheck supersedes_proposal_id exists
SESSION_NOT_OPENNegotiation already concludedCheck session state

Real-world scenario: multi-round negotiation

# Round 1: Initial offers
session.propose("p1", "Plan A", summary="$50k, 6-month term", sender="vendor")

# Round 2: Counter
session.counter_propose("p2", "p1", "Plan A Revised", summary="$45k, 12-month term", sender="client")

# Round 3: Final counter
session.counter_propose("p3", "p2", "Plan A Final", summary="$47k, 12-month, quarterly reviews", sender="vendor")

# Both accept the final version
session.accept("p3", sender="client")
session.accept("p3", sender="vendor")

# At this point, proj.accepted_proposal() == "p3"

API Reference

::: macp_sdk.proposal.ProposalSession

::: macp_sdk.proposal.ProposalProjection