> ## 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.

# Core concepts

> The harness conventions Holo is trained on. Get them right and either output format works.

export const CoordinateGrid = () => {
  const X0 = 60;
  const Y0 = 28;
  const W = 300;
  const H = 188;
  const px = X0 + 932 / 1000 * W;
  const py = Y0 + 880 / 1000 * H;
  const mono = {
    fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
    fontSize: 11
  };
  return <div className="not-prose my-6 flex justify-center">
      <svg width="420" height="252" viewBox="0 0 420 252" className="max-w-full">
        <g className="text-zinc-50 dark:text-zinc-900" fill="currentColor">
          <rect x={X0} y={Y0} width={W} height={H} rx="4" />
        </g>
        <g className="text-zinc-200 dark:text-zinc-800" stroke="currentColor" strokeWidth="1" fill="none">
          {[0.25, 0.5, 0.75].map(f => <line key={`v${f}`} x1={X0 + f * W} y1={Y0} x2={X0 + f * W} y2={Y0 + H} />)}
          {[0.25, 0.5, 0.75].map(f => <line key={`h${f}`} x1={X0} y1={Y0 + f * H} x2={X0 + W} y2={Y0 + f * H} />)}
        </g>
        <rect x={X0} y={Y0} width={W} height={H} rx="4" fill="none" stroke="currentColor" strokeWidth="1" className="text-zinc-300 dark:text-zinc-700" />

        <g className="text-zinc-900 dark:text-zinc-100" stroke="currentColor" strokeWidth="1.25" strokeDasharray="4 4" fill="none">
          <line x1={px} y1={Y0} x2={px} y2={py} />
          <line x1={X0} y1={py} x2={px} y2={py} />
        </g>
        <circle cx={px} cy={py} r="5" className="text-zinc-900 dark:text-zinc-100" fill="currentColor" />

        <g className="text-zinc-500 dark:text-zinc-400" fill="currentColor" style={mono}>
          <text x={X0} y={Y0 - 10}>(0, 0)</text>
          <text x={px} y={Y0 - 10} textAnchor="middle">x = 932</text>
          <text x={X0 - 8} y={py + 4} textAnchor="end">y = 880</text>
          <text x={X0 + W} y={Y0 + H + 18} textAnchor="end">(1000, 1000)</text>
        </g>
      </svg>
    </div>;
};

Holo is trained to act as a multi-step agent inside a specific harness. Five of its conventions have to come along for the model to behave well in yours; skip any one and quality suffers.

<CardGroup cols={3}>
  <Card title="Output format" icon="code-branch" href="#pick-an-output-format">Function calling or structured outputs, depending on the model.</Card>
  <Card title="Answer tool" icon="flag-checkered" href="#answer-tool">The only way the model signals it is done.</Card>
  <Card title="Reasoning" icon="brain" href="#reasoning">Read it, never replay it.</Card>
  <Card title="Coordinates" icon="crosshairs" href="#coordinates-in-0-1000">Normalized to `[0, 1000]`, scale back yourself.</Card>
  <Card title="Observations" icon="image" href="#observations">Screenshots in `<observation>` tags, last 3 kept.</Card>
  <Card title="Sampling" icon="sliders" href="#sampling">0.8 for loops, 0.0 for single calls.</Card>
</CardGroup>

Set up the OpenAI client first by following the [Quickstart](/models-api/quickstart).

## Pick an output format

|                                                                     | Holo3.1 (`holo3-1-35b-a3b`)       | Holo3 (`holo3-122b-a10b`)           |
| :------------------------------------------------------------------ | :-------------------------------- | :---------------------------------- |
| [Function calling](/models-api/build-an-agent/function-calling)     | <Icon icon="check" /> Recommended | <Icon icon="xmark" /> Not supported |
| [Structured outputs](/models-api/build-an-agent/structured-outputs) | <Icon icon="check" />             | <Icon icon="check" /> Only option   |

Pick one and stay in it. Only how you declare tools and read the output changes; everything on this page is shared.

## Answer tool

Holo is trained to end a task by calling a tool named `answer` with its work report in the `content` argument. Register it alongside your action tools, in both formats, and stop the loop when it is called. A step that comes back without any tool call is not a stop signal: treat it as a no-op, append the message, and keep looping. Only `answer` ends the run.

