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

# Agents

> A reusable configuration: environment, model, skills, instructions. Run a pre-built h/ agent or create your own with the Agents API.

export const CatalogGallery = ({items: itemsProp = null, kind = "agent", baseUrl = "https://agp.eu.hcompany.ai"}) => {
  const [query, setQuery] = useState("");
  const [copied, setCopied] = useState({});
  const [expanded, setExpanded] = useState({});
  const [fetched, setFetched] = useState(null);
  const [status, setStatus] = useState(itemsProp ? "ready" : "loading");
  const idOf = it => it.name || it.id || "";
  const HIDDEN = new Set(["h/holotab", "h/holo-desktop"]);
  useEffect(() => {
    if (itemsProp) return;
    const resource = ({
      agent: "agents",
      skill: "skills",
      environment: "environments"
    })[kind] || "agents";
    let cancelled = false;
    (async () => {
      setStatus("loading");
      try {
        const all = [];
        for (let page = 1; ; page += 1) {
          const resp = await fetch(`${baseUrl}/api/v2/${resource}?name=h/&size=100&page=${page}`, {
            headers: {
              Accept: "application/json"
            }
          });
          if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
          const payload = await resp.json();
          const batch = payload.items || [];
          all.push(...batch);
          const total = payload.total ?? all.length;
          if (!batch.length || all.length >= total) break;
        }
        const visible = all.filter(it => !HIDDEN.has(it.name));
        visible.sort((a, b) => (a.name || a.id || "").localeCompare(b.name || b.id || ""));
        if (!cancelled) {
          setFetched(visible);
          setStatus("ready");
        }
      } catch (e) {
        if (!cancelled) setStatus("error");
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [itemsProp, kind, baseUrl]);
  const items = itemsProp || fetched || [];
  const slug = s => String(s).replace(/[^\w.-]+/g, "-");
  const crc32 = bytes => {
    let c = ~0;
    for (let i = 0; i < bytes.length; i++) {
      c ^= bytes[i];
      for (let j = 0; j < 8; j++) c = c >>> 1 ^ 0xedb88320 & -(c & 1);
    }
    return ~c >>> 0;
  };
  const downloadSkill = it => {
    const enc = new TextEncoder();
    const safe = slug(idOf(it) || "skill");
    const fm = ["---", `name: ${JSON.stringify(idOf(it))}`];
    if (it.description) fm.push(`description: ${JSON.stringify(it.description)}`);
    if (it.source) fm.push(`source: ${JSON.stringify(it.source)}`);
    if (it.url_pattern) fm.push(`url_pattern: ${JSON.stringify(it.url_pattern)}`);
    fm.push("---", "", it.body || "", "");
    const data = enc.encode(fm.join("\n"));
    const name = enc.encode(`${safe}/SKILL.md`);
    const crc = crc32(data);
    const u16 = n => new Uint8Array([n & 255, n >>> 8 & 255]);
    const u32 = n => new Uint8Array([n & 255, n >>> 8 & 255, n >>> 16 & 255, n >>> 24 & 255]);
    const local = [u32(0x04034b50), u16(20), u16(0), u16(0), u16(0), u16(0), u32(crc), u32(data.length), u32(data.length), u16(name.length), u16(0), name, data];
    const localSize = local.reduce((s, c) => s + c.length, 0);
    const central = [u32(0x02014b50), u16(20), u16(20), u16(0), u16(0), u16(0), u16(0), u32(crc), u32(data.length), u32(data.length), u16(name.length), u16(0), u16(0), u16(0), u16(0), u32(0), u32(0), name];
    const centralSize = central.reduce((s, c) => s + c.length, 0);
    const eocd = [u32(0x06054b50), u16(0), u16(0), u16(1), u16(1), u32(centralSize), u32(localSize), u16(0)];
    const parts = [...local, ...central, ...eocd];
    const out = new Uint8Array(parts.reduce((s, c) => s + c.length, 0));
    let off = 0;
    for (const c of parts) {
      out.set(c, off);
      off += c.length;
    }
    const url = URL.createObjectURL(new Blob([out], {
      type: "application/zip"
    }));
    const a = document.createElement("a");
    a.href = url;
    a.download = `${safe}.zip`;
    document.body.appendChild(a);
    a.click();
    a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  };
  const copy = (key, text) => {
    try {
      navigator.clipboard.writeText(text);
    } catch (e) {}
    setCopied(s => ({
      ...s,
      [key]: true
    }));
    setTimeout(() => setCopied(s => ({
      ...s,
      [key]: false
    })), 1500);
  };
  const copyIcon = on => on ? <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
        <path d="M20 6L9 17l-5-5" />
      </svg> : <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
        <rect x="9" y="9" width="12" height="12" rx="2" />
        <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
      </svg>;
  const CopyChip = ({k, text, label}) => <button type="button" onClick={() => copy(k, text)} aria-label={label} title={label} className={`${copied[k] ? "shrink-0 rounded-md p-1.5 transition text-emerald-600 dark:text-emerald-400" : "shrink-0 rounded-md p-1.5 transition text-zinc-400 hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-100"}`}>
      {copyIcon(copied[k])}
    </button>;
  const ExpandChip = ({k}) => <button type="button" onClick={() => setExpanded(s => ({
    ...s,
    [k]: !s[k]
  }))} aria-label={expanded[k] ? "Hide body" : "Show body"} title={expanded[k] ? "Hide body" : "Show body"} className="shrink-0 rounded-md p-1.5 text-zinc-400 transition hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-100">
      <svg className={`${expanded[k] ? "h-4 w-4 rotate-180 transition-transform" : "h-4 w-4 transition-transform"}`} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
        <path d="M6 9l6 6 6-6" />
      </svg>
    </button>;
  const DownloadChip = ({it}) => <button type="button" onClick={() => downloadSkill(it)} aria-label="Download SKILL.md" title="Download SKILL.md" className="shrink-0 rounded-md p-1.5 text-zinc-400 transition hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-100">
      <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
        <path d="M12 3v12" />
        <path d="M7 12l5 5 5-5" />
        <path d="M5 21h14" />
      </svg>
    </button>;
  const glyphs = {
    environment: <svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
        <rect x="3" y="4" width="18" height="16" rx="1.5" />
        <path d="M3 9h18" />
        <path d="M7 6.5h.01M10 6.5h.01" />
      </svg>
  };
  const envKindGlyphs = {
    web: <svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
        <circle cx="12" cy="12" r="9" />
        <path d="M3 12h18" />
        <path d="M12 3c2.5 2.6 3.8 5.7 3.8 9s-1.3 6.4-3.8 9c-2.5-2.6-3.8-5.7-3.8-9s1.3-6.4 3.8-9z" />
      </svg>,
    desktop: <svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
        <rect x="3" y="4" width="18" height="12" rx="1.5" />
        <path d="M8 20h8" />
        <path d="M12 16v4" />
      </svg>,
    code: <svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
        <rect x="3" y="4" width="18" height="16" rx="1.5" />
        <path d="M7.5 9l3 3-3 3" />
        <path d="M13 15h4" />
      </svg>,
    mcp: <svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
        <path d="M9 2v6" />
        <path d="M15 2v6" />
        <path d="M6 8h12v2.5a6 6 0 0 1-12 0z" />
        <path d="M12 16.5V22" />
      </svg>,
    memory: <svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
        <ellipse cx="12" cy="5" rx="8" ry="3" />
        <path d="M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5" />
        <path d="M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6" />
      </svg>
  };
  const ClampedText = ({text, lines = 5}) => {
    const ref = useRef(null);
    const [open, setOpen] = useState(false);
    const [overflows, setOverflows] = useState(false);
    useEffect(() => {
      const el = ref.current;
      if (el) setOverflows(el.scrollHeight - el.clientHeight > 1);
    }, [text]);
    return <div className="min-w-0">
        <div ref={ref} className="whitespace-pre-wrap break-words" style={open ? undefined : {
      display: "-webkit-box",
      WebkitLineClamp: lines,
      WebkitBoxOrient: "vertical",
      overflow: "hidden"
    }}>
          {text}
        </div>
        {overflows || open ? <button type="button" onClick={() => setOpen(o => !o)} className="mt-1 text-xs font-medium text-zinc-500 transition hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100">
            {open ? "Show less" : "Show more"}
          </button> : null}
      </div>;
  };
  const KINDS = {
    agent: {
      noun: "agents",
      rows: it => {
        const out = [["model", it.model || "default"]];
        if (Array.isArray(it.environments) && it.environments.length) out.push(["environment", it.environments.map(e => typeof e === "string" ? e : e.id || e.kind || "inline").join(", ")]);
        if (it.instructions) out.push(["instructions", <ClampedText text={it.instructions} />]);
        if (Array.isArray(it.subagents) && it.subagents.length) out.push(["subagents", it.subagents.map(s => typeof s === "string" ? s : s.name || "").join(", ")]);
        if (Array.isArray(it.skills) && it.skills.length) out.push(["skills", <div className="flex flex-wrap gap-1.5">
              {it.skills.map(s => <a key={s} href={`/agents-api/skills/overview#${slug(s)}`} className="inline-flex items-center rounded-md bg-zinc-100 px-2 py-0.5 font-mono text-xs text-zinc-700 no-underline transition hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700">
                  {s}
                </a>)}
            </div>]);
        return out;
      }
    },
    environment: {
      noun: "environments",
      rows: it => {
        const out = [["kind", it.kind || "?"]];
        const mode = it.mode;
        if (mode && typeof mode === "object") {
          out.push(["mode", mode.type || "?"]);
          if (mode.type === "visual") {
            if (mode.width && mode.height) out.push(["viewport", `${mode.width} x ${mode.height}`]);
            if (mode.markdown) out.push(["markdown", "on"]);
          } else if (mode.type === "text" && mode.chunk_size) {
            out.push(["chunk size", `${mode.chunk_size} chars`]);
          }
        } else if (typeof mode === "string") {
          out.push(["mode", mode]);
          if (it.width && it.height) out.push(["viewport", `${it.width} x ${it.height}`]);
        }
        if (it.start_url) out.push(["start url", it.start_url.replace(/^https?:\/\//, "")]);
        if (it.network && it.network.proxy_url) out.push(["proxy", "configured"]);
        if (it.instructions) out.push(["instructions", <ClampedText text={it.instructions} />]);
        return out;
      }
    },
    skill: {
      noun: "skills",
      rows: it => {
        const out = [];
        if (it.source) out.push(["source", it.source]);
        if (it.url_pattern) out.push(["url pattern", it.url_pattern]);
        return out;
      },
      headerAction: it => <div className="-mr-1 flex shrink-0 items-center gap-0.5">
          <DownloadChip it={it} />
          {it.body ? <ExpandChip k={`${idOf(it)}:body`} /> : null}
        </div>,
      extra: it => it.body && expanded[`${idOf(it)}:body`] ? <div className="mt-5 border-t border-zinc-100 pt-4 dark:border-zinc-800/70">
            <div className="whitespace-pre-wrap text-sm leading-relaxed text-zinc-700 dark:text-zinc-300">
{it.body}
            </div>
          </div> : null
    }
  };
  const cfg = KINDS[kind] || KINDS.agent;
  const q = query.trim().toLowerCase();
  const matches = items.filter(it => !q ? true : [idOf(it), it.description, it.model, it.kind, it.start_url].filter(Boolean).join(" ").toLowerCase().includes(q));
  return <div className="not-prose">
      <div className="relative mb-6 w-72 max-w-full">
        <svg className="pointer-events-none absolute inset-y-0 left-0 my-auto h-4 w-4 text-zinc-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
          <circle cx="11" cy="11" r="7" />
          <path d="M21 21l-4.3-4.3" />
        </svg>
        <input type="text" value={query} onChange={e => setQuery(e.target.value)} placeholder={`Search ${matches.length} ${matches.length === 1 ? cfg.noun.replace(/s$/, "") : cfg.noun}`} className="w-full border-0 border-b border-zinc-200 bg-transparent py-2 pl-7 pr-3 text-sm text-zinc-900 placeholder-zinc-400 transition focus:border-zinc-900 focus:outline-none focus:ring-0 dark:border-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-500 dark:focus:border-zinc-100" />
      </div>

      {status === "loading" ? <div className="py-12 text-center text-sm text-zinc-400 dark:text-zinc-500">Loading {cfg.noun}.</div> : status === "error" ? <div className="py-12 text-center text-sm text-zinc-400 dark:text-zinc-500">
          Could not load {cfg.noun} right now. Please try again later.
        </div> : matches.length === 0 ? <div className="py-12 text-center text-sm text-zinc-400 dark:text-zinc-500">
          {items.length === 0 ? `No built-in ${cfg.noun} are published yet. This list updates automatically as they ship.` : `No ${cfg.noun} match "${query}".`}
        </div> : <div className="space-y-3">
          {matches.map(it => {
    const id = idOf(it);
    const nameKey = `${id}:name`;
    const list = cfg.rows(it);
    const icon = kind === "environment" ? envKindGlyphs[it.kind] || glyphs.environment : glyphs[kind];
    return <div key={id} id={slug(id)} className="min-w-0 scroll-mt-24 rounded-xl border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-950">
                <div className="flex items-start justify-between gap-4">
                  <div className="flex min-w-0 items-center gap-3">
                    {icon ? <span className="shrink-0 text-zinc-900 dark:text-zinc-100">{icon}</span> : null}
                    <button type="button" onClick={() => copy(nameKey, id)} title="Copy name" className="group flex min-w-0 items-center gap-1.5 text-left">
                      <span className="min-w-0 break-words font-mono text-[15px] font-bold tracking-tight text-zinc-900 dark:text-zinc-100">{id}</span>
                      <span className={`${copied[nameKey] ? "shrink-0 transition text-emerald-600 dark:text-emerald-400" : "shrink-0 transition text-zinc-300 opacity-0 group-hover:opacity-100 dark:text-zinc-600"}`}>
                        {copyIcon(copied[nameKey])}
                      </span>
                    </button>
                  </div>
                  {cfg.headerAction ? cfg.headerAction(it) : <div className="-mr-1 shrink-0"><CopyChip k={id} text={JSON.stringify(it, null, 2)} label="Copy JSON" /></div>}
                </div>

                {it.description ? <p className="mt-3 max-w-2xl text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">{it.description}</p> : null}

                {list.length ? <dl className="mt-5 space-y-2">
                    {list.map(([k, v]) => <div key={k} className="flex gap-4 text-sm">
                        <dt className="w-28 shrink-0 text-xs uppercase tracking-wider text-zinc-400 dark:text-zinc-500">{k}</dt>
                        <dd className={`${k === "instructions" || typeof v !== "string" ? "min-w-0 break-words text-zinc-700 dark:text-zinc-300" : "min-w-0 break-words font-mono text-zinc-700 dark:text-zinc-300"}`}>{v}</dd>
                      </div>)}
                  </dl> : null}

                {cfg.extra ? cfg.extra(it) : null}
              </div>;
  })}
        </div>}
    </div>;
};

An agent is the configuration a run needs: the environments it acts in, the model that drives it, and optional skills and instructions. Pass its name as `agent` when you create a [session](/agents-api/sessions/overview).

Names are scoped to your organization. H's pre-built agents and environments live under the reserved `h/` namespace (like `h/web-surfer-flash` and `h/browser`): every organization can use them, and they are read-only (modifying one returns `403`). Agents you create have no prefix.

## Pre-built agents

Run these as-is, with no setup. List them with [`GET /api/v2/agents`](/agents-api/agents/list). Pre-built managers like `h/deep-search-pro` delegate to [subagents](/agents-api/multi-agent).

<CatalogGallery kind="agent" />

## Configure an agent

| Field | Required | Description |
| - | - | - |
| `name` | Yes | The value you pass as `agent` when creating a session. |
| `description` | Yes | One line on what the agent does. A manager reads it to decide whether to delegate a task. |
| `environments` | Yes | The surfaces it acts on, like a [browser](/agents-api/browser/configuration) or a desktop. At most one per kind. `[]` only for a pure [manager](/agents-api/multi-agent) with `subagents` or an agent with [custom tools](/agents-api/custom-tools). |
| `model` | No | The Holo model. Default `holo4-27b`. Any id from the [Models API](/models-api/introduction#models), for example `holo4-35b-a3b` for lower latency. |
| `reasoning_effort` | No | How long the model thinks before each step: `disabled`, `low`, `medium`, `xhigh`. Unset uses the model default (`medium` on Holo4). See the support table below. |
| `instructions` | No | Appended to the system prompt to steer behavior. |
| `skills` | No | [Instruction fragments](/agents-api/skills/overview) the agent loads on demand. |
| `tools` | No | Tools the agent calls in your own code. See [Custom tools](/agents-api/custom-tools). |
| `subagents` | No | Agents this one can delegate to. Each runs as its own child session, in parallel, and returns one answer the manager folds into its own. See [Multi-agent](/agents-api/multi-agent). |
| `answer_format` | No | A [JSON Schema](https://json-schema.org/) the final answer must match ([structured output](/agents-api/structured-output)). Unset returns free-form text. Override per run with session [`overrides`](/agents-api/sessions/overview#overrides). |

Each `environments`, `skills`, or `subagents` entry is a catalog id (string, reusable) or an inline object (handy for one-offs). Exact constraints: [Create an agent](/agents-api/agents/create).

| Model | `reasoning_effort` support |
| - | - |
| `holo4-27b` | `disabled`, `low`, `medium`, `xhigh` all apply. |
| `holo4-35b-a3b` | No effort levels. Only `disabled` changes behavior (thinking off). |
| Holo3, Holo3.1 | Only `disabled` applies (thinking off). Other values keep the default. |

## Create your own

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -X POST https://agp.eu.hcompany.ai/api/v2/agents \
    -H "Authorization: Bearer $HAI_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "web-price-finder",
      "description": "Finds and reports prices on the public web.",
      "instructions": "Return one line: amount, currency, vendor, link. If the price needs a login, say so.",
      "model": "holo4-35b-a3b",
      "environments": [{"id": "price-browser", "kind": "web", "mode": {"type": "visual", "width": 1280, "height": 800}}]
    }'
  ```

  ```python Python theme={"system"}
  from hai_agents import Client

  client = Client()
  client.agents.create_agent(
      name="web-price-finder",
      description="Finds and reports prices on the public web.",
      instructions="Return one line: amount, currency, vendor, link. If the price needs a login, say so.",
      model="holo4-35b-a3b",
      environments=[{"id": "price-browser", "kind": "web", "mode": {"type": "visual", "width": 1280, "height": 800}}],
  )
  ```

  ```typescript TypeScript theme={"system"}
  import { HaiAgentsClient } from "hai-agents";

  const client = new HaiAgentsClient();
  await client.agents.createAgent({
    name: "web-price-finder",
    description: "Finds and reports prices on the public web.",
    instructions: "Return one line: amount, currency, vendor, link. If the price needs a login, say so.",
    model: "holo4-35b-a3b",
    environments: [{ id: "price-browser", kind: "web", mode: { type: "visual", width: 1280, height: 800 } }],
  });
  ```
</CodeGroup>

To run it, see the [Quickstart](/agents-api/quickstart#run-a-pre-built-agent) with `agent="web-price-finder"`.

## Context auto-compaction

Long sessions outgrow the context window, so every agent compacts its own history automatically, possibly several times per session. There is nothing to configure. After a step, once the last model call (prompt plus completion tokens) reaches about 70% of the window, the same model condenses past observations, tool outputs, and reasoning into a short summary. Your task and messages, the agent's notes and plan, earlier answers, and the latest tool calls and screenshots stay verbatim. It shows in [events](/agents-api/sessions/events) as a `flow_event` with `flow: "reset_history"` and `origin: "compactor"`, then a `message_event` from `caller_id: "compactor"` carrying the briefing.


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