refactor(cli): lift main()'s single-query branch into _run_single_query_mode + _configure_quiet_agent

This commit is contained in:
Teknium
2026-09-02 19:20:44 -07:00
parent b731a1675f
commit 719ad479ac
2 changed files with 96 additions and 94 deletions
+83 -90
View File
@@ -8626,6 +8626,87 @@ def _start_worktree_setup(list_tools, list_toolsets, worktree, w):
return _join_worktree
def _configure_quiet_agent(agent) -> None:
"""Neutralize every stdout-writing callback so -Q stdout carries only the final response (#93220)."""
agent.quiet_mode = True
agent.suppress_status_output = True
# No styled "Hermes" box, tool-gen status lines, or reasoning box.
agent.stream_delta_callback = None
agent.tool_gen_callback = None
agent.reasoning_callback = None
# Inline-diff and progress callbacks print directly to stdout and are gated
# by NEITHER quiet_mode nor tool_progress_mode (_on_tool_complete renders
# full file diffs; _on_tool_progress prints MoA reference blocks before its
# mode check), so they must go too.
agent.tool_progress_callback = None
agent.tool_start_callback = None
agent.tool_complete_callback = None
# Belt-and-braces for the executor's direct prints (they check
# agent.tool_progress_mode, initialized from display.tool_progress).
agent.tool_progress_mode = "off"
def _run_single_query_mode(cli, query, image, quiet, oneshot):
"""``-q``/``--image`` entry: seed an interactive session on a TTY, else run the one-shot turn and exit."""
# NEW DEFAULT (Aug 2026): on a real TTY, -q/--image seeds a normal interactive
# session with the prompt as the first turn, submitted LITERALLY (no slash/!
# dispatch). Answer-and-exit is kept for --oneshot, -Q, and every non-TTY
# invocation (kanban/cron/pipes) — see _should_seed_interactive().
if _should_seed_interactive(query, image, quiet, oneshot):
seeded_query, seeded_images = _collect_query_images(query, image)
logger.info(
"Seeding interactive session with -q prompt (%d chars, %d images)",
len(seeded_query or ""), len(seeded_images),
)
cli._seeded_first_message = _SeededQueryMessage(seeded_query, seeded_images)
cli.run()
return
# One-shot: no between-turns MCP late-binding refresh, so the agent waits the
# full MCP cold-start bound before its only tool snapshot (#51316).
cli._single_query_mode = True
# A -q run has NO user to answer approval prompts: the approval gate reads
# this marker (gateway.session_context.get_session_env, falling back to
# os.environ) and takes the deterministic approvals.single_query_mode path
# instead of waiting the full timeout (#86878).
os.environ["HERMES_SINGLE_QUERY_SESSION"] = "1"
if not cli._claim_active_session("cli", stderr=bool(quiet)):
sys.exit(1)
try:
query, single_query_images = _collect_query_images(query, image)
single_query_image_urls = _collect_kanban_task_images(single_query_images)
if quiet:
# Quiet mode: suppress banner, spinner, tool previews.
# Only print the final response and parseable session info.
cli.tool_progress_mode = "off"
if cli._ensure_runtime_credentials():
effective_query: Any = query
effective_query = _route_single_query_images(cli, query, effective_query, single_query_images, single_query_image_urls)
turn_route = cli._resolve_turn_agent_config(effective_query)
if turn_route["signature"] != cli._active_agent_route_signature:
cli.agent = None
if cli._init_agent(
model_override=turn_route["model"],
runtime_override=turn_route["runtime"],
request_overrides=turn_route.get("request_overrides"),
):
_configure_quiet_agent(cli.agent)
_run_quiet_single_query(cli, effective_query)
# Exit with error code if credentials or agent init fails
sys.exit(1)
# Single-query (`-q`): skip the welcome banner (~420 ms cold — version
# check + toolset/skill enumeration + Rich render). The session id /
# resume hint come from _print_exit_summary().
_query_label = query or ("[image attached]" if single_query_images else "")
if _query_label:
cli.console.print(f"[bold blue]Query:[/] {_query_label}")
cli._show_security_advisories()
cli.chat(query, images=single_query_images or None)
cli._print_exit_summary(clear_screen=False)
finally:
_finalize_single_query(cli)
def main(
query: str = None,
q: str = None,
@@ -8756,98 +8837,10 @@ def main(
_install_single_query_signal_handlers(cli)
# Handle single query mode
if query or image:
# NEW DEFAULT (Aug 2026): on a real TTY, a -q/--image invocation
# seeds a normal interactive session with the prompt as the first
# turn, submitted LITERALLY (no slash/! dispatch). Legacy
# answer-and-exit behavior is kept for --oneshot, -Q, and every
# non-TTY invocation (kanban/cron/pipes) — see
# _should_seed_interactive().
if _should_seed_interactive(query, image, quiet, oneshot):
seeded_query, seeded_images = _collect_query_images(query, image)
logger.info(
"Seeding interactive session with -q prompt (%d chars, %d images)",
len(seeded_query or ""), len(seeded_images),
)
cli._seeded_first_message = _SeededQueryMessage(seeded_query, seeded_images)
cli.run()
return
# One-shot mode: no between-turns MCP late-binding refresh, so the
# agent must wait the full MCP cold-start bound before its first
# (and only) tool snapshot. See #51316.
cli._single_query_mode = True
# Mark single-query for the approval gate. cli.py sets
# HERMES_INTERACTIVE earlier for interactive sudo prompts, but a -q
# run has NO user waiting to answer approval prompts. The gate reads
# this marker (via gateway.session_context.get_session_env, which falls
# back to os.environ when the session-context layer isn't engaged) and
# takes the deterministic approvals.single_query_mode path instead of
# waiting the full timeout. See #86878.
os.environ["HERMES_SINGLE_QUERY_SESSION"] = "1"
if not cli._claim_active_session("cli", stderr=bool(quiet)):
sys.exit(1)
try:
query, single_query_images = _collect_query_images(query, image)
single_query_image_urls = _collect_kanban_task_images(single_query_images)
if quiet:
# Quiet mode: suppress banner, spinner, tool previews.
# Only print the final response and parseable session info.
cli.tool_progress_mode = "off"
if cli._ensure_runtime_credentials():
effective_query: Any = query
effective_query = _route_single_query_images(cli, query, effective_query, single_query_images, single_query_image_urls)
turn_route = cli._resolve_turn_agent_config(effective_query)
if turn_route["signature"] != cli._active_agent_route_signature:
cli.agent = None
if cli._init_agent(
model_override=turn_route["model"],
runtime_override=turn_route["runtime"],
request_overrides=turn_route.get("request_overrides"),
):
cli.agent.quiet_mode = True
cli.agent.suppress_status_output = True
# Suppress streaming display callbacks so stdout stays
# machine-readable (no styled "Hermes" box, no tool-gen
# status lines, no reasoning box). The response is
# printed once below.
cli.agent.stream_delta_callback = None
cli.agent.tool_gen_callback = None
cli.agent.reasoning_callback = None
# Inline-diff and progress callbacks print directly to
# stdout and are gated by NEITHER quiet_mode nor
# tool_progress_mode: _on_tool_complete renders full
# file diffs via render_edit_diff_with_delta, and
# _on_tool_progress prints MoA reference blocks before
# its mode check. Neutralize them too so -Q stdout
# carries only the final response (#93220).
cli.agent.tool_progress_callback = None
cli.agent.tool_start_callback = None
cli.agent.tool_complete_callback = None
# Belt-and-braces for the executor's direct prints
# (they check agent.tool_progress_mode, initialized
# from display.tool_progress at construction).
cli.agent.tool_progress_mode = "off"
_run_quiet_single_query(cli, effective_query)
# Exit with error code if credentials or agent init fails
sys.exit(1)
else:
# Single-query (`-q`): skip the welcome banner (~420 ms cold —
# version check + toolset/skill enumeration + Rich render). The
# session id / resume hint come from _print_exit_summary().
_query_label = query or ("[image attached]" if single_query_images else "")
if _query_label:
cli.console.print(f"[bold blue]Query:[/] {_query_label}")
# Surface security advisories before the agent runs — short
# banner, doesn't depend on the welcome banner being shown.
cli._show_security_advisories()
cli.chat(query, images=single_query_images or None)
cli._print_exit_summary(clear_screen=False)
finally:
_finalize_single_query(cli)
_run_single_query_mode(cli, query, image, quiet, oneshot)
return
# Run interactive mode
cli.run()
+13 -4
View File
@@ -24,12 +24,19 @@ _QUIET_ANCHOR = "# Quiet mode: suppress banner, spinner, tool previews."
def _quiet_branch() -> str:
"""Return the source slice of the quiet single-query branch."""
"""Return the source of the quiet single-query branch.
The callback neutralizations live in ``_configure_quiet_agent`` (called from
the quiet branch of ``_run_single_query_mode``); the branch itself is the
window from the quiet-mode anchor to the ``_run_quiet_single_query`` hand-off.
"""
import inspect
source = Path(cli_mod.__file__).read_text(encoding="utf-8")
start = source.index(_QUIET_ANCHOR)
# A generous window covers the whole branch body up to the
# run_conversation call and beyond.
return source[start : start + 8000]
branch = source[start : start + 8000]
helper = inspect.getsource(cli_mod._configure_quiet_agent).replace("agent.", "cli.agent.")
return helper + branch
def test_quiet_branch_clears_reasoning_callback():
@@ -98,7 +105,9 @@ def test_quiet_branch_neutralizations_precede_run_conversation():
import inspect
assert "run_conversation(" in inspect.getsource(cli_mod._run_quiet_single_query)
assert "_configure_quiet_agent(cli.agent)" in branch
run_idx = branch.index("_run_quiet_single_query(cli,")
assert branch.index("_configure_quiet_agent(cli.agent)") < run_idx
for attr in (
"cli.agent.reasoning_callback = None",
"cli.agent.tool_progress_callback = None",