From 963b334d6b34aaab349482602a002cc9f7405066 Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 15 Mar 2026 18:51:05 +0000 Subject: [PATCH 1/8] Extend API doc generation to include 12 additional SDK modules Added documentation for the following new SDK modules: - context: AgentContext, Skills, SkillKnowledge, Triggers - hooks: Event-driven hooks for automation and control - critic: Critics for iterative refinement - mcp: MCP (Model Context Protocol) integration - plugin: Plugin system and marketplace - subagent: Sub-agent delegation - io: File storage abstractions (FileStore, LocalFileStore, etc.) - testing: Test utilities (TestLLM) - secret: Secret management (SecretSource, StaticSecret, LookupSecret) - skills: Skill management utilities - observability: Observability utilities - logger: Logging utilities Changes: - Updated scripts/generate-api-docs.py to include 12 new modules - Expanded class_to_module mapping for cross-reference resolution - Regenerated all API reference documentation - Auto-updated docs.json navigation with new pages Co-authored-by: openhands --- docs.json | 20 +- scripts/generate-api-docs.py | 119 +++- scripts/mint-config-snippet.json | 12 + sdk/api-reference/openhands.sdk.agent.mdx | 29 + sdk/api-reference/openhands.sdk.context.mdx | 287 ++++++++ .../openhands.sdk.conversation.mdx | 78 ++- sdk/api-reference/openhands.sdk.critic.mdx | 220 +++++++ sdk/api-reference/openhands.sdk.event.mdx | 84 ++- sdk/api-reference/openhands.sdk.hooks.mdx | 349 ++++++++++ sdk/api-reference/openhands.sdk.io.mdx | 220 +++++++ sdk/api-reference/openhands.sdk.llm.mdx | 78 ++- sdk/api-reference/openhands.sdk.logger.mdx | 39 ++ sdk/api-reference/openhands.sdk.mcp.mdx | 232 +++++++ .../openhands.sdk.observability.mdx | 22 + sdk/api-reference/openhands.sdk.plugin.mdx | 611 ++++++++++++++++++ sdk/api-reference/openhands.sdk.secret.mdx | 73 +++ sdk/api-reference/openhands.sdk.skills.mdx | 68 ++ sdk/api-reference/openhands.sdk.subagent.mdx | 74 +++ sdk/api-reference/openhands.sdk.testing.mdx | 130 ++++ sdk/api-reference/openhands.sdk.utils.mdx | 28 + sdk/api-reference/openhands.sdk.workspace.mdx | 113 ++++ 21 files changed, 2847 insertions(+), 39 deletions(-) create mode 100644 sdk/api-reference/openhands.sdk.context.mdx create mode 100644 sdk/api-reference/openhands.sdk.critic.mdx create mode 100644 sdk/api-reference/openhands.sdk.hooks.mdx create mode 100644 sdk/api-reference/openhands.sdk.io.mdx create mode 100644 sdk/api-reference/openhands.sdk.logger.mdx create mode 100644 sdk/api-reference/openhands.sdk.mcp.mdx create mode 100644 sdk/api-reference/openhands.sdk.observability.mdx create mode 100644 sdk/api-reference/openhands.sdk.plugin.mdx create mode 100644 sdk/api-reference/openhands.sdk.secret.mdx create mode 100644 sdk/api-reference/openhands.sdk.skills.mdx create mode 100644 sdk/api-reference/openhands.sdk.subagent.mdx create mode 100644 sdk/api-reference/openhands.sdk.testing.mdx diff --git a/docs.json b/docs.json index ac98e277b..567c5078a 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" ] } ] @@ -386,10 +386,22 @@ "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.testing", "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 c21108402..c29432d11 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', 'testing', '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 @@ -570,37 +570,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 +631,69 @@ 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', + # testing module + 'TestLLM': 'testing', + 'TestLLMExhaustedError': 'testing', + # 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 diff --git a/scripts/mint-config-snippet.json b/scripts/mint-config-snippet.json index 74571d27e..04283e2f6 100644 --- a/scripts/mint-config-snippet.json +++ b/scripts/mint-config-snippet.json @@ -4,10 +4,22 @@ "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.testing", "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 bac75f6d1..e53e0c226 100644 --- a/sdk/api-reference/openhands.sdk.agent.mdx +++ b/sdk/api-reference/openhands.sdk.agent.mdx @@ -32,6 +32,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. @@ -133,6 +146,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. diff --git a/sdk/api-reference/openhands.sdk.context.mdx b/sdk/api-reference/openhands.sdk.context.mdx new file mode 100644 index 000000000..b6c80b516 --- /dev/null +++ b/sdk/api-reference/openhands.sdk.context.mdx @@ -0,0 +1,287 @@ +--- +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]. diff --git a/sdk/api-reference/openhands.sdk.conversation.mdx b/sdk/api-reference/openhands.sdk.conversation.mdx index b5e99911f..974bf3d6e 100644 --- a/sdk/api-reference/openhands.sdk.conversation.mdx +++ b/sdk/api-reference/openhands.sdk.conversation.mdx @@ -245,7 +245,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 +317,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 @@ -679,6 +685,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,9 +750,29 @@ 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. @@ -756,7 +814,8 @@ 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: @@ -899,7 +958,7 @@ even when callable secrets fail on subsequent calls. #### Properties -- `secret_sources`: dict[str, SecretSource] +- `secret_sources`: dictstr, [SecretSource] #### Methods @@ -912,6 +971,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. diff --git a/sdk/api-reference/openhands.sdk.critic.mdx b/sdk/api-reference/openhands.sdk.critic.mdx new file mode 100644 index 000000000..9cd1dc30e --- /dev/null +++ b/sdk/api-reference/openhands.sdk.critic.mdx @@ -0,0 +1,220 @@ +--- +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 + +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]. diff --git a/sdk/api-reference/openhands.sdk.event.mdx b/sdk/api-reference/openhands.sdk.event.mdx index cc63ecda2..f3c5eb193 100644 --- a/sdk/api-reference/openhands.sdk.event.mdx +++ b/sdk/api-reference/openhands.sdk.event.mdx @@ -4,6 +4,34 @@ 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 +47,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] @@ -50,7 +78,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. @@ -162,7 +190,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 @@ -195,12 +223,48 @@ 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,7 +283,7 @@ 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 @@ -261,7 +325,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 @@ -284,7 +348,7 @@ 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 @@ -316,7 +380,7 @@ 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 @@ -338,7 +402,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 @@ -391,7 +455,7 @@ 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 000000000..9c0b2c336 --- /dev/null +++ b/sdk/api-reference/openhands.sdk.hooks.mdx @@ -0,0 +1,349 @@ +--- +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 (recommended): + 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 000000000..a3183ee87 --- /dev/null +++ b/sdk/api-reference/openhands.sdk.io.mdx @@ -0,0 +1,220 @@ +--- +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 4e6f8a90f..2405f5010 100644 --- a/sdk/api-reference/openhands.sdk.llm.mdx +++ b/sdk/api-reference/openhands.sdk.llm.mdx @@ -63,6 +63,54 @@ 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` @@ -124,6 +172,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 @@ -594,7 +643,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 @@ -679,7 +728,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 @@ -954,3 +1003,28 @@ 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]. diff --git a/sdk/api-reference/openhands.sdk.logger.mdx b/sdk/api-reference/openhands.sdk.logger.mdx new file mode 100644 index 000000000..f6dffa1ed --- /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 000000000..bd3d25dfb --- /dev/null +++ b/sdk/api-reference/openhands.sdk.mcp.mdx @@ -0,0 +1,232 @@ +--- +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 000000000..867ebbc34 --- /dev/null +++ b/sdk/api-reference/openhands.sdk.observability.mdx @@ -0,0 +1,22 @@ +--- +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: +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 000000000..ef859e259 --- /dev/null +++ b/sdk/api-reference/openhands.sdk.plugin.mdx @@ -0,0 +1,611 @@ +--- +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. + +Example: + +``` +`` +``` + +``` +` +``` + +json +{ + + “name”: “company-tools”, + “owner”: (configuration object), + “plugins”: [ + + > (configuration object) + + ], + “skills”: [ + + > (configuration object) + + ] + +## } + + +#### 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 000000000..9322e63e6 --- /dev/null +++ b/sdk/api-reference/openhands.sdk.secret.mdx @@ -0,0 +1,73 @@ +--- +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]. diff --git a/sdk/api-reference/openhands.sdk.skills.mdx b/sdk/api-reference/openhands.sdk.skills.mdx new file mode 100644 index 000000000..de8466099 --- /dev/null +++ b/sdk/api-reference/openhands.sdk.skills.mdx @@ -0,0 +1,68 @@ +--- +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 000000000..b972ef35f --- /dev/null +++ b/sdk/api-reference/openhands.sdk.subagent.mdx @@ -0,0 +1,74 @@ +--- +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.testing.mdx b/sdk/api-reference/openhands.sdk.testing.mdx new file mode 100644 index 000000000..166569a7a --- /dev/null +++ b/sdk/api-reference/openhands.sdk.testing.mdx @@ -0,0 +1,130 @@ +--- +title: openhands.sdk.testing +description: API reference for openhands.sdk.testing module +--- + + +Testing utilities for OpenHands SDK. + +This module provides test utilities that make it easy to write tests for +code that uses the OpenHands SDK, without needing to mock LiteLLM internals. + +### class TestLLM + +Bases: `LLM` + +A mock LLM for testing that returns scripted responses. + +TestLLM is a real LLM subclass that can be used anywhere an LLM is accepted: +in Agent(llm=…), in fallback_llms, in condensers, in routers, etc. + +Key features: +- No patching needed: just pass TestLLM as the llm= argument +- Tests speak in SDK types (Message, TextContent, MessageToolCall) +- Clear error when responses are exhausted +- Zero-cost metrics by default +- Always uses completion() path (uses_responses_api returns False) + +#### Example + +```pycon +>>> from openhands.sdk.testing import TestLLM +>>> from openhands.sdk.llm import Message, TextContent, MessageToolCall +>>> +>>> # Simple text response +>>> llm = TestLLM.from_messages([ +... Message(role="assistant", content=[TextContent(text="Done!")]), +... ]) +>>> +>>> # Response with tool calls +>>> llm = TestLLM.from_messages([ +... Message( +... role="assistant", +... content=[TextContent(text="")], +... tool_calls=[ +... MessageToolCall( +... id="call_1", +... name="my_tool", +... arguments='(configuration object)', +... origin="completion", +... ) +... ], +... ), +... Message(role="assistant", content=[TextContent(text="Done!")]), +... ]) +``` + + +#### Properties + +- `call_count`: int + Return the number of calls made to this TestLLM. +- `model`: str +- `model_config`: ClassVar[ConfigDict] = (configuration object) + Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +- `remaining_responses`: int + Return the number of remaining scripted responses. + +#### Methods + +#### __init__() + +Create a new model by parsing and validating input data from keyword arguments. + +Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be +validated to form a valid model. + +self is explicitly positional-only to allow self as a field name. + +#### completion() + +Return the next scripted response. + +* Parameters: + * `messages` – Input messages (ignored, but required for API compatibility) + * `tools` – Available tools (ignored) + * `_return_metrics` – Whether to return metrics (ignored) + * `add_security_risk_prediction` – Add security risk field (ignored) + * `on_token` – Streaming callback (ignored) + kwargs* – Additional arguments (ignored) +* Returns: + LLMResponse containing the next scripted message. +* Raises: + * [TestLLMExhaustedError](#class-testllmexhaustederror) – When no more scripted responses are available. + * `Exception` – Any scripted exception placed in the response queue. + +#### classmethod from_messages() + +Create a TestLLM with scripted responses and/or errors. + +* Parameters: + * `messages` – List of Message or Exception objects to return in order. + Each call to completion() or responses() consumes the next + item: Message objects are returned normally, Exception objects + are raised (like unittest.mock side_effect). + * `model` – Model name (default: “test-model”) + * `usage_id` – Usage ID for metrics (default: “test-llm”) + kwargs* – Additional LLM configuration options +* Returns: + A TestLLM instance configured with the scripted responses. + +#### 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. + +#### responses() + +Return the next scripted response (delegates to completion). + +For TestLLM, both completion() and responses() return from the same +queue of scripted responses. + +#### uses_responses_api() + +TestLLM always uses the completion path. diff --git a/sdk/api-reference/openhands.sdk.utils.mdx b/sdk/api-reference/openhands.sdk.utils.mdx index 237164e09..0ffa30fe4 100644 --- a/sdk/api-reference/openhands.sdk.utils.mdx +++ b/sdk/api-reference/openhands.sdk.utils.mdx @@ -32,6 +32,34 @@ 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 + +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 480666553..c26ffd06f 100644 --- a/sdk/api-reference/openhands.sdk.workspace.mdx +++ b/sdk/api-reference/openhands.sdk.workspace.mdx @@ -4,6 +4,109 @@ 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` @@ -363,6 +466,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. From f08123b197d244e5c19ffc512549e79ed560f76f Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 15 Mar 2026 19:11:29 +0000 Subject: [PATCH 2/8] Fix broken link/parsing issues and remove testing module - Remove testing module from documentation (as requested) - Fix blockquote markers (> characters) that break MDX parsing - Fix malformed Example code blocks with proper wrapping - Remove
tags and standalone backtick patterns - Handle orphaned code block openers - Add module filtering to only process allowed modules - Clean up multiple levels of nested blockquotes Co-authored-by: openhands --- docs.json | 1 - scripts/generate-api-docs.py | 173 +++++++++++++++++- scripts/mint-config-snippet.json | 1 - sdk/api-reference/openhands.sdk.agent.mdx | 19 +- sdk/api-reference/openhands.sdk.context.mdx | 6 +- .../openhands.sdk.conversation.mdx | 23 ++- sdk/api-reference/openhands.sdk.critic.mdx | 11 +- sdk/api-reference/openhands.sdk.hooks.mdx | 25 ++- sdk/api-reference/openhands.sdk.llm.mdx | 11 +- sdk/api-reference/openhands.sdk.logger.mdx | 13 +- sdk/api-reference/openhands.sdk.mcp.mdx | 25 ++- sdk/api-reference/openhands.sdk.plugin.mdx | 48 ++--- sdk/api-reference/openhands.sdk.security.mdx | 21 +-- sdk/api-reference/openhands.sdk.subagent.mdx | 2 +- sdk/api-reference/openhands.sdk.testing.mdx | 130 ------------- sdk/api-reference/openhands.sdk.tool.mdx | 33 ++-- sdk/api-reference/openhands.sdk.utils.mdx | 17 +- sdk/api-reference/openhands.sdk.workspace.mdx | 19 +- 18 files changed, 291 insertions(+), 287 deletions(-) delete mode 100644 sdk/api-reference/openhands.sdk.testing.mdx diff --git a/docs.json b/docs.json index 567c5078a..245bf1052 100644 --- a/docs.json +++ b/docs.json @@ -401,7 +401,6 @@ "sdk/api-reference/openhands.sdk.security", "sdk/api-reference/openhands.sdk.skills", "sdk/api-reference/openhands.sdk.subagent", - "sdk/api-reference/openhands.sdk.testing", "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 c29432d11..2f06541ac 100755 --- a/scripts/generate-api-docs.py +++ b/scripts/generate-api-docs.py @@ -175,7 +175,7 @@ def create_rst_files(self): 'tool', 'workspace', 'security', 'utils', # Additional important modules 'context', 'hooks', 'critic', 'mcp', 'plugin', - 'subagent', 'io', 'testing', 'secret', 'skills', + 'subagent', 'io', 'secret', 'skills', 'observability', 'logger', ] @@ -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,6 +295,89 @@ 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('') + continue + + result_lines.append(line) + i += 1 + + return '\n'.join(result_lines) def fix_header_hierarchy(self, content: str) -> str: """Fix header hierarchy to ensure proper nesting under class headers.""" @@ -453,6 +554,68 @@ 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) + + # 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 + + # 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' + # Run multiple times to handle nested blockquotes + prev_content = None + while prev_content != content: + prev_content = content + 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 followed by malformed content + # Pattern: ``` followed by content that doesn't have a matching closing ``` + # This handles Sphinx's broken JSON/code examples + lines = content.split('\n') + cleaned = [] + i = 0 + while i < len(lines): + line = lines[i] + # Check for orphaned code block + if line.strip() == '```': + # Look ahead to see if there's proper code and closing + 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 + + cleaned.append(line) + i += 1 + content = '\n'.join(cleaned) + lines = content.split('\n') cleaned_lines = [] @@ -529,10 +692,7 @@ 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 # Remove escaped characters that cause issues line = line.replace('\\*', '*') @@ -682,9 +842,6 @@ def remove_problematic_patterns(self, line: str) -> str: 'FileStore': 'io', 'LocalFileStore': 'io', 'InMemoryFileStore': 'io', - # testing module - 'TestLLM': 'testing', - 'TestLLMExhaustedError': 'testing', # secret module 'SecretSource': 'secret', 'StaticSecret': 'secret', diff --git a/scripts/mint-config-snippet.json b/scripts/mint-config-snippet.json index 04283e2f6..20e89ae5c 100644 --- a/scripts/mint-config-snippet.json +++ b/scripts/mint-config-snippet.json @@ -19,7 +19,6 @@ "sdk/api-reference/openhands.sdk.security", "sdk/api-reference/openhands.sdk.skills", "sdk/api-reference/openhands.sdk.subagent", - "sdk/api-reference/openhands.sdk.testing", "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 e53e0c226..6db11cfea 100644 --- a/sdk/api-reference/openhands.sdk.agent.mdx +++ b/sdk/api-reference/openhands.sdk.agent.mdx @@ -17,12 +17,11 @@ is provided by CriticMixin. #### Example -```pycon ->>> from openhands.sdk import LLM, Agent, Tool ->>> llm = LLM(model="claude-sonnet-4-20250514", api_key=SecretStr("key")) ->>> tools = [Tool(name="TerminalTool"), Tool(name="FileEditorTool")] ->>> agent = Agent(llm=llm, tools=tools) -``` +#### Example +from openhands.sdk import LLM, Agent, Tool +llm = LLM(model="claude-sonnet-4-20250514", api_key=SecretStr("key")) +tools = [Tool(name="TerminalTool"), Tool(name="FileEditorTool")] +agent = Agent(llm=llm, tools=tools) #### Properties @@ -52,8 +51,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 @@ -79,7 +78,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 @@ -204,7 +203,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 index b6c80b516..6b7ff2949 100644 --- a/sdk/api-reference/openhands.sdk.context.mdx +++ b/sdk/api-reference/openhands.sdk.context.mdx @@ -17,7 +17,7 @@ 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. +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 @@ -82,8 +82,8 @@ Custom suffix can typically includes: Skill categorization: - AgentSkills-format (SKILL.md): Always in `` (progressive - disclosure). If has triggers, content is ALSO auto-injected on trigger - in user prompts. +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 diff --git a/sdk/api-reference/openhands.sdk.conversation.mdx b/sdk/api-reference/openhands.sdk.conversation.mdx index 974bf3d6e..4edbc8f5e 100644 --- a/sdk/api-reference/openhands.sdk.conversation.mdx +++ b/sdk/api-reference/openhands.sdk.conversation.mdx @@ -176,19 +176,18 @@ while RemoteConversation connects to a remote agent server. #### Example -```pycon ->>> from openhands.sdk import LLM, Agent, Conversation ->>> from openhands.sdk.plugin import PluginSource ->>> llm = LLM(model="claude-sonnet-4-20250514", api_key=SecretStr("key")) ->>> agent = Agent(llm=llm, tools=[]) ->>> conversation = Conversation( +#### Example +from openhands.sdk import LLM, Agent, Conversation +from openhands.sdk.plugin import PluginSource +llm = LLM(model="claude-sonnet-4-20250514", api_key=SecretStr("key")) +agent = Agent(llm=llm, tools=[]) +conversation = Conversation( ... agent=agent, ... workspace="./workspace", ... plugins=[PluginSource(source="github:org/security-plugin", ref="v1.0")], ... ) ->>> conversation.send_message("Hello!") ->>> conversation.run() -``` +conversation.send_message("Hello!") +conversation.run() ### class ConversationExecutionStatus @@ -388,7 +387,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 @@ -584,7 +583,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 @@ -820,7 +819,7 @@ Remote conversation proxy that talks to an agent server. 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 diff --git a/sdk/api-reference/openhands.sdk.critic.mdx b/sdk/api-reference/openhands.sdk.critic.mdx index 9cd1dc30e..3ba4c3a58 100644 --- a/sdk/api-reference/openhands.sdk.critic.mdx +++ b/sdk/api-reference/openhands.sdk.critic.mdx @@ -165,15 +165,16 @@ automatically retry the task if the critic score is below the threshold. #### Example +```python critic = APIBasedCritic( -: server_url=”…”, +server_url=”…”, api_key=”…”, model_name=”critic”, iterative_refinement=IterativeRefinementConfig( - `
` - > success_threshold=0.7, - > max_iterations=3, - `
` + +success_threshold=0.7, +max_iterations=3, + ), ) diff --git a/sdk/api-reference/openhands.sdk.hooks.mdx b/sdk/api-reference/openhands.sdk.hooks.mdx index 9c0b2c336..cb79994aa 100644 --- a/sdk/api-reference/openhands.sdk.hooks.mdx +++ b/sdk/api-reference/openhands.sdk.hooks.mdx @@ -18,22 +18,21 @@ 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 (recommended): - config = HookConfig( +# Direct instantiation with typed fields() +config = HookConfig( - > pre_tool_use=[ - > : HookMatcher( - > : matcher=”terminal”, - > hooks=[HookDefinition(command=”block_dangerous.sh”)] - > `
` - > ) +pre_tool_use=[ +: HookMatcher( +: matcher=”terminal”, +hooks=[HookDefinition(command=”block_dangerous.sh”)] +) - > ] +] - ) +) - # Load from JSON file: - config = HookConfig.load(“.openhands/hooks.json”) +# Load from JSON file: +config = HookConfig.load(“.openhands/hooks.json”) #### Properties @@ -56,7 +55,7 @@ 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) diff --git a/sdk/api-reference/openhands.sdk.llm.mdx b/sdk/api-reference/openhands.sdk.llm.mdx index 2405f5010..63e3ab080 100644 --- a/sdk/api-reference/openhands.sdk.llm.mdx +++ b/sdk/api-reference/openhands.sdk.llm.mdx @@ -144,16 +144,15 @@ retry logic, and tool calling capabilities. #### Example -```pycon ->>> from openhands.sdk import LLM ->>> from pydantic import SecretStr ->>> llm = LLM( +#### Example +from openhands.sdk import LLM +from pydantic import SecretStr +llm = LLM( ... model="claude-sonnet-4-20250514", ... api_key=SecretStr("your-api-key"), ... usage_id="my-agent" ... ) ->>> # Use with agent or conversation -``` +# Use with agent or conversation #### Properties diff --git a/sdk/api-reference/openhands.sdk.logger.mdx b/sdk/api-reference/openhands.sdk.logger.mdx index f6dffa1ed..6063d7d5e 100644 --- a/sdk/api-reference/openhands.sdk.logger.mdx +++ b/sdk/api-reference/openhands.sdk.logger.mdx @@ -17,14 +17,13 @@ and JSON formatting for machine processing, depending on environment configurati * Returns: A configured Logger instance. -### Example +#### 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") -``` +### Example +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() diff --git a/sdk/api-reference/openhands.sdk.mcp.mdx b/sdk/api-reference/openhands.sdk.mcp.mdx index bd3d25dfb..790679205 100644 --- a/sdk/api-reference/openhands.sdk.mcp.mdx +++ b/sdk/api-reference/openhands.sdk.mcp.mdx @@ -14,39 +14,38 @@ 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 +with create_mcp_tools(config) as client: +: for tool in client.tools: +: # use tool - # Connection automatically closed +# Connection automatically closed Or manage lifecycle manually by calling sync_close() when done. diff --git a/sdk/api-reference/openhands.sdk.plugin.mdx b/sdk/api-reference/openhands.sdk.plugin.mdx index ef859e259..8d00cbc75 100644 --- a/sdk/api-reference/openhands.sdk.plugin.mdx +++ b/sdk/api-reference/openhands.sdk.plugin.mdx @@ -159,33 +159,28 @@ 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. -Example: +#### Example + -``` -`` -``` -``` -` -``` json { - “name”: “company-tools”, - “owner”: (configuration object), - “plugins”: [ +“name”: “company-tools”, +“owner”: (configuration object), +“plugins”: [ - > (configuration object) +(JSON configuration object) - ], - “skills”: [ +], +“skills”: [ - > (configuration object) +(JSON configuration object) - ] +] -## } +} #### Properties @@ -361,7 +356,7 @@ 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 @@ -372,8 +367,6 @@ plugin-name/ │ └── hooks.json ├── .mcp.json # External tool configuration (optional) └── README.md # Plugin documentation -`` - #### Properties @@ -436,7 +429,7 @@ the plugin. 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” + 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/ @@ -537,22 +530,19 @@ to fetch and load plugins from various sources. #### Examples ```pycon ->>> # GitHub repository ->>> PluginSource(source="github:owner/repo", ref="v1.0.0") -``` +# GitHub repository +PluginSource(source="github:owner/repo", ref="v1.0.0") ```pycon ->>> # Plugin from monorepo subdirectory ->>> PluginSource( +# Plugin from monorepo subdirectory +PluginSource( ... source="github:owner/monorepo", ... repo_path="plugins/my-plugin" ... ) -``` ```pycon ->>> # Local path ->>> PluginSource(source="/path/to/plugin") -``` +# Local path +PluginSource(source="/path/to/plugin") #### Properties diff --git a/sdk/api-reference/openhands.sdk.security.mdx b/sdk/api-reference/openhands.sdk.security.mdx index d3a2fad46..a504b7285 100644 --- a/sdk/api-reference/openhands.sdk.security.mdx +++ b/sdk/api-reference/openhands.sdk.security.mdx @@ -100,11 +100,10 @@ Environment Variables: #### Example -```pycon ->>> from openhands.sdk.security.grayswan import GraySwanAnalyzer ->>> analyzer = GraySwanAnalyzer() ->>> risk = analyzer.security_risk(action_event) -``` +#### Example +from openhands.sdk.security.grayswan import GraySwanAnalyzer +analyzer = GraySwanAnalyzer() +risk = analyzer.security_risk(action_event) #### Properties @@ -315,13 +314,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.subagent.mdx b/sdk/api-reference/openhands.sdk.subagent.mdx index b972ef35f..16056bb75 100644 --- a/sdk/api-reference/openhands.sdk.subagent.mdx +++ b/sdk/api-reference/openhands.sdk.subagent.mdx @@ -54,7 +54,7 @@ Agent Markdown files have YAML frontmatter with: - color (optional): Display color - permission_mode (optional): How the subagent handles permissions - (‘always_confirm’, ‘never_confirm’, ‘confirm_risky’). None inherits parent. +(‘always_confirm’, ‘never_confirm’, ‘confirm_risky’). None inherits parent. - max_iterations_per_run: Max iteration per run - hooks (optional): List of applicable hooks diff --git a/sdk/api-reference/openhands.sdk.testing.mdx b/sdk/api-reference/openhands.sdk.testing.mdx deleted file mode 100644 index 166569a7a..000000000 --- a/sdk/api-reference/openhands.sdk.testing.mdx +++ /dev/null @@ -1,130 +0,0 @@ ---- -title: openhands.sdk.testing -description: API reference for openhands.sdk.testing module ---- - - -Testing utilities for OpenHands SDK. - -This module provides test utilities that make it easy to write tests for -code that uses the OpenHands SDK, without needing to mock LiteLLM internals. - -### class TestLLM - -Bases: `LLM` - -A mock LLM for testing that returns scripted responses. - -TestLLM is a real LLM subclass that can be used anywhere an LLM is accepted: -in Agent(llm=…), in fallback_llms, in condensers, in routers, etc. - -Key features: -- No patching needed: just pass TestLLM as the llm= argument -- Tests speak in SDK types (Message, TextContent, MessageToolCall) -- Clear error when responses are exhausted -- Zero-cost metrics by default -- Always uses completion() path (uses_responses_api returns False) - -#### Example - -```pycon ->>> from openhands.sdk.testing import TestLLM ->>> from openhands.sdk.llm import Message, TextContent, MessageToolCall ->>> ->>> # Simple text response ->>> llm = TestLLM.from_messages([ -... Message(role="assistant", content=[TextContent(text="Done!")]), -... ]) ->>> ->>> # Response with tool calls ->>> llm = TestLLM.from_messages([ -... Message( -... role="assistant", -... content=[TextContent(text="")], -... tool_calls=[ -... MessageToolCall( -... id="call_1", -... name="my_tool", -... arguments='(configuration object)', -... origin="completion", -... ) -... ], -... ), -... Message(role="assistant", content=[TextContent(text="Done!")]), -... ]) -``` - - -#### Properties - -- `call_count`: int - Return the number of calls made to this TestLLM. -- `model`: str -- `model_config`: ClassVar[ConfigDict] = (configuration object) - Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. -- `remaining_responses`: int - Return the number of remaining scripted responses. - -#### Methods - -#### __init__() - -Create a new model by parsing and validating input data from keyword arguments. - -Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model. - -self is explicitly positional-only to allow self as a field name. - -#### completion() - -Return the next scripted response. - -* Parameters: - * `messages` – Input messages (ignored, but required for API compatibility) - * `tools` – Available tools (ignored) - * `_return_metrics` – Whether to return metrics (ignored) - * `add_security_risk_prediction` – Add security risk field (ignored) - * `on_token` – Streaming callback (ignored) - kwargs* – Additional arguments (ignored) -* Returns: - LLMResponse containing the next scripted message. -* Raises: - * [TestLLMExhaustedError](#class-testllmexhaustederror) – When no more scripted responses are available. - * `Exception` – Any scripted exception placed in the response queue. - -#### classmethod from_messages() - -Create a TestLLM with scripted responses and/or errors. - -* Parameters: - * `messages` – List of Message or Exception objects to return in order. - Each call to completion() or responses() consumes the next - item: Message objects are returned normally, Exception objects - are raised (like unittest.mock side_effect). - * `model` – Model name (default: “test-model”) - * `usage_id` – Usage ID for metrics (default: “test-llm”) - kwargs* – Additional LLM configuration options -* Returns: - A TestLLM instance configured with the scripted responses. - -#### 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. - -#### responses() - -Return the next scripted response (delegates to completion). - -For TestLLM, both completion() and responses() return from the same -queue of scripted responses. - -#### uses_responses_api() - -TestLLM always uses the completion path. diff --git a/sdk/api-reference/openhands.sdk.tool.mdx b/sdk/api-reference/openhands.sdk.tool.mdx index 62b85a290..a9c920028 100644 --- a/sdk/api-reference/openhands.sdk.tool.mdx +++ b/sdk/api-reference/openhands.sdk.tool.mdx @@ -202,38 +202,35 @@ Simple tool with no parameters: : class FinishTool(ToolDefinition[FinishAction, FinishObservation]): : @classmethod def create(cls, conv_state=None, - `
` + ``` ** ``` - `
` + params): - `
` - > return [cls(name=”finish”, …, executor=FinishExecutor())] + + return [cls(name=”finish”, …, executor=FinishExecutor())] Complex tool with initialization parameters: : class TerminalTool(ToolDefinition[TerminalAction, : TerminalObservation]): @classmethod def create(cls, conv_state, - `
` + ``` ** ``` - `
` + params): - `
` - > executor = TerminalExecutor( - > : working_dir=conv_state.workspace.working_dir, - > `
` - > ``` - > ** - > ``` - > `
` - > params, - `
` - > ) - > return [cls(name=”terminal”, …, executor=executor)] + + executor = TerminalExecutor( + : working_dir=conv_state.workspace.working_dir, + ``` + ** + params, + + ) + return [cls(name=”terminal”, …, executor=executor)] #### Properties diff --git a/sdk/api-reference/openhands.sdk.utils.mdx b/sdk/api-reference/openhands.sdk.utils.mdx index 0ffa30fe4..333eeffc7 100644 --- a/sdk/api-reference/openhands.sdk.utils.mdx +++ b/sdk/api-reference/openhands.sdk.utils.mdx @@ -48,17 +48,19 @@ exhausted. * Yields: Individual items of type T from each page -### Example +#### Example +```python async for event in page_iterator(event_service.search_events, limit=50): -: await send_event(event, websocket) +await send_event(event, websocket) async for conversation in page_iterator( -: conversation_service.search_conversations, +conversation_service.search_conversations, execution_status=ConversationExecutionStatus.RUNNING ): -: print(conversation.title) +print(conversation.title) + ### sanitize_openhands_mentions() @@ -76,13 +78,12 @@ preserving readability. The original case of the mention is preserved. ### Examples ```pycon ->>> sanitize_openhands_mentions("Thanks @OpenHands for the help!") +sanitize_openhands_mentions("Thanks @OpenHands for the help!") 'Thanks @u200dOpenHands for the help!' ->>> sanitize_openhands_mentions("Check @openhands and @OPENHANDS") +sanitize_openhands_mentions("Check @openhands and @OPENHANDS") 'Check @u200dopenhands and @u200dOPENHANDS' ->>> sanitize_openhands_mentions("No mention here") +sanitize_openhands_mentions("No mention here") 'No mention here' -``` ### sanitized_env() diff --git a/sdk/api-reference/openhands.sdk.workspace.mdx b/sdk/api-reference/openhands.sdk.workspace.mdx index c26ffd06f..e5969efe3 100644 --- a/sdk/api-reference/openhands.sdk.workspace.mdx +++ b/sdk/api-reference/openhands.sdk.workspace.mdx @@ -119,11 +119,10 @@ support the context manager protocol for safe resource management. #### Example -```pycon ->>> with workspace: +#### Example +with workspace: ... result = workspace.execute_command("echo 'hello'") ... content = workspace.read_file("example.txt") -``` #### Properties @@ -280,12 +279,11 @@ should operate directly on the host system. #### Example -```pycon ->>> workspace = LocalWorkspace(working_dir="/path/to/project") ->>> with workspace: +#### Example +workspace = LocalWorkspace(working_dir="/path/to/project") +with workspace: ... result = workspace.execute_command("ls -la") ... content = workspace.read_file("README.md") -``` #### Methods @@ -401,15 +399,14 @@ as it provides better isolation and security. #### Example -```pycon ->>> workspace = RemoteWorkspace( +#### Example +workspace = RemoteWorkspace( ... host="https://agent-server.example.com", ... working_dir="/workspace" ... ) ->>> with workspace: +with workspace: ... result = workspace.execute_command("ls -la") ... content = workspace.read_file("README.md") -``` #### Properties From be89528d146b84b3d6006a13e905b90b6474d374 Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 15 Mar 2026 19:14:57 +0000 Subject: [PATCH 3/8] Remove malformed Example sections that cause MDX parsing errors Added remove_malformed_examples() function to detect and remove Example sections containing raw JSON/code without proper code block formatting. These caused MDX parsing errors due to curly braces being interpreted as JSX expressions. Co-authored-by: openhands --- scripts/generate-api-docs.py | 61 ++++++++++++++++++++++ sdk/api-reference/openhands.sdk.plugin.mdx | 24 --------- 2 files changed, 61 insertions(+), 24 deletions(-) diff --git a/scripts/generate-api-docs.py b/scripts/generate-api-docs.py index 2f06541ac..ea0c28420 100755 --- a/scripts/generate-api-docs.py +++ b/scripts/generate-api-docs.py @@ -379,6 +379,63 @@ def fix_example_blocks(self, content: str) -> str: return '\n'.join(result_lines) + 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 @@ -616,6 +673,10 @@ def clean_markdown_content(self, content: str, filename: str) -> str: 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) + lines = content.split('\n') cleaned_lines = [] diff --git a/sdk/api-reference/openhands.sdk.plugin.mdx b/sdk/api-reference/openhands.sdk.plugin.mdx index 8d00cbc75..17141d1ba 100644 --- a/sdk/api-reference/openhands.sdk.plugin.mdx +++ b/sdk/api-reference/openhands.sdk.plugin.mdx @@ -159,30 +159,6 @@ 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. -#### Example - - - - -json -{ - -“name”: “company-tools”, -“owner”: (configuration object), -“plugins”: [ - -(JSON configuration object) - -], -“skills”: [ - -(JSON configuration object) - -] - -} - - #### Properties - `description`: str | None From caa5fb142a18e0857b86741bef815df389ee8d93 Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 15 Mar 2026 19:22:04 +0000 Subject: [PATCH 4/8] Fix class spacing and *args/**kwargs rendering issues 1. Add horizontal rules (---) before class headers to fix spacing issue where CSS margin-top: 0 was applied when h4 followed by h3. 2. Fix malformed *args and **kwargs patterns from Sphinx output. Sphinx was breaking these into weird code blocks like: ``` * ``` args Now properly renders as `*args` and `**kwargs`. Co-authored-by: openhands --- scripts/generate-api-docs.py | 78 ++++++++++++++++++++++- sdk/api-reference/openhands.sdk.hooks.mdx | 2 +- sdk/api-reference/openhands.sdk.llm.mdx | 10 +-- sdk/api-reference/openhands.sdk.mcp.mdx | 31 ++------- sdk/api-reference/openhands.sdk.tool.mdx | 28 +++----- sdk/api-reference/openhands.sdk.utils.mdx | 4 +- 6 files changed, 97 insertions(+), 56 deletions(-) diff --git a/scripts/generate-api-docs.py b/scripts/generate-api-docs.py index ea0c28420..041b037c3 100755 --- a/scripts/generate-api-docs.py +++ b/scripts/generate-api-docs.py @@ -379,6 +379,41 @@ def fix_example_blocks(self, content: str) -> str: return '\n'.join(result_lines) + 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 (### class ...) + if line.startswith('### class '): + # Don't add separator before the very first class (after frontmatter) + # Check if there's actual content before this (not just blank lines) + has_content_before = False + for j in range(i - 1, -1, -1): + if lines[j].strip(): + # Found non-blank 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 @@ -602,6 +637,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) @@ -624,6 +688,10 @@ def clean_markdown_content(self, content: str, filename: str) -> str: 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' @@ -677,6 +745,10 @@ def clean_markdown_content(self, content: str, filename: str) -> str: # 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 = [] @@ -754,10 +826,10 @@ def remove_problematic_patterns(self, line: str) -> str: line = re.sub(r'<(/?\w+[^>]*)>', r'`<\1>`', line) # 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 = {} diff --git a/sdk/api-reference/openhands.sdk.hooks.mdx b/sdk/api-reference/openhands.sdk.hooks.mdx index cb79994aa..e60f62a39 100644 --- a/sdk/api-reference/openhands.sdk.hooks.mdx +++ b/sdk/api-reference/openhands.sdk.hooks.mdx @@ -277,7 +277,7 @@ Bases: `BaseModel` Matches events to hooks based on patterns. -Supports exact match, wildcard (*), and regex (auto-detected or /pattern/). +Supports exact match, wildcard (\*), and regex (auto-detected or /pattern/). #### Properties diff --git a/sdk/api-reference/openhands.sdk.llm.mdx b/sdk/api-reference/openhands.sdk.llm.mdx index 63e3ab080..a9db98fcf 100644 --- a/sdk/api-reference/openhands.sdk.llm.mdx +++ b/sdk/api-reference/openhands.sdk.llm.mdx @@ -241,7 +241,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. @@ -320,7 +320,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 @@ -354,7 +354,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: @@ -792,7 +792,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: @@ -926,7 +926,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 diff --git a/sdk/api-reference/openhands.sdk.mcp.mdx b/sdk/api-reference/openhands.sdk.mcp.mdx index 790679205..28a8485a7 100644 --- a/sdk/api-reference/openhands.sdk.mcp.mdx +++ b/sdk/api-reference/openhands.sdk.mcp.mdx @@ -14,30 +14,11 @@ MCP client with sync helpers and lifecycle management. Extends fastmcp.Client with: : - call_async_from_sync(awaitable_or_fn, - - ``` - * - ``` - - args, timeout=None, - - ``` - ** - ``` - - kwargs) + `*args`, timeout=None, + `**kwargs`) - call_sync_from_async(fn, - - ``` - * - ``` - - args, - - ``` - ** - - kwargs) # await this from async code + `*args`, + `**kwargs`) # await this from async code After create_mcp_tools() populates it, use as a sync context manager: @@ -157,8 +138,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. diff --git a/sdk/api-reference/openhands.sdk.tool.mdx b/sdk/api-reference/openhands.sdk.tool.mdx index a9c920028..e94d04146 100644 --- a/sdk/api-reference/openhands.sdk.tool.mdx +++ b/sdk/api-reference/openhands.sdk.tool.mdx @@ -58,7 +58,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: @@ -75,7 +75,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,7 +101,7 @@ 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. @@ -125,7 +125,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: @@ -201,13 +201,7 @@ Features: Simple tool with no parameters: : class FinishTool(ToolDefinition[FinishAction, FinishObservation]): : @classmethod - def create(cls, conv_state=None, - - ``` - ** - ``` - - params): + def create(cls, conv_state=None, params): return [cls(name=”finish”, …, executor=FinishExecutor())] @@ -215,13 +209,7 @@ Complex tool with initialization parameters: : class TerminalTool(ToolDefinition[TerminalAction, : TerminalObservation]): @classmethod - def create(cls, conv_state, - - ``` - ** - ``` - - params): + def create(cls, conv_state, params): executor = TerminalExecutor( : working_dir=conv_state.workspace.working_dir, @@ -281,8 +269,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. diff --git a/sdk/api-reference/openhands.sdk.utils.mdx b/sdk/api-reference/openhands.sdk.utils.mdx index 333eeffc7..86a482980 100644 --- a/sdk/api-reference/openhands.sdk.utils.mdx +++ b/sdk/api-reference/openhands.sdk.utils.mdx @@ -43,8 +43,8 @@ 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 + `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 From 1a3d2e6b2fce22b9e90132add6fd9713cac573f0 Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 15 Mar 2026 19:24:07 +0000 Subject: [PATCH 5/8] Collapse API Reference section in left navigation by default Add 'collapsed: true' to the API Reference group to reduce visual clutter in the navigation sidebar. Co-authored-by: openhands --- docs.json | 1 + 1 file changed, 1 insertion(+) diff --git a/docs.json b/docs.json index 245bf1052..19ce6e3a9 100644 --- a/docs.json +++ b/docs.json @@ -384,6 +384,7 @@ }, { "group": "API Reference", + "collapsed": true, "pages": [ "sdk/api-reference/openhands.sdk.agent", "sdk/api-reference/openhands.sdk.context", From 3c4651f06d253577e2bf1e4edf51eb6303d2e867 Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 15 Mar 2026 19:36:29 +0000 Subject: [PATCH 6/8] Fix code block preservation and class separator detection --- scripts/generate-api-docs.py | 76 ++++++++++++------- sdk/api-reference/openhands.sdk.agent.mdx | 17 +++-- sdk/api-reference/openhands.sdk.context.mdx | 18 +++++ .../openhands.sdk.conversation.mdx | 56 ++++++++++++-- sdk/api-reference/openhands.sdk.critic.mdx | 22 ++++++ sdk/api-reference/openhands.sdk.event.mdx | 54 +++++++++++++ sdk/api-reference/openhands.sdk.hooks.mdx | 33 ++++++++ sdk/api-reference/openhands.sdk.io.mdx | 9 +++ sdk/api-reference/openhands.sdk.llm.mdx | 68 +++++++++++++++-- sdk/api-reference/openhands.sdk.logger.mdx | 11 +-- sdk/api-reference/openhands.sdk.mcp.mdx | 15 ++++ sdk/api-reference/openhands.sdk.plugin.mdx | 60 +++++++++++++-- sdk/api-reference/openhands.sdk.secret.mdx | 9 +++ sdk/api-reference/openhands.sdk.security.mdx | 33 +++++++- sdk/api-reference/openhands.sdk.skills.mdx | 6 ++ sdk/api-reference/openhands.sdk.subagent.mdx | 3 + sdk/api-reference/openhands.sdk.tool.mdx | 28 +++++++ sdk/api-reference/openhands.sdk.utils.mdx | 8 +- sdk/api-reference/openhands.sdk.workspace.mdx | 43 +++++++++-- 19 files changed, 499 insertions(+), 70 deletions(-) diff --git a/scripts/generate-api-docs.py b/scripts/generate-api-docs.py index 041b037c3..16cda2706 100755 --- a/scripts/generate-api-docs.py +++ b/scripts/generate-api-docs.py @@ -372,7 +372,10 @@ def fix_example_blocks(self, content: str) -> str: result_lines.extend(cleaned_code) result_lines.append('```') result_lines.append('') - continue + # 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 @@ -392,14 +395,21 @@ def add_class_separators(self, content: str) -> str: result = [] for i, line in enumerate(lines): - # Check if this is a class header (### class ...) - if line.startswith('### class '): + # 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) + # 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): - if lines[j].strip(): - # Found non-blank content + stripped = lines[j].strip() + if stripped and stripped != '---': # Ignore frontmatter delimiters + # Found non-blank, non-frontmatter content has_content_before = True break @@ -695,11 +705,14 @@ def clean_markdown_content(self, content: str, filename: str) -> str: # 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 - content = re.sub(r'^(\s*)>\s*', r'\1', content, flags=re.MULTILINE) + # 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) @@ -711,31 +724,42 @@ def clean_markdown_content(self, content: str, filename: str) -> str: # Clean up multiple consecutive blank lines (more than 2) content = re.sub(r'\n{4,}', '\n\n\n', content) - # Remove orphaned code block openers followed by malformed content - # Pattern: ``` followed by content that doesn't have a matching closing ``` + # 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 orphaned code block - if line.strip() == '```': - # Look ahead to see if there's proper code and closing - 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 + + # 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 diff --git a/sdk/api-reference/openhands.sdk.agent.mdx b/sdk/api-reference/openhands.sdk.agent.mdx index 6db11cfea..7bb57a659 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) @@ -17,11 +20,12 @@ is provided by CriticMixin. #### Example -#### Example -from openhands.sdk import LLM, Agent, Tool -llm = LLM(model="claude-sonnet-4-20250514", api_key=SecretStr("key")) -tools = [Tool(name="TerminalTool"), Tool(name="FileEditorTool")] -agent = Agent(llm=llm, tools=tools) +```pycon +>>> from openhands.sdk import LLM, Agent, Tool +>>> llm = LLM(model="claude-sonnet-4-20250514", api_key=SecretStr("key")) +>>> tools = [Tool(name="TerminalTool"), Tool(name="FileEditorTool")] +>>> agent = Agent(llm=llm, tools=tools) +``` #### Properties @@ -88,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` diff --git a/sdk/api-reference/openhands.sdk.context.mdx b/sdk/api-reference/openhands.sdk.context.mdx index 6b7ff2949..6bf771e24 100644 --- a/sdk/api-reference/openhands.sdk.context.mdx +++ b/sdk/api-reference/openhands.sdk.context.mdx @@ -4,6 +4,9 @@ description: API reference for openhands.sdk.context module --- + +--- + ### class AgentContext Bases: `BaseModel` @@ -100,6 +103,9 @@ This works by: Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + +--- + ### class BaseTrigger Bases: `BaseModel`, `ABC` @@ -112,6 +118,9 @@ Base class for all trigger types. Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + +--- + ### class KeywordTrigger Bases: [`BaseTrigger`](#class-basetrigger) @@ -132,6 +141,9 @@ These skills are activated when specific keywords appear in the user’s query. Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + +--- + ### class Skill Bases: `BaseModel` @@ -244,6 +256,9 @@ Convert this skill to a SkillInfo. * Returns: SkillInfo containing the skill’s essential information. + +--- + ### class SkillKnowledge Bases: `BaseModel` @@ -266,6 +281,9 @@ Configuration for the model, should be a dictionary conforming to [ConfigDict][p #### __init__() + +--- + ### class TaskTrigger Bases: [`BaseTrigger`](#class-basetrigger) diff --git a/sdk/api-reference/openhands.sdk.conversation.mdx b/sdk/api-reference/openhands.sdk.conversation.mdx index 4edbc8f5e..ea6a80e80 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` @@ -176,18 +185,22 @@ while RemoteConversation connects to a remote agent server. #### Example -#### Example -from openhands.sdk import LLM, Agent, Conversation -from openhands.sdk.plugin import PluginSource -llm = LLM(model="claude-sonnet-4-20250514", api_key=SecretStr("key")) -agent = Agent(llm=llm, tools=[]) -conversation = Conversation( +```pycon +>>> from openhands.sdk import LLM, Agent, Conversation +>>> from openhands.sdk.plugin import PluginSource +>>> llm = LLM(model="claude-sonnet-4-20250514", api_key=SecretStr("key")) +>>> agent = Agent(llm=llm, tools=[]) +>>> conversation = Conversation( ... agent=agent, ... workspace="./workspace", ... plugins=[PluginSource(source="github:org/security-plugin", ref="v1.0")], ... ) -conversation.send_message("Hello!") -conversation.run() +>>> conversation.send_message("Hello!") +>>> conversation.run() +``` + + +--- ### class ConversationExecutionStatus @@ -227,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` @@ -374,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` @@ -450,6 +469,9 @@ implement the visualization logic. * Parameters: `event` – The event to visualize + +--- + ### class DefaultConversationVisualizer Bases: [`ConversationVisualizerBase`](#class-conversationvisualizerbase) @@ -475,6 +497,9 @@ Initialize the visualizer. Main event handler that displays events with Rich formatting. + +--- + ### class EventLog Bases: [`EventsListBase`](#class-eventslistbase) @@ -510,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` @@ -525,6 +553,9 @@ RemoteEventsList implementations, avoiding circular imports in protocols. Add a new event to the list. + +--- + ### class LocalConversation Bases: [`BaseConversation`](#class-baseconversation) @@ -778,6 +809,9 @@ init_state(), ensuring secret names and descriptions appear in the prompt. SecretValue = str | Callable[[], str]. Callables are invoked lazily when a command references the secret key. + +--- + ### class RemoteConversation Bases: [`BaseConversation`](#class-baseconversation) @@ -934,6 +968,9 @@ Not implemented for remote conversations. #### update_secrets() + +--- + ### class SecretRegistry Bases: `OpenHandsModel` @@ -1022,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 index 3ba4c3a58..021e77ef6 100644 --- a/sdk/api-reference/openhands.sdk.critic.mdx +++ b/sdk/api-reference/openhands.sdk.critic.mdx @@ -4,6 +4,9 @@ description: API reference for openhands.sdk.critic module --- + +--- + ### class APIBasedCritic Bases: [`CriticBase`](#class-criticbase), `CriticClient` @@ -42,6 +45,9 @@ It takes context as an argument since that’s what pydantic-core passes when ca * `self` – The BaseModel instance. * `context` – The context. + +--- + ### class AgentFinishedCritic Bases: [`CriticBase`](#class-criticbase) @@ -68,6 +74,9 @@ Evaluate if an agent properly finished with a non-empty git patch. Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + +--- + ### class CriticBase Bases: `DiscriminatedUnionMixin`, `ABC` @@ -101,6 +110,9 @@ Subclasses can override this method to provide custom follow-up prompts. Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + +--- + ### class CriticResult Bases: `BaseModel` @@ -126,6 +138,9 @@ A critic result is a score and a message. Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + +--- + ### class EmptyPatchCritic Bases: [`CriticBase`](#class-criticbase) @@ -154,6 +169,9 @@ Evaluate if a git patch is non-empty. Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + +--- + ### class IterativeRefinementConfig Bases: `BaseModel` @@ -182,6 +200,7 @@ 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 @@ -195,6 +214,9 @@ conversation.run() # Will automatically retry if critic score < 0.7 Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + +--- + ### class PassCritic Bases: [`CriticBase`](#class-criticbase) diff --git a/sdk/api-reference/openhands.sdk.event.mdx b/sdk/api-reference/openhands.sdk.event.mdx index f3c5eb193..dbe22f441 100644 --- a/sdk/api-reference/openhands.sdk.event.mdx +++ b/sdk/api-reference/openhands.sdk.event.mdx @@ -4,6 +4,9 @@ description: API reference for openhands.sdk.event module --- + +--- + ### class ACPToolCallEvent Bases: [`Event`](#class-event) @@ -32,6 +35,9 @@ participate in LLM message conversion. - `tool_kind`: str | None - `visualize`: Text Return Rich Text representation of this tool call event. + +--- + ### class ActionEvent Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent) @@ -63,6 +69,9 @@ Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent) Individual message - may be incomplete for multi-action batches + +--- + ### class AgentErrorEvent Bases: [`ObservationBaseEvent`](#class-observationbaseevent) @@ -86,6 +95,9 @@ represents an error produced by the agent/scaffold, not model output. #### to_llm_message() + +--- + ### class Condensation Bases: [`Event`](#class-event) @@ -127,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) @@ -153,6 +168,9 @@ The action type, namely ActionType.CONDENSATION_REQUEST. * Type: str + +--- + ### class CondensationSummaryEvent Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent) @@ -172,6 +190,9 @@ This event represents a summary generated by a condenser. #### to_llm_message() + +--- + ### class ConversationStateUpdateEvent Bases: [`Event`](#class-event) @@ -211,6 +232,9 @@ This creates an event containing a snapshot of important state fields. #### classmethod validate_value() + +--- + ### class Event Bases: `DiscriminatedUnionMixin`, `ABC` @@ -229,6 +253,9 @@ Base class for all events. 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) @@ -265,6 +292,9 @@ This allows clients to track hook execution via the event stream. - `tool_name`: str | None - `visualize`: Text Return Rich Text representation of this hook execution event. + +--- + ### class LLMCompletionLogEvent Bases: [`Event`](#class-event) @@ -285,6 +315,9 @@ instead of writing it to a file inside the Docker container. - `model_name`: str - `source`: Literal['agent', 'user', 'environment', 'hook'] - `usage_id`: str + +--- + ### class LLMConvertibleEvent Bases: [`Event`](#class-event), `ABC` @@ -305,6 +338,9 @@ Convert event stream to LLM message stream, handling multi-action batches #### abstractmethod to_llm_message() + +--- + ### class MessageEvent Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent) @@ -335,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) @@ -351,6 +390,9 @@ Examples include tool execution, error, user reject. - `source`: Literal['agent', 'user', 'environment', 'hook'] - `tool_call_id`: str - `tool_name`: str + +--- + ### class ObservationEvent Bases: [`ObservationBaseEvent`](#class-observationbaseevent) @@ -369,6 +411,9 @@ Bases: [`ObservationBaseEvent`](#class-observationbaseevent) #### to_llm_message() + +--- + ### class PauseEvent Bases: [`Event`](#class-event) @@ -383,6 +428,9 @@ Event indicating that the agent execution was paused by user request. - `source`: Literal['agent', 'user', 'environment', 'hook'] - `visualize`: Text Return Rich Text representation of this pause event. + +--- + ### class SystemPromptEvent Bases: [`LLMConvertibleEvent`](#class-llmconvertibleevent) @@ -442,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) @@ -456,6 +507,9 @@ Event from VLLM representing token IDs used in LLM interaction. - `prompt_token_ids`: list[int] - `response_token_ids`: list[int] - `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 index e60f62a39..04c3ee326 100644 --- a/sdk/api-reference/openhands.sdk.hooks.mdx +++ b/sdk/api-reference/openhands.sdk.hooks.mdx @@ -9,6 +9,9 @@ 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` @@ -98,6 +101,9 @@ configs for each event type. Save hook configuration to a JSON file using snake_case field names. + +--- + ### class HookDecision Bases: `str`, `Enum` @@ -110,6 +116,9 @@ Decisions a hook can make about an operation. #### DENY = 'deny' + +--- + ### class HookDefinition Bases: `BaseModel` @@ -125,6 +134,9 @@ A single hook definition. Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. - `timeout`: int - `type`: [HookType](#class-hooktype) + +--- + ### class HookEvent Bases: `BaseModel` @@ -144,6 +156,9 @@ Data passed to hook scripts via stdin as JSON. - `tool_name`: str | None - `tool_response`: dict[str, Any] | None - `working_dir`: str | None + +--- + ### class HookEventProcessor Bases: `object` @@ -187,6 +202,9 @@ Run Stop hooks. Returns (should_stop, feedback). Set conversation state for blocking support. + +--- + ### class HookEventType Bases: `str`, `Enum` @@ -207,6 +225,9 @@ Types of hook events that can trigger hooks. #### USER_PROMPT_SUBMIT = 'UserPromptSubmit' + +--- + ### class HookExecutor Bases: `object` @@ -225,6 +246,9 @@ Execute a single hook. Execute multiple hooks in order, optionally stopping on block. + +--- + ### class HookManager Bases: `object` @@ -271,6 +295,9 @@ Run Stop hooks. Returns (should_stop, results). Run UserPromptSubmit hooks. + +--- + ### class HookMatcher Bases: `BaseModel` @@ -305,6 +332,9 @@ It takes context as an argument since that’s what pydantic-core passes when ca * `self` – The BaseModel instance. * `context` – The context. + +--- + ### class HookResult Bases: `BaseModel` @@ -335,6 +365,9 @@ Exit code 0 = success, exit code 2 = block operation. Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + +--- + ### class HookType Bases: `str`, `Enum` diff --git a/sdk/api-reference/openhands.sdk.io.mdx b/sdk/api-reference/openhands.sdk.io.mdx index a3183ee87..3175a549d 100644 --- a/sdk/api-reference/openhands.sdk.io.mdx +++ b/sdk/api-reference/openhands.sdk.io.mdx @@ -4,6 +4,9 @@ description: API reference for openhands.sdk.io module --- + +--- + ### class FileStore Bases: `ABC` @@ -89,6 +92,9 @@ Write contents to a file at the specified path. * `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) @@ -147,6 +153,9 @@ Write contents to a file at the specified path. * `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) diff --git a/sdk/api-reference/openhands.sdk.llm.mdx b/sdk/api-reference/openhands.sdk.llm.mdx index a9db98fcf..21ad14c1a 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,9 @@ Update tokens for an existing credential. * Returns: Updated credentials, or None if no existing credentials found + +--- + ### class FallbackStrategy Bases: `BaseModel` @@ -111,6 +117,9 @@ Try fallback LLMs in order. Merges metrics into primary on success. * Returns: LLMResponse from the first successful fallback, or None if all fail. + +--- + ### class ImageContent Bases: `BaseContent` @@ -131,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` @@ -144,15 +156,16 @@ retry logic, and tool calling capabilities. #### Example -#### Example -from openhands.sdk import LLM -from pydantic import SecretStr -llm = LLM( +```pycon +>>> from openhands.sdk import LLM +>>> from pydantic import SecretStr +>>> llm = LLM( ... model="claude-sonnet-4-20250514", ... api_key=SecretStr("your-api-key"), ... usage_id="my-agent" ... ) -# Use with agent or conversation +>>> # Use with agent or conversation +``` #### Properties @@ -367,6 +380,9 @@ Whether this model uses the OpenAI Responses API path. #### vision_is_active() + +--- + ### class LLMProfileStore Bases: `object` @@ -428,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` @@ -503,6 +522,9 @@ Subscribe to registry events. * Parameters: `callback` – Function to call when LLMs are created or updated. + +--- + ### class LLMResponse Bases: `BaseModel` @@ -553,6 +575,9 @@ ResponsesAPIResponse) for internal use * Type: litellm.types.utils.ModelResponse | litellm.types.llms.openai.ResponsesAPIResponse + +--- + ### class Message Bases: `BaseModel` @@ -624,6 +649,9 @@ Return serialized form. Either an instructions string (for system) or input items (for other roles). + +--- + ### class MessageToolCall Bases: `BaseModel` @@ -715,6 +743,9 @@ Configuration for the model, should be a dictionary conforming to [ConfigDict][p #### classmethod validate_accumulated_cost() + +--- + ### class MetricsSnapshot Bases: `BaseModel` @@ -737,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` @@ -762,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` @@ -837,6 +874,9 @@ Refresh credentials if they are expired. * Raises: `RuntimeError` – If token refresh fails. + +--- + ### class ReasoningItemModel Bases: `BaseModel` @@ -860,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` @@ -881,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` @@ -891,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) @@ -962,6 +1011,9 @@ Guarantee model exists before LLM base validation runs. #### classmethod validate_llms_not_empty() + +--- + ### class TextContent Bases: `BaseContent` @@ -980,6 +1032,9 @@ Bases: `BaseContent` Convert to LLM API format. + +--- + ### class ThinkingBlock Bases: `BaseModel` @@ -1003,6 +1058,9 @@ and passed back to the API for tool use scenarios. Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + +--- + ### class TokenUsage Bases: `BaseModel` diff --git a/sdk/api-reference/openhands.sdk.logger.mdx b/sdk/api-reference/openhands.sdk.logger.mdx index 6063d7d5e..16bbfe1a0 100644 --- a/sdk/api-reference/openhands.sdk.logger.mdx +++ b/sdk/api-reference/openhands.sdk.logger.mdx @@ -19,11 +19,12 @@ and JSON formatting for machine processing, depending on environment configurati #### Example -### Example -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") +```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() diff --git a/sdk/api-reference/openhands.sdk.mcp.mdx b/sdk/api-reference/openhands.sdk.mcp.mdx index 28a8485a7..4b3ff1a8a 100644 --- a/sdk/api-reference/openhands.sdk.mcp.mdx +++ b/sdk/api-reference/openhands.sdk.mcp.mdx @@ -6,6 +6,9 @@ description: API reference for openhands.sdk.mcp module MCP (Model Context Protocol) integration for agent-sdk. + +--- + ### class MCPClient Bases: `Client` @@ -67,6 +70,9 @@ then shutdown the background event loop. Safe to call multiple times. #### __init__() + +--- + ### class MCPToolAction Bases: `Action` @@ -96,6 +102,9 @@ 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]` @@ -169,6 +178,9 @@ generate the OpenAI-compatible tool schema. tools that may have safety risks, so the LLM can reason about the risk level before calling the tool. + +--- + ### class MCPToolExecutor Bases: `ToolExecutor` @@ -190,6 +202,9 @@ Executor for MCP tools. Execute the MCP tool call using the already-connected client. + +--- + ### class MCPToolObservation Bases: `Observation` diff --git a/sdk/api-reference/openhands.sdk.plugin.mdx b/sdk/api-reference/openhands.sdk.plugin.mdx index 17141d1ba..6d7abca8f 100644 --- a/sdk/api-reference/openhands.sdk.plugin.mdx +++ b/sdk/api-reference/openhands.sdk.plugin.mdx @@ -15,6 +15,9 @@ 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` @@ -69,6 +72,9 @@ format: /``:`` * Returns: A Skill object with the command content and a KeywordTrigger. + +--- + ### class GitHubURLComponents Bases: `NamedTuple` @@ -86,6 +92,9 @@ Parsed components of a GitHub blob/tree URL. Alias for field number 3 - `repo`: str Alias for field number 1 + +--- + ### class InstalledPluginInfo Bases: `BaseModel` @@ -118,6 +127,9 @@ Create InstalledPluginInfo from a loaded Plugin. Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + +--- + ### class InstalledPluginsMetadata Bases: `BaseModel` @@ -147,6 +159,9 @@ Configuration for the model, should be a dictionary conforming to [ConfigDict][p Save metadata to the installed plugins directory. + +--- + ### class Marketplace Bases: `BaseModel` @@ -207,6 +222,9 @@ Resolve a plugin’s source to a full path or URL. * Return type: Tuple of (source, [ref](#class-ref), subpath) where + +--- + ### class MarketplaceEntry Bases: `BaseModel` @@ -231,6 +249,9 @@ Source is a string path (local path or GitHub URL). - `name`: str - `source`: str - `version`: str | None + +--- + ### class MarketplaceMetadata Bases: `BaseModel` @@ -244,6 +265,9 @@ Optional metadata for a marketplace. - `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` @@ -264,6 +288,9 @@ The owner represents the maintainer or team responsible for the marketplace. Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + +--- + ### class MarketplacePluginEntry Bases: [`MarketplaceEntry`](#class-marketplaceentry) @@ -299,6 +326,9 @@ Plugins support both string sources and complex source objects Convert to PluginManifest (for strict=False entries). + +--- + ### class MarketplacePluginSource Bases: `BaseModel` @@ -324,6 +354,9 @@ Supports GitHub repositories and generic git URLs. Validate that required fields are present based on source type. + +--- + ### class Plugin Bases: `BaseModel` @@ -457,6 +490,9 @@ Load all plugins from a directory. Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + +--- + ### class PluginAuthor Bases: `BaseModel` @@ -479,6 +515,9 @@ Parse author from string format ‘Name ``’. Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + +--- + ### class PluginManifest Bases: `BaseModel` @@ -494,6 +533,9 @@ Plugin manifest from plugin.json. Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. - `name`: str - `version`: str + +--- + ### class PluginSource Bases: `BaseModel` @@ -506,19 +548,22 @@ to fetch and load plugins from various sources. #### Examples ```pycon -# GitHub repository -PluginSource(source="github:owner/repo", ref="v1.0.0") +>>> # GitHub repository +>>> PluginSource(source="github:owner/repo", ref="v1.0.0") +``` ```pycon -# Plugin from monorepo subdirectory -PluginSource( +>>> # Plugin from monorepo subdirectory +>>> PluginSource( ... source="github:owner/monorepo", ... repo_path="plugins/my-plugin" ... ) +``` ```pycon -# Local path -PluginSource(source="/path/to/plugin") +>>> # Local path +>>> PluginSource(source="/path/to/plugin") +``` #### Properties @@ -537,6 +582,9 @@ Configuration for the model, should be a dictionary conforming to [ConfigDict][p Validate repo_path is a safe relative path within the repository. + +--- + ### class ResolvedPluginSource Bases: `BaseModel` diff --git a/sdk/api-reference/openhands.sdk.secret.mdx b/sdk/api-reference/openhands.sdk.secret.mdx index 9322e63e6..0581983f6 100644 --- a/sdk/api-reference/openhands.sdk.secret.mdx +++ b/sdk/api-reference/openhands.sdk.secret.mdx @@ -8,6 +8,9 @@ Secret management module for handling sensitive data. This module provides classes and types for managing secrets in OpenHands. + +--- + ### class LookupSecret Bases: [`SecretSource`](#class-secretsource) @@ -30,6 +33,9 @@ Get the value of a secret in plain text Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + +--- + ### class SecretSource Bases: `DiscriminatedUnionMixin`, `ABC` @@ -51,6 +57,9 @@ Get the value of a secret in plain text Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + +--- + ### class StaticSecret Bases: [`SecretSource`](#class-secretsource) diff --git a/sdk/api-reference/openhands.sdk.security.mdx b/sdk/api-reference/openhands.sdk.security.mdx index a504b7285..62ac54f14 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) @@ -100,10 +112,11 @@ Environment Variables: #### Example -#### Example -from openhands.sdk.security.grayswan import GraySwanAnalyzer -analyzer = GraySwanAnalyzer() -risk = analyzer.security_risk(action_event) +```pycon +>>> from openhands.sdk.security.grayswan import GraySwanAnalyzer +>>> analyzer = GraySwanAnalyzer() +>>> risk = analyzer.security_risk(action_event) +``` #### Properties @@ -155,6 +168,9 @@ Set the events for context when analyzing actions. Validate that thresholds are properly ordered. + +--- + ### class LLMSecurityAnalyzer Bases: [`SecurityAnalyzerBase`](#class-securityanalyzerbase) @@ -181,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) @@ -205,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` @@ -273,6 +295,9 @@ and confirmation mode settings. * Returns: True if confirmation is required, False otherwise + +--- + ### class SecurityRisk Bases: `str`, `Enum` diff --git a/sdk/api-reference/openhands.sdk.skills.mdx b/sdk/api-reference/openhands.sdk.skills.mdx index de8466099..019d2ff3b 100644 --- a/sdk/api-reference/openhands.sdk.skills.mdx +++ b/sdk/api-reference/openhands.sdk.skills.mdx @@ -6,6 +6,9 @@ description: API reference for openhands.sdk.skills module Skill management utilities for OpenHands SDK. + +--- + ### class InstalledSkillInfo Bases: `BaseModel` @@ -38,6 +41,9 @@ Create InstalledSkillInfo from a loaded Skill. Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. + +--- + ### class InstalledSkillsMetadata Bases: `BaseModel` diff --git a/sdk/api-reference/openhands.sdk.subagent.mdx b/sdk/api-reference/openhands.sdk.subagent.mdx index 16056bb75..8f436f940 100644 --- a/sdk/api-reference/openhands.sdk.subagent.mdx +++ b/sdk/api-reference/openhands.sdk.subagent.mdx @@ -4,6 +4,9 @@ description: API reference for openhands.sdk.subagent module --- + +--- + ### class AgentDefinition Bases: `BaseModel` diff --git a/sdk/api-reference/openhands.sdk.tool.mdx b/sdk/api-reference/openhands.sdk.tool.mdx index e94d04146..929adf2cc 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]` @@ -66,6 +75,9 @@ Create FinishTool instance. #### name = 'finish' + +--- + ### class Observation Bases: `Schema`, `ABC` @@ -105,6 +117,9 @@ Utility to create an Observation from a simple text string. * Returns: An Observation instance with the text wrapped in a TextContent. + +--- + ### class ThinkTool Bases: `ToolDefinition[ThinkAction, ThinkObservation]` @@ -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` @@ -215,6 +239,7 @@ Complex tool with initialization parameters: : working_dir=conv_state.workspace.working_dir, ``` ** + ``` params, ) @@ -332,6 +357,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 86a482980..aba53890c 100644 --- a/sdk/api-reference/openhands.sdk.utils.mdx +++ b/sdk/api-reference/openhands.sdk.utils.mdx @@ -60,6 +60,7 @@ conversation_service.search_conversations, ): print(conversation.title) +``` ### sanitize_openhands_mentions() @@ -78,12 +79,13 @@ preserving readability. The original case of the mention is preserved. ### Examples ```pycon -sanitize_openhands_mentions("Thanks @OpenHands for the help!") +>>> sanitize_openhands_mentions("Thanks @OpenHands for the help!") 'Thanks @u200dOpenHands for the help!' -sanitize_openhands_mentions("Check @openhands and @OPENHANDS") +>>> sanitize_openhands_mentions("Check @openhands and @OPENHANDS") 'Check @u200dopenhands and @u200dOPENHANDS' -sanitize_openhands_mentions("No mention here") +>>> sanitize_openhands_mentions("No mention here") 'No mention here' +``` ### sanitized_env() diff --git a/sdk/api-reference/openhands.sdk.workspace.mdx b/sdk/api-reference/openhands.sdk.workspace.mdx index e5969efe3..1fe71180b 100644 --- a/sdk/api-reference/openhands.sdk.workspace.mdx +++ b/sdk/api-reference/openhands.sdk.workspace.mdx @@ -4,6 +4,9 @@ description: API reference for openhands.sdk.workspace module --- + +--- + ### class AsyncRemoteWorkspace Bases: `RemoteWorkspaceMixin` @@ -107,6 +110,9 @@ 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` @@ -119,10 +125,11 @@ support the context manager protocol for safe resource management. #### Example -#### Example -with workspace: +```pycon +>>> with workspace: ... result = workspace.execute_command("echo 'hello'") ... content = workspace.read_file("example.txt") +``` #### Properties @@ -225,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` @@ -246,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` @@ -267,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) @@ -279,11 +295,12 @@ should operate directly on the host system. #### Example -#### Example -workspace = LocalWorkspace(working_dir="/path/to/project") -with workspace: +```pycon +>>> workspace = LocalWorkspace(working_dir="/path/to/project") +>>> with workspace: ... result = workspace.execute_command("ls -la") ... content = workspace.read_file("README.md") +``` #### Methods @@ -387,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) @@ -399,14 +419,15 @@ as it provides better isolation and security. #### Example -#### Example -workspace = RemoteWorkspace( +```pycon +>>> workspace = RemoteWorkspace( ... host="https://agent-server.example.com", ... working_dir="/workspace" ... ) -with workspace: +>>> with workspace: ... result = workspace.execute_command("ls -la") ... content = workspace.read_file("README.md") +``` #### Properties @@ -515,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` From 35ff38b485f41bb26573e92630f6979e6bc9c7e0 Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 15 Mar 2026 19:41:26 +0000 Subject: [PATCH 7/8] Fix shell config examples being interpreted as markdown headers Add fix_shell_config_examples() to wrap shell-style configuration blocks (KEY=VALUE with # comments) in bash code blocks, preventing # comments from being rendered as markdown headers. Co-authored-by: openhands --- scripts/generate-api-docs.py | 73 +++++++++++++++++++ sdk/api-reference/openhands.sdk.context.mdx | 2 +- sdk/api-reference/openhands.sdk.critic.mdx | 3 +- sdk/api-reference/openhands.sdk.hooks.mdx | 1 - sdk/api-reference/openhands.sdk.llm.mdx | 2 +- .../openhands.sdk.observability.mdx | 2 + sdk/api-reference/openhands.sdk.secret.mdx | 2 +- sdk/api-reference/openhands.sdk.tool.mdx | 2 + 8 files changed, 81 insertions(+), 6 deletions(-) diff --git a/scripts/generate-api-docs.py b/scripts/generate-api-docs.py index 16cda2706..4b9c91e30 100755 --- a/scripts/generate-api-docs.py +++ b/scripts/generate-api-docs.py @@ -382,6 +382,75 @@ def fix_example_blocks(self, content: str) -> str: 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. @@ -688,6 +757,10 @@ def clean_markdown_content(self, content: str, filename: str) -> str: # 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('
', '') diff --git a/sdk/api-reference/openhands.sdk.context.mdx b/sdk/api-reference/openhands.sdk.context.mdx index 6bf771e24..d5c9acd4a 100644 --- a/sdk/api-reference/openhands.sdk.context.mdx +++ b/sdk/api-reference/openhands.sdk.context.mdx @@ -302,4 +302,4 @@ These skills are activated for specific task types and can modify prompts. #### model_config = (configuration object) -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +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.critic.mdx b/sdk/api-reference/openhands.sdk.critic.mdx index 021e77ef6..6d406f711 100644 --- a/sdk/api-reference/openhands.sdk.critic.mdx +++ b/sdk/api-reference/openhands.sdk.critic.mdx @@ -177,7 +177,6 @@ Configuration for the model, should be a dictionary conforming to [ConfigDict][p 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. @@ -240,4 +239,4 @@ Always evaluate as successful. #### model_config = (configuration object) -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +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.hooks.mdx b/sdk/api-reference/openhands.sdk.hooks.mdx index 04c3ee326..2fcc7c624 100644 --- a/sdk/api-reference/openhands.sdk.hooks.mdx +++ b/sdk/api-reference/openhands.sdk.hooks.mdx @@ -17,7 +17,6 @@ during agent execution, enabling deterministic control over agent behavior. Bases: `BaseModel` Configuration for all hooks. - Hooks can be configured either by loading from .openhands/hooks.json or by directly instantiating with typed fields: diff --git a/sdk/api-reference/openhands.sdk.llm.mdx b/sdk/api-reference/openhands.sdk.llm.mdx index 21ad14c1a..3e08c0c63 100644 --- a/sdk/api-reference/openhands.sdk.llm.mdx +++ b/sdk/api-reference/openhands.sdk.llm.mdx @@ -1084,4 +1084,4 @@ Metric tracking detailed token usage per completion call. #### model_config = (configuration object) -Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. +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.observability.mdx b/sdk/api-reference/openhands.sdk.observability.mdx index 867ebbc34..60c15f6e8 100644 --- a/sdk/api-reference/openhands.sdk.observability.mdx +++ b/sdk/api-reference/openhands.sdk.observability.mdx @@ -9,6 +9,7 @@ description: API reference for openhands.sdk.observability module 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 @@ -18,5 +19,6 @@ OTEL_EXPORTER_OTLP_TRACES_HEADERS=”Authorization=Bearer%20``,X-Key=` Date: Sun, 15 Mar 2026 19:46:36 +0000 Subject: [PATCH 8/8] Nest API Reference modules under collapsed Python SDK section - API Reference (collapsed) > Python SDK (collapsed) > modules - Provides better organization for future expansion (e.g., REST API docs) Co-authored-by: openhands --- docs.json | 44 ++++++++++++++++++++---------------- scripts/generate-api-docs.py | 18 +++++++++++++-- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/docs.json b/docs.json index 19ce6e3a9..ba0f48d4b 100644 --- a/docs.json +++ b/docs.json @@ -386,25 +386,31 @@ "group": "API Reference", "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" + { + "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 4b9c91e30..490434506 100755 --- a/scripts/generate-api-docs.py +++ b/scripts/generate-api-docs.py @@ -1201,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