Metadata-Version: 2.5
Name: hai-agents
Version: 1.4.3
Summary: Python SDK for H Company's Agents API: autonomous agents powered by Holo.
Project-URL: Homepage, https://hcompany.ai
Project-URL: Repository, https://github.com/hcompai/hai-agents-python
Project-URL: Issues, https://github.com/hcompai/hai-agents-python/issues
Project-URL: Documentation, https://hub.hcompany.ai/agents-api/introduction
Author-email: H Company <contact@hcompany.ai>
License: MIT
License-File: LICENSE
Keywords: agent,agents,browser-use,computer-use,h-company,llm,sdk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx<0.29.0,>=0.23.0
Requires-Dist: pydantic<3,>=2.0
Requires-Dist: typing-extensions>=4.6
Provides-Extra: all
Requires-Dist: hai-drivers[browser-cli,cua-cli,desktop-cli]>=0.1.13; extra == 'all'
Requires-Dist: hai-drivers[desktop]>=0.1.3; extra == 'all'
Requires-Dist: hai-drivers[web]>=0.1.3; extra == 'all'
Requires-Dist: mcp<2,>=1.10; extra == 'all'
Requires-Dist: prompt-toolkit<4,>=3.0.43; extra == 'all'
Requires-Dist: pynput>=1.7.6; (sys_platform == 'win32') and extra == 'all'
Requires-Dist: python-dotenv<2,>=1.2.3; extra == 'all'
Requires-Dist: pyyaml<7,>=6.0; extra == 'all'
Requires-Dist: rich<16,>=15.0.0; extra == 'all'
Requires-Dist: ripgrep-bin==15.2.0; (platform_machine in 'x86_64 AMD64 arm64 aarch64 ARM64') and extra == 'all'
Requires-Dist: typer<1,>=0.27.2; extra == 'all'
Provides-Extra: browser
Requires-Dist: hai-drivers[web]>=0.1.3; extra == 'browser'
Provides-Extra: cli
Requires-Dist: mcp<2,>=1.10; extra == 'cli'
Requires-Dist: prompt-toolkit<4,>=3.0.43; extra == 'cli'
Requires-Dist: python-dotenv<2,>=1.2.3; extra == 'cli'
Requires-Dist: pyyaml<7,>=6.0; extra == 'cli'
Requires-Dist: rich<16,>=15.0.0; extra == 'cli'
Requires-Dist: typer<1,>=0.27.2; extra == 'cli'
Provides-Extra: desktop
Requires-Dist: hai-drivers[desktop]>=0.1.3; extra == 'desktop'
Provides-Extra: workstation
Requires-Dist: hai-drivers[browser-cli,cua-cli,desktop-cli]>=0.1.13; extra == 'workstation'
Requires-Dist: pynput>=1.7.6; (sys_platform == 'win32') and extra == 'workstation'
Requires-Dist: ripgrep-bin==15.2.0; (platform_machine in 'x86_64 AMD64 arm64 aarch64 ARM64') and extra == 'workstation'
Description-Content-Type: text/markdown

<p align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://github.com/hcompai/hai-agents-python/blob/main/assets/banner-dark.gif?raw=true" />
    <img src="https://github.com/hcompai/hai-agents-python/blob/main/assets/banner-light.gif?raw=true" alt="Agents API" width="700" />
  </picture>
</p>

<p align="center">
  <a href="https://pypi.org/project/hai-agents/"><img src="https://img.shields.io/pypi/v/hai-agents.svg" alt="PyPI" /></a>
  <a href="https://pypi.org/project/hai-agents/"><img src="https://img.shields.io/pypi/pyversions/hai-agents.svg" alt="Python versions" /></a>
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
</p>

<p align="center">
  Python SDK for <a href="https://hcompany.ai">H Company</a>'s <a href="https://hub.hcompany.ai/agents-api/introduction">Agents API</a>.
</p>

<p align="center">
  <b><a href="https://hub.hcompany.ai/agents-api/introduction">Documentation</a></b>
  &nbsp;·&nbsp;
  <a href="https://platform.hcompany.ai/settings/api-keys">Get an API key</a>
  &nbsp;·&nbsp;
  <a href="https://pypi.org/project/hai-agents/">PyPI</a>
  &nbsp;·&nbsp;
  <a href="https://github.com/hcompai/hai-agents-ts">TypeScript SDK</a>
  &nbsp;·&nbsp;
  <a href="https://hcompany.ai">H Company</a>
