refactor: LangGraph gateway layer for UI-agnostic graph and thread access (#295)

* feat(gateway): graph gateway protocol

* refactor(cli): wire gateway in cli/tui

* refactor(gateway): centralize runtime gateway init

* chore(gateway): restrict RunRequest message type

* feat(gateway): add langgraph server gateway

* chore(cli): tighten serve runtime state typing

* refactor(cli): route async task state reads through graph gateway

* refactor(gateway): support graph targets in server gateway

* refactor(cli): route session commands through graph gateway

* refactor(cli): fold thread store under graph gateway

* refactor(gateway): route graph state access through gateway

* refactor(channels): wire graph gateway

* refactor(memory): preserve graph threads for cloning

* feat(gateway): add thread cloning

* fix(tui): pass effective workspace for thread creation

* chore(memory): add workspare dir to memory worker metadata

* fix(sessions): filter preloaded UUID registy entries by the current scope

* test(fakes): use https

* refactor(consumer): consolidate imports

* fix(stream): optional summarization event

* fix(gateway): resolve abbreviated thread IDs by search

* fix(gateway): page server thread listings

* fix(gateway): emit pending interrupt events

* style: fmt

* feat(gateway): persist workspace_dir & model in thread metadata

* fix(gateway): page server thread prefix resolution

* fix(gateway): expose server thread list metadata

* refactor: add back type def

* refactor: tighten types

* revert: add back worker thread deletion

The worker thread forking changes are out of scope for now, so to
maintain parity with the existing behavior we'll leave this intact.

* fix(gateway): apply compaction to server thread history

* refactor(stream): restore direct summary replay suppression

* fix(gateway): preserve compaction state and server stream output

* fix(gateway): close local stream generator on cancellation

---------

Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
This commit is contained in:
dinos
2026-06-22 15:54:14 +02:00
committed by GitHub
parent 6eb467e70b
commit bd307f3a11
49 changed files with 5071 additions and 1582 deletions
+63 -67
View File
@@ -6,6 +6,7 @@ from unittest.mock import Mock, patch
import pytest
from EvoScientist.stream.display import _create_event_loop, _get_event_loop
from tests.fakes import FakeGraphGateway
class _TrackingEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
@@ -128,7 +129,7 @@ class TestMultipleStreamingCalls:
# Mock agent that returns simple events
mock_agent = Mock()
async def mock_stream(*args, **kwargs):
async def mock_stream(_request):
"""Mock event stream."""
yield {"type": "text", "content": "test response"}
yield {"type": "done", "response": "test response"}
@@ -141,38 +142,39 @@ class TestMultipleStreamingCalls:
except RuntimeError:
pass
# Patch the stream_agent_events function
with patch(
"EvoScientist.stream.display.stream_agent_events", side_effect=mock_stream
):
# Patch Live to avoid terminal output during tests
with patch("EvoScientist.stream.display.Live"):
# First call
_run_streaming(
agent=mock_agent,
message="test message 1",
thread_id="thread1",
show_thinking=False,
interactive=True,
)
gateway = FakeGraphGateway(stream=mock_stream)
# Second call - this would fail with "Event loop is closed" before the fix
_run_streaming(
agent=mock_agent,
message="test message 2",
thread_id="thread1",
show_thinking=False,
interactive=True,
)
# Patch Live to avoid terminal output during tests
with patch("EvoScientist.stream.display.Live"):
# First call
_run_streaming(
agent=mock_agent,
message="test message 1",
thread_id="thread1",
show_thinking=False,
interactive=True,
gateway=gateway,
)
# Third call for good measure
_run_streaming(
agent=mock_agent,
message="test message 3",
thread_id="thread1",
show_thinking=False,
interactive=True,
)
# Second call - this would fail with "Event loop is closed" before the fix
_run_streaming(
agent=mock_agent,
message="test message 2",
thread_id="thread1",
show_thinking=False,
interactive=True,
gateway=gateway,
)
# Third call for good measure
_run_streaming(
agent=mock_agent,
message="test message 3",
thread_id="thread1",
show_thinking=False,
interactive=True,
gateway=gateway,
)
def test_loop_reused_across_calls(self):
"""Event loop should be reused across multiple calls."""
@@ -227,7 +229,7 @@ class TestMultipleStreamingCalls:
thinking = "Initial plan. " * 20
stream_calls = 0
async def mock_stream(*args, **kwargs):
async def mock_stream(_request):
nonlocal stream_calls
stream_calls += 1
if stream_calls == 1:
@@ -245,23 +247,20 @@ class TestMultipleStreamingCalls:
sent_thinking: list[str] = []
with patch(
"EvoScientist.stream.display.stream_agent_events",
side_effect=mock_stream,
):
with patch("EvoScientist.stream.display.Live"):
result = _run_streaming(
agent=mock_agent,
message="test message",
thread_id="thread1",
show_thinking=False,
interactive=True,
on_thinking=sent_thinking.append,
ask_user_prompt_fn=lambda _data: {
"answers": ["yes"],
"status": "answered",
},
)
with patch("EvoScientist.stream.display.Live"):
result = _run_streaming(
agent=mock_agent,
message="test message",
thread_id="thread1",
show_thinking=False,
interactive=True,
on_thinking=sent_thinking.append,
ask_user_prompt_fn=lambda _data: {
"answers": ["yes"],
"status": "answered",
},
gateway=FakeGraphGateway(stream=mock_stream),
)
assert result == "final answer"
assert sent_thinking == [thinking.rstrip()]
@@ -275,7 +274,7 @@ class TestMultipleStreamingCalls:
thinking_r2 = "Revised plan. " * 20
stream_calls = 0
async def mock_stream(*args, **kwargs):
async def mock_stream(_request):
nonlocal stream_calls
stream_calls += 1
if stream_calls == 1:
@@ -294,23 +293,20 @@ class TestMultipleStreamingCalls:
sent_thinking: list[str] = []
with patch(
"EvoScientist.stream.display.stream_agent_events",
side_effect=mock_stream,
):
with patch("EvoScientist.stream.display.Live"):
result = _run_streaming(
agent=mock_agent,
message="test message",
thread_id="thread1",
show_thinking=False,
interactive=True,
on_thinking=sent_thinking.append,
ask_user_prompt_fn=lambda _data: {
"answers": ["yes"],
"status": "answered",
},
)
with patch("EvoScientist.stream.display.Live"):
result = _run_streaming(
agent=mock_agent,
message="test message",
thread_id="thread1",
show_thinking=False,
interactive=True,
on_thinking=sent_thinking.append,
ask_user_prompt_fn=lambda _data: {
"answers": ["yes"],
"status": "answered",
},
gateway=FakeGraphGateway(stream=mock_stream),
)
assert result == "final answer"
assert sent_thinking == [thinking_r1.rstrip(), thinking_r2.rstrip()]