feat(backends,storage,core,markdown,infra): claude agent sdk backend, session store, transcript seeding, runner isolation

This commit is contained in:
hh
2026-08-28 01:56:39 +02:00
parent ee2dc918ee
commit e1f242a87a
28 changed files with 2154 additions and 875 deletions
+49 -13
View File
@@ -36,14 +36,18 @@ from raycast_api.config import Config as RaycastConfig
from beaver_gateway import config_loader
from beaver_gateway.agents.claude import ClaudeAgent
from beaver_gateway.agents.raycast import RaycastAgent
from beaver_gateway.backends.claude_code import ClaudeCodeBackendAdapter
from beaver_gateway.backends.claude_sdk import (
ClaudeSdkBackend,
RunnerConfig,
UsageEvent,
)
from beaver_gateway.backends.raycast import RaycastBackend
from beaver_gateway.core.auth import TokenStore
from beaver_gateway.core.registry import AgentRegistry, McpRegistry
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
from beaver_gateway.storage import Database, PostgresSessionStore, Usage, append_usage
if TYPE_CHECKING:
from fastmcp import FastMCP
@@ -122,8 +126,8 @@ async def _async_main() -> None:
await token_store.start()
stack.push_async_callback(token_store.stop)
# Internal MCP URLs must exist before we construct any
# ClaudeCodeBackendAdapter — adapters bake the URLs into their
# ``BackendOptions.mcp_servers`` at construction time. The
# ClaudeSdkBackend - adapters bake the URLs into their
# ``mcp_servers`` at construction time. The
# ``mcp_servers`` map is used by the Raycast backend, which
# needs in-process ``list_tools`` / ``call_tool`` access (the
# Raycast wire has no native MCP concept).
@@ -140,6 +144,7 @@ async def _async_main() -> None:
settings=settings,
agents=agents,
stack=stack,
db=db,
mcp_internal_urls=internal_urls,
mcp_servers=mcp_servers,
mcp_tools=mcp_tools,
@@ -252,6 +257,7 @@ async def _build_backends(
settings: Settings,
agents: AgentRegistry,
stack: AsyncExitStack,
db: Database,
mcp_internal_urls: dict[str, str],
mcp_servers: dict[str, FastMCP],
mcp_tools: dict[str, list[FastMCPTool]],
@@ -259,15 +265,13 @@ async def _build_backends(
"""Construct one backend per agent name.
The Raycast ``Client`` is shared across every ``RaycastAgent``
(bearer + device-id are process-wide), so we open it lazily only
when at least one ``RaycastAgent`` is present and close it via
(bearer + device-id are process-wide), so we open it lazily - only
when at least one ``RaycastAgent`` is present - and close it via
the caller's exit stack.
Each :class:`ClaudeAgent` gets its own
:class:`ClaudeCodeBackendAdapter`: ``BackendOptions`` pins
``cwd`` / ``model`` / ``system_prompt`` / ``mcp_servers`` for the
lifetime of the underlying ``ClaudeCodeBackend``, so different
agents can't share one.
Each :class:`ClaudeAgent` gets its own :class:`ClaudeSdkBackend`
(own live-session pool, own prompt and MCP set); all of them share
the session store and the usage sink.
"""
backends: dict[str, Backend] = {}
@@ -281,10 +285,42 @@ 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()
}
async def record_usage(event: UsageEvent) -> None:
row = Usage(
agent_name=event.agent_name,
conversation_id=event.conversation_id,
session_id=event.session_id,
model=event.model,
effort=event.effort,
input_tokens=event.usage.input_tokens,
output_tokens=event.usage.output_tokens,
cache_read_tokens=event.usage.cache_read_tokens,
cache_creation_tokens=event.usage.cache_creation_tokens,
cost_usd=event.usage.cost_usd,
duration_ms=event.usage.duration_ms,
num_turns=event.usage.num_turns,
)
try:
async with db.session() as session:
await append_usage(session, row)
except Exception: # noqa: BLE001
_log.exception("usage write failed for %s", event.agent_name)
for a in agents:
if isinstance(a, ClaudeAgent):
adapter = ClaudeCodeBackendAdapter(
agent=a, mcp_internal_urls=mcp_internal_urls
adapter = ClaudeSdkBackend(
agent=a,
mcp_internal_urls=mcp_internal_urls,
session_store=session_store,
mcp_tool_names=mcp_tool_names,
runner=runner,
usage_sink=record_usage,
)
await stack.enter_async_context(adapter)
backends[a.name] = adapter