From c41754ee3c85356b35947c5c6e5af5870a03cf9a Mon Sep 17 00:00:00 2001 From: VascoSch92 Date: Fri, 6 Feb 2026 18:28:11 +0100 Subject: [PATCH 1/3] new api docs generator... --- .mintignore | 3 + .python-version | 1 + pyproject.toml | 10 + scripts/generate-api-docs.py | 1097 +++++--------- scripts/generate-api-docs.sh | 63 +- sdk/api-reference/openhands.sdk.agent.mdx | 208 ++- .../openhands.sdk.conversation.mdx | 1281 +++++++++-------- sdk/api-reference/openhands.sdk.event.mdx | 409 +++--- sdk/api-reference/openhands.sdk.llm.mdx | 957 ++++++------ sdk/api-reference/openhands.sdk.security.mdx | 53 +- sdk/api-reference/openhands.sdk.tool.mdx | 405 +++--- sdk/api-reference/openhands.sdk.utils.mdx | 79 +- sdk/api-reference/openhands.sdk.workspace.mdx | 484 +++---- uv.lock | 48 + 14 files changed, 2440 insertions(+), 2658 deletions(-) create mode 100644 .mintignore create mode 100644 .python-version create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/.mintignore b/.mintignore new file mode 100644 index 00000000..8f93358d --- /dev/null +++ b/.mintignore @@ -0,0 +1,3 @@ +agent-sdk/ +scripts/ +.venv/ diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..24ee5b1b --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..5417e5c5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "docs" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "griffe>=1.15.0", + "pip>=26.0.1", +] diff --git a/scripts/generate-api-docs.py b/scripts/generate-api-docs.py index c2110840..bdd09188 100755 --- a/scripts/generate-api-docs.py +++ b/scripts/generate-api-docs.py @@ -1,785 +1,354 @@ #!/usr/bin/env python3 -""" -Simple API documentation generator for OpenHands SDK. +"""API documentation generator for OpenHands SDK. -This script generates clean, parser-friendly markdown documentation -by extracting docstrings and presenting them in a simple format. +Uses griffe (mkdocstrings) to extract API information from Python source +and generates .mdx files for the documentation site. """ -import os -import re -import json -import shutil import logging +import re import subprocess +import sys from pathlib import Path -from typing import Dict, List, Any -# Configure logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') -logger = logging.getLogger(__name__) +import griffe -class SimpleAPIDocGenerator: - def __init__(self, docs_dir: Path): - self.docs_dir = docs_dir - self.agent_sdk_dir = docs_dir / "agent-sdk" - self.output_dir = docs_dir / "sdk" / "api-reference" - self.sphinx_dir = docs_dir / "scripts" / "sphinx" - - def run(self): - """Main execution method.""" - logger.info("Starting simple API documentation generation...") - - # Step 1: Setup agent-sdk repository - self.setup_agent_sdk() - - # Step 2: Fix MDX syntax issues in agent-sdk files - self.fix_agent_sdk_mdx_syntax() - - # Step 3: Install the SDK - self.install_sdk() - - # Step 4: Generate documentation using Sphinx - self.generate_sphinx_docs() - - # Step 5: Clean and simplify the generated markdown - self.clean_generated_docs() - - # Step 6: Update navigation - self.update_navigation() - - logger.info("API documentation generation completed successfully!") - - def setup_agent_sdk(self): - """Clone or update the agent-sdk repository.""" - if self.agent_sdk_dir.exists(): - logger.info("Updating existing agent-sdk repository...") - self.run_command(["git", "fetch", "origin"], cwd=self.agent_sdk_dir) - self.run_command(["git", "reset", "--hard", "origin/main"], cwd=self.agent_sdk_dir) - else: - logger.info("Cloning agent-sdk repository...") - self.run_command([ - "git", "clone", - "https://github.com/OpenHands/software-agent-sdk.git", - str(self.agent_sdk_dir) - ]) - - def install_sdk(self): - """Install the SDK package.""" - logger.info("Installing openhands-sdk package...") - sdk_path = self.agent_sdk_dir / "openhands-sdk" - self.run_command([ - "python", "-m", "pip", "install", "-e", str(sdk_path) - ]) - - def fix_agent_sdk_mdx_syntax(self): - """Fix MDX syntax issues in agent-sdk files to prevent Mintlify parsing errors.""" - logger.info("Fixing MDX syntax issues in agent-sdk files...") - - # Fix email addresses in AGENTS.md - agents_md = self.agent_sdk_dir / "AGENTS.md" - if agents_md.exists(): - content = agents_md.read_text() - # Fix unescaped @ symbols in email addresses - content = re.sub(r'<([^<>]*@[^<>]*)>', r'<\1>', content) - agents_md.write_text(content) - - # Fix README.md - readme_md = self.agent_sdk_dir / "README.md" - if readme_md.exists(): - content = readme_md.read_text() - # Convert HTML comments to JSX format - content = re.sub(r'', r'{/* \1 */}', content, flags=re.DOTALL) - # Fix self-closing tags - content = re.sub(r'<(img|br|hr)([^>]*?)(?', r'<\1\2 />', content) - readme_md.write_text(content) - - def generate_sphinx_docs(self): - """Generate documentation using Sphinx.""" - logger.info("Generating documentation with Sphinx...") - - # Create Sphinx configuration - self.create_sphinx_config() - - # Generate RST files - self.create_rst_files() - - # Build documentation - self.build_sphinx_docs() - - def create_sphinx_config(self): - """Create a simple Sphinx configuration.""" - sphinx_source = self.sphinx_dir / "source" - sphinx_source.mkdir(parents=True, exist_ok=True) - - conf_py = sphinx_source / "conf.py" - conf_py.write_text(''' -import os -import sys -sys.path.insert(0, os.path.abspath('../../../agent-sdk/openhands-sdk')) - -project = 'OpenHands SDK' -copyright = '2024, OpenHands' -author = 'OpenHands' +logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") +logger = logging.getLogger(__name__) -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.napoleon', - 'sphinx_markdown_builder', +DOCS_DIR = Path(__file__).parent.parent +AGENT_SDK_DIR = DOCS_DIR / "agent-sdk" +SDK_SRC = AGENT_SDK_DIR / "openhands-sdk" +OUTPUT_DIR = DOCS_DIR / "sdk" / "api-reference" + +MODULES = [ + "openhands.sdk.agent", + "openhands.sdk.conversation", + "openhands.sdk.event", + "openhands.sdk.llm", + "openhands.sdk.security", + "openhands.sdk.tool", + "openhands.sdk.utils", + "openhands.sdk.workspace", ] -autodoc_default_options = { - 'members': True, - 'undoc-members': True, - 'show-inheritance': True, - 'special-members': '__init__', -} - -napoleon_google_docstring = True -napoleon_numpy_docstring = True -napoleon_include_init_with_doc = False -napoleon_include_private_with_doc = False - -html_theme = 'sphinx_rtd_theme' -''') - - def create_rst_files(self): - """Create RST files for the main SDK modules.""" - sphinx_source = self.sphinx_dir / "source" - - # Main index file - index_rst = sphinx_source / "index.rst" - index_rst.write_text(''' -OpenHands SDK API Reference -=========================== - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - openhands.sdk - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` -''') - - # Main SDK module - sdk_rst = sphinx_source / "openhands.sdk.rst" - sdk_rst.write_text(''' -openhands.sdk package -===================== - -.. automodule:: openhands.sdk - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. 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 -''') - - # Generate RST files for each major module - modules = [ - 'agent', 'conversation', 'event', 'llm', - 'tool', 'workspace', 'security', 'utils' - ] - - for module in modules: - module_rst = sphinx_source / f"openhands.sdk.{module}.rst" - module_rst.write_text(f''' -openhands.sdk.{module} module -{'=' * (len(f'openhands.sdk.{module} module'))} - -.. automodule:: openhands.sdk.{module} - :members: - :undoc-members: - :show-inheritance: -''') - - def build_sphinx_docs(self): - """Build the Sphinx documentation.""" - build_dir = self.sphinx_dir / "build" - source_dir = self.sphinx_dir / "source" - - # Clean previous build - if build_dir.exists(): - shutil.rmtree(build_dir) - - # Build markdown documentation - self.run_command([ - "sphinx-build", "-b", "markdown", - str(source_dir), str(build_dir) - ]) - - def clean_generated_docs(self): - """Clean and simplify the generated markdown files.""" - logger.info("Cleaning generated documentation...") - - build_dir = self.sphinx_dir / "build" - - # Remove old output directory - if self.output_dir.exists(): - shutil.rmtree(self.output_dir) - self.output_dir.mkdir(parents=True, exist_ok=True) - - # Process each markdown file - for md_file in build_dir.glob("*.md"): - if md_file.name == "index.md": - continue - - # Skip the top-level openhands.sdk.md file as it duplicates content - if md_file.name == "openhands.sdk.md": - logger.info(f"Skipping {md_file.name} (top-level duplicate)") - continue - - logger.info(f"Processing {md_file.name}") - content = md_file.read_text() - - # Clean the content - cleaned_content = self.clean_markdown_content(content, md_file.name) - - # Write to output directory with .mdx extension - output_filename = md_file.name.replace('.md', '.mdx') - output_file = self.output_dir / output_filename - output_file.write_text(cleaned_content) - - def clean_multiline_dictionaries(self, content: str) -> str: - """Clean multi-line dictionary patterns that cause parsing issues.""" - import re - - # Handle the specific problematic pattern that keeps appearing - # Pattern: For example: {"Reasoning:": "bold blue",\n "Thought:": "bold green"} - pattern1 = r'For example: \{"[^"]*":\s*"[^"]*",\s*\n\s*"[^"]*":\s*"[^"]*"\}' - content = re.sub(pattern1, 'For example: (configuration dictionary)', content, flags=re.DOTALL) - - # More general multi-line dictionary patterns - pattern2 = r'\{"[^"]*":\s*"[^"]*",\s*\n\s*"[^"]*":\s*"[^"]*"\}' - content = re.sub(pattern2, '(configuration dictionary)', content, flags=re.DOTALL) - - # Handle any remaining multi-line patterns with curly braces - pattern3 = r'\{[^{}]*\n[^{}]*\}' - content = re.sub(pattern3, '(configuration object)', content, flags=re.DOTALL) - - return content - - def fix_header_hierarchy(self, content: str) -> str: - """Fix header hierarchy to ensure proper nesting under class headers.""" - import re - - lines = content.split('\n') - result_lines = [] - in_class_section = False - - for line in lines: - # Check if we're entering a class section - if re.match(r'^### class ', line): - in_class_section = True - result_lines.append(line) - # Check if we're leaving a class section (another class or module header) - elif line.startswith('### ') and not line.startswith('### class '): - # This is a non-class h3 header within a class section - convert to h4 - if in_class_section: - line = '#' + line # Convert ### to #### - result_lines.append(line) - # Check if we hit another class or end of content - elif re.match(r'^### class ', line) or line.startswith('# '): - in_class_section = line.startswith('### class ') - result_lines.append(line) - else: - result_lines.append(line) - - return '\n'.join(result_lines) - - def reorganize_class_content(self, content: str) -> str: - """Reorganize class content to separate properties from methods.""" - import re - - lines = content.split('\n') - result_lines = [] - i = 0 - - while i < len(lines): - line = lines[i] - - # Check if this is a class header - if re.match(r'^### \*class\*', line): - # Process this class - class_lines, i = self.process_class_section(lines, i) - result_lines.extend(class_lines) - else: - result_lines.append(line) - i += 1 - - return '\n'.join(result_lines) - - def process_class_section(self, lines: list[str], start_idx: int) -> tuple[list[str], int]: - """Process a single class section, separating properties from methods.""" - import re - - result = [] - i = start_idx - - # Add the class header and description (including any ### Example sections) - while i < len(lines): - line = lines[i] - # Stop when we hit the first #### (class member) or another class - if line.startswith('####') or (line.startswith('### *class*') and i > start_idx): - break - # Fix Example headers to be h4 instead of h3 - if line.startswith('### ') and not line.startswith('### *class*'): - line = '#' + line # Convert ### to #### - result.append(line) - i += 1 - - # Collect all class members - properties = [] - methods = [] - - while i < len(lines): - line = lines[i] - - # Stop if we hit another class or module (but not ### Example sections) - if line.startswith('### *class*'): - break - - if line.startswith('####'): - # Determine if this is a property or method - member_lines, i = self.extract_member_section(lines, i) - - if self.is_property(member_lines[0]): - properties.extend(member_lines) - else: - methods.extend(member_lines) - else: - i += 1 - - # Add properties section if we have any - if properties: - result.append('') - result.append('#### Properties') - result.append('') - - # Convert property headers to list items - for prop_line in properties: - if prop_line.startswith('####'): - # Extract property name and type - prop_match = re.match(r'^####\s*([^*:]+)\s*\*?:?\s*(.*)$', prop_line) - if prop_match: - prop_name = prop_match.group(1).strip() - prop_type = prop_match.group(2).strip() - # Clean up the type annotation - prop_type = re.sub(r'^\*\s*', '', prop_type) # Remove leading * - prop_type = re.sub(r'\s*\*$', '', prop_type) # Remove trailing * - if prop_type: - result.append(f'- `{prop_name}`: {prop_type}') - else: - result.append(f'- `{prop_name}`') - elif prop_line.strip() and not prop_line.startswith('####'): - # Add description lines indented - result.append(f' {prop_line}') - - # Add methods section if we have any - if methods: - if properties: # Add spacing if we had properties - result.append('') - result.append('#### Methods') - result.append('') - result.extend(methods) - - return result, i - - def extract_member_section(self, lines: list[str], start_idx: int) -> tuple[list[str], int]: - """Extract all lines belonging to a single class member.""" - result = [] - i = start_idx - - # Add the header line - result.append(lines[i]) - i += 1 - - # Add all following lines until we hit another header or class - while i < len(lines): - line = lines[i] - if line.startswith('####') or line.startswith('###'): - break - result.append(line) - i += 1 - - return result, i - - def is_property(self, header_line: str) -> bool: - """Determine if a class member is a property or method.""" - import re - - # Properties typically have type annotations with *: type* pattern - if re.search(r'\*:\s*[^*]+\*', header_line): - return True - - # Methods have parentheses - if '(' in header_line and ')' in header_line: - return False - - # Properties often have : followed by type info - if ':' in header_line and not '(' in header_line: - return True - - # Default to method if unclear - return False - - def clean_markdown_content(self, content: str, filename: str) -> str: - """Clean markdown content to be parser-friendly.""" - # First handle multi-line dictionary patterns - content = self.clean_multiline_dictionaries(content) - - # Reorganize class content to separate properties from methods - content = self.reorganize_class_content(content) - - # Fix header hierarchy (Example sections should be h4 under class headers) - content = self.fix_header_hierarchy(content) - - lines = content.split('\n') - cleaned_lines = [] - - for line in lines: - # Skip empty lines and sphinx-specific content - if not line.strip(): - cleaned_lines.append(line) - continue - - # Clean headers - remove complex signatures, keep just names - if line.startswith('#'): - line = self.clean_header(line) - - # Skip module headers that duplicate the title - if line.startswith('# ') and ' module' in line: + +# --------------------------------------------------------------------------- +# Setup +# --------------------------------------------------------------------------- + +def setup_agent_sdk(): + """Clone or update the agent-sdk repository.""" + if AGENT_SDK_DIR.exists(): + logger.info("Updating agent-sdk...") + subprocess.run( + ["git", "fetch", "origin"], + cwd=AGENT_SDK_DIR, check=True, capture_output=True, + ) + subprocess.run( + ["git", "reset", "--hard", "origin/main"], + cwd=AGENT_SDK_DIR, check=True, capture_output=True, + ) + else: + logger.info("Cloning agent-sdk...") + subprocess.run( + ["git", "clone", + "https://github.com/OpenHands/software-agent-sdk.git", + str(AGENT_SDK_DIR)], + check=True, capture_output=True, + ) + + +def install_sdk(): + """Install the SDK package.""" + logger.info("Installing openhands-sdk...") + subprocess.run( + [sys.executable, "-m", "pip", "install", "-e", str(SDK_SRC)], + check=True, capture_output=True, + ) + + +# --------------------------------------------------------------------------- +# Rendering helpers +# --------------------------------------------------------------------------- + +def _escape_mdx(content: str) -> str: + """Escape characters that MDX would try to parse as JSX. + + Curly braces and bare patterns inside prose are replaced + so that Mintlify's MDX compiler does not choke on them. Content + inside fenced code blocks (``` ... ```) is left untouched. + """ + out_lines: list[str] = [] + in_code_block = False + + for line in content.split("\n"): + if line.startswith("```"): + in_code_block = not in_code_block + out_lines.append(line) + continue + + if in_code_block: + out_lines.append(line) + continue + + # Escape { and } outside of inline code spans + escaped = "" + in_backtick = False + for ch in line: + if ch == "`": + in_backtick = not in_backtick + if not in_backtick: + if ch == "{": + escaped += "\\{" continue - - # Remove problematic patterns - line = self.remove_problematic_patterns(line) - - cleaned_lines.append(line) - - # Add frontmatter - module_name = filename.replace('.md', '') - frontmatter = f'''--- -title: {module_name} -description: API reference for {module_name} module ---- - -''' - - return frontmatter + '\n'.join(cleaned_lines) - - def clean_header(self, line: str) -> str: - """Clean header lines to contain only class/method names.""" - # Extract just the class or method name from complex signatures - - # Pattern for class headers: "### *class* ClassName(...)" or "### class ClassName(...)" - class_match = re.match(r'^(#+)\s*\*?class\*?\s+([^(]+)', line) - if class_match: - level, class_name = class_match.groups() - # Extract just the class name (last part after the last dot) for readability - simple_class_name = class_name.strip().split('.')[-1] - return f"{level} class {simple_class_name}" - - # Pattern for method headers: "#### method_name(...)" - method_match = re.match(r'^(#+)\s*([^(]+)\(', line) - if method_match: - level, method_name = method_match.groups() - # Clean up the method name - method_name = method_name.strip().split('.')[-1] # Get just the method name - # Remove any decorators or prefixes - method_name = re.sub(r'^(static|class|abstract|property)\s+', '', method_name) - return f"{level} {method_name}()" - - # Pattern for property headers: "#### property property_name" - prop_match = re.match(r'^(#+)\s*property\s+([^:]+)', line) - if prop_match: - level, prop_name = prop_match.groups() - prop_name = prop_name.strip() - return f"{level} {prop_name}" - - # For other headers, just clean up basic formatting - line = re.sub(r'\*([^*]+)\*', r'\1', line) # Remove emphasis - return line - - def remove_problematic_patterns(self, line: str) -> str: - """Remove patterns that cause parsing issues.""" - # Remove all emphasis and bold formatting - line = re.sub(r'\*\*([^*]+)\*\*', r'\1', line) # Remove bold - line = re.sub(r'\*([^*]+)\*', r'\1', line) # Remove emphasis - - # Fix HTML-like tags (only actual HTML tags, not all < > characters) - # 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 - - # Remove escaped characters that cause issues - line = line.replace('\\*', '*') - line = line.replace('\\', '') - - # Fix dictionary/object literals that cause parsing issues - # Pattern: = {'key': 'value', 'key2': 'value2'} or = {} - if ' = {' in line and '}' in line: - # Replace with a simple description - line = re.sub(r' = \{[^}]*\}', ' = (configuration object)', line) - - # Fix JSON-like patterns that cause parsing issues - # Pattern: { "type": "function", "name": …, "description": …, "parameters": … } - if line.strip().startswith('{') and line.strip().endswith('}'): - # Replace with a simple description - line = '(JSON configuration object)' - - # Fix specific problematic dictionary patterns - if '{"Reasoning:": "bold blue",' in line or '"Thought:": "bold green"}' in line: - # Replace the entire line with a simple description - line = re.sub(r'.*\{"[^"]*":[^}]*\}.*', ' For example: (configuration dictionary)', line) - - # Fix ClassVar patterns - line = re.sub(r'ClassVar\[([^\]]+)\]', r'ClassVar[\1]', line) - - # Fix template string patterns like ${variable} - line = re.sub(r'\$\{[^}]+\}', '(variable)', line) - - # Fix asterisk in type annotations like "property name *: Type" - line = re.sub(r' \*:', ':', line) - - # Fix any remaining curly braces that cause parsing issues - if '{' in line and '}' in line: - line = re.sub(r'\{[^}]*\}', '(configuration object)', line) - - # Note: All cross-reference link conversion logic removed - we now just strip links entirely - class_to_module = { - 'Agent': 'agent', - 'AgentBase': 'agent', - 'AgentContext': 'agent', - 'Conversation': 'conversation', - 'BaseConversation': 'conversation', - 'LocalConversation': 'conversation', - 'RemoteConversation': 'conversation', - 'ConversationState': 'conversation', - 'ConversationStats': 'conversation', - 'Event': 'event', - 'LLMConvertibleEvent': 'event', - 'MessageEvent': 'event', - 'LLM': 'llm', - 'LLMRegistry': 'llm', - 'LLMResponse': 'llm', - 'Message': 'llm', - 'ImageContent': 'llm', - 'TextContent': 'llm', - 'ThinkingBlock': 'llm', - 'RedactedThinkingBlock': 'llm', - 'Metrics': 'llm', - 'RegistryEvent': 'llm', - 'SecurityManager': 'security', - 'Tool': 'tool', - 'ToolDefinition': 'tool', - 'Action': 'tool', - 'Observation': 'tool', - 'Workspace': 'workspace', - 'BaseWorkspace': 'workspace', - 'LocalWorkspace': 'workspace', - 'RemoteWorkspace': 'workspace', - 'WorkspaceFile': 'workspace', - 'WorkspaceFileEdit': 'workspace', - 'WorkspaceFileEditResult': 'workspace', - 'WorkspaceFileReadResult': 'workspace', - 'WorkspaceFileWriteResult': 'workspace', - 'WorkspaceListResult': 'workspace', - 'WorkspaceSearchResult': 'workspace', - 'WorkspaceSearchResultItem': 'workspace', - 'WorkspaceUploadResult': 'workspace', - 'WorkspaceWriteResult': 'workspace', - } - - # Fix anchor links - convert full module path anchors to simple class format - # Pattern: openhands.sdk.module.mdx#openhands.sdk.module.ClassName -> openhands.sdk.module#class-classname - def convert_anchor(match): - module_path = match.group(1) - full_class_path = match.group(2) - class_name = full_class_path.split('.')[-1].lower() - return f'openhands.sdk.{module_path}#class-{class_name}' - - line = re.sub(r'openhands\.sdk\.([^)#]+)\.mdx#openhands\.sdk\.\1\.([^)]+)', convert_anchor, line) - - # Also handle the .md# pattern before converting to .mdx - line = re.sub(r'openhands\.sdk\.([^)#]+)\.md#openhands\.sdk\.\1\.([^)]+)', convert_anchor, line) - - # Fix links pointing to the removed top-level openhands.sdk.md page - # Pattern: openhands.sdk.md#openhands.sdk.ClassName -> openhands.sdk.module#class-classname - def convert_toplevel_anchor(match): - full_class_path = match.group(1) - class_name = full_class_path.split('.')[-1] - - # Find the correct module for this class - if class_name in class_to_module: - module = class_to_module[class_name] - class_name_lower = class_name.lower() - return f'openhands.sdk.{module}#class-{class_name_lower}' - else: - # Fallback: try to guess module from class name - class_name_lower = class_name.lower() - return f'openhands.sdk.{class_name_lower}#class-{class_name_lower}' - - line = re.sub(r'openhands\.sdk\.md#openhands\.sdk\.([^)]+)', convert_toplevel_anchor, line) - - # Fix same-file anchor references (e.g., #openhands.sdk.llm.LLM -> #class-llm) - def convert_same_file_anchor(match): - full_class_path = match.group(1) - class_name = full_class_path.split('.')[-1].lower() - return f'#class-{class_name}' - - line = re.sub(r'#openhands\.sdk\.[^.]+\.([^)]+)', convert_same_file_anchor, line) - - # Fix invalid http:// links - line = re.sub(r'\[http://\]\(http://\)', 'http://', line) - - # Remove Python console prompt prefixes from examples - line = re.sub(r'^>`>`>` ', '', line) - - # Remove all cross-reference links - just keep the class names as plain text - # Pattern: [ClassName](openhands.sdk.module#class-classname) -> ClassName - line = re.sub(r'\[([^\]]+)\]\(openhands\.sdk\.[^)]+\)', r'\1', line) - - # Clean up malformed property entries with empty names - if '- ``:' in line and 'property ' in line: - # Extract the property name and type from malformed entries like: - # - ``: property service_to_llm : dict[str, [LLM](#openhands.sdk.llm.LLM)] - # - ``: abstract property conversation_stats : ConversationStats - match = re.search(r'- ``: (?:abstract )?property (\w+) : (.+)', line) - if match: - prop_name = match.group(1) - prop_type = match.group(2) - line = f'- `{prop_name}`: {prop_type}' - - # Format parameter names in backticks for parameter lists - # Pattern: " parameter_name – Description" -> " `parameter_name` – Description" - if line.strip().startswith('* ') or (line.startswith(' ') and ' – ' in line): - # This looks like a parameter line in a parameter list - # Match pattern: " * parameter_name – description" or " parameter_name – description" - param_match = re.match(r'^(\s*\*?\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*–\s*(.+)$', line) - if param_match: - indent = param_match.group(1) - param_name = param_match.group(2) - description = param_match.group(3) - line = f'{indent}`{param_name}` – {description}' - - return line - - def update_navigation(self): - """Update the navigation configuration.""" - logger.info("Updating navigation configuration...") - - # Generate navigation entries for all API files - api_files = list(self.output_dir.glob("*.mdx")) - nav_entries = [] - - for api_file in sorted(api_files): - module_name = api_file.stem - nav_entries.append(f'"sdk/api-reference/{module_name}"') - - # Create navigation snippet - nav_config = { - "navigation": [ - { - "group": "API Reference", - "pages": [entry.strip('"') for entry in nav_entries] - } - ] - } - - # Save navigation snippet - nav_file = self.docs_dir / "scripts" / "mint-config-snippet.json" - nav_file.write_text(json.dumps(nav_config, indent=2)) - - # Also update the main docs.json file - self.update_main_docs_json([entry.strip('"') for entry in nav_entries]) - - logger.info(f"Generated navigation for {len(nav_entries)} API reference files") - - def update_main_docs_json(self, nav_entries): - """Update the main docs.json file with the new API reference navigation.""" - docs_json_path = self.docs_dir / "docs.json" - - if not docs_json_path.exists(): - logger.warning("docs.json not found, skipping main navigation update") - return - - try: - with open(docs_json_path, 'r') as f: - docs_config = json.load(f) - - # Find and update the API Reference section - updated = False - for tab in docs_config.get("navigation", {}).get("tabs", []): - if tab.get("tab") == "SDK": - for page in tab.get("pages", []): - if isinstance(page, dict) and page.get("group") == "API Reference": - page["pages"] = nav_entries - updated = True - logger.info("Updated API Reference navigation in docs.json") - break - if updated: - break - - if updated: - with open(docs_json_path, 'w') as f: - json.dump(docs_config, f, indent=2) + if ch == "}": + escaped += "\\}" + continue + escaped += ch + # Escape bare patterns that MDX reads as HTML/JSX tags + escaped = re.sub(r"<([a-zA-Z][a-zA-Z0-9._@:/-]*)>", r"`<\1>`", escaped) + out_lines.append(escaped) + + return "\n".join(out_lines) + + +def _fmt(annotation) -> str: + """Format a type annotation as a string.""" + if annotation is None: + return "" + return str(annotation) + + +def _has_decorator(obj, name: str) -> bool: + """Check if a griffe object has a decorator containing *name*.""" + return any(name in str(d.value) for d in getattr(obj, "decorators", [])) + + +def _render_docstring(docstring: griffe.Docstring | None) -> list[str]: + """Parse a Google-style docstring into markdown lines.""" + if not docstring: + return [] + + lines: list[str] = [] + for section in docstring.parse("google"): + kind = section.kind + + if kind == griffe.DocstringSectionKind.text: + lines.append(section.value) + lines.append("") + + elif kind == griffe.DocstringSectionKind.parameters: + lines.append("**Parameters:**") + lines.append("") + for p in section.value: + ann = f" *{p.annotation}*" if p.annotation else "" + desc = f" – {p.description}" if p.description else "" + lines.append(f"- `{p.name}`{ann}{desc}") + lines.append("") + + elif kind == griffe.DocstringSectionKind.returns: + lines.append("**Returns:**") + lines.append("") + for r in section.value: + ann = f"*{r.annotation}* " if r.annotation else "" + lines.append(f"- {ann}{r.description or ''}".strip()) + lines.append("") + + elif kind == griffe.DocstringSectionKind.raises: + lines.append("**Raises:**") + lines.append("") + for e in section.value: + lines.append(f"- `{e.annotation}` – {e.description}") + lines.append("") + + elif kind == griffe.DocstringSectionKind.examples: + lines.append("**Example:**") + lines.append("") + for item in section.value: + if isinstance(item, tuple): + item_kind, value = item + if item_kind == "text": + lines.append(value) + else: + lines.append("```python") + lines.append(value) + lines.append("```") + lines.append("") + + return lines + + +def _field_description(attr: griffe.Attribute) -> str: + """Extract the description= keyword from a Field(...) value.""" + if attr.value is None: + return "" + match = re.search(r"description=['\"]([^'\"]+)['\"]", str(attr.value)) + return match.group(1) if match else "" + + +def _render_property_line(member) -> str: + """Render a property or attribute as a single bullet line.""" + if isinstance(member, griffe.Function): + type_str = f": {_fmt(member.returns)}" if member.returns else "" + desc = "" + if member.docstring: + for s in member.docstring.parse("google"): + if s.kind == griffe.DocstringSectionKind.text: + desc = f"\n {s.value}" + break + return f"- `{member.name}`{type_str}{desc}" + + # griffe.Attribute + type_str = f": {_fmt(member.annotation)}" if member.annotation else "" + desc = "" + if member.docstring: + desc = member.docstring.value + else: + desc = _field_description(member) + if desc: + desc = f"\n {desc}" + return f"- `{member.name}`{type_str}{desc}" + + +def _render_function(func: griffe.Function, level: int = 4) -> list[str]: + """Render a function/method as markdown.""" + hdr = "#" * level + lines: list[str] = [] + + # Build parameter signature + params: list[str] = [] + for p in func.parameters: + if p.name in ("self", "cls"): + continue + s = p.name + if p.annotation: + s += f": {_fmt(p.annotation)}" + if p.default is not None: + s += f" = {p.default}" + params.append(s) + + sig = ", ".join(params) + ret = f" -> {_fmt(func.returns)}" if func.returns else "" + abstract = "abstractmethod " if _has_decorator(func, "abstractmethod") else "" + + lines.append(f"**{abstract}{func.name}({sig}){ret}**") + lines.append("") + lines.extend(_render_docstring(func.docstring)) + return lines + + +def _render_class(cls: griffe.Class) -> list[str]: + """Render a class as markdown.""" + lines: list[str] = [] + + lines.append(f"## class {cls.name}") + lines.append("") + if cls.bases: + bases = ", ".join(f"`{b}`" for b in cls.bases) + lines.append(f"Bases: {bases}") + lines.append("") + + lines.extend(_render_docstring(cls.docstring)) + + # Collect public members + properties: list = [] + methods: list[griffe.Function] = [] + + for name, member in cls.members.items(): + if name.startswith("_") and name != "__init__": + continue + member = _resolve(member) + if member is None: + continue + + if isinstance(member, griffe.Attribute): + properties.append(member) + elif isinstance(member, griffe.Function): + if _has_decorator(member, "property"): + properties.append(member) else: - logger.warning("Could not find API Reference section in docs.json to update") - - except Exception as e: - logger.error(f"Error updating docs.json: {e}") - - def run_command(self, cmd: List[str], cwd: Path = None): - """Run a shell command with error handling.""" + methods.append(member) + + if properties: + lines.append("### Properties") + lines.append("") + for prop in properties: + lines.append(_render_property_line(prop)) + lines.append("") + + if methods: + lines.append("### Methods") + lines.append("") + for method in methods: + lines.extend(_render_function(method, level=3)) + + return lines + + +def _resolve(member): + """Resolve a griffe Alias to its final target.""" + if isinstance(member, griffe.Alias): try: - result = subprocess.run( - cmd, - cwd=cwd or self.docs_dir, - capture_output=True, - text=True, - check=True - ) - if result.stdout: - logger.debug(f"STDOUT: {result.stdout}") - if result.stderr: - logger.warning(f"STDERR: {result.stderr}") - except subprocess.CalledProcessError as e: - logger.error(f"Command failed: {' '.join(cmd)}") - logger.error(f"Exit code: {e.returncode}") - logger.error(f"STDOUT: {e.stdout}") - logger.error(f"STDERR: {e.stderr}") - raise + return member.final_target + except Exception: + return None + return member + + +# --------------------------------------------------------------------------- +# Module generation +# --------------------------------------------------------------------------- + +def generate_module_mdx(module_path: str) -> str: + """Load a module with griffe and render it as .mdx.""" + module = griffe.load(module_path, search_paths=[str(SDK_SRC)]) + + lines = [ + "---", + f"title: {module_path}", + f"description: API reference for {module_path}", + "---", + "", + ] + + for name, member in module.members.items(): + if name.startswith("_"): + continue + + obj = _resolve(member) + if obj is None: + continue + if isinstance(obj, griffe.Class): + lines.extend(_render_class(obj)) + elif isinstance(obj, griffe.Function): + lines.extend(_render_function(obj, level=2)) + + return _escape_mdx("\n".join(lines)) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- def main(): - """Main entry point.""" - docs_dir = Path(__file__).parent.parent - generator = SimpleAPIDocGenerator(docs_dir) - generator.run() + setup_agent_sdk() + install_sdk() + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + + for module_path in MODULES: + logger.info(f"Generating {module_path}...") + try: + content = generate_module_mdx(module_path) + output_file = OUTPUT_DIR / f"{module_path}.mdx" + output_file.write_text(content) + except Exception as e: + logger.error(f" Failed to generate {module_path}: {e}") + + logger.info("Done!") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/scripts/generate-api-docs.sh b/scripts/generate-api-docs.sh index c9d10f95..b9f69beb 100755 --- a/scripts/generate-api-docs.sh +++ b/scripts/generate-api-docs.sh @@ -1,77 +1,30 @@ #!/bin/bash -# API Documentation Generation Script (Shell Version) -# -# This is a simple shell wrapper around the Python script for convenience. -# For full functionality and error handling, use the Python version. +# API Documentation Generation Script +# +# Generates .mdx API reference files using griffe (mkdocstrings). set -e -# Get the directory of this script SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DOCS_ROOT="$(dirname "$SCRIPT_DIR")" -# Change to docs directory cd "$DOCS_ROOT" -# Check if Python script exists if [ ! -f "scripts/generate-api-docs.py" ]; then echo "Error: Python script not found at scripts/generate-api-docs.py" exit 1 fi -# Check if required packages are installed echo "Checking dependencies..." -python3 -c "import sphinx, sphinx_markdown_builder, myst_parser" 2>/dev/null || { - echo "Error: Required packages not installed." - echo "Please install them with: pip install sphinx sphinx-markdown-builder myst-parser" +python3 -c "import griffe" 2>/dev/null || { + echo "Error: griffe not installed." + echo "Please install it with: pip install griffe" exit 1 } -# Parse command line arguments -CLEAN="" -VERBOSE="" - -while [[ $# -gt 0 ]]; do - case $1 in - --clean) - CLEAN="--clean" - shift - ;; - --verbose|-v) - VERBOSE="--verbose" - shift - ;; - -h|--help) - echo "Usage: $0 [--clean] [--verbose]" - echo "" - echo "Options:" - echo " --clean Clean previous build artifacts before generating" - echo " --verbose Enable verbose output" - echo " --help Show this help message" - echo "" - echo "This script generates API reference documentation from the OpenHands SDK." - echo "Generated files will be placed in the sdk/api-reference/ directory." - exit 0 - ;; - *) - echo "Unknown option: $1" - echo "Use --help for usage information." - exit 1 - ;; - esac -done - -# Run the Python script echo "Generating API documentation..." -python3 scripts/generate-api-docs.py $CLEAN $VERBOSE +python3 scripts/generate-api-docs.py echo "" -echo "✅ API documentation generation completed!" -echo "📁 Generated files are in: sdk/api-reference/" -echo "⚙️ Mint.json config snippet: scripts/mint-config-snippet.json" -echo "" -echo "Next steps:" -echo "1. Review the generated documentation in sdk/api-reference/" -echo "2. Copy the configuration from scripts/mint-config-snippet.json" -echo "3. Add it to your docs.json navigation structure" \ No newline at end of file +echo "Done! Generated files are in: sdk/api-reference/" diff --git a/sdk/api-reference/openhands.sdk.agent.mdx b/sdk/api-reference/openhands.sdk.agent.mdx index f55127f3..cc71ad84 100644 --- a/sdk/api-reference/openhands.sdk.agent.mdx +++ b/sdk/api-reference/openhands.sdk.agent.mdx @@ -1,12 +1,11 @@ --- title: openhands.sdk.agent -description: API reference for openhands.sdk.agent module +description: API reference for openhands.sdk.agent --- +## class Agent -### class Agent - -Bases: [`AgentBase`](#class-agentbase) +Bases: `AgentBase` Main agent implementation for OpenHands. @@ -14,62 +13,25 @@ The Agent class provides the core functionality for running AI agents that can interact with tools, process messages, and execute actions. It inherits from AgentBase and implements the agent execution logic. -#### Example - -```pycon ->>> from openhands.sdk import LLM, Agent, Tool ->>> llm = LLM(model="claude-sonnet-4-20250514", api_key=SecretStr("key")) ->>> tools = [Tool(name="TerminalTool"), Tool(name="FileEditorTool")] ->>> agent = Agent(llm=llm, tools=tools) -``` - - -#### Properties - -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. - -#### Methods - -#### init_state() - -Initialize the empty conversation state to prepare the agent for user -messages. - -Typically this involves adding system message - -NOTE: state will be mutated in-place. - -#### model_post_init() +### Methods -This function is meant to behave like a BaseModel method to initialise private attributes. +**init_state(state: ConversationState, on_event: ConversationCallbackType) -> None** -It takes context as an argument since that’s what pydantic-core passes when calling it. +Initialize conversation state. -* Parameters: - * `self` – The BaseModel instance. - * `context` – The context. +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). +- A user MessageEvent should not appear before the SystemPromptEvent. -#### step() +These invariants keep event ordering predictable for downstream components +(condenser, UI, etc.) and also prevent accidentally materializing the full +event history during initialization. -Taking a step in the conversation. +**step(conversation: LocalConversation, on_event: ConversationCallbackType, on_token: ConversationTokenCallbackType | None = None) -> None** -Typically this involves: -1. Making a LLM call -2. Executing the tool -3. Updating the conversation state with - - 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 - -If the underlying LLM supports streaming, partial deltas are forwarded to -`on_token` before the full response is returned. - -NOTE: state will be mutated in-place. - -### class AgentBase +## class AgentBase Bases: `DiscriminatedUnionMixin`, `ABC` @@ -79,45 +41,45 @@ Agents are stateless and should be fully defined by their configuration. This base class provides the common interface and functionality that all agent implementations must follow. +### Properties -#### Properties - +- `model_config` +- `llm`: LLM + LLM configuration for the agent. +- `tools`: list[Tool] + List of tools to initialize for the agent. +- `mcp_config`: dict[str, Any] + Optional MCP configuration dictionary to create MCP tools. +- `filter_tools_regex`: str | None + Optional regex to filter the tools available to the agent by name. This is applied after any tools provided in `tools` and any MCP tools are added. +- `include_default_tools`: list[str] + List of default tool class names to include. By default, the agent includes - `agent_context`: AgentContext | None + Optional AgentContext to initialize the agent with specific context. +- `system_prompt_filename`: str + System prompt template filename. Can be either:\n- A relative filename (e.g., +- `security_policy_filename`: str + Security policy template filename. Can be either:\n- A relative filename (e.g., +- `system_prompt_kwargs`: dict[str, object] + Optional kwargs to pass to the system prompt Jinja2 template. - `condenser`: CondenserBase | None + Optional condenser to use for condensing conversation history. - `critic`: CriticBase | None -- `filter_tools_regex`: str | None -- `include_default_tools`: list[str] -- `llm`: LLM -- `mcp_config`: dict[str, Any] -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + EXPERIMENTAL: Optional critic to evaluate agent actions and messages in real-time. API and behavior may change without notice. May impact performance, especially in +- `prompt_dir`: str + Returns the directory where this class's module file is located. - `name`: str Returns the name of the Agent. -- `prompt_dir`: str - Returns the directory where this class’s module file is located. -- `security_policy_filename`: str - `system_message`: str Compute system message on-demand to maintain statelessness. -- `system_prompt_filename`: str -- `system_prompt_kwargs`: dict[str, object] -- `tools`: list[Tool] -- `tools_map`: dictstr, [ToolDefinition] +- `tools_map`: dict[str, ToolDefinition] Get the initialized tools map. - :raises RuntimeError: If the agent has not been initialized. - -#### Methods +Raises: + RuntimeError: If the agent has not been initialized. -#### get_all_llms() +### Methods -Recursively yield unique base-class LLM objects reachable from self. - -- Returns actual object references (not copies). -- De-dupes by id(LLM). -- Cycle-safe via a visited set for all traversed objects. -- Only yields objects whose type is exactly LLM (no subclasses). -- Does not handle dataclasses. - -#### init_state() +**init_state(state: ConversationState, on_event: ConversationCallbackType) -> None** Initialize the empty conversation state to prepare the agent for user messages. @@ -126,21 +88,7 @@ Typically this involves adding system message NOTE: state will be mutated in-place. -#### model_dump_succint() - -Like model_dump, but excludes None fields by default. - -#### 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. - -#### abstractmethod step() +**abstractmethod step(conversation: LocalConversation, on_event: ConversationCallbackType, on_token: ConversationTokenCallbackType | None = None) -> None** Taking a step in the conversation. @@ -148,41 +96,57 @@ Typically this involves: 1. Making a LLM call 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 If the underlying LLM supports streaming, partial deltas are forwarded to -`on_token` before the full response is returned. +``on_token`` before the full response is returned. NOTE: state will be mutated in-place. -#### verify() +**verify(persisted: AgentBase, events: Sequence[Any] | None = None) -> AgentBase** Verify that we can resume this agent from persisted state. -This PR’s goal is to not reconcile configuration between persisted and -runtime Agent instances. Instead, we verify compatibility requirements -and then continue with the runtime-provided Agent. +We do not merge configuration between persisted and runtime Agent +instances. Instead, we verify compatibility requirements and then +continue with the runtime-provided Agent. Compatibility requirements: - Agent class/type must match. -- Tools: - - - If events are provided, only tools that were actually used in history - must exist in runtime. - - If events are not provided, tool names must match exactly. - -All other configuration (LLM, agent_context, condenser, system prompts, -etc.) can be freely changed between sessions. - -* Parameters: - * `persisted` – The agent loaded from persisted state. - * `events` – Optional event sequence to scan for used tools if tool names - don’t match. -* Returns: - This runtime agent (self) if verification passes. -* Raises: - `ValueError` – If agent class or tools don’t match. +- Tools must match exactly (same tool names). + +Tools are part of the system prompt and cannot be changed mid-conversation. +To use different tools, start a new conversation or use conversation forking +(see https://github.com/OpenHands/OpenHands/issues/8560). + +All other configuration (LLM, agent_context, condenser, etc.) can be +freely changed between sessions. + +**Parameters:** + +- `persisted` *AgentBase* – The agent loaded from persisted state. +- `events` *Sequence[Any] | None* – Unused, kept for API compatibility. + +**Returns:** + +- *AgentBase* This runtime agent (self) if verification passes. + +**Raises:** + +- `ValueError` – If agent class or tools don't match. + +**model_dump_succint(kwargs = \{\})** + +Like model_dump, but excludes None fields by default. + +**get_all_llms() -> Generator[LLM, None, None]** + +Recursively yield unique *base-class* LLM objects reachable from `self`. + +- Returns actual object references (not copies). +- De-dupes by `id(LLM)`. +- Cycle-safe via a visited set for *all* traversed objects. +- Only yields objects whose type is exactly `LLM` (no subclasses). +- Does not handle dataclasses. diff --git a/sdk/api-reference/openhands.sdk.conversation.mdx b/sdk/api-reference/openhands.sdk.conversation.mdx index ee27a282..359bb910 100644 --- a/sdk/api-reference/openhands.sdk.conversation.mdx +++ b/sdk/api-reference/openhands.sdk.conversation.mdx @@ -1,10 +1,9 @@ --- title: openhands.sdk.conversation -description: API reference for openhands.sdk.conversation module +description: API reference for openhands.sdk.conversation --- - -### class BaseConversation +## class BaseConversation Bases: `ABC` @@ -14,52 +13,113 @@ This class defines the interface that all conversation implementations must foll Conversations manage the interaction between users and agents, handling message exchange, execution control, and state management. +### Properties -#### Properties - -- `confirmation_policy_active`: bool +- `id`: ConversationID +- `state`: ConversationStateProtocol - `conversation_stats`: ConversationStats -- `id`: UUID +- `confirmation_policy_active`: bool - `is_confirmation_mode_active`: bool Check if confirmation mode is active. - Returns True if BOTH conditions are met: - 1. The conversation state has a security analyzer set (not None) - 2. The confirmation policy is active -- `state`: ConversationStateProtocol -#### Methods +Returns True if BOTH conditions are met: +1. The conversation state has a security analyzer set (not None) +2. The confirmation policy is active + +### Methods -#### __init__() +**__init__() -> None** Initialize the base conversation with span tracking. -#### abstractmethod ask_agent() +**abstractmethod send_message(message: str | Message, sender: str | None = None) -> None** + +Send a message to the agent. + +**Parameters:** + +- `message` *str | Message* – Either a string (which will be converted to a user message) + or a Message object +- `sender` *str | None* – Optional identifier of the sender. Can be used to track + message origin in multi-agent scenarios. For example, when + one agent delegates to another, the sender can be set to + identify which agent is sending the message. + +**abstractmethod run() -> None** + +Execute the agent to process messages and perform actions. + +This method runs the agent until it finishes processing the current +message or reaches the maximum iteration limit. + +**abstractmethod set_confirmation_policy(policy: ConfirmationPolicyBase) -> None** + +Set the confirmation policy for the conversation. + +**abstractmethod set_security_analyzer(analyzer: SecurityAnalyzerBase | None) -> None** + +Set the security analyzer for the conversation. + +**abstractmethod reject_pending_actions(reason: str = 'User rejected the action') -> None** + +**abstractmethod pause() -> None** + +**abstractmethod update_secrets(secrets: Mapping[str, SecretValue]) -> None** + +**abstractmethod close() -> None** + +**abstractmethod generate_title(llm: LLM | None = None, max_length: int = 50) -> str** + +Generate a title for the conversation based on the first user message. + +**Parameters:** + +- `llm` *LLM | None* – Optional LLM to use for title generation. If not provided, + uses the agent's LLM. +- `max_length` *int* – Maximum length of the generated title. + +**Returns:** + +- *str* A generated title for the conversation. + +**Raises:** + +- `ValueError` – If no user messages are found in the conversation. + +**get_persistence_dir(persistence_base_dir: str | Path, conversation_id: ConversationID) -> str** + +Get the persistence directory for the conversation. + +**Parameters:** + +- `persistence_base_dir` *str | Path* – Base directory for persistence. Can be a string +path or Path object. +- `conversation_id` *ConversationID* – Unique conversation ID. + +**Returns:** + +- *str* String path to the conversation-specific persistence directory. +- *str* Always returns a normalized string path even if a Path was provided. + +**abstractmethod ask_agent(question: str) -> str** Ask the agent a simple, stateless question and get a direct LLM response. -This bypasses the normal conversation flow and does not modify, persist, +This bypasses the normal conversation flow and does **not** modify, persist, or become part of the conversation state. The request is not remembered by the main agent, no events are recorded, and execution status is untouched. -It is also thread-safe and may be called while conversation.run() is +It is also thread-safe and may be called while `conversation.run()` is executing in another thread. -* Parameters: - `question` – A simple string question to ask the agent -* Returns: - A string response from the agent +**Parameters:** -#### abstractmethod close() +- `question` *str* – A simple string question to ask the agent -#### static compose_callbacks() +**Returns:** -Compose multiple callbacks into a single callback function. +- *str* A string response from the agent -* Parameters: - `callbacks` – An iterable of callback functions -* Returns: - A single callback function that calls all provided callbacks - -#### abstractmethod condense() +**abstractmethod condense() -> None** Force condensation of the conversation history. @@ -70,73 +130,55 @@ and forces the agent to take a single step to process it. The condensation will be applied immediately and will modify the conversation state by adding a condensation event to the history. -* Raises: - `ValueError` – If no condenser is configured or the condenser doesn’t - handle condensation requests. +**Raises:** -#### abstractmethod generate_title() +- `ValueError` – If no condenser is configured or the condenser doesn't + handle condensation requests. -Generate a title for the conversation based on the first user message. +**abstractmethod execute_tool(tool_name: str, action: Action) -> Observation** -* Parameters: - * `llm` – Optional LLM to use for title generation. If not provided, - uses the agent’s LLM. - * `max_length` – Maximum length of the generated title. -* Returns: - A generated title for the conversation. -* Raises: - `ValueError` – If no user messages are found in the conversation. +Execute a tool directly without going through the agent loop. -#### static get_persistence_dir() +This method allows executing tools before or outside of the normal +conversation.run() flow. It handles agent initialization automatically, +so tools can be executed before the first run() call. -Get the persistence directory for the conversation. +Note: This method bypasses the agent loop, including confirmation +policies and security analyzer checks. Callers are responsible for +applying any safeguards before executing potentially destructive tools. -* Parameters: - * `persistence_base_dir` – Base directory for persistence. Can be a string - path or Path object. - * `conversation_id` – Unique conversation ID. -* Returns: - String path to the conversation-specific persistence directory. - Always returns a normalized string path even if a Path was provided. +This is useful for: +- Pre-run setup operations (e.g., indexing repositories) +- Manual tool execution for environment setup +- Testing tool behavior outside the agent loop -#### abstractmethod pause() +**Parameters:** -#### abstractmethod reject_pending_actions() - -#### abstractmethod run() - -Execute the agent to process messages and perform actions. - -This method runs the agent until it finishes processing the current -message or reaches the maximum iteration limit. +- `tool_name` *str* – The name of the tool to execute (e.g., "sleeptime_compute") +- `action` *Action* – The action to pass to the tool executor -#### abstractmethod send_message() +**Returns:** -Send a message to the agent. +- *Observation* The observation returned by the tool execution -* Parameters: - * `message` – Either a string (which will be converted to a user message) - or a Message object - * `sender` – Optional identifier of the sender. Can be used to track - message origin in multi-agent scenarios. For example, when - one agent delegates to another, the sender can be set to - identify which agent is sending the message. +**Raises:** -#### abstractmethod set_confirmation_policy() +- `KeyError` – If the tool is not found in the agent's tools +- `NotImplementedError` – If the tool has no executor -Set the confirmation policy for the conversation. +**compose_callbacks(callbacks: Iterable[CallbackType]) -> CallbackType** -#### abstractmethod set_security_analyzer() +Compose multiple callbacks into a single callback function. -Set the security analyzer for the conversation. +**Parameters:** -#### abstractmethod update_secrets() +- `callbacks` *Iterable[CallbackType]* – An iterable of callback functions -### class Conversation +**Returns:** -### class Conversation +- *CallbackType* A single callback function that calls all provided callbacks -Bases: `object` +## class Conversation Factory class for creating conversation instances with OpenHands agents. @@ -144,717 +186,713 @@ This factory automatically creates either a LocalConversation or RemoteConversat based on the workspace type provided. LocalConversation runs the agent locally, while RemoteConversation connects to a remote agent server. -* Returns: - LocalConversation if workspace is local, RemoteConversation if workspace - is remote. +**Returns:** -#### Example +- LocalConversation if workspace is local, RemoteConversation if workspace +- is remote. -```pycon ->>> from openhands.sdk import LLM, Agent, Conversation ->>> llm = LLM(model="claude-sonnet-4-20250514", api_key=SecretStr("key")) ->>> agent = Agent(llm=llm, tools=[]) ->>> conversation = Conversation(agent=agent, workspace="./workspace") ->>> conversation.send_message("Hello!") ->>> conversation.run() -``` +## class EventLog -### class ConversationExecutionStatus +Bases: `EventsListBase` -Bases: `str`, `Enum` +Persistent event log with locking for concurrent writes. -Enum representing the current execution state of the conversation. +This class provides thread-safe and process-safe event storage using +the FileStore's locking mechanism. Events are persisted to disk and +can be accessed by index or event ID. -#### Methods +### Methods -#### DELETING = 'deleting' +**__init__(fs: FileStore, dir_path: str = EVENTS_DIR) -> None** -#### ERROR = 'error' +**get_index(event_id: EventID) -> int** -#### FINISHED = 'finished' +Return the integer index for a given event_id. -#### IDLE = 'idle' +**get_id(idx: int) -> EventID** -#### PAUSED = 'paused' +Return the event_id for a given index. -#### RUNNING = 'running' +**append(event: Event) -> None** -#### STUCK = 'stuck' +Append an event with locking for thread/process safety. -#### WAITING_FOR_CONFIRMATION = 'waiting_for_confirmation' +**Raises:** -### class ConversationState +- `TimeoutError` – If the lock cannot be acquired within LOCK_TIMEOUT_SECONDS. +- `ValueError` – If an event with the same ID already exists. -Bases: `OpenHandsModel` +## class EventsListBase +Bases: `Sequence[Event]`, `ABC` -#### Properties +Abstract base class for event lists that can be appended to. -- `activated_knowledge_skills`: list[str] -- `agent`: AgentBase -- `blocked_actions`: dict[str, str] -- `blocked_messages`: dict[str, str] -- `confirmation_policy`: ConfirmationPolicyBase -- `env_observation_persistence_dir`: str | None - Directory for persisting environment observation files. -- `events`: [EventLog](#class-eventlog) -- `execution_status`: [ConversationExecutionStatus](#class-conversationexecutionstatus) -- `id`: UUID -- `max_iterations`: int -- `persistence_dir`: str | None -- `secret_registry`: [SecretRegistry](#class-secretregistry) -- `security_analyzer`: SecurityAnalyzerBase | None -- `stats`: ConversationStats -- `stuck_detection`: bool -- `workspace`: BaseWorkspace +This provides a common interface for both local EventLog and remote +RemoteEventsList implementations, avoiding circular imports in protocols. -#### Methods +### Methods -#### acquire() +**abstractmethod append(event: Event) -> None** -Acquire the lock. +Add a new event to the list. -* Parameters: - * `blocking` – If True, block until lock is acquired. If False, return - immediately. - * `timeout` – Maximum time to wait for lock (ignored if blocking=False). - -1 means wait indefinitely. -* Returns: - True if lock was acquired, False otherwise. +## class WebSocketConnectionError -#### block_action() +Bases: `RuntimeError` -Persistently record a hook-blocked action. +Raised when WebSocket connection fails to establish within the timeout. -#### block_message() +### Properties -Persistently record a hook-blocked user message. +- `conversation_id` +- `timeout` -#### classmethod create() +### Methods -Create a new conversation state or resume from persistence. +**__init__(conversation_id: ConversationID, timeout: float, message: str | None = None) -> None** -This factory method handles both new conversation creation and resumption -from persisted state. +## class LocalConversation -New conversation: -The provided Agent is used directly. Pydantic validation happens via the -cls() constructor. +Bases: `BaseConversation` -Restored conversation: -The provided Agent is validated against the persisted agent using -agent.load(). Tools must match (they may have been used in conversation -history), but all other configuration can be freely changed: LLM, -agent_context, condenser, system prompts, etc. +### Properties -* Parameters: - * `id` – Unique conversation identifier - * `agent` – The Agent to use (tools must match persisted on restore) - * `workspace` – Working directory for agent operations - * `persistence_dir` – Directory for persisting state and events - * `max_iterations` – Maximum iterations per run - * `stuck_detection` – Whether to enable stuck detection - * `cipher` – Optional cipher for encrypting/decrypting secrets in - persisted state. If provided, secrets are encrypted when - saving and decrypted when loading. If not provided, secrets - are redacted (lost) on serialization. -* Returns: - ConversationState ready for use -* Raises: - * `ValueError` – If conversation ID or tools mismatch on restore - * `ValidationError` – If agent or other fields fail Pydantic validation +- `agent`: AgentBase +- `workspace`: LocalWorkspace +- `max_iteration_per_run`: int +- `llm_registry`: LLMRegistry +- `delete_on_close`: bool +- `id`: ConversationID + Get the unique ID of the conversation. +- `state`: ConversationState + Get the conversation state. -#### static get_unmatched_actions() +It returns a protocol that has a subset of ConversationState methods +and properties. We will have the ability to access the same properties +of ConversationState on a remote conversation object. +But we won't be able to access methods that mutate the state. +- `conversation_stats` +- `stuck_detector`: StuckDetector | None + Get the stuck detector instance if enabled. +- `resolved_plugins`: list[ResolvedPluginSource] | None + Get the resolved plugin sources after plugins are loaded. -Find actions in the event history that don’t have matching observations. +Returns None if plugins haven't been loaded yet, or if no plugins +were specified. Use this for persistence to ensure conversation +resume uses the exact same plugin versions. -This method identifies ActionEvents that don’t have corresponding -ObservationEvents or UserRejectObservations, which typically indicates -actions that are pending confirmation or execution. +### Methods -* Parameters: - `events` – List of events to search through -* Returns: - List of ActionEvent objects that don’t have corresponding observations, - in chronological order +**__init__(agent: AgentBase, workspace: str | Path | LocalWorkspace, plugins: list[PluginSource] | None = None, persistence_dir: str | Path | None = None, conversation_id: ConversationID | None = None, callbacks: list[ConversationCallbackType] | None = None, token_callbacks: list[ConversationTokenCallbackType] | None = None, hook_config: HookConfig | None = None, max_iteration_per_run: int = 500, stuck_detection: bool = True, stuck_detection_thresholds: StuckDetectionThresholds | Mapping[str, int] | None = None, visualizer: type[ConversationVisualizerBase] | ConversationVisualizerBase | None = DefaultConversationVisualizer, secrets: Mapping[str, SecretValue] | None = None, delete_on_close: bool = True, cipher: Cipher | None = None, _: object = \{\})** -#### locked() +Initialize the conversation. -Return True if the lock is currently held by any thread. +**Parameters:** + +- `agent` *AgentBase* – The agent to use for the conversation. +- `workspace` *str | Path | LocalWorkspace* – Working directory for agent operations and tool execution. +Can be a string path, Path object, or LocalWorkspace instance. +- `plugins` *list[PluginSource] | None* – Optional list of plugins to load. Each plugin is specified +with a source (github:owner/repo, git URL, or local path), +optional ref (branch/tag/commit), and optional repo_path for +monorepos. Plugins are loaded in order with these merge +semantics: skills override by name (last wins), MCP config +override by key (last wins), hooks concatenate (all run). +- `persistence_dir` *str | Path | None* – Directory for persisting conversation state and events. +Can be a string path or Path object. +- `conversation_id` *ConversationID | None* – Optional ID for the conversation. If provided, will + be used to identify the conversation. The user might want to + suffix their persistent filestore with this ID. +- `callbacks` *list[ConversationCallbackType] | None* – Optional list of callback functions to handle events +- `token_callbacks` *list[ConversationTokenCallbackType] | None* – Optional list of callbacks invoked for streaming deltas +- `hook_config` *HookConfig | None* – Optional hook configuration to auto-wire session hooks. +If plugins are loaded, their hooks are combined with this config. +- `max_iteration_per_run` *int* – Maximum number of iterations per run +- `visualizer` *type[ConversationVisualizerBase] | ConversationVisualizerBase | None* – Visualization configuration. Can be: + - ConversationVisualizerBase subclass: Class to instantiate + (default: ConversationVisualizer) + - ConversationVisualizerBase instance: Use custom visualizer + - None: No visualization +- `stuck_detection` *bool* – Whether to enable stuck detection +- `stuck_detection_thresholds` *StuckDetectionThresholds | Mapping[str, int] | None* – Optional configuration for stuck detection + thresholds. Can be a StuckDetectionThresholds instance or + a dict with keys: 'action_observation', 'action_error', + 'monologue', 'alternating_pattern'. Values are integers + representing the number of repetitions before triggering. +- `cipher` *Cipher | None* – Optional cipher for encrypting/decrypting secrets in persisted + state. If provided, secrets are encrypted when saving and + decrypted when loading. If not provided, secrets are redacted + (lost) on serialization. + +**send_message(message: str | Message, sender: str | None = None) -> None** -#### model_config = (configuration object) +Send a message to the agent. -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +**Parameters:** -#### model_post_init() +- `message` *str | Message* – Either a string (which will be converted to a user message) + or a Message object +- `sender` *str | None* – Optional identifier of the sender. Can be used to track + message origin in multi-agent scenarios. For example, when + one agent delegates to another, the sender can be set to + identify which agent is sending the message. -This function is meant to behave like a BaseModel method to initialise private attributes. +**run() -> None** -It takes context as an argument since that’s what pydantic-core passes when calling it. +Runs the conversation until the agent finishes. -* Parameters: - * `self` – The BaseModel instance. - * `context` – The context. +In confirmation mode: +- First call: creates actions but doesn't execute them, stops and waits +- Second call: executes pending actions (implicit confirmation) -#### owned() +In normal mode: +- Creates and executes actions immediately -Return True if the lock is currently held by the calling thread. +Can be paused between steps -#### pop_blocked_action() +**set_confirmation_policy(policy: ConfirmationPolicyBase) -> None** -Remove and return a hook-blocked action reason, if present. +Set the confirmation policy and store it in conversation state. -#### pop_blocked_message() +**reject_pending_actions(reason: str = 'User rejected the action') -> None** -Remove and return a hook-blocked message reason, if present. +Reject all pending actions from the agent. -#### release() +This is a non-invasive method to reject actions between run() calls. +Also clears the agent_waiting_for_confirmation flag. -Release the lock. +**pause() -> None** -* Raises: - `RuntimeError` – If the current thread doesn’t own the lock. +Pause agent execution. -#### set_on_state_change() +This method can be called from any thread to request that the agent +pause execution. The pause will take effect at the next iteration +of the run loop (between agent steps). -Set a callback to be called when state changes. +Note: If called during an LLM completion, the pause will not take +effect until the current LLM call completes. -* Parameters: - `callback` – A function that takes an Event (ConversationStateUpdateEvent) - or None to remove the callback +**update_secrets(secrets: Mapping[str, SecretValue]) -> None** -### class ConversationVisualizerBase +Add secrets to the conversation. -Bases: `ABC` +**Parameters:** -Base class for conversation visualizers. +- `secrets` *Mapping[str, SecretValue]* – 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. -This abstract base class defines the interface that all conversation visualizers -must implement. Visualizers can be created before the Conversation is initialized -and will be configured with the conversation state automatically. +**set_security_analyzer(analyzer: SecurityAnalyzerBase | None) -> None** -The typical usage pattern: -1. Create a visualizer instance: +Set the security analyzer for the conversation. - viz = MyVisualizer() -1. Pass it to Conversation: conv = Conversation(agent, visualizer=viz) -2. Conversation automatically calls viz.initialize(state) to attach the state +**close() -> None** -You can also pass the uninstantiated class if you don’t need extra args -: for initialization, and Conversation will create it: - : conv = Conversation(agent, visualizer=MyVisualizer) +Close the conversation and clean up all tool executors. -Conversation will then calls MyVisualizer() followed by initialize(state) +**ask_agent(question: str) -> str** +Ask the agent a simple, stateless question and get a direct LLM response. -#### Properties +This bypasses the normal conversation flow and does **not** modify, persist, +or become part of the conversation state. The request is not remembered by +the main agent, no events are recorded, and execution status is untouched. +It is also thread-safe and may be called while `conversation.run()` is +executing in another thread. -- `conversation_stats`: ConversationStats | None - Get conversation stats from the state. +**Parameters:** -#### Methods +- `question` *str* – A simple string question to ask the agent -#### __init__() +**Returns:** -Initialize the visualizer base. +- *str* A string response from the agent -#### create_sub_visualizer() +**generate_title(llm: LLM | None = None, max_length: int = 50) -> str** -Create a visualizer for a sub-agent during delegation. +Generate a title for the conversation based on the first user message. -Override this method to support sub-agent visualization in multi-agent -delegation scenarios. The sub-visualizer will be used to display events -from the spawned sub-agent. +**Parameters:** -By default, returns None which means sub-agents will not have visualization. -Subclasses that support delegation (like DelegationVisualizer) should -override this method to create appropriate sub-visualizers. +- `llm` *LLM | None* – Optional LLM to use for title generation. If not provided, + uses self.agent.llm. +- `max_length` *int* – Maximum length of the generated title. -* Parameters: - `agent_id` – The identifier of the sub-agent being spawned -* Returns: - A visualizer instance for the sub-agent, or None if sub-agent - visualization is not supported +**Returns:** -#### final initialize() +- *str* A generated title for the conversation. -Initialize the visualizer with conversation state. +**Raises:** -This method is called by Conversation after the state is created, -allowing the visualizer to access conversation stats and other -state information. +- `ValueError` – If no user messages are found in the conversation. -Subclasses should not override this method, to ensure the state is set. +**condense() -> None** -* Parameters: - `state` – The conversation state object +Synchronously force condense the conversation history. -#### abstractmethod on_event() +If the agent is currently running, `condense()` will wait for the +ongoing step to finish before proceeding. -Handle a conversation event. +Raises ValueError if no compatible condenser exists. -This method is called for each event in the conversation and should -implement the visualization logic. +**execute_tool(tool_name: str, action: Action) -> Observation** -* Parameters: - `event` – The event to visualize +Execute a tool directly without going through the agent loop. -### class DefaultConversationVisualizer +This method allows executing tools before or outside of the normal +conversation.run() flow. It handles agent initialization automatically, +so tools can be executed before the first run() call. -Bases: [`ConversationVisualizerBase`](#class-conversationvisualizerbase) +Note: This method bypasses the agent loop, including confirmation +policies and security analyzer checks. Callers are responsible for +applying any safeguards before executing potentially destructive tools. -Handles visualization of conversation events with Rich formatting. +This is useful for: +- Pre-run setup operations (e.g., indexing repositories) +- Manual tool execution for environment setup +- Testing tool behavior outside the agent loop -Provides Rich-formatted output with semantic dividers and complete content display. +**Parameters:** -#### Methods +- `tool_name` *str* – The name of the tool to execute (e.g., "sleeptime_compute") +- `action` *Action* – The action to pass to the tool executor -#### __init__() +**Returns:** -Initialize the visualizer. +- *Observation* The observation returned by the tool execution -* Parameters: - * `highlight_regex` – Dictionary mapping regex patterns to Rich color styles - for highlighting keywords in the visualizer. - For example: (configuration object) - * `skip_user_messages` – If True, skip displaying user messages. Useful for - scenarios where user input is not relevant to show. +**Raises:** -#### on_event() +- `KeyError` – If the tool is not found in the agent's tools +- `NotImplementedError` – If the tool has no executor -Main event handler that displays events with Rich formatting. +## class RemoteConversation -### class EventLog +Bases: `BaseConversation` -Bases: [`EventsListBase`](#class-eventslistbase) +### Properties -Persistent event log with locking for concurrent writes. +- `agent`: AgentBase +- `max_iteration_per_run`: int +- `workspace`: RemoteWorkspace +- `delete_on_close`: bool +- `id`: ConversationID +- `state`: RemoteState + Access to remote conversation state. +- `conversation_stats` +- `stuck_detector` + Stuck detector for compatibility. +Not implemented for remote conversations. -This class provides thread-safe and process-safe event storage using -the FileStore’s locking mechanism. Events are persisted to disk and -can be accessed by index or event ID. +### Methods -#### Methods +**__init__(agent: AgentBase, workspace: RemoteWorkspace, plugins: list | None = None, conversation_id: ConversationID | None = None, callbacks: list[ConversationCallbackType] | None = None, max_iteration_per_run: int = 500, stuck_detection: bool = True, stuck_detection_thresholds: StuckDetectionThresholds | Mapping[str, int] | None = None, hook_config: HookConfig | None = None, visualizer: type[ConversationVisualizerBase] | ConversationVisualizerBase | None = DefaultConversationVisualizer, secrets: Mapping[str, SecretValue] | None = None, delete_on_close: bool = False, _: object = \{\}) -> None** -#### NOTE -For LocalFileStore, file locking via flock() does NOT work reliably -on NFS mounts or network filesystems. Users deploying with shared -storage should use alternative coordination mechanisms. +Remote conversation proxy that talks to an agent server. -#### __init__() +**Parameters:** + +- `agent` *AgentBase* – Agent configuration (will be sent to the server) +- `workspace` *RemoteWorkspace* – The working directory for agent operations and tool execution. +- `plugins` *list | None* – Optional list of plugins to load on the server. Each plugin + is a PluginSource specifying source, ref, and repo_path. +- `conversation_id` *ConversationID | None* – Optional existing conversation id to attach to +- `callbacks` *list[ConversationCallbackType] | None* – Optional callbacks to receive events (not yet streamed) +- `max_iteration_per_run` *int* – Max iterations configured on server +- `stuck_detection` *bool* – Whether to enable stuck detection on server +- `stuck_detection_thresholds` *StuckDetectionThresholds | Mapping[str, int] | None* – Optional configuration for stuck detection + thresholds. Can be a StuckDetectionThresholds instance or + a dict with keys: 'action_observation', 'action_error', + 'monologue', 'alternating_pattern'. Values are integers + representing the number of repetitions before triggering. +- `hook_config` *HookConfig | None* – Optional hook configuration for session hooks +- `visualizer` *type[ConversationVisualizerBase] | ConversationVisualizerBase | None* – Visualization configuration. Can be: + - ConversationVisualizerBase subclass: Class to instantiate + (default: ConversationVisualizer) + - ConversationVisualizerBase instance: Use custom visualizer + - None: No visualization +- `secrets` *Mapping[str, SecretValue] | None* – Optional secrets to initialize the conversation with + +**send_message(message: str | Message, sender: str | None = None) -> None** + +**run(blocking: bool = True, poll_interval: float = 1.0, timeout: float = 3600.0) -> None** -#### append() +Trigger a run on the server. -Append an event with locking for thread/process safety. +**Parameters:** -* Raises: - * `TimeoutError` – If the lock cannot be acquired within LOCK_TIMEOUT_SECONDS. - * `ValueError` – If an event with the same ID already exists. +- `blocking` *bool* – If True (default), wait for the run to complete by polling +the server. If False, return immediately after triggering the run. +- `poll_interval` *float* – Time in seconds between status polls (only used when +blocking=True). Default is 1.0 second. +- `timeout` *float* – Maximum time in seconds to wait for the run to complete +(only used when blocking=True). Default is 3600 seconds. -#### get_id() +**Raises:** -Return the event_id for a given index. +- `ConversationRunError` – If the run fails or times out. -#### get_index() +**set_confirmation_policy(policy: ConfirmationPolicyBase) -> None** -Return the integer index for a given event_id. +**set_security_analyzer(analyzer: SecurityAnalyzerBase | None) -> None** -### class EventsListBase +Set the security analyzer for the remote conversation. -Bases: `Sequence`[`Event`], `ABC` +**reject_pending_actions(reason: str = 'User rejected the action') -> None** -Abstract base class for event lists that can be appended to. +**pause() -> None** -This provides a common interface for both local EventLog and remote -RemoteEventsList implementations, avoiding circular imports in protocols. +**update_secrets(secrets: Mapping[str, SecretValue]) -> None** -#### Methods +**ask_agent(question: str) -> str** -#### abstractmethod append() +Ask the agent a simple, stateless question and get a direct LLM response. -Add a new event to the list. +This bypasses the normal conversation flow and does **not** modify, persist, +or become part of the conversation state. The request is not remembered by +the main agent, no events are recorded, and execution status is untouched. +It is also thread-safe and may be called while `conversation.run()` is +executing in another thread. -### class LocalConversation +**Parameters:** -Bases: [`BaseConversation`](#class-baseconversation) +- `question` *str* – A simple string question to ask the agent +**Returns:** -#### Properties +- *str* A string response from the agent -- `agent`: AgentBase -- `id`: UUID - Get the unique ID of the conversation. -- `llm_registry`: LLMRegistry -- `max_iteration_per_run`: int -- `state`: [ConversationState](#class-conversationstate) - Get the conversation state. - It returns a protocol that has a subset of ConversationState methods - and properties. We will have the ability to access the same properties - of ConversationState on a remote conversation object. - But we won’t be able to access methods that mutate the state. -- `stuck_detector`: [StuckDetector](#class-stuckdetector) | None - Get the stuck detector instance if enabled. -- `workspace`: LocalWorkspace +**generate_title(llm: LLM | None = None, max_length: int = 50) -> str** -#### Methods +Generate a title for the conversation based on the first user message. -#### __init__() +**Parameters:** -Initialize the conversation. +- `llm` *LLM | None* – Optional LLM to use for title generation. If provided, its usage_id + will be sent to the server. If not provided, uses the agent's LLM. +- `max_length` *int* – Maximum length of the generated title. -* Parameters: - * `agent` – The agent to use for the conversation - * `workspace` – Working directory for agent operations and tool execution. - Can be a string path, Path object, or LocalWorkspace instance. - * `persistence_dir` – Directory for persisting conversation state and events. - Can be a string path or Path object. - * `conversation_id` – Optional ID for the conversation. If provided, will - be used to identify the conversation. The user might want to - suffix their persistent filestore with this ID. - * `callbacks` – Optional list of callback functions to handle events - * `token_callbacks` – Optional list of callbacks invoked for streaming deltas - * `hook_config` – Optional hook configuration to auto-wire session hooks - * `max_iteration_per_run` – Maximum number of iterations per run - * `visualizer` – - - Visualization configuration. Can be: - - ConversationVisualizerBase subclass: Class to instantiate - > (default: ConversationVisualizer) - - ConversationVisualizerBase instance: Use custom visualizer - - None: No visualization - * `stuck_detection` – Whether to enable stuck detection - * `stuck_detection_thresholds` – Optional configuration for stuck detection - thresholds. Can be a StuckDetectionThresholds instance or - a dict with keys: ‘action_observation’, ‘action_error’, - ‘monologue’, ‘alternating_pattern’. Values are integers - representing the number of repetitions before triggering. - * `cipher` – Optional cipher for encrypting/decrypting secrets in persisted - state. If provided, secrets are encrypted when saving and - decrypted when loading. If not provided, secrets are redacted - (lost) on serialization. - -#### ask_agent() +**Returns:** -Ask the agent a simple, stateless question and get a direct LLM response. +- *str* A generated title for the conversation. -This bypasses the normal conversation flow and does not modify, persist, -or become part of the conversation state. The request is not remembered by -the main agent, no events are recorded, and execution status is untouched. -It is also thread-safe and may be called while conversation.run() is -executing in another thread. +**condense() -> None** -* Parameters: - `question` – A simple string question to ask the agent -* Returns: - A string response from the agent +Force condensation of the conversation history. -#### close() +This method sends a condensation request to the remote agent server. +The server will use the existing condensation request pattern to trigger +condensation if a condenser is configured and handles condensation requests. -Close the conversation and clean up all tool executors. +The condensation will be applied on the server side and will modify the +conversation state by adding a condensation event to the history. -#### condense() +**Raises:** -Synchronously force condense the conversation history. +- `HTTPError` – If the server returns an error (e.g., no condenser configured). -If the agent is currently running, condense() will wait for the -ongoing step to finish before proceeding. +**execute_tool(tool_name: str, action: Action) -> Observation** -Raises ValueError if no compatible condenser exists. +Execute a tool directly without going through the agent loop. -#### property conversation_stats +Note: This method is not yet supported for RemoteConversation. +Tool execution for remote conversations happens on the server side +during the normal agent loop. -#### generate_title() +**Parameters:** -Generate a title for the conversation based on the first user message. +- `tool_name` *str* – The name of the tool to execute +- `action` *Action* – The action to pass to the tool executor -* Parameters: - * `llm` – Optional LLM to use for title generation. If not provided, - uses self.agent.llm. - * `max_length` – Maximum length of the generated title. -* Returns: - A generated title for the conversation. -* Raises: - `ValueError` – If no user messages are found in the conversation. +**Raises:** -#### pause() +- `NotImplementedError` – Always, as this feature is not yet supported +for remote conversations. -Pause agent execution. +**close() -> None** -This method can be called from any thread to request that the agent -pause execution. The pause will take effect at the next iteration -of the run loop (between agent steps). +Close the conversation and clean up resources. -Note: If called during an LLM completion, the pause will not take -effect until the current LLM call completes. +Note: We don't close self._client here because it's shared with the workspace. +The workspace owns the client and will close it during its own cleanup. +Closing it here would prevent the workspace from making cleanup API calls. -#### reject_pending_actions() +**get_agent_final_response(events: Sequence[Event]) -> str** -Reject all pending actions from the agent. +Extract the final response from the agent. -This is a non-invasive method to reject actions between run() calls. -Also clears the agent_waiting_for_confirmation flag. +An agent can end a conversation in two ways: +1. By calling the finish tool +2. By returning a text message with no tool calls -#### run() +**Parameters:** -Runs the conversation until the agent finishes. +- `events` *Sequence[Event]* – List of conversation events to search through. -In confirmation mode: -- First call: creates actions but doesn’t execute them, stops and waits -- Second call: executes pending actions (implicit confirmation) +**Returns:** -In normal mode: -- Creates and executes actions immediately +- *str* The final response message from the agent, or empty string if not found. -Can be paused between steps +## class SecretRegistry -#### send_message() +Bases: `OpenHandsModel` -Send a message to the agent. +Manages secrets and injects them into bash commands when needed. -* Parameters: - * `message` – Either a string (which will be converted to a user message) - or a Message object - * `sender` – Optional identifier of the sender. Can be used to track - message origin in multi-agent scenarios. For example, when - one agent delegates to another, the sender can be set to - identify which agent is sending the message. +The secret registry stores a mapping of secret keys to SecretSources +that retrieve the actual secret values. When a bash command is about to be +executed, it scans the command for any secret keys and injects the corresponding +environment variables. -#### set_confirmation_policy() +Secret sources will redact / encrypt their sensitive values as appropriate when +serializing, depending on the content of the context. If a context is present +and contains a 'cipher' object, this is used for encryption. If it contains a +boolean 'expose_secrets' flag set to True, secrets are dunped in plain text. +Otherwise secrets are redacted. -Set the confirmation policy and store it in conversation state. +Additionally, it tracks the latest exported values to enable consistent masking +even when callable secrets fail on subsequent calls. -#### set_security_analyzer() +### Properties -Set the security analyzer for the conversation. +- `secret_sources`: dict[str, SecretSource] -#### update_secrets() +### Methods -Add secrets to the conversation. +**update_secrets(secrets: Mapping[str, SecretValue]) -> None** -* 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. +Add or update secrets in the manager. -### class RemoteConversation +**Parameters:** -Bases: [`BaseConversation`](#class-baseconversation) +- `secrets` *Mapping[str, SecretValue]* – Dictionary mapping secret keys to either string values + or callable functions that return string values +**find_secrets_in_text(text: str) -> set[str]** -#### Properties +Find all secret keys mentioned in the given text. -- `agent`: AgentBase -- `id`: UUID -- `max_iteration_per_run`: int -- `state`: RemoteState - Access to remote conversation state. -- `workspace`: RemoteWorkspace +**Parameters:** -#### Methods +- `text` *str* – The text to search for secret keys -#### __init__() +**Returns:** -Remote conversation proxy that talks to an agent server. +- *set[str]* Set of secret keys found in the text -* Parameters: - * `agent` – Agent configuration (will be sent to the server) - * `workspace` – The working directory for agent operations and tool execution. - * `conversation_id` – Optional existing conversation id to attach to - * `callbacks` – Optional callbacks to receive events (not yet streamed) - * `max_iteration_per_run` – Max iterations configured on server - * `stuck_detection` – Whether to enable stuck detection on server - * `stuck_detection_thresholds` – Optional configuration for stuck detection - thresholds. Can be a StuckDetectionThresholds instance or - 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 - * `visualizer` – - - Visualization configuration. Can be: - - ConversationVisualizerBase subclass: Class to instantiate - > (default: ConversationVisualizer) - - ConversationVisualizerBase instance: Use custom visualizer - - None: No visualization - * `secrets` – Optional secrets to initialize the conversation with - -#### ask_agent() +**get_secrets_as_env_vars(command: str) -> dict[str, str]** -Ask the agent a simple, stateless question and get a direct LLM response. +Get secrets that should be exported as environment variables for a command. -This bypasses the normal conversation flow and does not modify, persist, -or become part of the conversation state. The request is not remembered by -the main agent, no events are recorded, and execution status is untouched. -It is also thread-safe and may be called while conversation.run() is -executing in another thread. +**Parameters:** -* Parameters: - `question` – A simple string question to ask the agent -* Returns: - A string response from the agent +- `command` *str* – The bash command to check for secret references -#### close() +**Returns:** -Close the conversation and clean up resources. +- *dict[str, str]* Dictionary of environment variables to export (key -> value) -Note: We don’t close self._client here because it’s shared with the workspace. -The workspace owns the client and will close it during its own cleanup. -Closing it here would prevent the workspace from making cleanup API calls. +**mask_secrets_in_output(text: str) -> str** -#### condense() +Mask secret values in the given text. -Force condensation of the conversation history. +This method uses both the current exported values and attempts to get +fresh values from callables to ensure comprehensive masking. -This method sends a condensation request to the remote agent server. -The server will use the existing condensation request pattern to trigger -condensation if a condenser is configured and handles condensation requests. +**Parameters:** -The condensation will be applied on the server side and will modify the -conversation state by adding a condensation event to the history. +- `text` *str* – The text to mask secrets in -* Raises: - `HTTPError` – If the server returns an error (e.g., no condenser configured). +**Returns:** -#### property conversation_stats +- *str* Text with secret values replaced by `` -#### generate_title() +## class ConversationExecutionStatus -Generate a title for the conversation based on the first user message. +Bases: `str`, `Enum` -* Parameters: - * `llm` – Optional LLM to use for title generation. If provided, its usage_id - will be sent to the server. If not provided, uses the agent’s LLM. - * `max_length` – Maximum length of the generated title. -* Returns: - A generated title for the conversation. +Enum representing the current execution state of the conversation. -#### pause() +### Properties -#### reject_pending_actions() +- `IDLE` +- `RUNNING` +- `PAUSED` +- `WAITING_FOR_CONFIRMATION` +- `FINISHED` +- `ERROR` +- `STUCK` +- `DELETING` -#### run() +### Methods -Trigger a run on the server. +**is_terminal() -> bool** -* Parameters: - * `blocking` – If True (default), wait for the run to complete by polling - the server. If False, return immediately after triggering the run. - * `poll_interval` – Time in seconds between status polls (only used when - blocking=True). Default is 1.0 second. - * `timeout` – Maximum time in seconds to wait for the run to complete - (only used when blocking=True). Default is 3600 seconds. -* Raises: - `ConversationRunError` – If the run fails or times out. +Check if this status represents a terminal state. -#### send_message() +Terminal states indicate the run has completed and the agent is no longer +actively processing. These are: FINISHED, ERROR, STUCK. -Send a message to the agent. +Note: IDLE is NOT a terminal state - it's the initial state of a conversation +before any run has started. Including IDLE would cause false positives when +the WebSocket delivers the initial state update during connection. -* Parameters: - * `message` – Either a string (which will be converted to a user message) - or a Message object - * `sender` – Optional identifier of the sender. Can be used to track - message origin in multi-agent scenarios. For example, when - one agent delegates to another, the sender can be set to - identify which agent is sending the message. +**Returns:** -#### set_confirmation_policy() +- *bool* True if this is a terminal status, False otherwise. -Set the confirmation policy for the conversation. +## class ConversationState -#### set_security_analyzer() +Bases: `OpenHandsModel` -Set the security analyzer for the remote conversation. +### Properties -#### property stuck_detector +- `id`: ConversationID + Unique conversation ID +- `agent`: AgentBase + The agent running in the conversation. This is persisted to allow resuming conversations and check agent configuration to handle e.g., tool changes, LLM changes, etc. +- `workspace`: BaseWorkspace + Workspace used by the agent to execute commands and read/write files. Not the process working directory. +- `persistence_dir`: str | None + Directory for persisting conversation state and events. If None, conversation will not be persisted. +- `max_iterations`: int + Maximum number of iterations the agent can perform in a single run. +- `stuck_detection`: bool + Whether to enable stuck detection for the agent. +- `execution_status`: ConversationExecutionStatus +- `confirmation_policy`: ConfirmationPolicyBase +- `security_analyzer`: SecurityAnalyzerBase | None + Optional security analyzer to evaluate action risks. +- `activated_knowledge_skills`: list[str] + List of activated knowledge skills name +- `blocked_actions`: dict[str, str] + Actions blocked by PreToolUse hooks, keyed by action ID +- `blocked_messages`: dict[str, str] + Messages blocked by UserPromptSubmit hooks, keyed by message ID +- `stats`: ConversationStats + Conversation statistics for tracking LLM metrics +- `secret_registry`: SecretRegistry + Registry for handling secrets and sensitive data +- `events`: EventLog +- `env_observation_persistence_dir`: str | None + Directory for persisting environment observation files. -Stuck detector for compatibility. -Not implemented for remote conversations. +### Methods -#### update_secrets() +**set_on_state_change(callback: ConversationCallbackType | None) -> None** -### class SecretRegistry +Set a callback to be called when state changes. -Bases: `OpenHandsModel` +**Parameters:** -Manages secrets and injects them into bash commands when needed. +- `callback` *ConversationCallbackType | None* – A function that takes an Event (ConversationStateUpdateEvent) + or None to remove the callback -The secret registry stores a mapping of secret keys to SecretSources -that retrieve the actual secret values. When a bash command is about to be -executed, it scans the command for any secret keys and injects the corresponding -environment variables. +**create(id: ConversationID, agent: AgentBase, workspace: BaseWorkspace, persistence_dir: str | None = None, max_iterations: int = 500, stuck_detection: bool = True, cipher: Cipher | None = None) -> ConversationState** -Secret sources will redact / encrypt their sensitive values as appropriate when -serializing, depending on the content of the context. If a context is present -and contains a ‘cipher’ object, this is used for encryption. If it contains a -boolean ‘expose_secrets’ flag set to True, secrets are dunped in plain text. -Otherwise secrets are redacted. +Create a new conversation state or resume from persistence. -Additionally, it tracks the latest exported values to enable consistent masking -even when callable secrets fail on subsequent calls. +This factory method handles both new conversation creation and resumption +from persisted state. +**New conversation:** +The provided Agent is used directly. Pydantic validation happens via the +cls() constructor. -#### Properties +**Restored conversation:** +The provided Agent is validated against the persisted agent using +agent.load(). Tools must match (they may have been used in conversation +history), but all other configuration can be freely changed: LLM, +agent_context, condenser, system prompts, etc. -- `secret_sources`: dict[str, SecretSource] +**Parameters:** -#### Methods +- `id` *ConversationID* – Unique conversation identifier +- `agent` *AgentBase* – The Agent to use (tools must match persisted on restore) +- `workspace` *BaseWorkspace* – Working directory for agent operations +- `persistence_dir` *str | None* – Directory for persisting state and events +- `max_iterations` *int* – Maximum iterations per run +- `stuck_detection` *bool* – Whether to enable stuck detection +- `cipher` *Cipher | None* – Optional cipher for encrypting/decrypting secrets in + persisted state. If provided, secrets are encrypted when + saving and decrypted when loading. If not provided, secrets + are redacted (lost) on serialization. -#### find_secrets_in_text() +**Returns:** -Find all secret keys mentioned in the given text. +- *ConversationState* ConversationState ready for use -* Parameters: - `text` – The text to search for secret keys -* Returns: - Set of secret keys found in the text +**Raises:** -#### get_secrets_as_env_vars() +- `ValueError` – If conversation ID or tools mismatch on restore +- `ValidationError` – If agent or other fields fail Pydantic validation -Get secrets that should be exported as environment variables for a command. +**block_action(action_id: str, reason: str) -> None** -* Parameters: - `command` – The bash command to check for secret references -* Returns: - Dictionary of environment variables to export (key -> value) +Persistently record a hook-blocked action. -#### mask_secrets_in_output() +**pop_blocked_action(action_id: str) -> str | None** -Mask secret values in the given text. +Remove and return a hook-blocked action reason, if present. -This method uses both the current exported values and attempts to get -fresh values from callables to ensure comprehensive masking. +**block_message(message_id: str, reason: str) -> None** -* Parameters: - `text` – The text to mask secrets in -* Returns: - Text with secret values replaced by `` +Persistently record a hook-blocked user message. -#### model_config = (configuration object) +**pop_blocked_message(message_id: str) -> str | None** -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +Remove and return a hook-blocked message reason, if present. -#### model_post_init() +**get_unmatched_actions(events: Sequence[Event]) -> list[ActionEvent]** -This function is meant to behave like a BaseModel method to initialise private attributes. +Find actions in the event history that don't have matching observations. -It takes context as an argument since that’s what pydantic-core passes when calling it. +This method identifies ActionEvents that don't have corresponding +ObservationEvents or UserRejectObservations, which typically indicates +actions that are pending confirmation or execution. -* Parameters: - * `self` – The BaseModel instance. - * `context` – The context. +**Parameters:** -#### update_secrets() +- `events` *Sequence[Event]* – List of events to search through -Add or update secrets in the manager. +**Returns:** + +- *list[ActionEvent]* List of ActionEvent objects that don't have corresponding observations, +- *list[ActionEvent]* in chronological order -* Parameters: - `secrets` – Dictionary mapping secret keys to either string values - or callable functions that return string values +**acquire(blocking: bool = True, timeout: float = -1) -> bool** -### class StuckDetector +Acquire the lock. + +**Parameters:** + +- `blocking` *bool* – If True, block until lock is acquired. If False, return + immediately. +- `timeout` *float* – Maximum time to wait for lock (ignored if blocking=False). + -1 means wait indefinitely. + +**Returns:** + +- *bool* True if lock was acquired, False otherwise. + +**release() -> None** + +Release the lock. + +**Raises:** + +- `RuntimeError` – If the current thread doesn't own the lock. + +**locked() -> bool** + +Return True if the lock is currently held by any thread. -Bases: `object` +**owned() -> bool** + +Return True if the lock is currently held by the calling thread. + +## class StuckDetector Detects when an agent is stuck in repetitive or unproductive patterns. @@ -865,20 +903,129 @@ This detector analyzes the conversation history to identify various stuck patter 4. Repeating alternating action-observation patterns 5. Context window errors indicating memory issues +### Properties -#### Properties - -- `action_error_threshold`: int +- `state`: ConversationState +- `thresholds`: StuckDetectionThresholds - `action_observation_threshold`: int -- `alternating_pattern_threshold`: int +- `action_error_threshold`: int - `monologue_threshold`: int -- `state`: [ConversationState](#class-conversationstate) -- `thresholds`: StuckDetectionThresholds +- `alternating_pattern_threshold`: int -#### Methods +### Methods -#### __init__() +**__init__(state: ConversationState, thresholds: StuckDetectionThresholds | None = None)** -#### is_stuck() +**is_stuck() -> bool** Check if the agent is currently stuck. + +Note: To avoid materializing potentially large file-backed event histories, +only the last MAX_EVENTS_TO_SCAN_FOR_STUCK_DETECTION events are analyzed. +If a user message exists within this window, only events after it are checked. +Otherwise, all events in the window are analyzed. + +## class ConversationVisualizerBase + +Bases: `ABC` + +Base class for conversation visualizers. + +This abstract base class defines the interface that all conversation visualizers +must implement. Visualizers can be created before the Conversation is initialized +and will be configured with the conversation state automatically. + +The typical usage pattern: +1. Create a visualizer instance: + `viz = MyVisualizer()` +2. Pass it to Conversation: `conv = Conversation(agent, visualizer=viz)` +3. Conversation automatically calls `viz.initialize(state)` to attach the state + +You can also pass the uninstantiated class if you don't need extra args + for initialization, and Conversation will create it: + `conv = Conversation(agent, visualizer=MyVisualizer)` +Conversation will then calls `MyVisualizer()` followed by `initialize(state)` + +### Properties + +- `conversation_stats`: ConversationStats | None + Get conversation stats from the state. + +### Methods + +**__init__()** + +Initialize the visualizer base. + +**initialize(state: ConversationStateProtocol) -> None** + +Initialize the visualizer with conversation state. + +This method is called by Conversation after the state is created, +allowing the visualizer to access conversation stats and other +state information. + +Subclasses should not override this method, to ensure the state is set. + +**Parameters:** + +- `state` *ConversationStateProtocol* – The conversation state object + +**abstractmethod on_event(event: Event) -> None** + +Handle a conversation event. + +This method is called for each event in the conversation and should +implement the visualization logic. + +**Parameters:** + +- `event` *Event* – The event to visualize + +**create_sub_visualizer(agent_id: str) -> ConversationVisualizerBase | None** + +Create a visualizer for a sub-agent during delegation. + +Override this method to support sub-agent visualization in multi-agent +delegation scenarios. The sub-visualizer will be used to display events +from the spawned sub-agent. + +By default, returns None which means sub-agents will not have visualization. +Subclasses that support delegation (like DelegationVisualizer) should +override this method to create appropriate sub-visualizers. + +**Parameters:** + +- `agent_id` *str* – The identifier of the sub-agent being spawned + +**Returns:** + +- *ConversationVisualizerBase | None* A visualizer instance for the sub-agent, or None if sub-agent +- *ConversationVisualizerBase | None* visualization is not supported + +## class DefaultConversationVisualizer + +Bases: `ConversationVisualizerBase` + +Handles visualization of conversation events with Rich formatting. + +Provides Rich-formatted output with semantic dividers and complete content display. + +### Methods + +**__init__(highlight_regex: dict[str, str] | None = DEFAULT_HIGHLIGHT_REGEX, skip_user_messages: bool = False)** + +Initialize the visualizer. + +**Parameters:** + +- `highlight_regex` *dict[str, str] | None* – Dictionary mapping regex patterns to Rich color styles + for highlighting keywords in the visualizer. + For example: \{"Reasoning:": "bold blue", + "Thought:": "bold green"\} +- `skip_user_messages` *bool* – If True, skip displaying user messages. Useful for + scenarios where user input is not relevant to show. + +**on_event(event: Event) -> None** + +Main event handler that displays events with Rich formatting. diff --git a/sdk/api-reference/openhands.sdk.event.mdx b/sdk/api-reference/openhands.sdk.event.mdx index 5e2fbcaa..8825f172 100644 --- a/sdk/api-reference/openhands.sdk.event.mdx +++ b/sdk/api-reference/openhands.sdk.event.mdx @@ -1,131 +1,114 @@ --- title: openhands.sdk.event -description: API reference for openhands.sdk.event module +description: API reference for openhands.sdk.event --- +## class Event -### class ActionEvent - -Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent) +Bases: `DiscriminatedUnionMixin`, `ABC` +Base class for all events. -#### Properties +### Properties -- `action`: Action | None -- `critic_result`: CriticResult | None -- `llm_response_id`: str -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. -- `reasoning_content`: str | None -- `responses_reasoning_item`: ReasoningItemModel | None -- `security_risk`: SecurityRisk -- `source`: Literal['agent', 'user', 'environment'] -- `summary`: str | None -- `thinking_blocks`: list[ThinkingBlock | RedactedThinkingBlock] -- `thought`: Sequence[TextContent] -- `tool_call`: MessageToolCall -- `tool_call_id`: str -- `tool_name`: str +- `model_config`: ConfigDict +- `id`: EventID + Unique event id (ULID/UUID) +- `timestamp`: str + Event timestamp +- `source`: SourceType + The source of this event - `visualize`: Text - Return Rich Text representation of this action event. - -#### Methods - -#### to_llm_message() - -Individual message - may be incomplete for multi-action batches + Return Rich Text representation of this event. -### class AgentErrorEvent +This is a fallback implementation for unknown event types. +Subclasses should override this method to provide specific visualization. -Bases: [`ObservationBaseEvent`](#class-observationbaseevent) +## class LLMConvertibleEvent -Error triggered by the agent. - -Note: This event should not contain model “thought” or “reasoning_content”. It -represents an error produced by the agent/scaffold, not model output. +Bases: `Event`, `ABC` +Base class for events that can be converted to LLM messages. -#### Properties +### Methods -- `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'] -- `visualize`: Text - Return Rich Text representation of this agent error event. +**abstractmethod to_llm_message() -> Message** -#### Methods +**events_to_messages(events: list[LLMConvertibleEvent]) -> list[Message]** -#### to_llm_message() +Convert event stream to LLM message stream, handling multi-action batches -### class Condensation +## class Condensation -Bases: [`Event`](#class-event) +Bases: `Event` This action indicates a condensation of the conversation history is happening. +### Properties -#### Properties - -- `forgotten_event_ids`: list[str] -- `llm_response_id`: str -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. -- `source`: Literal['agent', 'user', 'environment'] +- `forgotten_event_ids`: list[EventID] + The IDs of the events that are being forgotten (removed from the `View` given to the LLM). - `summary`: str | None + An optional summary of the events being forgotten. - `summary_offset`: int | None + An optional offset to the start of the resulting view (after forgotten events have been removed) indicating where the summary should be inserted. If not provided, the summary will not be inserted into the view. +- `llm_response_id`: EventID + Completion or Response ID of the LLM response that generated this event +- `source`: SourceType - `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 CondensationRequest +- `summary_event`: CondensationSummaryEvent + Generates a CondensationSummaryEvent. -Bases: [`Event`](#class-event) +Since summary events are not part of the main event store and are generated +dynamically, this property ensures the created event has a unique and consistent +ID based on the condensation event's ID. -This action is used to request a condensation of the conversation history. +Raises: + ValueError: If no summary is present. +- `has_summary_metadata`: bool + Checks if both summary and summary_offset are present. +### Methods -#### Properties +**apply(events: list[LLMConvertibleEvent]) -> list[LLMConvertibleEvent]** -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. -- `source`: Literal['agent', 'user', 'environment'] -- `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. +Applies the condensation to a list of events. + +This method removes events that are marked to be forgotten and returns a new +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 -#### Methods +Bases: `Event` -#### action +This action is used to request a condensation of the conversation history. -The action type, namely ActionType.CONDENSATION_REQUEST. +### Properties -* Type: - str +- `source`: SourceType +- `visualize`: Text -### class CondensationSummaryEvent +## class CondensationSummaryEvent -Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent) +Bases: `LLMConvertibleEvent` This event represents a summary generated by a condenser. +### Properties -#### Properties - -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. -- `source`: Literal['agent', 'user', 'environment'] - `summary`: str The summary text. +- `source`: SourceType -#### Methods +### Methods -#### to_llm_message() +**to_llm_message() -> Message** -### class ConversationStateUpdateEvent +## class ConversationStateUpdateEvent -Bases: [`Event`](#class-event) +Bases: `Event` Event that contains conversation state updates. @@ -135,54 +118,38 @@ allowing remote clients to stay in sync without making REST API calls. All fields are serialized versions of the corresponding ConversationState fields to ensure compatibility with websocket transmission. +### Properties -#### Properties - +- `source`: SourceType - `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'] + Unique key for this state update event - `value`: Any + Serialized conversation state updates -#### Methods +### Methods -#### classmethod from_conversation_state() +**validate_key(key)** -Create a state update event from a ConversationState object. - -This creates an event containing a snapshot of important state fields. +**validate_value(value, info)** -* Parameters: - * `state` – The ConversationState to serialize - * `conversation_id` – The conversation ID for the event -* Returns: - A ConversationStateUpdateEvent with serialized state data +**from_conversation_state(state: ConversationState) -> ConversationStateUpdateEvent** -#### classmethod validate_key() +Create a state update event from a ConversationState object. -#### classmethod validate_value() +This creates an event containing a snapshot of important state fields. -### class Event +**Parameters:** -Bases: `DiscriminatedUnionMixin`, `ABC` +- `state` *ConversationState* – The ConversationState to serialize +- `conversation_id` – The conversation ID for the event -Base class for all events. +**Returns:** +- *ConversationStateUpdateEvent* A ConversationStateUpdateEvent with serialized state data -#### Properties +## class LLMCompletionLogEvent -- `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'] -- `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 LLMCompletionLogEvent - -Bases: [`Event`](#class-event) +Bases: `Event` Event containing LLM completion log data. @@ -190,165 +157,207 @@ When an LLM is configured with log_completions=True in a remote conversation, this event streams the completion log data back to the client through WebSocket instead of writing it to a file inside the Docker container. +### Properties -#### Properties - +- `source`: SourceType - `filename`: str + The intended filename for this log (relative to log directory) - `log_data`: str -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + The JSON-encoded log data to be written to the file - `model_name`: str -- `source`: Literal['agent', 'user', 'environment'] + The model name for context - `usage_id`: str -### class LLMConvertibleEvent + The LLM usage_id that produced this log -Bases: [`Event`](#class-event), `ABC` +## class ActionEvent -Base class for events that can be converted to LLM messages. +Bases: `LLMConvertibleEvent` +### Properties -#### Properties +- `source`: SourceType +- `thought`: Sequence[TextContent] + The thought process of the agent before taking this action +- `reasoning_content`: str | None + Intermediate reasoning/thinking content from reasoning models +- `thinking_blocks`: list[ThinkingBlock | RedactedThinkingBlock] + Anthropic thinking blocks from the LLM response +- `responses_reasoning_item`: ReasoningItemModel | None + OpenAI Responses reasoning item from model output +- `action`: Action | None + Single tool call returned by LLM (None when non-executable) +- `tool_name`: str + The name of the tool being called +- `tool_call_id`: ToolCallID + The unique id returned by LLM API for this tool call +- `tool_call`: MessageToolCall + The tool call received from the LLM response. We keep a copy of it so it is easier to construct it into LLM messageThis could be different from `action`: e.g., `tool_call` may contain `security_risk` field predicted by LLM when LLM risk analyzer is enabled, while `action` does not. +- `llm_response_id`: EventID + Completion or Response ID of the LLM response that generated this eventE.g., Can be used to group related actions from same LLM response. This helps in tracking and managing results of parallel function calling from the same LLM response. +- `security_risk`: risk.SecurityRisk + The LLM +- `critic_result`: CriticResult | None + Optional critic evaluation of this action and preceding history. +- `summary`: str | None + A concise summary (approximately 10 words) of what this action does, provided by the LLM for explainability and debugging. Examples of good summaries: +- `visualize`: Text + Return Rich Text representation of this action event. -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +### Methods -#### Methods +**to_llm_message() -> Message** -#### static events_to_messages() +Individual message - may be incomplete for multi-action batches -Convert event stream to LLM message stream, handling multi-action batches +## class AgentErrorEvent -#### abstractmethod to_llm_message() +Bases: `ObservationBaseEvent` -### class MessageEvent +Error triggered by the agent. -Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent) +Note: This event should not contain model "thought" or "reasoning_content". It +represents an error produced by the agent/scaffold, not model output. -Message from either agent or user. +### Properties -This is originally the “MessageAction”, but it suppose not to be tool call. +- `source`: SourceType +- `error`: str + The error message from the scaffold +- `visualize`: Text + Return Rich Text representation of this agent error event. + +### Methods + +**to_llm_message() -> Message** +## class MessageEvent + +Bases: `LLMConvertibleEvent` + +Message from either agent or user. -#### Properties +This is originally the "MessageAction", but it suppose not to be tool call. +### Properties + +- `model_config`: ConfigDict +- `source`: SourceType +- `llm_message`: Message + The exact LLM message for this message event +- `llm_response_id`: EventID | None + Completion or Response ID of the LLM response that generated this eventIf the source != - `activated_skills`: list[str] -- `critic_result`: CriticResult | None + List of activated skill name - `extended_content`: list[TextContent] -- `llm_message`: Message -- `llm_response_id`: str | None -- `model_config`: ClassVar[ConfigDict] = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. -- `reasoning_content`: str + List of content added by agent context - `sender`: str | None -- `source`: Literal['agent', 'user', 'environment'] + Optional identifier of the sender. Can be used to track message origin in multi-agent scenarios. +- `critic_result`: CriticResult | None + Optional critic evaluation of this message and preceding history. +- `reasoning_content`: str - `thinking_blocks`: Sequence[ThinkingBlock | RedactedThinkingBlock] Return the Anthropic thinking blocks from the LLM message. - `visualize`: Text Return Rich Text representation of this message event. -#### Methods +### Methods -#### to_llm_message() +**to_llm_message() -> Message** -### class ObservationBaseEvent +## class ObservationBaseEvent -Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent) +Bases: `LLMConvertibleEvent` Base class for anything as a response to a tool call. Examples include tool execution, error, user reject. +### Properties -#### Properties - -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. -- `source`: Literal['agent', 'user', 'environment'] -- `tool_call_id`: str +- `source`: SourceType - `tool_name`: str -### class ObservationEvent + The tool name that this observation is responding to +- `tool_call_id`: ToolCallID + The tool call id that this observation is responding to -Bases: [`ObservationBaseEvent`](#class-observationbaseevent) +## class ObservationEvent +Bases: `ObservationBaseEvent` -#### Properties +### Properties -- `action_id`: str -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. - `observation`: Observation + The observation (tool call) sent to LLM +- `action_id`: EventID + The action id that this observation is responding to - `visualize`: Text Return Rich Text representation of this observation event. -#### Methods +### Methods -#### to_llm_message() +**to_llm_message() -> Message** -### class PauseEvent +## class SystemPromptEvent -Bases: [`Event`](#class-event) - -Event indicating that the agent execution was paused by user request. +Bases: `LLMConvertibleEvent` +System prompt added by the agent. -#### Properties +### Properties -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. -- `source`: Literal['agent', 'user', 'environment'] +- `source`: SourceType +- `system_prompt`: TextContent + The system prompt text +- `tools`: list[ToolDefinition] + List of tools as ToolDefinition objects - `visualize`: Text - Return Rich Text representation of this pause event. -### class SystemPromptEvent + Return Rich Text representation of this system prompt event. -Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent) +### Methods -System prompt added by the agent. +**to_llm_message() -> Message** +## class UserRejectObservation -#### Properties +Bases: `ObservationBaseEvent` -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. -- `source`: Literal['agent', 'user', 'environment'] -- `system_prompt`: TextContent -- `tools`: list[ToolDefinition] +Observation when user rejects an action in confirmation mode. + +### Properties + +- `rejection_reason`: str + Reason for rejecting the action +- `action_id`: EventID + The action id that this observation is responding to - `visualize`: Text - Return Rich Text representation of this system prompt event. + Return Rich Text representation of this user rejection event. -#### Methods +### Methods -#### to_llm_message() +**to_llm_message() -> Message** -### class TokenEvent +## class TokenEvent -Bases: [`Event`](#class-event) +Bases: `Event` Event from VLLM representing token IDs used in LLM interaction. +### Properties -#### Properties - -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +- `source`: SourceType - `prompt_token_ids`: list[int] + The exact prompt token IDs for this message event - `response_token_ids`: list[int] -- `source`: Literal['agent', 'user', 'environment'] -### class UserRejectObservation + The exact response token IDs for this message event -Bases: [`ObservationBaseEvent`](#class-observationbaseevent) +## class PauseEvent -Observation when user rejects an action in confirmation mode. +Bases: `Event` +Event indicating that the agent execution was paused by user request. -#### Properties +### Properties -- `action_id`: str -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. -- `rejection_reason`: str +- `source`: SourceType - `visualize`: Text - Return Rich Text representation of this user rejection event. - -#### Methods - -#### to_llm_message() + Return Rich Text representation of this pause event. diff --git a/sdk/api-reference/openhands.sdk.llm.mdx b/sdk/api-reference/openhands.sdk.llm.mdx index fc63ab18..8e7c6ba7 100644 --- a/sdk/api-reference/openhands.sdk.llm.mdx +++ b/sdk/api-reference/openhands.sdk.llm.mdx @@ -1,30 +1,187 @@ --- title: openhands.sdk.llm -description: API reference for openhands.sdk.llm module +description: API reference for openhands.sdk.llm --- +## class CredentialStore -### class ImageContent +Store and retrieve OAuth credentials for LLM providers. -Bases: `BaseContent` +### Properties +- `credentials_dir`: Path + Get the credentials directory, creating it if necessary. -#### Properties +### Methods -- `image_urls`: list[str] -- `type`: Literal['image'] +**__init__(credentials_dir: Path | None = None)** -#### Methods +Initialize the credential store. -#### model_config = (configuration object) +**Parameters:** -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +- `credentials_dir` *Path | None* – Optional custom directory for storing credentials. + Defaults to ~/.local/share/openhands/auth/ -#### to_llm_dict() +**get(vendor: str) -> OAuthCredentials | None** -Convert to LLM API format. +Get stored credentials for a vendor. + +**Parameters:** + +- `vendor` *str* – The vendor/provider name (e.g., 'openai') + +**Returns:** + +- *OAuthCredentials | None* OAuthCredentials if found and valid, None otherwise + +**save(credentials: OAuthCredentials) -> None** + +Save credentials for a vendor. + +**Parameters:** + +- `credentials` *OAuthCredentials* – The OAuth credentials to save + +**delete(vendor: str) -> bool** + +Delete stored credentials for a vendor. + +**Parameters:** + +- `vendor` *str* – The vendor/provider name + +**Returns:** + +- *bool* True if credentials were deleted, False if they didn't exist + +**update_tokens(vendor: str, access_token: str, refresh_token: str | None, expires_in: int) -> OAuthCredentials | None** + +Update tokens for an existing credential. + +**Parameters:** + +- `vendor` *str* – The vendor/provider name +- `access_token` *str* – New access token +- `refresh_token` *str | None* – New refresh token (if provided) +- `expires_in` *int* – Token expiry in seconds + +**Returns:** + +- *OAuthCredentials | None* Updated credentials, or None if no existing credentials found + +## class OAuthCredentials + +Bases: `BaseModel` + +OAuth credentials for subscription-based LLM access. + +### Properties + +- `type`: Literal['oauth'] +- `vendor`: str + The vendor/provider (e.g., +- `access_token`: str + The OAuth access token +- `refresh_token`: str + The OAuth refresh token +- `expires_at`: int + Unix timestamp (ms) when the access token expires + +### Methods + +**is_expired() -> bool** + +Check if the access token is expired. + +## class OpenAISubscriptionAuth + +Handle OAuth authentication for OpenAI ChatGPT subscription access. + +### Properties + +- `vendor`: str + Get the vendor name. + +### Methods + +**__init__(credential_store: CredentialStore | None = None, oauth_port: int = DEFAULT_OAUTH_PORT)** + +Initialize the OpenAI subscription auth handler. + +**Parameters:** + +- `credential_store` *CredentialStore | None* – Optional custom credential store. +- `oauth_port` *int* – Port for the local OAuth callback server. + +**get_credentials() -> OAuthCredentials | None** + +Get stored credentials if they exist. + +**has_valid_credentials() -> bool** -### class LLM +Check if valid (non-expired) credentials exist. + +**refresh_if_needed() -> OAuthCredentials | None** + +Refresh credentials if they are expired. + +**Returns:** + +- *OAuthCredentials | None* Updated credentials, or None if no credentials exist. + +**Raises:** + +- `RuntimeError` – If token refresh fails. + +**login(open_browser: bool = True) -> OAuthCredentials** + +Perform OAuth login flow. + +This starts a local HTTP server to handle the OAuth callback, +opens the browser for user authentication, and waits for the +callback with the authorization code. + +**Parameters:** + +- `open_browser` *bool* – Whether to automatically open the browser. + +**Returns:** + +- *OAuthCredentials* The obtained OAuth credentials. + +**Raises:** + +- `RuntimeError` – If the OAuth flow fails or times out. + +**logout() -> bool** + +Remove stored credentials. + +**Returns:** + +- *bool* True if credentials were removed, False if none existed. + +**create_llm(model: str = 'gpt-5.2-codex', credentials: OAuthCredentials | None = None, instructions: str | None = None, llm_kwargs: Any = \{\}) -> LLM** + +Create an LLM instance configured for Codex subscription access. + +**Parameters:** + +- `model` *str* – The model to use (must be in OPENAI_CODEX_MODELS). +- `credentials` *OAuthCredentials | None* – OAuth credentials to use. If None, uses stored credentials. +- `instructions` *str | None* – Optional instructions for the Codex model. +- `**llm_kwargs` *Any* – Additional arguments to pass to LLM constructor. + +**Returns:** + +- *LLM* An LLM instance configured for Codex access. + +**Raises:** + +- `ValueError` – If the model is not supported or no credentials available. + +## class LLM Bases: `BaseModel`, `RetryMixin`, `NonNativeToolCallingMixin` @@ -35,241 +192,317 @@ language models through the litellm library. It handles model configuration, API authentication, retry logic, and tool calling capabilities. -#### Example - -```pycon ->>> from openhands.sdk import LLM ->>> from pydantic import SecretStr ->>> llm = LLM( -... model="claude-sonnet-4-20250514", -... api_key=SecretStr("your-api-key"), -... usage_id="my-agent" -... ) ->>> # Use with agent or conversation -``` - - -#### Properties +### Properties +- `model`: str + Model name. - `api_key`: str | SecretStr | None + API key. +- `base_url`: str | None + Custom base URL. - `api_version`: str | None + API version (e.g., Azure). - `aws_access_key_id`: str | SecretStr | None -- `aws_region_name`: str | None - `aws_secret_access_key`: str | SecretStr | None -- `base_url`: str | None -- `caching_prompt`: bool -- `custom_tokenizer`: str | None -- `disable_stop_word`: bool | None -- `disable_vision`: bool | None -- `drop_params`: bool -- `enable_encrypted_reasoning`: bool -- `extended_thinking_budget`: int | None -- `extra_headers`: dict[str, str] | None -- `force_string_serializer`: bool | None -- `input_cost_per_token`: float | None -- `litellm_extra_body`: dict[str, Any] -- `log_completions`: bool -- `log_completions_folder`: str -- `max_input_tokens`: int | None +- `aws_region_name`: str | None +- `openrouter_site_url`: str +- `openrouter_app_name`: str +- `num_retries`: int +- `retry_multiplier`: float +- `retry_min_wait`: int +- `retry_max_wait`: int +- `timeout`: int | None + HTTP timeout in seconds. Default is 300s (5 minutes). Set to None to disable timeout (not recommended for production). - `max_message_chars`: int + Approx max chars in each event/content sent to the LLM. +- `temperature`: float | None + Sampling temperature for response generation. Defaults to 0 for most models and provider default for reasoning models. +- `top_p`: float | None +- `top_k`: float | None +- `max_input_tokens`: int | None + The maximum number of input tokens. Note that this is currently unused, and the value at runtime is actually the total tokens in OpenAI (e.g. 128,000 tokens for GPT-4). - `max_output_tokens`: int | None -- `metrics`: [Metrics](#class-metrics) - Get usage metrics for this LLM instance. - * Returns: - Metrics object containing token usage, costs, and other statistics. -- `model`: str + The maximum number of output tokens. This is sent to the LLM. - `model_canonical_name`: str | None -- `model_config`: ClassVar[ConfigDict] = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. -- `model_info`: dict | None - Returns the model info dictionary. + Optional canonical model name for feature registry lookups. The OpenHands SDK maintains a model feature registry that maps model names to capabilities (e.g., vision support, prompt caching, responses API support). When using proxied or aliased model identifiers, set this field to the canonical model name (e.g., +- `extra_headers`: dict[str, str] | None + Optional HTTP headers to forward to LiteLLM requests. +- `input_cost_per_token`: float | None + The cost per input token. This will available in logs for user. +- `output_cost_per_token`: float | None + The cost per output token. This will available in logs for user. +- `ollama_base_url`: str | None +- `stream`: bool + Enable streaming responses from the LLM. When enabled, the provided `on_token` callback in .completions and .responses will be invoked for each chunk of tokens. +- `drop_params`: bool - `modify_params`: bool + Modify params allows litellm to do transformations like adding a default message, when a message is empty. +- `disable_vision`: bool | None + If model is vision capable, this option allows to disable image processing (useful for cost reduction). +- `disable_stop_word`: bool | None + Disable using of stop word. +- `caching_prompt`: bool + Enable caching of prompts. +- `log_completions`: bool + Enable logging of completions. +- `log_completions_folder`: str + The folder to log LLM completions to. Required if log_completions is True. +- `custom_tokenizer`: str | None + A custom tokenizer to use for token counting. - `native_tool_calling`: bool -- `num_retries`: int -- `ollama_base_url`: str | None -- `openrouter_app_name`: str -- `openrouter_site_url`: str -- `output_cost_per_token`: float | None -- `prompt_cache_retention`: str | None + Whether to use native tool calling. +- `force_string_serializer`: bool | None + Force using string content serializer when sending to LLM API. If None (default), auto-detect based on model. Useful for providers that do not support list content, like HuggingFace and Groq. - `reasoning_effort`: Literal['low', 'medium', 'high', 'xhigh', 'none'] | None + The effort to put into reasoning. This is a string that can be one of - `reasoning_summary`: Literal['auto', 'concise', 'detailed'] | None -- `retry_listener`: SkipJsonSchema[Callable[[int, int, BaseException | None], None] | None] -- `retry_max_wait`: int -- `retry_min_wait`: int -- `retry_multiplier`: float -- `safety_settings`: list[dict[str, str]] | None + The level of detail for reasoning summaries. This is a string that can be one of +- `enable_encrypted_reasoning`: bool + If True, ask for [ +- `prompt_cache_retention`: str | None + Retention policy for prompt cache. Only sent for GPT-5+ models; explicitly stripped for all other models. +- `extended_thinking_budget`: int | None + The budget tokens for extended thinking, supported by Anthropic models. - `seed`: int | None -- `stream`: bool + The seed to use for random number generation. +- `safety_settings`: list[dict[str, str]] | None + Deprecated: Safety settings for models that support them (like Mistral AI and Gemini). This field is deprecated in 1.10.0 and will be removed in 1.15.0. Safety settings are designed for consumer-facing content moderation, which is not relevant for coding agents. +- `usage_id`: str + Unique usage identifier for the LLM. Used for registry lookups, telemetry, and spend tracking. +- `litellm_extra_body`: dict[str, Any] + Additional key-value pairs to pass to litellm +- `retry_listener`: SkipJsonSchema[Callable[[int, int, BaseException | None], None] | None] +- `model_config`: ConfigDict +- `metrics`: Metrics + Get usage metrics for this LLM instance. + +Returns: + Metrics object containing token usage, costs, and other statistics. + +Example: + >>> cost = llm.metrics.accumulated_cost + >>> print(f"Total cost: $\{cost\}") - `telemetry`: Telemetry Get telemetry handler for this LLM instance. - * Returns: + +Returns: Telemetry object for managing logging and metrics callbacks. -- `temperature`: float | None -- `timeout`: int | None -- `top_k`: float | None -- `top_p`: float | None -- `usage_id`: str -#### Methods +Example: + >>> llm.telemetry.set_log_completions_callback(my_callback) +- `is_subscription`: bool + Check if this LLM uses subscription-based authentication. + +Returns True when the LLM was created via `LLM.subscription_login()`, +which uses the ChatGPT subscription Codex backend rather than the +standard OpenAI API. + +Returns: + bool: True if using subscription-based transport, False otherwise. +- `model_info`: dict | None + Returns the model info dictionary. + +### Methods -#### completion() +**restore_metrics(metrics: Metrics) -> None** + +**completion(messages: list[Message], tools: Sequence[ToolDefinition] | None = None, _return_metrics: bool = False, add_security_risk_prediction: bool = False, on_token: TokenCallbackType | None = None, kwargs = \{\}) -> LLMResponse** Generate a completion from the language model. This is the method for getting responses from the model via Completion API. It handles message formatting, tool calling, and response processing. -* Parameters: - * `messages` – List of conversation messages - * `tools` – Optional list of tools available to the model - * `_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 -* Returns: - LLMResponse containing the model’s response and metadata. +**Parameters:** -#### NOTE -Summary field is always added to tool schemas for transparency and -explainability of agent actions. +- `messages` *list[Message]* – List of conversation messages +- `tools` *Sequence[ToolDefinition] | None* – Optional list of tools available to the model +- `_return_metrics` *bool* – Whether to return usage metrics +- `add_security_risk_prediction` *bool* – Add security_risk field to tool schemas +- `on_token` *TokenCallbackType | None* – Optional callback for streaming tokens +- `**kwargs` – Additional arguments passed to the LLM API -* Raises: - `ValueError` – If streaming is requested (not supported). +**Returns:** -#### format_messages_for_llm() +- *LLMResponse* LLMResponse containing the model's response and metadata. -Formats Message objects for LLM consumption. +**Raises:** -#### format_messages_for_responses() +- `ValueError` – If streaming is requested (not supported). -Prepare (instructions, input[]) for the OpenAI Responses API. +**responses(messages: list[Message], tools: Sequence[ToolDefinition] | None = None, include: list[str] | None = None, store: bool | None = None, _return_metrics: bool = False, add_security_risk_prediction: bool = False, on_token: TokenCallbackType | None = None, kwargs = \{\}) -> LLMResponse** -- Skips prompt caching flags and string serializer concerns -- Uses Message.to_responses_value to get either instructions (system) +Alternative invocation path using OpenAI Responses API via LiteLLM. - or input items (others) -- Concatenates system instructions into a single instructions string +Maps Message[] -> (instructions, input[]) and returns LLMResponse. + +**Parameters:** + +- `messages` *list[Message]* – List of conversation messages +- `tools` *Sequence[ToolDefinition] | None* – Optional list of tools available to the model +- `include` *list[str] | None* – Optional list of fields to include in response +- `store` *bool | None* – Whether to store the conversation +- `_return_metrics` *bool* – Whether to return usage metrics +- `add_security_risk_prediction` *bool* – Add security_risk field to tool schemas +- `on_token` *TokenCallbackType | None* – Optional callback for streaming deltas +- `**kwargs` – Additional arguments passed to the API -#### get_token_count() +**vision_is_active() -> bool** -#### is_caching_prompt_active() +**is_caching_prompt_active() -> bool** Check if prompt caching is supported and enabled for current model. -* Returns: - True if prompt caching is supported and enabled for the given - : model. -* Return type: - boolean +**Returns:** -#### classmethod load_from_env() +- *bool* True if prompt caching is supported and enabled for the given +model. -#### classmethod load_from_json() +**uses_responses_api() -> bool** -#### model_post_init() +Whether this model uses the OpenAI Responses API path. -This function is meant to behave like a BaseModel method to initialise private attributes. +**format_messages_for_llm(messages: list[Message]) -> list[dict]** -It takes context as an argument since that’s what pydantic-core passes when calling it. +Formats Message objects for LLM consumption. -* Parameters: - * `self` – The BaseModel instance. - * `context` – The context. +**format_messages_for_responses(messages: list[Message]) -> tuple[str | None, list[dict[str, Any]]]** -#### responses() +Prepare (instructions, input[]) for the OpenAI Responses API. -Alternative invocation path using OpenAI Responses API via LiteLLM. +- Skips prompt caching flags and string serializer concerns +- Uses Message.to_responses_value to get either instructions (system) + or input items (others) +- Concatenates system instructions into a single instructions string +- For subscription mode, system prompts are prepended to user content -Maps Message[] -> (instructions, input[]) and returns LLMResponse. +**get_token_count(messages: list[Message]) -> int** -* Parameters: - * `messages` – List of conversation messages - * `tools` – Optional list of tools available to the model - * `include` – Optional list of fields to include in response - * `store` – Whether to store the conversation - * `_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 (not yet supported) - kwargs* – Additional arguments passed to the API +**load_from_json(json_path: str) -> LLM** -#### NOTE -Summary field is always added to tool schemas for transparency and -explainability of agent actions. +**load_from_env(prefix: str = 'LLM_') -> LLM** -#### restore_metrics() +**subscription_login(vendor: SupportedVendor, model: str, force_login: bool = False, open_browser: bool = True, llm_kwargs = \{\}) -> LLM** -#### uses_responses_api() +Authenticate with a subscription service and return an LLM instance. -Whether this model uses the OpenAI Responses API path. +This method provides subscription-based access to LLM models that are +available through chat subscriptions (e.g., ChatGPT Plus/Pro) rather +than API credits. It handles credential caching, token refresh, and +the OAuth login flow. + +Currently supported vendors: +- "openai": ChatGPT Plus/Pro subscription for Codex models + +Supported OpenAI models: +- gpt-5.1-codex-max +- gpt-5.1-codex-mini +- gpt-5.2 +- gpt-5.2-codex + +**Parameters:** + +- `vendor` *SupportedVendor* – The vendor/provider. Currently only "openai" is supported. +- `model` *str* – The model to use. Must be supported by the vendor's +subscription service. +- `force_login` *bool* – If True, always perform a fresh login even if valid +credentials exist. +- `open_browser` *bool* – Whether to automatically open the browser for the +OAuth login flow. +- `**llm_kwargs` – Additional arguments to pass to the LLM constructor. -#### vision_is_active() +**Returns:** -### class LLMRegistry +- *LLM* An LLM instance configured for subscription-based access. -Bases: `object` +**Raises:** + +- `ValueError` – If the vendor or model is not supported. +- `RuntimeError` – If authentication fails. + +## class LLMRegistry A minimal LLM registry for managing LLM instances by usage ID. This registry provides a simple way to manage multiple LLM instances, avoiding the need to recreate LLMs with the same configuration. - -#### Properties +### Properties - `registry_id`: str - `retry_listener`: Callable[[int, int], None] | None -- `subscriber`: Callable[[[RegistryEvent](#class-registryevent)], None] | None -- `usage_to_llm`: dict[str, [LLM](#class-llm)] - Access the internal usage-ID-to-LLM mapping. +- `subscriber`: Callable[[RegistryEvent], None] | None +- `usage_to_llm`: MappingProxyType[str, LLM] + Access the internal usage-ID-to-LLM mapping (read-only view). -#### Methods +### Methods -#### __init__() +**__init__(retry_listener: Callable[[int, int], None] | None = None)** Initialize the LLM registry. -* Parameters: - `retry_listener` – Optional callback for retry events. +**Parameters:** -#### add() +- `retry_listener` *Callable[[int, int], None] | None* – Optional callback for retry events. + +**subscribe(callback: Callable[[RegistryEvent], None]) -> None** + +Subscribe to registry events. + +**Parameters:** + +- `callback` *Callable[[RegistryEvent], None]* – Function to call when LLMs are created or updated. + +**notify(event: RegistryEvent) -> None** + +Notify subscribers of registry events. + +**Parameters:** + +- `event` *RegistryEvent* – The registry event to notify about. + +**add(llm: LLM) -> None** Add an LLM instance to the registry. -* Parameters: - `llm` – The LLM instance to register. -* Raises: - `ValueError` – If llm.usage_id already exists in the registry. +**Parameters:** + +- `llm` *LLM* – The LLM instance to register. + +**Raises:** + +- `ValueError` – If llm.usage_id already exists in the registry. -#### get() +**get(usage_id: str) -> LLM** Get an LLM instance from the registry. -* Parameters: - `usage_id` – Unique identifier for the LLM usage slot. -* Returns: - The LLM instance. -* Raises: - `KeyError` – If usage_id is not found in the registry. +**Parameters:** -#### list_usage_ids() +- `usage_id` *str* – Unique identifier for the LLM usage slot. -List all registered usage IDs. +**Returns:** -#### notify() +- *LLM* The LLM instance. -Notify subscribers of registry events. +**Raises:** -* Parameters: - `event` – The registry event to notify about. +- `KeyError` – If usage_id is not found in the registry. -#### subscribe() +**list_usage_ids() -> list[str]** -Subscribe to registry events. +List all registered usage IDs. + +## class RegistryEvent -* Parameters: - `callback` – Function to call when LLMs are created or updated. +Bases: `BaseModel` -### class LLMResponse +### Properties + +- `llm`: LLM +- `model_config`: ConfigDict + +## class LLMResponse Bases: `BaseModel` @@ -279,118 +512,111 @@ This type provides a clean interface for LLM completion results, exposing only OpenHands-native types to consumers while preserving access to the raw LiteLLM response for internal use. +### Properties -#### Properties - +- `message`: Message +- `metrics`: MetricsSnapshot +- `raw_response`: ModelResponse | ResponsesAPIResponse +- `model_config`: ConfigDict - `id`: str Get the response ID from the underlying LLM response. - This property provides a clean interface to access the response ID, - supporting both completion mode (ModelResponse) and response API modes - (ResponsesAPIResponse). - * Returns: - The response ID from the LLM response -- `message`: [Message](#class-message) -- `metrics`: [MetricsSnapshot](#class-metricssnapshot) -- `model_config`: ClassVar[ConfigDict] = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. -- `raw_response`: ModelResponse | ResponsesAPIResponse - -#### Methods -#### message +This property provides a clean interface to access the response ID, +supporting both completion mode (ModelResponse) and response API modes +(ResponsesAPIResponse). -The completion message converted to OpenHands Message type +Returns: + The response ID from the LLM response -* Type: - [openhands.sdk.llm.message.Message](#class-message) +## class ImageContent -#### metrics +Bases: `BaseContent` -Snapshot of metrics from the completion request +### Properties -* Type: - [openhands.sdk.llm.utils.metrics.MetricsSnapshot](#class-metricssnapshot) +- `type`: Literal['image'] +- `image_urls`: list[str] -#### raw_response +### Methods -The original LiteLLM response (ModelResponse or -ResponsesAPIResponse) for internal use +**to_llm_dict() -> list[dict[str, str | dict[str, str]]]** -* Type: - litellm.types.utils.ModelResponse | litellm.types.llms.openai.ResponsesAPIResponse +Convert to LLM API format. -### class Message +## class Message Bases: `BaseModel` +### Properties -#### Properties - -- `cache_enabled`: bool -- `contains_image`: bool -- `content`: Sequence[[TextContent](#class-textcontent) | [ImageContent](#class-imagecontent)] -- `force_string_serializer`: bool -- `function_calling_enabled`: bool -- `name`: str | None -- `reasoning_content`: str | None -- `responses_reasoning_item`: [ReasoningItemModel](#class-reasoningitemmodel) | None - `role`: Literal['user', 'system', 'assistant', 'tool'] -- `send_reasoning_content`: bool -- `thinking_blocks`: Sequence[[ThinkingBlock](#class-thinkingblock) | [RedactedThinkingBlock](#class-redactedthinkingblock)] +- `content`: Sequence[TextContent | ImageContent] +- `tool_calls`: list[MessageToolCall] | None - `tool_call_id`: str | None -- `tool_calls`: list[[MessageToolCall](#class-messagetoolcall)] | None -- `vision_enabled`: bool - -#### Methods - -#### classmethod from_llm_chat_message() - -Convert a LiteLLMMessage (Chat Completions) to our Message class. +- `name`: str | None +- `reasoning_content`: str | None + Intermediate reasoning/thinking content from reasoning models +- `thinking_blocks`: Sequence[ThinkingBlock | RedactedThinkingBlock] + Raw Anthropic thinking blocks for extended thinking feature +- `responses_reasoning_item`: ReasoningItemModel | None + OpenAI Responses reasoning item from model output +- `model_config` +- `contains_image`: bool -Provider-agnostic mapping for reasoning: -- Prefer message.reasoning_content if present (LiteLLM normalized field) -- Extract thinking_blocks from content array (Anthropic-specific) +### Methods -#### classmethod from_llm_responses_output() +**to_chat_dict(cache_enabled: bool, vision_enabled: bool, function_calling_enabled: bool, force_string_serializer: bool, send_reasoning_content: bool) -> dict[str, Any]** -Convert OpenAI Responses API output items into a single assistant Message. +Serialize message for OpenAI Chat Completions. -Policy (non-stream): -- Collect assistant text by concatenating output_text parts from message items -- Normalize function_call items to MessageToolCall list +**Parameters:** -#### model_config = (configuration object) +- `cache_enabled` *bool* – Whether prompt caching is active. +- `vision_enabled` *bool* – Whether vision/image processing is enabled. +- `function_calling_enabled` *bool* – Whether native function calling is enabled. +- `force_string_serializer` *bool* – Force string serializer instead of list format. +- `send_reasoning_content` *bool* – Whether to include reasoning_content in output. -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +Chooses the appropriate content serializer and then injects threading keys: +- Assistant tool call turn: role == "assistant" and self.tool_calls +- Tool result turn: role == "tool" and self.tool_call_id (with name) -#### to_chat_dict() +**to_responses_value(vision_enabled: bool) -> str | list[dict[str, Any]]** -Serialize message for OpenAI Chat Completions. +Return serialized form. -Chooses the appropriate content serializer and then injects threading keys: -- Assistant tool call turn: role == “assistant” and self.tool_calls -- Tool result turn: role == “tool” and self.tool_call_id (with name) +Either an instructions string (for system) or input items (for other roles). -#### to_responses_dict() +**to_responses_dict(vision_enabled: bool) -> list[dict[str, Any]]** Serialize message for OpenAI Responses (input parameter). -Produces a list of “input” items for the Responses API: -- system: returns [], system content is expected in ‘instructions’ -- user: one ‘message’ item with content parts -> input_text / input_image +Produces a list of "input" items for the Responses API: +- system: returns [], system content is expected in 'instructions' +- user: one 'message' item with content parts -> input_text / input_image (when vision enabled) - assistant: emits prior assistant content as input_text, and function_call items for tool_calls - tool: emits function_call_output items (one per TextContent) with matching call_id -#### to_responses_value() +**from_llm_chat_message(message: LiteLLMMessage) -> Message** -Return serialized form. +Convert a LiteLLMMessage (Chat Completions) to our Message class. -Either an instructions string (for system) or input items (for other roles). +Provider-agnostic mapping for reasoning: +- Prefer `message.reasoning_content` if present (LiteLLM normalized field) +- Extract `thinking_blocks` from content array (Anthropic-specific) -### class MessageToolCall +**from_llm_responses_output(output: Any) -> Message** + +Convert OpenAI Responses API output items into a single assistant Message. + +Policy (non-stream): +- Collect assistant text by concatenating output_text parts from message items +- Normalize function_call items to MessageToolCall list + +## class MessageToolCall Bases: `BaseModel` @@ -399,111 +625,38 @@ Transport-agnostic tool call representation. One canonical id is used for linking across actions/observations and for Responses function_call_output call_id. +### Properties -#### Properties - -- `arguments`: str - `id`: str + Canonical tool call id - `name`: str + Tool/function name +- `arguments`: str + JSON string of arguments - `origin`: Literal['completion', 'responses'] -- `costs`: list[Cost] -- `response_latencies`: list[ResponseLatency] -- `token_usages`: list[TokenUsage] + Originating API family -#### Methods +### Methods -#### classmethod from_chat_tool_call() +**from_chat_tool_call(tool_call: ChatCompletionMessageToolCall) -> MessageToolCall** Create a MessageToolCall from a Chat Completions tool call. -#### classmethod from_responses_function_call() +**from_responses_function_call(item: ResponseFunctionToolCall | OutputFunctionToolCall) -> MessageToolCall** Create a MessageToolCall from a typed OpenAI Responses function_call item. Note: OpenAI Responses function_call.arguments is already a JSON string. -#### model_config = (configuration object) - -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. - -#### to_chat_dict() +**to_chat_dict() -> dict[str, Any]** Serialize to OpenAI Chat Completions tool_calls format. -#### to_responses_dict() - -Serialize to OpenAI Responses ‘function_call’ input item format. - -#### add_cost() - -#### add_response_latency() - -#### add_token_usage() - -Add a single usage record. - -#### deep_copy() - -Create a deep copy of the Metrics object. - -#### diff() - -Calculate the difference between current metrics and a baseline. - -This is useful for tracking metrics for specific operations like delegates. +**to_responses_dict() -> dict[str, Any]** -* Parameters: - `baseline` – A metrics object representing the baseline state -* Returns: - A new Metrics object containing only the differences since the baseline +Serialize to OpenAI Responses 'function_call' input item format. -#### get() - -Return the metrics in a dictionary. - -#### get_snapshot() - -Get a snapshot of the current metrics without the detailed lists. - -#### initialize_accumulated_token_usage() - -#### log() - -Log the metrics. - -#### merge() - -Merge ‘other’ metrics into this one. - -#### model_config = (configuration object) - -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. - -#### classmethod validate_accumulated_cost() - -### class MetricsSnapshot - -Bases: `BaseModel` - -A snapshot of metrics at a point in time. - -Does not include lists of individual costs, latencies, or token usages. - - -#### Properties - -- `accumulated_cost`: float -- `accumulated_token_usage`: TokenUsage | None -- `max_budget_per_task`: float | None -- `model_name`: str - -#### Methods - -#### model_config = (configuration object) - -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. - -### class ReasoningItemModel +## class ReasoningItemModel Bases: `BaseModel` @@ -511,22 +664,15 @@ OpenAI Responses reasoning item (non-stream, subset we consume). Do not log or render encrypted_content. +### Properties -#### Properties - +- `id`: str | None +- `summary`: list[str] - `content`: list[str] | None - `encrypted_content`: str | None -- `id`: str | None - `status`: str | None -- `summary`: list[str] -#### Methods - -#### model_config = (configuration object) - -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. - -### class RedactedThinkingBlock +## class RedactedThinkingBlock Bases: `BaseModel` @@ -535,31 +681,55 @@ Redacted thinking block for previous responses without extended thinking. This is used as a placeholder for assistant messages that were generated before extended thinking was enabled. +### Properties -#### Properties - -- `data`: str - `type`: Literal['redacted_thinking'] +- `data`: str + The redacted thinking content + +## class TextContent + +Bases: `BaseContent` -#### Methods +### Properties -#### model_config = (configuration object) +- `type`: Literal['text'] +- `text`: str +- `model_config`: ConfigDict -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +### Methods -### class RegistryEvent +**to_llm_dict() -> list[dict[str, str | dict[str, str]]]** + +Convert to LLM API format. + +## class ThinkingBlock Bases: `BaseModel` +Anthropic thinking block for extended thinking feature. + +This represents the raw thinking blocks returned by Anthropic models +when extended thinking is enabled. These blocks must be preserved +and passed back to the API for tool use scenarios. -#### Properties +### Properties -- `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 +- `type`: Literal['thinking'] +- `thinking`: str + The thinking content +- `signature`: str | None + Cryptographic signature for the thinking block -Bases: [`LLM`](#class-llm) +**content_to_str(contents: Sequence[TextContent | ImageContent]) -> list[str]** + +Convert a list of TextContent and ImageContent to a list of strings. + +This is primarily used for display purposes. + +## class RouterLLM + +Bases: `LLM` Base class for multiple LLM acting as a unified LLM. This class provides a foundation for implementing model routing by @@ -570,45 +740,33 @@ Key features: - Delegates all other operations/properties to the selected LLM - Provides routing interface through select_llm() method +### Properties -#### Properties - -- `active_llm`: [LLM](#class-llm) | None -- `llms_for_routing`: dict[str, [LLM](#class-llm)] -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. - `router_name`: str + Name of the router +- `llms_for_routing`: dict[str, LLM] +- `active_llm`: LLM | None + Currently selected LLM instance -#### Methods +### Methods -#### completion() +**validate_llms_not_empty(v)** + +**completion(messages: list[Message], tools: Sequence[ToolDefinition] | None = None, return_metrics: bool = False, add_security_risk_prediction: bool = False, on_token: TokenCallbackType | None = None, kwargs = \{\}) -> LLMResponse** This method intercepts completion calls and routes them to the appropriate underlying LLM based on the routing logic implemented in select_llm(). -* Parameters: - * `messages` – List of conversation messages - * `tools` – Optional list of tools available to the model - * `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 - -#### NOTE -Summary field is always added to tool schemas for transparency and -explainability of agent actions. - -#### model_post_init() +**Parameters:** -This function is meant to behave like a BaseModel method to initialise private attributes. +- `messages` *list[Message]* – List of conversation messages +- `tools` *Sequence[ToolDefinition] | None* – Optional list of tools available to the model +- `return_metrics` *bool* – Whether to return usage metrics +- `add_security_risk_prediction` *bool* – Add security_risk field to tool schemas +- `on_token` *TokenCallbackType | None* – Optional callback for streaming tokens +- `**kwargs` – Additional arguments passed to the LLM API -It takes context as an argument since that’s what pydantic-core passes when calling it. - -* Parameters: - * `self` – The BaseModel instance. - * `context` – The context. - -#### abstractmethod select_llm() +**abstractmethod select_llm(messages: list[Message]) -> str** Select which LLM to use based on messages and events. @@ -616,56 +774,15 @@ This method implements the core routing logic for the RouterLLM. Subclasses should analyze the provided messages to determine which LLM from llms_for_routing is most appropriate for handling the request. -* Parameters: - `messages` – List of messages in the conversation that can be used - to inform the routing decision. -* Returns: - The key/name of the LLM to use from llms_for_routing dictionary. - -#### classmethod set_placeholder_model() - -Guarantee model exists before LLM base validation runs. - -#### classmethod validate_llms_not_empty() - -### class TextContent - -Bases: `BaseContent` - - -#### Properties - -- `enable_truncation`: bool -- `model_config`: ClassVar[ConfigDict] = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. -- `text`: str -- `type`: Literal['text'] - -#### Methods - -#### to_llm_dict() - -Convert to LLM API format. - -### class ThinkingBlock - -Bases: `BaseModel` - -Anthropic thinking block for extended thinking feature. +**Parameters:** -This represents the raw thinking blocks returned by Anthropic models -when extended thinking is enabled. These blocks must be preserved -and passed back to the API for tool use scenarios. +- `messages` *list[Message]* – List of messages in the conversation that can be used + to inform the routing decision. +**Returns:** -#### Properties - -- `signature`: str | None -- `thinking`: str -- `type`: Literal['thinking'] - -#### Methods +- *str* The key/name of the LLM to use from llms_for_routing dictionary. -#### model_config = (configuration object) +**set_placeholder_model(data)** -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +Guarantee `model` exists before LLM base validation runs. diff --git a/sdk/api-reference/openhands.sdk.security.mdx b/sdk/api-reference/openhands.sdk.security.mdx index 41fdd321..7e98b3c5 100644 --- a/sdk/api-reference/openhands.sdk.security.mdx +++ b/sdk/api-reference/openhands.sdk.security.mdx @@ -1,10 +1,9 @@ --- title: openhands.sdk.security -description: API reference for openhands.sdk.security module +description: API reference for openhands.sdk.security --- - -### class SecurityRisk +## class SecurityRisk Bases: `str`, `Enum` @@ -13,29 +12,24 @@ Security risk levels for actions. Based on OpenHands security risk levels but adapted for agent-sdk. Integer values allow for easy comparison and ordering. +### Properties -#### Properties - +- `UNKNOWN` +- `LOW` +- `MEDIUM` +- `HIGH` - `description`: str Get a human-readable description of the risk level. - `visualize`: Text Return Rich Text representation of this risk level. -#### Methods - -#### HIGH = 'HIGH' - -#### LOW = 'LOW' - -#### MEDIUM = 'MEDIUM' +### Methods -#### UNKNOWN = 'UNKNOWN' - -#### get_color() +**get_color() -> str** Get the color for displaying this risk level in Rich text. -#### is_riskier() +**is_riskier(other: SecurityRisk, reflexive: bool = True) -> bool** Check if this risk level is riskier than another. @@ -45,18 +39,21 @@ 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) + +This can be disabled by setting the `reflexive` parameter to False. + +**Parameters:** - # More concretely: - assert SecurityRisk.HIGH.is_riskier(SecurityRisk.HIGH) - assert SecurityRisk.MEDIUM.is_riskier(SecurityRisk.MEDIUM) - assert SecurityRisk.LOW.is_riskier(SecurityRisk.LOW) +- `other` *SecurityRisk* – The other risk level to compare against. +- `reflexive` *bool* – Whether the relationship is reflexive. -This can be disabled by setting the reflexive parameter to False. +**Raises:** -* Parameters: - other ([SecurityRisk*](#class-securityrisk)) – The other risk level to compare against. - reflexive (bool*) – Whether the relationship is reflexive. -* Raises: - `ValueError` – If either risk level is UNKNOWN. +- `ValueError` – If either risk level is UNKNOWN. diff --git a/sdk/api-reference/openhands.sdk.tool.mdx b/sdk/api-reference/openhands.sdk.tool.mdx index 62b85a29..fc38d753 100644 --- a/sdk/api-reference/openhands.sdk.tool.mdx +++ b/sdk/api-reference/openhands.sdk.tool.mdx @@ -1,139 +1,122 @@ --- title: openhands.sdk.tool -description: API reference for openhands.sdk.tool module +description: API reference for openhands.sdk.tool --- +## class FinishTool -### class Action +Bases: `ToolDefinition[FinishAction, FinishObservation]` -Bases: `Schema`, `ABC` +Tool for signaling the completion of a task or conversation. -Base schema for input action. +### Methods +**create(conv_state: ConversationState | None = None, params = \{\}) -> Sequence[Self]** -#### Properties +Create FinishTool instance. -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. -- `visualize`: Text - 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 +**Parameters:** -Bases: `Protocol` +- `conv_state` *ConversationState | None* – Optional conversation state (not used by FinishTool). +- `**params` – Additional parameters (none supported). -Protocol for tools that are guaranteed to have a non-None executor. +**Returns:** -This eliminates the need for runtime None checks and type narrowing -when working with tools that are known to be executable. +- *Sequence[Self]* A sequence containing a single FinishTool instance. +**Raises:** -#### Properties +- `ValueError` – If any parameters are provided. -- `executor`: [ToolExecutor](#class-toolexecutor)[Any, Any] -- `name`: str +## class ThinkTool -#### Methods +Bases: `ToolDefinition[ThinkAction, ThinkObservation]` -#### __init__() +Tool for logging thoughts without making changes. -### class FinishTool +### Methods -Bases: `ToolDefinition[FinishAction, FinishObservation]` +**create(conv_state: ConversationState | None = None, params = \{\}) -> Sequence[Self]** -Tool for signaling the completion of a task or conversation. +Create ThinkTool instance. +**Parameters:** -#### Properties +- `conv_state` *ConversationState | None* – Optional conversation state (not used by ThinkTool). +- `**params` – Additional parameters (none supported). -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +**Returns:** -#### Methods +- *Sequence[Self]* A sequence containing a single ThinkTool instance. -#### classmethod create() +**Raises:** -Create FinishTool instance. +- `ValueError` – If any parameters are provided. + +**list_registered_tools() -> list[str]** -* Parameters: - * `conv_state` – Optional conversation state (not used by FinishTool). - params* – Additional parameters (none supported). -* Returns: - A sequence containing a single FinishTool instance. -* Raises: - `ValueError` – If any parameters are provided. +**register_tool(name: str, factory: ToolDefinition | type[ToolDefinition] | Callable[..., Sequence[ToolDefinition]]) -> None** -#### name = 'finish' +**resolve_tool(tool_spec: Tool, conv_state: ConversationState) -> Sequence[ToolDefinition]** -### class Observation +## class Action Bases: `Schema`, `ABC` -Base schema for output observation. +Base schema for input action. + +### Properties + +- `visualize`: Text + 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. -#### Properties +## class Observation -- `ERROR_MESSAGE_HEADER`: ClassVar[str] = '[An error occurred during execution.]n' +Bases: `Schema`, `ABC` + +Base schema for output observation. + +### Properties + +- `ERROR_MESSAGE_HEADER`: str - `content`: list[TextContent | ImageContent] + Content returned from the tool as a list of TextContent/ImageContent objects. When there is an error, it should be written in this field. - `is_error`: bool -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + Whether the observation indicates an error - `text`: str Extract all text content from the observation. - * Returns: + +Returns: Concatenated text from all TextContent items in content. - `to_llm_content`: Sequence[TextContent | ImageContent] Default content formatting for converting observation to LLM readable content. - Subclasses can override to provide richer content (e.g., images, diffs). +Subclasses can override to provide richer content (e.g., images, diffs). - `visualize`: Text Return Rich Text representation of this observation. - Subclasses can override for custom visualization; by default we show the - same text that would be sent to the LLM. - -#### Methods - -#### classmethod from_text() - -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. -* Returns: - An Observation instance with the text wrapped in a TextContent. - -### class ThinkTool -Bases: `ToolDefinition[ThinkAction, ThinkObservation]` - -Tool for logging thoughts without making changes. +Subclasses can override for custom visualization; by default we show the +same text that would be sent to the LLM. +### Methods -#### Properties +**from_text(text: str, is_error: bool = False, kwargs: Any = \{\}) -> Self** -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. - -#### Methods +Utility to create an Observation from a simple text string. -#### classmethod create() +**Parameters:** -Create ThinkTool instance. +- `text` *str* – The text content to include in the observation. +- `is_error` *bool* – Whether this observation represents an error. +- `**kwargs` *Any* – Additional fields for the observation subclass. -* Parameters: - * `conv_state` – Optional conversation state (not used by ThinkTool). - params* – Additional parameters (none supported). -* Returns: - A sequence containing a single ThinkTool instance. -* Raises: - `ValueError` – If any parameters are provided. +**Returns:** -#### name = 'think' +- *Self* An Observation instance with the text wrapped in a TextContent. -### class Tool +## class Tool Bases: `BaseModel` @@ -141,48 +124,63 @@ Defines a tool to be initialized for the agent. This is only used in agent-sdk for type schema for server use. - -#### Properties +### Properties - `name`: str + Name of the tool class, e.g., - `params`: dict[str, Any] + Parameters for the tool -#### Methods - -#### model_config = (configuration object) - -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +### Methods -#### classmethod validate_name() +**validate_name(v: str) -> str** Validate that name is not empty. -#### classmethod validate_params() +**validate_params(v: dict[str, Any] | None) -> dict[str, Any]** Convert None params to empty dict. -### class ToolAnnotations +## class ExecutableTool + +Bases: `Protocol` + +Protocol for tools that are guaranteed to have a non-None executor. + +This eliminates the need for runtime None checks and type narrowing +when working with tools that are known to be executable. + +### Properties + +- `name`: str +- `executor`: ToolExecutor[Any, Any] + +## class ToolAnnotations Bases: `BaseModel` -Annotations to provide hints about the tool’s behavior. +Annotations to provide hints about the tool's behavior. Based on Model Context Protocol (MCP) spec: -[https://github.com/modelcontextprotocol/modelcontextprotocol/blob/caf3424488b10b4a7b1f8cb634244a450a1f4400/schema/2025-06-18/schema.ts#L838](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/caf3424488b10b4a7b1f8cb634244a450a1f4400/schema/2025-06-18/schema.ts#L838) - +https://github.com/modelcontextprotocol/modelcontextprotocol/blob/caf3424488b10b4a7b1f8cb634244a450a1f4400/schema/2025-06-18/schema.ts#L838 -#### Properties +### Properties +- `model_config`: ConfigDict +- `title`: str | None + A human-readable title for the tool. +- `readOnlyHint`: bool + If true, the tool does not modify its environment. Default: false - `destructiveHint`: bool + If true, the tool may perform destructive updates to its environment. If false, the tool performs only additive updates. (This property is meaningful only when `readOnlyHint == false`) Default: true - `idempotentHint`: bool -- `model_config`: ClassVar[ConfigDict] = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + If true, calling the tool repeatedly with the same arguments will have no additional effect on the its environment. (This property is meaningful only when `readOnlyHint == false`) Default: false - `openWorldHint`: bool -- `readOnlyHint`: bool -- `title`: str | None -### class ToolDefinition + If true, this tool may interact with an + +## class ToolDefinition -Bases: `DiscriminatedUnionMixin`, `ABC`, `Generic` +Bases: `DiscriminatedUnionMixin`, `ABC` Base class for all tool implementations. @@ -196,166 +194,153 @@ Features: - Coerce outputs only if an output model is defined; else return vanilla JSON. - Export MCP tool description. -#### Examples +**Example:** Simple tool with no parameters: -: class FinishTool(ToolDefinition[FinishAction, FinishObservation]): - : @classmethod - def create(cls, conv_state=None, - `
` - ``` - ** - ``` - `
` - params): - `
` - > return [cls(name=”finish”, …, executor=FinishExecutor())] + class FinishTool(ToolDefinition[FinishAction, FinishObservation]): + @classmethod + 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, - `
` - ``` - ** - ``` - `
` - params): - `
` - > executor = TerminalExecutor( - > : working_dir=conv_state.workspace.working_dir, - > `
` - > ``` - > ** - > ``` - > `
` - > params, - `
` - > ) - > return [cls(name=”terminal”, …, executor=executor)] - - -#### Properties - -- `action_type`: type[[Action](#class-action)] -- `annotations`: [ToolAnnotations](#class-toolannotations) | None + class TerminalTool(ToolDefinition[TerminalAction, + TerminalObservation]): + @classmethod + def create(cls, conv_state, **params): + executor = TerminalExecutor( + working_dir=conv_state.workspace.working_dir, + **params, + ) + return [cls(name="terminal", ..., executor=executor)] + +### Properties + +- `model_config`: ConfigDict +- `name`: str - `description`: str -- `executor`: Annotated[[ToolExecutor](#class-toolexecutor) | None, SkipJsonSchema()] +- `action_type`: type[Action] +- `observation_type`: type[Observation] | None +- `annotations`: ToolAnnotations | None - `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]. -- `name`: ClassVar[str] = '' -- `observation_type`: type[[Observation](#class-observation)] | None +- `executor`: SkipJsonSchema[ToolExecutor | None] - `title`: str -#### Methods +### Methods -#### action_from_arguments() +**abstractmethod create(args = (), kwargs = \{\}) -> Sequence[Self]** -Create an action from parsed arguments. +Create a sequence of Tool instances. -This method can be overridden by subclasses to provide custom logic -for creating actions from arguments (e.g., for MCP tools). +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. -* Parameters: - `arguments` – The parsed arguments from the tool call. -* Returns: - The action instance created from the arguments. +**Returns:** -#### as_executable() +- *Sequence[Self]* A sequence of Tool instances. Even single tools are returned as a sequence +- *Sequence[Self]* to provide a consistent interface and eliminate union return types. + +**set_executor(executor: ToolExecutor) -> Self** + +Create a new Tool instance with the given executor. + +**as_executable() -> ExecutableTool** Return this tool as an ExecutableTool, ensuring it has an executor. This method eliminates the need for runtime None checks by guaranteeing that the returned tool has a non-None executor. -* Returns: - This tool instance, typed as ExecutableTool. -* Raises: - `NotImplementedError` – If the tool has no executor. +**Returns:** -#### abstractmethod classmethod create() +- *ExecutableTool* This tool instance, typed as ExecutableTool. -Create a sequence of Tool instances. +**Raises:** -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. +- `NotImplementedError` – If the tool has no executor. -* 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. +**action_from_arguments(arguments: dict[str, Any]) -> Action** -#### classmethod resolve_kind() +Create an action from parsed arguments. -Resolve a kind string to its corresponding tool class. +This method can be overridden by subclasses to provide custom logic +for creating actions from arguments (e.g., for MCP tools). -* Parameters: - `kind` – The name of the tool class to resolve -* Returns: - The tool class corresponding to the kind -* Raises: - `ValueError` – If the kind is unknown +**Parameters:** -#### set_executor() +- `arguments` *dict[str, Any]* – The parsed arguments from the tool call. -Create a new Tool instance with the given executor. +**Returns:** + +- *Action* The action instance created from the arguments. -#### to_mcp_tool() +**to_mcp_tool(input_schema: dict[str, Any] | None = None, output_schema: dict[str, Any] | None = None) -> dict[str, Any]** 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. +**Parameters:** + +- `input_schema` *dict[str, Any] | None* – Optionally override the input schema. +- `output_schema` *dict[str, Any] | None* – Optionally override the output schema. -#### to_openai_tool() +**to_openai_tool(add_security_risk_prediction: bool = False, action_type: type[Schema] | None = None) -> ChatCompletionToolParam** Convert a Tool to an OpenAI tool. -* 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. - * `action_type` – Optionally override the action_type to use for the schema. - This is useful for MCPTool to use a dynamically created action type - based on the tool’s input schema. +**Parameters:** -#### NOTE -Summary field is always added to the schema for transparency and -explainability of agent actions. +- `add_security_risk_prediction` *bool* – 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. +- `action_type` *type[Schema] | None* – Optionally override the action_type to use for the schema. +This is useful for MCPTool to use a dynamically created action type +based on the tool's input schema. -#### to_responses_tool() +**to_responses_tool(add_security_risk_prediction: bool = False, action_type: type[Schema] | None = None) -> FunctionToolParam** Convert a Tool to a Responses API function tool (LiteLLM typed). For Responses API, function tools expect top-level keys: -(JSON configuration object) +\{ "type": "function", "name": ..., "description": ..., "parameters": ... \} + +**Parameters:** + +- `add_security_risk_prediction` *bool* – Whether to add a `security_risk` field +- `action_type` *type[Schema] | None* – Optional override for the action type + +**resolve_kind(kind: str) -> type** + +Resolve a kind string to its corresponding tool class. + +**Parameters:** + +- `kind` *str* – The name of the tool class to resolve + +**Returns:** + +- *type* The tool class corresponding to the kind -* Parameters: - * `add_security_risk_prediction` – Whether to add a security_risk field - * `action_type` – Optional override for the action type +**Raises:** -#### NOTE -Summary field is always added to the schema for transparency and -explainability of agent actions. +- `ValueError` – If the kind is unknown -### class ToolExecutor +## class ToolExecutor -Bases: `ABC`, `Generic` +Bases: `ABC` Executor function type for a Tool. -#### Methods +### Methods -#### close() +**close() -> None** Close the executor and clean up resources. diff --git a/sdk/api-reference/openhands.sdk.utils.mdx b/sdk/api-reference/openhands.sdk.utils.mdx index 45ba17dc..63472e53 100644 --- a/sdk/api-reference/openhands.sdk.utils.mdx +++ b/sdk/api-reference/openhands.sdk.utils.mdx @@ -1,38 +1,35 @@ --- title: openhands.sdk.utils -description: API reference for openhands.sdk.utils module +description: API reference for openhands.sdk.utils --- +**sanitized_env(env: Mapping[str, str] | None = None) -> dict[str, str]** -Utility functions for the OpenHands SDK. +Return a copy of *env* with sanitized values. -### deprecated() +PyInstaller-based binaries rewrite ``LD_LIBRARY_PATH`` so their vendored +libraries win. This function restores the original value so that subprocess +will not use them. + +**deprecated(deprecated_in: str, removed_in: str | date | None, current_version: str | None = None, details: str = '') -> Callable[[_FuncT], _FuncT]** Return a decorator that deprecates a callable with explicit metadata. Use this helper when you can annotate a function, method, or property with -@deprecated(…). It transparently forwards to `deprecation.deprecated()` -while filling in the SDK’s current version metadata unless custom values are +`@deprecated(...)`. It transparently forwards to :func:`deprecation.deprecated` +while filling in the SDK's current version metadata unless custom values are supplied. -### maybe_truncate() - -Truncate the middle of content if it exceeds the specified length. +**warn_deprecated(feature: str, deprecated_in: str, removed_in: str | date | None, current_version: str | None = None, details: str = '', stacklevel: int = 2) -> None** -Keeps the head and tail of the content to preserve context at both ends. -Optionally saves the full content to a file for later investigation. +Emit a deprecation warning for dynamic access to a legacy feature. -* Parameters: - * `content` – The text content to potentially truncate - * `truncate_after` – Maximum length before truncation. If None, no truncation occurs - * `truncate_notice` – Notice to insert in the middle when content is truncated - * `save_dir` – Working directory to save full content file in - * `tool_prefix` – Prefix for the saved file (e.g., “bash”, “browser”, “editor”) -* Returns: - Original content if under limit, or truncated content with head and tail - preserved and reference to saved file if applicable +Prefer this helper when a decorator is not practical—e.g. attribute accessors, +data migrations, or other runtime paths that must conditionally warn. Provide +explicit version metadata so the SDK reports consistent messages and upgrades +to :class:`deprecation.UnsupportedWarning` after the removal threshold. -### sanitize_openhands_mentions() +**sanitize_openhands_mentions(text: str) -> str** Sanitize @OpenHands mentions in text to prevent self-mention loops. @@ -40,27 +37,41 @@ This function inserts a zero-width joiner (ZWJ) after the @ symbol in @OpenHands mentions, making them non-clickable in GitHub comments while preserving readability. The original case of the mention is preserved. -* Parameters: - `text` – The text to sanitize -* Returns: - Text with sanitized @OpenHands mentions (e.g., “@OpenHands” -> “@‍OpenHands”) +**Parameters:** + +- `text` *str* – The text to sanitize + +**Returns:** + +- *str* Text with sanitized @OpenHands mentions (e.g., "@OpenHands" -> "@‍OpenHands") -### Examples +**Example:** -```pycon +```python >>> sanitize_openhands_mentions("Thanks @OpenHands for the help!") -'Thanks @u200dOpenHands for the help!' +'Thanks @\u200dOpenHands for the help!' >>> sanitize_openhands_mentions("Check @openhands and @OPENHANDS") -'Check @u200dopenhands and @u200dOPENHANDS' +'Check @\u200dopenhands and @\u200dOPENHANDS' >>> sanitize_openhands_mentions("No mention here") 'No mention here' ``` -### warn_deprecated() +**maybe_truncate(content: str, truncate_after: int | None = None, truncate_notice: str = DEFAULT_TRUNCATE_NOTICE, save_dir: str | None = None, tool_prefix: str = 'output') -> str** -Emit a deprecation warning for dynamic access to a legacy feature. +Truncate the middle of content if it exceeds the specified length. -Prefer this helper when a decorator is not practical—e.g. attribute accessors, -data migrations, or other runtime paths that must conditionally warn. Provide -explicit version metadata so the SDK reports consistent messages and upgrades -to `deprecation.UnsupportedWarning` after the removal threshold. +Keeps the head and tail of the content to preserve context at both ends. +Optionally saves the full content to a file for later investigation. + +**Parameters:** + +- `content` *str* – The text content to potentially truncate +- `truncate_after` *int | None* – Maximum length before truncation. If None, no truncation occurs +- `truncate_notice` *str* – Notice to insert in the middle when content is truncated +- `save_dir` *str | None* – Working directory to save full content file in +- `tool_prefix` *str* – Prefix for the saved file (e.g., "bash", "browser", "editor") + +**Returns:** + +- *str* Original content if under limit, or truncated content with head and tail +- *str* preserved and reference to saved file if applicable diff --git a/sdk/api-reference/openhands.sdk.workspace.mdx b/sdk/api-reference/openhands.sdk.workspace.mdx index 48066655..56564da7 100644 --- a/sdk/api-reference/openhands.sdk.workspace.mdx +++ b/sdk/api-reference/openhands.sdk.workspace.mdx @@ -1,10 +1,9 @@ --- title: openhands.sdk.workspace -description: API reference for openhands.sdk.workspace module +description: API reference for openhands.sdk.workspace --- - -### class BaseWorkspace +## class BaseWorkspace Bases: `DiscriminatedUnionMixin`, `ABC` @@ -14,281 +13,270 @@ Workspaces provide a sandboxed environment where agents can execute commands, read/write files, and perform other operations. All workspace implementations support the context manager protocol for safe resource management. -#### Example - -```pycon ->>> with workspace: -... result = workspace.execute_command("echo 'hello'") -... content = workspace.read_file("example.txt") -``` +### Properties +- `working_dir`: Annotated[str, BeforeValidator(_convert_path_to_str), Field(description='The working directory for agent operations and tool execution. Accepts both string paths and Path objects. Path objects are automatically converted to strings.')] -#### Properties +### Methods -- `working_dir`: Annotated[str, BeforeValidator(func=_convert_path_to_str, json_schema_input_type=PydanticUndefined), FieldInfo(annotation=NoneType, required=True, description='The working directory for agent operations and tool execution. Accepts both string paths and Path objects. Path objects are automatically converted to strings.')] +**abstractmethod execute_command(command: str, cwd: str | Path | None = None, timeout: float = 30.0) -> CommandResult** -#### Methods +Execute a bash command on the system. -#### abstractmethod execute_command() +**Parameters:** -Execute a bash command on the system. +- `command` *str* – The bash command to execute +- `cwd` *str | Path | None* – Working directory for the command (optional) +- `timeout` *float* – Timeout in seconds (defaults to 30.0) -* Parameters: - * `command` – The bash command to execute - * `cwd` – Working directory for the command (optional) - * `timeout` – Timeout in seconds (defaults to 30.0) -* Returns: - Result containing stdout, stderr, exit_code, and other - : metadata -* Return type: - [CommandResult](#class-commandresult) -* Raises: - `Exception` – If command execution fails +**Returns:** -#### abstractmethod file_download() +- *CommandResult* Result containing stdout, stderr, exit_code, and other +metadata -Download a file from the system. +**Raises:** -* Parameters: - * `source_path` – Path to the source file on the system - * `destination_path` – Path where the file should be downloaded -* Returns: - Result containing success status and metadata -* Return type: - [FileOperationResult](#class-fileoperationresult) -* Raises: - `Exception` – If file download fails +- `Exception` – If command execution fails -#### abstractmethod file_upload() +**abstractmethod file_upload(source_path: str | Path, destination_path: str | Path) -> FileOperationResult** Upload a file to the system. -* Parameters: - * `source_path` – Path to the source file - * `destination_path` – Path where the file should be uploaded -* Returns: - Result containing success status and metadata -* Return type: - [FileOperationResult](#class-fileoperationresult) -* Raises: - `Exception` – If file upload fails +**Parameters:** -#### abstractmethod git_changes() +- `source_path` *str | Path* – Path to the source file +- `destination_path` *str | Path* – Path where the file should be uploaded -Get the git changes for the repository at the path given. +**Returns:** -* 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 +- *FileOperationResult* Result containing success status and metadata -#### abstractmethod git_diff() +**Raises:** -Get the git diff for the file at the path given. +- `Exception` – If file upload fails -* 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 +**abstractmethod file_download(source_path: str | Path, destination_path: str | Path) -> FileOperationResult** -#### model_config = (configuration object) +Download a file from the system. -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +**Parameters:** -#### pause() +- `source_path` *str | Path* – Path to the source file on the system +- `destination_path` *str | Path* – Path where the file should be downloaded -Pause the workspace to conserve resources. +**Returns:** -For local workspaces, this is a no-op. -For container-based workspaces, this pauses the container. +- *FileOperationResult* Result containing success status and metadata -* Raises: - `NotImplementedError` – If the workspace type does not support pausing. +**Raises:** -#### resume() +- `Exception` – If file download fails -Resume a paused workspace. +**abstractmethod git_changes(path: str | Path) -> list[GitChange]** -For local workspaces, this is a no-op. -For container-based workspaces, this resumes the container. +Get the git changes for the repository at the path given. -* Raises: - `NotImplementedError` – If the workspace type does not support resuming. +**Parameters:** -### class CommandResult +- `path` *str | Path* – Path to the git repository -Bases: `BaseModel` +**Returns:** -Result of executing a command in the workspace. +- *list[GitChange]* list[GitChange]: List of changes +**Raises:** -#### Properties +- `Exception` – If path is not a git repository or getting changes failed -- `command`: str -- `exit_code`: int -- `stderr`: str -- `stdout`: str -- `timeout_occurred`: bool +**abstractmethod git_diff(path: str | Path) -> GitDiff** -#### Methods +Get the git diff for the file at the path given. -#### model_config = (configuration object) +**Parameters:** -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +- `path` *str | Path* – Path to the file -### class FileOperationResult +**Returns:** -Bases: `BaseModel` +- *GitDiff* Git diff -Result of a file upload or download operation. +**Raises:** +- `Exception` – If path is not a git repository or getting diff failed -#### Properties +**pause() -> None** -- `destination_path`: str -- `error`: str | None -- `file_size`: int | None -- `source_path`: str -- `success`: bool +Pause the workspace to conserve resources. -#### Methods +For local workspaces, this is a no-op. +For container-based workspaces, this pauses the container. -#### model_config = (configuration object) +**Raises:** -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +- `NotImplementedError` – If the workspace type does not support pausing. -### class LocalWorkspace +**resume() -> None** -Bases: [`BaseWorkspace`](#class-baseworkspace) +Resume a paused workspace. -Local workspace implementation that operates on the host filesystem. +For local workspaces, this is a no-op. +For container-based workspaces, this resumes the container. -LocalWorkspace provides direct access to the local filesystem and command execution -environment. It’s suitable for development and testing scenarios where the agent -should operate directly on the host system. +**Raises:** -#### Example +- `NotImplementedError` – If the workspace type does not support resuming. -```pycon ->>> workspace = LocalWorkspace(working_dir="/path/to/project") ->>> with workspace: -... result = workspace.execute_command("ls -la") -... content = workspace.read_file("README.md") -``` +## class LocalWorkspace -#### Methods +Bases: `BaseWorkspace` -#### __init__() +Local workspace implementation that operates on the host filesystem. -Create a new model by parsing and validating input data from keyword arguments. +LocalWorkspace provides direct access to the local filesystem and command execution +environment. It's suitable for development and testing scenarios where the agent +should operate directly on the host system. -Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model. +### Methods -self is explicitly positional-only to allow self as a field name. +**__init__(working_dir: str | Path, kwargs: Any = \{\})** -#### execute_command() +**execute_command(command: str, cwd: str | Path | None = None, timeout: float = 30.0) -> CommandResult** Execute a bash command locally. Uses the shared shell execution utility to run commands with proper timeout handling, output streaming, and error management. -* Parameters: - * `command` – The bash command to execute - * `cwd` – Working directory (optional) - * `timeout` – Timeout in seconds -* Returns: - Result with stdout, stderr, exit_code, command, and - : timeout_occurred -* Return type: - [CommandResult](#class-commandresult) - -#### file_download() +**Parameters:** -Download (copy) a file locally. +- `command` *str* – The bash command to execute +- `cwd` *str | Path | None* – Working directory (optional) +- `timeout` *float* – Timeout in seconds -For local systems, file download is implemented as a file copy operation -using shutil.copy2 to preserve metadata. +**Returns:** -* Parameters: - * `source_path` – Path to the source file - * `destination_path` – Path where the file should be copied -* Returns: - Result with success status and file information -* Return type: - [FileOperationResult](#class-fileoperationresult) +- *CommandResult* Result with stdout, stderr, exit_code, command, and +timeout_occurred -#### file_upload() +**file_upload(source_path: str | Path, destination_path: str | Path) -> FileOperationResult** Upload (copy) a file locally. For local systems, file upload is implemented as a file copy operation using shutil.copy2 to preserve metadata. -* Parameters: - * `source_path` – Path to the source file - * `destination_path` – Path where the file should be copied -* Returns: - Result with success status and file information -* Return type: - [FileOperationResult](#class-fileoperationresult) +**Parameters:** + +- `source_path` *str | Path* – Path to the source file +- `destination_path` *str | Path* – Path where the file should be copied -#### git_changes() +**Returns:** + +- *FileOperationResult* Result with success status and file information + +**file_download(source_path: str | Path, destination_path: str | Path) -> FileOperationResult** + +Download (copy) a file locally. + +For local systems, file download is implemented as a file copy operation +using shutil.copy2 to preserve metadata. + +**Parameters:** + +- `source_path` *str | Path* – Path to the source file +- `destination_path` *str | Path* – Path where the file should be copied + +**Returns:** + +- *FileOperationResult* Result with success status and file information + +**git_changes(path: str | Path) -> list[GitChange]** 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 +**Parameters:** + +- `path` *str | Path* – Path to the git repository + +**Returns:** + +- *list[GitChange]* list[GitChange]: List of changes -#### git_diff() +**Raises:** + +- `Exception` – If path is not a git repository or getting changes failed + +**git_diff(path: str | Path) -> GitDiff** 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 +**Parameters:** + +- `path` *str | Path* – Path to the file + +**Returns:** -#### model_config = (configuration object) +- *GitDiff* Git diff -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +**Raises:** -#### pause() +- `Exception` – If path is not a git repository or getting diff failed + +**pause() -> None** Pause the workspace (no-op for local workspaces). Local workspaces have nothing to pause since they operate directly on the host filesystem. -#### resume() +**resume() -> None** Resume the workspace (no-op for local workspaces). Local workspaces have nothing to resume since they operate directly on the host filesystem. -### class RemoteWorkspace +## class CommandResult + +Bases: `BaseModel` + +Result of executing a command in the workspace. + +### Properties + +- `command`: str + The command that was executed +- `exit_code`: int + Exit code of the command +- `stdout`: str + Standard output from the command +- `stderr`: str + Standard error from the command +- `timeout_occurred`: bool + Whether the command timed out during execution -Bases: `RemoteWorkspaceMixin`, [`BaseWorkspace`](#class-baseworkspace) +## class FileOperationResult + +Bases: `BaseModel` + +Result of a file upload or download operation. + +### Properties + +- `success`: bool + Whether the operation was successful +- `source_path`: str + Path to the source file +- `destination_path`: str + Path to the destination file +- `file_size`: int | None + Size of the file in bytes (if successful) +- `error`: str | None + Error message (if operation failed) + +## class RemoteWorkspace + +Bases: `RemoteWorkspaceMixin`, `BaseWorkspace` Remote workspace implementation that connects to an OpenHands agent server. @@ -296,123 +284,103 @@ RemoteWorkspace provides access to a sandboxed environment running on a remote OpenHands agent server. This is the recommended approach for production deployments as it provides better isolation and security. -#### Example - -```pycon ->>> workspace = RemoteWorkspace( -... host="https://agent-server.example.com", -... working_dir="/workspace" -... ) ->>> with workspace: -... result = workspace.execute_command("ls -la") -... content = workspace.read_file("README.md") -``` - - -#### Properties +### Properties +- `client`: httpx.Client - `alive`: bool Check if the remote workspace is alive by querying the health endpoint. - * Returns: + +Returns: True if the health endpoint returns a successful response, False otherwise. -- `client`: Client -#### Methods +### Methods + +**reset_client() -> None** + +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. -#### execute_command() +**execute_command(command: str, cwd: str | Path | None = None, timeout: float = 30.0) -> CommandResult** 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) +**Parameters:** -#### file_download() +- `command` *str* – The bash command to execute +- `cwd` *str | Path | None* – Working directory (optional) +- `timeout` *float* – Timeout in seconds -Download a file from the remote system. - -Requests the file from the remote system via HTTP API and saves it locally. +**Returns:** -* 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) +- *CommandResult* Result with stdout, stderr, exit_code, and other metadata -#### file_upload() +**file_upload(source_path: str | Path, destination_path: str | Path) -> FileOperationResult** 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) +**Parameters:** + +- `source_path` *str | Path* – Path to the local source file +- `destination_path` *str | Path* – Path where the file should be uploaded on remote system + +**Returns:** + +- *FileOperationResult* Result with success status and metadata + +**file_download(source_path: str | Path, destination_path: str | Path) -> FileOperationResult** -#### git_changes() +Download a file from the remote system. + +Requests the file from the remote system via HTTP API and saves it locally. + +**Parameters:** + +- `source_path` *str | Path* – Path to the source file on remote system +- `destination_path` *str | Path* – Path where the file should be saved locally + +**Returns:** + +- *FileOperationResult* Result with success status and metadata + +**git_changes(path: str | Path) -> list[GitChange]** 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 +**Parameters:** -#### git_diff() +- `path` *str | Path* – Path to the git repository -Get the git diff for the file at the path given. +**Returns:** -* 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 +- *list[GitChange]* list[GitChange]: List of changes -#### model_config = (configuration object) +**Raises:** -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +- `Exception` – If path is not a git repository or getting changes failed -#### model_post_init() +**git_diff(path: str | Path) -> GitDiff** -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. +Get the git diff for the file at the path given. -#### reset_client() +**Parameters:** -Reset the HTTP client to force re-initialization. +- `path` *str | Path* – Path to the file -This is useful when connection parameters (host, api_key) have changed -and the client needs to be recreated with new values. +**Returns:** -### class Workspace +- *GitDiff* Git diff -### class Workspace +**Raises:** -Bases: `object` +- `Exception` – If path is not a git repository or getting diff failed -Factory entrypoint that returns a LocalWorkspace or RemoteWorkspace. +## class Workspace -Usage: -: - Workspace(working_dir=…) -> LocalWorkspace - - Workspace(working_dir=…, host=”http://…”) -> RemoteWorkspace +Factory entrypoint that returns a LocalWorkspace or RemoteWorkspace. diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..a005a694 --- /dev/null +++ b/uv.lock @@ -0,0 +1,48 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "docs" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "griffe" }, + { name = "pip" }, +] + +[package.metadata] +requires-dist = [ + { name = "griffe", specifier = ">=1.15.0" }, + { name = "pip", specifier = ">=26.0.1" }, +] + +[[package]] +name = "griffe" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, +] + +[[package]] +name = "pip" +version = "26.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" }, +] From 530004b2bb40f5f47d2882707ac48f59af9316b6 Mon Sep 17 00:00:00 2001 From: VascoSch92 Date: Fri, 6 Feb 2026 18:47:59 +0100 Subject: [PATCH 2/3] nits --- scripts/generate-api-docs.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/generate-api-docs.py b/scripts/generate-api-docs.py index bdd09188..e2ffd3b9 100755 --- a/scripts/generate-api-docs.py +++ b/scripts/generate-api-docs.py @@ -10,18 +10,18 @@ import subprocess import sys from pathlib import Path - +from typing import Final import griffe logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") logger = logging.getLogger(__name__) -DOCS_DIR = Path(__file__).parent.parent -AGENT_SDK_DIR = DOCS_DIR / "agent-sdk" -SDK_SRC = AGENT_SDK_DIR / "openhands-sdk" -OUTPUT_DIR = DOCS_DIR / "sdk" / "api-reference" +DOCS_DIR: Final[Path] = Path(__file__).parent.parent +AGENT_SDK_DIR: Final[Path] = DOCS_DIR / "agent-sdk" +SDK_SRC: Final[Path] = AGENT_SDK_DIR / "openhands-sdk" +OUTPUT_DIR: Final[Path] = DOCS_DIR / "sdk" / "api-reference" -MODULES = [ +MODULES: Final[list[str]] = [ "openhands.sdk.agent", "openhands.sdk.conversation", "openhands.sdk.event", @@ -332,7 +332,7 @@ def generate_module_mdx(module_path: str) -> str: # Main # --------------------------------------------------------------------------- -def main(): +def main() -> None: setup_agent_sdk() install_sdk() From 0ef374e4c5edfed79bf48bcf7ebbb92355d19ec0 Mon Sep 17 00:00:00 2001 From: VascoSch92 Date: Mon, 9 Feb 2026 09:39:54 +0100 Subject: [PATCH 3/3] working in progress --- scripts/generate-api-docs.py | 30 +- sdk/api-reference/openhands.sdk.agent.mdx | 93 ++- .../openhands.sdk.conversation.mdx | 790 ++++++++++++++++-- sdk/api-reference/openhands.sdk.event.mdx | 68 +- sdk/api-reference/openhands.sdk.llm.mdx | 495 ++++++++++- sdk/api-reference/openhands.sdk.security.mdx | 19 +- sdk/api-reference/openhands.sdk.tool.mdx | 184 +++- sdk/api-reference/openhands.sdk.utils.mdx | 108 ++- sdk/api-reference/openhands.sdk.workspace.mdx | 221 ++++- 9 files changed, 1857 insertions(+), 151 deletions(-) diff --git a/scripts/generate-api-docs.py b/scripts/generate-api-docs.py index e2ffd3b9..1cc24b84 100755 --- a/scripts/generate-api-docs.py +++ b/scripts/generate-api-docs.py @@ -13,7 +13,7 @@ from typing import Final import griffe -logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") +logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s") logger = logging.getLogger(__name__) DOCS_DIR: Final[Path] = Path(__file__).parent.parent @@ -21,6 +21,8 @@ SDK_SRC: Final[Path] = AGENT_SDK_DIR / "openhands-sdk" OUTPUT_DIR: Final[Path] = DOCS_DIR / "sdk" / "api-reference" +SOURCE_CODE_BASE: Final[str] = "https://github.com/OpenHands/software-agent-sdk" + MODULES: Final[list[str]] = [ "openhands.sdk.agent", "openhands.sdk.conversation", @@ -216,6 +218,11 @@ def _render_function(func: griffe.Function, level: int = 4) -> list[str]: hdr = "#" * level lines: list[str] = [] + """ + + An example of a parameter field + + """ # Build parameter signature params: list[str] = [] for p in func.parameters: @@ -226,24 +233,31 @@ def _render_function(func: griffe.Function, level: int = 4) -> list[str]: s += f": {_fmt(p.annotation)}" if p.default is not None: s += f" = {p.default}" - params.append(s) + params.append( + f""" + + {p.docstring} + + """ + ) - sig = ", ".join(params) + # sig = ", ".join(params) + sig = "" ret = f" -> {_fmt(func.returns)}" if func.returns else "" abstract = "abstractmethod " if _has_decorator(func, "abstractmethod") else "" lines.append(f"**{abstract}{func.name}({sig}){ret}**") lines.append("") + lines.append(f"[source]({func.source_link})") + lines.append("") lines.extend(_render_docstring(func.docstring)) + lines.extend(params) return lines - def _render_class(cls: griffe.Class) -> list[str]: """Render a class as markdown.""" - lines: list[str] = [] + lines: list[str] = [f"## class {cls.name}", ""] - lines.append(f"## class {cls.name}") - lines.append("") if cls.bases: bases = ", ".join(f"`{b}`" for b in cls.bases) lines.append(f"Bases: {bases}") @@ -307,7 +321,7 @@ def generate_module_mdx(module_path: str) -> str: lines = [ "---", f"title: {module_path}", - f"description: API reference for {module_path}", + f"description: API reference for {module_path} module", "---", "", ] diff --git a/sdk/api-reference/openhands.sdk.agent.mdx b/sdk/api-reference/openhands.sdk.agent.mdx index cc71ad84..68d42ba5 100644 --- a/sdk/api-reference/openhands.sdk.agent.mdx +++ b/sdk/api-reference/openhands.sdk.agent.mdx @@ -1,6 +1,6 @@ --- title: openhands.sdk.agent -description: API reference for openhands.sdk.agent +description: API reference for openhands.sdk.agent module --- ## class Agent @@ -15,7 +15,9 @@ AgentBase and implements the agent execution logic. ### Methods -**init_state(state: ConversationState, on_event: ConversationCallbackType) -> None** +**init_state() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/agent/agent.py#L104-L196) Initialize conversation state. @@ -29,8 +31,35 @@ These invariants keep event ordering predictable for downstream components (condenser, UI, etc.) and also prevent accidentally materializing the full event history during initialization. -**step(conversation: LocalConversation, on_event: ConversationCallbackType, on_token: ConversationTokenCallbackType | None = None) -> None** + + None + + + + + None + + +**step() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/agent/agent.py#L249-L414) + + + + None + + + + + None + + + + + None + + ## class AgentBase Bases: `DiscriminatedUnionMixin`, `ABC` @@ -79,7 +108,9 @@ Raises: ### Methods -**init_state(state: ConversationState, on_event: ConversationCallbackType) -> None** +**init_state() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/agent/base.py#L239-L251) Initialize the empty conversation state to prepare the agent for user messages. @@ -88,7 +119,19 @@ Typically this involves adding system message NOTE: state will be mutated in-place. -**abstractmethod step(conversation: LocalConversation, on_event: ConversationCallbackType, on_token: ConversationTokenCallbackType | None = None) -> None** + + + None + + + + + None + + +**abstractmethod step() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/agent/base.py#L322-L343) Taking a step in the conversation. @@ -105,7 +148,24 @@ If the underlying LLM supports streaming, partial deltas are forwarded to NOTE: state will be mutated in-place. -**verify(persisted: AgentBase, events: Sequence[Any] | None = None) -> AgentBase** + + + None + + + + + None + + + + + None + + +**verify() -> AgentBase** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/agent/base.py#L345-L417) Verify that we can resume this agent from persisted state. @@ -137,12 +197,31 @@ freely changed between sessions. - `ValueError` – If agent class or tools don't match. -**model_dump_succint(kwargs = \{\})** + + + None + + + + + None + + +**model_dump_succint()** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/agent/base.py#L419-L427) Like model_dump, but excludes None fields by default. + + + None + + **get_all_llms() -> Generator[LLM, None, None]** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/agent/base.py#L429-L497) + Recursively yield unique *base-class* LLM objects reachable from `self`. - Returns actual object references (not copies). diff --git a/sdk/api-reference/openhands.sdk.conversation.mdx b/sdk/api-reference/openhands.sdk.conversation.mdx index 359bb910..65175b7f 100644 --- a/sdk/api-reference/openhands.sdk.conversation.mdx +++ b/sdk/api-reference/openhands.sdk.conversation.mdx @@ -1,6 +1,6 @@ --- title: openhands.sdk.conversation -description: API reference for openhands.sdk.conversation +description: API reference for openhands.sdk.conversation module --- ## class BaseConversation @@ -30,9 +30,13 @@ Returns True if BOTH conditions are met: **__init__() -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L107-L109) + Initialize the base conversation with span tracking. -**abstractmethod send_message(message: str | Message, sender: str | None = None) -> None** +**abstractmethod send_message() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L138-L150) Send a message to the agent. @@ -45,30 +49,76 @@ Send a message to the agent. one agent delegates to another, the sender can be set to identify which agent is sending the message. + + + None + + + + + None + + **abstractmethod run() -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L152-L159) + Execute the agent to process messages and perform actions. This method runs the agent until it finishes processing the current message or reaches the maximum iteration limit. -**abstractmethod set_confirmation_policy(policy: ConfirmationPolicyBase) -> None** +**abstractmethod set_confirmation_policy() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L161-L164) Set the confirmation policy for the conversation. -**abstractmethod set_security_analyzer(analyzer: SecurityAnalyzerBase | None) -> None** + + + None + + +**abstractmethod set_security_analyzer() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L166-L169) Set the security analyzer for the conversation. -**abstractmethod reject_pending_actions(reason: str = 'User rejected the action') -> None** + + None + + +**abstractmethod reject_pending_actions() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L188-L191) + + + + None + + **abstractmethod pause() -> None** -**abstractmethod update_secrets(secrets: Mapping[str, SecretValue]) -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L193-L194) + +**abstractmethod update_secrets() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L196-L197) + + + None + + **abstractmethod close() -> None** -**abstractmethod generate_title(llm: LLM | None = None, max_length: int = 50) -> str** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L199-L200) + +**abstractmethod generate_title() -> str** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L202-L217) Generate a title for the conversation based on the first user message. @@ -86,7 +136,19 @@ Generate a title for the conversation based on the first user message. - `ValueError` – If no user messages are found in the conversation. -**get_persistence_dir(persistence_base_dir: str | Path, conversation_id: ConversationID) -> str** + + + None + + + + + None + + +**get_persistence_dir() -> str** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L219-L234) Get the persistence directory for the conversation. @@ -101,7 +163,19 @@ path or Path object. - *str* String path to the conversation-specific persistence directory. - *str* Always returns a normalized string path even if a Path was provided. -**abstractmethod ask_agent(question: str) -> str** + + + None + + + + + None + + +**abstractmethod ask_agent() -> str** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L236-L252) Ask the agent a simple, stateless question and get a direct LLM response. @@ -119,8 +193,15 @@ executing in another thread. - *str* A string response from the agent + + + None + + **abstractmethod condense() -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L254-L269) + Force condensation of the conversation history. This method uses the existing condensation request pattern to trigger @@ -135,7 +216,9 @@ state by adding a condensation event to the history. - `ValueError` – If no condenser is configured or the condenser doesn't handle condensation requests. -**abstractmethod execute_tool(tool_name: str, action: Action) -> Observation** +**abstractmethod execute_tool() -> Observation** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L271-L299) Execute a tool directly without going through the agent loop. @@ -166,7 +249,19 @@ This is useful for: - `KeyError` – If the tool is not found in the agent's tools - `NotImplementedError` – If the tool has no executor -**compose_callbacks(callbacks: Iterable[CallbackType]) -> CallbackType** + + + None + + + + + None + + +**compose_callbacks() -> CallbackType** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L301-L317) Compose multiple callbacks into a single callback function. @@ -178,6 +273,11 @@ Compose multiple callbacks into a single callback function. - *CallbackType* A single callback function that calls all provided callbacks + + + None + + ## class Conversation Factory class for creating conversation instances with OpenHands agents. @@ -203,17 +303,45 @@ can be accessed by index or event ID. ### Methods -**__init__(fs: FileStore, dir_path: str = EVENTS_DIR) -> None** +**__init__() -> None** -**get_index(event_id: EventID) -> int** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/event_store.py#L41-L47) + + + + None + + + + + None + + +**get_index() -> int** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/event_store.py#L49-L54) Return the integer index for a given event_id. -**get_id(idx: int) -> EventID** + + + None + + +**get_id() -> EventID** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/event_store.py#L56-L62) Return the event_id for a given index. -**append(event: Event) -> None** + + + None + + +**append() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/event_store.py#L99-L132) Append an event with locking for thread/process safety. @@ -222,6 +350,11 @@ Append an event with locking for thread/process safety. - `TimeoutError` – If the lock cannot be acquired within LOCK_TIMEOUT_SECONDS. - `ValueError` – If an event with the same ID already exists. + + + None + + ## class EventsListBase Bases: `Sequence[Event]`, `ABC` @@ -233,10 +366,17 @@ RemoteEventsList implementations, avoiding circular imports in protocols. ### Methods -**abstractmethod append(event: Event) -> None** +**abstractmethod append() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/events_list_base.py#L14-L17) Add a new event to the list. + + + None + + ## class WebSocketConnectionError Bases: `RuntimeError` @@ -250,8 +390,25 @@ Raised when WebSocket connection fails to establish within the timeout. ### Methods -**__init__(conversation_id: ConversationID, timeout: float, message: str | None = None) -> None** +**__init__() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/exceptions.py#L10-L22) + + + None + + + + + None + + + + + None + + ## class LocalConversation Bases: `BaseConversation` @@ -284,7 +441,9 @@ resume uses the exact same plugin versions. ### Methods -**__init__(agent: AgentBase, workspace: str | Path | LocalWorkspace, plugins: list[PluginSource] | None = None, persistence_dir: str | Path | None = None, conversation_id: ConversationID | None = None, callbacks: list[ConversationCallbackType] | None = None, token_callbacks: list[ConversationTokenCallbackType] | None = None, hook_config: HookConfig | None = None, max_iteration_per_run: int = 500, stuck_detection: bool = True, stuck_detection_thresholds: StuckDetectionThresholds | Mapping[str, int] | None = None, visualizer: type[ConversationVisualizerBase] | ConversationVisualizerBase | None = DefaultConversationVisualizer, secrets: Mapping[str, SecretValue] | None = None, delete_on_close: bool = True, cipher: Cipher | None = None, _: object = \{\})** +**__init__()** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L76-L248) Initialize the conversation. @@ -325,7 +484,89 @@ If plugins are loaded, their hooks are combined with this config. decrypted when loading. If not provided, secrets are redacted (lost) on serialization. -**send_message(message: str | Message, sender: str | None = None) -> None** + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + +**send_message() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L423-L481) Send a message to the agent. @@ -338,8 +579,20 @@ Send a message to the agent. one agent delegates to another, the sender can be set to identify which agent is sending the message. + + + None + + + + + None + + **run() -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L483-L618) + Runs the conversation until the agent finishes. In confirmation mode: @@ -351,19 +604,35 @@ In normal mode: Can be paused between steps -**set_confirmation_policy(policy: ConfirmationPolicyBase) -> None** +**set_confirmation_policy() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L620-L624) Set the confirmation policy and store it in conversation state. -**reject_pending_actions(reason: str = 'User rejected the action') -> None** + + + None + + +**reject_pending_actions() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L626-L655) 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. + + + None + + **pause() -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L657-L680) + Pause agent execution. This method can be called from any thread to request that the agent @@ -373,7 +642,9 @@ of the run loop (between agent steps). Note: If called during an LLM completion, the pause will not take effect until the current LLM call completes. -**update_secrets(secrets: Mapping[str, SecretValue]) -> None** +**update_secrets() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L682-L693) Add secrets to the conversation. @@ -383,15 +654,31 @@ Add secrets to the conversation. SecretValue = str | Callable[[], str]. Callables are invoked lazily when a command references the secret key. -**set_security_analyzer(analyzer: SecurityAnalyzerBase | None) -> None** + + + None + + +**set_security_analyzer() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L695-L698) Set the security analyzer for the conversation. + + + None + + **close() -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L700-L731) + Close the conversation and clean up all tool executors. -**ask_agent(question: str) -> str** +**ask_agent() -> str** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L733-L798) Ask the agent a simple, stateless question and get a direct LLM response. @@ -409,7 +696,14 @@ executing in another thread. - *str* A string response from the agent -**generate_title(llm: LLM | None = None, max_length: int = 50) -> str** + + + None + + +**generate_title() -> str** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L800-L820) Generate a title for the conversation based on the first user message. @@ -427,8 +721,20 @@ Generate a title for the conversation based on the first user message. - `ValueError` – If no user messages are found in the conversation. + + + None + + + + + None + + **condense() -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L822-L869) + Synchronously force condense the conversation history. If the agent is currently running, `condense()` will wait for the @@ -436,7 +742,9 @@ ongoing step to finish before proceeding. Raises ValueError if no compatible condenser exists. -**execute_tool(tool_name: str, action: Action) -> Observation** +**execute_tool() -> Observation** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L871-L912) Execute a tool directly without going through the agent loop. @@ -467,6 +775,16 @@ This is useful for: - `KeyError` – If the tool is not found in the agent's tools - `NotImplementedError` – If the tool has no executor + + + None + + + + + None + + ## class RemoteConversation Bases: `BaseConversation` @@ -487,7 +805,9 @@ Not implemented for remote conversations. ### Methods -**__init__(agent: AgentBase, workspace: RemoteWorkspace, plugins: list | None = None, conversation_id: ConversationID | None = None, callbacks: list[ConversationCallbackType] | None = None, max_iteration_per_run: int = 500, stuck_detection: bool = True, stuck_detection_thresholds: StuckDetectionThresholds | Mapping[str, int] | None = None, hook_config: HookConfig | None = None, visualizer: type[ConversationVisualizerBase] | ConversationVisualizerBase | None = DefaultConversationVisualizer, secrets: Mapping[str, SecretValue] | None = None, delete_on_close: bool = False, _: object = \{\}) -> None** +**__init__() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py#L566-L790) Remote conversation proxy that talks to an agent server. @@ -514,9 +834,88 @@ Remote conversation proxy that talks to an agent server. - None: No visualization - `secrets` *Mapping[str, SecretValue] | None* – Optional secrets to initialize the conversation with -**send_message(message: str | Message, sender: str | None = None) -> None** -**run(blocking: bool = True, poll_interval: float = 1.0, timeout: float = 3600.0) -> None** + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + +**send_message() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py#L847-L863) + + + + None + + + + + None + + +**run() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py#L865-L913) Trigger a run on the server. @@ -533,19 +932,66 @@ blocking=True). Default is 1.0 second. - `ConversationRunError` – If the run fails or times out. -**set_confirmation_policy(policy: ConfirmationPolicyBase) -> None** -**set_security_analyzer(analyzer: SecurityAnalyzerBase | None) -> None** + + None + + + + + None + + + + + None + + +**set_confirmation_policy() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py#L1083-L1090) + + + + None + + +**set_security_analyzer() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py#L1092-L1100) Set the security analyzer for the remote conversation. -**reject_pending_actions(reason: str = 'User rejected the action') -> None** + + None + + +**reject_pending_actions() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py#L1102-L1109) + + + + None + + **pause() -> None** -**update_secrets(secrets: Mapping[str, SecretValue]) -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py#L1111-L1112) + +**update_secrets() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py#L1114-L1129) + + + + None + + +**ask_agent() -> str** -**ask_agent(question: str) -> str** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py#L1131-L1156) Ask the agent a simple, stateless question and get a direct LLM response. @@ -563,7 +1009,14 @@ executing in another thread. - *str* A string response from the agent -**generate_title(llm: LLM | None = None, max_length: int = 50) -> str** + + + None + + +**generate_title() -> str** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py#L1158-L1185) Generate a title for the conversation based on the first user message. @@ -577,8 +1030,20 @@ Generate a title for the conversation based on the first user message. - *str* A generated title for the conversation. + + + None + + + + + None + + **condense() -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py#L1187-L1200) + Force condensation of the conversation history. This method sends a condensation request to the remote agent server. @@ -592,7 +1057,9 @@ conversation state by adding a condensation event to the history. - `HTTPError` – If the server returns an error (e.g., no condenser configured). -**execute_tool(tool_name: str, action: Action) -> Observation** +**execute_tool() -> Observation** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py#L1202-L1222) Execute a tool directly without going through the agent loop. @@ -610,15 +1077,29 @@ during the normal agent loop. - `NotImplementedError` – Always, as this feature is not yet supported for remote conversations. + + + None + + + + + None + + **close() -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py#L1224-L1251) + Close the conversation and clean up resources. Note: We don't close self._client here because it's shared with the workspace. The workspace owns the client and will close it during its own cleanup. Closing it here would prevent the workspace from making cleanup API calls. -**get_agent_final_response(events: Sequence[Event]) -> str** +**get_agent_final_response() -> str** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/response_utils.py#L11-L41) Extract the final response from the agent. @@ -634,6 +1115,11 @@ An agent can end a conversation in two ways: - *str* The final response message from the agent, or empty string if not found. + + + None + + ## class SecretRegistry Bases: `OpenHandsModel` @@ -660,7 +1146,9 @@ even when callable secrets fail on subsequent calls. ### Methods -**update_secrets(secrets: Mapping[str, SecretValue]) -> None** +**update_secrets() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/secret_registry.py#L36-L47) Add or update secrets in the manager. @@ -669,7 +1157,14 @@ Add or update secrets in the manager. - `secrets` *Mapping[str, SecretValue]* – Dictionary mapping secret keys to either string values or callable functions that return string values -**find_secrets_in_text(text: str) -> set[str]** + + + None + + +**find_secrets_in_text() -> set[str]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/secret_registry.py#L49-L62) Find all secret keys mentioned in the given text. @@ -681,7 +1176,14 @@ Find all secret keys mentioned in the given text. - *set[str]* Set of secret keys found in the text -**get_secrets_as_env_vars(command: str) -> dict[str, str]** + + + None + + +**get_secrets_as_env_vars() -> dict[str, str]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/secret_registry.py#L64-L94) Get secrets that should be exported as environment variables for a command. @@ -693,7 +1195,14 @@ Get secrets that should be exported as environment variables for a command. - *dict[str, str]* Dictionary of environment variables to export (key -> value) -**mask_secrets_in_output(text: str) -> str** + + + None + + +**mask_secrets_in_output() -> str** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/secret_registry.py#L96-L117) Mask secret values in the given text. @@ -708,6 +1217,11 @@ fresh values from callables to ensure comprehensive masking. - *str* Text with secret values replaced by `` + + + None + + ## class ConversationExecutionStatus Bases: `str`, `Enum` @@ -729,6 +1243,8 @@ Enum representing the current execution state of the conversation. **is_terminal() -> bool** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/state.py#L48-L65) + Check if this status represents a terminal state. Terminal states indicate the run has completed and the agent is no longer @@ -780,7 +1296,9 @@ Bases: `OpenHandsModel` ### Methods -**set_on_state_change(callback: ConversationCallbackType | None) -> None** +**set_on_state_change() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/state.py#L177-L184) Set a callback to be called when state changes. @@ -789,7 +1307,14 @@ Set a callback to be called when state changes. - `callback` *ConversationCallbackType | None* – A function that takes an Event (ConversationStateUpdateEvent) or None to remove the callback -**create(id: ConversationID, agent: AgentBase, workspace: BaseWorkspace, persistence_dir: str | None = None, max_iterations: int = 500, stuck_detection: bool = True, cipher: Cipher | None = None) -> ConversationState** + + + None + + +**create() -> ConversationState** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/state.py#L207-L326) Create a new conversation state or resume from persistence. @@ -828,23 +1353,98 @@ agent_context, condenser, system prompts, etc. - `ValueError` – If conversation ID or tools mismatch on restore - `ValidationError` – If agent or other fields fail Pydantic validation -**block_action(action_id: str, reason: str) -> None** + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + +**block_action() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/state.py#L371-L373) Persistently record a hook-blocked action. -**pop_blocked_action(action_id: str) -> str | None** + + + None + + + + + None + + +**pop_blocked_action() -> str | None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/state.py#L375-L382) Remove and return a hook-blocked action reason, if present. -**block_message(message_id: str, reason: str) -> None** + + + None + + +**block_message() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/state.py#L384-L386) Persistently record a hook-blocked user message. -**pop_blocked_message(message_id: str) -> str | None** + + + None + + + + + None + + +**pop_blocked_message() -> str | None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/state.py#L388-L395) Remove and return a hook-blocked message reason, if present. -**get_unmatched_actions(events: Sequence[Event]) -> list[ActionEvent]** + + + None + + +**get_unmatched_actions() -> list[ActionEvent]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/state.py#L397-L424) Find actions in the event history that don't have matching observations. @@ -861,7 +1461,14 @@ actions that are pending confirmation or execution. - *list[ActionEvent]* List of ActionEvent objects that don't have corresponding observations, - *list[ActionEvent]* in chronological order -**acquire(blocking: bool = True, timeout: float = -1) -> bool** + + + None + + +**acquire() -> bool** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/state.py#L427-L440) Acquire the lock. @@ -876,8 +1483,20 @@ Acquire the lock. - *bool* True if lock was acquired, False otherwise. + + + None + + + + + None + + **release() -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/state.py#L442-L449) + Release the lock. **Raises:** @@ -886,10 +1505,14 @@ Release the lock. **locked() -> bool** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/state.py#L460-L464) + Return True if the lock is currently held by any thread. **owned() -> bool** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/state.py#L466-L470) + Return True if the lock is currently held by the calling thread. ## class StuckDetector @@ -914,10 +1537,24 @@ This detector analyzes the conversation history to identify various stuck patter ### Methods -**__init__(state: ConversationState, thresholds: StuckDetectionThresholds | None = None)** +**__init__()** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/stuck_detector.py#L38-L44) + + + None + + + + + None + + **is_stuck() -> bool** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/stuck_detector.py#L62-L138) + Check if the agent is currently stuck. Note: To avoid materializing potentially large file-backed event histories, @@ -955,9 +1592,13 @@ Conversation will then calls `MyVisualizer()` followed by `initialize(state)` **__init__()** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/visualizer/base.py#L33-L35) + Initialize the visualizer base. -**initialize(state: ConversationStateProtocol) -> None** +**initialize() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/visualizer/base.py#L37-L50) Initialize the visualizer with conversation state. @@ -971,7 +1612,14 @@ Subclasses should not override this method, to ensure the state is set. - `state` *ConversationStateProtocol* – The conversation state object -**abstractmethod on_event(event: Event) -> None** + + + None + + +**abstractmethod on_event() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/visualizer/base.py#L57-L67) Handle a conversation event. @@ -982,7 +1630,14 @@ implement the visualization logic. - `event` *Event* – The event to visualize -**create_sub_visualizer(agent_id: str) -> ConversationVisualizerBase | None** + + + None + + +**create_sub_visualizer() -> ConversationVisualizerBase | None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/visualizer/base.py#L69-L90) Create a visualizer for a sub-agent during delegation. @@ -1003,6 +1658,11 @@ override this method to create appropriate sub-visualizers. - *ConversationVisualizerBase | None* A visualizer instance for the sub-agent, or None if sub-agent - *ConversationVisualizerBase | None* visualization is not supported + + + None + + ## class DefaultConversationVisualizer Bases: `ConversationVisualizerBase` @@ -1013,7 +1673,9 @@ Provides Rich-formatted output with semantic dividers and complete content displ ### Methods -**__init__(highlight_regex: dict[str, str] | None = DEFAULT_HIGHLIGHT_REGEX, skip_user_messages: bool = False)** +**__init__()** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/visualizer/default.py#L224-L242) Initialize the visualizer. @@ -1026,6 +1688,24 @@ Initialize the visualizer. - `skip_user_messages` *bool* – If True, skip displaying user messages. Useful for scenarios where user input is not relevant to show. -**on_event(event: Event) -> None** + + + None + + + + + None + + +**on_event() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/visualizer/default.py#L244-L248) Main event handler that displays events with Rich formatting. + + + + None + + \ 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 8825f172..2ad67990 100644 --- a/sdk/api-reference/openhands.sdk.event.mdx +++ b/sdk/api-reference/openhands.sdk.event.mdx @@ -1,6 +1,6 @@ --- title: openhands.sdk.event -description: API reference for openhands.sdk.event +description: API reference for openhands.sdk.event module --- ## class Event @@ -34,10 +34,19 @@ Base class for events that can be converted to LLM messages. **abstractmethod to_llm_message() -> Message** -**events_to_messages(events: list[LLMConvertibleEvent]) -> list[Message]** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/base.py#L61-L63) + +**events_to_messages() -> list[Message]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/base.py#L90-L126) Convert event stream to LLM message stream, handling multi-action batches + + + None + + ## class Condensation Bases: `Event` @@ -70,7 +79,9 @@ Raises: ### Methods -**apply(events: list[LLMConvertibleEvent]) -> list[LLMConvertibleEvent]** +**apply() -> list[LLMConvertibleEvent]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/condenser.py#L83-L96) Applies the condensation to a list of events. @@ -79,6 +90,11 @@ 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. + + + None + + ## class CondensationRequest Bases: `Event` @@ -106,6 +122,8 @@ This event represents a summary generated by a condenser. **to_llm_message() -> Message** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/condenser.py#L128-L132) + ## class ConversationStateUpdateEvent Bases: `Event` @@ -128,11 +146,32 @@ to ensure compatibility with websocket transmission. ### Methods -**validate_key(key)** +**validate_key()** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/conversation_state.py#L38-L48) + + + + None + + +**validate_value()** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/conversation_state.py#L50-L79) -**validate_value(value, info)** -**from_conversation_state(state: ConversationState) -> ConversationStateUpdateEvent** + + None + + + + + None + + +**from_conversation_state() -> ConversationStateUpdateEvent** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/conversation_state.py#L81-L101) Create a state update event from a ConversationState object. @@ -147,6 +186,11 @@ This creates an event containing a snapshot of important state fields. - *ConversationStateUpdateEvent* A ConversationStateUpdateEvent with serialized state data + + + None + + ## class LLMCompletionLogEvent Bases: `Event` @@ -207,6 +251,8 @@ Bases: `LLMConvertibleEvent` **to_llm_message() -> Message** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/llm_convertible/action.py#L140-L149) + Individual message - may be incomplete for multi-action batches ## class AgentErrorEvent @@ -230,6 +276,8 @@ represents an error produced by the agent/scaffold, not model output. **to_llm_message() -> Message** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/llm_convertible/observation.py#L123-L131) + ## class MessageEvent Bases: `LLMConvertibleEvent` @@ -264,6 +312,8 @@ This is originally the "MessageAction", but it suppose not to be tool call. **to_llm_message() -> Message** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/llm_convertible/message.py#L116-L119) + ## class ObservationBaseEvent Bases: `LLMConvertibleEvent` @@ -297,6 +347,8 @@ Bases: `ObservationBaseEvent` **to_llm_message() -> Message** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/llm_convertible/observation.py#L45-L51) + ## class SystemPromptEvent Bases: `LLMConvertibleEvent` @@ -317,6 +369,8 @@ System prompt added by the agent. **to_llm_message() -> Message** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/llm_convertible/system.py#L47-L48) + ## class UserRejectObservation Bases: `ObservationBaseEvent` @@ -336,6 +390,8 @@ Observation when user rejects an action in confirmation mode. **to_llm_message() -> Message** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/llm_convertible/observation.py#L86-L92) + ## class TokenEvent Bases: `Event` diff --git a/sdk/api-reference/openhands.sdk.llm.mdx b/sdk/api-reference/openhands.sdk.llm.mdx index 8e7c6ba7..e0783c13 100644 --- a/sdk/api-reference/openhands.sdk.llm.mdx +++ b/sdk/api-reference/openhands.sdk.llm.mdx @@ -1,6 +1,6 @@ --- title: openhands.sdk.llm -description: API reference for openhands.sdk.llm +description: API reference for openhands.sdk.llm module --- ## class CredentialStore @@ -14,7 +14,9 @@ Store and retrieve OAuth credentials for LLM providers. ### Methods -**__init__(credentials_dir: Path | None = None)** +**__init__()** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/auth/credentials.py#L49-L57) Initialize the credential store. @@ -23,7 +25,14 @@ Initialize the credential store. - `credentials_dir` *Path | None* – Optional custom directory for storing credentials. Defaults to ~/.local/share/openhands/auth/ -**get(vendor: str) -> OAuthCredentials | None** + + + None + + +**get() -> OAuthCredentials | None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/auth/credentials.py#L72-L92) Get stored credentials for a vendor. @@ -35,7 +44,14 @@ Get stored credentials for a vendor. - *OAuthCredentials | None* OAuthCredentials if found and valid, None otherwise -**save(credentials: OAuthCredentials) -> None** + + + None + + +**save() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/auth/credentials.py#L94-L111) Save credentials for a vendor. @@ -43,7 +59,14 @@ Save credentials for a vendor. - `credentials` *OAuthCredentials* – The OAuth credentials to save -**delete(vendor: str) -> bool** + + + None + + +**delete() -> bool** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/auth/credentials.py#L113-L126) Delete stored credentials for a vendor. @@ -55,7 +78,14 @@ Delete stored credentials for a vendor. - *bool* True if credentials were deleted, False if they didn't exist -**update_tokens(vendor: str, access_token: str, refresh_token: str | None, expires_in: int) -> OAuthCredentials | None** + + + None + + +**update_tokens() -> OAuthCredentials | None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/auth/credentials.py#L128-L157) Update tokens for an existing credential. @@ -70,6 +100,26 @@ Update tokens for an existing credential. - *OAuthCredentials | None* Updated credentials, or None if no existing credentials found + + + None + + + + + None + + + + + None + + + + + None + + ## class OAuthCredentials Bases: `BaseModel` @@ -92,6 +142,8 @@ OAuth credentials for subscription-based LLM access. **is_expired() -> bool** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/auth/credentials.py#L39-L43) + Check if the access token is expired. ## class OpenAISubscriptionAuth @@ -105,7 +157,9 @@ Handle OAuth authentication for OpenAI ChatGPT subscription access. ### Methods -**__init__(credential_store: CredentialStore | None = None, oauth_port: int = DEFAULT_OAUTH_PORT)** +**__init__()** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/auth/openai.py#L345-L357) Initialize the OpenAI subscription auth handler. @@ -114,16 +168,32 @@ Initialize the OpenAI subscription auth handler. - `credential_store` *CredentialStore | None* – Optional custom credential store. - `oauth_port` *int* – Port for the local OAuth callback server. + + + None + + + + + None + + **get_credentials() -> OAuthCredentials | None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/auth/openai.py#L364-L366) + Get stored credentials if they exist. **has_valid_credentials() -> bool** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/auth/openai.py#L368-L371) + Check if valid (non-expired) credentials exist. **refresh_if_needed() -> OAuthCredentials | None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/auth/openai.py#L373-L397) + Refresh credentials if they are expired. **Returns:** @@ -134,7 +204,9 @@ Refresh credentials if they are expired. - `RuntimeError` – If token refresh fails. -**login(open_browser: bool = True) -> OAuthCredentials** +**login() -> OAuthCredentials** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/auth/openai.py#L399-L527) Perform OAuth login flow. @@ -154,15 +226,24 @@ callback with the authorization code. - `RuntimeError` – If the OAuth flow fails or times out. + + + None + + **logout() -> bool** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/auth/openai.py#L529-L535) + Remove stored credentials. **Returns:** - *bool* True if credentials were removed, False if none existed. -**create_llm(model: str = 'gpt-5.2-codex', credentials: OAuthCredentials | None = None, instructions: str | None = None, llm_kwargs: Any = \{\}) -> LLM** +**create_llm() -> LLM** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/auth/openai.py#L537-L611) Create an LLM instance configured for Codex subscription access. @@ -181,6 +262,26 @@ Create an LLM instance configured for Codex subscription access. - `ValueError` – If the model is not supported or no credentials available. + + + None + + + + + None + + + + + None + + + + + None + + ## class LLM Bases: `BaseModel`, `RetryMixin`, `NonNativeToolCallingMixin` @@ -304,9 +405,18 @@ Returns: ### Methods -**restore_metrics(metrics: Metrics) -> None** +**restore_metrics() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm.py#L531-L533) + + + + None + + +**completion() -> LLMResponse** -**completion(messages: list[Message], tools: Sequence[ToolDefinition] | None = None, _return_metrics: bool = False, add_security_risk_prediction: bool = False, on_token: TokenCallbackType | None = None, kwargs = \{\}) -> LLMResponse** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm.py#L535-L691) Generate a completion from the language model. @@ -330,7 +440,39 @@ It handles message formatting, tool calling, and response processing. - `ValueError` – If streaming is requested (not supported). -**responses(messages: list[Message], tools: Sequence[ToolDefinition] | None = None, include: list[str] | None = None, store: bool | None = None, _return_metrics: bool = False, add_security_risk_prediction: bool = False, on_token: TokenCallbackType | None = None, kwargs = \{\}) -> LLMResponse** + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + +**responses() -> LLMResponse** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm.py#L696-L892) Alternative invocation path using OpenAI Responses API via LiteLLM. @@ -347,10 +489,54 @@ Maps Message[] -> (instructions, input[]) and returns LLMResponse. - `on_token` *TokenCallbackType | None* – Optional callback for streaming deltas - `**kwargs` – Additional arguments passed to the API + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + **vision_is_active() -> bool** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm.py#L1024-L1027) + **is_caching_prompt_active() -> bool** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm.py#L1052-L1066) + Check if prompt caching is supported and enabled for current model. **Returns:** @@ -360,13 +546,24 @@ model. **uses_responses_api() -> bool** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm.py#L1068-L1072) + Whether this model uses the OpenAI Responses API path. -**format_messages_for_llm(messages: list[Message]) -> list[dict]** +**format_messages_for_llm() -> list[dict]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm.py#L1098-L1127) Formats Message objects for LLM consumption. -**format_messages_for_responses(messages: list[Message]) -> tuple[str | None, list[dict[str, Any]]]** + + + None + + +**format_messages_for_responses() -> tuple[str | None, list[dict[str, Any]]]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm.py#L1129-L1168) Prepare (instructions, input[]) for the OpenAI Responses API. @@ -376,13 +573,41 @@ Prepare (instructions, input[]) for the OpenAI Responses API. - Concatenates system instructions into a single instructions string - For subscription mode, system prompts are prepended to user content -**get_token_count(messages: list[Message]) -> int** -**load_from_json(json_path: str) -> LLM** + + None + + +**get_token_count() -> int** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm.py#L1170-L1193) + + + + None + + +**load_from_json() -> LLM** -**load_from_env(prefix: str = 'LLM_') -> LLM** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm.py#L1198-L1202) -**subscription_login(vendor: SupportedVendor, model: str, force_login: bool = False, open_browser: bool = True, llm_kwargs = \{\}) -> LLM** + + + None + + +**load_from_env() -> LLM** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm.py#L1204-L1257) + + + + None + + +**subscription_login() -> LLM** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm.py#L1259-L1316) Authenticate with a subscription service and return an LLM instance. @@ -420,6 +645,31 @@ OAuth login flow. - `ValueError` – If the vendor or model is not supported. - `RuntimeError` – If authentication fails. + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + ## class LLMRegistry A minimal LLM registry for managing LLM instances by usage ID. @@ -437,7 +687,9 @@ avoiding the need to recreate LLMs with the same configuration. ### Methods -**__init__(retry_listener: Callable[[int, int], None] | None = None)** +**__init__()** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm_registry.py#L33-L45) Initialize the LLM registry. @@ -445,7 +697,14 @@ Initialize the LLM registry. - `retry_listener` *Callable[[int, int], None] | None* – Optional callback for retry events. -**subscribe(callback: Callable[[RegistryEvent], None]) -> None** + + + None + + +**subscribe() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm_registry.py#L47-L53) Subscribe to registry events. @@ -453,7 +712,14 @@ Subscribe to registry events. - `callback` *Callable[[RegistryEvent], None]* – Function to call when LLMs are created or updated. -**notify(event: RegistryEvent) -> None** + + + None + + +**notify() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm_registry.py#L55-L65) Notify subscribers of registry events. @@ -461,7 +727,14 @@ Notify subscribers of registry events. - `event` *RegistryEvent* – The registry event to notify about. -**add(llm: LLM) -> None** + + + None + + +**add() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm_registry.py#L73-L95) Add an LLM instance to the registry. @@ -473,7 +746,14 @@ Add an LLM instance to the registry. - `ValueError` – If llm.usage_id already exists in the registry. -**get(usage_id: str) -> LLM** + + + None + + +**get() -> LLM** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm_registry.py#L97-L118) Get an LLM instance from the registry. @@ -489,8 +769,15 @@ Get an LLM instance from the registry. - `KeyError` – If usage_id is not found in the registry. + + + None + + **list_usage_ids() -> list[str]** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm_registry.py#L120-L123) + List all registered usage IDs. ## class RegistryEvent @@ -541,6 +828,8 @@ Bases: `BaseContent` **to_llm_dict() -> list[dict[str, str | dict[str, str]]]** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L189-L196) + Convert to LLM API format. ## class Message @@ -565,7 +854,9 @@ Bases: `BaseModel` ### Methods -**to_chat_dict(cache_enabled: bool, vision_enabled: bool, function_calling_enabled: bool, force_string_serializer: bool, send_reasoning_content: bool) -> dict[str, Any]** +**to_chat_dict() -> dict[str, Any]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L281-L329) Serialize message for OpenAI Chat Completions. @@ -581,13 +872,47 @@ Chooses the appropriate content serializer and then injects threading keys: - Assistant tool call turn: role == "assistant" and self.tool_calls - Tool result turn: role == "tool" and self.tool_call_id (with name) -**to_responses_value(vision_enabled: bool) -> str | list[dict[str, Any]]** + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + +**to_responses_value() -> str | list[dict[str, Any]]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L439-L449) Return serialized form. Either an instructions string (for system) or input items (for other roles). -**to_responses_dict(vision_enabled: bool) -> list[dict[str, Any]]** + + + None + + +**to_responses_dict() -> list[dict[str, Any]]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L451-L578) Serialize message for OpenAI Responses (input parameter). @@ -600,7 +925,14 @@ and function_call items for tool_calls - tool: emits function_call_output items (one per TextContent) with matching call_id -**from_llm_chat_message(message: LiteLLMMessage) -> Message** + + + None + + +**from_llm_chat_message() -> Message** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L590-L647) Convert a LiteLLMMessage (Chat Completions) to our Message class. @@ -608,7 +940,14 @@ Provider-agnostic mapping for reasoning: - Prefer `message.reasoning_content` if present (LiteLLM normalized field) - Extract `thinking_blocks` from content array (Anthropic-specific) -**from_llm_responses_output(output: Any) -> Message** + + + None + + +**from_llm_responses_output() -> Message** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L649-L705) Convert OpenAI Responses API output items into a single assistant Message. @@ -616,6 +955,11 @@ Policy (non-stream): - Collect assistant text by concatenating output_text parts from message items - Normalize function_call items to MessageToolCall list + + + None + + ## class MessageToolCall Bases: `BaseModel` @@ -638,22 +982,40 @@ for Responses function_call_output call_id. ### Methods -**from_chat_tool_call(tool_call: ChatCompletionMessageToolCall) -> MessageToolCall** +**from_chat_tool_call() -> MessageToolCall** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L38-L58) Create a MessageToolCall from a Chat Completions tool call. -**from_responses_function_call(item: ResponseFunctionToolCall | OutputFunctionToolCall) -> MessageToolCall** + + + None + + +**from_responses_function_call() -> MessageToolCall** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L60-L82) Create a MessageToolCall from a typed OpenAI Responses function_call item. Note: OpenAI Responses function_call.arguments is already a JSON string. + + + None + + **to_chat_dict() -> dict[str, Any]** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L84-L93) + Serialize to OpenAI Chat Completions tool_calls format. **to_responses_dict() -> dict[str, Any]** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L95-L111) + Serialize to OpenAI Responses 'function_call' input item format. ## class ReasoningItemModel @@ -701,6 +1063,8 @@ Bases: `BaseContent` **to_llm_dict() -> list[dict[str, str | dict[str, str]]]** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L174-L182) + Convert to LLM API format. ## class ThinkingBlock @@ -721,12 +1085,19 @@ and passed back to the API for tool use scenarios. - `signature`: str | None Cryptographic signature for the thinking block -**content_to_str(contents: Sequence[TextContent | ImageContent]) -> list[str]** +**content_to_str() -> list[str]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L708-L719) Convert a list of TextContent and ImageContent to a list of strings. This is primarily used for display purposes. + + + None + + ## class RouterLLM Bases: `LLM` @@ -750,9 +1121,18 @@ Key features: ### Methods -**validate_llms_not_empty(v)** +**validate_llms_not_empty()** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/router/base.py#L41-L48) -**completion(messages: list[Message], tools: Sequence[ToolDefinition] | None = None, return_metrics: bool = False, add_security_risk_prediction: bool = False, on_token: TokenCallbackType | None = None, kwargs = \{\}) -> LLMResponse** + + + None + + +**completion() -> LLMResponse** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/router/base.py#L50-L89) This method intercepts completion calls and routes them to the appropriate underlying LLM based on the routing logic implemented in select_llm(). @@ -766,7 +1146,39 @@ underlying LLM based on the routing logic implemented in select_llm(). - `on_token` *TokenCallbackType | None* – Optional callback for streaming tokens - `**kwargs` – Additional arguments passed to the LLM API -**abstractmethod select_llm(messages: list[Message]) -> str** + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + +**abstractmethod select_llm() -> str** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/router/base.py#L91-L105) Select which LLM to use based on messages and events. @@ -783,6 +1195,19 @@ LLM from llms_for_routing is most appropriate for handling the request. - *str* The key/name of the LLM to use from llms_for_routing dictionary. -**set_placeholder_model(data)** + + + None + + +**set_placeholder_model()** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/router/base.py#L117-L129) Guarantee `model` exists before LLM base validation runs. + + + + None + + \ 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 7e98b3c5..37dbe7c3 100644 --- a/sdk/api-reference/openhands.sdk.security.mdx +++ b/sdk/api-reference/openhands.sdk.security.mdx @@ -1,6 +1,6 @@ --- title: openhands.sdk.security -description: API reference for openhands.sdk.security +description: API reference for openhands.sdk.security module --- ## class SecurityRisk @@ -27,9 +27,13 @@ Integer values allow for easy comparison and ordering. **get_color() -> str** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/security/risk.py#L40-L48) + Get the color for displaying this risk level in Rich text. -**is_riskier(other: SecurityRisk, reflexive: bool = True) -> bool** +**is_riskier() -> bool** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/security/risk.py#L64-L100) Check if this risk level is riskier than another. @@ -57,3 +61,14 @@ This can be disabled by setting the `reflexive` parameter to False. **Raises:** - `ValueError` – If either risk level is UNKNOWN. + + + + None + + + + + None + + \ No newline at end of file diff --git a/sdk/api-reference/openhands.sdk.tool.mdx b/sdk/api-reference/openhands.sdk.tool.mdx index fc38d753..33c44cce 100644 --- a/sdk/api-reference/openhands.sdk.tool.mdx +++ b/sdk/api-reference/openhands.sdk.tool.mdx @@ -1,6 +1,6 @@ --- title: openhands.sdk.tool -description: API reference for openhands.sdk.tool +description: API reference for openhands.sdk.tool module --- ## class FinishTool @@ -11,7 +11,9 @@ Tool for signaling the completion of a task or conversation. ### Methods -**create(conv_state: ConversationState | None = None, params = \{\}) -> Sequence[Self]** +**create() -> Sequence[Self]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/builtins/finish.py#L72-L106) Create FinishTool instance. @@ -28,6 +30,16 @@ Create FinishTool instance. - `ValueError` – If any parameters are provided. + + + None + + + + + None + + ## class ThinkTool Bases: `ToolDefinition[ThinkAction, ThinkObservation]` @@ -36,7 +48,9 @@ Tool for logging thoughts without making changes. ### Methods -**create(conv_state: ConversationState | None = None, params = \{\}) -> Sequence[Self]** +**create() -> Sequence[Self]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/builtins/think.py#L84-L117) Create ThinkTool instance. @@ -53,12 +67,48 @@ Create ThinkTool instance. - `ValueError` – If any parameters are provided. + + + None + + + + + None + + **list_registered_tools() -> list[str]** -**register_tool(name: str, factory: ToolDefinition | type[ToolDefinition] | Callable[..., Sequence[ToolDefinition]]) -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/registry.py#L171-L173) + +**register_tool() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/registry.py#L119-L156) + + + + None + + + + + None + + +**resolve_tool() -> Sequence[ToolDefinition]** -**resolve_tool(tool_spec: Tool, conv_state: ConversationState) -> Sequence[ToolDefinition]** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/registry.py#L159-L168) + + + None + + + + + None + + ## class Action Bases: `Schema`, `ABC` @@ -102,7 +152,9 @@ same text that would be sent to the LLM. ### Methods -**from_text(text: str, is_error: bool = False, kwargs: Any = \{\}) -> Self** +**from_text() -> Self** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/schema.py#L223-L240) Utility to create an Observation from a simple text string. @@ -116,6 +168,21 @@ Utility to create an Observation from a simple text string. - *Self* An Observation instance with the text wrapped in a TextContent. + + + None + + + + + None + + + + + None + + ## class Tool Bases: `BaseModel` @@ -133,14 +200,28 @@ This is only used in agent-sdk for type schema for server use. ### Methods -**validate_name(v: str) -> str** +**validate_name() -> str** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/spec.py#L27-L33) Validate that name is not empty. -**validate_params(v: dict[str, Any] | None) -> dict[str, Any]** + + + None + + +**validate_params() -> dict[str, Any]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/spec.py#L35-L39) Convert None params to empty dict. + + + None + + ## class ExecutableTool Bases: `Protocol` @@ -227,7 +308,9 @@ Complex tool with initialization parameters: ### Methods -**abstractmethod create(args = (), kwargs = \{\}) -> Sequence[Self]** +**abstractmethod create() -> Sequence[Self]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/tool.py#L206-L223) Create a sequence of Tool instances. @@ -245,12 +328,31 @@ from conv_state and other optional parameters. - *Sequence[Self]* A sequence of Tool instances. Even single tools are returned as a sequence - *Sequence[Self]* to provide a consistent interface and eliminate union return types. -**set_executor(executor: ToolExecutor) -> Self** + + + None + + + + + None + + +**set_executor() -> Self** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/tool.py#L263-L265) Create a new Tool instance with the given executor. + + + None + + **as_executable() -> ExecutableTool** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/tool.py#L267-L281) + Return this tool as an ExecutableTool, ensuring it has an executor. This method eliminates the need for runtime None checks by guaranteeing @@ -264,7 +366,9 @@ that the returned tool has a non-None executor. - `NotImplementedError` – If the tool has no executor. -**action_from_arguments(arguments: dict[str, Any]) -> Action** +**action_from_arguments() -> Action** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/tool.py#L283-L295) Create an action from parsed arguments. @@ -279,7 +383,14 @@ for creating actions from arguments (e.g., for MCP tools). - *Action* The action instance created from the arguments. -**to_mcp_tool(input_schema: dict[str, Any] | None = None, output_schema: dict[str, Any] | None = None) -> dict[str, Any]** + + + None + + +**to_mcp_tool() -> dict[str, Any]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/tool.py#L328-L360) Convert a Tool to an MCP tool definition. @@ -290,7 +401,19 @@ Allow overriding input/output schemas (usually by subclasses). - `input_schema` *dict[str, Any] | None* – Optionally override the input schema. - `output_schema` *dict[str, Any] | None* – Optionally override the output schema. -**to_openai_tool(add_security_risk_prediction: bool = False, action_type: type[Schema] | None = None) -> ChatCompletionToolParam** + + + None + + + + + None + + +**to_openai_tool() -> ChatCompletionToolParam** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/tool.py#L381-L411) Convert a Tool to an OpenAI tool. @@ -304,7 +427,19 @@ the risk level before calling the tool. This is useful for MCPTool to use a dynamically created action type based on the tool's input schema. -**to_responses_tool(add_security_risk_prediction: bool = False, action_type: type[Schema] | None = None) -> FunctionToolParam** + + + None + + + + + None + + +**to_responses_tool() -> FunctionToolParam** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/tool.py#L413-L441) Convert a Tool to a Responses API function tool (LiteLLM typed). @@ -316,7 +451,19 @@ For Responses API, function tools expect top-level keys: - `add_security_risk_prediction` *bool* – Whether to add a `security_risk` field - `action_type` *type[Schema] | None* – Optional override for the action type -**resolve_kind(kind: str) -> type** + + + None + + + + + None + + +**resolve_kind() -> type** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/tool.py#L443-L476) Resolve a kind string to its corresponding tool class. @@ -332,6 +479,11 @@ Resolve a kind string to its corresponding tool class. - `ValueError` – If the kind is unknown + + + None + + ## class ToolExecutor Bases: `ABC` @@ -342,6 +494,8 @@ Executor function type for a Tool. **close() -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/tool.py#L121-L128) + Close the executor and clean up resources. Default implementation does nothing. Subclasses should override diff --git a/sdk/api-reference/openhands.sdk.utils.mdx b/sdk/api-reference/openhands.sdk.utils.mdx index 63472e53..049c37be 100644 --- a/sdk/api-reference/openhands.sdk.utils.mdx +++ b/sdk/api-reference/openhands.sdk.utils.mdx @@ -1,9 +1,11 @@ --- title: openhands.sdk.utils -description: API reference for openhands.sdk.utils +description: API reference for openhands.sdk.utils module --- -**sanitized_env(env: Mapping[str, str] | None = None) -> dict[str, str]** +**sanitized_env() -> dict[str, str]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/utils/command.py#L14-L36) Return a copy of *env* with sanitized values. @@ -11,7 +13,14 @@ PyInstaller-based binaries rewrite ``LD_LIBRARY_PATH`` so their vendored libraries win. This function restores the original value so that subprocess will not use them. -**deprecated(deprecated_in: str, removed_in: str | date | None, current_version: str | None = None, details: str = '') -> Callable[[_FuncT], _FuncT]** + + + None + + +**deprecated() -> Callable[[_FuncT], _FuncT]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/utils/deprecation.py#L29-L54) Return a decorator that deprecates a callable with explicit metadata. @@ -20,7 +29,29 @@ Use this helper when you can annotate a function, method, or property with while filling in the SDK's current version metadata unless custom values are supplied. -**warn_deprecated(feature: str, deprecated_in: str, removed_in: str | date | None, current_version: str | None = None, details: str = '', stacklevel: int = 2) -> None** + + + None + + + + + None + + + + + None + + + + + None + + +**warn_deprecated() -> None** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/utils/deprecation.py#L83-L112) Emit a deprecation warning for dynamic access to a legacy feature. @@ -29,7 +60,39 @@ data migrations, or other runtime paths that must conditionally warn. Provide explicit version metadata so the SDK reports consistent messages and upgrades to :class:`deprecation.UnsupportedWarning` after the removal threshold. -**sanitize_openhands_mentions(text: str) -> str** + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + +**sanitize_openhands_mentions() -> str** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/utils/github.py#L14-L44) Sanitize @OpenHands mentions in text to prevent self-mention loops. @@ -56,7 +119,14 @@ preserving readability. The original case of the mention is preserved. 'No mention here' ``` -**maybe_truncate(content: str, truncate_after: int | None = None, truncate_notice: str = DEFAULT_TRUNCATE_NOTICE, save_dir: str | None = None, tool_prefix: str = 'output') -> str** + + + None + + +**maybe_truncate() -> str** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/utils/truncate.py#L50-L117) Truncate the middle of content if it exceeds the specified length. @@ -75,3 +145,29 @@ Optionally saves the full content to a file for later investigation. - *str* Original content if under limit, or truncated content with head and tail - *str* preserved and reference to saved file if applicable + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + \ No newline at end of file diff --git a/sdk/api-reference/openhands.sdk.workspace.mdx b/sdk/api-reference/openhands.sdk.workspace.mdx index 56564da7..a9655870 100644 --- a/sdk/api-reference/openhands.sdk.workspace.mdx +++ b/sdk/api-reference/openhands.sdk.workspace.mdx @@ -1,6 +1,6 @@ --- title: openhands.sdk.workspace -description: API reference for openhands.sdk.workspace +description: API reference for openhands.sdk.workspace module --- ## class BaseWorkspace @@ -19,7 +19,9 @@ support the context manager protocol for safe resource management. ### Methods -**abstractmethod execute_command(command: str, cwd: str | Path | None = None, timeout: float = 30.0) -> CommandResult** +**abstractmethod execute_command() -> CommandResult** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/base.py#L69-L90) Execute a bash command on the system. @@ -38,7 +40,24 @@ metadata - `Exception` – If command execution fails -**abstractmethod file_upload(source_path: str | Path, destination_path: str | Path) -> FileOperationResult** + + + None + + + + + None + + + + + None + + +**abstractmethod file_upload() -> FileOperationResult** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/base.py#L92-L110) Upload a file to the system. @@ -55,7 +74,19 @@ Upload a file to the system. - `Exception` – If file upload fails -**abstractmethod file_download(source_path: str | Path, destination_path: str | Path) -> FileOperationResult** + + + None + + + + + None + + +**abstractmethod file_download() -> FileOperationResult** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/base.py#L112-L130) Download a file from the system. @@ -72,7 +103,19 @@ Download a file from the system. - `Exception` – If file download fails -**abstractmethod git_changes(path: str | Path) -> list[GitChange]** + + + None + + + + + None + + +**abstractmethod git_changes() -> list[GitChange]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/base.py#L132-L144) Get the git changes for the repository at the path given. @@ -88,7 +131,14 @@ Get the git changes for the repository at the path given. - `Exception` – If path is not a git repository or getting changes failed -**abstractmethod git_diff(path: str | Path) -> GitDiff** + + + None + + +**abstractmethod git_diff() -> GitDiff** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/base.py#L146-L158) Get the git diff for the file at the path given. @@ -104,8 +154,15 @@ Get the git diff for the file at the path given. - `Exception` – If path is not a git repository or getting diff failed + + + None + + **pause() -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/base.py#L160-L169) + Pause the workspace to conserve resources. For local workspaces, this is a no-op. @@ -117,6 +174,8 @@ For container-based workspaces, this pauses the container. **resume() -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/base.py#L171-L180) + Resume a paused workspace. For local workspaces, this is a no-op. @@ -138,9 +197,23 @@ should operate directly on the host system. ### Methods -**__init__(working_dir: str | Path, kwargs: Any = \{\})** +**__init__()** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/local.py#L31-L34) + + + + None + + -**execute_command(command: str, cwd: str | Path | None = None, timeout: float = 30.0) -> CommandResult** + + None + + +**execute_command() -> CommandResult** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/local.py#L36-L69) Execute a bash command locally. @@ -158,7 +231,24 @@ timeout handling, output streaming, and error management. - *CommandResult* Result with stdout, stderr, exit_code, command, and timeout_occurred -**file_upload(source_path: str | Path, destination_path: str | Path) -> FileOperationResult** + + + None + + + + + None + + + + + None + + +**file_upload() -> FileOperationResult** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/local.py#L71-L114) Upload (copy) a file locally. @@ -174,7 +264,19 @@ using shutil.copy2 to preserve metadata. - *FileOperationResult* Result with success status and file information -**file_download(source_path: str | Path, destination_path: str | Path) -> FileOperationResult** + + + None + + + + + None + + +**file_download() -> FileOperationResult** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/local.py#L116-L159) Download (copy) a file locally. @@ -190,7 +292,19 @@ using shutil.copy2 to preserve metadata. - *FileOperationResult* Result with success status and file information -**git_changes(path: str | Path) -> list[GitChange]** + + + None + + + + + None + + +**git_changes() -> list[GitChange]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/local.py#L161-L174) Get the git changes for the repository at the path given. @@ -206,7 +320,14 @@ Get the git changes for the repository at the path given. - `Exception` – If path is not a git repository or getting changes failed -**git_diff(path: str | Path) -> GitDiff** + + + None + + +**git_diff() -> GitDiff** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/local.py#L176-L189) Get the git diff for the file at the path given. @@ -222,8 +343,15 @@ Get the git diff for the file at the path given. - `Exception` – If path is not a git repository or getting diff failed + + + None + + **pause() -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/local.py#L191-L197) + Pause the workspace (no-op for local workspaces). Local workspaces have nothing to pause since they operate directly @@ -231,6 +359,8 @@ on the host filesystem. **resume() -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/local.py#L199-L205) + Resume the workspace (no-op for local workspaces). Local workspaces have nothing to resume since they operate directly @@ -297,12 +427,16 @@ Returns: **reset_client() -> None** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/remote/base.py#L34-L45) + 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. -**execute_command(command: str, cwd: str | Path | None = None, timeout: float = 30.0) -> CommandResult** +**execute_command() -> CommandResult** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/remote/base.py#L77-L98) Execute a bash command on the remote system. @@ -319,7 +453,24 @@ then polls for the output until the command completes. - *CommandResult* Result with stdout, stderr, exit_code, and other metadata -**file_upload(source_path: str | Path, destination_path: str | Path) -> FileOperationResult** + + + None + + + + + None + + + + + None + + +**file_upload() -> FileOperationResult** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/remote/base.py#L100-L118) Upload a file to the remote system. @@ -334,7 +485,19 @@ Reads the local file and sends it to the remote system via HTTP API. - *FileOperationResult* Result with success status and metadata -**file_download(source_path: str | Path, destination_path: str | Path) -> FileOperationResult** + + + None + + + + + None + + +**file_download() -> FileOperationResult** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/remote/base.py#L120-L138) Download a file from the remote system. @@ -349,7 +512,19 @@ Requests the file from the remote system via HTTP API and saves it locally. - *FileOperationResult* Result with success status and metadata -**git_changes(path: str | Path) -> list[GitChange]** + + + None + + + + + None + + +**git_changes() -> list[GitChange]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/remote/base.py#L140-L154) Get the git changes for the repository at the path given. @@ -365,7 +540,14 @@ Get the git changes for the repository at the path given. - `Exception` – If path is not a git repository or getting changes failed -**git_diff(path: str | Path) -> GitDiff** + + + None + + +**git_diff() -> GitDiff** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/remote/base.py#L156-L170) Get the git diff for the file at the path given. @@ -381,6 +563,11 @@ Get the git diff for the file at the path given. - `Exception` – If path is not a git repository or getting diff failed + + + None + + ## class Workspace Factory entrypoint that returns a LocalWorkspace or RemoteWorkspace.