Skip to content

Commit 952d7fb

Browse files
foleydangclaude
andcommitted
fix: remove dead code, fix __del__ safety, make agents.update version explicit
- Remove unused _new_request_id() and import uuid from transport.py - Fix __del__ to wrap is_closed check in try/except for interpreter shutdown safety - Make version a required kwarg in agents.update() instead of auto-retrieving - Move stream error handling from session_events into transport retry loop - Add unit tests for agents.update version contract Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 55d6cf3 commit 952d7fb

4 files changed

Lines changed: 127 additions & 108 deletions

File tree

dashscope/agentstudio/resources/agents.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,15 @@
66

77
from typing import Any, Mapping, Optional, Sequence
88

9-
from dashscope.agentstudio.pagination import (
10-
AsyncCursorPage,
11-
CursorPage,
12-
build_page,
13-
)
14-
from dashscope.agentstudio.resources._helpers import (
15-
_coerce_agent,
16-
_coerce_agent_version,
17-
)
9+
from dashscope.agentstudio.pagination import (AsyncCursorPage, CursorPage,
10+
build_page)
11+
from dashscope.agentstudio.resources._helpers import (_coerce_agent,
12+
_coerce_agent_version)
1813
from dashscope.agentstudio.types import Agent, AgentVersion
19-
from dashscope.agentstudio.types.params import (
20-
AgentCreateParams,
21-
AgentListParams,
22-
AgentUpdateParams,
23-
AgentVersionListParams,
24-
)
25-
14+
from dashscope.agentstudio.types.params import (AgentCreateParams,
15+
AgentListParams,
16+
AgentUpdateParams,
17+
AgentVersionListParams)
2618

2719
_PATH_AGENTS = "/agents"
2820

