Skip to content

fix: deliver plugin-transformed final_response through streaming gate - #29119

Closed
kenyonxu wants to merge 8 commits into
NousResearch:mainfrom
kenyonxu:fix/acp-streamed-final-response
Closed

fix: deliver plugin-transformed final_response through streaming gate#29119
kenyonxu wants to merge 8 commits into
NousResearch:mainfrom
kenyonxu:fix/acp-streamed-final-response

Conversation

@kenyonxu

Copy link
Copy Markdown
Contributor

Problem

Plugin hooks registered on transform_llm_output modify the final response AFTER streaming completes. However, three separate suppression points silently discard the transformed content:

  1. ACP adapter (acp_adapter/server.py): not streamed_message guard skips final_response delivery when streaming was used
  2. Gateway suppression (gateway/run.py): _streamed or _content_delivered check suppresses the normal final send entirely
  3. Gateway run_sync() (gateway/run.py): cherry-picks fields from the run_conversation result dict into a new response dict, but omits critical fields

The net effect is that any content appended by a transform_llm_output hook never reaches end users on streaming platforms (Discord, Telegram, ACP, etc.).

Fix (4 coordinated changes)

1. agent/conversation_loop.py

Set response_transformed=True when a transform_llm_output hook returns a non-empty string. Expose this as response_transformed in the result dict.

2. gateway/run.pyrun_sync() return dict

Add response_transformed to the cherry-picked fields so the flag survives the gateway's response construction.

3. gateway/run.py — suppression check

