diff --git a/.stats.yml b/.stats.yml index 917ae03..11ad6be 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 14 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/warp-bnavetta%2Fwarp-api-a912e2533a6f1fbeee38d4b7739b771ed5711c648a6a7f3d8769b8b2cb4f31fb.yml -openapi_spec_hash: ef48f8fcc46a51b00893505e9b52c95d -config_hash: e894152aaebba5a2e65e27efaf2712e2 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/warp-bnavetta%2Fwarp-api-a29592b2ba26cba9d89b95969d66506f49c08e140b76ce4aea4189e5c1dccc06.yml +openapi_spec_hash: 27a5de1f891104d5e47904ad8e4b4bd1 +config_hash: 40327fb76b7cce7b97f23de9b8d48efb diff --git a/README.md b/README.md index 9ea4d38..9aba4a6 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,69 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. +## Pagination + +List methods in the Oz API API are paginated. + +This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually: + +```python +from oz_agent_sdk import OzAPI + +client = OzAPI() + +all_runs = [] +# Automatically fetches more pages as needed. +for run in client.agent.runs.list(): + # Do something with run here + all_runs.append(run) +print(all_runs) +``` + +Or, asynchronously: + +```python +import asyncio +from oz_agent_sdk import AsyncOzAPI + +client = AsyncOzAPI() + + +async def main() -> None: + all_runs = [] + # Iterate through items across all pages, issuing requests as needed. + async for run in client.agent.runs.list(): + all_runs.append(run) + print(all_runs) + + +asyncio.run(main()) +``` + +Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages: + +```python +first_page = await client.agent.runs.list() +if first_page.has_next_page(): + print(f"will fetch next page using these details: {first_page.next_page_info()}") + next_page = await first_page.get_next_page() + print(f"number of items we just fetched: {len(next_page.runs)}") + +# Remove `await` for non-async usage. +``` + +Or just work directly with the returned data: + +```python +first_page = await client.agent.runs.list() + +print(f"next page cursor: {first_page.page_info.next_cursor}") # => "next page cursor: ..." +for run in first_page.runs: + print(run.run_id) + +# Remove `await` for non-async usage. +``` + ## Nested params Nested parameters are dictionaries, typed using `TypedDict`, for example: diff --git a/api.md b/api.md index 19e78a3..3ef6227 100644 --- a/api.md +++ b/api.md @@ -6,9 +6,11 @@ Types: from oz_agent_sdk.types import ( AgentSkill, AmbientAgentConfig, + AwsProviderConfig, CloudEnvironmentConfig, Error, ErrorCode, + GcpProviderConfig, McpServerConfig, Scope, UserProfile, @@ -22,7 +24,7 @@ Methods: - client.agent.list(\*\*params) -> AgentListResponse - client.agent.get_artifact(artifact_uid) -> AgentGetArtifactResponse -- client.agent.run(\*\*params) -> AgentRunResponse +- client.agent.run(\*\*params) -> AgentRunResponse ## Runs @@ -34,7 +36,6 @@ from oz_agent_sdk.types.agent import ( RunItem, RunSourceType, RunState, - RunListResponse, RunCancelResponse, ) ``` @@ -42,7 +43,7 @@ from oz_agent_sdk.types.agent import ( Methods: - client.agent.runs.retrieve(run_id) -> RunItem -- client.agent.runs.list(\*\*params) -> RunListResponse +- client.agent.runs.list(\*\*params) -> SyncRunsCursorPage[RunItem] - client.agent.runs.cancel(run_id) -> str ## Schedules diff --git a/src/oz_agent_sdk/_base_client.py b/src/oz_agent_sdk/_base_client.py index d1510d4..047d5bb 100644 --- a/src/oz_agent_sdk/_base_client.py +++ b/src/oz_agent_sdk/_base_client.py @@ -63,7 +63,7 @@ ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping from ._compat import PYDANTIC_V1, model_copy, model_dump -from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type +from ._models import GenericModel, SecurityOptions, FinalRequestOptions, validate_type, construct_type from ._response import ( APIResponse, BaseAPIResponse, @@ -432,9 +432,27 @@ def _make_status_error( ) -> _exceptions.APIStatusError: raise NotImplementedError() + def _auth_headers( + self, + security: SecurityOptions, # noqa: ARG002 + ) -> dict[str, str]: + return {} + + def _auth_query( + self, + security: SecurityOptions, # noqa: ARG002 + ) -> dict[str, str]: + return {} + + def _custom_auth( + self, + security: SecurityOptions, # noqa: ARG002 + ) -> httpx.Auth | None: + return None + def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx.Headers: custom_headers = options.headers or {} - headers_dict = _merge_mappings(self.default_headers, custom_headers) + headers_dict = _merge_mappings({**self._auth_headers(options.security), **self.default_headers}, custom_headers) self._validate_headers(headers_dict, custom_headers) # headers are case-insensitive while dictionaries are not. @@ -506,7 +524,7 @@ def _build_request( raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`") headers = self._build_headers(options, retries_taken=retries_taken) - params = _merge_mappings(self.default_query, options.params) + params = _merge_mappings({**self._auth_query(options.security), **self.default_query}, options.params) content_type = headers.get("Content-Type") files = options.files @@ -540,6 +558,10 @@ def _build_request( files = cast(HttpxRequestFiles, ForceMultipartDict()) prepared_url = self._prepare_url(options.url) + # preserve hard-coded query params from the url + if params and prepared_url.query: + params = {**dict(prepared_url.params.items()), **params} + prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0]) if "_" in prepared_url.host: # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} @@ -671,7 +693,6 @@ def default_headers(self) -> dict[str, str | Omit]: "Content-Type": "application/json", "User-Agent": self.user_agent, **self.platform_headers(), - **self.auth_headers, **self._custom_headers, } @@ -990,8 +1011,9 @@ def request( self._prepare_request(request) kwargs: HttpxSendArgs = {} - if self.custom_auth is not None: - kwargs["auth"] = self.custom_auth + custom_auth = self._custom_auth(options.security) + if custom_auth is not None: + kwargs["auth"] = custom_auth if options.follow_redirects is not None: kwargs["follow_redirects"] = options.follow_redirects @@ -1952,6 +1974,7 @@ def make_request_options( idempotency_key: str | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, post_parser: PostParser | NotGiven = not_given, + security: SecurityOptions | None = None, ) -> RequestOptions: """Create a dict of type RequestOptions without keys of NotGiven values.""" options: RequestOptions = {} @@ -1977,6 +2000,9 @@ def make_request_options( # internal options["post_parser"] = post_parser # type: ignore + if security is not None: + options["security"] = security + return options diff --git a/src/oz_agent_sdk/_client.py b/src/oz_agent_sdk/_client.py index 27973c0..81603d2 100644 --- a/src/oz_agent_sdk/_client.py +++ b/src/oz_agent_sdk/_client.py @@ -21,6 +21,7 @@ ) from ._utils import is_given, get_async_library from ._compat import cached_property +from ._models import SecurityOptions from ._version import __version__ from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import OzAPIError, APIStatusError @@ -112,9 +113,14 @@ def with_streaming_response(self) -> OzAPIWithStreamedResponse: def qs(self) -> Querystring: return Querystring(array_format="repeat") - @property @override - def auth_headers(self) -> dict[str, str]: + def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: + return { + **(self._bearer_auth if security.get("bearer_auth", False) else {}), + } + + @property + def _bearer_auth(self) -> dict[str, str]: api_key = self.api_key return {"Authorization": f"Bearer {api_key}"} @@ -287,9 +293,14 @@ def with_streaming_response(self) -> AsyncOzAPIWithStreamedResponse: def qs(self) -> Querystring: return Querystring(array_format="repeat") - @property @override - def auth_headers(self) -> dict[str, str]: + def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: + return { + **(self._bearer_auth if security.get("bearer_auth", False) else {}), + } + + @property + def _bearer_auth(self) -> dict[str, str]: api_key = self.api_key return {"Authorization": f"Bearer {api_key}"} diff --git a/src/oz_agent_sdk/_models.py b/src/oz_agent_sdk/_models.py index 29070e0..e22dd2a 100644 --- a/src/oz_agent_sdk/_models.py +++ b/src/oz_agent_sdk/_models.py @@ -791,6 +791,10 @@ def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]: return RootModel[type_] # type: ignore +class SecurityOptions(TypedDict, total=False): + bearer_auth: bool + + class FinalRequestOptionsInput(TypedDict, total=False): method: Required[str] url: Required[str] @@ -804,6 +808,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): json_data: Body extra_json: AnyMapping follow_redirects: bool + security: SecurityOptions @final @@ -818,6 +823,7 @@ class FinalRequestOptions(pydantic.BaseModel): idempotency_key: Union[str, None] = None post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + security: SecurityOptions = {"bearer_auth": True} content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override diff --git a/src/oz_agent_sdk/_qs.py b/src/oz_agent_sdk/_qs.py index ada6fd3..de8c99b 100644 --- a/src/oz_agent_sdk/_qs.py +++ b/src/oz_agent_sdk/_qs.py @@ -101,7 +101,10 @@ def _stringify_item( items.extend(self._stringify_item(key, item, opts)) return items elif array_format == "indices": - raise NotImplementedError("The array indices format is not supported yet") + items = [] + for i, item in enumerate(value): + items.extend(self._stringify_item(f"{key}[{i}]", item, opts)) + return items elif array_format == "brackets": items = [] key = key + "[]" diff --git a/src/oz_agent_sdk/_types.py b/src/oz_agent_sdk/_types.py index eac5f8b..cbb7d7e 100644 --- a/src/oz_agent_sdk/_types.py +++ b/src/oz_agent_sdk/_types.py @@ -36,7 +36,7 @@ from httpx import URL, Proxy, Timeout, Response, BaseTransport, AsyncBaseTransport if TYPE_CHECKING: - from ._models import BaseModel + from ._models import BaseModel, SecurityOptions from ._response import APIResponse, AsyncAPIResponse Transport = BaseTransport @@ -121,6 +121,7 @@ class RequestOptions(TypedDict, total=False): extra_json: AnyMapping idempotency_key: str follow_redirects: bool + security: SecurityOptions # Sentinel class used until PEP 0661 is accepted diff --git a/src/oz_agent_sdk/pagination.py b/src/oz_agent_sdk/pagination.py new file mode 100644 index 0000000..3f925ea --- /dev/null +++ b/src/oz_agent_sdk/pagination.py @@ -0,0 +1,85 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Generic, TypeVar, Optional +from typing_extensions import override + +from ._models import BaseModel +from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage + +__all__ = ["RunsCursorPagePageInfo", "SyncRunsCursorPage", "AsyncRunsCursorPage"] + +_T = TypeVar("_T") + + +class RunsCursorPagePageInfo(BaseModel): + has_next_page: Optional[bool] = None + + next_cursor: Optional[str] = None + + +class SyncRunsCursorPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + runs: List[_T] + page_info: Optional[RunsCursorPagePageInfo] = None + + @override + def _get_page_items(self) -> List[_T]: + runs = self.runs + if not runs: + return [] + return runs + + @override + def has_next_page(self) -> bool: + has_next_page = None + if self.page_info is not None: + if self.page_info.has_next_page is not None: + has_next_page = self.page_info.has_next_page + if has_next_page is not None and has_next_page is False: + return False + + return super().has_next_page() + + @override + def next_page_info(self) -> Optional[PageInfo]: + next_cursor = None + if self.page_info is not None: + if self.page_info.next_cursor is not None: + next_cursor = self.page_info.next_cursor + if not next_cursor: + return None + + return PageInfo(params={"cursor": next_cursor}) + + +class AsyncRunsCursorPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + runs: List[_T] + page_info: Optional[RunsCursorPagePageInfo] = None + + @override + def _get_page_items(self) -> List[_T]: + runs = self.runs + if not runs: + return [] + return runs + + @override + def has_next_page(self) -> bool: + has_next_page = None + if self.page_info is not None: + if self.page_info.has_next_page is not None: + has_next_page = self.page_info.has_next_page + if has_next_page is not None and has_next_page is False: + return False + + return super().has_next_page() + + @override + def next_page_info(self) -> Optional[PageInfo]: + next_cursor = None + if self.page_info is not None: + if self.page_info.next_cursor is not None: + next_cursor = self.page_info.next_cursor + if not next_cursor: + return None + + return PageInfo(params={"cursor": next_cursor}) diff --git a/src/oz_agent_sdk/resources/agent/agent.py b/src/oz_agent_sdk/resources/agent/agent.py index 4adaca9..a0240ce 100644 --- a/src/oz_agent_sdk/resources/agent/agent.py +++ b/src/oz_agent_sdk/resources/agent/agent.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Iterable +from typing import Any, Iterable, cast from typing_extensions import Literal import httpx @@ -163,8 +163,8 @@ def get_artifact( ) -> AgentGetArtifactResponse: """Retrieve an artifact by its UUID. - For screenshot artifacts, returns a - time-limited signed download URL. + For supported downloadable artifacts, returns + a time-limited signed download URL. Args: extra_headers: Send extra headers @@ -177,12 +177,17 @@ def get_artifact( """ if not artifact_uid: raise ValueError(f"Expected a non-empty value for `artifact_uid` but received {artifact_uid!r}") - return self._get( - path_template("/agent/artifacts/{artifact_uid}", artifact_uid=artifact_uid), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + return cast( + AgentGetArtifactResponse, + self._get( + path_template("/agent/artifacts/{artifact_uid}", artifact_uid=artifact_uid), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, AgentGetArtifactResponse + ), # Union types cannot be passed in as arguments in the type system ), - cast_to=AgentGetArtifactResponse, ) def run( @@ -192,6 +197,7 @@ def run( config: AmbientAgentConfigParam | Omit = omit, conversation_id: str | Omit = omit, interactive: bool | Omit = omit, + parent_run_id: str | Omit = omit, prompt: str | Omit = omit, skill: str | Omit = omit, team: bool | Omit = omit, @@ -203,10 +209,10 @@ def run( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AgentRunResponse: - """Spawn a cloud agent with a prompt and optional configuration. + """Alias for POST /agent/run. - The agent will be - queued for execution and assigned a unique run ID. + This is the preferred endpoint for creating new agent + runs. Behavior is identical to POST /agent/run. Args: attachments: Optional file attachments to include with the prompt (max 5). Attachments are @@ -219,6 +225,9 @@ def run( interactive: Whether the run should be interactive. If not set, defaults to false. + parent_run_id: Optional run ID of the parent that spawned this run. Used for orchestration + hierarchies. + prompt: The prompt/instruction for the agent to execute. Required unless a skill is specified via the skill field or config.skill_spec. @@ -244,13 +253,14 @@ def run( timeout: Override the client-level default timeout for this request, in seconds """ return self._post( - "/agent/run", + "/agent/runs", body=maybe_transform( { "attachments": attachments, "config": config, "conversation_id": conversation_id, "interactive": interactive, + "parent_run_id": parent_run_id, "prompt": prompt, "skill": skill, "team": team, @@ -377,8 +387,8 @@ async def get_artifact( ) -> AgentGetArtifactResponse: """Retrieve an artifact by its UUID. - For screenshot artifacts, returns a - time-limited signed download URL. + For supported downloadable artifacts, returns + a time-limited signed download URL. Args: extra_headers: Send extra headers @@ -391,12 +401,17 @@ async def get_artifact( """ if not artifact_uid: raise ValueError(f"Expected a non-empty value for `artifact_uid` but received {artifact_uid!r}") - return await self._get( - path_template("/agent/artifacts/{artifact_uid}", artifact_uid=artifact_uid), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + return cast( + AgentGetArtifactResponse, + await self._get( + path_template("/agent/artifacts/{artifact_uid}", artifact_uid=artifact_uid), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, AgentGetArtifactResponse + ), # Union types cannot be passed in as arguments in the type system ), - cast_to=AgentGetArtifactResponse, ) async def run( @@ -406,6 +421,7 @@ async def run( config: AmbientAgentConfigParam | Omit = omit, conversation_id: str | Omit = omit, interactive: bool | Omit = omit, + parent_run_id: str | Omit = omit, prompt: str | Omit = omit, skill: str | Omit = omit, team: bool | Omit = omit, @@ -417,10 +433,10 @@ async def run( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AgentRunResponse: - """Spawn a cloud agent with a prompt and optional configuration. + """Alias for POST /agent/run. - The agent will be - queued for execution and assigned a unique run ID. + This is the preferred endpoint for creating new agent + runs. Behavior is identical to POST /agent/run. Args: attachments: Optional file attachments to include with the prompt (max 5). Attachments are @@ -433,6 +449,9 @@ async def run( interactive: Whether the run should be interactive. If not set, defaults to false. + parent_run_id: Optional run ID of the parent that spawned this run. Used for orchestration + hierarchies. + prompt: The prompt/instruction for the agent to execute. Required unless a skill is specified via the skill field or config.skill_spec. @@ -458,13 +477,14 @@ async def run( timeout: Override the client-level default timeout for this request, in seconds """ return await self._post( - "/agent/run", + "/agent/runs", body=await async_maybe_transform( { "attachments": attachments, "config": config, "conversation_id": conversation_id, "interactive": interactive, + "parent_run_id": parent_run_id, "prompt": prompt, "skill": skill, "team": team, diff --git a/src/oz_agent_sdk/resources/agent/runs.py b/src/oz_agent_sdk/resources/agent/runs.py index 440bdcf..e6e55b5 100644 --- a/src/oz_agent_sdk/resources/agent/runs.py +++ b/src/oz_agent_sdk/resources/agent/runs.py @@ -9,7 +9,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import path_template, maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -18,12 +18,12 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) +from ...pagination import SyncRunsCursorPage, AsyncRunsCursorPage from ...types.agent import RunSourceType, run_list_params -from ..._base_client import make_request_options +from ..._base_client import AsyncPaginator, make_request_options from ...types.agent.run_item import RunItem from ...types.agent.run_state import RunState from ...types.agent.run_source_type import RunSourceType -from ...types.agent.run_list_response import RunListResponse __all__ = ["RunsResource", "AsyncRunsResource"] @@ -87,12 +87,13 @@ def retrieve( def list( self, *, - artifact_type: Literal["PLAN", "PULL_REQUEST", "SCREENSHOT"] | Omit = omit, + artifact_type: Literal["PLAN", "PULL_REQUEST", "SCREENSHOT", "FILE"] | Omit = omit, created_after: Union[str, datetime] | Omit = omit, created_before: Union[str, datetime] | Omit = omit, creator: str | Omit = omit, cursor: str | Omit = omit, environment_id: str | Omit = omit, + execution_location: Literal["LOCAL", "REMOTE"] | Omit = omit, limit: int | Omit = omit, model_id: str | Omit = omit, name: str | Omit = omit, @@ -111,14 +112,14 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> RunListResponse: + ) -> SyncRunsCursorPage[RunItem]: """Retrieve a paginated list of agent runs with optional filtering. Results default to `sort_by=updated_at` and `sort_order=desc`. Args: - artifact_type: Filter runs by artifact type (PLAN or PULL_REQUEST) + artifact_type: Filter runs by artifact type created_after: Filter runs created after this timestamp (RFC3339 format) @@ -130,6 +131,8 @@ def list( environment_id: Filter runs by environment ID + execution_location: Filter by where the run executed + limit: Maximum number of runs to return model_id: Filter by model ID @@ -169,8 +172,9 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - return self._get( + return self._get_api_list( "/agent/runs", + page=SyncRunsCursorPage[RunItem], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -184,6 +188,7 @@ def list( "creator": creator, "cursor": cursor, "environment_id": environment_id, + "execution_location": execution_location, "limit": limit, "model_id": model_id, "name": name, @@ -200,7 +205,7 @@ def list( run_list_params.RunListParams, ), ), - cast_to=RunListResponse, + model=RunItem, ) def cancel( @@ -299,15 +304,16 @@ async def retrieve( cast_to=RunItem, ) - async def list( + def list( self, *, - artifact_type: Literal["PLAN", "PULL_REQUEST", "SCREENSHOT"] | Omit = omit, + artifact_type: Literal["PLAN", "PULL_REQUEST", "SCREENSHOT", "FILE"] | Omit = omit, created_after: Union[str, datetime] | Omit = omit, created_before: Union[str, datetime] | Omit = omit, creator: str | Omit = omit, cursor: str | Omit = omit, environment_id: str | Omit = omit, + execution_location: Literal["LOCAL", "REMOTE"] | Omit = omit, limit: int | Omit = omit, model_id: str | Omit = omit, name: str | Omit = omit, @@ -326,14 +332,14 @@ async def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> RunListResponse: + ) -> AsyncPaginator[RunItem, AsyncRunsCursorPage[RunItem]]: """Retrieve a paginated list of agent runs with optional filtering. Results default to `sort_by=updated_at` and `sort_order=desc`. Args: - artifact_type: Filter runs by artifact type (PLAN or PULL_REQUEST) + artifact_type: Filter runs by artifact type created_after: Filter runs created after this timestamp (RFC3339 format) @@ -345,6 +351,8 @@ async def list( environment_id: Filter runs by environment ID + execution_location: Filter by where the run executed + limit: Maximum number of runs to return model_id: Filter by model ID @@ -384,14 +392,15 @@ async def list( timeout: Override the client-level default timeout for this request, in seconds """ - return await self._get( + return self._get_api_list( "/agent/runs", + page=AsyncRunsCursorPage[RunItem], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=await async_maybe_transform( + query=maybe_transform( { "artifact_type": artifact_type, "created_after": created_after, @@ -399,6 +408,7 @@ async def list( "creator": creator, "cursor": cursor, "environment_id": environment_id, + "execution_location": execution_location, "limit": limit, "model_id": model_id, "name": name, @@ -415,7 +425,7 @@ async def list( run_list_params.RunListParams, ), ), - cast_to=RunListResponse, + model=RunItem, ) async def cancel( diff --git a/src/oz_agent_sdk/resources/agent/schedules.py b/src/oz_agent_sdk/resources/agent/schedules.py index a4b5592..f9fbac0 100644 --- a/src/oz_agent_sdk/resources/agent/schedules.py +++ b/src/oz_agent_sdk/resources/agent/schedules.py @@ -275,8 +275,7 @@ def pause( ) -> ScheduledAgentItem: """Pause a scheduled agent. - The agent will not run until resumed. This sets the - enabled flag to false. + The agent will not run until resumed. Args: extra_headers: Send extra headers @@ -311,7 +310,7 @@ def resume( """Resume a paused scheduled agent. The agent will start running according to its - cron schedule. This sets the enabled flag to true. + cron schedule. Args: extra_headers: Send extra headers @@ -584,8 +583,7 @@ async def pause( ) -> ScheduledAgentItem: """Pause a scheduled agent. - The agent will not run until resumed. This sets the - enabled flag to false. + The agent will not run until resumed. Args: extra_headers: Send extra headers @@ -620,7 +618,7 @@ async def resume( """Resume a paused scheduled agent. The agent will start running according to its - cron schedule. This sets the enabled flag to true. + cron schedule. Args: extra_headers: Send extra headers diff --git a/src/oz_agent_sdk/types/__init__.py b/src/oz_agent_sdk/types/__init__.py index f4d2daa..4c520a7 100644 --- a/src/oz_agent_sdk/types/__init__.py +++ b/src/oz_agent_sdk/types/__init__.py @@ -11,6 +11,8 @@ from .mcp_server_config import McpServerConfig as McpServerConfig from .agent_run_response import AgentRunResponse as AgentRunResponse from .agent_list_response import AgentListResponse as AgentListResponse +from .aws_provider_config import AwsProviderConfig as AwsProviderConfig +from .gcp_provider_config import GcpProviderConfig as GcpProviderConfig from .ambient_agent_config import AmbientAgentConfig as AmbientAgentConfig from .mcp_server_config_param import McpServerConfigParam as McpServerConfigParam from .cloud_environment_config import CloudEnvironmentConfig as CloudEnvironmentConfig diff --git a/src/oz_agent_sdk/types/agent/__init__.py b/src/oz_agent_sdk/types/agent/__init__.py index 3484850..1176974 100644 --- a/src/oz_agent_sdk/types/agent/__init__.py +++ b/src/oz_agent_sdk/types/agent/__init__.py @@ -7,7 +7,6 @@ from .artifact_item import ArtifactItem as ArtifactItem from .run_list_params import RunListParams as RunListParams from .run_source_type import RunSourceType as RunSourceType -from .run_list_response import RunListResponse as RunListResponse from .run_cancel_response import RunCancelResponse as RunCancelResponse from .scheduled_agent_item import ScheduledAgentItem as ScheduledAgentItem from .schedule_create_params import ScheduleCreateParams as ScheduleCreateParams diff --git a/src/oz_agent_sdk/types/agent/artifact_item.py b/src/oz_agent_sdk/types/agent/artifact_item.py index 7cbba71..299379b 100644 --- a/src/oz_agent_sdk/types/agent/artifact_item.py +++ b/src/oz_agent_sdk/types/agent/artifact_item.py @@ -15,6 +15,8 @@ "PullRequestArtifactData", "ScreenshotArtifact", "ScreenshotArtifactData", + "FileArtifact", + "FileArtifactData", ] @@ -78,6 +80,37 @@ class ScreenshotArtifact(BaseModel): data: ScreenshotArtifactData +class FileArtifactData(BaseModel): + artifact_uid: str + """Unique identifier for the file artifact""" + + filename: str + """Last path component of filepath""" + + filepath: str + """Conversation-relative filepath for the uploaded file""" + + mime_type: str + """MIME type of the uploaded file""" + + description: Optional[str] = None + """Optional description of the file""" + + size_bytes: Optional[int] = None + """Size of the uploaded file in bytes""" + + +class FileArtifact(BaseModel): + artifact_type: Literal["FILE"] + """Type of the artifact""" + + created_at: datetime + """Timestamp when the artifact was created (RFC3339)""" + + data: FileArtifactData + + ArtifactItem: TypeAlias = Annotated[ - Union[PlanArtifact, PullRequestArtifact, ScreenshotArtifact], PropertyInfo(discriminator="artifact_type") + Union[PlanArtifact, PullRequestArtifact, ScreenshotArtifact, FileArtifact], + PropertyInfo(discriminator="artifact_type"), ] diff --git a/src/oz_agent_sdk/types/agent/run_item.py b/src/oz_agent_sdk/types/agent/run_item.py index 014ab6b..d246450 100644 --- a/src/oz_agent_sdk/types/agent/run_item.py +++ b/src/oz_agent_sdk/types/agent/run_item.py @@ -2,6 +2,7 @@ from typing import List, Optional from datetime import datetime +from typing_extensions import Literal from ..scope import Scope from ..._models import BaseModel @@ -160,9 +161,19 @@ class RunItem(BaseModel): creator: Optional[UserProfile] = None + execution_location: Optional[Literal["LOCAL", "REMOTE"]] = None + """Where the run executed: + + - LOCAL: Executed in the user's local Oz environment + - REMOTE: Executed by a remote/cloud worker + """ + is_sandbox_running: Optional[bool] = None """Whether the sandbox environment is currently running""" + parent_run_id: Optional[str] = None + """UUID of the parent run that spawned this run""" + request_usage: Optional[RequestUsage] = None """Resource usage information for the run""" diff --git a/src/oz_agent_sdk/types/agent/run_list_params.py b/src/oz_agent_sdk/types/agent/run_list_params.py index cb662bc..0b4e389 100644 --- a/src/oz_agent_sdk/types/agent/run_list_params.py +++ b/src/oz_agent_sdk/types/agent/run_list_params.py @@ -14,8 +14,8 @@ class RunListParams(TypedDict, total=False): - artifact_type: Literal["PLAN", "PULL_REQUEST", "SCREENSHOT"] - """Filter runs by artifact type (PLAN or PULL_REQUEST)""" + artifact_type: Literal["PLAN", "PULL_REQUEST", "SCREENSHOT", "FILE"] + """Filter runs by artifact type""" created_after: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] """Filter runs created after this timestamp (RFC3339 format)""" @@ -32,6 +32,9 @@ class RunListParams(TypedDict, total=False): environment_id: str """Filter runs by environment ID""" + execution_location: Literal["LOCAL", "REMOTE"] + """Filter by where the run executed""" + limit: int """Maximum number of runs to return""" diff --git a/src/oz_agent_sdk/types/agent/run_list_response.py b/src/oz_agent_sdk/types/agent/run_list_response.py deleted file mode 100644 index db1beb9..0000000 --- a/src/oz_agent_sdk/types/agent/run_list_response.py +++ /dev/null @@ -1,22 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from .run_item import RunItem -from ..._models import BaseModel - -__all__ = ["RunListResponse", "PageInfo"] - - -class PageInfo(BaseModel): - has_next_page: bool - """Whether there are more results available""" - - next_cursor: Optional[str] = None - """Opaque cursor for fetching the next page""" - - -class RunListResponse(BaseModel): - page_info: PageInfo - - runs: List[RunItem] diff --git a/src/oz_agent_sdk/types/agent_get_artifact_response.py b/src/oz_agent_sdk/types/agent_get_artifact_response.py index 277baf3..30405bc 100644 --- a/src/oz_agent_sdk/types/agent_get_artifact_response.py +++ b/src/oz_agent_sdk/types/agent_get_artifact_response.py @@ -1,14 +1,22 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import Union, Optional from datetime import datetime +from typing_extensions import Literal, Annotated, TypeAlias +from .._utils import PropertyInfo from .._models import BaseModel -__all__ = ["AgentGetArtifactResponse", "Data"] +__all__ = [ + "AgentGetArtifactResponse", + "ScreenshotArtifactResponse", + "ScreenshotArtifactResponseData", + "FileArtifactResponse", + "FileArtifactResponseData", +] -class Data(BaseModel): +class ScreenshotArtifactResponseData(BaseModel): """Response data for a screenshot artifact, including a signed download URL.""" content_type: str @@ -24,11 +32,11 @@ class Data(BaseModel): """Optional description of the screenshot""" -class AgentGetArtifactResponse(BaseModel): - """Response for artifact retrieval. Currently supports screenshot artifacts.""" +class ScreenshotArtifactResponse(BaseModel): + """Response for retrieving a screenshot artifact.""" - artifact_type: str - """Type of the artifact (e.g., SCREENSHOT)""" + artifact_type: Literal["SCREENSHOT"] + """Type of the artifact""" artifact_uid: str """Unique identifier (UUID) for the artifact""" @@ -36,5 +44,51 @@ class AgentGetArtifactResponse(BaseModel): created_at: datetime """Timestamp when the artifact was created (RFC3339)""" - data: Data + data: ScreenshotArtifactResponseData """Response data for a screenshot artifact, including a signed download URL.""" + + +class FileArtifactResponseData(BaseModel): + """Response data for a file artifact, including a signed download URL.""" + + content_type: str + """MIME type of the uploaded file""" + + download_url: str + """Time-limited signed URL to download the file""" + + expires_at: datetime + """Timestamp when the download URL expires (RFC3339)""" + + filename: str + """Last path component of filepath""" + + filepath: str + """Conversation-relative filepath for the uploaded file""" + + description: Optional[str] = None + """Optional description of the file""" + + size_bytes: Optional[int] = None + """Size of the uploaded file in bytes""" + + +class FileArtifactResponse(BaseModel): + """Response for retrieving a file artifact.""" + + artifact_type: Literal["FILE"] + """Type of the artifact""" + + artifact_uid: str + """Unique identifier (UUID) for the artifact""" + + created_at: datetime + """Timestamp when the artifact was created (RFC3339)""" + + data: FileArtifactResponseData + """Response data for a file artifact, including a signed download URL.""" + + +AgentGetArtifactResponse: TypeAlias = Annotated[ + Union[ScreenshotArtifactResponse, FileArtifactResponse], PropertyInfo(discriminator="artifact_type") +] diff --git a/src/oz_agent_sdk/types/agent_run_params.py b/src/oz_agent_sdk/types/agent_run_params.py index 88ae9a6..2a346ed 100644 --- a/src/oz_agent_sdk/types/agent_run_params.py +++ b/src/oz_agent_sdk/types/agent_run_params.py @@ -32,6 +32,12 @@ class AgentRunParams(TypedDict, total=False): interactive: bool """Whether the run should be interactive. If not set, defaults to false.""" + parent_run_id: str + """ + Optional run ID of the parent that spawned this run. Used for orchestration + hierarchies. + """ + prompt: str """ The prompt/instruction for the agent to execute. Required unless a skill is diff --git a/src/oz_agent_sdk/types/ambient_agent_config.py b/src/oz_agent_sdk/types/ambient_agent_config.py index 67667c8..abd16c1 100644 --- a/src/oz_agent_sdk/types/ambient_agent_config.py +++ b/src/oz_agent_sdk/types/ambient_agent_config.py @@ -1,13 +1,37 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Dict, Optional +from typing_extensions import Literal from pydantic import Field as FieldInfo from .._models import BaseModel from .mcp_server_config import McpServerConfig -__all__ = ["AmbientAgentConfig"] +__all__ = ["AmbientAgentConfig", "Harness"] + + +class Harness(BaseModel): + """ + Specifies which execution harness to use for the agent run. + Default (nil/empty) uses Warp's built-in harness. + """ + + auth_secret_name: Optional[str] = None + """Name of a managed secret to use as the authentication credential for the + harness. + + The secret must exist within the caller's personal or team scope. The + environment variable injected into the agent is determined by the secret type + (e.g. ANTHROPIC_API_KEY for anthropic_api_key secrets). + """ + + type: Optional[Literal["oz", "claude"]] = None + """The harness type identifier. + + - oz: Warp's built-in harness (default) + - claude: Claude Code harness + """ class AmbientAgentConfig(BaseModel): @@ -25,6 +49,19 @@ class AmbientAgentConfig(BaseModel): environment_id: Optional[str] = None """UID of the environment to run the agent in""" + harness: Optional[Harness] = None + """ + Specifies which execution harness to use for the agent run. Default (nil/empty) + uses Warp's built-in harness. + """ + + idle_timeout_minutes: Optional[int] = None + """ + Number of minutes to keep the agent environment alive after task completion. If + not set, defaults to 10 minutes. Maximum allowed value is min(60, + floor(max_instance_runtime_seconds / 60) for your billing tier). + """ + mcp_servers: Optional[Dict[str, McpServerConfig]] = None """Map of MCP server configurations by name""" diff --git a/src/oz_agent_sdk/types/ambient_agent_config_param.py b/src/oz_agent_sdk/types/ambient_agent_config_param.py index 1cd17fe..24da607 100644 --- a/src/oz_agent_sdk/types/ambient_agent_config_param.py +++ b/src/oz_agent_sdk/types/ambient_agent_config_param.py @@ -3,11 +3,34 @@ from __future__ import annotations from typing import Dict -from typing_extensions import TypedDict +from typing_extensions import Literal, TypedDict from .mcp_server_config_param import McpServerConfigParam -__all__ = ["AmbientAgentConfigParam"] +__all__ = ["AmbientAgentConfigParam", "Harness"] + + +class Harness(TypedDict, total=False): + """ + Specifies which execution harness to use for the agent run. + Default (nil/empty) uses Warp's built-in harness. + """ + + auth_secret_name: str + """Name of a managed secret to use as the authentication credential for the + harness. + + The secret must exist within the caller's personal or team scope. The + environment variable injected into the agent is determined by the secret type + (e.g. ANTHROPIC_API_KEY for anthropic_api_key secrets). + """ + + type: Literal["oz", "claude"] + """The harness type identifier. + + - oz: Warp's built-in harness (default) + - claude: Claude Code harness + """ class AmbientAgentConfigParam(TypedDict, total=False): @@ -25,6 +48,19 @@ class AmbientAgentConfigParam(TypedDict, total=False): environment_id: str """UID of the environment to run the agent in""" + harness: Harness + """ + Specifies which execution harness to use for the agent run. Default (nil/empty) + uses Warp's built-in harness. + """ + + idle_timeout_minutes: int + """ + Number of minutes to keep the agent environment alive after task completion. If + not set, defaults to 10 minutes. Maximum allowed value is min(60, + floor(max_instance_runtime_seconds / 60) for your billing tier). + """ + mcp_servers: Dict[str, McpServerConfigParam] """Map of MCP server configurations by name""" diff --git a/src/oz_agent_sdk/types/aws_provider_config.py b/src/oz_agent_sdk/types/aws_provider_config.py new file mode 100644 index 0000000..61020d8 --- /dev/null +++ b/src/oz_agent_sdk/types/aws_provider_config.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["AwsProviderConfig"] + + +class AwsProviderConfig(BaseModel): + """AWS IAM role assumption settings""" + + role_arn: str + """AWS IAM role ARN to assume""" diff --git a/src/oz_agent_sdk/types/cloud_environment_config.py b/src/oz_agent_sdk/types/cloud_environment_config.py index 30ecd79..15ef158 100644 --- a/src/oz_agent_sdk/types/cloud_environment_config.py +++ b/src/oz_agent_sdk/types/cloud_environment_config.py @@ -3,8 +3,10 @@ from typing import List, Optional from .._models import BaseModel +from .aws_provider_config import AwsProviderConfig +from .gcp_provider_config import GcpProviderConfig -__all__ = ["CloudEnvironmentConfig", "GitHubRepo", "Providers", "ProvidersAws", "ProvidersGcp"] +__all__ = ["CloudEnvironmentConfig", "GitHubRepo", "Providers"] class GitHubRepo(BaseModel): @@ -15,33 +17,13 @@ class GitHubRepo(BaseModel): """GitHub repository name""" -class ProvidersAws(BaseModel): - """AWS IAM role assumption settings""" - - role_arn: str - """AWS IAM role ARN to assume""" - - -class ProvidersGcp(BaseModel): - """GCP Workload Identity Federation settings""" - - project_number: str - """GCP project number""" - - workload_identity_federation_pool_id: str - """Workload Identity Federation pool ID""" - - workload_identity_federation_provider_id: str - """Workload Identity Federation provider ID""" - - class Providers(BaseModel): """Optional cloud provider configurations for automatic auth""" - aws: Optional[ProvidersAws] = None + aws: Optional[AwsProviderConfig] = None """AWS IAM role assumption settings""" - gcp: Optional[ProvidersGcp] = None + gcp: Optional[GcpProviderConfig] = None """GCP Workload Identity Federation settings""" diff --git a/src/oz_agent_sdk/types/gcp_provider_config.py b/src/oz_agent_sdk/types/gcp_provider_config.py new file mode 100644 index 0000000..168ab6a --- /dev/null +++ b/src/oz_agent_sdk/types/gcp_provider_config.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["GcpProviderConfig"] + + +class GcpProviderConfig(BaseModel): + """GCP Workload Identity Federation settings""" + + project_number: str + """GCP project number""" + + workload_identity_federation_pool_id: str + """Workload Identity Federation pool ID""" + + workload_identity_federation_provider_id: str + """Workload Identity Federation provider ID""" diff --git a/tests/api_resources/agent/test_runs.py b/tests/api_resources/agent/test_runs.py index 480564c..bf257f8 100644 --- a/tests/api_resources/agent/test_runs.py +++ b/tests/api_resources/agent/test_runs.py @@ -10,7 +10,8 @@ from tests.utils import assert_matches_type from oz_agent_sdk import OzAPI, AsyncOzAPI from oz_agent_sdk._utils import parse_datetime -from oz_agent_sdk.types.agent import RunItem, RunListResponse +from oz_agent_sdk.pagination import SyncRunsCursorPage, AsyncRunsCursorPage +from oz_agent_sdk.types.agent import RunItem base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -64,7 +65,7 @@ def test_path_params_retrieve(self, client: OzAPI) -> None: @parametrize def test_method_list(self, client: OzAPI) -> None: run = client.agent.runs.list() - assert_matches_type(RunListResponse, run, path=["response"]) + assert_matches_type(SyncRunsCursorPage[RunItem], run, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -76,6 +77,7 @@ def test_method_list_with_all_params(self, client: OzAPI) -> None: creator="creator", cursor="cursor", environment_id="environment_id", + execution_location="LOCAL", limit=1, model_id="model_id", name="name", @@ -89,7 +91,7 @@ def test_method_list_with_all_params(self, client: OzAPI) -> None: state=["QUEUED"], updated_after=parse_datetime("2019-12-27T18:11:19.117Z"), ) - assert_matches_type(RunListResponse, run, path=["response"]) + assert_matches_type(SyncRunsCursorPage[RunItem], run, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -99,7 +101,7 @@ def test_raw_response_list(self, client: OzAPI) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" run = response.parse() - assert_matches_type(RunListResponse, run, path=["response"]) + assert_matches_type(SyncRunsCursorPage[RunItem], run, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -109,7 +111,7 @@ def test_streaming_response_list(self, client: OzAPI) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" run = response.parse() - assert_matches_type(RunListResponse, run, path=["response"]) + assert_matches_type(SyncRunsCursorPage[RunItem], run, path=["response"]) assert cast(Any, response.is_closed) is True @@ -207,7 +209,7 @@ async def test_path_params_retrieve(self, async_client: AsyncOzAPI) -> None: @parametrize async def test_method_list(self, async_client: AsyncOzAPI) -> None: run = await async_client.agent.runs.list() - assert_matches_type(RunListResponse, run, path=["response"]) + assert_matches_type(AsyncRunsCursorPage[RunItem], run, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -219,6 +221,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncOzAPI) -> No creator="creator", cursor="cursor", environment_id="environment_id", + execution_location="LOCAL", limit=1, model_id="model_id", name="name", @@ -232,7 +235,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncOzAPI) -> No state=["QUEUED"], updated_after=parse_datetime("2019-12-27T18:11:19.117Z"), ) - assert_matches_type(RunListResponse, run, path=["response"]) + assert_matches_type(AsyncRunsCursorPage[RunItem], run, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -242,7 +245,7 @@ async def test_raw_response_list(self, async_client: AsyncOzAPI) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" run = await response.parse() - assert_matches_type(RunListResponse, run, path=["response"]) + assert_matches_type(AsyncRunsCursorPage[RunItem], run, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -252,7 +255,7 @@ async def test_streaming_response_list(self, async_client: AsyncOzAPI) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" run = await response.parse() - assert_matches_type(RunListResponse, run, path=["response"]) + assert_matches_type(AsyncRunsCursorPage[RunItem], run, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/agent/test_schedules.py b/tests/api_resources/agent/test_schedules.py index e8f8a24..9b22b45 100644 --- a/tests/api_resources/agent/test_schedules.py +++ b/tests/api_resources/agent/test_schedules.py @@ -40,6 +40,11 @@ def test_method_create_with_all_params(self, client: OzAPI) -> None: "base_prompt": "base_prompt", "computer_use_enabled": True, "environment_id": "environment_id", + "harness": { + "auth_secret_name": "auth_secret_name", + "type": "oz", + }, + "idle_timeout_minutes": 1, "mcp_servers": { "foo": { "args": ["string"], @@ -154,6 +159,11 @@ def test_method_update_with_all_params(self, client: OzAPI) -> None: "base_prompt": "base_prompt", "computer_use_enabled": True, "environment_id": "environment_id", + "harness": { + "auth_secret_name": "auth_secret_name", + "type": "oz", + }, + "idle_timeout_minutes": 1, "mcp_servers": { "foo": { "args": ["string"], @@ -395,6 +405,11 @@ async def test_method_create_with_all_params(self, async_client: AsyncOzAPI) -> "base_prompt": "base_prompt", "computer_use_enabled": True, "environment_id": "environment_id", + "harness": { + "auth_secret_name": "auth_secret_name", + "type": "oz", + }, + "idle_timeout_minutes": 1, "mcp_servers": { "foo": { "args": ["string"], @@ -509,6 +524,11 @@ async def test_method_update_with_all_params(self, async_client: AsyncOzAPI) -> "base_prompt": "base_prompt", "computer_use_enabled": True, "environment_id": "environment_id", + "harness": { + "auth_secret_name": "auth_secret_name", + "type": "oz", + }, + "idle_timeout_minutes": 1, "mcp_servers": { "foo": { "args": ["string"], diff --git a/tests/api_resources/test_agent.py b/tests/api_resources/test_agent.py index 00acc5b..b641c09 100644 --- a/tests/api_resources/test_agent.py +++ b/tests/api_resources/test_agent.py @@ -123,6 +123,11 @@ def test_method_run_with_all_params(self, client: OzAPI) -> None: "base_prompt": "base_prompt", "computer_use_enabled": True, "environment_id": "environment_id", + "harness": { + "auth_secret_name": "auth_secret_name", + "type": "oz", + }, + "idle_timeout_minutes": 1, "mcp_servers": { "foo": { "args": ["string"], @@ -140,7 +145,8 @@ def test_method_run_with_all_params(self, client: OzAPI) -> None: }, conversation_id="conversation_id", interactive=True, - prompt="Fix the bug in auth.go", + parent_run_id="parent_run_id", + prompt="prompt", skill="skill", team=True, title="title", @@ -277,6 +283,11 @@ async def test_method_run_with_all_params(self, async_client: AsyncOzAPI) -> Non "base_prompt": "base_prompt", "computer_use_enabled": True, "environment_id": "environment_id", + "harness": { + "auth_secret_name": "auth_secret_name", + "type": "oz", + }, + "idle_timeout_minutes": 1, "mcp_servers": { "foo": { "args": ["string"], @@ -294,7 +305,8 @@ async def test_method_run_with_all_params(self, async_client: AsyncOzAPI) -> Non }, conversation_id="conversation_id", interactive=True, - prompt="Fix the bug in auth.go", + parent_run_id="parent_run_id", + prompt="prompt", skill="skill", team=True, title="title", diff --git a/tests/test_client.py b/tests/test_client.py index 46b6ed3..988622f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -427,6 +427,30 @@ def test_default_query_option(self) -> None: client.close() + def test_hardcoded_query_params_in_url(self, client: OzAPI) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: OzAPI) -> None: request = client._build_request( FinalRequestOptions( @@ -849,7 +873,7 @@ def test_parse_retry_after_header( @mock.patch("oz_agent_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: OzAPI) -> None: - respx_mock.post("/agent/run").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/agent/runs").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): client.agent.with_streaming_response.run().__enter__() @@ -859,7 +883,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien @mock.patch("oz_agent_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: OzAPI) -> None: - respx_mock.post("/agent/run").mock(return_value=httpx.Response(500)) + respx_mock.post("/agent/runs").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): client.agent.with_streaming_response.run().__enter__() @@ -889,7 +913,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/agent/run").mock(side_effect=retry_handler) + respx_mock.post("/agent/runs").mock(side_effect=retry_handler) response = client.agent.with_raw_response.run() @@ -911,7 +935,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/agent/run").mock(side_effect=retry_handler) + respx_mock.post("/agent/runs").mock(side_effect=retry_handler) response = client.agent.with_raw_response.run(extra_headers={"x-stainless-retry-count": Omit()}) @@ -934,7 +958,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/agent/run").mock(side_effect=retry_handler) + respx_mock.post("/agent/runs").mock(side_effect=retry_handler) response = client.agent.with_raw_response.run(extra_headers={"x-stainless-retry-count": "42"}) @@ -1316,6 +1340,30 @@ async def test_default_query_option(self) -> None: await client.close() + async def test_hardcoded_query_params_in_url(self, async_client: AsyncOzAPI) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: OzAPI) -> None: request = client._build_request( FinalRequestOptions( @@ -1751,7 +1799,7 @@ async def test_parse_retry_after_header( @mock.patch("oz_agent_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncOzAPI) -> None: - respx_mock.post("/agent/run").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/agent/runs").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): await async_client.agent.with_streaming_response.run().__aenter__() @@ -1761,7 +1809,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, @mock.patch("oz_agent_sdk._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncOzAPI) -> None: - respx_mock.post("/agent/run").mock(return_value=httpx.Response(500)) + respx_mock.post("/agent/runs").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): await async_client.agent.with_streaming_response.run().__aenter__() @@ -1791,7 +1839,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/agent/run").mock(side_effect=retry_handler) + respx_mock.post("/agent/runs").mock(side_effect=retry_handler) response = await client.agent.with_raw_response.run() @@ -1815,7 +1863,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/agent/run").mock(side_effect=retry_handler) + respx_mock.post("/agent/runs").mock(side_effect=retry_handler) response = await client.agent.with_raw_response.run(extra_headers={"x-stainless-retry-count": Omit()}) @@ -1838,7 +1886,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/agent/run").mock(side_effect=retry_handler) + respx_mock.post("/agent/runs").mock(side_effect=retry_handler) response = await client.agent.with_raw_response.run(extra_headers={"x-stainless-retry-count": "42"})