diff --git a/src/plyngent/config/__init__.py b/src/plyngent/config/__init__.py index 1524a3b..d2188dd 100644 --- a/src/plyngent/config/__init__.py +++ b/src/plyngent/config/__init__.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING import tomlkit from tomlkit.exceptions import TOMLKitError +from .models import AgentConfig as AgentConfig from .models import AnthropicProvider as AnthropicProvider from .models import DatabaseConfig as DatabaseConfig from .models import DeepseekProvider as DeepseekProvider diff --git a/src/plyngent/config/models.py b/src/plyngent/config/models.py index dfcb660..ba11891 100644 --- a/src/plyngent/config/models.py +++ b/src/plyngent/config/models.py @@ -10,6 +10,14 @@ class DatabaseConfig(Struct, omit_defaults=True): password: str | None = None +class AgentConfig(Struct, omit_defaults=True): + """Single-user agent profile defaults.""" + + system_prompt: str = "" + max_tool_result_chars: int = 32_000 + parallel_tools: bool = True + + class ModelConfig(Struct, omit_defaults=True): """Capability flags for a model within a provider.""" diff --git a/src/plyngent/config/store.py b/src/plyngent/config/store.py index 1e29316..86a1642 100644 --- a/src/plyngent/config/store.py +++ b/src/plyngent/config/store.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, cast import msgspec import tomlkit -from .models import DatabaseConfig, Provider +from .models import AgentConfig, DatabaseConfig, Provider if TYPE_CHECKING: from collections.abc import Mapping, MutableMapping @@ -25,6 +25,14 @@ def _parse_database(raw: dict[str, object]) -> DatabaseConfig: return DatabaseConfig() +def _parse_agent(raw: dict[str, object]) -> AgentConfig: + """Parse the ``[agent]`` section, falling back to defaults.""" + try: + return msgspec.convert(raw, AgentConfig) + except msgspec.ValidationError: + return AgentConfig() + + def _parse_providers( document: tomlkit.TOMLDocument, ) -> tuple[dict[str, Provider], dict[str, object]]: @@ -73,6 +81,7 @@ class ConfigStore: _path: Path _document: tomlkit.TOMLDocument _database: DatabaseConfig + _agent: AgentConfig _providers: dict[str, Provider] _bad_providers: dict[str, object] @@ -81,6 +90,7 @@ class ConfigStore: self._document = document raw: dict[str, object] = document.unwrap() self._database = _parse_database(cast("dict[str, object]", raw.get("database", {}))) + self._agent = _parse_agent(cast("dict[str, object]", raw.get("agent", {}))) self._providers, self._bad_providers = _parse_providers(document) # -- database (read-only) -- @@ -90,6 +100,18 @@ class ConfigStore: """Read-only mapping view of database configuration.""" return MappingProxyType(msgspec.structs.asdict(self._database)) + # -- agent (read-only) -- + + @property + def agent(self) -> MappingProxyType[str, object]: + """Read-only mapping view of agent profile configuration.""" + return MappingProxyType(msgspec.structs.asdict(self._agent)) + + @property + def agent_config(self) -> AgentConfig: + """Typed agent profile (system prompt, tool budgets, etc.).""" + return self._agent + # -- providers (read/write) -- @property @@ -128,6 +150,7 @@ class ConfigStore: self._document = tomlkit.parse(f.read()) raw: dict[str, object] = self._document.unwrap() self._database = _parse_database(cast("dict[str, object]", raw.get("database", {}))) + self._agent = _parse_agent(cast("dict[str, object]", raw.get("agent", {}))) self._providers, self._bad_providers = _parse_providers(self._document) # -- internal sync helpers -- @@ -138,20 +161,26 @@ class ConfigStore: self._document[key] = tomlkit.table() return cast("MutableMapping[str, object]", self._document[key]) + def _sync_section(self, key: str, data: object) -> None: + raw: dict[str, object] = msgspec.to_builtins(data) + if not raw: + if key in self._document: + del self._document[key] + return + section = self._toml_table(key) + for k in list(section.keys()): + if k not in raw: + del section[k] + for k, v in raw.items(): + section[k] = v + def _sync_database_section(self) -> None: """Sync ``[database]`` to the document.""" - raw_db: dict[str, object] = msgspec.to_builtins(self._database) - if not raw_db: - if "database" in self._document: - del self._document["database"] - return + self._sync_section("database", self._database) - section = self._toml_table("database") - for k in list(section.keys()): - if k not in raw_db: - del section[k] - for k, v in raw_db.items(): - section[k] = v + def _sync_agent_section(self) -> None: + """Sync ``[agent]`` to the document.""" + self._sync_section("agent", self._agent) def _sync_providers_section(self) -> None: """Sync ``[providers]`` to the document.""" @@ -174,4 +203,5 @@ class ConfigStore: def _sync_to_document(self) -> None: """Incrementally sync all sections into the document.""" self._sync_database_section() + self._sync_agent_section() self._sync_providers_section() diff --git a/tests/test_config/test_agent_section.py b/tests/test_config/test_agent_section.py new file mode 100644 index 0000000..af1baae --- /dev/null +++ b/tests/test_config/test_agent_section.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from plyngent.config import load + +if TYPE_CHECKING: + from pathlib import Path + + +def test_agent_section_defaults(tmp_path: Path) -> None: + path = tmp_path / "c.toml" + _ = path.write_text("", encoding="utf-8") + store = load(path) + assert store.agent_config.system_prompt == "" + assert store.agent_config.max_tool_result_chars == 32_000 # noqa: PLR2004 + assert store.agent_config.parallel_tools is True + + +def test_agent_section_parse(tmp_path: Path) -> None: + path = tmp_path / "c.toml" + _ = path.write_text( + """ +[agent] +system_prompt = "Be brief." +max_tool_result_chars = 100 +parallel_tools = false +""", + encoding="utf-8", + ) + store = load(path) + assert store.agent_config.system_prompt == "Be brief." + assert store.agent_config.max_tool_result_chars == 100 # noqa: PLR2004 + assert store.agent_config.parallel_tools is False