From e2160da644e81873e8ea1680f5daaa082534414d Mon Sep 17 00:00:00 2001 From: wall Date: Thu, 20 Nov 2025 16:51:46 -0800 Subject: [PATCH 01/20] cp --- src/runloop_api_client/sdk/__init__.py | 9 ++- src/runloop_api_client/sdk/_types.py | 10 ++++ src/runloop_api_client/sdk/agent.py | 58 +++++++++++++++++++ src/runloop_api_client/sdk/async_.py | 67 ++++++++++++++++++++++ src/runloop_api_client/sdk/async_agent.py | 58 +++++++++++++++++++ src/runloop_api_client/sdk/sync.py | 68 +++++++++++++++++++++++ 6 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 src/runloop_api_client/sdk/agent.py create mode 100644 src/runloop_api_client/sdk/async_agent.py diff --git a/src/runloop_api_client/sdk/__init__.py b/src/runloop_api_client/sdk/__init__.py index 48b5e3103..fc4197480 100644 --- a/src/runloop_api_client/sdk/__init__.py +++ b/src/runloop_api_client/sdk/__init__.py @@ -5,7 +5,7 @@ from __future__ import annotations -from .sync import DevboxOps, ScorerOps, RunloopSDK, SnapshotOps, BlueprintOps, StorageObjectOps +from .sync import AgentOps, DevboxOps, ScorerOps, RunloopSDK, SnapshotOps, BlueprintOps, StorageObjectOps from .async_ import ( AsyncDevboxOps, AsyncScorerOps, @@ -13,12 +13,15 @@ AsyncSnapshotOps, AsyncBlueprintOps, AsyncStorageObjectOps, + AsyncAgentOps, ) +from .agent import Agent from .devbox import Devbox, NamedShell from .scorer import Scorer from .snapshot import Snapshot from .blueprint import Blueprint from .execution import Execution +from .async_agent import AsyncAgent from .async_devbox import AsyncDevbox, AsyncNamedShell from .async_scorer import AsyncScorer from .async_snapshot import AsyncSnapshot @@ -34,6 +37,8 @@ "RunloopSDK", "AsyncRunloopSDK", # Management interfaces + "AgentOps", + "AsyncAgentOps", "DevboxOps", "AsyncDevboxOps", "BlueprintOps", @@ -45,6 +50,8 @@ "StorageObjectOps", "AsyncStorageObjectOps", # Resource classes + "Agent", + "AsyncAgent", "Devbox", "AsyncDevbox", "Execution", diff --git a/src/runloop_api_client/sdk/_types.py b/src/runloop_api_client/sdk/_types.py index 028cb1805..76b9f533c 100644 --- a/src/runloop_api_client/sdk/_types.py +++ b/src/runloop_api_client/sdk/_types.py @@ -5,6 +5,8 @@ from ..lib.polling import PollingConfig from ..types.devboxes import DiskSnapshotListParams, DiskSnapshotUpdateParams from ..types.scenarios import ScorerListParams, ScorerCreateParams, ScorerUpdateParams, ScorerValidateParams +from ..types.agent_list_params import AgentListParams +from ..types.agent_create_params import AgentCreateParams from ..types.devbox_list_params import DevboxListParams from ..types.object_list_params import ObjectListParams from ..types.devbox_create_params import DevboxCreateParams, DevboxBaseCreateParams @@ -157,3 +159,11 @@ class SDKScorerUpdateParams(ScorerUpdateParams, LongRequestOptions): class SDKScorerValidateParams(ScorerValidateParams, LongRequestOptions): pass + + +class SDKAgentCreateParams(AgentCreateParams, LongRequestOptions): + pass + + +class SDKAgentListParams(AgentListParams, RequestOptions): + pass diff --git a/src/runloop_api_client/sdk/agent.py b/src/runloop_api_client/sdk/agent.py new file mode 100644 index 000000000..2484cdd1f --- /dev/null +++ b/src/runloop_api_client/sdk/agent.py @@ -0,0 +1,58 @@ +"""Agent resource class for synchronous operations.""" + +from __future__ import annotations + +from typing_extensions import Unpack, override + +from ._types import ( + RequestOptions, +) +from .._client import Runloop +from ..types.agent_view import AgentView + + +class Agent: + """Wrapper around synchronous agent operations.""" + + def __init__( + self, + client: Runloop, + agent_id: str, + ) -> None: + """Initialize the wrapper. + + :param client: Generated Runloop client + :type client: Runloop + :param agent_id: Agent identifier returned by the API + :type agent_id: str + """ + self._client = client + self._id = agent_id + + @override + def __repr__(self) -> str: + return f"" + + @property + def id(self) -> str: + """Return the agent identifier. + + :return: Unique agent ID + :rtype: str + """ + return self._id + + def get_info( + self, + **options: Unpack[RequestOptions], + ) -> AgentView: + """Retrieve the latest agent information. + + :param options: Optional request configuration + :return: Agent details + :rtype: AgentView + """ + return self._client.agents.retrieve( + self._id, + **options, + ) diff --git a/src/runloop_api_client/sdk/async_.py b/src/runloop_api_client/sdk/async_.py index 23f87d6e9..ba09fcfec 100644 --- a/src/runloop_api_client/sdk/async_.py +++ b/src/runloop_api_client/sdk/async_.py @@ -14,9 +14,11 @@ from ._types import ( LongRequestOptions, + SDKAgentListParams, SDKDevboxListParams, SDKObjectListParams, SDKScorerListParams, + SDKAgentCreateParams, SDKDevboxCreateParams, SDKObjectCreateParams, SDKScorerCreateParams, @@ -28,6 +30,7 @@ from .._types import Timeout, NotGiven, not_given from .._client import DEFAULT_MAX_RETRIES, AsyncRunloop from ._helpers import detect_content_type +from .async_agent import AsyncAgent from .async_devbox import AsyncDevbox from .async_scorer import AsyncScorer from .async_snapshot import AsyncSnapshot @@ -541,6 +544,66 @@ async def list(self, **params: Unpack[SDKScorerListParams]) -> list[AsyncScorer] """ page = await self._client.scenarios.scorers.list(**params) return [AsyncScorer(self._client, item.id) async for item in page] + +class AsyncAgentOps: + """High-level async manager for creating and managing agents. + + Accessed via ``runloop.agent`` from :class:`AsyncRunloopSDK`, provides + coroutines to create, retrieve, and list agents. + + Example: + >>> runloop = AsyncRunloopSDK() + >>> agent = await runloop.agent.create(name="my-agent") + >>> agents = await runloop.agent.list(limit=10) + """ + + def __init__(self, client: AsyncRunloop) -> None: + """Initialize the manager. + + :param client: Generated AsyncRunloop client to wrap + :type client: AsyncRunloop + """ + self._client = client + + async def create( + self, + **params: Unpack[SDKAgentCreateParams], + ) -> AsyncAgent: + """Create a new agent. + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for available parameters + :return: Wrapper bound to the newly created agent + :rtype: AsyncAgent + """ + agent_view = await self._client.agents.create( + **params, + ) + return AsyncAgent(self._client, agent_view.id) + + def from_id(self, agent_id: str) -> AsyncAgent: + """Attach to an existing agent by ID. + + :param agent_id: Existing agent ID + :type agent_id: str + :return: Wrapper bound to the requested agent + :rtype: AsyncAgent + """ + return AsyncAgent(self._client, agent_id) + + async def list( + self, + **params: Unpack[SDKAgentListParams], + ) -> list[AsyncAgent]: + """List agents accessible to the caller. + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentListParams` for available parameters + :return: Collection of agent wrappers + :rtype: list[AsyncAgent] + """ + page = await self._client.agents.list( + **params, + ) + return [AsyncAgent(self._client, item.id) for item in page.agents] class AsyncRunloopSDK: @@ -552,6 +615,8 @@ class AsyncRunloopSDK: :ivar api: Direct access to the generated async REST API client :vartype api: AsyncRunloop + :ivar agent: High-level async interface for agent management. + :vartype agent: AsyncAgentOps :ivar devbox: High-level async interface for devbox management :vartype devbox: AsyncDevboxOps :ivar blueprint: High-level async interface for blueprint management @@ -572,6 +637,7 @@ class AsyncRunloopSDK: """ api: AsyncRunloop + agent: AsyncAgentOps devbox: AsyncDevboxOps blueprint: AsyncBlueprintOps scorer: AsyncScorerOps @@ -616,6 +682,7 @@ def __init__( http_client=http_client, ) + self.agent = AsyncAgentOps(self.api) self.devbox = AsyncDevboxOps(self.api) self.blueprint = AsyncBlueprintOps(self.api) self.scorer = AsyncScorerOps(self.api) diff --git a/src/runloop_api_client/sdk/async_agent.py b/src/runloop_api_client/sdk/async_agent.py new file mode 100644 index 000000000..2d143a5dc --- /dev/null +++ b/src/runloop_api_client/sdk/async_agent.py @@ -0,0 +1,58 @@ +"""Agent resource class for asynchronous operations.""" + +from __future__ import annotations + +from typing_extensions import Unpack, override + +from ._types import ( + RequestOptions, +) +from .._client import AsyncRunloop +from ..types.agent_view import AgentView + + +class AsyncAgent: + """Async wrapper around agent operations.""" + + def __init__( + self, + client: AsyncRunloop, + agent_id: str, + ) -> None: + """Initialize the wrapper. + + :param client: Generated AsyncRunloop client + :type client: AsyncRunloop + :param agent_id: Agent identifier returned by the API + :type agent_id: str + """ + self._client = client + self._id = agent_id + + @override + def __repr__(self) -> str: + return f"" + + @property + def id(self) -> str: + """Return the agent identifier. + + :return: Unique agent ID + :rtype: str + """ + return self._id + + async def get_info( + self, + **options: Unpack[RequestOptions], + ) -> AgentView: + """Retrieve the latest agent information. + + :param options: Optional request configuration + :return: Agent details + :rtype: AgentView + """ + return await self._client.agents.retrieve( + self._id, + **options, + ) diff --git a/src/runloop_api_client/sdk/sync.py b/src/runloop_api_client/sdk/sync.py index 99410c2d0..4850f8d97 100644 --- a/src/runloop_api_client/sdk/sync.py +++ b/src/runloop_api_client/sdk/sync.py @@ -13,9 +13,11 @@ from ._types import ( LongRequestOptions, + SDKAgentListParams, SDKDevboxListParams, SDKObjectListParams, SDKScorerListParams, + SDKAgentCreateParams, SDKDevboxCreateParams, SDKObjectCreateParams, SDKScorerCreateParams, @@ -24,6 +26,7 @@ SDKDiskSnapshotListParams, SDKDevboxCreateFromImageParams, ) +from .agent import Agent from .devbox import Devbox from .scorer import Scorer from .._types import Timeout, NotGiven, not_given @@ -537,6 +540,67 @@ def list(self, **params: Unpack[SDKScorerListParams]) -> list[Scorer]: page = self._client.scenarios.scorers.list(**params) return [Scorer(self._client, item.id) for item in page] + +class AgentOps: + """High-level manager for creating and managing agents. + + Accessed via ``runloop.agent`` from :class:`RunloopSDK`, provides methods to + create, retrieve, and list agents. + + Example: + >>> runloop = RunloopSDK() + >>> agent = runloop.agent.create(name="my-agent") + >>> agents = runloop.agent.list(limit=10) + """ + + def __init__(self, client: Runloop) -> None: + """Initialize the manager. + + :param client: Generated Runloop client to wrap + :type client: Runloop + """ + self._client = client + + def create( + self, + **params: Unpack[SDKAgentCreateParams], + ) -> Agent: + """Create a new agent. + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for available parameters + :return: Wrapper bound to the newly created agent + :rtype: Agent + """ + agent_view = self._client.agents.create( + **params, + ) + return Agent(self._client, agent_view.id) + + def from_id(self, agent_id: str) -> Agent: + """Attach to an existing agent by ID. + + :param agent_id: Existing agent ID + :type agent_id: str + :return: Wrapper bound to the requested agent + :rtype: Agent + """ + return Agent(self._client, agent_id) + + def list( + self, + **params: Unpack[SDKAgentListParams], + ) -> list[Agent]: + """List agents accessible to the caller. + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentListParams` for available parameters + :return: Collection of agent wrappers + :rtype: list[Agent] + """ + page = self._client.agents.list( + **params, + ) + return [Agent(self._client, item.id) for item in page.agents] + class RunloopSDK: """High-level synchronous entry point for the Runloop SDK. @@ -547,6 +611,8 @@ class RunloopSDK: :ivar api: Direct access to the generated REST API client :vartype api: Runloop + :ivar agent: High-level interface for agent management. + :vartype agent: AgentOps :ivar devbox: High-level interface for devbox management :vartype devbox: DevboxOps :ivar blueprint: High-level interface for blueprint management @@ -567,6 +633,7 @@ class RunloopSDK: """ api: Runloop + agent: AgentOps devbox: DevboxOps blueprint: BlueprintOps scorer: ScorerOps @@ -611,6 +678,7 @@ def __init__( http_client=http_client, ) + self.agent = AgentOps(self.api) self.devbox = DevboxOps(self.api) self.blueprint = BlueprintOps(self.api) self.scorer = ScorerOps(self.api) From 5f1b6bd57995f3034e32261aaee48a1df5f51245 Mon Sep 17 00:00:00 2001 From: wall Date: Thu, 20 Nov 2025 17:26:13 -0800 Subject: [PATCH 02/20] cp --- src/runloop_api_client/sdk/__init__.py | 3 ++- src/runloop_api_client/sdk/_types.py | 2 +- src/runloop_api_client/sdk/sync.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/runloop_api_client/sdk/__init__.py b/src/runloop_api_client/sdk/__init__.py index fc4197480..b31e1667b 100644 --- a/src/runloop_api_client/sdk/__init__.py +++ b/src/runloop_api_client/sdk/__init__.py @@ -6,14 +6,15 @@ from __future__ import annotations from .sync import AgentOps, DevboxOps, ScorerOps, RunloopSDK, SnapshotOps, BlueprintOps, StorageObjectOps +from .agent import Agent from .async_ import ( + AsyncAgentOps, AsyncDevboxOps, AsyncScorerOps, AsyncRunloopSDK, AsyncSnapshotOps, AsyncBlueprintOps, AsyncStorageObjectOps, - AsyncAgentOps, ) from .agent import Agent from .devbox import Devbox, NamedShell diff --git a/src/runloop_api_client/sdk/_types.py b/src/runloop_api_client/sdk/_types.py index 76b9f533c..5654f9280 100644 --- a/src/runloop_api_client/sdk/_types.py +++ b/src/runloop_api_client/sdk/_types.py @@ -6,9 +6,9 @@ from ..types.devboxes import DiskSnapshotListParams, DiskSnapshotUpdateParams from ..types.scenarios import ScorerListParams, ScorerCreateParams, ScorerUpdateParams, ScorerValidateParams from ..types.agent_list_params import AgentListParams -from ..types.agent_create_params import AgentCreateParams from ..types.devbox_list_params import DevboxListParams from ..types.object_list_params import ObjectListParams +from ..types.agent_create_params import AgentCreateParams from ..types.devbox_create_params import DevboxCreateParams, DevboxBaseCreateParams from ..types.object_create_params import ObjectCreateParams from ..types.blueprint_list_params import BlueprintListParams diff --git a/src/runloop_api_client/sdk/sync.py b/src/runloop_api_client/sdk/sync.py index 4850f8d97..d9c6e8c20 100644 --- a/src/runloop_api_client/sdk/sync.py +++ b/src/runloop_api_client/sdk/sync.py @@ -11,6 +11,7 @@ import httpx +from .agent import Agent from ._types import ( LongRequestOptions, SDKAgentListParams, @@ -26,7 +27,6 @@ SDKDiskSnapshotListParams, SDKDevboxCreateFromImageParams, ) -from .agent import Agent from .devbox import Devbox from .scorer import Scorer from .._types import Timeout, NotGiven, not_given From fd5578c334944b07c46d0849b46694a0ce743bd8 Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Sat, 22 Nov 2025 00:52:22 -0800 Subject: [PATCH 03/20] persist additional state in the Agent and AsyncAgent objects --- src/runloop_api_client/sdk/agent.py | 13 +++++++++---- src/runloop_api_client/sdk/async_.py | 2 +- src/runloop_api_client/sdk/async_agent.py | 9 +++++++++ src/runloop_api_client/sdk/sync.py | 2 +- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/runloop_api_client/sdk/agent.py b/src/runloop_api_client/sdk/agent.py index 2484cdd1f..daf62428f 100644 --- a/src/runloop_api_client/sdk/agent.py +++ b/src/runloop_api_client/sdk/agent.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import Optional from typing_extensions import Unpack, override from ._types import ( @@ -18,6 +19,7 @@ def __init__( self, client: Runloop, agent_id: str, + agent_view: Optional[AgentView] = None, ) -> None: """Initialize the wrapper. @@ -28,6 +30,7 @@ def __init__( """ self._client = client self._id = agent_id + self._agent_view = agent_view @override def __repr__(self) -> str: @@ -52,7 +55,9 @@ def get_info( :return: Agent details :rtype: AgentView """ - return self._client.agents.retrieve( - self._id, - **options, - ) + if self._agent_view is None: + self._agent_view = self._client.agents.retrieve( + self._id, + **options, + ) + return self._agent_view diff --git a/src/runloop_api_client/sdk/async_.py b/src/runloop_api_client/sdk/async_.py index ba09fcfec..5a07de376 100644 --- a/src/runloop_api_client/sdk/async_.py +++ b/src/runloop_api_client/sdk/async_.py @@ -159,7 +159,7 @@ async def list( page = await self._client.devboxes.list( **params, ) - return [AsyncDevbox(self._client, item.id) for item in page.devboxes] + return [AsyncDevbox(self._client, item.id, item) for item in page.devboxes] class AsyncSnapshotOps: diff --git a/src/runloop_api_client/sdk/async_agent.py b/src/runloop_api_client/sdk/async_agent.py index 2d143a5dc..c82c51d55 100644 --- a/src/runloop_api_client/sdk/async_agent.py +++ b/src/runloop_api_client/sdk/async_agent.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import Optional from typing_extensions import Unpack, override from ._types import ( @@ -18,6 +19,7 @@ def __init__( self, client: AsyncRunloop, agent_id: str, + agent_view: Optional[AgentView] = None, ) -> None: """Initialize the wrapper. @@ -28,6 +30,7 @@ def __init__( """ self._client = client self._id = agent_id + self._agent_view = agent_view @override def __repr__(self) -> str: @@ -56,3 +59,9 @@ async def get_info( self._id, **options, ) + if self._agent_view is None: + self._agent_view = self._client.agents.retrieve( + self._id, + **options, + ) + return self._agent_view diff --git a/src/runloop_api_client/sdk/sync.py b/src/runloop_api_client/sdk/sync.py index d9c6e8c20..556293d57 100644 --- a/src/runloop_api_client/sdk/sync.py +++ b/src/runloop_api_client/sdk/sync.py @@ -599,7 +599,7 @@ def list( page = self._client.agents.list( **params, ) - return [Agent(self._client, item.id) for item in page.agents] + return [Agent(self._client, item.id, item) for item in page.agents] class RunloopSDK: From 395951c3e0d3853e14e2a0bdc9660665cf7a0bc4 Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Sat, 22 Nov 2025 01:17:51 -0800 Subject: [PATCH 04/20] adjust for class renames --- src/runloop_api_client/sdk/_types.py | 2 +- src/runloop_api_client/sdk/agent.py | 4 ++-- src/runloop_api_client/sdk/async_agent.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/runloop_api_client/sdk/_types.py b/src/runloop_api_client/sdk/_types.py index 5654f9280..4432c07a5 100644 --- a/src/runloop_api_client/sdk/_types.py +++ b/src/runloop_api_client/sdk/_types.py @@ -165,5 +165,5 @@ class SDKAgentCreateParams(AgentCreateParams, LongRequestOptions): pass -class SDKAgentListParams(AgentListParams, RequestOptions): +class SDKAgentListParams(AgentListParams, BaseRequestOptions): pass diff --git a/src/runloop_api_client/sdk/agent.py b/src/runloop_api_client/sdk/agent.py index daf62428f..812a34555 100644 --- a/src/runloop_api_client/sdk/agent.py +++ b/src/runloop_api_client/sdk/agent.py @@ -6,7 +6,7 @@ from typing_extensions import Unpack, override from ._types import ( - RequestOptions, + BaseRequestOptions, ) from .._client import Runloop from ..types.agent_view import AgentView @@ -47,7 +47,7 @@ def id(self) -> str: def get_info( self, - **options: Unpack[RequestOptions], + **options: Unpack[BaseRequestOptions], ) -> AgentView: """Retrieve the latest agent information. diff --git a/src/runloop_api_client/sdk/async_agent.py b/src/runloop_api_client/sdk/async_agent.py index c82c51d55..e9740ce47 100644 --- a/src/runloop_api_client/sdk/async_agent.py +++ b/src/runloop_api_client/sdk/async_agent.py @@ -6,7 +6,7 @@ from typing_extensions import Unpack, override from ._types import ( - RequestOptions, + BaseRequestOptions, ) from .._client import AsyncRunloop from ..types.agent_view import AgentView @@ -47,7 +47,7 @@ def id(self) -> str: async def get_info( self, - **options: Unpack[RequestOptions], + **options: Unpack[BaseRequestOptions], ) -> AgentView: """Retrieve the latest agent information. From ec52a805cd7bb5fce21836e23bd8a439fafad950 Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Sat, 22 Nov 2025 12:12:15 -0800 Subject: [PATCH 05/20] agent creation fix arg params --- src/runloop_api_client/sdk/async_.py | 6 +++--- src/runloop_api_client/sdk/sync.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/runloop_api_client/sdk/async_.py b/src/runloop_api_client/sdk/async_.py index 5a07de376..bedcae47a 100644 --- a/src/runloop_api_client/sdk/async_.py +++ b/src/runloop_api_client/sdk/async_.py @@ -159,7 +159,7 @@ async def list( page = await self._client.devboxes.list( **params, ) - return [AsyncDevbox(self._client, item.id, item) for item in page.devboxes] + return [AsyncDevbox(self._client, item.id) for item in page.devboxes] class AsyncSnapshotOps: @@ -578,7 +578,7 @@ async def create( agent_view = await self._client.agents.create( **params, ) - return AsyncAgent(self._client, agent_view.id) + return AsyncAgent(self._client, agent_view.id, agent_view) def from_id(self, agent_id: str) -> AsyncAgent: """Attach to an existing agent by ID. @@ -603,7 +603,7 @@ async def list( page = await self._client.agents.list( **params, ) - return [AsyncAgent(self._client, item.id) for item in page.agents] + return [AsyncAgent(self._client, item.id, item) for item in page.agents] class AsyncRunloopSDK: diff --git a/src/runloop_api_client/sdk/sync.py b/src/runloop_api_client/sdk/sync.py index 556293d57..38861b1d5 100644 --- a/src/runloop_api_client/sdk/sync.py +++ b/src/runloop_api_client/sdk/sync.py @@ -574,7 +574,7 @@ def create( agent_view = self._client.agents.create( **params, ) - return Agent(self._client, agent_view.id) + return Agent(self._client, agent_view.id, agent_view) def from_id(self, agent_id: str) -> Agent: """Attach to an existing agent by ID. From 5d95b03728d231b54cdc05fa1bb49830c301cf93 Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Sat, 22 Nov 2025 12:13:16 -0800 Subject: [PATCH 06/20] initial simple tests for AgentOps --- tests/sdk/conftest.py | 15 +++++++++++++ tests/sdk/test_agent.py | 43 +++++++++++++++++++++++++++++++++++++ tests/sdk/test_ops.py | 47 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 tests/sdk/test_agent.py diff --git a/tests/sdk/conftest.py b/tests/sdk/conftest.py index b61a93301..017d6bab7 100644 --- a/tests/sdk/conftest.py +++ b/tests/sdk/conftest.py @@ -21,6 +21,7 @@ "blueprint": "bp_123", "object": "obj_123", "scorer": "scorer_123", + "agent": "agent_123", } # Test URL constants @@ -96,6 +97,14 @@ class MockScorerView: type: str = "test_scorer" +@dataclass +class MockAgentView: + """Mock AgentView for testing.""" + + id: str = "agent_123" + name: str = "test-agent" + + def create_mock_httpx_client(methods: dict[str, Any] | None = None) -> AsyncMock: """ Create a mock httpx.AsyncClient with proper context manager setup. @@ -186,6 +195,12 @@ def scorer_view() -> MockScorerView: return MockScorerView() +@pytest.fixture +def agent_view() -> MockAgentView: + """Create a mock AgentView.""" + return MockAgentView() + + @pytest.fixture def mock_httpx_response() -> Mock: """Create a mock httpx.Response.""" diff --git a/tests/sdk/test_agent.py b/tests/sdk/test_agent.py new file mode 100644 index 000000000..7580b44d8 --- /dev/null +++ b/tests/sdk/test_agent.py @@ -0,0 +1,43 @@ +"""Comprehensive tests for sync Agent class.""" + +from __future__ import annotations + +from unittest.mock import Mock + +from tests.sdk.conftest import MockAgentView +from runloop_api_client.sdk import Agent + + +class TestAgent: + """Tests for Agent class.""" + + def test_init(self, mock_client: Mock) -> None: + """Test Agent initialization.""" + agent = Agent(mock_client, "agent_123") + assert agent.id == "agent_123" + + def test_repr(self, mock_client: Mock) -> None: + """Test Agent string representation.""" + agent = Agent(mock_client, "agent_123") + assert repr(agent) == "" + + def test_get_info(self, mock_client: Mock, agent_view: MockAgentView) -> None: + """Test get_info method.""" + mock_client.agents.retrieve.return_value = agent_view + + agent = Agent(mock_client, "agent_123") + result = agent.get_info( + extra_headers={"X-Custom": "value"}, + extra_query={"param": "value"}, + extra_body={"key": "value"}, + timeout=30.0, + ) + + assert result == agent_view + mock_client.agents.retrieve.assert_called_once_with( + "agent_123", + extra_headers={"X-Custom": "value"}, + extra_query={"param": "value"}, + extra_body={"key": "value"}, + timeout=30.0, + ) diff --git a/tests/sdk/test_ops.py b/tests/sdk/test_ops.py index 83c9117f4..09206c757 100644 --- a/tests/sdk/test_ops.py +++ b/tests/sdk/test_ops.py @@ -14,9 +14,10 @@ MockScorerView, MockSnapshotView, MockBlueprintView, + MockAgentView, create_mock_httpx_response, ) -from runloop_api_client.sdk import Devbox, Scorer, Snapshot, Blueprint, StorageObject +from runloop_api_client.sdk import Agent, Devbox, Scorer, Snapshot, Blueprint, StorageObject from runloop_api_client.sdk.sync import ( DevboxOps, ScorerOps, @@ -24,6 +25,7 @@ SnapshotOps, BlueprintOps, StorageObjectOps, + AgentOps, ) from runloop_api_client.lib.polling import PollingConfig @@ -653,6 +655,48 @@ def test_list_multiple(self, mock_client: Mock) -> None: mock_client.scenarios.scorers.list.assert_called_once() +class TestAgentClient: + """Tests for AgentClient class.""" + + def test_create(self, mock_client: Mock, agent_view: MockAgentView) -> None: + """Test create method.""" + mock_client.agents.create.return_value = agent_view + + client = AgentOps(mock_client) + agent = client.create( + name="test-agent", + metadata={"key": "value"}, + ) + + assert isinstance(agent, Agent) + assert agent.id == "agent_123" + mock_client.agents.create.assert_called_once() + + def test_from_id(self, mock_client: Mock) -> None: + """Test from_id method.""" + client = AgentOps(mock_client) + agent = client.from_id("agent_123") + + assert isinstance(agent, Agent) + assert agent.id == "agent_123" + + def test_list(self, mock_client: Mock, agent_view: MockAgentView) -> None: + """Test list method.""" + page = SimpleNamespace(agents=[agent_view]) + mock_client.agents.list.return_value = page + + client = AgentOps(mock_client) + agents = client.list( + limit=10, + starting_after="agent_000", + ) + + assert len(agents) == 1 + assert isinstance(agents[0], Agent) + assert agents[0].id == "agent_123" + mock_client.agents.list.assert_called_once() + + class TestRunloopSDK: """Tests for RunloopSDK class.""" @@ -660,6 +704,7 @@ def test_init(self) -> None: """Test RunloopSDK initialization.""" sdk = RunloopSDK(bearer_token="test-token") assert sdk.api is not None + assert isinstance(sdk.agent, AgentOps) assert isinstance(sdk.devbox, DevboxOps) assert isinstance(sdk.scorer, ScorerOps) assert isinstance(sdk.snapshot, SnapshotOps) From f15629cac4ff0a4c08d7572f4a6ff5d6a669fef5 Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Mon, 24 Nov 2025 15:31:19 -0800 Subject: [PATCH 07/20] add more deets to agent list test --- tests/sdk/conftest.py | 3 ++ tests/sdk/test_ops.py | 66 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/tests/sdk/conftest.py b/tests/sdk/conftest.py index 017d6bab7..6abf02684 100644 --- a/tests/sdk/conftest.py +++ b/tests/sdk/conftest.py @@ -103,6 +103,9 @@ class MockAgentView: id: str = "agent_123" name: str = "test-agent" + create_time_ms: int = 1234567890000 + is_public: bool = False + source: Any = None def create_mock_httpx_client(methods: dict[str, Any] | None = None) -> AsyncMock: diff --git a/tests/sdk/test_ops.py b/tests/sdk/test_ops.py index 09206c757..99365c270 100644 --- a/tests/sdk/test_ops.py +++ b/tests/sdk/test_ops.py @@ -680,9 +680,32 @@ def test_from_id(self, mock_client: Mock) -> None: assert isinstance(agent, Agent) assert agent.id == "agent_123" - def test_list(self, mock_client: Mock, agent_view: MockAgentView) -> None: + def test_list(self, mock_client: Mock) -> None: """Test list method.""" - page = SimpleNamespace(agents=[agent_view]) + # Create three agent views with different data + agent_view_1 = MockAgentView( + id="agent_001", + name="first-agent", + create_time_ms=1234567890000, + is_public=False, + source=None, + ) + agent_view_2 = MockAgentView( + id="agent_002", + name="second-agent", + create_time_ms=1234567891000, + is_public=True, + source={"type": "git", "git": {"repository": "https://github.com/example/repo"}}, + ) + agent_view_3 = MockAgentView( + id="agent_003", + name="third-agent", + create_time_ms=1234567892000, + is_public=False, + source={"type": "npm", "npm": {"package_name": "example-package"}}, + ) + + page = SimpleNamespace(agents=[agent_view_1, agent_view_2, agent_view_3]) mock_client.agents.list.return_value = page client = AgentOps(mock_client) @@ -691,9 +714,42 @@ def test_list(self, mock_client: Mock, agent_view: MockAgentView) -> None: starting_after="agent_000", ) - assert len(agents) == 1 - assert isinstance(agents[0], Agent) - assert agents[0].id == "agent_123" + # Verify we got three agents + assert len(agents) == 3 + assert all(isinstance(agent, Agent) for agent in agents) + + # Verify the agent IDs + assert agents[0].id == "agent_001" + assert agents[1].id == "agent_002" + assert agents[2].id == "agent_003" + + # Test that get_info() retrieves the cached AgentView for the first agent + info = agents[0].get_info() + assert info.id == "agent_001" + assert info.name == "first-agent" + assert info.create_time_ms == 1234567890000 + assert info.is_public is False + assert info.source is None + + # Test that get_info() retrieves the cached AgentView for the second agent + info = agents[1].get_info() + assert info.id == "agent_002" + assert info.name == "second-agent" + assert info.create_time_ms == 1234567891000 + assert info.is_public is True + assert info.source == {"type": "git", "git": {"repository": "https://github.com/example/repo"}} + + # Test that get_info() retrieves the cached AgentView for the third agent + info = agents[2].get_info() + assert info.id == "agent_003" + assert info.name == "third-agent" + assert info.create_time_ms == 1234567892000 + assert info.is_public is False + assert info.source == {"type": "npm", "npm": {"package_name": "example-package"}} + + # Verify that agents.retrieve was NOT called (because we're using cached data) + mock_client.agents.retrieve.assert_not_called() + mock_client.agents.list.assert_called_once() From c387897d8826bddc06cc9b349ca77ad766422b6f Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Tue, 25 Nov 2025 17:07:23 -0800 Subject: [PATCH 08/20] add some tests; not working yet --- tests/sdk/test_async_agent.py | 46 +++++ tests/sdk/test_async_ops.py | 117 ++++++++++- tests/smoketests/sdk/README.md | 32 ++- tests/smoketests/sdk/test_agent.py | 244 +++++++++++++++++++++++ tests/smoketests/sdk/test_agent.py~ | 195 ++++++++++++++++++ tests/smoketests/sdk/test_async_agent.py | 244 +++++++++++++++++++++++ 6 files changed, 870 insertions(+), 8 deletions(-) create mode 100644 tests/sdk/test_async_agent.py create mode 100644 tests/smoketests/sdk/test_agent.py create mode 100644 tests/smoketests/sdk/test_agent.py~ create mode 100644 tests/smoketests/sdk/test_async_agent.py diff --git a/tests/sdk/test_async_agent.py b/tests/sdk/test_async_agent.py new file mode 100644 index 000000000..a2bb9496c --- /dev/null +++ b/tests/sdk/test_async_agent.py @@ -0,0 +1,46 @@ +"""Comprehensive tests for async AsyncAgent class.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +import pytest + +from tests.sdk.conftest import MockAgentView +from runloop_api_client.sdk import AsyncAgent + + +class TestAsyncAgent: + """Tests for AsyncAgent class.""" + + def test_init(self, mock_async_client: AsyncMock) -> None: + """Test AsyncAgent initialization.""" + agent = AsyncAgent(mock_async_client, "agent_123") + assert agent.id == "agent_123" + + def test_repr(self, mock_async_client: AsyncMock) -> None: + """Test AsyncAgent string representation.""" + agent = AsyncAgent(mock_async_client, "agent_123") + assert repr(agent) == "" + + @pytest.mark.asyncio + async def test_get_info(self, mock_async_client: AsyncMock, agent_view: MockAgentView) -> None: + """Test get_info method.""" + mock_async_client.agents.retrieve = AsyncMock(return_value=agent_view) + + agent = AsyncAgent(mock_async_client, "agent_123") + result = await agent.get_info( + extra_headers={"X-Custom": "value"}, + extra_query={"param": "value"}, + extra_body={"key": "value"}, + timeout=30.0, + ) + + assert result == agent_view + mock_async_client.agents.retrieve.assert_called_once_with( + "agent_123", + extra_headers={"X-Custom": "value"}, + extra_query={"param": "value"}, + extra_body={"key": "value"}, + timeout=30.0, + ) diff --git a/tests/sdk/test_async_ops.py b/tests/sdk/test_async_ops.py index 340d86647..0e60d9dc1 100644 --- a/tests/sdk/test_async_ops.py +++ b/tests/sdk/test_async_ops.py @@ -16,9 +16,10 @@ MockScorerView, MockSnapshotView, MockBlueprintView, + MockAgentView, create_mock_httpx_response, ) -from runloop_api_client.sdk import AsyncDevbox, AsyncScorer, AsyncSnapshot, AsyncBlueprint, AsyncStorageObject +from runloop_api_client.sdk import AsyncAgent, AsyncDevbox, AsyncScorer, AsyncSnapshot, AsyncBlueprint, AsyncStorageObject from runloop_api_client.sdk.async_ import ( AsyncDevboxOps, AsyncScorerOps, @@ -26,6 +27,7 @@ AsyncSnapshotOps, AsyncBlueprintOps, AsyncStorageObjectOps, + AsyncAgentOps, ) from runloop_api_client.lib.polling import PollingConfig @@ -721,6 +723,118 @@ async def async_iter(): assert scorers[1].id == "scorer_002" mock_async_client.scenarios.scorers.list.assert_awaited_once() + +class TestAsyncAgentClient: + """Tests for AsyncAgentClient class.""" + + @pytest.mark.asyncio + async def test_create(self, mock_async_client: AsyncMock, agent_view: MockAgentView) -> None: + """Test create method.""" + mock_async_client.agents.create = AsyncMock(return_value=agent_view) + + client = AsyncAgentOps(mock_async_client) + agent = await client.create( + name="test-agent", + metadata={"key": "value"}, + ) + + assert isinstance(agent, AsyncAgent) + assert agent.id == "agent_123" + mock_async_client.agents.create.assert_called_once() + + def test_from_id(self, mock_async_client: AsyncMock) -> None: + """Test from_id method.""" + client = AsyncAgentOps(mock_async_client) + agent = client.from_id("agent_123") + + assert isinstance(agent, AsyncAgent) + assert agent.id == "agent_123" + + @pytest.mark.asyncio + async def test_list(self, mock_async_client: AsyncMock) -> None: + """Test list method.""" + # Create three agent views with different data + agent_view_1 = MockAgentView( + id="agent_001", + name="first-agent", + create_time_ms=1234567890000, + is_public=False, + source=None, + ) + agent_view_2 = MockAgentView( + id="agent_002", + name="second-agent", + create_time_ms=1234567891000, + is_public=True, + source={"type": "git", "git": {"repository": "https://github.com/example/repo"}}, + ) + agent_view_3 = MockAgentView( + id="agent_003", + name="third-agent", + create_time_ms=1234567892000, + is_public=False, + source={"type": "npm", "npm": {"package_name": "example-package"}}, + ) + + page = SimpleNamespace(agents=[agent_view_1, agent_view_2, agent_view_3]) + mock_async_client.agents.list = AsyncMock(return_value=page) + + # Mock retrieve to return the corresponding agent_view when called + async def mock_retrieve(agent_id, **kwargs): + if agent_id == "agent_001": + return agent_view_1 + elif agent_id == "agent_002": + return agent_view_2 + elif agent_id == "agent_003": + return agent_view_3 + return None + + mock_async_client.agents.retrieve = AsyncMock(side_effect=mock_retrieve) + + client = AsyncAgentOps(mock_async_client) + agents = await client.list( + limit=10, + starting_after="agent_000", + ) + + # Verify we got three agents + assert len(agents) == 3 + assert all(isinstance(agent, AsyncAgent) for agent in agents) + + # Verify the agent IDs + assert agents[0].id == "agent_001" + assert agents[1].id == "agent_002" + assert agents[2].id == "agent_003" + + # Test that get_info() retrieves the AgentView for the first agent + info = await agents[0].get_info() + assert info.id == "agent_001" + assert info.name == "first-agent" + assert info.create_time_ms == 1234567890000 + assert info.is_public is False + assert info.source is None + + # Test that get_info() retrieves the AgentView for the second agent + info = await agents[1].get_info() + assert info.id == "agent_002" + assert info.name == "second-agent" + assert info.create_time_ms == 1234567891000 + assert info.is_public is True + assert info.source == {"type": "git", "git": {"repository": "https://github.com/example/repo"}} + + # Test that get_info() retrieves the AgentView for the third agent + info = await agents[2].get_info() + assert info.id == "agent_003" + assert info.name == "third-agent" + assert info.create_time_ms == 1234567892000 + assert info.is_public is False + assert info.source == {"type": "npm", "npm": {"package_name": "example-package"}} + + # Verify that agents.retrieve was called three times (once for each get_info) + assert mock_async_client.agents.retrieve.call_count == 3 + + mock_async_client.agents.list.assert_called_once() + class TestAsyncRunloopSDK: """Tests for AsyncRunloopSDK class.""" @@ -729,6 +843,7 @@ def test_init(self) -> None: """Test AsyncRunloopSDK initialization.""" sdk = AsyncRunloopSDK(bearer_token="test-token") assert sdk.api is not None + assert isinstance(sdk.agent, AsyncAgentOps) assert isinstance(sdk.devbox, AsyncDevboxOps) assert isinstance(sdk.scorer, AsyncScorerOps) assert isinstance(sdk.snapshot, AsyncSnapshotOps) diff --git a/tests/smoketests/sdk/README.md b/tests/smoketests/sdk/README.md index 66e6c9dc4..65e42d5e9 100644 --- a/tests/smoketests/sdk/README.md +++ b/tests/smoketests/sdk/README.md @@ -1,12 +1,12 @@ # SDK End-to-End Smoke Tests -Comprehensive end-to-end tests for the object-oriented Python SDK (`runloop_api_client.sdk`). These tests run against the real Runloop API to validate critical workflows including devboxes, blueprints, snapshots, and storage objects. +Comprehensive end-to-end tests for the object-oriented Python SDK (`runloop_api_client.sdk`). These tests run against the real Runloop API to validate critical workflows including agents, devboxes, blueprints, snapshots, and storage objects. ## Overview The Python SDK provides both synchronous and asynchronous interfaces: -- **Synchronous SDK**: `RunloopSDK` with `Devbox`, `Blueprint`, `Snapshot`, `StorageObject` -- **Asynchronous SDK**: `AsyncRunloopSDK` with `AsyncDevbox`, `AsyncBlueprint`, `AsyncSnapshot`, `AsyncStorageObject` +- **Synchronous SDK**: `RunloopSDK` with `Agent`, `Devbox`, `Blueprint`, `Snapshot`, `StorageObject` +- **Asynchronous SDK**: `AsyncRunloopSDK` with `AsyncAgent`, `AsyncDevbox`, `AsyncBlueprint`, `AsyncSnapshot`, `AsyncStorageObject` These tests ensure both interfaces work correctly in real-world scenarios. @@ -15,6 +15,17 @@ These tests ensure both interfaces work correctly in real-world scenarios. ### Infrastructure - `conftest.py` - Pytest fixtures for SDK client instances +### Agent Tests +- `test_agent.py` - Synchronous agent operations +- `test_async_agent.py` - Asynchronous agent operations + +**Test Coverage:** +- Agent lifecycle (create, get_info) +- Agent listing and retrieval +- Agent creation with metadata +- Agent creation with different source types (npm, git) +- Public/private agent configuration + ### Devbox Tests - `test_devbox.py` - Synchronous devbox operations - `test_async_devbox.py` - Asynchronous devbox operations @@ -74,23 +85,30 @@ export RUNLOOP_API_KEY=your_api_key_here ### Run All SDK Smoke Tests ```bash -RUN_SMOKETESTS=1 uv run pytest -q -vv -m smoketest tests/smoketests/sdk/ +RUN_SMOKETESTS=1 uv run pytest -q -vv -n 20 -m smoketest tests/smoketests/sdk/ ``` ### Run Specific Test File ```bash -RUN_SMOKETESTS=1 uv run pytest -q -vv -m smoketest tests/smoketests/sdk/test_devbox.py +RUN_SMOKETESTS=1 uv run pytest -q -vv -n 20 -m smoketest tests/smoketests/sdk/test_devbox.py ``` ### Run Specific Test ```bash -RUN_SMOKETESTS=1 uv run pytest -q -vv -m smoketest -k "test_devbox_lifecycle" tests/smoketests/sdk/ +# match a test from anywhere in the directory +RUN_SMOKETESTS=1 uv run pytest -q -vv-m smoketest -k "test_devbox_lifecycle" tests/smoketests/sdk/ + +# specify the exact test +RUN_SMOKETESTS=1 uv run pytest -q -vv -m smoketest tests/smoketests/sdk/test_devbox.py::TestDevboxLifecycle::test_devbox_create + +# match a test from one file +RUN_SMOKETESTS=1 uv run pytest -q -vv -m smoketest -k "test_devbox_create" tests/smoketests/sdk/test_devbox.py ``` ### Run Only Sync or Async Tests ```bash # Sync tests only (files without 'async' prefix) -RUN_SMOKETESTS=1 uv run pytest -q -vv -m smoketest tests/smoketests/sdk/test_devbox.py tests/smoketests/sdk/test_blueprint.py tests/smoketests/sdk/test_snapshot.py tests/smoketests/sdk/test_storage_object.py +RUN_SMOKETESTS=1 uv run pytest -q -vv -m smoketest $(ls tests/smoketests/sdk/*.py | grep -v async) # Async tests only (files with 'async' prefix) RUN_SMOKETESTS=1 uv run pytest -q -vv -m smoketest tests/smoketests/sdk/test_async_*.py diff --git a/tests/smoketests/sdk/test_agent.py b/tests/smoketests/sdk/test_agent.py new file mode 100644 index 000000000..92f5c77cd --- /dev/null +++ b/tests/smoketests/sdk/test_agent.py @@ -0,0 +1,244 @@ +"""Synchronous SDK smoke tests for Agent operations.""" + +from __future__ import annotations + +import pytest + +from runloop_api_client.sdk import RunloopSDK +from tests.smoketests.utils import unique_name + +pytestmark = [pytest.mark.smoketest] + +THIRTY_SECOND_TIMEOUT = 30 +TWO_MINUTE_TIMEOUT = 120 + + +class TestAgentLifecycle: + """Test basic agent lifecycle operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_agent_create_basic(self, sdk_client: RunloopSDK) -> None: + """Test creating a basic agent.""" + name = unique_name("sdk-agent-basic") + agent = sdk_client.agent.create( + name=name, + source={ + "type": "object", + "object": { + "object_id": "obj_placeholder", + }, + }, + ) + + try: + assert agent is not None + assert agent.id is not None + assert len(agent.id) > 0 + + # Verify agent information + info = agent.get_info() + assert info.id == agent.id + assert info.name == name + finally: + # Agents don't have a delete method, they're managed by the API + pass + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_agent_create_with_metadata(self, sdk_client: RunloopSDK) -> None: + """Test creating an agent with metadata.""" + name = unique_name("sdk-agent-metadata") + metadata = { + "purpose": "sdk-testing", + "version": "1.0", + } + + agent = sdk_client.agent.create( + name=name, + metadata=metadata, + source={ + "type": "object", + "object": { + "object_id": "obj_placeholder", + }, + }, + ) + + try: + assert agent.id is not None + + # Verify metadata is preserved + info = agent.get_info() + assert info.name == name + # Note: Metadata handling may vary based on API implementation + finally: + pass + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_agent_get_info(self, sdk_client: RunloopSDK) -> None: + """Test retrieving agent information.""" + name = unique_name("sdk-agent-info") + agent = sdk_client.agent.create( + name=name, + source={ + "type": "object", + "object": { + "object_id": "obj_placeholder", + }, + }, + ) + + try: + info = agent.get_info() + + assert info.id == agent.id + assert info.name == name + assert info.create_time_ms > 0 + assert isinstance(info.is_public, bool) + finally: + pass + + +class TestAgentListing: + """Test agent listing and retrieval operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_list_agents(self, sdk_client: RunloopSDK) -> None: + """Test listing agents.""" + agents = sdk_client.agent.list(limit=10) + + assert isinstance(agents, list) + # List might be empty, that's okay + assert len(agents) >= 0 + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_get_agent_by_id(self, sdk_client: RunloopSDK) -> None: + """Test retrieving agent by ID.""" + # Create an agent + created = sdk_client.agent.create( + name=unique_name("sdk-agent-retrieve"), + source={ + "type": "object", + "object": { + "object_id": "obj_placeholder", + }, + }, + ) + + try: + # Retrieve it by ID + retrieved = sdk_client.agent.from_id(created.id) + assert retrieved.id == created.id + + # Verify it's the same agent + info = retrieved.get_info() + assert info.id == created.id + finally: + pass + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_list_multiple_agents(self, sdk_client: RunloopSDK) -> None: + """Test listing multiple agents after creation.""" + source_config = { + "type": "object", + "object": { + "object_id": "obj_placeholder", + }, + } + + # Create multiple agents + agent1 = sdk_client.agent.create(name=unique_name("sdk-agent-list-1"), source=source_config) + agent2 = sdk_client.agent.create(name=unique_name("sdk-agent-list-2"), source=source_config) + agent3 = sdk_client.agent.create(name=unique_name("sdk-agent-list-3"), source=source_config) + + try: + # List agents + agents = sdk_client.agent.list(limit=100) + + assert isinstance(agents, list) + assert len(agents) >= 3 + + # Verify our agents are in the list + agent_ids = [a.id for a in agents] + assert agent1.id in agent_ids + assert agent2.id in agent_ids + assert agent3.id in agent_ids + finally: + pass + + +class TestAgentCreationVariations: + """Test different agent creation scenarios.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_agent_with_is_public_flag(self, sdk_client: RunloopSDK) -> None: + """Test creating an agent with is_public flag.""" + name = unique_name("sdk-agent-public") + + # Create a public agent + agent = sdk_client.agent.create( + name=name, + is_public=True, + source={ + "type": "object", + "object": { + "object_id": "obj_placeholder", + }, + }, + ) + + try: + assert agent.id is not None + info = agent.get_info() + assert info.name == name + assert info.is_public is True + finally: + pass + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_agent_with_source_npm(self, sdk_client: RunloopSDK) -> None: + """Test creating an agent with npm source.""" + name = unique_name("sdk-agent-npm") + + agent = sdk_client.agent.create( + name=name, + source={ + "type": "npm", + "npm": { + "package_name": "@runloop/example-agent", + }, + }, + ) + + try: + assert agent.id is not None + info = agent.get_info() + assert info.name == name + assert info.source is not None + assert info.source.type == "npm" + finally: + pass + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_agent_with_source_git(self, sdk_client: RunloopSDK) -> None: + """Test creating an agent with git source.""" + name = unique_name("sdk-agent-git") + + agent = sdk_client.agent.create( + name=name, + source={ + "type": "git", + "git": { + "repository": "https://github.com/runloop/example-agent", + "ref": "main", + }, + }, + ) + + try: + assert agent.id is not None + info = agent.get_info() + assert info.name == name + assert info.source is not None + assert info.source.type == "git" + finally: + pass diff --git a/tests/smoketests/sdk/test_agent.py~ b/tests/smoketests/sdk/test_agent.py~ new file mode 100644 index 000000000..bd1e90e3f --- /dev/null +++ b/tests/smoketests/sdk/test_agent.py~ @@ -0,0 +1,195 @@ +"""Synchronous SDK smoke tests for Agent operations.""" + +from __future__ import annotations + +import pytest + +from runloop_api_client.sdk import RunloopSDK +from tests.smoketests.utils import unique_name + +pytestmark = [pytest.mark.smoketest] + +THIRTY_SECOND_TIMEOUT = 30 +TWO_MINUTE_TIMEOUT = 120 + + +class TestAgentLifecycle: + """Test basic agent lifecycle operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_agent_create_basic(self, sdk_client: RunloopSDK) -> None: + """Test creating a basic agent.""" + name = unique_name("sdk-agent-basic") + agent = sdk_client.agent.create(name=name) + + try: + assert agent is not None + assert agent.id is not None + assert len(agent.id) > 0 + + # Verify agent information + info = agent.get_info() + assert info.id == agent.id + assert info.name == name + finally: + # Agents don't have a delete method, they're managed by the API + pass + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_agent_create_with_metadata(self, sdk_client: RunloopSDK) -> None: + """Test creating an agent with metadata.""" + name = unique_name("sdk-agent-metadata") + metadata = { + "purpose": "sdk-testing", + "version": "1.0", + } + + agent = sdk_client.agent.create(name=name, metadata=metadata) + + try: + assert agent.id is not None + + # Verify metadata is preserved + info = agent.get_info() + assert info.name == name + # Note: Metadata handling may vary based on API implementation + finally: + pass + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_agent_get_info(self, sdk_client: RunloopSDK) -> None: + """Test retrieving agent information.""" + name = unique_name("sdk-agent-info") + agent = sdk_client.agent.create(name=name) + + try: + info = agent.get_info() + + assert info.id == agent.id + assert info.name == name + assert info.create_time_ms > 0 + assert isinstance(info.is_public, bool) + finally: + pass + + +class TestAgentListing: + """Test agent listing and retrieval operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_list_agents(self, sdk_client: RunloopSDK) -> None: + """Test listing agents.""" + agents = sdk_client.agent.list(limit=10) + + assert isinstance(agents, list) + # List might be empty, that's okay + assert len(agents) >= 0 + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_get_agent_by_id(self, sdk_client: RunloopSDK) -> None: + """Test retrieving agent by ID.""" + # Create an agent + created = sdk_client.agent.create(name=unique_name("sdk-agent-retrieve")) + + try: + # Retrieve it by ID + retrieved = sdk_client.agent.from_id(created.id) + assert retrieved.id == created.id + + # Verify it's the same agent + info = retrieved.get_info() + assert info.id == created.id + finally: + pass + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_list_multiple_agents(self, sdk_client: RunloopSDK) -> None: + """Test listing multiple agents after creation.""" + # Create multiple agents + agent1 = sdk_client.agent.create(name=unique_name("sdk-agent-list-1")) + agent2 = sdk_client.agent.create(name=unique_name("sdk-agent-list-2")) + agent3 = sdk_client.agent.create(name=unique_name("sdk-agent-list-3")) + + try: + # List agents + agents = sdk_client.agent.list(limit=100) + + assert isinstance(agents, list) + assert len(agents) >= 3 + + # Verify our agents are in the list + agent_ids = [a.id for a in agents] + assert agent1.id in agent_ids + assert agent2.id in agent_ids + assert agent3.id in agent_ids + finally: + pass + + +class TestAgentCreationVariations: + """Test different agent creation scenarios.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_agent_with_is_public_flag(self, sdk_client: RunloopSDK) -> None: + """Test creating an agent with is_public flag.""" + name = unique_name("sdk-agent-public") + + # Create a public agent + agent = sdk_client.agent.create(name=name, is_public=True) + + try: + assert agent.id is not None + info = agent.get_info() + assert info.name == name + assert info.is_public is True + finally: + pass + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_agent_with_source_npm(self, sdk_client: RunloopSDK) -> None: + """Test creating an agent with npm source.""" + name = unique_name("sdk-agent-npm") + + agent = sdk_client.agent.create( + name=name, + source={ + "type": "npm", + "npm": { + "package_name": "@runloop/example-agent", + }, + }, + ) + + try: + assert agent.id is not None + info = agent.get_info() + assert info.name == name + assert info.source is not None + assert info.source.type == "npm" + finally: + pass + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_agent_with_source_git(self, sdk_client: RunloopSDK) -> None: + """Test creating an agent with git source.""" + name = unique_name("sdk-agent-git") + + agent = sdk_client.agent.create( + name=name, + source={ + "type": "git", + "git": { + "repository": "https://github.com/runloop/example-agent", + "ref": "main", + }, + }, + ) + + try: + assert agent.id is not None + info = agent.get_info() + assert info.name == name + assert info.source is not None + assert info.source.type == "git" + finally: + pass diff --git a/tests/smoketests/sdk/test_async_agent.py b/tests/smoketests/sdk/test_async_agent.py new file mode 100644 index 000000000..d1e108ab0 --- /dev/null +++ b/tests/smoketests/sdk/test_async_agent.py @@ -0,0 +1,244 @@ +"""Asynchronous SDK smoke tests for Agent operations.""" + +from __future__ import annotations + +import pytest + +from runloop_api_client.sdk import AsyncRunloopSDK +from tests.smoketests.utils import unique_name + +pytestmark = [pytest.mark.smoketest, pytest.mark.asyncio] + +THIRTY_SECOND_TIMEOUT = 30 +TWO_MINUTE_TIMEOUT = 120 + + +class TestAsyncAgentLifecycle: + """Test basic async agent lifecycle operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_agent_create_basic(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test creating a basic agent.""" + name = unique_name("sdk-async-agent-basic") + agent = await async_sdk_client.agent.create( + name=name, + source={ + "type": "object", + "object": { + "object_id": "obj_placeholder", + }, + }, + ) + + try: + assert agent is not None + assert agent.id is not None + assert len(agent.id) > 0 + + # Verify agent information + info = await agent.get_info() + assert info.id == agent.id + assert info.name == name + finally: + # Agents don't have a delete method, they're managed by the API + pass + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_agent_create_with_metadata(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test creating an agent with metadata.""" + name = unique_name("sdk-async-agent-metadata") + metadata = { + "purpose": "sdk-testing", + "version": "1.0", + } + + agent = await async_sdk_client.agent.create( + name=name, + metadata=metadata, + source={ + "type": "object", + "object": { + "object_id": "obj_placeholder", + }, + }, + ) + + try: + assert agent.id is not None + + # Verify metadata is preserved + info = await agent.get_info() + assert info.name == name + # Note: Metadata handling may vary based on API implementation + finally: + pass + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_agent_get_info(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test retrieving agent information.""" + name = unique_name("sdk-async-agent-info") + agent = await async_sdk_client.agent.create( + name=name, + source={ + "type": "object", + "object": { + "object_id": "obj_placeholder", + }, + }, + ) + + try: + info = await agent.get_info() + + assert info.id == agent.id + assert info.name == name + assert info.create_time_ms > 0 + assert isinstance(info.is_public, bool) + finally: + pass + + +class TestAsyncAgentListing: + """Test async agent listing and retrieval operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_list_agents(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test listing agents.""" + agents = await async_sdk_client.agent.list(limit=10) + + assert isinstance(agents, list) + # List might be empty, that's okay + assert len(agents) >= 0 + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_get_agent_by_id(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test retrieving agent by ID.""" + # Create an agent + created = await async_sdk_client.agent.create( + name=unique_name("sdk-async-agent-retrieve"), + source={ + "type": "object", + "object": { + "object_id": "obj_placeholder", + }, + }, + ) + + try: + # Retrieve it by ID + retrieved = async_sdk_client.agent.from_id(created.id) + assert retrieved.id == created.id + + # Verify it's the same agent + info = await retrieved.get_info() + assert info.id == created.id + finally: + pass + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_list_multiple_agents(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test listing multiple agents after creation.""" + source_config = { + "type": "object", + "object": { + "object_id": "obj_placeholder", + }, + } + + # Create multiple agents + agent1 = await async_sdk_client.agent.create(name=unique_name("sdk-async-agent-list-1"), source=source_config) + agent2 = await async_sdk_client.agent.create(name=unique_name("sdk-async-agent-list-2"), source=source_config) + agent3 = await async_sdk_client.agent.create(name=unique_name("sdk-async-agent-list-3"), source=source_config) + + try: + # List agents + agents = await async_sdk_client.agent.list(limit=100) + + assert isinstance(agents, list) + assert len(agents) >= 3 + + # Verify our agents are in the list + agent_ids = [a.id for a in agents] + assert agent1.id in agent_ids + assert agent2.id in agent_ids + assert agent3.id in agent_ids + finally: + pass + + +class TestAsyncAgentCreationVariations: + """Test different async agent creation scenarios.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_agent_with_is_public_flag(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test creating an agent with is_public flag.""" + name = unique_name("sdk-async-agent-public") + + # Create a public agent + agent = await async_sdk_client.agent.create( + name=name, + is_public=True, + source={ + "type": "object", + "object": { + "object_id": "obj_placeholder", + }, + }, + ) + + try: + assert agent.id is not None + info = await agent.get_info() + assert info.name == name + assert info.is_public is True + finally: + pass + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_agent_with_source_npm(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test creating an agent with npm source.""" + name = unique_name("sdk-async-agent-npm") + + agent = await async_sdk_client.agent.create( + name=name, + source={ + "type": "npm", + "npm": { + "package_name": "@runloop/example-agent", + }, + }, + ) + + try: + assert agent.id is not None + info = await agent.get_info() + assert info.name == name + assert info.source is not None + assert info.source.type == "npm" + finally: + pass + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_agent_with_source_git(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test creating an agent with git source.""" + name = unique_name("sdk-async-agent-git") + + agent = await async_sdk_client.agent.create( + name=name, + source={ + "type": "git", + "git": { + "repository": "https://github.com/runloop/example-agent", + "ref": "main", + }, + }, + ) + + try: + assert agent.id is not None + info = await agent.get_info() + assert info.name == name + assert info.source is not None + assert info.source.type == "git" + finally: + pass From 12f7ae9dad815379a8607b75d6455c6729b13858 Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Tue, 25 Nov 2025 19:25:31 -0800 Subject: [PATCH 09/20] possible fix for bogus 404 error --- tests/smoketests/sdk/test_agent.py | 36 ++++++++++++------------ tests/smoketests/sdk/test_async_agent.py | 36 ++++++++++++------------ 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/tests/smoketests/sdk/test_agent.py b/tests/smoketests/sdk/test_agent.py index 92f5c77cd..1dc6378d4 100644 --- a/tests/smoketests/sdk/test_agent.py +++ b/tests/smoketests/sdk/test_agent.py @@ -23,9 +23,9 @@ def test_agent_create_basic(self, sdk_client: RunloopSDK) -> None: agent = sdk_client.agent.create( name=name, source={ - "type": "object", - "object": { - "object_id": "obj_placeholder", + "type": "npm", + "npm": { + "package_name": "@runloop/hello-world-agent", }, }, ) @@ -56,9 +56,9 @@ def test_agent_create_with_metadata(self, sdk_client: RunloopSDK) -> None: name=name, metadata=metadata, source={ - "type": "object", - "object": { - "object_id": "obj_placeholder", + "type": "npm", + "npm": { + "package_name": "@runloop/hello-world-agent", }, }, ) @@ -80,9 +80,9 @@ def test_agent_get_info(self, sdk_client: RunloopSDK) -> None: agent = sdk_client.agent.create( name=name, source={ - "type": "object", - "object": { - "object_id": "obj_placeholder", + "type": "npm", + "npm": { + "package_name": "@runloop/hello-world-agent", }, }, ) @@ -117,9 +117,9 @@ def test_get_agent_by_id(self, sdk_client: RunloopSDK) -> None: created = sdk_client.agent.create( name=unique_name("sdk-agent-retrieve"), source={ - "type": "object", - "object": { - "object_id": "obj_placeholder", + "type": "npm", + "npm": { + "package_name": "@runloop/hello-world-agent", }, }, ) @@ -139,9 +139,9 @@ def test_get_agent_by_id(self, sdk_client: RunloopSDK) -> None: def test_list_multiple_agents(self, sdk_client: RunloopSDK) -> None: """Test listing multiple agents after creation.""" source_config = { - "type": "object", - "object": { - "object_id": "obj_placeholder", + "type": "npm", + "npm": { + "package_name": "@runloop/hello-world-agent", }, } @@ -179,9 +179,9 @@ def test_agent_with_is_public_flag(self, sdk_client: RunloopSDK) -> None: name=name, is_public=True, source={ - "type": "object", - "object": { - "object_id": "obj_placeholder", + "type": "npm", + "npm": { + "package_name": "@runloop/hello-world-agent", }, }, ) diff --git a/tests/smoketests/sdk/test_async_agent.py b/tests/smoketests/sdk/test_async_agent.py index d1e108ab0..d086dcb7f 100644 --- a/tests/smoketests/sdk/test_async_agent.py +++ b/tests/smoketests/sdk/test_async_agent.py @@ -23,9 +23,9 @@ async def test_agent_create_basic(self, async_sdk_client: AsyncRunloopSDK) -> No agent = await async_sdk_client.agent.create( name=name, source={ - "type": "object", - "object": { - "object_id": "obj_placeholder", + "type": "npm", + "npm": { + "package_name": "@runloop/hello-world-agent", }, }, ) @@ -56,9 +56,9 @@ async def test_agent_create_with_metadata(self, async_sdk_client: AsyncRunloopSD name=name, metadata=metadata, source={ - "type": "object", - "object": { - "object_id": "obj_placeholder", + "type": "npm", + "npm": { + "package_name": "@runloop/hello-world-agent", }, }, ) @@ -80,9 +80,9 @@ async def test_agent_get_info(self, async_sdk_client: AsyncRunloopSDK) -> None: agent = await async_sdk_client.agent.create( name=name, source={ - "type": "object", - "object": { - "object_id": "obj_placeholder", + "type": "npm", + "npm": { + "package_name": "@runloop/hello-world-agent", }, }, ) @@ -117,9 +117,9 @@ async def test_get_agent_by_id(self, async_sdk_client: AsyncRunloopSDK) -> None: created = await async_sdk_client.agent.create( name=unique_name("sdk-async-agent-retrieve"), source={ - "type": "object", - "object": { - "object_id": "obj_placeholder", + "type": "npm", + "npm": { + "package_name": "@runloop/hello-world-agent", }, }, ) @@ -139,9 +139,9 @@ async def test_get_agent_by_id(self, async_sdk_client: AsyncRunloopSDK) -> None: async def test_list_multiple_agents(self, async_sdk_client: AsyncRunloopSDK) -> None: """Test listing multiple agents after creation.""" source_config = { - "type": "object", - "object": { - "object_id": "obj_placeholder", + "type": "npm", + "npm": { + "package_name": "@runloop/hello-world-agent", }, } @@ -179,9 +179,9 @@ async def test_agent_with_is_public_flag(self, async_sdk_client: AsyncRunloopSDK name=name, is_public=True, source={ - "type": "object", - "object": { - "object_id": "obj_placeholder", + "type": "npm", + "npm": { + "package_name": "@runloop/hello-world-agent", }, }, ) From 9660656b1c25ca611ad2b829af71c858469b693a Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Wed, 26 Nov 2025 10:28:50 -0800 Subject: [PATCH 10/20] remove spurious tests; add TODO about deleting agents --- tests/smoketests/sdk/test_agent.py | 65 ++++-------------------- tests/smoketests/sdk/test_async_agent.py | 65 ++++-------------------- 2 files changed, 18 insertions(+), 112 deletions(-) diff --git a/tests/smoketests/sdk/test_agent.py b/tests/smoketests/sdk/test_agent.py index 1dc6378d4..247b017ea 100644 --- a/tests/smoketests/sdk/test_agent.py +++ b/tests/smoketests/sdk/test_agent.py @@ -40,37 +40,9 @@ def test_agent_create_basic(self, sdk_client: RunloopSDK) -> None: assert info.id == agent.id assert info.name == name finally: - # Agents don't have a delete method, they're managed by the API - pass - - @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) - def test_agent_create_with_metadata(self, sdk_client: RunloopSDK) -> None: - """Test creating an agent with metadata.""" - name = unique_name("sdk-agent-metadata") - metadata = { - "purpose": "sdk-testing", - "version": "1.0", - } - - agent = sdk_client.agent.create( - name=name, - metadata=metadata, - source={ - "type": "npm", - "npm": { - "package_name": "@runloop/hello-world-agent", - }, - }, - ) - - try: - assert agent.id is not None - - # Verify metadata is preserved - info = agent.get_info() - assert info.name == name - # Note: Metadata handling may vary based on API implementation - finally: + # TODO: Add agent cleanup once delete endpoint is implemented + # Currently agents don't have a delete method - they persist after tests + # Once implemented, add: agent.delete() pass @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) @@ -95,6 +67,7 @@ def test_agent_get_info(self, sdk_client: RunloopSDK) -> None: assert info.create_time_ms > 0 assert isinstance(info.is_public, bool) finally: + # TODO: Add agent cleanup once delete endpoint is implemented pass @@ -133,6 +106,7 @@ def test_get_agent_by_id(self, sdk_client: RunloopSDK) -> None: info = retrieved.get_info() assert info.id == created.id finally: + # TODO: Add agent cleanup once delete endpoint is implemented pass @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) @@ -163,37 +137,14 @@ def test_list_multiple_agents(self, sdk_client: RunloopSDK) -> None: assert agent2.id in agent_ids assert agent3.id in agent_ids finally: + # TODO: Add agent cleanup once delete endpoint is implemented + # Should delete: agent1, agent2, agent3 pass class TestAgentCreationVariations: """Test different agent creation scenarios.""" - @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) - def test_agent_with_is_public_flag(self, sdk_client: RunloopSDK) -> None: - """Test creating an agent with is_public flag.""" - name = unique_name("sdk-agent-public") - - # Create a public agent - agent = sdk_client.agent.create( - name=name, - is_public=True, - source={ - "type": "npm", - "npm": { - "package_name": "@runloop/hello-world-agent", - }, - }, - ) - - try: - assert agent.id is not None - info = agent.get_info() - assert info.name == name - assert info.is_public is True - finally: - pass - @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) def test_agent_with_source_npm(self, sdk_client: RunloopSDK) -> None: """Test creating an agent with npm source.""" @@ -216,6 +167,7 @@ def test_agent_with_source_npm(self, sdk_client: RunloopSDK) -> None: assert info.source is not None assert info.source.type == "npm" finally: + # TODO: Add agent cleanup once delete endpoint is implemented pass @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) @@ -241,4 +193,5 @@ def test_agent_with_source_git(self, sdk_client: RunloopSDK) -> None: assert info.source is not None assert info.source.type == "git" finally: + # TODO: Add agent cleanup once delete endpoint is implemented pass diff --git a/tests/smoketests/sdk/test_async_agent.py b/tests/smoketests/sdk/test_async_agent.py index d086dcb7f..1a0363c06 100644 --- a/tests/smoketests/sdk/test_async_agent.py +++ b/tests/smoketests/sdk/test_async_agent.py @@ -40,37 +40,9 @@ async def test_agent_create_basic(self, async_sdk_client: AsyncRunloopSDK) -> No assert info.id == agent.id assert info.name == name finally: - # Agents don't have a delete method, they're managed by the API - pass - - @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) - async def test_agent_create_with_metadata(self, async_sdk_client: AsyncRunloopSDK) -> None: - """Test creating an agent with metadata.""" - name = unique_name("sdk-async-agent-metadata") - metadata = { - "purpose": "sdk-testing", - "version": "1.0", - } - - agent = await async_sdk_client.agent.create( - name=name, - metadata=metadata, - source={ - "type": "npm", - "npm": { - "package_name": "@runloop/hello-world-agent", - }, - }, - ) - - try: - assert agent.id is not None - - # Verify metadata is preserved - info = await agent.get_info() - assert info.name == name - # Note: Metadata handling may vary based on API implementation - finally: + # TODO: Add agent cleanup once delete endpoint is implemented + # Currently agents don't have a delete method - they persist after tests + # Once implemented, add: await agent.delete() pass @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) @@ -95,6 +67,7 @@ async def test_agent_get_info(self, async_sdk_client: AsyncRunloopSDK) -> None: assert info.create_time_ms > 0 assert isinstance(info.is_public, bool) finally: + # TODO: Add agent cleanup once delete endpoint is implemented pass @@ -133,6 +106,7 @@ async def test_get_agent_by_id(self, async_sdk_client: AsyncRunloopSDK) -> None: info = await retrieved.get_info() assert info.id == created.id finally: + # TODO: Add agent cleanup once delete endpoint is implemented pass @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) @@ -163,37 +137,14 @@ async def test_list_multiple_agents(self, async_sdk_client: AsyncRunloopSDK) -> assert agent2.id in agent_ids assert agent3.id in agent_ids finally: + # TODO: Add agent cleanup once delete endpoint is implemented + # Should delete: agent1, agent2, agent3 pass class TestAsyncAgentCreationVariations: """Test different async agent creation scenarios.""" - @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) - async def test_agent_with_is_public_flag(self, async_sdk_client: AsyncRunloopSDK) -> None: - """Test creating an agent with is_public flag.""" - name = unique_name("sdk-async-agent-public") - - # Create a public agent - agent = await async_sdk_client.agent.create( - name=name, - is_public=True, - source={ - "type": "npm", - "npm": { - "package_name": "@runloop/hello-world-agent", - }, - }, - ) - - try: - assert agent.id is not None - info = await agent.get_info() - assert info.name == name - assert info.is_public is True - finally: - pass - @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) async def test_agent_with_source_npm(self, async_sdk_client: AsyncRunloopSDK) -> None: """Test creating an agent with npm source.""" @@ -216,6 +167,7 @@ async def test_agent_with_source_npm(self, async_sdk_client: AsyncRunloopSDK) -> assert info.source is not None assert info.source.type == "npm" finally: + # TODO: Add agent cleanup once delete endpoint is implemented pass @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) @@ -241,4 +193,5 @@ async def test_agent_with_source_git(self, async_sdk_client: AsyncRunloopSDK) -> assert info.source is not None assert info.source.type == "git" finally: + # TODO: Add agent cleanup once delete endpoint is implemented pass From 72267bcaaa20d6dfa92cb37e3a6cd63ea3ab2da7 Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Wed, 26 Nov 2025 15:11:37 -0800 Subject: [PATCH 11/20] include 'test' in test object names --- tests/smoketests/sdk/test_agent.py | 16 ++++++++-------- tests/smoketests/sdk/test_async_agent.py | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/smoketests/sdk/test_agent.py b/tests/smoketests/sdk/test_agent.py index 247b017ea..b6df4af20 100644 --- a/tests/smoketests/sdk/test_agent.py +++ b/tests/smoketests/sdk/test_agent.py @@ -19,7 +19,7 @@ class TestAgentLifecycle: @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) def test_agent_create_basic(self, sdk_client: RunloopSDK) -> None: """Test creating a basic agent.""" - name = unique_name("sdk-agent-basic") + name = unique_name("sdk-agent-test-basic") agent = sdk_client.agent.create( name=name, source={ @@ -48,7 +48,7 @@ def test_agent_create_basic(self, sdk_client: RunloopSDK) -> None: @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) def test_agent_get_info(self, sdk_client: RunloopSDK) -> None: """Test retrieving agent information.""" - name = unique_name("sdk-agent-info") + name = unique_name("sdk-agent-test-info") agent = sdk_client.agent.create( name=name, source={ @@ -88,7 +88,7 @@ def test_get_agent_by_id(self, sdk_client: RunloopSDK) -> None: """Test retrieving agent by ID.""" # Create an agent created = sdk_client.agent.create( - name=unique_name("sdk-agent-retrieve"), + name=unique_name("sdk-agent-test-retrieve"), source={ "type": "npm", "npm": { @@ -120,9 +120,9 @@ def test_list_multiple_agents(self, sdk_client: RunloopSDK) -> None: } # Create multiple agents - agent1 = sdk_client.agent.create(name=unique_name("sdk-agent-list-1"), source=source_config) - agent2 = sdk_client.agent.create(name=unique_name("sdk-agent-list-2"), source=source_config) - agent3 = sdk_client.agent.create(name=unique_name("sdk-agent-list-3"), source=source_config) + agent1 = sdk_client.agent.create(name=unique_name("sdk-agent-test-list-1"), source=source_config) + agent2 = sdk_client.agent.create(name=unique_name("sdk-agent-test-list-2"), source=source_config) + agent3 = sdk_client.agent.create(name=unique_name("sdk-agent-test-list-3"), source=source_config) try: # List agents @@ -148,7 +148,7 @@ class TestAgentCreationVariations: @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) def test_agent_with_source_npm(self, sdk_client: RunloopSDK) -> None: """Test creating an agent with npm source.""" - name = unique_name("sdk-agent-npm") + name = unique_name("sdk-agent-test-npm") agent = sdk_client.agent.create( name=name, @@ -173,7 +173,7 @@ def test_agent_with_source_npm(self, sdk_client: RunloopSDK) -> None: @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) def test_agent_with_source_git(self, sdk_client: RunloopSDK) -> None: """Test creating an agent with git source.""" - name = unique_name("sdk-agent-git") + name = unique_name("sdk-agent-test-git") agent = sdk_client.agent.create( name=name, diff --git a/tests/smoketests/sdk/test_async_agent.py b/tests/smoketests/sdk/test_async_agent.py index 1a0363c06..255f54d05 100644 --- a/tests/smoketests/sdk/test_async_agent.py +++ b/tests/smoketests/sdk/test_async_agent.py @@ -19,7 +19,7 @@ class TestAsyncAgentLifecycle: @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) async def test_agent_create_basic(self, async_sdk_client: AsyncRunloopSDK) -> None: """Test creating a basic agent.""" - name = unique_name("sdk-async-agent-basic") + name = unique_name("sdk-async-agent-test-basic") agent = await async_sdk_client.agent.create( name=name, source={ @@ -48,7 +48,7 @@ async def test_agent_create_basic(self, async_sdk_client: AsyncRunloopSDK) -> No @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) async def test_agent_get_info(self, async_sdk_client: AsyncRunloopSDK) -> None: """Test retrieving agent information.""" - name = unique_name("sdk-async-agent-info") + name = unique_name("sdk-async-agent-test-info") agent = await async_sdk_client.agent.create( name=name, source={ @@ -88,7 +88,7 @@ async def test_get_agent_by_id(self, async_sdk_client: AsyncRunloopSDK) -> None: """Test retrieving agent by ID.""" # Create an agent created = await async_sdk_client.agent.create( - name=unique_name("sdk-async-agent-retrieve"), + name=unique_name("sdk-async-agent-test-retrieve"), source={ "type": "npm", "npm": { @@ -120,9 +120,9 @@ async def test_list_multiple_agents(self, async_sdk_client: AsyncRunloopSDK) -> } # Create multiple agents - agent1 = await async_sdk_client.agent.create(name=unique_name("sdk-async-agent-list-1"), source=source_config) - agent2 = await async_sdk_client.agent.create(name=unique_name("sdk-async-agent-list-2"), source=source_config) - agent3 = await async_sdk_client.agent.create(name=unique_name("sdk-async-agent-list-3"), source=source_config) + agent1 = await async_sdk_client.agent.create(name=unique_name("sdk-async-agent-test-list-1"), source=source_config) + agent2 = await async_sdk_client.agent.create(name=unique_name("sdk-async-agent-test-list-2"), source=source_config) + agent3 = await async_sdk_client.agent.create(name=unique_name("sdk-async-agent-test-list-3"), source=source_config) try: # List agents @@ -148,7 +148,7 @@ class TestAsyncAgentCreationVariations: @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) async def test_agent_with_source_npm(self, async_sdk_client: AsyncRunloopSDK) -> None: """Test creating an agent with npm source.""" - name = unique_name("sdk-async-agent-npm") + name = unique_name("sdk-async-agent-test-npm") agent = await async_sdk_client.agent.create( name=name, @@ -173,7 +173,7 @@ async def test_agent_with_source_npm(self, async_sdk_client: AsyncRunloopSDK) -> @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) async def test_agent_with_source_git(self, async_sdk_client: AsyncRunloopSDK) -> None: """Test creating an agent with git source.""" - name = unique_name("sdk-async-agent-git") + name = unique_name("sdk-async-agent-test-git") agent = await async_sdk_client.agent.create( name=name, From 90ecd1efc982beedd21acaddae3fc5b647203f42 Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Wed, 26 Nov 2025 16:44:08 -0800 Subject: [PATCH 12/20] add create_from_* functions to simplify calling conventions for agent creation --- src/runloop_api_client/sdk/sync.py | 145 ++++++++++++++++++++ tests/sdk/test_ops.py | 207 +++++++++++++++++++++++++++++ 2 files changed, 352 insertions(+) diff --git a/src/runloop_api_client/sdk/sync.py b/src/runloop_api_client/sdk/sync.py index 38861b1d5..fb214c000 100644 --- a/src/runloop_api_client/sdk/sync.py +++ b/src/runloop_api_client/sdk/sync.py @@ -576,6 +576,151 @@ def create( ) return Agent(self._client, agent_view.id, agent_view) + def create_from_npm( + self, + *, + package_name: str, + npm_version: Optional[str] = None, + registry_url: Optional[str] = None, + agent_setup: Optional[list[str]] = None, + **params: Unpack[SDKAgentCreateParams], + ) -> Agent: + """Create an agent from an NPM package. + + :param package_name: NPM package name + :type package_name: str + :param npm_version: NPM version constraint, defaults to None + :type npm_version: Optional[str], optional + :param registry_url: NPM registry URL, defaults to None + :type registry_url: Optional[str], optional + :param agent_setup: Setup commands to run after installation, defaults to None + :type agent_setup: Optional[list[str]], optional + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for additional parameters (excluding 'source') + :return: Wrapper bound to the newly created agent + :rtype: Agent + :raises ValueError: If 'source' is provided in params + """ + if "source" in params: + raise ValueError("Cannot specify 'source' when using create_from_npm(); source is automatically set to npm configuration") + + npm_config: dict = {"package_name": package_name} + if npm_version is not None: + npm_config["npm_version"] = npm_version + if registry_url is not None: + npm_config["registry_url"] = registry_url + if agent_setup is not None: + npm_config["agent_setup"] = agent_setup + + return self.create( + source={"type": "npm", "npm": npm_config}, + **params, + ) + + def create_from_pip( + self, + *, + package_name: str, + pip_version: Optional[str] = None, + registry_url: Optional[str] = None, + agent_setup: Optional[list[str]] = None, + **params: Unpack[SDKAgentCreateParams], + ) -> Agent: + """Create an agent from a Pip package. + + :param package_name: Pip package name + :type package_name: str + :param pip_version: Pip version constraint, defaults to None + :type pip_version: Optional[str], optional + :param registry_url: Pip registry URL, defaults to None + :type registry_url: Optional[str], optional + :param agent_setup: Setup commands to run after installation, defaults to None + :type agent_setup: Optional[list[str]], optional + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for additional parameters (excluding 'source') + :return: Wrapper bound to the newly created agent + :rtype: Agent + :raises ValueError: If 'source' is provided in params + """ + if "source" in params: + raise ValueError("Cannot specify 'source' when using create_from_pip(); source is automatically set to pip configuration") + + pip_config: dict = {"package_name": package_name} + if pip_version is not None: + pip_config["pip_version"] = pip_version + if registry_url is not None: + pip_config["registry_url"] = registry_url + if agent_setup is not None: + pip_config["agent_setup"] = agent_setup + + return self.create( + source={"type": "pip", "pip": pip_config}, + **params, + ) + + def create_from_git( + self, + *, + repository: str, + ref: Optional[str] = None, + agent_setup: Optional[list[str]] = None, + **params: Unpack[SDKAgentCreateParams], + ) -> Agent: + """Create an agent from a Git repository. + + :param repository: Git repository URL + :type repository: str + :param ref: Optional Git ref (branch/tag/commit), defaults to main/HEAD + :type ref: Optional[str], optional + :param agent_setup: Setup commands to run after cloning, defaults to None + :type agent_setup: Optional[list[str]], optional + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for additional parameters (excluding 'source') + :return: Wrapper bound to the newly created agent + :rtype: Agent + :raises ValueError: If 'source' is provided in params + """ + if "source" in params: + raise ValueError("Cannot specify 'source' when using create_from_git(); source is automatically set to git configuration") + + git_config: dict = {"repository": repository} + if ref is not None: + git_config["ref"] = ref + if agent_setup is not None: + git_config["agent_setup"] = agent_setup + + return self.create( + source={"type": "git", "git": git_config}, + **params, + ) + + def create_from_object( + self, + *, + object_id: str, + agent_setup: Optional[list[str]] = None, + **params: Unpack[SDKAgentCreateParams], + ) -> Agent: + """Create an agent from a storage object. + + :param object_id: Storage object ID + :type object_id: str + :param agent_setup: Setup commands to run after unpacking, defaults to None + :type agent_setup: Optional[list[str]], optional + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for additional parameters (excluding 'source') + :return: Wrapper bound to the newly created agent + :rtype: Agent + :raises ValueError: If 'source' is provided in params + """ + if "source" in params: + raise ValueError("Cannot specify 'source' when using create_from_object(); source is automatically set to object configuration") + + object_config: dict = {"object_id": object_id} + if agent_setup is not None: + object_config["agent_setup"] = agent_setup + + return self.create( + source={"type": "object", "object": object_config}, + **params, + ) + def from_id(self, agent_id: str) -> Agent: """Attach to an existing agent by ID. diff --git a/tests/sdk/test_ops.py b/tests/sdk/test_ops.py index 99365c270..53b8bed52 100644 --- a/tests/sdk/test_ops.py +++ b/tests/sdk/test_ops.py @@ -752,6 +752,213 @@ def test_list(self, mock_client: Mock) -> None: mock_client.agents.list.assert_called_once() + def test_create_from_npm(self, mock_client: Mock, agent_view: MockAgentView) -> None: + """Test create_from_npm factory method.""" + mock_client.agents.create.return_value = agent_view + + client = AgentOps(mock_client) + agent = client.create_from_npm( + name="test-agent", + package_name="@runloop/example-agent", + ) + + assert isinstance(agent, Agent) + assert agent.id == "agent_123" + mock_client.agents.create.assert_called_once_with( + source={ + "type": "npm", + "npm": { + "package_name": "@runloop/example-agent", + }, + }, + name="test-agent", + ) + + def test_create_from_npm_with_all_options(self, mock_client: Mock, agent_view: MockAgentView) -> None: + """Test create_from_npm factory method with all optional parameters.""" + mock_client.agents.create.return_value = agent_view + + client = AgentOps(mock_client) + agent = client.create_from_npm( + package_name="@runloop/example-agent", + npm_version="1.2.3", + registry_url="https://registry.example.com", + agent_setup=["npm install", "npm run setup"], + name="test-agent", + extra_headers={"X-Custom": "header"}, + ) + + assert isinstance(agent, Agent) + assert agent.id == "agent_123" + mock_client.agents.create.assert_called_once_with( + source={ + "type": "npm", + "npm": { + "package_name": "@runloop/example-agent", + "npm_version": "1.2.3", + "registry_url": "https://registry.example.com", + "agent_setup": ["npm install", "npm run setup"], + }, + }, + name="test-agent", + extra_headers={"X-Custom": "header"}, + ) + + def test_create_from_npm_raises_when_source_provided(self, mock_client: Mock) -> None: + """Test create_from_npm raises ValueError when source is provided in params.""" + client = AgentOps(mock_client) + + with pytest.raises(ValueError, match="Cannot specify 'source' when using create_from_npm"): + client.create_from_npm( + package_name="@runloop/example-agent", + name="test-agent", + source={"type": "git", "git": {"repository": "https://github.com/example/repo"}}, + ) + + def test_create_from_pip(self, mock_client: Mock, agent_view: MockAgentView) -> None: + """Test create_from_pip factory method.""" + mock_client.agents.create.return_value = agent_view + + client = AgentOps(mock_client) + agent = client.create_from_pip( + package_name="runloop-example-agent", + name="test-agent", + ) + + assert isinstance(agent, Agent) + assert agent.id == "agent_123" + mock_client.agents.create.assert_called_once_with( + source={ + "type": "pip", + "pip": { + "package_name": "runloop-example-agent", + }, + }, + name="test-agent", + ) + + def test_create_from_pip_with_all_options(self, mock_client: Mock, agent_view: MockAgentView) -> None: + """Test create_from_pip factory method with all optional parameters.""" + mock_client.agents.create.return_value = agent_view + + client = AgentOps(mock_client) + agent = client.create_from_pip( + package_name="runloop-example-agent", + pip_version="1.2.3", + registry_url="https://pypi.example.com", + agent_setup=["pip install extra-deps"], + name="test-agent", + ) + + assert isinstance(agent, Agent) + assert agent.id == "agent_123" + mock_client.agents.create.assert_called_once_with( + source={ + "type": "pip", + "pip": { + "package_name": "runloop-example-agent", + "pip_version": "1.2.3", + "registry_url": "https://pypi.example.com", + "agent_setup": ["pip install extra-deps"], + }, + }, + name="test-agent", + ) + + def test_create_from_git(self, mock_client: Mock, agent_view: MockAgentView) -> None: + """Test create_from_git factory method.""" + mock_client.agents.create.return_value = agent_view + + client = AgentOps(mock_client) + agent = client.create_from_git( + repository="https://github.com/example/agent-repo", + name="test-agent", + ) + + assert isinstance(agent, Agent) + assert agent.id == "agent_123" + mock_client.agents.create.assert_called_once_with( + source={ + "type": "git", + "git": { + "repository": "https://github.com/example/agent-repo", + }, + }, + name="test-agent", + ) + + def test_create_from_git_with_all_options(self, mock_client: Mock, agent_view: MockAgentView) -> None: + """Test create_from_git factory method with all optional parameters.""" + mock_client.agents.create.return_value = agent_view + + client = AgentOps(mock_client) + agent = client.create_from_git( + repository="https://github.com/example/agent-repo", + ref="develop", + agent_setup=["npm install", "npm run build"], + name="test-agent", + ) + + assert isinstance(agent, Agent) + assert agent.id == "agent_123" + mock_client.agents.create.assert_called_once_with( + source={ + "type": "git", + "git": { + "repository": "https://github.com/example/agent-repo", + "ref": "develop", + "agent_setup": ["npm install", "npm run build"], + }, + }, + name="test-agent", + ) + + def test_create_from_object(self, mock_client: Mock, agent_view: MockAgentView) -> None: + """Test create_from_object factory method.""" + mock_client.agents.create.return_value = agent_view + + client = AgentOps(mock_client) + agent = client.create_from_object( + object_id="obj_123", + name="test-agent", + ) + + assert isinstance(agent, Agent) + assert agent.id == "agent_123" + mock_client.agents.create.assert_called_once_with( + source={ + "type": "object", + "object": { + "object_id": "obj_123", + }, + }, + name="test-agent", + ) + + def test_create_from_object_with_agent_setup(self, mock_client: Mock, agent_view: MockAgentView) -> None: + """Test create_from_object factory method with agent_setup.""" + mock_client.agents.create.return_value = agent_view + + client = AgentOps(mock_client) + agent = client.create_from_object( + object_id="obj_123", + agent_setup=["chmod +x setup.sh", "./setup.sh"], + name="test-agent", + ) + + assert isinstance(agent, Agent) + assert agent.id == "agent_123" + mock_client.agents.create.assert_called_once_with( + source={ + "type": "object", + "object": { + "object_id": "obj_123", + "agent_setup": ["chmod +x setup.sh", "./setup.sh"], + }, + }, + name="test-agent", + ) + class TestRunloopSDK: """Tests for RunloopSDK class.""" From 8299c8366d74896a49820ccab870b98c2f9cdc21 Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Wed, 26 Nov 2025 16:54:57 -0800 Subject: [PATCH 13/20] add async create_from_* funcs --- src/runloop_api_client/sdk/async_.py | 145 +++++++++++++++++ tests/sdk/test_async_ops.py | 224 +++++++++++++++++++++++++++ 2 files changed, 369 insertions(+) diff --git a/src/runloop_api_client/sdk/async_.py b/src/runloop_api_client/sdk/async_.py index bedcae47a..546211c18 100644 --- a/src/runloop_api_client/sdk/async_.py +++ b/src/runloop_api_client/sdk/async_.py @@ -580,6 +580,151 @@ async def create( ) return AsyncAgent(self._client, agent_view.id, agent_view) + async def create_from_npm( + self, + *, + package_name: str, + npm_version: Optional[str] = None, + registry_url: Optional[str] = None, + agent_setup: Optional[list[str]] = None, + **params: Unpack[SDKAgentCreateParams], + ) -> AsyncAgent: + """Create an agent from an NPM package. + + :param package_name: NPM package name + :type package_name: str + :param npm_version: NPM version constraint, defaults to None + :type npm_version: Optional[str], optional + :param registry_url: NPM registry URL, defaults to None + :type registry_url: Optional[str], optional + :param agent_setup: Setup commands to run after installation, defaults to None + :type agent_setup: Optional[list[str]], optional + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for additional parameters (excluding 'source') + :return: Wrapper bound to the newly created agent + :rtype: AsyncAgent + :raises ValueError: If 'source' is provided in params + """ + if "source" in params: + raise ValueError("Cannot specify 'source' when using create_from_npm(); source is automatically set to npm configuration") + + npm_config: dict = {"package_name": package_name} + if npm_version is not None: + npm_config["npm_version"] = npm_version + if registry_url is not None: + npm_config["registry_url"] = registry_url + if agent_setup is not None: + npm_config["agent_setup"] = agent_setup + + return await self.create( + source={"type": "npm", "npm": npm_config}, + **params, + ) + + async def create_from_pip( + self, + *, + package_name: str, + pip_version: Optional[str] = None, + registry_url: Optional[str] = None, + agent_setup: Optional[list[str]] = None, + **params: Unpack[SDKAgentCreateParams], + ) -> AsyncAgent: + """Create an agent from a Pip package. + + :param package_name: Pip package name + :type package_name: str + :param pip_version: Pip version constraint, defaults to None + :type pip_version: Optional[str], optional + :param registry_url: Pip registry URL, defaults to None + :type registry_url: Optional[str], optional + :param agent_setup: Setup commands to run after installation, defaults to None + :type agent_setup: Optional[list[str]], optional + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for additional parameters (excluding 'source') + :return: Wrapper bound to the newly created agent + :rtype: AsyncAgent + :raises ValueError: If 'source' is provided in params + """ + if "source" in params: + raise ValueError("Cannot specify 'source' when using create_from_pip(); source is automatically set to pip configuration") + + pip_config: dict = {"package_name": package_name} + if pip_version is not None: + pip_config["pip_version"] = pip_version + if registry_url is not None: + pip_config["registry_url"] = registry_url + if agent_setup is not None: + pip_config["agent_setup"] = agent_setup + + return await self.create( + source={"type": "pip", "pip": pip_config}, + **params, + ) + + async def create_from_git( + self, + *, + repository: str, + ref: Optional[str] = None, + agent_setup: Optional[list[str]] = None, + **params: Unpack[SDKAgentCreateParams], + ) -> AsyncAgent: + """Create an agent from a Git repository. + + :param repository: Git repository URL + :type repository: str + :param ref: Optional Git ref (branch/tag/commit), defaults to main/HEAD + :type ref: Optional[str], optional + :param agent_setup: Setup commands to run after cloning, defaults to None + :type agent_setup: Optional[list[str]], optional + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for additional parameters (excluding 'source') + :return: Wrapper bound to the newly created agent + :rtype: AsyncAgent + :raises ValueError: If 'source' is provided in params + """ + if "source" in params: + raise ValueError("Cannot specify 'source' when using create_from_git(); source is automatically set to git configuration") + + git_config: dict = {"repository": repository} + if ref is not None: + git_config["ref"] = ref + if agent_setup is not None: + git_config["agent_setup"] = agent_setup + + return await self.create( + source={"type": "git", "git": git_config}, + **params, + ) + + async def create_from_object( + self, + *, + object_id: str, + agent_setup: Optional[list[str]] = None, + **params: Unpack[SDKAgentCreateParams], + ) -> AsyncAgent: + """Create an agent from a storage object. + + :param object_id: Storage object ID + :type object_id: str + :param agent_setup: Setup commands to run after unpacking, defaults to None + :type agent_setup: Optional[list[str]], optional + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKAgentCreateParams` for additional parameters (excluding 'source') + :return: Wrapper bound to the newly created agent + :rtype: AsyncAgent + :raises ValueError: If 'source' is provided in params + """ + if "source" in params: + raise ValueError("Cannot specify 'source' when using create_from_object(); source is automatically set to object configuration") + + object_config: dict = {"object_id": object_id} + if agent_setup is not None: + object_config["agent_setup"] = agent_setup + + return await self.create( + source={"type": "object", "object": object_config}, + **params, + ) + def from_id(self, agent_id: str) -> AsyncAgent: """Attach to an existing agent by ID. diff --git a/tests/sdk/test_async_ops.py b/tests/sdk/test_async_ops.py index 0e60d9dc1..fe287d861 100644 --- a/tests/sdk/test_async_ops.py +++ b/tests/sdk/test_async_ops.py @@ -835,6 +835,230 @@ async def mock_retrieve(agent_id, **kwargs): mock_async_client.agents.list.assert_called_once() + @pytest.mark.asyncio + async def test_create_from_npm(self, mock_async_client: AsyncMock, agent_view: MockAgentView) -> None: + """Test create_from_npm factory method.""" + mock_async_client.agents.create = AsyncMock(return_value=agent_view) + + client = AsyncAgentOps(mock_async_client) + agent = await client.create_from_npm( + name="test-agent", + package_name="@runloop/example-agent", + ) + + assert isinstance(agent, AsyncAgent) + assert agent.id == "agent_123" + mock_async_client.agents.create.assert_awaited_once_with( + source={ + "type": "npm", + "npm": { + "package_name": "@runloop/example-agent", + }, + }, + name="test-agent", + ) + + @pytest.mark.asyncio + async def test_create_from_npm_with_all_options( + self, mock_async_client: AsyncMock, agent_view: MockAgentView + ) -> None: + """Test create_from_npm factory method with all optional parameters.""" + mock_async_client.agents.create = AsyncMock(return_value=agent_view) + + client = AsyncAgentOps(mock_async_client) + agent = await client.create_from_npm( + name="test-agent", + package_name="@runloop/example-agent", + npm_version="1.2.3", + registry_url="https://registry.example.com", + agent_setup=["npm install", "npm run setup"], + extra_headers={"X-Custom": "header"}, + ) + + assert isinstance(agent, AsyncAgent) + assert agent.id == "agent_123" + mock_async_client.agents.create.assert_awaited_once_with( + source={ + "type": "npm", + "npm": { + "package_name": "@runloop/example-agent", + "npm_version": "1.2.3", + "registry_url": "https://registry.example.com", + "agent_setup": ["npm install", "npm run setup"], + }, + }, + name="test-agent", + extra_headers={"X-Custom": "header"}, + ) + + @pytest.mark.asyncio + async def test_create_from_npm_raises_when_source_provided(self, mock_async_client: AsyncMock) -> None: + """Test create_from_npm raises ValueError when source is provided in params.""" + client = AsyncAgentOps(mock_async_client) + + with pytest.raises(ValueError, match="Cannot specify 'source' when using create_from_npm"): + await client.create_from_npm( + name="test-agent", + package_name="@runloop/example-agent", + source={"type": "git", "git": {"repository": "https://github.com/example/repo"}}, + ) + + @pytest.mark.asyncio + async def test_create_from_pip(self, mock_async_client: AsyncMock, agent_view: MockAgentView) -> None: + """Test create_from_pip factory method.""" + mock_async_client.agents.create = AsyncMock(return_value=agent_view) + + client = AsyncAgentOps(mock_async_client) + agent = await client.create_from_pip( + name="test-agent", + package_name="runloop-example-agent", + ) + + assert isinstance(agent, AsyncAgent) + assert agent.id == "agent_123" + mock_async_client.agents.create.assert_awaited_once_with( + source={ + "type": "pip", + "pip": { + "package_name": "runloop-example-agent", + }, + }, + name="test-agent", + ) + + @pytest.mark.asyncio + async def test_create_from_pip_with_all_options( + self, mock_async_client: AsyncMock, agent_view: MockAgentView + ) -> None: + """Test create_from_pip factory method with all optional parameters.""" + mock_async_client.agents.create = AsyncMock(return_value=agent_view) + + client = AsyncAgentOps(mock_async_client) + agent = await client.create_from_pip( + name="test-agent", + package_name="runloop-example-agent", + pip_version="1.2.3", + registry_url="https://pypi.example.com", + agent_setup=["pip install extra-deps"], + ) + + assert isinstance(agent, AsyncAgent) + assert agent.id == "agent_123" + mock_async_client.agents.create.assert_awaited_once_with( + source={ + "type": "pip", + "pip": { + "package_name": "runloop-example-agent", + "pip_version": "1.2.3", + "registry_url": "https://pypi.example.com", + "agent_setup": ["pip install extra-deps"], + }, + }, + name="test-agent", + ) + + @pytest.mark.asyncio + async def test_create_from_git(self, mock_async_client: AsyncMock, agent_view: MockAgentView) -> None: + """Test create_from_git factory method.""" + mock_async_client.agents.create = AsyncMock(return_value=agent_view) + + client = AsyncAgentOps(mock_async_client) + agent = await client.create_from_git( + name="test-agent", + repository="https://github.com/example/agent-repo", + ) + + assert isinstance(agent, AsyncAgent) + assert agent.id == "agent_123" + mock_async_client.agents.create.assert_awaited_once_with( + source={ + "type": "git", + "git": { + "repository": "https://github.com/example/agent-repo", + }, + }, + name="test-agent", + ) + + @pytest.mark.asyncio + async def test_create_from_git_with_all_options( + self, mock_async_client: AsyncMock, agent_view: MockAgentView + ) -> None: + """Test create_from_git factory method with all optional parameters.""" + mock_async_client.agents.create = AsyncMock(return_value=agent_view) + + client = AsyncAgentOps(mock_async_client) + agent = await client.create_from_git( + name="test-agent", + repository="https://github.com/example/agent-repo", + ref="develop", + agent_setup=["npm install", "npm run build"], + ) + + assert isinstance(agent, AsyncAgent) + assert agent.id == "agent_123" + mock_async_client.agents.create.assert_awaited_once_with( + source={ + "type": "git", + "git": { + "repository": "https://github.com/example/agent-repo", + "ref": "develop", + "agent_setup": ["npm install", "npm run build"], + }, + }, + name="test-agent", + ) + + @pytest.mark.asyncio + async def test_create_from_object(self, mock_async_client: AsyncMock, agent_view: MockAgentView) -> None: + """Test create_from_object factory method.""" + mock_async_client.agents.create = AsyncMock(return_value=agent_view) + + client = AsyncAgentOps(mock_async_client) + agent = await client.create_from_object( + name="test-agent", + object_id="obj_123", + ) + + assert isinstance(agent, AsyncAgent) + assert agent.id == "agent_123" + mock_async_client.agents.create.assert_awaited_once_with( + source={ + "type": "object", + "object": { + "object_id": "obj_123", + }, + }, + name="test-agent", + ) + + @pytest.mark.asyncio + async def test_create_from_object_with_agent_setup( + self, mock_async_client: AsyncMock, agent_view: MockAgentView + ) -> None: + """Test create_from_object factory method with agent_setup.""" + mock_async_client.agents.create = AsyncMock(return_value=agent_view) + + client = AsyncAgentOps(mock_async_client) + agent = await client.create_from_object( + name="test-agent", + object_id="obj_123", + agent_setup=["chmod +x setup.sh", "./setup.sh"], + ) + + assert isinstance(agent, AsyncAgent) + assert agent.id == "agent_123" + mock_async_client.agents.create.assert_awaited_once_with( + source={ + "type": "object", + "object": { + "object_id": "obj_123", + "agent_setup": ["chmod +x setup.sh", "./setup.sh"], + }, + }, + name="test-agent", + ) + class TestAsyncRunloopSDK: """Tests for AsyncRunloopSDK class.""" From c5d45ed2e28fed6b47941319c36b4fd062d8a7c8 Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Mon, 1 Dec 2025 14:49:20 -0800 Subject: [PATCH 14/20] add instructions for generating HTML docs --- docs/Makefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/Makefile b/docs/Makefile index d4bb2cbb9..2a753b5f6 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,5 +1,11 @@ # Minimal makefile for Sphinx documentation # +# To rebuild the html docs, first make sure you've got the right deps installed via +# uv sync --group docs +# then from the docs directory (this dir) rebuild with +# uv run make html +# Look for generated docs under _build/html. + # You can set these variables from the command line, and also # from the environment for the first two. From e39a3112c533ccc9a08901b7ff8929cf3277e72a Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Mon, 1 Dec 2025 16:08:39 -0800 Subject: [PATCH 15/20] add agent stuff to left navbars --- docs/conf.py | 18 ++++++++++++++++++ docs/sdk/async/agent.rst | 7 +++++++ docs/sdk/async/index.rst | 2 +- docs/sdk/index.rst | 2 +- docs/sdk/sync/agent.rst | 7 +++++++ docs/sdk/sync/index.rst | 1 + 6 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 docs/sdk/async/agent.rst create mode 100644 docs/sdk/sync/agent.rst diff --git a/docs/conf.py b/docs/conf.py index 5e5d3232d..56c64224f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -37,6 +37,24 @@ html_theme = "furo" html_static_path = ["_static"] +# Furo theme options +html_theme_options = { + "navigation_with_keys": True, + "sidebar_hide_name": False, +} + +# Show the global toctree in sidebar +html_sidebars = { + "**": [ + "sidebar/scroll-start.html", + "sidebar/brand.html", + "sidebar/search.html", + "sidebar/navigation.html", + "sidebar/ethical-ads.html", + "sidebar/scroll-end.html", + ] +} + # -- Extension configuration ------------------------------------------------- # Autodoc settings diff --git a/docs/sdk/async/agent.rst b/docs/sdk/async/agent.rst new file mode 100644 index 000000000..724b23bd5 --- /dev/null +++ b/docs/sdk/async/agent.rst @@ -0,0 +1,7 @@ +Agent +====== + +The ``AsyncAgent`` class provides asynchronous methods for managing and interacting with stored Agents. + +.. automodule:: runloop_api_client.sdk.async_agent + :members: diff --git a/docs/sdk/async/index.rst b/docs/sdk/async/index.rst index 0ea16d5e4..2c8a72281 100644 --- a/docs/sdk/async/index.rst +++ b/docs/sdk/async/index.rst @@ -27,4 +27,4 @@ Asynchronous resource classes for working with devboxes, blueprints, snapshots, snapshot storage_object scorer - + agent diff --git a/docs/sdk/index.rst b/docs/sdk/index.rst index 0151a89d3..83958d554 100644 --- a/docs/sdk/index.rst +++ b/docs/sdk/index.rst @@ -9,6 +9,6 @@ The Runloop SDK provides both synchronous and asynchronous interfaces for managi :maxdepth: 2 :caption: SDK Documentation - sync/index async/index + sync/index types diff --git a/docs/sdk/sync/agent.rst b/docs/sdk/sync/agent.rst new file mode 100644 index 000000000..207c4b611 --- /dev/null +++ b/docs/sdk/sync/agent.rst @@ -0,0 +1,7 @@ +Agent +====== + +The ``Agent`` class provides synchronous methods for managing and interacting with stored Agents. + +.. automodule:: runloop_api_client.sdk.agent + :members: diff --git a/docs/sdk/sync/index.rst b/docs/sdk/sync/index.rst index e7d1ca616..063afa4ff 100644 --- a/docs/sdk/sync/index.rst +++ b/docs/sdk/sync/index.rst @@ -27,4 +27,5 @@ Synchronous resource classes for working with devboxes, blueprints, snapshots, a snapshot storage_object scorer + agent From 918896e04d42bd4c1a63b4588d05fb815e2a60b8 Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Mon, 1 Dec 2025 16:52:52 -0800 Subject: [PATCH 16/20] add example docs to python agent stuff --- src/runloop_api_client/sdk/agent.py | 14 ++++++- src/runloop_api_client/sdk/async_.py | 15 ++++++- src/runloop_api_client/sdk/async_agent.py | 14 ++++++- src/runloop_api_client/sdk/async_devbox.py | 2 +- src/runloop_api_client/sdk/devbox.py | 2 +- src/runloop_api_client/sdk/sync.py | 47 +++++++++++++++++++++- 6 files changed, 86 insertions(+), 8 deletions(-) diff --git a/src/runloop_api_client/sdk/agent.py b/src/runloop_api_client/sdk/agent.py index 812a34555..e0b0e0ec4 100644 --- a/src/runloop_api_client/sdk/agent.py +++ b/src/runloop_api_client/sdk/agent.py @@ -13,7 +13,19 @@ class Agent: - """Wrapper around synchronous agent operations.""" + """Wrapper around synchronous agent operations. + + This class provides a Pythonic interface for interacting with agents, + including retrieving agent information. + + Example: + >>> agent = runloop.agent.create_from_npm( + ... name="my-agent", + ... package_name="@runloop/example-agent" + ... ) + >>> info = agent.get_info() + >>> print(info.name) + """ def __init__( self, diff --git a/src/runloop_api_client/sdk/async_.py b/src/runloop_api_client/sdk/async_.py index 546211c18..0825c6b46 100644 --- a/src/runloop_api_client/sdk/async_.py +++ b/src/runloop_api_client/sdk/async_.py @@ -549,11 +549,22 @@ class AsyncAgentOps: """High-level async manager for creating and managing agents. Accessed via ``runloop.agent`` from :class:`AsyncRunloopSDK`, provides - coroutines to create, retrieve, and list agents. + coroutines to create, retrieve, and list agents from various sources (npm, pip, git, object storage). Example: >>> runloop = AsyncRunloopSDK() - >>> agent = await runloop.agent.create(name="my-agent") + >>> # Create agent from NPM package + >>> agent = await runloop.agent.create_from_npm( + ... name="my-agent", + ... package_name="@runloop/example-agent" + ... ) + >>> # Create agent from Git repository + >>> agent = await runloop.agent.create_from_git( + ... name="git-agent", + ... repository="https://github.com/user/agent-repo", + ... ref="main" + ... ) + >>> # List all agents >>> agents = await runloop.agent.list(limit=10) """ diff --git a/src/runloop_api_client/sdk/async_agent.py b/src/runloop_api_client/sdk/async_agent.py index e9740ce47..eefce2120 100644 --- a/src/runloop_api_client/sdk/async_agent.py +++ b/src/runloop_api_client/sdk/async_agent.py @@ -13,7 +13,19 @@ class AsyncAgent: - """Async wrapper around agent operations.""" + """Async wrapper around agent operations. + + This class provides an asynchronous interface for interacting with agents, + including retrieving agent information. + + Example: + >>> agent = await runloop.agent.create_from_npm( + ... name="my-agent", + ... package_name="@runloop/example-agent" + ... ) + >>> info = await agent.get_info() + >>> print(info.name) + """ def __init__( self, diff --git a/src/runloop_api_client/sdk/async_devbox.py b/src/runloop_api_client/sdk/async_devbox.py index 89080abef..3c6db3b31 100644 --- a/src/runloop_api_client/sdk/async_devbox.py +++ b/src/runloop_api_client/sdk/async_devbox.py @@ -54,7 +54,7 @@ class AsyncDevbox: management. Example: - >>> devbox = await sdk.devbox.create(name="my-devbox") + >>> devbox = await runloop.devbox.create(name="my-devbox") >>> async with devbox: ... result = await devbox.cmd.exec("echo 'hello'") ... print(await result.stdout()) diff --git a/src/runloop_api_client/sdk/devbox.py b/src/runloop_api_client/sdk/devbox.py index cad396815..22cda6fe3 100644 --- a/src/runloop_api_client/sdk/devbox.py +++ b/src/runloop_api_client/sdk/devbox.py @@ -54,7 +54,7 @@ class Devbox: The Devbox class supports context manager protocol for automatic cleanup. Example: - >>> with sdk.devbox.create(name="my-devbox") as devbox: + >>> with runloop.devbox.create(name="my-devbox") as devbox: ... result = devbox.cmd.exec("echo 'hello'") ... print(result.stdout()) # Devbox is automatically shutdown on exit diff --git a/src/runloop_api_client/sdk/sync.py b/src/runloop_api_client/sdk/sync.py index fb214c000..e4fad064c 100644 --- a/src/runloop_api_client/sdk/sync.py +++ b/src/runloop_api_client/sdk/sync.py @@ -545,11 +545,22 @@ class AgentOps: """High-level manager for creating and managing agents. Accessed via ``runloop.agent`` from :class:`RunloopSDK`, provides methods to - create, retrieve, and list agents. + create, retrieve, and list agents from various sources (npm, pip, git, object storage). Example: >>> runloop = RunloopSDK() - >>> agent = runloop.agent.create(name="my-agent") + >>> # Create agent from NPM package + >>> agent = runloop.agent.create_from_npm( + ... name="my-agent", + ... package_name="@runloop/example-agent" + ... ) + >>> # Create agent from Git repository + >>> agent = runloop.agent.create_from_git( + ... name="git-agent", + ... repository="https://github.com/user/agent-repo", + ... ref="main" + ... ) + >>> # List all agents >>> agents = runloop.agent.list(limit=10) """ @@ -587,6 +598,13 @@ def create_from_npm( ) -> Agent: """Create an agent from an NPM package. + Example: + >>> agent = runloop.agent.create_from_npm( + ... name="my-npm-agent", + ... package_name="@runloop/example-agent", + ... npm_version="^1.0.0" + ... ) + :param package_name: NPM package name :type package_name: str :param npm_version: NPM version constraint, defaults to None @@ -627,6 +645,13 @@ def create_from_pip( ) -> Agent: """Create an agent from a Pip package. + Example: + >>> agent = runloop.agent.create_from_pip( + ... name="my-pip-agent", + ... package_name="runloop-example-agent", + ... pip_version=">=1.0.0" + ... ) + :param package_name: Pip package name :type package_name: str :param pip_version: Pip version constraint, defaults to None @@ -666,6 +691,14 @@ def create_from_git( ) -> Agent: """Create an agent from a Git repository. + Example: + >>> agent = runloop.agent.create_from_git( + ... name="my-git-agent", + ... repository="https://github.com/user/agent-repo", + ... ref="main", + ... agent_setup=["npm install", "npm run build"] + ... ) + :param repository: Git repository URL :type repository: str :param ref: Optional Git ref (branch/tag/commit), defaults to main/HEAD @@ -700,6 +733,16 @@ def create_from_object( ) -> Agent: """Create an agent from a storage object. + Example: + >>> # First upload agent code as an object + >>> obj = runloop.storage_object.upload_from_dir("./my-agent") + >>> # Then create agent from the object + >>> agent = runloop.agent.create_from_object( + ... name="my-object-agent", + ... object_id=obj.id, + ... agent_setup=["chmod +x setup.sh", "./setup.sh"] + ... ) + :param object_id: Storage object ID :type object_id: str :param agent_setup: Setup commands to run after unpacking, defaults to None From d66f371b60ac46f092c6f985b990567a0d869868 Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Mon, 1 Dec 2025 17:04:10 -0800 Subject: [PATCH 17/20] fix html compile warnings --- docs/conf.py | 2 +- src/runloop_api_client/types/scoring_function.py | 2 +- src/runloop_api_client/types/scoring_function_param.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 56c64224f..dc74a74c2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -35,7 +35,7 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "furo" -html_static_path = ["_static"] +# html_static_path = ["_static"] # Furo theme options html_theme_options = { diff --git a/src/runloop_api_client/types/scoring_function.py b/src/runloop_api_client/types/scoring_function.py index c49ff61e4..ba4aea9e1 100644 --- a/src/runloop_api_client/types/scoring_function.py +++ b/src/runloop_api_client/types/scoring_function.py @@ -120,7 +120,7 @@ class ScorerTestBasedScoringFunction(BaseModel): class ScoringFunction(BaseModel): name: str - """Name of scoring function. Names must only contain [a-zA-Z0-9_-].""" + """Name of scoring function. Names must only contain ``[a-zA-Z0-9_-]``.""" scorer: Scorer """The scoring function to use for evaluating this scenario. diff --git a/src/runloop_api_client/types/scoring_function_param.py b/src/runloop_api_client/types/scoring_function_param.py index e2c9bc253..f9b6b26c7 100644 --- a/src/runloop_api_client/types/scoring_function_param.py +++ b/src/runloop_api_client/types/scoring_function_param.py @@ -116,7 +116,7 @@ class ScorerTestBasedScoringFunction(TypedDict, total=False): class ScoringFunctionParam(TypedDict, total=False): name: Required[str] - """Name of scoring function. Names must only contain [a-zA-Z0-9_-].""" + """Name of scoring function. Names must only contain ``[a-zA-Z0-9_-]``.""" scorer: Required[Scorer] """The scoring function to use for evaluating this scenario. From ea7b0b807bbda710075f6b70b35f153db7c20b8d Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Thu, 4 Dec 2025 09:14:03 -0800 Subject: [PATCH 18/20] add a few more sample commands --- CONTRIBUTING.md | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c428d61d5..9201cf4cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -89,13 +89,49 @@ Most tests require you to [set up a mock server](https://github.com/stoplightio/ ```sh # you will need npm installed -$ npx prism mock path/to/your/openapi.yml +npx prism mock path/to/your/openapi.yml ``` ```sh -$ ./scripts/test +# run all tests +./scripts/test + +# pass in pytest args, eg to show info on skipped tests: +./scripts/test -rs + +# Run tests for only one python version +UV_PYTHON=3.13 ./scripts/test +``` + +### Running pytets + +Assuming you have a uv virtual env set up in .venv, the following are helpful for more granular test running: + +```sh +# Run all tests pytests +.venv/bin/pytest tests/ + +# Run all SDK tests with verbose info +.venv/bin/pytest tests/sdk/ -v + +# Run specific test class +.venv/bin/pytest tests/sdk/test_clients.py::TestAgentClient -v + +# Run specific test method +.venv/bin/pytest tests/sdk/test_clients.py::TestAgentClient::test_create_from_npm -v + +# Run agent smoketests (requires RUNLOOP_API_KEY) +export RUNLOOP_API_KEY=your_key_here +.venv/bin/pytest tests/smoketests/sdk/test_agent.py -v + +# Run tests matching a pattern +.venv/bin/pytest tests/sdk/ -k "agent" -v + +# Run with coverage +.venv/bin/pytest tests/sdk/ --cov=src/runloop_api_client/sdk --cov-report=html ``` + ## Linting and formatting This repository uses [ruff](https://github.com/astral-sh/ruff) and From c6991e933284936383daef2c07320f10a4d9b762 Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Thu, 4 Dec 2025 09:30:16 -0800 Subject: [PATCH 19/20] remove accidentally added temp file --- tests/smoketests/sdk/test_agent.py~ | 195 ---------------------------- 1 file changed, 195 deletions(-) delete mode 100644 tests/smoketests/sdk/test_agent.py~ diff --git a/tests/smoketests/sdk/test_agent.py~ b/tests/smoketests/sdk/test_agent.py~ deleted file mode 100644 index bd1e90e3f..000000000 --- a/tests/smoketests/sdk/test_agent.py~ +++ /dev/null @@ -1,195 +0,0 @@ -"""Synchronous SDK smoke tests for Agent operations.""" - -from __future__ import annotations - -import pytest - -from runloop_api_client.sdk import RunloopSDK -from tests.smoketests.utils import unique_name - -pytestmark = [pytest.mark.smoketest] - -THIRTY_SECOND_TIMEOUT = 30 -TWO_MINUTE_TIMEOUT = 120 - - -class TestAgentLifecycle: - """Test basic agent lifecycle operations.""" - - @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) - def test_agent_create_basic(self, sdk_client: RunloopSDK) -> None: - """Test creating a basic agent.""" - name = unique_name("sdk-agent-basic") - agent = sdk_client.agent.create(name=name) - - try: - assert agent is not None - assert agent.id is not None - assert len(agent.id) > 0 - - # Verify agent information - info = agent.get_info() - assert info.id == agent.id - assert info.name == name - finally: - # Agents don't have a delete method, they're managed by the API - pass - - @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) - def test_agent_create_with_metadata(self, sdk_client: RunloopSDK) -> None: - """Test creating an agent with metadata.""" - name = unique_name("sdk-agent-metadata") - metadata = { - "purpose": "sdk-testing", - "version": "1.0", - } - - agent = sdk_client.agent.create(name=name, metadata=metadata) - - try: - assert agent.id is not None - - # Verify metadata is preserved - info = agent.get_info() - assert info.name == name - # Note: Metadata handling may vary based on API implementation - finally: - pass - - @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) - def test_agent_get_info(self, sdk_client: RunloopSDK) -> None: - """Test retrieving agent information.""" - name = unique_name("sdk-agent-info") - agent = sdk_client.agent.create(name=name) - - try: - info = agent.get_info() - - assert info.id == agent.id - assert info.name == name - assert info.create_time_ms > 0 - assert isinstance(info.is_public, bool) - finally: - pass - - -class TestAgentListing: - """Test agent listing and retrieval operations.""" - - @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) - def test_list_agents(self, sdk_client: RunloopSDK) -> None: - """Test listing agents.""" - agents = sdk_client.agent.list(limit=10) - - assert isinstance(agents, list) - # List might be empty, that's okay - assert len(agents) >= 0 - - @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) - def test_get_agent_by_id(self, sdk_client: RunloopSDK) -> None: - """Test retrieving agent by ID.""" - # Create an agent - created = sdk_client.agent.create(name=unique_name("sdk-agent-retrieve")) - - try: - # Retrieve it by ID - retrieved = sdk_client.agent.from_id(created.id) - assert retrieved.id == created.id - - # Verify it's the same agent - info = retrieved.get_info() - assert info.id == created.id - finally: - pass - - @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) - def test_list_multiple_agents(self, sdk_client: RunloopSDK) -> None: - """Test listing multiple agents after creation.""" - # Create multiple agents - agent1 = sdk_client.agent.create(name=unique_name("sdk-agent-list-1")) - agent2 = sdk_client.agent.create(name=unique_name("sdk-agent-list-2")) - agent3 = sdk_client.agent.create(name=unique_name("sdk-agent-list-3")) - - try: - # List agents - agents = sdk_client.agent.list(limit=100) - - assert isinstance(agents, list) - assert len(agents) >= 3 - - # Verify our agents are in the list - agent_ids = [a.id for a in agents] - assert agent1.id in agent_ids - assert agent2.id in agent_ids - assert agent3.id in agent_ids - finally: - pass - - -class TestAgentCreationVariations: - """Test different agent creation scenarios.""" - - @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) - def test_agent_with_is_public_flag(self, sdk_client: RunloopSDK) -> None: - """Test creating an agent with is_public flag.""" - name = unique_name("sdk-agent-public") - - # Create a public agent - agent = sdk_client.agent.create(name=name, is_public=True) - - try: - assert agent.id is not None - info = agent.get_info() - assert info.name == name - assert info.is_public is True - finally: - pass - - @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) - def test_agent_with_source_npm(self, sdk_client: RunloopSDK) -> None: - """Test creating an agent with npm source.""" - name = unique_name("sdk-agent-npm") - - agent = sdk_client.agent.create( - name=name, - source={ - "type": "npm", - "npm": { - "package_name": "@runloop/example-agent", - }, - }, - ) - - try: - assert agent.id is not None - info = agent.get_info() - assert info.name == name - assert info.source is not None - assert info.source.type == "npm" - finally: - pass - - @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) - def test_agent_with_source_git(self, sdk_client: RunloopSDK) -> None: - """Test creating an agent with git source.""" - name = unique_name("sdk-agent-git") - - agent = sdk_client.agent.create( - name=name, - source={ - "type": "git", - "git": { - "repository": "https://github.com/runloop/example-agent", - "ref": "main", - }, - }, - ) - - try: - assert agent.id is not None - info = agent.get_info() - assert info.name == name - assert info.source is not None - assert info.source.type == "git" - finally: - pass From 662b81ec88ea0faf1b618d2f326585de69af4c5d Mon Sep 17 00:00:00 2001 From: Rob von Behren Date: Thu, 4 Dec 2025 09:39:20 -0800 Subject: [PATCH 20/20] fix linter errors --- scripts/lint | 2 +- src/runloop_api_client/sdk/__init__.py | 1 - tests/sdk/test_async_ops.py | 15 +++++++++++---- tests/sdk/test_ops.py | 4 ++-- uv.lock | 4 ++-- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/scripts/lint b/scripts/lint index aab79d229..983f711d5 100755 --- a/scripts/lint +++ b/scripts/lint @@ -5,7 +5,7 @@ set -e cd "$(dirname "$0")/.." echo "==> Running lints" -uv run ruff check . +uv run ruff check . "$@" echo "==> Making sure it imports" uv run python -c 'import runloop_api_client' diff --git a/src/runloop_api_client/sdk/__init__.py b/src/runloop_api_client/sdk/__init__.py index b31e1667b..483f4b711 100644 --- a/src/runloop_api_client/sdk/__init__.py +++ b/src/runloop_api_client/sdk/__init__.py @@ -16,7 +16,6 @@ AsyncBlueprintOps, AsyncStorageObjectOps, ) -from .agent import Agent from .devbox import Devbox, NamedShell from .scorer import Scorer from .snapshot import Snapshot diff --git a/tests/sdk/test_async_ops.py b/tests/sdk/test_async_ops.py index fe287d861..8a455cd68 100644 --- a/tests/sdk/test_async_ops.py +++ b/tests/sdk/test_async_ops.py @@ -11,23 +11,30 @@ import pytest from tests.sdk.conftest import ( + MockAgentView, MockDevboxView, MockObjectView, MockScorerView, MockSnapshotView, MockBlueprintView, - MockAgentView, create_mock_httpx_response, ) -from runloop_api_client.sdk import AsyncAgent, AsyncDevbox, AsyncScorer, AsyncSnapshot, AsyncBlueprint, AsyncStorageObject +from runloop_api_client.sdk import ( + AsyncAgent, + AsyncDevbox, + AsyncScorer, + AsyncSnapshot, + AsyncBlueprint, + AsyncStorageObject, +) from runloop_api_client.sdk.async_ import ( + AsyncAgentOps, AsyncDevboxOps, AsyncScorerOps, AsyncRunloopSDK, AsyncSnapshotOps, AsyncBlueprintOps, AsyncStorageObjectOps, - AsyncAgentOps, ) from runloop_api_client.lib.polling import PollingConfig @@ -780,7 +787,7 @@ async def test_list(self, mock_async_client: AsyncMock) -> None: mock_async_client.agents.list = AsyncMock(return_value=page) # Mock retrieve to return the corresponding agent_view when called - async def mock_retrieve(agent_id, **kwargs): + async def mock_retrieve(agent_id, **_unused_kwargs): if agent_id == "agent_001": return agent_view_1 elif agent_id == "agent_002": diff --git a/tests/sdk/test_ops.py b/tests/sdk/test_ops.py index 53b8bed52..cf3a7216d 100644 --- a/tests/sdk/test_ops.py +++ b/tests/sdk/test_ops.py @@ -9,23 +9,23 @@ import pytest from tests.sdk.conftest import ( + MockAgentView, MockDevboxView, MockObjectView, MockScorerView, MockSnapshotView, MockBlueprintView, - MockAgentView, create_mock_httpx_response, ) from runloop_api_client.sdk import Agent, Devbox, Scorer, Snapshot, Blueprint, StorageObject from runloop_api_client.sdk.sync import ( + AgentOps, DevboxOps, ScorerOps, RunloopSDK, SnapshotOps, BlueprintOps, StorageObjectOps, - AgentOps, ) from runloop_api_client.lib.polling import PollingConfig diff --git a/uv.lock b/uv.lock index e5bd5b8fd..50fa85fae 100644 --- a/uv.lock +++ b/uv.lock @@ -777,7 +777,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -2098,7 +2098,7 @@ wheels = [ [[package]] name = "runloop-api-client" -version = "0.69.0" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "anyio" },