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

# Structured outputs

> The Holo3 and Holo3.1 agent loop with one constrained JSON object per step, and the mistakes specific to this format.

export const ChatStrip = ({rows, repeatFrom = 1}) => {
  const Body = ({text}) => text.split(/(`[^`]+`)/).map((part, i) => part.startsWith("`") ? <code key={i} className="rounded bg-zinc-100 px-1 py-0.5 font-mono text-[12px] text-zinc-800 dark:bg-zinc-800 dark:text-zinc-100">{part.slice(1, -1)}</code> : <span key={i}>{part}</span>);
  const Row = ({role, body}) => <div className="flex items-center gap-3">
      <span className={`${role === "system" ? "w-[84px] shrink-0 rounded-md px-2 py-0.5 text-center font-mono text-xs bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900" : role === "user" ? "w-[84px] shrink-0 rounded-md px-2 py-0.5 text-center font-mono text-xs bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-200" : role === "assistant" ? "w-[84px] shrink-0 rounded-md px-2 py-0.5 text-center font-mono text-xs border border-zinc-300 text-zinc-700 dark:border-zinc-600 dark:text-zinc-200" : "w-[84px] shrink-0 rounded-md px-2 py-0.5 text-center font-mono text-xs border border-dashed border-zinc-400 text-zinc-600 dark:border-zinc-500 dark:text-zinc-300"}`}>{role}</span>
      <span className="text-sm leading-5 text-zinc-700 dark:text-zinc-300"><Body text={body} /></span>
    </div>;
  return <div className="not-prose my-6 overflow-x-auto">
      <div className="flex min-w-[520px] flex-col gap-2 rounded-xl border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-950">
        {rows.slice(0, repeatFrom).map((r, i) => <Row key={i} {...r} />)}
        <div className="my-1 flex items-center gap-3 text-xs text-zinc-500 dark:text-zinc-400">
          <span className="h-px flex-1 bg-zinc-200 dark:bg-zinc-800" />
          <span className="whitespace-nowrap">repeats every step</span>
          <span className="h-px flex-1 bg-zinc-200 dark:bg-zinc-800" />
        </div>
        {rows.slice(repeatFrom).map((r, i) => <Row key={i} {...r} />)}
      </div>
    </div>;
};

export const Notice = ({kind = "note", title, children}) => {
  const kinds = {
    warning: {
      label: "User notice",
      icon: <>
          <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
          <path d="M12 9v4" />
          <path d="M12 17h.01" />
        </>
    },
    gotcha: {
      label: "Gotcha",
      icon: <>
          <circle cx="12" cy="12" r="10" />
          <path d="M12 16v-4" />
          <path d="M12 8h.01" />
        </>
    },
    note: {
      label: "Note",
      icon: <>
          <circle cx="12" cy="12" r="10" />
          <path d="M12 16v-4" />
          <path d="M12 8h.01" />
        </>
    }
  };
  const k = kinds[kind];
  return <div className="notice my-6 rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-800 dark:bg-zinc-950">
      <div className={`${kind === "warning" ? "not-prose flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-red-400/80 dark:text-red-400/70" : kind === "gotcha" ? "not-prose flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-amber-500/80 dark:text-amber-400/70" : "not-prose flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-zinc-400 dark:text-zinc-500"}`}>
        <svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          {k.icon}
        </svg>
        {k.label}
      </div>
      {title && <div className="not-prose mt-2 text-base font-semibold text-zinc-900 dark:text-zinc-100">{title}</div>}
      <div className="notice-body mt-3 text-sm leading-6 text-zinc-700 dark:text-zinc-300">{children}</div>
    </div>;
};

<Notice kind="note" title="Holo3 and Holo3.1 agent loops only">
  This is the agent-loop format for `holo3-122b-a10b` (its only option) and `holo3-1-35b-a3b`. Holo4 agents use [function calling](/models-api/build-an-agent/function-calling). Single-shot JSON, like [element localization](/models-api/element-localization), works on every model.
</Notice>

Each step, the decoder is constrained to emit one JSON object matching your schema, with the tool call as a field inside it, so output is always valid JSON. Read [Core concepts](/models-api/build-an-agent/core-concepts) first.

## Output JSON

```json theme={"system"}
{
  "note": "Submit succeeded; receipt URL is /orders/8421.",
  "thought": "Recording the receipt URL before navigating away.",
  "tool_call": {"tool_name": "click", "element": "Continue button at the bottom right", "x": 932, "y": 880}
}
```

<ResponseField name="note" type="string | null">
  The model's durable memory: facts from the current screen that later steps need (URLs, IDs, partial answers). `null` when nothing is new.
</ResponseField>

<ResponseField name="thought" type="string" required>
  A one-line plan for the next action.
</ResponseField>

<ResponseField name="tool_call" type="object" required>
  One variant of your tool union. Flat: `tool_name` sits next to the arguments, not in a nested `args` object.
</ResponseField>

## Constrain output to a tool union

Define each tool as a Pydantic model with a `Literal` `tool_name`, and use their union as the schema. The decoder emits exactly one variant, and you dispatch on `tool_name`.

```python Python theme={"system"}
from typing import Literal
from pydantic import BaseModel, Field

class ClickArgs(BaseModel):
    """Click at (x, y) coordinates"""
    tool_name: Literal["click"]
    element: str = Field(description="Detailed description of the target UI element to click on")
    x: int = Field(description="X coordinate as integer in [0, 1000]")
    y: int = Field(description="Y coordinate as integer in [0, 1000]")

class WriteArgs(BaseModel):
    """Type text into the currently focused element without clicking first"""
    tool_name: Literal["write"]
    content: str = Field(description="Content to write")
    press_enter: bool = Field(default=False, description="Whether to press Enter after typing")

class AnswerArgs(BaseModel):
    """Provide a final answer"""
    tool_name: Literal["answer"]
    content: str = Field(description="The answer content")

class Step(BaseModel):
    note: str | None = Field(default=None, description="Task-relevant information from the previous observation. Empty if nothing new.")
    thought: str = Field(description="Reasoning about next steps")
    tool_call: ClickArgs | WriteArgs | AnswerArgs
```

In TypeScript, write the same JSON Schema by hand: a `oneOf` of tool objects, each with `tool_name: { const: "..." }`.

<Notice kind="gotcha" title="Send the schema twice">
  Pass it in `extra_body={"structured_outputs": {"json": schema}}` and also append it to the system prompt in an `<output_format>` block. The model was trained with both, so dropping either hurts reliability. Do not use `tools` or `tool_choice`: the action arrives in `content`, not in `tool_calls`.
</Notice>

## Chat layout

Tool results go back as `user` messages wrapped in `<tool_output tool="...">`, not as `tool` messages:

<ChatStrip
  rows={[
{ role: "system", body: "your prompt, then the appended `<output_format>` schema block" },
{ role: "user", body: "`<observation>` screenshot and/or text `</observation>`" },
{ role: "assistant", body: "`{ note, thought, tool_call }`" },
{ role: "user", body: "`<tool_output tool=\"click\">` result `</tool_output>`" },
]}
/>

## A complete loop

Plug in your own `observation()` (the `<observation>` message from [Core concepts](/models-api/build-an-agent/core-concepts#observations)) and `execute(tool_call)`.

````python Python theme={"system"}
import json

schema = Step.model_json_schema()
system = SYSTEM_PROMPT + f"\n\n<output_format>\n```json\n{json.dumps(schema)}\n```\n</output_format>"
messages = [{"role": "system", "content": system}, {"role": "user", "content": task}]

for _ in range(MAX_STEPS):
    messages.append(observation())
    trim_to_last_n_images(messages, n=3)

    resp = client.chat.completions.create(
        model="holo3-1-35b-a3b",
        messages=messages,
        temperature=0.8,
        extra_body={"structured_outputs": {"json": schema}},
    )
    step = Step.model_validate_json(resp.choices[0].message.content)
    messages.append({"role": "assistant", "content": step.model_dump_json()})

    if step.tool_call.tool_name == "answer":
        return step.tool_call.content
    result = execute(step.tool_call)
    messages.append({"role": "user", "content": f'<tool_output tool="{step.tool_call.tool_name}">\n{result}\n</tool_output>'})
````

## Format-specific pitfalls

| Symptom | Likely cause |
| :- | :- |
| Model returns free-form text | `structured_outputs.json` missing, or the schema lacks a `Literal` `tool_name` |
| Tool result has no effect | Sent as a `tool` message instead of a `user` message with a `<tool_output>` wrapper |
| Model forgets earlier facts | `note` left empty: it is the only memory carried between turns |
| Reliability drops | Schema in `structured_outputs` but missing from the `<output_format>` block, or the reverse |


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