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
+47 -2
View File
@@ -63,6 +63,7 @@ if TYPE_CHECKING:
from beaver_gateway.core.injects import Priority
from beaver_gateway.core.registry import AgentRegistry
from beaver_gateway.core.sessions import SessionPool
from beaver_gateway.frontends.base import Frontend
from beaver_gateway.storage.db import Database
__all__ = [
@@ -138,6 +139,7 @@ class Conversations:
pool: SessionPool,
store: SessionStore,
texts: ConversationTexts | None = None,
frontends: Sequence[Frontend] = (),
normal_window: float = 3600.0,
idle_days: Sequence[int] = (2,),
idle_interval: float = 3600.0,
@@ -149,6 +151,7 @@ class Conversations:
self._pool = pool
self._store = store
self._texts = texts or ConversationTexts()
self._frontends = [f for f in frontends if f.name]
self._normal_window = normal_window
self._idle_days = tuple(sorted(idle_days))
self._idle_interval = idle_interval
@@ -185,7 +188,9 @@ class Conversations:
if kind not in KINDS:
msg = f"unknown conversation kind {kind!r}"
raise ValueError(msg)
self._claude_agent(agent)
if not self._claude_agent(agent).serves(kind):
msg = f"agent {agent!r} does not serve kind {kind!r}"
raise ValueError(msg)
now = datetime.now(UTC)
row = Conversation(
frontend=origin,
@@ -251,6 +256,9 @@ class Conversations:
external_id: str,
visible: bool = True,
) -> ConversationBinding:
if conv.kind not in self.frontend(frontend).kinds:
msg = f"frontend {frontend!r} does not show kind {conv.kind!r}"
raise ValueError(msg)
async with self._db.session() as session:
existing = list(
(
@@ -385,13 +393,41 @@ class Conversations:
out["busy"] = live.busy if live is not None else False
return out
# ---- routing -------------------------------------------------------
@property
def frontends(self) -> list[Frontend]:
return list(self._frontends)
def frontend(self, name: str) -> Frontend:
for fe in self._frontends:
if fe.name == name:
return fe
msg = f"unknown frontend {name!r}"
raise ValueError(msg)
def default_agent(self, kind: str) -> str | None:
for fe in self._frontends:
if kind in fe.kinds and (agent := fe.agent_for(kind)):
return agent
return None
async def materialize(self, conv: Conversation) -> ConversationBinding | None:
for fe in self._frontends:
if conv.kind not in fe.kinds:
continue
binding = await fe.materialize(conv)
if binding is not None:
return binding
return None
# ---- §3.1 api ------------------------------------------------------
async def spawn(
self,
*,
kind: str,
agent: str,
agent: str | None = None,
seed: str = "clean",
parent: Conversation | None = None,
text: str | None = None,
@@ -405,6 +441,12 @@ class Conversations:
if seed == "brief" and not text:
msg = "seed=brief needs text"
raise ValueError(msg)
if agent is None and kind == "branch" and parent is not None:
agent = parent.agent_name
agent = agent or self.default_agent(kind)
if agent is None:
msg = f"no default agent for kind {kind!r}; pass `agent`"
raise ValueError(msg)
session_id: str | None = None
if seed == "copy":
if parent is None or parent.session_id is None:
@@ -421,6 +463,7 @@ class Conversations:
origin=origin,
session_id=session_id,
)
await self.materialize(conv)
prompt = await self._seed_text(
SeedContext(
kind=kind, seed=seed, agent=agent, parent=parent, text=text, title=title
@@ -888,6 +931,8 @@ class Conversations:
conversation_id=conv.external_id,
turn_id=turn_id,
item=head.id,
source="queue",
prompt=prompt,
text=text,
)