diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e1a2442..f7014c3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.10.1" + ".": "0.11.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 917ae03..7345065 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-1b6767b852f3064c16752433a1cfd1db8ef38aa442dcb33b136e78fcc094bc51.yml +openapi_spec_hash: 1fdcb18c88b88e4d257b7748a4896b26 +config_hash: 1888db8b2f33dc16874aea51a90e78f7 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e6735e..5abdb25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 0.11.0 (2026-04-01) + +Full Changelog: [v0.10.1...v0.11.0](https://github.com/warpdotdev/oz-sdk-python/compare/v0.10.1...v0.11.0) + +### Features + +* Add VMIdleTimeoutMinutes param to API ([31b5a63](https://github.com/warpdotdev/oz-sdk-python/commit/31b5a63204a2baca1a41f0b4124408d2d2b7e6a4)) +* Address Stainless diagnostics ([58d59e3](https://github.com/warpdotdev/oz-sdk-python/commit/58d59e3593d27ad4bb17e340c384d9709f650806)) +* **api:** api update ([9fe02aa](https://github.com/warpdotdev/oz-sdk-python/commit/9fe02aad21c71cbab837f7d335e5234e1974bb90)) +* **api:** api update ([7fc79ef](https://github.com/warpdotdev/oz-sdk-python/commit/7fc79efe0897451670cb44cf29c0f99b06ffe821)) +* Create run for every local conversation and add filter ([aac84c5](https://github.com/warpdotdev/oz-sdk-python/commit/aac84c585eb3b0ed5262d683e544960a82eec2a2)) +* Endpoint to upload third-party harness block snapshots ([2f71989](https://github.com/warpdotdev/oz-sdk-python/commit/2f7198972cec56e72b802c7505fb445d10304bf4)) +* **internal:** implement indices array format for query and form serialization ([c518876](https://github.com/warpdotdev/oz-sdk-python/commit/c518876f3da1129d3c351982ed4a06590fd15f2e)) +* Use correct branch for Stainless PRs ([d7b298d](https://github.com/warpdotdev/oz-sdk-python/commit/d7b298d99c5dcb5e02530329fef222de92f3e9d6)) + ## 0.10.1 (2026-03-24) Full Changelog: [v0.10.0...v0.10.1](https://github.com/warpdotdev/oz-sdk-python/compare/v0.10.0...v0.10.1) 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/pyproject.toml b/pyproject.toml index e8c1daf..01da747 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oz-agent-sdk" -version = "0.10.1" +version = "0.11.0" description = "The official Python library for the oz-api API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/oz_agent_sdk/_base_client.py b/src/oz_agent_sdk/_base_client.py index d1510d4..fc8cfaf 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 @@ -671,7 +689,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 +1007,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 +1970,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 +1996,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/_version.py b/src/oz_agent_sdk/_version.py index 0f7ea40..113b87a 100644 --- a/src/oz_agent_sdk/_version.py +++ b/src/oz_agent_sdk/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "oz_agent_sdk" -__version__ = "0.10.1" # x-release-please-version +__version__ = "0.11.0" # x-release-please-version 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..c70be37 100644 --- a/src/oz_agent_sdk/resources/agent/agent.py +++ b/src/oz_agent_sdk/resources/agent/agent.py @@ -203,10 +203,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 @@ -244,7 +244,7 @@ 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, @@ -417,10 +417,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 @@ -458,7 +458,7 @@ 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, diff --git a/src/oz_agent_sdk/resources/agent/runs.py b/src/oz_agent_sdk/resources/agent/runs.py index 440bdcf..e13feec 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"] @@ -93,6 +93,7 @@ def list( 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,7 +304,7 @@ async def retrieve( cast_to=RunItem, ) - async def list( + def list( self, *, artifact_type: Literal["PLAN", "PULL_REQUEST", "SCREENSHOT"] | Omit = omit, @@ -308,6 +313,7 @@ async def list( 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/run_item.py b/src/oz_agent_sdk/types/agent/run_item.py index 014ab6b..0995dd7 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,6 +161,13 @@ 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""" 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..a1dc2af 100644 --- a/src/oz_agent_sdk/types/agent/run_list_params.py +++ b/src/oz_agent_sdk/types/agent/run_list_params.py @@ -15,7 +15,7 @@ class RunListParams(TypedDict, total=False): artifact_type: Literal["PLAN", "PULL_REQUEST", "SCREENSHOT"] - """Filter runs by artifact type (PLAN or PULL_REQUEST)""" + """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/ambient_agent_config.py b/src/oz_agent_sdk/types/ambient_agent_config.py index 67667c8..b667e62 100644 --- a/src/oz_agent_sdk/types/ambient_agent_config.py +++ b/src/oz_agent_sdk/types/ambient_agent_config.py @@ -25,6 +25,19 @@ class AmbientAgentConfig(BaseModel): environment_id: Optional[str] = None """UID of the environment to run the agent in""" + harness: Optional[str] = None + """ + Agent harness to use for the agent run. Default (empty) uses Warp's built-in Oz + 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..4f04d52 100644 --- a/src/oz_agent_sdk/types/ambient_agent_config_param.py +++ b/src/oz_agent_sdk/types/ambient_agent_config_param.py @@ -25,6 +25,19 @@ class AmbientAgentConfigParam(TypedDict, total=False): environment_id: str """UID of the environment to run the agent in""" + harness: str + """ + Agent harness to use for the agent run. Default (empty) uses Warp's built-in Oz + 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..1a1eceb 100644 --- a/tests/api_resources/agent/test_schedules.py +++ b/tests/api_resources/agent/test_schedules.py @@ -40,6 +40,8 @@ def test_method_create_with_all_params(self, client: OzAPI) -> None: "base_prompt": "base_prompt", "computer_use_enabled": True, "environment_id": "environment_id", + "harness": "harness", + "idle_timeout_minutes": 1, "mcp_servers": { "foo": { "args": ["string"], @@ -154,6 +156,8 @@ def test_method_update_with_all_params(self, client: OzAPI) -> None: "base_prompt": "base_prompt", "computer_use_enabled": True, "environment_id": "environment_id", + "harness": "harness", + "idle_timeout_minutes": 1, "mcp_servers": { "foo": { "args": ["string"], @@ -395,6 +399,8 @@ 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": "harness", + "idle_timeout_minutes": 1, "mcp_servers": { "foo": { "args": ["string"], @@ -509,6 +515,8 @@ 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": "harness", + "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..82f097a 100644 --- a/tests/api_resources/test_agent.py +++ b/tests/api_resources/test_agent.py @@ -123,6 +123,8 @@ def test_method_run_with_all_params(self, client: OzAPI) -> None: "base_prompt": "base_prompt", "computer_use_enabled": True, "environment_id": "environment_id", + "harness": "harness", + "idle_timeout_minutes": 1, "mcp_servers": { "foo": { "args": ["string"], @@ -140,7 +142,7 @@ def test_method_run_with_all_params(self, client: OzAPI) -> None: }, conversation_id="conversation_id", interactive=True, - prompt="Fix the bug in auth.go", + prompt="prompt", skill="skill", team=True, title="title", @@ -277,6 +279,8 @@ 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": "harness", + "idle_timeout_minutes": 1, "mcp_servers": { "foo": { "args": ["string"], @@ -294,7 +298,7 @@ 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", + prompt="prompt", skill="skill", team=True, title="title", diff --git a/tests/test_client.py b/tests/test_client.py index 46b6ed3..9b58870 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -849,7 +849,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 +859,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 +889,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 +911,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 +934,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"}) @@ -1751,7 +1751,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 +1761,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 +1791,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 +1815,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 +1838,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"})