Files
hermes-agent/agent/turn_author.py
Teknium a42388b3a9 feat(agent): a2a_key names a bot author's turns
Dropped from the #103888 salvage for lack of a consumer; the honcho a2a
session key is that consumer.
2026-09-10 10:45:57 -07:00

104 lines
4.2 KiB
Python

"""Who wrote the user side of a turn, carried from the dispatcher into the recipient's turn.
The dispatcher sets ``HERMES_TURN_AUTHOR`` on the recipient's one-shot subprocess only. A cached
gateway agent sees several authors over its lifetime, so the author is read per turn, never per agent.
"""
from __future__ import annotations
import json
import os
import socket
import unicodedata
from typing import Any, Dict, Mapping, MutableMapping, Optional
TURN_AUTHOR_ENV = "HERMES_TURN_AUTHOR"
_MAX_FIELD_LEN = 200
_DROPPED_CATEGORIES = frozenset({"Cc", "Cs", "Cn", "Co"})
_TRUTHY = frozenset({"true", "1", "yes"})
def _clean_text(value: Any) -> Optional[str]:
"""Strip whitespace, control and unassigned characters, then cap the length. None when nothing is left.
Format characters and non-breaking spaces stay so emoji sequences and display names survive."""
if not isinstance(value, str):
return None
text = "".join(ch for ch in value if unicodedata.category(ch) not in _DROPPED_CATEGORIES).strip()
if not text:
return None
return text[:_MAX_FIELD_LEN]
def _bot_flag(value: Any) -> bool:
if isinstance(value, str):
return value.strip().lower() in _TRUTHY
return isinstance(value, (bool, int)) and bool(value)
def bot_author_id(profile: str, origin: Optional[str] = None) -> str:
"""``bot:<profile>`` for a bot on the recipient's own install, ``bot:<origin>/<profile>`` for one on another
install. The origin is the Desktop's connection id on a relayed dm and the sender's hostname on a peer dm."""
origin = (origin or "").strip()
return f"bot:{origin}/{profile}" if origin else f"bot:{profile}"
def local_origin() -> str:
"""This machine's hostname as an author-id origin, cleaned like any author field. Empty when unknown."""
try:
host = socket.gethostname()
except Exception:
return ""
# A slash would split the id into a different origin and profile at parse time.
return (_clean_text(host) or "").replace("/", "")
def parse_turn_author(raw: Any) -> Optional[Dict[str, Any]]:
"""Normalize a dict or JSON string into ``{"id", "name", "is_bot"}``; None for anything else or without id and name.
The id is whatever the transport knows the sender by: ``bot:<profile>`` on a bot-mode delivery inside one
install, ``bot:<connection>/<profile>`` when the Desktop relayed it from another machine,
``bot:<hostname>/<profile>`` on a peer dm, the platform user id elsewhere.
An ``origin`` field qualifies a bare ``bot:<profile>`` id the same way."""
try:
if isinstance(raw, (str, bytes)):
raw = json.loads(raw)
if not isinstance(raw, Mapping):
return None
author = {
"id": _clean_text(raw.get("id")),
"name": _clean_text(raw.get("name")),
"is_bot": _bot_flag(raw.get("is_bot")),
}
if author["id"] is None and author["name"] is None:
return None
origin = _clean_text(raw.get("origin"))
if origin and author["id"] and author["id"].startswith("bot:") and "/" not in author["id"]:
author["id"] = _clean_text(bot_author_id(author["id"][len("bot:"):], origin))
return author
except Exception:
return None
def turn_author_from_env(environ: Mapping[str, str] = os.environ) -> Optional[Dict[str, Any]]:
"""The author the dispatcher placed in ``HERMES_TURN_AUTHOR``, or None."""
return parse_turn_author(environ.get(TURN_AUTHOR_ENV))
def take_turn_author_from_env(environ: MutableMapping[str, str] = os.environ) -> Optional[Dict[str, Any]]:
"""Read and remove ``HERMES_TURN_AUTHOR`` so subprocesses started during the turn do not inherit it."""
return parse_turn_author(environ.pop(TURN_AUTHOR_ENV, None))
def turn_author_env(author: Dict[str, Any]) -> Dict[str, str]:
"""The environment entry a dispatcher merges into a child's env."""
return {TURN_AUTHOR_ENV: json.dumps(author, separators=(",", ":"))}
def a2a_key(author: Optional[Dict[str, Any]]) -> Optional[str]:
"""``a2a:<bot id>``, the shared name for a bot author's turns. None for a human or an id-less bot."""
if not isinstance(author, dict) or not author.get("is_bot") or not author.get("id"):
return None
return f"a2a:{author['id']}"