harden server apis and agent loading

This commit is contained in:
bryan
2026-02-20 18:28:52 -08:00
parent 263d35bbd6
commit 6661934fed
5 changed files with 30 additions and 13 deletions
+7 -11
View File
@@ -13,9 +13,6 @@ from typing import Any
logger = logging.getLogger(__name__)
# Sentinel placed in _slots while an agent is loading (prevents duplicate loads).
_LOADING = object()
@dataclass
class AgentSlot:
@@ -39,6 +36,7 @@ class AgentManager:
def __init__(self, model: str | None = None) -> None:
self._slots: dict[str, AgentSlot] = {}
self._loading: set[str] = set()
self._model = model
self._lock = asyncio.Lock()
@@ -69,9 +67,9 @@ class AgentManager:
resolved_model = model or self._model
async with self._lock:
if resolved_id in self._slots:
if resolved_id in self._slots or resolved_id in self._loading:
raise ValueError(f"Agent '{resolved_id}' is already loaded")
self._slots[resolved_id] = _LOADING # claim slot
self._loading.add(resolved_id) # claim slot
try:
# Blocking I/O — load in executor (same as tui/app.py:362-368)
@@ -108,13 +106,14 @@ class AgentManager:
async with self._lock:
self._slots[resolved_id] = slot
self._loading.discard(resolved_id)
logger.info(f"Agent '{resolved_id}' loaded from {agent_path}")
return slot
except Exception:
async with self._lock:
self._slots.pop(resolved_id, None)
self._loading.discard(resolved_id)
raise
async def unload_agent(self, agent_id: str) -> bool:
@@ -137,13 +136,10 @@ class AgentManager:
return True
def get_agent(self, agent_id: str) -> AgentSlot | None:
slot = self._slots.get(agent_id)
if slot is _LOADING:
return None
return slot
return self._slots.get(agent_id)
def list_agents(self) -> list[AgentSlot]:
return [s for s in self._slots.values() if s is not _LOADING]
return list(self._slots.values())
async def shutdown_all(self) -> None:
"""Gracefully unload all agents. Called on server shutdown."""
+2 -2
View File
@@ -156,9 +156,9 @@ def _setup_static_serving(app: web.Application) -> None:
async def handle_spa(request: web.Request) -> web.FileResponse:
"""Serve static files with SPA fallback to index.html."""
rel_path = request.match_info.get("path", "")
file_path = dist_dir / rel_path
file_path = (dist_dir / rel_path).resolve()
if file_path.is_file():
if file_path.is_file() and file_path.is_relative_to(dist_dir):
return web.FileResponse(file_path)
# SPA fallback
+3
View File
@@ -64,6 +64,9 @@ async def handle_events(request: web.Request) -> web.StreamResponse:
if slot is None:
return web.json_response({"error": f"Agent '{agent_id}' not found"}, status=404)
if not slot.runtime:
return web.json_response({"error": "Agent runtime not started"}, status=503)
event_bus = slot.runtime.event_bus
event_types = _parse_event_types(request.query.get("types"))
+3
View File
@@ -118,6 +118,9 @@ class MockRuntime:
async def get_goal_progress(self):
return {"progress": 0.5, "criteria": []}
def find_awaiting_node(self):
return None, None
def get_stats(self):
return {"running": True, "executions": 1}
Generated
+15
View File
@@ -3273,6 +3273,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" },
]
[[package]]
name = "stripe"
version = "14.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "requests" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/49/67/8a38222a57fc2ba359c4dcb66528d94c00d803c7fde8f8d8470ad6bdccbb/stripe-14.3.0.tar.gz", hash = "sha256:4c76137d741bd43e8bb433a596c198ca20f4cdf17a8fe04604faf37c74b01978", size = 1463618, upload-time = "2026-01-28T21:20:29.856Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/4b/0b7d5920f2be5e42d72bdfc44a9fae57b422668bfc8dacdf2f74886f6daa/stripe-14.3.0-py3-none-any.whl", hash = "sha256:3e36b68b256c8970e99b703e195d947e2a2919095758788c7074ac4485ac255e", size = 2106980, upload-time = "2026-01-28T21:20:27.566Z" },
]
[[package]]
name = "textual"
version = "7.5.0"
@@ -3390,6 +3403,7 @@ dependencies = [
{ name = "pypdf" },
{ name = "python-dotenv" },
{ name = "resend" },
{ name = "stripe" },
]
[package.optional-dependencies]
@@ -3460,6 +3474,7 @@ requires-dist = [
{ name = "resend", specifier = ">=2.0.0" },
{ name = "restrictedpython", marker = "extra == 'all'", specifier = ">=7.0" },
{ name = "restrictedpython", marker = "extra == 'sandbox'", specifier = ">=7.0" },
{ name = "stripe", specifier = ">=14.3.0" },
]
provides-extras = ["dev", "sandbox", "ocr", "excel", "sql", "bigquery", "all"]