This page documents the protocol-layer data structures used by Summoner clients and flows.
The module defines:
Node: a token model used as a gate (matching incoming state).ArrowStyleandParsedRoute: a parsed representation of routes (objects or arrows).Sender,Receiver,Direction: protocol wrappers used by the client runtime.StateTape: an in-memory tape of active states, plus activation discovery.ClientIntent: a small enum used by the client lifecycle (quit vs travel vs abort).
class NodeRepresents a token used as either:
- a gate (pattern) in a route source, or
- a state stored on a
StateTape.
A Node is created from a single expression string and is normalized into:
kindin{ "plain", "all", "not", "oneof" }valuesastuple[str]orNone
Accepted syntaxes:
- Plain token:
foo,ok_1,StateX - All wildcard:
/all - Negation set:
/not(a,b,c) - One-of set:
/oneof(a,b,c)
Invalid syntax raises ValueError.
- Type:
str - Meaning: Token expression that defines the node.
A Node instance.
from summoner.protocol.process import Node
Node("OK")
Node("/all")
Node("/not(minor,major)")
Node("/oneof(a,b,c)")def accepts(self, state: Node) -> boolChecks whether this node (the gate) accepts another node (the state).
This is used when matching a parsed route's source nodes (gates) against states stored on a StateTape.
Key cases:
/allaccepts any state.- A plain gate matches plain state if names match.
- A plain gate accepts
/oneof(...)if the gate token is in the set. - A plain gate accepts
/not(...)if the gate token is not in the forbidden set. - A
/oneof(...)gate accepts a plain state if the state token is in the set. - A
/oneof(...)gate accepts another/oneof(...)if the sets intersect. - A
/not(...)gate accepts a plain state if the state token is not in the forbidden set.
If state is not a Node, raises TypeError.
- Type:
Node - Meaning: Concrete state node to test against this gate.
- Type:
bool - Meaning:
Trueif accepted, elseFalse.
from summoner.protocol.process import Node
gate = Node("/oneof(a,b)")
assert gate.accepts(Node("a")) is True
assert gate.accepts(Node("c")) is False
gate = Node("x")
assert gate.accepts(Node("/not(a,b)")) is True
assert gate.accepts(Node("/not(x,y)")) is Falseclass ArrowStyleDescribes the syntax used to render an "arrow route" string. It defines:
stem: single character used for the arrow shaft (example-)brackets: label delimiters (example("[", "]"))separator: token separator inside segments (example",")tip: arrow terminator (example">")
The constructor validates:
stemis exactly 1 character.- all parts are non-empty strings.
- no part overlaps another (substring conflicts).
separatordoes not contain reserved characters used by the parser or style.- style parts are safe to use with
re.escape.
- Type:
str - Meaning: One-character arrow shaft marker.
- Type:
tuple[str, str] - Meaning: Left and right bracket strings for the label part.
- Type:
str - Meaning: Separator for multiple tokens within a segment.
- Type:
str - Meaning: Arrow head or terminator string.
An ArrowStyle instance.
from summoner.protocol.process import ArrowStyle
style = ArrowStyle(
stem="-",
brackets=("[", "]"),
separator=",",
tip=">",
)class ParsedRouteRepresents a parsed route in a structured form:
source: tuple of gateNodeslabel: tuple of labelNodestarget: tuple of targetNodesstyle: optionalArrowStyleused for rendering
The string form of the route is precomputed and used for equality and hashing.
Interpretation:
- If the route has
targetorlabel, it is treated as an arrow route. - If it has no label and no target, it is treated as an object route (only source nodes).
- Type:
tuple[Node, ...]
- Type:
tuple[Node, ...]
- Type:
tuple[Node, ...]
- Type:
Optional[ArrowStyle]
A ParsedRoute instance.
from summoner.protocol.process import Node, ParsedRoute, ArrowStyle
style = ArrowStyle("-", ("[", "]"), ",", ">")
r = ParsedRoute(
source=(Node("A"),),
label=(Node("L"),),
target=(Node("B"),),
style=style,
)
assert r.is_arrow is True
assert r.has_label is True@property
def has_label(self) -> boolReturns True when the route has at least one label node.
bool
@property
def is_arrow(self) -> boolReturns True when the route has a target or a label. This indicates an arrow route.
bool
@property
def is_object(self) -> boolReturns True when the route is not an arrow route. This indicates an object route.
bool
@property
def is_initial(self) -> boolReturns True for arrow routes that have no source gates. These are "initial" routes that can fire without matching tape state.
bool
def activated_nodes(self, event: Optional[Event]) -> tuple[Node, ...]Given an event (typically one of Action.MOVE, Action.STAY, Action.TEST), returns the nodes that should be added to the tape.
Rules:
-
Object route (
is_object):Action.TEST: activates nothing- any other
Event: activatessource
-
Arrow route (
is_arrow):Action.MOVE: activateslabel + targetAction.TEST: activateslabelAction.STAY: activatessource
-
Any other input returns
().
- Type:
Optional[Event] - Meaning: The event produced by a receiver or state machine step.
- Type:
tuple[Node, ...] - Meaning: Nodes to be appended to a
StateTape.
from summoner.protocol.process import Node, ParsedRoute, ArrowStyle
from summoner.protocol.triggers import Action, Signal
style = ArrowStyle("-", ("[", "]"), ",", ">")
r = ParsedRoute(
source=(Node("A"),),
label=(Node("L"),),
target=(Node("B"),),
style=style,
)
sig = Signal((0,), "OK")
assert r.activated_nodes(Action.MOVE(sig)) == (Node("L"), Node("B"))
assert r.activated_nodes(Action.TEST(sig)) == (Node("L"),)
assert r.activated_nodes(Action.STAY(sig)) == (Node("A"),)@dataclass(frozen=True, init=False)
class SenderRepresents the frozen runtime record behind a registered sender.
Read the fields in four small groups:
- Emission shape:
fnis the async callable, andmultitells the runtime whetherfnreturns one payload or a list. - Reactivity:
actionsandtriggersdecide which events can wake the sender. - Payload delivery:
use_data,data_mode, andwhen_datadescribe whether matchedEvent.datais passed through, how it is delivered, and whether it should be admitted at all. - Scheduling:
everyandrun_whiledescribe timed sender behavior, whileregistration_idgives the scheduler a stable runtime identifier.
Senders are "reactive" when actions or triggers is set. The runtime calls responds_to(event) to decide whether a sender should run for a given event.
The Sender record also carries the metadata needed for data-aware and timed senders. After an event matches, the runtime may apply when_data to the delivered payload before invoking a reactive use_data=True sender.
In normal SDK usage, you rarely instantiate this class yourself. It is usually created by SummonerClient.send(...) or replayed by merger/translation tools.
fn:Callable[..., Awaitable]multi:boolactions:Optional[set[Type]]triggers:Optional[set[Signal]]use_data:booldata_mode:Optional[str]when_data:Anyevery:Optional[float]run_while:Anyregistration_id:Optional[str]
from summoner.client import SummonerClient
from summoner.protocol import Action
client = SummonerClient(name="summoner:client")
client.flow().activate()
@client.send(
route="chat_send",
on_actions={Action.MOVE},
use_data=True,
when_data=lambda data: data.get("ready") is True,
)
async def after_move(data):
return data
# The decorator above eventually becomes a Sender record carrying:
# fn, actions, use_data, when_data, and the other scheduler metadata.def responds_to(self, event: Any) -> boolChecks whether event satisfies the sender's action and trigger filters:
- If
actionsis set:eventmust be an instance of at least one class inactions. - If
triggersis set:extract_signal(event)must equal at least one signal intriggers.
If a filter is None, it is treated as "no constraint".
- Type:
Any - Meaning: An event-like object produced by receiver logic, typically an
EventorSignal.
bool
@dataclass(frozen=True)
class ReceiverRepresents a receiving handler registered by a client:
fn: async callable that consumes a payload and returns an optionalEventpriority: a tuple used for batch ordering
fn:Callable[[Union[str, dict]], Awaitable[Optional[Event]]]priority:tuple[int, ...]
from summoner.protocol.process import Receiver
# In SDK usage, Receiver is usually created internally by SummonerClient decorators.class Direction(Enum)Indicates whether a hook or handler belongs to the sending side or receiving side.
Values:
Direction.SENDDirection.RECEIVE
from summoner.protocol.process import Direction
assert Direction.SEND.name == "SEND"@dataclass(frozen=True)
class TapeActivationRepresents one activation of a receiver due to a tape match.
Fields capture:
key: the tape key that matched (orNonefor initial routes)state: the concrete tape state that matched (orNonefor initial routes)route: the parsed route that matchedfn: the receiver function to execute
key:Optional[str]state:Optional[Node]route:ParsedRoutefn:Callable[[Any], Awaitable]
class TapeType(Enum)Internal classification of tape input/output shapes:
SINGLE: a single stateMANY: a list of statesINDEX_SINGLE: dict key → single stateINDEX_MANY: dict key → list of states
This type is inferred by StateTape on construction and influences revert().
class StateTapeStores active states as a mapping:
- internal storage:
dict[str, list[Node]] - keys are prefixed by default with
tape:(configurable bywith_prefix)
Accepted constructor inputs:
Noneor unrecognized: creates an empty index-many tape.strorNode: treated asSINGLE.list[str|Node]ortuple[str|Node]: treated asMANY.dict[key -> str|Node]: treated asINDEX_SINGLE.dict[key -> (str|Node|list|tuple)]: treated asINDEX_MANY.
The tape is used by flow-enabled clients to decide which receivers should run.
- Type:
Any - Meaning: Initial tape content, in one of the accepted shapes.
- Type:
bool - Meaning: Whether to prefix keys with
tape:when building internal storage. - Default:
True
A StateTape instance.
from summoner.protocol.process import StateTape, Node
t1 = StateTape("A") # SINGLE
t2 = StateTape(["A", "B"]) # MANY
t3 = StateTape({"x": "A"}) # INDEX_SINGLE
t4 = StateTape({"x": ["A", Node("B")]}) # INDEX_MANYdef extend(self, states: Any) -> NoneExtends the tape with additional states.
The method constructs a local StateTape from states (without adding prefixes), then merges:
- missing keys are created
- nodes are appended to existing lists
- Type:
Any - Meaning: Additional tape content in any accepted constructor shape.
Returns None.
from summoner.protocol.process import StateTape
tape = StateTape({"x": ["A"]})
tape.extend({"x": ["B"], "y": ["C"]})def refresh(self) -> StateTapeCreates a fresh tape with the same set of keys but empty node lists. The returned tape keeps the same inferred tape type.
This is commonly used to compute the "next tape" in a flow step.
A new StateTape instance.
from summoner.protocol.process import StateTape
tape = StateTape({"x": ["A"], "y": ["B"]})
fresh = tape.refresh()
assert fresh.revert() == {"x": [], "y": []}def revert(self) -> Union[list[Node], dict[str, list[Node]], None]Converts internal tape storage back into an external representation:
- For
SINGLEandMANY: returns a single flattenedlist[Node]. - For index types: returns
dict[str, list[Node]]with prefixes removed.
list[Node]ordict[str, list[Node]]depending on tape type.
from summoner.protocol.process import StateTape
assert StateTape("A").revert() == [StateTape("A").revert()[0]]
assert isinstance(StateTape({"x": "A"}).revert(), dict)def collect_activations(
self,
receiver_index: dict[str, Receiver],
parsed_routes: dict[str, ParsedRoute],
) -> dict[tuple[int, ...], list[TapeActivation]]Computes which receivers should run, based on current tape states and parsed route gates.
For each route -> receiver in receiver_index:
-
Looks up
ParsedRouteinparsed_routes. If missing, skips it. -
If the route is initial (
parsed_route.is_initial), it activates once unconditionally. -
Otherwise, for every
(key, state)on the tape, checks whether any gate inparsed_route.sourceaccepts the state.- If a gate matches, produces a
TapeActivation(key, state, parsed_route, receiver.fn).
- If a gate matches, produces a
Returned activations are indexed by receiver priority (receiver.priority).
- Type:
dict[str, Receiver] - Meaning: Receiver registry keyed by route string.
- Type:
dict[str, ParsedRoute] - Meaning: Parsed routes keyed by normalized route string.
- Type:
dict[tuple[int, ...], list[TapeActivation]] - Meaning: Activations grouped by priority.
from summoner.protocol.process import StateTape, Receiver, ParsedRoute, Node, ArrowStyle
async def recv(payload):
return None
receiver_index = {"A": Receiver(fn=recv, priority=())}
parsed_routes = {"A": ParsedRoute(source=(Node("A"),), label=(), target=(), style=None)}
tape = StateTape(["A", "B"])
acts = tape.collect_activations(receiver_index=receiver_index, parsed_routes=parsed_routes)
assert () in acts
assert len(acts[()]) >= 1class ClientIntent(Enum)Represents the client's lifecycle intent:
QUIT: immediate exitTRAVEL: reconnect to a new host/portABORT: abort due to error
This enum is typically produced by the client runtime logic, not by protocol parsing.
from summoner.protocol.process import ClientIntent
assert ClientIntent.TRAVEL.name == "TRAVEL"
« Previous: Summoner.protocol.triggers | Next: Summoner.protocol.flow »