mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
feat(relay): Phase 4 thread lifecycle — handoff threads, semantic renames, reply_to context, hello command manifest (#71624)
- RelayAdapter.create_handoff_thread → one op-gated thread_create op
(Discord channel thread / Telegram forum topic / Slack named seed root);
None fallback contract preserved for the handoff watcher.
- RelayAdapter.rename_thread → thread_rename with only_if_current_name
no-clobber guard on the wire (connector enforces; Telegram guarded
renames fail safe). The native semantic-rename lane
(_is_discord_auto_thread_lane) lights over the relay via the
connector-stamped auto_thread_created/auto_thread_initial_name markers
parsed onto SessionSource in _event_from_wire.
- reply_to {text, author, is_own} wire parse onto the SAME MessageEvent
reply-context fields native adapters populate.
- gateway/relay/command_manifest.py: the gateway-declared slash-command
manifest (native Discord tree mirror) sent on the DISCORD hello; the
connector reconciles Discord's global registration (additive field,
older connectors ignore).
- Contract doc §OutboundAction ops + Phase 4 semantics sections.
- 12 new tests (tests/gateway/relay/test_relay_threads.py); relay suite
266 passed.
This commit is contained in:
parent
1161cc0b53
commit
40eebc7d70
5 changed files with 620 additions and 1 deletions
|
|
@ -397,6 +397,8 @@ The gateway calls the transport with action dicts. Source of truth:
|
|||
| `send_media` | `chat_id`, `media_kind`, `source_url`, `content?` (caption), `filename?`, `reply_to?`, `metadata?` | `{success: bool, message_id?, error?}` |
|
||||
| `prompt` | `chat_id`, `prompt_kind`, `prompt_id`, `content` (the question), `options[]{id,label,style?}`, `timeout_s?`, `reply_to?`, `metadata?` | `{success: bool, message_id?, error?}` |
|
||||
| `react` | `chat_id`, `message_id`, `emoji`, `remove?`, `metadata?` | `{success: bool, error?}` |
|
||||
| `thread_create` | `chat_id` (parent), `thread_name`, `message_id?` (anchor), `metadata?` | `{success: bool, thread_id?, error?}` |
|
||||
| `thread_rename` | `chat_id` (parent), `message_id` (the THREAD id), `thread_name`, `only_if_current_name?`, `metadata?` | `{success: bool, error?}` |
|
||||
|
||||
`get_chat_info(chat_id)` is a separate proxied call returning at least
|
||||
`{name, type}`.
|
||||
|
|
@ -478,6 +480,48 @@ treats reactions as cosmetic). WhatsApp sends a reaction message (empty
|
|||
emoji = remove). Reactions are best-effort by contract: a `react` failure
|
||||
must never fail a turn.
|
||||
|
||||
**`thread_create` / `thread_rename` (Phase 4 thread lifecycle).** One
|
||||
platform-abstract pair covers handoff threads, Telegram DM/forum topics, and
|
||||
LLM-title semantic renames. `thread_create`: Discord posts a channel thread
|
||||
(type 11) or a message-anchored thread when `message_id` is set; Telegram
|
||||
`createForumTopic` (topic id returned); Slack posts a NAMED seed root
|
||||
message and returns its `ts` (threads there are message-anchored — an
|
||||
explicit `message_id` anchor is echoed back verbatim). The created id rides
|
||||
`SendResult.thread_id`. `thread_rename`: Discord PATCHes the thread channel;
|
||||
Telegram `editForumTopic`. The **`only_if_current_name` no-clobber guard**
|
||||
is the native adapters' human-rename-wins semantics, enforced
|
||||
CONNECTOR-side: Discord reads the current name first and no-ops (structured
|
||||
`success:false`) on mismatch; Telegram has no topic-name read, so a GUARDED
|
||||
rename is unsatisfiable and fails safe (unguarded renames proceed). Slack
|
||||
does not advertise `thread_rename` (a root message's text is content, not a
|
||||
name). WhatsApp advertises neither (no threads).
|
||||
|
||||
**Auto-thread markers + gateway-declared command manifest (Phase 4
|
||||
inbound/handshake).** When the connector's auto-thread egress policy creates
|
||||
a Discord thread, later inbound events from that thread carry
|
||||
`source.auto_thread_created: true` + `source.auto_thread_initial_name` — the
|
||||
connector-observed evidence that lights the gateway's semantic-rename lane
|
||||
(the LLM session title renames the thread via a GUARDED `thread_rename`;
|
||||
per-instance memory, so in an N>1 fleet a miss simply never lights the
|
||||
lane). The gateway may also declare its slash-command set on the Discord
|
||||
`hello` frame (`command_manifest: [{name, description, options?}]`); the
|
||||
connector reconciles Discord's GLOBAL application-command registration
|
||||
against it (GET → diff → bulk PUT overwrite; idempotent, debounced,
|
||||
best-effort — a registration failure never affects the handshake). Commands
|
||||
still dispatch through the passthrough plane as before; the manifest only
|
||||
keeps Discord's registry in sync with what the gateway's dispatcher handles.
|
||||
|
||||
**Inbound `reply_to` enrichment (Phase 4).** A platform reply may carry
|
||||
`reply_to: {text?, author?, is_own?}` alongside `reply_to_message_id` — what
|
||||
the user QUOTED, populated only from data the connector already had in hand
|
||||
(Discord's inline `referenced_message`, Telegram's inline
|
||||
`reply_to_message`, WhatsApp `context.from` + a bounded per-instance
|
||||
inbound-text cache for the text leg). Absent fields mean the platform didn't
|
||||
carry the data — never triggers an extra platform API call. `is_own` = the
|
||||
quoted message was authored by the fronted bot (same evidence as the
|
||||
`is_reply_to_bot` relevance marker). The gateway maps these onto the same
|
||||
MessageEvent reply-context fields native adapters populate.
|
||||
|
||||
**`typing` `content?` (Slack status clear).** A `typing` frame normally omits
|
||||
`content` — the connector renders its platform's active indicator ("is
|
||||
typing…" Assistant status on Slack, one-shot typing elsewhere). An **empty
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue