Event model
Status: Implemented. See ADR-001 and ADR-003.
Domain events
Framework events are lightweight immutable Python values. Focused optional interaction metadata lives on a keyword-only base:
@dataclass(frozen=True, slots=True, kw_only=True)
class Event:
event_type: str = "event"
raw: object = field(default=None, repr=False, compare=False)
event_time: datetime | None = None
actor: UserRef | None = None
space: SpaceRef | None = None
thread: ThreadRef | None = None
dialog: DialogMetadata | None = None
The implemented hierarchy is:
Event(event_type="event", raw=None)— generic synthetic event.MessageEvent(text="", message=None, raw=None)— normalizedMESSAGE.ActionEvent(name="", parameters={}, form_inputs=..., source=...)—CARD_CLICKEDand existing synthetic actions;ActionSourceisMESSAGE,DIALOG, orHOMEonly when the wire proves that surface.AddedToSpaceEventandRemovedFromSpaceEvent— lifecycle interactions.WidgetUpdatedEvent— dynamic widget input/action data.CommandEvent(command_id=..., kind=...)— typed slash, quick, and message action command families without an invented friendly name. Message actions also preserve the targetMessageRef.AppHomeEventandFormSubmitEvent— App Home initialization/submission.UnknownEvent(original_type="", raw=None)— forward-compatible unknown.ErrorEvent(source_event=..., exception=...)— framework error-routing value withevent_type == "error".
Constructors remain keyword-only. UserRef, SpaceRef, ThreadRef, and
MessageRef deliberately expose only stable basic identifiers/metadata, not
full Google resource models. A parsed ThreadRef also retains its already-known
parent SpaceRef, which enables a zero-fetch thread.send(). No event contains
credentials, transport state, response capabilities, or Google client objects;
the dispatcher exposes its configured Bot only through request-local execution
context.
MessageEvent exposes deep immutable snapshots of stable read-side Google
message fields through attachments, annotations, mentions, quote, and
reaction_summaries, plus the normalized booleans is_private and
is_silent. They read the existing .raw snapshot and never perform network
I/O. No attachment or reaction write service is implied.
Commands and action surfaces
CommandKind normalizes Google's two documented wire families:
- slash commands:
MESSAGEplusmessage.slashCommand→SLASH_COMMAND; - quick commands:
APP_COMMANDplusappCommandMetadata→QUICK_COMMAND; - message actions: the same
APP_COMMANDenvelope →MESSAGE_ACTION(generally available, with no preview enrollment required).
The compatibility source_kind string remains available for callers
and unknown/mismatched values. @router.command remains the shared observer;
slash_command, quick_command, and message_action
observers make kind-specific routing explicit.
ActionSource is evidence-based: dialog metadata proves DIALOG, the wrapped
App Home chat envelope proves HOME, and a clicked message proves MESSAGE.
It stays None when the payload cannot prove the surface.
Immutability
The dataclass instances are frozen and slotted. Parameter/form mappings take immutable shallow snapshots. Synthetic callers can still supply an opaque raw object. The Google adapter supplies a deep snapshot of the complete decoded mapping. Raw remains opaque and can itself be mutable; it is excluded from repr and equality.
Unknown events
The adapter preserves a future external type without making it fatal:
event = UnknownEvent(
original_type="SOME_FUTURE_EVENT",
raw={"type": "SOME_FUTURE_EVENT"},
)
The unknown_event observer receives it before the generic event fallback.
Unknown type and malformed payload are separate outcomes.
Adapter boundary
Pydantic validates untrusted data only inside the interaction adapter. Known
external types with malformed required content raise a public parser error;
unknown strings map to UnknownEvent. No event ID is added because Google
interactions do not document a universal one.