Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ export HAI_API_KEY=hk-...

## Quickstart

Launch the built-in `h/web-surfer-holo3-1-35b` 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.
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() # reads HAI_API_KEY from the environment

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

Expand All @@ -73,7 +73,7 @@ You drive a session two ways. `run_session` creates it and blocks until it settl

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

Expand Down Expand Up @@ -110,7 +110,7 @@ By default a session ends as soon as the agent answers. Set `idle_timeout_s` to

```python
session = client.start_session(
agent="h/web-surfer-holo3-1-35b",
agent="h/web-surfer-pro",
idle_timeout_s=600,
messages="Find the top story on Hacker News",
)
Expand All @@ -137,7 +137,7 @@ class Jobs(BaseModel):

client = Client()
result = client.run_session(
agent="h/web-surfer-holo3-1-35b",
agent="h/web-surfer-pro",
messages="Find 3 open ML engineering roles in Paris.",
answer_schema=Jobs,
)
Expand All @@ -162,7 +162,7 @@ def get_weather(city: str) -> str:
client = Client()

result = client.run_session(
agent="h/web-surfer-holo3-1-35b",
agent="h/web-surfer-pro",
messages="What should I wear in Paris today?",
tools=[get_weather],
)
Expand All @@ -186,7 +186,7 @@ Start a session on a browser that already knows the user. A [browser profile](ht

```python
result = client.run_session(
agent="h/web-surfer-holo3-1-35b",
agent="h/web-surfer-pro",
messages="Open my dashboard and report any new alerts",
overrides={
"agent.environments[kind=web].browser_profile_id": "<profile-id>",
Expand All @@ -206,7 +206,7 @@ from hai_agents import AsyncClient
async def main():
client = AsyncClient()
result = await client.run_session(
agent="h/web-surfer-holo3-1-35b",
agent="h/web-surfer-pro",
messages="What are the top 3 stories on Hacker News right now?",
)
print(result.answer)
Expand Down
33 changes: 30 additions & 3 deletions src/hai_agents_cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@
)
)

DEFAULT_AGENT = "h/web-surfer-holo3-1-35b"

console = Console()
err_console = Console(stderr=True)

