From ee8da47aea1e403b1c33907db0421c55a17760d3 Mon Sep 17 00:00:00 2001 From: abonneth Date: Tue, 30 Jun 2026 22:27:40 +0200 Subject: [PATCH 1/3] Replace hardcoded hai run default with live agent picker Co-authored-by: Cursor --- README.md | 16 +++++----- src/hai_agents_cli/app.py | 30 +++++++++++++++++-- .../host_skills/hai-agents/SKILL.md | 6 ++-- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index d9dbeac..85eb2f9 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ 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 @@ -55,7 +55,7 @@ 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?", ) @@ -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", ) @@ -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", ) @@ -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, ) @@ -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], ) @@ -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": "", @@ -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) diff --git a/src/hai_agents_cli/app.py b/src/hai_agents_cli/app.py index 10ab91d..e4bb143 100644 --- a/src/hai_agents_cli/app.py +++ b/src/hai_agents_cli/app.py @@ -33,8 +33,6 @@ ) ) -DEFAULT_AGENT = "h/web-surfer-holo3-1-35b" - console = Console() err_console = Console(stderr=True) @@ -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( @@ -169,6 +169,7 @@ def run( """Run an agent task and print the final answer.""" state = _state(ctx) client = _client(state) + agent = agent or _select_agent(state, client) overrides = _parse_overrides(override or []) params: dict[str, Any] = { "agent": agent, @@ -579,6 +580,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, diff --git a/src/hai_agents_cli/host_skills/hai-agents/SKILL.md b/src/hai_agents_cli/host_skills/hai-agents/SKILL.md index 139e74e..4e90311 100644 --- a/src/hai_agents_cli/host_skills/hai-agents/SKILL.md +++ b/src/hai_agents_cli/host_skills/hai-agents/SKILL.md @@ -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. @@ -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. From 7291aeaee82da051173fd107f3a07caeb4c010a6 Mon Sep 17 00:00:00 2001 From: abonneth Date: Tue, 30 Jun 2026 22:29:36 +0200 Subject: [PATCH 2/3] test: pass --agent in CLI run tests; guard no-agent error Co-authored-by: Cursor --- tests/test_cli.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index a31ade9..9a2726f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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) @@ -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) @@ -101,7 +110,7 @@ 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) @@ -120,6 +129,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", From 1ee7c3b0f55cb737dd036d415e7564ab742832f1 Mon Sep 17 00:00:00 2001 From: abonneth Date: Tue, 30 Jun 2026 22:31:27 +0200 Subject: [PATCH 3/3] Validate payload before resolving agent in hai run Co-authored-by: Cursor --- src/hai_agents_cli/app.py | 5 ++++- tests/test_cli.py | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/hai_agents_cli/app.py b/src/hai_agents_cli/app.py index e4bb143..6dc33ee 100644 --- a/src/hai_agents_cli/app.py +++ b/src/hai_agents_cli/app.py @@ -169,7 +169,6 @@ def run( """Run an agent task and print the final answer.""" state = _state(ctx) client = _client(state) - agent = agent or _select_agent(state, client) overrides = _parse_overrides(override or []) params: dict[str, Any] = { "agent": agent, @@ -181,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) diff --git a/tests/test_cli.py b/tests/test_cli.py index 9a2726f..b629648 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -117,6 +117,15 @@ def test_run_rejects_oversized_payload_before_sending(monkeypatch) -> None: 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)