opentui(phase3): launcher integration — HERMES_TUI_ENGINE dual-engine

hermes --tui launches the native OpenTUI engine (Bun) when
HERMES_TUI_ENGINE=opentui (env) or display.tui_engine=opentui (config);
Ink stays the default and the shipping path is untouched.

- _resolve_tui_engine() (env > config > ink); refuses opentui on
  Windows/Termux (no Bun) -> falls back to ink with a notice.
- _make_opentui_argv() -> [bun, src/entry.real.tsx] (no build step).
- _bun_bin() with HERMES_BUN override.
- Branch at top of _make_tui_argv BEFORE _ensure_tui_node (Bun-only host
  must not bootstrap Node).
- Gate _launch_tui NODE_OPTIONS/--max-old-space-size on engine==ink (Bun
  is JSC; the V8 flag errors/ignores).

Verified end-to-end via tmux: real hermes --tui -> Bun -> OpenTUI ->
real Python gateway streamed a real reply. No-flag default still ink.
This commit is contained in:
alt-glitch
2026-06-08 11:11:54 +00:00
parent 24f74eb888
commit 2bd9c9b881
741 changed files with 17733 additions and 79889 deletions
+8 -48
View File
@@ -196,7 +196,7 @@ from agent.tool_dispatch_helpers import (
_extract_error_preview,
_trajectory_normalize_msg, # noqa: F401 # re-exported for tests that `from run_agent import _trajectory_normalize_msg`
)
from utils import atomic_json_write, base_url_host_matches, base_url_hostname, is_truthy_value, model_forces_max_completion_tokens
from utils import atomic_json_write, base_url_host_matches, base_url_hostname, is_truthy_value
@@ -358,7 +358,6 @@ class AIAgent:
save_trajectories: bool = False,
verbose_logging: bool = False,
quiet_mode: bool = False,
tool_progress_mode: str = "all",
ephemeral_system_prompt: str = None,
log_prefix_chars: int = 100,
log_prefix: str = "",
@@ -376,7 +375,6 @@ class AIAgent:
thinking_callback: callable = None,
reasoning_callback: callable = None,
clarify_callback: callable = None,
read_terminal_callback: callable = None,
step_callback: callable = None,
stream_delta_callback: callable = None,
interim_assistant_callback: callable = None,
@@ -432,7 +430,6 @@ class AIAgent:
save_trajectories=save_trajectories,
verbose_logging=verbose_logging,
quiet_mode=quiet_mode,
tool_progress_mode=tool_progress_mode,
ephemeral_system_prompt=ephemeral_system_prompt,
log_prefix_chars=log_prefix_chars,
log_prefix=log_prefix,
@@ -450,7 +447,6 @@ class AIAgent:
thinking_callback=thinking_callback,
reasoning_callback=reasoning_callback,
clarify_callback=clarify_callback,
read_terminal_callback=read_terminal_callback,
step_callback=step_callback,
stream_delta_callback=stream_delta_callback,
interim_assistant_callback=interim_assistant_callback,
@@ -1253,24 +1249,13 @@ class AIAgent:
def _max_tokens_param(self, value: int) -> dict:
"""Return the correct max tokens kwarg for the current provider.
OpenAI's newer models (gpt-4o, gpt-4.1, gpt-5+, o-series) require
'max_completion_tokens'. Azure OpenAI and GitHub Copilot also require
'max_completion_tokens' for those families served via their
OpenAI-compatible endpoints. OpenRouter, local models, and older
OpenAI's newer models (gpt-4o, o-series, gpt-5+) require
'max_completion_tokens'. Azure OpenAI also requires
'max_completion_tokens' for gpt-5.x models served via the
OpenAI-compatible endpoint. OpenRouter, local models, and older
OpenAI models use 'max_tokens'.
The check is URL-first (api.openai.com / Azure / Copilot all use the
new kwarg), then falls back to a model-name check so third-party
OpenAI-compatible endpoints fronting those models are recognised —
URL-only detection misses that case and silently sends the wrong
kwarg, which the upstream model rejects with a 400.
"""
if (
self._is_direct_openai_url()
or self._is_azure_openai_url()
or self._is_github_copilot_url()
or model_forces_max_completion_tokens(self.model)
):
if self._is_direct_openai_url() or self._is_azure_openai_url() or self._is_github_copilot_url():
return {"max_completion_tokens": value}
return {"max_tokens": value}
@@ -2831,18 +2816,11 @@ class AIAgent:
if state is None:
return
try:
from agent.credits_tracker import evaluate_credits_notices, is_free_tier_model
from agent.credits_tracker import evaluate_credits_notices
latch = getattr(self, "_credits_latch", None)
if latch is None:
latch = self._credits_latch = {"active": set(), "seen_below_90": False, "usage_band": None}
# Free-model gate: a depleted account on a free model can still
# inference, so the depleted error banner is suppressed. Local-data
# only (":free" suffix + pricing-cache peek) — never a network call.
model_is_free = is_free_tier_model(
getattr(self, "model", "") or "",
getattr(self, "base_url", "") or "",
)
to_show, to_clear = evaluate_credits_notices(state, latch, model_is_free=model_is_free)
to_show, to_clear = evaluate_credits_notices(state, latch)
for key in to_clear: # clears FIRST …
self._emit_notice_clear(key)
for notice in to_show: # … then shows (depleted lands last in a latest-wins slot)
@@ -3109,17 +3087,6 @@ class AIAgent:
except Exception:
pass
# 6. Free conversation history. Mirrors _release_evicted_agent_soft's
# soft-eviction clear — close() is the hard teardown for true session
# boundaries (/new, /reset, session expiry), so the message list won't
# be reused. Drops the reference proactively rather than waiting for
# the agent object itself to be collected, which matters when a caller
# still holds the closed agent (e.g. a draining background task).
try:
self._session_messages = []
except Exception:
pass
def _hydrate_todo_store(self, history: List[Dict[str, Any]]) -> None:
"""
Recover todo state from conversation history.
@@ -3937,13 +3904,6 @@ class AIAgent:
def _anthropic_messages_create(self, api_kwargs: dict):
if self.api_mode == "anthropic_messages":
self._try_refresh_anthropic_client_credentials()
# Defensive: strip Responses-only kwargs that can leak in under an
# api_mode-flip race (the Anthropic SDK raises a non-retryable
# TypeError on them). See #31673.
from agent.anthropic_adapter import sanitize_anthropic_kwargs
sanitize_anthropic_kwargs(
api_kwargs, log_prefix=getattr(self, "log_prefix", "")
)
return self._anthropic_client.messages.create(**api_kwargs)
def _rebuild_anthropic_client(self) -> None: