Guide

Roles, participants, and models

Name protocol seats, choose participant kinds, and keep provider policy at the model boundary.

ap.roles('Writer', 'Reviewer') returns role objects. A role is an address in the protocol; a participant is how that address is played. Pick the participant from the work the role must do, not from the model you happen to have available.

Choose by duty

The same projected role can move from Agent to Script to Human without a protocol rewrite. That substitution is the point of binding participants after the global contract has been checked.

Agent

Agent lets a language model fill typed payloads and select one of the offered message classes. It never routes the workflow. repair controls how many decode corrections it may try; memory supplies an initial Brief when you need context before the first envelope.

<!-- signature: ap.Agent -->

OptionTypeDefaultWhat it does
modelLanguageModelrequiredFills fields and chooses declared classes.
briefstr''Instructions for this role.
repairint1Decode corrections offered to the model.
memoryBrief | NoneNoneInitial role memory.
import agentsparty as ap

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


class Ask(ap.Message):
    """A question."""

    text: str


class Answer(ap.Message):
    """The reply."""

    text: str


session = ap.Session(
    ap.Protocol(
        ap.send(Reader, Writer, Ask),
        ap.send(Writer, Reader, Answer),
    ),
    {
        Writer: ap.Agent(
            ap.replies([Answer('ok')]),
            'answer briefly',
            repair=2,
        )
    },
)
answer = session.ask(Reader, Ask('q'), expect=Answer)

The model boundary

ap.model('provider:name') is where a provider identifier becomes a LanguageModel. Keep authentication, transport options, retries, fallback, usage accounting, and provider-specific request fields at this boundary. The Agent should know its brief and current offer, not provider credentials or retry policy.

# requires: api-key
import agentsparty as ap

model = ap.model('openai:gpt-5.6-luna', timeout=30)

Model wrappers return another LanguageModel, so written nesting determines execution order:

WrapperPurpose
retrying(inner, attempts)Retry an unavailable provider when another attempt is permitted.
fallback(primary, *rest)Try configured models in order.
metered(inner, tokens=...)Refuse a new call after the shared meter is spent.
traced(inner, tracer=...)Record model events through a tracer.
# requires: api-key
import agentsparty as ap

primary = ap.model('openai:gpt-5.6-luna', timeout=30)
model = ap.traced(
    ap.metered(
        ap.retrying(primary, attempts=2),
        tokens=8_000,
    )
)

ModelUnavailable means configuration, transport, or availability failed; ModelRefused means the provider answered but declined the request. A decode failure belongs to the message boundary, and an exhausted meter is a budget failure. Keep those cases distinct when choosing retry or fallback behaviour.

Human, HumanIo, Cli, Desk, and Offer

Use Human when a person should author a choice or payload. Cli connects that role to a console; Desk is the queue seam for a service.

Offer

An Offer represents what may be sent at a specific decision point:

FieldTypeMeaning
offer.subjectRoleThe role making the decision.
offer.receiverRoleThe role that will receive the chosen message.
offer.messagestuple[type[Message], ...]Message classes currently permitted to be sent.

An Offer is passed to HumanIo.choose(offer) and to functional models created with ap.replies(lambda offer: ...).

Desk in an async session

A Desk exposes offered message classes and accepts the selected value with submit.

import asyncio
import agentsparty as ap

Approver, Reporter = ap.roles('Approver', 'Reporter')


class Incident(ap.Message):
    """An incident report."""

    text: str


class Ship(ap.Message):
    """Ship the fix."""


desk = ap.Desk()
protocol = ap.Protocol(
    ap.send(Reporter, Approver, Incident),
    ap.choose(Approver, Reporter, {Ship: []}),
)


async def main() -> None:
    session = ap.Session(
        protocol,
        {Approver: ap.Human(desk)},
    )
    run = session.start(Reporter, Incident('database timeout'))
    while not desk.offered:
        await asyncio.sleep(0.001)
    await desk.submit(Ship())
    transcript = await run.result()


asyncio.run(main())

Use Cli for a terminal-driven program and Desk when another part of your application will answer later. Do not call submit when no decision is offered; that is a SessionError, and a class outside the offer is a SelectionError.

