fix: pass runtime route details into webui agent — v0.50.66
Forwards `api_mode`, `acp_command`, `acp_args`, and `credential_pool` from the resolved runtime provider into `AIAgent.__init__()` in the WebUI streaming path. Fixes Codex account switching and credential pool support for WebUI sessions. Also adds 6 defensive variable initializations to prevent NameError in cleanup paths. Tests: 1329 passed, 0 skipped. Full TestRuntimeRouteInjection suite passes. PR by @suinia. Rebased and CHANGELOG added by maintainer. Co-authored-by: suinia <suinia@users.noreply.github.com>
This commit is contained in:
@@ -551,6 +551,12 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta
|
|||||||
q = STREAMS.get(stream_id)
|
q = STREAMS.get(stream_id)
|
||||||
if q is None:
|
if q is None:
|
||||||
return
|
return
|
||||||
|
s = None
|
||||||
|
_rt = {}
|
||||||
|
old_cwd = None
|
||||||
|
old_exec_ask = None
|
||||||
|
old_session_key = None
|
||||||
|
old_hermes_home = None
|
||||||
|
|
||||||
# ── MCP Server Discovery (lazy import, idempotent) ──
|
# ── MCP Server Discovery (lazy import, idempotent) ──
|
||||||
# discover_mcp_tools() is called here (rather than at server startup) so that
|
# discover_mcp_tools() is called here (rather than at server startup) so that
|
||||||
@@ -829,6 +835,10 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta
|
|||||||
provider=resolved_provider,
|
provider=resolved_provider,
|
||||||
base_url=resolved_base_url,
|
base_url=resolved_base_url,
|
||||||
api_key=resolved_api_key,
|
api_key=resolved_api_key,
|
||||||
|
api_mode=_rt.get('api_mode'),
|
||||||
|
acp_command=_rt.get('command'),
|
||||||
|
acp_args=_rt.get('args'),
|
||||||
|
credential_pool=_rt.get('credential_pool'),
|
||||||
platform='cli',
|
platform='cli',
|
||||||
quiet_mode=True,
|
quiet_mode=True,
|
||||||
enabled_toolsets=_toolsets,
|
enabled_toolsets=_toolsets,
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ Covers:
|
|||||||
import ast
|
import ast
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
|
import queue
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
import unittest
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
REPO_ROOT = pathlib.Path(__file__).parent.parent
|
REPO_ROOT = pathlib.Path(__file__).parent.parent
|
||||||
STREAMING_PY = (REPO_ROOT / "api" / "streaming.py").read_text()
|
STREAMING_PY = (REPO_ROOT / "api" / "streaming.py").read_text()
|
||||||
@@ -88,6 +92,145 @@ class TestSessionDBInjection(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRuntimeRouteInjection(unittest.TestCase):
|
||||||
|
"""Verify WebUI forwards the resolved runtime route into AIAgent."""
|
||||||
|
|
||||||
|
def test_runtime_provider_keys_are_forwarded_to_agent(self):
|
||||||
|
"""WebUI must pass the runtime route fields that CLI already uses."""
|
||||||
|
for snippet in (
|
||||||
|
"api_mode=_rt.get('api_mode')",
|
||||||
|
"acp_command=_rt.get('command')",
|
||||||
|
"acp_args=_rt.get('args')",
|
||||||
|
"credential_pool=_rt.get('credential_pool')",
|
||||||
|
):
|
||||||
|
self.assertIn(
|
||||||
|
snippet,
|
||||||
|
STREAMING_PY,
|
||||||
|
f"Missing runtime route forwarding in AIAgent constructor: {snippet}",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_runtime_route_is_forwarded_from_resolver_into_agent_init(self):
|
||||||
|
"""The resolved ACP route should be passed through to AIAgent kwargs."""
|
||||||
|
import api.streaming as streaming
|
||||||
|
|
||||||
|
captured = {}
|
||||||
|
fake_session_db = object()
|
||||||
|
resolve_runtime_provider = mock.Mock(
|
||||||
|
return_value={
|
||||||
|
"provider": "openai-codex",
|
||||||
|
"base_url": "https://api.openai.com/v1",
|
||||||
|
"api_key": "rt-key",
|
||||||
|
"api_mode": "codex_responses",
|
||||||
|
"command": "codex",
|
||||||
|
"args": ["exec", "--json"],
|
||||||
|
"credential_pool": "openai-codex",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class FakeSession:
|
||||||
|
def __init__(self):
|
||||||
|
self.session_id = "sess-runtime-route"
|
||||||
|
self.title = "Existing title"
|
||||||
|
self.workspace = "/tmp"
|
||||||
|
self.model = "gpt-5.4"
|
||||||
|
self.messages = []
|
||||||
|
self.personality = None
|
||||||
|
self.input_tokens = 0
|
||||||
|
self.output_tokens = 0
|
||||||
|
self.estimated_cost = None
|
||||||
|
self.tool_calls = []
|
||||||
|
self.active_stream_id = None
|
||||||
|
self.pending_user_message = None
|
||||||
|
self.pending_attachments = []
|
||||||
|
self.pending_started_at = None
|
||||||
|
|
||||||
|
def save(self, touch_updated_at=True):
|
||||||
|
self._saved = True
|
||||||
|
|
||||||
|
def compact(self):
|
||||||
|
return {
|
||||||
|
"session_id": self.session_id,
|
||||||
|
"title": self.title,
|
||||||
|
"workspace": self.workspace,
|
||||||
|
"model": self.model,
|
||||||
|
"created_at": 0,
|
||||||
|
"updated_at": 0,
|
||||||
|
"pinned": False,
|
||||||
|
"archived": False,
|
||||||
|
"project_id": None,
|
||||||
|
"profile": None,
|
||||||
|
"input_tokens": self.input_tokens,
|
||||||
|
"output_tokens": self.output_tokens,
|
||||||
|
"estimated_cost": self.estimated_cost,
|
||||||
|
"personality": self.personality,
|
||||||
|
}
|
||||||
|
|
||||||
|
class CapturingAgent:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
captured["init_kwargs"] = kwargs
|
||||||
|
self.session_id = kwargs["session_id"]
|
||||||
|
self.context_compressor = None
|
||||||
|
self.session_prompt_tokens = 0
|
||||||
|
self.session_completion_tokens = 0
|
||||||
|
self.session_estimated_cost_usd = None
|
||||||
|
self.reasoning_config = None
|
||||||
|
self.ephemeral_system_prompt = None
|
||||||
|
self._last_error = None
|
||||||
|
|
||||||
|
def run_conversation(self, **kwargs):
|
||||||
|
captured["run_kwargs"] = kwargs
|
||||||
|
return {
|
||||||
|
"messages": [
|
||||||
|
{"role": "user", "content": kwargs["persist_user_message"]},
|
||||||
|
{"role": "assistant", "content": "ok"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def interrupt(self, _message):
|
||||||
|
captured["interrupted"] = True
|
||||||
|
|
||||||
|
fake_session = FakeSession()
|
||||||
|
fake_stream_id = "stream-runtime-route"
|
||||||
|
fake_queue = queue.Queue()
|
||||||
|
fake_runtime_module = types.ModuleType("hermes_cli.runtime_provider")
|
||||||
|
fake_runtime_module.resolve_runtime_provider = resolve_runtime_provider
|
||||||
|
fake_hermes_cli = types.ModuleType("hermes_cli")
|
||||||
|
fake_hermes_cli.runtime_provider = fake_runtime_module
|
||||||
|
fake_hermes_state = types.ModuleType("hermes_state")
|
||||||
|
fake_hermes_state.SessionDB = mock.Mock(return_value=fake_session_db)
|
||||||
|
|
||||||
|
with mock.patch.object(streaming, "get_session", return_value=fake_session), \
|
||||||
|
mock.patch.object(streaming, "_get_ai_agent", return_value=CapturingAgent), \
|
||||||
|
mock.patch.object(streaming, "resolve_model_provider", return_value=("gpt-5.4", "openai-codex", None)), \
|
||||||
|
mock.patch("api.config.get_config", return_value={}), \
|
||||||
|
mock.patch("api.config._resolve_cli_toolsets", return_value=[]), \
|
||||||
|
mock.patch.dict(
|
||||||
|
sys.modules,
|
||||||
|
{
|
||||||
|
"hermes_cli": fake_hermes_cli,
|
||||||
|
"hermes_cli.runtime_provider": fake_runtime_module,
|
||||||
|
"hermes_state": fake_hermes_state,
|
||||||
|
},
|
||||||
|
):
|
||||||
|
streaming.STREAMS[fake_stream_id] = fake_queue
|
||||||
|
streaming._run_agent_streaming(
|
||||||
|
session_id=fake_session.session_id,
|
||||||
|
msg_text="hello from webui",
|
||||||
|
model="gpt-5.4",
|
||||||
|
workspace="/tmp",
|
||||||
|
stream_id=fake_stream_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
resolve_runtime_provider.assert_called_once_with(requested="openai-codex")
|
||||||
|
init_kwargs = captured["init_kwargs"]
|
||||||
|
self.assertEqual(init_kwargs["api_mode"], "codex_responses")
|
||||||
|
self.assertEqual(init_kwargs["acp_command"], "codex")
|
||||||
|
self.assertEqual(init_kwargs["acp_args"], ["exec", "--json"])
|
||||||
|
self.assertEqual(init_kwargs["credential_pool"], "openai-codex")
|
||||||
|
self.assertEqual(init_kwargs["api_key"], "rt-key")
|
||||||
|
self.assertIs(init_kwargs["session_db"], fake_session_db)
|
||||||
|
|
||||||
|
|
||||||
class TestSessionDBAST(unittest.TestCase):
|
class TestSessionDBAST(unittest.TestCase):
|
||||||
"""AST-level checks: verify the try/except is not inside _ENV_LOCK (deadlock guard)."""
|
"""AST-level checks: verify the try/except is not inside _ENV_LOCK (deadlock guard)."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user