From 6e2d60100b67d82e695f55aba0ba190bd822a53b Mon Sep 17 00:00:00 2001 From: enyst Date: Sun, 26 Oct 2025 19:42:49 +0000 Subject: [PATCH 1/7] docs: runtime images and conda channel hosting; document OH_CONDA_CHANNEL_ALIAS usage\n\nCo-authored-by: openhands --- .../runtime/runtime-images-and-conda.mdx | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 openhands/runtime/runtime-images-and-conda.mdx diff --git a/openhands/runtime/runtime-images-and-conda.mdx b/openhands/runtime/runtime-images-and-conda.mdx new file mode 100644 index 00000000..ca76cfd4 --- /dev/null +++ b/openhands/runtime/runtime-images-and-conda.mdx @@ -0,0 +1,37 @@ +--- +title: Runtime images and conda channel hosting +--- + +This page explains when OpenHands runtime builds may contact anaconda.org and how to avoid it. + +Overview +- OpenHands runtime images are built with micromamba and we install Python and Poetry from the conda-forge channel +- Even when using only conda-forge, the default channel_alias expands conda-forge to https://conda.anaconda.org/conda-forge, which is hosted under anaconda.org + +Two usage paths +1) Use prebuilt runtime images +- No conda network activity occurs during normal use; no contact with anaconda.org on your side +- If your organization must avoid any anaconda.org contact entirely, build your own runtime image instead (path 2) and configure OpenHands to use it + +2) Build runtime images yourself +- As of PR #11516, you can override where the conda-forge channel resolves by setting an environment variable before building: + + export OH_CONDA_CHANNEL_ALIAS=https://repo.prefix.dev + # then run your standard OpenHands build/run flow that triggers runtime image build + +- With this set, our Dockerfile sets: + micromamba config set channel_alias https://repo.prefix.dev + +- We continue to reference -c conda-forge for packages, which will resolve under the configured host (e.g., repo.prefix.dev) instead of anaconda.org + +- You can point channel_alias to: + - A public alternative such as https://repo.prefix.dev + - An internal corporate mirror/proxy (e.g., Artifactory) that fronts conda-forge + +Notes +- We remove the defaults channel, so no packages are pulled from Anaconda's defaults repo +- However, without channel_alias, conda-forge still resolves under the anaconda.org domain by default, which is why you may see requests there + +References +- Conda channels and alias docs: https://www.anaconda.com/docs/getting-started/working-with-conda/channels +- Background: https://github.com/conda-forge/miniforge/issues/784 From fe736ff13e47cfb8debea3795271b84deb8c44a3 Mon Sep 17 00:00:00 2001 From: all-hands-bot Date: Sun, 26 Oct 2025 19:43:21 +0000 Subject: [PATCH 2/7] sync(openapi): agent-sdk/main 1f905f6 --- openapi/agent-sdk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi/agent-sdk.json b/openapi/agent-sdk.json index d818e244..8d814858 100644 --- a/openapi/agent-sdk.json +++ b/openapi/agent-sdk.json @@ -5171,7 +5171,7 @@ "version": { "type": "string", "title": "Version", - "default": "1.0.0a3" + "default": "1.0.0a4" }, "docs": { "type": "string", From 651db6f8fe14a534182ec0867c9f3e8bf3bfaa0b Mon Sep 17 00:00:00 2001 From: Engel Nyst Date: Sat, 10 Jan 2026 17:03:24 +0100 Subject: [PATCH 3/7] Update openapi/agent-sdk.json --- openapi/agent-sdk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi/agent-sdk.json b/openapi/agent-sdk.json index 8d814858..d818e244 100644 --- a/openapi/agent-sdk.json +++ b/openapi/agent-sdk.json @@ -5171,7 +5171,7 @@ "version": { "type": "string", "title": "Version", - "default": "1.0.0a4" + "default": "1.0.0a3" }, "docs": { "type": "string", From d96d3c2493379d5e45a7c618216bbb70c8031500 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Jan 2026 16:03:36 +0000 Subject: [PATCH 4/7] docs: sync code blocks from agent-sdk examples Synced from agent-sdk ref: main --- sdk/getting-started.mdx | 34 +++---- sdk/guides/agent-browser-use.mdx | 16 ++-- sdk/guides/agent-custom.mdx | 6 +- sdk/guides/agent-interactive-terminal.mdx | 9 +- sdk/guides/agent-server/api-sandbox.mdx | 15 +++- sdk/guides/agent-server/docker-sandbox.mdx | 68 +++++++++----- sdk/guides/agent-server/local-server.mdx | 10 ++- sdk/guides/agent-stuck-detector.mdx | 6 +- sdk/guides/context-condenser.mdx | 21 +++-- sdk/guides/convo-async.mdx | 19 ++-- sdk/guides/convo-pause-and-resume.mdx | 26 +++--- sdk/guides/convo-persistence.mdx | 16 ++-- .../convo-send-message-while-running.mdx | 16 ++-- sdk/guides/custom-tools.mdx | 90 +++++++++++++------ sdk/guides/hello-world.mdx | 34 +++---- sdk/guides/llm-image-input.mdx | 22 +++-- sdk/guides/llm-reasoning.mdx | 20 +++-- sdk/guides/llm-registry.mdx | 25 +++--- sdk/guides/llm-routing.mdx | 7 +- sdk/guides/mcp.mdx | 30 +++---- sdk/guides/metrics.mdx | 61 +++++++------ sdk/guides/secrets.mdx | 18 ++-- sdk/guides/security.mdx | 54 ++++++----- sdk/guides/skill.mdx | 48 +++++++--- 24 files changed, 395 insertions(+), 276 deletions(-) diff --git a/sdk/getting-started.mdx b/sdk/getting-started.mdx index 26aeb9db..969c4f61 100644 --- a/sdk/getting-started.mdx +++ b/sdk/getting-started.mdx @@ -74,33 +74,33 @@ Here's a complete example that creates an agent and asks it to perform a simple ```python icon="python" expandable examples/01_standalone_sdk/01_hello_world.py import os -from pydantic import SecretStr +from openhands.sdk import LLM, Agent, Conversation, Tool +from openhands.tools.file_editor import FileEditorTool +from openhands.tools.task_tracker import TaskTrackerTool +from openhands.tools.terminal import TerminalTool -from openhands.sdk import LLM, Conversation -from openhands.tools.preset.default import get_default_agent - -# Configure LLM and agent -# You can get an API key from https://app.all-hands.dev/settings/api-keys -api_key = os.getenv("LLM_API_KEY") -assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") -base_url = os.getenv("LLM_BASE_URL") llm = LLM( - model=model, - api_key=SecretStr(api_key), - base_url=base_url, - usage_id="agent", + model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"), + api_key=os.getenv("LLM_API_KEY"), + base_url=os.getenv("LLM_BASE_URL", None), +) + +agent = Agent( + llm=llm, + tools=[ + Tool(name=TerminalTool.name), + Tool(name=FileEditorTool.name), + Tool(name=TaskTrackerTool.name), + ], ) -agent = get_default_agent(llm=llm, cli_mode=True) -# Start a conversation and send some messages cwd = os.getcwd() conversation = Conversation(agent=agent, workspace=cwd) -# Send a message and let the agent run conversation.send_message("Write 3 facts about the current project into FACTS.txt.") conversation.run() +print("All done!") ``` Run the example: diff --git a/sdk/guides/agent-browser-use.mdx b/sdk/guides/agent-browser-use.mdx index d5e70de7..bc732540 100644 --- a/sdk/guides/agent-browser-use.mdx +++ b/sdk/guides/agent-browser-use.mdx @@ -22,10 +22,10 @@ from openhands.sdk import ( LLMConvertibleEvent, get_logger, ) -from openhands.sdk.tool import Tool, register_tool +from openhands.sdk.tool import Tool from openhands.tools.browser_use import BrowserToolSet -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -33,7 +33,7 @@ logger = get_logger(__name__) # Configure LLM api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( usage_id="agent", @@ -44,15 +44,12 @@ llm = LLM( # Tools cwd = os.getcwd() -register_tool("BashTool", BashTool) -register_tool("FileEditorTool", FileEditorTool) -register_tool("BrowserToolSet", BrowserToolSet) tools = [ Tool( - name="BashTool", + name=TerminalTool.name, ), - Tool(name="FileEditorTool"), - Tool(name="BrowserToolSet"), + Tool(name=FileEditorTool.name), + Tool(name=BrowserToolSet.name), ] # If you need fine-grained browser control, you can manually register individual browser @@ -80,7 +77,6 @@ conversation.send_message( ) conversation.run() - print("=" * 100) print("Conversation finished. Got the following LLM messages:") for i, message in enumerate(llm_messages): diff --git a/sdk/guides/agent-custom.mdx b/sdk/guides/agent-custom.mdx index 95197e2f..d83e4099 100644 --- a/sdk/guides/agent-custom.mdx +++ b/sdk/guides/agent-custom.mdx @@ -53,7 +53,7 @@ print(f"Working in: {workspace_dir}") # Configure LLM api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( model=model, @@ -146,6 +146,10 @@ print("\nCreated files:") for file_path in workspace_dir.rglob("*"): if file_path.is_file(): print(f" - {file_path.relative_to(workspace_dir)}") + +# Report cost +cost = llm.metrics.accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example diff --git a/sdk/guides/agent-interactive-terminal.mdx b/sdk/guides/agent-interactive-terminal.mdx index 33d47c63..b366cad0 100644 --- a/sdk/guides/agent-interactive-terminal.mdx +++ b/sdk/guides/agent-interactive-terminal.mdx @@ -23,8 +23,8 @@ from openhands.sdk import ( LLMConvertibleEvent, get_logger, ) -from openhands.sdk.tool import Tool, register_tool -from openhands.tools.execute_bash import BashTool +from openhands.sdk.tool import Tool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -32,7 +32,7 @@ logger = get_logger(__name__) # Configure LLM api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( usage_id="agent", @@ -43,10 +43,9 @@ llm = LLM( # Tools cwd = os.getcwd() -register_tool("BashTool", BashTool) tools = [ Tool( - name="BashTool", + name=TerminalTool.name, params={"no_change_timeout_seconds": 3}, ) ] diff --git a/sdk/guides/agent-server/api-sandbox.mdx b/sdk/guides/agent-server/api-sandbox.mdx index c58b0c1b..cfd075e0 100644 --- a/sdk/guides/agent-server/api-sandbox.mdx +++ b/sdk/guides/agent-server/api-sandbox.mdx @@ -50,7 +50,7 @@ assert api_key, "LLM_API_KEY required" llm = LLM( usage_id="agent", - model="litellm_proxy/anthropic/claude-sonnet-4-5-20250929", + model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"), base_url=os.getenv("LLM_BASE_URL"), api_key=SecretStr(api_key), ) @@ -61,10 +61,17 @@ if not runtime_api_key: exit(1) +# If GITHUB_SHA is set (e.g. running in CI of a PR), use that to ensure consistency +# Otherwise, use the latest image from main +server_image_sha = os.getenv("GITHUB_SHA") or "main" +server_image = f"ghcr.io/openhands/agent-server:{server_image_sha[:7]}-python-amd64" +logger.info(f"Using server image: {server_image}") + with APIRemoteWorkspace( runtime_api_url=os.getenv("RUNTIME_API_URL", "https://runtime.eval.all-hands.dev"), runtime_api_key=runtime_api_key, - server_image="ghcr.io/openhands/agent-server:main-python", + server_image=server_image, + image_pull_policy="Always", ) as workspace: agent = get_default_agent(llm=llm, cli_mode=True) received_events: list = [] @@ -80,7 +87,7 @@ with APIRemoteWorkspace( logger.info(f"Command completed: {result.exit_code}, {result.stdout}") conversation = Conversation( - agent=agent, workspace=workspace, callbacks=[event_callback], visualize=True + agent=agent, workspace=workspace, callbacks=[event_callback] ) assert isinstance(conversation, RemoteConversation) @@ -95,6 +102,8 @@ with APIRemoteWorkspace( conversation.send_message("Great! Now delete that file.") conversation.run() + cost = conversation.conversation_stats.get_combined_metrics().accumulated_cost + print(f"EXAMPLE_COST: {cost}") finally: conversation.close() ``` diff --git a/sdk/guides/agent-server/docker-sandbox.mdx b/sdk/guides/agent-server/docker-sandbox.mdx index 6d76bde0..72835f1c 100644 --- a/sdk/guides/agent-server/docker-sandbox.mdx +++ b/sdk/guides/agent-server/docker-sandbox.mdx @@ -36,14 +36,13 @@ from openhands.workspace import DockerWorkspace logger = get_logger(__name__) - # 1) Ensure we have LLM API key api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." llm = LLM( usage_id="agent", - model="litellm_proxy/anthropic/claude-sonnet-4-5-20250929", + model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"), base_url=os.getenv("LLM_BASE_URL"), api_key=SecretStr(api_key), ) @@ -58,12 +57,17 @@ def detect_platform(): # 2) Create a Docker-based remote workspace that will set up and manage -# the Docker container automatically +# the Docker container automatically. Use `DockerWorkspace` with a pre-built +# image or `DockerDevWorkspace` to automatically build the image on-demand. +# with DockerDevWorkspace( +# # dynamically build agent-server image +# base_image="nikolaik/python-nodejs:python3.12-nodejs22", +# host_port=8010, +# platform=detect_platform(), +# ) as workspace: with DockerWorkspace( - # dynamically build agent-server image - # base_image="nikolaik/python-nodejs:python3.12-nodejs22", # use pre-built image for faster startup - server_image="ghcr.io/openhands/agent-server:main-python", + server_image="ghcr.io/openhands/agent-server:latest-python", host_port=8010, platform=detect_platform(), ) as workspace: @@ -95,7 +99,6 @@ with DockerWorkspace( agent=agent, workspace=workspace, callbacks=[event_callback], - visualize=True, ) assert isinstance(conversation, RemoteConversation) @@ -109,7 +112,7 @@ with DockerWorkspace( logger.info("🚀 Running conversation...") conversation.run() logger.info("✅ First task completed!") - logger.info(f"Agent status: {conversation.state.agent_status}") + logger.info(f"Agent status: {conversation.state.execution_status}") # Wait for events to settle (no events for 2 seconds) logger.info("⏳ Waiting for events to stop...") @@ -121,6 +124,9 @@ with DockerWorkspace( conversation.send_message("Great! Now delete that file.") conversation.run() logger.info("✅ Second task completed!") + + cost = conversation.conversation_stats.get_combined_metrics().accumulated_cost + print(f"EXAMPLE_COST: {cost}") finally: print("\n🧹 Cleaning up conversation...") conversation.close() @@ -445,13 +451,12 @@ from openhands.workspace import DockerWorkspace logger = get_logger(__name__) - api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." llm = LLM( usage_id="agent", - model="litellm_proxy/anthropic/claude-sonnet-4-5-20250929", + model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"), base_url=os.getenv("LLM_BASE_URL"), api_key=SecretStr(api_key), ) @@ -465,10 +470,18 @@ def detect_platform(): return "linux/amd64" -# Create a Docker-based remote workspace with extra ports for browser access +# Create a Docker-based remote workspace with extra ports for browser access. +# Use `DockerWorkspace` with a pre-built image or `DockerDevWorkspace` to +# automatically build the image on-demand. +# with DockerDevWorkspace( +# # dynamically build agent-server image +# base_image="nikolaik/python-nodejs:python3.12-nodejs22", +# host_port=8010, +# platform=detect_platform(), +# ) as workspace: with DockerWorkspace( - base_image="nikolaik/python-nodejs:python3.12-nodejs22", - host_port=8010, + server_image="ghcr.io/openhands/agent-server:latest-python", + host_port=8011, platform=detect_platform(), extra_ports=True, # Expose extra ports for VSCode and VNC ) as workspace: @@ -495,7 +508,6 @@ with DockerWorkspace( agent=agent, workspace=workspace, callbacks=[event_callback], - visualize=True, ) assert isinstance(conversation, RemoteConversation) @@ -507,17 +519,25 @@ with DockerWorkspace( ) conversation.run() - # Wait for user confirm to exit - y = None - while y != "y": - y = input( - "Because you've enabled extra_ports=True in DockerWorkspace, " - "you can open a browser tab to see the *actual* browser OpenHands " - "is interacting with via VNC.\n\n" - "Link: http://localhost:8012/vnc.html?autoconnect=1&resize=remote\n\n" - "Press 'y' and Enter to exit and terminate the workspace.\n" - ">> " + cost = conversation.conversation_stats.get_combined_metrics().accumulated_cost + print(f"EXAMPLE_COST: {cost}") + + if os.getenv("CI"): + logger.info( + "CI environment detected; skipping interactive prompt and closing workspace." # noqa: E501 ) + else: + # Wait for user confirm to exit when running locally + y = None + while y != "y": + y = input( + "Because you've enabled extra_ports=True in DockerDevWorkspace, " + "you can open a browser tab to see the *actual* browser OpenHands " + "is interacting with via VNC.\n\n" + "Link: http://localhost:8012/vnc.html?autoconnect=1&resize=remote\n\n" + "Press 'y' and Enter to exit and terminate the workspace.\n" + ">> " + ) ``` ```bash Running the Example diff --git a/sdk/guides/agent-server/local-server.mdx b/sdk/guides/agent-server/local-server.mdx index cbde3b3e..73c4e84e 100644 --- a/sdk/guides/agent-server/local-server.mdx +++ b/sdk/guides/agent-server/local-server.mdx @@ -142,13 +142,13 @@ assert api_key is not None, "LLM_API_KEY environment variable is not set." llm = LLM( usage_id="agent", - model="litellm_proxy/anthropic/claude-sonnet-4-5-20250929", + model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"), base_url=os.getenv("LLM_BASE_URL"), api_key=SecretStr(api_key), ) title_gen_llm = LLM( usage_id="title-gen-llm", - model="litellm_proxy/openai/gpt-5-mini", + model=os.getenv("LLM_MODEL", "openhands/gpt-5-mini-2025-08-07"), base_url=os.getenv("LLM_BASE_URL"), api_key=SecretStr(api_key), ) @@ -185,7 +185,6 @@ with ManagedAPIServer(port=8001) as server: agent=agent, workspace=workspace, callbacks=[event_callback], - visualize=True, ) assert isinstance(conversation, RemoteConversation) @@ -206,7 +205,7 @@ with ManagedAPIServer(port=8001) as server: conversation.run() logger.info("✅ First task completed!") - logger.info(f"Agent status: {conversation.state.agent_status}") + logger.info(f"Agent status: {conversation.state.execution_status}") # Wait for events to stop coming (no events for 2 seconds) logger.info("⏳ Waiting for events to stop...") @@ -253,6 +252,9 @@ with ManagedAPIServer(port=8001) as server: if isinstance(event, ConversationStateUpdateEvent): logger.info(f" - {event}") + cost = conversation.conversation_stats.get_combined_metrics().accumulated_cost + print(f"EXAMPLE_COST: {cost}") + finally: # Clean up print("\n🧹 Cleaning up conversation...") diff --git a/sdk/guides/agent-stuck-detector.mdx b/sdk/guides/agent-stuck-detector.mdx index e1494ae3..e3c52bfb 100644 --- a/sdk/guides/agent-stuck-detector.mdx +++ b/sdk/guides/agent-stuck-detector.mdx @@ -40,7 +40,7 @@ logger = get_logger(__name__) # Configure LLM api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( usage_id="agent", @@ -85,6 +85,10 @@ print("=" * 100) print("Conversation finished. Got the following LLM messages:") for i, message in enumerate(llm_messages): print(f"Message {i}: {str(message)[:200]}") + +# Report cost +cost = llm.metrics.accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example diff --git a/sdk/guides/context-condenser.mdx b/sdk/guides/context-condenser.mdx index c06a333c..14e18cdb 100644 --- a/sdk/guides/context-condenser.mdx +++ b/sdk/guides/context-condenser.mdx @@ -76,10 +76,10 @@ from openhands.sdk import ( get_logger, ) from openhands.sdk.context.condenser import LLMSummarizingCondenser -from openhands.sdk.tool import Tool, register_tool -from openhands.tools.execute_bash import BashTool +from openhands.sdk.tool import Tool from openhands.tools.file_editor import FileEditorTool from openhands.tools.task_tracker import TaskTrackerTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -87,7 +87,7 @@ logger = get_logger(__name__) # Configure LLM api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( usage_id="agent", @@ -98,15 +98,12 @@ llm = LLM( # Tools cwd = os.getcwd() -register_tool("BashTool", BashTool) -register_tool("FileEditorTool", FileEditorTool) -register_tool("TaskTrackerTool", TaskTrackerTool) tools = [ Tool( - name="BashTool", + name=TerminalTool.name, ), - Tool(name="FileEditorTool"), - Tool(name="TaskTrackerTool"), + Tool(name=FileEditorTool.name), + Tool(name=TaskTrackerTool.name), ] # Create a condenser to manage the context. The condenser will automatically truncate @@ -163,7 +160,6 @@ conversation.send_message( ) conversation.run() - print("=" * 100) print("Conversation finished. Got the following LLM messages:") for i, message in enumerate(llm_messages): @@ -187,13 +183,16 @@ print("Sending message to deserialized conversation...") conversation.send_message("Finally, clean up by deleting both files.") conversation.run() - print("=" * 100) print("Conversation finished with LLM Summarizing Condenser.") print(f"Total LLM messages collected: {len(llm_messages)}") print("\nThe condenser automatically summarized older conversation history") print("when the conversation exceeded the configured max_size threshold.") print("This helps manage context length while preserving important information.") + +# Report cost +cost = conversation.conversation_stats.get_combined_metrics().accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example diff --git a/sdk/guides/convo-async.mdx b/sdk/guides/convo-async.mdx index e1021396..f31b690d 100644 --- a/sdk/guides/convo-async.mdx +++ b/sdk/guides/convo-async.mdx @@ -30,11 +30,11 @@ from openhands.sdk import ( get_logger, ) from openhands.sdk.conversation.types import ConversationCallbackType -from openhands.sdk.tool import Tool, register_tool +from openhands.sdk.tool import Tool from openhands.sdk.utils.async_utils import AsyncCallbackWrapper -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool from openhands.tools.task_tracker import TaskTrackerTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -42,7 +42,7 @@ logger = get_logger(__name__) # Configure LLM api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( usage_id="agent", @@ -53,15 +53,12 @@ llm = LLM( # Tools cwd = os.getcwd() -register_tool("BashTool", BashTool) -register_tool("FileEditorTool", FileEditorTool) -register_tool("TaskTrackerTool", TaskTrackerTool) tools = [ Tool( - name="BashTool", + name=TerminalTool.name, ), - Tool(name="FileEditorTool"), - Tool(name="TaskTrackerTool"), + Tool(name=FileEditorTool.name), + Tool(name=TaskTrackerTool.name), ] # Agent @@ -104,6 +101,10 @@ async def main(): for i, message in enumerate(llm_messages): print(f"Message {i}: {str(message)[:200]}") + # Report cost + cost = llm.metrics.accumulated_cost + print(f"EXAMPLE_COST: {cost}") + if __name__ == "__main__": asyncio.run(main()) diff --git a/sdk/guides/convo-pause-and-resume.mdx b/sdk/guides/convo-pause-and-resume.mdx index b4768998..e1e1b7fd 100644 --- a/sdk/guides/convo-pause-and-resume.mdx +++ b/sdk/guides/convo-pause-and-resume.mdx @@ -22,15 +22,15 @@ from openhands.sdk import ( Agent, Conversation, ) -from openhands.sdk.tool import Tool, register_tool -from openhands.tools.execute_bash import BashTool +from openhands.sdk.tool import Tool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool # Configure LLM api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( usage_id="agent", @@ -40,20 +40,17 @@ llm = LLM( ) # Tools -register_tool("BashTool", BashTool) -register_tool("FileEditorTool", FileEditorTool) tools = [ Tool( - name="BashTool", + name=TerminalTool.name, ), - Tool(name="FileEditorTool"), + Tool(name=FileEditorTool.name), ] # Agent agent = Agent(llm=llm, tools=tools) conversation = Conversation(agent, workspace=os.getcwd()) - print("=" * 60) print("Pause and Continue Example") print("=" * 60) @@ -66,7 +63,7 @@ conversation.send_message( "one number per line. After you finish, summarize what you did." ) -print(f"Initial status: {conversation.state.agent_status}") +print(f"Initial status: {conversation.state.execution_status}") print() # Start the agent in a background thread @@ -85,10 +82,9 @@ conversation.pause() # Wait for the thread to finish (it will stop when paused) thread.join() -print(f"Agent status after pause: {conversation.state.agent_status}") +print(f"Agent status after pause: {conversation.state.execution_status}") print() - # Phase 3: Send a new message while paused print("Phase 3: Sending a new message while agent is paused...") conversation.send_message( @@ -99,12 +95,16 @@ print() # Phase 4: Resume the agent with .run() print("Phase 4: Resuming agent with .run()...") -print(f"Status before resume: {conversation.state.agent_status}") +print(f"Status before resume: {conversation.state.execution_status}") # Resume execution conversation.run() -print(f"Final status: {conversation.state.agent_status}") +print(f"Final status: {conversation.state.execution_status}") + +# Report cost +cost = llm.metrics.accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example diff --git a/sdk/guides/convo-persistence.mdx b/sdk/guides/convo-persistence.mdx index 2523f17f..35b71359 100644 --- a/sdk/guides/convo-persistence.mdx +++ b/sdk/guides/convo-persistence.mdx @@ -23,9 +23,9 @@ from openhands.sdk import ( LLMConvertibleEvent, get_logger, ) -from openhands.sdk.tool import Tool, register_tool -from openhands.tools.execute_bash import BashTool +from openhands.sdk.tool import Tool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -33,7 +33,7 @@ logger = get_logger(__name__) # Configure LLM api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( usage_id="agent", @@ -44,11 +44,9 @@ llm = LLM( # Tools cwd = os.getcwd() -register_tool("BashTool", BashTool) -register_tool("FileEditorTool", FileEditorTool) tools = [ - Tool(name="BashTool"), - Tool(name="FileEditorTool"), + Tool(name=TerminalTool.name), + Tool(name=FileEditorTool.name), ] # Add MCP Tools @@ -110,6 +108,10 @@ conversation = Conversation( print("Sending message to deserialized conversation...") conversation.send_message("Hey what did you create? Return an agent finish action") conversation.run() + +# Report cost +cost = llm.metrics.accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example diff --git a/sdk/guides/convo-send-message-while-running.mdx b/sdk/guides/convo-send-message-while-running.mdx index df3ac386..44b9d175 100644 --- a/sdk/guides/convo-send-message-while-running.mdx +++ b/sdk/guides/convo-send-message-while-running.mdx @@ -62,15 +62,15 @@ from openhands.sdk import ( Agent, Conversation, ) -from openhands.sdk.tool import Tool, register_tool -from openhands.tools.execute_bash import BashTool +from openhands.sdk.tool import Tool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool # Configure LLM api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( usage_id="agent", @@ -81,13 +81,11 @@ llm = LLM( # Tools cwd = os.getcwd() -register_tool("BashTool", BashTool) -register_tool("FileEditorTool", FileEditorTool) tools = [ Tool( - name="BashTool", + name=TerminalTool.name, ), - Tool(name="FileEditorTool"), + Tool(name=FileEditorTool.name), ] # Agent @@ -152,6 +150,10 @@ if os.path.exists(document_path): os.remove(document_path) else: print("WARNING: Document.txt was not created") + +# Report cost +cost = llm.metrics.accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example diff --git a/sdk/guides/custom-tools.mdx b/sdk/guides/custom-tools.mdx index 12a33b46..fc70b47e 100644 --- a/sdk/guides/custom-tools.mdx +++ b/sdk/guides/custom-tools.mdx @@ -64,17 +64,16 @@ from openhands.sdk.tool import ( ToolExecutor, register_tool, ) -from openhands.tools.execute_bash import ( - BashExecutor, - ExecuteBashAction, - execute_bash_tool, -) from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import ( + TerminalAction, + TerminalExecutor, + TerminalTool, +) logger = get_logger(__name__) - # --- Action / Observation --- @@ -112,8 +111,8 @@ class GrepObservation(Observation): class GrepExecutor(ToolExecutor[GrepAction, GrepObservation]): - def __init__(self, bash: BashExecutor): - self.bash: BashExecutor = bash + def __init__(self, terminal: TerminalExecutor): + self.terminal: TerminalExecutor = terminal def __call__(self, action: GrepAction, conversation=None) -> GrepObservation: # noqa: ARG002 root = os.path.abspath(action.path) @@ -127,14 +126,16 @@ class GrepExecutor(ToolExecutor[GrepAction, GrepObservation]): else: cmd = f"grep -rHnE {pat} {root_q} 2>/dev/null | head -100" - result = self.bash(ExecuteBashAction(command=cmd)) + result = self.terminal(TerminalAction(command=cmd)) matches: list[str] = [] files: set[str] = set() # grep returns exit code 1 when no matches; treat as empty - if result.output.strip(): - for line in result.output.strip().splitlines(): + output_text = result.text + + if output_text.strip(): + for line in output_text.strip().splitlines(): matches.append(line) # Expect "path:line:content" — take the file part before first ":" file_path = line.split(":", 1)[0] @@ -155,10 +156,47 @@ _GREP_DESCRIPTION = """Fast content search tool. * When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead """ # noqa: E501 + +# --- Tool Definition --- + + +class GrepTool(ToolDefinition[GrepAction, GrepObservation]): + """A custom grep tool that searches file contents using regular expressions.""" + + @classmethod + def create( + cls, conv_state, terminal_executor: TerminalExecutor | None = None + ) -> Sequence[ToolDefinition]: + """Create GrepTool instance with a GrepExecutor. + + Args: + conv_state: Conversation state to get working directory from. + terminal_executor: Optional terminal executor to reuse. If not provided, + a new one will be created. + + Returns: + A sequence containing a single GrepTool instance. + """ + if terminal_executor is None: + terminal_executor = TerminalExecutor( + working_dir=conv_state.workspace.working_dir + ) + grep_executor = GrepExecutor(terminal_executor) + + return [ + cls( + description=_GREP_DESCRIPTION, + action_type=GrepAction, + observation_type=GrepObservation, + executor=grep_executor, + ) + ] + + # Configure LLM api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( usage_id="agent", @@ -172,28 +210,22 @@ cwd = os.getcwd() def _make_bash_and_grep_tools(conv_state) -> list[ToolDefinition]: - """Create execute_bash and custom grep tools sharing one executor.""" + """Create terminal and custom grep tools sharing one executor.""" - bash_executor = BashExecutor(working_dir=conv_state.workspace.working_dir) - bash_tool = execute_bash_tool.set_executor(executor=bash_executor) - - grep_executor = GrepExecutor(bash_executor) - grep_tool = ToolDefinition( - name="grep", - description=_GREP_DESCRIPTION, - action_type=GrepAction, - observation_type=GrepObservation, - executor=grep_executor, - ) + terminal_executor = TerminalExecutor(working_dir=conv_state.workspace.working_dir) + # terminal_tool = terminal_tool.set_executor(executor=terminal_executor) + terminal_tool = TerminalTool.create(conv_state, executor=terminal_executor)[0] - return [bash_tool, grep_tool] + # Use the GrepTool.create() method with shared terminal_executor + grep_tool = GrepTool.create(conv_state, terminal_executor=terminal_executor)[0] + + return [terminal_tool, grep_tool] -register_tool("FileEditorTool", FileEditorTool) register_tool("BashAndGrepToolSet", _make_bash_and_grep_tools) tools = [ - Tool(name="FileEditorTool"), + Tool(name=FileEditorTool.name), Tool(name="BashAndGrepToolSet"), ] @@ -226,6 +258,10 @@ print("=" * 100) print("Conversation finished. Got the following LLM messages:") for i, message in enumerate(llm_messages): print(f"Message {i}: {str(message)[:200]}") + +# Report cost +cost = llm.metrics.accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example diff --git a/sdk/guides/hello-world.mdx b/sdk/guides/hello-world.mdx index d37adf50..fa890c3e 100644 --- a/sdk/guides/hello-world.mdx +++ b/sdk/guides/hello-world.mdx @@ -12,33 +12,33 @@ This is the most basic example showing how to set up and run an OpenHands agent: ```python icon="python" examples/01_standalone_sdk/01_hello_world.py import os -from pydantic import SecretStr +from openhands.sdk import LLM, Agent, Conversation, Tool +from openhands.tools.file_editor import FileEditorTool +from openhands.tools.task_tracker import TaskTrackerTool +from openhands.tools.terminal import TerminalTool -from openhands.sdk import LLM, Conversation -from openhands.tools.preset.default import get_default_agent - -# Configure LLM and agent -# You can get an API key from https://app.all-hands.dev/settings/api-keys -api_key = os.getenv("LLM_API_KEY") -assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") -base_url = os.getenv("LLM_BASE_URL") llm = LLM( - model=model, - api_key=SecretStr(api_key), - base_url=base_url, - usage_id="agent", + model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"), + api_key=os.getenv("LLM_API_KEY"), + base_url=os.getenv("LLM_BASE_URL", None), +) + +agent = Agent( + llm=llm, + tools=[ + Tool(name=TerminalTool.name), + Tool(name=FileEditorTool.name), + Tool(name=TaskTrackerTool.name), + ], ) -agent = get_default_agent(llm=llm, cli_mode=True) -# Start a conversation and send some messages cwd = os.getcwd() conversation = Conversation(agent=agent, workspace=cwd) -# Send a message and let the agent run conversation.send_message("Write 3 facts about the current project into FACTS.txt.") conversation.run() +print("All done!") ``` ```bash Running the Example diff --git a/sdk/guides/llm-image-input.mdx b/sdk/guides/llm-image-input.mdx index 597c9747..1137830e 100644 --- a/sdk/guides/llm-image-input.mdx +++ b/sdk/guides/llm-image-input.mdx @@ -31,11 +31,10 @@ from openhands.sdk import ( TextContent, get_logger, ) -from openhands.sdk.tool.registry import register_tool from openhands.sdk.tool.spec import Tool -from openhands.tools.execute_bash import BashTool from openhands.tools.file_editor import FileEditorTool from openhands.tools.task_tracker import TaskTrackerTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -43,7 +42,7 @@ logger = get_logger(__name__) # Configure LLM (vision-capable model) api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( usage_id="vision-llm", @@ -55,18 +54,14 @@ assert llm.vision_is_active(), "The selected LLM model does not support vision i cwd = os.getcwd() -register_tool("BashTool", BashTool) -register_tool("FileEditorTool", FileEditorTool) -register_tool("TaskTrackerTool", TaskTrackerTool) - agent = Agent( llm=llm, tools=[ Tool( - name="BashTool", + name=TerminalTool.name, ), - Tool(name="FileEditorTool"), - Tool(name="TaskTrackerTool"), + Tool(name=FileEditorTool.name), + Tool(name=TaskTrackerTool.name), ], ) @@ -82,7 +77,7 @@ conversation = Conversation( agent=agent, callbacks=[conversation_callback], workspace=cwd ) -IMAGE_URL = "https://github.com/OpenHands/OpenHands/raw/main/docs/static/img/logo.png" +IMAGE_URL = "https://github.com/OpenHands/docs/raw/main/openhands/static/img/logo.png" conversation.send_message( Message( @@ -105,11 +100,14 @@ conversation.send_message( ) conversation.run() - print("=" * 100) print("Conversation finished. Got the following LLM messages:") for i, message in enumerate(llm_messages): print(f"Message {i}: {str(message)[:200]}") + +# Report cost +cost = llm.metrics.accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example diff --git a/sdk/guides/llm-reasoning.mdx b/sdk/guides/llm-reasoning.mdx index 9d59db90..3a96566d 100644 --- a/sdk/guides/llm-reasoning.mdx +++ b/sdk/guides/llm-reasoning.mdx @@ -32,14 +32,14 @@ from openhands.sdk import ( RedactedThinkingBlock, ThinkingBlock, ) -from openhands.sdk.tool import Tool, register_tool -from openhands.tools.execute_bash import BashTool +from openhands.sdk.tool import Tool +from openhands.tools.terminal import TerminalTool # Configure LLM for Anthropic Claude with extended thinking api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( @@ -50,8 +50,7 @@ llm = LLM( ) # Setup agent with bash tool -register_tool("BashTool", BashTool) -agent = Agent(llm=llm, tools=[Tool(name="BashTool")]) +agent = Agent(llm=llm, tools=[Tool(name=TerminalTool.name)]) # Callback to display thinking blocks @@ -82,6 +81,10 @@ conversation.send_message( ) conversation.run() print("✅ Done!") + +# Report cost +cost = llm.metrics.accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example @@ -153,11 +156,10 @@ from openhands.tools.preset.default import get_default_agent logger = get_logger(__name__) - api_key = os.getenv("LLM_API_KEY") or os.getenv("OPENAI_API_KEY") assert api_key, "Set LLM_API_KEY or OPENAI_API_KEY in your environment." -model = os.getenv("LLM_MODEL", "openhands/gpt-5-codex") +model = "openhands/gpt-5-mini-2025-08-07" # Use a model that supports Responses API base_url = os.getenv("LLM_BASE_URL") llm = LLM( @@ -203,6 +205,10 @@ print("Conversation finished. Got the following LLM messages:") for i, message in enumerate(llm_messages): ms = str(message) print(f"Message {i}: {ms[:200]}{'...' if len(ms) > 200 else ''}") + +# Report cost +cost = llm.metrics.accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example diff --git a/sdk/guides/llm-registry.mdx b/sdk/guides/llm-registry.mdx index 7c0dee1f..e987fd32 100644 --- a/sdk/guides/llm-registry.mdx +++ b/sdk/guides/llm-registry.mdx @@ -25,8 +25,8 @@ from openhands.sdk import ( TextContent, get_logger, ) -from openhands.sdk.tool import Tool, register_tool -from openhands.tools.execute_bash import BashTool +from openhands.sdk.tool import Tool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -34,7 +34,7 @@ logger = get_logger(__name__) # Configure LLM using LLMRegistry api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") # Create LLM instance @@ -54,8 +54,7 @@ llm = llm_registry.get("agent") # Tools cwd = os.getcwd() -register_tool("BashTool", BashTool) -tools = [Tool(name="BashTool")] +tools = [Tool(name=TerminalTool.name)] # Agent agent = Agent(llm=llm, tools=tools) @@ -88,17 +87,19 @@ same_llm = llm_registry.get("agent") print(f"Same LLM instance: {llm is same_llm}") # Demonstrate requesting a completion directly from an LLM -completion_response = llm.completion( +resp = llm.completion( messages=[ Message(role="user", content=[TextContent(text="Say hello in one word.")]) ] ) -# Access the response content -if completion_response.choices and completion_response.choices[0].message: # type: ignore - content = completion_response.choices[0].message.content # type: ignore - print(f"Direct completion response: {content}") -else: - print("No response content available") +# Access the response content via OpenHands LLMResponse +msg = resp.message +texts = [c.text for c in msg.content if isinstance(c, TextContent)] +print(f"Direct completion response: {texts[0] if texts else str(msg)}") + +# Report cost +cost = llm.metrics.accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example diff --git a/sdk/guides/llm-routing.mdx b/sdk/guides/llm-routing.mdx index 0766af67..b428235a 100644 --- a/sdk/guides/llm-routing.mdx +++ b/sdk/guides/llm-routing.mdx @@ -47,7 +47,7 @@ primary_llm = LLM( ) secondary_llm = LLM( usage_id="agent-secondary", - model="litellm_proxy/mistral/devstral-small-2507", + model="openhands/devstral-small-2507", base_url=base_url, api_key=SecretStr(api_key), ) @@ -103,11 +103,14 @@ conversation.send_message( ) conversation.run() - print("=" * 100) print("Conversation finished. Got the following LLM messages:") for i, message in enumerate(llm_messages): print(f"Message {i}: {str(message)[:200]}") + +# Report cost +cost = conversation.conversation_stats.get_combined_metrics().accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example diff --git a/sdk/guides/mcp.mdx b/sdk/guides/mcp.mdx index 1063dd19..18c36a99 100644 --- a/sdk/guides/mcp.mdx +++ b/sdk/guides/mcp.mdx @@ -28,9 +28,9 @@ from openhands.sdk import ( get_logger, ) from openhands.sdk.security.llm_analyzer import LLMSecurityAnalyzer -from openhands.sdk.tool import Tool, register_tool -from openhands.tools.execute_bash import BashTool +from openhands.sdk.tool import Tool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -38,7 +38,7 @@ logger = get_logger(__name__) # Configure LLM api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( usage_id="agent", @@ -48,11 +48,9 @@ llm = LLM( ) cwd = os.getcwd() -register_tool("BashTool", BashTool) -register_tool("FileEditorTool", FileEditorTool) tools = [ - Tool(name="BashTool"), - Tool(name="FileEditorTool"), + Tool(name=TerminalTool.name), + Tool(name=FileEditorTool.name), ] # Add MCP Tools @@ -69,7 +67,6 @@ agent = Agent( mcp_config=mcp_config, # This regex filters out all repomix tools except pack_codebase filter_tools_regex="^(?!repomix)(.*)|^repomix.*pack_codebase.*$", - security_analyzer=LLMSecurityAnalyzer(), ) llm_messages = [] # collect raw LLM messages @@ -86,6 +83,7 @@ conversation = Conversation( callbacks=[conversation_callback], workspace=cwd, ) +conversation.set_security_analyzer(LLMSecurityAnalyzer()) logger.info("Starting conversation with MCP integration...") conversation.send_message( @@ -101,6 +99,10 @@ print("=" * 100) print("Conversation finished. Got the following LLM messages:") for i, message in enumerate(llm_messages): print(f"Message {i}: {str(message)[:200]}") + +# Report cost +cost = llm.metrics.accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example @@ -158,9 +160,9 @@ from openhands.sdk import ( LLMConvertibleEvent, get_logger, ) -from openhands.sdk.tool import Tool, register_tool -from openhands.tools.execute_bash import BashTool +from openhands.sdk.tool import Tool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -168,7 +170,7 @@ logger = get_logger(__name__) # Configure LLM api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( usage_id="agent", @@ -178,13 +180,11 @@ llm = LLM( ) cwd = os.getcwd() -register_tool("BashTool", BashTool) -register_tool("FileEditorTool", FileEditorTool) tools = [ Tool( - name="BashTool", + name=TerminalTool.name, ), - Tool(name="FileEditorTool"), + Tool(name=FileEditorTool.name), ] mcp_config = { diff --git a/sdk/guides/metrics.mdx b/sdk/guides/metrics.mdx index a960431a..c57ddcf2 100644 --- a/sdk/guides/metrics.mdx +++ b/sdk/guides/metrics.mdx @@ -30,9 +30,9 @@ from openhands.sdk import ( LLMConvertibleEvent, get_logger, ) -from openhands.sdk.tool import Tool, register_tool -from openhands.tools.execute_bash import BashTool +from openhands.sdk.tool import Tool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -40,7 +40,7 @@ logger = get_logger(__name__) # Configure LLM api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( usage_id="agent", @@ -50,11 +50,9 @@ llm = LLM( ) cwd = os.getcwd() -register_tool("BashTool", BashTool) -register_tool("FileEditorTool", FileEditorTool) tools = [ - Tool(name="BashTool"), - Tool(name="FileEditorTool"), + Tool(name=TerminalTool.name), + Tool(name=FileEditorTool.name), ] # Add MCP Tools @@ -97,6 +95,10 @@ assert llm.metrics is not None print( f"Conversation finished. Final LLM metrics with details: {llm.metrics.model_dump()}" ) + +# Report cost +cost = llm.metrics.accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example @@ -156,8 +158,8 @@ from openhands.sdk import ( TextContent, get_logger, ) -from openhands.sdk.tool import Tool, register_tool -from openhands.tools.execute_bash import BashTool +from openhands.sdk.tool import Tool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -165,7 +167,7 @@ logger = get_logger(__name__) # Configure LLM using LLMRegistry api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") # Create LLM instance @@ -185,8 +187,7 @@ llm = llm_registry.get("agent") # Tools cwd = os.getcwd() -register_tool("BashTool", BashTool) -tools = [Tool(name="BashTool")] +tools = [Tool(name=TerminalTool.name)] # Agent agent = Agent(llm=llm, tools=tools) @@ -219,17 +220,19 @@ same_llm = llm_registry.get("agent") print(f"Same LLM instance: {llm is same_llm}") # Demonstrate requesting a completion directly from an LLM -completion_response = llm.completion( +resp = llm.completion( messages=[ Message(role="user", content=[TextContent(text="Say hello in one word.")]) ] ) -# Access the response content -if completion_response.choices and completion_response.choices[0].message: # type: ignore - content = completion_response.choices[0].message.content # type: ignore - print(f"Direct completion response: {content}") -else: - print("No response content available") +# Access the response content via OpenHands LLMResponse +msg = resp.message +texts = [c.text for c in msg.content if isinstance(c, TextContent)] +print(f"Direct completion response: {texts[0] if texts else str(msg)}") + +# Report cost +cost = llm.metrics.accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example @@ -272,11 +275,8 @@ from openhands.sdk import ( TextContent, get_logger, ) -from openhands.sdk.tool.registry import register_tool from openhands.sdk.tool.spec import Tool -from openhands.tools.execute_bash import ( - BashTool, -) +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -284,7 +284,7 @@ logger = get_logger(__name__) # Configure LLM using LLMRegistry api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") # Create LLM instance @@ -303,8 +303,6 @@ llm_condenser = LLM( ) # Tools -register_tool("BashTool", BashTool) - condenser = LLMSummarizingCondenser(llm=llm_condenser, max_size=10, keep_first=2) cwd = os.getcwd() @@ -312,7 +310,7 @@ agent = Agent( llm=llm, tools=[ Tool( - name="BashTool", + name=TerminalTool.name, ), ], condenser=condenser, @@ -327,11 +325,10 @@ conversation.send_message( ) conversation.run() - # Demonstrate extraneous costs part of the conversation second_llm = LLM( usage_id="demo-secondary", - model="litellm_proxy/anthropic/claude-sonnet-4-5-20250929", + model=model, base_url=os.getenv("LLM_BASE_URL"), api_key=SecretStr(api_key), ) @@ -340,7 +337,6 @@ completion_response = second_llm.completion( messages=[Message(role="user", content=[TextContent(text="echo 'More spend!'")])] ) - # Access total spend spend = conversation.conversation_stats.get_combined_metrics() print("\n=== Total Spend for Conversation ===\n") @@ -351,7 +347,6 @@ if spend.accumulated_token_usage: print(f"Cache Read Tokens: {spend.accumulated_token_usage.cache_read_tokens}") print(f"Cache Write Tokens: {spend.accumulated_token_usage.cache_write_tokens}") - spend_per_usage = conversation.conversation_stats.usage_to_metrics print("\n=== Spend Breakdown by Usage ID ===\n") rows = [] @@ -376,6 +371,10 @@ print( tablefmt="github", ) ) + +# Report cost +cost = conversation.conversation_stats.get_combined_metrics().accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example diff --git a/sdk/guides/secrets.mdx b/sdk/guides/secrets.mdx index 505d5a2c..40b487e7 100644 --- a/sdk/guides/secrets.mdx +++ b/sdk/guides/secrets.mdx @@ -19,16 +19,16 @@ from openhands.sdk import ( Agent, Conversation, ) -from openhands.sdk.conversation.secret_source import SecretSource -from openhands.sdk.tool import Tool, register_tool -from openhands.tools.execute_bash import BashTool +from openhands.sdk.secret import SecretSource +from openhands.sdk.tool import Tool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool # Configure LLM api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( usage_id="agent", @@ -38,11 +38,9 @@ llm = LLM( ) # Tools -register_tool("BashTool", BashTool) -register_tool("FileEditorTool", FileEditorTool) tools = [ - Tool(name="BashTool"), - Tool(name="FileEditorTool"), + Tool(name=TerminalTool.name), + Tool(name=FileEditorTool.name), ] # Agent @@ -66,6 +64,10 @@ conversation.run() conversation.send_message("just echo $SECRET_FUNCTION_TOKEN") conversation.run() + +# Report cost +cost = llm.metrics.accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example diff --git a/sdk/guides/security.mdx b/sdk/guides/security.mdx index a0b1601d..ff16d559 100644 --- a/sdk/guides/security.mdx +++ b/sdk/guides/security.mdx @@ -27,8 +27,12 @@ from collections.abc import Callable from pydantic import SecretStr from openhands.sdk import LLM, BaseConversation, Conversation -from openhands.sdk.conversation.state import AgentExecutionStatus, ConversationState +from openhands.sdk.conversation.state import ( + ConversationExecutionStatus, + ConversationState, +) from openhands.sdk.security.confirmation_policy import AlwaysConfirm, NeverConfirm +from openhands.sdk.security.llm_analyzer import LLMSecurityAnalyzer from openhands.tools.preset.default import get_default_agent @@ -76,10 +80,10 @@ def run_until_finished(conversation: BaseConversation, confirmer: Callable) -> N on reject, call reject_pending_actions(). Preserves original error if agent waits but no actions exist. """ - while conversation.state.agent_status != AgentExecutionStatus.FINISHED: + while conversation.state.execution_status != ConversationExecutionStatus.FINISHED: if ( - conversation.state.agent_status - == AgentExecutionStatus.WAITING_FOR_CONFIRMATION + conversation.state.execution_status + == ConversationExecutionStatus.WAITING_FOR_CONFIRMATION ): pending = ConversationState.get_unmatched_actions(conversation.state.events) if not pending: @@ -99,7 +103,7 @@ def run_until_finished(conversation: BaseConversation, confirmer: Callable) -> N # Configure LLM api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( usage_id="agent", @@ -108,11 +112,14 @@ llm = LLM( api_key=SecretStr(api_key), ) +agent = get_default_agent(llm=llm) +conversation = Conversation(agent=agent, workspace=os.getcwd()) + +# Conditionally add security analyzer based on environment variable add_security_analyzer = bool(os.getenv("ADD_SECURITY_ANALYZER", "").strip()) if add_security_analyzer: print("Agent security analyzer added.") -agent = get_default_agent(llm=llm, add_security_analyzer=add_security_analyzer) -conversation = Conversation(agent=agent, workspace=os.getcwd()) + conversation.set_security_analyzer(LLMSecurityAnalyzer()) # 1) Confirmation mode ON conversation.set_confirmation_policy(AlwaysConfirm()) @@ -145,7 +152,7 @@ print("\n=== Example Complete ===") print("Key points:") print( "- conversation.run() creates actions; confirmation mode " - "sets agent_status=WAITING_FOR_CONFIRMATION" + "sets execution_status=WAITING_FOR_CONFIRMATION" ) print("- User confirmation is handled via a single reusable function") print("- Rejection uses conversation.reject_pending_actions() and the loop continues") @@ -239,12 +246,15 @@ from collections.abc import Callable from pydantic import SecretStr from openhands.sdk import LLM, Agent, BaseConversation, Conversation -from openhands.sdk.conversation.state import AgentExecutionStatus, ConversationState +from openhands.sdk.conversation.state import ( + ConversationExecutionStatus, + ConversationState, +) from openhands.sdk.security.confirmation_policy import ConfirmRisky from openhands.sdk.security.llm_analyzer import LLMSecurityAnalyzer -from openhands.sdk.tool import Tool, register_tool -from openhands.tools.execute_bash import BashTool +from openhands.sdk.tool import Tool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool # Clean ^C exit: no stack trace noise @@ -293,14 +303,14 @@ def run_until_finished_with_security( """ Drive the conversation until FINISHED. - If WAITING_FOR_CONFIRMATION: ask the confirmer. - * On approve: set agent_status = IDLE (keeps original example’s behavior). + * On approve: set execution_status = IDLE (keeps original example’s behavior). * On reject: conversation.reject_pending_actions(...). - If WAITING but no pending actions: print warning and set IDLE (matches original). """ - while conversation.state.agent_status != AgentExecutionStatus.FINISHED: + while conversation.state.execution_status != ConversationExecutionStatus.FINISHED: if ( - conversation.state.agent_status - == AgentExecutionStatus.WAITING_FOR_CONFIRMATION + conversation.state.execution_status + == ConversationExecutionStatus.WAITING_FOR_CONFIRMATION ): pending = ConversationState.get_unmatched_actions(conversation.state.events) if not pending: @@ -319,7 +329,7 @@ def run_until_finished_with_security( # Configure LLM api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( usage_id="security-analyzer", @@ -329,23 +339,21 @@ llm = LLM( ) # Tools -register_tool("BashTool", BashTool) -register_tool("FileEditorTool", FileEditorTool) tools = [ Tool( - name="BashTool", + name=TerminalTool.name, ), - Tool(name="FileEditorTool"), + Tool(name=FileEditorTool.name), ] -# Agent with security analyzer -security_analyzer = LLMSecurityAnalyzer() -agent = Agent(llm=llm, tools=tools, security_analyzer=security_analyzer) +# Agent +agent = Agent(llm=llm, tools=tools) # Conversation with persisted filestore conversation = Conversation( agent=agent, persistence_dir="./.conversations", workspace="." ) +conversation.set_security_analyzer(LLMSecurityAnalyzer()) conversation.set_confirmation_policy(ConfirmRisky()) print("\n1) Safe command (LOW risk - should execute automatically)...") diff --git a/sdk/guides/skill.mdx b/sdk/guides/skill.mdx index 49e99638..ce1a5d30 100644 --- a/sdk/guides/skill.mdx +++ b/sdk/guides/skill.mdx @@ -27,9 +27,9 @@ from openhands.sdk.context import ( KeywordTrigger, Skill, ) -from openhands.sdk.tool import Tool, register_tool -from openhands.tools.execute_bash import BashTool +from openhands.sdk.tool import Tool from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool logger = get_logger(__name__) @@ -37,7 +37,7 @@ logger = get_logger(__name__) # Configure LLM api_key = os.getenv("LLM_API_KEY") assert api_key is not None, "LLM_API_KEY environment variable is not set." -model = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20250929") +model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") base_url = os.getenv("LLM_BASE_URL") llm = LLM( usage_id="agent", @@ -48,15 +48,29 @@ llm = LLM( # Tools cwd = os.getcwd() -register_tool("BashTool", BashTool) -register_tool("FileEditorTool", FileEditorTool) tools = [ Tool( - name="BashTool", + name=TerminalTool.name, ), - Tool(name="FileEditorTool"), + Tool(name=FileEditorTool.name), ] +# AgentContext provides flexible ways to customize prompts: +# 1. Skills: Inject instructions (always-active or keyword-triggered) +# 2. system_message_suffix: Append text to the system prompt +# 3. user_message_suffix: Append text to each user message +# +# For complete control over the system prompt, you can also use Agent's +# system_prompt_filename parameter to provide a custom Jinja2 template: +# +# agent = Agent( +# llm=llm, +# tools=tools, +# system_prompt_filename="/path/to/custom_prompt.j2", +# system_prompt_kwargs={"cli_mode": True, "repo": "my-project"}, +# ) +# +# See: https://docs.openhands.dev/sdk/guides/skill#customizing-system-prompts agent_context = AgentContext( skills=[ Skill( @@ -67,7 +81,7 @@ agent_context = AgentContext( # You can set it to be the path of a file that contains the skill content source=None, # trigger determines when the skill is active - # trigger=None means always active + # trigger=None means always active (repo skill) trigger=None, ), Skill( @@ -81,15 +95,18 @@ agent_context = AgentContext( trigger=KeywordTrigger(keywords=["flarglebargle"]), ), ], + # system_message_suffix is appended to the system prompt (always active) system_message_suffix="Always finish your response with the word 'yay!'", + # user_message_suffix is appended to each user message user_message_suffix="The first character of your response should be 'I'", + # You can also enable automatic load skills from + # public registry at https://github.com/OpenHands/skills + load_public_skills=True, ) - # Agent agent = Agent(llm=llm, tools=tools, agent_context=agent_context) - llm_messages = [] # collect raw LLM messages @@ -112,10 +129,21 @@ print("Now sending flarglebargle to trigger the knowledge skill!") conversation.send_message("flarglebargle!") conversation.run() +print("=" * 100) +print("Now triggering public skill 'github'") +conversation.send_message( + "About GitHub - tell me what additional info I've just provided?" +) +conversation.run() + print("=" * 100) print("Conversation finished. Got the following LLM messages:") for i, message in enumerate(llm_messages): print(f"Message {i}: {str(message)[:200]}") + +# Report cost +cost = llm.metrics.accumulated_cost +print(f"EXAMPLE_COST: {cost}") ``` ```bash Running the Example From e7b4a00cc56c0ee1de3867c66b9e6abc64fc3280 Mon Sep 17 00:00:00 2001 From: enyst Date: Sat, 10 Jan 2026 16:13:24 +0000 Subject: [PATCH 5/7] Docs: add conda channel alias runtime page to nav Co-authored-by: openhands --- docs.json | 3 ++- .../{runtime => usage/runtimes}/runtime-images-and-conda.mdx | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename openhands/{runtime => usage/runtimes}/runtime-images-and-conda.mdx (100%) diff --git a/docs.json b/docs.json index 127e4a4b..97c3bdcc 100644 --- a/docs.json +++ b/docs.json @@ -151,6 +151,7 @@ "group": "Runtime Configuration", "pages": [ "openhands/usage/runtimes/overview", + "openhands/usage/runtimes/runtime-images-and-conda", { "group": "Providers", "pages": [ @@ -577,4 +578,4 @@ "destination": "/openhands/usage/cli/resume" } ] -} \ No newline at end of file +} diff --git a/openhands/runtime/runtime-images-and-conda.mdx b/openhands/usage/runtimes/runtime-images-and-conda.mdx similarity index 100% rename from openhands/runtime/runtime-images-and-conda.mdx rename to openhands/usage/runtimes/runtime-images-and-conda.mdx From c117af8ea2760f5885fdd0619a05247590f85995 Mon Sep 17 00:00:00 2001 From: Engel Nyst Date: Sat, 10 Jan 2026 17:19:37 +0100 Subject: [PATCH 6/7] Update openhands/usage/runtimes/runtime-images-and-conda.mdx --- openhands/usage/runtimes/runtime-images-and-conda.mdx | 1 - 1 file changed, 1 deletion(-) diff --git a/openhands/usage/runtimes/runtime-images-and-conda.mdx b/openhands/usage/runtimes/runtime-images-and-conda.mdx index ca76cfd4..26bfbff7 100644 --- a/openhands/usage/runtimes/runtime-images-and-conda.mdx +++ b/openhands/usage/runtimes/runtime-images-and-conda.mdx @@ -33,5 +33,4 @@ Notes - However, without channel_alias, conda-forge still resolves under the anaconda.org domain by default, which is why you may see requests there References -- Conda channels and alias docs: https://www.anaconda.com/docs/getting-started/working-with-conda/channels - Background: https://github.com/conda-forge/miniforge/issues/784 From 756b1d9c33a562128be18d088acbcd236cf91a4b Mon Sep 17 00:00:00 2001 From: enyst Date: Sat, 10 Jan 2026 16:21:31 +0000 Subject: [PATCH 7/7] Docs: explain anaconda.org traffic up front Co-authored-by: openhands --- openhands/usage/runtimes/runtime-images-and-conda.mdx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openhands/usage/runtimes/runtime-images-and-conda.mdx b/openhands/usage/runtimes/runtime-images-and-conda.mdx index 26bfbff7..e0cd2302 100644 --- a/openhands/usage/runtimes/runtime-images-and-conda.mdx +++ b/openhands/usage/runtimes/runtime-images-and-conda.mdx @@ -4,6 +4,11 @@ title: Runtime images and conda channel hosting This page explains when OpenHands runtime builds may contact anaconda.org and how to avoid it. +Why you may still see anaconda.org traffic +- OpenHands runtime images do **not** use Anaconda's **defaults** channel, so we do not pull packages from Anaconda's defaults repo. +- However, `conda-forge` uses a default `channel_alias` that expands `conda-forge` to `https://conda.anaconda.org/conda-forge` (under the `anaconda.org` domain). + - Without overriding `channel_alias`, you may see network requests to `anaconda.org` even when you are only installing from `conda-forge`. + Overview - OpenHands runtime images are built with micromamba and we install Python and Poetry from the conda-forge channel - Even when using only conda-forge, the default channel_alias expands conda-forge to https://conda.anaconda.org/conda-forge, which is hosted under anaconda.org @@ -28,9 +33,5 @@ Two usage paths - A public alternative such as https://repo.prefix.dev - An internal corporate mirror/proxy (e.g., Artifactory) that fronts conda-forge -Notes -- We remove the defaults channel, so no packages are pulled from Anaconda's defaults repo -- However, without channel_alias, conda-forge still resolves under the anaconda.org domain by default, which is why you may see requests there - References - Background: https://github.com/conda-forge/miniforge/issues/784