</p>

## Contents

- [Installation](#installation)
- [Quickstart: command line](#quickstart-command-line)
- [Quickstart: Python](#quickstart-python)
- [Command line](#command-line)
- [Python SDK guides](#python-sdk-guides)
- [Documentation](#documentation)

## Installation

```bash
pip install "hai-agents[cli]"   # SDK and the holo command
pip install hai-agents          # SDK only
```

Python 3.10 or newer is required.

## Quickstart: command line

```bash
pip install "hai-agents[cli]"
holo login
holo "What's the top story on Hacker News?"
```

`holo login` signs you in with Google in the browser, or takes a key you paste with `holo login --key`, and stores it in `~/.config/hai/.env`. Any command does the same on first use in a terminal, so this step is optional. The task then runs on a hosted agent with its own browser: its steps stream to your terminal, and its answer prints at the end. In a terminal you can follow up on the same session, or run `holo` alone to start an interactive session.

## Quickstart: Python

Get an API key at [platform.hcompany.ai/settings/api-keys](https://platform.hcompany.ai/settings/api-keys) and export it, or run `holo login` once:

```bash
export HAI_API_KEY=hk-...
```

Launch the built-in `h/web-surfer-pro` agent, which ships with its own browser, and describe the task in plain language. `run_session` polls until the agent finishes and returns the final answer.

```python
from hai_agents import Client

client = Client()

result = client.run_session(
    agent="h/web-surfer-pro",
    messages="What are the top 3 stories on Hacker News right now?",
)

print(result.status)
print(result.answer)
```

`Client()` reads `HAI_API_KEY` from the environment, else the key `holo login` stored in `~/.config/hai/.env`.

`result` is a `SessionRunResult`: `id`, `status`, `answer`, the accumulated `events`, and `final_changes`.

## Command line

The `cli` extra installs the `holo` command, also available as `hai`.

```bash
holo "What's the top story on Hacker News?"      # run one task
holo                                            # interactive session
holo -a h/web-surfer-pro "Compare 3 laptops"    # pick an agent for one run
holo agents use h/web-surfer-pro                # change the default agent
holo -a h/holo-desktop "Open Calculator"        # drive this machine's desktop
holo --local "Summarize news.ycombinator.com"   # run the agent loop on this machine
holo sessions watch <session-id>                # follow a running session
holo mcp install                                # add the MCP server to your editor
```

Tasks run on `h/web-surfer-flash` unless you pass `--agent` or set a default with `holo agents use`. Run `holo --help`, or `holo <command> --help`, for every option.

`~/.holo/agents.md` joins the instructions of every agent holo runs, and `~/.holo/skills/<name>/SKILL.md` joins its skills.

### Interactive sessions

`holo` in a terminal opens a session you talk to in plain words. Each message continues the same session, with its browser and context, until you start fresh with `/new`.

| Key | Action |
| --- | --- |
| type, then enter | message the agent while it works |
| esc | pause the agent; esc again resumes |
| `/answer` | ask the agent to answer now |
| `/` | list commands: `/new`, `/retry`, `/resume`, `/agent`, `/live`, `/share`, `/status`, `/copy` |
| `?` | all shortcuts |
| ctrl+c | cancel the turn; twice to exit |

An agent that drives this desktop, like `h/holo-desktop`, uses your mouse and keyboard, so typing goes to your apps instead: press esc twice, from any app, to stop it.

### Commands

| Command | What it does |
| --- | --- |
| `holo run <task>` | run one task; same as `holo <task>` |
| `holo chat` | start an interactive session |
| `holo sessions` | `list`, `status`, `watch`, `events`, `send`, `cancel`, `share` sessions |
| `holo agents` | `list` agents, `get` one, `use` one as the default |
| `holo skills` | browse available skills |
| `holo mcp install` | let Cursor, Claude Code, Codex, and other MCP clients run agents through `holo mcp`; `--remote` adds the hosted server instead |
| `holo local` | serve a browser, desktop, or workstation on this machine for a session started elsewhere |
| `holo login`, `logout`, `whoami`, `doctor` | manage and check your credentials and setup |

### Scripts and CI

Scripts never prompt. Set `HAI_API_KEY`, or pipe a key into `holo login --key`, then use `--json` for machine-readable output or `-q` for the answer alone:

```bash
holo --json run "What's the top story on Hacker News?" | jq -r .answer
holo run -q "What's the top story on Hacker News?" > answer.txt
```

`--json` prints `session_id`, `status`, `answer`, `outcome`, `error`, `error_code`, and `agent_view_url`. The exit code is `0` when the session completes and `1` otherwise.

Credentials resolve from `--api-key`, then `HAI_API_KEY`, then `~/.config/hai/.env`.

## Python SDK guides

### How a session works

A session is one run of an agent against a task. It moves through a small set of states: `pending`, `running`, and then a settled state such as `completed`, `idle`, `failed`, `timed_out`, or `interrupted`.

You drive a session two ways. `run_session` creates it and blocks until it settles, which suits one-shot tasks. `start_session` creates it and returns a handle right away, so you can read and steer the agent while it works.

```python
session = client.start_session(
    agent="h/web-surfer-pro",
    messages="Find the top story on Hacker News",
)

print(session.id)
result = session.wait_for_completion()
print(result.status, result.answer)
```

### Watch and steer a running session

A handle bound to the session `id` exposes the full lifecycle. Read the agent's progress at three levels of detail:

```python
session.status()
session.changes(from_index=0)
session.get()
```

`status()` is a cheap snapshot with the state, step count, and token usage. `changes(from_index=0)` long-polls for new events and the final answer. `get()` returns the full Session resource.

While the session is not in a terminal state, you can intervene:

```python
session.send_message({"type": "user_message", "message": "Only consider the last 24 hours"})
session.pause()
session.resume()
session.force_answer()
session.cancel()
```

`send_message` redirects the agent on its next step and wakes an `idle` session. `pause` halts with state preserved until `resume`. `force_answer` makes the agent stop exploring and answer from what it has. `cancel` ends the session as `interrupted`.

### Multi-turn sessions

By default a session ends as soon as the agent answers. Set `idle_timeout_s` to keep it open: after each answer the session goes `idle` and waits that long for your next message, carrying its full context and browser state across turns.

```python
session = client.start_session(
    agent="h/web-surfer-pro",
    idle_timeout_s=600,
    messages="Find the top story on Hacker News",
)
first = session.wait_for_completion()

session.send_message({"type": "user_message", "message": "Now summarize its comments"})
second = session.wait_for_completion()
```

### Structured output

Pass a pydantic model as `answer_schema` and the agent's final answer comes back as a validated instance. The model's JSON schema is sent as the agent's answer format; the raw wire value stays at `result.final_changes.answer`.

```python
from pydantic import BaseModel
from hai_agents import Client

class Job(BaseModel):
    title: str
    company: str

class Jobs(BaseModel):
    jobs: list[Job]

client = Client()
result = client.run_session(
    agent="h/web-surfer-pro",
    messages="Find 3 open ML engineering roles in Paris.",
    answer_schema=Jobs,
)

for job in result.answer.jobs:
    print(job.title, "@", job.company)
```

A completed answer that does not match the schema raises `AnswerValidationError`, with the raw payload on `.raw`. Sessions that end without completing return their raw answer untouched.

### Custom tools

Expose your own Python functions to the agent. Pass them to `run_session` and the polling loop runs each one when the agent calls it, then posts the result back so the session continues. Any function with typed parameters and a docstring works; the input schema is derived from the signature.

```python
from hai_agents import Client

def get_weather(city: str) -> str:
    """Get the current weather for a city."""
    return f"Sunny in {city}"

client = Client()

result = client.run_session(
    agent="h/web-surfer-pro",
    messages="What should I wear in Paris today?",
    tools=[get_weather],
)
```

Use the `@tool` decorator to override the name or description:

```python
from hai_agents import tool

@tool(name="lookup_order", description="Look up an order by its id.")
def lookup(order_id: str) -> dict:
    return {"id": order_id, "status": "shipped"}
```

A tool that raises is reported to the agent as a tool error rather than crashing the run. With `AsyncClient`, tools may be `async def`.

#### Prebuilt: one-time passwords (2FA)

`hai_agents_tools` ships ready-made tools. `otp_tool` lets the agent ask for a one-time password, verification code, or confirmation link when a login or signup step needs one. Without a handler it prompts on stdin; `imap_otp_handler` reads the code straight from a mailbox over IMAP (for Gmail, use an app password).

```python
import os

from hai_agents import Client
from hai_agents_tools import imap_otp_handler, otp_tool

handler = imap_otp_handler(
    host="imap.gmail.com",
    username="agent-inbox@gmail.com",
    password=os.environ["GMAIL_APP_PASSWORD"],
)

client = Client()
result = client.run_session(
    agent="h/web-surfer-pro",
    messages="Log in to example.com and check for new notifications",
    tools=[otp_tool(handler)],
)
```

Like every custom tool, the handler runs entirely in your process: the IMAP credentials never leave your machine, and the agent only receives the single extracted code or link -- never mailbox contents.

### Browser profiles and vaults

Start a session on a browser that already knows the user. A [browser profile](https://hub.hcompany.ai/agents-api/browser/profiles) restores saved cookies and storage from an earlier session, and a [vault](https://hub.hcompany.ai/agents-api/vaults/overview) lets the agent sign in to sites with secrets that never enter its context. Bind both through per-run overrides:

```python
result = client.run_session(
    agent="h/web-surfer-pro",
    messages="Open my dashboard and report any new alerts",
    overrides={
        "agent.environments[kind=web].browser_profile_id": "<profile-id>",
        "agent.environments[kind=web].vault_id": "<vault-id>",
    },
)
```

### Async

`AsyncClient` mirrors `Client` for asyncio. Every session method is a coroutine.

```python
import asyncio
from hai_agents import AsyncClient

async def main():
    client = AsyncClient()
    result = await client.run_session(
        agent="h/web-surfer-pro",
        messages="What are the top 3 stories on Hacker News right now?",
    )
    print(result.answer)

asyncio.run(main())
```

### Local agents

`Client.local()` and `await AsyncClient.local()` run the agent on this machine through a local agent runtime,
started on demand or passed in with `runtime=...`. Each environment picks `host="user_device"` or `host="cloud"`.
`Client()` keeps using the hosted Agents API. Closing the client stops a runtime it started, unless other clients still
have active sessions there.

```python
from hai_agents import Client

with Client.local(local_options={"binary_path": "/path/to/hai-agent-runtime"}) as client:
    session = client.sessions.create_session(
        agent={
            "name": "local-example",
            "description": "Local workstation example",
            "instructions": "Answer the user's task using the workstation tools.",
            "environments": [
                {"id": "workstation", "kind": "workstation", "host": "user_device"}
            ],
        },
        messages=[{"type": "user_message", "message": "Print hello using the shell."}],
        max_steps=8,
        max_time_s=120,
    )
    # Poll or steer the session here, before leaving the client context.
```

Inference stays hosted (`HAI_API_KEY`) unless you pass `inference=Inference.self_hosted(url, model=...)`
(`from hai_agents_local.runtime import Inference`).

One agent drives this machine's desktop at a time: a second process asking for a `desktop` or `workstation` bridge
fails fast instead of sharing the mouse. `holo local stop` stops every local turn on the machine; so does a double Esc
once `hai_agents_local.killswitch.arm_esc_listener()` is armed (macOS, Windows, and X11 Linux).

### Inspect and share sessions

List past sessions and create a public replay link:

```python
page = client.sessions.list_sessions(size=10)
for summary in page.items:
    print(summary.id, summary.status)

link = client.sessions.share_session("<session-id>")
print(link.share_url)
```

### Regions and configuration

The client targets the EU region by default; pass `environment` to use the US region instead:

```python
from hai_agents import Client, HaiAgentsEnvironment

client = Client(environment=HaiAgentsEnvironment.US)
```

`Client` also accepts a custom `base_url`, and an `api_key` when you do not want to use the environment variable:

```python
client = Client(base_url="https://agp.hcompany.ai", api_key="hk-...")
```

### Errors

```python
from hai_agents import AnswerValidationError, UnprocessableEntityError
from hai_agents.core import ApiError
```

`ApiError` is the base for HTTP failures and carries `.status_code` and `.body`. `UnprocessableEntityError` is the 422 raised when a request fails validation. `AnswerValidationError` is raised when a completed answer does not match `answer_schema`, with the unparsed value on `.raw`.

### Webhooks

Verify the signature on an incoming webhook before trusting it:

```python
from hai_agents import verify_webhook, WebhookVerificationError

event = verify_webhook(request_body, signature, timestamp, secret)
print(event.type, event.data)
```

## Documentation

Guides, core concepts, and the full API reference live at **[hub.hcompany.ai/agents-api](https://hub.hcompany.ai/agents-api/introduction)**.

## License

[MIT](LICENSE)
