@@ -100,7 +100,7 @@ class CondenserSettings(BaseModel):
100100
101101
102102class VerificationSettings (BaseModel ):
103- """Combined critic and security settings."""
103+ """Critic and iterative-refinement settings for the agent ."""
104104
105105 # -- Critic --
106106 critic_enabled : bool = Field (
@@ -192,7 +192,10 @@ class VerificationSettings(BaseModel):
192192 },
193193 )
194194
195- # -- Security --
195+
196+ class ConversationVerificationSettings (BaseModel ):
197+ """Conversation-level confirmation and security settings."""
198+
196199 confirmation_mode : bool = Field (
197200 default = False ,
198201 description = "Require user confirmation before executing risky actions." ,
@@ -204,7 +207,7 @@ class VerificationSettings(BaseModel):
204207 },
205208 )
206209 security_analyzer : SecurityAnalyzerType | None = Field (
207- default = None ,
210+ default = "llm" ,
208211 description = "Security analyzer that evaluates actions before execution." ,
209212 json_schema_extra = {
210213 SETTINGS_METADATA_KEY : SettingsFieldMetadata (
@@ -375,6 +378,161 @@ def _diff_payload(base: Mapping[str, Any], target: Mapping[str, Any]) -> dict[st
375378 return diff
376379
377380
381+ _LEGACY_CONVERSATION_SETTINGS_VERSION = 1
382+ _CURRENT_CONVERSATION_SETTINGS_VERSION = 1
383+
384+
385+ def _coerce_persisted_conversation_settings_payload (
386+ payload : Mapping [str , Any ],
387+ ) -> dict [str , Any ]:
388+ if (
389+ _LEGACY_WRAPPED_SETTINGS_VERSION_KEY in payload
390+ or _LEGACY_WRAPPED_SETTINGS_SETTINGS_KEY in payload
391+ ):
392+ settings_payload = payload .get (_LEGACY_WRAPPED_SETTINGS_SETTINGS_KEY )
393+ if not isinstance (settings_payload , Mapping ):
394+ raise TypeError (
395+ "Persisted ConversationSettings settings payload must be a mapping."
396+ )
397+ version = payload .get (_LEGACY_WRAPPED_SETTINGS_VERSION_KEY )
398+ if version is None :
399+ return dict (settings_payload )
400+ if not isinstance (version , int ) or isinstance (version , bool ):
401+ raise TypeError (
402+ "Persisted ConversationSettings version must be an integer"
403+ " when provided."
404+ )
405+ migrated_payload = dict (settings_payload )
406+ migrated_payload [_PERSISTED_AGENT_SETTINGS_VERSION_KEY ] = version
407+ return migrated_payload
408+
409+ return dict (payload )
410+
411+
412+ def _migrate_persisted_conversation_settings_payload (
413+ payload : Mapping [str , Any ],
414+ ) -> dict [str , Any ]:
415+ migrated_payload = _coerce_persisted_conversation_settings_payload (payload )
416+ version = migrated_payload .get (
417+ _PERSISTED_AGENT_SETTINGS_VERSION_KEY ,
418+ _LEGACY_CONVERSATION_SETTINGS_VERSION ,
419+ )
420+ if not isinstance (version , int ) or isinstance (version , bool ):
421+ raise TypeError (
422+ "Persisted ConversationSettings schema_version must be an integer."
423+ )
424+ if version < _LEGACY_CONVERSATION_SETTINGS_VERSION :
425+ raise ValueError (
426+ f"Unsupported persisted ConversationSettings version { version } ."
427+ )
428+ if version > _CURRENT_CONVERSATION_SETTINGS_VERSION :
429+ raise ValueError (
430+ "Persisted ConversationSettings version is newer than this SDK supports."
431+ )
432+
433+ migrated_payload [_PERSISTED_AGENT_SETTINGS_VERSION_KEY ] = (
434+ _CURRENT_CONVERSATION_SETTINGS_VERSION
435+ )
436+ return migrated_payload
437+
438+
439+ class ConversationSettings (BaseModel ):
440+ CURRENT_PERSISTED_VERSION : ClassVar [int ] = _CURRENT_CONVERSATION_SETTINGS_VERSION
441+
442+ schema_version : int = Field (default = _CURRENT_CONVERSATION_SETTINGS_VERSION , ge = 1 )
443+ max_iterations : int = Field (
444+ default = 500 ,
445+ ge = 1 ,
446+ description = (
447+ "Maximum number of iterations the conversation will run before stopping."
448+ ),
449+ json_schema_extra = {
450+ SETTINGS_METADATA_KEY : SettingsFieldMetadata (
451+ label = "Max iterations" ,
452+ prominence = SettingProminence .MAJOR ,
453+ ).model_dump ()
454+ },
455+ )
456+ verification : ConversationVerificationSettings = Field (
457+ default_factory = ConversationVerificationSettings ,
458+ description = "Conversation confirmation and security settings." ,
459+ json_schema_extra = {
460+ SETTINGS_SECTION_METADATA_KEY : SettingsSectionMetadata (
461+ key = "verification" ,
462+ label = "Verification" ,
463+ ).model_dump ()
464+ },
465+ )
466+
467+ @classmethod
468+ def export_schema (cls ) -> SettingsSchema :
469+ """Export a structured schema describing configurable conversation settings."""
470+ return export_settings_schema (cls )
471+
472+ @classmethod
473+ def migrate_persisted_payload (cls , payload : Mapping [str , Any ]) -> dict [str , Any ]:
474+ """Return the latest canonical persisted ConversationSettings payload."""
475+ return _migrate_persisted_conversation_settings_payload (payload )
476+
477+ @classmethod
478+ def from_persisted (cls , payload : Mapping [str , Any ]) -> ConversationSettings :
479+ """Load persisted ConversationSettings after applying SDK-owned migrations."""
480+ return cls .model_validate (cls .migrate_persisted_payload (payload ))
481+
482+ def patch (self , payload : Mapping [str , Any ]) -> ConversationSettings :
483+ """Return a new settings object with a persisted patch applied."""
484+ base_payload = self .model_dump (mode = "json" )
485+ merged_payload = _merge_patch_payload (
486+ base_payload , _normalize_patch_payload (payload )
487+ )
488+ merged_payload [_PERSISTED_AGENT_SETTINGS_VERSION_KEY ] = (
489+ self .CURRENT_PERSISTED_VERSION
490+ )
491+ return type (self ).from_persisted (merged_payload )
492+
493+ def diff (self , target : ConversationSettings | Mapping [str , Any ]) -> dict [str , Any ]:
494+ """Return the minimal persisted patch from these settings to ``target``."""
495+ target_settings = (
496+ target
497+ if isinstance (target , ConversationSettings )
498+ else type (self ).from_persisted (target )
499+ )
500+ base_payload = self .model_dump (mode = "json" )
501+ target_payload = target_settings .model_dump (mode = "json" )
502+ return _diff_payload (base_payload , target_payload )
503+
504+ def build_confirmation_policy (self ):
505+ from openhands .sdk .security .confirmation_policy import (
506+ AlwaysConfirm ,
507+ ConfirmRisky ,
508+ NeverConfirm ,
509+ )
510+
511+ if not self .verification .confirmation_mode :
512+ return NeverConfirm ()
513+ if (self .verification .security_analyzer or "" ).lower () == "llm" :
514+ return ConfirmRisky ()
515+ return AlwaysConfirm ()
516+
517+ def build_security_analyzer (self ):
518+ analyzer_kind = (self .verification .security_analyzer or "" ).lower ()
519+ if not analyzer_kind or analyzer_kind == "none" :
520+ return None
521+ if analyzer_kind == "llm" :
522+ from openhands .sdk .security .llm_analyzer import LLMSecurityAnalyzer
523+
524+ return LLMSecurityAnalyzer ()
525+ return None
526+
527+ def to_start_request_kwargs (self ) -> dict [str , Any ]:
528+ """Return StartConversationRequest-compatible kwargs for these settings."""
529+ return {
530+ "confirmation_policy" : self .build_confirmation_policy (),
531+ "security_analyzer" : self .build_security_analyzer (),
532+ "max_iterations" : self .max_iterations ,
533+ }
534+
535+
378536class AgentSettings (BaseModel ):
379537 CURRENT_PERSISTED_VERSION : ClassVar [int ] = _CURRENT_AGENT_SETTINGS_VERSION
380538
0 commit comments