Architecture
Three-layer model
Runtime (Rust) — protocol enforcement, transitions, replay, persistence
↑
Language SDK — typed state models, action builders, session helpers
↑
Orchestrator — decision strategies, voting rules, AI heuristicsThe SDK is the middle layer: it provides typed convenience APIs for building and sending MACP envelopes, plus local state projections. It does not enforce business policy.
!!! info "What belongs where"
- SDK:
session.vote("p1", "approve")— builds envelope, sends to runtime, tracks locally - Orchestrator: "commit if >50% approved and no blocking objections" — your logic, your policy
- Runtime: validates the Commitment, transitions session to RESOLVED — protocol enforcement
Module map
macp_sdk/
├── client.py MacpClient (sync gRPC) + MacpStream (bidirectional streaming)
├── auth.py AuthConfig — bearer tokens and dev agent headers
├── base_session.py BaseSession ABC — shared start/commit/cancel/metadata
├── base_projection.py BaseProjection ABC — shared transcript/commitment tracking
├── decision.py DecisionSession — propose, evaluate, object, vote
├── proposal.py ProposalSession + ProposalProjection
├── task.py TaskSession + TaskProjection
├── handoff.py HandoffSession + HandoffProjection
├── quorum.py QuorumSession + QuorumProjection
├── projections.py DecisionProjection
├── envelope.py Low-level envelope/payload builders
├── errors.py Exception hierarchy
├── constants.py Mode URIs and version strings
├── retry.py RetryPolicy + retry_send helper
└── _logging.py SDK logger configurationBaseSession / BaseProjection pattern
All 5 mode session helpers inherit from BaseSession, which provides:
| Method | Purpose |
|---|---|
start() | Send SessionStart with intent, participants, TTL, context |
commit() | Send Commitment to resolve the session |
cancel() | Cancel session via CancelSession RPC |
metadata() | Query session metadata via GetSession RPC |
open_stream() | Open bidirectional stream via StreamSession RPC |
Mode-specific subclasses add only their action methods (e.g., propose(), vote()).
Similarly, all projections inherit from BaseProjection, which handles:
- Transcript tracking — append-only list of accepted envelopes
- Commitment detection — recognizes Commitment messages and updates phase
- Mode routing — ignores envelopes from other modes
Why projections exist
The runtime's GetSession RPC returns metadata only (state, TTL, versions) — not mode-specific state or transcript. The SDK therefore maintains local in-process projections that track every accepted envelope and derive mode state.
# Send a message
session.vote("p1", "approve", sender="alice")
# ↑ On success, the projection is updated automatically
# Query the local projection
proj = session.decision_projection
proj.vote_totals() # {"p1": 1}
proj.majority_winner() # "p1"Projection lifecycle
- Session helper sends an envelope via
client.send() - Runtime validates and accepts (Ack with
ok=true) - On success,
_send_and_track()callsprojection.apply_envelope(envelope) - Projection parses the payload and updates its local state
- Orchestrator queries the projection for decision-making
Important: projections are local
Projections only see envelopes sent through this session helper instance. If multiple SDK instances participate in the same session, each has its own partial view. For a complete view, use StreamSession to observe all accepted envelopes.
Client → Runtime interaction
┌─────────────┐ gRPC ┌─────────────┐
│ MacpClient │ ───── Initialize ────→ │ Runtime │
│ │ ───── Send ──────────→ │ (Rust) │
│ │ ←──── Ack ─────────── │ │
│ │ ───── GetSession ───→ │ │
│ │ ───── CancelSession → │ │
│ │ ←──→ StreamSession │ │
│ │ ───── ListModes ────→ │ │
│ │ ───── GetManifest ──→ │ │
│ │ ←─── WatchRegistry │ │
│ │ ←─── WatchRoots │ │
└─────────────┘ └─────────────┘All communication is client-initiated. The runtime never calls back into the SDK. If you need runtime-driven behavior, run a Python agent as a separate process that polls or streams from the runtime.
Data flow for a typical session
1. client.initialize() → negotiate capabilities
2. session.start(intent, participants) → SessionStart envelope → Ack
3. session.propose("p1", "option-a") → Proposal envelope → Ack → projection updated
4. session.vote("p1", "approve") → Vote envelope → Ack → projection updated
5. session.decision_projection.majority_winner() → query local state
6. session.commit(action="approved") → Commitment envelope → Ack → session RESOLVEDSteps 3–5 repeat for as many messages as the session requires. The projection accumulates state incrementally.
Extension modes
The runtime supports dynamic extension modes via RegisterExtMode, UnregisterExtMode, and PromoteMode RPCs. Extensions use a passthrough handler that validates declared message types:
# Register a custom mode
from macp.v1 import core_pb2
descriptor = core_pb2.ModeDescriptor(
mode="ext.my-custom.v1",
mode_version="1.0.0",
title="My Custom Mode",
message_types=["CustomMessage", "CustomResponse"],
terminal_message_types=["Commitment"],
)
client.register_ext_mode(descriptor, auth=admin_auth)
# Later: list registered extensions
ext_modes = client.list_ext_modes()
# Send messages using the low-level envelope builder
from macp_sdk.envelope import build_envelope, serialize_message
envelope = build_envelope(
mode="ext.my-custom.v1",
message_type="CustomMessage",
session_id="...",
payload=serialize_message(my_payload),
)
client.send(envelope, auth=auth)