> ## Documentation Index
> Fetch the complete documentation index at: https://docs.openhands.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Pip Tool Agent

> Load a custom tool from a published pip package via pip install --target and tool_module_qualnames.

<Card title="View source on GitHub" icon="github" href="https://github.com/OpenHands/enterprise-cookbook/tree/main/custom-agent-with-pip-tool" horizontal />

Load a custom tool from a **published Python package** in OpenHands Cloud. The package
is installed via `pip install --target /workspace`, making it importable by the frozen
agent-server, which then loads and registers the tool via `tool_module_qualnames`.

This is the natural evolution of `custom-agent-with-tool`: same loading mechanism
(declare the tool in `agent.tools`, map it in `tool_module_qualnames`), but the
*deploy* step becomes **"pip-install-into-working-dir"** instead of
**"upload source files."**

The example uses **[oh-markdown-tool](https://github.com/jpshackelford/oh-markdown-tool)**
(`oh-markdown-tool[openhands]==0.2.1`), a real published package that provides structural
markdown editing (renumber sections, manage TOC, etc.). The agent fixes a markdown doc
with messy numbering and adds a table of contents.

## How It Works

1. **Create a sandbox** (`POST /api/v1/sandboxes`) and wait for `RUNNING`.
2. **Install the package** into `/workspace` via `pip install --target`:
   ```bash theme={null}
   pip install --target /workspace --no-deps oh-markdown-tool[openhands]==0.2.1
   pip install --target /workspace mdformat pymarkdownlnt
   ```
   The `--target` flag puts the package into the conversation's working directory,
   which is on the agent-server's `sys.path`. We use `--no-deps` to skip dependencies
   that are already **bundled** in the frozen agent-server (`openhands-sdk`, `pydantic`,
   `rich`), then install only the **non-bundled** deps (`mdformat`, `pymarkdownlnt`)
   explicitly.
3. **Create a sample markdown file** with messy numbering (sections 5, 10, 3).
4. **Create a conversation** that maps the tool via `tool_module_qualnames`:
   ```python theme={null}
   "agent": {
       "tools": [
           {"name": "terminal"},
           {"name": "file_editor"},
           {"name": "markdown_document"},  # the custom tool
       ]
   },
   "tool_module_qualnames": {"markdown_document": "oh_markdown_tool.tool"},
   ```
   The agent-server imports `oh_markdown_tool.tool`, which registers the tool (the
   package's `tool.py` calls `register_tool("markdown_document", ...)` on import).
5. **Run and verify**:
   * The tool appears in `SystemPromptEvent.tools` (registered).
   * The tool appears in `ActionEvent` records (used).
   * The file is actually modified (TOC added, sections renumbered 1, 2, 3).

## Prerequisites

```bash theme={null}
pip install requests
export OH_API_KEY=your-openhands-cloud-api-key
export LLM_API_KEY=your-llm-api-key
```

Optional overrides (sensible defaults are used otherwise):

```bash theme={null}
export LLM_MODEL=litellm_proxy/claude-sonnet-4-5-20250929
export LLM_BASE_URL=https://llm-proxy.app.all-hands.dev/
```

## Run It

```bash theme={null}
python working_example.py            # runs and cleans up the sandbox
python working_example.py --keep     # leave the sandbox up for inspection
```

<Warning>
  **⚠️ Timing note**: If the script runs and deletes the sandbox too quickly, conversation
  events may not have synced from the agent-server to the main API yet, making the
  conversation appear empty or incomplete in the Cloud UI. To inspect conversation events
  in real time, **run with `--keep`** to leave the sandbox alive, or add a delay before
  cleanup. The script prints the conversation URL — you can view it while the sandbox is
  still running.
</Warning>

Expected output:

```text theme={null}
[demo] === Verification ===
[demo]   registered tools: ['terminal', 'file_editor', 'Markdown Document Tool', 'finish', 'think']
[demo]   tools used: ['file_editor', 'markdown_document']
[demo]   PASS: tool 'markdown_document' is registered
[demo]   PASS: tool 'markdown_document' was invoked by the agent
[demo]   PASS: file was modified (TOC added, sections renumbered)
[demo] SUCCESS: the tool was loaded from the pip package and used.
```

## Why `pip install --target` (and Not Plain `pip install`)?

On OpenHands Cloud the agent-server is a **frozen, self-contained binary** (built with
PyInstaller). A plain `pip install` targets the sandbox's *system* Python interpreter,
which the frozen server cannot see — so the module would never be importable.

The conversation's **working directory is on the agent-server's import path**, so
`pip install --target /workspace` places the package where the server can actually
import it. The bundled dependencies (`openhands-sdk`, `pydantic`, `rich`) are already
available inside the frozen binary, so we skip them (`--no-deps`) and only install the
package's own code plus its *non-bundled* dependencies.

This approach works for any published package, not just `oh-markdown-tool` — the key is
installing into the working directory and being aware of what's already bundled.

## Verification (Same Technique as the Other Examples)

Both this example and `custom-agent-with-tool` read the `SystemPromptEvent.tools` array
to confirm registration, and the `ActionEvent` records to confirm usage. This example
adds a third check: **did the tool actually do something?** We read the markdown file
after the run and confirm it was modified (TOC present, sections renumbered).

## The Tool: `oh-markdown-tool`

`oh-markdown-tool` is a real, published package ([PyPI](https://pypi.org/project/oh-markdown-tool/) |
[GitHub](https://github.com/jpshackelford/oh-markdown-tool)) that provides structural
markdown editing. It's built with the OpenHands SDK and demonstrates the full packaging
story: core library (no SDK dependency) + optional `[openhands]` extra for the agent
tool integration.

The tool offers commands like:

* `overview` — show document structure
* `renumber` — fix section numbering
* `toc_update` — add/update table of contents
* `move`, `insert`, `delete`, `promote`, `demote` — section operations
* `rewrap`, `lint`, `fix`, `cleanup` — formatting

For this example we use `renumber` + `toc_update` to fix a messy doc.

## Files

| File | Purpose |
| - | - |
| `working_example.py` | End-to-end runner: create sandbox → pip install → run → verify → clean up |

## Cleanup

Without `--keep` the sandbox is deleted automatically. With `--keep`, delete it when
done:

```bash theme={null}
curl -X DELETE "https://app.all-hands.dev/api/v1/sandboxes/<id>?sandbox_id=<id>" \
  -H "Authorization: Bearer $OH_API_KEY"
```

<Note>
  Note: `DELETE /api/v1/sandboxes/{id}` requires `sandbox_id` as **both** the path
  segment and a query parameter; omitting the query parameter returns HTTP 422 and
  leaks the sandbox.
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.