Skip to content

Commit cad179d

Browse files
neubigopenhands-agentxingyaoww
authored
refactor(settings): split agent and conversation settings schemas (#2789)
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
1 parent 85341cb commit cad179d

File tree

12 files changed

+683
-191
lines changed

12 files changed

+683
-191
lines changed

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ 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 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`.
110+
- Because persisted settings are not in production yet, prefer removing temporary compatibility fields and serializers outright instead of carrying legacy settings shims in the SDK.
111+
- Do not expose settings schema versions as public `CURRENT_PERSISTED_VERSION` class constants on `AgentSettings` or `ConversationSettings`; keep versioning internal to the `schema_version` field/defaults and private module constants.
112+
- `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, and prefer the direct settings-model constructor `create_request(...)` over separate request-wrapper helpers.
109113
- 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.
110114
- 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.
111115
- Workspace-wide uv resolver guardrails belong in the repository root `[tool.uv]` table. When `exclude-newer` is configured there, `uv lock` persists it into the root `uv.lock` `[options]` section as both an absolute cutoff and `exclude-newer-span`, and `uv sync --frozen` continues to use that locked workspace state.

openhands-agent-server/openhands/agent_server/event_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,8 +498,8 @@ async def start(self):
498498
tags=self.stored.tags,
499499
)
500500

501-
# Set confirmation mode if enabled
502501
conversation.set_confirmation_policy(self.stored.confirmation_policy)
502+
conversation.set_security_analyzer(self.stored.security_analyzer)
503503
self._conversation = conversation
504504

505505
# Register state change callback to automatically publish updates

openhands-agent-server/openhands/agent_server/models.py

Lines changed: 14 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,40 @@
11
from abc import ABC
22
from datetime import datetime
33
from enum import Enum
4-
from typing import Annotated, Any, Literal
4+
from typing import Any
55
from uuid import UUID, uuid4
66

7-
from pydantic import BaseModel, Discriminator, Field, Tag, field_validator
7+
from pydantic import BaseModel, Field, field_validator
88

9-
from openhands.agent_server.utils import OpenHandsUUID, utc_now
10-
from openhands.sdk import LLM, Agent, ImageContent, Message, TextContent
11-
from openhands.sdk.agent.acp_agent import ACPAgent
9+
from openhands.sdk import LLM, Agent
1210
from openhands.sdk.conversation.conversation_stats import ConversationStats
11+
from openhands.sdk.conversation.request import ( # re-export for backward compat
12+
ACPEnabledAgent as ACPEnabledAgent,
13+
SendMessageRequest as SendMessageRequest,
14+
StartACPConversationRequest as StartACPConversationRequest,
15+
StartConversationRequest as StartConversationRequest,
16+
)
1317
from openhands.sdk.conversation.secret_registry import SecretRegistry
1418
from openhands.sdk.conversation.state import ConversationExecutionStatus
1519
from openhands.sdk.conversation.types import ConversationTags
1620
from openhands.sdk.event.base import Event
1721
from openhands.sdk.hooks import HookConfig
22+
from openhands.sdk.llm.message import ( # re-export
23+
ImageContent as ImageContent,
24+
TextContent as TextContent,
25+
)
1826
from openhands.sdk.llm.utils.metrics import MetricsSnapshot
19-
from openhands.sdk.plugin import PluginSource
2027
from openhands.sdk.secret import SecretSource
2128
from openhands.sdk.security.analyzer import SecurityAnalyzerBase
2229
from openhands.sdk.security.confirmation_policy import (
2330
ConfirmationPolicyBase,
2431
NeverConfirm,
2532
)
26-
from openhands.sdk.subagent.schema import AgentDefinition
33+
from openhands.sdk.utils import OpenHandsUUID, utc_now
2734
from openhands.sdk.utils.models import (
2835
DiscriminatedUnionMixin,
2936
OpenHandsModel,
30-
kind_of,
3137
)
32-
from openhands.sdk.workspace import LocalWorkspace
3338
from openhands.sdk.workspace.base import BaseWorkspace
3439

3540

@@ -46,12 +51,6 @@ class ServerErrorEvent(Event):
4651
detail: str = Field(description="Details about the error")
4752

4853

49-
ACPEnabledAgent = Annotated[
50-
Annotated[Agent, Tag("Agent")] | Annotated[ACPAgent, Tag("ACPAgent")],
51-
Discriminator(kind_of),
52-
]
53-
54-
5554
class ConversationSortOrder(str, Enum):
5655
"""Enum for conversation sorting options."""
5756

@@ -68,126 +67,6 @@ class EventSortOrder(str, Enum):
6867
TIMESTAMP_DESC = "TIMESTAMP_DESC"
6968

7069

