Parcourir la source

fix: harden generic event kernel boundaries

Problem: partial schema validation and unguarded resolver/fallback/handler boundaries could leak exceptions or accept invalid event arguments. Risk: standard JSON Schema validation and async facade checks may reject definitions or calls previously tolerated; EventAgent concurrency is restored pending bounded scheduling.
zhenyu.hu il y a 2 semaines
Parent
commit
0bac6ce86e

+ 1 - 0
pyproject.toml

@@ -7,6 +7,7 @@ requires-python = ">=3.11"
 dependencies = [
     "fastapi>=0.111.0",
     "httpx>=0.27.0",
+    "jsonschema>=4.26.0",
     "pydantic-settings>=2.3.0",
     "uvicorn[standard]>=0.30.0",
     "websockets>=12.0",

+ 14 - 8
src/agent_lab/application/event_agent.py

@@ -1,3 +1,4 @@
+import asyncio
 import json
 from collections.abc import AsyncIterator, Iterable, Sequence
 from dataclasses import dataclass
@@ -9,6 +10,7 @@ from agent_lab.application.events import (
     EventExecutionContext,
     EventKernel,
     EventRequest,
+    ResolvedEventArguments,
 )
 from agent_lab.application.tools import (
     ToolExecutionContext,
@@ -91,17 +93,17 @@ class EventAgent:
     ) -> list[ChatMessage]:
         for event in events:
             self._raw_chunks_by_event[event.id] = []
-        replies: list[ChatMessage] = []
-        for event in events:
-            replies.append(
-                await self.handle(
+        return await asyncio.gather(
+            *[
+                self.handle(
                     event,
                     history=history,
                     system_prompt=system_prompt,
                     extra_body=extra_body,
                 )
-            )
-        return replies
+                for event in events
+            ]
+        )
 
     def raw_model_chunks(
         self,
@@ -121,7 +123,7 @@ class EventAgent:
         definition: EventDefinition,
         request: EventRequest,
         context: EventExecutionContext,
-    ) -> dict[str, Any] | None:
+    ) -> ResolvedEventArguments | None:
         assert self.chat_client is not None
         tool = self.registry.tool_schema(request.name)
         if tool is None:
@@ -158,7 +160,11 @@ class EventAgent:
 
             if item.kind == "provider_tool_call" and item.event is not None:
                 self._raw_chunks_by_event[event.id] = raw_chunks
-                return dict(item.event.arguments)
+                return ResolvedEventArguments(
+                    event_name=item.event.name,
+                    arguments=dict(item.event.arguments),
+                    raw_arguments=item.event.raw_arguments,
+                )
 
         self._raw_chunks_by_event[event.id] = raw_chunks
         return None

+ 2 - 0
src/agent_lab/application/events/__init__.py

@@ -9,6 +9,7 @@ from agent_lab.application.events.models import (
     EventResult,
     EventSource,
     EventStatus,
+    ResolvedEventArguments,
     ResultPolicy,
     RiskLevel,
 )
@@ -27,6 +28,7 @@ __all__ = [
     "EventResult",
     "EventSource",
     "EventStatus",
+    "ResolvedEventArguments",
     "ResultPolicy",
     "RiskLevel",
 ]

+ 248 - 57
src/agent_lab/application/events/kernel.py

@@ -1,10 +1,13 @@
 from __future__ import annotations
 
 import inspect
+import json
 from collections.abc import Awaitable, Callable, Iterable
 from dataclasses import replace
 from typing import Any
 
+from jsonschema.exceptions import ValidationError
+
 from agent_lab.application.events.models import (
     EventDefinition,
     EventExecutionContext,
@@ -12,13 +15,15 @@ from agent_lab.application.events.models import (
     EventResult,
     EventSource,
     EventStatus,
+    ResolvedEventArguments,
 )
 from agent_lab.application.events.registry import EventRegistry
 
 
+ArgumentFallbackResult = ResolvedEventArguments | None
 ArgumentFallback = Callable[
     [EventDefinition, EventRequest, EventExecutionContext],
-    Awaitable[dict[str, Any] | None],
+    ArgumentFallbackResult | Awaitable[ArgumentFallbackResult],
 ]
 
 
@@ -43,12 +48,17 @@ class EventKernel:
             return early_result
         assert definition is not None
         resolved_context = context or EventExecutionContext()
-        arguments = self._deterministic_arguments(
+
+        resolved, resolution_error = self._resolve_deterministic(
             definition,
             request,
             resolved_context,
         )
-        validation_error = self._validation_error(definition, arguments)
+        if resolution_error is not None:
+            return resolution_error
+        assert resolved is not None
+
+        validation_error = self._validation_error(definition, resolved.arguments)
         used_fallback = False
         if (
             validation_error is not None
@@ -57,27 +67,56 @@ class EventKernel:
             and self.argument_fallback is not None
         ):
             used_fallback = True
-            fallback_request = replace(request, arguments=arguments)
-            fallback_arguments = await self.argument_fallback(
+            fallback_request = replace(
+                request,
+                arguments=resolved.arguments,
+                raw_arguments=resolved.raw_arguments,
+            )
+            try:
+                fallback_value = self.argument_fallback(
+                    definition,
+                    fallback_request,
+                    resolved_context,
+                )
+                if inspect.isawaitable(fallback_value):
+                    fallback_value = await fallback_value
+            except Exception as exc:
+                return self._resolution_error(
+                    definition,
+                    request,
+                    f"event argument fallback failed: {exc}",
+                    resolved=resolved,
+                    used_fallback=True,
+                )
+            if fallback_value is not None:
+                resolved, fallback_error = self._normalize_fallback(
+                    definition,
+                    request,
+                    fallback_value,
+                )
+                if fallback_error is not None:
+                    return fallback_error
+                assert resolved is not None
+            validation_error = self._validation_error(
                 definition,
-                fallback_request,
-                resolved_context,
+                resolved.arguments,
             )
-            if fallback_arguments is not None:
-                arguments = fallback_arguments
-            validation_error = self._validation_error(definition, arguments)
 
         if validation_error is not None:
             return self._result(
                 definition,
                 request,
                 EventStatus.INVALID_ARGUMENTS,
-                arguments=arguments,
+                resolved=resolved,
                 error=validation_error,
                 used_fallback=used_fallback,
             )
 
-        resolved_request = replace(request, arguments=arguments)
+        resolved_request = replace(
+            request,
+            arguments=resolved.arguments,
+            raw_arguments=resolved.raw_arguments,
+        )
         try:
             payload = definition.handler(resolved_request)
             if inspect.isawaitable(payload):
@@ -87,15 +126,24 @@ class EventKernel:
                 definition,
                 request,
                 EventStatus.HANDLER_ERROR,
-                arguments=arguments,
+                resolved=resolved,
                 error=f"event handler failed: {exc}",
                 used_fallback=used_fallback,
             )
+        if not isinstance(payload, dict):
+            return self._result(
+                definition,
+                request,
+                EventStatus.HANDLER_ERROR,
+                resolved=resolved,
+                error="event handler returned non-object payload",
+                used_fallback=used_fallback,
+            )
         return self._result(
             definition,
             request,
             EventStatus.SUCCESS,
-            arguments=arguments,
+            resolved=resolved,
             payload=dict(payload),
             used_fallback=used_fallback,
         )
@@ -111,37 +159,69 @@ class EventKernel:
         if early_result is not None:
             return early_result
         assert definition is not None
-        arguments = self._deterministic_arguments(
+        resolved, resolution_error = self._resolve_deterministic(
             definition,
             request,
             context or EventExecutionContext(),
         )
-        validation_error = self._validation_error(definition, arguments)
+        if resolution_error is not None:
+            return resolution_error
+        assert resolved is not None
+        validation_error = self._validation_error(definition, resolved.arguments)
         if validation_error is not None:
             return self._result(
                 definition,
                 request,
                 EventStatus.INVALID_ARGUMENTS,
-                arguments=arguments,
+                resolved=resolved,
                 error=validation_error,
             )
+        if inspect.iscoroutinefunction(definition.handler):
+            return self._result(
+                definition,
+                request,
+                EventStatus.HANDLER_ERROR,
+                resolved=resolved,
+                error="event handler failed: async event handlers require EventKernel.execute",
+            )
+        resolved_request = replace(
+            request,
+            arguments=resolved.arguments,
+            raw_arguments=resolved.raw_arguments,
+        )
         try:
-            payload = definition.handler(replace(request, arguments=arguments))
-            if inspect.isawaitable(payload):
-                raise TypeError("async event handlers require EventKernel.execute")
+            payload = definition.handler(resolved_request)
         except Exception as exc:
             return self._result(
                 definition,
                 request,
                 EventStatus.HANDLER_ERROR,
-                arguments=arguments,
+                resolved=resolved,
                 error=f"event handler failed: {exc}",
             )
+        if inspect.isawaitable(payload):
+            if inspect.iscoroutine(payload):
+                payload.close()
+            return self._result(
+                definition,
+                request,
+                EventStatus.HANDLER_ERROR,
+                resolved=resolved,
+                error="event handler failed: async event handlers require EventKernel.execute",
+            )
+        if not isinstance(payload, dict):
+            return self._result(
+                definition,
+                request,
+                EventStatus.HANDLER_ERROR,
+                resolved=resolved,
+                error="event handler returned non-object payload",
+            )
         return self._result(
             definition,
             request,
             EventStatus.SUCCESS,
-            arguments=arguments,
+            resolved=resolved,
             payload=dict(payload),
         )
 
@@ -158,6 +238,7 @@ class EventKernel:
                 status=EventStatus.UNKNOWN,
                 source=request.source,
                 arguments=dict(request.arguments),
+                raw_arguments=request.raw_arguments,
                 error="unknown event",
             )
         if enabled_names is not None and request.name not in set(enabled_names):
@@ -165,56 +246,162 @@ class EventKernel:
                 definition,
                 request,
                 EventStatus.DISABLED,
-                arguments=dict(request.arguments),
+                resolved=ResolvedEventArguments(
+                    event_name=request.name,
+                    arguments=dict(request.arguments),
+                    raw_arguments=request.raw_arguments,
+                ),
                 error="event disabled",
             )
         return definition, None
 
-    def _deterministic_arguments(
+    def _resolve_deterministic(
         self,
         definition: EventDefinition,
         request: EventRequest,
         context: EventExecutionContext,
-    ) -> dict[str, Any]:
+    ) -> tuple[ResolvedEventArguments | None, EventResult | None]:
         if request.source is EventSource.PROVIDER_RESOLVED:
-            return dict(request.arguments)
-        if definition.resolver is None:
-            return dict(request.arguments)
-        return dict(definition.resolver(request, context))
+            return (
+                ResolvedEventArguments(
+                    event_name=request.name,
+                    arguments=dict(request.arguments),
+                    raw_arguments=request.raw_arguments,
+                ),
+                None,
+            )
+        try:
+            arguments = (
+                definition.resolver(request, context)
+                if definition.resolver is not None
+                else dict(request.arguments)
+            )
+        except Exception as exc:
+            return None, self._resolution_error(
+                definition,
+                request,
+                f"event argument resolver failed: {exc}",
+            )
+        if not isinstance(arguments, dict):
+            return None, self._resolution_error(
+                definition,
+                request,
+                "event argument resolver returned invalid payload",
+            )
+        try:
+            raw_arguments = self._json_arguments(arguments)
+        except (TypeError, ValueError) as exc:
+            return None, self._resolution_error(
+                definition,
+                request,
+                f"event argument resolver failed to serialize: {exc}",
+            )
+        return (
+            ResolvedEventArguments(
+                event_name=request.name,
+                arguments=dict(arguments),
+                raw_arguments=raw_arguments,
+            ),
+            None,
+        )
+
+    def _normalize_fallback(
+        self,
+        definition: EventDefinition,
+        request: EventRequest,
+        value: Any,
+    ) -> tuple[ResolvedEventArguments | None, EventResult | None]:
+        if not isinstance(value, ResolvedEventArguments):
+            return None, self._resolution_error(
+                definition,
+                request,
+                "event argument fallback returned invalid payload",
+                used_fallback=True,
+            )
+        if not isinstance(value.arguments, dict):
+            return None, self._resolution_error(
+                definition,
+                request,
+                "event argument fallback returned invalid payload",
+                used_fallback=True,
+            )
+        if value.event_name != request.name:
+            return None, self._resolution_error(
+                definition,
+                request,
+                f"fallback returned tool {value.event_name} for {request.name}",
+                resolved=value,
+                used_fallback=True,
+            )
+        try:
+            raw_arguments = json.loads(value.raw_arguments)
+        except (TypeError, json.JSONDecodeError):
+            return None, self._resolution_error(
+                definition,
+                request,
+                "fallback raw arguments are not valid JSON",
+                resolved=value,
+                used_fallback=True,
+            )
+        if raw_arguments != value.arguments:
+            return None, self._resolution_error(
+                definition,
+                request,
+                "fallback raw arguments do not match parsed arguments",
+                resolved=value,
+                used_fallback=True,
+            )
+        return value, None
 
     def _validation_error(
         self,
         definition: EventDefinition,
         arguments: dict[str, Any],
     ) -> str | None:
-        required = definition.parameters.get("required", [])
-        missing = [name for name in required if name not in arguments]
-        if missing:
-            return f"missing required arguments: {', '.join(missing)}"
-
-        properties = definition.parameters.get("properties", {})
-        for name, value in arguments.items():
-            expected_name = properties.get(name, {}).get("type")
-            if expected_name is not None and not self._matches_json_type(
-                value,
-                expected_name,
-            ):
-                return f"invalid argument type for {name}: expected {expected_name}"
+        validator = self.registry.validator(definition.name)
+        assert validator is not None
+        try:
+            validator.validate(arguments)
+        except ValidationError as exc:
+            if exc.validator == "required":
+                missing = [
+                    name
+                    for name in exc.validator_value
+                    if name not in exc.instance
+                ]
+                return f"missing required arguments: {', '.join(missing)}"
+            if exc.validator == "type" and exc.path:
+                return (
+                    f"invalid argument type for {exc.path[-1]}: "
+                    f"expected {exc.validator_value}"
+                )
+            return f"invalid event arguments: {exc.message}"
+        except Exception as exc:
+            return f"event argument validation failed: {exc}"
         return None
 
-    def _matches_json_type(self, value: Any, expected_name: str) -> bool:
-        if expected_name == "number":
-            return not isinstance(value, bool) and isinstance(value, (int, float))
-        if expected_name == "integer":
-            return not isinstance(value, bool) and isinstance(value, int)
-        python_types = {
-            "string": str,
-            "boolean": bool,
-            "object": dict,
-            "array": list,
-        }
-        expected_type = python_types.get(expected_name)
-        return expected_type is None or isinstance(value, expected_type)
+    def _resolution_error(
+        self,
+        definition: EventDefinition,
+        request: EventRequest,
+        error: str,
+        *,
+        resolved: ResolvedEventArguments | None = None,
+        used_fallback: bool = False,
+    ) -> EventResult:
+        return self._result(
+            definition,
+            request,
+            EventStatus.RESOLUTION_ERROR,
+            resolved=resolved
+            or ResolvedEventArguments(
+                event_name=request.name,
+                arguments=dict(request.arguments),
+                raw_arguments=request.raw_arguments,
+            ),
+            error=error,
+            used_fallback=used_fallback,
+        )
 
     def _result(
         self,
@@ -222,7 +409,7 @@ class EventKernel:
         request: EventRequest,
         status: EventStatus,
         *,
-        arguments: dict[str, Any],
+        resolved: ResolvedEventArguments,
         payload: dict[str, Any] | None = None,
         error: str | None = None,
         used_fallback: bool = False,
@@ -232,7 +419,8 @@ class EventKernel:
             event_name=request.name,
             status=status,
             source=request.source,
-            arguments=dict(arguments),
+            arguments=dict(resolved.arguments),
+            raw_arguments=resolved.raw_arguments,
             payload=payload or {},
             error=error,
             used_fallback=used_fallback,
@@ -245,3 +433,6 @@ class EventKernel:
             timeout_seconds=definition.timeout_seconds,
             terminal=definition.terminal,
         )
+
+    def _json_arguments(self, arguments: dict[str, Any]) -> str:
+        return json.dumps(arguments, ensure_ascii=False, separators=(",", ":"))

+ 9 - 0
src/agent_lab/application/events/models.py

@@ -18,6 +18,7 @@ class EventStatus(StrEnum):
     UNKNOWN = "unknown"
     DISABLED = "disabled"
     INVALID_ARGUMENTS = "invalid_arguments"
+    RESOLUTION_ERROR = "resolution_error"
     HANDLER_ERROR = "handler_error"
 
 
@@ -55,6 +56,13 @@ class EventRequest:
     raw_arguments: str = "{}"
 
 
+@dataclass(frozen=True)
+class ResolvedEventArguments:
+    event_name: str
+    arguments: dict[str, Any]
+    raw_arguments: str
+
+
 EventHandlerResult = dict[str, Any] | Awaitable[dict[str, Any]]
 EventHandler = Callable[[EventRequest], EventHandlerResult]
 EventArgumentResolver = Callable[
@@ -89,6 +97,7 @@ class EventResult:
     status: EventStatus
     source: EventSource
     arguments: dict[str, Any] = field(default_factory=dict)
+    raw_arguments: str = "{}"
     payload: dict[str, Any] = field(default_factory=dict)
     error: str | None = None
     used_fallback: bool = False

+ 16 - 0
src/agent_lab/application/events/registry.py

@@ -3,23 +3,39 @@ from __future__ import annotations
 from collections.abc import Iterable
 from copy import deepcopy
 
+from jsonschema import Draft202012Validator
+from jsonschema.exceptions import SchemaError
+
 from agent_lab.application.events.models import EventDefinition
 
 
 class EventRegistry:
     def __init__(self, definitions: Iterable[EventDefinition] = ()) -> None:
         self._definitions: dict[str, EventDefinition] = {}
+        self._validators: dict[str, Draft202012Validator] = {}
         for definition in definitions:
             self.register(definition)
 
     def register(self, definition: EventDefinition) -> None:
         if definition.name in self._definitions:
             raise ValueError(f"duplicate event definition: {definition.name}")
+        try:
+            Draft202012Validator.check_schema(definition.parameters)
+        except SchemaError as exc:
+            raise ValueError(
+                f"invalid event schema for {definition.name}: {exc.message}"
+            ) from None
         self._definitions[definition.name] = definition
+        self._validators[definition.name] = Draft202012Validator(
+            definition.parameters
+        )
 
     def definition(self, name: str) -> EventDefinition | None:
         return self._definitions.get(name)
 
+    def validator(self, name: str) -> Draft202012Validator | None:
+        return self._validators.get(name)
+
     def catalog(self, enabled_names: Iterable[str] | None = None) -> list[dict]:
         enabled = set(enabled_names) if enabled_names is not None else None
         return [

+ 34 - 4
src/agent_lab/application/tools.py

@@ -1,4 +1,5 @@
-from collections.abc import Callable, Iterable, Sequence
+import inspect
+from collections.abc import Awaitable, Callable, Iterable, Sequence
 from copy import deepcopy
 from dataclasses import dataclass
 from typing import Any
@@ -23,7 +24,10 @@ from agent_lab.domain.messages import ChatMessage
 ToolExecutionContext = EventExecutionContext
 
 
-ToolHandler = Callable[[ToolCallEvent], dict[str, Any]]
+ToolHandler = Callable[
+    [ToolCallEvent],
+    dict[str, Any] | Awaitable[dict[str, Any]],
+]
 ToolArgumentResolver = Callable[
     [ToolCallEvent, ToolExecutionContext],
     dict[str, Any],
@@ -110,6 +114,23 @@ class ToolRegistry:
         )
         return self.tool_payload(result)
 
+    async def handle_async(
+        self,
+        event: ToolCallEvent,
+        context: ToolExecutionContext | None = None,
+    ) -> dict[str, Any]:
+        result = await self.kernel.execute(
+            self.event_request(event),
+            context=context or ToolExecutionContext(history=()),
+        )
+        return self.tool_payload(result)
+
+    async def execute_async(self, event: ToolCallEvent) -> dict[str, Any]:
+        result = await self.kernel.execute(
+            self.event_request(event, source=EventSource.PROVIDER_RESOLVED)
+        )
+        return self.tool_payload(result)
+
     def event_request(
         self,
         event: ToolCallEvent,
@@ -133,6 +154,7 @@ class ToolRegistry:
             error = "tool disabled"
         elif result.status is EventStatus.HANDLER_ERROR:
             error = error.replace("event handler failed:", "tool handler failed:", 1)
+            error = error.replace("EventKernel.execute", "execute_async")
         return {"tool": result.event_name, "error": error}
 
     def _to_event_definition(self, definition: ToolDefinition) -> EventDefinition:
@@ -147,8 +169,16 @@ class ToolRegistry:
                 return {}
             return deepcopy(request.arguments)
 
-        def handler(request: EventRequest) -> dict[str, Any]:
-            return definition.handler(self._tool_event(request))
+        if inspect.iscoroutinefunction(definition.handler):
+
+            async def handler(request: EventRequest) -> dict[str, Any]:
+                return await definition.handler(self._tool_event(request))
+        else:
+
+            def handler(
+                request: EventRequest,
+            ) -> dict[str, Any] | Awaitable[dict[str, Any]]:
+                return definition.handler(self._tool_event(request))
 
         return EventDefinition(
             name=definition.name,

+ 102 - 0
tests/test_event_agent.py

@@ -1,3 +1,4 @@
+import asyncio
 import json
 from collections.abc import AsyncIterator
 
@@ -94,6 +95,25 @@ class WrongSourceToolCallChatClient:
         yield self.item
 
 
+class WrongNameToolCallingChatClient(ToolCallingChatClient):
+    async def stream_chat(
+        self,
+        messages: list[ChatMessage],
+        tools: list[dict],
+        params: AgentParams,
+        tool_choice: dict | None = None,
+    ) -> AsyncIterator[StreamItem]:
+        self.calls.append({"messages": list(messages), "tools": list(tools)})
+        yield StreamItem.provider_tool_call(
+            ToolCallEvent(
+                id="wrong-call",
+                name="wrong.tool",
+                arguments=self.arguments,
+                raw_arguments=json.dumps(self.arguments),
+            )
+        )
+
+
 @pytest.mark.asyncio
 async def test_event_agent_uses_deterministic_resolver_before_llm_fallback():
     chat_client = ToolCallingChatClient({"message": "LLM generated handoff"})
@@ -205,6 +225,88 @@ async def test_event_agent_calls_llm_once_for_incomplete_deterministic_arguments
     }
 
 
+@pytest.mark.asyncio
+async def test_event_agent_rejects_fallback_provider_call_for_wrong_tool_name():
+    chat_client = WrongNameToolCallingChatClient({"message": "wrong"})
+    registry = ToolRegistry(
+        [
+            ToolDefinition(
+                name="expected.tool",
+                description="Expected tool.",
+                parameters={
+                    "type": "object",
+                    "properties": {"message": {"type": "string"}},
+                    "required": ["message"],
+                },
+                handler=lambda event: {"tool": event.name},
+                argument_resolver=lambda event, context: {},
+            )
+        ]
+    )
+
+    reply = await EventAgent(
+        enabled_tools=["expected.tool"],
+        registry=registry,
+        chat_client=chat_client,
+    ).handle(
+        ToolCallEvent(
+            id="call-1",
+            name="expected.tool",
+            arguments={},
+            raw_arguments="{}",
+        )
+    )
+
+    assert json.loads(reply.content) == {
+        "tool": "expected.tool",
+        "error": "fallback returned tool wrong.tool for expected.tool",
+    }
+
+
+@pytest.mark.asyncio
+async def test_event_agent_handle_many_starts_independent_handlers_concurrently():
+    started: list[str] = []
+    both_started = asyncio.Event()
+
+    async def handler(event: ToolCallEvent) -> dict:
+        started.append(event.name)
+        if len(started) == 2:
+            both_started.set()
+        await asyncio.wait_for(both_started.wait(), timeout=0.2)
+        return {"tool": event.name}
+
+    registry = ToolRegistry(
+        [
+            ToolDefinition(
+                name=name,
+                description=f"Handle {name}.",
+                parameters={"type": "object"},
+                handler=handler,
+            )
+            for name in ("event.first", "event.second")
+        ]
+    )
+    events = [
+        ToolCallEvent(id=f"call-{index}", name=name, arguments={}, raw_arguments="{}")
+        for index, name in enumerate(("event.first", "event.second"), start=1)
+    ]
+
+    replies = await asyncio.wait_for(
+        EventAgent(
+            enabled_tools=[event.name for event in events],
+            registry=registry,
+        ).handle_many(events, history=[]),
+        timeout=0.5,
+    )
+
+    assert started == ["event.first", "event.second"]
+    assert [reply.name for reply in replies] == ["event.first", "event.second"]
+    assert [json.loads(reply.content) for reply in replies] == [
+        {"tool": "event.first"},
+        {"tool": "event.second"},
+    ]
+
+
 @pytest.mark.asyncio
 @pytest.mark.parametrize(
     "item",

+ 407 - 14
tests/test_event_kernel.py

@@ -1,5 +1,8 @@
 from __future__ import annotations
 
+import gc
+import json
+import warnings
 from typing import Any
 
 import pytest
@@ -11,6 +14,7 @@ from agent_lab.application.events import (
     EventKernel,
     EventRegistry,
     EventRequest,
+    ResolvedEventArguments,
     EventSource,
     EventStatus,
     ResultPolicy,
@@ -77,6 +81,13 @@ def test_registry_rejects_duplicate_definition_names():
         EventRegistry([_definition(), _definition()])
 
 
+def test_registry_rejects_invalid_draft_2020_12_schema():
+    definition = _definition(parameters={"type": 42})
+
+    with pytest.raises(ValueError, match="invalid event schema for example.lookup"):
+        EventRegistry([definition])
+
+
 def test_tool_registry_public_api_remains_compatible():
     registry = ToolRegistry(
         [
@@ -161,9 +172,13 @@ async def test_kernel_calls_fallback_once_when_required_arguments_are_incomplete
         definition: EventDefinition,
         request: EventRequest,
         context: EventExecutionContext,
-    ) -> dict[str, Any]:
+    ) -> ResolvedEventArguments:
         fallback_calls.append(dict(request.arguments))
-        return {"query": "resolved once"}
+        return ResolvedEventArguments(
+            event_name=definition.name,
+            arguments={"query": "resolved once"},
+            raw_arguments='{"query":"resolved once"}',
+        )
 
     registry = EventRegistry([_definition(resolver=lambda request, context: {})])
 
@@ -182,10 +197,14 @@ async def test_kernel_calls_fallback_once_when_required_arguments_are_incomplete
 async def test_kernel_does_not_fallback_when_definition_disallows_it():
     fallback_calls = 0
 
-    async def fallback(*args: Any) -> dict[str, Any]:
+    async def fallback(*args: Any) -> ResolvedEventArguments:
         nonlocal fallback_calls
         fallback_calls += 1
-        return {"query": "not allowed"}
+        return ResolvedEventArguments(
+            event_name="example.lookup",
+            arguments={"query": "not allowed"},
+            raw_arguments='{"query":"not allowed"}',
+        )
 
     registry = EventRegistry(
         [
@@ -217,10 +236,14 @@ async def test_provider_resolved_arguments_are_not_rewritten_or_fallen_back():
         resolver_calls += 1
         return {"query": "rewritten"}
 
-    async def fallback(*args: Any) -> dict[str, Any]:
+    async def fallback(*args: Any) -> ResolvedEventArguments:
         nonlocal fallback_calls
         fallback_calls += 1
-        return {"query": "fallback"}
+        return ResolvedEventArguments(
+            event_name="example.lookup",
+            arguments={"query": "fallback"},
+            raw_arguments='{"query":"fallback"}',
+        )
 
     registry = EventRegistry([_definition(resolver=resolver)])
 
@@ -244,10 +267,14 @@ async def test_provider_resolved_arguments_are_not_rewritten_or_fallen_back():
 async def test_provider_resolved_missing_arguments_return_invalid_without_fallback():
     fallback_calls = 0
 
-    async def fallback(*args: Any) -> dict[str, Any]:
+    async def fallback(*args: Any) -> ResolvedEventArguments:
         nonlocal fallback_calls
         fallback_calls += 1
-        return {"query": "fallback"}
+        return ResolvedEventArguments(
+            event_name="example.lookup",
+            arguments={"query": "fallback"},
+            raw_arguments='{"query":"fallback"}',
+        )
 
     result = await EventKernel(
         EventRegistry([_definition()]), argument_fallback=fallback
@@ -331,6 +358,363 @@ async def test_kernel_normalizes_handler_exceptions():
     assert result.error == "event handler failed: boom"
 
 
+@pytest.mark.asyncio
+async def test_kernel_validates_complete_draft_2020_12_schema():
+    definition = _definition(
+        parameters={
+            "type": "object",
+            "properties": {
+                "mode": {"enum": ["quick", "deep"]},
+                "target": {"type": ["string", "null"]},
+                "filters": {
+                    "type": "array",
+                    "items": {
+                        "type": "object",
+                        "properties": {"score": {"type": "number", "minimum": 0}},
+                        "required": ["score"],
+                        "additionalProperties": False,
+                    },
+                },
+            },
+            "required": ["mode", "target", "filters"],
+            "additionalProperties": False,
+        },
+        handler=lambda request: {"event": request.name},
+    )
+    kernel = EventKernel(EventRegistry([definition]))
+
+    valid = await kernel.execute(
+        EventRequest(
+            id="valid",
+            name=definition.name,
+            arguments={
+                "mode": "deep",
+                "target": None,
+                "filters": [{"score": 0.5}],
+            },
+            source=EventSource.PROVIDER_RESOLVED,
+        ),
+        enabled_names=[definition.name],
+    )
+    invalid = await kernel.execute(
+        EventRequest(
+            id="invalid",
+            name=definition.name,
+            arguments={
+                "mode": "other",
+                "target": 7,
+                "filters": [{"score": -1, "extra": True}],
+                "unexpected": True,
+            },
+            source=EventSource.PROVIDER_RESOLVED,
+        ),
+        enabled_names=[definition.name],
+    )
+
+    assert valid.status is EventStatus.SUCCESS
+    assert invalid.status is EventStatus.INVALID_ARGUMENTS
+    assert invalid.error.startswith("invalid event arguments:")
+
+
+@pytest.mark.asyncio
+async def test_kernel_normalizes_validator_runtime_exceptions():
+    definition = _definition(parameters={"$ref": "urn:agent-lab:missing-schema"})
+
+    result = await EventKernel(EventRegistry([definition])).execute(
+        EventRequest(
+            id="event-1",
+            name=definition.name,
+            arguments={},
+            source=EventSource.PROVIDER_RESOLVED,
+        ),
+        enabled_names=[definition.name],
+    )
+
+    assert result.status is EventStatus.INVALID_ARGUMENTS
+    assert result.error.startswith("event argument validation failed:")
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("boundary", ["resolver", "fallback"])
+async def test_kernel_normalizes_resolution_boundary_exceptions(boundary: str):
+    def resolver(request: EventRequest, context: EventExecutionContext) -> dict[str, Any]:
+        if boundary == "resolver":
+            raise RuntimeError("resolver boom")
+        return {}
+
+    async def fallback(*args: Any) -> ResolvedEventArguments:
+        raise RuntimeError("fallback boom")
+
+    result = await EventKernel(
+        EventRegistry([_definition(resolver=resolver)]),
+        argument_fallback=fallback,
+    ).execute(
+        EventRequest(id="event-1", name="example.lookup"),
+        enabled_names=["example.lookup"],
+    )
+
+    assert result.status is EventStatus.RESOLUTION_ERROR
+    assert result.error == f"event argument {boundary} failed: {boundary} boom"
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("boundary", ["resolver", "fallback"])
+async def test_kernel_normalizes_invalid_resolution_payloads(boundary: str):
+    resolver = (
+        (lambda request, context: None)
+        if boundary == "resolver"
+        else (lambda request, context: {})
+    )
+
+    async def fallback(*args: Any) -> Any:
+        return {"query": "legacy bare mapping"}
+
+    result = await EventKernel(
+        EventRegistry([_definition(resolver=resolver)]),
+        argument_fallback=fallback,
+    ).execute(
+        EventRequest(id="event-1", name="example.lookup"),
+        enabled_names=["example.lookup"],
+    )
+
+    assert result.status is EventStatus.RESOLUTION_ERROR
+    assert result.error == f"event argument {boundary} returned invalid payload"
+
+
+@pytest.mark.asyncio
+async def test_kernel_normalizes_non_json_resolver_arguments():
+    result = await EventKernel(
+        EventRegistry(
+            [
+                _definition(
+                    resolver=lambda request, context: {"query": object()}
+                )
+            ]
+        )
+    ).execute(
+        EventRequest(id="event-1", name="example.lookup"),
+        enabled_names=["example.lookup"],
+    )
+
+    assert result.status is EventStatus.RESOLUTION_ERROR
+    assert result.error.startswith("event argument resolver failed to serialize:")
+
+
+@pytest.mark.asyncio
+async def test_kernel_normalizes_non_object_fallback_arguments():
+    async def fallback(*args: Any) -> ResolvedEventArguments:
+        return ResolvedEventArguments(
+            event_name="example.lookup",
+            arguments=["not", "an", "object"],  # type: ignore[arg-type]
+            raw_arguments='["not","an","object"]',
+        )
+
+    result = await EventKernel(
+        EventRegistry([_definition(resolver=lambda request, context: {})]),
+        argument_fallback=fallback,
+    ).execute(
+        EventRequest(id="event-1", name="example.lookup"),
+        enabled_names=["example.lookup"],
+    )
+
+    assert result.status is EventStatus.RESOLUTION_ERROR
+    assert result.error == "event argument fallback returned invalid payload"
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+    ("resolved", "expected_error"),
+    [
+        (
+            ResolvedEventArguments(
+                event_name="another.event",
+                arguments={"query": "value"},
+                raw_arguments='{"query":"value"}',
+            ),
+            "fallback returned tool another.event for example.lookup",
+        ),
+        (
+            ResolvedEventArguments(
+                event_name="example.lookup",
+                arguments={"query": "value"},
+                raw_arguments="not-json",
+            ),
+            "fallback raw arguments are not valid JSON",
+        ),
+        (
+            ResolvedEventArguments(
+                event_name="example.lookup",
+                arguments={"query": "parsed"},
+                raw_arguments='{"query":"raw"}',
+            ),
+            "fallback raw arguments do not match parsed arguments",
+        ),
+    ],
+)
+async def test_kernel_rejects_inconsistent_structured_fallback(
+    resolved: ResolvedEventArguments,
+    expected_error: str,
+):
+    async def fallback(*args: Any) -> ResolvedEventArguments:
+        return resolved
+
+    result = await EventKernel(
+        EventRegistry([_definition(resolver=lambda request, context: {})]),
+        argument_fallback=fallback,
+    ).execute(
+        EventRequest(id="event-1", name="example.lookup"),
+        enabled_names=["example.lookup"],
+    )
+
+    assert result.status is EventStatus.RESOLUTION_ERROR
+    assert result.error == expected_error
+
+
+@pytest.mark.asyncio
+async def test_kernel_passes_consistent_fallback_arguments_and_raw_json_to_handler():
+    captured: list[EventRequest] = []
+
+    async def fallback(*args: Any) -> ResolvedEventArguments:
+        return ResolvedEventArguments(
+            event_name="example.lookup",
+            arguments={"query": "resolved"},
+            raw_arguments='{"query":"resolved"}',
+        )
+
+    result = await EventKernel(
+        EventRegistry(
+            [
+                _definition(
+                    resolver=lambda request, context: {},
+                    handler=lambda request: captured.append(request) or {"ok": True},
+                )
+            ]
+        ),
+        argument_fallback=fallback,
+    ).execute(
+        EventRequest(id="event-1", name="example.lookup"),
+        enabled_names=["example.lookup"],
+    )
+
+    assert result.status is EventStatus.SUCCESS
+    assert captured[0].arguments == json.loads(captured[0].raw_arguments)
+    assert result.raw_arguments == captured[0].raw_arguments
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("payload", [None, "text", 1, ["item"]])
+async def test_kernel_normalizes_invalid_handler_payloads(payload: Any):
+    result = await EventKernel(
+        EventRegistry([_definition(handler=lambda request: payload)])
+    ).execute(
+        EventRequest(
+            id="event-1",
+            name="example.lookup",
+            arguments={"query": "value"},
+            source=EventSource.PROVIDER_RESOLVED,
+        ),
+        enabled_names=["example.lookup"],
+    )
+
+    assert result.status is EventStatus.HANDLER_ERROR
+    assert result.error == "event handler returned non-object payload"
+
+
+@pytest.mark.asyncio
+async def test_async_kernel_supports_async_handler():
+    async def handler(request: EventRequest) -> dict[str, Any]:
+        return {"query": request.arguments["query"]}
+
+    result = await EventKernel(
+        EventRegistry([_definition(handler=handler)])
+    ).execute(
+        EventRequest(
+            id="event-1",
+            name="example.lookup",
+            arguments={"query": "async"},
+            source=EventSource.PROVIDER_RESOLVED,
+        ),
+        enabled_names=["example.lookup"],
+    )
+
+    assert result.status is EventStatus.SUCCESS
+    assert result.payload == {"query": "async"}
+
+
+@pytest.mark.asyncio
+async def test_tool_registry_async_entry_points_support_async_handler():
+    async def handler(event: ToolCallEvent) -> dict[str, Any]:
+        return {"tool": event.name, "query": event.arguments["query"]}
+
+    registry = ToolRegistry(
+        [
+            ToolDefinition(
+                name="async.lookup",
+                description="Async lookup.",
+                parameters={
+                    "type": "object",
+                    "properties": {"query": {"type": "string"}},
+                    "required": ["query"],
+                },
+                handler=handler,
+            )
+        ]
+    )
+    event = ToolCallEvent(
+        id="call-1",
+        name="async.lookup",
+        arguments={"query": "value"},
+        raw_arguments='{"query":"value"}',
+    )
+
+    assert await registry.handle_async(event) == {
+        "tool": "async.lookup",
+        "query": "value",
+    }
+    assert await registry.execute_async(event) == {
+        "tool": "async.lookup",
+        "query": "value",
+    }
+
+
+def test_tool_registry_sync_facade_rejects_async_handler_without_runtime_warning():
+    called = False
+
+    async def handler(event: ToolCallEvent) -> dict[str, Any]:
+        nonlocal called
+        called = True
+        return {"tool": event.name}
+
+    registry = ToolRegistry(
+        [
+            ToolDefinition(
+                name="async.lookup",
+                description="Async lookup.",
+                parameters={"type": "object"},
+                handler=handler,
+            )
+        ]
+    )
+    event = ToolCallEvent(
+        id="call-1",
+        name="async.lookup",
+        arguments={},
+        raw_arguments="{}",
+    )
+
+    with warnings.catch_warnings(record=True) as captured:
+        warnings.simplefilter("always")
+        payload = registry.execute(event)
+        gc.collect()
+
+    assert payload == {
+        "tool": "async.lookup",
+        "error": "tool handler failed: async event handlers require execute_async",
+    }
+    assert called is False
+    assert not [warning for warning in captured if issubclass(warning.category, RuntimeWarning)]
+
+
 @pytest.mark.asyncio
 async def test_kernel_rejects_boolean_for_json_number_arguments():
     definition = _definition(
@@ -391,11 +775,20 @@ async def test_definition_metadata_survives_registration_and_result_creation():
     assert result.terminal is True
 
 
-def test_kernel_source_has_no_builtin_event_name_branches():
-    from pathlib import Path
+@pytest.mark.asyncio
+@pytest.mark.parametrize("name", ["alpha.one", "beta-two", "任意.事件"])
+async def test_kernel_applies_identical_behavior_to_arbitrary_event_names(name: str):
+    definition = _definition(name=name)
 
-    source = Path("src/agent_lab/application/events/kernel.py").read_text()
+    result = await EventKernel(EventRegistry([definition])).execute(
+        EventRequest(
+            id="event-1",
+            name=name,
+            arguments={"query": "value"},
+            source=EventSource.PROVIDER_RESOLVED,
+        ),
+        enabled_names=[name],
+    )
 
-    assert "handoff_note" not in source
-    assert "mock_search" not in source
-    assert "mock_ticket" not in source
+    assert result.status is EventStatus.SUCCESS
+    assert result.payload == {"event": name, "query": "value"}

+ 175 - 0
uv.lock

@@ -9,6 +9,7 @@ source = { editable = "." }
 dependencies = [
     { name = "fastapi" },
     { name = "httpx" },
+    { name = "jsonschema" },
     { name = "pydantic-settings" },
     { name = "uvicorn", extra = ["standard"] },
     { name = "websockets" },
@@ -24,6 +25,7 @@ dev = [
 requires-dist = [
     { name = "fastapi", specifier = ">=0.111.0" },
     { name = "httpx", specifier = ">=0.27.0" },
+    { name = "jsonschema", specifier = ">=4.26.0" },
     { name = "pydantic-settings", specifier = ">=2.3.0" },
     { name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0" },
     { name = "websockets", specifier = ">=12.0" },
@@ -66,6 +68,15 @@ wheels = [
     { url = "https://mirrors.aliyun.com/pypi/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72" },
 ]
 
+[[package]]
+name = "attrs"
+version = "26.1.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309" },
+]
+
 [[package]]
 name = "certifi"
 version = "2026.6.17"
@@ -210,6 +221,33 @@ wheels = [
     { url = "https://mirrors.aliyun.com/pypi/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" },
 ]
 
+[[package]]
+name = "jsonschema"
+version = "4.26.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+    { name = "attrs" },
+    { name = "jsonschema-specifications" },
+    { name = "referencing" },
+    { name = "rpds-py" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+    { name = "referencing" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe" },
+]
+
 [[package]]
 name = "packaging"
 version = "26.2"
@@ -461,6 +499,143 @@ wheels = [
     { url = "https://mirrors.aliyun.com/pypi/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b" },
 ]
 
+[[package]]
+name = "referencing"
+version = "0.37.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+dependencies = [
+    { name = "attrs" },
+    { name = "rpds-py" },
+    { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "2026.6.3"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4" }
+wheels = [
+    { url = "https://mirrors.aliyun.com/pypi/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76" },
+    { url = "https://mirrors.aliyun.com/pypi/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826" },
+]
+
 [[package]]
 name = "starlette"
 version = "1.3.1"