@@ -82,6 +74,7 @@ def update(
8274
self,
8375
agent_id: str,
8476
*,
77+
version: int,
8578
name: Optional[str] = None,
8679
description: Optional[str] = None,
8780
model: Optional[str] = None,
@@ -91,9 +84,14 @@ def update(
9184
skills: Optional[Sequence[Mapping[str, Any]]] = None,
9285
metadata: Optional[Mapping[str, Any]] = None,
9386
) -> Agent:
94-
current = self.retrieve(agent_id)
87+
"""Update the latest version of an agent.
88+
89+
``version`` must equal the server's current version; the server rejects mismatches with
90+
HTTP 409. Call ``retrieve(agent_id)`` first to obtain the current version — the SDK does
91+
not auto-resolve it.
92+
"""
9593
body = AgentUpdateParams(
96-
version=current.version,
94+
version=version,
9795
name=name,
9896
description=description,
9997
model=model,
@@ -229,6 +227,7 @@ async def update(
229227
self,
230228
agent_id: str,
231229
*,
230+
version: int,
232231
name: Optional[str] = None,
233232
description: Optional[str] = None,
234233
model: Optional[str] = None,
@@ -238,9 +237,14 @@ async def update(
238237
skills: Optional[Sequence[Mapping[str, Any]]] = None,
239238
metadata: Optional[Mapping[str, Any]] = None,
240239
) -> Agent:
241-
current = await self.retrieve(agent_id)
240+
"""Update the latest version of an agent.
241+
242+
``version`` must equal the server's current version; the server rejects mismatches with
243+
HTTP 409. Call ``retrieve(agent_id)`` first to obtain the current version — the SDK does
244+
not auto-resolve it.
245+
"""
242246
body = AgentUpdateParams(
243-
version=current.version,
247+
version=version,
244248
name=name,
245249
description=description,
246250
model=model,

dashscope/agentstudio/resources/session_events.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
Sequence,
1515
)
1616

17-
from dashscope.agentstudio import exceptions
1817
from dashscope.agentstudio.pagination import (
1918
AsyncCursorPage,
2019
CursorPage,
@@ -132,22 +131,6 @@ def stream(
132131
stream=True,
133132
timeout=timeout or AGENTSTUDIO_DEFAULT_TIMEOUT,
134133
)
135-
if resp.status_code >= 400:
136-
try:
137-
resp.read()
138-
body = resp.json()
139-
except ValueError:
140-
body = {"raw": resp.text}
141-
try:
142-
raise exceptions.from_response(
143-
status_code=resp.status_code,
144-
body=body,
145-
headers=resp.headers,
146-
)
147-
finally:
148-
resp.close()
149-
# Transport is shared -- stream close only closes
150-
# the response, NOT the transport.
151134
return _TypedEventStream(
152135
EventStream(response=resp),
153136
)
@@ -294,22 +277,6 @@ async def stream(
294277
stream=True,
295278
timeout=timeout or AGENTSTUDIO_DEFAULT_TIMEOUT,
296279
)
297-
if resp.status_code >= 400:
298-
try:
299-
await resp.aread()
300-
body = resp.json()
301-
except ValueError:
302-
body = {"raw": resp.text}
303-
try:
304-
raise exceptions.from_response(
305-
status_code=resp.status_code,
306-
body=body,
307-
headers=resp.headers,
308-
)
309-
finally:
310-
await resp.aclose()
311-
# Transport is shared -- stream close only closes
312-
# the response, NOT the transport.
313280
return _AioTypedEventStream(
314281
AsyncEventStream(response=resp),
315282
)

dashscope/agentstudio/transport.py

Lines changed: 37 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
(async) so the rest of the SDK can speak in plain Python data structures.
77
The transport layer is responsible for:
88
9-
* injecting auth / workspace / uid / x-request-id headers,
9+
* injecting auth / workspace / uid headers,
1010
* JSON encoding / decoding the request and response bodies,
1111
* converting non-2xx responses into :class:`AgentStudioError`,
1212
* retry-on-network-error with bounded exponential backoff.
@@ -22,7 +22,6 @@
2222
import random
2323
import socket
2424
import time
25-
import uuid
2625
from dataclasses import dataclass
2726
from typing import Any, Dict, IO, Mapping, Optional, Tuple, Union
2827

@@ -132,15 +131,6 @@ def is_error_payload(payload: Any) -> bool:
132131
return False
133132

134133

135-
def _new_request_id() -> str:
136-
"""Generate ``req_<26-char ULID-ish>``.
137-
138-
We use a UUID4 substring rather than an actual ULID to avoid an extra
139-
runtime dependency – the backend treats this header opaquely.
140-
"""
141-
142-
return "req_" + uuid.uuid4().hex[:26]
143-
144134

145135
def _resolve_timeout(
146136
timeout: Union[float, httpx.Timeout, Tuple[float, float], None],
@@ -381,31 +371,31 @@ def request( # pylint: disable=too-many-branches
381371
if attempt >= self.max_retries or not should_retry:
382372
raise exceptions.APIConnectionError(str(exc)) from exc
383373
else:
384-
if not stream and _should_retry(
385-
resp.status_code,
386-
resp.headers,
374+
if stream:
375+
if resp.status_code >= 400:
376+
resp.read()
377+
else:
378+
return resp
379+
if _should_retry(
380+
resp.status_code, resp.headers,
387381
):
388382
last_exc = exceptions.APIStatusError(
389383
f"HTTP {resp.status_code}",
390384
status_code=resp.status_code,
391385
)
392-
if attempt >= self.max_retries:
393-
try:
394-
return self._parse(resp)
395-
finally:
396-
resp.close()
397-
wait = _get_retry_after(resp.headers) or _backoff(attempt)
398-
resp.close()
399-
time.sleep(wait)
400-
attempt += 1
401-
continue
402-
if stream:
403-
return resp
386+
if attempt < self.max_retries:
387+
wait = (
388+
_get_retry_after(resp.headers)
389+
or _backoff(attempt)
390+
)
391+
resp.close()
392+
time.sleep(wait)
393+
attempt += 1
394+
continue
404395
try:
405396
return self._parse(resp)
406397
finally:
407-
if not stream:
408-
resp.close()
398+
resp.close()
409399
time.sleep(_backoff(attempt))
410400
attempt += 1
411401
raise exceptions.APIConnectionError(
@@ -470,9 +460,9 @@ def __exit__(
470460
self.close()
471461

472462
def __del__(self) -> None:
473-
if self.is_closed:
474-
return
475463
try:
464+
if self.is_closed:
465+
return
476466
self.close()
477467
except Exception:
478468
pass
@@ -605,31 +595,31 @@ async def request( # pylint: disable=too-many-branches
605595
if attempt >= self.max_retries or not should_retry:
606596
raise exceptions.APIConnectionError(str(exc)) from exc
607597
else:
608-
if not stream and _should_retry(
609-
resp.status_code,
610-
resp.headers,
598+
if stream:
599+
if resp.status_code >= 400:
600+
await resp.aread()
601+
else:
602+
return resp
603+
if _should_retry(
604+
resp.status_code, resp.headers,
611605
):
612606
last_exc = exceptions.APIStatusError(
613607
f"HTTP {resp.status_code}",
614608
status_code=resp.status_code,
615609
)
616-
if attempt >= self.max_retries:
617-
try:
618-
return await self._parse(resp)
619-
finally:
620-
await resp.aclose()
621-
wait = _get_retry_after(resp.headers) or _backoff(attempt)
622-
await resp.aclose()
623-
await asyncio.sleep(wait)
624-
attempt += 1
625-
continue
626-
if stream:
627-
return resp
610+
if attempt < self.max_retries:
611+
wait = (
612+
_get_retry_after(resp.headers)
613+
or _backoff(attempt)
614+
)
615+
await resp.aclose()
616+
await asyncio.sleep(wait)
617+
attempt += 1
618+
continue
628619
try:
629620
return await self._parse(resp)
630621
finally:
631-
if not stream:
632-
await resp.aclose()
622+
await resp.aclose()
633623
await asyncio.sleep(_backoff(attempt))
634624
attempt += 1
635625
raise exceptions.APIConnectionError(

tests/unit/test_agentstudio_protocol.py

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,9 @@
2020

2121
from dashscope.agentstudio import exceptions
2222
from dashscope.agentstudio.transport import is_error_payload, unwrap
23-
from dashscope.agentstudio.types import (
24-
user_message,
25-
user_interrupt,
26-
user_tool_confirmation,
27-
user_custom_tool_result,
28-
user_define_outcome,
29-
)
30-
23+
from dashscope.agentstudio.types import (user_custom_tool_result,
24+
user_define_outcome, user_interrupt,
25+
user_message, user_tool_confirmation)
3126

3227
# ---------------------------------------------------------------------------
3328
# 1. error.{code, message}
@@ -276,3 +271,66 @@ def test_from_response_spring_default():
276271
}
277272
err = exceptions.from_response(status_code=404, body=body)
278273
assert isinstance(err, exceptions.NotFoundError)
274+
275+
276+
# ---------------------------------------------------------------------------
277+
# 4. agents.update version contract (no auto-retrieve)
278+
# ---------------------------------------------------------------------------
279+
280+
281+
class _RecordingTransport:
282+
"""Minimal transport that records requests and returns a canned agent."""
283+
284+
def __init__(self):
285+
self.calls = []
286+
287+
def request(self, method, path, **kwargs):
288+
self.calls.append({"method": method, "path": path, **kwargs})
289+
from dashscope.agentstudio.transport import APIResponse
290+
291+
return APIResponse(
292+
data={"id": "agent_1", "version": 3, "name": "demo"},
293+
request_id="req_1",
294+
)
295+
296+
297+
def _client_with_recording_transport():
298+
from dashscope.agentstudio import Client
299+
300+
c = Client(api_key="test-key", base_url="http://test")
301+
c.transport = _RecordingTransport()
302+
return c
303+
304+
305+
def test_agents_update_requires_version_kwarg():
306+
"""version is a required keyword-only argument — omitting it is a TypeError."""
307+
client = _client_with_recording_transport()
308+
with pytest.raises(TypeError):
309+
client.agents.update("agent_1", name="new-name") # type: ignore[call-arg]
310+
311+
312+
def test_agents_update_with_version_sends_body():
313+
"""update(agent_id, version=...) sends POST /agents/{id} with version in body."""
314+
client = _client_with_recording_transport()
315+
client.agents.update("agent_1", version=3, name="new-name")
316+
assert len(client.transport.calls) == 1
317+
call = client.transport.calls[0]
318+
assert call["method"] == "POST"
319+
assert call["path"] == "/agents/agent_1"
320+
body = call["json"]
321+
assert body["version"] == 3
322+
assert body["name"] == "new-name"
323+
324+
325+
def test_agents_update_does_not_auto_retrieve():
326+
"""SDK must NOT call retrieve() internally — caller manages version now.
327+
328+
Mirrors the Anthropic Managed Agents SDK: the server only accepts updates
329+
against the latest version (409 on mismatch), so the caller is responsible
330+
for retrieving the current version first.
331+
"""
332+
client = _client_with_recording_transport()
333+
client.agents.update("agent_1", version=3, name="new-name")
334+
# Only the POST should be recorded, no preceding GET.
335+
assert len(client.transport.calls) == 1
336+
assert client.transport.calls[0]["method"] == "POST"

0 commit comments

Comments
 (0)