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..1cc24b84 100755 --- a/scripts/generate-api-docs.py +++ b/scripts/generate-api-docs.py @@ -1,785 +1,368 @@ #!/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 +from typing import Final +import griffe -# Configure logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s") logger = logging.getLogger(__name__) -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' - -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.napoleon', - 'sphinx_markdown_builder', +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" + +SOURCE_CODE_BASE: Final[str] = "https://github.com/OpenHands/software-agent-sdk" + +MODULES: Final[list[str]] = [ + "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}' + 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] = [] + + """ + + An example of a parameter field + + """ + # 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( + f""" + + {p.docstring} + + """ + ) + + # 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] = [f"## class {cls.name}", ""] + + 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: - # 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 - + 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: - 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) - 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.""" + 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} module", + "---", + "", + ] + + 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() -> None: + 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: - 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 - - -def main(): - """Main entry point.""" - docs_dir = Path(__file__).parent.parent - generator = SimpleAPIDocGenerator(docs_dir) - generator.run() + 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..68d42ba5 100644 --- a/sdk/api-reference/openhands.sdk.agent.mdx +++ b/sdk/api-reference/openhands.sdk.agent.mdx @@ -3,10 +3,9 @@ title: openhands.sdk.agent description: API reference for openhands.sdk.agent module --- +## class Agent -### class Agent - -Bases: [`AgentBase`](#class-agentbase) +Bases: `AgentBase` Main agent implementation for OpenHands. @@ -14,62 +13,54 @@ 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 -#### Methods +**init_state() -> None** -#### init_state() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/agent/agent.py#L104-L196) -Initialize the empty conversation state to prepare the agent for user -messages. +Initialize conversation state. -Typically this involves adding system message - -NOTE: state will be mutated in-place. +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. -#### model_post_init() +These invariants keep event ordering predictable for downstream components +(condenser, UI, etc.) and also prevent accidentally materializing the full +event history during initialization. -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. + + None + + -* Parameters: - * `self` – The BaseModel instance. - * `context` – The context. + + None + + +**step() -> None** -#### step() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/agent/agent.py#L249-L414) -Taking a step in the conversation. - -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. + + None + + -NOTE: state will be mutated in-place. + + None + + -### class AgentBase + + None + + +## class AgentBase Bases: `DiscriminatedUnionMixin`, `ABC` @@ -79,45 +70,47 @@ 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 - -#### get_all_llms() +Raises: + RuntimeError: If the agent has not been initialized. -Recursively yield unique base-class LLM objects reachable from self. +### Methods -- 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() -> None** -#### init_state() +[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. @@ -126,21 +119,19 @@ 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. + + None + + -It takes context as an argument since that’s what pydantic-core passes when calling it. + + None + + +**abstractmethod step() -> None** -* Parameters: - * `self` – The BaseModel instance. - * `context` – The context. - -#### abstractmethod step() +[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. @@ -148,41 +139,93 @@ 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() + + + 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. -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. + + + + 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). +- 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..65175b7f 100644 --- a/sdk/api-reference/openhands.sdk.conversation.mdx +++ b/sdk/api-reference/openhands.sdk.conversation.mdx @@ -3,8 +3,7 @@ title: openhands.sdk.conversation description: API reference for openhands.sdk.conversation module --- - -### class BaseConversation +## class BaseConversation Bases: `ABC` @@ -14,847 +13,1509 @@ 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 - -#### __init__() - -Initialize the base conversation with span tracking. - -#### abstractmethod ask_agent() - -Ask the agent a simple, stateless question and get a direct LLM response. - -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: - `question` – A simple string question to ask the agent -* Returns: - A string response from the agent - -#### abstractmethod close() - -#### static compose_callbacks() -Compose multiple callbacks into a single callback function. - -* Parameters: - `callbacks` – An iterable of callback functions -* Returns: - A single callback function that calls all provided callbacks +Returns True if BOTH conditions are met: +1. The conversation state has a security analyzer set (not None) +2. The confirmation policy is active -#### abstractmethod condense() - -Force condensation of the conversation history. +### Methods -This method uses the existing condensation request pattern to trigger -condensation. It adds a CondensationRequest event to the conversation -and forces the agent to take a single step to process it. +**__init__() -> None** -The condensation will be applied immediately and will modify the conversation -state by adding a condensation event to the history. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L107-L109) -* Raises: - `ValueError` – If no condenser is configured or the condenser doesn’t - handle condensation requests. +Initialize the base conversation with span tracking. -#### abstractmethod generate_title() +**abstractmethod send_message() -> None** -Generate a title for the conversation based on the first user message. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L138-L150) -* 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. +Send a message to the agent. -#### static get_persistence_dir() +**Parameters:** -Get the persistence directory for the conversation. +- `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. -* 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. -#### abstractmethod pause() + + None + + -#### abstractmethod reject_pending_actions() + + None + + +**abstractmethod run() -> None** -#### abstractmethod run() +[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 send_message() - -Send a message to the agent. - -* 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. +**abstractmethod set_confirmation_policy() -> None** -#### abstractmethod set_confirmation_policy() +[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() - -Set the security analyzer for the conversation. - -#### abstractmethod update_secrets() - -### class Conversation - -### class Conversation - -Bases: `object` -Factory class for creating conversation instances with OpenHands agents. + + None + + +**abstractmethod set_security_analyzer() -> None** -This factory automatically creates either a LocalConversation or RemoteConversation -based on the workspace type provided. LocalConversation runs the agent locally, -while RemoteConversation connects to a remote agent server. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L166-L169) -* Returns: - LocalConversation if workspace is local, RemoteConversation if workspace - is remote. +Set the security analyzer for the conversation. -#### Example -```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() -``` + + None + + +**abstractmethod reject_pending_actions() -> None** -### class ConversationExecutionStatus +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L188-L191) -Bases: `str`, `Enum` -Enum representing the current execution state of the conversation. + + None + + +**abstractmethod pause() -> None** -#### Methods +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L193-L194) -#### DELETING = 'deleting' +**abstractmethod update_secrets() -> None** -#### ERROR = 'error' +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L196-L197) -#### FINISHED = 'finished' -#### IDLE = 'idle' + + None + + +**abstractmethod close() -> None** -#### PAUSED = 'paused' +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L199-L200) -#### RUNNING = 'running' +**abstractmethod generate_title() -> str** -#### STUCK = 'stuck' +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L202-L217) -#### WAITING_FOR_CONFIRMATION = 'waiting_for_confirmation' +Generate a title for the conversation based on the first user message. -### class ConversationState +**Parameters:** -Bases: `OpenHandsModel` +- `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:** -#### Properties +- *str* A generated title for the conversation. -- `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 +**Raises:** -#### Methods +- `ValueError` – If no user messages are found in the conversation. -#### acquire() -Acquire the lock. + + None + + -* 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. + + None + + +**get_persistence_dir() -> str** -#### block_action() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L219-L234) -Persistently record a hook-blocked action. +Get the persistence directory for the conversation. -#### block_message() +**Parameters:** -Persistently record a hook-blocked user message. +- `persistence_base_dir` *str | Path* – Base directory for persistence. Can be a string +path or Path object. +- `conversation_id` *ConversationID* – Unique conversation ID. -#### classmethod create() +**Returns:** -Create a new conversation state or resume from persistence. +- *str* String path to the conversation-specific persistence directory. +- *str* Always returns a normalized string path even if a Path was provided. -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. + + None + + -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. + + None + + +**abstractmethod ask_agent() -> str** -* 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 +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L236-L252) -#### static get_unmatched_actions() +Ask the agent a simple, stateless question and get a direct LLM response. -Find actions in the event history that don’t have matching observations. +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. -This method identifies ActionEvents that don’t have corresponding -ObservationEvents or UserRejectObservations, which typically indicates -actions that are pending confirmation or execution. +**Parameters:** -* Parameters: - `events` – List of events to search through -* Returns: - List of ActionEvent objects that don’t have corresponding observations, - in chronological order +- `question` *str* – A simple string question to ask the agent -#### locked() +**Returns:** -Return True if the lock is currently held by any thread. +- *str* A string response from the agent -#### model_config = (configuration object) -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + + None + + +**abstractmethod condense() -> None** -#### model_post_init() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L254-L269) -This function is meant to behave like a BaseModel method to initialise private attributes. +Force condensation of the conversation history. -It takes context as an argument since that’s what pydantic-core passes when calling it. +This method uses the existing condensation request pattern to trigger +condensation. It adds a CondensationRequest event to the conversation +and forces the agent to take a single step to process it. -* Parameters: - * `self` – The BaseModel instance. - * `context` – The context. +The condensation will be applied immediately and will modify the conversation +state by adding a condensation event to the history. -#### owned() +**Raises:** -Return True if the lock is currently held by the calling thread. +- `ValueError` – If no condenser is configured or the condenser doesn't + handle condensation requests. -#### pop_blocked_action() +**abstractmethod execute_tool() -> Observation** -Remove and return a hook-blocked action reason, if present. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L271-L299) -#### pop_blocked_message() +Execute a tool directly without going through the agent loop. -Remove and return a hook-blocked message reason, if present. +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. -#### release() +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. -Release the lock. +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 -* Raises: - `RuntimeError` – If the current thread doesn’t own the lock. +**Parameters:** -#### set_on_state_change() +- `tool_name` *str* – The name of the tool to execute (e.g., "sleeptime_compute") +- `action` *Action* – The action to pass to the tool executor -Set a callback to be called when state changes. +**Returns:** -* Parameters: - `callback` – A function that takes an Event (ConversationStateUpdateEvent) - or None to remove the callback +- *Observation* The observation returned by the tool execution -### class ConversationVisualizerBase +**Raises:** -Bases: `ABC` +- `KeyError` – If the tool is not found in the agent's tools +- `NotImplementedError` – If the tool has no executor -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. + + None + + -The typical usage pattern: -1. Create a visualizer instance: + + None + + +**compose_callbacks() -> CallbackType** - viz = MyVisualizer() -1. Pass it to Conversation: conv = Conversation(agent, visualizer=viz) -2. Conversation automatically calls viz.initialize(state) to attach the state +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/base.py#L301-L317) -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) +Compose multiple callbacks into a single callback function. -Conversation will then calls MyVisualizer() followed by initialize(state) +**Parameters:** +- `callbacks` *Iterable[CallbackType]* – An iterable of callback functions -#### Properties +**Returns:** -- `conversation_stats`: ConversationStats | None - Get conversation stats from the state. +- *CallbackType* A single callback function that calls all provided callbacks -#### Methods -#### __init__() + + None + + +## class Conversation -Initialize the visualizer base. +Factory class for creating conversation instances with OpenHands agents. -#### create_sub_visualizer() +This factory automatically creates either a LocalConversation or RemoteConversation +based on the workspace type provided. LocalConversation runs the agent locally, +while RemoteConversation connects to a remote agent server. -Create a visualizer for a sub-agent during delegation. +**Returns:** -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. +- LocalConversation if workspace is local, RemoteConversation if workspace +- is remote. -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. +## class EventLog -* 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 +Bases: `EventsListBase` -#### final initialize() +Persistent event log with locking for concurrent writes. -Initialize the visualizer with conversation state. +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. -This method is called by Conversation after the state is created, -allowing the visualizer to access conversation stats and other -state information. +### Methods -Subclasses should not override this method, to ensure the state is set. +**__init__() -> None** -* Parameters: - `state` – The conversation state object +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/event_store.py#L41-L47) -#### abstractmethod on_event() -Handle a conversation event. + + None + + -This method is called for each event in the conversation and should -implement the visualization logic. + + None + + +**get_index() -> int** -* Parameters: - `event` – The event to visualize +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/event_store.py#L49-L54) -### class DefaultConversationVisualizer +Return the integer index for a given event_id. -Bases: [`ConversationVisualizerBase`](#class-conversationvisualizerbase) -Handles visualization of conversation events with Rich formatting. + + None + + +**get_id() -> EventID** -Provides Rich-formatted output with semantic dividers and complete content display. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/event_store.py#L56-L62) -#### Methods +Return the event_id for a given index. -#### __init__() -Initialize the visualizer. + + None + + +**append() -> None** -* 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. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/event_store.py#L99-L132) -#### on_event() +Append an event with locking for thread/process safety. -Main event handler that displays events with Rich formatting. +**Raises:** -### class EventLog +- `TimeoutError` – If the lock cannot be acquired within LOCK_TIMEOUT_SECONDS. +- `ValueError` – If an event with the same ID already exists. -Bases: [`EventsListBase`](#class-eventslistbase) -Persistent event log with locking for concurrent writes. + + None + + +## class EventsListBase -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. +Bases: `Sequence[Event]`, `ABC` -#### Methods +Abstract base class for event lists that can be appended to. -#### 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. +This provides a common interface for both local EventLog and remote +RemoteEventsList implementations, avoiding circular imports in protocols. -#### __init__() +### Methods -#### append() +**abstractmethod append() -> None** -Append an event with locking for thread/process safety. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/events_list_base.py#L14-L17) -* Raises: - * `TimeoutError` – If the lock cannot be acquired within LOCK_TIMEOUT_SECONDS. - * `ValueError` – If an event with the same ID already exists. +Add a new event to the list. -#### get_id() -Return the event_id for a given index. + + None + + +## class WebSocketConnectionError -#### get_index() +Bases: `RuntimeError` -Return the integer index for a given event_id. +Raised when WebSocket connection fails to establish within the timeout. -### class EventsListBase +### Properties -Bases: `Sequence`[`Event`], `ABC` +- `conversation_id` +- `timeout` -Abstract base class for event lists that can be appended to. +### Methods -This provides a common interface for both local EventLog and remote -RemoteEventsList implementations, avoiding circular imports in protocols. +**__init__() -> None** -#### Methods +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/exceptions.py#L10-L22) -#### abstractmethod append() -Add a new event to the list. + + None + + -### class LocalConversation + + None + + -Bases: [`BaseConversation`](#class-baseconversation) + + None + + +## class LocalConversation +Bases: `BaseConversation` -#### Properties +### Properties - `agent`: AgentBase -- `id`: UUID - Get the unique ID of the conversation. -- `llm_registry`: LLMRegistry +- `workspace`: LocalWorkspace - `max_iteration_per_run`: int -- `state`: [ConversationState](#class-conversationstate) +- `llm_registry`: LLMRegistry +- `delete_on_close`: bool +- `id`: ConversationID + Get the unique ID of the conversation. +- `state`: 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 + +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. -- `workspace`: LocalWorkspace +- `resolved_plugins`: list[ResolvedPluginSource] | None + Get the resolved plugin sources after plugins are loaded. -#### Methods +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. -#### __init__() +### Methods -Initialize the conversation. +**__init__()** -* 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() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L76-L248) -Ask the agent a simple, stateless question and get a direct LLM response. +Initialize 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. +**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. + + + + 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) -* Parameters: - `question` – A simple string question to ask the agent -* Returns: - A string response from the agent +Send a message to the agent. -#### close() +**Parameters:** -Close the conversation and clean up all tool executors. +- `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. -#### condense() -Synchronously force condense the conversation history. + + None + + -If the agent is currently running, condense() will wait for the -ongoing step to finish before proceeding. + + None + + +**run() -> None** -Raises ValueError if no compatible condenser exists. +[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. -#### property conversation_stats +In confirmation mode: +- First call: creates actions but doesn't execute them, stops and waits +- Second call: executes pending actions (implicit confirmation) -#### generate_title() +In normal mode: +- Creates and executes actions immediately -Generate a title for the conversation based on the first user message. +Can be paused between steps -* 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. +**set_confirmation_policy() -> None** -#### pause() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L620-L624) -Pause agent execution. +Set the confirmation policy and store it in conversation state. -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). -Note: If called during an LLM completion, the pause will not take -effect until the current LLM call completes. + + None + + +**reject_pending_actions() -> None** -#### reject_pending_actions() +[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. -#### run() - -Runs the conversation until the agent finishes. - -In confirmation mode: -- First call: creates actions but doesn’t execute them, stops and waits -- Second call: executes pending actions (implicit confirmation) - -In normal mode: -- Creates and executes actions immediately -Can be paused between steps + + None + + +**pause() -> None** -#### send_message() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L657-L680) -Send a message to the agent. +Pause agent 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. +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_confirmation_policy() +Note: If called during an LLM completion, the pause will not take +effect until the current LLM call completes. -Set the confirmation policy and store it in conversation state. +**update_secrets() -> None** -#### set_security_analyzer() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L682-L693) -Set the security analyzer for the conversation. +Add secrets to the conversation. -#### update_secrets() +**Parameters:** -Add secrets to the conversation. +- `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. -* Parameters: - `secrets` – Dictionary mapping secret keys to values or no-arg callables. - SecretValue = str | Callable[[], str]. Callables are invoked lazily - when a command references the secret key. -### class RemoteConversation + + None + + +**set_security_analyzer() -> None** -Bases: [`BaseConversation`](#class-baseconversation) +[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. -#### Properties -- `agent`: AgentBase -- `id`: UUID -- `max_iteration_per_run`: int -- `state`: RemoteState - Access to remote conversation state. -- `workspace`: RemoteWorkspace + + None + + +**close() -> None** -#### Methods +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L700-L731) -#### __init__() +Close the conversation and clean up all tool executors. -Remote conversation proxy that talks to an agent server. +**ask_agent() -> str** -* 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() +[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. -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 - -#### close() - -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. - -#### condense() +**Parameters:** -Force condensation of the conversation history. +- `question` *str* – A simple string question to ask the agent -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. +**Returns:** -The condensation will be applied on the server side and will modify the -conversation state by adding a condensation event to the history. +- *str* A string response from the agent -* Raises: - `HTTPError` – If the server returns an error (e.g., no condenser configured). -#### property conversation_stats + + None + + +**generate_title() -> str** -#### generate_title() +[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. -* 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. - -#### pause() +**Parameters:** -#### reject_pending_actions() +- `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. -#### run() +**Returns:** -Trigger a run on the server. - -* 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. +- *str* A generated title for the conversation. -#### send_message() +**Raises:** -Send a message to the agent. +- `ValueError` – If no user messages are found in the conversation. -* 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. -#### set_confirmation_policy() + + None + + -Set the confirmation policy for the conversation. + + None + + +**condense() -> None** -#### set_security_analyzer() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L822-L869) -Set the security analyzer for the remote conversation. +Synchronously force condense the conversation history. -#### property stuck_detector +If the agent is currently running, `condense()` will wait for the +ongoing step to finish before proceeding. -Stuck detector for compatibility. -Not implemented for remote conversations. +Raises ValueError if no compatible condenser exists. -#### update_secrets() +**execute_tool() -> Observation** -### class SecretRegistry +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L871-L912) -Bases: `OpenHandsModel` +Execute a tool directly without going through the agent loop. -Manages secrets and injects them into bash commands when needed. +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. -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. +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. -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. +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 -Additionally, it tracks the latest exported values to enable consistent masking -even when callable secrets fail on subsequent calls. +**Parameters:** +- `tool_name` *str* – The name of the tool to execute (e.g., "sleeptime_compute") +- `action` *Action* – The action to pass to the tool executor -#### Properties +**Returns:** -- `secret_sources`: dict[str, SecretSource] +- *Observation* The observation returned by the tool execution -#### Methods +**Raises:** -#### find_secrets_in_text() +- `KeyError` – If the tool is not found in the agent's tools +- `NotImplementedError` – If the tool has no executor -Find all secret keys mentioned in the given text. -* Parameters: - `text` – The text to search for secret keys -* Returns: - Set of secret keys found in the text + + None + + -#### get_secrets_as_env_vars() + + None + + +## class RemoteConversation -Get secrets that should be exported as environment variables for a command. +Bases: `BaseConversation` -* Parameters: - `command` – The bash command to check for secret references -* Returns: - Dictionary of environment variables to export (key -> value) +### Properties -#### mask_secrets_in_output() +- `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. + +### Methods + +**__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. + +**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 + + + + 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. + +**Parameters:** + +- `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. + +**Raises:** + +- `ConversationRunError` – If the run fails or times out. + + + + 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. + + + + 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** + +[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** + +[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. + +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:** + +- `question` *str* – A simple string question to ask the agent + +**Returns:** + +- *str* A string response from the agent + + + + 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. + +**Parameters:** + +- `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. + +**Returns:** + +- *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. +The server will use the existing condensation request pattern to trigger +condensation if a condenser is configured and handles condensation requests. + +The condensation will be applied on the server side and will modify the +conversation state by adding a condensation event to the history. + +**Raises:** + +- `HTTPError` – If the server returns an error (e.g., no condenser configured). + +**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. + +Note: This method is not yet supported for RemoteConversation. +Tool execution for remote conversations happens on the server side +during the normal agent loop. + +**Parameters:** + +- `tool_name` *str* – The name of the tool to execute +- `action` *Action* – The action to pass to the tool executor + +**Raises:** + +- `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() -> 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. + +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 + +**Parameters:** + +- `events` *Sequence[Event]* – List of conversation events to search through. + +**Returns:** + +- *str* The final response message from the agent, or empty string if not found. + + + + None + + +## class SecretRegistry + +Bases: `OpenHandsModel` + +Manages secrets and injects them into bash commands when needed. + +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. + +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. + +Additionally, it tracks the latest exported values to enable consistent masking +even when callable secrets fail on subsequent calls. + +### Properties + +- `secret_sources`: dict[str, SecretSource] + +### Methods + +**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. + +**Parameters:** + +- `secrets` *Mapping[str, SecretValue]* – Dictionary mapping secret keys to either string values + or callable functions that return string values + + + + 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. + +**Parameters:** + +- `text` *str* – The text to search for secret keys + +**Returns:** + +- *set[str]* Set of secret keys found in the text + + + + 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. + +**Parameters:** + +- `command` *str* – The bash command to check for secret references + +**Returns:** + +- *dict[str, str]* Dictionary of environment variables to export (key -> value) + + + + 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. This method uses both the current exported values and attempts to get fresh values from callables to ensure comprehensive masking. -* Parameters: - `text` – The text to mask secrets in -* Returns: - Text with secret values replaced by `` +**Parameters:** -#### model_config = (configuration object) +- `text` *str* – The text to mask secrets in -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +**Returns:** -#### model_post_init() +- *str* Text with secret values replaced by `` -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. + + None + + +## class ConversationExecutionStatus -* Parameters: - * `self` – The BaseModel instance. - * `context` – The context. +Bases: `str`, `Enum` -#### update_secrets() +Enum representing the current execution state of the conversation. -Add or update secrets in the manager. +### Properties + +- `IDLE` +- `RUNNING` +- `PAUSED` +- `WAITING_FOR_CONFIRMATION` +- `FINISHED` +- `ERROR` +- `STUCK` +- `DELETING` + +### Methods + +**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 +actively processing. These are: FINISHED, ERROR, STUCK. + +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. + +**Returns:** + +- *bool* True if this is a terminal status, False otherwise. + +## class ConversationState + +Bases: `OpenHandsModel` + +### Properties + +- `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. + +### Methods + +**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. + +**Parameters:** + +- `callback` *ConversationCallbackType | None* – A function that takes an Event (ConversationStateUpdateEvent) + or None to remove the callback -* Parameters: - `secrets` – Dictionary mapping secret keys to either string values - or callable functions that return string values -### class StuckDetector + + None + + +**create() -> ConversationState** -Bases: `object` +[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. + +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. + +**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. + +**Parameters:** + +- `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. + +**Returns:** + +- *ConversationState* ConversationState ready for use + +**Raises:** + +- `ValueError` – If conversation ID or tools mismatch on restore +- `ValidationError` – If agent or other fields fail Pydantic validation + + + + 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. + + + + 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. + + + + 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. + + + + 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. + + + + 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. + +This method identifies ActionEvents that don't have corresponding +ObservationEvents or UserRejectObservations, which typically indicates +actions that are pending confirmation or execution. + +**Parameters:** + +- `events` *Sequence[Event]* – List of events to search through + +**Returns:** + +- *list[ActionEvent]* List of ActionEvent objects that don't have corresponding observations, +- *list[ActionEvent]* in chronological order + + + + 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. + +**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. + + + + 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:** + +- `RuntimeError` – If the current thread doesn't own 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 Detects when an agent is stuck in repetitive or unproductive patterns. @@ -865,20 +1526,186 @@ 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 + +**__init__()** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/stuck_detector.py#L38-L44) -#### Methods -#### __init__() + + None + + -#### is_stuck() + + 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, +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__()** + +[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() -> 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. + +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 + + + + 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. + +This method is called for each event in the conversation and should +implement the visualization logic. + +**Parameters:** + +- `event` *Event* – The event to visualize + + + + 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. + +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 + + + + None + + +## 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__()** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/conversation/visualizer/default.py#L224-L242) + +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. + + + + 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 5e2fbcaa..2ad67990 100644 --- a/sdk/api-reference/openhands.sdk.event.mdx +++ b/sdk/api-reference/openhands.sdk.event.mdx @@ -3,129 +3,130 @@ title: openhands.sdk.event description: API reference for openhands.sdk.event module --- +## 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 + Return Rich Text representation of this event. -#### to_llm_message() +This is a fallback implementation for unknown event types. +Subclasses should override this method to provide specific visualization. -Individual message - may be incomplete for multi-action batches +## class LLMConvertibleEvent -### class AgentErrorEvent +Bases: `Event`, `ABC` -Bases: [`ObservationBaseEvent`](#class-observationbaseevent) +Base class for events that can be converted to LLM messages. -Error triggered by the agent. +### Methods -Note: This event should not contain model “thought” or “reasoning_content”. It -represents an error produced by the agent/scaffold, not model output. +**abstractmethod to_llm_message() -> Message** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/base.py#L61-L63) -#### Properties +**events_to_messages() -> list[Message]** -- `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. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/base.py#L90-L126) -#### Methods +Convert event stream to LLM message stream, handling multi-action batches -#### to_llm_message() -### class Condensation + + None + + +## 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() -> 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. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/condenser.py#L83-L96) -#### Methods +Applies the condensation to a list of events. -#### action +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. -The action type, namely ActionType.CONDENSATION_REQUEST. -* Type: - str + + None + + +## class CondensationRequest -### class CondensationSummaryEvent +Bases: `Event` -Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent) +This action is used to request a condensation of the conversation history. -This event represents a summary generated by a condenser. +### Properties + +- `source`: SourceType +- `visualize`: Text +## class CondensationSummaryEvent + +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 +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/condenser.py#L128-L132) -Bases: [`Event`](#class-event) +## class ConversationStateUpdateEvent + +Bases: `Event` Event that contains conversation state updates. @@ -135,54 +136,64 @@ 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()** -Create a state update event from a ConversationState object. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/conversation_state.py#L38-L48) -This creates an event containing a snapshot of important state fields. -* Parameters: - * `state` – The ConversationState to serialize - * `conversation_id` – The conversation ID for the event -* Returns: - A ConversationStateUpdateEvent with serialized state data + + None + + +**validate_value()** -#### classmethod validate_key() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/conversation_state.py#L50-L79) -#### classmethod validate_value() -### class Event + + None + + -Bases: `DiscriminatedUnionMixin`, `ABC` + + None + + +**from_conversation_state() -> ConversationStateUpdateEvent** -Base class for all events. +[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. +This creates an event containing a snapshot of important state fields. -#### Properties +**Parameters:** -- `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 +- `state` *ConversationState* – The ConversationState to serialize +- `conversation_id` – The conversation ID for the event + +**Returns:** + +- *ConversationStateUpdateEvent* A ConversationStateUpdateEvent with serialized state data -Bases: [`Event`](#class-event) + + + None + + +## class LLMCompletionLogEvent + +Bases: `Event` Event containing LLM completion log data. @@ -190,165 +201,219 @@ 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 + +- `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. +### Methods -#### Properties +**to_llm_message() -> Message** -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/llm_convertible/action.py#L140-L149) -#### Methods +Individual message - may be incomplete for multi-action batches -#### static events_to_messages() +## class AgentErrorEvent -Convert event stream to LLM message stream, handling multi-action batches +Bases: `ObservationBaseEvent` -#### abstractmethod to_llm_message() +Error triggered by the agent. -### class MessageEvent +Note: This event should not contain model "thought" or "reasoning_content". It +represents an error produced by the agent/scaffold, not model output. -Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent) +### Properties -Message from either agent or user. +- `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** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/llm_convertible/observation.py#L123-L131) + +## class MessageEvent -This is originally the “MessageAction”, but it suppose not to be tool call. +Bases: `LLMConvertibleEvent` +Message from either agent or user. + +This is originally the "MessageAction", but it suppose not to be tool call. -#### Properties +### 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() -> Message** -#### to_llm_message() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/llm_convertible/message.py#L116-L119) -### 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 +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/llm_convertible/observation.py#L45-L51) -Bases: [`Event`](#class-event) +## class SystemPromptEvent -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** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/llm_convertible/system.py#L47-L48) -#### Properties +## class UserRejectObservation -- `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] +Bases: `ObservationBaseEvent` + +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 +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/event/llm_convertible/observation.py#L86-L92) -Bases: [`Event`](#class-event) +## class TokenEvent -Event from VLLM representing token IDs used in LLM interaction. +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..e0783c13 100644 --- a/sdk/api-reference/openhands.sdk.llm.mdx +++ b/sdk/api-reference/openhands.sdk.llm.mdx @@ -3,28 +3,286 @@ title: openhands.sdk.llm description: API reference for openhands.sdk.llm module --- +## 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__()** -#### Methods +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/auth/credentials.py#L49-L57) -#### model_config = (configuration object) +Initialize the credential store. -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +**Parameters:** -#### to_llm_dict() +- `credentials_dir` *Path | None* – Optional custom directory for storing credentials. + Defaults to ~/.local/share/openhands/auth/ -Convert to LLM API format. -### class LLM + + 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. + +**Parameters:** + +- `vendor` *str* – The vendor/provider name (e.g., 'openai') + +**Returns:** + +- *OAuthCredentials | None* OAuthCredentials if found and valid, None otherwise + + + + 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. + +**Parameters:** + +- `credentials` *OAuthCredentials* – The OAuth credentials to save + + + + 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. + +**Parameters:** + +- `vendor` *str* – The vendor/provider name + +**Returns:** + +- *bool* True if credentials were deleted, False if they didn't exist + + + + 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. + +**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 + + + + None + + + + + None + + + + + None + + + + + None + + +## 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** + +[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 + +Handle OAuth authentication for OpenAI ChatGPT subscription access. + +### Properties + +- `vendor`: str + Get the vendor name. + +### Methods + +**__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. + +**Parameters:** + +- `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:** + +- *OAuthCredentials | None* Updated credentials, or None if no credentials exist. + +**Raises:** + +- `RuntimeError` – If token refresh fails. + +**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. + +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. + + + + 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() -> 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. + +**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. + + + + None + + + + + None + + + + + None + + + + + None + + +## class LLM Bases: `BaseModel`, `RetryMixin`, `NonNativeToolCallingMixin` @@ -35,475 +293,732 @@ 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. -#### completion() +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 + +**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** + +[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. 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:** + +- `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 + +**Returns:** + +- *LLMResponse* LLMResponse containing the model's response and metadata. + +**Raises:** + +- `ValueError` – If streaming is requested (not supported). + + + + 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. + +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 + + + + 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** -#### NOTE -Summary field is always added to tool schemas for transparency and -explainability of agent actions. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm.py#L1052-L1066) -* Raises: - `ValueError` – If streaming is requested (not supported). +Check if prompt caching is supported and enabled for current model. + +**Returns:** + +- *bool* True if prompt caching is supported and enabled for the given +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() +**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() + + + 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. - 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 -#### get_token_count() -#### is_caching_prompt_active() + + None + + +**get_token_count() -> int** -Check if prompt caching is supported and enabled for current model. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm.py#L1170-L1193) -* Returns: - True if prompt caching is supported and enabled for the given - : model. -* Return type: - boolean -#### classmethod load_from_env() + + None + + +**load_from_json() -> LLM** -#### classmethod load_from_json() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm.py#L1198-L1202) -#### model_post_init() -This function is meant to behave like a BaseModel method to initialise private attributes. + + None + + +**load_from_env() -> LLM** -It takes context as an argument since that’s what pydantic-core passes when calling it. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm.py#L1204-L1257) -* Parameters: - * `self` – The BaseModel instance. - * `context` – The context. -#### responses() + + None + + +**subscription_login() -> LLM** -Alternative invocation path using OpenAI Responses API via LiteLLM. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm.py#L1259-L1316) -Maps Message[] -> (instructions, input[]) and returns LLMResponse. +Authenticate with a subscription service and return an LLM instance. -* 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 +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. -#### NOTE -Summary field is always added to tool schemas for transparency and -explainability of agent actions. +Currently supported vendors: +- "openai": ChatGPT Plus/Pro subscription for Codex models -#### restore_metrics() +Supported OpenAI models: +- gpt-5.1-codex-max +- gpt-5.1-codex-mini +- gpt-5.2 +- gpt-5.2-codex -#### uses_responses_api() +**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. + +**Returns:** + +- *LLM* An LLM instance configured for subscription-based access. + +**Raises:** + +- `ValueError` – If the vendor or model is not supported. +- `RuntimeError` – If authentication fails. -Whether this model uses the OpenAI Responses API path. -#### vision_is_active() + + None + + -### class LLMRegistry + + None + + -Bases: `object` + + None + + + + + None + + + + + None + + +## 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__()** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm_registry.py#L33-L45) 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. -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. + + None + + +**subscribe() -> None** -#### get() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm_registry.py#L47-L53) -Get an LLM instance from the registry. +Subscribe to registry events. -* 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() +- `callback` *Callable[[RegistryEvent], None]* – Function to call when LLMs are created or updated. -List all registered usage IDs. -#### notify() + + 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. -* Parameters: - `event` – The registry event to notify about. +**Parameters:** -#### subscribe() +- `event` *RegistryEvent* – The registry event to notify about. -Subscribe to registry events. -* Parameters: - `callback` – Function to call when LLMs are created or updated. + + None + + +**add() -> None** -### class LLMResponse +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm_registry.py#L73-L95) -Bases: `BaseModel` +Add an LLM instance to the registry. -Result of an LLM completion request. +**Parameters:** -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. +- `llm` *LLM* – The LLM instance to register. +**Raises:** -#### Properties +- `ValueError` – If llm.usage_id already exists in the registry. -- `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 + + None + + +**get() -> LLM** -#### message +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/llm_registry.py#L97-L118) -The completion message converted to OpenHands Message type +Get an LLM instance from the registry. -* Type: - [openhands.sdk.llm.message.Message](#class-message) +**Parameters:** -#### metrics +- `usage_id` *str* – Unique identifier for the LLM usage slot. -Snapshot of metrics from the completion request +**Returns:** -* Type: - [openhands.sdk.llm.utils.metrics.MetricsSnapshot](#class-metricssnapshot) +- *LLM* The LLM instance. -#### raw_response +**Raises:** -The original LiteLLM response (ModelResponse or -ResponsesAPIResponse) for internal use +- `KeyError` – If usage_id is not found in the registry. -* Type: - litellm.types.utils.ModelResponse | litellm.types.llms.openai.ResponsesAPIResponse -### class Message + + None + + +**list_usage_ids() -> list[str]** -Bases: `BaseModel` +[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. -#### Properties +## class RegistryEvent -- `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)] -- `tool_call_id`: str | None -- `tool_calls`: list[[MessageToolCall](#class-messagetoolcall)] | None -- `vision_enabled`: bool +Bases: `BaseModel` -#### Methods +### Properties -#### classmethod from_llm_chat_message() +- `llm`: LLM +- `model_config`: ConfigDict -Convert a LiteLLMMessage (Chat Completions) to our Message class. +## class LLMResponse -Provider-agnostic mapping for reasoning: -- Prefer message.reasoning_content if present (LiteLLM normalized field) -- Extract thinking_blocks from content array (Anthropic-specific) +Bases: `BaseModel` -#### classmethod from_llm_responses_output() +Result of an LLM completion request. -Convert OpenAI Responses API output items into a single assistant Message. +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. -Policy (non-stream): -- Collect assistant text by concatenating output_text parts from message items -- Normalize function_call items to MessageToolCall list +### Properties -#### model_config = (configuration object) +- `message`: Message +- `metrics`: MetricsSnapshot +- `raw_response`: ModelResponse | ResponsesAPIResponse +- `model_config`: ConfigDict +- `id`: str + Get the response ID from the underlying LLM response. -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +This property provides a clean interface to access the response ID, +supporting both completion mode (ModelResponse) and response API modes +(ResponsesAPIResponse). -#### to_chat_dict() +Returns: + The response ID from the LLM response -Serialize message for OpenAI Chat Completions. +## class ImageContent -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) +Bases: `BaseContent` -#### to_responses_dict() +### Properties -Serialize message for OpenAI Responses (input parameter). +- `type`: Literal['image'] +- `image_urls`: list[str] -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 +### Methods -#### to_responses_value() +**to_llm_dict() -> list[dict[str, str | dict[str, str]]]** -Return serialized form. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L189-L196) -Either an instructions string (for system) or input items (for other roles). +Convert to LLM API format. -### class MessageToolCall +## class Message Bases: `BaseModel` -Transport-agnostic tool call representation. +### Properties -One canonical id is used for linking across actions/observations and -for Responses function_call_output call_id. +- `role`: Literal['user', 'system', 'assistant', 'tool'] +- `content`: Sequence[TextContent | ImageContent] +- `tool_calls`: list[MessageToolCall] | None +- `tool_call_id`: str | None +- `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 +### Methods -#### Properties +**to_chat_dict() -> dict[str, Any]** -- `arguments`: str -- `id`: str -- `name`: str -- `origin`: Literal['completion', 'responses'] -- `costs`: list[Cost] -- `response_latencies`: list[ResponseLatency] -- `token_usages`: list[TokenUsage] +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L281-L329) -#### Methods +Serialize message for OpenAI Chat Completions. -#### classmethod from_chat_tool_call() +**Parameters:** -Create a MessageToolCall from a Chat Completions tool call. +- `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. -#### classmethod from_responses_function_call() +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) -Create a MessageToolCall from a typed OpenAI Responses function_call item. -Note: OpenAI Responses function_call.arguments is already a JSON string. + + None + + -#### model_config = (configuration object) + + None + + -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + + None + + -#### to_chat_dict() + + None + + -Serialize to OpenAI Chat Completions tool_calls format. + + 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). + + + + 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) -#### to_responses_dict() +Serialize message for OpenAI Responses (input parameter). -Serialize to OpenAI Responses ‘function_call’ input item format. +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 -#### add_cost() -#### add_response_latency() + + None + + +**from_llm_chat_message() -> Message** -#### add_token_usage() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L590-L647) -Add a single usage record. +Convert a LiteLLMMessage (Chat Completions) to our Message class. + +Provider-agnostic mapping for reasoning: +- Prefer `message.reasoning_content` if present (LiteLLM normalized field) +- Extract `thinking_blocks` from content array (Anthropic-specific) -#### deep_copy() -Create a deep copy of the Metrics object. + + None + + +**from_llm_responses_output() -> Message** -#### diff() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L649-L705) -Calculate the difference between current metrics and a baseline. +Convert OpenAI Responses API output items into a single assistant Message. -This is useful for tracking metrics for specific operations like delegates. +Policy (non-stream): +- Collect assistant text by concatenating output_text parts from message items +- Normalize function_call items to MessageToolCall list -* Parameters: - `baseline` – A metrics object representing the baseline state -* Returns: - A new Metrics object containing only the differences since the baseline -#### get() + + None + + +## class MessageToolCall -Return the metrics in a dictionary. +Bases: `BaseModel` -#### get_snapshot() +Transport-agnostic tool call representation. -Get a snapshot of the current metrics without the detailed lists. +One canonical id is used for linking across actions/observations and +for Responses function_call_output call_id. -#### initialize_accumulated_token_usage() +### Properties -#### log() +- `id`: str + Canonical tool call id +- `name`: str + Tool/function name +- `arguments`: str + JSON string of arguments +- `origin`: Literal['completion', 'responses'] + Originating API family -Log the metrics. +### Methods -#### merge() +**from_chat_tool_call() -> MessageToolCall** -Merge ‘other’ metrics into this one. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L38-L58) -#### model_config = (configuration object) +Create a MessageToolCall from a Chat Completions tool call. -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. -#### classmethod validate_accumulated_cost() + + None + + +**from_responses_function_call() -> MessageToolCall** -### class MetricsSnapshot +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L60-L82) -Bases: `BaseModel` +Create a MessageToolCall from a typed OpenAI Responses function_call item. -A snapshot of metrics at a point in time. +Note: OpenAI Responses function_call.arguments is already a JSON string. -Does not include lists of individual costs, latencies, or token usages. + + None + + +**to_chat_dict() -> dict[str, Any]** -#### Properties +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L84-L93) -- `accumulated_cost`: float -- `accumulated_token_usage`: TokenUsage | None -- `max_budget_per_task`: float | None -- `model_name`: str +Serialize to OpenAI Chat Completions tool_calls format. -#### Methods +**to_responses_dict() -> dict[str, Any]** -#### model_config = (configuration object) +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/message.py#L95-L111) -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +Serialize to OpenAI Responses 'function_call' input item format. -### class ReasoningItemModel +## class ReasoningItemModel Bases: `BaseModel` @@ -511,22 +1026,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 +1043,64 @@ 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 -#### Methods +## class TextContent -#### model_config = (configuration object) +Bases: `BaseContent` -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +### Properties -### class RegistryEvent +- `type`: Literal['text'] +- `text`: str +- `model_config`: ConfigDict + +### Methods + +**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 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 +- `type`: Literal['thinking'] +- `thinking`: str + The thinking content +- `signature`: str | None + Cryptographic signature for the thinking block + +**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. -- `llm`: [LLM](#class-llm) -- `model_config`: ClassVar[ConfigDict] = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. -### class RouterLLM -Bases: [`LLM`](#class-llm) + + None + + +## 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,102 +1111,103 @@ 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 - -#### completion() - -This method intercepts completion calls and routes them to the appropriate -underlying LLM based on the routing logic implemented in select_llm(). +### Methods -* 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 +**validate_llms_not_empty()** -#### NOTE -Summary field is always added to tool schemas for transparency and -explainability of agent actions. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/router/base.py#L41-L48) -#### model_post_init() -This function is meant to behave like a BaseModel method to initialise private attributes. + + None + + +**completion() -> LLMResponse** -It takes context as an argument since that’s what pydantic-core passes when calling it. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/router/base.py#L50-L89) -* Parameters: - * `self` – The BaseModel instance. - * `context` – The context. - -#### abstractmethod select_llm() - -Select which LLM to use based on messages and events. - -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. +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 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. +**Parameters:** -#### classmethod set_placeholder_model() +- `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 -Guarantee model exists before LLM base validation runs. -#### classmethod validate_llms_not_empty() + + None + + -### class TextContent + + None + + -Bases: `BaseContent` + + None + + + + None + + -#### Properties + + None + + -- `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'] + + None + + +**abstractmethod select_llm() -> str** -#### Methods +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/router/base.py#L91-L105) -#### to_llm_dict() +Select which LLM to use based on messages and events. -Convert to LLM API format. +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. -### class ThinkingBlock +**Parameters:** -Bases: `BaseModel` +- `messages` *list[Message]* – List of messages in the conversation that can be used + to inform the routing decision. -Anthropic thinking block for extended thinking feature. +**Returns:** -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. +- *str* The key/name of the LLM to use from llms_for_routing dictionary. -#### Properties + + None + + +**set_placeholder_model()** -- `signature`: str | None -- `thinking`: str -- `type`: Literal['thinking'] +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/llm/router/base.py#L117-L129) -#### Methods +Guarantee `model` exists before LLM base validation runs. -#### model_config = (configuration object) -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + + 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 41fdd321..37dbe7c3 100644 --- a/sdk/api-reference/openhands.sdk.security.mdx +++ b/sdk/api-reference/openhands.sdk.security.mdx @@ -3,8 +3,7 @@ title: openhands.sdk.security description: API reference for openhands.sdk.security module --- - -### class SecurityRisk +## class SecurityRisk Bases: `str`, `Enum` @@ -13,29 +12,28 @@ 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' +### Methods -#### LOW = 'LOW' +**get_color() -> str** -#### MEDIUM = 'MEDIUM' - -#### UNKNOWN = 'UNKNOWN' - -#### get_color() +[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() +**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. @@ -45,18 +43,32 @@ 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:** + +- `other` *SecurityRisk* – The other risk level to compare against. +- `reflexive` *bool* – Whether the relationship is reflexive. + +**Raises:** + +- `ValueError` – If either risk level is UNKNOWN. - # 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. + + None + + -* 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. + + 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 62b85a29..33c44cce 100644 --- a/sdk/api-reference/openhands.sdk.tool.mdx +++ b/sdk/api-reference/openhands.sdk.tool.mdx @@ -3,137 +3,187 @@ title: openhands.sdk.tool description: API reference for openhands.sdk.tool module --- +## 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() -> Sequence[Self]** -#### Properties +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/builtins/finish.py#L72-L106) -- `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 +Create FinishTool instance. -Bases: `Protocol` +**Parameters:** -Protocol for tools that are guaranteed to have a non-None executor. +- `conv_state` *ConversationState | None* – Optional conversation state (not used by FinishTool). +- `**params` – Additional parameters (none supported). -This eliminates the need for runtime None checks and type narrowing -when working with tools that are known to be executable. +**Returns:** +- *Sequence[Self]* A sequence containing a single FinishTool instance. -#### Properties +**Raises:** -- `executor`: [ToolExecutor](#class-toolexecutor)[Any, Any] -- `name`: str +- `ValueError` – If any parameters are provided. -#### Methods -#### __init__() + + None + + -### class FinishTool + + None + + +## class ThinkTool -Bases: `ToolDefinition[FinishAction, FinishObservation]` +Bases: `ToolDefinition[ThinkAction, ThinkObservation]` -Tool for signaling the completion of a task or conversation. +Tool for logging thoughts without making changes. +### Methods -#### Properties +**create() -> Sequence[Self]** -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/builtins/think.py#L84-L117) -#### Methods +Create ThinkTool instance. -#### classmethod create() +**Parameters:** -Create FinishTool instance. +- `conv_state` *ConversationState | None* – Optional conversation state (not used by ThinkTool). +- `**params` – Additional parameters (none supported). + +**Returns:** + +- *Sequence[Self]* A sequence containing a single ThinkTool instance. + +**Raises:** + +- `ValueError` – If any parameters are provided. + + + + None + + + + + None + + +**list_registered_tools() -> list[str]** + +[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]** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/registry.py#L159-L168) -* 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. -#### name = 'finish' + + None + + -### class Observation + + None + + +## 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. + +## class Observation +Bases: `Schema`, `ABC` + +Base schema for output observation. -#### Properties +### Properties -- `ERROR_MESSAGE_HEADER`: ClassVar[str] = '[An error occurred during execution.]n' +- `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. +Subclasses can override for custom visualization; by default we show the +same text that would be sent to the LLM. -### class ThinkTool +### Methods -Bases: `ToolDefinition[ThinkAction, ThinkObservation]` +**from_text() -> Self** -Tool for logging thoughts without making changes. +[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. -#### Properties +**Parameters:** -- `model_config`: = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +- `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. -#### Methods +**Returns:** -#### classmethod create() +- *Self* An Observation instance with the text wrapped in a TextContent. -Create ThinkTool instance. -* 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. + + None + + -#### name = 'think' + + None + + -### class Tool + + None + + +## class Tool Bases: `BaseModel` @@ -141,48 +191,77 @@ 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) +### Methods -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +**validate_name() -> str** -#### classmethod validate_name() +[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. -#### classmethod validate_params() + + + 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. -### class ToolAnnotations + + + None + + +## 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 +275,226 @@ 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() -> Sequence[Self]** -Create an action from parsed arguments. +[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. + +This method must be implemented by all subclasses to provide custom +initialization logic, typically initializing the executor with parameters +from conv_state and other optional parameters. + +**Parameters:** + +- `*args` – Variable positional arguments (typically conv_state as first arg). +- `**kwargs` – Optional parameters for tool initialization. + +**Returns:** + +- *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. -This method can be overridden by subclasses to provide custom logic -for creating actions from arguments (e.g., for MCP tools). -* Parameters: - `arguments` – The parsed arguments from the tool call. -* Returns: - The action instance created from the arguments. + + None + + -#### as_executable() + + 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 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() -> Action** -#### classmethod resolve_kind() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/tool.py#L283-L295) -Resolve a kind string to its corresponding tool class. +Create an action from parsed arguments. -* 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 +This method can be overridden by subclasses to provide custom logic +for creating actions from arguments (e.g., for MCP tools). -#### set_executor() +**Parameters:** -Create a new Tool instance with the given executor. +- `arguments` *dict[str, Any]* – The parsed arguments from the tool call. + +**Returns:** -#### to_mcp_tool() +- *Action* The action instance created from the arguments. + + + + 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. 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() + + + 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. -* 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:** + +- `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. -#### NOTE -Summary field is always added to the schema for transparency and -explainability of agent actions. -#### to_responses_tool() + + 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). 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 + + + + None + + + + + None + + +**resolve_kind() -> type** + +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/tool/tool.py#L443-L476) -* Parameters: - * `add_security_risk_prediction` – Whether to add a security_risk field - * `action_type` – Optional override for the action type +Resolve a kind string to its corresponding tool class. + +**Parameters:** + +- `kind` *str* – The name of the tool class to resolve + +**Returns:** -#### NOTE -Summary field is always added to the schema for transparency and -explainability of agent actions. +- *type* The tool class corresponding to the kind -### class ToolExecutor +**Raises:** -Bases: `ABC`, `Generic` +- `ValueError` – If the kind is unknown + + + + None + + +## class ToolExecutor + +Bases: `ABC` Executor function type for a Tool. -#### Methods +### Methods + +**close() -> None** -#### close() +[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. diff --git a/sdk/api-reference/openhands.sdk.utils.mdx b/sdk/api-reference/openhands.sdk.utils.mdx index 45ba17dc..049c37be 100644 --- a/sdk/api-reference/openhands.sdk.utils.mdx +++ b/sdk/api-reference/openhands.sdk.utils.mdx @@ -3,36 +3,96 @@ title: openhands.sdk.utils description: API reference for openhands.sdk.utils module --- +**sanitized_env() -> dict[str, str]** -Utility functions for the OpenHands SDK. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/utils/command.py#L14-L36) -### deprecated() +Return a copy of *env* with sanitized values. + +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. + + + + 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. 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. + + 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. + + 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. + +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. + + + + None + + + + + None + + + + + None + + + + + None + + + + + None + + -* 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 + + None + + +**sanitize_openhands_mentions() -> str** -### sanitize_openhands_mentions() +[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. @@ -40,27 +100,74 @@ 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:** -### Examples +- `text` *str* – The text to sanitize -```pycon +**Returns:** + +- *str* Text with sanitized @OpenHands mentions (e.g., "@OpenHands" -> "@‍OpenHands") + +**Example:** + +```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() -Emit a deprecation warning for dynamic access to a legacy feature. + + None + + +**maybe_truncate() -> str** -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. +[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. + +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 + + + + 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 48066655..a9655870 100644 --- a/sdk/api-reference/openhands.sdk.workspace.mdx +++ b/sdk/api-reference/openhands.sdk.workspace.mdx @@ -3,8 +3,7 @@ title: openhands.sdk.workspace description: API reference for openhands.sdk.workspace module --- - -### class BaseWorkspace +## class BaseWorkspace Bases: `DiscriminatedUnionMixin`, `ABC` @@ -14,281 +13,400 @@ 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 +### Properties -```pycon ->>> with workspace: -... result = workspace.execute_command("echo 'hello'") -... content = workspace.read_file("example.txt") -``` +- `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.')] +### Methods -#### Properties +**abstractmethod execute_command() -> CommandResult** -- `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.')] +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/base.py#L69-L90) -#### 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:** + +- `Exception` – If command execution fails -* 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 -#### abstractmethod file_upload() + + 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. -* 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:** + +- `source_path` *str | Path* – Path to the source file +- `destination_path` *str | Path* – Path where the file should be uploaded + +**Returns:** + +- *FileOperationResult* Result containing success status and metadata + +**Raises:** + +- `Exception` – If file upload fails + + + + 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. + +**Parameters:** -#### abstractmethod git_changes() +- `source_path` *str | Path* – Path to the source file on the system +- `destination_path` *str | Path* – Path where the file should be downloaded + +**Returns:** + +- *FileOperationResult* Result containing success status and metadata + +**Raises:** + +- `Exception` – If file download fails + + + + 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. -* 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:** -#### abstractmethod git_diff() +- *list[GitChange]* list[GitChange]: List of changes + +**Raises:** + +- `Exception` – If path is not a git repository or getting changes failed + + + + 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. -* 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:** + +- *GitDiff* Git diff + +**Raises:** + +- `Exception` – If path is not a git repository or getting diff failed -#### model_config = (configuration object) -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + + None + + +**pause() -> None** -#### pause() +[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. For container-based workspaces, this pauses the container. -* Raises: - `NotImplementedError` – If the workspace type does not support pausing. +**Raises:** -#### resume() +- `NotImplementedError` – If the workspace type does not support pausing. + +**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. For container-based workspaces, this resumes the container. -* Raises: - `NotImplementedError` – If the workspace type does not support resuming. +**Raises:** -### class CommandResult +- `NotImplementedError` – If the workspace type does not support resuming. -Bases: `BaseModel` - -Result of executing a command in the workspace. +## class LocalWorkspace +Bases: `BaseWorkspace` -#### Properties +Local workspace implementation that operates on the host filesystem. -- `command`: str -- `exit_code`: int -- `stderr`: str -- `stdout`: str -- `timeout_occurred`: bool +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. -#### Methods +### Methods -#### model_config = (configuration object) +**__init__()** -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/local.py#L31-L34) -### class FileOperationResult -Bases: `BaseModel` + + None + + -Result of a file upload or download operation. + + None + + +**execute_command() -> CommandResult** +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/local.py#L36-L69) -#### Properties +Execute a bash command locally. -- `destination_path`: str -- `error`: str | None -- `file_size`: int | None -- `source_path`: str -- `success`: bool +Uses the shared shell execution utility to run commands with proper +timeout handling, output streaming, and error management. -#### Methods +**Parameters:** -#### model_config = (configuration object) +- `command` *str* – The bash command to execute +- `cwd` *str | Path | None* – Working directory (optional) +- `timeout` *float* – Timeout in seconds -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +**Returns:** -### class LocalWorkspace +- *CommandResult* Result with stdout, stderr, exit_code, command, and +timeout_occurred -Bases: [`BaseWorkspace`](#class-baseworkspace) -Local workspace implementation that operates on the host filesystem. + + None + + -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. + + None + + -#### Example + + None + + +**file_upload() -> FileOperationResult** -```pycon ->>> workspace = LocalWorkspace(working_dir="/path/to/project") ->>> with workspace: -... result = workspace.execute_command("ls -la") -... content = workspace.read_file("README.md") -``` +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/local.py#L71-L114) -#### Methods +Upload (copy) a file locally. -#### __init__() +For local systems, file upload is implemented as a file copy operation +using shutil.copy2 to preserve metadata. -Create a new model by parsing and validating input data from keyword arguments. +**Parameters:** -Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model. +- `source_path` *str | Path* – Path to the source file +- `destination_path` *str | Path* – Path where the file should be copied -self is explicitly positional-only to allow self as a field name. +**Returns:** -#### execute_command() +- *FileOperationResult* Result with success status and file information -Execute a bash command locally. -Uses the shared shell execution utility to run commands with proper -timeout handling, output streaming, and error management. + + None + + -* 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) + + None + + +**file_download() -> FileOperationResult** -#### file_download() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/local.py#L116-L159) 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` – 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:** -#### file_upload() +- `source_path` *str | Path* – Path to the source file +- `destination_path` *str | Path* – Path where the file should be copied -Upload (copy) a file locally. +**Returns:** -For local systems, file upload is implemented as a file copy operation -using shutil.copy2 to preserve metadata. +- *FileOperationResult* Result with success status and file information -* 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) -#### git_changes() + + 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. -* 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 + + + + 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. -* 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 -#### model_config = (configuration object) +**Returns:** -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +- *GitDiff* Git diff -#### pause() +**Raises:** + +- `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 on the host filesystem. -#### resume() +**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 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 + +## 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`](#class-baseworkspace) +Bases: `RemoteWorkspaceMixin`, `BaseWorkspace` Remote workspace implementation that connects to an OpenHands agent server. @@ -296,123 +414,160 @@ 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 -#### execute_command() +**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() -> 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. 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. +**Returns:** -Requests the file from the remote system via HTTP API and saves it locally. +- *CommandResult* Result with stdout, stderr, exit_code, and other metadata + + + + None + + -* 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) + + None + + -#### file_upload() + + 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. 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 + -#### git_changes() + + 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. + +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 + + + + 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. -* 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() -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. + + None + + +**git_diff() -> GitDiff** -#### reset_client() +[source](https://github.com/OpenHands/software-agent-sdk/blob/30e22095607254194ab1f4d8859f658abe5628a1/openhands-sdk/openhands/sdk/workspace/remote/base.py#L156-L170) -Reset the HTTP client to force re-initialization. +Get the git diff for the file at the path given. -This is useful when connection parameters (host, api_key) have changed -and the client needs to be recreated with new values. +**Parameters:** -### class Workspace +- `path` *str | Path* – Path to the file -### class Workspace +**Returns:** -Bases: `object` +- *GitDiff* Git diff -Factory entrypoint that returns a LocalWorkspace or RemoteWorkspace. +**Raises:** -Usage: -: - Workspace(working_dir=…) -> LocalWorkspace - - Workspace(working_dir=…, host=”http://…”) -> RemoteWorkspace +- `Exception` – If path is not a git repository or getting diff failed + + + + None + + +## class Workspace + +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" }, +]