feat(plugins): observable gateway token stream (on_stream_delta/segment/end hooks) - #65077
feat(plugins): observable gateway token stream (on_stream_delta/segment/end hooks)#65077CocaKova wants to merge 1 commit into
Conversation
Replaces the in-tree gateway patch (install.py + monolithic keryx_stream.py) that was closed upstream (NousResearch/hermes-agent#57091 — "no third-party integrations in the core tree"). This version uses only the public plugin surface: - register(ctx) subscribes to the generic stream observer hooks on_stream_delta / on_stream_segment / on_stream_end (NousResearch/hermes-agent#65077) and mirrors each turn's tokens to an in-process hub. - Serves its own bearer-authed SSE side-channel (GET /keryx/stream) plus toolset view/toggle (GET/PUT /keryx/toolsets) on its own daemon thread — adds no gateway route, patches no core file. - Byte-exact delta coalescing preserved from the prior version so a fast model can't overflow a subscriber queue and break the client's stream/commit match. - Config in config.yaml (keryx_stream block); bearer token from env (KERYX_STREAM_TOKEN / API_SERVER_KEY). Toolset locks moved from KERYX_TOOLSETS_* env vars to config.yaml lists, per the non-secret-config rule. Out-of-scope routes from the old monolith (kanban / pet / skills / prune) are dropped from this streaming-focused plugin. plugin.yaml + pyproject.toml entry point for both directory and pip installs. 23 tests, ruff clean.
|
Thanks for providing a generic plugin seam rather than an in-tree integration. The premise is current: live main's Problems
Suggested changes
Automated hermes-sweeper review. |
Related competing streaming-hook design to #64317 and the #64161/#64182 expansion series. This PR invokes observers synchronously on the gateway stream worker; #64317 uses bounded asynchronous fan-out so plugin work cannot delay visible delivery. Please choose one delivery contract before merging either approach. |
…gment/end hooks Adds three generic observer hooks fired by the gateway stream consumer as a turn streams to a chat platform: - on_stream_delta(chat_id, metadata, message_id, delta) — per token - on_stream_segment(chat_id, metadata, message_id) — segment boundary - on_stream_end(chat_id, metadata, message_id, reason) — stream complete They let a plugin observe the live token stream — mirror it to an external side-channel/SSE, drive streaming metrics, bridge an alternate client — entirely from the standard plugin surface, without patching core streaming. Observers only (return values ignored). Callbacks run synchronously on the stream worker thread and must be non-blocking; the per-token path is gated on has_hook() so it costs nothing when no plugin is listening. Fired from the producer API (on_delta / on_segment_break / finish) so no internal worker-loop behavior changes — the hooks fire after the existing queue writes. Concrete consumer: a standalone Keryx live-streaming plugin (published separately per the third-party-integration policy) mirrors deltas to its own SSE side-channel — the generic-surface replacement for the in-tree bridge closed in NousResearch#57091, per AGENTS.md ("if a plugin needs a capability the framework doesn't expose, expand the generic plugin surface, never hardcode plugin-specific logic into core"). Documented in both public hook references — the plugin-hooks table in website/docs/developer-guide/plugins/index.md and full per-hook sections in website/docs/user-guide/features/hooks.md (gateway-only scope, payloads incl. the "done" end reason, observer-only, synchronous/non-blocking requirement) — plus the AGENTS.md plugin-hooks list. tests/gateway/test_stream_observer_hooks.py: firing order + payloads, no delta on empty/boundary text, gated no-op when unregistered (streaming unaffected), callback-exception isolation, VALID_HOOKS membership. 5 passed; stream-consumer, suppression, and streaming-defaults suites (274 total) green on current main.
4e18b7c to
6637a1f
Compare
|
Docs added in 6637a1f, recreated as a single commit on current main:
The dev-guide table rows link to the new user-guide anchors, matching the existing hooks. On the delivery-contract question raised in triage (vs #64317's bounded async fan-out): this PR deliberately keeps the same contract as every existing plugin hook — synchronous Tests: |
|
The streaming observer hook surface shipped on main via #84924 (salvage of #64317, @deaneeth — the tracker-named candidate on #64161, submitted before this PR): on_stream_start / on_stream_delta / on_stream_end / on_interim_message with a host-owned bounded queue keeping plugin callbacks off the token path, and reasoning deltas opt-in. Your design here converged on the same contract — thanks @CocaKova, and sorry this one sat long enough to collide. If the gateway/stream_consumer.py segmenting idea (delta→segment coalescing) still solves a problem the shipped hooks don't, that piece is welcome as a focused follow-up on top of the new surface. Closing as implemented on main. |
Adds three generic observer hooks fired by the gateway stream consumer as a turn streams to a chat platform:
on_stream_deltachat_id, metadata, message_id, deltaon_stream_segmentchat_id, metadata, message_idon_stream_endchat_id, metadata, message_id, reasonThey let a plugin observe the live token stream — mirror it to an external side-channel/SSE, drive streaming metrics, bridge an alternate client — entirely from the standard plugin surface, without patching core streaming.
Why
This is the generic-surface follow-up to #57091, which was correctly closed under the "no third-party-product integrations in the core tree" policy. That bridge needed live token deltas, which the plugin surface did not expose — so it patched
stream_consumer.py. Per AGENTS.md — "if a plugin needs a capability the framework doesn't expose, expand the generic plugin surface (new hook, new ctx method) — never hardcode plugin-specific logic into core" — the right fix is a generic hook, not a special-cased route. A standalone Keryx live-streaming plugin (published separately, installed under~/.hermes/plugins/) is the concrete consumer, so this is not speculative infrastructure. It is equally usable by any streaming observer (logging, metrics, alternate transports).Design / safety
on_delta/on_segment_break/finish), after the existing queue writes — no internal worker-loop behavior changes.has_hook(), so it costs nothing when no plugin is listening.invoke_hookand cannot break token delivery.Tests
tests/gateway/test_stream_observer_hooks.py: firing order + payloads, no delta event on empty/boundary text, gated no-op when unregistered (streaming path unaffected), callback-exception isolation,VALID_HOOKSmembership. 5 passed; existing stream-consumer + suppression suites (174) unchanged. AGENTS.md plugin-hooks list updated.🤖 Generated with Claude Code