Build a session protocol from msg, sequencing, alt, recursion, and par.
Five constructors shape a protocol tree: msg, >>, alt, rec / var,
and par. The rendered text and the tree it stands for have the same shape.
from agentsparty.protocol import Nothing, Text, alt, msg, par, rec, render, var
from agentsparty.kernel.role import roles
W, V, A, B, C, D = roles('W', 'V', 'A', 'B', 'C', 'D')
review = msg[W, V](Text('Draft')) >> alt[V, W](
Nothing('Approve'),
Text('Reject'),
)
poll = rec(
'Poll',
msg[A, B](Text('Tick')) >> var('Poll'),
).close()
split = par(
msg[A, B](Text('L')),
msg[C, D](Text('R')),
).close()
print(render(review))
print(render(poll))
print(render(split))Nothing is the payload-free codec when a branch carries only its label.
msg[S, R](payload) is one directed interaction: sender, receiver, and codec
become one node in the protocol tree. Each miniature below shows both views
of the same fragment: the text render prints on the left, the tree that
text stands for on the right.
One rendered line, one node; the two roles and the codec are its leaves.
left >> right sequences two fragments. The left child must finish before
the right child begins.
The seq node holds its children in order, so the order of the rendered
lines is the order of the tree.
The first role argument is the chooser; the second receives the branch label. Each branch is a case: a labelled codec, optionally followed by a continuation.
The alt node has exactly one child per declared case. A label that is not
a child does not exist at runtime either.
from agentsparty.protocol import Nothing, Text, alt, msg, render
from agentsparty.kernel.role import roles
Writer, Reviewer = roles('Writer', 'Reviewer')
Draft = Text('Draft')
Approve = Nothing('Approve')
Reject = Text('Reject')
protocol = msg[Writer, Reviewer](Draft) >> alt[Reviewer, Writer](
Approve, Reject
)
print(render(protocol))The Reject branch currently informs only the Writer. A third role whose
next action depends on the outcome still has to observe the choice;
knowledge of alt is the check.
rec('X', body) names a recursion variable; var('X') jumps back to it.
The jump must sit behind an interaction (guarded recursion) so the runtime
can unfold a bounded number of times.
var X is a leaf pointing back at its binder: the back edge in the tree is
the repeated X in the rendered text.
from agentsparty.protocol import Text, msg, rec, render, var
from agentsparty.kernel.role import roles
A, B = roles('A', 'B')
poll = rec(
'Poll',
msg[A, B](Text('Tick')) >> var('Poll'),
).close()
print(render(poll))repeat(times, fragment) unrolls a fixed number of copies and accepts
times == 0. Recursion that may run forever is a static question about
paths; the runtime bound is an Allowance. See
composition and termination.
Tracks must have disjoint roles, and they may interleave. There is no product payload and no implicit continuation once both finish; convergence is an ordinary message from a role that participates in both conversations.
The tracks are siblings, not a chain: neither is the other's continuation,
which is why nothing hangs below the par node.
from agentsparty.protocol import Text, msg, par, render
from agentsparty.kernel.role import roles
A, B, C, D = roles('A', 'B', 'C', 'D')
split = par(
msg[A, B](Text('L')),
msg[C, D](Text('R')),
).close()
print(render(split))The same operators exist as statements under
@choreography, and
the coding-agent harness
puts them to work on a five-role protocol.