core/config: add [agent] section for profile defaults

Parse and sync system_prompt, max_tool_result_chars, and parallel_tools.
This commit is contained in:
2026-07-14 23:24:17 +08:00
parent b1964951ea
commit 9ec06c7398
4 changed files with 85 additions and 12 deletions
+1
View File
@@ -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
+8
View File
@@ -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."""
+42 -12
View File
@@ -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()
+34
View File
@@ -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