Skip to content

perf(acp): reduce hermes acp cold-start connect time by ~4× - #56601

Closed
alanjds wants to merge 4 commits into
NousResearch:mainfrom
alanjds:feature/acp-fasttrack
Closed

perf(acp): reduce hermes acp cold-start connect time by ~4×#56601
alanjds wants to merge 4 commits into
NousResearch:mainfrom
alanjds:feature/acp-fasttrack

Conversation

@alanjds

@alanjds alanjds commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Issue

ACP process startup is slow (~66s from spawn to initialize response), blocking IDE integrations and making the hermes acp CLI command feel unresponsive.

Improvements

The savings are based on:

  • CLI import got completely bypassed for hermes acp — The fast-path in hermes_cli/main.py detects the acp subcommand before loading rich, prompt_toolkit, argparse, and all subcommands (~7s saved)
  • MCP discovery no longer blocks the initialize handshake — Tool discovery is scheduled as a background task (asyncio.create_task()) instead of blocking the main thread pre-loop (~11s saved)
  • Heavy imports run in a background thread while the loop services the client's first messageimport acp and pydantic/opentelemetry imports run in asyncio.to_thread(), keeping the event loop responsive and able to buffer incoming initialize bytes

Result

~4× speedup: Process now responds to initialize within 13–17s (cold pydantic cache), down from ~66s.

Changes

  • hermes_cli/main.py — Fast-path for hermes acp (mirrors existing Termux fast-path pattern)
  • acp_adapter/entry.py — Async startup with deferred heavy imports and background MCP discovery
  • tests/acp/test_entry.py — Updated test to stub asyncio pipe connections

alanjds added 3 commits June 29, 2026 19:00
Three changes that together cut the time from process spawn to the first
ACP initialize response from ~66s down to ~13–17s (cold pydantic cache).

hermes_cli/main.py — fast-path for `hermes acp`
  Detect the `acp` subcommand before the heavy module-level imports in
  hermes_cli.main (rich, prompt_toolkit, argparse, all subcommands) and
  jump directly to acp_adapter.entry.main.  This alone saves ~7s of
  import time on every `hermes acp` invocation.  Profile flag (-p/--profile)
  is honoured before the jump.  Pattern mirrors the existing Termux fast-path.

acp_adapter/entry.py — async startup with deferred heavy imports
  Replace the synchronous, pre-loop startup sequence with an async _run()
  coroutine that:
  1. Wires sys.stdin to an asyncio StreamReader immediately on event-loop
     start, so the client's initialize bytes are buffered in the kernel pipe
     while imports are in flight — they are never dropped.
  2. Runs `import acp` + `from acp_adapter.server import HermesACPAgent` in
     asyncio.to_thread(), keeping the event loop alive and responsive during
     the ~8s pydantic/opentelemetry import cost.
  3. Schedules MCP tool discovery (discover_mcp_tools) as a separate
     asyncio.create_task() background thread, so it does not delay the
     initialize response.
  Previously, discover_mcp_tools() blocked the main thread synchronously
  before asyncio.run() was even called, adding another ~11s.

tests/acp/test_entry.py — update test_main_enables_unstable_protocol
  The test now stubs connect_read_pipe / connect_write_pipe on
  asyncio.BaseEventLoop so the test does not require real stdio, and patches
  remain effective since Python's import cache ensures the same acp module
  object is used inside _heavy_imports.
@alt-glitch alt-glitch added type/perf Performance improvement or optimization P4 Best-effort: we will get to it when we get to it (no commitment) comp/acp Agent Communication Protocol adapter comp/cli CLI entry point, hermes_cli/, setup wizard tool/mcp MCP client and OAuth labels Jul 1, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for targeting a verified ACP cold-start bottleneck: current acp_adapter/entry.py:246-262 imports the ACP server and runs configured MCP discovery before starting the event loop.

Problems

  • hermes_cli/main.py:298 forwards ACP arguments straight to acp_adapter.entry. The normal ACP parser adds --accept-hooks (hermes_cli/subcommands/acp.py:21, _shared.py:21), while entry._parse_args() does not. Thus hermes acp --accept-hooks regresses to an argparse error.
  • acp_adapter/entry.py:287 backgrounds configured MCP discovery without coordinating with the first session. ACP constructs AIAgent in acp_adapter/session.py:645; its tool list is snapshotted in agent/agent_init.py:1195-1205. tools/mcp_tool.py:5444-5476 documents the required refresh when discovery finishes after that snapshot. The PR does not provide a bounded wait or ACP late-refresh path.

Suggested changes

  • Preserve ACP parser semantics in the fast path, especially --accept-hooks, rather than duplicating partial argument/profile handling.
  • Keep initialize non-blocking but synchronize discovery with the first agent snapshot or refresh it at a safe turn boundary; add a blocked-discovery regression test.
  • Add an ACP initialize transport test; the changed unit test only mocks pipes and checks the protocol flag.

Automated hermes-sweeper review.

Comment thread hermes_cli/main.py
_profiles_root = _Path.home() / ".hermes" / "profiles"
_os.environ["HERMES_HOME"] = str(_profiles_root / _profile)
from acp_adapter.entry import main as _acp_main # noqa: PLC0415
_acp_main(argv[i + 1:])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This bypasses the normal ACP parser, which accepts --accept-hooks through build_acp_parser(); acp_adapter.entry._parse_args() does not accept it. Preserve that flag’s behavior (and avoid duplicating the profile pre-parser) before forwarding arguments.

Comment thread acp_adapter/entry.py
except Exception:
logger.debug("MCP tool discovery failed at ACP startup", exc_info=True)

_asyncio.create_task(_discover_mcp_bg())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Configured MCP discovery can still be running when the client creates its first session. That session snapshots AIAgent.tools before the registry is populated, and this path has no bounded join or ACP late-refresh. Coordinate the task with the first tool snapshot or refresh at a safe turn boundary.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 15, 2026
@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Thanks for this — the analysis was right when filed, but the codebase has since absorbed each of the three optimizations through other merges, and the timings no longer reproduce on current main:

  1. Blocking MCP discovery at ACP startup — fixed by perf(acp): non-blocking startup via background MCP discovery + cache-safe late refresh #75985 (merged Aug 1, salvaging perf(mcp): non-blocking startup via background MCP discovery #32811): ACP now uses the shared hermes_cli/mcp_startup.py background-discovery daemon that CLI/TUI/gateway use, with a bounded snapshot wait before agent build, a cache-safe late refresh, and retry-after-failed-first-run — plus fix(acp): allow hosts to skip configured MCP startup #70405's HERMES_ACP_SKIP_CONFIGURED_MCP opt-out. Applying this branch's asyncio.create_task() variant would revert both.

  2. ~7s CLI import for hermes acp — the lazy-import work on main since July erased this. Measured on current main: import hermes_cli.main = 0.13s, full hermes acp --version end-to-end = 0.70s.

  3. ~8s pydantic/otel import for import acp — measured 0.14s on current main.

For IDE integrations wanting zero CLI overhead, the hermes-acp launcher (a [project.scripts] entry pointing straight at acp_adapter.entry:main, installed on PATH and self-healed by hermes update) already provides the fast path this PR's main.py bypass targets — 0.04s to --version, and it's documented as a first-class launch method on the ACP docs page.

Closing as superseded on current main rather than for any defect in the work — the direction was sound, and the MCP-discovery half of it is exactly what ended up shipping via #75985's design.

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/cli CLI entry point, hermes_cli/, setup wizard P4 Best-effort: we will get to it when we get to it (no commitment) sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows tool/mcp MCP client and OAuth type/perf Performance improvement or optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants