diff --git a/docs.json b/docs.json
index ac98e277..ba0f48d4 100644
--- a/docs.json
+++ b/docs.json
@@ -246,10 +246,10 @@
{
"group": "OpenHands Community",
"pages": [
- "overview/community",
- "overview/contributing",
- "overview/faqs",
- "openhands/usage/troubleshooting/feedback"
+ "overview/community",
+ "overview/contributing",
+ "overview/faqs",
+ "openhands/usage/troubleshooting/feedback"
]
}
]
@@ -384,15 +384,33 @@
},
{
"group": "API Reference",
+ "collapsed": true,
"pages": [
- "sdk/api-reference/openhands.sdk.agent",
- "sdk/api-reference/openhands.sdk.conversation",
- "sdk/api-reference/openhands.sdk.event",
- "sdk/api-reference/openhands.sdk.llm",
- "sdk/api-reference/openhands.sdk.security",
- "sdk/api-reference/openhands.sdk.tool",
- "sdk/api-reference/openhands.sdk.utils",
- "sdk/api-reference/openhands.sdk.workspace"
+ {
+ "group": "Python SDK",
+ "collapsed": true,
+ "pages": [
+ "sdk/api-reference/openhands.sdk.agent",
+ "sdk/api-reference/openhands.sdk.context",
+ "sdk/api-reference/openhands.sdk.conversation",
+ "sdk/api-reference/openhands.sdk.critic",
+ "sdk/api-reference/openhands.sdk.event",
+ "sdk/api-reference/openhands.sdk.hooks",
+ "sdk/api-reference/openhands.sdk.io",
+ "sdk/api-reference/openhands.sdk.llm",
+ "sdk/api-reference/openhands.sdk.logger",
+ "sdk/api-reference/openhands.sdk.mcp",
+ "sdk/api-reference/openhands.sdk.observability",
+ "sdk/api-reference/openhands.sdk.plugin",
+ "sdk/api-reference/openhands.sdk.secret",
+ "sdk/api-reference/openhands.sdk.security",
+ "sdk/api-reference/openhands.sdk.skills",
+ "sdk/api-reference/openhands.sdk.subagent",
+ "sdk/api-reference/openhands.sdk.tool",
+ "sdk/api-reference/openhands.sdk.utils",
+ "sdk/api-reference/openhands.sdk.workspace"
+ ]
+ }
]
}
]
diff --git a/scripts/generate-api-docs.py b/scripts/generate-api-docs.py
index c2110840..49043450 100755
--- a/scripts/generate-api-docs.py
+++ b/scripts/generate-api-docs.py
@@ -167,9 +167,21 @@ def create_rst_files(self):
* :ref:`search`
''')
- # Main SDK module
+ # Main SDK module - dynamically build the toctree based on the modules list
+ # (the list is defined below, so we define it here first)
+ all_modules = [
+ # Core modules (original)
+ 'agent', 'conversation', 'event', 'llm',
+ 'tool', 'workspace', 'security', 'utils',
+ # Additional important modules
+ 'context', 'hooks', 'critic', 'mcp', 'plugin',
+ 'subagent', 'io', 'secret', 'skills',
+ 'observability', 'logger',
+ ]
+
+ toctree_entries = "\n".join(f" openhands.sdk.{m}" for m in all_modules)
sdk_rst = sphinx_source / "openhands.sdk.rst"
- sdk_rst.write_text('''
+ sdk_rst.write_text(f'''
openhands.sdk package
=====================
@@ -184,23 +196,11 @@ def create_rst_files(self):
.. toctree::
:maxdepth: 1
- openhands.sdk.agent
- openhands.sdk.conversation
- openhands.sdk.event
- openhands.sdk.llm
- openhands.sdk.tool
- openhands.sdk.workspace
- openhands.sdk.security
- openhands.sdk.utils
+{toctree_entries}
''')
- # Generate RST files for each major module
- modules = [
- 'agent', 'conversation', 'event', 'llm',
- 'tool', 'workspace', 'security', 'utils'
- ]
-
- for module in modules:
+ # Generate RST files for each major module (reuse the list defined above)
+ for module in all_modules:
module_rst = sphinx_source / f"openhands.sdk.{module}.rst"
module_rst.write_text(f'''
openhands.sdk.{module} module
@@ -233,6 +233,17 @@ def clean_generated_docs(self):
build_dir = self.sphinx_dir / "build"
+ # Define the modules we want to include (must match the list in create_rst_files)
+ allowed_modules = [
+ # Core modules (original)
+ 'agent', 'conversation', 'event', 'llm',
+ 'tool', 'workspace', 'security', 'utils',
+ # Additional important modules
+ 'context', 'hooks', 'critic', 'mcp', 'plugin',
+ 'subagent', 'io', 'secret', 'skills',
+ 'observability', 'logger',
+ ]
+
# Remove old output directory
if self.output_dir.exists():
shutil.rmtree(self.output_dir)
@@ -247,6 +258,13 @@ def clean_generated_docs(self):
if md_file.name == "openhands.sdk.md":
logger.info(f"Skipping {md_file.name} (top-level duplicate)")
continue
+
+ # Only process files for modules in the allowed list
+ # File names are like: openhands.sdk.module.md
+ module_name = md_file.stem.replace('openhands.sdk.', '')
+ if module_name not in allowed_modules:
+ logger.info(f"Skipping {md_file.name} (not in allowed modules)")
+ continue
logger.info(f"Processing {md_file.name}")
content = md_file.read_text()
@@ -277,7 +295,261 @@ def clean_multiline_dictionaries(self, content: str) -> str:
content = re.sub(pattern3, '(configuration object)', content, flags=re.DOTALL)
return content
+
+ def fix_example_blocks(self, content: str) -> str:
+ """Fix example code blocks that are not properly formatted."""
+ import re
+
+ lines = content.split('\n')
+ result_lines = []
+ i = 0
+
+ while i < len(lines):
+ line = lines[i]
+
+ # Check if this is an Example header followed by unformatted code
+ # Handle both header-style and plain "Example:" format
+ is_example_header = (
+ line.strip() in ['#### Example', '### Example', '## Example'] or
+ line.strip() == 'Example:' or
+ line.strip() == 'Example'
+ )
+
+ if is_example_header:
+ # Normalize to h4 header
+ result_lines.append('#### Example')
+ result_lines.append('')
+ i += 1
+
+ # Skip any blank lines after the header
+ while i < len(lines) and not lines[i].strip():
+ i += 1
+
+ # Check if the next line looks like code (not a proper code block)
+ if i < len(lines) and not lines[i].startswith('```'):
+ # Collect all lines until we hit another header or blank line followed by a header
+ code_lines = []
+ while i < len(lines):
+ current = lines[i]
+ # Stop if we hit a header
+ if current.startswith('#'):
+ break
+ # Stop if we hit Properties or Methods sections
+ if current.strip() in ['#### Properties', '#### Methods', '### Properties', '### Methods']:
+ break
+ # Stop if we hit two blank lines (paragraph break)
+ if not current.strip() and i + 1 < len(lines) and lines[i + 1].startswith('#'):
+ break
+ code_lines.append(current)
+ i += 1
+
+ # Remove trailing blank lines from code
+ while code_lines and not code_lines[-1].strip():
+ code_lines.pop()
+
+ # Clean up the code lines
+ cleaned_code = []
+ for code_line in code_lines:
+ # Remove RST-style definition list markers
+ code_line = re.sub(r'^:\s*', '', code_line)
+ # Remove
tags
+ code_line = code_line.replace('`
`', '')
+ code_line = code_line.replace('
', '')
+ # Remove standalone > characters (blockquote artifacts)
+ code_line = re.sub(r'^\s*>\s*', '', code_line)
+ cleaned_code.append(code_line)
+
+ if cleaned_code:
+ # Determine language from content
+ first_non_empty = next((l for l in cleaned_code if l.strip()), '')
+ if first_non_empty.strip().startswith('{') or first_non_empty.strip() == 'json':
+ # Remove the standalone 'json' line if present
+ if cleaned_code and cleaned_code[0].strip() == 'json':
+ cleaned_code = cleaned_code[1:]
+ result_lines.append('```json')
+ else:
+ result_lines.append('```python')
+ result_lines.extend(cleaned_code)
+ result_lines.append('```')
+ result_lines.append('')
+ # else: Already has a proper code block starting with ```
+ # The code block lines will be added by subsequent iterations
+ # (i is already pointing to the ``` line)
+ continue
+
+ result_lines.append(line)
+ i += 1
+
+ return '\n'.join(result_lines)
+ def fix_shell_config_examples(self, content: str) -> str:
+ """Fix shell-style configuration examples where # comments are interpreted as headers.
+
+ Wraps configuration blocks (KEY=VALUE lines with # comments) in code blocks.
+ """
+ import re
+
+ lines = content.split('\n')
+ result = []
+ i = 0
+
+ while i < len(lines):
+ line = lines[i]
+
+ # Check if this line introduces a configuration example
+ # Common patterns: "Example configuration:", "Configuration example:", etc.
+ if re.match(r'^(Example\s+)?[Cc]onfiguration[:\s]', line.strip()) or \
+ line.strip().endswith('configuration:'):
+ result.append(line)
+ i += 1
+
+ # Collect following lines that look like config (KEY=VALUE or # comments)
+ config_lines = []
+ while i < len(lines):
+ current = lines[i]
+ stripped = current.strip()
+
+ # Stop conditions: empty line followed by non-config, or header
+ if not stripped:
+ # Check if next non-blank is a real header (method/class)
+ j = i + 1
+ while j < len(lines) and not lines[j].strip():
+ j += 1
+ if j < len(lines) and lines[j].startswith('###'):
+ break
+ config_lines.append(current)
+ i += 1
+ continue
+
+ # Real markdown headers (### method or class)
+ if stripped.startswith('### ') or stripped.startswith('#### '):
+ break
+
+ # Config-like lines: KEY=VALUE, # comment, or continuation
+ if re.match(r'^[A-Z_]+=', stripped) or \
+ stripped.startswith('#') or \
+ '=' in stripped:
+ config_lines.append(current)
+ i += 1
+ else:
+ # Non-config line
+ break
+
+ # Remove trailing blank lines from config
+ while config_lines and not config_lines[-1].strip():
+ config_lines.pop()
+
+ if config_lines:
+ # Wrap in code block
+ result.append('```bash')
+ result.extend(config_lines)
+ result.append('```')
+ continue
+
+ result.append(line)
+ i += 1
+
+ return '\n'.join(result)
+
+ def add_class_separators(self, content: str) -> str:
+ """Add horizontal rules before class headers to ensure proper spacing.
+
+ This fixes the CSS issue where h4 (method headers like init()) followed by
+ h3 (class headers) lose their margin-top due to Mintlify's CSS rule that
+ sets margin-top: 0 for elements following h4.
+ """
+ import re
+
+ lines = content.split('\n')
+ result = []
+
+ for i, line in enumerate(lines):
+ # Check if this is a class header
+ # Match both "### class ..." and "### *class* ..." formats
+ is_class_header = (
+ line.startswith('### class ') or
+ line.startswith('### *class* ')
+ )
+
+ if is_class_header:
+ # Don't add separator before the very first class (after frontmatter)
+ # Check if there's actual content before this (not just blank lines or frontmatter)
+ has_content_before = False
+ for j in range(i - 1, -1, -1):
+ stripped = lines[j].strip()
+ if stripped and stripped != '---': # Ignore frontmatter delimiters
+ # Found non-blank, non-frontmatter content
+ has_content_before = True
+ break
+
+ if has_content_before:
+ # Add a horizontal rule before the class header for visual separation
+ # This overrides the margin-top: 0 from CSS
+ result.append('')
+ result.append('---')
+ result.append('')
+
+ result.append(line)
+
+ return '\n'.join(result)
+
+ def remove_malformed_examples(self, content: str) -> str:
+ """Remove Example sections that contain malformed code (raw JSON/code without proper code blocks)."""
+ import re
+
+ lines = content.split('\n')
+ result = []
+ i = 0
+
+ while i < len(lines):
+ line = lines[i]
+
+ # Check if this is an Example header
+ if line.strip() == '#### Example':
+ # Look ahead to see if the example content is properly formatted
+ j = i + 1
+
+ # Skip blank lines
+ while j < len(lines) and not lines[j].strip():
+ j += 1
+
+ # Check if the next non-blank line starts a proper code block
+ if j < len(lines) and lines[j].startswith('```'):
+ # This is a properly formatted example, keep it
+ result.append(line)
+ i += 1
+ continue
+
+ # Check if the content contains curly braces (JSON/dict) without code blocks
+ # This would cause MDX parsing errors
+ example_content = []
+ k = j
+ while k < len(lines):
+ if lines[k].startswith('#'): # Hit next header
+ break
+ example_content.append(lines[k])
+ k += 1
+
+ example_text = '\n'.join(example_content)
+ has_curly_braces = '{' in example_text or '}' in example_text
+ has_proper_code_block = '```' in example_text
+
+ if has_curly_braces and not has_proper_code_block:
+ # This is a malformed example with raw code - skip it entirely
+ # Skip to the next header
+ i = k
+ continue
+ else:
+ # Keep this example
+ result.append(line)
+ i += 1
+ continue
+
+ result.append(line)
+ i += 1
+
+ return '\n'.join(result)
+
def fix_header_hierarchy(self, content: str) -> str:
"""Fix header hierarchy to ensure proper nesting under class headers."""
import re
@@ -444,6 +716,35 @@ def is_property(self, header_line: str) -> bool:
def clean_markdown_content(self, content: str, filename: str) -> str:
"""Clean markdown content to be parser-friendly."""
+ # FIRST: Fix malformed *args and **kwargs patterns from Sphinx
+ # These appear as: ```\n*\n```\n
\nargs or similar
+ # Convert to clean `*args` and `**kwargs`
+ content = re.sub(
+ r'
\s*```\s*\n\s*\*\*\s*\n\s*```\s*
\s*kwargs',
+ '`**kwargs`',
+ content
+ )
+ content = re.sub(
+ r'
\s*```\s*\n\s*\*\s*\n\s*```\s*
\s*args',
+ '`*args`',
+ content
+ )
+ # Also without
tags
+ content = re.sub(
+ r'```\s*\n\s*\*\*\s*\n\s*```[\s\n]*kwargs',
+ '`**kwargs`',
+ content
+ )
+ content = re.sub(
+ r'```\s*\n\s*\*\s*\n\s*```[\s\n]*args',
+ '`*args`',
+ content
+ )
+
+ # Handle escaped \*args and \*\*kwargs before other processing
+ content = content.replace('\\*\\*kwargs', '`**kwargs`')
+ content = content.replace('\\*args', '`*args`')
+
# First handle multi-line dictionary patterns
content = self.clean_multiline_dictionaries(content)
@@ -453,6 +754,98 @@ def clean_markdown_content(self, content: str, filename: str) -> str:
# Fix header hierarchy (Example sections should be h4 under class headers)
content = self.fix_header_hierarchy(content)
+ # Fix example code blocks that are not properly formatted
+ content = self.fix_example_blocks(content)
+
+ # Fix shell-style configuration examples that have # comments being interpreted as headers
+ # Pattern: lines like "Example configuration:" followed by KEY=VALUE and "# comment" lines
+ content = self.fix_shell_config_examples(content)
+
+ # Remove all
tags (wrapped in backticks or not)
+ content = content.replace('`
`', '')
+ content = content.replace('
', '')
+
+ # Clean up malformed code blocks with weird backtick patterns
+ # These come from Sphinx's markdown output
+ content = re.sub(r'```\s*\n``\s*\n```', '', content) # Empty weird block
+ content = re.sub(r'```\s*\n`\s*\n```', '', content) # Another weird pattern
+ content = re.sub(r'^## \}', '}', content, flags=re.MULTILINE) # Fix closing brace with header prefix
+
+ # Handle any remaining standalone code blocks with just * or ** (cleanup)
+ content = re.sub(r'\s*```\s*\n\s*\*\*\s*\n\s*```\s*', ' ', content)
+ content = re.sub(r'\s*```\s*\n\s*\*\s*\n\s*```\s*', ' ', content)
+
+ # Clean up blockquote markers that break MDX parsing
+ # Convert ' > text' to ' text' (indented blockquotes to plain indented text)
+ # Handle multiple levels of nesting like '> > text'
+ # BUT: Don't remove >>> which are Python REPL prompts!
+ # Run multiple times to handle nested blockquotes
+ prev_content = None
+ while prev_content != content:
+ prev_content = content
+ # Only match single > at start of line (not >>> or >>)
+ # Pattern: start of line, optional whitespace, single > not followed by >
+ content = re.sub(r'^(\s*)>(?!>)\s*', r'\1', content, flags=re.MULTILINE)
+
+ # Remove duplicate Example: lines after #### Example header
+ content = re.sub(r'(#### Example\n\n)Example:\n', r'\1', content)
+
+ # Remove malformed standalone backtick patterns
+ content = re.sub(r'^``\s*$', '', content, flags=re.MULTILINE)
+ content = re.sub(r'^`\s*$', '', content, flags=re.MULTILINE)
+
+ # Clean up multiple consecutive blank lines (more than 2)
+ content = re.sub(r'\n{4,}', '\n\n\n', content)
+
+ # Remove orphaned code block openers (but not closers!)
+ # Pattern: ``` opener followed by content that doesn't have a matching closing ```
+ # This handles Sphinx's broken JSON/code examples
+ # We track whether we're inside a code block to distinguish openers from closers
+ lines = content.split('\n')
+ cleaned = []
+ in_code_block = False
+ i = 0
+ while i < len(lines):
+ line = lines[i]
+
+ # Check for code block markers
+ if line.strip().startswith('```'):
+ if not in_code_block:
+ # This is an opener - check if it has a matching close
+ if line.strip() == '```':
+ # Standalone opener - look ahead for close
+ j = i + 1
+ has_close = False
+ while j < len(lines):
+ if lines[j].strip() == '```':
+ has_close = True
+ break
+ if lines[j].startswith('#'): # Hit a header - no proper close
+ break
+ j += 1
+
+ if not has_close:
+ # Skip this orphaned opener
+ i += 1
+ continue
+ # It's a valid opener (either has language or has close)
+ in_code_block = True
+ else:
+ # This is a closer
+ in_code_block = False
+
+ cleaned.append(line)
+ i += 1
+ content = '\n'.join(cleaned)
+
+ # Remove malformed Example sections that contain raw JSON/code without proper formatting
+ # These cause MDX parsing errors due to curly braces being interpreted as JSX
+ content = self.remove_malformed_examples(content)
+
+ # Add horizontal rules before class headers to ensure proper spacing
+ # This fixes the issue where h4 (method) followed by h3 (class) loses margin-top
+ content = self.add_class_separators(content)
+
lines = content.split('\n')
cleaned_lines = []
@@ -529,14 +922,11 @@ def remove_problematic_patterns(self, line: str) -> str:
# Only replace if it looks like an HTML tag: or
line = re.sub(r'<(/?\w+[^>]*)>', r'`<\1>`', line)
- # Fix Sphinx-generated blockquote markers that should be list continuations
- if line.startswith('> ') and not line.startswith('> **'):
- # This is likely a continuation of a bullet point, not a blockquote
- line = ' ' + line[2:] # Replace '> ' with proper indentation
+ # Note: Blockquote markers are now handled globally in clean_markdown_content
+ # Note: *args and **kwargs are now handled at the start of clean_markdown_content
- # Remove escaped characters that cause issues
- line = line.replace('\\*', '*')
- line = line.replace('\\', '')
+ # Remove remaining escaped backslashes, but preserve literal asterisks in code
+ line = re.sub(r'\\([^*])', r'\1', line) # Remove backslash before non-asterisks
# Fix dictionary/object literals that cause parsing issues
# Pattern: = {'key': 'value', 'key2': 'value2'} or = {}
@@ -570,37 +960,57 @@ def remove_problematic_patterns(self, line: str) -> str:
# Note: All cross-reference link conversion logic removed - we now just strip links entirely
class_to_module = {
+ # agent module
'Agent': 'agent',
'AgentBase': 'agent',
- 'AgentContext': 'agent',
+ # context module
+ 'AgentContext': 'context',
+ 'Skill': 'context',
+ 'SkillKnowledge': 'context',
+ 'BaseTrigger': 'context',
+ 'KeywordTrigger': 'context',
+ 'TaskTrigger': 'context',
+ 'SkillValidationError': 'context',
+ # conversation module
'Conversation': 'conversation',
'BaseConversation': 'conversation',
'LocalConversation': 'conversation',
'RemoteConversation': 'conversation',
'ConversationState': 'conversation',
'ConversationStats': 'conversation',
+ # event module
'Event': 'event',
'LLMConvertibleEvent': 'event',
'MessageEvent': 'event',
+ 'HookExecutionEvent': 'event',
+ # llm module
'LLM': 'llm',
'LLMRegistry': 'llm',
'LLMResponse': 'llm',
+ 'LLMProfileStore': 'llm',
+ 'LLMStreamChunk': 'llm',
+ 'FallbackStrategy': 'llm',
'Message': 'llm',
'ImageContent': 'llm',
'TextContent': 'llm',
'ThinkingBlock': 'llm',
'RedactedThinkingBlock': 'llm',
+ 'TokenUsage': 'llm',
'Metrics': 'llm',
'RegistryEvent': 'llm',
+ # security module
'SecurityManager': 'security',
+ # tool module
'Tool': 'tool',
'ToolDefinition': 'tool',
'Action': 'tool',
'Observation': 'tool',
+ # workspace module
'Workspace': 'workspace',
'BaseWorkspace': 'workspace',
'LocalWorkspace': 'workspace',
'RemoteWorkspace': 'workspace',
+ 'AsyncRemoteWorkspace': 'workspace',
'WorkspaceFile': 'workspace',
'WorkspaceFileEdit': 'workspace',
'WorkspaceFileEditResult': 'workspace',
@@ -611,6 +1021,66 @@ def remove_problematic_patterns(self, line: str) -> str:
'WorkspaceSearchResultItem': 'workspace',
'WorkspaceUploadResult': 'workspace',
'WorkspaceWriteResult': 'workspace',
+ # hooks module
+ 'HookConfig': 'hooks',
+ 'HookDefinition': 'hooks',
+ 'HookMatcher': 'hooks',
+ 'HookType': 'hooks',
+ 'HookExecutor': 'hooks',
+ 'HookResult': 'hooks',
+ 'HookManager': 'hooks',
+ 'HookEvent': 'hooks',
+ 'HookEventType': 'hooks',
+ 'HookDecision': 'hooks',
+ 'HookEventProcessor': 'hooks',
+ # critic module
+ 'CriticBase': 'critic',
+ 'CriticResult': 'critic',
+ 'IterativeRefinementConfig': 'critic',
+ 'AgentFinishedCritic': 'critic',
+ 'APIBasedCritic': 'critic',
+ 'EmptyPatchCritic': 'critic',
+ 'PassCritic': 'critic',
+ # mcp module
+ 'MCPClient': 'mcp',
+ 'MCPToolDefinition': 'mcp',
+ 'MCPToolAction': 'mcp',
+ 'MCPToolObservation': 'mcp',
+ 'MCPToolExecutor': 'mcp',
+ 'MCPError': 'mcp',
+ 'MCPTimeoutError': 'mcp',
+ # plugin module
+ 'Plugin': 'plugin',
+ 'PluginManifest': 'plugin',
+ 'PluginAuthor': 'plugin',
+ 'PluginSource': 'plugin',
+ 'ResolvedPluginSource': 'plugin',
+ 'CommandDefinition': 'plugin',
+ 'PluginFetchError': 'plugin',
+ 'Marketplace': 'plugin',
+ 'MarketplaceEntry': 'plugin',
+ 'MarketplaceOwner': 'plugin',
+ 'MarketplacePluginEntry': 'plugin',
+ 'MarketplacePluginSource': 'plugin',
+ 'MarketplaceMetadata': 'plugin',
+ 'InstalledPluginInfo': 'plugin',
+ 'InstalledPluginsMetadata': 'plugin',
+ 'GitHubURLComponents': 'plugin',
+ # subagent module
+ 'AgentDefinition': 'subagent',
+ # io module
+ 'FileStore': 'io',
+ 'LocalFileStore': 'io',
+ 'InMemoryFileStore': 'io',
+ # secret module
+ 'SecretSource': 'secret',
+ 'StaticSecret': 'secret',
+ 'LookupSecret': 'secret',
+ 'SecretValue': 'secret',
+ # skills module
+ 'InstalledSkillInfo': 'skills',
+ 'InstalledSkillsMetadata': 'skills',
+ 'SkillFetchError': 'skills',
}
# Fix anchor links - convert full module path anchors to simple class format
@@ -731,12 +1201,26 @@ def update_main_docs_json(self, nav_entries):
docs_config = json.load(f)
# Find and update the API Reference section
+ # Structure: API Reference (collapsed) > Python SDK (collapsed) > modules
updated = False
+ new_api_ref_structure = {
+ "group": "API Reference",
+ "collapsed": True,
+ "pages": [
+ {
+ "group": "Python SDK",
+ "collapsed": True,
+ "pages": nav_entries
+ }
+ ]
+ }
+
for tab in docs_config.get("navigation", {}).get("tabs", []):
if tab.get("tab") == "SDK":
- for page in tab.get("pages", []):
+ pages = tab.get("pages", [])
+ for i, page in enumerate(pages):
if isinstance(page, dict) and page.get("group") == "API Reference":
- page["pages"] = nav_entries
+ pages[i] = new_api_ref_structure
updated = True
logger.info("Updated API Reference navigation in docs.json")
break
diff --git a/scripts/mint-config-snippet.json b/scripts/mint-config-snippet.json
index 74571d27..20e89ae5 100644
--- a/scripts/mint-config-snippet.json
+++ b/scripts/mint-config-snippet.json
@@ -4,10 +4,21 @@
"group": "API Reference",
"pages": [
"sdk/api-reference/openhands.sdk.agent",
+ "sdk/api-reference/openhands.sdk.context",
"sdk/api-reference/openhands.sdk.conversation",
+ "sdk/api-reference/openhands.sdk.critic",
"sdk/api-reference/openhands.sdk.event",
+ "sdk/api-reference/openhands.sdk.hooks",
+ "sdk/api-reference/openhands.sdk.io",
"sdk/api-reference/openhands.sdk.llm",
+ "sdk/api-reference/openhands.sdk.logger",
+ "sdk/api-reference/openhands.sdk.mcp",
+ "sdk/api-reference/openhands.sdk.observability",
+ "sdk/api-reference/openhands.sdk.plugin",
+ "sdk/api-reference/openhands.sdk.secret",
"sdk/api-reference/openhands.sdk.security",
+ "sdk/api-reference/openhands.sdk.skills",
+ "sdk/api-reference/openhands.sdk.subagent",
"sdk/api-reference/openhands.sdk.tool",
"sdk/api-reference/openhands.sdk.utils",
"sdk/api-reference/openhands.sdk.workspace"
diff --git a/sdk/api-reference/openhands.sdk.agent.mdx b/sdk/api-reference/openhands.sdk.agent.mdx
index bac75f6d..7bb57a65 100644
--- a/sdk/api-reference/openhands.sdk.agent.mdx
+++ b/sdk/api-reference/openhands.sdk.agent.mdx
@@ -4,6 +4,9 @@ description: API reference for openhands.sdk.agent module
---
+
+---
+
### class Agent
Bases: `CriticMixin`, [`AgentBase`](#class-agentbase)
@@ -32,6 +35,19 @@ is provided by CriticMixin.
#### Methods
+#### get_dynamic_context()
+
+Get dynamic context for the system prompt, including secrets from state.
+
+This method pulls secrets from the conversation’s secret_registry and
+merges them with agent_context to build the dynamic portion of the
+system prompt.
+
+* Parameters:
+ `state` – The conversation state containing the secret_registry.
+* Returns:
+ The dynamic context string, or None if no context is configured.
+
#### init_state()
Initialize conversation state.
@@ -39,8 +55,8 @@ Initialize conversation state.
Invariants enforced by this method:
- If a SystemPromptEvent is already present, it must be within the first 3
- events (index 0 or 1 in practice; index 2 is included in the scan window
- to detect a user message appearing before the system prompt).
+events (index 0 or 1 in practice; index 2 is included in the scan window
+to detect a user message appearing before the system prompt).
- A user MessageEvent should not appear before the SystemPromptEvent.
These invariants keep event ordering predictable for downstream components
@@ -66,7 +82,7 @@ Typically this involves:
2. Executing the tool
3. Updating the conversation state with
- LLM calls (role=”assistant”) and tool results (role=”tool”)
+LLM calls (role=”assistant”) and tool results (role=”tool”)
4.1 If conversation is finished, set state.execution_status to FINISHED
4.2 Otherwise, just return, Conversation will kick off the next step
@@ -76,6 +92,9 @@ If the underlying LLM supports streaming, partial deltas are forwarded to
NOTE: state will be mutated in-place.
+
+---
+
### class AgentBase
Bases: `DiscriminatedUnionMixin`, `ABC`
@@ -133,6 +152,22 @@ agent implementations must follow.
#### Methods
+#### ask_agent()
+
+Optional override for stateless question answering.
+
+Subclasses (e.g. ACPAgent) may override this to provide their own
+implementation of ask_agent that bypasses the default LLM-based path.
+
+* Returns:
+ Response string, or `None` to use the default LLM-based approach.
+
+#### close()
+
+Clean up agent resources.
+
+No-op by default; ACPAgent overrides to terminate subprocess.
+
#### get_all_llms()
Recursively yield unique base-class LLM objects reachable from self.
@@ -175,7 +210,7 @@ Typically this involves:
2. Executing the tool
3. Updating the conversation state with
- LLM calls (role=”assistant”) and tool results (role=”tool”)
+LLM calls (role=”assistant”) and tool results (role=”tool”)
4.1 If conversation is finished, set state.execution_status to FINISHED
4.2 Otherwise, just return, Conversation will kick off the next step
diff --git a/sdk/api-reference/openhands.sdk.context.mdx b/sdk/api-reference/openhands.sdk.context.mdx
new file mode 100644
index 00000000..d5c9acd4
--- /dev/null
+++ b/sdk/api-reference/openhands.sdk.context.mdx
@@ -0,0 +1,305 @@
+---
+title: openhands.sdk.context
+description: API reference for openhands.sdk.context module
+---
+
+
+
+---
+
+### class AgentContext
+
+Bases: `BaseModel`
+
+Central structure for managing prompt extension.
+
+AgentContext unifies all the contextual inputs that shape how the system
+extends and interprets user prompts. It combines both static environment
+details and dynamic, user-activated extensions from skills.
+
+Specifically, it provides:
+- Repository context / Repo Skills: Information about the active codebase,
+
+branches, and repo-specific instructions contributed by repo skills.
+- Runtime context: Current execution environment (hosts, working
+ directory, secrets, date, etc.).
+- Conversation instructions: Optional task- or channel-specific rules
+ that constrain or guide the agent’s behavior across the session.
+- Knowledge Skills: Extensible components that can be triggered by user input
+ to inject knowledge or domain-specific guidance.
+
+Together, these elements make AgentContext the primary container responsible
+for assembling, formatting, and injecting all prompt-relevant context into
+LLM interactions.
+
+
+#### Properties
+
+- `current_datetime`: datetime | str | None
+- `load_public_skills`: bool
+- `load_user_skills`: bool
+- `marketplace_path`: str | None
+- `secrets`: Mapping[str, SecretValue] | None
+- `skills`: list[[Skill](#class-skill)]
+- `system_message_suffix`: str | None
+- `user_message_suffix`: str | None
+
+#### Methods
+
+#### get_formatted_datetime()
+
+Get formatted datetime string for inclusion in prompts.
+
+* Returns:
+ Formatted datetime string, or None if current_datetime is not set.
+ If current_datetime is a datetime object, it’s formatted as ISO 8601.
+ If current_datetime is already a string, it’s returned as-is.
+
+#### get_secret_infos()
+
+Get secret information (name and description) from the secrets field.
+
+* Returns:
+ List of dictionaries with ‘name’ and ‘description’ keys.
+ Returns an empty list if no secrets are configured.
+ Description will be None if not available.
+
+#### get_system_message_suffix()
+
+Get the system message with repo skill content and custom suffix.
+
+Custom suffix can typically includes:
+- Repository information (repo name, branch name, PR number, etc.)
+- Runtime information (e.g., available hosts, current date)
+- Conversation instructions (e.g., user preferences, task details)
+- Repository-specific instructions (collected from repo skills)
+- Available skills list (for AgentSkills-format and triggered skills)
+
+* Parameters:
+ * `llm_model` – Optional LLM model name for vendor-specific skill filtering.
+ * `llm_model_canonical` – Optional canonical LLM model name.
+ * `additional_secret_infos` – Optional list of additional secret info dicts
+ (with ‘name’ and ‘description’ keys) to merge with agent_context
+ secrets. Typically passed from conversation’s secret_registry.
+
+Skill categorization:
+- AgentSkills-format (SKILL.md): Always in `` (progressive
+
+disclosure). If has triggers, content is ALSO auto-injected on trigger
+in user prompts.
+- Legacy with trigger=None: Full content in `` (always active)
+- Legacy with triggers: Listed in ``, injected on trigger
+
+#### get_user_message_suffix()
+
+Augment the user’s message with knowledge recalled from skills.
+
+This works by:
+- Extracting the text content of the user message
+- Matching skill triggers against the query
+- Returning formatted knowledge and triggered skill names if relevant skills were triggered
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+
+---
+
+### class BaseTrigger
+
+Bases: `BaseModel`, `ABC`
+
+Base class for all trigger types.
+
+#### Methods
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+
+---
+
+### class KeywordTrigger
+
+Bases: [`BaseTrigger`](#class-basetrigger)
+
+Trigger for keyword-based skills.
+
+These skills are activated when specific keywords appear in the user’s query.
+
+
+#### Properties
+
+- `keywords`: list[str]
+- `type`: Literal['keyword']
+
+#### Methods
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+
+---
+
+### class Skill
+
+Bases: `BaseModel`
+
+A skill provides specialized knowledge or functionality.
+
+Skill behavior depends on format (is_agentskills_format) and trigger:
+
+AgentSkills format (SKILL.md files):
+- Always listed in `` with name, description, location
+- Agent reads full content on demand (progressive disclosure)
+- If has triggers: content is ALSO auto-injected when triggered
+
+Legacy OpenHands format:
+- With triggers: Listed in ``, content injected on trigger
+- Without triggers (None): Full content in ``, always active
+
+This model supports both OpenHands-specific fields and AgentSkills standard
+fields ([https://agentskills.io/specification](https://agentskills.io/specification)) for cross-platform compatibility.
+
+
+#### Properties
+
+- `MAX_DESCRIPTION_LENGTH`: ClassVar[int] = 1024
+- `PATH_TO_THIRD_PARTY_SKILL_NAME`: ClassVar[dict[str, str]] = (configuration object)
+- `allowed_tools`: list[str] | None
+- `compatibility`: str | None
+- `content`: str
+- `description`: str | None
+- `inputs`: list[InputMetadata]
+- `is_agentskills_format`: bool
+- `license`: str | None
+- `mcp_tools`: dict | None
+- `metadata`: dict[str, str] | None
+- `name`: str
+- `resources`: SkillResources | None
+- `source`: str | None
+- `trigger`: Annotated[[KeywordTrigger](#class-keywordtrigger) | [TaskTrigger](#class-tasktrigger), FieldInfo(annotation=NoneType, required=True, discriminator='type')] | None
+
+#### Methods
+
+#### extract_variables()
+
+Extract variables from the content.
+
+Variables are in the format (variable).
+
+#### get_skill_type()
+
+Determine the type of this skill.
+
+* Returns:
+ “agentskills” for AgentSkills format, “repo” for always-active skills,
+ “knowledge” for trigger-based skills.
+
+#### get_triggers()
+
+Extract trigger keywords from this skill.
+
+* Returns:
+ List of trigger strings, or empty list if no triggers.
+
+#### classmethod load()
+
+Load a skill from a markdown file with frontmatter.
+
+The agent’s name is derived from its path relative to skill_base_dir,
+or from the directory name for AgentSkills-style SKILL.md files.
+
+Supports both OpenHands-specific frontmatter fields and AgentSkills
+standard fields ([https://agentskills.io/specification](https://agentskills.io/specification)).
+
+* Parameters:
+ * `path` – Path to the skill file.
+ * `skill_base_dir` – Base directory for skills (used to derive relative names).
+ * `strict` – If True, enforce strict AgentSkills name validation.
+ If False, allow relaxed naming (e.g., for plugin compatibility).
+
+#### match_trigger()
+
+Match a trigger in the message.
+
+Returns the first trigger that matches the message, or None if no match.
+Only applies to KeywordTrigger and TaskTrigger types.
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+#### model_post_init()
+
+This function is meant to behave like a BaseModel method to initialise private attributes.
+
+It takes context as an argument since that’s what pydantic-core passes when calling it.
+
+* Parameters:
+ * `self` – The BaseModel instance.
+ * `context` – The context.
+
+#### requires_user_input()
+
+Check if this skill requires user input.
+
+Returns True if the content contains variables in the format (variable).
+
+#### to_skill_info()
+
+Convert this skill to a SkillInfo.
+
+* Returns:
+ SkillInfo containing the skill’s essential information.
+
+
+---
+
+### class SkillKnowledge
+
+Bases: `BaseModel`
+
+Represents knowledge from a triggered skill.
+
+
+#### Properties
+
+- `content`: str
+- `location`: str | None
+- `name`: str
+- `trigger`: str
+
+#### Methods
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+#### __init__()
+
+
+---
+
+### class TaskTrigger
+
+Bases: [`BaseTrigger`](#class-basetrigger)
+
+Trigger for task-specific skills.
+
+These skills are activated for specific task types and can modify prompts.
+
+
+#### Properties
+
+- `triggers`: list[str]
+- `type`: Literal['task']
+
+#### Methods
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
\ No newline at end of file
diff --git a/sdk/api-reference/openhands.sdk.conversation.mdx b/sdk/api-reference/openhands.sdk.conversation.mdx
index b5e99911..ea6a80e8 100644
--- a/sdk/api-reference/openhands.sdk.conversation.mdx
+++ b/sdk/api-reference/openhands.sdk.conversation.mdx
@@ -4,6 +4,9 @@ description: API reference for openhands.sdk.conversation module
---
+
+---
+
### class BaseConversation
Bases: `ABC`
@@ -158,8 +161,14 @@ Set the security analyzer for the conversation.
#### abstractmethod update_secrets()
+
+---
+
### class Conversation
+
+---
+
### class Conversation
Bases: `object`
@@ -190,6 +199,9 @@ while RemoteConversation connects to a remote agent server.
>>> conversation.run()
```
+
+---
+
### class ConversationExecutionStatus
Bases: `str`, `Enum`
@@ -228,6 +240,9 @@ the WebSocket delivers the initial state update during connection.
* Returns:
True if this is a terminal status, False otherwise.
+
+---
+
### class ConversationState
Bases: `OpenHandsModel`
@@ -245,7 +260,9 @@ Bases: `OpenHandsModel`
Directory for persisting environment observation files.
- `events`: [EventLog](#class-eventlog)
- `execution_status`: [ConversationExecutionStatus](#class-conversationexecutionstatus)
+- `hook_config`: HookConfig | None
- `id`: UUID
+- `last_user_message_id`: str | None
- `max_iterations`: int
- `persistence_dir`: str | None
- `secret_registry`: [SecretRegistry](#class-secretregistry)
@@ -315,8 +332,12 @@ agent_context, condenser, system prompts, etc.
Find actions in the event history that don’t have matching observations.
This method identifies ActionEvents that don’t have corresponding
-ObservationEvents or UserRejectObservations, which typically indicates
-actions that are pending confirmation or execution.
+ObservationEvents, UserRejectObservations, or AgentErrorEvents,
+which typically indicates actions that are pending confirmation or execution.
+
+Note: AgentErrorEvent is matched by tool_call_id (not action_id) because
+it doesn’t have an action_id field. This is important for crash recovery
+scenarios where an error event is emitted after a server restart.
* Parameters:
`events` – List of events to search through
@@ -369,6 +390,9 @@ Set a callback to be called when state changes.
`callback` – A function that takes an Event (ConversationStateUpdateEvent)
or None to remove the callback
+
+---
+
### class ConversationVisualizerBase
Bases: `ABC`
@@ -382,7 +406,7 @@ and will be configured with the conversation state automatically.
The typical usage pattern:
1. Create a visualizer instance:
- viz = MyVisualizer()
+viz = MyVisualizer()
1. Pass it to Conversation: conv = Conversation(agent, visualizer=viz)
2. Conversation automatically calls viz.initialize(state) to attach the state
@@ -445,6 +469,9 @@ implement the visualization logic.
* Parameters:
`event` – The event to visualize
+
+---
+
### class DefaultConversationVisualizer
Bases: [`ConversationVisualizerBase`](#class-conversationvisualizerbase)
@@ -470,6 +497,9 @@ Initialize the visualizer.
Main event handler that displays events with Rich formatting.
+
+---
+
### class EventLog
Bases: [`EventsListBase`](#class-eventslistbase)
@@ -505,6 +535,9 @@ Return the event_id for a given index.
Return the integer index for a given event_id.
+
+---
+
### class EventsListBase
Bases: `Sequence`[`Event`], `ABC`
@@ -520,6 +553,9 @@ RemoteEventsList implementations, avoiding circular imports in protocols.
Add a new event to the list.
+
+---
+
### class LocalConversation
Bases: [`BaseConversation`](#class-baseconversation)
@@ -578,7 +614,7 @@ Initialize the conversation.
Visualization configuration. Can be:
- ConversationVisualizerBase subclass: Class to instantiate
- > (default: ConversationVisualizer)
+ (default: ConversationVisualizer)
- ConversationVisualizerBase instance: Use custom visualizer
- None: No visualization
* `stuck_detection` – Whether to enable stuck detection
@@ -679,6 +715,38 @@ Reject all pending actions from the agent.
This is a non-invasive method to reject actions between run() calls.
Also clears the agent_waiting_for_confirmation flag.
+#### rerun_actions()
+
+Re-execute all actions from the conversation’s event history.
+
+This method iterates through all ActionEvents in the conversation and
+re-executes them using their original action parameters. Execution
+stops immediately if any tool call fails.
+
+WARNING: This is an advanced feature intended for specific use cases
+such as reproducing environment state from a saved conversation. Many
+tool operations are NOT idempotent:
+
+- File operations may fail if files already exist or were deleted
+- Terminal commands may have different effects on changed state
+- API calls may have side effects or return different results
+- Browser state may differ from the original session
+
+Use this method only when you understand that:
+1. Results may differ from the original conversation
+2. Some actions may fail due to changed environment state
+3. The workspace should typically be reset before rerunning
+
+* Parameters:
+ `rerun_log_path` – Optional directory path to save a rerun event log.
+ If provided, events will be written incrementally to disk using
+ EventLog, avoiding memory buildup for large conversations.
+* Returns:
+ True if all actions executed successfully, False if any action failed.
+* Raises:
+ `KeyError` – If a tool from the original conversation is not available.
+ This is a configuration error (different from execution failure).
+
#### run()
Runs the conversation until the agent finishes.
@@ -712,15 +780,38 @@ Set the confirmation policy and store it in conversation state.
Set the security analyzer for the conversation.
+#### switch_profile()
+
+Switch the agent’s LLM to a named profile.
+
+Loads the profile from the LLMProfileStore (cached in the registry
+after the first load) and updates the agent and conversation state.
+
+* Parameters:
+ `profile_name` – Name of a profile previously saved via LLMProfileStore.
+* Raises:
+ * `FileNotFoundError` – If the profile does not exist.
+ * `ValueError` – If the profile is corrupted or invalid.
+
#### update_secrets()
-Add secrets to the conversation.
+Add secrets to the conversation’s secret registry.
+
+Secrets are stored in the conversation’s secret_registry which:
+1. Provides environment variable injection during command execution
+2. Is read by the agent when building its system prompt (dynamic_context)
+
+The agent pulls secrets from the registry via get_dynamic_context() during
+init_state(), ensuring secret names and descriptions appear in the prompt.
* Parameters:
`secrets` – Dictionary mapping secret keys to values or no-arg callables.
SecretValue = str | Callable[[], str]. Callables are invoked lazily
when a command references the secret key.
+
+---
+
### class RemoteConversation
Bases: [`BaseConversation`](#class-baseconversation)
@@ -756,12 +847,13 @@ Remote conversation proxy that talks to an agent server.
a dict with keys: ‘action_observation’, ‘action_error’,
‘monologue’, ‘alternating_pattern’. Values are integers
representing the number of repetitions before triggering.
- * `hook_config` – Optional hook configuration for session hooks
+ * `hook_config` – Optional hook configuration sent to the server.
+ All hooks are executed server-side.
* `visualizer` –
Visualization configuration. Can be:
- ConversationVisualizerBase subclass: Class to instantiate
- > (default: ConversationVisualizer)
+ (default: ConversationVisualizer)
- ConversationVisualizerBase instance: Use custom visualizer
- None: No visualization
* `secrets` – Optional secrets to initialize the conversation with
@@ -876,6 +968,9 @@ Not implemented for remote conversations.
#### update_secrets()
+
+---
+
### class SecretRegistry
Bases: `OpenHandsModel`
@@ -899,7 +994,7 @@ even when callable secrets fail on subsequent calls.
#### Properties
-- `secret_sources`: dict[str, SecretSource]
+- `secret_sources`: dictstr, [SecretSource]
#### Methods
@@ -912,6 +1007,15 @@ Find all secret keys mentioned in the given text.
* Returns:
Set of secret keys found in the text
+#### get_secret_infos()
+
+Get secret information (name and description) for prompt inclusion.
+
+* Returns:
+ List of dictionaries with ‘name’ and ‘description’ keys.
+ Returns an empty list if no secrets are registered.
+ Description will be None if not available.
+
#### get_secrets_as_env_vars()
Get secrets that should be exported as environment variables for a command.
@@ -955,6 +1059,9 @@ Add or update secrets in the manager.
`secrets` – Dictionary mapping secret keys to either string values
or callable functions that return string values
+
+---
+
### class StuckDetector
Bases: `object`
diff --git a/sdk/api-reference/openhands.sdk.critic.mdx b/sdk/api-reference/openhands.sdk.critic.mdx
new file mode 100644
index 00000000..6d406f71
--- /dev/null
+++ b/sdk/api-reference/openhands.sdk.critic.mdx
@@ -0,0 +1,242 @@
+---
+title: openhands.sdk.critic
+description: API reference for openhands.sdk.critic module
+---
+
+
+
+---
+
+### class APIBasedCritic
+
+Bases: [`CriticBase`](#class-criticbase), `CriticClient`
+
+
+#### Properties
+
+- `model_config`: = (configuration object)
+ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+#### Methods
+
+#### evaluate()
+
+#### get_followup_prompt()
+
+Generate a detailed follow-up prompt with rubrics predictions.
+
+This override provides more detailed feedback than the base class,
+including all categorized features (agent behavioral issues,
+user follow-up patterns, infrastructure issues) with their probabilities.
+
+* Parameters:
+ * `critic_result` – The critic result from the previous iteration.
+ * `iteration` – The current iteration number (1-indexed).
+* Returns:
+ A detailed follow-up prompt string with rubrics predictions.
+
+#### model_post_init()
+
+This function is meant to behave like a BaseModel method to initialise private attributes.
+
+It takes context as an argument since that’s what pydantic-core passes when calling it.
+
+* Parameters:
+ * `self` – The BaseModel instance.
+ * `context` – The context.
+
+
+---
+
+### class AgentFinishedCritic
+
+Bases: [`CriticBase`](#class-criticbase)
+
+Critic that evaluates whether an agent properly finished a task.
+
+This critic checks two main criteria:
+1. The agent’s last action was a FinishAction (proper completion)
+2. The generated git patch is non-empty (actual changes were made)
+
+#### Methods
+
+#### evaluate()
+
+Evaluate if an agent properly finished with a non-empty git patch.
+
+* Parameters:
+ * `events` – List of events from the agent’s execution
+ * `git_patch` – Optional git patch generated by the agent
+* Returns:
+ CriticResult with score 1.0 if successful, 0.0 otherwise
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+
+---
+
+### class CriticBase
+
+Bases: `DiscriminatedUnionMixin`, `ABC`
+
+A critic is a function that takes in a list of events,
+optional git patch, and returns a score about the quality of agent’s action.
+
+
+#### Properties
+
+- `iterative_refinement`: [IterativeRefinementConfig](#class-iterativerefinementconfig) | None
+- `mode`: Literal['finish_and_message', 'all_actions']
+
+#### Methods
+
+#### abstractmethod evaluate()
+
+#### get_followup_prompt()
+
+Generate a follow-up prompt for iterative refinement.
+
+Subclasses can override this method to provide custom follow-up prompts.
+
+* Parameters:
+ * `critic_result` – The critic result from the previous iteration.
+ * `iteration` – The current iteration number (1-indexed).
+* Returns:
+ A follow-up prompt string to send to the agent.
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+
+---
+
+### class CriticResult
+
+Bases: `BaseModel`
+
+A critic result is a score and a message.
+
+
+#### Properties
+
+- `DISPLAY_THRESHOLD`: ClassVar[float] = 0.2
+- `THRESHOLD`: ClassVar[float] = 0.5
+- `message`: str | None
+- `metadata`: dict[str, Any] | None
+- `score`: float
+- `success`: bool
+ Whether the agent is successful.
+- `visualize`: Text
+ Return Rich Text representation of the critic result.
+
+#### Methods
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+
+---
+
+### class EmptyPatchCritic
+
+Bases: [`CriticBase`](#class-criticbase)
+
+Critic that only evaluates whether a git patch is non-empty.
+
+This critic checks only one criterion:
+- The generated git patch is non-empty (actual changes were made)
+
+Unlike AgentFinishedCritic, this critic does not check for proper
+agent completion with FinishAction.
+
+#### Methods
+
+#### evaluate()
+
+Evaluate if a git patch is non-empty.
+
+* Parameters:
+ * `events` – List of events from the agent’s execution (not used)
+ * `git_patch` – Optional git patch generated by the agent
+* Returns:
+ CriticResult with score 1.0 if patch is non-empty, 0.0 otherwise
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+
+---
+
+### class IterativeRefinementConfig
+
+Bases: `BaseModel`
+
+Configuration for iterative refinement based on critic feedback.
+When attached to a CriticBase, the Conversation.run() method will
+automatically retry the task if the critic score is below the threshold.
+
+#### Example
+
+```python
+critic = APIBasedCritic(
+server_url=”…”,
+ api_key=”…”,
+ model_name=”critic”,
+ iterative_refinement=IterativeRefinementConfig(
+
+success_threshold=0.7,
+max_iterations=3,
+
+ ),
+
+)
+agent = Agent(llm=llm, tools=tools, critic=critic)
+conversation = Conversation(agent=agent, workspace=workspace)
+conversation.send_message(“Create a calculator module…”)
+conversation.run() # Will automatically retry if critic score < 0.7
+```
+
+
+#### Properties
+
+- `max_iterations`: int
+- `success_threshold`: float
+
+#### Methods
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+
+---
+
+### class PassCritic
+
+Bases: [`CriticBase`](#class-criticbase)
+
+Critic that always returns success.
+
+This critic can be used when no evaluation is needed or when
+all instances should be considered successful regardless of their output.
+
+#### Methods
+
+#### evaluate()
+
+Always evaluate as successful.
+
+* Parameters:
+ * `events` – List of events from the agent’s execution (not used)
+ * `git_patch` – Optional git patch generated by the agent (not used)
+* Returns:
+ CriticResult with score 1.0 (always successful)
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
\ No newline at end of file
diff --git a/sdk/api-reference/openhands.sdk.event.mdx b/sdk/api-reference/openhands.sdk.event.mdx
index cc63ecda..dbe22f44 100644
--- a/sdk/api-reference/openhands.sdk.event.mdx
+++ b/sdk/api-reference/openhands.sdk.event.mdx
@@ -4,6 +4,40 @@ description: API reference for openhands.sdk.event module
---
+
+---
+
+### class ACPToolCallEvent
+
+Bases: [`Event`](#class-event)
+
+Event representing a tool call executed by an ACP server.
+
+Captures the tool name, inputs, outputs, and status from ACP
+`ToolCallStart` / `ToolCallProgress` notifications so they can
+be surfaced in the OpenHands event stream and visualizer.
+
+This is not an `LLMConvertibleEvent` — ACP tool calls do not
+participate in LLM message conversion.
+
+
+#### Properties
+
+- `is_error`: bool
+- `model_config`: = (configuration object)
+ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+- `raw_input`: Any | None
+- `raw_output`: Any | None
+- `source`: SourceType
+- `status`: str | None
+- `title`: str
+- `tool_call_id`: str
+- `tool_kind`: str | None
+- `visualize`: Text
+ Return Rich Text representation of this tool call event.
+
+---
+
### class ActionEvent
Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent)
@@ -19,7 +53,7 @@ Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent)
- `reasoning_content`: str | None
- `responses_reasoning_item`: ReasoningItemModel | None
- `security_risk`: SecurityRisk
-- `source`: Literal['agent', 'user', 'environment']
+- `source`: Literal['agent', 'user', 'environment', 'hook']
- `summary`: str | None
- `thinking_blocks`: list[ThinkingBlock | RedactedThinkingBlock]
- `thought`: Sequence[TextContent]
@@ -35,6 +69,9 @@ Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent)
Individual message - may be incomplete for multi-action batches
+
+---
+
### class AgentErrorEvent
Bases: [`ObservationBaseEvent`](#class-observationbaseevent)
@@ -50,7 +87,7 @@ represents an error produced by the agent/scaffold, not model output.
- `error`: str
- `model_config`: = (configuration object)
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-- `source`: Literal['agent', 'user', 'environment']
+- `source`: Literal['agent', 'user', 'environment', 'hook']
- `visualize`: Text
Return Rich Text representation of this agent error event.
@@ -58,6 +95,9 @@ represents an error produced by the agent/scaffold, not model output.
#### to_llm_message()
+
+---
+
### class Condensation
Bases: [`Event`](#class-event)
@@ -99,6 +139,9 @@ list of events. If the summary metadata is present (both summary and offset),
the corresponding CondensationSummaryEvent will be inserted at the specified
offset _after_ the forgotten events have been removed.
+
+---
+
### class CondensationRequest
Bases: [`Event`](#class-event)
@@ -125,6 +168,9 @@ The action type, namely ActionType.CONDENSATION_REQUEST.
* Type:
str
+
+---
+
### class CondensationSummaryEvent
Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent)
@@ -144,6 +190,9 @@ This event represents a summary generated by a condenser.
#### to_llm_message()
+
+---
+
### class ConversationStateUpdateEvent
Bases: [`Event`](#class-event)
@@ -162,7 +211,7 @@ to ensure compatibility with websocket transmission.
- `key`: str
- `model_config`: = (configuration object)
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-- `source`: Literal['agent', 'user', 'environment']
+- `source`: Literal['agent', 'user', 'environment', 'hook']
- `value`: Any
#### Methods
@@ -183,6 +232,9 @@ This creates an event containing a snapshot of important state fields.
#### classmethod validate_value()
+
+---
+
### class Event
Bases: `DiscriminatedUnionMixin`, `ABC`
@@ -195,12 +247,54 @@ Base class for all events.
- `id`: str
- `model_config`: ClassVar[ConfigDict] = (configuration object)
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-- `source`: Literal['agent', 'user', 'environment']
+- `source`: Literal['agent', 'user', 'environment', 'hook']
- `timestamp`: str
- `visualize`: Text
Return Rich Text representation of this event.
This is a fallback implementation for unknown event types.
Subclasses should override this method to provide specific visualization.
+
+---
+
+### class HookExecutionEvent
+
+Bases: [`Event`](#class-event)
+
+Event emitted when a hook is executed.
+
+This event provides observability into hook execution, including:
+- Which hook type was triggered
+- The command that was run
+- The result (success/blocked/error)
+- Any output from the hook
+
+This allows clients to track hook execution via the event stream.
+
+
+#### Properties
+
+- `action_id`: str | None
+- `additional_context`: str | None
+- `blocked`: bool
+- `error`: str | None
+- `exit_code`: int
+- `hook_command`: str
+- `hook_event_type`: Literal['PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'SessionStart', 'SessionEnd', 'Stop']
+- `hook_input`: dict[str, Any] | None
+- `message_id`: str | None
+- `model_config`: = (configuration object)
+ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+- `reason`: str | None
+- `source`: Literal['agent', 'user', 'environment', 'hook']
+- `stderr`: str
+- `stdout`: str
+- `success`: bool
+- `tool_name`: str | None
+- `visualize`: Text
+ Return Rich Text representation of this hook execution event.
+
+---
+
### class LLMCompletionLogEvent
Bases: [`Event`](#class-event)
@@ -219,8 +313,11 @@ instead of writing it to a file inside the Docker container.
- `model_config`: = (configuration object)
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- `model_name`: str
-- `source`: Literal['agent', 'user', 'environment']
+- `source`: Literal['agent', 'user', 'environment', 'hook']
- `usage_id`: str
+
+---
+
### class LLMConvertibleEvent
Bases: [`Event`](#class-event), `ABC`
@@ -241,6 +338,9 @@ Convert event stream to LLM message stream, handling multi-action batches
#### abstractmethod to_llm_message()
+
+---
+
### class MessageEvent
Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent)
@@ -261,7 +361,7 @@ This is originally the “MessageAction”, but it suppose not to be tool call.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- `reasoning_content`: str
- `sender`: str | None
-- `source`: Literal['agent', 'user', 'environment']
+- `source`: Literal['agent', 'user', 'environment', 'hook']
- `thinking_blocks`: Sequence[ThinkingBlock | RedactedThinkingBlock]
Return the Anthropic thinking blocks from the LLM message.
- `visualize`: Text
@@ -271,6 +371,9 @@ This is originally the “MessageAction”, but it suppose not to be tool call.
#### to_llm_message()
+
+---
+
### class ObservationBaseEvent
Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent)
@@ -284,9 +387,12 @@ Examples include tool execution, error, user reject.
- `model_config`: = (configuration object)
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-- `source`: Literal['agent', 'user', 'environment']
+- `source`: Literal['agent', 'user', 'environment', 'hook']
- `tool_call_id`: str
- `tool_name`: str
+
+---
+
### class ObservationEvent
Bases: [`ObservationBaseEvent`](#class-observationbaseevent)
@@ -305,6 +411,9 @@ Bases: [`ObservationBaseEvent`](#class-observationbaseevent)
#### to_llm_message()
+
+---
+
### class PauseEvent
Bases: [`Event`](#class-event)
@@ -316,9 +425,12 @@ Event indicating that the agent execution was paused by user request.
- `model_config`: = (configuration object)
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-- `source`: Literal['agent', 'user', 'environment']
+- `source`: Literal['agent', 'user', 'environment', 'hook']
- `visualize`: Text
Return Rich Text representation of this pause event.
+
+---
+
### class SystemPromptEvent
Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent)
@@ -338,7 +450,7 @@ when appropriate.
- `dynamic_context`: TextContent | None
- `model_config`: = (configuration object)
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-- `source`: Literal['agent', 'user', 'environment']
+- `source`: Literal['agent', 'user', 'environment', 'hook']
- `system_prompt`: TextContent
- `tools`: list[ToolDefinition]
- `visualize`: Text
@@ -378,6 +490,9 @@ are NOT applied here - they are applied by `LLM._apply_prompt_caching()`
when caching is enabled, which marks the static block (index 0) and leaves
the dynamic block (index 1) unmarked for cross-conversation cache sharing.
+
+---
+
### class TokenEvent
Bases: [`Event`](#class-event)
@@ -391,7 +506,10 @@ Event from VLLM representing token IDs used in LLM interaction.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- `prompt_token_ids`: list[int]
- `response_token_ids`: list[int]
-- `source`: Literal['agent', 'user', 'environment']
+- `source`: Literal['agent', 'user', 'environment', 'hook']
+
+---
+
### class UserRejectObservation
Bases: [`ObservationBaseEvent`](#class-observationbaseevent)
diff --git a/sdk/api-reference/openhands.sdk.hooks.mdx b/sdk/api-reference/openhands.sdk.hooks.mdx
new file mode 100644
index 00000000..2fcc7c62
--- /dev/null
+++ b/sdk/api-reference/openhands.sdk.hooks.mdx
@@ -0,0 +1,380 @@
+---
+title: openhands.sdk.hooks
+description: API reference for openhands.sdk.hooks module
+---
+
+
+OpenHands Hooks System - Event-driven hooks for automation and control.
+
+Hooks are event-driven scripts that execute at specific lifecycle events
+during agent execution, enabling deterministic control over agent behavior.
+
+
+---
+
+### class HookConfig
+
+Bases: `BaseModel`
+
+Configuration for all hooks.
+Hooks can be configured either by loading from .openhands/hooks.json or
+by directly instantiating with typed fields:
+
+# Direct instantiation with typed fields()
+config = HookConfig(
+
+pre_tool_use=[
+: HookMatcher(
+: matcher=”terminal”,
+hooks=[HookDefinition(command=”block_dangerous.sh”)]
+)
+
+]
+
+)
+
+# Load from JSON file:
+config = HookConfig.load(“.openhands/hooks.json”)
+
+
+#### Properties
+
+- `model_config`: = (configuration object)
+ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+- `post_tool_use`: list[[HookMatcher](#class-hookmatcher)]
+- `pre_tool_use`: list[[HookMatcher](#class-hookmatcher)]
+- `session_end`: list[[HookMatcher](#class-hookmatcher)]
+- `session_start`: list[[HookMatcher](#class-hookmatcher)]
+- `stop`: list[[HookMatcher](#class-hookmatcher)]
+- `user_prompt_submit`: list[[HookMatcher](#class-hookmatcher)]
+
+#### Methods
+
+#### classmethod from_dict()
+
+Create HookConfig from a dictionary.
+
+Supports both legacy format with “hooks” wrapper and direct format:
+: # Legacy format:
+(JSON configuration object)
+
+ # Direct format:
+(JSON configuration object)
+
+#### get_hooks_for_event()
+
+Get all hooks that should run for an event.
+
+#### has_hooks_for_event()
+
+Check if there are any hooks configured for an event type.
+
+#### is_empty()
+
+Check if this config has no hooks configured.
+
+#### classmethod load()
+
+Load config from path or search .openhands/hooks.json locations.
+
+* Parameters:
+ * `path` – Explicit path to hooks.json file. If provided, working_dir is ignored.
+ * `working_dir` – Project directory for discovering .openhands/hooks.json.
+ Falls back to cwd if not provided.
+
+#### classmethod merge()
+
+Merge multiple hook configs by concatenating handlers per event type.
+
+Each hook config may have multiple event types (pre_tool_use,
+post_tool_use, etc.). This method combines all matchers from all
+configs for each event type.
+
+* Parameters:
+ `configs` – List of HookConfig objects to merge.
+* Returns:
+ A merged HookConfig with all matchers concatenated, or None if no configs
+ or if the result is empty.
+
+#### save()
+
+Save hook configuration to a JSON file using snake_case field names.
+
+
+---
+
+### class HookDecision
+
+Bases: `str`, `Enum`
+
+Decisions a hook can make about an operation.
+
+#### Methods
+
+#### ALLOW = 'allow'
+
+#### DENY = 'deny'
+
+
+---
+
+### class HookDefinition
+
+Bases: `BaseModel`
+
+A single hook definition.
+
+
+#### Properties
+
+- `async_`: bool
+- `command`: str
+- `model_config`: = (configuration object)
+ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+- `timeout`: int
+- `type`: [HookType](#class-hooktype)
+
+---
+
+### class HookEvent
+
+Bases: `BaseModel`
+
+Data passed to hook scripts via stdin as JSON.
+
+
+#### Properties
+
+- `event_type`: [HookEventType](#class-hookeventtype)
+- `message`: str | None
+- `metadata`: dict[str, Any]
+- `model_config`: = (configuration object)
+ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+- `session_id`: str | None
+- `tool_input`: dict[str, Any] | None
+- `tool_name`: str | None
+- `tool_response`: dict[str, Any] | None
+- `working_dir`: str | None
+
+---
+
+### class HookEventProcessor
+
+Bases: `object`
+
+Processes events and runs hooks at appropriate points.
+
+Call set_conversation_state() after creating Conversation for blocking to work.
+
+HookExecutionEvent is emitted for each hook execution when emit_hook_events=True,
+providing full observability into hook execution for clients.
+
+#### Methods
+
+#### __init__()
+
+#### is_action_blocked()
+
+Check if an action was blocked by a hook.
+
+#### is_message_blocked()
+
+Check if a message was blocked by a hook.
+
+#### on_event()
+
+Process an event and run appropriate hooks.
+
+#### run_session_end()
+
+Run SessionEnd hooks. Call before conversation is closed.
+
+#### run_session_start()
+
+Run SessionStart hooks. Call after conversation is created.
+
+#### run_stop()
+
+Run Stop hooks. Returns (should_stop, feedback).
+
+#### set_conversation_state()
+
+Set conversation state for blocking support.
+
+
+---
+
+### class HookEventType
+
+Bases: `str`, `Enum`
+
+Types of hook events that can trigger hooks.
+
+#### Methods
+
+#### POST_TOOL_USE = 'PostToolUse'
+
+#### PRE_TOOL_USE = 'PreToolUse'
+
+#### SESSION_END = 'SessionEnd'
+
+#### SESSION_START = 'SessionStart'
+
+#### STOP = 'Stop'
+
+#### USER_PROMPT_SUBMIT = 'UserPromptSubmit'
+
+
+---
+
+### class HookExecutor
+
+Bases: `object`
+
+Executes hook commands with JSON I/O.
+
+#### Methods
+
+#### __init__()
+
+#### execute()
+
+Execute a single hook.
+
+#### execute_all()
+
+Execute multiple hooks in order, optionally stopping on block.
+
+
+---
+
+### class HookManager
+
+Bases: `object`
+
+Manages hook execution for a conversation.
+
+#### Methods
+
+#### __init__()
+
+#### cleanup_async_processes()
+
+Cleanup all background hook processes.
+
+#### get_blocking_reason()
+
+Get the reason for blocking from hook results.
+
+#### has_hooks()
+
+Check if there are hooks configured for an event type.
+
+#### run_post_tool_use()
+
+Run PostToolUse hooks after a tool completes.
+
+#### run_pre_tool_use()
+
+Run PreToolUse hooks. Returns (should_continue, results).
+
+#### run_session_end()
+
+Run SessionEnd hooks when a conversation ends.
+
+#### run_session_start()
+
+Run SessionStart hooks when a conversation begins.
+
+#### run_stop()
+
+Run Stop hooks. Returns (should_stop, results).
+
+#### run_user_prompt_submit()
+
+Run UserPromptSubmit hooks.
+
+
+---
+
+### class HookMatcher
+
+Bases: `BaseModel`
+
+Matches events to hooks based on patterns.
+
+Supports exact match, wildcard (\*), and regex (auto-detected or /pattern/).
+
+
+#### Properties
+
+- `hooks`: list[[HookDefinition](#class-hookdefinition)]
+- `matcher`: str
+
+#### Methods
+
+#### matches()
+
+Check if this matcher matches the given tool name.
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+#### model_post_init()
+
+This function is meant to behave like a BaseModel method to initialise private attributes.
+
+It takes context as an argument since that’s what pydantic-core passes when calling it.
+
+* Parameters:
+ * `self` – The BaseModel instance.
+ * `context` – The context.
+
+
+---
+
+### class HookResult
+
+Bases: `BaseModel`
+
+Result from executing a hook.
+
+Exit code 0 = success, exit code 2 = block operation.
+
+
+#### Properties
+
+- `additional_context`: str | None
+- `async_started`: bool
+- `blocked`: bool
+- `decision`: [HookDecision](#class-hookdecision) | None
+- `error`: str | None
+- `exit_code`: int
+- `reason`: str | None
+- `should_continue`: bool
+ Whether the operation should continue after this hook.
+- `stderr`: str
+- `stdout`: str
+- `success`: bool
+
+#### Methods
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+
+---
+
+### class HookType
+
+Bases: `str`, `Enum`
+
+Types of hooks that can be executed.
+
+#### Methods
+
+#### COMMAND = 'command'
+
+#### PROMPT = 'prompt'
diff --git a/sdk/api-reference/openhands.sdk.io.mdx b/sdk/api-reference/openhands.sdk.io.mdx
new file mode 100644
index 00000000..3175a549
--- /dev/null
+++ b/sdk/api-reference/openhands.sdk.io.mdx
@@ -0,0 +1,229 @@
+---
+title: openhands.sdk.io
+description: API reference for openhands.sdk.io module
+---
+
+
+
+---
+
+### class FileStore
+
+Bases: `ABC`
+
+Abstract base class for file storage operations.
+
+This class defines the interface for file storage backends that can
+handle basic file operations like reading, writing, listing, and deleting files.
+
+Implementations should provide a locking mechanism via the lock() context
+manager for thread/process-safe operations.
+
+#### Methods
+
+#### abstractmethod delete()
+
+Delete the file or directory at the specified path.
+
+* Parameters:
+ `path` – The file or directory path to delete.
+
+#### abstractmethod exists()
+
+Check if a file or directory exists at the specified path.
+
+* Parameters:
+ `path` – The file or directory path to check.
+* Returns:
+ True if the path exists, False otherwise.
+
+#### abstractmethod get_absolute_path()
+
+Get the absolute filesystem path for a given relative path.
+
+* Parameters:
+ `path` – The relative path within the file store.
+* Returns:
+ The absolute path on the filesystem.
+
+#### abstractmethod list()
+
+List all files and directories at the specified path.
+
+* Parameters:
+ `path` – The directory path to list contents from.
+* Returns:
+ A list of file and directory names in the specified path.
+
+#### abstractmethod lock()
+
+Acquire an exclusive lock for the given path.
+
+This context manager provides thread and process-safe locking.
+Implementations may use file-based locking, threading locks, or
+other mechanisms as appropriate.
+
+* Parameters:
+ * `path` – The path to lock (used to identify the lock).
+ * `timeout` – Maximum seconds to wait for lock acquisition.
+* Yields:
+ None when lock is acquired.
+* Raises:
+ `TimeoutError` – If lock cannot be acquired within timeout.
+
+#### NOTE
+File-based locking (flock) does NOT work reliably on NFS mounts
+or network filesystems.
+
+#### abstractmethod read()
+
+Read and return the contents of a file as a string.
+
+* Parameters:
+ `path` – The file path to read from.
+* Returns:
+ The file contents as a string.
+
+#### abstractmethod write()
+
+Write contents to a file at the specified path.
+
+* Parameters:
+ * `path` – The file path where contents should be written.
+ * `contents` – The data to write, either as string or bytes.
+
+
+---
+
+### class InMemoryFileStore
+
+Bases: [`FileStore`](#class-filestore)
+
+
+#### Properties
+
+- `files`: dict[str, str]
+
+#### Methods
+
+#### __init__()
+
+#### delete()
+
+Delete the file or directory at the specified path.
+
+* Parameters:
+ `path` – The file or directory path to delete.
+
+#### exists()
+
+Check if a file exists.
+
+#### get_absolute_path()
+
+Get absolute path (uses temp dir with unique instance ID).
+
+#### list()
+
+List all files and directories at the specified path.
+
+* Parameters:
+ `path` – The directory path to list contents from.
+* Returns:
+ A list of file and directory names in the specified path.
+
+#### lock()
+
+Acquire thread lock for in-memory store.
+
+#### read()
+
+Read and return the contents of a file as a string.
+
+* Parameters:
+ `path` – The file path to read from.
+* Returns:
+ The file contents as a string.
+
+#### write()
+
+Write contents to a file at the specified path.
+
+* Parameters:
+ * `path` – The file path where contents should be written.
+ * `contents` – The data to write, either as string or bytes.
+
+
+---
+
+### class LocalFileStore
+
+Bases: [`FileStore`](#class-filestore)
+
+
+#### Properties
+
+- `cache`: MemoryLRUCache
+- `root`: str
+
+#### Methods
+
+#### __init__()
+
+Initialize a LocalFileStore with caching.
+
+* Parameters:
+ * `root` – Root directory for file storage.
+ * `cache_limit_size` – Maximum number of cached entries (default: 500).
+ * `cache_memory_size` – Maximum cache memory in bytes (default: 20MB).
+
+#### NOTE
+The cache assumes exclusive access to files. External modifications
+to files will not be detected and may result in stale cache reads.
+
+#### delete()
+
+Delete the file or directory at the specified path.
+
+* Parameters:
+ `path` – The file or directory path to delete.
+
+#### exists()
+
+Check if a file or directory exists.
+
+#### get_absolute_path()
+
+Get absolute filesystem path.
+
+#### get_full_path()
+
+#### list()
+
+List all files and directories at the specified path.
+
+* Parameters:
+ `path` – The directory path to list contents from.
+* Returns:
+ A list of file and directory names in the specified path.
+
+#### lock()
+
+Acquire file-based lock using flock.
+
+#### read()
+
+Read and return the contents of a file as a string.
+
+* Parameters:
+ `path` – The file path to read from.
+* Returns:
+ The file contents as a string.
+
+#### write()
+
+Write contents to a file at the specified path.
+
+* Parameters:
+ * `path` – The file path where contents should be written.
+ * `contents` – The data to write, either as string or bytes.
diff --git a/sdk/api-reference/openhands.sdk.llm.mdx b/sdk/api-reference/openhands.sdk.llm.mdx
index 4e6f8a90..3e08c0c6 100644
--- a/sdk/api-reference/openhands.sdk.llm.mdx
+++ b/sdk/api-reference/openhands.sdk.llm.mdx
@@ -4,6 +4,9 @@ description: API reference for openhands.sdk.llm module
---
+
+---
+
### class CredentialStore
Bases: `object`
@@ -63,6 +66,60 @@ Update tokens for an existing credential.
* Returns:
Updated credentials, or None if no existing credentials found
+
+---
+
+### class FallbackStrategy
+
+Bases: `BaseModel`
+
+Encapsulates fallback behavior for LLM calls.
+
+When the primary LLM fails with a transient error (after retries),
+this strategy tries alternate LLMs loaded from LLMProfileStore profiles.
+Fallback is per-call: each new request starts with the primary model.
+
+
+#### Properties
+
+- `fallback_llms`: list[str]
+- `profile_store_dir`: str | Path | None
+
+#### Methods
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+#### model_post_init()
+
+This function is meant to behave like a BaseModel method to initialise private attributes.
+
+It takes context as an argument since that’s what pydantic-core passes when calling it.
+
+* Parameters:
+ * `self` – The BaseModel instance.
+ * `context` – The context.
+
+#### should_fallback()
+
+Whether this error type is eligible for fallback.
+
+#### try_fallback()
+
+Try fallback LLMs in order. Merges metrics into primary on success.
+
+* Parameters:
+ * `primary_model` – The primary model name (for logging).
+ * `primary_error` – The error from the primary model.
+ * `primary_metrics` – The primary LLM’s Metrics to merge fallback costs into.
+ * `call_fn` – A callable that takes an LLM instance and returns an LLMResponse.
+* Returns:
+ LLMResponse from the first successful fallback, or None if all fail.
+
+
+---
+
### class ImageContent
Bases: `BaseContent`
@@ -83,6 +140,9 @@ Configuration for the model, should be a dictionary conforming to [ConfigDict][p
Convert to LLM API format.
+
+---
+
### class LLM
Bases: `BaseModel`, `RetryMixin`, `NonNativeToolCallingMixin`
@@ -124,6 +184,7 @@ retry logic, and tool calling capabilities.
- `enable_encrypted_reasoning`: bool
- `extended_thinking_budget`: int | None
- `extra_headers`: dict[str, str] | None
+- `fallback_strategy`: [FallbackStrategy](#class-fallbackstrategy) | None
- `force_string_serializer`: bool | None
- `input_cost_per_token`: float | None
- `is_subscription`: bool
@@ -193,7 +254,7 @@ It handles message formatting, tool calling, and response processing.
* `_return_metrics` – Whether to return usage metrics
* `add_security_risk_prediction` – Add security_risk field to tool schemas
* `on_token` – Optional callback for streaming tokens
- kwargs* – Additional arguments passed to the LLM API
+ `kwargs`* – Additional arguments passed to the LLM API
* Returns:
LLMResponse containing the model’s response and metadata.
@@ -272,7 +333,7 @@ Maps Message[] -> (instructions, input[]) and returns LLMResponse.
* `_return_metrics` – Whether to return usage metrics
* `add_security_risk_prediction` – Add security_risk field to tool schemas
* `on_token` – Optional callback for streaming deltas
- kwargs* – Additional arguments passed to the API
+ `kwargs`* – Additional arguments passed to the API
#### NOTE
Summary field is always added to tool schemas for transparency and
@@ -306,7 +367,7 @@ Supported OpenAI models:
credentials exist.
* `open_browser` – Whether to automatically open the browser for the
OAuth login flow.
- llm_kwargs* – Additional arguments to pass to the LLM constructor.
+ \llm_kwargs* – Additional arguments to pass to the LLM constructor.
* Returns:
An LLM instance configured for subscription-based access.
* Raises:
@@ -319,6 +380,9 @@ Whether this model uses the OpenAI Responses API path.
#### vision_is_active()
+
+---
+
### class LLMProfileStore
Bases: `object`
@@ -380,6 +444,9 @@ Note that if a profile name already exists, it will be overwritten.
* Raises:
`TimeoutError` – If the lock cannot be acquired.
+
+---
+
### class LLMRegistry
Bases: `object`
@@ -455,6 +522,9 @@ Subscribe to registry events.
* Parameters:
`callback` – Function to call when LLMs are created or updated.
+
+---
+
### class LLMResponse
Bases: `BaseModel`
@@ -505,6 +575,9 @@ ResponsesAPIResponse) for internal use
* Type:
litellm.types.utils.ModelResponse | litellm.types.llms.openai.ResponsesAPIResponse
+
+---
+
### class Message
Bases: `BaseModel`
@@ -576,6 +649,9 @@ Return serialized form.
Either an instructions string (for system) or input items (for other roles).
+
+---
+
### class MessageToolCall
Bases: `BaseModel`
@@ -594,7 +670,7 @@ for Responses function_call_output call_id.
- `origin`: Literal['completion', 'responses']
- `costs`: list[Cost]
- `response_latencies`: list[ResponseLatency]
-- `token_usages`: list[TokenUsage]
+- `token_usages`: list[[TokenUsage](#class-tokenusage)]
#### Methods
@@ -667,6 +743,9 @@ Configuration for the model, should be a dictionary conforming to [ConfigDict][p
#### classmethod validate_accumulated_cost()
+
+---
+
### class MetricsSnapshot
Bases: `BaseModel`
@@ -679,7 +758,7 @@ Does not include lists of individual costs, latencies, or token usages.
#### Properties
- `accumulated_cost`: float
-- `accumulated_token_usage`: TokenUsage | None
+- `accumulated_token_usage`: [TokenUsage](#class-tokenusage) | None
- `max_budget_per_task`: float | None
- `model_name`: str
@@ -689,6 +768,9 @@ Does not include lists of individual costs, latencies, or token usages.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+---
+
### class OAuthCredentials
Bases: `BaseModel`
@@ -714,6 +796,9 @@ Check if the access token is expired.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+---
+
### class OpenAISubscriptionAuth
Bases: `object`
@@ -744,7 +829,7 @@ Create an LLM instance configured for Codex subscription access.
* `model` – The model to use (must be in OPENAI_CODEX_MODELS).
* `credentials` – OAuth credentials to use. If None, uses stored credentials.
* `instructions` – Optional instructions for the Codex model.
- llm_kwargs* – Additional arguments to pass to LLM constructor.
+ \llm_kwargs* – Additional arguments to pass to LLM constructor.
* Returns:
An LLM instance configured for Codex access.
* Raises:
@@ -789,6 +874,9 @@ Refresh credentials if they are expired.
* Raises:
`RuntimeError` – If token refresh fails.
+
+---
+
### class ReasoningItemModel
Bases: `BaseModel`
@@ -812,6 +900,9 @@ Do not log or render encrypted_content.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+---
+
### class RedactedThinkingBlock
Bases: `BaseModel`
@@ -833,6 +924,9 @@ before extended thinking was enabled.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+---
+
### class RegistryEvent
Bases: `BaseModel`
@@ -843,6 +937,9 @@ Bases: `BaseModel`
- `llm`: [LLM](#class-llm)
- `model_config`: ClassVar[ConfigDict] = (configuration object)
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+---
+
### class RouterLLM
Bases: [`LLM`](#class-llm)
@@ -878,7 +975,7 @@ underlying LLM based on the routing logic implemented in select_llm().
* `return_metrics` – Whether to return usage metrics
* `add_security_risk_prediction` – Add security_risk field to tool schemas
* `on_token` – Optional callback for streaming tokens
- kwargs* – Additional arguments passed to the LLM API
+ `kwargs`* – Additional arguments passed to the LLM API
#### NOTE
Summary field is always added to tool schemas for transparency and
@@ -914,6 +1011,9 @@ Guarantee model exists before LLM base validation runs.
#### classmethod validate_llms_not_empty()
+
+---
+
### class TextContent
Bases: `BaseContent`
@@ -932,6 +1032,9 @@ Bases: `BaseContent`
Convert to LLM API format.
+
+---
+
### class ThinkingBlock
Bases: `BaseModel`
@@ -954,3 +1057,31 @@ and passed back to the API for tool use scenarios.
#### model_config = (configuration object)
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+
+---
+
+### class TokenUsage
+
+Bases: `BaseModel`
+
+Metric tracking detailed token usage per completion call.
+
+
+#### Properties
+
+- `cache_read_tokens`: int
+- `cache_write_tokens`: int
+- `completion_tokens`: int
+- `context_window`: int
+- `model`: str
+- `per_turn_token`: int
+- `prompt_tokens`: int
+- `reasoning_tokens`: int
+- `response_id`: str
+
+#### Methods
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
\ No newline at end of file
diff --git a/sdk/api-reference/openhands.sdk.logger.mdx b/sdk/api-reference/openhands.sdk.logger.mdx
new file mode 100644
index 00000000..16bbfe1a
--- /dev/null
+++ b/sdk/api-reference/openhands.sdk.logger.mdx
@@ -0,0 +1,39 @@
+---
+title: openhands.sdk.logger
+description: API reference for openhands.sdk.logger module
+---
+
+
+### get_logger()
+
+Get a logger instance for the specified module.
+
+This function returns a configured logger that inherits from the root logger
+setup. The logger supports both Rich formatting for human-readable output
+and JSON formatting for machine processing, depending on environment configuration.
+
+* Parameters:
+ `name` – The name of the module, typically __name__.
+* Returns:
+ A configured Logger instance.
+
+#### Example
+
+```pycon
+>>> from openhands.sdk.logger import get_logger
+>>> logger = get_logger(__name__)
+>>> logger.info("This is an info message")
+>>> logger.error("This is an error message")
+```
+
+### rolling_log_view()
+
+Temporarily attach a rolling view handler that renders the last N log lines.
+
+- Local TTY & not CI & not JSON: pretty, live-updating view (Rich.Live)
+- CI / non-TTY: plain line-by-line (no terminal control)
+- JSON mode: buffer only; on exit emit ONE large log record with the full snapshot.
+
+### setup_logging()
+
+Configure the root logger. All child loggers inherit this setup.
diff --git a/sdk/api-reference/openhands.sdk.mcp.mdx b/sdk/api-reference/openhands.sdk.mcp.mdx
new file mode 100644
index 00000000..4b3ff1a8
--- /dev/null
+++ b/sdk/api-reference/openhands.sdk.mcp.mdx
@@ -0,0 +1,227 @@
+---
+title: openhands.sdk.mcp
+description: API reference for openhands.sdk.mcp module
+---
+
+
+MCP (Model Context Protocol) integration for agent-sdk.
+
+
+---
+
+### class MCPClient
+
+Bases: `Client`
+
+MCP client with sync helpers and lifecycle management.
+
+Extends fastmcp.Client with:
+: - call_async_from_sync(awaitable_or_fn,
+ `*args`, timeout=None,
+ `**kwargs`)
+ - call_sync_from_async(fn,
+ `*args`,
+ `**kwargs`) # await this from async code
+
+After create_mcp_tools() populates it, use as a sync context manager:
+
+with create_mcp_tools(config) as client:
+: for tool in client.tools:
+: # use tool
+
+# Connection automatically closed
+
+Or manage lifecycle manually by calling sync_close() when done.
+
+
+#### Properties
+
+- `tools`: list[[MCPToolDefinition](#class-mcptooldefinition)]
+ The MCP tools using this client connection (returns a copy).
+- `config`: dict | None
+- `timeout`: float
+
+#### Methods
+
+#### __init__()
+
+#### call_async_from_sync()
+
+Run a coroutine or async function on this client’s loop from sync code.
+
+Usage:
+: mcp.call_async_from_sync(async_fn, arg1, kw=…)
+ mcp.call_async_from_sync(coro)
+
+#### async call_sync_from_async()
+
+Await running a blocking function in the default threadpool from async code.
+
+#### async connect()
+
+Establish connection to the MCP server.
+
+#### sync_close()
+
+Synchronously close the MCP client and cleanup resources.
+
+This will attempt to call the async close() method if available,
+then shutdown the background event loop. Safe to call multiple times.
+
+#### __init__()
+
+
+---
+
+### class MCPToolAction
+
+Bases: `Action`
+
+Schema for MCP input action.
+
+It is just a thin wrapper around raw JSON and does
+not do any validation.
+
+Validation will be performed by MCPTool.__call__
+by constructing dynamically created Pydantic model
+from the MCP tool input schema.
+
+
+#### Properties
+
+- `data`: dict[str, Any]
+- `model_config`: = (configuration object)
+ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+#### Methods
+
+#### to_mcp_arguments()
+
+Return the data field as MCP tool call arguments.
+
+This is used to convert this action to MCP tool call arguments.
+The data field contains the dynamic fields from the tool call.
+
+
+---
+
+### class MCPToolDefinition
+
+Bases: `ToolDefinition[MCPToolAction, MCPToolObservation]`
+
+MCP Tool that wraps an MCP client and provides tool functionality.
+
+
+#### Properties
+
+- `mcp_tool`: Tool
+- `model_config`: = (configuration object)
+ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+- `name`: str
+ Return the MCP tool name instead of the class name.
+
+#### Methods
+
+#### action_from_arguments()
+
+Create an MCPToolAction from parsed arguments with early validation.
+
+We validate the raw arguments against the MCP tool’s input schema here so
+Agent._get_action_event can catch ValidationError and surface an
+AgentErrorEvent back to the model instead of crashing later during tool
+execution. On success, we return MCPToolAction with sanitized arguments.
+
+* Parameters:
+ `arguments` – The parsed arguments from the tool call.
+* Returns:
+ The MCPToolAction instance with data populated from the arguments.
+* Raises:
+ `ValidationError` – If the arguments do not conform to the tool schema.
+
+#### classmethod create()
+
+Create a sequence of Tool instances.
+
+This method must be implemented by all subclasses to provide custom
+initialization logic, typically initializing the executor with parameters
+from conv_state and other optional parameters.
+
+* Parameters:
+ `args`** – Variable positional arguments (typically conv_state as first arg).
+ `kwargs`* – Optional parameters for tool initialization.
+* Returns:
+ A sequence of Tool instances. Even single tools are returned as a sequence
+ to provide a consistent interface and eliminate union return types.
+
+#### to_mcp_tool()
+
+Convert a Tool to an MCP tool definition.
+
+Allow overriding input/output schemas (usually by subclasses).
+
+* Parameters:
+ * `input_schema` – Optionally override the input schema.
+ * `output_schema` – Optionally override the output schema.
+
+#### to_openai_tool()
+
+Convert a Tool to an OpenAI tool.
+
+For MCP, we dynamically create the action_type (type: Schema)
+from the MCP tool input schema, and pass it to the parent method.
+It will use the .model_fields from this pydantic model to
+generate the OpenAI-compatible tool schema.
+
+* Parameters:
+ `add_security_risk_prediction` – Whether to add a security_risk field
+ to the action schema for LLM to predict. This is useful for
+ tools that may have safety risks, so the LLM can reason about
+ the risk level before calling the tool.
+
+
+---
+
+### class MCPToolExecutor
+
+Bases: `ToolExecutor`
+
+Executor for MCP tools.
+
+
+#### Properties
+
+- `client`: [MCPClient](#class-mcpclient)
+- `timeout`: float
+- `tool_name`: str
+
+#### Methods
+
+#### __init__()
+
+#### async call_tool()
+
+Execute the MCP tool call using the already-connected client.
+
+
+---
+
+### class MCPToolObservation
+
+Bases: `Observation`
+
+Observation from MCP tool execution.
+
+
+#### Properties
+
+- `model_config`: = (configuration object)
+ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+- `tool_name`: str
+- `visualize`: Text
+ Return Rich Text representation of this observation.
+
+#### Methods
+
+#### classmethod from_call_tool_result()
+
+Create an MCPToolObservation from a CallToolResult.
diff --git a/sdk/api-reference/openhands.sdk.observability.mdx b/sdk/api-reference/openhands.sdk.observability.mdx
new file mode 100644
index 00000000..60c15f6e
--- /dev/null
+++ b/sdk/api-reference/openhands.sdk.observability.mdx
@@ -0,0 +1,24 @@
+---
+title: openhands.sdk.observability
+description: API reference for openhands.sdk.observability module
+---
+
+
+### maybe_init_laminar()
+
+Initialize Laminar if the environment variables are set.
+
+Example configuration:
+```bash
+OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://otel-collector:4317/v1/traces
+
+# comma separated, key=value url-encoded pairs
+OTEL_EXPORTER_OTLP_TRACES_HEADERS=”Authorization=Bearer%20``,X-Key=``”
+
+# grpc is assumed if not specified
+OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf # or grpc/protobuf
+# or
+OTEL_EXPORTER=otlp_http # or otlp_grpc
+```
+
+### observe()
diff --git a/sdk/api-reference/openhands.sdk.plugin.mdx b/sdk/api-reference/openhands.sdk.plugin.mdx
new file mode 100644
index 00000000..6d7abca8
--- /dev/null
+++ b/sdk/api-reference/openhands.sdk.plugin.mdx
@@ -0,0 +1,625 @@
+---
+title: openhands.sdk.plugin
+description: API reference for openhands.sdk.plugin module
+---
+
+
+Plugin module for OpenHands SDK.
+
+This module provides support for loading and managing plugins that bundle
+skills, hooks, MCP configurations, agents, and commands together.
+
+It also provides support for plugin marketplaces - directories that list
+available plugins with their metadata and source locations.
+
+Additionally, it provides utilities for managing installed plugins in the
+user’s home directory (~/.openhands/plugins/installed/).
+
+
+---
+
+### class CommandDefinition
+
+Bases: `BaseModel`
+
+Command definition loaded from markdown file.
+
+Commands are slash commands that users can invoke directly.
+They define instructions for the agent to follow.
+
+
+#### Properties
+
+- `allowed_tools`: list[str]
+- `argument_hint`: str | None
+- `content`: str
+- `description`: str
+- `metadata`: dict[str, Any]
+- `name`: str
+- `source`: str | None
+
+#### Methods
+
+#### classmethod load()
+
+Load a command definition from a markdown file.
+
+Command markdown files have YAML frontmatter with:
+- description: Command description
+- argument-hint: Hint for command arguments (string or list)
+- allowed-tools: List of allowed tools
+
+The body of the markdown is the command instructions.
+
+* Parameters:
+ `command_path` – Path to the command markdown file.
+* Returns:
+ Loaded CommandDefinition instance.
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+#### to_skill()
+
+Convert this command to a keyword-triggered Skill.
+
+Creates a Skill with a KeywordTrigger using the Claude Code namespacing
+format: /``:``
+
+* Parameters:
+ `plugin_name` – The name of the plugin this command belongs to.
+* Returns:
+ A Skill object with the command content and a KeywordTrigger.
+
+
+---
+
+### class GitHubURLComponents
+
+Bases: `NamedTuple`
+
+Parsed components of a GitHub blob/tree URL.
+
+
+#### Properties
+
+- `branch`: str
+ Alias for field number 2
+- `owner`: str
+ Alias for field number 0
+- `path`: str
+ Alias for field number 3
+- `repo`: str
+ Alias for field number 1
+
+---
+
+### class InstalledPluginInfo
+
+Bases: `BaseModel`
+
+Information about an installed plugin.
+
+This model tracks metadata about a plugin installation, including
+where it was installed from and when.
+
+
+#### Properties
+
+- `description`: str
+- `enabled`: bool
+- `install_path`: str
+- `installed_at`: str
+- `name`: str
+- `repo_path`: str | None
+- `resolved_ref`: str | None
+- `source`: str
+- `version`: str
+
+#### Methods
+
+#### classmethod from_plugin()
+
+Create InstalledPluginInfo from a loaded Plugin.
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+
+---
+
+### class InstalledPluginsMetadata
+
+Bases: `BaseModel`
+
+Metadata file for tracking all installed plugins.
+
+
+#### Properties
+
+- `plugins`: dict[str, [InstalledPluginInfo](#class-installedplugininfo)]
+
+#### Methods
+
+#### classmethod get_path()
+
+Get the metadata file path for the given installed plugins directory.
+
+#### classmethod load_from_dir()
+
+Load metadata from the installed plugins directory.
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+#### save_to_dir()
+
+Save metadata to the installed plugins directory.
+
+
+---
+
+### class Marketplace
+
+Bases: `BaseModel`
+
+A plugin marketplace that lists available plugins and skills.
+
+Follows the Claude Code marketplace structure for compatibility,
+with an additional skills field for standalone skill references.
+
+The marketplace.json file is located in .plugin/ or .claude-plugin/
+directory at the root of the marketplace repository.
+
+#### Properties
+
+- `description`: str | None
+- `metadata`: [MarketplaceMetadata](#class-marketplacemetadata) | None
+- `model_config`: = (configuration object)
+ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+- `name`: str
+- `owner`: [MarketplaceOwner](#class-marketplaceowner)
+- `path`: str | None
+- `plugins`: list[[MarketplacePluginEntry](#class-marketplacepluginentry)]
+- `skills`: list[[MarketplaceEntry](#class-marketplaceentry)]
+
+#### Methods
+
+#### get_plugin()
+
+Get a plugin entry by name.
+
+* Parameters:
+ `name` – Plugin name to look up.
+* Returns:
+ MarketplacePluginEntry if found, None otherwise.
+
+#### classmethod load()
+
+Load a marketplace from a directory.
+
+Looks for marketplace.json in .plugin/ or .claude-plugin/ directories.
+
+* Parameters:
+ `marketplace_path` – Path to the marketplace directory.
+* Returns:
+ Loaded Marketplace instance.
+* Raises:
+ * `FileNotFoundError` – If the marketplace directory or manifest doesn’t exist.
+ * `ValueError` – If the marketplace manifest is invalid.
+
+#### resolve_plugin_source()
+
+Resolve a plugin’s source to a full path or URL.
+
+* Returns:
+ - source: Resolved source string (path or URL)
+ - ref: Branch, tag, or commit reference (None for local paths)
+ - subpath: Subdirectory path within the repo (None if not specified)
+* Return type:
+ Tuple of (source, [ref](#class-ref), subpath) where
+
+
+---
+
+### class MarketplaceEntry
+
+Bases: `BaseModel`
+
+Base class for marketplace entries (plugins and skills).
+
+Both plugins and skills are pointers to directories:
+- Plugin directories contain: plugin.json, skills/, commands/, agents/, etc.
+- Skill directories contain: SKILL.md and optionally scripts/, references/, assets/
+
+Source is a string path (local path or GitHub URL).
+
+
+#### Properties
+
+- `author`: [PluginAuthor](#class-pluginauthor) | None
+- `category`: str | None
+- `description`: str | None
+- `homepage`: str | None
+- `model_config`: = (configuration object)
+ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+- `name`: str
+- `source`: str
+- `version`: str | None
+
+---
+
+### class MarketplaceMetadata
+
+Bases: `BaseModel`
+
+Optional metadata for a marketplace.
+
+
+#### Properties
+
+- `description`: str | None
+- `model_config`: = (configuration object)
+ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+- `version`: str | None
+
+---
+
+### class MarketplaceOwner
+
+Bases: `BaseModel`
+
+Owner information for a marketplace.
+
+The owner represents the maintainer or team responsible for the marketplace.
+
+
+#### Properties
+
+- `email`: str | None
+- `name`: str
+
+#### Methods
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+
+---
+
+### class MarketplacePluginEntry
+
+Bases: [`MarketplaceEntry`](#class-marketplaceentry)
+
+Plugin entry in a marketplace.
+
+Extends MarketplaceEntry with Claude Code compatibility fields for
+inline plugin definitions (when strict=False).
+
+Plugins support both string sources and complex source objects
+(MarketplacePluginSource) for GitHub/git URLs with ref and path.
+
+
+#### Properties
+
+- `agents`: str | list[str] | None
+- `commands`: str | list[str] | None
+- `hooks`: str | HooksConfigDict | None
+- `keywords`: list[str]
+- `license`: str | None
+- `lsp_servers`: LspServersDict | None
+- `mcp_servers`: McpServersDict | None
+- `model_config`: = (configuration object)
+ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+- `repository`: str | None
+- `source`: str | [MarketplacePluginSource](#class-marketplacepluginsource)
+- `strict`: bool
+- `tags`: list[str]
+
+#### Methods
+
+#### to_plugin_manifest()
+
+Convert to PluginManifest (for strict=False entries).
+
+
+---
+
+### class MarketplacePluginSource
+
+Bases: `BaseModel`
+
+Plugin source specification for non-local sources.
+
+Supports GitHub repositories and generic git URLs.
+
+
+#### Properties
+
+- `model_config`: = (configuration object)
+ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+- `path`: str | None
+- `ref`: str | None
+- `repo`: str | None
+- `source`: str
+- `url`: str | None
+
+#### Methods
+
+#### validate_source_fields()
+
+Validate that required fields are present based on source type.
+
+
+---
+
+### class Plugin
+
+Bases: `BaseModel`
+
+A plugin that bundles skills, hooks, MCP config, agents, and commands.
+
+Plugins follow the Claude Code plugin structure for compatibility:
+
+
+plugin-name/
+├── .claude-plugin/ # or .plugin/
+│ └── plugin.json # Plugin metadata
+├── commands/ # Slash commands (optional)
+├── agents/ # Specialized agents (optional)
+├── skills/ # Agent Skills (optional)
+├── hooks/ # Event handlers (optional)
+│ └── hooks.json
+├── .mcp.json # External tool configuration (optional)
+└── README.md # Plugin documentation
+
+#### Properties
+
+- `agents`: list[AgentDefinition]
+- `commands`: list[[CommandDefinition](#class-commanddefinition)]
+- `description`: str
+ Get the plugin description.
+- `hooks`: HookConfig | None
+- `manifest`: [PluginManifest](#class-pluginmanifest)
+- `mcp_config`: dict[str, Any] | None
+- `name`: str
+ Get the plugin name.
+- `path`: str
+- `skills`: list[Skill]
+- `version`: str
+ Get the plugin version.
+
+#### Methods
+
+#### add_mcp_config_to()
+
+Add this plugin’s MCP servers to an MCP config.
+
+Plugin MCP servers override existing servers with the same name.
+
+Merge semantics (Claude Code compatible):
+- mcpServers: deep-merge by server name (last plugin wins for same server)
+- Other top-level keys: shallow override (plugin wins)
+
+* Parameters:
+ `mcp_config` – Existing MCP config (or None to create new)
+* Returns:
+ New MCP config dict with this plugin’s servers added
+
+#### add_skills_to()
+
+Add this plugin’s skills to an agent context.
+
+Plugin skills override existing skills with the same name.
+Includes both explicit skills and command-derived skills.
+
+* Parameters:
+ * `agent_context` – Existing agent context (or None to create new)
+ * `max_skills` – Optional max total skills (raises ValueError if exceeded)
+* Returns:
+ New AgentContext with this plugin’s skills added
+* Raises:
+ `ValueError` – If max_skills limit would be exceeded
+
+#### classmethod fetch()
+
+Fetch a plugin from a remote source and return the local cached path.
+
+This method fetches plugins from remote sources (GitHub repositories, git URLs)
+and caches them locally. Use the returned path with Plugin.load() to load
+the plugin.
+
+* Parameters:
+ * `source` –
+
+ Plugin source - can be:
+ - Any git URL (GitHub, GitLab, Bitbucket, Codeberg, self-hosted, etc.)
+ e.g., “[https://gitlab.com/org/repo](https://gitlab.com/org/repo)”, “[git@bitbucket.org](mailto:git@bitbucket.org):team/repo.git”
+ - ”github:owner/repo” - GitHub shorthand (convenience syntax)
+ - ”/local/path” - Local path (returned as-is)
+ * `cache_dir` – Directory for caching. Defaults to ~/.openhands/cache/plugins/
+ * `ref` – Optional branch, tag, or commit to checkout.
+ * `update` – If True and cache exists, update it. If False, use cached as-is.
+ * `repo_path` – Subdirectory path within the git repository
+ (e.g., ‘plugins/my-plugin’ for monorepos). Only relevant for git
+ sources, not local paths. If specified, the returned path will
+ point to this subdirectory instead of the repository root.
+* Returns:
+ Path to the local plugin directory (ready for Plugin.load()).
+ If repo_path is specified, returns the path to that subdirectory.
+* Raises:
+ [PluginFetchError](#class-pluginfetcherror) – If fetching fails or repo_path doesn’t exist.
+
+#### get_all_skills()
+
+Get all skills including those converted from commands.
+
+Returns skills from both the skills/ directory and commands/ directory.
+Commands are converted to keyword-triggered skills using the format
+/``:``.
+
+* Returns:
+ Combined list of skills (original + command-derived skills).
+
+#### classmethod load()
+
+Load a plugin from a directory.
+
+* Parameters:
+ `plugin_path` – Path to the plugin directory.
+* Returns:
+ Loaded Plugin instance.
+* Raises:
+ * `FileNotFoundError` – If the plugin directory doesn’t exist.
+ * `ValueError` – If the plugin manifest is invalid.
+
+#### classmethod load_all()
+
+Load all plugins from a directory.
+
+* Parameters:
+ `plugins_dir` – Path to directory containing plugin subdirectories.
+* Returns:
+ List of loaded Plugin instances.
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+
+---
+
+### class PluginAuthor
+
+Bases: `BaseModel`
+
+Author information for a plugin.
+
+
+#### Properties
+
+- `email`: str | None
+- `name`: str
+
+#### Methods
+
+#### classmethod from_string()
+
+Parse author from string format ‘Name ``’.
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+
+---
+
+### class PluginManifest
+
+Bases: `BaseModel`
+
+Plugin manifest from plugin.json.
+
+
+#### Properties
+
+- `author`: [PluginAuthor](#class-pluginauthor) | None
+- `description`: str
+- `model_config`: = (configuration object)
+ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+- `name`: str
+- `version`: str
+
+---
+
+### class PluginSource
+
+Bases: `BaseModel`
+
+Specification for a plugin to load.
+
+This model describes where to find a plugin and is used by load_plugins()
+to fetch and load plugins from various sources.
+
+#### Examples
+
+```pycon
+>>> # GitHub repository
+>>> PluginSource(source="github:owner/repo", ref="v1.0.0")
+```
+
+```pycon
+>>> # Plugin from monorepo subdirectory
+>>> PluginSource(
+... source="github:owner/monorepo",
+... repo_path="plugins/my-plugin"
+... )
+```
+
+```pycon
+>>> # Local path
+>>> PluginSource(source="/path/to/plugin")
+```
+
+
+#### Properties
+
+- `ref`: str | None
+- `repo_path`: str | None
+- `source`: str
+
+#### Methods
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+#### classmethod validate_repo_path()
+
+Validate repo_path is a safe relative path within the repository.
+
+
+---
+
+### class ResolvedPluginSource
+
+Bases: `BaseModel`
+
+A plugin source with resolved ref (pinned to commit SHA).
+
+Used for persistence to ensure deterministic behavior across pause/resume.
+When a conversation is resumed, the resolved ref ensures we get exactly
+the same plugin version that was used when the conversation started.
+
+The resolved_ref is the actual commit SHA that was fetched, even if the
+original ref was a branch name like ‘main’. This prevents drift when
+branches are updated between pause and resume.
+
+
+#### Properties
+
+- `original_ref`: str | None
+- `repo_path`: str | None
+- `resolved_ref`: str | None
+- `source`: str
+
+#### Methods
+
+#### classmethod from_plugin_source()
+
+Create a ResolvedPluginSource from a PluginSource and resolved ref.
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+#### to_plugin_source()
+
+Convert back to PluginSource using the resolved ref.
+
+When loading from persistence, use the resolved_ref to ensure we get
+the exact same version that was originally fetched.
diff --git a/sdk/api-reference/openhands.sdk.secret.mdx b/sdk/api-reference/openhands.sdk.secret.mdx
new file mode 100644
index 00000000..5d9afcf3
--- /dev/null
+++ b/sdk/api-reference/openhands.sdk.secret.mdx
@@ -0,0 +1,82 @@
+---
+title: openhands.sdk.secret
+description: API reference for openhands.sdk.secret module
+---
+
+
+Secret management module for handling sensitive data.
+
+This module provides classes and types for managing secrets in OpenHands.
+
+
+---
+
+### class LookupSecret
+
+Bases: [`SecretSource`](#class-secretsource)
+
+A secret looked up from some external url
+
+
+#### Properties
+
+- `headers`: dict[str, str]
+- `url`: str
+
+#### Methods
+
+#### get_value()
+
+Get the value of a secret in plain text
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+
+---
+
+### class SecretSource
+
+Bases: `DiscriminatedUnionMixin`, `ABC`
+
+Source for a named secret which may be obtained dynamically
+
+
+#### Properties
+
+- `description`: str | None
+
+#### Methods
+
+#### abstractmethod get_value()
+
+Get the value of a secret in plain text
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+
+---
+
+### class StaticSecret
+
+Bases: [`SecretSource`](#class-secretsource)
+
+A secret stored locally
+
+
+#### Properties
+
+- `value`: SecretStr | None
+
+#### Methods
+
+#### get_value()
+
+Get the value of a secret in plain text
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
\ No newline at end of file
diff --git a/sdk/api-reference/openhands.sdk.security.mdx b/sdk/api-reference/openhands.sdk.security.mdx
index d3a2fad4..62ac54f1 100644
--- a/sdk/api-reference/openhands.sdk.security.mdx
+++ b/sdk/api-reference/openhands.sdk.security.mdx
@@ -4,6 +4,9 @@ description: API reference for openhands.sdk.security module
---
+
+---
+
### class AlwaysConfirm
Bases: [`ConfirmationPolicyBase`](#class-confirmationpolicybase)
@@ -28,6 +31,9 @@ is required before executing an action based on its security risk level.
True if the action requires user confirmation before execution,
False if the action can proceed without confirmation.
+
+---
+
### class ConfirmRisky
Bases: [`ConfirmationPolicyBase`](#class-confirmationpolicybase)
@@ -60,6 +66,9 @@ is required before executing an action based on its security risk level.
#### classmethod validate_threshold()
+
+---
+
### class ConfirmationPolicyBase
Bases: `DiscriminatedUnionMixin`, `ABC`
@@ -84,6 +93,9 @@ is required before executing an action based on its security risk level.
True if the action requires user confirmation before execution,
False if the action can proceed without confirmation.
+
+---
+
### class GraySwanAnalyzer
Bases: [`SecurityAnalyzerBase`](#class-securityanalyzerbase)
@@ -156,6 +168,9 @@ Set the events for context when analyzing actions.
Validate that thresholds are properly ordered.
+
+---
+
### class LLMSecurityAnalyzer
Bases: [`SecurityAnalyzerBase`](#class-securityanalyzerbase)
@@ -182,6 +197,9 @@ This method checks if the action has a security_risk attribute set by the LLM
and returns it. The LLM may not always provide this attribute but it defaults to
UNKNOWN if not explicitly set.
+
+---
+
### class NeverConfirm
Bases: [`ConfirmationPolicyBase`](#class-confirmationpolicybase)
@@ -206,6 +224,9 @@ is required before executing an action based on its security risk level.
True if the action requires user confirmation before execution,
False if the action can proceed without confirmation.
+
+---
+
### class SecurityAnalyzerBase
Bases: `DiscriminatedUnionMixin`, `ABC`
@@ -274,6 +295,9 @@ and confirmation mode settings.
* Returns:
True if confirmation is required, False otherwise
+
+---
+
### class SecurityRisk
Bases: `str`, `Enum`
@@ -315,13 +339,13 @@ less risky than HIGH. UNKNOWN is not comparable to any other level.
To make this act like a standard well-ordered domain, we reflexively consider
risk levels to be riskier than themselves. That is:
- for risk_level in list(SecurityRisk):
- : assert risk_level.is_riskier(risk_level)
+for risk_level in list(SecurityRisk):
+: assert risk_level.is_riskier(risk_level)
- # More concretely:
- assert SecurityRisk.HIGH.is_riskier(SecurityRisk.HIGH)
- assert SecurityRisk.MEDIUM.is_riskier(SecurityRisk.MEDIUM)
- assert SecurityRisk.LOW.is_riskier(SecurityRisk.LOW)
+# More concretely:
+assert SecurityRisk.HIGH.is_riskier(SecurityRisk.HIGH)
+assert SecurityRisk.MEDIUM.is_riskier(SecurityRisk.MEDIUM)
+assert SecurityRisk.LOW.is_riskier(SecurityRisk.LOW)
This can be disabled by setting the reflexive parameter to False.
diff --git a/sdk/api-reference/openhands.sdk.skills.mdx b/sdk/api-reference/openhands.sdk.skills.mdx
new file mode 100644
index 00000000..019d2ff3
--- /dev/null
+++ b/sdk/api-reference/openhands.sdk.skills.mdx
@@ -0,0 +1,74 @@
+---
+title: openhands.sdk.skills
+description: API reference for openhands.sdk.skills module
+---
+
+
+Skill management utilities for OpenHands SDK.
+
+
+---
+
+### class InstalledSkillInfo
+
+Bases: `BaseModel`
+
+Information about an installed skill.
+
+
+#### Properties
+
+- `allowed_tools`: list[str] | None
+- `compatibility`: str | None
+- `description`: str
+- `enabled`: bool
+- `install_path`: str
+- `installed_at`: str
+- `license`: str | None
+- `metadata`: dict[str, str] | None
+- `name`: str
+- `repo_path`: str | None
+- `resolved_ref`: str | None
+- `source`: str
+
+#### Methods
+
+#### classmethod from_skill()
+
+Create InstalledSkillInfo from a loaded Skill.
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+
+---
+
+### class InstalledSkillsMetadata
+
+Bases: `BaseModel`
+
+Metadata file for tracking installed skills.
+
+
+#### Properties
+
+- `skills`: dict[str, [InstalledSkillInfo](#class-installedskillinfo)]
+
+#### Methods
+
+#### classmethod get_path()
+
+Get the metadata file path for the given installed skills directory.
+
+#### classmethod load_from_dir()
+
+Load metadata from the installed skills directory.
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+#### save_to_dir()
+
+Save metadata to the installed skills directory.
diff --git a/sdk/api-reference/openhands.sdk.subagent.mdx b/sdk/api-reference/openhands.sdk.subagent.mdx
new file mode 100644
index 00000000..8f436f94
--- /dev/null
+++ b/sdk/api-reference/openhands.sdk.subagent.mdx
@@ -0,0 +1,77 @@
+---
+title: openhands.sdk.subagent
+description: API reference for openhands.sdk.subagent module
+---
+
+
+
+---
+
+### class AgentDefinition
+
+Bases: `BaseModel`
+
+Agent definition loaded from Markdown file.
+
+Agents are specialized configurations that can be triggered based on
+user input patterns. They define custom system prompts and tool access.
+
+
+#### Properties
+
+- `color`: str | None
+- `description`: str
+- `hooks`: HookConfig | None
+- `max_iteration_per_run`: int | None
+- `mcp_servers`: dict[str, Any] | None
+- `metadata`: dict[str, Any]
+- `model`: str
+- `name`: str
+- `permission_mode`: str | None
+- `profile_store_dir`: str | None
+- `skills`: list[str]
+- `source`: str | None
+- `system_prompt`: str
+- `tools`: list[str]
+- `when_to_use_examples`: list[str]
+
+#### Methods
+
+#### get_confirmation_policy()
+
+Convert permission_mode to a ConfirmationPolicyBase instance.
+
+Returns None when permission_mode is None (inherit parent policy).
+
+#### classmethod load()
+
+Load an agent definition from a Markdown file.
+
+Agent Markdown files have YAML frontmatter with:
+- name: Agent name
+- description: Description with optional `` tags for triggering
+- tools (optional): List of allowed tools
+- skills (optional): Comma-separated skill names or list of skill names
+- mcp_servers (optional): MCP server configurations mapping
+- model (optional): Model profile to use (default: ‘inherit’)
+- color (optional): Display color
+- permission_mode (optional): How the subagent handles permissions
+
+(‘always_confirm’, ‘never_confirm’, ‘confirm_risky’). None inherits parent.
+- max_iterations_per_run: Max iteration per run
+- hooks (optional): List of applicable hooks
+
+The body of the Markdown is the system prompt.
+
+* Parameters:
+ `agent_path` – Path to the agent Markdown file.
+* Returns:
+ Loaded AgentDefinition instance.
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+#### NOTE
+Failures to load individual files are logged as warnings with stack traces
+but do not halt the overall loading process.
diff --git a/sdk/api-reference/openhands.sdk.tool.mdx b/sdk/api-reference/openhands.sdk.tool.mdx
index 62b85a29..661814d0 100644
--- a/sdk/api-reference/openhands.sdk.tool.mdx
+++ b/sdk/api-reference/openhands.sdk.tool.mdx
@@ -4,6 +4,9 @@ description: API reference for openhands.sdk.tool module
---
+
+---
+
### class Action
Bases: `Schema`, `ABC`
@@ -19,6 +22,9 @@ Base schema for input action.
Return Rich Text representation of this action.
This method can be overridden by subclasses to customize visualization.
The base implementation displays all action fields systematically.
+
+---
+
### class ExecutableTool
Bases: `Protocol`
@@ -38,6 +44,9 @@ when working with tools that are known to be executable.
#### __init__()
+
+---
+
### class FinishTool
Bases: `ToolDefinition[FinishAction, FinishObservation]`
@@ -58,7 +67,7 @@ Create FinishTool instance.
* Parameters:
* `conv_state` – Optional conversation state (not used by FinishTool).
- params* – Additional parameters (none supported).
+ \params* – Additional parameters (none supported).
* Returns:
A sequence containing a single FinishTool instance.
* Raises:
@@ -66,6 +75,9 @@ Create FinishTool instance.
#### name = 'finish'
+
+---
+
### class Observation
Bases: `Schema`, `ABC`
@@ -75,7 +87,7 @@ Base schema for output observation.
#### Properties
-- `ERROR_MESSAGE_HEADER`: ClassVar[str] = '[An error occurred during execution.]n'
+- `ERROR_MESSAGE_HEADER`: ClassVar[str] = '[An error occurred during execution.]\n'
- `content`: list[TextContent | ImageContent]
- `is_error`: bool
- `model_config`: = (configuration object)
@@ -101,10 +113,13 @@ Utility to create an Observation from a simple text string.
* Parameters:
* `text` – The text content to include in the observation.
* `is_error` – Whether this observation represents an error.
- kwargs* – Additional fields for the observation subclass.
+ `kwargs`* – Additional fields for the observation subclass.
* Returns:
An Observation instance with the text wrapped in a TextContent.
+
+---
+
### class ThinkTool
Bases: `ToolDefinition[ThinkAction, ThinkObservation]`
@@ -125,7 +140,7 @@ Create ThinkTool instance.
* Parameters:
* `conv_state` – Optional conversation state (not used by ThinkTool).
- params* – Additional parameters (none supported).
+ \params* – Additional parameters (none supported).
* Returns:
A sequence containing a single ThinkTool instance.
* Raises:
@@ -133,6 +148,9 @@ Create ThinkTool instance.
#### name = 'think'
+
+---
+
### class Tool
Bases: `BaseModel`
@@ -161,6 +179,9 @@ Validate that name is not empty.
Convert None params to empty dict.
+
+---
+
### class ToolAnnotations
Bases: `BaseModel`
@@ -180,6 +201,9 @@ Based on Model Context Protocol (MCP) spec:
- `openWorldHint`: bool
- `readOnlyHint`: bool
- `title`: str | None
+
+---
+
### class ToolDefinition
Bases: `DiscriminatedUnionMixin`, `ABC`, `Generic`
@@ -201,39 +225,25 @@ Features:
Simple tool with no parameters:
: class FinishTool(ToolDefinition[FinishAction, FinishObservation]):
: @classmethod
- def create(cls, conv_state=None,
- `
`
- ```
- **
- ```
- `
`
- params):
- `
`
- > return [cls(name=”finish”, …, executor=FinishExecutor())]
+ def create(cls, conv_state=None, params):
+
+ return [cls(name=”finish”, …, executor=FinishExecutor())]
Complex tool with initialization parameters:
: class TerminalTool(ToolDefinition[TerminalAction,
: TerminalObservation]):
@classmethod
- def create(cls, conv_state,
- `
`
+ def create(cls, conv_state, params):
+
+ executor = TerminalExecutor(
+ : working_dir=conv_state.workspace.working_dir,
```
**
```
- `
`
- params):
- `
`
- > executor = TerminalExecutor(
- > : working_dir=conv_state.workspace.working_dir,
- > `
`
- > ```
- > **
- > ```
- > `
`
- > params,
- `
`
- > )
- > return [cls(name=”terminal”, …, executor=executor)]
+ params,
+
+ )
+ return [cls(name=”terminal”, …, executor=executor)]
#### Properties
@@ -245,7 +255,9 @@ Complex tool with initialization parameters:
- `meta`: dict[str, Any] | None
- `model_config`: ClassVar[ConfigDict] = (configuration object)
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+```bash
- `name`: ClassVar[str] = ''
+```
- `observation_type`: type[[Observation](#class-observation)] | None
- `title`: str
@@ -284,8 +296,8 @@ initialization logic, typically initializing the executor with parameters
from conv_state and other optional parameters.
* Parameters:
- args** – Variable positional arguments (typically conv_state as first arg).
- kwargs* – Optional parameters for tool initialization.
+ `args`** – Variable positional arguments (typically conv_state as first arg).
+ `kwargs`* – Optional parameters for tool initialization.
* Returns:
A sequence of Tool instances. Even single tools are returned as a sequence
to provide a consistent interface and eliminate union return types.
@@ -347,6 +359,9 @@ For Responses API, function tools expect top-level keys:
Summary field is always added to the schema for transparency and
explainability of agent actions.
+
+---
+
### class ToolExecutor
Bases: `ABC`, `Generic`
diff --git a/sdk/api-reference/openhands.sdk.utils.mdx b/sdk/api-reference/openhands.sdk.utils.mdx
index 237164e0..aba53890 100644
--- a/sdk/api-reference/openhands.sdk.utils.mdx
+++ b/sdk/api-reference/openhands.sdk.utils.mdx
@@ -32,6 +32,37 @@ Optionally saves the full content to a file for later investigation.
Original content if under limit, or truncated content with head and tail
preserved and reference to saved file if applicable
+### page_iterator()
+
+Iterate over items from paginated search results.
+
+This utility function handles pagination automatically by calling the search
+function repeatedly with updated page_id parameters until all pages are
+exhausted.
+
+* Parameters:
+ * `search_func` – An async function that returns a PageProtocol[T] object
+ with ‘items’ and ‘next_page_id’ attributes
+ `args`** – Positional arguments to pass to the search function
+ `kwargs`* – Keyword arguments to pass to the search function
+* Yields:
+ Individual items of type T from each page
+
+#### Example
+
+```python
+async for event in page_iterator(event_service.search_events, limit=50):
+await send_event(event, websocket)
+
+async for conversation in page_iterator(
+conversation_service.search_conversations,
+ execution_status=ConversationExecutionStatus.RUNNING
+
+):
+print(conversation.title)
+```
+
+
### sanitize_openhands_mentions()
Sanitize @OpenHands mentions in text to prevent self-mention loops.
diff --git a/sdk/api-reference/openhands.sdk.workspace.mdx b/sdk/api-reference/openhands.sdk.workspace.mdx
index 48066655..1fe71180 100644
--- a/sdk/api-reference/openhands.sdk.workspace.mdx
+++ b/sdk/api-reference/openhands.sdk.workspace.mdx
@@ -4,6 +4,115 @@ description: API reference for openhands.sdk.workspace module
---
+
+---
+
+### class AsyncRemoteWorkspace
+
+Bases: `RemoteWorkspaceMixin`
+
+Async Remote Workspace Implementation.
+
+
+#### Properties
+
+- `alive`: bool
+ Check if the remote workspace is alive by querying the health endpoint.
+ * Returns:
+ True if the health endpoint returns a successful response, False otherwise.
+- `client`: AsyncClient
+
+#### Methods
+
+#### async execute_command()
+
+Execute a bash command on the remote system.
+
+This method starts a bash command via the remote agent server API,
+then polls for the output until the command completes.
+
+* Parameters:
+ * `command` – The bash command to execute
+ * `cwd` – Working directory (optional)
+ * `timeout` – Timeout in seconds
+* Returns:
+ Result with stdout, stderr, exit_code, and other metadata
+* Return type:
+ [CommandResult](#class-commandresult)
+
+#### async file_download()
+
+Download a file from the remote system.
+
+Requests the file from the remote system via HTTP API and saves it locally.
+
+* Parameters:
+ * `source_path` – Path to the source file on remote system
+ * `destination_path` – Path where the file should be saved locally
+* Returns:
+ Result with success status and metadata
+* Return type:
+ [FileOperationResult](#class-fileoperationresult)
+
+#### async file_upload()
+
+Upload a file to the remote system.
+
+Reads the local file and sends it to the remote system via HTTP API.
+
+* Parameters:
+ * `source_path` – Path to the local source file
+ * `destination_path` – Path where the file should be uploaded on remote system
+* Returns:
+ Result with success status and metadata
+* Return type:
+ [FileOperationResult](#class-fileoperationresult)
+
+#### async git_changes()
+
+Get the git changes for the repository at the path given.
+
+* Parameters:
+ `path` – Path to the git repository
+* Returns:
+ List of changes
+* Return type:
+ list[GitChange]
+* Raises:
+ `Exception` – If path is not a git repository or getting changes failed
+
+#### async git_diff()
+
+Get the git diff for the file at the path given.
+
+* Parameters:
+ `path` – Path to the file
+* Returns:
+ Git diff
+* Return type:
+ GitDiff
+* Raises:
+ `Exception` – If path is not a git repository or getting diff failed
+
+#### model_config = (configuration object)
+
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+#### model_post_init()
+
+Override this method to perform additional initialization after __init__ and model_construct.
+This is useful if you want to do some validation that requires the entire model to be initialized.
+
+#### async reset_client()
+
+Reset the HTTP client to force re-initialization.
+
+This is useful when connection parameters (host, api_key) have changed
+and the client needs to be recreated with new values.
+
+
+---
+
### class BaseWorkspace
Bases: `DiscriminatedUnionMixin`, `ABC`
@@ -123,6 +232,9 @@ For container-based workspaces, this resumes the container.
* Raises:
`NotImplementedError` – If the workspace type does not support resuming.
+
+---
+
### class CommandResult
Bases: `BaseModel`
@@ -144,6 +256,9 @@ Result of executing a command in the workspace.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+---
+
### class FileOperationResult
Bases: `BaseModel`
@@ -165,6 +280,9 @@ Result of a file upload or download operation.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+
+---
+
### class LocalWorkspace
Bases: [`BaseWorkspace`](#class-baseworkspace)
@@ -286,6 +404,9 @@ Resume the workspace (no-op for local workspaces).
Local workspaces have nothing to resume since they operate directly
on the host filesystem.
+
+---
+
### class RemoteWorkspace
Bases: `RemoteWorkspaceMixin`, [`BaseWorkspace`](#class-baseworkspace)
@@ -363,6 +484,16 @@ Reads the local file and sends it to the remote system via HTTP API.
* Return type:
[FileOperationResult](#class-fileoperationresult)
+#### get_server_info()
+
+Return server metadata from the agent-server.
+
+This is useful for debugging version mismatches between the local SDK and
+the remote agent-server image.
+
+* Returns:
+ A JSON-serializable dict returned by GET /server_info.
+
#### git_changes()
Get the git changes for the repository at the path given.
@@ -405,8 +536,14 @@ Reset the HTTP client to force re-initialization.
This is useful when connection parameters (host, api_key) have changed
and the client needs to be recreated with new values.
+
+---
+
### class Workspace
+
+---
+
### class Workspace
Bases: `object`