refactor(agents,core,frontends): typed kinds, prompts per kind, frontend agents as parameters

This commit is contained in:
hh
2026-08-28 16:36:44 +02:00
parent 1d8d65b69a
commit bd2d2368e1
11 changed files with 129 additions and 50 deletions
+48 -26
View File
@@ -1,26 +1,24 @@
"""Claude agent definition, backed by the Claude Agent SDK.
The system prompt is either ``system_prompt`` verbatim or, when
``prompt_sources`` is set, the concatenation of those files (or
``(tag, file)`` pairs) assembled at every session spawn (see
``core/prompt.py``). ``skill_sets`` are
directories of ``<skill>/SKILL.md`` folders; each becomes a local SDK
plugin. Nothing from disk is loaded otherwise: the adapter runs with
``setting_sources=[]``.
The system prompt is ``system_prompt`` verbatim or, per conversation kind,
the granules named in ``prompts`` assembled at every session spawn (see
``core/prompt.py``). ``skill_sets`` are directories of ``<skill>/SKILL.md``
folders; each becomes a local SDK plugin. Nothing from disk is loaded
otherwise: the adapter runs with ``setting_sources=[]``.
"""
from __future__ import annotations
from collections.abc import Mapping # noqa: TC003 - pydantic runtime
from pathlib import Path # noqa: TC003 - pydantic runtime
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, model_validator
from beaver_gateway.agents.base import BaseAgent
from beaver_gateway.core.kinds import KINDS, Kind
from beaver_gateway.core.prompt import PromptSource # noqa: TC001 - pydantic runtime
__all__ = ["ClaudeAgent", "ClaudeOptions"]
__all__ = ["ClaudeAgent", "ClaudeOptions", "Prompts"]
class ClaudeOptions(BaseModel):
@@ -48,19 +46,39 @@ class ClaudeOptions(BaseModel):
killed mid-turn loses at most the frame in flight."""
class Prompts(BaseModel):
"""Prompt assembly per conversation kind (§3.12): the granules, in order.
A kind left ``None`` is not served by the agent; ``ClaudeAgent.kinds``
follows from the kinds set here.
"""
model_config = ConfigDict(frozen=True)
master: tuple[PromptSource, ...] | None = None
branch: tuple[PromptSource, ...] | None = None
deep: tuple[PromptSource, ...] | None = None
job: tuple[PromptSource, ...] | None = None
fork: tuple[PromptSource, ...] | None = None
def for_kind(self, kind: Kind) -> tuple[PromptSource, ...] | None:
return getattr(self, kind)
@property
def kinds(self) -> tuple[Kind, ...]:
return tuple(k for k in KINDS if self.for_kind(k) is not None)
class ClaudeAgent(BaseAgent):
cwd: Path
system_prompt: str = ""
prompt_sources: tuple[PromptSource, ...] = ()
prompt_sources_by_kind: Mapping[str, tuple[PromptSource, ...]] = Field(
default_factory=dict
)
"""Per conversation kind (``master``/``branch``/``deep``/``job``/``fork``)
assembly; falls back to ``prompt_sources``. Constant per kind (§3.12)."""
"""Verbatim prompt for agents without ``prompts`` (tests, one-offs)."""
kinds: tuple[str, ...] = ()
prompts: Prompts = Field(default_factory=Prompts)
kinds: tuple[Kind, ...] = ()
"""Conversation kinds this agent serves; ``create``/``spawn`` reject the
rest. Defaults to the keys of ``prompt_sources_by_kind`` or ``("deep",)``."""
rest. Defaults to the kinds ``prompts`` covers, or ``("deep",)`` for a
verbatim ``system_prompt``."""
skill_sets: tuple[Path, ...] = ()
gateway_tools: tuple[str, ...] = ()
@@ -69,16 +87,20 @@ class ClaudeAgent(BaseAgent):
options: ClaudeOptions = Field(default_factory=ClaudeOptions)
@model_validator(mode="before")
@classmethod
def _default_kinds(cls, data: Any) -> Any:
if isinstance(data, dict) and not data.get("kinds"):
by_kind = data.get("prompt_sources_by_kind") or {}
data = {**data, "kinds": tuple(by_kind) or ("deep",)}
return data
@model_validator(mode="after")
def _kinds_follow_prompts(self) -> ClaudeAgent:
covered = self.prompts.kinds
if not self.kinds:
object.__setattr__(self, "kinds", covered or ("deep",))
elif covered:
missing = [k for k in self.kinds if k not in covered]
if missing:
msg = f"agent {self.name!r} serves {missing} without a prompt"
raise ValueError(msg)
return self
def prompt_for(self, kind: str) -> tuple[PromptSource, ...]:
return self.prompt_sources_by_kind.get(kind, self.prompt_sources)
def prompt_for(self, kind: Kind) -> tuple[PromptSource, ...] | None:
return self.prompts.for_kind(kind)
def serves(self, kind: str) -> bool:
return kind in self.kinds