Skip to content

Commit cea253b

Browse files
[agentserver] core: pre-commit cleanup — pylint/mypy/pyright clean
This commit brings core to pristine pre-commit state for the spec-016 landing (no new findings introduced by this PR): Pylint: - All findings in new files (`_client.py`, `_task_api_logging_policy.py`) and modified files (`_manager.py`, `_decorator.py`, `_context.py`, `_metadata.py`, `_local_provider.py`, `_resume_route.py`, `_base.py`, `durable/__init__.py`) are now resolved. Net delta vs origin/main: 0. Rating now 10.00/10. Fixes include: filled in docstring :param/:type/:return/:rtype where missing; added `# pylint: disable=broad-exception-caught` on defensive parse paths; added `# pylint: disable=protected-access` on cross-module internal-state reads; renamed redefined exception variables (`exc` → `transport_exc`) in `_handle_failure`; collapsed dead unused-variable assignments in `_execute_task_loop` and `_try_drain_steering`; added too-many-* disables to functions that intentionally exceed limits (`AgentServerHost.__init__`, `get_active_run`, `_execute_task_loop`, `_try_drain_steering`, `LocalFileTaskProvider.update`, `_handle_resume_request`, `_lifecycle_start_inner`, `TaskManager`). - `EtagConflict` removed from the public `__init__.py` re-exports (it was already an internal-only exception per the spec 015 closeout note in `test_contract_completeness.py`). `test_steering.py` updated to import it directly from `_exceptions`. - `_base.py`: removed the inner-scope reimports of `asyncio` and `signal` in `_serve_with_shutdown_trigger` (use the top-level imports that were already present). Mypy: - Sample files (`durable_streaming.py`, `durable_source.py`, `durable_retry.py`) were calling a stale API (`host._task_manager`, `Task.run()` without required `task_id=` keyword) — fixed to use `get_task_manager()` and pass `task_id=`. The `durable_source.py` sample also referenced a non-existent `source=` decorator parameter — rewritten to use `tags=` (the existing provenance facility). Also dropped the corresponding misleading "Source tracking" bullet from the `durable/__init__.py` module docstring. - Net new mypy errors: 0 (only the pre-existing `selfhosted_invocation.py` attr-defined error remains, identical to origin/main baseline). Pyright: - `_decorator.py` `input_type` assignment: added `type: ignore` on the `Any | type[Any]` narrowing. - `_local_provider.py` status assignment from `TaskCreateRequest.status`: added `type: ignore`. - `_run.py` `self._status`: explicit `TaskStatus` annotation. - Pyright check passes overall. Sphinx: passes. Tests: core 439 passed (+ 6 skipped); invocations 244 passed (+ 2 skipped) — no regressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7dbc8f0 commit cea253b

15 files changed

Lines changed: 239 additions & 116 deletions

File tree

sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_base.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ def _read_task_manager_shutdown_grace() -> float:
4848
handlers need to checkpoint — for example the conformance suite
4949
runs with a 1s grace so the in-process shutdown marker fires
5050
before the handler completes naturally.
51+
52+
:return: Grace period in seconds (non-negative).
53+
:rtype: float
5154
"""
5255
raw = (
5356
os.environ.get("AGENTSERVER_TASK_MANAGER_SHUTDOWN_GRACE_SECONDS")
@@ -184,7 +187,7 @@ class MyHost(InvocationAgentServerHost, ResponsesAgentServerHost):
184187

185188
_DEFAULT_ACCESS_LOG_FORMAT = '%(h)s "%(r)s" %(s)s %(b)s %(D)sμs'
186189

187-
def __init__(
190+
def __init__( # pylint: disable=too-many-statements
188191
self,
189192
*,
190193
applicationinsights_connection_string: Optional[str] = None,
@@ -516,11 +519,8 @@ async def _serve_with_shutdown_trigger() -> None:
516519
get to fire pre-shutdown callbacks synchronously on signal
517520
receipt, before Hypercorn begins its graceful drain.
518521
"""
519-
import asyncio as _asyncio # pylint: disable=do-not-import-asyncio,import-outside-toplevel
520-
import signal as _signal # pylint: disable=import-outside-toplevel
521-
522-
loop = _asyncio.get_event_loop()
523-
signal_event = _asyncio.Event()
522+
loop = asyncio.get_event_loop()
523+
signal_event = asyncio.Event()
524524