## Reasoning

Holo returns two streams on every call: a thinking trace in `message.reasoning` and the action in `content`. Reasoning is essential in agent mode (Holo was trained to plan before each step), so leave it on; `reasoning_effort: "medium"` is a sensible default.

<CodeGroup>
  ```python Python theme={"system"}
  extra_body={"chat_template_kwargs": {"enable_thinking": True}}
  ```

  ```typescript TypeScript theme={"system"}
  // H-specific fields are passed through in the request body
  ...({ chat_template_kwargs: { enable_thinking: true } } as any)
  ```
</CodeGroup>

Past reasoning is dropped between turns by the [Qwen 3.5 chat template](https://huggingface.co/Qwen/Qwen3.5-35B-A3B/blob/main/chat_template.jinja) Holo inherits, so anything the model needs to remember has to flow through `content`. When re-adding the assistant message to the conversation, push only the parsed output. Do not splice the reasoning back in.

## Coordinates in `[0, 1000]`

Send a screenshot at any size. Holo returns coordinates as integers in `[0, 1000]`, normalized to that image. Scale back to pixels using its dimensions:

<CoordinateGrid />

<CodeGroup>
  ```python Python theme={"system"}
  abs_x = int((x / 1000) * screenshot.width)
  abs_y = int((y / 1000) * screenshot.height)
  ```

  ```typescript TypeScript theme={"system"}
  const absX = Math.round((x / 1000) * screenshot.width);
  const absY = Math.round((y / 1000) * screenshot.height);
  ```
</CodeGroup>

Origin is top-left. Send and scale against the same image bytes. Any resize, crop, or DPI mismatch will misclick. Pick one pixel unit (CSS or device) and stay in it end to end.

## Observations

Each step, the current screenshot goes in as a `user` message wrapped in `<observation>` tags:

<CodeGroup>
  ```python Python theme={"system"}
  messages.append({"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>"},
  ]})
  ```

  ```typescript TypeScript theme={"system"}
  messages.push({
    role: "user",
    content: [
      { type: "text", text: "<observation>\n" },
      { type: "image_url", image_url: { url: `data:image/png;base64,${b64}` } },
      { type: "text", text: "\n</observation>" },
    ],
  });
  ```
</CodeGroup>

Keep at most the last 3 screenshots in context. More degrades accuracy. Replace older screenshots with a short text placeholder and keep the `<observation>` wrapper:

<CodeGroup>
  ```python Python expandable theme={"system"}
  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)
  ```

  ```typescript TypeScript expandable theme={"system"}
  function trimToLastNImages(messages: any[], n = 3) {
    let seen = 0;
    for (let i = messages.length - 1; i >= 0; i--) {
      const msg = messages[i];
      if (msg.role !== "user" || !Array.isArray(msg.content)) continue;
      for (const chunk of msg.content) {
        if (chunk.type !== "image_url") continue;
        seen += 1;
        if (seen > n) {
          chunk.type = "text";
          chunk.text = "[screenshot evicted]";
          delete chunk.image_url;
        }
      }
    }
  }
  ```
</CodeGroup>

Both loop guides call this helper right after appending each observation.

## Sampling

Agent loops run with `temperature: 0.8`. Single-call tasks such as [element localization](/models-api/element-localization) and [document OCR](/models-api/document-ocr) run with thinking off and `temperature: 0.0`.

## Common pitfalls

| Symptom                              | Likely cause                                                                                                          |
| :----------------------------------- | :-------------------------------------------------------------------------------------------------------------------- |
| Clicks land far from the target      | Coordinates not scaled to screenshot dimensions, or the screenshot was resized between send and execute               |
| Model loops, forgets earlier facts   | Durable facts not carried forward in `content`, or older `<observation>` wrappers dropped instead of stripped to text |
| Context window fills up              | Image budget not enforced                                                                                             |
| Reasoning leaks into the action      | `<think>...</think>` written inline in `content` instead of read from `message.reasoning`                             |
| Quality collapses after one bad step | Raw model output replayed in history instead of the parsed result                                                     |
| Run stops early, or never ends       | Loop exits on an empty tool call instead of on `answer`, or no `answer` tool registered                               |

Format-specific pitfalls are listed on each loop guide.
