Skip to content

Commit cc1f534

Browse files
refactor(sdk): drop remaining legacy settings code
Co-authored-by: openhands <openhands@all-hands.dev>
1 parent 088c873 commit cc1f534

File tree

2 files changed

+10
-175
lines changed

2 files changed

+10
-175
lines changed

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ When reviewing code, provide constructive feedback:
106106
- `SettingsFieldSchema` intentionally does not export a `required` flag. If a consumer needs nullability semantics, inspect the underlying Python typing rather than inferring from SDK defaults.
107107
- `AgentSettings.tools` is part of the exported settings schema so the schema stays aligned with the settings payload that round-trips through `AgentSettings` and drives `create_agent()`.
108108
- `AgentSettings.mcp_config` now uses FastMCP's typed `MCPConfig` at runtime. When serializing settings back to plain data (e.g. `model_dump()` or `create_agent()`), keep the output compact with `exclude_none=True, exclude_defaults=True` so callers still see the familiar `.mcp.json`-style dict shape.
109-
- Persisted `AgentSettings` now have an SDK-owned canonical migration contract: legacy v1 payloads are the raw unversioned settings mapping, while the current canonical format is v2 `{\"version\": 2, \"settings\": ...}`. Use `AgentSettings.load_persisted()`/`dump_persisted()` so consumers share the same upgrade path and preserve sparse overlays with `exclude_unset=True`.
109+
- Persisted SDK settings should use the direct `model_dump()` shape with a top-level `schema_version`; avoid adding wrapped payload formats or legacy migration shims in `openhands/sdk/settings/model.py`.
110110
- `ConversationSettings` owns the conversation-scoped confirmation controls directly (`confirmation_mode`, `security_analyzer`); keep those fields top-level on the model and grouped into the exported `verification` section via schema metadata rather than nested helper models.
111111
- Anthropic malformed tool-use/tool-result history errors (for example, missing or duplicated ``tool_result`` blocks) are intentionally mapped to a dedicated `LLMMalformedConversationHistoryError` and caught separately in `Agent.step()`, so recovery can still use condensation while logs preserve that this was malformed history rather than a true context-window overflow.
112112
- AgentSkills progressive disclosure goes through `AgentContext.get_system_message_suffix()` into `<available_skills>`, and `openhands.sdk.context.skills.to_prompt()` truncates each prompt description to 1024 characters because the AgentSkills specification caps `description` at 1-1024 characters.

openhands-sdk/openhands/sdk/settings/model.py

Lines changed: 9 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from collections.abc import Callable, Mapping
3+
from collections.abc import Mapping
44
from copy import deepcopy
55
from enum import Enum
66
from pathlib import Path
@@ -205,121 +205,14 @@ def _default_llm_settings() -> LLM:
205205
return LLM(model=model)
206206

207207

208-
# Persisted settings payloads currently use schema_version 1.
209-
_LEGACY_AGENT_SETTINGS_VERSION = 1
210-
_CURRENT_AGENT_SETTINGS_VERSION = 1
211-
_PERSISTED_AGENT_SETTINGS_VERSION_KEY = "schema_version"
212-
_LEGACY_WRAPPED_SETTINGS_VERSION_KEY = "version"
213-
_LEGACY_WRAPPED_SETTINGS_SETTINGS_KEY = "settings"
208+
_SCHEMA_VERSION_KEY = "schema_version"
209+
_AGENT_SETTINGS_SCHEMA_VERSION = 1
210+
_CONVERSATION_SETTINGS_SCHEMA_VERSION = 1
214211

215212

216213
_MISSING = object()
217214

218215

