mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
refactor(relay): drop the flat_dm_status knob — liveliness is unconditional; add relay docs page
flat_dm_status was speculative config (rubric violation): no user wants 'make my agent look dead', and the only real consumer of status suppression was native's placement-contamination guard — which the relay lane handles structurally (QA-6/7 send-side anchor strip, leak-guard test), not via preference. Status now anchors whenever an inbound ts exists, in both modes. Docs: new website/docs/user-guide/messaging/relay.md — enterprise-only relay lane page documenting the platforms.relay.extra.<platform> subset shape (nested wins, flat fallback), the Slack reply_in_thread control, and always-on liveliness. Kept out of the native slack.md on purpose: relay controls are not Slack config.
This commit is contained in:
parent
9864e00fb4
commit
85a75f3155
3 changed files with 72 additions and 39 deletions
|
|
@ -311,24 +311,6 @@ class RelayAdapter(BasePlatformAdapter):
|
|||
except Exception: # noqa: BLE001 - config shape is operator-owned
|
||||
return True
|
||||
|
||||
def _flat_dm_status_enabled(self) -> bool:
|
||||
"""Liveliness in flat-DM mode: anchor the STATUS (not the reply) to the
|
||||
triggering message's ts.
|
||||
|
||||
``assistant.threads.setStatus`` on a message ts renders "… thinking" in
|
||||
that message's thread-footer space and vanishes on clear — no message
|
||||
artifact. Native suppresses this in flat mode because ITS response
|
||||
routing could inherit the activated thread; the relay lane's sends are
|
||||
explicitly flat in flat mode (QA-6/7 anchor strip), so the status
|
||||
anchor cannot leak into reply placement here. Default ON — flat DMs
|
||||
get a live status billboard while replies still post at the DM root.
|
||||
Opt out: platforms.relay.extra.slack.flat_dm_status: false.
|
||||
"""
|
||||
try:
|
||||
return bool(self._relay_slack_extra().get("flat_dm_status", True))
|
||||
except Exception: # noqa: BLE001 - config shape is operator-owned
|
||||
return True
|
||||
|
||||
def _stamp_slack_session_thread(self, event) -> None:
|
||||
"""Native session-keying parity for fronted Slack (QA-3).
|
||||
|
||||
|
|
@ -1041,17 +1023,15 @@ class RelayAdapter(BasePlatformAdapter):
|
|||
and self._platform_by_chat.get(str(chat_id)) == Platform.SLACK.value
|
||||
and self._chat_type_by_chat.get(str(chat_id)) == "dm"
|
||||
):
|
||||
anchor = self._last_inbound_ts_by_chat.get(str(chat_id))
|
||||
# Thread mode: status targets the per-message thread (QA-1).
|
||||
# Flat mode: the status can STILL anchor to the triggering ts —
|
||||
# Flat mode: the status STILL anchors to the triggering ts —
|
||||
# setStatus renders in the footer space and clears without a
|
||||
# message artifact, and flat sends strip their anchors (QA-6/7)
|
||||
# so reply placement cannot inherit it. Gated separately
|
||||
# (flat_dm_status, default on) for a clean opt-out.
|
||||
if anchor and (
|
||||
self._effective_reply_in_thread()
|
||||
or self._flat_dm_status_enabled()
|
||||
):
|
||||
# so reply placement cannot inherit it. Unconditional: liveliness
|
||||
# is not a preference, it ships in whatever form the mode
|
||||
# supports (no speculative opt-out knob).
|
||||
anchor = self._last_inbound_ts_by_chat.get(str(chat_id))
|
||||
if anchor:
|
||||
md["thread_id"] = anchor
|
||||
# Rich status parity (QA-1): run.py's live-status lane stashes the
|
||||
# current per-tool phrase via set_status_text() (base class store).
|
||||
|
|
@ -1111,10 +1091,7 @@ class RelayAdapter(BasePlatformAdapter):
|
|||
and self._chat_type_by_chat.get(str(chat_id)) == "dm"
|
||||
):
|
||||
anchor = self._last_inbound_ts_by_chat.get(str(chat_id))
|
||||
if anchor and (
|
||||
self._effective_reply_in_thread()
|
||||
or self._flat_dm_status_enabled()
|
||||
):
|
||||
if anchor:
|
||||
md["thread_id"] = anchor
|
||||
try:
|
||||
await self._transport.send_outbound(
|
||||
|
|
|
|||
|
|
@ -289,15 +289,16 @@ async def test_typing_flat_mode_status_anchors_to_trigger_ts_by_default():
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_typing_flat_mode_opt_out_drops_anchor():
|
||||
"""flat_dm_status: false restores the fully-anchorless flat posture."""
|
||||
adapter, stub = _wire_with_ts("D1", "dm", "1700.0042")
|
||||
adapter.config.extra = {
|
||||
"slack": {"reply_in_thread": False, "flat_dm_status": False}
|
||||
}
|
||||
await adapter.send_typing("D1", metadata=None)
|
||||
typing = [f for f in stub.sent if f["op"] == "typing"]
|
||||
assert typing and "thread_id" not in typing[-1]["metadata"]
|
||||
async def test_typing_anchors_unconditionally_in_both_modes():
|
||||
"""Liveliness is not a preference: the status anchors whenever an inbound
|
||||
ts exists, regardless of reply_in_thread. Placement safety comes from the
|
||||
QA-6/7 send-side anchor strip, not from suppressing the status."""
|
||||
for extra in ({}, {"slack": {"reply_in_thread": False}}):
|
||||
adapter, stub = _wire_with_ts("D1", "dm", "1700.0042")
|
||||
adapter.config.extra = extra
|
||||
await adapter.send_typing("D1", metadata=None)
|
||||
typing = [f for f in stub.sent if f["op"] == "typing"]
|
||||
assert typing and typing[-1]["metadata"].get("thread_id") == "1700.0042"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
55
website/docs/user-guide/messaging/relay.md
Normal file
55
website/docs/user-guide/messaging/relay.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Relay (Team Gateway) — Enterprise
|
||||
|
||||
> **Enterprise-only.** The relay lane applies when your Hermes gateway is
|
||||
> fronted by a [Team Gateway connector](https://github.com/NousResearch/gateway-gateway)
|
||||
> — the enterprise deployment model where the connector owns the platform
|
||||
> credentials (e.g. one org Slack app) and Hermes speaks a relay protocol to
|
||||
> it instead of connecting to the platform natively. Standalone/native
|
||||
> installs can ignore this page; your platform's own page (e.g.
|
||||
> [Slack](slack.md)) applies instead.
|
||||
|
||||
## How configuration works on the relay lane
|
||||
|
||||
The relay adapter is platform-neutral: the connector tells Hermes which
|
||||
platform it fronts, and Hermes expresses per-turn decisions (threading,
|
||||
status, placement) as frame metadata the connector executes mechanically.
|
||||
|
||||
A small set of **platform behavior controls** exist for the fronted platform.
|
||||
They live under `platforms.relay.extra.<platform>` — a supported *subset* of
|
||||
that platform's native options — NOT under the native platform block:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
slack: # native adapter settings — ignored on the relay lane
|
||||
...
|
||||
relay:
|
||||
extra:
|
||||
slack: # relay-lane subset for fronted Slack
|
||||
reply_in_thread: true
|
||||
```
|
||||
|
||||
Resolution order: the nested `extra.<platform>` object wins → a legacy flat
|
||||
key on `extra` is honored as a fallback → the option's default.
|
||||
|
||||
## Supported controls — Slack
|
||||
|
||||
| Key | Default | Effect |
|
||||
|---|---|---|
|
||||
| `reply_in_thread` | `true` | `true`: thread-per-message — each top-level DM message opens its own thread carrying the entire turn (status, tool progress, approval cards, final reply), and each message runs as its own session so concurrent messages execute in parallel. `false`: flat rolling DM — everything posts at the DM root, one shared session, a second message steers the in-flight turn. |
|
||||
|
||||
Semantics match the native Slack adapter's `reply_in_thread`
|
||||
([Slack docs](slack.md)); the relay subset exists so relay-fronted behavior
|
||||
is configured explicitly rather than inherited from a native block that the
|
||||
relay lane does not read.
|
||||
|
||||
In-progress "thinking…" statuses (with live per-tool phrases) are always on,
|
||||
in whatever form the mode supports: the thread's replies footer in
|
||||
thread-per-message mode, or anchored to the triggering message in flat mode.
|
||||
They require only the `chat:write` bot scope — no Slack assistant surface.
|
||||
|
||||
Changing a control takes effect on gateway restart — `hermes config set
|
||||
platforms.relay.extra.slack.reply_in_thread false` and restart; no connector
|
||||
deploy is involved.
|
||||
|
||||
Other fronted platforms currently have no relay-lane controls; the set grows
|
||||
as enterprise deployments need them (each addition is documented here).
|
||||
Loading…
Add table
Add a link
Reference in a new issue