Skip to content

fix(acp): wire stream_delta_callback for real-time ACP text delta streaming - #13842

Closed
Git-on-my-level wants to merge 1 commit into
NousResearch:mainfrom
Git-on-my-level:fix/acp-stream-delta-bridge
Closed

fix(acp): wire stream_delta_callback for real-time ACP text delta streaming#13842
Git-on-my-level wants to merge 1 commit into
NousResearch:mainfrom
Git-on-my-level:fix/acp-stream-delta-bridge

Conversation

@Git-on-my-level

Copy link
Copy Markdown
Contributor

Root Cause

ACP turns emit partial session/update progress (thinking spinners, tool start/complete events, interim commentary), then go silent for the entire duration of run_conversation() before the final output arrives via prompt_return instead of a streamed terminal lifecycle event.

Why: stream_delta_callback was never wired in the ACP adapter

The ACP adapter (acp_adapter/server.py) sets several callbacks on AIAgent:

Callback Set by ACP? Called by run_agent.py? Result
thinking_callback Works — spinner text
tool_progress_callback Works — ToolCallStart
step_callback Works — ToolCallComplete
interim_assistant_callback ✅ (via _emit_interim_assistant_message) Works — commentary
message_callback ❌ Never called Dead code
stream_delta_callback ❌ NOT SET ✅ (_fire_stream_delta) Text deltas silently dropped

The critical gap: _fire_stream_delta() (line 5211 in run_agent.py) is the method that fires for every LLM streaming token. It only dispatches to stream_delta_callback and _stream_callback. Since ACP sets neither, every text delta from the LLM is silently discarded.

CAR's completion_source="prompt_return" (not stream_terminal_event) confirms no timely terminal lifecycle signal was received — it only got the final result through the RPC return path, ~30 minutes after the last progress update.

Fix

  • Add make_stream_delta_cb() factory in acp_adapter/events.py that forwards each text delta to conn.session_update() as an agent_message_chunk
  • Wire it as agent.stream_delta_callback in the prompt() method
  • Add unit + integration tests

Files Changed

  • acp_adapter/events.py — new make_stream_delta_cb() factory (+17 lines)
  • acp_adapter/server.py — wire callback in prompt() (+4 lines)
  • tests/acp/test_events.pyTestStreamDeltaCallback class (+28 lines)
  • tests/acp/test_server.py — integration test test_prompt_wires_stream_delta_callback (+30 lines)

Fixes #9

…eaming

Root cause: ACP turns emit partial session/update progress (thinking
spinners, tool events) then go silent for the entire duration of
run_conversation() because stream_delta_callback was never wired in the
ACP adapter. The LLM's streamed text deltas were silently discarded
since _fire_stream_delta() only dispatches to stream_delta_callback
and _stream_callback — neither of which ACP set.

Fix: Add make_stream_delta_cb() factory that forwards each text delta
to conn.session_update() as an agent_message_chunk, and wire it as
agent.stream_delta_callback in prompt(). This gives ACP clients
(CAR/PMA/VS Code) real-time token-by-token progress during LLM
streaming, eliminating the multi-minute silent gap before final
prompt_return delivery.

Also suppresses the final_response session_update when stream_delta_cb
is active to avoid sending duplicate text to clients that append
incremental chunks (P1 from automated review).

Fixes #9
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/acp Agent Communication Protocol adapter comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint labels Apr 22, 2026
@Git-on-my-level
Git-on-my-level deleted the fix/acp-stream-delta-bridge branch April 22, 2026 04:52
@Git-on-my-level
Git-on-my-level restored the fix/acp-stream-delta-bridge branch April 22, 2026 04:53
@trevorgordon981

Copy link
Copy Markdown
Contributor

I have verified this solution by inspecting the code changes and running the new test suite. The fix introduces to forward live LLM text deltas to ACP clients as updates, enabling real-time token-by-token streaming. It correctly suppresses the final when streaming is active to prevent duplicate text delivery. The new unit tests confirm the callback emits chunks for non-empty text and ignores empty strings, while the server tests verify the suppression logic. This significantly improves the responsiveness of ACP integrations in VS Code, Zed, and JetBrains IDEs.

Tested and confirmed. ✅

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for identifying the ACP streaming gap. This is an automated hermes-sweeper review; current main already provides this behavior and has since strengthened the final-response path.

  • acp_adapter/server.py:1415-1448 creates an ACP stream callback, forwards text through make_message_cb, and assigns it to agent.stream_delta_callback.
  • run_agent.py:4680-4731 delivers every non-empty streaming delta through that callback.
  • acp_adapter/server.py:1630-1641 suppresses duplicate finals only after an actual streamed message, while still delivering a post-stream transformed response; tests/acp/test_server.py:1254-1319 covers both behaviors.
  • The strengthened behavior shipped in 60d20a37c (fix(acp): only deliver final_response after streaming when transformed), included in v2026.5.28.

Closing as implemented on main.

@teknium1 teknium1 closed this Jul 12, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jul 12, 2026
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 P2 Medium — degraded but workaround exists sweeper:implemented-on-main Sweeper: behavior already present on current main type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants