This page documents SummonerAgent, the main Aurora client class.
Use SummonerAgent when you want one concrete client object that can do three Aurora-specific things on top of the core SummonerClient runtime:
- host a
SummonerIdentity, - process keyed receivers with per-key serialization and optional replay protection,
- export Aurora DNA that preserves keyed receiver behavior.
The core SummonerClient API for connection lifecycle, ordinary @receive and @send handlers, hooks, flow integration, reconnection, and run configuration remains documented in the core SDK reference. This page focuses only on what summoner.aurora.agentclass adds on top.
For the broader module overview, start at Summoner.aurora.
Import the module directly as:
from summoner.aurora.agentclass import SummonerAgentNote
SummonerAgent is also re-exported from summoner.aurora. This page uses the direct module path so the reference matches the public Aurora modules.
Installation and build.txt setup are covered once in Agent Extensions.
Most applications use SummonerAgent in roughly this order:
- Construct the agent.
- Optionally attach a
SummonerIdentityif the agent needs identity, session, or envelope features. - Register keyed receivers where messages for the same logical entity must not overlap.
- Export Aurora DNA if the configured agent will later be merged or translated.
The sections below follow that same path.
The snippet below shows the smallest useful Aurora shape before the detailed method reference begins.
from summoner.aurora import SummonerAgent
agent = SummonerAgent(name="aurora:agent")
identity = agent.attach_identity(store_dir="./identity")
identity.id("id.json", password=b"strong-passphrase")
@agent.keyed_receive("account:update", key_by="account_id", seq_by="seq")
async def on_account_update(payload):
return None
aurora_dna = agent.dna()def __init__(self, name: Optional[str] = None) -> NoneConstructs a SummonerAgent and prepares both the underlying core client state and the Aurora-specific state layered on top of it.
At a high level, construction does three things:
- creates the normal
SummonerClientruntime, - initializes
agent.identitytoNone, - prepares the keyed-receiver and DNA-export state Aurora needs later.
Internal Aurora state prepared during construction
Aurora sets up the following internal fields:
self._key_mutex: Optional[AsyncKeyedMutex]starts asNoneand is created lazily when the first keyed receiver is registered.self._seq_seen: dict[tuple[str, Hashable], int]stores unbounded last-seen sequence numbers.self._seq_seen_bounded: dict[str, dict[Hashable, int]]stores bounded replay history per route whenseq_history_max_entriesis used.self._seq_seen_lru: dict[str, OrderedDict[Hashable, None]]tracks least-recently-updated keys for bounded eviction.self._seq_seen_evictions: intcounts bounded replay-cache evictions.self._dna_aurora_receivers: list[dict[str, Any]]stores the Aurora-specific receiver metadata later needed for Aurora DNA export.
- Type:
Optional[str] - Meaning: Human-readable identifier for logs and diagnostics.
- Default behavior: Delegated to the base client constructor.
Returns a SummonerAgent instance.
from summoner.aurora import SummonerAgent
agent = SummonerAgent(name="aurora:agent")identity: Optional[SummonerIdentity]SummonerAgent exposes its attached identity through the identity attribute.
This attribute is None until you call attach_identity(...). After attachment, it holds the exact SummonerIdentity instance currently hosted by the agent.
Attachment and identity-file initialization are separate steps. A newly attached identity may still need id(...) before session, envelope, peer-tracking, or continuity workflows are meaningful.
Optional[SummonerIdentity]
- Read
agent.identitywhen you want the full identity engine after attachment. - Call
agent.require_identity()when the workflow should fail immediately if no identity is attached. - Call
agent.has_identity()when you only need to test whether one is present.
identity = agent.attach_identity(store_dir="./identity")
public_id = identity.id("id.json", password=b"strong-passphrase")@staticmethod
def identity_versions() -> dict[str, str]Returns the public identity compatibility versions that SummonerAgent knows how to host.
This is mainly useful for diagnostics, audits, or compatibility logging. The returned dictionary tells you which public integration contract, controls API, identity-record format, envelope format, and built-in store formats are available through the current module build.
Returns a dictionary with the keys below:
integrationcontrolsid_recordenvelopepayload_encryptionhistory_proofsessions_storepeer_keys_storereplay_store
agent = SummonerAgent(name="aurora:agent")
versions = agent.identity_versions()
envelope_version = versions["envelope"]def attach_identity(
self,
identity: Optional[SummonerIdentity] = None,
controls: Optional[object] = None,
**summoner_identity_kwargs: Any,
) -> SummonerIdentityAttaches a SummonerIdentity to the current agent.
This is the normal way to give a SummonerAgent an active SummonerIdentity. The method supports two patterns:
- attach an existing
SummonerIdentityinstance, or - construct a new
SummonerIdentityfrom keyword arguments and attach it immediately.
If controls is provided, the method attaches those controls to the identity before storing it on agent.identity.
- Type:
Optional[SummonerIdentity] - Meaning: Existing identity object to attach.
- Default:
None
- Type:
Optional[object] - Meaning: Optional controls object to attach before storing the identity on the agent.
- Default:
None
- Type:
Any - Meaning: Keyword arguments passed to
SummonerIdentity(...)whenidentityis not supplied. - Validation: If
identityis provided, you must not also provide constructor kwargs.
Returns the attached SummonerIdentity instance.
- If both
identityandsummoner_identity_kwargsare provided,ValueErroris raised. - If
identityis provided but is not aSummonerIdentity,TypeErroris raised.
from summoner.aurora import SummonerAgent, SummonerIdentity
agent = SummonerAgent(name="aurora:agent")
identity = SummonerIdentity(store_dir="./identity")
identity.id("id.json", password=b"strong-passphrase")
agent.attach_identity(identity)from summoner.aurora import SummonerAgent
agent = SummonerAgent(name="aurora:agent")
identity = agent.attach_identity(store_dir="./identity")
identity.id("id.json", password=b"strong-passphrase")def detach_identity(self) -> Optional[SummonerIdentity]Detaches the currently hosted identity, if any, and clears agent.identity.
The detached identity object itself is returned unchanged. The method does not close the identity, reset its internal stores, or remove attached controls. It only removes the host reference from the current agent.
Returns the detached SummonerIdentity, or None if no identity was attached.
from summoner.aurora import SummonerAgent
identity = agent.detach_identity()
if identity is not None:
other = SummonerAgent(name="aurora:other")
other.attach_identity(identity)def require_identity(self) -> SummonerIdentityReturns the currently attached identity.
Use this when the workflow requires an active identity and the absence of one should be treated as an immediate error instead of optional state.
This is an attachment check only. It does not verify that id(...) has already loaded or generated the local identity file.
Returns the current SummonerIdentity.
Raises RuntimeError if no identity is attached.
identity = agent.require_identity()def has_identity(self) -> boolReturns whether an identity is currently attached to the agent.
This is a lightweight attachment check only. It returns True as soon as attach_identity(...) stores a SummonerIdentity on the agent, even if id(...) has not been called yet.
Returns True when agent.identity is populated, otherwise False.
if not agent.has_identity():
agent.attach_identity(store_dir="./identity")def keyed_receive(
self,
route: str,
key_by: Union[str, Callable[[Any], Hashable]],
priority: Union[int, tuple[int, ...]] = (),
seq_by: Union[None, str, Callable[[Any], int]] = None,
seq_history_max_entries: Optional[int] = None,
)Registers an async receiver handler that enforces two important guarantees for a given route/key pair:
- Per-key mutual exclusion within the current Python process.
- Optional replay protection based on a monotonic sequence field.
This is the Aurora feature most people reach for when one agent is responsible for many logical entities at once. It lets different keys run concurrently while still ensuring that updates for the same key do not overlap.
If seq_by is provided, the wrapper drops stale or duplicate messages whose sequence is less than or equal to the last sequence seen for that (normalized_route, key).
Registration flow inside keyed_receive(...)
At a high level, keyed_receive(...) does the following:
- Validates that
routeis a string and strips leading/trailing whitespace. - Normalizes
seq_history_max_entries. - Serializes the
key_byandseq_byextractor specifications so Aurora can preserve keyed-receiver metadata in DNA exports. - Builds a key extractor from
key_by. - Builds a sequence extractor from
seq_by. - Validates that the decorated handler is
async, accepts exactly one parameter, and matches the base receiver contract. - Normalizes
priorityinto the tuple form used by the client receiver registry. - Captures Aurora-specific DNA metadata for the receiver.
- Schedules the actual receiver registration.
The list below describes what Aurora means by "keyed" in practice.
- Lock granularity is
(normalized_route, key). - Different routes do not share locks, even if they compute the same key.
- The lock covers the entire handler body.
Aurora can also keep lightweight replay memory for each key when you supply seq_by.
- Replay protection is enabled only when
seq_byis provided and extraction returns an integer. - If
seq_history_max_entriesis not provided, the wrapper keeps last-seen state inself._seq_seenkeyed by(normalized_route, key). - If
seq_history_max_entriesis provided, the wrapper stores replay state per route inself._seq_seen_boundedand evicts least-recently-updated keys usingself._seq_seen_lru. - Any message with
seq <= last_seen_seqis dropped. - Replay state resets on process restart and can also be cleared manually with
clear_keyed_receive_replay_state(...).
When flow is enabled:
- the decorator attempts to parse the supplied
route, - if parsing succeeds,
str(parsed_route)is used as the runtime route key, - if parsing fails, the raw route is registered and a warning is logged.
Aurora is defensive by default:
- Invalid
route,key_by,seq_by,priority, orseq_history_max_entriesvalues raise during decoration or setup. - Key extraction failures drop the message instead of raising into the dispatcher.
- Missing or unhashable keys drop the message.
- Sequence extraction failures disable replay checking for that message.
- Aurora awaits the wrapped handler directly and does not add an extra catch-and-log layer around exceptions from the handler body. Exception behavior otherwise follows the base client runtime.
- Type:
str - Meaning: Logical route string, with the same semantics as
@receive. - Normalization: Leading/trailing whitespace is stripped.
- Flow: If flow is enabled, route parsing may normalize the effective route key.
- Type:
Union[str, Callable[[Any], Hashable]] - Meaning: Extractor used to compute the per-entity key.
- String behavior: Dict key (if payload is a dict) or attribute name (otherwise).
- Callable behavior: Called as
key_by(payload). - Validation: Keys must be hashable. Missing, invalid, or unhashable keys cause the message to be dropped.
- Required: If
key_byisNone,ValueErroris raised.
- Type:
Union[int, tuple[int, ...]] - Meaning: Receiver priority, identical to
@receive. - Default:
() - Normalization: If an integer is provided, it is converted to a 1-tuple
(priority,).
- Type:
Union[None, str, Callable[[Any], int]] - Meaning: Optional monotonic sequence extractor for replay protection.
- String behavior: Dict key (if payload is a dict) or attribute name (otherwise).
- Callable behavior: Called as
seq_by(payload). - Conversion: Best-effort
int(...). Failures yieldNonefor that message, disabling replay checking for it. - State: Stored in-memory and resets on process restart.
- Type:
Optional[int] - Meaning: Optional per-route cap for replay history when
seq_byis enabled. - Default:
None - Behavior: When omitted, replay state grows in an unbounded table keyed by
(route, key). When provided, Aurora keeps only the most recently updated keys for each route. - Validation: Must be
Noneor a positive integer.boolvalues are rejected. Providing it withoutseq_byraisesValueError.
Returns a decorator. The decorated function is registered as a receiver handler, but the internal dispatcher executes a wrapped function that applies locking and optional replay logic.
The decorated handler must:
- be
async - accept exactly one argument (the payload)
For normal receiver code, Aurora recommends the same return shape used in the core SDK: Optional[Event] or None.
Decoration-time validation is looser than that recommendation:
- explicit
Anyreturn annotations are accepted, - a missing return annotation only triggers a warning.
If the function is not async, TypeError is raised at decoration time.
If the function does not accept exactly one argument, TypeError is also raised at decoration time.
- Critical-section scope: The lock covers the entire handler body. Keep work inside the lock short and move slow I/O out of the critical section when possible.
- Fairness: The underlying lock is not strictly FIFO.
- Process scope: Mutual exclusion is per Python process and event loop. It does not serialize work across processes or machines.
- Unbounded replay state: If you use
seq_bywithoutseq_history_max_entries, replay state grows with the number of distinct(route, key)pairs seen during the process lifetime. - Bounded replay state: If you set
seq_history_max_entries, Aurora evicts the least-recently-updated keys per route. An evicted key loses replay memory and is treated as unseen if it appears again later.
from summoner.aurora import SummonerAgent
agent = SummonerAgent(name="aurora:agent")
@agent.keyed_receive("game:move", key_by="player_id", priority=0)
async def on_move(payload):
# Only one move per (route, player_id) runs at a time.
return Nonefrom summoner.aurora import SummonerAgent
agent = SummonerAgent(name="aurora:agent")
@agent.keyed_receive("account:update", key_by="account_id", seq_by="seq")
async def on_account_update(payload):
# Drops any payload with seq <= last seen for this (route, account_id).
return Nonefrom summoner.aurora import SummonerAgent
agent = SummonerAgent(name="aurora:agent")
@agent.keyed_receive(
"account:update",
key_by="account_id",
seq_by="seq",
seq_history_max_entries=10_000,
)
async def on_account_update(payload):
return Nonefrom summoner.aurora import SummonerAgent
agent = SummonerAgent(name="aurora:agent")
@agent.keyed_receive(
"player:event",
key_by=lambda p: (p["zone_id"], p["player"]["id"]),
seq_by=lambda p: int(p["meta"]["ts_ns"]),
)
async def on_player_event(payload):
return Nonedef clear_keyed_receive_replay_state(
self,
route: Optional[str] = None,
) -> NoneClears replay-protection state accumulated by @keyed_receive(...).
- If
routeisNone, Aurora clears all unbounded and bounded replay tables. - If
routeis provided, Aurora strips surrounding whitespace and clears replay entries only for that normalized route. - This is useful in test harnesses, process-local resets, or controlled operational workflows where you intentionally want to forget last-seen sequences.
- The cumulative eviction counter reported by
keyed_receive_replay_stats()is not reset by this method.
- Type:
Optional[str] - Meaning: Optional route whose replay state should be cleared.
- Default:
None(clear all routes)
Returns None.
agent.clear_keyed_receive_replay_state("account:update")
agent.clear_keyed_receive_replay_state()def keyed_receive_replay_stats(self) -> dict[str, int]Returns a lightweight summary of the replay caches currently maintained by Aurora across both unbounded and bounded modes.
This method is useful when you want to confirm that replay memory is staying within operational expectations or when you are debugging a keyed-receiver deployment.
The routes and entries counts reflect current in-memory replay state. The evictions value is cumulative since process start, so it can remain non-zero even after clear_keyed_receive_replay_state(...).
Returns a dictionary with:
routes: number of routes that currently have replay entriesentries: total number of replay entries across all internal replay tablesevictions: total number of bounded replay entries evicted since process start
stats = agent.keyed_receive_replay_stats()A typical result looks like {"routes": 2, "entries": 145, "evictions": 12}.
def core_dna(
self,
include_context: bool = False,
*,
allow_lossy: bool = False,
) -> strExports a core SDK DNA JSON string.
This method is intentionally strict when Aurora keyed receivers are present:
- If the agent has no Aurora keyed receivers,
core_dna(...)behaves like the normal core client DNA export. - If the agent has Aurora keyed receivers and
allow_lossy=False, Aurora raisesRuntimeErrorinstead of silently dropping keyed semantics. - If the agent has Aurora keyed receivers and
allow_lossy=True, Aurora downgrades each keyed receiver into a plainreceiveentry.
That lossy downgrade preserves the handler source metadata and route/priority information, but it does not preserve:
- per-key mutual exclusion,
- replay filtering via
seq_by, - bounded replay-table settings such as
seq_history_max_entries.
Use this method only when the tool reading the DNA expects core DNA or when you intentionally want Aurora behavior stripped away.
- Type:
bool - Meaning: Whether to include context entries in the exported DNA.
- Default:
False
- Type:
bool - Meaning: Whether Aurora may downgrade keyed receivers into plain
receiveentries when exporting core DNA. - Default:
False
Returns a JSON string representing core DNA.
- If keyed receivers exist and
allow_lossy=False,RuntimeErroris raised.
def aurora_dna(self, include_context: bool = False) -> strExports an Aurora DNA JSON string that preserves Aurora keyed-receiver behavior.
For each keyed receiver, Aurora writes an aurora:keyed_receive entry containing:
- the route and priority,
- serialized
key_byandseq_byextractor specifications, - optional
seq_history_max_entries, - handler source metadata used for replay.
Use this method when the DNA may later be replayed by Aurora-aware tools such as Agent Merger and Translation, or whenever keyed receiver semantics must survive export/import intact.
- Type:
bool - Meaning: Whether to include context entries in the exported DNA.
- Default:
False
Returns a JSON string representing Aurora DNA.
def dna(
self,
include_context: bool = False,
*,
flavor: str = "aurora",
allow_lossy: bool = False,
) -> strAurora overrides the base dna(...) method with a flavor parameter.
flavor="aurora"callsaurora_dna(...).flavor="core"callscore_dna(..., allow_lossy=allow_lossy).
The most important public behavior change is that Aurora defaults to flavor="aurora", so plain agent.dna() exports Aurora DNA rather than core DNA.
- Use
agent.dna()oragent.aurora_dna()when keyed receiver semantics must survive export/import. - Use
agent.core_dna(..., allow_lossy=True)only when a downstream consumer expects core DNA and you accept the loss of keyed locking and replay semantics. - If you plan to use Agent Merger and Translation, prefer Aurora DNA.
- Type:
bool - Meaning: Whether to include context entries in the exported DNA.
- Default:
False
- Type:
str - Meaning: Export flavor selector.
- Allowed values:
"aurora"or"core" - Default:
"aurora"
- Type:
bool - Meaning: Passed through to
core_dna(...)whenflavor="core". - Default:
False
Returns a JSON string representing the selected DNA flavor.
- Unknown
flavorvalues raiseValueError. flavor="core"may still raiseRuntimeErrorif Aurora keyed receivers exist andallow_lossy=False.
« Previous: Summoner.aurora | Next: Summoner.aurora.agentmerger »