refactor(agents,core,frontends): typed kinds, prompts per kind, frontend agents as parameters
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user