Guide

Projection and branch visibility

Derive role-local endpoints and reject duties that depend on an unseen branch.

A participant cannot act on the whole conversation. Projection derives the local endpoint for each role: what that role may send, receive, select, and offer. The global type describes the session; an endpoint describes one seat.

One tree fans out into as many endpoints as there are roles: each node either becomes a local action for some role or disappears from that role's endpoint entirely.

Spelled out, the fan-out is three columns beside the global messages, with each global line and its local counterparts on one row.

Projection is a compile-time-style review

It answers one narrow question before execution: can every role construct one local endpoint from the shared tree? It does not decide whether the eventual content is useful.

Three endpoint projections

Sending becomes !, receiving becomes ?, and an uninvolved role sees nothing.

Select a global message to light the local lines it becomes across all three endpoints.

Global protocolWriterReviewerArchivist

Pick a global message (↑/↓ to walk): it becomes ! for the sender, ? for the receiver, and nothing at all for the role it never reaches.

Text version
Writer -> Reviewer : Draft(str)
Reviewer -> Writer : Note(str)
Writer -> Archivist : Final(str)
end

Writer:
  !Reviewer : Draft(str)
  ?Reviewer : Note(str)
  !Archivist : Final(str)
  end

Reviewer:
  ?Writer : Draft(str)
  !Writer : Note(str)
  end

Archivist:
  ?Writer : Final(str)
  end

The projection rule

For a message A -> B : M:

  • endpoint A gets an output to B;
  • endpoint B gets an input from A; and
  • every other role gets no local action for that node.

For a choose, the chooser gets an internal select, the informed peer gets an external offer, and an uninvolved role gets the merge of all projected continuations.

import agentsparty as ap

Writer, Reviewer, Reader = ap.roles('Writer', 'Reviewer', 'Reader')


class Draft(ap.Message):
    """The article draft."""

    text: str


class Approve(ap.Message):
    """Accept the draft."""


class Reject(ap.Message):
    """Refuse the draft."""


class Final(ap.Message):
    """The finished article."""

    text: str


class Rejected(ap.Message):
    """The draft was not accepted."""

    text: str


protocol = ap.Protocol(
    ap.send(Writer, Reviewer, Draft),
    ap.choose(
        Reviewer,
        Writer,
        {
            Approve: [ap.send(Writer, Reader, Final)],
            Reject: [ap.send(Writer, Reader, Rejected)],
        },
    ),
)
print(ap.render(protocol))

Final lives inside the Approve branch and Rejected inside Reject. A rejected draft never claims to be a final document.

Projectability

Merging an invisible choose is safe only when the remaining branch continuations merge into one endpoint. If one branch asks a role to receive and another asks it to send, that role would need information it never received. Session raises ProjectionError at that boundary, before any model exists. Read .role / .where / .recipe; do not parse the string.

import agentsparty as ap

A, B, C = ap.roles('A', 'B', 'C')


class Yes(ap.Message):
    """Yes."""


class No(ap.Message):
    """No."""


class Y(ap.Message):
    """Y."""

    text: str


class N(ap.Message):
    """N."""

    text: str


protocol = ap.Protocol(
    ap.choose(
        A,
        B,
        {Yes: [ap.send(A, C, Y)], No: [ap.send(C, A, N)]},
    ),
)
try:
    ap.Session(
        protocol,
        {
            A: ap.Script([]),
            B: ap.Script([]),
            C: ap.Script([]),
        },
    )
except ap.ProjectionError as error:
    print(type(error).__name__, 'before runtime')
    print(error.role)
    print(error.where)
    print(error.recipe)

A successful projection gives each role a single local protocol consistent with the global tree. A participant can still fail, a human can still walk away, and an external API can still hang. The checked properties are listed in what you can rely on.

Repair an unseen branch

A role does not need to hear every choice. It needs enough information to perform the same next duty on every branch it cannot observe. If two branches both tell C to receive the same class from the same sender, those endpoints can merge. If one tells C to send and another tells it to receive, C needs an explicit branch signal.

There are three common repairs:

  1. Send a branch-specific message to the role whose duties diverge.
  2. Move the choice to the role that already owns the differing work.
  3. Reshape the continuations so the uninformed role performs the same action.

A prompt such as “infer whether the request was accepted” does not supply protocol knowledge. It creates a dependency on text, timing, or shared state that projection and journal replay cannot see.

What projection proves

Projection proves that the global protocol has a compatible local view for each declared role. It checks that sends and receives line up, that a choice does not force a role to guess which branch happened, and that recursion can be assembled into a valid endpoint. It also gives the runtime the exact offer at each point, which lets Agent, Machine, Policy, Human, and Script bind against the same contract.

Projection does not prove that a human will answer, that a model will converge, that an HTTP call is available, or that an external write is idempotent. Those are runtime and application properties. Keeping the boundary explicit makes a projection error useful: it identifies a design problem before any of those other variables enter the picture.

Read the diagnostic

ProjectionError.role names the role whose endpoint could not be constructed. where points into the global protocol, and recipe describes the change that would make the duties merge. A UI can display these fields directly; it should not parse the exception string. The error is raised during Session creation, so a failing projection also means no provider call has happened.

When the error reports blindness to an alternative, send a branch marker to that role or move the choice to the role that already owns the differing work.

Stable

On this page