Reactions / Emotes — a system-wide orchestrator output¶
Status: core + messaging renderer done (this slice). The other frontend renderers (webui / terminal-ui / geniusbot /
agent_server.py) are specified here as a thin contract for their (separate) repos. CONCEPT:AU-ECO.reactions.emitted-alongside-reply / AU-ECO.reactions.one-emote-registry-governance / ECO-4.81.
Reactions used to be a messaging-only feature: the instinctive-reaction heuristic, the
available-emoji menu, and the Telegram setMessageReaction call all lived inside
messaging/ (CONCEPT:AU-ECO.messaging.messaging-renderer-core-reaction). That made "react with 👍" impossible for any other surface
to inherit — exactly the per-surface sprawl the
Universal capability — ONE core, thin entrypoints rule forbids.
This change promotes reactions to a first-class output of the universal orchestrator, so every entrypoint inherits it natively and renders it for its medium.
The core (built once, in agent_utilities/orchestration/reactions.py)¶
flowchart LR
turn["Agent turn / inbound message"] --> decide["decide_reaction()\n(model-agnostic heuristic)"]
decide --> reg["EmoteRegistry\n(menu + ActionPolicy governance)"]
reg --> ar["AgentReaction\n{emote, target_message_id?, intensity?}"]
ar --> tg["Telegram renderer\nsend_reaction / setMessageReaction"]
ar -.-> web["agent-webui\nreaction chip"]
ar -.-> tui["agent-terminal-ui\nemote glyph"]
ar -.-> gb["geniusbot\ndesktop affordance"]
ar -.-> api["agent_server.py\nresponse.reaction field"]
| Piece | Concept | What it is |
|---|---|---|
AgentReaction |
ECO-4.79 | The structured output a turn emits: {emote, target_message_id?, intensity?}. Optional + lightweight — no reaction ⇒ None. to_dict() / from_dict() serialize it for envelopes and renderers. |
EmoteRegistry |
AU-ECO.reactions.one-emote-registry-governance | The one menu of available emotes + the governance gate (allows(emote, actor, context)), reusing the ActionPolicy decision point (reaction kind). No per-surface emote list. |
decide_reaction() |
ECO-4.79 | The instinctive, model-agnostic decision (a tool-free completion, bounded to 10 s) — moved out of messaging/router.py so every entrypoint shares one heuristic. Opt out with REACTIONS=0 (legacy MESSAGING_REACTIONS=0 still honored). |
The renderer contract (the ONLY per-surface code)¶
A renderer is a function that takes a core AgentReaction and paints it for its medium. It
contains no decision logic — the orchestrator already decided.
async def render_reaction(reaction: AgentReaction, *, context) -> bool:
"""Paint reaction.emote on this medium; return True if rendered."""
The reaction is delivered to a renderer as the dict form (AgentReaction.to_dict()), which is
stable across process / repo boundaries:
| Entrypoint | Status | What it implements |
|---|---|---|
| messaging (Telegram, …) | ✅ done | MessagingService.render_reaction(platform, channel_id, reaction) → react() → backend send_reaction → Telegram setMessageReaction. The router's _react_in_background now calls the core decide_reaction and renders the result (CONCEPT:AU-ECO.messaging.messaging-as-renderer). Other backends (Slack reactions.add, …) expose send_reaction and degrade gracefully where the emote is unsupported. |
agent-webui |
▢ stub (separate repo) | Render reaction.emote as an emoji reaction chip on the assistant message; map intensity to chip emphasis if present. Read the reaction field off the turn's response (below). No emote list of its own — the menu is EmoteRegistry.available(). |
agent-terminal-ui |
▢ stub (separate repo) | Render an inline emote glyph / reaction line next to the turn (e.g. a dim 👀 suffix). target_message_id is usually None here (standalone glyph). |
geniusbot |
▢ stub (separate repo) | Surface a desktop reaction affordance (a small emoji badge on the message bubble / a toast). |
agents/*/…/agent_server.py |
▢ stub (separate repo) | Add an optional reaction field to the A2A/HTTP response envelope carrying AgentReaction.to_dict(); clients (webui/geniusbot) render it. The orchestrator populates it when a turn reacts. |
Rules every renderer follows¶
- No decision, no menu. The renderer never decides whether or which to react and
never hard-codes an emote list — it renders what the core produced and reads the menu from
EmoteRegistry.available(). - Degrade, never error. An emote a medium can't render maps to its nearest supported one or is dropped. A reaction is cosmetic — it must never block or fail the actual turn.
- Governance is the core's. Whether a principal may react is decided by
EmoteRegistry.allows(...)(ActionPolicyreactionkind) — the renderer does not re-check permissions.
Definition of done¶
A new emote, a new governance rule, or a change to the reaction heuristic lands in one
place (orchestration/reactions.py) and shows up correctly on chat, web, terminal,
desktop, and API with no per-surface change. If a reaction change means editing N
entrypoints, it's in the wrong layer.
Follow-ups (separate repos — not in this slice)¶
agent-webui: reaction chip component + readreactionoff the response envelope.agent-terminal-ui: inline emote glyph renderer.geniusbot: desktop reaction affordance.agents/*(agent_server.py): add thereactionresponse-envelope field + have the orchestrator populate it when a turn reacts (the field is defined here; the wiring into the A2A response shape lives in those packages).