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