Expand Down Expand Up @@ -153,7 +151,9 @@ def _parse_overrides(items: list[str]) -> dict[str, Any]:
def run(
ctx: typer.Context,
task: str = typer.Argument(..., help="Task to run."),
agent: str = typer.Option(DEFAULT_AGENT, "--agent", "-a", help="Registered agent name."),
agent: str | None = typer.Option(
None, "--agent", "-a", help="Registered agent name. Omit to pick from a list (see `hai agents list`)."
),
max_steps: int = typer.Option(20, "--max-steps", min=1, max=200, help="Maximum reasoning steps."),
max_time_s: float = typer.Option(180.0, "--max-time", min=1.0, max=1800.0, help="Maximum run seconds."),
override: list[str] = typer.Option(
Expand All @@ -180,6 +180,10 @@ def run(
params["overrides"] = overrides
try:
assert_request_under_limit(params)
except Exception as exc:
_raise_cli_error(exc)
params["agent"] = agent or _select_agent(state, client)
try:
session = client.sessions.create_session(**params)
except Exception as exc:
_raise_cli_error(exc)
Expand Down Expand Up @@ -579,6 +583,29 @@ def _client(state: AppState) -> Client:
_raise_cli_error(exc)


def _select_agent(state: AppState, client: Client) -> str:
"""Pick an agent from the live catalog; require --agent when non-interactive."""
if state.json_output or not (sys.stdin.isatty() and sys.stdout.isatty()):
_raise_cli_error(RuntimeError("No agent specified. Pass --agent (see `hai agents list`)."))
try:
agents = client.agents.list_agents(page=1, size=100).items
except Exception as exc:
_raise_cli_error(exc)
if not agents:
_raise_cli_error(RuntimeError("No agents available. Create one, then pass --agent."))
console.print("Select an agent:")
for i, item in enumerate(agents, 1):
desc = " ".join((item.description or "").split())
if len(desc) > 80:
desc = desc[:77] + "..."
line = f" [bold]{i}[/bold]. {item.name}"
console.print(f"{line} [dim]{desc}[/dim]" if desc else line)
choice = typer.prompt("Agent number", type=int)
if not 1 <= choice <= len(agents):
_raise_cli_error(RuntimeError(f"Choice must be between 1 and {len(agents)}."))
return agents[choice - 1].name


def _print_run_result(result, json_output: bool, agent_view_url: str | None = None) -> None:
payload = {
"session_id": result.id,
Expand Down
6 changes: 3 additions & 3 deletions src/hai_agents_cli/host_skills/hai-agents/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Delegate a task to an autonomous H Company agent (Holo-powered web

# hai-agents

The `hai-agents` MCP server runs H Company's autonomous agents (e.g. the `h/web-surfer-holo3` family) on H Company's infrastructure and streams back a single answer. You hand an agent a self-contained `task`; it opens its own browse-and-act loop in a remote browser and works until it has an answer, hits its step or time budget, or is cancelled. Nothing runs on the user's machine.
The `hai-agents` MCP server runs H Company's autonomous agents on H Company's infrastructure and streams back a single answer. You hand an agent a self-contained `task`; it opens its own browse-and-act loop in a remote browser and works until it has an answer, hits its step or time budget, or is cancelled. Nothing runs on the user's machine.

The agent is blind to this conversation. It sees only the `task` string, so fold in the context it needs: which site or source, what "done" looks like, the shape of the answer you want back. Keep the user's own action verbs and any literal text (search queries, message bodies) verbatim, since the agent grounds well on natural imperatives. Paraphrase by adding context, not by rewording.

Expand Down Expand Up @@ -35,11 +35,11 @@ This loop is the whole protocol: a run that finishes fast returns straight from
User: *"find the cheapest direct flight from Paris to Lisbon next Friday"*

run_agent
agent: h/web-surfer-holo3-1-35b
agent: h/web-surfer-pro
task: On Google Flights, find the cheapest direct (non-stop) flight from Paris (any CDG/ORY) to Lisbon (LIS) departing next Friday, returning the airline, departure and arrival times, and total price in EUR.

User: *"is the H Company Agent API quickstart still showing the curl example"*

run_agent
agent: h/web-surfer-holo3-1-35b
agent: h/web-surfer-pro
task: Open https://hub.hcompany.ai/computer-use-agents/quickstart and confirm whether the quickstart page still shows a curl example. Return yes or no and quote the first line of the example if present.
26 changes: 23 additions & 3 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def test_run_prints_json(monkeypatch) -> None:
monkeypatch.setattr(app_module, "_client", lambda state: _RunClient())
monkeypatch.setattr(app_module, "wait_for_session", _fake_wait_for_session)

result = runner.invoke(app, ["--json", "run", "hello"], env={"HAI_API_KEY": "hk-test"})
result = runner.invoke(app, ["--json", "run", "hello", "-a", "h/test-agent"], env={"HAI_API_KEY": "hk-test"})

assert result.exit_code == 0, result.output
payload = json.loads(result.output)
Expand All @@ -51,12 +51,21 @@ def test_run_prints_live_view_link(monkeypatch) -> None:
monkeypatch.setattr(app_module, "_client", lambda state: _RunClient())
monkeypatch.setattr(app_module, "wait_for_session", _fake_wait_for_session)

result = runner.invoke(app, ["run", "hello"], env={"HAI_API_KEY": "hk-test"})
result = runner.invoke(app, ["run", "hello", "-a", "h/test-agent"], env={"HAI_API_KEY": "hk-test"})

assert result.exit_code == 0, result.output
assert "https://platform.example.test/agent-view/sess_1" in result.output


def test_run_without_agent_requires_selection(monkeypatch) -> None:
monkeypatch.setattr(app_module, "_client", lambda state: _RunClient())

result = runner.invoke(app, ["run", "hello"], env={"HAI_API_KEY": "hk-test"})

assert result.exit_code != 0
assert "--agent" in _error_text(result)


def test_share_session_prints_absolute_url(monkeypatch) -> None:
client = _Client()
monkeypatch.setattr(app_module, "_client", lambda state: client)
Expand Down Expand Up @@ -101,13 +110,22 @@ def test_run_rejects_oversized_payload_before_sending(monkeypatch) -> None:
client = _RunClient(capture=captured)
monkeypatch.setattr(app_module, "_client", lambda state: client)

result = runner.invoke(app, ["run", "x" * (6 * 1024 * 1024)], env={"HAI_API_KEY": "hk-test"})
result = runner.invoke(app, ["run", "x" * (6 * 1024 * 1024), "-a", "h/test-agent"], env={"HAI_API_KEY": "hk-test"})

assert result.exit_code != 0
assert "over the" in _error_text(result)
assert not captured # no HTTP call was attempted


def test_run_validates_payload_before_agent_selection(monkeypatch) -> None:
monkeypatch.setattr(app_module, "_client", lambda state: _RunClient())

result = runner.invoke(app, ["run", "x" * (6 * 1024 * 1024)], env={"HAI_API_KEY": "hk-test"})

assert result.exit_code != 0
assert "over the" in _error_text(result)


def test_run_parses_overrides(monkeypatch) -> None:
captured: dict = {}
client = _RunClient(capture=captured)
Expand All @@ -120,6 +138,8 @@ def test_run_parses_overrides(monkeypatch) -> None:
[
"run",
"hello",
"-a",
"h/test-agent",
"-o",
"agent.environments[kind=web].start_url=https://bing.com",
"-o",
Expand Down