219-
def _assign_dotted_value(target: dict[str, Any], dotted_key: str, value: Any) -> None:
220-
current = target
221-
parts = dotted_key.split(".")
222-
for part in parts[:-1]:
223-
current = current.setdefault(part, {})
224-
current[parts[-1]] = deepcopy(value)
225-
226-
227-
def _normalize_legacy_payload(payload: Mapping[str, Any]) -> dict[str, Any]:
228-
normalized: dict[str, Any] = {}
229-
for key, value in payload.items():
230-
if key == _PERSISTED_AGENT_SETTINGS_VERSION_KEY:
231-
continue
232-
if "." in key:
233-
_assign_dotted_value(normalized, key, value)
234-
continue
235-
normalized[key] = deepcopy(value)
236-
return normalized
237-
238-
239-
def _migrate_agent_settings_v1_to_v2(payload: Mapping[str, Any]) -> dict[str, Any]:
240-
migrated = _normalize_legacy_payload(payload)
241-
migrated[_PERSISTED_AGENT_SETTINGS_VERSION_KEY] = _CURRENT_AGENT_SETTINGS_VERSION
242-
return migrated
243-
244-
245-
_AGENT_SETTINGS_MIGRATIONS: dict[int, Callable[[Mapping[str, Any]], dict[str, Any]]] = {
246-
_LEGACY_AGENT_SETTINGS_VERSION: _migrate_agent_settings_v1_to_v2,
247-
}
248-
249-
250-
def _coerce_persisted_agent_settings_payload(
251-
payload: Mapping[str, Any],
252-
) -> dict[str, Any]:
253-
if (
254-
_LEGACY_WRAPPED_SETTINGS_VERSION_KEY in payload
255-
or _LEGACY_WRAPPED_SETTINGS_SETTINGS_KEY in payload
256-
):
257-
settings_payload = payload.get(_LEGACY_WRAPPED_SETTINGS_SETTINGS_KEY)
258-
if not isinstance(settings_payload, Mapping):
259-
raise TypeError(
260-
"Persisted AgentSettings settings payload must be a mapping."
261-
)
262-
version = payload.get(_LEGACY_WRAPPED_SETTINGS_VERSION_KEY)
263-
if version is None:
264-
return dict(settings_payload)
265-
if not isinstance(version, int) or isinstance(version, bool):
266-
raise TypeError(
267-
"Persisted AgentSettings version must be an integer when provided."
268-
)
269-
migrated_payload = dict(settings_payload)
270-
migrated_payload[_PERSISTED_AGENT_SETTINGS_VERSION_KEY] = version
271-
return migrated_payload
272-
273-
return dict(payload)
274-
275-
276-
def _migrate_persisted_agent_settings_payload(
277-
payload: Mapping[str, Any],
278-
) -> dict[str, Any]:
279-
migrated_payload = _coerce_persisted_agent_settings_payload(payload)
280-
version = migrated_payload.get(
281-
_PERSISTED_AGENT_SETTINGS_VERSION_KEY, _LEGACY_AGENT_SETTINGS_VERSION
282-
)
283-
if not isinstance(version, int) or isinstance(version, bool):
284-
raise TypeError("Persisted AgentSettings schema_version must be an integer.")
285-
if version < _LEGACY_AGENT_SETTINGS_VERSION:
286-
raise ValueError(f"Unsupported persisted AgentSettings version {version}.")
287-
if version > _CURRENT_AGENT_SETTINGS_VERSION:
288-
raise ValueError(
289-
"Persisted AgentSettings version is newer than this SDK supports."
290-
)
291-
292-
while version < _CURRENT_AGENT_SETTINGS_VERSION:
293-
migrator = _AGENT_SETTINGS_MIGRATIONS.get(version)
294-
if migrator is None:
295-
raise ValueError(f"Missing AgentSettings migrator for version {version}.")
296-
migrated_payload = migrator(migrated_payload)
297-
version = migrated_payload[_PERSISTED_AGENT_SETTINGS_VERSION_KEY]
298-
299-
return migrated_payload
300-
301-
302-
def _normalize_patch_payload(payload: Mapping[str, Any]) -> dict[str, Any]:
303-
if (
304-
_LEGACY_WRAPPED_SETTINGS_VERSION_KEY in payload
305-
or _LEGACY_WRAPPED_SETTINGS_SETTINGS_KEY in payload
306-
):
307-
wrapped_payload = payload.get(_LEGACY_WRAPPED_SETTINGS_SETTINGS_KEY)
308-
if not isinstance(wrapped_payload, Mapping):
309-
raise TypeError("AgentSettings patch payload must be a mapping.")
310-
payload = wrapped_payload
311-
312-
normalized: dict[str, Any] = {}
313-
for key, value in payload.items():
314-
if key == _PERSISTED_AGENT_SETTINGS_VERSION_KEY:
315-
continue
316-
if "." in key:
317-
_assign_dotted_value(normalized, key, value)
318-
continue
319-
normalized[key] = deepcopy(value)
320-
return normalized
321-
322-
323216
def _merge_patch_payload(
324217
base: Mapping[str, Any], patch: Mapping[str, Any]
325218
) -> dict[str, Any]:
@@ -336,7 +229,7 @@ def _merge_patch_payload(
336229
def _diff_payload(base: Mapping[str, Any], target: Mapping[str, Any]) -> dict[str, Any]:
337230
diff: dict[str, Any] = {}
338231
for key in sorted(set(base) | set(target)):
339-
if key == _PERSISTED_AGENT_SETTINGS_VERSION_KEY:
232+
if key == _SCHEMA_VERSION_KEY:
340233
continue
341234
base_value = base.get(key, _MISSING)
342235
target_value = target.get(key, _MISSING)
@@ -356,68 +249,10 @@ def _diff_payload(base: Mapping[str, Any], target: Mapping[str, Any]) -> dict[st
356249
return diff
357250

358251

359-
_LEGACY_CONVERSATION_SETTINGS_VERSION = 1
360-
_CURRENT_CONVERSATION_SETTINGS_VERSION = 1
361-
362-
363-
def _coerce_persisted_conversation_settings_payload(
364-
payload: Mapping[str, Any],
365-
) -> dict[str, Any]:
366-
if (
367-
_LEGACY_WRAPPED_SETTINGS_VERSION_KEY in payload
368-
or _LEGACY_WRAPPED_SETTINGS_SETTINGS_KEY in payload
369-
):
370-
settings_payload = payload.get(_LEGACY_WRAPPED_SETTINGS_SETTINGS_KEY)
371-
if not isinstance(settings_payload, Mapping):
372-
raise TypeError(
373-
"Persisted ConversationSettings settings payload must be a mapping."
374-
)
375-
version = payload.get(_LEGACY_WRAPPED_SETTINGS_VERSION_KEY)
376-
if version is None:
377-
return dict(settings_payload)
378-
if not isinstance(version, int) or isinstance(version, bool):
379-
raise TypeError(
380-
"Persisted ConversationSettings version must be an integer"
381-
" when provided."
382-
)
383-
migrated_payload = dict(settings_payload)
384-
migrated_payload[_PERSISTED_AGENT_SETTINGS_VERSION_KEY] = version
385-
return migrated_payload
386-
387-
return dict(payload)
388-
389-
390-
def _migrate_persisted_conversation_settings_payload(
391-
payload: Mapping[str, Any],
392-
) -> dict[str, Any]:
393-
migrated_payload = _coerce_persisted_conversation_settings_payload(payload)
394-
version = migrated_payload.get(
395-
_PERSISTED_AGENT_SETTINGS_VERSION_KEY,
396-
_LEGACY_CONVERSATION_SETTINGS_VERSION,
397-
)
398-
if not isinstance(version, int) or isinstance(version, bool):
399-
raise TypeError(
400-
"Persisted ConversationSettings schema_version must be an integer."
401-
)
402-
if version < _LEGACY_CONVERSATION_SETTINGS_VERSION:
403-
raise ValueError(
404-
f"Unsupported persisted ConversationSettings version {version}."
405-
)
406-
if version > _CURRENT_CONVERSATION_SETTINGS_VERSION:
407-
raise ValueError(
408-
"Persisted ConversationSettings version is newer than this SDK supports."
409-
)
410-
411-
migrated_payload[_PERSISTED_AGENT_SETTINGS_VERSION_KEY] = (
412-
_CURRENT_CONVERSATION_SETTINGS_VERSION
413-
)
414-
return migrated_payload
415-
416-
417252
class ConversationSettings(BaseModel):
418-
CURRENT_PERSISTED_VERSION: ClassVar[int] = _CURRENT_CONVERSATION_SETTINGS_VERSION
253+
CURRENT_PERSISTED_VERSION: ClassVar[int] = _CONVERSATION_SETTINGS_SCHEMA_VERSION
419254

420-
schema_version: int = Field(default=_CURRENT_CONVERSATION_SETTINGS_VERSION, ge=1)
255+
schema_version: int = Field(default=_CONVERSATION_SETTINGS_SCHEMA_VERSION, ge=1)
421256
max_iterations: int = Field(
422257
default=500,
423258
ge=1,
@@ -499,9 +334,9 @@ def to_start_request_kwargs(self) -> dict[str, Any]:
499334

500335

501336
class AgentSettings(BaseModel):
502-
CURRENT_PERSISTED_VERSION: ClassVar[int] = _CURRENT_AGENT_SETTINGS_VERSION
337+
CURRENT_PERSISTED_VERSION: ClassVar[int] = _AGENT_SETTINGS_SCHEMA_VERSION
503338

504-
schema_version: int = Field(default=_CURRENT_AGENT_SETTINGS_VERSION, ge=1)
339+
schema_version: int = Field(default=_AGENT_SETTINGS_SCHEMA_VERSION, ge=1)
505340
agent: str = Field(
506341
default="CodeActAgent",
507342
description="Agent class to use.",

0 commit comments

Comments
 (0)