Proposal Mode

Mode identifier: macp.mode.proposal.v1 Participant model: peer Determinism: semantic-deterministic

Purpose

Peer-to-peer negotiation with proposals, counterproposals, accepts, rejects, and withdrawals.

Canonical references: RFC-MACP-0008 (Proposal Mode) is normative for the state machine, authority rules, and validation constraints. See also the spec mode summaries and runtime modes guide › Proposal Mode for validation as implemented. This page covers the TypeScript API.

Session Lifecycle

SessionStart → Proposal → CounterProposal? → Accept/Reject/Withdraw → Commitment

API

ProposalSession

import { ProposalSession } from 'macp-sdk-typescript';

const session = new ProposalSession(client);
await session.start({ intent: '...', participants: ['bob'], ttlMs: 60_000 });

Methods

MethodMessage TypeDescription
propose(input)ProposalSubmit an initial proposal
counterPropose(input)CounterProposalSubmit a counterproposal that supersedes another
accept(input)AcceptAccept a proposal
reject(input)RejectReject a proposal (optionally terminal)
withdraw(input)WithdrawWithdraw a proposal
commit(input)CommitmentFinalize the negotiation

Like every mode session, ProposalSession also exposes the shared lifecycle helpers — metadata(), cancel(reason), suspend(reason), resume(reason), and openStream(). suspend() (proto 0.1.3+) is a non-terminal pause: the runtime banks the remaining TTL and rejects messages until resume() restores SESSION_STATE_OPEN and the banked TTL. See Decision Mode → Lifecycle helpers.

Propose

await session.propose({
  proposalId: 'p1',
  title: 'Use React',
  summary: 'Mature ecosystem with large community',
  tags: ['frontend', 'framework'],
});

Counter-Propose

await session.counterPropose({
  proposalId: 'p2',
  supersedesProposalId: 'p1',  // links to original
  title: 'Use Svelte',
  summary: 'Lighter bundle, better DX',
  sender: 'bob',
  auth: Auth.devAgent('bob'),
});

Accept / Reject / Withdraw

await session.accept({ proposalId: 'p2', reason: 'agreed' });

// Non-terminal rejection (negotiation continues)
await session.reject({ proposalId: 'p1', terminal: false, reason: 'too heavy' });

// Terminal rejection (proposal permanently rejected)
await session.reject({ proposalId: 'p1', terminal: true, reason: 'blocked' });

// Withdraw own proposal
await session.withdraw({ proposalId: 'p1', reason: 'superseded' });

ProposalProjection

State

PropertyTypeDescription
proposalsMap<string, ProposalRecord>All proposals with status tracking
acceptsProposalAcceptRecord[]All accept messages
rejectionsProposalRejectRecord[]All rejection messages
transcriptEnvelope[]All accepted envelopes
phase'Negotiating' | 'TerminalRejected' | 'Committed'Current phase
commitmentRecord<string, unknown> | undefinedCommitment payload if resolved

ProposalRecord Status

Each proposal tracks a status field:

StatusMeaning
openActive, can be accepted/rejected/withdrawn
acceptedReserved in the type; the projection tracks accepts in accepts[] — use isAccepted(id)
rejectedTerminally rejected (non-terminal rejects leave status open)
withdrawnWithdrawn by the proposer

Counter-proposals set supersedes to link back to the original.

Query Helpers

session.projection.activeProposals();           // proposals with status 'open'
session.projection.liveProposals();             // Map of all non-withdrawn proposals
session.projection.latestProposal();            // most recently submitted
session.projection.isAccepted('p2');            // true if any Accept exists
session.projection.isTerminallyRejected('p1');  // true if terminal Reject exists
session.projection.hasTerminalRejection();      // true if any proposal was terminally rejected
session.projection.acceptedProposal();          // proposalId if every Accept targets one proposal
session.projection.isCommitted;                 // true once a Commitment is applied
session.projection.isPositiveOutcome;           // undefined until committed; then outcomePositive

RFC Validation Rules

The runtime enforces the cross-message rules — unique proposal_ids, CounterProposal/Accept/Reject/Withdraw referencing an existing proposal, withdrawn proposals staying withdrawn, and latest-Accept-wins retargeting. The normative rule set lives in RFC-MACP-0008 §4; the runtime modes guide › Proposal Mode documents validation as implemented.

Example

See examples/proposal-smoke.ts.