Skip to content

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

Closed
Git-on-my-level wants to merge 21 commits 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#13810
Git-on-my-level wants to merge 21 commits 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

Git-on-my-level and others added 21 commits April 9, 2026 13:05
…t, suppress build stderr

Group 1 patches re-applied on upstream main (e94008c):
- hermes_cli/update_channel.py: new channel detection (prod vs main)
- hermes_cli/main.py: channel-aware fetch/ref in cmd_update, prod pre-sync
- hermes_cli/gateway.py: launchd_restart_for_update with healthcheck
- hermes_cli/banner.py: channel display in banner
- hermes_cli/main.py: capture_output=True on .[all] install fallback
- tests updated for new contract
…ompression

- telegram.py: _is_nonretryable_auth_error() detects 401/Unauthorized,
  sets retryable=False to prevent infinite retry loops on bad tokens
- run.py: /usage command counts all message roles (not just user/assistant),
  preventing 2-5x undercount in tool-heavy conversations
- run.py: pass full transcript to compressor instead of filtering to
  user/assistant only, allowing compressor to prune stale tool results
…del runtime

- Interrupt handler now checks if queued message is a command (starts with /)
  and dispatches it via _handle_message instead of treating as plain text
- Session-scoped model overrides from /model now flow through to _run_agent's
  agent creation via _resolve_session_runtime_config()
- Fallback model tracking uses session-aware model instead of global default
Add allowed_inbound_targets config support to TelegramAdapter for
filtering inbound messages to specific chat/topic combinations.

- Parse config from extra.allowed_inbound_targets (dict, string, int forms)
- _is_inbound_target_allowed: allowlist check, DMs always pass
- _is_inbound_message_allowed: convenience wrapper for PTB Message objects
- Guard all four inbound handlers: text, command, location, media
- Add tests for parsing, filtering, and handler rejection
- Document in telegram.md with YAML examples and behavior notes
Merge-based updates caused compounding conflicts over time and required
force-push to recover from anyway. Rebase keeps prod as a clean linear
stack of patches on top of upstream main.

- Uses --force-with-lease for safer pushes
- Warns on large drift (>100 commits, >20 patches)
- Pauses on rebase conflicts for manual resolution
_requested_turn is defined inside the run_sync() closure but was
referenced from the outer _run_agent() async scope, causing a NameError
on every turn where the agent has a model attribute.
Conflict resolutions:
- gateway/run.py compression: keep prod model resolution + full history
- gateway/run.py /status: keep prod full-history estimation with last_prompt_tokens
- gateway/run.py fallback eviction: take upstream _run_failed + _is_intentional_model_switch
- gateway/run.py pending handling: take upstream _dequeue_pending_event + command safety net
- gateway/run.py queue fallback: take upstream merge_pending_message_event
- gateway/run.py _run_agent recurse: pass both session_overrides (prod) + event_message_id (upstream)
- hermes_cli/gateway.py: keep prod launchd helper functions + upstream _wait_for_gateway_exit signature
Switch prod pre-sync and update-prod-branch.sh from tracking origin/main
to merging only the latest v* release tag. This keeps prod on stable
upstream releases rather than pulling in every post-tag commit.

- Add _latest_upstream_tag() helper that fetches tags and resolves the
  highest v* tag via --sort=-version:refname
- _attempt_prod_prefetch_sync() now merges the latest tag (not origin/main)
  and skips sync if fork/prod already contains it
- update-prod-branch.sh rebases onto the latest tag, exits early if
  prod is current, and reports tag-relative patch counts
- Update tests and hermes-fork-prod skill docs to match new behavior
Upstream changes absorbed (no prod-only conflict):
- run.py: upstream now has session-scoped /model overrides, full-history
  flush with live memory guard, busy-ack, deprecated cwd warnings,
  _parse_session_key, channel prompts, try/except in flush agent

Prod-only re-applied on upstream base:
- telegram.py: inbound-topic allowlist (_parse_allowed_inbound_targets,
  _is_inbound_target_allowed, _is_inbound_message_allowed) wired into
  _should_process_message for all message types
- telegram.py: non-retryable auth error detection (_is_nonretryable_auth_error)
  with telegram_invalid_token fatal error (non-retryable) vs connect_error (retryable)
… ACP

Wires AIAgent.interim_assistant_callback in the ACP adapter so editor clients
(CAR, VS Code, Zed) receive interim assistant commentary messages during tool
execution. Commentary is emitted as agent_message_chunk updates with _meta.phase
set to commentary, following the ACP schema extension pattern.

Fixes the gap where gateway-path commentary works but ACP-path drops it.
…eaming

Root cause: ACP turns emitted partial session/update progress (thinking
spinners, tool events) then went 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.

Fixes #9
@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround 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:26
@Git-on-my-level

Copy link
Copy Markdown
Contributor Author

Sorry forgot to cherry pick

@Git-on-my-level
Git-on-my-level restored the fix/acp-stream-delta-bridge branch April 22, 2026 04:27
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 P1 High — major feature broken, no workaround type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants