Skip to content

Commit 5af7c25

Browse files
committed
feat(sdk): add axons to OO SDK
1 parent ceed348 commit 5af7c25

15 files changed

Lines changed: 670 additions & 3 deletions

File tree

src/runloop_api_client/resources/axons.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,12 @@ def subscribe_sse(
227227
"""
228228
if not id:
229229
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
230+
default_headers: Headers = {"Accept": "text/event-stream"}
231+
merged_headers = default_headers if extra_headers is None else {**default_headers, **extra_headers}
230232
return self._get(
231233
path_template("/v1/axons/{id}/subscribe/sse", id=id),
232234
options=make_request_options(
233-
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
235+
extra_headers=merged_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
234236
),
235237
cast_to=AxonEventView,
236238
stream=True,
@@ -437,10 +439,12 @@ async def subscribe_sse(
437439
"""
438440
if not id:
439441
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
442+
default_headers: Headers = {"Accept": "text/event-stream"}
443+
merged_headers = default_headers if extra_headers is None else {**default_headers, **extra_headers}
440444
return await self._get(
441445
path_template("/v1/axons/{id}/subscribe/sse", id=id),
442446
options=make_request_options(
443-
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
447+
extra_headers=merged_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
444448
),
445449
cast_to=AxonEventView,
446450
stream=True,

src/runloop_api_client/sdk/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
from __future__ import annotations
77

8+
from .axon import Axon
89
from .sync import (
10+
AxonOps,
911
AgentOps,
1012
DevboxOps,
1113
ScorerOps,
@@ -23,6 +25,7 @@
2325
from .agent import Agent
2426
from ._types import ScenarioPreview
2527
from .async_ import (
28+
AsyncAxonOps,
2629
AsyncAgentOps,
2730
AsyncDevboxOps,
2831
AsyncScorerOps,
@@ -45,6 +48,7 @@
4548
from .benchmark import Benchmark
4649
from .blueprint import Blueprint
4750
from .execution import Execution
51+
from .async_axon import AsyncAxon
4852
from .mcp_config import McpConfig
4953
from .async_agent import AsyncAgent
5054
from .async_devbox import AsyncDevbox, AsyncNamedShell
@@ -78,6 +82,8 @@
7882
# Management interfaces
7983
"AgentOps",
8084
"AsyncAgentOps",
85+
"AxonOps",
86+
"AsyncAxonOps",
8187
"BenchmarkOps",
8288
"AsyncBenchmarkOps",
8389
"DevboxOps",
@@ -103,6 +109,8 @@
103109
# Resource classes
104110
"Agent",
105111
"AsyncAgent",
112+
"Axon",
113+
"AsyncAxon",
106114
"AsyncSecret",
107115
"Benchmark",
108116
"AsyncBenchmark",

src/runloop_api_client/sdk/_types.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
InputContext,
66
ScenarioView,
77
AgentListParams,
8+
AxonCreateParams,
89
DevboxListParams,
910
ObjectListParams,
1011
AgentCreateParams,
12+
AxonPublishParams,
1113
DevboxCreateParams,
1214
ObjectCreateParams,
1315
ScenarioListParams,
@@ -186,6 +188,14 @@ class SDKAgentListParams(AgentListParams, BaseRequestOptions):
186188
pass
187189

188190

191+
class SDKAxonCreateParams(AxonCreateParams, LongRequestOptions):
192+
pass
193+
194+
195+
class SDKAxonPublishParams(AxonPublishParams, LongRequestOptions):
196+
pass
197+
198+
189199
class SDKScenarioListParams(ScenarioListParams, BaseRequestOptions):
190200
pass
191201

src/runloop_api_client/sdk/async_.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
BaseRequestOptions,
1515
LongRequestOptions,
1616
SDKAgentListParams,
17+
SDKAxonCreateParams,
1718
SDKDevboxListParams,
1819
SDKObjectListParams,
1920
SDKScorerListParams,
@@ -38,6 +39,7 @@
3839
from .._types import Timeout, NotGiven, not_given
3940
from .._client import DEFAULT_MAX_RETRIES, AsyncRunloop
4041
from ._helpers import detect_content_type
42+
from .async_axon import AsyncAxon
4143
from .async_agent import AsyncAgent
4244
from .async_devbox import AsyncDevbox
4345
from .async_scorer import AsyncScorer
@@ -521,6 +523,33 @@ async def upload_from_bytes(
521523
return obj
522524

523525

526+
class AsyncAxonOps:
527+
"""[Beta] Create and manage axons (async). Access via ``runloop.axon``.
528+
529+
Example:
530+
>>> runloop = AsyncRunloopSDK()
531+
>>> axon = await runloop.axon.create()
532+
>>> await axon.publish(event_type="test", origin="USER_EVENT", payload="{}", source="sdk")
533+
"""
534+
535+
def __init__(self, client: AsyncRunloop) -> None:
536+
self._client = client
537+
538+
async def create(self, **params: Unpack[SDKAxonCreateParams]) -> AsyncAxon:
539+
"""[Beta] Create a new axon."""
540+
response = await self._client.axons.create(**params)
541+
return AsyncAxon(self._client, response.id)
542+
543+
def from_id(self, axon_id: str) -> AsyncAxon:
544+
"""Get an AsyncAxon instance for an existing axon ID."""
545+
return AsyncAxon(self._client, axon_id)
546+
547+
async def list(self, **options: Unpack[BaseRequestOptions]) -> list[AsyncAxon]:
548+
"""[Beta] List all active axons."""
549+
result = await self._client.axons.list(**options)
550+
return [AsyncAxon(self._client, axon.id) for axon in result.axons]
551+
552+
524553
class AsyncScorerOps:
525554
"""Create and manage custom scorers (async). Access via ``runloop.scorer``.
526555
@@ -1205,6 +1234,8 @@ class AsyncRunloopSDK:
12051234
:vartype api: AsyncRunloop
12061235
:ivar agent: High-level async interface for agent management.
12071236
:vartype agent: AsyncAgentOps
1237+
:ivar axon: [Beta] High-level async interface for axon management
1238+
:vartype axon: AsyncAxonOps
12081239
:ivar benchmark: High-level async interface for benchmark management
12091240
:vartype benchmark: AsyncBenchmarkOps
12101241
:ivar devbox: High-level async interface for devbox management
@@ -1238,6 +1269,7 @@ class AsyncRunloopSDK:
12381269

12391270
api: AsyncRunloop
12401271
agent: AsyncAgentOps
1272+
axon: AsyncAxonOps
12411273
benchmark: AsyncBenchmarkOps
12421274
devbox: AsyncDevboxOps
12431275
blueprint: AsyncBlueprintOps
@@ -1289,6 +1321,7 @@ def __init__(
12891321
)
12901322

12911323
self.agent = AsyncAgentOps(self.api)
1324+
self.axon = AsyncAxonOps(self.api)
12921325
self.benchmark = AsyncBenchmarkOps(self.api)
12931326
self.devbox = AsyncDevboxOps(self.api)
12941327
self.blueprint = AsyncBlueprintOps(self.api)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Axon resource class for asynchronous operations."""
2+
3+
from __future__ import annotations
4+
5+
from typing_extensions import Unpack, override
6+
7+
from ._types import (
8+
BaseRequestOptions,
9+
SDKAxonPublishParams,
10+
)
11+
from .._client import AsyncRunloop
12+
from .._streaming import AsyncStream
13+
from ..types.axon_view import AxonView
14+
from ..types.axon_event_view import AxonEventView
15+
from ..types.publish_result_view import PublishResultView
16+
17+
18+
class AsyncAxon:
19+
"""[Beta] Wrapper around asynchronous axon operations.
20+
21+
Axons are event communication channels that support publishing events
22+
and subscribing to event streams via server-sent events (SSE).
23+
Obtain instances via ``runloop.axon.create()`` or ``runloop.axon.from_id()``.
24+
25+
Example:
26+
>>> runloop = AsyncRunloopSDK()
27+
>>> axon = await runloop.axon.create()
28+
>>> await axon.publish(event_type="task_done", origin="AGENT_EVENT", payload="{}", source="my-agent")
29+
"""
30+
31+
def __init__(self, client: AsyncRunloop, axon_id: str) -> None:
32+
self._client = client
33+
self._id = axon_id
34+
35+
@override
36+
def __repr__(self) -> str:
37+
return f"<AsyncAxon id={self._id!r}>"
38+
39+
@property
40+
def id(self) -> str:
41+
return self._id
42+
43+
async def get_info(self, **options: Unpack[BaseRequestOptions]) -> AxonView:
44+
"""[Beta] Retrieve the latest axon information."""
45+
return await self._client.axons.retrieve(self._id, **options)
46+
47+
async def publish(self, **params: Unpack[SDKAxonPublishParams]) -> PublishResultView:
48+
"""[Beta] Publish an event to this axon."""
49+
return await self._client.axons.publish(self._id, **params)
50+
51+
async def subscribe_sse(self, **options: Unpack[BaseRequestOptions]) -> AsyncStream[AxonEventView]:
52+
"""[Beta] Subscribe to this axon's event stream via SSE."""
53+
return await self._client.axons.subscribe_sse(self._id, **options)

src/runloop_api_client/sdk/axon.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Axon resource class for synchronous operations."""
2+
3+
from __future__ import annotations
4+
5+
from typing_extensions import Unpack, override
6+
7+
from ._types import (
8+
BaseRequestOptions,
9+
SDKAxonPublishParams,
10+
)
11+
from .._client import Runloop
12+
from .._streaming import Stream
13+
from ..types.axon_view import AxonView
14+
from ..types.axon_event_view import AxonEventView
15+
from ..types.publish_result_view import PublishResultView
16+
17+
18+
class Axon:
19+
"""[Beta] Wrapper around synchronous axon operations.
20+
21+
Axons are event communication channels that support publishing events
22+
and subscribing to event streams via server-sent events (SSE).
23+
Obtain instances via ``runloop.axon.create()`` or ``runloop.axon.from_id()``.
24+
25+
Example:
26+
>>> runloop = RunloopSDK()
27+
>>> axon = runloop.axon.create()
28+
>>> axon.publish(event_type="task_done", origin="AGENT_EVENT", payload="{}", source="my-agent")
29+
"""
30+
31+
def __init__(self, client: Runloop, axon_id: str) -> None:
32+
self._client = client
33+
self._id = axon_id
34+
35+
@override
36+
def __repr__(self) -> str:
37+
return f"<Axon id={self._id!r}>"
38+
39+
@property
40+
def id(self) -> str:
41+
return self._id
42+
43+
def get_info(self, **options: Unpack[BaseRequestOptions]) -> AxonView:
44+
"""[Beta] Retrieve the latest axon information."""
45+
return self._client.axons.retrieve(self._id, **options)
46+
47+
def publish(self, **params: Unpack[SDKAxonPublishParams]) -> PublishResultView:
48+
"""[Beta] Publish an event to this axon."""
49+
return self._client.axons.publish(self._id, **params)
50+
51+
def subscribe_sse(self, **options: Unpack[BaseRequestOptions]) -> Stream[AxonEventView]:
52+
"""[Beta] Subscribe to this axon's event stream via SSE."""
53+
return self._client.axons.subscribe_sse(self._id, **options)

src/runloop_api_client/sdk/sync.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99

1010
import httpx
1111

12+
from .axon import Axon
1213
from .agent import Agent
1314
from ._types import (
1415
BaseRequestOptions,
1516
LongRequestOptions,
1617
SDKAgentListParams,
18+
SDKAxonCreateParams,
1719
SDKDevboxListParams,
1820
SDKObjectListParams,
1921
SDKScorerListParams,
@@ -516,6 +518,33 @@ def upload_from_bytes(
516518
return obj
517519

518520

521+
class AxonOps:
522+
"""[Beta] Create and manage axons. Access via ``runloop.axon``.
523+
524+
Example:
525+
>>> runloop = RunloopSDK()
526+
>>> axon = runloop.axon.create()
527+
>>> axon.publish(event_type="test", origin="USER_EVENT", payload="{}", source="sdk")
528+
"""
529+
530+
def __init__(self, client: Runloop) -> None:
531+
self._client = client
532+
533+
def create(self, **params: Unpack[SDKAxonCreateParams]) -> Axon:
534+
"""[Beta] Create a new axon."""
535+
response = self._client.axons.create(**params)
536+
return Axon(self._client, response.id)
537+
538+
def from_id(self, axon_id: str) -> Axon:
539+
"""Get an Axon instance for an existing axon ID."""
540+
return Axon(self._client, axon_id)
541+
542+
def list(self, **options: Unpack[BaseRequestOptions]) -> list[Axon]:
543+
"""[Beta] List all active axons."""
544+
result = self._client.axons.list(**options)
545+
return [Axon(self._client, axon.id) for axon in result.axons]
546+
547+
519548
class ScorerOps:
520549
"""Create and manage custom scorers. Access via ``runloop.scorer``.
521550
@@ -1230,6 +1259,8 @@ class RunloopSDK:
12301259
:vartype api: Runloop
12311260
:ivar agent: High-level interface for agent management.
12321261
:vartype agent: AgentOps
1262+
:ivar axon: [Beta] High-level interface for axon management
1263+
:vartype axon: AxonOps
12331264
:ivar benchmark: High-level interface for benchmark management
12341265
:vartype benchmark: BenchmarkOps
12351266
:ivar devbox: High-level interface for devbox management
@@ -1263,6 +1294,7 @@ class RunloopSDK:
12631294

12641295
api: Runloop
12651296
agent: AgentOps
1297+
axon: AxonOps
12661298
benchmark: BenchmarkOps
12671299
devbox: DevboxOps
12681300
blueprint: BlueprintOps
@@ -1314,6 +1346,7 @@ def __init__(
13141346
)
13151347

13161348
self.agent = AgentOps(self.api)
1349+
self.axon = AxonOps(self.api)
13171350
self.benchmark = BenchmarkOps(self.api)
13181351
self.devbox = DevboxOps(self.api)
13191352
self.blueprint = BlueprintOps(self.api)

tests/sdk/conftest.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"object": "obj_123",
2424
"scorer": "sco_123",
2525
"agent": "agt_123",
26+
"axon": "axn_123",
2627
"scenario": "scn_123",
2728
"scenario_run": "scr_123",
2829
"benchmark": "bmd_123",
@@ -115,6 +116,23 @@ class MockAgentView:
115116
source: Any = None
116117

117118

119+
@dataclass
120+
class MockAxonView:
121+
"""Mock AxonView for testing."""
122+
123+
id: str = TEST_IDS["axon"]
124+
created_at_ms: int = 1234567890000
125+
name: str = "test-axon"
126+
127+
128+
@dataclass
129+
class MockPublishResultView:
130+
"""Mock PublishResultView for testing."""
131+
132+
sequence: int = 1
133+
timestamp_ms: int = 1234567890000
134+
135+
118136
@dataclass
119137
class MockScenarioView:
120138
"""Mock ScenarioView for testing."""
@@ -307,6 +325,12 @@ def agent_view() -> MockAgentView:
307325
return MockAgentView()
308326

309327

328+
@pytest.fixture
329+
def axon_view() -> MockAxonView:
330+
"""Create a mock AxonView."""
331+
return MockAxonView()
332+
333+
310334
@pytest.fixture
311335
def scenario_view() -> MockScenarioView:
312336
"""Create a mock ScenarioView."""

0 commit comments

Comments
 (0)