Skip to content

feat(plugins): carry the delivery target on stream observer payloads - #90367

Open
CocaKova wants to merge 1 commit into
NousResearch:mainfrom
CocaKova:feat/stream-observer-chat-target
Open

feat(plugins): carry the delivery target on stream observer payloads#90367
CocaKova wants to merge 1 commit into
NousResearch:mainfrom
CocaKova:feat/stream-observer-chat-target

Conversation

@CocaKova

Copy link
Copy Markdown
Contributor

The gap

on_stream_start / on_stream_delta / on_stream_end / on_interim_message identify a turn by session_id. That answers which stored history does this extend — not which conversation is this being delivered to.

One gateway serves many chats at once. A plugin that mirrors a turn's tokens somewhere per-chat — a second client, a protocol bridge, a web view — receives every delta of every concurrent turn with no way to tell them apart, and has to patch core to recover an identity the agent is already holding.

The change

_stream_hook_base_payload() gains chat_id, chat_type and thread_id next to the existing surface, so the delivery target is (surface, chat_id) — the same pair the gateway keys a chat on.

  • Values come from the _chat_id / _chat_type / _thread_id attributes agent_init already sets for gateway sessions. Nothing new is plumbed.
  • Defensive getattr with "" defaults: a CLI turn reports no chat rather than omitting the keys and making consumers guess.
  • on_interim_message is folded onto the same base payload instead of hand-building its half of it. A consumer routing deltas by chat should not find the identity present on its deltas and missing on its interim messages; it also picks up turn_id and iteration for free.
  • Payload documented in the VALID_HOOKS comment block.

Observer-only, off the token path, no behaviour change for anything that ignores the new keys.

Consumer

A standalone gateway side-channel plugin that streams a turn into the Matrix room it came from, so a phone client can render tokens live without the homeserver taking an m.replace edit per tick. Without the target it cannot tell two rooms apart. It ships separately, per the plugin policy.

Relationship to #90077

Independent and adjacent — that one adds usage to on_stream_end, this one adds the target to the shared base payload. They touch the same file and the same test module, so whichever lands second will want a trivial rebase. Together they are what lets that plugin stop being a core patch.

Tests

2 added, 14 passing in tests/run_agent/test_plugin_stream_hooks.py. tests/hermes_cli/test_plugins.py also green (64 passing).

  • the target reaches all four hooks for a gateway-shaped agent
  • a CLI turn reports empty strings, not missing keys

(Supersedes #90310 — identical commit, reopened from a correctly-named branch. Renaming the head branch on the fork closed the original.)

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/plugins Plugin system and bundled plugins labels Aug 20, 2026
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference; please use your judgment.

Reviewed by reviewer-e (AI automated review).

Well-reasoned payload extension: routing streamed tokens to the right conversation genuinely is a different question than which stored history a turn extends, and chat_id/chat_type/thread_id answer it directly. Folding on_interim_message onto the shared _stream_hook_base_payload() also quietly fixes the interim hook's missing session/model/provider/surface fields, and always-present-empty-string keys (instead of absent ones) spare consumers key-existence guessing — both behaviors are pinned by the new gateway-shaped and CLI-shape tests.

Nit (non-blocking): hermes_cli/plugins.py:170 — the base-payload field list is now hand-maintained prose in the hook registry comment, duplicated against _stream_hook_base_payload(); the next added field will inevitably update one and not the other (this PR itself had to touch both). Point the comment at the code as the source of truth ("see AIAgent._stream_hook_base_payload for the exact base payload") instead of enumerating fields.

The stream observer hooks identify a turn by session_id, which answers
which stored history it extends — not which conversation it is being
delivered to. One gateway serves many chats concurrently, so a plugin
that mirrors a turn's tokens somewhere per-chat (a second client, a
protocol bridge, a web view) has no way to route them, and ends up
needing core patches to recover an identity the agent already holds.

_stream_hook_base_payload now carries chat_id, chat_type and thread_id
alongside surface, so the target is (surface, chat_id) — the same pair
the gateway keys a chat on. The values come from the _chat_id/_chat_type/
_thread_id attributes agent_init already sets for gateway sessions;
defensive getattr with "" defaults, so a CLI turn reports no chat rather
than omitting the keys and making consumers guess.

on_interim_message is folded onto the same base payload: a consumer
routing deltas by chat should not find the identity present there and
missing on interim messages. That also picks up turn_id and iteration,
which it was building by hand.

Payload and target semantics are documented at
_stream_hook_base_payload; the VALID_HOOKS comment points there rather
than duplicating the field list. Real consumer is a standalone gateway
side-channel plugin that streams a turn into the Matrix room it came
from; without the target it cannot tell two rooms apart.

Tests: 2 added, 14 passing in tests/run_agent/test_plugin_stream_hooks.py
@CocaKova
CocaKova force-pushed the feat/stream-observer-chat-target branch from b6196e8 to d29d869 Compare August 23, 2026 21:13
@CocaKova

Copy link
Copy Markdown
Contributor Author

Addressed the nit in d29d869: the VALID_HOOKS comment no longer enumerates the base payload fields — it points at AIAgent._stream_hook_base_payload (run_agent.py) as the source of truth for the fields and the delivery-target semantics. Comment-only change; 14/14 stream-hook tests still passing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants