feat(core,frontends,agents): agent kinds, frontend routing, anthropic on conversations, vault mirror

This commit is contained in:
hh
2026-08-28 03:50:33 +02:00
parent e3074c266a
commit 827fa0977b
17 changed files with 1009 additions and 292 deletions
+101 -11
View File
@@ -29,8 +29,47 @@ from beaver_gateway.core.transcript import (
strip_tool_entries,
window_entries,
)
from beaver_gateway.frontends.base import Frontend
from beaver_gateway.storage import Database
from beaver_gateway.storage.models import InjectQueueItem
from beaver_gateway.storage.models import (
Conversation,
ConversationBinding,
InjectQueueItem,
)
class StubFrontend(Frontend):
def __init__(
self,
name: str,
kinds: tuple[str, ...],
agents: dict[str, str] | None = None,
*,
home: bool = False,
) -> None:
self.name = name
self.kinds = kinds
self.agents = agents or {}
self.home = home
self.materialized: list[str] = []
self.conversations = None
def configure(self, runtime) -> None:
pass
async def serve(self) -> None:
pass
def agent_for(self, kind: str) -> str | None:
return self.agents.get(kind)
async def materialize(self, conv: Conversation) -> ConversationBinding | None:
if not self.home:
return None
self.materialized.append(conv.external_id)
return await self.conversations.bind(
conv, frontend=self.name, external_id=f"{self.name}:{conv.external_id}"
)
class ScriptedClient:
@@ -116,29 +155,41 @@ class World:
model="m",
system_prompt="hi",
cwd=root,
kinds=("master", "branch", "job", "fork"),
gateway_tools=("say",),
options=ClaudeOptions(effort="low"),
)
self.deep_agent = ClaudeAgent(
name="d", model="m", system_prompt="deep", cwd=root
)
self.markdown = StubFrontend("markdown", ("deep",), {"deep": "d"}, home=True)
self.api = StubFrontend("api", ("master", "branch", "deep", "job"))
self.pool = SessionPool(rss_limit=1 << 40, max_live=100)
self.backend = ClaudeSdkBackend(
agent=self.agent,
mcp_internal_urls={},
session_store=self.store,
client_factory=ScriptedClient,
work_dir=root / "work",
pool=self.pool,
self.backend, self.deep_backend = (
ClaudeSdkBackend(
agent=agent,
mcp_internal_urls={},
session_store=self.store,
client_factory=ScriptedClient,
work_dir=root / "work",
pool=self.pool,
)
for agent in (self.agent, self.deep_agent)
)
self.bus = EventBus()
self.conversations = Conversations(
db=self.db,
agents=AgentRegistry([self.agent]),
backends={"a": self.backend},
agents=AgentRegistry([self.agent, self.deep_agent]),
backends={"a": self.backend, "d": self.deep_backend},
bus=self.bus,
pool=self.pool,
store=self.store,
texts=ConversationTexts(),
frontends=[self.api, self.markdown],
idle_interval=3600,
)
self.markdown.conversations = self.conversations
self.api.conversations = self.conversations
async def setup(self) -> "World":
await self.db.create_all()
@@ -460,7 +511,7 @@ async def test_read_and_bindings(world: World) -> None:
build_entries(history, session_id=sid, cwd=str(world.root), model="m"),
)
conv = await world.conversations.create(
kind="deep", agent="a", origin="test", session_id=sid
kind="deep", agent="d", origin="test", session_id=sid
)
assert (
await world.conversations.read(conv)
@@ -544,3 +595,42 @@ def test_transcript_helpers() -> None:
)
== "user:\nq\n\nassistant:\na"
)
async def test_agent_kinds_gate_create(world: World) -> None:
with pytest.raises(ValueError, match="does not serve kind 'deep'"):
await world.conversations.create(kind="deep", agent="a", origin="test")
with pytest.raises(ValueError, match="does not serve kind 'master'"):
await world.conversations.create(kind="master", agent="d", origin="test")
with pytest.raises(ValueError, match="'d' does not serve kind 'master'"):
await world.conversations.spawn(kind="master", seed="clean", agent="d")
conv = await world.conversations.create(kind="deep", agent="d", origin="test")
assert conv.agent_name == "d"
async def test_frontend_kinds_gate_bind(world: World) -> None:
master = await world.conversations.create(kind="master", agent="a", origin="test")
with pytest.raises(ValueError, match="'markdown' does not show kind 'master'"):
await world.conversations.bind(master, frontend="markdown", external_id="m.md")
with pytest.raises(ValueError, match="unknown frontend 'telegram'"):
await world.conversations.bind(master, frontend="telegram", external_id="1")
await world.conversations.bind(master, frontend="api", external_id="x")
async def test_spawn_defaults_agent_and_materializes(world: World) -> None:
master = await world.conversations.create(kind="master", agent="a", origin="test")
deep = await world.conversations.spawn(
kind="deep", seed="brief", text="dig", title="t", parent=master, origin="mcp"
)
assert deep.agent_name == "d"
assert world.markdown.materialized == [deep.external_id]
bindings = await world.conversations.bindings(deep)
assert [(b.frontend, b.external_id) for b in bindings] == [
("markdown", f"markdown:{deep.external_id}")
]
branch = await world.conversations.spawn(kind="branch", seed="clean", parent=master)
assert branch.agent_name == "a"
assert world.markdown.materialized == [deep.external_id]
with pytest.raises(ValueError, match="no default agent for kind 'job'"):
await world.conversations.spawn(kind="job", seed="clean")
await world.settle(deep, 1)