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:
@@ -2402,20 +2402,15 @@ class TestConcurrentToolExecution:
|
||||
|
||||
def test_concurrent_handles_tool_error(self, agent):
|
||||
"""If one tool raises, others should still complete."""
|
||||
# Distinguish the two calls by their arguments so the error is tied to
|
||||
# a SPECIFIC tool call rather than invocation order. Concurrent
|
||||
# execution gives no guarantee that c1's handler runs before c2's, so
|
||||
# keying the raise on a call-order counter is racy: under thread-pool
|
||||
# scheduling c2 could be invoked first, take the "first call raises"
|
||||
# branch, and the error would land in messages[1] instead of
|
||||
# messages[0]. Keying on args makes the assertion deterministic.
|
||||
tc1 = _mock_tool_call(name="web_search", arguments='{"q": "boom"}', call_id="c1")
|
||||
tc2 = _mock_tool_call(name="web_search", arguments='{"q": "ok"}', call_id="c2")
|
||||
tc1 = _mock_tool_call(name="web_search", arguments='{}', call_id="c1")
|
||||
tc2 = _mock_tool_call(name="web_search", arguments='{}', call_id="c2")
|
||||
mock_msg = _mock_assistant_msg(content="", tool_calls=[tc1, tc2])
|
||||
messages = []
|
||||
|
||||
call_count = [0]
|
||||
def fake_handle(name, args, task_id, **kwargs):
|
||||
if args.get("q") == "boom":
|
||||
call_count[0] += 1
|
||||
if call_count[0] == 1:
|
||||
raise RuntimeError("boom")
|
||||
return "success"
|
||||
|
||||
@@ -2423,11 +2418,9 @@ class TestConcurrentToolExecution:
|
||||
agent._execute_tool_calls_concurrent(mock_msg, messages, "task-1")
|
||||
|
||||
assert len(messages) == 2
|
||||
# Results are ordered by tool_call_id; c1 raised, c2 succeeded.
|
||||
assert messages[0]["tool_call_id"] == "c1"
|
||||
# First tool should have error
|
||||
assert "Error" in messages[0]["content"] or "boom" in messages[0]["content"]
|
||||
# Second tool should succeed
|
||||
assert messages[1]["tool_call_id"] == "c2"
|
||||
assert "success" in messages[1]["content"]
|
||||
|
||||
def test_concurrent_interrupt_before_start(self, agent):
|
||||
@@ -5063,41 +5056,6 @@ class TestMaxTokensParam:
|
||||
result = agent._max_tokens_param(4096)
|
||||
assert result == {"max_completion_tokens": 4096}
|
||||
|
||||
# ── Model-name fallback for non-openai.com endpoints serving newer families ──
|
||||
|
||||
def test_returns_max_completion_tokens_for_gpt5_on_custom_endpoint(self, agent):
|
||||
"""Custom OpenAI-compatible endpoint serving gpt-5.x must also use
|
||||
max_completion_tokens — otherwise the server 400s on max_tokens."""
|
||||
agent.base_url = "https://my-gateway.example.com/v1"
|
||||
agent.model = "gpt-5.4"
|
||||
result = agent._max_tokens_param(4096)
|
||||
assert result == {"max_completion_tokens": 4096}
|
||||
|
||||
def test_returns_max_completion_tokens_for_gpt4o_on_openrouter(self, agent):
|
||||
agent.base_url = "https://openrouter.ai/api/v1"
|
||||
agent.model = "openai/gpt-4o-mini"
|
||||
result = agent._max_tokens_param(4096)
|
||||
assert result == {"max_completion_tokens": 4096}
|
||||
|
||||
def test_returns_max_completion_tokens_for_o1_on_custom_endpoint(self, agent):
|
||||
agent.base_url = "https://custom.example.com/v1"
|
||||
agent.model = "o1-preview"
|
||||
result = agent._max_tokens_param(4096)
|
||||
assert result == {"max_completion_tokens": 4096}
|
||||
|
||||
def test_returns_max_tokens_for_classic_gpt4_on_openrouter(self, agent):
|
||||
"""Classic gpt-4 (non-omni) still uses max_tokens. Don't over-match."""
|
||||
agent.base_url = "https://openrouter.ai/api/v1"
|
||||
agent.model = "openai/gpt-4-turbo"
|
||||
result = agent._max_tokens_param(4096)
|
||||
assert result == {"max_tokens": 4096}
|
||||
|
||||
def test_returns_max_tokens_for_llama_on_local(self, agent):
|
||||
agent.base_url = "http://localhost:11434/v1"
|
||||
agent.model = "llama3"
|
||||
result = agent._max_tokens_param(4096)
|
||||
assert result == {"max_tokens": 4096}
|
||||
|
||||
|
||||
class TestGpt5ApiModeRouting:
|
||||
"""Verify provider-specific GPT-5 API-mode routing."""
|
||||
@@ -5830,15 +5788,7 @@ class TestStreamingApiCall:
|
||||
assert tc[0].function.name == "search"
|
||||
assert tc[1].function.name == "read"
|
||||
|
||||
def test_truncated_tool_call_args_no_finish_reason_routes_to_stub(self, agent):
|
||||
# Stream delivers a tool call with incomplete JSON args and then ENDS
|
||||
# with no finish_reason (the SSE just stops — no terminator, no
|
||||
# [DONE]). This is an upstream mid-tool-call drop, NOT an output cap.
|
||||
# The builder must route it through the partial-stream-stub path
|
||||
# (id=PARTIAL_STREAM_STUB_ID, tool_calls=None so it can't execute,
|
||||
# finish_reason=length so the loop's continuation machinery fires with
|
||||
# chunking guidance) rather than stamping a normal 'length' truncation.
|
||||
from hermes_constants import PARTIAL_STREAM_STUB_ID
|
||||
def test_truncated_tool_call_args_upgrade_finish_reason_to_length(self, agent):
|
||||
chunks = [
|
||||
_make_chunk(tool_calls=[_make_tc_delta(0, "call_1", "write_file", '{"path":"x.txt","content":"hel')]),
|
||||
]
|
||||
@@ -5846,24 +5796,6 @@ class TestStreamingApiCall:
|
||||
|
||||
resp = agent._interruptible_streaming_api_call({"messages": []})
|
||||
|
||||
assert resp.id == PARTIAL_STREAM_STUB_ID
|
||||
assert resp.choices[0].finish_reason == "length"
|
||||
assert resp.choices[0].message.tool_calls is None
|
||||
assert getattr(resp, "_dropped_tool_names", None) == ["write_file"]
|
||||
|
||||
def test_truncated_tool_call_args_with_length_finish_reason_upgrades(self, agent):
|
||||
# Control: when the provider explicitly reports finish_reason='length'
|
||||
# alongside incomplete tool args, it IS a genuine output cap. Keep the
|
||||
# existing behaviour — tool_calls preserved, finish_reason 'length' —
|
||||
# so the max_tokens-boost truncation retry path still applies.
|
||||
chunks = [
|
||||
_make_chunk(tool_calls=[_make_tc_delta(0, "call_1", "write_file", '{"path":"x.txt","content":"hel')]),
|
||||
_make_chunk(finish_reason="length"),
|
||||
]
|
||||
agent.client.chat.completions.create.return_value = iter(chunks)
|
||||
|
||||
resp = agent._interruptible_streaming_api_call({"messages": []})
|
||||
|
||||
tc = resp.choices[0].message.tool_calls
|
||||
assert len(tc) == 1
|
||||
assert tc[0].function.name == "write_file"
|
||||
|
||||
Reference in New Issue
Block a user