mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:46:59 +00:00
fix(tools): use idle timeout for command TTS
This commit is contained in:
parent
43333acdda
commit
3884f078bd
2 changed files with 122 additions and 10 deletions
|
|
@ -14,6 +14,8 @@ differences) Windows.
|
|||
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
|
@ -37,6 +39,7 @@ from tools.tts_tool import (
|
|||
_render_command_tts_template,
|
||||
_resolve_command_provider_config,
|
||||
_resolve_max_text_length,
|
||||
_run_command_tts,
|
||||
_shell_quote_context,
|
||||
check_tts_requirements,
|
||||
text_to_speech_tool,
|
||||
|
|
@ -57,6 +60,13 @@ def _python_copy_command(output_placeholder: str = "{output_path}") -> str:
|
|||
)
|
||||
|
||||
|
||||
def _shell_command(*args: str) -> str:
|
||||
"""Return a shell command string for subprocess.Popen(shell=True)."""
|
||||
if os.name == "nt":
|
||||
return subprocess.list2cmdline(list(args))
|
||||
return " ".join(shlex.quote(str(arg)) for arg in args)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _resolve_command_provider_config / built-in precedence
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -348,6 +358,53 @@ class TestRenderCommandTtsTemplate:
|
|||
assert '"bob\'s voice"' in rendered
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _run_command_tts idle/progress timeout behavior
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestRunCommandTts:
|
||||
def test_stderr_progress_extends_beyond_timeout(self, tmp_path):
|
||||
script = tmp_path / "progress_then_exit.py"
|
||||
script.write_text(
|
||||
"\n".join([
|
||||
"import sys, time",
|
||||
"for idx in range(4):",
|
||||
" print(f'tick {idx}', file=sys.stderr, flush=True)",
|
||||
" time.sleep(0.15)",
|
||||
"print('done', flush=True)",
|
||||
]),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = _run_command_tts(
|
||||
_shell_command(sys.executable, "-u", str(script)),
|
||||
timeout=0.25,
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "tick 3" in result.stderr
|
||||
assert "done" in result.stdout
|
||||
|
||||
def test_silent_after_progress_still_times_out_with_stderr(self, tmp_path):
|
||||
script = tmp_path / "progress_then_hang.py"
|
||||
script.write_text(
|
||||
"\n".join([
|
||||
"import sys, time",
|
||||
"print('starting tier 1', file=sys.stderr, flush=True)",
|
||||
"time.sleep(1.0)",
|
||||
]),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(subprocess.TimeoutExpired) as excinfo:
|
||||
_run_command_tts(
|
||||
_shell_command(sys.executable, "-u", str(script)),
|
||||
timeout=0.2,
|
||||
)
|
||||
|
||||
assert "starting tier 1" in (excinfo.value.stderr or "")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# End-to-end: _generate_command_tts
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import shutil
|
|||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
|
@ -999,7 +1000,7 @@ def _terminate_command_tts_process_tree(proc: subprocess.Popen) -> None:
|
|||
|
||||
|
||||
def _run_command_tts(command: str, timeout: float) -> subprocess.CompletedProcess:
|
||||
"""Run a command-provider shell command with process-tree timeout cleanup."""
|
||||
"""Run a command-provider shell command with process-tree idle cleanup."""
|
||||
from agent.delegation_context import delegated_child_subprocess_env
|
||||
|
||||
popen_kwargs: Dict[str, Any] = {
|
||||
|
|
@ -1019,21 +1020,75 @@ def _run_command_tts(command: str, timeout: float) -> subprocess.CompletedProces
|
|||
popen_kwargs["start_new_session"] = True
|
||||
|
||||
proc = subprocess.Popen(command, **popen_kwargs, stdin=subprocess.DEVNULL)
|
||||
try:
|
||||
stdout, stderr = proc.communicate(timeout=timeout)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
_terminate_command_tts_process_tree(proc)
|
||||
output_queue: "queue.Queue[tuple[str, Optional[str]]]" = queue.Queue()
|
||||
chunks: Dict[str, list[str]] = {"stdout": [], "stderr": []}
|
||||
open_streams = {"stdout", "stderr"}
|
||||
|
||||
def read_stream(name: str, stream: Any) -> None:
|
||||
try:
|
||||
stdout, stderr = proc.communicate(timeout=1)
|
||||
except Exception:
|
||||
stdout = getattr(exc, "output", None)
|
||||
stderr = getattr(exc, "stderr", None)
|
||||
while True:
|
||||
chunk = stream.read(1)
|
||||
if not chunk:
|
||||
break
|
||||
output_queue.put((name, chunk))
|
||||
finally:
|
||||
output_queue.put((name, None))
|
||||
|
||||
readers = [
|
||||
threading.Thread(
|
||||
target=read_stream,
|
||||
args=("stdout", proc.stdout),
|
||||
daemon=True,
|
||||
),
|
||||
threading.Thread(
|
||||
target=read_stream,
|
||||
args=("stderr", proc.stderr),
|
||||
daemon=True,
|
||||
),
|
||||
]
|
||||
for reader in readers:
|
||||
reader.start()
|
||||
|
||||
deadline = time.monotonic() + timeout
|
||||
timed_out = False
|
||||
while open_streams:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
timed_out = True
|
||||
break
|
||||
try:
|
||||
name, chunk = output_queue.get(timeout=min(0.05, remaining))
|
||||
except queue.Empty:
|
||||
continue
|
||||
if chunk is None:
|
||||
open_streams.discard(name)
|
||||
continue
|
||||
chunks[name].append(chunk)
|
||||
deadline = time.monotonic() + timeout
|
||||
|
||||
if timed_out:
|
||||
_terminate_command_tts_process_tree(proc)
|
||||
for reader in readers:
|
||||
reader.join(timeout=0.5)
|
||||
while True:
|
||||
try:
|
||||
name, chunk = output_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
if chunk:
|
||||
chunks[name].append(chunk)
|
||||
stdout = "".join(chunks["stdout"])
|
||||
stderr = "".join(chunks["stderr"])
|
||||
raise subprocess.TimeoutExpired(
|
||||
command,
|
||||
timeout,
|
||||
output=stdout,
|
||||
stderr=stderr,
|
||||
) from exc
|
||||
)
|
||||
|
||||
stdout = "".join(chunks["stdout"])
|
||||
stderr = "".join(chunks["stderr"])
|
||||
proc.wait()
|
||||
|
||||
if proc.returncode:
|
||||
raise subprocess.CalledProcessError(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue