hermes - ✅(Solved) Fix [Bug]: chat completions SSE does not emit tool completion progress [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#16588Fetched 2026-04-28 06:52:20
View on GitHub
Comments
0
Participants
1
Timeline
5
Reactions
0
Author
Participants
Timeline (top)
labeled ×3cross-referenced ×2

Root Cause

In gateway/platforms/api_server.py, the /v1/chat/completions streaming branch defines _on_tool_progress() and passes only tool_progress_callback=_on_tool_progress to _run_agent().

AIAgent already emits exact lifecycle callbacks through tool_start_callback(tool_call_id, name, args) and tool_complete_callback(tool_call_id, name, args, result) in the tool execution paths. Those callbacks are available in the API server adapter and are used by the /v1/responses streaming path, but chat completions does not currently wire them.

Fix Action

Fix / Workaround

A minimal regression test can patch _run_agent to call tool_start_callback("call_terminal_1", "terminal", {...}), then tool_complete_callback("call_terminal_1", "terminal", {...}, result), and assert both custom SSE events are emitted.

PR fix notes

PR #16591: fix(api-server): emit chat tool completion progress

Description (problem / solution / changelog)

What does this PR do?

Emit structured hermes.tool.progress completion events from the OpenAI-compatible /v1/chat/completions streaming endpoint.

The Responses API path already receives exact tool lifecycle callbacks. The chat completions streaming path only forwarded legacy start-style progress, so API Server clients could show a tool as running until the final assistant response ended. This change wires tool_start_callback and tool_complete_callback into the chat stream and includes a stable toolCallId on both running and completed events.

Related Issue

Fixes #16588

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • gateway/platforms/api_server.py: adds a shared queue helper for chat-completion tool progress events and wires tool_start_callback / tool_complete_callback into _run_agent.
  • tests/gateway/test_api_server.py: updates streaming tool-progress tests to assert toolCallId and running status, and adds coverage for completed progress with output before the final assistant content.

How to Test

  1. Run scripts/run_tests.sh tests/gateway/test_api_server.py -k 'tool_progress or tool_completed_progress'.
  2. Run scripts/run_tests.sh tests/gateway/test_api_server.py.
  3. Run git diff --check HEAD~1..HEAD.

Targeted verification on macOS 15 / Python 3.12.10:

  • scripts/run_tests.sh tests/gateway/test_api_server.py -k 'tool_progress or tool_completed_progress': 3 passed, 3 warnings.
  • scripts/run_tests.sh tests/gateway/test_api_server.py: 120 passed, 78 warnings.
  • git diff --check HEAD~1..HEAD: passed.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 15, Python 3.12.10

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — no platform-specific code added
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

For New Skills

N/A

Screenshots / Logs

Not applicable. The behavior is covered by the streaming SSE assertions in tests/gateway/test_api_server.py.

Changed files

  • gateway/platforms/api_server.py (modified, +62/-9)
  • tests/gateway/test_api_server.py (modified, +53/-10)

PR #16666: fix(api-server): emit tool.completed lifecycle SSE for chat completions (#16588)

Description (problem / solution / changelog)

Summary

  • Add tool_start_callback / tool_complete_callback to the chat completions agent invocation so SSE consumers receive a matching status: completed event tied to the same toolCallId as the running event.
  • Emit the lifecycle pair on the existing event: hermes.tool.progress line; payload now carries toolCallId + status (running / completed) alongside the legacy emoji/label event.
  • Filter internal tools (names starting with _) and drop orphan completed callbacks that have no prior running so clients never see uncorrelatable lifecycle updates.
  • Adds two regression tests in tests/gateway/test_api_server.py: one asserts both halves of the lifecycle pair land on the wire with matching toolCallId; the other proves filtering handles internal tools and orphan completes.

The bug

/v1/chat/completions streaming emits event: hermes.tool.progress for tool start, but never a matching completed event with the exact toolCallId. Frontends rendering tool cards stay stuck in a "running" state until the full assistant response ends or they guess completion locally. The /v1/responses path already does this correctly via tool_start_callback / tool_complete_callback (api_server.py:1797-1829) — only chat completions was missing it.

The fix

Define _on_tool_start and _on_tool_complete in the chat completions handler that push tagged ("__tool_lifecycle__", payload) items to the existing stream queue. The SSE writer's _emit is extended to recognize the new tag on the same event: hermes.tool.progress SSE line, so no new event type is introduced — clients consuming the legacy emoji/label event keep working unchanged. A small _started_tool_call_ids set tracks running calls so a completed callback for a filtered-out internal tool, or one without a prior matching start, is silently dropped.

Test plan

  • Focused regression test test_stream_emits_tool_lifecycle_with_call_id: parses hermes.tool.progress payloads from the SSE body and asserts running + completed arrive in order with the same toolCallId.
  • Edge-case test test_stream_tool_lifecycle_skips_internal_and_orphan_completes: internal tool starts and orphan completes (no matching start) produce no lifecycle events on the wire.
  • Adjacent suite: tests/gateway/test_api_server.py, tests/gateway/test_api_server_runs.py, tests/gateway/test_api_server_multimodal.py — 159/159 pass.
  • Regression guard: with gateway/platforms/api_server.py reverted to origin/main, the new lifecycle test fails with missing running status; got []; with the fix applied it passes.

Related

  • Fixes #16588
  • Mirrors the existing structured-callback pattern used on the /v1/responses path (gateway/platforms/api_server.py:1797-1829).
  • Backward-compatible with the legacy emoji/label __tool_progress__ event introduced in #6972.

Changed files

  • gateway/platforms/api_server.py (modified, +52/-27)
  • tests/gateway/test_api_server.py (modified, +147/-12)

Code Example

scripts/run_tests.sh tests/gateway/test_api_server.py -k 'tool_progress or tool_completed_progress'
RAW_BUFFERClick to expand / collapse

Bug Description

/v1/chat/completions streaming emits custom event: hermes.tool.progress events for tool start, but it does not emit a matching completed progress event with the exact tool call id.

This leaves API-server clients that render tool lifecycle UI stuck in a running state until the full assistant response ends or until the client guesses completion locally. The issue is easy to miss on IM-style platforms because many adapters render transient progress messages differently, but API-server SSE consumers need explicit lifecycle events to update tool cards correctly.

Steps to Reproduce

  1. Start the API server gateway.
  2. Send a streaming /v1/chat/completions request that causes a tool call.
  3. Inspect the SSE stream.
  4. Observe event: hermes.tool.progress for the tool start, then text deltas/final response, but no status: completed event tied to the same toolCallId.

A minimal regression test can patch _run_agent to call tool_start_callback("call_terminal_1", "terminal", {...}), then tool_complete_callback("call_terminal_1", "terminal", {...}, result), and assert both custom SSE events are emitted.

Expected Behavior

For each non-internal tool call in chat-completions streaming, the API server should emit lifecycle progress events with stable correlation:

  • event: hermes.tool.progress with status: running and toolCallId
  • event: hermes.tool.progress with status: completed and the same toolCallId

The events should remain outside normal delta.content so clients do not persist progress markers into conversation history.

Actual Behavior

The chat-completions streaming branch only wires the legacy tool_progress_callback, which receives start-style events without exact call-id correlation. It does not pass tool_start_callback / tool_complete_callback into _run_agent, even though the Responses API streaming branch already uses those callbacks for structured function call lifecycle output.

Root Cause Analysis

In gateway/platforms/api_server.py, the /v1/chat/completions streaming branch defines _on_tool_progress() and passes only tool_progress_callback=_on_tool_progress to _run_agent().

AIAgent already emits exact lifecycle callbacks through tool_start_callback(tool_call_id, name, args) and tool_complete_callback(tool_call_id, name, args, result) in the tool execution paths. Those callbacks are available in the API server adapter and are used by the /v1/responses streaming path, but chat completions does not currently wire them.

Proposed Fix

Wire chat-completions streaming to tool_start_callback and tool_complete_callback, and queue custom hermes.tool.progress SSE payloads that include:

  • toolCallId
  • tool / name
  • status: running | completed
  • input / args
  • output / result for completed events

Keep the legacy tool_progress_callback as a start-only fallback for producers that do not provide exact call ids, and continue filtering internal tool names beginning with _.

Verification

A PR with regression coverage will run:

scripts/run_tests.sh tests/gateway/test_api_server.py -k 'tool_progress or tool_completed_progress'

extent analysis

TL;DR

Wire the chat-completions streaming to tool_start_callback and tool_complete_callback to emit custom hermes.tool.progress events with exact tool call ids.

Guidance

  • Review the _on_tool_progress function in gateway/platforms/api_server.py to understand how tool progress events are currently handled.
  • Update the /v1/chat/completions streaming branch to pass tool_start_callback and tool_complete_callback to _run_agent() to enable exact lifecycle callbacks.
  • Modify the tool_start_callback and tool_complete_callback functions to queue custom hermes.tool.progress SSE payloads with the required fields, such as toolCallId, tool, status, input, and output.
  • Ensure that the legacy tool_progress_callback is kept as a fallback for producers without exact call ids and that internal tool names starting with _ are filtered.

Example

def _on_tool_progress(tool_call_id, name, args, result=None):
    # Queue custom SSE payload for tool progress event
    if result is None:
        # Tool started
        payload = {
            'event': 'hermes.tool.progress',
            'data': {
                'toolCallId': tool_call_id,
                'tool': name,
                'status': 'running',
                'input': args
            }
        }
    else:
        # Tool completed
        payload = {
            'event': 'hermes.tool.progress',
            'data': {
                'toolCallId': tool_call_id,
                'tool': name,
                'status': 'completed',
                'input': args,
                'output': result
            }
        }
    # Send the payload as an SSE event

Notes

The proposed fix requires updates to the gateway/platforms/api_server.py file and may involve modifying the _run_agent function to accept the new callbacks

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix [Bug]: chat completions SSE does not emit tool completion progress [2 pull requests, 1 participants]