71-
class SendMessageRequest(BaseModel):
72-
"""Payload to send a message to the agent.
73-
74-
This is a simplified version of openhands.sdk.Message.
75-
"""
76-
77-
role: Literal["user", "system", "assistant", "tool"] = "user"
78-
content: list[TextContent | ImageContent] = Field(default_factory=list)
79-
run: bool = Field(
80-
default=False,
81-
description=("Whether the agent loop should automatically run if not running"),
82-
)
83-
84-
def create_message(self) -> Message:
85-
message = Message(role=self.role, content=self.content)
86-
return message
87-
88-
89-
class _StartConversationRequestBase(BaseModel):
90-
"""Common conversation creation fields shared by conversation contracts."""
91-
92-
workspace: LocalWorkspace = Field(
93-
...,
94-
description="Working directory for agent operations and tool execution",
95-
)
96-
conversation_id: UUID | None = Field(
97-
default=None,
98-
description=(
99-
"Optional conversation ID. If not provided, a random UUID will be "
100-
"generated."
101-
),
102-
)
103-
confirmation_policy: ConfirmationPolicyBase = Field(
104-
default=NeverConfirm(),
105-
description="Controls when the conversation will prompt the user before "
106-
"continuing. Defaults to never.",
107-
)
108-
initial_message: SendMessageRequest | None = Field(
109-
default=None, description="Initial message to pass to the LLM"
110-
)
111-
max_iterations: int = Field(
112-
default=500,
113-
ge=1,
114-
description="If set, the max number of iterations the agent will run "
115-
"before stopping. This is useful to prevent infinite loops.",
116-
)
117-
stuck_detection: bool = Field(
118-
default=True,
119-
description="If true, the conversation will use stuck detection to "
120-
"prevent infinite loops.",
121-
)
122-
secrets: dict[str, SecretSource] = Field(
123-
default_factory=dict,
124-
description="Secrets available in the conversation",
125-
)
126-
tool_module_qualnames: dict[str, str] = Field(
127-
default_factory=dict,
128-
description=(
129-
"Mapping of tool names to their module qualnames from the client's "
130-
"registry. These modules will be dynamically imported on the server "
131-
"to register the tools for this conversation."
132-
),
133-
)
134-
agent_definitions: list[AgentDefinition] = Field(
135-
default_factory=list,
136-
description=(
137-
"Agent definitions from the client's registry. These are "
138-
"registered on the server so that DelegateTool and TaskSetTool "
139-
"can see user-registered subagents."
140-
),
141-
)
142-
plugins: list[PluginSource] | None = Field(
143-
default=None,
144-
description=(
145-
"List of plugins to load for this conversation. Plugins are loaded "
146-
"and their skills/MCP config are merged into the agent. "
147-
"Hooks are extracted and stored for runtime execution."
148-
),
149-
)
150-
hook_config: HookConfig | None = Field(
151-
default=None,
152-
description=(
153-
"Optional hook configuration for this conversation. Hooks are shell "
154-
"scripts that run at key lifecycle events (PreToolUse, PostToolUse, "
155-
"UserPromptSubmit, Stop, etc.). If both hook_config and plugins are "
156-
"provided, they are merged with explicit hooks running before plugin "
157-
"hooks."
158-
),
159-
)
160-
tags: ConversationTags = Field(
161-
default_factory=dict,
162-
description=(
163-
"Key-value tags for the conversation. Keys must be lowercase "
164-
"alphanumeric. Values are arbitrary strings up to 256 characters."
165-
),
166-
)
167-
autotitle: bool = Field(
168-
default=True,
169-
description=(
170-
"If true, automatically generate a title for the conversation from "
171-
"the first user message using the conversation's LLM."
172-
),
173-
)
174-
175-
176-
class StartConversationRequest(_StartConversationRequestBase):
177-
"""Payload to create a new conversation.
178-
179-
Contains an Agent configuration along with conversation-specific options.
180-
"""
181-
182-
agent: Agent
183-
184-
185-
class StartACPConversationRequest(_StartConversationRequestBase):
186-
"""Payload to create a conversation with ACP-capable agent support."""
187-
188-
agent: ACPEnabledAgent
189-
190-
19170
class StoredConversation(StartACPConversationRequest):
19271
"""Stored details about a conversation.
19372

openhands-agent-server/openhands/agent_server/settings_router.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from fastapi import APIRouter
44

5-
from openhands.sdk.settings import AgentSettings, SettingsSchema
5+
from openhands.sdk.settings import AgentSettings, ConversationSettings, SettingsSchema
66

77

88
settings_router = APIRouter(prefix="/settings", tags=["Settings"])
@@ -13,7 +13,18 @@ def _get_agent_settings_schema() -> SettingsSchema:
1313
return AgentSettings.export_schema()
1414

1515

16-
@settings_router.get("/schema", response_model=SettingsSchema)
16+
@lru_cache(maxsize=1)
17+
def _get_conversation_settings_schema() -> SettingsSchema:
18+
return ConversationSettings.export_schema()
19+
20+
21+
@settings_router.get("/agent-schema", response_model=SettingsSchema)
1722
async def get_agent_settings_schema() -> SettingsSchema:
1823
"""Return the schema used to render AgentSettings-based settings forms."""
1924
return _get_agent_settings_schema()
25+
26+
27+
@settings_router.get("/conversation-schema", response_model=SettingsSchema)
28+
async def get_conversation_settings_schema() -> SettingsSchema:
29+
"""Return the schema used to render ConversationSettings-based forms."""
30+
return _get_conversation_settings_schema()

openhands-sdk/openhands/sdk/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from openhands.sdk.settings import (
4848
AgentSettings,
4949
CondenserSettings,
50+
ConversationSettings,
5051
SettingsChoice,
5152
SettingsFieldSchema,
5253
SettingsSchema,
@@ -137,6 +138,7 @@
137138
"AgentContext",
138139
"LLMSummarizingCondenser",
139140
"CondenserSettings",
141+
"ConversationSettings",
140142
"VerificationSettings",
141143
"AgentSettings",
142144
"SettingsChoice",

0 commit comments

Comments
 (0)