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.
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
Agets an output toB; - endpoint
Bgets an input fromA; 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:
- Send a branch-specific message to the role whose duties diverge.
- Move the choice to the role that already owns the differing work.
- 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.
Related
- agentsparty.kernel.errors.ProjectionError: structured diagnostic fields.
- Error taxonomy: the stable diagnostic fields.
- Protocols and composition: declare the branch a role must observe.
- Sessions, execution, and limits: projection runs at bind time.