Files
unsupportedpastels b355fa3af4 fix(agent): turn_daybreak_surface tells a refusing verdict from none
The helper returned False both for the gateway's "not the owner's chat"
verdict and for an agent nobody judged. A /bg task binds no session, so a
delegation backend reads its source as `cli` and only the verdict can refuse
it; with False and unjudged merged, the /bg verdict this PR adds could not
refuse a Daybreak route, and a backend that refused on False would also
refuse the classic CLI and the TUI backend, whose agents are unjudged.

Return Optional[bool]: True or False when the gateway judged the turn, None
when nothing did. Every `is True` caller is unchanged. Suggestion and test
from @BearHuddleston's review.

Co-authored-by: BearHuddleston <bear@bearhuddleston.dev>
2026-11-06 19:39:21 -05:00

73 lines
4.0 KiB
Python

"""Session source vocabulary: the ``source`` label a session row carries, and which sources have a
person reading the turn as it runs. A leaf (its one import is the stdlib-only session context), so
the ``run_agent`` facade and its siblings import it directly."""
from __future__ import annotations
from typing import Any, Optional
from gateway.session_context import get_session_env
# Sources that label the human conversation an interactive UI transport hosts. A finite ``hermes chat -q`` /
# one-shot child spawned from such a session inherits HERMES_SESSION_SOURCE (the terminal tool bridges the
# session env into child processes) but is NOT that conversation: labelling it ``tui``/``desktop`` lists it
# in the TUI/WebUI pickers as a resumable chat and lets ``hermes -c`` in the TUI continue it (#112550).
# Automation sources (tool, a2a, ...) are inherited on purpose; a Kanban worker's descendants drop its
# gateway-only ``kanban`` label (delegation_context.scrub_kanban_env).
UI_TRANSPORT_SOURCES = frozenset({"tui", "desktop"})
# Finite non-interactive CLI runs (``hermes chat -q``/``--oneshot``, ``hermes -z``) get their own source so human
# pickers hide them without title/cwd heuristics; ``hermes -c`` still treats them as CLI history.
ONESHOT_SOURCE = "oneshot"
CLI_FAMILY_SOURCES = frozenset({"cli", ONESHOT_SOURCE})
# The owner's own local surfaces: the only sources that carry OpenAI Daybreak (agent.daybreak). It is the
# approved account's own access (no third-party or downstream use); a plugin session tagged ``telegram`` is
# someone else's chat even when a local backend builds its agent.
DAYBREAK_SOURCES = UI_TRANSPORT_SOURCES | {"cli"}
# A person reads the turn as it runs: not cron, batch, a messaging chat, or a one-shot run.
ATTENDED_SOURCES = UI_TRANSPORT_SOURCES | {"cli", "acp"}
def create_surface(source: str) -> str:
"""The ``session.create`` label for a session source: ``desktop`` is the Desktop's platform name for
the ``gui`` surface (gateway.session_policy.SURFACES), which the create contract does not list."""
return "gui" if source == "desktop" else source
def session_source_for(platform: Optional[str]) -> str:
source = str(get_session_env("HERMES_SESSION_SOURCE", "") or "").strip()
single_query = get_session_env("HERMES_SINGLE_QUERY_SESSION", "") == "1"
explicit = get_session_env("HERMES_SESSION_SOURCE_EXPLICIT", "") == "1"
if single_query and not explicit and source in UI_TRANSPORT_SOURCES:
source = ""
if single_query and not source and (platform or "cli") == "cli":
return ONESHOT_SOURCE
return source or platform or "cli"
def turn_daybreak_surface() -> Optional[bool]:
"""The gateway's verdict on whether the agent running this turn serves the owner's own chat, for code with no
agent in hand (a delegation backend's ``route_task``): ``True`` or ``False`` when the gateway judged it (a turn
it runs, a ``/bg`` task), ``None`` when nothing did (classic CLI, the TUI backend, cron). A gateway-hosted chat's
platform is the ``local`` transport, and a ``/bg`` task binds no session (it reads as ``cli``), so ``False``
must refuse whatever the session source says."""
from agent.subagent_lifecycle import get_active_subagent_parent
verdict = getattr(get_active_subagent_parent(), "daybreak_surface", None)
return verdict if isinstance(verdict, bool) else None
def is_attended(agent: Any) -> bool:
"""A delegated child answers its parent, not a person, though its copied context still carries
the parent's source; a library caller (no platform) has no one reading either."""
platform = getattr(agent, "platform", None)
if not platform or platform == "subagent":
return False
# A gateway-admitted ``hermes chat -q`` / ``-z`` turn carries its one-shot fact on the admission,
# not in HERMES_SINGLE_QUERY_SESSION (the daemon never had that env).
from gateway.session_finite import finite_turn_required
if finite_turn_required() is True:
return False
return session_source_for(platform) in ATTENDED_SOURCES