525525
def _on_signal() -> None:
526526
# Run pre-shutdown callbacks BEFORE setting the event so
@@ -533,12 +533,12 @@ def _on_signal() -> None:
533533
signal_event.set()
534534

535535
for signal_name in ("SIGINT", "SIGTERM", "SIGBREAK"):
536-
if hasattr(_signal, signal_name):
536+
if hasattr(signal, signal_name):
537537
try:
538-
loop.add_signal_handler(getattr(_signal, signal_name), _on_signal)
538+
loop.add_signal_handler(getattr(signal, signal_name), _on_signal)
539539
except NotImplementedError:
540540
# Windows fallback — install via signal.signal directly.
541-
_signal.signal(getattr(_signal, signal_name), lambda *_: _on_signal())
541+
signal.signal(getattr(signal, signal_name), lambda *_: _on_signal())
542542

543543
await _hypercorn_serve(self, config, shutdown_trigger=signal_event.wait) # type: ignore[arg-type]
544544

sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/durable/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
backoff (see :class:`RetryPolicy` presets).
1818
- **Streaming** — emit incremental output via ``ctx.stream()`` and consume
1919
with ``async for chunk in task_run``.
20-
- **Source tracking** — attach immutable provenance metadata at task
21-
creation time via the ``source`` parameter.
2220
2321
Public API::
2422
@@ -43,7 +41,6 @@
4341
from ._context import EntryMode, TaskContext
4442
from ._decorator import Task, task
4543
from ._exceptions import (
46-
EtagConflict,
4744
LastInputIdPreconditionFailed,
4845
SteeringQueueFull,
4946
TaskCancelled,

sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/durable/_client.py

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def __init__(
9393
self.body_prefix = body_prefix
9494

9595

96-
def _classify_store_write_error(
96+
def _classify_store_write_error( # pylint: disable=too-many-return-statements
9797
status_code: int, body: bytes | None
9898
) -> ClassifiedOutcome:
9999
"""Classify a non-success task-store response per spec 016 FR-006.
@@ -106,6 +106,13 @@ def _classify_store_write_error(
106106
raises from inside the classifier; misshapen evictions are downgraded
107107
to ``"conflict"`` so the framework never invents an eviction event
108108
from noise (guard against false-positive evictions).
109+
110+
:param status_code: HTTP status code from the response.
111+
:type status_code: int
112+
:param body: Raw response body bytes, or ``None`` if no body.
113+
:type body: bytes | None
114+
:return: Classification outcome for the response.
115+
:rtype: ClassifiedOutcome
109116
"""
110117
# Transient: server-side problems, throttling, timeouts.
111118
if status_code in (408, 429) or 500 <= status_code < 600:
@@ -145,12 +152,19 @@ def _body_prefix(body: bytes | None, limit: int = _BODY_PREFIX_LIMIT) -> str | N
145152
Tolerant of non-UTF-8 (uses ``errors="replace"``) and non-bytes input.
146153
Used by the classified-error path so operators can see the start of a
147154
non-JSON response without dumping the whole body to logs.
155+
156+
:param body: Raw bytes from the response, or ``None``.
157+
:type body: bytes | None
158+
:param limit: Maximum characters to include in the prefix.
159+
:type limit: int
160+
:return: A truncated decoded prefix, or ``None`` if ``body`` is empty.
161+
:rtype: str | None
148162
"""
149163
if not body:
150164
return None
151165
try:
152166
text = bytes(body).decode("utf-8", errors="replace")
153-
except Exception: # noqa: BLE001
167+
except Exception: # pylint: disable=broad-exception-caught # noqa: BLE001
154168
return None
155169
if len(text) > limit:
156170
return text[:limit] + "…"
@@ -164,12 +178,19 @@ def _maybe_decompress(body: bytes | None, headers: Any) -> bytes | None:
164178
pipeline (FR-030), each call site is responsible for honoring
165179
``Content-Encoding``. Returns ``body`` unchanged for other encodings
166180
so the caller's defensive JSON-parse can produce a useful error.
181+
182+
:param body: Raw response bytes, or ``None``.
183+
:type body: bytes | None
184+
:param headers: Response headers (any mapping-like object).
185+
:type headers: Any
186+
:return: Decompressed body if applicable, otherwise ``body`` unchanged.
187+
:rtype: bytes | None
167188
"""
168189
if not body or not headers:
169190
return body
170191
try:
171192
encoding = headers.get("Content-Encoding") or headers.get("content-encoding")
172-
except Exception: # noqa: BLE001
193+
except Exception: # pylint: disable=broad-exception-caught # noqa: BLE001
173194
return body
174195
if not encoding:
175196
return body
@@ -195,7 +216,14 @@ def _parse_json_body(
195216
:class:`TransportClassifiedError` carrying the classification, the
196217
request id (if any), and a truncated body prefix.
197218
198-
Returns the parsed JSON value on success.
219+
:param response: The pipeline response object.
220+
:type response: Any
221+
:keyword method: HTTP method of the originating request (for error context).
222+
:paramtype method: str
223+
:keyword url: Request URL (for error context).
224+
:paramtype url: str
225+
:return: The parsed JSON value on success.
226+
:rtype: Any
199227
"""
200228
status = getattr(response, "status_code", 0)
201229
headers = getattr(response, "headers", {}) or {}
@@ -251,12 +279,19 @@ def _raise_classified(
251279
Replaces the legacy ``response.raise_for_status()`` call sites
252280
(spec 016 FR-032) so every non-success response funnels through
253281
the FR-006 classifier and carries the canonical outcome label.
282+
283+
:param response: The pipeline response object.
284+
:type response: Any
285+
:keyword method: HTTP method of the originating request (for error context).
286+
:paramtype method: str
287+
:keyword url: Request URL (for error context).
288+
:paramtype url: str
254289
"""
255290
status = getattr(response, "status_code", 0)
256291
headers = getattr(response, "headers", {}) or {}
257292
try:
258293
raw = response.body()
259-
except Exception: # noqa: BLE001
294+
except Exception: # pylint: disable=broad-exception-caught # noqa: BLE001
260295
raw = None
261296
body = _maybe_decompress(raw, headers) if raw else None
262297
classification = _classify_store_write_error(status, body)
@@ -288,6 +323,11 @@ def _build_default_policies(
288323
289324
``ContentDecodePolicy`` is intentionally NOT included — see module
290325
docstring for the responses-storage gzip lesson.
326+
327+
:param credential: Async token credential for the bearer-token policy.
328+
:type credential: AsyncTokenCredential
329+
:return: The default ordered policy chain.
330+
:rtype: list[Any]
291331
"""
292332
return [
293333
RequestIdPolicy(),
@@ -337,7 +377,7 @@ def __init__(
337377
) -> None:
338378
self._base_url = f"{project_endpoint.rstrip('/')}/tasks"
339379
self._credential = credential
340-
config = Configuration()
380+
config: Configuration = Configuration()
341381
config.user_agent_policy = UserAgentPolicy(base_user_agent=_USER_AGENT)
342382
self._policies: list[Any] = _build_default_policies(credential)
343383
self._client: AsyncPipelineClient = AsyncPipelineClient(
@@ -349,16 +389,25 @@ def __init__(
349389

350390
@property
351391
def policies(self) -> list[Any]:
352-
"""The policy chain in order — used by tests for composition assertions."""
392+
"""The policy chain in order — used by tests for composition assertions.
393+
394+
:return: A shallow copy of the configured policy chain.
395+
:rtype: list[Any]
396+
"""
353397
return list(self._policies)
354398

355399
async def _send(self, request: HttpRequest) -> Any:
356400
"""Send ``request`` through the pipeline and return the HTTP response.
357401
358402
The pipeline returns a ``PipelineResponse`` whose
359403
``http_response`` is the wire response we operate on.
404+
405+
:param request: The HTTP request to send.
406+
:type request: HttpRequest
407+
:return: The wire HTTP response.
408+
:rtype: Any
360409
"""
361-
pipeline_response = await self._client._pipeline.run(request) # noqa: SLF001
410+
pipeline_response = await self._client._pipeline.run(request) # pylint: disable=protected-access # noqa: SLF001
362411
return pipeline_response.http_response
363412

364413
async def create(self, request: TaskCreateRequest) -> TaskInfo:
@@ -523,7 +572,25 @@ async def list(
523572
tag: dict[str, str] | None = None,
524573
source_type: str | None = None,
525574
) -> list[TaskInfo]:
526-
"""List tasks via GET /tasks with automatic cursor pagination."""
575+
"""List tasks via GET /tasks with automatic cursor pagination.
576+
577+
:keyword agent_name: Filter to tasks owned by this agent name.
578+
:paramtype agent_name: str
579+
:keyword session_id: Filter to tasks for this session ID.
580+
:paramtype session_id: str
581+
:keyword status: Optional status filter (``pending``,
582+
``in_progress``, ``suspended``, ``completed``).
583+
:paramtype status: TaskStatus | None
584+
:keyword lease_owner: Optional lease-owner string filter.
585+
:paramtype lease_owner: str | None
586+
:keyword tag: Optional tag-equality filter (all key/value pairs
587+
must match).
588+
:paramtype tag: dict[str, str] | None
589+
:keyword source_type: Optional source-type filter.
590+
:paramtype source_type: str | None
591+
:return: All matching tasks across all pages.
592+
:rtype: list[TaskInfo]
593+
"""
527594
params: dict[str, str] = {
528595
"api-version": _API_VERSION,
529596
"agent_name": agent_name,

sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/durable/_context.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def pending_input_count(self) -> int:
168168
return 0
169169
try:
170170
return int(self._pending_count_provider())
171-
except Exception: # noqa: BLE001
171+
except Exception: # pylint: disable=broad-exception-caught # noqa: BLE001
172172
return 0
173173

174174
async def suspend(
@@ -231,6 +231,7 @@ async def exit_for_recovery(self) -> Any:
231231
Use as ``return await ctx.exit_for_recovery()``.
232232
233233
:return: The :class:`_ExitForRecovery` sentinel.
234+
:rtype: Any
234235
:raises RuntimeError: If called outside ``ctx.shutdown.is_set() == True``.
235236
"""
236237
if not self.shutdown.is_set():

sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/durable/_decorator.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ async def my_task(ctx: TaskContext[MyInput]) -> MyOutput:
3939
from ._result import TaskResult
4040
from ._retry import RetryPolicy
4141
from ._run import TaskRun
42-
from ._stream import StreamHandler, StreamHandlerFactory
42+
from ._stream import StreamHandler, StreamHandlerFactory # noqa: F401 # pylint: disable=unused-import
4343

4444
if TYPE_CHECKING:
4545
from ._models import TaskStatus
@@ -133,7 +133,7 @@ def _extract_generic_args(
133133

134134
ctx_hint = hints[ctx_param.name]
135135
args = get_args(ctx_hint)
136-
input_type: type[Any] = args[0] if args else Any
136+
input_type: type[Any] = args[0] if args else Any # type: ignore[assignment]
137137

138138
return_hint = hints.get("return", Any)
139139
# Unwrap Optional, Awaitable, etc.
@@ -817,7 +817,17 @@ def _create_steering_ack_run(
817817
task_id: str,
818818
future: Any,
819819
) -> TaskRun[Output]:
820-
"""Create a TaskRun for a queued steering input."""
820+
"""Create a TaskRun for a queued steering input.
821+
822+
:param manager: The task manager owning the active execution.
823+
:type manager: Any
824+
:param task_id: Stable task identifier.
825+
:type task_id: str
826+
:param future: Future that will resolve with the next-turn outcome.
827+
:type future: Any
828+
:return: A :class:`TaskRun` whose result resolves with the queued turn.
829+
:rtype: TaskRun[Output]
830+
"""
821831
return TaskRun(
822832
task_id=task_id,
823833
provider=manager.provider,
@@ -857,9 +867,6 @@ async def _lifecycle_start( # pylint: disable=too-many-locals
857867
from ._exceptions import ( # pylint: disable=import-outside-toplevel
858868
TaskConflictError,
859869
)
860-
from ._manager import ( # pylint: disable=import-outside-toplevel
861-
get_task_manager,
862-
)
863870

864871
# Spec 016 FR-008 (US2): orphan-sandbox eviction at scheduling
865872
# entry points MUST surface as TaskConflictError(current_status=
@@ -879,7 +886,7 @@ async def _lifecycle_start( # pylint: disable=too-many-locals
879886
raise TaskConflictError(task_id, "in_progress") from exc
880887
raise
881888

882-
async def _lifecycle_start_inner( # pylint: disable=too-many-locals
889+
async def _lifecycle_start_inner( # pylint: disable=too-many-locals,too-many-statements
883890
self,
884891
*,
885892
task_id: str,
@@ -891,6 +898,19 @@ async def _lifecycle_start_inner( # pylint: disable=too-many-locals
891898
892899
Split out so the outer wrapper can convert spec 016 FR-008 evictions
893900
to ``TaskConflictError`` without indenting the entire body.
901+
902+
:keyword task_id: Stable task identifier (same as outer method).
903+
:paramtype task_id: str
904+
:keyword input: Input value for the task (same as outer method).
905+
:paramtype input: Input
906+
:keyword input_id: Optional input identifier for sequential-input
907+
acceptance preconditions (same as outer method).
908+
:paramtype input_id: str | None
909+
:keyword if_last_input_id: Optional if-match precondition on the
910+
last persisted ``input_id`` (same as outer method).
911+
:paramtype if_last_input_id: str | None
912+
:return: A :class:`TaskRun` handle for the started task.
913+
:rtype: TaskRun[Output]
894914
"""
895915
from ._exceptions import ( # pylint: disable=import-outside-toplevel
896916
TaskConflictError,
@@ -1081,9 +1101,8 @@ async def _lifecycle_start_inner( # pylint: disable=too-many-locals
10811101
)
10821102
if self._opts.steerable:
10831103
# Steering path: append input to queue, signal cancel, return ack
1084-
ack_future = manager._register_steering_future(
1085-
task_id
1086-
) # pylint: disable=protected-access
1104+
# pylint: disable=protected-access
1105+
ack_future = manager._register_steering_future(task_id)
10871106
await self._append_steering_input(
10881107
manager,
10891108
task_id=task_id,
@@ -1093,9 +1112,8 @@ async def _lifecycle_start_inner( # pylint: disable=too-many-locals
10931112
if_last_input_id=if_last_input_id,
10941113
)
10951114
# Set cancel on in-memory context if task runs in this process
1096-
active = manager._active_tasks.get(
1097-
task_id
1098-
) # pylint: disable=protected-access
1115+
active = manager._active_tasks.get(task_id)
1116+
# pylint: enable=protected-access
10991117
if active:
11001118
active.context.cancel.set()
11011119
return self._create_steering_ack_run(manager, task_id, ack_future)

sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/durable/_local_provider.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ async def create(self, request: TaskCreateRequest) -> TaskInfo:
121121

122122
lease: LeaseInfo | None = None
123123
started_at: str | None = None
124-
status: TaskStatus = request.status
124+
status: TaskStatus = request.status # type: ignore[assignment]
125125

126126
if (
127127
request.lease_owner
@@ -174,9 +174,9 @@ async def get(self, task_id: str) -> TaskInfo | None:
174174
return None
175175
return self._read_task(path)
176176

177-
async def update(
177+
async def update( # pylint: disable=too-many-branches,too-many-statements
178178
self, task_id: str, patch: TaskPatchRequest
179-
) -> TaskInfo: # pylint: disable=too-many-branches,too-many-statements
179+
) -> TaskInfo:
180180
"""Update a task via PATCH semantics.
181181
182182
:param task_id: The task identifier.

0 commit comments

Comments
 (0)