From cad063bc93e5e9ffb5e738e688592e9ea79db6ce Mon Sep 17 00:00:00 2001 From: abonneth <206544678+abonneth@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:15:33 +0000 Subject: [PATCH 1/3] chore(sdk): sync to agent_platform@474bec4 (v0.1.13) --- openapi.json | 174 ++++++++++++++++++--- pyproject.toml | 2 +- src/hai_agents/__init__.py | 14 +- src/hai_agents/base_client.py | 27 +++- src/hai_agents/core/client_wrapper.py | 4 +- src/hai_agents/environments/client.py | 57 +++---- src/hai_agents/environments/raw_client.py | 69 ++++---- src/hai_agents/errors/__init__.py | 8 +- src/hai_agents/errors/not_found_error.py | 10 ++ src/hai_agents/polling.py | 33 ++++ src/hai_agents/quota/__init__.py | 4 + src/hai_agents/quota/client.py | 100 ++++++++++++ src/hai_agents/quota/raw_client.py | 127 +++++++++++++++ src/hai_agents/sessions/client.py | 23 +-- src/hai_agents/sessions/raw_client.py | 141 ++++++++++------- src/hai_agents/types/__init__.py | 6 + src/hai_agents/types/browser.py | 11 +- src/hai_agents/types/browser_network.py | 26 +++ src/hai_agents/types/token_quota_status.py | 28 ++++ uv.lock | 2 +- 20 files changed, 707 insertions(+), 159 deletions(-) create mode 100644 src/hai_agents/errors/not_found_error.py create mode 100644 src/hai_agents/quota/__init__.py create mode 100644 src/hai_agents/quota/client.py create mode 100644 src/hai_agents/quota/raw_client.py create mode 100644 src/hai_agents/types/browser_network.py create mode 100644 src/hai_agents/types/token_quota_status.py diff --git a/openapi.json b/openapi.json index 798029f..f73da24 100644 --- a/openapi.json +++ b/openapi.json @@ -1240,15 +1240,17 @@ "302": { "description": "Redirect to a presigned URL for a session-owned resource." }, - "422": { - "description": "Validation Error", + "200": { + "description": "Resource bytes. The API redirects to a presigned S3 URL; SDK clients follow the redirect and receive the raw object (e.g. screenshot image bytes).", "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } + "application/octet-stream": {} } + }, + "404": { + "description": "Session or resource not found." + }, + "4XX": { + "description": "Client error." } } } @@ -3296,6 +3298,43 @@ } } } + }, + "/api/v2/quota/tokens": { + "get": { + "tags": [ + "quota" + ], + "summary": "Get Token Quota", + "description": "Return the current org's token-quota snapshot for the FE meter.", + "operationId": "get_token_quota_api_v2_quota_tokens_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TokenQuotaStatus" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } } }, "components": { @@ -3778,12 +3817,6 @@ "title": "Kind", "default": "web" }, - "headless": { - "type": "boolean", - "title": "Headless", - "description": "Run without a visible window.", - "default": false - }, "width": { "type": "integer", "exclusiveMinimum": 0.0, @@ -3847,6 +3880,17 @@ ], "title": "Browser Profile Id", "description": "Id of a browser profile to load into this browser, restoring saved cookies and storage state from a prior session. The profile must belong to the caller's organization. Omit to run with a fresh profile." + }, + "network": { + "anyOf": [ + { + "$ref": "#/components/schemas/BrowserNetwork" + }, + { + "type": "null" + } + ], + "description": "Optional network configuration for the remote browser session. Applied only when a new runner session is provisioned (not when session_id is set)." } }, "type": "object", @@ -3856,6 +3900,25 @@ "title": "Browser", "description": "A web browser the agent navigates and acts on." }, + "BrowserNetwork": { + "properties": { + "proxy_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Proxy Url", + "description": "Optional bring-your-own HTTP/HTTPS/SOCKS proxy URL for browser egress (e.g. http://user:pass@proxy.example.com:8080). Applied when provisioning a new remote browser session. Only supported for headful chromium-based runners. Ignored when session_id attaches to an existing session." + } + }, + "type": "object", + "title": "BrowserNetwork", + "description": "Network egress settings for a remote browser session." + }, "BrowserProfileCreate": { "properties": { "name": { @@ -5014,17 +5077,6 @@ }, "PatchEnvironment": { "properties": { - "headless": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "title": "Headless" - }, "width": { "anyOf": [ { @@ -5123,6 +5175,16 @@ ], "title": "Browser Profile Id" }, + "network": { + "anyOf": [ + { + "$ref": "#/components/schemas/BrowserNetwork" + }, + { + "type": "null" + } + ] + }, "pip_packages": { "anyOf": [ { @@ -5935,6 +5997,70 @@ "title": "Skill", "description": "A named, reusable instruction an agent can draw on during a session." }, + "TokenQuotaStatus": { + "properties": { + "limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Limit" + }, + "used": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Used" + }, + "remaining": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Remaining" + }, + "window_start": { + "type": "string", + "format": "date-time", + "title": "Window Start" + }, + "window_end": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Window End" + } + }, + "type": "object", + "required": [ + "limit", + "used", + "remaining", + "window_start", + "window_end" + ], + "title": "TokenQuotaStatus", + "description": "Current per-org token usage snapshot." + }, "ToolDefinition": { "properties": { "name": { diff --git a/pyproject.toml b/pyproject.toml index dfc27bc..dc6f2e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "hai-agents" -version = "0.1.12" +version = "0.1.13" description = "Python SDK for H Company's Computer-Use Agents: autonomous agents powered by Holo." requires-python = ">=3.10" readme = "README.md" diff --git a/src/hai_agents/__init__.py b/src/hai_agents/__init__.py index 4dd4c1d..9b5a4b3 100644 --- a/src/hai_agents/__init__.py +++ b/src/hai_agents/__init__.py @@ -38,6 +38,7 @@ Browser, BrowserKind, BrowserMode, + BrowserNetwork, BrowserProfileList, BrowserProfileRead, Environment, @@ -100,6 +101,7 @@ SessionSummary, ShareLink, Skill, + TokenQuotaStatus, ToolDefinition, ToolRequest, ToolResultBatch, @@ -122,8 +124,8 @@ WebhookRecord, WebhookWithSecret, ) - from .errors import UnprocessableEntityError - from . import agents, browser_profiles, environments, sessions, skills, vaults, webhooks + from .errors import NotFoundError, UnprocessableEntityError + from . import agents, browser_profiles, environments, quota, sessions, skills, vaults, webhooks from ._default_clients import DefaultAioHttpClient, DefaultAsyncHttpxClient from .agents import ( ListAgentsRequestSortItem, @@ -200,6 +202,7 @@ "Browser": ".types", "BrowserKind": ".types", "BrowserMode": ".types", + "BrowserNetwork": ".types", "BrowserProfileList": ".types", "BrowserProfileRead": ".types", "Client": ".client", @@ -237,6 +240,7 @@ "MetricsUpdateEvent": ".types", "ModelCost": ".types", "ModelUsage": ".types", + "NotFoundError": ".errors", "ObservationEvent": ".types", "OnePasswordConfig": ".types", "OnePasswordConfigProvider": ".types", @@ -286,6 +290,7 @@ "ShareLink": ".types", "Skill": ".types", "TERMINAL_SESSION_STATUSES": ".polling", + "TokenQuotaStatus": ".types", "Tool": ".tools", "ToolDefinition": ".types", "ToolRequest": ".types", @@ -322,6 +327,7 @@ "environments": ".environments", "is_settled_session_status": ".polling", "is_terminal_session_status": ".polling", + "quota": ".quota", "run_session": ".polling", "sessions": ".sessions", "skills": ".skills", @@ -390,6 +396,7 @@ def __dir__(): "Browser", "BrowserKind", "BrowserMode", + "BrowserNetwork", "BrowserProfileList", "BrowserProfileRead", "Client", @@ -427,6 +434,7 @@ def __dir__(): "MetricsUpdateEvent", "ModelCost", "ModelUsage", + "NotFoundError", "ObservationEvent", "OnePasswordConfig", "OnePasswordConfigProvider", @@ -476,6 +484,7 @@ def __dir__(): "ShareLink", "Skill", "TERMINAL_SESSION_STATUSES", + "TokenQuotaStatus", "Tool", "ToolDefinition", "ToolRequest", @@ -512,6 +521,7 @@ def __dir__(): "environments", "is_settled_session_status", "is_terminal_session_status", + "quota", "run_session", "sessions", "skills", diff --git a/src/hai_agents/base_client.py b/src/hai_agents/base_client.py index 072bbcf..394c715 100644 --- a/src/hai_agents/base_client.py +++ b/src/hai_agents/base_client.py @@ -15,6 +15,7 @@ from .agents.client import AgentsClient, AsyncAgentsClient from .browser_profiles.client import AsyncBrowserProfilesClient, BrowserProfilesClient from .environments.client import AsyncEnvironmentsClient, EnvironmentsClient + from .quota.client import AsyncQuotaClient, QuotaClient from .sessions.client import AsyncSessionsClient, SessionsClient from .skills.client import AsyncSkillsClient, SkillsClient from .vaults.client import AsyncVaultsClient, VaultsClient @@ -72,7 +73,7 @@ def __init__( *, base_url: typing.Optional[str] = None, environment: HaiAgentsEnvironment = HaiAgentsEnvironment.EU, - api_key: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("HAI_API_KEY"), + api_key: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, max_retries: typing.Optional[int] = None, @@ -84,6 +85,8 @@ def __init__( timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read ) _defaulted_max_retries = max_retries if max_retries is not None else 2 + if api_key is None: + api_key = os.getenv("HAI_API_KEY") if api_key is None: raise ApiError(body="The client must be instantiated be either passing in api_key or setting HAI_API_KEY") self._client_wrapper = SyncClientWrapper( @@ -106,6 +109,7 @@ def __init__( self._webhooks: typing.Optional[WebhooksClient] = None self._browser_profiles: typing.Optional[BrowserProfilesClient] = None self._vaults: typing.Optional[VaultsClient] = None + self._quota: typing.Optional[QuotaClient] = None @property def sessions(self): @@ -163,6 +167,14 @@ def vaults(self): self._vaults = VaultsClient(client_wrapper=self._client_wrapper) return self._vaults + @property + def quota(self): + if self._quota is None: + from .quota.client import QuotaClient # noqa: E402 + + self._quota = QuotaClient(client_wrapper=self._client_wrapper) + return self._quota + def _make_default_async_client( timeout: typing.Optional[float], @@ -236,7 +248,7 @@ def __init__( *, base_url: typing.Optional[str] = None, environment: HaiAgentsEnvironment = HaiAgentsEnvironment.EU, - api_key: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("HAI_API_KEY"), + api_key: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, headers: typing.Optional[typing.Dict[str, str]] = None, async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, timeout: typing.Optional[float] = None, @@ -249,6 +261,8 @@ def __init__( timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read ) _defaulted_max_retries = max_retries if max_retries is not None else 2 + if api_key is None: + api_key = os.getenv("HAI_API_KEY") if api_key is None: raise ApiError(body="The client must be instantiated be either passing in api_key or setting HAI_API_KEY") self._client_wrapper = AsyncClientWrapper( @@ -270,6 +284,7 @@ def __init__( self._webhooks: typing.Optional[AsyncWebhooksClient] = None self._browser_profiles: typing.Optional[AsyncBrowserProfilesClient] = None self._vaults: typing.Optional[AsyncVaultsClient] = None + self._quota: typing.Optional[AsyncQuotaClient] = None @property def sessions(self): @@ -327,6 +342,14 @@ def vaults(self): self._vaults = AsyncVaultsClient(client_wrapper=self._client_wrapper) return self._vaults + @property + def quota(self): + if self._quota is None: + from .quota.client import AsyncQuotaClient # noqa: E402 + + self._quota = AsyncQuotaClient(client_wrapper=self._client_wrapper) + return self._quota + def _get_base_url(*, base_url: typing.Optional[str] = None, environment: HaiAgentsEnvironment) -> str: if base_url is not None: diff --git a/src/hai_agents/core/client_wrapper.py b/src/hai_agents/core/client_wrapper.py index e4d7637..071fc98 100644 --- a/src/hai_agents/core/client_wrapper.py +++ b/src/hai_agents/core/client_wrapper.py @@ -29,9 +29,9 @@ def get_headers(self) -> typing.Dict[str, str]: import platform headers: typing.Dict[str, str] = { - "User-Agent": "hai_agents/0.1.12", + "User-Agent": "hai_agents/0.1.13", "X-HCompany-Client-Name": "hai_agents", - "X-HCompany-Client-Version": "0.1.12", + "X-HCompany-Client-Version": "0.1.13", "X-HCompany-Client-Type": "sdk", "X-HCompany-Language": "Python", "X-HCompany-Runtime": f"python/{platform.python_version()}", diff --git a/src/hai_agents/environments/client.py b/src/hai_agents/environments/client.py index 52effc8..9978b5a 100644 --- a/src/hai_agents/environments/client.py +++ b/src/hai_agents/environments/client.py @@ -6,6 +6,7 @@ from ..core.request_options import RequestOptions from ..types.browser_kind import BrowserKind from ..types.browser_mode import BrowserMode +from ..types.browser_network import BrowserNetwork from ..types.environment import Environment from ..types.environment_kind import EnvironmentKind from ..types.environment_page import EnvironmentPage @@ -94,7 +95,6 @@ def create_environment( *, id: str, kind: typing.Optional[BrowserKind] = OMIT, - headless: typing.Optional[bool] = OMIT, width: typing.Optional[int] = OMIT, height: typing.Optional[int] = OMIT, start_url: typing.Optional[str] = OMIT, @@ -102,6 +102,7 @@ def create_environment( page_chars: typing.Optional[int] = OMIT, vault_id: typing.Optional[str] = OMIT, browser_profile_id: typing.Optional[str] = OMIT, + network: typing.Optional[BrowserNetwork] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> Environment: """ @@ -114,9 +115,6 @@ def create_environment( kind : typing.Optional[BrowserKind] - headless : typing.Optional[bool] - Run without a visible window. - width : typing.Optional[int] Viewport width in pixels. @@ -138,6 +136,9 @@ def create_environment( browser_profile_id : typing.Optional[str] Id of a browser profile to load into this browser, restoring saved cookies and storage state from a prior session. The profile must belong to the caller's organization. Omit to run with a fresh profile. + network : typing.Optional[BrowserNetwork] + Optional network configuration for the remote browser session. Applied only when a new runner session is provisioned (not when session_id is set). + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -160,7 +161,6 @@ def create_environment( _response = self._raw_client.create_environment( id=id, kind=kind, - headless=headless, width=width, height=height, start_url=start_url, @@ -168,6 +168,7 @@ def create_environment( page_chars=page_chars, vault_id=vault_id, browser_profile_id=browser_profile_id, + network=network, request_options=request_options, ) return _response.data @@ -208,7 +209,6 @@ def update_environment( *, id: str, kind: typing.Optional[BrowserKind] = OMIT, - headless: typing.Optional[bool] = OMIT, width: typing.Optional[int] = OMIT, height: typing.Optional[int] = OMIT, start_url: typing.Optional[str] = OMIT, @@ -216,6 +216,7 @@ def update_environment( page_chars: typing.Optional[int] = OMIT, vault_id: typing.Optional[str] = OMIT, browser_profile_id: typing.Optional[str] = OMIT, + network: typing.Optional[BrowserNetwork] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> Environment: """ @@ -230,9 +231,6 @@ def update_environment( kind : typing.Optional[BrowserKind] - headless : typing.Optional[bool] - Run without a visible window. - width : typing.Optional[int] Viewport width in pixels. @@ -254,6 +252,9 @@ def update_environment( browser_profile_id : typing.Optional[str] Id of a browser profile to load into this browser, restoring saved cookies and storage state from a prior session. The profile must belong to the caller's organization. Omit to run with a fresh profile. + network : typing.Optional[BrowserNetwork] + Optional network configuration for the remote browser session. Applied only when a new runner session is provisioned (not when session_id is set). + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -278,7 +279,6 @@ def update_environment( id_, id=id, kind=kind, - headless=headless, width=width, height=height, start_url=start_url, @@ -286,6 +286,7 @@ def update_environment( page_chars=page_chars, vault_id=vault_id, browser_profile_id=browser_profile_id, + network=network, request_options=request_options, ) return _response.data @@ -323,7 +324,6 @@ def patch_environment( self, id: str, *, - headless: typing.Optional[bool] = OMIT, width: typing.Optional[int] = OMIT, height: typing.Optional[int] = OMIT, start_url: typing.Optional[str] = OMIT, @@ -332,6 +332,7 @@ def patch_environment( page_chars: typing.Optional[int] = OMIT, vault_id: typing.Optional[str] = OMIT, browser_profile_id: typing.Optional[str] = OMIT, + network: typing.Optional[BrowserNetwork] = OMIT, pip_packages: typing.Optional[typing.Sequence[str]] = OMIT, env: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, mcp_servers: typing.Optional[typing.Sequence[McpServer]] = OMIT, @@ -346,8 +347,6 @@ def patch_environment( ---------- id : str - headless : typing.Optional[bool] - width : typing.Optional[int] height : typing.Optional[int] @@ -364,6 +363,8 @@ def patch_environment( browser_profile_id : typing.Optional[str] + network : typing.Optional[BrowserNetwork] + pip_packages : typing.Optional[typing.Sequence[str]] env : typing.Optional[typing.Dict[str, typing.Optional[str]]] @@ -395,7 +396,6 @@ def patch_environment( """ _response = self._raw_client.patch_environment( id, - headless=headless, width=width, height=height, start_url=start_url, @@ -404,6 +404,7 @@ def patch_environment( page_chars=page_chars, vault_id=vault_id, browser_profile_id=browser_profile_id, + network=network, pip_packages=pip_packages, env=env, mcp_servers=mcp_servers, @@ -498,7 +499,6 @@ async def create_environment( *, id: str, kind: typing.Optional[BrowserKind] = OMIT, - headless: typing.Optional[bool] = OMIT, width: typing.Optional[int] = OMIT, height: typing.Optional[int] = OMIT, start_url: typing.Optional[str] = OMIT, @@ -506,6 +506,7 @@ async def create_environment( page_chars: typing.Optional[int] = OMIT, vault_id: typing.Optional[str] = OMIT, browser_profile_id: typing.Optional[str] = OMIT, + network: typing.Optional[BrowserNetwork] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> Environment: """ @@ -518,9 +519,6 @@ async def create_environment( kind : typing.Optional[BrowserKind] - headless : typing.Optional[bool] - Run without a visible window. - width : typing.Optional[int] Viewport width in pixels. @@ -542,6 +540,9 @@ async def create_environment( browser_profile_id : typing.Optional[str] Id of a browser profile to load into this browser, restoring saved cookies and storage state from a prior session. The profile must belong to the caller's organization. Omit to run with a fresh profile. + network : typing.Optional[BrowserNetwork] + Optional network configuration for the remote browser session. Applied only when a new runner session is provisioned (not when session_id is set). + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -572,7 +573,6 @@ async def main() -> None: _response = await self._raw_client.create_environment( id=id, kind=kind, - headless=headless, width=width, height=height, start_url=start_url, @@ -580,6 +580,7 @@ async def main() -> None: page_chars=page_chars, vault_id=vault_id, browser_profile_id=browser_profile_id, + network=network, request_options=request_options, ) return _response.data @@ -628,7 +629,6 @@ async def update_environment( *, id: str, kind: typing.Optional[BrowserKind] = OMIT, - headless: typing.Optional[bool] = OMIT, width: typing.Optional[int] = OMIT, height: typing.Optional[int] = OMIT, start_url: typing.Optional[str] = OMIT, @@ -636,6 +636,7 @@ async def update_environment( page_chars: typing.Optional[int] = OMIT, vault_id: typing.Optional[str] = OMIT, browser_profile_id: typing.Optional[str] = OMIT, + network: typing.Optional[BrowserNetwork] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> Environment: """ @@ -650,9 +651,6 @@ async def update_environment( kind : typing.Optional[BrowserKind] - headless : typing.Optional[bool] - Run without a visible window. - width : typing.Optional[int] Viewport width in pixels. @@ -674,6 +672,9 @@ async def update_environment( browser_profile_id : typing.Optional[str] Id of a browser profile to load into this browser, restoring saved cookies and storage state from a prior session. The profile must belong to the caller's organization. Omit to run with a fresh profile. + network : typing.Optional[BrowserNetwork] + Optional network configuration for the remote browser session. Applied only when a new runner session is provisioned (not when session_id is set). + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -706,7 +707,6 @@ async def main() -> None: id_, id=id, kind=kind, - headless=headless, width=width, height=height, start_url=start_url, @@ -714,6 +714,7 @@ async def main() -> None: page_chars=page_chars, vault_id=vault_id, browser_profile_id=browser_profile_id, + network=network, request_options=request_options, ) return _response.data @@ -759,7 +760,6 @@ async def patch_environment( self, id: str, *, - headless: typing.Optional[bool] = OMIT, width: typing.Optional[int] = OMIT, height: typing.Optional[int] = OMIT, start_url: typing.Optional[str] = OMIT, @@ -768,6 +768,7 @@ async def patch_environment( page_chars: typing.Optional[int] = OMIT, vault_id: typing.Optional[str] = OMIT, browser_profile_id: typing.Optional[str] = OMIT, + network: typing.Optional[BrowserNetwork] = OMIT, pip_packages: typing.Optional[typing.Sequence[str]] = OMIT, env: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, mcp_servers: typing.Optional[typing.Sequence[McpServer]] = OMIT, @@ -782,8 +783,6 @@ async def patch_environment( ---------- id : str - headless : typing.Optional[bool] - width : typing.Optional[int] height : typing.Optional[int] @@ -800,6 +799,8 @@ async def patch_environment( browser_profile_id : typing.Optional[str] + network : typing.Optional[BrowserNetwork] + pip_packages : typing.Optional[typing.Sequence[str]] env : typing.Optional[typing.Dict[str, typing.Optional[str]]] @@ -839,7 +840,6 @@ async def main() -> None: """ _response = await self._raw_client.patch_environment( id, - headless=headless, width=width, height=height, start_url=start_url, @@ -848,6 +848,7 @@ async def main() -> None: page_chars=page_chars, vault_id=vault_id, browser_profile_id=browser_profile_id, + network=network, pip_packages=pip_packages, env=env, mcp_servers=mcp_servers, diff --git a/src/hai_agents/environments/raw_client.py b/src/hai_agents/environments/raw_client.py index 07494a5..6e348d1 100644 --- a/src/hai_agents/environments/raw_client.py +++ b/src/hai_agents/environments/raw_client.py @@ -14,6 +14,7 @@ from ..errors.unprocessable_entity_error import UnprocessableEntityError from ..types.browser_kind import BrowserKind from ..types.browser_mode import BrowserMode +from ..types.browser_network import BrowserNetwork from ..types.environment import Environment from ..types.environment_kind import EnvironmentKind from ..types.environment_page import EnvironmentPage @@ -121,7 +122,6 @@ def create_environment( *, id: str, kind: typing.Optional[BrowserKind] = OMIT, - headless: typing.Optional[bool] = OMIT, width: typing.Optional[int] = OMIT, height: typing.Optional[int] = OMIT, start_url: typing.Optional[str] = OMIT, @@ -129,6 +129,7 @@ def create_environment( page_chars: typing.Optional[int] = OMIT, vault_id: typing.Optional[str] = OMIT, browser_profile_id: typing.Optional[str] = OMIT, + network: typing.Optional[BrowserNetwork] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[Environment]: """ @@ -141,9 +142,6 @@ def create_environment( kind : typing.Optional[BrowserKind] - headless : typing.Optional[bool] - Run without a visible window. - width : typing.Optional[int] Viewport width in pixels. @@ -165,6 +163,9 @@ def create_environment( browser_profile_id : typing.Optional[str] Id of a browser profile to load into this browser, restoring saved cookies and storage state from a prior session. The profile must belong to the caller's organization. Omit to run with a fresh profile. + network : typing.Optional[BrowserNetwork] + Optional network configuration for the remote browser session. Applied only when a new runner session is provisioned (not when session_id is set). + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -179,7 +180,6 @@ def create_environment( json={ "id": id, "kind": kind, - "headless": headless, "width": width, "height": height, "start_url": start_url, @@ -187,6 +187,9 @@ def create_environment( "page_chars": page_chars, "vault_id": vault_id, "browser_profile_id": browser_profile_id, + "network": convert_and_respect_annotation_metadata( + object_=network, annotation=typing.Optional[BrowserNetwork], direction="write" + ), }, headers={ "content-type": "application/json", @@ -283,7 +286,6 @@ def update_environment( *, id: str, kind: typing.Optional[BrowserKind] = OMIT, - headless: typing.Optional[bool] = OMIT, width: typing.Optional[int] = OMIT, height: typing.Optional[int] = OMIT, start_url: typing.Optional[str] = OMIT, @@ -291,6 +293,7 @@ def update_environment( page_chars: typing.Optional[int] = OMIT, vault_id: typing.Optional[str] = OMIT, browser_profile_id: typing.Optional[str] = OMIT, + network: typing.Optional[BrowserNetwork] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[Environment]: """ @@ -305,9 +308,6 @@ def update_environment( kind : typing.Optional[BrowserKind] - headless : typing.Optional[bool] - Run without a visible window. - width : typing.Optional[int] Viewport width in pixels. @@ -329,6 +329,9 @@ def update_environment( browser_profile_id : typing.Optional[str] Id of a browser profile to load into this browser, restoring saved cookies and storage state from a prior session. The profile must belong to the caller's organization. Omit to run with a fresh profile. + network : typing.Optional[BrowserNetwork] + Optional network configuration for the remote browser session. Applied only when a new runner session is provisioned (not when session_id is set). + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -343,7 +346,6 @@ def update_environment( json={ "id": id, "kind": kind, - "headless": headless, "width": width, "height": height, "start_url": start_url, @@ -351,6 +353,9 @@ def update_environment( "page_chars": page_chars, "vault_id": vault_id, "browser_profile_id": browser_profile_id, + "network": convert_and_respect_annotation_metadata( + object_=network, annotation=typing.Optional[BrowserNetwork], direction="write" + ), }, headers={ "content-type": "application/json", @@ -437,7 +442,6 @@ def patch_environment( self, id: str, *, - headless: typing.Optional[bool] = OMIT, width: typing.Optional[int] = OMIT, height: typing.Optional[int] = OMIT, start_url: typing.Optional[str] = OMIT, @@ -446,6 +450,7 @@ def patch_environment( page_chars: typing.Optional[int] = OMIT, vault_id: typing.Optional[str] = OMIT, browser_profile_id: typing.Optional[str] = OMIT, + network: typing.Optional[BrowserNetwork] = OMIT, pip_packages: typing.Optional[typing.Sequence[str]] = OMIT, env: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, mcp_servers: typing.Optional[typing.Sequence[McpServer]] = OMIT, @@ -460,8 +465,6 @@ def patch_environment( ---------- id : str - headless : typing.Optional[bool] - width : typing.Optional[int] height : typing.Optional[int] @@ -478,6 +481,8 @@ def patch_environment( browser_profile_id : typing.Optional[str] + network : typing.Optional[BrowserNetwork] + pip_packages : typing.Optional[typing.Sequence[str]] env : typing.Optional[typing.Dict[str, typing.Optional[str]]] @@ -500,7 +505,6 @@ def patch_environment( f"api/v2/environments/{encode_path_param(id)}", method="PATCH", json={ - "headless": headless, "width": width, "height": height, "start_url": start_url, @@ -509,6 +513,9 @@ def patch_environment( "page_chars": page_chars, "vault_id": vault_id, "browser_profile_id": browser_profile_id, + "network": convert_and_respect_annotation_metadata( + object_=network, annotation=typing.Optional[BrowserNetwork], direction="write" + ), "pip_packages": pip_packages, "env": env, "mcp_servers": convert_and_respect_annotation_metadata( @@ -650,7 +657,6 @@ async def create_environment( *, id: str, kind: typing.Optional[BrowserKind] = OMIT, - headless: typing.Optional[bool] = OMIT, width: typing.Optional[int] = OMIT, height: typing.Optional[int] = OMIT, start_url: typing.Optional[str] = OMIT, @@ -658,6 +664,7 @@ async def create_environment( page_chars: typing.Optional[int] = OMIT, vault_id: typing.Optional[str] = OMIT, browser_profile_id: typing.Optional[str] = OMIT, + network: typing.Optional[BrowserNetwork] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[Environment]: """ @@ -670,9 +677,6 @@ async def create_environment( kind : typing.Optional[BrowserKind] - headless : typing.Optional[bool] - Run without a visible window. - width : typing.Optional[int] Viewport width in pixels. @@ -694,6 +698,9 @@ async def create_environment( browser_profile_id : typing.Optional[str] Id of a browser profile to load into this browser, restoring saved cookies and storage state from a prior session. The profile must belong to the caller's organization. Omit to run with a fresh profile. + network : typing.Optional[BrowserNetwork] + Optional network configuration for the remote browser session. Applied only when a new runner session is provisioned (not when session_id is set). + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -708,7 +715,6 @@ async def create_environment( json={ "id": id, "kind": kind, - "headless": headless, "width": width, "height": height, "start_url": start_url, @@ -716,6 +722,9 @@ async def create_environment( "page_chars": page_chars, "vault_id": vault_id, "browser_profile_id": browser_profile_id, + "network": convert_and_respect_annotation_metadata( + object_=network, annotation=typing.Optional[BrowserNetwork], direction="write" + ), }, headers={ "content-type": "application/json", @@ -812,7 +821,6 @@ async def update_environment( *, id: str, kind: typing.Optional[BrowserKind] = OMIT, - headless: typing.Optional[bool] = OMIT, width: typing.Optional[int] = OMIT, height: typing.Optional[int] = OMIT, start_url: typing.Optional[str] = OMIT, @@ -820,6 +828,7 @@ async def update_environment( page_chars: typing.Optional[int] = OMIT, vault_id: typing.Optional[str] = OMIT, browser_profile_id: typing.Optional[str] = OMIT, + network: typing.Optional[BrowserNetwork] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[Environment]: """ @@ -834,9 +843,6 @@ async def update_environment( kind : typing.Optional[BrowserKind] - headless : typing.Optional[bool] - Run without a visible window. - width : typing.Optional[int] Viewport width in pixels. @@ -858,6 +864,9 @@ async def update_environment( browser_profile_id : typing.Optional[str] Id of a browser profile to load into this browser, restoring saved cookies and storage state from a prior session. The profile must belong to the caller's organization. Omit to run with a fresh profile. + network : typing.Optional[BrowserNetwork] + Optional network configuration for the remote browser session. Applied only when a new runner session is provisioned (not when session_id is set). + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -872,7 +881,6 @@ async def update_environment( json={ "id": id, "kind": kind, - "headless": headless, "width": width, "height": height, "start_url": start_url, @@ -880,6 +888,9 @@ async def update_environment( "page_chars": page_chars, "vault_id": vault_id, "browser_profile_id": browser_profile_id, + "network": convert_and_respect_annotation_metadata( + object_=network, annotation=typing.Optional[BrowserNetwork], direction="write" + ), }, headers={ "content-type": "application/json", @@ -966,7 +977,6 @@ async def patch_environment( self, id: str, *, - headless: typing.Optional[bool] = OMIT, width: typing.Optional[int] = OMIT, height: typing.Optional[int] = OMIT, start_url: typing.Optional[str] = OMIT, @@ -975,6 +985,7 @@ async def patch_environment( page_chars: typing.Optional[int] = OMIT, vault_id: typing.Optional[str] = OMIT, browser_profile_id: typing.Optional[str] = OMIT, + network: typing.Optional[BrowserNetwork] = OMIT, pip_packages: typing.Optional[typing.Sequence[str]] = OMIT, env: typing.Optional[typing.Dict[str, typing.Optional[str]]] = OMIT, mcp_servers: typing.Optional[typing.Sequence[McpServer]] = OMIT, @@ -989,8 +1000,6 @@ async def patch_environment( ---------- id : str - headless : typing.Optional[bool] - width : typing.Optional[int] height : typing.Optional[int] @@ -1007,6 +1016,8 @@ async def patch_environment( browser_profile_id : typing.Optional[str] + network : typing.Optional[BrowserNetwork] + pip_packages : typing.Optional[typing.Sequence[str]] env : typing.Optional[typing.Dict[str, typing.Optional[str]]] @@ -1029,7 +1040,6 @@ async def patch_environment( f"api/v2/environments/{encode_path_param(id)}", method="PATCH", json={ - "headless": headless, "width": width, "height": height, "start_url": start_url, @@ -1038,6 +1048,9 @@ async def patch_environment( "page_chars": page_chars, "vault_id": vault_id, "browser_profile_id": browser_profile_id, + "network": convert_and_respect_annotation_metadata( + object_=network, annotation=typing.Optional[BrowserNetwork], direction="write" + ), "pip_packages": pip_packages, "env": env, "mcp_servers": convert_and_respect_annotation_metadata( diff --git a/src/hai_agents/errors/__init__.py b/src/hai_agents/errors/__init__.py index 1b96959..ff6cb74 100644 --- a/src/hai_agents/errors/__init__.py +++ b/src/hai_agents/errors/__init__.py @@ -6,8 +6,12 @@ from importlib import import_module if typing.TYPE_CHECKING: + from .not_found_error import NotFoundError from .unprocessable_entity_error import UnprocessableEntityError -_dynamic_imports: typing.Dict[str, str] = {"UnprocessableEntityError": ".unprocessable_entity_error"} +_dynamic_imports: typing.Dict[str, str] = { + "NotFoundError": ".not_found_error", + "UnprocessableEntityError": ".unprocessable_entity_error", +} def __getattr__(attr_name: str) -> typing.Any: @@ -31,4 +35,4 @@ def __dir__(): return sorted(lazy_attrs) -__all__ = ["UnprocessableEntityError"] +__all__ = ["NotFoundError", "UnprocessableEntityError"] diff --git a/src/hai_agents/errors/not_found_error.py b/src/hai_agents/errors/not_found_error.py new file mode 100644 index 0000000..75f557d --- /dev/null +++ b/src/hai_agents/errors/not_found_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError + + +class NotFoundError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=404, headers=headers, body=body) diff --git a/src/hai_agents/polling.py b/src/hai_agents/polling.py index 8ccbe0b..3d8a9d9 100644 --- a/src/hai_agents/polling.py +++ b/src/hai_agents/polling.py @@ -15,6 +15,7 @@ from .core.api_error import ApiError from .core.request_options import RequestOptions +from .sessions import SendSessionMessagesRequestBody_UserMessage from .tools import Tool, ToolInput, as_tools from .types.session_request_agent import SessionRequestAgent from .types.session_request_messages import SessionRequestMessages @@ -402,6 +403,20 @@ def wait_for_session( status = client.sessions.get_session_status(id) if is_settled_session_status(status.status): + if include_events: + while True: + if deadline is not None and time.monotonic() >= deadline: + raise TimeoutError(f"Session {id} did not settle within {timeout_seconds}s") + tail = client.sessions.get_session_changes( + id, from_index=next_from_index, limit=limit, include_events=True, wait_for_seconds=0 + ) + batch = tail.new_events or [] if tail is not None else [] + if not batch: + break + if tail.answer is not None: + last_changes = tail + events.extend(batch) + next_from_index += len(batch) changes = _final_changes(client, id, last_changes, limit) raw = changes.answer if changes is not None else None return SessionRunResult( @@ -531,6 +546,20 @@ async def async_wait_for_session( status = await client.sessions.get_session_status(id) if is_settled_session_status(status.status): + if include_events: + while True: + if deadline is not None and time.monotonic() >= deadline: + raise TimeoutError(f"Session {id} did not settle within {timeout_seconds}s") + tail = await client.sessions.get_session_changes( + id, from_index=next_from_index, limit=limit, include_events=True, wait_for_seconds=0 + ) + batch = tail.new_events or [] if tail is not None else [] + if not batch: + break + if tail.answer is not None: + last_changes = tail + events.extend(batch) + next_from_index += len(batch) changes = await _async_final_changes(client, id, last_changes, limit) raw = changes.answer if changes is not None else None return SessionRunResult( @@ -735,6 +764,8 @@ def changes(self, *, from_index: int = 0, **kwargs: typing.Any) -> typing.Option return self._client.sessions.get_session_changes(self.id, from_index=from_index, **kwargs) def send_message(self, message: typing.Any) -> None: + if isinstance(message, str): + message = SendSessionMessagesRequestBody_UserMessage(message=message) self._client.sessions.send_session_messages(self.id, request=message) def pause(self) -> None: @@ -801,6 +832,8 @@ async def changes(self, *, from_index: int = 0, **kwargs: typing.Any) -> typing. return await self._client.sessions.get_session_changes(self.id, from_index=from_index, **kwargs) async def send_message(self, message: typing.Any) -> None: + if isinstance(message, str): + message = SendSessionMessagesRequestBody_UserMessage(message=message) await self._client.sessions.send_session_messages(self.id, request=message) async def pause(self) -> None: diff --git a/src/hai_agents/quota/__init__.py b/src/hai_agents/quota/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/hai_agents/quota/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/hai_agents/quota/client.py b/src/hai_agents/quota/client.py new file mode 100644 index 0000000..6ed75ed --- /dev/null +++ b/src/hai_agents/quota/client.py @@ -0,0 +1,100 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.token_quota_status import TokenQuotaStatus +from .raw_client import AsyncRawQuotaClient, RawQuotaClient + + +class QuotaClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawQuotaClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawQuotaClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawQuotaClient + """ + return self._raw_client + + def get_token_quota(self, *, request_options: typing.Optional[RequestOptions] = None) -> TokenQuotaStatus: + """ + Return the current org's token-quota snapshot for the FE meter. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TokenQuotaStatus + Successful Response + + Examples + -------- + from hai_agents import Client + + client = Client( + api_key="YOUR_API_KEY", + ) + client.quota.get_token_quota() + """ + _response = self._raw_client.get_token_quota(request_options=request_options) + return _response.data + + +class AsyncQuotaClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawQuotaClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawQuotaClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawQuotaClient + """ + return self._raw_client + + async def get_token_quota(self, *, request_options: typing.Optional[RequestOptions] = None) -> TokenQuotaStatus: + """ + Return the current org's token-quota snapshot for the FE meter. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TokenQuotaStatus + Successful Response + + Examples + -------- + import asyncio + + from hai_agents import AsyncClient + + client = AsyncClient( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.quota.get_token_quota() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_token_quota(request_options=request_options) + return _response.data diff --git a/src/hai_agents/quota/raw_client.py b/src/hai_agents/quota/raw_client.py new file mode 100644 index 0000000..4a196e6 --- /dev/null +++ b/src/hai_agents/quota/raw_client.py @@ -0,0 +1,127 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.parse_error import ParsingError +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..errors.unprocessable_entity_error import UnprocessableEntityError +from ..types.http_validation_error import HttpValidationError +from ..types.token_quota_status import TokenQuotaStatus +from pydantic import ValidationError + + +class RawQuotaClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_token_quota( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[TokenQuotaStatus]: + """ + Return the current org's token-quota snapshot for the FE meter. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TokenQuotaStatus] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + "api/v2/quota/tokens", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TokenQuotaStatus, + parse_obj_as( + type_=TokenQuotaStatus, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + HttpValidationError, + parse_obj_as( + type_=HttpValidationError, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawQuotaClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_token_quota( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[TokenQuotaStatus]: + """ + Return the current org's token-quota snapshot for the FE meter. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TokenQuotaStatus] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v2/quota/tokens", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TokenQuotaStatus, + parse_obj_as( + type_=TokenQuotaStatus, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 422: + raise UnprocessableEntityError( + headers=dict(_response.headers), + body=typing.cast( + HttpValidationError, + parse_obj_as( + type_=HttpValidationError, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/hai_agents/sessions/client.py b/src/hai_agents/sessions/client.py index 1b20506..5e24f5b 100644 --- a/src/hai_agents/sessions/client.py +++ b/src/hai_agents/sessions/client.py @@ -752,7 +752,7 @@ def unshare_session(self, id: str, *, request_options: typing.Optional[RequestOp def get_session_resource( self, id: str, bucket: str, key: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> None: + ) -> typing.Iterator[bytes]: """ Redirect to a presigned S3 URL for a session-owned resource. @@ -765,11 +765,12 @@ def get_session_resource( key : str request_options : typing.Optional[RequestOptions] - Request-specific configuration. + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. Returns ------- - None + typing.Iterator[bytes] + Resource bytes. The API redirects to a presigned S3 URL; SDK clients follow the redirect and receive the raw object (e.g. screenshot image bytes). Examples -------- @@ -784,8 +785,8 @@ def get_session_resource( key="key", ) """ - _response = self._raw_client.get_session_resource(id, bucket, key, request_options=request_options) - return _response.data + with self._raw_client.get_session_resource(id, bucket, key, request_options=request_options) as r: + yield from r.data class AsyncSessionsClient: @@ -1654,7 +1655,7 @@ async def main() -> None: async def get_session_resource( self, id: str, bucket: str, key: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> None: + ) -> typing.AsyncIterator[bytes]: """ Redirect to a presigned S3 URL for a session-owned resource. @@ -1667,11 +1668,12 @@ async def get_session_resource( key : str request_options : typing.Optional[RequestOptions] - Request-specific configuration. + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. Returns ------- - None + typing.AsyncIterator[bytes] + Resource bytes. The API redirects to a presigned S3 URL; SDK clients follow the redirect and receive the raw object (e.g. screenshot image bytes). Examples -------- @@ -1694,5 +1696,6 @@ async def main() -> None: asyncio.run(main()) """ - _response = await self._raw_client.get_session_resource(id, bucket, key, request_options=request_options) - return _response.data + async with self._raw_client.get_session_resource(id, bucket, key, request_options=request_options) as r: + async for _chunk in r.data: + yield _chunk diff --git a/src/hai_agents/sessions/raw_client.py b/src/hai_agents/sessions/raw_client.py index d2d30f1..daf2ab9 100644 --- a/src/hai_agents/sessions/raw_client.py +++ b/src/hai_agents/sessions/raw_client.py @@ -1,5 +1,6 @@ # This file was auto-generated by Fern from our API Definition. +import contextlib import datetime as dt import typing from json.decoder import JSONDecodeError @@ -13,6 +14,7 @@ from ..core.pydantic_utilities import parse_obj_as from ..core.request_options import RequestOptions from ..core.serialization import convert_and_respect_annotation_metadata +from ..errors.not_found_error import NotFoundError from ..errors.unprocessable_entity_error import UnprocessableEntityError from ..types.http_validation_error import HttpValidationError from ..types.page_session_event import PageSessionEvent @@ -1083,9 +1085,10 @@ def unshare_session( ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + @contextlib.contextmanager def get_session_resource( self, id: str, bucket: str, key: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[None]: + ) -> typing.Iterator[HttpResponse[typing.Iterator[bytes]]]: """ Redirect to a presigned S3 URL for a session-owned resource. @@ -1098,39 +1101,53 @@ def get_session_resource( key : str request_options : typing.Optional[RequestOptions] - Request-specific configuration. + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. Returns ------- - HttpResponse[None] + typing.Iterator[HttpResponse[typing.Iterator[bytes]]] + Resource bytes. The API redirects to a presigned S3 URL; SDK clients follow the redirect and receive the raw object (e.g. screenshot image bytes). """ - _response = self._client_wrapper.httpx_client.request( + with self._client_wrapper.httpx_client.stream( f"api/v2/sessions/{encode_path_param(id)}/resources/{encode_path_param(bucket)}/{encode_path_param(key)}", method="GET", request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return HttpResponse(response=_response, data=None) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - HttpValidationError, - parse_obj_as( - type_=HttpValidationError, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + ) as _response: + + def _stream() -> HttpResponse[typing.Iterator[bytes]]: + try: + if 200 <= _response.status_code < 300: + _chunk_size = request_options.get("chunk_size", None) if request_options is not None else None + return HttpResponse( + response=_response, data=(_chunk for _chunk in _response.iter_bytes(chunk_size=_chunk_size)) + ) + _response.read() + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.text + ) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.json(), + cause=e, + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + yield _stream() class AsyncRawSessionsClient: @@ -2188,9 +2205,10 @@ async def unshare_session( ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + @contextlib.asynccontextmanager async def get_session_resource( self, id: str, bucket: str, key: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[None]: + ) -> typing.AsyncIterator[AsyncHttpResponse[typing.AsyncIterator[bytes]]]: """ Redirect to a presigned S3 URL for a session-owned resource. @@ -2203,36 +2221,51 @@ async def get_session_resource( key : str request_options : typing.Optional[RequestOptions] - Request-specific configuration. + Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. Returns ------- - AsyncHttpResponse[None] + typing.AsyncIterator[AsyncHttpResponse[typing.AsyncIterator[bytes]]] + Resource bytes. The API redirects to a presigned S3 URL; SDK clients follow the redirect and receive the raw object (e.g. screenshot image bytes). """ - _response = await self._client_wrapper.httpx_client.request( + async with self._client_wrapper.httpx_client.stream( f"api/v2/sessions/{encode_path_param(id)}/resources/{encode_path_param(bucket)}/{encode_path_param(key)}", method="GET", request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return AsyncHttpResponse(response=_response, data=None) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - HttpValidationError, - parse_obj_as( - type_=HttpValidationError, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + ) as _response: + + async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[bytes]]: + try: + if 200 <= _response.status_code < 300: + _chunk_size = request_options.get("chunk_size", None) if request_options is not None else None + return AsyncHttpResponse( + response=_response, + data=(_chunk async for _chunk in _response.aiter_bytes(chunk_size=_chunk_size)), + ) + await _response.aread() + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.text + ) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.json(), + cause=e, + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + yield await _stream() diff --git a/src/hai_agents/types/__init__.py b/src/hai_agents/types/__init__.py index 31aa7d3..3187803 100644 --- a/src/hai_agents/types/__init__.py +++ b/src/hai_agents/types/__init__.py @@ -39,6 +39,7 @@ from .browser import Browser from .browser_kind import BrowserKind from .browser_mode import BrowserMode + from .browser_network import BrowserNetwork from .browser_profile_list import BrowserProfileList from .browser_profile_read import BrowserProfileRead from .environment import Environment @@ -103,6 +104,7 @@ from .session_summary import SessionSummary from .share_link import ShareLink from .skill import Skill + from .token_quota_status import TokenQuotaStatus from .tool_definition import ToolDefinition from .tool_request import ToolRequest from .tool_result_batch import ToolResultBatch @@ -158,6 +160,7 @@ "Browser": ".browser", "BrowserKind": ".browser_kind", "BrowserMode": ".browser_mode", + "BrowserNetwork": ".browser_network", "BrowserProfileList": ".browser_profile_list", "BrowserProfileRead": ".browser_profile_read", "Environment": ".environment", @@ -220,6 +223,7 @@ "SessionSummary": ".session_summary", "ShareLink": ".share_link", "Skill": ".skill", + "TokenQuotaStatus": ".token_quota_status", "ToolDefinition": ".tool_definition", "ToolRequest": ".tool_request", "ToolResultBatch": ".tool_result_batch", @@ -297,6 +301,7 @@ def __dir__(): "Browser", "BrowserKind", "BrowserMode", + "BrowserNetwork", "BrowserProfileList", "BrowserProfileRead", "Environment", @@ -359,6 +364,7 @@ def __dir__(): "SessionSummary", "ShareLink", "Skill", + "TokenQuotaStatus", "ToolDefinition", "ToolRequest", "ToolResultBatch", diff --git a/src/hai_agents/types/browser.py b/src/hai_agents/types/browser.py index 29381ad..0f854ac 100644 --- a/src/hai_agents/types/browser.py +++ b/src/hai_agents/types/browser.py @@ -6,6 +6,7 @@ from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel from .browser_kind import BrowserKind from .browser_mode import BrowserMode +from .browser_network import BrowserNetwork class Browser(UniversalBaseModel): @@ -19,11 +20,6 @@ class Browser(UniversalBaseModel): """ kind: typing.Optional[BrowserKind] = "web" - headless: typing.Optional[bool] = pydantic.Field(default=None) - """ - Run without a visible window. - """ - width: typing.Optional[int] = pydantic.Field(default=None) """ Viewport width in pixels. @@ -59,6 +55,11 @@ class Browser(UniversalBaseModel): Id of a browser profile to load into this browser, restoring saved cookies and storage state from a prior session. The profile must belong to the caller's organization. Omit to run with a fresh profile. """ + network: typing.Optional[BrowserNetwork] = pydantic.Field(default=None) + """ + Optional network configuration for the remote browser session. Applied only when a new runner session is provisioned (not when session_id is set). + """ + if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 else: diff --git a/src/hai_agents/types/browser_network.py b/src/hai_agents/types/browser_network.py new file mode 100644 index 0000000..4e0915e --- /dev/null +++ b/src/hai_agents/types/browser_network.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class BrowserNetwork(UniversalBaseModel): + """ + Network egress settings for a remote browser session. + """ + + proxy_url: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional bring-your-own HTTP/HTTPS/SOCKS proxy URL for browser egress (e.g. http://user:pass@proxy.example.com:8080). Applied when provisioning a new remote browser session. Only supported for headful chromium-based runners. Ignored when session_id attaches to an existing session. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/hai_agents/types/token_quota_status.py b/src/hai_agents/types/token_quota_status.py new file mode 100644 index 0000000..56ae023 --- /dev/null +++ b/src/hai_agents/types/token_quota_status.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class TokenQuotaStatus(UniversalBaseModel): + """ + Current per-org token usage snapshot. + """ + + limit: typing.Optional[int] = None + used: typing.Optional[int] = None + remaining: typing.Optional[int] = None + window_start: dt.datetime + window_end: typing.Optional[dt.datetime] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/uv.lock b/uv.lock index 0066b4a..c661210 100644 --- a/uv.lock +++ b/uv.lock @@ -88,7 +88,7 @@ wheels = [ [[package]] name = "hai-agents" -version = "0.1.12" +version = "0.1.13" source = { editable = "." } dependencies = [ { name = "httpx" }, From 1c76fd90407ac6065946f9bc728830710b22c57a Mon Sep 17 00:00:00 2001 From: abonneth Date: Thu, 25 Jun 2026 16:25:57 +0200 Subject: [PATCH 2/3] chore(sdk): bump version to 1.0.0 for release Co-authored-by: Cursor --- pyproject.toml | 2 +- src/hai_agents/core/client_wrapper.py | 4 ++-- uv.lock | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dc6f2e5..16c0c00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "hai-agents" -version = "0.1.13" +version = "1.0.0" description = "Python SDK for H Company's Computer-Use Agents: autonomous agents powered by Holo." requires-python = ">=3.10" readme = "README.md" diff --git a/src/hai_agents/core/client_wrapper.py b/src/hai_agents/core/client_wrapper.py index 071fc98..4db9121 100644 --- a/src/hai_agents/core/client_wrapper.py +++ b/src/hai_agents/core/client_wrapper.py @@ -29,9 +29,9 @@ def get_headers(self) -> typing.Dict[str, str]: import platform headers: typing.Dict[str, str] = { - "User-Agent": "hai_agents/0.1.13", + "User-Agent": "hai_agents/1.0.0", "X-HCompany-Client-Name": "hai_agents", - "X-HCompany-Client-Version": "0.1.13", + "X-HCompany-Client-Version": "1.0.0", "X-HCompany-Client-Type": "sdk", "X-HCompany-Language": "Python", "X-HCompany-Runtime": f"python/{platform.python_version()}", diff --git a/uv.lock b/uv.lock index c661210..cc933e8 100644 --- a/uv.lock +++ b/uv.lock @@ -88,7 +88,7 @@ wheels = [ [[package]] name = "hai-agents" -version = "0.1.13" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "httpx" }, From 95cd47ea1f9abd867a47e52d9e4dadfc1ab431b1 Mon Sep 17 00:00:00 2001 From: abonneth Date: Thu, 25 Jun 2026 17:18:53 +0200 Subject: [PATCH 3/3] fix: iterate get_session_resource example + drop headless from browser test Co-authored-by: Cursor --- src/hai_agents/sessions/client.py | 10 ++++++---- tests/integration/test_session_browser.py | 6 ++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/hai_agents/sessions/client.py b/src/hai_agents/sessions/client.py index 5e24f5b..5bcf251 100644 --- a/src/hai_agents/sessions/client.py +++ b/src/hai_agents/sessions/client.py @@ -779,11 +779,12 @@ def get_session_resource( client = Client( api_key="YOUR_API_KEY", ) - client.sessions.get_session_resource( + for _chunk in client.sessions.get_session_resource( id="id", bucket="bucket", key="key", - ) + ): + pass """ with self._raw_client.get_session_resource(id, bucket, key, request_options=request_options) as r: yield from r.data @@ -1687,11 +1688,12 @@ async def get_session_resource( async def main() -> None: - await client.sessions.get_session_resource( + async for _chunk in client.sessions.get_session_resource( id="id", bucket="bucket", key="key", - ) + ): + pass asyncio.run(main()) diff --git a/tests/integration/test_session_browser.py b/tests/integration/test_session_browser.py index 10b264a..14e9169 100644 --- a/tests/integration/test_session_browser.py +++ b/tests/integration/test_session_browser.py @@ -5,9 +5,8 @@ local_browser provisioning, the agent using whatever page it lands on, and end-to-end answer extraction. -Bing is used because headless browsers can read it without CAPTCHA. The engine -lives in the env spec, not the prompt -- mirroring how a product injects a start -URL via config. +Bing is used because it can be read without a CAPTCHA. The engine lives in the +env spec, not the prompt -- mirroring how a product injects a start URL via config. Marked ``slow`` (~60-120s, more tokens than the code-env test). Skipped by default; opt in with ``pytest -m "integration and slow"`` or RUN_SLOW_SDK_TESTS=1. @@ -64,7 +63,6 @@ def test_browser_session_finds_paris_weather(client: Client, run_id: str, create { "id": "browser", "kind": "local_browser", - "headless": True, "width": 1280, "height": 800, "start_url": SEARCH_ENGINE_START_URL,