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

> ## Agent Instructions
> H Platform has four products: the Agents API (managed computer-use agents, base URL https://agp.eu.hcompany.ai/api/v2 or https://agp.hcompany.ai/api/v2 for the US), the Models API (OpenAI-compatible Holo vision-language models at https://api.hcompany.ai/v1), HoloDesktop CLI (Holo on the user's own desktop), and HoloTab (a free no-code Chrome extension that runs Holo in the user's browser, with recordable routines and schedules).
> Authenticate with a bearer API key from the HAI_API_KEY environment variable. SDKs: `pip install hai-agents` (Python, `from hai_agents import Client`) and `npm install hai-agents` (TypeScript, `import { HaiAgentsClient } from "hai-agents"`). CLI: `hai`.
> Agents do work in a browser or on a desktop; describe the task as an imperative instruction. To run a task quickly, prefer the pre-built agent `h/web-surfer-flash`. Read results from the session's `latest_answer` after it reaches a terminal status.
> Sessions are the unit of work; wait for a terminal status (completed, failed, timed_out, interrupted) before reading the answer. Use webhooks or the `changes` long-poll endpoint to follow progress.

# Browser agent

> A complete Holo browser agent in one Python file: Playwright takes screenshots, Holo returns clicks and keystrokes, the loop runs until the answer tool is called.

A browser agent in about 100 lines. Playwright drives a headless Chromium, each screenshot goes to Holo, and Holo answers with the next click, keystroke, scroll, or URL. It applies every convention from [Core concepts](/models-api/build-an-agent/core-concepts) in the [function calling](/models-api/build-an-agent/function-calling) format.

<Steps titleSize="h3">
  <Step title="Install">
    ```bash theme={"system"}
    pip install openai playwright
    playwright install chromium
    export HAI_API_KEY="your-api-key-here"
    ```
  </Step>

  <Step title="Save the agent">
    ```python browser_agent.py expandable theme={"system"}
    import base64
    import json
    import os

    from openai import OpenAI
    from playwright.sync_api import sync_playwright

    client = OpenAI(base_url="https://api.hcompany.ai/v1/", api_key=os.environ["HAI_API_KEY"])
    MODEL = "holo4-35b-a3b"
    WIDTH, HEIGHT = 1280, 800


    def fn(name: str, description: str, **properties) -> dict:
        return {
            "type": "function",
            "function": {
                "name": name,
                "description": description,
                "parameters": {"type": "object", "properties": properties, "required": list(properties)},
            },
        }


    INT = {"type": "integer", "description": "Coordinate as integer in [0, 1000]"}
    tools = [
        fn("click", "Click at (x, y) coordinates", element={"type": "string", "description": "Detailed description of the target UI element"}, x=INT, y=INT),
        fn("type", "Type text into the focused element, optionally pressing Enter", text={"type": "string"}, press_enter={"type": "boolean"}),
        fn("scroll", "Scroll the page", direction={"type": "string", "enum": ["up", "down"]}),
        fn("goto", "Navigate to a URL", url={"type": "string"}),
        fn("answer", "Provide a final answer", content={"type": "string", "description": "The answer content"}),
    ]


    def execute(page, name: str, args: dict) -> str:
        if name == "click":
            page.mouse.click(args["x"] / 1000 * WIDTH, args["y"] / 1000 * HEIGHT)
        elif name == "type":
            page.keyboard.type(args["text"])
            if args.get("press_enter"):
                page.keyboard.press("Enter")
        elif name == "scroll":
            page.mouse.wheel(0, HEIGHT * 0.8 * (1 if args["direction"] == "down" else -1))
        elif name == "goto":
            page.goto(args["url"])
        page.wait_for_timeout(1000)
        page.wait_for_load_state()
        return f"Done. Current URL: {page.url}"


    def observation(page) -> dict:
        b64 = base64.b64encode(page.screenshot()).decode()
        return {"role": "user", "content": [
            {"type": "text", "text": "<observation>\n"},
            {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}},
            {"type": "text", "text": "\n</observation>"},
        ]}


    def trim_to_last_n_images(messages, n=3):
        seen = 0
        for msg in reversed(messages):
            if msg["role"] != "user" or not isinstance(msg["content"], list):
                continue
            for chunk in msg["content"]:
                if chunk.get("type") != "image_url":
                    continue
                seen += 1
                if seen > n:
                    chunk["type"] = "text"
                    chunk["text"] = "[screenshot evicted]"
                    chunk.pop("image_url", None)


    def run(task: str, start_url: str, max_steps: int = 20) -> str:
        with sync_playwright() as p:
            page = p.chromium.launch().new_page(viewport={"width": WIDTH, "height": HEIGHT})
            page.goto(start_url)
            messages = [
                {"role": "system", "content": "You are a web agent. You see the browser through screenshots and act with your tools."},
                {"role": "user", "content": task},
            ]
            for _ in range(max_steps):
                messages.append(observation(page))
                trim_to_last_n_images(messages)
                resp = client.chat.completions.create(
                    model=MODEL,
                    messages=messages,
                    tools=tools,
                    tool_choice="required",
                    temperature=0.8,
                    extra_body={"chat_template_kwargs": {"enable_thinking": True}},
                )
                msg = resp.choices[0].message
                messages.append({"role": "assistant", "content": msg.content, "tool_calls": msg.tool_calls})
                if not msg.tool_calls:
                    continue
                call = msg.tool_calls[0]
                args = json.loads(call.function.arguments)
                if call.function.name == "answer":
                    return args["content"]
                print(f"--- {call.function.name}({args})")
                messages.append({"role": "tool", "tool_call_id": call.id, "content": execute(page, call.function.name, args)})
        return "Step budget exhausted"


    if __name__ == "__main__":
        print(run("Search Wikipedia for Ada Lovelace and tell me her date of birth.", "https://en.wikipedia.org"))
    ```
  </Step>

  <Step title="Run it">
    ```bash theme={"system"}
    python browser_agent.py
    ```

    ```text Output theme={"system"}
    --- click({'element': 'Search Wikipedia input field at the top of the page', 'x': 364, 'y': 43})
    --- type({'text': 'Ada Lovelace', 'press_enter': True})
    Ada Lovelace was born on December 10, 1815.
    ```
  </Step>
</Steps>

## How it works

* **Screen size.** The viewport is fixed at 1280 by 800 and screenshots are taken at that size, so scaling the `[0, 1000]` coordinates back is one multiplication.
* **Settling.** After each action the loop waits a second, then for the page to load. Without the wait, a screenshot can land mid-navigation and show the model a stale page.
* **Memory.** Only the last 3 screenshots stay in context. Older ones become a text placeholder inside their `<observation>` wrapper.
* **Stopping.** The run ends only when Holo calls `answer`. A step without a tool call is skipped.

To drive a desktop or an Android device instead, swap Playwright for your OS or emulator driver: `screenshot()` and `execute()` change, the loop does not.

## Next steps

<CardGroup cols={2}>
  <Card title="Hybrid agent" icon="shuffle" href="/models-api/cookbooks/hybrid-agent">
    Add a code runner to this agent.
  </Card>

  <Card title="Core concepts" icon="arrows-rotate" href="/models-api/build-an-agent/core-concepts">
    The loop conventions Holo is trained on.
  </Card>
</CardGroup>
