Protocol-first orchestration for AI agents.
Declarative multiparty session protocols for AI agents
Inspired by Multiparty Session Types (MPST), this project focuses on declarative session protocols for AI agents; it is a practical protocol-oriented experiment, not a claim to implement MPST as a type system.
What it is
You describe one typed global conversation between named roles. agentsparty
projects it onto every role, rejects the conversations a role could not
follow, and runs the session. A language model fills a typed payload or picks a
declared branch — it does not route the workflow, invent roles, or call
arbitrary code. The protocol owns control; the model owns content.
Install
pip install agentsparty
pip install "agentsparty[openai]" # optional OpenAI Responses backend
One agent
The smallest session is a request and a reply. ap.patterns.request_reply
builds it, and ask plays the caller from your program — it sends the task
and returns what comes back. Roles are declared with ap.roles(...): a typo
is a NameError, not a new role.
import agentsparty as ap
Reader, Writer = ap.roles('Reader', 'Writer')
shape = ap.patterns.request_reply(Reader, Writer)
model = ap.model('openai:gpt-5.6-luna')
session = ap.Session(
shape,
{
Writer: ap.Agent(model, 'Write three paragraphs.'),
},
)
article = session.ask(
Reader,
shape.Ask('What does a multi-agent AI system mean?'),
expect=shape.Answer,
)
print(article.text)
A session, end to end
A Requester hands a Writer a task. The Writer drafts an article and the
Reviewer approves it or sends it back with a critique — a loop that ends the
moment the Reviewer approves. ap.patterns.review_loop builds the shape.
The cast is a dict: one participant spec per role. A dict comprehension is
the bulk form when several roles share a model.
import agentsparty as ap
Requester, Writer, Reviewer = ap.roles(
'Requester', 'Writer', 'Reviewer'
)
shape = ap.patterns.review_loop(Requester, Writer, Reviewer)
model = ap.model('openai:gpt-5.6-luna')
briefs = {
Writer: 'Write the article. Revise it to address any critique.',
Reviewer: (
'Approve the draft once it is ready; otherwise Reject it with '
'a short critique.'
),
}
session = ap.Session(
shape,
{
role: ap.Agent(model, brief)
for role, brief in briefs.items()
},
)
article = session.ask(
Requester,
shape.Task('What does a multi-agent AI system mean?'),
expect=shape.Final,
)
print(article.text)
The task travels as a message, never as the Writer's system prompt. On each rejected draft the Writer sends the Requester a notice, so the Requester can tell a still-running loop from the final article — the pattern inserts that notice and projects its shape before returning it.
ask only plays a role that authors exactly one message and then listens;
a role that chooses what to send belongs in the cast. Which of the two a role
is comes from the projection, not from a flag.
The same session, as a runnable file, is
examples/online/quickstart.py:
export OPENAI_API_KEY=...
uv run python examples/online/quickstart.py
Declare your own conversation
A message is a class. A protocol is a list of steps: send, choose,
loop / repeat, par, closed by Protocol.
import agentsparty as ap
Reader, Writer = ap.roles('Reader', 'Writer')
class Question(ap.Message):
"""What the reader asks."""
text: str
class Article(ap.Message):
"""The answer, three paragraphs."""
text: str
protocol = ap.Protocol(
ap.send(Reader, Writer, Question),
ap.send(Writer, Reader, Article),
)
Protocol patterns
The shapes agent systems keep re-deriving are ready-made in ap.patterns.
Each returns a Shape: a closed protocol plus the message classes it
declares (shape.Answer is a real ap.Message subclass).
| Pattern | What it is | Example |
|---|---|---|
request_reply |
a call and its answer | examples/online/hello.py |
review_loop |
draft, review, and a notice until accepted | examples/online/quickstart.py |
pipeline |
a relay chain, each stage handing work on | examples/online/pipeline.py |
triage |
a router that hands work to one of several desks | examples/online/help_desk.py |
swarm |
peers answer or hand work around a ring | examples/online/agent_swarm.py |
best_of |
several candidates collected and selected | examples/online/best_of.py |
Patterns project their shape before returning it, so a pattern cannot be handed a conversation that refuses to run. See the patterns guide.
What it refuses to run
The same declarative surface rejects a conversation that cannot be carried out by the roles it names — before the first model call. Hand-write the review loop without the notice, so the Requester is never told the loop went round:
import agentsparty as ap
Requester, Writer, Reviewer = ap.roles(
'Requester', 'Writer', 'Reviewer'
)
class Task(ap.Message):
"""The writing task."""
text: str
class Draft(ap.Message):
"""The article draft."""
text: str
class Approve(ap.Message):
"""Accept the draft."""
class Reject(ap.Message):
"""Send the draft back."""
text: str
class Final(ap.Message):
"""The finished article."""
text: str
protocol = ap.Protocol(
ap.send(Requester, Writer, Task),
ap.loop(
ap.send(Writer, Reviewer, Draft),
ap.choose(
Reviewer,
Writer,
{
Approve: [
ap.send(Writer, Requester, Final)
],
Reject: [ap.repeat()],
},
),
),
)
print(ap.render(protocol))
try:
ap.Session(
protocol,
{Writer: ap.Script([]), Reviewer: ap.Script([])},
)
except ap.ProjectionError as error:
print(error.role)
print(error.where)
print(error.recipe)
Projection fails, and the error names the role, both branches, and the fix —
which is exactly the notice the working session sends. The fields
.role / .where / .recipe carry the same diagnosis:
role 'Requester' cannot tell the branches of the alt Reviewer -> Writer apart:
on 'Approve' it must receive Final from Writer (as Requester), on 'Reject' it must loop at 'loop.0'.
A role that behaves differently per branch must be told which branch was taken — add a message from Reviewer (or Writer) to Requester inside each branch.
No API key, no network, and no model call: the refusal is pure projection.
A protocol built from ap.patterns cannot fail this way: every pattern inserts
its synchronising messages and projects its shape before it is returned.
The full ladder of examples, from a two-message session to a coding harness, is in examples/README.md.
Deterministic runs
A test does not call a network. ap.replies is a model that returns
scripted messages; ap.Script is a role that says known messages in order.
import agentsparty as ap
A, B = ap.roles('A', 'B')
class Ask(ap.Message):
"""A question."""
text: str
class Answer(ap.Message):
"""The reply."""
text: str
protocol = ap.Protocol(
ap.send(A, B, Ask), ap.send(B, A, Answer)
)
transcript = ap.Session(
protocol,
{
A: ap.Script([Ask('q')]),
B: ap.Agent(ap.replies([Answer('ok')]), ''),
},
).run()
print(transcript.last(Answer) == Answer('ok'))
Embedding in a service
Session.start returns a live Run. A person outside the process decides
through ap.Desk.
import agentsparty as ap
A, B = ap.roles('A', 'B')
class Ask(ap.Message):
"""A question."""
text: str
class Answer(ap.Message):
"""The reply."""
text: str
async def open_ticket(text: str) -> ap.Run:
desk = ap.Desk()
protocol = ap.Protocol(
ap.send(A, B, Ask), ap.send(B, A, Answer)
)
return ap.Session(protocol, {B: ap.Human(desk)}).start(
A, Ask(text)
)
When to use — and when not to
Use when
- the allowed interaction shape is known up front
- every role must only act on messages it actually receives
- you need projection to fail closed before the first model call
- tools are roles with a protocol surface, not free function-calling
- sessions must resume from recorded decisions without re-asking
Do not use when
- one agent with a free tool set is enough (use a simpler agent SDK)
- the route must be discovered at run time by the model
- you need a large catalogue of vendor integrations out of the box
- you require a stable API before 1.0 (this project is research / 0.x)
- you need multi-process or multi-machine transport (the runtime is in-process)
Links
- Documentation
- Examples
- Messages
- Protocol
- Sessions and runs
- Projection
- Protocol patterns
- A very gentle introduction to Multiparty Session Types
- Global Types for Agent Interaction Protocols
- Security
- Contributing
Status
Research framework at 0.2.x. The public surface is agentsparty.__all__
(68 names) plus the tier-2 submodules; see
tests/public_api.txt.
What agentsparty proves, checks at run time, and deliberately leaves to the
application — including the non-guarantees (no deadlock-freedom, no liveness,
no exactly-once) — is set out in the
guarantee table.
Exception types are stable; message text and journal formats are not.
Security
Untrusted payloads and web content are data, not instructions. User-written
handlers run with the privileges of the host process — sandbox them, and
validate paths or commands before any effect. Give a hand-built OpenAI client
a finite transport timeout; ap.model('openai:...') already applies one.
within= on a step, Allowance, and ap.metered bound branch windows,
protocol steps, and token spend. Journals and tracers persist payloads and
model output in plaintext.
Private reports and security guidance: SECURITY.md.
Development
uv sync --all-groups
just all # or: uv run nox -t ci
Agent conventions for contributors live in AGENTS.md.
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agentsparty-0.2.0.tar.gz.
File metadata
- Download URL: agentsparty-0.2.0.tar.gz
- Upload date:
- Size: 134.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"NixOS","version":"25.11","id":"xantusia","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b055710ee2ebf6a16cff1d56f1b0e47485c5ae2a29c6d12924f0f968e957629b
|
|
| MD5 |
9fa3c95bd127adaa9c401c4595c4f7b3
|
|
| BLAKE2b-256 |
e9b5182196f86e6c88ca9490df532993f69baad699d1d9cf9e86517b20e01e64
|
File details
Details for the file agentsparty-0.2.0-py3-none-any.whl.
File metadata
- Download URL: agentsparty-0.2.0-py3-none-any.whl
- Upload date:
- Size: 167.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"NixOS","version":"25.11","id":"xantusia","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
992db57a2e14b68424c3993fc28adc94578a41b48cc495cdc8c2b836cb8cd46f
|
|
| MD5 |
2d52c8d7fb4fae4fea10ce18fd78952b
|
|
| BLAKE2b-256 |
2d373f0166c99e3125cee30022ac2e932572b804b83fcf3bb4092d5929348b8c
|