When response_transformed=True, edit the existing streamed message in-place (via the adapter's edit_message) with the full transformed content, then set already_sent=True. This avoids creating a duplicate message while ensuring the transformed content reaches the client.

4. gateway/stream_consumer.py

Expose message_id and accumulated_text as public read-only properties so the gateway can perform the in-place edit.

5. acp_adapter/server.py

Remove the not streamed_message guard — the final response (possibly transformed by plugins) should always be delivered to the ACP client.

Test plan

  1. Enable a transform_llm_output plugin that appends text
  2. Send a message via Discord or any streaming platform
  3. Verify the appended text appears in the SAME message (not a duplicate)
  4. Disable the plugin — verify no duplicate messages or extra text

@alt-glitch alt-glitch added P3 Low — cosmetic, nice to have type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/gateway Gateway runner, session dispatch, delivery comp/acp Agent Communication Protocol adapter comp/plugins Plugin system and bundled plugins labels May 20, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This PR includes ~3295 graphify-out/ cache files (2.7M additions, 3300 changed files total) alongside the 5-file core fix. The actual plugin-streaming fix touches acp_adapter/server.py, agent/conversation_loop.py, gateway/run.py, gateway/stream_consumer.py, and a design doc. Please rebase onto current main and remove the graphify-out/ directory from the branch — it appears to be a local analysis artifact that was accidentally committed.

Note: PR #29074 (closed) addressed the ACP-only subset of this same issue. This PR is broader (gateway + ACP + conversation_loop), so it's not a strict duplicate, but please verify the ACP portion doesn't conflict with whatever fix landed from #29074.

kenyonxu added 8 commits May 24, 2026 17:21
…ut hook now visible

When streaming is active, streamed_message=True skipped the final_response
update, causing plugin hooks like transform_llm_output to be silently
invisible. Remove the `not streamed_message` guard so the final response
(possibly transformed by plugins) is always delivered to the ACP client.
…s streaming suppression

When a transform_llm_output hook modifies final_response after streaming,
the gateway was silently discarding the transformed content because
streamed=True / content_delivered=True triggered the final-send
suppression. Three changes:

1. conversation_loop: set `_response_transformed=True` when a
   transform_llm_output hook returns a non-empty string, and expose it
   as `response_transformed` in the result dict.

2. gateway/run: skip the final-send suppression when
   `response_transformed` is True — the transformed response must
   reach the client even if streaming already sent the original text.

3. acp_adapter/server: remove `not streamed_message` guard so
   final_response is always delivered (ACP path fixed separately).
…turn dict

run_sync() cherry-picks fields from the run_conversation result dict into
a new response dict for the gateway. response_transformed was missing from
the cherry-pick list, so the gateway always saw it as False and suppressed
the final send even though a transform_llm_output hook had modified the content.
… response_transformed

When a transform_llm_output hook appends content after streaming, the previous
fix skipped the final-send suppression which caused the full response to be
sent as a NEW message (duplicate). Instead, edit the existing streamed message
in-place to append the transformed content, then set already_sent=True.

Added stream_consumer.message_id and .accumulated_text public properties.
…ng suppression

Document the three-layer suppression bug that prevented plugin-transformed
final_response content from reaching users on streaming platforms (Discord,
Telegram, ACP). Includes the 5-file coordinated fix and data flow diagram.
@kenyonxu
kenyonxu force-pushed the fix/acp-streamed-final-response branch from 148c434 to 3dfa713 Compare May 24, 2026 09:34
@kenyonxu

Copy link
Copy Markdown
Contributor Author

Thanks for the review!

  1. graphify-out/ cleaned up: Rebased onto latest main and removed all ~3295 graphify-out/ files from the branch history. Also added graphify-out/ to .gitignore to prevent accidental re-commit. The diff is now down to 6 files / 158 lines.

  2. Re: conflict with fix(acp): deliver final_response after streaming — plugin hooks now visible #29074: I checked — fix(acp): deliver final_response after streaming — plugin hooks now visible #29074 was closed without being merged (merged_at is null, merged_by is null). No related fix landed in upstream main. The ACP portion here is the same code that was in the closed fix(acp): deliver final_response after streaming — plugin hooks now visible #29074, so there is no conflict to worry about.

teknium1 added a commit that referenced this pull request May 24, 2026
PR #29119 dropped the 'not streamed_message' guard unconditionally so
that plugin-transformed responses (transform_llm_output hook) would
reach ACP clients. That regressed test_prompt_does_not_duplicate_streamed_final_message:
when no transform happened, the streamed text was re-sent as a duplicate
final delivery.

Tighten the condition to mirror the gateway side: deliver after streaming
only when response_transformed=True. Otherwise keep the old guard.

Adds test_prompt_delivers_transformed_response_after_streaming so the
transformed path stays covered.
teknium1 added a commit that referenced this pull request May 24, 2026
…ming

Adds a test that fails without the gateway fix, exercising the
response_transformed=True branch in _finalize_response: a streamed
response whose final text was modified by a transform_llm_output
plugin hook must be edit_message'd in place (not duplicate-sent),
with already_sent=True so the normal final-send is skipped.

Also drops two minor leftovers from the salvaged PR #29119:

  * accumulated_text property on GatewayStreamConsumer (unused)
  * duplicate _response_transformed=False inside the hook try block
teknium1 added a commit that referenced this pull request May 24, 2026
PR #29119 dropped the 'not streamed_message' guard unconditionally so
that plugin-transformed responses (transform_llm_output hook) would
reach ACP clients. That regressed test_prompt_does_not_duplicate_streamed_final_message:
when no transform happened, the streamed text was re-sent as a duplicate
final delivery.

Tighten the condition to mirror the gateway side: deliver after streaming
only when response_transformed=True. Otherwise keep the old guard.

Adds test_prompt_delivers_transformed_response_after_streaming so the
transformed path stays covered.
@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #31433. Your 4 substantive commits were cherry-picked onto current main with your authorship preserved in git log — see commits 7eb6c7f, 8edeebe, a4ceead, 5cb21e3. Follow-ups (a regression test + a tightening of the ACP guard so non-transformed streams stay single-delivery) added on top. Thanks for the fix!

@teknium1 teknium1 closed this May 24, 2026
sahilm-ti pushed a commit to sahilm-ti/hermes-agent that referenced this pull request May 25, 2026
…ming

Adds a test that fails without the gateway fix, exercising the
response_transformed=True branch in _finalize_response: a streamed
response whose final text was modified by a transform_llm_output
plugin hook must be edit_message'd in place (not duplicate-sent),
with already_sent=True so the normal final-send is skipped.

Also drops two minor leftovers from the salvaged PR NousResearch#29119:

  * accumulated_text property on GatewayStreamConsumer (unused)
  * duplicate _response_transformed=False inside the hook try block
sahilm-ti pushed a commit to sahilm-ti/hermes-agent that referenced this pull request May 25, 2026
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…ming

Adds a test that fails without the gateway fix, exercising the
response_transformed=True branch in _finalize_response: a streamed
response whose final text was modified by a transform_llm_output
plugin hook must be edit_message'd in place (not duplicate-sent),
with already_sent=True so the normal final-send is skipped.

Also drops two minor leftovers from the salvaged PR NousResearch#29119:

  * accumulated_text property on GatewayStreamConsumer (unused)
  * duplicate _response_transformed=False inside the hook try block
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
PR NousResearch#29119 dropped the 'not streamed_message' guard unconditionally so
that plugin-transformed responses (transform_llm_output hook) would
reach ACP clients. That regressed test_prompt_does_not_duplicate_streamed_final_message:
when no transform happened, the streamed text was re-sent as a duplicate
final delivery.

Tighten the condition to mirror the gateway side: deliver after streaming
only when response_transformed=True. Otherwise keep the old guard.

Adds test_prompt_delivers_transformed_response_after_streaming so the
transformed path stays covered.
alt-glitch pushed a commit that referenced this pull request Jun 14, 2026
…ming

Adds a test that fails without the gateway fix, exercising the
response_transformed=True branch in _finalize_response: a streamed
response whose final text was modified by a transform_llm_output
plugin hook must be edit_message'd in place (not duplicate-sent),
with already_sent=True so the normal final-send is skipped.

Also drops two minor leftovers from the salvaged PR #29119:

  * accumulated_text property on GatewayStreamConsumer (unused)
  * duplicate _response_transformed=False inside the hook try block
alt-glitch pushed a commit that referenced this pull request Jun 14, 2026
PR #29119 dropped the 'not streamed_message' guard unconditionally so
that plugin-transformed responses (transform_llm_output hook) would
reach ACP clients. That regressed test_prompt_does_not_duplicate_streamed_final_message:
when no transform happened, the streamed text was re-sent as a duplicate
final delivery.

Tighten the condition to mirror the gateway side: deliver after streaming
only when response_transformed=True. Otherwise keep the old guard.

Adds test_prompt_delivers_transformed_response_after_streaming so the
transformed path stays covered.
T02200059 pushed a commit to T02200059/hermes-agent that referenced this pull request Jun 18, 2026
…ming

Adds a test that fails without the gateway fix, exercising the
response_transformed=True branch in _finalize_response: a streamed
response whose final text was modified by a transform_llm_output
plugin hook must be edit_message'd in place (not duplicate-sent),
with already_sent=True so the normal final-send is skipped.

Also drops two minor leftovers from the salvaged PR NousResearch#29119:

  * accumulated_text property on GatewayStreamConsumer (unused)
  * duplicate _response_transformed=False inside the hook try block
T02200059 pushed a commit to T02200059/hermes-agent that referenced this pull request Jun 18, 2026
T02200059 pushed a commit to T02200059/hermes-agent that referenced this pull request Jun 18, 2026
PR NousResearch#29119 dropped the 'not streamed_message' guard unconditionally so
that plugin-transformed responses (transform_llm_output hook) would
reach ACP clients. That regressed test_prompt_does_not_duplicate_streamed_final_message:
when no transform happened, the streamed text was re-sent as a duplicate
final delivery.

Tighten the condition to mirror the gateway side: deliver after streaming
only when response_transformed=True. Otherwise keep the old guard.

Adds test_prompt_delivers_transformed_response_after_streaming so the
transformed path stays covered.
liuchanchen pushed a commit to liuchanchen/hermes-agent that referenced this pull request Jun 23, 2026
…ming

Adds a test that fails without the gateway fix, exercising the
response_transformed=True branch in _finalize_response: a streamed
response whose final text was modified by a transform_llm_output
plugin hook must be edit_message'd in place (not duplicate-sent),
with already_sent=True so the normal final-send is skipped.

Also drops two minor leftovers from the salvaged PR NousResearch#29119:

  * accumulated_text property on GatewayStreamConsumer (unused)
  * duplicate _response_transformed=False inside the hook try block
liuchanchen pushed a commit to liuchanchen/hermes-agent that referenced this pull request Jun 23, 2026
liuchanchen pushed a commit to liuchanchen/hermes-agent that referenced this pull request Jun 23, 2026
PR NousResearch#29119 dropped the 'not streamed_message' guard unconditionally so
that plugin-transformed responses (transform_llm_output hook) would
reach ACP clients. That regressed test_prompt_does_not_duplicate_streamed_final_message:
when no transform happened, the streamed text was re-sent as a duplicate
final delivery.

Tighten the condition to mirror the gateway side: deliver after streaming
only when response_transformed=True. Otherwise keep the old guard.

Adds test_prompt_delivers_transformed_response_after_streaming so the
transformed path stays covered.
xyshanren pushed a commit to xyshanren/hermes-agent-cn that referenced this pull request Jun 25, 2026
xyshanren pushed a commit to xyshanren/hermes-agent-cn that referenced this pull request Jun 25, 2026
PR NousResearch#29119 dropped the 'not streamed_message' guard unconditionally so
that plugin-transformed responses (transform_llm_output hook) would
reach ACP clients. That regressed test_prompt_does_not_duplicate_streamed_final_message:
when no transform happened, the streamed text was re-sent as a duplicate
final delivery.

Tighten the condition to mirror the gateway side: deliver after streaming
only when response_transformed=True. Otherwise keep the old guard.

Adds test_prompt_delivers_transformed_response_after_streaming so the
transformed path stays covered.
xyshanren pushed a commit to xyshanren/hermes-agent-cn that referenced this pull request Jun 25, 2026
…ming

Adds a test that fails without the gateway fix, exercising the
response_transformed=True branch in _finalize_response: a streamed
response whose final text was modified by a transform_llm_output
plugin hook must be edit_message'd in place (not duplicate-sent),
with already_sent=True so the normal final-send is skipped.

Also drops two minor leftovers from the salvaged PR NousResearch#29119:

  * accumulated_text property on GatewayStreamConsumer (unused)
  * duplicate _response_transformed=False inside the hook try block
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…ming

Adds a test that fails without the gateway fix, exercising the
response_transformed=True branch in _finalize_response: a streamed
response whose final text was modified by a transform_llm_output
plugin hook must be edit_message'd in place (not duplicate-sent),
with already_sent=True so the normal final-send is skipped.

Also drops two minor leftovers from the salvaged PR NousResearch#29119:

  * accumulated_text property on GatewayStreamConsumer (unused)
  * duplicate _response_transformed=False inside the hook try block
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
PR NousResearch#29119 dropped the 'not streamed_message' guard unconditionally so
that plugin-transformed responses (transform_llm_output hook) would
reach ACP clients. That regressed test_prompt_does_not_duplicate_streamed_final_message:
when no transform happened, the streamed text was re-sent as a duplicate
final delivery.

Tighten the condition to mirror the gateway side: deliver after streaming
only when response_transformed=True. Otherwise keep the old guard.

Adds test_prompt_delivers_transformed_response_after_streaming so the
transformed path stays covered.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/acp Agent Communication Protocol adapter comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants