refactor(runtime): centralize async bridges under an owned runtime (#376)

* feat(runtime): add application-scoped async runtime

* refactor(cli): use owned runtime for session stats

* refactor(onboard): use the owned async runtime

* docs(runtime): record async bridge ownership

* refactor(middleware): keep sync fallback synchronous

* refactor(mcp): load tools on an owned runtime

* refactor(cli): share owned runtime across entry points

* refactor(channels): make inbound sync bridge explicit

* refactor(stream): run Rich streaming on owned runtime

* chore(runtime): remove nest-asyncio dependency

* refactor(asyncio): require active loops in async code

* docs(runtime): document final event loop ownership

* fix(stream): cancel stalled owned streams

* fix(cli): recover cleanly from stream cancellation

* fix(runtime): drain executor work before shutdown

* fix(runtime): terminate cancelled shell process trees

* fix(models): let fallback bypass selector failures

* fix(cli): reset interrupt handling between turns

* docs: rm implementation spec

* fix(serve): cancel active turns during shutdown

* fix(runtime): protect settlement from waiter cancellation

* fix(backends): reject empty shell commands

* fix(runtime): terminate descendants after shell exit

* fix(mcp): keep standalone discovery off channel loop

* fix(cli): own and settle interactive prompt cancellation

* fix(serve): keep channel sends off runtime loop

* fix(stream): scope cancel context to iterator steps

* refactor(serve): require the owned async runtime

* fix(channels): keep interactive sends off runtime loop

* fix(selector): surface fallback without log spam

* test(runtime): normalize Windows shell marker

* fix(cli): serialize interactive session turns

* fix(shell): bound output drain after termination

* fix(ui): do not retry owned runtime failures

* fix(shell): allow signal-safe registry reentry

* fix(shell): avoid terminating reused process ids

* fix(channels): preserve streaming send order

* fix(cli): report runtime shutdown timeouts cleanly

* fix(mcp): guide async callers to async loader

* docs(runtime): clarify reserved async bridge APIs

* fix(runtime): bound code interpreter cleanup

* test(shell): use active Python for drain regression

---------

Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
This commit is contained in:
dinos
2026-07-27 15:17:57 +02:00
committed by GitHub
parent ac58caab7b
commit 8b1451cdda
47 changed files with 4076 additions and 780 deletions
+45
View File
@@ -16,8 +16,10 @@ from langchain_core.messages import AIMessage, HumanMessage
from EvoScientist.middleware.events import NoOpSink
from EvoScientist.middleware.model_fallback import (
_guard_and_fallback,
_guard_and_fallback_sync,
_is_non_fallbackable,
_try_fallbacks,
_try_fallbacks_sync,
add_fallback,
clear_fallbacks,
)
@@ -410,6 +412,49 @@ class TestGuardAndFallback:
invoke.assert_awaited_once()
class TestSynchronousFallback:
"""The sync middleware path must not create or nest an event loop."""
def test_first_fallback_succeeds_without_async_bridge(self):
add_fallback("fb-model", "fb-provider")
req = _fake_request()
invoke = MagicMock(return_value=AI_RESPONSE)
with patch("EvoScientist.llm.models.get_chat_model") as mock_gcm:
mock_gcm.return_value = MagicMock()
result = _try_fallbacks_sync(req, invoke, Exception("503 boom"), _SINK)
assert result is AI_RESPONSE
invoke.assert_called_once()
def test_guard_rejects_non_fallbackable_error_before_handler(self):
add_fallback("fb", "prov")
req = _fake_request()
invoke = MagicMock()
with pytest.raises(ContextOverflowError):
_guard_and_fallback_sync(
ContextOverflowError("overflow"), req, invoke, _SINK
)
invoke.assert_not_called()
def test_middleware_sync_entrypoint_uses_native_traversal(self):
from EvoScientist.middleware.model_fallback import ModelFallbackMiddleware
add_fallback("fb", "prov")
req = _fake_request()
response = AI_RESPONSE
handler = MagicMock(side_effect=[Exception("503 primary"), response])
with patch("EvoScientist.llm.models.get_chat_model") as mock_gcm:
mock_gcm.return_value = MagicMock()
result = ModelFallbackMiddleware().wrap_model_call(req, handler)
assert result is response
assert handler.call_count == 2
# ═════════════════════════════════════════════════════════════════
# 4. UI emit callback
# ═════════════════════════════════════════════════════════════════