Skip to content

feat(moa): stream the aggregator response to the user - #55625

Merged
teknium1 merged 2 commits into
mainfrom
hermes/hermes-a4e658ce
Jun 30, 2026
Merged

feat(moa): stream the aggregator response to the user#55625
teknium1 merged 2 commits into
mainfrom
hermes/hermes-a4e658ce

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

MoA sessions now stream the aggregator's response to the user live, instead of fetching it whole and showing nothing until the entire turn finishes.

Changes

  • agent/moa_loop.py: MoAChatCompletions.create() honors stream=True — runs the references first, then returns the aggregator's raw token stream. Non-streaming path (stream=False) is byte-identical to before.
  • agent/auxiliary_client.py: call_llm() gains stream/stream_options; when streaming it returns the raw SDK iterator and skips _validate_llm_response + the fallback chain (the caller owns reassembly/fallback).
  • agent/conversation_loop.py: stop hard-disabling streaming for provider == "moa" — stream only when a display/TTS consumer is present (_has_stream_consumers()).
  • scripts/release.py: AUTHOR_MAP entry for @lEWFkRAD.

Why

conversation_loop hard-disabled streaming for provider in {"copilot-acp", "moa"}, and MoAChatCompletions.create() fetched the aggregator reply whole. On long turns the user saw minutes of silence before any output.

The fix reuses the existing streaming consumer (which already handles text-delta delivery, tool-call reassembly, stale-stream detection, and non-streaming fallback) rather than adding a parallel streaming path — because for provider: moa, _create_request_openai_client returns the MoAClient facade itself, so the consumer already invokes MoAChatCompletions.create(stream=True).

Cache-safe: streaming changes only how the final assistant text is delivered — no message-history mutation, no prompt-cache invalidation. References still run first.

Validation

Before After
MoA turn with display consumer whole-response, silent until done aggregator streams live
Quiet mode / subagent / health-check whole-response whole-response (unchanged)
stream=False aggregator call no stream/stream_options/timeout keys (byte-identical)
MoA test suites (streaming + loop + config + slot api_mode) 48 passed, 0 failed

Salvaged from #53848 (@lEWFkRAD), cherry-picked onto current main with authorship preserved.

Infographic

MoA aggregator streaming


Nous Research

lEWFkRAD and others added 2 commits June 30, 2026 04:40
MoA sessions could not stream: the gateway streaming toggle was a no-op for
provider "moa", so users saw nothing until the entire response finished — minutes
of silence on long turns. The aggregator's reply was always fetched whole.

Root cause was twofold:
  1. conversation_loop hard-disabled streaming for provider in {"copilot-acp",
     "moa"} (MoA grouped with the ACP client, whose facade isn't a stream).
  2. MoAChatCompletions.create() fetched the aggregator response whole via
     call_llm(), which had no streaming mode.

For provider "moa", _create_request_openai_client() returns the MoAClient facade
itself, so the existing streaming consumer already calls
MoAChatCompletions.create(stream=True). We reuse that battle-tested consumer
(text-delta delivery, tool_call reassembly, stale-stream detection, non-streaming
fallback) instead of adding a parallel streaming path.

Changes:
  - call_llm() gains stream/stream_options. When streaming it returns the raw SDK
    stream iterator directly, bypassing _validate_llm_response and the
    temperature/max_tokens/payment fallback chain (which assume a complete
    response). The caller owns reassembly and fallback.
  - MoAChatCompletions.create() runs the references first (unchanged), then when
    stream=True returns the aggregator's raw stream, forwarding stream_options and
    the consumer's per-request read timeout. stream=False is byte-identical to
    before (no stream/stream_options/timeout forwarded).
  - conversation_loop streams MoA only when a display/TTS consumer is present;
    quiet/subagent/health-check paths keep the complete-response path.

Tests: tests/run_agent/test_moa_streaming.py — create() stream/non-stream
branches, stream_options + timeout forwarding, call_llm raw-stream return vs
validated non-stream. Existing MoA tests unchanged (20 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alt-glitch alt-glitch added type/feature New feature or request comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have labels Jun 30, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Salvage of #53848 (by @lEWFkRAD) — same fix (MoA aggregator streaming) rebased with authorship credited via the scripts/release.py AUTHOR_MAP entry. Tracking as related, not a duplicate; the original #53848 should be closed in favor of this once merged. Related to the MoA virtual-provider feature #46081.

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: LGTM

Clean streaming support for the MoA aggregator. When the agent has a display/TTS consumer, the aggregator's raw token stream is returned directly; without consumers, the complete-response path is preserved byte-identically.

Looks Good

  • Stream path is well-isolated: stream=True returns the raw SDK iterator, skips _validate_llm_response (which assumes complete responses)
  • Non-streaming path is explicitly preserved — no stream/stream_options/timeout keys leak through
  • MoA facade's create() correctly extracts stream/stream_options from api_kwargs and forwards timeout
  • 6 comprehensive tests in test_moa_streaming.py cover: stream path, non-stream invariance, timeout forwarding, caller stream_options passthrough, and both call_llm paths
  • conversation_loop.py gating is correct: MoA only streams when consumers are present

Reviewed by Hermes Agent

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 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.

4 participants