From dca2abd9a39165cf55ea64545cb0534dca1f0a24 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Fri, 19 Jun 2026 16:47:31 +0200 Subject: [PATCH] feat(agent): route load-session through a no-op session store port --- sdks/python/agenta/sdk/agents/__init__.py | 12 +++++- .../sdk/agents/adapters/vercel/routing.py | 25 +++++++++--- sdks/python/agenta/sdk/agents/interfaces.py | 38 +++++++++++++++++++ .../pytest/utils/test_messages_endpoint.py | 30 +++++++++++++++ 4 files changed, 99 insertions(+), 6 deletions(-) diff --git a/sdks/python/agenta/sdk/agents/__init__.py b/sdks/python/agenta/sdk/agents/__init__.py index 19d0ac77b0..b1cd4370d2 100644 --- a/sdks/python/agenta/sdk/agents/__init__.py +++ b/sdks/python/agenta/sdk/agents/__init__.py @@ -49,7 +49,15 @@ to_messages, ) from .errors import ToolResolutionError, UnsupportedHarnessError -from .interfaces import Backend, Environment, Harness, Sandbox, Session +from .interfaces import ( + Backend, + Environment, + Harness, + NoopSessionStore, + Sandbox, + Session, + SessionStore, +) from .mcp import ( MCPConfigurationError, MCPError, @@ -157,6 +165,8 @@ "Backend", "Sandbox", "Session", + "SessionStore", + "NoopSessionStore", "Environment", "Harness", # Errors diff --git a/sdks/python/agenta/sdk/agents/adapters/vercel/routing.py b/sdks/python/agenta/sdk/agents/adapters/vercel/routing.py index f50a790cc2..746b40a304 100644 --- a/sdks/python/agenta/sdk/agents/adapters/vercel/routing.py +++ b/sdks/python/agenta/sdk/agents/adapters/vercel/routing.py @@ -12,13 +12,15 @@ from agenta.sdk.contexts.tracing import tracing_context_manager from agenta.sdk.models.workflows import ( LoadSessionRequest, + LoadSessionResponse, WorkflowBatchResponse, WorkflowInvokeRequest, WorkflowRequestData, WorkflowStreamingResponse, ) -from .messages import vercel_ui_messages_to_messages +from ...interfaces import NoopSessionStore, SessionStore +from .messages import message_to_vercel_ui_message, vercel_ui_messages_to_messages # An opaque, project-scoped session id (RFC ยง4.1): bounded length, restricted charset. _SESSION_ID_RE = re.compile(r"^[A-Za-z0-9._:-]{1,128}$") @@ -122,12 +124,24 @@ async def messages_endpoint(req: Request, request: WorkflowInvokeRequest): return messages_endpoint -def make_load_session_endpoint(): - """Build the v1 ``POST /load-session`` stub endpoint.""" +def make_load_session_endpoint( + *, + session_store: Optional[SessionStore] = None, +): + """Build the v1 ``POST /load-session`` endpoint over the session-store port.""" + store = session_store or NoopSessionStore() async def load_session_endpoint(req: Request, request: LoadSessionRequest): + messages = await store.load(request.session_id) + response = LoadSessionResponse( + session_id=request.session_id, + messages=[ + message_to_vercel_ui_message(message, message_id=f"msg-{idx}") + for idx, message in enumerate(messages, start=1) + ], + ) return JSONResponse( - content={"session_id": request.session_id, "messages": []}, + content=response.model_dump(mode="json"), ) return load_session_endpoint @@ -146,6 +160,7 @@ def register_agent_message_routes( make_not_acceptable_response: Callable[[str, Any], Response], make_stream_response: Callable[[WorkflowStreamingResponse, str], Response], handle_failure: Callable[[Exception], Any], + session_store: Optional[SessionStore] = None, ) -> None: """Register ``/messages`` and ``/load-session`` on a FastAPI app/router target.""" target.add_api_route( @@ -165,6 +180,6 @@ def register_agent_message_routes( ) target.add_api_route( prefix + "/load-session", - make_load_session_endpoint(), + make_load_session_endpoint(session_store=session_store), methods=["POST"], ) diff --git a/sdks/python/agenta/sdk/agents/interfaces.py b/sdks/python/agenta/sdk/agents/interfaces.py index 3822c11b12..a7df7280d5 100644 --- a/sdks/python/agenta/sdk/agents/interfaces.py +++ b/sdks/python/agenta/sdk/agents/interfaces.py @@ -86,6 +86,44 @@ async def destroy(self) -> None: return None +class SessionStore(ABC): + """Durable conversation history behind the agent session id. + + The cold runtime still receives the full message history on every turn. This port is the + place a platform-backed or file-backed store attaches when the server owns that history. + """ + + @abstractmethod + async def load(self, session_id: str) -> Sequence[Message]: + """Return the neutral message history for ``session_id``.""" + + @abstractmethod + async def save_turn( + self, + session_id: str, + *, + messages: Sequence[Message], + result: Optional[AgentResult] = None, + ) -> None: + """Persist one completed cold turn.""" + + +class NoopSessionStore(SessionStore): + """Session store adapter used until server-owned history persistence lands.""" + + async def load(self, session_id: str) -> Sequence[Message]: + return () + + async def save_turn( + self, + session_id: str, + *, + messages: Sequence[Message], + result: Optional[AgentResult] = None, + ) -> None: + return None + + # --------------------------------------------------------------------------- # Backend (the engine) # --------------------------------------------------------------------------- diff --git a/sdks/python/oss/tests/pytest/utils/test_messages_endpoint.py b/sdks/python/oss/tests/pytest/utils/test_messages_endpoint.py index e2c6cdfe71..f3c33c1fc9 100644 --- a/sdks/python/oss/tests/pytest/utils/test_messages_endpoint.py +++ b/sdks/python/oss/tests/pytest/utils/test_messages_endpoint.py @@ -11,18 +11,22 @@ without ``ag.init()``. """ +import json from unittest.mock import MagicMock, patch import pytest from fastapi import FastAPI from fastapi.testclient import TestClient +from agenta.sdk.agents import Message from agenta.sdk.agents.adapters.vercel.routing import ( inject_stream_session_id, + make_load_session_endpoint, resolve_session_id, ) from agenta.sdk.decorators.routing import route from agenta.sdk.models.workflows import ( + LoadSessionRequest, WorkflowBatchResponse, WorkflowServiceStatus, WorkflowStreamingResponse, @@ -224,3 +228,29 @@ def test_load_session_returns_stub_history(client): res = client.post("/load-session", json={"session_id": "sess_abc"}) assert res.status_code == 200 assert res.json() == {"session_id": "sess_abc", "messages": []} + + +@pytest.mark.asyncio +async def test_load_session_uses_session_store_port(): + class _Store: + async def load(self, session_id): + assert session_id == "sess_abc" + return [Message(role="user", content="hello")] + + async def save_turn(self, session_id, *, messages, result=None): + raise AssertionError("load-session should only load") + + endpoint = make_load_session_endpoint(session_store=_Store()) + response = await endpoint(None, LoadSessionRequest(session_id="sess_abc")) + + assert response.status_code == 200 + assert json.loads(response.body) == { + "session_id": "sess_abc", + "messages": [ + { + "id": "msg-1", + "role": "user", + "parts": [{"type": "text", "text": "hello"}], + } + ], + }