|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +from collections.abc import Callable, Mapping |
3 | 4 | from enum import Enum |
4 | 5 | from pathlib import Path |
5 | | -from typing import TYPE_CHECKING, Any, Literal, get_args, get_origin |
| 6 | +from typing import TYPE_CHECKING, Any, ClassVar, Literal, get_args, get_origin |
6 | 7 |
|
7 | 8 | from fastmcp.mcp_config import MCPConfig |
8 | 9 | from pydantic import BaseModel, Field, SecretStr, field_serializer, field_validator |
@@ -220,7 +221,85 @@ def _default_llm_settings() -> LLM: |
220 | 221 | return LLM(model=model) |
221 | 222 |
|
222 | 223 |
|
| 224 | +# Canonical persisted AgentSettings payload contract: |
| 225 | +# - v1 (legacy): raw, unversioned AgentSettings mapping |
| 226 | +# - v2 (current): {"version": 2, "settings": <partial AgentSettings mapping>} |
| 227 | +_LEGACY_AGENT_SETTINGS_VERSION = 1 |
| 228 | +_CURRENT_AGENT_SETTINGS_VERSION = 2 |
| 229 | +_PERSISTED_AGENT_SETTINGS_VERSION_KEY = "version" |
| 230 | +_PERSISTED_AGENT_SETTINGS_SETTINGS_KEY = "settings" |
| 231 | + |
| 232 | + |
| 233 | +def _migrate_agent_settings_v1_to_v2(payload: dict[str, Any]) -> dict[str, Any]: |
| 234 | + return payload |
| 235 | + |
| 236 | + |
| 237 | +_AGENT_SETTINGS_MIGRATIONS: dict[int, Callable[[dict[str, Any]], dict[str, Any]]] = { |
| 238 | + _LEGACY_AGENT_SETTINGS_VERSION: _migrate_agent_settings_v1_to_v2, |
| 239 | +} |
| 240 | + |
| 241 | + |
| 242 | +def _coerce_persisted_agent_settings_payload( |
| 243 | + payload: Mapping[str, Any], |
| 244 | +) -> tuple[int, dict[str, Any]]: |
| 245 | + if ( |
| 246 | + _PERSISTED_AGENT_SETTINGS_VERSION_KEY in payload |
| 247 | + or _PERSISTED_AGENT_SETTINGS_SETTINGS_KEY in payload |
| 248 | + ): |
| 249 | + version = payload.get(_PERSISTED_AGENT_SETTINGS_VERSION_KEY) |
| 250 | + if not isinstance(version, int) or isinstance(version, bool): |
| 251 | + raise TypeError( |
| 252 | + "Persisted AgentSettings version must be an integer when provided." |
| 253 | + ) |
| 254 | + if version < _LEGACY_AGENT_SETTINGS_VERSION: |
| 255 | + raise ValueError(f"Unsupported persisted AgentSettings version {version}.") |
| 256 | + settings_payload = payload.get(_PERSISTED_AGENT_SETTINGS_SETTINGS_KEY) |
| 257 | + if not isinstance(settings_payload, Mapping): |
| 258 | + raise TypeError( |
| 259 | + "Persisted AgentSettings settings payload must be a mapping." |
| 260 | + ) |
| 261 | + return version, dict(settings_payload) |
| 262 | + |
| 263 | + return _LEGACY_AGENT_SETTINGS_VERSION, dict(payload) |
| 264 | + |
| 265 | + |
| 266 | +def _migrate_persisted_agent_settings_payload( |
| 267 | + payload: Mapping[str, Any], |
| 268 | +) -> dict[str, Any]: |
| 269 | + version, settings_payload = _coerce_persisted_agent_settings_payload(payload) |
| 270 | + |
| 271 | + if version > _CURRENT_AGENT_SETTINGS_VERSION: |
| 272 | + raise ValueError( |
| 273 | + "Persisted AgentSettings version is newer than this SDK supports." |
| 274 | + ) |
| 275 | + |
| 276 | + while version < _CURRENT_AGENT_SETTINGS_VERSION: |
| 277 | + migrator = _AGENT_SETTINGS_MIGRATIONS.get(version) |
| 278 | + if migrator is None: |
| 279 | + raise ValueError(f"Missing AgentSettings migrator for version {version}.") |
| 280 | + settings_payload = migrator(settings_payload) |
| 281 | + version += 1 |
| 282 | + |
| 283 | + return { |
| 284 | + _PERSISTED_AGENT_SETTINGS_VERSION_KEY: version, |
| 285 | + _PERSISTED_AGENT_SETTINGS_SETTINGS_KEY: settings_payload, |
| 286 | + } |
| 287 | + |
| 288 | + |
| 289 | +def _dump_persisted_agent_settings_payload(settings: AgentSettings) -> dict[str, Any]: |
| 290 | + return { |
| 291 | + _PERSISTED_AGENT_SETTINGS_VERSION_KEY: settings.CURRENT_PERSISTED_VERSION, |
| 292 | + _PERSISTED_AGENT_SETTINGS_SETTINGS_KEY: settings.model_dump( |
| 293 | + mode="json", |
| 294 | + exclude_unset=True, |
| 295 | + context={"expose_secrets": True}, |
| 296 | + ), |
| 297 | + } |
| 298 | + |
| 299 | + |
223 | 300 | class AgentSettings(BaseModel): |
| 301 | + CURRENT_PERSISTED_VERSION: ClassVar[int] = _CURRENT_AGENT_SETTINGS_VERSION |
| 302 | + |
224 | 303 | agent: str = Field( |
225 | 304 | default="CodeActAgent", |
226 | 305 | description="Agent class to use.", |
@@ -304,6 +383,28 @@ def export_schema(cls) -> SettingsSchema: |
304 | 383 | """Export a structured schema describing configurable agent settings.""" |
305 | 384 | return export_settings_schema(cls) |
306 | 385 |
|
| 386 | + @classmethod |
| 387 | + def migrate_persisted_payload(cls, payload: Mapping[str, Any]) -> dict[str, Any]: |
| 388 | + """Return the latest canonical persisted AgentSettings payload. |
| 389 | +
|
| 390 | + Legacy v1 payloads were stored as the raw, unversioned AgentSettings |
| 391 | + mapping. The current canonical v2 payload stores that mapping under a |
| 392 | + top-level ``settings`` key alongside an integer ``version``. |
| 393 | + """ |
| 394 | + return _migrate_persisted_agent_settings_payload(payload) |
| 395 | + |
| 396 | + @classmethod |
| 397 | + def load_persisted(cls, payload: Mapping[str, Any]) -> AgentSettings: |
| 398 | + """Load persisted AgentSettings after applying SDK-owned migrations.""" |
| 399 | + migrated_payload = cls.migrate_persisted_payload(payload) |
| 400 | + settings_payload = migrated_payload[_PERSISTED_AGENT_SETTINGS_SETTINGS_KEY] |
| 401 | + assert isinstance(settings_payload, dict) |
| 402 | + return cls.model_validate(settings_payload) |
| 403 | + |
| 404 | + def dump_persisted(self) -> dict[str, Any]: |
| 405 | + """Dump AgentSettings in the latest canonical persisted payload format.""" |
| 406 | + return _dump_persisted_agent_settings_payload(self) |
| 407 | + |
307 | 408 | def create_agent(self) -> Agent: |
308 | 409 | """Build an :class:`Agent` purely from these settings. |
309 | 410 |
|
|
0 commit comments