feat(core,backends,frontends,storage): conversations, inject queue, session pool, gateway mcp tools, api frontend

This commit is contained in:
hh
2026-08-28 03:08:30 +02:00
parent ab52fdc2b8
commit e3074c266a
28 changed files with 3543 additions and 345 deletions
+47 -1
View File
@@ -22,6 +22,7 @@ from __future__ import annotations
import asyncio
import contextlib
import functools
import logging
import signal
from contextlib import AsyncExitStack
@@ -43,13 +44,18 @@ from beaver_gateway.backends.claude_sdk import (
)
from beaver_gateway.backends.raycast import RaycastBackend
from beaver_gateway.core.auth import TokenStore
from beaver_gateway.core.bus import EventBus
from beaver_gateway.core.conversations import Conversations
from beaver_gateway.core.gateway_tools import build_tool_server
from beaver_gateway.core.registry import AgentRegistry, McpRegistry
from beaver_gateway.core.sessions import SessionPool
from beaver_gateway.frontends.base import GatewayRuntime
from beaver_gateway.mcp.internal_app import build_internal_app
from beaver_gateway.settings import Settings
from beaver_gateway.storage import Database, PostgresSessionStore, Usage, append_usage
if TYPE_CHECKING:
from claude_agent_sdk import McpSdkServerConfig
from fastmcp import FastMCP
from fastmcp.tools.base import Tool as FastMCPTool
from starlette.applications import Starlette
@@ -140,15 +146,32 @@ async def _async_main() -> None:
# surfaces at startup instead of mid-conversation.
mcp_tools = await _prefetch_mcp_tools(mcp_servers)
pool = SessionPool()
bus = EventBus()
late = _LateConversations()
session_store = PostgresSessionStore(db)
backends: dict[str, Backend] = await _build_backends(
settings=settings,
agents=agents,
stack=stack,
db=db,
session_store=session_store,
mcp_internal_urls=internal_urls,
mcp_servers=mcp_servers,
mcp_tools=mcp_tools,
pool=pool,
late=late,
)
conversations = Conversations(
db=db,
agents=agents,
backends=backends,
bus=bus,
pool=pool,
store=session_store,
texts=gateway.texts,
)
late.conversations = conversations
runtime = GatewayRuntime(
agents=agents,
@@ -161,6 +184,9 @@ async def _async_main() -> None:
admin_pass=settings.admin_pass,
session_secret=settings.session_secret,
frontends=tuple(gateway.frontends),
conversations=conversations,
bus=bus,
pool=pool,
)
for fe in gateway.frontends:
@@ -186,7 +212,10 @@ async def _async_main() -> None:
# it in this path and exit cleanly (Phase 0 DoD).
return
await conversations.start()
stack.push_async_callback(conversations.stop)
async with asyncio.TaskGroup() as tg:
tg.create_task(pool.reap_loop())
if internal_app is not None:
tg.create_task(_serve_internal_mcp(internal_app, settings=settings))
for fe in gateway.frontends:
@@ -252,15 +281,31 @@ async def _serve_internal_mcp(app: Starlette, *, settings: Settings) -> None:
await server.serve()
class _LateConversations:
"""Backends need a tool-server factory before the service that backs it exists."""
conversations: Conversations | None = None
def server(
self, key: str, _kind: str, names: tuple[str, ...]
) -> McpSdkServerConfig | None:
if self.conversations is None or not names:
return None
return build_tool_server(self.conversations, conversation_key=key, names=names)
async def _build_backends(
*,
settings: Settings,
agents: AgentRegistry,
stack: AsyncExitStack,
db: Database,
session_store: PostgresSessionStore,
mcp_internal_urls: dict[str, str],
mcp_servers: dict[str, FastMCP],
mcp_tools: dict[str, list[FastMCPTool]],
pool: SessionPool,
late: _LateConversations,
) -> dict[str, Backend]:
"""Construct one backend per agent name.
@@ -285,7 +330,6 @@ async def _build_backends(
for a in raycast_agents:
backends[a.name] = raycast_backend
session_store = PostgresSessionStore(db)
runner = RunnerConfig(user=settings.claude_runner_user, home=settings.claude_home)
mcp_tool_names = {
name: [t.name for t in tools] for name, tools in mcp_tools.items()
@@ -321,6 +365,8 @@ async def _build_backends(
mcp_tool_names=mcp_tool_names,
runner=runner,
usage_sink=record_usage,
pool=pool,
tool_server=functools.partial(late.server, names=a.gateway_tools),
)
await stack.enter_async_context(adapter)
backends[a.name] = adapter