Messages
A message is a class. Its fields are the payload; its docstring is the intent.
A protocol message is a subclass of ap.Message. The class is the wire
value: its fields are the payload, its docstring is the intent a model
reads, and name = '…' in the class body overrides the wire label
(default: the class name).
Use a message when a value crosses a protocol edge. Keep routing decisions in the protocol and business data in fields; that split makes both sides inspectable before a session starts.
A message is a boundary, not a bag of metadata
The class names the legal operation, the instance carries one occurrence, and the codec decides what can cross the wire. Keeping those roles separate is why handlers can work with typed attributes instead of raw dictionaries.
import agentsparty as ap
class Draft(ap.Message):
"""The chapter as drafted."""
text: str
draft = Draft('hello')Control and content
A message class sits on the boundary between protocol control and participant content:
| Concern | Owner | Constraint |
|---|---|---|
| Topology | Protocol | A participant cannot add a role or route. |
| Visibility | projection | A role cannot react to an unseen branch. |
| Message class | send or choose | A participant must select a class on offer. |
| Field value | participant | The protocol does not invent payload data. |
| Refinement | message codec | Invalid data does not reach a handler. |
The same rule applies to every participant kind. A model may choose Approve
or Reject when both are offered; a machine handler may return one declared
reply; a human may submit one offered class. None of them can create a third
route at runtime.
Fields
Field count determines the wire shape: no fields carry None, one field carries
that field's value, and several fields form a closed object.
import agentsparty as ap
class Ready(ap.Message):
"""Planning is finished."""
class Write(ap.Message):
"""Relative path and the full file contents."""
path: str
content: str
Ready().raw()
Write(path='a.py', content='x').raw()A field may not be called name; that name overrides the label:
import agentsparty as ap
class GiveUp(ap.Message):
"""Why this will not converge."""
text: str
name = 'Give up'Refinements
Annotated plus ap.where or ap.between defines boundary constraints.
codec_of applies the predicate when the message is declared, so a value
that fails it is refused at construction and decode.
| Helper | Contract |
|---|---|
ap.where(requirement, holds) | Accept a value only when holds(value) is true. |
ap.between(low, high) | Accept a numeric value in the inclusive interval. |
from typing import Annotated
import agentsparty as ap
RelPath = Annotated[str, ap.where('stays relative', lambda p: '..' not in p)]
Percent = Annotated[float, ap.between(0, 100)]
class Read(ap.Message):
"""A path to read."""
path: RelPath
class Score(ap.Message):
"""A review score."""
value: Percent
Read(path='src/main.py')
good = Score(87.5)The predicate is not a routing rule. If a value is structurally valid but the workflow
should take a different path, declare that path with choose rather than hiding
business logic inside a refinement.
Intent
The class docstring becomes Message.__intent__. Agents use it as the meaning
of a choice or payload, while the protocol still decides which class is legal.
This is separate from a field's runtime constraint: intent guides the
participant, while ap.where rejects a value that violates the contract.
import agentsparty as ap
class Ship(ap.Message):
"""Accept the current revision for delivery."""Wire forms
raw() has one predictable shape at the boundary:
| Declared fields | Python value | Wire value |
|---|---|---|
| 0 | Ready() | None |
| 1 | Draft('text') | 'text' |
| N | Write(path='a', content='x') | {'path': 'a', 'content': 'x'} |
import agentsparty as ap
class Ready(ap.Message):
"""No payload."""
class Draft(ap.Message):
"""One text field."""
text: str
class Write(ap.Message):
"""Two fields."""
path: str
content: str
Ready().raw()
Draft('text').raw()
Write(path='a', content='x').raw()The message in a session
The same class travels through the whole path: ap.send names the protocol
node, Draft('…') is the value, Machine keys a handler, expect=Draft
types a host call, and transcript.last(Draft) reads the delivered history.
import agentsparty as ap
Author, Editor = ap.roles('Author', 'Editor')
class Draft(ap.Message):
"""A draft for review."""
text: str
class Review(ap.Message):
"""The editor's response."""
text: str
protocol = ap.Protocol(
ap.send(Author, Editor, Draft),
ap.send(Editor, Author, Review),
)
transcript = ap.Session(
protocol,
{
Author: ap.Script([Draft('first')]),
Editor: ap.Agent(
ap.replies([Review('ship it')]),
'review the draft',
),
},
).run()Class, instance, and wire value
These three forms answer different questions:
| Form | Example | Question it answers |
|---|---|---|
| Class | Draft | Which operation is legal here? |
| Instance | Draft('chapter 1') | What content is being delivered? |
| Wire value | 'chapter 1' | What crosses the serialization boundary? |
Do not pass an instance to send; the protocol needs the class so it can
project duties before execution. Do not pass a raw dictionary to a handler;
the runtime decodes it into the declared class first.
Naming and compatibility
The class name is a useful default label because it gives renderings and
transcripts a stable vocabulary. Use name only when the wire-facing label
needs to be different, such as a human-facing Give up choice. Keep the Python
class name stable as well: code, type checks, and transcript.last() use the
class object, while journals store the encoded label and payload.
Adding a field changes the wire form from one value to an object and therefore
changes the protocol identity. Treat it as a protocol revision, not a harmless
implementation detail. In 0.2.x, start a new journal after that revision.
An application that must retain old decisions owns and tests its conversion
against both protocol versions; the library does not promise a stable journal
migration format yet.
Related
- agentsparty.message.Message: class reference for
Message. - Protocols and composition: place message classes on edges and branches.
- Roles, participants, and models: see who fills declared content.
- Sessions, execution, and limits: deliver messages and inspect transcripts.