Machine

Machine is a service role answered by a handler table. A handler receives the request instance and returns a declared reply instance. It runs in the host process, so Machine is a protocol boundary, not a sandbox.

Handler entryMeaning
Request: handlerAccept this request class.
handler(message)Return one class currently on offer.
Missing or extra classRejected when the session binds.
import agentsparty as ap

Client, Service = ap.roles('Client', 'Service')


class Lookup(ap.Message):
    """Find a record."""

    key: str


class Found(ap.Message):
    """The record."""

    value: str


protocol = ap.Protocol(
    ap.send(Client, Service, Lookup),
    ap.send(Service, Client, Found),
)
transcript = ap.Session(
    protocol,
    {
        Client: ap.Script([Lookup('answer')]),
        Service: ap.Machine({
            Lookup: lambda message: Found(f'value:{message.key}')
        }),
    },
).run()

Policy and View

Policy is the right fit when the decision depends on prior messages or the current offer. Its function receives a View, not the machine or runtime, so a policy can be tested as a pure decision.

View members

MemberTypeWhat it returns
view.seentuple[Envelope, ...]All delivered envelopes seen so far.
view.receiverRoleThis role's own identity.
view.offeredfrozenset[type[Message]]Message classes currently on offer.
view.last(Kind)Kind | NoneNewest message of Kind in seen history.
view.sent(Kind)tuple[Kind, ...]Messages of Kind sent by this role, oldest first.
view.got(Kind)tuple[Kind, ...]Messages of Kind received by this role, oldest first.
view.count(Kind)intTotal count of messages of Kind in seen history.
import agentsparty as ap


class Draft(ap.Message):
    """A draft to review."""

    text: str


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


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

    reason: str


def decide(view: ap.View) -> ap.Message:
    draft = view.last(Draft)
    if draft is not None and len(draft.text) > 10 and Approve in view.offered:
        return Approve()
    return Reject(reason='draft is too short')


reviewer = ap.Policy(decide)

Choose Policy for a small, history-based rule. Choose Machine for a direct request-to-reply table; choose Agent when the content itself needs a model.

Script

Script is the deterministic participant for known messages, in order. It is ideal for offline tests and replay fixtures. It is not a fallback participant: when the protocol asks for one more message after the list ends, the run raises SessionError for an exhausted script.

import agentsparty as ap

Author, Editor = ap.roles('Author', 'Editor')


class Ping(ap.Message):
    """A ping."""


cast = {Author: ap.Script([Ping()]), Editor: ap.Script([])}

Binding is a contract check

The cast is not a loose bag of objects. Its keys must be exactly the roles in the protocol, and each specification must be able to perform the projected duties for that role. A missing role, an extra role, or a handler for the wrong message is rejected while Session is constructed. The service can therefore reject an invalid cast before it accepts work or starts a provider call.

Participants are factories in the cast, not long-lived runtime objects. A Session can be run twice because it binds fresh participants for each run. Do not keep mutable agent or machine state in a global object if a rerun should start clean; pass an explicit factory or create the specification with the right initial state.

Binding is the fail-closed boundary

If a role is missing, extra, or unable to perform its projected duties, fix the cast before accepting work. A later model correction cannot make an incomplete cast valid.

Choosing the smallest participant

If the role needs to…Start withWhy
Return a fixed fixtureScriptNo decision logic or provider call.
Map a request to a resultMachineThe handler table is visible and direct.
Choose from history with a rulePolicyThe decision is a pure function of View.
Write or interpret languageAgentA model fills declared content.
Wait for an operatorHumanCli and Desk provide the I/O seam.

It is common to mix them in one session: an Agent drafts, a Machine calls a local service, a Policy decides a retry, and a Human approves the final branch. They share the same projected endpoint, so the protocol remains authoritative even when the implementation changes.

Participant state and replay

Agent memory is a brief over the messages it has seen, Machine memory is whatever its handler closes over, and Policy receives a fresh View derived from the session history. Journaling records authored decisions, not arbitrary heap state. If a handler writes to an external system, replay can skip the handler only when that decision was journaled; the external write still needs an idempotency key or an application-level reconciliation path.

Stable

On this page